Skip to content

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_archive(
    source: OpenSourceInput,
    *,
    format: ArchiveFormat | str | None = None,
    streaming: bool,
    seekable_members: bool = False,
    concurrent_operations: bool = False,
    password: PasswordInput = None,
    encoding: str | None = None,
    config: ArchiveyConfig | None = None,
) -> ForwardArchiveReader

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 random open() works. Without it, seek() raises io.UnsupportedOperation. A backward seek may re-decompress from the start when there is no index or accelerator. A stream_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: random open() / read() / get() / members(), iteration, stream_members() passes and extract_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, and close() waits for calls in flight. Overlap is correct, not always cheaper. Without it, a second overlapping open(), or a random-access call or another pass beside a running pass, raises ArchiveyUsageError; 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.formats does not allow the format.

  • PackageNotInstalledError –

    The format needs an optional package that is not installed, such as pycdlib for ISO.

  • ResourceLimitError –

    The archive exceeds a limit that is checked at open, such as ListingLimits max_members while a ZIP, 7z, RAR or ISO index is parsed, or DecoderLimits for data the open decodes.

  • DiagnosticRaisedError –

    A diagnostic that the DiagnosticPolicy in 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 TypeError or ValueError), or streaming=True is combined with concurrent_operations=True.

  • FileNotFoundError –

    The path does not exist. Other OSError from 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=True and the source cannot seek.

  • ResourceLimitError –

    The stream asks a decoder for more memory than DecoderLimits allows, raised by the read of the returned stream that starts the decoder.

  • UnsupportedFeatureError –

    config.formats does not allow the format.

  • PackageNotInstalledError –

    The format needs an optional package that is not installed.

  • DiagnosticRaisedError –

    A diagnostic that the DiagnosticPolicy in 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 TypeError or ValueError): a directory path, or a container format passed as format=.

  • 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:

archivey.FormatInfo dataclass

The result of detect_format() — the detected format plus how sure we are.

