Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/formats/iso9660.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
34 changes: 34 additions & 0 deletions tests/test_iso9660.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading