Skip to content

Commit

Permalink
net/http: add a package to parse and serialize Structured Field Values
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dunglas committed Aug 26, 2020
1 parent 758ac37 commit 24ff72f
Show file tree
Hide file tree
Showing 39 changed files with 2,925 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "src/net/http/sfv/structured-field-tests"]
path = src/net/http/sfv/structured-field-tests
url = https://github.com/httpwg/structured-field-tests
11 changes: 11 additions & 0 deletions src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,16 @@ var depsRules = `
< golang.org/x/net/idna
< golang.org/x/net/http/httpguts, golang.org/x/net/http/httpproxy;
encoding/base64,
errors,
fmt,
io,
math,
reflect,
strconv,
strings
< net/http/sfv;
NET, crypto/tls
< net/http/httptrace;
Expand All @@ -423,6 +433,7 @@ var depsRules = `
golang.org/x/net/http2/hpack,
net/http/internal,
net/http/httptrace,
net/http/sfv,
mime/multipart,
log
< net/http;
Expand Down
55 changes: 55 additions & 0 deletions src/net/http/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package http
import (
"io"
"net/http/httptrace"
"net/http/sfv"
"net/textproto"
"sort"
"strings"
Expand Down Expand Up @@ -37,6 +38,24 @@ func (h Header) Set(key, value string) {
textproto.MIMEHeader(h).Set(key, value)
}

// SetStructured sets the header entries associated with key to the
// given structured value.
// It encodes the structured value as text before setting it.
// It replaces any existing values associated with key.
// The key is case insensitive; it is canonicalized by
// textproto.CanonicalMIMEHeaderKey.
// To use non-canonical keys, assign to the map directly.
func (h Header) SetStructured(key string, value sfv.StructuredFieldValue) error {
v, err := sfv.Marshal(value)
if err != nil {
return err
}

h.Set(key, v)

return nil
}

// Get gets the first value associated with the given key. If
// there are no values associated with the key, Get returns "".
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
Expand All @@ -46,6 +65,42 @@ func (h Header) Get(key string) string {
return textproto.MIMEHeader(h).Get(key)
}

// GetItem returns the item
// (according to the Structured Field Values specification)
// associated with the headers having the given key.
// If the key doesn't exist or if the value format
// is not a valid item, an error is returned.
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To use non-canonical keys,
// access the map directly.
func (h Header) GetItem(key string) (sfv.Item, error) {
return sfv.UnmarshalItem(h.Values(key))
}

// GetList returns the list
// (according to the Structured Field Values specification)
// associated with the headers having the given key.
// If the key doesn't exist or if the value format
// is not a valid list, an error is returned.
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To use non-canonical keys,
// access the map directly.
func (h Header) GetList(key string) (sfv.List, error) {
return sfv.UnmarshalList(h.Values(key))
}

// GetDictionary returns the dictionary
// (according to the Structured Field Values specification)
// associated with the given key.
// If the key doesn't exist or if the value format
// is not a valid dictionary, an error is returned.
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To use non-canonical keys,
// access the map directly.
func (h Header) GetDictionary(key string) (*sfv.Dictionary, error) {
return sfv.UnmarshalDictionary(h.Values(key))
}

// Values returns all values associated with the given key.
// It is case insensitive; textproto.CanonicalMIMEHeaderKey is
// used to canonicalize the provided key. To use non-canonical
Expand Down
61 changes: 61 additions & 0 deletions src/net/http/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package http
import (
"bytes"
"internal/race"
"net/http/sfv"
"reflect"
"runtime"
"testing"
Expand Down Expand Up @@ -251,3 +252,63 @@ func TestCloneOrMakeHeader(t *testing.T) {
})
}
}

func TestSetStructured(t *testing.T) {
bar := sfv.NewItem(sfv.Token("bar"))
bar.Params.Add("baz", 42)

l := sfv.List{
bar,
sfv.NewItem(false),
}

d := sfv.NewDictionary()
d.Add("a", bar)
d.Add("b", sfv.NewItem(false))

tests := []struct {
key string
value sfv.StructuredFieldValue
err bool
serialized string
}{
{"Item", sfv.NewItem("bar"), false, `"bar"`},
{"List", l, false, `bar;baz=42, ?0`},
{"Dict", d, false, `a=bar;baz=42, b=?0`},
{"Dict", sfv.NewItem(999999999999999999), true, ""},
}

h := Header{}
for _, tt := range tests {
err := h.SetStructured(tt.key, tt.value) != nil
if err != tt.err {
t.Errorf("Got: %#v\nWant: %#v", err, tt.err)
}

if err {
continue
}

s := h.Get(tt.key)
if s != tt.serialized {
t.Errorf("Got: %#v\nWant: %#v", s, tt.serialized)
}

var r sfv.StructuredFieldValue
switch tt.value.(type) {
case sfv.Item:
r, _ = h.GetItem(tt.key)
case sfv.List:
r, _ = h.GetList(tt.key)
case *sfv.Dictionary:
r, _ = h.GetDictionary(tt.key)
default:
panic("type not found")
}

s, _ = sfv.Marshal(r)
if s != tt.serialized {
t.Errorf("Got: %#v\nWant: %#v", s, tt.serialized)
}
}
}
106 changes: 106 additions & 0 deletions src/net/http/sfv/bareitem.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sfv

