From e34c61d4fa78031520640f192a85eee084e28ead Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 22 Sep 2020 21:57:08 -0400 Subject: [PATCH 1/2] bpo-40564: Avoid copying state from extant ZipFile. --- Lib/test/test_zipfile.py | 33 +++++++++++++++++++++++++++++++++ Lib/zipfile.py | 23 +++++++++++++++++------ 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_zipfile.py b/Lib/test/test_zipfile.py index 2851051425bf12f..ce8694ab4501b50 100644 --- a/Lib/test/test_zipfile.py +++ b/Lib/test/test_zipfile.py @@ -2886,6 +2886,33 @@ def test_open(self): data = strm.read() assert data == "content of a" + def test_open_write(self): + """ + If the zipfile is open for write, it should be possible to + write bytes or text to it. + """ + zf = zipfile.Path(zipfile.ZipFile(io.BytesIO(), mode='w')) + with zf.joinpath('file.bin').open('wb') as strm: + strm.write(b'binary contents') + with zf.joinpath('file.txt').open('w') as strm: + strm.write('text file') + + def test_open_extant_directory(self): + """ + Attempting to open a directory raises IsADirectoryError. + """ + zf = zipfile.Path(add_dirs(build_alpharep_fixture())) + with self.assertRaises(IsADirectoryError): + zf.joinpath('b').open() + + def test_open_missing_directory(self): + """ + Attempting to open a missing directory raises FileNotFoundError. + """ + zf = zipfile.Path(add_dirs(build_alpharep_fixture())) + with self.assertRaises(FileNotFoundError): + zf.joinpath('z').open() + def test_read(self): for alpharep in self.zipfile_alpharep(): root = zipfile.Path(alpharep) @@ -2983,6 +3010,12 @@ def test_implied_dirs_performance(self): data = ['/'.join(string.ascii_lowercase + str(n)) for n in range(10000)] zipfile.CompleteDirs._implied_dirs(data) + def test_read_does_not_close(self): + for alpharep in self.zipfile_ondisk(): + with zipfile.ZipFile(alpharep) as file: + for rep in range(2): + zipfile.Path(file, 'a.txt').read_text() + if __name__ == "__main__": unittest.main() diff --git a/Lib/zipfile.py b/Lib/zipfile.py index 915698f9e058893..7fb782a1d604694 100644 --- a/Lib/zipfile.py +++ b/Lib/zipfile.py @@ -2195,13 +2195,12 @@ def make(cls, source): if not isinstance(source, ZipFile): return cls(source) - # Only allow for FastPath when supplied zipfile is read-only + # Only allow for FastLookup when supplied zipfile is read-only if 'r' not in source.mode: cls = CompleteDirs - res = cls.__new__(cls) - vars(res).update(vars(source)) - return res + source.__class__ = cls + return source class FastLookup(CompleteDirs): @@ -2290,17 +2289,29 @@ class Path: __repr = "{self.__class__.__name__}({self.root.filename!r}, {self.at!r})" def __init__(self, root, at=""): + """ + Construct a Path from a ZipFile or filename. + + Note: When the source is an existing ZipFile object, + its type (__class__) will be mutated to a + specialized type. If the caller wishes to retain the + original type, the caller should either create a + separate ZipFile object or pass a filename. + """ self.root = FastLookup.make(root) self.at = at - def open(self, mode='r', *args, **kwargs): + def open(self, mode='r', *args, pwd=None, **kwargs): """ Open this entry as text or binary following the semantics of ``pathlib.Path.open()`` by passing arguments through to io.TextIOWrapper(). """ - pwd = kwargs.pop('pwd', None) + if self.is_dir(): + raise IsADirectoryError(self) zip_mode = mode[0] + if not self.exists() and zip_mode == 'r': + raise FileNotFoundError(self) stream = self.root.open(self.at, zip_mode, pwd=pwd) if 'b' in mode: if args or kwargs: From eba5474d7c6a8e3b8c9526951ce2d526fc3d856b Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2020 03:33:40 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2020-09-23-03-33-37.bpo-40564.iXQqMq.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2020-09-23-03-33-37.bpo-40564.iXQqMq.rst diff --git a/Misc/NEWS.d/next/Library/2020-09-23-03-33-37.bpo-40564.iXQqMq.rst b/Misc/NEWS.d/next/Library/2020-09-23-03-33-37.bpo-40564.iXQqMq.rst new file mode 100644 index 000000000000000..085534734ec948d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-09-23-03-33-37.bpo-40564.iXQqMq.rst @@ -0,0 +1 @@ +In ``zipfile.Path``, mutate the passed ZipFile object type instead of making a copy. Prevents issues when both the local copy and the caller’s copy attempt to close the same file handle. \ No newline at end of file