Просмотр исходного кода

feat(response): almost standard-compliant Accept-Encoding parser

gudvinr 2 недель назад
Родитель
Сommit
7769fa06ef
2 измененных файлов с 210 добавлено и 0 удалено
  1. 72 0
      internal/http/response/encoding.go
  2. 138 0
      internal/http/response/encoding_test.go

+ 72 - 0
internal/http/response/encoding.go

@@ -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
+}

+ 138 - 0
internal/http/response/encoding_test.go

@@ -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)
+		}
+	}
+}