Skip to content

cuebin: add multi-BIN cue support - #242

Merged
iTechMedic merged 2 commits into
danifunker:mainfrom
iTechMedic:multibin-cue-support
Aug 1, 2026
Merged

cuebin: add multi-BIN cue support#242
iTechMedic merged 2 commits into
danifunker:mainfrom
iTechMedic:multibin-cue-support

Conversation

@iTechMedic

@iTechMedic iTechMedic commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes #241.

Adds support for cue sheets with multiple FILE entries, the layout redump-style rips use when each track is its own .bin.

How it works. Actual data-file sizes are threaded into the cue track calculations, without which the parser cannot place any file after the first. The referenced bins are treated as one concatenated logical seek space, so the SCSI layer above still addresses a single flat image; each LBA resolves to a source file and a local offset within it, and a read that reaches a file boundary continues into the next file until the request is satisfied. Every referenced file gets its own FatFs fast-seek link map, without which seeks into the later files walk the FAT chain.

Validation. A multi-FILE cue is refused, naming the entry at fault, if a referenced file is missing, is zero bytes, or uses an unsafe absolute or parent-directory path. Empty files were the real-world failure: they open successfully, contribute nothing, and collapse every later file's base into an invalid TOC. Names are checked before anything is opened.

Single-FILE cues are unchanged, including the same-stem .bin fallback for sheets with a stale FILE line.

Also corrects two defects this exposed: an unstored PREGAP was charged twice when it crossed a FILE boundary, and READ TRACK INFORMATION retained a parser pointer across a second next_track() call, which made it report every track as zero sectors long on any image.

Tests. Split audio, mixed-mode data with a genuine ISO9660 PVD read at LBA 16, cross-boundary reads, EOF, missing files, empty files, unsafe paths, stale FILE names, one link map per .bin, PREGAP, TOC and lead-out. 184 pass normally, 187 with WITH_CHD=1, 184 under ASan/UBSan.

Hardware. Verified with 17-file and 13-file images across three systems. Per-file FatFs fast-seek maps eliminate split-image audio underruns on an Intel 440LX USB 1.1 host; data and all audio tracks remain available.

FTP upload reliability is outside this PR and is tracked in #243.

A redump-style rip stores one .bin per track and names each in its own FILE
line. CUEParser already carried what was needed to place them -- filename and
file_index per track, and a next_track() overload taking the previous file's
size -- but nothing supplied the sizes, so every caller used the no-argument
form and passed zero. With zero, each FILE after the first starts where the
previous track's INDEX said rather than where its bytes ran out, and the
tracks pile up on top of each other.

Rather than thread a size through all 18 next_track() call sites, CUEParser
now carries the table itself. set_file_sizes() is handed the device's sizes
once, where SetDevice() adopts the image, and the no-argument next_track()
selects the ending file's size when it crosses a FILE line. The explicit
overload stays for cueutil, and a device that reports no sizes -- CHD, MDS,
every single-file cue -- gets a zero and behaves exactly as before. The sheet
itself is untouched: REM SESSION, REM LEAD-OUT, PREGAP and both INDEX forms
are still read from the original text.

Seek() space becomes the cue's data files concatenated in order, so the SCSI
layer above keeps addressing one flat image and only the mapping underneath
changes. Read() no longer stops at a .bin boundary; it reads from each file in
turn until the caller's byte count is satisfied, because a short read is a
medium error to the gadget and a failed transfer to the host. Reading at
exactly GetSize() returns 0 without logging, past it is still an error, and a
cache window still never spans two files. Every file is opened at mount and
held open, since opening on demand would put an f_open in the path of an audio
read, which is the one place that cannot afford it.

The loader uses the FILE names only when there is more than one, resolving
them against the cue's own directory. A single-FILE cue still takes its data
file from the cue's own name: rips with a stale FILE line next to a correctly
named .bin are common and work today. A multi-FILE sheet is refused unless
every file it names is present, openable and larger than zero, and the error
says which one is at fault. Zero bytes was the failure that looked like
success -- David's Alien Trilogy rip had tracks 3 and 7 uploaded empty, and
they opened without error, contributed nothing, and slid every later file's
base down on top of them. CueResolveLBA likewise refuses a multi-file sheet
with a short size table rather than inventing a plausible layout.