import (
"errors"
"fmt"
"reflect"
"strings"
)

// ErrInvalidBareItem is returned when a bare item is invalid.
var ErrInvalidBareItem = errors.New(
"invalid bare item type (allowed types are bool, string, int64, float64, []byte and Token)",
)

// assertBareItem asserts that v is a valid bare item
// according to https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#item.
//
// v can be either:
//
// * an integer (Section 3.3.1.)
// * a decimal (Section 3.3.2.)
// * a string (Section 3.3.3.)
// * a token (Section 3.3.4.)
// * a byte sequence (Section 3.3.5.)
// * a boolean (Section 3.3.6.)
func assertBareItem(v interface{}) {
switch v.(type) {
case bool,
string,
int,
int8,
int16,
int32,
int64,
uint,
uint8,
uint16,
uint32,
uint64,
float32,
float64,
[]byte,
Token:
return
default:
panic(fmt.Errorf("%w: got %s", ErrInvalidBareItem, reflect.TypeOf(v)))
}
}

// marshalBareItem serializes as defined in
// https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#ser-bare-item.
func marshalBareItem(b *strings.Builder, v interface{}) error {
switch v := v.(type) {
case bool:
return marshalBoolean(b, v)
case string:
return marshalString(b, v)
case int64:
return marshalInteger(b, v)
case int, int8, int16, int32:
return marshalInteger(b, reflect.ValueOf(v).Int())
case uint, uint8, uint16, uint32, uint64:
// Casting an uint64 to an int64 is possible because the maximum allowed value is 999,999,999,999,999
return marshalInteger(b, int64(reflect.ValueOf(v).Uint()))
case float32, float64:
return marshalDecimal(b, v.(float64))
case []byte:
return marshalBinary(b, v)
case Token:
return v.marshalSFV(b)
default:
panic(ErrInvalidBareItem)
}
}

// parseBareItem parses as defined in
// https://httpwg.org/http-extensions/draft-ietf-httpbis-header-structure.html#parse-bare-item.
func parseBareItem(s *scanner) (interface{}, error) {
if s.eof() {
return nil, &UnmarshalError{s.off, ErrUnexpectedEndOfString}
}

c := s.data[s.off]
switch c {
case '"':
return parseString(s)
case '?':
return parseBoolean(s)
case '*':
return parseToken(s)
case ':':
return parseBinary(s)
case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
return parseNumber(s)
default:
if isAlpha(c) {
return parseToken(s)
}

return nil, &UnmarshalError{s.off, ErrUnrecognizedCharacter}
}
}
65 changes: 65 additions & 0 deletions src/net/http/sfv/bareitem_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package sfv

import (
"reflect"
"strings"
"testing"
"time"
)

func TestParseBareItem(t *testing.T) {
data := []struct {
in string
out interface{}
err bool
}{
{"?1", true, false},
{"?0", false, false},
{"22", int64(22), false},
{"-2.2", -2.2, false},
{`"foo"`, "foo", false},
{"abc", Token("abc"), false},
{"*abc", Token("*abc"), false},
{":YWJj:", []byte("abc"), false},
{"", nil, true},
{"~", nil, true},
}

for _, d := range data {
s := &scanner{data: d.in}

i, err := parseBareItem(s)
if d.err && err == nil {
t.Errorf("parseBareItem(%s): error expected", d.in)
}

if !d.err && !reflect.DeepEqual(d.out, i) {
t.Errorf("parseBareItem(%s) = %v, %v; %v, <nil> expected", d.in, i, err, d.out)
}
}
}

func TestMarshalBareItem(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()

var b strings.Builder
_ = marshalBareItem(&b, time.Second)
}

func TestAssertBareItem(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Errorf("The code did not panic")
}
}()

assertBareItem(time.Second)
}
Loading

0 comments on commit 24ff72f

Please sign in to comment.