1use std::path::{Path, PathBuf};
2
3fn backup_path_for(path: &Path) -> Option<PathBuf> {
4 let filename = path.file_name()?.to_string_lossy();
5 let ts = std::time::SystemTime::now()
6 .duration_since(std::time::UNIX_EPOCH)
7 .map_or(0, |d| d.as_secs());
8 Some(path.with_file_name(format!("{filename}.lean-ctx.{ts}.bak")))
9}
10
11pub fn snapshot_mtime(path: &Path) -> Option<std::time::SystemTime> {
12 std::fs::metadata(path).ok().and_then(|m| m.modified().ok())
13}
14
15pub fn write_atomic_with_backup(path: &Path, content: &str) -> Result<(), String> {
16 write_atomic_with_backup_checked(path, content, None)
17}
18
19pub fn write_atomic_with_backup_checked(
20 path: &Path,
21 content: &str,
22 expected_mtime: Option<std::time::SystemTime>,
23) -> Result<(), String> {
24 if path.exists() {
25 if let Some(expected) = expected_mtime {
26 let current = snapshot_mtime(path);
27 if current != Some(expected) {
28 return Err(format!(
29 "file was modified externally since last read: {}",
30 path.display()
31 ));
32 }
33 }
34 if let Some(bak) = backup_path_for(path) {
35 let _ = std::fs::copy(path, &bak);
36 }
37 }
38
39 write_atomic(path, content)
40}
41
42pub fn write_atomic(path: &Path, content: &str) -> Result<(), String> {
43 reject_symlink(path)?;
44
45 if let Some(parent) = path.parent() {
46 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
47 }
48
49 let parent = path
50 .parent()
51 .ok_or_else(|| "invalid path (no parent directory)".to_string())?;
52 let filename = path
53 .file_name()
54 .ok_or_else(|| "invalid path (no filename)".to_string())?
55 .to_string_lossy();
56
57 let pid = std::process::id();
58 let nanos = std::time::SystemTime::now()
59 .duration_since(std::time::UNIX_EPOCH)
60 .map_or(0, |d| d.as_nanos());
61
62 let tmp = parent.join(format!(".{filename}.lean-ctx.tmp.{pid}.{nanos}"));
63 std::fs::write(&tmp, content).map_err(|e| e.to_string())?;
64
65 #[cfg(windows)]
66 {
67 if path.exists() {
68 let _ = std::fs::remove_file(path);
69 }
70 }
71
72 std::fs::rename(&tmp, path).map_err(|e| {
73 format!(
74 "atomic write failed: {} (tmp: {})",
75 e,
76 tmp.to_string_lossy()
77 )
78 })?;
79
80 restrict_file_permissions(path);
81
82 Ok(())
83}
84
85fn reject_symlink(path: &Path) -> Result<(), String> {
86 if path.exists()
87 && path
88 .symlink_metadata()
89 .is_ok_and(|m| m.file_type().is_symlink())
90 {
91 return Err(format!(
92 "refusing to write through symlink: {}",
93 path.display()
94 ));
95 }
96 Ok(())
97}
98
99#[cfg(unix)]
100fn restrict_file_permissions(path: &Path) {
101 use std::os::unix::fs::PermissionsExt;
102 let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
103}
104
105#[cfg(not(unix))]
106fn restrict_file_permissions(_path: &Path) {}