Coverage Report

Created: 2026-07-21 00:46

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-stt-registry-types/src/fs.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
//! Small filesystem helpers shared by the daemon and the indexer.
3
4
use std::io::Write;
5
use std::path::{Path, PathBuf};
6
7
/// Atomically write `contents` to `path`: write a sibling temp file, flush and
8
/// `fsync` it, then rename it over `path`. A crash mid-write leaves either the
9
/// old file or the new one, never a truncated mix. The temp file lives in the
10
/// same directory so the rename stays on one filesystem (a cross-device rename
11
/// is not atomic).
12
///
13
/// Replaces the daemon's cache-write and the indexer's two `index.json` writers
14
/// (which wrote in place, non-atomically, and disagreed on a trailing newline).
15
///
16
/// # Errors
17
/// Returns any I/O error from creating/writing/syncing the temp file or the
18
/// final rename. On error the temp file is best-effort removed.
19
18
pub fn write_atomic(path: &Path, contents: &[u8]) -> std::io::Result<()> {
20
18
    let mut tmp = path.as_os_str().to_owned();
21
18
    tmp.push(".tmp");
22
18
    let tmp = PathBuf::from(tmp);
23
24
    // Scope the file handle so it is closed before the rename.
25
18
    let write_result = (|| {
26
18
        let mut f = std::fs::File::create(&tmp)
?0
;
27
18
        f.write_all(contents)
?0
;
28
18
        f.flush()
?0
;
29
18
        f.sync_all()
30
    })();
31
18
    if let Err(
e0
) = write_result {
32
0
        let _ = std::fs::remove_file(&tmp);
33
0
        return Err(e);
34
18
    }
35
36
18
    if let Err(
e0
) = std::fs::rename(&tmp, path) {
37
0
        let _ = std::fs::remove_file(&tmp);
38
0
        return Err(e);
39
18
    }
40
18
    Ok(())
41
18
}
42
43
#[cfg(test)]
44
mod tests {
45
    use super::write_atomic;
46
47
    #[test]
48
2
    fn writes_and_overwrites_atomically() {
49
2
        let dir = std::env::temp_dir().join(format!("stt-write-atomic-{}", std::process::id()));
50
2
        std::fs::create_dir_all(&dir).unwrap();
51
2
        let path = dir.join("index.json");
52
53
2
        write_atomic(&path, b"first").unwrap();
54
2
        assert_eq!(std::fs::read(&path).unwrap(), b"first");
55
        // Overwriting replaces the contents and leaves no `.tmp` sibling.
56
2
        write_atomic(&path, b"second").unwrap();
57
2
        assert_eq!(std::fs::read(&path).unwrap(), b"second");
58
2
        let mut tmp = path.clone().into_os_string();
59
2
        tmp.push(".tmp");
60
2
        assert!(
61
2
            !std::path::Path::new(&tmp).exists(),
62
            "temp file must be gone"
63
        );
64
65
2
        std::fs::remove_dir_all(&dir).unwrap();
66
2
    }
67
}