pub use crate::salvage::Attestation;
use crate::salvage::SalvageStatus;
use crate::{EntryKind, Error, Fidelity, FidelityReport};
pub fn check_error_is_classified(e: &Error) -> Result<(), String> {
match e.exit_code() {
1 => Err(format!(
"a decoder raised {e:?}, which maps to exit code 1 — that means \
stuffr failed, not that the input was bad"
)),
_ => Ok(()),
}
}
pub fn check_entry_size(
declared: Option<u64>,
produced: u64,
name: &str,
kind: &EntryKind,
) -> Result<(), String> {
if matches!(kind, EntryKind::Symlink { .. }) {
return Ok(());
}
match declared {
Some(d) if d != produced => Err(format!(
"entry {name:?} declared {d} bytes but produced {produced}"
)),
_ => Ok(()),
}
}
pub fn check_entry_count(
declared: Option<usize>,
enumerated: usize,
report: &FidelityReport,
) -> Result<(), String> {
let Some(d) = declared else { return Ok(()) };
if d == enumerated {
return Ok(());
}
if report
.warnings
.iter()
.any(|w| matches!(w, Fidelity::EntryCountMismatch { .. }))
{
return Ok(());
}
Err(format!(
"the index declares {d} entries but {enumerated} were enumerated, and the \
fidelity report does not carry an EntryCountMismatch warning saying so"
))
}
pub fn check_fidelity_claim(report: &FidelityReport, approximated: bool) -> Result<(), String> {
if report.is_lossless() && approximated {
return Err(format!(
"the report claims nothing was approximated ({:?}, no warnings) \
but something was",
report.rung
));
}
Ok(())
}
pub fn check_entries_carried(
source: &[String],
written: &[String],
report: &FidelityReport,
) -> Result<(), String> {
let arrived: std::collections::HashSet<&str> = written.iter().map(String::as_str).collect();
let skipped: std::collections::HashSet<&str> = report
.warnings
.iter()
.filter_map(|w| match w {
Fidelity::EntrySkipped { entry, .. } | Fidelity::EncryptedEntrySkipped { entry } => {
Some(entry.as_str())
}
_ => None,
})
.collect();
match source
.iter()
.find(|n| !arrived.contains(n.as_str()) && !skipped.contains(n.as_str()))
{
Some(missing) => Err(format!(
"the source held entry {missing:?}, the converted archive does not, and \
the fidelity report names no skip for it"
)),
None => Ok(()),
}
}
pub fn check_salvage_claim(
status: &SalvageStatus,
verifier_was_checked: bool,
attestation: Attestation,
) -> Result<(), String> {
match status {
SalvageStatus::Intact => match attestation {
Attestation::ContentChecksum if verifier_was_checked => Ok(()),
Attestation::ContentChecksum => {
Err("reported Intact without checking a checksum".into())
}
Attestation::HeaderChecksumOnly => Err(
"reported Intact — a content checksum agreed — for a format whose only \
checksum covers the header; Complete is that format's tier"
.into(),
),
Attestation::Nothing => Err(
"reported Intact — a checksum agreed — for a format that offers nothing to \
check against"
.into(),
),
},
SalvageStatus::Complete => match attestation {
Attestation::HeaderChecksumOnly if verifier_was_checked => Ok(()),
Attestation::HeaderChecksumOnly => Err(
"reported Complete without checking the header checksum this format offers; \
Unverified is the honest answer when it was not used"
.into(),
),
Attestation::ContentChecksum => Err(
"reported Complete for a format that carries a content checksum; the honest \
answer is Intact if it agreed, Partial if it did not, Unverified if it was \
never compared"
.into(),
),
Attestation::Nothing => Err(
"reported Complete — every declared byte present and the header \
self-verified — for a format with no checksum and no self-verifying header; \
Unattested is that format's tier"
.into(),
),
},
SalvageStatus::Unattested => match attestation {
Attestation::Nothing if !verifier_was_checked => Ok(()),
Attestation::Nothing => Err(
"reported Unattested while claiming a verifier was checked, for a format \
that has none to check"
.into(),
),
Attestation::ContentChecksum | Attestation::HeaderChecksumOnly => Err(
"reported Unattested — nothing attests this is an entry — for a format that \
does offer an attestation"
.into(),
),
},
SalvageStatus::Unverified(_) | SalvageStatus::Partial => Ok(()),
}
}
#[cfg(test)]
mod broken_honesty {
use super::*;
use crate::Error;
use crate::fidelity::Rung;
#[test]
fn an_io_error_from_hostile_bytes_is_refused_because_it_exits_one() {
let e = Error::Io(std::io::Error::other("boom"));
let msg = check_error_is_classified(&e).expect_err("exit 1 must be refused");
assert!(
msg.contains("exit code 1"),
"the failure must name why: {msg}"
);
}
#[test]
fn corrupt_unsupported_and_resource_limit_are_all_permitted() {
for e in [
Error::Corrupt("bad".into()),
Error::Unsupported("nope".into()),
Error::ResourceLimit("too big".into()),
Error::NotSeekable {
format: crate::FormatId::new("zip"),
},
] {
assert!(
check_error_is_classified(&e).is_ok(),
"{e:?} must be permitted"
);
}
}
#[test]
fn a_declared_size_that_disagrees_with_bytes_produced_is_refused() {
let msg = check_entry_size(Some(100), 42, "a.txt", &EntryKind::File)
.expect_err("mismatch must fail");
assert!(msg.contains("a.txt"), "must name the entry: {msg}");
assert!(
msg.contains("100") && msg.contains("42"),
"must name both numbers: {msg}"
);
assert!(check_entry_size(None, 42, "a.txt", &EntryKind::File).is_ok());
assert!(check_entry_size(Some(42), 42, "a.txt", &EntryKind::File).is_ok());
for kind in [EntryKind::Dir, EntryKind::Other] {
assert!(
check_entry_size(Some(100), 42, "a.txt", &kind).is_err(),
"{kind:?} must stay in scope"
);
}
}
#[test]
fn a_symlink_whose_target_was_consumed_eagerly_is_permitted() {
let link = EntryKind::Symlink {
target: "real.txt".into(),
};
assert!(
check_entry_size(Some(8), 0, "link.txt", &link).is_ok(),
"a symlink's target is read by the container, not by the caller"
);
}
#[test]
fn a_declared_count_that_disagrees_with_entries_enumerated_is_refused() {
let silent = FidelityReport::new(Rung::Exact);
let msg = check_entry_count(Some(8), 6, &silent).expect_err("mismatch must fail");
assert!(
msg.contains("8") && msg.contains("6"),
"must name both: {msg}"
);
assert!(check_entry_count(None, 6, &silent).is_ok());
assert!(check_entry_count(Some(6), 6, &silent).is_ok());
}
#[test]
fn a_declared_count_mismatch_the_report_owns_up_to_is_permitted() {
let mut declared = FidelityReport::new(Rung::Exact);
declared.warn(Fidelity::EntryCountMismatch {
format: crate::FormatId::new("zip"),
declared: 8,
enumerated: 6,
reason: "records sharing a name collapse into one".into(),
});
assert!(
check_entry_count(Some(8), 6, &declared).is_ok(),
"a shortfall the report owns up to is a supported outcome, not a violation"
);
let mut unrelated = FidelityReport::new(Rung::Exact);
unrelated.warn(Fidelity::MetadataIncomplete {
entry: "a.txt".into(),
fields: crate::MetaFields::default(),
});
assert!(
check_entry_count(Some(8), 6, &unrelated).is_err(),
"an unrelated warning does not declare a count shortfall"
);
}
#[test]
fn claiming_exact_with_no_warnings_while_approximating_is_refused() {
let clean = FidelityReport::new(Rung::Exact);
let msg = check_fidelity_claim(&clean, true).expect_err("a false claim must fail");
assert!(
msg.contains("approximated"),
"must say what was claimed: {msg}"
);
assert!(check_fidelity_claim(&clean, false).is_ok());
}
#[test]
fn an_entry_a_conversion_drops_silently_is_refused() {
let names = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let clean = FidelityReport::new(Rung::Exact);
let msg = check_entries_carried(&names(&["a", "b"]), &names(&["a"]), &clean)
.expect_err("a dropped entry must fail");
assert!(msg.contains("\"b\""), "must name the entry: {msg}");
let mut other = FidelityReport::new(Rung::Exact);
other.warn(Fidelity::EntrySkipped {
entry: "c".into(),
reason: "special".into(),
});
assert!(check_entries_carried(&names(&["a", "b"]), &names(&["a"]), &other).is_err());
assert!(check_entries_carried(&names(&["a", "b"]), &names(&["b", "a"]), &clean).is_ok());
}
#[test]
fn an_entry_a_conversion_skips_and_names_is_permitted() {
let names = |v: &[&str]| v.iter().map(|s| s.to_string()).collect::<Vec<_>>();
let mut skipped = FidelityReport::new(Rung::Exact);
skipped.warn(Fidelity::EntrySkipped {
entry: "dir/".into(),
reason: "no directory entries".into(),
});
assert!(
check_entries_carried(&names(&["dir/", "x", "x"]), &names(&["x"]), &skipped).is_ok()
);
let mut encrypted = FidelityReport::new(Rung::Exact);
encrypted.warn(Fidelity::EncryptedEntrySkipped {
entry: "secret".into(),
});
assert!(check_entries_carried(&names(&["secret"]), &[], &encrypted).is_ok());
}
#[test]
fn an_intact_status_whose_checksum_was_never_checked_is_refused() {
let msg = check_salvage_claim(&SalvageStatus::Intact, false, Attestation::ContentChecksum)
.expect_err("Intact without a checked checksum must be refused");
assert!(msg.contains("Intact"), "must name the status: {msg}");
assert!(
check_salvage_claim(&SalvageStatus::Intact, true, Attestation::ContentChecksum).is_ok()
);
}
#[test]
fn an_intact_status_over_a_format_with_nothing_to_check_is_refused() {
let msg = check_salvage_claim(&SalvageStatus::Intact, true, Attestation::Nothing)
.expect_err("Intact over a format offering nothing must be refused");
assert!(msg.contains("Intact"), "must name the status: {msg}");
}
#[test]
fn an_intact_status_over_a_header_only_checksum_is_refused() {
let msg = check_salvage_claim(
&SalvageStatus::Intact,
true,
Attestation::HeaderChecksumOnly,
)
.expect_err("Intact over a header-only checksum must be refused");
assert!(msg.contains("Intact"), "must name the status: {msg}");
assert!(
msg.contains("Complete"),
"must name the tier that IS honest: {msg}"
);
}
#[test]
fn complete_without_the_attestation_it_claims_is_refused() {
let err = check_salvage_claim(
&SalvageStatus::Complete,
false,
Attestation::HeaderChecksumOnly,
)
.expect_err("Complete whose header checksum went unchecked must be refused");
assert!(
err.contains("Complete"),
"the message must name the tier it refused: {err}"
);
}
#[test]
fn complete_over_a_format_that_attests_nothing_is_refused() {
let err = check_salvage_claim(&SalvageStatus::Complete, false, Attestation::Nothing)
.expect_err("Complete over a format with no attestation at all must be refused");
assert!(err.contains("Complete"), "must name the tier: {err}");
assert!(
err.contains("Unattested"),
"must name the tier that IS honest here: {err}"
);
}
#[test]
fn complete_over_a_content_checksum_is_refused_even_when_checked() {
for checked in [true, false] {
let err = check_salvage_claim(
&SalvageStatus::Complete,
checked,
Attestation::ContentChecksum,
)
.expect_err("Complete over a content checksum must be refused");
assert!(err.contains("Complete"), "must name the tier: {err}");
}
}
#[test]
fn unattested_is_refused_when_the_format_can_self_verify() {
for attestation in [
Attestation::HeaderChecksumOnly,
Attestation::ContentChecksum,
] {
let err = check_salvage_claim(&SalvageStatus::Unattested, false, attestation)
.expect_err("Unattested over an attesting format must be refused");
assert!(
err.contains("Unattested"),
"message must name the tier: {err}"
);
}
}
#[test]
fn unattested_claiming_a_checked_verifier_is_refused() {
let err = check_salvage_claim(&SalvageStatus::Unattested, true, Attestation::Nothing)
.expect_err("a checked verifier in a format with none must be refused");
assert!(
err.contains("Unattested"),
"message must name the tier: {err}"
);
}
#[test]
fn unattested_is_permitted_where_nothing_attests() {
check_salvage_claim(&SalvageStatus::Unattested, false, Attestation::Nothing)
.expect("cpio and ar have no verifier of any kind");
}
#[test]
fn each_class_is_permitted_its_one_honest_tier() {
assert!(
check_salvage_claim(&SalvageStatus::Intact, true, Attestation::ContentChecksum).is_ok(),
"zip/arc/zoo/lha/arj with an agreeing CRC"
);
assert!(
check_salvage_claim(
&SalvageStatus::Complete,
true,
Attestation::HeaderChecksumOnly
)
.is_ok(),
"tar with a validated header checksum"
);
assert!(
check_salvage_claim(&SalvageStatus::Unattested, false, Attestation::Nothing).is_ok(),
"cpio/ar"
);
}
#[test]
fn unverified_and_partial_are_permitted_for_every_class() {
use crate::salvage::UnverifiedCause;
let statuses = [
SalvageStatus::Partial,
SalvageStatus::Unverified(UnverifiedCause::UndecodableMethod),
SalvageStatus::Unverified(UnverifiedCause::NoDeclaredLength),
SalvageStatus::Unverified(UnverifiedCause::OverEntryCeiling {
needed: 2,
ceiling: 1,
}),
];
for status in &statuses {
for attestation in ALL_ATTESTATIONS {
for checked in [true, false] {
assert!(
check_salvage_claim(status, checked, attestation).is_ok(),
"{status:?} / {attestation:?} / checked={checked} must be permitted"
);
}
}
}
}
const ALL_ATTESTATIONS: [Attestation; 3] = [
Attestation::ContentChecksum,
Attestation::HeaderChecksumOnly,
Attestation::Nothing,
];
#[test]
fn the_whole_truth_table_permits_exactly_one_claim_per_class() {
let expected_ok = |status: &SalvageStatus, checked: bool, a: Attestation| match status {
SalvageStatus::Intact => a == Attestation::ContentChecksum && checked,
SalvageStatus::Complete => a == Attestation::HeaderChecksumOnly && checked,
SalvageStatus::Unattested => a == Attestation::Nothing && !checked,
SalvageStatus::Unverified(_) | SalvageStatus::Partial => true,
};
let mut permitted = 0;
for status in [
SalvageStatus::Intact,
SalvageStatus::Complete,
SalvageStatus::Unattested,
] {
for attestation in ALL_ATTESTATIONS {
for checked in [true, false] {
let got = check_salvage_claim(&status, checked, attestation).is_ok();
let want = expected_ok(&status, checked, attestation);
assert_eq!(
got, want,
"{status:?} / {attestation:?} / checked={checked}: oracle said {got}, \
the rule says {want}"
);
permitted += usize::from(got);
}
}
}
assert_eq!(
permitted, 3,
"exactly one cell per claim-bearing tier is honest — 18 cells, 3 permitted"
);
}
}