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}