#[cfg(feature = "chd")]
use std::path::Path;
#[cfg(feature = "chd")]
use libchdman_rs::cd::{list_tracks, TrackType as LibTrackType};
#[cfg(feature = "chd")]
use libchdman_rs::Chd;
#[cfg(feature = "chd")]
use crate::error::{OpticaldiscsError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChdMedia {
Cd,
GdRom,
Dvd,
HardDisk,
Av,
Unknown,
}
impl ChdMedia {
pub const fn is_optical(self) -> bool {
matches!(self, Self::Cd | Self::GdRom | Self::Dvd)
}
pub const fn has_tracks(self) -> bool {
matches!(self, Self::Cd | Self::GdRom)
}
pub const fn display_name(self) -> &'static str {
match self {
Self::Cd => "CD-ROM",
Self::GdRom => "GD-ROM",
Self::Dvd => "DVD",
Self::HardDisk => "hard-disk",
Self::Av => "A/V",
Self::Unknown => "unrecognised",
}
}
}
#[cfg(feature = "chd")]
pub fn chd_media(path: impl AsRef<Path>) -> Result<ChdMedia> {
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:?}")))?;
Ok(classify(&info))
}
#[cfg(feature = "chd")]
fn classify(info: &libchdman_rs::ChdInfo) -> ChdMedia {
if info.is_gd {
ChdMedia::GdRom
} else if info.is_cd {
ChdMedia::Cd
} else if info.is_dvd {
ChdMedia::Dvd
} else if info.is_av {
ChdMedia::Av
} else if info.is_hd {
ChdMedia::HardDisk
} else {
ChdMedia::Unknown
}
}
#[cfg(feature = "chd")]
fn map_cd_err(path: &Path, e: libchdman_rs::ChdError) -> OpticaldiscsError {
match e {
libchdman_rs::ChdError::NotCdMedia => OpticaldiscsError::UnsupportedFormat(format!(
"{} is a CHD without CD/GD-ROM geometry, not a disc image",
path.display()
)),
other => OpticaldiscsError::Chd(format!("read CHD tracks: {other:?}")),
}
}
pub const fn is_supported() -> bool {
cfg!(feature = "chd")
}
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 {
#[cfg(feature = "chd")]
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 is_gdrom(&self) -> bool {
self.tracks
.iter()
.any(|t| t.is_data() && t.frame_offset >= crate::sector_reader::GDROM_HD_START_LBA)
}
pub fn find_gdrom_hd_track(&self) -> Option<&ChdTrack> {
self.tracks
.iter()
.find(|t| t.is_data() && t.frame_offset >= crate::sector_reader::GDROM_HD_START_LBA)
}
pub fn find_gdrom_hd_tracks(&self) -> Vec<&ChdTrack> {
self.tracks
.iter()
.filter(|t| t.is_data() && t.frame_offset >= crate::sector_reader::GDROM_HD_START_LBA)
.collect()
}
}
#[cfg(feature = "chd")]
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 media = classify(&info);
if !media.has_tracks() {
return Err(OpticaldiscsError::UnsupportedFormat(format!(
"{} is a {} CHD, which carries no CD/GD-ROM track metadata{}",
path.display(),
media.display_name(),
if media == ChdMedia::Dvd {
" — read it with DvdChdSectorReader"
} else {
""
},
)));
}
let lib_tracks = list_tracks(&chd).map_err(|e| map_cd_err(path, 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());
}
#[cfg(feature = "chd")]
#[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(_)));
}
#[cfg(feature = "chd")]
fn dvd_chd_fixture(iso: &[u8]) -> (tempfile::TempDir, std::path::PathBuf) {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let iso_path = dir.path().join("disc.iso");
let mut f = std::fs::File::create(&iso_path).unwrap();
f.write_all(iso).unwrap();
f.flush().unwrap();
drop(f);
let chd_path = dir.path().join("disc.chd");
libchdman_rs::dvd::create_from_iso(
&iso_path,
&chd_path,
libchdman_rs::dvd::DvdCreateOptions::default(),
&mut |_| {},
&|| false,
)
.expect("could not build the DVD CHD fixture");
(dir, chd_path)
}
#[cfg(feature = "chd")]
fn minimal_iso(label: &str) -> Vec<u8> {
use crate::iso9660::{build_test_pvd_sector, PVD_SECTOR};
use crate::sector_reader::SECTOR_SIZE;
let sector = SECTOR_SIZE as usize;
let mut img = vec![0u8; 32 * sector];
let pvd = build_test_pvd_sector(label, 18, SECTOR_SIZE as u32);
let off = PVD_SECTOR as usize * sector;
img[off..off + pvd.len()].copy_from_slice(&pvd);
img[17 * sector] = 0xFF;
img[17 * sector + 1..17 * sector + 6].copy_from_slice(b"CD001");
img[17 * sector + 6] = 1;
let mut dir_off = 18 * sector;
for id in [b"\x00".as_slice(), b"\x01".as_slice()] {
let rec_len = (33 + id.len()).next_multiple_of(2);
img[dir_off] = rec_len as u8;
img[dir_off + 2..dir_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[dir_off + 6..dir_off + 10].copy_from_slice(&18u32.to_be_bytes());
img[dir_off + 10..dir_off + 14].copy_from_slice(&(sector as u32).to_le_bytes());
img[dir_off + 14..dir_off + 18].copy_from_slice(&(sector as u32).to_be_bytes());
img[dir_off + 25] = 0x02; img[dir_off + 32] = id.len() as u8;
img[dir_off + 33..dir_off + 33 + id.len()].copy_from_slice(id);
dir_off += rec_len;
}
img
}
#[cfg(feature = "chd")]
#[test]
fn dvd_chd_is_classified_read_and_probed() {
use crate::sector_reader::{DvdChdSectorReader, SectorReader, SECTOR_SIZE};
let iso = minimal_iso("DVD_CHD_TEST");
let (_dir, chd_path) = dvd_chd_fixture(&iso);
let media = chd_media(&chd_path).unwrap();
assert_eq!(media, ChdMedia::Dvd);
assert!(media.is_optical());
assert!(!media.has_tracks());
match open_chd(&chd_path) {
Err(OpticaldiscsError::UnsupportedFormat(msg)) => {
assert!(msg.contains("DVD"), "got: {msg}");
assert!(msg.contains("DvdChdSectorReader"), "got: {msg}");
}
other => panic!("expected UnsupportedFormat, got {other:?}"),
}
let mut reader = DvdChdSectorReader::open(&chd_path).unwrap();
assert_eq!(reader.logical_bytes(), iso.len() as u64);
let pvd_sector = reader.read_sector(16).unwrap();
let want = &iso[16 * SECTOR_SIZE as usize..17 * SECTOR_SIZE as usize];
assert_eq!(pvd_sector, want, "sector 16 should match the source ISO");
let spanning = reader.read_bytes(16 * SECTOR_SIZE + 8, 16).unwrap();
assert_eq!(&spanning[..], &want[8..24]);
assert!(reader.read_sector(1_000_000).is_err());
assert!(reader.read_bytes(reader.logical_bytes() - 4, 8).is_err());
let info = crate::detect::DiscImageInfo::open(&chd_path).unwrap();
assert_eq!(info.format, crate::DiscFormat::Chd);
assert_eq!(info.filesystem, crate::FilesystemType::Iso9660);
assert_eq!(info.volume_label.as_deref(), Some("DVD_CHD_TEST"));
let mut fs = crate::browse::open_disc_filesystem(&info).unwrap();
let root = fs.root().unwrap();
fs.list_directory(&root)
.expect("root of a DVD CHD should be listable");
}
#[cfg(feature = "chd")]
#[test]
fn dvd_reader_refuses_non_dvd_media() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let raw = dir.path().join("disk.img");
let mut f = std::fs::File::create(&raw).unwrap();
f.write_all(&vec![0u8; 64 * 1024]).unwrap();
f.flush().unwrap();
drop(f);
let chd_path = dir.path().join("disk.chd");
libchdman_rs::hd::create_from_path(
&raw,
&chd_path,
libchdman_rs::hd::HdCreateOptions::default(),
&mut |_| {},
&|| false,
)
.unwrap();
match crate::sector_reader::DvdChdSectorReader::open(&chd_path) {
Err(OpticaldiscsError::UnsupportedFormat(msg)) => {
assert!(msg.contains("not a DVD image"), "got: {msg}");
assert!(msg.contains("hard-disk"), "got: {msg}");
}
Err(other) => panic!("expected UnsupportedFormat, got {other:?}"),
Ok(_) => panic!("a hard-disk CHD must not open as a DVD image"),
}
}
#[cfg(feature = "chd")]
#[test]
fn hard_disk_chd_is_unsupported_not_an_abort() {
use std::io::Write;
let dir = tempfile::tempdir().unwrap();
let raw = dir.path().join("disk.img");
let mut f = std::fs::File::create(&raw).unwrap();
f.write_all(&vec![0u8; 64 * 1024]).unwrap();
f.flush().unwrap();
drop(f);
let chd_path = dir.path().join("disk.chd");
libchdman_rs::hd::create_from_path(
&raw,
&chd_path,
libchdman_rs::hd::HdCreateOptions::default(),
&mut |_| {},
&|| false,
)
.expect("could not build the hard-disk CHD fixture");
let info = Chd::open(chd_path.to_str().unwrap(), false, None)
.unwrap()
.info()
.unwrap();
assert!(info.is_hd, "fixture should be hard-disk media");
assert!(
!info.is_cd && !info.is_gd,
"fixture should not be CD/GD-ROM"
);
match open_chd(&chd_path) {
Err(OpticaldiscsError::UnsupportedFormat(msg)) => {
assert!(msg.contains("hard-disk"), "got: {msg}");
}
other => panic!("expected UnsupportedFormat, got {other:?}"),
}
let err = crate::detect::DiscImageInfo::open(&chd_path).unwrap_err();
assert!(
matches!(err, OpticaldiscsError::UnsupportedFormat(_)),
"expected UnsupportedFormat, got {err:?}"
);
}
#[test]
fn is_supported_tracks_the_feature() {
assert_eq!(is_supported(), cfg!(feature = "chd"));
assert_eq!(is_supported(), crate::DiscFormat::Chd.is_supported());
}
}