On the recorded CPython 3.13.5 implementation, a tiny ZIP returns None from ZipFile.testzip() even though one member occurrence is damaged. In the supplied commissioning run, two central-directory entries were both named manifest.txt. The earlier occurrence had one payload byte flipped while its recorded CRC stayed unchanged. The built-in scan returned None, yet a second scan that opened each individual ZipInfo and attempted a complete read raised BadZipFile for ordinal 0.
The boundary is narrow: one authored, in-memory, ZIP_STORED fixture on CPython 3.13.5. This is not a claim about every Python patch, every ZIP reader, archive authenticity, or a current upstream vulnerability. It also does not make duplicate names universally invalid. The rejection policy in this playbook is application-specific: a small release bundle must contain exactly one approved manifest.txt, exactly one approved readme.txt, no other members, and the approved bytes for both.
The repair is therefore two-layered. First, preserve every central-directory occurrence and read each occurrence to EOF by passing its ZipInfo object to ZipFile.open(). Second, apply an independent manifest policy for unique names, required membership, unexpected members, and approved content. Nine cases separate those questions. Only the complete unique archive is accepted. Corruption, duplicates, missing or extra members, and CRC-valid wrong content are rejected, while changed baselines or incomplete evidence are held for investigation.
Declare exactly what a successful archive check must establish
The acceptance contract is deliberately small. The approved archive contains two member occurrences and only two:
manifest.txt must contain b'release=approved\n'.
readme.txt must contain b'notes\n'.
In these byte literals, \n represents one newline byte, not a backslash followed by n. Each filename must occur once. No third name is allowed. Attempt to consume every central-directory occurrence to EOF through its own ZipInfo, retaining either a completed read or the expected integrity failure. A completed read must produce the declared byte count; content acceptance additionally requires the SHA-256 expected for that approved name. These are different requirements, not one undifferentiated Boolean called “valid.”
CRC coverage asks whether the data read for a recorded occurrence agrees with its recorded CRC. A zipfile duplicate filename CRC check is incomplete for this contract if it cannot establish which occurrence supplied those bytes. Name policy requires one occurrence per approved name. Membership policy requires all approved names and excludes unexpected names. Content policy compares completed member digests with an independently authored content manifest. Those checks do not establish provenance or a signer’s identity. The distinction complements release approval beyond an attestation, a contrasting gate rather than evidence for these ZIP bytes.
This separation drives the decisions used throughout the playbook. ACCEPT_BOUNDED is reserved for the single complete reviewed manifest. REJECT_ARCHIVE covers any CRC failure, duplicate name, missing or unexpected member, or wrong approved content. REPAIR_INTEGRITY_SCAN identifies a reader implementation that misses a recorded occurrence, even when the same input is also rejected. HOLD applies when the baseline changes, the report is missing, an inventory is incomplete, or an unexpected exception prevents a supported conclusion.
Separate filenames from member occurrences
A filename is a label. A ZipInfo returned by infolist() describes a member occurrence. A filename alone identifies that occurrence unambiguously within this inventory only when the name is unique. The question behind Python testzip duplicate members is therefore about identity: did the scanner read the occurrence it enumerated, or another record with the same label? The duplicate fixture has three records in order: an earlier manifest.txt containing release=old\n, a later manifest.txt containing release=approved\n, and readme.txt containing notes\n. There are three occurrences but only two distinct names.
The Python zipfile reference documents that infolist() returns one ZipInfo for each archive member in archive order. It also documents that open() can receive a ZipInfo, specifically noting that this form matters when duplicate names exist. That is the identity boundary the scanner must preserve. Enumerate with infolist(), assign an ordinal, retain filename and header_offset, and pass the same object to open().
A name-keyed dictionary is still useful after the inventory exists. It can count occurrences, detect missing names, and organize approved content checks. It must not replace the occurrence ledger. With duplicates, assigning by_name[info.filename] = info overwrites an earlier record. The resulting map can look clean while the evidence has already been discarded.
Preserve identity before constructing a name-based summary
The occurrence ledger identifies each parsed member by ordinal, filename, and local-header offset. The independent oracle is the separately authored entry list, approved byte literals, and expected case outcomes, not the archive’s own metadata. Ordinal gives a position within this infolist() result; filename supports policy checks. In these authored archives, header_offset distinguishes the duplicate records and locates their local headers. Keep all three fields, but do not describe this as an independent ZIP parser or assume every possible archive layout has the same properties.
Other summaries answer narrower questions. A filename count proves how many labels were observed, not which payloads were read. A total byte count can remain plausible after one occurrence is skipped. A whole-archive digest identifies the archive byte string but does not enumerate the path the reader took through it. The ledger therefore keeps all records, including duplicates that policy will reject. Evidence should remain richer than the final decision.
Pin the interpreter and the expected nine-case ledger
The supplied commissioning observation is pinned to CPython 3.13.5 on Linux. It reports PASS_9_CASES for nine named cases, starting at 2026-10-15T06:47:33.806294+00:00 and finishing at 2026-10-15T06:47:33.811456+00:00. The embedded driver’s SHA-256 is a3b11ebc14000f70fa41b6647952b9a52a6c255645d948d3204c9013bdf1a732. Those values identify one local run and one exact source file. They are not a benchmark, cross-platform guarantee, or assertion about a newer interpreter.
Version discipline matters because the Python 3.13 documentation URL tracks a maintenance branch, not an immutable copy of the 3.13.5 implementation. It can describe a newer patch while a local interpreter still runs older code. Use the library reference for API meaning and the pinned source plus recorded fixture outcomes for this behavioral claim. Reading updated documentation is not equivalent to testing the interpreter that will run the check.
The exact CPython 3.13.5 zipfile implementation explains the result. Its testzip() loop walks self.filelist, but opens each item with self.open(zinfo.filename, "r"). For a string argument, the reader resolves the name through its name-to-record mapping, where a later duplicate has replaced the earlier value. In the first-duplicate corruption case, the enumerated earlier record is therefore not the record actually reopened.
Treat documentation and implementation evidence honestly
The library reference broadly describes testzip() as reading archive files and checking CRCs and headers. The pinned source and supplied counterexample reveal a narrower behavior when duplicate names are present. The honest response is not to force one source to erase the other. Documented intent, implementation mechanism, and observed fixture result are separate evidence types.
This is an implementation-specific counterexample, not an affected-version inventory or a security advisory. The recorded evidence does not establish a CVE, current upstream issue status, a fix release, or security impact. A different patch may legitimately change the built-in result. The defensible claim remains the dated CPython 3.13.5 behavior, with its authored inputs and explicit expected ledger, rather than an unverified statement about what all Python installations do today.
Run the complete in-memory integrity experiment
Save the complete source as zip_member_fixture.py using UTF-8 without a byte-order mark, LF line endings, the displayed indentation and blank lines, and one final newline. That serialization produces the stated source digest; display wrapping is not an extra source line. The driver accepts no archive path. It authors every archive in memory and writes results.json beneath a fresh temporary directory beside the script. That location must be writable. Run it in the recorded environment with:
python zip_member_fixture.pyThe fixture uses io.BytesIO in-memory binary streams, so constructing and reading the archive payloads requires no external archive file. It uses ZIP_STORED to simplify the controlled payload layout, not as a production compression recommendation. The driver reads its own source for hashing and writes a report; it does not extract members. Duplicate writer messages are captured through the warnings testing context manager and retained as diagnostics. A captured warning helps explain authoring behavior. Silence would not establish integrity, membership, or acceptance.
"""Owned in-memory ZIP_STORED fixtures; no extraction or external archives."""
import hashlib
import io
import json
import platform
import struct
import tempfile
import warnings
import zipfile
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
APPROVED = {'manifest.txt': b'release=approved\n', 'readme.txt': b'notes\n'}
UNIQUE = list(APPROVED.items())
DUPLICATE = [('manifest.txt', b'release=old\n')] + UNIQUE
def require(ok, message):
if not ok:
raise RuntimeError(message)
def archive(entries):
buffer = io.BytesIO()
with warnings.catch_warnings(record=True) as seen:
warnings.simplefilter('always')
with zipfile.ZipFile(buffer, 'w', compression=zipfile.ZIP_STORED) as z:
for name, content in entries:
info = zipfile.ZipInfo(name, date_time=(2000, 1, 1, 0, 0, 0))
info.compress_type = zipfile.ZIP_STORED
z.writestr(info, content)
return buffer.getvalue(), [str(w.message) for w in seen]
def damage_owned_payload(raw, ordinal):
# Only call this on bytes authored by archive() in this script.
data = bytearray(raw)
with zipfile.ZipFile(io.BytesIO(data)) as z:
info = z.infolist()[ordinal]
require(info.compress_type == zipfile.ZIP_STORED and info.file_size > 0, 'Mutation requires a nonempty stored member')
start = info.header_offset
require(data[start:start + 4] == b'PK\x03\x04', 'Missing local header')
name_len, extra_len = struct.unpack_from('<HH', data, start + 26)
position = start + 30 + name_len + extra_len
require(position + info.file_size <= z.start_dir, 'Unexpected fixture layout')
data[position] ^= 1 # Deliberately leave the recorded CRC unchanged.
return bytes(data), position
def audit(raw):
result = {'archive_sha256': hashlib.sha256(raw).hexdigest(), 'members': []}
with zipfile.ZipFile(io.BytesIO(raw)) as z:
infos = z.infolist()
counts = Counter(info.filename for info in infos)
result['duplicates'] = {name: n for name, n in counts.items() if n > 1}
result['builtin_testzip'] = z.testzip()
for ordinal, info in enumerate(infos):
row = {'ordinal': ordinal, 'name': info.filename, 'header_offset': info.header_offset, 'declared_crc': info.CRC, 'declared_size': info.file_size, 'status': 'FAIL'}
result['members'].append(row)
digest, size = hashlib.sha256(), 0
try:
with z.open(info, 'r') as stream: # ZipInfo, NOT info.filename.
while chunk := stream.read(4096):
digest.update(chunk)
size += len(chunk)
require(size == info.file_size, 'Length mismatch')
row.update(status='OK', actual_size=size, sha256=digest.hexdigest())
except zipfile.BadZipFile as error:
row['error'] = type(error).__name__
# Application policy is independent of CRC integrity.
result['missing'] = sorted(set(APPROVED) - set(counts))
result['unexpected'] = sorted(set(counts) - set(APPROVED))
result['bad_ordinals'] = [r['ordinal'] for r in result['members'] if r['status'] != 'OK']
result['content_matches'] = all(r['status'] == 'OK' and r.get('sha256') == hashlib.sha256(APPROVED.get(r['name'], b'')).hexdigest() for r in result['members'])
result['accepted'] = not (result['duplicates'] or result['missing'] or result['unexpected'] or result['bad_ordinals']) and result['content_matches']
return result
def main():
root = Path(tempfile.mkdtemp(prefix='zip-members-', dir=Path(__file__).parent))
report = {'status': 'FAIL', 'python': platform.python_version(), 'platform': platform.platform(), 'started_utc': datetime.now(timezone.utc).isoformat(), 'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), 'cases': []}
# id, entries, damaged occurrence, expected bad occurrences, builtin result, accepted
cases = [
('Z1', UNIQUE, None, [], None, True),
('Z2', UNIQUE, 0, [0], 'manifest.txt', False),
('Z3', DUPLICATE, None, [], None, False),
('Z4', DUPLICATE, 0, [0], None, False),
('Z5', DUPLICATE, 1, [1], 'manifest.txt', False),
('Z6', [('manifest.txt', APPROVED['manifest.txt'])] + UNIQUE, None, [], None, False),
('Z7', [('readme.txt', b'notes\n')], None, [], None, False),
('Z8', UNIQUE + [('extra.txt', b'extra\n')], None, [], None, False),
('Z9', [('manifest.txt', b'release=rejected\n'), UNIQUE[1]], None, [], None, False),
]
try:
for identity, entries, damaged, bad, builtin, accepted in cases:
row = {'id': identity, 'status': 'FAIL'}
report['cases'].append(row)
raw, messages = archive(entries)
row['writer_warnings'] = messages
if damaged is not None:
raw, row['changed_byte_offset'] = damage_owned_payload(raw, damaged)
row.update(audit(raw))
require(row['bad_ordinals'] == bad, identity + ': wrong member coverage')
require(row['builtin_testzip'] == builtin, identity + ': testzip baseline changed')
require(row['accepted'] is accepted, identity + ': wrong application decision')
require(len(row['members']) == len(entries), identity + ': incomplete inventory')
row['status'] = 'PASS'
require(len(report['cases']) == 9, 'Incomplete cases')
report['status'] = 'PASS_9_CASES'
except Exception as error:
report['error'] = {'type': type(error).__name__, 'message': str(error)}
raise
finally:
report['finished_utc'] = datetime.now(timezone.utc).isoformat()
(root / 'results.json').write_text(json.dumps(report, indent=2) + '\n')
print(root / 'results.json')
if __name__ == '__main__':
main()The report starts in FAIL state, and every case row starts in FAIL state. A row changes to PASS only after four expectations agree: occurrence-level bad ordinals, the pinned built-in return, the application acceptance Boolean, and complete member inventory length. The top-level status changes to PASS_9_CASES only after all nine rows pass and the expected case count is present.
The review labels REPAIR_INTEGRITY_SCAN, REJECT_ARCHIVE, ACCEPT_BOUNDED, and HOLD are conclusions derived from the report. The unchanged driver emits case status, bad_ordinals, policy fields, and accepted; it does not emit a separate decision-label key. Read PASS alongside accepted rather than treating either field as a substitute for the other.
The scan paths have different reporting shapes. On the pinned source, testzip() returns a filename when its scan catches BadZipFile and returns None after normal completion; other exceptions can propagate. Its returned filename cannot distinguish ordinals when names collide, and an early return does not provide a complete inventory. The custom audit records every occurrence in this controlled fixture, including rows after an expected integrity failure. Reviewers can distinguish “no bad filename was returned” from “each occurrence received a complete read attempt and a retained outcome.”
PASS_9_CASES is a test-harness verdict, not a release verdict. It means the expected positive and negative behavior matched; eight archives are intentionally unacceptable. When a check inside the protected case loop raises, the finally block attempts to write the report and the exception is re-raised. Setup before that block can fail, and report writing can itself fail. A missing report, unexpected exception, truncated cases array, or incomplete member inventory therefore requires HOLD, not an optimistic pass.
The fixed timestamp (2000, 1, 1, 0, 0, 0) removes clock variation from the authored entries. The driver also fingerprints the source, each whole archive, and every successfully read member. These hashes support reproducibility and comparison. They do not authenticate an origin or authorize a release. Any edited driver is a different test artifact and needs its own source digest and report.
Establish healthy and uniquely damaged controls
Z1 is the positive control. It contains one approved manifest.txt and one approved readme.txt. testzip() is expected to return None, both per-ZipInfo reads complete, names are unique, required membership is exact, and the two member hashes match the literal approved bytes. Z1 alone receives ACCEPT_BOUNDED.
Z2 is the unique-name corruption control. The driver creates the same two-member archive, locates ordinal 0, flips one payload byte, and leaves the recorded CRC unchanged. Because the only manifest.txt record is both the enumerated occurrence and the name-resolved occurrence, the pinned built-in scan is expected to return manifest.txt. The occurrence-addressed scan opens ordinal 0, reads toward EOF, and records BadZipFile; its bad_ordinals list is [0]. The archive receives REJECT_ARCHIVE for failed integrity.
These controls matter before interpreting duplicates. Z1 shows that the fixture can produce the one acceptable archive. Z2 shows that both scan paths detect a damaged unique occurrence. Without those controls, Z4 could be misread as a general failure to trigger CRC checking, a malformed mutation, or an unrelated parser error.
Keep controlled corruption inside the authored fixture
damage_owned_payload() is not a repair utility for arbitrary ZIP files. It is a setup helper for byte strings authored by the same script. It requires a nonempty ZIP_STORED member, checks the local-header signature, reads filename and extra-field lengths with struct.unpack_from(), and checks that the computed payload ends no later than start_dir. The '<HH' format explicitly selects two little-endian unsigned short values. The struct reference explains that decoding operation, not the ZIP header layout. Only then does the helper flip one byte.
That bounded use is important. The helper assumes the layout created by archive(); start_dir is a zipfile implementation detail, not a public format-validation contract. This is not a ZIP format validator, external-file mutator, recovery command, or parser for unfamiliar archives. The experiment excludes extraction, path handling, symbolic links, ZIP bombs, encryption, signatures, downloads, and production recovery. Expanding the mutator beyond the owned fixture would require a different design and a ZIP-format-level review.
Reproduce the missed earlier duplicate and the later contrast
Z3, Z4, and Z5 begin with the same three occurrence identities. Ordinal 0 is the earlier manifest.txt with old bytes. Ordinal 1 is the later approved manifest.txt. Ordinal 2 is the approved readme.txt. The archive’s name map resolves manifest.txt to the later record, but the occurrence ledger retains all three.
Z3 damages nothing. Every occurrence read completes, bad_ordinals is empty, and the pinned built-in result is None. The archive is still rejected. It contains two occurrences of a name that the application contract permits once, and the earlier manifest bytes are not approved. This is the healthy-CRC duplicate control, not an accepted release bundle.
Z4 flips a byte in ordinal 0 only, leaving that occurrence’s recorded CRC unchanged. In the supplied CPython 3.13.5 run, testzip() returned None. The occurrence scan opened the ordinal-0 ZipInfo, attempted a complete read, caught BadZipFile, and recorded [0]. Ordinals 1 and 2 remained readable. The scanner decision is REPAIR_INTEGRITY_SCAN because the name-based path missed a recorded occurrence. The archive decision is also REJECT_ARCHIVE because integrity failed and a duplicate name exists. For this authored layout, the expected Z4 inventory retains all three records:
Ordinal | Filename | header_offset | Declared bytes | Expected read outcome |
0 | manifest.txt | 0 | 12 | FAIL: BadZipFile |
1 | manifest.txt | 54 | 17 | OK |
2 | readme.txt | 113 | 6 | OK |
Z5 flips a byte in ordinal 1 instead. That later record is selected when manifest.txt is reopened by name. The built-in result is manifest.txt; the occurrence scan records [1]. The pinned source explains why even the first loop iteration, nominally enumerating ordinal 0, can reach the damaged later record. Z4 and Z5 therefore localize the gap to identity resolution, not random CRC behavior: occurrence-addressed reads detect each deliberately changed payload, while name resolution selects which payload the built-in scan reads.
Read each ZipInfo to EOF and retain the failed occurrence
The repair can be stated plainly: read ZipInfo instead of filename. It keeps the object obtained from infolist() and passes that object directly to z.open(info, 'r'). It does not call z.open(info.filename). The reader then consumes the stream in bounded 4096-byte chunks until read() returns an empty byte string. While reading, it updates a SHA-256 object and counts the bytes delivered.
Only after successful completion does the row change from FAIL to OK and receive actual_size and sha256. The driver also checks that the delivered byte count equals info.file_size. If the expected ZIP integrity exception occurs, the row retains its ordinal, filename, header_offset, declared CRC, and declared size, and records the exception type. That leaves enough evidence to say which occurrence failed rather than reducing the event to an ambiguous filename.
The controlled loop continues to the next ZipInfo after BadZipFile. That choice is suitable for this tiny, authored test inventory because the goal is a complete ledger. It should not be generalized into a recovery claim for arbitrary damaged archives. Unexpected exception types are not swallowed. They escape, keep the top-level report from reaching PASS_9_CASES, and produce HOLD until investigated.
A partial read is not a completed integrity check
Opening a member proves that a stream object was created. Reading a prefix proves only that those bytes were delivered. Neither demonstrates that the occurrence was consumed to EOF or that end-of-stream integrity processing completed. The evidence row should therefore remain failed by default until the read loop terminates normally and the length check succeeds.
This playbook does not promise the precise read call on which every Python patch or compression method raises. No member receives OK merely because open succeeded or a prefix was readable. A retained BadZipFile is a known integrity failure and requires REJECT_ARCHIVE. By contrast, an unfinished read with no retained integrity outcome, an unexplained byte-count mismatch, or a missing member row leaves evidence incomplete and requires HOLD. Absence of successful EOF completion is not, by itself, a reason to erase a known rejection.
Reject healthy duplicates without confusing policy and corruption
Z3 and Z6 demonstrate why duplicate-name policy must remain separate from CRC status. Z3 has two manifest.txt occurrences with different bytes. Z6 has two manifest.txt occurrences with identical approved bytes. In both cases, every occurrence read is expected to complete and testzip() is expected to return None. Neither archive has a CRC failure.
Both are nevertheless REJECT_ARCHIVE. The application manifest requires exactly one occurrence of each approved name. Equal bytes do not make the second occurrence disappear, and a name-keyed summary must not silently choose one. Duplicate rejection removes ambiguity about which record a later consumer, tool, or policy check might select. It is a bounded release-bundle rule, not a universal declaration that ZIP archives with duplicate names are unreadable.
Writer warnings reinforce the diagnosis but do not make the decision. The fixture records the duplicate-name warning produced while authoring these archives. A different writer may warn differently or not at all. The reader must count the actual ZipInfo records regardless of warning history.
The archive review stops before extraction. The separate boundary of staged tar extraction concerns what exists after extraction and whether a candidate tree may be promoted. Here there is no extraction, staging directory, path normalization, or deployment pointer. The proof is entirely about occurrence-level reads and an in-memory manifest gate.
Test missing, unexpected and wrong-content members
Z7 contains only readme.txt. That occurrence reads successfully, and testzip() returns None. The archive is rejected because manifest.txt is missing. Integrity success for existing records cannot establish that a required record exists. In this case content_matches is true because the present readme bytes are approved, but accepted is false because the missing-name check is independent. This is why a loop over observed members cannot replace required-membership validation.
Z8 contains the approved pair plus extra.txt. All three occurrence reads complete, and the built-in scan reports no corruption. The archive is rejected because the observed name set exceeds the approved set. The content comparison’s fallback for an unknown name is not an authorization rule: the explicit unexpected-name check rejects the extra regardless of its bytes. An extra member is not permitted merely because its CRC is valid; the bundle contract says no extras.
Z9 contains manifest.txt and readme.txt exactly once, but the manifest bytes are release=rejected\n. The ZIP CRC is internally consistent with those bytes, so both scan paths complete. The archive is still rejected because the bytes do not match the independently authored approved content.
The driver uses SHA-256 through hashlib to fingerprint completed member bytes, then compares each digest with the digest of the literal APPROVED value for that name. This is a digest-based comparison against independently specified content, not direct byte-for-byte comparison code or proof of authenticity. Merely computing a hash is not a policy. The expected bytes must be defined independently before inspecting the archive. Reading candidate content and then declaring its newly computed hash to be the expectation would establish only self-consistency.
CRC and SHA-256 also have different roles in this fixture. CRC is the ZIP reader’s corruption signal for recorded member data. SHA-256 is a convenient content identity for the application ledger. Neither supplies a signature, trusted origin, approval history, or protection against an authorized producer creating the wrong release. Those broader claims remain out of scope.
Reconcile every occurrence with every required decision
The matrix separates literal expected bad ordinals from the per-ZipInfo failures reported in the supplied CPython 3.13.5 commissioning run. The built-in column records its testzip() returns. These reported results are the pinned expectations for a rerun, not observations from an unspecified newer interpreter. The final columns separate the accepted Boolean from the resulting review decision.
Case and fixture | Expected bad ordinals | testzip() return | Reported bad ordinals | accepted | Review decision |
Z1: Approved unique pair | [] | None | [] | True | ACCEPT_BOUNDED |
Z2: Unique manifest; damage 0 | [0] | manifest.txt | [0] | False | REJECT_ARCHIVE |
Z3: Old duplicate; no damage | [] | None | [] | False | REJECT_ARCHIVE |
Z4: Earlier duplicate; damage 0 | [0] | None | [0] | False | REPAIR_INTEGRITY_SCAN |
Z5: Later duplicate; damage 1 | [1] | manifest.txt | [1] | False | REJECT_ARCHIVE |
Z6: Equal-byte duplicate | [] | None | [] | False | REJECT_ARCHIVE |
Z7: Missing manifest.txt | [] | None | [] | False | REJECT_ARCHIVE |
Z8: Unexpected extra.txt | [] | None | [] | False | REJECT_ARCHIVE |
Z9: Wrong manifest content | [] | None | [] | False | REJECT_ARCHIVE |
PASS_9_CASES means every row matched this expected matrix. It does not mean nine archives were acceptable. Z1 is the only ACCEPT_BOUNDED input. Z2 through Z9 are deliberate negative controls, and Z4 additionally demonstrates that the name-based integrity scan needs repair for this baseline.
Keep the report’s nested members array as the detailed occurrence evidence. Each completed audit row preserves ordinal, filename, header_offset, declared CRC, declared size, status, and either a completed size and digest or the expected exception type. An unexpected exception can prevent audit() from returning that inventory to the case row, so a missing array is HOLD. The compact matrix cannot replace it: Z4 needs the failed ordinal 0 and separate readable records for ordinals 1 and 2.
Reconciliation should be mechanical. First compare the number of members rows with the authored entry count. Then compare bad_ordinals with the expected list. Next evaluate duplicate counts, missing names, unexpected names, and content hashes. Finally derive the archive decision from those independent fields. This order prevents a later policy rejection from hiding an earlier coverage defect, and it prevents a clean CRC result from being misreported as release approval.
Maintaining both the earlier-duplicate and later-duplicate mutations is part of repeatable negative-control evidence. If only Z4 existed, a future failure could be blamed on a broken mutator. If only Z5 existed, the name-resolution gap would remain invisible. The pair proves that the same fixture can expose the selected occurrence and the skipped occurrence.
Keep the coverage verdict independent of release approval
There are two verdict axes. Coverage asks whether every recorded occurrence received a completed integrity read or a retained failure record. Release approval asks whether the fully inventoried archive has unique approved names, exact membership, and approved content. A scan can cover every occurrence and still reject Z3, Z6, Z7, Z8, or Z9 for policy. Conversely, a name dictionary can appear to contain the approved manifest.txt and readme.txt while excluding the damaged earlier record in Z4.
Bounded acceptance requires complete coverage, successful integrity reads, and a satisfied manifest policy. A recorded integrity failure produces REJECT_ARCHIVE. Duplicates, missing or extra names, and content mismatches also produce REJECT_ARCHIVE. A demonstrated coverage defect independently produces REPAIR_INTEGRITY_SCAN. An incomplete ledger or unexplained baseline change requires HOLD on the qualification claim; it does not reverse an already established reason to reject the archive. No incomplete run is an acceptance.
Replace the scan without discarding useful historical evidence
The practical repair is small: retain the ZipInfo list, open each object rather than its filename, read each stream to EOF, and write an occurrence ledger before applying manifest policy. There is no need to patch a production Python installation or monkey-patch zipfile internals. A project can own this bounded scanner as application code and keep the pinned built-in result as a regression observation.
Do not “repair” a rejected archive by deleting earlier duplicate entries until a convenient check passes. That destroys the bytes that explain the mismatch and may silently select content that was never independently approved. When an accepted bundle is needed, rebuild it from an approved source of truth with one occurrence per required name, then run the complete integrity and manifest checks on the rebuilt bytes.
Retention should follow the project’s evidence policy. Keeping the rejected archive preserves the bytes needed for later reinspection. Keeping only its digest identifies those bytes but cannot reconstruct them. In this authored experiment, the retained source and case definition provide a reproducible fixture; that is not a general substitute for retaining an external artifact. Whatever the retention choice, cleanup or rebuilding must not rewrite the historical report as though the original archive had passed.
Content and occurrence integrity are not the only artifact contracts. File-mode preservation as another artifact contract illustrates a separate dimension that this in-memory ZIP playbook intentionally excludes. A future release gate may combine content, modes, provenance, and deployment policy, but each dimension should keep its own evidence and failure reason.
Decide what to do when a rerun behaves differently
A rerun must record its actual Python patch, platform string, start and finish times, source digest, archive fingerprints, warnings, and complete cases array. Preserve the commissioning report separately. A new run on CPython 3.13.5 is still a distinct execution, and a run on another patch must name that patch. Recompute the driver digest each time: unchanged source should yield the same value, while edited source needs its own fingerprint.
Suppose Z4 returns manifest.txt on a later patch instead of None. The current driver deliberately treats that as testzip baseline changed and stops the pinned expectation. That failure is useful. Retain the report and inspect the new implementation and occurrence ledger. A legitimate upstream correction may have made the built-in scan occurrence-aware. A different result could also come from a fixture edit, runtime packaging difference, or unrelated exception. The test should not silently rewrite its expected value before the cause is understood.
Even if the built-in behavior is corrected, the application rules remain necessary. testzip() does not establish that names are unique, required names are present, unexpected names are absent, or completed bytes match the approved manifest. Z3, Z6, Z7, Z8, and Z9 continue to exercise those independent decisions.
A changed documentation page is not execution evidence either. The maintenance-branch reference can describe an updated contract while a local interpreter still runs older code. Record source review, documentation review, and behavioral execution as separate lines of evidence. When any required line is missing or contradictory, use HOLD until the discrepancy is resolved.
Do not turn a version difference into a fabricated observation
The commissioning outcomes are attributed to the dated CPython 3.13.5 run, and the pinned source explains its name-resolution mechanism. Neither a matching source inspection nor a result from one build establishes behavior on another Python patch, operating system, or implementation. A later reproduction needs its own runtime details and report even when its expected matrix is unchanged. Keep the historical observation, the future expectation, and any newly observed result distinguishable.
For a new execution, record what actually ran and retain its report. Generate new timestamps and runtime details rather than copying the old record. Recompute the source digest instead of assuming it; identical source bytes can legitimately have the same digest across runs. A syntax review is not a behavioral pass, and a documentation diff is not a runtime result. If the pinned baseline cannot be reproduced with complete evidence, use HOLD while investigating that qualification failure.
Assign archive ownership and regression responsibilities
The package owner defines the approved member names and exact bytes. That owner decides whether one occurrence is required, whether extra members are forbidden, and when the manifest changes. The reader maintainer preserves central-directory identity, completes every read, records failures without collapsing them by name, and keeps unexpected exceptions fail-closed. QA owns the nine-case matrix, especially both duplicate corruption controls, and checks that failures occur for the intended reason.
Changes to any of four boundaries require requalification: the Python interpreter, archive authoring method, integrity scanner, or approved manifest. A patch upgrade can alter built-in behavior. A writer change can alter warnings or layout. A scanner refactor can accidentally reopen by name. A manifest update can make yesterday’s accepted bytes obsolete. Each change needs a new execution report and a freshly computed source digest; an interpreter-only change does not require unchanged driver bytes to acquire a different hash.
This ownership model is a narrow example of software verification and delivery practice. Automation is valuable when its contract is explicit, its negative controls are retained, and its output distinguishes coverage from approval. A green command without that traceability is weaker evidence than a well-scoped rejection with a complete ledger.
Develop stronger artifact-review practice through software engineering
Archive readers are one place to practice broader engineering habits: define acceptance before inspection, preserve identity before summarizing, separate integrity from policy, retain negative controls, and stop on incomplete evidence. Those habits transfer to configuration loaders, package validators, API clients, database migrations, and release automation even when the specific ZIP behavior does not.
For broader foundations, review Refonte Learning’s Software Engineering programme. Its official page lists three months and 12–14 hours per week, with software lifecycle, full-stack development, cloud and microservices, application security, performance, and practical projects among its topics. The programme-specific overview addresses people pursuing or having completed relevant undergraduate study. The page also describes a Training Certificate and a Certificate of Internship after successful completion. Its mention of a potential internship should not be read as an individual placement guarantee.
Those general programme foundations do not establish that this specific zipfile audit or a named CPython patch is taught. The immediate engineering action remains concrete: keep the nine-case fixture under version control, retain occurrence identity through every read, and evaluate the independent manifest separately. Reserve ACCEPT_BOUNDED for complete evidence and the one approved unique pair. A green diagnostic is useful only when its coverage and its acceptance contract are both understood.
