use crate::archive::EntryMeta;
use crate::error::{Error, Result};
use crate::source::SeekRead;
pub const MAX_SALVAGE_ENTRY: u64 = 4 * 1024 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verifier {
Crc32(u32),
Crc16(u16),
}
#[derive(Debug)]
pub struct Candidate {
pub offset: u64,
pub meta: EntryMeta,
pub declared_len: Option<u64>,
pub verifier: Option<Verifier>,
pub available_len: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnverifiedCause {
UndecodableMethod,
NoDeclaredLength,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SalvageStatus {
Intact,
Complete,
Unverified(UnverifiedCause),
Partial,
}
#[derive(Debug)]
pub struct SalvagedEntry {
pub scan_position: usize,
pub offset: u64,
pub meta: EntryMeta,
pub status: SalvageStatus,
pub shadows: Option<usize>,
pub collides_with: Option<usize>,
}
#[derive(Debug)]
pub struct SalvageOutcome {
pub entries: Vec<SalvagedEntry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PartialPolicy {
Keep,
Skip,
Ask,
}
pub struct SalvagePolicy {
pub partial: PartialPolicy,
pub max_entry: u64,
pub strict: bool,
}
impl Default for SalvagePolicy {
fn default() -> Self {
Self {
partial: PartialPolicy::Keep,
max_entry: MAX_SALVAGE_ENTRY,
strict: false,
}
}
}
pub trait SalvageScan {
fn next_candidate(&mut self, src: &mut dyn SeekRead, from: u64) -> Result<Option<Candidate>>;
fn verify(&self, _src: &mut dyn SeekRead, _candidate: &Candidate) -> Result<SalvageStatus> {
Ok(SalvageStatus::Complete)
}
}
pub fn salvage_all(
scan: &mut dyn SalvageScan,
src: &mut dyn SeekRead,
policy: &SalvagePolicy,
) -> Result<SalvageOutcome> {
let candidates = collect_candidates(scan, src, policy)?;
annotate_candidates(&*scan, src, candidates)
}
pub fn collect_candidates(
scan: &mut dyn SalvageScan,
src: &mut dyn SeekRead,
policy: &SalvagePolicy,
) -> Result<Vec<Candidate>> {
let mut candidates = Vec::new();
let mut from = 0u64;
while let Some(candidate) = scan.next_candidate(src, from)? {
let bounded_len = candidate.available_len.or(candidate.declared_len);
if let Some(len) = bounded_len
&& len > policy.max_entry
{
return Err(Error::ResourceLimit(format!(
"declared entry length {len} bytes exceeds the {}-byte salvage ceiling \
(see MAX_SALVAGE_ENTRY)",
policy.max_entry
)));
}
let advance = bounded_len.unwrap_or(1).max(1);
from = candidate.offset.saturating_add(advance);
candidates.push(candidate);
}
Ok(candidates)
}
pub fn annotate_candidates(
scan: &dyn SalvageScan,
src: &mut dyn SeekRead,
candidates: Vec<Candidate>,
) -> Result<SalvageOutcome> {
let mut entries = Vec::with_capacity(candidates.len());
let mut seen: Vec<(String, Option<u64>, Verifier, usize)> = Vec::new();
let mut names_seen: Vec<(String, usize)> = Vec::new();
for (scan_position, candidate) in candidates.into_iter().enumerate() {
let status = scan.verify(src, &candidate)?;
let declared_len = candidate.declared_len;
let verifier = candidate.verifier;
let is_degenerate = declared_len == Some(0);
let shadows = if is_degenerate {
None
} else {
verifier.and_then(|v| {
seen.iter()
.find(|&&(ref seen_name, seen_len, seen_verifier, _)| {
seen_verifier == v
&& seen_len == declared_len
&& *seen_name == candidate.meta.name
})
.map(|&(_, _, _, earlier_position)| earlier_position)
})
};
if !is_degenerate
&& shadows.is_none()
&& let Some(v) = verifier
{
seen.push((candidate.meta.name.clone(), declared_len, v, scan_position));
}
let earliest_with_name = names_seen
.iter()
.find(|(name, _)| *name == candidate.meta.name)
.map(|&(_, position)| position);
let collides_with = if shadows.is_some() {
None
} else {
earliest_with_name
};
if earliest_with_name.is_none() {
names_seen.push((candidate.meta.name.clone(), scan_position));
}
entries.push(SalvagedEntry {
scan_position,
offset: candidate.offset,
meta: candidate.meta,
status,
shadows,
collides_with,
});
}
Ok(SalvageOutcome { entries })
}
#[cfg(test)]
mod tests {
use std::io::{Cursor, Read, Seek, SeekFrom};
use super::*;
fn bytes(data: &[u8]) -> Cursor<Vec<u8>> {
Cursor::new(data.to_vec())
}
struct PanickingReader(Cursor<Vec<u8>>);
impl Read for PanickingReader {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let backing_len = self.0.get_ref().len();
assert!(
buf.len() <= backing_len,
"oversized read requested: {} bytes against a {backing_len}-byte source \
— an allocation or read was sized from a declared length before it was bounded",
buf.len()
);
self.0.read(buf)
}
}
impl Seek for PanickingReader {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
self.0.seek(pos)
}
}
fn panicking_reader(len: usize) -> PanickingReader {
PanickingReader(Cursor::new(vec![0u8; len]))
}
struct NeverFinds;
impl SalvageScan for NeverFinds {
fn next_candidate(
&mut self,
_src: &mut dyn SeekRead,
_from: u64,
) -> Result<Option<Candidate>> {
Ok(None)
}
}
struct DeclaresLength(u64);
impl SalvageScan for DeclaresLength {
fn next_candidate(
&mut self,
_src: &mut dyn SeekRead,
from: u64,
) -> Result<Option<Candidate>> {
if from > 0 {
return Ok(None);
}
Ok(Some(Candidate {
offset: 0,
meta: EntryMeta::file("absurd"),
declared_len: Some(self.0),
verifier: None,
available_len: None,
}))
}
}
struct DeclaresMoreThanIsThere {
declared: u64,
available: u64,
}
impl SalvageScan for DeclaresMoreThanIsThere {
fn next_candidate(
&mut self,
_src: &mut dyn SeekRead,
from: u64,
) -> Result<Option<Candidate>> {
if from > 0 {
return Ok(None);
}
Ok(Some(Candidate {
offset: 0,
meta: EntryMeta::file("cut-short"),
declared_len: Some(self.declared),
verifier: Some(Verifier::Crc32(1)),
available_len: Some(self.available),
}))
}
}
#[test]
fn a_scanner_that_finds_nothing_yields_no_entries() {
let out = salvage_all(
&mut NeverFinds,
&mut bytes(&[0u8; 4096]),
&SalvagePolicy::default(),
);
assert!(
out.unwrap().entries.is_empty(),
"a scanner finding nothing must yield nothing"
);
}
#[test]
fn an_absurd_declared_length_is_refused_before_the_allocation_it_would_size() {
let mut scan = DeclaresLength(u64::MAX);
let err = salvage_all(
&mut scan,
&mut panicking_reader(4096),
&SalvagePolicy::default(),
)
.unwrap_err();
assert_eq!(err.exit_code(), 6);
}
#[test]
fn a_declared_length_within_the_ceiling_is_collected_as_complete() {
let mut scan = DeclaresLength(4096);
let out = salvage_all(
&mut scan,
&mut panicking_reader(4096),
&SalvagePolicy::default(),
)
.unwrap();
assert_eq!(out.entries.len(), 1);
let entry = &out.entries[0];
assert_eq!(entry.scan_position, 0);
assert_eq!(entry.offset, 0);
assert_eq!(entry.status, SalvageStatus::Complete);
assert_eq!(entry.shadows, None);
assert_eq!(entry.collides_with, None);
}
#[test]
fn a_candidate_whose_payload_ran_out_is_still_collected() {
let mut scan = DeclaresMoreThanIsThere {
declared: 4000,
available: 1950,
};
let out = salvage_all(
&mut scan,
&mut panicking_reader(4096),
&SalvagePolicy::default(),
)
.unwrap();
assert_eq!(
out.entries.len(),
1,
"a header found but not completable is a fact worth reporting"
);
}
#[test]
fn a_truncated_candidate_is_bounded_by_what_is_there_not_by_what_it_claimed() {
let mut scan = DeclaresMoreThanIsThere {
declared: u64::MAX,
available: 2048,
};
let out = salvage_all(
&mut scan,
&mut panicking_reader(4096),
&SalvagePolicy::default(),
)
.expect("an absent tail must not abort a run that recovered real entries");
assert_eq!(out.entries.len(), 1);
}
#[test]
fn a_truncated_candidate_whose_available_bytes_exceed_the_ceiling_is_refused() {
let mut scan = DeclaresMoreThanIsThere {
declared: u64::MAX,
available: 4096,
};
let err = salvage_all(
&mut scan,
&mut panicking_reader(4096),
&SalvagePolicy {
max_entry: 1024,
..SalvagePolicy::default()
},
)
.unwrap_err();
assert_eq!(err.exit_code(), 6);
}
#[test]
fn the_default_policy_is_recovery_biased() {
let policy = SalvagePolicy::default();
assert_eq!(policy.partial, PartialPolicy::Keep);
assert_eq!(policy.max_entry, MAX_SALVAGE_ENTRY);
assert!(!policy.strict);
}
struct ScriptedCandidates {
plan: Vec<(&'static str, Option<u64>, Option<Verifier>, SalvageStatus)>,
next: usize,
}
impl SalvageScan for ScriptedCandidates {
fn next_candidate(
&mut self,
_src: &mut dyn SeekRead,
_from: u64,
) -> Result<Option<Candidate>> {
if self.next >= self.plan.len() {
return Ok(None);
}
let offset = self.next as u64;
let (name, declared_len, verifier, _) = self.plan[self.next];
self.next += 1;
Ok(Some(Candidate {
offset,
meta: EntryMeta::file(name),
declared_len,
verifier,
available_len: None,
}))
}
fn verify(&self, _src: &mut dyn SeekRead, candidate: &Candidate) -> Result<SalvageStatus> {
Ok(self.plan[candidate.offset as usize].3)
}
}
#[test]
fn verify_decides_each_entrys_status_not_a_hardcoded_default() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"a",
Some(1),
Some(Verifier::Crc32(1)),
SalvageStatus::Intact,
),
("b", Some(1), None, SalvageStatus::Complete),
(
"c",
Some(1),
Some(Verifier::Crc32(2)),
SalvageStatus::Partial,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[0].status, SalvageStatus::Intact);
assert_eq!(out.entries[1].status, SalvageStatus::Complete);
assert_eq!(out.entries[2].status, SalvageStatus::Partial);
}
#[test]
fn a_later_candidate_sharing_an_earlier_verifier_is_marked_as_its_shadow() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"dup",
Some(1),
Some(Verifier::Crc32(7)),
SalvageStatus::Intact,
),
(
"other",
Some(1),
Some(Verifier::Crc32(9)),
SalvageStatus::Intact,
),
(
"dup",
Some(1),
Some(Verifier::Crc32(7)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[0].shadows, None);
assert_eq!(out.entries[1].shadows, None);
assert_eq!(
out.entries[2].shadows,
Some(0),
"must point at the earliest matching position"
);
}
#[test]
fn candidates_with_no_verifier_never_shadow_each_other() {
let mut scan = ScriptedCandidates {
plan: vec![
("a", Some(1), None, SalvageStatus::Complete),
("a", Some(1), None, SalvageStatus::Complete),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[0].shadows, None);
assert_eq!(out.entries[1].shadows, None);
}
#[test]
fn a_matching_verifier_under_a_different_name_is_never_marked_as_a_shadow() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"one.txt",
Some(4),
Some(Verifier::Crc32(42)),
SalvageStatus::Intact,
),
(
"two.txt",
Some(4),
Some(Verifier::Crc32(42)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(
out.entries[1].shadows, None,
"same checksum, different name — not a shadow"
);
}
#[test]
fn a_zero_length_declared_verifier_never_shadows_even_under_a_matching_name() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"empty.txt",
Some(0),
Some(Verifier::Crc32(0)),
SalvageStatus::Complete,
),
(
"empty.txt",
Some(0),
Some(Verifier::Crc32(0)),
SalvageStatus::Complete,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[0].shadows, None);
assert_eq!(
out.entries[1].shadows, None,
"a zero-byte checksum must never mark a shadow, even under an identical name"
);
assert_eq!(
out.entries[1].collides_with,
Some(0),
"the NAME still repeats, and that is a directly observed fact no checksum \
guard applies to"
);
}
#[test]
fn a_repeated_name_with_different_content_is_reported_as_a_collision() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"dup.txt",
Some(21),
Some(Verifier::Crc32(7)),
SalvageStatus::Intact,
),
(
"other.txt",
Some(21),
Some(Verifier::Crc32(9)),
SalvageStatus::Intact,
),
(
"dup.txt",
Some(21),
Some(Verifier::Crc32(11)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[2].shadows, None, "the content does not agree");
assert_eq!(
out.entries[2].collides_with,
Some(0),
"but the name does, and that is the fact `list` warns about"
);
assert_eq!(out.entries[0].collides_with, None);
assert_eq!(out.entries[1].collides_with, None);
}
#[test]
fn a_proven_shadow_is_not_also_reported_as_a_name_collision() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"dup.txt",
Some(21),
Some(Verifier::Crc32(7)),
SalvageStatus::Intact,
),
(
"dup.txt",
Some(21),
Some(Verifier::Crc32(7)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[1].shadows, Some(0));
assert_eq!(out.entries[1].collides_with, None);
}
#[test]
fn a_third_record_under_one_name_names_the_earliest_position() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"dup",
Some(4),
Some(Verifier::Crc32(1)),
SalvageStatus::Intact,
),
(
"dup",
Some(4),
Some(Verifier::Crc32(2)),
SalvageStatus::Intact,
),
(
"dup",
Some(4),
Some(Verifier::Crc32(3)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[1].collides_with, Some(0));
assert_eq!(out.entries[2].collides_with, Some(0));
}
#[test]
fn distinct_names_are_never_reported_as_colliding() {
let mut scan = ScriptedCandidates {
plan: vec![
(
"one.txt",
Some(4),
Some(Verifier::Crc32(42)),
SalvageStatus::Intact,
),
(
"two.txt",
Some(4),
Some(Verifier::Crc32(42)),
SalvageStatus::Intact,
),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(out.entries[0].collides_with, None);
assert_eq!(out.entries[1].collides_with, None);
}
#[test]
fn a_repeated_name_collides_even_with_no_verifier_to_compare() {
let mut scan = ScriptedCandidates {
plan: vec![
("same", Some(4), None, SalvageStatus::Complete),
("same", Some(4), None, SalvageStatus::Complete),
],
next: 0,
};
let out = salvage_all(&mut scan, &mut bytes(&[0u8; 16]), &SalvagePolicy::default())
.expect("scripted candidates must annotate cleanly");
assert_eq!(
out.entries[1].shadows, None,
"no verifier, so nothing proves they are copies"
);
assert_eq!(
out.entries[1].collides_with,
Some(0),
"but the name repeats, which needs no verifier to observe"
);
}
}