Some checks failed
Orchestrator / Commit Message Validation (push) Has been skipped
Orchestrator / Lint (push) Successful in 2m5s
Orchestrator / Windows (x86-64) (push) Failing after 6m13s
Orchestrator / Linux (x86-64) (push) Failing after 20m59s
Orchestrator / Create Release (push) Has been skipped
Major additions to the xenia-rs Rust port: - CPU: ~170 new PPC opcode implementations (FPU, VMX128, 64-bit ALU, load/store variants) - XEX: Full LZX (normal) decompression pipeline with AES-128-CBC decryption via mspack FFI - XEX: Parse file format info, import libraries, and security info AES key from headers - VFS: Rewrite XISO disc image to use seek-based I/O (handles 7GB+ images without loading into memory) - App: Auto-detect ISO files and extract default.xex for all CLI commands - App: Add `info` and `browse` CLI subcommands - Kernel: Expand HLE exports from 14 to 40 stubs (memory, threading, TLS, I/O, video) - Memory: Add bounds checking on all guest memory accesses to prevent segfaults - Types: Add Vec128 array-based accessors (from_u32x4_array, from_f32x4_array, etc.) Tested against Project Sylpheed (USA) disc image - all four CLI commands (browse, info, disasm, exec) work correctly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
186 lines
6.0 KiB
Rust
186 lines
6.0 KiB
Rust
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<u8>,
|
|
}
|
|
|
|
/// 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<String>, path: &std::path::Path) -> Result<Self, VfsError> {
|
|
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<VfsEntry> {
|
|
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<VfsEntry>) {
|
|
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<Vec<VfsEntry>, VfsError> {
|
|
Ok(self.read_entries())
|
|
}
|
|
|
|
fn read_file(&self, path: &str) -> Result<Vec<u8>, 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<VfsEntry, VfsError> {
|
|
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()))
|
|
}
|
|
}
|