pub mod audit;
pub mod dir;
pub mod el_torito;
pub mod error;
pub mod file_reader;
pub mod path_table;
pub mod pvd;
pub mod rock_ridge;
pub mod sector;
pub mod session;
pub mod udf;
pub use error::IsoError;
pub const MAX_DIR_SIZE: u32 = 64 * 1024 * 1024;
pub const MAX_WALK_DEPTH: usize = 256;
pub use file_reader::IsoFileReader;
pub use pvd::IsoDateTime;
pub use sector::SectorMode;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct WalkEntry {
pub path: String,
pub depth: usize,
pub record: DirRecord,
}
pub use audit::{BothEndianMismatch, GapHit, PreSysHit, SlackHit, SymlinkIssue};
#[derive(Debug, Clone)]
pub struct ToolFingerprint {
pub tool: String,
pub version: Option<String>,
pub confidence: &'static str,
pub evidence: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct PathTableAudit {
pub path_table_lbas: Vec<u32>,
pub tree_lbas: Vec<u32>,
pub phantom_lbas: Vec<u32>,
pub ghost_lbas: Vec<u32>,
}
#[derive(Debug, Clone)]
pub struct TimelineEntry {
pub path: String,
pub is_dir: bool,
pub size: u32,
pub modify_ts: Option<[u8; 7]>,
pub anomaly: Option<String>,
}
#[derive(Debug, Clone)]
pub struct FileHash {
pub path: String,
pub size: u32,
pub sha256_hex: String,
}
pub use dir::{DirRecord, FILE_FLAG_MULTI_EXTENT};
use std::io::{Read, Seek, SeekFrom};
use dir::parse_dir_records;
use el_torito::{boot_catalog_lba, parse_boot_catalog, BootEntry};
use pvd::{
PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor, BOOT_RECORD_TYPE, PVD_TYPE, SVD_TYPE,
TERMINATOR_TYPE,
};
use rock_ridge::{continuation, has_sp_entry, sp_skip as extract_sp_skip};
use sector::read_sector_data;
use udf::{detect_udf, parse_udf_state, read_dir_at_lba, read_fe_data, UdfState};
pub use udf::UdfFileEntry;
pub struct IsoReader<R> {
inner: R,
mode: SectorMode,
pvd: PrimaryVolumeDescriptor,
svd: Option<SupplementaryVolumeDescriptor>,
boot_catalog_lba: Option<u32>,
pub session_pvd_lbas: Vec<u64>,
pub has_udf: bool,
pub has_rock_ridge: bool,
sp_skip: usize,
udf_state: Option<UdfState>,
}
impl<R: Read + Seek> IsoReader<R> {
pub fn open(mut reader: R) -> Result<Self, IsoError> {
let mode = SectorMode::detect(&mut reader)?;
let session_pvd_lbas = scan_sessions(&mut reader, mode)?;
let active_pvd_lba = session_pvd_lbas.last().copied().ok_or(IsoError::NotAnIso)?;
let (pvd, svd, boot_cat_lba, has_rock_ridge, sp_skip) =
read_volume_descriptors(&mut reader, mode, active_pvd_lba)?;
let has_udf = detect_udf(&mut reader);
let udf_state = if has_udf {
parse_udf_state(&mut reader)
} else {
None
};
Ok(Self {
inner: reader,
mode,
pvd,
svd,
boot_catalog_lba: boot_cat_lba,
session_pvd_lbas,
has_udf,
has_rock_ridge,
sp_skip,
udf_state,
})
}
pub fn read_sector_raw(&mut self, lba: u64) -> Result<[u8; 2048], IsoError> {
let mut buf = [0u8; 2048];
read_sector_data(&mut self.inner, self.mode, lba, &mut buf)?;
Ok(buf)
}
pub fn sector_mode(&self) -> SectorMode {
self.mode
}
pub fn volume_label(&self) -> &str {
&self.pvd.volume_label
}
pub fn system_id(&self) -> &str { &self.pvd.system_id }
pub fn volume_set_id(&self) -> &str { &self.pvd.volume_set_id }
pub fn publisher_id(&self) -> &str { &self.pvd.publisher_id }
pub fn data_preparer_id(&self) -> &str { &self.pvd.data_preparer_id }
pub fn application_id(&self) -> &str { &self.pvd.application_id }
pub fn copyright_file_id(&self) -> &str { &self.pvd.copyright_file_id }
pub fn abstract_file_id(&self) -> &str { &self.pvd.abstract_file_id }
pub fn bibliographic_file_id(&self) -> &str { &self.pvd.bibliographic_file_id }
pub fn volume_creation_time(&self) -> Option<&IsoDateTime> { self.pvd.volume_creation_time.as_ref() }
pub fn volume_modification_time(&self) -> Option<&IsoDateTime> { self.pvd.volume_modification_time.as_ref() }
pub fn volume_expiration_time(&self) -> Option<&IsoDateTime> { self.pvd.volume_expiration_time.as_ref() }
pub fn volume_effective_time(&self) -> Option<&IsoDateTime> { self.pvd.volume_effective_time.as_ref() }
pub fn volume_space_size(&self) -> u32 { self.pvd.volume_space_size }
pub fn logical_block_size(&self) -> u16 { self.pvd.logical_block_size }
pub fn path_table_size(&self) -> u32 { self.pvd.path_table_size }
pub fn l_path_table_lba(&self) -> u32 { self.pvd.l_path_table_lba }
pub fn m_path_table_lba(&self) -> u32 { self.pvd.m_path_table_lba }
pub fn joliet_label(&self) -> Option<&str> {
self.svd
.as_ref()
.filter(|s| s.is_joliet)
.map(|s| s.volume_label.as_str())
}
pub fn session_count(&self) -> usize {
self.session_pvd_lbas.len()
}
pub fn has_rock_ridge(&self) -> bool {
self.has_rock_ridge
}
pub fn has_joliet(&self) -> bool {
self.svd.as_ref().is_some_and(|s| s.is_joliet)
}
pub fn has_udf(&self) -> bool {
self.has_udf
}
pub fn read_root_dir(&mut self) -> Result<Vec<DirRecord>, IsoError> {
self.read_dir(self.pvd.root_dir_lba, self.pvd.root_dir_size)
}
pub fn read_session_root_dir(&mut self, idx: usize) -> Result<Vec<DirRecord>, IsoError> {
let pvd_lba = *self.session_pvd_lbas.get(idx).ok_or_else(|| {
IsoError::NotFound(format!("session index {idx} out of range ({})", self.session_pvd_lbas.len()))
})?;
let (pvd, _svd, _boot, _rr, _skip) =
read_volume_descriptors(&mut self.inner, self.mode, pvd_lba)?;
self.read_dir(pvd.root_dir_lba, pvd.root_dir_size)
}
pub fn read_dir(&mut self, lba: u32, size: u32) -> Result<Vec<DirRecord>, IsoError> {
if size > MAX_DIR_SIZE {
return Err(IsoError::ResourceLimit(format!(
"directory size {size} bytes exceeds limit {MAX_DIR_SIZE}"
)));
}
let mut data = vec![0u8; size as usize];
let sector_size = 2048;
let sectors = (size as usize).div_ceil(sector_size);
for i in 0..sectors {
let offset = i * sector_size;
let end = (offset + sector_size).min(size as usize);
let mut sector_buf = [0u8; 2048];
read_sector_data(
&mut self.inner,
self.mode,
lba as u64 + i as u64,
&mut sector_buf,
)?;
data[offset..end].copy_from_slice(§or_buf[..end - offset]);
}
let mut records = parse_dir_records(&data)?;
if self.sp_skip > 0 {
for rec in &mut records {
let skip = self.sp_skip.min(rec.system_use.len());
rec.system_use.drain(..skip);
}
}
for rec in &mut records {
if let Some(ce) = continuation(&rec.system_use) {
let start = ce.offset as usize;
let end = start + ce.len as usize;
if end <= 2048 {
let mut ce_buf = [0u8; 2048];
read_sector_data(&mut self.inner, self.mode, ce.lba as u64, &mut ce_buf)?;
rec.system_use.extend_from_slice(&ce_buf[start..end]);
}
}
}
let mut merged: Vec<DirRecord> = Vec::with_capacity(records.len());
let mut iter = records.into_iter().peekable();
while let Some(mut rec) = iter.next() {
if rec.flags & FILE_FLAG_MULTI_EXTENT != 0 {
loop {
if let Some(next) = iter.peek() {
if next.name_bytes == rec.name_bytes {
let next = iter.next().unwrap();
rec.extra_extents.push((next.lba, next.size));
rec.flags &= !FILE_FLAG_MULTI_EXTENT;
if next.flags & FILE_FLAG_MULTI_EXTENT == 0 {
break;
}
} else {
break;
}
} else {
break;
}
}
}
merged.push(rec);
}
Ok(merged)
}
pub fn open_file(&self, entry: &DirRecord) -> Result<IsoFileReader<R>, IsoError>
where
R: Clone,
{
if entry.is_dir() {
return Err(IsoError::NotFound("entry is a directory".into()));
}
Ok(IsoFileReader::new(
self.inner.clone(),
self.mode,
entry.lba,
entry.size,
entry.extra_extents.clone(),
))
}
pub fn read_file_entry(&mut self, entry: &DirRecord) -> Result<Vec<u8>, IsoError> {
if entry.is_dir() {
return Err(IsoError::NotFound("entry is a directory".into()));
}
let mut data = Vec::new();
self.append_extent(entry.lba, entry.size, &mut data)?;
for &(lba, size) in &entry.extra_extents {
self.append_extent(lba, size, &mut data)?;
}
Ok(data)
}
fn append_extent(&mut self, lba: u32, size: u32, out: &mut Vec<u8>) -> Result<(), IsoError> {
let sector_size = 2048usize;
let sectors = (size as usize).div_ceil(sector_size);
for i in 0..sectors {
let offset = i * sector_size;
let end = (offset + sector_size).min(size as usize);
let mut sector_buf = [0u8; 2048];
read_sector_data(&mut self.inner, self.mode, lba as u64 + i as u64, &mut sector_buf)?;
out.extend_from_slice(§or_buf[..end - offset]);
}
Ok(())
}
pub fn walk(&mut self) -> Result<Vec<WalkEntry>, IsoError> {
let root_lba = self.pvd.root_dir_lba;
let root_size = self.pvd.root_dir_size;
let mut out = Vec::new();
self.walk_dir(root_lba, root_size, String::new(), 0, &mut out)?;
Ok(out)
}
fn walk_dir(
&mut self,
lba: u32,
size: u32,
prefix: String,
depth: usize,
out: &mut Vec<WalkEntry>,
) -> Result<(), IsoError> {
if depth > MAX_WALK_DEPTH {
return Err(IsoError::ResourceLimit(format!(
"directory nesting depth {depth} exceeds limit {MAX_WALK_DEPTH}"
)));
}
for rec in self.read_dir(lba, size)? {
let name = if let Some(rr) = rock_ridge::alternate_name(&rec.system_use) {
rr
} else {
rec.iso_name()
};
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}/{name}")
};
if rec.is_dir() {
let child_lba = rec.lba;
let child_size = rec.size;
out.push(WalkEntry { path: path.clone(), depth, record: rec });
self.walk_dir(child_lba, child_size, path, depth + 1, out)?;
} else {
out.push(WalkEntry { path, depth, record: rec });
}
}
Ok(())
}
pub fn find_entry(&mut self, path: &str) -> Result<DirRecord, IsoError> {
let parts: Vec<&str> = path
.trim_matches('/')
.split('/')
.filter(|p| !p.is_empty())
.collect();
let mut lba = self.pvd.root_dir_lba;
let mut size = self.pvd.root_dir_size;
for (depth, part) in parts.iter().enumerate() {
if *part == ".." {
return Err(IsoError::PathTraversal);
}
let entries = self.read_dir(lba, size)?;
let is_last = depth == parts.len() - 1;
let needle = part.to_ascii_uppercase();
let found = entries
.into_iter()
.find(|e| {
let iso = e.iso_name().to_ascii_uppercase();
let rr =
rock_ridge::alternate_name(&e.system_use).map(|n| n.to_ascii_uppercase());
iso == needle || rr.as_deref() == Some(needle.as_str())
})
.ok_or_else(|| IsoError::NotFound(part.to_string()))?;
if is_last {
return Ok(found);
}
if !found.is_dir() {
return Err(IsoError::NotFound(format!("{part} is not a directory")));
}
lba = found.lba;
size = found.size;
}
Err(IsoError::NotFound(path.into()))
}
pub fn find_path(&mut self, path: &str) -> Result<Option<DirRecord>, IsoError> {
match self.find_entry(path) {
Ok(entry) => Ok(Some(entry)),
Err(IsoError::NotFound(_)) => Ok(None),
Err(e) => Err(e),
}
}
pub fn boot_entries(&mut self) -> Result<Vec<BootEntry>, IsoError> {
let cat_lba = match self.boot_catalog_lba {
Some(l) => l,
None => return Ok(Vec::new()),
};
let mut buf = [0u8; 2048];
read_sector_data(&mut self.inner, self.mode, cat_lba as u64, &mut buf)?;
Ok(parse_boot_catalog(&buf))
}
pub fn read_udf_root_dir(&mut self) -> Result<Vec<UdfFileEntry>, IsoError> {
let (partition_start, root_lba) = self
.udf_state
.as_ref()
.map(|s| (s.partition_start, s.root_fe_lba))
.ok_or_else(|| IsoError::BadDescriptor("UDF structure not available".into()))?;
read_dir_at_lba(&mut self.inner, partition_start, root_lba)
.ok_or_else(|| IsoError::BadDescriptor("UDF root directory unreadable".into()))
}
pub fn read_udf_dir(&mut self, entry: &UdfFileEntry) -> Result<Vec<UdfFileEntry>, IsoError> {
let partition_start = self
.udf_state
.as_ref()
.map(|s| s.partition_start)
.ok_or_else(|| IsoError::BadDescriptor("UDF structure not available".into()))?;
read_dir_at_lba(&mut self.inner, partition_start, entry.fe_lba)
.ok_or_else(|| IsoError::BadDescriptor("UDF directory unreadable".into()))
}
pub fn read_udf_file(&mut self, entry: &UdfFileEntry) -> Result<Vec<u8>, IsoError> {
let partition_start = self
.udf_state
.as_ref()
.map(|s| s.partition_start)
.ok_or_else(|| IsoError::BadDescriptor("UDF structure not available".into()))?;
read_fe_data(&mut self.inner, partition_start, entry.fe_lba)
.ok_or_else(|| IsoError::NotFound("UDF file data unreadable".into()))
}
pub fn fingerprint_tool(&self) -> ToolFingerprint {
const SIGS: &[(&str, &str, &str)] = &[
("XORRISO", "xorriso", "HIGH"),
("xorriso", "xorriso", "HIGH"),
("MKISOFS", "mkisofs", "HIGH"),
("mkisofs", "mkisofs", "HIGH"),
("GENISOIMAGE", "genisoimage", "HIGH"),
("genisoimage", "genisoimage", "HIGH"),
("IMGBURN", "ImgBurn", "HIGH"),
("ImgBurn", "ImgBurn", "HIGH"),
("HDIUTIL", "hdiutil (macOS)", "HIGH"),
("hdiutil", "hdiutil (macOS)", "HIGH"),
("ISOMASTER", "IsoMaster", "HIGH"),
("NERO", "Nero", "MEDIUM"),
];
let haystack = format!("{} {}", self.data_preparer_id(), self.application_id());
for (needle, name, conf) in SIGS {
if let Some(pos) = haystack.find(needle) {
let after = &haystack[pos + needle.len()..];
let version = extract_version(after)
.or_else(|| extract_version(&haystack));
let conf: &'static str = match *conf {
"HIGH" => "HIGH",
"MEDIUM" => "MEDIUM",
_ => "LOW",
};
return ToolFingerprint {
tool: (*name).to_owned(),
version,
confidence: conf,
evidence: vec![format!("PVD field contains '{needle}'")],
};
}
}
ToolFingerprint {
tool: "unknown".to_owned(),
version: None,
confidence: "LOW",
evidence: Vec::new(),
}
}
pub fn audit_path_table(&mut self) -> Result<PathTableAudit, IsoError> {
use path_table::parse_l_path_table;
use std::collections::HashSet;
let pt_lba = self.pvd.l_path_table_lba;
let pt_size = self.pvd.path_table_size as usize;
let sectors = pt_size.div_ceil(2048).max(1);
let mut pt_data = Vec::with_capacity(sectors * 2048);
for i in 0..sectors {
let raw = self.read_sector_raw(pt_lba as u64 + i as u64)?;
pt_data.extend_from_slice(&raw);
}
let pt_slice = &pt_data[..pt_size.min(pt_data.len())];
let pt_entries = parse_l_path_table(pt_slice).unwrap_or_default();
let path_table_lbas: Vec<u32> = pt_entries.iter().map(|e| e.lba).collect();
let pt_set: HashSet<u32> = path_table_lbas.iter().copied().collect();
let tree_entries = self.walk()?;
let mut tree_set: HashSet<u32> = tree_entries
.iter()
.filter(|e| e.record.is_dir())
.map(|e| e.record.lba)
.collect();
tree_set.insert(self.pvd.root_dir_lba);
let mut tree_lbas: Vec<u32> = tree_set.iter().copied().collect();
tree_lbas.sort_unstable();
let mut phantom_lbas: Vec<u32> = pt_set.difference(&tree_set).copied().collect();
let mut ghost_lbas: Vec<u32> = tree_set.difference(&pt_set).copied().collect();
phantom_lbas.sort_unstable();
ghost_lbas.sort_unstable();
Ok(PathTableAudit {
path_table_lbas,
tree_lbas,
phantom_lbas,
ghost_lbas,
})
}
pub fn audit_both_endian(&mut self) -> Result<Vec<audit::BothEndianMismatch>, IsoError> {
use audit::BothEndianMismatch;
let mut out: Vec<BothEndianMismatch> = Vec::new();
let pvd_raw = self.read_sector_raw(16)?;
let pvd_off = self.mode.user_data_pos(16);
macro_rules! chk32 {
($off:expr, $name:expr) => {{
let le = u32::from_le_bytes(pvd_raw[$off..$off+4].try_into().unwrap()) as u64;
let be = u32::from_be_bytes(pvd_raw[$off+4..$off+8].try_into().unwrap()) as u64;
if le != be { out.push(BothEndianMismatch {
context: "PVD".into(), field: $name.into(),
byte_offset: pvd_off + $off as u64, le_val: le, be_val: be,
}); }
}};
}
macro_rules! chk16 {
($off:expr, $name:expr) => {{
let le = u16::from_le_bytes(pvd_raw[$off..$off+2].try_into().unwrap()) as u64;
let be = u16::from_be_bytes(pvd_raw[$off+2..$off+4].try_into().unwrap()) as u64;
if le != be { out.push(BothEndianMismatch {
context: "PVD".into(), field: $name.into(),
byte_offset: pvd_off + $off as u64, le_val: le, be_val: be,
}); }
}};
}
chk32!(80, "volume_space_size");
chk16!(120, "volume_set_size");
chk16!(124, "volume_sequence_number");
chk16!(128, "logical_block_size");
chk32!(132, "path_table_size");
let entries = self.walk()?;
let mut seen = std::collections::HashSet::new();
seen.insert(self.pvd.root_dir_lba);
for e in &entries {
if e.record.is_dir() { seen.insert(e.record.lba); }
}
for dir_lba in seen {
let raw = self.read_sector_raw(dir_lba as u64)?;
let sec_off = self.mode.user_data_pos(dir_lba as u64);
let ctx = format!("dir:lba={dir_lba}");
let mut pos = 0usize;
while pos < raw.len() {
let rl = raw[pos] as usize;
if rl == 0 { pos += 1; continue; }
if rl < 33 || pos + rl > raw.len() { break; }
let le = u32::from_le_bytes(raw[pos+2..pos+6].try_into().unwrap()) as u64;
let be = u32::from_be_bytes(raw[pos+6..pos+10].try_into().unwrap()) as u64;
if le != be { out.push(BothEndianMismatch {
context: ctx.clone(), field: "entry_lba".into(),
byte_offset: sec_off + pos as u64 + 2, le_val: le, be_val: be,
}); }
let le = u32::from_le_bytes(raw[pos+10..pos+14].try_into().unwrap()) as u64;
let be = u32::from_be_bytes(raw[pos+14..pos+18].try_into().unwrap()) as u64;
if le != be { out.push(BothEndianMismatch {
context: ctx.clone(), field: "entry_size".into(),
byte_offset: sec_off + pos as u64 + 10, le_val: le, be_val: be,
}); }
pos += rl;
}
}
Ok(out)
}
pub fn audit_pre_system(&mut self) -> Result<Vec<audit::PreSysHit>, IsoError> {
const MAGIC: &[(&[u8], &str)] = &[
(b"MZ", "MZ/PE"),
(&[0x7F, b'E', b'L', b'F'], "ELF"),
(&[b'P', b'K', 0x03, 0x04], "ZIP"),
(b"%PDF", "PDF"),
(&[0x37, 0x7A, 0xBC, 0xAF], "7z"),
];
let mut out = Vec::new();
for sector in 0u8..16 {
let raw = self.read_sector_raw(sector as u64)?;
if raw.iter().all(|&b| b == 0) { continue; }
let kind = MAGIC.iter()
.find(|(sig, _)| raw.starts_with(sig))
.map(|(_, k)| *k)
.unwrap_or("non-zero");
out.push(audit::PreSysHit { sector, kind });
}
Ok(out)
}
pub fn audit_symlinks(&mut self) -> Result<Vec<audit::SymlinkIssue>, IsoError> {
let entries = self.walk()?;
let mut out = Vec::new();
for e in entries {
if e.record.is_dir() { continue; }
if let Some(target) = rock_ridge::symlink_target(&e.record.system_use) {
let issue = if target.contains("..") {
"path-traversal"
} else if target.starts_with('/') {
"absolute"
} else {
continue;
};
out.push(audit::SymlinkIssue {
entry_path: e.path,
target,
issue,
});
}
}
Ok(out)
}
pub fn audit_file_slack(&mut self) -> Result<Vec<audit::SlackHit>, IsoError> {
let entries = self.walk()?;
let mut out = Vec::new();
for e in entries {
if e.record.is_dir() { continue; }
let size = e.record.size;
let remainder = size % 2048;
let slack_bytes = if remainder == 0 { 0 } else { 2048 - remainder };
if slack_bytes == 0 {
out.push(audit::SlackHit {
entry_path: e.path, lba: e.record.lba,
file_size: size, slack_bytes: 0, nonzero: false,
});
continue;
}
let sectors = (size as u64).div_ceil(2048);
let last_lba = e.record.lba as u64 + sectors - 1;
let raw = self.read_sector_raw(last_lba)?;
let data_end = remainder as usize;
let nonzero = raw[data_end..].iter().any(|&b| b != 0);
out.push(audit::SlackHit {
entry_path: e.path, lba: e.record.lba,
file_size: size, slack_bytes, nonzero,
});
}
Ok(out)
}
pub fn timeline(&mut self) -> Result<Vec<TimelineEntry>, IsoError> {
let entries = self.walk()?;
let mut out: Vec<TimelineEntry> = entries
.into_iter()
.filter(|e| !e.record.is_dir())
.map(|e| {
let modify_ts = rock_ridge::timestamps(&e.record.system_use)
.and_then(|ts| ts.modify);
let anomaly = modify_ts.and_then(|ts| {
if ts[0] == 70 && ts[1] == 1 && ts[2] == 1
&& ts[3] == 0 && ts[4] == 0 && ts[5] == 0
{
Some("epoch-date".to_string())
} else {
None
}
});
TimelineEntry {
path: e.path, is_dir: false,
size: e.record.size, modify_ts, anomaly,
}
})
.collect();
out.sort_by(|a, b| a.modify_ts.cmp(&b.modify_ts));
Ok(out)
}
pub fn hashlist(&mut self) -> Result<Vec<FileHash>, IsoError> {
use sha2::{Digest, Sha256};
let entries = self.walk()?;
let mut out: Vec<FileHash> = Vec::new();
for e in entries {
if e.record.is_dir() { continue; }
let data = self.read_file_entry(&e.record)?;
let hash = Sha256::digest(&data);
let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
out.push(FileHash { path: e.path, size: e.record.size, sha256_hex: hex });
}
out.sort_by(|a, b| a.path.cmp(&b.path));
Ok(out)
}
pub fn audit_sector_gaps(&mut self) -> Result<Vec<audit::GapHit>, IsoError> {
let total = self.volume_space_size();
let entries = self.walk()?;
let mut alloc: std::collections::HashSet<u32> = (0..=15).collect();
for lba in 16u32..512 {
let raw = match self.read_sector_raw(lba as u64) {
Ok(r) => r,
Err(_) => break,
};
if &raw[1..6] != b"CD001" {
break;
}
alloc.insert(lba);
if raw[0] == 0xFF {
break; }
}
alloc.insert(self.pvd.root_dir_lba);
let pt_sectors = (self.pvd.path_table_size as u64).div_ceil(2048).max(1) as u32;
for base in [self.pvd.l_path_table_lba, self.pvd.m_path_table_lba] {
for s in 0..pt_sectors {
alloc.insert(base + s);
}
}
let mark_ce = |alloc: &mut std::collections::HashSet<u32>, su: &[u8]| {
if let Some(ce) = rock_ridge::continuation(su) {
let end = ce.offset.saturating_add(ce.len);
let ce_sectors = (end as u64).div_ceil(2048).max(1) as u32;
for s in 0..ce_sectors { alloc.insert(ce.lba + s); }
}
};
for e in &entries {
let sectors = (e.record.size as u64).div_ceil(2048) as u32;
for s in 0..sectors.max(1) { alloc.insert(e.record.lba + s); }
mark_ce(&mut alloc, &e.record.system_use);
}
if let Ok(root_records) = self.read_dir(self.pvd.root_dir_lba, self.pvd.root_dir_size) {
for rec in &root_records {
mark_ce(&mut alloc, &rec.system_use);
}
}
if let Ok(raw) = self.read_sector_raw(self.pvd.root_dir_lba as u64) {
let len = raw[0] as usize;
if len >= 34 && len <= raw.len() {
let name_len = raw[32] as usize;
let su_start = 33 + name_len + (if name_len % 2 == 0 { 1 } else { 0 });
if su_start < len {
mark_ce(&mut alloc, &raw[su_start..len]);
}
}
}
if let Some(svd) = self.svd.as_ref() {
let svd_root_lba = svd.root_dir_lba;
let svd_root_size = svd.root_dir_size;
let svd_pt_sectors =
(svd.path_table_size as u64).div_ceil(2048).max(1) as u32;
let svd_l = svd.l_path_table_lba;
let svd_m = svd.m_path_table_lba;
for base in [svd_l, svd_m] {
if base != 0 {
for s in 0..svd_pt_sectors { alloc.insert(base + s); }
}
}
let mut worklist = vec![(svd_root_lba, svd_root_size)];
let mut visited = std::collections::HashSet::new();
while let Some((lba, size)) = worklist.pop() {
if !visited.insert(lba) { continue; }
let dir_sectors = (size as u64).div_ceil(2048).max(1) as u32;
for s in 0..dir_sectors { alloc.insert(lba + s); }
if let Ok(children) = self.read_dir(lba, size) {
for c in children {
if c.is_dir() {
worklist.push((c.lba, c.size));
} else {
let fs = (c.size as u64).div_ceil(2048).max(1) as u32;
for s in 0..fs { alloc.insert(c.lba + s); }
}
}
}
}
}
if let Some(cat) = self.boot_catalog_lba {
alloc.insert(cat);
}
if let Ok(boot) = self.boot_entries() {
for b in &boot {
let bytes = b.sector_count as u64 * 512;
let bs = bytes.div_ceil(2048).max(1) as u32;
for s in 0..bs { alloc.insert(b.lba + s); }
}
}
let cap = total.min(512);
let mut out = Vec::new();
for lba in 0..cap {
if alloc.contains(&lba) { continue; }
let raw = self.read_sector_raw(lba as u64)?;
let nonzero = raw.iter().any(|&b| b != 0);
out.push(audit::GapHit { lba, nonzero });
}
Ok(out)
}
}
fn extract_version(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
i += 1;
}
let run = &s[start..i];
if run.contains('.') {
return Some(run.trim_end_matches('.').to_owned());
}
} else {
i += 1;
}
}
None
}
fn scan_sessions<R: Read + Seek>(reader: &mut R, mode: SectorMode) -> Result<Vec<u64>, IsoError> {
let mut lbas = Vec::new();
let mut buf = [0u8; 2048];
for lba in 16u64..4096 {
let pos = mode.user_data_pos(lba);
reader.seek(SeekFrom::Start(pos))?;
match reader.read_exact(&mut buf) {
Ok(()) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
Err(e) => return Err(e.into()),
}
if buf[0] == 0x01 && &buf[1..6] == b"CD001" && buf[6] == 0x01 {
lbas.push(lba);
}
if buf[0] == TERMINATOR_TYPE && &buf[1..6] == b"CD001" {
}
}
Ok(lbas)
}
fn read_volume_descriptors<R: Read + Seek>(
reader: &mut R,
mode: SectorMode,
first_pvd_lba: u64,
) -> Result<
(
PrimaryVolumeDescriptor,
Option<SupplementaryVolumeDescriptor>,
Option<u32>,
bool,
usize,
),
IsoError,
> {
let mut buf = [0u8; 2048];
let mut pvd: Option<PrimaryVolumeDescriptor> = None;
let mut svd: Option<SupplementaryVolumeDescriptor> = None;
let mut boot_cat: Option<u32> = None;
let mut has_rr = false;
let mut sp_skip = 0usize;
let mut lba = first_pvd_lba;
loop {
read_sector_data(reader, mode, lba, &mut buf)?;
match buf[0] {
PVD_TYPE => {
let p = PrimaryVolumeDescriptor::parse(&buf)?;
if !has_rr {
let (rr, skip) = check_rock_ridge(reader, mode, p.root_dir_lba)?;
has_rr = rr;
sp_skip = skip;
}
pvd = Some(p);
}
SVD_TYPE => {
if let Ok(s) = SupplementaryVolumeDescriptor::parse(&buf) {
if s.is_joliet {
svd = Some(s);
}
}
}
BOOT_RECORD_TYPE => {
boot_cat = boot_catalog_lba(&buf);
}
TERMINATOR_TYPE => break,
_ => {}
}
lba += 1;
}
pvd.ok_or_else(|| IsoError::BadDescriptor("no PVD found in VD chain".into()))
.map(|p| (p, svd, boot_cat, has_rr, sp_skip))
}
fn check_rock_ridge<R: Read + Seek>(
reader: &mut R,
mode: SectorMode,
root_dir_lba: u32,
) -> Result<(bool, usize), IsoError> {
let mut buf = [0u8; 2048];
read_sector_data(reader, mode, root_dir_lba as u64, &mut buf)?;
let offset = 0usize;
if buf[offset] == 0 {
return Ok((false, 0));
}
let len = buf[offset] as usize;
if len < 34 {
return Ok((false, 0));
}
let name_len = buf[offset + 32] as usize;
let su_start = 33 + name_len + (if name_len % 2 == 0 { 1 } else { 0 });
if su_start >= len {
return Ok((false, 0));
}
let su = &buf[offset + su_start..offset + len];
let found = has_sp_entry(su);
let skip = if found { extract_sp_skip(su) } else { 0 };
Ok((found, skip))
}