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
17 changes: 9 additions & 8 deletions src/poetry/utils/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -350,14 +350,15 @@ def _get_filtered_attrs(
if mode != member.mode:
new_attrs["mode"] = mode
# 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
# Unlike newer versions, TarFile.chown() of older Python
# versions does not tolerate None here: it passes the ids
# straight to os.chown() and looks the names up with
# pwd/grp, catching only KeyError. Use values that are
# accepted and resolve to "leave ownership alone".
new_attrs["uid"] = -1
new_attrs["gid"] = -1
new_attrs["uname"] = ""
new_attrs["gname"] = ""
# Check link destination for 'data'
if member.islnk() or member.issym():
if os.path.isabs(member.linkname):
Expand Down
62 changes: 62 additions & 0 deletions tests/utils/test_helpers.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import contextlib
import sys
import tarfile

from pathlib import Path
Expand Down Expand Up @@ -223,6 +224,67 @@ def test_extractall_sdist_no_path_traversal(
assert (dest / target.as_posix().lstrip("/")).exists()


def test_extractall_sdist_old_python_owner_filter(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The fallback filter must not hand None to old TarFile.chown()."""
import io

archive = tmp_path / "owner.tar.gz"
dest = tmp_path / "dest"
dest.mkdir()
with tarfile.open(archive, "w:gz") as tar:
member = tarfile.TarInfo("file.txt")
member.size = 4
tar.addfile(member, io.BytesIO(b"data"))

# Take the fallback path and skip the stdlib filter, as on old Python.
monkeypatch.setattr(sys, "version_info", (3, 10, 12))
monkeypatch.setattr(
tarfile.TarFile,
"_get_filter_function",
lambda self, filter: lambda member, dest_path: member,
raising=False,
)

original_getmembers = tarfile.TarFile.getmembers

def unowned_members(self: tarfile.TarFile) -> list[tarfile.TarInfo]:
members = original_getmembers(self)
for member in members:
for attr in ("uid", "gid", "uname", "gname"):
# object.__setattr__ because the declared types are not optional
object.__setattr__(member, attr, None)
return members

monkeypatch.setattr(tarfile.TarFile, "getmembers", unowned_members)

def lookup(name: str) -> int:
"""Behaves like pwd.getpwnam()/grp.getgrnam() for an unknown name."""
if not isinstance(name, str):
raise TypeError(f"argument must be str, not {type(name).__name__}")
raise KeyError(name)

chown_args: list[tuple[int, int, str, str]] = []

def old_chown(
self: tarfile.TarFile, member: tarfile.TarInfo, target: str, numeric_owner: bool
) -> None:
# Old TarFile.chown() resolved the names and caught only KeyError.
with contextlib.suppress(KeyError):
lookup(member.gname)
with contextlib.suppress(KeyError):
lookup(member.uname)
chown_args.append((member.uid, member.gid, member.uname, member.gname))

monkeypatch.setattr(tarfile.TarFile, "chown", old_chown)

extractall(source=archive, dest=dest, zip=False)

assert (dest / "file.txt").read_bytes() == b"data"
assert chown_args == [(-1, -1, "", "")]


@pytest.mark.parametrize("link_type", [tarfile.SYMTYPE, tarfile.LNKTYPE])
@pytest.mark.parametrize("relative", [False, True])
@pytest.mark.parametrize("existing", [False, True])
Expand Down
Loading