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
60 changes: 51 additions & 9 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,9 @@ def test_force_zip64(self):
# sizes to 0xFFFFFFFF to indicate to the extractor that the zip64
# record should be read. Additionally, it would not set the required
# version to indicate that zip64 extensions are required to extract it.
# GH-152486 describes another issue where force_zip64 was honored only
# by the local file entry but not by the central directory for a small
# file.
# This test replicates the situation and reads the raw data to specifically ensure:
# - The required extract version is always >= ZIP64_VERSION
# - The compressed and uncompressed size in the file headers are both
Expand All @@ -1207,8 +1210,9 @@ def test_force_zip64(self):
# bytes are checked to ensure that they line up with the zip spec.
# The spec for this can be found at: https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT
# The relevant sections for this test are:
# - 4.3.7 for local file header
# - 4.5.3 for zip64 extra field
# - 4.3.7 for local file header
# - 4.3.12 for central directory
# - 4.5.3 for zip64 extra field

data = io.BytesIO()
with zipfile.ZipFile(data, mode="w", allowZip64=True) as zf:
Expand All @@ -1217,11 +1221,11 @@ def test_force_zip64(self):

zipdata = data.getvalue()

# pull out and check zip information
# check the local file entry
(
header, vers, os, flags, comp, csize, usize, fn_len,
ex_total_len, filename, ex_id, ex_len, ex_usize, ex_csize, cd_sig
) = struct.unpack("<4sBBHH8xIIHH8shhQQx4s", zipdata[:63])
ex_total_len, filename, ex_id, ex_len, ex_usize, ex_csize
) = struct.unpack("<4sBBHH8xIIHH8sHHQQx", zipdata[:59])

self.assertEqual(header, b"PK\x03\x04") # local file header
self.assertGreaterEqual(vers, zipfile.ZIP64_VERSION) # requires zip64 to extract
Expand All @@ -1236,12 +1240,45 @@ def test_force_zip64(self):
self.assertEqual(ex_len, 16) # 16 bytes of data
self.assertEqual(ex_usize, 1) # uncompressed size
self.assertEqual(ex_csize, 1) # compressed size
self.assertEqual(cd_sig, b"PK\x01\x02") # ensure the central directory header is next

z = zipfile.ZipFile(io.BytesIO(zipdata))
zinfos = z.infolist()
# check the entry in central directory,
# which should immediately follow the local file entry
(
header, v_made, v_ext, os, flags, comp, csize, usize, fn_len,
ex_total_len, comment_len, in_attr, local_offset, filename,
ex_id, ex_len, ex_usize, ex_csize
) = struct.unpack("<4sBxBBHH8xIIHHH2xH4xI8sHHQQ", zipdata[59:133])

self.assertEqual(header, b"PK\x01\x02") # central directory file header
self.assertGreaterEqual(v_made, zipfile.ZIP64_VERSION)
self.assertGreaterEqual(v_ext, zipfile.ZIP64_VERSION)
self.assertEqual(os, 0)
self.assertEqual(flags, 0)
self.assertEqual(comp, 0)
self.assertEqual(csize, 0xFFFFFFFF)
self.assertEqual(usize, 0xFFFFFFFF)
self.assertEqual(fn_len, 8)
self.assertEqual(ex_total_len, 20)
self.assertEqual(comment_len, 0) # no file comment
self.assertEqual(local_offset, 0)
self.assertEqual(ex_id, 1)
self.assertEqual(ex_len, 16)
self.assertEqual(ex_usize, 1)
self.assertEqual(ex_csize, 1)

# check the ZipInfo object
with zipfile.ZipFile(data) as zf:
zinfos = zf.infolist()
zinfo = zf.getinfo('text.txt')

self.assertEqual(len(zinfos), 1)
self.assertGreaterEqual(zinfos[0].extract_version, zipfile.ZIP64_VERSION) # requires zip64 to extract
self.assertGreaterEqual(zinfo.extract_version, zipfile.ZIP64_VERSION) # requires zip64 to extract
self.assertEqual(zinfo.extra, (
b'\x01\x00'
b'\x10\x00'
b'\x01\x00\x00\x00\x00\x00\x00\x00'
b'\x01\x00\x00\x00\x00\x00\x00\x00'
))

def test_unseekable_zip_unknown_filesize(self):
"""Test that creating a zip with/without seeking will raise a RuntimeError if zip64 was required but not used"""
Expand Down Expand Up @@ -1448,6 +1485,11 @@ def comparable_zinfo(zinfo):
# pretending it's always set.
attrs['flag_bits'] |= 0x800

# Check decoded data (file size, compressed size) and ignore zip64 subfield
# in extra data since is may be inconsistent for a ZipInfo before writing
# and one read from a file.
attrs['extra'] = zipfile._Extra.strip(attrs['extra'], (1,))

return attrs

_struct_pack = struct.pack
Expand Down
9 changes: 7 additions & 2 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,7 @@ class ZipInfo:
'file_size',
'_raw_time',
'_end_offset',
'_force_zip64',
)

def __init__(self, filename="NoName", date_time=(1980,1,1,0,0,0)):
Expand Down Expand Up @@ -488,6 +489,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._force_zip64 = None # Whether zip64 extension is enforced
# 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 @@ -2202,6 +2204,7 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False):
zinfo = self.getinfo(name)

if mode == 'w':
zinfo._force_zip64 = force_zip64
return self._open_to_write(zinfo, force_zip64=force_zip64)

if self._writing:
Expand Down Expand Up @@ -2646,8 +2649,10 @@ def _write_end_record(self):
dosdate = (dt[0] - 1980) << 9 | dt[1] << 5 | dt[2]
dostime = dt[3] << 11 | dt[4] << 5 | (dt[5] // 2)
extra = []
if zinfo.file_size > ZIP64_LIMIT \
or zinfo.compress_size > ZIP64_LIMIT:
if (zinfo._force_zip64
or zinfo.file_size > ZIP64_LIMIT
or zinfo.compress_size > ZIP64_LIMIT
):
extra.append(zinfo.file_size)
extra.append(zinfo.compress_size)
file_size = 0xffffffff
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix an issue where :meth:`~zipfile.ZipFile.open` with ``force_zip64=True`` would write the Zip64 extra field to the local file header but fail to do so in the central directory for small files.
Loading