use super::*;
fn digest(byte: u8) -> SegmentBytesDigest {
[byte; 32]
}
fn seg(segment_id: u64, digest_byte: u8) -> BackupSegmentRef {
BackupSegmentRef {
segment_id,
bytes_digest: digest(digest_byte),
}
}
fn manifest(schema_version: u32, segments: Vec<BackupSegmentRef>) -> BackupManifestBody {
BackupManifestBody {
schema_version,
backup_id: digest(0xAA),
layout_revision: 7,
tooling_revision: 9,
segments,
}
}
const ZERO_DIGEST: SegmentBytesDigest = [0u8; 32];
#[test]
fn sort_backup_segment_refs_orders_ascending_by_id_then_digest() {
let mut refs = vec![seg(3, 0x30), seg(1, 0x10), seg(2, 0x22), seg(2, 0x11)];
sort_backup_segment_refs(&mut refs);
let ids: Vec<u64> = refs.iter().map(|r| r.segment_id).collect();
assert_eq!(
ids,
vec![1, 2, 2, 3],
"segment ids must be ascending after sort"
);
assert_eq!(
refs[1].bytes_digest,
digest(0x11),
"equal-id rows must break ties on bytes_digest ascending"
);
assert_eq!(refs[2].bytes_digest, digest(0x22));
}
#[test]
fn backup_manifest_body_hash_is_nonzero_reorder_invariant_and_content_sensitive() {
let ordered = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x10), seg(2, 0x20)],
);
let reordered = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(2, 0x20), seg(1, 0x10)],
);
let changed = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x10), seg(2, 0x21)],
);
let h_ordered = backup_manifest_body_hash(&ordered).expect("hash ordered");
let h_reordered = backup_manifest_body_hash(&reordered).expect("hash reordered");
let h_changed = backup_manifest_body_hash(&changed).expect("hash changed");
assert_ne!(
h_ordered, ZERO_DIGEST,
"a real manifest body hash must never be the all-zero default digest"
);
assert_eq!(
h_ordered, h_reordered,
"caller-supplied segment order must not change the normalized body hash"
);
assert_ne!(
h_ordered, h_changed,
"changing a segment digest MUST change the body hash (empty-body-bytes mutant would collapse both to one constant)"
);
}
#[test]
fn collect_segment_digest_index_distinguishes_duplicate_from_inconsistent() {
let mut dup_findings = Vec::new();
let dup_map = collect_segment_digest_index(&[seg(5, 0x55), seg(5, 0x55)], &mut dup_findings);
assert!(
dup_findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::DuplicateSegmentRef { segment_id: 5, .. }
)),
"two byte-identical rows for id 5 must produce a DuplicateSegmentRef, got {dup_findings:?}"
);
assert!(
!dup_findings
.iter()
.any(|f| matches!(f, BackupEnvelopeFinding::InconsistentSegmentId { .. })),
"identical rows must NOT be reported as an id inconsistency"
);
assert_eq!(
dup_map.get(&5u64),
Some(&digest(0x55)),
"the first digest for an id is retained in the index"
);
let mut bad_findings = Vec::new();
let _bad_map = collect_segment_digest_index(&[seg(6, 0x60), seg(6, 0x61)], &mut bad_findings);
assert!(
bad_findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::InconsistentSegmentId {
segment_id: 6,
first_digest,
second_digest,
} if *first_digest == digest(0x60) && *second_digest == digest(0x61)
)),
"id 6 with two different digests must produce an InconsistentSegmentId carrying both digests, got {bad_findings:?}"
);
assert!(
!bad_findings
.iter()
.any(|f| matches!(f, BackupEnvelopeFinding::DuplicateSegmentRef { .. })),
"a genuine digest disagreement must NOT be reported as an exact duplicate"
);
}
#[test]
fn audit_flags_unsupported_schema_and_duplicate_segments() {
let unsupported = BACKUP_MANIFEST_BODY_SCHEMA_VERSION.wrapping_add(1);
let findings =
audit_backup_manifest_segments(&manifest(unsupported, vec![seg(2, 0x20), seg(2, 0x20)]));
assert!(
findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::UnsupportedManifestBodySchemaVersion {
observed,
expected,
} if *observed == unsupported && *expected == BACKUP_MANIFEST_BODY_SCHEMA_VERSION
)),
"an unsupported body schema version must be flagged, got {findings:?}"
);
assert!(
findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::DuplicateSegmentRef { segment_id: 2, .. }
)),
"the duplicate segment must still be reported alongside the schema finding"
);
}
#[test]
fn audit_accepts_supported_schema_without_a_schema_finding() {
let findings = audit_backup_manifest_segments(&manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x10), seg(2, 0x20)],
));
assert!(
!findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::UnsupportedManifestBodySchemaVersion { .. }
)),
"a supported schema version must produce no unsupported-schema finding, got {findings:?}"
);
}
#[test]
fn restore_proof_report_flags_missing_mismatch_and_unexpected() {
let expected = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x11), seg(2, 0x22), seg(4, 0x44)],
);
let observed = vec![seg(2, 0x2F), seg(3, 0x33), seg(4, 0x44)];
let report = restore_proof_report_body(&expected, &observed).expect("restore proof body");
assert_ne!(
report.manifest_body_hash, ZERO_DIGEST,
"the restore proof must anchor on the real (non-zero) manifest body hash"
);
assert!(
report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::MissingExpectedSegment { segment_id: 1 }
)),
"expected id 1 absent from observed must be MissingExpectedSegment, got {:?}",
report.findings
);
assert!(
report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::SegmentBytesDigestMismatch {
segment_id: 2,
expected,
observed,
} if *expected == digest(0x22) && *observed == digest(0x2F)
)),
"id 2 with differing digests must be a SegmentBytesDigestMismatch carrying both, got {:?}",
report.findings
);
assert!(
report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::UnexpectedObservedSegment { segment_id: 3 }
)),
"observed id 3 not in the manifest must be UnexpectedObservedSegment, got {:?}",
report.findings
);
assert!(
!report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::UnexpectedObservedSegment { segment_id: 2 | 4 }
)),
"expected ids (2, 4) must never be reported as unexpected (the `!contains_key` guard)"
);
assert!(
!report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::MissingExpectedSegment { segment_id: 4 }
| BackupEnvelopeFinding::SegmentBytesDigestMismatch { segment_id: 4, .. }
)),
"the fully-matching id 4 row must produce none of the discrepancy findings"
);
}
#[test]
fn restore_proof_report_flags_unsupported_schema() {
let unsupported = BACKUP_MANIFEST_BODY_SCHEMA_VERSION.wrapping_add(3);
let report =
restore_proof_report_body(&manifest(unsupported, vec![seg(1, 0x10)]), &[seg(1, 0x10)])
.expect("restore proof body");
assert!(
report.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::UnsupportedManifestBodySchemaVersion { observed, .. }
if *observed == unsupported
)),
"an unsupported expected-manifest schema must be flagged, got {:?}",
report.findings
);
assert_eq!(
report.schema_version, RESTORE_PROOF_REPORT_SCHEMA_VERSION,
"the report body must stamp its own v1 schema version, not the manifest's"
);
}
#[test]
fn restore_proof_report_body_hash_is_nonzero_and_findings_order_invariant() {
let expected = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x11), seg(2, 0x22)],
);
let observed = vec![seg(2, 0x2F), seg(3, 0x33)];
let report = restore_proof_report_body(&expected, &observed).expect("restore proof body");
let h = restore_proof_report_body_hash(&report).expect("hash report");
assert_ne!(
h, ZERO_DIGEST,
"a real restore-proof body hash must never be the all-zero default digest"
);
let mut permuted = report.clone();
permuted.findings.reverse();
permuted.observed_segments_sorted.reverse();
let h_permuted = restore_proof_report_body_hash(&permuted).expect("hash permuted");
assert_eq!(
h, h_permuted,
"the restore-proof body hash must normalize findings/observed order before hashing"
);
}
#[test]
fn verify_backup_manifest_envelope_flags_claimed_hash_mismatch_both_polarities() {
let body = manifest(
BACKUP_MANIFEST_BODY_SCHEMA_VERSION,
vec![seg(1, 0x10), seg(2, 0x20)],
);
let envelope = BackupManifestEnvelope {
body: body.clone(),
envelope_schema_version: 1,
generated_at_wall_ms: None,
diagnostic_note: None,
signatures: Vec::new(),
attestations: Vec::new(),
};
let correct = backup_manifest_body_hash(&body).expect("correct hash");
let matched =
verify_backup_manifest_envelope(&envelope, correct, |_sig, _body| -> Result<(), String> {
Ok(())
})
.expect("verify with correct claim");
assert!(
!matched
.findings
.iter()
.any(|f| matches!(f, BackupEnvelopeFinding::ManifestBodyHashMismatch { .. })),
"a correct claimed hash must NOT produce a ManifestBodyHashMismatch, got {:?}",
matched.findings
);
let wrong_claim = digest(0xEE);
let mismatched = verify_backup_manifest_envelope(
&envelope,
wrong_claim,
|_sig, _body| -> Result<(), String> { Ok(()) },
)
.expect("verify with wrong claim");
assert!(
mismatched.findings.iter().any(|f| matches!(
f,
BackupEnvelopeFinding::ManifestBodyHashMismatch { claimed, computed }
if *claimed == wrong_claim && *computed == correct
)),
"a wrong claimed hash must produce a ManifestBodyHashMismatch carrying claimed+computed, got {:?}",
mismatched.findings
);
}