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  }