diff --git a/crates/core/src/session/mod.rs b/crates/core/src/session/mod.rs index 76b577b..763f84e 100644 --- a/crates/core/src/session/mod.rs +++ b/crates/core/src/session/mod.rs @@ -236,7 +236,7 @@ fn create_session_tree(dir: &Path) -> Result<(), AppError> { } #[cfg(not(unix))] { - std::fs::create_dir_all(dir.join("trace"))?; + crate::private_file_parent::ensure_private(&dir.join("trace"))?; } Ok(()) } diff --git a/crates/core/src/trace.rs b/crates/core/src/trace.rs index c40b30d..c9fafe0 100644 --- a/crates/core/src/trace.rs +++ b/crates/core/src/trace.rs @@ -248,7 +248,7 @@ pub(crate) fn ensure_trace_dir(dir: &Path) -> Result<(), AppError> { .create(dir)?; } #[cfg(not(unix))] - std::fs::create_dir_all(dir)?; + crate::private_file_parent::ensure_private(dir)?; Ok(()) } diff --git a/crates/ffi/tests/c_abi_windows_private_file_install.rs b/crates/ffi/tests/c_abi_windows_private_file_install.rs index 4eff168..d396d9c 100644 --- a/crates/ffi/tests/c_abi_windows_private_file_install.rs +++ b/crates/ffi/tests/c_abi_windows_private_file_install.rs @@ -8,6 +8,8 @@ use common::{ad_adapter_create, ad_adapter_destroy, with_isolated_home}; use std::path::{Path, PathBuf}; use std::process::Command; +const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x400; + struct ProbeOps; impl PrivateFileOps for ProbeOps {} @@ -24,27 +26,34 @@ fn plant_junction(link: &Path, target: &Path) { "mklink /J must succeed without privilege: {}", String::from_utf8_lossy(&output.stderr) ); + let attributes = { + use std::os::windows::fs::MetadataExt; + std::fs::symlink_metadata(link) + .expect("junction link exists") + .file_attributes() + }; + assert!( + attributes & FILE_ATTRIBUTE_REPARSE_POINT != 0, + "planted link must carry FILE_ATTRIBUTE_REPARSE_POINT" + ); } -fn regular_files_under(root: &Path) -> Vec { - let mut files = Vec::new(); +fn entries_under(root: &Path) -> Vec { + let mut entries = Vec::new(); let mut pending = vec![root.to_path_buf()]; while let Some(directory) = pending.pop() { - let Ok(entries) = std::fs::read_dir(&directory) else { + let Ok(read) = std::fs::read_dir(&directory) else { continue; }; - for entry in entries.flatten() { - let Ok(file_type) = entry.file_type() else { - continue; - }; - if file_type.is_dir() { - pending.push(entry.path()); - } else { - files.push(entry.path()); + for entry in read.flatten() { + let path = entry.path(); + if entry.file_type().is_ok_and(|file_type| file_type.is_dir()) { + pending.push(path.clone()); } + entries.push(path); } } - files + entries } /// Core's five private-file primitives are `pub(crate)`, so the behavioral arm @@ -86,10 +95,10 @@ fn adapter_create_without_ad_init_installs_the_windows_private_file_ops() { "a manifest write through a junction component must be refused \ by the installed WindowsPrivateFile" ); - let leaked = regular_files_under(&target); + let leaked = entries_under(&target); assert!( leaked.is_empty(), - "no session artifact may land under the junction target: {leaked:?}" + "no session artifact — file or directory — may land under the junction target: {leaked:?}" ); }); } diff --git a/src/tests/windows_private_file_install.rs b/src/tests/windows_private_file_install.rs index 766b983..5c10419 100644 --- a/src/tests/windows_private_file_install.rs +++ b/src/tests/windows_private_file_install.rs @@ -74,25 +74,22 @@ fn parse_envelope(output: &Output) -> serde_json::Value { serde_json::from_slice(&output.stdout).expect("stdout is one JSON envelope") } -fn regular_files_under(root: &Path) -> Vec { - let mut files = Vec::new(); +fn entries_under(root: &Path) -> Vec { + let mut entries = Vec::new(); let mut pending = vec![root.to_path_buf()]; while let Some(directory) = pending.pop() { - let Ok(entries) = std::fs::read_dir(&directory) else { + let Ok(read) = std::fs::read_dir(&directory) else { continue; }; - for entry in entries.flatten() { - let Ok(file_type) = entry.file_type() else { - continue; - }; - if file_type.is_dir() { - pending.push(entry.path()); - } else { - files.push(entry.path()); + for entry in read.flatten() { + let path = entry.path(); + if entry.file_type().is_ok_and(|file_type| file_type.is_dir()) { + pending.push(path.clone()); } + entries.push(path); } } - files + entries } #[test] @@ -112,10 +109,10 @@ fn session_start_through_a_junction_home_is_refused_by_the_installed_windows_ops success means the portable default wrote through the junction" ); assert_eq!(envelope["ok"], false); - let leaked = regular_files_under(&target); + let leaked = entries_under(&target); assert!( leaked.is_empty(), - "no session artifact may land under the junction target: {leaked:?}" + "no session artifact — file or directory — may land under the junction target: {leaked:?}" ); }