straw/rust
Kayos c960a1f424 vc=68: audit-fix sprint round 1 (11 HIGH + MED batch)
Block B — enrichment lifecycle drift:
  * SubscriptionFeedViewModel tracks enrichJob, cancelled in refresh
    + clearInMemoryCache so spam-refresh and cache-toggle no longer
    leave a globalScope coroutine writing to a destroyed _ui
  * Enrich now runs on viewModelScope, channels snapshotted at job
    start so the terminal merge doesn't read a stale subs list
  * mergeFromCache moved off Main on both the refresh path AND the
    init-hydration path — 750-item flatMap+sort+regex no longer
    blocks the UI thread
  * VideoDetailViewModel dual loadedUrl bookkeeping collapsed to
    the UiState field only; the rejected-URL path also stamps
    loadedUrl so the gate reads coherently

Block A — auto-update authenticity:
  * AppUpdateClient pins the fdroid.sulkta.com leaf SPKI + the
    Let's Encrypt E7 intermediate via OkHttp CertificatePinner
  * file.name accepted only when matching ^/[A-Za-z0-9._-]+\.apk$
  * versionCode clamped to (0, 10_000_000] before we trust the
    'update available' notification — a hostile index can no longer
    pin us to MAX_VALUE

Block C — captureResumePosition perf:
  * ResumePositionsStore.record short-circuits when the existing
    entry matches position+duration so the 5s poll's
    before !== next guard actually skips the SP write
  * JSON encode + SP write off Main via globalScope IO

Block D — Rust feed.rs hardening:
  * Shared reqwest Client via OnceLock — 50 channels no longer
    pay 50 TLS handshakes
  * Response body capped at 2 MiB via bytes_stream — adversarial
    feeds can't OOM the JVM
  * parse_rss returns partial results on quick-xml errors instead
    of nuking everything already parsed
  * extract_channel_id widened (m./www./http(s)?/trailing path)
    and validates exact 24-char UC<22 base64-ish>
  * Skip entries with empty title/published
  * iso_to_relative future dates → 'just now' (clock skew
    no longer pins items to top)
  * civil_to_days year clamp 1970..=2200 before the i64 arithmetic
  * Redirect chain capped at 3
  * Dropped the broken lexicographic sort on upload_date_relative
  * Cap parsed entries at 50 per channel

MED batch:
  * ThumbnailProgressOverlay uses derivedStateOf so only rows
    whose specific entry changed recompose on the 5s positions tick
  * EnrichmentStore.put short-circuits on identical view+duration
    so re-enrich within TTL doesn't write SP
  * EnrichmentStore.load prunes TTL-expired entries on hydration
  * FeedRefreshWorker distinguishes transient (Result.retry) from
    parse (Result.success) failures
  * WorkManager interval coerceAtLeast(15L) on both schedulers
2026-05-26 20:53:25 -07:00
..
strawcore vc=68: audit-fix sprint round 1 (11 HIGH + MED batch) 2026-05-26 20:53:25 -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.