mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-10-02 00:45:16 -04:00
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:
1 parent
a1982c6908
commit
24579530be
339 files changed
+38711
-11288
No files matched your search
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Generated
+2
@@ -6,3 +6,5 @@ glob.iml
|
||||
*.dot
|
||||
*.png
|
||||
*.svg
|
||||
patterns.txt
|
||||
*.bench
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
sudo: false
|
||||
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.5.3
|
||||
|
||||
script:
|
||||
- go test -v ./...
|
||||
+36
-21
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,8 @@
|
||||
//go:build !globdebug
|
||||
// +build !globdebug
|
||||
|
||||
package debug
|
||||
|
||||
const Enabled = false
|
||||
|
||||
func Printf(f string, args ...any) {}
|
||||
+14
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,4 +1,4 @@
|
||||
# github.com/lestrrat-go/dsig [](https://github.com/lestrrat-go/dsig/actions/workflows/ci.yml) [](https://pkg.go.dev/github.com/lestrrat-go/dsig) [](https://codecov.io/github/lestrrat-go/dsig?branch=v1)
|
||||
# github.com/lestrrat-go/dsig [](https://github.com/lestrrat-go/dsig/actions/workflows/ci.yml) [](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
@@ -34,4 +34,4 @@ const (
|
||||
// EdDSA signature algorithms
|
||||
// These use Edwards-curve Digital Signature Algorithm (supports Ed25519 and Ed448)
|
||||
EdDSA = "EDDSA"
|
||||
)
|
||||
)
|
||||
+41
-5
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -8,7 +8,6 @@ go_library(
|
||||
deps = [
|
||||
"//jwk",
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
"@org_golang_x_crypto//ed25519",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
+2
-4
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -27,4 +27,4 @@ alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jwkbb",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
)
|
||||
+1
-1
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in new issue
Block a user