feat(response): almost standard-compliant Accept-Encoding parser
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type acceptEncodingParser struct {
|
||||
// accepted contains all encoding that particular parser instance advertises.
|
||||
accepted []string
|
||||
}
|
||||
|
||||
// AcceptEncoding creates parser instance for "Accept-Encoding" header values.
|
||||
// It accepts list of encodings recognized by user of this parser instance.
|
||||
func AcceptEncoding(accepted ...string) *acceptEncodingParser {
|
||||
return &acceptEncodingParser{accepted: accepted}
|
||||
}
|
||||
|
||||
// Parse parses input string according to [HTTP Semantics]
|
||||
// and returns first encoding that can be understood by us.
|
||||
//
|
||||
// Currently this function ignores set weights other than q=0.
|
||||
// Encodings with q=0 will not be considered.
|
||||
//
|
||||
// If string is empty or no encoding was accepted function returns "identity".
|
||||
//
|
||||
// For "identity;q=0" and "*;q=0" function returns an empty string. In that case,
|
||||
// if no other encoding was accepted, 406 Not Acceptable should be returned.
|
||||
//
|
||||
// [HTTP Semantics]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Accept-Encoding.
|
||||
func (p *acceptEncodingParser) Parse(acceptEncoding string) string {
|
||||
accepted := "identity"
|
||||
|
||||
for enc := range strings.SplitSeq(acceptEncoding, ",") {
|
||||
enc = strings.TrimSpace(enc)
|
||||
|
||||
if qi := strings.IndexByte(enc, ';'); qi > -1 {
|
||||
qstr := strings.TrimPrefix(enc[qi:], ";")
|
||||
qstr = strings.TrimSpace(qstr)
|
||||
qstr = strings.TrimPrefix(qstr, "q=")
|
||||
|
||||
q, err := strconv.ParseFloat(qstr, 64)
|
||||
if err != nil {
|
||||
continue // Ignore weird float values.
|
||||
}
|
||||
|
||||
enc = strings.TrimSpace(enc[:qi])
|
||||
|
||||
if q == 0 && slices.Contains([]string{"identity", "*"}, enc) {
|
||||
accepted = "" // Explicitly disabled, so can't be used as fallback.
|
||||
continue
|
||||
}
|
||||
|
||||
if q == 0 {
|
||||
continue // Skipping unwanted.
|
||||
}
|
||||
}
|
||||
|
||||
if !slices.Contains(p.accepted, enc) {
|
||||
continue // Skipping unsupported.
|
||||
}
|
||||
|
||||
accepted = enc
|
||||
break
|
||||
}
|
||||
|
||||
return accepted
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package response
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAcceptEncoding(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
acceptable := []string{
|
||||
"br", "gzip", "deflate",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acceptEncoding string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "Empty input",
|
||||
acceptEncoding: "",
|
||||
want: "identity",
|
||||
},
|
||||
{
|
||||
name: "q=0 and identity",
|
||||
acceptEncoding: "identity;q=0",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "q=0 and *",
|
||||
acceptEncoding: "*;q=0",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "gzip",
|
||||
acceptEncoding: "gzip",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "gzip and br",
|
||||
acceptEncoding: "gzip,br",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "br and gzip",
|
||||
acceptEncoding: "br,gzip,deflate",
|
||||
want: "br",
|
||||
},
|
||||
{
|
||||
name: "unsupported encoding",
|
||||
acceptEncoding: "unknown",
|
||||
want: "identity",
|
||||
},
|
||||
{
|
||||
name: "empty encoding",
|
||||
acceptEncoding: ",",
|
||||
want: "identity",
|
||||
},
|
||||
{
|
||||
name: "multiple encodings and q=0",
|
||||
acceptEncoding: "gzip;q=0,br;q=0",
|
||||
want: "identity",
|
||||
},
|
||||
{
|
||||
// We want br here but weights are not supported.
|
||||
name: "multiple encodings and q values",
|
||||
acceptEncoding: "gzip;q=0.5,br;q=0.8",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "multiple encodings and wildcard",
|
||||
acceptEncoding: "*;q=0,gzip,br",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "multiple encodings and wildcard and q=0",
|
||||
acceptEncoding: "*;q=0,gzip,br;q=0",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
// We want br here but weights are not supported.
|
||||
name: "multiple encodings and wildcard and q values",
|
||||
acceptEncoding: "*;q=0.5,gzip;q=0.8,br",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "multiple encodings and wildcard and q values and q=0",
|
||||
acceptEncoding: "*;q=0.5,gzip;q=0.8,br;q=0",
|
||||
want: "gzip",
|
||||
},
|
||||
{
|
||||
name: "invalid q value",
|
||||
acceptEncoding: "gzip;q=abc,deflate",
|
||||
want: "deflate",
|
||||
},
|
||||
{
|
||||
name: "wrong spaces placing around q value",
|
||||
acceptEncoding: "gzip;q= 0.5, deflate;q=0.8",
|
||||
want: "deflate",
|
||||
},
|
||||
{
|
||||
name: "correct spaces placing around q value",
|
||||
acceptEncoding: "gzip ; q=0.5, deflate;q=0.8",
|
||||
want: "gzip",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Instantiate parser for each test to make sure it doesn't return cached values.
|
||||
parser := AcceptEncoding(acceptable...)
|
||||
|
||||
got := parser.Parse(test.acceptEncoding)
|
||||
if got != test.want {
|
||||
t.Errorf("Parse(%q) = %q, want %q", test.acceptEncoding, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAcceptEncoding(b *testing.B) {
|
||||
encoding := "identity;q=0,gzip,whatever"
|
||||
expected := "gzip"
|
||||
|
||||
parser := AcceptEncoding("br", "gzip", "deflate")
|
||||
|
||||
for b.Loop() {
|
||||
got := parser.Parse(encoding)
|
||||
if got != expected {
|
||||
b.Errorf("Parse(%q) = %q, want %q", encoding, got, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user