Phase C+11.1 — `cache:/access` / `cache:/recent` dir-vs-file fix ================================================================ File: crates/xenia-kernel/src/exports.rs Function: open_cache_file Hunk 1: replace the post-existing-file-wins block with a disposition-gated mkdir + STATUS_OBJECT_NAME_NOT_FOUND on miss. Pre-fix (Phase C+11 HEAD): let host_exists_as_dir = host_path.is_dir(); let host_exists_as_file = host_path.is_file(); let is_dir_open = host_exists_as_dir || (!host_exists_as_file && !want_non_dir && want_dir); if is_dir_open { // For non-existent paths the guest wants us to create as a // directory, mkdir-p; canary's HostPathDevice does the same // when FILE_DIRECTORY_FILE is set on a kCreate disposition. if want_dir && !host_path.exists() { if let Err(e) = std::fs::create_dir_all(host_path) { ...STATUS_UNSUCCESSFUL... } } // ... falls through to SUCCESS / handle alloc Post-fix (Phase C+11.1): let host_exists_as_dir = host_path.is_dir(); let host_exists_as_file = host_path.is_file(); let is_dir_open = host_exists_as_dir || (!host_exists_as_file && !want_non_dir && want_dir); if is_dir_open { // Phase C+11.1 — only create the host directory when the // disposition is *create-capable*. Mirrors canary's // `VirtualFileSystem::OpenFile` (virtual_file_system.cc:265-273): // for `FileDisposition::kOpen`/`kOverwrite` on a non-existent // path the function returns `X_STATUS_OBJECT_NAME_NOT_FOUND` // *before* any `CreatePath` call — i.e. mkdir is never invoked // on these dispositions. The pre-fix code (Phase C+11) called // `create_dir_all` whenever `want_dir && !host_path.exists()`, // so Sylpheed's cold-boot probes for `cache:/access`, // `cache:/ignore`, `cache:/recent` (disp=1, opts=0x7) succeeded // and produced spurious host directories. Canary instead // returns NOT_FOUND, after which Sylpheed re-creates these as // FILES via `disp=5` + `FILE_NON_DIRECTORY_FILE`. // // Create-capable dispositions (mkdir OK): // 0 FILE_SUPERSEDE // 2 FILE_CREATE // 3 FILE_OPEN_IF // 5 FILE_OVERWRITE_IF // Non-create dispositions (must miss when path is absent): // 1 FILE_OPEN // 4 FILE_OVERWRITE let disp_is_create_capable = matches!( create_disposition, FILE_SUPERSEDE | FILE_CREATE | FILE_OPEN_IF | FILE_OVERWRITE_IF ); if !host_path.exists() { if !disp_is_create_capable { if handle_out != 0 { mem.write_u32(handle_out, 0); } write_io_status_block( mem, io_status_block, STATUS_OBJECT_NAME_NOT_FOUND as u32, 0, ); tracing::info!( "cache open (dir) MISS path={:?} disp={} opts={:#x} -> NOT_FOUND", guest_path, create_disposition, create_options ); return STATUS_OBJECT_NAME_NOT_FOUND; } // create-capable + want_dir → mkdir-p the directory. if want_dir { if let Err(e) = std::fs::create_dir_all(host_path) { ...STATUS_UNSUCCESSFUL... } } } // ... falls through to SUCCESS / handle alloc as before Hunk 2 (tests): two new unit tests added in crates/xenia-kernel/src/exports.rs after `cache_top_level_manifests_create_as_files`: - cache_open_directory_on_missing_path_returns_not_found Loops over the three cold-probe paths Sylpheed actually emits (cache:\\access, cache:\\ignore, cache:\\recent) and asserts NtCreateFile + disp=1 + opts=0x7 returns NOT_FOUND, writes handle=0, and leaves no host entry on disk. - cache_disp5_after_disp1_miss_creates_file Pins the canary two-call sequence: cold disp=1 returns NOT_FOUND; immediately following disp=5 + opts=0x60 (FILE_NON_DIRECTORY_FILE) succeeds and produces a host FILE. LOC summary: ~30 added in open_cache_file (mkdir gate + NOT_FOUND return branch + comments), ~6 removed (the unconditional mkdir flow); ~88 lines of test code for the two new tests.