//! Path normalization for kernel file I/O. //! //! Guests pass file paths inside an `OBJECT_ATTRIBUTES` struct that points at //! an `ANSI_STRING` descriptor. Those paths come in several Xbox-flavored //! forms — NT device paths (`\Device\Cdrom0\...`), drive letters (`D:\...`, //! `d:\...`), or symbolic link prefixes (`game:\...`). We strip whichever //! prefix applies and return a plain slash-separated path relative to the //! mounted VFS root, so `VfsDevice::read_file` can look it up directly. use xenia_memory::{GuestMemory, MemoryAccess}; /// Xbox `ANSI_STRING`: /// u16 Length /// u16 MaximumLength /// u32 Buffer (guest pointer) fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option { if ptr == 0 { return None; } let length = mem.read_u16(ptr) as u32; let buffer = mem.read_u32(ptr + 4); if buffer == 0 || length == 0 { return Some(String::new()); } let mut out = String::with_capacity(length as usize); for i in 0..length { let c = mem.read_u8(buffer + i); if c == 0 { break; } out.push(c as char); } Some(out) } /// Xbox `OBJECT_ATTRIBUTES`: /// u32 RootDirectory (handle) /// u32 Name (pointer to ANSI_STRING) /// u32 Attributes fn read_object_attributes_name(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option { if obj_attrs_ptr == 0 { return None; } let name_ptr = mem.read_u32(obj_attrs_ptr + 4); read_ansi_string(mem, name_ptr) } /// Known Xbox device prefixes that need to be stripped before looking a path /// up in the VFS. The list mirrors the symbolic links xenia-canary sets up /// at boot (see `xboxkrnl_io.cc`). Case-insensitive matching. const DEVICE_PREFIXES: &[&str] = &[ "\\Device\\Cdrom0\\", "\\Device\\Harddisk0\\Partition1\\", "\\Device\\Harddisk0\\Partition0\\", "\\Device\\Harddisk0\\", "\\Device\\Mu0\\", "\\Device\\Mu1\\", "\\Device\\Mass0\\", "\\Device\\Mass1\\", "\\Device\\Mass2\\", "\\SystemRoot\\", "\\??\\", "game:\\", "d:\\", "D:\\", ]; /// Strip any Xbox device prefix and normalize backslashes to forward slashes. /// Returns the path relative to the VFS root. pub fn normalize_path(raw: &str) -> String { let mut s = raw.trim().to_string(); // Case-insensitive prefix strip. let lowered = s.to_ascii_lowercase(); for prefix in DEVICE_PREFIXES { let pl = prefix.to_ascii_lowercase(); if lowered.starts_with(&pl) { s = s[pl.len()..].to_string(); break; } } // Drop any leading slash/backslash that survived prefix stripping. while s.starts_with('\\') || s.starts_with('/') { s.remove(0); } // Canonical form: forward slashes. s.replace('\\', "/") } /// Convenience: read the OBJECT_ATTRIBUTES struct at `obj_attrs_ptr` and /// return a normalized VFS path. Returns `None` if the struct pointer or its /// inner name pointer is null. pub fn object_attributes_to_vfs_path(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option { let raw = read_object_attributes_name(mem, obj_attrs_ptr)?; if raw.is_empty() { return None; } Some(normalize_path(&raw)) } #[cfg(test)] mod tests { use super::*; #[test] fn strips_device_cdrom() { assert_eq!(normalize_path("\\Device\\Cdrom0\\default.xex"), "default.xex"); } #[test] fn strips_drive_letter_lowercase() { assert_eq!(normalize_path("d:\\media\\shared\\foo.pkg"), "media/shared/foo.pkg"); } #[test] fn strips_drive_letter_uppercase() { assert_eq!(normalize_path("D:\\default.xex"), "default.xex"); } #[test] fn strips_game_prefix() { assert_eq!(normalize_path("game:\\data\\whatever.bin"), "data/whatever.bin"); } #[test] fn preserves_relative_path() { assert_eq!(normalize_path("scripts/init.lua"), "scripts/init.lua"); } #[test] fn handles_partition1() { assert_eq!( normalize_path("\\Device\\Harddisk0\\Partition1\\content\\abc.sav"), "content/abc.sav" ); } }