|
9 | 9 | import itertools
|
10 | 10 | import os
|
11 | 11 | import posixpath
|
| 12 | +import re |
12 | 13 | import shutil
|
13 | 14 | import stat
|
14 | 15 | import struct
|
@@ -2243,7 +2244,65 @@ def _difference(minuend, subtrahend):
|
2243 | 2244 | return itertools.filterfalse(set(subtrahend).__contains__, minuend)
|
2244 | 2245 |
|
2245 | 2246 |
|
2246 |
| -class CompleteDirs(ZipFile): |
| 2247 | +class SanitizedNames: |
| 2248 | + """ |
| 2249 | + ZipFile mix-in to ensure names are sanitized. |
| 2250 | + """ |
| 2251 | + |
| 2252 | + def namelist(self): |
| 2253 | + return list(map(self._sanitize, super().namelist())) |
| 2254 | + |
| 2255 | + @staticmethod |
| 2256 | + def _sanitize(name): |
| 2257 | + r""" |
| 2258 | + Ensure a relative path with posix separators and no dot names. |
| 2259 | + Modeled after |
| 2260 | + https://github.com/python/cpython/blob/bcc1be39cb1d04ad9fc0bd1b9193d3972835a57c/Lib/zipfile/__init__.py#L1799-L1813 |
| 2261 | + but provides consistent cross-platform behavior. |
| 2262 | + >>> san = SanitizedNames._sanitize |
| 2263 | + >>> san('/foo/bar') |
| 2264 | + 'foo/bar' |
| 2265 | + >>> san('//foo.txt') |
| 2266 | + 'foo.txt' |
| 2267 | + >>> san('foo/.././bar.txt') |
| 2268 | + 'foo/bar.txt' |
| 2269 | + >>> san('foo../.bar.txt') |
| 2270 | + 'foo../.bar.txt' |
| 2271 | + >>> san('\\foo\\bar.txt') |
| 2272 | + 'foo/bar.txt' |
| 2273 | + >>> san('D:\\foo.txt') |
| 2274 | + 'D/foo.txt' |
| 2275 | + >>> san('\\\\server\\share\\file.txt') |
| 2276 | + 'server/share/file.txt' |
| 2277 | + >>> san('\\\\?\\GLOBALROOT\\Volume3') |
| 2278 | + '?/GLOBALROOT/Volume3' |
| 2279 | + >>> san('\\\\.\\PhysicalDrive1\\root') |
| 2280 | + 'PhysicalDrive1/root' |
| 2281 | + Retain any trailing slash. |
| 2282 | + >>> san('abc/') |
| 2283 | + 'abc/' |
| 2284 | + Raises a ValueError if the result is empty. |
| 2285 | + >>> san('../..') |
| 2286 | + Traceback (most recent call last): |
| 2287 | + ... |
| 2288 | + ValueError: Empty filename |
| 2289 | + """ |
| 2290 | + |
| 2291 | + def allowed(part): |
| 2292 | + return part and part not in {'..', '.'} |
| 2293 | + |
| 2294 | + # Remove the drive letter. |
| 2295 | + # Don't use ntpath.splitdrive, because that also strips UNC paths |
| 2296 | + bare = re.sub('^([A-Z]):', r'\1', name, flags=re.IGNORECASE) |
| 2297 | + clean = bare.replace('\\', '/') |
| 2298 | + parts = clean.split('/') |
| 2299 | + joined = '/'.join(filter(allowed, parts)) |
| 2300 | + if not joined: |
| 2301 | + raise ValueError("Empty filename") |
| 2302 | + return joined + '/' * name.endswith('/') |
| 2303 | + |
| 2304 | + |
| 2305 | +class CompleteDirs(SanitizedNames, ZipFile): |
2247 | 2306 | """
|
2248 | 2307 | A ZipFile subclass that ensures that implied directories
|
2249 | 2308 | are always included in the namelist.
|
|
0 commit comments