Skip to content

Land F-05/F-08/F-09/F-12 + SnapshotSettings field-drop onto main - #340

Merged
RicherTunes merged 9 commits into
mainfrom
chore/land-tidal-f-fixes-to-main
Jun 5, 2026
Merged

RicherTunes merged 9 commits into
mainfrom
chore/land-tidal-f-fixes-to-main

Conversation

@RicherTunes

Copy link
Copy Markdown
Owner

Lands the adversarial-review fixes that were validated on chore/converge-common-ed3c27b onto main (squash-merge for linear history).

  • F-05: Common pin converged to 961dfae (PR #618 security batch — RemoteMediaUriGuard, SafeDirectoryCleanup, FileTokenStore zeroization)
  • F-08: enhanced track assembly streams to a temp file instead of buffering the whole track in a MemoryStream
  • F-09: download output paths root-contained via Common PathTraversalGuard (refuses writes outside DownloadPath)
  • F-12: sync-over-async allowlist made semantic (file+pattern+method, dropped drift-prone line numbers)
  • SnapshotSettings field-drop sweep (SaveSyncedLyrics + UseLRCLIB)

ext-common-sha.txt resolved to 961dfae; submodule gitlink fast-forwards to match. Full Tidalarr.Tests green locally (1321 passed, 17 skipped, 0 failed).

🤖 Generated with Claude Code

RicherTunes and others added 8 commits June 1, 2026 14:16
Re-pins ext/Lidarr.Plugin.Common to RicherTunes/Lidarr.Plugin.Common@ed3c27b
(current main HEAD) to converge the streaming trio onto one Common SHA and
satisfy the Ecosystem Pin Drift guard. Supersedes the stale bump to 79d2140
(#317). Additive forward bump (manifest parsers, hostbridge size-estimator
seam, MSBuild deps.json-race fix); commonVersion stays 1.18.0-dev.

Both the gitlink and ext-common-sha.txt now equal ed3c27b8edfd2a125665daf7b7d754b3ecf5c341.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… parity discipline

Adds a short pointer (not a restatement — consolidation applies to docs too) to
ext/Lidarr.Plugin.Common/AGENTS.md 'Ecosystem Consolidation & Parity Discipline':
find-once -> sweep-five across the 5 copy-paste-adjacent plugins, push shared logic into
Common (plugins adopt it; the out-of-tree DRM seam stays plugin-owned+public), the parity
matrix is a contract, verify architecture before assuming a bug class sweeps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oss-plugin field-drop sweep)

Cross-plugin sweep of the SnapshotSettings field-drop bug class (found first in amazonmusicarr, where the
host snapshot dropped EnableDrm and DRM never activated in background downloads).

Tidal's inline snapshotter copied 8 of the settings' 10 read-write properties — it silently dropped
SaveSyncedLyrics + UseLRCLIB, so the background download (which reads the pre-await snapshot) ignored the
user's lyric settings and used defaults. The existing host->core parity test didn't catch it (it checks a
different mapping, not the snapshotter).

Extracted the snapshotter into an internal SnapshotSettings that reflection-copies EVERY read-write
property — structurally cannot drop a field. Added a unit test pinning the full contract (a reflection
sweep over all properties) + the two previously-dropped lyric fields.

Sweep results: apple's SnapshotSettings already copies all 10 fields (and has a test) — not vulnerable;
qobuz/brainarr have no such snapshot pattern. Candidate to consolidate into a Common helper later.

Build 0-error; 187 download-area tests pass + 2 new snapshot tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review F-09 (MEDIUM): TidalDownloadClient's public write boundaries
(DownloadTrackEnhancedAsync, DownloadTrackWithMetadataAsync) created directories
and wrote/moved/deleted files based on a caller-provided outputPath without
checking it stays inside the configured DownloadPath — a hostile-controlled path
(e.g. via crafted filename/segment) could write outside the download root.

Fix: add IsOutputPathAllowed (canonical-form containment via Common's
PathTraversalGuard.IsPathWithinRoot — resolves "..", defends sibling-prefix +
case-twin) and call EnsureOutputPathAllowed as the first statement in both
methods, before any mkdir/write/move/delete/tag. The system temp dir is allowed
as an alternate root because DownloadTrackAsync stages there before the host
imports.

Mirrors amazon's existing IsPathWithinRoot guards (parity). Sibling sweep of the
"download write containment" class: amazon already guarded, qobuz cleanup is the
separate F-10, apple is parallel-AI-owned, brainarr has no download client.

