storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/httprange.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2015, 2016 MinIO, Inc.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package cmd
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  )
    25  
    26  const (
    27  	byteRangePrefix = "bytes="
    28  )
    29  
    30  // HTTPRangeSpec represents a range specification as supported by S3 GET
    31  // object request.
    32  //
    33  // Case 1: Not present -> represented by a nil RangeSpec
    34  // Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10}
    35  // Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1}
    36  // Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1}
    37  type HTTPRangeSpec struct {
    38  	// Does the range spec refer to a suffix of the object?
    39  	IsSuffixLength bool
    40  
    41  	// Start and end offset specified in range spec
    42  	Start, End int64
    43  }
    44  
    45  // GetLength - get length of range
    46  func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) {
    47  	switch {
    48  	case resourceSize < 0:
    49  		return 0, errors.New("Resource size cannot be negative")
    50  
    51  	case h == nil:
    52  		rangeLength = resourceSize
    53  
    54  	case h.IsSuffixLength:
    55  		specifiedLen := -h.Start
    56  		rangeLength = specifiedLen
    57  		if specifiedLen > resourceSize {
    58  			rangeLength = resourceSize
    59  		}
    60  
    61  	case h.Start >= resourceSize:
    62  		return 0, errInvalidRange
    63  
    64  	case h.End > -1:
    65  		end := h.End
    66  		if resourceSize <= end {
    67  			end = resourceSize - 1
    68  		}
    69  		rangeLength = end - h.Start + 1
    70  
    71  	case h.End == -1:
    72  		rangeLength = resourceSize - h.Start
    73  
    74  	default:
    75  		return 0, errors.New("Unexpected range specification case")
    76  	}
    77  
    78  	return rangeLength, nil
    79  }
    80  
    81  // GetOffsetLength computes the start offset and length of the range
    82  // given the size of the resource
    83  func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) {
    84  	if h == nil {
    85  		// No range specified, implies whole object.
    86  		return 0, resourceSize, nil
    87  	}
    88  
    89  	length, err = h.GetLength(resourceSize)
    90  	if err != nil {
    91  		return 0, 0, err
    92  	}
    93  
    94  	start = h.Start
    95  	if h.IsSuffixLength {
    96  		start = resourceSize + h.Start
    97  		if start < 0 {
    98  			start = 0
    99  		}
   100  	}
   101  	return start, length, nil
   102  }
   103  
   104  // Parse a HTTP range header value into a HTTPRangeSpec
   105  func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) {
   106  	// Return error if given range string doesn't start with byte range prefix.
   107  	if !strings.HasPrefix(rangeString, byteRangePrefix) {
   108  		return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix)
   109  	}
   110  
   111  	// Trim byte range prefix.
   112  	byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix)
   113  
   114  	// Check if range string contains delimiter '-', else return error. eg. "bytes=8"
   115  	sepIndex := strings.Index(byteRangeString, "-")
   116  	if sepIndex == -1 {
   117  		return nil, fmt.Errorf("'%s' does not have a valid range value", rangeString)
   118  	}
   119  
   120  	offsetBeginString := byteRangeString[:sepIndex]
   121  	offsetBegin := int64(-1)
   122  	// Convert offsetBeginString only if its not empty.
   123  	if len(offsetBeginString) > 0 {
   124  		if offsetBeginString[0] == '+' {
   125  			return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString)
   126  		} else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil {
   127  			return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString)
   128  		} else if offsetBegin < 0 {
   129  			return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin)
   130  		}
   131  	}
   132  
   133  	offsetEndString := byteRangeString[sepIndex+1:]
   134  	offsetEnd := int64(-1)
   135  	// Convert offsetEndString only if its not empty.
   136  	if len(offsetEndString) > 0 {
   137  		if offsetEndString[0] == '+' {
   138  			return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString)
   139  		} else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil {
   140  			return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString)
   141  		} else if offsetEnd < 0 {
   142  			return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd)
   143  		}
   144  	}
   145  
   146  	switch {
   147  	case offsetBegin > -1 && offsetEnd > -1:
   148  		if offsetBegin > offsetEnd {
   149  			return nil, errInvalidRange
   150  		}
   151  		return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil
   152  	case offsetBegin > -1:
   153  		return &HTTPRangeSpec{false, offsetBegin, -1}, nil
   154  	case offsetEnd > -1:
   155  		if offsetEnd == 0 {
   156  			return nil, errInvalidRange
   157  		}
   158  		return &HTTPRangeSpec{true, -offsetEnd, -1}, nil
   159  	default:
   160  		// rangeString contains first and last byte positions missing. eg. "bytes=-"
   161  		return nil, fmt.Errorf("'%s' does not have valid range value", rangeString)
   162  	}
   163  }
   164  
   165  // String returns stringified representation of range for a particular resource size.
   166  func (h *HTTPRangeSpec) String(resourceSize int64) string {
   167  	if h == nil {
   168  		return ""
   169  	}
   170  	off, length, err := h.GetOffsetLength(resourceSize)
   171  	if err != nil {
   172  		return ""
   173  	}
   174  	return fmt.Sprintf("%d-%d", off, off+length-1)
   175  }