lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

backup.rs (18409B)


      1 use radroots_storage::{
      2     Error,
      3     backup::{
      4         BackupFormatVersion, BackupId, BackupManifest, BackupMember, BackupMemberKind,
      5         BackupOperation, BackupPlan, BackupSecretPolicy, BackupStage, BackupTransition,
      6         MemberDigest, MemberVerification, ReliabilityRevision, RestoreMemberStatus,
      7         RestoreOperation, RestorePlan, RestoreStage, RestoreTransition, StorageReliability,
      8     },
      9     status::{
     10         IntegrityHealth, IntegrityStatus, ShutdownState, StorageBackend, StorageOpenMode,
     11         StorageStatus, WriterPolicy,
     12     },
     13 };
     14 
     15 fn member(path: &str, kind: BackupMemberKind, byte: u8) -> BackupMember {
     16     BackupMember::new(path, kind, 100, MemberDigest::new([byte; 32])).expect("backup member")
     17 }
     18 
     19 fn manifest(policy: BackupSecretPolicy) -> BackupManifest {
     20     let mut members = vec![member(
     21         "runtime/runtime.sqlite",
     22         BackupMemberKind::Runtime,
     23         1,
     24     )];
     25     if policy == BackupSecretPolicy::IncludeProtectedStorage {
     26         members.push(member(
     27             "private/private.sqlite",
     28             BackupMemberKind::Protected,
     29             2,
     30         ));
     31     }
     32     BackupManifest::new(
     33         BackupFormatVersion::V1,
     34         BackupId::new([7; 16]).expect("backup id"),
     35         100,
     36         policy,
     37         members,
     38     )
     39     .expect("backup manifest")
     40 }
     41 
     42 #[test]
     43 fn manifests_reject_unsafe_duplicate_and_policy_violating_members() {
     44     assert_eq!(
     45         BackupMember::new(
     46             "../runtime.sqlite",
     47             BackupMemberKind::Runtime,
     48             1,
     49             MemberDigest::new([1; 32]),
     50         ),
     51         Err(Error::InvalidBackupMemberPath)
     52     );
     53     let runtime = member("runtime.sqlite", BackupMemberKind::Runtime, 1);
     54     assert_eq!(
     55         BackupManifest::new(
     56             BackupFormatVersion::V1,
     57             BackupId::new([1; 16]).expect("id"),
     58             1,
     59             BackupSecretPolicy::ExcludeProtectedStorage,
     60             vec![runtime.clone(), runtime],
     61         ),
     62         Err(Error::DuplicateBackupMember)
     63     );
     64     assert_eq!(
     65         BackupManifest::new(
     66             BackupFormatVersion::V1,
     67             BackupId::new([1; 16]).expect("id"),
     68             1,
     69             BackupSecretPolicy::ExcludeProtectedStorage,
     70             vec![member("private.sqlite", BackupMemberKind::Protected, 2)],
     71         ),
     72         Err(Error::BackupSecretPolicyViolation)
     73     );
     74 }
     75 
     76 #[test]
     77 fn backup_requires_capture_verification_before_atomic_finalization() {
     78     let plan = BackupPlan::new(
     79         BackupId::new([7; 16]).expect("backup id"),
     80         BackupFormatVersion::V1,
     81         BackupSecretPolicy::IncludeProtectedStorage,
     82         90,
     83     )
     84     .expect("backup plan");
     85     let planned = BackupOperation::planned(plan);
     86     assert_eq!(
     87         planned.transition(
     88             ReliabilityRevision::INITIAL,
     89             BackupTransition::Finalize,
     90             100,
     91         ),
     92         Err(Error::InvalidBackupTransition)
     93     );
     94     let captured = planned
     95         .transition(
     96             ReliabilityRevision::INITIAL,
     97             BackupTransition::Captured(manifest(BackupSecretPolicy::IncludeProtectedStorage)),
     98             100,
     99         )
    100         .expect("captured backup");
    101     let verified = captured
    102         .transition(captured.revision(), BackupTransition::Verified, 110)
    103         .expect("verified backup");
    104     let finalized = verified
    105         .transition(verified.revision(), BackupTransition::Finalize, 120)
    106         .expect("finalized backup");
    107     assert_eq!(finalized.stage(), BackupStage::Finalized);
    108     assert_eq!(finalized.manifest().expect("manifest").total_bytes(), 200);
    109     assert_eq!(
    110         finalized.transition(finalized.revision(), BackupTransition::Fail, 130),
    111         Err(Error::ReliabilityOperationTerminal)
    112     );
    113 }
    114 
    115 #[test]
    116 fn restore_stages_and_verifies_every_member_before_replacement() {
    117     let manifest = manifest(BackupSecretPolicy::IncludeProtectedStorage);
    118     assert_eq!(
    119         RestorePlan::new(
    120             manifest.clone(),
    121             BackupSecretPolicy::ExcludeProtectedStorage,
    122             200,
    123         ),
    124         Err(Error::BackupSecretPolicyViolation)
    125     );
    126     let plan = RestorePlan::new(
    127         manifest.clone(),
    128         BackupSecretPolicy::IncludeProtectedStorage,
    129         200,
    130     )
    131     .expect("restore plan");
    132     let staging = RestoreOperation::staging(plan);
    133     let verifying = staging
    134         .transition(ReliabilityRevision::INITIAL, RestoreTransition::Staged, 210)
    135         .expect("staged restore");
    136     let failed_evidence = vec![
    137         RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified)
    138             .expect("status"),
    139         RestoreMemberStatus::new("private/private.sqlite", MemberVerification::HashMismatch)
    140             .expect("status"),
    141     ];
    142     assert_eq!(
    143         verifying.transition(
    144             verifying.revision(),
    145             RestoreTransition::Verified(failed_evidence),
    146             220,
    147         ),
    148         Err(Error::RestoreMemberVerificationFailed)
    149     );
    150     let verified = manifest
    151         .members()
    152         .iter()
    153         .map(|member| {
    154             RestoreMemberStatus::new(member.relative_path(), MemberVerification::Verified)
    155                 .expect("verified status")
    156         })
    157         .collect();
    158     let finalizing = verifying
    159         .transition(
    160             verifying.revision(),
    161             RestoreTransition::Verified(verified),
    162             220,
    163         )
    164         .expect("verified restore");
    165     let finalized = finalizing
    166         .transition(finalizing.revision(), RestoreTransition::Finalize, 230)
    167         .expect("finalized restore");
    168     assert_eq!(finalized.stage(), RestoreStage::Finalized);
    169 }
    170 
    171 #[test]
    172 fn integrity_and_storage_status_reject_inconsistent_runtime_claims() {
    173     let integrity =
    174         IntegrityStatus::new(IntegrityHealth::Healthy, Some(100), 2, 0).expect("integrity status");
    175     let status = StorageStatus::new(
    176         StorageBackend::Sqlite,
    177         StorageOpenMode::ReadWriteExisting,
    178         WriterPolicy::AdvisoryProcessLock,
    179         ShutdownState::Open,
    180         integrity,
    181         true,
    182         5_000,
    183     )
    184     .expect("storage status");
    185     assert!(status.wal_enabled());
    186     assert_eq!(status.busy_timeout_ms(), 5_000);
    187     assert_eq!(
    188         IntegrityStatus::new(IntegrityHealth::Healthy, Some(100), 1, 1),
    189         Err(Error::InvalidIntegrityStatus)
    190     );
    191     assert_eq!(
    192         StorageStatus::new(
    193             StorageBackend::Sqlite,
    194             StorageOpenMode::ReadWriteExisting,
    195             WriterPolicy::NoWriter,
    196             ShutdownState::Open,
    197             integrity,
    198             true,
    199             5_000,
    200         ),
    201         Err(Error::InvalidStorageStatus)
    202     );
    203 }
    204 
    205 #[test]
    206 fn reliability_spi_is_dyn_compatible_and_versions_are_independent() {
    207     fn accepts_dyn(_: Option<&dyn StorageReliability>) {}
    208     accepts_dyn(None);
    209     assert_eq!(BackupFormatVersion::V1.get(), 1);
    210     assert_eq!(
    211         BackupFormatVersion::new(0),
    212         Err(Error::InvalidBackupVersion)
    213     );
    214     assert_eq!(BackupId::new([0; 16]), Err(Error::InvalidBackupId));
    215 }
    216 
    217 #[test]
    218 fn backup_json_cannot_bypass_constructor_invariants() {
    219     let valid = serde_json::to_value(manifest(BackupSecretPolicy::ExcludeProtectedStorage))
    220         .expect("serialize manifest");
    221 
    222     let mut unsafe_path = valid.clone();
    223     unsafe_path["members"][0]["relative_path"] = serde_json::json!("../runtime.sqlite");
    224     assert!(serde_json::from_value::<BackupManifest>(unsafe_path).is_err());
    225 
    226     let mut forged_total = valid.clone();
    227     forged_total["total_bytes"] = serde_json::json!(101);
    228     assert!(serde_json::from_value::<BackupManifest>(forged_total).is_err());
    229 
    230     let mut duplicate = valid.clone();
    231     duplicate["members"] =
    232         serde_json::json!([valid["members"][0].clone(), valid["members"][0].clone()]);
    233     duplicate["total_bytes"] = serde_json::json!(200);
    234     assert!(serde_json::from_value::<BackupManifest>(duplicate).is_err());
    235 
    236     assert!(serde_json::from_str::<BackupId>("[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]").is_err());
    237     assert!(serde_json::from_str::<BackupFormatVersion>("0").is_err());
    238     assert!(serde_json::from_str::<ReliabilityRevision>("0").is_err());
    239 }
    240 
    241 #[test]
    242 fn restore_json_cannot_bypass_policy_or_timestamp_invariants() {
    243     let restore = RestorePlan::new(
    244         manifest(BackupSecretPolicy::IncludeProtectedStorage),
    245         BackupSecretPolicy::IncludeProtectedStorage,
    246         200,
    247     )
    248     .expect("restore plan");
    249     let valid = serde_json::to_value(restore).expect("serialize restore plan");
    250 
    251     let mut rejected_policy = valid.clone();
    252     rejected_policy["accepted_secret_policy"] = serde_json::json!("exclude_protected_storage");
    253     assert!(serde_json::from_value::<RestorePlan>(rejected_policy).is_err());
    254 
    255     let mut zero_timestamp = valid;
    256     zero_timestamp["requested_at_unix_ms"] = serde_json::json!(0);
    257     assert!(serde_json::from_value::<RestorePlan>(zero_timestamp).is_err());
    258 
    259     let unsafe_status = serde_json::json!({
    260         "relative_path": "runtime/../private.sqlite",
    261         "verification": "verified"
    262     });
    263     assert!(serde_json::from_value::<RestoreMemberStatus>(unsafe_status).is_err());
    264 }
    265 
    266 #[test]
    267 fn backup_value_models_cover_all_bounds_and_accessors() {
    268     let backup_id = BackupId::new([1; 16]).unwrap();
    269     assert_eq!(backup_id.as_bytes(), &[1; 16]);
    270     let digest = MemberDigest::new([2; 32]);
    271     assert_eq!(digest.as_bytes(), &[2; 32]);
    272     for path in [
    273         "",
    274         "/absolute",
    275         "../escape",
    276         "a/../b",
    277         "a/./b",
    278         "a//b",
    279         "a\\b",
    280         " leading",
    281         "bad\npath",
    282     ] {
    283         assert_eq!(
    284             BackupMember::new(path, BackupMemberKind::Runtime, 1, digest),
    285             Err(Error::InvalidBackupMemberPath)
    286         );
    287         assert_eq!(
    288             RestoreMemberStatus::new(path, MemberVerification::Verified),
    289             Err(Error::InvalidBackupMemberPath)
    290         );
    291     }
    292     assert_eq!(
    293         BackupMember::new("runtime.sqlite", BackupMemberKind::Runtime, 0, digest),
    294         Err(Error::InvalidBackupMemberLength)
    295     );
    296     let runtime =
    297         BackupMember::new("runtime.sqlite", BackupMemberKind::Metadata, 10, digest).unwrap();
    298     assert_eq!(runtime.relative_path(), "runtime.sqlite");
    299     assert_eq!(runtime.kind(), BackupMemberKind::Metadata);
    300     assert_eq!(runtime.byte_length(), 10);
    301     assert_eq!(runtime.sha256(), digest);
    302     for (created, members) in [(0, vec![runtime.clone()]), (1, vec![])] {
    303         assert_eq!(
    304             BackupManifest::new(
    305                 BackupFormatVersion::V1,
    306                 backup_id,
    307                 created,
    308                 BackupSecretPolicy::ExcludeProtectedStorage,
    309                 members,
    310             ),
    311             Err(Error::InvalidBackupManifest)
    312         );
    313     }
    314     let huge = BackupMember::new("huge", BackupMemberKind::Runtime, u64::MAX, digest).unwrap();
    315     let one = BackupMember::new("one", BackupMemberKind::Runtime, 1, digest).unwrap();
    316     assert_eq!(
    317         BackupManifest::new(
    318             BackupFormatVersion::V1,
    319             backup_id,
    320             1,
    321             BackupSecretPolicy::ExcludeProtectedStorage,
    322             vec![huge, one],
    323         ),
    324         Err(Error::InvalidBackupManifest)
    325     );
    326     let manifest = manifest(BackupSecretPolicy::IncludeProtectedStorage);
    327     assert_eq!(manifest.format_version(), BackupFormatVersion::V1);
    328     assert_eq!(manifest.backup_id().as_bytes(), &[7; 16]);
    329     assert_eq!(manifest.created_at_unix_ms(), 100);
    330     assert_eq!(
    331         manifest.secret_policy(),
    332         BackupSecretPolicy::IncludeProtectedStorage
    333     );
    334     assert_eq!(manifest.members().len(), 2);
    335     assert!(manifest.member("runtime/runtime.sqlite").is_some());
    336     assert!(manifest.member("missing").is_none());
    337 
    338     assert_eq!(
    339         BackupPlan::new(
    340             backup_id,
    341             BackupFormatVersion::V1,
    342             BackupSecretPolicy::ExcludeProtectedStorage,
    343             0,
    344         ),
    345         Err(Error::InvalidBackupTimestamp)
    346     );
    347     let plan = BackupPlan::new(
    348         backup_id,
    349         BackupFormatVersion::V1,
    350         BackupSecretPolicy::ExcludeProtectedStorage,
    351         10,
    352     )
    353     .unwrap();
    354     assert_eq!(plan.backup_id(), backup_id);
    355     assert_eq!(plan.format_version(), BackupFormatVersion::V1);
    356     assert_eq!(
    357         plan.secret_policy(),
    358         BackupSecretPolicy::ExcludeProtectedStorage
    359     );
    360     assert_eq!(plan.requested_at_unix_ms(), 10);
    361     assert_eq!(
    362         ReliabilityRevision::new(0),
    363         Err(Error::InvalidReliabilityRevision)
    364     );
    365     assert_eq!(ReliabilityRevision::new(2).unwrap().get(), 2);
    366 }
    367 
    368 #[test]
    369 fn backup_transition_matrix_rejects_revision_time_and_manifest_mismatch() {
    370     let plan = BackupPlan::new(
    371         BackupId::new([7; 16]).unwrap(),
    372         BackupFormatVersion::V1,
    373         BackupSecretPolicy::IncludeProtectedStorage,
    374         90,
    375     )
    376     .unwrap();
    377     let planned = BackupOperation::planned(plan.clone());
    378     assert_eq!(planned.plan(), &plan);
    379     assert_eq!(planned.revision(), ReliabilityRevision::INITIAL);
    380     assert_eq!(planned.stage(), BackupStage::Planned);
    381     assert!(planned.manifest().is_none());
    382     assert_eq!(planned.updated_at_unix_ms(), 90);
    383     assert_eq!(
    384         planned.transition(
    385             ReliabilityRevision::new(2).unwrap(),
    386             BackupTransition::Fail,
    387             100
    388         ),
    389         Err(Error::ReliabilityRevisionConflict)
    390     );
    391     assert_eq!(
    392         planned.transition(planned.revision(), BackupTransition::Fail, 89),
    393         Err(Error::InvalidBackupTimestamp)
    394     );
    395     let wrong_id = BackupManifest::new(
    396         BackupFormatVersion::V1,
    397         BackupId::new([8; 16]).unwrap(),
    398         100,
    399         BackupSecretPolicy::IncludeProtectedStorage,
    400         vec![member("runtime", BackupMemberKind::Runtime, 1)],
    401     )
    402     .unwrap();
    403     assert_eq!(
    404         planned.transition(
    405             planned.revision(),
    406             BackupTransition::Captured(wrong_id),
    407             100
    408         ),
    409         Err(Error::BackupManifestPlanMismatch)
    410     );
    411     let wrong_version = BackupManifest::new(
    412         BackupFormatVersion::new(2).unwrap(),
    413         plan.backup_id(),
    414         100,
    415         plan.secret_policy(),
    416         vec![member("runtime", BackupMemberKind::Runtime, 1)],
    417     )
    418     .unwrap();
    419     assert_eq!(
    420         planned.transition(
    421             planned.revision(),
    422             BackupTransition::Captured(wrong_version),
    423             100
    424         ),
    425         Err(Error::BackupManifestPlanMismatch)
    426     );
    427     let wrong_policy = manifest(BackupSecretPolicy::ExcludeProtectedStorage);
    428     assert_eq!(
    429         planned.transition(
    430             planned.revision(),
    431             BackupTransition::Captured(wrong_policy),
    432             100
    433         ),
    434         Err(Error::BackupManifestPlanMismatch)
    435     );
    436     assert_eq!(
    437         planned.transition(planned.revision(), BackupTransition::Verified, 100),
    438         Err(Error::InvalidBackupTransition)
    439     );
    440     let failed = planned
    441         .transition(planned.revision(), BackupTransition::Fail, 100)
    442         .unwrap();
    443     assert_eq!(failed.stage(), BackupStage::Failed);
    444     assert_eq!(
    445         failed.transition(failed.revision(), BackupTransition::Fail, 101),
    446         Err(Error::ReliabilityOperationTerminal)
    447     );
    448 }
    449 
    450 #[test]
    451 fn restore_transition_and_member_verification_matrix_is_complete() {
    452     let backup_manifest = manifest(BackupSecretPolicy::IncludeProtectedStorage);
    453     assert_eq!(
    454         RestorePlan::new(
    455             backup_manifest.clone(),
    456             BackupSecretPolicy::IncludeProtectedStorage,
    457             0,
    458         ),
    459         Err(Error::InvalidRestoreTimestamp)
    460     );
    461     let plan = RestorePlan::new(
    462         backup_manifest.clone(),
    463         BackupSecretPolicy::IncludeProtectedStorage,
    464         200,
    465     )
    466     .unwrap();
    467     assert_eq!(plan.manifest(), &backup_manifest);
    468     assert_eq!(
    469         plan.accepted_secret_policy(),
    470         BackupSecretPolicy::IncludeProtectedStorage
    471     );
    472     assert_eq!(plan.requested_at_unix_ms(), 200);
    473     let staging = RestoreOperation::staging(plan.clone());
    474     assert_eq!(staging.plan(), &plan);
    475     assert_eq!(staging.revision(), ReliabilityRevision::INITIAL);
    476     assert_eq!(staging.stage(), RestoreStage::Staging);
    477     assert!(staging.member_status().is_empty());
    478     assert_eq!(
    479         staging.transition(
    480             ReliabilityRevision::new(2).unwrap(),
    481             RestoreTransition::Staged,
    482             201
    483         ),
    484         Err(Error::ReliabilityRevisionConflict)
    485     );
    486     assert_eq!(
    487         staging.transition(staging.revision(), RestoreTransition::Staged, 199),
    488         Err(Error::InvalidRestoreTimestamp)
    489     );
    490     assert_eq!(
    491         staging.transition(staging.revision(), RestoreTransition::Finalize, 201),
    492         Err(Error::InvalidRestoreTransition)
    493     );
    494     let verifying = staging
    495         .transition(staging.revision(), RestoreTransition::Staged, 201)
    496         .unwrap();
    497     for statuses in [
    498         vec![],
    499         vec![
    500             RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified)
    501                 .unwrap(),
    502             RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified)
    503                 .unwrap(),
    504         ],
    505         vec![
    506             RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified)
    507                 .unwrap(),
    508             RestoreMemberStatus::new("foreign", MemberVerification::Verified).unwrap(),
    509         ],
    510         vec![
    511             RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified)
    512                 .unwrap(),
    513             RestoreMemberStatus::new("private/private.sqlite", MemberVerification::Missing)
    514                 .unwrap(),
    515         ],
    516     ] {
    517         assert_eq!(
    518             verifying.transition(
    519                 verifying.revision(),
    520                 RestoreTransition::Verified(statuses),
    521                 202
    522             ),
    523             Err(Error::RestoreMemberVerificationFailed)
    524         );
    525     }
    526     let status =
    527         RestoreMemberStatus::new("runtime/runtime.sqlite", MemberVerification::Verified).unwrap();
    528     assert_eq!(status.relative_path(), "runtime/runtime.sqlite");
    529     assert_eq!(status.verification(), MemberVerification::Verified);
    530     let failed = verifying
    531         .transition(verifying.revision(), RestoreTransition::Fail, 202)
    532         .unwrap();
    533     assert_eq!(failed.stage(), RestoreStage::Failed);
    534     assert_eq!(
    535         failed.transition(failed.revision(), RestoreTransition::Fail, 203),
    536         Err(Error::ReliabilityOperationTerminal)
    537     );
    538 }