Skip to content

Commit f2e909b

Browse files
committed
internal/httpsfv: implement parsing support for Dictionary and List type.
This change implements the Parse functions for the Dictionary and List type. At this point, we should be able to use internal/httpsfv package to extract information from any HTTP SFV that follows RFC 8941. In future changes, we will add additional types introduced in RFC 9651 to achieve feature parity with it. Additionally, we will add Parse functions for all the HTTP SFV types, such that users of the package do not need to do their own type assertions and conversions. Note that the Dictionary and List type do not have a consume function. This is because both types never appear as a child of other types, meaning it is guaranteed to always consume its entire string input. For go/golang#75500 Change-Id: I376dca274d920a4bea276ebb4d49a9cd768c79fe Reviewed-on: https://go-review.googlesource.com/c/net/+/707100 Reviewed-by: Damien Neil <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Nicholas Husin <[email protected]>
1 parent 7d8cfce commit f2e909b

File tree

2 files changed

+242
-15
lines changed

2 files changed

+242
-15
lines changed

internal/httpsfv/httpsfv.go

Lines changed: 99 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -48,24 +48,56 @@ func countLeftWhitespace(s string) int {
4848
return i
4949
}
5050

51-
// TODO(nsh): Implement other consume functions that will be needed to fully
52-
// deal with all possible HTTP SFV, specifically:
53-
// - consumeDictionary(s string, f func(key, val, param string)) (consumed, rest string, ok bool)
54-
// For example, given `a=123,b;a="a", i`, ConsumeDictionary will call f() 3 times
55-
// with the following args:
56-
// - key: `a`, val: `123`, param: ``
57-
// - key: `b`, val: ``, param:`;a="a"`
58-
// - key: `i`, val: ``, param: ``
59-
//
60-
// - consumeList(s string, f func(member, param string)) (consumed, rest string, ok bool)
61-
// For example, given `123.456;i, ("foo" "bar"; lvl=2); lvl=1`, ConsumeList will
62-
// call f() 2 times with the following args:
63-
// - member: `123.456`, param: `i`
64-
// - member: `("foo" "bar"; lvl=2)`, param: `; lvl=1`
65-
6651
// TODO(nsh): Implement corresponding parse functions for all consume functions
6752
// that exists.
6853

54+
// ParseList is used to parse a string that represents a list in an
55+
// HTTP Structured Field Values.
56+
//
57+
// Given a string that represents a list, it will call the given function using
58+
// each of the members and parameters contained in the list. This allows the
59+
// caller to extract information out of the list.
60+
//
61+
// This function will return once it encounters the end of the string, or
62+
// something that is not a list. If it cannot consume the entire given
63+
// string, the ok value returned will be false.
64+
//
65+
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-list.
66+
func ParseList(s string, f func(member, param string)) (ok bool) {
67+
for len(s) != 0 {
68+
var member, param string
69+
if len(s) != 0 && s[0] == '(' {
70+
if member, s, ok = consumeBareInnerList(s, nil); !ok {
71+
return ok
72+
}
73+
} else {
74+
if member, s, ok = consumeBareItem(s); !ok {
75+
return ok
76+
}
77+
}
78+
if param, s, ok = consumeParameter(s, nil); !ok {
79+
return ok
80+
}
81+
if f != nil {
82+
f(member, param)
83+
}
84+
85+
s = s[countLeftWhitespace(s):]
86+
if len(s) == 0 {
87+
break
88+
}
89+
if s[0] != ',' {
90+
return false
91+
}
92+
s = s[1:]
93+
s = s[countLeftWhitespace(s):]
94+
if len(s) == 0 {
95+
return false
96+
}
97+
}
98+
return true
99+
}
100+
69101
// consumeBareInnerList consumes an inner list
70102
// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list),
71103
// except for the inner list's top-most parameter.
@@ -151,6 +183,58 @@ func ParseItem(s string, f func(bareItem, param string)) (ok bool) {
151183
return rest == "" && ok
152184
}
153185

