diff --git a/src/soup_cli/utils/canary.py b/src/soup_cli/utils/canary.py index d246783..98b2075 100644 --- a/src/soup_cli/utils/canary.py +++ b/src/soup_cli/utils/canary.py @@ -166,6 +166,10 @@ def _harden_permissions(path: str) -> None: def load_manifest(path: str) -> tuple[Canary, ...]: """Read a canary manifest written by :func:`write_manifest`.""" safe = enforce_under_cwd_and_no_symlink(str(path), "manifest") + if not os.path.isfile(safe): + # Without this the caller surfaces a raw (locale-dependent) OS error + # from getsize — e.g. "[WinError 2] Не удается найти указанный файл". + raise ValueError(f"manifest not found: {path}") if os.path.getsize(safe) > _MAX_MANIFEST_BYTES: raise ValueError( f"manifest too large (max {_MAX_MANIFEST_BYTES} bytes)" diff --git a/tests/test_v07136.py b/tests/test_v07136.py index 7b7b376..b8542d4 100644 --- a/tests/test_v07136.py +++ b/tests/test_v07136.py @@ -1430,6 +1430,18 @@ class TestCanaryManifest: generate_canaries(count=1, seed=0), str(tmp_path / "esc.json") ) + def test_missing_manifest_is_a_friendly_message(self, tmp_path, monkeypatch): + """Not a raw locale-dependent OS error. + + Live smoke surfaced "[WinError 2] Не удается найти указанный файл" + leaking straight through from os.path.getsize. + """ + from soup_cli.utils.canary import load_manifest + + monkeypatch.chdir(tmp_path) + with pytest.raises(ValueError, match="manifest not found"): + load_manifest("nope.json") + def test_malformed_manifest_rejected(self, tmp_path, monkeypatch): from pathlib import Path