Skip to content

Commit c6c0608

Browse files
gh-133998: Fix tarfile error on out-of-range mtime in streaming gzip mode (#151828)
tarfile.open(..., "w|gz", mtime=...) packed mtime into the gzip header's 32-bit field with no range check, so a value < 0 or >= 2**32 raised struct.error. Mirror the merged gzip fix: substitute 0 for out-of-range values and coerce to int (RFC 1952), so floats and out-of-range system clocks behave the same as in gzip and the non-streaming "w:gz" path (which already delegates to GzipFile and was unaffected). Co-authored-by: Victor Stinner <vstinner@python.org>
1 parent fbbf4f4 commit c6c0608

3 files changed

Lines changed: 48 additions & 2 deletions

File tree

‎Lib/tarfile.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -453,8 +453,12 @@ def _init_write_gz(self, compresslevel, mtime):
453453
self.zlib.DEF_MEM_LEVEL,
454454
0)
455455
if mtime is None:
456-
mtime = int(time.time())
457-
timestamp = struct.pack("<L", mtime)
456+
mtime = time.time()
457+
# gh-133998: substitute 0 for an out-of-range mtime and coerce to int,
458+
# mirroring gzip (RFC 1952), so struct.pack cannot raise struct.error.
459+
if not 0 <= mtime < 2**32:
460+
mtime = 0
461+
timestamp = struct.pack("<L", int(mtime))
458462
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
459463
if self.name.endswith(".gz"):
460464
self.name = self.name[:-3]

‎Lib/test/test_tarfile.py‎

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2050,6 +2050,44 @@ def test_create_with_mtime(self):
20502050
fobj.read()
20512051
self.assertEqual(fobj.mtime, 0)
20522052

2053+
def test_create_with_mtime_out_of_range(self):
2054+
# gh-133998: an mtime outside the gzip header's 32-bit range is
2055+
# stored as 0 rather than raising struct.error.
2056+
self.addCleanup(os_helper.unlink, tmpname)
2057+
for mtime in (-1, 2**32):
2058+
with self.subTest(mtime=mtime):
2059+
tarfile.open(tmpname, self.mode, mtime=mtime).close()
2060+
with self.open(tmpname, 'r') as fobj:
2061+
fobj.read()
2062+
self.assertEqual(fobj.mtime, 0)
2063+
2064+
def test_create_with_mtime_at_boundary(self):
2065+
# gh-133998: the largest in-range mtime is preserved, not clamped.
2066+
mtime = 2**32 - 1
2067+
tarfile.open(tmpname, self.mode, mtime=mtime).close()
2068+
with self.open(tmpname, 'r') as fobj:
2069+
fobj.read()
2070+
self.assertEqual(fobj.mtime, mtime)
2071+
2072+
def test_create_with_out_of_range_clock(self):
2073+
# gh-133998: an out-of-range system clock (mtime defaulting to
2074+
# time.time()) is stored as 0 rather than raising struct.error.
2075+
for clock in (-1, 2**32):
2076+
with self.subTest(clock=clock):
2077+
with unittest.mock.patch('time.time', return_value=float(clock)):
2078+
tarfile.open(tmpname, self.mode).close()
2079+
with self.open(tmpname, 'r') as fobj:
2080+
fobj.read()
2081+
self.assertEqual(fobj.mtime, 0)
2082+
os_helper.unlink(tmpname)
2083+
2084+
def test_create_with_float_mtime(self):
2085+
# gh-133998: a float mtime is truncated like gzip, not rejected.
2086+
tarfile.open(tmpname, self.mode, mtime=123456789.9).close()
2087+
with self.open(tmpname, 'r') as fobj:
2088+
fobj.read()
2089+
self.assertEqual(fobj.mtime, 123456789)
2090+
20532091
def test_create_without_mtime(self):
20542092
before = int(time.time())
20552093
tarfile.open(tmpname, self.mode).close()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix :func:`tarfile.open` in streaming write mode (``w|gz``) raising
2+
:exc:`struct.error` when given an *mtime* outside the range of the gzip
3+
header's 32-bit timestamp field. Out-of-range values are now stored as ``0``,
4+
matching :mod:`gzip`.

0 commit comments

Comments
 (0)