From 5d3e413a78421467b30088e11f5ea7956c2f8d54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Thu, 20 Feb 2025 17:45:18 +0100 Subject: [PATCH 1/2] feat: support for RFC 9651 --- README.md | 2 +- bareitem.go | 23 +++++++-- bareitem_test.go | 1 + binary.go | 4 +- binary_test.go | 2 +- boolean.go | 4 +- boolean_test.go | 2 +- date.go | 41 ++++++++++++++++ date_test.go | 63 +++++++++++++++++++++++++ decimal.go | 2 +- decimal_test.go | 2 +- dictionary.go | 4 +- dictionary_test.go | 2 +- displaystring.go | 105 +++++++++++++++++++++++++++++++++++++++++ displaystring_test.go | 69 +++++++++++++++++++++++++++ encode.go | 6 +-- encode_test.go | 12 +++-- httpwg_test.go | 79 +++++++++++++++++-------------- innerlist.go | 6 +-- integer.go | 8 ++-- integer_test.go | 2 +- item.go | 6 +-- item_test.go | 2 +- key.go | 6 +-- key_test.go | 2 +- list.go | 10 ++-- list_test.go | 2 +- member.go | 2 +- params.go | 6 +-- params_test.go | 2 +- string.go | 9 ++-- string_test.go | 6 +-- structured-field-tests | 2 +- token.go | 6 +-- token_test.go | 2 +- 35 files changed, 403 insertions(+), 99 deletions(-) create mode 100644 date.go create mode 100644 date_test.go create mode 100644 displaystring.go create mode 100644 displaystring_test.go diff --git a/README.md b/README.md index 46b47f0..4e7325e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # httpsfv: Structured Field Values for HTTP in Go -This [Go (golang)](https://golang.org) library implements parsing and serialization for [Structured Field Values for HTTP (RFC 8941)](https://httpwg.org/specs/rfc8941.html). +This [Go (golang)](https://golang.org) library implements parsing and serialization for [Structured Field Values for HTTP (RFC 9651 and 8941)](https://httpwg.org/specs/rfc9651.html). [![PkgGoDev](https://pkg.go.dev/badge/github.com/dunglas/httpsfv)](https://pkg.go.dev/github.com/dunglas/httpsfv) ![CI](https://github.com/dunglas/httpsfv/workflows/CI/badge.svg) diff --git a/bareitem.go b/bareitem.go index 6900928..17b2c0b 100644 --- a/bareitem.go +++ b/bareitem.go @@ -5,15 +5,16 @@ import ( "fmt" "reflect" "strings" + "time" ) // 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)", + "invalid bare item type (allowed types are bool, string, int64, float64, []byte, time.Time and Token)", ) // assertBareItem asserts that v is a valid bare item -// according to https://httpwg.org/specs/rfc8941.html#item. +// according to https://httpwg.org/specs/rfc9651.html#item. // // v can be either: // @@ -23,6 +24,8 @@ var ErrInvalidBareItem = errors.New( // * a token (Section 3.3.4.) // * a byte sequence (Section 3.3.5.) // * a boolean (Section 3.3.6.) +// * a date (Section 3.3.7.) +// * a display string (Section 3.3.8.) func assertBareItem(v interface{}) { switch v.(type) { case bool, @@ -40,7 +43,9 @@ func assertBareItem(v interface{}) { float32, float64, []byte, - Token: + time.Time, + Token, + DisplayString: return default: panic(fmt.Errorf("%w: got %s", ErrInvalidBareItem, reflect.TypeOf(v))) @@ -48,7 +53,7 @@ func assertBareItem(v interface{}) { } // marshalBareItem serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-bare-item. +// https://httpwg.org/specs/rfc9651.html#ser-bare-item. func marshalBareItem(b *strings.Builder, v interface{}) error { switch v := v.(type) { case bool: @@ -66,15 +71,19 @@ func marshalBareItem(b *strings.Builder, v interface{}) error { return marshalDecimal(b, v.(float64)) case []byte: return marshalBinary(b, v) + case time.Time: + return marshalDate(b, v) case Token: return v.marshalSFV(b) + case DisplayString: + return v.marshalSFV(b) default: panic(ErrInvalidBareItem) } } // parseBareItem parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-bare-item. +// https://httpwg.org/specs/rfc9651.html#parse-bare-item. func parseBareItem(s *scanner) (interface{}, error) { if s.eof() { return nil, &UnmarshalError{s.off, ErrUnexpectedEndOfString} @@ -92,6 +101,10 @@ func parseBareItem(s *scanner) (interface{}, error) { return parseBinary(s) case '-', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': return parseNumber(s) + case '@': + return parseDate(s) + case '%': + return parseDisplayString(s) default: if isAlpha(c) { return parseToken(s) diff --git a/bareitem_test.go b/bareitem_test.go index 9a270b2..c289474 100644 --- a/bareitem_test.go +++ b/bareitem_test.go @@ -23,6 +23,7 @@ func TestParseBareItem(t *testing.T) { {"abc", Token("abc"), false}, {"*abc", Token("*abc"), false}, {":YWJj:", []byte("abc"), false}, + {"@1659578233", time.Unix(1659578233, 0), false}, {"", nil, true}, {"~", nil, true}, } diff --git a/binary.go b/binary.go index 5ca1276..8aaf017 100644 --- a/binary.go +++ b/binary.go @@ -10,7 +10,7 @@ import ( var ErrInvalidBinaryFormat = errors.New("invalid binary format") // marshalBinary serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-binary. +// https://httpwg.org/specs/rfc9651.html#ser-binary. func marshalBinary(b *strings.Builder, bs []byte) error { if err := b.WriteByte(':'); err != nil { return err @@ -27,7 +27,7 @@ func marshalBinary(b *strings.Builder, bs []byte) error { } // parseBinary parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-binary. +// https://httpwg.org/specs/rfc9651.html#parse-binary. func parseBinary(s *scanner) ([]byte, error) { if s.eof() || s.data[s.off] != ':' { return nil, &UnmarshalError{s.off, ErrInvalidBinaryFormat} diff --git a/binary_test.go b/binary_test.go index 4cce97d..ca7f4a9 100644 --- a/binary_test.go +++ b/binary_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestBinary(t *testing.T) { +func TestMarshalBinary(t *testing.T) { t.Parallel() var bd strings.Builder diff --git a/boolean.go b/boolean.go index 2dd1cca..557e759 100644 --- a/boolean.go +++ b/boolean.go @@ -9,7 +9,7 @@ import ( var ErrInvalidBooleanFormat = errors.New("invalid boolean format") // marshalBoolean serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-boolean. +// https://httpwg.org/specs/rfc9651.html#ser-boolean. func marshalBoolean(bd io.StringWriter, b bool) error { if b { _, err := bd.WriteString("?1") @@ -23,7 +23,7 @@ func marshalBoolean(bd io.StringWriter, b bool) error { } // parseBoolean parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-boolean. +// https://httpwg.org/specs/rfc9651.html#parse-boolean. func parseBoolean(s *scanner) (bool, error) { if s.eof() || s.data[s.off] != '?' { return false, &UnmarshalError{s.off, ErrInvalidBooleanFormat} diff --git a/boolean_test.go b/boolean_test.go index 5d7cfc7..20722ac 100644 --- a/boolean_test.go +++ b/boolean_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestBooleanMarshalSFV(t *testing.T) { +func TestMarshalBoolean(t *testing.T) { t.Parallel() var b strings.Builder diff --git a/date.go b/date.go new file mode 100644 index 0000000..75e66be --- /dev/null +++ b/date.go @@ -0,0 +1,41 @@ +package httpsfv + +import ( + "errors" + "io" + "time" +) + +var ErrInvalidDateFormat = errors.New("invalid date format") + +// marshalDate serializes as defined in +// https://httpwg.org/specs/rfc9651.html#ser-date. +func marshalDate(b io.StringWriter, i time.Time) error { + _, err := b.WriteString("@") + if err != nil { + return err + } + + return marshalInteger(b, i.Unix()) +} + +// parseDate parses as defined in +// https://httpwg.org/specs/rfc9651.html#parse-date. +func parseDate(s *scanner) (time.Time, error) { + if s.eof() || s.data[s.off] != '@' { + return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat} + } + s.off++ + + n, err := parseNumber(s) + if err != nil { + return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat} + } + + i, ok := n.(int64) + if !ok { + return time.Time{}, &UnmarshalError{s.off, ErrInvalidDateFormat} + } + + return time.Unix(i, 0), nil +} diff --git a/date_test.go b/date_test.go new file mode 100644 index 0000000..3b3ca14 --- /dev/null +++ b/date_test.go @@ -0,0 +1,63 @@ +package httpsfv + +import ( + "strings" + "testing" + "time" +) + +func TestMarshalDate(t *testing.T) { + t.Parallel() + + data := []struct { + in time.Time + expected string + valid bool + }{ + {time.Unix(1659578233, 0), "@1659578233", true}, + {time.Unix(9999999999999999, 0), "@", false}, + } + + var b strings.Builder + + for _, d := range data { + b.Reset() + + err := marshalDate(&b, d.in) + if d.valid && err != nil { + t.Errorf("error not expected for %v, got %v", d.in, err) + } else if !d.valid && err == nil { + t.Errorf("error expected for %v, got %v", d.in, err) + } + + if b.String() != d.expected { + t.Errorf("got %v; want %v", b.String(), d.expected) + } + } +} + +func TestParseDate(t *testing.T) { + t.Parallel() + + data := []struct { + in string + out time.Time + err bool + }{ + {"@1659578233", time.Unix(1659578233, 0), false}, + {"invalid", time.Time{}, true}, + } + + for _, d := range data { + s := &scanner{data: d.in} + + i, err := parseDate(s) + if d.err && err == nil { + t.Errorf("parse%s): error expected", d.in) + } + + if !d.err && d.out != i { + t.Errorf("parse%s) = %v, %v; %v, expected", d.in, i, err, d.out) + } + } +} diff --git a/decimal.go b/decimal.go index 33f5157..1a1659d 100644 --- a/decimal.go +++ b/decimal.go @@ -14,7 +14,7 @@ const maxDecDigit = 3 var ErrInvalidDecimal = errors.New("the integer portion is larger than 12 digits: invalid decimal") // marshalDecimal serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-decimal. +// https://httpwg.org/specs/rfc9651.html#ser-decimal. // // TODO(dunglas): add support for decimal float type when one will be available // (https://github.com/golang/go/issues/19787) diff --git a/decimal_test.go b/decimal_test.go index 69c02df..c079c6d 100644 --- a/decimal_test.go +++ b/decimal_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestDecimalMarshalSFV(t *testing.T) { +func TestMarshalDecimal(t *testing.T) { t.Parallel() data := []struct { diff --git a/dictionary.go b/dictionary.go index 80d12dd..7e73bcd 100644 --- a/dictionary.go +++ b/dictionary.go @@ -6,7 +6,7 @@ import ( ) // Dictionary is an ordered map of name-value pairs. -// See https://httpwg.org/specs/rfc8941.html#dictionary +// See https://httpwg.org/specs/rfc9651.html#dictionary // Values can be: // * Item (Section 3.3.) // * Inner List (Section 3.1.1.) @@ -101,7 +101,7 @@ func (d *Dictionary) marshalSFV(b *strings.Builder) error { } // UnmarshalDictionary parses a dictionary as defined in -// https://httpwg.org/specs/rfc8941.html#parse-dictionary. +// https://httpwg.org/specs/rfc9651.html#parse-dictionary. func UnmarshalDictionary(v []string) (*Dictionary, error) { s := &scanner{ data: strings.Join(v, ","), diff --git a/dictionary_test.go b/dictionary_test.go index eb08394..f06d6c4 100644 --- a/dictionary_test.go +++ b/dictionary_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestDictionnary(t *testing.T) { +func TestMarshalDictionnary(t *testing.T) { t.Parallel() dict := NewDictionary() diff --git a/displaystring.go b/displaystring.go new file mode 100644 index 0000000..a01d6f5 --- /dev/null +++ b/displaystring.go @@ -0,0 +1,105 @@ +package httpsfv + +import ( + "encoding/hex" + "errors" + "strings" + "unicode" + "unicode/utf8" +) + +type DisplayString string + +var ErrInvalidDisplayString = errors.New("invalid display string type") + +var notVcharOrSp = &unicode.RangeTable{ + R16: []unicode.Range16{ + {0x0000, 0x001f, 1}, + {0x007f, 0x00ff, 1}, + }, + LatinOffset: 2, +} + +// marshalSFV serializes as defined in +// https://httpwg.org/specs/rfc9651.html#ser-string. +func (s DisplayString) marshalSFV(b *strings.Builder) error { + if _, err := b.WriteString(`%"`); err != nil { + return err + } + + for i := 0; i < len(s); i++ { + if s[i] == '%' || s[i] == '"' || unicode.Is(notVcharOrSp, rune(s[i])) { + b.WriteRune('%') + b.WriteString(hex.EncodeToString([]byte{s[i]})) + + continue + } + + b.WriteByte(s[i]) + } + + b.WriteByte('"') + + return nil +} + +// parseDisplayString parses as defined in +// https://httpwg.org/specs/rfc9651.html#parse-display. +func parseDisplayString(s *scanner) (DisplayString, error) { + if s.eof() || len(s.data[s.off:]) < 2 || s.data[s.off:2] != `%"` { + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} + } + s.off += 2 + + var b strings.Builder + for !s.eof() { + c := s.data[s.off] + s.off++ + + switch c { + case '%': + if len(s.data[s.off:]) < 2 { + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} + } + c0 := unhex(s.data[s.off]) + if c0 == 0 { + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} + } + + c1 := unhex(s.data[s.off+1]) + if c1 == 0 { + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} + } + + b.WriteByte(c0<<4 | c1) + s.off += 2 + case '"': + r := b.String() + if !utf8.ValidString(r) { + return "", ErrInvalidDisplayString + } + + return DisplayString(r), nil + + default: + if unicode.Is(notVcharOrSp, rune(c)) { + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} + } + + b.WriteByte(c) + } + } + + return "", &UnmarshalError{s.off, ErrInvalidDisplayString} +} + +func unhex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + default: + return 0 + } +} diff --git a/displaystring_test.go b/displaystring_test.go new file mode 100644 index 0000000..11fe66e --- /dev/null +++ b/displaystring_test.go @@ -0,0 +1,69 @@ +package httpsfv + +import ( + "fmt" + "strings" + "testing" +) + +func TestMarshalDisplayString(t *testing.T) { + t.Parallel() + + data := []struct { + in string + expected string + valid bool + }{ + {"foo", `%"foo"`, true}, + {"Kévin", `%"K%c3%a9vin"`, true}, + } + + var b strings.Builder + + for _, d := range data { + b.Reset() + + err := DisplayString(d.in).marshalSFV(&b) + if d.valid && err != nil { + t.Errorf("error not expected for %v, got %v", d.in, err) + } else if !d.valid && err == nil { + t.Errorf("error expected for %v, got %v", d.in, err) + } + + if b.String() != d.expected { + t.Errorf("got %v; want %v", b.String(), d.expected) + } + } +} + +func TestParseDisplayString(t *testing.T) { + t.Parallel() + + data := []struct { + in string + out string + err bool + }{ + {`%"foo"`, "foo", false}, + {`%"K%c3%a9vin"`, "Kévin", false}, + {`%"K%00vin"`, "", true}, + {`"K%e9vin"`, "", true}, + {`%K%e9vin"`, "", true}, + {`%"K%e9vin`, "", true}, + } + + for _, d := range data { + s := &scanner{data: d.in} + + i, err := parseDisplayString(s) + if d.err && err == nil { + t.Errorf("parse(%s): error expected", d.in) + } + + if !d.err && d.out != string(i) { + fmt.Printf("%q\n", i) + fmt.Printf("%q\n", d.out) + t.Errorf("parse(%s) = %v, %v; %v, expected", d.in, i, err, d.out) + } + } +} diff --git a/encode.go b/encode.go index 29d2979..3004b58 100644 --- a/encode.go +++ b/encode.go @@ -1,5 +1,5 @@ // Package httpsfv implements serializing and parsing -// of Structured Field Values for HTTP as defined in RFC 8941. +// of Structured Field Values for HTTP as defined in RFC 9651. // // Structured Field Values are either lists, dictionaries or items. Dedicated types are provided for all of them. // Dedicated types are also used for tokens, parameters and inner lists. @@ -11,7 +11,7 @@ // byte[], for byte sequences // bool, for booleans // -// The specification is available at https://httpwg.org/specs/rfc8941.html. +// The specification is available at https://httpwg.org/specs/rfc9651.html. package httpsfv import ( @@ -29,7 +29,7 @@ type StructuredFieldValue interface { } // Marshal returns the HTTP Structured Value serialization of v -// as defined in https://httpwg.org/specs/rfc8941.html#text-serialize. +// as defined in https://httpwg.org/specs/rfc9651.html#text-serialize. // // v must be a List, a Dictionary, an Item or an InnerList. func Marshal(v StructuredFieldValue) (string, error) { diff --git a/encode_test.go b/encode_test.go index 1b3972e..2fd34ae 100644 --- a/encode_test.go +++ b/encode_test.go @@ -1,6 +1,9 @@ package httpsfv -import "testing" +import ( + "testing" + "time" +) func TestMarshal(t *testing.T) { t.Parallel() @@ -16,8 +19,11 @@ func TestMarshal(t *testing.T) { tok.Params.Add("a", "b") d.Add("tok", tok) - if res, _ := Marshal(d); res != `i=22.1;foo;bar=baz, tok=foo;a="b"` { - t.Errorf("marshal: bad result") + date := NewItem(time.Date(1988, 21, 01, 0, 0, 0, 0, time.Local)) + d.Add("d", date) + + if res, _ := Marshal(d); res != `i=22.1;foo;bar=baz, tok=foo;a="b", d=@620604000` { + t.Errorf("marshal: bad result: %q", res) } } diff --git a/httpwg_test.go b/httpwg_test.go index 0a37854..0b9f242 100644 --- a/httpwg_test.go +++ b/httpwg_test.go @@ -7,6 +7,7 @@ import ( "reflect" "strings" "testing" + "time" ) const ( @@ -52,6 +53,12 @@ func valToBareItem(e interface{}) interface{} { return bi case "token": return Token(bareItem["value"].(string)) + case "date": + u, _ := bareItem["value"].(json.Number).Int64() + + return time.Unix(u, 0) + case "displaystring": + return DisplayString(bareItem["value"].(string)) default: } @@ -135,8 +142,6 @@ func valToDictionary(e interface{}) *Dictionary { } func TestOfficialTestSuiteParsing(t *testing.T) { - t.Parallel() - const dir = "structured-field-tests/" f, _ := os.Open(dir) files, _ := f.Readdir(-1) @@ -155,40 +160,42 @@ func TestOfficialTestSuiteParsing(t *testing.T) { _ = dec.Decode(&tests) for _, te := range tests { - var ( - expected, got StructuredFieldValue - err error - ) - - switch te.HeaderType { - case ITEM: - expected = valToItem(te.Expected) - got, err = UnmarshalItem(te.Raw) - case LIST: - expected = valToList(te.Expected) - got, err = UnmarshalList(te.Raw) - case DICTIONARY: - expected = valToDictionary(te.Expected) - got, err = UnmarshalDictionary(te.Raw) - default: - panic("unknown header type") - } - - if te.MustFail && err == nil { - t.Errorf("%s: %s: must fail", n, te.Name) - - continue - } - - if (!te.MustFail && !te.CanFail) && err != nil { - t.Errorf("%s: %s: must not fail, got error %s", n, te.Name, err) - - continue - } - - if err == nil && !reflect.DeepEqual(expected, got) { - t.Errorf("%s: %s: %#v expected, got %#v", n, te.Name, expected, got) - } + t.Run(n+"/"+te.Name, func(t *testing.T) { + var ( + expected, got StructuredFieldValue + err error + ) + + switch te.HeaderType { + case ITEM: + expected = valToItem(te.Expected) + got, err = UnmarshalItem(te.Raw) + case LIST: + expected = valToList(te.Expected) + got, err = UnmarshalList(te.Raw) + case DICTIONARY: + expected = valToDictionary(te.Expected) + got, err = UnmarshalDictionary(te.Raw) + default: + panic("unknown header type") + } + + if te.MustFail && err == nil { + t.Errorf("%s: %s: must fail", n, te.Name) + + return + } + + if (!te.MustFail && !te.CanFail) && err != nil { + t.Errorf("%s: %s: must not fail, got error %s", n, te.Name, err) + + return + } + + if err == nil && !reflect.DeepEqual(expected, got) { + t.Errorf("%s: %s: %#v expected, got %#v", n, te.Name, expected, got) + } + }) } } } diff --git a/innerlist.go b/innerlist.go index 531ae06..97fba93 100644 --- a/innerlist.go +++ b/innerlist.go @@ -9,7 +9,7 @@ import ( var ErrInvalidInnerListFormat = errors.New("invalid inner list format") // InnerList represents an inner list as defined in -// https://httpwg.org/specs/rfc8941.html#inner-list. +// https://httpwg.org/specs/rfc9651.html#inner-list. type InnerList struct { Items []Item Params *Params @@ -19,7 +19,7 @@ func (il InnerList) member() { } // marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-innerlist. +// https://httpwg.org/specs/rfc9651.html#ser-innerlist. func (il InnerList) marshalSFV(b *strings.Builder) error { if err := b.WriteByte('('); err != nil { return err @@ -46,7 +46,7 @@ func (il InnerList) marshalSFV(b *strings.Builder) error { } // parseInnerList parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-item-or-list. +// https://httpwg.org/specs/rfc9651.html#parse-item-or-list. func parseInnerList(s *scanner) (InnerList, error) { if s.eof() || s.data[s.off] != '(' { return InnerList{}, &UnmarshalError{s.off, ErrInvalidInnerListFormat} diff --git a/integer.go b/integer.go index 55033df..c544bb7 100644 --- a/integer.go +++ b/integer.go @@ -22,8 +22,8 @@ const ( typeDecimal ) -// marshalInteger serialized as defined in -// https://httpwg.org/specs/rfc8941.html#integer. +// marshalInteger serializes as defined in +// https://httpwg.org/specs/rfc9651.html#integer. func marshalInteger(b io.StringWriter, i int64) error { if i < -999999999999999 || i > 999999999999999 { return ErrNumberOutOfRange @@ -35,7 +35,7 @@ func marshalInteger(b io.StringWriter, i int64) error { } // parseNumber parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-number. +// https://httpwg.org/specs/rfc9651.html#parse-number. func parseNumber(s *scanner) (interface{}, error) { neg := isNeg(s) if neg && s.eof() { @@ -51,7 +51,7 @@ func parseNumber(s *scanner) (interface{}, error) { var ( decSepOff int - t int = typeInteger + t = typeInteger ) for s.off < len(s.data) { diff --git a/integer_test.go b/integer_test.go index 751935f..099e2b9 100644 --- a/integer_test.go +++ b/integer_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestIntegerMarshalSFV(t *testing.T) { +func TestMarshalInteger(t *testing.T) { t.Parallel() data := []struct { diff --git a/item.go b/item.go index 1577c2a..000c999 100644 --- a/item.go +++ b/item.go @@ -5,7 +5,7 @@ import ( ) // Item is a bare value and associated parameters. -// See https://httpwg.org/specs/rfc8941.html#item. +// See https://httpwg.org/specs/rfc9651.html#item. type Item struct { Value interface{} Params *Params @@ -22,7 +22,7 @@ func (i Item) member() { } // marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-item. +// https://httpwg.org/specs/rfc9651.html#ser-item. func (i Item) marshalSFV(b *strings.Builder) error { if i.Value == nil { return ErrInvalidBareItem @@ -36,7 +36,7 @@ func (i Item) marshalSFV(b *strings.Builder) error { } // UnmarshalItem parses an item as defined in -// https://httpwg.org/specs/rfc8941.html#parse-item. +// https://httpwg.org/specs/rfc9651.html#parse-item. func UnmarshalItem(v []string) (Item, error) { s := &scanner{ data: strings.Join(v, ","), diff --git a/item_test.go b/item_test.go index 244188d..588e5a2 100644 --- a/item_test.go +++ b/item_test.go @@ -50,7 +50,7 @@ func TestMarshalItem(t *testing.T) { } } -func TestItemParamsMarshalSFV(t *testing.T) { +func TestParseItemParamsMarshalSFV(t *testing.T) { t.Parallel() i := NewItem(Token("bar")) diff --git a/key.go b/key.go index e347003..cbb4e9f 100644 --- a/key.go +++ b/key.go @@ -24,7 +24,7 @@ func isKeyChar(c byte) bool { } // checkKey checks if the given value is a valid parameter key according to -// https://httpwg.org/specs/rfc8941.html#param. +// https://httpwg.org/specs/rfc9651.html#param. func checkKey(k string) error { if len(k) == 0 { return fmt.Errorf("a key cannot be empty: %w", ErrInvalidKeyFormat) @@ -44,7 +44,7 @@ func checkKey(k string) error { } // marshalKey serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-key. +// https://httpwg.org/specs/rfc9651.html#ser-key. func marshalKey(b io.StringWriter, k string) error { if err := checkKey(k); err != nil { return err @@ -56,7 +56,7 @@ func marshalKey(b io.StringWriter, k string) error { } // parseKey parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-key. +// https://httpwg.org/specs/rfc9651.html#parse-key. func parseKey(s *scanner) (string, error) { if s.eof() { return "", &UnmarshalError{s.off, ErrInvalidKeyFormat} diff --git a/key_test.go b/key_test.go index 3a2eb42..8896aae 100644 --- a/key_test.go +++ b/key_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestKeyMarshalSFV(t *testing.T) { +func TestMarshalKey(t *testing.T) { t.Parallel() data := []struct { diff --git a/list.go b/list.go index fc1d32b..7c20b0d 100644 --- a/list.go +++ b/list.go @@ -10,11 +10,11 @@ var ErrInvalidListFormat = errors.New("invalid list format") // List contains items an inner lists. // -// See https://httpwg.org/specs/rfc8941.html#list +// See https://httpwg.org/specs/rfc9651.html#list type List []Member // marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-list. +// https://httpwg.org/specs/rfc9651.html#ser-list. func (l List) marshalSFV(b *strings.Builder) error { s := len(l) for i := 0; i < s; i++ { @@ -33,7 +33,7 @@ func (l List) marshalSFV(b *strings.Builder) error { } // UnmarshalList parses a list as defined in -// https://httpwg.org/specs/rfc8941.html#parse-list. +// https://httpwg.org/specs/rfc9651.html#parse-list. func UnmarshalList(v []string) (List, error) { s := &scanner{ data: strings.Join(v, ","), @@ -50,7 +50,7 @@ func UnmarshalList(v []string) (List, error) { } // parseList parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-list. +// https://httpwg.org/specs/rfc9651.html#parse-list. func parseList(s *scanner) (List, error) { var l List @@ -85,7 +85,7 @@ func parseList(s *scanner) (List, error) { } // parseItemOrInnerList parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-item-or-list. +// https://httpwg.org/specs/rfc9651.html#parse-item-or-list. func parseItemOrInnerList(s *scanner) (Member, error) { if s.eof() { return nil, &UnmarshalError{s.off, ErrInvalidInnerListFormat} diff --git a/list_test.go b/list_test.go index bb622bf..9810c1e 100644 --- a/list_test.go +++ b/list_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestList(t *testing.T) { +func TestMarshalList(t *testing.T) { t.Parallel() params := NewParams() diff --git a/member.go b/member.go index 31b3017..f6b0ced 100644 --- a/member.go +++ b/member.go @@ -2,7 +2,7 @@ package httpsfv // Member is a marker interface for members of dictionaries and lists. // -// See https://httpwg.org/specs/rfc8941.html#list. +// See https://httpwg.org/specs/rfc9651.html#list. type Member interface { member() marshaler diff --git a/params.go b/params.go index 30a9e2e..769fd0e 100644 --- a/params.go +++ b/params.go @@ -7,7 +7,7 @@ import ( // Params are an ordered map of key-value pairs that are associated with an item or an inner list. // -// See https://httpwg.org/specs/rfc8941.html#param. +// See https://httpwg.org/specs/rfc9651.html#param. type Params struct { names []string values map[string]interface{} @@ -75,7 +75,7 @@ func (p *Params) Names() []string { } // marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-params. +// https://httpwg.org/specs/rfc9651.html#ser-params. func (p *Params) marshalSFV(b *strings.Builder) error { if p == nil { return ErrMissingParameters @@ -107,7 +107,7 @@ func (p *Params) marshalSFV(b *strings.Builder) error { } // parseParams parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-param. +// https://httpwg.org/specs/rfc9651.html#parse-param. func parseParams(s *scanner) (*Params, error) { p := NewParams() diff --git a/params_test.go b/params_test.go index 57ebade..f31f284 100644 --- a/params_test.go +++ b/params_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestParameters(t *testing.T) { +func TestMarshalParameters(t *testing.T) { t.Parallel() p := NewParams() diff --git a/string.go b/string.go index 7cca386..23c8133 100644 --- a/string.go +++ b/string.go @@ -2,7 +2,6 @@ package httpsfv import ( "errors" - "io" "strings" "unicode" ) @@ -10,9 +9,9 @@ import ( // ErrInvalidStringFormat is returned when a string format is invalid. var ErrInvalidStringFormat = errors.New("invalid string format") -// marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-string. -func marshalString(b io.ByteWriter, s string) error { +// marshalString serializes as defined in +// https://httpwg.org/specs/rfc9651.html#ser-string. +func marshalString(b *strings.Builder, s string) error { if err := b.WriteByte('"'); err != nil { return err } @@ -42,7 +41,7 @@ func marshalString(b io.ByteWriter, s string) error { } // parseString parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-string. +// https://httpwg.org/specs/rfc9651.html#parse-string. func parseString(s *scanner) (string, error) { if s.eof() || s.data[s.off] != '"' { return "", &UnmarshalError{s.off, ErrInvalidStringFormat} diff --git a/string_test.go b/string_test.go index 917d43e..3353f29 100644 --- a/string_test.go +++ b/string_test.go @@ -6,7 +6,7 @@ import ( "unicode" ) -func TestStringMarshalSFV(t *testing.T) { +func TestMarshalString(t *testing.T) { t.Parallel() data := []struct { @@ -69,11 +69,11 @@ func TestParseString(t *testing.T) { i, err := parseString(s) if d.err && err == nil { - t.Errorf("parse%s): error expected", d.in) + t.Errorf("parseString(%s): error expected", d.in) } if !d.err && d.out != i { - t.Errorf("parse%s) = %v, %v; %v, expected", d.in, i, err, d.out) + t.Errorf("parseString(%s) = %v, %v; %v, expected", d.in, i, err, d.out) } } } diff --git a/structured-field-tests b/structured-field-tests index faed1f9..7970aff 160000 --- a/structured-field-tests +++ b/structured-field-tests @@ -1 +1 @@ -Subproject commit faed1f92942abd4fb5d61b1f9f0dc359f499f1d7 +Subproject commit 7970aff32a4e73452beedde861f4eb52ed4243b0 diff --git a/token.go b/token.go index 9fe3285..718f1cc 100644 --- a/token.go +++ b/token.go @@ -24,12 +24,12 @@ func isExtendedTchar(c byte) bool { var ErrInvalidTokenFormat = errors.New("invalid token format") // Token represents a token as defined in -// https://httpwg.org/specs/rfc8941.html#token. +// https://httpwg.org/specs/rfc9651.html#token. // A specific type is used to distinguish tokens from strings. type Token string // marshalSFV serializes as defined in -// https://httpwg.org/specs/rfc8941.html#ser-token. +// https://httpwg.org/specs/rfc9651.html#ser-token. func (t Token) marshalSFV(b io.StringWriter) error { if len(t) == 0 { return fmt.Errorf("a token cannot be empty: %w", ErrInvalidTokenFormat) @@ -51,7 +51,7 @@ func (t Token) marshalSFV(b io.StringWriter) error { } // parseToken parses as defined in -// https://httpwg.org/specs/rfc8941.html#parse-token. +// https://httpwg.org/specs/rfc9651.html#parse-token. func parseToken(s *scanner) (Token, error) { if s.eof() || (!isAlpha(s.data[s.off]) && s.data[s.off] != '*') { return "", &UnmarshalError{s.off, ErrInvalidTokenFormat} diff --git a/token_test.go b/token_test.go index 1ca4bbb..b8de63a 100644 --- a/token_test.go +++ b/token_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -func TestTokenMarshalSFV(t *testing.T) { +func TestMarshalToken(t *testing.T) { t.Parallel() data := []struct { From 078ba1b52c7ae88682f0dff80e955dbbf98f1260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Fri, 21 Feb 2025 08:38:10 +0100 Subject: [PATCH 2/2] fix test --- .github/FUNDING.yml | 1 - encode_test.go | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index 3df6d04..0000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: dunglas diff --git a/encode_test.go b/encode_test.go index 2fd34ae..64f1c5a 100644 --- a/encode_test.go +++ b/encode_test.go @@ -19,10 +19,10 @@ func TestMarshal(t *testing.T) { tok.Params.Add("a", "b") d.Add("tok", tok) - date := NewItem(time.Date(1988, 21, 01, 0, 0, 0, 0, time.Local)) + date := NewItem(time.Date(1988, 21, 01, 0, 0, 0, 0, time.UTC)) d.Add("d", date) - if res, _ := Marshal(d); res != `i=22.1;foo;bar=baz, tok=foo;a="b", d=@620604000` { + if res, _ := Marshal(d); res != `i=22.1;foo;bar=baz, tok=foo;a="b", d=@620611200` { t.Errorf("marshal: bad result: %q", res) } }