straw/rust/strawcore/src/error.rs
Sulkta 1ba4450b99 Public-flip audit: scrub audit-ticket prefixes + internal references + tighten README
URLs → git.sulkta.com. Audit-ticket prefixes (SPEC §N, audit Track X, vc=N
audit-fix, FIX (audit ...), PORT DEVIATION) stripped from comments — technical
reasoning retained. Build-host references softened.
README sheds marketing scaffolding + stale status tables.
2026-05-27 13:29:53 -07:00

101 lines
3.8 KiB
Rust

// Strawcore error type — anything that can fail in our Rust core surfaces
// as one of these variants. UniFFI maps it to a Kotlin sealed class
// `StrawcoreException` so Kotlin can pattern-match on the kind.
use thiserror::Error;
#[derive(Debug, Error, uniffi::Error)]
pub enum StrawcoreError {
#[error("network: {msg}")]
Network { msg: String },
#[error("extractor: {msg}")]
Extractor { msg: String },
#[error("not found: {what}")]
NotFound { what: String },
#[error("unsupported: {detail}")]
Unsupported { detail: String },
#[error("age restricted")]
AgeRestricted,
#[error("geo restricted")]
GeoRestricted,
#[error("private content")]
Private,
#[error("requires login: {detail}")]
RequiresLogin { detail: String },
}
/// Drop the `continue=<signed-url>` param from a google.com/sorry/...
/// URL while leaving every other param intact. Used only for surfacing
/// recaptcha challenge URLs to the UI; keeps the URL tappable for the
/// user to solve the challenge while scrubbing the embedded
/// googlevideo signature.
fn strip_continue_param(url: &str) -> String {
let (base, query) = match url.split_once('?') {
Some(pair) => pair,
None => return url.to_owned(),
};
let filtered: Vec<&str> = query
.split('&')
.filter(|kv| {
let key = kv.split_once('=').map(|(k, _)| k).unwrap_or(*kv);
!key.eq_ignore_ascii_case("continue")
})
.collect();
if filtered.is_empty() {
base.to_owned()
} else {
format!("{}?{}", base, filtered.join("&"))
}
}
impl From<strawcore_core::exceptions::ExtractionError> for StrawcoreError {
fn from(e: strawcore_core::exceptions::ExtractionError) -> Self {
use strawcore_core::exceptions::{ContentUnavailable, ExtractionError, NetworkError};
match e {
ExtractionError::Network(NetworkError::Recaptcha { url }) => {
// Strip the `continue=` query param before propagating.
// google.com/sorry/index carries the full signed
// googlevideo URL in `continue=` so the user can be
// sent back to the stream after solving — but
// surfacing that to the UI is a credential leak via
// screenshot, and Kotlin's LogDump scrubber only
// catches googlevideo.com hosts. The challenge URL
// itself still solves without `continue=`, so the
// user can tap to unblock without leaking the
// signature/expire/pot token.
StrawcoreError::RequiresLogin {
detail: format!("reCAPTCHA challenge: {}", strip_continue_param(&url)),
}
}
ExtractionError::Network(NetworkError::Transport(msg)) => {
StrawcoreError::Network { msg }
}
ExtractionError::Parsing(p) => StrawcoreError::Extractor {
msg: p.to_string(),
},
ExtractionError::ContentUnavailable(ContentUnavailable::AgeRestricted) => {
StrawcoreError::AgeRestricted
}
ExtractionError::ContentUnavailable(ContentUnavailable::GeoRestricted) => {
StrawcoreError::GeoRestricted
}
ExtractionError::ContentUnavailable(ContentUnavailable::Private) => {
StrawcoreError::Private
}
ExtractionError::ContentUnavailable(other) => StrawcoreError::Extractor {
msg: other.to_string(),
},
ExtractionError::DownloaderMissing => StrawcoreError::Extractor {
msg: "downloader not initialized".into(),
},
ExtractionError::Other(msg) => StrawcoreError::Extractor { msg },
}
}
}