Skip to content

gh-154481: Fix reading multi-frame Zstandard members in zipfile - #154537

Open
lpyu001 wants to merge 4 commits into
python:mainfrom
lpyu001:zip
Open

lpyu001 wants to merge 4 commits into
python:mainfrom
lpyu001:zip

Conversation

@lpyu001

@lpyu001 lpyu001 commented Jul 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fix zipfile reading of Zstandard-compressed archive members containing multiple concatenated frames.

ZstdDecompressor intentionally stops after one frame and stores any remaining compressed input in unused_data. ZipExtFile previously treated the decompressor's eof state as the end of the entire ZIP member. As a result, later frames were ignored, which could truncate the returned data or cause an incorrect CRC-32 error.

Fix

When a Zstandard frame ends, ZipExtFile now:

  1. Retrieves any remaining compressed input from unused_data.
  2. Creates a new ZstdDecompressor for the next frame.
  3. Continues with unused_data, or reads more compressed data when the frame boundary falls between archive reads.
  4. Marks the ZIP member as finished only after all compressed input and buffered unused data have been consumed.

Invalid Zstandard data is reported as BadZipFile, with the original ZstdError preserved as its cause.

This keeps the single-frame semantics of ZstdDecompressor unchanged and handles concatenated frames at the zipfile streaming layer.

Tests

Regression tests cover:

  • Two frames in one archive read and frames split across reads.
  • Seeking forward and backward across a frame boundary.
  • A skippable frame between data frames and invalid trailing data.
  • A replacement decompressor without optional state attributes.

@lpyu001

lpyu001 commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

cc @emmatyping

@emmatyping emmatyping left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for working on this!

I think there are a few changes that should ensure this change is robust and won't break anyone:

  1. Currently, this will change the exception raised when trailing bytes that are not a zstd frame from BadZipFile (on a mismatched crc/length check) to ZstdError. I think if there is a ZstdError, we should probably raise BadZipFile from that exception.
  2. I'd like a few more tests to ensure this works as expected:
    a. seek() across a frame boundary
    b. trailing data and skippable frames
    c. frames split across reads

Comment thread Lib/zipfile/__init__.py Outdated
Comment on lines +1200 to +1201
# Continue with the next Zstandard frame.
data = self._decompressor.unused_data

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you want to check for .eof first, so we know the decompressor is on a frame boundary.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since the decompressor can be monkeypatched (ref https://github.com/lpyu001/cpython/blob/2468c35c3b3375fe0745fa5b5429d47098ebb45e/Lib/test/test_zipfile/test_core.py#L4938), we should use getattr() instead of direct attribute access.

Preserve BadZipFile with the ZstdError as its cause. Cover seeking across frames, skippable frames, trailing data, split reads, and replacement decompressors.
@lpyu001

lpyu001 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for your review! I've updated the code accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants