Skip to main content

Candidate

Struct Candidate 

Source
#[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
Non-exhaustive structs could have additional fields added in future. Therefore, non-exhaustive structs cannot be constructed in external crates using the traditional Struct { .. } syntax; cannot be matched against without a wildcard ..; and struct update syntax will not work.
§offset: u64

Byte offset of the record’s header in the archive.

§payload_start: u64

Byte 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: bool

The 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

Source

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 and annotate_candidates reports UnverifiedCause::NoDeclaredLength rather than reading it.
  • verifier: None — the format offers nothing to prove the content with, so no shadow is ever claimed for this record (see SalvagedEntry::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.

Source

pub fn with_declared_len(self, declared_len: Option<u64>) -> Self

Source

pub fn with_verifier(self, verifier: Option<Verifier>) -> Self

Source

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.

Source

pub fn with_marked_deleted(self, marked_deleted: bool) -> Self

Sets Self::marked_deleted — Ruling S-R. See that field’s doc.

Trait Implementations§

Source§

impl Debug for Candidate

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.