use std::path::Path;
use libchdman_rs::cd::{list_tracks, TrackType as LibTrackType};
use libchdman_rs::Chd;
use crate::error::{OpticaldiscsError, Result};
pub const CHD_CD_FRAME_SIZE: u64 = 2448;
pub const CHD_MODE1_DATA_OFFSET: u64 = 16;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChdTrackType {
Mode1Raw,
Mode1Cooked,
Mode2Raw,
Mode2Form1,
Mode2Form2,
Audio,
Unknown(String),
}
impl ChdTrackType {
fn from_lib(t: LibTrackType) -> Self {
match t {
LibTrackType::Mode1Raw => ChdTrackType::Mode1Raw,
LibTrackType::Mode1 => ChdTrackType::Mode1Cooked,
LibTrackType::Mode2Raw => ChdTrackType::Mode2Raw,
LibTrackType::Mode2Form1 => ChdTrackType::Mode2Form1,
LibTrackType::Mode2Form2 => ChdTrackType::Mode2Form2,
LibTrackType::Audio => ChdTrackType::Audio,
LibTrackType::Mode2 => ChdTrackType::Unknown("MODE2".into()),
LibTrackType::Mode2FormMix => ChdTrackType::Unknown("MODE2_FORM_MIX".into()),
}
}
pub fn data_offset(&self) -> u64 {
match self {
ChdTrackType::Mode1Raw | ChdTrackType::Mode2Raw => CHD_MODE1_DATA_OFFSET,
_ => 0,
}
}
pub fn is_data(&self) -> bool {
matches!(
self,
ChdTrackType::Mode1Raw
| ChdTrackType::Mode1Cooked
| ChdTrackType::Mode2Raw
| ChdTrackType::Mode2Form1
| ChdTrackType::Mode2Form2
) || matches!(self, ChdTrackType::Unknown(s) if s.starts_with("MODE2"))
}
pub fn is_audio(&self) -> bool {
*self == ChdTrackType::Audio
}
}
#[derive(Debug, Clone)]
pub struct ChdTrack {
pub track_no: u32,
pub track_type: ChdTrackType,
pub frames: u32,
pub frame_offset: u64,
}
impl ChdTrack {
pub fn is_data(&self) -> bool {
self.track_type.is_data()
}
pub fn is_audio(&self) -> bool {
self.track_type.is_audio()
}
pub fn data_offset(&self) -> u64 {
self.track_type.data_offset()
}
}
#[derive(Debug)]
pub struct ChdInfo {
pub hunk_size: u32,
pub logical_size: u64,
pub tracks: Vec<ChdTrack>,
}
impl ChdInfo {
pub fn find_first_data_track(&self) -> Option<&ChdTrack> {
self.tracks.iter().find(|t| t.is_data())
}
}
pub fn open_chd(path: impl AsRef<Path>) -> Result<ChdInfo> {
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 path: {}", path.display())))?;
let chd = Chd::open(path_str, false, None)
.map_err(|e| OpticaldiscsError::Chd(format!("failed to open CHD: {e:?}")))?;
let info = chd
.info()
.map_err(|e| OpticaldiscsError::Chd(format!("CHD info: {e:?}")))?;
let lib_tracks =
list_tracks(&chd).map_err(|e| OpticaldiscsError::Chd(format!("list CHD tracks: {e:?}")))?;
let mut tracks: Vec<ChdTrack> = lib_tracks
.into_iter()
.map(|t| ChdTrack {
track_no: t.track_num,
track_type: ChdTrackType::from_lib(t.track_type),
frames: t.frames,
frame_offset: 0,
})
.collect();
tracks.sort_by_key(|t| t.track_no);
let mut offset = 0u64;
for track in &mut tracks {
track.frame_offset = offset;
offset += track.frames as u64;
}
log::debug!(
"CHD opened: hunk_bytes={}, logical_bytes={}, tracks={}",
info.hunk_bytes,
info.logical_bytes,
tracks.len()
);
Ok(ChdInfo {
hunk_size: info.hunk_bytes,
logical_size: info.logical_bytes,
tracks,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn track_data_offsets() {
assert_eq!(ChdTrackType::Mode1Raw.data_offset(), 16);
assert_eq!(ChdTrackType::Mode2Raw.data_offset(), 16);
assert_eq!(ChdTrackType::Mode1Cooked.data_offset(), 0);
assert_eq!(ChdTrackType::Mode2Form1.data_offset(), 0);
assert_eq!(ChdTrackType::Mode2Form2.data_offset(), 0);
assert_eq!(ChdTrackType::Audio.data_offset(), 0);
assert_eq!(ChdTrackType::Unknown("OTHER".into()).data_offset(), 0);
}
#[test]
fn is_data_classifications() {
assert!(ChdTrackType::Mode1Raw.is_data());
assert!(ChdTrackType::Mode1Cooked.is_data());
assert!(ChdTrackType::Mode2Raw.is_data());
assert!(ChdTrackType::Mode2Form1.is_data());
assert!(ChdTrackType::Mode2Form2.is_data());
assert!(!ChdTrackType::Audio.is_data());
assert!(ChdTrackType::Audio.is_audio());
assert!(ChdTrackType::Unknown("MODE2".into()).is_data());
assert!(!ChdTrackType::Unknown("OTHER".into()).is_data());
}
#[test]
fn find_first_data_track_returns_first_data() {
let info = ChdInfo {
hunk_size: 16384,
logical_size: 1024 * 1024,
tracks: vec![
ChdTrack {
track_no: 1,
track_type: ChdTrackType::Audio,
frames: 1000,
frame_offset: 0,
},
ChdTrack {
track_no: 2,
track_type: ChdTrackType::Mode1Raw,
frames: 5000,
frame_offset: 1000,
},
],
};
let track = info.find_first_data_track().unwrap();
assert_eq!(track.track_no, 2);
}
#[test]
fn find_first_data_track_audio_only() {
let info = ChdInfo {
hunk_size: 16384,
logical_size: 0,
tracks: vec![ChdTrack {
track_no: 1,
track_type: ChdTrackType::Audio,
frames: 1000,
frame_offset: 0,
}],
};
assert!(info.find_first_data_track().is_none());
}
#[test]
fn open_chd_nonexistent_returns_io_error() {
let err = open_chd("nonexistent_file_that_does_not_exist.chd").unwrap_err();
assert!(matches!(err, OpticaldiscsError::Io(_)));
}
}