test/bans.sh
Ref: Size: 11.7 KiB History
#!/bin/sh
# bans.sh — the folder rules' own pin. `make check` runs it after
# `zig build check`.
#
# build.zig's `checkSourceBan` reads the production lines of every file
# under src/ for bytes no import graph can catch. Nothing asserted that it
# still bites: a needle list edited down to nothing, a folder list that
# stopped naming a folder, or a `test` skip widened to swallow production
# lines would all leave a green tree saying the rules hold. This plants ONE
# representative needle per rule in a scratch file under one banned folder
# and requires the refusal by name, then plants one INSIDE a test block and
# requires no refusal at all — the skip is as load-bearing as the ban, since
# a test that drives a VT with escape bytes must stay legal.
#
# The scratch file is removed by an EXIT trap, so a failing assertion never
# leaves a planted needle behind to refuse every later build in the tree.
#
# The last check in this file is not build.zig's at all: it reads the SHELL
# scripts for one shape no compiler sees, for the same reason as the rest.
# A rule nothing asserts is a rule that has already stopped biting.
set -u
ZIG="${1:-deps/zig/zig}"
ROOT=$(cd "$(dirname "$0")/.." && pwd)
cd "$ROOT" || exit 1
[ -x "$ZIG" ] || { echo "bans FAIL: no zig at $ZIG (pass it as \$1)"; exit 1; }
PROBE=""
cleanup() { [ -z "$PROBE" ] || rm -f "$PROBE"; }
trap cleanup EXIT INT TERM
FAILED=0
# plant FOLDER BODY — a fresh .zig file under FOLDER holding BODY. The
# basename is this run's pid and carries no prefix any other build check
# claims: `server_test_` would trip checkSiblingTestsReached first and the
# rule under test would never be reached.
plant() {
PROBE="$1/zz_bans_probe_$$.zig"
printf '%s\n' "$2" > "$PROBE"
}
unplant() {
[ -z "$PROBE" ] || rm -f "$PROBE"
PROBE=""
}
# must_break RULE FOLDER BODY — the planted needle has to stop the build
# with that rule's own fatal.
#
# `zig build check` is the gate's own spelling and is what runs here.
# `checkSourceBans` is called from `build(b)`, so the refusal happens while
# the build graph is still being constructed: no step runs, no test is
# compiled, and the whole call costs about as long as reading src/.
must_break() {
_rule="$1"
plant "$2" "$3"
_out=$("$ZIG" build check 2>&1)
_rc=$?
unplant
if [ "$_rc" -eq 0 ]; then
echo "bans FAIL: rule $_rule: a needle planted in $2 built cleanly (rc 0)."
echo " The rule is not reading that folder's production lines."
FAILED=1
return
fi
case "$_out" in
*"folder rule $_rule broken"*)
echo "bans ok: rule $_rule refuses its needle in $2" ;;
*)
echo "bans FAIL: rule $_rule: the build failed, but not with"
echo " 'folder rule $_rule broken'. What it said:"
echo "$_out" | tail -20
FAILED=1 ;;
esac
}
# must_skip FOLDER BODY — the same needles inside a `test` block are legal,
# and the build must not so much as mention a rule.
#
# `zig build --help` rather than `zig build check`: this case has to reach
# the END of `build(b)` to prove nothing fatalled, and `check` would then
# go on to run the whole unit suite to answer a question the configure pass
# has already answered. `--help` constructs the identical graph — same
# `build(b)`, same `checkSourceBans` call — and runs no step.
must_skip() {
plant "$1" "$2"
_out=$("$ZIG" build --help 2>&1)
_rc=$?
unplant
if [ "$_rc" -ne 0 ] || [ "${_out#*folder rule}" != "$_out" ]; then
echo "bans FAIL: a needle inside a test block was refused (rc $_rc)."
echo " The test skip is what lets a test drive a VT or spawn"
echo " a shell; without it those tests cannot be written."
echo "$_out" | tail -20
FAILED=1
else
echo "bans ok: a needle inside a test block is skipped"
fi
}
# One needle per rule, each in a folder that rule names, and each chosen so
# no EARLIER rule matches the same line — the checker fatals on the first
# hit, so a body that tripped rule 4 would say nothing about rule 7.
must_break 4 src/client 'const probe = "isatty";'
must_break 5 src/engine 'const probe = "/bin/sh";'
must_break 6 src/server 'const probe = std.posix.fork();'
must_break 7 src/server 'const probe = std.os.linux.O.RDONLY;'
# An exemption permits one occurrence, never the rest of the file.
must_skip src/client '// folder rule 4 exemption: fixture checks a permitted occurrence.
const permitted = "isatty";'
must_break 4 src/client '// folder rule 4 exemption: fixture checks a permitted occurrence.
const permitted = "isatty";
const forbidden = "tcgetattr";'
must_break 4 src/client '// folder rule 4 exemption:
const forbidden = "isatty";'
# A container-level `test` opens at column 0 and its `}` closes there,
# which is the line arithmetic checkSourceBan relies on and `zig fmt
# --check` already guarantees.
must_skip src/server 'test "bans probe" {
const probe = std.os.linux.O.RDONLY;
_ = probe;
}'
# ---- the shell rule: no backtick inside an UNQUOTED heredoc -------------
#
# In an unquoted heredoc the shell runs a backquoted word as a command
# substitution wherever it stands, comments included. Most ptyclient scripts
# in this suite are QUOTED heredocs and safe; the ones that interpolate a
# fingerprint or a port have to leave the delimiter bare, and those are the
# ones this rule is for. This repo quotes a key or a verb in backticks
# everywhere else,
# so writing one into such a comment is the natural thing to do and it is a
# trap. There IS an expect program on macOS: the shell starts it, it reads
# the rest of the heredoc as its own script, and the group hangs until its
# budget runs out with nothing on stdout. That cost four gate runs on the
# macOS port before anyone spelled the rule (2026-09-04), and one of the
# four was the comment written to explain it.
#
# The same four directories shellGate compiles, so a script added tomorrow
# is covered without anybody remembering to add it. A heredoc whose
# delimiter is QUOTED is left alone, which is what makes this a rule about
# the opener rather than a ban on a byte.
heredoc_backticks() {
awk '
# The delimiter this line opens a heredoc with, or "" for a line that
# opens none. A three-angle form is a here-string and has no body. A
# QUOTED delimiter performs no substitution in its body, so there is
# nothing there to catch; the quotes are spelled \047 and \042 so this
# whole program fits inside one shell-quoted string.
function delim_of(line, p, rest, c) {
if (index(line, "<<<") > 0) return ""
p = index(line, "<<")
if (p == 0) return ""
rest = substr(line, p + 2)
sub(/^-/, "", rest)
sub(/^[ \t]+/, "", rest)
c = substr(rest, 1, 1)
if (c == "\047" || c == "\042") return ""
if (match(rest, /^[A-Za-z_][A-Za-z0-9_]*/)) return substr(rest, RSTART, RLENGTH)
return ""
}
FNR == 1 { inbody = 0 }
# Matched ANYWHERE on the line and not just at its end: the site this
# rule was written for spells its opener with a trailing space after
# the delimiter, and a first sweep that anchored on end-of-line walked
# straight past it.
!inbody {
d = delim_of($0)
if (d != "") { inbody = 1; delim = d; opened = FNR }
next
}
{
line = $0
gsub(/^[ \t]+|[ \t]+$/, "", line)
if (line == delim) { inbody = 0; next }
if (index($0, "\140") > 0)
printf "%s:%d (heredoc opened at %d): %s\n", FILENAME, FNR, opened, $0
}
' "$@"
}
# The positive: the check has to SEE the shape, in both spellings of the
# opener, or it is a line in a file that never fires. Planted rather than
# quoted out of the tree, because the tree is the negative below and a
# check that only ever reads a clean tree proves nothing.
SHPROBE="$ROOT/test/zz_bans_heredoc_$$.sh"
# printf and not a heredoc, because this script would otherwise have to
# contain the very shape it refuses — and the trailing space after the
# first opener is half of what is being pinned.
#
# The FIXTURE's opener is BUILT rather than written. The sweep's opener
# detector spares a quoted delimiter, and it sees a quote right after the
# two angle brackets in the awk program at the top of `heredoc_backticks`,
# so those literals do not fool it — but a bare opener written out here
# would make it read this script's own fixture as a heredoc and report the
# three backticks planted below. The only ways out of that are an exception
# for this file or a rule that stops covering it. Neither is worth a pair of
# characters.
_a='<'
_hd="$_a$_a"
printf '%s\n' \
"cat ${_hd}EOF " \
'# prose quoting a `verb` the way this repo quotes one' \
'EOF' \
"cat ${_hd}-INNER" \
'# and a `second` under the dash form of the opener' \
'INNER' \
"cat ${_hd}'QUOTED'" \
'# a `third` here is inert, because the delimiter is quoted' \
'QUOTED' > "$SHPROBE"
_hits=$(heredoc_backticks "$SHPROBE" | wc -l | tr -d ' ')
rm -f "$SHPROBE"
if [ "$_hits" -eq 2 ]; then
echo "bans ok: the heredoc rule catches both openers and spares a quoted one"
else
echo "bans FAIL: the heredoc rule found $_hits of the 2 planted backticks."
echo " An unquoted opener must be caught, including the spelling"
echo " with a trailing space after the delimiter, and a quoted"
echo " one must be spared."
FAILED=1
fi
# The negative: the tree as it stands.
_sh=""
for _d in test tools deps deps/quic; do
for _f in "$ROOT/$_d"/*.sh; do
[ -f "$_f" ] && _sh="$_sh $_f"
done
done
# shellcheck disable=SC2086 # the list is words, and split is the point
_left=$(heredoc_backticks $_sh)
if [ -z "$_left" ]; then
echo "bans ok: no backtick inside an unquoted heredoc"
else
echo "bans FAIL: a backtick inside an unquoted heredoc:"
printf '%s\n' "$_left" | sed 's/^/ /'
echo " The shell runs it as a command substitution. Say the word"
echo " in plain prose instead; there is no way to quote it here."
FAILED=1
fi
# The module split, which no folder rule can express: a folder rule reads
# bytes inside a file, and this is about which module a file belongs to.
# `checkGrantsUsed` refuses a grant nobody spends, but nothing refuses a
# grant somebody adds — a client row that reached for the engine again
# would build green and put ghostty-vt back in every client binary.
split_fail() {
echo "bans FAIL: $1"
FAILED=1
}
# The term module is the wasm root and the client's whole view of the wire,
# so it spells no emulator. engine.zig is the one file allowed the
# dependency, and term.zig must not re-export it: a re-export would hand
# every term importer the engine back without touching the table.
if grep -l '"ghostty-vt"' src/engine/term.zig src/engine/protocol.zig \
src/engine/replica.zig src/engine/grid.zig >/dev/null 2>&1; then
split_fail "a term child imports ghostty-vt"
fi
if grep -q 'engine.zig' src/engine/term.zig; then
split_fail "term.zig re-exports the engine"
fi
# No client row links the engine in production: the client parses no VT and
# authors screens through an engine only under test. The table writes one
# row per line, so a row's `.imports` list is on the line that names it, and
# `.test_imports` cannot match because the character before `imports` there
# is an underscore rather than a dot.
for row in client wall webhub term mux; do
if grep -E "\.name = \"$row\"" build.zig | grep -qE '\.imports = &\.[{][^}]*"engine"'; then
split_fail "module row $row imports engine outside its tests"
fi
done
[ "$FAILED" -eq 0 ] && echo "bans ok: the engine is the daemon's alone"
[ "$FAILED" -eq 0 ] || { echo "bans: FAILED"; exit 1; }
echo "bans: every folder rule bit, the test skip held, and no heredoc runs its own comments"