From ded5332c462968bc1437dd600b8ec297b08d27ac Mon Sep 17 00:00:00 2001 From: Mads Hvelplund Date: Fri, 19 Jun 2026 06:56:38 +0200 Subject: [PATCH] fix: log unreadable subtrees during size pre-flight os.walk silently swallowed directory-listing errors, so a permission-denied subtree could make a large repo under-count and slip past the pre-flight. Surface such omissions via an onerror warning. --- strix/interface/utils.py | 14 +++++++++++++- tests/test_local_sources.py | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/strix/interface/utils.py b/strix/interface/utils.py index 95a70f04..bffc0d47 100644 --- a/strix/interface/utils.py +++ b/strix/interface/utils.py @@ -1,5 +1,6 @@ import ipaddress import json +import logging import os import re import secrets @@ -23,6 +24,9 @@ from rich.text import Text from strix.config import load_settings +logger = logging.getLogger(__name__) + + def get_severity_color(severity: str) -> str: severity_colors = { "critical": "#dc2626", @@ -1219,9 +1223,17 @@ def directory_size_bytes(path: Path) -> int: Best-effort: files that disappear or can't be stat'd mid-walk are skipped. Used as a cheap (stat-only) pre-flight to estimate the cost of streaming a local target into the sandbox before we actually try to copy it. + + Directories that can't be listed (e.g. permission denied) are logged and + skipped rather than silently dropped — so an under-count is at least + visible — but the returned total then excludes their contents. """ + + def _on_walk_error(error: OSError) -> None: + logger.warning("Could not read %s while measuring size: %s", error.filename, error) + total = 0 - for root, _dirs, files in os.walk(path, followlinks=False): + for root, _dirs, files in os.walk(path, followlinks=False, onerror=_on_walk_error): for name in files: file_path = os.path.join(root, name) # noqa: PTH118 try: diff --git a/tests/test_local_sources.py b/tests/test_local_sources.py index 1cda1767..bd3448de 100644 --- a/tests/test_local_sources.py +++ b/tests/test_local_sources.py @@ -2,6 +2,9 @@ from __future__ import annotations +import logging +import os +import sys from typing import TYPE_CHECKING, Any import pytest @@ -49,6 +52,28 @@ def test_directory_size_skips_symlinks(tmp_path: Path) -> None: assert directory_size_bytes(tmp_path) == 100 +@pytest.mark.skipif(sys.platform == "win32", reason="relies on POSIX permissions") +def test_directory_size_logs_and_skips_unreadable_subdir( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + if hasattr(os, "geteuid") and os.geteuid() == 0: + pytest.skip("root bypasses directory permissions") + _write_file(tmp_path / "top.txt", 100) + locked = tmp_path / "locked" + locked.mkdir() + _write_file(locked / "secret.bin", 9999) + locked.chmod(0o000) + try: + with caplog.at_level(logging.WARNING): + size = directory_size_bytes(tmp_path) + finally: + locked.chmod(0o755) + # The unreadable subtree is excluded (not silently treated as readable) and + # the omission is logged rather than vanishing without a trace. + assert size == 100 + assert any("Could not read" in record.message for record in caplog.records) + + def test_find_oversized_returns_nothing_under_limit(tmp_path: Path) -> None: _write_file(tmp_path / "a.txt", 100) targets = [_local_target(str(tmp_path))]