LESSON 84 · ADVANCED PROJECT42 min

Project: Verify a ZIP Archive Against a Manifest

Verify a small in-memory ZIP against an expected manifest without writing archive members to disk.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Project: Verify a ZIP Archive Against a Manifest

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Checking the names inside an archive is useful, but it cannot tell whether a file's bytes changed. Imagine a ZIP that contains a.txt, b.txt, and c.txt. Our manifest expects a.txt, b.txt, and old.txt. A name-only comparison can identify old.txt as missing and c.txt as extra. It will completely miss that b.txt has the wrong content. In this project we will read an in-memory ZIP, calculate SHA-256 digests for its file members, and compare them with expected digests in a manifest mapping. The result will have three sorted lists: missing, extra, and changed.

  2. 00:44

    We will not extract any member to disk. We will also reject duplicate file names inside the ZIP, because two members with the same name make a name-to-digest report ambiguous. This is a teaching fixture, not a general safe archive-processing service. Large or hostile ZIPs need size limits, resource controls, and careful operational policies. A manifest is also not an authenticity guarantee by itself: it must come from a trustworthy source if you want the comparison to mean anything about provenance. The lesson concentrates on a clear, testable integrity report for controlled inputs. The two lines correctly show a missing file and an extra file.

  3. 01:27

    They say nothing about b.txt because its name is present in both places. If someone changed its bytes from good to bad without renaming it, a name-only check would report no problem for that member. We need a content digest in the manifest. SHA-256 maps a byte sequence to a fixed- length digest; the verifier can calculate the digest of the bytes read from the ZIP and compare it with the expected digest for the same name. This does not prove that the manifest itself is authentic. If an attacker can replace both the archive and the expected manifest, the comparison can still pass. For this lesson, treat the supplied manifest as the expected reference and be

  4. 02:10

    explicit about that assumption. Another subtle issue is ZIP duplicates: a ZIP can contain more than one entry with the same filename. Converting names directly to a set would erase that ambiguity. We should inspect the entries before building a dictionary and reject duplicate file names, not let whichever member is read last silently win. Both digests have the same hexadecimal length, but they differ because the input bytes differ. The verifier needs to compare the digest strings for members that appear in both the ZIP and the manifest. A missing file should be reported as missing, not also changed, because there

  5. 02:50

    are no archive bytes to hash under that name. An extra file should be reported as extra, not changed, because the manifest has no expected digest for it. These categories should be mutually understandable even if the caller later chooses to display them together. We will use `ZipFile.infolist()` to inspect actual entries. Directory entries have no file content to compare for this project, so we ignore them. Before hashing, we collect the file names and reject duplicates. Then we read each file member through ZipFile without extracting it to a path. This keeps the example focused on content comparison.

  6. 03:32

    Reading still consumes memory and CPU, so do not feed arbitrary untrusted archives to this small teaching function and assume that no extraction automatically makes every possible archive safe. The verifier opens bytes through io.BytesIO and ZipFile, so no file is extracted. It filters out directory entries, checks that file names are unique, and builds a mapping from each file name to the SHA-256 digest of its content. The sets of names determine missing and extra members. For names present on both sides, digest inequality determines changed members. Sorting each list gives a deterministic report independent of ZIP entry order or

  7. 04:15

    manifest insertion order. The output now captures all three facts about the fixture: old.txt is absent, c.txt was not expected, and b.txt has content different from the manifest. The function does not return the file bytes, write them to disk, or declare the archive safe to execute. It answers only the comparison question under this project's assumptions. If you later apply the idea to real downloads, you need a trusted manifest or signature plus limits on compressed and uncompressed sizes. A matching digest against an untrusted manifest is merely agreement between two

  8. 04:56

    untrusted inputs, not proof of origin. A directory entry is ignored, while the file inside it is hashed and matches its expected digest. The report is empty in all three categories. The second fixture deliberately writes two file entries with the same name. Python warns while constructing such a ZIP; we suppress that warning only to keep this deliberate test output clear. The verifier must reject the duplicate before it creates a dictionary that could discard one of the entries. This check is about report ambiguity, not a claim that duplicate rejection solves all archive risks.

  9. 05:36

    The companion project task will also check sorted output, missing and extra members, and a changed digest. If your changed list stays empty when b.txt contains bad bytes, you are probably comparing only names. If duplicate names silently pass, look at whether you used a set before checking how many file entries existed. If a directory appears as an extra file, filter directory entries before comparing. After the exercise, inspect the archive without extracting it and explain exactly what each reported category means. Keep the trust statement attached to the result: the manifest must come from a source you trust if the comparison is meant to establish integrity against an

  10. 06:19

    adversary.

