Skip to content

Commit 7d8cfce

Browse files
committed
internal/httpsfv: add support for Bare Inner List and Item type.
This change implements the consume and Parse functions for both the Item and Bare Inner List type. This is part of a chain of changes that is needed in order for us to fully support HTTP Structured Field Values parsing as defined in RFC 9651. In future changes, we will utilize the support for Bare Inner List and Item that is added here to support more complex types, namely Dictionary and List. Note that Bare Inner List is something we define on our own. We define a Bare Inner List as an Inner List without the top-most parameter meant for the Inner List. For example, the Inner List `(a;b c;d);e` would translate to the Bare Inner List `(a;b c;d)`. We have done this because the parameter of an Inner List will be exposed to the user via ParseDictionary() or ParseList() too. By implementing Bare Inner List, we can avoid having two ways of accessing the Inner List parameter, and incurring the cost of a more complex implementation for Inner List and other types that utilize Inner List (e.g. if we have consumeInnerList, ParseDictionary will have to use consumeInnerList and backtrack the consumption to separate out the InnerList parameter). For go/golang#75500 Change-Id: I9b418d10b5755195d1cc3ff5f7ea211423bc4b48 Reviewed-on: https://go-review.googlesource.com/c/net/+/707099 Reviewed-by: Damien Neil <[email protected]> Reviewed-by: Nicholas Husin <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent fe9bcbc commit 7d8cfce

File tree

2 files changed

+318
-20
lines changed

2 files changed

+318
-20
lines changed

internal/httpsfv/httpsfv.go

