Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actions/term-wall/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,7 @@ no dependence on the caller's cwd, environment, or git identity
(configure user.name/user.email in each temp repo). Every test asserts
the exit code and the output shape. Push and pull-request events are
simulated with an event JSON file and the `GITHUB_*` variables.

## Content surface, pinned

The content surface is every blob the scanned commit's tree tracks, read from the object store, never from the working tree. Every blob is scanned bytewise; nothing tracked is unscannable. A symlink entry is scanned as the blob it is, its target path text, and is never followed. A blob that is not valid UTF-8 reports each hit with the location `<path> line <n>`, `n` counting newline-separated segments from 1, and the third field `[binary blob]` in place of the line. A blob the wall cannot read is a refusal of class `git work tree`: `git work tree: expected a readable blob at <path>; found <error>; needed the object`.
49 changes: 41 additions & 8 deletions .github/actions/term-wall/term-wall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@
# may not.
#
# Surfaces, in order:
# 1. tracked file content (case-insensitive, binaries skipped)
# 1. tracked file content — every blob the scanned commit's tree
# tracks, read from the object store, never from the working tree,
# and scanned bytewise (case-insensitive): nothing tracked is
# unscannable. A symlink entry is scanned as the blob it is — its
# target path text — and never followed. A blob that is not valid
# UTF-8 reports each hit with "[binary blob]" in place of the line.
# 2. tracked file paths
# 3. the commit messages of the change — pull_request: base..head;
# push: before..head, or only the head commit when before is all
Expand Down Expand Up @@ -77,13 +82,41 @@ scan_name() { # scan_name <surface> <value> — for surfaces whose location
emit "$surface" "$masked" "$masked"
}

# 1. tracked content
content=$(git ls-files -z 2>/dev/null | xargs -0 -r grep -I -H -i -n -E -- "$pat" 2>/dev/null | mask || true)
if [[ -n $content ]]; then
while IFS= read -r line; do
file=${line%%:*}; rest=${line#*:}
emit "content" "$file line ${rest%%:*}" "${rest#*:}"
done <<< "$content"
# 1. tracked content — every blob of the scanned commit's tree, from the
# object store, bytewise (grep -a, never -I). A symlink entry (mode
# 120000) is a blob holding its target path and is never followed; a
# gitlink (type commit) is not a blob and has no content here. A blob
# the wall cannot read is a refusal — could-not-look is never a pass.
if git rev-parse -q --verify 'HEAD^{commit}' >/dev/null 2>&1; then
git ls-tree -r HEAD >/dev/null 2>&1 \
|| refuse 'git work tree: expected a readable tree at HEAD; found git ls-tree cannot read it; needed the object'
while IFS= read -r -d '' entry; do
meta=${entry%%$'\t'*}
path=${entry#*$'\t'}
read -r _mode type oid <<< "$meta"
[[ $type == blob ]] || continue
if ! err=$(git cat-file -e "$oid" 2>&1); then
err=$(printf '%s' "${err:-git cat-file cannot read $oid}" | tr '\n' ' ')
refuse "$(printf 'git work tree: expected a readable blob at %s; found %s; needed the object' "$path" "$err" | mask)"
fi
if git cat-file blob "$oid" 2>/dev/null | iconv -f UTF-8 -t UTF-8 >/dev/null 2>&1; then
# NUL is valid UTF-8 (U+0000) but a shell variable cannot hold
# it — drop it before masking, never after.
found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | tr -d '\000' | mask || true)
[[ -n $found ]] || continue
while IFS= read -r line; do
emit "content" "$path line ${line%%:*}" "${line#*:}"
done <<< "$found"
else
# Not valid UTF-8: never echo its bytes — only line numbers
# (newline-separated segments, from 1) and "[binary blob]".
found=$(git cat-file blob "$oid" 2>/dev/null | grep -a -i -n -E -- "$pat" | cut -d: -f1 || true)
[[ -n $found ]] || continue
while IFS= read -r n; do
emit "content" "$path line $n" "[binary blob]"
done <<< "$found"
fi
done < <(git ls-tree -r -z HEAD 2>/dev/null)
fi

# 2. tracked paths
Expand Down
57 changes: 54 additions & 3 deletions .github/actions/term-wall/tests/test_term_wall.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,62 @@ def test_tracked_content_hit_is_masked(self):
self.commit("add fixture")
self.assert_hit(self.run_wall(), "content")

def test_binary_tracked_content_is_skipped(self):
(self.repo / "fixture.bin").write_bytes(b"\x00" + PLANT.encode("ascii") + b"\xff")
self.commit("add binary fixture")
def test_non_utf8_committed_blob_hit_uses_binary_placeholder(self):
(self.repo / "fixture.bin").write_bytes(
b"ordinary first line\ncontains " + PLANT.encode("ascii") + b" \xff\n"
)
self.commit("add non-UTF-8 fixture")

result = self.run_wall()

self.assertEqual(result.returncode, 1, result)
self.assertEqual(result.stderr, "")
self.assertEqual(
result.stdout,
"content: fixture.bin line 2: [binary blob]\n",
)

def test_dangling_symlink_target_text_is_scanned_as_content(self):
os.symlink(f"absent-{PLANT}-target", self.repo / "dangling-link")
self.commit("add dangling symlink")

result = self.run_wall()

self.assertEqual(result.returncode, 1, result)
self.assertEqual(result.stderr, "")
self.assertEqual(
result.stdout,
"content: dangling-link line 1: absent-[forbidden name]-target\n",
)

def test_clean_symlink_to_committed_clean_file_is_clean(self):
os.symlink("clean.txt", self.repo / "clean-link")
self.commit("add clean symlink")

self.assert_clean(self.run_wall())

def test_uncommitted_working_tree_content_is_not_scanned(self):
self.write("tracked.txt", "committed clean content\n")
self.commit("add clean tracked file")
self.write("tracked.txt", f"working tree contains {PLANT}\n")

self.assert_clean(self.run_wall())

def test_committed_blob_deleted_only_from_working_tree_is_scanned(self):
self.write("deleted.txt", f"committed content contains {PLANT}\n")
self.commit("add planted tracked file")
(self.repo / "deleted.txt").unlink()

result = self.run_wall()

self.assertEqual(result.returncode, 1, result)
self.assertEqual(result.stderr, "")
self.assertEqual(
result.stdout,
"content: deleted.txt line 1: committed content contains "
"[forbidden name]\n",
)

def test_tracked_path_hit_is_masked(self):
self.write(f"notes-{PLANT}.txt", "ordinary text\n")
self.commit("add fixture")
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
t=$(mktemp -d); cd "$t"; git init -q; git config user.email ci@ci.invalid; git config user.name ci
printf 'clean\n' > c.txt; git add c.txt; git commit -qm clean
env -u GITHUB_EVENT_PATH bash "$wall"
printf 'x %s y\n' "$TERM_WALL_PLANT" > p.txt; git add p.txt
printf 'x %s y\n' "$TERM_WALL_PLANT" > p.txt; git add p.txt; git commit -qm plant
rc=0; out=$(env -u GITHUB_EVENT_PATH bash "$wall") || rc=$?
[ "$rc" -eq 1 ] || { echo "::error::the wall did not exit 1 on a planted fault (exit $rc)"; exit 1; }
case $out in *"$TERM_WALL_PLANT"*) echo "::error::the wall printed the raw plant"; exit 1;; esac
Expand Down
Loading