From 6eb0392aaaa4120d0bf5ee951fadafa85eb7469e Mon Sep 17 00:00:00 2001 From: DuProcess <273172371+DuProcess@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:00:17 -0400 Subject: [PATCH] Preserve Windows readonly file modes --- src/bin/dosh-client.rs | 60 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/src/bin/dosh-client.rs b/src/bin/dosh-client.rs index 3f4d0f4..10d6a34 100644 --- a/src/bin/dosh-client.rs +++ b/src/bin/dosh-client.rs @@ -3811,7 +3811,19 @@ fn mode_for(path: &Path, preserve: bool) -> Result> { } } -#[cfg(not(unix))] +#[cfg(windows)] +fn mode_for(path: &Path, preserve: bool) -> Result> { + if !preserve { + return Ok(None); + } + let metadata = fs::metadata(path)?; + Ok(Some(windows_mode_from_readonly( + metadata.is_dir(), + metadata.permissions().readonly(), + ))) +} + +#[cfg(not(any(unix, windows)))] fn mode_for(_path: &Path, _preserve: bool) -> Result> { Ok(None) } @@ -3830,11 +3842,40 @@ fn set_local_mode(path: &Path, mode: Option, preserve: bool) -> Result<()> fs::set_permissions(path, permissions).with_context(|| format!("chmod {}", path.display())) } -#[cfg(not(unix))] +#[cfg(windows)] +fn set_local_mode(path: &Path, mode: Option, preserve: bool) -> Result<()> { + if !preserve { + return Ok(()); + } + let Some(mode) = mode else { + return Ok(()); + }; + let mut permissions = fs::metadata(path)?.permissions(); + permissions.set_readonly(windows_readonly_from_mode(mode)); + fs::set_permissions(path, permissions) + .with_context(|| format!("set readonly mode {}", path.display())) +} + +#[cfg(not(any(unix, windows)))] fn set_local_mode(_path: &Path, _mode: Option, _preserve: bool) -> Result<()> { Ok(()) } +#[cfg_attr(not(any(test, windows)), allow(dead_code))] +fn windows_mode_from_readonly(is_dir: bool, readonly: bool) -> u32 { + match (is_dir, readonly) { + (true, true) => 0o555, + (true, false) => 0o777, + (false, true) => 0o444, + (false, false) => 0o666, + } +} + +#[cfg_attr(not(any(test, windows)), allow(dead_code))] +fn windows_readonly_from_mode(mode: u32) -> bool { + mode & 0o222 == 0 +} + fn parse_local_forwards(raw: &[String]) -> Result> { raw.iter().map(|value| parse_local_forward(value)).collect() } @@ -10100,7 +10141,7 @@ mod tests { toml_bare_key_or_quoted, top_trace_events, trace_report_warnings, unix_update_script, update_binary_version_for_installer, update_installer_url, update_version_status, upsert_managed_block, valid_forward_host, vscode_safe_alias, wake_repaint_retry_deadline, - windows_update_script, + windows_mode_from_readonly, windows_readonly_from_mode, windows_update_script, }; use dosh::config::{ClientConfig, CommandExtension, HostConfig}; use dosh::native::EnvVar; @@ -10186,6 +10227,19 @@ mod tests { assert!(local_symlink_target_is_dir(&link, "missing-dir\\")); } + #[test] + fn windows_mode_mapping_preserves_readonly_semantics() { + assert_eq!(windows_mode_from_readonly(false, true), 0o444); + assert_eq!(windows_mode_from_readonly(false, false), 0o666); + assert_eq!(windows_mode_from_readonly(true, true), 0o555); + assert_eq!(windows_mode_from_readonly(true, false), 0o777); + + assert!(windows_readonly_from_mode(0o444)); + assert!(windows_readonly_from_mode(0o555)); + assert!(!windows_readonly_from_mode(0o644)); + assert!(!windows_readonly_from_mode(0o755)); + } + #[test] fn cleanup_stream_state_removes_all_per_stream_state_only_for_target() { let stream_id = 42;