Tests: TidalDownloadClientPathContainmentTests — 5 pure-helper cases + 2 wired
integration tests (escape path refused with a download-path error, no file
written). RED→GREEN; full download-client suite 99/99.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ck MemoryStream (F-08)

Adversarial review F-08 (MEDIUM): DownloadTrackEnhancedAsync assembled the full
track via DownloadAndAssembleAsync(manifest), which buffers the entire decrypted
track in a MemoryStream (File.ReadAllBytes + in-memory decrypt), then copied that
to the output FileStream — pinning a large hi-res track's full size on the managed
heap and double-buffering (disk → memory → disk).

Fix: switch to the already-existing file-backed path DownloadAndAssembleToFileStreamAsync,
which assembles to a temp file (DeleteOnClose), decrypts in place on the file
stream, and returns a seekable FileStream. The downstream header-validate + copy
logic is unchanged (works on any seekable stream). Sequential (maxConcurrency 1)
to preserve this path's historical ordering contract.

The MemoryStream overload stays — it remains public API and is covered by the chunk
downloader's own tests; only the enhanced client path moves off it. Tidal-specific
class (amazon/qobuz already stream to disk; apple parallel-AI-owned).

Verified: Enhanced + ChunkDownloader suites 69/69, full Tidalarr.Tests 1315/1332
(17 pre-existing skips), 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…numbers (F-12)

Identify allowlist entries by file + pattern + method instead of drift-prone line
numbers (the linter matches by file/pattern and reports the accurate current line
at scan time — the JSON 'line' field was redundant + stale, e.g. TidalLidarrIndexer
listed 506 vs actual 596). Part of the cross-plugin F-12 sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review F-05 (HIGH): re-pin tidal's ext/Lidarr.Plugin.Common submodule +
ext-common-sha.txt from ed3c27b to 961dfaea60710f61f885f101f3c233baf7a650cb
(origin/main after security PR #618 merged). commonVersion stays 1.18.0-dev (matches
merged Common — truthful).

Adoption fix for F-01's strict-by-default SSRF guard: the merged Common makes
ChunkedHttpAssembler SSRF-strict by default. TidalChunkDownloader now constructs it
with RemoteMediaUriPolicy { AllowHttp = true, ResolveDns = false } — the guard's value
for Tidal is blocking literal private/loopback/CGNAT/metadata-host targets (the real
SSRF vectors). AllowHttp preserves the pre-#618 behaviour (Tidal manifests can serve
http segment URLs; the assembler had no scheme restriction before), and ResolveDns is
skipped on the hot path (manifest source is authenticated/trusted). The production
orchestrator uses the stream-provider path, which doesn't hit the URL guard, so no
change needed there.

Verified against merged Common: download/chunk/orchestrator 160/160, full Tidalarr.Tests
1315/1332 (17 pre-existing skips), 0 failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings the adversarial-review fixes carried on chore/converge-common-ed3c27b onto main:
- F-05: Common pin converged to 961dfae (PR #618 security batch)
- F-08: enhanced track assembly streams to file (no whole-track MemoryStream)
- F-09: download output paths root-contained (PathTraversalGuard)
- F-12: sync-over-async allowlist made semantic (drop stale line numbers)
- SnapshotSettings field-drop sweep (SaveSyncedLyrics + UseLRCLIB)

Resolves ext-common-sha.txt to 961dfae (newest Common; submodule gitlink fast-forwards to match).

# Conflicts:
#	ext-common-sha.txt
Comment on lines +398 to +404
foreach (var p in typeof(TidalLidarrDownloadClientSettings).GetProperties())
{
if (p.CanRead && p.CanWrite && p.GetIndexParameters().Length == 0)
{
p.SetValue(snapshot, p.GetValue(live));
}
}
… (de-flake)

DownloadAndAssembleToFileStreamAsync_WithProgress_ReportsProgress used
System.Progress<T>, whose callbacks marshal asynchronously with no ordering
guarantee, so progressReports[0] could observe the count-2 report before count-1
(Assert.Equal Expected:1 Actual:2 — flaked in CI's Test-with-Coverage, passed
locally). Switched to the file's existing SyncProgress<T> (synchronous, inline)
which the other ordered-assert tests already use, making order + count
deterministic. Pre-existing flake; not introduced by the F-05/F-08 work.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@RicherTunes
RicherTunes merged commit ca6e461 into main Jun 5, 2026
14 checks passed
@RicherTunes
RicherTunes deleted the chore/land-tidal-f-fixes-to-main branch June 5, 2026 13:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants