build(deps): bump github.com/open-policy-agent/opa from 1.19.1 to 1.21.0

Bumps [github.com/open-policy-agent/opa](https://github.com/open-policy-agent/opa) from 1.19.1 to 1.21.0.
- [Release notes](https://github.com/open-policy-agent/opa/releases)
- [Changelog](https://github.com/open-policy-agent/opa/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-policy-agent/opa/compare/v1.19.1...v1.21.0)

---
updated-dependencies:
- dependency-name: github.com/open-policy-agent/opa
  dependency-version: 1.20.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
This commit is contained in:
dependabot[bot] authored and GitHub committed 2026-10-01 10:03:43 +00:00
1 parent a1982c6908
commit 24579530be
339 files changed
+38711 -11288

No files matched your search

+7 -7
View File
@@ -57,11 +57,11 @@ require (
github.com/nats-io/nats-server/v2 v2.15.0
github.com/nats-io/nats.go v1.54.0
github.com/olekukonko/errors v1.3.0
github.com/olekukonko/tablewriter v1.1.4
github.com/olekukonko/tablewriter v1.1.5
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.32.1
github.com/onsi/gomega v1.43.1
github.com/open-policy-agent/opa v1.19.1
github.com/open-policy-agent/opa v1.21.0
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067
github.com/opencloud-eu/reva/v2 v2.50.1-0.20261001091108-11d87fb6b985
@@ -222,7 +222,7 @@ require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/go-test/deep v1.1.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/glob v1.0.0 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
@@ -260,11 +260,11 @@ require (
github.com/kovidgoyal/go-shm v1.0.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.2.1 // indirect
github.com/lestrrat-go/dsig v1.4.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.5 // indirect
github.com/lestrrat-go/jwx/v3 v3.1.1 // indirect
github.com/lestrrat-go/httprc/v3 v3.0.6 // indirect
github.com/lestrrat-go/jwx/v3 v3.3.0 // indirect
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/libregraph/oidc-go v1.1.0 // indirect
github.com/longsleep/go-metrics v1.0.0 // indirect
@@ -356,7 +356,7 @@ require (
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/vektah/gqlparser/v2 v2.5.36 // indirect
github.com/vektah/gqlparser/v2 v2.5.37 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wk8/go-ordered-map v1.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
+16 -16
View File
@@ -231,8 +231,8 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3h
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I=
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE=
github.com/dgraph-io/badger/v4 v4.9.4 h1:bcw+waCpzRZ2nmcSPbnPvDVhiEsn98TKmvnAhK7r7LM=
github.com/dgraph-io/badger/v4 v4.9.4/go.mod h1:nJjaJTUOSsQEBhsq209FmwCvMJzEA3e74RjZw6V2pQI=
github.com/dgraph-io/badger/v4 v4.9.6 h1:IQqMPVGLNCQr1b4Mu8lHkYm/xyqFRsyKaFEtyLi9CCQ=
github.com/dgraph-io/badger/v4 v4.9.6/go.mod h1:Xa9dAupjbwAacupWFCpa6YEn9E1PjBXkfZYr2I/8aWg=
github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM=
github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
@@ -395,8 +395,8 @@ github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/glob v1.0.0 h1:p+FKbLEIsK1yZ39/OINwFvqNb5oyPY4H8xcy6uYu8dg=
github.com/gobwas/glob v1.0.0/go.mod h1:oWCdo522i2P1n/hMXGNWs7yoV4wy/ciZuUIbvKj5rkc=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -616,16 +616,16 @@ github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69 h1:ZLo0bX
github.com/leonelquinteros/gotext v1.7.3-0.20260422134830-b012b4ccae69/go.mod h1:ksG5iXViKefoupjy+0qQjAVoaDnylnQ1ejWl9g14wh8=
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.2.1 h1:MwxzZhE4+4fguHi+uDALKVlC3Cn+O1QU1Q/F8D7hVIc=
github.com/lestrrat-go/dsig v1.2.1/go.mod h1:RD2eOaidyPvpc7IJQoO3Qq52RWdy8ZcJs8lrOnoa1Kc=
github.com/lestrrat-go/dsig v1.4.0 h1:g7LUjK8cT74A5DzBXJI5HzsJuLhoYN0Wzj4nuOMIrH8=
github.com/lestrrat-go/dsig v1.4.0/go.mod h1:I8Nddg/vN2cUl/h8N7SRRApLnNNeyZPIqLYpvpOtGGo=
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.5 h1:S+Mb4L2I+bM6JGTibLmxExhyTOqnXjqx+zi9MoXw/TM=
github.com/lestrrat-go/httprc/v3 v3.0.5/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.1.1 h1:yd9AdPmZ4INnQ7k42IrzXYpnEG803+SrQ6hdMvzHJzw=
github.com/lestrrat-go/jwx/v3 v3.1.1/go.mod h1:uw/MN2M/Xiu4FhwcIwH11Zsh9JWx9SWzgALl7/uIEkU=
github.com/lestrrat-go/httprc/v3 v3.0.6 h1:4FpLQ18KK/ypPbVU3NLWJNRvH3kcYiqKqWfKGqNWxxI=
github.com/lestrrat-go/httprc/v3 v3.0.6/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/jwx/v3 v3.3.0 h1:OXcYvQOQ7cxWzeZ/Q9sYk8ABe/kCSI371WmuACiCT+4=
github.com/lestrrat-go/jwx/v3 v3.3.0/go.mod h1:eIJhDcKHBwcgxqv8RiIylV67TVl1wJp/265IAHY1Db8=
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/libregraph/idm v0.5.0 h1:tDMwKbAOZzdeDYMxVlY5PbSqRKO7dbAW9KT42A51WSk=
@@ -741,8 +741,8 @@ github.com/olekukonko/errors v1.3.0 h1:teJvgLGUEqMzBUms+Dj3/3szNqCG/Jdw9iDbum8fR
github.com/olekukonko/errors v1.3.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA=
github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88=
github.com/olekukonko/tablewriter v1.1.4 h1:ORUMI3dXbMnRlRggJX3+q7OzQFDdvgbN9nVWj1drm6I=
github.com/olekukonko/tablewriter v1.1.4/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/olekukonko/tablewriter v1.1.5 h1:4LoZSfMySpMQY3PT8RWJsJeuEuMIoo9xGRgvmqjg6IQ=
github.com/olekukonko/tablewriter v1.1.5/go.mod h1:+kedxuyTtgoZLwif3P1Em4hARJs+mVnzKxmsCL/C5RY=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
@@ -753,8 +753,8 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.43.1 h1:vGIPFuYrIO6/0Z09s0I0QQQgFchiX4+tb1re3MScJYo=
github.com/onsi/gomega v1.43.1/go.mod h1:e/C2HwaZ1DhvjzXXuFhcR7hY7Sh9pl7MmoWKEjzwcdA=
github.com/open-policy-agent/opa v1.19.1 h1:aB1nOncChnTbQurjRQVJnjTJxditt8VqszlbaM3GGKU=
github.com/open-policy-agent/opa v1.19.1/go.mod h1:pb6Y6klyf7X7X8uXNDflruA9dQC2gMqWROXI5w/kvv0=
github.com/open-policy-agent/opa v1.21.0 h1:k/N0fieTkBPM0H7mIOrMd/xZPaMsxW70jIzIPeOBst4=
github.com/open-policy-agent/opa v1.21.0/go.mod h1:eJL6KUOIaW5YLnhJEA6sm3FOYRDJaHZvYT6geATbpPk=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a h1:Sakl76blJAaM6NxylVkgSzktjo2dS504iDotEFJsh3M=
github.com/opencloud-eu/go-micro-plugins/v4/store/nats-js-kv v0.0.0-20250512152754-23325793059a/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+lP5lUUIzjRGDg93WrQfZJZCaV1ZP3KeyXi8bzY=
@@ -981,8 +981,8 @@ github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/vektah/gqlparser/v2 v2.5.36 h1:CN9mKVHgMkc+XftdOWIhb4HEL8wKSYkFAqhf8booa7s=
github.com/vektah/gqlparser/v2 v2.5.36/go.mod h1:cAJ9qwVgPaUkWv6Gn8vn0mqOE0Ui5Pn56wNy5396XWo=
github.com/vektah/gqlparser/v2 v2.5.37 h1:jbb1Ilv+xBklV6653tKb4oVUupPNTLb5LmrnBKVI12Y=
github.com/vektah/gqlparser/v2 v2.5.37/go.mod h1:9O4Ox6Ngd3Y12bMD3w6i3CRQXh8W1oC1q0m6olCymDM=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+2
View File
@@ -6,3 +6,5 @@ glob.iml
*.dot
*.png
*.svg
patterns.txt
*.bench
-9
View File
@@ -1,9 +0,0 @@
sudo: false
language: go
go:
- 1.5.3
script:
- go test -v ./...
+36 -21
View File
@@ -1,26 +1,41 @@
#! /bin/bash
#!/bin/bash
#
# Compares the benchmarks of the current branch against a git revision:
#
# ./bench.sh v0.2.3 # all the benchmarks
# ./bench.sh master 'Match' # the ones matching a -bench regexp
#
# The results are written to *.bench files in the current directory and
# compared with benchstat (go install golang.org/x/perf/cmd/benchstat@latest).
bench() {
filename="/tmp/$1-$2.bench"
if test -e "${filename}";
then
echo "Already exists ${filename}"
else
backup=`git rev-parse --abbrev-ref HEAD`
git checkout $1
echo -n "Creating ${filename}... "
go test ./... -run=NONE -bench=$2 > "${filename}" -benchmem
echo "OK"
git checkout ${backup}
sleep 5
fi
set -eu
prev=$1
what=${2:-.}
curr=$(git rev-parse --abbrev-ref HEAD)
rnd=$(head -c4 </dev/urandom | xxd -p)
file() {
echo "$rnd-$1.bench" | tr "/" "_"
}
bench() {
local rev=$1
local out
out=$(file "$rev")
if [[ -e "$out" ]]; then
echo "Already exists $out"
return
fi
git checkout -q "$rev"
echo -n "Creating $out... "
go test ./... -run=none -benchmem -bench="$what" >"$out"
echo "OK"
git checkout -q "$curr"
sleep 5
}
to=$1
current=`git rev-parse --abbrev-ref HEAD`
bench "$prev"
bench "$curr"
bench ${to} $2
bench ${current} $2
benchcmp $3 "/tmp/${to}-$2.bench" "/tmp/${current}-$2.bench"
benchstat "$(file "$prev")" "$(file "$curr")"
-525
View File
@@ -1,525 +0,0 @@
package compiler
// TODO use constructor with all matchers, and to their structs private
// TODO glue multiple Text nodes (like after QuoteMeta)
import (
"fmt"
"reflect"
"github.com/gobwas/glob/match"
"github.com/gobwas/glob/syntax/ast"
"github.com/gobwas/glob/util/runes"
)
func optimizeMatcher(matcher match.Matcher) match.Matcher {
switch m := matcher.(type) {
case match.Any:
if len(m.Separators) == 0 {
return match.NewSuper()
}
case match.AnyOf:
if len(m.Matchers) == 1 {
return m.Matchers[0]
}
return m
case match.List:
if m.Not == false && len(m.List) == 1 {
return match.NewText(string(m.List))
}
return m
case match.BTree:
m.Left = optimizeMatcher(m.Left)
m.Right = optimizeMatcher(m.Right)
r, ok := m.Value.(match.Text)
if !ok {
return m
}
var (
leftNil = m.Left == nil
rightNil = m.Right == nil
)
if leftNil && rightNil {
return match.NewText(r.Str)
}
_, leftSuper := m.Left.(match.Super)
lp, leftPrefix := m.Left.(match.Prefix)
la, leftAny := m.Left.(match.Any)
_, rightSuper := m.Right.(match.Super)
rs, rightSuffix := m.Right.(match.Suffix)
ra, rightAny := m.Right.(match.Any)
switch {
case leftSuper && rightSuper:
return match.NewContains(r.Str, false)
case leftSuper && rightNil:
return match.NewSuffix(r.Str)
case rightSuper && leftNil:
return match.NewPrefix(r.Str)
case leftNil && rightSuffix:
return match.NewPrefixSuffix(r.Str, rs.Suffix)
case rightNil && leftPrefix:
return match.NewPrefixSuffix(lp.Prefix, r.Str)
case rightNil && leftAny:
return match.NewSuffixAny(r.Str, la.Separators)
case leftNil && rightAny:
return match.NewPrefixAny(r.Str, ra.Separators)
}
return m
}
return matcher
}
func compileMatchers(matchers []match.Matcher) (match.Matcher, error) {
if len(matchers) == 0 {
return nil, fmt.Errorf("compile error: need at least one matcher")
}
if len(matchers) == 1 {
return matchers[0], nil
}
if m := glueMatchers(matchers); m != nil {
return m, nil
}
idx := -1
maxLen := -1
var val match.Matcher
for i, matcher := range matchers {
if l := matcher.Len(); l != -1 && l >= maxLen {
maxLen = l
idx = i
val = matcher
}
}
if val == nil { // not found matcher with static length
r, err := compileMatchers(matchers[1:])
if err != nil {
return nil, err
}
return match.NewBTree(matchers[0], nil, r), nil
}
left := matchers[:idx]
var right []match.Matcher
if len(matchers) > idx+1 {
right = matchers[idx+1:]
}
var l, r match.Matcher
var err error
if len(left) > 0 {
l, err = compileMatchers(left)
if err != nil {
return nil, err
}
}
if len(right) > 0 {
r, err = compileMatchers(right)
if err != nil {
return nil, err
}
}
return match.NewBTree(val, l, r), nil
}
func glueMatchers(matchers []match.Matcher) match.Matcher {
if m := glueMatchersAsEvery(matchers); m != nil {
return m
}
if m := glueMatchersAsRow(matchers); m != nil {
return m
}
return nil
}
func glueMatchersAsRow(matchers []match.Matcher) match.Matcher {
if len(matchers) <= 1 {
return nil
}
var (
c []match.Matcher
l int
)
for _, matcher := range matchers {
if ml := matcher.Len(); ml == -1 {
return nil
} else {
c = append(c, matcher)
l += ml
}
}
return match.NewRow(l, c...)
}
func glueMatchersAsEvery(matchers []match.Matcher) match.Matcher {
if len(matchers) <= 1 {
return nil
}
var (
hasAny bool
hasSuper bool
hasSingle bool
min int
separator []rune
)
for i, matcher := range matchers {
var sep []rune
switch m := matcher.(type) {
case match.Super:
sep = []rune{}
hasSuper = true
case match.Any:
sep = m.Separators
hasAny = true
case match.Single:
sep = m.Separators
hasSingle = true
min++
case match.List:
if !m.Not {
return nil
}
sep = m.List
hasSingle = true
min++
default:
return nil
}
// initialize
if i == 0 {
separator = sep
}
if runes.Equal(sep, separator) {
continue
}
return nil
}
if hasSuper && !hasAny && !hasSingle {
return match.NewSuper()
}
if hasAny && !hasSuper && !hasSingle {
return match.NewAny(separator)
}
if (hasAny || hasSuper) && min > 0 && len(separator) == 0 {
return match.NewMin(min)
}
every := match.NewEveryOf()
if min > 0 {
every.Add(match.NewMin(min))
if !hasAny && !hasSuper {
every.Add(match.NewMax(min))
}
}
if len(separator) > 0 {
every.Add(match.NewContains(string(separator), true))
}
return every
}
func minimizeMatchers(matchers []match.Matcher) []match.Matcher {
var done match.Matcher
var left, right, count int
for l := 0; l < len(matchers); l++ {
for r := len(matchers); r > l; r-- {
if glued := glueMatchers(matchers[l:r]); glued != nil {
var swap bool
if done == nil {
swap = true
} else {
cl, gl := done.Len(), glued.Len()
swap = cl > -1 && gl > -1 && gl > cl
swap = swap || count < r-l
}
if swap {
done = glued
left = l
right = r
count = r - l
}
}
}
}
if done == nil {
return matchers
}
next := append(append([]match.Matcher{}, matchers[:left]...), done)
if right < len(matchers) {
next = append(next, matchers[right:]...)
}
if len(next) == len(matchers) {
return next
}
return minimizeMatchers(next)
}
// minimizeAnyOf tries to apply some heuristics to minimize number of nodes in given tree
func minimizeTree(tree *ast.Node) *ast.Node {
switch tree.Kind {
case ast.KindAnyOf:
return minimizeTreeAnyOf(tree)
default:
return nil
}
}
// minimizeAnyOf tries to find common children of given node of AnyOf pattern
// it searches for common children from left and from right
// if any common children are found – then it returns new optimized ast tree
// else it returns nil
func minimizeTreeAnyOf(tree *ast.Node) *ast.Node {
if !areOfSameKind(tree.Children, ast.KindPattern) {
return nil
}
commonLeft, commonRight := commonChildren(tree.Children)
commonLeftCount, commonRightCount := len(commonLeft), len(commonRight)
if commonLeftCount == 0 && commonRightCount == 0 { // there are no common parts
return nil
}
var result []*ast.Node
if commonLeftCount > 0 {
result = append(result, ast.NewNode(ast.KindPattern, nil, commonLeft...))
}
var anyOf []*ast.Node
for _, child := range tree.Children {
reuse := child.Children[commonLeftCount : len(child.Children)-commonRightCount]
var node *ast.Node
if len(reuse) == 0 {
// this pattern is completely reduced by commonLeft and commonRight patterns
// so it become nothing
node = ast.NewNode(ast.KindNothing, nil)
} else {
node = ast.NewNode(ast.KindPattern, nil, reuse...)
}
anyOf = appendIfUnique(anyOf, node)
}
switch {
case len(anyOf) == 1 && anyOf[0].Kind != ast.KindNothing:
result = append(result, anyOf[0])
case len(anyOf) > 1:
result = append(result, ast.NewNode(ast.KindAnyOf, nil, anyOf...))
}
if commonRightCount > 0 {
result = append(result, ast.NewNode(ast.KindPattern, nil, commonRight...))
}
return ast.NewNode(ast.KindPattern, nil, result...)
}
func commonChildren(nodes []*ast.Node) (commonLeft, commonRight []*ast.Node) {
if len(nodes) <= 1 {
return
}
// find node that has least number of children
idx := leastChildren(nodes)
if idx == -1 {
return
}
tree := nodes[idx]
treeLength := len(tree.Children)
// allocate max able size for rightCommon slice
// to get ability insert elements in reverse order (from end to start)
// without sorting
commonRight = make([]*ast.Node, treeLength)
lastRight := treeLength // will use this to get results as commonRight[lastRight:]
var (
breakLeft bool
breakRight bool
commonTotal int
)
for i, j := 0, treeLength-1; commonTotal < treeLength && j >= 0 && !(breakLeft && breakRight); i, j = i+1, j-1 {
treeLeft := tree.Children[i]
treeRight := tree.Children[j]
for k := 0; k < len(nodes) && !(breakLeft && breakRight); k++ {
// skip least children node
if k == idx {
continue
}
restLeft := nodes[k].Children[i]
restRight := nodes[k].Children[j+len(nodes[k].Children)-treeLength]
breakLeft = breakLeft || !treeLeft.Equal(restLeft)
// disable searching for right common parts, if left part is already overlapping
breakRight = breakRight || (!breakLeft && j <= i)
breakRight = breakRight || !treeRight.Equal(restRight)
}
if !breakLeft {
commonTotal++
commonLeft = append(commonLeft, treeLeft)
}
if !breakRight {
commonTotal++
lastRight = j
commonRight[j] = treeRight
}
}
commonRight = commonRight[lastRight:]
return
}
func appendIfUnique(target []*ast.Node, val *ast.Node) []*ast.Node {
for _, n := range target {
if reflect.DeepEqual(n, val) {
return target
}
}
return append(target, val)
}
func areOfSameKind(nodes []*ast.Node, kind ast.Kind) bool {
for _, n := range nodes {
if n.Kind != kind {
return false
}
}
return true
}
func leastChildren(nodes []*ast.Node) int {
min := -1
idx := -1
for i, n := range nodes {
if idx == -1 || (len(n.Children) < min) {
min = len(n.Children)
idx = i
}
}
return idx
}
func compileTreeChildren(tree *ast.Node, sep []rune) ([]match.Matcher, error) {
var matchers []match.Matcher
for _, desc := range tree.Children {
m, err := compile(desc, sep)
if err != nil {
return nil, err
}
matchers = append(matchers, optimizeMatcher(m))
}
return matchers, nil
}
func compile(tree *ast.Node, sep []rune) (m match.Matcher, err error) {
switch tree.Kind {
case ast.KindAnyOf:
// todo this could be faster on pattern_alternatives_combine_lite (see glob_test.go)
if n := minimizeTree(tree); n != nil {
return compile(n, sep)
}
matchers, err := compileTreeChildren(tree, sep)
if err != nil {
return nil, err
}
return match.NewAnyOf(matchers...), nil
case ast.KindPattern:
if len(tree.Children) == 0 {
return match.NewNothing(), nil
}
matchers, err := compileTreeChildren(tree, sep)
if err != nil {
return nil, err
}
m, err = compileMatchers(minimizeMatchers(matchers))
if err != nil {
return nil, err
}
case ast.KindAny:
m = match.NewAny(sep)
case ast.KindSuper:
m = match.NewSuper()
case ast.KindSingle:
m = match.NewSingle(sep)
case ast.KindNothing:
m = match.NewNothing()
case ast.KindList:
l := tree.Value.(ast.List)
m = match.NewList([]rune(l.Chars), l.Not)
case ast.KindRange:
r := tree.Value.(ast.Range)
m = match.NewRange(r.Lo, r.Hi, r.Not)
case ast.KindText:
t := tree.Value.(ast.Text)
m = match.NewText(t.Text)
default:
return nil, fmt.Errorf("could not compile tree: unknown node type")
}
return optimizeMatcher(m), nil
}
func Compile(tree *ast.Node, sep []rune) (match.Matcher, error) {
m, err := compile(tree, sep)
if err != nil {
return nil, err
}
return m, nil
}
+124 -46
View File
@@ -1,80 +1,158 @@
package glob
import (
"github.com/gobwas/glob/compiler"
"fmt"
"github.com/gobwas/glob/internal/debug"
"github.com/gobwas/glob/syntax"
)
// Glob represents compiled glob pattern.
type Glob interface {
Match(string) bool
// SyntaxError is returned by [Compile] when the given pattern can not be
// parsed. Offset points at the place in the pattern the error was detected
// at, so the tooling can do things like:
//
// {a,b
// ----^ unclosed `{`
type SyntaxError struct {
// Offset is a byte offset in the pattern.
Offset int
// Reason describes the error.
Reason string
}
// Compile creates Glob for given pattern and strings (if any present after pattern) as separators.
func (s *SyntaxError) Error() string {
return fmt.Sprintf("glob: syntax error at %d: %s", s.Offset, s.Reason)
}
// Pattern represents a compiled glob pattern.
//
// A pattern is compiled into a tree of matchers:
//
// `a` => "a"
// `a*` => ["a"·*]
// `{a*,b}` => {["a"·*]|"b"}
//
// Matching is a backtracking walk over that tree; see [Pattern.Match].
type Pattern struct {
// str is the pattern text the Pattern was compiled from; see
// [Pattern.String].
str string
// sep are the separators the Pattern was compiled with; see
// [Pattern.Separators].
sep []rune
// m is the root of the matcher tree; see [matcher].
m matcher
// state tells whether matching m needs the backtracking state, that
// is, whether it may save checkpoints; see [needsState]. A pattern
// without them is matched with a plain call chain.
state bool
// The match preconditions: every matching string is at least minLen
// bytes and ends with suffix. They fail the obvious mismatches in O(1)
// instead of a backtracking walk -- e.g. `a*a*a*b` requires the
// trailing `b`, no matter how the stars go.
//
// A precondition pays off only when it catches a mismatch earlier than
// the walk would, which is why:
//
// - there is no required prefix: the walk is left-to-right, so a
// leading literal is the first thing checked anyway, while a bad
// suffix or length is discovered last, after the whole
// backtracking exploration;
//
// - they are computed for the stateful patterns only: a stateless
// pattern is a plain call chain whose matchers perform these very
// checks themselves (e.g. suffixMatcher is a HasSuffix), so the
// precondition would only duplicate them.
minLen int
suffix string
}
// String returns the source text used to compile the pattern, the same way
// [regexp.Regexp.String] does.
//
// Note that separators are not part of String: they are given to Compile
// alongside the pattern text.
func (p *Pattern) String() string {
return p.str
}
// Separators returns the separators the pattern was compiled with, in the
// order they were given to Compile; nil when there are none.
//
// The returned slice is the very one given to Compile, sharing its backing
// array: it is not copied on the way in or out. Matching does not use it.
func (p *Pattern) Separators() []rune {
return p.sep
}
func init() {
// The matcher tree is unexported; hand its rendering to the in-module
// tooling (cmd/globtest -v) without widening the public API.
debug.Tree = func(p any) string {
return p.(*Pattern).m.String()
}
}
// Compile compiles the glob pattern. The separators, if given, are the
// characters `*` and `?` do not match (`**` does); they can not be changed
// after the compilation, see [Pattern.Separators]. A malformed pattern is
// reported with a [*SyntaxError].
//
// The pattern syntax is:
//
// pattern:
// { term }
// pattern:
// { term }
//
// term:
// `*` matches any sequence of non-separator characters
// `**` matches any sequence of characters
// `?` matches any single non-separator character
// `[` [ `!` ] { character-range } `]`
// character class (must be non-empty)
// `{` pattern-list `}`
// pattern alternatives
// c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
// `\` c matches character c
// term:
// `*` matches any sequence of non-separator characters
// `**` matches any sequence of characters
// `?` matches any single non-separator character
// `[` [ `!` ] class `]`
// character class; `!` negates it
// `{` pattern-list `}`
// pattern alternatives
// c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
// `\` c matches character c
//
// character-range:
// c matches character c (c != `\\`, `-`, `]`)
// `\` c matches character c
// lo `-` hi matches character c for lo <= c <= hi
// class:
// lo `-` hi matches character c for lo <= c <= hi
// { c } matches any of the listed characters (c != `\`, `]`;
// `\` c matches c, `-` is literal here); must be non-empty
//
// pattern-list:
// pattern { `,` pattern }
// comma-separated (without spaces) patterns
//
func Compile(pattern string, separators ...rune) (Glob, error) {
ast, err := syntax.Parse(pattern)
if err != nil {
return nil, err
}
matcher, err := compiler.Compile(ast, separators)
if err != nil {
return nil, err
}
return matcher, nil
// pattern-list:
// pattern { `,` pattern }
// comma-separated (without spaces) patterns
func Compile(pattern string, separators ...rune) (*Pattern, error) {
return compile(pattern, separators)
}
// MustCompile is the same as Compile, except that if Compile returns error, this will panic
func MustCompile(pattern string, separators ...rune) Glob {
// MustCompile is the same as Compile, except that if Compile returns error,
// this will panic.
func MustCompile(pattern string, separators ...rune) *Pattern {
g, err := Compile(pattern, separators...)
if err != nil {
panic(err)
}
return g
}
// QuoteMeta returns a string that quotes all glob pattern meta characters
// inside the argument text; For example, QuoteMeta(`{foo*}`) returns `\[foo\*\]`.
// QuoteMeta returns a copy of the s having all glob meta characters escaped.
func QuoteMeta(s string) string {
// 2 is a pessimistic way of allocating an extra byte per each byte in s.
b := make([]byte, 2*len(s))
// a byte loop is correct because all meta characters are ASCII
j := 0
// A byte loop is correct here because all meta characters are ASCII.
for i := 0; i < len(s); i++ {
if syntax.Special(s[i]) {
if syntax.IsSpecial(s[i]) {
b[j] = '\\'
j++
}
b[j] = s[i]
j++
}
return string(b[0:j])
}
+8
View File
@@ -0,0 +1,8 @@
//go:build !globdebug
// +build !globdebug
package debug
const Enabled = false
func Printf(f string, args ...any) {}
+14
View File
@@ -0,0 +1,14 @@
//go:build globdebug
// +build globdebug
package debug
import (
"fmt"
)
const Enabled = true
func Printf(f string, args ...any) {
fmt.Printf(f, args...)
}
+8
View File
@@ -0,0 +1,8 @@
package debug
// Tree renders the matcher tree of a compiled *glob.Pattern.
//
// It is set by package glob at init time: the pattern internals are
// unexported, and this keeps them so while letting the in-module tooling
// (cmd/globtest -v) print them. The format is not stable.
var Tree func(pattern any) string
+788
View File
@@ -0,0 +1,788 @@
package glob
import (
"fmt"
"slices"
"strconv"
"strings"
"sync"
"unicode/utf8"
"unsafe"
"github.com/gobwas/glob/internal/debug"
)
// Match reports whether s matches the pattern.
func (p *Pattern) Match(s string) bool {
var x matchContext
if p.state {
if len(s) < p.minLen || !strings.HasSuffix(s, p.suffix) {
return false
}
state := acquireState()
defer releaseState(state)
x.state = state
}
for {
n, match := p.m.Match(x, s[x.offset:])
// Note: debug.Enabled is a build-tag constant; when it is false the
// whole block (including the argument evaluation) is compiled away.
if debug.Enabled && x.state != nil {
debug.Printf("stack: %s\n", formatStack(x.state.stack))
debug.Printf("stars: %s\n", formatStack(x.state.stars))
}
if match && n == len(s[x.offset:]) {
if debug.Enabled {
debug.Printf("match!\n")
}
return true
}
if x.state == nil {
// The pattern never saves checkpoints (see [needsState]):
// nothing to backtrack to.
return false
}
var (
c checkpoint
k checkpointKind
)
switch {
case len(x.state.stars) > 0:
if debug.Enabled {
debug.Printf("has star\n")
}
c = popLast(&x.state.stars)
k = checkpointStars
case len(x.state.stack) > 0:
if debug.Enabled {
debug.Printf("has stack\n")
}
c = popLast(&x.state.stack)
k = checkpointStack
default:
if debug.Enabled {
debug.Printf("no match\n")
}
return false
}
x.offset = c.offset
x.kind = k
x.frame = frame{
path: c.path,
depth: 0,
}
}
}
// matcher is a node of the tree a pattern compiles into.
//
// Match matches the beginning of s and reports how many bytes it consumed;
// the caller goes on with the rest. A matcher may consume nothing and still
// match: a void does, and so does a non-terminal star -- it stores its
// restart points instead and lets the walk continue, to be resumed from one
// of them on a mismatch later; see [Pattern.Match]. The context tells where
// in the input and in the tree the matcher is; see [matchContext].
//
// String renders the node for the debug output and cmd/globtest -v; the
// notation is described at [Pattern].
type matcher interface {
Match(matchContext, string) (n int, matched bool)
String() string
}
// frame tells where in the matcher tree the walk currently is: the path from
// the root down to the current node, and the depth of the node.
//
// A checkpoint stores the frame's path; resuming it re-enters the tree from
// the root and follows the path back to the very node that saved it (an
// alternative to try, or a star to restart) -- see [Pattern.Match],
// [multiMatcher.Match] and [altMatcher.Match].
//
// For `{a*b,c}y`, compiled into [{["a"·*·"b"]|"c"}·"y"], the frame at each
// node the walk visits is:
//
// node path depth
// [{["a"·*·"b"]|"c"}·"y"] [] 0 the root
// {["a"·*·"b"]|"c"} [0]* 1
// ["a"·*·"b"] [0 0]* 2
// "a" [0 0 0]* 3
// * [0 0 1] 3
// "b" [0 0 2] 3
// "c" [0 1] 2 reached only by resuming the
// checkpoint the alt saved
// "y" [1] 1
//
// * the zeros are virtual: turning to child #0 records nothing, so the
// path actually stored is [] -- see path below.
type frame struct {
// path addresses the current node: path[d] is the index of the child taken
// at depth d, that is, in the multiMatcher or altMatcher d levels below
// the root.
//
// For `{a*b,c}y`, compiled into [{["a"·*·"b"]|"c"}·"y"]:
//
// [0 1] the "c" alternative -- what the alt checkpoints when it
// enters ["a"·*·"b"], to be tried on a mismatch
//
// [0 0 1] the star: child #1 of ["a"·*·"b"], which is alternative #0
// of the alt, which is child #0 of the root sequence -- what
// the star's restart points carry
//
// It is recorded lazily: an index is stored only when the walk turns to a
// child other than #0 (see [matchContext.branch]), so the path may be
// shorter than the current depth -- the missing trailing entries are
// implicitly zero, and [frame.index] reads them as such. In the example
// above, at "a" the path is still empty, not [0 0 0]: the alt, alternative
// #0 and "a" were all entered as child #0.
path []int
// depth is the depth of the current node (the one [frame.path] leads to):
// 0 at the root, 1 at its children, and so on; [matchContext.next]
// increments it on every descent. It is also where the node's own entry in
// path lives: path[depth] is the child to take next -- [frame.index] reads
// it, [matchContext.branch] writes it.
//
// It is not len(path): the path is recorded lazily, so it may fall short
// of the depth, and, when resuming a checkpoint, it is the whole path of
// the checkpoint, reaching beyond the depth all the way down to the node
// to resume at.
//
// To say it the other way, depth is what len(path) would be were the path
// always recorded in full -- the virtual zeros included -- and cut at the
// current node: the length of the node's full address in the tree.
//
// For `{a*b,c}y`, compiled into [{["a"·*·"b"]|"c"}·"y"], the nodes at
// each depth are:
//
// 0 [{["a"·*·"b"]|"c"}·"y"] the root sequence
// 1 {["a"·*·"b"]|"c"}, "y" its children
// 2 ["a"·*·"b"], "c" the alternatives
// 3 "a", *, "b" the children of ["a"·*·"b"]
depth int
}
// index returns the index of the child to take at the current node: the one
// the path leads to when resuming a checkpoint, #0 otherwise.
func (v frame) index() int {
if v.depth >= len(v.path) {
return 0
}
return v.path[v.depth]
}
// checkpoint is a place to resume the walk from on a mismatch.
type checkpoint struct {
// offset is the position in the input to resume at.
offset int
// path leads to the node that saved the checkpoint; see [frame].
//
// Unlike a live frame's, it is always at full length -- the virtual zeros
// written out -- so len(path) is the depth of that node, and a checkpoint
// needs no depth of its own: resuming starts at the root and tells it has
// arrived by comparing the walk's depth against len(path); see
// [matchContext.branch], [matchContext.storeStar] and [altMatcher.Match].
path []int
}
// checkpointKind tells which pile a checkpoint was taken from during the
// backtracking in [Pattern.Match].
type checkpointKind int
const (
// checkpointStack is an alternative checkpoint saved by altMatcher.
checkpointStack checkpointKind = iota
// checkpointStars is a star restart point saved by starMatcher.
checkpointStars
)
// matchContext is what a matcher is called with: where in the input and in
// the tree it is, plus the backtracking state shared by the whole walk. It
// is passed by value, so a matcher's changes to it are seen by its
// descendants only.
type matchContext struct {
// offset is the position in the whole input the current node matches from.
// The matchers see only the remainder of the input, so it is what a
// checkpoint records to resume at the same place; see [checkpoint].
offset int
// frame is where in the matcher tree the current node is; see [frame].
frame frame
// state holds the checkpoint piles and the path arena shared by the
// whole walk.
state *matchState
// kind tells which pile the checkpoint being resumed was taken from.
// See [altMatcher.Match] for its use.
kind checkpointKind
// starsFloor is the number of star restart points that existed when
// the walk entered the current alternative. The entries below it were
// born outside of the alternative and must not be discarded by the
// stars inside it; see [matchContext.storeStar].
starsFloor int
}
// push saves an alternative checkpoint at the current offset for the node f
// leads to; see [altMatcher.Match].
func (x matchContext) push(f frame) {
x.state.stack = append(x.state.stack, checkpoint{
offset: x.offset,
path: f.path,
})
if debug.Enabled {
debug.Printf(
"checkpoint offset=%d path=%v\n",
x.offset, f.path,
)
}
}
// storeStar saves a restart point for the current star at offset bytes
// further in the input; reset tells whether the star may discard the pending
// restart points first, see below.
func (x matchContext) storeStar(offset int, reset bool) {
path := x.frame.path
if d := x.frame.depth; len(path) < d {
// The walk records an index in path only when it turns to a child
// other than the first one; levels entered at child #0 are implicit.
// Store the path at its full length (the missing entries are always
// zeros) so that the alts above can tell this checkpoint from their
// own. See [altMatcher.Match].
p := x.state.allocPath(d)
copy(p, path)
path = p
}
if reset {
// This star can extend over anything the pending restart points could
// reach -- they are redundant, discard them. See
// research.swtch.com/glob.
//
// However, only the restart points born inside the current alternative
// may be discarded. An outer star, when resumed, re-enters the
// enclosing alt and may pick another alternative -- something this
// star, locked inside its own alternative, can not absorb. See the
// `*{*0,}` test: the outer star must survive the inner one to reach
// the empty alternative.
x.state.stars = x.state.stars[:x.starsFloor]
}
x.state.stars = append(x.state.stars, checkpoint{
offset: x.offset + offset,
path: path,
})
if debug.Enabled {
debug.Printf(
"star offset=%d path=%v reset=%t\n",
x.offset+offset, path, reset,
)
}
}
// next returns the context for a child of the current node, matching offset
// bytes further in the input: one level deeper in the tree.
func (x matchContext) next(offset int) matchContext {
x.offset = x.offset + offset
x.frame.depth += 1
return x
}
// branch returns a copy of the current frame with its path turned to child i
// at the current level, discarding the deeper levels. The new path is
// allocated from the state's arena.
func (x matchContext) branch(i int) frame {
f := x.frame
path := x.state.allocPath(f.depth + 1)
copy(path, f.path)
path[f.depth] = i
f.path = path
return f
}
// matchState holds the backtracking state of a single [Pattern.Match] call.
// The states are pooled globally: the buffers keep their grown capacity
// between the matches, so a steady-state Match does not allocate them.
type matchState struct {
// stars are the star restart points and stack the alternative
// checkpoints, both LIFO. On a mismatch the walk resumes from the most
// recent restart point, if any, before the most recent alternative; see
// [Pattern.Match].
stars []checkpoint
stack []checkpoint
// arena is the buffer the checkpoint paths are allocated from; see
// [matchState.allocPath]. It is bulk-freed when the match ends, which
// spares the per-path lifetime reasoning: a path may be shared between
// the current frame and several checkpoints.
arena []int
}
// allocPath returns a zeroed []int of length n allocated from the state's
// arena. When the arena runs out of capacity, a fresh chunk is started; the
// paths allocated from the previous chunks stay valid, since the chunks are
// kept alive by the paths referencing them.
func (st *matchState) allocPath(n int) []int {
if cap(st.arena)-len(st.arena) < n {
st.arena = make([]int, 0, max(2*cap(st.arena), n, 32))
}
p := st.arena[len(st.arena) : len(st.arena)+n : len(st.arena)+n]
st.arena = st.arena[:len(st.arena)+n]
clear(p)
return p
}
var statePool sync.Pool // Pool[*matchState]
// acquireState takes a state from the pool, or makes a new one.
func acquireState() *matchState {
if st, _ := statePool.Get().(*matchState); st != nil {
return st
}
return &matchState{}
}
// releaseState empties the state and puts it back to the pool.
func releaseState(st *matchState) {
resetCheckpoints(&st.stars)
resetCheckpoints(&st.stack)
// The arena holds no references; keep the (largest) chunk as is.
st.arena = st.arena[:0]
statePool.Put(st)
}
// resetCheckpoints empties s keeping its capacity. The whole backing array
// is zeroed (not only the live part) to drop the references to the
// checkpoint paths popped during the match.
func resetCheckpoints(s *[]checkpoint) {
full := (*s)[:cap(*s)]
clear(full)
*s = full[:0]
}
// multiMatcher is a sequence, ["a"·*·"b"]: it matches its children one
// after another, each on the input the previous ones left.
type multiMatcher []matcher
func (ms multiMatcher) String() string {
var sb strings.Builder
sb.WriteByte('[')
for i, m := range ms {
if i > 0 {
sb.WriteString("·")
}
sb.WriteString(m.String())
}
sb.WriteByte(']')
return sb.String()
}
func (ms multiMatcher) Match(x matchContext, s string) (n int, ok bool) {
for i := x.frame.index(); i < len(ms); i++ {
if i != x.frame.index() && x.state != nil {
// The path is recorded for the checkpoints the descendants may
// save; in a stateless walk (see [needsState]) there are none
// and nobody would ever read it.
x.frame = x.branch(i)
}
child := x.next(n)
k, ok := ms[i].Match(child, s[n:])
if debug.Enabled {
debug.Printf(
"[%T@%p] #%d match %#q against %[5]T(%[5]s) at path=%v depth=%d => %d %t\n",
ms, unsafe.SliceData(ms), i, s[n:], ms[i],
child.frame.path, child.frame.depth, k, ok,
)
}
if !ok {
return 0, false
}
n += k
}
return n, true
}
// altMatcher is a group of alternatives, {"a"|"b"}: it matches the one the
// walk is at (the first one when entered anew), having saved a checkpoint
// for the next one to be tried on a mismatch later.
type altMatcher []matcher
func (ms altMatcher) String() string {
var sb strings.Builder
sb.WriteByte('{')
for i, m := range ms {
if i > 0 {
sb.WriteString("|")
}
sb.WriteString(m.String())
}
sb.WriteByte('}')
return sb.String()
}
func (ms altMatcher) Match(x matchContext, s string) (int, bool) {
i := x.frame.index()
// Save a checkpoint for the next alternative to consider it in case of
// a mismatch later (if any). This must be done only when:
//
// - the alt is entered for the first time (the resume path ends above
// this level, or there is none);
//
// - the resume path ends exactly at this level with an alternative
// checkpoint -- its job is "try alternative #i", so the one for the
// next alternative must be saved now. Note that a star restart point
// may end at this level too (a star being a direct child of the alt,
// as in `{*,b}`) -- it must not trigger a save.
//
// Otherwise the walk is merely passing through this alt on its way to
// resume a deeper checkpoint -- the one for the next alternative was
// already saved when the alt was entered for the first time, and saving
// it again on every star restart would blow the stack up exponentially.
// See the "alternatives" tests.
if next := i + 1; next < len(ms) {
d := len(x.frame.path)
if x.frame.depth >= d || (x.frame.depth == d-1 && x.kind == checkpointStack) {
x.push(x.branch(next))
}
}
// The stars below this level may only discard the restart points born
// inside the same alternative; see [matchContext.storeStar].
x.starsFloor = len(x.state.stars)
child := x.next(0)
n, match := ms[i].Match(child, s)
if debug.Enabled {
debug.Printf(
"[%T@%p] #%d match %#q against %[5]T(%[5]s) at path=%v depth=%d => %d %t\n",
ms, unsafe.SliceData(ms), i, s, ms[i],
child.frame.path, child.frame.depth, n, match,
)
}
return n, match
}
// textMatcher is a literal, "abc".
type textMatcher struct {
Text string
}
func (m *textMatcher) String() string {
return strconv.Quote(m.Text)
}
func (m *textMatcher) Match(_ matchContext, s string) (int, bool) {
if strings.HasPrefix(s, m.Text) {
return len(m.Text), true
}
return 0, false
}
// charMatcher is a `?`: any single character but a separator.
type charMatcher struct {
Sep []rune
}
func (m *charMatcher) String() string {
var sb strings.Builder
sb.WriteByte('?')
if len(m.Sep) > 0 {
sb.WriteByte('(')
formatRunes(&sb, m.Sep)
sb.WriteByte(')')
}
return sb.String()
}
func (m *charMatcher) Match(_ matchContext, s string) (int, bool) {
if len(s) == 0 {
return 0, false
}
r, n := utf8.DecodeRuneInString(s)
if slices.Contains(m.Sep, r) {
return 0, false
}
return n, true
}
// starMatcher is a `*` or a `**`: any sequence of characters, but for the
// separators in the former case.
type starMatcher struct {
// Sep are the separators the star may not extend over; empty for `**`.
Sep []rune
// SepStr is Sep as a string, for the byte-wise scans below.
SepStr string
// Next is the literal the matcher right after this star begins with
// (when it is a textMatcher), set by [annotateStars]. A restart point
// at a position where the literal does not occur is a guaranteed
// mismatch, so the star jumps between its occurrences instead of
// retrying at every rune.
Next string
// Terminal is set by [annotateStars] when nothing follows this star
// anywhere in the pattern: the star then consumes everything in its
// reach at once, and no restart point can change the outcome.
Terminal bool
}
// reach returns the length of the prefix of s the star may extend over:
// everything up to the nearest separator.
func (m *starMatcher) reach(s string) int {
if m.SepStr == "" {
return len(s)
}
if e := strings.IndexAny(s, m.SepStr); e >= 0 {
return e
}
return len(s)
}
// storeSkip stores the restart point at the next occurrence of the m.Next
// literal instead of the next rune.
func (m *starMatcher) storeSkip(x matchContext, s string) {
reach := m.reach(s)
// Look for the occurrences starting within the star's reach; the
// literal itself may extend past it (it may contain the separators).
// Note that a valid UTF-8 literal can not match at a mid-rune
// position, so the one-byte skip below is rune-safe.
end := min(reach+len(m.Next), len(s))
j := strings.Index(s[1:end], m.Next)
if j < 0 || 1+j > reach {
return
}
x.storeStar(1+j, len(m.Sep) == 0)
}
func (m *starMatcher) String() string {
var sb strings.Builder
sb.WriteByte('*')
if len(m.Sep) > 0 {
sb.WriteByte('(')
formatRunes(&sb, m.Sep)
sb.WriteByte(')')
}
return sb.String()
}
func (m *starMatcher) Match(x matchContext, s string) (int, bool) {
if m.Terminal {
// Nothing follows this star in the pattern: either it consumes
// the whole remainder within its reach, or the match fails.
return m.reach(s), true
}
if len(s) == 0 {
return 0, true
}
if m.Next != "" {
m.storeSkip(x, s)
return 0, true
}
r, n := utf8.DecodeRuneInString(s)
if !slices.Contains(m.Sep, r) {
// The star may extend over the rune: save the restart point past
// it. A separator-free star (`**`) can extend over anything the
// pending restart points could reach, so it may discard them; see
// [matchContext.storeStar].
x.storeStar(n, len(m.Sep) == 0)
}
return 0, true
}
// runeRangeMatcher is a character range class, `[a-z]` or `[!a-z]`.
type runeRangeMatcher struct {
Lo rune
Hi rune
Not bool
}
func (m *runeRangeMatcher) String() string {
var sb strings.Builder
if m.Not {
sb.WriteByte('!')
}
sb.WriteByte('[')
sb.WriteRune(m.Lo)
sb.WriteByte('-')
sb.WriteRune(m.Hi)
sb.WriteByte(']')
return sb.String()
}
func (m *runeRangeMatcher) Match(_ matchContext, s string) (int, bool) {
// Note that an invalid byte decodes as U+FFFD, and is matched as such,
// the same way regexp does; only the empty input is a mismatch.
r, n := utf8.DecodeRuneInString(s)
if n == 0 {
return 0, false
}
ok := m.Lo <= r && r <= m.Hi
if ok != m.Not {
return n, true
}
return 0, false
}
// runeSetMatcher is a character set class, `[abc]` or `[!abc]`.
type runeSetMatcher struct {
Set map[rune]struct{}
Not bool
}
// formatRunes writes rs, sorted and comma-separated, to sb.
func formatRunes(sb *strings.Builder, rs []rune) {
rs = slices.Clone(rs) // Not to reorder the caller's, e.g. a matcher's Sep.
slices.Sort(rs)
for i, r := range rs {
if i > 0 {
sb.WriteByte(',')
}
sb.WriteRune(r)
}
}
func (m *runeSetMatcher) String() string {
rs := make([]rune, 0, len(m.Set))
for r := range m.Set {
rs = append(rs, r)
}
var sb strings.Builder
if m.Not {
sb.WriteByte('!')
}
sb.WriteByte('[')
formatRunes(&sb, rs)
sb.WriteByte(']')
return sb.String()
}
func (m *runeSetMatcher) Match(_ matchContext, s string) (int, bool) {
// See the note in runeRangeMatcher.Match.
r, n := utf8.DecodeRuneInString(s)
if n == 0 {
return 0, false
}
if _, has := m.Set[r]; has != m.Not {
return n, true
}
return 0, false
}
// voidMatcher matches the empty string: an empty alternative, `{a,}`. In a
// sequence it is dropped by [normalizeSequence].
type voidMatcher struct{}
func (*voidMatcher) String() string {
return "void"
}
func (*voidMatcher) Match(matchContext, string) (int, bool) {
return 0, true
}
// The shaped matchers below are the compile-time rewrites of the common
// terminal sub-sequences; see [specialize]. Nothing may follow them in the
// pattern, so each one either consumes the whole remainder of the input or
// fails -- deterministically, storing no checkpoints.
// prefixMatcher is a terminal `abc*`.
type prefixMatcher struct {
Text string
Sep string
}
func (m *prefixMatcher) String() string {
return "prefix(" + strconv.Quote(m.Text) + ")"
}
func (m *prefixMatcher) Match(_ matchContext, s string) (int, bool) {
if strings.HasPrefix(s, m.Text) && noSep(s[len(m.Text):], m.Sep) {
return len(s), true
}
return 0, false
}
// suffixMatcher is a terminal `*abc`.
type suffixMatcher struct {
Text string
Sep string
}
func (m *suffixMatcher) String() string {
return "suffix(" + strconv.Quote(m.Text) + ")"
}
func (m *suffixMatcher) Match(_ matchContext, s string) (int, bool) {
if strings.HasSuffix(s, m.Text) && noSep(s[:len(s)-len(m.Text)], m.Sep) {
return len(s), true
}
return 0, false
}
// prefixSuffixMatcher is a terminal `abc*def`.
type prefixSuffixMatcher struct {
Prefix string
Suffix string
Sep string
}
func (m *prefixSuffixMatcher) String() string {
return "prefix_suffix(" + strconv.Quote(m.Prefix) + "," + strconv.Quote(m.Suffix) + ")"
}
func (m *prefixSuffixMatcher) Match(_ matchContext, s string) (int, bool) {
// The length check keeps the prefix and the suffix from overlapping:
// `a*ant` must not match `ant`.
if len(s) >= len(m.Prefix)+len(m.Suffix) &&
strings.HasPrefix(s, m.Prefix) &&
strings.HasSuffix(s, m.Suffix) &&
noSep(s[len(m.Prefix):len(s)-len(m.Suffix)], m.Sep) {
return len(s), true
}
return 0, false
}
// containsMatcher is a terminal `*abc*` with the separator-free stars.
type containsMatcher struct {
Text string
}
func (m *containsMatcher) String() string {
return "contains(" + strconv.Quote(m.Text) + ")"
}
func (m *containsMatcher) Match(_ matchContext, s string) (int, bool) {
if strings.Contains(s, m.Text) {
return len(s), true
}
return 0, false
}
// noSep reports whether s contains none of the separators.
func noSep(s, sep string) bool {
return sep == "" || !strings.ContainsAny(s, sep)
}
// formatCheckpoint renders c as path@offset for the debug output.
func formatCheckpoint(c checkpoint) string {
return fmt.Sprintf("%v@%d", c.path, c.offset)
}
// formatStack renders the checkpoint pile s for the debug output, the most
// recent one last.
func formatStack(s []checkpoint) string {
var sb strings.Builder
for i, c := range s {
if i > 0 {
sb.WriteString(" -> ")
}
sb.WriteString(formatCheckpoint(c))
}
return sb.String()
}
// popLast panics if s is empty.
func popLast[T any, E ~[]T](s *E) T {
n := len(*s)
r := (*s)[n-1]
*s = (*s)[:n-1]
return r
}
-45
View File
@@ -1,45 +0,0 @@
package match
import (
"fmt"
"github.com/gobwas/glob/util/strings"
)
type Any struct {
Separators []rune
}
func NewAny(s []rune) Any {
return Any{s}
}
func (self Any) Match(s string) bool {
return strings.IndexAnyRunes(s, self.Separators) == -1
}
func (self Any) Index(s string) (int, []int) {
found := strings.IndexAnyRunes(s, self.Separators)
switch found {
case -1:
case 0:
return 0, segments0
default:
s = s[:found]
}
segments := acquireSegments(len(s))
for i := range s {
segments = append(segments, i)
}
segments = append(segments, len(s))
return 0, segments
}
func (self Any) Len() int {
return lenNo
}
func (self Any) String() string {
return fmt.Sprintf("<any:![%s]>", string(self.Separators))
}
-82
View File
@@ -1,82 +0,0 @@
package match
import "fmt"
type AnyOf struct {
Matchers Matchers
}
func NewAnyOf(m ...Matcher) AnyOf {
return AnyOf{Matchers(m)}
}
func (self *AnyOf) Add(m Matcher) error {
self.Matchers = append(self.Matchers, m)
return nil
}
func (self AnyOf) Match(s string) bool {
for _, m := range self.Matchers {
if m.Match(s) {
return true
}
}
return false
}
func (self AnyOf) Index(s string) (int, []int) {
index := -1
segments := acquireSegments(len(s))
for _, m := range self.Matchers {
idx, seg := m.Index(s)
if idx == -1 {
continue
}
if index == -1 || idx < index {
index = idx
segments = append(segments[:0], seg...)
continue
}
if idx > index {
continue
}
// here idx == index
segments = appendMerge(segments, seg)
}
if index == -1 {
releaseSegments(segments)
return -1, nil
}
return index, segments
}
func (self AnyOf) Len() (l int) {
l = -1
for _, m := range self.Matchers {
ml := m.Len()
switch {
case l == -1:
l = ml
continue
case ml == -1:
return -1
case l != ml:
return -1
}
}
return
}
func (self AnyOf) String() string {
return fmt.Sprintf("<any_of:[%s]>", self.Matchers)
}
-146
View File
@@ -1,146 +0,0 @@
package match
import (
"fmt"
"unicode/utf8"
)
type BTree struct {
Value Matcher
Left Matcher
Right Matcher
ValueLengthRunes int
LeftLengthRunes int
RightLengthRunes int
LengthRunes int
}
func NewBTree(Value, Left, Right Matcher) (tree BTree) {
tree.Value = Value
tree.Left = Left
tree.Right = Right
lenOk := true
if tree.ValueLengthRunes = Value.Len(); tree.ValueLengthRunes == -1 {
lenOk = false
}
if Left != nil {
if tree.LeftLengthRunes = Left.Len(); tree.LeftLengthRunes == -1 {
lenOk = false
}
}
if Right != nil {
if tree.RightLengthRunes = Right.Len(); tree.RightLengthRunes == -1 {
lenOk = false
}
}
if lenOk {
tree.LengthRunes = tree.LeftLengthRunes + tree.ValueLengthRunes + tree.RightLengthRunes
} else {
tree.LengthRunes = -1
}
return tree
}
func (self BTree) Len() int {
return self.LengthRunes
}
// todo?
func (self BTree) Index(s string) (int, []int) {
return -1, nil
}
func (self BTree) Match(s string) bool {
inputLen := len(s)
// self.Length, self.RLen and self.LLen are values meaning the length of runes for each part
// here we manipulating byte length for better optimizations
// but these checks still works, cause minLen of 1-rune string is 1 byte.
if self.LengthRunes != -1 && self.LengthRunes > inputLen {
return false
}
// try to cut unnecessary parts
// by knowledge of length of right and left part
var offset, limit int
if self.LeftLengthRunes >= 0 {
offset = self.LeftLengthRunes
}
if self.RightLengthRunes >= 0 {
limit = inputLen - self.RightLengthRunes
} else {
limit = inputLen
}
for offset < limit {
// search for matching part in substring
index, segments := self.Value.Index(s[offset:limit])
if index == -1 {
releaseSegments(segments)
return false
}
l := s[:offset+index]
var left bool
if self.Left != nil {
left = self.Left.Match(l)
} else {
left = l == ""
}
if left {
for i := len(segments) - 1; i >= 0; i-- {
length := segments[i]
var right bool
var r string
// if there is no string for the right branch
if inputLen <= offset+index+length {
r = ""
} else {
r = s[offset+index+length:]
}
if self.Right != nil {
right = self.Right.Match(r)
} else {
right = r == ""
}
if right {
releaseSegments(segments)
return true
}
}
}
_, step := utf8.DecodeRuneInString(s[offset+index:])
offset += index + step
releaseSegments(segments)
}
return false
}
func (self BTree) String() string {
const n string = "<nil>"
var l, r string
if self.Left == nil {
l = n
} else {
l = self.Left.String()
}
if self.Right == nil {
r = n
} else {
r = self.Right.String()
}
return fmt.Sprintf("<btree:[%s<-%s->%s]>", l, self.Value, r)
}
-58
View File
@@ -1,58 +0,0 @@
package match
import (
"fmt"
"strings"
)
type Contains struct {
Needle string
Not bool
}
func NewContains(needle string, not bool) Contains {
return Contains{needle, not}
}
func (self Contains) Match(s string) bool {
return strings.Contains(s, self.Needle) != self.Not
}
func (self Contains) Index(s string) (int, []int) {
var offset int
idx := strings.Index(s, self.Needle)
if !self.Not {
if idx == -1 {
return -1, nil
}
offset = idx + len(self.Needle)
if len(s) <= offset {
return 0, []int{offset}
}
s = s[offset:]
} else if idx != -1 {
s = s[:idx]
}
segments := acquireSegments(len(s) + 1)
for i := range s {
segments = append(segments, offset+i)
}
return 0, append(segments, offset+len(s))
}
func (self Contains) Len() int {
return lenNo
}
func (self Contains) String() string {
var not string
if self.Not {
not = "!"
}
return fmt.Sprintf("<contains:%s[%s]>", not, self.Needle)
}
-99
View File
@@ -1,99 +0,0 @@
package match
import (
"fmt"
)
type EveryOf struct {
Matchers Matchers
}
func NewEveryOf(m ...Matcher) EveryOf {
return EveryOf{Matchers(m)}
}
func (self *EveryOf) Add(m Matcher) error {
self.Matchers = append(self.Matchers, m)
return nil
}
func (self EveryOf) Len() (l int) {
for _, m := range self.Matchers {
if ml := m.Len(); l > 0 {
l += ml
} else {
return -1
}
}
return
}
func (self EveryOf) Index(s string) (int, []int) {
var index int
var offset int
// make `in` with cap as len(s),
// cause it is the maximum size of output segments values
next := acquireSegments(len(s))
current := acquireSegments(len(s))
sub := s
for i, m := range self.Matchers {
idx, seg := m.Index(sub)
if idx == -1 {
releaseSegments(next)
releaseSegments(current)
return -1, nil
}
if i == 0 {
// we use copy here instead of `current = seg`
// cause seg is a slice from reusable buffer `in`
// and it could be overwritten in next iteration
current = append(current, seg...)
} else {
// clear the next
next = next[:0]
delta := index - (idx + offset)
for _, ex := range current {
for _, n := range seg {
if ex+delta == n {
next = append(next, n)
}
}
}
if len(next) == 0 {
releaseSegments(next)
releaseSegments(current)
return -1, nil
}
current = append(current[:0], next...)
}
index = idx + offset
sub = s[index:]
offset += idx
}
releaseSegments(next)
return index, current
}
func (self EveryOf) Match(s string) bool {
for _, m := range self.Matchers {
if !m.Match(s) {
return false
}
}
return true
}
func (self EveryOf) String() string {
return fmt.Sprintf("<every_of:[%s]>", self.Matchers)
}
-49
View File
@@ -1,49 +0,0 @@
package match
import (
"fmt"
"github.com/gobwas/glob/util/runes"
"unicode/utf8"
)
type List struct {
List []rune
Not bool
}
func NewList(list []rune, not bool) List {
return List{list, not}
}
func (self List) Match(s string) bool {
r, w := utf8.DecodeRuneInString(s)
if len(s) > w {
return false
}
inList := runes.IndexRune(self.List, r) != -1
return inList == !self.Not
}
func (self List) Len() int {
return lenOne
}
func (self List) Index(s string) (int, []int) {
for i, r := range s {
if self.Not == (runes.IndexRune(self.List, r) == -1) {
return i, segmentsByRuneLength[utf8.RuneLen(r)]
}
}
return -1, nil
}
func (self List) String() string {
var not string
if self.Not {
not = "!"
}
return fmt.Sprintf("<list:%s[%s]>", not, string(self.List))
}
-81
View File
@@ -1,81 +0,0 @@
package match
// todo common table of rune's length
import (
"fmt"
"strings"
)
const lenOne = 1
const lenZero = 0
const lenNo = -1
type Matcher interface {
Match(string) bool
Index(string) (int, []int)
Len() int
String() string
}
type Matchers []Matcher
func (m Matchers) String() string {
var s []string
for _, matcher := range m {
s = append(s, fmt.Sprint(matcher))
}
return fmt.Sprintf("%s", strings.Join(s, ","))
}
// appendMerge merges and sorts given already SORTED and UNIQUE segments.
func appendMerge(target, sub []int) []int {
lt, ls := len(target), len(sub)
out := make([]int, 0, lt+ls)
for x, y := 0, 0; x < lt || y < ls; {
if x >= lt {
out = append(out, sub[y:]...)
break
}
if y >= ls {
out = append(out, target[x:]...)
break
}
xValue := target[x]
yValue := sub[y]
switch {
case xValue == yValue:
out = append(out, xValue)
x++
y++
case xValue < yValue:
out = append(out, xValue)
x++
case yValue < xValue:
out = append(out, yValue)
y++
}
}
target = append(target[:0], out...)
return target
}
func reverseSegments(input []int) {
l := len(input)
m := l / 2
for i := 0; i < m; i++ {
input[i], input[l-i-1] = input[l-i-1], input[i]
}
}
-49
View File
@@ -1,49 +0,0 @@
package match
import (
"fmt"
"unicode/utf8"
)
type Max struct {
Limit int
}
func NewMax(l int) Max {
return Max{l}
}
func (self Max) Match(s string) bool {
var l int
for range s {
l += 1
if l > self.Limit {
return false
}
}
return true
}
func (self Max) Index(s string) (int, []int) {
segments := acquireSegments(self.Limit + 1)
segments = append(segments, 0)
var count int
for i, r := range s {
count++
if count > self.Limit {
break
}
segments = append(segments, i+utf8.RuneLen(r))
}
return 0, segments
}
func (self Max) Len() int {
return lenNo
}
func (self Max) String() string {
return fmt.Sprintf("<max:%d>", self.Limit)
}
-57
View File
@@ -1,57 +0,0 @@
package match
import (
"fmt"
"unicode/utf8"
)
type Min struct {
Limit int
}
func NewMin(l int) Min {
return Min{l}
}
func (self Min) Match(s string) bool {
var l int
for range s {
l += 1
if l >= self.Limit {
return true
}
}
return false
}
func (self Min) Index(s string) (int, []int) {
var count int
c := len(s) - self.Limit + 1
if c <= 0 {
return -1, nil
}
segments := acquireSegments(c)
for i, r := range s {
count++
if count >= self.Limit {
segments = append(segments, i+utf8.RuneLen(r))
}
}
if len(segments) == 0 {
return -1, nil
}
return 0, segments
}
func (self Min) Len() int {
return lenNo
}
func (self Min) String() string {
return fmt.Sprintf("<min:%d>", self.Limit)
}
-27
View File
@@ -1,27 +0,0 @@
package match
import (
"fmt"
)
type Nothing struct{}
func NewNothing() Nothing {
return Nothing{}
}
func (self Nothing) Match(s string) bool {
return len(s) == 0
}
func (self Nothing) Index(s string) (int, []int) {
return 0, segments0
}
func (self Nothing) Len() int {
return lenZero
}
func (self Nothing) String() string {
return fmt.Sprintf("<nothing>")
}
-50
View File
@@ -1,50 +0,0 @@
package match
import (
"fmt"
"strings"
"unicode/utf8"
)
type Prefix struct {
Prefix string
}
func NewPrefix(p string) Prefix {
return Prefix{p}
}
func (self Prefix) Index(s string) (int, []int) {
idx := strings.Index(s, self.Prefix)
if idx == -1 {
return -1, nil
}
length := len(self.Prefix)
var sub string
if len(s) > idx+length {
sub = s[idx+length:]
} else {
sub = ""
}
segments := acquireSegments(len(sub) + 1)
segments = append(segments, length)
for i, r := range sub {
segments = append(segments, length+i+utf8.RuneLen(r))
}
return idx, segments
}
func (self Prefix) Len() int {
return lenNo
}
func (self Prefix) Match(s string) bool {
return strings.HasPrefix(s, self.Prefix)
}
func (self Prefix) String() string {
return fmt.Sprintf("<prefix:%s>", self.Prefix)
}
-55
View File
@@ -1,55 +0,0 @@
package match
import (
"fmt"
"strings"
"unicode/utf8"
sutil "github.com/gobwas/glob/util/strings"
)
type PrefixAny struct {
Prefix string
Separators []rune
}
func NewPrefixAny(s string, sep []rune) PrefixAny {
return PrefixAny{s, sep}
}
func (self PrefixAny) Index(s string) (int, []int) {
idx := strings.Index(s, self.Prefix)
if idx == -1 {
return -1, nil
}
n := len(self.Prefix)
sub := s[idx+n:]
i := sutil.IndexAnyRunes(sub, self.Separators)
if i > -1 {
sub = sub[:i]
}
seg := acquireSegments(len(sub) + 1)
seg = append(seg, n)
for i, r := range sub {
seg = append(seg, n+i+utf8.RuneLen(r))
}
return idx, seg
}
func (self PrefixAny) Len() int {
return lenNo
}
func (self PrefixAny) Match(s string) bool {
if !strings.HasPrefix(s, self.Prefix) {
return false
}
return sutil.IndexAnyRunes(s[len(self.Prefix):], self.Separators) == -1
}
func (self PrefixAny) String() string {
return fmt.Sprintf("<prefix_any:%s![%s]>", self.Prefix, string(self.Separators))
}
-62
View File
@@ -1,62 +0,0 @@
package match
import (
"fmt"
"strings"
)
type PrefixSuffix struct {
Prefix, Suffix string
}
func NewPrefixSuffix(p, s string) PrefixSuffix {
return PrefixSuffix{p, s}
}
func (self PrefixSuffix) Index(s string) (int, []int) {
prefixIdx := strings.Index(s, self.Prefix)
if prefixIdx == -1 {
return -1, nil
}
suffixLen := len(self.Suffix)
if suffixLen <= 0 {
return prefixIdx, []int{len(s) - prefixIdx}
}
if (len(s) - prefixIdx) <= 0 {
return -1, nil
}
segments := acquireSegments(len(s) - prefixIdx)
for sub := s[prefixIdx:]; ; {
suffixIdx := strings.LastIndex(sub, self.Suffix)
if suffixIdx == -1 {
break
}
segments = append(segments, suffixIdx+suffixLen)
sub = sub[:suffixIdx]
}
if len(segments) == 0 {
releaseSegments(segments)
return -1, nil
}
reverseSegments(segments)
return prefixIdx, segments
}
func (self PrefixSuffix) Len() int {
return lenNo
}
func (self PrefixSuffix) Match(s string) bool {
return strings.HasPrefix(s, self.Prefix) && strings.HasSuffix(s, self.Suffix)
}
func (self PrefixSuffix) String() string {
return fmt.Sprintf("<prefix_suffix:[%s,%s]>", self.Prefix, self.Suffix)
}
-48
View File
@@ -1,48 +0,0 @@
package match
import (
"fmt"
"unicode/utf8"
)
type Range struct {
Lo, Hi rune
Not bool
}
func NewRange(lo, hi rune, not bool) Range {
return Range{lo, hi, not}
}
func (self Range) Len() int {
return lenOne
}
func (self Range) Match(s string) bool {
r, w := utf8.DecodeRuneInString(s)
if len(s) > w {
return false
}
inRange := r >= self.Lo && r <= self.Hi
return inRange == !self.Not
}
func (self Range) Index(s string) (int, []int) {
for i, r := range s {
if self.Not != (r >= self.Lo && r <= self.Hi) {
return i, segmentsByRuneLength[utf8.RuneLen(r)]
}
}
return -1, nil
}
func (self Range) String() string {
var not string
if self.Not {
not = "!"
}
return fmt.Sprintf("<range:%s[%s,%s]>", not, string(self.Lo), string(self.Hi))
}
-77
View File
@@ -1,77 +0,0 @@
package match
import (
"fmt"
)
type Row struct {
Matchers Matchers
RunesLength int
Segments []int
}
func NewRow(len int, m ...Matcher) Row {
return Row{
Matchers: Matchers(m),
RunesLength: len,
Segments: []int{len},
}
}
func (self Row) matchAll(s string) bool {
var idx int
for _, m := range self.Matchers {
length := m.Len()
var next, i int
for next = range s[idx:] {
i++
if i == length {
break
}
}
if i < length || !m.Match(s[idx:idx+next+1]) {
return false
}
idx += next + 1
}
return true
}
func (self Row) lenOk(s string) bool {
var i int
for range s {
i++
if i > self.RunesLength {
return false
}
}
return self.RunesLength == i
}
func (self Row) Match(s string) bool {
return self.lenOk(s) && self.matchAll(s)
}
func (self Row) Len() (l int) {
return self.RunesLength
}
func (self Row) Index(s string) (int, []int) {
for i := range s {
if len(s[i:]) < self.RunesLength {
break
}
if self.matchAll(s[i:]) {
return i, self.Segments
}
}
return -1, nil
}
func (self Row) String() string {
return fmt.Sprintf("<row_%d:[%s]>", self.RunesLength, self.Matchers)
}
-91
View File
@@ -1,91 +0,0 @@
package match
import (
"sync"
)
type SomePool interface {
Get() []int
Put([]int)
}
var segmentsPools [1024]sync.Pool
func toPowerOfTwo(v int) int {
v--
v |= v >> 1
v |= v >> 2
v |= v >> 4
v |= v >> 8
v |= v >> 16
v++
return v
}
const (
cacheFrom = 16
cacheToAndHigher = 1024
cacheFromIndex = 15
cacheToAndHigherIndex = 1023
)
var (
segments0 = []int{0}
segments1 = []int{1}
segments2 = []int{2}
segments3 = []int{3}
segments4 = []int{4}
)
var segmentsByRuneLength [5][]int = [5][]int{
0: segments0,
1: segments1,
2: segments2,
3: segments3,
4: segments4,
}
func init() {
for i := cacheToAndHigher; i >= cacheFrom; i >>= 1 {
func(i int) {
segmentsPools[i-1] = sync.Pool{New: func() interface{} {
return make([]int, 0, i)
}}
}(i)
}
}
func getTableIndex(c int) int {
p := toPowerOfTwo(c)
switch {
case p >= cacheToAndHigher:
return cacheToAndHigherIndex
case p <= cacheFrom:
return cacheFromIndex
default:
return p - 1
}
}
func acquireSegments(c int) []int {
// make []int with less capacity than cacheFrom
// is faster than acquiring it from pool
if c < cacheFrom {
return make([]int, 0, c)
}
return segmentsPools[getTableIndex(c)].Get().([]int)[:0]
}
func releaseSegments(s []int) {
c := cap(s)
// make []int with less capacity than cacheFrom
// is faster than acquiring it from pool
if c < cacheFrom {
return
}
segmentsPools[getTableIndex(c)].Put(s)
}
-43
View File
@@ -1,43 +0,0 @@
package match
import (
"fmt"
"github.com/gobwas/glob/util/runes"
"unicode/utf8"
)
// single represents ?
type Single struct {
Separators []rune
}
func NewSingle(s []rune) Single {
return Single{s}
}
func (self Single) Match(s string) bool {
r, w := utf8.DecodeRuneInString(s)
if len(s) > w {
return false
}
return runes.IndexRune(self.Separators, r) == -1
}
func (self Single) Len() int {
return lenOne
}
func (self Single) Index(s string) (int, []int) {
for i, r := range s {
if runes.IndexRune(self.Separators, r) == -1 {
return i, segmentsByRuneLength[utf8.RuneLen(r)]
}
}
return -1, nil
}
func (self Single) String() string {
return fmt.Sprintf("<single:![%s]>", string(self.Separators))
}
-35
View File
@@ -1,35 +0,0 @@
package match
import (
"fmt"
"strings"
)
type Suffix struct {
Suffix string
}
func NewSuffix(s string) Suffix {
return Suffix{s}
}
func (self Suffix) Len() int {
return lenNo
}
func (self Suffix) Match(s string) bool {
return strings.HasSuffix(s, self.Suffix)
}
func (self Suffix) Index(s string) (int, []int) {
idx := strings.Index(s, self.Suffix)
if idx == -1 {
return -1, nil
}
return 0, []int{idx + len(self.Suffix)}
}
func (self Suffix) String() string {
return fmt.Sprintf("<suffix:%s>", self.Suffix)
}
-43
View File
@@ -1,43 +0,0 @@
package match
import (
"fmt"
"strings"
sutil "github.com/gobwas/glob/util/strings"
)
type SuffixAny struct {
Suffix string
Separators []rune
}
func NewSuffixAny(s string, sep []rune) SuffixAny {
return SuffixAny{s, sep}
}
func (self SuffixAny) Index(s string) (int, []int) {
idx := strings.Index(s, self.Suffix)
if idx == -1 {
return -1, nil
}
i := sutil.LastIndexAnyRunes(s[:idx], self.Separators) + 1
return i, []int{idx + len(self.Suffix) - i}
}
func (self SuffixAny) Len() int {
return lenNo
}
func (self SuffixAny) Match(s string) bool {
if !strings.HasSuffix(s, self.Suffix) {
return false
}
return sutil.IndexAnyRunes(s[:len(s)-len(self.Suffix)], self.Separators) == -1
}
func (self SuffixAny) String() string {
return fmt.Sprintf("<suffix_any:![%s]%s>", string(self.Separators), self.Suffix)
}
-33
View File
@@ -1,33 +0,0 @@
package match
import (
"fmt"
)
type Super struct{}
func NewSuper() Super {
return Super{}
}
func (self Super) Match(s string) bool {
return true
}
func (self Super) Len() int {
return lenNo
}
func (self Super) Index(s string) (int, []int) {
segments := acquireSegments(len(s) + 1)
for i := range s {
segments = append(segments, i)
}
segments = append(segments, len(s))
return 0, segments
}
func (self Super) String() string {
return fmt.Sprintf("<super>")
}
-45
View File
@@ -1,45 +0,0 @@
package match
import (
"fmt"
"strings"
"unicode/utf8"
)
// raw represents raw string to match
type Text struct {
Str string
RunesLength int
BytesLength int
Segments []int
}
func NewText(s string) Text {
return Text{
Str: s,
RunesLength: utf8.RuneCountInString(s),
BytesLength: len(s),
Segments: []int{len(s)},
}
}
func (self Text) Match(s string) bool {
return self.Str == s
}
func (self Text) Len() int {
return self.RunesLength
}
func (self Text) Index(s string) (int, []int) {
index := strings.Index(s, self.Str)
if index == -1 {
return -1, nil
}
return index, self.Segments
}
func (self Text) String() string {
return fmt.Sprintf("<text:`%v`>", self.Str)
}
+679
View File
@@ -0,0 +1,679 @@
package glob
import (
"slices"
"unicode/utf8"
"github.com/gobwas/glob/internal/debug"
"github.com/gobwas/glob/syntax"
)
// compile parses the pattern into a matcher tree (see below), simplifies and
// specializes it, and computes the match-time hints and preconditions; see
// [simplify], [specialize], [annotateStars], [needsState], [minLength] and
// [requiredSuffix]. It is what [Compile] wraps.
func compile(str string, sep []rune) (*Pattern, error) {
if debug.Enabled {
debug.Printf("compiling %#q\n", str)
}
// The matchers keep sep and read it while matching, and the variadic slice
// may alias an array owned by the caller: give them a copy of their own.
//
// The pattern itself keeps the slice as given, to return it from
// Separators() without cloning.
var (
sepCopy = slices.Clone(sep)
sepStr = string(sep)
)
type operator struct {
kind int
index int
}
const (
opTerms = iota
opList
)
/*
Stack-based parsing is a technique used to evaluate mathematical
expressions by leveraging the properties of the LIFO (Last-In,
First-Out) data structure, the stack. It involves using two stacks: one
for operands (numbers) and one for operators. By processing the
expression from left to right and strategically pushing and popping
elements from the stacks, the expression can be effectively evaluated.
https://cp-algorithms.com/string/expression_parsing.html
Here the operands are matchers and the only operators are the braces
and the commas inside them, so it goes as follows:
- a leaf token (text, `?`, `*`, `**`, `[...]`) pushes its matcher
onto the stack;
- `{` pushes two operators, both remembering the current stack
length: opTerms marks where the alternatives of the group will
be collected, opList marks where the terms of the current
alternative begin;
- `,` pops the opList, collapses the terms above its index into a
single multiMatcher (or a voidMatcher when there are none, as in
`{,a}`), and pushes a fresh opList for the next alternative;
- `}` pops the opList and collapses the last alternative the same
way, then pops the opTerms and collapses everything above its
index -- one matcher per alternative by now -- into an
altMatcher;
- at the EOF whatever is left on the stack is the top-level
sequence; a leftover operator means an unclosed `{`.
For example, `a{b*,c}d` goes like this (list@i is an opList with
index i, likewise terms@i):
token stack operators
a "a"
{ "a" terms@1 list@1
b "a" "b" terms@1 list@1
* "a" "b" * terms@1 list@1
, "a" ["b"·*] terms@1 list@2
c "a" ["b"·*] "c" terms@1 list@2
} "a" ["b"·*] ["c"] terms@1
"a" {["b"·*]|["c"]}
d "a" {["b"·*]|["c"]} "d"
EOF ["a"·{["b"·*]|["c"]}·"d"]
The result is then simplified (["c"] becomes "c") and specialized;
see [simplify] and [specialize].
*/
var (
stack []matcher
operators []operator
)
lex := syntax.NewLexer(str)
parsing:
for {
token := lex.Next()
if debug.Enabled {
debug.Printf("token: %s\n", token)
}
switch token.Type {
case syntax.EOF:
break parsing
case syntax.Error:
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: token.Data,
}
case syntax.Single:
stack = append(stack, &charMatcher{
Sep: sepCopy,
})
case syntax.Text:
stack = append(stack, &textMatcher{
Text: token.Data,
})
case syntax.RangeOpen:
m, err := parseRange(lex)
if err != nil {
return nil, err
}
stack = append(stack, m)
case syntax.Any:
stack = append(stack, &starMatcher{
Sep: sepCopy,
SepStr: sepStr,
})
case syntax.Super:
stack = append(stack, &starMatcher{
Sep: nil,
})
case syntax.TermsOpen:
// Note that the `{` opens both the group and its first
// alternative: every alternative is delimited by an opList
// operator. This way TermsClose always collapses the trailing
// alternative into a single matcher first, even when the group
// has no commas at all, e.g. `{ab*}`.
operators = append(operators,
operator{kind: opTerms, index: len(stack)},
operator{kind: opList, index: len(stack)},
)
if debug.Enabled {
debug.Printf("terms enter: %d\n", len(stack))
}
case syntax.TermSeparator:
k := len(operators) - 1
if k < 0 {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unexpected `,`",
}
}
x := operators[k]
if x.kind == opList {
// Remove the most recent "comma" operator.
// Note that the previous one is the terms operator.
operators = operators[:k]
}
i := x.index
// Handle the `{,a}` case.
if i == len(stack) {
// Empty matchers.
stack = append(stack, &voidMatcher{})
} else {
stack[i] = multiMatcher(slices.Clone(stack[i:]))
stack = stack[:i+1]
if debug.Enabled {
debug.Printf("terms next: %d: %s\n", i, stack[i])
}
}
operators = append(operators, operator{
kind: opList,
index: len(stack),
})
if debug.Enabled {
debug.Printf("terms separator: %d\n", len(stack))
}
case syntax.TermsClose:
for {
k := len(operators) - 1
if k < 0 {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unexpected `}`",
}
}
x := operators[k]
operators = operators[:k]
i := x.index
c := slices.Clone(stack[i:])
var m matcher
switch x.kind {
case opTerms:
m = altMatcher(c)
case opList:
m = multiMatcher(c)
}
// Handle the `{a,}` case.
if i == len(stack) {
stack = append(stack, m)
} else {
stack = stack[:i+1]
stack[i] = m
}
if debug.Enabled {
debug.Printf(
"terms leave(%d): %d: %s\n",
x.kind, i, stack[i],
)
}
if x.kind == opTerms {
break
}
}
default:
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unexpected token " + token.String(),
}
}
}
if len(operators) != 0 {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unclosed `{`",
}
}
m := simplify(multiMatcher(stack))
m = specialize(m, true)
annotateStars(m, true)
if debug.Enabled {
debug.Printf("compiled %#q: %s\n", str, m)
}
p := &Pattern{
str: str,
sep: sep,
m: m,
state: needsState(m),
}
if p.state {
p.minLen = minLength(m)
p.suffix = requiredSuffix(m)
}
return p, nil
}
// parseRange parses a character class, called right after its opening `[`
// was read; it consumes the tokens up to and including the closing `]`. The
// class is either a range, `[a-c]`, or a set, `[abc]`, either possibly
// negated with a leading `!`; see [runeRangeMatcher] and [runeSetMatcher].
func parseRange(lex *syntax.Lexer) (matcher, error) {
// -1 marks a range boundary as unset: any decoded rune, including
// U+0000, is non-negative.
var (
not bool
lo, hi rune = -1, -1
chars map[rune]struct{}
)
for {
token := lex.Next()
switch token.Type {
case syntax.EOF:
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unclosed `[`",
}
case syntax.Error:
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: token.Data,
}
case syntax.Not:
not = true
case syntax.RangeLo:
r, w := utf8.DecodeRuneInString(token.Data)
if len(token.Data) > w {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unexpected length of range lo character",
}
}
lo = r
case syntax.RangeBetween:
// The `-` between lo and hi: nothing to do.
case syntax.RangeHi:
r, w := utf8.DecodeRuneInString(token.Data)
if len(token.Data) > w {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "unexpected length of range hi character",
}
}
hi = r
if hi < lo {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "range hi character is less than lo",
}
}
case syntax.Text:
chars = make(map[rune]struct{})
for _, r := range token.Data {
chars[r] = struct{}{}
}
case syntax.RangeClose:
isRange := lo >= 0 && hi >= 0
isChars := chars != nil
if isChars == isRange {
return nil, &SyntaxError{
Offset: lex.Offset(),
Reason: "could not parse range",
}
}
if isRange {
return &runeRangeMatcher{
Lo: lo,
Hi: hi,
Not: not,
}, nil
}
return &runeSetMatcher{
Set: chars,
Not: not,
}, nil
}
}
}
// simplify rewrites the freshly parsed tree into its canonical shape, bottom
// up: the sequences are normalized (see [normalizeSequence]), and a sequence
// or a group of alternatives with a single child is replaced by the child,
// with none -- by a void.
func simplify(m matcher) matcher {
var (
ms []matcher
isMulti bool
)
switch v := m.(type) {
case multiMatcher:
ms, isMulti = v, true
case altMatcher:
ms = v
default:
return m
}
for i, m := range ms {
ms[i] = simplify(m)
}
if isMulti {
ms = normalizeSequence(ms)
}
switch len(ms) {
case 0:
return &voidMatcher{}
case 1:
return ms[0]
}
if isMulti {
return multiMatcher(ms)
}
return altMatcher(ms)
}
// normalizeSequence rewrites a sequence of (already simplified) matchers
// into a simpler equivalent one:
//
// ["a"·["b"·"c"]·"d"] => ["a"·"b"·"c"·"d"] inline the nested sequences
// ["a"·void] => ["a"] drop the void matchers
// ["a"·"b"] => ["ab"] merge the adjacent literals
// [*·**] => [**] coalesce the adjacent stars
//
// Longer literals also make better star jumps; see [annotateStars].
func normalizeSequence(ms []matcher) []matcher {
if !needsNormalize(ms) {
// The common case: nothing to rewrite, no copy needed.
return ms
}
out := make([]matcher, 0, len(ms))
var push func(m matcher)
push = func(m matcher) {
switch v := m.(type) {
case multiMatcher:
for _, c := range v {
push(c)
}
return
case *voidMatcher:
return
case *textMatcher:
if len(out) > 0 {
if prev, ok := out[len(out)-1].(*textMatcher); ok {
out[len(out)-1] = &textMatcher{Text: prev.Text + v.Text}
return
}
}
case *starMatcher:
if len(out) > 0 {
if prev, ok := out[len(out)-1].(*starMatcher); ok {
// Adjacent stars are equivalent to the most general
// of them: the one not limited by separators, if any.
if len(prev.Sep) > 0 && len(v.Sep) == 0 {
out[len(out)-1] = v
}
return
}
}
}
out = append(out, m)
}
for _, m := range ms {
push(m)
}
return out
}
// needsNormalize reports whether [normalizeSequence] would change ms, so
// that the common case skips the copy.
func needsNormalize(ms []matcher) bool {
for i, m := range ms {
switch m.(type) {
case multiMatcher, *voidMatcher:
return true
case *textMatcher:
if i > 0 {
if _, ok := ms[i-1].(*textMatcher); ok {
return true
}
}
case *starMatcher:
if i > 0 {
if _, ok := ms[i-1].(*starMatcher); ok {
return true
}
}
}
}
return false
}
// specialize rewrites the terminal sub-sequences of the simplified matcher
// tree into the shaped matchers -- [prefixMatcher], [suffixMatcher],
// [prefixSuffixMatcher] and [containsMatcher]; see [foldTail] for the
// rewrites. The tail flag tells whether nothing follows m in the pattern;
// only there the rewrites apply, since a shaped matcher consumes the whole
// remainder of the input.
func specialize(m matcher, tail bool) matcher {
switch v := m.(type) {
case altMatcher:
// Every alternative ends where the alt ends.
for i, c := range v {
v[i] = specialize(c, tail)
}
return v
case multiMatcher:
for i, c := range v {
v[i] = specialize(c, tail && i == len(v)-1)
}
if !tail {
return v
}
ms := foldTail([]matcher(v))
if len(ms) == 1 {
return ms[0]
}
return multiMatcher(ms)
}
return m
}
// foldTail repeatedly folds the two trailing matchers of the terminal
// sequence ms into a shaped one, while possible:
//
// [..·"abc"·*] => [..·prefix("abc")]
// [..·*·"abc"] => [..·suffix("abc")]
// [..·"abc"·prefix("def")] => [..·prefix("abcdef")]
// [..·*·prefix("abc")] => [..·contains("abc")] (separator-free)
// [..·"abc"·suffix("def")] => [..·prefix_suffix("abc","def")]
// [..·*·contains("abc")] => [..·contains("abc")] (separator-free)
func foldTail(ms []matcher) []matcher {
for len(ms) >= 2 {
var (
prev = ms[len(ms)-2]
folded matcher
)
switch last := ms[len(ms)-1].(type) {
case *starMatcher:
if t, ok := prev.(*textMatcher); ok {
folded = &prefixMatcher{Text: t.Text, Sep: last.SepStr}
}
case *textMatcher:
if star, ok := prev.(*starMatcher); ok {
folded = &suffixMatcher{Text: last.Text, Sep: star.SepStr}
}
case *prefixMatcher:
switch p := prev.(type) {
case *textMatcher:
folded = &prefixMatcher{Text: p.Text + last.Text, Sep: last.Sep}
case *starMatcher:
if p.SepStr == "" && last.Sep == "" {
folded = &containsMatcher{Text: last.Text}
}
}
case *suffixMatcher:
if t, ok := prev.(*textMatcher); ok {
folded = &prefixSuffixMatcher{
Prefix: t.Text,
Suffix: last.Text,
Sep: last.Sep,
}
}
case *containsMatcher:
if star, ok := prev.(*starMatcher); ok && star.SepStr == "" {
folded = last
}
}
if folded == nil {
break
}
ms = ms[:len(ms)-1]
ms[len(ms)-1] = folded
}
return ms
}
// annotateStars computes the compile-time hints for the star matchers, in
// order to keep the number of restart points they store at match time low:
//
// - a star directly followed by a literal jumps between the literal
// occurrences instead of retrying at every rune (see
// [starMatcher.storeSkip]);
//
// - a star with nothing after it anywhere in the pattern (tail is true
// for m and the star closes it) consumes its whole reach at once and
// stores no restart points at all.
func annotateStars(m matcher, tail bool) {
switch v := m.(type) {
case multiMatcher:
for i, c := range v {
last := i == len(v)-1
star, ok := c.(*starMatcher)
if !ok {
annotateStars(c, tail && last)
continue
}
star.Terminal = tail && last
if !last {
star.Next = leadingLiteral(v[i+1])
}
}
case altMatcher:
for _, c := range v {
annotateStars(c, tail)
}
case *starMatcher:
v.Terminal = tail
}
}
// leadingLiteral returns the literal the given matcher is guaranteed to
// begin its match with, if any.
func leadingLiteral(m matcher) string {
switch v := m.(type) {
case *textMatcher:
return v.Text
case *prefixMatcher:
return v.Text
case *prefixSuffixMatcher:
return v.Prefix
}
return ""
}
// minLength returns the minimum length in bytes of a string m can match.
func minLength(m matcher) (n int) {
switch v := m.(type) {
case *textMatcher:
return len(v.Text)
case *charMatcher, *runeRangeMatcher, *runeSetMatcher:
return 1
case *prefixMatcher:
return len(v.Text)
case *suffixMatcher:
return len(v.Text)
case *prefixSuffixMatcher:
return len(v.Prefix) + len(v.Suffix)
case *containsMatcher:
return len(v.Text)
case multiMatcher:
for _, c := range v {
n += minLength(c)
}
return n
case altMatcher:
n = minLength(v[0])
for _, c := range v[1:] {
n = min(n, minLength(c))
}
return n
}
return 0 // A star or a void.
}
// requiredSuffix returns the literal every string m matches must end with.
func requiredSuffix(m matcher) string {
switch v := m.(type) {
case *textMatcher:
return v.Text
case *suffixMatcher:
return v.Text
case *prefixSuffixMatcher:
return v.Suffix
case multiMatcher:
return requiredSuffix(v[len(v)-1])
case altMatcher:
s := requiredSuffix(v[0])
for _, c := range v[1:] {
s = commonSuffix(s, requiredSuffix(c))
if s == "" {
break
}
}
return s
}
return "" // A star, a single-character matcher or a void.
}
// commonSuffix returns the longest common suffix of a and b, never splitting
// a multi-byte rune.
func commonSuffix(a, b string) string {
i := 0
for i < len(a) && i < len(b) {
ra, wa := utf8.DecodeLastRuneInString(a[:len(a)-i])
rb, wb := utf8.DecodeLastRuneInString(b[:len(b)-i])
if ra != rb || wa != wb {
break
}
i += wa
}
return a[len(a)-i:]
}
// needsState reports whether matching m may save a checkpoint. Only the
// alts and the non-terminal stars do; a pattern without them is matched
// with a plain call chain -- see [Pattern.Match].
func needsState(m matcher) bool {
switch v := m.(type) {
case altMatcher:
return true
case multiMatcher:
return slices.ContainsFunc(v, needsState)
case *starMatcher:
return !v.Terminal
}
return false
}
+166 -71
View File
@@ -1,6 +1,6 @@
# glob.[go](https://golang.org)
[![GoDoc][godoc-image]][godoc-url] [![Build Status][travis-image]][travis-url]
[![GoDoc][godoc-image]][godoc-url] [![CI][ci-image]][ci-url]
> Go Globbing Library.
@@ -19,130 +19,225 @@ package main
import "github.com/gobwas/glob"
func main() {
var g glob.Glob
var g *glob.Pattern
// create simple glob
g = glob.MustCompile("*.github.com")
g.Match("api.github.com") // true
// quote meta characters and then create simple glob
// quote meta characters and then create simple glob
g = glob.MustCompile(glob.QuoteMeta("*.github.com"))
g.Match("*.github.com") // true
// create new glob with set of delimiters as ["."]
g = glob.MustCompile("api.*.com", '.')
g.Match("api.github.com") // true
g.Match("api.gi.hub.com") // false
// create new glob with set of delimiters as ["."]
// but now with super wildcard
g = glob.MustCompile("api.**.com", '.')
g.Match("api.github.com") // true
g.Match("api.gi.hub.com") // true
// create glob with single symbol wildcard
g = glob.MustCompile("?at")
g.Match("cat") // true
g.Match("fat") // true
g.Match("at") // false
// create glob with single symbol wildcard and delimiters ['f']
g = glob.MustCompile("?at", 'f')
g.Match("cat") // true
g.Match("fat") // false
g.Match("at") // false
// create glob with character-list matchers
g.Match("at") // false
// create glob with character-list matchers
g = glob.MustCompile("[abc]at")
g.Match("cat") // true
g.Match("bat") // true
g.Match("fat") // false
g.Match("at") // false
// create glob with character-list matchers
// create glob with character-list matchers
g = glob.MustCompile("[!abc]at")
g.Match("cat") // false
g.Match("bat") // false
g.Match("fat") // true
g.Match("at") // false
// create glob with character-range matchers
g.Match("at") // false
// create glob with character-range matchers
g = glob.MustCompile("[a-c]at")
g.Match("cat") // true
g.Match("bat") // true
g.Match("fat") // false
g.Match("at") // false
// create glob with character-range matchers
// create glob with character-range matchers
g = glob.MustCompile("[!a-c]at")
g.Match("cat") // false
g.Match("bat") // false
g.Match("fat") // true
g.Match("at") // false
// create glob with pattern-alternatives list
g.Match("at") // false
// create glob with pattern-alternatives list
g = glob.MustCompile("{cat,bat,[fr]at}")
g.Match("cat") // true
g.Match("bat") // true
g.Match("fat") // true
g.Match("rat") // true
g.Match("at") // false
g.Match("zat") // false
g.Match("at") // false
g.Match("zat") // false
}
```
## Performance
`Compile` reports malformed patterns with a `*glob.SyntaxError` carrying the
byte offset and the reason:
This library is created for compile-once patterns. This means, that compilation could take time, but
strings matching is done faster, than in case when always parsing template.
```go
_, err := glob.Compile("{a,b")
// err: glob: syntax error at 4: unclosed `{`
```
If you will not use compiled `glob.Glob` object, and do `g := glob.MustCompile(pattern); g.Match(...)` every time, then your code will be much more slower.
A compiled `Pattern` captures what it was compiled from, so it can be passed
around instead of the raw arguments and inspected when needed (`String()` makes
it a `fmt.Stringer`, like `regexp.Regexp`):
Run `go test -bench=.` from source root to see the benchmarks:
Pattern | Fixture | Match | Speed (ns/op)
--------|---------|-------|--------------
`[a-z][!a-x]*cat*[h][!b]*eyes*` | `my cat has very bright eyes` | `true` | 432
`[a-z][!a-x]*cat*[h][!b]*eyes*` | `my dog has very bright eyes` | `false` | 199
`https://*.google.*` | `https://account.google.com` | `true` | 96
`https://*.google.*` | `https://google.com` | `false` | 66
`{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}` | `http://yahoo.com` | `true` | 163
`{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}` | `http://google.com` | `false` | 197
`{https://*gobwas.com,http://exclude.gobwas.com}` | `https://safe.gobwas.com` | `true` | 22
`{https://*gobwas.com,http://exclude.gobwas.com}` | `http://safe.gobwas.com` | `false` | 24
`abc*` | `abcdef` | `true` | 8.15
`abc*` | `af` | `false` | 5.68
`*def` | `abcdef` | `true` | 8.84
`*def` | `af` | `false` | 5.74
`ab*ef` | `abcdef` | `true` | 15.2
`ab*ef` | `af` | `false` | 10.4
The same things with `regexp` package:
Pattern | Fixture | Match | Speed (ns/op)
--------|---------|-------|--------------
`^[a-z][^a-x].*cat.*[h][^b].*eyes.*$` | `my cat has very bright eyes` | `true` | 2553
`^[a-z][^a-x].*cat.*[h][^b].*eyes.*$` | `my dog has very bright eyes` | `false` | 1383
`^https:\/\/.*\.google\..*$` | `https://account.google.com` | `true` | 1205
`^https:\/\/.*\.google\..*$` | `https://google.com` | `false` | 767
`^(https:\/\/.*\.google\..*|.*yandex\..*|.*yahoo\..*|.*mail\.ru)$` | `http://yahoo.com` | `true` | 1435
`^(https:\/\/.*\.google\..*|.*yandex\..*|.*yahoo\..*|.*mail\.ru)$` | `http://google.com` | `false` | 1674
`^(https:\/\/.*gobwas\.com|http://exclude.gobwas.com)$` | `https://safe.gobwas.com` | `true` | 1039
`^(https:\/\/.*gobwas\.com|http://exclude.gobwas.com)$` | `http://safe.gobwas.com` | `false` | 272
`^abc.*$` | `abcdef` | `true` | 237
`^abc.*$` | `af` | `false` | 100
`^.*def$` | `abcdef` | `true` | 464
`^.*def$` | `af` | `false` | 265
`^ab.*ef$` | `abcdef` | `true` | 375
`^ab.*ef$` | `af` | `false` | 145
[godoc-image]: https://godoc.org/github.com/gobwas/glob?status.svg
[godoc-url]: https://godoc.org/github.com/gobwas/glob
[travis-image]: https://travis-ci.org/gobwas/glob.svg?branch=master
[travis-url]: https://travis-ci.org/gobwas/glob
```go
g := glob.MustCompile("*.github.com", '.')
g.String() // "*.github.com"
g.Separators() // []rune{'.'}
```
## Syntax
Syntax is inspired by [standard wildcards](http://tldp.org/LDP/GNU-Linux-Tools-Summary/html/x11655.htm),
except that `**` is aka super-asterisk, that do not sensitive for separators.
with one addition: `**` (the "super-asterisk"), which matches any sequence
of characters *including* the separators, where `*` stops at them. Note that
it is just that -- a `*` that crosses separators -- and not the `**/`
"globstar" of shells and file globbers: `**/x` requires the literal `/`, so
it does not match `x`; use `{**/,}x` for that. The same applies to a
`**` between separators, e.g. `a/**/b` does not match `a/b`.
```
pattern:
{ term }
term:
`*` matches any sequence of non-separator characters
`**` matches any sequence of characters
`?` matches any single non-separator character
`[` [ `!` ] class `]`
character class; `!` negates it
`{` pattern-list `}`
pattern alternatives
c matches character c (c != `*`, `**`, `?`, `\`, `[`, `{`, `}`)
`\` c matches character c
class:
lo `-` hi matches character c for lo <= c <= hi
{ c } matches any of the listed characters (c != `\`, `]`;
`\` c matches c, `-` is literal here); must be non-empty
pattern-list:
pattern { `,` pattern }
comma-separated (without spaces) patterns
```
### Escaping
The backslash is the escape character: `\*` is a literal asterisk, and a
backslash itself is `\\`. Mind the Go string literals: `"foo\\bar"` is the
pattern `foo\bar`, which is the literal `foobar`, not `foo\bar`. To match a
backslash (e.g. in the Windows paths) write `"foo\\\\bar"` or `` `foo\\bar` ``,
or use `QuoteMeta` on the literal part.
### Separators
The separators are not part of the pattern syntax -- they are configured
once, at compilation time, as the extra arguments of `Compile`:
```go
g := glob.MustCompile("api.*.com", '.', '/')
```
They only limit the wildcards: `*` and `?` never match a separator, while
`**` matches across them; the literals and the character classes are not
affected. With no separators given, `*` and `**` are equivalent. A compiled
`*glob.Pattern` keeps its separators for all matches -- to match the same
pattern with different separators, compile it again.
## Performance
This library is created for compile-once patterns. This means, that
compilation could take time, but strings matching is done faster, than in
case when always parsing template.
If you will not use compiled `*glob.Pattern` object, and do
`g := glob.MustCompile(pattern); g.Match(...)` every time, then your code
will be much more slower.
`Match` performs zero allocations and is safe for concurrent use. Common
pattern shapes (literals, prefixes, suffixes, substrings) are recognized at
compile time and matched with plain string comparisons; the backtracking
engine behind the rest is differentially fuzzed against the `regexp` package
(see `FuzzMatchRegexp`).
Run `go test -bench=.` from source root to see the benchmarks (the numbers
below are from an Apple M4):
Pattern | Fixture | Match | Speed (ns/op)
--------|---------|-------|--------------
`[a-z][!a-x]*cat*[h][!b]*eyes*` | `my cat has very bright eyes` | `true` | 141
`[a-z][!a-x]*cat*[h][!b]*eyes*` | `my dog has very bright eyes` | `false` | 46
`https://*.google.*` | `https://account.google.com` | `true` | 16
`https://*.google.*` | `https://google.com` | `false` | 13
`{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}` | `http://yahoo.com` | `true` | 61
`{https://*.google.*,*yandex.*,*yahoo.*,*mail.ru}` | `http://google.com` | `false` | 70
`{https://*gobwas.com,http://exclude.gobwas.com}` | `https://safe.gobwas.com` | `true` | 24
`{https://*gobwas.com,http://exclude.gobwas.com}` | `http://safe.gobwas.com` | `false` | 32
`google.com` | `google.com` | `true` | 5.0
`google.com` | `gobwas.com` | `false` | 3.9
`abc*` | `abcdef` | `true` | 4.1
`abc*` | `af` | `false` | 3.0
`*def` | `abcdef` | `true` | 4.1
`*def` | `af` | `false` | 2.9
`ab*ef` | `abcdef` | `true` | 6.0
`ab*ef` | `af` | `false` | 3.0
The same things with the `regexp` package -- not to pick on it (it is a
general-purpose engine with much stronger guarantees), but as a reference
for how the glob-shaped specialization pays off per pattern. The regular
expressions are the exact equivalents: anchored, and with the `s` flag
where there is a `*`, since a `*` matches a newline like any other
character (see `BenchmarkCompareGlobAndRegexp`):
Pattern | Fixture | Match | Speed (ns/op) | glob is
--------|---------|-------|---------------|--------
`(?s)^[a-z][^a-x].*cat.*[h][^b].*eyes.*$` | `my cat has very bright eyes` | `true` | 505 | 3.6x faster
`(?s)^[a-z][^a-x].*cat.*[h][^b].*eyes.*$` | `my dog has very bright eyes` | `false` | 221 | 4.9x faster
`(?s)^https://.*\.google\..*$` | `https://account.google.com` | `true` | 251 | 16x faster
`(?s)^https://.*\.google\..*$` | `https://google.com` | `false` | 128 | 9.6x faster
`(?s)^(https://.*\.google\..*\|.*yandex\..*\|.*yahoo\..*\|.*mail\.ru)$` | `http://yahoo.com` | `true` | 396 | 6.5x faster
`(?s)^(https://.*\.google\..*\|.*yandex\..*\|.*yahoo\..*\|.*mail\.ru)$` | `http://google.com` | `false` | 558 | 8.0x faster
`(?s)^(https://.*gobwas\.com\|http://exclude\.gobwas\.com)$` | `https://safe.gobwas.com` | `true` | 210 | 8.8x faster
`(?s)^(https://.*gobwas\.com\|http://exclude\.gobwas\.com)$` | `http://safe.gobwas.com` | `false` | 46 | 1.4x faster
`^google\.com$` | `google.com` | `true` | 25 | 5.0x faster
`^google\.com$` | `gobwas.com` | `false` | 17 | 4.3x faster
`(?s)^abc.*$` | `abcdef` | `true` | 43 | 10x faster
`(?s)^abc.*$` | `af` | `false` | 1.5 | 2.0x slower
`(?s)^.*def$` | `abcdef` | `true` | 73 | 18x faster
`(?s)^.*def$` | `af` | `false` | 1.5 | 1.9x slower
`(?s)^ab.*ef$` | `abcdef` | `true` | 77 | 13x faster
`(?s)^ab.*ef$` | `af` | `false` | 1.5 | 2.0x slower
(The three `slower` rows are the tiny-mismatch cases. Both engines reject
them with the same literal check; `regexp` just reaches it through less
call overhead. In absolute terms it is 1.5ns vs 3ns -- negligible either
way.)
[godoc-image]: https://pkg.go.dev/badge/github.com/gobwas/glob.svg
[godoc-url]: https://pkg.go.dev/github.com/gobwas/glob
[ci-image]: https://github.com/gobwas/glob/actions/workflows/ci.yml/badge.svg?branch=master
[ci-url]: https://github.com/gobwas/glob/actions/workflows/ci.yml
-122
View File
@@ -1,122 +0,0 @@
package ast
import (
"bytes"
"fmt"
)
type Node struct {
Parent *Node
Children []*Node
Value interface{}
Kind Kind
}
func NewNode(k Kind, v interface{}, ch ...*Node) *Node {
n := &Node{
Kind: k,
Value: v,
}
for _, c := range ch {
Insert(n, c)
}
return n
}
func (a *Node) Equal(b *Node) bool {
if a.Kind != b.Kind {
return false
}
if a.Value != b.Value {
return false
}
if len(a.Children) != len(b.Children) {
return false
}
for i, c := range a.Children {
if !c.Equal(b.Children[i]) {
return false
}
}
return true
}
func (a *Node) String() string {
var buf bytes.Buffer
buf.WriteString(a.Kind.String())
if a.Value != nil {
buf.WriteString(" =")
buf.WriteString(fmt.Sprintf("%v", a.Value))
}
if len(a.Children) > 0 {
buf.WriteString(" [")
for i, c := range a.Children {
if i > 0 {
buf.WriteString(", ")
}
buf.WriteString(c.String())
}
buf.WriteString("]")
}
return buf.String()
}
func Insert(parent *Node, children ...*Node) {
parent.Children = append(parent.Children, children...)
for _, ch := range children {
ch.Parent = parent
}
}
type List struct {
Not bool
Chars string
}
type Range struct {
Not bool
Lo, Hi rune
}
type Text struct {
Text string
}
type Kind int
const (
KindNothing Kind = iota
KindPattern
KindList
KindRange
KindText
KindAny
KindSuper
KindSingle
KindAnyOf
)
func (k Kind) String() string {
switch k {
case KindNothing:
return "Nothing"
case KindPattern:
return "Pattern"
case KindList:
return "List"
case KindRange:
return "Range"
case KindText:
return "Text"
case KindAny:
return "Any"
case KindSuper:
return "Super"
case KindSingle:
return "Single"
case KindAnyOf:
return "AnyOf"
default:
return ""
}
}
-157
View File
@@ -1,157 +0,0 @@
package ast
import (
"errors"
"fmt"
"github.com/gobwas/glob/syntax/lexer"
"unicode/utf8"
)
type Lexer interface {
Next() lexer.Token
}
type parseFn func(*Node, Lexer) (parseFn, *Node, error)
func Parse(lexer Lexer) (*Node, error) {
var parser parseFn
root := NewNode(KindPattern, nil)
var (
tree *Node
err error
)
for parser, tree = parserMain, root; parser != nil; {
parser, tree, err = parser(tree, lexer)
if err != nil {
return nil, err
}
}
return root, nil
}
func parserMain(tree *Node, lex Lexer) (parseFn, *Node, error) {
for {
token := lex.Next()
switch token.Type {
case lexer.EOF:
return nil, tree, nil
case lexer.Error:
return nil, tree, errors.New(token.Raw)
case lexer.Text:
Insert(tree, NewNode(KindText, Text{token.Raw}))
return parserMain, tree, nil
case lexer.Any:
Insert(tree, NewNode(KindAny, nil))
return parserMain, tree, nil
case lexer.Super:
Insert(tree, NewNode(KindSuper, nil))
return parserMain, tree, nil
case lexer.Single:
Insert(tree, NewNode(KindSingle, nil))
return parserMain, tree, nil
case lexer.RangeOpen:
return parserRange, tree, nil
case lexer.TermsOpen:
a := NewNode(KindAnyOf, nil)
Insert(tree, a)
p := NewNode(KindPattern, nil)
Insert(a, p)
return parserMain, p, nil
case lexer.Separator:
p := NewNode(KindPattern, nil)
Insert(tree.Parent, p)
return parserMain, p, nil
case lexer.TermsClose:
return parserMain, tree.Parent.Parent, nil
default:
return nil, tree, fmt.Errorf("unexpected token: %s", token)
}
}
return nil, tree, fmt.Errorf("unknown error")
}
func parserRange(tree *Node, lex Lexer) (parseFn, *Node, error) {
var (
not bool
lo rune
hi rune
chars string
)
for {
token := lex.Next()
switch token.Type {
case lexer.EOF:
return nil, tree, errors.New("unexpected end")
case lexer.Error:
return nil, tree, errors.New(token.Raw)
case lexer.Not:
not = true
case lexer.RangeLo:
r, w := utf8.DecodeRuneInString(token.Raw)
if len(token.Raw) > w {
return nil, tree, fmt.Errorf("unexpected length of lo character")
}
lo = r
case lexer.RangeBetween:
//
case lexer.RangeHi:
r, w := utf8.DecodeRuneInString(token.Raw)
if len(token.Raw) > w {
return nil, tree, fmt.Errorf("unexpected length of lo character")
}
hi = r
if hi < lo {
return nil, tree, fmt.Errorf("hi character '%s' should be greater than lo '%s'", string(hi), string(lo))
}
case lexer.Text:
chars = token.Raw
case lexer.RangeClose:
isRange := lo != 0 && hi != 0
isChars := chars != ""
if isChars == isRange {
return nil, tree, fmt.Errorf("could not parse range")
}
if isRange {
Insert(tree, NewNode(KindRange, Range{
Lo: lo,
Hi: hi,
Not: not,
}))
} else {
Insert(tree, NewNode(KindList, List{
Chars: chars,
Not: not,
}))
}
return parserMain, tree, nil
}
}
}
+381
View File
@@ -0,0 +1,381 @@
// Package syntax implements the lexer of the glob pattern syntax. The parser
// lives in package glob; the syntax itself is described at [glob.Compile].
package syntax
import (
"bytes"
"fmt"
"slices"
"unicode/utf8"
)
// TokenType tells the kind of a [Token].
type TokenType int
const (
// EOF marks the end of the input; the lexer returns it repeatedly.
EOF TokenType = iota
// Error carries an error message in Token.Data; the lexer keeps
// returning it once it happened. Note that the lexer catches only the
// errors local to a token (an invalid UTF-8 sequence, a malformed
// character class): the structural ones, like an unclosed `{`, are for
// the parser to detect.
Error
// Text is a run of literal characters, with the escapes resolved.
Text
// Any is the `*` wildcard.
Any
// Super is the `**` wildcard.
Super
// Single is the `?` wildcard.
Single
// Not is the `!` right after the `[` of a character class.
Not
// TermSeparator is the `,` between the alternatives of a `{...}` group.
// Outside of a group a comma is a plain Text character.
TermSeparator
// RangeOpen and RangeClose are the `[` and `]` of a character class.
// Between them the lexer produces either a Text token (a set of
// characters, `[abc]`) or a RangeLo, RangeBetween, RangeHi triple (a
// range, `[a-c]`), possibly preceded by Not.
RangeOpen
RangeClose
RangeLo
RangeHi
RangeBetween
// TermsOpen and TermsClose are the `{` and `}` of an alternatives group.
TermsOpen
TermsClose
)
func (tt TokenType) String() string {
switch tt {
case EOF:
return "eof"
case Error:
return "error"
case Text:
return "text"
case Any:
return "any"
case Super:
return "super"
case Single:
return "single"
case Not:
return "not"
case TermSeparator:
return "separator"
case RangeOpen:
return "range_open"
case RangeClose:
return "range_close"
case RangeLo:
return "range_lo"
case RangeHi:
return "range_hi"
case RangeBetween:
return "range_between"
case TermsOpen:
return "terms_open"
case TermsClose:
return "terms_close"
default:
return "<unknown token type>"
}
}
// Token is a lexeme of the pattern: its kind and the source text it was
// read from (or the error message for Error, the literal characters with
// the escapes resolved for Text).
type Token struct {
Type TokenType
Data string
}
func (t Token) String() string {
return fmt.Sprintf("%v<%q>", t.Type, t.Data)
}
const (
char_any = '*'
char_comma = ','
char_single = '?'
char_escape = '\\'
char_range_open = '['
char_range_close = ']'
char_terms_open = '{'
char_terms_close = '}'
char_range_not = '!'
char_range_between = '-'
)
var specials = []byte{
char_any,
char_single,
char_escape,
char_range_open,
char_range_close,
char_terms_open,
char_terms_close,
}
// IsSpecial reports whether c is a glob meta character, that is, one that
// [glob.QuoteMeta] escapes. Note that `,`, `!` and `-` are not among them:
// they are special only inside `{...}` and `[...]` respectively, which are.
func IsSpecial(c byte) bool {
return bytes.IndexByte(specials, c) != -1
}
type tokens []Token
func (i *tokens) shift() (ret Token) {
ret = (*i)[0]
copy(*i, (*i)[1:])
*i = (*i)[:len(*i)-1]
return
}
func (i *tokens) push(v Token) {
*i = append(*i, v)
}
func (i *tokens) empty() bool {
return len(*i) == 0
}
// eof is the end-of-input sentinel. It must not collide with any rune that
// can appear in a valid pattern -- note that U+0000 can.
const eof rune = -1
// Lexer splits a pattern into tokens; see [Lexer.Next].
type Lexer struct {
data string
pos int
err error
tokens tokens
termsLevel int
lastRune rune
lastRuneSize int
hasRune bool
}
// NewLexer returns a lexer over the source pattern.
func NewLexer(source string) *Lexer {
l := &Lexer{
data: source,
tokens: tokens(make([]Token, 0, 4)),
}
return l
}
// Offset returns the byte offset in the source the lexer stopped at, that
// is, the position right after the most recently returned token.
func (l *Lexer) Offset() int {
return l.pos
}
// Next returns the next token. Once the input is over it returns EOF, and
// once an error happened it returns that Error, repeatedly.
func (l *Lexer) Next() Token {
if l.err != nil {
return Token{Error, l.err.Error()}
}
if !l.tokens.empty() {
return l.tokens.shift()
}
l.fetchItem()
return l.Next()
}
func (l *Lexer) peek() (r rune, w int) {
if l.pos == len(l.data) {
return eof, 0
}
r, w = utf8.DecodeRuneInString(l.data[l.pos:])
if r == utf8.RuneError && w == 1 {
// An invalid encoding: a valid U+FFFD decodes at its width of 3.
l.errorf("invalid UTF-8 sequence")
r = eof
w = 0
}
return
}
func (l *Lexer) read() rune {
if l.hasRune {
l.hasRune = false
l.seek(l.lastRuneSize)
return l.lastRune
}
r, s := l.peek()
l.seek(s)
l.lastRune = r
l.lastRuneSize = s
return r
}
func (l *Lexer) seek(w int) {
l.pos += w
}
func (l *Lexer) unread() {
if l.hasRune {
l.errorf("could not unread rune")
return
}
l.seek(-l.lastRuneSize)
l.hasRune = true
}
func (l *Lexer) errorf(f string, v ...any) {
l.err = fmt.Errorf(f, v...)
}
func (l *Lexer) inTerms() bool {
return l.termsLevel > 0
}
func (l *Lexer) termsEnter() {
l.termsLevel++
}
func (l *Lexer) termsLeave() {
l.termsLevel--
}
var inTextBreakers = []rune{char_single, char_any, char_range_open, char_terms_open}
var inTermsBreakers = append(inTextBreakers, char_terms_close, char_comma)
func (l *Lexer) fetchItem() {
r := l.read()
switch {
case r == eof:
l.tokens.push(Token{EOF, ""})
case r == char_terms_open:
l.termsEnter()
l.tokens.push(Token{TermsOpen, string(r)})
case r == char_comma && l.inTerms():
l.tokens.push(Token{TermSeparator, string(r)})
case r == char_terms_close && l.inTerms():
l.tokens.push(Token{TermsClose, string(r)})
l.termsLeave()
case r == char_range_open:
l.tokens.push(Token{RangeOpen, string(r)})
l.fetchRange()
case r == char_single:
l.tokens.push(Token{Single, string(r)})
case r == char_any:
if l.read() == char_any {
l.tokens.push(Token{Super, string(r) + string(r)})
} else {
l.unread()
l.tokens.push(Token{Any, string(r)})
}
default:
l.unread()
var breakers []rune
if l.inTerms() {
breakers = inTermsBreakers
} else {
breakers = inTextBreakers
}
l.fetchText(breakers)
}
}
func (l *Lexer) fetchRange() {
var wantHi bool
var wantClose bool
var seenNot bool
for {
r := l.read()
if r == eof {
l.errorf("unexpected end of input")
return
}
if wantClose {
if r != char_range_close {
l.errorf("expected close range character")
} else {
l.tokens.push(Token{RangeClose, string(r)})
}
return
}
if wantHi {
l.tokens.push(Token{RangeHi, string(r)})
wantClose = true
continue
}
if !seenNot && r == char_range_not {
l.tokens.push(Token{Not, string(r)})
seenNot = true
continue
}
if n, w := l.peek(); n == char_range_between {
l.seek(w)
l.tokens.push(Token{RangeLo, string(r)})
l.tokens.push(Token{RangeBetween, string(n)})
wantHi = true
continue
}
l.unread() // unread first peek and fetch as text
l.fetchText([]rune{char_range_close})
wantClose = true
}
}
func (l *Lexer) fetchText(breakers []rune) {
var data []rune
var escaped bool
reading:
for {
r := l.read()
if r == eof {
if escaped {
l.errorf("trailing backslash")
}
break
}
if !escaped {
if r == char_escape {
escaped = true
continue
}
if slices.Index(breakers, r) != -1 {
l.unread()
break reading
}
}
escaped = false
data = append(data, r)
}
if len(data) > 0 {
l.tokens.push(Token{Text, string(data)})
}
}
-273
View File
@@ -1,273 +0,0 @@
package lexer
import (
"bytes"
"fmt"
"github.com/gobwas/glob/util/runes"
"unicode/utf8"
)
const (
char_any = '*'
char_comma = ','
char_single = '?'
char_escape = '\\'
char_range_open = '['
char_range_close = ']'
char_terms_open = '{'
char_terms_close = '}'
char_range_not = '!'
char_range_between = '-'
)
var specials = []byte{
char_any,
char_single,
char_escape,
char_range_open,
char_range_close,
char_terms_open,
char_terms_close,
}
func Special(c byte) bool {
return bytes.IndexByte(specials, c) != -1
}
type tokens []Token
func (i *tokens) shift() (ret Token) {
ret = (*i)[0]
copy(*i, (*i)[1:])
*i = (*i)[:len(*i)-1]
return
}
func (i *tokens) push(v Token) {
*i = append(*i, v)
}
func (i *tokens) empty() bool {
return len(*i) == 0
}
var eof rune = 0
type lexer struct {
data string
pos int
err error
tokens tokens
termsLevel int
lastRune rune
lastRuneSize int
hasRune bool
}
func NewLexer(source string) *lexer {
l := &lexer{
data: source,
tokens: tokens(make([]Token, 0, 4)),
}
return l
}
func (l *lexer) Next() Token {
if l.err != nil {
return Token{Error, l.err.Error()}
}
if !l.tokens.empty() {
return l.tokens.shift()
}
l.fetchItem()
return l.Next()
}
func (l *lexer) peek() (r rune, w int) {
if l.pos == len(l.data) {
return eof, 0
}
r, w = utf8.DecodeRuneInString(l.data[l.pos:])
if r == utf8.RuneError {
l.errorf("could not read rune")
r = eof
w = 0
}
return
}
func (l *lexer) read() rune {
if l.hasRune {
l.hasRune = false
l.seek(l.lastRuneSize)
return l.lastRune
}
r, s := l.peek()
l.seek(s)
l.lastRune = r
l.lastRuneSize = s
return r
}
func (l *lexer) seek(w int) {
l.pos += w
}
func (l *lexer) unread() {
if l.hasRune {
l.errorf("could not unread rune")
return
}
l.seek(-l.lastRuneSize)
l.hasRune = true
}
func (l *lexer) errorf(f string, v ...interface{}) {
l.err = fmt.Errorf(f, v...)
}
func (l *lexer) inTerms() bool {
return l.termsLevel > 0
}
func (l *lexer) termsEnter() {
l.termsLevel++
}
func (l *lexer) termsLeave() {
l.termsLevel--
}
var inTextBreakers = []rune{char_single, char_any, char_range_open, char_terms_open}
var inTermsBreakers = append(inTextBreakers, char_terms_close, char_comma)
func (l *lexer) fetchItem() {
r := l.read()
switch {
case r == eof:
l.tokens.push(Token{EOF, ""})
case r == char_terms_open:
l.termsEnter()
l.tokens.push(Token{TermsOpen, string(r)})
case r == char_comma && l.inTerms():
l.tokens.push(Token{Separator, string(r)})
case r == char_terms_close && l.inTerms():
l.tokens.push(Token{TermsClose, string(r)})
l.termsLeave()
case r == char_range_open:
l.tokens.push(Token{RangeOpen, string(r)})
l.fetchRange()
case r == char_single:
l.tokens.push(Token{Single, string(r)})
case r == char_any:
if l.read() == char_any {
l.tokens.push(Token{Super, string(r) + string(r)})
} else {
l.unread()
l.tokens.push(Token{Any, string(r)})
}
default:
l.unread()
var breakers []rune
if l.inTerms() {
breakers = inTermsBreakers
} else {
breakers = inTextBreakers
}
l.fetchText(breakers)
}
}
func (l *lexer) fetchRange() {
var wantHi bool
var wantClose bool
var seenNot bool
for {
r := l.read()
if r == eof {
l.errorf("unexpected end of input")
return
}
if wantClose {
if r != char_range_close {
l.errorf("expected close range character")
} else {
l.tokens.push(Token{RangeClose, string(r)})
}
return
}
if wantHi {
l.tokens.push(Token{RangeHi, string(r)})
wantClose = true
continue
}
if !seenNot && r == char_range_not {
l.tokens.push(Token{Not, string(r)})
seenNot = true
continue
}
if n, w := l.peek(); n == char_range_between {
l.seek(w)
l.tokens.push(Token{RangeLo, string(r)})
l.tokens.push(Token{RangeBetween, string(n)})
wantHi = true
continue
}
l.unread() // unread first peek and fetch as text
l.fetchText([]rune{char_range_close})
wantClose = true
}
}
func (l *lexer) fetchText(breakers []rune) {
var data []rune
var escaped bool
reading:
for {
r := l.read()
if r == eof {
break
}
if !escaped {
if r == char_escape {
escaped = true
continue
}
if runes.IndexRune(breakers, r) != -1 {
l.unread()
break reading
}
}
escaped = false
data = append(data, r)
}
if len(data) > 0 {
l.tokens.push(Token{Text, string(data)})
}
}
-88
View File
@@ -1,88 +0,0 @@
package lexer
import "fmt"
type TokenType int
const (
EOF TokenType = iota
Error
Text
Char
Any
Super
Single
Not
Separator
RangeOpen
RangeClose
RangeLo
RangeHi
RangeBetween
TermsOpen
TermsClose
)
func (tt TokenType) String() string {
switch tt {
case EOF:
return "eof"
case Error:
return "error"
case Text:
return "text"
case Char:
return "char"
case Any:
return "any"
case Super:
return "super"
case Single:
return "single"
case Not:
return "not"
case Separator:
return "separator"
case RangeOpen:
return "range_open"
case RangeClose:
return "range_close"
case RangeLo:
return "range_lo"
case RangeHi:
return "range_hi"
case RangeBetween:
return "range_between"
case TermsOpen:
return "terms_open"
case TermsClose:
return "terms_close"
default:
return "undef"
}
}
type Token struct {
Type TokenType
Raw string
}
func (t Token) String() string {
return fmt.Sprintf("%v<%q>", t.Type, t.Raw)
}
-14
View File
@@ -1,14 +0,0 @@
package syntax
import (
"github.com/gobwas/glob/syntax/ast"
"github.com/gobwas/glob/syntax/lexer"
)
func Parse(s string) (*ast.Node, error) {
return ast.Parse(lexer.NewLexer(s))
}
func Special(b byte) bool {
return lexer.Special(b)
}
-154
View File
@@ -1,154 +0,0 @@
package runes
func Index(s, needle []rune) int {
ls, ln := len(s), len(needle)
switch {
case ln == 0:
return 0
case ln == 1:
return IndexRune(s, needle[0])
case ln == ls:
if Equal(s, needle) {
return 0
}
return -1
case ln > ls:
return -1
}
head:
for i := 0; i < ls && ls-i >= ln; i++ {
for y := 0; y < ln; y++ {
if s[i+y] != needle[y] {
continue head
}
}
return i
}
return -1
}
func LastIndex(s, needle []rune) int {
ls, ln := len(s), len(needle)
switch {
case ln == 0:
if ls == 0 {
return 0
}
return ls
case ln == 1:
return IndexLastRune(s, needle[0])
case ln == ls:
if Equal(s, needle) {
return 0
}
return -1
case ln > ls:
return -1
}
head:
for i := ls - 1; i >= 0 && i >= ln; i-- {
for y := ln - 1; y >= 0; y-- {
if s[i-(ln-y-1)] != needle[y] {
continue head
}
}
return i - ln + 1
}
return -1
}
// IndexAny returns the index of the first instance of any Unicode code point
// from chars in s, or -1 if no Unicode code point from chars is present in s.
func IndexAny(s, chars []rune) int {
if len(chars) > 0 {
for i, c := range s {
for _, m := range chars {
if c == m {
return i
}
}
}
}
return -1
}
func Contains(s, needle []rune) bool {
return Index(s, needle) >= 0
}
func Max(s []rune) (max rune) {
for _, r := range s {
if r > max {
max = r
}
}
return
}
func Min(s []rune) rune {
min := rune(-1)
for _, r := range s {
if min == -1 {
min = r
continue
}
if r < min {
min = r
}
}
return min
}
func IndexRune(s []rune, r rune) int {
for i, c := range s {
if c == r {
return i
}
}
return -1
}
func IndexLastRune(s []rune, r rune) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == r {
return i
}
}
return -1
}
func Equal(a, b []rune) bool {
if len(a) == len(b) {
for i := 0; i < len(a); i++ {
if a[i] != b[i] {
return false
}
}
return true
}
return false
}
// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix []rune) bool {
return len(s) >= len(prefix) && Equal(s[0:len(prefix)], prefix)
}
// HasSuffix tests whether the string s ends with suffix.
func HasSuffix(s, suffix []rune) bool {
return len(s) >= len(suffix) && Equal(s[len(s)-len(suffix):], suffix)
}
-39
View File
@@ -1,39 +0,0 @@
package strings
import (
"strings"
"unicode/utf8"
)
func IndexAnyRunes(s string, rs []rune) int {
for _, r := range rs {
if i := strings.IndexRune(s, r); i != -1 {
return i
}
}
return -1
}
func LastIndexAnyRunes(s string, rs []rune) int {
for _, r := range rs {
i := -1
if 0 <= r && r < utf8.RuneSelf {
i = strings.LastIndexByte(s, byte(r))
} else {
sub := s
for len(sub) > 0 {
j := strings.IndexRune(s, r)
if j == -1 {
break
}
i = j
sub = sub[i+1:]
}
}
if i != -1 {
return i
}
}
return -1
}
+14
View File
@@ -0,0 +1,14 @@
version: "2"
# The linter set is golangci-lint's default. The point of this file is the
# formatters block below: without it nothing checked gofmt, and the drift that
# allowed put a closing code fence on the same line as a line of Go, which
# broke half of README.md once the doc generator started running.
formatters:
enable:
- gofmt
issues:
# Report every unformatted file. The default caps repeats of one message at
# three, which would hide the tail of exactly this kind of sweep.
max-same-issues: 0
+52 -1
View File
@@ -1,6 +1,57 @@
Changes
=======
v1.4.0 20 Aug 2026
* Add ML-DSA (FIPS 204) support: the `MLDSA44`, `MLDSA65`, and `MLDSA87`
algorithms, the `SignMLDSA()` / `VerifyMLDSA()` primitives, and a new
`MLDSAFamily` algorithm family. ML-DSA requires Go 1.27 or later, which is
when `crypto/mldsa` joins the standard library; on earlier toolchains the
constants are not declared and the algorithms are not registered.
An ML-DSA key carries its own parameter set, so naming an algorithm that
disagrees with the key is an error on both the sign and verify paths.
`SignMLDSA()` takes a `crypto.SignerOpts` so that both signing modes stay
reachable: an `*mldsa.Options` supplies a context string, and
`crypto.MLDSAMu` signs a pre-hashed mu message representative.
`VerifyMLDSA()` takes an `*mldsa.Options` because verification has a single
mode. `SignDigest()` and `VerifyDigest()` return an error for ML-DSA, as
they already do for EdDSA.
* The minimum Go version is now 1.25.
v1.3.0 13 Apr 2026
* Add `SignWithOpts()` and `VerifyWithOpts()`, which thread an optional
`crypto.SignerOpts` through to the underlying signer. For built-in
families (HMAC, RSA, ECDSA, EdDSA) the opts argument is ignored. For
the `Custom` family, opts are forwarded to the algorithm's Meta when
it implements the new `SignerWithOpts` / `VerifierWithOpts` interfaces;
otherwise the dispatcher falls back to the plain `Signer` / `Verifier`
methods and the opts are dropped. The canonical use case is composite
ML-DSA signatures, where a per-call domain-separation context
(`*mldsa.Options`) must reach `filippo.io/mldsa`.
* `Sign()` is now a one-line wrapper around `SignWithOpts()` (and
`Verify()` likewise wraps `VerifyWithOpts()`). The public signatures
of `Sign` and `Verify` are unchanged; the only observable difference
for existing callers is one extra call frame.
* `RegisterAlgorithm()` for the `Custom` family now accepts a Meta that
implements only `SignerWithOpts` / `VerifierWithOpts` (in addition to
the existing `Signer` / `Verifier` paths).
* In dsig v2, `Sign` / `Verify` will absorb the opts parameter and
`SignWithOpts` / `VerifyWithOpts` will be removed. The same migration
is planned for `SignDigest` / `VerifyDigest` once a `DigestSigner`
interface for the `Custom` family lands. Doc comments on all four
entry points flag the upcoming change.
v1.2.2 13 Apr 2026
* Add `SignECDSADER()` and `VerifyECDSADER()` primitive helpers for ECDSA
signatures in ASN.1 DER-encoded `Ecdsa-Sig-Value` form (RFC 3279 §2.2.3),
as used by X.509/PKIX and composite signature schemes. The existing
`SignECDSA()`/`VerifyECDSA()` functions remain the canonical entry points
for the JWS-native fixed-length r||s format (RFC 7515 §3.4).
v1.2.1 7 Apr 2026
* Add `SignDigest()` for signing pre-computed digests. Supported for HMAC,
RSA (PKCS1v15 and PSS), and ECDSA families. EdDSA and Custom return an error.
@@ -25,4 +76,4 @@ v1.1.0 2 Apr 2026
algorithm name. Use `UnregisterAlgorithm()` first if you need to replace it.
v1.0.0 - 18 Aug 2025
* Initial release
* Initial release
+50 -2
View File
@@ -1,4 +1,4 @@
# 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)
# 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)
Go module providing low-level digital signature operations.
@@ -9,6 +9,7 @@ While there are many standards for generating and verifying digital signatures,
* RSA signatures (PKCS1v15 and PSS)
* ECDSA signatures (P-256, P-384, P-521)
* EdDSA signatures (Ed25519, Ed448)
* ML-DSA post-quantum signatures (ML-DSA-44, ML-DSA-65, ML-DSA-87), on Go 1.27 and later
* HMAC signatures (SHA-256, SHA-384, SHA-512)
* Support for crypto.Signer interface
* Custom algorithm registration via `Signer`/`Verifier` interfaces
@@ -143,6 +144,53 @@ source: [examples/dsig_readme_example_test.go](https://github.com/lestrrat-go/ds
| `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 |
| `MLDSA44` | ML-DSA-44 (FIPS 204), NIST level 2 | *mldsa.PrivateKey / *mldsa.PublicKey |
| `MLDSA65` | ML-DSA-65 (FIPS 204), NIST level 3 | *mldsa.PrivateKey / *mldsa.PublicKey |
| `MLDSA87` | ML-DSA-87 (FIPS 204), NIST level 5 | *mldsa.PrivateKey / *mldsa.PublicKey |
The three ML-DSA algorithms need Go 1.27 or later, which is when `crypto/mldsa`
joins the standard library. On earlier toolchains the constants are not declared
and the algorithms are not registered.
Name the constant that matches the key you generated. A key knows its own
parameter set, and naming a different one is an error, so a key cannot be used
under a weaker set by accident:
```go
sk, _ := mldsa.GenerateKey(mldsa.MLDSA65())
sig, _ := dsig.Sign(sk, dsig.MLDSA65, payload, nil)
err := dsig.Verify(sk.PublicKey(), dsig.MLDSA65, payload, sig)
_, err = dsig.Sign(sk, dsig.MLDSA44, payload, nil)
// dsig.SignWithOpts: ML-DSA parameter set mismatch: key is ML-DSA-65, algorithm is ML-DSA-44
```
## ML-DSA context strings
ML-DSA can mix a caller-chosen string into the signature. Give each job a
different context and one key can sign for several of them without a signature
made for one job verifying as another, so a login token cannot be presented as
a file receipt.
Signing and verifying must use the same context. A verifier that supplies the
wrong one, or none at all, sees an ordinary invalid signature and cannot tell
which mistake was made:
```go
login := &mldsa.Options{Context: "my-app/login-token"}
receipt := &mldsa.Options{Context: "my-app/file-receipt"}
sig, _ := dsig.SignWithOpts(sk, dsig.MLDSA65, payload, login, nil)
err := dsig.VerifyWithOpts(sk.PublicKey(), dsig.MLDSA65, payload, sig, login)
// nil
err = dsig.VerifyWithOpts(sk.PublicKey(), dsig.MLDSA65, payload, sig, receipt)
// mldsa: invalid signature
```
A context is at most 255 bytes, and it is empty when opts is nil.
# Description
@@ -160,4 +208,4 @@ 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
* [github.com/lestrrat-go/jwx](https://github.com/lestrrat-go/jwx) - JOSE (JWA/JWE/JWK/JWS/JWT) implementation
+1 -1
View File
@@ -34,4 +34,4 @@ const (
// EdDSA signature algorithms
// These use Edwards-curve Digital Signature Algorithm (supports Ed25519 and Ed448)
EdDSA = "EDDSA"
)
)
+41 -5
View File
@@ -28,6 +28,12 @@ const (
ECDSA
EdDSAFamily
Custom
// MLDSAFamily covers the ML-DSA parameter sets. It is deliberately not
// Custom: Custom means this library knows nothing about the algorithm,
// which would be false here and misleads callers that switch on Family.
//
// It sits after Custom so the values earlier releases assigned stay put.
MLDSAFamily
maxFamily
)
@@ -44,6 +50,8 @@ func (f Family) String() string {
return "EdDSA"
case Custom:
return "Custom"
case MLDSAFamily:
return "ML-DSA"
default:
return "InvalidFamily"
}
@@ -85,6 +93,22 @@ type Signer interface {
Sign(key any, payload []byte, rand io.Reader) ([]byte, error)
}
// SignerWithOpts is an optional interface that Custom-family signers
// can implement to receive a per-call [crypto.SignerOpts]. The
// canonical use case is ML-DSA, whose Sign method accepts an
// *mldsa.Options carrying a domain-separation context that the plain
// [Signer] interface cannot convey. Custom Meta values that do not
// implement this interface still work with [SignWithOpts]: the
// dispatcher falls back to the plain [Signer.Sign] method and the opts
// argument is dropped.
//
// Implementing both [Signer] and SignerWithOpts is supported, but
// implementing only SignerWithOpts is sufficient because the dispatcher
// checks for it first.
type SignerWithOpts interface {
SignWithOpts(key any, payload []byte, opts crypto.SignerOpts, rand io.Reader) ([]byte, error)
}
// Verifier is an interface for custom verification implementations.
// For the Custom algorithm family, info.Meta must implement this interface
// to support verification. The implementation struct can carry any additional
@@ -93,6 +117,12 @@ type Verifier interface {
Verify(key any, payload, signature []byte) error
}
// VerifierWithOpts is the verification counterpart of [SignerWithOpts].
// See [SignerWithOpts] for usage notes.
type VerifierWithOpts interface {
VerifyWithOpts(key any, payload, signature []byte, opts crypto.SignerOpts) error
}
var algorithms = make(map[string]AlgorithmInfo)
var builtinAlgorithms = make(map[string]struct{})
var muAlgorithms sync.RWMutex
@@ -102,7 +132,8 @@ var muAlgorithms sync.RWMutex
// info.Meta should contain extra metadata for some algorithms. HMAC, RSA, and ECDSA
// families need their respective metadata (HMACFamilyMeta, RSAFamilyMeta, and
// ECDSAFamilyMeta). Metadata for EdDSA is optional. For the Custom family, Meta
// must implement at least one of the Signer or Verifier interfaces.
// must implement at least one of the Signer, SignerWithOpts, Verifier, or
// VerifierWithOpts interfaces.
//
// Re-registration of an already-registered algorithm name is rejected. Use
// UnregisterAlgorithm to remove it first if you need to replace it.
@@ -130,11 +161,17 @@ func RegisterAlgorithm(name string, info AlgorithmInfo) error {
}
case EdDSAFamily:
// EdDSA metadata is optional for now
case Custom:
case Custom, MLDSAFamily:
// Both families carry their implementation in Meta. The other families
// put passive metadata there. For ML-DSA this is forced: crypto/mldsa
// exists only from Go 1.27, so the algorithm cannot be described by a
// value type this file could name.
_, isSigner := info.Meta.(Signer)
_, isSignerWithOpts := info.Meta.(SignerWithOpts)
_, isVerifier := info.Meta.(Verifier)
if !isSigner && !isVerifier {
return fmt.Errorf("custom algorithm %s: Meta must implement Signer and/or Verifier", name)
_, isVerifierWithOpts := info.Meta.(VerifierWithOpts)
if !isSigner && !isSignerWithOpts && !isVerifier && !isVerifierWithOpts {
return fmt.Errorf("%s algorithm %s: Meta must implement Signer, SignerWithOpts, Verifier, or VerifierWithOpts", info.Family, name)
}
default:
return fmt.Errorf("unsupported algorithm family %s for algorithm %s", info.Family, name)
@@ -272,4 +309,3 @@ func init() {
builtinAlgorithms[name] = struct{}{}
}
}
+41 -1
View File
@@ -12,7 +12,6 @@ import (
"github.com/lestrrat-go/dsig/internal/ecutil"
)
func ecdsaGetSignerKey(key any) (*ecdsa.PrivateKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
@@ -124,6 +123,47 @@ func SignECDSA(key *ecdsa.PrivateKey, payload []byte, h crypto.Hash, rr io.Reade
return PackECDSASignature(r, s, key.Curve.Params().BitSize)
}
// SignECDSADER generates an ECDSA signature in ASN.1 DER-encoded Ecdsa-Sig-Value
// format (RFC 3279 §2.2.3), as required by X.509/PKIX and composite signature
// schemes such as draft-ietf-lamps-pq-composite-sigs. For the fixed-length
// JWS r||s format (RFC 7515 §3.4), use SignECDSA instead.
//
// The payload is hashed with h before signing. rr provides randomness; if nil,
// rand.Reader is used.
func SignECDSADER(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
}
sig, err := ecdsa.SignASN1(rr, key, digest)
if err != nil {
return nil, fmt.Errorf(`failed to sign payload using ecdsa: %w`, err)
}
return sig, nil
}
// VerifyECDSADER verifies an ECDSA signature in ASN.1 DER-encoded
// Ecdsa-Sig-Value format. See SignECDSADER for the format distinction. The
// payload is hashed with h before verification.
func VerifyECDSADER(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash) error {
hh := h.New()
hh.Write(payload)
digest := hh.Sum(nil)
if !ecdsa.VerifyASN1(key, digest, signature) {
return NewVerificationError("invalid ECDSA signature")
}
return nil
}
// 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).
+193
View File
@@ -0,0 +1,193 @@
//go:build go1.27
package dsig
import (
"crypto"
"crypto/mldsa"
"fmt"
"io"
)
// ML-DSA signature algorithms, the post-quantum scheme specified in FIPS 204.
// The three names identify the three parameter sets, which differ in security
// level and in key and signature sizes.
//
// These names match what crypto/mldsa's Parameters.String reports, so the
// parameter set a key carries can be compared against the algorithm name
// directly.
//
// ML-DSA is available only when dsig is built with Go 1.27 or later, which is
// when crypto/mldsa becomes part of the standard library. On earlier
// toolchains these algorithms are not registered and not declared.
const (
MLDSA44 = "ML-DSA-44"
MLDSA65 = "ML-DSA-65"
MLDSA87 = "ML-DSA-87"
)
func init() {
for _, params := range []mldsa.Parameters{mldsa.MLDSA44(), mldsa.MLDSA65(), mldsa.MLDSA87()} {
name := params.String()
if err := RegisterAlgorithm(name, AlgorithmInfo{
Family: MLDSAFamily,
Meta: &mldsaAlgorithm{params: params},
}); err != nil {
panic(fmt.Sprintf("failed to register algorithm %s: %v", name, err))
}
builtinAlgorithms[name] = struct{}{}
}
}
// SignMLDSA generates an ML-DSA signature for the given payload.
//
// opts may be nil, which signs payload directly with no context. Pass an
// *[mldsa.Options] to supply a domain-separation context, which [VerifyMLDSA]
// then requires to match.
//
// opts is a [crypto.SignerOpts] so that both of ML-DSA's signing modes stay
// expressible. Passing [crypto.MLDSAMu] means payload holds a pre-hashed μ
// message representative. That mode is a shortcut for callers who already have
// μ, and it produces an ordinary signature; [VerifyMLDSA] checks it against the
// original message, and the verify side needs no counterpart.
//
// crypto/mldsa rejects any other opts value, so a mistaken type cannot be
// silently downgraded to a context-free signature.
func SignMLDSA(key *mldsa.PrivateKey, payload []byte, opts crypto.SignerOpts) ([]byte, error) {
if key == nil {
return nil, fmt.Errorf(`dsig.SignMLDSA: key cannot be nil`)
}
// The io.Reader argument is ignored by crypto/mldsa; signing draws its own
// randomness. SignDeterministic is the variant that draws none.
return key.Sign(nil, payload, opts)
}
// VerifyMLDSA verifies an ML-DSA signature for the given payload.
//
// opts may be nil. It must carry the same Context that was used to produce the
// signature, otherwise verification fails.
//
// Verification has a single mode, so opts is a concrete *[mldsa.Options]. μ is
// derived from the message, so a signature made from a pre-hashed μ verifies
// here against the original message.
func VerifyMLDSA(key *mldsa.PublicKey, payload, signature []byte, opts *mldsa.Options) error {
if key == nil {
return fmt.Errorf(`dsig.VerifyMLDSA: key cannot be nil`)
}
return mldsa.Verify(key, payload, signature, opts)
}
// mldsaAlgorithm is the Custom-family adapter that binds one ML-DSA parameter
// set to the registry. It carries the parameter set so that every operation can
// check the caller's key against the algorithm that was asked for.
type mldsaAlgorithm struct {
params mldsa.Parameters
}
// requireMLDSAParams reports whether a caller-supplied key belongs to the
// parameter set this algorithm was registered for. crypto/mldsa's Parameters is
// a comparable value naming one of the three FIPS 204 sets, so a plain
// comparison suffices.
//
// The check matters because the key owns the parameter set, and the call only
// names one. Without it, an ML-DSA-65 key would happily produce and verify
// ML-DSA-65 signatures while the caller believed it had selected ML-DSA-44.
// Anything that reads the algorithm name to decide a post-quantum security
// level would then be misled, so the mismatch is an error.
func (a *mldsaAlgorithm) requireMLDSAParams(got mldsa.Parameters) error {
if got != a.params {
return fmt.Errorf(`ML-DSA parameter set mismatch: key is %s, algorithm is %s`, got, a.params)
}
return nil
}
func (a *mldsaAlgorithm) privateKey(key any) (*mldsa.PrivateKey, error) {
sk, ok := key.(*mldsa.PrivateKey)
if !ok {
return nil, fmt.Errorf(`expected *mldsa.PrivateKey, got %T`, key)
}
if err := a.requireMLDSAParams(sk.PublicKey().Parameters()); err != nil {
return nil, err
}
return sk, nil
}
// publicKey narrows the key types the verify surface accepts. A private key is
// allowed so callers holding only one half do not have to unwrap it themselves.
func (a *mldsaAlgorithm) publicKey(key any) (*mldsa.PublicKey, error) {
var pk *mldsa.PublicKey
switch k := key.(type) {
case *mldsa.PublicKey:
pk = k
case *mldsa.PrivateKey:
pk = k.PublicKey()
default:
return nil, fmt.Errorf(`expected *mldsa.PublicKey or *mldsa.PrivateKey, got %T`, key)
}
if err := a.requireMLDSAParams(pk.Parameters()); err != nil {
return nil, err
}
return pk, nil
}
// mldsaOptions narrows a crypto.SignerOpts to the concrete type crypto/mldsa
// accepts. A non-nil value of any other type is an error. Dropping it would let
// a caller believe their Context was in force while the operation actually ran
// with an empty context, which is a signature substitution vector for schemes
// that rely on domain separation.
func mldsaOptions(opts crypto.SignerOpts) (*mldsa.Options, error) {
if opts == nil {
return nil, nil
}
mldsaOpts, ok := opts.(*mldsa.Options)
if !ok {
return nil, fmt.Errorf(`expected *mldsa.Options, got %T`, opts)
}
return mldsaOpts, nil
}
func (a *mldsaAlgorithm) Sign(key any, payload []byte, _ io.Reader) ([]byte, error) {
sk, err := a.privateKey(key)
if err != nil {
return nil, fmt.Errorf(`dsig.Sign: %w`, err)
}
return SignMLDSA(sk, payload, nil)
}
// SignWithOpts implements [SignerWithOpts], forwarding an *mldsa.Options
// Context to crypto/mldsa.
func (a *mldsaAlgorithm) SignWithOpts(key any, payload []byte, opts crypto.SignerOpts, _ io.Reader) ([]byte, error) {
sk, err := a.privateKey(key)
if err != nil {
return nil, fmt.Errorf(`dsig.SignWithOpts: %w`, err)
}
// Validated but deliberately not narrowed. SignMLDSA takes a
// crypto.SignerOpts, so converting to a typed nil here would hand
// crypto/mldsa a non-nil interface holding a nil pointer.
if _, err := mldsaOptions(opts); err != nil {
return nil, fmt.Errorf(`dsig.SignWithOpts: %w`, err)
}
return SignMLDSA(sk, payload, opts)
}
func (a *mldsaAlgorithm) Verify(key any, payload, signature []byte) error {
pk, err := a.publicKey(key)
if err != nil {
return fmt.Errorf(`dsig.Verify: %w`, err)
}
return VerifyMLDSA(pk, payload, signature, nil)
}
// VerifyWithOpts implements [VerifierWithOpts]. See [SignerWithOpts] for the
// rationale on rejecting a foreign opts type.
func (a *mldsaAlgorithm) VerifyWithOpts(key any, payload, signature []byte, opts crypto.SignerOpts) error {
pk, err := a.publicKey(key)
if err != nil {
return fmt.Errorf(`dsig.VerifyWithOpts: %w`, err)
}
mldsaOpts, err := mldsaOptions(opts)
if err != nil {
return fmt.Errorf(`dsig.VerifyWithOpts: %w`, err)
}
return VerifyMLDSA(pk, payload, signature, mldsaOpts)
}
+45 -6
View File
@@ -14,10 +14,34 @@ import (
// 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.
//
// Deprecated in spirit: in the next major release of dsig (v2), the
// signature of Sign will change to match [SignWithOpts], i.e. it will
// accept an additional [crypto.SignerOpts] parameter immediately before
// rr. Callers that need to pass per-call options today should use
// [SignWithOpts]; callers that do not can keep using Sign and migrate
// when v2 ships by threading a nil opts argument through at the call
// site.
func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
return SignWithOpts(key, alg, payload, nil, rr)
}
// SignWithOpts is like [Sign] but threads an optional [crypto.SignerOpts]
// through to the underlying signer. For built-in families (HMAC, RSA,
// ECDSA, EdDSA) the opts argument is ignored — those algorithms have no
// per-call options the dsig layer understands. For Custom-family
// algorithms whose Meta implements [SignerWithOpts], the opts are
// forwarded; otherwise the plain [Signer.Sign] method is called and
// opts are dropped.
//
// This function exists as a transitional API. In the next major release
// of dsig (v2) it will be removed and its signature will become the
// canonical shape of [Sign]. Code that uses SignWithOpts today will need
// a mechanical rename to Sign (and nothing else) when v2 ships.
func SignWithOpts(key any, alg string, payload []byte, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
info, ok := GetAlgorithmInfo(alg)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: unsupported signature algorithm %q`, alg)
return nil, fmt.Errorf(`dsig.SignWithOpts: unsupported signature algorithm %q`, alg)
}
switch info.Family {
@@ -29,10 +53,10 @@ func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
return dispatchECDSASign(key, info, payload, rr)
case EdDSAFamily:
return dispatchEdDSASign(key, info, payload, rr)
case Custom:
return dispatchCustomSign(key, info, payload, rr)
case Custom, MLDSAFamily:
return dispatchMetaSign(key, info, payload, opts, rr)
default:
return nil, fmt.Errorf(`dsig.Sign: unsupported signature family %q`, info.Family)
return nil, fmt.Errorf(`dsig.SignWithOpts: unsupported signature family %q`, info.Family)
}
}
@@ -100,7 +124,10 @@ func dispatchECDSASign(key any, info AlgorithmInfo, payload []byte, rr io.Reader
return SignECDSA(privkey, payload, meta.Hash, rr)
}
func dispatchCustomSign(key any, info AlgorithmInfo, payload []byte, rr io.Reader) ([]byte, error) {
func dispatchMetaSign(key any, info AlgorithmInfo, payload []byte, opts crypto.SignerOpts, rr io.Reader) ([]byte, error) {
if signer, ok := info.Meta.(SignerWithOpts); ok {
return signer.SignWithOpts(key, payload, opts, rr)
}
signer, ok := info.Meta.(Signer)
if !ok {
return nil, fmt.Errorf(`dsig.Sign: algorithm has no signer registered`)
@@ -121,6 +148,14 @@ func dispatchCustomSign(key any, info AlgorithmInfo, payload []byte, rr io.Reade
//
// rr is an io.Reader that provides randomness for signing. If rr is nil,
// it defaults to rand.Reader.
//
// Deprecated in spirit: in the next major release of dsig (v2), the
// signature of SignDigest will gain a [crypto.SignerOpts] parameter to
// align with [Sign]. No SignDigestWithOpts shim exists in v1 because
// Custom-family algorithms (the only ones that would benefit from
// per-call opts) are rejected outright today; once a DigestSigner
// interface for the Custom family is added, the opts parameter will
// appear at the same time.
func SignDigest(key any, alg string, digest []byte, rr io.Reader) ([]byte, error) {
info, ok := GetAlgorithmInfo(alg)
if !ok {
@@ -140,6 +175,11 @@ func SignDigest(key any, alg string, digest []byte, rr io.Reader) ([]byte, error
return nil, fmt.Errorf(`dsig.SignDigest: EdDSA does not support digest-based signing`)
case Custom:
return nil, fmt.Errorf(`dsig.SignDigest: custom algorithms do not support digest-based signing`)
case MLDSAFamily:
// ML-DSA's pre-hashed mode takes a mu representative. That is a
// different thing from a plain digest; pass mu to Sign with
// crypto.MLDSAMu.
return nil, fmt.Errorf(`dsig.SignDigest: ML-DSA does not support digest-based signing`)
default:
return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature family %q`, info.Family)
}
@@ -207,4 +247,3 @@ func dispatchECDSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.R
}
return PackECDSASignature(r, s, privkey.Curve.Params().BitSize)
}
+42 -5
View File
@@ -9,10 +9,33 @@ import (
)
// Verify verifies a digital signature using the specified key and algorithm.
//
// Deprecated in spirit: in the next major release of dsig (v2), the
// signature of Verify will change to match [VerifyWithOpts], i.e. it
// will accept an additional [crypto.SignerOpts] parameter at the end.
// Callers that need to pass per-call options today should use
// [VerifyWithOpts]; callers that do not can keep using Verify and
// migrate when v2 ships by threading a nil opts argument through at
// the call site.
func Verify(key any, alg string, payload, signature []byte) error {
return VerifyWithOpts(key, alg, payload, signature, nil)
}
// VerifyWithOpts is like [Verify] but threads an optional
// [crypto.SignerOpts] through to the underlying verifier. For built-in
// families (HMAC, RSA, ECDSA, EdDSA) the opts argument is ignored. For
// Custom-family algorithms whose Meta implements [VerifierWithOpts],
// the opts are forwarded; otherwise the plain [Verifier.Verify] method
// is called and opts are dropped.
//
// This function exists as a transitional API. In the next major release
// of dsig (v2) it will be removed and its signature will become the
// canonical shape of [Verify]. Code that uses VerifyWithOpts today will
// need a mechanical rename to Verify (and nothing else) when v2 ships.
func VerifyWithOpts(key any, alg string, payload, signature []byte, opts crypto.SignerOpts) error {
info, ok := GetAlgorithmInfo(alg)
if !ok {
return fmt.Errorf(`dsig.Verify: unsupported signature algorithm %q`, alg)
return fmt.Errorf(`dsig.VerifyWithOpts: unsupported signature algorithm %q`, alg)
}
switch info.Family {
@@ -24,10 +47,10 @@ func Verify(key any, alg string, payload, signature []byte) error {
return dispatchECDSAVerify(key, info, payload, signature)
case EdDSAFamily:
return dispatchEdDSAVerify(key, info, payload, signature)
case Custom:
return dispatchCustomVerify(key, info, payload, signature)
case Custom, MLDSAFamily:
return dispatchMetaVerify(key, info, payload, signature, opts)
default:
return fmt.Errorf(`dsig.Verify: unsupported signature family %q`, info.Family)
return fmt.Errorf(`dsig.VerifyWithOpts: unsupported signature family %q`, info.Family)
}
}
@@ -41,6 +64,14 @@ func Verify(key any, alg string, payload, signature []byte) error {
// parameter is not used because it is already incorporated into the MAC.
//
// EdDSA and Custom families are not supported and return an error.
//
// Deprecated in spirit: in the next major release of dsig (v2), the
// signature of VerifyDigest will gain a [crypto.SignerOpts] parameter
// to align with [Verify]. No VerifyDigestWithOpts shim exists in v1
// because Custom-family algorithms (the only ones that would benefit
// from per-call opts) are rejected outright today; once a
// DigestVerifier interface for the Custom family is added, the opts
// parameter will appear at the same time.
func VerifyDigest(key any, alg string, digest, signature []byte) error {
info, ok := GetAlgorithmInfo(alg)
if !ok {
@@ -62,6 +93,9 @@ func VerifyDigest(key any, alg string, digest, signature []byte) error {
// TODO: a DigestVerifier interface (optional, checked here) would let
// custom algorithms opt in to digest-based verification.
return fmt.Errorf(`dsig.VerifyDigest: custom algorithms do not support digest-based verification`)
case MLDSAFamily:
// mu is derived from the message, so there is no digest to supply here.
return fmt.Errorf(`dsig.VerifyDigest: ML-DSA does not support digest-based verification`)
default:
return fmt.Errorf(`dsig.VerifyDigest: unsupported signature family %q`, info.Family)
}
@@ -196,7 +230,10 @@ func dispatchEdDSAVerify(key any, _ AlgorithmInfo, payload, signature []byte) er
return VerifyEdDSA(pubkey, payload, signature)
}
func dispatchCustomVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
func dispatchMetaVerify(key any, info AlgorithmInfo, payload, signature []byte, opts crypto.SignerOpts) error {
if verifier, ok := info.Meta.(VerifierWithOpts); ok {
return verifier.VerifyWithOpts(key, payload, signature, opts)
}
verifier, ok := info.Meta.(Verifier)
if !ok {
return fmt.Errorf(`dsig.Verify: algorithm has no verifier registered`)
+7
View File
@@ -1,6 +1,13 @@
Changes
=======
v3.0.6 07 Jun 2026
* Back off on HTTP fetch failure (connection refused, DNS failure,
timeout) by scheduling the next refresh at now+MinInterval, instead
of re-dispatching the resource in a tight ~1s loop (#119, #130)
* Document anchoring of RegexpWhitelist patterns
(e.g. `^https://example\.com/`) and add a runnable example (#125)
v3.0.5 30 Mar 2026
* Fix periodic check deadlock when number of ready resources exceeds
outgoing channel buffer, which caused circular wait between controller
+52
View File
@@ -65,6 +65,58 @@ If the values obtained from the headers fall within that range, the value from t
used. If the value is larger than the maximum, the maximum is used. If the value is lower
than the minimum, the minimum is used.
# Whitelisting URLs
By default the client allows all URLs. If you store resources whose URLs come from
untrusted sources, you should restrict what can be fetched by passing a whitelist
via `httprc.WithWhitelist`. Several implementations are provided: `BlockAllWhitelist`,
`InsecureWhitelist` (allow all), `MapWhitelist` (exact string match), and
`RegexpWhitelist`.
## A note on `RegexpWhitelist` patterns
`RegexpWhitelist` matches each URL with `(*regexp.Regexp).MatchString`, which returns
true when the pattern matches **any substring** of the URL. Patterns are **not**
anchored for you, so a naive pattern can allow far more than you intend.
Consider the difference between these two patterns:
```go
// BAD: unanchored, dots unescaped
regexp.MustCompile(`http://example.com`)
// GOOD: anchored at the start, dots escaped, host terminated with `/`
regexp.MustCompile(`^https://example\.com/`)
```
The unanchored `http://example.com` pattern will happily allow URLs such as:
- `http://example.com.attacker.com/evil` — the real host is `attacker.com`; the
pattern only required `example.com` to appear *somewhere*, and without a trailing
`/` it does not stop at the end of the host.
- `http://attacker.com/?redirect=http://example.com` — the pattern appears inside
the query string, so the match succeeds even though the host is `attacker.com`.
- `httpsX//exampleYcom` — `.` is the regular-expression "any character"
metacharacter, so the dots match more than literal dots.
To pin a pattern to a specific origin:
1. **Anchor the start** with `^` so the match must begin at the start of the URL.
2. **Escape the dots** (`\.`) so they only match a literal `.`.
3. **Terminate the host** with `/` so `example.com` cannot be extended into
`example.com.attacker.com`.
A couple of edge cases to keep in mind:
- Requiring the trailing `/` means the bare origin `https://example.com` (no path)
will not match. Add a second pattern such as `^https://example\.com$` if you need
to allow it.
- If your URLs may include a port, allow for it explicitly, e.g.
`^https://example\.com(:\d+)?/`.
See `ExampleRegexpWhitelist` in `whitelist_example_test.go` for a runnable
demonstration of the difference between anchored and unanchored patterns.
# SYNOPSIS
<!-- INCLUDE(client_example_test.go) -->
+4
View File
@@ -232,6 +232,10 @@ func (r *ResourceBase[T]) Sync(ctx context.Context) error {
traceSink.Put(ctx, fmt.Sprintf("httprc.Resource.Sync: fetching %q", r.u))
res, err := httpcl.Do(req)
if err != nil {
// Schedule retry after MinInterval so that connection failures
// don't cause a tight retry loop (the resource's Next stays at
// epoch if we don't update it here).
r.SetNext(time.Now().Add(r.MinInterval()))
return fmt.Errorf(`httprc.Resource.Sync: failed to execute HTTP request: %w`, err)
}
defer res.Body.Close()
+23
View File
@@ -49,6 +49,29 @@ func (InsecureWhitelist) IsAllowed(_ string) bool { return true }
// RegexpWhitelist is a jwk.Whitelist object comprised of a list of *regexp.Regexp
// objects. All entries in the list are tried until one matches. If none of the
// *regexp.Regexp objects match, then the URL is deemed unallowed.
//
// Matching is performed using (*regexp.Regexp).MatchString, which succeeds when
// the pattern matches ANY substring of the URL — it is NOT anchored automatically.
// This has important security implications: a pattern like `http://example.com`
// will match URLs you almost certainly did not intend to allow, such as
// `http://example.com.attacker.com/` (the host is actually attacker.com) or
// `http://attacker.com/?u=http://example.com` (the pattern appears in the query).
//
// To restrict to a specific origin, anchor the pattern at the start with `^`,
// escape the dots in the host (`.` is the "any character" metacharacter in a
// regular expression), and terminate the host with a `/` so that it cannot be
// extended into a subdomain:
//
// // GOOD: only matches the example.com origin and its paths
// regexp.MustCompile(`^https://example\.com/`)
//
// // BAD: also matches example.com.attacker.com, attacker.com/?x=http://example.com, httpsX//exampleYcom, ...
// regexp.MustCompile(`http://example.com`)
//
// Note that requiring a trailing `/` means the bare origin URL `https://example.com`
// (no path) will not match; register an additional pattern such as
// `^https://example\.com$` if you need to allow it. Likewise, account for an
// optional port (e.g. `^https://example\.com(:\d+)?/`) if your URLs may include one.
type RegexpWhitelist struct {
mu sync.RWMutex
patterns []*regexp.Regexp
+7 -1
View File
@@ -1,9 +1,15 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
load("@gazelle//:def.bzl", "gazelle")
load("@rules_go//go:def.bzl", "go_library", "go_test")
# gazelle:prefix github.com/lestrrat-go/jwx/v3
# gazelle:go_naming_convention import_alias
# Scratch directories that are not part of the module. Without these,
# gazelle walks bazel's own output tree under .gauntlet and rewrites every
# BUILD file to point at copies of the repo it finds in there.
# gazelle:exclude .gauntlet
# gazelle:exclude .tmp
gazelle(name = "gazelle")
go_library(
+56
View File
@@ -4,6 +4,62 @@ 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.3.0 8 Sep 2026
* [jwt][jws][jwe][jwk] Custom claim, header, and JWK field names are now
JSON-escaped on output. Previously a name was written between the quotes
as is, so a name containing `"` could close its own member and add
members the application never set. For example, calling `Set` with the
name `x":0,"admin` produced a signed token containing `"admin":true`.
Every name now yields exactly one member, and names that need no
escaping serialize exactly as before.
If your application accepts custom names from callers, an exact-match
allowlist was never affected. A blocklist of reserved names, or an
allowlist by namespace prefix, could be bypassed by this defect. Both are
reasonable designs; the bug was in the serializer. Prefer an exact-match
allowlist, and if you accept a prefix, require the rest of the name to be
a plain identifier.
Fixed in v4.5.0 and v3.3.0. v2, v1, and v0 contain the same code and are
unmaintained; see SECURITY.md. (GHSA-4cf7-xm37-g63h)
* [jws] Added `jws.WithStrictECDSA(bool)`, a `jws.Sign` option that rejects
anything RFC 7518 forbids for an ECDSA signature. Today that is Section
3.4's binding of ES256 to P-256, ES384 to P-384, and ES512 to P-521, so
signing a P-521 key under `jwa.ES256()` fails instead of producing a JWS
that other JOSE implementations reject.
The default is unchanged: without the option, a mismatched curve and
algorithm still sign exactly as before. `jws.Verify` is unaffected either
way. `jwt.Sign` callers can reach the option through
`jwt.WithSignOption(jws.WithStrictECDSA(true))`. (#2323)
v3.2.0 27 Jul 2026
* [jwe] Correct the JSON `"aad"` member so it contains only
BASE64URL of the external Additional Authenticated Data, rather than the
combined value used as the content-encryption AAD. Add
`jwe.WithAuthenticateData` for encrypting JSON JWEs with external AAD;
the value is included in the shared AEAD input for all recipients, and
compact serialization rejects non-empty external AAD. (#2276, #2278)
* [jwk] Added opt-in retention of unparseable JWK Set entries. Passing
`jwk.WithStrictKeySetParsing(false)` to `jwk.Parse` (or setting it
globally via `jwk.Configure`) keeps an entry whose key type is not
understood — for example a post-quantum key published alongside
classical keys — as a `jwk.UnsupportedKey` placeholder instead of
failing the whole set (RFC 7517 §5). The placeholder preserves the
entry's original JSON (marshaling round-trips losslessly) and the
parse error via `Reason()`; use `jwk.IsUnsupportedKey` to detect one.
The default is unchanged: v3 still fails the whole set on the first
unparseable entry, so existing callers see no difference. The same
option exists in v4 with the opposite default (v4 retains by
default, v3 stays strict by default); call sites that pass the
option explicitly keep the same meaning across the v3→v4 migration.
Placeholders
are rejected by `jws`/`jwe` key selection, `jwk.Export`,
`jwk.AssignKeyID`, and `jwk.PublicSetOf` (which accepts a new
`jwk.WithOmitUnsupportedKeys(true)` to drop them). (#2263)
v3.1.1 7 May 2026
* [jws] Coordinated RFC 7797 `b64=false` handling pass: `jws.Verify`
rejects payloads with `b64=false` unless `b64` is also listed in
+12 -4
View File
@@ -2,13 +2,21 @@
## Supported Versions
Most recent two major versions will receive security updates
Security fixes are published for the versions marked below. The
[State of support](https://github.com/lestrrat-go/jwx/discussions/1079)
discussion is the canonical, up-to-date statement; this table summarizes it.
| Version | Supported |
| -------- | ------------------ |
| v3.x.x | :white_check_mark: |
| v2.x.x | :white_check_mark: |
| < v2.0.0 | :x: |
| v4.x.x | :white_check_mark: Current release |
| v3.x.x | :white_check_mark: Previous release; receives regular fixes |
| v2.x.x | :x: Unmaintained. Do not use |
| v1.x.x | :x: Unmaintained. Do not use |
| < v1.0.0 | :x: Unmaintained. Do not use |
Unmaintained versions receive no fixes of any kind, including for issues
already fixed in a supported version. Each advisory names the versions that
carry the fix; a version not named there stays affected.
## Reporting a Vulnerability
+14 -2
View File
@@ -1,4 +1,4 @@
load("@rules_go//go:def.bzl", "go_library")
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "json",
@@ -9,7 +9,10 @@ go_library(
],
importpath = "github.com/lestrrat-go/jwx/v3/internal/json",
visibility = ["//:__subpackages__"],
deps = ["//internal/base64"],
deps = [
"//internal/base64",
"//internal/tokens",
],
)
alias(
@@ -17,3 +20,12 @@ alias(
actual = ":json",
visibility = ["//:__subpackages__"],
)
go_test(
name = "json_test",
srcs = ["json_test.go"],
deps = [
":json",
"@com_github_stretchr_testify//require",
],
)
+29
View File
@@ -7,6 +7,7 @@ import (
"sync/atomic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
var useNumber atomic.Uint32
@@ -164,3 +165,31 @@ func (dc *decodeCtx) Registry() *Registry {
func (dc *decodeCtx) StrictStrings() bool {
return dc.strictStrings
}
// WriteQuotedKey writes key as a quoted JSON object member name followed by
// the separating colon and a space.
//
// Member names come from public methods such as Set and Builder.Claim, so
// they may contain any byte, including `"`. A name copied raw between the
// quotes could end its own member and start further ones, so the serialized
// object would no longer match the one the caller built
// (GHSA-4cf7-xm37-g63h). A name that needs no escaping is written directly,
// which keeps the common path free of allocations. Every other name goes
// through the JSON string encoder.
func WriteQuotedKey(buf *bytes.Buffer, key string) error {
if tokens.IsJSONSafeASCII(key) {
buf.WriteByte(tokens.DoubleQuote)
buf.WriteString(key)
buf.WriteString(`": `)
return nil
}
encoded, err := Marshal(key)
if err != nil {
return fmt.Errorf(`failed to encode object member name: %w`, err)
}
buf.Write(encoded)
buf.WriteByte(tokens.Colon)
buf.WriteByte(' ')
return nil
}
-1
View File
@@ -8,7 +8,6 @@ go_library(
deps = [
"//jwk",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@org_golang_x_crypto//ed25519",
],
)
+2 -4
View File
@@ -21,11 +21,9 @@ alias(
go_test(
name = "pool_test",
srcs = [
"byte_slice_test.go",
],
srcs = ["byte_slice_test.go"],
deps = [
":pool",
"@com_github_stretchr_testify//require",
],
)
)
+14
View File
@@ -11,6 +11,20 @@ const (
Period = '.'
)
// IsJSONSafeASCII reports whether s can be concatenated into a
// hand-built JSON string literal without escaping. Any byte that
// would require a JSON escape (control bytes, `"`, `\`) or any
// non-ASCII byte disqualifies the value.
func IsJSONSafeASCII(s string) bool {
for i := range len(s) {
c := s[i]
if c < 0x20 || c >= 0x7f || c == '"' || c == '\\' {
return false
}
}
return true
}
// Cryptographic key sizes
const (
KeySize16 = 16
+4 -4
View File
@@ -24,20 +24,20 @@ go_library(
go_test(
name = "jwa_test",
srcs = [
"builtin_registry_test.go",
"compression_gen_test.go",
"content_encryption_gen_test.go",
"cross_kind_test.go",
"elliptic_gen_test.go",
"jwa_test.go",
"key_encryption_gen_test.go",
"key_type_gen_test.go",
"options_gen_test.go",
"registry_snapshot_test.go",
"signature_gen_test.go",
],
embed = [":jwa"],
deps = [
"@com_github_stretchr_testify//require",
"@com_github_lestrrat_go_option_v2//:option",
],
deps = ["@com_github_stretchr_testify//require"],
)
alias(
+11 -4
View File
@@ -23,40 +23,47 @@ go_library(
deps = [
"//cert",
"//internal/base64",
"//transform",
"//internal/json",
"//internal/tokens",
"//internal/keyconv",
"//internal/pool",
"//internal/tokens",
"//jwa",
"//jwe/internal/aescbc",
"//jwe/internal/cipher",
"//jwe/internal/content_crypt",
"//jwe/internal/keygen",
"//jwe/jwebb",
"//jwk",
"//transform",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@com_github_lestrrat_go_option_v2//:option",
"@org_golang_x_crypto//pbkdf2",
],
)
go_test(
name = "jwe_test",
srcs = [
"bench_encrypt_test.go",
"encrypt_aad_test.go",
"filter_test.go",
"fuzz_test.go",
"gh402_test.go",
"headers_test.go",
"jwe_aad_internal_test.go",
"jwe_crit_test.go",
"jwe_test.go",
"message_aad_test.go",
"message_test.go",
"options_gen_test.go",
"recipient_headers_test.go",
"speed_test.go",
"unsupported_key_test.go",
],
embed = [":jwe"],
deps = [
"//cert",
"//internal/json",
"//internal/jwxtest",
"//internal/tokens",
"//jwa",
"//jwk",
"@com_github_stretchr_testify//require",
+3 -3
View File
@@ -962,9 +962,9 @@ func (h *stdHeaders) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+1 -1
View File
@@ -12,7 +12,7 @@ go_test(
name = "aescbc_test",
srcs = ["aescbc_test.go"],
embed = [":aescbc"],
deps = ["@com_github_stretchr_testify//require"]
deps = ["@com_github_stretchr_testify//require"],
)
alias(
+1 -3
View File
@@ -9,10 +9,9 @@ go_library(
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/cipher",
visibility = ["//:__subpackages__"],
deps = [
"//jwa",
"//internal/tokens",
"//jwe/internal/aescbc",
"//jwe/internal/keygen",
"//internal/tokens",
],
)
@@ -21,7 +20,6 @@ go_test(
srcs = ["cipher_test.go"],
deps = [
":cipher",
"//jwa",
"//internal/tokens",
"@com_github_stretchr_testify//require",
],
+4 -1
View File
@@ -9,7 +9,10 @@ go_library(
go_test(
name = "concatkdf_test",
srcs = ["concatkdf_test.go"],
srcs = [
"bench_test.go",
"concatkdf_test.go",
],
embed = [":concatkdf"],
deps = [
"//jwa",
+11 -4
View File
@@ -1,4 +1,4 @@
load("@rules_go//go:def.bzl", "go_library")
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "keygen",
@@ -9,10 +9,8 @@ go_library(
importpath = "github.com/lestrrat-go/jwx/v3/jwe/internal/keygen",
visibility = ["//:__subpackages__"],
deps = [
"//internal/ecutil",
"//jwa",
"//jwe/internal/concatkdf",
"//internal/tokens",
"//jwe/internal/concatkdf",
"//jwk",
],
)
@@ -22,3 +20,12 @@ alias(
actual = ":keygen",
visibility = ["//jwe:__subpackages__"],
)
go_test(
name = "keygen_test",
srcs = ["keygen_test.go"],
deps = [
":keygen",
"@com_github_stretchr_testify//require",
],
)
+21 -3
View File
@@ -877,6 +877,7 @@ type encryptContext struct {
compression jwa.CompressionAlgorithm
format int
pbes2Count int
authenticatedData []byte
builders []*recipientBuilder
protected Headers
legacyHeaderMerging bool
@@ -897,6 +898,7 @@ func freeEncryptContext(ec *encryptContext) *encryptContext {
ec.compression = jwa.NoCompress()
ec.format = fmtCompact
ec.pbes2Count = 0
ec.authenticatedData = nil
ec.builders = ec.builders[:0]
ec.protected = nil
return ec
@@ -949,6 +951,12 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
return err
}
ec.compression = comp
case identAuthenticateData{}:
var aad []byte
if err := option.Value(&aad); err != nil {
return err
}
ec.authenticatedData = aad
case identMergeProtectedHeaders{}:
var mp bool
if err := option.Value(&mp); err != nil {
@@ -994,6 +1002,10 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
}
}
if len(ec.authenticatedData) > 0 && ec.format == fmtCompact {
return fmt.Errorf(`cannot use compact serialization with external authenticated data (use WithJSON())`)
}
if useRawCEK {
if len(ec.builders) != 1 {
return fmt.Errorf(`multiple recipients for ECDH-ES/DIRECT mode are not supported`)
@@ -1193,12 +1205,13 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
}
}
aad, err := protected.Encode()
protectedAAD, err := protected.Encode()
if err != nil {
return nil, fmt.Errorf(`failed to base64 encode protected headers: %w`, err)
}
iv, ciphertext, tag, err := contentcrypt.Encrypt(cek, payload, aad)
contentAAD := concatAAD(protectedAAD, base64.Encode(ec.authenticatedData))
iv, ciphertext, tag, err := contentcrypt.Encrypt(cek, payload, contentAAD)
if err != nil {
return nil, fmt.Errorf(`failed to encrypt payload: %w`, err)
}
@@ -1207,7 +1220,7 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
// pre-encoded headers and raw fields, avoiding the full Message
// construction and redundant header re-encoding that Compact() does.
if ec.format == fmtCompact {
return compactSerialize(aad, recipients[0].EncryptedKey(), iv, ciphertext, tag), nil
return compactSerialize(protectedAAD, recipients[0].EncryptedKey(), iv, ciphertext, tag), nil
}
msg := msgPool.Get()
@@ -1228,6 +1241,11 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
if err := msg.Set(TagKey, tag); err != nil {
return nil, fmt.Errorf(`failed to set %s: %w`, TagKey, err)
}
if len(ec.authenticatedData) > 0 {
if err := msg.Set(AuthenticatedDataKey, ec.authenticatedData); err != nil {
return nil, fmt.Errorf(`failed to set %s: %w`, AuthenticatedDataKey, err)
}
}
switch ec.format {
case fmtJSON:
+11 -4
View File
@@ -4,6 +4,7 @@ go_library(
name = "jwebb",
srcs = [
"content_cipher.go",
"jwebb.go",
"key_decrypt_asymmetric.go",
"key_decrypt_symmetric.go",
"key_encrypt_asymmetric.go",
@@ -16,11 +17,11 @@ go_library(
deps = [
"//internal/keyconv",
"//internal/pool",
"//internal/tokens",
"//jwe/internal/cipher",
"//jwe/internal/concatkdf",
"//jwe/internal/content_crypt",
"//jwe/internal/keygen",
"//internal/tokens",
"@org_golang_x_crypto//pbkdf2",
],
)
@@ -32,12 +33,18 @@ go_test(
"jwebb_test.go",
"keywrap_test.go",
],
embed = [":jwebb"],
deps = [
":jwebb",
"//internal/jwxtest",
"//internal/tokens",
"//jwa",
"//jwe/internal/keygen",
"//internal/tokens",
"@com_github_stretchr_testify//require",
],
)
)
alias(
name = "go_default_library",
actual = ":jwebb",
visibility = ["//visibility:public"],
)
+5
View File
@@ -108,6 +108,11 @@ type keySetProvider struct {
}
func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, r Recipient, msg *Message) error {
if uk, ok := key.(jwk.UnsupportedKey); ok {
kid, _ := uk.KeyID()
return fmt.Errorf(`key %q has unsupported key type %q and cannot be used for decryption; an extension module may be required to parse it: %w`, kid, uk.KeyType().String(), uk.Reason())
}
if usage, ok := key.KeyUsage(); ok {
if usage != "" && usage != jwk.ForEncryption.String() {
kid, _ := key.KeyID()
+4 -13
View File
@@ -227,32 +227,23 @@ func (m *Message) MarshalJSON() ([]byte, error) {
})
}
var encodedProtectedHeaders []byte
if h := m.ProtectedHeaders(); h != nil {
v, err := h.Encode()
if err != nil {
return nil, fmt.Errorf(`failed to encode protected headers: %w`, err)
}
encodedProtectedHeaders = v
if len(encodedProtectedHeaders) <= 2 { // '{}'
encodedProtectedHeaders = nil
} else {
if len(v) > 2 { // '{}'
fields = append(fields, jsonKV{
Key: ProtectedHeadersKey,
Value: fmt.Sprintf("%q", encodedProtectedHeaders),
Value: fmt.Sprintf("%q", v),
})
}
}
if aad := m.AuthenticatedData(); len(aad) > 0 {
aad = base64.Encode(aad)
if encodedProtectedHeaders != nil {
aad = concatAAD(encodedProtectedHeaders, aad)
}
buf.Reset()
if err := enc.Encode(aad); err != nil {
if err := enc.Encode(base64.EncodeToString(aad)); err != nil {
return nil, fmt.Errorf(`failed to encode %s field: %w`, AuthenticatedDataKey, err)
}
fields = append(fields, jsonKV{
@@ -377,7 +368,7 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
if proxy.Headers != nil || len(proxy.EncryptedKey) > 0 {
recipient := NewRecipient()
// `"heders"` could be empty. If that's the case, just skip the
// `"headers"` could be empty. If that's the case, just skip the
// following unmarshaling step
if proxy.Headers != nil {
hdrs := NewHeaders()
+12
View File
@@ -1,6 +1,8 @@
package jwe
import (
"bytes"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/option/v2"
@@ -76,6 +78,16 @@ func WithProtectedHeaders(h Headers) EncryptOption {
return &encryptOption{option.New(identProtectedHeaders{}, cloned)}
}
// WithAuthenticateData specifies the external Additional Authenticated Data
// to use when encrypting a JSON JWE.
//
// The data is copied before it is stored in the option. External Additional
// Authenticated Data is not supported by compact serialization; pass
// WithJSON() to select JSON serialization.
func WithAuthenticateData(aad []byte) EncryptOption {
return &encryptOption{option.New(identAuthenticateData{}, bytes.Clone(aad))}
}
type withKey struct {
alg jwa.KeyAlgorithm
key any
+7
View File
@@ -60,6 +60,13 @@ options:
skip_option: true
- ident: ProtectedHeaders
skip_option: true
- ident: AuthenticateData
skip_option: true
interface: EncryptOption
argument_type: '[]byte'
comment: |
WithAuthenticateData specifies the external Additional Authenticated Data
to use when encrypting a JSON JWE.
- ident: PerRecipientHeaders
skip_option: true
- ident: KeyProvider
+5
View File
@@ -169,6 +169,7 @@ type withKeySetSuboption struct {
func (*withKeySetSuboption) withKeySetSuboption() {}
type identAuthenticateData struct{}
type identCBCBufferSize struct{}
type identCEK struct{}
type identCompress struct{}
@@ -193,6 +194,10 @@ type identProtectedHeaders struct{}
type identRequireKid struct{}
type identSerialization struct{}
func (identAuthenticateData) String() string {
return "WithAuthenticateData"
}
func (identCBCBufferSize) String() string {
return "WithCBCBufferSize"
}
+13 -4
View File
@@ -5,6 +5,7 @@ go_library(
srcs = [
"cache.go",
"convert.go",
"doc.go",
"ecdsa.go",
"ecdsa_gen.go",
"errors.go",
@@ -25,6 +26,7 @@ go_library(
"set.go",
"symmetric.go",
"symmetric_gen.go",
"unsupported.go",
"usage.go",
"whitelist.go",
"x509.go",
@@ -35,14 +37,14 @@ go_library(
"//cert",
"//internal/base64",
"//internal/ecutil",
"//transform",
"//internal/json",
"//internal/pool",
"//internal/tokens",
"//internal/tokens",
"//jwa",
"//jwk/ecdsa",
"//jwk/internal/registry",
"//jwk/jwkbb",
"//transform",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@com_github_lestrrat_go_httprc_v3//:httprc",
"@com_github_lestrrat_go_option_v2//:option",
@@ -52,13 +54,20 @@ go_library(
go_test(
name = "jwk_test",
srcs = [
"bench_set_test.go",
"ecdsa_test.go",
"filter_test.go",
"fuzz_test.go",
"headers_test.go",
"jwk_internal_test.go",
"jwk_test.go",
"jwk_zero_on_error_test.go",
"okp_length_test.go",
"options_gen_test.go",
"refresh_test.go",
"rsa_thumbprint_test.go",
"rsa_validate_test.go",
"set_test.go",
"unsupported_test.go",
"x5c_test.go",
],
data = glob(["testdata/**"]),
@@ -72,7 +81,7 @@ go_test(
"//internal/tokens",
"//jwa",
"//jwk/ecdsa",
"//jws",
"//jwk/jwkunsafe",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@com_github_lestrrat_go_httprc_v3//:httprc",
"@com_github_lestrrat_go_httprc_v3//tracesink",
+5
View File
@@ -409,6 +409,11 @@ func bytesToKey(src any) (Key, error) {
// especially when the object implements the `jwk.Key` interface via
// embedding.
func Export(key Key, dst any) error {
if uk, ok := key.(UnsupportedKey); ok {
kid, _ := uk.KeyID()
return fmt.Errorf(`jwk.Export: cannot export an unsupported key (kty=%q, kid=%q) that could not be parsed; an extension module may be required: %w`, uk.KeyType().String(), kid, uk.Reason())
}
// dst better be a pointer
rv := reflect.ValueOf(dst)
if rv.Kind() != reflect.Ptr {
+1 -1
View File
@@ -29,7 +29,7 @@
// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey))
// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey))
//
// See examples/jwk_parse_example_test.go and other files in the exmaples/ directory for more.
// See examples/jwk_parse_example_test.go and other files in the examples/ directory for more.
//
// # Advanced Usage: Registering a custom key type and conversion routines
//
+11 -1
View File
@@ -1,4 +1,4 @@
load("@rules_go//go:def.bzl", "go_library")
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "ecdsa",
@@ -13,3 +13,13 @@ alias(
actual = ":ecdsa",
visibility = ["//visibility:public"],
)
go_test(
name = "ecdsa_test",
srcs = ["ecdsa_test.go"],
embed = [":ecdsa"],
deps = [
"//jwa",
"@com_github_stretchr_testify//require",
],
)
+6 -6
View File
@@ -734,9 +734,9 @@ func (h *ecdsaPublicKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
@@ -1558,9 +1558,9 @@ func (h *ecdsaPrivateKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+7 -6
View File
@@ -121,12 +121,13 @@ type Set interface {
}
type set struct {
keys []Key
mu sync.RWMutex
dc DecodeCtx
privateParams map[string]any
maxKeys int // scratch cap consumed by UnmarshalJSON; 0 means use global default
rejectDuplicateKID bool // scratch flag consumed by UnmarshalJSON; false falls back to global
keys []Key
mu sync.RWMutex
dc DecodeCtx
privateParams map[string]any
maxKeys int // scratch cap consumed by UnmarshalJSON; 0 means use global default
rejectDuplicateKID *bool // scratch override consumed by UnmarshalJSON; nil falls back to global
strictKeySetParsing *bool // scratch override consumed by UnmarshalJSON; nil falls back to global
}
type PublicKeyer interface {
+51 -3
View File
@@ -44,8 +44,17 @@ var maxKeys atomic.Int64
// Tunable via WithRejectDuplicateKID / Configure(WithRejectDuplicateKID(...)).
var rejectDuplicateKID atomic.Bool
// strictKeySetParsing controls how Parse/UnmarshalJSON treat an entry in
// a JWKS "keys" array that cannot be parsed. Default is true (fail-fast):
// the first unparseable entry fails the whole set, preserving v3's
// historical behavior. When false, the entry is retained as an
// UnsupportedKey placeholder (unless WithIgnoreParseError drops it).
// Tunable via WithStrictKeySetParsing / Configure(WithStrictKeySetParsing(...)).
var strictKeySetParsing atomic.Bool
func init() {
maxKeys.Store(1000)
strictKeySetParsing.Store(true)
if err := RegisterProbeField(reflect.StructField{
Name: "Kty",
@@ -118,12 +127,17 @@ func Import(raw any) (Key, error) {
// to remove any fields, if necessary.
func PublicSetOf(v Set, options ...PublicSetOption) (Set, error) {
var allowSymmetric bool
var omitUnsupported bool
for _, option := range options {
switch option.Ident() {
case identAllowSymmetric{}:
if err := option.Value(&allowSymmetric); err != nil {
return nil, fmt.Errorf(`failed to retrieve AllowSymmetric option value: %w`, err)
}
case identOmitUnsupportedKeys{}:
if err := option.Value(&omitUnsupported); err != nil {
return nil, fmt.Errorf(`failed to retrieve OmitUnsupportedKeys option value: %w`, err)
}
}
}
@@ -135,6 +149,13 @@ func PublicSetOf(v Set, options ...PublicSetOption) (Set, error) {
if !ok {
return nil, fmt.Errorf(`key not found`)
}
if uk, ok := k.(UnsupportedKey); ok {
if omitUnsupported {
continue
}
kid, _ := uk.KeyID()
return nil, fmt.Errorf(`jwk.PublicSetOf: input set contains an unsupported key (kty=%q, kid=%q, index=%d) that could not be parsed; there is no way to prove it holds no private material, so it is not passed through. Pass jwk.WithOmitUnsupportedKeys(true) to drop such entries from the output: %w`, uk.KeyType().String(), kid, i, uk.Reason())
}
if k.KeyType() == jwa.OctetSeq() && !allowSymmetric {
kid, _ := k.KeyID()
return nil, fmt.Errorf(`jwk.PublicSetOf: input set contains a symmetric key (kid=%q, index=%d); symmetric keys have no public form and would leak secret material if published. Remove symmetric keys from the set before calling PublicSetOf, or pass jwk.WithAllowSymmetric(true) to opt into legacy pass-through behavior`, kid, i)
@@ -363,6 +384,7 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
var pemDecoder PEMDecoder
maxK := int(maxKeys.Load())
rejectDupKid := rejectDuplicateKID.Load()
strict := strictKeySetParsing.Load()
for _, option := range options {
switch option.Ident() {
case identPEM{}:
@@ -394,6 +416,10 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
if err := option.Value(&rejectDupKid); err != nil {
return nil, parseerr(`failed to retrieve RejectDuplicateKID option value: %w`, err)
}
case identStrictKeySetParsing{}:
if err := option.Value(&strict); err != nil {
return nil, parseerr(`failed to retrieve StrictKeySetParsing option value: %w`, err)
}
case identTypedField{}:
var pair typedFieldPair // temporary var needed for typed field
if err := option.Value(&pair); err != nil {
@@ -459,9 +485,20 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
setter.setMaxKeys(maxK)
defer setter.setMaxKeys(0)
}
if setter, ok := s.(interface{ setRejectDuplicateKID(bool) }); ok && rejectDupKid {
setter.setRejectDuplicateKID(true)
defer setter.setRejectDuplicateKID(false)
// Propagate the resolved reject-duplicate-KID flag. A pointer
// distinguishes "not set by Parse" (nil → Set.UnmarshalJSON uses the
// global default) from an explicit per-call true/false, so a per-call
// false overrides a global true.
if setter, ok := s.(interface{ setRejectDuplicateKID(*bool) }); ok {
setter.setRejectDuplicateKID(&rejectDupKid)
defer setter.setRejectDuplicateKID(nil)
}
// Propagate the resolved strict flag. A pointer distinguishes "not
// set by Parse" (nil → Set.UnmarshalJSON uses the global default of
// true) from an explicit per-call true/false.
if setter, ok := s.(interface{ setStrictKeySetParsing(*bool) }); ok {
setter.setStrictKeySetParsing(&strict)
defer setter.setStrictKeySetParsing(nil)
}
// Dispatch JWK-vs-JWKS up front. Set.UnmarshalJSON requires JWKS
@@ -541,6 +578,11 @@ func ParseString(s string, options ...ParseOption) (Set, error) {
// recomputation (for example, when upgrading to a stronger thumbprint hash
// via `jwk.WithThumbprintHash`).
func AssignKeyID(key Key, options ...AssignKeyIDOption) error {
if uk, ok := key.(UnsupportedKey); ok {
kid, _ := uk.KeyID()
return fmt.Errorf(`jwk.AssignKeyID: cannot assign a key ID to an unsupported key (kty=%q, kid=%q) that could not be parsed; its thumbprint cannot be computed: %w`, uk.KeyType().String(), kid, uk.Reason())
}
hash := crypto.SHA256
var force bool
for _, option := range options {
@@ -845,6 +887,12 @@ func Configure(options ...GlobalOption) {
continue
}
rejectDuplicateKID.Store(v)
case identStrictKeySetParsing{}:
var v bool
if err := option.Value(&v); err != nil {
continue
}
strictKeySetParsing.Store(v)
}
}
+1 -1
View File
@@ -27,4 +27,4 @@ alias(
name = "go_default_library",
actual = ":jwkbb",
visibility = ["//visibility:public"],
)
)
+1 -1
View File
@@ -40,7 +40,7 @@ func (k *okpPrivateKey) KeyKind() KeyKind { return okpKeyKind(k.Crv) }
// Because this is an elliptic curve based Diffie Hellman protocol, it is also referred to
// as ECDH.
//
// OKP keys are used to represent private/public pairs of thse elliptic curve
// OKP keys are used to represent private/public pairs of these elliptic curve
// keys. But note that the name just means Octet Key Pair.
func (k *okpPublicKey) Import(rawKeyIf any) error {
+6 -6
View File
@@ -684,9 +684,9 @@ func (h *okpPublicKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
@@ -1454,9 +1454,9 @@ func (h *okpPrivateKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+48
View File
@@ -280,6 +280,39 @@ options:
This does not affect `(*Set).AddKey` — programmatic additions
remain permissive (AddKey dedupes only by pointer identity).
- ident: StrictKeySetParsing
interface: GlobalParseOption
argument_type: bool
comment: |
WithStrictKeySetParsing controls what happens when an entry in a
JWK Set's "keys" array cannot be parsed.
In v3 the default is true (strict): the first unparseable entry
fails the entire set, exactly as older v3 releases did. Existing
callers therefore see no change in behavior.
Pass `WithStrictKeySetParsing(false)` to opt into retention. In
that mode an unparseable entry is neither dropped nor fatal — it
is kept in the set as a `jwk.UnsupportedKey` placeholder that
preserves the entry's original JSON and the error that prevented
parsing (RFC 7517 §5). This lets a set that mixes understood and
not-yet-understood keys (for example, post-quantum keys published
by an identity provider) remain usable for the keys you do
understand.
Note the cross-version difference: v4 defaults to false (retain),
while v3 defaults to true (fail-fast). The option means the same
thing in both — only the default differs — so call sites that pass
it explicitly are source-compatible across the v3→v4 migration.
This option is distinct from `WithIgnoreParseError`, which silently
*drops* unparseable entries instead of retaining placeholders.
`WithIgnoreParseError(true)` takes precedence regardless of the
strict setting: the entry is dropped rather than failing the set
or being retained.
Can be set globally via `jwk.Configure()` or per-call on
`jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
- ident: AllowSymmetric
interface: PublicSetOption
argument_type: bool
@@ -296,3 +329,18 @@ options:
Pass `WithAllowSymmetric(true)` only if you are certain the
resulting set will not be published. When true, symmetric keys
are passed through unchanged, matching the legacy behavior.
- ident: OmitUnsupportedKeys
interface: PublicSetOption
argument_type: bool
comment: |
WithOmitUnsupportedKeys controls how `jwk.PublicSetOf` treats
`jwk.UnsupportedKey` placeholders in the input set.
By default this option is false: a placeholder in the input is an
error, because there is no way to prove that an unparseable entry
contains no private material, and passing it through would risk
republishing a private key.
Pass `WithOmitUnsupportedKeys(true)` to drop placeholders from the
output set instead. Use this when you intend to publish the public
set and want unparseable entries silently excluded.
+58
View File
@@ -213,9 +213,11 @@ type identMaxFetchBodySize struct{}
type identMaxKeys struct{}
type identMinRSAModulusBits struct{}
type identMinRSAPublicExponent struct{}
type identOmitUnsupportedKeys struct{}
type identPEM struct{}
type identPEMDecoder struct{}
type identRejectDuplicateKID struct{}
type identStrictKeySetParsing struct{}
type identStrictKeyUsage struct{}
type identThumbprintHash struct{}
type identWaitReady struct{}
@@ -265,6 +267,10 @@ func (identMinRSAPublicExponent) String() string {
return "WithMinRSAPublicExponent"
}
func (identOmitUnsupportedKeys) String() string {
return "WithOmitUnsupportedKeys"
}
func (identPEM) String() string {
return "WithPEM"
}
@@ -277,6 +283,10 @@ func (identRejectDuplicateKID) String() string {
return "WithRejectDuplicateKID"
}
func (identStrictKeySetParsing) String() string {
return "WithStrictKeySetParsing"
}
func (identStrictKeyUsage) String() string {
return "WithStrictKeyUsage"
}
@@ -448,6 +458,21 @@ func WithMinRSAPublicExponent(v int) GlobalOption {
return &globalOption{option.New(identMinRSAPublicExponent{}, v)}
}
// WithOmitUnsupportedKeys controls how `jwk.PublicSetOf` treats
// `jwk.UnsupportedKey` placeholders in the input set.
//
// By default this option is false: a placeholder in the input is an
// error, because there is no way to prove that an unparseable entry
// contains no private material, and passing it through would risk
// republishing a private key.
//
// Pass `WithOmitUnsupportedKeys(true)` to drop placeholders from the
// output set instead. Use this when you intend to publish the public
// set and want unparseable entries silently excluded.
func WithOmitUnsupportedKeys(v bool) PublicSetOption {
return &publicSetOption{option.New(identOmitUnsupportedKeys{}, v)}
}
// WithPEM specifies that the input to `Parse()` is a PEM encoded key.
//
// This option is planned to be deprecated in the future. The plan is to
@@ -488,6 +513,39 @@ func WithRejectDuplicateKID(v bool) GlobalParseOption {
return &globalParseOption{option.New(identRejectDuplicateKID{}, v)}
}
// WithStrictKeySetParsing controls what happens when an entry in a
// JWK Set's "keys" array cannot be parsed.
//
// In v3 the default is true (strict): the first unparseable entry
// fails the entire set, exactly as older v3 releases did. Existing
// callers therefore see no change in behavior.
//
// Pass `WithStrictKeySetParsing(false)` to opt into retention. In
// that mode an unparseable entry is neither dropped nor fatal — it
// is kept in the set as a `jwk.UnsupportedKey` placeholder that
// preserves the entry's original JSON and the error that prevented
// parsing (RFC 7517 §5). This lets a set that mixes understood and
// not-yet-understood keys (for example, post-quantum keys published
// by an identity provider) remain usable for the keys you do
// understand.
//
// Note the cross-version difference: v4 defaults to false (retain),
// while v3 defaults to true (fail-fast). The option means the same
// thing in both — only the default differs — so call sites that pass
// it explicitly are source-compatible across the v3→v4 migration.
//
// This option is distinct from `WithIgnoreParseError`, which silently
// *drops* unparseable entries instead of retaining placeholders.
// `WithIgnoreParseError(true)` takes precedence regardless of the
// strict setting: the entry is dropped rather than failing the set
// or being retained.
//
// Can be set globally via `jwk.Configure()` or per-call on
// `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
func WithStrictKeySetParsing(v bool) GlobalParseOption {
return &globalParseOption{option.New(identStrictKeySetParsing{}, v)}
}
// WithStrictKeyUsage specifies if during JWK parsing, the "use" field
// should be confined to the values that have been registered via
// `jwk.RegisterKeyType()`. By default this option is true, and the
+6 -6
View File
@@ -692,9 +692,9 @@ func (h *rsaPublicKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
@@ -1707,9 +1707,9 @@ func (h *rsaPrivateKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+28 -5
View File
@@ -211,10 +211,14 @@ func (s *set) setMaxKeys(n int) {
s.maxKeys = n
}
func (s *set) setRejectDuplicateKID(v bool) {
func (s *set) setRejectDuplicateKID(v *bool) {
s.rejectDuplicateKID = v
}
func (s *set) setStrictKeySetParsing(v *bool) {
s.strictKeySetParsing = v
}
// UnmarshalJSON streams a JWKS document. The "keys" array is read
// element-by-element with the configured cap enforced BEFORE the
// (cap+1)-th element is decoded — an attacker-controlled input length
@@ -241,7 +245,14 @@ func (s *set) UnmarshalJSON(data []byte) error {
if maxK <= 0 {
maxK = int(maxKeys.Load())
}
rejectDupKid := s.rejectDuplicateKID || rejectDuplicateKID.Load()
rejectDupKid := rejectDuplicateKID.Load()
if s.rejectDuplicateKID != nil {
rejectDupKid = *s.rejectDuplicateKID
}
strict := strictKeySetParsing.Load()
if s.strictKeySetParsing != nil {
strict = *s.strictKeySetParsing
}
dec := json.NewDecoder(bytes.NewReader(data))
LOOP:
@@ -285,11 +296,23 @@ LOOP:
}
key, err := ParseKey(raw, options...)
if err != nil {
if !ignoreParseError {
// ignoreParseError is checked first so its
// long-standing "drop the entry" behavior is
// unchanged regardless of the strict flag. Then:
// strict (v3 default) fails the whole set; otherwise
// the entry is retained as an UnsupportedKey
// placeholder (RFC 7517 §5, opt-in via
// WithStrictKeySetParsing(false)).
if ignoreParseError {
i++
continue
}
if strict {
return fmt.Errorf(`failed to decode key #%d in "keys": %w`, i, err)
}
i++
continue
// dec.Decode may reuse its buffer, so
// newUnsupportedKey clones the raw bytes.
key = newUnsupportedKey(raw, err)
}
if seenKIDs != nil {
if kid, ok := key.KeyID(); ok && kid != "" {
+3 -3
View File
@@ -638,9 +638,9 @@ func (h *symmetricKey) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+318
View File
@@ -0,0 +1,318 @@
package jwk
import (
"bytes"
"crypto"
"errors"
"fmt"
"github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/cert"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/jwa"
)
// UnsupportedKey is a placeholder for a JWK Set entry that could not be
// parsed into a usable key. Per RFC 7517 §5, an entry inside a "keys"
// array whose key type is not understood, that is missing required
// members, or whose values are out of the supported range may be retained
// as an UnsupportedKey instead of failing the whole set.
//
// In v3 retention is opt-in: pass `jwk.WithStrictKeySetParsing(false)` to
// `jwk.Parse` (or set it globally via `jwk.Configure`). By default v3
// still fails the whole set on the first unparseable entry, so existing
// callers see no change. (In v4 retention is the default; the option
// carries the same meaning in both, only the default differs.)
//
// A placeholder preserves the entry's original JSON — marshaling it with
// json.Marshal (alone or as part of its set) reproduces the entry, so a
// set containing one round-trips losslessly — and it preserves the error
// that prevented parsing (via [UnsupportedKey.Reason]).
//
// An UnsupportedKey cannot be used for any cryptographic operation:
// [UnsupportedKey.Thumbprint], [UnsupportedKey.PublicKey] and
// [UnsupportedKey.Validate] all return an error wrapping Reason(), and
// the key is rejected by cryptographic consumers such as
// jws.Verify / jwe.Decrypt with a descriptive per-key error.
//
// Use [IsUnsupportedKey] to check whether a key is a placeholder. Use a
// type assertion when you also need the placeholder's details:
//
// if uk, ok := key.(jwk.UnsupportedKey); ok {
// // key type key.KeyType() is not supported by this build;
// // uk.Reason() explains why, an extension module may be required.
// }
type UnsupportedKey interface {
Key
// Reason returns the error that prevented the entry from parsing.
Reason() error
// isUnsupportedKey seals this interface: only the placeholder type
// produced by this package implements it. Without the seal, any
// third-party Key that happens to define a Reason() error method
// would satisfy UnsupportedKey and be rejected as a placeholder by
// jwk.Export, jwk.AssignKeyID, and jws/jwe key selection.
isUnsupportedKey()
}
// IsUnsupportedKey reports whether key is a placeholder retained for a
// JWK Set entry that could not be parsed. Only placeholders produced by
// this package satisfy the check; a user-defined Key type can never be
// mistaken for one. It is the sanctioned way to
// skip placeholders when iterating a set; type-assert to
// [UnsupportedKey] when you also need Reason().
func IsUnsupportedKey(key Key) bool {
_, ok := key.(UnsupportedKey)
return ok
}
// unsupportedKey is the concrete implementation of [UnsupportedKey].
//
// It is effectively immutable after construction: the mutators [Set] and
// [Remove] return errors without modifying any field, so no locking is
// required for concurrent reads. The best-effort common members are
// parsed once in [newUnsupportedKey].
type unsupportedKey struct {
raw []byte
reason error
rawKty string
ktyPresent bool
algorithm *jwa.KeyAlgorithm
keyID *string
}
var _ UnsupportedKey = &unsupportedKey{}
var _ Key = &unsupportedKey{}
// newUnsupportedKey builds a placeholder from the verbatim entry bytes
// and the error that prevented parsing. raw is cloned because the
// decoder buffer it came from may be reused.
func newUnsupportedKey(raw []byte, reason error) *unsupportedKey {
// reason is always non-nil in practice (a placeholder only exists
// because ParseKey failed), but guard anyway so a nil can never
// reach the %w verbs that wrap Reason().
if reason == nil {
reason = errors.New(`unspecified parse error`)
}
k := &unsupportedKey{
raw: bytes.Clone(raw),
reason: reason,
}
k.parseBestEffort()
return k
}
// parseBestEffort re-parses the minimum set of members needed to make
// the placeholder discoverable and nameable: "kid" (LookupKeyID and the
// duplicate-kid check), "kty" (error messages, KeyType()), and "alg"
// (error messages). A member that fails to parse is simply left absent.
// Everything else stays unparsed — the raw JSON is the entry's
// authoritative representation.
func (k *unsupportedKey) parseBestEffort() {
var fields map[string]json.RawMessage
if err := json.Unmarshal(k.raw, &fields); err != nil {
return
}
if raw, ok := fields[KeyTypeKey]; ok {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
k.rawKty = s
k.ktyPresent = true
}
}
if raw, ok := fields[KeyIDKey]; ok {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
k.keyID = &s
}
}
if raw, ok := fields[AlgorithmKey]; ok {
var s string
if err := json.Unmarshal(raw, &s); err == nil {
if alg, err := jwa.KeyAlgorithmFrom(s); err == nil {
k.algorithm = &alg
}
}
}
}
func (k *unsupportedKey) Reason() error {
return k.reason
}
// isUnsupportedKey implements the [UnsupportedKey] interface seal.
func (k *unsupportedKey) isUnsupportedKey() {}
// unsupportederr wraps the placeholder's Reason() in an error explaining
// that the operation cannot be performed on an unsupported key.
func (k *unsupportedKey) unsupportederr(op string) error {
kid := ""
if k.keyID != nil {
kid = *k.keyID
}
return fmt.Errorf(`jwk: cannot %s an unsupported key (kty=%q, kid=%q): the entry could not be parsed: %w`, op, k.rawKty, kid, k.reason)
}
func (k *unsupportedKey) KeyType() jwa.KeyType {
if !k.ktyPresent {
return jwa.EmptyKeyType()
}
return jwa.NewKeyType(k.rawKty)
}
func (k *unsupportedKey) Algorithm() (jwa.KeyAlgorithm, bool) {
if k.algorithm != nil {
return *k.algorithm, true
}
return nil, false
}
func (k *unsupportedKey) KeyID() (string, bool) {
if k.keyID != nil {
return *k.keyID, true
}
return "", false
}
// The remaining standard members are not mirrored: nothing consumes them
// on a placeholder (key selection rejects it before ever checking usage),
// and the raw JSON already carries them for round-tripping.
func (k *unsupportedKey) KeyOps() (KeyOperationList, bool) {
return nil, false
}
func (k *unsupportedKey) KeyUsage() (string, bool) {
return "", false
}
func (k *unsupportedKey) X509CertChain() (*cert.Chain, bool) {
return nil, false
}
func (k *unsupportedKey) X509CertThumbprint() (string, bool) {
return "", false
}
func (k *unsupportedKey) X509CertThumbprintS256() (string, bool) {
return "", false
}
func (k *unsupportedKey) X509URL() (string, bool) {
return "", false
}
func (k *unsupportedKey) Has(name string) bool {
switch name {
case KeyTypeKey:
return k.ktyPresent
case AlgorithmKey:
return k.algorithm != nil
case KeyIDKey:
return k.keyID != nil
default:
return false
}
}
// Get retrieves the best-effort common members (kty, alg, kid) into dst.
// Any other field is reported as absent — the raw JSON remains its only
// representation.
func (k *unsupportedKey) Get(name string, dst any) error {
switch name {
case KeyTypeKey:
if !k.ktyPresent {
return fmt.Errorf(`field %q not found`, name)
}
return blackmagic.AssignIfCompatible(dst, k.KeyType())
case AlgorithmKey:
if k.algorithm == nil {
return fmt.Errorf(`field %q not found`, name)
}
return blackmagic.AssignIfCompatible(dst, *k.algorithm)
case KeyIDKey:
if k.keyID == nil {
return fmt.Errorf(`field %q not found`, name)
}
return blackmagic.AssignIfCompatible(dst, *k.keyID)
default:
return fmt.Errorf(`field %q not found`, name)
}
}
func (k *unsupportedKey) Keys() []string {
keys := make([]string, 0, 3)
if k.ktyPresent {
keys = append(keys, KeyTypeKey)
}
if k.algorithm != nil {
keys = append(keys, AlgorithmKey)
}
if k.keyID != nil {
keys = append(keys, KeyIDKey)
}
return keys
}
// Set always returns an error: the verbatim raw JSON is the single
// source of truth for serialization, so mutation is not allowed (it
// would make the marshaled form diverge from the accessor view).
func (k *unsupportedKey) Set(string, any) error {
return k.unsupportederr("modify")
}
// Remove always returns an error, for the same reason as [Set].
func (k *unsupportedKey) Remove(string) error {
return k.unsupportederr("modify")
}
// Validate reports the retained parse error: a placeholder is by
// definition not a valid key. The error is wrapped in a key validation
// error so it classifies like every other built-in Key.Validate failure
// (jwk.IsKeyValidationError is true), while Reason() stays reachable
// through the wrapping chain.
func (k *unsupportedKey) Validate() error {
return NewKeyValidationError(k.unsupportederr("validate"))
}
// Thumbprint always returns an error: RFC 7638 thumbprints require the
// per-kty required members, which are not understood for a placeholder.
func (k *unsupportedKey) Thumbprint(crypto.Hash) ([]byte, error) {
return nil, k.unsupportederr("compute the thumbprint of")
}
// PublicKey always returns an error: whether the entry contains private
// material is unknowable, so no public projection can be derived safely.
func (k *unsupportedKey) PublicKey() (Key, error) {
return nil, k.unsupportederr("derive the public key of")
}
// Clone returns an independent copy of the placeholder. Placeholders are
// first-class set members, so they clone like any other key.
func (k *unsupportedKey) Clone() (Key, error) {
dst := &unsupportedKey{
raw: bytes.Clone(k.raw),
reason: k.reason,
rawKty: k.rawKty,
ktyPresent: k.ktyPresent,
}
if k.algorithm != nil {
tmp := *k.algorithm
dst.algorithm = &tmp
}
if k.keyID != nil {
tmp := *k.keyID
dst.keyID = &tmp
}
return dst, nil
}
// MarshalJSON emits the verbatim raw JSON of the original entry. This is
// the round-trip guarantee: a set containing a placeholder re-serializes
// the unknown entry unchanged.
func (k *unsupportedKey) MarshalJSON() ([]byte, error) {
return bytes.Clone(k.raw), nil
}
+16 -7
View File
@@ -15,9 +15,9 @@ go_library(
"message.go",
"options.go",
"options_gen.go",
"signer.go",
"sign_context.go",
"signature_builder.go",
"signer.go",
"streaming_detached.go",
"verifier.go",
"verify_context.go",
@@ -27,14 +27,14 @@ go_library(
deps = [
"//cert",
"//internal/base64",
"//internal/ecutil",
"//internal/json",
"//internal/tokens",
"//internal/keyconv",
"//internal/pool",
"//internal/tokens",
"//jwa",
"//jwk",
"//jws/internal/keytype",
"//jws/internal/jwsbb",
"//jws/internal/keyalg",
"//jws/jwsbb",
"//jws/legacy",
"//transform",
@@ -47,25 +47,34 @@ go_library(
go_test(
name = "jws_test",
srcs = [
"es256k_test.go",
"bench_marshal_test.go",
"bench_serialize_test.go",
"filter_test.go",
"format_detect_test.go",
"fuzz_test.go",
"headers_nil_test.go",
"headers_test.go",
"jws_crit_test.go",
"jws_internal_test.go",
"jws_test.go",
"key_provider_test.go",
"message_test.go",
"options_gen_test.go",
"signer_test.go",
"streaming_detached_test.go",
"unsupported_key_test.go",
],
embed = [":jws"],
deps = [
"//cert",
"//internal/base64",
"//internal/ecutil",
"//internal/json",
"//internal/jwxtest",
"//internal/tokens",
"//jwa",
"//jwk",
"//jwt",
"//jws/legacy",
"@com_github_lestrrat_go_dsig//:dsig",
"@com_github_lestrrat_go_httprc_v3//:httprc",
"@com_github_stretchr_testify//require",
],
+12 -18
View File
@@ -3,6 +3,8 @@ package jws
import (
"errors"
"fmt"
"github.com/lestrrat-go/jwx/v3/jws/internal/keyalg"
)
// errCritPresent is returned by VerifyCompactFast when the protected
@@ -45,25 +47,17 @@ func ErrB64Present() error {
return errB64Present
}
// errUnclassifiableKey is the common sentinel for AlgorithmsForKey
// failures: the key shape cannot be matched to any registered key type
// for signing. Three different code paths land here — Import-failed,
// kty-not-registered, and shape-rejected (e.g. ecdh) — but they're all
// the same logical "we can't classify this key" outcome from the
// caller's perspective. Wrap-with-this lets callers branch on
// errors.Is(err, jws.ErrUnclassifiableKey()) instead of pattern-matching
// the three error-message shapes the function previously emitted.
var errUnclassifiableKey = errors.New("jws: key cannot be classified for signing")
// ErrUnclassifiableKey returns the sentinel that jws.AlgorithmsForKey
// (and indirectly jws.Sign / jws.Verify when option-time validation
// fails) wraps when the supplied key cannot be matched to a registered
// key type. Branching on this sentinel is the right way to ask "is this
// a 'we can't tell what this key is' failure?" — the wrapping error
// also carries the concrete %T or %q diagnostic in its message, so the
// human-readable error stays specific.
// ErrUnclassifiableKey returns the sentinel that jws.Sign and jws.Verify
// wrap when option-time validation cannot match the supplied key to a
// registered key type. Branching on this sentinel is the right way to ask
// "is this a 'we can't tell what this key is' failure?" — the wrapping
// error also carries the concrete %T or %q diagnostic in its message, so
// the human-readable error stays specific.
//
// The sentinel itself lives in jws/internal/keyalg, which owns key
// classification.
func ErrUnclassifiableKey() error {
return errUnclassifiableKey
return keyalg.ErrUnclassifiableKey
}
type signError struct {
+3 -3
View File
@@ -812,9 +812,9 @@ func (h *stdHeaders) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
buf.WriteByte('"')
buf.WriteString(pair.Name)
buf.WriteString(`": `)
if err := json.WriteQuotedKey(buf, pair.Name); err != nil {
return nil, fmt.Errorf(`failed to encode field name %q: %w`, pair.Name, err)
}
buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
+25
View File
@@ -0,0 +1,25 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "jwsbb",
srcs = ["ecdsacurve.go"],
importpath = "github.com/lestrrat-go/jwx/v3/jws/internal/jwsbb",
visibility = ["//jws:__subpackages__"],
deps = ["@com_github_lestrrat_go_dsig//:dsig"],
)
alias(
name = "go_default_library",
actual = ":jwsbb",
visibility = ["//jws:__subpackages__"],
)
go_test(
name = "jwsbb_test",
srcs = ["ecdsacurve_test.go"],
deps = [
":jwsbb",
"@com_github_lestrrat_go_dsig//:dsig",
"@com_github_stretchr_testify//require",
],
)
+117
View File
@@ -0,0 +1,117 @@
package jwsbb
import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"fmt"
"github.com/lestrrat-go/dsig"
)
// This file enforces the RFC 7518 Section 3.4 binding between an ECDSA JWS
// algorithm and the curve its key must sit on (ES256/P-256, ES384/P-384,
// ES512/P-521). It is sign-side only, and jws reaches it only when the caller
// passes jws.WithStrictECDSA(true).
//
// The check is opt-in because the old permissive behavior is an interop
// defect, not a security hole: the signer controls both the key and the
// algorithm at the call site, and the JWS it produces is a genuine signature
// under its own key. Turning the check on by default would break working
// callers to fix a conformance problem they may not have.
//
// jws.Verify never reaches this file at all. It infers algorithms from a key
// when a JWKS entry carries no "alg" (see jws/internal/keyalg.Candidates and
// the deprecated jws.AlgorithmsForKey, whose godoc freezes that inference),
// and it must stay exactly as permissive as it is today.
// RequireECDSACurve reports whether key sits on the curve RFC 7518 Section
// 3.4 binds joseAlg to. It returns nil -- never an error -- when the binding
// cannot be established: dsigAlg is an ECDSA-family algorithm outside the
// three JOSE built-ins (e.g. ES256K, whether from the jwx_es256k build tag
// or an extension module), or key carries no readable curve. Only positive
// evidence of a mismatch is an error.
func RequireECDSACurve(joseAlg, dsigAlg string, key any) error {
want, ok := curveForDsigAlgorithm(dsigAlg)
if !ok {
return nil
}
pub := ecdsaPublicKeyOf(key)
if pub == nil || pub.Curve == nil {
return nil
}
if pub.Curve == want {
return nil
}
gotParams := pub.Curve.Params()
if gotParams == nil {
return nil
}
wantParams := want.Params()
if wantParams != nil && gotParams.Name == wantParams.Name {
return nil
}
return fmt.Errorf(`ECDSA curve mismatch: key is on %s, algorithm %q requires %s`,
curveName(pub.Curve), joseAlg, curveName(want))
}
// curveForDsigAlgorithm maps a dsig ECDSA algorithm name to the curve RFC
// 7518 Section 3.4 requires for it. Only the three JOSE built-ins are
// known; anything else (custom-curve extensions such as ES256K) misses
// deliberately, so the caller passes the key through unchecked.
func curveForDsigAlgorithm(dsigAlg string) (elliptic.Curve, bool) {
switch dsigAlg {
case dsig.ECDSAWithP256AndSHA256:
return elliptic.P256(), true
case dsig.ECDSAWithP384AndSHA384:
return elliptic.P384(), true
case dsig.ECDSAWithP521AndSHA512:
return elliptic.P521(), true
default:
return nil, false
}
}
// ecdsaPublicKeyOf extracts an *ecdsa.PublicKey from key, or nil when key is
// not (or does not expose) an ECDSA key. Callers pass an already-converted
// key (jwk.Key unwrapping happens before this is called), so only the raw Go
// crypto forms and an opaque crypto.Signer are handled here.
func ecdsaPublicKeyOf(key any) *ecdsa.PublicKey {
switch k := key.(type) {
case *ecdsa.PrivateKey:
if k == nil {
return nil
}
return &k.PublicKey
case ecdsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PublicKey:
return k
case ecdsa.PublicKey:
return &k
case crypto.Signer:
pub, ok := k.Public().(*ecdsa.PublicKey)
if !ok {
return nil
}
return pub
default:
return nil
}
}
// curveName returns crv.Params().Name, guarding a nil Params() the same way
// the comparison in RequireECDSACurve does.
func curveName(crv elliptic.Curve) string {
if crv == nil {
return "<nil>"
}
params := crv.Params()
if params == nil {
return "<unknown>"
}
return params.Name
}
+18
View File
@@ -0,0 +1,18 @@
load("@rules_go//go:def.bzl", "go_library")
go_library(
name = "keyalg",
srcs = ["keyalg.go"],
importpath = "github.com/lestrrat-go/jwx/v3/jws/internal/keyalg",
visibility = ["//jws:__subpackages__"],
deps = [
"//jwa",
"//jwk",
],
)
alias(
name = "go_default_library",
actual = ":keyalg",
visibility = ["//jws:__subpackages__"],
)
+259
View File
@@ -0,0 +1,259 @@
// Package keyalg works out which signature algorithms a key can be used
// with, and owns the registration tables it reads to decide.
//
// The answer is a guess, on purpose. jws.Verify uses it to pick
// algorithms to try when a JWKS key has no "alg" field, and option
// handling uses it to catch a key that clearly does not go with the
// algorithm asked for. It is not a check for whether a key and an
// algorithm are a valid pair, and the list can be wider than any one RFC
// allows for a given key.
//
// This package is internal to jwx. The jws package still has
// AlgorithmsForKey, a one-line wrapper over [Candidates], but that is
// deprecated and was never meant for callers outside jwx. Everything in
// the tree calls this package instead.
package keyalg
import (
"crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"errors"
"fmt"
"slices"
"sync"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwk"
)
// ErrUnclassifiableKey is the common sentinel for [Candidates] failures:
// the key shape cannot be matched to any registered key type for signing.
// Three different code paths land here — Import-failed, kty-not-registered,
// and shape-rejected (e.g. ecdh) — but they're all the same logical "we
// can't classify this key" outcome from the caller's perspective.
// Wrap-with-this lets callers branch on errors.Is instead of
// pattern-matching the three error-message shapes.
//
// The jws package re-exports this through jws.ErrUnclassifiableKey().
var ErrUnclassifiableKey = errors.New("jws: key cannot be classified for signing")
// curver is implemented by jwk.Key types that carry curve information.
type curver interface {
Crv() (jwa.EllipticCurveAlgorithm, bool)
}
var mu sync.RWMutex
var keyTypeToAlgorithms = make(map[jwa.KeyType][]jwa.SignatureAlgorithm)
var algorithmToKeyTypes = make(map[jwa.SignatureAlgorithm][]jwa.KeyType)
var curveToAlgorithms = make(map[jwa.EllipticCurveAlgorithm][]jwa.SignatureAlgorithm)
func init() {
RegisterForKeyType(jwa.OKP(), jwa.EdDSA())
RegisterForCurve(jwa.Ed25519(), jwa.EdDSAEd25519())
for _, alg := range []jwa.SignatureAlgorithm{jwa.HS256(), jwa.HS384(), jwa.HS512()} {
RegisterForKeyType(jwa.OctetSeq(), alg)
}
for _, alg := range []jwa.SignatureAlgorithm{jwa.RS256(), jwa.RS384(), jwa.RS512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} {
RegisterForKeyType(jwa.RSA(), alg)
}
for _, alg := range []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512()} {
RegisterForKeyType(jwa.EC(), alg)
}
}
// RegisterForKeyType records alg as usable with keys of type kty.
//
// This backs jws.RegisterAlgorithmForKeyType, which extension modules
// call from init() to add their own algorithms.
func RegisterForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) {
mu.Lock()
defer mu.Unlock()
keyTypeToAlgorithms[kty] = append(keyTypeToAlgorithms[kty], alg)
if !slices.Contains(algorithmToKeyTypes[alg], kty) {
algorithmToKeyTypes[alg] = append(algorithmToKeyTypes[alg], kty)
}
}
// RegisterForCurve scopes alg to the given elliptic curve. When
// [Candidates] can determine a key's curve, an algorithm registered under
// some curve is offered only for keys on that curve, instead of for every
// key of its key type.
//
// This backs jws.RegisterAlgorithmForCurve. It is append-only and
// deduplicates entries, so builtin registrations cannot be overwritten by
// external modules.
func RegisterForCurve(crv jwa.EllipticCurveAlgorithm, alg jwa.SignatureAlgorithm) {
mu.Lock()
defer mu.Unlock()
if slices.Contains(curveToAlgorithms[crv], alg) {
return
}
curveToAlgorithms[crv] = append(curveToAlgorithms[crv], alg)
}
// KeyTypesFor returns the key types registered for alg. The reverse index
// is maintained at registration time so this is an O(1) lookup. It returns
// nil if no key type is registered for alg, which signals callers to skip
// any prefilter.
func KeyTypesFor(alg jwa.SignatureAlgorithm) []jwa.KeyType {
mu.RLock()
defer mu.RUnlock()
// Copy so the caller can safely iterate without holding the lock;
// RegisterForKeyType may append concurrently after we return.
// Typical length is 1.
return slices.Clone(algorithmToKeyTypes[alg])
}
// Candidates returns the signature algorithms that key could be used
// with. It only takes into consideration keys/algorithms for verification
// purposes, as this is the only usage where one may need to dynamically
// figure out which method to use.
//
// When the key's curve is known, algorithms registered for that curve via
// [RegisterForCurve] are combined with key-type-level algorithms to
// produce a more precise result. The curve is known for a [jwk.Key] that
// has a Crv() method, for raw ed25519 keys, and for any raw key that
// reaches the [jwk.Import] fallback below.
//
// ECDSA is the exception. A raw [ecdsa.PublicKey] or [ecdsa.PrivateKey] is
// classified by key type alone and its Curve field is never read. No
// builtin registration binds P-256, P-384, or P-521 to an algorithm
// either, so every EC key reports the full ES* list no matter which curve
// it sits on. RFC 7518 Section 3.4 is stricter than that; jws.Sign
// enforces it only when the caller passes jws.WithStrictECDSA(true).
//
// Accepted key shapes (resolved in order):
//
// 1. [jwk.Key] — kty is read directly; if the implementation also exposes
// Crv(), the curve refines the result.
// 2. Stdlib crypto types: [rsa.PublicKey] / [rsa.PrivateKey] (and pointer
// forms), [ecdsa.PublicKey] / [ecdsa.PrivateKey] (and pointer forms),
// [ed25519.PublicKey], [ed25519.PrivateKey], and [byte] slices for
// symmetric keys.
// 3. [crypto/ecdh.PublicKey] / [crypto/ecdh.PrivateKey] (and pointer
// forms) — explicitly rejected; ECDH keys are key-agreement only.
// Returns an error wrapping [ErrUnclassifiableKey].
// 4. [crypto.Signer] (e.g. KMS-backed adapters) — resolved once via
// .Public(); the public key is then re-classified through tiers 1–2
// or the [jwk.Import] fallback below. To prevent infinite recursion,
// a Signer whose .Public() is itself a Signer is left for the
// downstream dispatcher to handle.
// 5. [jwk.Import] fallback — anything else is offered to the import
// registry, allowing extension modules to register their own raw key
// types.
//
// All "we cannot classify this key" failures wrap [ErrUnclassifiableKey],
// so callers can branch with errors.Is rather than pattern-matching error
// strings. The wrapping error keeps the concrete %T or %q diagnostic in
// its message for human readers.
func Candidates(key any) ([]jwa.SignatureAlgorithm, error) {
var kty jwa.KeyType
var crv jwa.EllipticCurveAlgorithm
var hasCrv bool
switch key := key.(type) {
case jwk.Key:
kty = key.KeyType()
if ck, ok := key.(curver); ok {
crv, hasCrv = ck.Crv()
}
case rsa.PublicKey, *rsa.PublicKey, rsa.PrivateKey, *rsa.PrivateKey:
kty = jwa.RSA()
case ecdsa.PublicKey, *ecdsa.PublicKey, ecdsa.PrivateKey, *ecdsa.PrivateKey:
kty = jwa.EC()
case ed25519.PublicKey, ed25519.PrivateKey:
kty = jwa.OKP()
crv = jwa.Ed25519()
hasCrv = true
case *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey:
// ecdh keys are for key agreement (X25519/X448), not signing.
// Reject at the API boundary instead of returning a misleading
// algorithm list that would fail deeper in the signing stack.
return nil, fmt.Errorf(`%w: key type %T cannot be used for signing (ecdh keys are key-agreement only)`, ErrUnclassifiableKey, key)
case []byte:
kty = jwa.OctetSeq()
default:
// For crypto.Signer from external packages (e.g. KMS-backed signers),
// extract the underlying public key type via .Public().
// Standard library types (*rsa.PrivateKey, etc.) are already handled
// by the concrete cases above.
var signerPubErr error
if signer, ok := key.(crypto.Signer); ok {
pub := signer.Public()
// Guard: only recurse if the public key is not itself a crypto.Signer,
// to prevent infinite recursion from pathological implementations.
if _, isSigner := pub.(crypto.Signer); !isSigner {
algs, err := Candidates(pub)
if err == nil {
return algs, nil
}
// Save the inner classification error so a
// downstream Import-fallback failure can surface
// both diagnostics. A successful Import discards
// signerPubErr — only the eventual failure path
// joins them.
signerPubErr = err
}
}
imported, err := jwk.Import(key)
if err != nil {
outer := fmt.Errorf(`%w: unknown key type %T`, ErrUnclassifiableKey, key)
if signerPubErr != nil {
return nil, errors.Join(outer, signerPubErr)
}
return nil, outer
}
kty = imported.KeyType()
if ck, ok := imported.(curver); ok {
crv, hasCrv = ck.Crv()
}
}
mu.RLock()
defer mu.RUnlock()
ktyAlgs, ok := keyTypeToAlgorithms[kty]
if !ok {
return nil, fmt.Errorf(`%w: unregistered key type %q`, ErrUnclassifiableKey, kty)
}
// If we know the curve and there are curve-specific registrations,
// return only key-type-level algorithms (those not registered under
// any curve) plus curve-specific algorithms for this curve.
if hasCrv {
crvAlgs := curveToAlgorithms[crv]
return filterForCurve(ktyAlgs, crvAlgs), nil
}
return ktyAlgs, nil
}
// filterForCurve returns the subset of ktyAlgs that are not registered
// under any curve (i.e., generic for the key type) plus the curve-specific
// algorithms from crvAlgs.
func filterForCurve(ktyAlgs, crvAlgs []jwa.SignatureAlgorithm) []jwa.SignatureAlgorithm {
var result []jwa.SignatureAlgorithm
// Add key-type-level algorithms that are not claimed by any curve
for _, alg := range ktyAlgs {
if !isRegisteredUnderAnyCurve(alg) {
result = append(result, alg)
}
}
// Add curve-specific algorithms
result = append(result, crvAlgs...)
return result
}
func isRegisteredUnderAnyCurve(alg jwa.SignatureAlgorithm) bool {
for _, algs := range curveToAlgorithms {
if slices.Contains(algs, alg) {
return true
}
}
return false
}
Loaded 100 of 339 files, more files were not shown because too many files have changed in this diff. Show more