use super::entry::{EntryType, FileEntry, FileTimestamps};
use super::filesystem::{Filesystem, FilesystemError};
use crate::iso9660::{Iso9660DateTime, PrimaryVolumeDescriptor};
use crate::sector_reader::{SectorReader, SECTOR_SIZE};
pub struct Iso9660Filesystem {
reader: Box<dyn SectorReader>,
root_location: u32,
root_size: u32,
volume_id: String,
joliet: bool,
}
impl Iso9660Filesystem {
pub fn new(mut reader: Box<dyn SectorReader>) -> Result<Self, FilesystemError> {
let pvd = PrimaryVolumeDescriptor::read_from(&mut *reader)
.map_err(|e| FilesystemError::Parse(e.to_string()))?;
let rock_ridge = detect_rock_ridge_root(
reader.as_mut(),
pvd.root_directory_lba,
pvd.root_directory_size,
);
let joliet_svd = if rock_ridge {
None
} else {
crate::iso9660::JolietVolumeDescriptor::find(reader.as_mut())
.ok()
.flatten()
};
let (root_location, root_size, volume_id, joliet) = match joliet_svd {
Some(j) => (
j.root_directory_lba,
j.root_directory_size,
j.volume_id,
true,
),
None => (
pvd.root_directory_lba,
pvd.root_directory_size,
pvd.volume_id,
false,
),
};
Ok(Self {
reader,
root_location,
root_size,
volume_id,
joliet,
})
}
fn read_directory_data(
&mut self,
location: u32,
size: u32,
) -> Result<Vec<u8>, FilesystemError> {
if size == 0 {
return Ok(Vec::new());
}
let byte_offset = location as u64 * SECTOR_SIZE;
self.reader
.read_bytes(byte_offset, size as usize)
.map_err(sector_to_fs_err)
}
fn parse_directory(&mut self, data: &[u8], parent_path: &str) -> Vec<FileEntry> {
let mut entries = Vec::new();
let mut offset = 0usize;
while offset < data.len() {
let record_length = data[offset] as usize;
if record_length == 0 {
let next_sector = (offset / SECTOR_SIZE as usize + 1) * SECTOR_SIZE as usize;
if next_sector >= data.len() {
break;
}
offset = next_sector;
continue;
}
if offset + record_length > data.len() {
break;
}
if let Some(rec) = DirectoryRecord::parse(&data[offset..offset + record_length]) {
if !rec.is_self() && !rec.is_parent() {
let mut name = rec.clean_name(self.joliet);
let rr = super::rockridge::parse(&rec.system_use, self.reader.as_mut());
if let Some(alt) = &rr.name {
name = alt.clone();
}
let path = if parent_path == "/" {
format!("/{}", name)
} else {
format!("{}/{}", parent_path, name)
};
let mut entry = if rec.is_directory() {
FileEntry::new_directory(name, path, rec.extent_location as u64)
} else {
FileEntry::new_file(
name,
path,
rec.data_length as u64,
rec.extent_location as u64,
)
};
if entry.is_directory() {
entry.size = rec.data_length as u64;
}
if let Some(recorded) = rec.recorded {
entry.timestamps = Some(FileTimestamps::Iso9660 {
recorded,
created: rr.created,
modified: rr.modified,
accessed: rr.accessed,
});
}
if let Some(posix) = rr.posix {
entry.posix = Some(posix);
}
if let Some(target) = rr.symlink_target {
entry.symlink_target = Some(target);
}
entries.push(entry);
}
}
offset += record_length;
}
entries.sort_by(|a, b| match (a.entry_type, b.entry_type) {
(EntryType::Directory, EntryType::File) => std::cmp::Ordering::Less,
(EntryType::File, EntryType::Directory) => std::cmp::Ordering::Greater,
_ => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
});
entries
}
}
impl Filesystem for Iso9660Filesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
let mut root = FileEntry::root(self.root_location as u64);
root.size = self.root_size as u64;
Ok(root)
}
fn list_directory(&mut self, entry: &FileEntry) -> Result<Vec<FileEntry>, FilesystemError> {
if !entry.is_directory() {
return Err(FilesystemError::NotADirectory(entry.path.clone()));
}
let (location, size) = if entry.path == "/" {
(self.root_location, self.root_size)
} else {
(entry.location as u32, entry.size as u32)
};
let data = self.read_directory_data(location, size)?;
Ok(self.parse_directory(&data, &entry.path))
}
fn read_file(&mut self, entry: &FileEntry) -> Result<Vec<u8>, FilesystemError> {
if entry.is_directory() {
return Err(FilesystemError::NotADirectory(format!(
"{} is a directory",
entry.path
)));
}
let byte_offset = entry.location * SECTOR_SIZE;
self.reader
.read_bytes(byte_offset, entry.size as usize)
.map_err(sector_to_fs_err)
}
fn read_file_range(
&mut self,
entry: &FileEntry,
offset: u64,
length: usize,
) -> Result<Vec<u8>, FilesystemError> {
if entry.is_directory() {
return Err(FilesystemError::NotADirectory(format!(
"{} is a directory",
entry.path
)));
}
let actual_length = length.min(entry.size.saturating_sub(offset) as usize);
if actual_length == 0 {
return Ok(Vec::new());
}
let file_byte_start = entry.location * SECTOR_SIZE;
self.reader
.read_bytes(file_byte_start + offset, actual_length)
.map_err(sector_to_fs_err)
}
fn read_resource_fork(
&mut self,
_entry: &FileEntry,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn read_resource_fork_range(
&mut self,
_entry: &FileEntry,
_offset: u64,
_length: usize,
) -> Result<Option<Vec<u8>>, FilesystemError> {
Ok(None)
}
fn volume_name(&self) -> Option<&str> {
if self.volume_id.is_empty() {
None
} else {
Some(&self.volume_id)
}
}
}
#[derive(Debug, Clone)]
struct DirectoryRecord {
extent_location: u32,
data_length: u32,
file_flags: u8,
file_identifier: Vec<u8>,
recorded: Option<Iso9660DateTime>,
system_use: Vec<u8>,
}
impl DirectoryRecord {
fn parse(data: &[u8]) -> Option<Self> {
if data.len() < 33 || data[0] == 0 {
return None;
}
let extent_location = u32::from_le_bytes(data[2..6].try_into().ok()?);
let data_length = u32::from_le_bytes(data[10..14].try_into().ok()?);
let recorded = Iso9660DateTime::parse(&data[18..25]);
let file_flags = data[25];
let id_len = data[32] as usize;
if data.len() < 33 + id_len {
return None;
}
let file_identifier = data[33..33 + id_len].to_vec();
let su_start = 33 + id_len + (1 - id_len % 2);
let system_use = data.get(su_start..).map(|s| s.to_vec()).unwrap_or_default();
Some(Self {
extent_location,
data_length,
file_flags,
file_identifier,
recorded,
system_use,
})
}
fn is_directory(&self) -> bool {
(self.file_flags & 0x02) != 0
}
fn is_self(&self) -> bool {
self.file_identifier.is_empty() || self.file_identifier == [0x00]
}
fn is_parent(&self) -> bool {
self.file_identifier == [0x01]
}
fn clean_name(&self, joliet: bool) -> String {
if self.is_self() {
return ".".to_string();
}
if self.is_parent() {
return "..".to_string();
}
let decoded = if joliet {
crate::iso9660::decode_utf16be(&self.file_identifier)
} else {
String::from_utf8_lossy(&self.file_identifier).into_owned()
};
let name = match decoded.rfind(';') {
Some(idx) => &decoded[..idx],
None => &decoded[..],
};
if self.is_directory() {
name.trim_end_matches('.').to_string()
} else {
name.to_string()
}
}
}
fn detect_rock_ridge_root(reader: &mut dyn SectorReader, root_lba: u32, root_size: u32) -> bool {
if root_size == 0 {
return false;
}
let want = (root_size as usize).min(SECTOR_SIZE as usize);
let data = match reader.read_bytes(root_lba as u64 * SECTOR_SIZE, want) {
Ok(d) => d,
Err(_) => return false,
};
if data.is_empty() {
return false;
}
let rec_len = data[0] as usize;
if rec_len == 0 || rec_len > data.len() {
return false;
}
match DirectoryRecord::parse(&data[..rec_len]) {
Some(rec) => super::rockridge::detect(&rec.system_use),
None => false,
}
}
fn sector_to_fs_err(e: crate::error::OpticaldiscsError) -> FilesystemError {
match e {
crate::error::OpticaldiscsError::Io(io_err) => FilesystemError::Io(io_err),
e => FilesystemError::InvalidData(e.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::iso9660::build_test_pvd_sector;
use crate::sector_reader::SECTOR_SIZE;
use std::io::{Cursor, Read, Seek, SeekFrom};
struct CursorReader(Cursor<Vec<u8>>);
impl SectorReader for CursorReader {
fn read_sector(&mut self, lba: u64) -> crate::error::Result<Vec<u8>> {
self.0
.seek(SeekFrom::Start(lba * SECTOR_SIZE))
.map_err(crate::error::OpticaldiscsError::Io)?;
let mut buf = vec![0u8; SECTOR_SIZE as usize];
self.0
.read_exact(&mut buf)
.map_err(crate::error::OpticaldiscsError::Io)?;
Ok(buf)
}
}
fn build_iso_image(label: &str, files: &[(&str, &[u8])]) -> Vec<u8> {
const SECTOR: usize = SECTOR_SIZE as usize;
let total_sectors = 19 + files.len();
let mut img = vec![0u8; total_sectors * SECTOR];
let pvd = build_test_pvd_sector(label, 18, SECTOR as u32);
img[16 * SECTOR..17 * SECTOR].copy_from_slice(&pvd);
img[17 * SECTOR] = 0xFF;
img[17 * SECTOR + 1..17 * SECTOR + 6].copy_from_slice(b"CD001");
img[17 * SECTOR + 6] = 1;
let mut dir = vec![0u8; SECTOR];
let mut dir_off = 0usize;
let append_rec = |buf: &mut Vec<u8>,
off: &mut usize,
extent_lba: u32,
data_len: u32,
flags: u8,
id: &[u8]| {
let id_len = id.len();
let rec_len = 33 + id_len;
let rec_len = if rec_len % 2 == 0 {
rec_len
} else {
rec_len + 1
};
buf[*off] = rec_len as u8;
buf[*off + 2..*off + 6].copy_from_slice(&extent_lba.to_le_bytes());
buf[*off + 6..*off + 10].copy_from_slice(&extent_lba.to_be_bytes());
buf[*off + 10..*off + 14].copy_from_slice(&data_len.to_le_bytes());
buf[*off + 14..*off + 18].copy_from_slice(&data_len.to_be_bytes());
buf[*off + 25] = flags;
buf[*off + 32] = id_len as u8;
buf[*off + 33..*off + 33 + id_len].copy_from_slice(id);
*off += rec_len;
};
append_rec(&mut dir, &mut dir_off, 18, SECTOR as u32, 0x02, b"\x00");
append_rec(&mut dir, &mut dir_off, 18, SECTOR as u32, 0x02, b"\x01");
for (i, (name, content)) in files.iter().enumerate() {
let file_lba = 19 + i as u32;
append_rec(
&mut dir,
&mut dir_off,
file_lba,
content.len() as u32,
0x00, name.as_bytes(),
);
let sector_off = (19 + i) * SECTOR;
let copy_len = content.len().min(SECTOR);
img[sector_off..sector_off + copy_len].copy_from_slice(&content[..copy_len]);
}
img[18 * SECTOR..19 * SECTOR].copy_from_slice(&dir);
img
}
#[test]
fn empty_root_directory_returns_no_entries() {
let img = build_iso_image("EMPTY_ROOT", &[]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert!(entries.is_empty());
}
#[test]
fn root_directory_lists_files() {
let content = b"hello world";
let img = build_iso_image("BROWSE_TEST", &[("README.TXT;1", content)]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "README.TXT"); assert!(entries[0].is_file());
assert_eq!(entries[0].size, content.len() as u64);
}
#[test]
fn read_file_returns_correct_content() {
let content = b"ISO 9660 file content";
let img = build_iso_image("READ_TEST", &[("DATA.BIN;1", content)]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
let file = entries.iter().find(|e| e.name == "DATA.BIN").unwrap();
let data = fs.read_file(file).unwrap();
assert_eq!(data, content);
}
#[test]
fn read_file_range_returns_slice() {
let content = b"abcdefghij";
let img = build_iso_image("RANGE_TEST", &[("RANGE.TXT;1", content)]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
let file = entries.iter().find(|e| e.name == "RANGE.TXT").unwrap();
let slice = fs.read_file_range(file, 3, 4).unwrap();
assert_eq!(slice, b"defg");
}
#[test]
fn volume_name_returned() {
let img = build_iso_image("MY_DISC", &[]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let fs = Iso9660Filesystem::new(reader).unwrap();
assert_eq!(fs.volume_name(), Some("MY_DISC"));
}
#[test]
fn list_directory_on_file_returns_error() {
let content = b"data";
let img = build_iso_image("ERR_TEST", &[("FILE.TXT;1", content)]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
let file = entries.iter().find(|e| e.is_file()).unwrap();
assert!(fs.list_directory(file).is_err());
}
#[test]
fn read_resource_fork_returns_none() {
let content = b"data";
let img = build_iso_image("RSRC_TEST", &[("FILE.TXT;1", content)]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
let file = entries.iter().find(|e| e.is_file()).unwrap();
assert!(fs.read_resource_fork(file).unwrap().is_none());
assert!(fs.read_resource_fork_range(file, 0, 4).unwrap().is_none());
assert!(file.resource_fork_size.is_none());
assert!(file.type_code.is_none());
assert!(file.creator_code.is_none());
}
#[test]
fn read_file_on_directory_returns_error() {
let img = build_iso_image("ERR_TEST2", &[]);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
assert!(fs.read_file(&root).is_err());
}
#[test]
fn multiple_files_sorted_alphabetically() {
let img = build_iso_image(
"SORT_TEST",
&[
("ZEBRA.TXT;1", b"z"),
("APPLE.TXT;1", b"a"),
("MANGO.TXT;1", b"m"),
],
);
let reader = Box::new(CursorReader(Cursor::new(img)));
let mut fs = Iso9660Filesystem::new(reader).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
let names: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
assert_eq!(names, ["APPLE.TXT", "MANGO.TXT", "ZEBRA.TXT"]);
}
#[test]
fn directory_record_parse_file() {
let name = b"TEST.TXT;1";
let id_len = name.len();
let rec_len = 33 + id_len;
let mut data = vec![0u8; rec_len];
data[0] = rec_len as u8;
data[2..6].copy_from_slice(&42u32.to_le_bytes()); data[10..14].copy_from_slice(&1024u32.to_le_bytes()); data[25] = 0; data[32] = id_len as u8;
data[33..33 + id_len].copy_from_slice(name);
let rec = DirectoryRecord::parse(&data).unwrap();
assert_eq!(rec.extent_location, 42);
assert_eq!(rec.data_length, 1024);
assert!(!rec.is_directory());
assert!(!rec.is_self());
assert!(!rec.is_parent());
assert_eq!(rec.clean_name(false), "TEST.TXT");
}
#[test]
fn directory_record_parse_self_entry() {
let mut data = vec![0u8; 34];
data[0] = 34;
data[25] = 0x02;
data[32] = 1;
data[33] = 0x00; let rec = DirectoryRecord::parse(&data).unwrap();
assert!(rec.is_self());
assert!(rec.is_directory());
}
#[test]
fn directory_record_parse_parent_entry() {
let mut data = vec![0u8; 34];
data[0] = 34;
data[25] = 0x02;
data[32] = 1;
data[33] = 0x01; let rec = DirectoryRecord::parse(&data).unwrap();
assert!(rec.is_parent());
}
#[test]
fn directory_record_too_short_returns_none() {
assert!(DirectoryRecord::parse(&[0u8; 32]).is_none());
}
#[test]
fn clean_name_strips_version_suffix() {
let rec = DirectoryRecord {
extent_location: 0,
data_length: 0,
file_flags: 0,
file_identifier: b"ARCHIVE.TAR;1".to_vec(),
recorded: None,
system_use: Vec::new(),
};
assert_eq!(rec.clean_name(false), "ARCHIVE.TAR");
}
#[test]
fn clean_name_strips_directory_trailing_dot() {
let rec = DirectoryRecord {
extent_location: 0,
data_length: 0,
file_flags: 0x02,
file_identifier: b"SYSTEM.".to_vec(),
recorded: None,
system_use: Vec::new(),
};
assert_eq!(rec.clean_name(false), "SYSTEM");
}
#[test]
fn clean_name_decodes_joliet_utf16be() {
let mut id: Vec<u8> = Vec::new();
for u in "Café;1".encode_utf16() {
id.extend_from_slice(&u.to_be_bytes());
}
let rec = DirectoryRecord {
extent_location: 0,
data_length: 0,
file_flags: 0,
file_identifier: id,
recorded: None,
system_use: Vec::new(),
};
assert_eq!(rec.clean_name(true), "Café");
}
use crate::browse::entry::FileTimestamps;
fn susp(sig: &[u8; 2], data: &[u8]) -> Vec<u8> {
let mut v = vec![sig[0], sig[1], (4 + data.len()) as u8, 1];
v.extend_from_slice(data);
v
}
fn both(n: u32) -> Vec<u8> {
let mut v = n.to_le_bytes().to_vec();
v.extend_from_slice(&n.to_be_bytes());
v
}
fn dir_record(
extent: u32,
size: u32,
flags: u8,
id: &[u8],
recorded: Option<[u8; 7]>,
system_use: &[u8],
) -> Vec<u8> {
let id_len = id.len();
let su_start = 33 + id_len + (1 - id_len % 2);
let mut rec_len = su_start + system_use.len();
if rec_len % 2 != 0 {
rec_len += 1; }
let mut r = vec![0u8; rec_len];
r[0] = rec_len as u8;
r[2..6].copy_from_slice(&extent.to_le_bytes());
r[6..10].copy_from_slice(&extent.to_be_bytes());
r[10..14].copy_from_slice(&size.to_le_bytes());
r[14..18].copy_from_slice(&size.to_be_bytes());
if let Some(d) = recorded {
r[18..25].copy_from_slice(&d);
}
r[25] = flags;
r[32] = id_len as u8;
r[33..33 + id_len].copy_from_slice(id);
r[su_start..su_start + system_use.len()].copy_from_slice(system_use);
r
}
#[test]
fn rock_ridge_end_to_end() {
const SEC: usize = SECTOR_SIZE as usize;
let mut img = vec![0u8; 21 * SEC];
img[16 * SEC..17 * SEC].copy_from_slice(&build_test_pvd_sector("RR_DISC", 18, SEC as u32));
img[17 * SEC] = 0xFF;
img[17 * SEC + 1..17 * SEC + 6].copy_from_slice(b"CD001");
let mut px = Vec::new();
px.extend(both(0o100_644)); px.extend(both(1)); px.extend(both(501)); px.extend(both(20)); let mut tf = vec![0b0000_0010u8]; tf.extend_from_slice(&[98, 6, 1, 8, 30, 0, 0]);
let mut nm = vec![0u8];
nm.extend_from_slice(b"real name.txt");
let mut su = Vec::new();
su.extend(susp(b"PX", &px));
su.extend(susp(b"TF", &tf));
su.extend(susp(b"NM", &nm));
let mut dir = Vec::new();
dir.extend(dir_record(
18,
SEC as u32,
0x02,
&[0x00],
None,
&susp(b"SP", &[0xBE, 0xEF, 0]),
));
dir.extend(dir_record(18, SEC as u32, 0x02, &[0x01], None, &[]));
dir.extend(dir_record(
20,
5,
0x00,
b"FILE.TXT;1",
Some([97, 3, 18, 16, 45, 47, 0]),
&su,
));
img[18 * SEC..18 * SEC + dir.len()].copy_from_slice(&dir);
img[20 * SEC..20 * SEC + 5].copy_from_slice(b"hello");
let mut fs = Iso9660Filesystem::new(Box::new(CursorReader(Cursor::new(img)))).unwrap();
assert!(!fs.joliet, "Rock Ridge disc should browse the primary tree");
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 1);
let f = &entries[0];
assert_eq!(f.name, "real name.txt");
let px = f.posix.expect("posix present");
assert_eq!(px.uid, 501);
assert_eq!(px.gid, 20);
assert_eq!(px.permission_bits(), 0o644);
match f.timestamps {
Some(FileTimestamps::Iso9660 {
recorded, modified, ..
}) => {
assert_eq!(recorded.year(), 1997);
assert_eq!(modified.expect("TF modify").year(), 1998);
}
_ => panic!("expected ISO 9660 timestamps"),
}
}
#[test]
fn joliet_end_to_end_prefers_unicode_names() {
const SEC: usize = SECTOR_SIZE as usize;
let mut img = vec![0u8; 21 * SEC];
img[16 * SEC..17 * SEC].copy_from_slice(&build_test_pvd_sector("PRIMARY", 18, SEC as u32));
let mut svd = build_test_pvd_sector("PRIMARY", 19, SEC as u32);
svd[0] = 0x02; svd[88..91].copy_from_slice(b"%/E"); img[17 * SEC..18 * SEC].copy_from_slice(&svd);
let mut pdir = Vec::new();
pdir.extend(dir_record(18, SEC as u32, 0x02, &[0x00], None, &[]));
pdir.extend(dir_record(18, SEC as u32, 0x02, &[0x01], None, &[]));
pdir.extend(dir_record(20, 5, 0x00, b"FILE.TXT;1", None, &[]));
img[18 * SEC..18 * SEC + pdir.len()].copy_from_slice(&pdir);
let uni: Vec<u8> = "Café.txt;1"
.encode_utf16()
.flat_map(u16::to_be_bytes)
.collect();
let mut jdir = Vec::new();
jdir.extend(dir_record(19, SEC as u32, 0x02, &[0x00], None, &[]));
jdir.extend(dir_record(19, SEC as u32, 0x02, &[0x01], None, &[]));
jdir.extend(dir_record(20, 5, 0x00, &uni, None, &[]));
img[19 * SEC..19 * SEC + jdir.len()].copy_from_slice(&jdir);
img[20 * SEC..20 * SEC + 5].copy_from_slice(b"hello");
let mut fs = Iso9660Filesystem::new(Box::new(CursorReader(Cursor::new(img)))).unwrap();
assert!(fs.joliet, "should select the Joliet tree");
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].name, "Café.txt");
}
}