Understand the concept

A manifest maps each expected archive member name to the SHA-256 digest of its expected bytes. A verifier must answer three distinct questions: which expected files are missing, which unlisted files are extra, and which listed files contain different bytes. Comparing only names misses changed content. Comparing only a total file count misses replacements. This project returns each category separately so a learner can diagnose the actual mismatch.

zipfile.ZipFile can read members from an io.BytesIO object. The lesson creates tiny archives entirely in memory, which makes the browser console reproducible and avoids writing to a visitor's machine. It filters out directory entries, rejects duplicate file names as ambiguous, and computes hashlib.sha256 for each expected present file. The function never calls extract or extractall. The result lists are sorted so output is deterministic even when dictionary or archive order changes.

A digest is a content fingerprint, not a proof of who produced the file. If an attacker can replace both archive and manifest, matching hashes do not authenticate the publisher. A production updater needs a trusted manifest or signature. Archives from untrusted sources can also consume excessive memory or contain dangerous path names. This small educational function is limited to fixture archives and does not claim safe processing of arbitrary downloads.

The checked task includes a changed member, a missing expected member, an unexpected archive member, an empty archive, and duplicate-name detection. The report shape is {'missing': [...], 'extra': [...], 'changed': [...]}. Learners can build the fixture ZIP in memory, run the broken name-only checker, then add actual byte hashing. This project combines file-like objects, mapping comparison, sorting, and hashing into one verifiable result.

  • Manifest maps names to expected digests
  • Name equality does not prove content equality
  • ZIP members can be read without extraction
  • Sorted categories make reports deterministic

See it step by step

Read the code, predict the output, then compare it with the result.

01. Hash the exact member bytes

PYTHON
import hashlib
print(hashlib.sha256(b'alpha').hexdigest() == hashlib.sha256(b'alpha').hexdigest())
print(hashlib.sha256(b'bad').hexdigest() == hashlib.sha256(b'good').hexdigest())
EXPECTED OUTPUT
True
False

A digest comparison detects content changes that file-name comparison cannot.

02. Read a tiny ZIP without extracting

PYTHON
import io, zipfile
buffer = io.BytesIO()
with zipfile.ZipFile(buffer,'w') as archive: archive.writestr('a.txt',b'alpha')
with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as archive:
    print(archive.namelist())
    print(archive.read('a.txt'))
EXPECTED OUTPUT
['a.txt']
b'alpha'

ZipFile reads the member bytes from a memory buffer; no filesystem extraction occurs.

03. Separate missing and extra names

PYTHON
expected = {'a.txt','old.txt'}
actual = {'a.txt','c.txt'}
print(sorted(expected - actual))
print(sorted(actual - expected))
EXPECTED OUTPUT
['old.txt']
['c.txt']

Two set differences give independently meaningful report categories.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Show why names do not verify bytes

The expected manifest says that b.txt should contain the bytes for good. The archive contains b.txt, but its bytes are bad. A checker that looks only at member names will report no missing or extra files and declare success. The content mismatch is invisible until the bytes themselves are compared.

A SHA-256 digest gives a compact representation of byte content for this integrity check. Compute it from the exact bytes read from the archive, not from a decoded and re-encoded text guess. The digest alone cannot authenticate the publisher; the manifest must come from a trusted source for that stronger claim.

PYTHON
import hashlib
expected={'b.txt':hashlib.sha256(b'good').hexdigest()}
actual_name='b.txt'
actual_bytes=b'bad'
print(actual_name in expected)
print(hashlib.sha256(actual_bytes).hexdigest()==expected[actual_name])
EXPECTED OUTPUT
True
False
Follow the reasoning
  1. The name is present.
  2. The bytes have a different digest.
  3. Name and content checks answer different questions.
02 / 03

Read members in memory and split the report

ZipFile accepts an io.BytesIO buffer, so this project can create and inspect tiny fixture archives without touching the filesystem. archive.read returns member bytes directly; there is no reason to call extractall. Directory entries are not file payloads in our manifest, so the final verifier ignores them.

