Update tarfile from v3.14.3

Assisted-by: Codex:gpt-6
This commit is contained in:
CPython Developers authored and Jeong, YunWon committed 2026-02-27 16:53:55 +09:00
1 parent 4e7411754b
commit a620ae7941
2 files changed
+2414 -240

No files matched your search

Vendored Executable → Regular
+375 -146
View File
@@ -1,4 +1,3 @@
#!/usr/bin/env python3
#-------------------------------------------------------------------
# tarfile.py
#-------------------------------------------------------------------
@@ -46,7 +45,6 @@ import time
import struct
import copy
import re
import warnings
try:
import pwd
@@ -69,7 +67,7 @@ __all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError",
"DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter",
"tar_filter", "FilterError", "AbsoluteLinkError",
"OutsideDestinationError", "SpecialFileError", "AbsolutePathError",
"LinkOutsideDestinationError"]
"LinkOutsideDestinationError", "LinkFallbackError"]
#---------------------------------------------------------
@@ -341,7 +339,7 @@ class _Stream:
"""
def __init__(self, name, mode, comptype, fileobj, bufsize,
compresslevel):
compresslevel, preset):
"""Construct a _Stream object.
"""
self._extfileobj = True
@@ -355,7 +353,7 @@ class _Stream:
fileobj = _StreamProxy(fileobj)
comptype = fileobj.getcomptype()
self.name = name or ""
self.name = os.fspath(name) if name is not None else ""
self.mode = mode
self.comptype = comptype
self.fileobj = fileobj
@@ -395,17 +393,23 @@ class _Stream:
import lzma
except ImportError:
raise CompressionError("lzma module is not available") from None
# XXX: RUSTPYTHON; xz is not supported yet
raise CompressionError("lzma module is not available") from None
if mode == "r":
self.dbuf = b""
self.cmp = lzma.LZMADecompressor()
self.exception = lzma.LZMAError
else:
self.cmp = lzma.LZMACompressor()
self.cmp = lzma.LZMACompressor(preset=preset)
elif comptype == "zst":
try:
from compression import zstd
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:
self.cmp = zstd.ZstdCompressor()
elif comptype != "tar":
raise CompressionError("unknown compression type %r" % comptype)
@@ -597,6 +601,8 @@ class _StreamProxy(object):
return "bz2"
elif self.buf.startswith((b"\x5d\x00\x00\x80", b"\xfd7zXZ")):
return "xz"
elif self.buf.startswith(b"\x28\xb5\x2f\xfd"):
return "zst"
else:
return "tar"
@@ -641,6 +647,10 @@ class _FileInFile(object):
def flush(self):
pass
@property
def mode(self):
return 'rb'
def readable(self):
return True
@@ -756,10 +766,22 @@ class LinkOutsideDestinationError(FilterError):
super().__init__(f'{tarinfo.name!r} would link to {path!r}, '
+ 'which is outside the destination')
class LinkFallbackError(FilterError):
def __init__(self, tarinfo, path):
self.tarinfo = tarinfo
self._path = path
super().__init__(f'link {tarinfo.name!r} would be extracted as a '
+ f'copy of {path!r}, which was rejected')
# Errors caused by filters -- both "fatal" and "non-fatal" -- that
# we consider to be issues with the argument, rather than a bug in the
# filter function
_FILTER_ERRORS = (FilterError, OSError, ExtractError)
def _get_filtered_attrs(member, dest_path, for_data=True):
new_attrs = {}
name = member.name
dest_path = os.path.realpath(dest_path)
dest_path = os.path.realpath(dest_path, strict=os.path.ALLOW_MISSING)
# Strip leading / (tar's directory separator) from filenames.
# Include os.sep (target OS directory separator) as well.
if name.startswith(('/', os.sep)):
@@ -769,7 +791,8 @@ 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
target_path = os.path.realpath(os.path.join(dest_path, name))
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:
raise OutsideDestinationError(member, target_path)
# Limit permissions (no high bits, and go-w)
@@ -807,6 +830,9 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
if member.islnk() or member.issym():
if os.path.isabs(member.linkname):
raise AbsoluteLinkError(member)
normalized = os.path.normpath(member.linkname)
if normalized != member.linkname:
new_attrs['linkname'] = normalized
if member.issym():
target_path = os.path.join(dest_path,
os.path.dirname(name),
@@ -814,7 +840,8 @@ def _get_filtered_attrs(member, dest_path, for_data=True):
else:
target_path = os.path.join(dest_path,
member.linkname)
target_path = os.path.realpath(target_path)
target_path = os.path.realpath(target_path,
strict=os.path.ALLOW_MISSING)
if os.path.commonpath([target_path, dest_path]) != dest_path:
raise LinkOutsideDestinationError(member, target_path)
return new_attrs
@@ -847,6 +874,9 @@ _NAMED_FILTERS = {
# Sentinel for replace() defaults, meaning "don't change the attribute"
_KEEP = object()
# Header length is digits followed by a space.
_header_length_prefix_re = re.compile(br"([0-9]{1,20}) ")
class TarInfo(object):
"""Informational class which holds the details about an
archive member given by a tar header block.
@@ -877,7 +907,7 @@ class TarInfo(object):
pax_headers = ('A dictionary containing key-value pairs of an '
'associated pax extended header.'),
sparse = 'Sparse member information.',
tarfile = None,
_tarfile = None,
_sparse_structs = None,
_link_target = None,
)
@@ -906,6 +936,24 @@ class TarInfo(object):
self.sparse = None # sparse member information
self.pax_headers = {} # pax header information
@property
def tarfile(self):
import warnings
warnings.warn(
'The undocumented "tarfile" attribute of TarInfo objects '
+ 'is deprecated and will be removed in Python 3.16',
DeprecationWarning, stacklevel=2)
return self._tarfile
@tarfile.setter
def tarfile(self, tarfile):
import warnings
warnings.warn(
'The undocumented "tarfile" attribute of TarInfo objects '
+ 'is deprecated and will be removed in Python 3.16',
DeprecationWarning, stacklevel=2)
self._tarfile = tarfile
@property
def path(self):
'In pax headers, "name" is called "path".'
@@ -1200,7 +1248,7 @@ class TarInfo(object):
for keyword, value in pax_headers.items():
keyword = keyword.encode("utf-8")
if binary:
# Try to restore the original byte representation of `value'.
# Try to restore the original byte representation of 'value'.
# Needless to say, that the encoding must match the string.
value = value.encode(encoding, "surrogateescape")
else:
@@ -1416,37 +1464,59 @@ class TarInfo(object):
else:
pax_headers = tarfile.pax_headers.copy()
# Check if the pax header contains a hdrcharset field. This tells us
# the encoding of the path, linkpath, uname and gname fields. Normally,
# these fields are UTF-8 encoded but since POSIX.1-2008 tar
# implementations are allowed to store them as raw binary strings if
# the translation to UTF-8 fails.
match = re.search(br"\d+ hdrcharset=([^\n]+)\n", buf)
if match is not None:
pax_headers["hdrcharset"] = match.group(1).decode("utf-8")
# For the time being, we don't care about anything other than "BINARY".
# The only other value that is currently allowed by the standard is
# "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
hdrcharset = pax_headers.get("hdrcharset")
if hdrcharset == "BINARY":
encoding = tarfile.encoding
else:
encoding = "utf-8"
# Parse pax header information. A record looks like that:
# "%d %s=%s\n" % (length, keyword, value). length is the size
# of the complete record including the length field itself and
# the newline. keyword and value are both UTF-8 encoded strings.
regex = re.compile(br"(\d+) ([^=]+)=")
# the newline.
pos = 0
while match := regex.match(buf, pos):
length, keyword = match.groups()
length = int(length)
if length == 0:
encoding = None
raw_headers = []
while len(buf) > pos and buf[pos] != 0x00:
if not (match := _header_length_prefix_re.match(buf, pos)):
raise InvalidHeaderError("invalid header")
try:
length = int(match.group(1))
except ValueError:
raise InvalidHeaderError("invalid header")
# Headers must be at least 5 bytes, shortest being '5 x=\n'.
# Value is allowed to be empty.
if length < 5:
raise InvalidHeaderError("invalid header")
if pos + length > len(buf):
raise InvalidHeaderError("invalid header")
value = buf[match.end(2) + 1:match.start(1) + length - 1]
header_value_end_offset = match.start(1) + length - 1 # Last byte of the header
keyword_and_value = buf[match.end(1) + 1:header_value_end_offset]
raw_keyword, equals, raw_value = keyword_and_value.partition(b"=")
# Check the framing of the header. The last character must be '\n' (0x0A)
if not raw_keyword or equals != b"=" or buf[header_value_end_offset] != 0x0A:
raise InvalidHeaderError("invalid header")
raw_headers.append((length, raw_keyword, raw_value))
# Check if the pax header contains a hdrcharset field. This tells us
# the encoding of the path, linkpath, uname and gname fields. Normally,
# these fields are UTF-8 encoded but since POSIX.1-2008 tar
# implementations are allowed to store them as raw binary strings if
# the translation to UTF-8 fails. For the time being, we don't care about
# anything other than "BINARY". The only other value that is currently
# allowed by the standard is "ISO-IR 10646 2000 UTF-8" in other words UTF-8.
# Note that we only follow the initial 'hdrcharset' setting to preserve
# the initial behavior of the 'tarfile' module.
if raw_keyword == b"hdrcharset" and encoding is None:
if raw_value == b"BINARY":
encoding = tarfile.encoding
else: # This branch ensures only the first 'hdrcharset' header is used.
encoding = "utf-8"
pos += length
# If no explicit hdrcharset is set, we use UTF-8 as a default.
if encoding is None:
encoding = "utf-8"
# After parsing the raw headers we can decode them to text.
for length, raw_keyword, raw_value in raw_headers:
# Normally, we could just use "utf-8" as the encoding and "strict"
# as the error handler, but we better not take the risk. For
# example, GNU tar <= 1.23 is known to store filenames it cannot
@@ -1454,17 +1524,16 @@ class TarInfo(object):
# hdrcharset=BINARY header).
# We first try the strict standard encoding, and if that fails we
# fall back on the user's encoding and error handler.
keyword = self._decode_pax_field(keyword, "utf-8", "utf-8",
keyword = self._decode_pax_field(raw_keyword, "utf-8", "utf-8",
tarfile.errors)
if keyword in PAX_NAME_FIELDS:
value = self._decode_pax_field(value, encoding, tarfile.encoding,
value = self._decode_pax_field(raw_value, encoding, tarfile.encoding,
tarfile.errors)
else:
value = self._decode_pax_field(value, "utf-8", "utf-8",
value = self._decode_pax_field(raw_value, "utf-8", "utf-8",
tarfile.errors)
pax_headers[keyword] = value
pos += length
# Fetch the next header.
try:
@@ -1479,7 +1548,7 @@ class TarInfo(object):
elif "GNU.sparse.size" in pax_headers:
# GNU extended sparse format version 0.0.
self._proc_gnusparse_00(next, pax_headers, buf)
self._proc_gnusparse_00(next, raw_headers)
elif pax_headers.get("GNU.sparse.major") == "1" and pax_headers.get("GNU.sparse.minor") == "0":
# GNU extended sparse format version 1.0.
@@ -1501,15 +1570,24 @@ class TarInfo(object):
return next
def _proc_gnusparse_00(self, next, pax_headers, buf):
def _proc_gnusparse_00(self, next, raw_headers):
"""Process a GNU tar extended sparse header, version 0.0.
"""
offsets = []
for match in re.finditer(br"\d+ GNU.sparse.offset=(\d+)\n", buf):
offsets.append(int(match.group(1)))
numbytes = []
for match in re.finditer(br"\d+ GNU.sparse.numbytes=(\d+)\n", buf):
numbytes.append(int(match.group(1)))
for _, keyword, value in raw_headers:
if keyword == b"GNU.sparse.offset":
try:
offsets.append(int(value.decode()))
except ValueError:
raise InvalidHeaderError("invalid header")
elif keyword == b"GNU.sparse.numbytes":
try:
numbytes.append(int(value.decode()))
except ValueError:
raise InvalidHeaderError("invalid header")
next.sparse = list(zip(offsets, numbytes))
def _proc_gnusparse_01(self, next, pax_headers):
@@ -1569,6 +1647,9 @@ class TarInfo(object):
"""Round up a byte count by BLOCKSIZE and return it,
e.g. _block(834) => 1024.
"""
# Only non-negative offsets are allowed
if count < 0:
raise InvalidHeaderError("invalid offset")
blocks, remainder = divmod(count, BLOCKSIZE)
if remainder:
blocks += 1
@@ -1645,14 +1726,14 @@ 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):
"""Open an (uncompressed) tar archive `name'. `mode' is either 'r' to
errorlevel=None, copybufsize=None, stream=False):
"""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'
file or 'w' to create a new file overwriting an existing one. 'mode'
defaults to 'r'.
If `fileobj' is given, it is used for reading or writing data. If it
can be determined, `mode' is overridden by `fileobj's mode.
`fileobj' is not closed, when TarFile is closed.
If 'fileobj' is given, it is used for reading or writing data. If it
can be determined, 'mode' is overridden by 'fileobj's mode.
'fileobj' is not closed, when TarFile is closed.
"""
modes = {"r": "rb", "a": "r+b", "w": "wb", "x": "xb"}
if mode not in modes:
@@ -1677,6 +1758,8 @@ class TarFile(object):
self.name = os.path.abspath(name) if name else None
self.fileobj = fileobj
self.stream = stream
# Init attributes.
if format is not None:
self.format = format
@@ -1709,6 +1792,8 @@ class TarFile(object):
# current position in the archive file
self.inodes = {} # dictionary caching the inodes of
# archive members already added
self._unames = {} # Cached mappings of uid -> uname
self._gnames = {} # Cached mappings of gid -> gname
try:
if self.mode == "r":
@@ -1764,11 +1849,13 @@ class TarFile(object):
'r:gz' open for reading with gzip compression
'r:bz2' open for reading with bzip2 compression
'r:xz' open for reading with lzma compression
'r:zst' open for reading with zstd compression
'a' or 'a:' open for appending, creating the file if necessary
'w' or 'w:' open for writing without compression
'w:gz' open for writing with gzip compression
'w:bz2' open for writing with bzip2 compression
'w:xz' open for writing with lzma compression
'w:zst' open for writing with zstd compression
'x' or 'x:' create a tarfile exclusively without compression, raise
an exception if the file is already created
@@ -1778,16 +1865,20 @@ class TarFile(object):
if the file is already created
'x:xz' create an lzma compressed tarfile, raise an exception
if the file is already created
'x:zst' create a zstd compressed tarfile, raise an exception
if the file is already created
'r|*' open a stream of tar blocks with transparent compression
'r|' open an uncompressed stream of tar blocks for reading
'r|gz' open a gzip compressed stream of tar blocks
'r|bz2' open a bzip2 compressed stream of tar blocks
'r|xz' open an lzma compressed stream of tar blocks
'r|zst' open a zstd compressed stream of tar blocks
'w|' open an uncompressed stream for writing
'w|gz' open a gzip compressed stream for writing
'w|bz2' open a bzip2 compressed stream for writing
'w|xz' open an lzma compressed stream for writing
'w|zst' open a zstd compressed stream for writing
"""
if not name and not fileobj:
@@ -1832,10 +1923,17 @@ class TarFile(object):
if filemode not in ("r", "w"):
raise ValueError("mode must be 'r' or 'w'")
if "compresslevel" in kwargs and comptype not in ("gz", "bz2"):
raise ValueError(
"compresslevel is only valid for w|gz and w|bz2 modes"
)
if "preset" in kwargs and comptype not in ("xz",):
raise ValueError("preset is only valid for w|xz mode")
compresslevel = kwargs.pop("compresslevel", 9)
preset = kwargs.pop("preset", None)
stream = _Stream(name, filemode, comptype, fileobj, bufsize,
compresslevel)
compresslevel, preset)
try:
t = cls(name, filemode, stream, **kwargs)
except:
@@ -1931,9 +2029,6 @@ class TarFile(object):
except ImportError:
raise CompressionError("lzma module is not available") from None
# XXX: RUSTPYTHON; xz is not supported yet
raise CompressionError("lzma module is not available") from None
fileobj = LZMAFile(fileobj or name, mode, preset=preset)
try:
@@ -1949,12 +2044,48 @@ class TarFile(object):
t._extfileobj = False
return t
@classmethod
def zstopen(cls, name, mode="r", fileobj=None, level=None, options=None,
zstd_dict=None, **kwargs):
"""Open zstd compressed tar archive name for reading or writing.
Appending is not allowed.
"""
if mode not in ("r", "w", "x"):
raise ValueError("mode must be 'r', 'w' or 'x'")
try:
from compression.zstd import ZstdFile, ZstdError
except ImportError:
raise CompressionError("compression.zstd module is not available") from None
fileobj = ZstdFile(
fileobj or name,
mode,
level=level,
options=options,
zstd_dict=zstd_dict
)
try:
t = cls.taropen(name, mode, fileobj, **kwargs)
except (ZstdError, EOFError) as e:
fileobj.close()
if mode == 'r':
raise ReadError("not a zstd file") from e
raise
except Exception:
fileobj.close()
raise
t._extfileobj = False
return t
# All *open() methods are registered here.
OPEN_METH = {
"tar": "taropen", # uncompressed tar
"gz": "gzopen", # gzip compressed tar
"bz2": "bz2open", # bzip2 compressed tar
"xz": "xzopen" # lzma compressed tar
"xz": "xzopen", # lzma compressed tar
"zst": "zstopen", # zstd compressed tar
}
#--------------------------------------------------------------------------
@@ -1982,7 +2113,7 @@ class TarFile(object):
self.fileobj.close()
def getmember(self, name):
"""Return a TarInfo object for member `name'. If `name' can not be
"""Return a TarInfo object for member 'name'. If 'name' can not be
found in the archive, KeyError is raised. If a member occurs more
than once in the archive, its last occurrence is assumed to be the
most up-to-date version.
@@ -2010,9 +2141,9 @@ class TarFile(object):
def gettarinfo(self, name=None, arcname=None, fileobj=None):
"""Create a TarInfo object from the result of os.stat or equivalent
on an existing file. The file is either named by `name', or
specified as a file object `fileobj' with a file descriptor. If
given, `arcname' specifies an alternative name for the file in the
on an existing file. The file is either named by 'name', or
specified as a file object 'fileobj' with a file descriptor. If
given, 'arcname' specifies an alternative name for the file in the
archive, otherwise, the name is taken from the 'name' attribute of
'fileobj', or the 'name' argument. The name should be a text
string.
@@ -2036,7 +2167,7 @@ class TarFile(object):
# Now, fill the TarInfo object with
# information specific for the file.
tarinfo = self.tarinfo()
tarinfo.tarfile = self # Not needed
tarinfo._tarfile = self # To be removed in 3.16.
# Use os.stat or os.lstat, depending on if symlinks shall be resolved.
if fileobj is None:
@@ -2090,16 +2221,23 @@ class TarFile(object):
tarinfo.mtime = statres.st_mtime
tarinfo.type = type
tarinfo.linkname = linkname
# Calls to pwd.getpwuid() and grp.getgrgid() tend to be expensive. To
# speed things up, cache the resolved usernames and group names.
if pwd:
try:
tarinfo.uname = pwd.getpwuid(tarinfo.uid)[0]
except KeyError:
pass
if tarinfo.uid not in self._unames:
try:
self._unames[tarinfo.uid] = pwd.getpwuid(tarinfo.uid)[0]
except KeyError:
self._unames[tarinfo.uid] = ''
tarinfo.uname = self._unames[tarinfo.uid]
if grp:
try:
tarinfo.gname = grp.getgrgid(tarinfo.gid)[0]
except KeyError:
pass
if tarinfo.gid not in self._gnames:
try:
self._gnames[tarinfo.gid] = grp.getgrgid(tarinfo.gid)[0]
except KeyError:
self._gnames[tarinfo.gid] = ''
tarinfo.gname = self._gnames[tarinfo.gid]
if type in (CHRTYPE, BLKTYPE):
if hasattr(os, "major") and hasattr(os, "minor"):
@@ -2108,11 +2246,15 @@ class TarFile(object):
return tarinfo
def list(self, verbose=True, *, members=None):
"""Print a table of contents to sys.stdout. If `verbose' is False, only
the names of the members are printed. If it is True, an `ls -l'-like
output is produced. `members' is optional and must be a subset of the
"""Print a table of contents to sys.stdout. If 'verbose' is False, only
the names of the members are printed. If it is True, an 'ls -l'-like
output is produced. 'members' is optional and must be a subset of the
list returned by getmembers().
"""
# Convert tarinfo type to stat type.
type2mode = {REGTYPE: stat.S_IFREG, SYMTYPE: stat.S_IFLNK,
FIFOTYPE: stat.S_IFIFO, CHRTYPE: stat.S_IFCHR,
DIRTYPE: stat.S_IFDIR, BLKTYPE: stat.S_IFBLK}
self._check()
if members is None:
@@ -2122,7 +2264,8 @@ class TarFile(object):
if tarinfo.mode is None:
_safe_print("??????????")
else:
_safe_print(stat.filemode(tarinfo.mode))
modetype = type2mode.get(tarinfo.type, 0)
_safe_print(stat.filemode(modetype | tarinfo.mode))
_safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid,
tarinfo.gname or tarinfo.gid))
if tarinfo.ischr() or tarinfo.isblk():
@@ -2146,11 +2289,11 @@ class TarFile(object):
print()
def add(self, name, arcname=None, recursive=True, *, filter=None):
"""Add the file `name' to the archive. `name' may be any type of file
(directory, fifo, symbolic link, etc.). If given, `arcname'
"""Add the file 'name' to the archive. 'name' may be any type of file
(directory, fifo, symbolic link, etc.). If given, 'arcname'
specifies an alternative name for the file in the archive.
Directories are added recursively by default. This can be avoided by
setting `recursive' to False. `filter' is a function
setting 'recursive' to False. 'filter' is a function
that expects a TarInfo object argument and returns the changed
TarInfo object, if it returns None the TarInfo object will be
excluded from the archive.
@@ -2197,13 +2340,16 @@ class TarFile(object):
self.addfile(tarinfo)
def addfile(self, tarinfo, fileobj=None):
"""Add the TarInfo object `tarinfo' to the archive. If `fileobj' is
given, it should be a binary file, and tarinfo.size bytes are read
from it and added to the archive. You can create TarInfo objects
directly, or by using gettarinfo().
"""Add the TarInfo object 'tarinfo' to the archive. If 'tarinfo' represents
a non zero-size regular file, the 'fileobj' argument should be a binary file,
and tarinfo.size bytes are read from it and added to the archive.
You can create TarInfo objects directly, or by using gettarinfo().
"""
self._check("awx")
if fileobj is None and tarinfo.isreg() and tarinfo.size != 0:
raise ValueError("fileobj not provided for non zero-size regular file")
tarinfo = copy.copy(tarinfo)
buf = tarinfo.tobuf(self.format, self.encoding, self.errors)
@@ -2225,12 +2371,7 @@ class TarFile(object):
if filter is None:
filter = self.extraction_filter
if filter is None:
warnings.warn(
'Python 3.14 will, by default, filter extracted tar '
+ 'archives and reject files or modify their metadata. '
+ 'Use the filter argument to control this behavior.',
DeprecationWarning)
return fully_trusted_filter
return data_filter
if isinstance(filter, str):
raise TypeError(
'String names are not supported for '
@@ -2248,12 +2389,12 @@ class TarFile(object):
filter=None):
"""Extract all members from the archive to the current working
directory and set owner, modification time and permissions on
directories afterwards. `path' specifies a different directory
to extract to. `members' is optional and must be a subset of the
list returned by getmembers(). If `numeric_owner` is True, only
directories afterwards. 'path' specifies a different directory
to extract to. 'members' is optional and must be a subset of the
list returned by getmembers(). If 'numeric_owner' is True, only
the numbers for user/group names are used and not the names.
The `filter` function will be called on each member just
The 'filter' function will be called on each member just
before extraction.
It can return a changed TarInfo or None to skip the member.
String names of common filters are accepted.
@@ -2265,81 +2406,124 @@ class TarFile(object):
members = self
for member in members:
tarinfo = self._get_extract_tarinfo(member, filter_function, path)
tarinfo, unfiltered = self._get_extract_tarinfo(
member, filter_function, path)
if tarinfo is None:
continue
if tarinfo.isdir():
# For directories, delay setting attributes until later,
# since permissions can interfere with extraction and
# extracting contents can reset mtime.
directories.append(tarinfo)
directories.append(unfiltered)
self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(),
numeric_owner=numeric_owner)
numeric_owner=numeric_owner,
filter_function=filter_function)
# Reverse sort directories.
directories.sort(key=lambda a: a.name, reverse=True)
# Set correct owner, mtime and filemode on directories.
for tarinfo in directories:
dirpath = os.path.join(path, tarinfo.name)
for unfiltered in directories:
try:
# Need to re-apply any filter, to take the *current* filesystem
# state into account.
try:
tarinfo = filter_function(unfiltered, path)
except _FILTER_ERRORS as exc:
self._log_no_directory_fixup(unfiltered, repr(exc))
continue
if tarinfo is None:
self._log_no_directory_fixup(unfiltered,
'excluded by filter')
continue
dirpath = os.path.join(path, tarinfo.name)
try:
lstat = os.lstat(dirpath)
except FileNotFoundError:
self._log_no_directory_fixup(tarinfo, 'missing')
continue
if not stat.S_ISDIR(lstat.st_mode):
# This is no longer a directory; presumably a later
# member overwrote the entry.
self._log_no_directory_fixup(tarinfo, 'not a directory')
continue
self.chown(tarinfo, dirpath, numeric_owner=numeric_owner)
self.utime(tarinfo, dirpath)
self.chmod(tarinfo, dirpath)
except ExtractError as e:
self._handle_nonfatal_error(e)
def _log_no_directory_fixup(self, member, reason):
self._dbg(2, "tarfile: Not fixing up directory %r (%s)" %
(member.name, reason))
def extract(self, member, path="", set_attrs=True, *, numeric_owner=False,
filter=None):
"""Extract a member from the archive to the current working directory,
using its full name. Its file information is extracted as accurately
as possible. `member' may be a filename or a TarInfo object. You can
specify a different directory using `path'. File attributes (owner,
mtime, mode) are set unless `set_attrs' is False. If `numeric_owner`
as possible. 'member' may be a filename or a TarInfo object. You can
specify a different directory using 'path'. File attributes (owner,
mtime, mode) are set unless 'set_attrs' is False. If 'numeric_owner'
is True, only the numbers for user/group names are used and not
the names.
The `filter` function will be called before extraction.
The 'filter' function will be called before extraction.
It can return a changed TarInfo or None to skip the member.
String names of common filters are accepted.
"""
filter_function = self._get_filter_function(filter)
tarinfo = self._get_extract_tarinfo(member, filter_function, path)
tarinfo, unfiltered = self._get_extract_tarinfo(
member, filter_function, path)
if tarinfo is not None:
self._extract_one(tarinfo, path, set_attrs, numeric_owner)
def _get_extract_tarinfo(self, member, filter_function, path):
"""Get filtered TarInfo (or None) from member, which might be a str"""
if isinstance(member, str):
tarinfo = self.getmember(member)
else:
tarinfo = member
"""Get (filtered, unfiltered) TarInfos from *member*
unfiltered = tarinfo
*member* might be a string.
Return (None, None) if not found.
"""
if isinstance(member, str):
unfiltered = self.getmember(member)
else:
unfiltered = member
filtered = None
try:
tarinfo = filter_function(tarinfo, path)
except (OSError, FilterError) as e:
filtered = filter_function(unfiltered, path)
except (OSError, UnicodeEncodeError, FilterError) as e:
self._handle_fatal_error(e)
except ExtractError as e:
self._handle_nonfatal_error(e)
if tarinfo is None:
if filtered is None:
self._dbg(2, "tarfile: Excluded %r" % unfiltered.name)
return None
# Prepare the link target for makelink().
if tarinfo.islnk():
tarinfo = copy.copy(tarinfo)
tarinfo._link_target = os.path.join(path, tarinfo.linkname)
return tarinfo
return None, None
def _extract_one(self, tarinfo, path, set_attrs, numeric_owner):
"""Extract from filtered tarinfo to disk"""
# Prepare the link target for makelink().
if filtered.islnk():
filtered = copy.copy(filtered)
filtered._link_target = os.path.join(path, filtered.linkname)
return filtered, unfiltered
def _extract_one(self, tarinfo, path, set_attrs, numeric_owner,
filter_function=None):
"""Extract from filtered tarinfo to disk.
filter_function is only used when extracting a *different*
member (e.g. as fallback to creating a symlink)
"""
self._check("r")
try:
self._extract_member(tarinfo, os.path.join(path, tarinfo.name),
set_attrs=set_attrs,
numeric_owner=numeric_owner)
except OSError as e:
numeric_owner=numeric_owner,
filter_function=filter_function,
extraction_root=path)
except (OSError, UnicodeEncodeError) as e:
self._handle_fatal_error(e)
except ExtractError as e:
self._handle_nonfatal_error(e)
@@ -2364,10 +2548,10 @@ class TarFile(object):
self._dbg(1, "tarfile: %s %s" % (type(e).__name__, e))
def extractfile(self, member):
"""Extract a member from the archive as a file object. `member' may be
a filename or a TarInfo object. If `member' is a regular file or
"""Extract a member from the archive as a file object. 'member' may be
a filename or a TarInfo object. If 'member' is a regular file or
a link, an io.BufferedReader object is returned. For all other
existing members, None is returned. If `member' does not appear
existing members, None is returned. If 'member' does not appear
in the archive, KeyError is raised.
"""
self._check("r")
@@ -2396,9 +2580,13 @@ class TarFile(object):
return None
def _extract_member(self, tarinfo, targetpath, set_attrs=True,
numeric_owner=False):
"""Extract the TarInfo object tarinfo to a physical
numeric_owner=False, *, filter_function=None,
extraction_root=None):
"""Extract the filtered TarInfo object tarinfo to a physical
file called targetpath.
filter_function is only used when extracting a *different*
member (e.g. as fallback to creating a symlink)
"""
# Fetch the TarInfo object for the given name
# and build the destination pathname, replacing
@@ -2411,7 +2599,7 @@ class TarFile(object):
if upperdirs and not os.path.exists(upperdirs):
# Create directories that are not part of the archive with
# default permissions.
os.makedirs(upperdirs)
os.makedirs(upperdirs, exist_ok=True)
if tarinfo.islnk() or tarinfo.issym():
self._dbg(1, "%s -> %s" % (tarinfo.name, tarinfo.linkname))
@@ -2427,7 +2615,10 @@ class TarFile(object):
elif tarinfo.ischr() or tarinfo.isblk():
self.makedev(tarinfo, targetpath)
elif tarinfo.islnk() or tarinfo.issym():
self.makelink(tarinfo, targetpath)
self.makelink_with_filter(
tarinfo, targetpath,
filter_function=filter_function,
extraction_root=extraction_root)
elif tarinfo.type not in SUPPORTED_TYPES:
self.makeunknown(tarinfo, targetpath)
else:
@@ -2510,10 +2701,18 @@ class TarFile(object):
os.makedev(tarinfo.devmajor, tarinfo.devminor))
def makelink(self, tarinfo, targetpath):
return self.makelink_with_filter(tarinfo, targetpath, None, None)
def makelink_with_filter(self, tarinfo, targetpath,
filter_function, extraction_root):
"""Make a (symbolic) link called targetpath. If it cannot be created
(platform limitation), we try to make a copy of the referenced file
instead of a link.
filter_function is only used when extracting a *different*
member (e.g. as fallback to creating a link).
"""
keyerror_to_extracterror = False
try:
# For systems that support symbolic and hard links.
if tarinfo.issym():
@@ -2521,18 +2720,41 @@ class TarFile(object):
# Avoid FileExistsError on following os.symlink.
os.unlink(targetpath)
os.symlink(tarinfo.linkname, targetpath)
return
else:
if os.path.exists(tarinfo._link_target):
if os.path.lexists(targetpath):
# Avoid FileExistsError on following os.link.
os.unlink(targetpath)
os.link(tarinfo._link_target, targetpath)
else:
self._extract_member(self._find_link_target(tarinfo),
targetpath)
return
except symlink_exception:
keyerror_to_extracterror = True
try:
unfiltered = self._find_link_target(tarinfo)
except KeyError:
if keyerror_to_extracterror:
raise ExtractError(
"unable to resolve link inside archive") from None
else:
raise
if filter_function is None:
filtered = unfiltered
else:
if extraction_root is None:
raise ExtractError(
"makelink_with_filter: if filter_function is not None, "
+ "extraction_root must also not be None")
try:
self._extract_member(self._find_link_target(tarinfo),
targetpath)
except KeyError:
raise ExtractError("unable to resolve link inside archive") from None
filtered = filter_function(unfiltered, extraction_root)
except _FILTER_ERRORS as cause:
raise LinkFallbackError(tarinfo, unfiltered.name) from cause
if filtered is not None:
self._extract_member(filtered, targetpath,
filter_function=filter_function,
extraction_root=extraction_root)
def chown(self, tarinfo, targetpath, numeric_owner):
"""Set owner of targetpath according to tarinfo. If numeric_owner
@@ -2564,7 +2786,8 @@ class TarFile(object):
os.lchown(targetpath, u, g)
else:
os.chown(targetpath, u, g)
except OSError as e:
except (OSError, OverflowError) as e:
# OverflowError can be raised if an ID doesn't fit in 'id_t'
raise ExtractError("could not change owner") from e
def chmod(self, tarinfo, targetpath):
@@ -2647,7 +2870,9 @@ class TarFile(object):
break
if tarinfo is not None:
self.members.append(tarinfo)
# if streaming the file we do not want to cache the tarinfo
if not self.stream:
self.members.append(tarinfo)
else:
self._loaded = True
@@ -2698,11 +2923,12 @@ class TarFile(object):
def _load(self):
"""Read through the entire archive file and look for readable
members.
members. This should not run if the file is set to stream.
"""
while self.next() is not None:
pass
self._loaded = True
if not self.stream:
while self.next() is not None:
pass
self._loaded = True
def _check(self, mode=None):
"""Check if TarFile is still open, and if the operation's mode
@@ -2812,7 +3038,7 @@ def main():
import argparse
description = 'A simple command-line interface for tarfile module.'
parser = argparse.ArgumentParser(description=description)
parser = argparse.ArgumentParser(description=description, color=True)
parser.add_argument('-v', '--verbose', action='store_true', default=False,
help='Verbose output')
parser.add_argument('--filter', metavar='<filtername>',
@@ -2892,6 +3118,9 @@ def main():
'.tbz': 'bz2',
'.tbz2': 'bz2',
'.tb2': 'bz2',
# zstd
'.zst': 'zst',
'.tzst': 'zst',
}
tar_mode = 'w:' + compressions[ext] if ext in compressions else 'w'
tar_files = args.create
+2039 -94
View File
File diff suppressed because it is too large. Load diff