master
go 220 lines 7.05 KB
Raw
1 package cli
2
3 import (
4 "strings"
5 "testing"
6
7 "github.com/ipfs/kubo/core/commands/cmdutils"
8 "github.com/stretchr/testify/assert"
9 )
10
11 func TestCleanAndTrimUnicode(t *testing.T) {
12 tests := []struct {
13 name string
14 input string
15 expected string
16 }{
17 {
18 name: "Basic ASCII",
19 input: "kubo/1.0.0",
20 expected: "kubo/1.0.0",
21 },
22 {
23 name: "Polish characters preserved",
24 input: "test-ąęćłńóśźż",
25 expected: "test-ąęćłńóśźż",
26 },
27 {
28 name: "Chinese characters preserved",
29 input: "版本-中文测试",
30 expected: "版本-中文测试",
31 },
32 {
33 name: "Arabic text preserved",
34 input: "اختبار-العربية",
35 expected: "اختبار-العربية",
36 },
37 {
38 name: "Emojis preserved",
39 input: "version-1.0-🚀-🎉",
40 expected: "version-1.0-🚀-🎉",
41 },
42 {
43 name: "Complex Unicode with combining marks preserved",
44 input: "h̸̢̢̢̢̢̢̢̢̢̢e̵̵̵̵̵̵̵̵̵̵l̷̷̷̷̷̷̷̷̷̷l̶̶̶̶̶̶̶̶̶̶o̴̴̴̴̴̴̴̴̴̴",
45 expected: "h̸̢̢̢̢̢̢̢̢̢̢e̵̵̵̵̵̵̵̵̵̵l̷̷̷̷̷̷̷̷̷̷l̶̶̶̶̶̶̶̶̶̶o̴̴̴̴̴̴̴̴̴̴", // Preserved as-is (only 50 runes)
46 },
47 {
48 name: "Long text with combining marks truncated at 128",
49 input: strings.Repeat("ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂͌́", 10), // Very long text (260 runes)
50 expected: "ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂͌́ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂͌́ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂͌́ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂͌́ẽ̸̢̛̖̬͈͉͖͇͈̭̥́̓̌̾͊̊̂̄̍̅̂", // Truncated at 128 runes
51 },
52 {
53 name: "Zero-width characters replaced with U+FFFD",
54 input: "test\u200Bzero\u200Cwidth\u200D\uFEFFchars",
55 expected: "test�zero�width��chars",
56 },
57 {
58 name: "RTL/LTR override replaced with U+FFFD",
59 input: "test\u202Drtl\u202Eltr\u202Aoverride",
60 expected: "test�rtl�ltr�override",
61 },
62 {
63 name: "Bidi isolates replaced with U+FFFD",
64 input: "test\u2066bidi\u2067isolate\u2068text\u2069end",
65 expected: "test�bidi�isolate�text�end",
66 },
67 {
68 name: "Control characters replaced with U+FFFD",
69 input: "test\x00null\x1Fescape\x7Fdelete",
70 expected: "test�null�escape�delete",
71 },
72 {
73 name: "Combining marks preserved",
74 input: "e\u0301\u0302\u0303\u0304\u0305", // e with 5 combining marks
75 expected: "e\u0301\u0302\u0303\u0304\u0305", // All preserved
76 },
77 {
78 name: "No truncation at 70 characters",
79 input: "123456789012345678901234567890123456789012345678901234567890123456789",
80 expected: "123456789012345678901234567890123456789012345678901234567890123456789",
81 },
82 {
83 name: "No truncation with Unicode - 70 rockets preserved",
84 input: strings.Repeat("🚀", 70),
85 expected: strings.Repeat("🚀", 70),
86 },
87 {
88 name: "Empty string",
89 input: "",
90 expected: "",
91 },
92 {
93 name: "Only whitespace with control chars",
94 input: " \t\n ",
95 expected: "\uFFFD\uFFFD", // Tab and newline become U+FFFD, spaces trimmed
96 },
97 {
98 name: "Leading and trailing whitespace",
99 input: " test ",
100 expected: "test",
101 },
102 {
103 name: "Complex mix - invisible chars replaced with U+FFFD, Unicode preserved",
104 input: "kubo/1.0-🚀\u200B h̸̢̏̔ḛ̶̽̀s̵t\u202E-ąęł-中文",
105 expected: "kubo/1.0-🚀� h̸̢̏̔ḛ̶̽̀s̵t�-ąęł-中文",
106 },
107 {
108 name: "Emoji with skin tone preserved",
109 input: "👍🏽", // Thumbs up with skin tone modifier
110 expected: "👍🏽", // Preserved as-is
111 },
112 {
113 name: "Mixed scripts preserved",
114 input: "Hello-你好-مرحبا-Здравствуйте",
115 expected: "Hello-你好-مرحبا-Здравствуйте",
116 },
117 {
118 name: "Format characters replaced with U+FFFD",
119 input: "test\u00ADsoft\u2060word\u206Fnom\u200Ebreak",
120 expected: "test�soft�word�nom�break", // Soft hyphen, word joiner, etc replaced
121 },
122 {
123 name: "Complex Unicode text with many combining marks (91 runes, no truncation)",
124 input: "ț̸̢͙̞̖̏̔ȩ̶̰͓̪͎̱̠̥̳͔̽̀̃̿̌̾̀͗̕̕͜s̵̢̛̖̬͈͉͖͇͈̭̥̃́̓̌̾͊̊̂̄̍̅̂͌́ͅţ̴̯̹̪͖͓̘̊́̑̄̋̈́͐̈́̔̇̄̂́̎̓͛͠ͅ test",
125 expected: "ț̸̢͙̞̖̏̔ȩ̶̰͓̪͎̱̠̥̳͔̽̀̃̿̌̾̀͗̕̕͜s̵̢̛̖̬͈͉͖͇͈̭̥̃́̓̌̾͊̊̂̄̍̅̂͌́ͅţ̴̯̹̪͖͓̘̊́̑̄̋̈́͐̈́̔̇̄̂́̎̓͛͠ͅ test", // Not truncated (91 < 128)
126 },
127 {
128 name: "Truncation at 128 characters",
129 input: strings.Repeat("a", 150),
130 expected: strings.Repeat("a", 128),
131 },
132 {
133 name: "Truncation with Unicode at 128",
134 input: strings.Repeat("🚀", 150),
135 expected: strings.Repeat("🚀", 128),
136 },
137 {
138 name: "Private use characters preserved (per spec)",
139 input: "test\uE000\uF8FF", // Private use area characters
140 expected: "test\uE000\uF8FF", // Should be preserved
141 },
142 {
143 name: "U+FFFD replacement for multiple categories",
144 input: "a\x00b\u200Cc\u202Ed", // control, format chars
145 expected: "a\uFFFDb\uFFFDc\uFFFDd", // All replaced with U+FFFD
146 },
147 }
148
149 for _, tt := range tests {
150 t.Run(tt.name, func(t *testing.T) {
151 result := cmdutils.CleanAndTrim(tt.input)
152 assert.Equal(t, tt.expected, result, "CleanAndTrim(%q) = %q, want %q", tt.input, result, tt.expected)
153 })
154 }
155 }
156
157 func TestCleanAndTrimIdempotent(t *testing.T) {
158 // Test that applying CleanAndTrim twice gives the same result
159 inputs := []string{
160 "test-ąęćłńóśźż",
161 "版本-中文测试",
162 "version-1.0-🚀-🎉",
163 "h̸e̵l̷l̶o̴ w̸o̵r̷l̶d̴",
164 "test\u200Bzero\u200Cwidth",
165 }
166
167 for _, input := range inputs {
168 once := cmdutils.CleanAndTrim(input)
169 twice := cmdutils.CleanAndTrim(once)
170 assert.Equal(t, once, twice, "CleanAndTrim should be idempotent for %q", input)
171 }
172 }
173
174 func TestCleanAndTrimSecurity(t *testing.T) {
175 // Test that all invisible/dangerous characters are removed
176 tests := []struct {
177 name string
178 input string
179 check func(string) bool
180 }{
181 {
182 name: "No zero-width spaces",
183 input: "test\u200B\u200C\u200Dtest",
184 check: func(s string) bool {
185 return !strings.Contains(s, "\u200B") && !strings.Contains(s, "\u200C") && !strings.Contains(s, "\u200D")
186 },
187 },
188 {
189 name: "No bidi overrides",
190 input: "test\u202A\u202B\u202C\u202D\u202Etest",
191 check: func(s string) bool {
192 for _, r := range []rune{0x202A, 0x202B, 0x202C, 0x202D, 0x202E} {
193 if strings.ContainsRune(s, r) {
194 return false
195 }
196 }
197 return true
198 },
199 },
200 {
201 name: "No control characters",
202 input: "test\x00\x01\x02\x1F\x7Ftest",
203 check: func(s string) bool {
204 for _, r := range s {
205 if r < 0x20 || r == 0x7F {
206 return false
207 }
208 }
209 return true
210 },
211 },
212 }
213
214 for _, tt := range tests {
215 t.Run(tt.name, func(t *testing.T) {
216 result := cmdutils.CleanAndTrim(tt.input)
217 assert.True(t, tt.check(result), "Security check failed for %q -> %q", tt.input, result)
218 })
219 }
220 }