diff --git a/crates/engine/src/cmd/recovery.rs b/crates/engine/src/cmd/recovery.rs index 9f046b39b..85650327a 100644 --- a/crates/engine/src/cmd/recovery.rs +++ b/crates/engine/src/cmd/recovery.rs @@ -252,6 +252,18 @@ impl FolderStore { } } + /// Create area folder `dir`, and the store's folder when it is missing. On Unix every folder + /// made here is owner-only (`0o700`): the copies are unsaved documents, and with the default + /// mode other accounts on the machine could read them. A store folder that already exists + /// keeps its mode, since it can be one the user picked in Preferences. + fn create_area(dir: &std::path::Path) -> std::result::Result<(), String> { + let mut builder = std::fs::DirBuilder::new(); + builder.recursive(true); + #[cfg(unix)] + std::os::unix::fs::DirBuilderExt::mode(&mut builder, 0o700); + builder.create(dir).map_err(|e| format!("{}: {e}", dir.display())) + } + /// The plain names of the files (or, `dirs`, folders) in `dir`; none when it doesn't exist. fn names(dir: &std::path::Path, dirs: bool) -> std::result::Result, String> { let entries = match std::fs::read_dir(dir) { @@ -285,7 +297,7 @@ impl RecoveryStore for FolderStore { fn write(&self, name: &str, bytes: &[u8]) -> std::result::Result<(), String> { let f = self.file(name)?; if let Some(dir) = f.parent() { - std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + Self::create_area(dir)?; } vectorcraft_format::write_atomic(&f, bytes).map_err(|e| format!("{}: {e}", f.display())) } @@ -304,7 +316,7 @@ impl RecoveryStore for FolderStore { } fn lock(&self, area: &str) -> std::result::Result { let dir = self.area(area)?; - std::fs::create_dir_all(&dir).map_err(|e| format!("{}: {e}", dir.display()))?; + Self::create_area(&dir)?; let path = dir.join(LOCK_FILE); let file = std::fs::OpenOptions::new() .read(true) diff --git a/crates/engine/src/tests_recovery.rs b/crates/engine/src/tests_recovery.rs index 3d732ab5c..e22d29d86 100644 --- a/crates/engine/src/tests_recovery.rs +++ b/crates/engine/src/tests_recovery.rs @@ -363,6 +363,33 @@ fn folder_areas_are_locked_by_running_apps() { let _ = std::fs::remove_dir_all(&dir); } +/// The copies are unsaved documents: on Unix the folders the store makes are owner-only, and a +/// store folder that already exists (one picked in Preferences) keeps its mode. +#[cfg(unix)] +#[test] +fn the_folder_store_makes_owner_only_folders() { + use std::os::unix::fs::PermissionsExt as _; + let mode = |p: &std::path::Path| std::fs::metadata(p).unwrap().permissions().mode() & 0o777; + let base = std::env::temp_dir().join(format!("vc-recovery-modes-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&base); + // A store folder that doesn't exist yet is made with the first copy, owner-only like its area. + let new = base.join("new"); + let store = FolderStore::new(&new); + store.write("1-1/Untitled-1-1.vectorcraft", b"copy").unwrap(); + assert_eq!(mode(&new), 0o700); + assert_eq!(mode(&new.join("1-1")), 0o700); + assert!(matches!(store.lock("1-2"), Ok(cmd::recovery::Lock::Held(_)))); + assert_eq!(mode(&new.join("1-2")), 0o700); + // A folder that already exists keeps its mode; the area made inside it is owner-only. + let picked = base.join("picked"); + std::fs::create_dir_all(&picked).unwrap(); + std::fs::set_permissions(&picked, std::fs::Permissions::from_mode(0o755)).unwrap(); + FolderStore::new(&picked).write("2-1/Untitled-1-1.vectorcraft", b"copy").unwrap(); + assert_eq!(mode(&picked), 0o755); + assert_eq!(mode(&picked.join("2-1")), 0o700); + let _ = std::fs::remove_dir_all(&base); +} + #[test] fn saving_reverting_or_closing_removes_the_copy() { let dir = std::env::temp_dir().join(format!("vc-recovery-save-{}", std::process::id()));