#[non_exhaustive]pub struct Candidate {
pub offset: u64,
pub payload_start: u64,
pub meta: EntryMeta,
pub declared_len: Option<u64>,
pub verifier: Option<Verifier>,
pub available_len: Option<u64>,
pub marked_deleted: bool,
}Expand description
A record a scanner believes it found, before anything is decoded.
§Why this is #[non_exhaustive] AND has a constructor
Ruling S-I, deferred from Salvage Stage 2 Task 1 to Task 9 on purpose:
this struct is the one shape SalvageScan::next_candidate forces an
external implementor to build, so every field added to it used to break
every out-of-crate scanner at once — and at 0.5.0 all four crates are
published, so “out-of-crate” is not hypothetical. The attribute alone
would have been the wrong fix (CONTRIBUTING.md’s #[non_exhaustive]
section says why: it forbids .. construction from another crate, which
is exactly what an implementor must do), which is why it waited for
Candidate::new — and for all five in-tree scanners to exist, so the
constructor’s shape could be read off real callers rather than guessed.
The three arguments Candidate::new takes are the three facts every
scanner knows the instant it recognises a record. The four with_*
setters cover the rest, each taking the field’s own type unchanged so
nothing is hidden behind a conversion.
Fields (Non-exhaustive)§
This struct is marked as non-exhaustive
Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.offset: u64Byte offset of the record’s header in the archive.
payload_start: u64Byte offset where this record’s PAYLOAD begins — past whatever fixed and variable-length header fields this format’s record carries (a zip local header’s name/extra fields, ARC’s fixed 28-byte record, and so on).
Task 3c added this field after entries.rs’s write path hardcoded
a ZIP local header’s own 30-byte-plus-name-plus-extra layout as
the way to locate every format’s payload — which crashed
(Error::Io, exit 1, the wildcard this project treats as a
defect) the moment a non-zip scanner (ARC) reported a candidate
near the end of the file, because reading 30 bytes from ITS offset
ran past EOF. Each scanner already computes this value once, at
discovery, to know where ITS OWN payload begins — this field
carries that computation forward instead of a consumer re-deriving
(and mis-deriving) it later. A future scanner (zoo, lha, arj) fills
this the same way; nothing outside the scanner that discovered a
candidate needs to know its record’s own layout at all.
meta: EntryMeta§declared_len: Option<u64>Declared payload length. None when the header does not carry one.
verifier: Option<Verifier>The checksum the ORIGINAL writer computed, when the format has one.
None is what produces the intact/complete split honestly, rather
than by a per-format convention someone has to remember.
available_len: Option<u64>How many of declared_len’s bytes the SOURCE actually holds, when
it holds fewer — the truncated-tail case. None means the declared
payload is entirely present (the ordinary case) or nothing was
declared at all; Some(n) always means n < declared_len.
§Why a scanner reports this instead of dropping the candidate
A truncated archive — an interrupted download — is the single most
common damaged zip there is, and it was the one shape salvage said
nothing at all about. zip_salvage.rs’s validation gate refused any
candidate whose declared payload ran past the end of the file, which
is a correct refusal to INVENT the missing bytes (zip -FF
fabricates them and reports success) implemented as SILENCE: no row,
no note, no non-zero exit. Swept across six truncation points inside
a three-header archive’s last entry, every one reported “2 scanned”
at exit 0 — the third header was not even counted.
Reporting the candidate with this field set is what turns the
refusal into a statement. Nothing is invented: the declared length
stays exactly what the header said, so a reader can see both figures,
and the bytes that ARE present are still a genuine prefix. What a
scanner must NOT do is fold the shortfall into declared_len — that
would make a truncated entry indistinguishable from a whole one of
the smaller size, which is the same mistake in a different place.
marked_deleted: boolThe archive’s own record marks this entry DELETED — the format’s owner removed it and the writer left the record, and its payload, in place.
false for every format that has no such flag (zip, tar, ar, cpio,
arc), which is why it is a plain bool rather than an Option:
“this format cannot express deletion” and “this record is not
deleted” lead to the identical, unannotated row, and a third state
would be a distinction no consumer could act on.
§Why a scanner reports one rather than dropping it (Ruling S-R)
Salvage Stage 2 Task 4, for ZOO — the first format here with the
flag. zoo d marks an entry deleted = 1 and leaves it whole in the
file; zoolist.c neither lists nor extracts it, and legacy::zoo’s
reader follows that exactly. The flag is ONE BYTE, so in a damaged
archive a bit flip turns a live entry into one no ordinary verb will
ever hand back — which is precisely the archive this verb exists for.
So the scanner reports it.
And the report carries a marker, which is the part that took a
review to get right. Everywhere else salvage accepts less than an
ordinary entry it says so on the artifact or in the row: a Partial
lands as NAME.partial and never under its real name, a second
record under a taken name lands as NAME.salvaged-N, a proven copy
prints [shadowed]. A deleted record reported Intact under its
real name at exit 0 was the one place that leniency was invisible —
four verbs call the archive empty and the fifth writes the file, with
nothing telling a user which it was.
The exit code deliberately does NOT move. Recovering a deleted
record is this verb working as designed, not degraded fidelity — see
stuffr::entries’s own exit-code aggregation, which this field is
invisible to.
Implementations§
Source§impl Candidate
impl Candidate
Sourcepub fn new(offset: u64, payload_start: u64, meta: EntryMeta) -> Self
pub fn new(offset: u64, payload_start: u64, meta: EntryMeta) -> Self
A candidate at offset, whose payload begins at payload_start,
carrying meta.
Nothing else is claimed. The four fields this does not take default to the answers that assert the least:
declared_len: None— the header carries no length, so nothing bounds the payload andannotate_candidatesreportsUnverifiedCause::NoDeclaredLengthrather than reading it.verifier: None— the format offers nothing to prove the content with, so no shadow is ever claimed for this record (seeSalvagedEntry::shadows, which needs a verifier to compare).available_len: None— every declared byte is present, which is the ordinary case; a scanner sets this only when it has MEASURED a shortfall.marked_deleted: false— the format has no deleted flag, or this record is not marked. Four of the five in-tree scanners leave it.
One default is worth naming because it decides a STATUS, not just a
field: with no verifier, SalvageScan::verify’s own default
answers SalvageStatus::Unattested — the tier that asserts the
least, matching the discipline of the four above. It answered
SalvageStatus::Complete until Salvage Stage 3, which was the
lenient direction: Complete claims a header self-check the default
never performs, and routes to exit 0. A scanner carrying a checksum
has to write its own verify regardless (the default checks
nothing), and that verify reads this very field, so the omission
fails in the scanner’s own first test rather than silently.
Sourcepub fn with_declared_len(self, declared_len: Option<u64>) -> Self
pub fn with_declared_len(self, declared_len: Option<u64>) -> Self
Sets Self::declared_len.
Sourcepub fn with_verifier(self, verifier: Option<Verifier>) -> Self
pub fn with_verifier(self, verifier: Option<Verifier>) -> Self
Sets Self::verifier.
Sourcepub fn with_available_len(self, available_len: Option<u64>) -> Self
pub fn with_available_len(self, available_len: Option<u64>) -> Self
Sets Self::available_len — the truncated-tail case, and only
when the shortfall was measured. See that field’s doc for why a
scanner must never fold it into Self::declared_len instead.
Sourcepub fn with_marked_deleted(self, marked_deleted: bool) -> Self
pub fn with_marked_deleted(self, marked_deleted: bool) -> Self
Sets Self::marked_deleted — Ruling S-R. See that field’s doc.