Documentation
¶
Overview ¶
Package textsafe holds the one predicate for the characters that forge, reorder, or hide text: the C1 control block and the zero-width / bidi "invisible" set. It exists because the 2026-09-05 red-probe round (round 4) proved that every scrub, gate, and canonicalizer in the tree walked the C0 range and stopped there, so a C1 CSI byte or a bidi override rode through headers, log lines, storage keys, page titles, terminal transcripts, and identity canonicalizers untouched. Each sink combines its existing C0/DEL handling with the helpers here; the controlbytes and invisibleident analyzers credit a scrub only once its body reaches this set.
The set is deliberately narrow: combining marks and ordinary diacritics fall through. Only codepoints with no visible glyph of their own, whose sole effect is to rearrange or vanish the surrounding text, are named. It mirrors the list that framework/pagination first enumerated as isUnicodeInvisible.
Index ¶
- func ContainsInvisible(s string) bool
- func ContainsUnsafe(s string) bool
- func HasControlBytes(s string) bool
- func IsC1(r rune) bool
- func IsInvisible(r rune) bool
- func IsUnsafe(r rune) bool
- func Recovered(v any) string
- func SanitizeControlBytes(s string) string
- func ScrubControlBytes(s string) string
- func StripInvisible(s string) string
- func StripUnsafe(s string) string
- func Truncate(s string, max int) string
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ContainsInvisible ¶
ContainsInvisible reports whether s carries any invisible/bidi codepoint. Refusal-style gates use it in place of stripping.
func ContainsUnsafe ¶
ContainsUnsafe reports whether s carries any C0/DEL/C1/invisible codepoint.
func HasControlBytes ¶ added in v0.86.0
HasControlBytes reports whether s carries any ASCII C0 control byte (0x00–0x1F) or DEL (0x7F). It walks bytes, not runes: a valid UTF-8 sequence never contains a byte below 0x20, so the byte walk answers exactly "does a 7-bit control byte appear". This is deliberately NOT the rune-based IsUnsafe/ContainsUnsafe set above — C1 controls and the zero-width/bidi codepoints are out of scope; the callers it replaces gate header values, log fast paths, cache keys, and file names on the ASCII control range only.
Replaces the five byte-identical copies: cmd/repolint hasControlChar, core/handler needsHeaderSanitize, core/middleware containsCtrl, framework/ui hasCtl (screen_cache.go) and hasControlBytes (safety.go).
func IsC1 ¶
IsC1 reports whether r is in the C1 control block (U+0080–U+009F). In UTF-8 each C1 control is a two-byte sequence >= 0x80, so a byte walker keyed on r < 0x20 never sees it; the 8-bit CSI (U+009B) and OSC (U+009D) forms drive terminal escapes exactly as ESC-[ does.
func IsInvisible ¶
IsInvisible reports whether r is a zero-width, joiner, or bidi control codepoint: no glyph of its own, present only to reorder or hide neighbouring text. Trojan-Source (the bidi overrides and isolates) and the zero-width smuggling set both live here.
func IsUnsafe ¶
IsUnsafe reports whether r is a C0 control (U+0000–U+001F), DEL (U+007F), a C1 control, or an invisible/bidi codepoint: the full set no header, key, identity, log line, title, or terminal transcript built from data should carry.
func Recovered ¶
Recovered renders a recover() value for a log sink: fmt.Sprint, then every C0/DEL/C1/invisible codepoint removed, then truncated to 4 KiB. A panic value is whatever the panicking code held at the time, which on a request path is request bytes, so it gets the same scrub a request header does before it reaches the operator's terminal.
Every in-function recover-and-log site uses this instead of logging the raw value (the 2026-09-06 round found nine that did not).
func SanitizeControlBytes ¶ added in v0.86.0
SanitizeControlBytes removes every C0 control byte and DEL from s, leaving all other bytes — including non-ASCII — untouched. The fast path returns s unchanged when it is already clean. Response header values, CORS tokens, route-group prefixes, and DSL literals pass through this so a CR/LF/NUL cannot smuggle a second header line or a forged log line downstream. It REMOVES the bytes rather than percent-encoding them; percent-encoding is ScrubControlBytes' contract, for values rendered into log lines.
Replaces the four body-identical copies: core/handler sanitizeHeaderValue, core/middleware stripCtrlBytes, framework/ routegroup stripPrefixCtrlBytes, framework/dsl stripDSLControlBytes.
func ScrubControlBytes ¶ added in v0.86.0
ScrubControlBytes percent-encodes every character that can forge, break, or reorder a rendered log line in a request-derived value, URL path, method, or a panic that embeds a request string: the C0 controls and DEL, the C1 controls (U+0080–U+009F — the 8-bit CSI 0x9B and OSC 0x9D drive terminal escapes exactly as ESC-[ does, NEL 0x85 breaks the line), and the zero-width/bidi set (RLO and friends visually rewrite the logged path). An attacker then can't forge a fake log entry, reorder one, or smuggle a terminal-control payload into an operator's tail/less session. slog's JSON handler escapes C0 for valid JSON but leaves C1/bidi runes raw (verified 2026-09-05: a raw C2 9B lands in the encoded line), and a JSON-escaped \r\n is still visible to text grep, with naive log shippers rendering the injected payload on its own line.
r.URL.Path is percent-DECODED, so %0d%0a / %c2%9b / %e2%80%ae in the raw request are a real CRLF / U+009B / U+202E by the time they reach any sink here. Stray non-UTF-8 bytes in 0x80..0x9F are the 8-bit C1 forms on the wire (a bare 0x9B from %9B) and are encoded like their rune counterparts; other invalid bytes pass through untouched.
Replaces the two byte-identical copies: core/middleware scrubControlBytes (logging.go) and battery/log scrubControlBytes (middleware.go), which had drifted into maintained parity.
func StripInvisible ¶
StripInvisible removes every invisible/bidi codepoint from s. It leaves C0/C1 controls in place for callers that scrub those separately; use StripUnsafe to remove the whole set at once.
func StripUnsafe ¶
StripUnsafe removes every C0 control, DEL, C1 control, and invisible/bidi codepoint from s. The fast path returns s unchanged when it is already clean.
func Truncate ¶ added in v0.86.0
Truncate returns s capped at max bytes, ending in the " … (truncated)" marker when the cap bites so a consumer can see the entry was cut. When max is too small to fit the marker, s is cut at exactly max bytes with no marker. The cut is byte-aligned; callers pass multi-KiB caps where a split rune at the seam is cosmetic.
Replaces the three byte-identical copies: core/handler truncateLog, core/middleware truncate (recovery.go), battery/log truncateString. Recovered keeps its own inline spelling on purpose: its marker is "…(truncated)" (no leading space) and it backs off to a rune boundary, so it is not this function.
Types ¶
This section is empty.