use std::path::{Path, PathBuf};
use crate::error::{OpticaldiscsError, Result};
#[derive(Debug, Clone)]
pub struct DiscJugglerTrack {
pub track_no: u32,
pub is_data: bool,
pub physical_sector_size: u64,
pub data_offset: u64,
pub file_byte_offset: u64,
pub base_lba: u64,
pub frame_count: u64,
pub data_path: PathBuf,
}
pub fn parse_discjuggler(cdi_path: &Path) -> Result<Vec<DiscJugglerTrack>> {
let data = std::fs::read(cdi_path).map_err(OpticaldiscsError::Io)?;
if data.len() < 8 {
return Err(OpticaldiscsError::Parse("file too small for CDI".into()));
}
let dlen = le32(&data, data.len() - 4)? as usize;
if dlen < 4 || dlen > data.len() {
return Err(OpticaldiscsError::Parse(
"implausible CDI descriptor length".into(),
));
}
let desc = &data[data.len() - dlen..];
let mut cur = Cursor { d: desc, p: 0 };
let num_sessions = cur.u8()?;
let mut cur_offset: u64 = 0;
let mut tracks = Vec::new();
for _ in 0..=num_sessions {
load_session(&mut cur, &mut cur_offset, cdi_path, &mut tracks)?;
}
if tracks.is_empty() {
return Err(OpticaldiscsError::Parse(
"no tracks in CDI descriptor".into(),
));
}
Ok(tracks)
}
fn load_session(
cur: &mut Cursor,
cur_offset: &mut u64,
cdi_path: &Path,
out: &mut Vec<DiscJugglerTrack>,
) -> Result<()> {
let num_tracks = *cur.peek(1)?;
cur.skip(15)?;
for i in 0..num_tracks {
load_track(cur, cur_offset, u32::from(i) + 1, cdi_path, out)?;
}
Ok(())
}
fn load_track(
cur: &mut Cursor,
cur_offset: &mut u64,
track_no: u32,
cdi_path: &Path,
out: &mut Vec<DiscJugglerTrack>,
) -> Result<()> {
parse_header(cur)?;
let num_indices = cur.u16()? as usize;
let mut first_index = 0u32;
for i in 0..num_indices {
let idx = cur.u32()?;
if i == 0 {
first_index = idx; }
}
let num_cdtext = cur.u32()? as usize;
for _ in 0..num_cdtext {
parse_cdtext(cur)?;
}
cur.skip(2)?; let track_mode = cur.u32()?;
cur.skip(4)?; let _session_idx = cur.u32()?;
let _track_idx = cur.u32()?;
let start_address = cur.u32()?;
let track_length = cur.u32()?;
cur.skip(16)?; let read_mode = cur.u32()?;
let _track_ctl = cur.u32()?;
cur.skip(9)?; cur.skip(12)?; let _isrc_valid = cur.u32()?;
cur.skip(99)?;
let (main_size, subchannel_size) = decode_read_mode(read_mode)?;
let physical_sector_size = u64::from(main_size) + u64::from(subchannel_size);
let is_data = matches!(track_mode, 1 | 2);
let pregap = u64::from(first_index);
let track_start = *cur_offset;
out.push(DiscJugglerTrack {
track_no,
is_data,
physical_sector_size,
data_offset: user_data_offset(track_mode, main_size),
file_byte_offset: track_start + pregap * physical_sector_size,
base_lba: u64::from(start_address),
frame_count: u64::from(track_length),
data_path: cdi_path.to_path_buf(),
});
*cur_offset += physical_sector_size * u64::from(track_length);
Ok(())
}
fn parse_header(cur: &mut Cursor) -> Result<()> {
cur.skip(16)?;
let filename_length = cur.u8()? as usize;
cur.skip(filename_length)?;
cur.skip(29)?;
cur.skip(2)?; Ok(())
}
fn parse_cdtext(cur: &mut Cursor) -> Result<()> {
for _ in 0..18 {
let length = cur.u8()? as usize;
if length != 0 {
cur.skip(length)?;
}
}
Ok(())
}
fn decode_read_mode(read_mode: u32) -> Result<(u32, u32)> {
match read_mode {
0 => Ok((2048, 0)), 1 => Ok((2336, 0)), 2 => Ok((2352, 0)), 3 => Ok((2352, 16)), 4 => Ok((2352, 96)), other => Err(OpticaldiscsError::Parse(format!(
"invalid CDI read mode: {other}"
))),
}
}
fn user_data_offset(track_mode: u32, main_size: u32) -> u64 {
match track_mode {
1 => {
if main_size >= 2352 {
16 } else {
0 }
}
2 => match main_size {
s if s >= 2352 => 24, 2336 => 8, _ => 0,
},
_ => 0, }
}
struct Cursor<'a> {
d: &'a [u8],
p: usize,
}
impl Cursor<'_> {
fn peek(&self, off: usize) -> Result<&u8> {
self.d.get(self.p + off).ok_or_else(trunc)
}
fn skip(&mut self, n: usize) -> Result<()> {
let end = self.p.checked_add(n).ok_or_else(trunc)?;
if end > self.d.len() {
return Err(trunc());
}
self.p = end;
Ok(())
}
fn u8(&mut self) -> Result<u8> {
let v = *self.d.get(self.p).ok_or_else(trunc)?;
self.p += 1;
Ok(v)
}
fn u16(&mut self) -> Result<u16> {
let v = le16(self.d, self.p)?;
self.p += 2;
Ok(v)
}
fn u32(&mut self) -> Result<u32> {
let v = le32(self.d, self.p)?;
self.p += 4;
Ok(v)
}
}
fn trunc() -> OpticaldiscsError {
OpticaldiscsError::Parse("truncated CDI descriptor".into())
}
fn le16(d: &[u8], o: usize) -> Result<u16> {
d.get(o..o + 2)
.map(|b| u16::from_le_bytes([b[0], b[1]]))
.ok_or_else(trunc)
}
fn le32(d: &[u8], o: usize) -> Result<u32> {
d.get(o..o + 4)
.map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
.ok_or_else(trunc)
}
#[cfg(test)]
mod tests {
use super::*;
fn push_track(
desc: &mut Vec<u8>,
track_mode: u32,
read_mode: u32,
start_address: u32,
track_length: u32,
pregap: u32,
) {
desc.extend_from_slice(&[0u8; 16]);
desc.push(0); desc.extend_from_slice(&[0u8; 29]);
desc.extend_from_slice(&[0u8; 2]); desc.extend_from_slice(&2u16.to_le_bytes()); desc.extend_from_slice(&pregap.to_le_bytes());
desc.extend_from_slice(&(track_length - pregap).to_le_bytes());
desc.extend_from_slice(&0u32.to_le_bytes()); desc.extend_from_slice(&[0u8; 2]); desc.extend_from_slice(&track_mode.to_le_bytes());
desc.extend_from_slice(&[0u8; 4]);
desc.extend_from_slice(&0u32.to_le_bytes()); desc.extend_from_slice(&0u32.to_le_bytes()); desc.extend_from_slice(&start_address.to_le_bytes());
desc.extend_from_slice(&track_length.to_le_bytes());
desc.extend_from_slice(&[0u8; 16]);
desc.extend_from_slice(&read_mode.to_le_bytes());
desc.extend_from_slice(&0u32.to_le_bytes()); desc.extend_from_slice(&[0u8; 9]);
desc.extend_from_slice(&[0u8; 12]); desc.extend_from_slice(&0u32.to_le_bytes()); desc.extend_from_slice(&[0u8; 99]);
}
fn build_cdi(sessions: &[Vec<(u32, u32, u32, u32, u32)>]) -> Vec<u8> {
let mut total_sectors = 0u64;
for s in sessions {
for &(_, rm, _, len, _) in s {
let (main, sub) = decode_read_mode(rm).unwrap();
total_sectors += (u64::from(main) + u64::from(sub)) * u64::from(len);
}
}
let mut desc = Vec::new();
desc.push(sessions.len() as u8); for s in sessions {
desc.push(0); desc.push(s.len() as u8); desc.extend_from_slice(&[0u8; 13]); for &(tm, rm, sa, len, pg) in s {
push_track(&mut desc, tm, rm, sa, len, pg);
}
}
desc.push(0);
desc.push(0);
desc.extend_from_slice(&[0u8; 13]);
let dlen = (desc.len() + 4) as u32;
let mut file = vec![0u8; (total_sectors * 2352) as usize];
if file.is_empty() {
file = vec![0u8; 2352];
}
file.extend_from_slice(&desc);
file.extend_from_slice(&dlen.to_le_bytes());
file
}
fn write(bytes: &[u8]) -> tempfile::NamedTempFile {
use std::io::Write;
let mut f = tempfile::Builder::new().suffix(".cdi").tempfile().unwrap();
f.write_all(bytes).unwrap();
f.flush().unwrap();
f
}
#[test]
fn parses_single_mode2_track() {
let img = build_cdi(&[vec![(2, 1, 0, 250, 150)]]);
let f = write(&img);
let tracks = parse_discjuggler(f.path()).unwrap();
assert_eq!(tracks.len(), 1);
let t = &tracks[0];
assert!(t.is_data);
assert_eq!(t.physical_sector_size, 2336);
assert_eq!(t.data_offset, 8);
assert_eq!(t.base_lba, 0);
assert_eq!(t.file_byte_offset, 150 * 2336);
assert_eq!(t.frame_count, 250);
}
#[test]
fn accumulates_offsets_across_sessions() {
let img = build_cdi(&[vec![(0, 2, 0, 452, 150)], vec![(2, 1, 11702, 500, 150)]]);
let f = write(&img);
let tracks = parse_discjuggler(f.path()).unwrap();
assert_eq!(tracks.len(), 2);
assert!(!tracks[0].is_data);
let data = &tracks[1];
assert!(data.is_data);
assert_eq!(data.base_lba, 11702);
assert_eq!(data.file_byte_offset, 452 * 2352 + 150 * 2336);
}
#[test]
fn read_mode_and_data_offset_tables() {
assert_eq!(decode_read_mode(0).unwrap(), (2048, 0));
assert_eq!(decode_read_mode(1).unwrap(), (2336, 0));
assert_eq!(decode_read_mode(3).unwrap(), (2352, 16));
assert!(decode_read_mode(9).is_err());
assert_eq!(user_data_offset(1, 2048), 0);
assert_eq!(user_data_offset(1, 2352), 16);
assert_eq!(user_data_offset(2, 2336), 8);
assert_eq!(user_data_offset(2, 2352), 24);
assert_eq!(user_data_offset(0, 2352), 0); }
#[test]
fn rejects_non_cdi() {
let f = write(b"not a disc juggler image at all, no descriptor");
assert!(parse_discjuggler(f.path()).is_err());
}
}