mirror of
https://github.com/versity/versitygw.git
synced 2025-12-23 13:15:18 +00:00
fix: add test cases and fix behavior for head/get range requests
This adds a bunch of test cases for non-0 len object, 0 len object, and directory objects to match verified AWS responses for the various range bytes cases. This fixes the posix head/get range responses for these test cases as well.
This commit is contained in:
@@ -95,40 +95,43 @@ var (
|
||||
// for invalid inputs, it returns no error, but isValid=false
|
||||
// `InvalidRange` error is returnd, only if startoffset is greater than the object size
|
||||
func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error) {
|
||||
// Return full object (invalid range, no error) if header empty
|
||||
if acceptRange == "" {
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
rangeKv := strings.Split(acceptRange, "=")
|
||||
|
||||
if len(rangeKv) != 2 {
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
if rangeKv[0] != "bytes" {
|
||||
if rangeKv[0] != "bytes" { // unsupported unit -> ignore
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
bRange := strings.Split(rangeKv[1], "-")
|
||||
if len(bRange) != 2 {
|
||||
if len(bRange) != 2 { // malformed / multi-range
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
// Parse start; empty start indicates a suffix-byte-range-spec (e.g. bytes=-100)
|
||||
startOffset, err := strconv.ParseInt(bRange[0], 10, strconv.IntSize)
|
||||
if startOffset > int64(math.MaxInt) || startOffset < int64(math.MinInt) {
|
||||
return 0, size, false, errInvalidRange
|
||||
}
|
||||
if err != nil && bRange[0] != "" {
|
||||
if err != nil && bRange[0] != "" { // invalid numeric start (non-empty) -> ignore range
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
// If end part missing (e.g. bytes=100-)
|
||||
if bRange[1] == "" {
|
||||
if bRange[0] == "" {
|
||||
if bRange[0] == "" { // bytes=- (meaningless) -> ignore
|
||||
return 0, size, false, nil
|
||||
}
|
||||
// start beyond or at size is unsatisfiable -> error (RequestedRangeNotSatisfiable)
|
||||
if startOffset >= size {
|
||||
return 0, 0, false, errInvalidRange
|
||||
}
|
||||
// bytes=100- => from start to end
|
||||
return startOffset, size - startOffset, true, nil
|
||||
}
|
||||
|
||||
@@ -136,28 +139,37 @@ func ParseObjectRange(size int64, acceptRange string) (int64, int64, bool, error
|
||||
if endOffset > int64(math.MaxInt) {
|
||||
return 0, size, false, errInvalidRange
|
||||
}
|
||||
if err != nil {
|
||||
if err != nil { // invalid numeric end -> ignore range
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
if startOffset > endOffset {
|
||||
return 0, size, false, nil
|
||||
}
|
||||
|
||||
// for ranges like 'bytes=-100' return the last bytes specified with 'endOffset'
|
||||
// Suffix range handling (bRange[0] == "")
|
||||
if bRange[0] == "" {
|
||||
// Disallow -0 (always unsatisfiable)
|
||||
if endOffset == 0 {
|
||||
return 0, 0, false, errInvalidRange
|
||||
}
|
||||
// For zero-sized objects any positive suffix is treated as invalid (ignored, no error)
|
||||
if size == 0 {
|
||||
return 0, size, false, nil
|
||||
}
|
||||
// Clamp to object size (request more bytes than exist -> entire object)
|
||||
endOffset = min(endOffset, size)
|
||||
return size - endOffset, endOffset, true, nil
|
||||
}
|
||||
|
||||
// Normal range (start-end)
|
||||
if startOffset > endOffset { // start > end -> ignore
|
||||
return 0, size, false, nil
|
||||
}
|
||||
// Start beyond or at end of object -> error
|
||||
if startOffset >= size {
|
||||
return 0, 0, false, errInvalidRange
|
||||
}
|
||||
|
||||
// Adjust end beyond object size (trim)
|
||||
if endOffset >= size {
|
||||
endOffset = size - 1
|
||||
}
|
||||
|
||||
return startOffset, endOffset - startOffset + 1, true, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -3484,6 +3484,11 @@ func (p *Posix) GetObject(_ context.Context, input *s3.GetObjectInput) (*s3.GetO
|
||||
}
|
||||
|
||||
if fid.IsDir() {
|
||||
_, _, _, err := backend.ParseObjectRange(0, *input.Range)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userMetaData := make(map[string]string)
|
||||
|
||||
objMeta := p.loadObjectMetaData(nil, bucket, object, &fid, userMetaData)
|
||||
@@ -3794,6 +3799,9 @@ func (p *Posix) HeadObject(ctx context.Context, input *s3.HeadObjectInput) (*s3.
|
||||
}
|
||||
|
||||
size := fi.Size()
|
||||
if fi.IsDir() {
|
||||
size = 0
|
||||
}
|
||||
|
||||
startOffset, length, isValid, err := backend.ParseObjectRange(size, getString(input.Range))
|
||||
if err != nil {
|
||||
|
||||
@@ -171,6 +171,8 @@ func TestHeadObject(s *S3Conf) {
|
||||
HeadObject_non_existing_dir_object(s)
|
||||
HeadObject_invalid_parent_dir(s)
|
||||
HeadObject_with_range(s)
|
||||
HeadObject_zero_len_with_range(s)
|
||||
HeadObject_dir_with_range(s)
|
||||
//TODO: remove the condition after implementing checksums in azure
|
||||
if !s.azureTests {
|
||||
HeadObject_not_enabled_checksum_mode(s)
|
||||
@@ -197,6 +199,8 @@ func TestGetObject(s *S3Conf) {
|
||||
GetObject_non_existing_key(s)
|
||||
GetObject_directory_object_noslash(s)
|
||||
GetObject_with_range(s)
|
||||
GetObject_zero_len_with_range(s)
|
||||
GetObject_dir_with_range(s)
|
||||
GetObject_invalid_parent(s)
|
||||
GetObject_large_object(s)
|
||||
//TODO: remove the condition after implementing checksums in azure
|
||||
@@ -1002,6 +1006,8 @@ func GetIntTests() IntTests {
|
||||
"HeadObject_name_too_long": HeadObject_name_too_long,
|
||||
"HeadObject_invalid_parent_dir": HeadObject_invalid_parent_dir,
|
||||
"HeadObject_with_range": HeadObject_with_range,
|
||||
"HeadObject_zero_len_with_range": HeadObject_zero_len_with_range,
|
||||
"HeadObject_dir_with_range": HeadObject_dir_with_range,
|
||||
"HeadObject_not_enabled_checksum_mode": HeadObject_not_enabled_checksum_mode,
|
||||
"HeadObject_checksums": HeadObject_checksums,
|
||||
"HeadObject_success": HeadObject_success,
|
||||
@@ -1016,6 +1022,8 @@ func GetIntTests() IntTests {
|
||||
"GetObject_non_existing_key": GetObject_non_existing_key,
|
||||
"GetObject_directory_object_noslash": GetObject_directory_object_noslash,
|
||||
"GetObject_with_range": GetObject_with_range,
|
||||
"GetObject_zero_len_with_range": GetObject_zero_len_with_range,
|
||||
"GetObject_dir_with_range": GetObject_dir_with_range,
|
||||
"GetObject_invalid_parent": GetObject_invalid_parent,
|
||||
"GetObject_large_object": GetObject_large_object,
|
||||
"GetObject_checksums": GetObject_checksums,
|
||||
|
||||
@@ -3758,7 +3758,7 @@ func HeadObject_invalid_parent_dir(s *S3Conf) error {
|
||||
func HeadObject_with_range(s *S3Conf) error {
|
||||
testName := "HeadObject_with_range"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
obj, objLength := "my-obj", int64(2300)
|
||||
obj, objLength := "my-obj", int64(100)
|
||||
_, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
@@ -3776,7 +3776,7 @@ func HeadObject_with_range(s *S3Conf) error {
|
||||
})
|
||||
cancel()
|
||||
if err == nil && expectErr {
|
||||
return fmt.Errorf("expected err 'RequestedRangeNotSatisfiable' error, instead got nil")
|
||||
return fmt.Errorf("%v: expected err 'RequestedRangeNotSatisfiable' error, instead got nil", rg)
|
||||
}
|
||||
if err != nil {
|
||||
if !expectErr {
|
||||
@@ -3786,75 +3786,94 @@ func HeadObject_with_range(s *S3Conf) error {
|
||||
var ae smithy.APIError
|
||||
if errors.As(err, &ae) {
|
||||
if ae.ErrorCode() != "RequestedRangeNotSatisfiable" {
|
||||
return fmt.Errorf("expected RequestedRangeNotSatisfiable, instead got %v", ae.ErrorCode())
|
||||
return fmt.Errorf("%v: expected RequestedRangeNotSatisfiable, instead got %v", rg, ae.ErrorCode())
|
||||
}
|
||||
if ae.ErrorMessage() != "Requested Range Not Satisfiable" {
|
||||
return fmt.Errorf("expected the error message to be 'Requested Range Not Satisfiable', instead got %v", ae.ErrorMessage())
|
||||
return fmt.Errorf("%v: expected the error message to be 'Requested Range Not Satisfiable', instead got %v", rg, ae.ErrorMessage())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid error got %w", err)
|
||||
return fmt.Errorf("%v: invalid error got %w", rg, err)
|
||||
}
|
||||
|
||||
if getString(res.AcceptRanges) != "bytes" {
|
||||
return fmt.Errorf("expected accept ranges to be 'bytes', instead got %v", getString(res.AcceptRanges))
|
||||
return fmt.Errorf("%v: expected accept ranges to be 'bytes', instead got %v", rg, getString(res.AcceptRanges))
|
||||
}
|
||||
if res.ContentLength == nil {
|
||||
return fmt.Errorf("expected non nil content-length")
|
||||
return fmt.Errorf("%v: expected non nil content-length", rg)
|
||||
}
|
||||
if *res.ContentLength != cLength {
|
||||
return fmt.Errorf("expected content-length to be %v, instead got %v", cLength, *res.ContentLength)
|
||||
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rg, cLength, *res.ContentLength)
|
||||
}
|
||||
if getString(res.ContentRange) != contentRange {
|
||||
return fmt.Errorf("expected content-range to be %v, instead got %v", contentRange, getString(res.ContentRange))
|
||||
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rg, contentRange, getString(res.ContentRange))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reference server expectations for a 100-byte object.
|
||||
for _, el := range []struct {
|
||||
objRange string
|
||||
contentRange string
|
||||
contentLength int64
|
||||
expectedErr bool
|
||||
}{
|
||||
// invalid ranges: no error
|
||||
{"100", "", objLength, false},
|
||||
{"100-", "", objLength, false},
|
||||
{"invalid_range", "", objLength, false},
|
||||
{"bytes=120", "", objLength, false},
|
||||
{"bytes=20-10", "", objLength, false},
|
||||
// The following inputs should NOT produce an error and return the full object with empty Content-Range.
|
||||
{"bytes=,", "", objLength, false},
|
||||
{"bytes= -1", "", objLength, false},
|
||||
{"bytes=--1", "", objLength, false},
|
||||
{"bytes=0 -1", "", objLength, false},
|
||||
{"bytes=0--1", "", objLength, false},
|
||||
{"bytes=10-5", "", objLength, false}, // start > end treated as invalid
|
||||
{"bytes=abc", "", objLength, false},
|
||||
{"bytes=abc-xyz", "", objLength, false},
|
||||
{"bytes=a-z", "", objLength, false},
|
||||
{"foo=0-1", "", objLength, false}, // unsupported unit
|
||||
{"bytes=00-01", "bytes 0-1/100", 2, false}, // valid numeric despite leading zeros
|
||||
{"bytes=abc-xyz", "", objLength, false}, // retain legacy invalid pattern
|
||||
{"bytes=100-x", "", objLength, false},
|
||||
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength-100), "", objLength, false},
|
||||
// valid ranges
|
||||
{"bytes=-1000000", fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, false},
|
||||
{"bytes=100-", fmt.Sprintf("bytes 100-%v/%v", objLength-1, objLength), objLength - 100, false},
|
||||
{"bytes=-100", fmt.Sprintf("bytes %v-%v/%v", objLength-100, objLength-1, objLength), 100, false},
|
||||
{"bytes=0-", fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, false},
|
||||
{"bytes=100-200", fmt.Sprintf("bytes 100-200/%v", objLength), 101, false},
|
||||
{fmt.Sprintf("bytes=100-%v", objLength), fmt.Sprintf("bytes 100-%v/%v", objLength-1, objLength), objLength - 100, false},
|
||||
{fmt.Sprintf("bytes=0-%v", objLength), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, false},
|
||||
{fmt.Sprintf("bytes=0-%v", objLength-1), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, false},
|
||||
{fmt.Sprintf("bytes=-%v", objLength), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, false},
|
||||
{"bytes=0-0,1-2", "", objLength, false}, // multiple ranges unsupported -> ignore
|
||||
|
||||
// not satisfiable ranges: return error
|
||||
{fmt.Sprintf("bytes=%v-", objLength), "", 0, true},
|
||||
{fmt.Sprintf("bytes=%v-", objLength+2), "", 0, true},
|
||||
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength+100), "", 0, true},
|
||||
// Valid suffix ranges (negative forms)
|
||||
{"bytes=-1", "bytes 99-99/100", 1, false},
|
||||
{"bytes=-2", "bytes 98-99/100", 2, false},
|
||||
{"bytes=-10", "bytes 90-99/100", 10, false},
|
||||
{"bytes=-100", "bytes 0-99/100", objLength, false},
|
||||
{"bytes=-101", "bytes 0-99/100", objLength, false}, // larger than object -> entire object
|
||||
|
||||
// Standard byte ranges
|
||||
{"bytes=0-0", "bytes 0-0/100", 1, false},
|
||||
{"bytes=0-99", "bytes 0-99/100", objLength, false},
|
||||
{"bytes=0-100", "bytes 0-99/100", objLength, false}, // end past object -> trimmed
|
||||
{"bytes=0-999999", "bytes 0-99/100", objLength, false},
|
||||
{"bytes=1-99", "bytes 1-99/100", objLength - 1, false},
|
||||
{"bytes=50-99", "bytes 50-99/100", 50, false},
|
||||
{"bytes=50-", "bytes 50-99/100", 50, false},
|
||||
{"bytes=0-", "bytes 0-99/100", objLength, false},
|
||||
{"bytes=99-99", "bytes 99-99/100", 1, false},
|
||||
|
||||
// Ranges expected to produce RequestedRangeNotSatisfiable
|
||||
{"bytes=-0", "", 0, true},
|
||||
{"bytes=100-100", "", 0, true},
|
||||
{"bytes=100-110", "", 0, true},
|
||||
} {
|
||||
err := testRange(el.objRange, el.contentRange, el.contentLength, el.expectedErr)
|
||||
if err != nil {
|
||||
if err := testRange(el.objRange, el.contentRange, el.contentLength, el.expectedErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func HeadObject_zero_len_with_range(s *S3Conf) error {
|
||||
testName := "HeadObject_zero_len_with_range"
|
||||
return headObject_zero_len_with_range_helper(testName, "my-obj", s)
|
||||
}
|
||||
|
||||
func HeadObject_dir_with_range(s *S3Conf) error {
|
||||
testName := "HeadObject_dir_with_range"
|
||||
return headObject_zero_len_with_range_helper(testName, "my-dir/", s)
|
||||
}
|
||||
|
||||
func HeadObject_success(s *S3Conf) error {
|
||||
testName := "HeadObject_success"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
@@ -4273,7 +4292,8 @@ func GetObject_directory_object_noslash(s *S3Conf) error {
|
||||
func GetObject_with_range(s *S3Conf) error {
|
||||
testName := "GetObject_with_range"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
obj, objLength := "my-obj", int64(800)
|
||||
// Match HeadObject_with_range: 100-byte object
|
||||
obj, objLength := "my-obj", int64(100)
|
||||
res, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
@@ -4291,18 +4311,16 @@ func GetObject_with_range(s *S3Conf) error {
|
||||
Range: &rng,
|
||||
})
|
||||
if err == nil && expErr != nil {
|
||||
return fmt.Errorf("expected err %w, instead got nil", expErr)
|
||||
return fmt.Errorf("expected err %v, instead got nil", expErr)
|
||||
}
|
||||
if err != nil {
|
||||
if expErr == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parsedErr, ok := expErr.(s3err.APIError)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid error type provided, expected s3err.APIError")
|
||||
}
|
||||
|
||||
return checkApiErr(err, parsedErr)
|
||||
}
|
||||
|
||||
@@ -4328,7 +4346,6 @@ func GetObject_with_range(s *S3Conf) error {
|
||||
if !isSameData(outData, expData) {
|
||||
return fmt.Errorf("incorrect data retrieved")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4339,42 +4356,65 @@ func GetObject_with_range(s *S3Conf) error {
|
||||
expData []byte
|
||||
expErr error
|
||||
}{
|
||||
// invalid ranges: no error
|
||||
{"100", "", objLength, res.data, nil},
|
||||
{"100-", "", objLength, res.data, nil},
|
||||
{"invalid_range", "", objLength, res.data, nil},
|
||||
{"bytes=120", "", objLength, res.data, nil},
|
||||
{"bytes=20-10", "", objLength, res.data, nil},
|
||||
// Invalid / ignored ranges (return full object, empty Content-Range)
|
||||
{"bytes=,", "", objLength, res.data, nil},
|
||||
{"bytes= -1", "", objLength, res.data, nil},
|
||||
{"bytes=--1", "", objLength, res.data, nil},
|
||||
{"bytes=0 -1", "", objLength, res.data, nil},
|
||||
{"bytes=0--1", "", objLength, res.data, nil},
|
||||
{"bytes=10-5", "", objLength, res.data, nil},
|
||||
{"bytes=abc", "", objLength, res.data, nil},
|
||||
{"bytes=a-z", "", objLength, res.data, nil},
|
||||
{"foo=0-1", "", objLength, res.data, nil},
|
||||
{"bytes=abc-xyz", "", objLength, res.data, nil},
|
||||
{"bytes=100-x", "", objLength, res.data, nil},
|
||||
{"bytes=0-0,1-2", "", objLength, res.data, nil},
|
||||
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength-100), "", objLength, res.data, nil},
|
||||
// valid ranges
|
||||
{"bytes=-1000000", fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, res.data, nil},
|
||||
{"bytes=100-", fmt.Sprintf("bytes 100-%v/%v", objLength-1, objLength), objLength - 100, res.data[100:], nil},
|
||||
{"bytes=-100", fmt.Sprintf("bytes %v-%v/%v", objLength-100, objLength-1, objLength), 100, res.data[objLength-100:], nil},
|
||||
{"bytes=0-", fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, res.data, nil},
|
||||
{"bytes=100-200", fmt.Sprintf("bytes 100-200/%v", objLength), 101, res.data[100:201], nil},
|
||||
{fmt.Sprintf("bytes=100-%v", objLength), fmt.Sprintf("bytes 100-%v/%v", objLength-1, objLength), objLength - 100, res.data[100:], nil},
|
||||
{fmt.Sprintf("bytes=0-%v", objLength), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, res.data, nil},
|
||||
{fmt.Sprintf("bytes=0-%v", objLength-1), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, res.data, nil},
|
||||
{fmt.Sprintf("bytes=-%v", objLength), fmt.Sprintf("bytes 0-%v/%v", objLength-1, objLength), objLength, res.data, nil},
|
||||
|
||||
// not satisfiable ranges: return error
|
||||
{fmt.Sprintf("bytes=%v-", objLength), "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{fmt.Sprintf("bytes=%v-", objLength+2), "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{fmt.Sprintf("bytes=%v-%v", objLength+2, objLength+100), "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
// Valid numeric with leading zeros
|
||||
{"bytes=00-01", "bytes 0-1/100", 2, res.data[0:2], nil},
|
||||
|
||||
// Suffix ranges
|
||||
{"bytes=-1", "bytes 99-99/100", 1, res.data[99:], nil},
|
||||
{"bytes=-2", "bytes 98-99/100", 2, res.data[98:], nil},
|
||||
{"bytes=-10", "bytes 90-99/100", 10, res.data[90:], nil},
|
||||
{"bytes=-100", "bytes 0-99/100", objLength, res.data, nil},
|
||||
{"bytes=-101", "bytes 0-99/100", objLength, res.data, nil},
|
||||
|
||||
// Standard byte ranges
|
||||
{"bytes=0-0", "bytes 0-0/100", 1, res.data[0:1], nil},
|
||||
{"bytes=0-99", "bytes 0-99/100", objLength, res.data, nil},
|
||||
{"bytes=0-100", "bytes 0-99/100", objLength, res.data, nil},
|
||||
{"bytes=0-999999", "bytes 0-99/100", objLength, res.data, nil},
|
||||
{"bytes=1-99", "bytes 1-99/100", 99, res.data[1:], nil},
|
||||
{"bytes=50-99", "bytes 50-99/100", 50, res.data[50:], nil},
|
||||
{"bytes=50-", "bytes 50-99/100", 50, res.data[50:], nil},
|
||||
{"bytes=0-", "bytes 0-99/100", objLength, res.data, nil},
|
||||
{"bytes=99-99", "bytes 99-99/100", 1, res.data[99:], nil},
|
||||
|
||||
// Unsatisfiable -> error
|
||||
{"bytes=-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{"bytes=100-100", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{"bytes=100-110", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
} {
|
||||
err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr)
|
||||
if err != nil {
|
||||
if err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func GetObject_zero_len_with_range(s *S3Conf) error {
|
||||
testName := "GetObject_zero_len_with_range"
|
||||
return getObject_zero_len_with_range_helper(testName, "my-obj", s)
|
||||
}
|
||||
|
||||
func GetObject_dir_with_range(s *S3Conf) error {
|
||||
testName := "GetObject_dir_with_range"
|
||||
return getObject_zero_len_with_range_helper(testName, "my-dir/", s)
|
||||
}
|
||||
|
||||
func GetObject_invalid_parent(s *S3Conf) error {
|
||||
testName := "GetObject_invalid_parent"
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
|
||||
@@ -1457,3 +1457,175 @@ func randomizeCase(s string) string {
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func headObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
objLength := int64(0)
|
||||
_, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testRange := func(rg, contentRange string, cLength int64, expectErr bool) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
res, err := s3client.HeadObject(ctx, &s3.HeadObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Range: &rg,
|
||||
})
|
||||
cancel()
|
||||
if err == nil && expectErr {
|
||||
return fmt.Errorf("%v: expected err 'RequestedRangeNotSatisfiable' error, instead got nil", rg)
|
||||
}
|
||||
if err != nil {
|
||||
if !expectErr {
|
||||
return err
|
||||
}
|
||||
var ae smithy.APIError
|
||||
if errors.As(err, &ae) {
|
||||
if ae.ErrorCode() != "RequestedRangeNotSatisfiable" {
|
||||
return fmt.Errorf("%v: expected RequestedRangeNotSatisfiable, instead got %v", rg, ae.ErrorCode())
|
||||
}
|
||||
if ae.ErrorMessage() != "Requested Range Not Satisfiable" {
|
||||
return fmt.Errorf("%v: expected the error message to be 'Requested Range Not Satisfiable', instead got %v", rg, ae.ErrorMessage())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%v: invalid error got %w", rg, err)
|
||||
}
|
||||
|
||||
if getString(res.AcceptRanges) != "bytes" {
|
||||
return fmt.Errorf("%v: expected accept ranges to be 'bytes', instead got %v", rg, getString(res.AcceptRanges))
|
||||
}
|
||||
if res.ContentLength == nil {
|
||||
return fmt.Errorf("%v: expected non nil content-length", rg)
|
||||
}
|
||||
if *res.ContentLength != cLength {
|
||||
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rg, cLength, *res.ContentLength)
|
||||
}
|
||||
if getString(res.ContentRange) != contentRange {
|
||||
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rg, contentRange, getString(res.ContentRange))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reference server expectations for a 0-byte object.
|
||||
for _, el := range []struct {
|
||||
objRange string
|
||||
contentRange string
|
||||
contentLength int64
|
||||
expectedErr bool
|
||||
}{
|
||||
{"bytes=abc", "", objLength, false},
|
||||
{"bytes=a-z", "", objLength, false},
|
||||
{"bytes=,", "", objLength, false},
|
||||
{"bytes=0-0,1-2", "", objLength, false},
|
||||
{"foo=0-1", "", objLength, false},
|
||||
{"bytes=--1", "", objLength, false},
|
||||
{"bytes=0--1", "", objLength, false},
|
||||
{"bytes= -1", "", objLength, false},
|
||||
{"bytes=0 -1", "", objLength, false},
|
||||
{"bytes=-1", "", objLength, false}, // reference server returns no error, empty Content-Range
|
||||
{"bytes=00-01", "", objLength, true}, // RequestedRangeNotSatisfiable
|
||||
{"bytes=-0", "", 0, true},
|
||||
{"bytes=0-0", "", 0, true},
|
||||
{"bytes=0-", "", 0, true},
|
||||
} {
|
||||
if err := testRange(el.objRange, el.contentRange, el.contentLength, el.expectedErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func getObject_zero_len_with_range_helper(testName, obj string, s *S3Conf) error {
|
||||
return actionHandler(s, testName, func(s3client *s3.Client, bucket string) error {
|
||||
objLength := int64(0)
|
||||
res, err := putObjectWithData(objLength, &s3.PutObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
}, s3client)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
testGetObjectRange := func(rng, contentRange string, cLength int64, expData []byte, expErr error) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), shortTimeout)
|
||||
defer cancel()
|
||||
out, err := s3client.GetObject(ctx, &s3.GetObjectInput{
|
||||
Bucket: &bucket,
|
||||
Key: &obj,
|
||||
Range: &rng,
|
||||
})
|
||||
if err == nil && expErr != nil {
|
||||
return fmt.Errorf("%v: expected err %v, instead got nil", rng, expErr)
|
||||
}
|
||||
if err != nil {
|
||||
if expErr == nil {
|
||||
return err
|
||||
}
|
||||
parsedErr, ok := expErr.(s3err.APIError)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid error type provided, expected s3err.APIError")
|
||||
}
|
||||
return checkApiErr(err, parsedErr)
|
||||
}
|
||||
|
||||
if out.ContentLength == nil {
|
||||
return fmt.Errorf("%v: expected non nil content-length", rng)
|
||||
}
|
||||
if *out.ContentLength != cLength {
|
||||
return fmt.Errorf("%v: expected content-length to be %v, instead got %v", rng, cLength, *out.ContentLength)
|
||||
}
|
||||
if getString(out.AcceptRanges) != "bytes" {
|
||||
return fmt.Errorf("%v: expected accept-ranges to be 'bytes', instead got %v", rng, getString(out.AcceptRanges))
|
||||
}
|
||||
if getString(out.ContentRange) != contentRange {
|
||||
return fmt.Errorf("%v: expected content-range to be %v, instead got %v", rng, contentRange, getString(out.ContentRange))
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(out.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v: read object data: %w", rng, err)
|
||||
}
|
||||
out.Body.Close()
|
||||
if !isSameData(data, expData) {
|
||||
return fmt.Errorf("%v: incorrect data retrieved", rng)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, el := range []struct {
|
||||
rng string
|
||||
contentRange string
|
||||
cLength int64
|
||||
expData []byte
|
||||
expErr error
|
||||
}{
|
||||
{"bytes=abc", "", objLength, res.data, nil},
|
||||
{"bytes=a-z", "", objLength, res.data, nil},
|
||||
{"bytes=,", "", objLength, res.data, nil},
|
||||
{"bytes=0-0,1-2", "", objLength, res.data, nil},
|
||||
{"foo=0-1", "", objLength, res.data, nil},
|
||||
{"bytes=--1", "", objLength, res.data, nil},
|
||||
{"bytes=0--1", "", objLength, res.data, nil},
|
||||
{"bytes= -1", "", objLength, res.data, nil},
|
||||
{"bytes=0 -1", "", objLength, res.data, nil},
|
||||
{"bytes=-1", "", objLength, res.data, nil},
|
||||
// error (RequestedRangeNotSatisfiable)
|
||||
{"bytes=00-01", "", objLength, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{"bytes=-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{"bytes=0-0", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
{"bytes=0-", "", 0, nil, s3err.GetAPIError(s3err.ErrInvalidRange)},
|
||||
} {
|
||||
if err := testGetObjectRange(el.rng, el.contentRange, el.cLength, el.expData, el.expErr); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user