A FILE name is also checked before anything opens it. FatFs runs with
FF_FS_RPATH 2 and takes both slashes as separators, so an absolute name or a
".." component would resolve outside the directory holding the cue; both are
refused, naming the entry at fault. "." components and nested relative paths
are left alone because "./track.bin" and "tracks/track01.bin" are ordinary.

For the same reason GetByteOffsetForLBA no longer falls back to the
single-file offset helper when a split rip's LBA cannot be resolved. That
answer assumes one flat .bin, so it addresses the wrong file; it now returns
an invalid offset and lets Seek() fail instead of serving wrong bytes. The
single-file path is unchanged.

Two defects this exposed. GetLeadoutLBA measured the last track against the
whole image, which is right for one file and wrong for a concatenation, so a
split rip's leadout landed at 738 instead of 438; it now measures against the
file the last track lives in. And READ TRACK INFORMATION kept a CUETrackInfo
pointer across a second next_track(), which overwrites the struct it points
into, so trackLength came out as next minus itself and every track on every
image reported 0 sectors. That one was never split-rip specific.

The tests mount through loadCueBinIsoFileDevice rather than a helper that
copied its logic, which is what makes the enumeration and the cue-relative
path resolution covered at all. That meant linking util.cpp into the suite: it
supplies the real FatFsOptimizer, so the harness stub is gone, and its CHD
branch is compiled out in the no-libchdr build. The mixed-mode fixture is the
tracked ISO wrapped into real MODE1/2352 sectors, so the test reads LBA 16 and
checks a genuine ISO9660 Primary Volume Descriptor, and pins medium type 0x03.

Each data file gets its own FatFs fast-seek link map. Only the file the device
was constructed with used to get one, so every seek into an adopted file
walked the FAT chain -- and a split rip keeps its audio tracks in exactly those
files. On a USB 1.1 host there is no slack to absorb that: the audio queue
drained and playback broke up. A CLMT describes one file and is never shared;
it stays best effort, so a file whose map cannot be built is still accepted and
reads normally. The host FatFs seam counts CREATE_LINKMAP requests so the tests
can pin one map per .bin.

Verified the empty-file check is load-bearing: with it disabled the new test
mounts the broken image instead of refusing it, and with per-file fast seek
removed the link-map count drops to 1 and its test fails. Verified on hardware
with a 17-file image and a separate 13-file image across three machines --
data track and all audio tracks present, the previously silent tracks 3 and 7
play, and a 440LX USB 1.1 host that crackled on a split rip now plays clean.

184 tests pass, 187 with WITH_CHD=1, 184 under ASan/UBSan.

Refs danifunker#241
David's split rip came up with tracks missing. An unstored PREGAP costs disc
addresses but no file bytes, so the parser carries it in cumulative_offset and
adds it when it turns an INDEX time into an LBA. At a FILE line the next
file's start was computed from the previous track's data_start, which already
had cumulative_offset folded in -- so the gap was charged again, and every
track after the first PREGAP moved that far too late. A 150-frame gap put
track 3 at LBA 600 instead of 450, and the tracks walked off the end of the
disc the leadout described.

Single-file cues never saw it because file_start stays 0 for them, and the
split-rip tests that existed used stored pregaps (INDEX 00), which cost file
bytes and leave cumulative_offset at zero. The fix subtracts the offset back
out where file_start is built, so the INDEX lines add it exactly once.

Refs danifunker#241
@iTechMedic
iTechMedic merged commit 46d0330 into danifunker:main Aug 1, 2026
1 check passed
@iTechMedic
iTechMedic deleted the multibin-cue-support branch August 1, 2026 02:49
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.

Add support for CUE sheets that reference multiple BIN files

1 participant