straw/rust
Kayos da48109a4d vc=40: loop round 2/5 — round-1 misses + new HIGHs from round-5 audits
Three parallel Opus round-5 audits ran on vc=39. The big finds were
regressions on vc=39's own fixes — the retry-init wiring was incomplete,
the URL-fence pattern missed CancellationException swallowing, and the
SettingsStore idempotency branch was dead code. Real round-5 work.

HIGH
  R5-1  Rust `ensure_initialized` was only called from `init_logging`
        — extractor entry points never invoked it, so the 5s-backoff
        retry from vc=39 was unreachable. A cold-boot init failure
        still bricked extraction for the whole process. Now every
        `search()` / `stream_info()` / `channel_info()` calls
        ensure_initialized at entry; cheap when INITIALIZED is true.
  R5-2  runtime.rs in-progress race: prior shape stamped
        LAST_ATTEMPT_MS at the *start* of an attempt, then let
        concurrent callers short-circuit via the backoff check
        and proceed to call into the extractor before init
        completed. New: lock FIRST (mutex IS the in-progress
        queue), re-check INITIALIZED under lock, only THEN check
        backoff (and only stamp it on failure).
  R5-3  `runCatching` in VideoDetailViewModel + SubscriptionFeed
        swallows CancellationException — when a job was cancelled
        mid-suspend inside channelInfo/RYD/SB, the inner cancellation
        was eaten, the lambda returned its default, the job carried
        past the runCatching to its terminal write, and the URL fence
        let it through because same-URL races can't be distinguished
        by string equality. New util.runCatchingCancellable
        re-throws CancellationException; all coroutine-body
        runCatching sites in the affected VMs migrated.
  R5-4  SearchViewModel.submit post-network fence only guarded
        `_ui.update`. SearchCache.record + pool rebuild proceeded
        for a cancelled query → ghost cache entries for queries
        the user abandoned mid-stroke. Now: re-check the query
        AFTER the IO suspend and before the cache write.
  R5-5  ChannelViewModel.load / VideoDetailViewModel.load now gate
        the extractor entry on isAllowedYtUrl(channelUrl/streamUrl).
        Prior shape only gated recordWatch persistence — extractor
        invocation for poisoned uploaderUrl still happened.

MED
  R5-6  SettingsStore.set{MaxResolution,ThemeMode}: vc=39 used
        `updateAndGet { r } == r` which is unconditionally true
        (lambda ignores prior) — the in-memory equality check was
        dead code. SP-side check still gated the disk write so the
        feature worked, but the dead branch was a comment-vs-code
        liar. Rewrote with explicit before-capture + equality
        gate.
  R5-7  SponsorBlockSkipLoop polled `controller.currentPosition`
        every 150ms even when paused — paused-overnight playback
        ate ~24k binder calls/hour. Now: when `!isPlaying`, sleep
        1s and continue.
  R5-8  StrawApp.appScope had no CoroutineExceptionHandler — an
        uncaught throwable in a top-level launch could crash on
        cold start even with SupervisorJob (top-level failure
        still propagates to default handler). Added handler that
        logs via strawLogW.
  R5-9  YtUrl.isAllowedYtUrl now requires http/https scheme
        (schemeless `//host/...` URLs no longer pass) and strips
        a single trailing dot from host (RFC FQDN form). Defense
        in depth.

LOW
  R5-10 NowPlaying.set() removed — non-CAS setter footgun
        alongside the race-free claim()/CAS path. No external
        callers (grep clean). Doc-comment updated.

Deferred (later round / different scope):
  - PlaylistsStore URL canonicalization (round-5 MED-1 — needs a
    shared YT-id-extract util; not blocking).
  - Release R8 + Nav rememberSaveable (still deferred).
  - LazyColumn keys + collectAsStateWithLifecycle (cosmetic).
  - SponsorBlockSkipLoop currentMediaItem binding (round-5
    deferred).
2026-05-25 15:12:30 -07:00
..
strawcore vc=40: loop round 2/5 — round-1 misses + new HIGHs from round-5 audits 2026-05-25 15:12:30 -07:00
Cargo.toml v0.1.0-V (vc=9): U-3 — streamInfo via rustypipe drives VideoDetail+Player 2026-05-24 08:52:43 -07:00
README.md v0.1.0-U (vc=8): Phase U-1 + U-2 — Rust core + rustypipe search 2026-05-24 08:36:50 -07:00

rust/ — strawcore: Rust YouTube core for Straw

Phase U- of the Straw build. Goal: replace the Java NewPipeExtractor dependency with a Rust core (rustypipe + tokio + reqwest), exposed to the Kotlin/Compose UI via UniFFI. Compose UI stays in Kotlin — only the YouTube/Innertube fetching layer moves to Rust.

Phases

Phase What
U-1 Toolchain + UniFFI smoke test (hello_from_rust) round-tripping through JNA. No real APIs yet.
U-2 rustypipe search. SearchViewModel calls the Rust core.
U-3 rustypipe streamInfo + streams. VideoDetailViewModel + PlayerViewModel use it.
U-4 rustypipe channel + tabs. ChannelViewModel + SubscriptionFeedViewModel.
U-5 Rip NewPipeExtractor Java dep. Measure APK + cold-fetch latency before/after.
U-6 (stretch) SponsorBlock + RYD HTTP through reqwest + tokio in the same lib.

Build chain

crafting-table
├── rustup stable (target add: aarch64-linux-android, armv7-linux-androideabi,
│                  x86_64-linux-android, i686-linux-android)
├── cargo-ndk      (cross-compile helper)
├── android-sdk    (ANDROID_HOME, sdkmanager, build-tools, platforms)
└── android-ndk    (ANDROID_NDK_HOME, r27c LTS at /caches/android-sdk/ndk/...)

Gradle (strawApp/build.gradle.kts)
├── cargoBuild         Exec task → cargo ndk -t <abi>... -o jniLibs/ build --release
├── uniffiBindgen      Exec task → cargo run --bin uniffi-bindgen ... --library libstrawcore.so
└── source-set wiring  generated Kotlin lands in strawApp/src/main/java/uniffi/strawcore/

Runtime (StrawApp.onCreate)
├── System.loadLibrary("strawcore")
└── uniffi.strawcore.initLogging()

Why UniFFI (and not raw JNI / JNA bindings)

  • Hand-written JNI: tedious, error-prone, every type change is two files (Kotlin + Rust) that must stay in sync.
  • Raw JNA: better, but you still hand-write the Kotlin side and worry about string ownership.
  • UniFFI: write Rust, annotate with #[uniffi::export], get a Kotlin shim generated. Strings, structs, enums, Result types, async functions all cross the boundary transparently. The runtime is JNA under the hood.

When in doubt

  • cargo check -p strawcore --target aarch64-linux-android — fast iteration.
  • cargo run --bin uniffi-bindgen -- generate ... — regenerate Kotlin bindings.
  • adb logcat -s strawcore — Rust log::info!() lands here.
  • aapt dump badging strawApp/build/outputs/apk/debug/strawApp-debug.apk — inspect what ABIs/native-libs the APK carries.