Lines changed: 87 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,21 +62,95 @@ func countLeftWhitespace(s string) int {
6262
// call f() 2 times with the following args:
6363
// - member: `123.456`, param: `i`
6464
// - member: `("foo" "bar"; lvl=2)`, param: `; lvl=1`
65-
//
66-
// - consumeItem(s string, f func(bareItem, param string)) (consumed, rest string, ok bool)
67-
// For example, given `"foo"; bar=baz;foo=bar`, ConsumeItem will call f() with
68-
// the following args:
69-
// - bareItem: `"foo"`, param: `; bar=baz;foo=bar`
70-
//
71-
// - consumeInnerList(s string f func(bareItem, param, listParam string)) (consumed, rest string, ok bool)
72-
// For example, given `("foo"; a=1;b=2 "bar";baz;lvl=2);lvl=1`, ConsumeInnerList
73-
// will call f() 2 times with the following args:
74-
// - bareItem: `"foo"`, param: `; a=1;b=2`, listParam: `;lvl=1`
75-
// - bareItem: `"bar"`, param: `;baz;lvl=2`, listParam: `;lvl=1`
7665

7766
// TODO(nsh): Implement corresponding parse functions for all consume functions
7867
// that exists.
7968

69+
// consumeBareInnerList consumes an inner list
70+
// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list),
71+
// except for the inner list's top-most parameter.
72+
// For example, given `(a;b c;d);e`, it will consume only `(a;b c;d)`
73+
func consumeBareInnerList(s string, f func(bareItem, param string)) (consumed, rest string, ok bool) {
74+
if len(s) == 0 || s[0] != '(' {
75+
return "", s, false
76+
}
77+
rest = s[1:]
78+
for len(rest) != 0 {
79+
var bareItem, param string
80+
rest = rest[countLeftWhitespace(rest):]
81+
if len(rest) != 0 && rest[0] == ')' {
82+
rest = rest[1:]
83+
break
84+
}
85+
if bareItem, rest, ok = consumeBareItem(rest); !ok {
86+
return "", s, ok
87+
}
88+
if param, rest, ok = consumeParameter(rest, nil); !ok {
89+
return "", s, ok
90+
}
91+
if len(rest) == 0 || (rest[0] != ')' && !isSP(rest[0])) {
92+
return "", s, false
93+
}
94+
if f != nil {
95+
f(bareItem, param)
96+
}
97+
}
98+
return s[:len(s)-len(rest)], rest, true
99+
}
100+
101+
// ParseBareInnerList is used to parse a string that represents a bare inner
102+
// list in an HTTP Structured Field Values.
103+
//
104+
// We define a bare inner list as an inner list
105+
// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list),
106+
// without the top-most parameter of the inner list. For example, given the
107+
// inner list `(a;b c;d);e`, the bare inner list would be `(a;b c;d)`.
108+
//
109+
// Given a string that represents a bare inner list, it will call the given
110+
// function using each of the bare item and parameter within the bare inner
111+
// list. This allows the caller to extract information out of the bare inner
112+
// list.
113+
//
114+
// This function will return once it encounters the end of the bare inner list,
115+
// or something that is not a bare inner list. If it cannot consume the entire
116+
// given string, the ok value returned will be false.
117+
func ParseBareInnerList(s string, f func(bareItem, param string)) (ok bool) {
118+
_, rest, ok := consumeBareInnerList(s, f)
119+
return rest == "" && ok
120+
}
121+
122+
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item.
123+
func consumeItem(s string, f func(bareItem, param string)) (consumed, rest string, ok bool) {
124+
var bareItem, param string
125+
if bareItem, rest, ok = consumeBareItem(s); !ok {
126+
return "", s, ok
127+
}
128+
if param, rest, ok = consumeParameter(rest, nil); !ok {
129+
return "", s, ok
130+
}
131+
if f != nil {
132+
f(bareItem, param)
133+
}
134+
return s[:len(s)-len(rest)], rest, true
135+
}
136+
137+
// ParseItem is used to parse a string that represents an item in an HTTP
138+
// Structured Field Values.
139+
//
140+
// Given a string that represents an item, it will call the given function
141+
// once, with the bare item and the parameter of the item. This allows the
142+
// caller to extract information out of the parameter.
143+
//
144+
// This function will return once it encounters the end of the string, or
145+
// something that is not an item. If it cannot consume the entire given
146+
// string, the ok value returned will be false.
147+
//
148+
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-item.
149+
func ParseItem(s string, f func(bareItem, param string)) (ok bool) {
150+
_, rest, ok := consumeItem(s, f)
151+
return rest == "" && ok
152+
}
153+
80154
// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param.
81155
func consumeParameter(s string, f func(key, val string)) (consumed, rest string, ok bool) {
82156
rest = s
@@ -87,9 +161,7 @@ func consumeParameter(s string, f func(key, val string)) (consumed, rest string,
87161
break
88162
}
89163
rest = rest[1:]
90-
if i := countLeftWhitespace(rest); i > 0 {
91-
rest = rest[i:]
92-
}
164+
rest = rest[countLeftWhitespace(rest):]
93165
key, rest, ok = consumeKey(rest)
94166
if !ok {
95167
return "", s, ok
@@ -122,10 +194,7 @@ func consumeParameter(s string, f func(key, val string)) (consumed, rest string,
122194
// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param.
123195
func ParseParameter(s string, f func(key, val string)) (ok bool) {
124196
_, rest, ok := consumeParameter(s, f)
125-
if rest != "" {
126-
return false
127-
}
128-
return ok
197+
return rest == "" && ok
129198
}
130199

131200
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-key.
@@ -196,7 +265,6 @@ func consumeString(s string) (consumed, rest string, ok bool) {
196265
if len(s) == 0 || s[0] != '"' {
197266
return "", s, false
198267
}
199-
200268
for i := 1; i < len(s); i++ {
201269
switch ch := s[i]; ch {
202270
case '\\':
@@ -215,7 +283,6 @@ func consumeString(s string) (consumed, rest string, ok bool) {
215283
}
216284
}
217285
}
218-
219286
return "", s, false
220287
}
221288

internal/httpsfv/httpsfv_test.go

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,242 @@
44
package httpsfv
55

66
import (
7+
"slices"
78
"strconv"
89
"strings"
910
"testing"
1011
)
1112

13+
func TestConsumeBareInnerList(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
in string
17+
wantBareItems []string
18+
wantParams []string
19+
wantListParam string
20+
wantOk bool
21+
}{
22+
{
23+
name: "valid inner list without param",
24+
in: `(a b c)`,
25+
wantBareItems: []string{"a", "b", "c"},
26+
wantParams: []string{"", "", ""},
27+
wantOk: true,
28+
},
29+
{
30+
name: "valid inner list with param",
31+
in: `(a;d b c;e)`,
32+
wantBareItems: []string{"a", "b", "c"},
33+
wantParams: []string{";d", "", ";e"},
34+
wantOk: true,
35+
},
36+
{
37+
name: "valid inner list with fake ending parenthesis",
38+
in: `(")";foo=")")`,
39+
wantBareItems: []string{`")"`},
40+
wantParams: []string{`;foo=")"`},
41+
wantOk: true,
42+
},
43+
{
44+
name: "valid inner list with list parameter",
45+
in: `(a b;c); d`,
46+
wantBareItems: []string{"a", "b"},
47+
wantParams: []string{"", ";c"},
48+
wantOk: true,
49+
},
50+
{
51+
name: "valid inner list with more content after",
52+
in: `(a b;c); d, a`,
53+
wantBareItems: []string{"a", "b"},
54+
wantParams: []string{"", ";c"},
55+
wantOk: true,
56+
},
57+
{
58+
name: "invalid inner list",
59+
in: `(a b;c `,
60+
wantBareItems: []string{"a", "b"},
61+
wantParams: []string{"", ";c"},
62+
},
63+
}
64+
65+
for _, tc := range tests {
66+
var gotBareItems, gotParams []string
67+
f := func(bareItem, param string) {
68+
gotBareItems = append(gotBareItems, bareItem)
69+
gotParams = append(gotParams, param)
70+
}
71+
gotConsumed, gotRest, ok := consumeBareInnerList(tc.in, f)
72+
if ok != tc.wantOk {
73+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
74+
}
75+
if !slices.Equal(tc.wantBareItems, gotBareItems) {
76+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotBareItems, tc.wantBareItems)
77+
}
78+
if !slices.Equal(tc.wantParams, gotParams) {
79+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParams, tc.wantParams)
80+
}
81+
if gotConsumed+gotRest != tc.in {
82+
t.Fatalf("test %q: %#v + %#v != %#v", tc.name, gotConsumed, gotRest, tc.in)
83+
}
84+
}
85+
}
86+
87+
func TestParseBareInnerList(t *testing.T) {
88+
tests := []struct {
89+
name string
90+
in string
91+
wantBareItems []string
92+
wantParams []string
93+
wantOk bool
94+
}{
95+
{
96+
name: "valid inner list",
97+
in: `(a b;c)`,
98+
wantBareItems: []string{"a", "b"},
99+
wantParams: []string{"", ";c"},
100+
wantOk: true,
101+
},
102+
{
103+
name: "valid inner list with list parameter",
104+
in: `(a b;c); d`,
105+
wantBareItems: []string{"a", "b"},
106+
wantParams: []string{"", ";c"},
107+
},
108+
{
109+
name: "invalid inner list",
110+
in: `(a b;c `,
111+
wantBareItems: []string{"a", "b"},
112+
wantParams: []string{"", ";c"},
113+
},
114+
}
115+
116+
for _, tc := range tests {
117+
var gotBareItems, gotParams []string
118+
f := func(bareItem, param string) {
119+
gotBareItems = append(gotBareItems, bareItem)
120+
gotParams = append(gotParams, param)
121+
}
122+
ok := ParseBareInnerList(tc.in, f)
123+
if ok != tc.wantOk {
124+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
125+
}
126+
if !slices.Equal(tc.wantBareItems, gotBareItems) {
127+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotBareItems, tc.wantBareItems)
128+
}
129+
if !slices.Equal(tc.wantParams, gotParams) {
130+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParams, tc.wantParams)
131+
}
132+
}
133+
}
134+
135+
func TestConsumeItem(t *testing.T) {
136+
tests := []struct {
137+
name string
138+
in string
139+
wantBareItem string
140+
wantParam string
141+
wantOk bool
142+
}{
143+
{
144+
name: "valid bare item",
145+
in: `fookey`,
146+
wantBareItem: `fookey`,
147+
wantOk: true,
148+
},
149+
{
150+
name: "valid bare item and param",
151+
in: `fookey; a="123"`,
152+
wantBareItem: `fookey`,
153+
wantParam: `; a="123"`,
154+
wantOk: true,
155+
},
156+
{
157+
name: "valid item with content after",
158+
in: `fookey; a="123", otheritem; otherparam=1`,
159+
wantBareItem: `fookey`,
160+
wantParam: `; a="123"`,
161+
wantOk: true,
162+
},
163+
{
164+
name: "invalid just param",
165+
in: `;a="123"`,
166+
},
167+
}
168+
169+
for _, tc := range tests {
170+
var gotBareItem, gotParam string
171+
f := func(bareItem, param string) {
172+
gotBareItem = bareItem
173+
gotParam = param
174+
}
175+
gotConsumed, gotRest, ok := consumeItem(tc.in, f)
176+
if ok != tc.wantOk {
177+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
178+
}
179+
if tc.wantBareItem != gotBareItem {
180+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotBareItem, tc.wantBareItem)
181+
}
182+
if tc.wantParam != gotParam {
183+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParam, tc.wantParam)
184+
}
185+
if gotConsumed+gotRest != tc.in {
186+
t.Fatalf("test %q: %#v + %#v != %#v", tc.name, gotConsumed, gotRest, tc.in)
187+
}
188+
}
189+
}
190+
191+
func TestParseItem(t *testing.T) {
192+
tests := []struct {
193+
name string
194+
in string
195+
wantBareItem string
196+
wantParam string
197+
wantOk bool
198+
}{
199+
{
200+
name: "valid bare item",
201+
in: `fookey`,
202+
wantBareItem: `fookey`,
203+
wantOk: true,
204+
},
205+
{
206+
name: "valid bare item and param",
207+
in: `fookey; a="123"`,
208+
wantBareItem: `fookey`,
209+
wantParam: `; a="123"`,
210+
wantOk: true,
211+
},
212+
{
213+
name: "valid item with content after",
214+
in: `fookey; a="123", otheritem; otherparam=1`,
215+
wantBareItem: `fookey`,
216+
wantParam: `; a="123"`,
217+
},
218+
{
219+
name: "invalid just param",
220+
in: `;a="123"`,
221+
},
222+
}
223+
224+
for _, tc := range tests {
225+
var gotBareItem, gotParam string
226+
f := func(bareItem, param string) {
227+
gotBareItem = bareItem
228+
gotParam = param
229+
}
230+
ok := ParseItem(tc.in, f)
231+
if ok != tc.wantOk {
232+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
233+
}
234+
if tc.wantBareItem != gotBareItem {
235+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotBareItem, tc.wantBareItem)
236+
}
237+
if tc.wantParam != gotParam {
238+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParam, tc.wantParam)
239+
}
240+
}
241+
}
242+
12243
func TestConsumeParameter(t *testing.T) {
13244
tests := []struct {
14245
name string

0 commit comments

Comments
 (0)