186+
// ParseDictionary is used to parse a string that represents a dictionary in an
187+
// HTTP Structured Field Values.
188+
//
189+
// Given a string that represents a dictionary, it will call the given function
190+
// using each of the keys, values, and parameters contained in the dictionary.
191+
// This allows the caller to extract information out of the dictionary.
192+
//
193+
// This function will return once it encounters the end of the string, or
194+
// something that is not a dictionary. If it cannot consume the entire given
195+
// string, the ok value returned will be false.
196+
//
197+
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-dictionary.
198+
func ParseDictionary(s string, f func(key, val, param string)) (ok bool) {
199+
for len(s) != 0 {
200+
var key, val, param string
201+
val = "?1" // Default value for empty val is boolean true.
202+
if key, s, ok = consumeKey(s); !ok {
203+
return ok
204+
}
205+
if len(s) != 0 && s[0] == '=' {
206+
s = s[1:]
207+
if len(s) != 0 && s[0] == '(' {
208+
if val, s, ok = consumeBareInnerList(s, nil); !ok {
209+
return ok
210+
}
211+
} else {
212+
if val, s, ok = consumeBareItem(s); !ok {
213+
return ok
214+
}
215+
}
216+
}
217+
if param, s, ok = consumeParameter(s, nil); !ok {
218+
return ok
219+
}
220+
if f != nil {
221+
f(key, val, param)
222+
}
223+
s = s[countLeftWhitespace(s):]
224+
if len(s) == 0 {
225+
break
226+
}
227+
if s[0] == ',' {
228+
s = s[1:]
229+
}
230+
s = s[countLeftWhitespace(s):]
231+
if len(s) == 0 {
232+
return false
233+
}
234+
}
235+
return true
236+
}
237+
154238
// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param.
155239
func consumeParameter(s string, f func(key, val string)) (consumed, rest string, ok bool) {
156240
rest = s

internal/httpsfv/httpsfv_test.go

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,73 @@ import (
1010
"testing"
1111
)
1212

13+
func TestParseList(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
in string
17+
wantMembers []string
18+
wantParams []string
19+
wantOk bool
20+
}{
21+
{
22+
name: "valid list",
23+
in: `a, b,c`,
24+
wantMembers: []string{"a", "b", "c"},
25+
wantParams: []string{"", "", ""},
26+
wantOk: true,
27+
},
28+
{
29+
name: "valid list with params",
30+
in: `a;foo=bar, b,c; baz=baz`,
31+
wantMembers: []string{"a", "b", "c"},
32+
wantParams: []string{";foo=bar", "", "; baz=baz"},
33+
wantOk: true,
34+
},
35+
{
36+
name: "valid list with fake commas",
37+
in: `a;foo=",", (",")`,
38+
wantMembers: []string{"a", `(",")`},
39+
wantParams: []string{`;foo=","`, ""},
40+
wantOk: true,
41+
},
42+
{
43+
name: "valid list with inner list member",
44+
in: `(a b c); foo, bar;baz`,
45+
wantMembers: []string{"(a b c)", "bar"},
46+
wantParams: []string{"; foo", ";baz"},
47+
wantOk: true,
48+
},
49+
{
50+
name: "invalid list with trailing comma",
51+
in: `a;foo=bar, b,c; baz=baz,`,
52+
wantMembers: []string{"a", "b", "c"},
53+
wantParams: []string{";foo=bar", "", "; baz=baz"},
54+
},
55+
{
56+
name: "invalid list with unclosed string",
57+
in: `", b, c,d`,
58+
},
59+
}
60+
61+
for _, tc := range tests {
62+
var gotMembers, gotParams []string
63+
f := func(member, param string) {
64+
gotMembers = append(gotMembers, member)
65+
gotParams = append(gotParams, param)
66+
}
67+
ok := ParseList(tc.in, f)
68+
if ok != tc.wantOk {
69+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
70+
}
71+
if !slices.Equal(tc.wantMembers, gotMembers) {
72+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotMembers, tc.wantMembers)
73+
}
74+
if !slices.Equal(tc.wantParams, gotParams) {
75+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParams, tc.wantParams)
76+
}
77+
}
78+
}
79+
1380
func TestConsumeBareInnerList(t *testing.T) {
1481
tests := []struct {
1582
name string
@@ -240,6 +307,82 @@ func TestParseItem(t *testing.T) {
240307
}
241308
}
242309

310+
func TestParseDictionary(t *testing.T) {
311+
tests := []struct {
312+
name string
313+
in string
314+
wantVal string
315+
wantParam string
316+
wantOk bool
317+
}{
318+
{
319+
name: "valid dictionary with simple value",
320+
in: `a=b, want=foo, c=d`,
321+
wantVal: "foo",
322+
wantOk: true,
323+
},
324+
{
325+
name: "valid dictionary with implicit value",
326+
in: `a, want, c=d`,
327+
wantVal: "?1",
328+
wantOk: true,
329+
},
330+
{
331+
name: "valid dictionary with parameter",
332+
in: `a, want=foo;bar=baz, c=d`,
333+
wantVal: "foo",
334+
wantParam: ";bar=baz",
335+
wantOk: true,
336+
},
337+
{
338+
name: "valid dictionary with inner list",
339+
in: `a, want=(a b c d;e;f);g=h, c=d`,
340+
wantVal: "(a b c d;e;f)",
341+
wantParam: ";g=h",
342+
wantOk: true,
343+
},
344+
{
345+
name: "valid dictionary with fake commas",
346+
in: `a=(";");b=";",want=foo;bar`,
347+
wantVal: "foo",
348+
wantParam: ";bar",
349+
wantOk: true,
350+
},
351+
{
352+
name: "invalid dictionary with bad key",
353+
in: `UPPERCASEKEY=BAD, want=foo, c=d`,
354+
},
355+
{
356+
name: "invalid dictionary with trailing comma",
357+
in: `trailing=comma,`,
358+
},
359+
{
360+
name: "invalid dictionary with unclosed string",
361+
in: `a=""",want=foo;bar`,
362+
},
363+
}
364+
365+
for _, tc := range tests {
366+
var gotVal, gotParam string
367+
f := func(key, val, param string) {
368+
if key == "want" {
369+
gotVal = val
370+
gotParam = param
371+
}
372+
}
373+
ok := ParseDictionary(tc.in, f)
374+
if ok != tc.wantOk {
375+
t.Fatalf("test %q: want ok to be %v, got: %v", tc.name, tc.wantOk, ok)
376+
}
377+
if tc.wantVal != gotVal {
378+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotVal, tc.wantVal)
379+
}
380+
if tc.wantParam != gotParam {
381+
t.Fatalf("test %q: mismatch.\n got: %#v\nwant: %#v\n", tc.name, gotParam, tc.wantParam)
382+
}
383+
}
384+
}
385+
243386
func TestConsumeParameter(t *testing.T) {
244387
tests := []struct {
245388
name string

0 commit comments

Comments
 (0)