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
52 changes: 50 additions & 2 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,12 +676,12 @@ def test_add_file_after_2107(self):
self.skipTest(f"Linux VFS/XFS kernel bug detected: {mtime_ns=}")

with zipfile.ZipFile(TESTFN2, "w") as zipfp:
self.assertRaises(struct.error, zipfp.write, TESTFN)
self.assertRaises(ValueError, zipfp.write, TESTFN)

with zipfile.ZipFile(TESTFN2, "w", strict_timestamps=False) as zipfp:
zipfp.write(TESTFN)
zinfo = zipfp.getinfo(TESTFN)
self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59))
self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 58))


@requires_zlib()
Expand Down Expand Up @@ -4476,6 +4476,10 @@ def test_create_zipinfo_before_1980(self):
self.assertRaises(ValueError,
zipfile.ZipInfo, 'seventies', (1979, 1, 1, 0, 0, 0))

def test_create_zipinfo_after_2107(self):
self.assertRaises(ValueError,
zipfile.ZipInfo, 'future', (2108, 1, 1, 0, 0, 0))

def test_create_empty_zipinfo_repr(self):
"""Before bpo-26185, repr() on empty ZipInfo object was failing."""
zi = zipfile.ZipInfo(filename="empty")
Expand Down Expand Up @@ -5528,6 +5532,50 @@ def test_from_file(self):
self.assertFalse(zi.is_dir())
self.assertEqual(zi.file_size, os.path.getsize(__file__))

def test_from_file_rejects_timestamp_after_2107(self):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=2**63, st_size=5)
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
with self.assertRaises(ValueError):
zipfile.ZipInfo.from_file('future')

def test_from_file_clamps_timestamp_after_2107_round_trip(self):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=2**63, st_size=5)
mtime = (2108, 1, 1, 0, 0, 0, 0, 1, -1)
with mock.patch.object(zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime', return_value=mtime):
zinfo = zipfile.ZipInfo.from_file(
'future', strict_timestamps=False)

expected = (2107, 12, 31, 23, 59, 58)
self.assertEqual(zinfo.date_time, expected)
archive = io.BytesIO()
with zipfile.ZipFile(archive, 'w') as zf:
zf.writestr(zinfo, b'future')
with zipfile.ZipFile(archive) as zf:
self.assertEqual(zf.getinfo('future').date_time, expected)

def test_from_file_clamps_localtime_overflow(self):
cases = (
(2**63, (2107, 12, 31, 23, 59, 58)),
(-2**63, (1980, 1, 1, 0, 0, 0)),
)
for exception in (OverflowError, OSError):
for timestamp, expected in cases:
with self.subTest(exception=exception, timestamp=timestamp):
st = mock.Mock(st_mode=stat.S_IFREG | 0o644,
st_mtime=timestamp, st_size=5)
with mock.patch.object(
zipfile.os, 'stat', return_value=st), \
mock.patch.object(zipfile.time, 'localtime',
side_effect=exception):
zinfo = zipfile.ZipInfo.from_file(
'future', strict_timestamps=False)
self.assertEqual(zinfo.date_time, expected)

def test_from_file_pathlike(self):
zi = zipfile.ZipInfo.from_file(FakePath(__file__))
self.assertEqual(posixpath.basename(zi.filename), 'test_core.py')
Expand Down
23 changes: 17 additions & 6 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,8 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):

if date_time[0] < 1980:
raise ValueError('ZIP does not support timestamps before 1980')
if date_time[0] > 2107:
raise ValueError('ZIP does not support timestamps after 2107')

# Standard values:
self.compress_type = ZIP_STORED # Type of compression for the file
Expand Down Expand Up @@ -643,12 +645,21 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
filename = os.fspath(filename)
st = os.stat(filename)
isdir = stat.S_ISDIR(st.st_mode)
mtime = time.localtime(st.st_mtime)
date_time = mtime[0:6]
if not strict_timestamps and date_time[0] < 1980:
date_time = (1980, 1, 1, 0, 0, 0)
elif not strict_timestamps and date_time[0] > 2107:
date_time = (2107, 12, 31, 23, 59, 59)
try:
mtime = time.localtime(st.st_mtime)
except (OverflowError, OSError):
if strict_timestamps:
raise
if st.st_mtime < 0:
date_time = (1980, 1, 1, 0, 0, 0)
else:
date_time = (2107, 12, 31, 23, 59, 58)
else:
date_time = mtime[0:6]
if not strict_timestamps and date_time[0] < 1980:
date_time = (1980, 1, 1, 0, 0, 0)
elif not strict_timestamps and date_time[0] > 2107:
date_time = (2107, 12, 31, 23, 59, 58)
# Create ZipInfo instance to store file information
if arcname is None:
arcname = filename
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Improve :mod:`zipfile` timestamp validation. Reject years after 2107 and
clamp out-of-range filesystem timestamps when ``strict_timestamps`` is false.
Loading