github.com/xhghs/rclone@v1.51.1-0.20200430155106-e186a28cced8/lib/encoder/encoder_test.go (about) 1 package encoder 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 "testing" 9 10 "github.com/spf13/pflag" 11 "github.com/stretchr/testify/assert" 12 ) 13 14 // Check it satisfies the interfaces 15 var ( 16 _ pflag.Value = (*MultiEncoder)(nil) 17 _ fmt.Scanner = (*MultiEncoder)(nil) 18 ) 19 20 func TestEncodeString(t *testing.T) { 21 for _, test := range []struct { 22 mask MultiEncoder 23 want string 24 }{ 25 {0, "None"}, 26 {EncodeZero, "None"}, 27 {EncodeDoubleQuote, "DoubleQuote"}, 28 {EncodeDot, "Dot"}, 29 {EncodeWin, "LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe"}, 30 {EncodeHashPercent, "Hash,Percent"}, 31 {EncodeSlash | EncodeDollar | EncodeColon, "Slash,Dollar,Colon"}, 32 {EncodeSlash | (1 << 31), "Slash,0x80000000"}, 33 } { 34 got := test.mask.String() 35 assert.Equal(t, test.want, got) 36 } 37 38 } 39 40 func TestEncodeSet(t *testing.T) { 41 for _, test := range []struct { 42 in string 43 want MultiEncoder 44 wantErr bool 45 }{ 46 {"", 0, true}, 47 {"None", 0, false}, 48 {"None", EncodeZero, false}, 49 {"DoubleQuote", EncodeDoubleQuote, false}, 50 {"Dot", EncodeDot, false}, 51 {"LtGt,DoubleQuote,Colon,Question,Asterisk,Pipe", EncodeWin, false}, 52 {"Hash,Percent", EncodeHashPercent, false}, 53 {"Slash,Dollar,Colon", EncodeSlash | EncodeDollar | EncodeColon, false}, 54 {"Slash,0x80000000", EncodeSlash | (1 << 31), false}, 55 {"Blerp", 0, true}, 56 {"0xFGFFF", 0, true}, 57 } { 58 var got MultiEncoder 59 err := got.Set(test.in) 60 assert.Equal(t, test.wantErr, err != nil, err) 61 assert.Equal(t, test.want, got, test.in) 62 } 63 64 } 65 66 type testCase struct { 67 mask MultiEncoder 68 in string 69 out string 70 } 71 72 func TestEncodeSingleMask(t *testing.T) { 73 for i, tc := range testCasesSingle { 74 e := tc.mask 75 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 76 got := e.Encode(tc.in) 77 if got != tc.out { 78 t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got) 79 } 80 got2 := e.Decode(got) 81 if got2 != tc.in { 82 t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2) 83 } 84 }) 85 } 86 } 87 88 func TestEncodeSingleMaskEdge(t *testing.T) { 89 for i, tc := range testCasesSingleEdge { 90 e := tc.mask 91 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 92 got := e.Encode(tc.in) 93 if got != tc.out { 94 t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got) 95 } 96 got2 := e.Decode(got) 97 if got2 != tc.in { 98 t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2) 99 } 100 }) 101 } 102 } 103 104 func TestEncodeDoubleMaskEdge(t *testing.T) { 105 for i, tc := range testCasesDoubleEdge { 106 e := tc.mask 107 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 108 got := e.Encode(tc.in) 109 if got != tc.out { 110 t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got) 111 } 112 got2 := e.Decode(got) 113 if got2 != tc.in { 114 t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2) 115 } 116 }) 117 } 118 } 119 120 func TestEncodeInvalidUnicode(t *testing.T) { 121 for i, tc := range []testCase{ 122 { 123 mask: EncodeInvalidUtf8, 124 in: "\xBF", 125 out: "‛BF", 126 }, { 127 mask: EncodeInvalidUtf8, 128 in: "\xBF\xFE", 129 out: "‛BF‛FE", 130 }, { 131 mask: EncodeInvalidUtf8, 132 in: "a\xBF\xFEb", 133 out: "a‛BF‛FEb", 134 }, { 135 mask: EncodeInvalidUtf8, 136 in: "a\xBFξ\xFEb", 137 out: "a‛BFξ‛FEb", 138 }, { 139 mask: EncodeInvalidUtf8 | EncodeBackSlash, 140 in: "a\xBF\\\xFEb", 141 out: "a‛BF\‛FEb", 142 }, { 143 mask: 0, 144 in: "\xBF", 145 out: "\xBF", 146 }, { 147 mask: 0, 148 in: "\xBF\xFE", 149 out: "\xBF\xFE", 150 }, { 151 mask: 0, 152 in: "a\xBF\xFEb", 153 out: "a\xBF\xFEb", 154 }, { 155 mask: 0, 156 in: "a\xBFξ\xFEb", 157 out: "a\xBFξ\xFEb", 158 }, { 159 mask: EncodeBackSlash, 160 in: "a\xBF\\\xFEb", 161 out: "a\xBF\\xFEb", 162 }, 163 } { 164 e := tc.mask 165 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 166 got := e.Encode(tc.in) 167 if got != tc.out { 168 t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got) 169 } 170 got2 := e.Decode(got) 171 if got2 != tc.in { 172 t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2) 173 } 174 }) 175 } 176 } 177 178 func TestEncodeDot(t *testing.T) { 179 for i, tc := range []testCase{ 180 { 181 mask: 0, 182 in: ".", 183 out: ".", 184 }, { 185 mask: EncodeDot, 186 in: ".", 187 out: ".", 188 }, { 189 mask: 0, 190 in: "..", 191 out: "..", 192 }, { 193 mask: EncodeDot, 194 in: "..", 195 out: "..", 196 }, { 197 mask: EncodeDot, 198 in: "...", 199 out: "...", 200 }, { 201 mask: EncodeDot, 202 in: ". .", 203 out: ". .", 204 }, 205 } { 206 e := tc.mask 207 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 208 got := e.Encode(tc.in) 209 if got != tc.out { 210 t.Errorf("Encode(%q) want %q got %q", tc.in, tc.out, got) 211 } 212 got2 := e.Decode(got) 213 if got2 != tc.in { 214 t.Errorf("Decode(%q) want %q got %q", got, tc.in, got2) 215 } 216 }) 217 } 218 } 219 220 func TestDecodeHalf(t *testing.T) { 221 for i, tc := range []testCase{ 222 { 223 mask: 0, 224 in: "‛", 225 out: "‛", 226 }, { 227 mask: 0, 228 in: "‛‛", 229 out: "‛", 230 }, { 231 mask: 0, 232 in: "‛a‛", 233 out: "‛a‛", 234 }, { 235 mask: EncodeInvalidUtf8, 236 in: "a‛B‛Eg", 237 out: "a‛B‛Eg", 238 }, { 239 mask: EncodeInvalidUtf8, 240 in: "a‛B\‛Eg", 241 out: "a‛B\‛Eg", 242 }, { 243 mask: EncodeInvalidUtf8 | EncodeBackSlash, 244 in: "a‛B\‛Eg", 245 out: "a‛B\\‛Eg", 246 }, 247 } { 248 e := tc.mask 249 t.Run(strconv.FormatInt(int64(i), 10), func(t *testing.T) { 250 got := e.Decode(tc.in) 251 if got != tc.out { 252 t.Errorf("Decode(%q) want %q got %q", tc.in, tc.out, got) 253 } 254 }) 255 } 256 } 257 258 const oneDrive = (Standard | 259 EncodeWin | 260 EncodeBackSlash | 261 EncodeHashPercent | 262 EncodeDel | 263 EncodeCtl | 264 EncodeLeftTilde | 265 EncodeRightSpace | 266 EncodeRightPeriod) 267 268 var benchTests = []struct { 269 in string 270 out string 271 }{ 272 {"", ""}, 273 {"abc 123", "abc 123"}, 274 {`\*<>?:|#%".~`, `\*<>?:|#%".~`}, 275 {`\*<>?:|#%".~/\*<>?:|#%".~`, `\*<>?:|#%".~/\*<>?:|#%".~`}, 276 {" leading space", " leading space"}, 277 {"~leading tilde", "~leading tilde"}, 278 {"trailing dot.", "trailing dot."}, 279 {" leading space/ leading space/ leading space", " leading space/ leading space/ leading space"}, 280 {"~leading tilde/~leading tilde/~leading tilde", "~leading tilde/~leading tilde/~leading tilde"}, 281 {"leading tilde/~leading tilde", "leading tilde/~leading tilde"}, 282 {"trailing dot./trailing dot./trailing dot.", "trailing dot./trailing dot./trailing dot."}, 283 } 284 285 func benchReplace(b *testing.B, f func(string) string) { 286 for range make([]struct{}, b.N) { 287 for _, test := range benchTests { 288 got := f(test.in) 289 if got != test.out { 290 b.Errorf("Encode(%q) want %q got %q", test.in, test.out, got) 291 } 292 } 293 } 294 } 295 296 func benchRestore(b *testing.B, f func(string) string) { 297 for range make([]struct{}, b.N) { 298 for _, test := range benchTests { 299 got := f(test.out) 300 if got != test.in { 301 b.Errorf("Decode(%q) want %q got %q", got, test.in, got) 302 } 303 } 304 } 305 } 306 func BenchmarkOneDriveReplaceNew(b *testing.B) { 307 benchReplace(b, oneDrive.Encode) 308 } 309 func BenchmarkOneDriveReplaceOld(b *testing.B) { 310 benchReplace(b, replaceReservedChars) 311 } 312 func BenchmarkOneDriveRestoreNew(b *testing.B) { 313 benchRestore(b, oneDrive.Decode) 314 } 315 func BenchmarkOneDriveRestoreOld(b *testing.B) { 316 benchRestore(b, restoreReservedChars) 317 } 318 319 var ( 320 charMap = map[rune]rune{ 321 '\\': '\', // FULLWIDTH REVERSE SOLIDUS 322 '*': '*', // FULLWIDTH ASTERISK 323 '<': '<', // FULLWIDTH LESS-THAN SIGN 324 '>': '>', // FULLWIDTH GREATER-THAN SIGN 325 '?': '?', // FULLWIDTH QUESTION MARK 326 ':': ':', // FULLWIDTH COLON 327 '|': '|', // FULLWIDTH VERTICAL LINE 328 '#': '#', // FULLWIDTH NUMBER SIGN 329 '%': '%', // FULLWIDTH PERCENT SIGN 330 '"': '"', // FULLWIDTH QUOTATION MARK - not on the list but seems to be reserved 331 '.': '.', // FULLWIDTH FULL STOP 332 '~': '~', // FULLWIDTH TILDE 333 ' ': '␠', // SYMBOL FOR SPACE 334 } 335 invCharMap map[rune]rune 336 fixEndingInPeriod = regexp.MustCompile(`\.(/|$)`) 337 fixEndingWithSpace = regexp.MustCompile(` (/|$)`) 338 fixStartingWithTilde = regexp.MustCompile(`(/|^)~`) 339 ) 340 341 func init() { 342 // Create inverse charMap 343 invCharMap = make(map[rune]rune, len(charMap)) 344 for k, v := range charMap { 345 invCharMap[v] = k 346 } 347 } 348 349 // replaceReservedChars takes a path and substitutes any reserved 350 // characters in it 351 func replaceReservedChars(in string) string { 352 // Folder names can't end with a period '.' 353 in = fixEndingInPeriod.ReplaceAllString(in, string(charMap['.'])+"$1") 354 // OneDrive for Business file or folder names cannot begin with a tilde '~' 355 in = fixStartingWithTilde.ReplaceAllString(in, "$1"+string(charMap['~'])) 356 // Apparently file names can't start with space either 357 in = fixEndingWithSpace.ReplaceAllString(in, string(charMap[' '])+"$1") 358 // Encode reserved characters 359 return strings.Map(func(c rune) rune { 360 if replacement, ok := charMap[c]; ok && c != '.' && c != '~' && c != ' ' { 361 return replacement 362 } 363 return c 364 }, in) 365 } 366 367 // restoreReservedChars takes a path and undoes any substitutions 368 // made by replaceReservedChars 369 func restoreReservedChars(in string) string { 370 return strings.Map(func(c rune) rune { 371 if replacement, ok := invCharMap[c]; ok { 372 return replacement 373 } 374 return c 375 }, in) 376 }