diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index eed7894..bbfa485 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -113,10 +113,16 @@ jobs: id: lint run: | # Inline invisible character detection (from empty-linter's core patterns). - # Checks for: zero-width spaces, zero-width joiners, BOM, soft hyphens, - # non-breaking spaces, null bytes, and other invisible Unicode in source files. + # Checks for: C0 control characters, zero-width spaces/joiners, BOM, + # soft hyphens, NBSP, bidi overrides, word joiner, null bytes. + # + # ⚠ CODEPOINT escapes \x{a0}, NOT UTF-8 byte sequences \xc2\xa0. grep -P + # matches CHARACTERS, so the byte form never matched anything: the previous + # pattern caught 0 of 6 invisible-character test cases. Verified 2026-08-27. + # ⚠ -a is required or grep skips any file containing a NUL as "binary". + # ⚠ A LEADING BOM cannot be matched by grep (it strips one); checked separately. set +e - PATTERNS='\xc2\xa0|\xe2\x80\x8b|\xe2\x80\x8c|\xe2\x80\x8d|\xef\xbb\xbf|\xc2\xad|\xe2\x80\x8e|\xe2\x80\x8f|\xe2\x80\xaa|\xe2\x80\xab|\xe2\x80\xac|\xe2\x80\xad|\xe2\x80\xae|\x00' + PATTERNS='\x00|[\x01-\x08\x0B\x0C\x0E-\x1F]|\x{a0}|\x{ad}|\x{200b}|\x{200c}|\x{200d}|\x{200e}|\x{200f}|\x{202a}|\x{202b}|\x{202c}|\x{202d}|\x{202e}|\x{2060}|\x{feff}' find "$GITHUB_WORKSPACE" \ -not -path '*/.git/*' -not -path '*/node_modules/*' \ -not -path '*/.deno/*' -not -path '*/target/*' \ @@ -127,7 +133,16 @@ jobs: -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ - -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null + -exec grep -aPrl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null + # A LEADING BOM is stripped by grep before matching, so it must be + # checked byte-wise. Mid-file BOMs are caught by the pattern above. + while IFS= read -r bf; do + [ -z "$bf" ] && continue + if [ "$(head -c3 "$bf" | od -An -tx1 | tr -d " ")" = "efbbbf" ]; then + echo "$bf" >> /tmp/empty-lint-results.txt + fi + done < <(find "$GITHUB_WORKSPACE" -not -path '*/.git/*' -not -path '*/node_modules/*' -type f \( -name '*.yml' -o -name '*.yaml' -o -name '*.json' -o -name '*.toml' \) 2>/dev/null) + sort -u -o /tmp/empty-lint-results.txt /tmp/empty-lint-results.txt 2>/dev/null || true EL_EXIT=$? set -e diff --git a/config.ncl b/config.ncl index 81a0de0..ab9a7e5 100644 --- a/config.ncl +++ b/config.ncl @@ -34,6 +34,10 @@ let OutputFormat = [| 'text, 'json, 'hex |] in # Known invisible artifacts (uses proven SafeHex for detection) artifacts = [ { name = "NULL", hex = "0x00", severity = 'Critical, fix_action = "remove" }, + # C0 controls U+0001..U+001F excluding TAB/LF/CR. Illegal in YAML and + # fatal to symbolic parsers; a stray 0x08 made a workflow unloadable and + # therefore silently non-running. Matched as a RANGE by is_c0_control. + { name = "C0-CONTROL", hex = "0x01-0x1F", severity = 'Critical, fix_action = "remove" }, { name = "NBSP", hex = "0xA0", severity = 'Error, fix_action = "replace:20" }, { name = "ZWSP", hex = "0x200B", severity = 'Error, fix_action = "remove" }, { name = "BOM", hex = "0xFEFF", severity = 'Warning, fix_action = "remove" }, diff --git a/stdlib/ByteDetector.affine b/stdlib/ByteDetector.affine index 0d229c2..1b317b3 100644 --- a/stdlib/ByteDetector.affine +++ b/stdlib/ByteDetector.affine @@ -41,6 +41,13 @@ pub fn known_artifacts() -> [ArtifactDef] { ] } +// C0 control characters (U+0001..U+001F) are illegal in YAML and corrupt +// symbolic parsers. TAB (9), LF (10) and CR (13) are legitimate whitespace +// and are excluded. NULL (0) keeps its own named entry above. +pub fn is_c0_control(byte_val: Int) -> Bool { + byte_val > 0 && byte_val < 32 && byte_val != 9 && byte_val != 10 && byte_val != 13 +} + pub fn get_artifact_def(byte_val: Int) -> Option { let defs = known_artifacts(); for d in defs { @@ -48,6 +55,9 @@ pub fn get_artifact_def(byte_val: Int) -> Option { return Some(d); } } + if is_c0_control(byte_val) { + return Some(#{ name: "C0-CONTROL", byte_value: byte_val, severity: Critical, fix_action: "remove" }); + } None }