cuelabs.dev/go/oci/ociregistry@v0.0.0-20240906074133-82eb438dd565/ociserver/range.go (about)

     1  // Copyright 2023 CUE Labs AG
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package ociserver
    16  
    17  import (
    18  	"errors"
    19  	"net/textproto"
    20  	"strconv"
    21  	"strings"
    22  )
    23  
    24  // This adapted from the code in fs.go in the net/http package.
    25  // The main difference is that it's more limited because we
    26  // don't have access to the size before parsing the range,
    27  // (because otherwise we'd have to make an extra round trip
    28  // to fetch the size before making the actual request).
    29  
    30  // httpRange specifies a byte range as requested by a client.
    31  // If end is negative, it represents the end of the file.
    32  type httpRange struct {
    33  	start, end int64
    34  }
    35  
    36  // parseRange parses a Range header string as per RFC 7233.
    37  func parseRange(s string) ([]httpRange, error) {
    38  	if s == "" {
    39  		return nil, nil // header not present
    40  	}
    41  	const b = "bytes="
    42  	if !strings.HasPrefix(s, b) {
    43  		return nil, errors.New("invalid range")
    44  	}
    45  	ranges := make([]httpRange, 0, 1)
    46  	for _, ra := range strings.Split(s[len(b):], ",") {
    47  		ra = textproto.TrimString(ra)
    48  		if ra == "" {
    49  			continue
    50  		}
    51  		start, end, ok := strings.Cut(ra, "-")
    52  		if !ok {
    53  			return nil, errors.New("invalid range")
    54  		}
    55  		start, end = textproto.TrimString(start), textproto.TrimString(end)
    56  		var r httpRange
    57  		if start == "" {
    58  			// If no start is specified, end specifies the
    59  			// range start relative to the end of the file,
    60  			// and we are dealing with <suffix-length>
    61  			// which has to be a non-negative integer as per
    62  			// RFC 7233 Section 2.1 "Byte-Ranges".
    63  			if end == "" || end[0] == '-' {
    64  				return nil, errors.New("invalid range")
    65  			}
    66  			return nil, errors.New("end-relative range not supported")
    67  		} else {
    68  			i, err := strconv.ParseInt(start, 10, 64)
    69  			if err != nil || i < 0 {
    70  				return nil, errors.New("invalid range")
    71  			}
    72  			r.start = i
    73  			if end == "" {
    74  				// If no end is specified, range extends to end of the file.
    75  				r.end = -1
    76  			} else {
    77  				i, err := strconv.ParseInt(end, 10, 64)
    78  				if err != nil || r.start > i {
    79  					return nil, errors.New("invalid range")
    80  				}
    81  				r.end = i + 1
    82  			}
    83  		}
    84  		ranges = append(ranges, r)
    85  	}
    86  	return ranges, nil
    87  }