diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml index 3aa259f..858f615 100644 --- a/.github/workflows/default.yml +++ b/.github/workflows/default.yml @@ -15,6 +15,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] + version: [1.25.x, 1.26.x] runs-on: ${{ matrix.os }} env: OUTPUTDIR: coverage @@ -23,10 +24,9 @@ jobs: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: fetch-depth: 0 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: - go-version-file: go.mod - cache-dependency-path: go.sum + go-version: ${{ matrix.version }} - name: Run tests run: make test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 7afcdd7..c5d161e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: - go-version-file: go.mod + go-version-file: "go.mod" # Golangci-lint action is flaky, so we run it manually - name: Run golangci-lint shell: bash run: | - go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1.2 - make lint \ No newline at end of file + go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 + make lint diff --git a/.golangci.yaml b/.golangci.yaml index 56a2e56..f50e591 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -109,7 +109,7 @@ linters: goheader: values: const: - AUTHOR: "Bart Venter " + AUTHOR: "Bart Venter <72999113+bartventer@users.noreply.github.com>" template: |- Copyright (c) {{ YEAR }} {{ AUTHOR }} diff --git a/.vscode/settings.json b/.vscode/settings.json index ae7210e..bdaaeda 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -16,6 +16,7 @@ "--output.text.path=stdout", "--output.text.print-issued-lines=false", "--show-stats=false", + "--disable goheader", "--fix" ], "editor.detectIndentation": true diff --git a/context.go b/context.go index ddd41cf..bbcf252 100644 --- a/context.go +++ b/context.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/go.mod b/go.mod index fea5801..c0d75b1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/bartventer/httpcache go 1.25 + +toolchain go1.26.1 diff --git a/helpers.go b/helpers.go index 06e51ee..52e6767 100644 --- a/helpers.go +++ b/helpers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/helpers_test.go b/helpers_test.go index 732bfc4..bd1d0f4 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/cacheabilityevaluator.go b/internal/cacheabilityevaluator.go index f0a6f23..a4bef11 100644 --- a/internal/cacheabilityevaluator.go +++ b/internal/cacheabilityevaluator.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/cacheabilityevaluator_test.go b/internal/cacheabilityevaluator_test.go index e7fd80d..7a26556 100644 --- a/internal/cacheabilityevaluator_test.go +++ b/internal/cacheabilityevaluator_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/cacheinvalidator.go b/internal/cacheinvalidator.go index 13ad403..bd0c5f4 100644 --- a/internal/cacheinvalidator.go +++ b/internal/cacheinvalidator.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/cacheinvalidator_test.go b/internal/cacheinvalidator_test.go index 880afeb..9599690 100644 --- a/internal/cacheinvalidator_test.go +++ b/internal/cacheinvalidator_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/ccdirectives.go b/internal/ccdirectives.go index 23e88f9..4c365c3 100644 --- a/internal/ccdirectives.go +++ b/internal/ccdirectives.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -60,33 +60,32 @@ func (r RawDeltaSeconds) Value() (dur time.Duration, valid bool) { return time.Duration(seconds) * time.Second, true } -// RawCSVSeq is a string that represents a sequence of comma-separated values. -type RawCSVSeq string +// RawCSV is a string that represents a sequence of comma-separated values. +type RawCSV string // Value returns an iterator over the raw comma-separated string and a boolean indicating // whether the result is valid. -func (s RawCSVSeq) Value() (seq iter.Seq[string], valid bool) { +func (s RawCSV) Value() (seq iter.Seq[string], valid bool) { if len(s) == 0 { return } - return TrimmedCSVSeq(string(s)), true + return TrimmedCSV(string(s)), true } -// directivesSeq2 returns an iterator over all key-value pairs in a string of +// directives returns an iterator over all key-value pairs in a string of // cache directives (as specified in 9111, §5.2.1 and 5.2.2). The // iterator yields the key (token) and value (argument) of each directive. // // It guarantees that the key is always non-empty, and if a value is not // present, it yields an empty string as the value. -func directivesSeq2(s string) iter.Seq2[string, string] { +func directives(s string) iter.Seq2[string, string] { return func(yield func(string, string) bool) { - for part := range TrimmedCSVSeq(s) { + for part := range TrimmedCSV(s) { key, value, found := strings.Cut(part, "=") if !found { key = textproto.TrimString(part) value = "" } else { - // value = textproto.TrimString(ParseQuotedString(value)) value = textproto.TrimString(value) } if len(key) == 0 { @@ -102,12 +101,7 @@ func directivesSeq2(s string) iter.Seq2[string, string] { // parseDirectives parses a string of cache directives and returns a map // where the keys are the directive names and the values are the arguments. func parseDirectives(s string) map[string]string { - return maps.Collect(directivesSeq2(s)) -} - -func hasToken(d map[string]string, token string) bool { - _, ok := d[token] - return ok + return maps.Collect(directives(s)) } func getDurationDirective(d map[string]string, token string) (dur time.Duration, valid bool) { @@ -213,12 +207,12 @@ func (d CCResponseDirectives) MustUnderstand() bool { } // NoCache parses the "no-cache" response directive as defined in RFC 9111, §5.2.2.4. -func (d CCResponseDirectives) NoCache() (fields RawCSVSeq, present bool) { +func (d CCResponseDirectives) NoCache() (fields RawCSV, present bool) { v, ok := d["no-cache"] if !ok { return } - return RawCSVSeq(ParseQuotedString(v)), true + return RawCSV(ParseQuotedString(v)), true } // NoStore reports the presence of the "no-store" response directive as defined in RFC 9111, §5.2.2.5. diff --git a/internal/ccdirectives_test.go b/internal/ccdirectives_test.go index fc3da28..53ca521 100644 --- a/internal/ccdirectives_test.go +++ b/internal/ccdirectives_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -84,7 +84,7 @@ func TestParseCCRequestDirectives_AllDirectives(t *testing.T) { t.Run("NoCache (quoted CSV)", func(t *testing.T) { raw := got["no-cache"] - noCacheSeq, valid := RawCSVSeq(ParseQuotedString(raw)).Value() + noCacheSeq, valid := RawCSV(ParseQuotedString(raw)).Value() testutil.RequireTrue(t, valid) expectedNoCache := []string{"foo", "bar"} var gotNoCache []string diff --git a/internal/clock.go b/internal/clock.go index 086dadf..e873bc5 100644 --- a/internal/clock.go +++ b/internal/clock.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/doc.go b/internal/doc.go index 653c14b..92c157f 100644 --- a/internal/doc.go +++ b/internal/doc.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/entry.go b/internal/entry.go index 320c462..c00f8ff 100644 --- a/internal/entry.go +++ b/internal/entry.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/entry_test.go b/internal/entry_test.go index a5e429b..95a2f7e 100644 --- a/internal/entry_test.go +++ b/internal/entry_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/freshness.go b/internal/freshness.go index 9f89dfa..8f628de 100644 --- a/internal/freshness.go +++ b/internal/freshness.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/freshness_test.go b/internal/freshness_test.go index c6f8dc1..12d9330 100644 --- a/internal/freshness_test.go +++ b/internal/freshness_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/header.go b/internal/header.go index c21bafb..7df7f78 100644 --- a/internal/header.go +++ b/internal/header.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/helpers.go b/internal/helpers.go index 9b0aa64..672338f 100644 --- a/internal/helpers.go +++ b/internal/helpers.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,28 +21,19 @@ import ( "net/url" "strconv" "strings" -) -func defaultPort(scheme string) string { - switch scheme { - case "http": - return "80" - case "https": - return "443" - default: - return "" - } -} + "github.com/bartventer/httpcache/internal/urlutil" +) // sameOrigin checks if two URIs have the same origin (scheme, host, port). func sameOrigin(a, b *url.URL) bool { aPort := a.Port() if aPort == "" { - aPort = defaultPort(a.Scheme) + aPort = urlutil.DefaultPort(a.Scheme) } bPort := b.Port() if bPort == "" { - bPort = defaultPort(b.Scheme) + bPort = urlutil.DefaultPort(b.Scheme) } return strings.EqualFold(a.Scheme, b.Scheme) && strings.EqualFold(a.Hostname(), b.Hostname()) && @@ -74,7 +65,7 @@ func hopByHopHeaders(respHeader http.Header) map[string]struct{} { // Also see net/http/response.go "respExcludeHeader" for additional excluded headers. } // Fields listed in the Connection header field - for field := range TrimmedCSVCanonicalSeq(respHeader.Get("Connection")) { + for field := range TrimmedCSVCanonical(respHeader.Get("Connection")) { m[field] = struct{}{} } return m @@ -116,43 +107,6 @@ func isStaleErrorAllowed(code int) bool { } } -// From Go's net/url package. -// Copyright 2009 The Go Authors. All rights reserved. -// -// splitHostPort separates host and port. If the port is not valid, it returns -// the entire input as host, and it doesn't check the validity of the host. -// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. -func splitHostPort(hostPort string) (host, port string) { - host = hostPort - colon := strings.LastIndexByte(host, ':') - if colon != -1 && validOptionalPort(host[colon:]) { - host, port = host[:colon], host[colon+1:] - } - if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { - host = host[1 : len(host)-1] - } - return -} - -// From Go's net/url package. -// Copyright 2009 The Go Authors. All rights reserved. -// -// validOptionalPort reports whether port is either an empty string or matches "/^:\d*$/". -func validOptionalPort(port string) bool { - if port == "" { - return true - } - if port[0] != ':' { - return false - } - for _, b := range port[1:] { - if b < '0' || b > '9' { - return false - } - } - return true -} - func IsUnsafeMethod(method string) bool { switch method { case http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch: @@ -166,9 +120,9 @@ func IsNonErrorStatus(status int) bool { return (status >= 200 && status < 400) } -// TrimmedCSVSeq returns an iterator over the raw comma-separated string. +// TrimmedCSV returns an iterator over the raw comma-separated string. // It yields each part of the string, trimmed of whitespace, and does not split inside quoted strings. -func TrimmedCSVSeq(s string) iter.Seq[string] { +func TrimmedCSV(s string) iter.Seq[string] { return func(yield func(string) bool) { var part strings.Builder inQuotes := false @@ -206,14 +160,19 @@ func TrimmedCSVSeq(s string) iter.Seq[string] { } } -// TrimmedCSVCanonicalSeq is the same as [TrimmedCSVSeq], but it yields each part +// TrimmedCSVCanonical is the same as [TrimmedCSV], but it yields each part // in canonical form. -func TrimmedCSVCanonicalSeq(s string) iter.Seq[string] { +func TrimmedCSVCanonical(s string) iter.Seq[string] { return func(yield func(string) bool) { - for part := range TrimmedCSVSeq(s) { + for part := range TrimmedCSV(s) { if !yield(http.CanonicalHeaderKey(part)) { return } } } } + +func hasToken[Map ~map[K]V, K comparable, V any](m Map, token K) bool { + _, ok := m[token] + return ok +} diff --git a/internal/log.go b/internal/log.go index 16c72b6..a788af5 100644 --- a/internal/log.go +++ b/internal/log.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/mocks.go b/internal/mocks.go index 90512fb..3ab4d54 100644 --- a/internal/mocks.go +++ b/internal/mocks.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/normalization.go b/internal/normalization.go index 78f7f03..6ec08a9 100644 --- a/internal/normalization.go +++ b/internal/normalization.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -142,7 +142,7 @@ func normalizeHeaderValue(field, value string) string { // normalizeOrderInsensitive normalizes comma-separated values where order doesn't matter. func normalizeOrderInsensitive(value string) string { - parts := slices.Sorted(TrimmedCSVSeq(value)) + parts := slices.Sorted(TrimmedCSV(value)) return strings.Join(parts, ",") } @@ -163,7 +163,7 @@ func normalizeOrderInsensitiveWithQValues(value string) string { q unique.Handle[float64] // the quality value (q=0.8); default is 1.0 params []string // any additional parameters (sorted) } - parts := slices.Collect(TrimmedCSVSeq(value)) + parts := slices.Collect(TrimmedCSV(value)) qualityParts := make([]qualityValue, 0, len(parts)) outer: for i := range parts { @@ -192,13 +192,10 @@ outer: } slices.Sort(params) } - if cap(params) > len(params) { - params = slices.Clip(params) - } qualityParts = append(qualityParts, qualityValue{ main: main, q: q, - params: params, + params: slices.Clip(params), }) } @@ -262,6 +259,23 @@ func normalizeEncodingHeader(value string) string { return normalizeOrderInsensitive(value) } +func normalizedVaryHeader(vary string, reqHeader http.Header) iter.Seq2[string, string] { + return func(yield func(string, string) bool) { + for name := range TrimmedCSVCanonical(vary) { + values := reqHeader[name] + value := "" + // an empty value is valid and means "no variation" + if len(values) > 0 { + // NOTE: The policy of this cache is to use just the first header line + value = normalizeHeaderValue(name, values[0]) + } + if !yield(name, value) { + return + } + } + } +} + // VaryHeaderNormalizer describes the interface implemented by types that can // normalize request header field values given a Vary header field value, as per // RFC 9111 §4.1. @@ -277,27 +291,13 @@ func (f VaryHeaderNormalizerFunc) NormalizeVaryHeader( ) iter.Seq2[string, string] { return f(vary, reqHeader) } -func NewVaryHeaderNormalizer() VaryHeaderNormalizer { - return VaryHeaderNormalizerFunc(normalizeVaryHeaderSeq2) -} -func normalizeVaryHeaderSeq2(vary string, reqHeader http.Header) iter.Seq2[string, string] { - return func(yield func(string, string) bool) { - for name := range TrimmedCSVCanonicalSeq(vary) { - values := reqHeader[name] - value := "" - // an empty value is valid and means "no variation" - if len(values) > 0 { - // NOTE: The policy of this cache is to use just the first header line - value = normalizeHeaderValue(name, values[0]) - } - if !yield(name, value) { - return - } - } - } +func NewVaryHeaderNormalizer() VaryHeaderNormalizer { + return VaryHeaderNormalizerFunc(normalizedVaryHeader) } +// HeaderValueNormalizer describes the interface implemented by types that can normalize +// header field values according to the rules defined in RFC 9111 §4.1. type HeaderValueNormalizer interface { NormalizeHeaderValue(field, value string) string } diff --git a/internal/normalization_test.go b/internal/normalization_test.go index d5bf188..febb6b7 100644 --- a/internal/normalization_test.go +++ b/internal/normalization_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/quotedstring.go b/internal/quotedstring.go index b10dd92..3c7847d 100644 --- a/internal/quotedstring.go +++ b/internal/quotedstring.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/quotedstring_test.go b/internal/quotedstring_test.go index bdaeaf0..efc6d82 100644 --- a/internal/quotedstring_test.go +++ b/internal/quotedstring_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/requestmethodchecker.go b/internal/requestmethodchecker.go index 5c69dae..bbd8491 100644 --- a/internal/requestmethodchecker.go +++ b/internal/requestmethodchecker.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/requestmethodchecker_test.go b/internal/requestmethodchecker_test.go index 2bac94f..9922d33 100644 --- a/internal/requestmethodchecker_test.go +++ b/internal/requestmethodchecker_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/responsecache.go b/internal/responsecache.go index 8d53d00..01cee9c 100644 --- a/internal/responsecache.go +++ b/internal/responsecache.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/responsecache_test.go b/internal/responsecache_test.go index 8e22e98..1aa5b60 100644 --- a/internal/responsecache_test.go +++ b/internal/responsecache_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/responsestorerer.go b/internal/responsestorerer.go index d442a01..87dd953 100644 --- a/internal/responsestorerer.go +++ b/internal/responsestorerer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/responsestorerer_test.go b/internal/responsestorerer_test.go index 72ff02a..dbfa566 100644 --- a/internal/responsestorerer_test.go +++ b/internal/responsestorerer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 78dc65a..dbcc20e 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,7 +19,6 @@ import ( "cmp" "errors" "reflect" - "testing" ) var ErrSample = errors.New("an error") @@ -30,32 +29,37 @@ type T interface { Fatalf(format string, args ...interface{}) } +type testFunc func(format string, args ...interface{}) + +func (tf testFunc) do(fallback string, msgAndArgs ...interface{}) { + if len(msgAndArgs) > 0 { + if format, ok := msgAndArgs[0].(string); ok && len(msgAndArgs) > 1 { + tf(format, msgAndArgs[1:]...) + } else { + tf("%v", msgAndArgs...) + } + } else { + tf(fallback) + } +} + func assert(t T, condition bool, msgAndArgs ...interface{}) bool { t.Helper() - //nolint:nestif // Acceptable for readability if !condition { - if len(msgAndArgs) > 0 { - if format, ok := msgAndArgs[0].(string); ok && len(msgAndArgs) > 1 { - t.Errorf(format, msgAndArgs[1:]...) - } else { - t.Errorf("%v", msgAndArgs...) - } - } else { - t.Errorf("assert failed") - } + testFunc(t.Errorf).do("assert failed", msgAndArgs...) return false } return true } -func require(t *testing.T, condition bool, msgAndArgs ...interface{}) { +func require(t T, condition bool, msgAndArgs ...interface{}) { t.Helper() if !condition { - t.Fatalf("require failed: %s", msgAndArgs) + testFunc(t.Fatalf).do("require failed", msgAndArgs...) } } -func AssertEqual[T cmp.Ordered](t *testing.T, expected, actual T, msgAndArgs ...interface{}) bool { +func AssertEqual[V cmp.Ordered](t T, expected, actual V, msgAndArgs ...interface{}) bool { t.Helper() got := cmp.Compare(expected, actual) return assert( @@ -68,35 +72,35 @@ func AssertEqual[T cmp.Ordered](t *testing.T, expected, actual T, msgAndArgs ... ) } -func AssertTrue(t *testing.T, condition bool, msgAndArgs ...interface{}) bool { +func AssertTrue(t T, condition bool, msgAndArgs ...interface{}) bool { t.Helper() return assert(t, condition, "assertTrue failed: condition is false, %s", msgAndArgs) } -func hasError(t *testing.T, err error) bool { +func hasError(t T, err error) bool { t.Helper() return err != nil } -func RequireError(t *testing.T, err error, msgAndArgs ...interface{}) { +func RequireError(t T, err error, msgAndArgs ...interface{}) { t.Helper() got := hasError(t, err) require(t, got, "requireError failed: expected error, got nil, %s", msgAndArgs) } -func RequireNoError(t *testing.T, err error, msgAndArgs ...interface{}) { +func RequireNoError(t T, err error, msgAndArgs ...interface{}) { t.Helper() got := hasError(t, err) require(t, !got, "requireNoError failed: expected no error, got %v, %s", err, msgAndArgs) } -func RequireErrorIs(t *testing.T, err error, target error, msgAndArgs ...interface{}) { +func RequireErrorIs(t T, err error, target error, msgAndArgs ...interface{}) { t.Helper() got := errors.Is(err, target) require(t, got, "requireErrorIs failed: expected error %v, got %v, %s", target, err, msgAndArgs) } -func RequireErrorAs(t *testing.T, err error, target interface{}, msgAndArgs ...interface{}) { +func RequireErrorAs(t T, err error, target interface{}, msgAndArgs ...interface{}) { t.Helper() got := errors.As(err, target) require( @@ -109,7 +113,7 @@ func RequireErrorAs(t *testing.T, err error, target interface{}, msgAndArgs ...i ) } -func RequireTrue(t *testing.T, condition bool, msgAndArgs ...interface{}) { +func RequireTrue(t T, condition bool, msgAndArgs ...interface{}) { t.Helper() require( t, @@ -140,7 +144,7 @@ func isNil(object interface{}) bool { return false } -func AssertNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) bool { +func AssertNil(t T, object interface{}, msgAndArgs ...interface{}) bool { t.Helper() got := isNil(object) return assert( @@ -152,7 +156,7 @@ func AssertNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) bool ) } -func AssertNotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) bool { +func AssertNotNil(t T, object interface{}, msgAndArgs ...interface{}) bool { t.Helper() got := !isNil(object) return assert( @@ -163,7 +167,7 @@ func AssertNotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) b ) } -func RequireNotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) { +func RequireNotNil(t T, object interface{}, msgAndArgs ...interface{}) { t.Helper() got := isNil(object) require( @@ -174,7 +178,7 @@ func RequireNotNil(t *testing.T, object interface{}, msgAndArgs ...interface{}) ) } -func RequirePanics(t *testing.T, f func(), msgAndArgs ...interface{}) bool { +func RequirePanics(t T, f func(), msgAndArgs ...interface{}) bool { t.Helper() defer func() { got := recover() diff --git a/internal/testutil/testutil_test.go b/internal/testutil/testutil_test.go index 73c91ad..2c9a792 100644 --- a/internal/testutil/testutil_test.go +++ b/internal/testutil/testutil_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/urlkeyer.go b/internal/urlkeyer.go index ec801f6..e311d60 100644 --- a/internal/urlkeyer.go +++ b/internal/urlkeyer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package internal import ( "net/url" - "strings" - "unicode" + + "github.com/bartventer/httpcache/pkg/urlkey" ) // URLKeyer describes the interface implemented by types that can generate a @@ -32,123 +32,4 @@ func (f URLKeyerFunc) URLKey(u *url.URL) string { return f(u) } -func NewURLKeyer() URLKeyer { return URLKeyerFunc(makeURLKey) } - -// makeURLKey returns a normalized URL string suitable for use as a cache key. -// This helps ensure that URLs which are semantically the same but differ in minor ways -// (such as case, percent-encoding, or default ports) will map to the same cache entry. -// -// Reasons for normalization: -// - Go's http.Request.URL is parsed but not fully normalized. -// - URLs may use uppercase percent-encodings (%7E vs %7e). -// - Default ports (:80 for HTTP, :443 for HTTPS) may be included unnecessarily. -// - Hostnames may use different case (EXAMPLE.com). -// - Paths may be empty. -// - Dot-segments like /a/./b/../c may appear in paths. -// - The Go standard library does not guarantee normalization for all these cases. -// -// References: -// - RFC 3986 §6.2: https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 -// - RFC 7230 §2.7.3: https://datatracker.ietf.org/doc/html/rfc7230#section-2.7.3 -func makeURLKey(u *url.URL) string { - if u.Opaque != "" { - return u.Opaque - } - // RFC 3986 §6.2.2.3: Path normalization (dot-segment removal) is handled by - // [url.URL.ResolveReference], which uses the RFC 3986 §5.2.4 algorithm. - base, _ := url.Parse(u.Scheme + "://" + u.Host) - normalized := base.ResolveReference(u) - - // RFC 3986 §6.2.2.1: Scheme is lowercased (already done by [url.Parse]). - scheme := normalized.Scheme - - host, port := splitHostPort(normalized.Host) - defaultP := defaultPort(scheme) - if port == "" { - port = defaultP - } - // RFC 3986 §6.2.2.1: Host is lowercased. - hostPort := strings.ToLower(host) - - // RFC 3986 §6.2.3: Only include port if it is non-default for the scheme. - if port != "" && port != defaultP { - hostPort = hostPort + ":" + port - } - - // RFC 3986 §6.2.3: An empty path for http/https is normalized to "/". - // Also see https://datatracker.ietf.org/doc/html/rfc7230#section-2.7.3 - path := normalized.EscapedPath() - if path == "" && (scheme == "http" || scheme == "https") { - path = "/" - } - - // RFC 3986 §6.2.2.2: Normalize percent-encoding in path. - path = normalizePercentEncoding(path) - result := scheme + "://" + hostPort + path - - // RFC 3986 §6.2.2.2: Normalize percent-encoding in query, if present. - if normalized.RawQuery != "" { - result += "?" + normalizePercentEncoding(normalized.RawQuery) - } - - // RFC 3986 §6.1 Equivalence: "fragment components (if any) should be excluded from - // the comparison" - return result -} - -// normalizePercentEncoding rewrites percent-encoded characters in a URL path or query -// so that unreserved characters are decoded, and all hex digits are uppercase. -// Follows RFC 3986 §6.2.2.2. -func normalizePercentEncoding(s string) string { - var b strings.Builder - i := 0 - for i < len(s) { - if s[i] == '%' && i+2 < len(s) && - isHexDigit(s[i+1]) && isHexDigit(s[i+2]) { - hexVal := fromHex(s[i+1])<<4 | fromHex(s[i+2]) - r := rune(hexVal) - if isUnreserved(r) { - b.WriteRune(r) - } else { - b.WriteString(percentEncodeUpper(hexVal)) - } - i += 3 - } else { - b.WriteByte(s[i]) - i++ - } - } - return b.String() -} - -func isHexDigit(c byte) bool { - return ('0' <= c && c <= '9') || - ('A' <= c && c <= 'F') || - ('a' <= c && c <= 'f') -} - -func fromHex(c byte) byte { - switch { - case '0' <= c && c <= '9': - return c - '0' - case 'a' <= c && c <= 'f': - return c - 'a' + 10 - case 'A' <= c && c <= 'F': - return c - 'A' + 10 - } - return 0 -} - -// isUnreserved reports whether r is an unreserved character per RFC 3986 §2.3. -func isUnreserved(r rune) bool { - return unicode.IsLetter(r) || unicode.IsDigit(r) || - r == '-' || r == '.' || r == '_' || r == '~' -} - -const hex = "0123456789ABCDEF" - -// percentEncodeUpper returns the percent-encoded form of b using uppercase -// hex digits as specified in RFC 3986 §2.1. -func percentEncodeUpper(b byte) string { - return "%" + string(hex[b>>4]) + string(hex[b&0x0F]) -} +func NewURLKeyer() URLKeyer { return URLKeyerFunc(urlkey.FromURL) } diff --git a/internal/urlutil/urlutil.go b/internal/urlutil/urlutil.go new file mode 100644 index 0000000..ba629c0 --- /dev/null +++ b/internal/urlutil/urlutil.go @@ -0,0 +1,52 @@ +// Package urlutil provides utility functions for URL manipulation. +package urlutil + +import "strings" + +func DefaultPort(scheme string) string { + switch scheme { + case "http": + return "80" + case "https": + return "443" + default: + return "" + } +} + +// SplitHostPort separates host and port. If the port is not valid, it returns +// the entire input as host, and it doesn't check the validity of the host. +// Unlike net.SplitHostPort, but per RFC 3986, it requires ports to be numeric. +// +// From Go's net/url package. +// Copyright 2009 The Go Authors. All rights reserved. +func SplitHostPort(hostPort string) (host, port string) { + host = hostPort + colon := strings.LastIndexByte(host, ':') + if colon != -1 && validOptionalPort(host[colon:]) { + host, port = host[:colon], host[colon+1:] + } + if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") { + host = host[1 : len(host)-1] + } + return +} + +// validOptionalPort reports whether port is either an empty string or matches "/^:\d*$/". +// +// From Go's net/url package. +// Copyright 2009 The Go Authors. All rights reserved. +func validOptionalPort(port string) bool { + if port == "" { + return true + } + if port[0] != ':' { + return false + } + for _, b := range port[1:] { + if b < '0' || b > '9' { + return false + } + } + return true +} diff --git a/internal/validationresponsehandler.go b/internal/validationresponsehandler.go index 04227ee..7c4d48b 100644 --- a/internal/validationresponsehandler.go +++ b/internal/validationresponsehandler.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/validationresponsehandler_test.go b/internal/validationresponsehandler_test.go index 7189242..7c3c6a7 100644 --- a/internal/validationresponsehandler_test.go +++ b/internal/validationresponsehandler_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/varymatcher.go b/internal/varymatcher.go index c42c5f5..4bf0577 100644 --- a/internal/varymatcher.go +++ b/internal/varymatcher.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,7 +21,7 @@ import ( ) // VaryMatcher defines the interface implemented by types that can match -// request headers nominated by the a cached response's Vary header against +// request headers nominated by a cached response's Vary header against // the headers of an incoming request. type VaryMatcher interface { VaryHeadersMatch(cachedHdrs ResponseRefs, reqHdr http.Header) (int, bool) diff --git a/internal/varymatcher_test.go b/internal/varymatcher_test.go index 2e8f44d..e22eb4b 100644 --- a/internal/varymatcher_test.go +++ b/internal/varymatcher_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/options.go b/options.go index 225f86a..a0232c1 100644 --- a/options.go +++ b/options.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/example_test.go b/pkg/urlkey/example_test.go similarity index 71% rename from internal/example_test.go rename to pkg/urlkey/example_test.go index f907efb..53a7a70 100644 --- a/internal/example_test.go +++ b/pkg/urlkey/example_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,23 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package urlkey_test import ( "fmt" "net/url" + + "github.com/bartventer/httpcache/pkg/urlkey" ) -func Example_makeURLKey() { +func ExampleFromURL() { u, err := url.Parse( "https://example.com:8443/abc?query=param&another=value#fragment=part1&part2", ) if err != nil { - fmt.Println("Error parsing URL:", err) - return + panic(err) } - cacheKey := makeURLKey(u) - fmt.Println("Cache Key:", cacheKey) + key := urlkey.FromURL(u) + fmt.Println(key) // Output: - // Cache Key: https://example.com:8443/abc?query=param&another=value + // https://example.com:8443/abc?query=param&another=value } diff --git a/pkg/urlkey/urlkey.go b/pkg/urlkey/urlkey.go new file mode 100644 index 0000000..589bbec --- /dev/null +++ b/pkg/urlkey/urlkey.go @@ -0,0 +1,141 @@ +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package urlkey derives stable cache-key strings from URLs by applying +// pragmatic HTTP URL normalization. +// +// The normalization follows RFC 3986 equivalence guidance and HTTP URI rules +// where applicable ([RFC 3986 §6.2], [RFC 7230 §2.7.3]). It is intended for cache +// key generation, not as a full URI canonicalization framework. +// +// [RFC 3986 §6.2]: https://datatracker.ietf.org/doc/html/rfc3986#section-6.2 +// [RFC 7230 §2.7.3]: https://datatracker.ietf.org/doc/html/rfc7230#section-2.7.3 +package urlkey + +import ( + "net/url" + "strings" + "unicode" + + "github.com/bartventer/httpcache/internal/urlutil" +) + +// FromURL returns a normalized URL string suitable for use as a cache key. +// +// It normalizes scheme/host case, default ports, dot-segments, and +// percent-encoding (for path and query), and excludes fragments. +// +// For opaque URLs (u.Opaque != ""), the opaque value is returned unchanged. +func FromURL(u *url.URL) string { + if u.Opaque != "" { + return u.Opaque + } + // RFC 3986 §6.2.2.3: Path normalization (dot-segment removal) is handled by + // [url.URL.ResolveReference], which uses the RFC 3986 §5.2.4 algorithm. + base, _ := url.Parse(u.Scheme + "://" + u.Host) + normalized := base.ResolveReference(u) + + // RFC 3986 §6.2.2.1: Scheme is lowercased (already done by [url.Parse]). + scheme := normalized.Scheme + + host, port := urlutil.SplitHostPort(normalized.Host) + defaultP := urlutil.DefaultPort(scheme) + if port == "" { + port = defaultP + } + // RFC 3986 §6.2.2.1: Host is lowercased. + hostPort := strings.ToLower(host) + + // RFC 3986 §6.2.3: Only include port if it is non-default for the scheme. + if port != "" && port != defaultP { + hostPort = hostPort + ":" + port + } + + // RFC 3986 §6.2.3: An empty path for http/https is normalized to "/". + // Also see https://datatracker.ietf.org/doc/html/rfc7230#section-2.7.3 + path := normalized.EscapedPath() + if path == "" && (scheme == "http" || scheme == "https") { + path = "/" + } + + // RFC 3986 §6.2.2.2: Normalize percent-encoding in path. + path = normalizePercentEncoding(path) + result := scheme + "://" + hostPort + path + + // RFC 3986 §6.2.2.2: Normalize percent-encoding in query, if present. + if normalized.RawQuery != "" { + result += "?" + normalizePercentEncoding(normalized.RawQuery) + } + + // RFC 3986 §6.1 Equivalence: "fragment components (if any) should be excluded from + // the comparison" + return result +} + +// normalizePercentEncoding rewrites percent-encoded characters in a URL path or query +// so that unreserved characters are decoded, and all hex digits are uppercase. +// Follows RFC 3986 §6.2.2.2. +func normalizePercentEncoding(s string) string { + var b strings.Builder + i := 0 + for i < len(s) { + if s[i] == '%' && i+2 < len(s) && + isHexDigit(s[i+1]) && isHexDigit(s[i+2]) { + hexVal := fromHex(s[i+1])<<4 | fromHex(s[i+2]) + r := rune(hexVal) + if isUnreserved(r) { + b.WriteRune(r) + } else { + b.WriteString(percentEncodeUpper(hexVal)) + } + i += 3 + } else { + b.WriteByte(s[i]) + i++ + } + } + return b.String() +} + +func isHexDigit(c byte) bool { + return ('0' <= c && c <= '9') || + ('A' <= c && c <= 'F') || + ('a' <= c && c <= 'f') +} + +func fromHex(c byte) byte { + switch { + case '0' <= c && c <= '9': + return c - '0' + case 'a' <= c && c <= 'f': + return c - 'a' + 10 + case 'A' <= c && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +// isUnreserved reports whether r is an unreserved character per RFC 3986 §2.3. +func isUnreserved(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || + r == '-' || r == '.' || r == '_' || r == '~' +} + +const hex = "0123456789ABCDEF" + +// percentEncodeUpper returns the percent-encoded form of b using uppercase +// hex digits as specified in RFC 3986 §2.1. +func percentEncodeUpper(b byte) string { + return "%" + string(hex[b>>4]) + string(hex[b&0x0F]) +} diff --git a/internal/urlkeyer_test.go b/pkg/urlkey/urlkey_test.go similarity index 94% rename from internal/urlkeyer_test.go rename to pkg/urlkey/urlkey_test.go index 7dd8159..f6201f3 100644 --- a/internal/urlkeyer_test.go +++ b/pkg/urlkey/urlkey_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package internal +package urlkey import ( "net/url" @@ -21,7 +21,7 @@ import ( "github.com/bartventer/httpcache/internal/testutil" ) -func Test_makeKey(t *testing.T) { +func TestFromURL(t *testing.T) { tests := []struct { raw string expected string @@ -80,6 +80,6 @@ func Test_makeKey(t *testing.T) { t.Errorf("url.Parse(%q) failed: %v", tt.raw, err) continue } - testutil.AssertEqual(t, makeURLKey(u), tt.expected, "makeURLKey(%q)", tt.raw) + testutil.AssertEqual(t, FromURL(u), tt.expected, "FromURL(%q)", tt.raw) } } diff --git a/roundtripper.go b/roundtripper.go index a353f0d..bb10c36 100644 --- a/roundtripper.go +++ b/roundtripper.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/roundtripper_test.go b/roundtripper_test.go index 7543431..976b0db 100644 --- a/roundtripper_test.go +++ b/roundtripper_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/acceptance/acceptance.go b/store/acceptance/acceptance.go index 8de36b7..b847e29 100644 --- a/store/acceptance/acceptance.go +++ b/store/acceptance/acceptance.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/cache.go b/store/cache.go index 545f273..5c272d2 100644 --- a/store/cache.go +++ b/store/cache.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/driver/driver.go b/store/driver/driver.go index 0b37c56..be2c370 100644 --- a/store/driver/driver.go +++ b/store/driver/driver.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/expapi/expapi.go b/store/expapi/expapi.go index cc4cfb7..d14517d 100644 --- a/store/expapi/expapi.go +++ b/store/expapi/expapi.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -108,6 +108,7 @@ func retrieve(conn driver.Conn) http.Handler { } w.Header().Set("Content-Type", "application/octet-stream") w.WriteHeader(http.StatusOK) + //nolint:gosec // (G705) Intended: serve binary cache bytes for debugging. if _, err := w.Write(value); err != nil { http.Error( w, diff --git a/store/expapi/expapi_test.go b/store/expapi/expapi_test.go index fc73b59..dee646d 100644 --- a/store/expapi/expapi_test.go +++ b/store/expapi/expapi_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/encrypt.go b/store/fscache/encrypt.go index 8831da2..0ff6d5b 100644 --- a/store/fscache/encrypt.go +++ b/store/fscache/encrypt.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/encrypt_test.go b/store/fscache/encrypt_test.go index 52d9883..f01f3c3 100644 --- a/store/fscache/encrypt_test.go +++ b/store/fscache/encrypt_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/filenamer.go b/store/fscache/filenamer.go index 0c7b577..1d125f0 100644 --- a/store/fscache/filenamer.go +++ b/store/fscache/filenamer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/filenamer_test.go b/store/fscache/filenamer_test.go index be158ce..428b6bd 100644 --- a/store/fscache/filenamer_test.go +++ b/store/fscache/filenamer_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/fscache.go b/store/fscache/fscache.go index fad3d34..2a97bf1 100644 --- a/store/fscache/fscache.go +++ b/store/fscache/fscache.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/fscache_test.go b/store/fscache/fscache_test.go index 6bafeae..6473cbc 100644 --- a/store/fscache/fscache_test.go +++ b/store/fscache/fscache_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/fscache/issue16_test.go b/store/fscache/issue16_test.go index 476393d..e0edbcd 100644 --- a/store/fscache/issue16_test.go +++ b/store/fscache/issue16_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/internal/registry/registry.go b/store/internal/registry/registry.go index ca0bb17..2995423 100644 --- a/store/internal/registry/registry.go +++ b/store/internal/registry/registry.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/internal/registry/registry_test.go b/store/internal/registry/registry_test.go index f22edd6..6511296 100644 --- a/store/internal/registry/registry_test.go +++ b/store/internal/registry/registry_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/memcache/memcache.go b/store/memcache/memcache.go index eb41ee8..b94b891 100644 --- a/store/memcache/memcache.go +++ b/store/memcache/memcache.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/store/memcache/memcache_test.go b/store/memcache/memcache_test.go index 06007c0..29f0435 100644 --- a/store/memcache/memcache_test.go +++ b/store/memcache/memcache_test.go @@ -1,4 +1,4 @@ -// Copyright (c) 2025 Bart Venter +// Copyright (c) 2026 Bart Venter <72999113+bartventer@users.noreply.github.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License.