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
22 changes: 19 additions & 3 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,13 +661,15 @@ class _FileInFile(object):
object.
"""

def __init__(self, fileobj, offset, size, name, blockinfo=None):
def __init__(self, fileobj, offset, size, name, blockinfo=None,
exception=OSError):
self.fileobj = fileobj
self.offset = offset
self.size = size
self.position = 0
self.name = name
self.closed = False
self.exception = exception

if blockinfo is None:
blockinfo = [(0, size)]
Expand Down Expand Up @@ -744,7 +746,10 @@ def read(self, size=None):
length = min(size, stop - self.position)
if data:
self.fileobj.seek(offset + (self.position - start))
b = self.fileobj.read(length)
try:
b = self.fileobj.read(length)
except self.exception as e:
raise ReadError(f"invalid compressed data: {e}") from e
if len(b) != length:
raise ReadError("unexpected end of data")
buf += b
Expand All @@ -767,7 +772,8 @@ class ExFileObject(io.BufferedReader):

def __init__(self, tarfile, tarinfo):
fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data,
tarinfo.size, tarinfo.name, tarinfo.sparse)
tarinfo.size, tarinfo.name, tarinfo.sparse,
exception=tarfile.exception)
super().__init__(fileobj)
#class ExFileObject

Expand Down Expand Up @@ -1789,6 +1795,12 @@ class TarFile(object):

fileobject = ExFileObject # The file-object for extractfile().

exception = OSError # The exception raised by the underlying
# fileobj.read() on corrupt compressed
# data past the header (see gzopen() etc.
# for the more specific exception types
# used by each compression format).

extraction_filter = None # The default filter for extraction.

def __init__(self, name=None, mode="r", fileobj=None, format=None,
Expand Down Expand Up @@ -2034,6 +2046,7 @@ def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs):

try:
from gzip import GzipFile
import zlib
except ImportError:
raise CompressionError("gzip module is not available") from None

Expand All @@ -2056,6 +2069,7 @@ def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs):
fileobj.close()
raise
t._extfileobj = False
t.exception = zlib.error
return t

@classmethod
Expand Down Expand Up @@ -2112,6 +2126,7 @@ def xzopen(cls, name, mode="r", fileobj=None, preset=None, **kwargs):
fileobj.close()
raise
t._extfileobj = False
t.exception = LZMAError
return t

@classmethod
Expand Down Expand Up @@ -2147,6 +2162,7 @@ def zstopen(cls, name, mode="r", fileobj=None, level=None, options=None,
fileobj.close()
raise
t._extfileobj = False
t.exception = ZstdError
return t

# All *open() methods are registered here.
Expand Down
66 changes: 66 additions & 0 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,72 @@ def runTest(self):
tarfile.open(fileobj=f, mode='r|gz')


class FileInFileExceptionWrappingTest(unittest.TestCase):
"""
See: https://github.com/python/cpython/issues/156057

_FileInFile.read() must wrap whatever exception type its
underlying fileobj.read() raises for corrupt compressed data,
using the exception class attribute each TarFile.*open()
classmethod configures, in tarfile.ReadError -- and must not
swallow an exception of an unconfigured type.
"""

class _FakeFileobj:
def __init__(self, exc):
self.exc = exc

def seek(self, position):
pass

def read(self, size):
raise self.exc

def test_wraps_configured_exception_type(self):
class Boom(Exception):
pass

fif = tarfile._FileInFile(
self._FakeFileobj(Boom("simulated corrupt compressed data")),
offset=0, size=10, name="member", exception=Boom)
with self.assertRaises(tarfile.ReadError):
fif.read()

def test_does_not_wrap_unconfigured_exception_type(self):
class Boom(Exception):
pass

class Unrelated(Exception):
pass

fif = tarfile._FileInFile(
self._FakeFileobj(Unrelated("not the configured exception type")),
offset=0, size=10, name="member", exception=Boom)
with self.assertRaises(Unrelated):
fif.read()


@support.requires_gzip()
class GzipExtractfileWrapsCompressionErrorTest(unittest.TestCase):
"""
See: https://github.com/python/cpython/issues/156057

Confirms the real gzopen() -> ExFileObject -> _FileInFile wiring:
a zlib.error raised while reading a member's compressed data past
the header surfaces as tarfile.ReadError, not the raw zlib.error.
"""

def test_extractfile_read_wraps_zlib_error(self):
with tarfile.open(gzipname, mode='r:gz') as tar:
tarinfo = tar.getmember("ustar/regtype")
with tar.extractfile(tarinfo) as fobj:
with unittest.mock.patch.object(
tar.fileobj, "read",
side_effect=zlib.error("simulated corrupt data")):
with self.assertRaises(tarfile.ReadError):
fobj.read()


class MemberReadTest(ReadTest, unittest.TestCase):

def _test_member(self, tarinfo, chksum=None, **kwargs):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Wrap the underlying compression library's exception in :exc:`tarfile.ReadError`
when a member's compressed data is corrupt past its header, for all of
:mod:`tarfile`'s supported compression formats (gzip, bzip2, lzma, zstd).
Previously the raw exception (e.g. :exc:`zlib.error`) could leak through
:meth:`tarfile.TarFile.extractfile`. Patch by Jason Mak.
Loading