Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

net/http: add a package to parse and serialize Structured Field Values #41045

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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