///|
pub(all) struct SnapshotVerification {
matches : Bool
checked_files : Int32
verified_checksums : Int32
diagnostics : Array[Diagnostic]
} derive(ToJson)
///|
pub extend SnapshotVerification with ToJson::{to_json}
///|
/// Recheck every recorded digest, without treating unchanged timestamps as evidence.
/// This checks preservation against a trusted local snapshot, not RFC conformance.
pub fn verify_snapshot_inventory(
expected : BagSnapshot,
observed : Array[FileRecord],
directories : Array[String],
) -> SnapshotVerification raise FormatError {
validate_snapshot(expected)
let diagnostics : Array[Diagnostic] = []
let before : Map[String, FileRecord] = Map([])
let after : Map[String, FileRecord] = Map([])
let expected_dirs : Map[String, Bool] = Map([])
let observed_dirs : Map[String, Bool] = Map([])
for file in expected.files {
before[file.path] = file
}
for directory in expected.directories {
expected_dirs[directory] = true
}
for file in observed {
check_path(file.path) catch {
Invalid(message) =>
issue(diagnostics, "snapshot_unsafe_path", file.path, message)
}
if after.contains(file.path) {
issue(
diagnostics,
"snapshot_duplicate_file",
file.path,
"duplicate observed file record",
)
}
if file.size < 0L {
issue(
diagnostics,
"snapshot_invalid_size",
file.path,
"negative observed byte count",
)
}
after[file.path] = file
}
for directory in directories {
check_path(directory) catch {
Invalid(message) =>
issue(diagnostics, "snapshot_unsafe_directory", directory, message)
}
if observed_dirs.contains(directory) {
issue(
diagnostics, "snapshot_duplicate_directory", directory, "duplicate observed directory",
)
}
if after.contains(directory) {
issue(
diagnostics, "snapshot_path_collision", directory, "observed path is both file and directory",
)
}
observed_dirs[directory] = true
}
let mut checked_files = 0
let mut verified_checksums = 0
for path in sorted_map_keys(before) {
let original = before[path]
match after.get(path) {
None =>
issue(
diagnostics, "snapshot_missing_file", path, "recorded file is missing",
)
Some(file) => {
checked_files = checked_files + 1
if file.size != original.size {
issue(
diagnostics, "snapshot_size_changed", path, "byte count differs from preservation record",
)
}
for name in sorted_map_keys(original.digests) {
let alg = algorithm(name)
match file.digests.get(name) {
None =>
issue(
diagnostics,
"snapshot_digest_unavailable",
path,
"host did not recompute recorded algorithm " + name,
)
Some(digest) =>
if !valid_checksum(digest, alg) {
issue(
diagnostics,
"snapshot_digest_invalid",
path,
"host supplied a malformed " + name + " digest",
)
} else if digest[:].to_lower().to_owned() !=
original.digests[name][:].to_lower().to_owned() {
issue(
diagnostics,
"snapshot_digest_changed",
path,
"raw-byte " +
name +
" digest differs from preservation record",
)
} else {
verified_checksums = verified_checksums + 1
}
}
}
}
}
}
for path in sorted_map_keys(after) {
if !before.contains(path) {
issue(
diagnostics, "snapshot_unexpected_file", path, "unrecorded file is present",
)
}
}
for path in sorted_map_keys(expected_dirs) {
if !observed_dirs.contains(path) {
issue(
diagnostics, "snapshot_missing_directory", path, "recorded directory is missing",
)
}
}
for path in sorted_map_keys(observed_dirs) {
if !expected_dirs.contains(path) {
issue(
diagnostics, "snapshot_unexpected_directory", path, "unrecorded directory is present",
)
}
}
{
matches: !diagnostics.any(d => d.severity == Error),
checked_files,
verified_checksums,
diagnostics,
}
}
///|
pub(all) struct PreservationReport {
preserved : Bool
bag : Report
inventory : SnapshotVerification
metadata : Array[MetadataChange]
declaration_matches : Bool
} derive(ToJson)
///|
pub extend PreservationReport with ToJson::{to_json}
///|
/// Combine independent preservation evidence with a fresh full BagIt validation.
pub fn revalidate_snapshot(
expected : BagSnapshot,
current : BagView,
) -> PreservationReport raise FormatError {
let inventory = verify_snapshot_inventory(
expected,
current.files,
current.directories,
)
let bag = validate(current)
let declaration_matches = match current.texts.get("bagit.txt") {
Some(text) => {
let declaration = parse_declaration(text) catch {
Invalid(_) => { version: "", encoding: "", }
}
declaration.version == expected.bag_version &&
declaration.encoding == expected.encoding
}
None => false
}
let fields = match current.texts.get("bag-info.txt") {
Some(text) => parse_metadata(text) catch { Invalid(_) => [] }
None => []
}
let metadata = compare_metadata(expected.metadata, fields)
{
preserved: bag.valid &&
inventory.matches &&
declaration_matches &&
metadata.is_empty(),
bag,
inventory,
metadata,
declaration_matches,
}
}