package sanitizer import ( "strings" "sync" "testing" ) func eq[T comparable](tb testing.TB, got, want T, ctx string) { tb.Helper() if got != want { tb.Errorf("%s: got %#v, want %#v", ctx, got, want) } } func TestNewSanitizer(t *testing.T) { // No rules configured means full passthrough s := New() in := "abc\x00xyz" eq(t, s.Sanitize(in), in, "default passthrough") } func TestSingleRule(t *testing.T) { tests := []struct { name string sanitizer *Sanitizer in, want string }{ {"strip non-printable", New().Rule(FilterNonPrintable, TransformStrip), "a\x00b", "ab"}, {"strip non-printable run", New().Rule(FilterNonPrintable, TransformStrip), "test\x01\x02\x03", "test"}, {"hex encode non-printable", New().Rule(FilterNonPrintable, TransformHexEncode), "a\x00b", "a<00>b"}, {"hex encode bell and tab", New().Rule(FilterNonPrintable, TransformHexEncode), "bell\x07tab\x09", "bell<07>tab<09>"}, {"json escape newline", New().Rule(FilterControl, TransformJSONEscape), "line1\nline2", `line1\nline2`}, {"json escape tab", New().Rule(FilterControl, TransformJSONEscape), "tab\there", `tab\there`}, {"json escape nul", New().Rule(FilterControl, TransformJSONEscape), "null\x00byte", `null\u0000byte`}, {"strip whitespace", New().Rule(FilterWhitespace, TransformStrip), "no spaces here", "nospaceshere"}, {"strip tabs", New().Rule(FilterWhitespace, TransformStrip), "tabs\t\tgone", "tabsgone"}, {"strip shell semicolon", New().Rule(FilterShellSpecial, TransformStrip), "cmd; echo test", "cmd echo test"}, {"strip shell pipe", New().Rule(FilterShellSpecial, TransformStrip), "no | pipes", "no pipes"}, {"strip shell dollar", New().Rule(FilterShellSpecial, TransformStrip), "$var", "var"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { eq(t, tt.sanitizer.Sanitize(tt.in), tt.want, tt.name) }) } } func TestRuleFunc(t *testing.T) { // Predicate rules take priority over filter evaluation within the same rule s := New().RuleFunc(func(r rune) bool { return r == 'x' }, TransformStrip) eq(t, s.Sanitize("axbxc"), "abc", "predicate strip") eq(t, s.Sanitize("clean"), "clean", "predicate miss") s = New().RuleFunc(func(r rune) bool { return r > 0x7f }, TransformHexEncode) eq(t, s.Sanitize("a√b"), "ab", "predicate hex encode") } func TestPolicy(t *testing.T) { tests := []struct { name string policy PolicyPreset in, want string }{ {"txt control", PolicyTxt, "hello\x07world", "hello<07>world"}, {"txt clean", PolicyTxt, "clean text", "clean text"}, // Tab is non-printable per strconv.IsPrint and is encoded like any control byte {"txt tab", PolicyTxt, "col1\tcol2", "col1<09>col2"}, {"json newline", PolicyJSON, "line1\nline2", `line1\nline2`}, {"json tab", PolicyJSON, "\ttab", `\ttab`}, {"shell semicolon", PolicyShell, "cmd; echo", "cmdecho"}, {"shell whitespace", PolicyShell, "no spaces", "nospaces"}, {"raw passthrough", PolicyRaw, "a\x00b", "a\x00b"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { eq(t, New().Policy(tt.policy).Sanitize(tt.in), tt.want, tt.name) }) } t.Run("unknown policy is a no-op", func(t *testing.T) { s := New().Policy(PolicyPreset("bogus")) eq(t, s.Sanitize("a\x00b"), "a\x00b", "unknown preset") }) } func TestPolicyShellExtended(t *testing.T) { s := New().Policy(PolicyShell) eq(t, s.Sanitize(`a'b"c`), "abc", "quotes") eq(t, s.Sanitize(`a\b`), "ab", "backslash") eq(t, s.Sanitize("file*?"), "file", "glob") eq(t, s.Sanitize("rm -rf *"), "rm-rf", "whitespace and glob") eq(t, s.Sanitize("a\x00\x1bb"), "ab", "control") eq(t, s.Sanitize("a{b}c[d]e~f!g"), "abcdefg", "braces, brackets, tilde, bang") } func TestRuleOrdering(t *testing.T) { t.Run("policy precedes later rules", func(t *testing.T) { // Rules append in call order and the first match wins, so a Policy // registered first shadows overlapping custom rules s := New().Policy(PolicyTxt).Rule(FilterControl, TransformStrip) eq(t, s.Sanitize("a\x07b\x00c"), "a<07>b<00>c", "policy wins") }) t.Run("first rule wins", func(t *testing.T) { s := New(). Rule(FilterControl, TransformStrip). Rule(FilterControl, TransformHexEncode) // unreachable eq(t, s.Sanitize("a\x00b"), "ab", "first rule") }) t.Run("chained distinct filters", func(t *testing.T) { s := New(). Rule(FilterWhitespace, TransformStrip). Rule(FilterShellSpecial, TransformHexEncode) eq(t, s.Sanitize("cmd; echo hello"), "cmd<3b>echohello", "chained") }) t.Run("policy plus custom rules", func(t *testing.T) { s := New(). Policy(PolicyTxt). Rule(FilterControl, TransformStrip). Rule(FilterWhitespace, TransformJSONEscape) // \x07 and \x7F are non-printable and match PolicyTxt first; // the space matches the whitespace rule but JSON-escapes to itself eq(t, s.Sanitize("a\x07b c\x7Fd"), "a<07>b c<7f>d", "combined") }) } func TestCompositeFilter(t *testing.T) { s := New().Rule(FilterShellSpecial|FilterWhitespace, TransformStrip) eq(t, s.Sanitize("cmd; echo hello"), "cmdechohello", "composite mask") eq(t, s.Sanitize("no |pipes| no spaces"), "nopipesnospaces", "composite mask") } func TestTransformPriority(t *testing.T) { // applyTransform evaluates Strip first; only one transform applies per rule s := New().Rule(FilterControl, TransformStrip|TransformHexEncode) eq(t, s.Sanitize("a\x00b"), "ab", "strip precedence") } func TestEdgeCases(t *testing.T) { strip := New().Rule(FilterNonPrintable, TransformStrip) hex := New().Rule(FilterNonPrintable, TransformHexEncode) eq(t, strip.Sanitize(""), "", "empty string") eq(t, strip.Sanitize("\x00\x01\x02\x03"), "", "fully stripped") eq(t, hex.Sanitize("Hello 世界 ✓"), "Hello 世界 ✓", "printable UTF-8 passthrough") // U+0085 (NEL) is one non-printable rune encoded as two UTF-8 bytes eq(t, hex.Sanitize("line1\u0085line2"), "line1line2", "multi-byte control") } func TestHexMarkerEscaping(t *testing.T) { s := New().Policy(PolicyTxt) eq(t, s.Sanitize("a\x00b"), "a<00>b", "actual NUL") // Literal '<' is encoded so input cannot forge a marker eq(t, s.Sanitize("a<00>b"), "a<3c>00>b", "literal marker text") } func TestSanitizeCleanFastPath(t *testing.T) { s := New().Policy(PolicyTxt) in := "clean ascii text" eq(t, s.Sanitize(in), in, "unchanged") if n := testing.AllocsPerRun(100, func() { _ = s.Sanitize(in) }); n != 0 { t.Errorf("clean input allocated %v times, want 0", n) } } func TestAppendSanitize(t *testing.T) { s := New().Policy(PolicyTxt) buf := append([]byte(nil), "prefix:"...) buf = s.AppendSanitize(buf, "a\x00b") eq(t, string(buf), "prefix:a<00>b", "append with rules") // No rules configured appends verbatim buf = append([]byte(nil), "prefix:"...) buf = New().AppendSanitize(buf, "a\x00b") eq(t, string(buf), "prefix:a\x00b", "append passthrough") } func TestSanitizerConcurrent(t *testing.T) { s := New().Policy(PolicyTxt) var wg sync.WaitGroup for range 16 { wg.Add(1) go func() { defer wg.Done() for range 500 { // Errorf is goroutine-safe; Fatal variants are not if got := s.Sanitize("a\x00b\x07c"); got != "a<00>b<07>c" { t.Errorf("concurrent Sanitize: got %q", got) return } } }() } wg.Wait() } func TestSerializerWriteString(t *testing.T) { t.Run("raw applies sanitizer", func(t *testing.T) { se := NewSerializer("raw", New().Rule(FilterNonPrintable, TransformHexEncode)) var buf []byte se.WriteString(&buf, "test\x00data") eq(t, string(buf), "test<00>data", "raw") }) t.Run("txt quotes conditionally", func(t *testing.T) { se := NewSerializer("txt", New()) var buf []byte se.WriteString(&buf, "hello world") eq(t, string(buf), `"hello world"`, "quoted") buf = nil se.WriteString(&buf, "nospace") eq(t, string(buf), "nospace", "unquoted") buf = nil se.WriteString(&buf, `has"quote`) eq(t, string(buf), `"has\"quote"`, "escaped quote") }) t.Run("json escapes transport characters", func(t *testing.T) { se := NewSerializer("json", New()) var buf []byte se.WriteString(&buf, "line1\nline2\t\"quoted\"") eq(t, string(buf), `"line1\nline2\t\"quoted\""`, "escapes") buf = nil se.WriteString(&buf, "null\x00byte") eq(t, string(buf), `"null\u0000byte"`, "control escape") buf = nil se.WriteString(&buf, "héllo 世界") eq(t, string(buf), `"héllo 世界"`, "UTF-8 passthrough") }) t.Run("json applies sanitizer before escaping", func(t *testing.T) { se := NewSerializer("json", New().Policy(PolicyTxt)) var buf []byte se.WriteString(&buf, "a\x00b") eq(t, string(buf), `"a<00>b"`, "layered") }) } func TestSerializerScalars(t *testing.T) { san := New() t.Run("numbers and booleans", func(t *testing.T) { se := NewSerializer("json", san) var buf []byte se.WriteNumber(&buf, "42") se.WriteBool(&buf, true) se.WriteBool(&buf, false) eq(t, string(buf), "42truefalse", "scalars are unquoted") }) t.Run("nil per format", func(t *testing.T) { var buf []byte NewSerializer("raw", san).WriteNil(&buf) eq(t, string(buf), "nil", "raw nil") buf = nil NewSerializer("json", san).WriteNil(&buf) eq(t, string(buf), "null", "json nil") buf = nil NewSerializer("txt", san).WriteNil(&buf) eq(t, string(buf), "null", "txt nil") }) t.Run("complex values", func(t *testing.T) { var buf []byte NewSerializer("raw", san).WriteComplex(&buf, map[string]int{"a": 1}) eq(t, string(buf), "map[a:1]", "map formatting") }) } func TestNeedsQuotes(t *testing.T) { tests := []struct { format string in string want bool }{ {"json", "anything", true}, {"raw", "anything", false}, {"txt", "", true}, {"txt", "plain", false}, {"txt", "has space", true}, {"txt", "semi;colon", true}, {"txt", "pipe|char", true}, {"txt", "brace{x}", true}, {"txt", "percent%", true}, {"txt", "equals=", true}, {"txt", "ctrl\x01", true}, {"txt", "dash-underscore_", false}, } for _, tt := range tests { se := NewSerializer(tt.format, New()) if got := se.NeedsQuotes(tt.in); got != tt.want { t.Errorf("NeedsQuotes(%s, %q) = %v, want %v", tt.format, tt.in, got, tt.want) } } } func BenchmarkSanitizer(b *testing.B) { input := strings.Repeat("normal text\x00\n\t", 100) benchmarks := []struct { name string sanitizer *Sanitizer }{ {"Passthrough", New()}, {"SingleRule", New().Rule(FilterNonPrintable, TransformHexEncode)}, {"Policy", New().Policy(PolicyTxt)}, {"Complex", New(). Policy(PolicyTxt). Rule(FilterControl, TransformStrip). Rule(FilterWhitespace, TransformJSONEscape)}, } for _, bm := range benchmarks { b.Run(bm.name, func(b *testing.B) { b.ReportAllocs() for b.Loop() { _ = bm.sanitizer.Sanitize(input) } }) } } func BenchmarkSanitizerClean(b *testing.B) { s := New().Policy(PolicyTxt) input := strings.Repeat("clean ascii text ", 100) b.ReportAllocs() for b.Loop() { _ = s.Sanitize(input) } }