1mod analysis;
7pub mod audit;
8pub mod bw5;
9pub mod ccd;
10pub mod cdi;
11pub mod cdtext;
12pub mod cdtoc;
13pub mod cue;
14pub mod dir;
15pub mod el_torito;
16pub mod error;
17pub mod file_reader;
18pub mod findings;
19pub mod mds;
20pub mod nrg;
21pub mod offset;
22mod opener;
23pub mod path_table;
24pub mod pvd;
25pub mod rock_ridge;
26pub mod sector;
27pub mod session;
28pub mod subq;
29pub mod toc;
30
31pub use analysis::{
32 analyse, analyse_with_options, AnalyseOptions, BootRecord, IsoAnalysis, IsoVolumeInfo,
33};
34pub use error::IsoError;
35pub use opener::{open, ReadSeek};
36
37pub const MAX_DIR_SIZE: u32 = 64 * 1024 * 1024; pub const MAX_WALK_DEPTH: usize = 256;
46pub use file_reader::IsoFileReader;
47pub use pvd::IsoDateTime;
48pub use sector::SectorMode;
49
50#[derive(Debug, Clone)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
53pub struct WalkEntry {
54 pub path: String,
56 pub depth: usize,
58 pub record: DirRecord,
60}
61
62pub use audit::{BothEndianMismatch, GapHit, PreSysHit, SlackHit, SymlinkIssue};
63
64#[derive(Debug, Clone)]
66pub struct ToolFingerprint {
67 pub tool: String,
69 pub version: Option<String>,
71 pub confidence: &'static str,
73 pub evidence: Vec<String>,
75}
76
77#[derive(Debug, Clone)]
79pub struct PathTableAudit {
80 pub path_table_lbas: Vec<u32>,
81 pub tree_lbas: Vec<u32>,
82 pub phantom_lbas: Vec<u32>,
84 pub ghost_lbas: Vec<u32>,
86}
87
88#[derive(Debug, Clone)]
91pub struct LostFile {
92 pub name: String,
94 pub lba: u32,
96 pub size: u32,
98 pub parent_lba: u32,
100}
101
102#[derive(Debug, Clone)]
104pub struct TimelineEntry {
105 pub path: String,
107 pub is_dir: bool,
108 pub size: u32,
109 pub modify_ts: Option<[u8; 7]>,
111 pub anomaly: Option<String>,
113}
114
115#[derive(Debug, Clone)]
117pub struct FileHash {
118 pub path: String,
119 pub size: u32,
120 pub sha256_hex: String,
122}
123
124pub use dir::{DirRecord, FILE_FLAG_MULTI_EXTENT};
125
126use std::io::{Read, Seek, SeekFrom};
127
128use dir::parse_dir_records;
129use el_torito::{boot_catalog_lba, parse_boot_catalog, BootEntry};
130use pvd::{
131 PrimaryVolumeDescriptor, SupplementaryVolumeDescriptor, BOOT_RECORD_TYPE, PVD_TYPE, SVD_TYPE,
132 TERMINATOR_TYPE,
133};
134use rock_ridge::{continuation, has_sp_entry, sp_skip as extract_sp_skip};
135use sector::read_sector_data;
136
137pub struct IsoReader<R> {
142 inner: R,
143 mode: SectorMode,
144 pvd: PrimaryVolumeDescriptor,
145 svd: Option<SupplementaryVolumeDescriptor>,
146 boot_catalog_lba: Option<u32>,
147 pub session_pvd_lbas: Vec<u64>,
149 pub has_rock_ridge: bool,
150 has_udf: bool,
153 sp_skip: usize,
155}
156
157impl<R: Read + Seek> IsoReader<R> {
158 pub fn open(mut reader: R) -> Result<Self, IsoError> {
160 let mode = SectorMode::detect(&mut reader)?;
161
162 let session_pvd_lbas = scan_sessions(&mut reader, mode)?;
165 let Some(&active_pvd_lba) = session_pvd_lbas.last() else {
166 return Err(IsoError::NotAnIso);
167 };
168
169 let (pvd, svd, boot_cat_lba, has_rock_ridge, sp_skip) =
170 read_volume_descriptors(&mut reader, mode, active_pvd_lba)?;
171
172 let has_udf = detect_udf(&mut reader, mode)?;
173
174 Ok(Self {
175 inner: reader,
176 mode,
177 pvd,
178 svd,
179 boot_catalog_lba: boot_cat_lba,
180 session_pvd_lbas,
181 has_rock_ridge,
182 has_udf,
183 sp_skip,
184 })
185 }
186
187 pub fn read_sector_raw(&mut self, lba: u64) -> Result<[u8; 2048], IsoError> {
192 let mut buf = [0u8; 2048];
193 read_sector_data(&mut self.inner, self.mode, lba, &mut buf)?;
194 Ok(buf)
195 }
196
197 pub fn sector_mode(&self) -> SectorMode {
199 self.mode
200 }
201
202 pub fn read_subchannel_q(&mut self, lba: u64) -> Result<Option<[u8; 12]>, IsoError> {
209 match self.mode {
210 SectorMode::Raw2448 | SectorMode::Raw2448Mode2 => {}
211 _ => return Ok(None),
212 }
213 let pos = lba * self.mode.physical_sector_size() + 2352;
214 self.inner.seek(SeekFrom::Start(pos))?;
215 let mut sub = [0u8; 96];
216 self.inner.read_exact(&mut sub)?;
217 Ok(subq::extract_q(&sub))
218 }
219
220 pub fn scan_subchannel_q(&mut self) -> Result<subq::QSummary, IsoError> {
227 match self.mode {
228 SectorMode::Raw2448 | SectorMode::Raw2448Mode2 => {}
229 _ => return Ok(subq::QSummary::default()),
230 }
231 let phys = self.mode.physical_sector_size();
232 let mut frames = Vec::new();
233 let mut lba = 0u64;
234 let mut sub = [0u8; 96];
235 loop {
236 self.inner.seek(SeekFrom::Start(lba * phys + 2352))?;
237 match self.inner.read_exact(&mut sub) {
238 Ok(()) => {}
239 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
240 Err(e) => return Err(e.into()),
241 }
242 if let Some(raw) = subq::extract_q(&sub) {
243 if subq::q_crc_valid(&raw) {
244 if let Some(frame) = subq::decode_q(&raw) {
245 frames.push(frame);
246 }
247 }
248 }
249 lba += 1;
250 }
251 Ok(subq::summarize_q(frames))
252 }
253
254 pub fn volume_label(&self) -> &str {
256 &self.pvd.volume_label
257 }
258
259 pub fn system_id(&self) -> &str {
262 &self.pvd.system_id
263 }
264 pub fn volume_set_id(&self) -> &str {
265 &self.pvd.volume_set_id
266 }
267 pub fn publisher_id(&self) -> &str {
268 &self.pvd.publisher_id
269 }
270 pub fn data_preparer_id(&self) -> &str {
271 &self.pvd.data_preparer_id
272 }
273 pub fn application_id(&self) -> &str {
274 &self.pvd.application_id
275 }
276 pub fn copyright_file_id(&self) -> &str {
277 &self.pvd.copyright_file_id
278 }
279 pub fn abstract_file_id(&self) -> &str {
280 &self.pvd.abstract_file_id
281 }
282 pub fn bibliographic_file_id(&self) -> &str {
283 &self.pvd.bibliographic_file_id
284 }
285 pub fn volume_creation_time(&self) -> Option<&IsoDateTime> {
286 self.pvd.volume_creation_time.as_ref()
287 }
288 pub fn volume_modification_time(&self) -> Option<&IsoDateTime> {
289 self.pvd.volume_modification_time.as_ref()
290 }
291 pub fn volume_expiration_time(&self) -> Option<&IsoDateTime> {
292 self.pvd.volume_expiration_time.as_ref()
293 }
294 pub fn volume_effective_time(&self) -> Option<&IsoDateTime> {
295 self.pvd.volume_effective_time.as_ref()
296 }
297 pub fn volume_space_size(&self) -> u32 {
298 self.pvd.volume_space_size
299 }
300 pub fn logical_block_size(&self) -> u16 {
301 self.pvd.logical_block_size
302 }
303 pub fn path_table_size(&self) -> u32 {
304 self.pvd.path_table_size
305 }
306 pub fn l_path_table_lba(&self) -> u32 {
307 self.pvd.l_path_table_lba
308 }
309 pub fn m_path_table_lba(&self) -> u32 {
310 self.pvd.m_path_table_lba
311 }
312
313 pub fn joliet_label(&self) -> Option<&str> {
315 self.svd.as_ref().filter(|s| s.is_joliet).map(|s| s.volume_label.as_str())
316 }
317
318 pub fn session_count(&self) -> usize {
320 self.session_pvd_lbas.len()
321 }
322
323 pub fn has_rock_ridge(&self) -> bool {
325 self.has_rock_ridge
326 }
327
328 #[must_use]
331 pub fn has_udf(&self) -> bool {
332 self.has_udf
333 }
334
335 pub fn has_joliet(&self) -> bool {
337 self.svd.as_ref().is_some_and(|s| s.is_joliet)
338 }
339
340 pub fn has_enhanced_volume_descriptor(&self) -> bool {
344 self.svd.as_ref().is_some_and(SupplementaryVolumeDescriptor::is_enhanced)
345 }
346
347 pub fn read_root_dir(&mut self) -> Result<Vec<DirRecord>, IsoError> {
349 self.read_dir(self.pvd.root_dir_lba, self.pvd.root_dir_size)
350 }
351
352 pub fn read_session_root_dir(&mut self, idx: usize) -> Result<Vec<DirRecord>, IsoError> {
356 let pvd_lba = *self.session_pvd_lbas.get(idx).ok_or_else(|| {
357 IsoError::NotFound(format!(
358 "session index {idx} out of range ({})",
359 self.session_pvd_lbas.len()
360 ))
361 })?;
362 let (pvd, _svd, _boot, _rr, _skip) =
363 read_volume_descriptors(&mut self.inner, self.mode, pvd_lba)?;
364 self.read_dir(pvd.root_dir_lba, pvd.root_dir_size)
365 }
366
367 pub fn read_dir(&mut self, lba: u32, size: u32) -> Result<Vec<DirRecord>, IsoError> {
369 if size > MAX_DIR_SIZE {
370 return Err(IsoError::ResourceLimit(format!(
371 "directory size {size} bytes exceeds limit {MAX_DIR_SIZE}"
372 )));
373 }
374 let mut data = vec![0u8; size as usize];
375 let sector_size = 2048;
376 let sectors = (size as usize).div_ceil(sector_size);
377 for i in 0..sectors {
378 let offset = i * sector_size;
379 let end = (offset + sector_size).min(size as usize);
380 let mut sector_buf = [0u8; 2048];
381 read_sector_data(&mut self.inner, self.mode, lba as u64 + i as u64, &mut sector_buf)?;
382 data[offset..end].copy_from_slice(§or_buf[..end - offset]);
383 }
384 let mut records = parse_dir_records(&data)?;
385
386 if self.sp_skip > 0 {
390 for rec in &mut records {
391 let skip = self.sp_skip.min(rec.system_use.len());
392 rec.system_use.drain(..skip);
393 }
394 }
395
396 for rec in &mut records {
398 if let Some(ce) = continuation(&rec.system_use) {
399 let start = ce.offset as usize;
400 let end = start + ce.len as usize;
401 if end <= 2048 {
402 let mut ce_buf = [0u8; 2048];
403 read_sector_data(&mut self.inner, self.mode, ce.lba as u64, &mut ce_buf)?;
404 rec.system_use.extend_from_slice(&ce_buf[start..end]);
405 }
406 }
407 }
408
409 let mut merged: Vec<DirRecord> = Vec::with_capacity(records.len());
413 let mut iter = records.into_iter().peekable();
414 while let Some(mut rec) = iter.next() {
415 if rec.flags & FILE_FLAG_MULTI_EXTENT != 0 {
416 while let Some(next) = iter.peek() {
417 if next.name_bytes != rec.name_bytes {
418 break;
419 }
420 let next = iter.next().unwrap();
421 rec.extra_extents.push((next.lba, next.size));
422 rec.flags &= !FILE_FLAG_MULTI_EXTENT;
423 if next.flags & FILE_FLAG_MULTI_EXTENT == 0 {
424 break;
425 }
426 }
427 }
428 merged.push(rec);
429 }
430
431 Ok(merged)
432 }
433
434 pub fn open_file(&self, entry: &DirRecord) -> Result<IsoFileReader<R>, IsoError>
439 where
440 R: Clone,
441 {
442 if entry.is_dir() {
443 return Err(IsoError::NotFound("entry is a directory".into()));
444 }
445 Ok(IsoFileReader::new(
446 self.inner.clone(),
447 self.mode,
448 entry.lba,
449 entry.size,
450 entry.extra_extents.clone(),
451 ))
452 }
453
454 pub fn read_file_entry(&mut self, entry: &DirRecord) -> Result<Vec<u8>, IsoError> {
458 if entry.is_dir() {
459 return Err(IsoError::NotFound("entry is a directory".into()));
460 }
461 let mut data = Vec::new();
462 self.append_extent(entry.lba, entry.size, &mut data)?;
463 for &(lba, size) in &entry.extra_extents {
464 self.append_extent(lba, size, &mut data)?;
465 }
466 Ok(data)
467 }
468
469 fn append_extent(&mut self, lba: u32, size: u32, out: &mut Vec<u8>) -> Result<(), IsoError> {
470 let sector_size = 2048usize;
471 let sectors = (size as usize).div_ceil(sector_size);
472 for i in 0..sectors {
473 let offset = i * sector_size;
474 let end = (offset + sector_size).min(size as usize);
475 let mut sector_buf = [0u8; 2048];
476 read_sector_data(&mut self.inner, self.mode, lba as u64 + i as u64, &mut sector_buf)?;
477 out.extend_from_slice(§or_buf[..end - offset]);
478 }
479 Ok(())
480 }
481
482 pub fn walk(&mut self) -> Result<Vec<WalkEntry>, IsoError> {
488 let root_lba = self.pvd.root_dir_lba;
489 let root_size = self.pvd.root_dir_size;
490 let mut out = Vec::new();
491 let mut visited = std::collections::HashSet::new();
492 self.walk_dir(root_lba, root_size, String::new(), 0, &mut out, &mut visited)?;
493 Ok(out)
494 }
495
496 pub fn walk_joliet(&mut self) -> Result<Vec<WalkEntry>, IsoError> {
503 let Some((lba, size)) =
504 self.svd.as_ref().filter(|s| s.is_joliet).map(|s| (s.root_dir_lba, s.root_dir_size))
505 else {
506 return Ok(Vec::new());
507 };
508 let mut out = Vec::new();
509 let mut visited = std::collections::HashSet::new();
510 self.walk_dir(lba, size, String::new(), 0, &mut out, &mut visited)?;
511 Ok(out)
512 }
513
514 pub fn walk_session(&mut self, idx: usize) -> Result<Vec<WalkEntry>, IsoError> {
525 let pvd_lba = *self.session_pvd_lbas.get(idx).ok_or_else(|| {
526 IsoError::NotFound(format!(
527 "session index {idx} out of range ({})",
528 self.session_pvd_lbas.len()
529 ))
530 })?;
531 let (pvd, _svd, _boot, _rr, _skip) =
532 read_volume_descriptors(&mut self.inner, self.mode, pvd_lba)?;
533 let mut out = Vec::new();
534 let mut visited = std::collections::HashSet::new();
535 self.walk_dir(
536 pvd.root_dir_lba,
537 pvd.root_dir_size,
538 String::new(),
539 0,
540 &mut out,
541 &mut visited,
542 )?;
543 Ok(out)
544 }
545
546 fn walk_dir(
547 &mut self,
548 lba: u32,
549 size: u32,
550 prefix: String,
551 depth: usize,
552 out: &mut Vec<WalkEntry>,
553 visited: &mut std::collections::HashSet<u32>,
554 ) -> Result<(), IsoError> {
555 if !visited.insert(lba) {
559 return Ok(());
560 }
561 if depth > MAX_WALK_DEPTH {
562 return Err(IsoError::ResourceLimit(format!(
563 "directory nesting depth {depth} exceeds limit {MAX_WALK_DEPTH}"
564 )));
565 }
566 for rec in self.read_dir(lba, size)? {
567 let name = if let Some(rr) = rock_ridge::alternate_name(&rec.system_use) {
568 rr
569 } else {
570 rec.iso_name()
571 };
572 let path = if prefix.is_empty() { name.clone() } else { format!("{prefix}/{name}") };
573 if rec.is_dir() {
574 let child_lba = rec.lba;
575 let child_size = rec.size;
576 out.push(WalkEntry { path: path.clone(), depth, record: rec });
577 match self.walk_dir(child_lba, child_size, path, depth + 1, out, visited) {
583 Ok(()) => {}
584 Err(IsoError::Io(io)) if io.kind() == std::io::ErrorKind::UnexpectedEof => {}
585 Err(e) => return Err(e),
586 }
587 } else {
588 out.push(WalkEntry { path, depth, record: rec });
589 }
590 }
591 Ok(())
592 }
593
594 pub fn find_entry(&mut self, path: &str) -> Result<DirRecord, IsoError> {
598 let parts: Vec<&str> =
599 path.trim_matches('/').split('/').filter(|p| !p.is_empty()).collect();
600
601 let mut lba = self.pvd.root_dir_lba;
602 let mut size = self.pvd.root_dir_size;
603
604 for (depth, part) in parts.iter().enumerate() {
605 if *part == ".." {
606 return Err(IsoError::PathTraversal);
607 }
608 let entries = self.read_dir(lba, size)?;
609 let is_last = depth == parts.len() - 1;
610 let needle = part.to_ascii_uppercase();
611 let found = entries
612 .into_iter()
613 .find(|e| {
614 let iso = e.iso_name().to_ascii_uppercase();
615 let rr =
616 rock_ridge::alternate_name(&e.system_use).map(|n| n.to_ascii_uppercase());
617 iso == needle || rr.as_deref() == Some(needle.as_str())
618 })
619 .ok_or_else(|| IsoError::NotFound(part.to_string()))?;
620
621 if is_last {
622 return Ok(found);
623 }
624 if !found.is_dir() {
625 return Err(IsoError::NotFound(format!("{part} is not a directory")));
626 }
627 lba = found.lba;
628 size = found.size;
629 }
630 Err(IsoError::NotFound(path.into()))
631 }
632
633 pub fn find_path(&mut self, path: &str) -> Result<Option<DirRecord>, IsoError> {
639 match self.find_entry(path) {
640 Ok(entry) => Ok(Some(entry)),
641 Err(IsoError::NotFound(_)) => Ok(None),
642 Err(e) => Err(e),
643 }
644 }
645
646 pub fn boot_entries(&mut self) -> Result<Vec<BootEntry>, IsoError> {
648 let cat_lba = match self.boot_catalog_lba {
649 Some(l) => l,
650 None => return Ok(Vec::new()),
651 };
652 let mut buf = [0u8; 2048];
653 read_sector_data(&mut self.inner, self.mode, cat_lba as u64, &mut buf)?;
654 Ok(parse_boot_catalog(&buf))
655 }
656
657 pub fn fingerprint_tool(&self) -> ToolFingerprint {
664 const SIGS: &[(&str, &str, &str)] = &[
665 ("XORRISO", "xorriso", "HIGH"),
666 ("xorriso", "xorriso", "HIGH"),
667 ("MKISOFS", "mkisofs", "HIGH"),
668 ("mkisofs", "mkisofs", "HIGH"),
669 ("GENISOIMAGE", "genisoimage", "HIGH"),
670 ("genisoimage", "genisoimage", "HIGH"),
671 ("IMGBURN", "ImgBurn", "HIGH"),
672 ("ImgBurn", "ImgBurn", "HIGH"),
673 ("HDIUTIL", "hdiutil (macOS)", "HIGH"),
674 ("hdiutil", "hdiutil (macOS)", "HIGH"),
675 ("ISOMASTER", "IsoMaster", "HIGH"),
676 ("NERO", "Nero", "MEDIUM"),
677 ];
678 let haystack = format!("{} {}", self.data_preparer_id(), self.application_id());
679 for (needle, name, conf) in SIGS {
680 if let Some(pos) = haystack.find(needle) {
681 let after = &haystack[pos + needle.len()..];
686 let version = extract_version(after).or_else(|| extract_version(&haystack));
687 let conf: &'static str = match *conf {
688 "HIGH" => "HIGH",
689 "MEDIUM" => "MEDIUM",
690 _ => "LOW",
691 };
692 return ToolFingerprint {
693 tool: (*name).to_owned(),
694 version,
695 confidence: conf,
696 evidence: vec![format!("PVD field contains '{needle}'")],
697 };
698 }
699 }
700 ToolFingerprint {
701 tool: "unknown".to_owned(),
702 version: None,
703 confidence: "LOW",
704 evidence: Vec::new(),
705 }
706 }
707
708 fn read_path_table_bytes(&mut self, lba: u32) -> Result<Vec<u8>, IsoError> {
711 let size = self.pvd.path_table_size as usize;
712 let sectors = size.div_ceil(2048).max(1);
713 let mut data = Vec::with_capacity(sectors * 2048);
714 for i in 0..sectors {
715 let raw = self.read_sector_raw(u64::from(lba) + i as u64)?;
716 data.extend_from_slice(&raw);
717 }
718 data.truncate(size.min(data.len()));
719 Ok(data)
720 }
721
722 pub fn audit_path_table(&mut self) -> Result<PathTableAudit, IsoError> {
727 use path_table::parse_l_path_table;
728 use std::collections::HashSet;
729
730 let pt_data = self.read_path_table_bytes(self.pvd.l_path_table_lba)?;
732 let pt_entries = parse_l_path_table(&pt_data).unwrap_or_default();
733 let path_table_lbas: Vec<u32> = pt_entries.iter().map(|e| e.lba).collect();
734 let pt_set: HashSet<u32> = path_table_lbas.iter().copied().collect();
735
736 let tree_entries = self.walk()?;
738 let mut tree_set: HashSet<u32> =
739 tree_entries.iter().filter(|e| e.record.is_dir()).map(|e| e.record.lba).collect();
740 tree_set.insert(self.pvd.root_dir_lba);
741
742 let mut tree_lbas: Vec<u32> = tree_set.iter().copied().collect();
743 tree_lbas.sort_unstable();
744
745 let mut phantom_lbas: Vec<u32> = pt_set.difference(&tree_set).copied().collect();
746 let mut ghost_lbas: Vec<u32> = tree_set.difference(&pt_set).copied().collect();
747 phantom_lbas.sort_unstable();
748 ghost_lbas.sort_unstable();
749
750 Ok(PathTableAudit { path_table_lbas, tree_lbas, phantom_lbas, ghost_lbas })
751 }
752
753 pub fn audit_path_table_endian(
766 &mut self,
767 ) -> Result<Vec<path_table::PathTableMismatch>, IsoError> {
768 use path_table::{parse_l_path_table, parse_m_path_table, validate_path_tables};
769
770 let l_lba = self.pvd.l_path_table_lba;
771 let m_lba = self.pvd.m_path_table_lba;
772 if l_lba == 0 || m_lba == 0 {
773 return Ok(Vec::new());
774 }
775 let l_bytes = self.read_path_table_bytes(l_lba)?;
776 let m_bytes = self.read_path_table_bytes(m_lba)?;
777 let l = parse_l_path_table(&l_bytes).unwrap_or_default();
778 let m = parse_m_path_table(&m_bytes).unwrap_or_default();
779 Ok(validate_path_tables(&l, &m))
780 }
781
782 pub fn recover_lost_files(&mut self) -> Result<Vec<LostFile>, IsoError> {
791 let phantom = self.audit_path_table()?.phantom_lbas;
792 let mut lost = Vec::new();
793 for dir_lba in phantom {
794 let probe = self.read_dir(dir_lba, 2048)?;
796 let dir_size = probe.first().map_or(2048, |r| r.size.max(2048));
797 let records = if dir_size > 2048 { self.read_dir(dir_lba, dir_size)? } else { probe };
798 for r in records {
799 if !r.is_dir() {
800 lost.push(LostFile {
801 name: r.iso_name(),
802 lba: r.lba,
803 size: r.size,
804 parent_lba: dir_lba,
805 });
806 }
807 }
808 }
809 Ok(lost)
810 }
811
812 pub fn audit_both_endian(&mut self) -> Result<Vec<audit::BothEndianMismatch>, IsoError> {
813 use audit::BothEndianMismatch;
814 let mut out: Vec<BothEndianMismatch> = Vec::new();
815
816 let pvd_raw = self.read_sector_raw(16)?;
818 let pvd_off = self.mode.user_data_pos(16);
819
820 macro_rules! chk32 {
821 ($off:expr, $name:expr) => {{
822 let le = u32::from_le_bytes(pvd_raw[$off..$off + 4].try_into().unwrap()) as u64;
823 let be = u32::from_be_bytes(pvd_raw[$off + 4..$off + 8].try_into().unwrap()) as u64;
824 if le != be {
825 out.push(BothEndianMismatch {
826 context: "PVD".into(),
827 field: $name.into(),
828 byte_offset: pvd_off + $off as u64,
829 le_val: le,
830 be_val: be,
831 });
832 }
833 }};
834 }
835 macro_rules! chk16 {
836 ($off:expr, $name:expr) => {{
837 let le = u16::from_le_bytes(pvd_raw[$off..$off + 2].try_into().unwrap()) as u64;
838 let be = u16::from_be_bytes(pvd_raw[$off + 2..$off + 4].try_into().unwrap()) as u64;
839 if le != be {
840 out.push(BothEndianMismatch {
841 context: "PVD".into(),
842 field: $name.into(),
843 byte_offset: pvd_off + $off as u64,
844 le_val: le,
845 be_val: be,
846 });
847 }
848 }};
849 }
850 chk32!(80, "volume_space_size");
851 chk16!(120, "volume_set_size");
852 chk16!(124, "volume_sequence_number");
853 chk16!(128, "logical_block_size");
854 chk32!(132, "path_table_size");
855
856 let entries = self.walk()?;
858 let mut seen = std::collections::HashSet::new();
859 seen.insert(self.pvd.root_dir_lba);
861 for e in &entries {
862 if e.record.is_dir() {
863 seen.insert(e.record.lba);
864 }
865 }
866 for dir_lba in seen {
867 let raw = match self.read_sector_raw(dir_lba as u64) {
871 Ok(raw) => raw,
872 Err(IsoError::Io(io)) if io.kind() == std::io::ErrorKind::UnexpectedEof => continue,
873 Err(e) => return Err(e),
874 };
875 let sec_off = self.mode.user_data_pos(dir_lba as u64);
876 let ctx = format!("dir:lba={dir_lba}");
877 let mut pos = 0usize;
878 while pos < raw.len() {
879 let rl = raw[pos] as usize;
880 if rl == 0 {
881 pos += 1;
882 continue;
883 }
884 if rl < 33 || pos + rl > raw.len() {
885 break;
886 }
887 let le = u32::from_le_bytes(raw[pos + 2..pos + 6].try_into().unwrap()) as u64;
889 let be = u32::from_be_bytes(raw[pos + 6..pos + 10].try_into().unwrap()) as u64;
890 if le != be {
891 out.push(BothEndianMismatch {
892 context: ctx.clone(),
893 field: "entry_lba".into(),
894 byte_offset: sec_off + pos as u64 + 2,
895 le_val: le,
896 be_val: be,
897 });
898 }
899 let le = u32::from_le_bytes(raw[pos + 10..pos + 14].try_into().unwrap()) as u64;
901 let be = u32::from_be_bytes(raw[pos + 14..pos + 18].try_into().unwrap()) as u64;
902 if le != be {
903 out.push(BothEndianMismatch {
904 context: ctx.clone(),
905 field: "entry_size".into(),
906 byte_offset: sec_off + pos as u64 + 10,
907 le_val: le,
908 be_val: be,
909 });
910 }
911 pos += rl;
912 }
913 }
914 Ok(out)
915 }
916
917 pub fn audit_pre_system(&mut self) -> Result<Vec<audit::PreSysHit>, IsoError> {
918 const MAGIC: &[(&[u8], &str)] = &[
919 (b"MZ", "MZ/PE"),
920 (&[0x7F, b'E', b'L', b'F'], "ELF"),
921 (&[b'P', b'K', 0x03, 0x04], "ZIP"),
922 (b"%PDF", "PDF"),
923 (&[0x37, 0x7A, 0xBC, 0xAF], "7z"),
924 ];
925 let mut out = Vec::new();
926 for sector in 0u8..16 {
927 let raw = self.read_sector_raw(sector as u64)?;
928 if raw.iter().all(|&b| b == 0) {
929 continue;
930 }
931 let kind = MAGIC
932 .iter()
933 .find(|(sig, _)| raw.starts_with(sig))
934 .map(|(_, k)| *k)
935 .unwrap_or("non-zero");
936 out.push(audit::PreSysHit { sector, kind });
937 }
938 Ok(out)
939 }
940
941 pub fn audit_symlinks(&mut self) -> Result<Vec<audit::SymlinkIssue>, IsoError> {
942 let entries = self.walk()?;
943 let mut out = Vec::new();
944 for e in entries {
945 if e.record.is_dir() {
946 continue;
947 }
948 if let Some(target) = rock_ridge::symlink_target(&e.record.system_use) {
949 let issue = if target.contains("..") {
950 "path-traversal"
951 } else if target.starts_with('/') {
952 "absolute"
953 } else {
954 continue;
955 };
956 out.push(audit::SymlinkIssue { entry_path: e.path, target, issue });
957 }
958 }
959 Ok(out)
960 }
961
962 pub fn audit_file_slack(&mut self) -> Result<Vec<audit::SlackHit>, IsoError> {
963 let entries = self.walk()?;
964 let mut out = Vec::new();
965 for e in entries {
966 if e.record.is_dir() {
967 continue;
968 }
969 let size = e.record.size;
970 let remainder = size % 2048;
971 let slack_bytes = if remainder == 0 { 0 } else { 2048 - remainder };
972 if slack_bytes == 0 {
973 out.push(audit::SlackHit {
974 entry_path: e.path,
975 lba: e.record.lba,
976 file_size: size,
977 slack_bytes: 0,
978 nonzero: false,
979 });
980 continue;
981 }
982 let sectors = (size as u64).div_ceil(2048);
983 let last_lba = e.record.lba as u64 + sectors - 1;
984 let raw = match self.read_sector_raw(last_lba) {
989 Ok(raw) => raw,
990 Err(IsoError::Io(io)) if io.kind() == std::io::ErrorKind::UnexpectedEof => continue,
991 Err(e) => return Err(e),
992 };
993 let data_end = remainder as usize;
994 let nonzero = raw[data_end..].iter().any(|&b| b != 0);
995 out.push(audit::SlackHit {
996 entry_path: e.path,
997 lba: e.record.lba,
998 file_size: size,
999 slack_bytes,
1000 nonzero,
1001 });
1002 }
1003 Ok(out)
1004 }
1005
1006 pub fn timeline(&mut self) -> Result<Vec<TimelineEntry>, IsoError> {
1011 let entries = self.walk()?;
1012 let mut out: Vec<TimelineEntry> = entries
1013 .into_iter()
1014 .filter(|e| !e.record.is_dir())
1015 .map(|e| {
1016 let modify_ts =
1017 rock_ridge::timestamps(&e.record.system_use).and_then(|ts| ts.modify);
1018 let anomaly = modify_ts.and_then(|ts| {
1019 if ts[0] == 70
1020 && ts[1] == 1
1021 && ts[2] == 1
1022 && ts[3] == 0
1023 && ts[4] == 0
1024 && ts[5] == 0
1025 {
1026 Some("epoch-date".to_string())
1027 } else {
1028 None
1029 }
1030 });
1031 TimelineEntry {
1032 path: e.path,
1033 is_dir: false,
1034 size: e.record.size,
1035 modify_ts,
1036 anomaly,
1037 }
1038 })
1039 .collect();
1040 out.sort_by_key(|a| a.modify_ts);
1042 Ok(out)
1043 }
1044
1045 pub fn hashlist(&mut self) -> Result<Vec<FileHash>, IsoError> {
1046 use sha2::{Digest, Sha256};
1047 let entries = self.walk()?;
1048 let mut out: Vec<FileHash> = Vec::new();
1049 for e in entries {
1050 if e.record.is_dir() {
1051 continue;
1052 }
1053 let data = self.read_file_entry(&e.record)?;
1054 let hash = Sha256::digest(&data);
1055 let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
1056 out.push(FileHash { path: e.path, size: e.record.size, sha256_hex: hex });
1057 }
1058 out.sort_by(|a, b| a.path.cmp(&b.path));
1059 Ok(out)
1060 }
1061
1062 pub fn audit_sector_gaps(&mut self) -> Result<Vec<audit::GapHit>, IsoError> {
1063 let total = self.volume_space_size();
1064 let entries = self.walk()?;
1065
1066 let mut alloc: std::collections::HashSet<u32> = (0..=15).collect();
1070 for lba in 16u32..512 {
1071 let raw = match self.read_sector_raw(lba as u64) {
1072 Ok(r) => r,
1073 Err(_) => break,
1074 };
1075 if &raw[1..6] != b"CD001" {
1076 break;
1077 }
1078 alloc.insert(lba);
1079 if raw[0] == 0xFF {
1080 break; }
1082 }
1083 alloc.insert(self.pvd.root_dir_lba);
1084
1085 let pt_sectors = (self.pvd.path_table_size as u64).div_ceil(2048).max(1) as u32;
1089 for base in [self.pvd.l_path_table_lba, self.pvd.m_path_table_lba] {
1090 for s in 0..pt_sectors {
1091 alloc.insert(base + s);
1092 }
1093 }
1094
1095 let mark_ce = |alloc: &mut std::collections::HashSet<u32>, su: &[u8]| {
1097 if let Some(ce) = rock_ridge::continuation(su) {
1098 let end = ce.offset.saturating_add(ce.len);
1099 let ce_sectors = (end as u64).div_ceil(2048).max(1) as u32;
1100 for s in 0..ce_sectors {
1101 alloc.insert(ce.lba + s);
1102 }
1103 }
1104 };
1105
1106 for e in &entries {
1107 let sectors = (e.record.size as u64).div_ceil(2048) as u32;
1108 for s in 0..sectors.max(1) {
1109 alloc.insert(e.record.lba + s);
1110 }
1111 mark_ce(&mut alloc, &e.record.system_use);
1113 }
1114
1115 if let Ok(root_records) = self.read_dir(self.pvd.root_dir_lba, self.pvd.root_dir_size) {
1119 for rec in &root_records {
1120 mark_ce(&mut alloc, &rec.system_use);
1121 }
1122 }
1123 if let Ok(raw) = self.read_sector_raw(self.pvd.root_dir_lba as u64) {
1126 let len = raw[0] as usize;
1127 if len >= 34 && len <= raw.len() {
1128 let name_len = raw[32] as usize;
1129 let su_start = 33 + name_len + (if name_len % 2 == 0 { 1 } else { 0 });
1130 if su_start < len {
1131 mark_ce(&mut alloc, &raw[su_start..len]);
1132 }
1133 }
1134 }
1135
1136 if let Some(svd) = self.svd.as_ref() {
1141 let svd_root_lba = svd.root_dir_lba;
1142 let svd_root_size = svd.root_dir_size;
1143 let svd_pt_sectors = (svd.path_table_size as u64).div_ceil(2048).max(1) as u32;
1144 let svd_l = svd.l_path_table_lba;
1145 let svd_m = svd.m_path_table_lba;
1146 for base in [svd_l, svd_m] {
1147 if base != 0 {
1148 for s in 0..svd_pt_sectors {
1149 alloc.insert(base + s);
1150 }
1151 }
1152 }
1153 let mut worklist = vec![(svd_root_lba, svd_root_size)];
1155 let mut visited = std::collections::HashSet::new();
1156 while let Some((lba, size)) = worklist.pop() {
1157 if !visited.insert(lba) {
1158 continue;
1159 }
1160 let dir_sectors = (size as u64).div_ceil(2048).max(1) as u32;
1161 for s in 0..dir_sectors {
1162 alloc.insert(lba + s);
1163 }
1164 if let Ok(children) = self.read_dir(lba, size) {
1165 for c in children {
1166 if c.is_dir() {
1167 worklist.push((c.lba, c.size));
1168 } else {
1169 let fs = (c.size as u64).div_ceil(2048).max(1) as u32;
1170 for s in 0..fs {
1171 alloc.insert(c.lba + s);
1172 }
1173 }
1174 }
1175 }
1176 }
1177 }
1178
1179 if let Some(cat) = self.boot_catalog_lba {
1181 alloc.insert(cat);
1182 }
1183 if let Ok(boot) = self.boot_entries() {
1184 for b in &boot {
1185 let bytes = b.sector_count as u64 * 512;
1188 let bs = bytes.div_ceil(2048).max(1) as u32;
1189 for s in 0..bs {
1190 alloc.insert(b.lba + s);
1191 }
1192 }
1193 }
1194
1195 let cap = total.min(512);
1196 let mut out = Vec::new();
1197 for lba in 0..cap {
1198 if alloc.contains(&lba) {
1199 continue;
1200 }
1201 let raw = self.read_sector_raw(lba as u64)?;
1202 let nonzero = raw.iter().any(|&b| b != 0);
1203 out.push(audit::GapHit { lba, nonzero });
1204 }
1205 Ok(out)
1206 }
1207}
1208
1209fn extract_version(s: &str) -> Option<String> {
1216 let bytes = s.as_bytes();
1217 let mut i = 0;
1218 while i < bytes.len() {
1219 if bytes[i].is_ascii_digit() {
1220 let start = i;
1221 while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
1222 i += 1;
1223 }
1224 let run = &s[start..i];
1225 if run.contains('.') {
1226 return Some(run.trim_end_matches('.').to_owned());
1227 }
1228 } else {
1229 i += 1;
1230 }
1231 }
1232 None
1233}
1234
1235fn scan_sessions<R: Read + Seek>(reader: &mut R, mode: SectorMode) -> Result<Vec<u64>, IsoError> {
1237 let mut lbas = Vec::new();
1238 let mut buf = [0u8; 2048];
1239
1240 for lba in 16u64..4096 {
1241 let pos = mode.user_data_pos(lba);
1242 reader.seek(SeekFrom::Start(pos))?;
1243 match reader.read_exact(&mut buf) {
1244 Ok(()) => {}
1245 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1246 Err(e) => return Err(e.into()),
1247 }
1248 if buf[0] == 0x01 && &buf[1..6] == b"CD001" && buf[6] == 0x01 {
1249 lbas.push(lba);
1250 }
1251 if buf[0] == TERMINATOR_TYPE && &buf[1..6] == b"CD001" {
1252 }
1255 }
1256 Ok(lbas)
1257}
1258
1259fn detect_udf<R: Read + Seek>(reader: &mut R, mode: SectorMode) -> Result<bool, IsoError> {
1263 let mut buf = [0u8; 2048];
1264 for lba in 16u64..32 {
1265 let pos = mode.user_data_pos(lba);
1266 reader.seek(SeekFrom::Start(pos))?;
1267 match reader.read_exact(&mut buf) {
1268 Ok(()) => {}
1269 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
1270 Err(e) => return Err(e.into()),
1271 }
1272 if &buf[1..6] == b"NSR02" || &buf[1..6] == b"NSR03" {
1273 return Ok(true);
1274 }
1275 }
1276 Ok(false)
1277}
1278
1279type VolumeDescriptors =
1282 (PrimaryVolumeDescriptor, Option<SupplementaryVolumeDescriptor>, Option<u32>, bool, usize);
1283
1284fn read_volume_descriptors<R: Read + Seek>(
1286 reader: &mut R,
1287 mode: SectorMode,
1288 first_pvd_lba: u64,
1289) -> Result<VolumeDescriptors, IsoError> {
1290 let mut buf = [0u8; 2048];
1291 let mut pvd: Option<PrimaryVolumeDescriptor> = None;
1292 let mut svd: Option<SupplementaryVolumeDescriptor> = None;
1293 let mut boot_cat: Option<u32> = None;
1294 let mut has_rr = false;
1295 let mut sp_skip = 0usize;
1296
1297 let mut lba = first_pvd_lba;
1298 loop {
1299 read_sector_data(reader, mode, lba, &mut buf)?;
1300 match buf[0] {
1301 PVD_TYPE => {
1302 let p = PrimaryVolumeDescriptor::parse(&buf)?;
1303 if !has_rr {
1305 let (rr, skip) = check_rock_ridge(reader, mode, p.root_dir_lba)?;
1306 has_rr = rr;
1307 sp_skip = skip;
1308 }
1309 pvd = Some(p);
1310 }
1311 SVD_TYPE => {
1312 if let Ok(s) = SupplementaryVolumeDescriptor::parse(&buf) {
1313 let keep = s.is_joliet
1317 || (s.is_enhanced() && svd.as_ref().is_none_or(|e| !e.is_joliet));
1318 if keep {
1319 svd = Some(s);
1320 }
1321 }
1322 }
1323 BOOT_RECORD_TYPE => {
1324 boot_cat = boot_catalog_lba(&buf);
1325 }
1326 TERMINATOR_TYPE => break,
1327 _ => {}
1328 }
1329 lba += 1;
1330 }
1331
1332 pvd.ok_or_else(|| IsoError::BadDescriptor("no PVD found in VD chain".into()))
1333 .map(|p| (p, svd, boot_cat, has_rr, sp_skip))
1334}
1335
1336fn check_rock_ridge<R: Read + Seek>(
1341 reader: &mut R,
1342 mode: SectorMode,
1343 root_dir_lba: u32,
1344) -> Result<(bool, usize), IsoError> {
1345 let mut buf = [0u8; 2048];
1346 read_sector_data(reader, mode, root_dir_lba as u64, &mut buf)?;
1347 let offset = 0usize;
1348 if buf[offset] == 0 {
1349 return Ok((false, 0));
1350 }
1351 let len = buf[offset] as usize;
1352 if len < 34 {
1353 return Ok((false, 0));
1354 }
1355 let name_len = buf[offset + 32] as usize;
1356 let su_start = 33 + name_len + (if name_len % 2 == 0 { 1 } else { 0 });
1357 if su_start >= len {
1358 return Ok((false, 0));
1359 }
1360 let su = &buf[offset + su_start..offset + len];
1361 let found = has_sp_entry(su);
1362 let skip = if found { extract_sp_skip(su) } else { 0 };
1363 Ok((found, skip))
1364}