use std::collections::HashSet;
use super::entry::{EntryType, FileEntry};
use super::filesystem::{Filesystem, FilesystemError};
use crate::sector_reader::{SectorReader, SECTOR_SIZE};
pub const XDVDFS_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA";
const VD_OFFSET: u64 = 0x1_0000;
pub const XDVDFS_BASES: [u64; 4] = [0, 0x0FD9_0000, 0x0208_0000, 0x1830_0000];
const ATTR_DIRECTORY: u8 = 0x10;
const MAX_DIR_ENTRIES: usize = 100_000;
pub fn detect(reader: &mut dyn SectorReader) -> Option<u64> {
for &base in &XDVDFS_BASES {
if let Ok(d) = reader.read_bytes(base + VD_OFFSET, XDVDFS_MAGIC.len()) {
if d == XDVDFS_MAGIC {
return Some(base);
}
}
}
None
}
pub fn root_file_extent(
reader: &mut dyn SectorReader,
base: u64,
name: &str,
) -> Option<(u32, u32)> {
let vd = reader.read_bytes(base + VD_OFFSET, 0x20).ok()?;
if vd.len() < 0x20 || &vd[..20] != XDVDFS_MAGIC {
return None;
}
let root_sector = le32(&vd, 0x14);
let root_size = le32(&vd, 0x18);
if root_size == 0 {
return None;
}
let data = reader
.read_bytes(base + root_sector as u64 * SECTOR_SIZE, root_size as usize)
.ok()?;
for e in walk_dir_tree(&data) {
if !e.is_dir && e.name.eq_ignore_ascii_case(name) {
return Some((e.start_sector, e.size));
}
}
None
}
pub struct XdvdfsFilesystem {
reader: Box<dyn SectorReader>,
base: u64,
root_sector: u32,
root_size: u32,
}
impl XdvdfsFilesystem {
pub fn new(mut reader: Box<dyn SectorReader>) -> Result<Self, FilesystemError> {
let base = detect(reader.as_mut())
.ok_or_else(|| FilesystemError::Parse("no XDVDFS volume descriptor".into()))?;
let vd = reader
.read_bytes(base + VD_OFFSET, 0x20)
.map_err(sector_to_fs_err)?;
let root_sector = le32(&vd, 0x14);
let root_size = le32(&vd, 0x18);
Ok(Self {
reader,
base,
root_sector,
root_size,
})
}
fn read_extent(&mut self, sector: u32, size: u32) -> Result<Vec<u8>, FilesystemError> {
if size == 0 {
return Ok(Vec::new());
}
self.reader
.read_bytes(self.base + sector as u64 * SECTOR_SIZE, size as usize)
.map_err(sector_to_fs_err)
}
}
impl Filesystem for XdvdfsFilesystem {
fn root(&mut self) -> Result<FileEntry, FilesystemError> {
let mut root = FileEntry::root(self.root_sector 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 (sector, size) = if entry.path == "/" {
(self.root_sector, self.root_size)
} else {
(entry.location as u32, entry.size as u32)
};
let data = self.read_extent(sector, size)?;
let parent = entry.path.trim_end_matches('/');
let mut entries: Vec<FileEntry> = walk_dir_tree(&data)
.into_iter()
.map(|e| {
let path = format!("{parent}/{}", e.name);
let mut fe = if e.is_dir {
FileEntry::new_directory(e.name, path, e.start_sector as u64)
} else {
FileEntry::new_file(e.name, path, e.size as u64, e.start_sector as u64)
};
if e.is_dir {
fe.size = e.size as u64;
}
fe
})
.collect();
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()),
});
Ok(entries)
}
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
)));
}
self.read_extent(entry.location as u32, entry.size as u32)
}
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.min(entry.size.saturating_sub(offset) as usize);
if actual == 0 {
return Ok(Vec::new());
}
let byte = self.base + entry.location * SECTOR_SIZE + offset;
self.reader
.read_bytes(byte, actual)
.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> {
None
}
}
struct RawEntry {
name: String,
start_sector: u32,
size: u32,
is_dir: bool,
}
fn walk_dir_tree(data: &[u8]) -> Vec<RawEntry> {
let mut out = Vec::new();
if data.len() < 14 {
return out;
}
let mut stack: Vec<u16> = vec![0];
let mut visited: HashSet<u16> = HashSet::new();
while let Some(woff) = stack.pop() {
if woff == 0xFFFF {
continue;
}
if !visited.insert(woff) || visited.len() > MAX_DIR_ENTRIES {
continue;
}
let boff = woff as usize * 4;
if boff + 14 > data.len() {
continue;
}
let left = le16(data, boff);
let right = le16(data, boff + 2);
let start_sector = le32(data, boff + 4);
let size = le32(data, boff + 8);
let attrs = data[boff + 0x0C];
let nlen = data[boff + 0x0D] as usize;
if boff + 14 + nlen > data.len() {
continue;
}
let name = latin1(&data[boff + 14..boff + 14 + nlen]);
if !name.is_empty() && name != "." && name != ".." {
out.push(RawEntry {
name,
start_sector,
size,
is_dir: attrs & ATTR_DIRECTORY != 0,
});
}
if left != 0 {
stack.push(left);
}
if right != 0 {
stack.push(right);
}
}
out
}
fn le16(b: &[u8], o: usize) -> u16 {
u16::from_le_bytes([b[o], b[o + 1]])
}
fn le32(b: &[u8], o: usize) -> u32 {
u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]])
}
fn latin1(bytes: &[u8]) -> String {
bytes.iter().map(|&b| b as char).collect()
}
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 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];
let _ = self.0.read(&mut buf);
Ok(buf)
}
}
#[allow(clippy::too_many_arguments)]
fn put_entry(
buf: &mut [u8],
word_off: u16,
left: u16,
right: u16,
start: u32,
size: u32,
is_dir: bool,
name: &str,
) {
let o = word_off as usize * 4;
buf[o..o + 2].copy_from_slice(&left.to_le_bytes());
buf[o + 2..o + 4].copy_from_slice(&right.to_le_bytes());
buf[o + 4..o + 8].copy_from_slice(&start.to_le_bytes());
buf[o + 8..o + 12].copy_from_slice(&size.to_le_bytes());
buf[o + 0x0C] = if is_dir { ATTR_DIRECTORY } else { 0 };
buf[o + 0x0D] = name.len() as u8;
buf[o + 14..o + 14 + name.len()].copy_from_slice(name.as_bytes());
}
fn build_image() -> Vec<u8> {
const SEC: usize = SECTOR_SIZE as usize;
let mut img = vec![0u8; 40 * SEC];
let vd = 0x10000usize;
img[vd..vd + 20].copy_from_slice(XDVDFS_MAGIC);
img[vd + 0x14..vd + 0x18].copy_from_slice(&33u32.to_le_bytes());
img[vd + 0x18..vd + 0x1C].copy_from_slice(&(SEC as u32).to_le_bytes());
let mut root = vec![0u8; SEC];
put_entry(&mut root, 0, 0xFFFF, 5, 34, SEC as u32, true, "SUBDIR");
put_entry(&mut root, 5, 0xFFFF, 0xFFFF, 35, 11, false, "GAME.XBE");
img[33 * SEC..34 * SEC].copy_from_slice(&root);
let mut sub = vec![0u8; SEC];
put_entry(&mut sub, 0, 0xFFFF, 0xFFFF, 36, 5, false, "INNER.BIN");
img[34 * SEC..35 * SEC].copy_from_slice(&sub);
img[35 * SEC..35 * SEC + 11].copy_from_slice(b"hello world");
img[36 * SEC..36 * SEC + 5].copy_from_slice(b"inner");
img
}
#[test]
fn browses_tree_and_reads_files() {
let img = build_image();
let mut fs = XdvdfsFilesystem::new(Box::new(CursorReader(Cursor::new(img)))).unwrap();
let root = fs.root().unwrap();
let entries = fs.list_directory(&root).unwrap();
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].name, "SUBDIR");
assert!(entries[0].is_directory());
assert_eq!(entries[1].name, "GAME.XBE");
assert!(entries[1].is_file());
assert_eq!(entries[1].size, 11);
assert_eq!(fs.read_file(&entries[1]).unwrap(), b"hello world");
assert_eq!(fs.read_file_range(&entries[1], 6, 5).unwrap(), b"world");
let sub = fs.list_directory(&entries[0]).unwrap();
assert_eq!(sub.len(), 1);
assert_eq!(sub[0].name, "INNER.BIN");
assert_eq!(sub[0].path, "/SUBDIR/INNER.BIN");
assert_eq!(fs.read_file(&sub[0]).unwrap(), b"inner");
}
#[test]
fn detects_base_and_finds_root_file() {
let img = build_image();
let mut r = CursorReader(Cursor::new(img));
assert_eq!(detect(&mut r), Some(0));
assert_eq!(root_file_extent(&mut r, 0, "game.xbe"), Some((35, 11)));
assert_eq!(root_file_extent(&mut r, 0, "missing"), None);
}
#[test]
fn rejects_non_xdvdfs() {
let img = vec![0u8; 40 * SECTOR_SIZE as usize];
assert!(XdvdfsFilesystem::new(Box::new(CursorReader(Cursor::new(img)))).is_err());
}
}