Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,94 @@ class ZstdTestsWithSourceFile(AbstractTestsWithSourceFile,
unittest.TestCase):
compression = zipfile.ZIP_ZSTANDARD

def _make_archive(self, data, compressed):
class Compressor:
def compress(self, value):
return compressed

def flush(self):
return b''

archive = io.BytesIO()
with mock.patch.object(zipfile, '_get_compressor',
return_value=Compressor()):
with zipfile.ZipFile(archive, 'w', self.compression) as zipfp:
zipfp.writestr('multiframe', data)
return archive

def test_read_multiframe(self):
first = b'first frame' * 20
second = b'second frame' * 20
data = first + second
compressed = (zipfile.zstd.compress(first) +
zipfile.zstd.compress(second))
archive = self._make_archive(data, compressed)

with zipfile.ZipFile(archive) as zipfp:
self.assertEqual(zipfp.read('multiframe'), data)

with zipfile.ZipFile(archive) as zipfp:
with zipfp.open('multiframe') as fp:
self.assertEqual(fp.seek(len(first) + 2), len(first) + 2)
self.assertEqual(fp.read(), second[2:])
self.assertEqual(fp.seek(len(first) - 2), len(first) - 2)
self.assertEqual(fp.read(4),
data[len(first) - 2:len(first) + 2])

# Read one compressed byte at a time, splitting frames across reads.
with zipfile.ZipFile(archive) as zipfp:
with zipfp.open('multiframe') as fp:
fp.MIN_READ_SIZE = 1
self.assertEqual(b''.join(iter(lambda: fp.read(1), b'')),
data)

def test_read_multiframe_with_skippable_frame(self):
first = b'first frame'
second = b'second frame'
skippable = struct.pack('<II', 0x184D2A50, 4) + b'skip'
compressed = (zipfile.zstd.compress(first) + skippable +
zipfile.zstd.compress(second))
archive = self._make_archive(first + second, compressed)
with zipfile.ZipFile(archive) as zipfp:
self.assertEqual(zipfp.read('multiframe'), first + second)

def test_read_multiframe_with_trailing_data(self):
compressed = (zipfile.zstd.compress(b'first frame') +
b'invalid trailing bytes')
archive = self._make_archive(b'first frame' + b'second frame',
compressed)
with zipfile.ZipFile(archive) as zipfp:
with self.assertRaises(zipfile.BadZipFile) as cm:
zipfp.read('multiframe')
self.assertIsInstance(cm.exception.__cause__, zipfile.zstd.ZstdError)

def test_read_monkeypatched_zstd_decompressor(self):
class FrameDecompressor:
eof = False

def decompress(self, data, max_length=-1):
if data:
self.eof = True
return data

class StreamDecompressor:
def decompress(self, data, max_length=-1):
return data

data = b'content'
archive = self._make_archive(data, data)
for decompressor in FrameDecompressor, StreamDecompressor:
with self.subTest(decompressor=decompressor.__name__):
with mock.patch.object(zipfile, '_get_decompressor',
side_effect=lambda _: decompressor()):
with zipfile.ZipFile(archive) as zipfp:
self.assertEqual(zipfp.read('multiframe'), data)
with zipfp.open('multiframe') as fp:
fp.MIN_READ_SIZE = 1
self.assertEqual(
b''.join(iter(lambda: fp.read(1), b'')), data)


class AbstractTestZip64InSmallFiles:
# These tests test the ZIP64 functionality without using large files,
# see test_zipfile64 for proper tests.
Expand Down
54 changes: 38 additions & 16 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,13 @@ def _read1(self, n):
data = self._decompressor.unconsumed_tail
if n > len(data):
data += self._read2(n - len(data))
elif (self._compress_type == ZIP_ZSTANDARD and
getattr(self._decompressor, "eof", False)):
# Continue with the next Zstandard frame.
data = getattr(self._decompressor, "unused_data", b'')
self._decompressor = _get_decompressor(self._compress_type)
if not data:
data = self._read2(n)
elif self._compress_type == ZIP_STORED:
data = self._read2(n)
else:
Expand All @@ -1220,22 +1227,37 @@ def _read1(self, n):
# DEFLATE path above) so that a small compressed member cannot
# expand into one unbounded read.
try:
data = self._decompressor.decompress(data, max(n, self.MIN_READ_SIZE))
except TypeError:
# See MonkeypatchedDecompressorTests in test_core.py
warnings._deprecated(
'one-argument decompress()',
'The decompress() method of '
+ type(self._decompressor).__name__
+ ' should take two arguments, data and max_length.'
+ ' One-argument calls will stop working before'
+ ' Python 3.21.',
remove=(3, 21),
stacklevel=4)
data = self._decompressor.decompress(data)
self._eof = (self._decompressor.eof or
self._compress_left <= 0 and
getattr(self._decompressor, "needs_input", True))
try:
data = self._decompressor.decompress(data, max(n, self.MIN_READ_SIZE))
except TypeError:
# See MonkeypatchedDecompressorTests in test_core.py
warnings._deprecated(
'one-argument decompress()',
'The decompress() method of '
+ type(self._decompressor).__name__
+ ' should take two arguments, data and max_length.'
+ ' One-argument calls will stop working before'
+ ' Python 3.21.',
remove=(3, 21),
stacklevel=4)
data = self._decompressor.decompress(data)
except Exception as exc:
if (self._compress_type == ZIP_ZSTANDARD and
zstd is not None and isinstance(exc, zstd.ZstdError)):
raise BadZipFile(
f"Bad Zstandard data for file {self.name!r}") from exc
raise
if self._compress_type == ZIP_ZSTANDARD:
if getattr(self._decompressor, "eof", False):
self._eof = (self._compress_left <= 0 and
not getattr(self._decompressor, "unused_data", b''))
else:
self._eof = (self._compress_left <= 0 and
getattr(self._decompressor, "needs_input", True))
else:
self._eof = (self._decompressor.eof or
self._compress_left <= 0 and
getattr(self._decompressor, "needs_input", True))

data = data[:self._left]
self._left -= len(data)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Fix :mod:`zipfile` reading of Zstandard-compressed archive members containing
multiple frames, which could previously return truncated data or raise a
spurious CRC error.
Loading