Update zipfile from v3.14.3

This commit is contained in:
CPython Developers
2026-02-24 00:27:23 +00:00
committed by Jeong, YunWon
parent 3ec7b5c375
commit 000025e09c
3 changed files with 91 additions and 31 deletions

View File

@@ -31,10 +31,15 @@ try:
except ImportError:
lzma = None
try:
from compression import zstd # We may need its compression method
except ImportError:
zstd = None
__all__ = ["BadZipFile", "BadZipfile", "error",
"ZIP_STORED", "ZIP_DEFLATED", "ZIP_BZIP2", "ZIP_LZMA",
"is_zipfile", "ZipInfo", "ZipFile", "PyZipFile", "LargeZipFile",
"Path"]
"ZIP_ZSTANDARD", "is_zipfile", "ZipInfo", "ZipFile", "PyZipFile",
"LargeZipFile", "Path"]
class BadZipFile(Exception):
pass
@@ -58,12 +63,14 @@ ZIP_STORED = 0
ZIP_DEFLATED = 8
ZIP_BZIP2 = 12
ZIP_LZMA = 14
ZIP_ZSTANDARD = 93
# Other ZIP compression methods not supported
DEFAULT_VERSION = 20
ZIP64_VERSION = 45
BZIP2_VERSION = 46
LZMA_VERSION = 63
ZSTANDARD_VERSION = 63
# we recognize (but not necessarily support) all features up to that version
MAX_EXTRACT_VERSION = 63
@@ -227,8 +234,19 @@ class _Extra(bytes):
def _check_zipfile(fp):
try:
if _EndRecData(fp):
return True # file has correct magic number
endrec = _EndRecData(fp)
if endrec:
if endrec[_ECD_ENTRIES_TOTAL] == 0 and endrec[_ECD_SIZE] == 0 and endrec[_ECD_OFFSET] == 0:
return True # Empty zipfiles are still zipfiles
elif endrec[_ECD_DISK_NUMBER] == endrec[_ECD_DISK_START]:
# Central directory is on the same disk
fp.seek(sum(_handle_prepended_data(endrec)))
if endrec[_ECD_SIZE] >= sizeCentralDir:
data = fp.read(sizeCentralDir) # CD is where we expect it to be
if len(data) == sizeCentralDir:
centdir = struct.unpack(structCentralDir, data) # CD is the right size
if centdir[_CD_SIGNATURE] == stringCentralDir:
return True # First central directory entry has correct magic number
except OSError:
pass
return False
@@ -241,7 +259,9 @@ def is_zipfile(filename):
result = False
try:
if hasattr(filename, "read"):
pos = filename.tell()
result = _check_zipfile(fp=filename)
filename.seek(pos)
else:
with open(filename, "rb") as fp:
result = _check_zipfile(fp)
@@ -249,6 +269,19 @@ def is_zipfile(filename):
pass
return result
def _handle_prepended_data(endrec, debug=0):
size_cd = endrec[_ECD_SIZE] # bytes in central directory
offset_cd = endrec[_ECD_OFFSET] # offset of central directory
# "concat" is zero, unless zip was concatenated to another file
concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
if debug > 2:
inferred = concat + offset_cd
print("given, inferred, offset", offset_cd, inferred, concat)
return offset_cd, concat
def _EndRecData64(fpin, offset, endrec):
"""
Read the ZIP64 end-of-archive records and use that to update endrec
@@ -519,6 +552,8 @@ class ZipInfo:
min_version = max(BZIP2_VERSION, min_version)
elif self.compress_type == ZIP_LZMA:
min_version = max(LZMA_VERSION, min_version)
elif self.compress_type == ZIP_ZSTANDARD:
min_version = max(ZSTANDARD_VERSION, min_version)
self.extract_version = max(min_version, self.extract_version)
self.create_version = max(min_version, self.create_version)
@@ -619,6 +654,28 @@ class ZipInfo:
return zinfo
def _for_archive(self, archive):
"""Resolve suitable defaults from the archive.
Resolve the date_time, compression attributes, and external attributes
to suitable defaults as used by :method:`ZipFile.writestr`.
Return self.
"""
# gh-91279: Set the SOURCE_DATE_EPOCH to a specific timestamp
epoch = os.environ.get('SOURCE_DATE_EPOCH')
get_time = int(epoch) if epoch else time.time()
self.date_time = time.localtime(get_time)[:6]
self.compress_type = archive.compression
self.compress_level = archive.compresslevel
if self.filename.endswith('/'): # pragma: no cover
self.external_attr = 0o40775 << 16 # drwxrwxr-x
self.external_attr |= 0x10 # MS-DOS directory flag
else:
self.external_attr = 0o600 << 16 # ?rw-------
return self
def is_dir(self):
"""Return True if this archive member is a directory."""
if self.filename.endswith('/'):
@@ -758,6 +815,7 @@ compressor_names = {
14: 'lzma',
18: 'terse',
19: 'lz77',
93: 'zstd',
97: 'wavpack',
98: 'ppmd',
}
@@ -777,6 +835,10 @@ def _check_compression(compression):
if not lzma:
raise RuntimeError(
"Compression requires the (missing) lzma module")
elif compression == ZIP_ZSTANDARD:
if not zstd:
raise RuntimeError(
"Compression requires the (missing) compression.zstd module")
else:
raise NotImplementedError("That compression method is not supported")
@@ -793,6 +855,8 @@ def _get_compressor(compress_type, compresslevel=None):
# compresslevel is ignored for ZIP_LZMA
elif compress_type == ZIP_LZMA:
return LZMACompressor()
elif compress_type == ZIP_ZSTANDARD:
return zstd.ZstdCompressor(level=compresslevel)
else:
return None
@@ -807,6 +871,8 @@ def _get_decompressor(compress_type):
return bz2.BZ2Decompressor()
elif compress_type == ZIP_LZMA:
return LZMADecompressor()
elif compress_type == ZIP_ZSTANDARD:
return zstd.ZstdDecompressor()
else:
descr = compressor_names.get(compress_type)
if descr:
@@ -1326,7 +1392,8 @@ class ZipFile:
mode: The mode can be either read 'r', write 'w', exclusive create 'x',
or append 'a'.
compression: ZIP_STORED (no compression), ZIP_DEFLATED (requires zlib),
ZIP_BZIP2 (requires bz2) or ZIP_LZMA (requires lzma).
ZIP_BZIP2 (requires bz2), ZIP_LZMA (requires lzma), or
ZIP_ZSTANDARD (requires compression.zstd).
allowZip64: if True ZipFile will create files with ZIP64 extensions when
needed, otherwise it will raise an exception when this would
be necessary.
@@ -1335,6 +1402,9 @@ class ZipFile:
When using ZIP_STORED or ZIP_LZMA this keyword has no effect.
When using ZIP_DEFLATED integers 0 through 9 are accepted.
When using ZIP_BZIP2 integers 1 through 9 are accepted.
When using ZIP_ZSTANDARD integers -7 though 22 are common,
see the CompressionParameter enum in compression.zstd for
details.
"""
@@ -1468,21 +1538,17 @@ class ZipFile:
raise BadZipFile("File is not a zip file")
if self.debug > 1:
print(endrec)
size_cd = endrec[_ECD_SIZE] # bytes in central directory
offset_cd = endrec[_ECD_OFFSET] # offset of central directory
self._comment = endrec[_ECD_COMMENT] # archive comment
# "concat" is zero, unless zip was concatenated to another file
concat = endrec[_ECD_LOCATION] - size_cd - offset_cd
offset_cd, concat = _handle_prepended_data(endrec, self.debug)
if self.debug > 2:
inferred = concat + offset_cd
print("given, inferred, offset", offset_cd, inferred, concat)
# self.start_dir: Position of start of central directory
self.start_dir = offset_cd + concat
if self.start_dir < 0:
raise BadZipFile("Bad offset for central directory")
fp.seek(self.start_dir, 0)
size_cd = endrec[_ECD_SIZE]
data = fp.read(size_cd)
fp = io.BytesIO(data)
total = 0
@@ -1771,8 +1837,8 @@ class ZipFile:
def extract(self, member, path=None, pwd=None):
"""Extract a member from the archive to the current working directory,
using its full name. Its file information is extracted as accurately
as possible. `member' may be a filename or a ZipInfo object. You can
specify a different directory using `path'. You can specify the
as possible. 'member' may be a filename or a ZipInfo object. You can
specify a different directory using 'path'. You can specify the
password to decrypt the file using 'pwd'.
"""
if path is None:
@@ -1784,8 +1850,8 @@ class ZipFile:
def extractall(self, path=None, members=None, pwd=None):
"""Extract all members from the archive to the current working
directory. `path' specifies a different directory to extract to.
`members' is optional and must be a subset of the list returned
directory. 'path' specifies a different directory to extract to.
'members' is optional and must be a subset of the list returned
by namelist(). You can specify the password to decrypt all files
using 'pwd'.
"""
@@ -1929,18 +1995,10 @@ class ZipFile:
the name of the file in the archive."""
if isinstance(data, str):
data = data.encode("utf-8")
if not isinstance(zinfo_or_arcname, ZipInfo):
zinfo = ZipInfo(filename=zinfo_or_arcname,
date_time=time.localtime(time.time())[:6])
zinfo.compress_type = self.compression
zinfo.compress_level = self.compresslevel
if zinfo.filename.endswith('/'):
zinfo.external_attr = 0o40775 << 16 # drwxrwxr-x
zinfo.external_attr |= 0x10 # MS-DOS directory flag
else:
zinfo.external_attr = 0o600 << 16 # ?rw-------
else:
if isinstance(zinfo_or_arcname, ZipInfo):
zinfo = zinfo_or_arcname
else:
zinfo = ZipInfo(zinfo_or_arcname)._for_archive(self)
if not self.fp:
raise ValueError(
@@ -2059,6 +2117,8 @@ class ZipFile:
min_version = max(BZIP2_VERSION, min_version)
elif zinfo.compress_type == ZIP_LZMA:
min_version = max(LZMA_VERSION, min_version)
elif zinfo.compress_type == ZIP_ZSTANDARD:
min_version = max(ZSTANDARD_VERSION, min_version)
extract_version = max(min_version, zinfo.extract_version)
create_version = max(min_version, zinfo.create_version)
@@ -2301,7 +2361,7 @@ def main(args=None):
import argparse
description = 'A simple command-line interface for zipfile module.'
parser = argparse.ArgumentParser(description=description)
parser = argparse.ArgumentParser(description=description, color=True)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument('-l', '--list', metavar='<zipfile>',
help='Show listing of a zipfile')

View File

@@ -281,7 +281,7 @@ class Path:
>>> str(path.parent)
'mem'
If the zipfile has no filename, such attributes are not
If the zipfile has no filename, such attributes are not
valid and accessing them will raise an Exception.
>>> zf.filename = None

View File

@@ -36,9 +36,9 @@ class Translator:
Apply '(?s:)' to create a non-matching group that
matches newlines (valid on Unix).
Append '\Z' to imply fullmatch even when match is used.
Append '\z' to imply fullmatch even when match is used.
"""
return rf'(?s:{pattern})\Z'
return rf'(?s:{pattern})\z'
def match_dirs(self, pattern):
"""