Skip to content

Commit 564fcc1

Browse files
committed
Add SanitizedNames mixin.
1 parent 79a309f commit 564fcc1

File tree

1 file changed

+62
-0
lines changed

1 file changed

+62
-0
lines changed

zipp/__init__.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,68 @@ def __setstate__(self, state):
8686
super().__init__(*args, **kwargs)
8787

8888

89+
class SanitizedNames:
90+
"""
91+
ZipFile mix-in to ensure names are sanitized.
92+
"""
93+
94+
def namelist(self):
95+
return list(map(self._sanitize, super().namelist()))
96+
97+
@staticmethod
98+
def _sanitize(name):
99+
r"""
100+
Ensure a relative path with posix separators and no dot names.
101+
102+
Modeled after
103+
https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813
104+
but provides consistent cross-platform behavior.
105+
106+
>>> san = SanitizedNames._sanitize
107+
>>> san('/foo/bar')
108+
'foo/bar'
109+
>>> san('//foo.txt')
110+
'foo.txt'
111+
>>> san('foo/.././bar.txt')
112+
'foo/bar.txt'
113+
>>> san('foo../.bar.txt')
114+
'foo../.bar.txt'
115+
>>> san('\\foo\\bar.txt')
116+
'foo/bar.txt'
117+
>>> san('D:\\foo.txt')
118+
'D/foo.txt'
119+
>>> san('\\\\server\\share\\file.txt')
120+
'server/share/file.txt'
121+
>>> san('\\\\?\\GLOBALROOT\\Volume3')
122+
'?/GLOBALROOT/Volume3'
123+
>>> san('\\\\.\\PhysicalDrive1\\root')
124+
'PhysicalDrive1/root'
125+
126+
Retain any trailing slash.
127+
>>> san('abc/')
128+
'abc/'
129+
130+
Raises a ValueError if the result is empty.
131+
>>> san('../..')
132+
Traceback (most recent call last):
133+
...
134+
ValueError: Empty filename
135+
"""
136+
137+
def allowed(part):
138+
return part and part not in {'..', '.'}
139+
140+
# Remove the drive letter.
141+
# Don't use ntpath.splitdrive, because that also strips UNC paths
142+
bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE)
143+
clean = bare.replace('\\', '/')
144+
parts = clean.split('/')
145+
joined = '/'.join(filter(allowed, parts))
146+
if not joined:
147+
raise ValueError("Empty filename")
148+
return joined + '/' * name.endswith('/')
149+
150+
89151
class CompleteDirs(InitializedState, zipfile.ZipFile):
90152
"""
91153
A ZipFile subclass that ensures that implied directories

0 commit comments

Comments
 (0)