feat: initialise workspace — Milestone 1 asset explorer
Three-crate Cargo workspace structured per PROJECT.md spec: - crates/sylpheed-formats — Xbox 360 format parsers (no Bevy) - crates/sylpheed-viewer — Bevy 0.15 asset viewer + egui UI - crates/sylpheed-cli — CLI tools (extract/list/sniff/texture) Milestone 1 features: - XISO disc image reading via xdvdfs 0.8 - XPR2 texture container parsing + Morton de-tiling - D3DFORMAT → wgpu TextureFormat mapping (DXT1/3/5, DXN, ARGB) - Custom Bevy AssetLoader for .xpr files - Orbit camera (LMB orbit, RMB pan, scroll zoom) - egui file browser + RE notes panel - CLI: extract / list / sniff / texture info / texture export - GitHub Actions CI (Linux, macOS, Windows, WASM) - Trunk WASM build config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
168
crates/sylpheed-formats/src/xiso.rs
Normal file
168
crates/sylpheed-formats/src/xiso.rs
Normal file
@@ -0,0 +1,168 @@
|
||||
//! XISO / XDVDFS disc image reading.
|
||||
//!
|
||||
//! Xbox 360 game discs use the XDVDFS filesystem (also called XISO).
|
||||
//! This module wraps the `xdvdfs` crate to extract game files into memory
|
||||
//! or onto disk for further processing.
|
||||
//!
|
||||
//! ## Reference projects
|
||||
//! - xdvdfs (Rust): https://github.com/antangelo/xdvdfs
|
||||
//! - extract-xiso (C): https://github.com/XboxDev/extract-xiso
|
||||
//! - Xenia emulator: https://github.com/xenia-canary/xenia-canary
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use tracing::{debug, info};
|
||||
use xdvdfs::layout::VolumeDescriptor;
|
||||
|
||||
/// A handle to an open XISO image.
|
||||
pub struct XisoReader<F: Read + Seek> {
|
||||
volume: VolumeDescriptor,
|
||||
file: F,
|
||||
}
|
||||
|
||||
impl<F: Read + Seek + Send + Sync + 'static> XisoReader<F> {
|
||||
pub async fn open(mut file: F) -> Result<Self> {
|
||||
let volume = xdvdfs::read::read_volume(&mut file)
|
||||
.await
|
||||
.context("Failed to read XDVDFS volume descriptor. Is this a valid Xbox 360 ISO?")?;
|
||||
|
||||
info!(
|
||||
"Opened XISO: root directory table at sector {}",
|
||||
{ let s = volume.root_table.region.sector; s }
|
||||
);
|
||||
Ok(Self { volume, file })
|
||||
}
|
||||
|
||||
/// List all files in the disc image, recursively (directories excluded).
|
||||
pub async fn list_all_files(&mut self) -> Result<Vec<String>> {
|
||||
let entries = self
|
||||
.volume
|
||||
.root_table
|
||||
.file_tree(&mut self.file)
|
||||
.await
|
||||
.context("Failed to walk XISO file tree")?;
|
||||
|
||||
let mut paths = Vec::new();
|
||||
for (parent, entry) in &entries {
|
||||
if entry.node.dirent.is_directory() {
|
||||
continue;
|
||||
}
|
||||
let name = entry
|
||||
.name_str::<std::io::Error>()
|
||||
.unwrap_or(std::borrow::Cow::Borrowed("<invalid>"));
|
||||
// file_tree builds parent paths with a leading slash (e.g. "/dat").
|
||||
// Trim it so we get "dat/filename" instead of "/dat/filename".
|
||||
let full_path = format!("{}/{}", parent, name);
|
||||
paths.push(full_path.trim_start_matches('/').to_string());
|
||||
}
|
||||
Ok(paths)
|
||||
}
|
||||
|
||||
/// Read the raw bytes of a file by its path inside the ISO.
|
||||
pub async fn read_file(&mut self, path: &str) -> Result<Vec<u8>> {
|
||||
let dirent = self
|
||||
.volume
|
||||
.root_table
|
||||
.walk_path(&mut self.file, path)
|
||||
.await
|
||||
.with_context(|| format!("File not found in ISO: {}", path))?;
|
||||
|
||||
let data = dirent
|
||||
.node
|
||||
.dirent
|
||||
.read_data_all(&mut self.file)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read file data: {}", path))?;
|
||||
|
||||
debug!("Read {} bytes from {}", data.len(), path);
|
||||
Ok(data.into_vec())
|
||||
}
|
||||
|
||||
/// Extract all files from the ISO into a directory on disk.
|
||||
pub async fn extract_all(&mut self, output_dir: &Path) -> Result<ExtractStats> {
|
||||
info!("Extracting ISO to {}", output_dir.display());
|
||||
std::fs::create_dir_all(output_dir).context("Failed to create output directory")?;
|
||||
|
||||
let entries = self
|
||||
.volume
|
||||
.root_table
|
||||
.file_tree(&mut self.file)
|
||||
.await
|
||||
.context("Failed to walk XISO file tree")?;
|
||||
|
||||
let mut stats = ExtractStats::default();
|
||||
for (parent, entry) in &entries {
|
||||
if entry.node.dirent.is_directory() {
|
||||
continue;
|
||||
}
|
||||
let name = entry
|
||||
.name_str::<std::io::Error>()
|
||||
.unwrap_or(std::borrow::Cow::Borrowed("<invalid>"));
|
||||
let rel_path = format!("{}/{}", parent, name);
|
||||
let rel_path = rel_path.trim_start_matches('/');
|
||||
|
||||
let disk_path =
|
||||
output_dir.join(rel_path.replace('/', std::path::MAIN_SEPARATOR_STR));
|
||||
|
||||
if let Some(parent_dir) = disk_path.parent() {
|
||||
std::fs::create_dir_all(parent_dir).with_context(|| {
|
||||
format!("Failed to create dir: {}", parent_dir.display())
|
||||
})?;
|
||||
}
|
||||
|
||||
let data = entry
|
||||
.node
|
||||
.dirent
|
||||
.read_data_all(&mut self.file)
|
||||
.await
|
||||
.with_context(|| format!("Failed to read: {}", rel_path))?;
|
||||
|
||||
std::fs::write(&disk_path, &*data)
|
||||
.with_context(|| format!("Failed to write: {}", disk_path.display()))?;
|
||||
|
||||
stats.files_extracted += 1;
|
||||
stats.bytes_extracted += data.len() as u64;
|
||||
debug!("Extracted {}", rel_path);
|
||||
}
|
||||
|
||||
info!(
|
||||
"Extraction complete: {} files, {} bytes",
|
||||
stats.files_extracted, stats.bytes_extracted
|
||||
);
|
||||
Ok(stats)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open an XISO from a file path (the common case).
|
||||
pub async fn open_iso(path: &Path) -> Result<XisoReader<std::fs::File>> {
|
||||
let file = std::fs::File::open(path)
|
||||
.with_context(|| format!("Cannot open ISO: {}", path.display()))?;
|
||||
XisoReader::open(file).await
|
||||
}
|
||||
|
||||
/// Statistics from an extraction operation.
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ExtractStats {
|
||||
pub files_extracted: usize,
|
||||
pub bytes_extracted: u64,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore = "requires a real ISO image — set SYLPHEED_ISO env var"]
|
||||
async fn test_list_iso() {
|
||||
let iso_path =
|
||||
std::env::var("SYLPHEED_ISO").expect("Set SYLPHEED_ISO to your ISO path");
|
||||
let mut reader = open_iso(Path::new(&iso_path)).await.unwrap();
|
||||
let files = reader.list_all_files().await.unwrap();
|
||||
for f in &files {
|
||||
println!("{}", f);
|
||||
}
|
||||
println!("Total: {} files", files.len());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user