use std::fs::File;
use std::io::{BufReader, Read, Seek, SeekFrom};
use std::path::Path;
use crate::error::{OpticaldiscsError, Result};
pub const SECTOR_SIZE: u64 = 2048;
pub const RAW_SECTOR_SIZE: u64 = 2352;
pub const MODE1_DATA_OFFSET: u64 = 16;
pub const MODE2_DATA_OFFSET: u64 = 24;
const RAW_SYNC: [u8; 12] = [
0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00,
];
pub trait SectorReader: Send {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>>;
fn read_bytes(&mut self, byte_offset: u64, length: usize) -> Result<Vec<u8>> {
let sector_lba = byte_offset / SECTOR_SIZE;
let sector_off = (byte_offset % SECTOR_SIZE) as usize;
let mut out = Vec::with_capacity(length);
let mut remaining = length;
let mut lba = sector_lba;
let mut offset = sector_off;
while remaining > 0 {
let sector = self.read_sector(lba)?;
let available = sector.len().saturating_sub(offset);
let take = remaining.min(available);
out.extend_from_slice(§or[offset..offset + take]);
remaining -= take;
lba += 1;
offset = 0;
}
Ok(out)
}
}
pub struct IsoSectorReader {
file: BufReader<File>,
physical_sector_size: u64,
data_offset: u64,
}
impl IsoSectorReader {
pub fn new(path: impl AsRef<Path>) -> Result<Self> {
let mut file = BufReader::new(File::open(path.as_ref()).map_err(OpticaldiscsError::Io)?);
let mut head = [0u8; 16];
let (physical_sector_size, data_offset) = match file.read_exact(&mut head) {
Ok(()) if head[..12] == RAW_SYNC => {
let data_offset = match head[15] {
2 => MODE2_DATA_OFFSET,
_ => MODE1_DATA_OFFSET, };
(RAW_SECTOR_SIZE, data_offset)
}
_ => (SECTOR_SIZE, 0),
};
file.seek(SeekFrom::Start(0))
.map_err(OpticaldiscsError::Io)?;
Ok(Self {
file,
physical_sector_size,
data_offset,
})
}
fn is_raw(&self) -> bool {
self.physical_sector_size != SECTOR_SIZE
}
}
impl SectorReader for IsoSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
let offset = lba * self.physical_sector_size + self.data_offset;
self.file
.seek(SeekFrom::Start(offset))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.file
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
fn read_bytes(&mut self, byte_offset: u64, length: usize) -> Result<Vec<u8>> {
if self.is_raw() {
let sector_lba = byte_offset / SECTOR_SIZE;
let sector_off = (byte_offset % SECTOR_SIZE) as usize;
let mut out = Vec::with_capacity(length);
let mut remaining = length;
let mut lba = sector_lba;
let mut offset = sector_off;
while remaining > 0 {
let sector = self.read_sector(lba)?;
let available = sector.len().saturating_sub(offset);
let take = remaining.min(available);
out.extend_from_slice(§or[offset..offset + take]);
remaining -= take;
lba += 1;
offset = 0;
}
return Ok(out);
}
self.file
.seek(SeekFrom::Start(byte_offset))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; length];
self.file
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
pub struct BinCueSectorReader {
file: BufReader<File>,
file_byte_offset: u64,
physical_sector_size: u64,
data_offset: u64,
}
impl BinCueSectorReader {
pub fn open(track: &crate::bincue::BinTrack) -> Result<Self> {
let file = File::open(&track.bin_path).map_err(OpticaldiscsError::Io)?;
Ok(Self {
file: BufReader::new(file),
file_byte_offset: track.file_byte_offset,
physical_sector_size: track.sector_size(),
data_offset: track.data_offset(),
})
}
}
impl SectorReader for BinCueSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
let physical_offset =
self.file_byte_offset + lba * self.physical_sector_size + self.data_offset;
self.file
.seek(SeekFrom::Start(physical_offset))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.file
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
pub struct ChdSectorReader {
inner: libchdman_rs::cd::CdCookedReader,
}
impl ChdSectorReader {
pub fn open(path: impl AsRef<Path>, track: &crate::chd::ChdTrack) -> Result<Self> {
let path = path.as_ref();
std::fs::metadata(path).map_err(OpticaldiscsError::Io)?;
let path_str = path.to_str().ok_or_else(|| {
OpticaldiscsError::Chd(format!("non-UTF-8 CHD path: {}", path.display()))
})?;
let chd = libchdman_rs::Chd::open(path_str, false, None)
.map_err(|e| OpticaldiscsError::Chd(format!("failed to open CHD: {e:?}")))?;
let track_index = track.track_no.checked_sub(1).ok_or_else(|| {
OpticaldiscsError::Chd(format!(
"invalid track_no {} (must be >= 1)",
track.track_no
))
})?;
let inner =
libchdman_rs::cd::CdCookedReader::open_track(chd, track_index).map_err(|e| {
OpticaldiscsError::Chd(format!("open CHD track {}: {e:?}", track.track_no))
})?;
Ok(Self { inner })
}
}
impl SectorReader for ChdSectorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
self.inner
.seek(SeekFrom::Start(lba * SECTOR_SIZE))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.inner
.read_exact(&mut buf)
.map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
fn raw_sector(mode: u8, data: &[u8]) -> Vec<u8> {
let mut s = vec![0u8; RAW_SECTOR_SIZE as usize];
s[..12].copy_from_slice(&RAW_SYNC);
s[15] = mode;
let off = if mode == 2 {
MODE2_DATA_OFFSET
} else {
MODE1_DATA_OFFSET
} as usize;
let n = data.len().min(SECTOR_SIZE as usize);
s[off..off + n].copy_from_slice(&data[..n]);
s
}
fn write_tmp_iso(bytes: &[u8]) -> tempfile::NamedTempFile {
let mut f = tempfile::Builder::new().suffix(".iso").tempfile().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
f
}
#[test]
fn cooked_iso_reads_directly() {
let mut img = vec![0u8; 18 * SECTOR_SIZE as usize];
let off = 16 * SECTOR_SIZE as usize;
img[off..off + 5].copy_from_slice(b"CD001");
let f = write_tmp_iso(&img);
let mut r = IsoSectorReader::new(f.path()).unwrap();
assert!(!r.is_raw());
assert_eq!(&r.read_sector(16).unwrap()[..5], b"CD001");
assert_eq!(&r.read_bytes(16 * SECTOR_SIZE, 5).unwrap(), b"CD001");
}
#[test]
fn raw_mode1_iso_is_detected_and_stripped() {
let mut sectors: Vec<u8> = Vec::new();
for lba in 0..18u8 {
let mut data = vec![0u8; SECTOR_SIZE as usize];
if lba == 16 {
data[..5].copy_from_slice(b"CD001");
data[100..105].copy_from_slice(b"HELLO");
}
sectors.extend_from_slice(&raw_sector(1, &data));
}
let f = write_tmp_iso(§ors);
let mut r = IsoSectorReader::new(f.path()).unwrap();
assert!(r.is_raw());
assert_eq!(r.physical_sector_size, RAW_SECTOR_SIZE);
assert_eq!(r.data_offset, MODE1_DATA_OFFSET);
assert_eq!(&r.read_sector(16).unwrap()[..5], b"CD001");
assert_eq!(&r.read_bytes(16 * SECTOR_SIZE + 100, 5).unwrap(), b"HELLO");
}
#[test]
fn raw_mode2_iso_uses_offset_24() {
let mut sectors: Vec<u8> = Vec::new();
for lba in 0..18u8 {
let mut data = vec![0u8; SECTOR_SIZE as usize];
if lba == 16 {
data[..5].copy_from_slice(b"CD001");
}
sectors.extend_from_slice(&raw_sector(2, &data));
}
let f = write_tmp_iso(§ors);
let mut r = IsoSectorReader::new(f.path()).unwrap();
assert!(r.is_raw());
assert_eq!(r.data_offset, MODE2_DATA_OFFSET);
assert_eq!(&r.read_sector(16).unwrap()[..5], b"CD001");
}
}