diff --git a/crates/xenia-kernel/src/exports.rs b/crates/xenia-kernel/src/exports.rs index d0dcbd0..344fa1b 100644 --- a/crates/xenia-kernel/src/exports.rs +++ b/crates/xenia-kernel/src/exports.rs @@ -1652,6 +1652,79 @@ fn nt_set_information_file(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut return; } + // XFileRenameInformation (10): move the backing file to a new path. + // Sylpheed's asset-cache decompresses each packed resource to a staging + // `cache:\.tmp` then renames it into its final nested path + // `cache:\\\`. Without an actual host-FS rename the + // nested target stays empty, the later read-back of the decompressed + // asset (e.g. the title logo texture `\69d8e45c\e\534ffea`) misses, and + // the logo never loads. Mirror canary `xboxkrnl_io_info.cc:226` + // (`X_FILE_RENAME_INFORMATION{ replace_existing@0, root_dir_handle@4, + // ansi_string@8 }` → `file->Rename(TranslateAnsiPath(ansi_string))`). + if info_class == 10 { + // Read the target path from the embedded ANSI_STRING at info_ptr+8. + let target_raw = match crate::path::read_ansi_string(mem, info_ptr + 8) { + Some(s) if !s.is_empty() => s, + _ => { + const STATUS_OBJECT_NAME_INVALID: u64 = 0xC000_0033; + ctx.gpr[3] = STATUS_OBJECT_NAME_INVALID; + return; + } + }; + // Resolve the destination against the host cache backing dir. We only + // support renames within the writable `cache:` mount (the only place + // a guest can create files); disc/synth entries are read-only. + let new_host = state.resolve_cache_path(&target_raw); + // Current backing host path of the handle. + let old_host = match state.objects.get(&handle) { + Some(KernelObject::File { host_path: Some(hp), .. }) => Some(hp.clone()), + Some(KernelObject::File { .. }) => None, + _ => { + ctx.gpr[3] = STATUS_INVALID_HANDLE; + return; + } + }; + let status: u64 = match (old_host, new_host) { + (Some(old), Some(new)) => { + if let Some(parent) = new.parent() { + let _ = std::fs::create_dir_all(parent); + } + match std::fs::rename(&old, &new) { + Ok(()) => { + // Update the handle so subsequent I/O targets the new + // host path + guest path. + if let Some(KernelObject::File { path, host_path, .. }) = + state.objects.get_mut(&handle) + { + *path = crate::path::normalize_path(&target_raw); + *host_path = Some(new.clone()); + } + tracing::info!( + "NtSetInformationFile rename cache {:?} -> {:?} ({:?})", + old, new, target_raw + ); + STATUS_SUCCESS + } + Err(e) => { + tracing::warn!( + "NtSetInformationFile rename {:?} -> {:?} failed: {}", + old, new, e + ); + STATUS_UNSUCCESSFUL + } + } + } + // Non-cache (read-only VFS) source/target: acknowledge without a + // host move, matching the prior permissive behaviour. + _ => STATUS_SUCCESS, + }; + if iosb_ptr != 0 { + write_io_status_block(mem, iosb_ptr, status as u32, info_length); + } + ctx.gpr[3] = status; + return; + } + // Handle lookup. let Some(KernelObject::File { size, position, host_path, .. }) = state.objects.get_mut(&handle) else { ctx.gpr[3] = STATUS_INVALID_HANDLE; @@ -5581,6 +5654,67 @@ mod tests { } } + /// `NtSetInformationFile` class 10 (`XFileRenameInformation`) must move + /// the backing host file to the new `cache:` path and update the handle. + /// Mirrors Sylpheed's asset-cache `.tmp` → `\\\` move; + /// without it the nested target stays empty and the decompressed asset + /// (logo texture) never reads back. Faithful to canary `file->Rename`. + #[test] + fn nt_set_information_file_rename_moves_cache_file() { + let (mut ctx, mut mem, mut state) = fresh(); + // Real temp cache root + a staging `.tmp` file with known bytes. + let root = std::env::temp_dir().join(format!("xenia-rs-rename-test-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).unwrap(); + let old_host = root.join("69d8e45ce534ffea.tmp"); + std::fs::write(&old_host, b"LOGOTEX!").unwrap(); + state.cache_root = Some(root.clone()); + // Open handle whose backing host_path is the staging file. + let handle = state.alloc_handle_for(KernelObject::File { + path: "69d8e45ce534ffea.tmp".to_string(), + size: 8, + position: 0, + data: Arc::new(Vec::new()), + dir_enum_pos: None, + host_path: Some(old_host.clone()), + }); + // X_FILE_RENAME_INFORMATION { replace@0, root_dir@4, ANSI_STRING@8 }. + // ANSI_STRING { len u16, max u16, buf u32 } at info_ptr+8; buffer holds + // the target path "cache:\69d8e45c\e\534ffea". + let info_ptr = SCRATCH_BASE + 0x100; + let str_buf = SCRATCH_BASE + 0x200; + let target = b"cache:\\69d8e45c\\e\\534ffea"; + for (i, b) in target.iter().enumerate() { + mem.write_u8(str_buf + i as u32, *b); + } + mem.write_u32(info_ptr, 0); // replace_existing + mem.write_u32(info_ptr + 4, 0); // root_dir_handle + mem.write_u16(info_ptr + 8, target.len() as u16); // ANSI_STRING.Length + mem.write_u16(info_ptr + 10, target.len() as u16); // MaximumLength + mem.write_u32(info_ptr + 12, str_buf); // Buffer + let iosb_ptr = SCRATCH_BASE + 0x140; + ctx.gpr[3] = handle as u64; + ctx.gpr[4] = iosb_ptr as u64; + ctx.gpr[5] = info_ptr as u64; + ctx.gpr[6] = 16; + ctx.gpr[7] = 10; // XFileRenameInformation + nt_set_information_file(&mut ctx, &mut mem, &mut state); + assert_eq!(ctx.gpr[3], STATUS_SUCCESS); + // Staging file gone; nested target exists with the same bytes. + let new_host = root.join("69d8e45c").join("e").join("534ffea"); + assert!(!old_host.exists(), "staging .tmp should be moved away"); + assert_eq!(std::fs::read(&new_host).unwrap(), b"LOGOTEX!"); + // Handle now points at the new host + guest path. + match state.objects.get(&handle) { + Some(KernelObject::File { host_path: Some(hp), path, .. }) => { + assert_eq!(hp, &new_host); + assert_eq!(path, "cache:/69d8e45c/e/534ffea"); + } + _ => panic!("file handle lost or host_path missing"), + } + let _ = std::fs::remove_dir_all(&root); + } + /// Read-only VFS — truncating to a different size must fail with /// `STATUS_UNSUCCESSFUL`, matching Canary's error path when /// `file->SetLength(...)` can't honour the request. diff --git a/crates/xenia-kernel/src/path.rs b/crates/xenia-kernel/src/path.rs index edb7e7d..659f414 100644 --- a/crates/xenia-kernel/src/path.rs +++ b/crates/xenia-kernel/src/path.rs @@ -13,7 +13,7 @@ use xenia_memory::{GuestMemory, MemoryAccess}; /// u16 Length /// u16 MaximumLength /// u32 Buffer (guest pointer) -fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option { +pub fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option { if ptr == 0 { return None; }