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 <dneil@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Nicholas Husin <husin@google.com>
This commit is contained in:
Nicholas S. Husin
2025-09-29 11:14:12 -04:00
committed by Nicholas Husin
parent 7d8cfcee6c
commit f2e909b982
2 changed files with 242 additions and 15 deletions

View File

@@ -48,24 +48,56 @@ func countLeftWhitespace(s string) int {
return i
}
// TODO(nsh): Implement other consume functions that will be needed to fully
// deal with all possible HTTP SFV, specifically:
// - consumeDictionary(s string, f func(key, val, param string)) (consumed, rest string, ok bool)
// For example, given `a=123,b;a="a", i`, ConsumeDictionary will call f() 3 times
// with the following args:
// - key: `a`, val: `123`, param: ``
// - key: `b`, val: ``, param:`;a="a"`
// - key: `i`, val: ``, param: ``
//
// - consumeList(s string, f func(member, param string)) (consumed, rest string, ok bool)
// For example, given `123.456;i, ("foo" "bar"; lvl=2); lvl=1`, ConsumeList will
// call f() 2 times with the following args:
// - member: `123.456`, param: `i`
// - member: `("foo" "bar"; lvl=2)`, param: `; lvl=1`
// TODO(nsh): Implement corresponding parse functions for all consume functions
// that exists.
// ParseList is used to parse a string that represents a list in an
// HTTP Structured Field Values.
//
// Given a string that represents a list, it will call the given function using
// each of the members and parameters contained in the list. This allows the
// caller to extract information out of the list.
//
// This function will return once it encounters the end of the string, or
// something that is not a list. If it cannot consume the entire given
// string, the ok value returned will be false.
//
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-list.
func ParseList(s string, f func(member, param string)) (ok bool) {
for len(s) != 0 {
var member, param string
if len(s) != 0 && s[0] == '(' {
if member, s, ok = consumeBareInnerList(s, nil); !ok {
return ok
}
} else {
if member, s, ok = consumeBareItem(s); !ok {
return ok
}
}
if param, s, ok = consumeParameter(s, nil); !ok {
return ok
}
if f != nil {
f(member, param)
}
s = s[countLeftWhitespace(s):]
if len(s) == 0 {
break
}
if s[0] != ',' {
return false
}
s = s[1:]
s = s[countLeftWhitespace(s):]
if len(s) == 0 {
return false
}
}
return true
}
// consumeBareInnerList consumes an inner list
// (https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-an-inner-list),
// except for the inner list's top-most parameter.
@@ -151,6 +183,58 @@ func ParseItem(s string, f func(bareItem, param string)) (ok bool) {
return rest == "" && ok
}
// ParseDictionary is used to parse a string that represents a dictionary in an
// HTTP Structured Field Values.
//
// Given a string that represents a dictionary, it will call the given function
// using each of the keys, values, and parameters contained in the dictionary.
// This allows the caller to extract information out of the dictionary.
//
// This function will return once it encounters the end of the string, or
// something that is not a dictionary. If it cannot consume the entire given
// string, the ok value returned will be false.
//
// https://www.rfc-editor.org/rfc/rfc9651.html#name-parsing-a-dictionary.
func ParseDictionary(s string, f func(key, val, param string)) (ok bool) {
for len(s) != 0 {
var key, val, param string
val = "?1" // Default value for empty val is boolean true.
if key, s, ok = consumeKey(s); !ok {
return ok
}
if len(s) != 0 && s[0] == '=' {
s = s[1:]
if len(s) != 0 && s[0] == '(' {
if val, s, ok = consumeBareInnerList(s, nil); !ok {
return ok
}
} else {
if val, s, ok = consumeBareItem(s); !ok {
return ok
}
}
}
if param, s, ok = consumeParameter(s, nil); !ok {
return ok
}
if f != nil {
f(key, val, param)
}
s = s[countLeftWhitespace(s):]
if len(s) == 0 {
break
}
if s[0] == ',' {
s = s[1:]
}
s = s[countLeftWhitespace(s):]
if len(s) == 0 {
return false
}
}
return true
}
// https://www.rfc-editor.org/rfc/rfc9651.html#parse-param.
func consumeParameter(s string, f func(key, val string)) (consumed, rest string, ok bool) {
rest = s