Skip to content

Commit

Permalink
zipfile: Support for extended_*time attributes
Browse files Browse the repository at this point in the history
zipinfo now supports attributes like extended_atime,
extended_ctime, extended_mtime

Signed-off-by: Abhijeet Kasurde <[email protected]>
  • Loading branch information
Akasurde committed Jun 29, 2024
1 parent 92893fd commit 03a1553
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,11 @@ Instances have the following methods and attributes:

Size of the uncompressed file.

.. attribute:: ZipInfo.extended_mtime

Extended modified timestamp of the uncompressed file.

.. versionadded:: 3.14

.. _zipfile-commandline:
.. program:: zipfile
Expand Down
51 changes: 51 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import array
import contextlib
import datetime
import importlib.util
import io
import itertools
Expand Down Expand Up @@ -1877,6 +1878,54 @@ def test_read_after_write_unicode_filenames(self):
zipfp.writestr('приклад', b'sample')
self.assertEqual(zipfp.read('приклад'), b'sample')

def create_zipfile_with_extended_timestamp_extra_data(self, filename, extra_timestamp_data):
with zipfile.ZipFile(TESTFN, mode='w') as zf:
filename_encoded = filename.encode("utf-8")
zip_info = zipfile.ZipInfo(filename)

tag_for_extra_block = b'\x55\x54'

# Set only modification time
flags = b'\x01'

# modification time
if extra_timestamp_data is not None:
mtime = struct.pack('<l', extra_timestamp_data)
else:
mtime = b''
extra_data = flags + mtime

tsize = len(extra_data).to_bytes(2, 'little')

zip_info.extra = tag_for_extra_block + tsize + extra_data

# add the file to the ZIP archive
zf.writestr(zip_info, b'Hello World!')

def test_read_zipfile_containing_extended_timestamp_extra_field(self):
timestamp_data = int(datetime.datetime(2024, 6, 26, 9, 27, 30).timestamp())
self.create_zipfile_with_extended_timestamp_extra_data("aayush.txt", timestamp_data)
with zipfile.ZipFile(TESTFN, "r") as zf:
self.assertEqual(zf.filelist[0].extended_mtime, timestamp_data)

def test_read_zipfile_containing_timestamp_beyond_2038(self):
# Test for 2038 problem
timestamp_data = -1
self.create_zipfile_with_extended_timestamp_extra_data("aayush.txt", timestamp_data)
with zipfile.ZipFile(TESTFN, "r") as zf:
self.assertEqual(zf.filelist[0].extended_mtime, timestamp_data)

def test_read_zipfile_zero_timestamp_extra_field(self):
timestamp_data = 0
self.create_zipfile_with_extended_timestamp_extra_data("aayush.txt", timestamp_data)
with zipfile.ZipFile(TESTFN, "r") as zf:
self.assertEqual(zf.filelist[0].extended_mtime, timestamp_data)

def test_read_zipfile_none_timestamp_extra_field(self):
self.create_zipfile_with_extended_timestamp_extra_data("aayush.txt", None)
with zipfile.ZipFile(TESTFN, "r") as zf:
self.assertIsNone(zf.filelist[0].extended_mtime)

def test_exclusive_create_zip_file(self):
"""Test exclusive creating a new zipfile."""
unlink(TESTFN2)
Expand Down Expand Up @@ -2227,6 +2276,8 @@ def test_create_empty_zipinfo_default_attributes(self):
self.assertEqual(zi.file_size, 0)
self.assertEqual(zi.compress_size, 0)

self.assertIsNone(zi.extended_mtime)

def test_zipfile_with_short_extra_field(self):
"""If an extra field in the header is less than 4 bytes, skip it."""
zipdata = (
Expand Down
11 changes: 11 additions & 0 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ class ZipInfo:
'file_size',
'_raw_time',
'_end_offset',
'extended_mtime',
)

def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
Expand Down Expand Up @@ -431,6 +432,7 @@ def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
self.compress_size = 0 # Size of the compressed file
self.file_size = 0 # Size of the uncompressed file
self._end_offset = None # Start of the next local header or central directory
self.extended_mtime = None # Extended modified timestamp
# Other attributes are set by class ZipFile:
# header_offset Byte offset to the file header
# CRC CRC-32 of the uncompressed file
Expand Down Expand Up @@ -563,6 +565,15 @@ def _decodeExtra(self, filename_crc):
except UnicodeDecodeError as e:
raise BadZipFile('Corrupt unicode path extra field (0x7075): invalid utf-8 bytes') from e

elif tp == 0x5455:
if ln == 0:
raise BadZipFile('Empty extended timestamp extra field (0x5455)')
flags = extra[4]
if flags & 0x1 and ln >= 5:
try:
self.extended_mtime, = unpack("<l", extra[5:9])
except struct.error as e:
raise BadZipFile("Corrupt extended stamp extra field (0x5455)") from e
extra = extra[ln+4:]

@classmethod
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add attribute :attr:`zipfile.ZipInfo.extended_mtime`.

0 comments on commit 03a1553

Please sign in to comment.