Skip to content

Commit 24ff72f

Browse files
committed
net/http: add a package to parse and serialize Structured Field Values
Structured Field Values fields value for HTTP is an upcoming RFC defining data types for headers and trailers. This new package implements the specification. New methods are also added to the Header type to manipulate structured values.
1 parent 758ac37 commit 24ff72f

39 files changed

+2925
-0
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "src/net/http/sfv/structured-field-tests"]
2+
path = src/net/http/sfv/structured-field-tests
3+
url = https://github.com/httpwg/structured-field-tests

src/go/build/deps_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,16 @@ var depsRules = `
414414
< golang.org/x/net/idna
415415
< golang.org/x/net/http/httpguts, golang.org/x/net/http/httpproxy;
416416
417+
encoding/base64,
418+
errors,
419+
fmt,
420+
io,
421+
math,
422+
reflect,
423+
strconv,
424+
strings
425+
< net/http/sfv;
426+
417427
NET, crypto/tls
418428
< net/http/httptrace;
419429
@@ -423,6 +433,7 @@ var depsRules = `
423433
golang.org/x/net/http2/hpack,
424434
net/http/internal,
425435
net/http/httptrace,
436+
net/http/sfv,
426437
mime/multipart,
427438
log
428439
< net/http;

src/net/http/header.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77
import (
88
"io"
99
"net/http/httptrace"
10+
"net/http/sfv"
1011
"net/textproto"
1112
"sort"
1213
"strings"
@@ -37,6 +38,24 @@ func (h Header) Set(key, value string) {
3738
textproto.MIMEHeader(h).Set(key, value)
3839
}
3940

41+
// SetStructured sets the header entries associated with key to the
42+
// given structured value.
43+
// It encodes the structured value as text before setting it.
44+
// It replaces any existing values associated with key.
45+
// The key is case insensitive; it is canonicalized by
46+
// textproto.CanonicalMIMEHeaderKey.
47+
// To use non-canonical keys, assign to the map directly.
48+
func (h Header) SetStructured(key string, value sfv.StructuredFieldValue) error {
49+
v, err := sfv.Marshal(value)
50+
if err != nil {
51+
return err
52+
}
53+
54+
h.Set(key, v)
55+
56+
return nil
57+
}
58+
4059
// Get gets the first value associated with the given key. If
4160
// there are no values associated with the key, Get returns "".
4261
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
@@ -46,6 +65,42 @@ func (h Header) Get(key string) string {
4665
return textproto.MIMEHeader(h).Get(key)
4766
}
4867

68+
// GetItem returns the item
69+
// (according to the Structured Field Values specification)
70+
// associated with the headers having the given key.
71+
// If the key doesn't exist or if the value format
72+
// is not a valid item, an error is returned.
73+
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
74+
// used to canonicalize the provided key. To use non-canonical keys,
75+
// access the map directly.
76+
func (h Header) GetItem(key string) (sfv.Item, error) {
77+
return sfv.UnmarshalItem(h.Values(key))
78+
}
79+
80+
// GetList returns the list
81+
// (according to the Structured Field Values specification)
82+
// associated with the headers having the given key.
83+
// If the key doesn't exist or if the value format
84+
// is not a valid list, an error is returned.
85+
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
86+
// used to canonicalize the provided key. To use non-canonical keys,
87+
// access the map directly.
88+
func (h Header) GetList(key string) (sfv.List, error) {
89+
return sfv.UnmarshalList(h.Values(key))
90+
}
91+
92+
// GetDictionary returns the dictionary
93+
// (according to the Structured Field Values specification)
94+
// associated with the given key.
95+
// If the key doesn't exist or if the value format
96+
// is not a valid dictionary, an error is returned.
97+
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
98+
// used to canonicalize the provided key. To use non-canonical keys,
99+
// access the map directly.
100+
func (h Header) GetDictionary(key string) (*sfv.Dictionary, error) {
101+
return sfv.UnmarshalDictionary(h.Values(key))
102+
}
103+
49104
// Values returns all values associated with the given key.
50105
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
51106
// used to canonicalize the provided key. To use non-canonical

src/net/http/header_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package http
77
import (
88
"bytes"
99
"internal/race"
10+
"net/http/sfv"
1011
"reflect"
1112
"runtime"
1213
"testing"
@@ -251,3 +252,63 @@ func TestCloneOrMakeHeader(t *testing.T) {
251252
})
252253
}
253254
}
255+
256+
func TestSetStructured(t *testing.T) {
257+
bar := sfv.NewItem(sfv.Token("bar"))
258+
bar.Params.Add("baz", 42)
259+
260+
l := sfv.List{
261+
bar,
262+
sfv.NewItem(false),
263+
}
264+
265+
d := sfv.NewDictionary()
266+
d.Add("a", bar)
267+
d.Add("b", sfv.NewItem(false))
268+
269+
tests := []struct {
270+
key string
271+
value sfv.StructuredFieldValue
272+
err bool
273+
serialized string
274+
}{
275+
{"Item", sfv.NewItem("bar"), false, `"bar"`},
276+
{"List", l, false, `bar;baz=42, ?0`},
277+
{"Dict", d, false, `a=bar;baz=42, b=?0`},
278+
{"Dict", sfv.NewItem(999999999999999999), true, ""},
279+
}
280+
281+
h := Header{}
282+
for _, tt := range tests {
283+
err := h.SetStructured(tt.key, tt.value) != nil
284+
if err != tt.err {
285+
t.Errorf("Got: %#v\nWant: %#v", err, tt.err)
286+
}
287+
288+
if err {
289+
continue
290+
}
291+
292+
s := h.Get(tt.key)
293+
if s != tt.serialized {
294+
t.Errorf("Got: %#v\nWant: %#v", s, tt.serialized)
295+
}
296+
297+
var r sfv.StructuredFieldValue
298+
switch tt.value.(type) {
299+
case sfv.Item:
300+
r, _ = h.GetItem(tt.key)
301+
case sfv.List:
302+
r, _ = h.GetList(tt.key)
303+
case *sfv.Dictionary:
304+
r, _ = h.GetDictionary(tt.key)
305+
default:
306+
panic("type not found")
307+
}
308+
309+
s, _ = sfv.Marshal(r)
310+
if s != tt.serialized {
311+
t.Errorf("Got: %#v\nWant: %#v", s, tt.serialized)
312+
}
313+
}
314+
}

