mirror of https://github.com/python/cpython
677 lines
22 KiB
Python
677 lines
22 KiB
Python
import io
|
|
import itertools
|
|
import contextlib
|
|
import pathlib
|
|
import pickle
|
|
import stat
|
|
import sys
|
|
import time
|
|
import unittest
|
|
import zipfile
|
|
import zipfile._path
|
|
|
|
from test.support.os_helper import temp_dir, FakePath
|
|
|
|
from ._functools import compose
|
|
from ._itertools import Counter
|
|
|
|
from ._test_params import parameterize, Invoked
|
|
|
|
|
|
class jaraco:
|
|
class itertools:
|
|
Counter = Counter
|
|
|
|
|
|
def _make_link(info: zipfile.ZipInfo): # type: ignore[name-defined]
|
|
info.external_attr |= stat.S_IFLNK << 16
|
|
|
|
|
|
def build_alpharep_fixture():
|
|
"""
|
|
Create a zip file with this structure:
|
|
|
|
.
|
|
├── a.txt
|
|
├── n.txt (-> a.txt)
|
|
├── b
|
|
│ ├── c.txt
|
|
│ ├── d
|
|
│ │ └── e.txt
|
|
│ └── f.txt
|
|
├── g
|
|
│ └── h
|
|
│ └── i.txt
|
|
└── j
|
|
├── k.bin
|
|
├── l.baz
|
|
└── m.bar
|
|
|
|
This fixture has the following key characteristics:
|
|
|
|
- a file at the root (a)
|
|
- a file two levels deep (b/d/e)
|
|
- multiple files in a directory (b/c, b/f)
|
|
- a directory containing only a directory (g/h)
|
|
- a directory with files of different extensions (j/klm)
|
|
- a symlink (n) pointing to (a)
|
|
|
|
"alpha" because it uses alphabet
|
|
"rep" because it's a representative example
|
|
"""
|
|
data = io.BytesIO()
|
|
zf = zipfile.ZipFile(data, "w")
|
|
zf.writestr("a.txt", b"content of a")
|
|
zf.writestr("b/c.txt", b"content of c")
|
|
zf.writestr("b/d/e.txt", b"content of e")
|
|
zf.writestr("b/f.txt", b"content of f")
|
|
zf.writestr("g/h/i.txt", b"content of i")
|
|
zf.writestr("j/k.bin", b"content of k")
|
|
zf.writestr("j/l.baz", b"content of l")
|
|
zf.writestr("j/m.bar", b"content of m")
|
|
zf.writestr("n.txt", b"a.txt")
|
|
_make_link(zf.infolist()[-1])
|
|
|
|
zf.filename = "alpharep.zip"
|
|
return zf
|
|
|
|
|
|
alpharep_generators = [
|
|
Invoked.wrap(build_alpharep_fixture),
|
|
Invoked.wrap(compose(zipfile._path.CompleteDirs.inject, build_alpharep_fixture)),
|
|
]
|
|
|
|
pass_alpharep = parameterize(['alpharep'], alpharep_generators)
|
|
|
|
|
|
class TestPath(unittest.TestCase):
|
|
def setUp(self):
|
|
self.fixtures = contextlib.ExitStack()
|
|
self.addCleanup(self.fixtures.close)
|
|
|
|
def zipfile_ondisk(self, alpharep):
|
|
tmpdir = pathlib.Path(self.fixtures.enter_context(temp_dir()))
|
|
buffer = alpharep.fp
|
|
alpharep.close()
|
|
path = tmpdir / alpharep.filename
|
|
with path.open("wb") as strm:
|
|
strm.write(buffer.getvalue())
|
|
return path
|
|
|
|
@pass_alpharep
|
|
def test_iterdir_and_types(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert root.is_dir()
|
|
a, n, b, g, j = root.iterdir()
|
|
assert a.is_file()
|
|
assert b.is_dir()
|
|
assert g.is_dir()
|
|
c, f, d = b.iterdir()
|
|
assert c.is_file() and f.is_file()
|
|
(e,) = d.iterdir()
|
|
assert e.is_file()
|
|
(h,) = g.iterdir()
|
|
(i,) = h.iterdir()
|
|
assert i.is_file()
|
|
|
|
@pass_alpharep
|
|
def test_is_file_missing(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert not root.joinpath('missing.txt').is_file()
|
|
|
|
@pass_alpharep
|
|
def test_iterdir_on_file(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
a, n, b, g, j = root.iterdir()
|
|
with self.assertRaises(ValueError):
|
|
a.iterdir()
|
|
|
|
@pass_alpharep
|
|
def test_subdir_is_dir(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert (root / 'b').is_dir()
|
|
assert (root / 'b/').is_dir()
|
|
assert (root / 'g').is_dir()
|
|
assert (root / 'g/').is_dir()
|
|
|
|
@pass_alpharep
|
|
def test_open(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
a, n, b, g, j = root.iterdir()
|
|
with a.open(encoding="utf-8") as strm:
|
|
data = strm.read()
|
|
self.assertEqual(data, "content of a")
|
|
with a.open('r', "utf-8") as strm: # not a kw, no gh-101144 TypeError
|
|
data = strm.read()
|
|
self.assertEqual(data, "content of a")
|
|
|
|
def test_open_encoding_utf16(self):
|
|
in_memory_file = io.BytesIO()
|
|
zf = zipfile.ZipFile(in_memory_file, "w")
|
|
zf.writestr("path/16.txt", "This was utf-16".encode("utf-16"))
|
|
zf.filename = "test_open_utf16.zip"
|
|
root = zipfile.Path(zf)
|
|
(path,) = root.iterdir()
|
|
u16 = path.joinpath("16.txt")
|
|
with u16.open('r', "utf-16") as strm:
|
|
data = strm.read()
|
|
assert data == "This was utf-16"
|
|
with u16.open(encoding="utf-16") as strm:
|
|
data = strm.read()
|
|
assert data == "This was utf-16"
|
|
|
|
def test_open_encoding_errors(self):
|
|
in_memory_file = io.BytesIO()
|
|
zf = zipfile.ZipFile(in_memory_file, "w")
|
|
zf.writestr("path/bad-utf8.bin", b"invalid utf-8: \xff\xff.")
|
|
zf.filename = "test_read_text_encoding_errors.zip"
|
|
root = zipfile.Path(zf)
|
|
(path,) = root.iterdir()
|
|
u16 = path.joinpath("bad-utf8.bin")
|
|
|
|
# encoding= as a positional argument for gh-101144.
|
|
data = u16.read_text("utf-8", errors="ignore")
|
|
assert data == "invalid utf-8: ."
|
|
with u16.open("r", "utf-8", errors="surrogateescape") as f:
|
|
assert f.read() == "invalid utf-8: \udcff\udcff."
|
|
|
|
# encoding= both positional and keyword is an error; gh-101144.
|
|
with self.assertRaisesRegex(TypeError, "encoding"):
|
|
data = u16.read_text("utf-8", encoding="utf-8")
|
|
|
|
# both keyword arguments work.
|
|
with u16.open("r", encoding="utf-8", errors="strict") as f:
|
|
# error during decoding with wrong codec.
|
|
with self.assertRaises(UnicodeDecodeError):
|
|
f.read()
|
|
|
|
@unittest.skipIf(
|
|
not getattr(sys.flags, 'warn_default_encoding', 0),
|
|
"Requires warn_default_encoding",
|
|
)
|
|
@pass_alpharep
|
|
def test_encoding_warnings(self, alpharep):
|
|
"""EncodingWarning must blame the read_text and open calls."""
|
|
assert sys.flags.warn_default_encoding
|
|
root = zipfile.Path(alpharep)
|
|
with self.assertWarns(EncodingWarning) as wc:
|
|
root.joinpath("a.txt").read_text()
|
|
assert __file__ == wc.filename
|
|
with self.assertWarns(EncodingWarning) as wc:
|
|
root.joinpath("a.txt").open("r").close()
|
|
assert __file__ == wc.filename
|
|
|
|
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', encoding="utf-8") as strm:
|
|
strm.write('text file')
|
|
|
|
@pass_alpharep
|
|
def test_open_extant_directory(self, alpharep):
|
|
"""
|
|
Attempting to open a directory raises IsADirectoryError.
|
|
"""
|
|
zf = zipfile.Path(alpharep)
|
|
with self.assertRaises(IsADirectoryError):
|
|
zf.joinpath('b').open()
|
|
|
|
@pass_alpharep
|
|
def test_open_binary_invalid_args(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
with self.assertRaises(ValueError):
|
|
root.joinpath('a.txt').open('rb', encoding='utf-8')
|
|
with self.assertRaises(ValueError):
|
|
root.joinpath('a.txt').open('rb', 'utf-8')
|
|
|
|
@pass_alpharep
|
|
def test_open_missing_directory(self, alpharep):
|
|
"""
|
|
Attempting to open a missing directory raises FileNotFoundError.
|
|
"""
|
|
zf = zipfile.Path(alpharep)
|
|
with self.assertRaises(FileNotFoundError):
|
|
zf.joinpath('z').open()
|
|
|
|
@pass_alpharep
|
|
def test_read(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
a, n, b, g, j = root.iterdir()
|
|
assert a.read_text(encoding="utf-8") == "content of a"
|
|
# Also check positional encoding arg (gh-101144).
|
|
assert a.read_text("utf-8") == "content of a"
|
|
assert a.read_bytes() == b"content of a"
|
|
|
|
@pass_alpharep
|
|
def test_joinpath(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
a = root.joinpath("a.txt")
|
|
assert a.is_file()
|
|
e = root.joinpath("b").joinpath("d").joinpath("e.txt")
|
|
assert e.read_text(encoding="utf-8") == "content of e"
|
|
|
|
@pass_alpharep
|
|
def test_joinpath_multiple(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
e = root.joinpath("b", "d", "e.txt")
|
|
assert e.read_text(encoding="utf-8") == "content of e"
|
|
|
|
@pass_alpharep
|
|
def test_traverse_truediv(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
a = root / "a.txt"
|
|
assert a.is_file()
|
|
e = root / "b" / "d" / "e.txt"
|
|
assert e.read_text(encoding="utf-8") == "content of e"
|
|
|
|
@pass_alpharep
|
|
def test_pathlike_construction(self, alpharep):
|
|
"""
|
|
zipfile.Path should be constructable from a path-like object
|
|
"""
|
|
zipfile_ondisk = self.zipfile_ondisk(alpharep)
|
|
pathlike = FakePath(str(zipfile_ondisk))
|
|
zipfile.Path(pathlike)
|
|
|
|
@pass_alpharep
|
|
def test_traverse_pathlike(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
root / FakePath("a")
|
|
|
|
@pass_alpharep
|
|
def test_parent(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert (root / 'a').parent.at == ''
|
|
assert (root / 'a' / 'b').parent.at == 'a/'
|
|
|
|
@pass_alpharep
|
|
def test_dir_parent(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert (root / 'b').parent.at == ''
|
|
assert (root / 'b/').parent.at == ''
|
|
|
|
@pass_alpharep
|
|
def test_missing_dir_parent(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert (root / 'missing dir/').parent.at == ''
|
|
|
|
@pass_alpharep
|
|
def test_mutability(self, alpharep):
|
|
"""
|
|
If the underlying zipfile is changed, the Path object should
|
|
reflect that change.
|
|
"""
|
|
root = zipfile.Path(alpharep)
|
|
a, n, b, g, j = root.iterdir()
|
|
alpharep.writestr('foo.txt', 'foo')
|
|
alpharep.writestr('bar/baz.txt', 'baz')
|
|
assert any(child.name == 'foo.txt' for child in root.iterdir())
|
|
assert (root / 'foo.txt').read_text(encoding="utf-8") == 'foo'
|
|
(baz,) = (root / 'bar').iterdir()
|
|
assert baz.read_text(encoding="utf-8") == 'baz'
|
|
|
|
HUGE_ZIPFILE_NUM_ENTRIES = 2**13
|
|
|
|
def huge_zipfile(self):
|
|
"""Create a read-only zipfile with a huge number of entries entries."""
|
|
strm = io.BytesIO()
|
|
zf = zipfile.ZipFile(strm, "w")
|
|
for entry in map(str, range(self.HUGE_ZIPFILE_NUM_ENTRIES)):
|
|
zf.writestr(entry, entry)
|
|
zf.mode = 'r'
|
|
return zf
|
|
|
|
def test_joinpath_constant_time(self):
|
|
"""
|
|
Ensure joinpath on items in zipfile is linear time.
|
|
"""
|
|
root = zipfile.Path(self.huge_zipfile())
|
|
entries = jaraco.itertools.Counter(root.iterdir())
|
|
for entry in entries:
|
|
entry.joinpath('suffix')
|
|
# Check the file iterated all items
|
|
assert entries.count == self.HUGE_ZIPFILE_NUM_ENTRIES
|
|
|
|
@pass_alpharep
|
|
def test_read_does_not_close(self, alpharep):
|
|
alpharep = self.zipfile_ondisk(alpharep)
|
|
with zipfile.ZipFile(alpharep) as file:
|
|
for rep in range(2):
|
|
zipfile.Path(file, 'a.txt').read_text(encoding="utf-8")
|
|
|
|
@pass_alpharep
|
|
def test_subclass(self, alpharep):
|
|
class Subclass(zipfile.Path):
|
|
pass
|
|
|
|
root = Subclass(alpharep)
|
|
assert isinstance(root / 'b', Subclass)
|
|
|
|
@pass_alpharep
|
|
def test_filename(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert root.filename == pathlib.Path('alpharep.zip')
|
|
|
|
@pass_alpharep
|
|
def test_root_name(self, alpharep):
|
|
"""
|
|
The name of the root should be the name of the zipfile
|
|
"""
|
|
root = zipfile.Path(alpharep)
|
|
assert root.name == 'alpharep.zip' == root.filename.name
|
|
|
|
@pass_alpharep
|
|
def test_suffix(self, alpharep):
|
|
"""
|
|
The suffix of the root should be the suffix of the zipfile.
|
|
The suffix of each nested file is the final component's last suffix, if any.
|
|
Includes the leading period, just like pathlib.Path.
|
|
"""
|
|
root = zipfile.Path(alpharep)
|
|
assert root.suffix == '.zip' == root.filename.suffix
|
|
|
|
b = root / "b.txt"
|
|
assert b.suffix == ".txt"
|
|
|
|
c = root / "c" / "filename.tar.gz"
|
|
assert c.suffix == ".gz"
|
|
|
|
d = root / "d"
|
|
assert d.suffix == ""
|
|
|
|
@pass_alpharep
|
|
def test_suffixes(self, alpharep):
|
|
"""
|
|
The suffix of the root should be the suffix of the zipfile.
|
|
The suffix of each nested file is the final component's last suffix, if any.
|
|
Includes the leading period, just like pathlib.Path.
|
|
"""
|
|
root = zipfile.Path(alpharep)
|
|
assert root.suffixes == ['.zip'] == root.filename.suffixes
|
|
|
|
b = root / 'b.txt'
|
|
assert b.suffixes == ['.txt']
|
|
|
|
c = root / 'c' / 'filename.tar.gz'
|
|
assert c.suffixes == ['.tar', '.gz']
|
|
|
|
d = root / 'd'
|
|
assert d.suffixes == []
|
|
|
|
e = root / '.hgrc'
|
|
assert e.suffixes == []
|
|
|
|
@pass_alpharep
|
|
def test_suffix_no_filename(self, alpharep):
|
|
alpharep.filename = None
|
|
root = zipfile.Path(alpharep)
|
|
assert root.joinpath('example').suffix == ""
|
|
assert root.joinpath('example').suffixes == []
|
|
|
|
@pass_alpharep
|
|
def test_stem(self, alpharep):
|
|
"""
|
|
The final path component, without its suffix
|
|
"""
|
|
root = zipfile.Path(alpharep)
|
|
assert root.stem == 'alpharep' == root.filename.stem
|
|
|
|
b = root / "b.txt"
|
|
assert b.stem == "b"
|
|
|
|
c = root / "c" / "filename.tar.gz"
|
|
assert c.stem == "filename.tar"
|
|
|
|
d = root / "d"
|
|
assert d.stem == "d"
|
|
|
|
assert (root / ".gitignore").stem == ".gitignore"
|
|
|
|
@pass_alpharep
|
|
def test_root_parent(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert root.parent == pathlib.Path('.')
|
|
root.root.filename = 'foo/bar.zip'
|
|
assert root.parent == pathlib.Path('foo')
|
|
|
|
@pass_alpharep
|
|
def test_root_unnamed(self, alpharep):
|
|
"""
|
|
It is an error to attempt to get the name
|
|
or parent of an unnamed zipfile.
|
|
"""
|
|
alpharep.filename = None
|
|
root = zipfile.Path(alpharep)
|
|
with self.assertRaises(TypeError):
|
|
root.name
|
|
with self.assertRaises(TypeError):
|
|
root.parent
|
|
|
|
# .name and .parent should still work on subs
|
|
sub = root / "b"
|
|
assert sub.name == "b"
|
|
assert sub.parent
|
|
|
|
@pass_alpharep
|
|
def test_match_and_glob(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert not root.match("*.txt")
|
|
|
|
assert list(root.glob("b/c.*")) == [zipfile.Path(alpharep, "b/c.txt")]
|
|
assert list(root.glob("b/*.txt")) == [
|
|
zipfile.Path(alpharep, "b/c.txt"),
|
|
zipfile.Path(alpharep, "b/f.txt"),
|
|
]
|
|
|
|
@pass_alpharep
|
|
def test_glob_recursive(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
files = root.glob("**/*.txt")
|
|
assert all(each.match("*.txt") for each in files)
|
|
|
|
assert list(root.glob("**/*.txt")) == list(root.rglob("*.txt"))
|
|
|
|
@pass_alpharep
|
|
def test_glob_dirs(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert list(root.glob('b')) == [zipfile.Path(alpharep, "b/")]
|
|
assert list(root.glob('b*')) == [zipfile.Path(alpharep, "b/")]
|
|
|
|
@pass_alpharep
|
|
def test_glob_subdir(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert list(root.glob('g/h')) == [zipfile.Path(alpharep, "g/h/")]
|
|
assert list(root.glob('g*/h*')) == [zipfile.Path(alpharep, "g/h/")]
|
|
|
|
@pass_alpharep
|
|
def test_glob_subdirs(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
|
|
assert list(root.glob("*/i.txt")) == []
|
|
assert list(root.rglob("*/i.txt")) == [zipfile.Path(alpharep, "g/h/i.txt")]
|
|
|
|
@pass_alpharep
|
|
def test_glob_does_not_overmatch_dot(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
|
|
assert list(root.glob("*.xt")) == []
|
|
|
|
@pass_alpharep
|
|
def test_glob_single_char(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
|
|
assert list(root.glob("a?txt")) == [zipfile.Path(alpharep, "a.txt")]
|
|
assert list(root.glob("a[.]txt")) == [zipfile.Path(alpharep, "a.txt")]
|
|
assert list(root.glob("a[?]txt")) == []
|
|
|
|
@pass_alpharep
|
|
def test_glob_chars(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
|
|
assert list(root.glob("j/?.b[ai][nz]")) == [
|
|
zipfile.Path(alpharep, "j/k.bin"),
|
|
zipfile.Path(alpharep, "j/l.baz"),
|
|
]
|
|
|
|
def test_glob_empty(self):
|
|
root = zipfile.Path(zipfile.ZipFile(io.BytesIO(), 'w'))
|
|
with self.assertRaises(ValueError):
|
|
root.glob('')
|
|
|
|
@pass_alpharep
|
|
def test_eq_hash(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert root == zipfile.Path(alpharep)
|
|
|
|
assert root != (root / "a.txt")
|
|
assert (root / "a.txt") == (root / "a.txt")
|
|
|
|
root = zipfile.Path(alpharep)
|
|
assert root in {root}
|
|
|
|
@pass_alpharep
|
|
def test_is_symlink(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
assert not root.joinpath('a.txt').is_symlink()
|
|
assert root.joinpath('n.txt').is_symlink()
|
|
|
|
@pass_alpharep
|
|
def test_relative_to(self, alpharep):
|
|
root = zipfile.Path(alpharep)
|
|
relative = root.joinpath("b", "c.txt").relative_to(root / "b")
|
|
assert str(relative) == "c.txt"
|
|
|
|
relative = root.joinpath("b", "d", "e.txt").relative_to(root / "b")
|
|
assert str(relative) == "d/e.txt"
|
|
|
|
@pass_alpharep
|
|
def test_inheritance(self, alpharep):
|
|
cls = type('PathChild', (zipfile.Path,), {})
|
|
file = cls(alpharep).joinpath('some dir').parent
|
|
assert isinstance(file, cls)
|
|
|
|
@parameterize(
|
|
['alpharep', 'path_type', 'subpath'],
|
|
itertools.product(
|
|
alpharep_generators,
|
|
[str, FakePath],
|
|
['', 'b/'],
|
|
),
|
|
)
|
|
def test_pickle(self, alpharep, path_type, subpath):
|
|
zipfile_ondisk = path_type(str(self.zipfile_ondisk(alpharep)))
|
|
|
|
saved_1 = pickle.dumps(zipfile.Path(zipfile_ondisk, at=subpath))
|
|
restored_1 = pickle.loads(saved_1)
|
|
first, *rest = restored_1.iterdir()
|
|
assert first.read_text(encoding='utf-8').startswith('content of ')
|
|
|
|
@pass_alpharep
|
|
def test_extract_orig_with_implied_dirs(self, alpharep):
|
|
"""
|
|
A zip file wrapped in a Path should extract even with implied dirs.
|
|
"""
|
|
source_path = self.zipfile_ondisk(alpharep)
|
|
zf = zipfile.ZipFile(source_path)
|
|
# wrap the zipfile for its side effect
|
|
zipfile.Path(zf)
|
|
zf.extractall(source_path.parent)
|
|
|
|
@pass_alpharep
|
|
def test_getinfo_missing(self, alpharep):
|
|
"""
|
|
Validate behavior of getinfo on original zipfile after wrapping.
|
|
"""
|
|
zipfile.Path(alpharep)
|
|
with self.assertRaises(KeyError):
|
|
alpharep.getinfo('does-not-exist')
|
|
|
|
def test_malformed_paths(self):
|
|
"""
|
|
Path should handle malformed paths gracefully.
|
|
|
|
Paths with leading slashes are not visible.
|
|
|
|
Paths with dots are treated like regular files.
|
|
"""
|
|
data = io.BytesIO()
|
|
zf = zipfile.ZipFile(data, "w")
|
|
zf.writestr("/one-slash.txt", b"content")
|
|
zf.writestr("//two-slash.txt", b"content")
|
|
zf.writestr("../parent.txt", b"content")
|
|
zf.filename = ''
|
|
root = zipfile.Path(zf)
|
|
assert list(map(str, root.iterdir())) == ['../']
|
|
assert root.joinpath('..').joinpath('parent.txt').read_bytes() == b'content'
|
|
|
|
def test_unsupported_names(self):
|
|
"""
|
|
Path segments with special characters are readable.
|
|
|
|
On some platforms or file systems, characters like
|
|
``:`` and ``?`` are not allowed, but they are valid
|
|
in the zip file.
|
|
"""
|
|
data = io.BytesIO()
|
|
zf = zipfile.ZipFile(data, "w")
|
|
zf.writestr("path?", b"content")
|
|
zf.writestr("V: NMS.flac", b"fLaC...")
|
|
zf.filename = ''
|
|
root = zipfile.Path(zf)
|
|
contents = root.iterdir()
|
|
assert next(contents).name == 'path?'
|
|
assert next(contents).name == 'V: NMS.flac'
|
|
assert root.joinpath('V: NMS.flac').read_bytes() == b"fLaC..."
|
|
|
|
def test_backslash_not_separator(self):
|
|
"""
|
|
In a zip file, backslashes are not separators.
|
|
"""
|
|
data = io.BytesIO()
|
|
zf = zipfile.ZipFile(data, "w")
|
|
zf.writestr(DirtyZipInfo.for_name("foo\\bar", zf), b"content")
|
|
zf.filename = ''
|
|
root = zipfile.Path(zf)
|
|
(first,) = root.iterdir()
|
|
assert not first.is_dir()
|
|
assert first.name == 'foo\\bar'
|
|
|
|
@pass_alpharep
|
|
def test_interface(self, alpharep):
|
|
from importlib.resources.abc import Traversable
|
|
|
|
zf = zipfile.Path(alpharep)
|
|
assert isinstance(zf, Traversable)
|
|
|
|
|
|
class DirtyZipInfo(zipfile.ZipInfo):
|
|
"""
|
|
Bypass name sanitization.
|
|
"""
|
|
|
|
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
|