Fields:

  • format

    (ArchiveFormat) –

    The detected format.

  • confidence

    (DetectionConfidence) –

    How far the evidence behind format goes.

  • detected_by

    (DetectedBy) –

    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_offset

    (int, default: 0 ) –

    Where the archive starts in the source. Nonzero only for a self-extracting archive, so payload_offset > 0 is the test for one.

  • diagnostics

    (DiagnosticSummary, default: DiagnosticSummary(total_count=0, counts=mappingproxy({}), retained=(), dropped_count=0) ) –

    What detection reported on its way to the answer.

  • corroborated

    (bool, 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". False also means "not a probe at all", so an exact magic hit reads False too. A later release may replace this field with a record of the evidence that separates the two cases.

  • cost_receipt

    (DetectionCostReceipt | 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 when detect_format() followed a stub to its split volume.

    detect_format() always fills it; a directory gets the zero receipt. None only on a FormatInfo that detection did not produce.

  • unavailable_tiers

    (tuple[TierSkip, ...], default: () ) –

    The detection tiers that did not run, each a TierSkip with its TierSkipReason: not enabled by policy, a capability the source lacks, or the budget ran out. Empty when every tier ran.

  • from_plugin

    (bool) –

    (computed property) Whether format is 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:

  • format

    (ArchiveFormat) –

    The format this answer is about.

  • support

    (FormatSupport) –

    How much of the format can be read.

  • missing

    (tuple[MissingComponent, ...], default: () ) –

    What to install to read more of the format. Empty when support is FULL.

  • required_source

    (StreamCapability, default: StreamCapability.SEEKABLE ) –

    The weakest source shape this format can be read from.

    StreamCapability is ordered, so this is a minimum: a source is strong enough when availability.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 a StreamNotSeekableError to catch.

    Independent of support — a format whose optional dependency is missing still answers the question. The SEEKABLE default 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:

  • name

    (str) –

    The package or program, such as "pycdlib", "brotli" or "unrar".

  • install_hint

    (str) –

    For a package, the command that installs it, such as "pip install archivey[recommended]". For a program such as unrar, the whole message a read raises without it.

  • unlocks

    (tuple[str, ...], default: () ) –

    The codecs or features it enables, such as ("ppmd",).

  • reason

    (str, 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.

  • advice

    (str) –

    (computed property) What a report of this component says to do: reason when set, otherwise install_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 (check error is None for 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, raise report.error yourself.
  • members_report_if_available() — never scans; None if 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, as archivey.detect_format() would report it.

    The result of the detection the open ran, kept rather than repeated: its confidence, detected_by and payload_offset describe this reader's source. A directory reports DIRECTORY / CERTAIN / "directory". None when format= 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:

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:

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:

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:

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 (TruncatedError when 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 DecoderLimits allows, or a scan for the name exceeds ListingLimits.

  • DiagnosticRaisedError –

    A diagnostic that the DiagnosticPolicy in 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:

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's ZipExtFile does, so open_archive(reader.open(member)) can match the member's extension during detection (.tar.br and 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, as io.BytesIO does.

  • 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, and None otherwise: a BGZF file reports None until a seek or a read finds its size. A stream from stream_members() that has not opened yet reports None rather 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.

    KEY once the stream has opened its member and nothing doubts its password, STRUCTURE once a read has reached the end with the length checks passing, and CONTENT once 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 from stream_members() opens its member at its first read, and is NONE until then.

    Compare it with verifiable: a TAR member has no checksum, so STRUCTURE is the most it can reach. Reading this property reads nothing from the archive.

  • verifiable (Verification) –

    (computed property) The most verified can reach by a read from the start to the end.

    CONTENT when the member carries a checksum archivey can compute, STRUCTURE when it carries none, and NONE for 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 from stream_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 from STRUCTURE to NONE. 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) and ArchiveMember objects, read once, so a generator works. A name selects every member with exactly that name (a directory is "dir/"); an ArchiveMember selects that member only, by identity. A bare str is refused: pass ["name"] for one member;
  • a predicate Callable[[ArchiveMember], bool] that returns True for 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:

  • members

    (tuple[ArchiveMember, ...]) –

    The members listed, all of them when error is None.

  • error

    (ArchiveyError | None) –

    The damage that stopped the listing, or None when it completed.

  • diagnostics

    (DiagnosticSummary) –

    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:

  • type

    (MemberType) –

    What kind of entry this is (file, directory, symlink, …).

  • name

    (str) –

    Normalized member path, /-separated, decoded for display and lookup.

  • raw_name

    (bytes | None, default: None ) –

    The member name exactly as stored in the archive, undecoded. None when 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).

    name is raw_name decoded and normalized, then put through the format's own naming rules. Where the archive stores the name twice, raw_name is 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 in extra["alternate_raw_name"].

    The naming rules that make name differ from raw_name decoded:

    • ISO: in a plain ISO 9660 image, name drops the ;N version suffix (and the . before an empty extension) that raw_name keeps; the version is in extra["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_name holds the original file name from the gzip header (FNAME), and extra["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_name is b"".
    • A RAR5 file-version-history member gets a ;n suffix in name (as WinRAR and unrar show it); raw_name is the stored name without it.
  • size

    (int | None, default: None ) –

    Uncompressed size in bytes, or None if 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 with None and filled the same way, by a read whose length and CRC-32 check out. Until then member_state_final is False.

  • compressed_size

    (int | None, default: None ) –

    Compressed size in bytes, or None if unknown.

  • modified

    (datetime | None, default: None ) –

    Last-modified time, if recorded.

  • accessed

    (datetime | None, default: None ) –

    Last-access time, if recorded.

  • created

    (datetime | 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 stores st_ctime, or may, that time is ctime instead: 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 gets created only from libarchive's PAX LIBARCHIVE.creationtime. Directory listing uses st_birthtime only.

  • ctime

    (datetime | 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 ctime record (TAR), or a creation slot whose writer fills it from st_ctime or cannot be shown not to (RAR, 7z, ZIP; see created). It is what the archive stored, so a Windows writer can put a birth time here: libarchive on Windows fills the PAX ctime with it and stamps its ZIP and 7z members as Unix. A format with a single creation slot (RAR, 7z, ZIP) fills at most one of created and ctime. 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.ctime prefers the birth time when there is one. A directory listing reports st_ctime on every OS but Windows, where Python's st_ctime was the creation time before 3.12.

    Extraction cannot restore it: every OS sets a file's st_ctime itself.

  • mode

    (int | None, default: None ) –

    Unix permission bits, or None if the format/entry carries no mode.

  • uid

    (int | None, default: None ) –

    Owner user id, if recorded.

  • gid

    (int | None, default: None ) –

    Owner group id, if recorded.

  • uname

    (str | None, default: None ) –

    Owner user name, if recorded.

  • gname

    (str | None, default: None ) –

    Owner group name, if recorded.

  • (str | None, default: None ) –

    For a symlink/hardlink, the raw target path string as stored. For a FILE that is a stored copy of an earlier member (extra["is_file_copy"]), the source's path as stored.

  • (ArchiveMember | None, default: None ) –

    For a link, the resolved target member within this archive, if found. For a FILE that is a stored copy (extra["is_file_copy"]), the member whose bytes it repeats.

  • compression

    (tuple[CompressionMethod, ...], default: tuple() ) –

    Codec chain in compress order — pre-filters first, packing codec last.

  • is_encrypted

    (bool, default: False ) –

    Whether this member's data is encrypted.

    True also 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 a MEMBER_HEADER_RECORD_SKIPPED diagnostic saying the header was cut short. A member with no such diagnostic is a definite answer.

  • is_current

    (bool, default: True ) –

    Last-entry-wins: True for 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 past ListingLimits stamps only the members it kept, so a copy it yields after the limit cannot mark an earlier one superseded.

  • is_sparse

    (bool, default: False ) –

    Whether this member is stored as a sparse file.

  • comment

    (str | None, default: None ) –

    Per-member comment, if the format records one.

  • create_system

    (CreateSystem | None, default: None ) –

    The operating system that wrote the entry. For ZIP and RAR it decides how the stored mode and attributes are read. None for formats that do not record one; see CreateSystem for what each format sets.

  • windows_attrs

    (int | None, default: None ) –

    Raw Windows file-attribute bitmask, if recorded.

  • hashes

    (Mapping[HashAlgorithm, bytes], default: dict() ) –

    Stored content digests keyed by HashAlgorithm (values always bytes).

    CRC-32 is four big-endian bytes: a CRC you computed yourself compares as zlib.crc32(data).to_bytes(4, "big"). Excluded from equality.

  • extra

    (MemberExtra, 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 as object. The EXTRA_* constants on this module remain the names for the keys they cover.

  • diagnostics

    (tuple[Diagnostic, ...]) –

    (computed property) Read-only tuple of diagnostics attached to this member (may be empty).

  • member_id

    (int) –

    (computed property)

  • is_file

    (bool) –

    (computed property)

  • is_dir

    (bool) –

    (computed property)

  • (bool) –

    (computed property)

  • is_other

    (bool) –

    (computed property)

  • is_anti

    (bool) –

    (computed property)

  • is_junction

    (bool) –

    (computed property) The archive recorded this symlink as a Windows NTFS junction.

    False means 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 reads False.
    • 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 False here.
    • TAR and ISO have no junction concept and always read False.

    is_reparse_point comes from metadata the archive always carries, and is the check to use when a Windows link of either kind matters.

  • is_reparse_point

    (bool) –

    (computed property) The archive recorded this entry as a Windows symlink or junction.

    Deliberately not gated on type the way is_junction is: 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_final

    (bool) –

    (computed property) No field of this member will change from here on.

    False while 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() or get(), or the end of a stream_members() pass or of iterating the reader. Until then a later member with the same name can still make this one is_current=False, and links get link_target_member. A streaming=True pass yields every member before that point, in every format. A members_report_if_available() peek resolves no links, so its members read False until 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 sets link_target, and a Windows reparse point whose data is not a link buffer becomes a FILE. open() and extract_all() read it when they reach the link, on the member they are given, a copy included.
    • A member whose size is None until 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 it False.

    Everywhere else it is True. Closing the reader makes it True for every member it listed, since a closed reader updates nothing. So does the end of a streaming=True pass, or its failure, for a size no read filled: nothing can read the member again. A copy made with replace() is never stamped, so only its unread link target can change, except the copy a streaming extract_all hands its filter: it reads False while the walk can still change the member it came from, and the filter may then be called again for that member. Attached diagnostics are not fields and can still grow. Which fields change in practice is listed per format in the user docs.

  • (bool) –

    (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_target None, open() raises LinkTargetNotFoundError with reason NOT_RECORDED for it, and extraction records it as ExtractionStatus.LINK_TARGET_UNAVAILABLE rather than as a failure.

    False does not mean the target is known. link_target can also be None because 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_targets is False). This property is True only 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:

  • format

    (ArchiveFormat) –

    The detected (container, stream) format of the archive.

  • format_version

    (str | None) –

    Format version string, e.g. "4.5" for ZIP or "5" for RAR5; None if unknown.

  • is_solid

    (bool) –

    Whether decompressing one member may require decompressing earlier ones.

  • member_count

    (int | None) –

    Number of members, or None when a count would require scanning the whole archive.

    Also None when the archive cannot supply a count: a ZIP whose central directory is damaged, or holds more entries than ListingLimits.max_members. Then members() raises too. A ZIP's count comes from walking its central directory, which reading this field runs if the listing has not.

  • comment

    (str | None) –

    Archive-level comment, if the format records one.

  • is_encrypted

    (bool) –

    Header-level encryption (7z, RAR5) — not per-member encryption (see ArchiveMember.is_encrypted).

  • is_multivolume

    (bool) –

    Whether the archive spans multiple volumes.

  • cost

    (CostReceipt) –

    Listing/access cost receipt for the archive (see the access-mode-and-cost capability).

  • extra

    (ArchiveInfoExtra, default: ArchiveInfoExtra() ) –

    Format-specific archive-level metadata, keyed by namespaced strings (mirrors ArchiveMember.extra). For example the ISO backend records the auto-selected namespace as extra["iso.namespace"]. Excluded from __eq__. Known keys and their value types are ArchiveInfoExtra; unknown keys stay legal and read as object.

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. Implies is_reparse_point.
  • is_reparse_point (bool) — ZIP, 7z, RAR, directory. The weaker, metadata-only sibling of is_junction: a Windows symlink or junction rather than a POSIX one.
  • is_file_copy (bool) — RAR. A FILE member whose bytes the archive stores once under an earlier member (a RAR5 file reference, rar -oi). link_target names that source and link_target_member is 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-less OTHER such an entry usually is, the FILE it becomes when it carries data (its bytes are the content, as unzip, 7-Zip, bsdtar and zipfile deliver them), a RAR file copy, or the DIRECTORY or SYMLINK a 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 an OTHER member that is not a special file (a GNU dumpdir in TAR). TAR keeps tar.type as well.
  • alternate_raw_name (bytes) — ZIP. The other stored spelling of the name, when the archive stores two and raw_name is the one name was 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 when size is listed as None; a read to the end checks the length against it and fills in size, 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 raises TypeError, and dict(...) gives a copy to change. It is a dict subclass, so json.dumps takes it.
  • tar.devmajor (int) — set only for a character or block device (typeflag 3 or 4) 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 carries MEMBER_HEADER_RECORD_SKIPPED).
  • tar.devminor (int) — present exactly when tar.devmajor is.
  • 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. size stays None until a full read fills it in.
  • iso.version (int) — plain ISO 9660 only: the ;N file version stripped from the name. Versions of one name share it; the highest is listed last and is the current one.

__getitem__(key)

__getitem__(key: Literal['is_junction']) -> bool
__getitem__(key: Literal['is_reparse_point']) -> bool
__getitem__(key: Literal['is_file_copy']) -> bool
__getitem__(
    key: Literal["special_file_type"],
) -> SpecialFileType
__getitem__(key: Literal['alternate_raw_name']) -> bytes
__getitem__(key: Literal['size_mod_2_32']) -> int
__getitem__(key: Literal['rar.extract_version']) -> int
__getitem__(key: Literal['rar.file_version']) -> int
__getitem__(key: Literal['rar.tweaked_crc32']) -> int
__getitem__(key: Literal['rar.tweaked_blake2sp']) -> bytes
__getitem__(key: Literal['zip.compress_type']) -> int
__getitem__(key: Literal['zip.aes_vendor_version']) -> int
__getitem__(key: Literal['zip.aes_strength']) -> int
__getitem__(key: Literal['zip.aes_actual_method']) -> int
__getitem__(
    key: Literal["zip.extra_fields"],
) -> dict[int, bytes]
__getitem__(key: Literal['tar.type']) -> bytes
__getitem__(
    key: Literal["tar.pax_headers"],
) -> Mapping[str, str]
__getitem__(key: Literal['tar.devmajor']) -> int
__getitem__(key: Literal['tar.devminor']) -> int
__getitem__(key: Literal['gzip.original_filename']) -> str
__getitem__(key: Literal['gzip.isize']) -> int
__getitem__(key: Literal['iso.version']) -> int
__getitem__(key: str) -> object

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)

__getitem__(key)

__getitem__(key: Literal['iso.namespace']) -> str
__getitem__(key: Literal['zip.volume_count']) -> int
__getitem__(key: Literal['rar.volume_count']) -> int
__getitem__(key: Literal['7z.volume_count']) -> int
__getitem__(key: str) -> object

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:

  • container

    (ContainerFormat | PluginContainer) –

    The archive format, such as ZIP or TAR, or a member of a PluginContainer subclass for a format a plugin adds.

  • stream

    (StreamFormat) –

    The compression around the archive, such as GZIP for a .tar.gz, or UNCOMPRESSED.

  • display_name

    (str) –

    (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 as tar.lz, gets CONTAINER_STREAM from the two enum member names ("TAR_LZIP"), or the container name alone when the stream is UNCOMPRESSED (a plugin's "CPIO"); its repr() stays the explicit constructor form. _FORMAT_NAMES is 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. The StreamFormat says which compressor.

  • UNKNOWN ('unknown') –

    What detection reports when no format matches. Passing a format with this container to open_archive() or open_stream() raises ArchiveyUsageError.

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 .lzma file 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 compress format.

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 FILE too, with extra["special_file_type"] saying what the archive called it.

  • DIRECTORY ('directory')
  • SYMLINK ('symlink') –

    A symbolic link. link_target holds the target as stored.

  • HARDLINK ('hardlink') –

    A hard link to another member, whose data it shares. link_target names that member, and link_target_member is 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_file is 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:

  • algo

    (CompressionAlgorithm) –

    The method or filter.

  • level

    (int | None, default: None ) –

    RAR only: the compression level, from 1 (fastest) to 5 (best), as rar -m1 to -m5 set it. A stored RAR member is STORED with no level, and other formats leave it None.

  • properties

    (bytes | 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 .xz stream, 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 UNKNOWN for a number not listed here. A pass over a pipe leaves it None until it reaches the central directory.
  • RAR 1.5 to 4: FAT (MS-DOS), OS2_HPFS, WINDOWS_NTFS, UNIX, MACINTOSH or BEOS; UNKNOWN for any other host byte.
  • RAR5: WINDOWS_NTFS or UNIX.
  • 7z records no host. archivey reports UNIX when the member stores a Unix mode, and WINDOWS_NTFS otherwise.
  • TAR, ISO, a directory and a single-file format such as .gz leave create_system as None.

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:

  • results

    (tuple[ExtractionResult, ...]) –

    One result per member processed, in the order they were processed.

  • diagnostics

    (DiagnosticSummary) –

    The diagnostics emitted during this extract_all() call.

archivey.ExtractionResult dataclass

One entry per member processed, returned from extract_all().

Fields:

  • member

    (ArchiveMember) –

    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.

  • path

    (Path | None) –

    Where the member was written, or None if it was not.

  • status

    (ExtractionStatus) –

    What happened to the member.

  • error

    (ArchiveyError | OSError | None, default: None ) –

    Why the member failed or was blocked, when extraction went on past it. An OSError for a file system error on this member, otherwise an ArchiveyError.

  • requested_path

    (Path | None, default: None ) –

    Where the member was to be written before overwrite settled a collision.

    Usually the same as path. Under OverwritePolicy.RENAME the two differ when the member, or a directory it is in, was renamed (dd/f written at dd (1)/f). After a collision settled by SKIP or ERROR, this is set and path is None. On an OVERWRITTEN result it keeps the path this member wrote to, which a later member then took over. That later member's collided_with equals this path, whether this member wrote a file or a directory (outside ExtractionPolicy.TRUSTED), so join the two through collided_with: the later member's own path can differ from this one in case on a case-insensitive file system, or in spelling through a symlink of the archive's. None for a member refused or failed before its destination was settled.

  • presented_name

    (str | None, default: None ) –

    The member's name before a safety rewrite changed it on disk, or None when none did; rewrites says which ones ran.

    After a re-root this is the stored name. When a filter renamed the member, it is the filter's name, which neither member.name (the archive's spelling) nor path (the final one) shows.

  • rewrites

    (frozenset[NameRewrite], default: frozenset() ) –

    The safety rewrites that changed the name. Empty exactly when presented_name is None.

  • failure_group_id

    (str | 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_size

    (int | None, default: None ) –

    How many results share failure_group_id.

  • collided_with

    (Path | None, default: None ) –

    The destination an earlier member of this run wrote, which this member collided with, whatever overwrite then did. None when no member of this run held the name: an entry already on disk before the run started is not counted, and stays in requested_path. The member it collided with is the result whose path equals this, or whose requested_path does once that result is OVERWRITTEN.

    A directory this run created on disk, from a DIRECTORY member 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 in dest is not counted, even when a DIRECTORY member of this run merged into it and reports it.

  • kept_mode

    (int | None, default: None ) –

    On a DIRECTORY result 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.

    None otherwise. Such a directory keeps its times too, so None does 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 overwrite is OverwritePolicy.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 report EXTRACTED at 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_error says. The archive is reported with the SYMLINK_TARGET_UNAVAILABLE diagnostic, which DiagnosticPolicy.STRICT raises.

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/x extracts as etc/x). STRICT refuses 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, under STRICT only, a trailing dot or space removed from each segment. TRUSTED never 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:

  • member

    (ArchiveMember) –

    The member being extracted.

  • bytes_written

    (int) –

    Bytes written so far in the whole call.

  • total_bytes_estimated

    (int | None) –

    An estimate of the bytes the whole call writes, or None if the archive records no sizes.

  • members_done

    (int) –

    Members processed so far, whatever their outcome.

  • members_total

    (int | None) –

    Members the call processes, or None when counting them would need a scan of the whole archive.

  • member_bytes_written

    (int) –

    Bytes written so far for the current member.

  • members_extracted

    (int) –

    Members written so far. Like members_blocked, it leaves out a member still in progress.

  • members_blocked

    (int) –

    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 are rwxr-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/x as etc/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, and OnError decides whether extraction goes on.

  • SKIP ('skip') –

    The existing entry stays, and the member's result is NOT_OVERWRITTEN. This is not a failure, so OnError does 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.txt becomes a (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 FilterRejectionError that refused it.

  • NAME_COLLISION ('name_collision') –

    Two members of the archive would be written to the same path. Raises NameCollisionError before overwrite is applied, so even with OverwritePolicy.RENAME the second member is not written. Not checked under ExtractionPolicy.TRUSTED.

  • NAME_SANITIZED ('name_sanitized') –

    A name is rewritten to its portable spelling, or an absolute name is extracted inside the destination. Raises NameRewrittenError. Under ExtractionPolicy.STRICT an absolute name is refused instead, so BLOCKED_MEMBER is 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, read ExtractionResult.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 at etc/x inside the destination. STANDARD and TRUSTED already do this; under STRICT it 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_rapidgzip

    (AcceleratorMode, default: AcceleratorMode.AUTO ) –

    Whether to use the rapidgzip accelerator for gzip, zlib and raw deflate.

    It gives seekable member streams over those codecs, and decodes in parallel. Under AUTO it is used only when the compressed input is known to be at least RAPIDGZIP_AUTO_MIN_COMPRESSED_SIZE bytes (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, AUTO uses 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 AUTO threshold 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 against max_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), AUTO uses the standard library, logging one warning per process on the archivey.streams logger, and ON raises ResourceUnavailableError. OFF never starts a child.

  • use_indexed_bzip2

    (AcceleratorMode, default: AcceleratorMode.AUTO ) –

    Whether to use rapidgzip's bundled bzip2 backend for random access into bzip2.

    Under AUTO it is used only when the compressed input is known to be at least INDEXED_BZIP2_AUTO_MIN_COMPRESSED_SIZE bytes (or its size is unknown). Like the gzip decoder, it runs in a child Python process, one per open stream, reused the same way (see use_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, AUTO uses the standard library, logging one warning per process on the archivey.streams logger, and ON raises ResourceUnavailableError.

  • process_isolation

    (ProcessIsolation, 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_encoding

    (str, 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 explicit encoding= on open_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_concatenation

    (bool, default: False ) –

    Read a RAR member whose stored name contains * or ? even when that name also matches other members.

    unrar selects 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 (ExtractionLimits does not cover open() or read()); on a solid archive those bytes are already inside AccessCost.SOLID. Set True to read it anyway. A glob name that matches no other member is unaffected either way. Under rar_decompressor=AUTO, an installed unar reads such a member by index instead of the refusal (see RarDecompressor), so this flag matters only when unrar must read it.

  • rar_decompressor

    (RarDecompressor, 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.

  • (bool, 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 finished stream_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_all reads the targets of links its selector and filter accept, and open() reads the target of a link it follows. Fixed for the reader's lifetime.

  • extraction_limits

    (ExtractionLimits, 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_limits

    (ListingLimits, default: ListingLimits(max_members=262144, max_metadata_bytes=67108864) ) –

    Caps on the size of a member listing. See ListingLimits.

  • decoder_limits

    (DecoderLimits, 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_limits

    (SpoolLimits, default: SpoolLimits(max_bytes=1073741824) ) –

    Caps on what a reader writes to temporary storage. See SpoolLimits.

  • detection_budget

    (DetectionBudget, 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() and detect_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_BUDGET and THOROUGH_BUDGET in archivey.detection_cost are the other presets; to change a single limit, dataclasses.replace one.

  • always_probe_content

    (bool, 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() and detect_format() run a probe only when the source's name ends in one of that format's extensions: .lzma or .tlz for LZMA Alone, .zz or .zlib for zlib, .br or .brotli for Brotli, and the .tar. form of each except .tlz. An extensionless raw stream of these formats is then refused with FormatDetectionError. If you read such sources, set this to True, pass format=, or use open_stream(), which always tries every probe because its caller already says the source is a compressed stream.

  • diagnostic_policy

    (DiagnosticPolicy, default: DiagnosticPolicy() ) –

    Whether each diagnostic code is ignored, collected or raised.

    See DiagnosticPolicy.

  • max_retained_diagnostic_references

    (int, 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_diagnostic

    (OnDiagnostic | None, default: None ) –

    A callback called with each diagnostic the policy collects or raises, as it is emitted, or None.

  • formats

    (FormatPolicy, 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:

  • only

    (frozenset[ContainerFormat | PluginContainer | StreamFormat] | None) –

    The containers and compressions that may open, or None for all of them.

  • exclude

    (frozenset[ContainerFormat | PluginContainer | StreamFormat]) –

    The containers and compressions that may not open.

  • plugins

    (bool) –

    Whether registered plugin formats may open. Use False with exclude; under only a plugin opens only when its container is listed.

  • replace

    (Mapping[ContainerFormat, type[FormatHandler]]) –

    Built-in containers read by a plugin instead of the built-in reader.

    A key is ContainerFormat.ZIP, SEVEN_Z, RAR or ISO. Its plugin is a archivey.formats.FormatHandler that is not registered: its format is the built-in one (ArchiveFormat.ZIP), and it declares no extensions, magic or probe, because the file is found by the built-in detection. The reader's format is the built-in format and format_info.from_plugin is 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 with UnsupportedFeatureError, a source that cannot seek with StreamNotSeekableError, and a raw CD sector image detected as ISO as the built-in reader refuses it. The format's own ArchiveyConfig options reach the plugin through ReaderContext.config.

  • _turns_off_plugins

    (bool) –

    (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_bytes

    (int | 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 under on_error="continue".

  • max_ratio

    (float | 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 before ratio_activation_threshold bytes of output, and the archive-wide check on a source of unknown size, such as a pipe, not before live_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_threshold

    (int, default: 5242880 ) –

    Output bytes before max_ratio is 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_threshold as well. It cannot be None; set max_ratio to None to turn the ratio guard off.

  • max_entries

    (int | 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. None removes both bounds, so a hostile archive can then make the rechecks take time quadratic in its size.

  • live_ratio_activation_threshold

    (int, 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.gz read 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_bytes still bounds the total. Lower it to stop such a bomb sooner; raise it for piped archives with longer blank starts. A value below ratio_activation_threshold has no effect. It cannot be None; set max_ratio to None to 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_members

    (int | 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_bytes

    (int | 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-level extra keys 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 in extra["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_memory

    (int | 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, call and jump branches), 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=PPMd declares 16 MiB and -mx9 256 MiB, whatever the input. Reaching 2 GiB therefore takes an explicit mem=2g, which cannot go above it at any member size; only mem=3g and mem=4g do. 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 deliberate mem=2g as well; it refuses the top of the field. Reading archives written with mem=3g or above means raising it or passing UNLIMITED.

    The LZMA dictionary sits further under it. xz Utils refuses to write a dictionary over 1.5 GiB, and its -9 preset 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 -md above 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 .lzma are 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_rounds

    (int | 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_count asks for 2**kdf_count PBKDF2-HMAC-SHA256 rounds, 7z's NumCyclesPower for 2**cycles SHA-256 rounds, each up to 2**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 inside hashlib with 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**18 SHA-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 + 32 rounds), the checksum HashKey (+ 16) and, when archivey decrypts a stored member or the headers itself, the AES key (+ 0). A RAR5 read through unrar costs two derivations per password, a stored member three.

    2**25 is about ten seconds of hashing: two 7z derivations at the 2**24 maximum, but not one maximum-cost RAR5 member, which needs two or three; 64 passwords at 7-Zip's 2**19; about 500 at rar's 2**15. Exceeding it raises ResourceLimitError before the derivation that would cross it starts. Code that opens archives it did not choose may want 2**24, which still admits every archive the usual tools write.

  • max_ppmd_held_input

    (int | 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_isolation is ProcessIsolation.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. None holds 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_bytes

    (int | 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 ResourceLimitError before 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.

    0 refuses every copy: a stream source then reads only the members archivey can read without unrar, such as stored, unencrypted RAR members. The copy goes to the platform temporary directory (tempfile.gettempdir()); where that is memory-backed, such as tmpfs, 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=True on open_archive, seekable=True on open_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_SIZE for gzip, zlib and raw deflate, and INDEXED_BZIP2_AUTO_MIN_COMPRESSED_SIZE for 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 least RAPIDGZIP_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 PackageNotInstalledError when its package is not installed.

  • OFF ('off') –

    Never use it. The standard library decodes the stream. Set both use_rapidgzip and use_indexed_bzip2 to 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 cost NATIVE_DECODERS gives for single streams. Where no child process can be started, a PPMd member raises ResourceUnavailableError and rapidgzip under AUTO falls 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 under KNOWN_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 raises ResourceUnavailableError; 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_rapidgzip and use_indexed_bzip2 under AUTO use the standard library, and ON is refused with ArchiveyUsageError when 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 unrar 6.0 or later (or the RARLAB rar writer). It reads every RAR archive, including encrypted ones.

  • UNAR ('unar') –

    unar 1.10 or later, from The Unarchiver's XADMaster library. It is free software, and brew install unar installs it on macOS.

    It is not a full substitute: archivey refuses, before unar runs, every read that unar is known to get wrong. A password is passed on unar's command line, where other local users can see it while it runs. Selecting UNAR when unar is not installed raises PackageNotInstalledError, even when unrar is available.

  • AUTO ('auto') –

    The default: unrar when a usable one is on PATH, otherwise unar.

    The choice is made once per opened archive, when it is opened, and holds for every read of it: a read that unar refuses is not retried with unrar. One exception goes the other way: a member whose name unrar cannot be given, or whose glob name also matches earlier members (rar_allow_glob_member_concatenation), is read with unar when an accepted one is installed, because unar selects members by index. That applies only to a read that needs no password, from a file with nothing before the RAR (so unar gets no password and no copy), and to a member unar reads correctly; otherwise the unrar refusal stands.

    With neither installed, a read raises PackageNotInstalledError naming unrar. On a machine with unar and no RARLAB program, a password is passed on unar's command line; select UNRAR to rule that out.

  • NONE ('none') –

    No program at all, for a caller who does not want unrar or unar run 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 cryptography installed. A compressed member, an encrypted one split across volumes, or a split one with a volume missing, raises UnsupportedFeatureError before anything runs. A stored, encrypted member in one part raises PackageNotInstalledError naming cryptography when that package is missing. A compressed RAR 1.5-4 comment is None. The format_availability() query takes no config and still runs the unrar identification probe; pass it check_programs=False to 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:

  • member

    (ArchiveMember | None) –

    The member being decrypted, or None for archive-level (header) decryption.

  • attempt

    (int) –

    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_id

    (str) –

    A random id, unique to this occurrence.

  • code

    (DiagnosticCode) –

    What kind of event this is.

  • message

    (str) –

    A sentence describing the event, with control characters escaped.

  • context

    (DiagnosticContext) –

    The event's details as typed fields with raw values, such as the archive and member names. Its kind field 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 .exe look 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's detected_format says 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 record tells 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_target unset, and the context's reason says 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 FILE with 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 size as None until 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 stays OTHER.

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.STRICT is DiagnosticPolicy(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 to ARCHIVE_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.PEDANTIC is DiagnosticPolicy(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:

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, and diagnostics forbids 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). In STRICT it would turn a peek at a ZipCrypto or RAR3/4 encrypted member into DiagnosticRaisedError. 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 carries format_unconfirmed=True. A policy that resolves it to RAISE still gets that typed error, through escalate_as. Its sibling EXTENSION_FORMAT_UNCONFIRMED is 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's members= 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-ZIP zip -FI) produces the shape, and the member's bytes are delivered and checked like any file's, so nothing is hidden; STRICT would 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. STRICT would refuse an archive of legitimately named files. Its sibling MEMBER_NAME_ZERO_WIDTH_CHARACTER is 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 as b.gz is an ordinary mistake, and STRICT would 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 siblings MEMBER_LOCAL_NAME_DIFFERS and MEMBER_LOCAL_METHOD_DIFFERS are 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. STRICT would 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 is None, with the length and CRC-32 checked on a read. STRICT would 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 a FILE, so nothing is hidden. STRICT would refuse an archive GNU tar extracts.

archivey.DiagnosticSummary dataclass

Immutable point-in-time snapshot of diagnostic counts and retained detail.

Fields:

  • total_count

    (int) –

    Every diagnostic emitted in the summary's scope, ignored ones included.

  • counts

    (Mapping[DiagnosticCode, int], default: mappingproxy({}) ) –

    The exact count for each code, ignored ones included.

  • retained

    (tuple[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_references budget.

  • dropped_count

    (int, default: 0 ) –

    How many of total_count are not in retained.

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. !r escapes 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 (as raw_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, IndexError from a parser), write that description instead and keep the original as __cause__. tests/test_escaping.py checks 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

Bases: OpenError

Source is non-seekable but this format/backend needs seek.

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

Bases: ReadError

Password required or wrong password.

archivey.LinkTargetNotFoundError

Bases: ReadError

A symlink/hardlink target, or a file copy's source, is absent from the archive.

reason says why, so a caller can act on the cause without reading the message.

reason = reason instance-attribute

Why the link has no member to follow to.

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_unrecorded is True for 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

Bases: ArchiveyError

Error extracting a member to disk.

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_cost

    (ListingCost) –

    Cost of enumerating all members.

  • access_cost

    (AccessCost) –

    Cost of reading one member's data, given the format layout.

  • stream_capability

    (StreamCapability) –

    Seekability of the underlying source bytes.

  • solid_block_count

    (int | None, default: None ) –

    Number of distinct solid blocks (each one decompress pass), or None when not applicable / unknown. is_solid lives on ArchiveInfo, not here, to avoid duplicating the flag.

  • notes

    (tuple[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, so members() 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_bytes

    (int) –

    Bytes detection may read from the start of the source.

  • max_far_bytes

    (int) –

    Bytes the far-magic tier may read, for a signature at a fixed offset past the prefix. Separate from max_prefix_bytes because ISO's CD001 at 32 769 needs a window of about 32 KiB that a 4 KiB prefix budget would forbid.

  • max_scan_bytes

    (int) –

    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, and sfx_scan recorded as cut short.

  • max_decode_input

    (int) –

    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_output

    (int) –

    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_bytes

    (int) –

    The largest source a content-probe hit is re-checked against in full; 0 turns 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_bytes

    (int, default: 0 ) –

    Sum of range lengths requested via peek_range (overlapping peeks accumulate).

    Not comparable 1:1 with max_prefix_bytes: seek-based read_at does not charge here, and growing peeks bill each request in full. Prefer unique_bytes_read for "how much did we fetch from the source".

  • unique_bytes_read

    (int, 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_bytes

    (int, default: 0 ) –

    Bytes the far-magic tier peeked. Bounded by max_far_bytes.

  • scanned_bytes

    (int, default: 0 ) –

    Bytes the scan for an archive behind a prefix looked through. Bounded by max_scan_bytes.

  • decode_input

    (int, default: 0 ) –

    Compressed bytes decoded. Bounded by max_decode_input.

  • decode_output

    (int, default: 0 ) –

    Decoded bytes the inner-TAR probe produced. Bounded by max_decode_output.

  • passes

    (int, default: 1 ) –

    Detection passes this receipt sums, each run under the full budget.

    2 when detect_format followed 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:

  • tier

    (str) –

    The tier's name.

  • reason

    (TierSkipReason) –

    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, 1 or more.

  • is_dir (bool) –

    Whether name is 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 + 1 when 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: *.md selects README.md but not docs/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: **/*.md selects README.md and docs/api/guide.md, and docs/** selects the docs/ 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 ArchiveMember and 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 a ValueError.

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: *.md selects README.md and docs/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: docs and docs/ both select docs/ and docs/api/guide.md, but not a file named docs.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:

Raises:

  • ArchiveyUsageError –

    No pattern was given, or a pattern has an empty component ("", "/", "a//b"; a leading / is allowed). It is also a ValueError.