mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
Update gzip, tarfile, and shutil from CPython 3.15 (#9112)
* Preserve truncating FileIO mode and sync canonical IO compatibility Keep truncation metadata in both host backends so writable update streams report wb+. Synchronize the complete final CPython _pyio module and two complete canonical mode test definitions through scripts/update_lib. Assisted-by: OpenAI Codex: model identifier unavailable * Update gzip, tarfile, and shutil from CPython 3.15 Regenerate six Lib files using scripts/update_lib from CPython d220409a13b77de7a6c30abe16ea90db01528edc. Preserve upstream tests without agent-added marks. Assisted-by: OpenAI Codex:model version unavailable * Sync canonical spooled tempfile mode expectation Copy the complete TestSpooledTemporaryFile.test_properties definition from CPython 3.15.0 final d220409a using scripts/update_lib copy-test. Preserve all surrounding test content and existing markers. Assisted-by: OpenAI Codex:model version unavailable --------- Co-authored-by: youknowdot <336763669+youknowdot@users.noreply.github.com>
This commit is contained in:
1 parent
0f127b07e9
commit
f9b07b786c
12 files changed
+477
-143
No files matched your search
Vendored
+60
-36
@@ -550,7 +550,7 @@ class IOBase(metaclass=abc.ABCMeta):
|
||||
res += b
|
||||
if res.endswith(b"\n"):
|
||||
break
|
||||
return bytes(res)
|
||||
return res.take_bytes()
|
||||
|
||||
def __iter__(self):
|
||||
self._checkClosed()
|
||||
@@ -624,7 +624,7 @@ class RawIOBase(IOBase):
|
||||
if n < 0 or n > len(b):
|
||||
raise ValueError(f"readinto returned {n} outside buffer size {len(b)}")
|
||||
del b[n:]
|
||||
return bytes(b)
|
||||
return b.take_bytes()
|
||||
|
||||
def readall(self):
|
||||
"""Read until EOF, using multiple read() call."""
|
||||
@@ -632,7 +632,7 @@ class RawIOBase(IOBase):
|
||||
while data := self.read(DEFAULT_BUFFER_SIZE):
|
||||
res += data
|
||||
if res:
|
||||
return bytes(res)
|
||||
return res.take_bytes()
|
||||
else:
|
||||
# b'' or None
|
||||
return data
|
||||
@@ -882,16 +882,28 @@ class BytesIO(BufferedIOBase):
|
||||
_buffer = None
|
||||
|
||||
def __init__(self, initial_bytes=None):
|
||||
# Use to keep self._buffer and self._pos consistent.
|
||||
self._lock = Lock()
|
||||
|
||||
buf = bytearray()
|
||||
if initial_bytes is not None:
|
||||
buf += initial_bytes
|
||||
self._buffer = buf
|
||||
self._pos = 0
|
||||
|
||||
with self._lock:
|
||||
self._buffer = buf
|
||||
self._pos = 0
|
||||
|
||||
def __getstate__(self):
|
||||
if self.closed:
|
||||
raise ValueError("__getstate__ on closed file")
|
||||
return self.__dict__.copy()
|
||||
with self._lock:
|
||||
state = self.__dict__.copy()
|
||||
del state['_lock']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
self.__dict__.update(state)
|
||||
self._lock = Lock()
|
||||
|
||||
def getvalue(self):
|
||||
"""Return the bytes value (contents) of the buffer
|
||||
@@ -924,14 +936,16 @@ class BytesIO(BufferedIOBase):
|
||||
raise TypeError(f"{size!r} is not an integer")
|
||||
else:
|
||||
size = size_index()
|
||||
if size < 0:
|
||||
size = len(self._buffer)
|
||||
if len(self._buffer) <= self._pos:
|
||||
return b""
|
||||
newpos = min(len(self._buffer), self._pos + size)
|
||||
b = self._buffer[self._pos : newpos]
|
||||
self._pos = newpos
|
||||
return bytes(b)
|
||||
|
||||
with self._lock:
|
||||
if size < 0:
|
||||
size = len(self._buffer)
|
||||
if len(self._buffer) <= self._pos:
|
||||
return b""
|
||||
newpos = min(len(self._buffer), self._pos + size)
|
||||
b = self._buffer[self._pos : newpos]
|
||||
self._pos = newpos
|
||||
return bytes(b)
|
||||
|
||||
def read1(self, size=-1):
|
||||
"""This is the same as read.
|
||||
@@ -949,13 +963,14 @@ class BytesIO(BufferedIOBase):
|
||||
if n == 0:
|
||||
return 0
|
||||
|
||||
pos = self._pos
|
||||
if pos > len(self._buffer):
|
||||
# Pad buffer to pos with null bytes.
|
||||
self._buffer.resize(pos)
|
||||
self._buffer[pos:pos + n] = view
|
||||
self._pos += n
|
||||
return n
|
||||
with self._lock:
|
||||
pos = self._pos
|
||||
if pos > len(self._buffer):
|
||||
# Pad buffer to pos with null bytes.
|
||||
self._buffer.resize(pos)
|
||||
self._buffer[pos:pos + n] = view
|
||||
self._pos += n
|
||||
return n
|
||||
|
||||
def seek(self, pos, whence=0):
|
||||
if self.closed:
|
||||
@@ -971,9 +986,11 @@ class BytesIO(BufferedIOBase):
|
||||
raise ValueError("negative seek position %r" % (pos,))
|
||||
self._pos = pos
|
||||
elif whence == 1:
|
||||
self._pos = max(0, self._pos + pos)
|
||||
with self._lock:
|
||||
self._pos = max(0, self._pos + pos)
|
||||
elif whence == 2:
|
||||
self._pos = max(0, len(self._buffer) + pos)
|
||||
with self._lock:
|
||||
self._pos = max(0, len(self._buffer) + pos)
|
||||
else:
|
||||
raise ValueError("unsupported whence value")
|
||||
return self._pos
|
||||
@@ -986,18 +1003,20 @@ class BytesIO(BufferedIOBase):
|
||||
def truncate(self, pos=None):
|
||||
if self.closed:
|
||||
raise ValueError("truncate on closed file")
|
||||
if pos is None:
|
||||
pos = self._pos
|
||||
else:
|
||||
try:
|
||||
pos_index = pos.__index__
|
||||
except AttributeError:
|
||||
raise TypeError(f"{pos!r} is not an integer")
|
||||
|
||||
with self._lock:
|
||||
if pos is None:
|
||||
pos = self._pos
|
||||
else:
|
||||
pos = pos_index()
|
||||
if pos < 0:
|
||||
raise ValueError("negative truncate position %r" % (pos,))
|
||||
del self._buffer[pos:]
|
||||
try:
|
||||
pos_index = pos.__index__
|
||||
except AttributeError:
|
||||
raise TypeError(f"{pos!r} is not an integer")
|
||||
else:
|
||||
pos = pos_index()
|
||||
if pos < 0:
|
||||
raise ValueError("negative truncate position %r" % (pos,))
|
||||
del self._buffer[pos:]
|
||||
return pos
|
||||
|
||||
def readable(self):
|
||||
@@ -1486,6 +1505,7 @@ class FileIO(RawIOBase):
|
||||
_writable = False
|
||||
_appending = False
|
||||
_seekable = None
|
||||
_truncate = False
|
||||
_closefd = True
|
||||
|
||||
def __init__(self, file, mode='r', closefd=True, opener=None):
|
||||
@@ -1546,6 +1566,7 @@ class FileIO(RawIOBase):
|
||||
flags = 0
|
||||
elif 'w' in mode:
|
||||
self._writable = True
|
||||
self._truncate = True
|
||||
flags = os.O_CREAT | os.O_TRUNC
|
||||
elif 'a' in mode:
|
||||
self._writable = True
|
||||
@@ -1727,7 +1748,7 @@ class FileIO(RawIOBase):
|
||||
assert len(result) - bytes_read >= 1, \
|
||||
"os.readinto buffer size 0 will result in erroneous EOF / returns 0"
|
||||
result.resize(bytes_read)
|
||||
return bytes(result)
|
||||
return result.take_bytes()
|
||||
|
||||
def readinto(self, buffer):
|
||||
"""Same as RawIOBase.readinto()."""
|
||||
@@ -1871,7 +1892,10 @@ class FileIO(RawIOBase):
|
||||
return 'ab'
|
||||
elif self._readable:
|
||||
if self._writable:
|
||||
return 'rb+'
|
||||
if self._truncate:
|
||||
return 'wb+'
|
||||
else:
|
||||
return 'rb+'
|
||||
else:
|
||||
return 'rb'
|
||||
else:
|
||||
|
||||
Vendored
+65
-30
@@ -30,7 +30,7 @@ READ_BUFFER_SIZE = 128 * 1024
|
||||
_WRITE_BUFFER_SIZE = 4 * io.DEFAULT_BUFFER_SIZE
|
||||
|
||||
|
||||
def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_BEST,
|
||||
def open(filename, mode="rb", compresslevel=_COMPRESS_LEVEL_TRADEOFF,
|
||||
encoding=None, errors=None, newline=None):
|
||||
"""Open a gzip-compressed file in binary or text mode.
|
||||
|
||||
@@ -158,7 +158,7 @@ class GzipFile(_streams.BaseStream):
|
||||
myfileobj = None
|
||||
|
||||
def __init__(self, filename=None, mode=None,
|
||||
compresslevel=_COMPRESS_LEVEL_BEST, fileobj=None, mtime=None):
|
||||
compresslevel=_COMPRESS_LEVEL_TRADEOFF, fileobj=None, mtime=None):
|
||||
"""Constructor for the GzipFile class.
|
||||
|
||||
At least one of fileobj and filename must be given a
|
||||
@@ -189,9 +189,10 @@ class GzipFile(_streams.BaseStream):
|
||||
|
||||
The optional mtime argument is the timestamp requested by gzip.
|
||||
The time is in Unix format, i.e., seconds since 00:00:00 UTC,
|
||||
January 1, 1970. If mtime is omitted or None, the current time
|
||||
is used. Use mtime = 0 to generate a compressed stream that does
|
||||
not depend on creation time.
|
||||
January 1, 1970. Set mtime to 0 to generate a compressed stream
|
||||
that does not depend on creation time. If mtime is omitted or None,
|
||||
the current time is used. If the resulting mtime is outside the
|
||||
range 0 to 2**32-1, then the value 0 is used instead.
|
||||
|
||||
"""
|
||||
|
||||
@@ -270,8 +271,6 @@ class GzipFile(_streams.BaseStream):
|
||||
self.name = filename
|
||||
self.crc = zlib.crc32(b"")
|
||||
self.size = 0
|
||||
self.writebuf = []
|
||||
self.bufsize = 0
|
||||
self.offset = 0 # Current file offset for seek(), tell(), etc
|
||||
|
||||
def tell(self):
|
||||
@@ -299,6 +298,8 @@ class GzipFile(_streams.BaseStream):
|
||||
mtime = self._write_mtime
|
||||
if mtime is None:
|
||||
mtime = time.time()
|
||||
if not 0 <= mtime < 2**32:
|
||||
mtime = 0
|
||||
write32u(self.fileobj, int(mtime))
|
||||
if compresslevel == _COMPRESS_LEVEL_BEST:
|
||||
xfl = b'\002'
|
||||
@@ -488,40 +489,73 @@ def _read_exact(fp, n):
|
||||
return data
|
||||
|
||||
|
||||
def _read_until_null(fp, crc=None):
|
||||
'''Read until the first encountered null byte in fp.
|
||||
If crc is not None, update and return the CRC.
|
||||
'''
|
||||
if crc is None:
|
||||
while True:
|
||||
s = fp.read(1)
|
||||
if not s or s == b'\000':
|
||||
break
|
||||
else:
|
||||
while True:
|
||||
s = fp.read(1)
|
||||
crc = zlib.crc32(s, crc)
|
||||
if not s or s == b'\000':
|
||||
break
|
||||
return crc
|
||||
|
||||
|
||||
def _read_gzip_header(fp):
|
||||
'''Read a gzip header from `fp` and progress to the end of the header.
|
||||
|
||||
Returns last mtime if header was present or None otherwise.
|
||||
'''
|
||||
magic = fp.read(2)
|
||||
if magic == b'':
|
||||
if not magic:
|
||||
return None
|
||||
|
||||
if magic != b'\037\213':
|
||||
raise BadGzipFile('Not a gzipped file (%r)' % magic)
|
||||
|
||||
(method, flag, last_mtime) = struct.unpack("<BBIxx", _read_exact(fp, 8))
|
||||
base_header = _read_exact(fp, 8)
|
||||
(method, flag, last_mtime) = struct.unpack("<BBIxx", base_header)
|
||||
if method != 8:
|
||||
raise BadGzipFile('Unknown compression method')
|
||||
|
||||
if flag & FEXTRA:
|
||||
# Read & discard the extra field, if present
|
||||
extra_len, = struct.unpack("<H", _read_exact(fp, 2))
|
||||
_read_exact(fp, extra_len)
|
||||
if flag & FNAME:
|
||||
# Most common cases are no flags (gzip.compress, zlib.compress) or only
|
||||
# FNAME set (GzipFile, gzip command line application). Exit early
|
||||
# in those cases.
|
||||
if not flag:
|
||||
return last_mtime
|
||||
if flag == FNAME:
|
||||
# Read and discard a null-terminated string containing the filename
|
||||
while True:
|
||||
s = fp.read(1)
|
||||
if not s or s==b'\000':
|
||||
break
|
||||
if flag & FCOMMENT:
|
||||
# Read and discard a null-terminated string containing a comment
|
||||
while True:
|
||||
s = fp.read(1)
|
||||
if not s or s==b'\000':
|
||||
break
|
||||
_read_until_null(fp)
|
||||
return last_mtime
|
||||
|
||||
# Processing for more complex flags. Save header parts for FHCRC checking.
|
||||
if flag & FHCRC:
|
||||
_read_exact(fp, 2) # Read & discard the 16-bit header CRC
|
||||
crc = zlib.crc32(magic + base_header)
|
||||
else:
|
||||
crc = None
|
||||
if flag & FEXTRA:
|
||||
extra_len_bytes = _read_exact(fp, 2)
|
||||
extra_len, = struct.unpack("<H", extra_len_bytes)
|
||||
extra = _read_exact(fp, extra_len)
|
||||
if crc is not None:
|
||||
crc = zlib.crc32(extra_len_bytes, crc)
|
||||
crc = zlib.crc32(extra, crc)
|
||||
if flag & FNAME:
|
||||
crc = _read_until_null(fp, crc)
|
||||
if flag & FCOMMENT:
|
||||
crc = _read_until_null(fp, crc)
|
||||
if crc is not None:
|
||||
# Header CRC is the last 16 bits of a crc32.
|
||||
header_crc, = struct.unpack("<H", _read_exact(fp, 2))
|
||||
crc = crc & 0xFFFF
|
||||
if header_crc != crc:
|
||||
raise BadGzipFile(f"Corrupted gzip header. Checksums do not "
|
||||
f"match: {crc:04x} != {header_crc:04x}")
|
||||
return last_mtime
|
||||
|
||||
|
||||
@@ -623,7 +657,7 @@ class _GzipReader(_streams.DecompressReader):
|
||||
self._new_member = True
|
||||
|
||||
|
||||
def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0):
|
||||
def compress(data, compresslevel=_COMPRESS_LEVEL_TRADEOFF, *, mtime=0):
|
||||
"""Compress data in one shot and return the compressed string.
|
||||
|
||||
compresslevel sets the compression level in range of 0-9.
|
||||
@@ -634,6 +668,8 @@ def compress(data, compresslevel=_COMPRESS_LEVEL_BEST, *, mtime=0):
|
||||
gzip_data = zlib.compress(data, level=compresslevel, wbits=31)
|
||||
if mtime is None:
|
||||
mtime = time.time()
|
||||
if not 0 <= mtime < 2**32:
|
||||
mtime = 0
|
||||
# Reuse gzip header created by zlib, replace mtime and OS byte for
|
||||
# consistency.
|
||||
header = struct.pack("<4sLBB", gzip_data, int(mtime), gzip_data[8], 255)
|
||||
@@ -668,15 +704,14 @@ def decompress(data):
|
||||
def main():
|
||||
from argparse import ArgumentParser
|
||||
parser = ArgumentParser(description=
|
||||
"A simple command line interface for the gzip module: act like gzip, "
|
||||
"A simple command line interface for the `gzip` module: act like `gzip`, "
|
||||
"but do not delete the input file.",
|
||||
color=True,
|
||||
)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--fast', action='store_true', help='compress faster')
|
||||
group.add_argument('--best', action='store_true', help='compress better')
|
||||
group.add_argument("-d", "--decompress", action="store_true",
|
||||
help="act like gunzip instead of gzip")
|
||||
help="act like `gunzip` instead of `gzip`")
|
||||
|
||||
parser.add_argument("args", nargs="*", default=["-"], metavar='file')
|
||||
args = parser.parse_args()
|
||||
|
||||
Vendored
+20
-10
@@ -745,6 +745,7 @@ def _rmtree_safe_fd_step(stack, onexc):
|
||||
# save a call to os.lstat() when walking subdirectories.
|
||||
func, dirfd, path, orig_entry = stack.pop()
|
||||
name = path if orig_entry is None else orig_entry.name
|
||||
parent_fd = None if func is os.close else dirfd
|
||||
try:
|
||||
if func is os.close:
|
||||
os.close(dirfd)
|
||||
@@ -792,14 +793,14 @@ def _rmtree_safe_fd_step(stack, onexc):
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
except OSError as err:
|
||||
onexc(os.unlink, fullname, err)
|
||||
onexc(os.unlink, fullname, err, direntry=entry, dir_fd=topfd)
|
||||
except FileNotFoundError as err:
|
||||
if orig_entry is None or func is os.close:
|
||||
err.filename = path
|
||||
onexc(func, path, err)
|
||||
onexc(func, path, err, direntry=orig_entry, dir_fd=parent_fd)
|
||||
except OSError as err:
|
||||
err.filename = path
|
||||
onexc(func, path, err)
|
||||
onexc(func, path, err, direntry=orig_entry, dir_fd=parent_fd)
|
||||
|
||||
_use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <=
|
||||
os.supports_dir_fd and
|
||||
@@ -807,7 +808,8 @@ _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <=
|
||||
os.stat in os.supports_follow_symlinks)
|
||||
_rmtree_impl = _rmtree_safe_fd if _use_fd_functions else _rmtree_unsafe
|
||||
|
||||
def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||
def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None,
|
||||
_onexc_kwargs=False):
|
||||
"""Recursively delete a directory tree.
|
||||
|
||||
If dir_fd is not None, it should be a file descriptor open to a directory;
|
||||
@@ -830,24 +832,29 @@ def rmtree(path, ignore_errors=False, onerror=None, *, onexc=None, dir_fd=None):
|
||||
|
||||
sys.audit("shutil.rmtree", path, dir_fd)
|
||||
if ignore_errors:
|
||||
def onexc(*args):
|
||||
def onexc(*args, **kwargs):
|
||||
pass
|
||||
elif onerror is None and onexc is None:
|
||||
def onexc(*args):
|
||||
def onexc(*args, **kwargs):
|
||||
raise
|
||||
elif onexc is None:
|
||||
if onerror is None:
|
||||
def onexc(*args):
|
||||
def onexc(*args, **kwargs):
|
||||
raise
|
||||
else:
|
||||
# delegate to onerror
|
||||
def onexc(*args):
|
||||
def onexc(*args, **kwargs):
|
||||
func, path, exc = args
|
||||
if exc is None:
|
||||
exc_info = None, None, None
|
||||
else:
|
||||
exc_info = type(exc), exc, exc.__traceback__
|
||||
return onerror(func, path, exc_info)
|
||||
elif not _onexc_kwargs:
|
||||
# Only the internal caller in tempfile asks for the extra arguments.
|
||||
_onexc = onexc
|
||||
def onexc(func, path, err, **kwargs):
|
||||
return _onexc(func, path, err)
|
||||
|
||||
_rmtree_impl(path, dir_fd, onexc)
|
||||
|
||||
@@ -1216,19 +1223,22 @@ def make_archive(base_name, format, root_dir=None, base_dir=None, verbose=0,
|
||||
for arg, val in format_info[1]:
|
||||
kwargs[arg] = val
|
||||
|
||||
base_name = os.fspath(base_name)
|
||||
|
||||
if base_dir is None:
|
||||
base_dir = os.curdir
|
||||
else:
|
||||
base_dir = os.fspath(base_dir)
|
||||
|
||||
supports_root_dir = getattr(func, 'supports_root_dir', False)
|
||||
save_cwd = None
|
||||
if root_dir is not None:
|
||||
root_dir = os.fspath(root_dir)
|
||||
stmd = os.stat(root_dir).st_mode
|
||||
if not stat.S_ISDIR(stmd):
|
||||
raise NotADirectoryError(errno.ENOTDIR, 'Not a directory', root_dir)
|
||||
|
||||
if supports_root_dir:
|
||||
# Support path-like base_name here for backwards-compatibility.
|
||||
base_name = os.fspath(base_name)
|
||||
kwargs['root_dir'] = root_dir
|
||||
else:
|
||||
save_cwd = os.getcwd()
|
||||
|
||||
Vendored
+80
-39
@@ -28,7 +28,6 @@
|
||||
"""Read from and write to tar format archives.
|
||||
"""
|
||||
|
||||
version = "0.9.0"
|
||||
__author__ = "Lars Gust\u00e4bel (lars@gustaebel.de)"
|
||||
__credits__ = "Gustavo Niemeyer, Niels Gust\u00e4bel, Richard Townsend."
|
||||
|
||||
@@ -201,7 +200,6 @@ def itn(n, digits=8, format=DEFAULT_FORMAT):
|
||||
# base-256 representation. This allows values up to (256**(digits-1))-1.
|
||||
# A 0o200 byte indicates a positive number, a 0o377 byte a negative
|
||||
# number.
|
||||
original_n = n
|
||||
n = int(n)
|
||||
if 0 <= n < 8 ** (digits - 1):
|
||||
s = bytes("%0*o" % (digits - 1, n), "ascii") + NUL
|
||||
@@ -365,7 +363,7 @@ class _Stream:
|
||||
"""
|
||||
|
||||
def __init__(self, name, mode, comptype, fileobj, bufsize,
|
||||
compresslevel, preset):
|
||||
compresslevel, preset, mtime):
|
||||
"""Construct a _Stream object.
|
||||
"""
|
||||
self._extfileobj = True
|
||||
@@ -400,7 +398,7 @@ class _Stream:
|
||||
self.exception = zlib.error
|
||||
self._init_read_gz()
|
||||
else:
|
||||
self._init_write_gz(compresslevel)
|
||||
self._init_write_gz(compresslevel, mtime)
|
||||
|
||||
elif comptype == "bz2":
|
||||
try:
|
||||
@@ -408,7 +406,6 @@ class _Stream:
|
||||
except ImportError:
|
||||
raise CompressionError("bz2 module is not available") from None
|
||||
if mode == "r":
|
||||
self.dbuf = b""
|
||||
self.cmp = bz2.BZ2Decompressor()
|
||||
self.exception = OSError
|
||||
else:
|
||||
@@ -420,7 +417,6 @@ class _Stream:
|
||||
except ImportError:
|
||||
raise CompressionError("lzma module is not available") from None
|
||||
if mode == "r":
|
||||
self.dbuf = b""
|
||||
self.cmp = lzma.LZMADecompressor()
|
||||
self.exception = lzma.LZMAError
|
||||
else:
|
||||
@@ -431,7 +427,6 @@ class _Stream:
|
||||
except ImportError:
|
||||
raise CompressionError("compression.zstd module is not available") from None
|
||||
if mode == "r":
|
||||
self.dbuf = b""
|
||||
self.cmp = zstd.ZstdDecompressor()
|
||||
self.exception = zstd.ZstdError
|
||||
else:
|
||||
@@ -449,7 +444,7 @@ class _Stream:
|
||||
if hasattr(self, "closed") and not self.closed:
|
||||
self.close()
|
||||
|
||||
def _init_write_gz(self, compresslevel):
|
||||
def _init_write_gz(self, compresslevel, mtime):
|
||||
"""Initialize for writing with gzip compression.
|
||||
"""
|
||||
self.cmp = self.zlib.compressobj(compresslevel,
|
||||
@@ -457,7 +452,9 @@ class _Stream:
|
||||
-self.zlib.MAX_WBITS,
|
||||
self.zlib.DEF_MEM_LEVEL,
|
||||
0)
|
||||
timestamp = struct.pack("<L", int(time.time()))
|
||||
if mtime is None:
|
||||
mtime = int(time.time())
|
||||
timestamp = struct.pack("<L", mtime)
|
||||
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
|
||||
if self.name.endswith(".gz"):
|
||||
self.name = self.name[:-3]
|
||||
@@ -511,7 +508,6 @@ class _Stream:
|
||||
"""Initialize for reading a gzip compressed fileobj.
|
||||
"""
|
||||
self.cmp = self.zlib.decompressobj(-self.zlib.MAX_WBITS)
|
||||
self.dbuf = b""
|
||||
|
||||
# taken from gzip.GzipFile with some alterations
|
||||
if self.__read(2) != b"\037\213":
|
||||
@@ -571,26 +567,44 @@ class _Stream:
|
||||
if self.comptype == "tar":
|
||||
return self.__read(size)
|
||||
|
||||
c = len(self.dbuf)
|
||||
t = [self.dbuf]
|
||||
c = 0
|
||||
t = []
|
||||
while c < size:
|
||||
# Skip underlying buffer to avoid unaligned double buffering.
|
||||
if self.buf:
|
||||
buf = self.buf
|
||||
self.buf = b""
|
||||
if self.comptype == "gz":
|
||||
# zlib interface is different than others.
|
||||
# It returns data in unconsumed_tail.
|
||||
if self.buf:
|
||||
cbuf = self.buf
|
||||
self.buf = b""
|
||||
else:
|
||||
cbuf = self.fileobj.read(self.bufsize)
|
||||
if not cbuf:
|
||||
break
|
||||
|
||||
try:
|
||||
dbuf = self.cmp.decompress(cbuf, size - c)
|
||||
self.buf = self.cmp.unconsumed_tail
|
||||
except self.exception as e:
|
||||
raise ReadError("invalid compressed data") from e
|
||||
else:
|
||||
buf = self.fileobj.read(self.bufsize)
|
||||
if not buf:
|
||||
break
|
||||
try:
|
||||
buf = self.cmp.decompress(buf)
|
||||
except self.exception as e:
|
||||
raise ReadError("invalid compressed data") from e
|
||||
t.append(buf)
|
||||
c += len(buf)
|
||||
t = b"".join(t)
|
||||
self.dbuf = t[size:]
|
||||
return t[:size]
|
||||
# Other decompressors have needs_input.
|
||||
# decompress() can buffer data internally.
|
||||
if self.cmp.needs_input:
|
||||
cbuf = self.fileobj.read(self.bufsize)
|
||||
if not cbuf:
|
||||
break
|
||||
else:
|
||||
cbuf = b""
|
||||
|
||||
try:
|
||||
dbuf = self.cmp.decompress(cbuf, size - c)
|
||||
except self.exception as e:
|
||||
raise ReadError("invalid compressed data") from e
|
||||
|
||||
t.append(dbuf)
|
||||
c += len(dbuf)
|
||||
|
||||
return b"".join(t)
|
||||
|
||||
def __read(self, size):
|
||||
"""Return size bytes from stream. If internal buffer is empty,
|
||||
@@ -819,6 +833,13 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||
# For example, 'C:/foo' on Windows.
|
||||
raise AbsolutePathError(member)
|
||||
# Ensure we stay in the destination
|
||||
if '..' in name.replace(os.sep, '/').split('/'):
|
||||
# Directories are created from the name as given, so a name that
|
||||
# leaves the destination part-way through would create them
|
||||
# outside it even if the resolved path stays inside.
|
||||
normalized = os.path.normpath(name)
|
||||
if normalized != name:
|
||||
name = new_attrs['name'] = normalized
|
||||
target_path = os.path.realpath(os.path.join(dest_path, name),
|
||||
strict=os.path.ALLOW_MISSING)
|
||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||
@@ -895,11 +916,11 @@ def data_filter(member, dest_path):
|
||||
return member.replace(**new_attrs, deep=False)
|
||||
return member
|
||||
|
||||
_NAMED_FILTERS = {
|
||||
_NAMED_FILTERS = frozendict({
|
||||
"fully_trusted": fully_trusted_filter,
|
||||
"tar": tar_filter,
|
||||
"data": data_filter,
|
||||
}
|
||||
})
|
||||
|
||||
#------------------
|
||||
# Exported Classes
|
||||
@@ -1785,7 +1806,7 @@ class TarFile(object):
|
||||
def __init__(self, name=None, mode="r", fileobj=None, format=None,
|
||||
tarinfo=None, dereference=None, ignore_zeros=None, encoding=None,
|
||||
errors="surrogateescape", pax_headers=None, debug=None,
|
||||
errorlevel=None, copybufsize=None, stream=False):
|
||||
errorlevel=None, copybufsize=None, stream=False, mtime=None):
|
||||
"""Open an (uncompressed) tar archive 'name'. 'mode' is either 'r' to
|
||||
read from an existing archive, 'a' to append data to an existing
|
||||
file or 'w' to create a new file overwriting an existing one. 'mode'
|
||||
@@ -1989,10 +2010,11 @@ class TarFile(object):
|
||||
if "preset" in kwargs and comptype not in ("xz",):
|
||||
raise ValueError("preset is only valid for w|xz mode")
|
||||
|
||||
compresslevel = kwargs.pop("compresslevel", 9)
|
||||
compresslevel = kwargs.pop("compresslevel", 6)
|
||||
preset = kwargs.pop("preset", None)
|
||||
mtime = kwargs.pop("mtime", None)
|
||||
stream = _Stream(name, filemode, comptype, fileobj, bufsize,
|
||||
compresslevel, preset)
|
||||
compresslevel, preset, mtime)
|
||||
try:
|
||||
t = cls(name, filemode, stream, **kwargs)
|
||||
except:
|
||||
@@ -2015,7 +2037,7 @@ class TarFile(object):
|
||||
return cls(name, mode, fileobj, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def gzopen(cls, name, mode="r", fileobj=None, compresslevel=9, **kwargs):
|
||||
def gzopen(cls, name, mode="r", fileobj=None, compresslevel=6, **kwargs):
|
||||
"""Open gzip compressed tar archive name for reading or writing.
|
||||
Appending is not allowed.
|
||||
"""
|
||||
@@ -2028,7 +2050,8 @@ class TarFile(object):
|
||||
raise CompressionError("gzip module is not available") from None
|
||||
|
||||
try:
|
||||
fileobj = GzipFile(name, mode + "b", compresslevel, fileobj)
|
||||
mtime = kwargs.pop("mtime", None)
|
||||
fileobj = GzipFile(name, mode + "b", compresslevel, fileobj, mtime=mtime)
|
||||
except OSError as e:
|
||||
if fileobj is not None and mode == 'r':
|
||||
raise ReadError("not a gzip file") from e
|
||||
@@ -2259,7 +2282,7 @@ class TarFile(object):
|
||||
type = FIFOTYPE
|
||||
elif stat.S_ISLNK(stmd):
|
||||
type = SYMTYPE
|
||||
linkname = os.readlink(name)
|
||||
linkname = os.readlink(name).replace(os.sep, "/")
|
||||
elif stat.S_ISCHR(stmd):
|
||||
type = CHRTYPE
|
||||
elif stat.S_ISBLK(stmd):
|
||||
@@ -2782,7 +2805,13 @@ class TarFile(object):
|
||||
if os.path.lexists(targetpath):
|
||||
# Avoid FileExistsError on following os.symlink.
|
||||
os.unlink(targetpath)
|
||||
os.symlink(tarinfo.linkname, targetpath)
|
||||
link_target = tarinfo.linkname
|
||||
if os.name == "nt":
|
||||
# gh-57911: Posix-flavoured forward-slash path separators in
|
||||
# symlink targets aren't acknowledged by Windows, resulting
|
||||
# in corrupted links.
|
||||
link_target = link_target.replace("/", os.path.sep)
|
||||
os.symlink(link_target, targetpath)
|
||||
return
|
||||
else:
|
||||
if os.path.exists(tarinfo._link_target):
|
||||
@@ -2811,9 +2840,11 @@ class TarFile(object):
|
||||
"makelink_with_filter: if filter_function is not None, "
|
||||
+ "extraction_root must also not be None")
|
||||
try:
|
||||
filter_function(
|
||||
filtered = filter_function(
|
||||
unfiltered.replace(name=tarinfo.name, deep=False),
|
||||
extraction_root)
|
||||
if filtered is None:
|
||||
return
|
||||
filtered = filter_function(unfiltered, extraction_root)
|
||||
except _FILTER_ERRORS as cause:
|
||||
raise LinkFallbackError(tarinfo, unfiltered.name) from cause
|
||||
@@ -3104,7 +3135,7 @@ def main():
|
||||
import argparse
|
||||
|
||||
description = 'A simple command-line interface for tarfile module.'
|
||||
parser = argparse.ArgumentParser(description=description, color=True)
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||
help='Verbose output')
|
||||
parser.add_argument('--filter', metavar='<filtername>',
|
||||
@@ -3198,5 +3229,15 @@ def main():
|
||||
if args.verbose:
|
||||
print('{!r} file created.'.format(tar_name))
|
||||
|
||||
|
||||
def __getattr__(name):
|
||||
if name == "version":
|
||||
from warnings import _deprecated
|
||||
|
||||
_deprecated("version", remove=(3, 20))
|
||||
return "0.9.0" # Do not change
|
||||
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Vendored
+2
-2
@@ -568,8 +568,8 @@ class OtherFileTests:
|
||||
# test that the mode attribute is correct for various mode strings
|
||||
# given as init args
|
||||
try:
|
||||
for modes in [('w', 'wb'), ('wb', 'wb'), ('wb+', 'rb+'),
|
||||
('w+b', 'rb+'), ('a', 'ab'), ('ab', 'ab'),
|
||||
for modes in [('w', 'wb'), ('wb', 'wb'), ('wb+', 'wb+'),
|
||||
('w+b', 'wb+'), ('a', 'ab'), ('ab', 'ab'),
|
||||
('ab+', 'ab+'), ('a+b', 'ab+'), ('r', 'rb'),
|
||||
('rb', 'rb'), ('rb+', 'rb+'), ('r+b', 'rb+')]:
|
||||
# read modes are last so that TESTFN will exist first
|
||||
|
||||
Vendored
+54
-4
@@ -9,10 +9,10 @@ import os
|
||||
import struct
|
||||
import sys
|
||||
import unittest
|
||||
import warnings
|
||||
from subprocess import PIPE, Popen
|
||||
from unittest import mock
|
||||
from test.support import catch_unraisable_exception
|
||||
from test.support import import_helper
|
||||
from test.support import force_not_colorized_test_class, import_helper
|
||||
from test.support import os_helper
|
||||
from test.support import _4G, bigmemtest, requires_subprocess
|
||||
from test.support.script_helper import assert_python_ok, assert_python_failure
|
||||
@@ -351,10 +351,30 @@ class TestGzip(BaseTest):
|
||||
self.assertEqual(dataRead, data1)
|
||||
self.assertEqual(fRead.mtime, mtime)
|
||||
|
||||
def test_mtime_out_of_range(self):
|
||||
for mtime in (-1, 2**32):
|
||||
with gzip.GzipFile(self.filename, 'w', mtime=mtime) as fWrite:
|
||||
fWrite.write(data1)
|
||||
with gzip.GzipFile(self.filename) as fRead:
|
||||
fRead.read(1)
|
||||
self.assertEqual(fRead.mtime, 0)
|
||||
datac = gzip.compress(data1, mtime=mtime)
|
||||
with gzip.GzipFile(fileobj=io.BytesIO(datac)) as fRead:
|
||||
fRead.read(1)
|
||||
self.assertEqual(fRead.mtime, 0)
|
||||
|
||||
for mtime in (-1, 2**32):
|
||||
with mock.patch('time.time', return_value=float(mtime)):
|
||||
with gzip.GzipFile(self.filename, 'w') as fWrite:
|
||||
fWrite.write(data1)
|
||||
with gzip.GzipFile(self.filename) as fRead:
|
||||
fRead.read(1)
|
||||
self.assertEqual(fRead.mtime, 0)
|
||||
|
||||
def test_metadata(self):
|
||||
mtime = 123456789
|
||||
|
||||
with gzip.GzipFile(self.filename, 'w', mtime = mtime) as fWrite:
|
||||
with gzip.GzipFile(self.filename, 'w', mtime = mtime, compresslevel = 9) as fWrite:
|
||||
fWrite.write(data1)
|
||||
|
||||
with open(self.filename, 'rb') as fRead:
|
||||
@@ -640,7 +660,7 @@ class TestGzip(BaseTest):
|
||||
with open(self.filename, mode) as f:
|
||||
with gzip.GzipFile(fileobj=f) as g:
|
||||
self.assertEqual(g.mode, gzip.READ)
|
||||
for mode in "wb", "ab", "xb":
|
||||
for mode in "wb", "ab", "xb", "wb+", "ab+", "xb+":
|
||||
if "x" in mode:
|
||||
os_helper.unlink(self.filename)
|
||||
with open(self.filename, mode) as f:
|
||||
@@ -796,6 +816,35 @@ class TestGzip(BaseTest):
|
||||
compressed_data = gzip.compress(data1)
|
||||
self.assertRaises(EOFError, gzip.decompress, compressed_data[:-8])
|
||||
|
||||
def test_truncated_header(self):
|
||||
truncated_headers = [
|
||||
b"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00", # Missing OS byte
|
||||
b"\x1f\x8b\x08\x02\x00\x00\x00\x00\x00\xff", # FHRC, but no checksum
|
||||
b"\x1f\x8b\x08\x04\x00\x00\x00\x00\x00\xff", # FEXTRA, but no xlen
|
||||
b"\x1f\x8b\x08\x04\x00\x00\x00\x00\x00\xff\xaa\x00", # FEXTRA, xlen, but no data
|
||||
b"\x1f\x8b\x08\x08\x00\x00\x00\x00\x00\xff", # FNAME but no fname
|
||||
b"\x1f\x8b\x08\x10\x00\x00\x00\x00\x00\xff", # FCOMMENT, but no fcomment
|
||||
]
|
||||
for header in truncated_headers:
|
||||
with self.subTest(header=header):
|
||||
with self.assertRaises(EOFError):
|
||||
gzip.decompress(header)
|
||||
|
||||
def test_corrupted_gzip_header(self):
|
||||
header = (b"\x1f\x8b\x08\x1f\x00\x00\x00\x00\x00\xff" # All flags set
|
||||
b"\x05\x00" # Xlen = 5
|
||||
b"extra"
|
||||
b"name\x00"
|
||||
b"comment\x00")
|
||||
true_crc = zlib.crc32(header) & 0xFFFF
|
||||
corrupted_crc = true_crc ^ 0xFFFF
|
||||
corrupted_header = header + corrupted_crc.to_bytes(2, "little")
|
||||
with self.assertRaises(gzip.BadGzipFile) as err:
|
||||
gzip.decompress(corrupted_header)
|
||||
self.assertEqual(str(err.exception),
|
||||
f"Corrupted gzip header. Checksums do not "
|
||||
f"match: {true_crc:04x} != {corrupted_crc:04x}")
|
||||
|
||||
def test_read_truncated(self):
|
||||
data = data1*50
|
||||
# Drop the CRC (4 bytes) and file size (4 bytes).
|
||||
@@ -1058,6 +1107,7 @@ def create_and_remove_directory(directory):
|
||||
return decorator
|
||||
|
||||
|
||||
@force_not_colorized_test_class
|
||||
class TestCommandLine(unittest.TestCase):
|
||||
data = b'This is a simple test with gzip'
|
||||
|
||||
|
||||
Vendored
+2
-2
@@ -4385,8 +4385,8 @@ class MiscIOTest(unittest.TestCase):
|
||||
|
||||
f = self.open(os_helper.TESTFN, "w+", encoding="utf-8")
|
||||
self.assertEqual(f.mode, "w+")
|
||||
self.assertEqual(f.buffer.mode, "rb+") # Does it really matter?
|
||||
self.assertEqual(f.buffer.raw.mode, "rb+")
|
||||
self.assertEqual(f.buffer.mode, "wb+")
|
||||
self.assertEqual(f.buffer.raw.mode, "wb+")
|
||||
|
||||
g = self.open(f.fileno(), "wb", closefd=False)
|
||||
self.assertEqual(g.mode, "wb")
|
||||
|
||||
Vendored
+58
-1
@@ -493,6 +493,37 @@ class TestRmTree(BaseTest, unittest.TestCase):
|
||||
self.assertTrue(isinstance(exc, OSError))
|
||||
self.errorState = 3
|
||||
|
||||
@os_helper.skip_if_dac_override
|
||||
@os_helper.skip_unless_working_chmod
|
||||
@unittest.skipUnless(shutil.rmtree.avoids_symlink_attacks,
|
||||
'requires the fd based implementation of rmtree()')
|
||||
def test_on_exc_kwargs(self):
|
||||
os.mkdir(TESTFN)
|
||||
self.addCleanup(shutil.rmtree, TESTFN)
|
||||
|
||||
child_dir_path = os.path.join(TESTFN, 'b')
|
||||
child_file_path = os.path.join(child_dir_path, 'a')
|
||||
os.mkdir(child_dir_path)
|
||||
os_helper.create_empty_file(child_file_path)
|
||||
old_child_dir_mode = os.stat(child_dir_path).st_mode
|
||||
# Make unwritable.
|
||||
new_mode = stat.S_IREAD|stat.S_IEXEC
|
||||
os.chmod(child_dir_path, new_mode)
|
||||
|
||||
self.addCleanup(os.chmod, child_dir_path, old_child_dir_mode)
|
||||
|
||||
calls = []
|
||||
def onexc(func, path, err, direntry=None, dir_fd=None):
|
||||
calls.append((func, path, err))
|
||||
if func is os.unlink:
|
||||
self.assertEqual(direntry.name, os.path.basename(path))
|
||||
self.assertTrue(os.path.samestat(
|
||||
os.stat(path), os.stat(direntry.name, dir_fd=dir_fd)))
|
||||
|
||||
shutil.rmtree(TESTFN, onexc=onexc, _onexc_kwargs=True)
|
||||
self.assertIn((os.unlink, child_file_path),
|
||||
[(func, path) for func, path, err in calls])
|
||||
|
||||
@unittest.skipIf(sys.platform[:6] == 'cygwin',
|
||||
"This test can't be run on Cygwin (issue #1071513).")
|
||||
@os_helper.skip_if_dac_override
|
||||
@@ -2042,6 +2073,32 @@ class TestArchives(BaseTest, unittest.TestCase):
|
||||
self.assertEqual(make_archive('test', 'zip'), 'test.zip')
|
||||
self.assertTrue(os.path.isfile('test.zip'))
|
||||
|
||||
def test_make_archive_pathlike_cwd_default(self):
|
||||
called_args = []
|
||||
def archiver(base_name, base_dir, **kw):
|
||||
called_args.append((base_name, kw.get('root_dir')))
|
||||
|
||||
register_archive_format('xxx', archiver, [], 'xxx file')
|
||||
self.addCleanup(unregister_archive_format, 'xxx')
|
||||
with no_chdir:
|
||||
make_archive(FakePath('basename'), 'xxx')
|
||||
self.assertEqual(called_args, [('basename', None)])
|
||||
|
||||
def test_make_archive_pathlike_cwd_supports_root_dir(self):
|
||||
root_dir = self.mkdtemp()
|
||||
called_args = []
|
||||
def archiver(base_name, base_dir, **kw):
|
||||
called_args.append((base_name, base_dir, kw.get('root_dir')))
|
||||
archiver.supports_root_dir = True
|
||||
|
||||
register_archive_format('xxx', archiver, [], 'xxx file')
|
||||
self.addCleanup(unregister_archive_format, 'xxx')
|
||||
with no_chdir:
|
||||
make_archive(FakePath('basename'), 'xxx',
|
||||
root_dir=FakePath(root_dir),
|
||||
base_dir=FakePath('basedir'))
|
||||
self.assertEqual(called_args, [('basename', 'basedir', root_dir)])
|
||||
|
||||
def test_register_archive_format(self):
|
||||
|
||||
self.assertRaises(TypeError, register_archive_format, 'xxx', 1)
|
||||
@@ -3572,7 +3629,7 @@ class PublicAPITests(unittest.TestCase):
|
||||
target_api.append('disk_usage')
|
||||
self.assertEqual(set(shutil.__all__), set(target_api))
|
||||
with self.assertWarns(DeprecationWarning):
|
||||
from shutil import ExecError
|
||||
from shutil import ExecError # noqa: F401
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Vendored
+119
-14
@@ -10,6 +10,7 @@ import shutil
|
||||
import re
|
||||
import warnings
|
||||
import stat
|
||||
import time
|
||||
|
||||
import unittest
|
||||
import unittest.mock
|
||||
@@ -1646,6 +1647,22 @@ class WriteTest(WriteTestBase, unittest.TestCase):
|
||||
finally:
|
||||
os_helper.unlink(path)
|
||||
|
||||
@os_helper.skip_unless_symlink
|
||||
def test_symlink_target_normalization(self):
|
||||
# Test for gh-151669.
|
||||
path = os.path.join(TEMPDIR, "symlink")
|
||||
target = "subdir/link/target"
|
||||
os.symlink(target.replace("/", os.sep), path)
|
||||
try:
|
||||
tar = tarfile.open(tmpname, self.mode)
|
||||
try:
|
||||
tarinfo = tar.gettarinfo(path)
|
||||
self.assertEqual(tarinfo.linkname, target)
|
||||
finally:
|
||||
tar.close()
|
||||
finally:
|
||||
os_helper.unlink(path)
|
||||
|
||||
def test_add_self(self):
|
||||
# Test for #1257255.
|
||||
dstname = os.path.abspath(tmpname)
|
||||
@@ -1904,6 +1921,19 @@ class GzipStreamWriteTest(GzipTest, StreamWriteTest):
|
||||
payload = pathlib.Path(tmpname).read_text(encoding='latin-1')
|
||||
assert os.path.dirname(tmpname) not in payload
|
||||
|
||||
def test_create_with_mtime(self):
|
||||
tarfile.open(tmpname, self.mode, mtime=0).close()
|
||||
with self.open(tmpname, 'r') as fobj:
|
||||
fobj.read()
|
||||
self.assertEqual(fobj.mtime, 0)
|
||||
|
||||
def test_create_without_mtime(self):
|
||||
before = int(time.time())
|
||||
tarfile.open(tmpname, self.mode).close()
|
||||
after = int(time.time())
|
||||
with self.open(tmpname, 'r') as fobj:
|
||||
fobj.read()
|
||||
self.assertTrue(before <= fobj.mtime <= after)
|
||||
|
||||
class Bz2StreamWriteTest(Bz2Test, StreamWriteTest):
|
||||
decompressor = bz2.BZ2Decompressor if bz2 else None
|
||||
@@ -2210,6 +2240,19 @@ class GzipCreateTest(GzipTest, CreateTest):
|
||||
with tarfile.open(tmpname, 'r:gz', compresslevel=1) as tobj:
|
||||
pass
|
||||
|
||||
def test_create_with_mtime(self):
|
||||
tarfile.open(tmpname, self.mode, mtime=0).close()
|
||||
with self.open(tmpname, 'rb') as fobj:
|
||||
fobj.read()
|
||||
self.assertEqual(fobj.mtime, 0)
|
||||
|
||||
def test_create_without_mtime(self):
|
||||
before = int(time.time())
|
||||
tarfile.open(tmpname, self.mode).close()
|
||||
after = int(time.time())
|
||||
with self.open(tmpname, 'r') as fobj:
|
||||
fobj.read()
|
||||
self.assertTrue(before <= fobj.mtime <= after)
|
||||
|
||||
class Bz2CreateTest(Bz2Test, CreateTest):
|
||||
|
||||
@@ -2872,7 +2915,7 @@ class MiscTest(unittest.TestCase):
|
||||
str(excinfo.exception),
|
||||
)
|
||||
|
||||
@unittest.skipUnless(os_helper.can_symlink(), 'requires symlink support')
|
||||
@os_helper.skip_unless_symlink
|
||||
@unittest.skipUnless(hasattr(os, 'chmod'), "missing os.chmod")
|
||||
@unittest.mock.patch('os.chmod')
|
||||
def test_deferred_directory_attributes_update(self, mock_chmod):
|
||||
@@ -3925,6 +3968,20 @@ class TestExtractionFilters(unittest.TestCase):
|
||||
tarfile.AbsolutePathError,
|
||||
"""['"].*escaped.evil['"] has an absolute path""")
|
||||
|
||||
def test_parent_dir_out_and_back(self):
|
||||
# Test a member that leaves the destination and comes back.
|
||||
# The containment check looks at the resolved path, which stays
|
||||
# inside, but the intermediate directories are created from the
|
||||
# name as given, which does not.
|
||||
with ArchiveMaker() as arc:
|
||||
arc.add(f'../escaped.evil/../{self.destdir.name}/sub/file',
|
||||
content='content')
|
||||
|
||||
for filter in 'tar', 'data':
|
||||
with self.subTest(filter):
|
||||
with self.check_context(arc.open(), filter):
|
||||
self.expect_file('sub/file', content='content')
|
||||
|
||||
@symlink_test
|
||||
def test_parent_symlink(self):
|
||||
# Test interplaying symlinks
|
||||
@@ -3937,23 +3994,21 @@ class TestExtractionFilters(unittest.TestCase):
|
||||
arc.add('current', symlink_to='.')
|
||||
|
||||
# effectively points to ./../
|
||||
arc.add('parent', symlink_to='current/..')
|
||||
if self.dotdot_resolves_early and os_helper.can_symlink():
|
||||
arc.add('parent', symlink_to='current/../..')
|
||||
else:
|
||||
arc.add('parent', symlink_to='current/..')
|
||||
|
||||
arc.add('parent/evil')
|
||||
|
||||
if os_helper.can_symlink():
|
||||
with self.check_context(arc.open(), 'fully_trusted'):
|
||||
if self.raised_exception is not None:
|
||||
# Windows will refuse to create a file that's a symlink to itself
|
||||
# (and tarfile doesn't swallow that exception)
|
||||
self.expect_exception(FileExistsError)
|
||||
# The other cases will fail with this error too.
|
||||
# Skip the rest of this test.
|
||||
return
|
||||
self.expect_file('current', symlink_to='.')
|
||||
if self.dotdot_resolves_early:
|
||||
self.expect_file('parent', symlink_to='current/../..')
|
||||
else:
|
||||
self.expect_file('current', symlink_to='.')
|
||||
self.expect_file('parent', symlink_to='current/..')
|
||||
self.expect_file('../evil')
|
||||
self.expect_file('../evil')
|
||||
|
||||
with self.check_context(arc.open(), 'tar'):
|
||||
self.expect_exception(
|
||||
@@ -4147,6 +4202,21 @@ class TestExtractionFilters(unittest.TestCase):
|
||||
tarfile.AbsoluteLinkError,
|
||||
"'parent' is a link to an absolute path")
|
||||
|
||||
@symlink_test
|
||||
@os_helper.skip_unless_symlink
|
||||
def test_symlink_target_seperator_rewrite_on_windows(self):
|
||||
with ArchiveMaker() as arc:
|
||||
arc.add('link', symlink_to="relative/test/path")
|
||||
|
||||
with self.check_context(arc.open(), 'fully_trusted'):
|
||||
self.expect_file('link', type=tarfile.SYMTYPE)
|
||||
link_path = os.path.normpath(self.destdir / "link")
|
||||
link_target = os.readlink(link_path)
|
||||
if os.name == "nt":
|
||||
self.assertEqual(link_target, "relative\\test\\path")
|
||||
else:
|
||||
self.assertEqual(link_target, "relative/test/path")
|
||||
|
||||
def test_absolute_hardlink(self):
|
||||
# Test hardlink to an absolute path
|
||||
# Inspired by 'dirsymlink' in https://github.com/jwilk/traversal-archives
|
||||
@@ -4381,9 +4451,15 @@ class TestExtractionFilters(unittest.TestCase):
|
||||
for filter in 'tar', 'fully_trusted':
|
||||
with self.subTest(filter), self.check_context(arc.open(), filter):
|
||||
if not os_helper.can_symlink():
|
||||
self.expect_file("a/t/dummy")
|
||||
self.expect_file("b/")
|
||||
self.expect_file("c/")
|
||||
if filter == 'fully_trusted' or sys.platform == "win32":
|
||||
self.expect_file("a/t/dummy")
|
||||
self.expect_file("b/")
|
||||
self.expect_file("c/")
|
||||
else:
|
||||
self.expect_exception(
|
||||
tarfile.LinkFallbackError,
|
||||
"link 'boom' would be extracted as a copy of "
|
||||
+ "'c/escape', which was rejected")
|
||||
else:
|
||||
self.expect_file("a/t/dummy")
|
||||
self.expect_file("b/")
|
||||
@@ -4562,6 +4638,25 @@ class TestExtractionFilters(unittest.TestCase):
|
||||
if os_helper.can_chmod():
|
||||
self.assertFalse(path.stat().st_mode & stat.S_IWUSR)
|
||||
|
||||
@symlink_test
|
||||
def test_extract_filters_target_none(self):
|
||||
# Test that when extract() falls back to extracting (rather than
|
||||
# linking) a hardlink target, the member is skipped if the filter
|
||||
# returns None.
|
||||
with ArchiveMaker() as arc:
|
||||
arc.add('a/b/s', symlink_to='../escape')
|
||||
arc.add('q', hardlink_to='a/b/s')
|
||||
def filter_unsafe_members(member, path):
|
||||
try:
|
||||
return tarfile.data_filter(member, path)
|
||||
except tarfile.FilterError as error:
|
||||
return None
|
||||
with self.check_context(arc.open(), filter_unsafe_members):
|
||||
if os_helper.can_symlink():
|
||||
self.expect_file('a/b/s', symlink_to='../escape')
|
||||
else:
|
||||
self.expect_file('a/b/') # symlink is not extracted
|
||||
|
||||
def test_link_fallback_normalizes(self):
|
||||
# Make sure hardlink fallbacks work for non-normalized paths for all
|
||||
# filters
|
||||
@@ -5129,6 +5224,16 @@ class OffsetValidationTests(unittest.TestCase):
|
||||
self.assertEqual(members[0].offset, expected_offset)
|
||||
|
||||
|
||||
class TestModule(unittest.TestCase):
|
||||
def test_deprecated_version(self):
|
||||
with self.assertWarnsRegex(
|
||||
DeprecationWarning,
|
||||
"'version' is deprecated and slated for removal in Python 3.20",
|
||||
) as cm:
|
||||
getattr(tarfile, "version")
|
||||
self.assertEqual(cm.filename, __file__)
|
||||
|
||||
|
||||
def setUpModule():
|
||||
os_helper.unlink(TEMPDIR)
|
||||
os.makedirs(TEMPDIR)
|
||||
|
||||
Vendored
+1
-1
@@ -1409,7 +1409,7 @@ class TestSpooledTemporaryFile(BaseTestCase):
|
||||
|
||||
f.write(b'x')
|
||||
self.assertTrue(f._rolled)
|
||||
self.assertEqual(f.mode, 'rb+')
|
||||
self.assertEqual(f.mode, 'wb+')
|
||||
self.assertIsNotNone(f.name)
|
||||
with self.assertRaises(AttributeError):
|
||||
f.newlines
|
||||
|
||||
@@ -17,6 +17,7 @@ bitflagset::bitflag! {
|
||||
Readable = 1,
|
||||
Writable = 2,
|
||||
Appending = 3,
|
||||
Truncating = 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ impl FileMode {
|
||||
pub const READABLE: Self = Self::from_element(FileModeFlag::Readable);
|
||||
pub const WRITABLE: Self = Self::from_element(FileModeFlag::Writable);
|
||||
pub const APPENDING: Self = Self::from_element(FileModeFlag::Appending);
|
||||
pub const TRUNCATING: Self = Self::from_element(FileModeFlag::Truncating);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -72,7 +74,11 @@ impl FileMode {
|
||||
}
|
||||
} else if self.contains(&FileModeFlag::Readable) {
|
||||
if self.contains(&FileModeFlag::Writable) {
|
||||
"rb+"
|
||||
if self.contains(&FileModeFlag::Truncating) {
|
||||
"wb+"
|
||||
} else {
|
||||
"rb+"
|
||||
}
|
||||
} else {
|
||||
"rb"
|
||||
}
|
||||
@@ -109,7 +115,7 @@ pub fn parse_fileio_mode(mode_str: &str) -> Result<ParsedFileMode, FileModeError
|
||||
return Err(FileModeError::BadRwa);
|
||||
}
|
||||
rwa = true;
|
||||
mode |= FileMode::WRITABLE;
|
||||
mode |= FileMode::WRITABLE | FileMode::TRUNCATING;
|
||||
flags |= os::O_CREAT | os::O_TRUNC;
|
||||
}
|
||||
b'a' => {
|
||||
|
||||
@@ -13,6 +13,7 @@ bitflagset::bitflag! {
|
||||
Readable = 1,
|
||||
Writable = 2,
|
||||
Appending = 3,
|
||||
Truncating = 4,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +27,7 @@ impl FileMode {
|
||||
pub const READABLE: Self = Self::from_element(FileModeFlag::Readable);
|
||||
pub const WRITABLE: Self = Self::from_element(FileModeFlag::Writable);
|
||||
pub const APPENDING: Self = Self::from_element(FileModeFlag::Appending);
|
||||
pub const TRUNCATING: Self = Self::from_element(FileModeFlag::Truncating);
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
@@ -68,7 +70,11 @@ impl FileMode {
|
||||
}
|
||||
} else if self.contains(&FileModeFlag::Readable) {
|
||||
if self.contains(&FileModeFlag::Writable) {
|
||||
"rb+"
|
||||
if self.contains(&FileModeFlag::Truncating) {
|
||||
"wb+"
|
||||
} else {
|
||||
"rb+"
|
||||
}
|
||||
} else {
|
||||
"rb"
|
||||
}
|
||||
@@ -106,7 +112,7 @@ pub fn parse_fileio_mode(mode_str: &str) -> Result<ParsedFileMode, FileModeError
|
||||
return Err(FileModeError::BadRwa);
|
||||
}
|
||||
rwa = true;
|
||||
mode |= FileMode::WRITABLE;
|
||||
mode |= FileMode::WRITABLE | FileMode::TRUNCATING;
|
||||
flags |= O_CREAT | O_TRUNC;
|
||||
}
|
||||
b'a' => {
|
||||
|
||||
Reference in new issue
Block a user