Three round-3 Opus audits ran on vc=37. NO new CRITs (round-2 work
held) but real new HIGHs — several were vc=37 own-goals.
HIGH
R3-1 recordAllWatches dropped import on capacity=0. Old: when
watches store hit MAX_WATCHES (50), capacity=0, the whole
import was discarded silently. New: build fresh import list
capped at MAX_WATCHES, then combine + take(MAX_WATCHES) so
imports always land (truncating oldest current entries).
Also: skip SP write when next === before (no-op import on
already-saturated store no longer thrashes disk).
New recordAllSearches with same shape — round-3 CVE MED-6:
importHistory was per-row recordSearch.
R3-2 / CVE-2 SubscriptionsStore.addAll counter race. The vc=36
size-delta fix snapshot `cur = _subs.value` BEFORE
updateAndGet, so a concurrent toggle inflated `added`. New:
AtomicInteger reset at the start of each lambda re-run,
counted by checking each ref against the pre-image inside
the CAS. Exactly the additions THIS call made.
R3-3 refresh() empty-channels didn't cancel inFlight. Cancel
moved to the top of refresh() unconditionally so a refresh
on the prior sub set is killed before the empty branch
clears + wipes disk.
clearInMemoryCache also cancels inFlight — without it, a
cache-disable flip during a refresh could see fetchChannelInto
re-populate the just-cleared map.
R3-4 Non-atomic `_ui.value = it.copy(...)` at init hydrate path
and clearInMemoryCache. Replaced with `_ui.update {}` for
atomicity vs concurrent refresh writes. init's
lastFetchedAt write now uses maxOf so it never regresses
past a fresh refresh value.
CVE-1 state.error rendered raw UniFFI/Rust error strings to UI
— NetworkError::Recaptcha { url } embeds full signed
googlevideo URL. User screenshots a "reCAPTCHA at <URL>"
banner → leak. All four VMs (Channel/Detail/Feed/Search)
now scrub via LogDump.scrubLine before storing.
CVE-3 pruneCacheToSubs in init can clobber concurrent
fetchChannelInto writes. init's putAll → putIfAbsent so
a fresh entry from a parallel refresh isn't overwritten
with disk-stale data.
CVE-4 SIGNED_PARAM_RE over-redacted short tokens (`\bn=`
matched `n=42` counters from any wrapped lib). Split into
SIGNED_PARAM_LONG_RE (signature/sparams/lsig/cpn/expire/
pot/sig/key — match anywhere) and SIGNED_PARAM_SHORT_RE
(n/mn/ms/mo/pl/ip/ei — require `[?&]` immediately before).
Func-HIGH-1 refresh() swallowed CancellationException as a
user-visible error. Spam-tapping Refresh produced a
"refresh failed: StandaloneCoroutineCancelled" banner.
Re-throw CancellationException; catch only real errors.
MED
R3-5 reactiveFilter did N `.lowercase()` allocations per
keystroke. Switched to contains(ignoreCase = true) — zero
allocations.
CVE-MED-5 FileProvider cache-path was "." (whole cacheDir,
including SettingsImport workdirs). Narrowed to "logs/";
LogDump.capture now writes to cacheDir/logs/ to match.
CVE-MED-7 Downloader.Request.setTitle was the raw title
(bidi-override / control chars possible). Switched to
safeTitle.
CVE-MED-8 Rust hello_from_rust value-log scrubbed to name_len.
Func-LOW-4 recordAllWatches skip-write-on-no-change (`next !==
before`).
Deferred to a follow-up (not user-facing this ship):
R3-MED-6 — Settings setMaxResolution/setThemeMode/setCacheEnabled
not atomic via updateAndGet. Inconsistent with toggle()
but the Switch UI throttles enough that no real race.
R3-MED-8 — Minibar play-button reads live controller.isPlaying
instead of listener-tracked. One-frame oscillation on
super-fast double-tap.
R3-LOW — collectAsState vs collectAsStateWithLifecycle drift.
Func-LOW-6 — refreshIfStale isActive check is TOCTOU on a
non-existent multi-threaded call surface (LaunchedEffect
+ button are both Main).
55 lines
1.9 KiB
Rust
55 lines
1.9 KiB
Rust
// strawcore (wrapper) — UniFFI surface for the Straw Android app.
|
|
//
|
|
// Thin layer over the new Sulkta-Coop/strawcore-core crate. All extractor
|
|
// logic (InnerTube, JS deobf, stream parsing, search, channel, playlist)
|
|
// lives in core. This file:
|
|
// * re-exports the DTOs Kotlin expects under their familiar names
|
|
// * exposes #[uniffi::export] async fns that bridge Kotlin suspend funs
|
|
// to the core's blocking calls via tokio::task::spawn_blocking
|
|
// * owns init_logging() — also initializes the core Downloader
|
|
|
|
use std::sync::Once;
|
|
|
|
mod channel;
|
|
mod error;
|
|
mod runtime;
|
|
mod search;
|
|
mod stream;
|
|
|
|
// Re-exports so UniFFI sees the types at the crate root for macro discovery.
|
|
pub use channel::ChannelInfo;
|
|
pub use error::StrawcoreError;
|
|
pub use search::SearchItem;
|
|
pub use stream::{AudioStreamItem, StreamInfo, VideoStreamItem};
|
|
|
|
/// Initialize Android logging + the strawcore-core HTTP downloader.
|
|
/// Kotlin calls this from StrawApp.onCreate(). Idempotent.
|
|
#[uniffi::export]
|
|
pub fn init_logging() {
|
|
static ONCE: Once = Once::new();
|
|
ONCE.call_once(|| {
|
|
android_logger::init_once(
|
|
android_logger::Config::default()
|
|
.with_max_level(log::LevelFilter::Info)
|
|
.with_tag("strawcore"),
|
|
);
|
|
log::info!("strawcore initialized");
|
|
});
|
|
runtime::ensure_initialized();
|
|
}
|
|
|
|
/// Smoke-test entry point — round-trip a string through JNI.
|
|
/// Used during the initial UniFFI bring-up; kept for future smoke
|
|
/// debugging. Logs shape only — the `name` value never hits logcat
|
|
/// because a future caller might pass a real user-supplied string.
|
|
#[uniffi::export]
|
|
pub fn hello_from_rust(name: String) -> String {
|
|
log::info!("hello_from_rust called name_len={}", name.len());
|
|
format!(
|
|
"hello {} from rust 🦀 (strawcore v{})",
|
|
name,
|
|
env!("CARGO_PKG_VERSION")
|
|
)
|
|
}
|
|
|
|
uniffi::setup_scaffolding!();
|