diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 68a6acf..88ad1cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ proxy/ │ │ └── queries.go # CRUD operations │ ├── storage/ # Artifact file storage │ │ ├── storage.go # Storage interface -│ │ └── blob.go # gocloud.dev/blob backends (file, S3, Azure) +│ │ └── filesystem.go # Local filesystem impl │ ├── upstream/ # Upstream registry clients │ │ ├── fetcher.go # HTTP artifact fetching │ │ └── resolver.go # Download URL resolution @@ -72,7 +72,7 @@ Key types: ### `internal/storage` -Artifact file storage abstraction backed by `gocloud.dev/blob`. Supports local filesystem (`file://`), S3 (`s3://`), and Azure (`azblob://`) URLs. +Artifact file storage abstraction. Currently implements local filesystem storage. Designed to allow future backends (S3, GCS). Interface: ```go diff --git a/README.md b/README.md index 320a737..5fdd668 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Resolution order: package override, then ecosystem override, then global default | Conda | Python/R | Yes | ✓ | | CRAN | R | | ✓ | | Julia | Julia | | ✓ | +| Swift | Swift | | ✓ | | Container | Docker/OCI | | ✓ | | Debian | Debian/Ubuntu | | ✓ | | RPM | RHEL/Fedora | | ✓ | @@ -47,7 +48,6 @@ Resolution order: package override, then ecosystem override, then global default | Chef | Chef | | ✗ | | Generic | Any | | ✗ | | Helm | Kubernetes | | ✗ | -| Swift | Swift | | ✗ | | Vagrant | Vagrant | | ✗ | Cooldown requires publish timestamps in metadata. Registries without a "Yes" in the cooldown column either don't expose timestamps or haven't been wired up yet. @@ -340,6 +340,25 @@ ENV["JULIA_PKG_SERVER"] = "http://localhost:8080/julia" using Pkg; Pkg.update() ``` +### Swift + +Configure the proxy as the default registry for the current Swift package: + +```bash +swift package-registry set --allow-insecure-http http://localhost:8080/swift +``` + +Registry dependencies use their scoped package identifier in `Package.swift`: + +```swift +dependencies: [ + .package(id: "apple.swift-argument-parser", from: "1.2.0") +] +``` + +The proxy supports dependency resolution and source downloads. Publishing with +`swift package-registry publish` is not supported. + ### Docker / Container Registry Configure Docker to use the proxy as a registry mirror in `/etc/docker/daemon.json`: @@ -473,6 +492,7 @@ PROXY_DATABASE_URL=postgres://user:pass@localhost/proxy?sslmode=disable PROXY_LOG_LEVEL=info PROXY_LOG_FORMAT=text PROXY_ACCESS_LOG_PATH=/var/log/proxy/access.jsonl +PROXY_UPSTREAM_SWIFT=https://tuist.dev/api/registry/swift ``` ### Configuration File @@ -500,6 +520,7 @@ access_log: upstream: npm: "https://registry.npmjs.org" cargo: "https://index.crates.io" + swift: "https://tuist.dev/api/registry/swift" # Optional: version cooldown (see above) cooldown: @@ -669,6 +690,7 @@ Recently cached: | `GET /conda/*` | Conda/Anaconda protocol | | `GET /cran/*` | CRAN (R) protocol | | `GET /julia/*` | Julia Pkg server protocol | +| `GET /swift/*` | Swift Package Registry v1 protocol | | `GET /helm/{repository}/*` | HTTP Helm chart repository protocol | | `GET /v2/*` | OCI/Docker registry protocol | | `GET /debian/*` | Debian/APT repository protocol | diff --git a/cmd/proxy/main.go b/cmd/proxy/main.go index c5549ad..9db230d 100644 --- a/cmd/proxy/main.go +++ b/cmd/proxy/main.go @@ -208,6 +208,7 @@ func runServe() { fmt.Fprintf(os.Stderr, " PROXY_ACCESS_LOG_PATH JSONL access log path\n") fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_MAVEN Maven repository upstream URL\n") fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL Gradle Plugin Portal upstream URL\n") + fmt.Fprintf(os.Stderr, " PROXY_UPSTREAM_SWIFT Swift Package Registry upstream URL\n") fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_READ_ONLY Disable Gradle PUT uploads\n") fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE Max Gradle PUT request body size\n") fmt.Fprintf(os.Stderr, " PROXY_GRADLE_BUILD_CACHE_MAX_AGE Gradle cache max age eviction\n") @@ -275,10 +276,7 @@ func runServe() { logger := setupLogger(cfg.Log.Level, cfg.Log.Format) // Create and start server - srv, err := server.New(cfg, logger, server.BuildInfo{ - Version: Version, - Commit: Commit, - }) + srv, err := server.New(cfg, logger) if err != nil { logger.Error("failed to create server", "error", err) os.Exit(1) diff --git a/config.example.yaml b/config.example.yaml index 1df95b3..b5c4c1f 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -99,6 +99,9 @@ upstream: # Cargo crate download URL cargo_download: "https://static.crates.io/crates" + # Swift Package Registry URL (used by /swift endpoint) + swift: "https://tuist.dev/api/registry/swift" + # Debian/APT repository URL (used by /debian endpoint) debian: "http://deb.debian.org/debian" diff --git a/docs/architecture.md b/docs/architecture.md index 6d9bfda..cc070ba 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -269,6 +269,10 @@ HTTP protocol handlers for each registry type. - `handleIndex()` - Proxy sparse index - `handleDownload()` - Serve cached crate +**SwiftHandler:** +- Proxies the Swift Package Registry v1 read endpoints +- Rewrites release URLs and caches source archives + ### `internal/server` HTTP server setup, web UI, and API handlers. diff --git a/docs/configuration.md b/docs/configuration.md index 3b8b935..2506347 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -143,6 +143,7 @@ upstream: gradle_plugin_portal: "https://plugins.gradle.org/m2" cargo: "https://index.crates.io" cargo_download: "https://static.crates.io/crates" + swift: "https://tuist.dev/api/registry/swift" # Named HTTP Helm chart repositories, served at /helm/{name}/. helm: diff --git a/go.mod b/go.mod index d95ee13..beb444e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/git-pkgs/proxy -go 1.26.0 +go 1.25.6 toolchain go1.26.6 @@ -13,7 +13,7 @@ require ( github.com/git-pkgs/integrity v0.1.1 github.com/git-pkgs/magic v0.2.0 github.com/git-pkgs/purl v0.1.17 - github.com/git-pkgs/registries v0.8.1 + github.com/git-pkgs/registries v0.7.0 github.com/git-pkgs/spdx v0.3.1 github.com/git-pkgs/vers v0.3.1 github.com/git-pkgs/vulns v0.2.2 @@ -32,48 +32,45 @@ require ( ) require ( - 4d63.com/gocheckcompilerdirectives v1.4.0 // indirect + 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect - charm.land/lipgloss/v2 v2.0.6 // indirect - cloud.google.com/go/auth v0.21.0 // indirect + cloud.google.com/go/auth v0.18.2 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect - dev.gaijin.team/go/exhaustruct/v5 v5.0.3 // indirect - dev.gaijin.team/go/golib v0.8.1 // indirect + dev.gaijin.team/go/golib v0.6.0 // indirect github.com/4meepo/tagalign v1.4.3 // indirect - github.com/Abirdcfly/dupword v0.1.8 // indirect + github.com/Abirdcfly/dupword v0.1.7 // indirect github.com/AdminBenni/iota-mixing v1.0.0 // indirect - github.com/AlwxSin/noinlineerr v1.0.6 // indirect - github.com/Antonboom/errname v1.1.2 // indirect - github.com/Antonboom/nilnil v1.1.2 // indirect + github.com/AlwxSin/noinlineerr v1.0.5 // indirect + github.com/Antonboom/errname v1.1.1 // indirect + github.com/Antonboom/nilnil v1.1.1 // indirect github.com/Antonboom/testifylint v1.6.4 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0 // indirect - github.com/ClickHouse/clickhouse-go-linter v1.2.1 // indirect github.com/Djarvur/go-err113 v0.1.1 // indirect github.com/KyleBanks/depth v1.2.1 // indirect - github.com/Masterminds/semver/v3 v3.5.0 // indirect - github.com/MirrexOne/unqueryvet v1.5.4 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/MirrexOne/unqueryvet v1.5.3 // indirect github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect - github.com/alecthomas/chroma/v2 v2.27.0 // indirect + github.com/alecthomas/chroma/v2 v2.23.1 // indirect github.com/alecthomas/go-check-sumtype v0.3.1 // indirect github.com/alexkohler/nakedret/v2 v2.0.6 // indirect - github.com/alexkohler/prealloc v1.1.0 // indirect + github.com/alexkohler/prealloc v1.0.2 // indirect github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect github.com/anchore/go-struct-converter v0.1.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/ashanbrown/forbidigo/v2 v2.3.1 // indirect - github.com/ashanbrown/makezero/v2 v2.2.1 // indirect + github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect + github.com/ashanbrown/makezero/v2 v2.1.0 // indirect github.com/aws/aws-sdk-go-v2 v1.41.9 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 // indirect github.com/aws/aws-sdk-go-v2/config v1.32.20 // indirect @@ -93,49 +90,48 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.2 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 // indirect github.com/aws/smithy-go v1.26.0 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bkielbasa/cyclop v1.2.3 // indirect github.com/blizzy78/varnamelen v0.8.0 // indirect github.com/bombsimon/wsl/v4 v4.7.0 // indirect - github.com/bombsimon/wsl/v5 v5.9.0 // indirect + github.com/bombsimon/wsl/v5 v5.6.0 // indirect github.com/breml/bidichk v0.3.3 // indirect github.com/breml/errchkjson v0.4.1 // indirect - github.com/butuzov/ireturn v0.4.1 // indirect - github.com/butuzov/mirror v1.3.3 // indirect + github.com/butuzov/ireturn v0.4.0 // indirect + github.com/butuzov/mirror v1.3.0 // indirect github.com/catenacyber/perfsprint v0.10.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect github.com/cenk/backoff v2.2.1+incompatible // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charithe/durationcheck v0.0.11 // indirect - github.com/charmbracelet/colorprofile v0.4.3 // indirect - github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 // indirect - github.com/charmbracelet/x/ansi v0.11.8 // indirect - github.com/charmbracelet/x/term v0.2.2 // indirect - github.com/charmbracelet/x/termios v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.2.2 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/lipgloss v1.1.0 // indirect + github.com/charmbracelet/x/ansi v0.10.1 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect - github.com/clipperhouse/displaywidth v0.11.0 // indirect - github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect - github.com/dlclark/regexp2/v2 v2.2.1 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ecosyste-ms/ecosystems-go v0.4.0 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect - github.com/fatih/color v1.19.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect - github.com/firefart/nonamedreturns v1.0.8 // indirect + github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.21 // indirect + github.com/ghostiam/protogetter v0.3.20 // indirect github.com/git-pkgs/packageurl-go v0.3.1 // indirect github.com/git-pkgs/pom v0.1.5 // indirect github.com/github/go-spdx/v2 v2.7.0 // indirect - github.com/go-critic/go-critic v0.14.4 // indirect + github.com/go-critic/go-critic v0.14.3 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect @@ -156,82 +152,83 @@ require ( github.com/gofrs/flock v0.13.0 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/golangci/asciicheck v0.5.0 // indirect - github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 // indirect + github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.1 // indirect - github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 // indirect - github.com/golangci/golangci-lint/v2 v2.13.1 // indirect + github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect + github.com/golangci/golangci-lint/v2 v2.10.1 // indirect github.com/golangci/golines v0.15.0 // indirect github.com/golangci/misspell v0.8.0 // indirect github.com/golangci/plugin-module-register v0.1.2 // indirect github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/google/wire v0.7.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect - github.com/googleapis/gax-go/v2 v2.23.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/gordonklaus/ineffassign v0.2.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.2 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.9.0 // indirect + github.com/hashicorp/go-version v1.8.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jgautheron/goconst v1.11.0 // indirect + github.com/jgautheron/goconst v1.8.2 // indirect + github.com/jingyugao/rowserrcheck v1.1.1 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect - github.com/kisielk/errcheck v1.20.0 // indirect + github.com/kisielk/errcheck v1.9.0 // indirect github.com/kkHAIKE/contextcheck v1.1.6 // indirect github.com/kulti/thelper v0.7.1 // indirect github.com/kunwardeep/paralleltest v1.0.15 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lasiar/canonicalheader v1.1.2 // indirect github.com/ldez/exptostd v0.4.5 // indirect - github.com/ldez/gomoddirectives v0.9.0 // indirect + github.com/ldez/gomoddirectives v0.8.0 // indirect github.com/ldez/grignotin v0.10.1 // indirect github.com/ldez/structtags v0.6.1 // indirect github.com/ldez/tagliatelle v0.7.2 // indirect github.com/ldez/usetesting v0.5.0 // indirect github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lucasb-eyer/go-colorful v1.4.1 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/macabu/inamedparam v0.2.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect - github.com/manuelarte/funcorder v0.6.0 // indirect + github.com/manuelarte/funcorder v0.5.0 // indirect github.com/maratori/testableexamples v1.0.1 // indirect github.com/maratori/testpackage v1.1.2 // indirect github.com/matoous/godox v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.15 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.24 // indirect - github.com/mattn/go-runewidth v0.0.24 // indirect - github.com/mgechev/revive v1.15.0 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mgechev/revive v1.14.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moricho/tparallel v0.3.2 // indirect - github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nakabonne/nestif v0.3.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.24.0 // indirect + github.com/nunnatsa/ginkgolinter v0.23.0 // indirect github.com/oapi-codegen/nullable v1.2.0 // indirect github.com/oapi-codegen/runtime v1.6.0 // indirect github.com/package-url/packageurl-go v0.1.6 // indirect github.com/pandatix/go-cvss v0.6.2 // indirect github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.70.1 // indirect github.com/prometheus/procfs v0.21.1 // indirect github.com/quasilyte/go-ruleguard v0.4.5 // indirect @@ -239,25 +236,24 @@ require ( github.com/quasilyte/gogrep v0.5.0 // indirect github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/raeperd/recvcheck v0.3.0 // indirect + github.com/raeperd/recvcheck v0.2.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.16.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 // indirect github.com/rubyist/circuitbreaker v2.2.1+incompatible // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect - github.com/ryancurrah/gomodguard/v2 v2.1.3 // indirect - github.com/ryanrolds/sqlclosecheck v0.6.0 // indirect + github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sashamelentyev/interfacebloat v1.1.0 // indirect github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect - github.com/securego/gosec/v2 v2.28.0 // indirect - github.com/sirupsen/logrus v1.10.1 // indirect + github.com/securego/gosec/v2 v2.23.0 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sonatard/noctx v0.5.1 // indirect - github.com/sourcegraph/go-diff v0.8.0 // indirect + github.com/sonatard/noctx v0.4.0 // indirect + github.com/sourcegraph/go-diff v0.7.0 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.10.2 // indirect @@ -266,11 +262,11 @@ require ( github.com/spf13/viper v1.12.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stretchr/objx v0.5.3 // indirect - github.com/stretchr/testify v1.12.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/stretchr/testify v1.11.1 // indirect github.com/subosito/gotenv v1.4.1 // indirect - github.com/tetafro/godot v1.5.6 // indirect - github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 // indirect + github.com/tetafro/godot v1.5.4 // indirect + github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect @@ -278,8 +274,8 @@ require ( github.com/ultraware/funlen v0.2.0 // indirect github.com/ultraware/whitespace v0.2.0 // indirect github.com/urfave/cli/v2 v2.3.0 // indirect - github.com/uudashr/gocognit v1.2.1 // indirect - github.com/uudashr/iface v1.5.0 // indirect + github.com/uudashr/gocognit v1.2.0 // indirect + github.com/uudashr/iface v1.4.1 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect @@ -287,9 +283,9 @@ require ( github.com/ykadowak/zerologlint v0.1.5 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect - go-simpler.org/sloglint v0.12.0 // indirect + go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.4.0 // indirect - go.augendre.info/fatcontext v0.10.0 // indirect + go.augendre.info/fatcontext v0.9.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel v1.44.0 // indirect go.opentelemetry.io/otel/metric v1.44.0 // indirect @@ -299,28 +295,28 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect - go.yaml.in/yaml/v3 v3.0.5 // indirect - golang.org/x/crypto v0.55.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.54.0 // indirect golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect - golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/net v0.58.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.57.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect - golang.org/x/tools v0.49.0 // indirect + golang.org/x/text v0.40.0 // indirect + golang.org/x/tools v0.47.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/api v0.288.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 // indirect + google.golang.org/api v0.272.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect google.golang.org/grpc v1.82.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - honnef.co/go/tools v0.8.0 // indirect + honnef.co/go/tools v0.7.0 // indirect modernc.org/libc v1.74.4 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect - mvdan.cc/gofumpt v0.11.0 // indirect - mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673 // indirect + mvdan.cc/gofumpt v0.9.2 // indirect + mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index e2fedc7..e8efd91 100644 --- a/go.sum +++ b/go.sum @@ -1,15 +1,13 @@ -4d63.com/gocheckcompilerdirectives v1.4.0 h1:ZLq62rbGWVmQhiZ8kuNVIT/M09xCSTdJz9K3xOdT/CY= -4d63.com/gocheckcompilerdirectives v1.4.0/go.mod h1:9ZOAiMOjqC/nRwci2fcUXVHUNLG/cH6r6rhUh+jTFtQ= +4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= +4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= -charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= -charm.land/lipgloss/v2 v2.0.6/go.mod h1:ipDDJNSGa1hlwDtSfW1s2/xR8Vdhbut4PXh2zEKZd0Q= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.21.0 h1:g/QwYfYb2Ai6HH8oomAOyBaIHLbscZ4+T/F/f5JZHkE= -cloud.google.com/go/auth v0.21.0/go.mod h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s= +cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= +cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -26,25 +24,23 @@ codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= -dev.gaijin.team/go/exhaustruct/v5 v5.0.3 h1:yOeA7DNjlT8y4yfmN6nWWYYggA13N523YAj9/TXbuTM= -dev.gaijin.team/go/exhaustruct/v5 v5.0.3/go.mod h1:KwtBsX8nHHH1YxhxkpiBq6bfsmw5WnazWpNvJPHgY9Y= -dev.gaijin.team/go/golib v0.8.1 h1:JYju4x9BSo+QD/AYeHULVDcvEhiFg8wOi6pT0IaZF5E= -dev.gaijin.team/go/golib v0.8.1/go.mod h1:c5fu7t1RSGMxSQgcUYO1sODbzsYnOCXJLmHeNG1Eb+0= +dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= +dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= -github.com/Abirdcfly/dupword v0.1.8 h1:SrhcUuGsROBuChFxHALRYzyyPODWn9zwghmzPvD9Cd8= -github.com/Abirdcfly/dupword v0.1.8/go.mod h1:XZrhVnI7YGpsTiWZANSQaBJ4QpL/Tq5vIEdKJJAs9WI= +github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= +github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= -github.com/AlwxSin/noinlineerr v1.0.6 h1:KAvuxunTe9QxvqrFB7nZTdb/7Wzas4AvifslTnG0Ld8= -github.com/AlwxSin/noinlineerr v1.0.6/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= -github.com/Antonboom/errname v1.1.2 h1:dxwONZJua3VB8Xh/VaCjqAcqF645sWWv7xj26zy7tdQ= -github.com/Antonboom/errname v1.1.2/go.mod h1:YeZIpgLMxT+SNkruGgYkLhzq/9vs3fsolTZegKaKDZI= -github.com/Antonboom/nilnil v1.1.2 h1:aNlFuJhaEseXe4fHO3xbjXlSeEiQVYa2lEkWD2s2hAY= -github.com/Antonboom/nilnil v1.1.2/go.mod h1:0ynwvphOLmAuMwTNDyBnDZmSwZoDpcFXmUHmzoHH2WA= +github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= +github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= +github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= +github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= +github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= +github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= @@ -66,8 +62,6 @@ github.com/AzureAD/microsoft-authentication-library-for-go v1.7.0/go.mod h1:HKpQ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/ClickHouse/clickhouse-go-linter v1.2.1 h1:zGEKIyd5YL08ieWG/LOUmlau2DxbxPVOfAeo+4Jz3ck= -github.com/ClickHouse/clickhouse-go-linter v1.2.1/go.mod h1:pLorS7ffPTfuUV9M0SJgfHA/h/WQPQUk2FWG9x74cQ4= github.com/CycloneDX/cyclonedx-go v0.11.0 h1:GokP8FiRC+foiuwWhSSLpSD5H4hSWtGnR3wo7apkBFI= github.com/CycloneDX/cyclonedx-go v0.11.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= @@ -80,10 +74,10 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= -github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= -github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/MirrexOne/unqueryvet v1.5.4 h1:38QOxShO7JmMWT+eCdDMbcUgGCOeJphVkzzRgyLJgsQ= -github.com/MirrexOne/unqueryvet v1.5.4/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/MirrexOne/unqueryvet v1.5.3 h1:LpT3rsH+IY3cQddWF9bg4C7jsbASdGnrOSofY8IPEiw= +github.com/MirrexOne/unqueryvet v1.5.3/go.mod h1:fs9Zq6eh1LRIhsDIsxf9PONVUjYdFHdtkHIgZdJnyPU= github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -93,16 +87,16 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= -github.com/alecthomas/chroma/v2 v2.27.0 h1:FodwmyOBgJULFYmDqibcp9pvfDLWdtPRh9v/r5BXYZs= -github.com/alecthomas/chroma/v2 v2.27.0/go.mod h1:NjJ3ciIgrqBNeIkWZ4e46nseoLDslxU1LmfCoL+wcY8= +github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY= +github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= -github.com/alexkohler/prealloc v1.1.0 h1:cKGRBqlXw5iyQGLYhrXrDlcHxugXpTq4tQ5c91wkf8M= -github.com/alexkohler/prealloc v1.1.0/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= +github.com/alexkohler/prealloc v1.0.2 h1:MPo8cIkGkZytq7WNH9UHv3DIX1mPz1RatPXnZb0zHWQ= +github.com/alexkohler/prealloc v1.0.2/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= @@ -113,10 +107,10 @@ github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9 github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= -github.com/ashanbrown/forbidigo/v2 v2.3.1 h1:KAZijvQ7zeIBKbhikT4jCm0TLYXC4u78bTiLh/8JROI= -github.com/ashanbrown/forbidigo/v2 v2.3.1/go.mod h1:2QDkLTzU6TV937eFROamXrW92M3paehdae4HCDCOZCM= -github.com/ashanbrown/makezero/v2 v2.2.1 h1:A7uU8dgB1PA9aelTxHMfHIQ8Qev8AB3JLxJUBUsejqM= -github.com/ashanbrown/makezero/v2 v2.2.1/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= +github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= +github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= +github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= +github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= github.com/aws/aws-sdk-go-v2 v1.41.9 h1:/rYeyO2+HrMztAmxAq9++XJtFMqSIpSsNA0yDGALYq4= github.com/aws/aws-sdk-go-v2 v1.41.9/go.mod h1:+HsoOEX80qAVUitj1A2DhCNTjmb3edVyuDypb6LNEeo= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.11 h1:h5+3VT69KUBK24grGuuA5saDJTj2IIjLb9au668Fo5I= @@ -155,6 +149,8 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.42.3 h1:ErklX/7uhSbkAAeyQD/Y1OoQ9hO3 github.com/aws/aws-sdk-go-v2/service/sts v1.42.3/go.mod h1:ULe4HCzfKPiR6R3HEurE3b1upEkuk8AkMrOKtaOxKO8= github.com/aws/smithy-go v1.26.0 h1:9ouqbi+NyKP7fV3Te7UElCwdAb6Y8uk7LGwPE5tVe/s= github.com/aws/smithy-go v1.26.0/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= @@ -164,18 +160,18 @@ github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkAp github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= -github.com/bombsimon/wsl/v5 v5.9.0 h1:WCrgZ7RQnZO5oEwbVTlYgBdU3wL294kR1BSWV8vTfsU= -github.com/bombsimon/wsl/v5 v5.9.0/go.mod h1:kjo4HiAV5FDkHC8/uzJq9mBffEEd6WT/nvN7DoMovDM= +github.com/bombsimon/wsl/v5 v5.6.0 h1:4z+/sBqC5vUmSp1O0mS+czxwH9+LKXtCWtHH9rZGQL8= +github.com/bombsimon/wsl/v5 v5.6.0/go.mod h1:Uqt2EfrMj2NV8UGoN1f1Y3m0NpUVCsUdrNCdet+8LvU= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= -github.com/butuzov/ireturn v0.4.1 h1:vWb3NO4t77iku/sjCQ/2pHTQeOmxEhjIriJqRLg1Y+I= -github.com/butuzov/ireturn v0.4.1/go.mod h1:q+DXKzTDV5guNuXLnIab9fKXizTn2miZHLhxH7V/GB4= -github.com/butuzov/mirror v1.3.3 h1:v0RsWBhfFc1RQqE/f3sHpSttKDtodFn0gFmtYyD4/hA= -github.com/butuzov/mirror v1.3.3/go.mod h1:h9BzzwYnTiHO0GzgvaTqIg7VSsOUhdIv51cHFFBmX1w= +github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= +github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= +github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= +github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= @@ -186,24 +182,18 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= -github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q= -github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q= -github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886 h1:rdnVWKgJpTVXKuKuJyxDJ+NFJdUaUqGvyGy61OcvlbA= -github.com/charmbracelet/ultraviolet v0.0.0-20260811164956-006e29f97886/go.mod h1:nAw0d9PhFp1qdzi2xhQU5YOu5sVpDIHWlaW2Uz/bCro= -github.com/charmbracelet/x/ansi v0.11.8 h1:JMFwp0CgDC2+jcOB162HH5k7I3FVbgFSMMYg7dSPBQQ= -github.com/charmbracelet/x/ansi v0.11.8/go.mod h1:ZNN+3mXny/516oTQPLMPIBeSINvNJJQ8uQXDgbeJxY0= -github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= -github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= -github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= -github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= -github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM= -github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ= +github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= +github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= -github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= -github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= -github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -219,15 +209,12 @@ github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEy github.com/dave/jennifer v1.7.1 h1:B4jJJDHelWcDhlRQxWeo0Npa/pYKBLrirAQoTN45txo= github.com/dave/jennifer v1.7.1/go.mod h1:nXbxhEmQfOZhWml3D1cDK5M1FLnMSozpbFN/m3RmGZc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8= -github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dlclark/regexp2/v2 v2.2.1 h1:mf4KkFUj0gJuarK8P+LgiS+Lit7m9N1yAwEfPbee7R0= -github.com/dlclark/regexp2/v2 v2.2.1/go.mod h1:avUrQvPaLz2DrFNHJF0taWAFFX2C1GMSSoeiqFjcBmU= +github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/ecosyste-ms/ecosystems-go v0.4.0 h1:5A+zF+XWT8sYYYjlc2/tI1SmiDGzbHLyT9CapVc5dGA= @@ -241,22 +228,22 @@ github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= -github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= -github.com/fatih/color v1.19.0/go.mod h1:zNk67I0ZUT1bEGsSGyCZYZNrHuTkJJB+r6Q9VuMi0LE= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/felixge/httpsnoop v1.1.0 h1:3YtUj32ZZkqZtt3sZZsClsymw/QDuVfpNhoA31zeORc= -github.com/felixge/httpsnoop v1.1.0/go.mod h1:Zqxgdd+1Rkcz8euOqdr7lqgCRJztwr5hp9vDSi5UZCE= -github.com/firefart/nonamedreturns v1.0.8 h1:iB32Dl17zJl1zlVEj/WlUWgx0HiRyQ85OUw1WHa4/II= -github.com/firefart/nonamedreturns v1.0.8/go.mod h1:vxFNvm5AfP/8rgAKFzYmnqx0yp1HjrYsErZ9pHPTznA= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= +github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.21 h1:EeWTGvL/Eyosp653hiWb6Byx4b69iJC4/E+za7vQHoI= -github.com/ghostiam/protogetter v0.3.21/go.mod h1:iAKSpyoHwYzay+OpjoWgwzRtPFthEfuUvmlomTThck0= +github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0= +github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= github.com/git-pkgs/archives v0.5.1 h1:qwu/vsoerQZF1iysRtfcxpy1KIUSJJSpXJ5JNxzNoQw= github.com/git-pkgs/archives v0.5.1/go.mod h1:AKpkxnts49R9uAt1mL2ULYcHrmYujCDVu24IsFvW9so= github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w= @@ -273,8 +260,8 @@ github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY= github.com/git-pkgs/pom v0.1.5/go.mod h1:ufdMBe1lKzqOeP9IUb9NPZ458xKV8E8NvuyBMxOfwIk= github.com/git-pkgs/purl v0.1.17 h1:oRSd8tqllTLl74Wa4WnuqU500hXd9OdUnImOEswQUVE= github.com/git-pkgs/purl v0.1.17/go.mod h1:7u7ora8tQdrkS7Auclr5v8dCJdjN4ej6AbrvYZi2b7k= -github.com/git-pkgs/registries v0.8.1 h1:Yf2FFdARQ1HcdtZfWBYa5OZFwZHzhFYStiz7qbTDDUU= -github.com/git-pkgs/registries v0.8.1/go.mod h1:5dc3V7rOhAI5755L/bDtjtYV4D5XV4J/4ZtyIXSEs0U= +github.com/git-pkgs/registries v0.7.0 h1:+LbOOMHbvjmXGfsi88hcGH+SfTXYsXA3UY5KYI5mB7s= +github.com/git-pkgs/registries v0.7.0/go.mod h1:VCD4q+ZW0fInopzseg9rAmBEL553R2JQe60UHXtv26w= github.com/git-pkgs/spdx v0.3.1 h1:58JPY5X9pYpXvnzzZIgehItlBykeOOw52pNc4OBcS+c= github.com/git-pkgs/spdx v0.3.1/go.mod h1:cqRoZcvl530s/W+oGNvwjt4ODN8T1W6D/20MUZEFdto= github.com/git-pkgs/vers v0.3.1 h1:jy/ht2wIRJI5zQrccm6GTeYr+hGFwe2z8LV1HOr4Wco= @@ -285,8 +272,8 @@ github.com/github/go-spdx/v2 v2.7.0 h1:GzfXx4wFdlilARxmFRXW/mgUy3A4vSqZocCMFV6XF github.com/github/go-spdx/v2 v2.7.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= -github.com/go-critic/go-critic v0.14.4 h1:dSX4C3pWSeuMVxvQh6yG8U0ReSf3YOmKi4nwX5q7n/8= -github.com/go-critic/go-critic v0.14.4/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= +github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= +github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA= github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -304,8 +291,8 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-quicktest/qt v1.102.0 h1:HSQxCeh5YZH3EL3W39ixjtyaEhcWSXQHtHnMBzSs474= -github.com/go-quicktest/qt v1.102.0/go.mod h1:p4lGIVX+8Wa6ZPNDvqcxq36XpUDLh42FLetFU7odllI= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= @@ -346,14 +333,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= -github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202 h1:CbTB8KpqnViI6lIXxp03Oclc4VFHi3K4BWC1TacsZ+A= -github.com/golangci/dupl v0.0.0-20260401084720-c99c5cf5c202/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= +github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= -github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792 h1:WL8YKrt3UbOBqSRU7GpP5BTtQTMWtVtj+mfPijgZeIg= -github.com/golangci/gofmt v0.0.0-20260820135601-e84e05053792/go.mod h1:te5hX0dW4C5r6YbXs+6ysNr8Q5UTmdIqGbb+mlFiYmA= -github.com/golangci/golangci-lint/v2 v2.13.1 h1:RuM4OcluM4xFQcGuRE6R7jA33pqxK/W1EsBxpugdZjg= -github.com/golangci/golangci-lint/v2 v2.13.1/go.mod h1:HwX7mDzqHbcSxlhrTygjX1GJbAfQ3sJAqOx41qQlhDE= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= +github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= +github.com/golangci/golangci-lint/v2 v2.10.1 h1:flhw5Px6ojbLyEFzXvJn5B2HEdkkRlkhE1SnmCbQBiE= +github.com/golangci/golangci-lint/v2 v2.10.1/go.mod h1:dBsrOk6zj0vDhlTv+IiJGqkDokR24IVTS7W3EVfPTQY= github.com/golangci/golines v0.15.0 h1:Qnph25g8Y1c5fdo1X7GaRDGgnMHgnxh4Gk4VfPTtRx0= github.com/golangci/golines v0.15.0/go.mod h1:AZjXd23tbHMpowhtnGlj9KCNsysj72aeZVVHnVcZx10= github.com/golangci/misspell v0.8.0 h1:qvxQhiE2/5z+BVRo1kwYA8yGz+lOlu5Jfvtx2b04Jbg= @@ -362,8 +349,6 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3H github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba h1:lqtcnSMDuuJdu/LrKWi5RJzpSNLOJXYe/nzQutTI5kg= -github.com/golangci/rowserrcheck v0.0.0-20260419091836-c5f79b8a11ba/go.mod h1:sCBNcpRmhJCtbFGz49+IM3ETTFf7QdJ30AeYCd43NKk= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= @@ -387,10 +372,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18= -github.com/googleapis/enterprise-certificate-proxy v0.3.18 h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k= -github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k= -github.com/googleapis/gax-go/v2 v2.23.0 h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE= -github.com/googleapis/gax-go/v2 v2.23.0/go.mod h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= +github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= @@ -410,8 +395,8 @@ github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1T github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.9.0 h1:CeOIz6k+LoN3qX9Z0tyQrPtiB1DFYRPfCIBtaXPSCnA= -github.com/hashicorp/go-version v1.9.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -420,8 +405,10 @@ github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUq github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jgautheron/goconst v1.11.0 h1:KgN90z5qXt5f0Uzf3cWXev3hfMMFUyNeKdpkSBRvLDk= -github.com/jgautheron/goconst v1.11.0/go.mod h1:0p+wv1lFOiUr0IlNNT1nrm6+8DB8u2sU6KHGzFRXHDc= +github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= +github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= +github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= +github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= @@ -435,8 +422,8 @@ github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhE github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= -github.com/kisielk/errcheck v1.20.0 h1:9rwHBNKzd4wkDWcROy3DvFGNqEPlkxBg305rvk7HabI= -github.com/kisielk/errcheck v1.20.0/go.mod h1:O+f80MKNwX8Oor2jwgpeQ9An7uJm+hRSgT+h22knRJU= +github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= +github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk= @@ -458,8 +445,8 @@ github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0 github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= -github.com/ldez/gomoddirectives v0.9.0 h1:2YV/EX7nVlWL4jySusYTzBKHuE3D2fgcRsQuMa3yIoo= -github.com/ldez/gomoddirectives v0.9.0/go.mod h1:DdZzfm9MdXCjn2/UGYXCFfo+tzrp2Ib4iD2Q0kIJkwE= +github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= +github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= @@ -473,8 +460,8 @@ github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFB github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= -github.com/lucasb-eyer/go-colorful v1.4.1 h1:1EO+WB73+EH8EVbzlrG3KLAfEypQWVHIBqlTf+2hNss= -github.com/lucasb-eyer/go-colorful v1.4.1/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= @@ -486,8 +473,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= -github.com/manuelarte/funcorder v0.6.0 h1:0hBngc4fa1IgNiI65A7sFGkMvoMCc878RjqB5V7rWP0= -github.com/manuelarte/funcorder v0.6.0/go.mod h1:id3NDhXdQBmeqXH7eVC6Z89xS6JxvZ8kF9xUxpArU/g= +github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= +github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= @@ -496,24 +483,24 @@ github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= -github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI= github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= -github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/ay2DU= -github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= -github.com/mgechev/revive v1.15.0 h1:vJ0HzSBzfNyPbHKolgiFjHxLek9KUijhqh42yGoqZ8Q= -github.com/mgechev/revive v1.15.0/go.mod h1:LlAKO3QQe9OJ0pVZzI2GPa8CbXGZ/9lNpCGvK4T/a8A= +github.com/mgechev/revive v1.14.0 h1:CC2Ulb3kV7JFYt+izwORoS3VT/+Plb8BvslI/l1yZsc= +github.com/mgechev/revive v1.14.0/go.mod h1:MvnujelCZBZCaoDv5B3foPo6WWgULSSFxvfxp7GsPfo= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= -github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= -github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= @@ -525,16 +512,16 @@ github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhK github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.24.0 h1:Mp0EagluLFP98JatP6nqp/gGEoljNG97uf9AcxcBVy8= -github.com/nunnatsa/ginkgolinter v0.24.0/go.mod h1:2ZMRuzX6+3XXyY6UZOwb6n+MCocVGbkIsDBC4vuWz5c= +github.com/nunnatsa/ginkgolinter v0.23.0 h1:x3o4DGYOWbBMP/VdNQKgSj+25aJKx2Pe6lHr8gBcgf8= +github.com/nunnatsa/ginkgolinter v0.23.0/go.mod h1:9qN1+0akwXEccwV1CAcCDfcoBlWXHB+ML9884pL4SZ4= github.com/oapi-codegen/nullable v1.2.0 h1:VflFkDW980KhBPiFF7nWSyjg+r4Obqj8lXipV0UkP5w= github.com/oapi-codegen/nullable v1.2.0/go.mod h1:KUZ3vUzkmEKY90ksAmit2+5juDIhIZhfDl+0PwOQlFY= github.com/oapi-codegen/runtime v1.6.0 h1:7Xx+GlueD6nRuyKoCPzL434Jfi3BetbiJOrzCHp/VPU= github.com/oapi-codegen/runtime v1.6.0/go.mod h1:GwV7hC2hviaMzj+ITfHVRESK5J2W/GefVwIND/bMGvU= -github.com/onsi/ginkgo/v2 v2.32.0 h1:Hw7s2pVrQo/8Yz5N77qdnpHaoc+c6cC9WIV1Jce+J6E= -github.com/onsi/ginkgo/v2 v2.32.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= -github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= -github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI= +github.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= github.com/otiai10/copy v1.14.0 h1:dCI/t1iTdYGtkvCuBG2BgR6KZa83PTclw4U5n2wAllU= github.com/otiai10/copy v1.14.0/go.mod h1:ECfuL02W+/FkTWZWgQqXPWZgW9oeKCSQ5qVfSc4qc4w= @@ -548,17 +535,16 @@ github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITG github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= -github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea h1:sKwxy1H95npauwu8vtF95vG/syrL0p8fSZo/XlDg5gk= github.com/peterbourgon/g2s v0.0.0-20170223122336-d4e7ad98afea/go.mod h1:1VcHEd3ro4QMoHfiNl/j7Jkln9+KQuorp0PItHMJYNg= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.24.1 h1:JnJkREXzWxUdCuPFpIWZiPispT9xVV59uiuyR2bPlnU= github.com/prometheus/client_golang v1.24.1/go.mod h1:F+oSRECHg4sse5ucfYpYDeIv/hu68Zo0uoHKetWnzcE= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= @@ -577,14 +563,15 @@ github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/raeperd/recvcheck v0.3.0 h1:PM+XYvyxIj3bo+kobJfFTdTuU3Lmfu96mKDbyHDbRt8= -github.com/raeperd/recvcheck v0.3.0/go.mod h1:PZNwG+HztFYMH2ZPq0Hu3QgkV2yiA6VrtNz9c1fXWJo= +github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= +github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= -github.com/rogpeppe/go-internal v1.16.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529 h1:18kd+8ZUlt/ARXhljq+14TwAoKa61q6dX8jtwOf6DH8= github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= github.com/rubyist/circuitbreaker v2.2.1+incompatible h1:KUKd/pV8Geg77+8LNDwdow6rVCAYOp8+kHUyFvL6Mhk= @@ -594,31 +581,31 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= -github.com/ryancurrah/gomodguard/v2 v2.1.3 h1:E7sz3PJwE9Ba1reVxSpF6XLCPJZ74Kfw/LabTNM4GIA= -github.com/ryancurrah/gomodguard/v2 v2.1.3/go.mod h1:CQicdLGatWMxLX53JzoBjYlsNZhHbmLv2AVa0s2aivU= -github.com/ryanrolds/sqlclosecheck v0.6.0 h1:pEyL9okISdg1F1SEpJNlrEotkTGerv5BMk7U4AG0eVg= -github.com/ryanrolds/sqlclosecheck v0.6.0/go.mod h1:xyX16hsDaCMXHrMJ3JMzGf5OpDfHTOTTQrT7HOFUmeU= +github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= +github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= -github.com/securego/gosec/v2 v2.28.0 h1:ZsSdiDb0AtTpLFVol5z91gbMei9ZiLEPG/pZjZujp7c= -github.com/securego/gosec/v2 v2.28.0/go.mod h1:lb4/9AHe+lJy/kjWmWRWWsEipvbwGKuxf+tY1Pmjdnk= +github.com/securego/gosec/v2 v2.23.0 h1:h4TtF64qFzvnkqvsHC/knT7YC5fqyOCItlVR8+ptEBo= +github.com/securego/gosec/v2 v2.23.0/go.mod h1:qRHEgXLFuYUDkI2T7W7NJAmOkxVhkR0x9xyHOIcMNZ0= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.10.1 h1:xi4336Zh11WpU14fXR6I67V3yaTPQYwRx2WEtHbRg4Q= -github.com/sirupsen/logrus v1.10.1/go.mod h1:vsQHnG7xzNsxk3NrwboUiWPnIC3dmbjcGPykD7+tiHk= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sonatard/noctx v0.5.1 h1:wklWg9c9ZYugOAk7qG4yP4PBrlQsmSLPTvW1K4PRQMs= -github.com/sonatard/noctx v0.5.1/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= -github.com/sourcegraph/go-diff v0.8.0 h1:ipIyu4cTsLbIrln4l0qtHA3r0a7gyK4ntKjtQytHhvY= -github.com/sourcegraph/go-diff v0.8.0/go.mod h1:hWlcO7Al+UZStZAP8rBumHpCK5ZHQ5BXsMls8p4+F5E= +github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o= +github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= +github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= +github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= @@ -643,14 +630,14 @@ github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRk github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= -github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= -github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= @@ -661,10 +648,10 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= -github.com/tetafro/godot v1.5.6 h1:IEkrFCwXaYHlOn4mGzGS3F3dkP6m9t0jpwqBFPIkKiA= -github.com/tetafro/godot v1.5.6/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4 h1:SiHe5XLTn9sFWJ5pBwJ5FN/4j34q9ZlOAD//kMoMYp0= -github.com/timakin/bodyclose v0.0.0-20260129054331-73d1f95b84b4/go.mod h1:sDHLK7rb/59v/ZxZ7KtymgcoxuUMxjXq8gtu9VMOK8M= +github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= +github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= +github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= @@ -679,10 +666,10 @@ github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSW github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/uudashr/gocognit v1.2.1 h1:CSJynt5txTnORn/DkhiB4mZjwPuifyASC8/6Q0I/QS4= -github.com/uudashr/gocognit v1.2.1/go.mod h1:acaubQc6xYlXFEMb9nWX2dYBzJ/bIjEkc1zzvyIZg5Q= -github.com/uudashr/iface v1.5.0 h1:PgdMt4uAettGG8K/Kbamc4B9FABgUgnS3TLbl6fnjEk= -github.com/uudashr/iface v1.5.0/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= +github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= +github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= +github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -702,6 +689,7 @@ github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= @@ -710,20 +698,20 @@ go-simpler.org/assert v0.9.0 h1:PfpmcSvL7yAnWyChSjOz6Sp6m9j5lyK8Ok9pEL31YkQ= go-simpler.org/assert v0.9.0/go.mod h1:74Eqh5eI6vCK6Y5l3PI8ZYFXG4Sa+tkr70OIPJAUr28= go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= -go-simpler.org/sloglint v0.12.0 h1:UzWDlLWNE5FLqsvyq3tWYHuQMbqrervOhT8qPl4Mmw4= -go-simpler.org/sloglint v0.12.0/go.mod h1:jBjjC2bm8rYrs88oTRlFX497kWjJsyZWYoNaXkGRI6I= +go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= +go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= -go.augendre.info/fatcontext v0.10.0 h1:HhFopmivh8U1+AU7f0kuwUeg2eiIns7YsGQOMHwSJ90= -go.augendre.info/fatcontext v0.10.0/go.mod h1:pqpGvA9GlrXy+aXkp8L2dKz12Zp4g2FhzcAtwToU+2w= +go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= +go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= @@ -744,43 +732,51 @@ go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= -go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q= gocloud.dev v0.46.0/go.mod h1:ACQe+2qO+hEO+pdcvvsM+RB63r8TyGD1W3ESCLFyzvM= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f h1:+lI8cDJ4uceLipg2f1ODay7fEuLkk0BIHXd6PB8icxo= -golang.org/x/exp/typeparams v0.0.0-20260811152304-ee035b5b010f/go.mod h1:PqrXSW65cXDZH0k4IeUbhmg/bcAZDbzNz3byBpKCsXo= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -790,6 +786,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -798,8 +796,10 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -807,19 +807,27 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -828,11 +836,14 @@ golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWc golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= -golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= @@ -845,14 +856,14 @@ golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhS golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.288.0 h1:glhO/J88obKP5I269W3hB73dvBKrjU56ZfmNlNXpgTU= -google.golang.org/api v0.288.0/go.mod h1:lM2kYRzYUCBY91P9h6VF1PYmvhxii3O5hji37qRvIcY= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= -google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7 h1:jQ9p21COKWjP3VwuFrNRiiOTMh3mPpN45R7SLrH/HUU= -google.golang.org/genproto/googleapis/api v0.0.0-20260630182238-925bb5da69e7/go.mod h1:KqHwBx2upmfa1XSi1WuRvC+2VGCLtooKkfmyvRbUmqA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= +google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5 h1:JNfk58HZ8lfmXbYK2vx/UvsqIL59TzByCxPIX4TDmsE= +google.golang.org/genproto v0.0.0-20260316180232-0b37fe3546d5/go.mod h1:x5julN69+ED4PcFk/XWayw35O0lf/nGa4aNgODCmNmw= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= @@ -872,8 +883,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.8.0 h1:UacpzPr7D6i5BAjTkA7sNVcx4kIbhAZcQ4zYtKiXx68= -honnef.co/go/tools v0.8.0/go.mod h1:XA+OnlRA9EDh/ukGvXMNSZNKGwFQJ+5dER0ioUkOxks= +honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= +honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI= modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU= @@ -902,9 +913,9 @@ modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -mvdan.cc/gofumpt v0.11.0 h1:0H01XB95PnN2QgCSR9ELdZyTlJqNZ7181B0BTMh5VZc= -mvdan.cc/gofumpt v0.11.0/go.mod h1:BeT5wCsOJt6J9zT2MZIOGszjUHzFkn1/l9g6xAzqsXo= -mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673 h1:dEE6li4OPIE54oojY2qaayFS1fSp17G14si0gXRxl0U= -mvdan.cc/unparam v0.0.0-20260818115549-3f964bcb5673/go.mod h1:62roFV3D3nYOWIXv3PfGO4UYEKAotz2WgLywT87ONd8= +mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= +mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= +mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/config/config.go b/internal/config/config.go index a3dfbc6..f33853c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -63,6 +63,9 @@ import ( "gopkg.in/yaml.v3" ) +// DefaultSwiftUpstream is the Swift Package Registry used when none is configured. +const DefaultSwiftUpstream = "https://tuist.dev/api/registry/swift" + // Config holds all configuration for the proxy server. type Config struct { // Listen is the address to listen on (e.g., ":8080", "127.0.0.1:8080"). @@ -313,6 +316,10 @@ type UpstreamConfig struct { // Default: https://static.crates.io/crates CargoDownload string `json:"cargo_download" yaml:"cargo_download"` + // Swift is the upstream Swift Package Registry URL. + // Default: https://tuist.dev/api/registry/swift + Swift string `json:"swift" yaml:"swift"` + // Debian is the upstream APT repository base URL. // Example: http://archive.ubuntu.com/ubuntu would get Ubuntu. // Default: http://deb.debian.org/debian @@ -475,6 +482,7 @@ func Default() *Config { GradlePluginPortal: "https://plugins.gradle.org/m2", Cargo: "https://index.crates.io", CargoDownload: "https://static.crates.io/crates", + Swift: DefaultSwiftUpstream, Debian: "http://deb.debian.org/debian", }, Gradle: GradleConfig{ @@ -546,6 +554,7 @@ func setEnvBool(dst *bool, key string) { // - PROXY_LOG_LEVEL // - PROXY_LOG_FORMAT // - PROXY_ACCESS_LOG_PATH +// - PROXY_UPSTREAM_SWIFT // - PROXY_HEALTH_STORAGE_PROBE_INTERVAL func (c *Config) LoadFromEnv() { setEnvString(&c.Listen, "PROXY_LISTEN") @@ -565,6 +574,7 @@ func (c *Config) LoadFromEnv() { setEnvString(&c.AccessLog.Path, "PROXY_ACCESS_LOG_PATH") setEnvString(&c.Upstream.Maven, "PROXY_UPSTREAM_MAVEN") setEnvString(&c.Upstream.GradlePluginPortal, "PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL") + setEnvString(&c.Upstream.Swift, "PROXY_UPSTREAM_SWIFT") setEnvString(&c.Upstream.Debian, "PROXY_UPSTREAM_DEBIAN") setEnvString(&c.Cooldown.Default, "PROXY_COOLDOWN_DEFAULT") setEnvBool(&c.CacheMetadata, "PROXY_CACHE_METADATA") @@ -928,7 +938,8 @@ func ParseSize(s string) (int64, error) { } for _, s2 := range suffixes { - if numStr, ok := strings.CutSuffix(s, s2.suffix); ok { + if strings.HasSuffix(s, s2.suffix) { + numStr := strings.TrimSuffix(s, s2.suffix) num, err := strconv.ParseFloat(numStr, 64) if err != nil { return 0, fmt.Errorf("invalid number %q", numStr) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0ccc308..60c9cce 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -41,6 +41,9 @@ func TestDefault(t *testing.T) { if cfg.Upstream.GradlePluginPortal != "https://plugins.gradle.org/m2" { t.Errorf("Upstream.GradlePluginPortal = %q, want %q", cfg.Upstream.GradlePluginPortal, "https://plugins.gradle.org/m2") } + if cfg.Upstream.Swift != "https://tuist.dev/api/registry/swift" { + t.Errorf("Upstream.Swift = %q, want %q", cfg.Upstream.Swift, "https://tuist.dev/api/registry/swift") + } if cfg.Upstream.Debian != "http://deb.debian.org/debian" { t.Errorf("Upstream.Debian = %q, want %q", cfg.Upstream.Debian, "http://deb.debian.org/debian") } @@ -286,6 +289,7 @@ func TestLoadFromEnv(t *testing.T) { t.Setenv("PROXY_ACCESS_LOG_PATH", "/tmp/proxy-access.jsonl") t.Setenv("PROXY_UPSTREAM_MAVEN", "https://maven.example.com/repository/maven-public") t.Setenv("PROXY_UPSTREAM_GRADLE_PLUGIN_PORTAL", "https://plugins.example.com/m2") + t.Setenv("PROXY_UPSTREAM_SWIFT", "https://swift.example.com/registry") t.Setenv("PROXY_UPSTREAM_DEBIAN", "http://archive.ubuntu.com/ubuntu") t.Setenv("PROXY_GRADLE_BUILD_CACHE_READ_ONLY", "true") t.Setenv("PROXY_GRADLE_BUILD_CACHE_MAX_UPLOAD_SIZE", "32MB") @@ -319,6 +323,9 @@ func TestLoadFromEnv(t *testing.T) { if cfg.Upstream.GradlePluginPortal != "https://plugins.example.com/m2" { t.Errorf("Upstream.GradlePluginPortal = %q, want %q", cfg.Upstream.GradlePluginPortal, "https://plugins.example.com/m2") } + if cfg.Upstream.Swift != "https://swift.example.com/registry" { + t.Errorf("Upstream.Swift = %q, want %q", cfg.Upstream.Swift, "https://swift.example.com/registry") + } if cfg.Upstream.Debian != "http://archive.ubuntu.com/ubuntu" { t.Errorf("Upstream.Debian = %q, want %q", cfg.Upstream.Debian, "http://archive.ubuntu.com/ubuntu") } diff --git a/internal/enrichment/enrichment.go b/internal/enrichment/enrichment.go index 6b09db9..0c63b91 100644 --- a/internal/enrichment/enrichment.go +++ b/internal/enrichment/enrichment.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/git-pkgs/proxy/internal/packageurl" "github.com/git-pkgs/purl" "github.com/git-pkgs/registries" _ "github.com/git-pkgs/registries/all" // Import all registry implementations @@ -67,7 +68,10 @@ type VulnInfo struct { // EnrichPackage fetches metadata for a package from registry APIs. func (s *Service) EnrichPackage(ctx context.Context, ecosystem, name string) (*PackageInfo, error) { - purlStr := purl.MakePURLString(ecosystem, name, "") + purlStr := packageurl.MakeString(ecosystem, name, "") + if purlStr == "" { + return nil, nil + } pkg, err := registries.FetchPackageFromPURL(ctx, purlStr, s.regClient) if err != nil { @@ -102,7 +106,10 @@ func (s *Service) EnrichPackage(ctx context.Context, ecosystem, name string) (*P // EnrichVersion fetches metadata for a specific package version. func (s *Service) EnrichVersion(ctx context.Context, ecosystem, name, version string) (*VersionInfo, error) { - purlStr := purl.MakePURLString(ecosystem, name, version) + purlStr := packageurl.MakeString(ecosystem, name, version) + if purlStr == "" { + return nil, nil + } ver, err := registries.FetchVersionFromPURL(ctx, purlStr, s.regClient) if err != nil { @@ -134,9 +141,14 @@ func (s *Service) EnrichVersion(ctx context.Context, ecosystem, name, version st // BulkEnrichPackages fetches metadata for multiple packages in parallel. func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Ecosystem, Name string }) map[string]*PackageInfo { - purls := make([]string, len(packages)) - for i, pkg := range packages { - purls[i] = purl.MakePURLString(pkg.Ecosystem, pkg.Name, "") + purls := make([]string, 0, len(packages)) + for _, pkg := range packages { + if purlStr := packageurl.MakeString(pkg.Ecosystem, pkg.Name, ""); purlStr != "" { + purls = append(purls, purlStr) + } + } + if len(purls) == 0 { + return map[string]*PackageInfo{} } pkgData := registries.BulkFetchPackages(ctx, purls, s.regClient) @@ -147,7 +159,10 @@ func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Eco continue } - p, _ := purl.Parse(purlStr) + p, err := purl.Parse(purlStr) + if err != nil { + continue + } info := &PackageInfo{ Ecosystem: p.Type, Name: pkg.Name, @@ -174,7 +189,10 @@ func (s *Service) BulkEnrichPackages(ctx context.Context, packages []struct{ Eco // CheckVulnerabilities queries for vulnerabilities affecting a package version. func (s *Service) CheckVulnerabilities(ctx context.Context, ecosystem, name, version string) ([]VulnInfo, error) { - p := purl.MakePURL(ecosystem, name, version) + p := packageurl.Make(ecosystem, name, version) + if p == nil { + return nil, nil + } vulnList, err := s.vulnSource.Query(ctx, p) if err != nil { @@ -201,6 +219,50 @@ func (s *Service) CheckVulnerabilities(ctx context.Context, ecosystem, name, ver return results, nil } +// BulkCheckVulnerabilities queries vulnerabilities for multiple package versions. +func (s *Service) BulkCheckVulnerabilities(ctx context.Context, packages []struct{ Ecosystem, Name, Version string }) (map[string][]VulnInfo, error) { + purls := make([]*purl.PURL, 0, len(packages)) + supported := make([]int, 0, len(packages)) + for i, pkg := range packages { + if packagePURL := packageurl.Make(pkg.Ecosystem, pkg.Name, pkg.Version); packagePURL != nil { + purls = append(purls, packagePURL) + supported = append(supported, i) + } + } + result := make(map[string][]VulnInfo, len(purls)) + if len(purls) == 0 { + return result, nil + } + + vulnResults, err := s.vulnSource.QueryBatch(ctx, purls) + if err != nil { + return nil, err + } + + for i, vulnList := range vulnResults { + pkg := packages[supported[i]] + key := purls[i].String() + + var infos []VulnInfo + for _, v := range vulnList { + info := VulnInfo{ + ID: v.ID, + Summary: v.Summary, + Severity: v.SeverityLevel(), + CVSSScore: v.CVSSScore(), + FixedVersion: v.FixedVersion(pkg.Ecosystem, pkg.Name), + } + for _, ref := range v.References { + info.References = append(info.References, ref.URL) + } + infos = append(infos, info) + } + result[key] = infos + } + + return result, nil +} + // IsOutdated checks if a version is older than the latest version. func (s *Service) IsOutdated(currentVersion, latestVersion string) bool { if latestVersion == "" || currentVersion == "" { @@ -211,7 +273,10 @@ func (s *Service) IsOutdated(currentVersion, latestVersion string) bool { // GetLatestVersion fetches the latest version for a package. func (s *Service) GetLatestVersion(ctx context.Context, ecosystem, name string) (string, error) { - purlStr := purl.MakePURLString(ecosystem, name, "") + purlStr := packageurl.MakeString(ecosystem, name, "") + if purlStr == "" { + return "", nil + } latest, err := registries.FetchLatestVersionFromPURL(ctx, purlStr, s.regClient) if err != nil { @@ -251,6 +316,19 @@ func (s *Service) CategorizeLicense(license string) LicenseCategory { return LicenseUnknown } +// NormalizeLicense normalizes a license string to SPDX format. +func (s *Service) NormalizeLicense(license string) string { + if license == "" { + return "" + } + + if normalized, err := spdx.NormalizeExpressionLax(license); err == nil { + return normalized + } + + return license +} + // EnrichmentResult contains all enrichment data for a package version. type EnrichmentResult struct { Package *PackageInfo diff --git a/internal/enrichment/enrichment_test.go b/internal/enrichment/enrichment_test.go index e6a6dde..10113e8 100644 --- a/internal/enrichment/enrichment_test.go +++ b/internal/enrichment/enrichment_test.go @@ -1,11 +1,40 @@ package enrichment import ( + "context" "log/slog" "os" "testing" + + "github.com/git-pkgs/purl" + "github.com/git-pkgs/vulns" ) +type recordingVulnerabilitySource struct { + purls []*purl.PURL +} + +func (s *recordingVulnerabilitySource) Name() string { + return "recording" +} + +func (s *recordingVulnerabilitySource) Query(context.Context, *purl.PURL) ([]vulns.Vulnerability, error) { + return nil, nil +} + +func (s *recordingVulnerabilitySource) QueryBatch(_ context.Context, purls []*purl.PURL) ([][]vulns.Vulnerability, error) { + s.purls = purls + results := make([][]vulns.Vulnerability, len(purls)) + for i := range results { + results[i] = []vulns.Vulnerability{{ID: "TEST-1"}} + } + return results, nil +} + +func (s *recordingVulnerabilitySource) Get(context.Context, string) (*vulns.Vulnerability, error) { + return nil, nil +} + func TestNew(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) svc := New(logger) @@ -23,6 +52,68 @@ func TestNew(t *testing.T) { } } +func TestSwiftRegistryIdentitySkipsPURLDependentLookups(t *testing.T) { + svc := New(slog.New(slog.NewTextHandler(os.Stdout, nil))) + ctx := context.Background() + + packageInfo, err := svc.EnrichPackage(ctx, "swift", "apple/example") + if err != nil || packageInfo != nil { + t.Errorf("EnrichPackage() = %#v, %v; want nil, nil", packageInfo, err) + } + + versionInfo, err := svc.EnrichVersion(ctx, "swift", "apple/example", "1.2.3") + if err != nil || versionInfo != nil { + t.Errorf("EnrichVersion() = %#v, %v; want nil, nil", versionInfo, err) + } + + vulnerabilities, err := svc.CheckVulnerabilities(ctx, "swift", "apple/example", "1.2.3") + if err != nil || vulnerabilities != nil { + t.Errorf("CheckVulnerabilities() = %#v, %v; want nil, nil", vulnerabilities, err) + } + + latest, err := svc.GetLatestVersion(ctx, "swift", "apple/example") + if err != nil || latest != "" { + t.Errorf("GetLatestVersion() = %q, %v; want empty string, nil", latest, err) + } + + packages := []struct{ Ecosystem, Name string }{{Ecosystem: "swift", Name: "apple/example"}} + if got := svc.BulkEnrichPackages(ctx, packages); len(got) != 0 { + t.Errorf("BulkEnrichPackages() = %#v, want empty result", got) + } + + versions := []struct{ Ecosystem, Name, Version string }{ + {Ecosystem: "swift", Name: "apple/example", Version: "1.2.3"}, + } + got, err := svc.BulkCheckVulnerabilities(ctx, versions) + if err != nil || len(got) != 0 { + t.Errorf("BulkCheckVulnerabilities() = %#v, %v; want empty result, nil", got, err) + } +} + +func TestBulkCheckVulnerabilitiesFiltersUnsupportedPackageIdentities(t *testing.T) { + source := &recordingVulnerabilitySource{} + svc := New(slog.New(slog.NewTextHandler(os.Stdout, nil))) + svc.vulnSource = source + packages := []struct{ Ecosystem, Name, Version string }{ + {Ecosystem: "swift", Name: "apple/example", Version: "1.2.3"}, + {Ecosystem: "npm", Name: "lodash", Version: "4.17.21"}, + } + + got, err := svc.BulkCheckVulnerabilities(context.Background(), packages) + if err != nil { + t.Fatalf("BulkCheckVulnerabilities() error = %v", err) + } + if len(source.purls) != 1 || source.purls[0].String() != "pkg:npm/lodash@4.17.21" { + t.Fatalf("queried PURLs = %#v, want only lodash", source.purls) + } + if len(got["pkg:npm/lodash@4.17.21"]) != 1 { + t.Errorf("result = %#v, want lodash vulnerability", got) + } + if _, exists := got[""]; exists { + t.Error("result contains an empty PURL key") + } +} + func TestIsOutdated(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) svc := New(logger) @@ -74,3 +165,25 @@ func TestCategorizeLicense(t *testing.T) { } } } + +func TestNormalizeLicense(t *testing.T) { + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + svc := New(logger) + + tests := []struct { + input string + expected string + }{ + {"MIT", "MIT"}, + {"Apache 2", "Apache-2.0"}, + {"Apache-2.0", "Apache-2.0"}, + {"", ""}, + } + + for _, tc := range tests { + result := svc.NormalizeLicense(tc.input) + if result != tc.expected { + t.Errorf("NormalizeLicense(%q) = %q, want %q", tc.input, result, tc.expected) + } + } +} diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 6c65682..1d1b69d 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -19,6 +19,7 @@ import ( "github.com/git-pkgs/cooldown" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/metrics" + "github.com/git-pkgs/proxy/internal/packageurl" "github.com/git-pkgs/proxy/internal/storage" "github.com/git-pkgs/purl" "github.com/git-pkgs/registries/fetch" @@ -61,7 +62,18 @@ var artifactCopyBufferPool = sync.Pool{ //nolint:gochecknoglobals // shared acro // canonicalPackagePURL returns a versionless PURL in canonical form so cooldown // lookups match keys produced by config.CooldownConfig.NormalizedPackages. func canonicalPackagePURL(ecosystem, name string) string { - return purl.MakePURLString(ecosystem, name, "") + return packageurl.MakeString(ecosystem, name, "") +} + +var errUnsupportedPackageIdentity = errors.New("package identity cannot be represented as a PURL") + +func packagePURLStrings(ecosystem, name, version string) (string, string, error) { + packagePURL := packageurl.MakeString(ecosystem, name, "") + versionPURL := packageurl.MakeString(ecosystem, name, version) + if packagePURL == "" || versionPURL == "" { + return "", "", fmt.Errorf("%w: %s %q", errUnsupportedPackageIdentity, ecosystem, name) + } + return packagePURL, versionPURL, nil } const contentTypeJSON = "application/json" @@ -140,27 +152,32 @@ type CacheResult struct { ContentType string Hash string Cached bool + storagePath string } // GetOrFetchArtifact retrieves an artifact from cache or fetches from upstream. func (p *Proxy) GetOrFetchArtifact(ctx context.Context, ecosystem, name, version, filename string) (*CacheResult, error) { - if cached, err := p.GetCachedArtifact(ctx, ecosystem, name, version, filename); err != nil { + pkgPURL, versionPURL, err := packagePURLStrings(ecosystem, name, version) + if err != nil { + return nil, err + } + if cached, err := p.checkCache(ctx, pkgPURL, versionPURL, filename); err != nil { return nil, err } else if cached != nil { return cached, nil } metrics.RecordCacheMiss(ecosystem) - pkgPURL := purl.MakePURLString(ecosystem, name, "") - versionPURL := purl.MakePURLString(ecosystem, name, version) return p.fetchAndCache(ctx, ecosystem, name, version, filename, pkgPURL, versionPURL) } // GetCachedArtifact retrieves an artifact from cache without contacting an upstream. // It returns nil when no usable cache entry exists. func (p *Proxy) GetCachedArtifact(ctx context.Context, ecosystem, name, version, filename string) (*CacheResult, error) { - pkgPURL := purl.MakePURLString(ecosystem, name, "") - versionPURL := purl.MakePURLString(ecosystem, name, version) + pkgPURL, versionPURL, err := packagePURLStrings(ecosystem, name, version) + if err != nil { + return nil, err + } return p.checkCache(ctx, pkgPURL, versionPURL, filename) } @@ -170,8 +187,10 @@ func (p *Proxy) ClearCachedArtifact(ctx context.Context, ecosystem, name, versio if p.DB == nil || p.Storage == nil { return nil } - pkgPURL := purl.MakePURLString(ecosystem, name, "") - versionPURL := purl.MakePURLString(ecosystem, name, version) + pkgPURL, versionPURL, err := packagePURLStrings(ecosystem, name, version) + if err != nil { + return err + } cached, err := p.DB.GetCachedArtifact(pkgPURL, versionPURL, filename) if err != nil { return fmt.Errorf("looking up cached artifact: %w", err) @@ -205,6 +224,7 @@ func (p *Proxy) checkCache(ctx context.Context, pkgPURL, versionPURL, filename s ContentType: artifact.ContentType.String, Hash: artifact.ContentHash.String, Cached: true, + storagePath: artifact.StoragePath, } if p.DirectServe { @@ -864,19 +884,55 @@ func (p *Proxy) GetOrFetchArtifactFromURL(ctx context.Context, ecosystem, name, // GetOrFetchArtifactFromURLWithHeaders retrieves an artifact from cache or fetches from a URL // with additional request-specific HTTP headers. func (p *Proxy) GetOrFetchArtifactFromURLWithHeaders(ctx context.Context, ecosystem, name, version, filename, downloadURL string, headers http.Header) (*CacheResult, error) { - if cached, err := p.GetCachedArtifact(ctx, ecosystem, name, version, filename); err != nil { + return p.getOrFetchArtifactFromURL(ctx, ecosystem, name, version, filename, downloadURL, headers, "") +} + +func (p *Proxy) getOrFetchArtifactFromURL(ctx context.Context, ecosystem, name, version, filename, downloadURL string, headers http.Header, upstreamHash string) (*CacheResult, error) { + pkgPURL, versionPURL, err := packagePURLStrings(ecosystem, name, version) + if err != nil { + return nil, err + } + return p.getOrFetchArtifactFromURLWithCachePURLs( + ctx, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL, headers, upstreamHash, + ) +} + +func (p *Proxy) getOrFetchArtifactFromURLWithCachePURLs(ctx context.Context, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL string, headers http.Header, upstreamHash string) (*CacheResult, error) { + if cached, err := p.getCachedArtifactWithUpstreamHash(ctx, pkgPURL, versionPURL, filename, upstreamHash); err != nil { return nil, err } else if cached != nil { return cached, nil } metrics.RecordCacheMiss(ecosystem) - pkgPURL := purl.MakePURLString(ecosystem, name, "") - versionPURL := purl.MakePURLString(ecosystem, name, version) - return p.fetchAndCacheFromURL(ctx, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL, headers) + return p.fetchAndCacheFromURL(ctx, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL, headers, upstreamHash) } -func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL string, headers http.Header) (*CacheResult, error) { +// getCachedArtifactWithUpstreamHash returns a cached artifact whose recorded +// content hash matches the checksum the upstream currently declares for it. +// This detects an upstream re-publishing under the same version, which the +// stream integrity check in checkCache cannot: that check only verifies the +// stored blob against the hash recorded when it was cached. On mismatch the +// stale entry is discarded and nil is returned so the caller re-fetches. +func (p *Proxy) getCachedArtifactWithUpstreamHash(ctx context.Context, pkgPURL, versionPURL, filename, upstreamHash string) (*CacheResult, error) { + cached, err := p.checkCache(ctx, pkgPURL, versionPURL, filename) + if err != nil || cached == nil { + return cached, err + } + if artifactHashMatches(cached.Hash, upstreamHash) { + return cached, nil + } + + if cached.Reader != nil { + _ = cached.Reader.Close() + } + p.Logger.Warn("cached artifact hash disagrees with upstream metadata, discarding", + "purl", versionPURL, "filename", filename, "cached", cached.Hash, "upstream", upstreamHash) + p.discardCachedArtifact(ctx, versionPURL, filename, cached.storagePath) + return nil, nil +} + +func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, version, filename, pkgPURL, versionPURL, downloadURL string, headers http.Header, upstreamHash string) (*CacheResult, error) { p.Logger.Info("fetching from upstream", "ecosystem", ecosystem, "name", name, "version", version, "url", downloadURL) @@ -894,6 +950,12 @@ func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, versi if err != nil { return nil, fmt.Errorf("storing artifact: %w", err) } + if !artifactHashMatches(hash, upstreamHash) { + if err := p.Storage.Delete(ctx, storagePath); err != nil { + p.Logger.Warn("failed to discard artifact with mismatched checksum", "path", storagePath, "error", err) + } + return nil, fmt.Errorf("artifact checksum mismatch: upstream declared %s, got %s", upstreamHash, hash) + } if err := p.updateCacheDB(ecosystem, name, filename, pkgPURL, versionPURL, downloadURL, storagePath, hash, size, artifact.ContentType); err != nil { p.Logger.Warn("failed to update cache database", "error", err) @@ -912,3 +974,18 @@ func (p *Proxy) fetchAndCacheFromURL(ctx context.Context, ecosystem, name, versi Cached: false, }, nil } + +func artifactHashMatches(got, expected string) bool { + return expected == "" || strings.EqualFold(got, expected) +} + +func (p *Proxy) discardCachedArtifact(ctx context.Context, versionPURL, filename, storagePath string) { + if storagePath != "" { + if err := p.Storage.Delete(ctx, storagePath); err != nil { + p.Logger.Warn("failed to discard cached artifact", "path", storagePath, "error", err) + } + } + if err := p.DB.ClearArtifactCache(versionPURL, filename); err != nil { + p.Logger.Warn("failed to clear artifact cache record", "purl", versionPURL, "filename", filename, "error", err) + } +} diff --git a/internal/handler/handler_test.go b/internal/handler/handler_test.go index ec0e300..b65f001 100644 --- a/internal/handler/handler_test.go +++ b/internal/handler/handler_test.go @@ -105,15 +105,17 @@ type mockFetcher struct { fetchErrByURL map[string]error fetchCalled bool fetchedURL string + fetchedHeader http.Header } func (f *mockFetcher) Fetch(ctx context.Context, url string) (*fetch.Artifact, error) { return f.FetchWithHeaders(ctx, url, nil) } -func (f *mockFetcher) FetchWithHeaders(_ context.Context, url string, _ http.Header) (*fetch.Artifact, error) { +func (f *mockFetcher) FetchWithHeaders(_ context.Context, url string, headers http.Header) (*fetch.Artifact, error) { f.fetchCalled = true f.fetchedURL = url + f.fetchedHeader = headers.Clone() if f.fetchErrByURL != nil { if err, ok := f.fetchErrByURL[url]; ok { return nil, err @@ -403,6 +405,28 @@ func TestGetOrFetchArtifactFromURL_CacheMiss_StorageMissing(t *testing.T) { } } +func TestArtifactCacheRejectsUnsupportedPackageIdentity(t *testing.T) { + proxy, _, _, fetcher := setupTestProxy(t) + + _, err := proxy.GetCachedArtifact( + context.Background(), "swift", "apple/example", "1.2.3", "example-1.2.3.zip", + ) + if !errors.Is(err, errUnsupportedPackageIdentity) { + t.Fatalf("GetCachedArtifact() error = %v, want unsupported package identity", err) + } + + _, err = proxy.GetOrFetchArtifactFromURL( + context.Background(), "swift", "apple/example", "1.2.3", "example-1.2.3.zip", + "https://registry.example/apple/example/1.2.3.zip", + ) + if !errors.Is(err, errUnsupportedPackageIdentity) { + t.Fatalf("GetOrFetchArtifactFromURL() error = %v, want unsupported package identity", err) + } + if fetcher.fetchCalled { + t.Error("unsupported package identity reached the artifact fetcher") + } +} + func TestGetOrFetchArtifact_DirectServe_Redirect(t *testing.T) { proxy, db, store, fetcher := setupTestProxy(t) seedPackage(t, db, store, "npm", "lodash", "4.17.21", "lodash-4.17.21.tgz", "cached content") diff --git a/internal/handler/swift.go b/internal/handler/swift.go new file mode 100644 index 0000000..545407e --- /dev/null +++ b/internal/handler/swift.go @@ -0,0 +1,645 @@ +package handler + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/git-pkgs/proxy/internal/config" + "github.com/git-pkgs/proxy/internal/packageurl" +) + +const ( + swiftAcceptJSON = "application/vnd.swift.registry.v1+json" + swiftAcceptManifest = "application/vnd.swift.registry.v1+swift" + swiftAcceptArchive = "application/vnd.swift.registry.v1+zip" + swiftContentVersion = "1" + swiftMaxScopeLength = 39 + swiftMaxNameLength = 100 +) + +// SwiftHandler handles the read-only Swift Package Registry v1 protocol. +type SwiftHandler struct { + proxy *Proxy + upstreamURL string + proxyURL string +} + +// NewSwiftHandler creates a Swift Package Registry protocol handler. +func NewSwiftHandler(proxy *Proxy, proxyURL, upstreamURL string) *SwiftHandler { + if strings.TrimSpace(upstreamURL) == "" { + upstreamURL = config.DefaultSwiftUpstream + } + + return &SwiftHandler{ + proxy: proxy, + upstreamURL: strings.TrimSuffix(upstreamURL, "/"), + proxyURL: strings.TrimSuffix(proxyURL, "/"), + } +} + +// Routes returns the HTTP handler for Swift registry requests. +func (h *SwiftHandler) Routes() http.Handler { + mux := http.NewServeMux() + mux.HandleFunc("GET /identifiers", h.handleIdentifiers) + mux.HandleFunc("GET /{scope}/{name}/{version}/Package.swift", h.handleManifest) + mux.HandleFunc("GET /{scope}/{name}/{version}", h.handleRelease) + mux.HandleFunc("PUT /{scope}/{name}/{version}", h.handlePublishingUnsupported) + mux.HandleFunc("GET /{scope}/{name}", h.handlePackageReleases) + return mux +} + +func (h *SwiftHandler) handlePackageReleases(w http.ResponseWriter, r *http.Request) { + scope := r.PathValue("scope") + name := strings.TrimSuffix(r.PathValue("name"), ".json") + if !validSwiftScope(scope) || !validSwiftPackageName(name) { + writeSwiftProblem(w, http.StatusBadRequest, "invalid package identifier") + return + } + scope, name = canonicalSwiftPackage(scope, name) + + upstreamURL := h.buildUpstreamURL(scope, name, "", "", r.URL.RawQuery) + body, contentType, responseHeaders, err := h.fetchMetadataWithHeaders( + r.Context(), upstreamURL, requestAccept(r, swiftAcceptJSON), + ) + if err != nil { + h.writeMetadataError(w, err) + return + } + + rewritten, err := h.rewriteReleaseURLs(scope, name, body) + if err != nil { + h.proxy.Logger.Warn("failed to rewrite Swift release URLs", "error", err) + rewritten = body + } + for _, link := range responseHeaders.Values("Link") { + w.Header().Add("Link", h.rewriteLinkHeader(link, upstreamURL)) + } + writeSwiftMetadata(w, r, rewritten, contentType) +} + +func (h *SwiftHandler) handleRelease(w http.ResponseWriter, r *http.Request) { + scope := r.PathValue("scope") + name := r.PathValue("name") + version := r.PathValue("version") + if strings.HasSuffix(version, ".zip") { + h.handleSourceArchive(w, r, scope, name, strings.TrimSuffix(version, ".zip")) + return + } + + version = strings.TrimSuffix(version, ".json") + if !validSwiftPackageReference(scope, name, version) { + writeSwiftProblem(w, http.StatusBadRequest, "invalid package release") + return + } + scope, name = canonicalSwiftPackage(scope, name) + + upstreamURL := h.buildUpstreamURL(scope, name, version, "", r.URL.RawQuery) + body, contentType, err := h.proxy.FetchOrCacheMetadata( + r.Context(), "swift", swiftReleaseCacheKey(scope, name, version), upstreamURL, requestAccept(r, swiftAcceptJSON), + ) + if err != nil { + h.writeMetadataError(w, err) + return + } + writeSwiftMetadata(w, r, body, contentType) +} + +func (h *SwiftHandler) handleManifest(w http.ResponseWriter, r *http.Request) { + scope := r.PathValue("scope") + name := r.PathValue("name") + version := r.PathValue("version") + if !validSwiftPackageReference(scope, name, version) { + writeSwiftProblem(w, http.StatusBadRequest, "invalid package release") + return + } + scope, name = canonicalSwiftPackage(scope, name) + + upstreamURL := h.buildUpstreamURL(scope, name, version, "Package.swift", r.URL.RawQuery) + h.proxySwiftResource(w, r, upstreamURL, swiftAcceptManifest) +} + +func (h *SwiftHandler) handleIdentifiers(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("url") == "" { + writeSwiftProblem(w, http.StatusBadRequest, "url query parameter is required") + return + } + + upstreamURL := h.upstreamURL + "/identifiers?" + r.URL.RawQuery + cacheKey := swiftMetadataCacheKey("identifiers", r.URL.RawQuery) + body, contentType, err := h.proxy.FetchOrCacheMetadata( + r.Context(), "swift", cacheKey, upstreamURL, requestAccept(r, swiftAcceptJSON), + ) + if err != nil { + h.writeMetadataError(w, err) + return + } + writeSwiftMetadata(w, r, body, contentType) +} + +func (h *SwiftHandler) handlePublishingUnsupported(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Allow", "GET, HEAD") + writeSwiftProblem(w, http.StatusMethodNotAllowed, "publishing isn't supported") +} + +func (h *SwiftHandler) handleSourceArchive(w http.ResponseWriter, r *http.Request, scope, name, version string) { + if !validSwiftPackageReference(scope, name, version) { + writeSwiftProblem(w, http.StatusBadRequest, "invalid package release") + return + } + scope, name = canonicalSwiftPackage(scope, name) + + packageName := scope + "/" + name + filename := fmt.Sprintf("%s-%s.zip", name, version) + upstreamURL := h.buildUpstreamURL(scope, name, version+".zip", "", r.URL.RawQuery) + packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", packageName, version) + if packagePURL == "" || versionPURL == "" { + h.writeArtifactError(w, fmt.Errorf("%w: swift %q", errUnsupportedPackageIdentity, packageName)) + return + } + archiveInfo, infoErr := h.fetchArchiveInfo(r.Context(), scope, name, version) + if infoErr != nil { + h.writeArtifactError(w, fmt.Errorf("fetching release metadata: %w", infoErr)) + return + } + + if r.Method == http.MethodHead { + h.handleSourceArchiveHead(w, r, name, version, filename, packagePURL, versionPURL, upstreamURL, archiveInfo) + return + } + + headers := make(http.Header) + headers.Set("Accept", requestAccept(r, swiftAcceptArchive)) + result, err := h.proxy.getOrFetchArtifactFromURLWithCachePURLs( + r.Context(), "swift", packageName, version, filename, packagePURL, versionPURL, + upstreamURL, headers, archiveInfo.checksum, + ) + if err != nil { + h.writeArtifactError(w, err) + return + } + + result.ContentType = "application/zip" + setSwiftArchiveHeaders(w.Header(), name, version, result.Hash, archiveInfo) + serveArtifact(w, r.Method, result) +} + +func (h *SwiftHandler) handleSourceArchiveHead( + w http.ResponseWriter, + r *http.Request, + name, version, filename, packagePURL, versionPURL, upstreamURL string, + archiveInfo swiftArchiveInfo, +) { + result, err := h.proxy.getCachedArtifactWithUpstreamHash( + r.Context(), packagePURL, versionPURL, filename, archiveInfo.checksum, + ) + if err != nil { + h.writeArtifactError(w, err) + return + } + if result != nil { + result.ContentType = "application/zip" + setSwiftArchiveHeaders(w.Header(), name, version, result.Hash, archiveInfo) + serveArtifact(w, r.Method, result) + return + } + + size, err := h.probeSourceArchive(r.Context(), upstreamURL, requestAccept(r, swiftAcceptArchive)) + if err != nil { + h.writeArtifactError(w, err) + return + } + setSwiftArchiveHeaders(w.Header(), name, version, "", archiveInfo) + w.Header().Set("Content-Type", "application/zip") + if size >= 0 { + w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) + } + w.WriteHeader(http.StatusOK) +} + +func (h *SwiftHandler) probeSourceArchive(ctx context.Context, upstreamURL, accept string) (int64, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil) + if err != nil { + return 0, fmt.Errorf("creating upstream archive request: %w", err) + } + req.Header.Set("Accept", accept) + req.Header.Set("Range", "bytes=0-0") + h.proxy.applyUpstreamAuth(req) + + resp, err := h.proxy.HTTPClient.Do(req) + if err != nil { + return 0, fmt.Errorf("requesting upstream archive: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusNotFound { + return 0, ErrUpstreamNotFound + } + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + return 0, fmt.Errorf("upstream archive returned %d", resp.StatusCode) + } + + if resp.StatusCode == http.StatusPartialContent { + _, total, found := strings.Cut(resp.Header.Get("Content-Range"), "/") + if !found || total == "*" { + return -1, nil + } + if parsed, parseErr := strconv.ParseInt(total, 10, 64); parseErr == nil { + return parsed, nil + } + return -1, nil + } + + size := int64(-1) + if contentLength := resp.Header.Get("Content-Length"); contentLength != "" { + if parsed, parseErr := strconv.ParseInt(contentLength, 10, 64); parseErr == nil { + size = parsed + } + } + return size, nil +} + +func (h *SwiftHandler) fetchMetadataWithHeaders( + ctx context.Context, + upstreamURL, accept string, +) ([]byte, string, http.Header, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, upstreamURL, nil) + if err != nil { + return nil, "", nil, fmt.Errorf("creating upstream metadata request: %w", err) + } + req.Header.Set("Accept", accept) + h.proxy.applyUpstreamAuth(req) + + resp, err := h.proxy.HTTPClient.Do(req) + if err != nil { + return nil, "", nil, fmt.Errorf("requesting upstream metadata: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusNotFound { + return nil, "", nil, ErrUpstreamNotFound + } + if resp.StatusCode != http.StatusOK { + return nil, "", nil, fmt.Errorf("upstream metadata returned %d", resp.StatusCode) + } + + body, err := h.proxy.ReadMetadata(resp.Body) + if err != nil { + return nil, "", nil, fmt.Errorf("reading upstream metadata: %w", err) + } + contentType := resp.Header.Get("Content-Type") + if contentType == "" { + contentType = contentTypeJSON + } + return body, contentType, resp.Header.Clone(), nil +} + +type swiftReleaseMetadata struct { + Resources []struct { + Name string `json:"name"` + Type string `json:"type"` + Checksum string `json:"checksum"` + Signing *struct { + Signature string `json:"signatureBase64Encoded"` + Format string `json:"signatureFormat"` + } `json:"signing"` + } `json:"resources"` +} + +type swiftArchiveInfo struct { + checksum string + signature string + signatureFormat string +} + +func (h *SwiftHandler) fetchArchiveInfo(ctx context.Context, scope, name, version string) (swiftArchiveInfo, error) { + upstreamURL := h.buildUpstreamURL(scope, name, version, "", "") + body, _, err := h.proxy.FetchOrCacheMetadata( + ctx, "swift", swiftReleaseCacheKey(scope, name, version), upstreamURL, swiftAcceptJSON, + ) + if err != nil { + return swiftArchiveInfo{}, err + } + + var metadata swiftReleaseMetadata + if err := json.Unmarshal(body, &metadata); err != nil { + return swiftArchiveInfo{}, fmt.Errorf("parsing release metadata: %w", err) + } + for _, resource := range metadata.Resources { + if resource.Name != "source-archive" || resource.Type != "application/zip" { + continue + } + checksum, err := normalizeSwiftChecksum(resource.Checksum) + if err != nil { + return swiftArchiveInfo{}, err + } + info := swiftArchiveInfo{checksum: checksum} + if resource.Signing != nil { + if resource.Signing.Signature == "" || resource.Signing.Format == "" { + return swiftArchiveInfo{}, errors.New("source archive signing metadata is incomplete") + } + info.signature = resource.Signing.Signature + info.signatureFormat = resource.Signing.Format + } + return info, nil + } + + return swiftArchiveInfo{}, errors.New("source archive is missing from release metadata") +} + +func normalizeSwiftChecksum(checksum string) (string, error) { + digest, err := hex.DecodeString(checksum) + if err != nil || len(digest) != sha256.Size { + return "", errors.New("source archive checksum is not a SHA-256 digest") + } + return hex.EncodeToString(digest), nil +} + +func setSwiftArchiveHeaders(header http.Header, name, version, contentHash string, info swiftArchiveInfo) { + header.Set("Cache-Control", "public, immutable") + header.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s-%s.zip"`, name, version)) + header.Set("Content-Version", swiftContentVersion) + + checksum := info.checksum + if checksum == "" { + checksum = contentHash + } + if digest := swiftDigestHeader(checksum); digest != "" { + header.Set("Digest", digest) + } + if info.signature != "" && info.signatureFormat != "" { + header.Set("X-Swift-Package-Signature", info.signature) + header.Set("X-Swift-Package-Signature-Format", info.signatureFormat) + } +} + +func swiftDigestHeader(checksum string) string { + digest, err := hex.DecodeString(checksum) + if err != nil || len(digest) != sha256.Size { + return "" + } + return "sha-256=" + base64.StdEncoding.EncodeToString(digest) +} + +func (h *SwiftHandler) proxySwiftResource(w http.ResponseWriter, r *http.Request, upstreamURL, defaultAccept string) { + req, err := http.NewRequestWithContext(r.Context(), r.Method, upstreamURL, nil) + if err != nil { + writeSwiftProblem(w, http.StatusInternalServerError, "failed to create upstream request") + return + } + req.Header.Set("Accept", requestAccept(r, defaultAccept)) + for _, name := range []string{"If-Modified-Since", "If-None-Match"} { + if value := r.Header.Get(name); value != "" { + req.Header.Set(name, value) + } + } + h.proxy.applyUpstreamAuth(req) + + resp, err := h.proxy.HTTPClient.Do(req) + if err != nil { + writeSwiftProblem(w, http.StatusBadGateway, "upstream request failed") + return + } + defer func() { _ = resp.Body.Close() }() + + copySwiftResponseHeaders(w.Header(), resp.Header) + if location := resp.Header.Get("Location"); location != "" { + w.Header().Set("Location", h.rewriteRegistryURL(location, upstreamURL)) + } + for _, link := range resp.Header.Values("Link") { + w.Header().Add("Link", h.rewriteLinkHeader(link, upstreamURL)) + } + if w.Header().Get("Content-Version") == "" { + w.Header().Set("Content-Version", swiftContentVersion) + } + + w.WriteHeader(resp.StatusCode) + if r.Method != http.MethodHead { + _, _ = io.Copy(w, resp.Body) + } +} + +func copySwiftResponseHeaders(dst, src http.Header) { + for _, name := range []string{ + "Cache-Control", "Content-Disposition", "Content-Language", "Content-Length", + "Content-Type", "Content-Version", "Digest", "ETag", "Last-Modified", + "Retry-After", "Vary", "Warning", "X-Swift-Package-Signature", + "X-Swift-Package-Signature-Format", + } { + for _, value := range src.Values(name) { + dst.Add(name, value) + } + } +} + +func (h *SwiftHandler) rewriteLinkHeader(value, upstreamRequestURL string) string { + var result strings.Builder + for len(value) > 0 { + start := strings.IndexByte(value, '<') + if start < 0 { + result.WriteString(value) + break + } + endOffset := strings.IndexByte(value[start+1:], '>') + if endOffset < 0 { + result.WriteString(value) + break + } + end := start + 1 + endOffset + result.WriteString(value[:start+1]) + result.WriteString(h.rewriteRegistryURL(value[start+1:end], upstreamRequestURL)) + result.WriteByte('>') + value = value[end+1:] + } + return result.String() +} + +func (h *SwiftHandler) rewriteRegistryURL(rawURL, upstreamRequestURL string) string { + base, err := url.Parse(h.upstreamURL) + if err != nil { + return rawURL + } + requestURL, err := url.Parse(upstreamRequestURL) + if err != nil { + return rawURL + } + reference, err := url.Parse(rawURL) + if err != nil { + return rawURL + } + absolute := requestURL.ResolveReference(reference) + if !strings.EqualFold(absolute.Scheme, base.Scheme) || !strings.EqualFold(absolute.Host, base.Host) { + return rawURL + } + + basePath := strings.TrimSuffix(base.EscapedPath(), "/") + absolutePath := absolute.EscapedPath() + if absolutePath != basePath && !strings.HasPrefix(absolutePath, basePath+"/") { + return rawURL + } + suffix := strings.TrimPrefix(absolutePath, basePath) + rewritten := h.proxyURL + "/swift" + suffix + if absolute.RawQuery != "" { + rewritten += "?" + absolute.RawQuery + } + if absolute.Fragment != "" { + rewritten += "#" + absolute.Fragment + } + return rewritten +} + +func (h *SwiftHandler) rewriteReleaseURLs(scope, name string, body []byte) ([]byte, error) { + var metadata map[string]any + if err := json.Unmarshal(body, &metadata); err != nil { + return nil, err + } + releases, ok := metadata["releases"].(map[string]any) + if !ok { + return body, nil + } + + for version, value := range releases { + release, ok := value.(map[string]any) + if !ok { + continue + } + if _, hasURL := release["url"]; !hasURL { + continue + } + release["url"] = fmt.Sprintf( + "%s/swift/%s/%s/%s", + h.proxyURL, + url.PathEscape(scope), + url.PathEscape(name), + url.PathEscape(version), + ) + } + return json.Marshal(metadata) +} + +func (h *SwiftHandler) buildUpstreamURL(scope, name, version, resource, rawQuery string) string { + parts := []string{h.upstreamURL, url.PathEscape(scope), url.PathEscape(name)} + if version != "" { + parts = append(parts, url.PathEscape(version)) + } + if resource != "" { + parts = append(parts, resource) + } + result := strings.Join(parts, "/") + if rawQuery != "" { + result += "?" + rawQuery + } + return result +} + +func swiftMetadataCacheKey(parts ...string) string { + joined := strings.Join(parts, "\x00") + digest := sha256.Sum256([]byte(joined)) + return hex.EncodeToString(digest[:]) +} + +func swiftReleaseCacheKey(scope, name, version string) string { + return swiftMetadataCacheKey("release", scope, name, version) +} + +func requestAccept(r *http.Request, fallback string) string { + if accept := r.Header.Get("Accept"); accept != "" { + return accept + } + return fallback +} + +func writeSwiftMetadata(w http.ResponseWriter, r *http.Request, body []byte, contentType string) { + if contentType == "" { + contentType = "application/json" + } + digest := sha256.Sum256(body) + etag := fmt.Sprintf(`"%x"`, digest) + w.Header().Set("Content-Type", contentType) + w.Header().Set("Content-Version", swiftContentVersion) + w.Header().Set("ETag", etag) + if r.Header.Get("If-None-Match") == etag { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("Content-Length", strconv.Itoa(len(body))) + w.WriteHeader(http.StatusOK) + if r.Method != http.MethodHead { + _, _ = w.Write(body) + } +} + +func (h *SwiftHandler) writeMetadataError(w http.ResponseWriter, err error) { + if errors.Is(err, ErrUpstreamNotFound) { + writeSwiftProblem(w, http.StatusNotFound, "not found") + return + } + h.proxy.Logger.Error("Swift metadata request failed", "error", err) + writeSwiftProblem(w, http.StatusBadGateway, "upstream request failed") +} + +func (h *SwiftHandler) writeArtifactError(w http.ResponseWriter, err error) { + if errors.Is(err, ErrUpstreamNotFound) { + writeSwiftProblem(w, http.StatusNotFound, "release not found") + return + } + h.proxy.Logger.Error("Swift archive request failed", "error", err) + writeSwiftProblem(w, http.StatusBadGateway, "failed to fetch package") +} + +func writeSwiftProblem(w http.ResponseWriter, status int, detail string) { + w.Header().Set("Content-Type", "application/problem+json") + w.Header().Set("Content-Version", swiftContentVersion) + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(map[string]string{"detail": detail}) +} + +func validSwiftPackageReference(scope, name, version string) bool { + return validSwiftScope(scope) && validSwiftPackageName(name) && version != "" && version != "." && version != ".." && !strings.ContainsAny(version, "/\\") +} + +func canonicalSwiftPackage(scope, name string) (string, string) { + return strings.ToLower(scope), strings.ToLower(name) +} + +func validSwiftScope(scope string) bool { + return validSwiftIdentifier(scope, swiftMaxScopeLength, "-") +} + +func validSwiftPackageName(name string) bool { + return validSwiftIdentifier(name, swiftMaxNameLength, "-_") +} + +func validSwiftIdentifier(value string, maxLength int, separators string) bool { + if value == "" || len(value) > maxLength { + return false + } + previousSeparator := false + for i := 0; i < len(value); i++ { + character := value[i] + separator := strings.ContainsRune(separators, rune(character)) + if separator { + if i == 0 || i == len(value)-1 || previousSeparator { + return false + } + previousSeparator = true + continue + } + if (character < 'a' || character > 'z') && + (character < 'A' || character > 'Z') && + (character < '0' || character > '9') { + return false + } + previousSeparator = false + } + return true +} diff --git a/internal/handler/swift_test.go b/internal/handler/swift_test.go new file mode 100644 index 0000000..88ed30e --- /dev/null +++ b/internal/handler/swift_test.go @@ -0,0 +1,534 @@ +package handler + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/git-pkgs/proxy/internal/packageurl" + "github.com/git-pkgs/registries/fetch" +) + +func TestSwiftPackageReleasesRewritesRegistryURLs(t *testing.T) { + var gotAccept string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/registry/apple/swift-argument-parser" { + t.Errorf("upstream path = %q", r.URL.Path) + } + gotAccept = r.Header.Get("Accept") + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Content-Version", "1") + w.Header().Add("Link", `; rel="next"`) + _, _ = io.WriteString(w, `{"releases":{"1.2.0":{"url":"/registry/apple/swift-argument-parser/1.2.0"},"1.1.0":{}}}`) + })) + defer upstream.Close() + + proxy, _, _, _ := setupTestProxy(t) + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes() + req := httptest.NewRequest(http.MethodGet, "/APPLE/SWIFT-ARGUMENT-PARSER", nil) + req.Header.Set("Accept", swiftAcceptJSON) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) + } + if gotAccept != swiftAcceptJSON { + t.Errorf("upstream Accept = %q, want %q", gotAccept, swiftAcceptJSON) + } + if got := w.Header().Get("Content-Version"); got != "1" { + t.Errorf("Content-Version = %q, want 1", got) + } + if got := w.Header().Get("Link"); got != `; rel="next"` { + t.Errorf("Link = %q", got) + } + + var body struct { + Releases map[string]struct { + URL string `json:"url"` + } `json:"releases"` + } + if err := json.NewDecoder(w.Body).Decode(&body); err != nil { + t.Fatalf("decoding response: %v", err) + } + if got := body.Releases["1.2.0"].URL; got != "https://proxy.example/swift/apple/swift-argument-parser/1.2.0" { + t.Errorf("release URL = %q", got) + } + if got := body.Releases["1.1.0"].URL; got != "" { + t.Errorf("release without upstream URL gained URL %q", got) + } +} + +func TestSwiftReleaseMetadataSupportsJSONExtensionAndHead(t *testing.T) { + var requestMethods []string + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestMethods = append(requestMethods, r.Method) + if r.URL.Path != "/registry/apple/example/1.2.3" { + t.Errorf("upstream path = %q", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"id":"apple.example","version":"1.2.3","resources":[]}`) + })) + defer upstream.Close() + + proxy, _, _, _ := setupTestProxy(t) + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes() + + for _, method := range []string{http.MethodGet, http.MethodHead} { + req := httptest.NewRequest(method, "/APPLE/EXAMPLE/1.2.3.json", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Fatalf("%s status = %d, want 200", method, w.Code) + } + if method == http.MethodHead && w.Body.Len() != 0 { + t.Errorf("HEAD response body length = %d, want 0", w.Body.Len()) + } + } + if len(requestMethods) != 2 || requestMethods[0] != http.MethodGet || requestMethods[1] != http.MethodGet { + t.Errorf("upstream methods = %v, want metadata GETs", requestMethods) + } +} + +func TestSwiftManifestProxiesQueryAndRewritesLinks(t *testing.T) { + var upstream *httptest.Server + var gotAccept string + upstream = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("upstream method = %s, want GET", r.Method) + } + if r.URL.Path != "/registry/apple/example/1.2.3/Package.swift" { + t.Errorf("upstream path = %q", r.URL.Path) + } + if got := r.URL.Query().Get("swift-version"); got != "5.9" { + t.Errorf("swift-version = %q, want 5.9", got) + } + gotAccept = r.Header.Get("Accept") + w.Header().Set("Content-Type", "text/x-swift") + w.Header().Add("Link", fmt.Sprintf(`<%s/registry/apple/example/1.2.3/Package.swift?swift-version=5.8>; rel="alternate"; filename="Package@swift-5.8.swift"`, upstream.URL)) + w.Header().Add("Link", `; rel="canonical"`) + _, _ = io.WriteString(w, "// swift-tools-version: 5.9\n") + })) + defer upstream.Close() + + proxy, _, _, _ := setupTestProxy(t) + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes() + req := httptest.NewRequest(http.MethodGet, "/APPLE/EXAMPLE/1.2.3/Package.swift?swift-version=5.9", nil) + req.Header.Set("Accept", swiftAcceptManifest) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200", w.Code) + } + if gotAccept != swiftAcceptManifest { + t.Errorf("upstream Accept = %q, want %q", gotAccept, swiftAcceptManifest) + } + links := strings.Join(w.Header().Values("Link"), ",") + if !strings.Contains(links, "https://proxy.example/swift/apple/example/1.2.3/Package.swift?swift-version=5.8") { + t.Errorf("internal manifest Link was not rewritten: %q", links) + } + if !strings.Contains(links, "https://github.com/apple/example") { + t.Errorf("external canonical Link was changed: %q", links) + } + if got := w.Header().Get("Content-Version"); got != "1" { + t.Errorf("Content-Version = %q, want 1", got) + } +} + +func TestSwiftSourceArchiveCachesAndPreservesSecurityMetadata(t *testing.T) { + archive := []byte("swift source archive") + checksumBytes := sha256.Sum256(archive) + checksum := hex.EncodeToString(checksumBytes[:]) + signature := base64.StdEncoding.EncodeToString([]byte("signature")) + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/registry/apple/example/1.2.3" { + t.Errorf("metadata path = %q", r.URL.Path) + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q,"signing":{"signatureBase64Encoded":%q,"signatureFormat":"cms-1.0.0"}}]}`, checksum, signature) + })) + defer upstream.Close() + + proxy, db, _, fetcher := setupTestProxy(t) + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader(string(archive))), + Size: int64(len(archive)), + ContentType: "application/zip", + } + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes() + + requestArchive := func(method string) *httptest.ResponseRecorder { + req := httptest.NewRequest(method, "/apple/example/1.2.3.zip", nil) + req.Header.Set("Accept", swiftAcceptArchive) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + return w + } + + w := requestArchive(http.MethodGet) + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) + } + if got := w.Body.Bytes(); string(got) != string(archive) { + t.Errorf("archive body = %q", got) + } + if !fetcher.fetchCalled { + t.Fatal("archive fetcher was not called") + } + if got := fetcher.fetchedURL; got != upstream.URL+"/registry/apple/example/1.2.3.zip" { + t.Errorf("fetched URL = %q", got) + } + if got := fetcher.fetchedHeader.Get("Accept"); got != swiftAcceptArchive { + t.Errorf("archive Accept = %q, want %q", got, swiftAcceptArchive) + } + if got := w.Header().Get("Digest"); got != "sha-256="+base64.StdEncoding.EncodeToString(checksumBytes[:]) { + t.Errorf("Digest = %q", got) + } + if got := w.Header().Get("X-Swift-Package-Signature"); got != signature { + t.Errorf("signature = %q", got) + } + if got := w.Header().Get("X-Swift-Package-Signature-Format"); got != "cms-1.0.0" { + t.Errorf("signature format = %q", got) + } + if got := w.Header().Get("Content-Disposition"); got != `attachment; filename="example-1.2.3.zip"` { + t.Errorf("Content-Disposition = %q", got) + } + + packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3") + if strings.HasPrefix(packagePURL, "pkg:swift/") { + t.Fatalf("registry identity produced source PURL %q", packagePURL) + } + versionRecord, err := db.GetVersionByPURL(versionPURL) + if err != nil { + t.Fatalf("cached Swift version %q not found: %v", versionPURL, err) + } + if versionRecord == nil { + t.Fatalf("cached Swift version %q not found", versionPURL) + } + if versionRecord.PackagePURL != packagePURL { + t.Errorf("cached package PURL = %q, want %q", versionRecord.PackagePURL, packagePURL) + } + + fetcher.fetchCalled = false + w = requestArchive(http.MethodHead) + if w.Code != http.StatusOK { + t.Fatalf("HEAD status = %d, want 200", w.Code) + } + if w.Body.Len() != 0 { + t.Errorf("HEAD body length = %d, want 0", w.Body.Len()) + } + if got := w.Header().Get("Content-Length"); got != fmt.Sprint(len(archive)) { + t.Errorf("HEAD Content-Length = %q", got) + } + + w = requestArchive(http.MethodGet) + if w.Code != http.StatusOK || w.Body.String() != string(archive) { + t.Fatalf("cached response = %d %q", w.Code, w.Body.Bytes()) + } + if fetcher.fetchCalled { + t.Error("cached archive contacted artifact upstream") + } +} + +func TestSwiftSourceArchiveRejectsChecksumMismatch(t *testing.T) { + archive := []byte("unexpected archive") + expectedChecksum := sha256.Sum256([]byte("expected archive")) + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(expectedChecksum[:])) + })) + defer upstream.Close() + + proxy, db, store, fetcher := setupTestProxy(t) + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader(string(archive))), + Size: int64(len(archive)), + ContentType: "application/zip", + } + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes() + + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil)) + + if w.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String()) + } + if len(store.files) != 0 { + t.Errorf("mismatched archive remained in storage: %v", store.files) + } + packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3") + cached, err := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip") + if err != nil { + t.Fatalf("checking cache: %v", err) + } + if cached != nil { + t.Error("mismatched archive gained a cache record") + } +} + +func TestSwiftSourceArchiveCanonicalizesPackageIdentity(t *testing.T) { + archive := []byte("swift source archive") + checksumBytes := sha256.Sum256(archive) + checksum := hex.EncodeToString(checksumBytes[:]) + var metadataPaths []string + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + metadataPaths = append(metadataPaths, r.URL.Path) + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum) + })) + defer upstream.Close() + + proxy, db, store, fetcher := setupTestProxy(t) + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes() + requestArchive := func(path string) { + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader(string(archive))), + Size: int64(len(archive)), + ContentType: "application/zip", + } + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, path, nil)) + if w.Code != http.StatusOK { + t.Fatalf("GET %s status = %d, want 200; body: %s", path, w.Code, w.Body.String()) + } + } + + requestArchive("/apple/example/1.2.3.zip") + requestArchive("/APPLE/EXAMPLE/1.2.3.zip") + + if len(store.files) != 1 { + t.Errorf("cached files = %d, want 1", len(store.files)) + } + for _, path := range metadataPaths { + if path != "/apple/example/1.2.3" { + t.Errorf("metadata path = %q, want canonical lowercase path", path) + } + } + + canonicalPURL, _ := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3") + canonical, err := db.GetPackageByPURL(canonicalPURL) + if err != nil { + t.Fatalf("getting canonical package: %v", err) + } + if canonical == nil { + t.Fatalf("canonical package %q not found", canonicalPURL) + } + + nonCanonicalPURL, _ := packageurl.MakeCacheStrings("swift", "APPLE/EXAMPLE", "1.2.3") + if nonCanonicalPURL != canonicalPURL { + t.Errorf("uppercase cache PURL = %q, want %q", nonCanonicalPURL, canonicalPURL) + } +} + +func TestSwiftSourceArchiveHeadDiscardsCachedChecksumMismatch(t *testing.T) { + archive := []byte("cached archive") + upstreamChecksum := sha256.Sum256([]byte("upstream archive")) + + var probed bool + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, ".zip") { + probed = true + w.Header().Set("Content-Range", "bytes 0-0/456") + w.WriteHeader(http.StatusPartialContent) + _, _ = w.Write([]byte("x")) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, hex.EncodeToString(upstreamChecksum[:])) + })) + defer upstream.Close() + + proxy, db, store, fetcher := setupTestProxy(t) + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader(string(archive))), + Size: int64(len(archive)), + ContentType: "application/zip", + } + packagePURL, versionPURL := packageurl.MakeCacheStrings("swift", "apple/example", "1.2.3") + cached, err := proxy.getOrFetchArtifactFromURLWithCachePURLs( + context.Background(), "swift", "apple/example", "1.2.3", "example-1.2.3.zip", + packagePURL, versionPURL, upstream.URL+"/apple/example/1.2.3.zip", nil, "", + ) + if err != nil { + t.Fatalf("seeding cache: %v", err) + } + _ = cached.Reader.Close() + + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes() + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil)) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) + } + if !probed { + t.Error("stale cache entry was not replaced by an upstream probe") + } + if got := w.Header().Get("Content-Length"); got != "456" { + t.Errorf("Content-Length = %q, want 456 from upstream probe", got) + } + if len(store.files) != 0 { + t.Errorf("mismatched cached archive remained in storage: %v", store.files) + } + if rec, _ := db.GetCachedArtifact(packagePURL, versionPURL, "example-1.2.3.zip"); rec != nil { + t.Error("mismatched cache record was not cleared") + } +} + +func TestSwiftSourceArchiveRequiresReleaseMetadata(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + http.Error(w, "unavailable", http.StatusServiceUnavailable) + })) + defer upstream.Close() + + proxy, _, store, fetcher := setupTestProxy(t) + fetcher.artifact = &fetch.Artifact{ + Body: io.NopCloser(strings.NewReader("signed archive")), + ContentType: "application/zip", + } + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes() + + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest(http.MethodGet, "/apple/example/1.2.3.zip", nil)) + + if w.Code != http.StatusBadGateway { + t.Fatalf("status = %d, want 502; body: %s", w.Code, w.Body.String()) + } + if fetcher.fetchCalled { + t.Error("archive was fetched without release security metadata") + } + if len(store.files) != 0 { + t.Errorf("archive was cached without release security metadata: %v", store.files) + } +} + +func TestSwiftSourceArchiveColdHeadUsesRangeGetAcrossRedirect(t *testing.T) { + checksum := strings.Repeat("a", sha256.Size*2) + var archiveAccept string + var archiveMethod string + var archiveRange string + download := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + archiveMethod = r.Method + archiveRange = r.Header.Get("Range") + w.Header().Set("Content-Range", "bytes 0-0/123") + w.WriteHeader(http.StatusPartialContent) + _, _ = w.Write([]byte("x")) + })) + defer download.Close() + + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/apple/example/1.2.3": + w.Header().Set("Content-Type", "application/json") + _, _ = fmt.Fprintf(w, `{"id":"apple.example","version":"1.2.3","resources":[{"name":"source-archive","type":"application/zip","checksum":%q}]}`, checksum) + case "/apple/example/1.2.3.zip": + archiveAccept = r.Header.Get("Accept") + http.Redirect(w, r, download.URL, http.StatusSeeOther) + default: + http.NotFound(w, r) + } + })) + defer upstream.Close() + + proxy, _, _, _ := setupTestProxy(t) + proxy.HTTPClient = upstream.Client() + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL).Routes() + + w := httptest.NewRecorder() + handler.ServeHTTP(w, httptest.NewRequest(http.MethodHead, "/apple/example/1.2.3.zip", nil)) + + if w.Code != http.StatusOK { + t.Fatalf("status = %d, want 200; body: %s", w.Code, w.Body.String()) + } + if archiveMethod != http.MethodGet { + t.Errorf("download method = %q, want GET", archiveMethod) + } + if archiveRange != "bytes=0-0" { + t.Errorf("download Range = %q, want bytes=0-0", archiveRange) + } + if archiveAccept != swiftAcceptArchive { + t.Errorf("upstream Accept = %q, want %q", archiveAccept, swiftAcceptArchive) + } + if got := w.Header().Get("Content-Length"); got != "123" { + t.Errorf("Content-Length = %q, want 123", got) + } +} + +func TestSwiftIdentifiersAndPublishingUnsupported(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/registry/identifiers" { + t.Errorf("upstream path = %q", r.URL.Path) + } + if got := r.URL.Query().Get("url"); got != "https://github.com/apple/example" { + t.Errorf("lookup URL = %q", got) + } + w.Header().Set("Content-Type", "application/json") + _, _ = io.WriteString(w, `{"identifiers":["apple.example"]}`) + })) + defer upstream.Close() + + proxy, _, _, _ := setupTestProxy(t) + handler := NewSwiftHandler(proxy, "https://proxy.example", upstream.URL+"/registry").Routes() + + req := httptest.NewRequest(http.MethodGet, "/identifiers?url=https%3A%2F%2Fgithub.com%2Fapple%2Fexample", nil) + w := httptest.NewRecorder() + handler.ServeHTTP(w, req) + if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), "apple.example") { + t.Fatalf("identifier response = %d %q", w.Code, w.Body.String()) + } + + req = httptest.NewRequest(http.MethodGet, "/identifiers", nil) + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("missing URL status = %d, want 400", w.Code) + } + + req = httptest.NewRequest(http.MethodPut, "/apple/example/1.2.3", strings.NewReader("ignored")) + w = httptest.NewRecorder() + handler.ServeHTTP(w, req) + if w.Code != http.StatusMethodNotAllowed { + t.Errorf("publish status = %d, want 405", w.Code) + } + if got := w.Header().Get("Allow"); got != "GET, HEAD" { + t.Errorf("Allow = %q", got) + } +} + +func TestSwiftIdentifierValidation(t *testing.T) { + tests := []struct { + name string + value string + valid func(string) bool + want bool + }{ + {"scope", "apple", validSwiftScope, true}, + {"scope hyphen", "swift-server", validSwiftScope, true}, + {"scope underscore", "swift_server", validSwiftScope, false}, + {"scope repeated separator", "swift--server", validSwiftScope, false}, + {"package", "swift-argument_parser", validSwiftPackageName, true}, + {"package repeated separators", "swift-_argument", validSwiftPackageName, false}, + {"package trailing separator", "example-", validSwiftPackageName, false}, + {"package non-ASCII", "café", validSwiftPackageName, false}, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := test.valid(test.value); got != test.want { + t.Errorf("validation of %q = %v, want %v", test.value, got, test.want) + } + }) + } +} diff --git a/internal/mirror/registry.go b/internal/mirror/registry.go new file mode 100644 index 0000000..6b2c449 --- /dev/null +++ b/internal/mirror/registry.go @@ -0,0 +1,16 @@ +package mirror + +import ( + "context" + "fmt" +) + +// RegistrySource enumerates all packages in a registry for full mirroring. +// Registry enumeration is not yet implemented for any ecosystem. +type RegistrySource struct { + Ecosystem string +} + +func (s *RegistrySource) Enumerate(_ context.Context, _ func(PackageVersion) error) error { + return fmt.Errorf("registry enumeration is not yet implemented for ecosystem %q", s.Ecosystem) +} diff --git a/internal/mirror/registry_test.go b/internal/mirror/registry_test.go new file mode 100644 index 0000000..363bfea --- /dev/null +++ b/internal/mirror/registry_test.go @@ -0,0 +1,46 @@ +package mirror + +import ( + "context" + "testing" +) + +func TestRegistrySourceUnsupported(t *testing.T) { + source := &RegistrySource{Ecosystem: "golang"} + err := source.Enumerate(context.Background(), func(pv PackageVersion) error { + return nil + }) + if err == nil { + t.Fatal("expected error for unsupported ecosystem") + } +} + +func TestRegistrySourceNPMNotImplemented(t *testing.T) { + source := &RegistrySource{Ecosystem: "npm"} + err := source.Enumerate(context.Background(), func(pv PackageVersion) error { + return nil + }) + if err == nil { + t.Fatal("expected not-implemented error") + } +} + +func TestRegistrySourcePyPINotImplemented(t *testing.T) { + source := &RegistrySource{Ecosystem: "pypi"} + err := source.Enumerate(context.Background(), func(pv PackageVersion) error { + return nil + }) + if err == nil { + t.Fatal("expected not-implemented error") + } +} + +func TestRegistrySourceCargoNotImplemented(t *testing.T) { + source := &RegistrySource{Ecosystem: "cargo"} + err := source.Enumerate(context.Background(), func(pv PackageVersion) error { + return nil + }) + if err == nil { + t.Fatal("expected not-implemented error") + } +} diff --git a/internal/packageurl/packageurl.go b/internal/packageurl/packageurl.go new file mode 100644 index 0000000..7110942 --- /dev/null +++ b/internal/packageurl/packageurl.go @@ -0,0 +1,62 @@ +// Package packageurl builds package URLs from ecosystem-native package names. +package packageurl + +import ( + "strings" + + "github.com/git-pkgs/purl" +) + +// Make constructs a package URL from an ecosystem-native package name. +func Make(ecosystem, name, version string) *purl.PURL { + return purl.MakePURL(ecosystem, name, version) +} + +// MakeString constructs a package URL string. It returns an empty string when +// the package identity cannot be represented as a PURL. +func MakeString(ecosystem, name, version string) string { + return purl.MakePURLString(ecosystem, name, version) +} + +// WithVersionString returns a package PURL with its version replaced. It +// returns an empty string when packagePURL is invalid. +func WithVersionString(packagePURL, version string) string { + pkg, err := purl.Parse(packagePURL) + if err != nil { + return "" + } + return pkg.WithVersion(version).String() +} + +// MakeCacheStrings returns package and version PURLs suitable for artifact +// cache records. Swift registry identities use an explicit generic PURL until +// their source repository has been resolved. The result is independent of the +// configured upstream so cache entries survive an upstream.swift change, +// matching every other ecosystem. +func MakeCacheStrings(ecosystem, name, version string) (packagePURL, versionPURL string) { + if pkg := Make(ecosystem, name, ""); pkg != nil { + return pkg.String(), pkg.WithVersion(version).String() + } + if purl.NormalizeEcosystem(ecosystem) != "swift" { + return "", "" + } + + identity, ok := swiftRegistryIdentity(name) + if !ok { + return "", "" + } + + pkg := purl.New("generic", "swift-registry", identity, "", nil) + return pkg.String(), pkg.WithVersion(version).String() +} + +func swiftRegistryIdentity(name string) (string, bool) { + scope, packageName, found := strings.Cut(name, "/") + if !found { + scope, packageName, found = strings.Cut(name, ".") + } + if !found || scope == "" || packageName == "" || strings.ContainsAny(packageName, "/.") { + return "", false + } + return strings.ToLower(scope) + "." + strings.ToLower(packageName), true +} diff --git a/internal/packageurl/packageurl_test.go b/internal/packageurl/packageurl_test.go new file mode 100644 index 0000000..8982288 --- /dev/null +++ b/internal/packageurl/packageurl_test.go @@ -0,0 +1,75 @@ +package packageurl + +import "testing" + +func TestMakeSwiftRegistryIdentityUnsupported(t *testing.T) { + identities := []string{"apple.swift-argument-parser", "apple/swift-argument-parser"} + for _, identity := range identities { + t.Run(identity, func(t *testing.T) { + if got := Make("swift", identity, "1.8.2"); got != nil { + t.Errorf("Make() = %q, want nil", got.String()) + } + if got := MakeString("swift", identity, "1.8.2"); got != "" { + t.Errorf("MakeString() = %q, want empty string", got) + } + }) + } +} + +func TestMakeStringSwiftSourceCoordinate(t *testing.T) { + got := MakeString("swift", "github.com/apple/swift-package-manager", "1.7.0") + want := "pkg:swift/github.com/apple/swift-package-manager@1.7.0" + if got != want { + t.Errorf("MakeString() = %q, want %q", got, want) + } +} + +func TestWithVersionStringPreservesQualifiers(t *testing.T) { + packagePURL := "pkg:generic/swift-registry/apple.example?repository_url=https:%2F%2Fold.example%2Fswift" + got := WithVersionString(packagePURL, "1.2.3") + want := "pkg:generic/swift-registry/apple.example@1.2.3?repository_url=https:%2F%2Fold.example%2Fswift" + if got != want { + t.Errorf("WithVersionString() = %q, want %q", got, want) + } + + if got := WithVersionString("not a purl", "1.2.3"); got != "" { + t.Errorf("WithVersionString() = %q for invalid PURL, want empty string", got) + } +} + +func TestMakeCacheStringsSwiftRegistryIdentity(t *testing.T) { + packagePURL, versionPURL := MakeCacheStrings("swift", "APPLE/EXAMPLE", "1.2.3") + + wantPackage := "pkg:generic/swift-registry/apple.example" + if packagePURL != wantPackage { + t.Errorf("package PURL = %q, want %q", packagePURL, wantPackage) + } + wantVersion := "pkg:generic/swift-registry/apple.example@1.2.3" + if versionPURL != wantVersion { + t.Errorf("version PURL = %q, want %q", versionPURL, wantVersion) + } + + dottedPackage, dottedVersion := MakeCacheStrings("swift", "apple.example", "1.2.3") + if dottedPackage != packagePURL || dottedVersion != versionPURL { + t.Errorf("dotted identity cache PURLs = %q, %q; want %q, %q", dottedPackage, dottedVersion, packagePURL, versionPURL) + } +} + +func TestMakeCacheStringsUsesSourcePURLWhenAvailable(t *testing.T) { + packagePURL, versionPURL := MakeCacheStrings("swift", "github.com/apple/swift-package-manager", "1.7.0") + + if packagePURL != "pkg:swift/github.com/apple/swift-package-manager" { + t.Errorf("package PURL = %q", packagePURL) + } + if versionPURL != "pkg:swift/github.com/apple/swift-package-manager@1.7.0" { + t.Errorf("version PURL = %q", versionPURL) + } +} + +func TestMakeStringDelegatesOtherEcosystems(t *testing.T) { + got := MakeString("npm", "@babel/core", "7.23.0") + want := "pkg:npm/%40babel/core@7.23.0" + if got != want { + t.Errorf("MakeString() = %q, want %q", got, want) + } +} diff --git a/internal/server/browse.go b/internal/server/browse.go index 43ad9ae..fc5e658 100644 --- a/internal/server/browse.go +++ b/internal/server/browse.go @@ -14,7 +14,6 @@ import ( "github.com/git-pkgs/magic" "github.com/git-pkgs/proxy/internal/database" "github.com/git-pkgs/proxy/internal/handler" - "github.com/git-pkgs/purl" "github.com/go-chi/chi/v5" ) @@ -226,7 +225,7 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n dirPath := r.URL.Query().Get("path") // Get the artifact for this version - versionPURL := purl.MakePURLString(ecosystem, name, version) + versionPURL := s.cachedVersionPURL(ecosystem, name, version) artifacts, err := s.db.GetArtifactsByVersionPURL(versionPURL) if err != nil { notFound(w, "version not found") @@ -313,7 +312,7 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n } // Get the artifact for this version - versionPURL := purl.MakePURLString(ecosystem, name, version) + versionPURL := s.cachedVersionPURL(ecosystem, name, version) artifacts, err := s.db.GetArtifactsByVersionPURL(versionPURL) if err != nil { notFound(w, "version not found") @@ -534,8 +533,8 @@ type BrowseSourceData struct { // @Router /ui/api/compare/{ecosystem}/{name}/{fromVersion}/{toVersion} [get] func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem, name, fromVersion, toVersion string) { // Get artifacts for both versions - fromPURL := purl.MakePURLString(ecosystem, name, fromVersion) - toPURL := purl.MakePURLString(ecosystem, name, toVersion) + fromPURL := s.cachedVersionPURL(ecosystem, name, fromVersion) + toPURL := s.cachedVersionPURL(ecosystem, name, toVersion) fromArtifacts, err := s.db.GetArtifactsByVersionPURL(fromPURL) if err != nil || len(fromArtifacts) == 0 { diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go index f4f2f9a..3cc37c8 100644 --- a/internal/server/browse_test.go +++ b/internal/server/browse_test.go @@ -430,10 +430,6 @@ func TestHandleBrowseSourcePage(t *testing.T) { } } - if !strings.Contains(body, "proxy test-version (test-commit)") { - t.Error("browse source footer should contain proxy build information, not the package version") - } - // Check that the escapeHTML function is present for XSS protection if !strings.Contains(body, "function escapeHTML(str)") { t.Error("browse source page missing escapeHTML function for XSS protection") diff --git a/internal/server/dashboard.go b/internal/server/dashboard.go index 797a9ca..c4882fd 100644 --- a/internal/server/dashboard.go +++ b/internal/server/dashboard.go @@ -2,6 +2,7 @@ package server import ( "html/template" + "strings" "github.com/git-pkgs/proxy/internal/database" ) @@ -140,6 +141,7 @@ func supportedEcosystems() []string { "pub", "pypi", "rpm", + "swift", } } @@ -184,6 +186,8 @@ func ecosystemBadgeClasses(ecosystem string) string { return base + " bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300" case "julia": return base + " bg-emerald-100 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300" + case "swift": + return base + " bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300" case "oci": return base + " bg-sky-100 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300" case "deb": @@ -196,6 +200,11 @@ func ecosystemBadgeClasses(ecosystem string) string { } func getRegistryConfigs(baseURL string) []RegistryConfig { + swiftInsecureFlag := "" + if strings.HasPrefix(strings.ToLower(baseURL), "http://") { + swiftInsecureFlag = "--allow-insecure-http " + } + return []RegistryConfig{ { ID: "npm", @@ -396,6 +405,15 @@ local({

