ic-backup 0.63.1

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::super::*;
use crate::tests::support::create_json_durable;
use crate::tests::support::temp_dir;
use std::fs;

#[test]
fn create_only_publication_preserves_an_existing_record() {
    let root = temp_dir("ic-backup-create-json");
    let path = root.join("journal.json");
    create_json_durable(&path, &serde_json::json!({"intent": "original"})).expect("create record");
    let bytes = fs::read(&path).expect("read exact bytes");
    let error = create_json_durable(&path, &serde_json::json!({"intent": "different"}))
        .expect_err("existing record must not be replaced");
    assert!(
        matches!(error, PersistenceError::Publication(ic_host_fs::durable::NamedWriteError::BeforePublication { source: ref error, .. }) if error.kind() == io::ErrorKind::AlreadyExists)
    );
    assert_eq!(fs::read(&path).expect("read retained bytes"), bytes);
    fs::remove_dir_all(root).expect("remove successful fixture");
}

#[cfg(unix)]
#[test]
fn record_reads_enforce_exact_byte_limits_and_reject_symlinks() {
    let root = temp_dir("ic-backup-record-read");
    let path = root.join("journal.json");
    let value = serde_json::json!({"intent": "snapshot"});
    create_json_durable(&path, &value).expect("create record");
    let length = fs::metadata(&path).expect("record metadata").len();
    let decoded: serde_json::Value = read_json(&path, length).expect("exact bound accepts");
    assert_eq!(decoded, value);
    for bound in [0, length - 1] {
        assert!(
            matches!(read_json::<serde_json::Value>(&path, bound), Err(PersistenceError::RecordTooLarge { limit }) if limit == bound)
        );
    }
    let linked = root.join("linked.json");
    std::os::unix::fs::symlink(&path, &linked).expect("create record link");
    assert!(matches!(
        read_json::<serde_json::Value>(&linked, length),
        Err(PersistenceError::Io(_))
    ));
    assert!(
        matches!(read_json::<serde_json::Value>(&root, length), Err(PersistenceError::Io(ref error)) if error.kind() == io::ErrorKind::InvalidInput)
    );
    fs::write(&path, b"malformed").expect("write malformed record");
    assert!(matches!(
        read_json::<serde_json::Value>(&path, length),
        Err(PersistenceError::Json(_))
    ));
    fs::remove_dir_all(root).expect("remove successful fixture");
}

#[cfg(unix)]
#[test]
fn shared_record_reads_preserve_extreme_limits_missing_files_and_parent_aliases() {
    let root = temp_dir("ic-backup-shared-record-read");
    let path = root.join("record.json");
    fs::write(&path, b"null").expect("write exact JSON");
    assert_eq!(
        read_json::<serde_json::Value>(&path, u64::MAX).expect("large declared limit"),
        serde_json::Value::Null
    );
    assert!(matches!(
        read_json::<serde_json::Value>(&path, 0),
        Err(PersistenceError::RecordTooLarge { limit: 0 })
    ));
    assert_eq!(fs::read(&path).expect("original bytes"), b"null");
    let alias = root.join("parent-alias");
    std::os::unix::fs::symlink(&root, &alias).expect("selected parent alias");
    assert_eq!(
        read_json::<serde_json::Value>(&alias.join("record.json"), 4)
            .expect("parent selection remains caller owned"),
        serde_json::Value::Null
    );
    assert!(matches!(
        read_json::<serde_json::Value>(&root.join("missing"), 4),
        Err(PersistenceError::Io(error)) if error.kind() == io::ErrorKind::NotFound
    ));
    let dangling = root.join("dangling");
    std::os::unix::fs::symlink("missing", &dangling).expect("dangling final link");
    assert!(matches!(
        read_json::<serde_json::Value>(&dangling, 4),
        Err(PersistenceError::Io(error))
            if error.raw_os_error() == Some(rustix::io::Errno::LOOP.raw_os_error())
    ));
    fs::write(&path, []).expect("empty file");
    assert!(matches!(
        read_json::<serde_json::Value>(&path, 0),
        Err(PersistenceError::Json(error)) if error.is_eof()
    ));
    fs::remove_dir_all(root).expect("remove successful fixture");
}

#[cfg(unix)]
#[test]
fn new_machine_records_and_directories_have_private_permissions() {
    use std::os::unix::fs::PermissionsExt;
    let root = temp_dir("ic-backup-private-json");
    let parent = root.join("private");
    let path = parent.join("journal.json");
    create_json_durable(&path, &serde_json::json!({"intent": "snapshot"}))
        .expect("create private record");
    assert_eq!(
        fs::metadata(&path)
            .expect("file metadata")
            .permissions()
            .mode()
            & 0o777,
        0o600
    );
    assert_eq!(
        fs::metadata(&parent)
            .expect("parent metadata")
            .permissions()
            .mode()
            & 0o777,
        0o700
    );
    fs::remove_dir_all(root).expect("remove successful fixture");
}