Skip to main content

lean_ctx/
config_io.rs

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) {}