Or inside a running session:

ENV["JULIA_PKG_SERVER"] = "` + baseURL + `/julia"
 using Pkg; Pkg.update()
`), + }, + { + ID: "swift", + Name: "Swift Package Registry", + Language: "Swift", + Endpoint: "/swift/", + Instructions: template.HTML(`

Configure SwiftPM to use the proxy for this project:

+
swift package-registry set ` + swiftInsecureFlag + baseURL + `/swift
+

Use scoped package identifiers in Package.swift, for example apple.swift-argument-parser.

`), }, { ID: "oci", diff --git a/internal/server/eviction_test.go b/internal/server/eviction_test.go index 80badbe..9fa9e6b 100644 --- a/internal/server/eviction_test.go +++ b/internal/server/eviction_test.go @@ -15,7 +15,7 @@ import ( "github.com/git-pkgs/proxy/internal/storage" ) -func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) { +func setupEvictionTest(t *testing.T) (*database.DB, *storage.Filesystem) { t.Helper() tempDir := t.TempDir() @@ -27,7 +27,7 @@ func setupEvictionTest(t *testing.T) (*database.DB, *storage.Blob) { t.Fatalf("failed to create database: %v", err) } - store, err := storage.OpenBucket(context.Background(), "file://"+storagePath) + store, err := storage.NewFilesystem(storagePath) if err != nil { _ = db.Close() t.Fatalf("failed to create storage: %v", err) @@ -243,7 +243,7 @@ func TestStartEvictionLoop_UnlimitedSkips(t *testing.T) { } defer func() { _ = db.Close() }() - store, err := storage.OpenBucket(context.Background(), "file://"+storagePath) + store, err := storage.NewFilesystem(storagePath) if err != nil { t.Fatalf("failed to create storage: %v", err) } @@ -280,7 +280,7 @@ func defaultTestConfig(storagePath, dbPath string) *config.Config { return &config.Config{ Listen: ":8080", BaseURL: "http://localhost:8080", - Storage: config.StorageConfig{URL: "file://" + storagePath, MaxSize: ""}, + Storage: config.StorageConfig{Path: storagePath, MaxSize: ""}, Database: config.DatabaseConfig{ Driver: "sqlite", Path: dbPath, diff --git a/internal/server/health_test.go b/internal/server/health_test.go index 3b7eae8..c0f70c9 100644 --- a/internal/server/health_test.go +++ b/internal/server/health_test.go @@ -32,7 +32,7 @@ type fakeStorage struct { // Failure injection. storeErr error openErr error - readErr error // returned by the io.ReadCloser.Read after partial bytes + readErr error // returned by the io.ReadCloser.Read after partial bytes deleteErr error // Misbehavior knobs. @@ -132,8 +132,8 @@ func (f *fakeStorage) SignedURL(ctx context.Context, path string, expiry time.Du return "", storage.ErrSignedURLUnsupported } func (f *fakeStorage) UsedSpace(ctx context.Context) (int64, error) { return 0, nil } -func (f *fakeStorage) URL() string { return "fake://" } -func (f *fakeStorage) Close() error { return nil } +func (f *fakeStorage) URL() string { return "fake://" } +func (f *fakeStorage) Close() error { return nil } // --- Tests follow. First test: happy path --- diff --git a/internal/server/layout.go b/internal/server/layout.go index 2da9469..ef39858 100644 --- a/internal/server/layout.go +++ b/internal/server/layout.go @@ -2,24 +2,17 @@ package server import "net/http" -// BuildInfo identifies the running proxy binary. -type BuildInfo struct { - Version string - Commit string -} - -// Layout carries shared fields consumed by the base template. It is embedded -// in every page data struct so templates can access canonical URL and build -// information alongside the page's own fields. +// Layout carries per-request fields consumed by the shared base template +// (canonical URL, og:url). It is embedded in every page data struct so that +// templates can reference {{.UIBaseURL}} and {{.CanonicalPath}} alongside the +// page's own fields. type Layout struct { - BuildInfo BuildInfo UIBaseURL string CanonicalPath string } func (s *Server) layoutFor(r *http.Request) Layout { return Layout{ - BuildInfo: s.buildInfo, UIBaseURL: s.cfg.UIBaseURL, CanonicalPath: r.URL.Path, } diff --git a/internal/server/middleware.go b/internal/server/middleware.go index b6d483f..52f9b91 100644 --- a/internal/server/middleware.go +++ b/internal/server/middleware.go @@ -92,3 +92,17 @@ func requestEcosystem(path string) string { return "other" } } + +// ActiveRequestsMiddleware tracks the number of active requests using Prometheus metrics. +func ActiveRequestsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Don't track metrics endpoint itself + if r.URL.Path == "/metrics" { + next.ServeHTTP(w, r) + return + } + + // Implemented in server.go where metrics package is imported + next.ServeHTTP(w, r) + }) +} diff --git a/internal/server/middleware_test.go b/internal/server/middleware_test.go index 38905b2..eb81881 100644 --- a/internal/server/middleware_test.go +++ b/internal/server/middleware_test.go @@ -73,6 +73,36 @@ func TestGetRequestID(t *testing.T) { } } +func TestActiveRequestsMiddleware(t *testing.T) { + handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } +} + +func TestActiveRequestsMiddleware_SkipsMetricsEndpoint(t *testing.T) { + handler := ActiveRequestsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + + req := httptest.NewRequest(http.MethodGet, "/metrics", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Errorf("expected status 200, got %d", rec.Code) + } +} + func TestLoggerMiddleware(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) s := &Server{logger: logger} diff --git a/internal/server/server.go b/internal/server/server.go index bb964e8..c4f5e23 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -16,6 +16,7 @@ // - /conda/* - Conda/Anaconda protocol // - /cran/* - CRAN (R) protocol // - /julia/* - Julia Pkg server protocol +// - /swift/* - Swift Package Registry protocol // - /v2/* - OCI/Docker container registry protocol // - /debian/* - Debian/APT repository protocol // - /rpm/* - RPM/Yum repository protocol @@ -69,8 +70,8 @@ import ( upstreamhttp "github.com/git-pkgs/proxy/internal/httpclient" "github.com/git-pkgs/proxy/internal/metrics" "github.com/git-pkgs/proxy/internal/mirror" + "github.com/git-pkgs/proxy/internal/packageurl" "github.com/git-pkgs/proxy/internal/storage" - "github.com/git-pkgs/purl" "github.com/git-pkgs/registries/fetch" "github.com/git-pkgs/registries/safehttp" "github.com/git-pkgs/spdx" @@ -92,7 +93,6 @@ type Server struct { db *database.DB storage storage.Storage logger *slog.Logger - buildInfo BuildInfo http *http.Server templates *Templates cancel context.CancelFunc @@ -101,7 +101,7 @@ type Server struct { } // New creates a new Server with the given configuration. -func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, error) { +func New(cfg *config.Config, logger *slog.Logger) (*Server, error) { var activityLog *accesslog.Logger if cfg.AccessLog.Path != "" { var err error @@ -170,7 +170,6 @@ func New(cfg *config.Config, logger *slog.Logger, buildInfo BuildInfo) (*Server, db: db, storage: store, logger: logger, - buildInfo: buildInfo, templates: &Templates{}, healthCache: hc, accessLog: activityLog, @@ -261,6 +260,7 @@ func (s *Server) Start() error { condaHandler := handler.NewCondaHandler(proxy, s.cfg.BaseURL) cranHandler := handler.NewCRANHandler(proxy, s.cfg.BaseURL) juliaHandler := handler.NewJuliaHandler(proxy, s.cfg.BaseURL) + swiftHandler := handler.NewSwiftHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Swift) containerHandler := handler.NewContainerHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.OCI) helmHandler := handler.NewHelmHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Helm) debianHandler := handler.NewDebianHandler(proxy, s.cfg.BaseURL, s.cfg.Upstream.Debian) @@ -281,6 +281,7 @@ func (s *Server) Start() error { r.Mount("/conda", http.StripPrefix("/conda", condaHandler.Routes())) r.Mount("/cran", http.StripPrefix("/cran", cranHandler.Routes())) r.Mount("/julia", http.StripPrefix("/julia", juliaHandler.Routes())) + r.Mount("/swift", http.StripPrefix("/swift", swiftHandler.Routes())) r.Mount("/v2", http.StripPrefix("/v2", containerHandler.Routes())) r.Mount("/helm", http.StripPrefix("/helm", helmHandler.Routes())) r.Mount("/debian", http.StripPrefix("/debian", debianHandler.Routes())) @@ -815,7 +816,7 @@ func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem, return } - versionPURL := purl.MakePURLString(ecosystem, name, version) + versionPURL := packageurl.WithVersionString(pkg.PURL, version) ver, err := s.db.GetVersionByPURL(versionPURL) if err != nil || ver == nil { s.logger.Error("failed to get version", "error", err) @@ -857,6 +858,14 @@ func (s *Server) showVersion(w http.ResponseWriter, r *http.Request, ecosystem, } } +func (s *Server) cachedVersionPURL(ecosystem, name, version string) string { + pkg, err := s.db.GetPackageByEcosystemName(ecosystem, name) + if err != nil || pkg == nil { + return "" + } + return packageurl.WithVersionString(pkg.PURL, version) +} + func (s *Server) showBrowseSource(w http.ResponseWriter, r *http.Request, ecosystem, name, version string) { data := BrowseSourceData{ Layout: s.layoutFor(r), diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 77c32ae..db788d1 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -1,7 +1,6 @@ package server import ( - "context" "database/sql" "encoding/json" "fmt" @@ -52,7 +51,7 @@ func newTestServer(t *testing.T) *testServer { t.Fatalf("failed to create database: %v", err) } - store, err := storage.OpenBucket(context.Background(), "file://"+storagePath) + store, err := storage.NewFilesystem(storagePath) if err != nil { _ = db.Close() _ = os.RemoveAll(tempDir) @@ -66,7 +65,7 @@ func newTestServer(t *testing.T) *testServer { cfg := &config.Config{ BaseURL: "http://localhost:8080", - Storage: config.StorageConfig{URL: "file://" + storagePath}, + Storage: config.StorageConfig{Path: storagePath}, Database: config.DatabaseConfig{Path: dbPath}, } @@ -84,6 +83,7 @@ func newTestServer(t *testing.T) *testServer { goHandler := handler.NewGoHandler(proxy, cfg.BaseURL) pypiHandler := handler.NewPyPIHandler(proxy, cfg.BaseURL) gradleHandler := handler.NewGradleBuildCacheHandler(proxy) + swiftHandler := handler.NewSwiftHandler(proxy, cfg.BaseURL, cfg.Upstream.Swift) r.Mount("/npm", http.StripPrefix("/npm", npmHandler.Routes())) r.Mount("/cargo", http.StripPrefix("/cargo", cargoHandler.Routes())) @@ -91,6 +91,7 @@ func newTestServer(t *testing.T) *testServer { r.Mount("/go", http.StripPrefix("/go", goHandler.Routes())) r.Mount("/pypi", http.StripPrefix("/pypi", pypiHandler.Routes())) r.Mount("/gradle", http.StripPrefix("/gradle", gradleHandler.Routes())) + r.Mount("/swift", http.StripPrefix("/swift", swiftHandler.Routes())) hc, err := newHealthCache(store, "30s", logger) if err != nil { @@ -105,7 +106,6 @@ func newTestServer(t *testing.T) *testServer { db: db, storage: store, logger: logger, - buildInfo: BuildInfo{Version: "test-version", Commit: "test-commit"}, templates: &Templates{}, healthCache: hc, } @@ -315,9 +315,6 @@ func TestDashboard(t *testing.T) { if !strings.Contains(body, "Cached Artifacts") { t.Error("dashboard should contain stats") } - if !strings.Contains(body, "proxy test-version (test-commit)") { - t.Error("dashboard footer should contain build information") - } if !strings.Contains(body, "Popular Packages") { t.Error("dashboard should contain popular packages section") } @@ -333,11 +330,53 @@ func TestDashboard(t *testing.T) { if !strings.Contains(body, ">debian<") { t.Error("dashboard should show debian in supported ecosystems") } + if !strings.Contains(body, ">swift<") { + t.Error("dashboard should show swift in supported ecosystems") + } if !strings.Contains(body, "/openapi.json") { t.Error("page should link to the OpenAPI JSON spec") } } +func TestSwiftHandlerMounted(t *testing.T) { + ts := newTestServer(t) + defer ts.close() + + req := httptest.NewRequest(http.MethodPut, "/swift/apple/example/1.2.3", strings.NewReader("ignored")) + w := httptest.NewRecorder() + ts.handler.ServeHTTP(w, req) + + if w.Code != http.StatusMethodNotAllowed { + t.Fatalf("status = %d, want 405; body: %s", w.Code, w.Body.String()) + } +} + +func TestSwiftCachedVersionPURLUsesStoredPackagePURL(t *testing.T) { + ts := newTestServer(t) + defer ts.close() + + packagePURL := "pkg:generic/swift-registry/apple.example?repository_url=https:%2F%2Fold.example%2Fswift" + if err := ts.db.UpsertPackage(&database.Package{ + PURL: packagePURL, + Ecosystem: "swift", + Name: "apple/example", + }); err != nil { + t.Fatalf("failed to upsert package: %v", err) + } + + s := &Server{ + cfg: &config.Config{ + Upstream: config.UpstreamConfig{Swift: "https://new.example/swift"}, + }, + db: ts.db, + } + got := s.cachedVersionPURL("swift", "apple/example", "1.2.3") + want := "pkg:generic/swift-registry/apple.example@1.2.3?repository_url=https:%2F%2Fold.example%2Fswift" + if got != want { + t.Errorf("cachedVersionPURL() = %q, want %q", got, want) + } +} + func min(a, b int) int { if a < b { return a @@ -603,9 +642,6 @@ func TestVersionShowWithHitCount(t *testing.T) { if !strings.Contains(body, "42 cache hits") { t.Error("expected page to show hit count") } - if !strings.Contains(body, "proxy test-version (test-commit)") { - t.Error("version show footer should contain proxy build information, not the package version") - } } func TestSearchWithNullValues(t *testing.T) { @@ -1335,14 +1371,10 @@ func TestNewServer_StorageConnectivityCheck(t *testing.T) { logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - buildInfo := BuildInfo{Version: "test-version", Commit: "test-commit"} - srv, err := New(cfg, logger, buildInfo) + srv, err := New(cfg, logger) if err != nil { t.Fatalf("New() failed: %v", err) } - if srv.buildInfo != buildInfo { - t.Errorf("build info = %#v, want %#v", srv.buildInfo, buildInfo) - } // On Windows, OpenBucket normalises to file:///C:/path; on Unix the // absolute path already starts with /, so file:// + /path == file:///path. @@ -1360,13 +1392,13 @@ func TestNewServer_InvalidAccessLogFailsBeforeDatabaseInit(t *testing.T) { tempDir := t.TempDir() dbPath := filepath.Join(tempDir, "test.db") cfg := &config.Config{ - Storage: config.StorageConfig{URL: "file://" + filepath.Join(tempDir, "artifacts")}, + Storage: config.StorageConfig{Path: filepath.Join(tempDir, "artifacts")}, Database: config.DatabaseConfig{Path: dbPath}, AccessLog: config.AccessLogConfig{Path: filepath.Join(tempDir, "missing", "access.jsonl")}, } logger := slog.New(slog.NewTextHandler(io.Discard, nil)) - if _, err := New(cfg, logger, BuildInfo{}); err == nil { + if _, err := New(cfg, logger); err == nil { t.Fatal("New() succeeded with invalid access log path") } else if !strings.Contains(err.Error(), "initializing access log") { t.Fatalf("New() error = %v, want access log initialization error", err) diff --git a/internal/server/templates/layout/footer.html b/internal/server/templates/layout/footer.html index 33daddf..5aa970d 100644 --- a/internal/server/templates/layout/footer.html +++ b/internal/server/templates/layout/footer.html @@ -12,11 +12,6 @@ github.com/git-pkgs/proxy

- {{if .BuildInfo.Version}} -

- proxy {{.BuildInfo.Version}}{{if .BuildInfo.Commit}} ({{.BuildInfo.Commit}}){{end}} -

- {{end}}

Resources

diff --git a/internal/server/templates_test.go b/internal/server/templates_test.go index 158278b..d42b5c1 100644 --- a/internal/server/templates_test.go +++ b/internal/server/templates_test.go @@ -186,64 +186,6 @@ func TestRenderEmitsCanonicalAndOG(t *testing.T) { } } -func TestFooterUsesBuildInfoWhenPageDefinesVersion(t *testing.T) { - templates := &Templates{} - buildInfo := BuildInfo{Version: "proxy-build-1.2.3", Commit: "abc123def"} - wantFooter := "proxy proxy-build-1.2.3 (abc123def)" - - tests := []struct { - name string - page string - data any - shadow string - }{ - { - name: "version show page", - page: "version_show", - data: VersionShowData{ - Layout: Layout{BuildInfo: buildInfo}, - Package: &database.Package{ - PURL: "pkg:npm/lodash", - Ecosystem: "npm", - Name: "lodash", - }, - Version: &database.Version{ - PURL: "pkg:npm/lodash@9.9.9", - PackagePURL: "pkg:npm/lodash", - }, - }, - shadow: "9.9.9", - }, - { - name: "browse source page", - page: "browse_source", - data: BrowseSourceData{ - Layout: Layout{BuildInfo: buildInfo}, - Ecosystem: "npm", - PackageName: "lodash", - Version: "9.9.9", - }, - shadow: "9.9.9", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - w := httptest.NewRecorder() - if err := templates.Render(w, tt.page, tt.data); err != nil { - t.Fatalf("Render(%q) failed: %v", tt.page, err) - } - body := w.Body.String() - if !strings.Contains(body, wantFooter) { - t.Errorf("footer missing build info %q", wantFooter) - } - if strings.Contains(body, "proxy "+tt.shadow) { - t.Errorf("footer used page Version %q instead of BuildInfo", tt.shadow) - } - }) - } -} - func TestRenderOmitsCanonicalWhenUIBaseURLUnset(t *testing.T) { templates := &Templates{} @@ -519,6 +461,20 @@ func TestEcosystemBadgeLabel(t *testing.T) { } } +func TestSwiftRegistryInstructionsAllowLocalHTTP(t *testing.T) { + registries := getRegistryConfigs("http://localhost:8080") + for _, registry := range registries { + if registry.ID != "swift" { + continue + } + if !strings.Contains(string(registry.Instructions), "--allow-insecure-http") { + t.Error("Swift HTTP instructions do not allow the insecure local registry") + } + return + } + t.Fatal("Swift registry instructions not found") +} + func TestEcosystemBadgeClasses(t *testing.T) { // Every supported ecosystem should return a non-empty class string ecosystems := supportedEcosystems() diff --git a/internal/storage/filesystem.go b/internal/storage/filesystem.go new file mode 100644 index 0000000..d509e9d --- /dev/null +++ b/internal/storage/filesystem.go @@ -0,0 +1,277 @@ +package storage + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "io" + fsys "io/fs" + "os" + "path/filepath" + "strings" + "time" +) + +// Filesystem implements Storage using the local filesystem. +type Filesystem struct { + root string +} + +// NewFilesystem creates a new filesystem storage rooted at the given directory. +// The directory will be created if it does not exist. +func NewFilesystem(root string) (*Filesystem, error) { + absRoot, err := filepath.Abs(root) + if err != nil { + return nil, fmt.Errorf("resolving root path: %w", err) + } + + if err := os.MkdirAll(absRoot, dirPermissions); err != nil { + return nil, fmt.Errorf("creating root directory: %w", err) + } + + return &Filesystem{root: absRoot}, nil +} + +func (fs *Filesystem) fullPath(path string) (string, error) { + localPath, err := cleanStoragePath(path) + if err != nil { + return "", err + } + return filepath.Join(fs.root, localPath), nil +} + +func (fs *Filesystem) prefixPath(prefix string) (string, error) { + if prefix == "" { + return fs.root, nil + } + return fs.fullPath(prefix) +} + +func cleanStoragePath(path string) (string, error) { + if path == "." || strings.Contains(path, `\`) || !filepath.IsLocal(path) { + return "", fmt.Errorf("%w: invalid storage path", ErrNotFound) + } + + localPath, err := filepath.Localize(path) + if err != nil || localPath == "." || !filepath.IsLocal(localPath) { + return "", fmt.Errorf("%w: invalid storage path", ErrNotFound) + } + + return localPath, nil +} + +func (fs *Filesystem) Store(ctx context.Context, path string, r io.Reader) (int64, string, error) { + fullPath, err := fs.fullPath(path) + if err != nil { + return 0, "", err + } + + dir := filepath.Dir(fullPath) + if err := os.MkdirAll(dir, dirPermissions); err != nil { + return 0, "", fmt.Errorf("creating directory: %w", err) + } + + // Write to temp file first for atomic operation + tmpFile, err := os.CreateTemp(dir, ".tmp-*") + if err != nil { + return 0, "", fmt.Errorf("creating temp file: %w", err) + } + tmpPath := tmpFile.Name() + + // Clean up temp file on error + success := false + defer func() { + if !success { + _ = tmpFile.Close() + _ = os.Remove(tmpPath) + } + }() + + // Write content and compute hash + h := sha256.New() + w := io.MultiWriter(tmpFile, h) + + size, err := io.Copy(w, r) + if err != nil { + return 0, "", fmt.Errorf("writing content: %w", err) + } + + if err := tmpFile.Close(); err != nil { + return 0, "", fmt.Errorf("closing temp file: %w", err) + } + + // Atomic rename + if err := os.Rename(tmpPath, fullPath); err != nil { + return 0, "", fmt.Errorf("renaming temp file: %w", err) + } + + success = true + hash := hex.EncodeToString(h.Sum(nil)) + return size, hash, nil +} + +func (fs *Filesystem) Open(ctx context.Context, path string) (io.ReadCloser, error) { + fullPath, err := fs.fullPath(path) + if err != nil { + return nil, err + } + + f, err := os.Open(fullPath) + if err != nil { + if os.IsNotExist(err) { + return nil, ErrNotFound + } + return nil, fmt.Errorf("opening file: %w", err) + } + + return f, nil +} + +func (fs *Filesystem) Exists(ctx context.Context, path string) (bool, error) { + fullPath, err := fs.fullPath(path) + if err != nil { + return false, err + } + + _, err = os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("checking file: %w", err) + } + + return true, nil +} + +func (fs *Filesystem) Delete(ctx context.Context, path string) error { + fullPath, err := fs.fullPath(path) + if err != nil { + return err + } + + err = os.Remove(fullPath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("removing file: %w", err) + } + + // Try to clean up empty parent directories + dir := filepath.Dir(fullPath) + for dir != fs.root { + if err := os.Remove(dir); err != nil { + break // Directory not empty or other error + } + dir = filepath.Dir(dir) + } + + return nil +} + +func (fs *Filesystem) SignedURL(_ context.Context, _ string, _ time.Duration) (string, error) { + return "", ErrSignedURLUnsupported +} + +func (fs *Filesystem) Size(ctx context.Context, path string) (int64, error) { + fullPath, err := fs.fullPath(path) + if err != nil { + return 0, err + } + + info, err := os.Stat(fullPath) + if err != nil { + if os.IsNotExist(err) { + return 0, ErrNotFound + } + return 0, fmt.Errorf("stat file: %w", err) + } + + return info.Size(), nil +} + +func (fs *Filesystem) UsedSpace(ctx context.Context) (int64, error) { + var total int64 + + err := filepath.Walk(fs.root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if !info.IsDir() { + total += info.Size() + } + return nil + }) + if err != nil { + return 0, fmt.Errorf("walking directory: %w", err) + } + + return total, nil +} + +// ListPrefix returns object metadata for paths under a prefix. +func (fs *Filesystem) ListPrefix(ctx context.Context, prefix string) ([]ObjectInfo, error) { + searchRoot, err := fs.prefixPath(prefix) + if err != nil { + return nil, err + } + + if _, err := os.Stat(searchRoot); err != nil { + if os.IsNotExist(err) { + return []ObjectInfo{}, nil + } + return nil, fmt.Errorf("stat prefix: %w", err) + } + + objects := make([]ObjectInfo, 0) + err = filepath.WalkDir(searchRoot, func(path string, entry fsys.DirEntry, err error) error { + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + + info, err := entry.Info() + if err != nil { + return err + } + + relPath, err := filepath.Rel(fs.root, path) + if err != nil { + return err + } + + objects = append(objects, ObjectInfo{ + Path: filepath.ToSlash(relPath), + Size: info.Size(), + ModTime: info.ModTime(), + }) + + return nil + }) + if err != nil { + return nil, fmt.Errorf("walking prefix: %w", err) + } + + return objects, nil +} + +// Root returns the root directory of the storage. +func (fs *Filesystem) Root() string { + return fs.root +} + +// FullPath returns the full filesystem path for a storage path. +// Useful for serving files directly or debugging. +// Returns an error if the resulting path would escape the storage root. +func (fs *Filesystem) FullPath(path string) (string, error) { + return fs.fullPath(path) +} + +func (fs *Filesystem) URL() string { + return "file://" + filepath.ToSlash(fs.root) +} + +func (fs *Filesystem) Close() error { + return nil +} diff --git a/internal/storage/filesystem_test.go b/internal/storage/filesystem_test.go new file mode 100644 index 0000000..de0e418 --- /dev/null +++ b/internal/storage/filesystem_test.go @@ -0,0 +1,331 @@ +package storage + +import ( + "context" + "crypto/sha256" + "encoding/hex" + "errors" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" +) + +func TestNewFilesystem(t *testing.T) { + dir := t.TempDir() + root := filepath.Join(dir, "cache") + + fs, err := NewFilesystem(root) + if err != nil { + t.Fatalf("NewFilesystem failed: %v", err) + } + + if _, err := os.Stat(root); err != nil { + t.Errorf("root directory not created: %v", err) + } + + if fs.Root() != root { + t.Errorf("Root() = %q, want %q", fs.Root(), root) + } +} + +func TestFilesystemStore(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + content := "test content for storage" + + size, hash, err := fs.Store(ctx, "npm/lodash/4.17.21/lodash.tgz", strings.NewReader(content)) + if err != nil { + t.Fatalf("Store failed: %v", err) + } + + if size != int64(len(content)) { + t.Errorf("size = %d, want %d", size, len(content)) + } + + h := sha256.Sum256([]byte(content)) + wantHash := hex.EncodeToString(h[:]) + if hash != wantHash { + t.Errorf("hash = %s, want %s", hash, wantHash) + } + + // Verify file exists on disk + fullPath, err := fs.FullPath("npm/lodash/4.17.21/lodash.tgz") + if err != nil { + t.Fatalf("FullPath failed: %v", err) + } + data, err := os.ReadFile(fullPath) + if err != nil { + t.Fatalf("reading stored file: %v", err) + } + if string(data) != content { + t.Errorf("stored content = %q, want %q", string(data), content) + } +} + +func TestFilesystemStoreAtomic(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + // Store initial content + _, _, err := fs.Store(ctx, "test/file.txt", strings.NewReader("initial")) + if err != nil { + t.Fatalf("initial Store failed: %v", err) + } + + // Overwrite with new content + _, _, err = fs.Store(ctx, "test/file.txt", strings.NewReader("updated")) + if err != nil { + t.Fatalf("update Store failed: %v", err) + } + + // Verify updated content + r, err := fs.Open(ctx, "test/file.txt") + if err != nil { + t.Fatalf("Open failed: %v", err) + } + defer func() { _ = r.Close() }() + + data, _ := io.ReadAll(r) + if string(data) != "updated" { + t.Errorf("content = %q, want %q", string(data), "updated") + } +} + +func TestFilesystemOpen(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + content := "readable content" + + _, _, _ = fs.Store(ctx, "test/read.txt", strings.NewReader(content)) + + r, err := fs.Open(ctx, "test/read.txt") + if err != nil { + t.Fatalf("Open failed: %v", err) + } + defer func() { _ = r.Close() }() + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("ReadAll failed: %v", err) + } + if string(data) != content { + t.Errorf("content = %q, want %q", string(data), content) + } +} + +func TestFilesystemOpenNotFound(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + _, err := fs.Open(ctx, "does/not/exist.txt") + if !errors.Is(err, ErrNotFound) { + t.Errorf("Open non-existent = %v, want ErrNotFound", err) + } +} + +func TestFilesystemExists(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + exists, err := fs.Exists(ctx, "test/exists.txt") + if err != nil { + t.Fatalf("Exists failed: %v", err) + } + if exists { + t.Error("Exists returned true for non-existent file") + } + + _, _, _ = fs.Store(ctx, "test/exists.txt", strings.NewReader("content")) + + exists, err = fs.Exists(ctx, "test/exists.txt") + if err != nil { + t.Fatalf("Exists after store failed: %v", err) + } + if !exists { + t.Error("Exists returned false for existing file") + } +} + +func TestFilesystemDelete(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + _, _, _ = fs.Store(ctx, "test/delete/nested/file.txt", strings.NewReader("content")) + + err := fs.Delete(ctx, "test/delete/nested/file.txt") + if err != nil { + t.Fatalf("Delete failed: %v", err) + } + + exists, _ := fs.Exists(ctx, "test/delete/nested/file.txt") + if exists { + t.Error("file still exists after delete") + } + + // Empty parent directories should be cleaned up + nestedDir, err := fs.FullPath("test/delete/nested") + if err != nil { + t.Fatalf("FullPath failed: %v", err) + } + if _, err := os.Stat(nestedDir); !os.IsNotExist(err) { + t.Error("empty nested directory not cleaned up") + } +} + +func TestFilesystemDeleteNotFound(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + // Delete non-existent file should not error + err := fs.Delete(ctx, "does/not/exist.txt") + if err != nil { + t.Errorf("Delete non-existent = %v, want nil", err) + } +} + +func TestFilesystemSize(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + content := "size test content" + + _, _, _ = fs.Store(ctx, "test/size.txt", strings.NewReader(content)) + + size, err := fs.Size(ctx, "test/size.txt") + if err != nil { + t.Fatalf("Size failed: %v", err) + } + if size != int64(len(content)) { + t.Errorf("Size = %d, want %d", size, len(content)) + } +} + +func TestFilesystemSizeNotFound(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + _, err := fs.Size(ctx, "does/not/exist.txt") + if !errors.Is(err, ErrNotFound) { + t.Errorf("Size non-existent = %v, want ErrNotFound", err) + } +} + +func TestFilesystemUsedSpace(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + // Empty storage + used, err := fs.UsedSpace(ctx) + if err != nil { + t.Fatalf("UsedSpace failed: %v", err) + } + if used != 0 { + t.Errorf("UsedSpace empty = %d, want 0", used) + } + + // Add some files + _, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa")) // 4 bytes + _, _, _ = fs.Store(ctx, "b.txt", strings.NewReader("bbbbbb")) // 6 bytes + _, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc")) // 5 bytes + + used, err = fs.UsedSpace(ctx) + if err != nil { + t.Fatalf("UsedSpace failed: %v", err) + } + if used != 15 { + t.Errorf("UsedSpace = %d, want 15", used) + } +} + +func TestFilesystemLargeFile(t *testing.T) { + assertLargeFileRoundTrip(t, createTestFilesystem(t)) +} + +func TestFilesystemRejectsInvalidPaths(t *testing.T) { + tmp := t.TempDir() + fs, err := NewFilesystem(tmp) + if err != nil { + t.Fatal(err) + } + for _, p := range []string{ + "", + ".", + "../etc/passwd", + "../../etc/passwd", + "a/../../etc/passwd", + "/etc/passwd", + "test//file.txt", + "test/./file.txt", + "test/../file.txt", + `test\..\file.txt`, + } { + name := p + if name == "" { + name = "empty" + } + + t.Run(name, func(t *testing.T) { + ctx := context.Background() + + if _, err := fs.FullPath(p); !errors.Is(err, ErrNotFound) { + t.Errorf("FullPath(%q) = %v, want ErrNotFound", p, err) + } + if _, err := fs.Open(ctx, p); err == nil { + t.Errorf("Open(%q) should reject invalid path", p) + } + if _, _, err := fs.Store(ctx, p, strings.NewReader("x")); err == nil { + t.Errorf("Store(%q) should reject invalid path", p) + } + if _, err := fs.Exists(ctx, p); err == nil { + t.Errorf("Exists(%q) should reject invalid path", p) + } + if err := fs.Delete(ctx, p); err == nil { + t.Errorf("Delete(%q) should reject invalid path", p) + } + if _, err := fs.Size(ctx, p); err == nil { + t.Errorf("Size(%q) should reject invalid path", p) + } + if _, err := fs.ListPrefix(ctx, p); p != "" && err == nil { + t.Errorf("ListPrefix(%q) should reject invalid path", p) + } + }) + } +} + +func TestFilesystemListPrefixAllowsEmptyPrefix(t *testing.T) { + fs := createTestFilesystem(t) + ctx := context.Background() + + _, _, _ = fs.Store(ctx, "a.txt", strings.NewReader("aaaa")) + _, _, _ = fs.Store(ctx, "c/d.txt", strings.NewReader("ccccc")) + + objects, err := fs.ListPrefix(ctx, "") + if err != nil { + t.Fatalf("ListPrefix empty prefix failed: %v", err) + } + if len(objects) != 2 { + t.Fatalf("ListPrefix empty prefix returned %d objects, want 2", len(objects)) + } +} + +func TestFilesystemSignedURLUnsupported(t *testing.T) { + fs := createTestFilesystem(t) + + _, err := fs.SignedURL(context.Background(), "test/file.txt", time.Minute) + if !errors.Is(err, ErrSignedURLUnsupported) { + t.Errorf("SignedURL = %v, want ErrSignedURLUnsupported", err) + } +} + +func createTestFilesystem(t *testing.T) *Filesystem { + t.Helper() + dir := t.TempDir() + + fs, err := NewFilesystem(dir) + if err != nil { + t.Fatalf("NewFilesystem failed: %v", err) + } + return fs +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 3d0be1c..e11db53 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -11,6 +11,8 @@ package storage import ( "context" + "crypto/sha256" + "encoding/hex" "errors" "io" "time" @@ -78,3 +80,42 @@ func ArtifactPath(ecosystem, namespace, name, version, filename string) string { } return ecosystem + "/" + name + "/" + version + "/" + filename } + +// HashingReader wraps a reader and computes SHA256 hash as content is read. +type HashingReader struct { + r io.Reader + hash []byte + h interface{ Sum([]byte) []byte } + size int64 + done bool +} + +func NewHashingReader(r io.Reader) *HashingReader { + h := sha256.New() + return &HashingReader{ + r: io.TeeReader(r, h), + h: h, + } +} + +func (hr *HashingReader) Read(p []byte) (n int, err error) { + n, err = hr.r.Read(p) + hr.size += int64(n) + if err == io.EOF { + hr.done = true + hr.hash = hr.h.Sum(nil) + } + return +} + +func (hr *HashingReader) Sum() string { + if !hr.done { + hr.hash = hr.h.Sum(nil) + hr.done = true + } + return hex.EncodeToString(hr.hash) +} + +func (hr *HashingReader) Size() int64 { + return hr.size +} diff --git a/internal/storage/storage_test.go b/internal/storage/storage_test.go index 65f5a23..97800f0 100644 --- a/internal/storage/storage_test.go +++ b/internal/storage/storage_test.go @@ -6,6 +6,7 @@ import ( "crypto/sha256" "encoding/hex" "io" + "strings" "testing" ) @@ -34,6 +35,30 @@ func TestArtifactPath(t *testing.T) { } } +func TestHashingReader(t *testing.T) { + content := "hello world" + r := NewHashingReader(strings.NewReader(content)) + + data, err := io.ReadAll(r) + if err != nil { + t.Fatalf("ReadAll failed: %v", err) + } + + if string(data) != content { + t.Errorf("got content %q, want %q", string(data), content) + } + + if r.Size() != int64(len(content)) { + t.Errorf("got size %d, want %d", r.Size(), len(content)) + } + + h := sha256.Sum256([]byte(content)) + wantHash := hex.EncodeToString(h[:]) + if r.Sum() != wantHash { + t.Errorf("got hash %s, want %s", r.Sum(), wantHash) + } +} + // assertLargeFileRoundTrip stores a 1MB file in the given storage, verifies size and // hash, then reads it back and confirms the content matches. func assertLargeFileRoundTrip(t *testing.T, s Storage) {