Skip to content

Commit f4de693

Browse files
committed
gh-154481: Fix reading multi-frame Zstandard members in zipfile
1 parent 7745710 commit f4de693

3 files changed

Lines changed: 43 additions & 0 deletions

File tree

Lib/test/test_zipfile/test_core.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,36 @@ class ZstdTestsWithSourceFile(AbstractTestsWithSourceFile,
715715
unittest.TestCase):
716716
compression = zipfile.ZIP_ZSTANDARD
717717

718+
def test_read_multiframe(self):
719+
first = b'first frame' * 20
720+
second = b'second frame' * 20
721+
data = first + second
722+
723+
class MultiframeCompressor:
724+
def compress(self, value):
725+
value = bytes(value)
726+
return (zipfile.zstd.compress(value[:len(first)]) +
727+
zipfile.zstd.compress(value[len(first):]))
728+
729+
def flush(self):
730+
return b''
731+
732+
archive = io.BytesIO()
733+
with mock.patch.object(zipfile, '_get_compressor',
734+
return_value=MultiframeCompressor()):
735+
with zipfile.ZipFile(archive, 'w', self.compression) as zipfp:
736+
zipfp.writestr('multiframe', data)
737+
738+
with zipfile.ZipFile(archive) as zipfp:
739+
self.assertEqual(zipfp.read('multiframe'), data)
740+
741+
# Exercise a frame boundary between two reads from the archive.
742+
with zipfile.ZipFile(archive) as zipfp:
743+
with zipfp.open('multiframe') as fp:
744+
fp.MIN_READ_SIZE = 1
745+
self.assertEqual(b''.join(iter(lambda: fp.read(1), b'')),
746+
data)
747+
718748
class AbstractTestZip64InSmallFiles:
719749
# These tests test the ZIP64 functionality without using large files,
720750
# see test_zipfile64 for proper tests.

Lib/zipfile/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,6 +1186,13 @@ def _read1(self, n):
11861186
data = self._decompressor.unconsumed_tail
11871187
if n > len(data):
11881188
data += self._read2(n - len(data))
1189+
elif (self._compress_type == ZIP_ZSTANDARD and
1190+
self._decompressor.eof):
1191+
# Continue with the next Zstandard frame.
1192+
data = self._decompressor.unused_data
1193+
self._decompressor = _get_decompressor(self._compress_type)
1194+
if not data:
1195+
data = self._read2(n)
11891196
else:
11901197
data = self._read2(n)
11911198

@@ -1199,6 +1206,10 @@ def _read1(self, n):
11991206
not self._decompressor.unconsumed_tail)
12001207
if self._eof:
12011208
data += self._decompressor.flush()
1209+
elif self._compress_type == ZIP_ZSTANDARD:
1210+
data = self._decompressor.decompress(data)
1211+
self._eof = (self._compress_left <= 0 and
1212+
not self._decompressor.unused_data)
12021213
else:
12031214
data = self._decompressor.decompress(data)
12041215
self._eof = self._decompressor.eof or self._compress_left <= 0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix :mod:`zipfile` to correctly read Zstandard-compressed archive members
2+
that could previously be truncated or incorrectly rejected.

0 commit comments

Comments
 (0)