fe38451f
test: bans.sh refuses a backtick inside an unquoted heredoc
a73x 2026-09-04 10:16
Commit message
test/bans.sh
| Old | New | ||
|---|---|---|---|
| @@ -14,6 +14,10 @@ | |||
| 14 | # | 14 | # |
| 15 | # The scratch file is removed by an EXIT trap, so a failing assertion never | 15 | # The scratch file is removed by an EXIT trap, so a failing assertion never |
| 16 | # leaves a planted needle behind to refuse every later build in the tree. | 16 | # leaves a planted needle behind to refuse every later build in the tree. |
| 17 | # | ||
| 18 | # The last check in this file is not build.zig's at all: it reads the SHELL | ||
| 19 | # scripts for one shape no compiler sees, for the same reason as the rest. | ||
| 20 | # A rule nothing asserts is a rule that has already stopped biting. | ||
| 17 | set -u | 21 | set -u |
| 18 | 22 | ||
| 19 | ZIG="${1:-deps/zig/zig}" | 23 | ZIG="${1:-deps/zig/zig}" |
| @@ -111,5 +115,119 @@ must_skip src/server 'test "bans probe" { | |||
| 111 | _ = probe; | 115 | _ = probe; |
| 112 | }' | 116 | }' |
| 113 | 117 | ||
| 118 | # ---- the shell rule: no backtick inside an UNQUOTED heredoc ------------- | ||
| 119 | # | ||
| 120 | # In an unquoted heredoc the shell runs a backquoted word as a command | ||
| 121 | # substitution wherever it stands, comments included. Every ptyclient script | ||
| 122 | # in this suite is an unquoted heredoc — they interpolate a fingerprint or a | ||
| 123 | # port — and this repo quotes a key or a verb in backticks everywhere else, | ||
| 124 | # so writing one into such a comment is the natural thing to do and it is a | ||
| 125 | # trap. There IS an expect program on macOS: the shell starts it, it reads | ||
| 126 | # the rest of the heredoc as its own script, and the group hangs until its | ||
| 127 | # budget runs out with nothing on stdout. That cost four gate runs on the | ||
| 128 | # macOS port before anyone spelled the rule (2026-09-04), and one of the | ||
| 129 | # four was the comment written to explain it. | ||
| 130 | # | ||
| 131 | # The same four directories shellGate compiles, so a script added tomorrow | ||
| 132 | # is covered without anybody remembering to add it. A heredoc whose | ||
| 133 | # delimiter is QUOTED is left alone, which is what makes this a rule about | ||
| 134 | # the opener rather than a ban on a byte. | ||
| 135 | heredoc_backticks() { | ||
| 136 | awk ' | ||
| 137 | # The delimiter this line opens a heredoc with, or "" for a line that | ||
| 138 | # opens none. A three-angle form is a here-string and has no body. A | ||
| 139 | # QUOTED delimiter performs no substitution in its body, so there is | ||
| 140 | # nothing there to catch; the quotes are spelled \047 and \042 so this | ||
| 141 | # whole program fits inside one shell-quoted string. | ||
| 142 | function delim_of(line, p, rest, c) { | ||
| 143 | if (index(line, "<<<") > 0) return "" | ||
| 144 | p = index(line, "<<") | ||
| 145 | if (p == 0) return "" | ||
| 146 | rest = substr(line, p + 2) | ||
| 147 | sub(/^-/, "", rest) | ||
| 148 | sub(/^[ \t]+/, "", rest) | ||
| 149 | c = substr(rest, 1, 1) | ||
| 150 | if (c == "\047" || c == "\042") return "" | ||
| 151 | if (match(rest, /^[A-Za-z_][A-Za-z0-9_]*/)) return substr(rest, RSTART, RLENGTH) | ||
| 152 | return "" | ||
| 153 | } | ||
| 154 | FNR == 1 { inbody = 0 } | ||
| 155 | # Matched ANYWHERE on the line and not just at its end: the site this | ||
| 156 | # rule was written for spells its opener with a trailing space after | ||
| 157 | # the delimiter, and a first sweep that anchored on end-of-line walked | ||
| 158 | # straight past it. | ||
| 159 | !inbody { | ||
| 160 | d = delim_of($0) | ||
| 161 | if (d != "") { inbody = 1; delim = d; opened = FNR } | ||
| 162 | next | ||
| 163 | } | ||
| 164 | { | ||
| 165 | line = $0 | ||
| 166 | gsub(/^[ \t]+|[ \t]+$/, "", line) | ||
| 167 | if (line == delim) { inbody = 0; next } | ||
| 168 | if (index($0, "\140") > 0) | ||
| 169 | printf "%s:%d (heredoc opened at %d): %s\n", FILENAME, FNR, opened, $0 | ||
| 170 | } | ||
| 171 | ' "$@" | ||
| 172 | } | ||
| 173 | |||
| 174 | # The positive: the check has to SEE the shape, in both spellings of the | ||
| 175 | # opener, or it is a line in a file that never fires. Planted rather than | ||
| 176 | # quoted out of the tree, because the tree is the negative below and a | ||
| 177 | # check that only ever reads a clean tree proves nothing. | ||
| 178 | SHPROBE="$ROOT/test/zz_bans_heredoc_$$.sh" | ||
| 179 | # printf and not a heredoc, because this script would otherwise have to | ||
| 180 | # contain the very shape it refuses — and the trailing space after the | ||
| 181 | # first opener is half of what is being pinned. | ||
| 182 | # | ||
| 183 | # The opener is BUILT rather than written, so that no line of this file | ||
| 184 | # holds the two angle brackets next to each other. Spelling them here would | ||
| 185 | # make the negative sweep below read this script's own fixture as a heredoc | ||
| 186 | # and report its three backticks, and the only ways out of that are an | ||
| 187 | # exception for this file or a rule that stops covering it. Neither is | ||
| 188 | # worth a pair of characters. | ||
| 189 | _a='<' | ||
| 190 | _hd="$_a$_a" | ||
| 191 | printf '%s\n' \ | ||
| 192 | "cat ${_hd}EOF " \ | ||
| 193 | '# prose quoting a `verb` the way this repo quotes one' \ | ||
| 194 | 'EOF' \ | ||
| 195 | "cat ${_hd}-INNER" \ | ||
| 196 | '# and a `second` under the dash form of the opener' \ | ||
| 197 | 'INNER' \ | ||
| 198 | "cat ${_hd}'QUOTED'" \ | ||
| 199 | '# a `third` here is inert, because the delimiter is quoted' \ | ||
| 200 | 'QUOTED' > "$SHPROBE" | ||
| 201 | _hits=$(heredoc_backticks "$SHPROBE" | wc -l | tr -d ' ') | ||
| 202 | rm -f "$SHPROBE" | ||
| 203 | if [ "$_hits" -eq 2 ]; then | ||
| 204 | echo "bans ok: the heredoc rule catches both openers and spares a quoted one" | ||
| 205 | else | ||
| 206 | echo "bans FAIL: the heredoc rule found $_hits of the 2 planted backticks." | ||
| 207 | echo " An unquoted opener must be caught, including the spelling" | ||
| 208 | echo " with a trailing space after the delimiter, and a quoted" | ||
| 209 | echo " one must be spared." | ||
| 210 | FAILED=1 | ||
| 211 | fi | ||
| 212 | |||
| 213 | # The negative: the tree as it stands. | ||
| 214 | _sh="" | ||
| 215 | for _d in test tools deps deps/quic; do | ||
| 216 | for _f in "$ROOT/$_d"/*.sh; do | ||
| 217 | [ -f "$_f" ] && _sh="$_sh $_f" | ||
| 218 | done | ||
| 219 | done | ||
| 220 | # shellcheck disable=SC2086 # the list is words, and split is the point | ||
| 221 | _left=$(heredoc_backticks $_sh) | ||
| 222 | if [ -z "$_left" ]; then | ||
| 223 | echo "bans ok: no backtick inside an unquoted heredoc" | ||
| 224 | else | ||
| 225 | echo "bans FAIL: a backtick inside an unquoted heredoc:" | ||
| 226 | printf '%s\n' "$_left" | sed 's/^/ /' | ||
| 227 | echo " The shell runs it as a command substitution. Say the word" | ||
| 228 | echo " in plain prose instead; there is no way to quote it here." | ||
| 229 | FAILED=1 | ||
| 230 | fi | ||
| 231 | |||
| 114 | [ "$FAILED" -eq 0 ] || { echo "bans: FAILED"; exit 1; } | 232 | [ "$FAILED" -eq 0 ] || { echo "bans: FAILED"; exit 1; } |
| 115 | echo "bans: every folder rule bit, and the test skip held" | 233 | echo "bans: every folder rule bit, the test skip held, and no heredoc runs its own comments" |