Skip to main content

codex_cli/
fs.rs

1use anyhow::{Context, Result};
2use sha2::{Digest, Sha256};
3use std::fs::{self, File, OpenOptions};
4use std::io::{Read, Write};
5use std::path::{Path, PathBuf};
6use std::time::{SystemTime, UNIX_EPOCH};
7
8#[cfg(unix)]
9use std::os::unix::fs::PermissionsExt;
10
11pub const SECRET_FILE_MODE: u32 = 0o600;
12
13pub fn sha256_file(path: &Path) -> Result<String> {
14    let mut file = File::open(path)
15        .with_context(|| format!("failed to open for sha256: {}", path.display()))?;
16    let mut hasher = Sha256::new();
17    let mut buf = [0u8; 8192];
18    loop {
19        let read = file.read(&mut buf)?;
20        if read == 0 {
21            break;
22        }
23        hasher.update(&buf[..read]);
24    }
25    let digest = hasher.finalize();
26    let mut out = String::with_capacity(digest.len() * 2);
27    for byte in digest {
28        out.push_str(&format!("{:02x}", byte));
29    }
30    Ok(out)
31}
32
33pub fn write_atomic(path: &Path, contents: &[u8], mode: u32) -> Result<()> {
34    if let Some(parent) = path.parent() {
35        fs::create_dir_all(parent)
36            .with_context(|| format!("failed to create dir: {}", parent.display()))?;
37    }
38
39    let mut attempt = 0u32;
40    loop {
41        let tmp_path = temp_path(path, attempt);
42        match OpenOptions::new()
43            .write(true)
44            .create_new(true)
45            .open(&tmp_path)
46        {
47            Ok(mut file) => {
48                file.write_all(contents).with_context(|| {
49                    format!("failed to write temp file: {}", tmp_path.display())
50                })?;
51                file.flush().ok();
52
53                set_permissions(&tmp_path, mode)?;
54                drop(file);
55
56                fs::rename(&tmp_path, path).with_context(|| {
57                    format!(
58                        "failed to rename {} -> {}",
59                        tmp_path.display(),
60                        path.display()
61                    )
62                })?;
63                set_permissions(path, mode)?;
64                return Ok(());
65            }
66            Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
67                attempt += 1;
68                if attempt > 10 {
69                    return Err(err).context("failed to create unique temp file");
70                }
71            }
72            Err(err) => return Err(err).context("failed to create temp file"),
73        }
74    }
75}
76
77pub fn write_timestamp(path: &Path, iso: Option<&str>) -> Result<()> {
78    if let Some(parent) = path.parent() {
79        fs::create_dir_all(parent)
80            .with_context(|| format!("failed to create dir: {}", parent.display()))?;
81    }
82
83    if let Some(raw) = iso {
84        let trimmed = raw.split(&['\n', '\r'][..]).next().unwrap_or("");
85        if !trimmed.is_empty() {
86            fs::write(path, trimmed)
87                .with_context(|| format!("failed to write timestamp: {}", path.display()))?;
88            return Ok(());
89        }
90    }
91
92    let _ = fs::remove_file(path);
93    Ok(())
94}
95
96#[cfg(unix)]
97fn set_permissions(path: &Path, mode: u32) -> Result<()> {
98    let perm = fs::Permissions::from_mode(mode);
99    fs::set_permissions(path, perm)
100        .with_context(|| format!("failed to set permissions: {}", path.display()))?;
101    Ok(())
102}
103
104#[cfg(not(unix))]
105fn set_permissions(_path: &Path, _mode: u32) -> Result<()> {
106    Ok(())
107}
108
109fn temp_path(path: &Path, attempt: u32) -> PathBuf {
110    let filename = path
111        .file_name()
112        .and_then(|name| name.to_str())
113        .unwrap_or("tmp");
114    let pid = std::process::id();
115    let nanos = SystemTime::now()
116        .duration_since(UNIX_EPOCH)
117        .map(|d| d.as_nanos())
118        .unwrap_or(0);
119    let tmp_name = format!(".{filename}.tmp-{pid}-{nanos}-{attempt}");
120    path.with_file_name(tmp_name)
121}