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
18 changes: 16 additions & 2 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ ZipFile objects

.. class:: ZipFile(file, mode='r', compression=ZIP_STORED, allowZip64=True, \
compresslevel=None, *, strict_timestamps=True, \
metadata_encoding=None)
with_ext_timestamps=False, metadata_encoding=None)

Open a ZIP file, where *file* can be a path to a file (a string), a
file-like object or a :term:`path-like object`.
Expand Down Expand Up @@ -227,6 +227,9 @@ ZipFile objects
Similar behavior occurs with files newer than 2107-12-31,
the timestamp is also set to the limit.

The *with_ext_timestamps* controls whether to fill the extended timestamps
when writing files to the archive.

When mode is ``'r'``, *metadata_encoding* may be set to the name of a codec,
which will be used to decode metadata such as the names of members and ZIP
comments.
Expand Down Expand Up @@ -285,6 +288,9 @@ ZipFile objects
Added support for specifying member name encoding for reading
metadata in the zipfile's directory and file headers.

.. versionchanged:: next
Added the *with_ext_timestamps* keyword-only parameter.


.. method:: ZipFile.close()

Expand Down Expand Up @@ -885,7 +891,8 @@ There is one classmethod to make a :class:`ZipInfo` instance for a filesystem
file:

.. classmethod:: ZipInfo.from_file(filename, arcname=None, *, \
strict_timestamps=True)
strict_timestamps=True, \
with_ext_timestamps=False)

Construct a :class:`ZipInfo` instance for a file on the filesystem, in
preparation for adding it to a zip file.
Expand All @@ -902,6 +909,10 @@ file:
Similar behavior occurs with files newer than 2107-12-31,
the timestamp is also set to the limit.

Setting ``with_ext_timestamps=True`` fills the file's extended timestamps
to the extra data, which allows other ZIP tools to recover the timestamp
more accurately when extracting the file.

.. versionadded:: 3.6

.. versionchanged:: 3.6.2
Expand All @@ -910,6 +921,9 @@ file:
.. versionchanged:: 3.8
Added the *strict_timestamps* keyword-only parameter.

.. versionchanged:: next
Added the *with_ext_timestamps* keyword-only parameter.


Instances have the following methods and attributes:

Expand Down
85 changes: 85 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -676,6 +676,91 @@ def test_add_file_after_2107(self):
zinfo = zipfp.getinfo(TESTFN)
self.assertEqual(zinfo.date_time, (2107, 12, 31, 23, 59, 59))

def test_add_file_with_ext_timestamp(self):
"""Check that calling ZipFile.write() sets extra data according to
with_ext_timestamps parameter."""
mtime = 946684800.123456
mtime_ns = 946684800_123456789
atime_ns = 946684800_987654321
ctime_ns = 946684800_555555555

with mock.patch('os.stat_result.st_mtime', mtime), \
mock.patch('os.stat_result.st_mtime_ns', mtime_ns), \
mock.patch('os.stat_result.st_atime_ns', atime_ns), \
mock.patch('os.stat_result.st_ctime_ns', ctime_ns):

# with_ext_timestamps=False (default)
with zipfile.ZipFile(TESTFN2, "w") as zipfp:
zipfp.write(TESTFN)

with zipfile.ZipFile(TESTFN2) as zipfp:
zinfo = zipfp.infolist()[0]

self.assertEqual(zinfo.extra, b'')

# with_ext_timestamps=True
with zipfile.ZipFile(TESTFN2, "w", with_ext_timestamps=True) as zipfp:
zipfp.write(TESTFN)

with zipfile.ZipFile(TESTFN2) as zipfp:
zinfo = zipfp.infolist()[0]

self.assertEqual(zinfo.date_time[0], 2000)

# NTFS Extra Field (0x000a)
delta = 116444736000000000
ntfs_field = struct.unpack_from('<HHLHHQQQ', zinfo.extra)
self.assertEqual(ntfs_field, (
0x000a, 32,
0, 0x0001, 24,
mtime_ns // 100 + delta,
atime_ns // 100 + delta,
ctime_ns // 100 + delta,
))

# Extended timestamp (0x5455)
ut_field = struct.unpack_from('<HHBL', zinfo.extra, struct.calcsize('<HHLHHQQQ'))
self.assertEqual(ut_field, (0x5455, 5, 1, int(mtime)))

def test_add_file_with_ext_timestamp_after_2038(self):
"""Extended timestamp field should exist for a timestamp after
2038-01-19T03:14:07Z."""
mtime = 2147483648.123456 # 2038-01-19T03:14:08.123456Z

