straw/strawApp
Kayos e76a325faa vc=35: audit-fix sprint — 5 CRIT + 14 HIGH + opportunistic MEDs
Three Opus max-effort audits (CVE/security, code-health, function-
correctness) on the vc=34 surface returned a consolidated punch list
of 5 CRIT + 14 HIGH + ~10 MED. This commit lands all CRIT + HIGH +
the cheap MEDs in one cohesive pass.

CRIT — privacy + main-thread blocks
  S1  IosSafeHttpDataSource was logging full pre-signed googlevideo
      URLs (signature/sig/pot/expire/cpn) via raw android.util.Log.i
      (no DEBUG gate). Then LogDump.capture would scrape its own PID's
      logcat and ship it via the share sheet — a "report a bug to
      Telegram" silently exfiltrated session credentials. Fixed by
      switching all log calls to strawLogD/strawLogW (gated on
      BuildConfig.DEBUG), dropping the full-URL log entirely, and
      adding a regex scrub pass in LogDump for googlevideo URLs +
      signed-param keys before the file hits disk.
  S2  Downloader.enqueue handed signed googlevideo URLs to the system
      DownloadManager, where they leak into DM's SQLite, logcat, the
      system notification, and apps holding ACCESS_DOWNLOAD_MANAGER.
      Set VISIBILITY_HIDDEN + setVisibleInDownloadsUi(false) so the
      URL never surfaces in any external surface. Added the
      DOWNLOAD_WITHOUT_NOTIFICATION permission DM requires.
  S3  SettingsImport extracted the user's full newpipe.db (every sub,
      watch, search) into cacheDir, deleted on finally. A force-kill
      mid-import left the DB on disk indefinitely. Wrapped cleanup in
      withContext(NonCancellable), switched workDir to createTempFile
      (unguessable name), and added StrawApp.onCreate sweep of stale
      newpipe-import-* dirs on every cold start.
  C1  SearchViewModel.reactiveFilter ran a fresh SharedPreferences.
      getString + Json.decodeFromString on FeedCache (~225 KB) AND
      SearchCache (~150 KB) on EVERY keystroke. Hoisted into a
      MutableStateFlow<List<StreamItem>> pool, built once on
      Dispatchers.IO at VM init and refreshed after each successful
      submit. Reactive filter now walks an in-memory list.
  C2  SubscriptionFeedViewModel.init did the same FeedCache.load()
      synchronously on the main thread at construction (first compose
      pass blocked on the JSON decode). Moved into
      viewModelScope.launch + withContext(Dispatchers.IO).

HIGH — function correctness + defense in depth
  B1+B2  SearchScreen when-branch order: loading + error short-
         circuited before the results branch, hiding the cached
         preview the VM explicitly kept visible on cache-hit + on
         network failure. Refactored to render the cached list under
         a thin progress bar / error banner instead.
  B3   Downloads "tap completed row" silently failed since minSdk 24
       (FileUriExposedException on the file:// URI). Route through
       FileProvider with new file_paths.xml entries for
       Movies/audio + Movies/video.
  B6   Manifest VIEW intent-filter was missing music.youtube.com and
       youtube-nocookie.com hosts even though YT_HOSTS allowed them
       — added both.
  B7   SponsorBlockSkipLoop fired one Toast per skip with no rate
       limit; sponsor-dense videos painted 20+ Toasts over 40s after
       the seeks completed. 3s rate limit per cur.streamUrl.
  Q8   resolvePlayback.pickVideo fallback used maxByOrNull when the
       comment said "lowest available" — a 480p-capped user on a
       1080p-only upload got 1080p (their data cap blown). Switched
       to minByOrNull { height } when nothing fits the cap.
  S1   SettingsImport extractZip had no size or entry-count caps —
       zip-bomb could fill cacheDir. Added MAX_DB_BYTES (256 MB),
       MAX_PREFS_BYTES (1 MB), MAX_ZIP_ENTRIES (64). copyBounded /
       readBoundedBytes helpers replace the unbounded copyTo /
       readBytes.
  S2   Manifest: android:allowBackup=false +
       android:dataExtractionRules=@xml/data_extraction_rules.xml +
       android:fullBackupContent=false. Excludes root/file/database/
       sharedpref/external from both cloud-backup and device-transfer
       so the user's full search + watch history doesn't ride to
       Google Drive.
  S3   Dropped isLenient = true from every Json {} instance (7 sites
       across data/ and net/). Lenient parser was buying nothing on
       data we wrote ourselves and was a hardening gap on the third-
       party SponsorBlock + RYD endpoints (community-run; malformed
       payload could feed bad timestamps into the skip loop).
  S4   SubscriptionsStore.addAll + HistoryStore.recordAllWatches bulk
       methods, used by SettingsImport. Per-row toggle was O(N²) +
       N SP writes; bulk path is O(N) + 1 write.
  C3   SubsPane infinite-scroll LaunchedEffect keyed on
       (displayed.size, hasMore) — both mutated BY the effect. The
       collector cancelled itself mid-stream and dropped emissions,
       producing "scroll to bottom, nothing more loads". Re-keyed on
       listState + filteredCount; the collect lambda reads state
       through the snapshotFlow producer to avoid stale captures.
  C7   liveDrag + playbackSpeed: mutableFloatStateOf instead of
       mutableStateOf — no Float boxing on the 100Hz drag callback.
  C8   LogDump.capture is now suspend on Dispatchers.IO. The Settings
       click handler launches into scope; button shows "Exporting…"
       while in flight.

MED — cheap wins picked up in passing
  Q9   reactiveFilter clears the cached preview when current query no
       longer has any matches (was leaving stale results visible).
  Q10  Hide-watched filter excludes blank video IDs from watchedIds
       — a blank in the set used to match every malformed-URL feed
       item and silently hide them.
  Q13  PiP button in VideoDetail bails with a Toast on null
       controller OR null resolved playback (was falling through to
       enterPictureInPictureMode with no stream).
  C17  SpeedPickerDialog row used fillMaxSize inside an AlertDialog;
       only the first row got non-zero height. Fixed to fillMaxWidth.

Deferred to vc=36 follow-up (touch surface area we don't want to
churn in the same ship):
  - C6 atomic setPlayingFrom guard in StrawMediaController
  - S3 (full) — direct-streaming download replacing DownloadManager
  - MED-C16 LazyColumn refactor in VideoDetailScreen
  - MED-Q12 loadedUrl assignment ordering hardening
2026-05-25 13:27:30 -07:00
..
src/main vc=35: audit-fix sprint — 5 CRIT + 14 HIGH + opportunistic MEDs 2026-05-25 13:27:30 -07:00
build.gradle.kts vc=26: look + feel pass — sulkta.com palette + Material Icons 2026-05-25 17:46:23 +00:00