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:
youknowdotandyouknowdot authored and GitHub committed 2026-10-15 10:03:58 +09:00
1 parent 0f127b07e9
commit f9b07b786c
12 files changed
+477 -143

No files matched your search

+60 -36
View File
@@ -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
View File
@@ -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()
+20 -10
View File
@@ -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()
+80 -39
View File
@@ -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()
+2 -2
View File
@@ -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
+54 -4
View File
@@ -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'
+2 -2
View File
@@ -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")
+58 -1
View File
@@ -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__':
+119 -14
View File
@@ -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)
+1 -1
View File
@@ -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
+8 -2
View File
@@ -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' => {
+8 -2
View File
@@ -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' => {