use std::io::{Read, Seek, SeekFrom};
const VERSIONS: [u32; 3] = [0x8000_0004, 0x8000_0005, 0x8000_0006];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CdiInfo {
pub version: u32,
pub descriptor_length: u32,
}
pub fn detect<R: Read + Seek>(reader: &mut R) -> Option<CdiInfo> {
let size = reader.seek(SeekFrom::End(0)).ok()?;
if size < 8 {
return None;
}
reader.seek(SeekFrom::End(-8)).ok()?;
let mut tail = [0u8; 8];
reader.read_exact(&mut tail).ok()?;
let version = u32::from_le_bytes([tail[0], tail[1], tail[2], tail[3]]);
let descriptor_length = u32::from_le_bytes([tail[4], tail[5], tail[6], tail[7]]);
if !VERSIONS.contains(&version) {
return None;
}
if descriptor_length == 0 || u64::from(descriptor_length) > size {
return None;
}
Some(CdiInfo { version, descriptor_length })
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CdiTrackKind {
Audio,
Mode1,
Mode2Formless,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CdiTrack {
pub session: u16,
pub sequence: u32,
pub kind: CdiTrackKind,
pub start_sector: u32,
pub length_sectors: u32,
pub bytes_per_sector: u16,
pub raw_bytes_per_sector: u16,
}
impl CdiTrack {
#[must_use]
pub fn end_sector(&self) -> u32 {
self.start_sector.saturating_add(self.length_sectors).saturating_sub(1)
}
}
pub fn tracks<R: Read + Seek>(reader: &mut R) -> Option<Vec<CdiTrack>> {
let info = detect(reader)?;
let size = reader.seek(SeekFrom::End(0)).ok()?;
let dsc_len = u64::from(info.descriptor_length);
if dsc_len > size {
return None;
}
reader.seek(SeekFrom::Start(size - dsc_len)).ok()?;
let mut descriptor = vec![0u8; info.descriptor_length as usize];
reader.read_exact(&mut descriptor).ok()?;
parse_descriptor(&descriptor)
}
fn is_session_header(d: &[u8], p: usize) -> bool {
let Some(s) = d.get(p..p + 15) else {
return false;
};
s[0] == 0x00
&& s[2] == 0x00
&& s[3] == 0x00
&& s[4] == 0x00
&& s[5] == 0x00
&& s[6] == 0x00
&& s[7] == 0x00
&& s[8] == 0x00
&& s[9] == 0x01
&& s[10] == 0x00
&& s[11] == 0x00
&& s[12] == 0x00
&& s[13] == 0xFF
&& s[14] == 0xFF
}
fn rd_u16(d: &[u8], p: usize) -> Option<u16> {
Some(u16::from_le_bytes(d.get(p..p + 2)?.try_into().ok()?))
}
fn rd_u32(d: &[u8], p: usize) -> Option<u32> {
Some(u32::from_le_bytes(d.get(p..p + 4)?.try_into().ok()?))
}
fn parse_descriptor(d: &[u8]) -> Option<Vec<CdiTrack>> {
let max_s = *d.first()?;
if max_s == 0 || max_s > 99 {
return None;
}
let mut pos = 1usize;
let mut tracks: Vec<CdiTrack> = Vec::new();
let mut session_no: u16 = 0;
for _s in 0..=max_s {
if !is_session_header(d, pos) {
let mut found = false;
while pos + 16 < d.len() {
if is_session_header(d, pos) {
found = true;
break;
}
pos += 1;
}
if !found {
break;
}
break;
}
let max_t = d.get(pos + 1).copied()?;
if max_t > 99 {
return None;
}
session_no += 1;
pos += 15;
for _t in 0..max_t {
let sequence = u32::try_from(tracks.len()).ok()?.checked_add(1)?;
let track = parse_track(d, &mut pos, session_no, sequence)?;
tracks.push(track);
}
}
if tracks.is_empty() {
None
} else {
Some(tracks)
}
}
fn parse_track(d: &[u8], pos: &mut usize, session: u16, sequence: u32) -> Option<CdiTrack> {
let mut p = pos.checked_add(16)?; let flen = *d.get(p)? as usize;
p = p.checked_add(1)?.checked_add(flen)?; p = p.checked_add(29)?; let _medium_type = rd_u16(d, p)?;
p = p.checked_add(2)?;
let max_i = rd_u16(d, p)? as usize;
p = p.checked_add(2)?;
p = p.checked_add(max_i.checked_mul(4)?)?;
let max_c = rd_u32(d, p)? as usize;
p = p.checked_add(4)?;
for _c in 0..max_c {
for _cb in 0..18 {
let b_len = *d.get(p)? as usize;
p = p.checked_add(1)?;
if b_len > 0 {
p = p.checked_add(b_len)?;
}
}
}
p = p.checked_add(2)?;
let track_mode = rd_u32(d, p)?;
p = p.checked_add(4)?;
p = p.checked_add(4)?; let _session_seq = rd_u32(d, p)?;
p = p.checked_add(4)?;
let _track_seq = rd_u32(d, p)?; p = p.checked_add(4)?;
let mut start_sector = rd_u32(d, p)?;
p = p.checked_add(4)?;
let mut track_len = rd_u32(d, p)?; if start_sector == 0 {
track_len = track_len.saturating_sub(150);
} else {
start_sector = start_sector.saturating_sub(150);
}
p = p.checked_add(4)?;
p = p.checked_add(16)?; let read_mode = rd_u32(d, p)?;
p = p.checked_add(4)?;
let _track_ctl = rd_u32(d, p)?;
p = p.checked_add(4)?;
p = p.checked_add(9)?; p = p.checked_add(12)?; let _isrc_valid = rd_u32(d, p)?;
p = p.checked_add(4)?;
p = p.checked_add(87)?; let _session_type = *d.get(p)?;
p = p.checked_add(1)?;
p = p.checked_add(5)?; let _track_follows = *d.get(p)?;
p = p.checked_add(2)?;
let _end_address = rd_u32(d, p)?;
p = p.checked_add(4)?;
let (kind, bytes_per_sector, raw_bytes_per_sector) = decode_mode(track_mode, read_mode)?;
*pos = p;
Some(CdiTrack {
session,
sequence,
kind,
start_sector,
length_sectors: track_len,
bytes_per_sector,
raw_bytes_per_sector,
})
}
fn decode_mode(track_mode: u32, read_mode: u32) -> Option<(CdiTrackKind, u16, u16)> {
match track_mode {
0 => match read_mode {
2..=4 => Some((CdiTrackKind::Audio, 2352, 2352)),
_ => None,
},
1 => {
let raw = match read_mode {
0 => 2048,
2..=4 => 2352,
_ => return None,
};
Some((CdiTrackKind::Mode1, 2048, raw))
}
2 => {
let raw = match read_mode {
1 => 2336,
2..=4 => 2352,
_ => return None,
};
Some((CdiTrackKind::Mode2Formless, 2336, raw))
}
_ => None,
}
}