headerlist.go raw

   1  package http
   2  
   3  import (
   4  	"fmt"
   5  	"strconv"
   6  	"strings"
   7  	"unicode"
   8  )
   9  
  10  func splitHeaderListValues(vs []string, splitFn func(string) ([]string, error)) ([]string, error) {
  11  	values := make([]string, 0, len(vs))
  12  
  13  	for i := 0; i < len(vs); i++ {
  14  		parts, err := splitFn(vs[i])
  15  		if err != nil {
  16  			return nil, err
  17  		}
  18  		values = append(values, parts...)
  19  	}
  20  
  21  	return values, nil
  22  }
  23  
  24  // SplitHeaderListValues attempts to split the elements of the slice by commas,
  25  // and return a list of all values separated. Returns error if unable to
  26  // separate the values.
  27  func SplitHeaderListValues(vs []string) ([]string, error) {
  28  	return splitHeaderListValues(vs, quotedCommaSplit)
  29  }
  30  
  31  func quotedCommaSplit(v string) (parts []string, err error) {
  32  	v = strings.TrimSpace(v)
  33  
  34  	expectMore := true
  35  	for i := 0; i < len(v); i++ {
  36  		if unicode.IsSpace(rune(v[i])) {
  37  			continue
  38  		}
  39  		expectMore = false
  40  
  41  		// leading  space in part is ignored.
  42  		// Start of value must be non-space, or quote.
  43  		//
  44  		// - If quote, enter quoted mode, find next non-escaped quote to
  45  		//   terminate the value.
  46  		// - Otherwise, find next comma to terminate value.
  47  
  48  		remaining := v[i:]
  49  
  50  		var value string
  51  		var valueLen int
  52  		if remaining[0] == '"' {
  53  			//------------------------------
  54  			// Quoted value
  55  			//------------------------------
  56  			var j int
  57  			var skipQuote bool
  58  			for j += 1; j < len(remaining); j++ {
  59  				if remaining[j] == '\\' || (remaining[j] != '\\' && skipQuote) {
  60  					skipQuote = !skipQuote
  61  					continue
  62  				}
  63  				if remaining[j] == '"' {
  64  					break
  65  				}
  66  			}
  67  			if j == len(remaining) || j == 1 {
  68  				return nil, fmt.Errorf("value %v missing closing double quote",
  69  					remaining)
  70  			}
  71  			valueLen = j + 1
  72  
  73  			tail := remaining[valueLen:]
  74  			var k int
  75  			for ; k < len(tail); k++ {
  76  				if !unicode.IsSpace(rune(tail[k])) && tail[k] != ',' {
  77  					return nil, fmt.Errorf("value %v has non-space trailing characters",
  78  						remaining)
  79  				}
  80  				if tail[k] == ',' {
  81  					expectMore = true
  82  					break
  83  				}
  84  			}
  85  			value = remaining[:valueLen]
  86  			value, err = strconv.Unquote(value)
  87  			if err != nil {
  88  				return nil, fmt.Errorf("failed to unquote value %v, %w", value, err)
  89  			}
  90  
  91  			// Pad valueLen to include trailing space(s) so `i` is updated correctly.
  92  			valueLen += k
  93  
  94  		} else {
  95  			//------------------------------
  96  			// Unquoted value
  97  			//------------------------------
  98  
  99  			// Index of the next comma is the length of the value, or end of string.
 100  			valueLen = strings.Index(remaining, ",")
 101  			if valueLen != -1 {
 102  				expectMore = true
 103  			} else {
 104  				valueLen = len(remaining)
 105  			}
 106  			value = strings.TrimSpace(remaining[:valueLen])
 107  		}
 108  
 109  		i += valueLen
 110  		parts = append(parts, value)
 111  
 112  	}
 113  
 114  	if expectMore {
 115  		parts = append(parts, "")
 116  	}
 117  
 118  	return parts, nil
 119  }
 120  
 121  // SplitHTTPDateTimestampHeaderListValues attempts to split the HTTP-Date
 122  // timestamp values in the slice by commas, and return a list of all values
 123  // separated. The split is aware of the HTTP-Date timestamp format, and will skip
 124  // comma within the timestamp value. Returns an error if unable to split the
 125  // timestamp values.
 126  func SplitHTTPDateTimestampHeaderListValues(vs []string) ([]string, error) {
 127  	return splitHeaderListValues(vs, splitHTTPDateHeaderValue)
 128  }
 129  
 130  func splitHTTPDateHeaderValue(v string) ([]string, error) {
 131  	if n := strings.Count(v, ","); n <= 1 {
 132  		// Nothing to do if only contains a no, or single HTTPDate value
 133  		return []string{v}, nil
 134  	} else if n%2 == 0 {
 135  		return nil, fmt.Errorf("invalid timestamp HTTPDate header comma separations, %q", v)
 136  	}
 137  
 138  	var parts []string
 139  	var i, j int
 140  
 141  	var doSplit bool
 142  	for ; i < len(v); i++ {
 143  		if v[i] == ',' {
 144  			if doSplit {
 145  				doSplit = false
 146  				parts = append(parts, strings.TrimSpace(v[j:i]))
 147  				j = i + 1
 148  			} else {
 149  				// Skip the first comma in the timestamp value since that
 150  				// separates the day from the rest of the timestamp.
 151  				//
 152  				// Tue, 17 Dec 2019 23:48:18 GMT
 153  				doSplit = true
 154  			}
 155  		}
 156  	}
 157  	// Add final part
 158  	if j < len(v) {
 159  		parts = append(parts, strings.TrimSpace(v[j:]))
 160  	}
 161  
 162  	return parts, nil
 163  }
 164