diff --git a/docs/formats/iso9660.md b/docs/formats/iso9660.md index 62b74cd..bc8721f 100644 --- a/docs/formats/iso9660.md +++ b/docs/formats/iso9660.md @@ -85,3 +85,9 @@ Listing both puts two entries under one path. Extracting both writes 8 590 bytes Preferring Joliet is a decision about *names* ([ADR-0019](../adr/0019-prefer-joliet-names.md)). It is not a decision about whether the disc is readable, and the difference matters because these are rips: a supplementary descriptor whose root extent is damaged sits over a primary tree that is still perfectly good. Committing to Joliet and walking out empty produces a claimed disc with no files and no explanation — the signature [ADR-0012](../adr/0012-a-probe-must-confirm-a-file.md) exists to reject. So the walk falls back to the primary tree when the preferred one yields nothing, and the fallback is asserted both ways: it fires on a disc whose Joliet root points past the end of the image, and does *not* fire on a healthy disc whose primary tree would also have worked. + +## Directory-record LBAs are read relative to the resolved origin + +Every read this backend does is `origin + LBA * SECTOR_SIZE`, where `origin` is the offset the filesystem probe resolved ([ADR-0005](../adr/0005-probe-for-the-filesystem-origin.md)) — the descriptor scan, the directory walk and `read_file` all add the record's extent to `origin` rather than reading it as an absolute image address. That is what makes a non-zero origin work: a Nero image's 150-sector pregap, or a hybrid disc whose ISO track sits ahead of the sampler partition, resolves to a non-zero `origin` and every extent is read relative to it. This is the same failure the EMU3 backend actually had (a scan that returned file-relative addresses, so at a non-zero origin every bank listed and read back empty), and it is pinned here by a synthetic test that prepends a 150-sector pregap and asserts the payload *reads back* from the resolved origin — a listing alone passes even when the reads are wrong. + +The assumption underneath this is that an ISO's directory-record LBAs count from the **start of the ISO filesystem**, which is what `origin` points at. That holds for the pregap and hybrid cases above, where the ISO track is the whole cooked stream from `origin` onward. **Watch for** a real hybrid disc whose ISO LBAs instead count from the physical disc start — the absolute sector on the medium, ahead of the pregap — rather than from the track: there the `origin +` term would double-count and the reads would land past the data. None is known in the collection, so it is a caveat to remember, not a case handled today; it would need its own treatment (subtracting the track's absolute start from each LBA) and its own fixture. diff --git a/tests/test_iso9660.py b/tests/test_iso9660.py index 208f1bc..2490e78 100644 --- a/tests/test_iso9660.py +++ b/tests/test_iso9660.py @@ -87,6 +87,40 @@ def test_origin_probe_selects_iso9660_for_an_iso_disc(tmp_path, payload): assert origin.backend.name == "iso9660" +def test_origin_resolves_when_the_pregap_is_inside_the_cooked_stream(tmp_path, payload): + """ADR-0005 for the hybrid case it names, asserted rather than assumed. + + The container hands over a cooked stream with the 150 zeroed sectors still + in it -- a hybrid disc with an ISO track ahead of the real filesystem, or a + raw rip -- so the origin probe, not an assumption of byte 0, must resolve to + the byte the primary volume descriptor sits on. Getting it wrong reads as an + empty disc, not an error. + + This also pins the assumption that directory-record LBAs are relative to the + resolved origin: the backend reads ``origin + extent * SECTOR_SIZE``, and the + fixture's extents count from its own byte 0, so they land only if the origin + is threaded through. The read-back is the assertion that matters -- an + extent-vs-origin bug can list every file and still read pregap zeros, which + is exactly how the EMU3 bug in PR #49 hid behind a listing that looked whole. + """ + pregap = b"\x00" * (150 * 2048) + body = fixtures.make_iso9660({"KICK.WAV": payload, "SNARE.WAV": payload}, label="HYBRID LIB") + path = tmp_path / "gap.iso" + path.write_bytes(pregap + body) + image = FlatImage(path) + + origin = find_origin(image) + assert origin is not None + assert origin.backend.name == "iso9660" + assert origin.offset == 150 * 2048 + + volume = next(iter(origin.backend.volumes(image, origin.offset))) + assert volume.name == "HYBRID LIB" + assert [f.name for f in volume.files] == ["KICK.WAV", "SNARE.WAV"] + for entry in volume.files: + assert BACKEND.read_file(image, origin.offset, entry) == payload + + # Vintage Pro's SamplePool holds 1 061 files under 1 001 short names: MagicISO # caps the 8.3 name at twelve characters total and lets the "~1000" counter eat # the extension, so every index from 1000 up masters as VINTA~1000.E. Three