use iso9660_forensic::findings::AnomalyKind;
use iso9660_forensic::{analyse, IsoReader};
use std::io::Cursor;
fn make_two_session_iso() -> Vec<u8> {
const S: usize = 2048;
let mut img = vec![0u8; 30 * S];
let write_pvd = |img: &mut Vec<u8>, pvd_lba: usize, root_lba: u32| {
let p = &mut img[pvd_lba * S..(pvd_lba + 1) * S];
p[0] = 0x01;
p[1..6].copy_from_slice(b"CD001");
p[6] = 0x01;
p[80..84].copy_from_slice(&30u32.to_le_bytes());
p[84..88].copy_from_slice(&30u32.to_be_bytes());
p[128..130].copy_from_slice(&2048u16.to_le_bytes());
p[130..132].copy_from_slice(&2048u16.to_be_bytes());
p[132..136].copy_from_slice(&10u32.to_le_bytes());
p[140..144].copy_from_slice(&1u32.to_le_bytes());
p[148..152].copy_from_slice(&1u32.to_be_bytes());
p[156] = 34;
p[158..162].copy_from_slice(&root_lba.to_le_bytes());
p[162..166].copy_from_slice(&root_lba.to_be_bytes());
p[166..170].copy_from_slice(&2048u32.to_le_bytes());
p[170..174].copy_from_slice(&2048u32.to_be_bytes());
p[181] = 0x02;
p[188] = 1;
};
let write_term = |img: &mut Vec<u8>, lba: usize| {
let t = &mut img[lba * S..(lba + 1) * S];
t[0] = 0xFF;
t[1..6].copy_from_slice(b"CD001");
t[6] = 0x01;
};
let write_dir = |img: &mut Vec<u8>, dir_lba: usize, file_name: &[u8]| {
let d = &mut img[dir_lba * S..(dir_lba + 1) * S];
d[0] = 34;
d[2..6].copy_from_slice(&(dir_lba as u32).to_le_bytes());
d[10..14].copy_from_slice(&2048u32.to_le_bytes());
d[25] = 0x02;
d[32] = 1;
let o = 34;
d[o] = 34;
d[o + 2..o + 6].copy_from_slice(&(dir_lba as u32).to_le_bytes());
d[o + 10..o + 14].copy_from_slice(&2048u32.to_le_bytes());
d[o + 25] = 0x02;
d[o + 32] = 1;
d[o + 33] = 0x01;
let nl = file_name.len();
let rec_len = 33 + nl + (if nl % 2 == 0 { 1 } else { 0 });
let o = 68;
d[o] = rec_len as u8;
d[o + 32] = nl as u8;
d[o + 33..o + 33 + nl].copy_from_slice(file_name);
};
write_pvd(&mut img, 16, 18);
write_term(&mut img, 17);
write_dir(&mut img, 18, b"SESSION0");
write_pvd(&mut img, 24, 26);
write_term(&mut img, 25);
write_dir(&mut img, 26, b"SESSION1");
img
}
#[test]
fn two_session_iso_detects_both() {
let img = make_two_session_iso();
let reader = IsoReader::open(Cursor::new(img)).unwrap();
assert_eq!(reader.session_count(), 2, "must detect 2 sessions");
}
#[test]
fn read_session_root_dir_session0() {
let img = make_two_session_iso();
let mut reader = IsoReader::open(Cursor::new(img)).unwrap();
let records = reader.read_session_root_dir(0).unwrap();
let names: Vec<String> = records.iter().map(|r| r.iso_name()).collect();
assert!(
names.contains(&"SESSION0".to_string()),
"session 0 root dir must contain SESSION0, got: {names:?}"
);
assert!(!names.contains(&"SESSION1".to_string()), "session 0 must NOT contain SESSION1");
}
#[test]
fn read_session_root_dir_session1() {
let img = make_two_session_iso();
let mut reader = IsoReader::open(Cursor::new(img)).unwrap();
let records = reader.read_session_root_dir(1).unwrap();
let names: Vec<String> = records.iter().map(|r| r.iso_name()).collect();
assert!(
names.contains(&"SESSION1".to_string()),
"session 1 root dir must contain SESSION1, got: {names:?}"
);
}
#[test]
fn read_session_out_of_bounds_returns_error() {
let img = make_two_session_iso();
let mut reader = IsoReader::open(Cursor::new(img)).unwrap();
assert!(
reader.read_session_root_dir(99).is_err(),
"out-of-bounds session index must return an error"
);
}
#[test]
fn default_read_root_dir_uses_last_session() {
let img = make_two_session_iso();
let mut reader = IsoReader::open(Cursor::new(img)).unwrap();
let records = reader.read_root_dir().unwrap();
let names: Vec<String> = records.iter().map(|r| r.iso_name()).collect();
assert!(
names.contains(&"SESSION1".to_string()),
"default root dir must be the last session (SESSION1), got: {names:?}"
);
}
#[test]
fn walk_session_returns_each_session_tree() {
let img = make_two_session_iso();
let mut r = IsoReader::open(Cursor::new(img)).unwrap();
let s0: Vec<String> = r.walk_session(0).unwrap().iter().map(|e| e.path.clone()).collect();
let s1: Vec<String> = r.walk_session(1).unwrap().iter().map(|e| e.path.clone()).collect();
assert!(s0.iter().any(|p| p.contains("SESSION0")), "session 0 tree: {s0:?}");
assert!(s1.iter().any(|p| p.contains("SESSION1")), "session 1 tree: {s1:?}");
assert!(!s1.iter().any(|p| p.contains("SESSION0")), "active session must not list SESSION0");
let active: Vec<String> = r.walk().unwrap().iter().map(|e| e.path.clone()).collect();
assert_eq!(active, s1, "walk() must equal walk_session(last session)");
}
#[test]
fn walk_session_out_of_range_errors() {
let img = make_two_session_iso();
let mut r = IsoReader::open(Cursor::new(img)).unwrap();
assert!(r.walk_session(99).is_err(), "out-of-range session index must error");
}
#[test]
fn superseded_file_from_earlier_session_is_flagged() {
let img = make_two_session_iso();
let a = analyse(&mut Cursor::new(img)).expect("analyse");
let f = a
.anomalies
.iter()
.find(|x| x.code == "ISO-SUPERSEDED-FILE")
.expect("superseded file should be flagged");
match &f.kind {
AnomalyKind::SupersededFile { entry_path, session, status, .. } => {
assert!(entry_path.contains("SESSION0"), "{:?}", f.kind);
assert_eq!(*session, 0, "from the oldest session");
assert_eq!(status.as_str(), "deleted", "absent from the active tree");
}
other => panic!("wrong kind: {other:?}"),
}
}