Skip to main content

safemesh_crdt/
persistence.rs

1// Copyright (C) 2026 Ben Cassie
2// SPDX-License-Identifier: Apache-2.0
3//! Full-file replacement extracted from m2slice::Replica::persist. Callers
4//! serialize writers and keep the destination directory in place. Syncing the
5//! directory completes durability of the rename on the supported local FS.
6use std::{
7    fs::{self, File},
8    io::{self, Write},
9    path::Path,
10};
11
12pub(crate) fn replace(path: &Path, bytes: &[u8]) -> io::Result<()> {
13    let parent = File::open(path.parent().ok_or_else(|| io::Error::other("no parent"))?)?;
14    checkpoint(1)?;
15    let temporary = path.with_extension("tmp");
16    let mut file = File::create(&temporary)?;
17    // Exercise a short write followed by an I/O error, not only a failure
18    // before any bytes reached the temporary file.
19    #[cfg(test)]
20    if FAULT.with(|fault| fault.get() == (2, false)) {
21        file.write_all(&bytes[..bytes.len() / 2])?;
22    }
23    checkpoint(2)?;
24    file.write_all(bytes)?;
25    checkpoint(3)?;
26    file.sync_all()?;
27    checkpoint(4)?;
28    fs::rename(&temporary, path)?;
29    checkpoint(5)?;
30    parent.sync_all()?;
31    checkpoint(6)?;
32    Ok(())
33}
34
35pub(crate) fn checkpoint(_boundary: u8) -> io::Result<()> {
36    #[cfg(test)]
37    FAULT.with(|fault| {
38        let (boundary, crash) = fault.get();
39        if boundary == _boundary {
40            if crash {
41                std::process::exit(77);
42            }
43            return Err(io::Error::other("injected uncertain I/O"));
44        }
45        Ok(())
46    })?;
47    Ok(())
48}
49
50#[cfg(test)]
51std::thread_local! {
52    pub(crate) static FAULT: std::cell::Cell<(u8, bool)> = const { std::cell::Cell::new((0, false)) };
53}