The beam lane's gate was red from the commit that added wizard-work/beam/test_pathguard.py: it imported pytest for pytest.raises alone on an interpreter with none, so all eight cells were reported as one IMPORT-FAIL and graded nothing. The lane already vendored beam_assert.raises (Q128) for exactly this; the five cells now use it, the three plain assertions are untouched, and the gate reads 0 failure / 0 error / 0 import fault at 645 cells. Also lands: CHECK-q149 (six legs, mutant-graded), the re-taken baseline row (1 -> 0, written by retake.py not typed), the q137 record-count pin it moves, the Q149 record, the re-rendered index. Evidence: /root/.hermes/wizard-outputs/volt/q149-beam-gate/
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
"""pathguard cells, standard library only.
|
|
|
|
Q149 (2026-11-01): this module imported `pytest` and used it for
|
|
`pytest.raises` alone. The interpreter `beam/CHECK` resolves has no pytest --
|
|
Q128 moved the lane's four other pytest-only files onto `beam_assert` for
|
|
exactly that reason -- so the import failed and `run_lane_tests.py` reported
|
|
all eight cells as one `IMPORT-FAIL test_pathguard.py` line: eight cells graded
|
|
nothing, and the lane gate was red with a message that reads like a missing
|
|
library rather than a broken one. The fix is the lane's existing shim, not a
|
|
new idiom: `beam_assert.raises` is the `pytest.raises(X)` equivalent this
|
|
directory already vendors. Cell bodies are unchanged.
|
|
"""
|
|
|
|
import os
|
|
import tempfile
|
|
|
|
from beam_assert import raises
|
|
from pathguard import safe_join, PathTraversalError
|
|
|
|
|
|
def test_basic_join():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
out = safe_join(d, "a", "b.txt")
|
|
assert out == os.path.join(os.path.realpath(d), "a", "b.txt")
|
|
|
|
|
|
def test_dotdot_escape_rejected():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with raises(PathTraversalError):
|
|
safe_join(d, "..", "etc", "passwd")
|
|
|
|
|
|
def test_absolute_part_rejected():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with raises(PathTraversalError):
|
|
safe_join(d, "/etc/passwd")
|
|
|
|
|
|
def test_symlink_escape_rejected():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
base = os.path.join(d, "base")
|
|
os.makedirs(base)
|
|
target = os.path.join(d, "secret.txt")
|
|
with open(target, "w") as f:
|
|
f.write("secret")
|
|
link = os.path.join(base, "link")
|
|
os.symlink(target, link)
|
|
# reading through link to outside must be rejected when resolved?
|
|
# safe_join resolves; link itself is inside base by name but points outside.
|
|
# Our function resolves symlinks and checks containment -> should reject.
|
|
with raises(PathTraversalError):
|
|
safe_join(base, "link")
|
|
|
|
|
|
def test_valid_nested():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
sub = os.path.join(d, "sub")
|
|
os.makedirs(sub)
|
|
out = safe_join(d, "sub", "file.txt")
|
|
assert out.startswith(os.path.realpath(d) + os.sep)
|
|
|
|
|
|
def test_empty_parts_ignored():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
out = safe_join(d, "", None, "x")
|
|
assert out.endswith("x")
|
|
|
|
|
|
def test_nul_rejected():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
with raises(PathTraversalError):
|
|
safe_join(d, "a\x00b")
|
|
|
|
|
|
def test_base_not_dir():
|
|
with tempfile.TemporaryDirectory() as d:
|
|
f = os.path.join(d, "f")
|
|
open(f, "w").close()
|
|
with raises(PathTraversalError):
|
|
safe_join(f, "x")
|