Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/default.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3
make lint
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ linters:
goheader:
values:
const:
AUTHOR: "Bart Venter <bartventer@proton.me>"
AUTHOR: "Bart Venter <72999113+bartventer@users.noreply.github.com>"
template: |-
Copyright (c) {{ YEAR }} {{ AUTHOR }}

Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"--output.text.path=stdout",
"--output.text.print-issued-lines=false",
"--show-stats=false",
"--disable goheader",
"--fix"
],
"editor.detectIndentation": true
Expand Down
2 changes: 1 addition & 1 deletion context.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
module github.com/bartventer/httpcache

go 1.25

toolchain go1.26.1
2 changes: 1 addition & 1 deletion helpers.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion helpers_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/cacheabilityevaluator.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/cacheabilityevaluator_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/cacheinvalidator.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/cacheinvalidator_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
28 changes: 11 additions & 17 deletions internal/ccdirectives.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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) {
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions internal/ccdirectives_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/clock.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/entry.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/entry_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/freshness.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/freshness_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/header.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
73 changes: 16 additions & 57 deletions internal/helpers.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand All @@ -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()) &&
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/log.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
2 changes: 1 addition & 1 deletion internal/mocks.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2025 Bart Venter <bartventer@proton.me>
// 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.
Expand Down
Loading
Loading