API reference
This page lists archivey's public API, grouped by what you use it for. Each entry is headed by the
full name to import, such as archivey.open_archive or archivey.terminal.quoted, and each group
links to the page that explains it, where there is one.
Opening
See Opening an archive.
archivey.open_archive(source, *, format=None, streaming=False, seekable_members=False, concurrent_operations=False, password=None, encoding=None, config=None)
open_archive(
source: OpenSourceInput,
*,
format: ArchiveFormat | str | None = None,
streaming: Literal[False] = False,
seekable_members: bool = False,
concurrent_operations: bool = False,
password: PasswordInput = None,
encoding: str | None = None,
config: ArchiveyConfig | None = None,
) -> ArchiveReader
Open an archive for reading.
streaming=False (the default) opens for random access and fails fast at open
time on a non-seekable source. streaming=True promises forward-only, single-pass
access (works on any source, but disables random-access methods).
The return type follows the access mode: ArchiveReader for the
default streaming=False, and ForwardArchiveReader, which
lacks members(), get(), open() and read(), for streaming=True
or a flag whose value is known only at run time. A type checker then flags a
random-access call on a streaming reader. Every reader is an ArchiveReader at
run time; the difference is in the annotations only.
By default member streams are forward-only, one may be open at a time, and reader operations do not overlap. Two keyword flags opt into more, each unlocking one specific trap:
seekable_members=True—seek()on a member stream from randomopen()works. Without it,seek()raisesio.UnsupportedOperation. A backward seek may re-decompress from the start when there is no index or accelerator. Astream_members()stream never seeks, with or without this flag.concurrent_operations=True— operations on the reader may overlap, from one thread or several, on every format: randomopen()/read()/get()/members(), iteration,stream_members()passes andextract_all(). Each works as if it ran on a reader of its own. A single member stream object still has one owner, first-touch materialization is coordinated rather than parallel, andclose()waits for calls in flight. Overlap is correct, not always cheaper. Without it, a second overlappingopen(), or a random-access call or another pass beside a running pass, raisesArchiveyUsageError;close()beside a pass suspended at a yield closes the reader, and resuming the pass raises.
open_stream uses the same vocabulary for the single-stream case
(open_stream(..., seekable=True)); concurrency is meaningless there, so it has
no counterpart. Declared concurrency does not gate solid open-order cost — see
AccessCost / stream_members().
streaming=True combined with concurrent_operations=True is rejected
(ArchiveyUsageError): a forward-only pass cannot fan out.
config supplies library tuning knobs (accelerator modes, the diagnostic
policy, default extraction limits, and listing resource limits via
listing_limits). None selects the module default
DEFAULT_ARCHIVEY_CONFIG.
The format is auto-detected from the source's magic bytes (then its extension) unless
format= is passed explicitly. A directory path opens as a directory pseudo-archive.
A non-seekable stream keeps the bytes detection peeked in a replay prefix that the
backend's first reads drain, so detection never consumes bytes the backend needs.
A stream must be blocking: when a non-blocking one has nothing ready (its read
returns None), opening or reading raises BlockingIOError, not an archivey
error.
A seekable stream source is taken to hold the archive starting at its current
position: detection peeks from there and restores the position, and the opener
then rebases a mid-positioned stream to a zero origin so every backend sees the
archive begin at tell() == 0 (an archive embedded mid-file works uniformly,
without manual slicing).
encoding names the codec for member names whose encoding the archive does not
declare and whose bytes are not valid UTF-8 (ZIP, TAR, ISO and RAR 1.5-4). A name
whose bytes are valid UTF-8 is decoded as UTF-8 in every format, with or without
encoding, and encoding replaces only the format's fallback (cp437 or
ArchiveyConfig.zip_unflagged_fallback_encoding for ZIP, the host's code page for
RAR 1.5-4, surrogate escapes for TAR and ISO). member.raw_name keeps the stored
bytes. A ZIP comment without the UTF-8 flag (the archive comment always) and an
8-bit RAR 1.5-4 comment decode the same way as a name without a declared encoding,
and a byte the codec does not define survives as a lone surrogate. The one
difference is that a RAR comment's last fallback is always windows-1252. 7z,
directory and single-file sources decode names another way, ignore it, and record
ENCODING_ARGUMENT_UNUSED.
source may be an ordered sequence of paths or binary streams that together form
a multi-volume archive (7z concatenates volumes; RAR opens volume 1 and lets
unrar resolve siblings). A length-1 sequence is treated as a single source.
password accepts a single value, an ordered sequence of candidate passwords, or
a provider callable. List the most likely password first — especially for 7z, where
each wrong candidate pays an expensive key derivation.
With multiple candidates (or a provider), formats whose per-open password check is
weak may need a confirmation read before a candidate is accepted. For traditional
ZipCrypto this is usually cheap: compressed members are confirmed from a bounded
decompressed prefix. STORED ZipCrypto members are the niche exception — roughly
1/256 of wrong passwords pass the one-byte open check, and with no decompressor to
reject garbage the reader must scan the member once (CRC over every surviving
candidate in parallel) to decide. That full pass is rare in practice (multiple
passwords and a colliding wrong candidate and a STORED member) but can matter
for very large stored members. With a single password there is no confirmation
read, so a wrong ZipCrypto password that passes the one-byte check fails on the
member's own read, as an EncryptionError saying the password may be wrong or
the member corrupt.
A password supplied for a format with no encryption at all (TAR, ISO, a
directory, the single-file compressed streams) is accepted, not refused — it is
a resource offered, not a claim about this archive. A concrete password (a string,
bytes, or a list of them) is recorded as PASSWORD_ARGUMENT_UNUSED; a provider
callable is not, because it is only a way to ask for a password, and such a format
never asks. That is what lets a batch job pass one keyring at every archive. The
check is per format, made before any header is read: an unencrypted ZIP, 7z or RAR
records nothing, because those formats can use a password. Diagnostics also log
at WARNING by default, so a job passing a password list will log once per
archive of a format without encryption; silence it with
ArchiveyConfig(diagnostic_policy=DiagnosticPolicy(ignore={
DiagnosticCode.PASSWORD_ARGUMENT_UNUSED})), which keeps the count without the log
line.
Reader calls made after the open raise the same ReadError subclasses for damage,
a wrong password or a missing link target that shows only when a member is read.
Raises:
-
FormatDetectionError–No format matches the source's contents or name.
-
StreamNotSeekableError–The source cannot seek and
streaming=False, or the format needs to seek in either access mode (every format except TAR, ZIP and the single-file compressed streams). -
CorruptionError–The archive's header is damaged or cut short. A numbered split set with a missing part raises the subclass
TruncatedError. -
EncryptionError–The member list is encrypted (7z, RAR) and no password given opens it.
-
UnsupportedFeatureError–The format is recognized, but this archive uses a variant or feature archivey cannot read, or
config.formatsdoes not allow the format. -
PackageNotInstalledError–The format needs an optional package that is not installed, such as
pycdlibfor ISO. -
ResourceLimitError–The archive exceeds a limit that is checked at open, such as
ListingLimitsmax_memberswhile a ZIP, 7z, RAR or ISO index is parsed, orDecoderLimitsfor data the open decodes. -
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded while the archive is detected or opened. -
ArchiveyUsageError–One argument has a type or value the call cannot use (then also a
TypeErrororValueError), orstreaming=Trueis combined withconcurrent_operations=True. -
FileNotFoundError–The path does not exist. Other
OSErrorfrom reading the source is not wrapped either.
archivey.open_stream(source, *, format=None, seekable=False, config=None)
Open a single-file compressed stream and return a decompressing stream.
This is the compressed-streams entry point for a bare .gz / .bz2 / .xz /
… payload (no archive container). seekable is the same capability
open_archive() spells seekable_members; concurrency is not a concept here
— the call returns exactly one stream — so there is no counterpart to
concurrent_operations.
seekable=False (the default) returns a forward-only stream: seekable() is
False, seek() raises io.UnsupportedOperation, and no seek index or
accelerator is instantiated. Pass seekable=True to opt into the
seekable-decompressor-streams contract (native indexes, demand-driven accelerator
AUTO, loud slow rewinds on the non-accelerated path).
format accepts a StreamFormat, a raw-stream
ArchiveFormat (e.g. ArchiveFormat.GZ), a compressed-tar
ArchiveFormat (e.g. ArchiveFormat.TAR_GZ), or None to
auto-detect.
For a compressed tar (.tar.gz, .tar.xz, …), whether detected or passed as
format=ArchiveFormat.TAR_GZ and so on, the returned stream removes the
compression layer only and yields the tar bytes, as gzip.open does;
open_archive(open_stream(p), streaming=True) then lists the same members as
open_archive(p). Any other container (ZIP, 7z, RAR, ISO, an uncompressed tar)
has no compression layer to remove: detecting one raises
FormatDetectionError, and passing one as format= raises
ArchiveyUsageError. Use open_archive() for those.
A stream must be blocking: when a non-blocking one has nothing ready (its read
returns None), opening or reading raises BlockingIOError, not an archivey
error.
Reading the returned stream raises CorruptionError (or TruncatedError) for
damaged data, on the read that finds it.
Raises:
-
FormatDetectionError–No compression format matches the source, or the source is an archive container (ZIP, 7z, RAR, ISO, an uncompressed tar).
-
StreamNotSeekableError–seekable=Trueand the source cannot seek. -
ResourceLimitError–The stream asks a decoder for more memory than
DecoderLimitsallows, raised by the read of the returned stream that starts the decoder. -
UnsupportedFeatureError–config.formatsdoes not allow the format. -
PackageNotInstalledError–The format needs an optional package that is not installed.
-
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded while the stream is detected or opened. -
ArchiveyUsageError–One argument has a type or value the call cannot use (then also a
TypeErrororValueError): a directory path, or a container format passed asformat=. -
FileNotFoundError–The path does not exist.
archivey.detect_format(source, *, config=None, follow_stub_volumes=True)
Identify the archive format of source without fully opening it.
Returns a FormatInfo. Raises FormatDetectionError when no magic
pattern matches and no extension guess is available. A non-blocking stream with
nothing ready (its read returns None) raises BlockingIOError instead:
bytes that have not arrived are not the same as no bytes. Detection diagnostics
are on the returned FormatInfo.diagnostics, recorded into a standalone
collector made from config (or the library default).
What detection may read and decode is config.detection_budget
(ArchiveyConfig.detection_budget), BALANCED by default — the same
budget open_archive() detects under for that config.
LZMA Alone, zlib and Brotli have no magic. Their content probes run only when the
source's name ends in that format's extension, unless
ArchiveyConfig.always_probe_content is set; a nameless raw stream of these
formats raises FormatDetectionError.
config.formats (FormatPolicy) does not change the answer for
a built-in format. A plugin format it turns off is not detected: its magic,
extensions and content probe are left out, and unavailable_tiers records each
tier that left one out as NOT_ENABLED_BY_POLICY.
A stub-only .exe / .sfx (no archive magic) beside a 7-Zip split first
volume is detected as that volume's format when follow_stub_volumes is true
— the default, so detect_format("vol.exe") agrees with open_archive.
open_archive probes with this flag off, then switches the source itself.
The returned cost_receipt and unavailable_tiers then cover both passes, the
stub's and the volume's. Each pass runs under the full budget, and the receipt's
passes is 2.
A path to any part of a numbered split set (set.7z.002, set.zip.003) or to
a RAR continuation is detected on the set open_archive reads for it: the parts
joined in order, or RAR volume 1. A middle part alone has no magic at offset 0.
That resolution runs before detection reads a byte, and it can raise:
TruncatedError for a numbered set with a gap (set.zip.002 and
set.zip.003 with no set.zip.001), as open_archive does. A lone first
part (set.zip.001 with no other part) is detected as the format its bytes
show, where open_archive refuses it as an incomplete set.
A directory path returns ArchiveFormat.DIRECTORY with CERTAIN
confidence and detected_by="directory", matching open_archive, which reads
it as a directory archive. Nothing is read to decide that, so cost_receipt is
the zero receipt (one pass, no bytes).
Raises:
-
FormatDetectionError–No format matches the source's contents or name.
-
TruncatedError–The path names part of a numbered split set with a gap.
-
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded during detection. -
ArchiveyUsageError–One argument has a type or value the call cannot use (then also a
TypeErrororValueError). -
FileNotFoundError–The path does not exist.
archivey.FormatInfo
dataclass
The result of detect_format() — the detected format plus how sure
we are.
Fields:
-
(formatArchiveFormat) –The detected format.
-
(confidenceDetectionConfidence) –How far the evidence behind
formatgoes. -
(detected_byDetectedBy) –Which evidence decided:
"magic","extension","content_probe","sfx_scan"or"directory".An open set: a later release may add a detection step with a new value, so code that matches on it should handle a value it does not know.
"sfx_scan"covers any archive found behind a prefix, a#!launcher included, not only a self-extractor's executable stub. -
(payload_offsetint, default:0) –Where the archive starts in the source. Nonzero only for a self-extracting archive, so
payload_offset > 0is the test for one. -
(diagnosticsDiagnosticSummary, default:DiagnosticSummary(total_count=0, counts=mappingproxy({}), retained=(), dropped_count=0)) –What detection reported on its way to the answer.
-
(corroboratedbool, default:False) –Provisional and informational, not part of the
detect_format()contract: whether a matching extension, an inner-TAR upgrade, or a self-checking header (a TAR header checksum) corroborated a content-probe claim.Read it only together with
detected_by == "content_probe".Falsealso means "not a probe at all", so an exact magic hit readsFalsetoo. A later release may replace this field with a record of the evidence that separates the two cases. -
(cost_receiptDetectionCostReceipt | None, default:None) –The work detection did, as a
DetectionCostReceipt: bytes read and scanned, decode input and output, and the number of passes. It covers the whole call, both passes whendetect_format()followed a stub to its split volume.detect_format()always fills it; a directory gets the zero receipt.Noneonly on aFormatInfothat detection did not produce. -
(unavailable_tierstuple[TierSkip, ...], default:()) –The detection tiers that did not run, each a
TierSkipwith itsTierSkipReason: not enabled by policy, a capability the source lacks, or the budget ran out. Empty when every tier ran. -
(from_pluginbool) –(computed property) Whether
formatis one a plugin added (archivey.formats), not a built-in one. A plugin's parser is not covered by archivey's limits, so a caller that reads untrusted input can refuse such a format.
archivey.DetectionConfidence
Bases: Enum
How much detect_format() trusts the format it reports.
Provisional in 0.2.x: which grade a detection step reports may change in a later
release (a two-byte magic reported below CERTAIN, for instance). Branch on the
format, not on the grade.
Members:
-
CERTAIN('certain') –An exact magic-byte match at the expected offset.
-
PROBABLE('probable') –A structural or content probe matched: the inner-TAR probe, the SFX scan, or a content probe hit (LZMA-alone, zlib, or Brotli whose first meta-block is compressed or whose extension agrees).
-
GUESS('guess') –No confirmation strong enough to rely on: an extension-only guess, or a content probe hit in the weak evidence class (today: extensionless Brotli whose first meta-block is uncompressed or metadata).
archivey.format_availability(fmt, *, check_programs=True)
Public query: the tri-state support level of fmt and its missing components.
fmt is an ArchiveFormat — the (container, stream) pair —
or that format spelled as a string, such as "zip" or "tar.gz". Anything
else, a StreamFormat or a ContainerFormat
included, raises ArchiveyUsageError rather than answering: each
is half of the pair, not a format.
RAR is PARTIAL, with unrar in missing, when no RARLAB unrar or
rar 6.0 or later is on PATH: it still opens and lists, and stored members
read. To find out, the first call runs each candidate once to read its banner, for
at most 10 seconds each; the answer is cached per binary.
It describes the default rar_decompressor (AUTO) and does not change with
ArchiveyConfig. check_programs=False runs no program: missing programs are
then not counted, and RAR reports as it would with unrar installed.
archivey.FormatAvailability
dataclass
The support level of a format plus the components needed to raise it.
Fields:
-
(formatArchiveFormat) –The format this answer is about.
-
(supportFormatSupport) –How much of the format can be read.
-
(missingtuple[MissingComponent, ...], default:()) –What to install to read more of the format. Empty when
supportisFULL. -
(required_sourceStreamCapability, default:StreamCapability.SEEKABLE) –The weakest source shape this format can be read from.
StreamCapabilityis ordered, so this is a minimum: a source is strong enough whenavailability.required_source <= reader.cost.stream_capability. It exists so that "can I pipe this straight in, or must I buffer it to disk first?" is a query rather than aStreamNotSeekableErrorto catch.Independent of
support— a format whose optional dependency is missing still answers the question. TheSEEKABLEdefault is the conservative answer, and is what a format with no registered backend at all reports.
archivey.FormatSupport
Bases: Enum
How much of a known format this installation can read.
Members:
-
FULL('full') –The format can be read, with every optional codec and program it can use.
-
PARTIAL('partial') –Archives open and list, and members in the common methods read, but an optional codec or program is missing.
-
NONE('none') –The format cannot be read: its reader is missing, or for a single-file format, its only codec.
archivey.MissingComponent
dataclass
A package, extra, or external tool required for a format (or a codec inside it).
Appears on FormatAvailability when something is absent, and as
the requirement on internal codec/backend descriptors. Defined in this leaf
module (not the registry) so codec descriptors can reference it without a
registry ↔ codecs import cycle.
Fields:
-
(namestr) –The package or program, such as
"pycdlib","brotli"or"unrar". -
(install_hintstr) –For a package, the command that installs it, such as
"pip install archivey[recommended]". For a program such asunrar, the whole message a read raises without it. -
(unlockstuple[str, ...], default:()) –The codecs or features it enables, such as
("ppmd",). -
(reasonstr, default:'') –Why archivey does not use the package on this interpreter, where installing it would not help, such as
"archivey does not use lz4 under PyPy, where its frame decoder crashes the interpreter". Empty when installing it is the fix. -
(advicestr) –(computed property) What a report of this component says to do:
reasonwhen set, otherwiseinstall_hint.
message(purpose, *, note='')
Text for the PackageNotInstalledError raised when this component is absent.
Every raise site builds its message here so the advice a caller gets when a
read fails cannot drift from the install_hint that listing and
FormatAvailability report. The two used to be independent
strings, and the extras consolidation updated the hints while every raise
kept advertising extras that no longer exist.
note appends a sentence for cases where the bare hint would mislead (e.g.
a backport that is pointless on a Python version shipping the module).
When reason is set it replaces the install advice, which would not help.
For package entries only: a program entry's install_hint is already a
complete message.
archivey.list_supported_formats()
Public query: formats readable now (support FULL or PARTIAL).
archivey.list_known_formats()
Public query: every format the registry knows, including support NONE.
Readers and streams
See Choosing how to read, in particular Reading once.
open_archive returns an ArchiveReader unless you pass streaming=True. Then its return type is
ForwardArchiveReader, which leaves out members, get, read and open, so a type checker
flags calls to them. The object is still an ArchiveReader at run time, and those calls raise
ArchiveyUsageError. stream_members() is the way to read members.
archivey.ForwardArchiveReader
Bases: ABC
An open archive that the code reads in one forward pass.
It has every reader method except members(),
get(), open() and
read(), which need random access. Iterate it, call
stream_members() or extract_all(), or call members_report() for
the member list.
open_archive(..., streaming=True) returns this type, so a type checker flags
those four calls, which would raise ArchiveyUsageError at run time. The default
open_archive() returns ArchiveReader, which adds the four, so a
function annotated with ForwardArchiveReader accepts a reader of either mode.
The check is static only: every reader is an ArchiveReader at run time. A
second pass on a streaming reader still raises, and no type checker can see that.
Use in a with block.
Listing APIs (easy to mix up):
members()— complete list or raise; random-access only.members_report()— either mode. Returns a report on archive damage (checkerror is Nonefor completeness); raises on limits and usage errors. On a streaming reader it uses up the forward pass (starting it, or finishing an interrupted one). For complete-or-raise there, raisereport.erroryourself.members_report_if_available()— never scans;Noneif not yet cached.
Parameters:
-
format(ArchiveFormat) –(computed property) The detected
(container, stream)format of the open archive. -
format_info(FormatInfo | None) –(computed property) How
archivey.open_archive()identified the format, asarchivey.detect_format()would report it.The result of the detection the open ran, kept rather than repeated: its
confidence,detected_byandpayload_offsetdescribe this reader's source. A directory reportsDIRECTORY/CERTAIN/"directory".Nonewhenformat=was passed, so no detection ran. -
info(ArchiveInfo) –(computed property) Archive-level metadata (format, solidity, counts, encryption, cost).
-
cost(CostReceipt) –(computed property) The listing/access cost receipt for this archive (see
access-mode-and-cost). -
diagnostics(DiagnosticSummary) –(computed property) Fresh immutable cumulative snapshot of diagnostics for this reader.
__contains__(member)
abstractmethod
Whether member (an ArchiveMember) was yielded by this reader.
Identity-based and O(1) — no scan — so it is valid in any access mode; useful to
disambiguate members when several readers are in play. Name lookup is
get(), and a non-ArchiveMember operand raises
TypeError (this also keeps the in operator from silently falling back to
a full iteration, which would consume a streaming reader's single forward
pass).
__iter__()
abstractmethod
Iterate members in archive order (served from cache once materialized).
Returns a MemberPass of members. On a streaming reader it is the
reader's one pass and holds the reader while paused, as a
stream_members() pass does; close it, or use it in a with block, to
end it early. On a random-access reader it holds nothing between members.
close()
abstractmethod
Release resources held by the reader. Idempotent.
Member streams do not outlive the reader. Any still open when close()
runs are closed with it, in the order they were opened, the same way
zipfile.ZipFile.close() and tarfile.TarFile.close() behave — reading one
afterwards fails as it would for any closed file, and closing it again is a
no-op. The archive's own source is released after the last of them, never
underneath a stream still reading through it.
A MemberPass, from stream_members() or from iterating a
streaming reader, that is still alive does not stop the close: the reader
closes, and advancing that pass afterwards raises ArchiveyUsageError.
Using the reader itself after close() raises ArchiveyUsageError.
extract_all(dest, *, members=None, filter=None, policy=ExtractionPolicy.STRICT, overwrite=OverwritePolicy.ERROR, on_error=OnError.STOP, abort_on=(), on_progress=None, limits=None, dry_run=False)
abstractmethod
Extract members to dest (safe-by-default; see safe-extraction).
members selects which members to extract: any iterable of names or
ArchiveMembers (read once, so a generator works), a predicate, or None
for all. filter runs after the policy transform and before the safety
checks, so it sees unsafe members too; it may rename/sanitize a member (return
a .replace()d copy) or skip it (return None), and the safety checks run
on what it returns. The call runs under the config the reader was opened with;
limits overrides its extraction limits for this call only. Returns an ExtractionReport whose diagnostic
summary is the delta for this extraction call.
abort_on names events that end the whole call the first time they occur —
raising instead of returning a report. It is independent of on_error: see
AbortOn.
dry_run=True runs the same extraction into a private scratch directory and
removes it afterwards. Every check runs and every member body is read and
verified, but files are created empty and nothing is written under dest.
What dest holds already is read, and met as a real run meets it: the
overwrite policy applies to it, and a file or symlink in the way is
refused, skipped, replaced or renamed around in the same way. The report reads
as the report of a real extraction into the same dest would, with paths
under dest.
A member refused by a safety check is skipped and reported as BLOCKED; it
raises only when abort_on names it.
Raises:
-
ExtractionError–Under
on_error="stop"(the default), a member could not be written, such as a file already at its destination underoverwrite="error". Its subclassesFilterRejectionError,NameCollisionErrorandNameRewrittenErrorare raised for the eventsabort_onnames, whateveron_errorsays. -
ReadError–Under
on_error="stop", a member's data could not be read: aCorruptionErrorfor damage, anEncryptionErrorfor a missing or wrong password, aLinkTargetNotFoundError. -
ResourceLimitError–The extraction exceeds
ExtractionLimits: a total stops the extraction whateveron_errorsays, and a single member overmax_ratiofails on its own, underon_error. A member that asks a decoder for more memory thanDecoderLimitsallows fails on its own too. -
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, whateveron_errorsays. -
ArchiveyUsageError–One argument has a type or value the call cannot use (then also a
TypeErrororValueError), or the reader is closed. -
OSError–destcannot be created or written to.
members_report()
abstractmethod
Materialize the member listing and return a report.
report.error is None means report.members is complete. A non-None
error is terminal archive damage found while listing (a
CorruptionError, including
TruncatedError): the tuple is the recovered
prefix. Only that damage is returned instead of raised. Every other failure
raises, as it does from members(): a
ResourceLimitError when the listing exceeds
ListingLimits, ArchiveyUsageError
on misuse (closed reader, overlapping call), and other read failures (a password
error, an escalated diagnostic, a pass that failed earlier, an I/O error).
Random access: does not consume anything; the members are the ones
members() returns. Streaming: runs the single forward pass
to its end (from the start, or finishing one interrupted by an early
break), so no later pass is possible; after a completed pass it returns the
cached report.
A stream_members() or for member in reader pass enforces
ListingLimits too: it raises the same ResourceLimitError at the member
that crosses a limit.
For a complete-or-raise list on a streaming reader::
report = reader.members_report()
if report.error is not None:
raise report.error
members_report_if_available()
abstractmethod
A member-list report if available without scanning, else None.
Never scans or consumes the forward pass, so it is safe to call on any reader
(including a streaming one).
stream_members(members=None, *, file_copy_streams=True)
abstractmethod
Yield (member, stream) pairs in archive order with bounded memory.
members is an optional selector: a predicate, any iterable of names or
members (read once, so a generator works), or None for all. The yielded
stream is valid only until the iterator advances.
The stream is None for every member that has no data of its own: links,
special entries, tombstones and most directories. A directory that declares
data (MEMBER_DIRECTORY_DATA_IGNORED, in ZIP, TAR and RAR) gets a stream
with the bytes open() delivers for it, under the same
checks, so a forward-only reader reaches them too. The exception is a ZIP
directory written with a data descriptor, read from a source that cannot
seek: its size comes after its data, so it gets None. Data archivey
cannot decode is refused on the first read, as open() refuses it. Test
is_file, not stream is not None, to tell a file from such a
directory.
The returned MemberPass can be closed, or used in a with block, to
end the pass early; see there for when that matters.
The pass enforces ListingLimits as members() does. A pass that lists
as it goes (every streaming reader, and TAR) yields the members before the
limit and then raises ResourceLimitError at the member that crosses it;
the other readers list the whole archive first, so they raise before the first
yield. An archive with more members than the default max_members needs a
higher limit, or ListingLimits.UNLIMITED, to be streamed; the reader then
keeps every member until it closes.
A member whose data is damaged raises from its own stream read and does not end the pass; on a solid archive the later members of its block fail the same way. The pass raises only when damage to the archive's structure stops it finding the next member, such as a TAR cut inside a member.
file_copy_streams=False also yields None for a file that the archive
stores as a copy of an earlier member (extra["is_file_copy"], a RAR5 file
copy). Its bytes and digests are its source's, link_target_member, so a
caller that records hashes can reuse the source's. A solid RAR pass then keeps
nothing for the copies; by default it keeps each source until the pass ends,
in memory or in a temporary file within SpoolLimits.max_bytes.
The yielded stream is forward-only on every format, with or without
seekable_members=True: seekable() is False, seek() raises
io.UnsupportedOperation, and tell() works. A nested archive read from
it is a non-seekable source: open_archive() accepts it only with
streaming=True and only for TAR, ZIP and single-file compressors. For a member
you need to seek, nested archives included, use open() under
seekable_members=True.
On a default reader the pass owns the reader while it runs:
open(), read(),
get(), members(),
members_report(), iteration and another pass raise
ArchiveyUsageError inside the loop. On a reader opened with
concurrent_operations=True they all work, from the loop body or from
another thread, on every format, so the answer does not depend on the
format. They share the pass's walk of the archive's headers, and a call that
needs members the pass has not reached yet reads ahead for it.
extract_all() works beside a pass too.
Raises:
-
ArchiveyUsageError–The reader is closed; it is a streaming reader whose one pass has already started; or, without
concurrent_operations=True, another pass or a random-access call is running. -
ResourceLimitError–The listing exceeds
ListingLimits. -
CorruptionError–Damage to the archive's structure stops the pass from finding the next member.
-
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded while the pass lists the archive.
Errors in a member's data raise from that member's stream, not from the pass:
CorruptionError for damage,
EncryptionError, UnsupportedFeatureError
and PackageNotInstalledError for data archivey cannot decode,
and ResourceLimitError for a member that asks a decoder for
more memory than DecoderLimits allows.
archivey.ArchiveReader
Bases: ForwardArchiveReader
The public, read-only interface to an archive opened for random access.
Returned by archivey.open_archive() with the default streaming=False.
It has every ForwardArchiveReader method and adds four for lookup and
random member access: members(), get(), open() and
read(). Annotate against this type when the code needs those methods;
concrete machinery lives in the internal BaseArchiveReader helper. Use in a
with block.
get(name, default=None)
abstractmethod
Look up a member by its normalized name, returning default if absent.
This is the name-lookup entry point; open()/read() also accept a
name directly. May trigger a scan; on a streaming reader raises
ArchiveyUsageError, as does a name that is not a str (a bytes
name or an ArchiveMember). With duplicate member names, returns the last
(the one a sequential extraction would leave on disk).
Raises:
-
ArchiveyUsageError–The reader is closed or streaming, or
nameis not astr. -
CorruptionError–The scan finds the member list damaged, as
members()raises it. -
ResourceLimitError–The listing exceeds
ListingLimits. -
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded while the archive is listed.
members()
abstractmethod
All members as a list. May trigger a scan; raises ArchiveyUsageError
on a streaming reader (there, use members_report(),
which uses up the pass, or iterate stream_members()
and ignore the streams; both apply ListingLimits).
Raises terminal archive-level listing errors instead of returning an
incomplete list.
Raises:
-
CorruptionError–The archive is damaged partway through its member list (
members_report()returns the members found before the damage instead). -
ResourceLimitError–The listing exceeds
ListingLimits. -
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on, recorded while the archive is listed. -
ArchiveyUsageError–The reader is closed or streaming.
open(member)
abstractmethod
Open a member as a binary stream, following symlinks/hardlinks. Accepts a
member object or a name (an unknown name raises KeyError; a member object
that was not yielded by this reader raises ArchiveyUsageError — same identity
rule as member in reader). The caller is responsible for closing the returned
stream. Returns an ArchiveStream (usable as BinaryIO).
Cost, when reader.cost.access_cost is SOLID (solid 7z/RAR, any
compressed tar): members share one compression run, so opening one decodes
every member before it. Doing that for each member in turn is quadratic in the
archive size. On a 7z or RAR reader, the first call that decodes again what an
earlier one decoded logs one INFO record per reader on the
archivey.backends logger saying so, and a compressed tar's backward seeks
report STREAM_REWIND_REDECOMPRESSES. Prefer
stream_members(), which decodes the run once.
Raises:
-
KeyError–No member has that name.
-
ArchiveyUsageError–The member has no data of its own (a directory whose header declares none, a tombstone, a special entry); the member object came from another reader; another member stream is open and the reader was opened without
concurrent_operations=True; or the reader is closed or streaming. -
LinkTargetNotFoundError–The member is a link whose target is not a member of the archive.
-
EncryptionError–The member is encrypted and no password given decrypts it.
-
CorruptionError–The member's data is damaged, raised by
open()or by the read of the returned stream that finds the damage (TruncatedErrorwhen it seems to end early). -
UnsupportedFeatureError–The member uses a compression method or feature archivey cannot decode.
-
PackageNotInstalledError–Decoding the member needs an optional package that is not installed.
-
ResourceLimitError–The member asks a decoder for more memory than
DecoderLimitsallows, or a scan for the name exceedsListingLimits. -
DiagnosticRaisedError–A diagnostic that the
DiagnosticPolicyin the config raises on.
read(member)
abstractmethod
Read a member's full contents as bytes (unbounded — prefer open()
or stream_members() for anything not known to be small).
Carries open()'s solid-archive cost: on a SOLID archive this decodes
every member preceding the requested one, so a loop over all members is
quadratic. Use stream_members() for that.
Raises:
-
KeyError–No member has that name.
-
ArchiveyUsageError–As
open()raises it. -
ArchiveyError–As
open()and reading its stream raise it:LinkTargetNotFoundError,EncryptionError,CorruptionError,UnsupportedFeatureError,PackageNotInstalledError,ResourceLimitErrororDiagnosticRaisedError.
archivey.ArchiveStream
Bases: ReadOnlyIOStream
A member's bytes, as a read-only binary file.
open(), stream_members()
and open_stream() return one; there is no need to build one yourself.
It is an io.RawIOBase, and seekable() says whether it can seek.
Parameters:
-
name(str) –(computed property) The member's name inside its archive, or raise
AttributeError.A member stream carries the name its archive gives it, as
zipfile'sZipExtFiledoes, soopen_archive(reader.open(member))can match the member's extension during detection (.tar.brand other formats found only by name). It is a member name, not a filesystem path. A stream with no member (open_stream) has no name and raises, asio.BytesIOdoes. -
diagnostics(DiagnosticSummary) –(computed property) Diagnostic snapshot for events this stream's own calls emitted, or empty.
-
size(int | None) –(computed property) Total decompressed byte length when cheaply known, else
None.A member stream knows its size from the archive's metadata. A stream from
open_stream()reports it when the format stores it where it can be read without decompressing, andNoneotherwise: a BGZF file reportsNoneuntil a seek or a read finds its size. A stream fromstream_members()that has not opened yet reportsNonerather than opening itself just to answer. -
verified(Verification) –(computed property) How much of this stream's data has been checked so far, as a
Verification.KEYonce the stream has opened its member and nothing doubts its password,STRUCTUREonce a read has reached the end with the length checks passing, andCONTENTonce a checksum stored with the data has matched every byte. It never goes down: a seek back to the start runs the checks again, and what they proved stays proved. A read that raises leaves it where it was. A stream fromstream_members()opens its member at its first read, and isNONEuntil then.Compare it with
verifiable: a TAR member has no checksum, soSTRUCTUREis the most it can reach. Reading this property reads nothing from the archive. -
verifiable(Verification) –(computed property) The most
verifiedcan reach by a read from the start to the end.CONTENTwhen the member carries a checksum archivey can compute,STRUCTUREwhen it carries none, andNONEfor an encrypted member with no checksum whose password passed only a weak check. It rises when another member's checksum confirms that password. A stream fromstream_members()learns what its member carries when it opens, at its first read. Until then it answers from what it knows without opening (the member's stored digest), and the answer can settle lower when it opens: a password watch that only appears then takes a member with no checksum fromSTRUCTUREtoNONE. Once open, it never falls. Reading this property reads nothing from the archive.
close()
Close the stream and release what it holds in the reader. Closing it again does nothing.
nearest_resume_offset(target)
abstractmethod
The offset, at or before target, that a seek to target decodes from.
None when the stream cannot say. Which offsets a stream can resume from
depends on the decoder, so the answer for the same member can change between
releases.
read(n=-1)
abstractmethod
Read up to n bytes of the member; -1 or None reads to EOF.
A stream that checks the member's length caps each read at the bytes left while
it is still checking. Otherwise n goes to the decoder as given, so what a
very large n costs is the decoder's: a TAR member's reader allocates a
buffer of n bytes, and read(2**62) raises MemoryError there, as
open(path, "rb").read(2**62) does on a plain file. The stream is still
usable afterwards.
read_ahead(need, n)
abstractmethod
At least need bytes, or fewer only at the end, and at most n.
For a reader's own buffer over a codec stream (the TAR walk on a source that
cannot seek): the inner's read_ahead gives what the feed that completes
need decoded, so the buffer does not wait for bytes nobody asked for yet.
Errors are those of read(need). A stream that checks its bytes (a fused
verifier) or has a verdict to repeat answers with read(n).
seek(offset, whence=io.SEEK_SET)
abstractmethod
Move to offset, as on any io stream; raises when it can't seek.
Moving backwards in compressed data can mean decompressing again from the
nearest place the decoder can resume from, which
nearest_resume_offset() reports.
seekable()
abstractmethod
Whether seek() works on this stream.
False for a stream from stream_members(),
and for one from open() unless the archive was
opened with seekable_members=True.
tell()
abstractmethod
The current position in the member's decompressed bytes.
archivey.Verification
Bases: IntEnum
How much of a member stream's data has been checked, from least to most.
ArchiveStream.verified is the level a stream has reached, and
ArchiveStream.verifiable the most it can reach by a read from the start to
the end. The levels are ordered, so stream.verified >= Verification.STRUCTURE
reads as "at least the structure was checked".
Members:
-
NONE(0) –Nothing is checked yet. The stream has not opened its member, or the member's password passed only a check that a wrong password can pass by chance, such as the one-byte check of old ZIP encryption.
-
KEY(1) –The bytes come from the right key: the member is not encrypted, or its password passed a check a wrong password cannot pass by chance.
-
STRUCTURE(2) –The right key, and a read reached the end with the length and every end-of-data check passing.
-
CONTENT(3) –The structure, and a checksum stored with the data matched every byte: the member's CRC-32 or BLAKE2sp, the WinZip AES authentication code, or the checksum of a gzip, bzip2, lzip or zlib stream read from its start.
archivey.MemberPass
Bases: Iterator[_T_co]
A pass over an archive's members: an iterator that can also be closed, directly
or by a with block.
stream_members() returns one that yields
(member, stream) pairs, and iterating the reader returns one that yields
members. Iterate it as any iterator. Closing it ends the pass early and releases
the reader: the current member stream, if any, is closed. Closing does not give a
streaming reader a second pass: iterating it, stream_members()
and extract_all() still raise ArchiveyUsageError,
and members_report() finishes the interrupted walk.
Closing an ended pass does nothing, and advancing a closed one ends the iteration.
Closing a pass while it is executing (another thread inside next(), or a
selector closing its own pass) raises ArchiveyUsageError and leaves the pass
running.
A pass that runs to its end ends on its own, and so, on CPython, does one a
for loop leaves with break. Otherwise a pass stays alive while anything
refers to it: a variable, the target of with … as, or a traceback that holds
the loop's frame while an error is handled. It also outlives the loop on an
interpreter that does not free it at once, such as PyPy. Close it yourself in
those cases. While a paused stream_members() pass is
alive, a default reader refuses its other calls; a pass from iterating a
random-access reader holds nothing between members::
with reader.stream_members() as members:
for member, stream in members:
if member.is_file and stream is not None:
process(member, stream.read())
close()
abstractmethod
End the pass: close the current member stream and release the reader.
Raises ArchiveyUsageError while the pass is executing.
archivey.MemberSelector = MemberSelectorArg
module-attribute
Which members stream_members() and
extract_all() act on. One of:
- an iterable of member names (
str) andArchiveMemberobjects, read once, so a generator works. A name selects every member with exactly that name (a directory is"dir/"); anArchiveMemberselects that member only, by identity. A barestris refused: pass["name"]for one member; - a predicate
Callable[[ArchiveMember], bool]that returnsTruefor the members to select; None, for all members.
archivey.MemberListReport
dataclass
Immutable member-list outcome: recovered members plus listing honesty.
error is None means members is a complete archive listing. A non-None
error means the tuple is the recovered prefix and the error is the terminal
archive-level damage that stopped listing.
Like ExtractionReport, the report iterates, indexes, and sizes as its
primary sequence so common for member in report / len(report) idioms work.
Fields:
-
(memberstuple[ArchiveMember, ...]) –The members listed, all of them when
errorisNone. -
(errorArchiveyError | None) –The damage that stopped the listing, or
Nonewhen it completed. -
(diagnosticsDiagnosticSummary) –The diagnostics emitted while listing.
Members and archive details
See Open and list and Members with the same name.
archivey.ArchiveMember
dataclass
One archive entry.
Mutable on purpose: backends fill late-bound fields in place after the member
is first constructed (link_target_member, digests, attached diagnostics).
Callers must treat instances as read-only — use replace() for edits.
Fields:
-
(typeMemberType) –What kind of entry this is (file, directory, symlink, …).
-
(namestr) –Normalized member path,
/-separated, decoded for display and lookup. -
(raw_namebytes | None, default:None) –The member name exactly as stored in the archive, undecoded.
Nonewhen the format has no name field for this member or the bytes cannot be recovered from the decoded name;b""when the format has the field and the member left it empty (7z).nameisraw_namedecoded and normalized, then put through the format's own naming rules. Where the archive stores the name twice,raw_nameis the spelling the name was decoded from: a ZIP name taken from its Info-ZIP Unicode Path extra field (0x7075) has that field's UTF-8 bytes here, and the header's bytes inextra["alternate_raw_name"].The naming rules that make
namediffer fromraw_namedecoded:- ISO: in a plain ISO 9660 image,
namedrops the;Nversion suffix (and the.before an empty extension) thatraw_namekeeps; the version is inextra["iso.version"]. A superseded version keeps its stored spelling. A Rock Ridge name that is not UTF-8 and takes its Joliet name keeps the Rock Ridge bytes here. - A gzip file's member is named after the archive source (
"data"when the source has no file name);raw_nameholds the original file name from the gzip header (FNAME), andextra["gzip.original_filename"]holds those bytes decoded as Latin-1. - A 7z member stored with no name is named after the archive source, and
raw_nameisb"". - A RAR5 file-version-history member gets a
;nsuffix inname(as WinRAR andunrarshow it);raw_nameis the stored name without it.
- ISO: in a plain ISO 9660 image,
-
(sizeint | None, default:None) –Uncompressed size in bytes, or
Noneif unknown (e.g. a streaming entry).A single-file member (
.gz,.bz2, …) whose format records no size gets one from a read: a read from the start to the end that skips no byte, which the decoder finishes without error sets it to the bytes it returned. A large member of a macOS ZIP, whose size the archive stores only modulo 2**32, is listed withNoneand filled the same way, by a read whose length and CRC-32 check out. Until thenmember_state_finalisFalse. -
(compressed_sizeint | None, default:None) –Compressed size in bytes, or
Noneif unknown. -
(modifieddatetime | None, default:None) –Last-modified time, if recorded.
-
(accesseddatetime | None, default:None) –Last-access time, if recorded.
-
(createddatetime | None, default:None) –The birth (creation) time, if recorded (rare; most formats store only mtime).
Never Unix
st_ctime(inode change), in any format. Where a writer storesst_ctime, or may, that time isctimeinstead: a creation slot filled on Unix (a RAR or 7z made there, a ZIP from any host but FAT, OS/2, NTFS or VFAT, an unknown host too) and the Rock Ridge and PAX change times. A TAR getscreatedonly from libarchive's PAXLIBARCHIVE.creationtime. Directory listing usesst_birthtimeonly. -
(ctimedatetime | None, default:None) –The Unix inode change time (
st_ctime), if recorded, or a time that may be it.From an archive: the Rock Ridge attribute-change time (ISO), the PAX
ctimerecord (TAR), or a creation slot whose writer fills it fromst_ctimeor cannot be shown not to (RAR, 7z, ZIP; seecreated). It is what the archive stored, so a Windows writer can put a birth time here: libarchive on Windows fills the PAXctimewith it and stamps its ZIP and 7z members as Unix. A format with a single creation slot (RAR, 7z, ZIP) fills at most one ofcreatedandctime. Rock Ridge and libarchive's PAX records store both times separately and so can fill both, as can a directory listing.member.created or member.ctimeprefers the birth time when there is one. A directory listing reportsst_ctimeon every OS but Windows, where Python'sst_ctimewas the creation time before 3.12.Extraction cannot restore it: every OS sets a file's
st_ctimeitself. -
(modeint | None, default:None) –Unix permission bits, or
Noneif the format/entry carries no mode. -
(uidint | None, default:None) –Owner user id, if recorded.
-
(gidint | None, default:None) –Owner group id, if recorded.
-
(unamestr | None, default:None) –Owner user name, if recorded.
-
(gnamestr | None, default:None) –Owner group name, if recorded.
-
(link_targetstr | None, default:None) –For a symlink/hardlink, the raw target path string as stored. For a
FILEthat is a stored copy of an earlier member (extra["is_file_copy"]), the source's path as stored. -
(link_target_memberArchiveMember | None, default:None) –For a link, the resolved target member within this archive, if found. For a
FILEthat is a stored copy (extra["is_file_copy"]), the member whose bytes it repeats. -
(compressiontuple[CompressionMethod, ...], default:tuple()) –Codec chain in compress order — pre-filters first, packing codec last.
-
(is_encryptedbool, default:False) –Whether this member's data is encrypted.
Truealso when the backend could not rule encryption out. A RAR5 member whose header stopped part-way through its optional records is reported this way, because answering "not encrypted" from a header nobody finished reading would be a wrong answer rather than a missing one; the member then carries aMEMBER_HEADER_RECORD_SKIPPEDdiagnostic saying the header was cut short. A member with no such diagnostic is a definite answer. -
(is_currentbool, default:True) –Last-entry-wins:
Truefor the live final state of this path.Duplicate names keep earlier rows with
is_current=False(history / superseded).get()returns the current one.Stamped when the listing is complete. A
stream_members()pass sets it after its last member; one that went pastListingLimitsstamps only the members it kept, so a copy it yields after the limit cannot mark an earlier one superseded. -
(is_sparsebool, default:False) –Whether this member is stored as a sparse file.
-
(commentstr | None, default:None) –Per-member comment, if the format records one.
-
(create_systemCreateSystem | None, default:None) –The operating system that wrote the entry. For ZIP and RAR it decides how the stored mode and attributes are read.
Nonefor formats that do not record one; seeCreateSystemfor what each format sets. -
(windows_attrsint | None, default:None) –Raw Windows file-attribute bitmask, if recorded.
-
(hashesMapping[HashAlgorithm, bytes], default:dict()) –Stored content digests keyed by
HashAlgorithm(values alwaysbytes).CRC-32 is four big-endian bytes: a CRC you computed yourself compares as
zlib.crc32(data).to_bytes(4, "big"). Excluded from equality. -
(extraMemberExtra, default:MemberExtra()) –Format-specific extra fields (e.g.
extra["is_junction"]). Excluded from equality.Known keys and their value types are
MemberExtra. Unknown keys (third-party or future) stay legal and read asobject. TheEXTRA_*constants on this module remain the names for the keys they cover. -
(diagnosticstuple[Diagnostic, ...]) –(computed property) Read-only tuple of diagnostics attached to this member (may be empty).
-
(member_idint) –(computed property)
-
(is_filebool) –(computed property)
-
(is_dirbool) –(computed property)
-
(is_linkbool) –(computed property)
-
(is_otherbool) –(computed property)
-
(is_antibool) –(computed property)
-
(is_junctionbool) –(computed property) The archive recorded this symlink as a Windows NTFS junction.
Falsemeans no junction was detected, not that the entry is not one:- RAR names the link kind in a header field, so a RAR junction is reported while listing.
- A directory source asks the filesystem (
os.DirEntry.is_junction), so a scan on Windows reports junctions under Python 3.12 and later; under 3.11 a junction is listed as a symlink and readsFalse. - ZIP and 7z keep the junction's reparse tag in the member's data, so they
report one only when the writer stored that reparse buffer. 7-Zip stores
none for a directory reparse point, and a junction is always one, so a
junction in an archive 7-Zip wrote reads
Falsehere. - TAR and ISO have no junction concept and always read
False.
is_reparse_pointcomes from metadata the archive always carries, and is the check to use when a Windows link of either kind matters. -
(is_reparse_pointbool) –(computed property) The archive recorded this entry as a Windows symlink or junction.
Deliberately not gated on
typethe wayis_junctionis: a member the archive flags as a reparse point whose data turns out not to be a link buffer is presented as an ordinary file or directory, and the flag still records what the archive said about it. -
(member_state_finalbool) –(computed property) No field of this member will change from here on.
Falsewhile archivey can still update the member in place, which happens in three cases:- A member of a listing whose links have not been resolved yet. That listing
is complete, and the member settled, when a call that resolves links
returns:
members(),members_report()orget(), or the end of astream_members()pass or of iterating the reader. Until then a later member with the same name can still make this oneis_current=False, and links getlink_target_member. Astreaming=Truepass yields every member before that point, in every format. Amembers_report_if_available()peek resolves no links, so its members readFalseuntil a later call or pass does. A random-access pass with no listing call first yields from a complete listing in some formats and walks as it goes in others, so read the flag rather than assume. - A symlink whose target is stored as member data (ZIP, 7z, a RAR 4 symlink)
that nothing has read yet, which is the case under
ArchiveyConfig.read_link_targets=False. Reading it setslink_target, and a Windows reparse point whose data is not a link buffer becomes aFILE.open()andextract_all()read it when they reach the link, on the member they are given, a copy included. - A member whose
sizeisNoneuntil a full read fills it in: a single-file member (.gz,.bz2, …) whose format records no size, read from the start to the end without skipping a byte; or a large member of a macOS ZIP whose size the archive stores only modulo 2**32, read to the end with a matching CRC-32. A read that skips bytes, stops early or fails leaves itFalse.
Everywhere else it is
True. Closing the reader makes itTruefor every member it listed, since a closed reader updates nothing. So does the end of astreaming=Truepass, or its failure, for a size no read filled: nothing can read the member again. A copy made withreplace()is never stamped, so only its unread link target can change, except the copy a streamingextract_allhands itsfilter: it readsFalsewhile the walk can still change the member it came from, and the filter may then be called again for that member. Attacheddiagnosticsare not fields and can still grow. Which fields change in practice is listed per format in the user docs. - A member of a listing whose links have not been resolved yet. That listing
is complete, and the member settled, when a call that resolves links
returns:
-
(link_target_unrecordedbool) –(computed property) The archive records no target for this link.
Some writers store a link with nowhere to point: 7-Zip writes a directory symlink or junction this way, and a Windows reparse buffer can name nothing. Such a link has
link_targetNone,open()raisesLinkTargetNotFoundErrorwithreasonNOT_RECORDEDfor it, and extraction records it asExtractionStatus.LINK_TARGET_UNAVAILABLErather than as a failure.Falsedoes not mean the target is known.link_targetcan also beNonebecause the archive stores a target this read could not produce (damaged, encrypted, or out of reach), or because the target is stored as member data that nothing has read yet (ArchiveyConfig.read_link_targetsisFalse). This property isTrueonly when the archive itself left the target out.
modified_utc(tz_for_naive=None)
The modification time as a timezone-aware UTC datetime, or None.
modified itself is faithful to what the archive stores: naive when the
format records local wall-clock time (ZIP's DOS field, RAR4), aware when it
records UTC or an offset — so naive and aware values from one archive cannot be
compared or sorted directly. This helper makes that usable: an aware value is
converted to UTC; a naive one first gets tz_for_naive attached (the caller's
explicit assumption about where the archive was created), defaulting to the
local timezone when not given. Whether the stored value was wall-clock remains
visible on the field itself: member.modified.tzinfo is None.
replace(**kwargs)
Return a copy with the given fields changed; never mutates self.
object is not a check: neither the keyword names nor the value types are
verified statically. A misspelled field is still a TypeError from
dataclasses.replace at runtime; a wrongly typed value is accepted, and
shows up only where it is used. Typing the names would need a per-field
TypedDict, which is deliberately deferred.
No listing holds the copy, so nothing stamps is_current on it or resolves
its links to members; its member_state_final waits only on an unread link
target, which reading the copy fills in place (the copy extract_all hands
a filter is the exception: see member_state_final). Closing the reader cannot reach
a copy, so a copy whose target is never read keeps reading False. A read
fills a late size into the reader's own member, never into a copy.
archivey.ArchiveInfo
dataclass
Archive-level metadata, available immediately after open_archive() without
a full member scan.
Fields:
-
(formatArchiveFormat) –The detected
(container, stream)format of the archive. -
(format_versionstr | None) –Format version string, e.g.
"4.5"for ZIP or"5"for RAR5;Noneif unknown. -
(is_solidbool) –Whether decompressing one member may require decompressing earlier ones.
-
(member_countint | None) –Number of members, or
Nonewhen a count would require scanning the whole archive.Also
Nonewhen the archive cannot supply a count: a ZIP whose central directory is damaged, or holds more entries thanListingLimits.max_members. Thenmembers()raises too. A ZIP's count comes from walking its central directory, which reading this field runs if the listing has not. -
(commentstr | None) –Archive-level comment, if the format records one.
-
(is_encryptedbool) –Header-level encryption (7z, RAR5) — not per-member encryption (see
ArchiveMember.is_encrypted). -
(is_multivolumebool) –Whether the archive spans multiple volumes.
-
(costCostReceipt) –Listing/access cost receipt for the archive (see the
access-mode-and-costcapability). -
(extraArchiveInfoExtra, default:ArchiveInfoExtra()) –Format-specific archive-level metadata, keyed by namespaced strings (mirrors
ArchiveMember.extra). For example the ISO backend records the auto-selected namespace asextra["iso.namespace"]. Excluded from__eq__. Known keys and their value types areArchiveInfoExtra; unknown keys stay legal and read asobject.
archivey.HashAlgorithm
Bases: StrEnum
Digest algorithms that may appear as keys in ArchiveMember.hashes.
Members:
-
CRC32('crc32') –CRC-32, as four big-endian bytes.
-
BLAKE2SP('blake2sp') –BLAKE2sp, 32 bytes. Only RAR5 stores it.
archivey.MemberExtra
Bases: dict[str, object]
Per-member format-specific metadata on ArchiveMember.
A dict[str, object] whose known keys return their declared types from a
subscript (extra["zip.compress_type"] is an int). Unknown keys
(third-party or future) stay legal and read as object. The EXTRA_*
constants on this module still name the keys they cover.
Writes are not type-checked: a wrong-type assignment to a known key falls
through to the str → object fallback, same as an unknown key. .get()
returns object for every key. Assign a MemberExtra({...}) (or mutate
the existing bag); a bare dict is not assignable to the field.
Known keys:
is_junction(bool) — ZIP, 7z, RAR, directory. A Windows NTFS junction; ZIP and 7z set it only when the writer stored the junction's reparse data, and 7-Zip does not. Impliesis_reparse_point.is_reparse_point(bool) — ZIP, 7z, RAR, directory. The weaker, metadata-only sibling ofis_junction: a Windows symlink or junction rather than a POSIX one.is_file_copy(bool) — RAR. AFILEmember whose bytes the archive stores once under an earlier member (a RAR5 file reference,rar -oi).link_targetnames that source andlink_target_memberis it.special_file_type("fifo" | "char_device" | "block_device" | "socket" | "unknown") — every format. What the entry's stored Unix mode (ZIP, 7z, RAR, Rock Ridge) or TAR typeflag named when that is a device, FIFO or socket;"unknown"is a mode whose file-type bits no Unix type uses. Set whatever type the member ends up with: the stream-lessOTHERsuch an entry usually is, theFILEit becomes when it carries data (its bytes are the content, as unzip, 7-Zip, bsdtar andzipfiledeliver them), a RAR file copy, or theDIRECTORYorSYMLINKa structural marker made of it (a ZIP name ending in/, an ISO directory record, a reparse-point bit) over such a mode. The key says what the mode named; the type says what the archive's structure decided. Absent on anOTHERmember that is not a special file (a GNU dumpdir in TAR). TAR keepstar.typeas well.alternate_raw_name(bytes) — ZIP. The other stored spelling of the name, when the archive stores two andraw_nameis the onenamewas decoded from: for a ZIP name taken from its Info-ZIP Unicode Path extra field (0x7075), the header's legacy bytes.size_mod_2_32(int) — ZIP. The member's size modulo 2³², when that is all the archive stores: a large member of a ZIP macOS Archive Utility wrote without ZIP64, whose real size only a read can tell. Set whensizeis listed asNone; a read to the end checks the length against it and fills insize, and the key stays.rar.extract_version(int)rar.file_version(int)rar.tweaked_crc32(int)rar.tweaked_blake2sp(bytes)zip.compress_type(int)zip.aes_vendor_version(int)zip.aes_strength(int)zip.aes_actual_method(int)zip.extra_fields(dict[int, bytes]) — the central directory extra fields archivey took nothing from, by tag (the first of several with one tag), such as a vendor's own field or one archivey knows but found malformed. Absent when there are none, which is the usual case. When a later version learns to read a field, it leaves this dict. A pass over a pipe sets it once it reaches the central directory, at the end.tar.type(bytes)tar.pax_headers(Mapping[str, str]) — the member's PAX records, the global ones in force included. Read-only: a change raisesTypeError, anddict(...)gives a copy to change. It is adictsubclass, sojson.dumpstakes it.tar.devmajor(int) — set only for a character or block device (typeflag3or4) in a ustar or GNU header whose field is a number; absent for a FIFO, for a v7 header, and for a device whose field is not a number (that member carriesMEMBER_HEADER_RECORD_SKIPPED).tar.devminor(int) — present exactly whentar.devmajoris.gzip.original_filename(str)gzip.isize(int) — the gzip trailer's ISIZE field: the last four bytes of the file, read only when its end is cheap to reach (not from a pipe or another archive's member). For a file of one gzip member with nothing after it, this is the uncompressed size modulo 2**32. It is not the size for a file of several members (it covers only the last; a bgzip file ends with an empty member and reads 0) or with bytes after the last one.sizestaysNoneuntil a full read fills it in.iso.version(int) — plain ISO 9660 only: the;Nfile version stripped from the name. Versions of one name share it; the highest is listed last and is the current one.
__getitem__(key)
archivey.ArchiveInfoExtra
Bases: dict[str, object]
Archive-level format-specific metadata on ArchiveInfo.
Same shape as MemberExtra over a separate key set — do not merge
the two bags.
Known keys:
iso.namespace(str)zip.volume_count(int)rar.volume_count(int)7z.volume_count(int)
Formats and member types
See Format.
archivey.ArchiveFormat
dataclass
A (container, stream) pair identifying how an archive is packaged.
Prefer the named class attributes (ArchiveFormat.ZIP, ArchiveFormat.TAR_GZ,
…) over constructing pairs by hand. Those names are assigned immediately below
the class body; the ClassVar declarations exist so type checkers see them
without per-use suppressions. _FORMAT_NAMES is built from the same
assignments so repr / display_name stay in sync automatically.
A pair built by hand from strings — ArchiveFormat("raw_stream", "gz"), say,
from a format round-tripped through a config file — is converted to the enum
members at construction, with the same spellings the other enum arguments take,
and an unknown spelling raises ArchiveyUsageError there.
Fields:
-
(containerContainerFormat | PluginContainer) –The archive format, such as
ZIPorTAR, or a member of aPluginContainersubclass for a format a plugin adds. -
(streamStreamFormat) –The compression around the archive, such as
GZIPfor a.tar.gz, orUNCOMPRESSED. -
(display_namestr) –(computed property) Human-readable name for this format, e.g.
"ZIP","TAR_GZ".Uses the predefined named-instance attribute name (
ZIP,TAR_GZ, …). A pair with no named constant, such astar.lz, getsCONTAINER_STREAMfrom the two enum member names ("TAR_LZIP"), or the container name alone when the stream isUNCOMPRESSED(a plugin's"CPIO"); itsrepr()stays the explicit constructor form._FORMAT_NAMESis populated just after the class definition — safe at runtime because this property is never called before the module is fully loaded.
Members:
-
ZIP(ContainerFormat.ZIP, StreamFormat.UNCOMPRESSED) -
TAR(ContainerFormat.TAR, StreamFormat.UNCOMPRESSED) -
TAR_GZ(ContainerFormat.TAR, StreamFormat.GZIP) -
TAR_BZ2(ContainerFormat.TAR, StreamFormat.BZIP2) -
TAR_XZ(ContainerFormat.TAR, StreamFormat.XZ) -
TAR_ZST(ContainerFormat.TAR, StreamFormat.ZSTD) -
TAR_LZ4(ContainerFormat.TAR, StreamFormat.LZ4) -
GZ(ContainerFormat.RAW_STREAM, StreamFormat.GZIP) -
BZ2(ContainerFormat.RAW_STREAM, StreamFormat.BZIP2) -
XZ(ContainerFormat.RAW_STREAM, StreamFormat.XZ) -
ZST(ContainerFormat.RAW_STREAM, StreamFormat.ZSTD) -
LZ4(ContainerFormat.RAW_STREAM, StreamFormat.LZ4) -
LZIP(ContainerFormat.RAW_STREAM, StreamFormat.LZIP) -
LZMA_ALONE(ContainerFormat.RAW_STREAM, StreamFormat.LZMA_ALONE) -
ZLIB(ContainerFormat.RAW_STREAM, StreamFormat.ZLIB) -
BROTLI(ContainerFormat.RAW_STREAM, StreamFormat.BROTLI) -
Z(ContainerFormat.RAW_STREAM, StreamFormat.UNIX_COMPRESS) -
SEVEN_Z(ContainerFormat.SEVEN_Z, StreamFormat.UNCOMPRESSED) -
RAR(ContainerFormat.RAR, StreamFormat.UNCOMPRESSED) -
ISO(ContainerFormat.ISO, StreamFormat.UNCOMPRESSED) -
DMG(ContainerFormat.DMG, StreamFormat.UNCOMPRESSED) -
DIRECTORY(ContainerFormat.DIRECTORY, StreamFormat.UNCOMPRESSED) -
UNKNOWN(ContainerFormat.UNKNOWN, StreamFormat.UNCOMPRESSED)
file_extension()
The on-disk file extension for this format, without a leading dot.
Used for extension-based naming and detection — e.g. choosing the output
filename when converting between formats, or matching by extension in the
detector. Examples: ZIP -> "zip", TAR_GZ -> "tar.gz",
GZ -> "gz". Formats with no on-disk file representation
(DIRECTORY, UNKNOWN) return "".
archivey.ContainerFormat
Bases: StrEnum
The archive format that holds the members, the first half of an
ArchiveFormat.
Members:
-
ZIP('zip') -
TAR('tar') -
RAR('rar') -
SEVEN_Z('7z') -
ISO('iso') –An ISO 9660 disc image.
-
DMG('dmg') –An Apple disk image. archivey recognises one so that opening it can refuse it by name, but cannot read it.
-
DIRECTORY('directory') –A folder on disk, read as if it were an archive.
-
RAW_STREAM('raw_stream') –No container: a single compressed file, such as a
.gz. TheStreamFormatsays which compressor. -
UNKNOWN('unknown') –What detection reports when no format matches. Passing a format with this container to
open_archive()oropen_stream()raisesArchiveyUsageError.
archivey.PluginContainer
Bases: StrEnum
Base class for a container enum that a format plugin defines.
A plugin subclasses it with one member per container it adds, and builds its
ArchiveFormat from that member::
class CpioContainer(PluginContainer):
CPIO = "cpio"
CPIO = ArchiveFormat(CpioContainer.CPIO, StreamFormat.UNCOMPRESSED)
Provisional, with the rest of archivey.formats. Code that matches on
ArchiveFormat.container needs a default case once a plugin is registered.
archivey.StreamFormat
Bases: StrEnum
The compression around the whole container, the second half of an
ArchiveFormat. Each value is the format's file extension.
Members:
-
UNCOMPRESSED('uncompressed') –No compression around the container. ZIP, 7z, RAR, ISO and a plain TAR use this; their members can still be compressed inside.
-
GZIP('gz') -
BZIP2('bz2') -
XZ('xz') -
ZSTD('zst') -
LZ4('lz4') -
LZIP('lz') -
LZMA_ALONE('lzma') –The legacy
.lzmafile format, also called LZMA Alone. -
ZLIB('zz') -
BROTLI('br') –Brotli has no signature, so archivey detects it by the file name.
-
UNIX_COMPRESS('Z') –The Unix
compressformat.
archivey.MemberType
Bases: Enum
Kind of archive entry.
Members:
-
FILE('file') –A regular file. A device, FIFO or socket entry that carries data is a
FILEtoo, withextra["special_file_type"]saying what the archive called it. -
DIRECTORY('directory') -
SYMLINK('symlink') –A symbolic link.
link_targetholds the target as stored. -
HARDLINK('hardlink') –A hard link to another member, whose data it shares.
link_targetnames that member, andlink_target_memberis the member when the archive has it. -
OTHER('other') –An entry with no data that is not a file, directory or link, such as a device, FIFO or socket. Extraction refuses it under every policy.
-
ANTI('anti') –A deletion marker from a 7z incremental update, saying that a file was removed. It is not a file (
is_fileis false), and extraction skips it.
archivey.CompressionMethod
dataclass
One codec in a member's filter chain.
Members store tuple[CompressionMethod, ...]. Order matches the compress /
pack direction: pre-filters first, packing codec last (closest to the stored
bytes). Example: 7z (BCJ2, LZMA2) — decompress by applying LZMA2, then BCJ2.
Fields:
-
(algoCompressionAlgorithm) –The method or filter.
-
(levelint | None, default:None) –RAR only: the compression level, from 1 (fastest) to 5 (best), as
rar -m1to-m5set it. A stored RAR member isSTOREDwith no level, and other formats leave itNone. -
(propertiesbytes | None, default:None) –7z only: the coder's properties as stored, such as LZMA's dictionary size. Other formats leave it
None.
archivey.CompressionAlgorithm
Bases: Enum
A compression method or filter in a member's CompressionMethod chain.
Later versions may add members, so treat the set as open-ended.
Members:
-
STORED('stored') –No compression.
-
DEFLATE('deflate') -
DEFLATE64('deflate64') -
BZIP2('bzip2') -
LZMA('lzma') -
LZMA2('lzma2') -
ZSTD('zstd') -
LZ4('lz4') -
BROTLI('brotli') -
PPMD('ppmd') -
XZ('xz') –A complete
.xzstream, with its own header and checks, as a ZIP member with method 95 stores it. -
BCJ('bcj') –A filter that makes x86 executable code compress better. It does not compress by itself.
-
BCJ2('bcj2') –7z's variant of
BCJ, which splits the code into four streams. -
DELTA('delta') –A filter that stores the difference between bytes a fixed distance apart. It does not compress by itself.
-
RAR('rar') –RAR's own compression, any version. Not the same as
ContainerFormat.RAR, which is the archive format. -
UNKNOWN('unknown') –A method archivey does not recognise. Listing still works.
archivey.CreateSystem
Bases: Enum
The operating system that wrote a member, in ArchiveMember.create_system.
The values are ZIP's "version made by" host numbers. Other formats map their own host field onto them:
- ZIP: any of these values, and
UNKNOWNfor a number not listed here. A pass over a pipe leaves itNoneuntil it reaches the central directory. - RAR 1.5 to 4:
FAT(MS-DOS),OS2_HPFS,WINDOWS_NTFS,UNIX,MACINTOSHorBEOS;UNKNOWNfor any other host byte. - RAR5:
WINDOWS_NTFSorUNIX. - 7z records no host. archivey reports
UNIXwhen the member stores a Unix mode, andWINDOWS_NTFSotherwise. - TAR, ISO, a directory and a single-file format such as
.gzleavecreate_systemasNone.
Members:
-
FAT(0) -
AMIGA(1) -
OPENVMS(2) -
UNIX(3) -
VM_CMS(4) -
ATARI_ST(5) -
OS2_HPFS(6) -
MACINTOSH(7) -
Z_SYSTEM(8) -
CPM(9) -
WINDOWS_NTFS(10) -
MVS(11) -
VSE(12) -
ACORN_RISC(13) -
VFAT(14) -
ALTERNATE_MVS(15) -
BEOS(16) -
TANDEM(17) -
OS_400(18) -
OS_X_DARWIN(19) -
UNKNOWN(255)
Extraction
See Extracting.
archivey.ExtractionReport
dataclass
Immutable extraction outcome: fixed result tuple plus diagnostic summary.
results is a frozen outcome structure. Each ExtractionResult is frozen,
but ExtractionResult.member refers to the live mutable ArchiveMember
(caller-read-only), whose late-bound metadata and member diagnostics may still be
filled in place.
The report iterates, indexes, and sizes as its results sequence, so the common
for result in reader.extract_all(...) / len(...) / report[0] idioms keep
working while report.diagnostics exposes the call's diagnostic summary. It covers
that ForwardArchiveReader.extract_all() call only; detection and open
diagnostics stay on reader.diagnostics.
Fields:
-
(resultstuple[ExtractionResult, ...]) –One result per member processed, in the order they were processed.
-
(diagnosticsDiagnosticSummary) –The diagnostics emitted during this
extract_all()call.
archivey.ExtractionResult
dataclass
One entry per member processed, returned from extract_all().
Fields:
-
(memberArchiveMember) –The member this result is about. It is the reader's own member, so fields that are filled in late may still change; treat it as read-only.
-
(pathPath | None) –Where the member was written, or
Noneif it was not. -
(statusExtractionStatus) –What happened to the member.
-
(errorArchiveyError | OSError | None, default:None) –Why the member failed or was blocked, when extraction went on past it. An
OSErrorfor a file system error on this member, otherwise anArchiveyError. -
(requested_pathPath | None, default:None) –Where the member was to be written before
overwritesettled a collision.Usually the same as
path. UnderOverwritePolicy.RENAMEthe two differ when the member, or a directory it is in, was renamed (dd/fwritten atdd (1)/f). After a collision settled bySKIPorERROR, this is set andpathisNone. On anOVERWRITTENresult it keeps the path this member wrote to, which a later member then took over. That later member'scollided_withequals this path, whether this member wrote a file or a directory (outsideExtractionPolicy.TRUSTED), so join the two throughcollided_with: the later member's ownpathcan differ from this one in case on a case-insensitive file system, or in spelling through a symlink of the archive's.Nonefor a member refused or failed before its destination was settled. -
(presented_namestr | None, default:None) –The member's name before a safety rewrite changed it on disk, or
Nonewhen none did;rewritessays which ones ran.After a re-root this is the stored name. When a
filterrenamed the member, it is the filter's name, which neithermember.name(the archive's spelling) norpath(the final one) shows. -
(rewritesfrozenset[NameRewrite], default:frozenset()) –The safety rewrites that changed the name. Empty exactly when
presented_nameisNone. -
(failure_group_idstr | None, default:None) –Set when one failed hardlink source makes several link results fail: those results share this id. Compare ids for equality only; their format and order mean nothing, and they differ between runs.
-
(failure_group_sizeint | None, default:None) –How many results share
failure_group_id. -
(collided_withPath | None, default:None) –The destination an earlier member of this run wrote, which this member collided with, whatever
overwritethen did.Nonewhen no member of this run held the name: an entry already on disk before the run started is not counted, and stays inrequested_path. The member it collided with is the result whosepathequals this, or whoserequested_pathdoes once that result isOVERWRITTEN.A directory this run created on disk, from a
DIRECTORYmember or as the parent of an earlier member, holds its name too: a file landing on it records the directory's path. A directory that was only created as a parent has no result of its own. A directory that was already indestis not counted, even when aDIRECTORYmember of this run merged into it and reports it. -
(kept_modeint | None, default:None) –On a
DIRECTORYresult whose directory existed before the run, such as the destination itself for a./member: the mode that directory kept, when the member asked for another one. On Windows only the read-only attribute is compared.Noneotherwise. Such a directory keeps its times too, soNonedoes not mean the member's times were applied.
archivey.ExtractionStatus
Bases: StrEnum
The outcome recorded for a single member in its ExtractionResult.
Members:
-
EXTRACTED('extracted') –Written to
path. -
NOT_OVERWRITTEN('not_overwritten') –Not written, because an entry was already at the destination and
overwriteisOverwritePolicy.SKIP. -
SUPERSEDED('superseded') –Not written, because a later member with the same name replaces it.
-
OVERWRITTEN('overwritten') –Written, then replaced by a later member under
OverwritePolicy.REPLACE. The result changes to this when the later member arrives, so two members never both reportEXTRACTEDat one path, as when two names differ only in case. -
BLOCKED('blocked') –Refused by a safety check or by the policy.
-
FAILED('failed') –Extraction failed: damaged data, a limit such as the expansion ratio, or a write error.
-
LINK_TARGET_UNAVAILABLE('link_target_unavailable') –A symlink whose target the archive never recorded, so it cannot be written. Not a failure: extraction goes on whatever
on_errorsays. The archive is reported with theSYMLINK_TARGET_UNAVAILABLEdiagnostic, whichDiagnosticPolicy.STRICTraises.
archivey.NameRewrite
Bases: StrEnum
A safety rewrite that changed a member's name before it reached disk.
ExtractionResult.rewrites holds one value per rewrite that ran on the member,
and ExtractionResult.presented_name holds the name before them.
Both can run on one member: under STANDARD, /a/what?.txt is re-rooted to
a/what?.txt and then written a/what%3F.txt.
Members:
-
REROOTED('rerooted') –An absolute name was moved inside the destination by dropping its root (
/etc/xextracts asetc/x).STRICTrefuses such a name instead. -
PORTABLE_NAME('portable_name') –The name was respelled so that every supported OS stores it the same way: a
\written as/, a byte that is not UTF-8 or a character Windows refuses (<>"|?*and 0x01-0x1F) written as%XX, and, underSTRICTonly, a trailing dot or space removed from each segment.TRUSTEDnever rewrites a name.
archivey.ExtractionProgress
dataclass
Progress snapshot for the on_progress callback.
For FILE members the callback MAY fire multiple times as bytes are written
(about once per copy chunk); each member still gets a terminal report with
member_bytes_written equal to the member's size (or the final observed
byte count when size is unknown). Directories, symlinks, and hardlinks
produce a single report with member_bytes_written == 0.
Fields:
-
(memberArchiveMember) –The member being extracted.
-
(bytes_writtenint) –Bytes written so far in the whole call.
-
(total_bytes_estimatedint | None) –An estimate of the bytes the whole call writes, or
Noneif the archive records no sizes. -
(members_doneint) –Members processed so far, whatever their outcome.
-
(members_totalint | None) –Members the call processes, or
Nonewhen counting them would need a scan of the whole archive. -
(member_bytes_writtenint) –Bytes written so far for the current member.
-
(members_extractedint) –Members written so far. Like
members_blocked, it leaves out a member still in progress. -
(members_blockedint) –Members refused so far by a safety check or by the policy.
archivey.ExtractionPolicy
Bases: Enum
How much of an archive member to trust when writing it to the destination.
The policy decides how names and permissions are written. Some checks hold under
every policy, because the write itself would be unsafe or would differ by OS: a name
or link that leads outside the destination, a name with a NUL, a device node, and a
symlink target with a Windows drive letter or UNC root (C:/Windows,
//server/share), which Windows refuses and so is refused on every OS. A hardlink
is refused when the member it takes its data from is.
Members:
-
STRICT('strict') –The default, for archives from anywhere. A name is rewritten to a spelling every supported OS stores the same way, or refused when that is not possible: absolute names, Windows reserved names such as
CON, names with:, and names with characters that reorder how they display are refused. Files keep their permissions without the setuid, setgid and sticky bits and without write access for group and others, and a file its owner cannot execute is executable by nobody. Directories arerwxr-xr-x, and owners are not applied. -
STANDARD('standard') –For archives you mostly trust, such as your own. As
STRICT, but trailing dots and spaces are kept (a folder named only dots or spaces, such as..., is still refused), an absolute name is extracted inside the destination (/etc/xasetc/x, as tar and unzip do), and permissions are kept as stored without the setuid, setgid and sticky bits. -
TRUSTED('trusted') –For archives you trust, to reproduce them faithfully. Names are written as stored, except that an absolute name is extracted inside the destination. Permissions are kept as stored, and owners too when running as root. A name built to display as something else is extracted, where the other policies refuse it: it lands inside the destination under its stored bytes, so the risk is to a person reading the directory, not to the file system.
archivey.OverwritePolicy
Bases: Enum
What to do when a destination entry already exists where a member would be written.
Members:
-
ERROR('error') –The default. The member fails with
ExtractionError, andOnErrordecides whether extraction goes on. -
SKIP('skip') –The existing entry stays, and the member's result is
NOT_OVERWRITTEN. This is not a failure, soOnErrordoes not apply. -
REPLACE('replace') –The existing entry is deleted, then the member is written, so it is never written through a link that was there. A directory member merges into an existing directory. Any other member replaces an existing directory only if it is empty, and otherwise fails.
-
RENAME('rename') –The member is written next to it, with a number before the extension:
a.txtbecomesa (1).txt.
archivey.OnError
Bases: Enum
What to do when an individual member cannot be extracted.
It covers failures only: damaged data, a write error, an existing entry under
OverwritePolicy.ERROR. A member refused by a safety check is recorded as
BLOCKED and extraction goes on under either value; AbortOn.BLOCKED_MEMBER
stops on one instead.
Members:
-
STOP('stop') –The default. The first failure raises, and extraction stops there.
-
CONTINUE('continue') –The failure is recorded in the report as
FAILED, its partial output is removed, and extraction goes on with the next member.
archivey.AbortOn
Bases: StrEnum
Events that abort the whole extraction the first time they occur.
Passed as abort_on= to extract_all() (a collection; empty by
default). Independent of OnError and of DiagnosticPolicy: an event named
here aborts whatever those are set to, and one not named here never aborts.
The abort is immediate. The triggering member's partial output is removed, no later
member is processed, and no ExtractionReport is returned. Output already written
for earlier members stays on disk, as with OnError.STOP: an abort stops the run,
it does not roll it back. The exception is a ZIP read from a pipe, whose pass stops
before the central directory says which members are links: it removes the files it
wrote that could still be links (see "A ZIP from a pipe" in the extraction guide).
There is no member for extraction failures, because OnError.STOP already raises
on the first one.
Members:
-
BLOCKED_MEMBER('blocked_member') –A member is refused by a safety check or by the policy. Raises the
FilterRejectionErrorthat refused it. -
NAME_COLLISION('name_collision') –Two members of the archive would be written to the same path. Raises
NameCollisionErrorbeforeoverwriteis applied, so even withOverwritePolicy.RENAMEthe second member is not written. Not checked underExtractionPolicy.TRUSTED. -
NAME_SANITIZED('name_sanitized') –A name is rewritten to its portable spelling, or an absolute name is extracted inside the destination. Raises
NameRewrittenError. UnderExtractionPolicy.STRICTan absolute name is refused instead, soBLOCKED_MEMBERis the one that catches it. No policy implies this: it is for callers who refuse any name on disk that differs from the archive's, such as mirroring tools. To list rewrites without stopping, readExtractionResult.rewrites.
archivey.MemberFilter = Callable[[ArchiveMember], 'ArchiveMember | None']
module-attribute
archivey.sanitize_names(member)
Rewrite a member's name so that extraction writes it instead of refusing it.
Pass it as filter= to extract_all() (or call it from your own filter) to
extract every member that has a safe place to go under a rewritten name, instead of
refusing the members with an unsafe one. It changes:
- a rooted name (
/etc/x,C:\x,\\host\share\x): the root is dropped, so the member lands atetc/xinside the destination.STANDARDandTRUSTEDalready do this; underSTRICTit happens only with this filter. - a
..component: resolved against the segment before it (a/../b→b), and dropped when there is none (../x→x). - a bidirectional override or isolate character: removed.
- a Windows-reserved device name (
CON,NUL.txt) or a:in a segment:_is added after the reserved stem (CON_,NUL_.txt) and:becomes_. - a NUL character: becomes
_.
A hardlink's target is left as stored: it names a member, not a path.
A symlink's target gets the character and segment rewrites only (file:stream →
file_stream, sub/NUL → sub/NUL_), so a link to a rewritten member points
at the name that was written. Its root and its .. components are kept: it is a
path on the filesystem, and one that points outside the destination is still
refused. A symlink target with a Windows drive or UNC root (C:/x,
//host/share) gets the character rewrites only, and is still refused.
A symlink target the archive stores as member data and that is read only after
this filter has run (ArchiveyConfig.read_link_targets=False, or a streaming
pass) gets the same rewrite: extract_all calls the filter again once the
target is read.
Two members can end up with the same name (a/../x and x). The second is then
handled by the overwrite option like any other clash. The result's
member.name keeps the stored name and path shows where it was written.
Returns member unchanged when nothing needed rewriting.
Configuration and passwords
See Configuration, Passwords and Hardening.
archivey.ArchiveyConfig
dataclass
Library tuning knobs passed as config= to open_archive(),
open_stream() and detect_format().
Per-call operationals (format, streaming, password, extraction's
members/filter/policy/…) stay keyword arguments — not fields here.
Fields:
-
(use_rapidgzipAcceleratorMode, default:AcceleratorMode.AUTO) –Whether to use the
rapidgzipaccelerator for gzip, zlib and raw deflate.It gives seekable member streams over those codecs, and decodes in parallel. Under
AUTOit is used only when the compressed input is known to be at leastRAPIDGZIP_AUTO_MIN_COMPRESSED_SIZEbytes (or its size is unknown) and the decompressed size can be verified, so a truncated stream cannot be swallowed silently. With seeking asked for, that is enough. Without it,AUTOuses it only on Linux and macOS, when the size is known, the source can seek and the data compresses 3:1 or less: more compressible data, and Windows, where the child starts more slowly, read faster with the standard library.rapidgzip runs in a child Python process, because it aborts the process on a stream that ends early; starting one costs about 45 ms, which is why the
AUTOthreshold is 16 MiB. Each open stream has a child of its own. On Linux, the members of one archive file on disk reuse children: a member whose stream closed without an error hands its child to the next one, which skips the start. A reused child counts what earlier members left in its memory againstmax_decoder_memory, so one that kept more than a quarter of the cap is not reused. Those children end when the archive closes. Where no child can be started (a frozen application, archivey imported from a zip, or a spawn or temporary file the operating system refuses),AUTOuses the standard library, logging one warning per process on thearchivey.streamslogger, andONraisesResourceUnavailableError.OFFnever starts a child. -
(use_indexed_bzip2AcceleratorMode, default:AcceleratorMode.AUTO) –Whether to use rapidgzip's bundled bzip2 backend for random access into bzip2.
Under
AUTOit is used only when the compressed input is known to be at leastINDEXED_BZIP2_AUTO_MIN_COMPRESSED_SIZEbytes (or its size is unknown). Like the gzip decoder, it runs in a child Python process, one per open stream, reused the same way (seeuse_rapidgzip). No crash has been seen in it; it is isolated as a precaution, because it comes from the same library as the gzip decoder, which does crash on some input. Where no child can be started,AUTOuses the standard library, logging one warning per process on thearchivey.streamslogger, andONraisesResourceUnavailableError. -
(process_isolationProcessIsolation, default:ProcessIsolation.KNOWN_CRASHES) –Which native decoders run in a child process. See
ProcessIsolation.Accepts the member or its name (
"known_crashes","native_decoders","none"). -
(zip_unflagged_fallback_encodingstr, default:'cp437') –Encoding for a ZIP member name that is neither flagged nor valid UTF-8.
A name stored without the UTF-8 flag is tried as UTF-8 first; this encoding is used when those bytes are not valid UTF-8. The default is cp437, as the ZIP specification (APPNOTE) says. Set a local code page (for example
"cp1252"or"shift_jis") for archives known to come from one. An explicitencoding=onopen_archive()replaces this fallback for that archive; a name whose bytes are valid UTF-8 is still decoded as UTF-8. -
(rar_allow_glob_member_concatenationbool, default:False) –Read a RAR member whose stored name contains
*or?even when that name also matches other members.unrarselects members by an include mask, so such a name can match other members as well. Names like this are almost always constructed, so the read is refused by default. On a nonsolid archive the extra decode is also unbounded and not reported (ExtractionLimitsdoes not coveropen()orread()); on a solid archive those bytes are already insideAccessCost.SOLID. SetTrueto read it anyway. A glob name that matches no other member is unaffected either way. Underrar_decompressor=AUTO, an installedunarreads such a member by index instead of the refusal (seeRarDecompressor), so this flag matters only whenunrarmust read it. -
(rar_decompressorRarDecompressor, default:RarDecompressor.AUTO) –Which external program decompresses RAR member data. See
RarDecompressor.Accepts the member or its name (
"unrar","unar","auto","none"). The listing does not depend on it, except that the selected program decodes compressed RAR 1.5-4 comments. -
(read_link_targetsbool, default:True) –Whether the reader reads a symlink's target when the format stores it as member data (ZIP, 7z, RAR3/4) rather than in the header.
True: listing and a finishedstream_members()pass read every such target, selected or not, so the report matches random access. On ZIP and 7z that can decompress data nobody selected and consult the password provider.False: the reader reads none of them on its own;extract_allreads the targets of links its selector and filter accept, andopen()reads the target of a link it follows. Fixed for the reader's lifetime. -
(extraction_limitsExtractionLimits, default:ExtractionLimits(max_extracted_bytes=2147483648, max_ratio=1000.0, ratio_activation_threshold=5242880, max_entries=262144, live_ratio_activation_threshold=268435456)) –Decompression-bomb guards for extraction. See
ExtractionLimits. -
(listing_limitsListingLimits, default:ListingLimits(max_members=262144, max_metadata_bytes=67108864)) –Caps on the size of a member listing. See
ListingLimits. -
(decoder_limitsDecoderLimits, default:DecoderLimits(max_decoder_memory=2147483648, max_key_derivation_rounds=33554432, max_ppmd_held_input=16777216)) –Caps on what the archive may make a decoder allocate or compute.
See
DecoderLimits. -
(spool_limitsSpoolLimits, default:SpoolLimits(max_bytes=1073741824)) –Caps on what a reader writes to temporary storage. See
SpoolLimits. -
(detection_budgetDetectionBudget, default:DetectionBudget(max_prefix_bytes=4096, max_far_bytes=32774, max_scan_bytes=2097152, max_decode_input=1048576, max_decode_output=1048576, completion_window_bytes=65536)) –Upper bounds on what format detection may read and decode.
Used by
open_archive()anddetect_format()alike. The default,BALANCED, covers every format archivey reads at the offset it specifies, a self-extracting archive within 2 MiB of the start, and the content probes.FAST_BUDGETandTHOROUGH_BUDGETinarchivey.detection_costare the other presets; to change a single limit,dataclasses.replaceone. -
(always_probe_contentbool, default:False) –Whether detection tries every content probe, whatever the source is named.
LZMA Alone, zlib and Brotli have no magic that detection can trust, so only a content probe (a trial decode of the first bytes) recognises them. Arbitrary binary files sometimes pass a probe, so by default
open_archive()anddetect_format()run a probe only when the source's name ends in one of that format's extensions:.lzmaor.tlzfor LZMA Alone,.zzor.zlibfor zlib,.bror.brotlifor Brotli, and the.tar.form of each except.tlz. An extensionless raw stream of these formats is then refused withFormatDetectionError. If you read such sources, set this toTrue, passformat=, or useopen_stream(), which always tries every probe because its caller already says the source is a compressed stream. -
(diagnostic_policyDiagnosticPolicy, default:DiagnosticPolicy()) –Whether each diagnostic code is ignored, collected or raised.
See
DiagnosticPolicy. -
(max_retained_diagnostic_referencesint, default:256) –How many references to diagnostics the library keeps per collector.
Each diagnostic retained in the summary takes a slot, and each one attached to a member takes another. Counts stay exact past the cap. A random-access member walk also keeps, until it ends, a record that a walk started over after a failure replays: the codes of the diagnostics emitted for the members it has built, and the full diagnostics only for the member being typed. That record takes no slot.
-
(on_diagnosticOnDiagnostic | None, default:None) –A callback called with each diagnostic the policy collects or raises, as it is emitted, or
None. -
(formatsFormatPolicy, default:FormatPolicy()) –Which formats this call may open. See
FormatPolicy.The default allows every format archivey reads and every registered plugin format.
__post_init__()
Check the fields at construction, and convert the two that hold enums.
A config field is read wherever it is needed, which is never where it was
written: ArchiveyConfig(extraction_limits="none") builds fine and then
fails part-way through an extraction as AttributeError: 'str' object has no
attribute 'max_extracted_bytes' — a private attribute name, and no mention of
the argument the caller actually got wrong. Checking config= at the entry
points does not reach this: the object passed there is an ArchiveyConfig,
and the wrong type is one field in.
The two accelerator fields are converted rather than only checked, because
their consumers test them with is (AcceleratorMode.enabled_for()): a
string that survived construction would not be refused on use, it would read as
"neither ON nor OFF" and silently take the AUTO path. Converting here means the
field always holds a member, and a bad spelling names itself at the call site
that wrote it rather than during some later stream open.
They stay annotated AcceleratorMode rather than AcceleratorMode | str
because that is what they hold once constructed, and it keeps every consumer
honest. A string is still accepted at construction — a type checker flags it,
which is the right answer for a typed caller who has the enum imported anyway,
and an untyped script gets the conversion.
That conversion goes through object.__setattr__ because the dataclass is
frozen and it rewrites the field rather than only inspecting it. The checks
above reject without writing, so they need no such thing.
archivey.DEFAULT_ARCHIVEY_CONFIG = ArchiveyConfig()
module-attribute
The configuration used when a call is given no config=: ArchiveyConfig().
archivey.FormatPolicy
dataclass
Which formats one call may open.
Set it on ArchiveyConfig.formats::
# ZIP, and TAR with or without gzip or xz. Nothing else opens.
UPLOADS = ArchiveyConfig(
formats=FormatPolicy(only={C.ZIP, C.TAR, S.GZIP, S.XZ})
)
# Everything except RAR, and no plugin formats.
NO_RAR = ArchiveyConfig(formats=FormatPolicy(exclude={C.RAR}, plugins=False))
An ArchiveFormat is a container and a compression, and both
halves must pass. only={C.TAR} refuses .tar.gz until S.GZIP is listed
too; StreamFormat.UNCOMPRESSED always passes. The default policy allows every
format.
open_archive() and open_stream() refuse a format
the policy does not allow with UnsupportedFeatureError,
whose message names every entry to change; format= does not bypass the policy.
detect_format() reports the true format of a built-in file whatever
the policy says, so a caller can check allows() before opening. A plugin
format the policy turns off is not detected at all: its magic, extensions and
content probe are left out of that call, and the detection receipt records each
tier that left one out as NOT_ENABLED_BY_POLICY.
The policy decides which reader runs, not what detection reads: magic, the
self-extracting archive scan and the built-in content probes still run on every
input, within ArchiveyConfig.detection_budget.
replace= reads a built-in container with a plugin instead of the built-in
reader (see replace)::
MY_ZIP = ArchiveyConfig(formats=FormatPolicy(replace={C.ZIP: MyZipPlugin}))
Fields:
-
(onlyfrozenset[ContainerFormat | PluginContainer | StreamFormat] | None) –The containers and compressions that may open, or
Nonefor all of them. -
(excludefrozenset[ContainerFormat | PluginContainer | StreamFormat]) –The containers and compressions that may not open.
-
(pluginsbool) –Whether registered plugin formats may open. Use
Falsewithexclude; underonlya plugin opens only when its container is listed. -
(replaceMapping[ContainerFormat, type[FormatHandler]]) –Built-in containers read by a plugin instead of the built-in reader.
A key is
ContainerFormat.ZIP,SEVEN_Z,RARorISO. Its plugin is aarchivey.formats.FormatHandlerthat is not registered: itsformatis the built-in one (ArchiveFormat.ZIP), and it declares noextensions,magicorprobe, because the file is found by the built-in detection. The reader'sformatis the built-in format andformat_info.from_pluginis false. The plugin gets what any indexed plugin gets, a seekable source from the start of the archive, so a multi-volume set and an archive behind a self-extracting stub are refused withUnsupportedFeatureError, a source that cannot seek withStreamNotSeekableError, and a raw CD sector image detected as ISO as the built-in reader refuses it. The format's ownArchiveyConfigoptions reach the plugin throughReaderContext.config. -
(_turns_off_pluginsbool) –(computed property) Whether some plugin format could be off: the cheap test detection makes before it filters each plugin signal through
allows().
allows(fmt)
Whether fmt may open under this policy.
archivey.ExtractionLimits
dataclass
Decompression-bomb limits for extraction.
Applied by extract_all().
None on a guard field disables that guard. UNLIMITED sets the three
guard fields to None; ratio_activation_threshold and
live_ratio_activation_threshold are parameters of the ratio guard rather than
guards of their own, and are moot once max_ratio is None.
Fields:
-
(max_extracted_bytesint | None, default:2147483648) –Most bytes one extraction may write in total, across every member. 2 GiB.
Bytes copied rather than decoded count too: a hard link written as a copy because it crosses a device boundary or goes past the filesystem's link-count limit. Only the link-count copies also count toward the archive-wide
max_ratio, so this is the one guard that bounds a cross-device copy. Crossing it stops the whole extraction, even underon_error="continue". -
(max_ratiofloat | None, default:1000.0) –Largest decompressed-to-compressed ratio allowed.
1000.0.Checked per member (a member over it fails on its own, and
on_error="continue"moves on) and across the archive (which stops the extraction). The per-member check needs the member's compressed size; where the format or access mode does not give one, only the archive-wide check applies. Neither check starts beforeratio_activation_thresholdbytes of output, and the archive-wide check on a source of unknown size, such as a pipe, not beforelive_ratio_activation_threshold. The archive-wide check also counts the copies written when a file runs out of hard-link slots (1024 names on NTFS), since the archive's declared link count drives them.A member's output never passes the size the archive records for it. Where both sizes are recorded, a member over the per-member ratio is refused before any of it is written.
-
(ratio_activation_thresholdint, default:5242880) –Output bytes before
max_ratiois checked. 5 MiB.Small members compress extremely well without being bombs, so the ratio is only judged once a member (or the archive, for the archive-wide check) has produced this much. The archive-wide check on a source of unknown size, such as a pipe, waits for
live_ratio_activation_thresholdas well. It cannot beNone; setmax_ratiotoNoneto turn the ratio guard off. -
(max_entriesint | None, default:262144) –Most entries one extraction may create: files, directories and links.
Crossing it stops the whole extraction, even under
on_error="continue".It also bounds, separately, how many times one extraction rechecks a symlink because a later member changed a path the link resolves through. Only a hostile archive comes near it, and crossing it stops the extraction the same way.
Noneremoves both bounds, so a hostile archive can then make the rechecks take time quadratic in its size. -
(live_ratio_activation_thresholdint, default:268435456) –Output bytes before the archive-wide ratio is checked on a source of unknown size. 256 MiB.
When the archive's size is not known up front, as for a
.tar.gzread from a pipe, the archive-wide check divides the output by the compressed bytes read so far. An honest archive whose data starts with a long blank run, such as a disk image, looks like a bomb until the data after that run arrives: blank data compresses past 1000:1. The same archive read from a file passes, because there the check divides by the whole file. So from such a source the archive-wide check waits for this much output, and a blank start shorter than this passes as it does from a file. Past it, the check still divides by the bytes read so far, so a longer blank start can still be refused; once a pipe has been read to its end, the two checks agree. A bomb read this way can write this much before the check stops it;max_extracted_bytesstill bounds the total. Lower it to stop such a bomb sooner; raise it for piped archives with longer blank starts. A value belowratio_activation_thresholdhas no effect. It cannot beNone; setmax_ratiotoNoneto turn the ratio guard off.
archivey.ListingLimits
dataclass
Caps on the member listing a reader builds and keeps.
Applied from the reader's open ArchiveyConfig.listing_limits for its lifetime.
None on a field disables that guard. UNLIMITED disables both.
Every call that lists members enforces them, in both access modes: members(),
members_report(), stream_members(), for member in reader and
extract_all. Each raises ResourceLimitError at
the member that crosses a cap.
ZIP, 7z, RAR and ISO apply max_members at parse (ZIP at open, or as it reads
the directory when the end record understates the count), 7z and RAR weigh the
names and comments their headers hold against max_metadata_bytes (RAR's
compressed RAR 1.5-4 comments by declared or packed size, whichever is larger,
before decoding them), and ISO the directory records, path tables and UDF
descriptors pycdlib parses, so open_archive raises. A ZIP read from a
stream that cannot seek has no directory to read at open: its pass charges
max_members on each local header as it yields the member, and again on the
directory's entries after the last member, and weighs the directory's text
against max_metadata_bytes as it arrives. A ZIP file whose end record sits
behind more than 64 KiB of trailing bytes is walked the same way at open, which
charges max_members on each local header.
ISO also counts each path-table entry against max_members (a table of more than
max_members + 1 entries is refused): every entry is a directory, which is a
member anyway, so real images never notice. ISO counts the UDF tree pycdlib
parses against max_members too, one per UDF name, although members() does
not list UDF: an image whose UDF tree has more names than its ISO 9660 tree can be
refused for the UDF tree. ISO counts members per tree but weighs bytes as one sum
for the whole image, every tree together.
TAR refuses, in every mode, an extended header (PAX or GNU long name) that declares
more than is left of max_metadata_bytes, before reading it.
Fields:
-
(max_membersint | None, default:262144) –Most members a listing may hold.
Each listed member costs about 1.5 KiB of memory (measured by
scripts/measure_limit_costs.py), so the default allows about 384 MiB at open on 7z, RAR and ISO, where a tiny compressed header can declare that many members (ZIP checks the count too, but its directory costs about 46 bytes per entry). A current Linux kernel source tree has about 90 000 files, a third of the default; raise this for archives with more members, and lower it when opening untrusted archives on a server with less memory to spare. -
(max_metadata_bytesint | None, default:67108864) –Most bytes of text a listing may retain across its members. 64 MiB.
Counts member names (and raw names), comments, link targets, owner and group names and the string or bytes values in
extra, plus the keys of a dict nested in it (such as TAR's PAX keywords), plus the archive comment. The top-levelextrakeys are fixed per format and do not count. Non-ASCII text counts four bytes per character, so it is an upper bound rather than an exact size. A TAR sparse member's map, which the listing keeps to read the member, counts 24 bytes per entry, and each ZIP extra field kept inextra["zip.extra_fields"]counts 72 bytes plus its data.A ZIP read from a stream that cannot seek also keeps the data of each member short enough to be a symlink until its directory says which members are links. That data does not count here; the pass holds at most this many bytes of it, dropping the longest first. A link whose data was dropped is listed without its target and reports
SYMLINK_TARGET_UNAVAILABLE.
archivey.DecoderLimits
dataclass
Caps on what a decoder may allocate or compute because the archive said to.
Several codecs size their working memory from a number in the archive's own header rather than from anything the caller chose: 7z PPMd var.H carries a 32-bit window size, ZIP method 98 an 8-bit megabyte count, LZMA a 32-bit dictionary size. Those numbers are attacker-chosen, they are read before a single byte of member data is, and the allocation that follows is not proportional to the archive's size — a 153-byte 7z can ask for 4 GiB.
What is capped today: both PPMd paths, and the LZMA dictionary size
wherever an archive declares one — 7z LZMA and LZMA2, ZIP method 14, each xz
block, .lzma and each lzip member — and the window each zstd frame declares,
on .zst, ZIP method 93 and 7z. RAR's dictionary is checked too, before
unrar or unar is started, at what that program allocates for it. unar
writes to the whole declared dictionary, so the declared size counts. unrar
writes to it only as output is produced, so the declared size counts only up
to the unpacked bytes the read decodes: the member's own size, or in a solid
archive the members up to and including it. The two decoders that allocate what
the header says, pyppmd and liblzma, differ. A refused allocation inside pyppmd
takes its process down (a child process unless process_isolation is NONE).
liblzma does raise MemoryError when it cannot reserve the dictionary, but a
reservation that succeeds is its real cost: the dictionary fills as output is
written, so a 151 KB stream declaring 4 GiB held 1.1 GiB resident after
producing 1 GiB, where the same stream declaring 1 MiB held 59 MB. The
dictionary bounds how much of the output the decoder keeps, and the archive
picks it.
max_decoder_memory also caps the rapidgzip accelerator for gzip, zlib and raw
DEFLATE, though no archive declares a size there. rapidgzip keeps whole decoded
chunks in memory, so a 1 MB gzip file of zeros made it hold close to 1 GiB. Its
child process is stopped when its memory grows past the cap, and the standard
library reads the rest of the stream, so the result does not change; only the
speed does. The child checks every millisecond and again before it answers each
read, so the peak can pass the cap by what the decoder allocates between two
checks: measured, about 17 MB on four cores. On a busy machine the timed check can
wait for a processor, and then the check before the next read stops it. Where the
platform does not report a process's peak memory, the child is not capped.
The same shape holds for key derivation, which costs time rather than memory:
RAR5 and 7z headers say how many hashing rounds turn a password into a key,
and max_key_derivation_rounds caps their total over one open archive.
This is not an ExtractionLimits field, and the difference is not
cosmetic. The bomb guards there measure output: they count bytes as an
extraction produces them and stop when the total or the ratio says the
archive is lying about its size. A decoder's working memory is neither
output nor proportional to it, it is claimed up front, and it is claimed on
open() and read() as much as on extract_all() — paths
ExtractionLimits does not cover at all.
Applied from the reader's open ArchiveyConfig.decoder_limits for its
lifetime, as ListingLimits is: the codec-layer view is built once
when the reader is.
None on a field disables that guard. UNLIMITED disables every one.
Where an archive declares the size, exceeding a guard raises
ResourceLimitError before the allocation,
which is the only place it can be raised in this process: the process has no
recourse once the request is in the allocator's hands. The rapidgzip child is
the exception: nothing declares its size, so its memory is measured after the
allocation, in its own process, and going over the cap raises nothing to the
caller. The child is stopped and the standard library continues the stream.
Detection is not capped. Formats without magic (.lzma, and the
compressed tar inside .tar.xz and its siblings) are recognised by
decoding a small sample, and that sample is decoded with no decoder limit,
because a capped probe would report a different format for a caller who
passed UNLIMITED. It does not need one: a probe builds its LZMA
decoder with only the dictionary its sample needs (4 to 64 KiB, whatever the
header declares), which decodes the sample to the same bytes, and lowers
zstd's window limit to libzstd's default 128 MiB, treating a frame over it
as unrecognised. The open that follows detection is capped as described
here.
Under a memory cap (a container limit, RLIMIT_AS, a small machine) a
refused native allocation does not always surface as MemoryError —
pyppmd 1.3.1 dies on double free or corruption and takes the
interpreter with it, so no try/except around the decode can contain
it.
Fields:
-
(max_decoder_memoryint | None, default:2147483648) –Largest archive-declared working set a single decoder may allocate. 2 GiB.
A 7z folder runs all its decoders at once (the stages of a coder chain, and a BCJ2 folder's
main,callandjumpbranches), so when it has more than one, their LZMA dictionaries, PPMd memory sizes and zstd windows count together against it. A zstd window counts as the first frame declares it, when the zstd coder reads a packed stream directly.That number is a policy choice, not a limit of the format, so here is what it was chosen against. Measured on 7-Zip 23.01, a writer declares whatever
-m0=PPMd:mem=…asked for, reduced for a small member to 16× its size rounded up to a power of two (floor 64 KiB). Its presets never ask for much: plain-m0=PPMddeclares 16 MiB and-mx9256 MiB, whatever the input. Reaching 2 GiB therefore takes an explicitmem=2g, which cannot go above it at any member size; onlymem=3gandmem=4gdo. The field itself is 32 bits, so a header may declare just under 4 GiB — and a 153-byte archive may declare it, which is the case the cap is really for, since asking costs an attacker nothing and it is read before any member data is.2 GiB is the last round value below that 32-bit ceiling, matching
ExtractionLimits.max_extracted_bytes. It admits every archive 7-Zip's own presets write, by a factor of eight, and admits a deliberatemem=2gas well; it refuses the top of the field. Reading archives written withmem=3gor above means raising it or passingUNLIMITED.The LZMA dictionary sits further under it. xz Utils refuses to write a dictionary over 1.5 GiB, and its
-9preset writes 64 MiB whatever the input. 7-Zip 23.01 shrinks the dictionary it declares to about the member's size (4 KiB for a 25-byte member, 7 MiB for 6.7 MB, 96 MiB for 100 MB, all asked for with-md=1536m), so declaring more than 2 GiB takes a member over 2 GiB and an explicit-mdabove 2g. lzip's field cannot say more than 512 MiB. The same number serves both codecs because it answers the same question, and xz and.lzmaare checked through liblzma's own memory limit, which also counts about 64 KiB of the decoder's overhead; a 128 KiB allowance keeps a dictionary exactly at the cap readable there as on every other path.What the default is not is a promise about the machine. The cap bounds what an archive may ask for; whether an allocation succeeds is a property of the host, and the two are independent. A process with less headroom than the cap — a container under a memory limit, a small VM — gets nothing from the default: a declaration below 2 GiB passes the guard, and the allocation that follows is the one that fails, which is the unsurvivable case described above. Such a process should set the cap under its own headroom, anchored on the limit it runs with rather than on anything 7-Zip writes. Code that opens files it did not choose — an upload endpoint, a mail scanner — wants the same move for a different reason: 256 MiB still takes everything the PPMd and LZMA presets produce.
-
(max_key_derivation_roundsint | None, default:33554432) –Total rounds of password-to-key derivation one open archive may run.
2**25.RAR5 and 7z let the archive choose how expensive a key is to derive: RAR5's
kdf_countasks for2**kdf_countPBKDF2-HMAC-SHA256 rounds, 7z'sNumCyclesPowerfor2**cyclesSHA-256 rounds, each up to2**24(a few seconds). Each derivation is capped already; this caps the sum. RAR5 salts each encryption record and 7z each folder, so an archive can make every member cost a fresh derivation, and a candidate password list multiplies that again. The work runs insidehashlibwith the GIL released and cannot be interrupted, so what the caller sees is a process that stops responding.Rounds are counted as the archive declares them, per derivation that actually runs. A key the reader already derived for the same password and salt comes from its cache and costs nothing, and real writers reuse one salt across an archive (rar 7.00 writes one per archiving run, 7-Zip 23.01 writes none), so an ordinary archive spends one or two derivations whatever its size. Every candidate password that is tried counts, right or wrong. The RAR3 scheme, whose cost is fixed at
2**18SHA-1 rounds, counts at that number; ZIP AES (a fixed 1000 rounds) is not counted.RAR5 charges one derivation per output it needs from a password: the PswCheck (
2**kdf_count + 32rounds), the checksum HashKey (+ 16) and, when archivey decrypts a stored member or the headers itself, the AES key (+ 0). A RAR5 read throughunrarcosts two derivations per password, a stored member three.2**25is about ten seconds of hashing: two 7z derivations at the2**24maximum, but not one maximum-cost RAR5 member, which needs two or three; 64 passwords at 7-Zip's2**19; about 500 at rar's2**15. Exceeding it raisesResourceLimitErrorbefore the derivation that would cross it starts. Code that opens archives it did not choose may want2**24, which still admits every archive the usual tools write. -
(max_ppmd_held_inputint | None, default:16777216) –Largest PPMd member, in compressed bytes, held and handed to the decoder whole. 16 MiB.
The decoder is pyppmd, in a child process unless
process_isolationisProcessIsolation.NONE. Fed whole, it is asked little after a corrupt member ends early, the state in which it can crash. Fed in pieces, it cannot tell a corrupt stream that ended from a valid one waiting for input. Holding costs memory in this process: the held input, and about as much again while it is copied. A larger member, and a ZIP member read from a stream that cannot seek with its sizes stored after its data, is fed in pieces.Noneholds every member whose size is known, at a memory cost that grows with the member.
archivey.SpoolLimits
dataclass
Caps on what a reader writes to temporary storage, mostly copies of its source.
Some reads need the archive as a file on disk even when the caller passed a stream.
Today that is RAR: unrar and unar, the programs that decode member data, take
a filesystem path, so a RAR opened from a BytesIO or another file object is
copied to a temporary file (a volume set, to a temporary directory) the first time a
member has to go through one of them. The copy is of the whole archive, and it is
removed when the reader closes.
A source opened from a path is read in place and not copied, with two exceptions
that are bounded here too. A RAR with a prefix before it (an SFX stub, say) read
with rar_decompressor='unar' is copied from where the RAR starts. And files
that must be linked into a temporary directory, which unar always reads from
and unrar needs for an explicit list of volumes it would not find by name, are
copied there when the system can make neither a symlink nor a hard link.
Applied from the reader's open ArchiveyConfig.spool_limits for its lifetime.
None on a field disables that guard. UNLIMITED disables it.
Fields:
-
(max_bytesint | None, default:1073741824) –Most bytes one reader may write to temporary storage. 1 GiB.
That is the copy of its source, and the sources a solid RAR pass keeps for file copies. A volume set counts as one copy: the limit applies to the total across its volumes. When the size is known before the copy starts, an archive over the limit raises
ResourceLimitErrorbefore anything is written. Otherwise the copy stops before it passes the limit. Either way the partial copy is removed, and the error names this field. The limit holds for the reader, not per attempt: once a copy is refused, later reads that need it are refused without copying again.0refuses every copy: a stream source then reads only the members archivey can read withoutunrar, such as stored, unencrypted RAR members. The copy goes to the platform temporary directory (tempfile.gettempdir()); where that is memory-backed, such astmpfs, this limit is a memory limit.The same allowance holds the sources of RAR5 file copies (
rar -oi) in a solid pass (stream_members, extraction): the pass keeps each source as it decodes it, up to 8 MiB in memory and the rest in a temporary file within this limit, so a copy does not decode the solid stream again. That file counts only from the source's first decoded byte, after any copy of the archive the pass needs, until the pass ends and deletes it. Unlike a copy's bytes, which keep counting after the copy is removed, the kept file's bytes then stop counting. A source the limit has no room for is not kept, and its copies decode it again from the archive; that is never refused.
archivey.AcceleratorMode
Bases: Enum
Whether to use an optional accelerator that gives seekable streams over a codec.
Members:
-
AUTO('auto') –Use it when seekable streams were asked for (
seekable_members=Trueonopen_archive,seekable=Trueonopen_stream), or for gzip, zlib and raw deflate on Linux and macOS a large sequential read (below), its package is installed, and the compressed input is known to be large enough to pay for it:RAPIDGZIP_AUTO_MIN_COMPRESSED_SIZEfor gzip, zlib and raw deflate, andINDEXED_BZIP2_AUTO_MIN_COMPRESSED_SIZEfor bzip2. An input of unknown size counts as large enough. For gzip, zlib and raw deflate the decompressed size must also be known, so that a truncated stream cannot be read short without an error. Also for gzip, zlib and raw deflate, on Linux and macOS, a read with no seeking asked for uses it when the source can seek, the compressed input is known to be at leastRAPIDGZIP_AUTO_MIN_COMPRESSED_SIZE, and the data compresses 3:1 or less, since rapidgzip decodes in parallel. Otherwise the standard library decodes the stream. -
ON('on') –Always use it. Raises
PackageNotInstalledErrorwhen its package is not installed. -
OFF('off') –Never use it. The standard library decodes the stream. Set both
use_rapidgzipanduse_indexed_bzip2to this when results must not depend on the machine.
enabled_for(*, seekable, available, input_size=None, min_size=None)
Resolve the tri-state to "use the accelerator?".
ON always returns True (the caller checks availability and raises
PackageNotInstalledError if the package is missing — the user asked for it
explicitly; min_size is ignored). AUTO enables it only when seekable
is true and the package is available, so a missing package falls back silently.
seekable is the demand for the accelerator: declared seeking, or for the
DEFLATE family also a large sequential read judged worth it
(_sequential_read_pays in codecs/rapidgzip_select.py). When min_size
is set and input_size is known and strictly below that threshold, AUTO
also falls back (tiny members do not repay per-stream accelerator setup).
Unknown input_size keeps the pre-threshold AUTO behaviour.
archivey.ProcessIsolation
Bases: Enum
Which native decoders run in a child process, where a crash on hostile input costs the member being read instead of the caller's process.
A decoder in a child process whose process crashes raises
CorruptionError; one killed from outside raises
ResourceUnavailableError (SIGKILL, most often the out-of-memory killer)
or ReadError. Checksums and AES decryption always run in the
caller's process, and no setting bounds how long a decoder takes.
Members:
-
KNOWN_CRASHES('known_crashes') –The default: only the decoders seen to crash on some input run in a child process: rapidgzip (
use_rapidgzip,use_indexed_bzip2) and pyppmd (every PPMd member). Every other decoder runs in the caller's process. Processes are reused for later decoders. Measured on one Linux machine (scripts/bench_process_isolation.py), each PPMd decoder costs about 1.4 to 2 ms more than in-process: a non-solid 7z of 1024 PPMd members of 4 KiB took about twice as long. A ZIP member or a non-solid 7z member has a decoder of its own. A solid PPMd folder is one decoder for all its members: it pays that once, and about 0.1 to 0.35 ms for each 4 KiB member read from it; the decoded bytes cross between processes on top of that, at the copying costNATIVE_DECODERSgives for single streams. Where no child process can be started, a PPMd member raisesResourceUnavailableErrorand rapidgzip underAUTOfalls back to the standard library. -
NATIVE_DECODERS('native_decoders') –Every native decoder runs in a child process: zlib, bz2, lzma, zstd, lz4, brotli, inflate64, pyppmd and rapidgzip, including the trial decodes format detection runs.
Each open decoder has a process of its own, and processes are reused for later decoders. Measured on one Linux machine (
scripts/bench_process_isolation.py), a single stream (1 to 64 MiB for gzip and zstd, 1 to 16 MiB for bzip2 and xz) took about 1.05 to 1.5 times as long for bzip2, 1.1 to 1.6 times for xz, 1.4 to 2 times for gzip and deflate, and 2 to 6 times for zstd, mostly copying data between processes. Each decoder also costs about 0.6 to 1 ms (ZIP deflate and non-solid 7z LZMA2 members), and a PPMd decoder about 1.4 to 2 ms, as underKNOWN_CRASHES; an archive of many small members pays mostly that. A solid 7z folder is one decoder, plus about 0.1 to 0.3 ms for each 4 KiB member read from it, with the copying of larger members on top. An idle process holds about 9 MB. Where no child process can be started (a frozen application, archivey imported from a zip, a refused spawn), opening a decoder raisesResourceUnavailableError; nothing is decoded in the caller's process. -
NONE('none') –No child process at all: every decoder runs in the caller's process.
Not recommended for archives you do not trust: rapidgzip and pyppmd are isolated by default because they crash the process on some input, and here a crash takes the caller with it. It suits trusted files where speed matters, tests, and environments that cannot start processes. PPMd members decode in-process at any size, and rapidgzip is not used:
use_rapidgzipanduse_indexed_bzip2underAUTOuse the standard library, andONis refused withArchiveyUsageErrorwhen the config is built.
archivey.RarDecompressor
Bases: Enum
Which external program decompresses RAR member data.
The native parser lists a RAR archive without either program, except for a
compressed RAR 1.5-4 comment, which the selected program decodes (without it the
comment is None). Otherwise one of them is needed only to read a compressed
member, or an encrypted one archivey does not decrypt itself. Stored members are read
directly; a stored, encrypted member in one part is decrypted by archivey (AES, RAR
2.9 and later) when cryptography is installed, and by the program otherwise.
With UNRAR or UNAR, archivey never changes from one program to the other.
Members:
-
UNRAR('unrar') –RARLAB
unrar6.0 or later (or the RARLABrarwriter). It reads every RAR archive, including encrypted ones. -
UNAR('unar') –unar1.10 or later, from The Unarchiver's XADMaster library. It is free software, andbrew install unarinstalls it on macOS.It is not a full substitute: archivey refuses, before
unarruns, every read thatunaris known to get wrong. A password is passed onunar's command line, where other local users can see it while it runs. SelectingUNARwhenunaris not installed raisesPackageNotInstalledError, even whenunraris available. -
AUTO('auto') –The default:
unrarwhen a usable one is onPATH, otherwiseunar.The choice is made once per opened archive, when it is opened, and holds for every read of it: a read that
unarrefuses is not retried withunrar. One exception goes the other way: a member whose nameunrarcannot be given, or whose glob name also matches earlier members (rar_allow_glob_member_concatenation), is read withunarwhen an accepted one is installed, becauseunarselects members by index. That applies only to a read that needs no password, from a file with nothing before the RAR (sounargets no password and no copy), and to a memberunarreads correctly; otherwise theunrarrefusal stands.With neither installed, a read raises
PackageNotInstalledErrornamingunrar. On a machine withunarand no RARLAB program, a password is passed onunar's command line; selectUNRARto rule that out. -
NONE('none') –No program at all, for a caller who does not want
unrarorunarrun on its archives.Opening and listing work as with the others, and so does reading a stored (uncompressed) member that is not encrypted, also when it is split across volumes or sits in a solid archive, and one that is encrypted, in one part, with
cryptographyinstalled. A compressed member, an encrypted one split across volumes, or a split one with a volume missing, raisesUnsupportedFeatureErrorbefore anything runs. A stored, encrypted member in one part raisesPackageNotInstalledErrornamingcryptographywhen that package is missing. A compressed RAR 1.5-4 comment isNone. Theformat_availability()query takes no config and still runs theunraridentification probe; pass itcheck_programs=Falseto avoid that.
archivey.PasswordInput = str | bytes | Sequence[str | bytes] | PasswordProvider | None
module-attribute
Accepted password= shapes: one value, an ordered candidate list, a provider, or None.
archivey.PasswordRequest
dataclass
Context passed to a PasswordProvider when a password is needed.
Fields:
-
(memberArchiveMember | None) –The member being decrypted, or
Nonefor archive-level (header) decryption. -
(attemptint) –1 on the first ask for this unit; increments on every later ask for it.
Each ask follows a failure: either the previous answer failed to decrypt the unit, or it was a password that had already failed for this unit (a known-good password from an earlier unit, a listed candidate) and was skipped without a second try. A repeated answer is never decrypted twice. Asking stops when the provider returns
None, or after three answers in a row that it had already given for this unit; one or two repeats in a row do not stop it, so a person at a prompt who types the same password again is asked again. A provider walking a stored list should skip duplicates in it: three equal entries in a row end the asking.
archivey.PasswordProvider = Callable[[PasswordRequest], str | bytes | None]
module-attribute
Callable consulted when static password candidates fail for an encrypted unit.
Diagnostics
See Diagnostics.
archivey.Diagnostic
dataclass
One immutable advisory occurrence: something archivey noticed and reported instead of raising.
message and context carry the same facts for different readers.
message is display text, and its control characters are escaped: it includes
names from the archive, and a name carrying \x1b[2K\r could otherwise
erase the terminal line reporting it and write something else in its place.
context keeps the raw values, for matching on a name or for a structured log
(see to_dict()). ArchiveyError escapes its message
and keeps member_name raw for the same reason.
Fields:
-
(occurrence_idstr) –A random id, unique to this occurrence.
-
(codeDiagnosticCode) –What kind of event this is.
-
(messagestr) –A sentence describing the event, with control characters escaped.
-
(contextDiagnosticContext) –The event's details as typed fields with raw values, such as the archive and member names. Its
kindfield says which context class it is.
archivey.DiagnosticContext = NameNormalizationContext | NameEncodingContext | MemberNameControlsContext | UnusedArgumentContext | EmptyArchiveContext | UnconfirmedFormatContext | FormatConflictContext | ScanRaceContext | ArchiveEofContext | UnlistedDataContext | MemberTimestampContext | MemberHeaderRecordContext | SymlinkTargetContext | DigestContext | DirectoryDataContext | LinkDataContext | EncryptedVerificationContext | SeekIndexContext | StreamRewindContext | SelectorUnmatchedContext | SpecialFileDataContext | LocalHeaderContext | OffsetWrapContext | SizeWrapContext | UnknownMemberTypeContext
module-attribute
archivey.DiagnosticCode
Bases: StrEnum
Stable machine codes for advisory events.
The codes in ARCHIVE_INTEGRITY_CODES report the archive's own bytes or
metadata as anomalous, and DiagnosticPolicy.STRICT raises them. A diagnostic's
context carries the details, such as the member and the field involved.
Members:
-
MEMBER_NAME_NORMALIZED('member_name_normalized') –A member's stored name was changed to give
name, for example its separators or a.segment. -
MEMBER_NAME_ENCODING_INFERRED('member_name_encoding_inferred') –A member's name was decoded with an encoding archivey inferred, not one the archive or the caller declared.
-
MEMBER_NAME_BIDI_CONTROL('member_name_bidi_control') –A member's name or link target contains bidi controls, which change the order the text displays in and can make an
.exelook like a.png. -
MEMBER_NAME_ZERO_WIDTH_CHARACTER('member_name_zero_width_character') –A member's name or link target contains a zero-width space, word joiner or byte-order mark, so two different names can look the same.
-
MEMBER_NAME_INVISIBLE_CHARACTER('member_name_invisible_character') –A member's name or link target contains another invisible format character, such as a zero-width joiner or a soft hyphen. These occur in ordinary names, such as in emoji.
-
FORMAT_EXTENSION_CONFLICT('format_extension_conflict') –The file name's extension suggests one format and the content another. archivey goes by the content.
-
EXPLICIT_FORMAT_LISTED_EMPTY('explicit_format_listed_empty') –An archive opened with
format=listed no members. The context'sdetected_formatsays what detection finds in the content. -
EXTENSION_FORMAT_UNCONFIRMED('extension_format_unconfirmed') –A format chosen from the file name alone, with nothing in the content to confirm it, listed no members or failed to read.
-
PROBE_FORMAT_UNCONFIRMED('probe_format_unconfirmed') –A format found only by decoding a sample of the content failed to read, so the file may not be in that format.
-
EMPTY_ARCHIVE('empty_archive') –The listing finished without error and found no members.
-
ENCODING_ARGUMENT_UNUSED('encoding_argument_unused') –encoding=was passed for a format that decodes member names without it (7z, a directory, a single-file format), or for a format whose replacement reader (FormatPolicy(replace=)) does not apply one. -
PASSWORD_ARGUMENT_UNUSED('password_argument_unused') –A password was passed for a format with no encryption (TAR, ISO, a directory, a single-file format), or for a format whose replacement reader (
FormatPolicy(replace=)) takes no password. A password provider callable is not reported, since such a reader never calls it. -
SCAN_DIRECTORY_VANISHED('scan_directory_vanished') –A directory opened as an archive changed while it was read: a subdirectory listed earlier was gone.
-
SCAN_ENTRY_VANISHED('scan_entry_vanished') –A directory opened as an archive changed while it was read: an entry listed earlier was gone.
-
ARCHIVE_EOF_MARKER_MISSING('archive_eof_marker_missing') –The record that marks the end of the archive is missing or short, such as a TAR's two zero blocks or a ZIP's end of central directory record. The archive may be truncated.
-
ARCHIVE_TRAILING_DATA('archive_trailing_data') –Bytes other than zeros follow the end of the archive or of the compressed stream.
-
ARCHIVE_UNLISTED_DATA('archive_unlisted_data') –A ZIP read from a pipe holds a member its central directory does not list. It is left out of the listing, as a read from a file never sees it.
-
MEMBER_TIMESTAMP_INVALID('member_timestamp_invalid') –A stored timestamp could not be used, so that time is left unset.
-
MEMBER_HEADER_RECORD_SKIPPED('member_header_record_skipped') –An optional record in a member's header could not be read and was dropped, so the field it would fill is left unset rather than wrong. RAR also reports two faults here that drop no record, because they too are header damage a strict policy should refuse: bytes in a RAR5 header that belong to no record, and a RAR5 quick-open record holding FILE header copies that match no header the walk reached. The context's
recordtells them apart. -
SYMLINK_TARGET_UNAVAILABLE('symlink_target_unavailable') –A link's target could not be read: it is empty, too long, damaged, or needs a codec or package that is missing. The link stays listed with
link_targetunset, and the context'sreasonsays why. -
DIGEST_UNVERIFIABLE('digest_unverifiable') –A member's data cannot be checked against a stored checksum, so damage may go unnoticed.
-
MEMBER_DIRECTORY_DATA_IGNORED('member_directory_data_ignored') –A directory entry declares data, which extraction does not write. The directory is still created.
-
MEMBER_LINK_DATA_IGNORED('member_link_data_ignored') –A TAR symlink or hard link header declares data, which nothing reads. The link is listed and extracted as a link.
-
ENCRYPTED_MEMBER_UNVERIFIED('encrypted_member_unverified') –An encrypted member's stream was closed or seeked before its checksum confirmed the password, and nothing stronger had confirmed it, so the bytes read may come from a wrong password.
-
SEEK_INDEX_DEGRADED('seek_index_degraded') –Building a stream's seek index failed, was skipped or was thinned, so a seek may decompress more than usual.
-
STREAM_REWIND_REDECOMPRESSES('stream_rewind_redecompresses') –A backward seek decompressed at least 1 MiB again from an earlier point.
-
MEMBER_SELECTOR_UNMATCHED('member_selector_unmatched') –An entry of
members=matched no member of the archive. -
MEMBER_SPECIAL_FILE_HAS_DATA('member_special_file_has_data') –An entry stored as a device, FIFO or socket carries data, so it is listed as a
FILEwith that data, as unzip, 7-Zip and bsdtar extract it. -
MEMBER_LOCAL_NAME_DIFFERS('member_local_name_differs') –A ZIP member's local header stores another name than its central directory entry. The member keeps the central directory's name, as unzip and 7-Zip read it; a tool that trusts the local header shows the other one.
-
MEMBER_LOCAL_METHOD_DIFFERS('member_local_method_differs') –A ZIP member's local header names another compression method than its central directory entry. The member is decoded by the local header's method, the one a read from a pipe uses, and the data is still checked against its CRC-32.
-
MEMBER_LOCAL_CRC_USED('member_local_crc_used') –A ZIP member's central directory entry stores CRC-32 0 and its local header another value, which the data is checked against. Adobe AIR writes such files.
-
ARCHIVE_OFFSET_WRAPPED('archive_offset_wrapped') –A ZIP stores an offset past 4 GiB in a 32-bit field without ZIP64, as macOS Archive Utility does, and archivey corrected it.
-
MEMBER_SIZE_WRAPPED('member_size_wrapped') –A ZIP member's stored sizes are its real sizes modulo 2³², as macOS Archive Utility writes a member over 4 GiB without ZIP64. archivey corrects them, or lists
sizeasNoneuntil a read can tell. -
MEMBER_TYPE_UNKNOWN('member_type_unknown') –A member's stored type is one archivey does not know: a TAR typeflag, or the file-type bits of another format's mode (a format plugin may report it). One that declares data is listed as a
FILE, as GNU tar extracts it; one that declares none staysOTHER.
archivey.DiagnosticPolicy
dataclass
Which diagnostic codes to ignore and which to raise; every other code is collected.
Matching is by code only. Both arguments take any iterable of DiagnosticCode
(or their values as strings), and the fields always hold a frozenset; a code in
both is refused. A policy is built with set operations on the code sets, for example
DiagnosticPolicy(raise_on=ARCHIVE_INTEGRITY_CODES - {DiagnosticCode.X}).
Two ready-made instances:
DiagnosticPolicy.STRICTisDiagnosticPolicy(raise_on=ARCHIVE_INTEGRITY_CODES): RAISE on the codes about the archive's own bytes, COLLECT on everything else. It is the recommended strict mode because it is version-stable in the way that matters: a patch release may add new codes but adds none toARCHIVE_INTEGRITY_CODES, unless the new code reports damage archivey should already have caught. A minor release may add to the set, and each addition is a deliberate decision.DiagnosticPolicy.PEDANTICisDiagnosticPolicy(raise_on=frozenset(DiagnosticCode)): RAISE on every code, including the argument-hygiene and access-pattern ones.frozenset(DiagnosticCode)is every code of the installed version, so it raises on codes added after the caller wrote it.
Fields:
-
(ignorefrozenset[DiagnosticCode]) –Codes that are counted but not kept, logged or passed to
on_diagnostic. -
(raise_onfrozenset[DiagnosticCode]) –Codes that raise
DiagnosticRaisedErroronce recorded.
archivey.ARCHIVE_INTEGRITY_CODES = frozenset({DiagnosticCode.MEMBER_NAME_NORMALIZED, DiagnosticCode.MEMBER_NAME_ENCODING_INFERRED, DiagnosticCode.MEMBER_NAME_BIDI_CONTROL, DiagnosticCode.MEMBER_NAME_ZERO_WIDTH_CHARACTER, DiagnosticCode.EXTENSION_FORMAT_UNCONFIRMED, DiagnosticCode.SCAN_DIRECTORY_VANISHED, DiagnosticCode.SCAN_ENTRY_VANISHED, DiagnosticCode.ARCHIVE_EOF_MARKER_MISSING, DiagnosticCode.ARCHIVE_TRAILING_DATA, DiagnosticCode.ARCHIVE_UNLISTED_DATA, DiagnosticCode.MEMBER_TIMESTAMP_INVALID, DiagnosticCode.MEMBER_HEADER_RECORD_SKIPPED, DiagnosticCode.SYMLINK_TARGET_UNAVAILABLE, DiagnosticCode.DIGEST_UNVERIFIABLE, DiagnosticCode.MEMBER_DIRECTORY_DATA_IGNORED, DiagnosticCode.MEMBER_LINK_DATA_IGNORED, DiagnosticCode.SEEK_INDEX_DEGRADED, DiagnosticCode.MEMBER_LOCAL_NAME_DIFFERS, DiagnosticCode.MEMBER_LOCAL_METHOD_DIFFERS})
module-attribute
Codes reporting the archive's own bytes or metadata as anomalous.
The raise_on set of DiagnosticPolicy.STRICT. Fifteen codes are deliberately out,
and the reasons are part of the contract rather than an oversight:
EMPTY_ARCHIVE— an empty archive is legitimate, anddiagnosticsforbids treating zero members as an error.ENCODING_ARGUMENT_UNUSED/PASSWORD_ARGUMENT_UNUSED— argument hygiene. A pipeline that speculatively passes a password to every call would otherwise raise on every unencrypted archive.EXPLICIT_FORMAT_LISTED_EMPTY—format=is an override, and an override that halts the caller is not an override.STREAM_REWIND_REDECOMPRESSES— reports the caller's access pattern rather than the archive, and is most useful as a deliberately targeted tripwire.ENCRYPTED_MEMBER_UNVERIFIED— fires only when the caller abandons a member stream before EOF (extraction reads every member to EOF and never fires it). InSTRICTit would turn a peek at a ZipCrypto or RAR3/4 encrypted member intoDiagnosticRaisedError. Revisit when member streams report their verification state (dev-docs/IDEAS.md, "Verification state as data") and retire this code.PROBE_FORMAT_UNCONFIRMED— a probe-only identification is an advisory about what the file is (its bytes passed that format's content check), not a finding about the archive's own bytes, and it only accompanies a read that already failed with a typed error that carriesformat_unconfirmed=True. A policy that resolves it to RAISE still gets that typed error, throughescalate_as. Its siblingEXTENSION_FORMAT_UNCONFIRMEDis in the set: it also fires on an extension-only empty listing, a successful open that no byte confirmed.MEMBER_SELECTOR_UNMATCHED— reports the caller'smembers=argument, not the archive. A job that passes one fixed list of names to many archives would otherwise raise on every archive that lacks one of them.MEMBER_SPECIAL_FILE_HAS_DATA— a documented writer option (Info-ZIPzip -FI) produces the shape, and the member's bytes are delivered and checked like any file's, so nothing is hidden;STRICTwould refuse an ordinary archive (DR-25).MEMBER_NAME_INVISIBLE_CHARACTER— zero-width joiners are part of ordinary emoji sequences, non-joiners of Persian and Indic spelling, and tag characters of subdivision flags.STRICTwould refuse an archive of legitimately named files. Its siblingMEMBER_NAME_ZERO_WIDTH_CHARACTERis in the set: U+200B, U+2060 and U+FEFF have no use inside a filename.FORMAT_EXTENSION_CONFLICT— reports the file's name, not its bytes: the bytes win and the archive reads in full. A tarball saved asb.gzis an ordinary mistake, andSTRICTwould refuse a well-formed archive over it. The code is still recorded.MEMBER_LOCAL_CRC_USED— a common packager (Adobe AIR) writes the shape, and the data is still checked, against the local header's CRC-32, so nothing is left unverified. Its siblingsMEMBER_LOCAL_NAME_DIFFERSandMEMBER_LOCAL_METHOD_DIFFERSare in the set: there a tool that trusts the other header shows the member under another name, or decodes other bytes.ARCHIVE_OFFSET_WRAPPED— macOS Archive Utility (Finder's Compress) writes a ZIP past 4 GiB this way by default and reads it back, and a correction is kept only when a local header with the member's name is there, with the CRC-32 still checked.STRICTwould refuse an ordinary Mac archive.MEMBER_SIZE_WRAPPED— the same Mac archives: a member past 4 GiB, or a deflated one over about 4 MB compressed in an archive of any length, whose size only a read can tell. A size is corrected only where the layout or deflate's ratio forces it, and an unknown one isNone, with the length and CRC-32 checked on a read.STRICTwould refuse ordinary Mac archives.MEMBER_TYPE_UNKNOWN— a TAR typeflag archivey does not know. GNU tar extracts such a member as a regular file with a warning, and archivey lists its data as aFILE, so nothing is hidden.STRICTwould refuse an archive GNU tar extracts.
archivey.DiagnosticSummary
dataclass
Immutable point-in-time snapshot of diagnostic counts and retained detail.
Fields:
-
(total_countint) –Every diagnostic emitted in the summary's scope, ignored ones included.
-
(countsMapping[DiagnosticCode, int], default:mappingproxy({})) –The exact count for each code, ignored ones included.
-
(retainedtuple[Diagnostic, ...], default:()) –The diagnostics kept in full, in the order they were emitted. Ignored codes are not kept, and nor is anything past the
ArchiveyConfig.max_retained_diagnostic_referencesbudget. -
(dropped_countint, default:0) –How many of
total_countare not inretained.
archivey.OnDiagnostic = Callable[[Diagnostic], None]
module-attribute
Optional synchronous callback invoked for COLLECT/RAISE diagnostics.
Errors
archivey's exceptions have two roots. ArchiveyError covers problems with the archive
or its environment. ArchiveyUsageError covers mistakes in the calling code and is
deliberately outside that tree, so except ArchiveyError does not hide them. The
entries below follow the class tree: each group starts with its base class, except the
group from ResourceLimitError to DiagnosticRaisedError. Those four are direct
subclasses of ArchiveyError and unrelated to each other, and only ResourceLimitError
has a subclass, ResourceUnavailableError, listed after it.
Errors and diagnostics explains which one to catch.
archivey.ArchiveyError
Bases: Exception
Root of all Archivey exceptions.
The message is escaped. Call sites build messages by interpolating
archive-derived text — a member name, or a destination path built from one — and
those are attacker-controlled. An exception message reaches a terminal by more
routes than any one consumer controls: print(e), logging.exception, a
third-party error reporter, and above all an uncaught exception whose traceback
the interpreter prints itself, whose final line is str(e). Escaping in the
handler that displays it protects only the routes someone remembered to
configure; escaping here protects all of them, with no configuration.
So message is stored escaped, and that escaped form is what
str(), args[0] and repr() all render. raw_message keeps the
text as the call site wrote it, for the one job the escaped form cannot do: being
embedded in another message that will escape it in turn.
archive_name, member_name, link_target and source_format stay
raw too, for callers that need the real value to act on rather than to print.
str() renders the names through !r, which escapes them for display in
turn — so a name available as an attribute should not also be interpolated into
the message, or it prints twice. Prefer prose plus attributes:
FilterRejectionError("Symlink target escapes destination", member_name=name,
link_target=target).
format_unconfirmed is a boolean (default False): True when the format
claim rested only on a content probe with nothing corroborating it (no matching
extension, no inner-TAR upgrade), or only on the file extension because every
content signal declined, so a decode failure should not be read as "this known
format truncated." Confidence is irrelevant to the flag.
Escape exactly once, at the outermost message. Everything a message interpolates should therefore be raw when it goes in — which is what the two helpers are for, and why neither of them is a matter of taste:
- a member name, link target or path →
quoted(), not!r.!rescapes first, and this escapes the backslashes it introduced. - a caught exception that might be one of ours →
exception_detail(), not{exc}. It gives the unescaped text of one of ours (asraw_message_of()does), and the type name of an exception that has no text. A handler that catches only a third-party decoder's type can use{exc}. - never
{exc!r}for a caught exception: it shows Python's spelling (error('unpack_from requires …')), not what is wrong with the archive, and escapes one of ours a second time. When the exception's own text does not describe the archive (struct.error,IndexErrorfrom a parser), write that description instead and keep the original as__cause__.tests/test_escaping.pychecks this rule.
Note this is the opposite of the rule for logger.* calls, whose records the CLI
does not escape: there, %r is what makes an interpolated name inert and must
stay.
archivey.OpenError
Bases: ArchiveyError
Reading could not start: the source is not a recognized archive, is not seekable where the format needs it, or a volume file cannot be opened.
A recognized archive whose header is damaged, cut short or encrypted raises a
ReadError subclass instead, from open_archive() as from any later call.
archivey.FormatDetectionError
Bases: OpenError
Could not detect an archive format, or detected one this call cannot use.
The second case is open_stream() on a container such as a ZIP: detection
succeeded, but the source is not a single-file compressed stream.
archivey.StreamNotSeekableError
archivey.ReadError
Bases: ArchiveyError
The archive's data is bad or cannot be read, at open or later.
Raised directly when an external decoder or a child process fails without saying why; the subclasses name the common causes.
archivey.CorruptionError
Bases: ReadError
The archive's bytes are damaged: a checksum mismatch, a bad header or data block.
Data that ends early is damage too, so TruncatedError is a subclass and
except CorruptionError catches it.
archivey.TruncatedError
Bases: CorruptionError
Damage that looks like the data ending early.
A best-effort label, not a diagnosis: damage that decodes short can raise it, and some
cut-short headers raise a plain CorruptionError. Do not branch on it.
archivey.EncryptionError
archivey.LinkTargetNotFoundError
archivey.LinkTargetNotFoundReason
Bases: StrEnum
Why a link has no member to follow to: LinkTargetNotFoundError.reason.
Members:
-
NOT_RECORDED('not_recorded') –The archive records no target for this link. Not damage: a writer such as 7-Zip stores some links this way, and
ArchiveMember.link_target_unrecordedisTruefor them. -
UNREADABLE('unreadable') –The archive records a target, and this read could not produce it: the data holding it is damaged, encrypted, or out of reach. A diagnostic on the member says which.
-
UNRESOLVED('unresolved') –The target is known, but following it reaches no member that holds data: it names no member of the archive, or one the link may not use (a symlink target that leaves the archive root, a hardlink to a later member), or the chain of links loops.
archivey.ExtractionError
archivey.FilterRejectionError
Bases: ExtractionError
A safety check refused to write the member.
One type for every check: a path that escapes the destination (.. or an absolute
path), a symlink that resolves outside it, a device node, FIFO or socket, a name the
destination OS cannot store safely under the active policy (see safe-extraction),
and a name built to display as something it is not (a Unicode bidi override or
isolate). The member is BLOCKED whichever check fired, and the message says which.
archivey.NameCollisionError
Bases: ExtractionError
A member resolved to a destination another member of this run already claimed.
Raised only when the caller opted in with AbortOn.NAME_COLLISION; without that
opt-in a collision is not an error at all — OverwritePolicy resolves it and both
members' ExtractionResult records the outcome. It is raised for every
non-TRUSTED collision regardless of how the policy would have resolved it, since
the trigger is the collision itself, not its resolution.
archivey.NameRewrittenError
Bases: ExtractionError
A member name was rewritten: to its portable spelling, or re-rooted inside the destination.
Raised only when the caller opted in with AbortOn.NAME_SANITIZED — a narrow
escape hatch for callers who require the on-disk name to match the archive's byte
for byte. Without the opt-in the rewrite is a success, recorded as
ExtractionResult.presented_name.
archivey.ResourceLimitError
Bases: ArchiveyError
A configured resource limit was exceeded, or the system did not give a resource.
Covers ListingLimits listing caps,
parse-time max_members on ZIP, 7z, RAR and ISO,
ExtractionLimits bomb guards,
DecoderLimits caps on archive-declared decoder memory
and key-derivation work, and SpoolLimits, the cap on
copying a stream source to temporary storage; opening the archive from a path
avoids that copy. A shortage of the system rather than a configured limit raises
the subclass ResourceUnavailableError.
Sibling of ExtractionError (not a subclass): these are not filter/path
failures.
archivey.ResourceUnavailableError
Bases: ResourceLimitError
The system did not give a resource the read needed; no configured limit was exceeded.
Raised when a decoder or helper program is ended by SIGKILL (most often the
out-of-memory killer), when a decoder that
ProcessIsolation runs only in a child process cannot
have one (a frozen application, a refused spawn), and when a PPMd decoder process
cannot allocate the member's model (a container memory limit). The data may be
valid: read it again with more memory, or where child processes can start. Raising
a limit in the config does not help.
A subclass of ResourceLimitError, so code that catches that type catches
this one too.
archivey.UnsupportedFeatureError
Bases: ArchiveyError
The archive is recognized, but uses something archivey cannot handle.
A variant, codec or layout of a known format (a raw CD sector image, a UDIF disk
image, a 7z coder graph that is not a tree of chains, a multi-volume set where the
format has none),
or a request this archive or backend cannot serve (a password unrar cannot be
given). The problem is the archive, not the calling code: that raises
ArchiveyUsageError.
It is also raised for a format the caller's FormatPolicy
(ArchiveyConfig.formats) does not allow. source_format is that format, and
config.formats.allows(exc.source_format) tells this case apart.
An unknown compression method, codec or version number read from a header with no checksum (a ZIP method, for example) may also mean that header is damaged. Archivey reports it as unsupported because it cannot tell the two apart.
A refused seek() or tell() on a member stream is not this class: a member
stream has to keep behaving like a file object, so it raises
io.UnsupportedOperation (a subclass of OSError and ValueError).
archivey.PackageNotInstalledError
Bases: ArchiveyError
A required optional package or external tool is absent.
Raised both when a whole format needs it (opening an ISO without pycdlib) and
when one member does (a PPMd member without pyppmd). The message names what to
install.
archivey.DiagnosticRaisedError
Bases: ArchiveyError
A diagnostic was escalated to an error via DiagnosticPolicy.
Always-stop: extraction MUST NOT catch this as a per-member failure under
OnError.CONTINUE. Carries the escalated Diagnostic.
archivey.ArchiveyUsageError
Bases: Exception
Caller misuse of the Archivey API — deliberately not an ArchiveyError.
except ArchiveyError wraps archive/environment problems; usage errors indicate
a bug in calling code and must not be swallowed by those handlers.
It covers what the argument types alone cannot rule out: calling a method the
reader's mode forbids (members() on a streaming=True reader), using a closed
reader, opening a second overlapping member stream without
concurrent_operations=True (the message names the open_archive() call site),
and driving the reader from inside a diagnostic callback. Those raise this class
itself.
A wrong argument raises it too, as a private subclass that is also a
TypeError (an argument of a type the call cannot use: config="strict",
open_archive(0)) or a ValueError (a usable type with a value the call
refuses: an unknown format spelling, a negative limit). So except TypeError,
except ValueError and except ArchiveyUsageError all catch a bad argument.
The subclasses are not public names; catch the builtin or this class.
The message is escaped on the same terms as ArchiveyError's. A usage
error's text is mostly archivey's own, so the escaping is usually a no-op — but
"mostly" is not a property worth carving an exception into, and a usage error is
free to name the member that provoked it.
refused_member_type marks one case by type rather than by message. It is the
type of the member open() / read() refused because it is not a file
(DIRECTORY, ANTI or OTHER), and None on every other usage error.
When the caller opened a link, it is the type of the member the link resolved to:
a symlink to a directory gives MemberType.DIRECTORY.
Access cost
See Solid archives for what these mean for your reads.
archivey.CostReceipt
dataclass
Machine-readable description of an opened archive's access costs.
The three axes are orthogonal and must not be conflated: listing_cost is about
enumeration, access_cost about the format layout, and stream_capability about
the source bytes. See the access-mode-and-cost capability spec for the full model.
Fields:
-
(listing_costListingCost) –Cost of enumerating all members.
-
(access_costAccessCost) –Cost of reading one member's data, given the format layout.
-
(stream_capabilityStreamCapability) –Seekability of the underlying source bytes.
-
(solid_block_countint | None, default:None) –Number of distinct solid blocks (each one decompress pass), or
Nonewhen not applicable / unknown.is_solidlives onArchiveInfo, not here, to avoid duplicating the flag. -
(notestuple[str, ...], default:tuple()) –Human-readable caveats about the cost figures.
archivey.ListingCost
Bases: Enum
How expensive it is to enumerate all members (list names + metadata).
The value names how the member list is read through the source the archive was
opened on: from an index, by walking member headers, or by decompressing. It does
not count the work of finding the index: a ZIP whose end record sits behind a long
tail is walked to find its central directory and is still INDEXED. Nor does it
say when the work is paid: a backend may do it inside open_archive and serve
members() from memory (RAR). One format can need different work on different
sources: a ZIP read from a pipe is REQUIRES_SCANNING. The value is set per
backend and source, so a damaged archive listed by a fallback walk (a ZIP whose
directory is gone) keeps its backend's value.
Members:
-
INDEXED('indexed') –Members can be listed without scanning header-to-header or decompressing payload.
Examples: ZIP central directory, 7z header, ISO directory tree at open, and a single-file compressed stream (gzip, xz, …), whose one member is known from the header without decompressing. A filesystem directory is not indexed (its walk is
REQUIRES_SCANNING). -
REQUIRES_SCANNING('requires_scanning') –No index, but members can be enumerated by seeking/scanning header-to-header without decompressing payload (e.g. an uncompressed tar, a RAR archive, or a filesystem directory walk).
RAR walks its headers inside
open_archive, somembers()then returns from memory. -
REQUIRES_DECOMPRESSION('requires_decompression') –The stream must be decompressed to reach the member headers (e.g. a compressed tar).
archivey.AccessCost
Bases: Enum
How expensive it is to read one member's data, given the format layout.
Members:
-
DIRECT('direct') –Any member can be read without touching other members.
-
SOLID('solid') –Reading member N may require decompressing earlier members in its solid block.
archivey.StreamCapability
Bases: Enum
A property of the underlying source bytes, independent of the format layout.
Ordered by strength, weakest first: FORWARD_ONLY < SEEKABLE. There is only
one possible direction — a seekable source can serve every read a forward-only one
can — so the comparison means "is at least as strong as", not a preference. That is
what lets a requirement stated as a capability (FormatAvailability.required_source)
be tested against a source's actual capability (CostReceipt.stream_capability)
with <= instead of a lookup table.
ListingCost and AccessCost are deliberately not ordered: their members name
kinds of work, not strengths of one resource.
Members:
-
SEEKABLE('seekable') –The source supports arbitrary
seek(); positions can be revisited. -
FORWARD_ONLY('forward_only') –Non-seekable source (pipe/socket): it cannot be rewound at all. Re-reading any earlier position requires a brand-new stream.
Detection budgets
These live in the archivey.detection_cost module and are not re-exported from archivey. A budget
bounds what detect_format and open_archive may read and decode before
they answer. You set it with ArchiveyConfig(detection_budget=...), and BALANCED_BUDGET is the
default. The receipt on FormatInfo.cost_receipt says what detection
actually did.
archivey.detection_cost.DetectionBudget
dataclass
Upper bounds on what detection may spend.
Content-probe header reads at an offset are not a budget field: the Brotli chain walk
caps them itself, at CHAIN_MAX_LINKS (8) header reads of 24 bytes. The Brotli
chain decode that follows reads [0, end): it is charged to max_decode_input
and runs only when end is within the prefix/far/scan ceiling.
Fields:
-
(max_prefix_bytesint) –Bytes detection may read from the start of the source.
-
(max_far_bytesint) –Bytes the far-magic tier may read, for a signature at a fixed offset past the prefix. Separate from
max_prefix_bytesbecause ISO'sCD001at 32 769 needs a window of about 32 KiB that a 4 KiB prefix budget would forbid. -
(max_scan_bytesint) –How far into the source the scan for an archive behind a prefix (an SFX stub) looks for a signature. A candidate found there is judged on its whole header, which may extend past the window by up to 132 KiB or
max_scan_bytes, whichever is smaller; a header that does not fit is rejected, andsfx_scanrecorded as cut short. -
(max_decode_inputint) –Compressed bytes decoding may consume, one allowance for the whole call, shared by the content probes, the Brotli chain decode, their completion check and the inner-TAR probe.
-
(max_decode_outputint) –Decoded bytes the inner-TAR probe may produce. A content probe's output is bounded by the codec's own drain instead: 4 KiB, 64 KiB with the whole source in hand, and the sample length (up to 1 MiB) for the Brotli chain decode. That output is not charged, as it is mostly a copy of input already charged to
max_decode_input. -
(completion_window_bytesint) –The largest source a content-probe hit is re-checked against in full;
0turns the check off.
archivey.detection_cost.DetectionBudgetPreset
Bases: Enum
Named detection budgets. BALANCED is the detect_format default.
Members:
-
BALANCED('balanced') -
FAST('fast') -
THOROUGH('thorough')
archivey.detection_cost.BALANCED_BUDGET = DetectionBudget(max_prefix_bytes=4096, max_far_bytes=_ISO_FAR_BYTES, max_scan_bytes=_SFX_SCAN_BYTES, max_decode_input=_INNER_TAR_DECODE, max_decode_output=_INNER_TAR_DECODE, completion_window_bytes=_COMPLETION_WINDOW)
module-attribute
archivey.detection_cost.FAST_BUDGET = DetectionBudget(max_prefix_bytes=4096, max_far_bytes=_ISO_FAR_BYTES, max_scan_bytes=256 * 1024, max_decode_input=64 * 1024, max_decode_output=64 * 1024, completion_window_bytes=0)
module-attribute
archivey.detection_cost.THOROUGH_BUDGET = DetectionBudget(max_prefix_bytes=4096, max_far_bytes=_ISO_FAR_BYTES, max_scan_bytes=_SFX_SCAN_BYTES, max_decode_input=_INNER_TAR_DECODE, max_decode_output=_INNER_TAR_DECODE, completion_window_bytes=_INNER_TAR_DECODE)
module-attribute
archivey.detection_cost.DetectionCostReceipt
dataclass
Measured detection work — charged as reads happen, not reconstructed afterwards.
Fields:
-
(prefix_bytesint, default:0) –Sum of range lengths requested via
peek_range(overlapping peeks accumulate).Not comparable 1:1 with
max_prefix_bytes: seek-basedread_atdoes not charge here, and growing peeks bill each request in full. Preferunique_bytes_readfor "how much did we fetch from the source". -
(unique_bytes_readint, default:0) –Bytes fetched from the source.
A forward read of the prefix counts each of those bytes once. The trailer read counts its block as well, including when a later tier then reads the same bytes as part of the prefix.
-
(far_bytesint, default:0) –Bytes the far-magic tier peeked. Bounded by
max_far_bytes. -
(scanned_bytesint, default:0) –Bytes the scan for an archive behind a prefix looked through. Bounded by
max_scan_bytes. -
(decode_inputint, default:0) –Compressed bytes decoded. Bounded by
max_decode_input. -
(decode_outputint, default:0) –Decoded bytes the inner-TAR probe produced. Bounded by
max_decode_output. -
(passesint, default:1) –Detection passes this receipt sums, each run under the full budget.
2 when
detect_formatfollowed a stub-only executable to its sibling split volume: the stub's pass and the volume's pass.
archivey.detection_cost.TierSkip
dataclass
A tier that detection did not run, with the reason.
Fields:
-
(tierstr) –The tier's name.
-
(reasonTierSkipReason) –Why it did not run.
archivey.detection_cost.TierSkipReason
Bases: Enum
Why a detection tier did not run.
Distinct reasons matter: NOT_ENABLED_BY_POLICY does not make the search incomplete,
while CAPABILITY_UNAVAILABLE and BUDGET_EXHAUSTED do.
Members:
-
NOT_ENABLED_BY_POLICY('not_enabled_by_policy') -
CAPABILITY_UNAVAILABLE('capability_unavailable') -
BUDGET_EXHAUSTED('budget_exhausted')
Display and naming helpers
These live in the archivey.terminal and archivey.paths modules and are not
re-exported from archivey. archivey's own command-line tool is built on them.
The archivey.terminal helpers are for showing archive-derived text, such as member
names, to a person without letting it control the terminal (Names in other
encodings).
archivey.terminal.escape_control_chars(text)
Backslash-escape non-printable characters in attacker-controlled text.
Rendering of each escape is delegated to repr(), whose escape set is exactly
not str.isprintable() (verified across the whole code space) and whose spellings
— \n, \x1b, \u202e, \U0001d173 — are the ones a Python developer
already reads. Only two characters are handled here: backslash, which repr would
escape but which we must escape ourselves since we are not emitting quotes, and a
surrogateescaped byte, which is the one place we deliberately differ.
A surrogateescaped byte renders as the underlying octet (\x80) rather than
repr's \udc80: the name never contained U+DC80: that code point is an
artifact of decoding a byte that was not valid in the declared encoding, and the
operator cares about the byte.
Printable characters pass through, so ordinary café and 日本語.txt member
names stay readable — which rules out ascii() and the unicode_escape codec,
and str.encode(errors="backslashreplace") is unusable here for the opposite
reason: error handlers fire only on unencodable characters, so \x1b — the byte
this exists to defend against — would pass through raw.
Backslash is escaped, without which the form would be ambiguous: a member literally
named a\nb would render identically to one containing a newline. The cost is
that a native Windows path passed through this doubles its separators; callers
holding a path should render it /-separated first.
Not fully lossless. A real C1 character and a surrogateescaped byte of the same
value collide: both U+009B and U+DC9B render as \x9b, as do U+00A0
and byte 0xA0. Distinguishing them needs a separate notation for
surrogateescaped bytes; the guarantee here is that the output is inert and that
no character is silently dropped, not that the input is uniquely recoverable.
archivey.terminal.display_path(path)
Render a filesystem path for a message, /-separated.
Escaping doubles backslashes, so a native Windows path interpolated raw comes out
as C:\\Users\\out\\a.txt — lossless, but harder to read and to paste back.
Rendering /-separated first leaves the escape nothing to double, and any
backslash that does survive into the output is then a character in a name, which
is exactly what should be escaped.
This is the message-side counterpart of the rule the CLI's print sites already
follow (cli spec: member-derived paths are rendered relative and /-separated
before escaping).
archivey.terminal.quoted(text)
Delimit archive-derived text in a message without escaping it.
f"...{name!r}..." in a message would escape the name, and then the message's own
escaping (ArchiveyError, Diagnostic) would escape the backslashes repr introduced,
rendering a hostile name doubly-escaped: EV\\x1b[2KIL where EV\x1b[2KIL was
meant. Quoting here and letting the message escape once produces the delimiters
without the doubling.
Use this — not !r — for member names, link targets and paths interpolated into a
message that escapes itself. !r remains correct for values that are not
archive-derived (a format, a code, an exception type) and for logger.* call
sites, whose records are not escaped by the CLI and where %r is what makes an
interpolated name inert.
The delimiter is chosen, the way repr chooses it, rather than escaped: a
name may legitimately contain a quote, and 'it's' leaves a reader unable to see
where the name ends. Choosing costs nothing, whereas escaping the quote would write
a backslash for the message escape to double — the failure this helper exists to
avoid. A name containing both quote characters falls back to ' and stays
ambiguous; that tail is unreachable without escaping.
archivey.paths holds the naming rules extraction applies to destination paths, for a
front end that moves or renames what extraction wrote and wants the names a direct
extraction would have chosen.
archivey.paths.numbered_name(name, n, *, is_dir)
Return name with the counter n added, as OverwritePolicy.RENAME spells it.
When a member's destination is taken, OverwritePolicy.RENAME writes it under the
first free name of this form, counting from n=1. A file keeps its extension:
the counter goes before the final suffix. A directory has no suffix, so the counter
goes after the whole name.
name is a single path component, not a path: only its final suffix moves.
numbered_name("photo.jpg", 1, is_dir=False) 'photo (1).jpg' numbered_name("notes.tar.gz", 2, is_dir=False) 'notes.tar (2).gz' numbered_name("photos.2024", 1, is_dir=True) 'photos.2024 (1)'
Parameters:
-
name(str) –The name that is taken.
-
n(int) –The counter,
1or more. -
is_dir(bool) –Whether
nameis a directory.
Returns:
-
str–The candidate name. This function does not look at the filesystem: the caller
-
str–checks whether the candidate is free and tries
n + 1when it is not.
It also holds two functions that turn patterns into a members= argument, for
selecting by pattern where members= itself matches names exactly. They read the same
pattern differently: match_glob keeps * inside one folder, as archivey's CLI does,
and match_tar_glob follows tar, where * also matches / (Extracting all or some
members).
archivey.paths.match_glob(*patterns, with_contents=False)
Return a members= predicate that selects the members matching any of patterns.
with archivey.open_archive("site.zip") as archive: # doctest: +SKIP ... archive.extract_all("out/", members=match_glob("docs/**", "*.md"))
The patterns are matched against the whole member name, /-separated, and
case-sensitively:
*matches any run of characters inside one folder:*.mdselectsREADME.mdbut notdocs/guide.md.?matches one character other than/.[abc],[a-z]and[!abc]match one character from, or not from, the set, never/.[*]matches a literal*.**as a whole path component matches any number of folders, including none:**/*.mdselectsREADME.mdanddocs/api/guide.md, anddocs/**selects thedocs/folder and everything under it. Elsewhere,**is*.
* and ? match a leading . too, so hidden files are not skipped.
A directory member is matched by its name without the trailing /, so docs/*
selects the folder docs/api/ as well as the file docs/index.md. A pattern that
ends in / selects only directories: */ selects every top-level folder.
With with_contents=True, a member is also selected when a folder it is in
matches, so a folder comes with everything under it: match_glob("docs",
with_contents=True) selects docs/ and docs/api/guide.md, and
match_glob("*/", with_contents=True) selects everything that is in a folder.
archivey's command-line tool matches its patterns this way.
A \ in a pattern is an ordinary character, on Windows too, because member names
use / and a TAR member name can contain a \.
A pattern that matches nothing is not reported: MEMBER_SELECTOR_UNMATCHED covers
only the names in the collection form of members=.
For tar's rules, where
* also matches / and docs selects the folder and everything in it, use
match_tar_glob(): match_tar_glob("*.md") selects .md files at every
depth, while match_glob("*.md") selects top-level ones only.
Parameters:
-
*patterns(str, default:()) –One or more glob patterns. A member is selected when any of them matches.
-
with_contents(bool, default:False) –Also select every member inside a matching folder.
Returns:
-
Callable[[ArchiveMember | str], bool]–A callable that takes an
ArchiveMemberand returns whether it -
Callable[[ArchiveMember | str], bool]–is selected. It also takes a member name as a
str, where a trailing/ -
Callable[[ArchiveMember | str], bool]–marks a directory.
Raises:
-
ArchiveyUsageError–No pattern was given, a pattern has an empty component (
"","/","a//b"; a leading/is allowed), or a[...]set is malformed, such as the reversed range[z-a]. It is also aValueError.
archivey.paths.match_tar_glob(*patterns)
Return a members= predicate that matches patterns the way tar does.
with archivey.open_archive("site.tar") as archive: # doctest: +SKIP ... archive.extract_all("out/", members=match_tar_glob("docs", "*.md"))
Each pattern is matched against the whole member name with
fnmatch.fnmatchcase, so it is case-sensitive and:
*matches any run of characters,/included:*.mdselectsREADME.mdanddocs/api/guide.md.**is the same as*.?matches any one character,[abc]/[!abc]one character from, or not from, the set.- A pattern that names a folder, with or without its trailing
/, selects the folder and everything under it:docsanddocs/both selectdocs/anddocs/api/guide.md, but not a file nameddocs.txt.
For patterns where * stays inside one folder, use match_glob(). A \ in
a pattern is an ordinary character, on Windows too. A pattern that matches nothing is
not reported.
Parameters:
-
*patterns(str, default:()) –One or more patterns. A member is selected when any of them matches.
Returns:
-
Callable[[ArchiveMember | str], bool]–A callable that takes an
ArchiveMember, or a member name as a -
Callable[[ArchiveMember | str], bool]–str, and returns whether it is selected.
Raises:
-
ArchiveyUsageError–No pattern was given, or a pattern has an empty component (
"","/","a//b"; a leading/is allowed). It is also aValueError.