Skip to content
Open
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
26 changes: 20 additions & 6 deletions src/commoncode/archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,30 @@
from os import path

from commoncode.system import on_windows
from commoncode.system import py314

"""
Mimimal tar and zip file handling, primarily for testing.
"""


def _extractall(tar, target_dir, members=None, filter=None):
"""
Extract `members` from `tar` in `target_dir`.

Always pass an explicit ``filter`` to ``tarfile.extractall()`` when this
is supported. Python 3.12 deprecated extracting without a filter and
Python 3.14 changes the default to ``data``, which would silently drop
permissions, owners, links and other metadata. Using ``fully_trusted``
keeps the historical behavior unless a caller requests another filter.
"""
filter = filter or "fully_trusted"
try:
tar.extractall(target_dir, members=members, filter=filter)
except TypeError:
# Python versions that do not support the ``filter`` argument
tar.extractall(target_dir, members=members)


def _extract_tar_raw(test_path, target_dir, to_bytes, *args, **kwargs):
"""
Raw simplified extract for certain really weird paths and file
Expand All @@ -29,7 +46,7 @@ def _extract_tar_raw(test_path, target_dir, to_bytes, *args, **kwargs):
tar = None
try:
tar = tarfile.open(test_path)
tar.extractall(path=target_dir)
_extractall(tar, target_dir)
finally:
if tar:
tar.close()
Expand Down Expand Up @@ -59,10 +76,7 @@ def extract_tar(location, target_dir, verbatim=False, filter=None, *args, **kwar
if not verbatim:
tarinfo.mode = 0o755
to_extract.append(tarinfo)
if py314 and filter:
tar.extractall(target_dir, members=to_extract, filter=filter)
else:
tar.extractall(target_dir, members=to_extract)
_extractall(tar, target_dir, members=to_extract, filter=filter)
finally:
if tar:
tar.close()
Expand Down
5 changes: 4 additions & 1 deletion src/commoncode/timeutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#

from datetime import datetime
from datetime import timezone
from datetime import tzinfo
from functools import update_wrapper
from functools import wraps
Expand Down Expand Up @@ -58,7 +59,9 @@ def time2tstamp(dt=None, path_safe=True):
to convert back to a datetime object.
"""
# TODO: check that the dt is effectively in UTC
datim = dt or datetime.utcnow()
# datetime.utcnow() is deprecated in Python 3.12+. Use a timezone-aware
# datetime and drop the timezone to keep returning a naive UTC datetime.
datim = dt or datetime.now(timezone.utc).replace(tzinfo=None)
iso = datim.isoformat()
if path_safe:
iso = iso.replace(":", "").replace("/", "_")
Expand Down
109 changes: 109 additions & 0 deletions tests/test_archive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/commoncode for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#

import io
import tarfile
import warnings

import pytest

from commoncode.archive import extract_tar
from commoncode.archive import extract_tar_raw


def make_tar(location, name="file.txt", content=b"hello"):
with tarfile.open(location, "w") as tar:
info = tarfile.TarInfo(name)
info.size = len(content)
tar.addfile(info, io.BytesIO(content))


def has_tar_deprecation_warning(caught):
return [
warning
for warning in caught
if issubclass(warning.category, DeprecationWarning)
and "tar archive" in str(warning.message).lower()
]


def test_extract_tar_does_not_emit_a_deprecation_warning(tmp_path):
archive = str(tmp_path / "test.tar")
make_tar(archive)
target_dir = str(tmp_path / "target")

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
extract_tar(archive, target_dir)

assert not has_tar_deprecation_warning(caught)
with open(str(tmp_path / "target" / "file.txt")) as extracted:
assert extracted.read() == "hello"


def test_extract_tar_raw_does_not_emit_a_deprecation_warning(tmp_path):
archive = str(tmp_path / "test.tar")
make_tar(archive)
target_dir = str(tmp_path / "target")

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
extract_tar_raw(archive, target_dir)

assert not has_tar_deprecation_warning(caught)
with open(str(tmp_path / "target" / "file.txt")) as extracted:
assert extracted.read() == "hello"


def test_extract_tar_honors_an_explicit_filter(tmp_path):
if not hasattr(tarfile, "FilterError"):
pytest.skip("tarfile extraction filters require Python 3.12+")

archive = str(tmp_path / "test.tar")
make_tar(archive, name="../escaped.txt")
target_dir = tmp_path / "target"
target_dir.mkdir()

# The "data" filter rejects members that would escape the target directory
try:
extract_tar(str(archive), str(target_dir), filter="data")
except tarfile.FilterError:
pass
else:
raise AssertionError("expected the 'data' filter to reject ../escaped.txt")

assert not (tmp_path / "escaped.txt").exists()


class LegacyTar:
"""
Simulate a tarfile.TarFile from a Python without tar extraction filters.
"""

def __init__(self):
self.calls = []

def extractall(self, path, members=None, **kwargs):
if "filter" in kwargs:
raise TypeError("TarFile.extractall() got an unexpected keyword argument 'filter'")
self.calls.append((path, members))


def test_extractall_falls_back_on_pythons_without_filters(tmp_path):
from commoncode.archive import _extractall

tar = LegacyTar()
target_dir = str(tmp_path)

_extractall(tar, target_dir, members=["member"], filter="data")
_extractall(tar, target_dir, members=["member"])

assert tar.calls == [
(target_dir, ["member"]),
(target_dir, ["member"]),
]
16 changes: 15 additions & 1 deletion tests/test_timeutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
# See https://aboutcode.org for more information about nexB OSS projects.
#

import warnings
from datetime import datetime
from datetime import timezone

from commoncode.testcase import FileBasedTesting
from commoncode.timeutils import UTC
Expand Down Expand Up @@ -34,11 +36,23 @@ def test_time2tstamp_raises_on_non_datetime(self):
self.assertRaises(AttributeError, time2tstamp, 1)

def test_time2tstamp_tstamp2time_is_idempotent(self):
dt = datetime.utcnow()
dt = datetime.now(timezone.utc).replace(tzinfo=None)
ts = time2tstamp(dt)
dt_from_ts = tstamp2time(ts)
assert dt_from_ts == dt

def test_time2tstamp_does_not_emit_deprecation_warnings(self):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
time2tstamp()

utcnow_warnings = [
str(warning.message)
for warning in caught
if issubclass(warning.category, DeprecationWarning) and "utcnow" in str(warning.message)
]
assert not utcnow_warnings

def test_tstamp2time_format(self):
import re

Expand Down