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
9 changes: 7 additions & 2 deletions Lib/compression/_common/_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,13 @@ def readall(self):
# sys.maxsize means the max length of output buffer is unlimited,
# so that the whole input buffer can be decompressed within one
# .decompress() call.
while data := self.read(sys.maxsize):
chunks.append(data)
try:
while data := self.read(sys.maxsize):
chunks.append(data)
except EOFError as exc:
if chunks:
exc.partial_data = b"".join(chunks)
raise

return b"".join(chunks)

Expand Down
11 changes: 9 additions & 2 deletions Lib/gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -677,9 +677,10 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0):
return header + gzip_data[10:]


def decompress(data):
def decompress(data, *, partial=False):
"""Decompress a gzip compressed string in one shot.
Return the decompressed string.
If partial is True, returns data decompressed prior to an unexpected EOF.
"""
decompressed_members = []
while True:
Expand All @@ -691,8 +692,14 @@ def decompress(data):
# Read all the data except the header
decompressed = do.decompress(data[fp.tell():])
if not do.eof or len(do.unused_data) < 8:
raise EOFError("Compressed file ended before the end-of-stream "
if partial and decompressed:
decompressed_members.append(decompressed)
return b"".join(decompressed_members)
exc = EOFError("Compressed file ended before the end-of-stream "
"marker was reached")
if decompressed:
exc.partial_data = decompressed
raise exc
crc, length = struct.unpack("<II", do.unused_data[:8])
if crc != zlib.crc32(decompressed):
raise BadGzipFile("CRC check failed")
Expand Down
11 changes: 11 additions & 0 deletions Lib/test/test_gzip.py
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,17 @@ def test_decompress_missing_trailer(self):
compressed_data = gzip.compress(data1)
self.assertRaises(EOFError, gzip.decompress, compressed_data[:-8])

def test_decompress_truncated_partial(self):
compressed_data = gzip.compress(data1)
truncated = compressed_data[:-8]
self.assertEqual(gzip.decompress(truncated, partial=True), data1)
try:
gzip.decompress(truncated)
except EOFError as exc:
self.assertEqual(getattr(exc, "partial_data", None), data1)
else:
self.fail("EOFError was not raised")

def test_truncated_header(self):
truncated_headers = [
b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00", # Missing OS byte
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add partial data recovery support in :func:`gzip.decompress` via ``partial=True`` and attach ``partial_data`` attribute to :exc:`EOFError` on truncated gzip streams.
Loading