Skip to main content

Read ZIP members with a shared byte budget

2026-11-12 17:48:36.460723+00:00
Read ZIP members with a shared byte budget

AI assisted the preparation of this article, and its Python example was tested locally with synthetic fixtures.

A ZIP archive can contain many small members whose combined uncompressed data exceeds the amount an application intended to inspect. Apply a single budget across the archive rather than giving every member a fresh full allowance.

Flow: ZIP bytes, then Count and sizes, then Shared budget, then Results or error.

Check the directory, then count actual reads

This example reads a small in-memory ZIP. It returns member names and bytes; it never extracts a path or writes a file. The caller supplies both the maximum member count and a total uncompressed-byte budget.

import zipfile

def read_bounded(stream, remaining):
    parts = []
    while True:
        chunk = stream.read(min(4096, remaining + 1))
        if not chunk:
            return b"".join(parts)
        if len(chunk) > remaining:
            raise ValueError("uncompressed byte budget exceeded")
        parts.append(chunk)
        remaining -= len(chunk)

def read_members(source, max_members, budget):
    if max_members < 0 or budget < 0:
        raise ValueError("limits must be nonnegative")
    with zipfile.ZipFile(source) as archive:
        members = archive.infolist()
        if len(members) > max_members:
            raise ValueError("too many members")
        if sum(member.file_size for member in members) > budget:
            raise ValueError("advertised byte budget exceeded")
        result = []
        for member in members:
            with archive.open(member) as stream:
                data = read_bounded(stream, budget)
            budget -= len(data)
            result.append((member.filename, data))
        return result

The directory check can reject an archive before its members are read. Advertised sizes are untrusted, so the reader also counts bytes actually returned by each stream. Each completed member reduces the remaining shared allowance before the next one starts.

The reader requests at most one byte beyond its remaining allowance to detect an overrun. That probe byte can be read from the stream, but it is never appended to the accepted result. An overrun raises and the function returns no partial result. The one-byte probe means the stream may deliver one more byte than the accepted budget; do not describe the number of bytes read as strictly capped at the budget.

Test the boundary and its limits

Use a real BytesIO archive with two stored members. Check exact total-budget success, a member-count rejection, an advertised-size rejection and a corrupted stored member that raises a CRC error. A separate synthetic BytesIO reader can return more data than the allowance to exercise the actual-read branch without assuming how ZipExtFile handles dishonest metadata. That seam tests the counting control flow, not every malformed ZIP implementation.

Member names in this example are labels only. They are not safe paths and must not be passed to a filesystem extraction routine without a separate path and symlink policy. No filesystem extraction happens here.

This small example retains accepted data in memory. The compressed input itself may already be large, and decompression can consume substantial CPU before the byte limit is reached. Encryption, time limits, concurrency, archive nesting and upstream upload limits need separate policies. The Python ZIP documentation describes resource and integrity pitfalls; this example is not complete decompression-bomb protection.