github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/cmd/httprange.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 "errors" 22 "fmt" 23 "strconv" 24 "strings" 25 ) 26 27 const ( 28 byteRangePrefix = "bytes=" 29 ) 30 31 // HTTPRangeSpec represents a range specification as supported by S3 GET 32 // object request. 33 // 34 // Case 1: Not present -> represented by a nil RangeSpec 35 // Case 2: bytes=1-10 (absolute start and end offsets) -> RangeSpec{false, 1, 10} 36 // Case 3: bytes=10- (absolute start offset with end offset unspecified) -> RangeSpec{false, 10, -1} 37 // Case 4: bytes=-30 (suffix length specification) -> RangeSpec{true, -30, -1} 38 type HTTPRangeSpec struct { 39 // Does the range spec refer to a suffix of the object? 40 IsSuffixLength bool 41 42 // Start and end offset specified in range spec 43 Start, End int64 44 } 45 46 // GetLength - get length of range 47 func (h *HTTPRangeSpec) GetLength(resourceSize int64) (rangeLength int64, err error) { 48 switch { 49 case resourceSize < 0: 50 return 0, errors.New("Resource size cannot be negative") 51 52 case h == nil: 53 rangeLength = resourceSize 54 55 case h.IsSuffixLength: 56 specifiedLen := -h.Start 57 rangeLength = specifiedLen 58 if specifiedLen > resourceSize { 59 rangeLength = resourceSize 60 } 61 62 case h.Start >= resourceSize: 63 return 0, errInvalidRange 64 65 case h.End > -1: 66 end := h.End 67 if resourceSize <= end { 68 end = resourceSize - 1 69 } 70 rangeLength = end - h.Start + 1 71 72 case h.End == -1: 73 rangeLength = resourceSize - h.Start 74 75 default: 76 return 0, errors.New("Unexpected range specification case") 77 } 78 79 return rangeLength, nil 80 } 81 82 // GetOffsetLength computes the start offset and length of the range 83 // given the size of the resource 84 func (h *HTTPRangeSpec) GetOffsetLength(resourceSize int64) (start, length int64, err error) { 85 if h == nil { 86 // No range specified, implies whole object. 87 return 0, resourceSize, nil 88 } 89 90 length, err = h.GetLength(resourceSize) 91 if err != nil { 92 return 0, 0, err 93 } 94 95 start = h.Start 96 if h.IsSuffixLength { 97 start = resourceSize + h.Start 98 if start < 0 { 99 start = 0 100 } 101 } 102 return start, length, nil 103 } 104 105 // Parse a HTTP range header value into a HTTPRangeSpec 106 func parseRequestRangeSpec(rangeString string) (hrange *HTTPRangeSpec, err error) { 107 // Return error if given range string doesn't start with byte range prefix. 108 if !strings.HasPrefix(rangeString, byteRangePrefix) { 109 return nil, fmt.Errorf("'%s' does not start with '%s'", rangeString, byteRangePrefix) 110 } 111 112 // Trim byte range prefix. 113 byteRangeString := strings.TrimPrefix(rangeString, byteRangePrefix) 114 115 // Check if range string contains delimiter '-', else return error. eg. "bytes=8" 116 sepIndex := strings.Index(byteRangeString, "-") 117 if sepIndex == -1 { 118 return nil, fmt.Errorf("'%s' does not have a valid range value", rangeString) 119 } 120 121 offsetBeginString := byteRangeString[:sepIndex] 122 offsetBegin := int64(-1) 123 // Convert offsetBeginString only if its not empty. 124 if len(offsetBeginString) > 0 { 125 if offsetBeginString[0] == '+' { 126 return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetBeginString) 127 } else if offsetBegin, err = strconv.ParseInt(offsetBeginString, 10, 64); err != nil { 128 return nil, fmt.Errorf("'%s' does not have a valid first byte position value", rangeString) 129 } else if offsetBegin < 0 { 130 return nil, fmt.Errorf("First byte position is negative ('%d')", offsetBegin) 131 } 132 } 133 134 offsetEndString := byteRangeString[sepIndex+1:] 135 offsetEnd := int64(-1) 136 // Convert offsetEndString only if its not empty. 137 if len(offsetEndString) > 0 { 138 if offsetEndString[0] == '+' { 139 return nil, fmt.Errorf("Byte position ('%s') must not have a sign", offsetEndString) 140 } else if offsetEnd, err = strconv.ParseInt(offsetEndString, 10, 64); err != nil { 141 return nil, fmt.Errorf("'%s' does not have a valid last byte position value", rangeString) 142 } else if offsetEnd < 0 { 143 return nil, fmt.Errorf("Last byte position is negative ('%d')", offsetEnd) 144 } 145 } 146 147 switch { 148 case offsetBegin > -1 && offsetEnd > -1: 149 if offsetBegin > offsetEnd { 150 return nil, errInvalidRange 151 } 152 return &HTTPRangeSpec{false, offsetBegin, offsetEnd}, nil 153 case offsetBegin > -1: 154 return &HTTPRangeSpec{false, offsetBegin, -1}, nil 155 case offsetEnd > -1: 156 if offsetEnd == 0 { 157 return nil, errInvalidRange 158 } 159 return &HTTPRangeSpec{true, -offsetEnd, -1}, nil 160 default: 161 // rangeString contains first and last byte positions missing. eg. "bytes=-" 162 return nil, fmt.Errorf("'%s' does not have valid range value", rangeString) 163 } 164 } 165 166 // String returns stringified representation of range for a particular resource size. 167 func (h *HTTPRangeSpec) String(resourceSize int64) string { 168 if h == nil { 169 return "" 170 } 171 off, length, err := h.GetOffsetLength(resourceSize) 172 if err != nil { 173 return "" 174 } 175 return fmt.Sprintf("%d-%d", off, off+length-1) 176 } 177 178 // ToHeader returns the Range header value. 179 func (h *HTTPRangeSpec) ToHeader() (string, error) { 180 if h == nil { 181 return "", nil 182 } 183 start := strconv.Itoa(int(h.Start)) 184 end := strconv.Itoa(int(h.End)) 185 switch { 186 case h.Start >= 0 && h.End >= 0: 187 if h.Start > h.End { 188 return "", errInvalidRange 189 } 190 case h.IsSuffixLength: 191 end = strconv.Itoa(int(h.Start * -1)) 192 start = "" 193 case h.Start > -1: 194 end = "" 195 default: 196 return "", fmt.Errorf("does not have valid range value") 197 } 198 return fmt.Sprintf("bytes=%s-%s", start, end), nil 199 }