From 77779297be6803a68bfb3bfef95683864555e4b9 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Thu, 7 Sep 2023 15:12:20 +1000 Subject: [PATCH] feat!: add ContentType, expanded Accept parsing BREAKING CHANGES * ContentType{} to describe a full content type descriptor * ParseAccept() returns a slice of acceptable ContentTypes found, ordered by quality factor * ParseContentType() returns (ContentType, bool) * CheckFormat() returns a single, preferred ContentType * Remove spaces from default ContentType formatting * Deprecate ResponseContentTypeHeader * Deprecate RequestAcceptHeader --- http/constants.go | 85 ++++++++++++++++++++-- http/constants_test.go | 17 ++++- http/parse.go | 161 +++++++++++++++++++++++------------------ http/parse_test.go | 121 +++++++++++++++++++++---------- 4 files changed, 262 insertions(+), 122 deletions(-) diff --git a/http/constants.go b/http/constants.go index bae19e9..8af4f72 100644 --- a/http/constants.go +++ b/http/constants.go @@ -1,34 +1,105 @@ package trustlesshttp -import "fmt" +import ( + "strconv" + "strings" +) + +type ContentTypeOrder string const ( MimeTypeCar = "application/vnd.ipld.car" // The only accepted MIME type MimeTypeCarVersion = "1" // We only accept version 1 of the MIME type FormatParameterCar = "car" // The only valid format parameter value FilenameExtCar = ".car" // The only valid filename extension - DefaultIncludeDupes = true // The default value for an unspecified "dups" parameter. See https://github.com/ipfs/specs/pull/412. ResponseCacheControlHeader = "public, max-age=29030400, immutable" // Magic cache control values + DefaultIncludeDupes = true // The default value for an unspecified "dups" parameter. + DefaultOrder = ContentTypeOrderDfs // The default value for an unspecified "order" parameter. + + ContentTypeOrderDfs ContentTypeOrder = "dfs" + ContentTypeOrderUnk ContentTypeOrder = "unk" ) var ( ResponseChunkDelimeter = []byte("0\r\n") // An http/1.1 chunk delimeter, used for specifying an early end to the response - baseContentType = fmt.Sprintf("%s; version=%s; order=dfs", MimeTypeCar, MimeTypeCarVersion) ) +type ContentType struct { + Mime string + Order ContentTypeOrder + Duplicates bool + Quality float32 +} + +func (ct ContentType) String() string { + sb := strings.Builder{} + sb.WriteString(ct.Mime) + sb.WriteString(";version=") + sb.WriteString(MimeTypeCarVersion) + sb.WriteString(";order=") + sb.WriteString(string(ct.Order)) + if ct.Duplicates { + sb.WriteString(";dups=y") + } else { + sb.WriteString(";dups=n") + } + if ct.Quality < 1 && ct.Quality >= 0.00 { + sb.WriteString(";q=") + // write quality with max 3 decimal places + sb.WriteString(strconv.FormatFloat(float64(ct.Quality), 'g', 3, 32)) + } + return sb.String() +} + +type ContentTypeOption func(ct *ContentType) + +func WithContentTypeOrder(order ContentTypeOrder) ContentTypeOption { + return func(ct *ContentType) { + ct.Order = order + } +} + +func WithContentTypeDuplicates(duplicates bool) ContentTypeOption { + return func(ct *ContentType) { + ct.Duplicates = duplicates + } +} + +func WithContentTypeQuality(quality float32) ContentTypeOption { + return func(ct *ContentType) { + ct.Quality = quality + } +} + +func NewContentType(opt ...ContentTypeOption) ContentType { + ct := ContentType{ + Mime: MimeTypeCar, + Order: DefaultOrder, + Duplicates: DefaultIncludeDupes, + Quality: 1, + } + for _, o := range opt { + o(&ct) + } + return ct +} + // ResponseContentTypeHeader returns the value for the Content-Type header for a // Trustless Gateway response which will vary depending on whether duplicates // are included or not. Otherwise, the header is the same for all responses. +// +// Deprecated: Use NewContentType().String() instead. func ResponseContentTypeHeader(duplicates bool) string { - if duplicates { - return baseContentType + "; dups=y" - } - return baseContentType + "; dups=n" + ct := NewContentType() + ct.Duplicates = duplicates + return ct.String() } // RequestAcceptHeader returns the value for the Accept header for a Trustless // Gateway request which will vary depending on whether duplicates are included // or not. Otherwise, the header is the same for all requests. +// +// Deprecated: Use NewContentType().String() instead. func RequestAcceptHeader(duplicates bool) string { return ResponseContentTypeHeader(duplicates) } diff --git a/http/constants_test.go b/http/constants_test.go index 90f729c..037c92e 100644 --- a/http/constants_test.go +++ b/http/constants_test.go @@ -8,8 +8,17 @@ import ( ) func TestContentType(t *testing.T) { - require.Equal(t, "application/vnd.ipld.car; version=1; order=dfs; dups=y", trustlesshttp.ResponseContentTypeHeader(true)) - require.Equal(t, "application/vnd.ipld.car; version=1; order=dfs; dups=y", trustlesshttp.RequestAcceptHeader(true)) - require.Equal(t, "application/vnd.ipld.car; version=1; order=dfs; dups=n", trustlesshttp.ResponseContentTypeHeader(false)) - require.Equal(t, "application/vnd.ipld.car; version=1; order=dfs; dups=n", trustlesshttp.RequestAcceptHeader(false)) + req := require.New(t) + + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y", trustlesshttp.ResponseContentTypeHeader(true)) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y", trustlesshttp.RequestAcceptHeader(true)) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=n", trustlesshttp.ResponseContentTypeHeader(false)) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=n", trustlesshttp.RequestAcceptHeader(false)) + + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y", trustlesshttp.NewContentType().String()) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y;q=0.8", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeQuality(0.8)).String()) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y;q=0.333", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeQuality(1.0/3.0)).String()) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=y", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeQuality(-1.0)).String()) + req.Equal("application/vnd.ipld.car;version=1;order=dfs;dups=n", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeDuplicates(false)).String()) + req.Equal("application/vnd.ipld.car;version=1;order=unk;dups=n", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeDuplicates(false), trustlesshttp.WithContentTypeOrder(trustlesshttp.ContentTypeOrderUnk)).String()) } diff --git a/http/parse.go b/http/parse.go index c9e9bd3..e26ba24 100644 --- a/http/parse.go +++ b/http/parse.go @@ -5,6 +5,8 @@ import ( "fmt" "net/http" "path/filepath" + "sort" + "strconv" "strings" "github.com/ipfs/go-cid" @@ -73,31 +75,32 @@ func ParseFilename(req *http.Request) (string, error) { // // IPFS Trustless Gateway only allows the "car" format query parameter // https://specs.ipfs.tech/http-gateways/path-gateway/#format-request-query-parameter -func CheckFormat(req *http.Request) (bool, error) { - includeDupes := DefaultIncludeDupes +func CheckFormat(req *http.Request) (ContentType, error) { + // check if format is "car" + format := req.URL.Query().Get("format") + var validFormat bool + if format != "" { + if format != FormatParameterCar { + return ContentType{}, fmt.Errorf("invalid format parameter; unsupported: %q", format) + } + validFormat = true + } accept := req.Header.Get("Accept") if accept != "" { // check if Accept header includes application/vnd.ipld.car - var validAccept bool - validAccept, includeDupes = ParseAccept(accept) - if !validAccept { - return false, fmt.Errorf("invalid Accept header; unsupported: %q", accept) + accepts := ParseAccept(accept) + if len(accepts) == 0 { + return ContentType{}, fmt.Errorf("invalid Accept header; unsupported: %q", accept) } - } - // check if format is "car" - format := req.URL.Query().Get("format") - if format != "" && format != FormatParameterCar { - return false, fmt.Errorf("invalid format parameter; unsupported: %q", format) + return accepts[0], nil // pick the top one we can support } - // if neither are provided return - // one of them has to be given with a CAR type since we only return CAR data - if accept == "" && format == "" { - return false, fmt.Errorf("neither a valid Accept header nor format parameter were provided") + if validFormat { + return NewContentType(), nil // default is acceptable in this case (no accept but format=car) } - return includeDupes, nil + return ContentType{}, fmt.Errorf("neither a valid Accept header nor format parameter were provided") } // ParseAccept validates a request Accept header and returns whether or not @@ -106,72 +109,88 @@ func CheckFormat(req *http.Request) (bool, error) { // This will operate the same as ParseContentType except that it is less strict // with the format specifier, allowing for "application/*" and "*/*" as well as // the standard "application/vnd.ipld.car". -func ParseAccept(acceptHeader string) (validAccept bool, includeDupes bool) { - return parseContentType(acceptHeader, false) +func ParseAccept(acceptHeader string) []ContentType { + acceptTypes := strings.Split(acceptHeader, ",") + accepts := make([]ContentType, 0, len(acceptTypes)) + for _, acceptType := range acceptTypes { + accept, valid := parseContentType(acceptType, false) + if valid { + accepts = append(accepts, accept) + } + } + // sort accepts by ContentType#Quality + sort.SliceStable(accepts, func(i, j int) bool { + return accepts[i].Quality > accepts[j].Quality + }) + return accepts } -// ParseContentType validates a response Content-Type header and returns whether -// or not duplicate blocks are expected in the response. +// ParseContentType validates a response Content-Type header and returns +// a ContentType descriptor form and a boolean to indicate whether or not +// the header value was valid or not. // -// This will operate the same as ParseAccept except that it strictly only -// allows the "application/vnd.ipld.car" Content-Type. -func ParseContentType(contentTypeHeader string) (validContentType bool, includeDupes bool) { +// This will operate similar to ParseAccept except that it strictly only +// allows the "application/vnd.ipld.car" Content-Type (and it won't accept +// comma separated list of content types). +func ParseContentType(contentTypeHeader string) (ContentType, bool) { return parseContentType(contentTypeHeader, true) } -func parseContentType(header string, strictType bool) (validAccept bool, includeDupes bool) { - acceptTypes := strings.Split(header, ",") - validAccept = false - includeDupes = DefaultIncludeDupes - for _, acceptType := range acceptTypes { - typeParts := strings.Split(acceptType, ";") - if typeParts[0] == MimeTypeCar || (!strictType && (typeParts[0] == "*/*" || typeParts[0] == "application/*")) { - validAccept = true - if typeParts[0] == MimeTypeCar { - // parse additional car attributes outlined in IPIP-412: https://github.com/ipfs/specs/pull/412 - for _, nextPart := range typeParts[1:] { - pair := strings.Split(nextPart, "=") - if len(pair) == 2 { - attr := strings.TrimSpace(pair[0]) - value := strings.TrimSpace(pair[1]) - switch attr { - case "dups": - switch value { - case "y": - includeDupes = true - case "n": - includeDupes = false - default: - // don't accept unexpected values - validAccept = false - } - case "version": - switch value { - case MimeTypeCarVersion: - default: - validAccept = false - } - case "order": - switch value { - case "dfs": - case "unk": - default: - // we only do dfs, which also satisfies unk, future extensions are not yet supported - validAccept = false - } - default: - // ignore others - } +func parseContentType(header string, strictType bool) (ContentType, bool) { + typeParts := strings.Split(header, ";") + mime := strings.TrimSpace(typeParts[0]) + if mime == MimeTypeCar || (!strictType && (mime == "*/*" || mime == "application/*")) { + contentType := NewContentType() + contentType.Mime = mime + // parse additional car attributes outlined in IPIP-412 + // https://specs.ipfs.tech/http-gateways/trustless-gateway/ + for _, nextPart := range typeParts[1:] { + pair := strings.Split(nextPart, "=") + if len(pair) == 2 { + attr := strings.TrimSpace(pair[0]) + value := strings.TrimSpace(pair[1]) + switch attr { + case "dups": + switch value { + case "y": + contentType.Duplicates = true + case "n": + contentType.Duplicates = false + default: + // don't accept unexpected values + return ContentType{}, false + } + case "version": + switch value { + case MimeTypeCarVersion: + default: + return ContentType{}, false } + case "order": + switch value { + case "dfs": + contentType.Order = ContentTypeOrderDfs + case "unk": + contentType.Order = ContentTypeOrderUnk + default: + // we only do dfs, which also satisfies unk, future extensions are not yet supported + return ContentType{}, false + } + case "q": + // parse quality + quality, err := strconv.ParseFloat(value, 32) + if err != nil || quality < 0 || quality > 1 { + return ContentType{}, false + } + contentType.Quality = float32(quality) + default: + // ignore others } } - // only break if further validation didn't fail - if validAccept { - break - } } + return contentType, true } - return + return ContentType{}, false } var ( diff --git a/http/parse_test.go b/http/parse_test.go index de5b090..20aa6ed 100644 --- a/http/parse_test.go +++ b/http/parse_test.go @@ -106,22 +106,22 @@ func TestParseFilename(t *testing.T) { func TestCheckFormat(t *testing.T) { for _, tc := range []struct { - name string - accept string - query string - expectDups bool - err string + name string + accept string + query string + expectAccept trustlesshttp.ContentType + err string }{ - {"empty (err)", "", "", false, "neither a valid Accept header nor format parameter were provided"}, - {"format=bop (err)", "", "format=bop", true, "invalid format parameter; unsupported: \"bop\""}, - {"format=car", "", "format=car", true, ""}, - {"plain accept", "application/vnd.ipld.car", "", true, ""}, - {"accept dups", "application/vnd.ipld.car; dups=y", "", true, ""}, - {"accept no dups", "application/vnd.ipld.car; dups=n", "", false, ""}, - {"accept no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", "", false, ""}, - {"valid accept but format=bop (err)", "application/vnd.ipld.car; dups=y", "format=bop", true, "invalid format parameter; unsupported: \"bop\""}, - {"valid accept but format=car", "application/vnd.ipld.car; dups=y", "format=car", true, ""}, - {"invalid accept but format=car", "application/vnd.ipld.car; dups=YES!", "format=car", false, "invalid Accept header; unsupported"}, + {"empty (err)", "", "", trustlesshttp.ContentType{}, "neither a valid Accept header nor format parameter were provided"}, + {"format=bop (err)", "", "format=bop", trustlesshttp.NewContentType(), "invalid format parameter; unsupported: \"bop\""}, + {"format=car", "", "format=car", trustlesshttp.NewContentType(), ""}, + {"plain accept", "application/vnd.ipld.car", "", trustlesshttp.NewContentType(), ""}, + {"accept dups", "application/vnd.ipld.car; dups=y", "", trustlesshttp.NewContentType(), ""}, + {"accept no dups", "application/vnd.ipld.car; dups=n", "", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeDuplicates(false)), ""}, + {"accept no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", "", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeDuplicates(false)), ""}, + {"valid accept but format=bop (err)", "application/vnd.ipld.car; dups=y", "format=bop", trustlesshttp.NewContentType(), "invalid format parameter; unsupported: \"bop\""}, + {"valid accept but format=car", "application/vnd.ipld.car; dups=y", "format=car", trustlesshttp.NewContentType(), ""}, + {"invalid accept but format=car", "application/vnd.ipld.car; dups=YES!", "format=car", trustlesshttp.NewContentType(trustlesshttp.WithContentTypeDuplicates(false)), "invalid Accept header; unsupported"}, } { t.Run(tc.name, func(t *testing.T) { req := &http.Request{} @@ -129,10 +129,10 @@ func TestCheckFormat(t *testing.T) { if tc.accept != "" { req.Header = http.Header{"Accept": []string{tc.accept}} } - dups, err := trustlesshttp.CheckFormat(req) + accept, err := trustlesshttp.CheckFormat(req) if tc.err == "" { require.NoError(t, err) - require.Equal(t, tc.expectDups, dups) + require.Equal(t, tc.expectAccept, accept) } else { require.Error(t, err) require.Contains(t, err.Error(), tc.err) @@ -141,43 +141,84 @@ func TestCheckFormat(t *testing.T) { } } -func TestParseAcceptAndContentType(t *testing.T) { +func TestParseContentType(t *testing.T) { for _, tc := range []struct { name string accept string - expectValidAccept bool expectValidContentType bool expectDups bool }{ - {"empty (err)", "", false, false, true}, - {"plain", "application/vnd.ipld.car", true, true, true}, - {"*/*", "*/*", true, false, true}, - {"application/*", "application/*", true, false, true}, - {"dups", "application/vnd.ipld.car; dups=y", true, true, true}, - {"no dups", "application/vnd.ipld.car; dups=n", true, true, false}, - {"no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", true, true, false}, - {"version=1", "application/vnd.ipld.car; version=1; dups=n", true, true, false}, - {"version=2", "application/vnd.ipld.car; version=2; dups=n", false, false, false}, - {"order=dfs", "application/vnd.ipld.car; order=dfs; dups=n", true, true, false}, - {"order=unk", "application/vnd.ipld.car; order=unk; dups=n", true, true, false}, - {"order=bork", "application/vnd.ipld.car; order=bork; dups=y", false, false, true}, - {"complete", "application/vnd.ipld.car; order=dfs; dups=y; version=1", true, true, true}, - {"complete (squish)", "application/vnd.ipld.car;order=dfs;dups=y;version=1", true, true, true}, - {"complete (shuffle)", "application/vnd.ipld.car;version=1;dups=y;order=dfs;", true, true, true}, - {"complete (cruft)", "application/vnd.ipld.car;;version=1; bip ; dups=n ;bop;order=dfs;--", true, true, false}, + {"empty (err)", "", false, false}, + {"plain", "application/vnd.ipld.car", true, true}, + {"*/*", "*/*", false, false}, + {"application/*", "application/*", false, false}, + {"dups", "application/vnd.ipld.car; dups=y", true, true}, + {"no dups", "application/vnd.ipld.car; dups=n", true, false}, + {"no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", true, false}, + {"version=1", "application/vnd.ipld.car; version=1; dups=n", true, false}, + {"version=2", "application/vnd.ipld.car; version=2; dups=n", false, false}, + {"order=dfs", "application/vnd.ipld.car; order=dfs; dups=n", true, false}, + {"order=unk", "application/vnd.ipld.car; order=unk; dups=n", true, false}, + {"order=bork", "application/vnd.ipld.car; order=bork; dups=y", false, false}, + {"complete", "application/vnd.ipld.car; order=dfs; dups=y; version=1", true, true}, + {"complete (squish)", "application/vnd.ipld.car;order=dfs;dups=y;version=1", true, true}, + {"complete (shuffle)", "application/vnd.ipld.car;version=1;dups=y;order=dfs;", true, true}, + {"complete (cruft)", "application/vnd.ipld.car;;version=1; bip ; dups=n ;bop;order=dfs;--", true, false}, } { t.Run(tc.name, func(t *testing.T) { - valid, dups := trustlesshttp.ParseAccept(tc.accept) - require.Equal(t, tc.expectValidAccept, valid) - require.Equal(t, tc.expectDups, dups) - - valid, dups = trustlesshttp.ParseContentType(tc.accept) + valid, dups := trustlesshttp.ParseContentType(tc.accept) require.Equal(t, tc.expectValidContentType, valid) require.Equal(t, tc.expectDups, dups) }) } } +func TestParseAccept(t *testing.T) { + for _, tc := range []struct { + name string + accept string + expected []trustlesshttp.ContentType + }{ + {"empty (err)", "", []trustlesshttp.ContentType{}}, + {"plain", "application/vnd.ipld.car", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"*/*", "*/*", []trustlesshttp.ContentType{{Mime: "*/*", Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"application/*", "application/*", []trustlesshttp.ContentType{{Mime: "application/*", Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"dups", "application/vnd.ipld.car; dups=y", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"no dups", "application/vnd.ipld.car; dups=n", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"no dups and cruft", "application/vnd.ipld.car; dups=n; bip; bop", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"version=1", "application/vnd.ipld.car; version=1; dups=n", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"version=2", "application/vnd.ipld.car; version=2; dups=n", []trustlesshttp.ContentType{}}, + {"order=dfs", "application/vnd.ipld.car; order=dfs; dups=n", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"order=unk", "application/vnd.ipld.car; order=unk; dups=n", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderUnk, Quality: 1.0}}}, + {"order=bork", "application/vnd.ipld.car; order=bork; dups=y", []trustlesshttp.ContentType{}}, + {"complete", "application/vnd.ipld.car; order=dfs; dups=y; version=1", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"complete (squish)", "application/vnd.ipld.car;order=dfs;dups=y;version=1", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"complete (shuffle)", "application/vnd.ipld.car;version=1;dups=y;order=dfs;", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"complete (cruft)", "application/vnd.ipld.car;;version=1; bip ; dups=n ;bop;order=dfs;--", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}}}, + {"q", "application/vnd.ipld.car; order=dfs; q=0.77; dups=n", []trustlesshttp.ContentType{{Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 0.77}}}, + {"q=bork", "application/vnd.ipld.car; order=dfs; q=bork; dups=n", []trustlesshttp.ContentType{}}, + {"q=-1", "application/vnd.ipld.car; order=dfs; q=-0.1; dups=n", []trustlesshttp.ContentType{}}, + + { + "ordered", + "application/vnd.ipld.car;dups=n;order=unk;q=0.8, text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.1, application/vnd.ipld.car;dups=y;order=dfs;q=0.9 , application/vnd.ipld.car, application/vnd.ipld.car;dups=y;order=unk;q=0.7, application/vnd.ipld.car;dups=y;order=dfs;q=0.7", + []trustlesshttp.ContentType{ + {Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 1.0}, + {Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 0.9}, + {Mime: trustlesshttp.MimeTypeCar, Duplicates: false, Order: trustlesshttp.ContentTypeOrderUnk, Quality: 0.8}, + {Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderUnk, Quality: 0.7}, + {Mime: trustlesshttp.MimeTypeCar, Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 0.7}, + {Mime: "*/*", Duplicates: true, Order: trustlesshttp.ContentTypeOrderDfs, Quality: 0.1}, + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + accepts := trustlesshttp.ParseAccept(tc.accept) + require.Equal(t, tc.expected, accepts) + }) + } +} + func TestParseUrlPath(t *testing.T) { for _, tc := range []struct { name string