mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +08:00
* feat: add FileIO extension for file transfer abstraction Introduce extension/fileio package with Provider/FileIO/File interfaces and a global registry, following the same pattern as extension/credential. - Add LocalFileIO default implementation with path validation and atomic writes - Wire FileIOProvider into Factory and resolve at runtime via RuntimeContext.FileIO() - Factory holds Provider (not resolved instance), deferring resolution to execution time
92 lines
3.3 KiB
Go
92 lines
3.3 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package validate
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/internal/charcheck"
|
|
)
|
|
|
|
func TestSanitizeForTerminal_StripsEscapesAndDangerousChars(t *testing.T) {
|
|
for _, tt := range []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
// ── GIVEN: normal text → THEN: unchanged ──
|
|
{"plain text", "hello world", "hello world"},
|
|
{"unicode text", "你好世界", "你好世界"},
|
|
{"empty", "", ""},
|
|
|
|
// ── GIVEN: tab and newline → THEN: preserved ──
|
|
{"preserve tab", "col1\tcol2", "col1\tcol2"},
|
|
{"preserve newline", "line1\nline2", "line1\nline2"},
|
|
|
|
// ── GIVEN: ANSI CSI sequences → THEN: stripped, text preserved ──
|
|
{"clear screen", "before\x1b[2Jafter", "beforeafter"},
|
|
{"red color", "before\x1b[31mred\x1b[0mafter", "beforeredafter"},
|
|
{"bold", "before\x1b[1mbold\x1b[0mafter", "beforeboldafter"},
|
|
{"cursor move", "before\x1b[10;20Hafter", "beforeafter"},
|
|
{"multiple sequences", "\x1b[31m\x1b[1mhello\x1b[0m", "hello"},
|
|
|
|
// ── GIVEN: ANSI OSC sequences → THEN: stripped ──
|
|
{"OSC title change", "before\x1b]0;evil title\x07after", "beforeafter"},
|
|
{"OSC with text", "text\x1b]2;new title\x07more", "textmore"},
|
|
|
|
// ── GIVEN: C0 control characters → THEN: stripped ──
|
|
{"null byte", "hello\x00world", "helloworld"},
|
|
{"bell", "hello\x07world", "helloworld"},
|
|
{"backspace", "hello\x08world", "helloworld"},
|
|
{"escape alone", "hello\x1bworld", "helloworld"},
|
|
{"carriage return", "hello\rworld", "helloworld"},
|
|
{"DEL", "hello\x7fworld", "helloworld"},
|
|
|
|
// ── GIVEN: dangerous Unicode → THEN: stripped ──
|
|
{"zero width space", "hello\u200Bworld", "helloworld"},
|
|
{"BOM", "hello\uFEFFworld", "helloworld"},
|
|
{"bidi RLO", "hello\u202Eworld", "helloworld"},
|
|
{"bidi LRI", "hello\u2066world", "helloworld"},
|
|
{"line separator", "hello\u2028world", "helloworld"},
|
|
|
|
// ── GIVEN: mixed attack payload → THEN: all dangerous content stripped ──
|
|
{"ansi + null + bidi", "\x1b[31m\x00\u202Ehello\x1b[0m", "hello"},
|
|
{"realistic injection", "Status: \x1b[32mOK\x1b[0m (fake)", "Status: OK (fake)"},
|
|
} {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// WHEN: SanitizeForTerminal processes the input
|
|
got := SanitizeForTerminal(tt.input)
|
|
|
|
// THEN: output matches expected sanitized result
|
|
if got != tt.want {
|
|
t.Errorf("SanitizeForTerminal(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsDangerousUnicode_IdentifiesAllDangerousRanges(t *testing.T) {
|
|
// ── GIVEN: known dangerous Unicode code points → THEN: returns true ──
|
|
dangerous := []rune{
|
|
0x200B, 0x200C, 0x200D, // zero-width
|
|
0xFEFF, // BOM
|
|
0x202A, 0x202B, 0x202C, 0x202D, 0x202E, // bidi
|
|
0x2028, 0x2029, // separators
|
|
0x2066, 0x2067, 0x2068, 0x2069, // isolates
|
|
}
|
|
for _, r := range dangerous {
|
|
if !charcheck.IsDangerousUnicode(r) {
|
|
t.Errorf("charcheck.IsDangerousUnicode(%U) = false, want true", r)
|
|
}
|
|
}
|
|
|
|
// ── GIVEN: safe Unicode code points → THEN: returns false ──
|
|
safe := []rune{'A', '中', '!', ' ', '\t', '\n', 0x200A, 0x2070}
|
|
for _, r := range safe {
|
|
if charcheck.IsDangerousUnicode(r) {
|
|
t.Errorf("charcheck.IsDangerousUnicode(%U) = true, want false", r)
|
|
}
|
|
}
|
|
}
|