How to Read Duplicate File Names in a ZIP File with Python
Do both note.txt entries survive a Base64 round trip?
Decode the Base64 file, enumerate its ZIP directory, and read each entry by its ZipInfo object: our two note.txt entries contain different data, while a filename lookup returns only one.
This is a directory-identity problem after decoding. A filename is not enough to distinguish entries when names repeat. Base64 Decode can restore the archive bytes; the experiment below checks which ZIP entry a subsequent read actually selects.
The same name, different bytes
We created duplicates.zip on 2026-10-18 with Python 3.12.14. Both entries are named note.txt. Directory index 0 contains FIRST: red sample.\n, measuring 19 bytes; index 1 contains SECOND: blue sample.\n, measuring 21 bytes. The displayed backslash-n represents one LF byte. These are synthetic public records, and their combined payload is 40 bytes.
We selected ZIP_STORED, so our entries have no compression-size ambiguity: stored and uncompressed sizes are equal. We fixed the timestamps to 1980-01-01, used no archive comment, and preserved both directory records. Creating the duplicate archive emitted Duplicate name: 'note.txt'. We recorded that warning rather than renaming an entry or replacing its content to silence it.
| Index | Name | Central offset | Local offset | Payload bytes | CRC32 | Content |
|---|---|---|---|---|---|---|
| 0 | note.txt | 116 | 0 | 19 | dc058d19 | FIRST: red sample. + LF |
| 1 | note.txt | 170 | 57 | 21 | 56052464 | SECOND: blue sample. + LF |
Our complete archive measured 246 bytes and encoded to 328 Base64 characters. Strict Base64 decoding produced a byte-for-byte identical 246-byte archive. The two payloads account for 19 + 21 = 40 bytes. Local headers and their names account for 76 bytes; the central directory occupies 108; the end record occupies 22. Those measured parts add to 40 + 76 + 108 + 22 = 246.
What an indexed read recovered
We retained the list from infolist() without converting it to a filename-keyed dictionary. Reading the ZipInfo at index 0 recovered the 19-byte FIRST record with CRC32 dc058d19. Reading index 1 recovered the 21-byte SECOND record with CRC32 56052464. Our independent byte parser located both central records and checked their corresponding local headers, payload lengths, and calculated CRC values.
On that same decoded archive, archive.read("note.txt") returned only the SECOND record, measuring 21 bytes. The name list still contained two note.txt strings, while its set contained one distinct name. The archive had not lost the FIRST payload during Base64 decoding: the indexed read recovered it from the same bytes. The filename-only read simply did not return both records.
| Fixture | Archive bytes | Directory entries | Distinct names | Total payload bytes | Name-read bytes | Name-read record |
|---|---|---|---|---|---|---|
| single | 133 | 1 | 1 | 19 | 19 | FIRST |
| duplicates | 246 | 2 | 1 | 40 | 21 | SECOND |
| reversed | 246 | 2 | 1 | 40 | 19 | FIRST |
For our order control, we wrote SECOND first and FIRST second under the same repeated name. The archive still measured 246 bytes, held two directory entries and one distinct name, and retained 40 payload bytes. A filename read now returned FIRST, measuring 19 bytes. Index-based reads still recovered both records. This reversal shows the observed selection followed entry order in this Python run, rather than a preference for the red or blue text.
The single-entry control contained FIRST alone. It measured 133 archive bytes and encoded to 180 Base64 characters. Both an indexed read and a filename read returned the same 19-byte payload. That control cannot expose the duplicate-name difference by itself. In the duplicate fixture, our Node byte parser independently recovered both directory rows, their offsets, and the same 40 payload bytes; its CRC calculations matched the Python inspector.
Reproduce the directory-index check
Download duplicates.b64.txt, keep its filename, and run the following example in the same directory. It reads the public fixture into memory without extracting either entry onto disk.
import base64, io, zipfile
from pathlib import Path
raw = base64.b64decode(
Path("duplicates.b64.txt").read_text("ascii"), validate=True)
with zipfile.ZipFile(io.BytesIO(raw)) as archive:
entries = archive.infolist()
for index, entry in enumerate(entries):
content = archive.read(entry)
print(index, entry.filename, len(content),
f"{entry.CRC:08x}", repr(content))
named = archive.read("note.txt")
print("by name:", len(named), repr(named))
0 note.txt 19 dc058d19 b'FIRST: red sample.\n'
1 note.txt 21 56052464 b'SECOND: blue sample.\n'
by name: 21 b'SECOND: blue sample.\n'
Download the sample generator and directory inspector and run python reproduce.py to create all three fixtures. It writes their Base64 values, original archives, per-index text outputs, filename-read outputs, and measurement JSON into a generated directory. The inspector retains the duplicate entries in directory order and verifies that the recorded structural sizes sum to each complete archive's measured length.
Downloads: duplicate archive, preserved directory index, reversed control, single control, raw measurements. The duplicate archive's SHA-256 is 5b30506bff47b747a1fcbe885c8acdf305ab85dd7c253dedb6bf76e2be8cb5f3.
Watch the measured directory experiment
Watch on YouTube. Original diagrams and synthesized English narration; English subtitles included.
What this test does and does not establish
These measurements concern the supplied ZIP_STORED fixtures and the named Python API. They do not establish how every ZIP viewer or extractor resolves duplicate names. Python documents directory-order enumeration and ZipInfo access for duplicate names. The general API advice is documented behavior; our particular files, byte accounting, and observed outputs are the experiment.
Our script only parses the small, unencrypted, stored fixtures it generates. It is not a general ZIP64, encrypted ZIP, or compressed-entry validator. Do not use this unlimited in-memory example for untrusted uploads. Apply archive and output limits separately in production. CRC32 is a corruption check, not proof of authenticity. This test preserves records in memory; it does not test extraction overwrite policies or claim that a particular desktop app loses a file.
Why do I see one filename when my ZIP has two entries?
Count directory entries separately from distinct names. If names repeat, retain the directory index and entry metadata, and read each entry object. Verify recovered bytes against the expected record before deciding that the Base64 conversion discarded data.