Skip to content

ZipFile.close() writes wrong offsets after a failed repack() #156434

Description

@fedonman

Bug description:

_ZipRepacker.repack() updates ZipInfo.header_offset for each member before moving that member's bytes, so a failing _copy_bytes() leaves the in-memory offsets describing a layout that was never written. remove() has already set _didModify, so a later normal close() commits a central directory built from those offsets: the caller handles the OSError, closes cleanly, and gets an archive that zipfile itself cannot read.

import io, zipfile

class Flaky(io.BytesIO):
    countdown = None
    def write(self, b):
        if self.countdown is not None:
            self.countdown -= 1
            if self.countdown < 0:
                raise OSError(28, 'No space left on device')
        return super().write(b)

buf = io.BytesIO()
with zipfile.ZipFile(buf, 'w') as zf:
    for c in 'abcd':
        zf.writestr(c + '.txt', c.upper().encode() * 5000)

flaky = Flaky(buf.getvalue())
with zipfile.ZipFile(flaky, 'a') as zf:
    flaky.countdown = 1
    try:
        zf.repack([zf.remove('b.txt')], chunk_size=4096)
    except OSError as exc:
        print('repack raised:', exc)
    flaky.countdown = None        # space freed; the caller closes normally

with zipfile.ZipFile(flaky) as zf:
    print('testzip:', zf.testzip())
repack raised: [Errno 28] No space left on device
testzip: c.txt

Expected: after the caller has handled the OSError, close() should either leave a readable archive or raise, rather than committing offsets that no write produced.

remove() and repack() are new in 3.16, so no released version is affected.

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions