a3s-box-runtime 3.2.4

MicroVM runtime engine — VM lifecycle, OCI images, attestation, networking
Documentation
//! Shared helper for surviving a corrupt on-disk JSON store file.
//!
//! A persisted store (`index.json`, `networks.json`, `volumes.json`) must never
//! be able to brick the whole runtime: a truncated file after a crash, or a
//! schema change in a binary upgrade, should degrade to an empty store the next
//! pull/create repopulates — not a hard error that blocks CRI/CLI startup. This
//! mirrors the CLI's `boxes.json` `parse_or_quarantine` hardening.

use std::path::{Path, PathBuf};

/// Move a corrupt store file aside to a unique `*.corrupt-*` sibling so the
/// next save cannot overwrite it (the original is preserved for recovery).
/// Falls back to a copy if rename fails (e.g. cross-device). Returns the backup
/// path on success, `None` if even the copy failed.
pub(crate) fn quarantine_corrupt(path: &Path) -> Option<PathBuf> {
    a3s_box_core::fs_atomic::quarantine_corrupt(path)
}

/// Render the backup path from [`quarantine_corrupt`] for a log message.
pub(crate) fn quarantine_label(path: &Path) -> String {
    quarantine_corrupt(path)
        .map(|p| p.display().to_string())
        .unwrap_or_else(|| "<backup failed>".to_string())
}

/// Copy a store file aside to a unique `*.corrupt-*` sibling
/// WITHOUT removing the original.
///
/// Unlike [`quarantine_corrupt`] (whole-file unreadable → move aside), this is
/// for the *per-entry* skip path: the file still parses and its surviving
/// entries stay live, but the next save rewrites it with only the survivors and
/// would erase the un-deserializable entries (e.g. a schema mismatch after an
/// upgrade) with no backup. Copying — not moving — preserves those entries for
/// recovery while leaving the live catalog untouched. Returns the backup path,
/// `None` if the copy failed.
pub(crate) fn quarantine_copy(path: &Path) -> Option<PathBuf> {
    a3s_box_core::fs_atomic::quarantine_copy(path)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn quarantine_copy_preserves_original_and_backs_up() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("index.json");
        std::fs::write(&path, b"original contents").unwrap();

        let backup = quarantine_copy(&path).expect("copy should succeed");

        // The live file is untouched (per-entry skip keeps its survivors).
        assert!(path.exists(), "original must remain in place");
        assert_eq!(std::fs::read(&path).unwrap(), b"original contents");
        // A recovery copy exists with the same bytes.
        assert!(backup.exists(), "backup copy must exist");
        assert_eq!(std::fs::read(&backup).unwrap(), b"original contents");
        assert!(backup.to_string_lossy().contains(".corrupt-"));
    }

    #[test]
    fn quarantine_corrupt_moves_original_aside() {
        let dir = tempfile::tempdir().unwrap();
        let path = dir.path().join("index.json");
        std::fs::write(&path, b"corrupt").unwrap();

        let backup = quarantine_corrupt(&path).expect("move should succeed");

        // Whole-file path removes the original so the next save can't reuse it.
        assert!(!path.exists(), "original must be moved aside");
        assert!(backup.exists());
        assert_eq!(std::fs::read(&backup).unwrap(), b"corrupt");
    }
}