3"""Git-authored file census and encoding-independent rare-token search."""
5from __future__
import annotations
10from collections.abc
import Callable, Collection
11from dataclasses
import dataclass
12from pathlib
import Path, PurePosixPath
13from tempfile
import TemporaryDirectory
15from git_environment
import LOCAL_GIT_ENVIRONMENT, isolated_git_environment, trusted_git_executable
17BATCH_HEADER_FIELDS = 3
18REGULAR_INDEX_MODES = frozenset({
"100644",
"100755"})
19TOKEN_ENCODINGS = (
"utf-8",
"utf-16-le",
"utf-16-be")
20SELFTEST_TOKEN =
"repair-" +
"entry"
23class CensusError(RuntimeError):
24 """Raised when Git or the worktree cannot prove an authored-file census."""
27@dataclass(frozen=
True)
29 """One stage-zero regular-file candidate from the inherited live index."""
36@dataclass(frozen=True)
38 """One independently validated index or worktree byte source."""
45GitRunner = Callable[[list[str], Path, bytes |
None], subprocess.CompletedProcess[bytes]]
48def _default_git_runner(
49 argv: list[str], cwd: Path, input_data: bytes |
None
50) -> subprocess.CompletedProcess[bytes]:
51 """Run one non-mutating Git census command."""
52 return subprocess.run(
53 argv, cwd=cwd, input=input_data, capture_output=
True, check=
False
59 args: tuple[str, ...],
60 runner: GitRunner = _default_git_runner,
61 input_data: bytes |
None =
None,
63 """Return warning-free Git output or fail closed."""
64 argv = [trusted_git_executable(), *args]
66 result = runner(argv, repo_root, input_data)
67 except OSError
as exc:
68 message = f
"Git authored-file census could not start: {exc}"
69 raise CensusError(message)
from exc
70 stderr = result.stderr.decode(
"utf-8", errors=
"replace").strip()
71 if result.returncode != 0:
72 detail = stderr
or f
"exit {result.returncode}"
73 message = f
"Git authored-file census failed: {detail}"
74 raise CensusError(message)
76 message = f
"Git authored-file census warned: {stderr}"
77 raise CensusError(message)
81def _nul_records(output: bytes) -> tuple[bytes, ...]:
82 """Parse complete NUL-delimited Git records fail closed."""
85 if not output.endswith(b
"\0"):
86 message =
"Git authored-file census returned a truncated record"
87 raise CensusError(message)
88 records = tuple(output[:-1].split(b
"\0"))
89 if any(
not record
for record
in records):
90 message =
"Git authored-file census returned an empty path"
91 raise CensusError(message)
95def _decode_git_path(raw: bytes) -> str:
96 """Decode and constrain one repository-relative Git path."""
98 relative = raw.decode(
"utf-8")
99 except UnicodeDecodeError
as exc:
100 message =
"Git authored-file census found a non-UTF-8 path"
101 raise CensusError(message)
from exc
102 pure = PurePosixPath(relative)
103 if not relative
or pure.is_absolute()
or ".." in pure.parts
or pure.as_posix() != relative:
104 message = f
"Git authored-file census returned unsafe path {relative!r}"
105 raise CensusError(message)
109def _git_paths(repo_root: Path, args: tuple[str, ...]) -> tuple[str, ...]:
110 """Return unique decoded paths from one Git query."""
111 records = _nul_records(_git_output(repo_root, args))
112 paths = tuple(_decode_git_path(record)
for record
in records)
113 if len(paths) != len(set(paths)):
114 message =
"Git authored-file census returned duplicate paths"
115 raise CensusError(message)
119def _git_index_entries(repo_root: Path) -> tuple[IndexEntry, ...]:
120 """Parse the inherited live index, rejecting conflicts and duplicates."""
123 output = _git_output(repo_root, (
"ls-files",
"-z",
"--stage"))
124 for record
in _nul_records(output):
126 metadata, raw_path = record.split(b
"\t", 1)
127 raw_mode, raw_object_id, raw_stage = metadata.split(b
" ", 2)
128 mode = raw_mode.decode(
"ascii")
129 object_id = raw_object_id.decode(
"ascii")
130 except ValueError
as exc:
131 message =
"Git authored-file census returned malformed stage data"
132 raise CensusError(message)
from exc
133 except UnicodeDecodeError
as exc:
134 message =
"Git authored-file census returned non-ASCII stage metadata"
135 raise CensusError(message)
from exc
136 relative = _decode_git_path(raw_path)
137 if raw_stage != b
"0":
138 message = f
"Git authored-file census found an unresolved index stage: {relative}"
139 raise CensusError(message)
141 message = f
"Git authored-file census returned a duplicate index path: {relative}"
142 raise CensusError(message)
143 if len(object_id)
not in {40, 64}
or any(
144 character
not in "0123456789abcdef" for character
in object_id
146 message = f
"Git authored-file census returned an invalid object ID: {relative}"
147 raise CensusError(message)
149 entries.append(IndexEntry(relative, mode, object_id))
150 return tuple(entries)
153def _scoped_index_entries(
154 entries: Collection[IndexEntry], excluded_parts: Collection[str]
155) -> tuple[IndexEntry, ...]:
156 """Apply explicit tree exclusions, then require regular index modes."""
158 for entry
in entries:
159 if any(part
in excluded_parts
for part
in PurePosixPath(entry.relative).parts):
161 if entry.mode ==
"120000":
162 message = f
"first-party authored path is an index symlink: {entry.relative}"
163 raise CensusError(message)
164 if entry.mode
not in REGULAR_INDEX_MODES:
165 message = f
"first-party authored path has unsupported index mode: {entry.relative}"
166 raise CensusError(message)
171def _parse_blob_batch(output: bytes, entries: Collection[IndexEntry]) -> tuple[bytes, ...]:
172 """Parse exact `git cat-file --batch` blob records fail closed."""
175 for entry
in entries:
176 line_end = output.find(b
"\n", cursor)
178 message =
"Git authored-file blob batch returned a truncated header"
179 raise CensusError(message)
180 header = output[cursor:line_end].split(b
" ")
182 len(header) != BATCH_HEADER_FIELDS
183 or header[0] != entry.object_id.encode()
184 or header[1] != b
"blob"
186 message = f
"Git authored-file blob batch returned wrong metadata: {entry.relative}"
187 raise CensusError(message)
189 size = int(header[2])
190 except ValueError
as exc:
191 message = f
"Git authored-file blob batch returned an invalid size: {entry.relative}"
192 raise CensusError(message)
from exc
195 if size < 0
or end >= len(output)
or output[end : end + 1] != b
"\n":
196 message = f
"Git authored-file blob batch truncated content: {entry.relative}"
197 raise CensusError(message)
198 blobs.append(output[start:end])
200 if cursor != len(output):
201 message =
"Git authored-file blob batch returned trailing data"
202 raise CensusError(message)
206def _index_blobs(repo_root: Path, entries: Collection[IndexEntry]) -> tuple[bytes, ...]:
207 """Read all inherited-index blobs in one warning-free Git process."""
210 request = b
"".join(f
"{entry.object_id}\n".encode()
for entry
in entries)
211 output = _git_output(repo_root, (
"cat-file",
"--batch"), input_data=request)
212 return _parse_blob_batch(output, entries)
215def path_lstat(path: Path) -> os.stat_result:
216 """Read one worktree path without following a symbolic link."""
220def path_read_bytes(path: Path) -> bytes:
221 """Read one proven-regular authored file as uninterpreted bytes."""
222 return path.read_bytes()
225def read_file(path: Path, reader: Callable[[Path], bytes] = path_read_bytes) -> bytes:
226 """Read one authored file and convert every I/O failure into census failure."""
229 except OSError
as exc:
230 message = f
"authored file cannot be read: {path}: {exc}"
231 raise CensusError(message)
from exc
234def _authored_inventory(
236 excluded_parts: Collection[str],
237 lstat_file: Callable[[Path], os.stat_result] = path_lstat,
238) -> tuple[tuple[IndexEntry, ...], tuple[str, ...]]:
239 """Return validated index entries and present nonignored worktree paths."""
240 entries = _scoped_index_entries(_git_index_entries(repo_root), excluded_parts)
241 untracked = _git_paths(repo_root, (
"ls-files",
"-z",
"--others",
"--exclude-standard"))
244 for relative
in untracked
245 if not any(part
in excluded_parts
for part
in PurePosixPath(relative).parts)
247 index_paths = {entry.relative
for entry
in entries}
249 for relative
in sorted(index_paths | set(untracked)):
250 path = repo_root / relative
252 path_stat = lstat_file(path)
253 except FileNotFoundError
as exc:
254 if relative
in index_paths:
256 message = f
"authored path disappeared during census: {relative}"
257 raise CensusError(message)
from exc
258 except OSError
as exc:
259 message = f
"cannot inspect authored path {relative}: {exc}"
260 raise CensusError(message)
from exc
261 if stat.S_ISLNK(path_stat.st_mode):
262 message = f
"first-party authored path is a worktree symlink: {relative}"
263 raise CensusError(message)
264 if not stat.S_ISREG(path_stat.st_mode):
265 message = f
"first-party authored path is not a regular file: {relative}"
266 raise CensusError(message)
267 worktree.append(relative)
268 return entries, tuple(worktree)
273 excluded_parts: Collection[str],
274 lstat_file: Callable[[Path], os.stat_result] = path_lstat,
276 """Return the union of live-index and nonignored worktree paths."""
277 entries, worktree = _authored_inventory(repo_root, excluded_parts, lstat_file)
278 relative_paths = {entry.relative
for entry
in entries} | set(worktree)
279 return [repo_root / relative
for relative
in sorted(relative_paths)]
284 excluded_parts: Collection[str],
285 lstat_file: Callable[[Path], os.stat_result] = path_lstat,
286 reader: Callable[[Path], bytes] = path_read_bytes,
287) -> tuple[AuthoredSource, ...]:
288 """Read inherited-index blobs and present worktree bytes independently."""
289 entries, worktree = _authored_inventory(repo_root, excluded_parts, lstat_file)
291 AuthoredSource(entry.relative,
"index", data)
292 for entry, data
in zip(entries, _index_blobs(repo_root, entries), strict=
True)
294 for relative
in worktree:
295 data = read_file(repo_root / relative, reader)
296 sources.append(AuthoredSource(relative,
"worktree", data))
297 return tuple(sources)
302 tokens: Collection[str],
303 encodings: Collection[str] = TOKEN_ENCODINGS,
305 """Find rare tokens in each explicitly supported text encoding."""
307 token
for token
in tokens
if any(token.encode(encoding)
in data
for encoding
in encodings)
311def init_test_repo(repo_root: Path) ->
None:
312 """Initialize one throwaway Git repository for semantic selftests."""
313 _run_test_git(repo_root,
"init",
"-q")
316def _run_test_git(repo_root: Path, *args: str) ->
None:
317 """Run one mutating Git command inside a throwaway selftest repository."""
319 [trusted_git_executable(), *args],
326def _write(repo_root: Path, relative: str, data: bytes = b
"fixture\n") -> Path:
327 """Write one throwaway census fixture."""
328 path = repo_root / relative
329 path.parent.mkdir(parents=
True, exist_ok=
True)
330 path.write_bytes(data)
334def _raises_census(operation: Callable[[], object]) -> bool:
335 """Return whether one semantic selftest operation fails closed."""
343def _source_hits(repo_root: Path, excluded_parts: Collection[str]) -> set[tuple[str, str]]:
344 """Return path/view pairs containing the synthetic rare token."""
346 (source.relative, source.view)
347 for source
in authored_sources(repo_root, excluded_parts)
348 if token_hits(source.data, (SELFTEST_TOKEN,))
352def _selftest_index_worktree_views(excluded_parts: Collection[str]) -> list[str]:
353 """Prove index and worktree bytes are scanned as independent views."""
354 token = SELFTEST_TOKEN.encode()
356 (
"staged unsafe/worktree safe", token, b
"safe\n", {(
"entry",
"index")}),
357 (
"index unsafe/worktree deleted", token,
None, {(
"entry",
"index")}),
358 (
"index safe/worktree unsafe", b
"safe\n", token, {(
"entry",
"worktree")}),
361 for name, index_data, worktree_data, expected
in cases:
362 with TemporaryDirectory()
as tmp:
365 entry = _write(root,
"entry", index_data)
366 _run_test_git(root,
"add",
"entry")
367 if worktree_data
is None:
370 entry.write_bytes(worktree_data)
371 if _source_hits(root, excluded_parts) != expected:
372 failures.append(f
" {name} did not preserve both byte views")
376def _selftest_scope(excluded_parts: Collection[str]) -> list[str]:
377 """Prove Git inventory, exclusions, ignores and deletion semantics."""
378 with TemporaryDirectory()
as tmp:
381 _write(root,
".gitignore", b
"/build-cov/\n")
382 _write(root,
"tracked.txt")
383 _write(root,
"deleted.txt")
384 excluded = tuple(f
"nested/{part}/ignored" for part
in sorted(excluded_parts))
385 for relative
in excluded:
386 _write(root, relative, SELFTEST_TOKEN.encode())
387 tracked_excluded = tuple(path
for path
in excluded
if "/.git/" not in path)
388 _run_test_git(root,
"add",
"-f",
".gitignore",
"tracked.txt",
"deleted.txt")
390 _run_test_git(root,
"add",
"-f",
"--", *tracked_excluded)
396 "user.email=fixture@example.invalid",
401 _run_test_git(root,
"rm",
"-q",
"deleted.txt")
402 _write(root,
"untracked.txt")
403 _write(root,
"build-cov/ignored", SELFTEST_TOKEN.encode())
405 path.relative_to(root).as_posix()
for path
in authored_files(root, excluded_parts)
407 required = {
".gitignore",
"tracked.txt",
"untracked.txt"}
408 forbidden = {*excluded,
"deleted.txt",
"build-cov/ignored"}
409 if not required <= inputs
or forbidden & inputs:
410 return [
" Git census did not preserve tracked/untracked/ignored/deleted scope"]
414def _selftest_symlinks(excluded_parts: Collection[str]) -> list[str]:
415 """Prove worktree/index aliases fail while excluded vendor aliases stay out."""
417 with TemporaryDirectory()
as tmp:
420 _write(root,
"vendor/target")
421 (root /
"alias").symlink_to(
"vendor/target")
422 if not _raises_census(
lambda: authored_files(root, excluded_parts)):
423 failures.append(
" an untracked first-party worktree symlink was accepted")
424 with TemporaryDirectory()
as tmp:
427 _write(root,
"vendor/target")
428 alias = root /
"alias"
429 alias.symlink_to(
"vendor/target")
430 _run_test_git(root,
"add",
"alias")
432 alias.write_text(
"regular replacement\n", encoding=
"utf-8")
433 if not _raises_census(
lambda: authored_files(root, excluded_parts)):
434 failures.append(
" a first-party index symlink was accepted after worktree replacement")
435 with TemporaryDirectory()
as tmp:
438 _write(root,
"vendor/target")
439 alias = _write(root,
"alias")
440 _run_test_git(root,
"add",
"alias")
442 alias.symlink_to(
"vendor/target")
443 if not _raises_census(
lambda: authored_sources(root, excluded_parts)):
444 failures.append(
" a worktree symlink was accepted over a regular index entry")
445 with TemporaryDirectory()
as tmp:
448 _write(root,
"vendor/target")
449 (root /
"vendor/alias").symlink_to(
"target")
450 _run_test_git(root,
"add",
"-f",
"vendor/target",
"vendor/alias")
451 if _raises_census(
lambda: authored_files(root, excluded_parts)):
452 failures.append(
" an explicitly excluded vendor symlink entered first-party scope")
456def _selftest_encodings(excluded_parts: Collection[str]) -> list[str]:
457 """Prove UTF-8, UTF-16LE, UTF-16BE and BOM token discovery end to end."""
459 "utf8": SELFTEST_TOKEN.encode(
"utf-8"),
460 "utf16le": SELFTEST_TOKEN.encode(
"utf-16-le"),
461 "utf16be": SELFTEST_TOKEN.encode(
"utf-16-be"),
462 "utf16bom": b
"\xff\xfe" + SELFTEST_TOKEN.encode(
"utf-16-le"),
465 for name, data
in cases.items():
466 with TemporaryDirectory()
as tmp:
469 relative = f
"{name}.blob"
470 _write(root, relative, data)
471 _run_test_git(root,
"add", relative)
472 expected = {(relative,
"index"), (relative,
"worktree")}
473 if _source_hits(root, excluded_parts) != expected:
474 failures.append(f
" authored {name} rare token was not detected")
478def _commit_fixture(repo_root: Path) ->
None:
479 """Commit the current throwaway index with local synthetic identity."""
485 "user.email=fixture@example.invalid",
492def _selftest_staged_deletions(excluded_parts: Collection[str]) -> list[str]:
493 """Prove staged deletion drops only index data while retained bytes stay scanned."""
495 with TemporaryDirectory()
as tmp:
498 _write(root,
"entry", SELFTEST_TOKEN.encode())
499 _run_test_git(root,
"add",
"entry")
500 _commit_fixture(root)
501 _run_test_git(root,
"rm",
"-q",
"entry")
502 if any(source.relative ==
"entry" for source
in authored_sources(root, excluded_parts)):
503 failures.append(
" staged deletion remained in an authored byte view")
504 with TemporaryDirectory()
as tmp:
507 _write(root,
"entry", SELFTEST_TOKEN.encode())
508 _run_test_git(root,
"add",
"entry")
509 _commit_fixture(root)
510 _run_test_git(root,
"rm",
"--cached",
"-q",
"entry")
511 if _source_hits(root, excluded_parts) != {(
"entry",
"worktree")}:
512 failures.append(
" staged index deletion hid retained nonignored worktree bytes")
516def _failed_git_runner(
517 argv: list[str], _cwd: Path, _input: bytes |
None
518) -> subprocess.CompletedProcess[bytes]:
519 """Return one synthetic failing Git result."""
520 return subprocess.CompletedProcess(argv, 128, b
"", b
"fatal")
523def _warned_git_runner(
524 argv: list[str], _cwd: Path, _input: bytes |
None
525) -> subprocess.CompletedProcess[bytes]:
526 """Return a success status carrying an unreadable-directory warning."""
527 warning = b
"warning: could not open directory: Permission denied"
528 return subprocess.CompletedProcess(argv, 0, b
"", warning)
531def _denied_git_runner(
532 _argv: list[str], _cwd: Path, _input: bytes |
None
533) -> subprocess.CompletedProcess[bytes]:
534 """Raise the execution failure used by the Git fail-closed test."""
535 message =
"fixture denied"
536 raise PermissionError(message)
539def _denied_path(_path: Path) -> bytes:
540 """Raise the permission failure used by stat/read fail-closed tests."""
541 message =
"fixture denied"
542 raise PermissionError(message)
545def _selftest_fail_closed(excluded_parts: Collection[str]) -> list[str]:
546 """Prove Git warnings/failures and stat/read errors cannot shrink the scan."""
548 if not _raises_census(
lambda: _git_output(Path.cwd(), (
"ls-files",), _failed_git_runner)):
549 failures.append(
" a failing Git authored census was accepted")
550 if not _raises_census(
lambda: _git_output(Path.cwd(), (
"ls-files",), _warned_git_runner)):
551 failures.append(
" an unreadable-directory Git warning was accepted")
552 if not _raises_census(
lambda: _git_output(Path.cwd(), (
"ls-files",), _denied_git_runner)):
553 failures.append(
" a Git execution error was accepted")
554 with TemporaryDirectory()
as tmp:
557 file_path = _write(root,
"blocked")
558 if not _raises_census(
lambda: authored_files(root, excluded_parts, _denied_path)):
559 failures.append(
" an authored lstat error was accepted")
560 if not _raises_census(
lambda: authored_sources(root, excluded_parts, reader=_denied_path)):
561 failures.append(f
" an authored read error was accepted for {file_path.name}")
565def _fixture_selftests(excluded_parts: Collection[str]) -> list[str]:
566 """Run every nested-repository semantic fixture in the current environment."""
568 _selftest_index_worktree_views(excluded_parts)
569 + _selftest_scope(excluded_parts)
570 + _selftest_symlinks(excluded_parts)
571 + _selftest_encodings(excluded_parts)
572 + _selftest_staged_deletions(excluded_parts)
573 + _selftest_fail_closed(excluded_parts)
577def _repo_snapshot(repo_root: Path) -> tuple[bytes, bytes, bytes]:
578 """Capture the outer state that a nested fixture must not mutate."""
579 with isolated_git_environment():
581 _git_output(repo_root, (
"rev-parse",
"HEAD")),
582 (repo_root /
".git" /
"index").read_bytes(),
583 _git_output(repo_root, (
"status",
"--porcelain=v1",
"-z")),
587def _selftest_hostile_environment(excluded_parts: Collection[str]) -> list[str]:
588 """Prove hook-local Git routing cannot capture nested census fixtures."""
590 with TemporaryDirectory()
as tmp:
592 with isolated_git_environment():
593 init_test_repo(outer)
594 _write(outer,
"outer-sentinel")
595 _run_test_git(outer,
"add",
"outer-sentinel")
601 "user.email=fixture@example.invalid",
606 before = _repo_snapshot(outer)
607 original = {name: os.environ.get(name)
for name
in LOCAL_GIT_ENVIRONMENT}
608 hostile = dict.fromkeys(LOCAL_GIT_ENVIRONMENT,
"hostile")
611 "GIT_DIR": str(outer /
".git"),
612 "GIT_WORK_TREE": str(outer),
613 "GIT_INDEX_FILE": str(outer /
".git" /
"index"),
618 os.environ.update(hostile)
619 with isolated_git_environment():
620 if any(os.environ.get(name) ==
"hostile" for name
in LOCAL_GIT_ENVIRONMENT):
621 failures.append(
" nested census retained a hostile Git routing/config value")
622 failures.extend(_fixture_selftests(excluded_parts))
624 for name, value
in original.items():
626 os.environ.pop(name,
None)
628 os.environ[name] = value
629 if _repo_snapshot(outer) != before:
630 failures.append(
" nested census changed the hostile outer Git repository")
634def selftest(excluded_parts: Collection[str]) -> list[str]:
635 """Prove the Git-authored census and raw-token detector in both directions."""
636 with isolated_git_environment():
637 failures = _fixture_selftests(excluded_parts)
638 return failures + _selftest_hostile_environment(excluded_parts)