use crate::tree::TreeNode;
use crate::Result;
use std::io::{Read, Seek, SeekFrom};
const SECTOR_SIZE: u64 = 2048;
const PRIMARY_VOLUME_DESCRIPTOR_SECTOR: u64 = 16;
#[derive(Debug, Clone)]
pub struct DirectoryRecord {
pub extent_location: u32,
pub data_length: u32,
pub is_directory: bool,
pub filename: String,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum VolumeDescriptorType {
Primary,
Joliet,
}
pub fn parse_iso9660<R: Read + Seek>(file: &mut R) -> Result<TreeNode> {
parse_iso9660_verbose(file, false)
}
pub fn parse_iso9660_verbose<R: Read + Seek>(file: &mut R, verbose: bool) -> Result<TreeNode> {
let mut primary_vd: Option<Vec<u8>> = None;
let mut joliet_vd: Option<Vec<u8>> = None;
let mut sector = PRIMARY_VOLUME_DESCRIPTOR_SECTOR;
loop {
file.seek(SeekFrom::Start(sector * SECTOR_SIZE))?;
let mut buffer = vec![0u8; SECTOR_SIZE as usize];
if file.read_exact(&mut buffer).is_err() {
break;
}
if &buffer[1..6] != b"CD001" {
if sector == PRIMARY_VOLUME_DESCRIPTOR_SECTOR {
if verbose {
eprintln!(
" ISO 9660 signature 'CD001' not found at sector {}. Found: {:?}",
sector,
String::from_utf8_lossy(&buffer[1..6])
);
}
return Err("Not a valid ISO 9660 filesystem".into());
}
break;
}
let vd_type = buffer[0];
match vd_type {
1 => {
if verbose {
eprintln!(" Found Primary Volume Descriptor at sector {}", sector);
}
primary_vd = Some(buffer);
}
2 => {
let escape = &buffer[88..91];
if escape == b"%/@" || escape == b"%/C" || escape == b"%/E" {
if verbose {
eprintln!(" Found Joliet Volume Descriptor at sector {}", sector);
}
joliet_vd = Some(buffer);
}
}
255 => {
if verbose {
eprintln!(" Volume Descriptor Set Terminator at sector {}", sector);
}
break;
}
_ => {}
}
sector += 1;
}
let (buffer, vd_type) = if let Some(buf) = joliet_vd {
(buf, VolumeDescriptorType::Joliet)
} else if let Some(buf) = primary_vd {
(buf, VolumeDescriptorType::Primary)
} else {
return Err("Not a valid ISO 9660 filesystem".into());
};
if verbose {
eprintln!(
" Using {} Volume Descriptor",
if vd_type == VolumeDescriptorType::Joliet {
"Joliet"
} else {
"Primary"
}
);
}
let root_record = parse_directory_record(&buffer[156..], vd_type)?;
if verbose {
eprintln!(
" Root directory at sector {}, size {} bytes",
root_record.extent_location, root_record.data_length
);
}
let mut root_node = TreeNode::new_directory("/".to_string());
let use_rock_ridge = if vd_type == VolumeDescriptorType::Primary {
detect_rock_ridge(file, &root_record)?
} else {
false
};
if verbose && use_rock_ridge {
eprintln!(" Rock Ridge extensions detected");
}
parse_directory(
file,
&root_record,
&mut root_node,
vd_type,
use_rock_ridge,
verbose,
)?;
root_node.calculate_directory_size();
Ok(root_node)
}
fn detect_rock_ridge<R: Read + Seek>(file: &mut R, dir_record: &DirectoryRecord) -> Result<bool> {
file.seek(SeekFrom::Start(
dir_record.extent_location as u64 * SECTOR_SIZE,
))?;
let mut buffer = vec![0u8; dir_record.data_length.min(4096) as usize];
file.read_exact(&mut buffer)?;
if buffer.len() < 34 {
return Ok(false);
}
let record_length = buffer[0] as usize;
let filename_length = buffer[32] as usize;
let su_start = 33 + filename_length + ((filename_length + 1) % 2);
if su_start + 7 <= record_length && record_length <= buffer.len() {
let sig = &buffer[su_start..su_start + 2];
if sig == b"SP" || sig == b"RR" {
return Ok(true);
}
if sig == b"NM" || sig == b"PX" {
return Ok(true);
}
}
Ok(false)
}
fn parse_directory_record(data: &[u8], vd_type: VolumeDescriptorType) -> Result<DirectoryRecord> {
if data.len() < 34 {
return Err("Directory record too short".into());
}
let length = data[0];
if length == 0 {
return Err("Zero-length directory record".into());
}
let extent_location = u32::from_le_bytes([data[2], data[3], data[4], data[5]]);
let data_length = u32::from_le_bytes([data[10], data[11], data[12], data[13]]);
let file_flags = data[25];
let filename_length = data[32] as usize;
if 33 + filename_length > data.len() {
return Err("Directory record filename extends past buffer".into());
}
let is_directory = (file_flags & 0x02) != 0;
let filename = if filename_length == 0 || (filename_length == 1 && data[33] == 0) {
".".to_string()
} else if filename_length == 1 && data[33] == 1 {
"..".to_string()
} else if vd_type == VolumeDescriptorType::Joliet {
let utf16_data: Vec<u16> = data[33..33 + filename_length]
.chunks_exact(2)
.map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]]))
.collect();
let raw_name = String::from_utf16_lossy(&utf16_data);
if let Some(semicolon_pos) = raw_name.find(';') {
raw_name[..semicolon_pos].to_string()
} else {
raw_name
}
} else {
let raw_name = String::from_utf8_lossy(&data[33..33 + filename_length]);
let cleaned_name = if let Some(semicolon_pos) = raw_name.find(';') {
&raw_name[..semicolon_pos]
} else {
&raw_name
};
cleaned_name.trim_end_matches('.').to_string()
};
Ok(DirectoryRecord {
extent_location,
data_length,
is_directory,
filename,
})
}
fn extract_rock_ridge_name(
data: &[u8],
record_length: usize,
filename_length: usize,
) -> Option<String> {
let su_start = 33 + filename_length + ((filename_length + 1) % 2);
if su_start >= record_length {
return None;
}
let su_area = &data[su_start..record_length];
let mut offset = 0;
let mut name_parts: Vec<u8> = Vec::new();
while offset + 4 <= su_area.len() {
let sig = &su_area[offset..offset + 2];
let entry_len = su_area[offset + 2] as usize;
if entry_len < 4 || offset + entry_len > su_area.len() {
break;
}
if sig == b"NM" {
let flags = su_area[offset + 4];
if flags & 0x02 != 0 {
} else if flags & 0x04 != 0 {
} else {
name_parts.extend_from_slice(&su_area[offset + 5..offset + entry_len]);
}
}
offset += entry_len;
}
if name_parts.is_empty() {
None
} else {
Some(String::from_utf8_lossy(&name_parts).to_string())
}
}
fn parse_directory<R: Read + Seek>(
file: &mut R,
dir_record: &DirectoryRecord,
parent_node: &mut TreeNode,
vd_type: VolumeDescriptorType,
use_rock_ridge: bool,
verbose: bool,
) -> Result<()> {
if !dir_record.is_directory || dir_record.data_length == 0 {
return Ok(());
}
file.seek(SeekFrom::Start(
dir_record.extent_location as u64 * SECTOR_SIZE,
))?;
let mut buffer = vec![0u8; dir_record.data_length as usize];
file.read_exact(&mut buffer)?;
let mut offset = 0;
while offset < buffer.len() {
if buffer[offset] == 0 {
let next_sector = (offset / SECTOR_SIZE as usize + 1) * SECTOR_SIZE as usize;
if next_sector <= offset {
offset += 1;
} else {
offset = next_sector;
}
continue;
}
let record_length = buffer[offset] as usize;
if record_length == 0 || offset + record_length > buffer.len() {
break;
}
if let Ok(mut record) = parse_directory_record(&buffer[offset..], vd_type) {
if use_rock_ridge
&& vd_type == VolumeDescriptorType::Primary
&& record.filename != "."
&& record.filename != ".."
{
let filename_length = buffer[offset + 32] as usize;
if let Some(rr_name) = extract_rock_ridge_name(
&buffer[offset..offset + record_length],
record_length,
filename_length,
) {
record.filename = rr_name;
}
}
if record.filename != "." && record.filename != ".." {
if verbose {
eprintln!(
" Found {}: {}",
if record.is_directory { "dir" } else { "file" },
record.filename
);
}
if record.is_directory {
let mut dir_node = TreeNode::new_directory(record.filename.clone());
parse_directory(
file,
&record,
&mut dir_node,
vd_type,
use_rock_ridge,
verbose,
)?;
parent_node.add_child(dir_node);
} else {
let file_node = TreeNode::new_file_with_location(
record.filename.clone(),
record.data_length as u64,
record.extent_location as u64 * SECTOR_SIZE,
record.data_length as u64,
);
parent_node.add_child(file_node);
}
}
}
offset += record_length;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
const S: usize = 2048;
fn make_iso_primary_only() -> Vec<u8> {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1; img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
img[pvd + 6] = 1;
let root_off = pvd + 156;
img[root_off] = 34; img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes()); img[root_off + 6..root_off + 10].copy_from_slice(&18u32.to_be_bytes()); img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes()); img[root_off + 14..root_off + 18].copy_from_slice(&(S as u32).to_be_bytes()); img[root_off + 25] = 0x02; img[root_off + 32] = 1; img[root_off + 33] = 0;
let vdst = 17 * S;
img[vdst] = 255; img[vdst + 1..vdst + 6].copy_from_slice(b"CD001");
let dir = 18 * S;
img[dir] = 34;
img[dir + 2..dir + 6].copy_from_slice(&18u32.to_le_bytes());
img[dir + 10..dir + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[dir + 25] = 0x02; img[dir + 32] = 1;
img[dir + 33] = 0;
let e1 = dir + 34;
img[e1] = 34;
img[e1 + 2..e1 + 6].copy_from_slice(&18u32.to_le_bytes());
img[e1 + 10..e1 + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[e1 + 25] = 0x02;
img[e1 + 32] = 1;
img[e1 + 33] = 1;
let e2 = e1 + 34;
let name = b"HELLO.TXT;1";
img[e2] = (33 + name.len()) as u8; img[e2 + 2..e2 + 6].copy_from_slice(&19u32.to_le_bytes()); img[e2 + 10..e2 + 14].copy_from_slice(&11u32.to_le_bytes()); img[e2 + 25] = 0x00; img[e2 + 32] = name.len() as u8;
img[e2 + 33..e2 + 33 + name.len()].copy_from_slice(name);
img[19 * S..19 * S + 11].copy_from_slice(b"Hello World");
img
}
fn make_iso_joliet() -> Vec<u8> {
let mut img = make_iso_primary_only();
img.resize(S * 22, 0);
let svd = 17 * S;
img[svd] = 2; img[svd + 1..svd + 6].copy_from_slice(b"CD001");
img[svd + 6] = 1;
img[svd + 88..svd + 91].copy_from_slice(b"%/@");
let jroot = svd + 156;
img[jroot] = 34;
img[jroot + 2..jroot + 6].copy_from_slice(&20u32.to_le_bytes());
img[jroot + 10..jroot + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[jroot + 25] = 0x02; img[jroot + 32] = 1;
img[jroot + 33] = 0;
let vdst = 18 * S;
img[vdst] = 255;
img[vdst + 1..vdst + 6].copy_from_slice(b"CD001");
let jdir = 20 * S;
img[jdir] = 34;
img[jdir + 2..jdir + 6].copy_from_slice(&20u32.to_le_bytes());
img[jdir + 10..jdir + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[jdir + 25] = 0x02;
img[jdir + 32] = 1;
img[jdir + 33] = 0;
let je1 = jdir + 34;
img[je1] = 34;
img[je1 + 2..je1 + 6].copy_from_slice(&20u32.to_le_bytes());
img[je1 + 10..je1 + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[je1 + 25] = 0x02;
img[je1 + 32] = 1;
img[je1 + 33] = 1;
let joliet_name: Vec<u8> = "hi.txt"
.encode_utf16()
.flat_map(|c| c.to_be_bytes())
.collect();
let je2 = je1 + 34;
img[je2] = (33 + joliet_name.len()) as u8;
img[je2 + 2..je2 + 6].copy_from_slice(&19u32.to_le_bytes()); img[je2 + 10..je2 + 14].copy_from_slice(&11u32.to_le_bytes());
img[je2 + 25] = 0x00; img[je2 + 32] = joliet_name.len() as u8;
img[je2 + 33..je2 + 33 + joliet_name.len()].copy_from_slice(&joliet_name);
img
}
#[test]
fn directory_record_too_short_errors() {
let buf = [0u8; 10]; assert!(parse_directory_record(&buf, VolumeDescriptorType::Primary).is_err());
}
#[test]
fn directory_record_zero_length_errors() {
let mut buf = [0u8; 40];
buf[0] = 0; assert!(parse_directory_record(&buf, VolumeDescriptorType::Primary).is_err());
}
#[test]
fn directory_record_dot_entry() {
let mut buf = [0u8; 40];
buf[0] = 34;
buf[32] = 1; buf[33] = 0; let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert_eq!(rec.filename, ".");
}
#[test]
fn directory_record_dotdot_entry() {
let mut buf = [0u8; 40];
buf[0] = 34;
buf[32] = 1;
buf[33] = 1; let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert_eq!(rec.filename, "..");
}
#[test]
fn directory_record_primary_strips_version() {
let mut buf = [0u8; 50];
let name = b"FILE.TXT;1";
buf[0] = (33 + name.len()) as u8;
buf[32] = name.len() as u8;
buf[33..33 + name.len()].copy_from_slice(name);
let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert_eq!(rec.filename, "FILE.TXT");
}
#[test]
fn directory_record_joliet_unicode() {
let name: Vec<u8> = "hi".encode_utf16().flat_map(|c| c.to_be_bytes()).collect();
let mut buf = vec![0u8; 33 + name.len() + 2];
buf[0] = (33 + name.len()) as u8;
buf[32] = name.len() as u8;
buf[33..33 + name.len()].copy_from_slice(&name);
let rec = parse_directory_record(&buf, VolumeDescriptorType::Joliet).unwrap();
assert_eq!(rec.filename, "hi");
}
#[test]
fn directory_record_is_directory_flag() {
let mut buf = [0u8; 40];
buf[0] = 34;
buf[25] = 0x02; buf[32] = 1;
buf[33] = 0;
let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert!(rec.is_directory);
}
#[test]
fn parse_iso9660_rejects_non_iso() {
let mut c = Cursor::new(vec![0u8; S * 20]);
assert!(parse_iso9660(&mut c).is_err());
}
#[test]
fn parse_iso9660_verbose_rejects_non_iso() {
let mut c = Cursor::new(vec![0u8; S * 20]);
assert!(parse_iso9660_verbose(&mut c, true).is_err());
}
#[test]
fn parse_iso9660_no_vd_returns_err() {
let mut img = vec![0u8; S * 20];
img[16 * S] = 255; img[16 * S + 1..16 * S + 6].copy_from_slice(b"CD001");
let mut c = Cursor::new(img);
assert!(parse_iso9660(&mut c).is_err());
}
#[test]
fn parse_iso9660_primary_root_has_file() {
let img = make_iso_primary_only();
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("should parse");
assert_eq!(root.name, "/");
assert!(root.is_directory);
let node = root.find_node("/HELLO.TXT");
assert!(node.is_some(), "HELLO.TXT not found in root");
}
#[test]
fn parse_iso9660_primary_file_size() {
let img = make_iso_primary_only();
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).unwrap();
let node = root.find_node("/HELLO.TXT").unwrap();
assert_eq!(node.size, 11);
}
#[test]
fn parse_iso9660_joliet_prefers_joliet() {
let img = make_iso_joliet();
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("should parse");
assert!(
root.find_node("/hi.txt").is_some(),
"Joliet entry not found"
);
}
#[test]
fn parse_iso9660_verbose_primary_works() {
let img = make_iso_primary_only();
let mut c = Cursor::new(img);
let root = parse_iso9660_verbose(&mut c, true).unwrap();
assert_eq!(root.name, "/");
}
#[test]
fn rock_ridge_nm_entry_extracted() {
let mut data = vec![0u8; 60];
data[0] = 60; data[32] = 1; data[33] = 0; let su_off = 34;
data[su_off] = b'N';
data[su_off + 1] = b'M';
data[su_off + 2] = 13; data[su_off + 3] = 1; data[su_off + 4] = 0; data[su_off + 5..su_off + 13].copy_from_slice(b"longname");
let result = extract_rock_ridge_name(&data, 60, 1);
assert_eq!(result, Some("longname".to_string()));
}
#[test]
fn rock_ridge_nm_parent_flag_skipped() {
let mut data = vec![0u8; 60];
data[0] = 60;
data[32] = 1;
data[33] = 0;
let su_off = 34;
data[su_off] = b'N';
data[su_off + 1] = b'M';
data[su_off + 2] = 13;
data[su_off + 3] = 1;
data[su_off + 4] = 0x04; data[su_off + 5..su_off + 13].copy_from_slice(b"ignored!");
let result = extract_rock_ridge_name(&data, 60, 1);
assert_eq!(result, None);
}
#[test]
fn parse_linux_iso_succeeds() {
let path = std::path::Path::new("test_data/test_linux.iso");
if !path.exists() {
return;
}
let mut f = std::fs::File::open(path).unwrap();
let root = parse_iso9660(&mut f).expect("should parse test_linux.iso");
assert_eq!(root.name, "/");
assert!(!root.children.is_empty(), "root should have children");
}
#[test]
fn parse_macos_iso_joliet() {
let path = std::path::Path::new("test_data/test_macos.iso");
if !path.exists() {
return;
}
let mut f = std::fs::File::open(path).unwrap();
let root = parse_iso9660(&mut f).expect("should parse test_macos.iso");
assert_eq!(root.name, "/");
assert!(!root.children.is_empty());
}
#[test]
fn directory_record_filename_extends_past_buffer() {
let mut buf = [0u8; 50];
buf[0] = 50;
buf[32] = 100; assert!(parse_directory_record(&buf, VolumeDescriptorType::Primary).is_err());
}
#[test]
fn directory_record_primary_no_semicolon() {
let name = b"HELLO";
let mut buf = vec![0u8; 33 + name.len() + 2];
buf[0] = (33 + name.len()) as u8;
buf[32] = name.len() as u8;
buf[33..33 + name.len()].copy_from_slice(name);
let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert_eq!(rec.filename, "HELLO");
}
#[test]
fn directory_record_joliet_with_version_suffix() {
let name: Vec<u8> = "hi.txt;1"
.encode_utf16()
.flat_map(|c| c.to_be_bytes())
.collect();
let mut buf = vec![0u8; 33 + name.len() + 2];
buf[0] = (33 + name.len()) as u8;
buf[32] = name.len() as u8;
buf[33..33 + name.len()].copy_from_slice(&name);
let rec = parse_directory_record(&buf, VolumeDescriptorType::Joliet).unwrap();
assert_eq!(rec.filename, "hi.txt");
}
#[test]
fn directory_record_empty_filename_length_zero() {
let mut buf = [0u8; 40];
buf[0] = 34;
buf[32] = 0; let rec = parse_directory_record(&buf, VolumeDescriptorType::Primary).unwrap();
assert_eq!(rec.filename, ".");
}
#[test]
fn rock_ridge_name_su_start_too_large() {
let data = vec![0u8; 40];
let result = extract_rock_ridge_name(&data, 40, 10);
assert_eq!(result, None);
}
#[test]
fn rock_ridge_nm_current_flag_skipped() {
let mut data = vec![0u8; 60];
let su_off = 34;
data[su_off] = b'N';
data[su_off + 1] = b'M';
data[su_off + 2] = 13;
data[su_off + 3] = 1;
data[su_off + 4] = 0x02; data[su_off + 5..su_off + 13].copy_from_slice(b"ignored!");
let result = extract_rock_ridge_name(&data, 60, 1);
assert_eq!(
result, None,
"CURRENT-flagged NM entry should produce no name"
);
}
#[test]
fn rock_ridge_entry_too_short_breaks() {
let mut data = vec![0u8; 60];
let su_off = 34;
data[su_off] = b'N';
data[su_off + 1] = b'M';
data[su_off + 2] = 2; let result = extract_rock_ridge_name(&data, 60, 1);
assert_eq!(result, None);
}
#[test]
fn rock_ridge_while_loop_terminates_normally() {
let mut data = vec![0u8; 50];
let su_off = 34;
data[su_off] = b'N';
data[su_off + 1] = b'M';
data[su_off + 2] = 8; data[su_off + 3] = 1; data[su_off + 4] = 0; data[su_off + 5] = b'h';
data[su_off + 6] = b'i';
data[su_off + 7] = b'!';
let result = extract_rock_ridge_name(&data, 42, 1);
assert_eq!(result, Some("hi!".to_string()));
}
#[test]
fn parse_iso9660_breaks_on_non_cd001_sector() {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[root_off + 25] = 0x02; img[root_off + 32] = 1;
img[root_off + 33] = 0; let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("should succeed despite non-CD001 at sector 17");
assert_eq!(root.name, "/");
}
#[test]
fn parse_iso9660_unknown_vd_type_ignored() {
let mut img = make_iso_primary_only();
img.resize(S * 22, 0);
img[17 * S] = 3; img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
img[18 * S] = 255;
img[18 * S + 1..18 * S + 6].copy_from_slice(b"CD001");
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("unknown VD type should be ignored");
assert_eq!(root.name, "/");
}
#[test]
fn parse_iso9660_verbose_joliet_path() {
let img = make_iso_joliet();
let mut c = Cursor::new(img);
let root = parse_iso9660_verbose(&mut c, true).expect("verbose Joliet parse");
assert!(root.find_node("/hi.txt").is_some());
}
#[test]
fn parse_iso9660_non_joliet_svd_ignored() {
let mut img = vec![0u8; S * 22];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&19u32.to_le_bytes()); img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[root_off + 25] = 0x02;
img[root_off + 32] = 1;
img[root_off + 33] = 0;
img[17 * S] = 2;
img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
img[18 * S] = 255;
img[18 * S + 1..18 * S + 6].copy_from_slice(b"CD001");
let d = 19 * S;
img[d] = 34;
img[d + 2..d + 6].copy_from_slice(&19u32.to_le_bytes());
img[d + 10..d + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[d + 25] = 0x02;
img[d + 32] = 1;
img[d + 33] = 0;
let e1 = d + 34;
img[e1] = 34;
img[e1 + 2..e1 + 6].copy_from_slice(&19u32.to_le_bytes());
img[e1 + 10..e1 + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[e1 + 25] = 0x02;
img[e1 + 32] = 1;
img[e1 + 33] = 1;
let e2 = e1 + 34;
let name = b"HELLO.TXT;1";
img[e2] = (33 + name.len()) as u8;
img[e2 + 2..e2 + 6].copy_from_slice(&20u32.to_le_bytes()); img[e2 + 10..e2 + 14].copy_from_slice(&11u32.to_le_bytes());
img[e2 + 32] = name.len() as u8;
img[e2 + 33..e2 + 33 + name.len()].copy_from_slice(name);
img[20 * S..20 * S + 11].copy_from_slice(b"Hello World");
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("non-Joliet SVD should be ignored");
assert!(root.find_node("/HELLO.TXT").is_some());
}
#[test]
fn parse_directory_non_directory_returns_ok() {
let img = make_iso_primary_only();
let rec = DirectoryRecord {
extent_location: 0,
data_length: 100,
is_directory: false,
filename: "file.txt".to_string(),
};
let mut parent = crate::tree::TreeNode::new_directory("/".to_string());
let mut c = Cursor::new(img);
let result = parse_directory(
&mut c,
&rec,
&mut parent,
VolumeDescriptorType::Primary,
false,
false,
);
assert!(result.is_ok());
assert!(parent.children.is_empty());
}
#[test]
fn parse_directory_zero_byte_padding_advances_to_sector() {
let mut dir_img = vec![0u8; S * 20];
let pvd = 16 * S;
dir_img[pvd] = 1;
dir_img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
dir_img[root_off] = 34;
dir_img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
dir_img[root_off + 25] = 0x02;
dir_img[root_off + 32] = 1;
dir_img[root_off + 33] = 0;
dir_img[17 * S] = 255;
dir_img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let d = 18 * S;
dir_img[d] = 34;
dir_img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 10..d + 14].copy_from_slice(&(S as u32).to_le_bytes());
dir_img[d + 25] = 0x02;
dir_img[d + 32] = 1;
dir_img[d + 33] = 0;
dir_img[d + 34] = 34;
dir_img[d + 34 + 2..d + 34 + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 34 + 10..d + 34 + 14].copy_from_slice(&(S as u32).to_le_bytes());
dir_img[d + 34 + 25] = 0x02;
dir_img[d + 34 + 32] = 1;
dir_img[d + 34 + 33] = 1;
let mut c = Cursor::new(dir_img);
let root = parse_iso9660(&mut c).expect("zero-padded dir should parse");
assert_eq!(root.name, "/");
}
#[test]
fn parse_directory_record_parse_error_skipped() {
let mut dir_img = vec![0u8; S * 20];
let pvd = 16 * S;
dir_img[pvd] = 1;
dir_img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
dir_img[root_off] = 34;
dir_img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[root_off + 10..root_off + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[root_off + 25] = 0x02;
dir_img[root_off + 32] = 1;
dir_img[root_off + 33] = 0;
dir_img[17 * S] = 255;
dir_img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let d = 18 * S;
dir_img[d] = 34;
dir_img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 10..d + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[d + 25] = 0x02;
dir_img[d + 32] = 1;
dir_img[d + 33] = 0;
dir_img[d + 34] = 34;
dir_img[d + 34 + 2..d + 34 + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 34 + 10..d + 34 + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[d + 34 + 25] = 0x02;
dir_img[d + 34 + 32] = 1;
dir_img[d + 34 + 33] = 1;
dir_img[d + 68] = 32;
let mut c = Cursor::new(dir_img);
let root = parse_iso9660(&mut c).expect("bad dir entry should be skipped");
assert_eq!(root.name, "/");
assert!(root.children.is_empty());
}
#[test]
fn parse_directory_record_length_overflow_breaks() {
let mut dir_img = vec![0u8; S * 20];
let pvd = 16 * S;
dir_img[pvd] = 1;
dir_img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
dir_img[root_off] = 34;
dir_img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[root_off + 10..root_off + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[root_off + 25] = 0x02;
dir_img[root_off + 32] = 1;
dir_img[root_off + 33] = 0;
dir_img[17 * S] = 255;
dir_img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let d = 18 * S;
dir_img[d] = 34;
dir_img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 10..d + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[d + 25] = 0x02;
dir_img[d + 32] = 1;
dir_img[d + 33] = 0;
dir_img[d + 34] = 34;
dir_img[d + 34 + 2..d + 34 + 6].copy_from_slice(&18u32.to_le_bytes());
dir_img[d + 34 + 10..d + 34 + 14].copy_from_slice(&100u32.to_le_bytes());
dir_img[d + 34 + 25] = 0x02;
dir_img[d + 34 + 32] = 1;
dir_img[d + 34 + 33] = 1;
dir_img[d + 68] = 33;
let mut c = Cursor::new(dir_img);
let root = parse_iso9660(&mut c).expect("overflow record should break loop cleanly");
assert!(root.children.is_empty());
}
#[test]
fn detect_rock_ridge_short_buffer_returns_false() {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[root_off + 10..root_off + 14].copy_from_slice(&20u32.to_le_bytes()); img[root_off + 25] = 0x02;
img[root_off + 32] = 1;
img[root_off + 33] = 0;
img[17 * S] = 255;
img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("short root buffer should not error");
assert_eq!(root.name, "/");
}
#[test]
fn detect_rock_ridge_sp_signature_detected() {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[root_off + 25] = 0x02;
img[root_off + 32] = 1;
img[root_off + 33] = 0;
img[17 * S] = 255;
img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let d = 18 * S;
let record_len: u8 = 42;
let filename_length: u8 = 1;
img[d] = record_len;
img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
img[d + 10..d + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[d + 25] = 0x02;
img[d + 32] = filename_length;
img[d + 33] = 0; img[d + 34] = b'S';
img[d + 35] = b'P';
let mut c = Cursor::new(img.clone());
let root = parse_iso9660(&mut c).expect("SP rock ridge should parse");
assert_eq!(root.name, "/");
let mut c2 = Cursor::new(img);
let root2 = parse_iso9660_verbose(&mut c2, true).expect("verbose rock ridge");
assert_eq!(root2.name, "/");
}
#[test]
fn detect_rock_ridge_nm_px_signatures() {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[root_off + 25] = 0x02;
img[root_off + 32] = 1;
img[root_off + 33] = 0;
img[17 * S] = 255;
img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let d = 18 * S;
img[d] = 42;
img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
img[d + 10..d + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[d + 25] = 0x02;
img[d + 32] = 1;
img[d + 33] = 0;
img[d + 34] = b'P';
img[d + 35] = b'X';
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("PX rock ridge should parse");
assert_eq!(root.name, "/");
}
#[test]
fn parse_iso9660_rock_ridge_name_in_directory() {
let mut img = vec![0u8; S * 20];
let pvd = 16 * S;
img[pvd] = 1;
img[pvd + 1..pvd + 6].copy_from_slice(b"CD001");
let root_off = pvd + 156;
img[root_off] = 34;
img[root_off + 2..root_off + 6].copy_from_slice(&18u32.to_le_bytes());
img[root_off + 10..root_off + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[root_off + 25] = 0x02;
img[root_off + 32] = 1;
img[root_off + 33] = 0;
let d = 18 * S;
img[d] = 42;
img[d + 2..d + 6].copy_from_slice(&18u32.to_le_bytes());
img[d + 10..d + 14].copy_from_slice(&(S as u32).to_le_bytes());
img[d + 25] = 0x02;
img[d + 32] = 1;
img[d + 33] = 0; img[d + 34] = b'S';
img[d + 35] = b'P';
img[17 * S] = 255;
img[17 * S + 1..17 * S + 6].copy_from_slice(b"CD001");
let e = d + 42;
let iso_name = b"F;1";
let rr_name = b"longname.txt";
let entry_len = 33 + iso_name.len() + 17;
img[e] = entry_len as u8;
img[e + 2..e + 6].copy_from_slice(&19u32.to_le_bytes()); img[e + 10..e + 14].copy_from_slice(&11u32.to_le_bytes()); img[e + 25] = 0x00; img[e + 32] = iso_name.len() as u8;
img[e + 33..e + 33 + iso_name.len()].copy_from_slice(iso_name);
let su = e + 36;
img[su] = b'N';
img[su + 1] = b'M';
img[su + 2] = (5 + rr_name.len()) as u8;
img[su + 3] = 1; img[su + 4] = 0; img[su + 5..su + 5 + rr_name.len()].copy_from_slice(rr_name);
img[19 * S..19 * S + 11].copy_from_slice(b"Hello World");
let mut c = Cursor::new(img);
let root = parse_iso9660(&mut c).expect("rock ridge with NM entry");
assert!(
root.find_node("/longname.txt").is_some(),
"Rock Ridge NM name not applied"
);
}
}