ic-backup 0.4.2

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use crate::{
    model::consistency::ConsistencyGuaranteeRecord,
    test_support::{
        membership::{hash, plan},
        restore_safety, temp_dir,
    },
};
use std::{
    fs, io,
    os::unix::fs::{PermissionsExt, symlink},
    path::PathBuf,
};

fn layout() -> (PathBuf, BackupLayoutGuard) {
    let root = temp_dir("ic-backup-fence-obligation");
    fs::create_dir(&root).unwrap();
    let guard = BackupLayoutGuard::acquire(&root).unwrap();
    (root, guard)
}
fn fence() -> ApplicationFenceBinding {
    ApplicationFenceBinding {
        identity: hash("56"),
        membership_revision: hash("78"),
    }
}
#[test]
fn immutable_original_obligation_requires_retained_plan_and_requirement() {
    let (root, layout) = layout();
    let plan = plan();
    let requirement = ConsistencyRequirementRecord::new(
        &plan,
        ConsistencyGuaranteeRecord::ApplicationCoordinated,
    );
    let fence = fence();
    let input = FenceObligationRequirement::Capture {
        requirement: &requirement,
        fence: &fence,
    };
    let record = FenceObligationRecord::for_capture(&plan, &requirement, 0, &fence).unwrap();
    assert!(matches!(
        create_fence_obligation(&layout, &plan, input, &record),
        Err(FenceObligationPersistenceError::Consistency(_))
    ));
    super::super::create_operation_plan(&layout, &plan).unwrap();
    assert!(create_fence_obligation(&layout, &plan, input, &record).is_err());
    assert!(!root.join("fence-obligation.json").exists());
    super::super::create_consistency_requirement(&layout, &plan, &requirement).unwrap();
    create_fence_obligation(&layout, &plan, input, &record).unwrap();
    let path = root.join("fence-obligation.json");
    let bytes = fs::read(&path).unwrap();
    assert_eq!(
        fs::metadata(&path).unwrap().permissions().mode() & 0o777,
        0o600
    );
    assert_eq!(
        read_fence_obligation(&layout, &plan, input, &record.digest()).unwrap(),
        record
    );
    assert!(
        matches!(create_fence_obligation(&layout, &plan, input, &record), Err(FenceObligationPersistenceError::Persistence(PersistenceError::Io(ref error))) if error.kind() == io::ErrorKind::AlreadyExists)
    );
    assert!(matches!(
        read_fence_obligation(&layout, &plan, input, &hash("90")),
        Err(FenceObligationPersistenceError::DigestMismatch)
    ));
    let lock = JournalLock::acquire(&path).unwrap();
    assert!(matches!(
        read_fence_obligation(&layout, &plan, input, &record.digest()),
        Err(FenceObligationPersistenceError::Lock(
            JournalLockError::Locked { .. }
        ))
    ));
    drop(lock);
    assert_eq!(fs::read(&path).unwrap(), bytes);
    drop(layout);
    let layout = BackupLayoutGuard::acquire(&root).unwrap();
    assert_eq!(
        read_fence_obligation(&layout, &plan, input, &record.digest()).unwrap(),
        record
    );
    drop(layout);
    fs::remove_dir_all(root).unwrap();
}
#[test]
fn oversized_invalid_and_rebound_fence_bytes_are_rejected_without_repair() {
    let (root, layout) = layout();
    let plan = plan();
    let requirement = ConsistencyRequirementRecord::new(
        &plan,
        ConsistencyGuaranteeRecord::ApplicationCoordinated,
    );
    let fence = fence();
    let input = FenceObligationRequirement::Capture {
        requirement: &requirement,
        fence: &fence,
    };
    let record = FenceObligationRecord::for_capture(&plan, &requirement, 0, &fence).unwrap();
    super::super::create_operation_plan(&layout, &plan).unwrap();
    super::super::create_consistency_requirement(&layout, &plan, &requirement).unwrap();
    let path = root.join("fence-obligation.json");
    for bytes in [
        vec![b' '; usize::try_from(MAX_FENCE_OBLIGATION_BYTES).unwrap() + 1],
        b"{}".to_vec(),
    ] {
        fs::write(&path, &bytes).unwrap();
        assert!(matches!(
            read_fence_obligation(&layout, &plan, input, &record.digest()),
            Err(FenceObligationPersistenceError::Persistence(
                PersistenceError::RecordTooLarge { .. } | PersistenceError::Json(_)
            ))
        ));
        assert_eq!(fs::read(&path).unwrap(), bytes);
    }
    let rebound = FenceObligationRecord::for_capture(
        &plan,
        &requirement,
        0,
        &ApplicationFenceBinding {
            identity: hash("90"),
            ..fence.clone()
        },
    )
    .unwrap();
    fs::write(&path, serde_json::to_vec(&rebound).unwrap()).unwrap();
    let bytes = fs::read(&path).unwrap();
    assert!(matches!(
        read_fence_obligation(&layout, &plan, input, &rebound.digest()),
        Err(FenceObligationPersistenceError::Obligation(
            FenceObligationError::BindingMismatch
        ))
    ));
    assert_eq!(fs::read(&path).unwrap(), bytes);
    drop(layout);
    fs::remove_dir_all(root).unwrap();
}
#[test]
fn symlink_and_replaced_layout_preserve_original_obligation() {
    let (root, layout) = layout();
    let plan = plan();
    let requirement = ConsistencyRequirementRecord::new(
        &plan,
        ConsistencyGuaranteeRecord::ApplicationCoordinated,
    );
    let fence = fence();
    let input = FenceObligationRequirement::Capture {
        requirement: &requirement,
        fence: &fence,
    };
    let record = FenceObligationRecord::for_capture(&plan, &requirement, 0, &fence).unwrap();
    super::super::create_operation_plan(&layout, &plan).unwrap();
    super::super::create_consistency_requirement(&layout, &plan, &requirement).unwrap();
    create_fence_obligation(&layout, &plan, input, &record).unwrap();
    let path = root.join("fence-obligation.json");
    let retained = root.join("retained.json");
    fs::rename(&path, &retained).unwrap();
    symlink(&retained, &path).unwrap();
    assert!(read_fence_obligation(&layout, &plan, input, &record.digest()).is_err());
    let old = root.with_extension("retained");
    fs::rename(&root, &old).unwrap();
    fs::create_dir(&root).unwrap();
    assert!(read_fence_obligation(&layout, &plan, input, &record.digest()).is_err());
    assert!(create_fence_obligation(&layout, &plan, input, &record).is_err());
    assert!(!path.exists());
    drop(layout);
    fs::remove_dir_all(root).unwrap();
    fs::remove_dir_all(old).unwrap();
}
#[test]
fn restoration_requires_both_original_plans_and_unchanged_source_layout() {
    let (root, layout) = layout();
    let (source_root, source_layout) = self::layout();
    let plan = plan();
    let source = restore_safety::source();
    let requirement = restore_safety::requirement(
        &plan,
        &source,
        crate::model::restore_safety::RestoreSafetyLaneRecord::ApplicationFenced,
    );
    let input = FenceObligationRequirement::Restore {
        source_layout: &source_layout,
        source: &source,
        requirement: &requirement,
    };
    let record = FenceObligationRecord::for_restore(&plan, &source, &requirement, 0).unwrap();
    super::super::create_operation_plan(&layout, &plan).unwrap();
    super::super::create_operation_plan(&source_layout, &source).unwrap();
    assert!(matches!(
        create_fence_obligation(&layout, &plan, input, &record),
        Err(FenceObligationPersistenceError::Restore(_))
    ));
    super::super::create_restore_safety_requirement(
        &layout,
        &source_layout,
        &plan,
        &source,
        &requirement,
    )
    .unwrap();
    create_fence_obligation(&layout, &plan, input, &record).unwrap();
    assert_eq!(
        read_fence_obligation(&layout, &plan, input, &record.digest()).unwrap(),
        record
    );
    let bytes = fs::read(root.join("fence-obligation.json")).unwrap();
    let old = source_root.with_extension("retained");
    fs::rename(&source_root, &old).unwrap();
    fs::create_dir(&source_root).unwrap();
    assert!(read_fence_obligation(&layout, &plan, input, &record.digest()).is_err());
    assert_eq!(fs::read(root.join("fence-obligation.json")).unwrap(), bytes);
    drop(layout);
    drop(source_layout);
    fs::remove_dir_all(root).unwrap();
    fs::remove_dir_all(source_root).unwrap();
    fs::remove_dir_all(old).unwrap();
}