use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use crate::apm::find_hfs_partition_offset;
use crate::bincue::parse_cue_tracks;
use crate::chd::open_chd;
use crate::efs::{EfsSuperblock, EFS_BLOCKSIZE, EFS_MAGIC_NEW, EFS_MAGIC_OLD};
use crate::error::{OpticaldiscsError, Result};
use crate::formats::{DiscFormat, FilesystemType};
use crate::hfs::MasterDirectoryBlock;
use crate::hfsplus::{extract_volume_name_from_catalog, HfsPlusVolumeHeader};
use crate::iso9660::PrimaryVolumeDescriptor;
use crate::sector_reader::{BinCueSectorReader, ChdSectorReader, IsoSectorReader, SectorReader};
use crate::sgi::{SgiVolumeHeader, SGI_VOLHDR_MAGIC};
const CHD_MAGIC: &[u8; 8] = b"MComprHD";
const ISO_MAGIC_OFFSET: u64 = 16 * 2048 + 1;
pub fn detect_format(path: &Path) -> Result<DiscFormat> {
if let Some(fmt) = DiscFormat::from_path(path) {
return Ok(fmt);
}
let mut file = std::fs::File::open(path).map_err(OpticaldiscsError::Io)?;
let mut magic = [0u8; 8];
if file.read_exact(&mut magic).is_ok() && &magic == CHD_MAGIC {
return Ok(DiscFormat::Chd);
}
if file.seek(SeekFrom::Start(ISO_MAGIC_OFFSET)).is_ok() {
let mut id = [0u8; 5];
if file.read_exact(&mut id).is_ok() && &id == b"CD001" {
return Ok(DiscFormat::Iso);
}
}
Err(OpticaldiscsError::UnsupportedFormat(format!(
"unrecognised format: {}",
path.display()
)))
}
pub fn detect_filesystem(reader: &mut dyn SectorReader) -> Result<FilesystemType> {
probe_filesystem(reader).map(|(fs, _pvd)| fs)
}
#[derive(Debug)]
pub struct DiscImageInfo {
pub path: PathBuf,
pub format: DiscFormat,
pub filesystem: FilesystemType,
pub volume_label: Option<String>,
pub pvd: Option<PrimaryVolumeDescriptor>,
pub hfs_mdb: Option<MasterDirectoryBlock>,
pub hfsplus_header: Option<HfsPlusVolumeHeader>,
pub sgi_header: Option<SgiVolumeHeader>,
pub efs_partition_offset: Option<u64>,
#[cfg(feature = "toc")]
pub toc: Option<crate::toc::DiscTOC>,
}
impl DiscImageInfo {
pub fn open(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref();
let format = detect_format(path)?;
match format {
DiscFormat::Iso => Self::probe_iso(path),
DiscFormat::BinCue => Self::probe_bincue(path),
DiscFormat::Chd => Self::probe_chd(path),
DiscFormat::MdsMdf => Err(OpticaldiscsError::UnsupportedFormat(
"MDS/MDF is not supported".into(),
)),
}
}
fn build(
path: &Path,
format: DiscFormat,
reader: &mut dyn SectorReader,
#[cfg(feature = "toc")] toc: Option<crate::toc::DiscTOC>,
) -> Result<Self> {
let (mut filesystem, pvd) = probe_filesystem(reader)?;
let (hfs_mdb, hfsplus_header, hfs_volume_label) = probe_hfs_detail(reader, filesystem);
let sgi = probe_sgi_detail(reader);
if filesystem == FilesystemType::Unknown {
if let Some(fs) = sgi.filesystem {
filesystem = fs;
}
}
let volume_label = pvd
.as_ref()
.map(|p| p.volume_id.clone())
.or(hfs_volume_label)
.or(sgi.volume_label);
Ok(Self {
path: path.to_path_buf(),
format,
filesystem,
volume_label,
pvd,
hfs_mdb,
hfsplus_header,
sgi_header: sgi.header,
efs_partition_offset: sgi.efs_partition_offset,
#[cfg(feature = "toc")]
toc,
})
}
fn probe_bincue(path: &Path) -> Result<Self> {
let cue_path = if path
.extension()
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
== Some("bin".into())
{
let stem = path.file_stem().unwrap_or_default();
let cue = path.with_file_name(format!("{}.cue", stem.to_string_lossy()));
if cue.exists() {
cue
} else {
return Err(OpticaldiscsError::NotFound(format!(
"no matching .cue found for {}",
path.display()
)));
}
} else {
path.to_path_buf()
};
let tracks = parse_cue_tracks(&cue_path)?;
let data_track = tracks
.iter()
.find(|t| t.is_data())
.ok_or(OpticaldiscsError::NoDataTrack)?
.clone();
let mut reader = BinCueSectorReader::open(&data_track)?;
#[cfg(feature = "toc")]
let toc = build_bincue_toc(&tracks);
Self::build(
path,
DiscFormat::BinCue,
&mut reader,
#[cfg(feature = "toc")]
toc,
)
}
fn probe_iso(path: &Path) -> Result<Self> {
let mut reader = IsoSectorReader::new(path)?;
Self::build(
path,
DiscFormat::Iso,
&mut reader,
#[cfg(feature = "toc")]
None,
)
}
fn probe_chd(path: &Path) -> Result<Self> {
let chd_info = open_chd(path)?;
#[cfg(feature = "toc")]
let toc = build_chd_toc(&chd_info.tracks);
let data_track = match chd_info.find_first_data_track() {
Some(track) => track.clone(),
None => {
return Ok(Self {
path: path.to_path_buf(),
format: DiscFormat::Chd,
filesystem: FilesystemType::Unknown,
volume_label: None,
pvd: None,
hfs_mdb: None,
hfsplus_header: None,
sgi_header: None,
efs_partition_offset: None,
#[cfg(feature = "toc")]
toc,
});
}
};
let mut reader = ChdSectorReader::open(path, &data_track)?;
Self::build(
path,
DiscFormat::Chd,
&mut reader,
#[cfg(feature = "toc")]
toc,
)
}
}
#[cfg(feature = "toc")]
fn build_bincue_toc(tracks: &[crate::bincue::BinTrack]) -> Option<crate::toc::DiscTOC> {
use crate::toc::{DiscTOC, TrackInfo};
if tracks.is_empty() {
return None;
}
let track_infos: Vec<TrackInfo> = tracks
.iter()
.map(|t| {
let offset_frames = (t.file_byte_offset / t.sector_size()) as u32;
TrackInfo {
number: t.track_no as u8,
offset: offset_frames,
track_type: t.track_type.cue_label().to_string(),
}
})
.collect();
let last = tracks.last()?;
let file_len = std::fs::metadata(&last.bin_path).ok()?.len();
let last_start_frames = last.file_byte_offset / last.sector_size();
let last_frames = file_len.saturating_sub(last.file_byte_offset) / last.sector_size();
let lead_out_raw = (last_start_frames + last_frames) as u32;
DiscTOC::from_tracks(&track_infos, lead_out_raw)
}
#[cfg(feature = "toc")]
fn build_chd_toc(tracks: &[crate::chd::ChdTrack]) -> Option<crate::toc::DiscTOC> {
use crate::toc::{DiscTOC, TrackInfo};
if tracks.is_empty() {
return None;
}
let track_infos: Vec<TrackInfo> = tracks
.iter()
.map(|t| TrackInfo {
number: t.track_no as u8,
offset: t.frame_offset as u32,
track_type: format!("{:?}", t.track_type),
})
.collect();
let last = tracks.last()?;
let lead_out_raw = last.frame_offset as u32 + last.frames;
DiscTOC::from_tracks(&track_infos, lead_out_raw)
}
pub(crate) fn resolve_apple_hfs(
reader: &mut dyn SectorReader,
partition_offset: u64,
) -> (FilesystemType, u64) {
let buf = match reader.read_bytes(partition_offset + 1024, 162) {
Ok(b) => b,
Err(_) => return (FilesystemType::Unknown, partition_offset),
};
let sig = u16::from_be_bytes([buf[0], buf[1]]);
match sig {
0x4244 => {
let embedded_sig = u16::from_be_bytes([buf[124], buf[125]]);
if embedded_sig == 0x482B {
let block_size = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]) as u64;
let first_alloc_block = u16::from_be_bytes([buf[28], buf[29]]) as u64;
let embedded_start = u16::from_be_bytes([buf[126], buf[127]]) as u64;
let hfsplus_offset =
partition_offset + first_alloc_block * 512 + embedded_start * block_size;
(FilesystemType::HfsPlus, hfsplus_offset)
} else {
(FilesystemType::Hfs, partition_offset)
}
}
0x482B | 0x4858 => (FilesystemType::HfsPlus, partition_offset),
_ => (FilesystemType::Unknown, partition_offset),
}
}
fn probe_hfs_detail(
reader: &mut dyn SectorReader,
filesystem: FilesystemType,
) -> (
Option<MasterDirectoryBlock>,
Option<HfsPlusVolumeHeader>,
Option<String>,
) {
let raw_offset = find_hfs_partition_offset(reader).unwrap_or(0);
let (_resolved_fs, resolved_offset) = resolve_apple_hfs(reader, raw_offset);
match filesystem {
FilesystemType::Hfs => match MasterDirectoryBlock::read_from(reader, raw_offset) {
Ok(mdb) => {
let label = mdb.volume_name.clone();
(Some(mdb), None, Some(label))
}
Err(_) => (None, None, None),
},
FilesystemType::HfsPlus => match HfsPlusVolumeHeader::read_from(reader, resolved_offset) {
Ok(vh) => {
let label = extract_volume_name_from_catalog(reader, resolved_offset)
.ok()
.flatten();
(None, Some(vh), label)
}
Err(_) => (None, None, None),
},
_ => (None, None, None),
}
}
struct SgiProbe {
header: Option<SgiVolumeHeader>,
efs_partition_offset: Option<u64>,
filesystem: Option<FilesystemType>,
volume_label: Option<String>,
}
fn probe_sgi_detail(reader: &mut dyn SectorReader) -> SgiProbe {
let empty = SgiProbe {
header: None,
efs_partition_offset: None,
filesystem: None,
volume_label: None,
};
let magic_bytes = match reader.read_bytes(0, 4) {
Ok(b) => b,
Err(_) => return empty,
};
let magic = u32::from_be_bytes([
magic_bytes[0],
magic_bytes[1],
magic_bytes[2],
magic_bytes[3],
]);
if magic != SGI_VOLHDR_MAGIC {
return empty;
}
let header = match SgiVolumeHeader::read_from(reader) {
Ok(h) => h,
Err(_) => return empty,
};
for entry in &header.partitions {
if entry.is_empty() {
continue;
}
if entry.partition_type().is_disk_wide_wrapper() {
continue;
}
let byte_off = entry.start_offset();
let sb_byte = byte_off + EFS_BLOCKSIZE;
let sb = match reader.read_bytes(sb_byte, EFS_BLOCKSIZE as usize) {
Ok(b) => b,
Err(_) => continue,
};
let sb_magic = u32::from_be_bytes([sb[28], sb[29], sb[30], sb[31]]);
if sb_magic == EFS_MAGIC_OLD || sb_magic == EFS_MAGIC_NEW {
let label = EfsSuperblock::parse(&sb)
.ok()
.map(|s| s.label())
.filter(|l| !l.is_empty());
return SgiProbe {
header: Some(header),
efs_partition_offset: Some(byte_off),
filesystem: Some(FilesystemType::Efs),
volume_label: label,
};
}
}
SgiProbe {
header: Some(header),
efs_partition_offset: None,
filesystem: None,
volume_label: None,
}
}
pub(crate) fn probe_filesystem(
reader: &mut dyn SectorReader,
) -> Result<(FilesystemType, Option<PrimaryVolumeDescriptor>)> {
if let Ok(pvd) = PrimaryVolumeDescriptor::read_from(reader) {
return Ok((FilesystemType::Iso9660, Some(pvd)));
}
if let Ok(sig_bytes) = reader.read_bytes(1024, 2) {
let sig = u16::from_be_bytes([sig_bytes[0], sig_bytes[1]]);
if sig == 0x4244 || sig == 0x482B || sig == 0x4858 {
let (fs, _offset) = resolve_apple_hfs(reader, 0);
if fs != FilesystemType::Unknown {
return Ok((fs, None));
}
}
}
if let Ok(entries) = crate::apm::parse_partition_map(reader) {
if let Some(partition) = entries.iter().find(|e| e.is_hfs()) {
let offset = partition.start_block as u64 * 512;
let (fs, _resolved_offset) = resolve_apple_hfs(reader, offset);
if fs != FilesystemType::Unknown {
return Ok((fs, None));
}
}
}
Ok((FilesystemType::Unknown, None))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iso9660::{build_test_pvd_sector, PVD_SECTOR};
use crate::sector_reader::SECTOR_SIZE;
use std::io::{Cursor, Read, Seek, SeekFrom};
struct CursorReader(Cursor<Vec<u8>>);
impl SectorReader for CursorReader {
fn read_sector(&mut self, lba: u64) -> Result<Vec<u8>> {
self.0
.seek(SeekFrom::Start(lba * SECTOR_SIZE))
.map_err(OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.0.read_exact(&mut buf).map_err(OpticaldiscsError::Io)?;
Ok(buf)
}
}
fn iso_image_with_label(label: &str) -> Vec<u8> {
let sectors = PVD_SECTOR as usize + 2;
let mut img = vec![0u8; sectors * SECTOR_SIZE as usize];
let pvd = build_test_pvd_sector(label, 18, 2048);
let off = PVD_SECTOR as usize * SECTOR_SIZE as usize;
img[off..off + 2048].copy_from_slice(&pvd);
img
}
#[test]
fn detects_iso9660() {
let img = iso_image_with_label("PROBE_TEST");
let mut reader = CursorReader(Cursor::new(img));
let (fs, pvd) = probe_filesystem(&mut reader).unwrap();
assert_eq!(fs, FilesystemType::Iso9660);
assert_eq!(pvd.unwrap().volume_id, "PROBE_TEST");
}
#[test]
fn detects_hfs_signature() {
let mut img = vec![0u8; 17 * SECTOR_SIZE as usize];
img[1024] = 0x42; img[1025] = 0x44; let mut reader = CursorReader(Cursor::new(img));
let (fs, pvd) = probe_filesystem(&mut reader).unwrap();
assert_eq!(fs, FilesystemType::Hfs);
assert!(pvd.is_none());
}
#[test]
fn detects_hfsplus_signature() {
let mut img = vec![0u8; 17 * SECTOR_SIZE as usize];
img[1024] = 0x48; img[1025] = 0x2B; let mut reader = CursorReader(Cursor::new(img));
let (fs, _) = probe_filesystem(&mut reader).unwrap();
assert_eq!(fs, FilesystemType::HfsPlus);
}
#[test]
fn unknown_for_empty_image() {
let img = vec![0u8; 17 * SECTOR_SIZE as usize];
let mut reader = CursorReader(Cursor::new(img));
let (fs, pvd) = probe_filesystem(&mut reader).unwrap();
assert_eq!(fs, FilesystemType::Unknown);
assert!(pvd.is_none());
}
#[test]
fn detect_format_by_extension() {
assert_eq!(
detect_format(Path::new("disc.iso")).unwrap(),
DiscFormat::Iso
);
assert_eq!(
detect_format(Path::new("disc.cue")).unwrap(),
DiscFormat::BinCue
);
assert_eq!(
detect_format(Path::new("disc.chd")).unwrap(),
DiscFormat::Chd
);
}
#[test]
fn detect_format_no_extension_unknown() {
let err = detect_format(Path::new("disc_no_ext")).unwrap_err();
assert!(matches!(
err,
OpticaldiscsError::UnsupportedFormat(_) | OpticaldiscsError::Io(_)
));
}
#[test]
fn detect_format_magic_bytes_iso() {
use std::io::Write;
let mut f = tempfile::Builder::new()
.suffix(".img") .tempfile()
.unwrap();
let size = 17 * 2048 + 6;
let mut buf = vec![0u8; size];
buf[32769..32774].copy_from_slice(b"CD001");
f.write_all(&buf).unwrap();
f.flush().unwrap();
let fmt = detect_format(f.path()).unwrap();
assert_eq!(fmt, DiscFormat::Iso);
}
#[test]
fn detect_format_magic_bytes_chd() {
use std::io::Write;
let mut f = tempfile::Builder::new()
.suffix(".img") .tempfile()
.unwrap();
let mut buf = vec![0u8; 256];
buf[..8].copy_from_slice(b"MComprHD");
f.write_all(&buf).unwrap();
f.flush().unwrap();
let fmt = detect_format(f.path()).unwrap();
assert_eq!(fmt, DiscFormat::Chd);
}
#[test]
fn detect_filesystem_iso9660() {
let img = iso_image_with_label("FS_TEST");
let mut reader = CursorReader(Cursor::new(img));
assert_eq!(
detect_filesystem(&mut reader).unwrap(),
FilesystemType::Iso9660
);
}
#[test]
fn detect_filesystem_unknown() {
let img = vec![0u8; 17 * SECTOR_SIZE as usize];
let mut reader = CursorReader(Cursor::new(img));
assert_eq!(
detect_filesystem(&mut reader).unwrap(),
FilesystemType::Unknown
);
}
#[test]
fn resolve_native_hfsplus() {
let mut img = vec![0u8; 4096];
img[1024] = 0x48; img[1025] = 0x2B; let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, 0);
assert_eq!(fs, FilesystemType::HfsPlus);
assert_eq!(offset, 0);
}
#[test]
fn resolve_native_hfsx() {
let mut img = vec![0u8; 4096];
img[1024] = 0x48; img[1025] = 0x58; let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, 0);
assert_eq!(fs, FilesystemType::HfsPlus);
assert_eq!(offset, 0);
}
#[test]
fn resolve_pure_hfs() {
let mut img = vec![0u8; 4096];
img[1024] = 0x42; img[1025] = 0x44; let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, 0);
assert_eq!(fs, FilesystemType::Hfs);
assert_eq!(offset, 0);
}
#[test]
fn resolve_embedded_hfsplus() {
let mut img = vec![0u8; 1024 * 1024]; img[1024] = 0x42; img[1025] = 0x44; img[1044..1048].copy_from_slice(&4096u32.to_be_bytes());
img[1052..1054].copy_from_slice(&4u16.to_be_bytes());
img[1148] = 0x48;
img[1149] = 0x2B;
img[1150..1152].copy_from_slice(&2u16.to_be_bytes());
let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, 0);
assert_eq!(fs, FilesystemType::HfsPlus);
assert_eq!(offset, 10240);
}
#[test]
fn resolve_embedded_hfsplus_with_partition_offset() {
let partition_offset: u64 = 32768; let mut img = vec![0u8; 1024 * 1024];
let base = partition_offset as usize + 1024;
img[base] = 0x42; img[base + 1] = 0x44; img[base + 20..base + 24].copy_from_slice(&4096u32.to_be_bytes());
img[base + 28..base + 30].copy_from_slice(&4u16.to_be_bytes());
img[base + 124] = 0x48;
img[base + 125] = 0x2B;
img[base + 126..base + 128].copy_from_slice(&2u16.to_be_bytes());
let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, partition_offset);
assert_eq!(fs, FilesystemType::HfsPlus);
assert_eq!(offset, 43008);
}
#[test]
fn resolve_unknown_signature() {
let img = vec![0u8; 4096];
let mut reader = CursorReader(Cursor::new(img));
let (fs, offset) = resolve_apple_hfs(&mut reader, 0);
assert_eq!(fs, FilesystemType::Unknown);
assert_eq!(offset, 0);
}
#[test]
fn probe_sgi_detail_finds_efs_partition() {
use crate::sgi::tests::build_test_volhdr;
use crate::sgi::SgiPartitionType;
const FIRST: u32 = 32;
let mut parts = vec![(0u32, 0u32, 0u32); 16];
parts[7] = (1000, FIRST, SgiPartitionType::SysV.as_u32());
let vh = build_test_volhdr(&parts);
let total_sectors = (FIRST as u64) + 8;
let mut img = vec![0u8; (total_sectors * SECTOR_SIZE) as usize];
img[..vh.len()].copy_from_slice(&vh);
let sb_magic_off = (FIRST as usize) * 512 + 512 + 28;
img[sb_magic_off..sb_magic_off + 4].copy_from_slice(&0x0007_2959u32.to_be_bytes());
let mut reader = CursorReader(Cursor::new(img));
let probe = probe_sgi_detail(&mut reader);
assert!(probe.header.is_some());
assert_eq!(probe.filesystem, Some(FilesystemType::Efs));
assert_eq!(probe.efs_partition_offset, Some((FIRST as u64) * 512));
}
#[test]
fn probe_sgi_detail_returns_none_without_sgi_magic() {
let img = vec![0u8; 4 * SECTOR_SIZE as usize];
let mut reader = CursorReader(Cursor::new(img));
let probe = probe_sgi_detail(&mut reader);
assert!(probe.header.is_none());
assert!(probe.efs_partition_offset.is_none());
assert!(probe.filesystem.is_none());
}
#[test]
fn probe_detects_embedded_hfsplus_without_apm() {
let mut img = vec![0u8; 17 * SECTOR_SIZE as usize];
img[1024] = 0x42; img[1025] = 0x44; img[1044..1048].copy_from_slice(&2048u32.to_be_bytes());
img[1052..1054].copy_from_slice(&0u16.to_be_bytes());
img[1148] = 0x48;
img[1149] = 0x2B;
img[1150..1152].copy_from_slice(&0u16.to_be_bytes());
let mut reader = CursorReader(Cursor::new(img));
let (fs, pvd) = probe_filesystem(&mut reader).unwrap();
assert_eq!(fs, FilesystemType::HfsPlus);
assert!(pvd.is_none());
}
}