// SPDX-License-Identifier: MIT // Copyright (c) 2026 Raphael Amorim // 1. Raw .bin (Mode2/2352): single-file image, one data track. The // historical layout for our test discs and most plain dumps. // 2. .cue + .bin(s): multi-track support via the CUE parser in // cue.jam. Required for games shipping CDDA tracks after the // data track (Brave Fencer Musashi, Wipeout, etc.). // // LBA convention follows the PSX BIOS: // LBA = 150 -> start of track 1 data (00:02:00 on the disc) // LBA = 150 + N -> Nth sector of the data track in the .bin file const { Cue, createCue, cueReset, cueParse, cueRead, cueQuery, cueGetTrackCount, cueGetTrackLba, cueGetTrackNumber } = import("cue"); const { Box } = import("std/box"); const { File, exists } = import("std/fs"); const { print } = import("std/fmt"); const SECTOR_SIZE: u32 = 2352; const LEAD_IN_SECTORS: u32 = 150; pub const Disc = struct { file: File, loaded: u8, isCue: u8, pad1: u8, pad2: u8, cuePtr: Box(Cue), }; pub fn createDisc() Box(Disc) { return Box(Disc).init(Disc { file: File { handle: 0 as *mut[] u8 }, loaded: 0, isCue: 0, pad1: 0, pad2: 0, cuePtr: createCue(), }); } pub fn discClose(d: *mut[] Disc) { if (d[0].isCue != 0) { cueReset(d[0].cuePtr.ptrMut()); d[0].isCue = 0; } else if (d[0].loaded != 0) { d[0].file.close(); } d[0].loaded = 0; } pub fn pathEndsWithCue(p: []u8) bool { var n: u32 = 0; while (p.ptr[n] != 0) { n = n + 1; } if (n < 4) { return false; } const a: u32 = p.ptr[n - 4] as u32; const b: u32 = p.ptr[n - 3] as u32; const cC: u32 = p.ptr[n - 2] as u32; const e: u32 = p.ptr[n - 1] as u32; // ".cue" or ".CUE" if (a == 0x2E && (b == 0x63 || b == 0x43) && (cC == 0x75 || cC == 0x55) && (e == 0x65 || e == 0x45)) { return true; } return false; } pub fn discOpen(d: *mut[] Disc, path: []u8) i32 { if (pathEndsWithCue(path)) { // TODO: Revisit // Reuse the always-allocated Box(Cue) at d[0].cuePtr - cueParse // populates it in place, so we don't have to deal with assigning // a new Box over the old one (jam doesn't auto-drop on field // overwrite). On parse failure, cueReset clears whatever // partial state landed before the failure. var raw: *mut[] u8 = d[0].cuePtr.ptrMut() as *mut[] u8; if (cueParse(raw, path) != 0) { cueReset(d[0].cuePtr.ptrMut()); return 0; } d[0].isCue = 1; d[0].loaded = 1; const tracks: u32 = cueGetTrackCount(raw); print("Opened CUE: {path} ({tracks} tracks)\n"); return 1; } match (File.open(path)) { Some(f) { d[0].file = f; d[0].loaded = 1; print("Opened disc image: {path}\n"); return 1; } None { return 0; } } return 0; } pub fn fillPregapSector(buf: *mut[] u8) { var i: u32 = 0; while (i < SECTOR_SIZE) { buf[i] = 0; i = i + 1; } i = 1; while (i < 11) { buf[i] = 0xFF; i = i + 1; } } pub fn discRead(d: *mut[] Disc, lba: u32, buf: *mut[] u8) i32 { if (d[0].loaded == 0) { return 0; } if (d[0].isCue != 0) { return cueRead(d[0].cuePtr.ptrMut() as *mut[] u8, lba, buf); } if (lba < LEAD_IN_SECTORS) { fillPregapSector(buf); return 1; } const off: i64 = ((lba - LEAD_IN_SECTORS) as i64) * (SECTOR_SIZE as i64); var f: File = d[0].file; f.seek(off, 0); const got: u64 = f.read(buf[0..SECTOR_SIZE]); if (got != SECTOR_SIZE as u64) { return 0; } return 1; } pub fn discLoaded(d: *mut[] Disc) i32 { if (d[0].loaded == 0) { return 0; } return 1; } pub fn discTrackCount(d: *mut[] Disc) u32 { if (d[0].loaded == 0) { return 0; } if (d[0].isCue != 0) { return cueGetTrackCount(d[0].cuePtr.ptrMut() as *mut[] u8); } return 1; } // Absolute start LBA of `track` (track 0 = lead-out / total). Returns // 0xFFFFFFFF for a track past the end (cue's TS_FAR sentinel). pub fn discTrackLba(d: *mut[] Disc, track: u32) u32 { if (d[0].isCue != 0) { return cueGetTrackLba(d[0].cuePtr.ptrMut() as *mut[] u8, track); } if (track > 1) { return 0xFFFFFFFF; } return LEAD_IN_SECTORS; } pub fn discTrackNumber(d: *mut[] Disc, lba: u32) u32 { if (d[0].isCue != 0) { return cueGetTrackNumber(d[0].cuePtr.ptrMut() as *mut[] u8, lba); } return 1; } // Track state at `lba`: 0=DATA, 1=AUDIO, 2=PREGAP, 3=FAR pub fn discQuery(d: *mut[] Disc, lba: u32) u32 { if (d[0].isCue != 0) { return cueQuery(d[0].cuePtr.ptrMut() as *mut[] u8, lba); } if (lba < LEAD_IN_SECTORS) { return 2; } return 0; }