mirror of
https://github.com/RustPython/RustPython.git
synced 2026-06-02 19:39:49 +09:00
Update tarfile to 3.12.3 (#5714)
* Update tarfile to 3.12.3 * Unmark tests
This commit is contained in:
438
Lib/tarfile.py
vendored
438
Lib/tarfile.py
vendored
@@ -46,6 +46,7 @@ import time
|
||||
import struct
|
||||
import copy
|
||||
import re
|
||||
import warnings
|
||||
|
||||
try:
|
||||
import pwd
|
||||
@@ -57,19 +58,19 @@ except ImportError:
|
||||
grp = None
|
||||
|
||||
# os.symlink on Windows prior to 6.0 raises NotImplementedError
|
||||
symlink_exception = (AttributeError, NotImplementedError)
|
||||
try:
|
||||
# OSError (winerror=1314) will be raised if the caller does not hold the
|
||||
# SeCreateSymbolicLinkPrivilege privilege
|
||||
symlink_exception += (OSError,)
|
||||
except NameError:
|
||||
pass
|
||||
# OSError (winerror=1314) will be raised if the caller does not hold the
|
||||
# SeCreateSymbolicLinkPrivilege privilege
|
||||
symlink_exception = (AttributeError, NotImplementedError, OSError)
|
||||
|
||||
# from tarfile import *
|
||||
__all__ = ["TarFile", "TarInfo", "is_tarfile", "TarError", "ReadError",
|
||||
"CompressionError", "StreamError", "ExtractError", "HeaderError",
|
||||
"ENCODING", "USTAR_FORMAT", "GNU_FORMAT", "PAX_FORMAT",
|
||||
"DEFAULT_FORMAT", "open"]
|
||||
"DEFAULT_FORMAT", "open","fully_trusted_filter", "data_filter",
|
||||
"tar_filter", "FilterError", "AbsoluteLinkError",
|
||||
"OutsideDestinationError", "SpecialFileError", "AbsolutePathError",
|
||||
"LinkOutsideDestinationError"]
|
||||
|
||||
|
||||
#---------------------------------------------------------
|
||||
# tar constants
|
||||
@@ -158,6 +159,8 @@ else:
|
||||
def stn(s, length, encoding, errors):
|
||||
"""Convert a string to a null-terminated bytes object.
|
||||
"""
|
||||
if s is None:
|
||||
raise ValueError("metadata cannot contain None")
|
||||
s = s.encode(encoding, errors)
|
||||
return s[:length] + (length - len(s)) * NUL
|
||||
|
||||
@@ -328,15 +331,17 @@ class _LowLevelFile:
|
||||
class _Stream:
|
||||
"""Class that serves as an adapter between TarFile and
|
||||
a stream-like object. The stream-like object only
|
||||
needs to have a read() or write() method and is accessed
|
||||
blockwise. Use of gzip or bzip2 compression is possible.
|
||||
A stream-like object could be for example: sys.stdin,
|
||||
sys.stdout, a socket, a tape device etc.
|
||||
needs to have a read() or write() method that works with bytes,
|
||||
and the method is accessed blockwise.
|
||||
Use of gzip or bzip2 compression is possible.
|
||||
A stream-like object could be for example: sys.stdin.buffer,
|
||||
sys.stdout.buffer, a socket, a tape device etc.
|
||||
|
||||
_Stream is intended to be used only internally.
|
||||
"""
|
||||
|
||||
def __init__(self, name, mode, comptype, fileobj, bufsize):
|
||||
def __init__(self, name, mode, comptype, fileobj, bufsize,
|
||||
compresslevel):
|
||||
"""Construct a _Stream object.
|
||||
"""
|
||||
self._extfileobj = True
|
||||
@@ -368,10 +373,10 @@ class _Stream:
|
||||
self.zlib = zlib
|
||||
self.crc = zlib.crc32(b"")
|
||||
if mode == "r":
|
||||
self._init_read_gz()
|
||||
self.exception = zlib.error
|
||||
self._init_read_gz()
|
||||
else:
|
||||
self._init_write_gz()
|
||||
self._init_write_gz(compresslevel)
|
||||
|
||||
elif comptype == "bz2":
|
||||
try:
|
||||
@@ -383,7 +388,7 @@ class _Stream:
|
||||
self.cmp = bz2.BZ2Decompressor()
|
||||
self.exception = OSError
|
||||
else:
|
||||
self.cmp = bz2.BZ2Compressor()
|
||||
self.cmp = bz2.BZ2Compressor(compresslevel)
|
||||
|
||||
elif comptype == "xz":
|
||||
try:
|
||||
@@ -410,13 +415,14 @@ class _Stream:
|
||||
if hasattr(self, "closed") and not self.closed:
|
||||
self.close()
|
||||
|
||||
def _init_write_gz(self):
|
||||
def _init_write_gz(self, compresslevel):
|
||||
"""Initialize for writing with gzip compression.
|
||||
"""
|
||||
self.cmp = self.zlib.compressobj(9, self.zlib.DEFLATED,
|
||||
-self.zlib.MAX_WBITS,
|
||||
self.zlib.DEF_MEM_LEVEL,
|
||||
0)
|
||||
self.cmp = self.zlib.compressobj(compresslevel,
|
||||
self.zlib.DEFLATED,
|
||||
-self.zlib.MAX_WBITS,
|
||||
self.zlib.DEF_MEM_LEVEL,
|
||||
0)
|
||||
timestamp = struct.pack("<L", int(time.time()))
|
||||
self.__write(b"\037\213\010\010" + timestamp + b"\002\377")
|
||||
if self.name.endswith(".gz"):
|
||||
@@ -603,12 +609,12 @@ class _FileInFile(object):
|
||||
object.
|
||||
"""
|
||||
|
||||
def __init__(self, fileobj, offset, size, blockinfo=None):
|
||||
def __init__(self, fileobj, offset, size, name, blockinfo=None):
|
||||
self.fileobj = fileobj
|
||||
self.offset = offset
|
||||
self.size = size
|
||||
self.position = 0
|
||||
self.name = getattr(fileobj, "name", None)
|
||||
self.name = name
|
||||
self.closed = False
|
||||
|
||||
if blockinfo is None:
|
||||
@@ -705,13 +711,138 @@ class ExFileObject(io.BufferedReader):
|
||||
|
||||
def __init__(self, tarfile, tarinfo):
|
||||
fileobj = _FileInFile(tarfile.fileobj, tarinfo.offset_data,
|
||||
tarinfo.size, tarinfo.sparse)
|
||||
tarinfo.size, tarinfo.name, tarinfo.sparse)
|
||||
super().__init__(fileobj)
|
||||
#class ExFileObject
|
||||
|
||||
|
||||
#-----------------------------
|
||||
# extraction filters (PEP 706)
|
||||
#-----------------------------
|
||||
|
||||
class FilterError(TarError):
|
||||
pass
|
||||
|
||||
class AbsolutePathError(FilterError):
|
||||
def __init__(self, tarinfo):
|
||||
self.tarinfo = tarinfo
|
||||
super().__init__(f'member {tarinfo.name!r} has an absolute path')
|
||||
|
||||
class OutsideDestinationError(FilterError):
|
||||
def __init__(self, tarinfo, path):
|
||||
self.tarinfo = tarinfo
|
||||
self._path = path
|
||||
super().__init__(f'{tarinfo.name!r} would be extracted to {path!r}, '
|
||||
+ 'which is outside the destination')
|
||||
|
||||
class SpecialFileError(FilterError):
|
||||
def __init__(self, tarinfo):
|
||||
self.tarinfo = tarinfo
|
||||
super().__init__(f'{tarinfo.name!r} is a special file')
|
||||
|
||||
class AbsoluteLinkError(FilterError):
|
||||
def __init__(self, tarinfo):
|
||||
self.tarinfo = tarinfo
|
||||
super().__init__(f'{tarinfo.name!r} is a link to an absolute path')
|
||||
|
||||
class LinkOutsideDestinationError(FilterError):
|
||||
def __init__(self, tarinfo, path):
|
||||
self.tarinfo = tarinfo
|
||||
self._path = path
|
||||
super().__init__(f'{tarinfo.name!r} would link to {path!r}, '
|
||||
+ 'which is outside the destination')
|
||||
|
||||
def _get_filtered_attrs(member, dest_path, for_data=True):
|
||||
new_attrs = {}
|
||||
name = member.name
|
||||
dest_path = os.path.realpath(dest_path)
|
||||
# Strip leading / (tar's directory separator) from filenames.
|
||||
# Include os.sep (target OS directory separator) as well.
|
||||
if name.startswith(('/', os.sep)):
|
||||
name = new_attrs['name'] = member.path.lstrip('/' + os.sep)
|
||||
if os.path.isabs(name):
|
||||
# Path is absolute even after stripping.
|
||||
# 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))
|
||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||
raise OutsideDestinationError(member, target_path)
|
||||
# Limit permissions (no high bits, and go-w)
|
||||
mode = member.mode
|
||||
if mode is not None:
|
||||
# Strip high bits & group/other write bits
|
||||
mode = mode & 0o755
|
||||
if for_data:
|
||||
# For data, handle permissions & file types
|
||||
if member.isreg() or member.islnk():
|
||||
if not mode & 0o100:
|
||||
# Clear executable bits if not executable by user
|
||||
mode &= ~0o111
|
||||
# Ensure owner can read & write
|
||||
mode |= 0o600
|
||||
elif member.isdir() or member.issym():
|
||||
# Ignore mode for directories & symlinks
|
||||
mode = None
|
||||
else:
|
||||
# Reject special files
|
||||
raise SpecialFileError(member)
|
||||
if mode != member.mode:
|
||||
new_attrs['mode'] = mode
|
||||
if for_data:
|
||||
# Ignore ownership for 'data'
|
||||
if member.uid is not None:
|
||||
new_attrs['uid'] = None
|
||||
if member.gid is not None:
|
||||
new_attrs['gid'] = None
|
||||
if member.uname is not None:
|
||||
new_attrs['uname'] = None
|
||||
if member.gname is not None:
|
||||
new_attrs['gname'] = None
|
||||
# Check link destination for 'data'
|
||||
if member.islnk() or member.issym():
|
||||
if os.path.isabs(member.linkname):
|
||||
raise AbsoluteLinkError(member)
|
||||
if member.issym():
|
||||
target_path = os.path.join(dest_path,
|
||||
os.path.dirname(name),
|
||||
member.linkname)
|
||||
else:
|
||||
target_path = os.path.join(dest_path,
|
||||
member.linkname)
|
||||
target_path = os.path.realpath(target_path)
|
||||
if os.path.commonpath([target_path, dest_path]) != dest_path:
|
||||
raise LinkOutsideDestinationError(member, target_path)
|
||||
return new_attrs
|
||||
|
||||
def fully_trusted_filter(member, dest_path):
|
||||
return member
|
||||
|
||||
def tar_filter(member, dest_path):
|
||||
new_attrs = _get_filtered_attrs(member, dest_path, False)
|
||||
if new_attrs:
|
||||
return member.replace(**new_attrs, deep=False)
|
||||
return member
|
||||
|
||||
def data_filter(member, dest_path):
|
||||
new_attrs = _get_filtered_attrs(member, dest_path, True)
|
||||
if new_attrs:
|
||||
return member.replace(**new_attrs, deep=False)
|
||||
return member
|
||||
|
||||
_NAMED_FILTERS = {
|
||||
"fully_trusted": fully_trusted_filter,
|
||||
"tar": tar_filter,
|
||||
"data": data_filter,
|
||||
}
|
||||
|
||||
#------------------
|
||||
# Exported Classes
|
||||
#------------------
|
||||
|
||||
# Sentinel for replace() defaults, meaning "don't change the attribute"
|
||||
_KEEP = object()
|
||||
|
||||
class TarInfo(object):
|
||||
"""Informational class which holds the details about an
|
||||
archive member given by a tar header block.
|
||||
@@ -792,12 +923,44 @@ class TarInfo(object):
|
||||
def __repr__(self):
|
||||
return "<%s %r at %#x>" % (self.__class__.__name__,self.name,id(self))
|
||||
|
||||
def replace(self, *,
|
||||
name=_KEEP, mtime=_KEEP, mode=_KEEP, linkname=_KEEP,
|
||||
uid=_KEEP, gid=_KEEP, uname=_KEEP, gname=_KEEP,
|
||||
deep=True, _KEEP=_KEEP):
|
||||
"""Return a deep copy of self with the given attributes replaced.
|
||||
"""
|
||||
if deep:
|
||||
result = copy.deepcopy(self)
|
||||
else:
|
||||
result = copy.copy(self)
|
||||
if name is not _KEEP:
|
||||
result.name = name
|
||||
if mtime is not _KEEP:
|
||||
result.mtime = mtime
|
||||
if mode is not _KEEP:
|
||||
result.mode = mode
|
||||
if linkname is not _KEEP:
|
||||
result.linkname = linkname
|
||||
if uid is not _KEEP:
|
||||
result.uid = uid
|
||||
if gid is not _KEEP:
|
||||
result.gid = gid
|
||||
if uname is not _KEEP:
|
||||
result.uname = uname
|
||||
if gname is not _KEEP:
|
||||
result.gname = gname
|
||||
return result
|
||||
|
||||
def get_info(self):
|
||||
"""Return the TarInfo's attributes as a dictionary.
|
||||
"""
|
||||
if self.mode is None:
|
||||
mode = None
|
||||
else:
|
||||
mode = self.mode & 0o7777
|
||||
info = {
|
||||
"name": self.name,
|
||||
"mode": self.mode & 0o7777,
|
||||
"mode": mode,
|
||||
"uid": self.uid,
|
||||
"gid": self.gid,
|
||||
"size": self.size,
|
||||
@@ -820,6 +983,9 @@ class TarInfo(object):
|
||||
"""Return a tar header as a string of 512 byte blocks.
|
||||
"""
|
||||
info = self.get_info()
|
||||
for name, value in info.items():
|
||||
if value is None:
|
||||
raise ValueError("%s may not be None" % name)
|
||||
|
||||
if format == USTAR_FORMAT:
|
||||
return self.create_ustar_header(info, encoding, errors)
|
||||
@@ -950,6 +1116,12 @@ class TarInfo(object):
|
||||
devmajor = stn("", 8, encoding, errors)
|
||||
devminor = stn("", 8, encoding, errors)
|
||||
|
||||
# None values in metadata should cause ValueError.
|
||||
# itn()/stn() do this for all fields except type.
|
||||
filetype = info.get("type", REGTYPE)
|
||||
if filetype is None:
|
||||
raise ValueError("TarInfo.type must not be None")
|
||||
|
||||
parts = [
|
||||
stn(info.get("name", ""), 100, encoding, errors),
|
||||
itn(info.get("mode", 0) & 0o7777, 8, format),
|
||||
@@ -958,7 +1130,7 @@ class TarInfo(object):
|
||||
itn(info.get("size", 0), 12, format),
|
||||
itn(info.get("mtime", 0), 12, format),
|
||||
b" ", # checksum field
|
||||
info.get("type", REGTYPE),
|
||||
filetype,
|
||||
stn(info.get("linkname", ""), 100, encoding, errors),
|
||||
info.get("magic", POSIX_MAGIC),
|
||||
stn(info.get("uname", ""), 32, encoding, errors),
|
||||
@@ -1264,11 +1436,7 @@ class TarInfo(object):
|
||||
# the newline. keyword and value are both UTF-8 encoded strings.
|
||||
regex = re.compile(br"(\d+) ([^=]+)=")
|
||||
pos = 0
|
||||
while True:
|
||||
match = regex.match(buf, pos)
|
||||
if not match:
|
||||
break
|
||||
|
||||
while match := regex.match(buf, pos):
|
||||
length, keyword = match.groups()
|
||||
length = int(length)
|
||||
if length == 0:
|
||||
@@ -1468,6 +1636,8 @@ class TarFile(object):
|
||||
|
||||
fileobject = ExFileObject # The file-object for extractfile().
|
||||
|
||||
extraction_filter = None # The default filter for extraction.
|
||||
|
||||
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,
|
||||
@@ -1659,7 +1829,9 @@ class TarFile(object):
|
||||
if filemode not in ("r", "w"):
|
||||
raise ValueError("mode must be 'r' or 'w'")
|
||||
|
||||
stream = _Stream(name, filemode, comptype, fileobj, bufsize)
|
||||
compresslevel = kwargs.pop("compresslevel", 9)
|
||||
stream = _Stream(name, filemode, comptype, fileobj, bufsize,
|
||||
compresslevel)
|
||||
try:
|
||||
t = cls(name, filemode, stream, **kwargs)
|
||||
except:
|
||||
@@ -1940,7 +2112,10 @@ class TarFile(object):
|
||||
members = self
|
||||
for tarinfo in members:
|
||||
if verbose:
|
||||
_safe_print(stat.filemode(tarinfo.mode))
|
||||
if tarinfo.mode is None:
|
||||
_safe_print("??????????")
|
||||
else:
|
||||
_safe_print(stat.filemode(tarinfo.mode))
|
||||
_safe_print("%s/%s" % (tarinfo.uname or tarinfo.uid,
|
||||
tarinfo.gname or tarinfo.gid))
|
||||
if tarinfo.ischr() or tarinfo.isblk():
|
||||
@@ -1948,8 +2123,11 @@ class TarFile(object):
|
||||
("%d,%d" % (tarinfo.devmajor, tarinfo.devminor)))
|
||||
else:
|
||||
_safe_print("%10d" % tarinfo.size)
|
||||
_safe_print("%d-%02d-%02d %02d:%02d:%02d" \
|
||||
% time.localtime(tarinfo.mtime)[:6])
|
||||
if tarinfo.mtime is None:
|
||||
_safe_print("????-??-?? ??:??:??")
|
||||
else:
|
||||
_safe_print("%d-%02d-%02d %02d:%02d:%02d" \
|
||||
% time.localtime(tarinfo.mtime)[:6])
|
||||
|
||||
_safe_print(tarinfo.name + ("/" if tarinfo.isdir() else ""))
|
||||
|
||||
@@ -2036,32 +2214,63 @@ class TarFile(object):
|
||||
|
||||
self.members.append(tarinfo)
|
||||
|
||||
def extractall(self, path=".", members=None, *, numeric_owner=False):
|
||||
def _get_filter_function(self, filter):
|
||||
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
|
||||
if isinstance(filter, str):
|
||||
raise TypeError(
|
||||
'String names are not supported for '
|
||||
+ 'TarFile.extraction_filter. Use a function such as '
|
||||
+ 'tarfile.data_filter directly.')
|
||||
return filter
|
||||
if callable(filter):
|
||||
return filter
|
||||
try:
|
||||
return _NAMED_FILTERS[filter]
|
||||
except KeyError:
|
||||
raise ValueError(f"filter {filter!r} not found") from None
|
||||
|
||||
def extractall(self, path=".", members=None, *, numeric_owner=False,
|
||||
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
|
||||
the numbers for user/group names are used and not the names.
|
||||
|
||||
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.
|
||||
"""
|
||||
directories = []
|
||||
|
||||
filter_function = self._get_filter_function(filter)
|
||||
if members is None:
|
||||
members = self
|
||||
|
||||
for tarinfo in members:
|
||||
for member in members:
|
||||
tarinfo = self._get_extract_tarinfo(member, filter_function, path)
|
||||
if tarinfo is None:
|
||||
continue
|
||||
if tarinfo.isdir():
|
||||
# Extract directories with a safe mode.
|
||||
# For directories, delay setting attributes until later,
|
||||
# since permissions can interfere with extraction and
|
||||
# extracting contents can reset mtime.
|
||||
directories.append(tarinfo)
|
||||
tarinfo = copy.copy(tarinfo)
|
||||
tarinfo.mode = 0o700
|
||||
# Do not set_attrs directories, as we will do that further down
|
||||
self.extract(tarinfo, path, set_attrs=not tarinfo.isdir(),
|
||||
numeric_owner=numeric_owner)
|
||||
self._extract_one(tarinfo, path, set_attrs=not tarinfo.isdir(),
|
||||
numeric_owner=numeric_owner)
|
||||
|
||||
# Reverse sort directories.
|
||||
directories.sort(key=lambda a: a.name)
|
||||
directories.reverse()
|
||||
directories.sort(key=lambda a: a.name, reverse=True)
|
||||
|
||||
# Set correct owner, mtime and filemode on directories.
|
||||
for tarinfo in directories:
|
||||
@@ -2071,12 +2280,10 @@ class TarFile(object):
|
||||
self.utime(tarinfo, dirpath)
|
||||
self.chmod(tarinfo, dirpath)
|
||||
except ExtractError as e:
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
self._handle_nonfatal_error(e)
|
||||
|
||||
def extract(self, member, path="", set_attrs=True, *, numeric_owner=False):
|
||||
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
|
||||
@@ -2084,35 +2291,70 @@ class TarFile(object):
|
||||
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.
|
||||
"""
|
||||
self._check("r")
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
unfiltered = tarinfo
|
||||
try:
|
||||
tarinfo = filter_function(tarinfo, path)
|
||||
except (OSError, FilterError) as e:
|
||||
self._handle_fatal_error(e)
|
||||
except ExtractError as e:
|
||||
self._handle_nonfatal_error(e)
|
||||
if tarinfo 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
|
||||
|
||||
def _extract_one(self, tarinfo, path, set_attrs, numeric_owner):
|
||||
"""Extract from filtered tarinfo to disk"""
|
||||
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:
|
||||
if self.errorlevel > 0:
|
||||
raise
|
||||
else:
|
||||
if e.filename is None:
|
||||
self._dbg(1, "tarfile: %s" % e.strerror)
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename))
|
||||
self._handle_fatal_error(e)
|
||||
except ExtractError as e:
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
self._handle_nonfatal_error(e)
|
||||
|
||||
def _handle_nonfatal_error(self, e):
|
||||
"""Handle non-fatal error (ExtractError) according to errorlevel"""
|
||||
if self.errorlevel > 1:
|
||||
raise
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
|
||||
def _handle_fatal_error(self, e):
|
||||
"""Handle "fatal" error according to self.errorlevel"""
|
||||
if self.errorlevel > 0:
|
||||
raise
|
||||
elif isinstance(e, OSError):
|
||||
if e.filename is None:
|
||||
self._dbg(1, "tarfile: %s" % e.strerror)
|
||||
else:
|
||||
self._dbg(1, "tarfile: %s" % e)
|
||||
self._dbg(1, "tarfile: %s %r" % (e.strerror, e.filename))
|
||||
else:
|
||||
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
|
||||
@@ -2199,11 +2441,16 @@ class TarFile(object):
|
||||
"""Make a directory called targetpath.
|
||||
"""
|
||||
try:
|
||||
# Use a safe mode for the directory, the real mode is set
|
||||
# later in _extract_member().
|
||||
os.mkdir(targetpath, 0o700)
|
||||
if tarinfo.mode is None:
|
||||
# Use the system's default mode
|
||||
os.mkdir(targetpath)
|
||||
else:
|
||||
# Use a safe mode for the directory, the real mode is set
|
||||
# later in _extract_member().
|
||||
os.mkdir(targetpath, 0o700)
|
||||
except FileExistsError:
|
||||
pass
|
||||
if not os.path.isdir(targetpath):
|
||||
raise
|
||||
|
||||
def makefile(self, tarinfo, targetpath):
|
||||
"""Make a file called targetpath.
|
||||
@@ -2244,6 +2491,9 @@ class TarFile(object):
|
||||
raise ExtractError("special devices not supported by system")
|
||||
|
||||
mode = tarinfo.mode
|
||||
if mode is None:
|
||||
# Use mknod's default
|
||||
mode = 0o600
|
||||
if tarinfo.isblk():
|
||||
mode |= stat.S_IFBLK
|
||||
else:
|
||||
@@ -2265,7 +2515,6 @@ class TarFile(object):
|
||||
os.unlink(targetpath)
|
||||
os.symlink(tarinfo.linkname, targetpath)
|
||||
else:
|
||||
# See extract().
|
||||
if os.path.exists(tarinfo._link_target):
|
||||
os.link(tarinfo._link_target, targetpath)
|
||||
else:
|
||||
@@ -2290,15 +2539,19 @@ class TarFile(object):
|
||||
u = tarinfo.uid
|
||||
if not numeric_owner:
|
||||
try:
|
||||
if grp:
|
||||
if grp and tarinfo.gname:
|
||||
g = grp.getgrnam(tarinfo.gname)[2]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if pwd:
|
||||
if pwd and tarinfo.uname:
|
||||
u = pwd.getpwnam(tarinfo.uname)[2]
|
||||
except KeyError:
|
||||
pass
|
||||
if g is None:
|
||||
g = -1
|
||||
if u is None:
|
||||
u = -1
|
||||
try:
|
||||
if tarinfo.issym() and hasattr(os, "lchown"):
|
||||
os.lchown(targetpath, u, g)
|
||||
@@ -2310,6 +2563,8 @@ class TarFile(object):
|
||||
def chmod(self, tarinfo, targetpath):
|
||||
"""Set file permissions of targetpath according to tarinfo.
|
||||
"""
|
||||
if tarinfo.mode is None:
|
||||
return
|
||||
try:
|
||||
os.chmod(targetpath, tarinfo.mode)
|
||||
except OSError as e:
|
||||
@@ -2318,10 +2573,13 @@ class TarFile(object):
|
||||
def utime(self, tarinfo, targetpath):
|
||||
"""Set modification time of targetpath according to tarinfo.
|
||||
"""
|
||||
mtime = tarinfo.mtime
|
||||
if mtime is None:
|
||||
return
|
||||
if not hasattr(os, 'utime'):
|
||||
return
|
||||
try:
|
||||
os.utime(targetpath, (tarinfo.mtime, tarinfo.mtime))
|
||||
os.utime(targetpath, (mtime, mtime))
|
||||
except OSError as e:
|
||||
raise ExtractError("could not change modification time") from e
|
||||
|
||||
@@ -2339,6 +2597,8 @@ class TarFile(object):
|
||||
|
||||
# Advance the file pointer.
|
||||
if self.offset != self.fileobj.tell():
|
||||
if self.offset == 0:
|
||||
return None
|
||||
self.fileobj.seek(self.offset - 1)
|
||||
if not self.fileobj.read(1):
|
||||
raise ReadError("unexpected end of data")
|
||||
@@ -2397,13 +2657,26 @@ class TarFile(object):
|
||||
members = self.getmembers()
|
||||
|
||||
# Limit the member search list up to tarinfo.
|
||||
skipping = False
|
||||
if tarinfo is not None:
|
||||
members = members[:members.index(tarinfo)]
|
||||
try:
|
||||
index = members.index(tarinfo)
|
||||
except ValueError:
|
||||
# The given starting point might be a (modified) copy.
|
||||
# We'll later skip members until we find an equivalent.
|
||||
skipping = True
|
||||
else:
|
||||
# Happy fast path
|
||||
members = members[:index]
|
||||
|
||||
if normalize:
|
||||
name = os.path.normpath(name)
|
||||
|
||||
for member in reversed(members):
|
||||
if skipping:
|
||||
if tarinfo.offset == member.offset:
|
||||
skipping = False
|
||||
continue
|
||||
if normalize:
|
||||
member_name = os.path.normpath(member.name)
|
||||
else:
|
||||
@@ -2412,14 +2685,16 @@ class TarFile(object):
|
||||
if name == member_name:
|
||||
return member
|
||||
|
||||
if skipping:
|
||||
# Starting point was not found
|
||||
raise ValueError(tarinfo)
|
||||
|
||||
def _load(self):
|
||||
"""Read through the entire archive file and look for readable
|
||||
members.
|
||||
"""
|
||||
while True:
|
||||
tarinfo = self.next()
|
||||
if tarinfo is None:
|
||||
break
|
||||
while self.next() is not None:
|
||||
pass
|
||||
self._loaded = True
|
||||
|
||||
def _check(self, mode=None):
|
||||
@@ -2504,6 +2779,7 @@ class TarFile(object):
|
||||
#--------------------
|
||||
# exported functions
|
||||
#--------------------
|
||||
|
||||
def is_tarfile(name):
|
||||
"""Return True if name points to a tar archive that we
|
||||
are able to handle, else return False.
|
||||
@@ -2512,7 +2788,9 @@ def is_tarfile(name):
|
||||
"""
|
||||
try:
|
||||
if hasattr(name, "read"):
|
||||
pos = name.tell()
|
||||
t = open(fileobj=name)
|
||||
name.seek(pos)
|
||||
else:
|
||||
t = open(name)
|
||||
t.close()
|
||||
@@ -2530,6 +2808,10 @@ def main():
|
||||
parser = argparse.ArgumentParser(description=description)
|
||||
parser.add_argument('-v', '--verbose', action='store_true', default=False,
|
||||
help='Verbose output')
|
||||
parser.add_argument('--filter', metavar='<filtername>',
|
||||
choices=_NAMED_FILTERS,
|
||||
help='Filter for extraction')
|
||||
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('-l', '--list', metavar='<tarfile>',
|
||||
help='Show listing of a tarfile')
|
||||
@@ -2541,8 +2823,12 @@ def main():
|
||||
help='Create tarfile from sources')
|
||||
group.add_argument('-t', '--test', metavar='<tarfile>',
|
||||
help='Test if a tarfile is valid')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.filter and args.extract is None:
|
||||
parser.exit(1, '--filter is only valid for extraction\n')
|
||||
|
||||
if args.test is not None:
|
||||
src = args.test
|
||||
if is_tarfile(src):
|
||||
@@ -2573,7 +2859,7 @@ def main():
|
||||
|
||||
if is_tarfile(src):
|
||||
with TarFile.open(src, 'r:*') as tf:
|
||||
tf.extractall(path=curdir)
|
||||
tf.extractall(path=curdir, filter=args.filter)
|
||||
if args.verbose:
|
||||
if curdir == '.':
|
||||
msg = '{!r} file is extracted.'.format(src)
|
||||
|
||||
4
Lib/test/test_shutil.py
vendored
4
Lib/test/test_shutil.py
vendored
@@ -2000,13 +2000,9 @@ class TestArchives(BaseTest, unittest.TestCase):
|
||||
('Python 3.14', DeprecationWarning)):
|
||||
self.check_unpack_archive(format)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_unpack_archive_tar(self):
|
||||
self.check_unpack_tarball('tar')
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
@support.requires_zlib()
|
||||
def test_unpack_archive_gztar(self):
|
||||
self.check_unpack_tarball('gztar')
|
||||
|
||||
Reference in New Issue
Block a user