Python · 5180 bytes Raw Blame History
1 """Path confinement, binary sniff, glob-to-regex, and enumeration."""
2
3 from __future__ import annotations
4
5 import logging
6 from pathlib import Path
7
8 import pytest
9
10 from dlm.directives.errors import DirectivePolicyError
11 from dlm.directives.safety import (
12 _compile_glob,
13 confine_path,
14 enumerate_matching_files,
15 is_probably_binary,
16 )
17
18 # ---- glob compiler ----------------------------------------------------------
19
20
21 @pytest.mark.parametrize(
22 ("pattern", "path", "expected"),
23 [
24 ("*.py", "foo.py", True),
25 ("*.py", "foo.txt", False),
26 ("*.py", "sub/foo.py", False), # single `*` doesn't cross `/`
27 ("**/*.py", "foo.py", True),
28 ("**/*.py", "a/b/foo.py", True),
29 ("tests/**", "tests/a.py", True),
30 ("tests/**", "tests/a/b.py", True),
31 ("tests/**", "src/a.py", False),
32 ("src/**/*.rs", "src/x.rs", True),
33 ("src/**/*.rs", "src/a/b.rs", True),
34 ("foo?.md", "foo1.md", True),
35 ("foo?.md", "foo12.md", False),
36 ],
37 )
38 def test_glob_compiler(pattern: str, path: str, expected: bool) -> None:
39 assert bool(_compile_glob(pattern).fullmatch(path)) is expected
40
41
42 # ---- confine_path -----------------------------------------------------------
43
44
45 def test_confine_strict_accepts_child(tmp_path: Path) -> None:
46 child = tmp_path / "a" / "b"
47 child.mkdir(parents=True)
48 resolved = confine_path(child, tmp_path, strict=True)
49 assert resolved == child.resolve()
50
51
52 def test_confine_strict_rejects_sibling(tmp_path: Path) -> None:
53 sibling = tmp_path.parent / "other"
54 with pytest.raises(DirectivePolicyError):
55 confine_path(sibling, tmp_path, strict=True)
56
57
58 def test_confine_permissive_allows_external(tmp_path: Path) -> None:
59 external = tmp_path.parent
60 # doesn't raise, returns resolved path
61 result = confine_path(external, tmp_path, strict=False)
62 assert result == external.resolve()
63
64
65 def test_confine_strict_symlink_escape_refused(tmp_path: Path) -> None:
66 outside = tmp_path.parent / "outside_target"
67 outside.mkdir(exist_ok=True)
68 try:
69 link = tmp_path / "escape"
70 link.symlink_to(outside)
71 with pytest.raises(DirectivePolicyError):
72 confine_path(link, tmp_path, strict=True)
73 finally:
74 if outside.exists():
75 outside.rmdir()
76
77
78 def test_confine_permissive_symlink_escape_logs(
79 tmp_path: Path, caplog: pytest.LogCaptureFixture
80 ) -> None:
81 outside = tmp_path.parent / "outside_log"
82 outside.mkdir(exist_ok=True)
83 try:
84 link = tmp_path / "escape"
85 link.symlink_to(outside)
86 caplog.set_level(logging.WARNING, logger="dlm.directives.safety")
87 confine_path(link, tmp_path, strict=False)
88 assert any("symlink" in rec.message for rec in caplog.records)
89 finally:
90 if outside.exists():
91 outside.rmdir()
92
93
94 def test_confine_expands_tilde(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
95 fake_home = tmp_path / "home"
96 fake_home.mkdir()
97 monkeypatch.setenv("HOME", str(fake_home))
98 # ~/foo resolves under fake home
99 resolved = confine_path(Path("~/foo"), fake_home, strict=True)
100 assert resolved == (fake_home / "foo").resolve()
101
102
103 # ---- binary sniff ----------------------------------------------------------
104
105
106 def test_is_probably_binary_finds_nul() -> None:
107 assert is_probably_binary(b"hello\x00world") is True
108
109
110 def test_is_probably_binary_plain_text() -> None:
111 assert is_probably_binary(b"hello world\nthis is text\n") is False
112
113
114 def test_is_probably_binary_nul_past_sample() -> None:
115 # NUL beyond the first 1 KiB → not flagged
116 data = b"A" * 2048 + b"\x00"
117 assert is_probably_binary(data) is False
118
119
120 # ---- enumerate_matching_files ---------------------------------------------
121
122
123 def test_enumerate_is_deterministic(tmp_path: Path) -> None:
124 for name in ("z.py", "a.py", "m.py"):
125 (tmp_path / name).write_text("x")
126 got = list(enumerate_matching_files(tmp_path, include=("**/*.py",), exclude=()))
127 assert [p.name for p in got] == ["a.py", "m.py", "z.py"]
128
129
130 def test_enumerate_exclude_wins(tmp_path: Path) -> None:
131 (tmp_path / "keep.py").write_text("x")
132 (tmp_path / "skip.py").write_text("x")
133 got = list(enumerate_matching_files(tmp_path, include=("**/*.py",), exclude=("skip.py",)))
134 assert [p.name for p in got] == ["keep.py"]
135
136
137 def test_enumerate_nested(tmp_path: Path) -> None:
138 (tmp_path / "a").mkdir()
139 (tmp_path / "a" / "nested.py").write_text("x")
140 (tmp_path / "top.py").write_text("x")
141 got = list(enumerate_matching_files(tmp_path, include=("**/*.py",), exclude=()))
142 rels = [p.relative_to(tmp_path).as_posix() for p in got]
143 assert rels == ["a/nested.py", "top.py"]
144
145
146 def test_enumerate_single_file(tmp_path: Path) -> None:
147 target = tmp_path / "one.md"
148 target.write_text("x")
149 got = list(enumerate_matching_files(target, include=("*.md",), exclude=()))
150 assert got == [target]
151
152
153 def test_enumerate_missing_root_yields_nothing(tmp_path: Path) -> None:
154 missing = tmp_path / "does_not_exist"
155 got = list(enumerate_matching_files(missing, include=("**/*",), exclude=()))
156 assert got == []