Skip to content

gh-123424: add ZipInfo._for_archive to set suitable default properties #123429

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 29, 2024
Merged
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
11 changes: 11 additions & 0 deletions Doc/library/zipfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,17 @@ The module defines the following items:
formerly protected :attr:`!_compresslevel`. The older protected name
continues to work as a property for backwards compatibility.


.. method:: _for_archive(archive)

Resolve the date_time, compression attributes, and external attributes
to suitable defaults as used by :meth:`ZipFile.writestr`.

Returns self for chaining.

.. versionadded:: 3.14


.. function:: is_zipfile(filename)

Returns ``True`` if *filename* is a valid ZIP file based on its magic number,
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,14 @@ uuid
in :rfc:`9562`.
(Contributed by Bénédikt Tran in :gh:`89083`.)

zipinfo
-------

* Added :func:`ZipInfo._for_archive <zipfile.ZipInfo._for_archive>`
to resolve suitable defaults for a :class:`~zipfile.ZipInfo` object
as used by :func:`ZipFile.writestr <zipfile.ZipFile.writestr>`.

(Contributed by Bénédikt Tran in :gh:`123424`.)

.. Add improved modules above alphabetically, not here at the end.

Expand Down
19 changes: 1 addition & 18 deletions Lib/test/test_zipfile/_path/test_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,7 +634,7 @@ def test_backslash_not_separator(self):
"""
data = io.BytesIO()
zf = zipfile.ZipFile(data, "w")
zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
zf.writestr(DirtyZipInfo("foo\\bar")._for_archive(zf), b"content")
zf.filename = ''
root = zipfile.Path(zf)
(first,) = root.iterdir()
Expand All @@ -657,20 +657,3 @@ class DirtyZipInfo(zipfile.ZipInfo):
def __init__(self, filename, *args, **kwargs):
super().__init__(filename, *args, **kwargs)
self.filename = filename

@classmethod
def for_name(cls, name, archive):
"""
Construct the same way that ZipFile.writestr does.

TODO: extract this functionality and re-use
"""
self = cls(filename=name, date_time=time.localtime(time.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
29 changes: 29 additions & 0 deletions Lib/test/test_zipfile/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import itertools
import os
import posixpath
import stat
import struct
import subprocess
import sys
Expand Down Expand Up @@ -2211,6 +2212,34 @@ def test_create_empty_zipinfo_repr(self):
zi = zipfile.ZipInfo(filename="empty")
self.assertEqual(repr(zi), "<ZipInfo filename='empty' file_size=0>")

def test_for_archive(self):
base_filename = TESTFN2.rstrip('/')

with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
compression=zipfile.ZIP_STORED) as zf:
# no trailing forward slash
zi = zipfile.ZipInfo(base_filename)._for_archive(zf)
self.assertEqual(zi.compress_level, 1)
self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
# ?rw- --- ---
filemode = stat.S_IRUSR | stat.S_IWUSR
# filemode is stored as the highest 16 bits of external_attr
self.assertEqual(zi.external_attr >> 16, filemode)
self.assertEqual(zi.external_attr & 0xFF, 0) # no MS-DOS flag

with zipfile.ZipFile(TESTFN, mode="w", compresslevel=1,
compression=zipfile.ZIP_STORED) as zf:
# with a trailing slash
zi = zipfile.ZipInfo(f'{base_filename}/')._for_archive(zf)
self.assertEqual(zi.compress_level, 1)
self.assertEqual(zi.compress_type, zipfile.ZIP_STORED)
# d rwx rwx r-x
filemode = stat.S_IFDIR
filemode |= stat.S_IRWXU | stat.S_IRWXG
filemode |= stat.S_IROTH | stat.S_IXOTH
self.assertEqual(zi.external_attr >> 16, filemode)
self.assertEqual(zi.external_attr & 0xFF, 0x10) # MS-DOS flag

def test_create_empty_zipinfo_default_attributes(self):
"""Ensure all required attributes are set."""
zi = zipfile.ZipInfo()
Expand Down
33 changes: 22 additions & 11 deletions Lib/zipfile/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import sys
import threading
import time
from typing import Self

try:
import zlib # We may need its compression method
Expand Down Expand Up @@ -605,6 +606,24 @@ def from_file(cls, filename, arcname=None, *, strict_timestamps=True):

return zinfo

def _for_archive(self, archive: ZipFile) -> Self:
"""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.
"""
self.date_time = time.localtime(time.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('/'):
Expand Down Expand Up @@ -1908,18 +1927,10 @@ def writestr(self, zinfo_or_arcname, data,
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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add :meth:`zipfile.ZipInfo._for_archive` setting default properties on :class:`~zipfile.ZipInfo` objects. Patch by Bénédikt Tran and Jason R. Coombs.
Loading