Two set differences separate missing expected names from unexpected actual names. Sort both lists for stable output even if an archive was written in a different order. For names present in both sets, compare SHA-256 digests to find changed members. This third category is the one the broken name-only checker omitted.

PYTHON
import io, zipfile
buffer=io.BytesIO()
with zipfile.ZipFile(buffer,'w') as archive:
    archive.writestr('a.txt',b'alpha')
    archive.writestr('c.txt',b'extra')
with zipfile.ZipFile(io.BytesIO(buffer.getvalue())) as archive:
    actual=set(archive.namelist())
    expected={'a.txt','old.txt'}
    print(sorted(expected-actual))
    print(sorted(actual-expected))
    print(archive.read('a.txt'))
EXPECTED OUTPUT
['old.txt']
['c.txt']
b'alpha'
Follow the reasoning
  1. Compare expected and actual names in both directions.
  2. Read content from the ZIP buffer itself.
  3. No extraction is performed.
03 / 03

Verify missing, extra, and changed members

The final function lists non-directory members and rejects duplicate file names. A duplicate name makes archive.read(name) ambiguous, so reporting a clean result would be misleading. It then builds sorted missing and extra lists and hashes each expected member that is actually present. The output categories are separate and deterministic.

The prepared fixture has one correct file, one changed file, one extra file, and one missing expected file. The checker also covers an empty archive and a directory entry. This is a deliberately small in-memory educational archive; a production verifier must place limits on untrusted ZIP sizes and validate the origin of the expected manifest.

PYTHON
import io, zipfile, hashlib
def verify_archive(zip_bytes, manifest):
    with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
        names=[info.filename for info in archive.infolist() if not info.is_dir()]
        if len(names)!=len(set(names)):
            raise ValueError('duplicate ZIP member')
        actual=set(names); expected=set(manifest)
        return {'missing':sorted(expected-actual),'extra':sorted(actual-expected),'changed':sorted(name for name in expected&actual if hashlib.sha256(archive.read(name)).hexdigest()!=manifest[name])}
buffer=io.BytesIO()
with zipfile.ZipFile(buffer,'w') as archive:
    archive.writestr('a.txt',b'alpha')
    archive.writestr('b.txt',b'bad')
    archive.writestr('c.txt',b'extra')
manifest={'a.txt':hashlib.sha256(b'alpha').hexdigest(),'b.txt':hashlib.sha256(b'good').hexdigest(),'old.txt':hashlib.sha256(b'old').hexdigest()}
print(verify_archive(buffer.getvalue(),manifest))
EXPECTED OUTPUT
{'missing': ['old.txt'], 'extra': ['c.txt'], 'changed': ['b.txt']}
Follow the reasoning
  1. Reject ambiguous duplicate names.
  2. Use set differences for absent and unexpected members.
  3. Hash present expected members to detect changed bytes.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnComplete verify_archive(zip_bytes, manifest). Ignore directory entries, reject duplicate file names with ValueError, and return sorted missing, extra, and changed lists. Compare SHA-256 of each present expected member with its manifest digest; never extract files. Prepared output must be {'missing': ['old.txt'], 'extra': ['c.txt'], 'changed': ['b.txt']}.
Need a hint?

Inside ZipFile, gather info.filename for non-directory members and reject duplicate names. Use set differences for missing/extra. For each expected name also present, compare hashlib.sha256(archive.read(name)).hexdigest() to manifest[name].

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.Can matching ZIP member names prove the content is unchanged?

    02.Does this project need extractall to compute a digest?

    03.Does a matching SHA-256 manifest authenticate a publisher by itself?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Checking names but not content

    COMMON MISTAKE
    changed = []  # names match, so files match
    FIX
    changed = [name for name in common if hashlib.sha256(archive.read(name)).hexdigest() != manifest[name]]

    What happens: A changed b.txt is reported as healthy.
    Hash member bytes for every expected present file.

    Extracting an untrusted archive to inspect it

    COMMON MISTAKE
    archive.extractall(output_dir)
    FIX
    data = archive.read(name)
    digest = hashlib.sha256(data).hexdigest()

    What happens: Inspection writes every member to disk and may expose path or resource risks.
    This small fixture verifier only needs bytes, not extraction.

    Browse this lesson’s problem fixes

    Explore the runnable code

    The GitHub project includes setup instructions and source code you can run on your computer. Use this lesson for the guided examples, browser task, and quiz.

    View this project on GitHub

    Sources and further reading