|
|
|
|
@@ -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:\<hash><tail>.tmp` then renames it into its final nested path
|
|
|
|
|
// `cache:\<hash>\<dir>\<file>`. 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` → `\<hash>\<dir>\<file>` 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.
|
|
|
|
|
|