From 64681af6c69ec52a76ef64877ae56851ccab5f8e Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Mon, 19 May 2025 15:37:47 -0400 Subject: [PATCH 01/10] gh-133998: Fix gzip file creation when time is out of range --- Doc/library/gzip.rst | 9 ++++++--- Lib/gzip.py | 2 ++ Lib/test/test_gzip.py | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index c9d96085ef739d7..664ef86eab59630 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -101,9 +101,12 @@ The module defines the following items: is no compression. The default is ``9``. The optional *mtime* argument is the timestamp requested by gzip. The time - is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. - If *mtime* is omitted or ``None``, the current time is used. Use *mtime* = 0 - to generate a compressed stream that does not depend on creation time. + is in Unix format, i.e., seconds since *00:00:00 UTC, January 1, 1970*. + Use *mtime* = ``0`` to generate a compressed stream that does not depend on + creation time. If *mtime* is omitted or ``None``, the current time is used; + however, if the current time is outside the range + *00:00:00 UTC, January 1, 1970* through *06:28:15 UTC, February 7, 2106*, + then the value ``0`` is used instead. See below for the :attr:`mtime` attribute that is set when decompressing. diff --git a/Lib/gzip.py b/Lib/gzip.py index c00f51858de0f0d..85742dfbabd66c8 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -297,6 +297,8 @@ def _write_gzip_header(self, compresslevel): mtime = self._write_mtime if mtime is None: mtime = time.time() + if mtime < 0 or mtime >= 2**32: + mtime = 0 write32u(self.fileobj, int(mtime)) if compresslevel == _COMPRESS_LEVEL_BEST: xfl = b'\002' diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index fa5de7c190e6a36..58eca7b1fa439bc 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -11,6 +11,7 @@ import unittest import warnings from subprocess import PIPE, Popen +from unittest import mock from test.support import catch_unraisable_exception from test.support import import_helper from test.support import os_helper @@ -351,6 +352,27 @@ def test_mtime(self): self.assertEqual(dataRead, data1) self.assertEqual(fRead.mtime, mtime) + def test_mtime_out_of_range(self): + # exception should be raised when mtime<0 or mtime>=2**32 and is + # explicitly specified + with self.assertRaises(Exception): + with gzip.GzipFile(self.filename, 'w', mtime=-1) as fWrite: + pass + with self.assertRaises(Exception): + with gzip.GzipFile(self.filename, 'w', mtime=2**32) as fWrite: + pass + + # mtime should be set to 0 when time.time() is out of range and mtime is + # not explicitly given + for mtime in (-1, 2**32): + with mock.patch('time.time', return_value=float(mtime)): + with gzip.GzipFile(self.filename, 'w') as fWrite: + fWrite.write(data1) + with gzip.GzipFile(self.filename) as fRead: + fRead.read() + self.assertEqual(fRead.mtime, 0) + + def test_metadata(self): mtime = 123456789 From 3241cf27b0ffae5843687d088f174c215603ab0f Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Mon, 19 May 2025 20:29:36 +0000 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20b?= =?UTF-8?q?lurb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst diff --git a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst new file mode 100644 index 000000000000000..518c3f7021496fb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst @@ -0,0 +1 @@ +Fix ``struct.error`` exception when creating a file with ``gzip.GzipFile()`` if the system time is outside the range *00:00:00 UTC, January 1, 1970* through *06:28:15 UTC, February 7, 2106*. From 79d8a72dd72651ba110d3c0bfa59f902e55b18fa Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Mon, 19 May 2025 16:51:23 -0400 Subject: [PATCH 03/10] Remove extra newline --- Lib/test/test_gzip.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 58eca7b1fa439bc..8b0afae049c530a 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -372,7 +372,6 @@ def test_mtime_out_of_range(self): fRead.read() self.assertEqual(fRead.mtime, 0) - def test_metadata(self): mtime = 123456789 From e701310c970ec9e723cc173f0001f34fc657e90f Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Wed, 21 May 2025 10:59:05 -0400 Subject: [PATCH 04/10] Address review comments regarding documentation --- Doc/library/gzip.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index 664ef86eab59630..4bf2a592cbd936b 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -101,12 +101,12 @@ The module defines the following items: is no compression. The default is ``9``. The optional *mtime* argument is the timestamp requested by gzip. The time - is in Unix format, i.e., seconds since *00:00:00 UTC, January 1, 1970*. - Use *mtime* = ``0`` to generate a compressed stream that does not depend on + is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. Set + *mtime* to ``0`` to generate a compressed stream that does not depend on creation time. If *mtime* is omitted or ``None``, the current time is used; - however, if the current time is outside the range - *00:00:00 UTC, January 1, 1970* through *06:28:15 UTC, February 7, 2106*, - then the value ``0`` is used instead. + however, if the current time is outside the range 00:00:00 UTC, January 1, + 1970 through 06:28:15 UTC, February 7, 2106, then the value ``0`` is used + instead. See below for the :attr:`mtime` attribute that is set when decompressing. From 90a881b888f7939fc8d9655285b44764ef6b861f Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Wed, 21 May 2025 11:12:18 -0400 Subject: [PATCH 05/10] Keep docstring in line with documentation --- Lib/gzip.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 85742dfbabd66c8..06d6d7277a68651 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -188,8 +188,11 @@ def __init__(self, filename=None, mode=None, The optional mtime argument is the timestamp requested by gzip. The time is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. - If mtime is omitted or None, the current time is used. Use mtime = 0 - to generate a compressed stream that does not depend on creation time. + Set mtime to 0 to generate a compressed stream that does not depend on + creation time. If mtime is omitted or None, the current time is used; + however, if the current time is outside the range 00:00:00 UTC, January + 1, 1970 through 06:28:15 UTC, February 7, 2106, then the value 0 is used + instead. """ From 9638ca0fe5339ba2d7611d26e56e936f4ae4ba7e Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Wed, 21 May 2025 11:15:54 -0400 Subject: [PATCH 06/10] Improve error message when mtime is out of range --- Lib/gzip.py | 3 +++ Lib/test/test_gzip.py | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 06d6d7277a68651..5285354d8c6e7aa 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -206,6 +206,9 @@ def __init__(self, filename=None, mode=None, if mode and 'b' not in mode: mode += 'b' + if mtime is not None and (mtime < 0 or mtime >= 2**32): + raise ValueError(f'mtime must be in the range 0 through {2**32-1}') + try: if fileobj is None: fileobj = self.myfileobj = builtins.open(filename, mode or 'rb') diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 8b0afae049c530a..0882c823d574106 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -353,12 +353,12 @@ def test_mtime(self): self.assertEqual(fRead.mtime, mtime) def test_mtime_out_of_range(self): - # exception should be raised when mtime<0 or mtime>=2**32 and is + # ValueError should be raised when mtime<0 or mtime>=2**32 and is # explicitly specified - with self.assertRaises(Exception): + with self.assertRaises(ValueError): with gzip.GzipFile(self.filename, 'w', mtime=-1) as fWrite: pass - with self.assertRaises(Exception): + with self.assertRaises(ValueError): with gzip.GzipFile(self.filename, 'w', mtime=2**32) as fWrite: pass From 20da98ae35facbbd12d7e567b413ed0217fda7ce Mon Sep 17 00:00:00 2001 From: Aohan Dang Date: Wed, 21 May 2025 12:03:22 -0400 Subject: [PATCH 07/10] Re-format changelog message --- .../next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst index 518c3f7021496fb..37177cdcb5e4b8d 100644 --- a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst +++ b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst @@ -1 +1 @@ -Fix ``struct.error`` exception when creating a file with ``gzip.GzipFile()`` if the system time is outside the range *00:00:00 UTC, January 1, 1970* through *06:28:15 UTC, February 7, 2106*. +Fix ``struct.error`` exception when creating a file with ``gzip.GzipFile()`` if the system time is outside the range 00:00:00 UTC, January 1, 1970 through 06:28:15 UTC, February 7, 2106. From ff424fca93fc557b6d330ab36ae4a650e98f6473 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 21 May 2026 22:59:37 +0300 Subject: [PATCH 08/10] Reset out-of-range mtime to 0 even if explicitly provided. --- Lib/gzip.py | 7 ++----- Lib/test/test_gzip.py | 16 ++++++---------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index ed838991e56c008..f391f057a06b296 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -206,9 +206,6 @@ def __init__(self, filename=None, mode=None, if mode and 'b' not in mode: mode += 'b' - if mtime is not None and (mtime < 0 or mtime >= 2**32): - raise ValueError(f'mtime must be in the range 0 through {2**32-1}') - try: if fileobj is None: fileobj = self.myfileobj = builtins.open(filename, mode or 'rb') @@ -301,8 +298,8 @@ def _write_gzip_header(self, compresslevel): mtime = self._write_mtime if mtime is None: mtime = time.time() - if mtime < 0 or mtime >= 2**32: - mtime = 0 + if not 0 <= mtime < 2**32: + mtime = 0 write32u(self.fileobj, int(mtime)) if compresslevel == _COMPRESS_LEVEL_BEST: xfl = b'\002' diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 43e16533182fbf5..c91e208f5f1b946 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -352,17 +352,13 @@ def test_mtime(self): self.assertEqual(fRead.mtime, mtime) def test_mtime_out_of_range(self): - # ValueError should be raised when mtime<0 or mtime>=2**32 and is - # explicitly specified - with self.assertRaises(ValueError): - with gzip.GzipFile(self.filename, 'w', mtime=-1) as fWrite: - pass - with self.assertRaises(ValueError): - with gzip.GzipFile(self.filename, 'w', mtime=2**32) as fWrite: - pass + for mtime in (-1, 2**32): + with gzip.GzipFile(self.filename, 'w', mtime=mtime) as fWrite: + fWrite.write(data1) + with gzip.GzipFile(self.filename) as fRead: + fRead.read() + self.assertEqual(fRead.mtime, 0) - # mtime should be set to 0 when time.time() is out of range and mtime is - # not explicitly given for mtime in (-1, 2**32): with mock.patch('time.time', return_value=float(mtime)): with gzip.GzipFile(self.filename, 'w') as fWrite: From 52acf9e3dbcbc2d8add5ba08a64c4221666c1a88 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 21 May 2026 23:14:38 +0300 Subject: [PATCH 09/10] Fix also gzip.compress(). --- Lib/gzip.py | 2 ++ Lib/test/test_gzip.py | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index f391f057a06b296..f5014aca4cced6e 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -668,6 +668,8 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0): gzip_data = zlib.compress(data, level=compresslevel, wbits=31) if mtime is None: mtime = time.time() + if not 0 <= mtime < 2**32: + mtime = 0 # Reuse gzip header created by zlib, replace mtime and OS byte for # consistency. header = struct.pack("<4sLBB", gzip_data, int(mtime), gzip_data[8], 255) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index c91e208f5f1b946..cafac9d3c8be6e7 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -356,7 +356,11 @@ def test_mtime_out_of_range(self): with gzip.GzipFile(self.filename, 'w', mtime=mtime) as fWrite: fWrite.write(data1) with gzip.GzipFile(self.filename) as fRead: - fRead.read() + fRead.read(1) + self.assertEqual(fRead.mtime, 0) + datac = gzip.compress(data1, mtime=mtime) + with gzip.GzipFile(fileobj=io.BytesIO(datac)) as fRead: + fRead.read(1) self.assertEqual(fRead.mtime, 0) for mtime in (-1, 2**32): @@ -364,7 +368,7 @@ def test_mtime_out_of_range(self): with gzip.GzipFile(self.filename, 'w') as fWrite: fWrite.write(data1) with gzip.GzipFile(self.filename) as fRead: - fRead.read() + fRead.read(1) self.assertEqual(fRead.mtime, 0) def test_metadata(self): From e4e7d535e8078321cd6bb3ff5c4e8e13897e7be7 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Thu, 21 May 2026 23:24:58 +0300 Subject: [PATCH 10/10] Update docs. --- Doc/library/gzip.rst | 5 +++-- Lib/gzip.py | 7 +++---- .../Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst | 6 +++++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Doc/library/gzip.rst b/Doc/library/gzip.rst index d2e008a8693ef7f..2c667ddc522399c 100644 --- a/Doc/library/gzip.rst +++ b/Doc/library/gzip.rst @@ -112,8 +112,9 @@ The module defines the following items: *mtime* to ``0`` to generate a compressed stream that does not depend on creation time. If *mtime* is omitted or ``None``, the current time is used; however, if the current time is outside the range 00:00:00 UTC, January 1, - 1970 through 06:28:15 UTC, February 7, 2106, then the value ``0`` is used - instead. + 1970 through 06:28:15 UTC, February 7, 2106, or explicitly passed *mtime* + argument is outside the range ``0`` to ``2**32-1``, then the value ``0`` + is used instead. See below for the :attr:`mtime` attribute that is set when decompressing. diff --git a/Lib/gzip.py b/Lib/gzip.py index f5014aca4cced6e..8720acc4db99762 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -189,10 +189,9 @@ def __init__(self, filename=None, mode=None, The optional mtime argument is the timestamp requested by gzip. The time is in Unix format, i.e., seconds since 00:00:00 UTC, January 1, 1970. Set mtime to 0 to generate a compressed stream that does not depend on - creation time. If mtime is omitted or None, the current time is used; - however, if the current time is outside the range 00:00:00 UTC, January - 1, 1970 through 06:28:15 UTC, February 7, 2106, then the value 0 is used - instead. + creation time. If mtime is omitted or None, the current time is used. + If the resulting mtime is outside the range 0 to 2**32-1, then the + value 0 is used instead. """ diff --git a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst index 37177cdcb5e4b8d..77d92628beefacd 100644 --- a/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst +++ b/Misc/NEWS.d/next/Library/2025-05-19-20-29-35.gh-issue-133998.KmElUw.rst @@ -1 +1,5 @@ -Fix ``struct.error`` exception when creating a file with ``gzip.GzipFile()`` if the system time is outside the range 00:00:00 UTC, January 1, 1970 through 06:28:15 UTC, February 7, 2106. +Fix :exc:`struct.error` exception when creating a file with +:class:`gzip.GzipFile` or compressing data with :func:`gzip.compress` +if the system time is outside the range 00:00:00 UTC, January 1, 1970 +through 06:28:15 UTC, February 7, 2106, or explicitly passed *mtime* +argument is outside the range ``0`` to ``2**32-1``.