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
8 changes: 6 additions & 2 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,8 +427,12 @@ def _init_write_gz(self, compresslevel, mtime):
self.zlib.DEF_MEM_LEVEL,
0)
if mtime is None:
mtime = int(time.time())
timestamp = struct.pack("<L", mtime)
mtime = time.time()
# gh-133998: substitute 0 for an out-of-range mtime and coerce to int,
# mirroring gzip (RFC 1952), so struct.pack cannot raise struct.error.
if not 0 <= mtime < 2**32:
mtime = 0
timestamp = struct.pack("<L", int(mtime))
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
if self.name.endswith(".gz"):
self.name = self.name[:-3]
Expand Down
38 changes: 38 additions & 0 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -1885,6 +1885,44 @@ def test_create_with_mtime(self):
fobj.read()
self.assertEqual(fobj.mtime, 0)

def test_create_with_mtime_out_of_range(self):
# gh-133998: an mtime outside the gzip header's 32-bit range is
# stored as 0 rather than raising struct.error.
for mtime in (-1, 2**32):
with self.subTest(mtime=mtime):
tarfile.open(tmpname, self.mode, mtime=mtime).close()
with self.open(tmpname, 'r') as fobj:
fobj.read()
self.assertEqual(fobj.mtime, 0)
os_helper.unlink(tmpname)

def test_create_with_mtime_at_boundary(self):
# gh-133998: the largest in-range mtime is preserved, not clamped.
mtime = 2**32 - 1
tarfile.open(tmpname, self.mode, mtime=mtime).close()
with self.open(tmpname, 'r') as fobj:
fobj.read()
self.assertEqual(fobj.mtime, mtime)

def test_create_with_out_of_range_clock(self):
# gh-133998: an out-of-range system clock (mtime defaulting to
# time.time()) is stored as 0 rather than raising struct.error.
for clock in (-1, 2**32):
with self.subTest(clock=clock):
with unittest.mock.patch('time.time', return_value=float(clock)):
tarfile.open(tmpname, self.mode).close()
with self.open(tmpname, 'r') as fobj:
fobj.read()
self.assertEqual(fobj.mtime, 0)
os_helper.unlink(tmpname)

def test_create_with_float_mtime(self):
# gh-133998: a float mtime is truncated like gzip, not rejected.
tarfile.open(tmpname, self.mode, mtime=123456789.9).close()
with self.open(tmpname, 'r') as fobj:
fobj.read()
self.assertEqual(fobj.mtime, 123456789)

def test_create_without_mtime(self):
before = int(time.time())
tarfile.open(tmpname, self.mode).close()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix :func:`tarfile.open` in streaming write mode (``w|gz``) raising
:exc:`struct.error` when given an *mtime* outside the range of the gzip
header's 32-bit timestamp field. Out-of-range values are now stored as ``0``,
matching :mod:`gzip`.
Loading