diff --git a/dev_tools/pylibs/edgacknowledg/client/test/__init__.py b/dev_tools/pylibs/edgacknowledg/client/test/__init__.py index caacc92693..e3a53770ca 100644 --- a/dev_tools/pylibs/edgacknowledg/client/test/__init__.py +++ b/dev_tools/pylibs/edgacknowledg/client/test/__init__.py @@ -55,6 +55,16 @@ __all__ = ['IllegalFileAccessError'] +def _run_test_output( + path: Path, max_rt_entries: Optional[int], + max_rt_member_bytes: Optional[int]) -> edgtest.runtest.RunTestOutput: + return edgtest.runtest.RunTestOutput( + path, + max_entries = max_rt_entries, + max_member_bytes = max_rt_member_bytes + ) + + def _rt_path_for(config_dir: ConfinedPath, test_id: str) -> ConfinedPath: return config_dir / str(PurePosixPath(test_id).with_suffix( edgtest.runtest.RUN_TEST_OUTPUT_SUFFIX @@ -62,7 +72,9 @@ def _rt_path_for(config_dir: ConfinedPath, test_id: str) -> ConfinedPath: def _create_status_dict(elog_file_paths: List[ConfinedPath], *, - is_multi_config_run: bool) -> _StatusDict: + is_multi_config_run: bool, + max_rt_entries: Optional[int], + max_rt_member_bytes: Optional[int]) -> _StatusDict: # test_id -> config_name -> combined_diff_hash. pending: Dict[str, Dict[str, str]] = {} lines_by_test_config: Dict[str, Dict[str, List[Dict[str, str]]]] = {} @@ -85,7 +97,11 @@ def _create_status_dict(elog_file_paths: List[ConfinedPath], *, if config_name not in pending[key]: rt_file_path = _rt_path_for(elog_file_path.parent, key) pending[key][config_name] = ( - edgtest.runtest.grouping_combined_diff_hash(rt_file_path.path) + edgtest.runtest.grouping_combined_diff_hash( + rt_file_path.path, + max_entries = max_rt_entries, + max_member_bytes = max_rt_member_bytes + ) ) lines_by_test_config[key][config_name] = [] @@ -183,7 +199,9 @@ async def run_test_source_handler(channel: Channel, context: SandboxContext, test_home: ConfinedPath, run_dir: ConfinedPath, - changes_files: Dict[str, ConfinedPath] + changes_files: Dict[str, ConfinedPath], *, + max_rt_entries: Optional[int] = None, + max_rt_member_bytes: Optional[int] = None ) -> None: '''Handle one test source request on ``channel``. Every file is reached from ``test_home`` or ``run_dir``, so it can't leave them, and commands @@ -222,7 +240,9 @@ def test_id_to_path(test_id: str) -> ConfinedPath: 'name': 'Changes List', 'statuses': _create_status_dict( _matching_change_files(changes_files, 'changes.elog'), - is_multi_config_run = is_multi_config_run + is_multi_config_run = is_multi_config_run, + max_rt_entries = max_rt_entries, + max_rt_member_bytes = max_rt_member_bytes ) } ) @@ -237,7 +257,9 @@ def test_id_to_path(test_id: str) -> ConfinedPath: 'name': 'Regressions List', 'statuses': _create_status_dict( regress_files, - is_multi_config_run = is_multi_config_run + is_multi_config_run = is_multi_config_run, + max_rt_entries = max_rt_entries, + max_rt_member_bytes = max_rt_member_bytes ) } ) @@ -252,7 +274,9 @@ def test_id_to_path(test_id: str) -> ConfinedPath: 'name': 'Improvements List', 'statuses': _create_status_dict( improve_files, - is_multi_config_run = is_multi_config_run + is_multi_config_run = is_multi_config_run, + max_rt_entries = max_rt_entries, + max_rt_member_bytes = max_rt_member_bytes ) } ) @@ -314,7 +338,8 @@ async def form_curr_diff_text(config_name: str, tmp_dir = Path(tmp_dir_name) entries: List[Tuple[Dict[str, Any], List[str]]] = [] - with edgtest.runtest.RunTestOutput(case_rt_path.path) as rt_output: + with _run_test_output(case_rt_path.path, max_rt_entries, + max_rt_member_bytes) as rt_output: for case_number, case in enumerate( rt_output.metadata.get('cases', []), start = 1 @@ -413,14 +438,16 @@ async def form_curr_diff_text(config_name: str, ) if request_tag == RequestTags.TEST_SHOW_DIFF: - with edgtest.runtest.RunTestOutput(rt_file_path.path) as rt_output: + with _run_test_output(rt_file_path.path, max_rt_entries, + max_rt_member_bytes) as rt_output: static_text = ''.join(rt_output.combined_static_diff()) client_response = { 'id': configured_test_id, **trunc_display_content(static_text) } elif request_tag == RequestTags.TEST_SHOW_RAW_OUTPUT: - with edgtest.runtest.RunTestOutput(rt_file_path.path) as rt_output: + with _run_test_output(rt_file_path.path, max_rt_entries, + max_rt_member_bytes) as rt_output: raw_output = ''.join(rt_output.combined_raw_output()) client_response = { 'id': configured_test_id, @@ -602,7 +629,9 @@ def __init__(self, slug: str, context: SandboxContext, test_home: ConfinedPath, - root: ConfinedPath) -> None: + root: ConfinedPath, *, + max_rt_entries: Optional[int] = None, + max_rt_member_bytes: Optional[int] = None) -> None: '''Requests reach files only through ``test_home`` and the run directory ``root``, and run commands in ``context``. ''' @@ -611,6 +640,8 @@ def __init__(self, self._context = context self._test_home = test_home self._root = root + self._max_rt_entries = max_rt_entries + self._max_rt_member_bytes = max_rt_member_bytes def _changes_files(self) -> Dict[str, ConfinedPath]: '''Each configuration's changes file in the run, found now.''' @@ -638,7 +669,9 @@ async def handle_request(self, channel: Channel) -> None: self._context, self._test_home, self._root, - await edgutil.async_to_thread(self._changes_files) + await edgutil.async_to_thread(self._changes_files), + max_rt_entries = self._max_rt_entries, + max_rt_member_bytes = self._max_rt_member_bytes ) async def handle_test_edit_request( diff --git a/dev_tools/pylibs/edgacknowledg/forge/config.py b/dev_tools/pylibs/edgacknowledg/forge/config.py index 07485371f1..0ace242519 100644 --- a/dev_tools/pylibs/edgacknowledg/forge/config.py +++ b/dev_tools/pylibs/edgacknowledg/forge/config.py @@ -34,10 +34,10 @@ # The largest diff (of tests/, in bytes) a pull request may have: it is # held in memory, and copied and split into lines more than once. DEFAULT_MAX_DIFF_BYTES = 64 * 1024 * 1024 -# The most entries an artifact may have (its zip's, and its tarballs' -# together), and the largest its zip's central directory may be: zipfile -# reads all of that directory into memory, before we can count. CI's test -# results reach about 170,000 entries, of about 106 bytes each there. +# The most entries an artifact zip may have, and the largest its central +# directory may be: zipfile reads all of that directory into memory, before +# we can count. CI's test results reach about 170,000 entries, of about 106 +# bytes each there. DEFAULT_MAX_ARTIFACT_ENTRIES = 500_000 DEFAULT_MAX_ARTIFACT_DIRECTORY_BYTES = 64 * 1024 * 1024 diff --git a/dev_tools/pylibs/edgacknowledg/forge/publisher/pool_process.py b/dev_tools/pylibs/edgacknowledg/forge/publisher/pool_process.py index 7d7d40670e..95c48d1318 100644 --- a/dev_tools/pylibs/edgacknowledg/forge/publisher/pool_process.py +++ b/dev_tools/pylibs/edgacknowledg/forge/publisher/pool_process.py @@ -51,6 +51,8 @@ # Parallel cg_annotate runs in one process (it answers one request at a # time, so this only matters to a request running several). _BENCH_WORKERS = 2 +_MAX_RT_ENTRIES = 10_000 +_MAX_RT_MEMBER_BYTES = 64 * 1024 * 1024 def build_handler(section: SectionRecord, @@ -67,7 +69,9 @@ def build_handler(section: SectionRecord, section.slug, context, context.subpath(section.home), - context.subpath(section.root) + context.subpath(section.root), + max_rt_entries = _MAX_RT_ENTRIES, + max_rt_member_bytes = _MAX_RT_MEMBER_BYTES ) if section.kind == KIND_BENCH: session = BenchReviewSession( diff --git a/dev_tools/pylibs/edgacknowledg/forge/workspace.py b/dev_tools/pylibs/edgacknowledg/forge/workspace.py index 81b0634f77..1571aca917 100644 --- a/dev_tools/pylibs/edgacknowledg/forge/workspace.py +++ b/dev_tools/pylibs/edgacknowledg/forge/workspace.py @@ -33,7 +33,6 @@ import shutil import stat import struct -import tarfile import urllib.parse import zipfile @@ -348,22 +347,6 @@ def _central_directory_size(zip_path: Path) -> Optional[int]: return directory_size -def _check_inner_tarball(path: Path, budget: _Budget) -> None: - '''Charge a (gzipped) tarball's decompressed size and entries to - ``budget``; reviewers' requests decompress these, so they're - bombs-in-waiting otherwise. - ''' - try: - with tarfile.open(path, 'r:*') as archive: - for member in archive: - budget.count_entry() - if not (member.isfile() or member.isdir()): - raise UnsafeArchiveError(f"{path.name} contains a link or device") - budget.spend(member.size) - except tarfile.TarError as ex: - raise UnsafeArchiveError(f"{path.name} is not a valid tarball") from ex - - def extract_artifact( zip_path: Path, destination: Path, *, max_bytes: int, max_entries: int = DEFAULT_MAX_ARTIFACT_ENTRIES, @@ -372,8 +355,8 @@ def extract_artifact( Rejects absolute or escaping paths and symlinks, and a zip whose central directory is over ``max_directory_bytes``, and stops once more than - ``max_bytes`` would be written, or ``max_entries`` entries made (counting - inner tarballs' contents too). + ``max_bytes`` would be written, or ``max_entries`` zip entries made. + Inner tarballs are checked when opened by a reviewer. ''' directory_size = _central_directory_size(zip_path) if directory_size is not None and directory_size > max_directory_bytes: @@ -382,7 +365,6 @@ def extract_artifact( f"({ENV_PREFIX}MAX_ARTIFACT_DIRECTORY_BYTES)" ) budget = _Budget(max_bytes, max_entries) - tarballs = [] try: with zipfile.ZipFile(zip_path) as archive: for info in archive.infolist(): @@ -407,14 +389,9 @@ def extract_artifact( break budget.spend(len(chunk)) sink.write(chunk) - if target.name.endswith(('.tar.gz', '.tgz', '.tar')): - tarballs.append(target) except zipfile.BadZipFile as ex: raise UnsafeArchiveError('artifact is not a valid zip archive') from ex - for tarball in tarballs: - _check_inner_tarball(tarball, budget) - class Workspaces: '''Clones and worktrees under ``home`` for one forge; a diff (``diff``) diff --git a/dev_tools/pylibs/edgtest/runtest/__init__.py b/dev_tools/pylibs/edgtest/runtest/__init__.py index 0308d4b651..1a22a0c1dc 100644 --- a/dev_tools/pylibs/edgtest/runtest/__init__.py +++ b/dev_tools/pylibs/edgtest/runtest/__init__.py @@ -958,12 +958,18 @@ def _unlink_missing_ok(path: Path) -> None: elif path.exists(): path.unlink() -def read_combined_diff_hash(rt_file_path: Path) -> Optional[str]: +def read_combined_diff_hash(rt_file_path: Path, *, + max_entries: Optional[int] = None, + max_member_bytes: Optional[int] = None + ) -> Optional[str]: '''Return metadata.combined_diff_hash, or None when there is no diff.''' - with RunTestOutput(rt_file_path) as rt_output: + with RunTestOutput(rt_file_path, max_entries = max_entries, + max_member_bytes = max_member_bytes) as rt_output: return rt_output.metadata.get('combined_diff_hash') -def grouping_combined_diff_hash(rt_file_path: Path) -> str: +def grouping_combined_diff_hash(rt_file_path: Path, *, + max_entries: Optional[int] = None, + max_member_bytes: Optional[int] = None) -> str: '''Return a combined diff hash suitable for grouping configs. Missing .rt.tar files use the sentinel "missing". Archives with no @@ -971,7 +977,10 @@ def grouping_combined_diff_hash(rt_file_path: Path) -> str: ''' if not rt_file_path.exists(): return 'missing' - combined = read_combined_diff_hash(rt_file_path) + combined = read_combined_diff_hash( + rt_file_path, max_entries = max_entries, + max_member_bytes = max_member_bytes + ) if combined is None: return hash_diff_bytes(b'') return combined @@ -1320,8 +1329,11 @@ def _read_tar_lines_member(tar: tarfile.TarFile, class RunTestOutput: '''Context manager for reading a structured .rt.tar run artifact once.''' - def __init__(self, path: Path) -> None: + def __init__(self, path: Path, *, max_entries: Optional[int] = None, + max_member_bytes: Optional[int] = None) -> None: self.path = path + self._max_entries = max_entries + self._max_member_bytes = max_member_bytes self._tar: Optional[tarfile.TarFile] = None self._metadata: Optional[Dict[str, Any]] = None self._member_names: Optional[Set[str]] = None @@ -1332,6 +1344,22 @@ def __enter__(self) -> 'RunTestOutput': 'r:gz' if RT_TAR_GZ_SUPPORTED else 'r', format = tarfile.PAX_FORMAT ) + try: + if self._max_entries is not None or self._max_member_bytes is not None: + for count, member in enumerate(self._tar, start = 1): + if self._max_entries is not None and count > self._max_entries: + raise ValueError('run artifact has too many entries') + if not (member.isfile() or member.isdir()): + raise RunTestOutputLinkError( + f"{member.name} is a link or device" + ) + if (self._max_member_bytes is not None and + member.size > self._max_member_bytes): + raise ValueError(f"run artifact member {member.name} is too large") + except BaseException: + self._tar.close() + self._tar = None + raise return self def __exit__(self, _type: Any, _value: Any, _tb: Any) -> None: diff --git a/dev_tools/services/acknowledg-forge/README.md b/dev_tools/services/acknowledg-forge/README.md index 1568da7961..1e596424cc 100644 --- a/dev_tools/services/acknowledg-forge/README.md +++ b/dev_tools/services/acknowledg-forge/README.md @@ -111,8 +111,8 @@ from the environment variable shown as a fallback. | `ACKNOWLEDG_FORGE_COMMENTS` | `0` | `1` enables comments (needs the forge token) | | `ACKNOWLEDG_FORGE_SETTLE_TIMEOUT` | `7200` | Seconds after a push to stop waiting for CI before commenting | | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_BYTES` | 256 MiB | Largest artifact download | -| `ACKNOWLEDG_FORGE_MAX_EXTRACTED_BYTES` | 2 GiB | Largest extracted artifact (including inner tarballs) | -| `ACKNOWLEDG_FORGE_MAX_ARTIFACT_ENTRIES` | `500000` | Most entries in an artifact (including inner tarballs'). CI's test results reach about 170,000 | +| `ACKNOWLEDG_FORGE_MAX_EXTRACTED_BYTES` | 2 GiB | Largest extracted artifact (including stored, compressed inner tarballs, but not their unextracted contents) | +| `ACKNOWLEDG_FORGE_MAX_ARTIFACT_ENTRIES` | `500000` | Most ZIP entries in an artifact. Inner tarballs are checked when read | | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_DIRECTORY_BYTES` | 64 MiB | Largest artifact zip central directory, which is read into memory whole (about 106 bytes per entry in CI's test results) | | `ACKNOWLEDG_FORGE_MAX_DIFF_BYTES` | 64 MiB | Largest diff of a pull request's `tests/`, which is held in memory and copied more than once: raising it may need a higher `mem_limit` for the service ([`compose.yaml`](compose.yaml)) | | `ACKNOWLEDG_FORGE_MAX_SPARSE_PATTERNS` | `20000` | Most sparse-checkout patterns in a review's worktree (up to two per test). Checking out takes time proportional to patterns times paths | diff --git a/dev_tools/services/acknowledg-forge/SANDBOXING.md b/dev_tools/services/acknowledg-forge/SANDBOXING.md index c89d1401a9..00ee19c935 100644 --- a/dev_tools/services/acknowledg-forge/SANDBOXING.md +++ b/dev_tools/services/acknowledg-forge/SANDBOXING.md @@ -279,7 +279,7 @@ sequenceDiagram Note over S: check out its merge-base,
symlinks as plain files Note over S: check_plain_tree(worktree) S->>F: download the CI artifact (size-capped) - Note over S: extract it: no links, no escaping paths,
inner tarballs size-capped + Note over S: extract it: no links or escaping paths,
stored inner tarballs size-capped S->>B: edg-rematerialize-test-run diff Note over B: worktree read-only,
build-staging writable,
time-limited B-->>S: output, exit status @@ -512,9 +512,9 @@ them. Several layers keep the service from reading through one: writes, which also may not hold hard links. - The section's directories, including the bench baselines a pull request supplies, must be real directories. -- CI artifacts may not contain links or escaping paths, nor may the - `.rt.tar.gz` files inside them. Reading those tarballs later refuses link - members as well. +- CI artifact ZIPs may not contain links or escaping paths. When a reviewer + opens a `.rt.tar.gz` file inside one, its entries are checked for links + and devices, excessive counts, and oversized members. - The handlers open files through `ConfinedPath`, which opens each path component with `O_NOFOLLOW` relative to the last, so a symlink anywhere on the way is refused, even one created after a check. A refused path @@ -525,8 +525,10 @@ them. Several layers keep the service from reading through one: | Resource | Limit | | --- | --- | -| Artifact download | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_BYTES` (256 MiB), also counting the decompressed size of inner tarballs | -| Artifact entries | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_ENTRIES` (500,000), counting inner tarballs' too; and its zip's central directory, `ACKNOWLEDG_FORGE_MAX_ARTIFACT_DIRECTORY_BYTES` (64 MiB), checked before zipfile reads it | +| Artifact download | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_BYTES` (256 MiB) | +| Extracted artifact | `ACKNOWLEDG_FORGE_MAX_EXTRACTED_BYTES` (2 GiB), counting the stored size of inner tarballs, not their unextracted contents | +| Artifact entries | `ACKNOWLEDG_FORGE_MAX_ARTIFACT_ENTRIES` (500,000) for ZIP entries, 10,000 per inner tarball when opened; and its zip's central directory, `ACKNOWLEDG_FORGE_MAX_ARTIFACT_DIRECTORY_BYTES` (64 MiB), checked before zipfile reads it | +| Inner tarball member | 64 MiB per member when a reviewer opens a tarball | | A pull request's diff of `tests/` | `ACKNOWLEDG_FORGE_MAX_DIFF_BYTES` (64 MiB) | | A review worktree's sparse-checkout patterns | `ACKNOWLEDG_FORGE_MAX_SPARSE_PATTERNS` (20,000) | | A file read for a reviewer | 64 MiB; a larger one fails the request | diff --git a/dev_tools/services/acknowledg-forge/tests/test_publisher.py b/dev_tools/services/acknowledg-forge/tests/test_publisher.py index 1fd14a5bbc..5978f23b5f 100644 --- a/dev_tools/services/acknowledg-forge/tests/test_publisher.py +++ b/dev_tools/services/acknowledg-forge/tests/test_publisher.py @@ -19,6 +19,7 @@ import uuid from pathlib import Path +from types import SimpleNamespace from typing import Any, AsyncIterator, Callable, Optional import pytest @@ -36,6 +37,7 @@ rich_link, rich_text, ) +from edgacknowledg.client.shared import SandboxContext from edgacknowledg.forge import serving from edgacknowledg.forge.db import ( ForgeStore, @@ -51,6 +53,7 @@ review_identity, ) from edgacknowledg.forge.publisher.client import PublisherClient +from edgacknowledg.forge.publisher.pool_process import build_handler from edgacknowledg.forge.publisher.protocol import ( MESSAGE_LIMIT, decode_review, @@ -174,6 +177,34 @@ def _empty_sync() -> dict[str, Any]: } +async def test_sync_does_not_send_rt_limits(tmp_path: Path, + store: ForgeStore, + monkeypatch: pytest.MonkeyPatch + ) -> None: + reviews, section = _layout(tmp_path) + publisher = _client('ws://127.0.0.1:1', reviews, store, + api_port = 1) + requests: list[dict[str, Any]] = [] + + async def capture(request: dict[str, Any]) -> None: + requests.append(request) + + monkeypatch.setattr(publisher, '_request', capture) + await publisher._sync(ReviewIdentity(uuid.uuid4(), NAME, ''), [section]) + details = requests[0]['sections'][0]['details'] + assert details == section.details + + +def test_pool_uses_fixed_rt_limits(tmp_path: Path) -> None: + _, section = _layout(tmp_path) + sandbox = SimpleNamespace( + context_for = lambda home: SandboxContext(home) + ) + handler = build_handler(section, sandbox, None) + assert handler._max_rt_entries == 10_000 + assert handler._max_rt_member_bytes == 64 * 1024 * 1024 + + async def _wait_for(condition: Callable[[], bool]) -> None: for _ in range(300): if condition(): diff --git a/dev_tools/services/acknowledg-forge/tests/test_safety.py b/dev_tools/services/acknowledg-forge/tests/test_safety.py index 822538d76a..14c275e5c4 100644 --- a/dev_tools/services/acknowledg-forge/tests/test_safety.py +++ b/dev_tools/services/acknowledg-forge/tests/test_safety.py @@ -17,6 +17,9 @@ import pytest +import edgtest + +from edgacknowledg.client.test import _run_test_output from edgacknowledg.forge import processor from edgacknowledg.forge.processor import _changes_test_ids, _read_ci_metadata from edgacknowledg.forge.sandbox import Sandbox, SandboxUnavailableError @@ -75,7 +78,7 @@ def test_extract_caps_total_size(tmp_path: Path) -> None: extract_artifact(archive, destination, max_bytes = 10_000) -def test_extract_caps_inner_tarballs(tmp_path: Path) -> None: +def test_extract_counts_inner_tarballs_compressed_size(tmp_path: Path) -> None: payload = io.BytesIO() with tarfile.open(fileobj = payload, mode = 'w:gz') as tarball: data = b'0' * 100_000 @@ -85,8 +88,47 @@ def test_extract_caps_inner_tarballs(tmp_path: Path) -> None: archive = _zip(tmp_path / 'a.zip', {'cfg/t.rt.tar.gz': payload.getvalue()}) destination = tmp_path / 'out' destination.mkdir() - with pytest.raises(UnsafeArchiveError): - extract_artifact(archive, destination, max_bytes = 10_000) + compressed_size = len(payload.getvalue()) + extract_artifact(archive, destination, max_bytes = compressed_size) + assert (destination / 'cfg' / 't.rt.tar.gz').read_bytes() == payload.getvalue() + + smaller = tmp_path / 'smaller' + smaller.mkdir() + with pytest.raises(UnsafeArchiveError, + match = 'artifact exceeds the extracted size limit'): + extract_artifact(archive, smaller, max_bytes = compressed_size - 1) + + +def test_run_output_checks_inner_members_on_read(tmp_path: Path) -> None: + path = tmp_path / 'a.rt.tar.gz' + with tarfile.open(path, 'w:gz') as archive: + archive.addfile(tarfile.TarInfo('a.txt'), io.BytesIO()) + archive.addfile(tarfile.TarInfo('b.txt'), io.BytesIO()) + with _run_test_output(path, None, None) as output: + assert output.member_names == {'a.txt', 'b.txt'} + with pytest.raises(ValueError, match = 'too many entries'): + with _run_test_output(path, 1, None): + pass + with pytest.raises(ValueError, match = 'too many entries'): + with edgtest.runtest.RunTestOutput(path, max_entries = 1): + pass + + with tarfile.open(path, 'w:gz') as archive: + member = tarfile.TarInfo('a.txt') + member.size = 100 + archive.addfile(member, io.BytesIO(b'x' * 100)) + with pytest.raises(ValueError, match = 'too large'): + with edgtest.runtest.RunTestOutput(path, max_member_bytes = 10): + pass + + with tarfile.open(path, 'w:gz') as archive: + member = tarfile.TarInfo('a.txt') + member.type = tarfile.SYMTYPE + member.linkname = 'elsewhere' + archive.addfile(member) + with pytest.raises(edgtest.runtest.RunTestOutputLinkError): + with edgtest.runtest.RunTestOutput(path, max_entries = 2): + pass def test_extract_caps_entries(tmp_path: Path) -> None: @@ -101,7 +143,7 @@ def test_extract_caps_entries(tmp_path: Path) -> None: max_entries = 3 ) - # Inner tarballs' entries count too. + # Inner tarballs' entries are checked when opened for review. payload = io.BytesIO() with tarfile.open(fileobj = payload, mode = 'w:gz') as tarball: for n in range(2): @@ -112,13 +154,12 @@ def test_extract_caps_entries(tmp_path: Path) -> None: }) destination = tmp_path / 'out2' destination.mkdir() - with pytest.raises(UnsafeArchiveError, match = 'more than 3 entries'): - extract_artifact( - archive, - destination, - max_bytes = 10_000, - max_entries = 3 - ) + extract_artifact( + archive, + destination, + max_bytes = 10_000, + max_entries = 3 + ) def test_extract_caps_the_central_directory(tmp_path: Path) -> None: