Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Doc/library/tarfile.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,10 @@ reused in custom filters:
paths (in case the name is absolute
even after stripping slashes, e.g. ``C:/foo`` on Windows).
This raises :class:`~tarfile.AbsolutePathError`.
- Normalize filenames (:attr:`TarInfo.name`) that contain ``..`` components
using :func:`os.path.normpath`.
Note that this removes internal ``..`` components, which may change the
meaning of the name if it traverses symbolic links.
- :ref:`Refuse <tarfile-extraction-refuse>` to extract files whose absolute
path (after following symlinks) would end up outside the destination.
This raises :class:`~tarfile.OutsideDestinationError`.
Expand All @@ -1120,6 +1124,10 @@ reused in custom filters:

Return the modified ``TarInfo`` member.

.. versionchanged:: next

Filenames containing ``..`` components are now normalized.

.. function:: data_filter(member, path)

Implements the ``'data'`` filter.
Expand Down
7 changes: 7 additions & 0 deletions Lib/tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -833,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:
Expand Down
25 changes: 23 additions & 2 deletions Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1271,15 +1271,20 @@ def set_memlimit(limit: str) -> None:


def _memory_watchdog(pid):
"""Return a function printing the memory usage of process *pid*."""
"""Return a function printing the memory usage of process *pid*.

The largest value it saw is kept in its ``peak`` attribute.
"""
# Imported here: test.support does not depend on test.libregrtest.
from test.libregrtest.utils import get_process_memory_usage

def watch():
mem = get_process_memory_usage(pid)
if mem is not None:
watch.peak = max(watch.peak, mem)
print(f" ... process data size: {mem / (1024 ** 3):.1f} GiB",
flush=True)
watch.peak = 0
return watch


Expand Down Expand Up @@ -1333,7 +1338,23 @@ def wrapper(self):
qualname = f'{cls.__qualname__}.{f.__name__}'
proc = isolation._start_test(cls.__module__, qualname)
watchdog = _memory_watchdog(proc.pid) if verbose else None
isolation._replay_test(self, *proc.wait(tick=watchdog))
payload, output, returncode = proc.wait(tick=watchdog)
if watchdog:
# The subprocess measures its own peak exactly. What the
# parent sampled is only a lower bound.
maxrss = payload and payload.get('maxrss')
peak = maxrss or watchdog.peak
if peak:
print(f" ... peak memory use: "
f"{peak / (1024 ** 3):.1f} GiB"
f"{'' if maxrss else ' or more'}", flush=True)
majflt = payload and payload.get('majflt')
if majflt:
# The test did not fit in memory, so its timing means
# little.
print(f" ... {majflt} major page faults: the test "
f"waited for the disk", flush=True)
isolation._replay_test(self, payload, output, returncode)
return

return f(self, maxsize)
Expand Down
40 changes: 39 additions & 1 deletion Lib/test/support/subprocess_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,45 @@ def _outcome(kind, test, detail):
for t, tb in result.expectedFailures]
outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped]

payload = {'outcomes': outcomes, 'durations': result.id_durations}
def _usage():
"""What this process used: peak resident set size in bytes, and the
number of major page faults it took, either of which can be None.

A major page fault is served from disk, so a non-zero count means swapping.

The modules are imported here, after the test has run, so that the test
does not see them.
"""
try:
import resource
except ImportError:
pass
else:
usage = resource.getrusage(resource.RUSAGE_SELF)
# Solaris and illumos leave these fields at 0, which no live process
# has, so treat it as "not supported".
if not usage.ru_maxrss:
return {'maxrss': None, 'majflt': None}
# ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs.
maxrss = usage.ru_maxrss
return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024,
'majflt': usage.ru_majflt}
try:
import os
import _winapi
handle = _winapi.OpenProcess(
_winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid())
except (ImportError, OSError):
return {'maxrss': None, 'majflt': None}
try:
info = _winapi.GetProcessMemoryInfo(handle)
finally:
_winapi.CloseHandle(handle)
# PageFaultCount counts all faults, not only the ones served from disk.
return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None}


payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()}
with open(outfile, 'wb') as f:
marshal.dump(payload, f)

Expand Down
173 changes: 172 additions & 1 deletion Lib/test/test_clinic.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ def test_ambiguous_group_and_optional_parameters(self):
/
[clinic start generated code]*/
"""
self.expect_failure(block, err)
self.expect_failure(block, err, lineno=2)

def test_star_after_vararg(self):
err = "'my_test_func' uses '*' more than once."
Expand Down Expand Up @@ -3063,9 +3063,22 @@ def test_state_func_docstring_no_summary(self):
m.func
docstring1
docstring2
docstring3
"""
# The line which should have been left blank.
self.expect_failure(block, err, lineno=3)

def test_state_func_docstring_long_summary(self):
err = "Summary line for 'm.func' is too long!"
block = f"""
module m
m.func
{'x' * 100}

Body.
"""
self.expect_failure(block, err, lineno=2)

def test_state_func_docstring_only_one_param_template(self):
err = "You may not specify {parameters} more than once in a docstring!"
block = """
Expand All @@ -3077,6 +3090,7 @@ def test_state_func_docstring_only_one_param_template(self):
{parameters}
these are the params again:
{parameters}
and this is the end of the docstring
"""
self.expect_failure(block, err, lineno=7)

Expand Down Expand Up @@ -3667,6 +3681,163 @@ def test_cli_converters_no_converters(self):
f.write("/*[clinic input]\n[clinic start generated code]*/\n")
self.assertEqual(self.expect_success("--converters", fn), "")

