headerlist.go

  1package http
  2
  3import (
  4	"fmt"
  5	"strconv"
  6	"strings"
  7	"unicode"
  8)
  9
 10func 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.
 27func SplitHeaderListValues(vs []string) ([]string, error) {
 28	return splitHeaderListValues(vs, quotedCommaSplit)
 29}
 30
 31func 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.
126func SplitHTTPDateTimestampHeaderListValues(vs []string) ([]string, error) {
127	return splitHeaderListValues(vs, splitHTTPDateHeaderValue)
128}
129
130func 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}