Documentation
¶
Overview ¶
Package termsafe holds the one canonical sanitiser for a string built from untrusted content (commit subjects, refs, file paths, repo prose, error text echoing a malformed file) before it is written to a terminal or a human report. It is the single primitive every render path shares — the terminal analogue of fsutil's guarded read: a hostile or archived repository controls this text, and left raw it can spoof, corrupt, or visually rewrite the report.
Index ¶
- func CleanProse(s string, capBytes int) string
- func CleanProseLine(s string, capBytes int) string
- func EncodeHiddenRunes(s string) string
- func IsHidden(r rune) bool
- func OpensBalancedCodeSpan(s string) bool
- func Sanitize(s string) string
- func SanitizeAll(in []string) []string
- func SanitizeBlock(s string) string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CleanProse ¶
CleanProse neutralises one untrusted prose field and caps it at capBytes, preserving interior whitespace runs. The cap is applied last and the result is re-trimmed, so a cut landing mid-word leaves no dangling space; a cut landing mid-rune drops the partial rune rather than emitting replacement bytes.
func CleanProseLine ¶
CleanProseLine is CleanProse for a field that must occupy exactly one line: every whitespace run collapses to a single space before the cap is applied. Use it wherever the prose lands in a file whose line structure is machine-read.
func EncodeHiddenRunes ¶ added in v0.6.8
EncodeHiddenRunes percent-encodes every rune the terminal sanitizer would mask — C0/DEL, the 2-byte-encoded C1 range, bidi overrides and zero-width runes — so a value built from an untrusted source (a redirect-supplied final address, say) is recorded losslessly but can no longer smuggle terminal escapes or Trojan-Source reordering into a JSON surface or a committed baseline. This is the canonical encoder for the JSON/record boundary that Sanitize's doc note points at (iss-359); a value the terminal render path masks with '?' is encoded here instead, so the byte is preserved rather than substituted.
It is lossless in both directions a mask is not: a hidden rune is percent-encoded as its UTF-8 bytes, and a byte that is not valid UTF-8 — which strings.Map would silently rewrite to U+FFFD — is percent-encoded raw. A canonical address never carries these runes (net/url rejects C0 outright and percent-encodes the path itself), so encoding them cannot break a legitimate final URL.
func IsHidden ¶ added in v0.10.0
IsHidden reports whether r is a bidirectional control or a zero-width rune: a rune that makes rendered text read differently from its bytes, or hides text altogether. It is the predicate Sanitize masks these with, exported so a parser that refuses such runes outright judges them by the same set.
func OpensBalancedCodeSpan ¶ added in v0.7.1
OpensBalancedCodeSpan reports whether s BEGINS with a backtick run that a later run of exactly the same length closes — that is, whether s starts with a code span rather than with literal backticks.
It exists so a caller that escapes leading block markers can ask the cleaner's own grammar instead of guessing. A leading run that opens a balanced span opens no block: a backtick fence's info string may not contain backticks, so a run with a matching closer on the same line is an inline span by construction, and escaping it would kill the shelter the cleaner's exemption relies on. Only an unbalanced leading run is a fence, and the cleaner no longer emits one.
func Sanitize ¶
Sanitize replaces every terminal-display attack rune with a visible '?' (tab becomes a space). It neutralises:
- C0 controls (<0x20) and DEL (0x7f) — these carry ESC, so a raw ANSI escape in a commit subject could recolour, move the cursor, or corrupt the report; a newline is masked too, so an injected line break cannot forge extra lines;
- the C1 range (0x80–0x9F) — U+009B (CSI) acts like ESC[ on an 8-bit terminal, so masking ESC (a C0 control) alone would leave an equivalent path open;
- bidirectional override/isolate controls (the "Trojan Source" class) and zero-width characters, which reorder or hide text so the rendered line differs from the bytes — the reader sees something the file does not say.
JSON output is NOT covered by encoding/json's own escaping: it escapes only C0 (below 0x20) and U+2028/9 — DEL, the C1 range, bidi overrides and zero-width runes pass through raw (pinned by TestJSONLeavesC1AndBidiRaw). A JSON surface whose strings reach a terminal or a committed record needs its values shape-checked or encoded at the boundary that produced them (iss-359); this function is the mask for the human/terminal render path.
func SanitizeAll ¶
SanitizeAll sanitises every member of a slice, returning a new slice.
func SanitizeBlock ¶
SanitizeBlock sanitises multi-line text while keeping its line structure: each line is sanitised on its own and the line terminators are preserved. Sanitize masks a newline (an injected line break must not forge extra report lines), which is right for a value interpolated into one line and wrong for output that IS lines — a rendered diff or a quoted file excerpt, where flattening the terminators destroys the artefact the reader needs. Use this only where the line breaks are the render's own, never for a single untrusted value.
The carriage return of a CRLF pair survives; every other carriage return is masked. That distinction is load-bearing in both directions: a BARE CR moves the cursor to the column zero of the line being written, so it can overprint what the reader already saw — the attack Sanitize masks — whereas the CR of a CRLF is immediately committed by its newline and can overprint nothing. And a rendered patch against a CRLF file must carry those pairs or no patch tool will apply it, so dropping them would silently void the artefact.
Types ¶
This section is empty.