src/net/http/sfv/bareitem.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package sfv
6+
7+
import (
8+
"errors"
9+
"fmt"
10+
"reflect"
11+
"strings"
12+
)
13+
14+
// ErrInvalidBareItem is returned when a bare item is invalid.
15+
var ErrInvalidBareItem = errors.New(
16+
"invalid bare item type (allowed types are bool, string, int64, float64, []byte and Token)",
17+
)
18+
19+
// assertBareItem asserts that v is a valid bare item
20+
// according to https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#item.
21+
//
22+
// v can be either:
23+
//
24+
// * an integer (Section 3.3.1.)
25+
// * a decimal (Section 3.3.2.)
26+
// * a string (Section 3.3.3.)
27+
// * a token (Section 3.3.4.)
28+
// * a byte sequence (Section 3.3.5.)
29+
// * a boolean (Section 3.3.6.)
30+
func assertBareItem(v interface{}) {
31+
switch v.(type) {
32+
case bool,
33+
string,
34+
int,
35+
int8,
36+
int16,
37+
int32,
38+
int64,
39+
uint,
40+
uint8,
41+
uint16,
42+
uint32,
43+
uint64,
44+
float32,
45+
float64,
46+
[]byte,
47+
Token:
48+
return
49+
default:
50+
panic(fmt.Errorf("%w: got %s", ErrInvalidBareItem, reflect.TypeOf(v)))
51+
}
52+
}
53+
54+
// marshalBareItem serializes as defined in
55+
// https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#ser-bare-item.
56+
func marshalBareItem(b *strings.Builder, v interface{}) error {
57+
switch v := v.(type) {
58+
case bool:
59+
return marshalBoolean(b, v)
60+
case string:
61+
return marshalString(b, v)
62+
case int64:
63+
return marshalInteger(b, v)
64+
case int, int8, int16, int32:
65+
return marshalInteger(b, reflect.ValueOf(v).Int())
66+
case uint, uint8, uint16, uint32, uint64:
67+
// Casting an uint64 to an int64 is possible because the maximum allowed value is 999,999,999,999,999
68+
return marshalInteger(b, int64(reflect.ValueOf(v).Uint()))
69+
case float32, float64:
70+
return marshalDecimal(b, v.(float64))
71+
case []byte:
72+
return marshalBinary(b, v)
73+
case Token:
74+
return v.marshalSFV(b)
75+
default:
76+
panic(ErrInvalidBareItem)
77+
}
78+
}
79+
80+
// parseBareItem parses as defined in
81+
// https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#parse-bare-item.
82+
func parseBareItem(s *scanner) (interface{}, error) {
83+
if s.eof() {
84+
return nil, &UnmarshalError{s.off, ErrUnexpectedEndOfString}
85+
}
86+
87+
c := s.data[s.off]
88+
switch c {
89+
case '"':
90+
return parseString(s)
91+
case '?':
92+
return parseBoolean(s)
93+
case '*':
94+
return parseToken(s)
95+
case ':':
96+
return parseBinary(s)
97+
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
98+
return parseNumber(s)
99+
default:
100+
if isAlpha(c) {
101+
return parseToken(s)
102+
}
103+
104+
return nil, &UnmarshalError{s.off, ErrUnrecognizedCharacter}
105+
}
106+
}

src/net/http/sfv/bareitem_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2020 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package sfv
6+
7+
import (
8+
"reflect"
9+
"strings"
10+
"testing"
11+
"time"
12+
)
13+
14+
func TestParseBareItem(t *testing.T) {
15+
data := []struct {
16+
in string
17+
out interface{}
18+
err bool
19+
}{
20+
{"?1", true, false},
21+
{"?0", false, false},
22+
{"22", int64(22), false},
23+
{"-2.2", -2.2, false},
24+
{`"foo"`, "foo", false},
25+
{"abc", Token("abc"), false},
26+
{"*abc", Token("*abc"), false},
27+
{":YWJj:", []byte("abc"), false},
28+
{"", nil, true},
29+
{"~", nil, true},
30+
}
31+
32+
for _, d := range data {
33+
s := &scanner{data: d.in}
34+
35+
i, err := parseBareItem(s)
36+
if d.err && err == nil {
37+
t.Errorf("parseBareItem(%s): error expected", d.in)
38+
}
39+
40+
if !d.err && !reflect.DeepEqual(d.out, i) {
41+
t.Errorf("parseBareItem(%s) = %v, %v; %v, <nil> expected", d.in, i, err, d.out)
42+
}
43+
}
44+
}
45+
46+
func TestMarshalBareItem(t *testing.T) {
47+
defer func() {
48+
if r := recover(); r == nil {
49+
t.Errorf("The code did not panic")
50+
}
51+
}()
52+
53+
var b strings.Builder
54+
_ = marshalBareItem(&b, time.Second)
55+
}
56+
57+
func TestAssertBareItem(t *testing.T) {
58+
defer func() {
59+
if r := recover(); r == nil {
60+
t.Errorf("The code did not panic")
61+
}
62+
}()
63+
64+
assertBareItem(time.Second)
65+
}

0 commit comments

Comments
 (0)