#[non_exhaustive]pub enum Error {
Show 16 variants
Io(Error),
Usage(String),
FormatNotEnabled(FormatId),
UnknownFormat {
seen: String,
},
AmbiguousFormat {
candidates: String,
},
NotSeekable {
format: FormatId,
},
SpillLimitExceeded {
limit: u64,
},
ResourceLimit(String),
Corrupt(String),
UnsafePath {
path: String,
reason: &'static str,
},
FidelityDegraded(usize),
EntryNotFound(String),
Unsupported(String),
CapabilityUnavailable {
format: FormatId,
available: &'static str,
requested: &'static str,
},
ChainTooDeep {
depth: usize,
},
NotAnArchive {
chain: String,
},
}Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
Io(Error)
Stuffr’s environment failed: a disk error, a permission failure, a broken pipe. Exit 1, which this project reserves for “stuffr itself failed” and nothing else.
Constructed through a hand-written From impl, not a derived one
— see below Error’s own impl block. The derived #[from] was
blind to whether the failing read sat above or below a codec decoder,
which made a bare ? below one wrong by default; four sites had it
wrong at once. crate::source::decode_side carries the whole
argument.
Usage(String)
FormatNotEnabled(FormatId)
UnknownFormat
AmbiguousFormat
NotSeekable
SpillLimitExceeded
ResourceLimit(String)
Corrupt(String)
UnsafePath
FidelityDegraded(usize)
EntryNotFound(String)
Unsupported(String)
ChainTooDeep
Nesting exceeded crate::probe::MAX_CHAIN_DEPTH. Names the bound so
the message is actionable rather than merely a refusal.
NotAnArchive
The chain resolved to raw bytes with no container to open — e.g.
list/test pointed at a plain codec stream, or at a file whose
magic and extension both name none. Names what the input actually
resolved to (chain.describe()), so “unpack this .gz” is actionable
rather than a bare refusal.
Implementations§
Source§impl Error
impl Error
Sourcepub fn entry_index_out_of_range(index: usize, entries: usize) -> Self
pub fn entry_index_out_of_range(index: usize, entries: usize) -> Self
“Entry number index does not exist; here is what does.”
One constructor rather than a format! at each raiser, because there
are two routes to an out-of-range index and they must report it
IDENTICALLY: a container’s own crate::ArchiveRead::by_index finds
it immediately (zip, which has a central directory to check against),
while a caller counting a forward walk finds it only at the end of the
archive (tar, ar, cpio, and anything on a pipe). A script that could
tell those apart from the message would be reading an implementation
detail of the source it was handed.
Self::EntryNotFound rather than Self::Usage because the variant
exists for exactly this — “you named an entry this archive does not
have” — and carries the same exit code, 2. The payload is shaped to
read inside that variant’s own entry \{0}` not found` wording.
Indices are 0-based throughout, so the valid range is stated
explicitly rather than left to be derived from the count: 0-5 is
harder to get wrong than “6 entries” is.
Sourcepub fn hardlink_not_storable(container: &str, name: &str) -> Self
pub fn hardlink_not_storable(container: &str, name: &str) -> Self
A container whose caps say !stores_hardlinks was handed an
EntryKind::Hardlink: refused (exit 3) before anything is written,
never stored as a regular file. plan_entry_write keeps links away
from such writers; this is each writer’s backstop, and the ONE wording.
Sourcepub fn exit_code(&self) -> i32
pub fn exit_code(&self) -> i32
Maps to the process exit code, per the spec’s table. Lives here rather than in the CLI so it is unit-testable without a process spawn.
§Exit 5 or exit 6 on an absurd header field: the rule the guards actually follow
Five of this workspace’s guards refuse a declared header field before it can do damage, and they split across two codes. The table is exhaustive of the five it names, NOT of every bounding check in the tree — see the known exception below it:
| guard | error | exit |
|---|---|---|
cpio.rs’s c_namesize ceiling | ResourceLimit | 6 |
ar.rs’s refuse_if_over (GNU name table, BSD identifier) | ResourceLimit | 6 |
ar.rs’s /N index past the declared name table | Corrupt | 5 |
zip.rs’s entry payload that never delivers its declared size | Corrupt | 5 |
zip.rs’s index that reaches no entry over a file that opens with one | Corrupt | 5 |
The rule is a fact about what stuffr DID, not about what the input declared:
- Exit 6 — stuffr declined to ask the allocator. The field was measured against a structural ceiling and refused before anything was sized from it. Nothing was read past the header.
- Exit 5 — stuffr read the bytes and they contradict each other. No allocation was ever in question; what is wrong is the file’s agreement with itself.
It is NOT “could a bigger machine honour it”. Three of those sites used to argue that locally, and their own reproducers falsify it: cpio’s is a 274-byte file declaring a 2.6 GiB name, which no machine anywhere can deliver, and it answers 6; ar’s two guards split across 5 and 6 on the same 68-byte file. Nothing about the input’s plausibility separates them, so a caller cannot predict the code from the file — only from which guard stopped first. Each site links here rather than re-deriving a local justification, because four local derivations produced four different rules for one contract.
One live pair does NOT follow this rule, and saying so here is the
point of writing the rule down. cpio.rs’s and zip.rs’s
read_symlink_target apply the SAME predicate against the SAME
constant — declared > MAX_SYMLINK_TARGET_LEN, 65,536 — and answer
differently: cpio ResourceLimit/6, zip Corrupt/5. By the rule above
both are 6: the field is measured against a structural ceiling and
refused before anything is sized from it. zip’s own message even reads
“refusing to allocate”, which is this rule’s definition of exit 6.
It is left inconsistent rather than fixed silently because changing it is a user-visible exit-code change and no test covers either site’s code today — an untested behaviour change is not something to slip in beside a documentation fix. Whoever aligns them should write that test first. Two earlier attempts to state this contract were each falsified by a live site; this is the third, and it names its own exception rather than waiting to be caught by a fourth reader.
What a caller can still read off the code: exit 6 says a LIMIT decided
the outcome, exit 5 says the FILE did and no budget changes that. The
container ceilings above are fixed rather than tunable (OpenOpts
carries no memory field, unlike DecodeOpts::memory_limit), so exit 6
there is not an invitation to retry with a bigger budget — it is still
the honest code, because a ceiling and not the bytes is what decided.
Sourcepub fn from_decode_io(e: Error) -> Self
pub fn from_decode_io(e: Error) -> Self
Classifies an io error that arose while decoding.
Malformed input becomes Error::Corrupt (exit 5); everything else
stays Error::Io (exit 1). InvalidData is the convention every
codec’s decoder is expected to normalise onto for malformed input; a
codec whose backend disagrees adapts at its own boundary rather than
this classification changing. gzip is one such codec: flate2’s
pure-Rust backend actually raises InvalidInput and UnexpectedEof,
never InvalidData, so gzip’s decoder wraps it in an adapter that
folds both onto InvalidData before this function ever sees them.
OutOfMemory becomes Error::ResourceLimit (exit 6), not
Corrupt: a codec whose dictionary allocation would exceed
DecodeOpts::memory_limit is refusing to allocate, not reporting a
damaged file, and exit 5 would actively mislead the caller about
which of those happened. A codec that raises this kind is expected to
have NOT routed it through the same InvalidData-folding adapter it
uses for malformed input, the same way InvalidData above is a
convention the codec opts into deliberately.
One rule here rather than a helper each codec calls: putting the
decision in nine places means the natural code — a bare ? on an
io::Error — silently bypasses it, and a convention whose failure mode
is invisible is not a convention. Conformance property 9 enforces the
codec half, that malformed input surfaces as InvalidData at all.
Lives in stuffr-core rather than in ops because Phase 2’s containers
need the identical classification on their own decode paths.
Task 4b (Phase 3a’s fuzzing harness) added a fourth call site:
probe.rs’s resolve_chain_deep_with re-probes a DECODED stream
whenever a codec has no container above it and the path named
nothing further (stdin, or an extension that ran out) — the loop
documented on resolve_chain_deep’s own doc comment. That re-probe
used to read through a bare ?, so a truncated zlib stream
(\x78\xda\x0a, three bytes) or an empty file named .lz reported
Error::Io — exit 1, “stuffr failed” — for input that is simply
corrupt, while stuffr cat on the IDENTICAL bytes (via
ops::decompress, whose decode read was already wired to this
function) correctly exited 5. This is deliberately NOT filed as a
sixth instance of exit_code’s _ => 1 wildcard misfiling a KNOWN
variant (see that match’s own comment for the five it has produced):
Error::Io reaching that wildcard is correct, by construction —
it has no dedicated arm because the wildcard IS its arm. The bug
here was upstream of exit_code entirely: the wrong variant got
constructed in the first place, at a read that passed through a
decoder without this classifier’s protection.
Trait Implementations§
Source§impl Error for Error
impl Error for Error
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<Error> for Error
The single conversion every ? on a read path passes through.
impl From<Error> for Error
The single conversion every ? on a read path passes through.
Hand-written rather than derived with thiserror’s #[from], because the
derived one could not see the one fact that decides the answer: whether the
read that failed sat BELOW a codec decoder (the archive’s fault) or above
one (stuffr’s environment failing). crate::source::mark_decode_side
records that at the read, and this is where it is spent.
Before this existed, the distinction was carried by whether the author of a
given ? remembered to write map_err(Error::from_decode_io) instead — so
the natural code was wrong by default, and four sites had it wrong at once
(spill.rs’s ladder rung 3, lha.rs’s Lha::open, cpio.rs’s per-header
read-ahead, zip.rs’s read_fixed/skip). A 21-byte zstd stream whose
decoded bytes opened with ARJ’s 60 EA magic reached the first of them and
reported i/o error — exit 1, “stuffr failed” — where stuffr cat on the
identical bytes correctly said the archive was corrupt.
Error::from_decode_io folds only InvalidData and OutOfMemory, so
this cannot mistake an environment failure for corruption even when one
propagates up through a decoder: it keeps its own kind, so it stays
Error::Io.