Errors and diagnostics
What gets raised, what gets recorded, and — if an archive turns out to be damaged — what you can still get out of it.
The exception tree
Every failure that comes from the archive or its environment derives from
ArchiveyError, so one except covers them all:
from archivey import open_archive, ArchiveyError
try:
with open_archive("maybe.7z") as reader:
reader.extract_all("out/")
except ArchiveyError as e:
print("could not process archive:", e)
React to specific cases with the subtypes:
| Exception | Raised when |
|---|---|
OpenError |
the source can't be opened — FormatDetectionError (unknown format), UnsupportedFormatError, StreamNotSeekableError (a pipe, where the format or the access mode needs seek) |
EncryptionError |
a password is required, missing, or wrong |
CorruptionError / TruncatedError |
the archive is malformed or cut short |
PackageNotInstalledError |
an optional package or tool is absent, or RARLAB unrar/rar is older than 6.0 (see Install) |
FilterRejectionError |
extraction blocked an unsafe member — PathTraversalError, SymlinkEscapeError, SpecialFileError |
NameCollisionError / NameRewrittenError |
raised only when you opted in with abort_on (see Safe extraction); without it, a collision or a portable-name rewrite is recorded in the result, not raised |
ResourceLimitError |
a listing/extraction safety limit (member count, size) was exceeded |
Mistakes in your code are deliberately kept out of that hierarchy: opening a second
overlapping stream without concurrent_members=True, using a closed reader, and similar
misuse raise ArchiveyUsageError (e.g.
ConcurrentAccessError), which is not an ArchiveyError — so a blanket
except ArchiveyError never silently swallows a bug. (When an archive genuinely can't
provide an operation — seeking a non-seekable member, a format that can't list — that is a
real ArchiveyError: UnsupportedOperationError.)
Diagnostics
Structured advisories are queryable on the reader and on the extraction report — not
only in logs. Prefer reader.diagnostics and the returned ExtractionReport over
hoping something appeared in a log handler. See the diagnostics capability and the
API reference.
Things that are said with a diagnostic rather than an exception
A handful of conditions are real enough to tell you about and not wrong enough to
refuse. Each has a DiagnosticCode you can match on, and any of them can be escalated
to an exception with a DiagnosticPolicy if your program would rather stop:
| Code | Means |
|---|---|
EMPTY_ARCHIVE |
The listing finished, with no error, and there were no members. Not an error: an empty tar is a real thing (tar cf empty.tar --files-from /dev/null), and it is byte-identical to a zero-filled junk file of the same size. |
EXTENSION_FORMAT_UNCONFIRMED |
The format came from the filename, nothing in the bytes confirmed it, and the listing came back empty. The classic shape is 32 KiB of zeros called z.tar. |
PROBE_FORMAT_UNCONFIRMED |
A single-file format came from a content probe with nothing corroborating it (no matching extension, no inner-TAR upgrade), and a later decode failed — at any detection confidence. The matching exception also has format_unconfirmed=True. Partial output may already have been produced. |
EXPLICIT_FORMAT_LISTED_EMPTY |
You passed format=, the listing came back empty, and detection disagrees. format= stays an override — wrong extensions are exactly what it is for — so this tells you rather than refusing. |
PASSWORD_ARGUMENT_UNUSED |
You passed password= to a format with no encryption. Passing a keyring across a batch of mixed archives is the intended use, so it is accepted and simply never consulted. |
ENCODING_ARGUMENT_UNUSED |
You passed encoding= to a backend that decodes names another way — 7z stores UTF-16, RAR decodes in its own parser, directory and single-file names come from the filesystem. |
MEMBER_NAME_BIDI_CONTROL |
A member name contains a Unicode bidi formatting control. The context names the exact codepoints, because an override (U+202A–202E, U+2066–2069 — how evilgnp.exe displays as a .png) is a different thing from a directional mark (U+061C, U+200E, U+200F), which appears in ordinary Arabic and Hebrew filenames. |
What is not here: per-member extraction outcomes
Extraction is the one operation that returns a structured per-item report, so
ExtractionReport.results is the sole record of what
happened to each member — blocked, failed, collided, renamed, rewritten. None of it is
also a diagnostic. A fact has exactly one authoritative channel, and when a return value
can carry it, the return value wins.
Practically: to find out what extraction did, read results, not
report.diagnostics. The summary still carries what was observed while reading the
archive during extraction (invalid timestamps, unresolvable symlinks, unverifiable
digests, stream rewinds) — those have no per-member result to live on.
Escalation is not lost with the codes: abort_on (see
Safe extraction) is the named opt-in for being stopped by a blocked
member, a collision, or a name rewrite.
Named policy presets
Hand-curating a disposition per code means re-reading the whole taxonomy every release. Two constructors do it for you:
from archivey import ArchiveyConfig, DiagnosticPolicy, ARCHIVE_INTEGRITY_CODES
config = ArchiveyConfig(diagnostic_policy=DiagnosticPolicy.strict())
DiagnosticPolicy.strict()raises onARCHIVE_INTEGRITY_CODES— the codes that report the archive's own bytes or metadata as anomalous — and collects the rest.DiagnosticPolicy.pedantic()raises on everything.
Five codes are deliberately outside the strict set: EMPTY_ARCHIVE (an empty archive is
legitimate), PASSWORD_ARGUMENT_UNUSED and ENCODING_ARGUMENT_UNUSED (argument
hygiene — a pipeline passing a password to every call would otherwise raise on every
unencrypted archive), EXPLICIT_FORMAT_LISTED_EMPTY (an override that halts you is not
an override), and STREAM_REWIND_REDECOMPRESSES (your access pattern, not the archive
— most useful as a targeted tripwire). ARCHIVE_INTEGRITY_CODES is exported, so you can
build your own policy from it.
New codes may appear in minor releases. A policy with default=RAISE is therefore
not version-stable: an upgrade can start raising on events your working program never
produced. strict(), whose membership is versioned alongside the taxonomy, is the
recommended strict mode. Removing a code stays a breaking change.
When an archive is damaged
The rest of this page is the detail behind "we raise rather than quietly returning wrong data". Most callers never need it; reach for it when you are recovering data from archives you do not control.
Listing a damaged archive
members() / scan_members() assert a complete listing (raise on terminal
archive damage). When you want the recoverable prefix and the error together, use
members_report():
with archivey.open_archive("messy.tar") as reader:
report = reader.members_report()
for member in report: # recovered members (may be a prefix)
print(member.name)
if report.error is not None: # incomplete listing
raise report.error
__iter__ / stream_members() yield the prefix then raise on the same failures.
Diagnostics alone are not the primary signal. This is not salvage (resync past damage);
--salvage remains reserved. Random-access extract still fail-closes before writing
when listing ends in terminal damage.
The integrity guarantee
Read a member to its end and Archivey checks it. Where the archive stores a
checksum or an authentication tag, a full read verifies it and raises if it does not
match. Stop early and nothing is checked. Errors always come from read(), never from
close() — so a finally block can't mask one.
"To its end" means read(-1), reading until read() returns b"", or — for a member
with a declared size — reading that many bytes.
What that does and does not promise:
- We try to raise on every error we can detect — not on every error. Some formats store no checksum at all, and some damage decodes into something that looks perfectly valid.
CorruptionErrorvsTruncatedErroris a best-effort guess, not a diagnosis. Damage that happens to decode into a shorter stream is indistinguishable from a genuine truncation. Don't branch on which one you got —except archivey.ReadErrorcatches both.- Bytes delivered before the error are of unknown quality. When a compressed member fails mid-stream, some of what you already read is probably fine — but we can't tell you which part, or how much. Treat the prefix as unverified: not known-good, not known-bad.
- A full-length return means the checksum matched. Trust it as far as you trust that digest.
- A short return with no exception does not mean "complete".
read(member.size)on a truncated member hands back what it has and stays quiet. Check the length — or just read again, because the next read raises.
That last point is what makes the ordinary chunked loop safe: it delivers every byte that was readable and then raises, rather than ending quietly on a short member. So the recoverable prefix and the error both reach you.
buf = bytearray()
try:
with reader.open("member.bin") as stream:
while chunk := stream.read(1 << 20):
buf.extend(chunk)
except archivey.ReadError:
... # buf holds everything that was readable; the member is damaged
If you need certainty regardless of how you read — partial reads, seeks, or "never
hand me unverified bytes" — VerificationMode.STRICT verifies a whole member before
returning any of it.
What each call does
For a member whose declared size is 500 bytes, truncated after 110:
| Call | Corrupt at full length | Truncated after 110 of 500 |
|---|---|---|
read(109) |
not yet at the end — no error | returns 109, no error |
read(110) |
not yet at the end — no error | returns 110, no error |
read(111) |
not yet at the end — no error | returns 110; the next read() raises |
read(member.size) |
raises CorruptionError |
returns 110 short, no exception |
read(-1) |
raises CorruptionError |
raises TruncatedError |
chunked until b"" |
raises on the chunk that reaches the size, and withholds it | delivers the prefix, then raises |
partial read, then close() |
quiet — you stopped early | quiet — you stopped early |
The one row worth remembering is read(member.size): it raises on corruption but
returns a short buffer on truncation. Known-wrong bytes are withheld; an apparently
incomplete prefix is handed over. So a short return from that call is a signal, not a
success — check the length.
Members with no declared size have nothing to read to, so read(n) can't
self-certify at all. Use read(-1) or read until b"".