|
| 1 | +#!/usr/bin/env bash |
| 2 | +# |
| 3 | +# Guard: a path that came out of a directory walk is never narrowed directly. |
| 4 | +# |
| 5 | +# WHY |
| 6 | +# |
| 7 | +# On Windows `std::filesystem::path::string()` converts the native (wide) name |
| 8 | +# through the process ANSI code page and THROWS std::system_error when a |
| 9 | +# character has no spelling there — "No mapping for the Unicode character |
| 10 | +# exists in the target multi-byte code page". Off Windows the same call is a |
| 11 | +# copy that cannot fail, so nothing on Linux or macOS — including their tests — |
| 12 | +# can see the hazard. |
| 13 | +# |
| 14 | +# It has cost two incidents, each wearing a different mask: |
| 15 | +# |
| 16 | +# #230 a walked index tree held a CJK-named issue template; the throw |
| 17 | +# escaped to std::terminate → __fastfail → git-bash reported a bare |
| 18 | +# exit 127, which reads as "command not found". |
| 19 | +# #516 cpp-httplib ships test/www/<CJK>Dir/ and the `include_dirs = { "*" }` |
| 20 | +# convention walks the whole extracted tarball; the throw escaped to |
| 21 | +# main()'s catch as `internal: unhandled exception`, which reads as an |
| 22 | +# extraction/encoding bug in the downloader. |
| 23 | +# |
| 24 | +# #231 hardened three call sites and missed a fourth — `is_excluded_walk_dir`, |
| 25 | +# which runs ONE LINE EARLIER in the same walk loop. A fifth site is what this |
| 26 | +# script exists to make expensive. |
| 27 | +# |
| 28 | +# THE RULE |
| 29 | +# |
| 30 | +# * Comparing against ASCII literals? Compare as `path`. Do not narrow. |
| 31 | +# * Need a stable identity (hash, key)? `u8string()` — UTF-8 everywhere, |
| 32 | +# never touches the code page. |
| 33 | +# * Need a build-facing string (compiler |
| 34 | +# argument, ninja file, CDB)? `mcpp::modgraph::try_narrow()`, |
| 35 | +# and handle the nullopt. |
| 36 | +# |
| 37 | +# WHAT THIS DOES AND DOES NOT CATCH |
| 38 | +# |
| 39 | +# It greps the leaf layers that walk trees mcpp does not control. It catches a |
| 40 | +# NEW direct narrowing written there. It does NOT catch a path narrowed after |
| 41 | +# being passed out to another layer — that is what the try_narrow convention is |
| 42 | +# for, and no grep can enforce it. Do not read a pass here as "audited". |
| 43 | +# |
| 44 | +# `.extension()` is deliberately NOT matched: an extension is ASCII in every |
| 45 | +# case that reaches these predicates, so matching it would produce only noise — |
| 46 | +# and noise is how a gate gets suppressed. |
| 47 | +# |
| 48 | +# Escape hatch: `// NARROW-OK: <reason>` on the line itself or within the two |
| 49 | +# lines above it. Use it when the input provably cannot carry an unspellable |
| 50 | +# name, and say why — a bare marker with no argument is worse than no gate, |
| 51 | +# because it reads as "someone checked". |
| 52 | +# |
| 53 | +# Usage: bash .github/tools/check_narrow_conversions.sh [repo_dir] |
| 54 | + |
| 55 | +set -uo pipefail |
| 56 | + |
| 57 | +REPO_DIR="${1:-$(pwd)}" |
| 58 | +cd "$REPO_DIR" || { echo "FAIL: cannot cd to $REPO_DIR" >&2; exit 1; } |
| 59 | + |
| 60 | +# SCOPE, and why it is this narrow. |
| 61 | +# |
| 62 | +# The hazard needs a path from a tree MCPP DOES NOT CONTROL. Two directories |
| 63 | +# qualify: src/modgraph walks arbitrary package and project trees, and |
| 64 | +# src/scaffold enumerates third-party template providers. |
| 65 | +# |
| 66 | +# The first draft of this guard also covered src/pack and src/manifest and |
| 67 | +# produced 22 hits, ~20 of them false: src/pack narrows names MCPP ITSELF |
| 68 | +# produced (staging roots, built binaries, strip artifacts — all derived from |
| 69 | +# validated ASCII package/target names), and src/manifest only ever narrows an |
| 70 | +# `.extension()`. A gate with twenty false positives is a gate that gets |
| 71 | +# suppressed within a month, and the suppression then becomes the only record |
| 72 | +# that a rule existed. The real hazards in those two directories were fixed by |
| 73 | +# hand instead (pack/digest.cppm, which feeds on an unfiltered |
| 74 | +# recursive_directory_iterator over a published package). |
| 75 | +# |
| 76 | +# So: a pass here does NOT mean "the tree is audited". It means no NEW direct |
| 77 | +# narrowing was written where this class originates. |
| 78 | +SCAN_DIRS="src/modgraph src/scaffold" |
| 79 | + |
| 80 | +PATTERN='\.(filename|stem)\(\)\.(generic_)?string\(\)' |
| 81 | + |
| 82 | +fail=0 |
| 83 | +found=0 |
| 84 | + |
| 85 | +for dir in $SCAN_DIRS; do |
| 86 | + [ -d "$dir" ] || { echo "FAIL: $dir does not exist — this guard has gone stale" >&2; exit 1; } |
| 87 | + while IFS= read -r file; do |
| 88 | + # Strip // line comments before matching: several of these files DESCRIBE |
| 89 | + # the forbidden call in prose (that is the point of the comments), and a |
| 90 | + # guard that trips on its own documentation gets deleted. |
| 91 | + while IFS=: read -r lineno text; do |
| 92 | + [ -n "${lineno:-}" ] || continue |
| 93 | + found=1 |
| 94 | + # NARROW-OK on the line itself, or on either of the two lines above it. |
| 95 | + ctx=$(sed -n "$(( lineno > 2 ? lineno - 2 : 1 )),${lineno}p" "$file") |
| 96 | + case "$ctx" in |
| 97 | + *NARROW-OK:*) continue ;; |
| 98 | + esac |
| 99 | + echo "FAIL: $file:$lineno narrows a path directly:" >&2 |
| 100 | + echo " ${text# }" >&2 |
| 101 | + fail=1 |
| 102 | + done < <(sed 's://.*::' "$file" | grep -nE "$PATTERN") |
| 103 | + done < <(find "$dir" -type f \( -name '*.cppm' -o -name '*.cpp' -o -name '*.hpp' \) | sort) |
| 104 | +done |
| 105 | + |
| 106 | +if [ "$fail" = 1 ]; then |
| 107 | + cat >&2 <<'EOF' |
| 108 | +
|
| 109 | + Use one of: |
| 110 | + - compare as std::filesystem::path (ASCII literals; no narrowing) |
| 111 | + - p.u8string() (stable identity: hashes, keys) |
| 112 | + - mcpp::modgraph::try_narrow(p) (build-facing; handle nullopt) |
| 113 | + or annotate with `// NARROW-OK: <why this input cannot carry such a name>`. |
| 114 | +
|
| 115 | + Background: mcpp#516, mcpp#230, src/modgraph/glob.cppm. |
| 116 | +EOF |
| 117 | + exit 1 |
| 118 | +fi |
| 119 | + |
| 120 | +if [ "$found" = 0 ]; then |
| 121 | + echo "ok: no direct path narrowing in $SCAN_DIRS" |
| 122 | +else |
| 123 | + echo "ok: every direct narrowing in $SCAN_DIRS carries a NARROW-OK rationale" |
| 124 | +fi |
| 125 | +exit 0 |
0 commit comments