with mock.patch('os.stat_result.st_mtime', mtime):
with zipfile.ZipFile(TESTFN2, "w", strict_timestamps=False,
with_ext_timestamps=True) as zipfp:
zipfp.write(TESTFN)

with zipfile.ZipFile(TESTFN2) as zipfp:
zinfo = zipfp.infolist()[0]

self.assertEqual(zinfo.date_time[0], 2038)

# Extended timestamp (0x5455)
ntfs_field_len = struct.calcsize('<HHLHHQQQ')
ut_field = struct.unpack_from('<HHBL', zinfo.extra, ntfs_field_len)
self.assertEqual(ut_field, (0x5455, 5, 1, int(mtime)))

def test_add_file_with_ext_timestamp_after_2106(self):
"""Extended timestamp field should not exist for a timestamp after
2106-02-07T06:28:15Z."""
mtime = 4294967296.123456 # 2106-02-07T06:28:16.123456Z

with mock.patch('os.stat_result.st_mtime', mtime):
with zipfile.ZipFile(TESTFN2, "w", strict_timestamps=False,
with_ext_timestamps=True) as zipfp:
zipfp.write(TESTFN)

with zipfile.ZipFile(TESTFN2) as zipfp:
zinfo = zipfp.infolist()[0]

self.assertEqual(zinfo.date_time[0], 2106)

# Only an NTFS Extra Field (0x000a) exists
ntfs_field_len = struct.calcsize('<HHLHHQQQ')
self.assertEqual(len(zinfo.extra), ntfs_field_len)


@requires_zlib()
class DeflateTestsWithSourceFile(AbstractTestsWithSourceFile,
Expand Down
29 changes: 26 additions & 3 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,8 @@ def _decodeExtra(self, filename_crc):
extra = extra[ln+4:]

@classmethod
def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
def from_file(cls, filename, arcname=None, *, strict_timestamps=True,
with_ext_timestamps=False):
"""Construct an appropriate ZipInfo for a file on the filesystem.

filename should be the path to a file or directory on the
Expand Down Expand Up @@ -665,6 +666,25 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):
else:
zinfo.file_size = st.st_size

if with_ext_timestamps:
# NTFS Extra Field (0x000a)
delta = 116444736000000000
ft_mtime = st.st_mtime_ns // 100 + delta
ft_atime = st.st_atime_ns // 100 + delta
ft_ctime = st.st_ctime_ns // 100 + delta
ntfs_tag = struct.pack('<LHHQQQ', 0, 0x0001, 24, ft_mtime, ft_atime, ft_ctime)
extra = struct.pack('<HH', 0x000a, len(ntfs_tag)) + ntfs_tag

# Extended timestamp (0x5455)
# According to libzip's doc, the timestamps should be 4-byte
# unsigned integers:
# https://libzip.org/specifications/extrafld.txt
mtime = int(st.st_mtime)
if 0 <= mtime <= 0xFFFF_FFFF:
extra += struct.pack('<HHBL', 0x5455, 5, 0x01, mtime)

zinfo.extra = extra

return zinfo

def _for_archive(self, archive):
Expand Down Expand Up @@ -1896,7 +1916,8 @@ class ZipFile:
_ignore_invalid_names = False

def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True,
compresslevel=None, *, strict_timestamps=True, metadata_encoding=None):
compresslevel=None, *, strict_timestamps=True,
with_ext_timestamps=False, metadata_encoding=None):
"""Open the ZIP file with mode read 'r', write 'w', exclusive create
'x', or append 'a'."""
if mode not in ('r', 'w', 'x', 'a'):
Expand All @@ -1915,6 +1936,7 @@ def __init__(self, file, mode="r", compression=ZIP_STORED, allowZip64=True,
self.pwd = None
self._comment = b''
self._strict_timestamps = strict_timestamps
self._with_ext_timestamps = with_ext_timestamps
self.metadata_encoding = metadata_encoding

# Check that we don't try to write with nonconforming codecs
Expand Down Expand Up @@ -2526,7 +2548,8 @@ def write(self, filename, arcname=None,
)

zinfo = ZipInfo.from_file(filename, arcname,
strict_timestamps=self._strict_timestamps)
strict_timestamps=self._strict_timestamps,
with_ext_timestamps=self._with_ext_timestamps)

if zinfo.is_dir():
zinfo.compress_size = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Introduce ``with_ext_timestamps`` parameter to
:class:`~zipfile.ZipFile` and :meth:`~zipfile.ZipInfo.from_file` to
fill extra data with the file's extended timestamps when writing a
file to the archive.
Loading