// 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=` 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 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 }, } } }