use crate::{VfsDevice, VfsEntry, VfsError}; use std::io::{Read, Seek, SeekFrom}; /// XISO disc image device. Parses Xbox 360 disc images (GDFX/XISO format). pub struct DiscImageDevice { name: String, path: std::path::PathBuf, game_offset: u64, /// Cached root directory buffer (typically small, a few KB). root_buffer: Vec, } /// XISO sector size pub const SECTOR_SIZE: u64 = 0x800; /// GDFX magic string const GDFX_MAGIC: &[u8; 20] = b"MICROSOFT*XBOX*MEDIA"; /// File attribute: directory const FILE_ATTRIBUTE_DIRECTORY: u8 = 0x10; /// Known game partition offsets to try const LIKELY_OFFSETS: &[u64] = &[ 0x0000_0000, 0x0000_FB20, 0x0002_0600, 0x0208_0000, 0x0FD9_0000, ]; impl DiscImageDevice { pub fn open(name: impl Into, path: &std::path::Path) -> Result { let mut file = std::fs::File::open(path)?; // Find the game partition by locating the GDFX magic at sector 32 let mut game_offset = 0u64; let mut magic_found = false; let mut magic_buf = [0u8; 20]; for &offset in LIKELY_OFFSETS { let magic_pos = offset + 32 * SECTOR_SIZE; if file.seek(SeekFrom::Start(magic_pos)).is_ok() && file.read_exact(&mut magic_buf).is_ok() && magic_buf == *GDFX_MAGIC { game_offset = offset; magic_found = true; break; } } if !magic_found { return Err(VfsError::InvalidFormat( "GDFX magic not found - not a valid XISO disc image".into(), )); } // Read root directory info from sector 32 header let fs_ptr = game_offset + 32 * SECTOR_SIZE; file.seek(SeekFrom::Start(fs_ptr + 20))?; let mut buf4 = [0u8; 4]; file.read_exact(&mut buf4)?; let root_sector = u32::from_le_bytes(buf4) as u64; file.read_exact(&mut buf4)?; let root_size = u32::from_le_bytes(buf4) as u64; let root_byte_offset = game_offset + root_sector * SECTOR_SIZE; // Read the root directory buffer into memory (typically small) file.seek(SeekFrom::Start(root_byte_offset))?; let mut root_buffer = vec![0u8; root_size as usize]; file.read_exact(&mut root_buffer)?; Ok(Self { name: name.into(), path: path.to_path_buf(), game_offset, root_buffer, }) } /// Read all directory entries from the root directory tree. fn read_entries(&self) -> Vec { let mut entries = Vec::new(); self.read_entry(&self.root_buffer, 0, &mut entries); entries } /// Recursively read a directory entry from the binary tree structure. fn read_entry(&self, buffer: &[u8], ordinal: u16, entries: &mut Vec) { let p = ordinal as usize * 4; if p + 14 > buffer.len() { return; } let node_l = u16::from_le_bytes([buffer[p], buffer[p + 1]]); let node_r = u16::from_le_bytes([buffer[p + 2], buffer[p + 3]]); let sector = u32::from_le_bytes([buffer[p + 4], buffer[p + 5], buffer[p + 6], buffer[p + 7]]) as u64; let length = u32::from_le_bytes([buffer[p + 8], buffer[p + 9], buffer[p + 10], buffer[p + 11]]) as u64; let attributes = buffer[p + 12]; let name_length = buffer[p + 13] as usize; if p + 14 + name_length > buffer.len() { return; } // Traverse left subtree first (smaller names) if node_l != 0 && node_l != 0xFFFF { self.read_entry(buffer, node_l, entries); } // Read this entry's name let name = String::from_utf8_lossy(&buffer[p + 14..p + 14 + name_length]).to_string(); let is_directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; let file_offset = self.game_offset + sector * SECTOR_SIZE; entries.push(VfsEntry { name, is_directory, size: length, offset: file_offset, }); // Traverse right subtree (larger names) if node_r != 0 && node_r != 0xFFFF { self.read_entry(buffer, node_r, entries); } } } impl VfsDevice for DiscImageDevice { fn name(&self) -> &str { &self.name } fn list_root(&self) -> Result, VfsError> { Ok(self.read_entries()) } fn read_file(&self, path: &str) -> Result, VfsError> { let entries = self.read_entries(); let entry = entries.iter() .find(|e| e.name.eq_ignore_ascii_case(path) && !e.is_directory) .ok_or_else(|| VfsError::NotFound(path.to_string()))?; let offset = entry.offset; let size = entry.size as usize; // Read from file using seek let mut file = std::fs::File::open(&self.path)?; let file_len = file.seek(SeekFrom::End(0))?; if offset + size as u64 > file_len { return Err(VfsError::NotFound(format!( "File data extends past end of image: {} (offset={:#x}, size={:#x}, image_len={:#x})", path, offset, size, file_len ))); } file.seek(SeekFrom::Start(offset))?; let mut buf = vec![0u8; size]; let bytes_read = file.read(&mut buf)?; if bytes_read < size { // Try reading the rest let mut total = bytes_read; while total < size { let n = file.read(&mut buf[total..])?; if n == 0 { return Err(VfsError::NotFound(format!( "Short read: got {} of {} bytes for {}", total, size, path ))); } total += n; } } Ok(buf) } fn stat(&self, path: &str) -> Result { let entries = self.read_entries(); entries.into_iter() .find(|e| e.name.eq_ignore_ascii_case(path)) .ok_or_else(|| VfsError::NotFound(path.to_string())) } }