build(deps): bump the dependencies group across 2 directories with 12 updates

Bumps the dependencies group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [github.com/huandu/go-sqlbuilder](https://github.com/huandu/go-sqlbuilder) | `1.36.1` | `1.37.0` |
| [github.com/lestrrat-go/jwx/v3](https://github.com/lestrrat-go/jwx) | `3.0.10` | `3.0.11` |
| [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) | `1.23.0` | `1.23.2` |
| [github.com/spf13/cobra](https://github.com/spf13/cobra) | `1.9.1` | `1.10.1` |
| [github.com/spf13/viper](https://github.com/spf13/viper) | `1.20.1` | `1.21.0` |
| [golang.org/x/net](https://github.com/golang/net) | `0.43.0` | `0.44.0` |
| [golang.org/x/time](https://github.com/golang/time) | `0.12.0` | `0.13.0` |
| [google.golang.org/grpc](https://github.com/grpc/grpc-go) | `1.75.0` | `1.75.1` |
| google.golang.org/protobuf | `1.36.8` | `1.36.9` |

Bumps the dependencies group with 2 updates in the /e2e directory: [github.com/testcontainers/testcontainers-go](https://github.com/testcontainers/testcontainers-go) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `github.com/huandu/go-sqlbuilder` from 1.36.1 to 1.37.0
- [Release notes](https://github.com/huandu/go-sqlbuilder/releases)
- [Commits](https://github.com/huandu/go-sqlbuilder/compare/v1.36.1...v1.37.0)

Updates `github.com/lestrrat-go/jwx/v3` from 3.0.10 to 3.0.11
- [Release notes](https://github.com/lestrrat-go/jwx/releases)
- [Changelog](https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes)
- [Commits](https://github.com/lestrrat-go/jwx/compare/v3.0.10...v3.0.11)

Updates `github.com/prometheus/client_golang` from 1.23.0 to 1.23.2
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.23.0...v1.23.2)

Updates `github.com/spf13/cobra` from 1.9.1 to 1.10.1
- [Release notes](https://github.com/spf13/cobra/releases)
- [Commits](https://github.com/spf13/cobra/compare/v1.9.1...v1.10.1)

Updates `github.com/spf13/pflag` from 1.0.7 to 1.0.9
- [Release notes](https://github.com/spf13/pflag/releases)
- [Commits](https://github.com/spf13/pflag/compare/v1.0.7...v1.0.9)

Updates `github.com/spf13/viper` from 1.20.1 to 1.21.0
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.20.1...v1.21.0)

Updates `golang.org/x/net` from 0.43.0 to 0.44.0
- [Commits](https://github.com/golang/net/compare/v0.43.0...v0.44.0)

Updates `golang.org/x/time` from 0.12.0 to 0.13.0
- [Commits](https://github.com/golang/time/compare/v0.12.0...v0.13.0)

Updates `google.golang.org/grpc` from 1.75.0 to 1.75.1
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.75.0...v1.75.1)

Updates `google.golang.org/protobuf` from 1.36.8 to 1.36.9

Updates `github.com/testcontainers/testcontainers-go` from 0.38.0 to 0.39.0
- [Release notes](https://github.com/testcontainers/testcontainers-go/releases)
- [Commits](https://github.com/testcontainers/testcontainers-go/compare/v0.38.0...v0.39.0)

Updates `modernc.org/sqlite` from 1.38.2 to 1.39.0
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.38.2...v1.39.0)

---
updated-dependencies:
- dependency-name: github.com/huandu/go-sqlbuilder
  dependency-version: 1.37.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: github.com/lestrrat-go/jwx/v3
  dependency-version: 3.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: github.com/prometheus/client_golang
  dependency-version: 1.23.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: github.com/spf13/cobra
  dependency-version: 1.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: github.com/spf13/pflag
  dependency-version: 1.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: github.com/spf13/viper
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: golang.org/x/net
  dependency-version: 0.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: golang.org/x/time
  dependency-version: 0.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: google.golang.org/grpc
  dependency-version: 1.75.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: google.golang.org/protobuf
  dependency-version: 1.36.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: dependencies
- dependency-name: github.com/testcontainers/testcontainers-go
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
- dependency-name: modernc.org/sqlite
  dependency-version: 1.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-09-22 15:33:56 +00:00
committed by Stephan Renatus
parent 5d21f9ac13
commit d3d014d18d
201 changed files with 21132 additions and 5606 deletions
+20 -17
View File
@@ -12,12 +12,12 @@ require (
github.com/lib/pq v1.10.9
github.com/microsoft/go-mssqldb v1.9.3
github.com/open-policy-agent/opa v1.8.0
github.com/testcontainers/testcontainers-go v0.38.0
modernc.org/sqlite v1.38.2
github.com/testcontainers/testcontainers-go v0.39.0
modernc.org/sqlite v1.39.0
)
require (
dario.cat/mergo v1.0.1 // indirect
dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
@@ -58,13 +58,16 @@ require (
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/huandu/go-sqlbuilder v1.36.1 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/huandu/go-sqlbuilder v1.37.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.10 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/jwx/v3 v3.0.11 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
@@ -89,15 +92,15 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_golang v1.23.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.5 // indirect
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/tchap/go-patricia/v2 v2.3.3 // indirect
@@ -121,17 +124,17 @@ require (
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.41.0 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/time v0.12.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.13.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
google.golang.org/grpc v1.75.1 // indirect
google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.66.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
+45 -36
View File
@@ -1,5 +1,5 @@
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
@@ -124,10 +124,13 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
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/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-sqlbuilder v1.36.1 h1:4S17aR2BPW8L2PeotAD4iVhSMjwzk6CO8ABmp3tMEhY=
github.com/huandu/go-sqlbuilder v1.36.1/go.mod h1:59Zjq93ndlKI6O5kHmkXQpDgriBAPMKuhByEOy6xYK8=
github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=
github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
github.com/huandu/go-sqlbuilder v1.37.0 h1:hXgk2rTnlgFgKsmFpizhe6g/oz1wxef4qk3ixFhK6a0=
github.com/huandu/go-sqlbuilder v1.37.0/go.mod h1:zdONH67liL+/TvoUMwnZP/sUYGSSvHh9psLe/HpXn8E=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
@@ -142,12 +145,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w=
github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@@ -211,12 +218,12 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
@@ -232,13 +239,14 @@ github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v4 v4.25.5 h1:rtd9piuSMGeU8g1RMXjZs9y9luK5BwtnG7dZaQUJAsc=
github.com/shirou/gopsutil/v4 v4.25.5/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
@@ -246,8 +254,8 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc=
github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k=
github.com/testcontainers/testcontainers-go v0.38.0 h1:d7uEapLcv2P8AvH8ahLqDMMxda2W9gQN1nRbHS28HBw=
github.com/testcontainers/testcontainers-go v0.38.0/go.mod h1:C52c9MoHpWO+C4aqmgSU+hxlR5jlEayWtgYrb8Pzz1w=
github.com/testcontainers/testcontainers-go v0.39.0 h1:uCUJ5tA+fcxbFAB0uP3pIK3EJ2IjjDUHFSZ1H1UxAts=
github.com/testcontainers/testcontainers-go v0.39.0/go.mod h1:qmHpkG7H5uPf/EvOORKvS6EuDkBUPE3zpVGaH9NL7f8=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
@@ -299,8 +307,8 @@ go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
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.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -311,13 +319,13 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -329,16 +337,16 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -355,13 +363,14 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
@@ -387,8 +396,8 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek=
modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
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=
+24 -22
View File
@@ -18,20 +18,20 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hashicorp/golang-lru/v2 v2.0.7
github.com/huandu/go-sqlbuilder v1.36.1
github.com/lestrrat-go/jwx/v3 v3.0.10
github.com/huandu/go-sqlbuilder v1.37.0
github.com/lestrrat-go/jwx/v3 v3.0.11
github.com/olekukonko/tablewriter v0.0.5
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/peterh/liner v1.2.2
github.com/prometheus/client_golang v1.23.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9
github.com/sergi/go-diff v1.4.0
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af
github.com/spf13/cobra v1.9.1
github.com/spf13/pflag v1.0.7
github.com/spf13/viper v1.20.1
github.com/spf13/cobra v1.10.1
github.com/spf13/pflag v1.0.10
github.com/spf13/viper v1.21.0
github.com/tchap/go-patricia/v2 v2.3.3
github.com/vektah/gqlparser/v2 v2.5.30
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415
@@ -44,10 +44,10 @@ require (
go.opentelemetry.io/otel/sdk v1.37.0
go.opentelemetry.io/otel/trace v1.37.0
go.uber.org/automaxprocs v1.6.0
golang.org/x/net v0.43.0
golang.org/x/time v0.12.0
google.golang.org/grpc v1.75.0
google.golang.org/protobuf v1.36.8
golang.org/x/net v0.44.0
golang.org/x/time v0.13.0
google.golang.org/grpc v1.75.1
google.golang.org/protobuf v1.36.9
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c
gopkg.in/yaml.v3 v3.0.1
oras.land/oras-go/v2 v2.6.0
@@ -72,6 +72,7 @@ require (
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/flatbuffers v25.2.10+incompatible // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect
github.com/huandu/go-clone v1.7.3 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
@@ -79,8 +80,10 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.0 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -89,30 +92,29 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.65.0 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/segmentio/asm v1.2.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.41.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
+50 -45
View File
@@ -81,10 +81,13 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 h1:X5VWvz21y3gzm9Nw/kaUeku/1+u
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1/go.mod h1:Zanoh4+gvIgluNqcfMVTJueD4wSS5hT7zTt4Mrutd90=
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/huandu/go-assert v1.1.5/go.mod h1:yOLvuqZwmcHIC5rIzrBhT7D3Q9c3GFnd0JrPVhn/06U=
github.com/huandu/go-assert v1.1.6 h1:oaAfYxq9KNDi9qswn/6aE0EydfxSa+tWZC1KabNitYs=
github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs=
github.com/huandu/go-sqlbuilder v1.36.1 h1:4S17aR2BPW8L2PeotAD4iVhSMjwzk6CO8ABmp3tMEhY=
github.com/huandu/go-sqlbuilder v1.36.1/go.mod h1:59Zjq93ndlKI6O5kHmkXQpDgriBAPMKuhByEOy6xYK8=
github.com/huandu/go-clone v1.7.3 h1:rtQODA+ABThEn6J5LBTppJfKmZy/FwfpMUWa8d01TTQ=
github.com/huandu/go-clone v1.7.3/go.mod h1:ReGivhG6op3GYr+UY3lS6mxjKp7MIGTknuU5TbTVaXE=
github.com/huandu/go-sqlbuilder v1.37.0 h1:hXgk2rTnlgFgKsmFpizhe6g/oz1wxef4qk3ixFhK6a0=
github.com/huandu/go-sqlbuilder v1.37.0/go.mod h1:zdONH67liL+/TvoUMwnZP/sUYGSSvHh9psLe/HpXn8E=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -105,12 +108,16 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc/v3 v3.0.0 h1:nZUx/zFg5uc2rhlu1L1DidGr5Sj02JbXvGSpnY4LMrc=
github.com/lestrrat-go/httprc/v3 v3.0.0/go.mod h1:k2U1QIiyVqAKtkffbg+cUmsyiPGQsb9aAfNQiNFuQ9Q=
github.com/lestrrat-go/jwx/v3 v3.0.10 h1:XuoCBhZBncRIjMQ32HdEc76rH0xK/Qv2wq5TBouYJDw=
github.com/lestrrat-go/jwx/v3 v3.0.10/go.mod h1:kNMedLgTpHvPJkK5EMVa1JFz+UVyY2dMmZKu3qjl/Pk=
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
github.com/lestrrat-go/jwx/v3 v3.0.11 h1:yEeUGNUuNjcez/Voxvr7XPTYNraSQTENJgtVTfwvG/w=
github.com/lestrrat-go/jwx/v3 v3.0.11/go.mod h1:XSOAh2SiXm0QgRe3DulLZLyt+wUuEdFo81zuKTLcvgQ=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
@@ -141,12 +148,12 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc=
github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE=
github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg=
@@ -159,27 +166,27 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=
github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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=
@@ -230,12 +237,10 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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=
@@ -243,8 +248,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
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.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -264,8 +269,8 @@ 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.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -274,8 +279,8 @@ 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.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -290,8 +295,8 @@ 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.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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=
@@ -306,10 +311,10 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
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.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
@@ -330,10 +335,10 @@ google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c h1:
google.golang.org/genproto/googleapis/api v0.0.0-20250818200422-3122310a409c/go.mod h1:ea2MjsO70ssTfCjiwHgI0ZFqcw45Ksuk2ckf9G468GA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c h1:qXWI/sQtv5UKboZ/zUk7h+mrf/lXORyI+n9DKDAusdg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c/go.mod h1:gw1tLEfykwDz2ET4a12jcXt4couGAm7IwsVaTy0Sflo=
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+40
View File
@@ -0,0 +1,40 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
# Intellij
*.iml
.idea/
# VS Code
debug
debug_test
.vscode/
# Mac
.DS_Store
# go workspace
go.work
go.work.sum
@@ -1,19 +1,19 @@
Copyright (c) 2017-2021 Uber Technologies, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Copyright (c) 2019 Huan Du
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+296
View File
@@ -0,0 +1,296 @@
# go-clone: Clone any Go data structure deeply and thoroughly
[![Go](https://github.com/huandu/go-clone/workflows/Go/badge.svg)](https://github.com/huandu/go-clone/actions)
[![Go Doc](https://godoc.org/github.com/huandu/go-clone?status.svg)](https://pkg.go.dev/github.com/huandu/go-clone)
[![Go Report](https://goreportcard.com/badge/github.com/huandu/go-clone)](https://goreportcard.com/report/github.com/huandu/go-clone)
[![Coverage Status](https://coveralls.io/repos/github/huandu/go-clone/badge.svg?branch=master)](https://coveralls.io/github/huandu/go-clone?branch=master)
Package `clone` provides functions to deep clone any Go data. It also provides a wrapper to protect a pointer from any unexpected mutation.
For users who use Go 1.18+, it's recommended to import `github.com/huandu/go-clone/generic` for generic APIs and arena support.
`Clone`/`Slowly` can clone unexported fields and "no-copy" structs as well. Use this feature wisely.
## Install
Use `go get` to install this package.
```shell
go get github.com/huandu/go-clone
```
## Usage
### `Clone` and `Slowly`
If we want to clone any Go value, use `Clone`.
```go
t := &T{...}
v := clone.Clone(t).(*T)
reflect.DeepEqual(t, v) // true
```
For the sake of performance, `Clone` doesn't deal with values containing pointer cycles.
If we need to clone such values, use `Slowly` instead.
```go
type ListNode struct {
Data int
Next *ListNode
}
node1 := &ListNode{
Data: 1,
}
node2 := &ListNode{
Data: 2,
}
node3 := &ListNode{
Data: 3,
}
node1.Next = node2
node2.Next = node3
node3.Next = node1
// We must use `Slowly` to clone a circular linked list.
node := Slowly(node1).(*ListNode)
for i := 0; i < 10; i++ {
fmt.Println(node.Data)
node = node.Next
}
```
### Generic APIs
Starting from go1.18, Go started to support generic. With generic syntax, `Clone`/`Slowly` and other APIs can be called much cleaner like following.
```go
import "github.com/huandu/go-clone/generic"
type MyType struct {
Foo string
}
original := &MyType{
Foo: "bar",
}
// The type of cloned is *MyType instead of interface{}.
cloned := Clone(original)
println(cloned.Foo) // Output: bar
```
It's required to update minimal Go version to 1.18 to opt-in generic syntax. It may not be a wise choice to update this package's `go.mod` and drop so many old Go compilers for such syntax candy. Therefore, I decide to create a new standalone package `github.com/huandu/go-clone/generic` to provide APIs with generic syntax.
For new users who use Go 1.18+, the generic package is preferred and recommended.
### Arena support
Starting from Go1.20, arena is introduced as a new way to allocate memory. It's quite useful to improve overall performance in special scenarios.
In order to clone a value with memory allocated from an arena, there are new methods `ArenaClone` and `ArenaCloneSlowly` available in `github.com/huandu/go-clone/generic`.
```go
// ArenaClone recursively deep clones v to a new value in arena a.
// It works in the same way as Clone, except it allocates all memory from arena.
func ArenaClone[T any](a *arena.Arena, v T) (nv T)
// ArenaCloneSlowly recursively deep clones v to a new value in arena a.
// It works in the same way as Slowly, except it allocates all memory from arena.
func ArenaCloneSlowly[T any](a *arena.Arena, v T) (nv T)
```
Due to limitations in arena API, memory of the internal data structure of `map` and `chan` is always allocated in heap by Go runtime ([see this issue](https://github.com/golang/go/issues/56230)).
**Warning**: Per [discussion in the arena proposal](https://github.com/golang/go/issues/51317), the arena package may be changed incompatibly or removed in future. All arena related APIs in this package will be changed accordingly.
### Struct tags
There are some struct tags to control how to clone a struct field.
```go
type T struct {
Normal *int
Foo *int `clone:"skip"` // Skip cloning this field so that Foo will be zero in cloned value.
Bar *int `clone:"-"` // "-" is an alias of skip.
Baz *int `clone:"shadowcopy"` // Copy this field by shadow copy.
}
a := 1
t := &T{
Normal: &a,
Foo: &a,
Bar: &a,
Baz: &a,
}
v := clone.Clone(t).(*T)
fmt.Println(v.Normal == t.Normal) // false
fmt.Println(v.Foo == nil) // true
fmt.Println(v.Bar == nil) // true
fmt.Println(v.Baz == t.Baz) // true
```
### Memory allocations and the `Allocator`
The `Allocator` is designed to allocate memory when cloning. It's also used to hold all customizations, e.g. custom clone functions, scalar types and opaque pointers, etc. There is a default allocator which allocates memory from heap. Almost all public APIs in this package use this default allocator to do their job.
We can control how to allocate memory by creating a new `Allocator` by `NewAllocator`. It enables us to take full control over memory allocation when cloning. See [Allocator sample code](https://pkg.go.dev/github.com/huandu/go-clone#example-Allocator) to understand how to customize an allocator.
Let's take a closer look at the `NewAllocator` function.
```go
func NewAllocator(pool unsafe.Pointer, methods *AllocatorMethods) *Allocator
```
- The first parameter `pool` is a pointer to a memory pool. It's used to allocate memory for cloning. It can be `nil` if we don't need a memory pool.
- The second parameter `methods` is a pointer to a struct which contains all methods to allocate memory. It can be `nil` if we don't need to customize memory allocation.
- The `Allocator` struct is allocated from the `methods.New` or the `methods.Parent` allocator or from heap.
The `Parent` in `AllocatorMethods` is used to indicate the parent of the new allocator. With this feature, we can orgnize allocators into a tree structure. All customizations, including custom clone functions, scalar types and opaque pointers, etc, are inherited from parent allocators.
There are some APIs designed for convenience.
- We can create dedicated allocators for heap or arena by calling `FromHeap()` or `FromArena(a *arena.Arena)`.
- We can call `MakeCloner(allocator)` to create a helper struct with `Clone` and `CloneSlowly` methods in which the type of in and out parameters is `interface{}`.
### Mark struct type as scalar
Some struct types can be considered as scalar.
A well-known case is `time.Time`.
Although there is a pointer `loc *time.Location` inside `time.Time`, we always use `time.Time` by value in all methods.
When cloning `time.Time`, it should be OK to return a shadow copy.
Currently, following types are marked as scalar by default.
- `time.Time`
- `reflect.Value`
If there is any type defined in built-in package should be considered as scalar, please open new issue to let me know.
I will update the default.
If there is any custom type should be considered as scalar, call `MarkAsScalar` to mark it manually. See [MarkAsScalar sample code](https://pkg.go.dev/github.com/huandu/go-clone#example-MarkAsScalar) for more details.
### Mark pointer type as opaque
Some pointer values are used as enumerable const values.
A well-known case is `elliptic.Curve`. In package `crypto/tls`, curve type of a certificate is checked by comparing values to pre-defined curve values, e.g. `elliptic.P521()`. In this case, the curve values, which are pointers or structs, cannot be cloned deeply.
Currently, following types are marked as scalar by default.
- `elliptic.Curve`, which is `*elliptic.CurveParam` or `elliptic.p256Curve`.
- `reflect.Type`, which is `*reflect.rtype` defined in `runtime`.
If there is any pointer type defined in built-in package should be considered as opaque, please open new issue to let me know.
I will update the default.
If there is any custom pointer type should be considered as opaque, call `MarkAsOpaquePointer` to mark it manually. See [MarkAsOpaquePointer sample code](https://pkg.go.dev/github.com/huandu/go-clone#example-MarkAsOpaquePointer) for more details.
### Clone "no-copy" types defined in `sync` and `sync/atomic`
There are some "no-copy" types like `sync.Mutex`, `atomic.Value`, etc.
They cannot be cloned by copying all fields one by one, but we can alloc a new zero value and call methods to do proper initialization.
Currently, all "no-copy" types defined in `sync` and `sync/atomic` can be cloned properly using following strategies.
- `sync.Mutex`: Cloned value is a newly allocated zero mutex.
- `sync.RWMutex`: Cloned value is a newly allocated zero mutex.
- `sync.WaitGroup`: Cloned value is a newly allocated zero wait group.
- `sync.Cond`: Cloned value is a cond with a newly allocated zero lock.
- `sync.Pool`: Cloned value is an empty pool with the same `New` function.
- `sync.Map`: Cloned value is a sync map with cloned key/value pairs.
- `sync.Once`: Cloned value is a once type with the same done flag.
- `atomic.Value`/`atomic.Bool`/`atomic.Int32`/`atomic.Int64`/`atomic.Uint32`/`atomic.Uint64`/`atomic.Uintptr`: Cloned value is a new atomic value with the same value.
If there is any type defined in built-in package should be considered as "no-copy" types, please open new issue to let me know.
I will update the default.
### Set custom clone functions
If default clone strategy doesn't work for a struct type, we can call `SetCustomFunc` to register a custom clone function.
```go
SetCustomFunc(reflect.TypeOf(MyType{}), func(allocator *Allocator, old, new reflect.Value) {
// Customized logic to copy the old to the new.
// The old's type is MyType.
// The new is a zero value of MyType and new.CanAddr() always returns true.
})
```
We can use `allocator` to clone any value or allocate new memory.
It's allowed to call `allocator.Clone` or `allocator.CloneSlowly` on `old` to clone its struct fields in depth without worrying about dead loop.
See [SetCustomFunc sample code](https://pkg.go.dev/github.com/huandu/go-clone#example-SetCustomFunc) for more details.
### Clone `atomic.Pointer[T]`
As there is no way to predefine a custom clone function for generic type `atomic.Pointer[T]`, cloning such atomic type is not supported by default. If we want to support it, we need to register a custom clone function manually.
Suppose we instantiate `atomic.Pointer[T]` with type `MyType1` and `MyType2` in a project, and then we can register custom clone functions like following.
```go
import "github.com/huandu/go-clone/generic"
func init() {
// Register all instantiated atomic.Pointer[T] types in this project.
clone.RegisterAtomicPointer[MyType1]()
clone.RegisterAtomicPointer[MyType2]()
}
```
### `Wrap`, `Unwrap` and `Undo`
Package `clone` provides `Wrap`/`Unwrap` functions to protect a pointer value from any unexpected mutation.
It's useful when we want to protect a variable which should be immutable by design,
e.g. global config, the value stored in context, the value sent to a chan, etc.
```go
// Suppose we have a type T defined as following.
// type T struct {
// Foo int
// }
v := &T{
Foo: 123,
}
w := Wrap(v).(*T) // Wrap value to protect it.
// Use w freely. The type of w is the same as that of v.
// It's OK to modify w. The change will not affect v.
w.Foo = 456
fmt.Println(w.Foo) // 456
fmt.Println(v.Foo) // 123
// Once we need the original value stored in w, call `Unwrap`.
orig := Unwrap(w).(*T)
fmt.Println(orig == v) // true
fmt.Println(orig.Foo) // 123
// Or, we can simply undo any change made in w.
// Note that `Undo` is significantly slower than `Unwrap`, thus
// the latter is always preferred.
Undo(w)
fmt.Println(w.Foo) // 123
```
## Performance
Here is the performance data running on my dev machine.
```text
go 1.20.1
goos: darwin
goarch: amd64
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkSimpleClone-12 7164530 156.7 ns/op 24 B/op 1 allocs/op
BenchmarkComplexClone-12 628056 1871 ns/op 1488 B/op 21 allocs/op
BenchmarkUnwrap-12 15498139 78.02 ns/op 0 B/op 0 allocs/op
BenchmarkSimpleWrap-12 3882360 309.7 ns/op 72 B/op 2 allocs/op
BenchmarkComplexWrap-12 949654 1245 ns/op 736 B/op 15 allocs/op
```
## License
This package is licensed under MIT license. See LICENSE for details.
+335
View File
@@ -0,0 +1,335 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
import (
"reflect"
"runtime"
"sync"
"unsafe"
)
const fieldTagName = "clone"
const fieldTagValueSkip = "skip"
const fieldTagValueSkipAlias = "-"
const fieldTagValueShadowCopy = "shadowcopy"
var typeOfAllocator = reflect.TypeOf(Allocator{})
// defaultAllocator is the default allocator and allocates memory from heap.
var defaultAllocator = &Allocator{
new: heapNew,
makeSlice: heapMakeSlice,
makeMap: heapMakeMap,
makeChan: heapMakeChan,
isScalar: IsScalar,
}
// Allocator is a utility type for memory allocation.
type Allocator struct {
parent *Allocator
pool unsafe.Pointer
new func(pool unsafe.Pointer, t reflect.Type) reflect.Value
makeSlice func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value
makeMap func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value
makeChan func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value
isScalar func(t reflect.Kind) bool
cachedStructTypes sync.Map
cachedPointerTypes sync.Map
cachedCustomFuncTypes sync.Map
}
// FromHeap creates an allocator which allocate memory from heap.
func FromHeap() *Allocator {
return NewAllocator(nil, nil)
}
// NewAllocator creates an allocator which allocate memory from the pool.
// Both pool and methods are optional.
//
// If methods.New is not nil, the allocator itself is created by calling methods.New.
//
// The pool is a pointer to the memory pool which is opaque to the allocator.
// It's methods's responsibility to allocate memory from the pool properly.
func NewAllocator(pool unsafe.Pointer, methods *AllocatorMethods) (allocator *Allocator) {
parent := methods.parent()
new := methods.new(parent, pool)
// Allocate the allocator from the pool.
val := new(pool, typeOfAllocator)
allocator = (*Allocator)(unsafe.Pointer(val.Pointer()))
runtime.KeepAlive(val)
allocator.pool = pool
allocator.new = new
allocator.makeSlice = methods.makeSlice(parent, pool)
allocator.makeMap = methods.makeMap(parent, pool)
allocator.makeChan = methods.makeChan(parent, pool)
allocator.isScalar = methods.isScalar(parent)
if parent == nil {
parent = defaultAllocator
}
allocator.parent = parent
return
}
// New returns a new zero value of t.
func (a *Allocator) New(t reflect.Type) reflect.Value {
return a.new(a.pool, t)
}
// MakeSlice creates a new zero-initialized slice value of t with len and cap.
func (a *Allocator) MakeSlice(t reflect.Type, len, cap int) reflect.Value {
return a.makeSlice(a.pool, t, len, cap)
}
// MakeMap creates a new map with minimum size n.
func (a *Allocator) MakeMap(t reflect.Type, n int) reflect.Value {
return a.makeMap(a.pool, t, n)
}
// MakeChan creates a new chan with buffer.
func (a *Allocator) MakeChan(t reflect.Type, buffer int) reflect.Value {
return a.makeChan(a.pool, t, buffer)
}
// Clone recursively deep clone val to a new value with memory allocated from a.
func (a *Allocator) Clone(val reflect.Value) reflect.Value {
return a.clone(val, true)
}
func (a *Allocator) clone(val reflect.Value, inCustomFunc bool) reflect.Value {
if !val.IsValid() {
return val
}
state := &cloneState{
allocator: a,
}
if inCustomFunc {
state.skipCustomFuncValue = val
}
return state.clone(val)
}
// CloneSlowly recursively deep clone val to a new value with memory allocated from a.
// It marks all cloned values internally, thus it can clone v with cycle pointer.
func (a *Allocator) CloneSlowly(val reflect.Value) reflect.Value {
return a.cloneSlowly(val, true)
}
func (a *Allocator) cloneSlowly(val reflect.Value, inCustomFunc bool) reflect.Value {
if !val.IsValid() {
return val
}
state := &cloneState{
allocator: a,
visited: visitMap{},
invalid: invalidPointers{},
}
if inCustomFunc {
state.skipCustomFuncValue = val
}
cloned := state.clone(val)
state.fix(cloned)
return cloned
}
func (a *Allocator) loadStructType(t reflect.Type) (st structType) {
st, ok := a.lookupStructType(t)
if ok {
return
}
num := t.NumField()
zeroFeilds := make([]structFieldSize, 0, num)
pointerFields := make([]structFieldType, 0, num)
// Find pointer fields in depth-first order.
for i := 0; i < num; i++ {
field := t.Field(i)
ft := field.Type
k := ft.Kind()
tag := field.Tag.Get(fieldTagName)
if tag == fieldTagValueSkip || tag == fieldTagValueSkipAlias {
zeroFeilds = append(zeroFeilds, structFieldSize{
Offset: field.Offset,
Size: uintptr(ft.Size()),
})
continue
}
if tag == fieldTagValueShadowCopy || a.isScalar(k) {
continue
}
switch k {
case reflect.Array:
if ft.Len() == 0 {
continue
}
elem := ft.Elem()
if a.isScalar(elem.Kind()) {
continue
}
if elem.Kind() == reflect.Struct {
if fst := a.loadStructType(elem); fst.CanShadowCopy() {
continue
}
}
case reflect.Struct:
if fst := a.loadStructType(ft); fst.CanShadowCopy() {
continue
}
}
pointerFields = append(pointerFields, structFieldType{
Offset: field.Offset,
Index: i,
})
}
st = structType{}
if len(zeroFeilds) != 0 {
st.ZeroFields = append(st.ZeroFields, zeroFeilds...)
}
if len(pointerFields) != 0 {
st.PointerFields = append(st.PointerFields, pointerFields...)
}
// Load custom function.
current := a
for current != nil {
if fn, ok := current.cachedCustomFuncTypes.Load(t); ok {
st.fn = fn.(Func)
break
}
current = current.parent
}
a.cachedStructTypes.LoadOrStore(t, st)
return
}
func (a *Allocator) lookupStructType(t reflect.Type) (st structType, ok bool) {
var v interface{}
current := a
for current != nil {
v, ok = current.cachedStructTypes.Load(t)
if ok {
st = v.(structType)
return
}
current = current.parent
}
return
}
func (a *Allocator) isOpaquePointer(t reflect.Type) (ok bool) {
current := a
for current != nil {
if _, ok = current.cachedPointerTypes.Load(t); ok {
return
}
current = current.parent
}
return
}
// MarkAsScalar marks t as a scalar type so that all clone methods will copy t by value.
// If t is not struct or pointer to struct, MarkAsScalar ignores t.
//
// In the most cases, it's not necessary to call it explicitly.
// If a struct type contains scalar type fields only, the struct will be marked as scalar automatically.
//
// Here is a list of types marked as scalar by default:
// - time.Time
// - reflect.Value
func (a *Allocator) MarkAsScalar(t reflect.Type) {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
a.cachedStructTypes.Store(t, zeroStructType)
}
// MarkAsOpaquePointer marks t as an opaque pointer so that all clone methods will copy t by value.
// If t is not a pointer, MarkAsOpaquePointer ignores t.
//
// Here is a list of types marked as opaque pointers by default:
// - `elliptic.Curve`, which is `*elliptic.CurveParam` or `elliptic.p256Curve`;
// - `reflect.Type`, which is `*reflect.rtype` defined in `runtime`.
func (a *Allocator) MarkAsOpaquePointer(t reflect.Type) {
if t.Kind() != reflect.Ptr {
return
}
a.cachedPointerTypes.Store(t, struct{}{})
}
// SetCustomFunc sets a custom clone function for type t.
// If t is not struct or pointer to struct, SetCustomFunc ignores t.
//
// If fn is nil, remove the custom clone function for type t.
func (a *Allocator) SetCustomFunc(t reflect.Type, fn Func) {
if fn == nil {
a.cachedCustomFuncTypes.Delete(t)
return
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
if t.Kind() != reflect.Struct {
return
}
a.cachedCustomFuncTypes.Store(t, fn)
}
func heapNew(pool unsafe.Pointer, t reflect.Type) reflect.Value {
return reflect.New(t)
}
func heapMakeSlice(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value {
return reflect.MakeSlice(t, len, cap)
}
func heapMakeMap(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value {
return reflect.MakeMapWithSize(t, n)
}
func heapMakeChan(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value {
return reflect.MakeChan(t, buffer)
}
+115
View File
@@ -0,0 +1,115 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
import (
"reflect"
"unsafe"
)
// AllocatorMethods defines all methods required by allocator.
// If any of these methods is nil, allocator will use default method which allocates memory from heap.
type AllocatorMethods struct {
// Parent is the allocator which handles all unhandled methods.
// If it's nil, it will be the default allocator.
Parent *Allocator
New func(pool unsafe.Pointer, t reflect.Type) reflect.Value
MakeSlice func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value
MakeMap func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value
MakeChan func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value
IsScalar func(k reflect.Kind) bool
}
func (am *AllocatorMethods) parent() *Allocator {
if am != nil && am.Parent != nil {
return am.Parent
}
return nil
}
func (am *AllocatorMethods) new(parent *Allocator, pool unsafe.Pointer) func(pool unsafe.Pointer, t reflect.Type) reflect.Value {
if am != nil && am.New != nil {
return am.New
}
if parent != nil {
if parent.pool == pool {
return parent.new
} else {
return func(pool unsafe.Pointer, t reflect.Type) reflect.Value {
return parent.New(t)
}
}
}
return defaultAllocator.new
}
func (am *AllocatorMethods) makeSlice(parent *Allocator, pool unsafe.Pointer) func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value {
if am != nil && am.MakeSlice != nil {
return am.MakeSlice
}
if parent != nil {
if parent.pool == pool {
return parent.makeSlice
} else {
return func(pool unsafe.Pointer, t reflect.Type, len, cap int) reflect.Value {
return parent.MakeSlice(t, len, cap)
}
}
}
return defaultAllocator.makeSlice
}
func (am *AllocatorMethods) makeMap(parent *Allocator, pool unsafe.Pointer) func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value {
if am != nil && am.MakeMap != nil {
return am.MakeMap
}
if parent != nil {
if parent.pool == pool {
return parent.makeMap
} else {
return func(pool unsafe.Pointer, t reflect.Type, n int) reflect.Value {
return parent.MakeMap(t, n)
}
}
}
return defaultAllocator.makeMap
}
func (am *AllocatorMethods) makeChan(parent *Allocator, pool unsafe.Pointer) func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value {
if am != nil && am.MakeChan != nil {
return am.MakeChan
}
if parent != nil {
if parent.pool == pool {
return parent.makeChan
} else {
return func(pool unsafe.Pointer, t reflect.Type, buffer int) reflect.Value {
return parent.MakeChan(t, buffer)
}
}
}
return defaultAllocator.makeChan
}
func (am *AllocatorMethods) isScalar(parent *Allocator) func(t reflect.Kind) bool {
if am != nil && am.IsScalar != nil {
return am.IsScalar
}
if parent != nil {
return parent.isScalar
}
return defaultAllocator.isScalar
}
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
//go:build !(go1.20 && goexperiment.arenas)
// +build !go1.20 !goexperiment.arenas
package clone
const arenaIsEnabled = false
+9
View File
@@ -0,0 +1,9 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
//go:build go1.20 && goexperiment.arenas
// +build go1.20,goexperiment.arenas
package clone
const arenaIsEnabled = true
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
//go:build go1.19
// +build go1.19
package clone
import (
"reflect"
"sync/atomic"
)
func init() {
SetCustomFunc(reflect.TypeOf(atomic.Bool{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Bool.
oldValue := old.Addr().Interface().(*atomic.Bool)
newValue := new.Addr().Interface().(*atomic.Bool)
v := oldValue.Load()
newValue.Store(v)
})
SetCustomFunc(reflect.TypeOf(atomic.Int32{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Int32.
oldValue := old.Addr().Interface().(*atomic.Int32)
newValue := new.Addr().Interface().(*atomic.Int32)
v := oldValue.Load()
newValue.Store(v)
})
SetCustomFunc(reflect.TypeOf(atomic.Int64{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Int64.
oldValue := old.Addr().Interface().(*atomic.Int64)
newValue := new.Addr().Interface().(*atomic.Int64)
v := oldValue.Load()
newValue.Store(v)
})
SetCustomFunc(reflect.TypeOf(atomic.Uint32{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Uint32.
oldValue := old.Addr().Interface().(*atomic.Uint32)
newValue := new.Addr().Interface().(*atomic.Uint32)
v := oldValue.Load()
newValue.Store(v)
})
SetCustomFunc(reflect.TypeOf(atomic.Uint64{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Uint64.
oldValue := old.Addr().Interface().(*atomic.Uint64)
newValue := new.Addr().Interface().(*atomic.Uint64)
v := oldValue.Load()
newValue.Store(v)
})
SetCustomFunc(reflect.TypeOf(atomic.Uintptr{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Uintptr.
oldValue := old.Addr().Interface().(*atomic.Uintptr)
newValue := new.Addr().Interface().(*atomic.Uintptr)
v := oldValue.Load()
newValue.Store(v)
})
}
+891
View File
@@ -0,0 +1,891 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
// Package clone provides functions to deep clone any Go data.
// It also provides a wrapper to protect a pointer from any unexpected mutation.
package clone
import (
"fmt"
"reflect"
"unsafe"
)
var heapCloneState = &cloneState{
allocator: defaultAllocator,
}
var cloner = MakeCloner(defaultAllocator)
const zeroBytesCount = 256
var zeroBytes [zeroBytesCount]byte
var zero = zeroBytes[:]
// Clone recursively deep clone v to a new value in heap.
// It assumes that there is no pointer cycle in v,
// e.g. v has a pointer points to v itself.
// If there is a pointer cycle, use Slowly instead.
//
// Clone allocates memory and deeply copies values inside v in depth-first sequence.
// There are a few special rules for following types.
//
// - Scalar types: all number-like types are copied by value.
// - func: Copied by value as func is an opaque pointer at runtime.
// - string: Copied by value as string is immutable by design.
// - unsafe.Pointer: Copied by value as we don't know what's in it.
// - chan: A new empty chan is created as we cannot read data inside the old chan.
//
// Unlike many other packages, Clone is able to clone unexported fields of any struct.
// Use this feature wisely.
func Clone(v interface{}) interface{} {
return cloner.Clone(v)
}
func clone(allocator *Allocator, v interface{}) interface{} {
if v == nil {
return nil
}
val := reflect.ValueOf(v)
cloned := allocator.clone(val, false)
return cloned.Interface()
}
// Slowly recursively deep clone v to a new value in heap.
// It marks all cloned values internally, thus it can clone v with cycle pointer.
//
// Slowly works exactly the same as Clone. See Clone doc for more details.
func Slowly(v interface{}) interface{} {
return cloner.CloneSlowly(v)
}
func cloneSlowly(allocator *Allocator, v interface{}) interface{} {
if v == nil {
return nil
}
val := reflect.ValueOf(v)
cloned := allocator.cloneSlowly(val, false)
return cloned.Interface()
}
type cloneState struct {
allocator *Allocator
visited visitMap
invalid invalidPointers
// The value that should not be cloned by custom func.
// It's useful to avoid infinite loop when custom func calls allocator.Clone().
skipCustomFuncValue reflect.Value
}
type visit struct {
p uintptr
extra int
t reflect.Type
}
type visitMap map[visit]reflect.Value
type invalidPointers map[visit]reflect.Value
func (state *cloneState) clone(v reflect.Value) reflect.Value {
if state.allocator.isScalar(v.Kind()) {
return copyScalarValue(v)
}
switch v.Kind() {
case reflect.Array:
return state.cloneArray(v)
case reflect.Chan:
return state.allocator.MakeChan(v.Type(), v.Cap())
case reflect.Interface:
return state.cloneInterface(v)
case reflect.Map:
return state.cloneMap(v)
case reflect.Ptr:
return state.clonePtr(v)
case reflect.Slice:
return state.cloneSlice(v)
case reflect.Struct:
return state.cloneStruct(v)
case reflect.String:
return state.cloneString(v)
default:
panic(fmt.Errorf("go-clone: <bug> unsupported type `%v`", v.Type()))
}
}
func (state *cloneState) cloneArray(v reflect.Value) reflect.Value {
dst := state.allocator.New(v.Type())
state.copyArray(v, dst)
return dst.Elem()
}
func (state *cloneState) copyArray(src, nv reflect.Value) {
p := unsafe.Pointer(nv.Pointer()) // dst must be a Ptr.
dst := nv.Elem()
num := src.Len()
if state.allocator.isScalar(src.Type().Elem().Kind()) {
shadowCopy(src, p)
return
}
for i := 0; i < num; i++ {
dst.Index(i).Set(state.clone(src.Index(i)))
}
}
func (state *cloneState) cloneInterface(v reflect.Value) reflect.Value {
if v.IsNil() {
return reflect.Zero(v.Type())
}
t := v.Type()
elem := v.Elem()
return state.clone(elem).Convert(elem.Type()).Convert(t)
}
func (state *cloneState) cloneMap(v reflect.Value) reflect.Value {
if v.IsNil() {
return reflect.Zero(v.Type())
}
t := v.Type()
if state.visited != nil {
vst := visit{
p: v.Pointer(),
t: t,
}
if val, ok := state.visited[vst]; ok {
return val
}
}
nv := state.allocator.MakeMap(t, v.Len())
if state.visited != nil {
vst := visit{
p: v.Pointer(),
t: t,
}
state.visited[vst] = nv
}
for iter := mapIter(v); iter.Next(); {
key := state.clone(iter.Key())
value := state.clone(iter.Value())
nv.SetMapIndex(key, value)
}
return nv
}
func (state *cloneState) clonePtr(v reflect.Value) reflect.Value {
if v.IsNil() {
return reflect.Zero(v.Type())
}
t := v.Type()
if state.allocator.isOpaquePointer(t) {
if v.CanInterface() {
return v
}
ptr := state.allocator.New(t)
p := unsafe.Pointer(ptr.Pointer())
shadowCopy(v, p)
return ptr.Elem()
}
if state.visited != nil {
vst := visit{
p: v.Pointer(),
t: t,
}
if val, ok := state.visited[vst]; ok {
return val
}
}
src := v.Elem()
elemType := src.Type()
elemKind := src.Kind()
nv := state.allocator.New(elemType)
if state.visited != nil {
vst := visit{
p: v.Pointer(),
t: t,
}
state.visited[vst] = nv
}
switch elemKind {
case reflect.Struct:
state.copyStruct(src, nv)
case reflect.Array:
state.copyArray(src, nv)
default:
nv.Elem().Set(state.clone(src))
}
// If this pointer is the address of a struct field and it's a cycle pointer,
// it may be updated.
if state.visited != nil {
vst := visit{
p: v.Pointer(),
t: t,
}
nv = state.visited[vst]
}
return nv
}
func (state *cloneState) cloneSlice(v reflect.Value) reflect.Value {
if v.IsNil() {
return reflect.Zero(v.Type())
}
t := v.Type()
num := v.Len()
if state.visited != nil {
vst := visit{
p: v.Pointer(),
extra: num,
t: t,
}
if val, ok := state.visited[vst]; ok {
return val
}
}
c := v.Cap()
nv := state.allocator.MakeSlice(t, num, c)
if state.visited != nil {
vst := visit{
p: v.Pointer(),
extra: num,
t: t,
}
state.visited[vst] = nv
}
// For scalar slice, copy underlying values directly.
if state.allocator.isScalar(t.Elem().Kind()) {
src := unsafe.Pointer(v.Pointer())
dst := unsafe.Pointer(nv.Pointer())
sz := int(t.Elem().Size())
l := num * sz
cc := c * sz
copy((*[maxByteSize]byte)(dst)[:l:cc], (*[maxByteSize]byte)(src)[:l:cc])
} else {
for i := 0; i < num; i++ {
nv.Index(i).Set(state.clone(v.Index(i)))
}
}
return nv
}
func (state *cloneState) cloneStruct(v reflect.Value) reflect.Value {
t := v.Type()
nv := state.allocator.New(t)
state.copyStruct(v, nv)
return nv.Elem()
}
var typeOfByteSlice = reflect.TypeOf([]byte(nil))
func (state *cloneState) cloneString(v reflect.Value) reflect.Value {
t := v.Type()
l := v.Len()
data := state.allocator.MakeSlice(typeOfByteSlice, l, l)
// The v is an unexported struct field.
if !v.CanInterface() {
v = reflect.ValueOf(v.String())
}
reflect.Copy(data, v)
nv := state.allocator.New(t)
slice := data.Interface().([]byte)
*(*stringHeader)(unsafe.Pointer(nv.Pointer())) = *(*stringHeader)(unsafe.Pointer(&slice))
return nv.Elem()
}
func (state *cloneState) copyStruct(src, nv reflect.Value) {
t := src.Type()
st := state.allocator.loadStructType(t)
ptr := unsafe.Pointer(nv.Pointer())
if st.Init(state.allocator, src, nv, state.skipCustomFuncValue == src) {
return
}
for _, pf := range st.ZeroFields {
p := unsafe.Pointer(uintptr(ptr) + pf.Offset)
sz := pf.Size
for sz > zeroBytesCount {
copy((*[zeroBytesCount]byte)(p)[:zeroBytesCount:zeroBytesCount], zero)
sz -= zeroBytesCount
p = unsafe.Pointer(uintptr(p) + zeroBytesCount)
}
copy((*[zeroBytesCount]byte)(p)[:sz:sz], zero)
}
for _, pf := range st.PointerFields {
i := int(pf.Index)
p := unsafe.Pointer(uintptr(ptr) + pf.Offset)
field := src.Field(i)
// This field can be referenced by a pointer or interface inside itself.
// Put the pointer to this field to visited to avoid any error.
//
// See https://github.com/huandu/go-clone/issues/3.
if state.visited != nil && field.CanAddr() {
ft := field.Type()
fp := field.Addr().Pointer()
vst := visit{
p: fp,
t: reflect.PtrTo(ft),
}
nv := reflect.NewAt(ft, p)
// The address of this field was visited, so fp must be a cycle pointer.
// As this field is not fully cloned, the val stored in visited[visit] must be wrong.
// It must be replaced by nv which will be the right value (it's incomplete right now).
//
// Unfortunately, if the val was used by previous clone routines,
// there is no easy way to fix wrong values - all pointers must be traversed and fixed.
if val, ok := state.visited[vst]; ok {
state.invalid[visit{
p: val.Pointer(),
t: vst.t,
}] = nv
}
state.visited[vst] = nv
}
v := state.clone(field)
shadowCopy(v, p)
}
}
var typeOfString = reflect.TypeOf("")
func shadowCopy(src reflect.Value, p unsafe.Pointer) {
switch src.Kind() {
case reflect.Bool:
*(*bool)(p) = src.Bool()
case reflect.Int:
*(*int)(p) = int(src.Int())
case reflect.Int8:
*(*int8)(p) = int8(src.Int())
case reflect.Int16:
*(*int16)(p) = int16(src.Int())
case reflect.Int32:
*(*int32)(p) = int32(src.Int())
case reflect.Int64:
*(*int64)(p) = src.Int()
case reflect.Uint:
*(*uint)(p) = uint(src.Uint())
case reflect.Uint8:
*(*uint8)(p) = uint8(src.Uint())
case reflect.Uint16:
*(*uint16)(p) = uint16(src.Uint())
case reflect.Uint32:
*(*uint32)(p) = uint32(src.Uint())
case reflect.Uint64:
*(*uint64)(p) = src.Uint()
case reflect.Uintptr:
*(*uintptr)(p) = uintptr(src.Uint())
case reflect.Float32:
*(*float32)(p) = float32(src.Float())
case reflect.Float64:
*(*float64)(p) = src.Float()
case reflect.Complex64:
*(*complex64)(p) = complex64(src.Complex())
case reflect.Complex128:
*(*complex128)(p) = src.Complex()
case reflect.Array:
t := src.Type()
if src.CanAddr() {
srcPtr := unsafe.Pointer(src.UnsafeAddr())
sz := t.Size()
copy((*[maxByteSize]byte)(p)[:sz:sz], (*[maxByteSize]byte)(srcPtr)[:sz:sz])
return
}
val := reflect.NewAt(t, p).Elem()
if src.CanInterface() {
val.Set(src)
return
}
sz := t.Elem().Size()
num := src.Len()
for i := 0; i < num; i++ {
elemPtr := unsafe.Pointer(uintptr(p) + uintptr(i)*sz)
shadowCopy(src.Index(i), elemPtr)
}
case reflect.Chan:
*((*uintptr)(p)) = src.Pointer()
case reflect.Func:
t := src.Type()
src = copyScalarValue(src)
val := reflect.NewAt(t, p).Elem()
val.Set(src)
case reflect.Interface:
*((*interfaceData)(p)) = parseReflectValue(src)
case reflect.Map:
*((*uintptr)(p)) = src.Pointer()
case reflect.Ptr:
*((*uintptr)(p)) = src.Pointer()
case reflect.Slice:
*(*sliceHeader)(p) = sliceHeader{
Data: src.Pointer(),
Len: src.Len(),
Cap: src.Cap(),
}
case reflect.String:
s := src.String()
val := reflect.NewAt(typeOfString, p).Elem()
val.SetString(s)
case reflect.Struct:
t := src.Type()
val := reflect.NewAt(t, p).Elem()
if src.CanInterface() {
val.Set(src)
return
}
num := t.NumField()
for i := 0; i < num; i++ {
field := t.Field(i)
fieldPtr := unsafe.Pointer(uintptr(p) + field.Offset)
shadowCopy(src.Field(i), fieldPtr)
}
case reflect.UnsafePointer:
// There is no way to copy unsafe.Pointer value.
*((*uintptr)(p)) = src.Pointer()
default:
panic(fmt.Errorf("go-clone: <bug> impossible type `%v` when cloning private field", src.Type()))
}
}
// fix tranverses v to update all pointer values in state.invalid.
func (state *cloneState) fix(v reflect.Value) {
if state == nil || len(state.invalid) == 0 {
return
}
fix := &fixState{
allocator: state.allocator,
fixed: fixMap{},
invalid: state.invalid,
}
fix.fix(v)
}
type fixState struct {
allocator *Allocator
fixed fixMap
invalid invalidPointers
}
type fixMap map[visit]struct{}
func (fix *fixState) new(t reflect.Type) reflect.Value {
return fix.allocator.New(t)
}
func (fix *fixState) fix(v reflect.Value) (copied reflect.Value, changed int) {
if fix.allocator.isScalar(v.Kind()) {
return
}
switch v.Kind() {
case reflect.Array:
return fix.fixArray(v)
case reflect.Chan:
// Do nothing.
return
case reflect.Interface:
return fix.fixInterface(v)
case reflect.Map:
return fix.fixMap(v)
case reflect.Ptr:
return fix.fixPtr(v)
case reflect.Slice:
return fix.fixSlice(v)
case reflect.Struct:
return fix.fixStruct(v)
case reflect.String:
// Do nothing.
return
default:
panic(fmt.Errorf("go-clone: <bug> unsupported type `%v`", v.Type()))
}
}
func (fix *fixState) fixArray(v reflect.Value) (copied reflect.Value, changed int) {
t := v.Type()
et := t.Elem()
kind := et.Kind()
if fix.allocator.isScalar(kind) {
return
}
l := v.Len()
for i := 0; i < l; i++ {
elem := v.Index(i)
if kind == reflect.Ptr {
vst := visit{
p: elem.Pointer(),
t: et,
}
if nv, ok := fix.invalid[vst]; ok {
// If elem cannot be set, v must be copied to make it settable.
// Don't do it unless there is no other choices.
if !elem.CanSet() {
copied = fix.new(t).Elem()
shadowCopy(v, unsafe.Pointer(copied.Addr().Pointer()))
_, changed = fix.fixArray(copied)
return
}
elem.Set(nv)
changed++
continue
}
}
fixed, c := fix.fix(elem)
changed += c
if fixed.IsValid() {
// If elem cannot be set, v must be copied to make it settable.
// Don't do it unless there is no other choices.
if !elem.CanSet() {
copied = fix.new(t).Elem()
shadowCopy(v, unsafe.Pointer(copied.Addr().Pointer()))
_, changed = fix.fixArray(copied)
return
}
elem.Set(fixed)
}
}
return
}
func (fix *fixState) fixInterface(v reflect.Value) (copied reflect.Value, changed int) {
if v.IsNil() {
return
}
elem := v.Elem()
t := elem.Type()
kind := elem.Kind()
if kind == reflect.Ptr {
vst := visit{
p: elem.Pointer(),
t: t,
}
if nv, ok := fix.invalid[vst]; ok {
copied = nv.Convert(v.Type())
changed++
return
}
}
copied, changed = fix.fix(elem)
if copied.IsValid() {
copied = copied.Convert(v.Type())
}
return
}
func (fix *fixState) fixMap(v reflect.Value) (copied reflect.Value, changed int) {
if v.IsNil() {
return
}
t := v.Type()
vst := visit{
p: v.Pointer(),
t: t,
}
if _, ok := fix.fixed[vst]; ok {
return
}
fix.fixed[vst] = struct{}{}
kt := t.Key()
et := t.Elem()
keyKind := kt.Kind()
elemKind := et.Kind()
if isScalar := fix.allocator.isScalar; isScalar(keyKind) && isScalar(elemKind) {
return
}
invalidKeys := map[reflect.Value][2]reflect.Value{}
for iter := mapIter(v); iter.Next(); {
key := iter.Key()
elem := iter.Value()
var fixed reflect.Value
c := 0
if elemKind == reflect.Ptr {
vst := visit{
p: elem.Pointer(),
t: et,
}
if nv, ok := fix.invalid[vst]; ok {
fixed = nv
c++
} else {
fixed, c = fix.fixPtr(elem)
}
} else {
fixed, c = fix.fix(elem)
}
changed += c
c = 0
if fixed.IsValid() {
v = forceSetMapIndex(v, key, fixed)
elem = fixed
fixed = reflect.Value{}
}
if keyKind == reflect.Ptr {
vst := visit{
p: key.Pointer(),
t: kt,
}
if nv, ok := fix.invalid[vst]; ok {
fixed = nv
c++
} else {
fixed, c = fix.fixPtr(key)
}
} else {
fixed, c = fix.fix(key)
}
changed += c
// Key cannot be changed immediately inside map range iteration.
// Do it later.
if fixed.IsValid() {
invalidKeys[key] = [2]reflect.Value{fixed, elem}
}
}
for key, kv := range invalidKeys {
v = forceSetMapIndex(v, key, reflect.Value{})
v = forceSetMapIndex(v, kv[0], kv[1])
}
return
}
func forceSetMapIndex(v, key, elem reflect.Value) (nv reflect.Value) {
nv = v
if !v.CanInterface() {
nv = forceClearROFlag(v)
}
if !key.CanInterface() {
key = forceClearROFlag(key)
}
if elem.IsValid() && !elem.CanInterface() {
elem = forceClearROFlag(elem)
}
nv.SetMapIndex(key, elem)
return
}
func (fix *fixState) fixPtr(v reflect.Value) (copied reflect.Value, changed int) {
if v.IsNil() {
return
}
vst := visit{
p: v.Pointer(),
t: v.Type(),
}
if _, ok := fix.invalid[vst]; ok {
panic(fmt.Errorf("go-clone: <bug> invalid pointers must have been fixed in other methods"))
}
if _, ok := fix.fixed[vst]; ok {
return
}
fix.fixed[vst] = struct{}{}
elem := v.Elem()
_, changed = fix.fix(elem)
return
}
func (fix *fixState) fixSlice(v reflect.Value) (copied reflect.Value, changed int) {
if v.IsNil() {
return
}
t := v.Type()
et := t.Elem()
kind := et.Kind()
if fix.allocator.isScalar(kind) {
return
}
l := v.Len()
p := unsafe.Pointer(v.Pointer())
vst := visit{
p: uintptr(p),
extra: l,
t: t,
}
if _, ok := fix.fixed[vst]; ok {
return
}
fix.fixed[vst] = struct{}{}
for i := 0; i < l; i++ {
elem := v.Index(i)
var fixed reflect.Value
c := 0
if kind == reflect.Ptr {
vst := visit{
p: elem.Pointer(),
t: et,
}
if nv, ok := fix.invalid[vst]; ok {
fixed = nv
} else {
fixed, c = fix.fixPtr(elem)
}
} else {
fixed, c = fix.fix(elem)
}
changed += c
if fixed.IsValid() {
sz := et.Size()
elemPtr := unsafe.Pointer(uintptr(p) + sz*uintptr(i))
shadowCopy(fixed, elemPtr)
}
}
return
}
func (fix *fixState) fixStruct(v reflect.Value) (copied reflect.Value, changed int) {
t := v.Type()
st := fix.allocator.loadStructType(t)
if len(st.PointerFields) == 0 {
return
}
for _, pf := range st.PointerFields {
i := int(pf.Index)
field := v.Field(i)
ft := field.Type()
if ft.Kind() == reflect.Ptr {
vst := visit{
p: field.Pointer(),
t: ft,
}
if nv, ok := fix.invalid[vst]; ok {
// If v is not addressable, a new struct must be allocated.
// Don't do it unless there is no other choices.
if !v.CanAddr() {
copied = fix.new(t).Elem()
shadowCopy(v, unsafe.Pointer(copied.Addr().Pointer()))
_, changed = fix.fixStruct(copied)
return
}
ptr := unsafe.Pointer(v.Addr().Pointer())
p := unsafe.Pointer(uintptr(ptr) + pf.Offset)
shadowCopy(nv, p)
continue
}
}
fixed, c := fix.fix(field)
changed += c
if fixed.IsValid() {
// If v is not addressable, a new struct must be allocated.
// Don't do it unless there is no other choices.
if !v.CanAddr() {
copied = fix.new(t).Elem()
shadowCopy(v, unsafe.Pointer(copied.Addr().Pointer()))
_, changed = fix.fixStruct(copied)
return
}
ptr := unsafe.Pointer(v.Addr().Pointer())
p := unsafe.Pointer(uintptr(ptr) + pf.Offset)
shadowCopy(fixed, p)
}
}
return
}
+27
View File
@@ -0,0 +1,27 @@
// Copyright 2023 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
// Cloner implements clone API with given allocator.
type Cloner struct {
allocator *Allocator
}
// MakeCloner creates a cloner with given allocator.
func MakeCloner(allocator *Allocator) Cloner {
return Cloner{
allocator: allocator,
}
}
// Clone clones v with given allocator.
func (c Cloner) Clone(v interface{}) interface{} {
return clone(c.allocator, v)
}
// CloneSlowly clones v with given allocator.
// It can clone v with cycle pointer.
func (c Cloner) CloneSlowly(v interface{}) interface{} {
return cloneSlowly(c.allocator, v)
}
+12
View File
@@ -0,0 +1,12 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
import "reflect"
// As golint reports warning on possible misuse of these headers,
// avoid to use these header types directly to silience golint.
type sliceHeader reflect.SliceHeader
type stringHeader reflect.StringHeader
+50
View File
@@ -0,0 +1,50 @@
package clone
import (
"reflect"
"unsafe"
)
const sizeOfPointers = unsafe.Sizeof((interface{})(0)) / unsafe.Sizeof(uintptr(0))
// interfaceData is the underlying data of an interface.
// As the reflect.Value's interfaceData method is deprecated,
// it may be broken in any Go release.
// It's better to create a custom to hold the data.
//
// The type of interfaceData fields must be poniters.
// It's a way to cheat Go compile to generate calls to write barrier
// when copying interfaces.
type interfaceData struct {
_ [sizeOfPointers]unsafe.Pointer
}
var reflectValuePtrOffset uintptr
func init() {
t := reflect.TypeOf(reflect.Value{})
found := false
fields := t.NumField()
for i := 0; i < fields; i++ {
field := t.Field(i)
if field.Type.Kind() == reflect.UnsafePointer {
found = true
reflectValuePtrOffset = field.Offset
break
}
}
if !found {
panic("go-clone: fail to find internal ptr field in reflect.Value")
}
}
// parseReflectValue returns the underlying interface data in a reflect value.
// It assumes that v is an interface value.
func parseReflectValue(v reflect.Value) interfaceData {
pv := (unsafe.Pointer)(uintptr(unsafe.Pointer(&v)) + reflectValuePtrOffset)
ptr := *(*unsafe.Pointer)(pv)
return *(*interfaceData)(ptr)
}
+41
View File
@@ -0,0 +1,41 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
// +build !go1.12
package clone
import (
"reflect"
)
type iter struct {
m reflect.Value
k reflect.Value
keys []reflect.Value
}
func mapIter(m reflect.Value) *iter {
return &iter{
m: m,
keys: m.MapKeys(),
}
}
func (it *iter) Next() bool {
if len(it.keys) == 0 {
return false
}
it.k = it.keys[0]
it.keys = it.keys[1:]
return true
}
func (it *iter) Key() reflect.Value {
return it.k
}
func (it *iter) Value() reflect.Value {
return it.m.MapIndex(it.k)
}
+14
View File
@@ -0,0 +1,14 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
// +build go1.12
package clone
import (
"reflect"
)
func mapIter(m reflect.Value) *reflect.MapIter {
return m.MapRange()
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
// maxByteSize is a large enough value to cheat Go compiler
// when converting unsafe address to []byte.
// It's not actually used in runtime.
//
// The value 2^30 is the max value AFAIK to make Go compiler happy on all archs.
const maxByteSize = 1 << 30
+311
View File
@@ -0,0 +1,311 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
import (
"crypto/elliptic"
"fmt"
"reflect"
"sync"
"sync/atomic"
"time"
"unsafe"
)
type structType struct {
ZeroFields []structFieldSize
PointerFields []structFieldType
fn Func
}
type structFieldSize struct {
Offset uintptr // The offset from the beginning of the struct.
Size uintptr // The size of the field.
}
type structFieldType struct {
Offset uintptr // The offset from the beginning of the struct.
Index int // The index of the field.
}
var zeroStructType = structType{}
func init() {
// Some well-known scalar-like structs.
MarkAsScalar(reflect.TypeOf(time.Time{}))
MarkAsScalar(reflect.TypeOf(reflect.Value{}))
// Special case for elliptic.Curve which is used by TLS ECC certificate.
// Package crypto/tls uses elliptic.Curve as enum values
// so that they should be treated as opaque pointers.
//
// As elliptic.Curve is an interface, it can be *elliptic.CurveParam or elliptic.p256Curve.
MarkAsOpaquePointer(reflect.TypeOf(&elliptic.CurveParams{}))
curves := []elliptic.Curve{
elliptic.P224(),
elliptic.P256(),
elliptic.P384(),
elliptic.P521(),
}
for _, curve := range curves {
MarkAsOpaquePointer(reflect.ValueOf(curve).Type())
}
// Special case for reflect.Type (actually *reflect.rtype):
// The *reflect.rtype should not be copied as it is immutable and
// may point to a variable that actual type is not reflect.rtype,
// e.g. *reflect.arrayType or *reflect.chanType.
MarkAsOpaquePointer(reflect.TypeOf(reflect.TypeOf(0)))
// Some well-known no-copy structs.
//
// Almost all structs defined in package "sync" and "sync/atomic" are set
// except `sync.Once` which can be safely cloned with a correct done value.
SetCustomFunc(reflect.TypeOf(sync.Mutex{}), emptyCloneFunc)
SetCustomFunc(reflect.TypeOf(sync.RWMutex{}), emptyCloneFunc)
SetCustomFunc(reflect.TypeOf(sync.WaitGroup{}), emptyCloneFunc)
SetCustomFunc(reflect.TypeOf(sync.Cond{}), func(allocator *Allocator, old, new reflect.Value) {
// Copy the New func from old value.
oldL := old.FieldByName("L")
newL := allocator.Clone(oldL)
new.FieldByName("L").Set(newL)
})
SetCustomFunc(reflect.TypeOf(sync.Pool{}), func(allocator *Allocator, old, new reflect.Value) {
// Copy the New func from old value.
oldFn := old.FieldByName("New")
newFn := allocator.Clone(oldFn)
new.FieldByName("New").Set(newFn)
})
SetCustomFunc(reflect.TypeOf(sync.Map{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone all values inside sync.Map.
oldMap := old.Addr().Interface().(*sync.Map)
newMap := new.Addr().Interface().(*sync.Map)
oldMap.Range(func(key, value interface{}) bool {
k := clone(allocator, key)
v := clone(allocator, value)
newMap.Store(k, v)
return true
})
})
SetCustomFunc(reflect.TypeOf(atomic.Value{}), func(allocator *Allocator, old, new reflect.Value) {
if !old.CanAddr() {
return
}
// Clone value inside atomic.Value.
oldValue := old.Addr().Interface().(*atomic.Value)
newValue := new.Addr().Interface().(*atomic.Value)
if v := oldValue.Load(); v != nil {
cloned := clone(allocator, v)
newValue.Store(cloned)
}
})
}
// MarkAsScalar marks t as a scalar type in heap allocator,
// so that all clone methods will copy t by value.
// If t is not struct or pointer to struct, MarkAsScalar ignores t.
//
// In the most cases, it's not necessary to call it explicitly.
// If a struct type contains scalar type fields only, the struct will be marked as scalar automatically.
//
// Here is a list of types marked as scalar by default:
// - time.Time
// - reflect.Value
func MarkAsScalar(t reflect.Type) {
defaultAllocator.MarkAsScalar(t)
}
// MarkAsOpaquePointer marks t as an opaque pointer in heap allocator,
// so that all clone methods will copy t by value.
// If t is not a pointer, MarkAsOpaquePointer ignores t.
//
// Here is a list of types marked as opaque pointers by default:
// - `elliptic.Curve`, which is `*elliptic.CurveParam` or `elliptic.p256Curve`;
// - `reflect.Type`, which is `*reflect.rtype` defined in `runtime`.
func MarkAsOpaquePointer(t reflect.Type) {
defaultAllocator.MarkAsOpaquePointer(t)
}
// Func is a custom func to clone value from old to new.
// The new is a zero value
// which `new.CanSet()` and `new.CanAddr()` is guaranteed to be true.
//
// Func must update the new to return result.
type Func func(allocator *Allocator, old, new reflect.Value)
// emptyCloneFunc is used to disable shadow copy.
// It's useful when cloning sync.Mutex as cloned value must be a zero value.
func emptyCloneFunc(allocator *Allocator, old, new reflect.Value) {}
// SetCustomFunc sets a custom clone function for type t in heap allocator.
// If t is not struct or pointer to struct, SetCustomFunc ignores t.
//
// If fn is nil, remove the custom clone function for type t.
func SetCustomFunc(t reflect.Type, fn Func) {
defaultAllocator.SetCustomFunc(t, fn)
}
// Init creates a new value of src.Type() and shadow copies all content from src.
// If noCustomFunc is set to true, custom clone function will be ignored.
//
// Init returns true if the value is cloned by a custom func.
// Caller should skip cloning struct fields in depth.
func (st *structType) Init(allocator *Allocator, src, nv reflect.Value, noCustomFunc bool) (done bool) {
dst := nv.Elem()
if !noCustomFunc && st.fn != nil {
if !src.CanInterface() {
src = forceClearROFlag(src)
}
st.fn(allocator, src, dst)
done = true
return
}
ptr := unsafe.Pointer(nv.Pointer())
shadowCopy(src, ptr)
done = len(st.PointerFields) == 0
return
}
func (st *structType) CanShadowCopy() bool {
return len(st.PointerFields) == 0 && st.fn == nil
}
// IsScalar returns true if k should be considered as a scalar type.
//
// For the sake of performance, string is considered as a scalar type unless arena is enabled.
// If we need to deep copy string value in some cases, we can create a new allocator with custom isScalar function
// in which we can return false when k is reflect.String.
//
// // Create a new allocator which treats string as non-scalar type.
// allocator := NewAllocator(nil, &AllocatorMethods{
// IsScalar: func(k reflect.Kind) bool {
// return k != reflect.String && IsScalar(k)
// },
// })
func IsScalar(k reflect.Kind) bool {
switch k {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
reflect.Float32, reflect.Float64,
reflect.Complex64, reflect.Complex128,
reflect.Func,
reflect.UnsafePointer,
reflect.Invalid:
return true
case reflect.String:
// If arena is not enabled, string can be copied as scalar safely
// as it's immutable by design.
return !arenaIsEnabled
}
return false
}
func copyScalarValue(src reflect.Value) reflect.Value {
if src.CanInterface() {
return src
}
dst := newScalarValue(src)
return dst.Convert(src.Type())
}
func newScalarValue(src reflect.Value) reflect.Value {
// src is an unexported field value. Copy its value.
switch src.Kind() {
case reflect.Bool:
return reflect.ValueOf(src.Bool())
case reflect.Int:
return reflect.ValueOf(int(src.Int()))
case reflect.Int8:
return reflect.ValueOf(int8(src.Int()))
case reflect.Int16:
return reflect.ValueOf(int16(src.Int()))
case reflect.Int32:
return reflect.ValueOf(int32(src.Int()))
case reflect.Int64:
return reflect.ValueOf(src.Int())
case reflect.Uint:
return reflect.ValueOf(uint(src.Uint()))
case reflect.Uint8:
return reflect.ValueOf(uint8(src.Uint()))
case reflect.Uint16:
return reflect.ValueOf(uint16(src.Uint()))
case reflect.Uint32:
return reflect.ValueOf(uint32(src.Uint()))
case reflect.Uint64:
return reflect.ValueOf(src.Uint())
case reflect.Uintptr:
return reflect.ValueOf(uintptr(src.Uint()))
case reflect.Float32:
return reflect.ValueOf(float32(src.Float()))
case reflect.Float64:
return reflect.ValueOf(src.Float())
case reflect.Complex64:
return reflect.ValueOf(complex64(src.Complex()))
case reflect.Complex128:
return reflect.ValueOf(src.Complex())
case reflect.String:
return reflect.ValueOf(src.String())
case reflect.Func:
t := src.Type()
if src.IsNil() {
return reflect.Zero(t)
}
// Don't use this trick unless we have no choice.
return forceClearROFlag(src)
case reflect.UnsafePointer:
return reflect.ValueOf(unsafe.Pointer(src.Pointer()))
}
panic(fmt.Errorf("go-clone: <bug> impossible type `%v` when cloning private field", src.Type()))
}
var typeOfInterface = reflect.TypeOf((*interface{})(nil)).Elem()
// forceClearROFlag clears all RO flags in v to make v accessible.
// It's a hack based on the fact that InterfaceData is always available on RO data.
// This hack can be broken in any Go version.
// Don't use it unless we have no choice, e.g. copying func in some edge cases.
func forceClearROFlag(v reflect.Value) reflect.Value {
var i interface{}
indirect := 0
// Save flagAddr.
for v.CanAddr() {
v = v.Addr()
indirect++
}
v = v.Convert(typeOfInterface)
nv := reflect.ValueOf(&i)
*(*interfaceData)(unsafe.Pointer(nv.Pointer())) = parseReflectValue(v)
cleared := nv.Elem().Elem()
for indirect > 0 {
cleared = cleared.Elem()
indirect--
}
return cleared
}
+166
View File
@@ -0,0 +1,166 @@
// Copyright 2019 Huan Du. All rights reserved.
// Licensed under the MIT license that can be found in the LICENSE file.
package clone
import (
"encoding/binary"
"hash/crc64"
"reflect"
"sync"
"unsafe"
)
var (
sizeOfChecksum = unsafe.Sizeof(uint64(0))
crc64Table = crc64.MakeTable(crc64.ECMA)
cachedWrapperTypes sync.Map
)
// Wrap creates a wrapper of v, which must be a pointer.
// If v is not a pointer, Wrap simply returns v and do nothing.
//
// The wrapper is a deep clone of v's value. It holds a shadow copy to v internally.
//
// t := &T{Foo: 123}
// v := Wrap(t).(*T) // v is a clone of t.
// reflect.DeepEqual(t, v) == true // v equals t.
// v.Foo = 456 // v.Foo is changed, but t.Foo doesn't change.
// orig := Unwrap(v) // Use `Unwrap` to discard wrapper and return original value, which is t.
// orig.(*T) == t // orig and t is exactly the same.
// Undo(v) // Use `Undo` to discard any change on v.
// v.Foo == t.Foo // Now, the value of v and t are the same again.
func Wrap(v interface{}) interface{} {
if v == nil {
return v
}
val := reflect.ValueOf(v)
pt := val.Type()
if val.Kind() != reflect.Ptr {
return v
}
t := pt.Elem()
elem := val.Elem()
ptr := unsafe.Pointer(val.Pointer())
cache, ok := cachedWrapperTypes.Load(t)
if !ok {
cache = reflect.StructOf([]reflect.StructField{
{
Name: "T",
Type: t,
Anonymous: true,
},
{
Name: "Checksum",
Type: reflect.TypeOf(uint64(0)),
},
{
Name: "Origin",
Type: pt,
},
})
cachedWrapperTypes.Store(t, cache)
}
wrapperType := cache.(reflect.Type)
pw := defaultAllocator.New(wrapperType)
wrapperPtr := unsafe.Pointer(pw.Pointer())
wrapper := pw.Elem()
// Equivalent code: wrapper.T = Clone(v)
field := wrapper.Field(0)
field.Set(heapCloneState.clone(elem))
// Equivalent code: wrapper.Checksum = makeChecksum(v)
checksumPtr := unsafe.Pointer((uintptr(wrapperPtr) + t.Size()))
*(*uint64)(checksumPtr) = makeChecksum(t, uintptr(wrapperPtr), uintptr(ptr))
// Equivalent code: wrapper.Origin = v
originPtr := unsafe.Pointer((uintptr(wrapperPtr) + t.Size() + sizeOfChecksum))
*(*uintptr)(originPtr) = uintptr(ptr)
return field.Addr().Interface()
}
func validateChecksum(t reflect.Type, ptr unsafe.Pointer) bool {
pw := uintptr(ptr)
orig := uintptr(getOrigin(t, ptr))
checksum := *(*uint64)(unsafe.Pointer(uintptr(ptr) + t.Size()))
expected := makeChecksum(t, pw, orig)
return checksum == expected
}
func makeChecksum(t reflect.Type, pw uintptr, orig uintptr) uint64 {
var data [binary.MaxVarintLen64 * 2]byte
binary.PutUvarint(data[:binary.MaxVarintLen64], uint64(pw))
binary.PutUvarint(data[binary.MaxVarintLen64:], uint64(orig))
return crc64.Checksum(data[:], crc64Table)
}
func getOrigin(t reflect.Type, ptr unsafe.Pointer) unsafe.Pointer {
return *(*unsafe.Pointer)(unsafe.Pointer(uintptr(ptr) + t.Size() + sizeOfChecksum))
}
// Unwrap returns v's original value if v is a wrapped value.
// Otherwise, simply returns v itself.
func Unwrap(v interface{}) interface{} {
if v == nil {
return v
}
val := reflect.ValueOf(v)
if !isWrapped(val) {
return v
}
origVal := origin(val)
return origVal.Interface()
}
func origin(val reflect.Value) reflect.Value {
pt := val.Type()
t := pt.Elem()
ptr := unsafe.Pointer(val.Pointer())
orig := getOrigin(t, ptr)
origVal := reflect.NewAt(t, orig)
return origVal
}
// Undo discards any change made in wrapped value.
// If v is not a wrapped value, nothing happens.
func Undo(v interface{}) {
if v == nil {
return
}
val := reflect.ValueOf(v)
if !isWrapped(val) {
return
}
origVal := origin(val)
elem := val.Elem()
elem.Set(heapCloneState.clone(origVal.Elem()))
}
func isWrapped(val reflect.Value) bool {
pt := val.Type()
if pt.Kind() != reflect.Ptr {
return false
}
t := pt.Elem()
ptr := unsafe.Pointer(val.Pointer())
return validateChecksum(t, ptr)
}
+46
View File
@@ -17,6 +17,7 @@
- [Use `sql.Named` in a builder](#use-sqlnamed-in-a-builder)
- [Argument modifiers](#argument-modifiers)
- [Freestyle builder](#freestyle-builder)
- [Clone builders](#clone-builders)
- [Using special syntax to build SQL](#using-special-syntax-to-build-sql)
- [Interpolate `args` in the `sql`](#interpolate-args-in-the-sql)
- [License](#license)
@@ -381,6 +382,51 @@ fmt.Println(args)
// [1 2]
```
### Clone builders
The `Clone` methods make any builder reusable as a template. You can create a partially initialized builder once (even as a global), then call `Clone()` to get an independent copy to customize per request. This avoids repeated setup while keeping shared templates immutable and safe for concurrent use.
Supported builders with `Clone`:
- [CreateTableBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CreateTableBuilder)
- [CTEBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEBuilder)
- [CTEQueryBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#CTEQueryBuilder)
- [DeleteBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#DeleteBuilder)
- [InsertBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#InsertBuilder)
- [SelectBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#SelectBuilder)
- [UnionBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UnionBuilder)
- [UpdateBuilder](https://pkg.go.dev/github.com/huandu/go-sqlbuilder#UpdateBuilder)
Example: define a global SELECT template and clone it per call
```go
package yourpkg
import "github.com/huandu/go-sqlbuilder"
// Global template — safe to reuse by cloning.
var baseUserSelect = sqlbuilder.NewSelectBuilder().
Select("id", "name", "email").
From("users").
Where("deleted_at IS NULL")
func ListActiveUsers(limit, offset int) (string, []interface{}) {
sb := baseUserSelect.Clone() // independent copy
sb.OrderBy("id").Asc()
sb.Limit(limit).Offset(offset)
return sb.Build()
}
func GetActiveUserByID(id int64) (string, []interface{}) {
sb := baseUserSelect.Clone() // start from the same template
sb.Where(sb.Equal("id", id))
sb.Limit(1)
return sb.Build()
}
```
The same template pattern applies to other builders. For example, keep a base `UpdateBuilder` with the table and common `SET` clauses, or a base `CTEBuilder` defining reusable CTEs, then `Clone()` and add query-specific `WHERE`/`ORDER BY`/`LIMIT`/`RETURNING` as needed.
### Using special syntax to build SQL
The `sqlbuilder` package incorporates special syntax for representing uncompiled SQL internally. To leverage this syntax for developing customized tools, the `Build` function can be utilized to compile it with the necessary arguments.
+79 -8
View File
@@ -6,9 +6,12 @@ package sqlbuilder
import (
"database/sql"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"github.com/huandu/go-clone"
)
// Args stores arguments associated with a SQL.
@@ -17,7 +20,7 @@ type Args struct {
Flavor Flavor
indexBase int
argValues []interface{}
argValues *valueStore
namedArgs map[string]int
sqlNamedArgs map[string]int
onlyNamed bool
@@ -48,7 +51,7 @@ func (args *Args) Add(arg interface{}) string {
}
func (args *Args) add(arg interface{}) int {
idx := len(args.argValues) + args.indexBase
idx := args.argValues.Len() + args.indexBase
switch a := arg.(type) {
case sql.NamedArg:
@@ -57,7 +60,7 @@ func (args *Args) add(arg interface{}) int {
}
if p, ok := args.sqlNamedArgs[a.Name]; ok {
arg = args.argValues[p]
arg = args.argValues.Load(p)
break
}
@@ -68,7 +71,7 @@ func (args *Args) add(arg interface{}) int {
}
if p, ok := args.namedArgs[a.name]; ok {
arg = args.argValues[p]
arg = args.argValues.Load(p)
break
}
@@ -78,10 +81,31 @@ func (args *Args) add(arg interface{}) int {
return idx
}
args.argValues = append(args.argValues, arg)
if args.argValues == nil {
args.argValues = &valueStore{}
}
args.argValues.Add(arg)
return idx
}
// Replace replaces the placeholder with arg.
//
// The placeholder must be the value returned by `Add`, e.g. "$1".
// If the placeholder is not found, this method does nothing.
func (args *Args) Replace(placeholder string, arg interface{}) {
dollar := strings.IndexRune(placeholder, '$')
if dollar != 0 {
return
}
if i, err := strconv.Atoi(placeholder[1:]); err == nil {
i -= args.indexBase
args.argValues.Set(i, arg)
}
}
// Compile compiles builder's format to standard sql and returns associated args.
//
// The format string uses a special syntax to represent arguments.
@@ -201,14 +225,14 @@ func (args *Args) compileDigits(ctx *argsCompileContext, format string, offset i
}
func (args *Args) compileSuccessive(ctx *argsCompileContext, format string, offset int) (string, int) {
if offset < 0 || offset >= len(args.argValues) {
if offset < 0 || offset >= args.argValues.Len() {
ctx.WriteString("/* INVALID ARG $")
ctx.WriteString(strconv.Itoa(offset))
ctx.WriteString(" */")
return format, offset
}
arg := args.argValues[offset]
arg := args.argValues.Load(offset)
ctx.WriteValue(arg)
return format, offset + 1
@@ -245,7 +269,7 @@ func (args *Args) mergeSQLNamedArgs(ctx *argsCompileContext) []interface{} {
sort.Ints(ints)
for _, i := range ints {
values = append(values, args.argValues[i])
values = append(values, args.argValues.Load(i))
}
return values
@@ -364,3 +388,50 @@ func (ctx *argsCompileContext) WriteValues(values []interface{}, sep string) {
ctx.WriteValue(v)
}
}
type valueStore struct {
Values []interface{}
}
func init() {
// The values in valueStore should be shadow-copied to avoid unnecessary cost.
t := reflect.TypeOf(valueStore{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
values := old.FieldByName("Values")
newValues := allocator.Clone(values)
new.FieldByName("Values").Set(newValues)
})
}
func (as *valueStore) Len() int {
if as == nil {
return 0
}
return len(as.Values)
}
// Add adds an arg to argsValues and returns its index.
func (as *valueStore) Add(arg interface{}) int {
as.Values = append(as.Values, arg)
return len(as.Values) - 1
}
// Set sets the arg value by index.
func (as *valueStore) Set(index int, arg interface{}) {
if as == nil || index < 0 || index >= len(as.Values) {
return
}
as.Values[index] = arg
}
// Load returns the arg value by index.
// Returns nil if index is out of range or as itself is nil.
func (as *valueStore) Load(index int) interface{} {
if as == nil || index < 0 || index >= len(as.Values) {
return nil
}
return as.Values[index]
}
+8
View File
@@ -5,6 +5,8 @@ package sqlbuilder
import (
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -29,6 +31,12 @@ func newCreateTableBuilder() *CreateTableBuilder {
}
}
// Clone returns a deep copy of CreateTableBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ctb *CreateTableBuilder) Clone() *CreateTableBuilder {
return clone.Clone(ctb).(*CreateTableBuilder)
}
// CreateTableBuilder is a builder to build CREATE TABLE.
type CreateTableBuilder struct {
verb string
+26 -1
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
cteMarkerInit injectionMarker = iota
cteMarkerAfterWith
@@ -25,6 +31,25 @@ func newCTEBuilder() *CTEBuilder {
}
}
// Clone returns a deep copy of CTEBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (cteb *CTEBuilder) Clone() *CTEBuilder {
return clone.Clone(cteb).(*CTEBuilder)
}
func init() {
t := reflect.TypeOf(CTEBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
cteb := cloned.Addr().Interface().(*CTEBuilder)
for i, b := range cteb.queries {
cteb.args.Replace(cteb.queryBuilderVars[i], b)
}
})
}
// CTEBuilder is a CTE (Common Table Expression) builder.
type CTEBuilder struct {
recursive bool
@@ -47,7 +72,7 @@ func (cteb *CTEBuilder) With(queries ...*CTEQueryBuilder) *CTEBuilder {
queryBuilderVars = append(queryBuilderVars, cteb.args.Add(query))
}
cteb.queries = queries
cteb.queries = append([]*CTEQueryBuilder(nil), queries...)
cteb.queryBuilderVars = queryBuilderVars
cteb.marker = cteMarkerAfterWith
return cteb
+25
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
cteQueryMarkerInit injectionMarker = iota
cteQueryMarkerAfterTable
@@ -29,10 +35,28 @@ func newCTEQueryBuilder() *CTEQueryBuilder {
}
}
// Clone returns a deep copy of CTEQueryBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ctetb *CTEQueryBuilder) Clone() *CTEQueryBuilder {
return clone.Clone(ctetb).(*CTEQueryBuilder)
}
func init() {
t := reflect.TypeOf(CTEQueryBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ctetb := cloned.Addr().Interface().(*CTEQueryBuilder)
ctetb.args.Replace(ctetb.builderVar, ctetb.builder)
})
}
// CTEQueryBuilder is a builder to build one table in CTE (Common Table Expression).
type CTEQueryBuilder struct {
name string
cols []string
builder Builder
builderVar string
// if true, this query's table name will be automatically added to the table list
@@ -62,6 +86,7 @@ func (ctetb *CTEQueryBuilder) Table(name string, cols ...string) *CTEQueryBuilde
// As sets the builder to select data.
func (ctetb *CTEQueryBuilder) As(builder Builder) *CTEQueryBuilder {
ctetb.builder = builder
ctetb.builderVar = ctetb.args.Add(builder)
ctetb.marker = cteQueryMarkerAfterAs
return ctetb
+24
View File
@@ -3,6 +3,12 @@
package sqlbuilder
import (
"reflect"
"github.com/huandu/go-clone"
)
const (
deleteMarkerInit injectionMarker = iota
deleteMarkerAfterWith
@@ -33,6 +39,24 @@ func newDeleteBuilder() *DeleteBuilder {
}
}
// Clone returns a deep copy of DeleteBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (db *DeleteBuilder) Clone() *DeleteBuilder {
return clone.Clone(db).(*DeleteBuilder)
}
func init() {
t := reflect.TypeOf(DeleteBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
db := cloned.Addr().Interface().(*DeleteBuilder)
db.args.Replace(db.whereClauseExpr, db.whereClauseProxy)
db.args.Replace(db.cteBuilderVar, db.cteBuilder)
})
}
// DeleteBuilder is a builder to build DELETE.
type DeleteBuilder struct {
*WhereClause
+8
View File
@@ -6,6 +6,8 @@ package sqlbuilder
import (
"fmt"
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -31,6 +33,12 @@ func newInsertBuilder() *InsertBuilder {
}
}
// Clone returns a deep copy of InsertBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ib *InsertBuilder) Clone() *InsertBuilder {
return clone.Clone(ib).(*InsertBuilder)
}
// InsertBuilder is a builder to build INSERT.
type InsertBuilder struct {
verb string
+21
View File
@@ -5,7 +5,10 @@ package sqlbuilder
import (
"fmt"
"reflect"
"strings"
"github.com/huandu/go-clone"
)
const (
@@ -55,6 +58,24 @@ func newSelectBuilder() *SelectBuilder {
}
}
// Clone returns a deep copy of SelectBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (sb *SelectBuilder) Clone() *SelectBuilder {
return clone.Clone(sb).(*SelectBuilder)
}
func init() {
t := reflect.TypeOf(SelectBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
sb := cloned.Addr().Interface().(*SelectBuilder)
sb.args.Replace(sb.whereClauseExpr, sb.whereClauseProxy)
sb.args.Replace(sb.cteBuilderVar, sb.cteBuilder)
})
}
// SelectBuilder is a builder to build SELECT.
type SelectBuilder struct {
*WhereClause
+144 -6
View File
@@ -3,6 +3,13 @@
package sqlbuilder
import (
"fmt"
"reflect"
"github.com/huandu/go-clone"
)
const (
unionDistinct = " UNION " // Default union type is DISTINCT.
unionAll = " UNION ALL "
@@ -27,15 +34,36 @@ func newUnionBuilder() *UnionBuilder {
}
}
// Clone returns a deep copy of UnionBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ub *UnionBuilder) Clone() *UnionBuilder {
return clone.Clone(ub).(*UnionBuilder)
}
func init() {
t := reflect.TypeOf(UnionBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ub := cloned.Addr().Interface().(*UnionBuilder)
for i, b := range ub.builders {
ub.args.Replace(ub.builderVars[i], b)
}
})
}
// UnionBuilder is a builder to build UNION.
type UnionBuilder struct {
opt string
builderVars []string
orderByCols []string
order string
limitVar string
offsetVar string
builders []Builder
builderVars []string
args *Args
injection *injection
@@ -72,6 +100,7 @@ func (ub *UnionBuilder) union(opt string, builders ...Builder) *UnionBuilder {
}
ub.opt = opt
ub.builders = append([]Builder(nil), builders...)
ub.builderVars = builderVars
ub.marker = unionMarkerAfterUnion
return ub
@@ -140,9 +169,16 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
buf := newStringBuilder()
ub.injection.WriteTo(buf, unionMarkerInit)
nestedSelect := (flavor == Oracle && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0)) ||
(flavor == Informix && len(ub.limitVar) > 0)
if len(ub.builderVars) > 0 {
needParen := flavor != SQLite
if nestedSelect {
buf.WriteLeadingString("SELECT * FROM (")
}
if needParen {
buf.WriteLeadingString("(")
buf.WriteString(ub.builderVars[0])
@@ -164,6 +200,10 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
buf.WriteRune(')')
}
}
if nestedSelect {
buf.WriteLeadingString(")")
}
}
ub.injection.WriteTo(buf, unionMarkerAfterUnion)
@@ -180,17 +220,115 @@ func (ub *UnionBuilder) BuildWithFlavor(flavor Flavor, initialArg ...interface{}
ub.injection.WriteTo(buf, unionMarkerAfterOrderBy)
}
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
switch flavor {
case MySQL, SQLite, ClickHouse:
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
}
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
}
}
case CQL:
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
}
case PostgreSQL:
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
}
if ((MySQL == flavor || Informix == flavor) && len(ub.limitVar) > 0) || PostgreSQL == flavor {
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
}
case Presto:
// There might be a hidden constraint in Presto requiring offset to be set before limit.
// The select statement documentation (https://prestodb.io/docs/current/sql/select.html)
// puts offset before limit, and Trino, which is based on Presto, seems
// to require this specific order.
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
}
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(ub.limitVar)
}
case SQLServer:
// If ORDER BY is not set, sort column #1 by default.
// It's required to make OFFSET...FETCH work.
if len(ub.orderByCols) == 0 && (len(ub.limitVar) > 0 || len(ub.offsetVar) > 0) {
buf.WriteLeadingString("ORDER BY 1")
}
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
buf.WriteString(" ROWS")
}
if len(ub.limitVar) > 0 {
if len(ub.offsetVar) == 0 {
buf.WriteLeadingString("OFFSET 0 ROWS")
}
buf.WriteLeadingString("FETCH NEXT ")
buf.WriteString(ub.limitVar)
buf.WriteString(" ROWS ONLY")
}
case Oracle:
// It's required to make OFFSET...FETCH work.
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(ub.offsetVar)
buf.WriteString(" ROWS")
}
if len(ub.limitVar) > 0 {
if len(ub.offsetVar) == 0 {
buf.WriteLeadingString("OFFSET 0 ROWS")
}
buf.WriteLeadingString("FETCH NEXT ")
buf.WriteString(ub.limitVar)
buf.WriteString(" ROWS ONLY")
}
case Informix:
// [SKIP N] FIRST M
// M must be greater than 0
if len(ub.limitVar) > 0 {
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("SKIP ")
buf.WriteString(ub.offsetVar)
}
buf.WriteLeadingString("FIRST ")
buf.WriteString(ub.limitVar)
}
case Doris:
// #192: Doris doesn't support ? in OFFSET and LIMIT.
if len(ub.limitVar) > 0 {
buf.WriteLeadingString("LIMIT ")
buf.WriteString(fmt.Sprint(ub.args.Value(ub.limitVar)))
if len(ub.offsetVar) > 0 {
buf.WriteLeadingString("OFFSET ")
buf.WriteString(fmt.Sprint(ub.args.Value(ub.offsetVar)))
}
}
}
if len(ub.limitVar) > 0 {
+21
View File
@@ -5,6 +5,9 @@ package sqlbuilder
import (
"fmt"
"reflect"
"github.com/huandu/go-clone"
)
const (
@@ -38,6 +41,24 @@ func newUpdateBuilder() *UpdateBuilder {
}
}
// Clone returns a deep copy of UpdateBuilder.
// It's useful when you want to create a base builder and clone it to build similar queries.
func (ub *UpdateBuilder) Clone() *UpdateBuilder {
return clone.Clone(ub).(*UpdateBuilder)
}
func init() {
t := reflect.TypeOf(UpdateBuilder{})
clone.SetCustomFunc(t, func(allocator *clone.Allocator, old, new reflect.Value) {
cloned := allocator.CloneSlowly(old)
new.Set(cloned)
ub := cloned.Addr().Interface().(*UpdateBuilder)
ub.args.Replace(ub.whereClauseExpr, ub.whereClauseProxy)
ub.args.Replace(ub.cteBuilderVar, ub.cteBuilder)
})
}
// UpdateBuilder is a builder to build UPDATE.
type UpdateBuilder struct {
*WhereClause
+32
View File
@@ -0,0 +1,32 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
+5
View File
@@ -0,0 +1,5 @@
Changes
=======
v1.0.0 18 Aug 2025
* Initial release
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 lestrrat-go
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+29
View File
@@ -0,0 +1,29 @@
package dsigsecp256k1
import (
"crypto"
"github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lestrrat-go/dsig"
)
const ECDSAWithSecp256k1AndSHA256 = "ECDSA_WITH_SECP256K1_AND_SHA256"
// init adds secp256k1 support when the dsig_secp256k1 build tag is used.
func init() {
// Register ES256K (secp256k1 + SHA256) support using the new API
err := dsig.RegisterAlgorithm(ECDSAWithSecp256k1AndSHA256, dsig.AlgorithmInfo{
Family: dsig.ECDSA,
Meta: dsig.ECDSAFamilyMeta{
Hash: crypto.SHA256,
},
})
if err != nil {
panic("failed to register secp256k1 algorithm: " + err.Error())
}
}
// secp256k1Curve returns the secp256k1 curve.
func Curve() *secp256k1.KoblitzCurve {
return secp256k1.S256()
}
+32
View File
@@ -0,0 +1,32 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Code coverage profiles and other test artifacts
*.out
coverage.*
*.coverprofile
profile.cov
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
go.work.sum
# env file
.env
# Editor/IDE
# .idea/
# .vscode/
+5
View File
@@ -0,0 +1,5 @@
Changes
=======
v1.0.0 - 18 Aug 2025
* Initial release
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 lestrrat-go
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+163
View File
@@ -0,0 +1,163 @@
# github.com/lestrrat-go/dsig [![CI](https://github.com/lestrrat-go/dsig/actions/workflows/ci.yml/badge.svg)](https://github.com/lestrrat-go/dsig/actions/workflows/ci.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/lestrrat-go/dsig.svg)](https://pkg.go.dev/github.com/lestrrat-go/dsig) [![codecov.io](https://codecov.io/github/lestrrat-go/dsig/coverage.svg?branch=v1)](https://codecov.io/github/lestrrat-go/dsig?branch=v1)
Go module providing low-level digital signature operations.
While there are many standards for generating and verifying digital signatures, the core operations are virtually the same. This module implements the core functionality of digital signature generation / verifications in a framework agnostic way.
# Features
* RSA signatures (PKCS1v15 and PSS)
* ECDSA signatures (P-256, P-384, P-521)
* EdDSA signatures (Ed25519, Ed448)
* HMAC signatures (SHA-256, SHA-384, SHA-512)
* Support for crypto.Signer interface
* Allows for dynamic additions of algorithms in limited cases.
# SYNOPSIS
<!-- INCLUDE(examples/dsig_readme_example_test.go) -->
```go
package examples_test
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"fmt"
"github.com/lestrrat-go/dsig"
)
func Example() {
payload := []byte("hello world")
// RSA signing and verification
{
privKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
fmt.Printf("failed to generate RSA key: %s\n", err)
return
}
// Sign with RSA-PSS SHA256
signature, err := dsig.Sign(privKey, dsig.RSAPSSWithSHA256, payload, nil)
if err != nil {
fmt.Printf("failed to sign with RSA: %s\n", err)
return
}
// Verify with RSA-PSS SHA256
err = dsig.Verify(&privKey.PublicKey, dsig.RSAPSSWithSHA256, payload, signature)
if err != nil {
fmt.Printf("failed to verify RSA signature: %s\n", err)
return
}
}
// ECDSA signing and verification
{
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
fmt.Printf("failed to generate ECDSA key: %s\n", err)
return
}
// Sign with ECDSA P-256 SHA256
signature, err := dsig.Sign(privKey, dsig.ECDSAWithP256AndSHA256, payload, nil)
if err != nil {
fmt.Printf("failed to sign with ECDSA: %s\n", err)
return
}
// Verify with ECDSA P-256 SHA256
err = dsig.Verify(&privKey.PublicKey, dsig.ECDSAWithP256AndSHA256, payload, signature)
if err != nil {
fmt.Printf("failed to verify ECDSA signature: %s\n", err)
return
}
}
// EdDSA signing and verification
{
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
fmt.Printf("failed to generate Ed25519 key: %s\n", err)
return
}
// Sign with EdDSA
signature, err := dsig.Sign(privKey, dsig.EdDSA, payload, nil)
if err != nil {
fmt.Printf("failed to sign with EdDSA: %s\n", err)
return
}
// Verify with EdDSA
err = dsig.Verify(pubKey, dsig.EdDSA, payload, signature)
if err != nil {
fmt.Printf("failed to verify EdDSA signature: %s\n", err)
return
}
}
// HMAC signing and verification
{
key := []byte("secret-key")
// Sign with HMAC SHA256
signature, err := dsig.Sign(key, dsig.HMACWithSHA256, payload, nil)
if err != nil {
fmt.Printf("failed to sign with HMAC: %s\n", err)
return
}
// Verify with HMAC SHA256
err = dsig.Verify(key, dsig.HMACWithSHA256, payload, signature)
if err != nil {
fmt.Printf("failed to verify HMAC signature: %s\n", err)
return
}
}
// OUTPUT:
}
```
source: [examples/dsig_readme_example_test.go](https://github.com/lestrrat-go/dsig/blob/v1/examples/dsig_readme_example_test.go)
<!-- END INCLUDE -->
# Supported Algorithms
| Constant | Algorithm | Key Type |
|----------|-----------|----------|
| `HMACWithSHA256` | HMAC using SHA-256 | []byte |
| `HMACWithSHA384` | HMAC using SHA-384 | []byte |
| `HMACWithSHA512` | HMAC using SHA-512 | []byte |
| `RSAPKCS1v15WithSHA256` | RSA PKCS#1 v1.5 using SHA-256 | *rsa.PrivateKey / *rsa.PublicKey |
| `RSAPKCS1v15WithSHA384` | RSA PKCS#1 v1.5 using SHA-384 | *rsa.PrivateKey / *rsa.PublicKey |
| `RSAPKCS1v15WithSHA512` | RSA PKCS#1 v1.5 using SHA-512 | *rsa.PrivateKey / *rsa.PublicKey |
| `RSAPSSWithSHA256` | RSA PSS using SHA-256 | *rsa.PrivateKey / *rsa.PublicKey |
| `RSAPSSWithSHA384` | RSA PSS using SHA-384 | *rsa.PrivateKey / *rsa.PublicKey |
| `RSAPSSWithSHA512` | RSA PSS using SHA-512 | *rsa.PrivateKey / *rsa.PublicKey |
| `ECDSAWithP256AndSHA256` | ECDSA using P-256 and SHA-256 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
| `ECDSAWithP384AndSHA384` | ECDSA using P-384 and SHA-384 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
| `ECDSAWithP521AndSHA512` | ECDSA using P-521 and SHA-512 | *ecdsa.PrivateKey / *ecdsa.PublicKey |
| `EdDSA` | EdDSA using Ed25519 or Ed448 | ed25519.PrivateKey / ed25519.PublicKey |
# Description
This library provides low-level digital signature operations. It does minimal parameter validation for performance, uses strongly typed APIs, and has minimal dependencies.
# Contributions
## Issues
For bug reports and feature requests, please include failing tests when possible.
## Pull Requests
Please include tests that exercise your changes.
# Related Libraries
* [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) - JOSE (JWA/JWE/JWK/JWS/JWT) implementation
+37
View File
@@ -0,0 +1,37 @@
package dsig
// This file defines verbose algorithm name constants that can be mapped to by
// different standards (RFC7518, FIDO, etc.) for interoperability.
//
// The algorithm names are intentionally verbose to avoid any ambiguity about
// the exact cryptographic operations being performed.
const (
// HMAC signature algorithms
// These use Hash-based Message Authentication Code with specified hash functions
HMACWithSHA256 = "HMAC_WITH_SHA256"
HMACWithSHA384 = "HMAC_WITH_SHA384"
HMACWithSHA512 = "HMAC_WITH_SHA512"
// RSA signature algorithms with PKCS#1 v1.5 padding
// These use RSA signatures with PKCS#1 v1.5 padding and specified hash functions
RSAPKCS1v15WithSHA256 = "RSA_PKCS1v15_WITH_SHA256"
RSAPKCS1v15WithSHA384 = "RSA_PKCS1v15_WITH_SHA384"
RSAPKCS1v15WithSHA512 = "RSA_PKCS1v15_WITH_SHA512"
// RSA signature algorithms with PSS padding
// These use RSA signatures with Probabilistic Signature Scheme (PSS) padding
RSAPSSWithSHA256 = "RSA_PSS_WITH_SHA256"
RSAPSSWithSHA384 = "RSA_PSS_WITH_SHA384"
RSAPSSWithSHA512 = "RSA_PSS_WITH_SHA512"
// ECDSA signature algorithms
// These use Elliptic Curve Digital Signature Algorithm with specified curves and hash functions
ECDSAWithP256AndSHA256 = "ECDSA_WITH_P256_AND_SHA256"
ECDSAWithP384AndSHA384 = "ECDSA_WITH_P384_AND_SHA384"
ECDSAWithP521AndSHA512 = "ECDSA_WITH_P521_AND_SHA512"
// EdDSA signature algorithms
// These use Edwards-curve Digital Signature Algorithm (supports Ed25519 and Ed448)
EdDSA = "EDDSA"
)
+45
View File
@@ -0,0 +1,45 @@
package dsig
import (
"crypto"
"crypto/rand"
"fmt"
"io"
)
// cryptosign is a low-level function that signs a payload using a crypto.Signer.
// If hash is crypto.Hash(0), the payload is signed directly without hashing.
// Otherwise, the payload is hashed using the specified hash function before signing.
//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
func cryptosign(signer crypto.Signer, payload []byte, hash crypto.Hash, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
if rr == nil {
rr = rand.Reader
}
var digest []byte
if hash == crypto.Hash(0) {
digest = payload
} else {
h := hash.New()
if _, err := h.Write(payload); err != nil {
return nil, fmt.Errorf(`failed to write payload to hash: %w`, err)
}
digest = h.Sum(nil)
}
return signer.Sign(rr, digest, opts)
}
// SignCryptoSigner generates a signature using a crypto.Signer interface.
// This function can be used for hardware security modules, smart cards,
// and other implementations of the crypto.Signer interface.
//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
//
// Returns the signature bytes or an error if signing fails.
func SignCryptoSigner(signer crypto.Signer, raw []byte, h crypto.Hash, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
if signer == nil {
return nil, fmt.Errorf("dsig.SignCryptoSigner: signer is nil")
}
return cryptosign(signer, raw, h, opts, rr)
}
+224
View File
@@ -0,0 +1,224 @@
// Package dsig provides digital signature operations for Go.
// It contains low-level signature generation and verification tools that
// can be used by other signing libraries
//
// The package follows these design principles:
// 1. Does minimal checking of input parameters (for performance); callers need to ensure that the parameters are valid.
// 2. All exported functions are strongly typed (i.e. they do not take `any` types unless they absolutely have to).
// 3. Does not rely on other high-level packages (standalone, except for internal packages).
package dsig
import (
"crypto"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
"sync"
)
// Family represents the cryptographic algorithm family
type Family int
const (
InvalidFamily Family = iota
HMAC
RSA
ECDSA
EdDSAFamily
maxFamily
)
// String returns the string representation of the Family
func (f Family) String() string {
switch f {
case HMAC:
return "HMAC"
case RSA:
return "RSA"
case ECDSA:
return "ECDSA"
case EdDSAFamily:
return "EdDSA"
default:
return "InvalidFamily"
}
}
// AlgorithmInfo contains metadata about a digital signature algorithm
type AlgorithmInfo struct {
Family Family // The cryptographic family (HMAC, RSA, ECDSA, EdDSA)
Meta any // Family-specific metadata
}
// HMACFamilyMeta contains metadata specific to HMAC algorithms
type HMACFamilyMeta struct {
HashFunc func() hash.Hash // Hash function constructor
}
// RSAFamilyMeta contains metadata specific to RSA algorithms
type RSAFamilyMeta struct {
Hash crypto.Hash // Hash algorithm
PSS bool // Whether to use PSS padding (false = PKCS#1 v1.5)
}
// ECDSAFamilyMeta contains metadata specific to ECDSA algorithms
type ECDSAFamilyMeta struct {
Hash crypto.Hash // Hash algorithm
}
// EdDSAFamilyMeta contains metadata specific to EdDSA algorithms
// Currently EdDSA doesn't need specific metadata, but this provides extensibility
type EdDSAFamilyMeta struct {
// Reserved for future use
}
var algorithms = make(map[string]AlgorithmInfo)
var muAlgorithms sync.RWMutex
// RegisterAlgorithm registers a new digital signature algorithm with the specified family and metadata.
//
// info.Meta should contain extra metadata for some algorithms. Currently HMAC, RSA,
// and ECDSA family of algorithms need their respective metadata (HMACFamilyMeta,
// RSAFamilyMeta, and ECDSAFamilyMeta). Metadata for other families are ignored.
func RegisterAlgorithm(name string, info AlgorithmInfo) error {
muAlgorithms.Lock()
defer muAlgorithms.Unlock()
// Validate the metadata matches the family
switch info.Family {
case HMAC:
if _, ok := info.Meta.(HMACFamilyMeta); !ok {
return fmt.Errorf("invalid HMAC metadata for algorithm %s", name)
}
case RSA:
if _, ok := info.Meta.(RSAFamilyMeta); !ok {
return fmt.Errorf("invalid RSA metadata for algorithm %s", name)
}
case ECDSA:
if _, ok := info.Meta.(ECDSAFamilyMeta); !ok {
return fmt.Errorf("invalid ECDSA metadata for algorithm %s", name)
}
case EdDSAFamily:
// EdDSA metadata is optional for now
default:
return fmt.Errorf("unsupported algorithm family %s for algorithm %s", info.Family, name)
}
algorithms[name] = info
return nil
}
// GetAlgorithmInfo retrieves the algorithm information for a given algorithm name.
// Returns the info and true if found, zero value and false if not found.
func GetAlgorithmInfo(name string) (AlgorithmInfo, bool) {
muAlgorithms.RLock()
defer muAlgorithms.RUnlock()
info, ok := algorithms[name]
return info, ok
}
func init() {
// Register all standard algorithms with their metadata
toRegister := map[string]AlgorithmInfo{
// HMAC algorithms
HMACWithSHA256: {
Family: HMAC,
Meta: HMACFamilyMeta{
HashFunc: sha256.New,
},
},
HMACWithSHA384: {
Family: HMAC,
Meta: HMACFamilyMeta{
HashFunc: sha512.New384,
},
},
HMACWithSHA512: {
Family: HMAC,
Meta: HMACFamilyMeta{
HashFunc: sha512.New,
},
},
// RSA PKCS#1 v1.5 algorithms
RSAPKCS1v15WithSHA256: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA256,
PSS: false,
},
},
RSAPKCS1v15WithSHA384: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA384,
PSS: false,
},
},
RSAPKCS1v15WithSHA512: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA512,
PSS: false,
},
},
// RSA PSS algorithms
RSAPSSWithSHA256: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA256,
PSS: true,
},
},
RSAPSSWithSHA384: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA384,
PSS: true,
},
},
RSAPSSWithSHA512: {
Family: RSA,
Meta: RSAFamilyMeta{
Hash: crypto.SHA512,
PSS: true,
},
},
// ECDSA algorithms
ECDSAWithP256AndSHA256: {
Family: ECDSA,
Meta: ECDSAFamilyMeta{
Hash: crypto.SHA256,
},
},
ECDSAWithP384AndSHA384: {
Family: ECDSA,
Meta: ECDSAFamilyMeta{
Hash: crypto.SHA384,
},
},
ECDSAWithP521AndSHA512: {
Family: ECDSA,
Meta: ECDSAFamilyMeta{
Hash: crypto.SHA512,
},
},
// EdDSA algorithm
EdDSA: {
Family: EdDSAFamily,
Meta: EdDSAFamilyMeta{},
},
}
for name, info := range toRegister {
if err := RegisterAlgorithm(name, info); err != nil {
panic(fmt.Sprintf("failed to register algorithm %s: %v", name, err))
}
}
}
+200
View File
@@ -0,0 +1,200 @@
package dsig
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"encoding/asn1"
"fmt"
"io"
"math/big"
"github.com/lestrrat-go/dsig/internal/ecutil"
)
func ecdsaGetSignerKey(key any) (*ecdsa.PrivateKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
if !isValidECDSAKey(key) {
return nil, nil, false, fmt.Errorf(`invalid key type %T for ECDSA algorithm`, key)
}
switch key.(type) {
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
// if it's ecdsa.PrivateKey, it's more efficient to
// go through the non-crypto.Signer route. Set isCryptoSigner to false
isCryptoSigner = false
}
}
if isCryptoSigner {
return nil, cs, true, nil
}
privkey, ok := key.(*ecdsa.PrivateKey)
if !ok {
return nil, nil, false, fmt.Errorf(`invalid key type %T. *ecdsa.PrivateKey is required`, key)
}
return privkey, nil, false, nil
}
// UnpackASN1ECDSASignature unpacks an ASN.1 encoded ECDSA signature into r and s values.
// This is typically used when working with crypto.Signer interfaces that return ASN.1 encoded signatures.
func UnpackASN1ECDSASignature(signed []byte, r, s *big.Int) error {
// Okay, this is silly, but hear me out. When we use the
// crypto.Signer interface, the PrivateKey is hidden.
// But we need some information about the key (its bit size).
//
// So while silly, we're going to have to make another call
// here and fetch the Public key.
// (This probably means that this information should be cached somewhere)
var p struct {
R *big.Int // TODO: get this from a pool?
S *big.Int
}
if _, err := asn1.Unmarshal(signed, &p); err != nil {
return fmt.Errorf(`failed to unmarshal ASN1 encoded signature: %w`, err)
}
r.Set(p.R)
s.Set(p.S)
return nil
}
// UnpackECDSASignature unpacks a JWS-format ECDSA signature into r and s values.
// The signature should be in the format specified by RFC 7515 (r||s as fixed-length byte arrays).
func UnpackECDSASignature(signature []byte, pubkey *ecdsa.PublicKey, r, s *big.Int) error {
keySize := ecutil.CalculateKeySize(pubkey.Curve)
if len(signature) != keySize*2 {
return fmt.Errorf(`invalid signature length for curve %q`, pubkey.Curve.Params().Name)
}
r.SetBytes(signature[:keySize])
s.SetBytes(signature[keySize:])
return nil
}
// PackECDSASignature packs the r and s values from an ECDSA signature into a JWS-format byte slice.
// The output format follows RFC 7515: r||s as fixed-length byte arrays.
func PackECDSASignature(r *big.Int, sbig *big.Int, curveBits int) ([]byte, error) {
keyBytes := curveBits / 8
if curveBits%8 > 0 {
keyBytes++
}
// Serialize r and s into fixed-length bytes
rBytes := r.Bytes()
rBytesPadded := make([]byte, keyBytes)
copy(rBytesPadded[keyBytes-len(rBytes):], rBytes)
sBytes := sbig.Bytes()
sBytesPadded := make([]byte, keyBytes)
copy(sBytesPadded[keyBytes-len(sBytes):], sBytes)
// Output as r||s
return append(rBytesPadded, sBytesPadded...), nil
}
// SignECDSA generates an ECDSA signature for the given payload using the specified private key and hash.
// The raw parameter should be the pre-computed signing input (typically header.payload).
//
// rr is an io.Reader that provides randomness for signing. if rr is nil, it defaults to rand.Reader.
func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
if !isValidECDSAKey(key) {
return nil, fmt.Errorf(`invalid key type %T for ECDSA algorithm`, key)
}
hh := h.New()
if _, err := hh.Write(payload); err != nil {
return nil, fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
}
digest := hh.Sum(nil)
if rr == nil {
rr = rand.Reader
}
// Sign and get r, s values
r, s, err := ecdsa.Sign(rr, key, digest)
if err != nil {
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
}
return PackECDSASignature(r, s, key.Curve.Params().BitSize)
}
// SignECDSACryptoSigner generates an ECDSA signature using a crypto.Signer interface.
// This function works with hardware security modules and other crypto.Signer implementations.
// The signature is converted from ASN.1 format to JWS format (r||s).
//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
func SignECDSACryptoSigner(signer crypto.Signer, raw []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
signed, err := SignCryptoSigner(signer, raw, h, h, rr)
if err != nil {
return nil, fmt.Errorf(`failed to sign payload using crypto.Signer: %w`, err)
}
return signECDSACryptoSigner(signer, signed)
}
func signECDSACryptoSigner(signer crypto.Signer, signed []byte) ([]byte, error) {
cpub := signer.Public()
pubkey, ok := cpub.(*ecdsa.PublicKey)
if !ok {
return nil, fmt.Errorf(`expected *ecdsa.PublicKey, got %T`, pubkey)
}
curveBits := pubkey.Curve.Params().BitSize
var r, s big.Int
if err := UnpackASN1ECDSASignature(signed, &r, &s); err != nil {
return nil, fmt.Errorf(`failed to unpack ASN1 encoded signature: %w`, err)
}
return PackECDSASignature(&r, &s, curveBits)
}
func ecdsaVerify(key *ecdsa.PublicKey, buf []byte, h crypto.Hash, r, s *big.Int) error {
hasher := h.New()
hasher.Write(buf)
digest := hasher.Sum(nil)
if !ecdsa.Verify(key, digest, r, s) {
return NewVerificationError("invalid ECDSA signature")
}
return nil
}
// VerifyECDSA verifies an ECDSA signature for the given payload.
// This function verifies the signature using the specified public key and hash algorithm.
// The payload parameter should be the pre-computed signing input (typically header.payload).
func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
var r, s big.Int
if err := UnpackECDSASignature(signature, key, &r, &s); err != nil {
return fmt.Errorf("dsig.VerifyECDSA: failed to unpack ECDSA signature: %w", err)
}
return ecdsaVerify(key, payload, h, &r, &s)
}
// VerifyECDSACryptoSigner verifies an ECDSA signature for crypto.Signer implementations.
// This function is useful for verifying signatures created by hardware security modules
// or other implementations of the crypto.Signer interface.
// The payload parameter should be the pre-computed signing input (typically header.payload).
func VerifyECDSACryptoSigner(signer crypto.Signer, payload, signature []byte, h crypto.Hash) error {
var pubkey *ecdsa.PublicKey
switch cpub := signer.Public(); cpub := cpub.(type) {
case ecdsa.PublicKey:
pubkey = &cpub
case *ecdsa.PublicKey:
pubkey = cpub
default:
return fmt.Errorf(`dsig.VerifyECDSACryptoSigner: expected *ecdsa.PublicKey, got %T`, cpub)
}
var r, s big.Int
if err := UnpackECDSASignature(signature, pubkey, &r, &s); err != nil {
return fmt.Errorf("dsig.VerifyECDSACryptoSigner: failed to unpack ASN.1 encoded ECDSA signature: %w", err)
}
return ecdsaVerify(pubkey, payload, h, &r, &s)
}
+44
View File
@@ -0,0 +1,44 @@
package dsig
import (
"crypto"
"crypto/ed25519"
"fmt"
)
func eddsaGetSigner(key any) (crypto.Signer, error) {
// The ed25519.PrivateKey object implements crypto.Signer, so we should
// simply accept a crypto.Signer here.
signer, ok := key.(crypto.Signer)
if ok {
if !isValidEDDSAKey(key) {
return nil, fmt.Errorf(`invalid key type %T for EdDSA algorithm`, key)
}
return signer, nil
}
// This fallback exists for cases when users give us a pointer instead of non-pointer, etc.
privkey, ok := key.(ed25519.PrivateKey)
if !ok {
return nil, fmt.Errorf(`failed to retrieve ed25519.PrivateKey out of %T`, key)
}
return privkey, nil
}
// SignEdDSA generates an EdDSA (Ed25519) signature for the given payload.
// The raw parameter should be the pre-computed signing input (typically header.payload).
// EdDSA is deterministic and doesn't require additional hashing of the input.
func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) {
return ed25519.Sign(key, payload), nil
}
// VerifyEdDSA verifies an EdDSA (Ed25519) signature for the given payload.
// This function verifies the signature using Ed25519 verification algorithm.
// The payload parameter should be the pre-computed signing input (typically header.payload).
// EdDSA is deterministic and provides strong security guarantees without requiring hash function selection.
func VerifyEdDSA(key ed25519.PublicKey, payload, signature []byte) error {
if !ed25519.Verify(key, payload, signature) {
return fmt.Errorf("invalid EdDSA signature")
}
return nil
}
+45
View File
@@ -0,0 +1,45 @@
package dsig
import (
"crypto/hmac"
"fmt"
"hash"
)
func toHMACKey(dst *[]byte, key any) error {
keyBytes, ok := key.([]byte)
if !ok {
return fmt.Errorf(`dsig.toHMACKey: invalid key type %T. []byte is required`, key)
}
if len(keyBytes) == 0 {
return fmt.Errorf(`dsig.toHMACKey: missing key while signing payload`)
}
*dst = keyBytes
return nil
}
// SignHMAC generates an HMAC signature for the given payload using the specified hash function and key.
// The raw parameter should be the pre-computed signing input (typically header.payload).
func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) {
h := hmac.New(hfunc, key)
if _, err := h.Write(payload); err != nil {
return nil, fmt.Errorf(`failed to write payload using hmac: %w`, err)
}
return h.Sum(nil), nil
}
// VerifyHMAC verifies an HMAC signature for the given payload.
// This function verifies the signature using the specified key and hash function.
// The payload parameter should be the pre-computed signing input (typically header.payload).
func VerifyHMAC(key, payload, signature []byte, hfunc func() hash.Hash) error {
expected, err := SignHMAC(key, payload, hfunc)
if err != nil {
return fmt.Errorf("failed to sign payload for verification: %w", err)
}
if !hmac.Equal(signature, expected) {
return NewVerificationError("invalid HMAC signature")
}
return nil
}
+76
View File
@@ -0,0 +1,76 @@
// Package ecutil defines tools that help with elliptic curve related
// computation
package ecutil
import (
"crypto/elliptic"
"math/big"
"sync"
)
const (
// size of buffer that needs to be allocated for EC521 curve
ec521BufferSize = 66 // (521 / 8) + 1
)
var ecpointBufferPool = sync.Pool{
New: func() any {
// In most cases the curve bit size will be less than this length
// so allocate the maximum, and keep reusing
buf := make([]byte, 0, ec521BufferSize)
return &buf
},
}
func getCrvFixedBuffer(size int) []byte {
//nolint:forcetypeassert
buf := *(ecpointBufferPool.Get().(*[]byte))
if size > ec521BufferSize && cap(buf) < size {
buf = append(buf, make([]byte, size-cap(buf))...)
}
return buf[:size]
}
// ReleaseECPointBuffer releases the []byte buffer allocated.
func ReleaseECPointBuffer(buf []byte) {
buf = buf[:cap(buf)]
buf[0] = 0x0
for i := 1; i < len(buf); i *= 2 {
copy(buf[i:], buf[:i])
}
buf = buf[:0]
ecpointBufferPool.Put(&buf)
}
func CalculateKeySize(crv elliptic.Curve) int {
// We need to create a buffer that fits the entire curve.
// If the curve size is 66, that fits in 9 bytes. If the curve
// size is 64, it fits in 8 bytes.
bits := crv.Params().BitSize
// For most common cases we know before hand what the byte length
// is going to be. optimize
var inBytes int
switch bits {
case 224, 256, 384: // TODO: use constant?
inBytes = bits / 8
case 521:
inBytes = ec521BufferSize
default:
inBytes = bits / 8
if (bits % 8) != 0 {
inBytes++
}
}
return inBytes
}
// AllocECPointBuffer allocates a buffer for the given point in the given
// curve. This buffer should be released using the ReleaseECPointBuffer
// function.
func AllocECPointBuffer(v *big.Int, crv elliptic.Curve) []byte {
buf := getCrvFixedBuffer(CalculateKeySize(crv))
v.FillBytes(buf)
return buf
}
+63
View File
@@ -0,0 +1,63 @@
package dsig
import (
"crypto"
"crypto/rsa"
"fmt"
"io"
)
func rsaGetSignerCryptoSignerKey(key any) (crypto.Signer, bool, error) {
if !isValidRSAKey(key) {
return nil, false, fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
}
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
return cs, true, nil
}
return nil, false, nil
}
// rsaPSSOptions returns the PSS options for RSA-PSS signatures with the specified hash.
// The salt length is set to equal the hash length as per RFC 7518.
func rsaPSSOptions(h crypto.Hash) rsa.PSSOptions {
return rsa.PSSOptions{
Hash: h,
SaltLength: rsa.PSSSaltLengthEqualsHash,
}
}
// SignRSA generates an RSA signature for the given payload using the specified private key and options.
// The raw parameter should be the pre-computed signing input (typically header.payload).
// If pss is true, RSA-PSS is used; otherwise, PKCS#1 v1.5 is used.
//
// The rr parameter is an optional io.Reader that can be used to provide randomness for signing.
// If rr is nil, it defaults to rand.Reader.
func SignRSA(key *rsa.PrivateKey, payload []byte, h crypto.Hash, pss bool, rr io.Reader) ([]byte, error) {
if !isValidRSAKey(key) {
return nil, fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
}
var opts crypto.SignerOpts = h
if pss {
rsaopts := rsaPSSOptions(h)
opts = &rsaopts
}
return cryptosign(key, payload, h, opts, rr)
}
// VerifyRSA verifies an RSA signature for the given payload and header.
// This function constructs the signing input by encoding the header and payload according to JWS specification,
// then verifies the signature using the specified public key and hash algorithm.
// If pss is true, RSA-PSS verification is used; otherwise, PKCS#1 v1.5 verification is used.
func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss bool) error {
if !isValidRSAKey(key) {
return fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
}
hasher := h.New()
hasher.Write(payload)
digest := hasher.Sum(nil)
if pss {
return rsa.VerifyPSS(key, h, digest, signature, &rsa.PSSOptions{Hash: h, SaltLength: rsa.PSSSaltLengthEqualsHash})
}
return rsa.VerifyPKCS1v15(key, h, digest, signature)
}
+100
View File
@@ -0,0 +1,100 @@
package dsig
import (
"crypto"
"crypto/rsa"
"fmt"
"io"
)
// Sign generates a digital signature using the specified key and algorithm.
//
// This function loads the signer registered in the dsig package _ONLY_.
// It does not support custom signers that the user might have registered.
//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
// Not all algorithms require this parameter, but it is included for consistency.
// 99% of the time, you can pass nil for rr, and it will work fine.
func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
info, ok := GetAlgorithmInfo(alg)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: unsupported signature algorithm %q`, alg)
}
switch info.Family {
case HMAC:
return dispatchHMACSign(key, info, payload)
case RSA:
return dispatchRSASign(key, info, payload, rr)
case ECDSA:
return dispatchECDSASign(key, info, payload, rr)
case EdDSAFamily:
return dispatchEdDSASign(key, info, payload, rr)
default:
return nil, fmt.Errorf(`dsig.Sign: unsupported signature family %q`, info.Family)
}
}
func dispatchHMACSign(key any, info AlgorithmInfo, payload []byte) ([]byte, error) {
meta, ok := info.Meta.(HMACFamilyMeta)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: invalid HMAC metadata`)
}
var hmackey []byte
if err := toHMACKey(&hmackey, key); err != nil {
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
}
return SignHMAC(hmackey, payload, meta.HashFunc)
}
func dispatchRSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
meta, ok := info.Meta.(RSAFamilyMeta)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: invalid RSA metadata`)
}
cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key)
if err != nil {
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
}
if isCryptoSigner {
var options crypto.SignerOpts = meta.Hash
if meta.PSS {
rsaopts := rsaPSSOptions(meta.Hash)
options = &rsaopts
}
return SignCryptoSigner(cs, payload, meta.Hash, options, rr)
}
privkey, ok := key.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: invalid key type %T. *rsa.PrivateKey is required`, key)
}
return SignRSA(privkey, payload, meta.Hash, meta.PSS, rr)
}
func dispatchEdDSASign(key any, _ AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
signer, err := eddsaGetSigner(key)
if err != nil {
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
}
return SignCryptoSigner(signer, payload, crypto.Hash(0), crypto.Hash(0), rr)
}
func dispatchECDSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
meta, ok := info.Meta.(ECDSAFamilyMeta)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: invalid ECDSA metadata`)
}
privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key)
if err != nil {
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
}
if isCryptoSigner {
return SignECDSACryptoSigner(cs, payload, meta.Hash, rr)
}
return SignECDSA(privkey, payload, meta.Hash, rr)
}
+66
View File
@@ -0,0 +1,66 @@
package dsig
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
)
// isValidRSAKey validates that the provided key type is appropriate for RSA algorithms.
// It returns false if the key is clearly incompatible (e.g., ECDSA or EdDSA keys).
func isValidRSAKey(key any) bool {
switch key.(type) {
case
ecdsa.PrivateKey, *ecdsa.PrivateKey,
ed25519.PrivateKey:
// these are NOT ok for RSA algorithms
return false
}
return true
}
// isValidECDSAKey validates that the provided key type is appropriate for ECDSA algorithms.
// It returns false if the key is clearly incompatible (e.g., RSA or EdDSA keys).
func isValidECDSAKey(key any) bool {
switch key.(type) {
case
ed25519.PrivateKey,
rsa.PrivateKey, *rsa.PrivateKey:
// these are NOT ok for ECDSA algorithms
return false
}
return true
}
// isValidEDDSAKey validates that the provided key type is appropriate for EdDSA algorithms.
// It returns false if the key is clearly incompatible (e.g., RSA or ECDSA keys).
func isValidEDDSAKey(key any) bool {
switch key.(type) {
case
ecdsa.PrivateKey, *ecdsa.PrivateKey,
rsa.PrivateKey, *rsa.PrivateKey:
// these are NOT ok for EdDSA algorithms
return false
}
return true
}
// VerificationError represents an error that occurred during signature verification.
type VerificationError struct {
message string
}
func (e *VerificationError) Error() string {
return e.message
}
// NewVerificationError creates a new verification error with the given message.
func NewVerificationError(message string) error {
return &VerificationError{message: message}
}
// IsVerificationError checks if the given error is a verification error.
func IsVerificationError(err error) bool {
_, ok := err.(*VerificationError)
return ok
}
+134
View File
@@ -0,0 +1,134 @@
package dsig
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"fmt"
)
// Verify verifies a digital signature using the specified key and algorithm.
//
// This function loads the verifier registered in the dsig package _ONLY_.
// It does not support custom verifiers that the user might have registered.
func Verify(key any, alg string, payload, signature []byte) error {
info, ok := GetAlgorithmInfo(alg)
if !ok {
return fmt.Errorf(`dsig.Verify: unsupported signature algorithm %q`, alg)
}
switch info.Family {
case HMAC:
return dispatchHMACVerify(key, info, payload, signature)
case RSA:
return dispatchRSAVerify(key, info, payload, signature)
case ECDSA:
return dispatchECDSAVerify(key, info, payload, signature)
case EdDSAFamily:
return dispatchEdDSAVerify(key, info, payload, signature)
default:
return fmt.Errorf(`dsig.Verify: unsupported signature family %q`, info.Family)
}
}
func dispatchHMACVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
meta, ok := info.Meta.(HMACFamilyMeta)
if !ok {
return fmt.Errorf(`dsig.Verify: invalid HMAC metadata`)
}
var hmackey []byte
if err := toHMACKey(&hmackey, key); err != nil {
return fmt.Errorf(`dsig.Verify: %w`, err)
}
return VerifyHMAC(hmackey, payload, signature, meta.HashFunc)
}
func dispatchRSAVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
meta, ok := info.Meta.(RSAFamilyMeta)
if !ok {
return fmt.Errorf(`dsig.Verify: invalid RSA metadata`)
}
var pubkey *rsa.PublicKey
if cs, ok := key.(crypto.Signer); ok {
cpub := cs.Public()
switch cpub := cpub.(type) {
case rsa.PublicKey:
pubkey = &cpub
case *rsa.PublicKey:
pubkey = cpub
default:
return fmt.Errorf(`dsig.Verify: failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key)
}
} else {
var ok bool
pubkey, ok = key.(*rsa.PublicKey)
if !ok {
return fmt.Errorf(`dsig.Verify: failed to retrieve *rsa.PublicKey out of %T`, key)
}
}
return VerifyRSA(pubkey, payload, signature, meta.Hash, meta.PSS)
}
func dispatchECDSAVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
meta, ok := info.Meta.(ECDSAFamilyMeta)
if !ok {
return fmt.Errorf(`dsig.Verify: invalid ECDSA metadata`)
}
pubkey, cs, isCryptoSigner, err := ecdsaGetVerifierKey(key)
if err != nil {
return fmt.Errorf(`dsig.Verify: %w`, err)
}
if isCryptoSigner {
return VerifyECDSACryptoSigner(cs, payload, signature, meta.Hash)
}
return VerifyECDSA(pubkey, payload, signature, meta.Hash)
}
func dispatchEdDSAVerify(key any, _ AlgorithmInfo, payload, signature []byte) error {
var pubkey ed25519.PublicKey
signer, ok := key.(crypto.Signer)
if ok {
v := signer.Public()
pubkey, ok = v.(ed25519.PublicKey)
if !ok {
return fmt.Errorf(`dsig.Verify: expected crypto.Signer.Public() to return ed25519.PublicKey, but got %T`, v)
}
} else {
var ok bool
pubkey, ok = key.(ed25519.PublicKey)
if !ok {
return fmt.Errorf(`dsig.Verify: failed to retrieve ed25519.PublicKey out of %T`, key)
}
}
return VerifyEdDSA(pubkey, payload, signature)
}
func ecdsaGetVerifierKey(key any) (*ecdsa.PublicKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
switch key.(type) {
case ecdsa.PublicKey, *ecdsa.PublicKey:
// if it's ecdsa.PublicKey, it's more efficient to
// go through the non-crypto.Signer route. Set isCryptoSigner to false
isCryptoSigner = false
}
}
if isCryptoSigner {
return nil, cs, true, nil
}
pubkey, ok := key.(*ecdsa.PublicKey)
if !ok {
return nil, nil, false, fmt.Errorf(`invalid key type %T. *ecdsa.PublicKey is required`, key)
}
return pubkey, nil, false, nil
}
+2
View File
@@ -29,6 +29,7 @@ linters:
- nakedret
- nestif
- nlreturn
- noinlineerr
- nonamedreturns
- paralleltest
- tagliatelle
@@ -37,6 +38,7 @@ linters:
- varnamelen
- wrapcheck
- wsl
- wsl_v5
settings:
govet:
disable:
+4 -1
View File
@@ -1,7 +1,10 @@
Changes
=======
v3.0.0 UNRELEASED
v3.0.1 18 Aug 2025
* Refresh() no longer requires the resource to be ready.
v3.0.0 5 Jun 2025
[Breaking Changes]
* The entire API has been re-imagined for Go versions that allow typed parameters
+4 -6
View File
@@ -71,6 +71,7 @@ func (c *ctrlBackend) refreshResource(ctx context.Context, req refreshRequest) {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] START %q", req.u))
defer c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] END %q", req.u))
u := req.u
r, ok := c.items[u]
if !ok {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] %s is not registered", req.u))
@@ -78,12 +79,9 @@ func (c *ctrlBackend) refreshResource(ctx context.Context, req refreshRequest) {
return
}
// Make sure it's ready
if err := r.Ready(ctx); err != nil {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: [refresh] %s did not become ready: %v", req.u, err))
sendReply(ctx, req.reply, struct{}{}, err)
return
}
// Note: We don't wait for r.Ready() here because refresh should work
// regardless of whether the resource has been fetched before. This allows
// refresh to work with resources registered using WithWaitReady(false).
r.SetNext(time.Unix(0, 0))
sendWorkerSynchronous(ctx, c.syncoutgoing, synchronousRequest{
+2
View File
@@ -39,10 +39,12 @@ func (w worker) Run(ctx context.Context, readywg *sync.WaitGroup, donewg *sync.W
case sr := <-w.nextsync:
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: syncing %q (synchronous)", sr.resource.URL()))
if err := sr.resource.Sync(ctx); err != nil {
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: FAILED to sync %q (synchronous): %s", sr.resource.URL(), err))
sendReply(ctx, sr.reply, struct{}{}, err)
sr.resource.SetBusy(false)
return
}
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: SUCCESS syncing %q (synchronous)", sr.resource.URL()))
sr.resource.SetBusy(false)
sendReply(ctx, sr.reply, struct{}{}, nil)
w.sendAdjustIntervalRequest(ctx, sr.resource)
+14
View File
@@ -4,6 +4,20 @@ Changes
v3 has many incompatibilities with v2. To see the full list of differences between
v2 and v3, please read the Changes-v3.md file (https://github.com/lestrrat-go/jwx/blob/develop/v3/Changes-v3.md)
v3.0.11 14 Sep 2025
* [jwk] Add `(jwk.Cache).Shutdown()` method that delegates to the httprc controller
object, to shutdown the cache.
* [jwk] Change timing of `res.Body.Close()` call
* [jwe] Previously, ecdh.PrivateKey/ecdh.PublicKey were not properly handled
when used for encryption, which has been fixed.
* [jws/jwsbb] (EXPERIMENTAL/BREAKS COMPATIBILITY) Convert most functions into
thin wrappers around functions from github.com/lestrrat-go/dsig package.
As a related change, HAMCHashFuncFor/RSAHashFuncFor/ECDSAHashFuncFor/RSAPSSOptions
have been removed or unexported.
Users of this module should be using jwsbb.Sign() and jwsbb.Verify() instead of
algorithm specific jwsbb.SignRSA()/jwsbb.VerifyRSA() and such. If you feel the
need to use these functions, you should use github.com/lestrrat-go/dsig directly.
v3.0.10 04 Aug 2025
* [jws/jwsbb] Add `jwsbb.ErrHeaderNotFound()` to return the same error type as when
a non-existent header is requested. via `HeaderGetXXX()` functions. Previously, this
+4 -2
View File
@@ -9,9 +9,9 @@ bazel_dep(name = "rules_go", version = "0.55.1")
bazel_dep(name = "gazelle", version = "0.44.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.11.0")
# Go SDK setup - using Go 1.23.6 to match the toolchain in go.mod
# Go SDK setup - using Go 1.24.4 to match the toolchain in go.mod
go_sdk = use_extension("@rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.6")
go_sdk.download(version = "1.24.4")
# Go dependencies from go.mod
go_deps = use_extension("@gazelle//:extensions.bzl", "go_deps")
@@ -23,6 +23,8 @@ use_repo(
"com_github_decred_dcrd_dcrec_secp256k1_v4",
"com_github_goccy_go_json",
"com_github_lestrrat_go_blackmagic",
"com_github_lestrrat_go_dsig",
"com_github_lestrrat_go_dsig_secp256k1",
"com_github_lestrrat_go_httprc_v3",
"com_github_lestrrat_go_option_v2",
"com_github_segmentio_asm",
+89
View File
@@ -5,8 +5,10 @@ import (
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rsa"
"fmt"
"math/big"
"github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/jwk"
@@ -263,3 +265,90 @@ func ECDHPublicKey(dst, src any) error {
return blackmagic.AssignIfCompatible(dst, pubECDH)
}
// ecdhCurveToElliptic maps ECDH curves to elliptic curves
func ecdhCurveToElliptic(ecdhCurve ecdh.Curve) (elliptic.Curve, error) {
switch ecdhCurve {
case ecdh.P256():
return elliptic.P256(), nil
case ecdh.P384():
return elliptic.P384(), nil
case ecdh.P521():
return elliptic.P521(), nil
default:
return nil, fmt.Errorf(`keyconv: unsupported ECDH curve: %v`, ecdhCurve)
}
}
// ecdhPublicKeyToECDSA converts an ECDH public key to an ECDSA public key
func ecdhPublicKeyToECDSA(ecdhPubKey *ecdh.PublicKey) (*ecdsa.PublicKey, error) {
curve, err := ecdhCurveToElliptic(ecdhPubKey.Curve())
if err != nil {
return nil, err
}
pubBytes := ecdhPubKey.Bytes()
// Parse the uncompressed point format (0x04 prefix + X + Y coordinates)
if len(pubBytes) == 0 || pubBytes[0] != 0x04 {
return nil, fmt.Errorf(`keyconv: invalid ECDH public key format`)
}
keyLen := (len(pubBytes) - 1) / 2
if len(pubBytes) != 1+2*keyLen {
return nil, fmt.Errorf(`keyconv: invalid ECDH public key length`)
}
x := new(big.Int).SetBytes(pubBytes[1 : 1+keyLen])
y := new(big.Int).SetBytes(pubBytes[1+keyLen:])
return &ecdsa.PublicKey{
Curve: curve,
X: x,
Y: y,
}, nil
}
func ECDHToECDSA(dst, src any) error {
// convert ecdh.PublicKey to ecdsa.PublicKey, ecdh.PrivateKey to ecdsa.PrivateKey
// First, handle value types by converting to pointers
switch s := src.(type) {
case ecdh.PrivateKey:
src = &s
case ecdh.PublicKey:
src = &s
}
var privBytes []byte
var pubkey *ecdh.PublicKey
// Now handle the actual conversion with pointer types
switch src := src.(type) {
case *ecdh.PrivateKey:
pubkey = src.PublicKey()
privBytes = src.Bytes()
case *ecdh.PublicKey:
pubkey = src
default:
return fmt.Errorf(`keyconv: expected ecdh.PrivateKey, *ecdh.PrivateKey, ecdh.PublicKey, or *ecdh.PublicKey, got %T`, src)
}
// convert the public key
ecdsaPubKey, err := ecdhPublicKeyToECDSA(pubkey)
if err != nil {
return fmt.Errorf(`keyconv.ECDHToECDSA: failed to convert ECDH public key to ECDSA public key: %w`, err)
}
// return if we were being asked to convert *ecdh.PublicKey
if privBytes == nil {
return blackmagic.AssignIfCompatible(dst, ecdsaPubKey)
}
// Then create the private key with the public key embedded
ecdsaPrivKey := &ecdsa.PrivateKey{
D: new(big.Int).SetBytes(privBytes),
PublicKey: *ecdsaPubKey,
}
return blackmagic.AssignIfCompatible(dst, ecdsaPrivKey)
}
+33 -8
View File
@@ -96,23 +96,48 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
keyToUse = e.pubkey
}
// Handle ecdsa.PublicKey by value - convert to pointer
if pk, ok := keyToUse.(ecdsa.PublicKey); ok {
keyToUse = &pk
switch key := keyToUse.(type) {
case *ecdsa.PublicKey:
// no op
case ecdsa.PublicKey:
keyToUse = &key
case *ecdsa.PrivateKey:
keyToUse = &key.PublicKey
case ecdsa.PrivateKey:
keyToUse = &key.PublicKey
case *ecdh.PublicKey:
// no op
case ecdh.PublicKey:
keyToUse = &key
case ecdh.PrivateKey:
keyToUse = key.PublicKey()
case *ecdh.PrivateKey:
keyToUse = key.PublicKey()
}
// Determine key type and call appropriate function
switch key := keyToUse.(type) {
case *ecdh.PublicKey:
if key.Curve() == ecdh.X25519() {
if !keywrap {
return jwebb.KeyEncryptECDHESX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
}
return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
}
var ecdsaKey *ecdsa.PublicKey
if err := keyconv.ECDHToECDSA(&ecdsaKey, key); err != nil {
return nil, fmt.Errorf(`encrypt: failed to convert ECDH public key to ECDSA: %w`, err)
}
keyToUse = ecdsaKey
}
switch key := keyToUse.(type) {
case *ecdsa.PublicKey:
if !keywrap {
return jwebb.KeyEncryptECDHESECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
}
return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
case *ecdh.PublicKey:
if !keywrap {
return jwebb.KeyEncryptECDHESX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
}
return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
default:
return nil, fmt.Errorf(`encrypt: unsupported key type for ECDH-ES: %T`, keyToUse)
}
+4
View File
@@ -223,6 +223,10 @@ func (c *Cache) Unregister(ctx context.Context, u string) error {
return c.ctrl.Remove(ctx, u)
}
func (c *Cache) Shutdown(ctx context.Context) error {
return c.ctrl.ShutdownContext(ctx)
}
// CachedSet is a thin shim over jwk.Cache that allows the user to cloak
// jwk.Cache as if it's a `jwk.Set`. Behind the scenes, the `jwk.Set` is
// retrieved from the `jwk.Cache` for every operation.
+1 -1
View File
@@ -102,13 +102,13 @@ func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
if err != nil {
return nil, fmt.Errorf(`jwk.Fetch: request failed: %w`, err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf(`jwk.Fetch: request returned status %d, expected 200`, res.StatusCode)
}
buf, err := io.ReadAll(res.Body)
defer res.Body.Close()
if err != nil {
return nil, fmt.Errorf(`jwk.Fetch: failed to read response body for %q: %w`, u, err)
}
+1
View File
@@ -8,5 +8,6 @@ import (
)
func init() {
// Register ES256K to EC algorithm family
addAlgorithmForKeyType(jwa.EC(), jwa.ES256K())
}
+4
View File
@@ -19,6 +19,10 @@
// To verify, use `jws.Verify`. It will parse the `encodedjws` buffer
// and verify the result using `algorithm` and `key`. Upon successful
// verification, the original payload is returned, so you can work on it.
//
// As a sidenote, consider using github.com/lestrrat-go/htmsig if you
// looking for HTTP Message Signatures (RFC9421) -- it uses the same
// underlying signing/verification mechanisms as this module.
package jws
import (
+1
View File
@@ -23,6 +23,7 @@ go_library(
"//internal/pool",
"//internal/tokens",
"//jws/internal/keytype",
"@com_github_lestrrat_go_dsig//:dsig",
],
)
+25 -62
View File
@@ -3,59 +3,27 @@ package jwsbb
import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"encoding/asn1"
"fmt"
"io"
"math/big"
"github.com/lestrrat-go/dsig"
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
)
var ecdsaHashFuncs = map[string]crypto.Hash{
"ES256": crypto.SHA256,
"ES256K": crypto.SHA256,
"ES384": crypto.SHA384,
"ES512": crypto.SHA512,
}
func isSuppotedECDSAAlgorithm(alg string) bool {
_, ok := ecdsaHashFuncs[alg]
return ok
}
func ECDSAHashFuncFor(alg string) (crypto.Hash, error) {
if h, ok := ecdsaHashFuncs[alg]; ok {
return h, nil
// ecdsaHashToDsigAlgorithm maps ECDSA hash functions to dsig algorithm constants
func ecdsaHashToDsigAlgorithm(h crypto.Hash) (string, error) {
switch h {
case crypto.SHA256:
return dsig.ECDSAWithP256AndSHA256, nil
case crypto.SHA384:
return dsig.ECDSAWithP384AndSHA384, nil
case crypto.SHA512:
return dsig.ECDSAWithP521AndSHA512, nil
default:
return "", fmt.Errorf("unsupported ECDSA hash function: %v", h)
}
return 0, fmt.Errorf(`unsupported ECDSA algorithm %s`, alg)
}
func ecdsaGetSignerKey(key any) (*ecdsa.PrivateKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
if !keytype.IsValidECDSAKey(key) {
return nil, nil, false, fmt.Errorf(`cannot use key of type %T`, key)
}
switch key.(type) {
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
// if it's ecdsa.PrivateKey, it's more efficient to
// go through the non-crypto.Signer route. Set isCryptoSigner to false
isCryptoSigner = false
}
}
if isCryptoSigner {
return nil, cs, true, nil
}
var privkey *ecdsa.PrivateKey
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
return nil, nil, false, fmt.Errorf(`invalid key type %T. ecdsa.PrivateKey is required: %w`, key, err)
}
return privkey, nil, false, nil
}
// UnpackASN1ECDSASignature unpacks an ASN.1 encoded ECDSA signature into r and s values.
@@ -120,24 +88,16 @@ func PackECDSASignature(r *big.Int, sbig *big.Int, curveBits int) ([]byte, error
// The raw parameter should be the pre-computed signing input (typically header.payload).
//
// rr is an io.Reader that provides randomness for signing. if rr is nil, it defaults to rand.Reader.
//
// This function is now a thin wrapper around dsig.SignECDSA. For new projects, you should
// consider using dsig instead of this function.
func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reader) ([]byte, error) {
hh := h.New()
if _, err := hh.Write(payload); err != nil {
return nil, fmt.Errorf(`failed to write payload using ecdsa: %w`, err)
}
digest := hh.Sum(nil)
if rr == nil {
rr = rand.Reader
}
// Sign and get r, s values
r, s, err := ecdsa.Sign(rr, key, digest)
dsigAlg, err := ecdsaHashToDsigAlgorithm(h)
if err != nil {
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
return nil, fmt.Errorf("jwsbb.SignECDSA: %w", err)
}
return PackECDSASignature(r, s, key.Curve.Params().BitSize)
return dsig.Sign(key, dsigAlg, payload, rr)
}
// SignECDSACryptoSigner generates an ECDSA signature using a crypto.Signer interface.
@@ -183,13 +143,16 @@ func ecdsaVerify(key *ecdsa.PublicKey, buf []byte, h crypto.Hash, r, s *big.Int)
// VerifyECDSA verifies an ECDSA signature for the given payload.
// This function verifies the signature using the specified public key and hash algorithm.
// The payload parameter should be the pre-computed signing input (typically header.payload).
//
// This function is now a thin wrapper around dsig.VerifyECDSA. For new projects, you should
// consider using dsig instead of this function.
func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
var r, s big.Int
if err := UnpackECDSASignature(signature, key, &r, &s); err != nil {
return fmt.Errorf("jwsbb.ECDSAVerifier: failed to unpack ECDSA signature: %w", err)
dsigAlg, err := ecdsaHashToDsigAlgorithm(h)
if err != nil {
return fmt.Errorf("jwsbb.VerifyECDSA: %w", err)
}
return ecdsaVerify(key, payload, h, &r, &s)
return dsig.Verify(key, dsigAlg, payload, signature)
}
// VerifyECDSACryptoSigner verifies an ECDSA signature for crypto.Signer implementations.
+11 -33
View File
@@ -1,52 +1,30 @@
package jwsbb
import (
"crypto"
"crypto/ed25519"
"fmt"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
"github.com/lestrrat-go/dsig"
)
func isSupportedEdDSAAlgorithm(alg string) bool {
return alg == "EdDSA"
}
func eddsaGetSigner(key any) (crypto.Signer, error) {
// The ed25519.PrivateKey object implements crypto.Signer, so we should
// simply accept a crypto.Signer here.
signer, ok := key.(crypto.Signer)
if ok {
if !keytype.IsValidEDDSAKey(key) {
return nil, fmt.Errorf(`cannot use key of type %T to generate EdDSA based signatures`, key)
}
return signer, nil
}
// This fallback exists for cases when jwk.Key was passed, or
// users gave us a pointer instead of non-pointer, etc.
var privkey ed25519.PrivateKey
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
return nil, fmt.Errorf(`failed to retrieve ed25519.PrivateKey out of %T: %w`, key, err)
}
return privkey, nil
}
// SignEdDSA generates an EdDSA (Ed25519) signature for the given payload.
// The raw parameter should be the pre-computed signing input (typically header.payload).
// EdDSA is deterministic and doesn't require additional hashing of the input.
//
// This function is now a thin wrapper around dsig.SignEdDSA. For new projects, you should
// consider using dsig instead of this function.
func SignEdDSA(key ed25519.PrivateKey, payload []byte) ([]byte, error) {
return ed25519.Sign(key, payload), nil
// Use dsig.Sign with EdDSA algorithm constant
return dsig.Sign(key, dsig.EdDSA, payload, nil)
}
// VerifyEdDSA verifies an EdDSA (Ed25519) signature for the given payload.
// This function verifies the signature using Ed25519 verification algorithm.
// The payload parameter should be the pre-computed signing input (typically header.payload).
// EdDSA is deterministic and provides strong security guarantees without requiring hash function selection.
//
// This function is now a thin wrapper around dsig.VerifyEdDSA. For new projects, you should
// consider using dsig instead of this function.
func VerifyEdDSA(key ed25519.PublicKey, payload, signature []byte) error {
if !ed25519.Verify(key, payload, signature) {
return fmt.Errorf("invalid EdDSA signature")
}
return nil
// Use dsig.Verify with EdDSA algorithm constant
return dsig.Verify(key, dsig.EdDSA, payload, signature)
}
+14
View File
@@ -0,0 +1,14 @@
//go:build jwx_es256k
package jwsbb
import (
dsigsecp256k1 "github.com/lestrrat-go/dsig-secp256k1"
)
const es256k = "ES256K"
func init() {
// Add ES256K mapping when this build tag is enabled
jwsToDsigAlgorithm[es256k] = dsigsecp256k1.ECDSAWithSecp256k1AndSHA256
}
+28 -43
View File
@@ -1,67 +1,52 @@
package jwsbb
import (
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"fmt"
"hash"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
"github.com/lestrrat-go/dsig"
)
var hmacHashFuncs = map[string]func() hash.Hash{
"HS256": sha256.New,
"HS384": sha512.New384,
"HS512": sha512.New,
}
func isSupportedHMACAlgorithm(alg string) bool {
_, ok := hmacHashFuncs[alg]
return ok
}
// HMACHashFuncFor returns the appropriate hash function for the given HMAC algorithm.
// Supported algorithms: HS256 (SHA-256), HS384 (SHA-384), HS512 (SHA-512).
// Returns the hash function constructor and an error if the algorithm is unsupported.
func HMACHashFuncFor(alg string) (func() hash.Hash, error) {
if h, ok := hmacHashFuncs[alg]; ok {
return h, nil
// hmacHashToDsigAlgorithm maps HMAC hash function sizes to dsig algorithm constants
func hmacHashToDsigAlgorithm(hfunc func() hash.Hash) (string, error) {
h := hfunc()
switch h.Size() {
case 32: // SHA256
return dsig.HMACWithSHA256, nil
case 48: // SHA384
return dsig.HMACWithSHA384, nil
case 64: // SHA512
return dsig.HMACWithSHA512, nil
default:
return "", fmt.Errorf("unsupported HMAC hash function: size=%d", h.Size())
}
return nil, fmt.Errorf("unsupported HMAC algorithm %s", alg)
}
func toHMACKey(dst *[]byte, key any) error {
if err := keyconv.ByteSliceKey(dst, key); err != nil {
return fmt.Errorf(`jws.toHMACKey: invalid key type %T. []byte is required: %w`, key, err)
}
if len(*dst) == 0 {
return fmt.Errorf(`jws.toHMACKey: missing key while signing payload`)
}
return nil
}
// SignHMAC generates an HMAC signature for the given payload using the specified hash function and key.
// The raw parameter should be the pre-computed signing input (typically header.payload).
//
// This function is now a thin wrapper around dsig.SignHMAC. For new projects, you should
// consider using dsig instead of this function.
func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) {
h := hmac.New(hfunc, key)
if _, err := h.Write(payload); err != nil {
return nil, fmt.Errorf(`failed to write payload using hmac: %w`, err)
dsigAlg, err := hmacHashToDsigAlgorithm(hfunc)
if err != nil {
return nil, fmt.Errorf("jwsbb.SignHMAC: %w", err)
}
return h.Sum(nil), nil
return dsig.Sign(key, dsigAlg, payload, nil)
}
// VerifyHMAC verifies an HMAC signature for the given payload.
// This function verifies the signature using the specified key and hash function.
// The payload parameter should be the pre-computed signing input (typically header.payload).
//
// This function is now a thin wrapper around dsig.VerifyHMAC. For new projects, you should
// consider using dsig instead of this function.
func VerifyHMAC(key, payload, signature []byte, hfunc func() hash.Hash) error {
expected, err := SignHMAC(key, payload, hfunc)
dsigAlg, err := hmacHashToDsigAlgorithm(hfunc)
if err != nil {
return fmt.Errorf("failed to sign payload for verification: %w", err)
return fmt.Errorf("jwsbb.VerifyHMAC: %w", err)
}
if !hmac.Equal(signature, expected) {
return fmt.Errorf("invalid HMAC signature")
}
return nil
return dsig.Verify(key, dsigAlg, payload, signature)
}
+65
View File
@@ -12,8 +12,40 @@
// 1. Does minimal checking of input parameters (for performance); callers need to ensure that the parameters are valid.
// 2. All exported functions are strongly typed (i.e. they do not take `any` types unless they absolutely have to).
// 3. Does not rely on other public jwx packages (they are standalone, except for internal packages).
//
// This implementation uses github.com/lestrrat-go/dsig as the underlying signature provider.
package jwsbb
import (
"github.com/lestrrat-go/dsig"
)
// JWS algorithm name constants
const (
// HMAC algorithms
hs256 = "HS256"
hs384 = "HS384"
hs512 = "HS512"
// RSA PKCS#1 v1.5 algorithms
rs256 = "RS256"
rs384 = "RS384"
rs512 = "RS512"
// RSA PSS algorithms
ps256 = "PS256"
ps384 = "PS384"
ps512 = "PS512"
// ECDSA algorithms
es256 = "ES256"
es384 = "ES384"
es512 = "ES512"
// EdDSA algorithm
edDSA = "EdDSA"
)
// Signer is a generic interface that defines the method for signing payloads.
// The type parameter K represents the key type (e.g., []byte for HMAC keys,
// *rsa.PrivateKey for RSA keys, *ecdsa.PrivateKey for ECDSA keys).
@@ -27,3 +59,36 @@ type Signer[K any] interface {
type Verifier[K any] interface {
Verify(key K, buf []byte, signature []byte) error
}
// JWS to dsig algorithm mapping
var jwsToDsigAlgorithm = map[string]string{
// HMAC algorithms
hs256: dsig.HMACWithSHA256,
hs384: dsig.HMACWithSHA384,
hs512: dsig.HMACWithSHA512,
// RSA PKCS#1 v1.5 algorithms
rs256: dsig.RSAPKCS1v15WithSHA256,
rs384: dsig.RSAPKCS1v15WithSHA384,
rs512: dsig.RSAPKCS1v15WithSHA512,
// RSA PSS algorithms
ps256: dsig.RSAPSSWithSHA256,
ps384: dsig.RSAPSSWithSHA384,
ps512: dsig.RSAPSSWithSHA512,
// ECDSA algorithms
es256: dsig.ECDSAWithP256AndSHA256,
es384: dsig.ECDSAWithP384AndSHA384,
es512: dsig.ECDSAWithP521AndSHA512,
// Note: ES256K requires external dependency and is handled separately
// EdDSA algorithm
edDSA: dsig.EdDSA,
}
// getDsigAlgorithm returns the dsig algorithm name for a JWS algorithm
func getDsigAlgorithm(jwsAlg string) (string, bool) {
dsigAlg, ok := jwsToDsigAlgorithm[jwsAlg]
return dsigAlg, ok
}
+40 -55
View File
@@ -6,53 +6,33 @@ import (
"fmt"
"io"
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
"github.com/lestrrat-go/dsig"
)
func rsaGetSignerCryptoSignerKey(key any) (crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
if !keytype.IsValidRSAKey(key) {
return nil, false, fmt.Errorf(`cannot use key of type %T`, key)
// rsaHashToDsigAlgorithm maps RSA hash functions to dsig algorithm constants
func rsaHashToDsigAlgorithm(h crypto.Hash, pss bool) (string, error) {
if pss {
switch h {
case crypto.SHA256:
return dsig.RSAPSSWithSHA256, nil
case crypto.SHA384:
return dsig.RSAPSSWithSHA384, nil
case crypto.SHA512:
return dsig.RSAPSSWithSHA512, nil
default:
return "", fmt.Errorf("unsupported hash algorithm for RSA-PSS: %v", h)
}
} else {
switch h {
case crypto.SHA256:
return dsig.RSAPKCS1v15WithSHA256, nil
case crypto.SHA384:
return dsig.RSAPKCS1v15WithSHA384, nil
case crypto.SHA512:
return dsig.RSAPKCS1v15WithSHA512, nil
default:
return "", fmt.Errorf("unsupported hash algorithm for RSA PKCS#1 v1.5: %v", h)
}
return cs, true, nil
}
return nil, false, nil
}
var rsaHashFuncs = map[string]struct {
Hash crypto.Hash
PSS bool // whether to use PSS padding
}{
"RS256": {Hash: crypto.SHA256, PSS: false},
"RS384": {Hash: crypto.SHA384, PSS: false},
"RS512": {Hash: crypto.SHA512, PSS: false},
"PS256": {Hash: crypto.SHA256, PSS: true},
"PS384": {Hash: crypto.SHA384, PSS: true},
"PS512": {Hash: crypto.SHA512, PSS: true},
}
func isSuppotedRSAAlgorithm(alg string) bool {
_, ok := rsaHashFuncs[alg]
return ok
}
// RSAHashFuncFor returns the appropriate hash function and PSS flag for the given RSA algorithm.
// Supported algorithms: RS256, RS384, RS512 (PKCS#1 v1.5) and PS256, PS384, PS512 (PSS).
// Returns the hash function, PSS flag, and an error if the algorithm is unsupported.
func RSAHashFuncFor(alg string) (crypto.Hash, bool, error) {
if h, ok := rsaHashFuncs[alg]; ok {
return h.Hash, h.PSS, nil
}
return 0, false, fmt.Errorf("unsupported RSA algorithm %s", alg)
}
// RSAPSSOptions returns the PSS options for RSA-PSS signatures with the specified hash.
// The salt length is set to equal the hash length as per RFC 7518.
func RSAPSSOptions(h crypto.Hash) rsa.PSSOptions {
return rsa.PSSOptions{
Hash: h,
SaltLength: rsa.PSSSaltLengthEqualsHash,
}
}
@@ -62,25 +42,30 @@ func RSAPSSOptions(h crypto.Hash) rsa.PSSOptions {
//
// The rr parameter is an optional io.Reader that can be used to provide randomness for signing.
// If rr is nil, it defaults to rand.Reader.
//
// This function is now a thin wrapper around dsig.SignRSA. For new projects, you should
// consider using dsig instead of this function.
func SignRSA(key *rsa.PrivateKey, payload []byte, h crypto.Hash, pss bool, rr io.Reader) ([]byte, error) {
var opts crypto.SignerOpts = h
if pss {
rsaopts := RSAPSSOptions(h)
opts = &rsaopts
dsigAlg, err := rsaHashToDsigAlgorithm(h, pss)
if err != nil {
return nil, fmt.Errorf("jwsbb.SignRSA: %w", err)
}
return cryptosign(key, payload, h, opts, rr)
return dsig.Sign(key, dsigAlg, payload, rr)
}
// VerifyRSA verifies an RSA signature for the given payload and header.
// This function constructs the signing input by encoding the header and payload according to JWS specification,
// then verifies the signature using the specified public key and hash algorithm.
// If pss is true, RSA-PSS verification is used; otherwise, PKCS#1 v1.5 verification is used.
//
// This function is now a thin wrapper around dsig.VerifyRSA. For new projects, you should
// consider using dsig instead of this function.
func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss bool) error {
hasher := h.New()
hasher.Write(payload)
digest := hasher.Sum(nil)
if pss {
return rsa.VerifyPSS(key, h, digest, signature, &rsa.PSSOptions{Hash: h, SaltLength: rsa.PSSSaltLengthEqualsHash})
dsigAlg, err := rsaHashToDsigAlgorithm(h, pss)
if err != nil {
return fmt.Errorf("jwsbb.VerifyRSA: %w", err)
}
return rsa.VerifyPKCS1v15(key, h, digest, signature)
return dsig.Verify(key, dsigAlg, payload, signature)
}
+71 -56
View File
@@ -2,94 +2,109 @@ package jwsbb
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"fmt"
"io"
"github.com/lestrrat-go/dsig"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
)
// Sign generates a JWS signature using the specified key and algorithm.
//
// This function loads the signer registered in the hwsbb package _ONLY_.
// This function loads the signer registered in the jwsbb package _ONLY_.
// It does not support custom signers that the user might have registered.
//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
// Not all algorithms require this parameter, but it is included for consistency.
// 99% of the time, you can pass nil for rr, and it will work fine.
func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
switch {
case isSupportedHMACAlgorithm(alg):
return dispatchHMACSign(key, alg, payload)
case isSuppotedRSAAlgorithm(alg):
return dispatchRSASign(key, alg, payload, rr)
case isSuppotedECDSAAlgorithm(alg):
return dispatchECDSASign(key, alg, payload, rr)
case isSupportedEdDSAAlgorithm(alg):
return dispatchEdDSASign(key, alg, payload, rr)
dsigAlg, ok := getDsigAlgorithm(alg)
if !ok {
return nil, fmt.Errorf(`jwsbb.Sign: unsupported signature algorithm %q`, alg)
}
return nil, fmt.Errorf(`jwsbb.Sign: unsupported signature algorithm %q`, alg)
// Get dsig algorithm info to determine key conversion strategy
dsigInfo, ok := dsig.GetAlgorithmInfo(dsigAlg)
if !ok {
return nil, fmt.Errorf(`jwsbb.Sign: dsig algorithm %q not registered`, dsigAlg)
}
switch dsigInfo.Family {
case dsig.HMAC:
return dispatchHMACSign(key, dsigAlg, payload)
case dsig.RSA:
return dispatchRSASign(key, dsigAlg, payload, rr)
case dsig.ECDSA:
return dispatchECDSASign(key, dsigAlg, payload, rr)
case dsig.EdDSAFamily:
return dispatchEdDSASign(key, dsigAlg, payload, rr)
default:
return nil, fmt.Errorf(`jwsbb.Sign: unsupported dsig algorithm family %q`, dsigInfo.Family)
}
}
func dispatchHMACSign(key any, alg string, payload []byte) ([]byte, error) {
h, err := HMACHashFuncFor(alg)
if err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: failed to get hash function for %s: %w`, alg, err)
}
func dispatchHMACSign(key any, dsigAlg string, payload []byte) ([]byte, error) {
var hmackey []byte
if err := toHMACKey(&hmackey, key); err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: %w`, err)
if err := keyconv.ByteSliceKey(&hmackey, key); err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. []byte is required: %w`, key, err)
}
return SignHMAC(hmackey, payload, h)
return dsig.Sign(hmackey, dsigAlg, payload, nil)
}
func dispatchRSASign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
h, pss, err := RSAHashFuncFor(alg)
if err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: failed to get hash function for %s: %w`, alg, err)
}
cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key)
if err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: %w`, err)
}
if isCryptoSigner {
var options crypto.SignerOpts = h
if pss {
rsaopts := RSAPSSOptions(h)
options = &rsaopts
func dispatchRSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an RSA key
if _, ok := signer.Public().(*rsa.PublicKey); ok {
return dsig.Sign(signer, dsigAlg, payload, rr)
}
return SignCryptoSigner(cs, payload, h, options, rr)
}
// Fall back to concrete key types
var privkey *rsa.PrivateKey
if err := keyconv.RSAPrivateKey(&privkey, key); err != nil {
return nil, fmt.Errorf(`jws.RSASigner: invalid key type %T. rsa.PrivateKey is required: %w`, key, err)
}
return SignRSA(privkey, payload, h, pss, rr)
}
func dispatchEdDSASign(key any, _ string, payload []byte, rr io.Reader) ([]byte, error) {
signer, err := eddsaGetSigner(key)
if err != nil {
return nil, fmt.Errorf(`jws.EdDSASigner: %w`, err)
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. *rsa.PrivateKey is required: %w`, key, err)
}
return SignCryptoSigner(signer, payload, crypto.Hash(0), crypto.Hash(0), rr)
return dsig.Sign(privkey, dsigAlg, payload, rr)
}
func dispatchECDSASign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
h, err := ECDSAHashFuncFor(alg)
if err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: failed to get hash function for %s: %w`, alg, err)
func dispatchECDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an ECDSA key
if _, ok := signer.Public().(*ecdsa.PublicKey); ok {
return dsig.Sign(signer, dsigAlg, payload, rr)
}
}
privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key)
if err != nil {
return nil, fmt.Errorf(`jws.ECDSASigner: %w`, err)
// Fall back to concrete key types
var privkey *ecdsa.PrivateKey
if err := keyconv.ECDSAPrivateKey(&privkey, key); err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. *ecdsa.PrivateKey is required: %w`, key, err)
}
if isCryptoSigner {
return SignECDSACryptoSigner(cs, payload, h, rr)
}
return SignECDSA(privkey, payload, h, rr)
return dsig.Sign(privkey, dsigAlg, payload, rr)
}
func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an EdDSA key
if _, ok := signer.Public().(ed25519.PublicKey); ok {
return dsig.Sign(signer, dsigAlg, payload, rr)
}
}
// Fall back to concrete key types
var privkey ed25519.PrivateKey
if err := keyconv.Ed25519PrivateKey(&privkey, key); err != nil {
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. ed25519.PrivateKey is required: %w`, key, err)
}
return dsig.Sign(privkey, dsigAlg, payload, rr)
}
+65 -87
View File
@@ -7,6 +7,7 @@ import (
"crypto/rsa"
"fmt"
"github.com/lestrrat-go/dsig"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
)
@@ -15,113 +16,90 @@ import (
// This function loads the verifier registered in the jwsbb package _ONLY_.
// It does not support custom verifiers that the user might have registered.
func Verify(key any, alg string, payload, signature []byte) error {
switch {
case isSupportedHMACAlgorithm(alg):
return dispatchHMACVerify(key, alg, payload, signature)
case isSuppotedRSAAlgorithm(alg):
return dispatchRSAVerify(key, alg, payload, signature)
case isSuppotedECDSAAlgorithm(alg):
return dispatchECDSAVerify(key, alg, payload, signature)
case isSupportedEdDSAAlgorithm(alg):
return dispatchEdDSAVerify(key, alg, payload, signature)
dsigAlg, ok := getDsigAlgorithm(alg)
if !ok {
return fmt.Errorf(`jwsbb.Verify: unsupported signature algorithm %q`, alg)
}
return fmt.Errorf(`jwsbb.Verify: unsupported signature algorithm %q`, alg)
// Get dsig algorithm info to determine key conversion strategy
dsigInfo, ok := dsig.GetAlgorithmInfo(dsigAlg)
if !ok {
return fmt.Errorf(`jwsbb.Verify: dsig algorithm %q not registered`, dsigAlg)
}
switch dsigInfo.Family {
case dsig.HMAC:
return dispatchHMACVerify(key, dsigAlg, payload, signature)
case dsig.RSA:
return dispatchRSAVerify(key, dsigAlg, payload, signature)
case dsig.ECDSA:
return dispatchECDSAVerify(key, dsigAlg, payload, signature)
case dsig.EdDSAFamily:
return dispatchEdDSAVerify(key, dsigAlg, payload, signature)
default:
return fmt.Errorf(`jwsbb.Verify: unsupported dsig algorithm family %q`, dsigInfo.Family)
}
}
func dispatchHMACVerify(key any, alg string, payload, signature []byte) error {
h, err := HMACHashFuncFor(alg)
if err != nil {
return fmt.Errorf(`jwsbb.Verify: failed to get hash function for %s: %w`, alg, err)
}
func dispatchHMACVerify(key any, dsigAlg string, payload, signature []byte) error {
var hmackey []byte
if err := toHMACKey(&hmackey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: %w`, err)
if err := keyconv.ByteSliceKey(&hmackey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. []byte is required: %w`, key, err)
}
return VerifyHMAC(hmackey, payload, signature, h)
return dsig.Verify(hmackey, dsigAlg, payload, signature)
}
func dispatchRSAVerify(key any, alg string, payload, signature []byte) error {
h, pss, err := RSAHashFuncFor(alg)
if err != nil {
return fmt.Errorf(`jwsbb.Verify: failed to get hash function for %s: %w`, alg, err)
func dispatchRSAVerify(key any, dsigAlg string, payload, signature []byte) error {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an RSA key
if _, ok := signer.Public().(*rsa.PublicKey); ok {
return dsig.Verify(signer, dsigAlg, payload, signature)
}
}
// Fall back to concrete key types
var pubkey *rsa.PublicKey
if cs, ok := key.(crypto.Signer); ok {
cpub := cs.Public()
switch cpub := cpub.(type) {
case rsa.PublicKey:
pubkey = &cpub
case *rsa.PublicKey:
pubkey = cpub
default:
return fmt.Errorf(`jwsbb.Verify: failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key)
}
} else {
if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: failed to retrieve rsa.PublicKey out of %T: %w`, key, err)
}
if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. *rsa.PublicKey is required: %w`, key, err)
}
return VerifyRSA(pubkey, payload, signature, h, pss)
return dsig.Verify(pubkey, dsigAlg, payload, signature)
}
func dispatchECDSAVerify(key any, alg string, payload, signature []byte) error {
h, err := ECDSAHashFuncFor(alg)
if err != nil {
return fmt.Errorf(`jwsbb.Verify: failed to get hash function for %s: %w`, alg, err)
}
pubkey, cs, isCryptoSigner, err := ecdsaGetVerifierKey(key)
if err != nil {
return fmt.Errorf(`jwsbb.Verify: %w`, err)
}
if isCryptoSigner {
return VerifyECDSACryptoSigner(cs, payload, signature, h)
}
return VerifyECDSA(pubkey, payload, signature, h)
}
func dispatchEdDSAVerify(key any, _ string, payload, signature []byte) error {
var pubkey ed25519.PublicKey
signer, ok := key.(crypto.Signer)
if ok {
v := signer.Public()
pubkey, ok = v.(ed25519.PublicKey)
if !ok {
return fmt.Errorf(`jwsbb.Verify: expected crypto.Signer.Public() to return ed25519.PublicKey, but got %T`, v)
}
} else {
if err := keyconv.Ed25519PublicKey(&pubkey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: failed to retrieve ed25519.PublicKey out of %T: %w`, key, err)
func dispatchECDSAVerify(key any, dsigAlg string, payload, signature []byte) error {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an ECDSA key
if _, ok := signer.Public().(*ecdsa.PublicKey); ok {
return dsig.Verify(signer, dsigAlg, payload, signature)
}
}
return VerifyEdDSA(pubkey, payload, signature)
}
func ecdsaGetVerifierKey(key any) (*ecdsa.PublicKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
switch key.(type) {
case ecdsa.PublicKey, *ecdsa.PublicKey:
// if it's ecdsa.PublicKey, it's more efficient to
// go through the non-crypto.Signer route. Set isCryptoSigner to false
isCryptoSigner = false
}
}
if isCryptoSigner {
return nil, cs, true, nil
}
// Fall back to concrete key types
var pubkey *ecdsa.PublicKey
if err := keyconv.ECDSAPublicKey(&pubkey, key); err != nil {
return nil, nil, false, fmt.Errorf(`invalid key type %T. ecdsa.PublicKey is required: %w`, key, err)
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. *ecdsa.PublicKey is required: %w`, key, err)
}
return pubkey, nil, false, nil
return dsig.Verify(pubkey, dsigAlg, payload, signature)
}
func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) error {
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an EdDSA key
if _, ok := signer.Public().(ed25519.PublicKey); ok {
return dsig.Verify(signer, dsigAlg, payload, signature)
}
}
// Fall back to concrete key types
var pubkey ed25519.PublicKey
if err := keyconv.Ed25519PublicKey(&pubkey, key); err != nil {
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. ed25519.PublicKey is required: %w`, key, err)
}
return dsig.Verify(pubkey, dsigAlg, payload, signature)
}
+2 -1
View File
@@ -95,7 +95,8 @@ func (v2) NewDesc(fqName, help string, variableLabels ConstrainableLabels, const
help: help,
variableLabels: variableLabels.compile(),
}
if !model.IsValidMetricName(model.LabelValue(fqName)) {
//nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme.
if !model.NameValidationScheme.IsValidMetricName(fqName) {
d.err = fmt.Errorf("%q is not a valid metric name", fqName)
return d
}
@@ -67,7 +67,7 @@ func RuntimeMetricsToProm(d *metrics.Description) (string, string, string, bool)
}
// Our current conversion moves to legacy naming, so use legacy validation.
valid := model.IsValidLegacyMetricName(namespace + "_" + subsystem + "_" + name)
valid := model.LegacyValidation.IsValidMetricName(namespace + "_" + subsystem + "_" + name)
switch d.Kind {
case metrics.KindUint64:
case metrics.KindFloat64:
+2 -1
View File
@@ -184,5 +184,6 @@ func validateLabelValues(vals []string, expectedNumberOfValues int) error {
}
func checkLabelName(l string) bool {
return model.LabelName(l).IsValid() && !strings.HasPrefix(l, reservedLabelPrefix)
//nolint:staticcheck // TODO: Don't use deprecated model.NameValidationScheme.
return model.NameValidationScheme.IsValidLabelName(l) && !strings.HasPrefix(l, reservedLabelPrefix)
}
@@ -262,7 +262,7 @@ func CollectAndFormat(c prometheus.Collector, format expfmt.FormatType, metricNa
// convertReaderToMetricFamily would read from a io.Reader object and convert it to a slice of
// dto.MetricFamily.
func convertReaderToMetricFamily(reader io.Reader) ([]*dto.MetricFamily, error) {
var tp expfmt.TextParser
tp := expfmt.NewTextParser(model.UTF8Validation)
notNormalized, err := tp.TextToMetricFamilies(reader)
if err != nil {
return nil, fmt.Errorf("converting reader to metric families failed: %w", err)
+32 -7
View File
@@ -70,19 +70,34 @@ func ResponseFormat(h http.Header) Format {
return FmtUnknown
}
// NewDecoder returns a new decoder based on the given input format.
// If the input format does not imply otherwise, a text format decoder is returned.
// NewDecoder returns a new decoder based on the given input format. Metric
// names are validated based on the provided Format -- if the format requires
// escaping, raditional Prometheues validity checking is used. Otherwise, names
// are checked for UTF-8 validity. Supported formats include delimited protobuf
// and Prometheus text format. For historical reasons, this decoder fallbacks
// to classic text decoding for any other format. This decoder does not fully
// support OpenMetrics although it may often succeed due to the similarities
// between the formats. This decoder may not support the latest features of
// Prometheus text format and is not intended for high-performance applications.
// See: https://github.com/prometheus/common/issues/812
func NewDecoder(r io.Reader, format Format) Decoder {
scheme := model.LegacyValidation
if format.ToEscapingScheme() == model.NoEscaping {
scheme = model.UTF8Validation
}
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{r: bufio.NewReader(r)}
return &protoDecoder{r: bufio.NewReader(r), s: scheme}
case TypeProtoText, TypeProtoCompact:
return &errDecoder{err: fmt.Errorf("format %s not supported for decoding", format)}
}
return &textDecoder{r: r}
return &textDecoder{r: r, s: scheme}
}
// protoDecoder implements the Decoder interface for protocol buffers.
type protoDecoder struct {
r protodelim.Reader
s model.ValidationScheme
}
// Decode implements the Decoder interface.
@@ -93,7 +108,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
if err := opts.UnmarshalFrom(d.r, v); err != nil {
return err
}
if !model.IsValidMetricName(model.LabelValue(v.GetName())) {
if !d.s.IsValidMetricName(v.GetName()) {
return fmt.Errorf("invalid metric name %q", v.GetName())
}
for _, m := range v.GetMetric() {
@@ -107,7 +122,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
if !model.LabelValue(l.GetValue()).IsValid() {
return fmt.Errorf("invalid label value %q", l.GetValue())
}
if !model.LabelName(l.GetName()).IsValid() {
if !d.s.IsValidLabelName(l.GetName()) {
return fmt.Errorf("invalid label name %q", l.GetName())
}
}
@@ -115,10 +130,20 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
return nil
}
// errDecoder is an error-state decoder that always returns the same error.
type errDecoder struct {
err error
}
func (d *errDecoder) Decode(*dto.MetricFamily) error {
return d.err
}
// textDecoder implements the Decoder interface for the text protocol.
type textDecoder struct {
r io.Reader
fams map[string]*dto.MetricFamily
s model.ValidationScheme
err error
}
@@ -126,7 +151,7 @@ type textDecoder struct {
func (d *textDecoder) Decode(v *dto.MetricFamily) error {
if d.err == nil {
// Read all metrics in one shot.
var p TextParser
p := NewTextParser(d.s)
d.fams, d.err = p.TextToMetricFamilies(d.r)
// If we don't get an error, store io.EOF for the end.
if d.err == nil {
+4 -6
View File
@@ -18,14 +18,12 @@ import (
"io"
"net/http"
"github.com/munnerz/goautoneg"
dto "github.com/prometheus/client_model/go"
"google.golang.org/protobuf/encoding/protodelim"
"google.golang.org/protobuf/encoding/prototext"
"github.com/prometheus/common/model"
"github.com/munnerz/goautoneg"
dto "github.com/prometheus/client_model/go"
)
// Encoder types encode metric families into an underlying wire protocol.
@@ -61,7 +59,7 @@ func (ec encoderCloser) Close() error {
// appropriate accepted type is found, FmtText is returned (which is the
// Prometheus text format). This function will never negotiate FmtOpenMetrics,
// as the support is still experimental. To include the option to negotiate
// FmtOpenMetrics, use NegotiateOpenMetrics.
// FmtOpenMetrics, use NegotiateIncludingOpenMetrics.
func Negotiate(h http.Header) Format {
escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String())))
for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) {
@@ -153,7 +151,7 @@ func NewEncoder(w io.Writer, format Format, options ...EncoderOption) Encoder {
case TypeProtoDelim:
return encoderCloser{
encode: func(v *dto.MetricFamily) error {
_, err := protodelim.MarshalTo(w, v)
_, err := protodelim.MarshalTo(w, model.EscapeMetricFamily(v, escapingScheme))
return err
},
close: func() error { return nil },
+8 -4
View File
@@ -36,9 +36,11 @@ const (
ProtoType = `application/vnd.google.protobuf`
ProtoProtocol = `io.prometheus.client.MetricFamily`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
OpenMetricsType = `application/openmetrics-text`
ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";"
OpenMetricsType = `application/openmetrics-text`
//nolint:revive // Allow for underscores.
OpenMetricsVersion_0_0_1 = "0.0.1"
//nolint:revive // Allow for underscores.
OpenMetricsVersion_1_0_0 = "1.0.0"
// The Content-Type values for the different wire protocols. Do not do direct
@@ -54,8 +56,10 @@ const (
// Deprecated: Use expfmt.NewFormat(expfmt.TypeProtoCompact) instead.
FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
//nolint:revive // Allow for underscores.
FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8`
// Deprecated: Use expfmt.NewFormat(expfmt.TypeOpenMetrics) instead.
//nolint:revive // Allow for underscores.
FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8`
)
@@ -188,8 +192,8 @@ func (f Format) FormatType() FormatType {
// Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid
// "escaping" term exists, that will be used. Otherwise, the global default will
// be returned.
func (format Format) ToEscapingScheme() model.EscapingScheme {
for _, p := range strings.Split(string(format), ";") {
func (f Format) ToEscapingScheme() model.EscapingScheme {
for _, p := range strings.Split(string(f), ";") {
toks := strings.Split(p, "=")
if len(toks) != 2 {
continue
+6 -3
View File
@@ -17,7 +17,11 @@
package expfmt
import "bytes"
import (
"bytes"
"github.com/prometheus/common/model"
)
// Fuzz text metric parser with with github.com/dvyukov/go-fuzz:
//
@@ -26,9 +30,8 @@ import "bytes"
//
// Further input samples should go in the folder fuzz/corpus.
func Fuzz(in []byte) int {
parser := TextParser{}
parser := NewTextParser(model.UTF8Validation)
_, err := parser.TextToMetricFamilies(bytes.NewReader(in))
if err != nil {
return 0
}
+5 -6
View File
@@ -22,11 +22,10 @@ import (
"strconv"
"strings"
dto "github.com/prometheus/client_model/go"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
)
type encoderOption struct {
@@ -249,7 +248,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily, options ...E
// Finally the samples, one line for each.
if metricType == dto.MetricType_COUNTER && strings.HasSuffix(name, "_total") {
compliantName = compliantName + "_total"
compliantName += "_total"
}
for _, metric := range in.Metric {
switch metricType {
@@ -477,7 +476,7 @@ func writeOpenMetricsNameAndLabelPairs(
if name != "" {
// If the name does not pass the legacy validity check, we must put the
// metric name inside the braces, quoted.
if !model.IsValidLegacyMetricName(name) {
if !model.LegacyValidation.IsValidMetricName(name) {
metricInsideBraces = true
err := w.WriteByte(separator)
written++
@@ -641,11 +640,11 @@ func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) {
if err != nil {
return written, err
}
err = (*e).Timestamp.CheckValid()
err = e.Timestamp.CheckValid()
if err != nil {
return written, err
}
ts := (*e).Timestamp.AsTime()
ts := e.Timestamp.AsTime()
// TODO(beorn7): Format this directly from components of ts to
// avoid overflow/underflow and precision issues of the float
// conversion.
+4 -4
View File
@@ -22,9 +22,9 @@ import (
"strings"
"sync"
"github.com/prometheus/common/model"
dto "github.com/prometheus/client_model/go"
"github.com/prometheus/common/model"
)
// enhancedWriter has all the enhanced write functions needed here. bufio.Writer
@@ -354,7 +354,7 @@ func writeNameAndLabelPairs(
if name != "" {
// If the name does not pass the legacy validity check, we must put the
// metric name inside the braces.
if !model.IsValidLegacyMetricName(name) {
if !model.LegacyValidation.IsValidMetricName(name) {
metricInsideBraces = true
err := w.WriteByte(separator)
written++
@@ -498,7 +498,7 @@ func writeInt(w enhancedWriter, i int64) (int, error) {
// writeName writes a string as-is if it complies with the legacy naming
// scheme, or escapes it in double quotes if not.
func writeName(w enhancedWriter, name string) (int, error) {
if model.IsValidLegacyMetricName(name) {
if model.LegacyValidation.IsValidMetricName(name) {
return w.WriteString(name)
}
var written int
+38 -6
View File
@@ -78,6 +78,14 @@ type TextParser struct {
// These indicate if the metric name from the current line being parsed is inside
// braces and if that metric name was found respectively.
currentMetricIsInsideBraces, currentMetricInsideBracesIsPresent bool
// scheme sets the desired ValidationScheme for names. Defaults to the invalid
// UnsetValidation.
scheme model.ValidationScheme
}
// NewTextParser returns a new TextParser with the provided nameValidationScheme.
func NewTextParser(nameValidationScheme model.ValidationScheme) TextParser {
return TextParser{scheme: nameValidationScheme}
}
// TextToMetricFamilies reads 'in' as the simple and flat text-based exchange
@@ -126,6 +134,7 @@ func (p *TextParser) TextToMetricFamilies(in io.Reader) (map[string]*dto.MetricF
func (p *TextParser) reset(in io.Reader) {
p.metricFamiliesByName = map[string]*dto.MetricFamily{}
p.currentLabelPairs = nil
if p.buf == nil {
p.buf = bufio.NewReader(in)
} else {
@@ -216,6 +225,9 @@ func (p *TextParser) startComment() stateFn {
return nil
}
p.setOrCreateCurrentMF()
if p.err != nil {
return nil
}
if p.skipBlankTab(); p.err != nil {
return nil // Unexpected end of input.
}
@@ -244,6 +256,9 @@ func (p *TextParser) readingMetricName() stateFn {
return nil
}
p.setOrCreateCurrentMF()
if p.err != nil {
return nil
}
// Now is the time to fix the type if it hasn't happened yet.
if p.currentMF.Type == nil {
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
@@ -311,6 +326,9 @@ func (p *TextParser) startLabelName() stateFn {
switch p.currentByte {
case ',':
p.setOrCreateCurrentMF()
if p.err != nil {
return nil
}
if p.currentMF.Type == nil {
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
}
@@ -319,6 +337,10 @@ func (p *TextParser) startLabelName() stateFn {
return p.startLabelName
case '}':
p.setOrCreateCurrentMF()
if p.err != nil {
p.currentLabelPairs = nil
return nil
}
if p.currentMF.Type == nil {
p.currentMF.Type = dto.MetricType_UNTYPED.Enum()
}
@@ -341,6 +363,12 @@ func (p *TextParser) startLabelName() stateFn {
p.currentLabelPair = &dto.LabelPair{Name: proto.String(p.currentToken.String())}
if p.currentLabelPair.GetName() == string(model.MetricNameLabel) {
p.parseError(fmt.Sprintf("label name %q is reserved", model.MetricNameLabel))
p.currentLabelPairs = nil
return nil
}
if !p.scheme.IsValidLabelName(p.currentLabelPair.GetName()) {
p.parseError(fmt.Sprintf("invalid label name %q", p.currentLabelPair.GetName()))
p.currentLabelPairs = nil
return nil
}
// Special summary/histogram treatment. Don't add 'quantile' and 'le'
@@ -353,13 +381,12 @@ func (p *TextParser) startLabelName() stateFn {
labels := make(map[string]struct{})
for _, l := range p.currentLabelPairs {
lName := l.GetName()
if _, exists := labels[lName]; !exists {
labels[lName] = struct{}{}
} else {
if _, exists := labels[lName]; exists {
p.parseError(fmt.Sprintf("duplicate label names for metric %q", p.currentMF.GetName()))
p.currentLabelPairs = nil
return nil
}
labels[lName] = struct{}{}
}
return p.startLabelValue
}
@@ -440,7 +467,8 @@ func (p *TextParser) readingValue() stateFn {
// When we are here, we have read all the labels, so for the
// special case of a summary/histogram, we can finally find out
// if the metric already exists.
if p.currentMF.GetType() == dto.MetricType_SUMMARY {
switch p.currentMF.GetType() {
case dto.MetricType_SUMMARY:
signature := model.LabelsToSignature(p.currentLabels)
if summary := p.summaries[signature]; summary != nil {
p.currentMetric = summary
@@ -448,7 +476,7 @@ func (p *TextParser) readingValue() stateFn {
p.summaries[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
} else if p.currentMF.GetType() == dto.MetricType_HISTOGRAM {
case dto.MetricType_HISTOGRAM:
signature := model.LabelsToSignature(p.currentLabels)
if histogram := p.histograms[signature]; histogram != nil {
p.currentMetric = histogram
@@ -456,7 +484,7 @@ func (p *TextParser) readingValue() stateFn {
p.histograms[signature] = p.currentMetric
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
} else {
default:
p.currentMF.Metric = append(p.currentMF.Metric, p.currentMetric)
}
if p.readTokenUntilWhitespace(); p.err != nil {
@@ -805,6 +833,10 @@ func (p *TextParser) setOrCreateCurrentMF() {
p.currentIsHistogramCount = false
p.currentIsHistogramSum = false
name := p.currentToken.String()
if !p.scheme.IsValidMetricName(name) {
p.parseError(fmt.Sprintf("invalid metric name %q", name))
return
}
if p.currentMF = p.metricFamiliesByName[name]; p.currentMF != nil {
return
}
+14 -21
View File
@@ -32,6 +32,12 @@ const (
// MetricNameLabel is the label name indicating the metric name of a
// timeseries.
MetricNameLabel = "__name__"
// MetricTypeLabel is the label name indicating the metric type of
// timeseries as per the PROM-39 proposal.
MetricTypeLabel = "__type__"
// MetricUnitLabel is the label name indicating the metric unit of
// timeseries as per the PROM-39 proposal.
MetricUnitLabel = "__unit__"
// SchemeLabel is the name of the label that holds the scheme on which to
// scrape a target.
@@ -100,34 +106,21 @@ type LabelName string
// IsValid returns true iff the name matches the pattern of LabelNameRE when
// NameValidationScheme is set to LegacyValidation, or valid UTF-8 if
// NameValidationScheme is set to UTF8Validation.
//
// Deprecated: This method should not be used and may be removed in the future.
// Use [ValidationScheme.IsValidLabelName] instead.
func (ln LabelName) IsValid() bool {
if len(ln) == 0 {
return false
}
switch NameValidationScheme {
case LegacyValidation:
return ln.IsValidLegacy()
case UTF8Validation:
return utf8.ValidString(string(ln))
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
}
return NameValidationScheme.IsValidLabelName(string(ln))
}
// IsValidLegacy returns true iff name matches the pattern of LabelNameRE for
// legacy names. It does not use LabelNameRE for the check but a much faster
// hardcoded implementation.
//
// Deprecated: This method should not be used and may be removed in the future.
// Use [LegacyValidation.IsValidLabelName] instead.
func (ln LabelName) IsValidLegacy() bool {
if len(ln) == 0 {
return false
}
for i, b := range ln {
// TODO: Apply De Morgan's law. Make sure there are tests for this.
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { //nolint:staticcheck
return false
}
}
return true
return LegacyValidation.IsValidLabelName(string(ln))
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
+5 -5
View File
@@ -114,10 +114,10 @@ func (ls LabelSet) Clone() LabelSet {
}
// Merge is a helper function to non-destructively merge two label sets.
func (l LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(l))
func (ls LabelSet) Merge(other LabelSet) LabelSet {
result := make(LabelSet, len(ls))
for k, v := range l {
for k, v := range ls {
result[k] = v
}
@@ -140,7 +140,7 @@ func (ls LabelSet) FastFingerprint() Fingerprint {
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (l *LabelSet) UnmarshalJSON(b []byte) error {
func (ls *LabelSet) UnmarshalJSON(b []byte) error {
var m map[LabelName]LabelValue
if err := json.Unmarshal(b, &m); err != nil {
return err
@@ -153,6 +153,6 @@ func (l *LabelSet) UnmarshalJSON(b []byte) error {
return fmt.Errorf("%q is not a valid label name", ln)
}
}
*l = LabelSet(m)
*ls = LabelSet(m)
return nil
}
+162 -34
View File
@@ -14,6 +14,7 @@
package model
import (
"encoding/json"
"errors"
"fmt"
"regexp"
@@ -23,6 +24,7 @@ import (
"unicode/utf8"
dto "github.com/prometheus/client_model/go"
"go.yaml.in/yaml/v2"
"google.golang.org/protobuf/proto"
)
@@ -62,16 +64,151 @@ var (
type ValidationScheme int
const (
// UnsetValidation represents an undefined ValidationScheme.
// Should not be used in practice.
UnsetValidation ValidationScheme = iota
// LegacyValidation is a setting that requires that all metric and label names
// conform to the original Prometheus character requirements described by
// MetricNameRE and LabelNameRE.
LegacyValidation ValidationScheme = iota
LegacyValidation
// UTF8Validation only requires that metric and label names be valid UTF-8
// strings.
UTF8Validation
)
var _ interface {
yaml.Marshaler
yaml.Unmarshaler
json.Marshaler
json.Unmarshaler
fmt.Stringer
} = new(ValidationScheme)
// String returns the string representation of s.
func (s ValidationScheme) String() string {
switch s {
case UnsetValidation:
return "unset"
case LegacyValidation:
return "legacy"
case UTF8Validation:
return "utf8"
default:
panic(fmt.Errorf("unhandled ValidationScheme: %d", s))
}
}
// MarshalYAML implements the yaml.Marshaler interface.
func (s ValidationScheme) MarshalYAML() (any, error) {
switch s {
case UnsetValidation:
return "", nil
case LegacyValidation, UTF8Validation:
return s.String(), nil
default:
panic(fmt.Errorf("unhandled ValidationScheme: %d", s))
}
}
// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (s *ValidationScheme) UnmarshalYAML(unmarshal func(any) error) error {
var scheme string
if err := unmarshal(&scheme); err != nil {
return err
}
return s.Set(scheme)
}
// MarshalJSON implements the json.Marshaler interface.
func (s ValidationScheme) MarshalJSON() ([]byte, error) {
switch s {
case UnsetValidation:
return json.Marshal("")
case UTF8Validation, LegacyValidation:
return json.Marshal(s.String())
default:
return nil, fmt.Errorf("unhandled ValidationScheme: %d", s)
}
}
// UnmarshalJSON implements the json.Unmarshaler interface.
func (s *ValidationScheme) UnmarshalJSON(bytes []byte) error {
var repr string
if err := json.Unmarshal(bytes, &repr); err != nil {
return err
}
return s.Set(repr)
}
// Set implements the pflag.Value interface.
func (s *ValidationScheme) Set(text string) error {
switch text {
case "":
// Don't change the value.
case LegacyValidation.String():
*s = LegacyValidation
case UTF8Validation.String():
*s = UTF8Validation
default:
return fmt.Errorf("unrecognized ValidationScheme: %q", text)
}
return nil
}
// IsValidMetricName returns whether metricName is valid according to s.
func (s ValidationScheme) IsValidMetricName(metricName string) bool {
switch s {
case LegacyValidation:
if len(metricName) == 0 {
return false
}
for i, b := range metricName {
if !isValidLegacyRune(b, i) {
return false
}
}
return true
case UTF8Validation:
if len(metricName) == 0 {
return false
}
return utf8.ValidString(metricName)
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %s", s.String()))
}
}
// IsValidLabelName returns whether labelName is valid according to s.
func (s ValidationScheme) IsValidLabelName(labelName string) bool {
switch s {
case LegacyValidation:
if len(labelName) == 0 {
return false
}
for i, b := range labelName {
// TODO: Apply De Morgan's law. Make sure there are tests for this.
if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { //nolint:staticcheck
return false
}
}
return true
case UTF8Validation:
if len(labelName) == 0 {
return false
}
return utf8.ValidString(labelName)
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %s", s))
}
}
// Type implements the pflag.Value interface.
func (ValidationScheme) Type() string {
return "validationScheme"
}
type EscapingScheme int
const (
@@ -101,7 +238,7 @@ const (
// Accept header, the default NameEscapingScheme will be used.
EscapingKey = "escaping"
// Possible values for Escaping Key:
// Possible values for Escaping Key.
AllowUTF8 = "allow-utf-8" // No escaping required.
EscapeUnderscores = "underscores"
EscapeDots = "dots"
@@ -175,34 +312,22 @@ func (m Metric) FastFingerprint() Fingerprint {
// IsValidMetricName returns true iff name matches the pattern of MetricNameRE
// for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is
// selected.
//
// Deprecated: This function should not be used and might be removed in the future.
// Use [ValidationScheme.IsValidMetricName] instead.
func IsValidMetricName(n LabelValue) bool {
switch NameValidationScheme {
case LegacyValidation:
return IsValidLegacyMetricName(string(n))
case UTF8Validation:
if len(n) == 0 {
return false
}
return utf8.ValidString(string(n))
default:
panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme))
}
return NameValidationScheme.IsValidMetricName(string(n))
}
// IsValidLegacyMetricName is similar to IsValidMetricName but always uses the
// legacy validation scheme regardless of the value of NameValidationScheme.
// This function, however, does not use MetricNameRE for the check but a much
// faster hardcoded implementation.
//
// Deprecated: This function should not be used and might be removed in the future.
// Use [LegacyValidation.IsValidMetricName] instead.
func IsValidLegacyMetricName(n string) bool {
if len(n) == 0 {
return false
}
for i, b := range n {
if !isValidLegacyRune(b, i) {
return false
}
}
return true
return LegacyValidation.IsValidMetricName(n)
}
// EscapeMetricFamily escapes the given metric names and labels with the given
@@ -310,13 +435,14 @@ func EscapeName(name string, scheme EscapingScheme) string {
case DotsEscaping:
// Do not early return for legacy valid names, we still escape underscores.
for i, b := range name {
if b == '_' {
switch {
case b == '_':
escaped.WriteString("__")
} else if b == '.' {
case b == '.':
escaped.WriteString("_dot_")
} else if isValidLegacyRune(b, i) {
case isValidLegacyRune(b, i):
escaped.WriteRune(b)
} else {
default:
escaped.WriteString("__")
}
}
@@ -327,13 +453,14 @@ func EscapeName(name string, scheme EscapingScheme) string {
}
escaped.WriteString("U__")
for i, b := range name {
if b == '_' {
switch {
case b == '_':
escaped.WriteString("__")
} else if isValidLegacyRune(b, i) {
case isValidLegacyRune(b, i):
escaped.WriteRune(b)
} else if !utf8.ValidRune(b) {
case !utf8.ValidRune(b):
escaped.WriteString("_FFFD_")
} else {
default:
escaped.WriteRune('_')
escaped.WriteString(strconv.FormatInt(int64(b), 16))
escaped.WriteRune('_')
@@ -345,7 +472,7 @@ func EscapeName(name string, scheme EscapingScheme) string {
}
}
// lower function taken from strconv.atoi
// lower function taken from strconv.atoi.
func lower(c byte) byte {
return c | ('x' - 'X')
}
@@ -409,11 +536,12 @@ func UnescapeName(name string, scheme EscapingScheme) string {
}
r := lower(escapedName[i])
utf8Val *= 16
if r >= '0' && r <= '9' {
switch {
case r >= '0' && r <= '9':
utf8Val += uint(r) - '0'
} else if r >= 'a' && r <= 'f' {
case r >= 'a' && r <= 'f':
utf8Val += uint(r) - 'a' + 10
} else {
default:
return name
}
i++
+6 -6
View File
@@ -126,14 +126,14 @@ func (t *Time) UnmarshalJSON(b []byte) error {
p := strings.Split(string(b), ".")
switch len(p) {
case 1:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
v, err := strconv.ParseInt(p[0], 10, 64)
if err != nil {
return err
}
*t = Time(v * second)
case 2:
v, err := strconv.ParseInt(string(p[0]), 10, 64)
v, err := strconv.ParseInt(p[0], 10, 64)
if err != nil {
return err
}
@@ -143,7 +143,7 @@ func (t *Time) UnmarshalJSON(b []byte) error {
if prec < 0 {
p[1] = p[1][:dotPrecision]
} else if prec > 0 {
p[1] = p[1] + strings.Repeat("0", prec)
p[1] += strings.Repeat("0", prec)
}
va, err := strconv.ParseInt(p[1], 10, 32)
@@ -170,15 +170,15 @@ func (t *Time) UnmarshalJSON(b []byte) error {
// This type should not propagate beyond the scope of input/output processing.
type Duration time.Duration
// Set implements pflag/flag.Value
// Set implements pflag/flag.Value.
func (d *Duration) Set(s string) error {
var err error
*d, err = ParseDuration(s)
return err
}
// Type implements pflag.Value
func (d *Duration) Type() string {
// Type implements pflag.Value.
func (*Duration) Type() string {
return "duration"
}
+8 -7
View File
@@ -191,7 +191,8 @@ func (ss SampleStream) String() string {
}
func (ss SampleStream) MarshalJSON() ([]byte, error) {
if len(ss.Histograms) > 0 && len(ss.Values) > 0 {
switch {
case len(ss.Histograms) > 0 && len(ss.Values) > 0:
v := struct {
Metric Metric `json:"metric"`
Values []SamplePair `json:"values"`
@@ -202,7 +203,7 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) {
Histograms: ss.Histograms,
}
return json.Marshal(&v)
} else if len(ss.Histograms) > 0 {
case len(ss.Histograms) > 0:
v := struct {
Metric Metric `json:"metric"`
Histograms []SampleHistogramPair `json:"histograms"`
@@ -211,7 +212,7 @@ func (ss SampleStream) MarshalJSON() ([]byte, error) {
Histograms: ss.Histograms,
}
return json.Marshal(&v)
} else {
default:
v := struct {
Metric Metric `json:"metric"`
Values []SamplePair `json:"values"`
@@ -258,7 +259,7 @@ func (s Scalar) String() string {
// MarshalJSON implements json.Marshaler.
func (s Scalar) MarshalJSON() ([]byte, error) {
v := strconv.FormatFloat(float64(s.Value), 'f', -1, 64)
return json.Marshal([...]interface{}{s.Timestamp, string(v)})
return json.Marshal([...]interface{}{s.Timestamp, v})
}
// UnmarshalJSON implements json.Unmarshaler.
@@ -349,9 +350,9 @@ func (m Matrix) Len() int { return len(m) }
func (m Matrix) Less(i, j int) bool { return m[i].Metric.Before(m[j].Metric) }
func (m Matrix) Swap(i, j int) { m[i], m[j] = m[j], m[i] }
func (mat Matrix) String() string {
matCp := make(Matrix, len(mat))
copy(matCp, mat)
func (m Matrix) String() string {
matCp := make(Matrix, len(m))
copy(matCp, m)
sort.Sort(matCp)
strs := make([]string, len(matCp))
+5 -5
View File
@@ -86,22 +86,22 @@ func (s *HistogramBucket) Equal(o *HistogramBucket) bool {
return s == o || (s.Boundaries == o.Boundaries && s.Lower == o.Lower && s.Upper == o.Upper && s.Count == o.Count)
}
func (b HistogramBucket) String() string {
func (s HistogramBucket) String() string {
var sb strings.Builder
lowerInclusive := b.Boundaries == 1 || b.Boundaries == 3
upperInclusive := b.Boundaries == 0 || b.Boundaries == 3
lowerInclusive := s.Boundaries == 1 || s.Boundaries == 3
upperInclusive := s.Boundaries == 0 || s.Boundaries == 3
if lowerInclusive {
sb.WriteRune('[')
} else {
sb.WriteRune('(')
}
fmt.Fprintf(&sb, "%g,%g", b.Lower, b.Upper)
fmt.Fprintf(&sb, "%g,%g", s.Lower, s.Upper)
if upperInclusive {
sb.WriteRune(']')
} else {
sb.WriteRune(')')
}
fmt.Fprintf(&sb, ":%v", b.Count)
fmt.Fprintf(&sb, ":%v", s.Count)
return sb.String()
}
+2 -2
View File
@@ -66,8 +66,8 @@ func (et *ValueType) UnmarshalJSON(b []byte) error {
return nil
}
func (e ValueType) String() string {
switch e {
func (et ValueType) String() string {
switch et {
case ValNone:
return "<ValNone>"
case ValScalar:
+2 -2
View File
@@ -1,4 +1,4 @@
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
if ! has nix_direnv_version || ! nix_direnv_version 3.1.0; then
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.1.0/direnvrc" "sha256-yMJ2OVMzrFaDPn7q8nCBZFRYpL/f0RcHzhmw/i6btJM="
fi
use flake . --impure
+29 -19
View File
@@ -1,27 +1,37 @@
version: "2"
run:
timeout: 10m
linters-settings:
gci:
sections:
- standard
- default
- prefix(github.com/sagikazarmark/locafero)
goimports:
local-prefixes: github.com/sagikazarmark/locafero
misspell:
locale: US
nolintlint:
allow-leading-space: false # require machine-readable nolint directives (with no leading space)
allow-unused: false # report any unused nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
revive:
confidence: 0
linters:
enable:
- gci
- goimports
- errcheck
- govet
- ineffassign
- misspell
- nolintlint
- revive
- staticcheck
- unused
settings:
misspell:
locale: US
nolintlint:
allow-unused: false # report any unused nolint directives
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
formatters:
enable:
- gci
- gofmt
- gofumpt
- goimports
- golines
settings:
gci:
sections:
- standard
- default
- localmodule
+2 -2
View File
@@ -2,8 +2,8 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/sagikazarmark/locafero/ci.yaml?style=flat-square)](https://github.com/sagikazarmark/locafero/actions/workflows/ci.yaml)
[![go.dev reference](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white&style=flat-square)](https://pkg.go.dev/mod/github.com/sagikazarmark/locafero)
![Go Version](https://img.shields.io/badge/go%20version-%3E=1.20-61CFDD.svg?style=flat-square)
[![built with nix](https://img.shields.io/badge/builtwith-nix-7d81f7?style=flat-square)](https://builtwithnix.org)
![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/sagikazarmark/locafero?style=flat-square&color=61CFDD)
[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/sagikazarmark/locafero/badge?style=flat-square)](https://deps.dev/go/github.com%252Fsagikazarmark%252Flocafero)
**Finder library for [Afero](https://github.com/spf13/afero) ported from [go-finder](https://github.com/sagikazarmark/go-finder).**
+8 -4
View File
@@ -5,19 +5,23 @@ import "io/fs"
// FileType represents the kind of entries [Finder] can return.
type FileType int
// FileType represents the kind of entries [Finder] can return.
const (
FileTypeAll FileType = iota
FileTypeAny FileType = iota
FileTypeFile
FileTypeDir
// Deprecated: Use [FileTypeAny] instead.
FileTypeAll = FileTypeAny
)
func (ft FileType) matchFileInfo(info fs.FileInfo) bool {
func (ft FileType) match(info fs.FileInfo) bool {
switch ft {
case FileTypeAll:
case FileTypeAny:
return true
case FileTypeFile:
return !info.IsDir()
return info.Mode().IsRegular()
case FileTypeDir:
return info.IsDir()
+55 -49
View File
@@ -1,4 +1,4 @@
// Package finder looks for files and directories in an {fs.Fs} filesystem.
// Package locafero looks for files and directories in an {fs.Fs} filesystem.
package locafero
import (
@@ -7,7 +7,7 @@ import (
"path/filepath"
"strings"
"github.com/sourcegraph/conc/iter"
"github.com/sourcegraph/conc/pool"
"github.com/spf13/afero"
)
@@ -44,65 +44,66 @@ type Finder struct {
// Find looks for files and directories in an [afero.Fs] filesystem.
func (f Finder) Find(fsys afero.Fs) ([]string, error) {
// Arbitrary go routine limit (TODO: make this a parameter)
// pool := pool.NewWithResults[[]string]().WithMaxGoroutines(5).WithErrors().WithFirstError()
type searchItem struct {
path string
name string
}
var searchItems []searchItem
p := pool.NewWithResults[[]searchResult]().WithMaxGoroutines(5).WithErrors().WithFirstError()
for _, searchPath := range f.Paths {
searchPath := searchPath
for _, searchName := range f.Names {
searchName := searchName
p.Go(func() ([]searchResult, error) {
// If the name contains any glob character, perform a glob match
if strings.ContainsAny(searchName, globMatch) {
return globWalkSearch(fsys, searchPath, searchName, f.Type)
}
searchItems = append(searchItems, searchItem{searchPath, searchName})
// pool.Go(func() ([]string, error) {
// // If the name contains any glob character, perform a glob match
// if strings.ContainsAny(searchName, globMatch) {
// return globWalkSearch(fsys, searchPath, searchName, f.Type)
// }
//
// return statSearch(fsys, searchPath, searchName, f.Type)
// })
return statSearch(fsys, searchPath, searchName, f.Type)
})
}
}
// allResults, err := pool.Wait()
// if err != nil {
// return nil, err
// }
allResults, err := iter.MapErr(searchItems, func(item *searchItem) ([]string, error) {
// If the name contains any glob character, perform a glob match
if strings.ContainsAny(item.name, globMatch) {
return globWalkSearch(fsys, item.path, item.name, f.Type)
}
return statSearch(fsys, item.path, item.name, f.Type)
})
searchResults, err := flatten(p.Wait())
if err != nil {
return nil, err
}
var results []string
for _, r := range allResults {
results = append(results, r...)
// Return early if no results were found
if len(searchResults) == 0 {
return nil, nil
}
// Sort results in alphabetical order for now
// sort.Strings(results)
results := make([]string, 0, len(searchResults))
for _, searchResult := range searchResults {
results = append(results, searchResult.path)
}
return results, nil
}
func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
var results []string
type searchResult struct {
path string
info fs.FileInfo
}
func flatten[T any](results [][]T, err error) ([]T, error) {
if err != nil {
return nil, err
}
var flattened []T
for _, r := range results {
flattened = append(flattened, r...)
}
return flattened, nil
}
func globWalkSearch(
fsys afero.Fs,
searchPath string,
searchName string,
searchType FileType,
) ([]searchResult, error) {
var results []searchResult
err := afero.Walk(fsys, searchPath, func(p string, fileInfo fs.FileInfo, err error) error {
if err != nil {
@@ -123,7 +124,7 @@ func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchT
}
// Skip unmatching type
if !searchType.matchFileInfo(fileInfo) {
if !searchType.match(fileInfo) {
return result
}
@@ -133,7 +134,7 @@ func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchT
}
if match {
results = append(results, p)
results = append(results, searchResult{p, fileInfo})
}
return result
@@ -145,7 +146,12 @@ func globWalkSearch(fsys afero.Fs, searchPath string, searchName string, searchT
return results, nil
}
func statSearch(fsys afero.Fs, searchPath string, searchName string, searchType FileType) ([]string, error) {
func statSearch(
fsys afero.Fs,
searchPath string,
searchName string,
searchType FileType,
) ([]searchResult, error) {
filePath := filepath.Join(searchPath, searchName)
fileInfo, err := fsys.Stat(filePath)
@@ -157,9 +163,9 @@ func statSearch(fsys afero.Fs, searchPath string, searchName string, searchType
}
// Skip unmatching type
if !searchType.matchFileInfo(fileInfo) {
if !searchType.match(fileInfo) {
return nil, nil
}
return []string{filePath}, nil
return []searchResult{{filePath, fileInfo}}, nil
}
+100 -317
View File
@@ -2,30 +2,32 @@
"nodes": {
"cachix": {
"inputs": {
"devenv": "devenv_2",
"devenv": [
"devenv"
],
"flake-compat": [
"devenv"
],
"git-hooks": [
"devenv",
"flake-compat"
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"pre-commit-hooks": [
"devenv",
"pre-commit-hooks"
]
},
"locked": {
"lastModified": 1712055811,
"narHash": "sha256-7FcfMm5A/f02yyzuavJe06zLa9hcMHsagE28ADcmQvk=",
"lastModified": 1748883665,
"narHash": "sha256-R0W7uAg+BLoHjMRMQ8+oiSbTq8nkGz5RDpQ+ZfxxP3A=",
"owner": "cachix",
"repo": "cachix",
"rev": "02e38da89851ec7fec3356a5c04bc8349cae0e30",
"rev": "f707778d902af4d62d8dd92c269f8e70de09acbe",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "latest",
"repo": "cachix",
"type": "github"
}
@@ -33,52 +35,21 @@
"devenv": {
"inputs": {
"cachix": "cachix",
"flake-compat": "flake-compat_2",
"nix": "nix_2",
"nixpkgs": "nixpkgs_2",
"pre-commit-hooks": "pre-commit-hooks"
},
"locked": {
"lastModified": 1725907707,
"narHash": "sha256-s3pbtzZmVPHzc86WQjK7MGZMNvvw6hWnFMljEkllAfM=",
"owner": "cachix",
"repo": "devenv",
"rev": "2bbbbc468fc02257265a79652a8350651cca495a",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"devenv_2": {
"inputs": {
"flake-compat": [
"devenv",
"cachix",
"flake-compat"
],
"flake-compat": "flake-compat",
"git-hooks": "git-hooks",
"nix": "nix",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix",
"pre-commit-hooks": [
"devenv",
"cachix",
"pre-commit-hooks"
]
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1708704632,
"narHash": "sha256-w+dOIW60FKMaHI1q5714CSibk99JfYxm0CzTinYWr+Q=",
"lastModified": 1753981111,
"narHash": "sha256-uBJOyMxOkGRmxhD2M5rbN2aV6oP1T2AKq5oBaHHC4mw=",
"owner": "cachix",
"repo": "devenv",
"rev": "2ee4450b0f4b95a1b90f2eb5ffea98b90e48c196",
"rev": "d4d70df706b153b601a87ab8e81c88a0b1a373b6",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "python-rewrite",
"repo": "devenv",
"type": "github"
}
@@ -86,27 +57,11 @@
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"lastModified": 1747046372,
"narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -116,15 +71,37 @@
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"devenv",
"nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1725234343,
"narHash": "sha256-+ebgonl3NbiKD2UD0x4BszCZQ6sTfL4xioaM49o5B3Y=",
"lastModified": 1753121425,
"narHash": "sha256-TVcTNvOeWWk1DXljFxVRp+E0tzG1LhrVjOGGoMHuXio=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "567b938d64d4b4112ee253b9274472dc3a346eb6",
"rev": "644e0fc48951a860279da645ba77fe4a6e814c5e",
"type": "github"
},
"original": {
@@ -133,39 +110,29 @@
"type": "github"
}
},
"flake-utils": {
"git-hooks": {
"inputs": {
"systems": "systems"
"flake-compat": [
"devenv",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"lastModified": 1750779888,
"narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
@@ -173,7 +140,7 @@
"inputs": {
"nixpkgs": [
"devenv",
"pre-commit-hooks",
"git-hooks",
"nixpkgs"
]
},
@@ -192,165 +159,49 @@
}
},
"nix": {
"inputs": {
"flake-compat": "flake-compat",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression"
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"repo": "nix",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688870561,
"narHash": "sha256-4UYkifnPEw1nAzqqPOTL2MvWtm3sNGw1UTYTalkTcGY=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "165b1650b753316aa7f1787f3005a8d2da0f5301",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nix_2": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-parts": "flake-parts",
"git-hooks-nix": [
"devenv",
"git-hooks"
],
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-regression": "nixpkgs-regression_2"
"nixpkgs-23-11": [
"devenv"
],
"nixpkgs-regression": [
"devenv"
]
},
"locked": {
"lastModified": 1712911606,
"narHash": "sha256-BGvBhepCufsjcUkXnEEXhEVjwdJAwPglCC2+bInc794=",
"owner": "domenkozar",
"lastModified": 1752773918,
"narHash": "sha256-dOi/M6yNeuJlj88exI+7k154z+hAhFcuB8tZktiW7rg=",
"owner": "cachix",
"repo": "nix",
"rev": "b24a9318ea3f3600c1e24b4a00691ee912d4de12",
"rev": "031c3cf42d2e9391eee373507d8c12e0f9606779",
"type": "github"
},
"original": {
"owner": "domenkozar",
"ref": "devenv-2.21",
"owner": "cachix",
"ref": "devenv-2.30",
"repo": "nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1692808169,
"narHash": "sha256-x9Opq06rIiwdwGeK2Ykj69dNc2IvUH1fY55Wm7atwrE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9201b5ff357e781bf014d0330d18555695df7ba8",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1725233747,
"narHash": "sha256-Ss8QWLXdr2JCBPcYChJhz4xJm+h/xjl4G0c0XlP6a74=",
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://github.com/NixOS/nixpkgs/archive/356624c12086a18f2ea2825fed34523d60ccc4e3.tar.gz"
}
},
"nixpkgs-regression": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-regression_2": {
"locked": {
"lastModified": 1643052045,
"narHash": "sha256-uGJ0VXIhWKGXxkeNnq4TvV3CIOkUJ3PAoLZ3HMzNVMw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "215d4d0fd80ca5163643b03a33fde804a29cc1e2",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1710695816,
"narHash": "sha256-3Eh7fhEID17pv9ZxrPwCLfqXnYP006RKzSs0JptsN84=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "614b4613980a522ba49f0d194531beddbb7220d3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713361204,
"narHash": "sha256-TA6EDunWTkc5FvDCqU3W2T3SFn0gRZqh6D/hJnM02MM=",
"lastModified": 1750441195,
"narHash": "sha256-yke+pm+MdgRb6c0dPt8MgDhv7fcBbdjmv1ZceNTyzKg=",
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "285676e87ad9f0ca23d8714a6ab61e7e027020c6",
"rev": "0ceffe312871b443929ff3006960d29b120dc627",
"type": "github"
},
"original": {
@@ -360,110 +211,42 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs-lib": {
"locked": {
"lastModified": 1725910328,
"narHash": "sha256-n9pCtzGZ0httmTwMuEbi5E78UQ4ZbQMr1pzi5N0LAG8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5775c2583f1801df7b790bf7f7d710a19bac66f4",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"devenv",
"cachix",
"devenv",
"nixpkgs"
]
},
"locked": {
"lastModified": 1692876271,
"narHash": "sha256-IXfZEkI0Mal5y1jr6IRWMqK8GW2/f28xJenZIPQqkY0=",
"lastModified": 1751159883,
"narHash": "sha256-urW/Ylk9FIfvXfliA1ywh75yszAbiTEVgpPeinFyVZo=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "d5006be9c2c2417dafb2e2e5034d83fabd207ee3",
"repo": "nixpkgs.lib",
"rev": "14a40a1d7fb9afa4739275ac642ed7301a9ba1ab",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"repo": "nixpkgs.lib",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": [
"devenv",
"flake-compat"
],
"flake-utils": "flake-utils_2",
"gitignore": "gitignore",
"nixpkgs": [
"devenv",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"nixpkgs_2": {
"locked": {
"lastModified": 1713775815,
"narHash": "sha256-Wu9cdYTnGQQwtT20QQMg7jzkANKQjwBD9iccfGKkfls=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "2ac4dcbf55ed43f3be0bae15e181f08a57af24a4",
"lastModified": 1753939845,
"narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "94def634a20494ee057c76998843c015909d6311",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"flake-parts": "flake-parts",
"nixpkgs": "nixpkgs_3"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_2"
}
}
},
+22 -44
View File
@@ -1,64 +1,42 @@
{
description = "Finder library for Afero";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
devenv.url = "github:cachix/devenv";
};
outputs = inputs@{ flake-parts, ... }:
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.devenv.flakeModule
];
systems = [ "x86_64-linux" "aarch64-darwin" ];
systems = [
"x86_64-linux"
"aarch64-darwin"
];
perSystem = { config, self', inputs', pkgs, system, ... }: rec {
devenv.shells = {
default = {
languages = {
go.enable = true;
go.package = pkgs.lib.mkDefault pkgs.go_1_23;
};
perSystem =
{ pkgs, ... }:
{
devenv.shells = {
default = {
languages = {
go.enable = true;
go.package = pkgs.lib.mkDefault pkgs.go_1_24;
};
packages = with pkgs; [
just
packages = with pkgs; [
just
golangci-lint
];
golangci-lint
];
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
};
ci = devenv.shells.default;
ci_1_21 = {
imports = [ devenv.shells.ci ];
languages = {
go.package = pkgs.go_1_21;
};
};
ci_1_22 = {
imports = [ devenv.shells.ci ];
languages = {
go.package = pkgs.go_1_22;
};
};
ci_1_23 = {
imports = [ devenv.shells.ci ];
languages = {
go.package = pkgs.go_1_23;
# https://github.com/cachix/devenv/issues/528#issuecomment-1556108767
containers = pkgs.lib.mkForce { };
};
};
};
};
};
}
+5 -2
View File
@@ -2,10 +2,13 @@ default:
just --list
test:
go test -race -v ./...
go test -count 10 -shuffle on -race -v ./...
fuzz:
go test -race -v -fuzz=Fuzz -fuzztime=60s ./...
lint:
golangci-lint run
fmt:
golangci-lint run --fix
golangci-lint fmt
+24
View File
@@ -0,0 +1,24 @@
.DEFAULT_GOAL := help
GO_BIN ?= $(shell go env GOPATH)/bin
.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
$(GO_BIN)/golangci-lint:
@echo "==> Installing golangci-lint within "${GO_BIN}""
@go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
.PHONY: lint
lint: $(GO_BIN)/golangci-lint ## Run linting on Go files
@echo "==> Linting Go source files"
@golangci-lint run -v --fix -c .golangci.yml ./...
.PHONY: test
test: ## Run tests
go test -race -v ./... -coverprofile ./coverage.txt
.PHONY: bench
bench: ## Run benchmarks. See https://pkg.go.dev/cmd/go#hdr-Testing_flags
go test ./... -bench . -benchtime 5s -timeout 0 -run=XXX -cpu 1 -benchmem
@@ -1,10 +0,0 @@
//go:build !go1.20
// +build !go1.20
package multierror
import "go.uber.org/multierr"
var (
Join = multierr.Combine
)
@@ -1,10 +0,0 @@
//go:build go1.20
// +build go1.20
package multierror
import "errors"
var (
Join = errors.Join
)

Some files were not shown because too many files have changed in this diff Show More