github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/object-api-utils_test.go (about) 1 // Copyright (c) 2015-2021 MinIO, Inc. 2 // 3 // This file is part of MinIO Object Storage stack 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package cmd 19 20 import ( 21 "bytes" 22 "context" 23 "fmt" 24 "io" 25 "net/http" 26 "net/http/httptest" 27 "path" 28 "reflect" 29 "runtime" 30 "strconv" 31 "testing" 32 33 "github.com/klauspost/compress/s2" 34 "github.com/minio/minio/internal/auth" 35 "github.com/minio/minio/internal/config/compress" 36 "github.com/minio/minio/internal/crypto" 37 "github.com/minio/pkg/v2/trie" 38 ) 39 40 func pathJoinOld(elem ...string) string { 41 trailingSlash := "" 42 if len(elem) > 0 { 43 if hasSuffixByte(elem[len(elem)-1], SlashSeparatorChar) { 44 trailingSlash = SlashSeparator 45 } 46 } 47 return path.Join(elem...) + trailingSlash 48 } 49 50 func BenchmarkPathJoinOld(b *testing.B) { 51 b.Run("PathJoin", func(b *testing.B) { 52 b.ResetTimer() 53 b.ReportAllocs() 54 55 for i := 0; i < b.N; i++ { 56 pathJoinOld("volume", "path/path/path") 57 } 58 }) 59 } 60 61 func BenchmarkPathJoin(b *testing.B) { 62 b.Run("PathJoin", func(b *testing.B) { 63 b.ResetTimer() 64 b.ReportAllocs() 65 66 for i := 0; i < b.N; i++ { 67 pathJoin("volume", "path/path/path") 68 } 69 }) 70 } 71 72 // Wrapper 73 func TestPathTraversalExploit(t *testing.T) { 74 if runtime.GOOS != globalWindowsOSName { 75 t.Skip() 76 } 77 defer DetectTestLeak(t)() 78 ExecExtendedObjectLayerAPITest(t, testPathTraversalExploit, []string{"PutObject"}) 79 } 80 81 // testPathTraversal exploit test, exploits path traversal on windows 82 // with following object names "\\../.minio.sys/config/iam/${username}/identity.json" 83 // #16852 84 func testPathTraversalExploit(obj ObjectLayer, instanceType, bucketName string, apiRouter http.Handler, 85 credentials auth.Credentials, t *testing.T, 86 ) { 87 if err := newTestConfig(globalMinioDefaultRegion, obj); err != nil { 88 t.Fatalf("Initializing config.json failed") 89 } 90 91 objectName := `\../.minio.sys/config/hello.txt` 92 93 // initialize HTTP NewRecorder, this records any mutations to response writer inside the handler. 94 rec := httptest.NewRecorder() 95 // construct HTTP request for Get Object end point. 96 req, err := newTestSignedRequestV4(http.MethodPut, getPutObjectURL("", bucketName, objectName), 97 int64(5), bytes.NewReader([]byte("hello")), credentials.AccessKey, credentials.SecretKey, map[string]string{}) 98 if err != nil { 99 t.Fatalf("failed to create HTTP request for Put Object: <ERROR> %v", err) 100 } 101 102 // Since `apiRouter` satisfies `http.Handler` it has a ServeHTTP to execute the logic of the handler. 103 // Call the ServeHTTP to execute the handler. 104 apiRouter.ServeHTTP(rec, req) 105 106 ctx, cancel := context.WithCancel(GlobalContext) 107 defer cancel() 108 109 // Now check if we actually wrote to backend (regardless of the response 110 // returned by the server). 111 z := obj.(*erasureServerPools) 112 xl := z.serverPools[0].sets[0] 113 erasureDisks := xl.getDisks() 114 parts, errs := readAllFileInfo(ctx, erasureDisks, "", bucketName, objectName, "", false, false) 115 for i := range parts { 116 if errs[i] == nil { 117 if parts[i].Name == objectName { 118 t.Errorf("path traversal allowed to allow writing to minioMetaBucket: %s", instanceType) 119 } 120 } 121 } 122 } 123 124 // Tests validate bucket name. 125 func TestIsValidBucketName(t *testing.T) { 126 testCases := []struct { 127 bucketName string 128 shouldPass bool 129 }{ 130 // cases which should pass the test. 131 // passing in valid bucket names. 132 {"lol", true}, 133 {"1-this-is-valid", true}, 134 {"1-this-too-is-valid-1", true}, 135 {"this.works.too.1", true}, 136 {"1234567", true}, 137 {"123", true}, 138 {"s3-eu-west-1.amazonaws.com", true}, 139 {"ideas-are-more-powerful-than-guns", true}, 140 {"testbucket", true}, 141 {"1bucket", true}, 142 {"bucket1", true}, 143 {"a.b", true}, 144 {"ab.a.bc", true}, 145 // cases for which test should fail. 146 // passing invalid bucket names. 147 {"------", false}, 148 {"my..bucket", false}, 149 {"192.168.1.1", false}, 150 {"$this-is-not-valid-too", false}, 151 {"contains-$-dollar", false}, 152 {"contains-^-caret", false}, 153 {"contains-$-dollar", false}, 154 {"contains-$-dollar", false}, 155 {"......", false}, 156 {"", false}, 157 {"a", false}, 158 {"ab", false}, 159 {".starts-with-a-dot", false}, 160 {"ends-with-a-dot.", false}, 161 {"ends-with-a-dash-", false}, 162 {"-starts-with-a-dash", false}, 163 {"THIS-BEGINS-WITH-UPPERCASe", false}, 164 {"tHIS-ENDS-WITH-UPPERCASE", false}, 165 {"ThisBeginsAndEndsWithUpperCasE", false}, 166 {"una ñina", false}, 167 {"dash-.may-not-appear-next-to-dot", false}, 168 {"dash.-may-not-appear-next-to-dot", false}, 169 {"dash-.-may-not-appear-next-to-dot", false}, 170 {"lalalallalallalalalallalallalala-thestring-size-is-greater-than-63", false}, 171 } 172 173 for i, testCase := range testCases { 174 isValidBucketName := IsValidBucketName(testCase.bucketName) 175 if testCase.shouldPass && !isValidBucketName { 176 t.Errorf("Test case %d: Expected \"%s\" to be a valid bucket name", i+1, testCase.bucketName) 177 } 178 if !testCase.shouldPass && isValidBucketName { 179 t.Errorf("Test case %d: Expected bucket name \"%s\" to be invalid", i+1, testCase.bucketName) 180 } 181 } 182 } 183 184 // Tests for validate object name. 185 func TestIsValidObjectName(t *testing.T) { 186 testCases := []struct { 187 objectName string 188 shouldPass bool 189 }{ 190 // cases which should pass the test. 191 // passing in valid object name. 192 {"object", true}, 193 {"The Shining Script <v1>.pdf", true}, 194 {"Cost Benefit Analysis (2009-2010).pptx", true}, 195 {"117Gn8rfHL2ACARPAhaFd0AGzic9pUbIA/5OCn5A", true}, 196 {"SHØRT", true}, 197 {"f*le", true}, 198 {"contains-^-caret", true}, 199 {"contains-|-pipe", true}, 200 {"contains-`-tick", true}, 201 {"..test", true}, 202 {".. test", true}, 203 {". test", true}, 204 {".test", true}, 205 {"There are far too many object names, and far too few bucket names!", true}, 206 {"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~/!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~)", true}, 207 {"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~", true}, 208 {"␀␁␂␃␄␅␆␇␈␉␊␋␌␍␎␏␐␑␒␓␔␕␖␗␘␙␚␛␜␝␞␟␡", true}, 209 {"trailing VT␋/trailing VT␋", true}, 210 {"␋leading VT/␋leading VT", true}, 211 {"~leading tilde", true}, 212 {"\rleading CR", true}, 213 {"\nleading LF", true}, 214 {"\tleading HT", true}, 215 {"trailing CR\r", true}, 216 {"trailing LF\n", true}, 217 {"trailing HT\t", true}, 218 // cases for which test should fail. 219 // passing invalid object names. 220 {"", false}, 221 {"a/b/c/", false}, 222 {"../../etc", false}, 223 {"../../", false}, 224 {"/../../etc", false}, 225 {" ../etc", false}, 226 {"./././", false}, 227 {"./etc", false}, 228 {`contains//double/forwardslash`, false}, 229 {`//contains/double-forwardslash-prefix`, false}, 230 {string([]byte{0xff, 0xfe, 0xfd}), false}, 231 } 232 233 for i, testCase := range testCases { 234 isValidObjectName := IsValidObjectName(testCase.objectName) 235 if testCase.shouldPass && !isValidObjectName { 236 t.Errorf("Test case %d: Expected \"%s\" to be a valid object name", i+1, testCase.objectName) 237 } 238 if !testCase.shouldPass && isValidObjectName { 239 t.Errorf("Test case %d: Expected object name \"%s\" to be invalid", i+1, testCase.objectName) 240 } 241 } 242 } 243 244 // Tests getCompleteMultipartMD5 245 func TestGetCompleteMultipartMD5(t *testing.T) { 246 testCases := []struct { 247 parts []CompletePart 248 expectedResult string 249 expectedErr string 250 }{ 251 // Wrong MD5 hash string, returns md5um of hash 252 {[]CompletePart{{ETag: "wrong-md5-hash-string"}}, "0deb8cb07527b4b2669c861cb9653607-1", ""}, 253 254 // Single CompletePart with valid MD5 hash string. 255 {[]CompletePart{{ETag: "cf1f738a5924e645913c984e0fe3d708"}}, "10dc1617fbcf0bd0858048cb96e6bd77-1", ""}, 256 257 // Multiple CompletePart with valid MD5 hash string. 258 {[]CompletePart{{ETag: "cf1f738a5924e645913c984e0fe3d708"}, {ETag: "9ccbc9a80eee7fb6fdd22441db2aedbd"}}, "0239a86b5266bb624f0ac60ba2aed6c8-2", ""}, 259 } 260 261 for i, test := range testCases { 262 result := getCompleteMultipartMD5(test.parts) 263 if result != test.expectedResult { 264 t.Fatalf("test %d failed: expected: result=%v, got=%v", i+1, test.expectedResult, result) 265 } 266 } 267 } 268 269 // TestIsMinioBucketName - Tests isMinioBucketName helper function. 270 func TestIsMinioMetaBucketName(t *testing.T) { 271 testCases := []struct { 272 bucket string 273 result bool 274 }{ 275 // MinIO meta bucket. 276 { 277 bucket: minioMetaBucket, 278 result: true, 279 }, 280 // MinIO meta bucket. 281 { 282 bucket: minioMetaMultipartBucket, 283 result: true, 284 }, 285 // MinIO meta bucket. 286 { 287 bucket: minioMetaTmpBucket, 288 result: true, 289 }, 290 // Normal bucket 291 { 292 bucket: "mybucket", 293 result: false, 294 }, 295 } 296 297 for i, test := range testCases { 298 actual := isMinioMetaBucketName(test.bucket) 299 if actual != test.result { 300 t.Errorf("Test %d - expected %v but received %v", 301 i+1, test.result, actual) 302 } 303 } 304 } 305 306 // Tests RemoveStandardStorageClass method. Expectation is metadata map 307 // should be cleared of x-amz-storage-class, if it is set to STANDARD 308 func TestRemoveStandardStorageClass(t *testing.T) { 309 tests := []struct { 310 name string 311 metadata map[string]string 312 want map[string]string 313 }{ 314 { 315 name: "1", 316 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD"}, 317 want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"}, 318 }, 319 { 320 name: "2", 321 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"}, 322 want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"}, 323 }, 324 { 325 name: "3", 326 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"}, 327 want: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86"}, 328 }, 329 } 330 for _, tt := range tests { 331 if got := removeStandardStorageClass(tt.metadata); !reflect.DeepEqual(got, tt.want) { 332 t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got) 333 } 334 } 335 } 336 337 // Tests CleanMetadata method. Expectation is metadata map 338 // should be cleared of etag, md5Sum and x-amz-storage-class, if it is set to STANDARD 339 func TestCleanMetadata(t *testing.T) { 340 tests := []struct { 341 name string 342 metadata map[string]string 343 want map[string]string 344 }{ 345 { 346 name: "1", 347 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD"}, 348 want: map[string]string{"content-type": "application/octet-stream"}, 349 }, 350 { 351 name: "2", 352 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY"}, 353 want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "REDUCED_REDUNDANCY"}, 354 }, 355 { 356 name: "3", 357 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "md5Sum": "abcde"}, 358 want: map[string]string{"content-type": "application/octet-stream"}, 359 }, 360 } 361 for _, tt := range tests { 362 if got := cleanMetadata(tt.metadata); !reflect.DeepEqual(got, tt.want) { 363 t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got) 364 } 365 } 366 } 367 368 // Tests CleanMetadataKeys method. Expectation is metadata map 369 // should be cleared of keys passed to CleanMetadataKeys method 370 func TestCleanMetadataKeys(t *testing.T) { 371 tests := []struct { 372 name string 373 metadata map[string]string 374 keys []string 375 want map[string]string 376 }{ 377 { 378 name: "1", 379 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "STANDARD", "md5": "abcde"}, 380 keys: []string{"etag", "md5"}, 381 want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "STANDARD"}, 382 }, 383 { 384 name: "2", 385 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "x-amz-storage-class": "REDUCED_REDUNDANCY", "md5sum": "abcde"}, 386 keys: []string{"etag", "md5sum"}, 387 want: map[string]string{"content-type": "application/octet-stream", "x-amz-storage-class": "REDUCED_REDUNDANCY"}, 388 }, 389 { 390 name: "3", 391 metadata: map[string]string{"content-type": "application/octet-stream", "etag": "de75a98baf2c6aef435b57dd0fc33c86", "xyz": "abcde"}, 392 keys: []string{"etag", "xyz"}, 393 want: map[string]string{"content-type": "application/octet-stream"}, 394 }, 395 } 396 for _, tt := range tests { 397 if got := cleanMetadataKeys(tt.metadata, tt.keys...); !reflect.DeepEqual(got, tt.want) { 398 t.Errorf("Test %s failed, expected %v, got %v", tt.name, tt.want, got) 399 } 400 } 401 } 402 403 // Tests isCompressed method 404 func TestIsCompressed(t *testing.T) { 405 testCases := []struct { 406 objInfo ObjectInfo 407 result bool 408 err bool 409 }{ 410 0: { 411 objInfo: ObjectInfo{ 412 UserDefined: map[string]string{ 413 "X-Minio-Internal-compression": compressionAlgorithmV1, 414 "content-type": "application/octet-stream", 415 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 416 }, 417 }, 418 result: true, 419 }, 420 1: { 421 objInfo: ObjectInfo{ 422 UserDefined: map[string]string{ 423 "X-Minio-Internal-compression": compressionAlgorithmV2, 424 "content-type": "application/octet-stream", 425 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 426 }, 427 }, 428 result: true, 429 }, 430 2: { 431 objInfo: ObjectInfo{ 432 UserDefined: map[string]string{ 433 "X-Minio-Internal-compression": "unknown/compression/type", 434 "content-type": "application/octet-stream", 435 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 436 }, 437 }, 438 result: true, 439 err: true, 440 }, 441 3: { 442 objInfo: ObjectInfo{ 443 UserDefined: map[string]string{ 444 "X-Minio-Internal-compression": compressionAlgorithmV2, 445 "content-type": "application/octet-stream", 446 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 447 crypto.MetaIV: "yes", 448 }, 449 }, 450 result: true, 451 err: false, 452 }, 453 4: { 454 objInfo: ObjectInfo{ 455 UserDefined: map[string]string{ 456 "X-Minio-Internal-XYZ": "klauspost/compress/s2", 457 "content-type": "application/octet-stream", 458 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 459 }, 460 }, 461 result: false, 462 }, 463 5: { 464 objInfo: ObjectInfo{ 465 UserDefined: map[string]string{ 466 "content-type": "application/octet-stream", 467 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 468 }, 469 }, 470 result: false, 471 }, 472 } 473 for i, test := range testCases { 474 t.Run(strconv.Itoa(i), func(t *testing.T) { 475 got := test.objInfo.IsCompressed() 476 if got != test.result { 477 t.Errorf("IsCompressed: Expected %v but received %v", 478 test.result, got) 479 } 480 got, gErr := test.objInfo.IsCompressedOK() 481 if got != test.result { 482 t.Errorf("IsCompressedOK: Expected %v but received %v", 483 test.result, got) 484 } 485 if gErr != nil != test.err { 486 t.Errorf("IsCompressedOK: want error: %t, got error: %v", test.err, gErr) 487 } 488 }) 489 } 490 } 491 492 // Tests excludeForCompression. 493 func TestExcludeForCompression(t *testing.T) { 494 testCases := []struct { 495 object string 496 header http.Header 497 result bool 498 }{ 499 { 500 object: "object.txt", 501 header: http.Header{ 502 "Content-Type": []string{"application/zip"}, 503 }, 504 result: true, 505 }, 506 { 507 object: "object.zip", 508 header: http.Header{ 509 "Content-Type": []string{"application/XYZ"}, 510 }, 511 result: true, 512 }, 513 { 514 object: "object.json", 515 header: http.Header{ 516 "Content-Type": []string{"application/json"}, 517 }, 518 result: false, 519 }, 520 { 521 object: "object.txt", 522 header: http.Header{ 523 "Content-Type": []string{"text/plain"}, 524 }, 525 result: false, 526 }, 527 { 528 object: "object", 529 header: http.Header{ 530 "Content-Type": []string{"text/something"}, 531 }, 532 result: false, 533 }, 534 } 535 for i, test := range testCases { 536 got := excludeForCompression(test.header, test.object, compress.Config{ 537 Enabled: true, 538 }) 539 if got != test.result { 540 t.Errorf("Test %d - expected %v but received %v", 541 i+1, test.result, got) 542 } 543 } 544 } 545 546 func BenchmarkGetPartFileWithTrie(b *testing.B) { 547 b.ResetTimer() 548 549 entriesTrie := trie.NewTrie() 550 for i := 1; i <= 10000; i++ { 551 entriesTrie.Insert(fmt.Sprintf("%.5d.8a034f82cb9cb31140d87d3ce2a9ede3.67108864", i)) 552 } 553 554 for i := 1; i <= 10000; i++ { 555 partFile := getPartFile(entriesTrie, i, "8a034f82cb9cb31140d87d3ce2a9ede3") 556 if partFile == "" { 557 b.Fatal("partFile returned is empty") 558 } 559 } 560 561 b.ReportAllocs() 562 } 563 564 func TestGetActualSize(t *testing.T) { 565 testCases := []struct { 566 objInfo ObjectInfo 567 result int64 568 }{ 569 { 570 objInfo: ObjectInfo{ 571 UserDefined: map[string]string{ 572 "X-Minio-Internal-compression": "klauspost/compress/s2", 573 "X-Minio-Internal-actual-size": "100000001", 574 "content-type": "application/octet-stream", 575 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 576 }, 577 Parts: []ObjectPartInfo{ 578 { 579 Size: 39235668, 580 ActualSize: 67108864, 581 }, 582 { 583 Size: 19177372, 584 ActualSize: 32891137, 585 }, 586 }, 587 }, 588 result: 100000001, 589 }, 590 { 591 objInfo: ObjectInfo{ 592 UserDefined: map[string]string{ 593 "X-Minio-Internal-compression": "klauspost/compress/s2", 594 "X-Minio-Internal-actual-size": "841", 595 "content-type": "application/octet-stream", 596 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 597 }, 598 Parts: []ObjectPartInfo{}, 599 }, 600 result: 841, 601 }, 602 { 603 objInfo: ObjectInfo{ 604 UserDefined: map[string]string{ 605 "X-Minio-Internal-compression": "klauspost/compress/s2", 606 "content-type": "application/octet-stream", 607 "etag": "b3ff3ef3789147152fbfbc50efba4bfd-2", 608 }, 609 Parts: []ObjectPartInfo{}, 610 }, 611 result: -1, 612 }, 613 } 614 for i, test := range testCases { 615 got, _ := test.objInfo.GetActualSize() 616 if got != test.result { 617 t.Errorf("Test %d - expected %d but received %d", 618 i+1, test.result, got) 619 } 620 } 621 } 622 623 func TestGetCompressedOffsets(t *testing.T) { 624 testCases := []struct { 625 objInfo ObjectInfo 626 offset int64 627 startOffset int64 628 snappyStartOffset int64 629 firstPart int 630 }{ 631 0: { 632 objInfo: ObjectInfo{ 633 Parts: []ObjectPartInfo{ 634 { 635 Size: 39235668, 636 ActualSize: 67108864, 637 }, 638 { 639 Size: 19177372, 640 ActualSize: 32891137, 641 }, 642 }, 643 }, 644 offset: 79109865, 645 startOffset: 39235668, 646 snappyStartOffset: 12001001, 647 firstPart: 1, 648 }, 649 1: { 650 objInfo: ObjectInfo{ 651 Parts: []ObjectPartInfo{ 652 { 653 Size: 39235668, 654 ActualSize: 67108864, 655 }, 656 { 657 Size: 19177372, 658 ActualSize: 32891137, 659 }, 660 }, 661 }, 662 offset: 19109865, 663 startOffset: 0, 664 snappyStartOffset: 19109865, 665 }, 666 2: { 667 objInfo: ObjectInfo{ 668 Parts: []ObjectPartInfo{ 669 { 670 Size: 39235668, 671 ActualSize: 67108864, 672 }, 673 { 674 Size: 19177372, 675 ActualSize: 32891137, 676 }, 677 }, 678 }, 679 offset: 0, 680 startOffset: 0, 681 snappyStartOffset: 0, 682 }, 683 } 684 for i, test := range testCases { 685 startOffset, snappyStartOffset, firstPart, _, _ := getCompressedOffsets(test.objInfo, test.offset, nil) 686 if startOffset != test.startOffset { 687 t.Errorf("Test %d - expected startOffset %d but received %d", 688 i, test.startOffset, startOffset) 689 } 690 if snappyStartOffset != test.snappyStartOffset { 691 t.Errorf("Test %d - expected snappyOffset %d but received %d", 692 i, test.snappyStartOffset, snappyStartOffset) 693 } 694 if firstPart != test.firstPart { 695 t.Errorf("Test %d - expected firstPart %d but received %d", 696 i, test.firstPart, firstPart) 697 } 698 } 699 } 700 701 func TestS2CompressReader(t *testing.T) { 702 tests := []struct { 703 name string 704 data []byte 705 wantIdx bool 706 }{ 707 {name: "empty", data: nil}, 708 {name: "small", data: []byte("hello, world!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")}, 709 {name: "large", data: bytes.Repeat([]byte("hello, world"), 1000000), wantIdx: true}, 710 } 711 712 for _, tt := range tests { 713 t.Run(tt.name, func(t *testing.T) { 714 buf := make([]byte, 100) // make small buffer to ensure multiple reads are required for large case 715 716 r, idxCB := newS2CompressReader(bytes.NewReader(tt.data), int64(len(tt.data)), false) 717 defer r.Close() 718 719 var rdrBuf bytes.Buffer 720 _, err := io.CopyBuffer(&rdrBuf, r, buf) 721 if err != nil { 722 t.Fatal(err) 723 } 724 r.Close() 725 idx := idxCB() 726 if !tt.wantIdx && len(idx) > 0 { 727 t.Errorf("index returned above threshold") 728 } 729 if tt.wantIdx { 730 if idx == nil { 731 t.Errorf("no index returned") 732 } 733 var index s2.Index 734 _, err = index.Load(s2.RestoreIndexHeaders(idx)) 735 if err != nil { 736 t.Errorf("error loading index: %v", err) 737 } 738 t.Log("size:", len(idx)) 739 t.Log(string(index.JSON())) 740 if index.TotalUncompressed != int64(len(tt.data)) { 741 t.Errorf("Expected size %d, got %d", len(tt.data), index.TotalUncompressed) 742 } 743 } 744 var stdBuf bytes.Buffer 745 w := s2.NewWriter(&stdBuf) 746 _, err = io.CopyBuffer(w, bytes.NewReader(tt.data), buf) 747 if err != nil { 748 t.Fatal(err) 749 } 750 err = w.Close() 751 if err != nil { 752 t.Fatal(err) 753 } 754 755 var ( 756 got = rdrBuf.Bytes() 757 want = stdBuf.Bytes() 758 ) 759 if !bytes.Equal(got, want) { 760 t.Errorf("encoded data does not match\n\t%q\n\t%q", got, want) 761 } 762 763 var decBuf bytes.Buffer 764 decRdr := s2.NewReader(&rdrBuf) 765 _, err = io.Copy(&decBuf, decRdr) 766 if err != nil { 767 t.Fatal(err) 768 } 769 770 if !bytes.Equal(tt.data, decBuf.Bytes()) { 771 t.Errorf("roundtrip failed\n\t%q\n\t%q", tt.data, decBuf.Bytes()) 772 } 773 }) 774 } 775 } 776 777 func Test_pathNeedsClean(t *testing.T) { 778 type pathTest struct { 779 path, result string 780 } 781 782 cleantests := []pathTest{ 783 // Already clean 784 {"", "."}, 785 {"abc", "abc"}, 786 {"abc/def", "abc/def"}, 787 {"a/b/c", "a/b/c"}, 788 {".", "."}, 789 {"..", ".."}, 790 {"../..", "../.."}, 791 {"../../abc", "../../abc"}, 792 {"/abc", "/abc"}, 793 {"/abc/def", "/abc/def"}, 794 {"/", "/"}, 795 796 // Remove trailing slash 797 {"abc/", "abc"}, 798 {"abc/def/", "abc/def"}, 799 {"a/b/c/", "a/b/c"}, 800 {"./", "."}, 801 {"../", ".."}, 802 {"../../", "../.."}, 803 {"/abc/", "/abc"}, 804 805 // Remove doubled slash 806 {"abc//def//ghi", "abc/def/ghi"}, 807 {"//abc", "/abc"}, 808 {"///abc", "/abc"}, 809 {"//abc//", "/abc"}, 810 {"abc//", "abc"}, 811 812 // Remove . elements 813 {"abc/./def", "abc/def"}, 814 {"/./abc/def", "/abc/def"}, 815 {"abc/.", "abc"}, 816 817 // Remove .. elements 818 {"abc/def/ghi/../jkl", "abc/def/jkl"}, 819 {"abc/def/../ghi/../jkl", "abc/jkl"}, 820 {"abc/def/..", "abc"}, 821 {"abc/def/../..", "."}, 822 {"/abc/def/../..", "/"}, 823 {"abc/def/../../..", ".."}, 824 {"/abc/def/../../..", "/"}, 825 {"abc/def/../../../ghi/jkl/../../../mno", "../../mno"}, 826 827 // Combinations 828 {"abc/./../def", "def"}, 829 {"abc//./../def", "def"}, 830 {"abc/../../././../def", "../../def"}, 831 } 832 for _, test := range cleantests { 833 want := test.path != test.result 834 got := pathNeedsClean([]byte(test.path)) 835 if !got { 836 t.Logf("no clean: %q", test.path) 837 } 838 if want && !got { 839 t.Errorf("input: %q, want %v, got %v", test.path, want, got) 840 } 841 } 842 }