Skip to main content

iso9660_forensic/
opener.rs

1//! Resolve an optical image *path* to a byte source over its ISO 9660 data
2//! track.
3//!
4//! An optical image arrives in several container shapes: a raw `.iso`, a `.cue`
5//! sheet pointing at a `.bin`, a CloneCD `.ccd` pointing at an `.img`, or a
6//! Nero `.nrg` / Alcohol `.mds` / CDRDAO `.toc` whose data track sits at a byte
7//! offset inside a larger file. [`open`] hides those differences: it returns a
8//! `Read + Seek` positioned to read the ISO 9660 volume, ready for
9//! [`crate::analyse`] or [`crate::IsoReader`]. A higher-level tool composes this
10//! with its own evidence-container layer (E01/VMDK/…) for non-optical inputs.
11
12use std::fs::File;
13use std::io::{BufReader, Read, Seek};
14use std::path::Path;
15
16use crate::offset::OffsetReader;
17use crate::sector::SectorMode;
18use crate::{cue, mds, nrg, toc, IsoError};
19
20/// A seekable byte source, type-erased so the different container resolutions
21/// (plain file, offset-windowed track) unify behind one return type.
22pub trait ReadSeek: Read + Seek {}
23impl<T: Read + Seek> ReadSeek for T {}
24
25/// Open an optical image by path, resolving its container to a `Read + Seek`
26/// over the ISO 9660 data track.
27///
28/// Resolves `.cue`→`.bin` and `.ccd`→`.img` (same-basename data file), and
29/// windows the data track of `.nrg` / `.mds` / `.toc`. Any other extension is
30/// opened as a raw image.
31pub fn open<P: AsRef<Path>>(path: P) -> Result<Box<dyn ReadSeek>, IsoError> {
32    let path = path.as_ref();
33    let ext = path.extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
34    match ext.as_deref() {
35        Some("nrg") => open_nrg(path),
36        Some("mds") => open_mds(path),
37        Some("toc") => open_toc(path),
38        Some("cue") => open_plain(&resolve_cue_bin(path)?),
39        Some("ccd") => open_plain(&resolve_ccd_img(path)?),
40        _ => open_plain(path),
41    }
42}
43
44fn open_plain(path: &Path) -> Result<Box<dyn ReadSeek>, IsoError> {
45    Ok(Box::new(BufReader::new(File::open(path)?)))
46}
47
48/// Window a Nero `.nrg` to its first data track.
49fn open_nrg(path: &Path) -> Result<Box<dyn ReadSeek>, IsoError> {
50    let mut f = File::open(path)?;
51    let image = nrg::parse(&mut f)?;
52    let track = image.data_track().ok_or_else(|| {
53        IsoError::BadDescriptor(format!("no data track in NRG {}", path.display()))
54    })?;
55    Ok(Box::new(OffsetReader::new(BufReader::new(f), track.start_offset, track.size)?))
56}
57
58/// Window an Alcohol `.mds`'s sibling `.mdf` to its first data track.
59fn open_mds(path: &Path) -> Result<Box<dyn ReadSeek>, IsoError> {
60    let mut desc = File::open(path)?;
61    let image = mds::parse(&mut desc)?;
62    let track = image.data_track().ok_or_else(|| {
63        IsoError::BadDescriptor(format!("no data track in MDS {}", path.display()))
64    })?;
65    let mdf = File::open(path.with_extension("mdf"))?;
66    Ok(Box::new(OffsetReader::new(BufReader::new(mdf), track.start_offset, track.data_size())?))
67}
68
69/// Window a CDRDAO `.toc`'s data file to its first data track.
70fn open_toc(path: &Path) -> Result<Box<dyn ReadSeek>, IsoError> {
71    let text = std::fs::read_to_string(path)?;
72    let sheet = toc::parse(&text);
73    let track = sheet.data_track().ok_or_else(|| {
74        IsoError::BadDescriptor(format!("no data track in TOC {}", path.display()))
75    })?;
76    let datafile = track.datafile.as_deref().ok_or_else(|| {
77        IsoError::BadDescriptor(format!("TOC data track has no DATAFILE: {}", path.display()))
78    })?;
79    let data_path = path.parent().unwrap_or_else(|| Path::new(".")).join(datafile);
80    let f = File::open(&data_path)?;
81    let file_len = f.metadata().map(|m| m.len()).unwrap_or(0);
82    let avail = file_len.saturating_sub(track.file_offset);
83    let sector_size = track.mode.sector_mode().map_or(2352, SectorMode::physical_sector_size);
84    let len = if track.length_sectors > 0 {
85        (u64::from(track.length_sectors) * sector_size).min(avail)
86    } else {
87        avail
88    };
89    Ok(Box::new(OffsetReader::new(BufReader::new(f), track.file_offset, len)?))
90}
91
92/// Resolve a CUE sheet to the `.bin` holding its first data track.
93fn resolve_cue_bin(path: &Path) -> Result<std::path::PathBuf, IsoError> {
94    let text = std::fs::read_to_string(path)?;
95    let sheet = cue::parse(&text);
96    let (file_name, _track) = sheet.data_track().ok_or_else(|| {
97        IsoError::BadDescriptor(format!("no data track in CUE sheet {}", path.display()))
98    })?;
99    Ok(path.parent().unwrap_or_else(|| Path::new(".")).join(file_name))
100}
101
102/// Resolve a CloneCD `.ccd` to its same-basename `.img`.
103fn resolve_ccd_img(path: &Path) -> Result<std::path::PathBuf, IsoError> {
104    let img = path.with_extension("img");
105    if img.is_file() {
106        Ok(img)
107    } else {
108        Err(IsoError::BadDescriptor(format!(
109            "no .img alongside CloneCD control file {}",
110            path.display()
111        )))
112    }
113}