LIST_CODE = dedent("""
/*[clinic input]
func
a: int
/

Docstring.
[clinic start generated code]*/

/*[clinic input]
cloned = func
[clinic start generated code]*/

/*[clinic input]
module m
class m.C "void *" ""
class m.C.D "void *" ""
[clinic start generated code]*/

/*[clinic input]
m.C.meth
self: self(type="void *")
a: object
[
b: object
]
/

Docstring.
[clinic start generated code]*/

/*[clinic input]
@classmethod
m.C.__new__
a: object

Docstring.
[clinic start generated code]*/

/*[clinic input]
@getter
m.C.prop
[clinic start generated code]*/

/*[clinic input]
@setter
m.C.prop
[clinic start generated code]*/

/*[clinic input]
m.C.D.meth
self: self(type="void *")

Docstring.
[clinic start generated code]*/
""")

def make_list_file(self, tmp_dir):
fn = os.path.join(tmp_dir, "test.c")
with open(fn, "w", encoding="utf-8") as f:
f.write(self.LIST_CODE)
return fn

LIST_OUTPUT = [
" func($module, a, /)",
" cloned($module, a, /)",
" module m",
" class m.C",
# A signature with an option group is only for the docstring.
" m.C.meth(a, [b])",
" m.C(a)",
" getter m.C.prop",
" setter m.C.prop",
" class m.C.D",
" m.C.D.meth($self, /)",
]

def test_cli_list(self):
with os_helper.temp_dir() as tmp_dir:
fn = self.make_list_file(tmp_dir)
pre_mtime = os.stat(fn).st_mtime_ns
out = self.expect_success("--list", fn)
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
# Nothing is written.
with open(fn, encoding="utf-8") as f:
self.assertEqual(f.read(), self.LIST_CODE)
self.assertEqual(os.stat(fn).st_mtime_ns, pre_mtime)
self.assertEqual(os.listdir(tmp_dir), ["test.c"])

def test_cli_list_no_clinic_block(self):
with os_helper.temp_dir() as tmp_dir:
fn = os.path.join(tmp_dir, "test.c")
with open(fn, "w", encoding="utf-8") as f:
f.write("int x;\n")
self.assertEqual(self.expect_success("--list", fn), "")

def test_cli_list_no_definitions(self):
with os_helper.temp_dir() as tmp_dir:
fn = os.path.join(tmp_dir, "test.c")
with open(fn, "w", encoding="utf-8") as f:
f.write("/*[clinic input]\n[clinic start generated code]*/\n")
self.assertEqual(self.expect_success("--list", fn), "")

def test_cli_list_make(self):
with os_helper.temp_dir() as tmp_dir:
fn = self.make_list_file(tmp_dir)
out = self.expect_success("--list", "--make", "--srcdir", tmp_dir)
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
self.assertEqual(os.listdir(tmp_dir), ["test.c"])

def test_cli_list_verbose(self):
with os_helper.temp_dir() as tmp_dir:
fn = self.make_list_file(tmp_dir)
# The progress does not mix with the report.
out, err, code = self.run_clinic("-v", "--list", fn)
self.assertEqual(code, 0)
self.assertEqual(err.splitlines(), [fn])
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)

def test_cli_list_checksum_mismatch(self):
with os_helper.temp_dir() as tmp_dir:
fn = self.make_list_file(tmp_dir)
with open(fn, "a", encoding="utf-8") as f:
f.write("/*[clinic end generated code: "
"output=0123456789abcdef input=fedcba9876543210]*/\n")
_, err = self.expect_failure("--list", fn)
self.assertIn("Checksum mismatch!", err)
# The check is skipped with --force.
out = self.expect_success("-f", "--list", fn)
self.assertEqual(out.splitlines(), [fn] + self.LIST_OUTPUT)
self.assertEqual(os.listdir(tmp_dir), ["test.c"])

def test_cli_list_external(self):
# A file which uses getters, setters and nested classes.
source = support.findfile('clinic.test.c')
out = self.expect_success("--list", source)
lines = out.splitlines()
self.assertEqual(lines[0], source)
for line in (" class Test",
" getter Test.property",
" setter Test.property",
" Test.class_method($type, /)",
" module m",
" class m.T"):
with self.subTest(line=line):
self.assertIn(line, lines)

def test_cli_fail_list_and_dry_run(self):
for opt in "--dry-run", "--diff":
with self.subTest(opt=opt):
_, err = self.expect_failure("--list", opt, "test.c")
self.assertIn("can't use --dry-run or --diff with --list", err)

def test_cli_fail_list_and_converters(self):
_, err = self.expect_failure("--list", "--converters", "test.c")
self.assertIn("can't use --converters with --list", err)

def test_cli_fail_directory(self):
with os_helper.temp_dir() as tmp_dir:
subdir = os.path.join(tmp_dir, "test.c")
Expand Down
14 changes: 14 additions & 0 deletions Lib/test/test_tarfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -4092,6 +4092,20 @@ def test_absolute(self):
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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update Android builds to SQLite 3.53.4. Additionally, enable the ``median()``
and ``percentile()`` functions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Fix the :mod:`tarfile` ``tar`` and ``data`` extraction filters creating
directories outside the destination for members whose name leaves the
destination and returns to it, such as ``../evil/../dest/sub/file``. The
containment check used the resolved path, but intermediate directories were
created from the name as given.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
In verbose mode, a test decorated with :func:`~test.support.bigmemtest` now
reports how much memory it really used, and the number of major page faults
it took, if any.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Argument Clinic: report errors on the offending line.
Errors in a docstring were reported on the line which ends the block, and
errors detected when generating the code were reported without any position.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Add the ``--list`` option to Argument Clinic.
It prints the modules, classes and functions which Argument Clinic defines in
the specified files, each function with its signature.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Update Windows builds to SQLite 3.53.4. Additionally, enable the ``median()``
and ``percentile()`` functions.
Loading
Loading