diff --git a/go.mod b/go.mod
index 31beb08bee..1165256086 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
github.com/Nerzal/gocloak/v13 v13.9.0
github.com/bbalet/stopwords v1.0.0
github.com/beevik/etree v1.6.0
- github.com/blevesearch/bleve/v2 v2.5.7
+ github.com/blevesearch/bleve/v2 v2.6.0
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.18.0
github.com/cs3org/go-cs3apis v0.0.0-20260424072047-8d9ef7076ae9
@@ -55,13 +55,13 @@ require (
github.com/libregraph/lico v0.66.0
github.com/mna/pigeon v1.3.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
- github.com/nats-io/nats-server/v2 v2.14.0
+ github.com/nats-io/nats-server/v2 v2.14.2
github.com/nats-io/nats.go v1.51.0
github.com/olekukonko/tablewriter v1.1.4
github.com/onsi/ginkgo v1.16.5
github.com/onsi/ginkgo/v2 v2.28.3
github.com/onsi/gomega v1.40.0
- github.com/open-policy-agent/opa v1.15.2
+ github.com/open-policy-agent/opa v1.17.1
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260310090739-853d972b282d
github.com/opencloud-eu/reva/v2 v2.46.4-0.20260615073558-209c2cd3b52b
@@ -95,7 +95,7 @@ require (
go-micro.dev/v4 v4.11.0
go.etcd.io/bbolt v1.4.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0
- go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
go.opentelemetry.io/contrib/zpages v0.68.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
@@ -130,7 +130,7 @@ require (
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
- github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
+ github.com/RoaringBitmap/roaring/v2 v2.14.5 // indirect
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/ajg/form v1.5.1 // indirect
github.com/alexedwards/argon2id v1.0.0 // indirect
@@ -140,24 +140,25 @@ require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bitly/go-simplejson v0.5.0 // indirect
- github.com/bits-and-blooms/bitset v1.22.0 // indirect
- github.com/blevesearch/bleve_index_api v1.2.11 // indirect
- github.com/blevesearch/geo v0.2.4 // indirect
- github.com/blevesearch/go-faiss v1.0.26 // indirect
+ github.com/bits-and-blooms/bitset v1.24.2 // indirect
+ github.com/blevesearch/bleve_index_api v1.3.11 // indirect
+ github.com/blevesearch/geo v0.2.5 // indirect
+ github.com/blevesearch/go-faiss v1.1.0 // indirect
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
github.com/blevesearch/gtreap v0.1.1 // indirect
- github.com/blevesearch/mmap-go v1.0.4 // indirect
- github.com/blevesearch/scorch_segment_api/v2 v2.3.13 // indirect
+ github.com/blevesearch/mmap-go v1.2.0 // indirect
+ github.com/blevesearch/scorch_segment_api/v2 v2.4.7 // indirect
github.com/blevesearch/segment v0.9.1 // indirect
github.com/blevesearch/snowballstem v0.9.0 // indirect
github.com/blevesearch/upsidedown_store_api v1.0.2 // indirect
- github.com/blevesearch/vellum v1.1.0 // indirect
- github.com/blevesearch/zapx/v11 v11.4.2 // indirect
- github.com/blevesearch/zapx/v12 v12.4.2 // indirect
- github.com/blevesearch/zapx/v13 v13.4.2 // indirect
- github.com/blevesearch/zapx/v14 v14.4.2 // indirect
- github.com/blevesearch/zapx/v15 v15.4.2 // indirect
- github.com/blevesearch/zapx/v16 v16.2.8 // indirect
+ github.com/blevesearch/vellum v1.2.0 // indirect
+ github.com/blevesearch/zapx/v11 v11.4.3 // indirect
+ github.com/blevesearch/zapx/v12 v12.4.3 // indirect
+ github.com/blevesearch/zapx/v13 v13.4.3 // indirect
+ github.com/blevesearch/zapx/v14 v14.4.3 // indirect
+ github.com/blevesearch/zapx/v15 v15.4.3 // indirect
+ github.com/blevesearch/zapx/v16 v16.3.4 // indirect
+ github.com/blevesearch/zapx/v17 v17.1.2 // indirect
github.com/bluele/gcache v0.0.2 // indirect
github.com/bombsimon/logrusr/v3 v3.1.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -182,7 +183,7 @@ require (
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/deckarep/golang-set v1.8.0 // indirect
- github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
github.com/dgraph-io/ristretto v0.2.0 // indirect
github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
@@ -199,7 +200,7 @@ require (
github.com/evanphx/json-patch/v5 v5.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
- github.com/fsnotify/fsnotify v1.9.0 // indirect
+ github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/gdexlab/go-render v1.0.1 // indirect
github.com/go-acme/lego/v4 v4.4.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
@@ -227,14 +228,14 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.1 // indirect
- github.com/goccy/go-json v0.10.5 // indirect
+ github.com/goccy/go-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gofrs/uuid v4.4.0+incompatible // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
- github.com/golang/snappy v0.0.4 // indirect
+ github.com/golang/snappy v1.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
@@ -256,18 +257,18 @@ require (
github.com/json-iterator/go v1.1.12 // indirect
github.com/juliangruber/go-intersect v1.1.0 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
- github.com/klauspost/compress v1.18.5 // indirect
+ github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kovidgoyal/go-parallel v1.1.1 // indirect
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.0.0 // indirect
+ github.com/lestrrat-go/dsig v1.2.1 // 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.2 // indirect
- github.com/lestrrat-go/jwx/v3 v3.0.13 // 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/option/v2 v2.0.0 // indirect
github.com/libregraph/oidc-go v1.1.0 // indirect
github.com/longsleep/go-metrics v1.0.0 // indirect
@@ -302,8 +303,8 @@ require (
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
- github.com/nats-io/jwt/v2 v2.8.1 // indirect
- github.com/nats-io/nkeys v0.4.15 // indirect
+ github.com/nats-io/jwt/v2 v2.8.2 // indirect
+ github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oklog/run v1.2.0 // indirect
@@ -327,7 +328,7 @@ require (
github.com/prometheus/alertmanager v0.31.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
- github.com/prometheus/procfs v0.17.0 // indirect
+ github.com/prometheus/procfs v0.20.1 // indirect
github.com/prometheus/statsd_exporter v0.22.8 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/rs/xid v1.6.0 // indirect
@@ -365,8 +366,8 @@ require (
github.com/toorop/go-dkim v0.0.0-20201103131630-e1cd1a0a5208 // indirect
github.com/trustelem/zxcvbn v1.0.1 // indirect
github.com/urfave/cli/v2 v2.27.7 // indirect
- github.com/valyala/fastjson v1.6.7 // indirect
- github.com/vektah/gqlparser/v2 v2.5.32 // indirect
+ github.com/valyala/fastjson v1.6.10 // indirect
+ github.com/vektah/gqlparser/v2 v2.5.33 // 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
@@ -386,7 +387,7 @@ require (
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
- go.yaml.in/yaml/v2 v2.4.3 // indirect
+ go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/sys v0.45.0 // indirect
diff --git a/go.sum b/go.sum
index 45cb574aa0..4f2743eac4 100644
--- a/go.sum
+++ b/go.sum
@@ -91,8 +91,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87/go.mod h1:iGLljf5n9GjT6kc0HBvyI1nOKnGQbNB66VzSNbK5iks=
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
-github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
-github.com/RoaringBitmap/roaring/v2 v2.4.5/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
+github.com/RoaringBitmap/roaring/v2 v2.14.5 h1:ckd0o545JqDPeVJDgeFoaM21eBixUnlWfYgjE5VnyWw=
+github.com/RoaringBitmap/roaring/v2 v2.14.5/go.mod h1:eq4wdNXxtJIS/oikeCzdX1rBzek7ANzbth041hrU8Q4=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
@@ -145,46 +145,47 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y=
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
-github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
-github.com/bits-and-blooms/bitset v1.22.0 h1:Tquv9S8+SGaS3EhyA+up3FXzmkhxPGjQQCkcs2uw7w4=
-github.com/bits-and-blooms/bitset v1.22.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.24.2 h1:M7/NzVbsytmtfHbumG+K2bremQPMJuqv1JD3vOaFxp0=
+github.com/bits-and-blooms/bitset v1.24.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
-github.com/blevesearch/bleve/v2 v2.5.7 h1:2d9YrL5zrX5EBBW++GOaEKjE+NPWeZGaX77IM26m1Z8=
-github.com/blevesearch/bleve/v2 v2.5.7/go.mod h1:yj0NlS7ocGC4VOSAedqDDMktdh2935v2CSWOCDMHdSA=
-github.com/blevesearch/bleve_index_api v1.2.11 h1:bXQ54kVuwP8hdrXUSOnvTQfgK0KI1+f9A0ITJT8tX1s=
-github.com/blevesearch/bleve_index_api v1.2.11/go.mod h1:rKQDl4u51uwafZxFrPD1R7xFOwKnzZW7s/LSeK4lgo0=
-github.com/blevesearch/geo v0.2.4 h1:ECIGQhw+QALCZaDcogRTNSJYQXRtC8/m8IKiA706cqk=
-github.com/blevesearch/geo v0.2.4/go.mod h1:K56Q33AzXt2YExVHGObtmRSFYZKYGv0JEN5mdacJJR8=
-github.com/blevesearch/go-faiss v1.0.26 h1:4dRLolFgjPyjkaXwff4NfbZFdE/dfywbzDqporeQvXI=
-github.com/blevesearch/go-faiss v1.0.26/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
+github.com/blevesearch/bleve/v2 v2.6.0 h1:Cyd3dd4q5tCbOV8MnKUVRUDYMHOir9xn12NZzXVSEd4=
+github.com/blevesearch/bleve/v2 v2.6.0/go.mod h1:gLmI8lWgHgrIYf7UpUX7JISI1CaqC6VScu46mHThuAY=
+github.com/blevesearch/bleve_index_api v1.3.11 h1:x29vbV8OjWfLcrDVd7Lr1q+BkLNS0JWNEig0MCVnKH4=
+github.com/blevesearch/bleve_index_api v1.3.11/go.mod h1:xvd48t5XMeeioWQ5/jZvgLrV98flT2rdvEJ3l/ki4Ko=
+github.com/blevesearch/geo v0.2.5 h1:yJg9FX1oRwLnjXSXF+ECHfXFTF4diF02Ca/qUGVjJhE=
+github.com/blevesearch/geo v0.2.5/go.mod h1:Jhq7WE2K6mJTx1xS44M2pUO6Io+wjCSHh1+co3YOgH4=
+github.com/blevesearch/go-faiss v1.1.0 h1:xM7Jc0ZUCv5lssG9Ohj3Jv0SdTpxcUABU1dDt9XVsc4=
+github.com/blevesearch/go-faiss v1.1.0/go.mod h1:OMGQwOaRRYxrmeNdMrXJPvVx8gBnvE5RYrr0BahNnkk=
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
github.com/blevesearch/gtreap v0.1.1 h1:2JWigFrzDMR+42WGIN/V2p0cUvn4UP3C4Q5nmaZGW8Y=
github.com/blevesearch/gtreap v0.1.1/go.mod h1:QaQyDRAT51sotthUWAH4Sj08awFSSWzgYICSZ3w0tYk=
-github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
-github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
-github.com/blevesearch/scorch_segment_api/v2 v2.3.13 h1:ZPjv/4VwWvHJZKeMSgScCapOy8+DdmsmRyLmSB88UoY=
-github.com/blevesearch/scorch_segment_api/v2 v2.3.13/go.mod h1:ENk2LClTehOuMS8XzN3UxBEErYmtwkE7MAArFTXs9Vc=
+github.com/blevesearch/mmap-go v1.2.0 h1:l33nNKPFcBjJUMwem6sAYJPUzhUCABoK9FxZDGiFNBI=
+github.com/blevesearch/mmap-go v1.2.0/go.mod h1:Vd6+20GBhEdwJnU1Xohgt88XCD/CTWcqbCNxkZpyBo0=
+github.com/blevesearch/scorch_segment_api/v2 v2.4.7 h1:GlMzW08hcsM3DnLUxhyF/1PcDal1qtvvIuytuph5djw=
+github.com/blevesearch/scorch_segment_api/v2 v2.4.7/go.mod h1://IJ7tG3QCf0cWW/aVSXqy77tc1AvLu3fcJLYEvOAFs=
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
github.com/blevesearch/upsidedown_store_api v1.0.2 h1:U53Q6YoWEARVLd1OYNc9kvhBMGZzVrdmaozG2MfoB+A=
github.com/blevesearch/upsidedown_store_api v1.0.2/go.mod h1:M01mh3Gpfy56Ps/UXHjEO/knbqyQ1Oamg8If49gRwrQ=
-github.com/blevesearch/vellum v1.1.0 h1:CinkGyIsgVlYf8Y2LUQHvdelgXr6PYuvoDIajq6yR9w=
-github.com/blevesearch/vellum v1.1.0/go.mod h1:QgwWryE8ThtNPxtgWJof5ndPfx0/YMBh+W2weHKPw8Y=
-github.com/blevesearch/zapx/v11 v11.4.2 h1:l46SV+b0gFN+Rw3wUI1YdMWdSAVhskYuvxlcgpQFljs=
-github.com/blevesearch/zapx/v11 v11.4.2/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
-github.com/blevesearch/zapx/v12 v12.4.2 h1:fzRbhllQmEMUuAQ7zBuMvKRlcPA5ESTgWlDEoB9uQNE=
-github.com/blevesearch/zapx/v12 v12.4.2/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
-github.com/blevesearch/zapx/v13 v13.4.2 h1:46PIZCO/ZuKZYgxI8Y7lOJqX3Irkc3N8W82QTK3MVks=
-github.com/blevesearch/zapx/v13 v13.4.2/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
-github.com/blevesearch/zapx/v14 v14.4.2 h1:2SGHakVKd+TrtEqpfeq8X+So5PShQ5nW6GNxT7fWYz0=
-github.com/blevesearch/zapx/v14 v14.4.2/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
-github.com/blevesearch/zapx/v15 v15.4.2 h1:sWxpDE0QQOTjyxYbAVjt3+0ieu8NCE0fDRaFxEsp31k=
-github.com/blevesearch/zapx/v15 v15.4.2/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
-github.com/blevesearch/zapx/v16 v16.2.8 h1:SlnzF0YGtSlrsOE3oE7EgEX6BIepGpeqxs1IjMbHLQI=
-github.com/blevesearch/zapx/v16 v16.2.8/go.mod h1:murSoCJPCk25MqURrcJaBQ1RekuqSCSfMjXH4rHyA14=
+github.com/blevesearch/vellum v1.2.0 h1:xkDiOEsHc2t3Cp0NsNZZ36pvc130sCzcGKOPMzXe+e0=
+github.com/blevesearch/vellum v1.2.0/go.mod h1:uEcfBJz7mAOf0Kvq6qoEKQQkLODBF46SINYNkZNae4k=
+github.com/blevesearch/zapx/v11 v11.4.3 h1:PTZOO5loKpHC/x/GzmPZNa9cw7GZIQxd5qRjwij9tHY=
+github.com/blevesearch/zapx/v11 v11.4.3/go.mod h1:4gdeyy9oGa/lLa6D34R9daXNUvfMPZqUYjPwiLmekwc=
+github.com/blevesearch/zapx/v12 v12.4.3 h1:eElXvAaAX4m04t//CGBQAtHNPA+Q6A1hHZVrN3LSFYo=
+github.com/blevesearch/zapx/v12 v12.4.3/go.mod h1:TdFmr7afSz1hFh/SIBCCZvcLfzYvievIH6aEISCte58=
+github.com/blevesearch/zapx/v13 v13.4.3 h1:qsdhRhaSpVnqDFlRiH9vG5+KJ+dE7KAW9WyZz/KXAiE=
+github.com/blevesearch/zapx/v13 v13.4.3/go.mod h1:knK8z2NdQHlb5ot/uj8wuvOq5PhDGjNYQQy0QDnopZk=
+github.com/blevesearch/zapx/v14 v14.4.3 h1:GY4Hecx0C6UTmiNC2pKdeA2rOKiLR5/rwpU9WR51dgM=
+github.com/blevesearch/zapx/v14 v14.4.3/go.mod h1:rz0XNb/OZSMjNorufDGSpFpjoFKhXmppH9Hi7a877D8=
+github.com/blevesearch/zapx/v15 v15.4.3 h1:iJiMJOHrz216jyO6lS0m9RTCEkprUnzvqAI2lc/0/CU=
+github.com/blevesearch/zapx/v15 v15.4.3/go.mod h1:1pssev/59FsuWcgSnTa0OeEpOzmhtmr/0/11H0Z8+Nw=
+github.com/blevesearch/zapx/v16 v16.3.4 h1:hDAqA8qusZTNbPEL7//w5P65UZ2de6yhSeUaTbp0Po0=
+github.com/blevesearch/zapx/v16 v16.3.4/go.mod h1:zqkPPqs9GS9FzVWzCO3Wf1X044yWAV17+4zb+FTiEHg=
+github.com/blevesearch/zapx/v17 v17.1.2 h1:avbOk2igaASNoiy0BE/jPgcxAnRI2PGeydeP4hg7Ikk=
+github.com/blevesearch/zapx/v17 v17.1.2/go.mod h1:WQObxKrqUX7cd0G1GMvDfc/bmZzQvoy7APOPimx7DiI=
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
@@ -196,8 +197,8 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3 h1:h8Z0hBv5tg/uZMKu8V47+DKWYVQg0lYP8lXDQq7uRpE=
github.com/butonic/go-micro/v4 v4.11.1-0.20241115112658-b5d4de5ed9b3/go.mod h1:eE/tD53n3KbVrzrWxKLxdkGw45Fg1qaNLWjpJMvIUF4=
-github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE=
-github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4=
+github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
+github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
@@ -277,8 +278,8 @@ github.com/davidbyttow/govips/v2 v2.18.0 h1:pZRshWVYvewP/TZx3yZ7YeC42WyLXg53tHy5
github.com/davidbyttow/govips/v2 v2.18.0/go.mod h1:8+nst5zfMoats12PgmmAPh6p5OfjDaXK0BXMFl/vOcM=
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/deepmap/oapi-codegen v1.3.11/go.mod h1:suMvK7+rKlx3+tpa8ByptmvoXbAV70wERKTOGH3hLp0=
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=
@@ -349,8 +350,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
-github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
+github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gdexlab/go-render v1.0.1 h1:rxqB3vo5s4n1kF0ySmoNeSPRYkEsyHgln4jFIQY7v0U=
@@ -477,8 +478,8 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
-github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
-github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
+github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw=
@@ -535,8 +536,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
-github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
@@ -719,8 +720,8 @@ github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
-github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
-github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
@@ -760,16 +761,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.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
-github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
+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-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.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0=
-github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
-github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
-github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
+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/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=
@@ -896,14 +897,14 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8m
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04/go.mod h1:5sN+Lt1CaY4wsPvgQH/jsuJi4XO2ssZbdsIizr4CVC8=
-github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
-github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
-github.com/nats-io/nats-server/v2 v2.14.0 h1:+8q0HrDFotwLLcGH/legOEOnowunhK+aZ4GYBIWpQlM=
-github.com/nats-io/nats-server/v2 v2.14.0/go.mod h1:ImVUUDvfClJbb6cuJQRc1VmgDCXKM5ds0OoiG9MVOKo=
+github.com/nats-io/jwt/v2 v2.8.2 h1:XXRgB60MSTnqsRwejQurVDs/hcv2dkt+86GjI+I/bMc=
+github.com/nats-io/jwt/v2 v2.8.2/go.mod h1:Ag/56sq9OblL4JgdYufDd16Egb17Kr/8WwwuO/forVc=
+github.com/nats-io/nats-server/v2 v2.14.2 h1:Q7dRhCY03Y00rETFW3KV+KGaCIajlDfWgWUVgbMxyuk=
+github.com/nats-io/nats-server/v2 v2.14.2/go.mod h1:lWpb1bSpRELZfRdlMkdz8E7lbXKKyNe8RIn0vvepIHs=
github.com/nats-io/nats.go v1.51.0 h1:ByW84XTz6W03GSSsygsZcA+xgKK8vPGaa/FCAAEHnAI=
github.com/nats-io/nats.go v1.51.0/go.mod h1:26HypzazeOkyO3/mqd1zZd53STJN0EjCYF9Uy2ZOBno=
-github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
-github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
+github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
+github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
@@ -940,8 +941,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.40.0 h1:Vtol0e1MghCD2ZVIilPDIg44XSL9l2QAn8ZNaljWcJc=
github.com/onsi/gomega v1.40.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A=
-github.com/open-policy-agent/opa v1.15.2 h1:dS9q+0Yvruq/VNvWJc5qCvCchn715OWc3HLHXn/UCCc=
-github.com/open-policy-agent/opa v1.15.2/go.mod h1:c6SN+7jSsUcKJLQc5P4yhwx8YYDRbjpAiGkBOTqxaa4=
+github.com/open-policy-agent/opa v1.17.1 h1:wO0MOux/VCqY41aVAD6Toe1p3A7O7DlRZ1RHmYSpoS8=
+github.com/open-policy-agent/opa v1.17.1/go.mod h1:lcuZYSlqQpXFzsA6EJCELmfR5+nNOpZYX+eo7xaIIlk=
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=
@@ -1051,8 +1052,8 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
-github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
-github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/prometheus/statsd_exporter v0.22.7/go.mod h1:N/TevpjkIh9ccs6nuzY3jQn9dFqnUakOjnEuMPJJJnI=
github.com/prometheus/statsd_exporter v0.22.8 h1:Qo2D9ZzaQG+id9i5NYNGmbf1aa/KxKbB9aKfMS+Yib0=
github.com/prometheus/statsd_exporter v0.22.8/go.mod h1:/DzwbTEaFTE0Ojz5PqcSk6+PFHOPWGxdXVr6yC8eFOM=
@@ -1229,12 +1230,12 @@ github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/X
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/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
-github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM=
-github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
+github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
+github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
-github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc=
-github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
+github.com/vektah/gqlparser/v2 v2.5.33 h1:lRp8aIeNUNbimf/axZd7ETg24q06hBtPaas+TcvI/7E=
+github.com/vektah/gqlparser/v2 v2.5.33/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts=
github.com/vinyldns/go-vinyldns v0.0.0-20200917153823-148a5f6b8f14/go.mod h1:RWc47jtnVuQv6+lY3c768WtXCas/Xi+U5UFc5xULmYg=
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
@@ -1300,8 +1301,8 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0 h1:2yEATaop1/a1I4psnSLgWVPLWwCzkqWakgJy7xTDVy0=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.69.0/go.mod h1:D7J12YRapIekYyPWgGPlA/23pRmpSEZC5xJC/TTLI9U=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
go.opentelemetry.io/contrib/zpages v0.68.0 h1:H5yrUwxPrbvhzdBxjQD+VXMtPjIBfp8NWNVvQT8E30M=
go.opentelemetry.io/contrib/zpages v0.68.0/go.mod h1:sZGctYYO4UOHItj9bx3F+t/s+u1Fv8CHCJ5s2eR2cjU=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
@@ -1337,8 +1338,8 @@ go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
-go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
+go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
+go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20180621125126-a49355c7e3f8/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1783,7 +1784,6 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/.gitignore b/vendor/github.com/RoaringBitmap/roaring/v2/.gitignore
index 851f323dba..eb738f1610 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/.gitignore
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/.gitignore
@@ -3,3 +3,4 @@ roaring-fuzz.zip
workdir
coverage.out
testdata/all3.classic
+/vendor
\ No newline at end of file
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/Makefile b/vendor/github.com/RoaringBitmap/roaring/v2/Makefile
new file mode 100644
index 0000000000..033fc7b8e9
--- /dev/null
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/Makefile
@@ -0,0 +1,10 @@
+# Display general help about this command
+help:
+ @echo ""
+ @echo "The following commands are available:"
+ @echo " make unconvert : Find unnecessary type conversions"
+ @echo ""
+
+# Find unnecessary type conversions
+unconvert:
+ go tool unconvert -apply ./...
\ No newline at end of file
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/README.md b/vendor/github.com/RoaringBitmap/roaring/v2/README.md
index b7e9684aff..eb99b82668 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/README.md
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/README.md
@@ -25,6 +25,7 @@ Roaring bitmaps are used by several major systems such as [Apache Lucene][lucene
[pinot]: http://github.com/linkedin/pinot/wiki
[vsts]: https://www.visualstudio.com/team-services/
[atlas]: https://github.com/Netflix/atlas
+[quanta]: https://github.com/disney/quanta
Roaring bitmaps are found to work well in many important applications:
@@ -44,6 +45,10 @@ The ``roaring`` Go library is used by
* [trident](https://github.com/NetApp/trident)
* [Husky](https://www.datadoghq.com/blog/engineering/introducing-husky/)
* [FrostDB](https://github.com/polarsignals/frostdb)
+* [Disney Quanta](https://github.com/disney/quanta)
+
+
+
This library is used in production in several systems, it is part of the [Awesome Go collection](https://awesome-go.com).
@@ -370,7 +375,7 @@ go get github.com/RoaringBitmap/real-roaring-datasets
BENCH_REAL_DATA=1 go test -bench BenchmarkRealData -run -
```
-### Iterative use
+### Interactive use
You can use roaring with gore:
@@ -414,4 +419,14 @@ The two versions were written independently.
### Mailing list/discussion group
-https://groups.google.com/forum/#!forum/roaring-bitmaps
+https://groups.google.com/g/roaring-bitmaps
+
+## Stars
+
+
+[](https://www.star-history.com/#RoaringBitmap/roaring&Date)
+
+### Further reading
+
+
Mastering Programming: From Testing to Performance in Go
+
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/arraycontainer.go b/vendor/github.com/RoaringBitmap/roaring/v2/arraycontainer.go
index 2e75c5ad4b..784894d820 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/arraycontainer.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/arraycontainer.go
@@ -11,6 +11,7 @@ type arrayContainer struct {
var (
ErrArrayIncorrectSort = errors.New("incorrectly sorted array")
+ ErrEmptyArray = errors.New("empty array")
ErrArrayInvalidSize = errors.New("invalid array size")
)
@@ -61,6 +62,10 @@ func (ac *arrayContainer) getManyIterator() manyIterable {
return &shortIterator{ac.content, 0}
}
+func (ac *arrayContainer) getUnsetIterator() shortPeekable {
+ return newArrayContainerUnsetIterator(ac.content)
+}
+
func (ac *arrayContainer) minimum() uint16 {
return ac.content[0] // assume not empty
}
@@ -417,8 +422,10 @@ func (ac *arrayContainer) iorArray(value2 *arrayContainer) container {
func (ac *arrayContainer) iorBitmap(bc2 *bitmapContainer) container {
bc1 := ac.toBitmapContainer()
bc1.iorBitmap(bc2)
- *ac = *newArrayContainerFromBitmap(bc1)
- return ac
+ // DO NOT DO THIS:
+ // *ac = *newArrayContainerFromBitmap(bc1)
+ // This will create gigantic array containers in the case of repeated calls to iorBitmap.
+ return bc1
}
func (ac *arrayContainer) iorRun16(rc *runContainer16) container {
@@ -621,6 +628,30 @@ func (ac *arrayContainer) xor(a container) container {
panic("unsupported container type")
}
+func (ac *arrayContainer) ixor(a container) container {
+ switch x := a.(type) {
+ case *arrayContainer:
+ return ac.ixorArray(x)
+ case *bitmapContainer:
+ return ac.ixorBitmap(x)
+ case *runContainer16:
+ return ac.ixorRun16(x)
+ }
+ panic("unsupported container type")
+}
+
+func (ac *arrayContainer) ixorArray(value2 *arrayContainer) container {
+ return ac.xorArray(value2)
+}
+
+func (ac *arrayContainer) ixorBitmap(value2 *bitmapContainer) container {
+ return value2.ixor(ac)
+}
+
+func (ac *arrayContainer) ixorRun16(value2 *runContainer16) container {
+ return value2.ixor(ac)
+}
+
func (ac *arrayContainer) xorArray(value2 *arrayContainer) container {
value1 := ac
totalCardinality := value1.getCardinality() + value2.getCardinality()
@@ -962,12 +993,12 @@ func (ac *arrayContainer) resetTo(a container) {
x.fillArray(ac.content)
case *runContainer16:
- card := int(x.getCardinality())
+ card := x.getCardinality()
ac.realloc(card)
cur := 0
for _, r := range x.iv {
- for val := r.start; val <= r.last(); val++ {
- ac.content[cur] = val
+ for val := int(r.start); val <= int(r.last()); val++ {
+ ac.content[cur] = uint16(val)
cur++
}
}
@@ -1289,7 +1320,7 @@ func (ac *arrayContainer) validate() error {
cardinality := ac.getCardinality()
if cardinality <= 0 {
- return ErrArrayInvalidSize
+ return ErrEmptyArray
}
if cardinality > arrayDefaultMaxSize {
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/bitmapcontainer.go b/vendor/github.com/RoaringBitmap/roaring/v2/bitmapcontainer.go
index 10bc0f1c7c..9972963451 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/bitmapcontainer.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/bitmapcontainer.go
@@ -262,6 +262,39 @@ func (bc *bitmapContainer) getManyIterator() manyIterable {
return newBitmapContainerManyIterator(bc)
}
+type bitmapContainerUnsetIterator struct {
+ ptr *bitmapContainer
+ i int
+}
+
+func (bcui *bitmapContainerUnsetIterator) next() uint16 {
+ j := bcui.i
+ bcui.i = bcui.ptr.NextUnsetBit(uint(bcui.i) + 1)
+ return uint16(j)
+}
+
+func (bcui *bitmapContainerUnsetIterator) hasNext() bool {
+ return bcui.i >= 0 && bcui.i < 65536
+}
+
+func (bcui *bitmapContainerUnsetIterator) peekNext() uint16 {
+ return uint16(bcui.i)
+}
+
+func (bcui *bitmapContainerUnsetIterator) advanceIfNeeded(minval uint16) {
+ if bcui.hasNext() && bcui.peekNext() < minval {
+ bcui.i = bcui.ptr.NextUnsetBit(uint(minval))
+ }
+}
+
+func newBitmapContainerUnsetIterator(a *bitmapContainer) *bitmapContainerUnsetIterator {
+ return &bitmapContainerUnsetIterator{a, a.NextUnsetBit(0)}
+}
+
+func (bc *bitmapContainer) getUnsetIterator() shortPeekable {
+ return newBitmapContainerUnsetIterator(bc)
+}
+
func (bc *bitmapContainer) getSizeInBytes() int {
return len(bc.bitmap) * 8
}
@@ -882,6 +915,43 @@ func (bc *bitmapContainer) iandBitmap(value2 *bitmapContainer) container {
return bc
}
+func (bc *bitmapContainer) ixor(a container) container {
+ switch x := a.(type) {
+ case *arrayContainer:
+ return bc.ixorArray(x)
+ case *bitmapContainer:
+ return bc.ixorBitmap(x)
+ case *runContainer16:
+ return bc.ixorRun16(x)
+ }
+ panic("unsupported container type")
+}
+
+func (bc *bitmapContainer) ixorArray(value2 *arrayContainer) container {
+ vbc := value2.toBitmapContainer()
+ return bc.ixorBitmap(vbc)
+}
+
+func (bc *bitmapContainer) ixorRun16(value2 *runContainer16) container {
+ rcb := value2.toBitmapContainer()
+ return bc.ixorBitmap(rcb)
+}
+
+func (bc *bitmapContainer) ixorBitmap(value2 *bitmapContainer) container {
+ newCardinality := int(popcntXorSlice(bc.bitmap, value2.bitmap))
+ if newCardinality > arrayDefaultMaxSize {
+ for k := 0; k < len(bc.bitmap); k++ {
+ bc.bitmap[k] = bc.bitmap[k] ^ value2.bitmap[k]
+ }
+ bc.cardinality = newCardinality
+ return bc
+ }
+ ac := newArrayContainerSize(newCardinality)
+ fillArrayXOR(ac.content, bc.bitmap, value2.bitmap)
+ ac.content = ac.content[:newCardinality]
+ return ac
+}
+
func (bc *bitmapContainer) andNot(a container) container {
switch x := a.(type) {
case *arrayContainer:
@@ -1100,7 +1170,7 @@ func (bc *bitmapContainer) NextSetBit(i uint) int {
return -1
}
w := bc.bitmap[x]
- w = w >> uint(i%64)
+ w = w >> (i % 64)
if w != 0 {
return int(i) + countTrailingZeros(w)
}
@@ -1113,6 +1183,29 @@ func (bc *bitmapContainer) NextSetBit(i uint) int {
return -1
}
+func (bc *bitmapContainer) NextUnsetBit(i uint) int {
+ var (
+ x = i / 64
+ length = uint(len(bc.bitmap))
+ )
+ if x >= length {
+ return int(i)
+ }
+ w := bc.bitmap[x]
+ w = w >> (i % 64)
+ w = ^w
+ if w != 0 {
+ return int(i) + countTrailingZeros(w)
+ }
+ x++
+ for ; x < length; x++ {
+ if bc.bitmap[x] != 0xFFFFFFFFFFFFFFFF {
+ return int(x*64) + countTrailingZeros(^bc.bitmap[x])
+ }
+ }
+ return int(length * 64)
+}
+
// PrevSetBit returns the previous set bit e.g the previous int packed into the bitmaparray
func (bc *bitmapContainer) PrevSetBit(i int) int {
if i < 0 {
@@ -1136,7 +1229,7 @@ func (bc *bitmapContainer) uPrevSetBit(i uint) int {
b := i % 64
- w = w << uint(63-b)
+ w = w << (63 - b)
if w != 0 {
return int(i) - countLeadingZeros(w)
}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/iter.go b/vendor/github.com/RoaringBitmap/roaring/v2/iter.go
new file mode 100644
index 0000000000..1c43379ee2
--- /dev/null
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/iter.go
@@ -0,0 +1,44 @@
+package roaring
+
+import "iter"
+
+// Values returns an iterator that yields the elements of the bitmap in
+// increasing order. Starting with Go 1.23, users can use a for loop to iterate
+// over it.
+func Values(b *Bitmap) iter.Seq[uint32] {
+ return func(yield func(uint32) bool) {
+ it := b.Iterator()
+ for it.HasNext() {
+ if !yield(it.Next()) {
+ return
+ }
+ }
+ }
+}
+
+// Backward returns an iterator that yields the elements of the bitmap in
+// decreasing order. Starting with Go 1.23, users can use a for loop to iterate
+// over it.
+func Backward(b *Bitmap) iter.Seq[uint32] {
+ return func(yield func(uint32) bool) {
+ it := b.ReverseIterator()
+ for it.HasNext() {
+ if !yield(it.Next()) {
+ return
+ }
+ }
+ }
+}
+
+// Unset creates an iterator that yields values in the range [min, max] that are NOT contained in the bitmap.
+// The iterator becomes invalid if the bitmap is modified (e.g., with Add or Remove).
+func Unset(b *Bitmap, min, max uint32) iter.Seq[uint32] {
+ return func(yield func(uint32) bool) {
+ it := b.UnsetIterator(uint64(min), uint64(max)+1)
+ for it.HasNext() {
+ if !yield(it.Next()) {
+ return
+ }
+ }
+ }
+}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/parallel.go b/vendor/github.com/RoaringBitmap/roaring/v2/parallel.go
index 9208e3e380..01ba856f8a 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/parallel.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/parallel.go
@@ -370,23 +370,23 @@ func ParOr(parallelism int, bitmaps ...*Bitmap) *Bitmap {
var chunkSize int
var chunkCount int
- if parallelism*4 > int(keyRange) {
+ if parallelism*4 > keyRange {
chunkSize = 1
- chunkCount = int(keyRange)
+ chunkCount = keyRange
} else {
chunkCount = parallelism * 4
- chunkSize = (int(keyRange) + chunkCount - 1) / chunkCount
+ chunkSize = (keyRange + chunkCount - 1) / chunkCount
}
- if chunkCount*chunkSize < int(keyRange) {
+ if chunkCount*chunkSize < keyRange {
// it's fine to panic to indicate an implementation error
panic(fmt.Sprintf("invariant check failed: chunkCount * chunkSize < keyRange, %d * %d < %d", chunkCount, chunkSize, keyRange))
}
chunks := make([]*roaringArray, chunkCount)
- chunkSpecChan := make(chan parChunkSpec, minOfInt(maxOfInt(64, 2*parallelism), int(chunkCount)))
- chunkChan := make(chan parChunk, minOfInt(32, int(chunkCount)))
+ chunkSpecChan := make(chan parChunkSpec, minOfInt(maxOfInt(64, 2*parallelism), chunkCount))
+ chunkChan := make(chan parChunk, minOfInt(32, chunkCount))
orFunc := func() {
for spec := range chunkSpecChan {
@@ -412,7 +412,7 @@ func ParOr(parallelism int, bitmaps ...*Bitmap) *Bitmap {
spec := parChunkSpec{
start: uint16(int(lKey) + i*chunkSize),
end: uint16(minOfInt(int(lKey)+(i+1)*chunkSize-1, int(hKey))),
- idx: int(i),
+ idx: i,
}
chunkSpecChan <- spec
}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaring.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaring.go
index 9972a51e25..e9fdfe8582 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/roaring.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaring.go
@@ -68,10 +68,10 @@ func (rb *Bitmap) DenseSize() uint64 {
maximum := 1 + uint64(rb.Maximum())
if maximum > (capacity - wordSize + 1) {
- return uint64(capacity >> log2WordSize)
+ return capacity >> log2WordSize
}
- return uint64((maximum + (wordSize - 1)) >> log2WordSize)
+ return (maximum + (wordSize - 1)) >> log2WordSize
}
// ToDense returns a slice of uint64s representing the bitmap as a dense bitmap.
@@ -421,6 +421,11 @@ func FromBitSet(bitset *bitset.BitSet) *Bitmap {
// ToArray creates a new slice containing all of the integers stored in the Bitmap in sorted order
func (rb *Bitmap) ToArray() []uint32 {
array := make([]uint32, rb.GetCardinality())
+ ar := rb.toArray(&array)
+ return *ar
+}
+
+func (rb *Bitmap) toArray(array *[]uint32) *[]uint32 {
pos := 0
pos2 := 0
@@ -428,11 +433,18 @@ func (rb *Bitmap) ToArray() []uint32 {
hs := uint32(rb.highlowcontainer.getKeyAtIndex(pos)) << 16
c := rb.highlowcontainer.getContainerAtIndex(pos)
pos++
- pos2 = c.fillLeastSignificant16bits(array, pos2, hs)
+ pos2 = c.fillLeastSignificant16bits(*array, pos2, hs)
}
return array
}
+// ToExistingArray stores all of the integers stored in the Bitmap in sorted order in the
+// slice that is given to ToExistingArray. It is the callers duty to make sure the slice
+// has the right size.
+func (rb *Bitmap) ToExistingArray(array *[]uint32) *[]uint32 {
+ return rb.toArray(array)
+}
+
// GetSizeInBytes estimates the memory usage of the Bitmap. Note that this
// might differ slightly from the amount of bytes required for persistent storage
func (rb *Bitmap) GetSizeInBytes() uint64 {
@@ -599,7 +611,7 @@ func (ii *intReverseIterator) init() {
ii.shortIter = reverseIterator{t.content, len(t.content) - 1}
ii.iter = &ii.shortIter
case *runContainer16:
- index := int(len(t.iv)) - 1
+ index := len(t.iv) - 1
pos := uint16(0)
if index >= 0 {
@@ -730,6 +742,182 @@ func (ii *manyIntIterator) Initialize(a *Bitmap) {
ii.init()
}
+type unsetIterator struct {
+ containerIndex int
+ nextKey int
+ hs uint32
+ iter shortPeekable
+ highlowcontainer *roaringArray
+
+ arrayUnsetIter arrayContainerUnsetIterator
+ runUnsetIter runUnsetIterator16
+ bitmapUnsetIter bitmapContainerUnsetIterator
+ emptyContainerVal uint16
+
+ start, end uint64
+}
+
+// HasNext returns true if there are more integers to iterate over
+func (iui *unsetIterator) HasNext() bool {
+ // Skip containers that have no unset bits in our range
+ for iui.nextKey < 65536 && uint64(iui.nextKey)<<16 < iui.end {
+ if iui.iter == nil {
+ // We're in an empty container gap, which has unset bits
+ if uint64(iui.nextKey)<<16|uint64(iui.emptyContainerVal) < iui.end {
+ return true
+ }
+ // Move to next container
+ iui.nextKey++
+ iui.containerIndex++
+ iui.init()
+ continue
+ }
+ if iui.iter.hasNext() {
+ // Check if next value is within range
+ nextVal := (uint64(iui.nextKey) << 16) | uint64(iui.iter.peekNext())
+ if nextVal < iui.end {
+ return true
+ }
+ }
+ // Current container has no more unset bits in range, move to next
+ iui.nextKey++
+ iui.containerIndex++
+ iui.init()
+ }
+ return false
+}
+
+func (iui *unsetIterator) init() {
+ // Check if we've gone past the end range
+ if uint64(iui.nextKey)<<16 >= iui.end {
+ iui.iter = nil
+ return
+ }
+
+ // Check if we're in an empty container gap
+ if iui.containerIndex >= iui.highlowcontainer.size() ||
+ iui.highlowcontainer.getKeyAtIndex(iui.containerIndex) > uint16(iui.nextKey) {
+ // We're in a gap - iterate through empty container
+ iui.emptyContainerVal = 0
+ // If this container overlaps with start, advance to start
+ if uint64(iui.nextKey)<<16 < iui.start && iui.start < uint64(iui.nextKey+1)<<16 {
+ iui.emptyContainerVal = uint16(iui.start)
+ }
+ iui.iter = nil
+ return
+ }
+
+ // We're in an actual container
+ iui.hs = uint32(iui.nextKey) << 16
+ c := iui.highlowcontainer.getContainerAtIndex(iui.containerIndex)
+ switch t := c.(type) {
+ case *arrayContainer:
+ iui.arrayUnsetIter = *newArrayContainerUnsetIterator(t.content)
+ iui.iter = &iui.arrayUnsetIter
+ case *runContainer16:
+ iui.runUnsetIter = *t.newRunUnsetIterator16()
+ iui.iter = &iui.runUnsetIter
+ case *bitmapContainer:
+ iui.bitmapUnsetIter = *newBitmapContainerUnsetIterator(t)
+ iui.iter = &iui.bitmapUnsetIter
+ }
+
+ // If this container overlaps with start, advance to the low bits of start
+ if uint64(iui.nextKey)<<16 < iui.start && iui.start < uint64(iui.nextKey+1)<<16 {
+ iui.iter.advanceIfNeeded(uint16(iui.start))
+ }
+}
+
+// Next returns the next integer
+func (iui *unsetIterator) Next() uint32 {
+ if iui.iter == nil {
+ // We're in an empty container gap
+ x := (uint32(iui.nextKey) << 16) | uint32(iui.emptyContainerVal)
+ iui.emptyContainerVal++
+ if iui.emptyContainerVal == 0 || uint64(iui.nextKey)<<16|uint64(iui.emptyContainerVal) >= iui.end {
+ // Wrapped around or reached end, move to next container
+ iui.nextKey++
+ iui.init()
+ }
+ return x
+ }
+
+ x := uint32(iui.iter.next()) | iui.hs
+ if !iui.iter.hasNext() || uint64(iui.nextKey)<<16|uint64(iui.iter.peekNext()) >= iui.end {
+ iui.nextKey++
+ iui.containerIndex++
+ iui.init()
+ }
+ return x
+}
+
+// PeekNext peeks the next value without advancing the iterator
+func (iui *unsetIterator) PeekNext() uint32 {
+ if !iui.HasNext() {
+ panic("PeekNext() called when HasNext() returns false")
+ }
+ if iui.iter == nil {
+ return (uint32(iui.nextKey) << 16) | uint32(iui.emptyContainerVal)
+ }
+ return uint32(iui.iter.peekNext()&maxLowBit) | iui.hs
+}
+
+// AdvanceIfNeeded advances as long as the next value is smaller than minval
+func (iui *unsetIterator) AdvanceIfNeeded(minval uint32) {
+ targetKey := int(minval >> 16)
+
+ for iui.HasNext() && iui.nextKey < targetKey {
+ iui.nextKey++
+ // Find the next container that matches or exceeds nextKey
+ for iui.containerIndex < iui.highlowcontainer.size() &&
+ int(iui.highlowcontainer.getKeyAtIndex(iui.containerIndex)) < iui.nextKey {
+ iui.containerIndex++
+ }
+ iui.init()
+ }
+
+ if iui.HasNext() && iui.nextKey == targetKey {
+ if iui.iter != nil {
+ iui.iter.advanceIfNeeded(lowbits(minval))
+ if !iui.iter.hasNext() || uint64(iui.nextKey)<<16|uint64(iui.iter.peekNext()) >= iui.end {
+ iui.nextKey++
+ iui.containerIndex++
+ iui.init()
+ }
+ } else {
+ lowVal := lowbits(minval)
+ if iui.emptyContainerVal < lowVal {
+ iui.emptyContainerVal = lowVal
+ }
+ if uint64(iui.nextKey)<<16|uint64(iui.emptyContainerVal) >= iui.end {
+ iui.nextKey++
+ iui.containerIndex++
+ iui.init()
+ }
+ }
+ }
+}
+
+// Initialize configures the unset iterator to iterate over values in [start, end) that are not in the bitmap
+func (iui *unsetIterator) Initialize(a *Bitmap, start, end uint64) {
+ if end > 0x100000000 {
+ panic("end > 0x100000000")
+ }
+ iui.start = start
+ iui.end = end
+ iui.containerIndex = 0
+ iui.nextKey = int(start >> 16)
+ iui.highlowcontainer = &a.highlowcontainer
+
+ // Find the first container that matches or exceeds the start key
+ for iui.containerIndex < iui.highlowcontainer.size() &&
+ int(iui.highlowcontainer.getKeyAtIndex(iui.containerIndex)) < iui.nextKey {
+ iui.containerIndex++
+ }
+
+ iui.init()
+}
+
// String creates a string representation of the Bitmap
func (rb *Bitmap) String() string {
// inspired by https://github.com/fzandona/goroar/
@@ -812,6 +1000,14 @@ func (rb *Bitmap) ManyIterator() ManyIntIterable {
return p
}
+// UnsetIterator creates a new IntPeekable to iterate over values in the range [start, end) that are NOT contained in the bitmap.
+// The iterator becomes invalid if the bitmap is modified (e.g., with Add or Remove).
+func (rb *Bitmap) UnsetIterator(start, end uint64) IntPeekable {
+ p := new(unsetIterator)
+ p.Initialize(rb, start, end)
+ return p
+}
+
// Clone creates a copy of the Bitmap
func (rb *Bitmap) Clone() *Bitmap {
ptr := new(Bitmap)
@@ -1290,6 +1486,10 @@ main:
// Xor computes the symmetric difference between two bitmaps and stores the result in the current bitmap
func (rb *Bitmap) Xor(x2 *Bitmap) {
+ if rb == x2 {
+ rb.Clear()
+ return
+ }
pos1 := 0
pos2 := 0
length1 := rb.highlowcontainer.size()
@@ -1304,14 +1504,12 @@ func (rb *Bitmap) Xor(x2 *Bitmap) {
break
}
} else if s1 > s2 {
- c := x2.highlowcontainer.getWritableContainerAtIndex(pos2)
- rb.highlowcontainer.insertNewKeyValueAt(pos1, x2.highlowcontainer.getKeyAtIndex(pos2), c)
+ rb.highlowcontainer.insertNewKeyValueAt(pos1, x2.highlowcontainer.getKeyAtIndex(pos2), x2.highlowcontainer.getContainerAtIndex(pos2).clone())
length1++
pos1++
pos2++
} else {
- // TODO: couple be computed in-place for reduced memory usage
- c := rb.highlowcontainer.getContainerAtIndex(pos1).xor(x2.highlowcontainer.getContainerAtIndex(pos2))
+ c := rb.highlowcontainer.getWritableContainerAtIndex(pos1).ixor(x2.highlowcontainer.getContainerAtIndex(pos2))
if !c.isEmpty() {
rb.highlowcontainer.setContainerAtIndex(pos1, c)
pos1++
@@ -1358,7 +1556,8 @@ main:
}
s2 = x2.highlowcontainer.getKeyAtIndex(pos2)
} else {
- rb.highlowcontainer.replaceKeyAndContainerAtIndex(pos1, s1, rb.highlowcontainer.getUnionedWritableContainer(pos1, x2.highlowcontainer.getContainerAtIndex(pos2)), false)
+ newcont := rb.highlowcontainer.getUnionedWritableContainer(pos1, x2.highlowcontainer.getContainerAtIndex(pos2))
+ rb.highlowcontainer.replaceKeyAndContainerAtIndex(pos1, s1, newcont, false)
pos1++
pos2++
if (pos1 == length1) || (pos2 == length2) {
@@ -1376,6 +1575,10 @@ main:
// AndNot computes the difference between two bitmaps and stores the result in the current bitmap
func (rb *Bitmap) AndNot(x2 *Bitmap) {
+ if rb == x2 {
+ rb.Clear()
+ return
+ }
pos1 := 0
pos2 := 0
intersectionsize := 0
@@ -1465,7 +1668,6 @@ main:
}
s2 = x2.highlowcontainer.getKeyAtIndex(pos2)
} else {
-
answer.highlowcontainer.appendContainer(s1, x1.highlowcontainer.getContainerAtIndex(pos1).or(x2.highlowcontainer.getContainerAtIndex(pos2)), false)
pos1++
pos2++
@@ -1504,6 +1706,7 @@ main:
if !C.isEmpty() {
answer.highlowcontainer.appendContainer(s1, C, false)
}
+
pos1++
pos2++
if (pos1 == length1) || (pos2 == length2) {
@@ -1531,6 +1734,9 @@ main:
// Xor computes the symmetric difference between two bitmaps and returns the result
func Xor(x1, x2 *Bitmap) *Bitmap {
+ if x1 == x2 {
+ return NewBitmap()
+ }
answer := NewBitmap()
pos1 := 0
pos2 := 0
@@ -1568,6 +1774,9 @@ func Xor(x1, x2 *Bitmap) *Bitmap {
// AndNot computes the difference between two bitmaps and returns the result
func AndNot(x1, x2 *Bitmap) *Bitmap {
+ if x1 == x2 {
+ return NewBitmap()
+ }
answer := NewBitmap()
pos1 := 0
pos2 := 0
@@ -1669,11 +1878,11 @@ func (rb *Bitmap) Flip(rangeStart, rangeEnd uint64) {
for hb := hbStart; hb <= hbLast; hb++ {
var containerStart uint32
if hb == hbStart {
- containerStart = uint32(lbStart)
+ containerStart = lbStart
}
containerLast := max
if hb == hbLast {
- containerLast = uint32(lbLast)
+ containerLast = lbLast
}
i := rb.highlowcontainer.getIndex(uint16(hb))
@@ -1829,11 +2038,11 @@ func Flip(bm *Bitmap, rangeStart, rangeEnd uint64) *Bitmap {
for hb := hbStart; hb <= hbLast; hb++ {
var containerStart uint32
if hb == hbStart {
- containerStart = uint32(lbStart)
+ containerStart = lbStart
}
containerLast := max
if hb == hbLast {
- containerLast = uint32(lbLast)
+ containerLast = lbLast
}
i := bm.highlowcontainer.getIndex(uint16(hb))
@@ -1931,8 +2140,8 @@ func (rb *Bitmap) PreviousValue(target uint32) int64 {
return -1
}
- originalKey := highbits(uint32(target))
- query := lowbits(uint32(target))
+ originalKey := highbits(target)
+ query := lowbits(target)
var prevValue int64
prevValue = -1
containerIndex := rb.highlowcontainer.advanceUntil(originalKey, -1)
@@ -2133,6 +2342,34 @@ func (rb *Bitmap) Stats() Statistics {
return stats
}
+// Describe prints a description of the bitmap's containers to stdout
+func (rb *Bitmap) Describe() {
+ fmt.Printf("Bitmap with %d containers:\n", len(rb.highlowcontainer.containers))
+ for i, c := range rb.highlowcontainer.containers {
+ key := rb.highlowcontainer.keys[i]
+ shared := ""
+ if rb.highlowcontainer.needCopyOnWrite[i] {
+ shared = " (shared)"
+ }
+ switch c.(type) {
+ case *arrayContainer:
+ fmt.Printf(" Container %d (key %d): array, cardinality %d%s\n", i, key, c.getCardinality(), shared)
+ case *bitmapContainer:
+ fmt.Printf(" Container %d (key %d): bitmap, cardinality %d%s\n", i, key, c.getCardinality(), shared)
+ case *runContainer16:
+ fmt.Printf(" Container %d (key %d): run, cardinality %d%s\n", i, key, c.getCardinality(), shared)
+ default:
+ fmt.Printf(" Container %d (key %d): unknown type, cardinality %d%s\n", i, key, c.getCardinality(), shared)
+ }
+ }
+ valid := rb.Validate()
+ if valid != nil {
+ fmt.Printf(" Bitmap is INVALID: %v\n", valid)
+ } else {
+ fmt.Printf(" Bitmap is valid\n")
+ }
+}
+
// Validate checks if the bitmap is internally consistent.
// You may call it after deserialization to check that the bitmap is valid.
// This function returns an error if the bitmap is invalid, nil otherwise.
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/bsi64.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/bsi64.go
index 46dbe12103..5d6019db27 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/bsi64.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/bsi64.go
@@ -66,7 +66,7 @@ func (b *BSI) GetExistenceBitmap() *Bitmap {
// ValueExists tests whether the value exists.
func (b *BSI) ValueExists(columnID uint64) bool {
- return b.eBM.Contains(uint64(columnID))
+ return b.eBM.Contains(columnID)
}
// GetCardinality returns a count of unique column IDs for which a value has been set.
@@ -115,11 +115,37 @@ func (b *BSI) SetBigValue(columnID uint64, value *big.Int) {
b.eBM.Add(columnID)
}
+func (b *BSI) SetBigMany(foundSet *Bitmap, value *big.Int) {
+ // If max/min values are set to zero then automatically determine bit array size
+ if b.MaxValue == 0 && b.MinValue == 0 {
+ minBits := value.BitLen() + 1
+ if minBits == 1 {
+ minBits = 2
+ }
+ for len(b.bA) < minBits {
+ b.bA = append(b.bA, Bitmap{})
+ }
+ }
+ for i := b.BitCount(); i >= 0; i-- {
+ if value.Bit(i) == 0 {
+ b.bA[i].AndNot(foundSet)
+ } else {
+ b.bA[i].Or(foundSet)
+ }
+ }
+ b.eBM.Or(foundSet)
+}
+
// SetValue sets a value for a given columnID.
func (b *BSI) SetValue(columnID uint64, value int64) {
b.SetBigValue(columnID, big.NewInt(value))
}
+// SetMany sets a value for all columns in foundSet
+func (b *BSI) SetMany(foundSet *Bitmap, value int64) {
+ b.SetBigMany(foundSet, big.NewInt(value))
+}
+
// GetValue gets the value at the column ID. Second param will be false for non-existent values.
func (b *BSI) GetValue(columnID uint64) (value int64, exists bool) {
bv, exists := b.GetBigValue(columnID)
@@ -722,7 +748,7 @@ func transpose(e *task, batch []uint64, resultsChan chan *Bitmap, wg *sync.WaitG
results.RunOptimize()
}
for _, cID := range batch {
- if value, ok := e.bsi.GetValue(uint64(cID)); ok {
+ if value, ok := e.bsi.GetValue(cID); ok {
results.Add(uint64(value))
}
}
@@ -738,7 +764,7 @@ func (b *BSI) ParOr(parallelism int, bsis ...*BSI) {
bits := len(b.bA)
for i := 0; i < len(bsis); i++ {
if len(bsis[i].bA) > bits {
- bits = len(bsis[i].bA )
+ bits = len(bsis[i].bA)
}
}
@@ -931,7 +957,7 @@ func batchEqual(e *task, batch []uint64, resultsChan chan *Bitmap,
for i := 0; i < len(batch); i++ {
cID := batch[i]
- if value, ok := e.bsi.GetBigValue(uint64(cID)); ok {
+ if value, ok := e.bsi.GetBigValue(cID); ok {
if _, yes := e.values[string(value.Bytes())]; yes {
results.Add(cID)
}
@@ -942,11 +968,7 @@ func batchEqual(e *task, batch []uint64, resultsChan chan *Bitmap,
// ClearBits cleared the bits that exist in the target if they are also in the found set.
func ClearBits(foundSet, target *Bitmap) {
- iter := foundSet.Iterator()
- for iter.HasNext() {
- cID := iter.Next()
- target.Remove(cID)
- }
+ target.AndNot(foundSet)
}
// ClearValues removes the values found in foundSet
@@ -956,13 +978,13 @@ func (b *BSI) ClearValues(foundSet *Bitmap) {
wg.Add(1)
go func() {
defer wg.Done()
- ClearBits(foundSet, &b.eBM)
+ b.eBM.AndNot(foundSet)
}()
for i := 0; i < b.BitCount(); i++ {
wg.Add(1)
go func(j int) {
defer wg.Done()
- ClearBits(foundSet, &b.bA[j])
+ b.bA[j].AndNot(foundSet)
}(i)
}
wg.Wait()
@@ -1044,7 +1066,7 @@ func transposeWithCounts(input *BSI, filterSet *Bitmap, batch []uint64, resultsC
results.RunOptimize()
}
for _, cID := range batch {
- if value, ok := input.GetValue(uint64(cID)); ok {
+ if value, ok := input.GetValue(cID); ok {
if !filterSet.Contains(uint64(value)) {
continue
}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/iter.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/iter.go
new file mode 100644
index 0000000000..ce6c24dbd3
--- /dev/null
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/iter.go
@@ -0,0 +1,31 @@
+package roaring64
+
+import "iter"
+
+// Values returns an iterator that yields the elements of the bitmap in
+// increasing order. Starting with Go 1.23, users can use a for loop to iterate
+// over it.
+func Values(b *Bitmap) iter.Seq[uint64] {
+ return func(yield func(uint64) bool) {
+ it := b.Iterator()
+ for it.HasNext() {
+ if !yield(it.Next()) {
+ return
+ }
+ }
+ }
+}
+
+// Backward returns an iterator that yields the elements of the bitmap in
+// decreasing order. Starting with Go 1.23, users can use a for loop to iterate
+// over it.
+func Backward(b *Bitmap) iter.Seq[uint64] {
+ return func(yield func(uint64) bool) {
+ it := b.ReverseIterator()
+ for it.HasNext() {
+ if !yield(it.Next()) {
+ return
+ }
+ }
+ }
+}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/parallel64.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/parallel64.go
index 5dadc8deac..7bea3c6886 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/parallel64.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/parallel64.go
@@ -39,9 +39,13 @@ func ParOr(parallelism int, bitmaps ...*Bitmap) *Bitmap {
// on some systems, would block indefinitely.
keyRange := uint64(hKey) - uint64(lKey) + 1
if keyRange == 1 {
- // revert to FastOr. Since the key range is 0
- // no container-level aggregation parallelism is achievable
- return FastOr(bitmaps...)
+ // All bitmaps have the same key,
+ // we can merge the 32-bit roaring bitmaps in parallel
+ var bms32s = make([]*roaring.Bitmap, 0, len(bitmaps))
+ for _, b := range bitmaps {
+ bms32s = append(bms32s, b.highlowcontainer.containers...)
+ }
+ return roaring32AsRoaring64(roaring.ParOr(parallelism, bms32s...), lKey)
}
if parallelism == 0 {
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/roaring64.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/roaring64.go
index ebea5ffcb6..8589925ee4 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/roaring64.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaring64/roaring64.go
@@ -73,7 +73,7 @@ func (rb *Bitmap) WriteTo(stream io.Writer) (int64, error) {
return n, err
}
written, err := c.WriteTo(stream)
- n += int64(written)
+ n += written
if err != nil {
return n, err
}
@@ -119,7 +119,7 @@ func (rb *Bitmap) FromUnsafeBytes(data []byte) (p int64, err error) {
n, err := rb.highlowcontainer.containers[i].ReadFrom(stream)
if n == 0 || err != nil {
- return int64(n), fmt.Errorf("Could not deserialize bitmap for key #%d: %s", i, err)
+ return n, fmt.Errorf("Could not deserialize bitmap for key #%d: %s", i, err)
}
}
@@ -167,9 +167,9 @@ func (rb *Bitmap) ReadFrom(stream io.Reader) (p int64, err error) {
n, err := rb.highlowcontainer.containers[i].ReadFrom(stream)
if n == 0 || err != nil {
- return int64(n), fmt.Errorf("Could not deserialize bitmap for key #%d: %s", i, err)
+ return n, fmt.Errorf("Could not deserialize bitmap for key #%d: %s", i, err)
}
- p += int64(n)
+ p += n
}
return p, nil
}
@@ -249,7 +249,7 @@ func (rb *Bitmap) String() string {
counter := 0
if i.HasNext() {
counter = counter + 1
- buffer.WriteString(strconv.FormatUint(uint64(i.Next()), 10))
+ buffer.WriteString(strconv.FormatUint(i.Next(), 10))
}
for i.HasNext() {
buffer.WriteString(",")
@@ -259,7 +259,7 @@ func (rb *Bitmap) String() string {
buffer.WriteString("...")
break
}
- buffer.WriteString(strconv.FormatUint(uint64(i.Next()), 10))
+ buffer.WriteString(strconv.FormatUint(i.Next(), 10))
}
buffer.WriteString("}")
return buffer.String()
@@ -346,7 +346,7 @@ func (rb *Bitmap) CheckedAdd(x uint64) bool {
return true
}
-// AddInt adds the integer x to the bitmap (convenience method: the parameter is casted to uint32 and we call Add)
+// AddInt adds the integer x to the bitmap (convenience method: the parameter is casted to uint64 and we call Add)
func (rb *Bitmap) AddInt(x int) {
rb.Add(uint64(x))
}
@@ -1248,9 +1248,13 @@ func (rb *Bitmap) Validate() error {
// Roaring32AsRoaring64 inserts a 32-bit roaring bitmap into
// a 64-bit roaring bitmap. No copy is made.
func Roaring32AsRoaring64(bm32 *roaring.Bitmap) *Bitmap {
+ return roaring32AsRoaring64(bm32, 0)
+}
+
+func roaring32AsRoaring64(bm32 *roaring.Bitmap, key uint32) *Bitmap {
rb := NewBitmap()
rb.highlowcontainer.resize(0)
- rb.highlowcontainer.keys = append(rb.highlowcontainer.keys, 0)
+ rb.highlowcontainer.keys = append(rb.highlowcontainer.keys, key)
rb.highlowcontainer.containers = append(rb.highlowcontainer.containers, bm32)
rb.highlowcontainer.needCopyOnWrite = append(rb.highlowcontainer.needCopyOnWrite, false)
return rb
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/roaringarray.go b/vendor/github.com/RoaringBitmap/roaring/v2/roaringarray.go
index 40be90a56d..f533902356 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/roaringarray.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/roaringarray.go
@@ -39,7 +39,9 @@ type container interface {
not(start, final int) container // range is [firstOfRange,lastOfRange)
inot(firstOfRange, endx int) container // i stands for inplace, range is [firstOfRange,endx)
xor(r container) container
+ ixor(r container) container // i stands for inplace
getShortIterator() shortPeekable
+ getUnsetIterator() shortPeekable
iterate(cb func(x uint16) bool) bool
getReverseIterator() shortIterable
getManyIterator() manyIterable
@@ -108,7 +110,7 @@ func rangeOfOnes(start, last int) container {
if last < 0 {
panic("rangeOfOnes called with last < 0")
}
- return newRunContainer16Range(uint16(start), uint16(last))
+ return newRunContainer16Range(uint16(start), uint16(last)).toEfficientContainer()
}
type roaringArray struct {
@@ -588,7 +590,7 @@ func (ra *roaringArray) readFrom(stream internal.ByteInput, cookieHeader ...byte
var isRunBitmap []byte
if cookie&0x0000FFFF == serialCookie {
- size = uint32(cookie>>16 + 1)
+ size = cookie>>16 + 1
// create is-run-container bitmap
isRunBitmapSize := (int(size) + 7) / 8
isRunBitmap, err = stream.Next(isRunBitmapSize)
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/runcontainer.go b/vendor/github.com/RoaringBitmap/roaring/v2/runcontainer.go
index ac9ea1b456..4731da7363 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/runcontainer.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/runcontainer.go
@@ -41,7 +41,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import (
"errors"
"fmt"
- "sort"
+ "slices"
)
// runContainer16 does run-length encoding of sets of
@@ -113,18 +113,6 @@ func (rc *runContainer16) String() string {
return `runContainer16{` + is + `}`
}
-// uint16Slice is a sort.Sort convenience method
-type uint16Slice []uint16
-
-// Len returns the length of p.
-func (p uint16Slice) Len() int { return len(p) }
-
-// Less returns p[i] < p[j]
-func (p uint16Slice) Less(i, j int) bool { return p[i] < p[j] }
-
-// Swap swaps elements i and j.
-func (p uint16Slice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
-
// addHelper helps build a runContainer16.
type addHelper16 struct {
runstart uint16
@@ -183,7 +171,7 @@ func newRunContainer16FromVals(alreadySorted bool, vals ...uint16) *runContainer
ah := addHelper16{rc: rc}
if !alreadySorted {
- sort.Sort(uint16Slice(vals))
+ slices.Sort(vals)
}
n := len(vals)
var cur, prev uint16
@@ -386,8 +374,8 @@ func (rc *runContainer16) union(b *runContainer16) *runContainer16 {
var m []interval16
- alim := int(len(rc.iv))
- blim := int(len(b.iv))
+ alim := len(rc.iv)
+ blim := len(b.iv)
var na int // next from a
var nb int // next from b
@@ -497,8 +485,8 @@ func (rc *runContainer16) unionCardinality(b *runContainer16) uint {
// call it rc for consistency with the rest of the methods.
answer := uint(0)
- alim := int(len(rc.iv))
- blim := int(len(b.iv))
+ alim := len(rc.iv)
+ blim := len(b.iv)
var na int // next from a
var nb int // next from b
@@ -617,8 +605,8 @@ func (rc *runContainer16) indexOfIntervalAtOrAfter(key int, startIndex int) int
// intersection of rc (also known as 'a') and b.
func (rc *runContainer16) intersect(b *runContainer16) *runContainer16 {
a := rc
- numa := int(len(a.iv))
- numb := int(len(b.iv))
+ numa := len(a.iv)
+ numb := len(b.iv)
res := &runContainer16{}
if numa == 0 || numb == 0 {
return res
@@ -719,8 +707,8 @@ func (rc *runContainer16) intersectCardinality(b *runContainer16) int {
answer := int(0)
a := rc
- numa := int(len(a.iv))
- numb := int(len(b.iv))
+ numa := len(a.iv)
+ numb := len(b.iv)
if numa == 0 || numb == 0 {
return 0
}
@@ -847,7 +835,7 @@ func (rc *runContainer16) numIntervals() int {
// The search space is from startIndex to endxIndex. If endxIndex is set to zero, then there
// no upper bound.
func (rc *runContainer16) searchRange(key int, startIndex int, endxIndex int) (whichInterval16 int, alreadyPresent bool, numCompares int) {
- n := int(len(rc.iv))
+ n := len(rc.iv)
if n == 0 {
return -1, false, 0
}
@@ -1045,7 +1033,7 @@ func (rc *runContainer16) Add(k uint16) (wasNew bool) {
}
wasNew = true
- n := int(len(rc.iv))
+ n := len(rc.iv)
if index == -1 {
// we may need to extend the first run
if n > 0 {
@@ -1139,8 +1127,8 @@ func (rc *runContainer16) iterate(cb func(x uint16) bool) bool {
// returns true when there is at least one more value
// available in the iteration sequence.
func (ri *runIterator16) hasNext() bool {
- return int(len(ri.rc.iv)) > ri.curIndex+1 ||
- (int(len(ri.rc.iv)) == ri.curIndex+1 && ri.rc.iv[ri.curIndex].length >= ri.curPosInIndex)
+ return len(ri.rc.iv) > ri.curIndex+1 ||
+ (len(ri.rc.iv) == ri.curIndex+1 && ri.rc.iv[ri.curIndex].length >= ri.curPosInIndex)
}
// next returns the next value in the iteration sequence.
@@ -1169,7 +1157,7 @@ func (ri *runIterator16) advanceIfNeeded(minval uint16) {
}
// interval cannot be -1 because of minval > peekNext
- interval, isPresent, _ := ri.rc.searchRange(int(minval), ri.curIndex, int(len(ri.rc.iv)))
+ interval, isPresent, _ := ri.rc.searchRange(int(minval), ri.curIndex, len(ri.rc.iv))
// if the minval is present, set the curPosIndex at the right position
if isPresent {
@@ -1193,7 +1181,7 @@ type runReverseIterator16 struct {
// newRunReverseIterator16 returns a new empty run iterator.
func (rc *runContainer16) newRunReverseIterator16() *runReverseIterator16 {
- index := int(len(rc.iv)) - 1
+ index := len(rc.iv) - 1
pos := uint16(0)
if index >= 0 {
@@ -1254,8 +1242,17 @@ func (ri *runIterator16) nextMany(hs uint32, buf []uint32) int {
// allows BCE
buf2 := buf[n : n+moreVals]
- for i := range buf2 {
- buf2[i] = base + uint32(i)
+ i := 0
+ for ; i+3 < len(buf2); i += 4 {
+ buf2[i] = base
+ buf2[i+1] = base + 1
+ buf2[i+2] = base + 2
+ buf2[i+3] = base + 3
+ base += 4
+ }
+ for ; i < len(buf2); i++ {
+ buf2[i] = base
+ base++
}
// update values
@@ -1266,7 +1263,7 @@ func (ri *runIterator16) nextMany(hs uint32, buf []uint32) int {
ri.curPosInIndex = 0
ri.curIndex++
- if ri.curIndex == int(len(ri.rc.iv)) {
+ if ri.curIndex == len(ri.rc.iv) {
break
}
} else {
@@ -1295,8 +1292,17 @@ func (ri *runIterator16) nextMany64(hs uint64, buf []uint64) int {
// allows BCE
buf2 := buf[n : n+moreVals]
- for i := range buf2 {
- buf2[i] = base + uint64(i)
+ i := 0
+ for ; i+3 < len(buf2); i += 4 {
+ buf2[i] = base
+ buf2[i+1] = base + 1
+ buf2[i+2] = base + 2
+ buf2[i+3] = base + 3
+ base += 4
+ }
+ for ; i < len(buf2); i++ {
+ buf2[i] = base
+ base++
}
// update values
@@ -1307,7 +1313,7 @@ func (ri *runIterator16) nextMany64(hs uint64, buf []uint64) int {
ri.curPosInIndex = 0
ri.curIndex++
- if ri.curIndex == int(len(ri.rc.iv)) {
+ if ri.curIndex == len(ri.rc.iv) {
break
}
} else {
@@ -1416,7 +1422,7 @@ func (rc *runContainer16) findNextIntervalThatIntersectsStartingFrom(startIndex
if w < startIndex {
// not found and comes before lower bound startIndex,
// so just use the lower bound.
- if startIndex == int(len(rc.iv)) {
+ if startIndex == len(rc.iv) {
// also this bump up means that we are done
return startIndex, true
}
@@ -1542,7 +1548,7 @@ func (iv interval16) subtractInterval(del interval16) (left []interval16, delcou
func (rc *runContainer16) isubtract(del interval16) {
origiv := make([]interval16, len(rc.iv))
copy(origiv, rc.iv)
- n := int(len(rc.iv))
+ n := len(rc.iv)
if n == 0 {
return // already done.
}
@@ -1569,8 +1575,8 @@ func (rc *runContainer16) isubtract(del interval16) {
// would overwrite values in iv b/c res0 can have len 2. so
// write to origiv instead.
lost := 1 + ilast - istart
- changeSize := int(len(res0)) - lost
- newSize := int(len(rc.iv)) + changeSize
+ changeSize := len(res0) - lost
+ newSize := len(rc.iv) + changeSize
// rc.iv = append(pre, caboose...)
// return
@@ -1578,19 +1584,19 @@ func (rc *runContainer16) isubtract(del interval16) {
if ilast != istart {
res1, _ := rc.iv[ilast].subtractInterval(del)
res0 = append(res0, res1...)
- changeSize = int(len(res0)) - lost
- newSize = int(len(rc.iv)) + changeSize
+ changeSize = len(res0) - lost
+ newSize = len(rc.iv) + changeSize
}
switch {
case changeSize < 0:
// shrink
- copy(rc.iv[istart+int(len(res0)):], rc.iv[ilast+1:])
- copy(rc.iv[istart:istart+int(len(res0))], res0)
+ copy(rc.iv[istart+len(res0):], rc.iv[ilast+1:])
+ copy(rc.iv[istart:istart+len(res0)], res0)
rc.iv = rc.iv[:newSize]
return
case changeSize == 0:
// stay the same
- copy(rc.iv[istart:istart+int(len(res0))], res0)
+ copy(rc.iv[istart:istart+len(res0)], res0)
return
default:
// changeSize > 0 is only possible when ilast == istart.
@@ -1647,7 +1653,7 @@ func (rc *runContainer16) isubtract(del interval16) {
// INVAR: ilast < n-1
lost := ilast - istart
changeSize := -lost
- newSize := int(len(rc.iv)) + changeSize
+ newSize := len(rc.iv) + changeSize
if changeSize != 0 {
copy(rc.iv[ilast+1+changeSize:], rc.iv[ilast+1:])
}
@@ -1664,8 +1670,8 @@ func (rc *runContainer16) isubtract(del interval16) {
rc.iv[istart] = res0[0]
}
lost := 1 + (ilast - istart)
- changeSize := int(len(res0)) - lost
- newSize := int(len(rc.iv)) + changeSize
+ changeSize := len(res0) - lost
+ newSize := len(rc.iv) + changeSize
if changeSize != 0 {
copy(rc.iv[ilast+1+changeSize:], rc.iv[ilast+1:])
}
@@ -1676,8 +1682,8 @@ func (rc *runContainer16) isubtract(del interval16) {
// we can only shrink or stay the same size
res1, _ := rc.iv[ilast].subtractInterval(del)
lost := ilast - istart
- changeSize := int(len(res1)) - lost
- newSize := int(len(rc.iv)) + changeSize
+ changeSize := len(res1) - lost
+ newSize := len(rc.iv) + changeSize
if changeSize != 0 {
// move the tail first to make room for res1
copy(rc.iv[ilast+1+changeSize:], rc.iv[ilast+1:])
@@ -1823,7 +1829,11 @@ func (rc *runContainer16) and(a container) container {
}
switch c := a.(type) {
case *runContainer16:
- return rc.intersect(c)
+ // Important: there is no reason to believe that the
+ // result of intersecting two run containers is itself
+ // a run container. Hence we convert to efficient container.
+ // We only use run containers when they are efficient.
+ return rc.intersect(c).toEfficientContainer()
case *arrayContainer:
return rc.andArray(c)
case *bitmapContainer:
@@ -1835,7 +1845,7 @@ func (rc *runContainer16) and(a container) container {
func (rc *runContainer16) andCardinality(a container) int {
switch c := a.(type) {
case *runContainer16:
- return int(rc.intersectCardinality(c))
+ return rc.intersectCardinality(c)
case *arrayContainer:
return rc.andArrayCardinality(c)
case *bitmapContainer:
@@ -1885,11 +1895,19 @@ func (rc *runContainer16) iand(a container) container {
}
switch c := a.(type) {
case *runContainer16:
- return rc.inplaceIntersect(c)
+ // Important: there is no reason to believe that the
+ // result of intersecting two run containers is itself
+ // a run container. Hence we convert to efficient container.
+ // We only use run containers when they are efficient.
+ return rc.inplaceIntersect(c).toEfficientContainer()
case *arrayContainer:
+ // inplace intersection with array is not supported
+ // It is likely not very useful either.
return rc.andArray(c)
case *bitmapContainer:
- return rc.iandBitmapContainer(c)
+ // inplace intersection with bitmap is not supported
+ // It is very difficult to do this inplace and likely not useful.
+ return rc.andBitmapContainer(c)
}
panic("unsupported container type")
}
@@ -1900,12 +1918,6 @@ func (rc *runContainer16) inplaceIntersect(rc2 *runContainer16) container {
return rc
}
-func (rc *runContainer16) iandBitmapContainer(bc *bitmapContainer) container {
- isect := rc.andBitmapContainer(bc)
- *rc = *newRunContainer16FromContainer(isect)
- return rc
-}
-
func (rc *runContainer16) andArray(ac *arrayContainer) container {
if len(rc.iv) == 0 {
return newArrayContainer()
@@ -1943,7 +1955,7 @@ func (rc *runContainer16) andNot(a container) container {
case *bitmapContainer:
return rc.andNotBitmap(c)
case *runContainer16:
- return rc.andNotRunContainer16(c)
+ return rc.andNotRunContainer16(c).toEfficientContainer()
}
panic("unsupported container type")
}
@@ -1974,6 +1986,61 @@ func (rc *runContainer16) getManyIterator() manyIterable {
return rc.newManyRunIterator16()
}
+type runUnsetIterator16 struct {
+ rc *runContainer16
+ curIndex int
+ nextVal int
+}
+
+func (rc *runContainer16) newRunUnsetIterator16() *runUnsetIterator16 {
+ rui := &runUnsetIterator16{rc: rc, curIndex: 0, nextVal: 0}
+ if len(rc.iv) > 0 && rc.iv[0].start == 0 {
+ rui.nextVal = int(rc.iv[0].start) + int(rc.iv[0].length) + 1
+ rui.curIndex = 1
+ }
+ return rui
+}
+
+func (rui *runUnsetIterator16) hasNext() bool {
+ return rui.nextVal < 65536
+}
+
+func (rui *runUnsetIterator16) next() uint16 {
+ val := rui.nextVal
+ rui.nextVal++
+ if rui.curIndex < len(rui.rc.iv) && uint16(rui.nextVal) >= rui.rc.iv[rui.curIndex].start {
+ rui.nextVal = int(rui.rc.iv[rui.curIndex].start) + int(rui.rc.iv[rui.curIndex].length) + 1
+ rui.curIndex++
+ }
+ return uint16(val)
+}
+
+func (rui *runUnsetIterator16) peekNext() uint16 {
+ return uint16(rui.nextVal)
+}
+
+func (rui *runUnsetIterator16) advanceIfNeeded(minval uint16) {
+ if !rui.hasNext() || rui.peekNext() >= minval {
+ return
+ }
+ rui.nextVal = int(minval)
+ for rui.curIndex < len(rui.rc.iv) {
+ if rui.rc.iv[rui.curIndex].start+rui.rc.iv[rui.curIndex].length < minval {
+ rui.curIndex++
+ } else if rui.rc.iv[rui.curIndex].start <= minval {
+ rui.nextVal = int(rui.rc.iv[rui.curIndex].start) + int(rui.rc.iv[rui.curIndex].length) + 1
+ rui.curIndex++
+ break
+ } else {
+ break
+ }
+ }
+}
+
+func (rc *runContainer16) getUnsetIterator() shortPeekable {
+ return rc.newRunUnsetIterator16()
+}
+
// add the values in the range [firstOfRange, endx). endx
// is still abe to express 2^16 because it is an int not an uint16.
func (rc *runContainer16) iaddRange(firstOfRange, endx int) container {
@@ -2104,7 +2171,7 @@ func (rc *runContainer16) equals(o container) bool {
func (rc *runContainer16) iaddReturnMinimized(x uint16) container {
rc.Add(x)
- return rc
+ return rc.toEfficientContainer()
}
func (rc *runContainer16) iadd(x uint16) (wasNew bool) {
@@ -2113,7 +2180,7 @@ func (rc *runContainer16) iadd(x uint16) (wasNew bool) {
func (rc *runContainer16) iremoveReturnMinimized(x uint16) container {
rc.removeKey(x)
- return rc
+ return rc.toEfficientContainer()
}
func (rc *runContainer16) iremove(x uint16) bool {
@@ -2174,15 +2241,9 @@ func (rc *runContainer16) orArray(ac *arrayContainer) container {
if rc.isEmpty() {
return ac.clone()
}
- intervals, cardMinusOne := runArrayUnionToRuns(rc, ac)
+ intervals, cardminusone := runArrayUnionToRuns(rc, ac)
result := newRunContainer16TakeOwnership(intervals)
- if len(intervals) >= MaxNumIntervals && cardMinusOne >= arrayDefaultMaxSize {
- return newBitmapContainerFromRun(result)
- }
- if len(intervals)*2 > 1+int(cardMinusOne) {
- return result.toArrayContainer()
- }
- return result
+ return result.toEfficientContainerFromCardinality(int(cardminusone) + 1)
}
// orArray finds the union of rc and ac.
@@ -2200,7 +2261,7 @@ func (rc *runContainer16) ior(a container) container {
case *arrayContainer:
return rc.iorArray(c)
case *bitmapContainer:
- return rc.iorBitmapContainer(c)
+ return rc.orBitmapContainer(c)
}
panic("unsupported container type")
}
@@ -2212,16 +2273,17 @@ func (rc *runContainer16) inplaceUnion(rc2 *runContainer16) container {
rc.Add(uint16(i))
}
}
- return rc
+ return rc.toEfficientContainer()
}
-func (rc *runContainer16) iorBitmapContainer(bc *bitmapContainer) container {
- it := bc.getShortIterator()
- for it.hasNext() {
- rc.Add(it.next())
- }
- return rc
-}
+// Such code should not be used as it will not preserve the container invariants:
+//func (rc *runContainer16) iorBitmapContainer(bc *bitmapContainer) container {
+// it := bc.getShortIterator()
+// for it.hasNext() {
+// rc.Add(it.next())
+// }
+// return rc
+//}
func (rc *runContainer16) iorArray(ac *arrayContainer) container {
if rc.isEmpty() {
@@ -2235,13 +2297,8 @@ func (rc *runContainer16) iorArray(ac *arrayContainer) container {
// this can be done with methods like the in-place array container union
// but maybe lazily moving the remaining elements back.
rc.iv, cardMinusOne = runArrayUnionToRuns(rc, ac)
- if len(rc.iv) >= MaxNumIntervals && cardMinusOne >= arrayDefaultMaxSize {
- return newBitmapContainerFromRun(rc)
- }
- if len(rc.iv)*2 > 1+int(cardMinusOne) {
- return rc.toArrayContainer()
- }
- return rc
+ return rc.toEfficientContainerFromCardinality(int(cardMinusOne) + 1)
+
}
func runArrayUnionToRuns(rc *runContainer16, ac *arrayContainer) ([]interval16, uint16) {
@@ -2377,6 +2434,30 @@ func (rc *runContainer16) xor(a container) container {
panic("unsupported container type")
}
+func (rc *runContainer16) ixor(a container) container {
+ switch c := a.(type) {
+ case *arrayContainer:
+ return rc.ixorArray(c)
+ case *bitmapContainer:
+ return rc.ixorBitmap(c)
+ case *runContainer16:
+ return rc.ixorRunContainer16(c)
+ }
+ panic("unsupported container type")
+}
+
+func (rc *runContainer16) ixorArray(value2 *arrayContainer) container {
+ return rc.toBitmapContainer().ixor(value2)
+}
+
+func (rc *runContainer16) ixorBitmap(value2 *bitmapContainer) container {
+ return value2.ixor(rc)
+}
+
+func (rc *runContainer16) ixorRunContainer16(value2 *runContainer16) container {
+ return rc.toBitmapContainer().ixor(value2.toBitmapContainer())
+}
+
func (rc *runContainer16) iandNot(a container) container {
switch c := a.(type) {
case *arrayContainer:
@@ -2384,7 +2465,7 @@ func (rc *runContainer16) iandNot(a container) container {
case *bitmapContainer:
return rc.iandNotBitmap(c)
case *runContainer16:
- return rc.iandNotRunContainer16(c)
+ return rc.iandNotRunContainer16(c).toEfficientContainer()
}
panic("unsupported container type")
}
@@ -2399,11 +2480,11 @@ func (rc *runContainer16) inot(firstOfRange, endx int) container {
}
// TODO: minimize copies, do it all inplace; not() makes a copy.
rc = rc.Not(firstOfRange, endx)
- return rc
+ return rc.toEfficientContainer()
}
func (rc *runContainer16) rank(x uint16) int {
- n := int(len(rc.iv))
+ n := len(rc.iv)
xx := int(x)
w, already, _ := rc.search(xx)
if w < 0 {
@@ -2417,13 +2498,13 @@ func (rc *runContainer16) rank(x uint16) int {
for i := int(0); i <= w; i++ {
rnk += rc.iv[i].runlen()
}
- return int(rnk)
+ return rnk
}
for i := int(0); i < w; i++ {
rnk += rc.iv[i].runlen()
}
rnk += int(x-rc.iv[w].start) + 1
- return int(rnk)
+ return rnk
}
func (rc *runContainer16) selectInt(x uint16) int {
@@ -2431,7 +2512,7 @@ func (rc *runContainer16) selectInt(x uint16) int {
for k := range rc.iv {
nextOffset := offset + rc.iv[k].runlen()
if nextOffset > int(x) {
- return int(int(rc.iv[k].start) + (int(x) - offset))
+ return int(rc.iv[k].start) + (int(x) - offset)
}
offset = nextOffset
}
@@ -2455,10 +2536,11 @@ func (rc *runContainer16) andNotBitmap(bc *bitmapContainer) container {
func (rc *runContainer16) toBitmapContainer() *bitmapContainer {
bc := newBitmapContainer()
+ bc.cardinality = 0
for i := range rc.iv {
+ bc.cardinality += rc.iv[i].runlen()
bc.iaddRange(int(rc.iv[i].start), int(rc.iv[i].last())+1)
}
- bc.computeCardinality()
return bc
}
@@ -2473,21 +2555,23 @@ func (rc *runContainer16) iandNotArray(ac *arrayContainer) container {
rcb := rc.toBitmapContainer()
acb := ac.toBitmapContainer()
rcb.iandNotBitmapSurely(acb)
- // TODO: check size and optimize the return value
- // TODO: is inplace modification really required? If not, elide the copy.
- rc2 := newRunContainer16FromBitmapContainer(rcb)
- *rc = *rc2
- return rc
+ answer := rcb.toEfficientContainer()
+ if runrc, ok := answer.(*runContainer16); ok {
+ *rc = *runrc
+ return rc
+ }
+ return answer
}
func (rc *runContainer16) iandNotBitmap(bc *bitmapContainer) container {
rcb := rc.toBitmapContainer()
rcb.iandNotBitmapSurely(bc)
- // TODO: check size and optimize the return value
- // TODO: is inplace modification really required? If not, elide the copy.
- rc2 := newRunContainer16FromBitmapContainer(rcb)
- *rc = *rc2
- return rc
+ answer := rcb.toEfficientContainer()
+ if runrc, ok := answer.(*runContainer16); ok {
+ *rc = *runrc
+ return rc
+ }
+ return answer
}
func (rc *runContainer16) xorRunContainer16(x2 *runContainer16) container {
@@ -2523,6 +2607,20 @@ func (rc *runContainer16) toEfficientContainer() container {
return bc
}
+func (rc *runContainer16) toEfficientContainerFromCardinality(card int) container {
+ sizeAsRunContainer := rc.getSizeInBytes()
+ sizeAsBitmapContainer := bitmapContainerSizeInBytes()
+ sizeAsArrayContainer := arrayContainerSizeInBytes(card)
+ if sizeAsRunContainer < minOfInt(sizeAsBitmapContainer, sizeAsArrayContainer) {
+ return rc
+ }
+ if card <= arrayDefaultMaxSize {
+ return rc.toArrayContainer()
+ }
+ bc := newBitmapContainerFromRun(rc)
+ return bc
+}
+
func (rc *runContainer16) toArrayContainer() *arrayContainer {
ac := newArrayContainer()
for i := range rc.iv {
@@ -2619,7 +2717,7 @@ func (rc *runContainer16) addOffset(x uint16) (container, container) {
for _, iv := range rc.iv {
val := int(iv.start) + int(x)
- finalVal := int(val) + int(iv.length)
+ finalVal := val + int(iv.length)
if val <= 0xffff {
if finalVal <= 0xffff {
low.iv = append(low.iv, interval16{uint16(val), iv.length})
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/serialization_littleendian.go b/vendor/github.com/RoaringBitmap/roaring/v2/serialization_littleendian.go
index 16d356cafe..397805f802 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/serialization_littleendian.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/serialization_littleendian.go
@@ -6,9 +6,8 @@ package roaring
import (
"encoding/binary"
"errors"
+ "fmt"
"io"
- "reflect"
- "runtime"
"unsafe"
)
@@ -26,51 +25,30 @@ func (bc *bitmapContainer) writeTo(stream io.Writer) (int, error) {
}
func uint64SliceAsByteSlice(slice []uint64) []byte {
- // make a new slice header
- header := *(*reflect.SliceHeader)(unsafe.Pointer(&slice))
-
- // update its capacity and length
- header.Len *= 8
- header.Cap *= 8
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- result := *(*[]byte)(unsafe.Pointer(&header))
- runtime.KeepAlive(&slice)
-
- // return it
- return result
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ const size = unsafe.Sizeof(uint64(0))
+ return unsafe.Slice(((*byte)(unsafe.Pointer(ptr))), int(size)*len(slice))
}
func uint16SliceAsByteSlice(slice []uint16) []byte {
- // make a new slice header
- header := *(*reflect.SliceHeader)(unsafe.Pointer(&slice))
-
- // update its capacity and length
- header.Len *= 2
- header.Cap *= 2
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- result := *(*[]byte)(unsafe.Pointer(&header))
- runtime.KeepAlive(&slice)
-
- // return it
- return result
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ const size = unsafe.Sizeof(uint16(0))
+ return unsafe.Slice(((*byte)(unsafe.Pointer(ptr))), int(size)*len(slice))
}
func interval16SliceAsByteSlice(slice []interval16) []byte {
- // make a new slice header
- header := *(*reflect.SliceHeader)(unsafe.Pointer(&slice))
-
- // update its capacity and length
- header.Len *= 4
- header.Cap *= 4
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- result := *(*[]byte)(unsafe.Pointer(&header))
- runtime.KeepAlive(&slice)
-
- // return it
- return result
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ const size = unsafe.Sizeof(interval16{})
+ return unsafe.Slice(((*byte)(unsafe.Pointer(ptr))), int(size)*len(slice))
}
func (bc *bitmapContainer) asLittleEndianByteSlice() []byte {
@@ -86,69 +64,39 @@ func (bc *bitmapContainer) asLittleEndianByteSlice() []byte {
// or modified while you hold the returned slince.
// //
func byteSliceAsUint16Slice(slice []byte) (result []uint16) { // here we create a new slice holder
- if len(slice)%2 != 0 {
- panic("Slice size should be divisible by 2")
+ const sz = int(unsafe.Sizeof(uint16(0)))
+ if len(slice)%sz != 0 {
+ panic(fmt.Sprintf("Slice size should be divisible by %d", sz))
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / 2
- rHeader.Cap = bHeader.Cap / 2
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*uint16)(unsafe.Pointer(ptr)), len(slice)/sz)
}
func byteSliceAsUint64Slice(slice []byte) (result []uint64) {
- if len(slice)%8 != 0 {
- panic("Slice size should be divisible by 8")
+ const sz = int(unsafe.Sizeof(uint64(0)))
+ if len(slice)%sz != 0 {
+ panic(fmt.Sprintf("Slice size should be divisible by %d", sz))
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / 8
- rHeader.Cap = bHeader.Cap / 8
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*uint64)(unsafe.Pointer(ptr)), len(slice)/sz)
}
func byteSliceAsInterval16Slice(slice []byte) (result []interval16) {
- if len(slice)%4 != 0 {
- panic("Slice size should be divisible by 4")
+ const sz = int(unsafe.Sizeof(interval16{}))
+ if len(slice)%sz != 0 {
+ panic(fmt.Sprintf("Slice size should be divisible by %d", sz))
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / 4
- rHeader.Cap = bHeader.Cap / 4
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*interval16)(unsafe.Pointer(ptr)), len(slice)/sz)
}
func byteSliceAsContainerSlice(slice []byte) (result []container) {
@@ -158,114 +106,59 @@ func byteSliceAsContainerSlice(slice []byte) (result []container) {
if len(slice)%containerSize != 0 {
panic("Slice size should be divisible by unsafe.Sizeof(container)")
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / containerSize
- rHeader.Cap = bHeader.Cap / containerSize
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*container)(unsafe.Pointer(ptr)), len(slice)/containerSize)
}
func byteSliceAsBitsetSlice(slice []byte) (result []bitmapContainer) {
- bitsetSize := int(unsafe.Sizeof(bitmapContainer{}))
+ const bitsetSize = int(unsafe.Sizeof(bitmapContainer{}))
if len(slice)%bitsetSize != 0 {
panic("Slice size should be divisible by unsafe.Sizeof(bitmapContainer)")
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / bitsetSize
- rHeader.Cap = bHeader.Cap / bitsetSize
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*bitmapContainer)(unsafe.Pointer(ptr)), len(slice)/bitsetSize)
}
func byteSliceAsArraySlice(slice []byte) (result []arrayContainer) {
- arraySize := int(unsafe.Sizeof(arrayContainer{}))
+ const arraySize = int(unsafe.Sizeof(arrayContainer{}))
if len(slice)%arraySize != 0 {
panic("Slice size should be divisible by unsafe.Sizeof(arrayContainer)")
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / arraySize
- rHeader.Cap = bHeader.Cap / arraySize
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*arrayContainer)(unsafe.Pointer(ptr)), len(slice)/arraySize)
}
func byteSliceAsRun16Slice(slice []byte) (result []runContainer16) {
- run16Size := int(unsafe.Sizeof(runContainer16{}))
+ const run16Size = int(unsafe.Sizeof(runContainer16{}))
if len(slice)%run16Size != 0 {
panic("Slice size should be divisible by unsafe.Sizeof(runContainer16)")
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / run16Size
- rHeader.Cap = bHeader.Cap / run16Size
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*runContainer16)(unsafe.Pointer(ptr)), len(slice)/run16Size)
}
func byteSliceAsBoolSlice(slice []byte) (result []bool) {
- boolSize := int(unsafe.Sizeof(true))
+ const boolSize = int(unsafe.Sizeof(true))
if len(slice)%boolSize != 0 {
panic("Slice size should be divisible by unsafe.Sizeof(bool)")
}
- // reference: https://go101.org/article/unsafe.html
-
- // make a new slice header
- bHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
- rHeader := (*reflect.SliceHeader)(unsafe.Pointer(&result))
-
- // transfer the data from the given slice to a new variable (our result)
- rHeader.Data = bHeader.Data
- rHeader.Len = bHeader.Len / boolSize
- rHeader.Cap = bHeader.Cap / boolSize
-
- // instantiate result and use KeepAlive so data isn't unmapped.
- runtime.KeepAlive(&slice) // it is still crucial, GC can free it)
-
- // return result
- return
+ ptr := unsafe.SliceData(slice)
+ if ptr == nil {
+ return nil
+ }
+ return unsafe.Slice((*bool)(unsafe.Pointer(ptr)), len(slice)/boolSize)
}
// FrozenView creates a static view of a serialized bitmap stored in buf.
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/setutil.go b/vendor/github.com/RoaringBitmap/roaring/v2/setutil.go
index 8def774f5a..ec51cfc0fb 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/setutil.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/setutil.go
@@ -202,10 +202,22 @@ func intersects2by2(
set1 []uint16,
set2 []uint16,
) bool {
- // could be optimized if one set is much larger than the other one
if (len(set1) == 0) || (len(set2) == 0) {
return false
}
+ if len(set1)*64 < len(set2) {
+ return onesidedgallopingintersect2by2Bool(set1, set2)
+ } else if len(set2)*64 < len(set1) {
+ return onesidedgallopingintersect2by2Bool(set2, set1)
+ } else {
+ return intersects2by2Bool(set1, set2)
+ }
+}
+
+func intersects2by2Bool(
+ set1 []uint16,
+ set2 []uint16,
+) bool {
index1 := 0
index2 := 0
value1 := set1[index1]
@@ -244,6 +256,38 @@ mainwhile:
return false
}
+func onesidedgallopingintersect2by2Bool(
+ smallset []uint16,
+ largeset []uint16,
+) bool {
+ k1 := 0
+ k2 := 0
+ s1 := largeset[k1]
+ s2 := smallset[k2]
+mainwhile:
+ for {
+ if s1 < s2 {
+ k1 = advanceUntil(largeset, k1, len(largeset), s2)
+ if k1 == len(largeset) {
+ break mainwhile
+ }
+ s1 = largeset[k1]
+ }
+ if s2 < s1 {
+ k2++
+ if k2 == len(smallset) {
+ break mainwhile
+ }
+ s2 = smallset[k2]
+ } else {
+ // (set2[k2] == set1[k1])
+ return true
+ }
+
+ }
+ return false
+}
+
func localintersect2by2(
set1 []uint16,
set2 []uint16,
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/shortiterator.go b/vendor/github.com/RoaringBitmap/roaring/v2/shortiterator.go
index 15b78bd0c1..53252580fb 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/shortiterator.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/shortiterator.go
@@ -50,3 +50,53 @@ func (si *reverseIterator) next() uint16 {
si.loc--
return a
}
+
+type arrayContainerUnsetIterator struct {
+ content []uint16
+ // pos is the index of the next set bit that is >= nextVal.
+ // When nextVal reaches content[pos], pos is incremented.
+ pos int
+ nextVal int
+}
+
+func (acui *arrayContainerUnsetIterator) next() uint16 {
+ val := acui.nextVal
+ acui.nextVal++
+ for acui.pos < len(acui.content) && uint16(acui.nextVal) >= acui.content[acui.pos] {
+ acui.nextVal++
+ acui.pos++
+ }
+ return uint16(val)
+}
+
+func (acui *arrayContainerUnsetIterator) hasNext() bool {
+ return acui.nextVal < 65536
+}
+
+func (acui *arrayContainerUnsetIterator) peekNext() uint16 {
+ return uint16(acui.nextVal)
+}
+
+func (acui *arrayContainerUnsetIterator) advanceIfNeeded(minval uint16) {
+ if !acui.hasNext() || acui.peekNext() >= minval {
+ return
+ }
+ acui.nextVal = int(minval)
+ acui.pos = binarySearch(acui.content, minval)
+ if acui.pos < 0 {
+ acui.pos = -acui.pos - 1
+ }
+ for acui.pos < len(acui.content) && uint16(acui.nextVal) >= acui.content[acui.pos] {
+ acui.nextVal++
+ acui.pos++
+ }
+}
+
+func newArrayContainerUnsetIterator(content []uint16) *arrayContainerUnsetIterator {
+ acui := &arrayContainerUnsetIterator{content: content, pos: 0, nextVal: 0}
+ for acui.pos < len(acui.content) && uint16(acui.nextVal) >= acui.content[acui.pos] {
+ acui.nextVal++
+ acui.pos++
+ }
+ return acui
+}
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/smat.go b/vendor/github.com/RoaringBitmap/roaring/v2/smat.go
index c52c5f07cf..d0fcf292da 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/smat.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/smat.go
@@ -1,6 +1,3 @@
-//go:build gofuzz
-// +build gofuzz
-
/*
# Instructions for smat testing for roaring
@@ -11,69 +8,49 @@ To run the smat tests for roaring...
## Prerequisites
- $ go get github.com/dvyukov/go-fuzz/go-fuzz
- $ go get github.com/dvyukov/go-fuzz/go-fuzz-build
+Go 1.18 or later (for native fuzzing support).
## Steps
-1. Generate initial smat corpus:
+1. Generate initial smat corpus:
```
- go test -tags=gofuzz -run=TestGenerateSmatCorpus
+go test -tags=gofuzz -run=TestGenerateSmatCorpus
+```
+You should see a directory `workdir` created with initial corpus files.
+
+2. Run the fuzz test:
+```
+go test -run='^$' -fuzz=FuzzSmat -fuzztime=300s -timeout=60s
```
-2. Build go-fuzz test program with instrumentation:
-```
- go-fuzz-build -func FuzzSmat github.com/RoaringBitmap/roaring
-```
-
-3. Run go-fuzz:
-```
- go-fuzz -bin=./roaring-fuzz.zip -workdir=workdir/ -timeout=200
-```
-
-You should see output like...
-```
-2016/09/16 13:58:35 slaves: 8, corpus: 1 (3s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 3s
-2016/09/16 13:58:38 slaves: 8, corpus: 1 (6s ago), crashers: 0, restarts: 1/0, execs: 0 (0/sec), cover: 0, uptime: 6s
-2016/09/16 13:58:41 slaves: 8, corpus: 1 (9s ago), crashers: 0, restarts: 1/44, execs: 44 (5/sec), cover: 0, uptime: 9s
-2016/09/16 13:58:44 slaves: 8, corpus: 1 (12s ago), crashers: 0, restarts: 1/45, execs: 45 (4/sec), cover: 0, uptime: 12s
-2016/09/16 13:58:47 slaves: 8, corpus: 1 (15s ago), crashers: 0, restarts: 1/46, execs: 46 (3/sec), cover: 0, uptime: 15s
-2016/09/16 13:58:50 slaves: 8, corpus: 1 (18s ago), crashers: 0, restarts: 1/47, execs: 47 (3/sec), cover: 0, uptime: 18s
-2016/09/16 13:58:53 slaves: 8, corpus: 1 (21s ago), crashers: 0, restarts: 1/63, execs: 63 (3/sec), cover: 0, uptime: 21s
-2016/09/16 13:58:56 slaves: 8, corpus: 1 (24s ago), crashers: 0, restarts: 1/65, execs: 65 (3/sec), cover: 0, uptime: 24s
-2016/09/16 13:58:59 slaves: 8, corpus: 1 (27s ago), crashers: 0, restarts: 1/66, execs: 66 (2/sec), cover: 0, uptime: 27s
-2016/09/16 13:59:02 slaves: 8, corpus: 1 (30s ago), crashers: 0, restarts: 1/67, execs: 67 (2/sec), cover: 0, uptime: 30s
-2016/09/16 13:59:05 slaves: 8, corpus: 1 (33s ago), crashers: 0, restarts: 1/83, execs: 83 (3/sec), cover: 0, uptime: 33s
-2016/09/16 13:59:08 slaves: 8, corpus: 1 (36s ago), crashers: 0, restarts: 1/84, execs: 84 (2/sec), cover: 0, uptime: 36s
-2016/09/16 13:59:11 slaves: 8, corpus: 2 (0s ago), crashers: 0, restarts: 1/85, execs: 85 (2/sec), cover: 0, uptime: 39s
-2016/09/16 13:59:14 slaves: 8, corpus: 17 (2s ago), crashers: 0, restarts: 1/86, execs: 86 (2/sec), cover: 480, uptime: 42s
-2016/09/16 13:59:17 slaves: 8, corpus: 17 (5s ago), crashers: 0, restarts: 1/66, execs: 132 (3/sec), cover: 487, uptime: 45s
-2016/09/16 13:59:20 slaves: 8, corpus: 17 (8s ago), crashers: 0, restarts: 1/440, execs: 2645 (55/sec), cover: 487, uptime: 48s
-
-```
-
-Let it run, and if the # of crashers is > 0, check out the reports in
-the workdir where you should be able to find the panic goroutine stack
-traces.
+Adjust `-fuzztime` as needed for longer or shorter runs. If crashes are found,
+check the test output and the reproducer files in the `workdir` directory.
+You may copy the reproducers to roaring_tests.go
*/
package roaring
import (
+ "encoding/base64"
"fmt"
- "sort"
+ "os"
+ "path/filepath"
+ "runtime/debug"
+ "slices"
+ "strings"
+ "time"
"github.com/bits-and-blooms/bitset"
"github.com/mschoch/smat"
)
-// fuzz test using state machine driven by byte stream.
-func FuzzSmat(data []byte) int {
- return smat.Fuzz(&smatContext{}, smat.ActionID('S'), smat.ActionID('T'),
- smatActionMap, data)
-}
+// The native fuzz entry point lives in a _test.go file so the go test
+// fuzz engine discovers it. See smat_fuzz_test.go for the fuzz wrapper.
-var smatDebug = false
+var smatDebug = true
+
+const max_value = 1048576
+const max_pairs = 10
func smatLog(prefix, format string, args ...interface{}) {
if smatDebug {
@@ -90,22 +67,33 @@ type smatContext struct {
y int
actions int
+ // per-context last action for this fuzz worker
+ lastAction *actionRecord
+}
+
+// actionRecord stores a snapshot of the state just before an action runs.
+type actionRecord struct {
+ Name string
+ X, Y int
+ PairSnapshots []string // base64-encoded MarshalBinary of each pair's Bitmap
}
type smatPair struct {
bm *Bitmap
bs *bitset.BitSet
+ // parent context (nil if unknown)
+ ctx *smatContext
}
// ------------------------------------------------------------------
var smatActionMap = smat.ActionMap{
- smat.ActionID('X'): smatAction("x++", smatWrap(func(c *smatContext) { c.x++ })),
- smat.ActionID('x'): smatAction("x--", smatWrap(func(c *smatContext) { c.x-- })),
- smat.ActionID('Y'): smatAction("y++", smatWrap(func(c *smatContext) { c.y++ })),
- smat.ActionID('y'): smatAction("y--", smatWrap(func(c *smatContext) { c.y-- })),
- smat.ActionID('*'): smatAction("x*y", smatWrap(func(c *smatContext) { c.x = c.x * c.y })),
- smat.ActionID('<'): smatAction("x<<", smatWrap(func(c *smatContext) { c.x = c.x << 1 })),
+ smat.ActionID('X'): smatAction("x++", smatWrap(func(c *smatContext) { c.x = (c.x + 1) % max_value })),
+ smat.ActionID('x'): smatAction("x--", smatWrap(func(c *smatContext) { c.x = (c.x - 1 + max_value) % max_value })),
+ smat.ActionID('Y'): smatAction("y++", smatWrap(func(c *smatContext) { c.y = (c.y + 1) % max_value })),
+ smat.ActionID('y'): smatAction("y--", smatWrap(func(c *smatContext) { c.y = (c.y - 1 + max_value) % max_value })),
+ smat.ActionID('*'): smatAction("x*y", smatWrap(func(c *smatContext) { c.x = (c.x * c.y) % max_value })),
+ smat.ActionID('<'): smatAction("x<<", smatWrap(func(c *smatContext) { c.x = (c.x << 1) % max_value })),
smat.ActionID('^'): smatAction("swap", smatWrap(func(c *smatContext) { c.x, c.y = c.y, c.x })),
@@ -117,11 +105,13 @@ var smatActionMap = smat.ActionMap{
smat.ActionID('o'): smatAction(" or", smatWrap(smatOr)),
smat.ActionID('a'): smatAction(" and", smatWrap(smatAnd)),
+ smat.ActionID('z'): smatAction(" xor", smatWrap(smatXor)),
smat.ActionID('#'): smatAction(" cardinality", smatWrap(smatCardinality)),
smat.ActionID('O'): smatAction(" orCardinality", smatWrap(smatOrCardinality)),
smat.ActionID('A'): smatAction(" andCardinality", smatWrap(smatAndCardinality)),
+ smat.ActionID('Z'): smatAction(" xorCardinality", smatWrap(smatXorCardinality)),
smat.ActionID('c'): smatAction(" clear", smatWrap(smatClear)),
smat.ActionID('r'): smatAction(" runOptimize", smatWrap(smatRunOptimize)),
@@ -142,12 +132,12 @@ func init() {
for actionId := range smatActionMap {
ids = append(ids, int(actionId))
}
- sort.Ints(ids)
+ slices.Sort(ids)
pct := 100 / len(smatActionMap)
for _, actionId := range ids {
smatRunningPercentActions = append(smatRunningPercentActions,
- smat.PercentAction{pct, smat.ActionID(actionId)})
+ smat.PercentAction{Percent: pct, Action: smat.ActionID(actionId)})
}
smatActionMap[smat.ActionID('S')] = smatAction("SETUP", smatSetupFunc)
@@ -162,14 +152,153 @@ func smatRunning(next byte) smat.ActionID {
func smatAction(name string, f func(ctx smat.Context) (smat.State, error)) func(smat.Context) (smat.State, error) {
return func(ctx smat.Context) (smat.State, error) {
c := ctx.(*smatContext)
+
+ // Snapshot all pairs' bitmaps (base64 of MarshalBinary) before action
+ rec := actionRecord{Name: name, X: c.x, Y: c.y}
+ if len(c.pairs) > 0 {
+ rec.PairSnapshots = make([]string, 0, len(c.pairs))
+ for _, pair := range c.pairs {
+ if pair == nil || pair.bm == nil {
+ rec.PairSnapshots = append(rec.PairSnapshots, "")
+ continue
+ }
+ b, err := pair.bm.MarshalBinary()
+ if err != nil {
+ rec.PairSnapshots = append(rec.PairSnapshots, "")
+ } else {
+ rec.PairSnapshots = append(rec.PairSnapshots, base64.StdEncoding.EncodeToString(b))
+ }
+ }
+ }
+
+ // record per-context last action (no global mutex required)
+ if c != nil {
+ c.lastAction = &rec
+ }
+
+ // catch panics inside action to dump a repro and stack before re-panicking
+ defer func() {
+ if r := recover(); r != nil {
+ // best-effort: write quick repro with lastAction from context
+ var lastAction *actionRecord
+ if c != nil {
+ lastAction = c.lastAction
+ }
+ ts := time.Now().UnixNano()
+ repro := "// Reproducer generated by smat (panic)\n"
+ repro += "package roaring\n\n"
+ repro += "import (\n\t\"encoding/base64\"\n\t\"testing\"\n)\n\n"
+ repro += fmt.Sprintf("func TestFuzzerPanicRepro_%d(t *testing.T) {\n", ts)
+ // similar to checkEquals repro
+ if lastAction != nil && len(lastAction.PairSnapshots) > 0 {
+ pairIndex := lastAction.X % len(lastAction.PairSnapshots)
+ if pairIndex < len(lastAction.PairSnapshots) {
+ snapshot := lastAction.PairSnapshots[pairIndex]
+ if snapshot != "" && !strings.HasPrefix(snapshot, "<") {
+ repro += fmt.Sprintf("\tb, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshot)
+ repro += "\tbm := NewBitmap()\n"
+ repro += "\tbm.UnmarshalBinary(b)\n"
+ // perform the action that caused panic
+ if strings.Contains(lastAction.Name, "setBit") {
+ repro += fmt.Sprintf("\tbm.AddInt(%d)\n", lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "removeBit") {
+ repro += fmt.Sprintf("\tbm.Remove(%d)\n", lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "flip") {
+ repro += fmt.Sprintf("\tbm.Flip(uint64(%d), uint64(%d)+1)\n", lastAction.Y, lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "runOptimize") {
+ repro += "\tbm.RunOptimize()\n"
+ } else if strings.Contains(lastAction.Name, "clear") {
+ repro += "\tbm.Clear()\n"
+ } else if lastAction.Name == " or" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.Or(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " and" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.And(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " difference" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.AndNot(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " xor" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.Xor(bm2)\n"
+ }
+ }
+ } else {
+ repro += fmt.Sprintf("\t// Unhandled action: %s\n", lastAction.Name)
+ }
+ } else {
+ repro += "\t// invalid snapshot\n"
+ }
+ }
+ }
+ repro += "}\n"
+ if path, werr := saveReproFile("smat_panic_repro", ts, repro); werr == nil {
+ fmt.Printf("wrote panic repro to %s\n", path)
+ } else {
+ fmt.Printf("failed writing panic repro: %v\n", werr)
+ }
+ fmt.Printf("PANIC in action %s: %v\n", rec.Name, r)
+ fmt.Printf("stack:\n%s\n", debug.Stack())
+ panic(r)
+ }
+ }()
+
c.actions++
-
- smatLog(" ", "%s\n", name)
-
return f(ctx)
}
}
+// saveReproFile writes the given repro content to workdir/__test.go
+// or falls back to the OS temp dir. Returns full path or error.
+func saveReproFile(prefix string, ts int64, content string) (string, error) {
+ // try workdir
+ if err := os.MkdirAll("workdir", 0o755); err == nil {
+ fname := fmt.Sprintf("workdir/%s_%d_test.go", prefix, ts)
+ if err := os.WriteFile(fname, []byte(content), 0o644); err == nil {
+ return fname, nil
+ }
+ }
+ // fallback to temp
+ tmp := os.TempDir()
+ fname := fmt.Sprintf("%s_%d_test.go", prefix, ts)
+ full := filepath.Join(tmp, fname)
+ if err := os.WriteFile(full, []byte(content), 0o644); err == nil {
+ return full, nil
+ } else {
+ return "", err
+ }
+}
+
// Creates an smat action func based on a simple callback.
func smatWrap(cb func(c *smatContext)) func(smat.Context) (next smat.State, err error) {
return func(ctx smat.Context) (next smat.State, err error) {
@@ -203,10 +332,15 @@ func smatTeardownFunc(ctx smat.Context) (next smat.State, err error) {
// ------------------------------------------------------------------
func smatPushPair(c *smatContext) {
- c.pairs = append(c.pairs, &smatPair{
- bm: NewBitmap(),
- bs: bitset.New(100),
- })
+ if len(c.pairs) >= max_pairs {
+ return
+ }
+ p := &smatPair{
+ bm: NewBitmap(),
+ bs: bitset.New(100),
+ ctx: c,
+ }
+ c.pairs = append(c.pairs, p)
}
func smatPopPair(c *smatContext) {
@@ -217,6 +351,7 @@ func smatPopPair(c *smatContext) {
func smatSetBit(c *smatContext) {
c.withPair(c.x, func(p *smatPair) {
+ p.Validate()
y := uint32(c.y)
p.bm.AddInt(int(y))
p.bs.Set(uint(y))
@@ -226,6 +361,7 @@ func smatSetBit(c *smatContext) {
func smatRemoveBit(c *smatContext) {
c.withPair(c.x, func(p *smatPair) {
+ p.Validate()
y := uint32(c.y)
p.bm.Remove(y)
p.bs.Clear(uint(y))
@@ -236,6 +372,8 @@ func smatRemoveBit(c *smatContext) {
func smatAnd(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
px.bm.And(py.bm)
px.bs = px.bs.Intersection(py.bs)
px.checkEquals()
@@ -247,6 +385,8 @@ func smatAnd(c *smatContext) {
func smatOr(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
px.bm.Or(py.bm)
px.bs = px.bs.Union(py.bs)
px.checkEquals()
@@ -255,9 +395,24 @@ func smatOr(c *smatContext) {
})
}
+func smatXor(c *smatContext) {
+ c.withPair(c.x, func(px *smatPair) {
+ c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
+ px.bm.Xor(py.bm)
+ px.bs = px.bs.SymmetricDifference(py.bs)
+ px.checkEquals()
+ py.checkEquals()
+ })
+ })
+}
+
func smatAndCardinality(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
c0 := px.bm.AndCardinality(py.bm)
c1 := px.bs.IntersectionCardinality(py.bs)
if c0 != uint64(c1) {
@@ -272,6 +427,8 @@ func smatAndCardinality(c *smatContext) {
func smatOrCardinality(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
c0 := px.bm.OrCardinality(py.bm)
c1 := px.bs.UnionCardinality(py.bs)
if c0 != uint64(c1) {
@@ -283,8 +440,25 @@ func smatOrCardinality(c *smatContext) {
})
}
+func smatXorCardinality(c *smatContext) {
+ c.withPair(c.x, func(px *smatPair) {
+ c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
+ c0 := px.bm.OrCardinality(py.bm) - px.bm.AndCardinality(py.bm)
+ c1 := px.bs.SymmetricDifferenceCardinality(py.bs)
+ if c0 != uint64(c1) {
+ panic("expected same xor cardinality")
+ }
+ px.checkEquals()
+ py.checkEquals()
+ })
+ })
+}
+
func smatRunOptimize(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
+ px.Validate()
px.bm.RunOptimize()
px.checkEquals()
})
@@ -292,6 +466,7 @@ func smatRunOptimize(c *smatContext) {
func smatClear(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
+ px.Validate()
px.bm.Clear()
px.bs = px.bs.ClearAll()
px.checkEquals()
@@ -321,6 +496,8 @@ func smatIsEmpty(c *smatContext) {
func smatIntersects(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
v0 := px.bm.Intersects(py.bm)
v1 := px.bs.IntersectionCardinality(py.bs) > 0
if v0 != v1 {
@@ -335,6 +512,7 @@ func smatIntersects(c *smatContext) {
func smatFlip(c *smatContext) {
c.withPair(c.x, func(p *smatPair) {
+ p.Validate()
y := uint32(c.y)
p.bm.Flip(uint64(y), uint64(y)+1)
p.bs = p.bs.Flip(uint(y))
@@ -345,6 +523,8 @@ func smatFlip(c *smatContext) {
func smatDifference(c *smatContext) {
c.withPair(c.x, func(px *smatPair) {
c.withPair(c.y, func(py *smatPair) {
+ px.Validate()
+ py.Validate()
px.bm.AndNot(py.bm)
px.bs = px.bs.Difference(py.bs)
px.checkEquals()
@@ -354,11 +534,164 @@ func smatDifference(c *smatContext) {
}
func (p *smatPair) checkEquals() {
+ valid := p.bm.Validate()
+ if valid != nil {
+ // marshal current bitmap
+ var curSnap string
+ if p != nil && p.bm != nil {
+ if b, err := p.bm.MarshalBinary(); err == nil {
+ curSnap = base64.StdEncoding.EncodeToString(b)
+ } else {
+ curSnap = ""
+ }
+ } else {
+ curSnap = ""
+ }
+
+ // collect last action summary from context (per-worker)
+ last := ""
+ if p != nil && p.ctx != nil {
+ c := p.ctx
+ if c.lastAction != nil {
+ last = fmt.Sprintf("action=%s x=%d y=%d pairs=%d", c.lastAction.Name, c.lastAction.X, c.lastAction.Y, len(c.lastAction.PairSnapshots))
+ }
+ }
+
+ // If debugging enabled, log extra info
+ smatLog("ERROR: ", "bitmap invalid: %v\n", valid)
+
+ // build a reproducible test snippet that reconstructs the bitmap and replays the failing action
+ ts := time.Now().UnixNano()
+ testName := fmt.Sprintf("TestFuzzerRepro_%d", ts)
+ repro := "// Reproducer generated by smat\n"
+ repro += "package roaring\n\n"
+ repro += "import (\n\t\"encoding/base64\"\n\t\"testing\"\n)\n\n"
+ repro += fmt.Sprintf("func %s(t *testing.T) {\n", testName)
+ var lastAction *actionRecord
+ if p != nil && p.ctx != nil {
+ lastAction = p.ctx.lastAction
+ }
+ // use the snapshot of the modified pair
+ if lastAction != nil && len(lastAction.PairSnapshots) > 0 {
+ // assume the modified pair is x % len(pairs), but since pairs are in order, and x is lastAction.X
+ pairIndex := lastAction.X % len(lastAction.PairSnapshots)
+ if pairIndex < len(lastAction.PairSnapshots) {
+ snapshot := lastAction.PairSnapshots[pairIndex]
+ if snapshot != "" && !strings.HasPrefix(snapshot, "<") {
+ repro += fmt.Sprintf("\tb, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshot)
+ repro += "\tbm := NewBitmap()\n"
+ repro += "\tbm.UnmarshalBinary(b)\n"
+ repro += "\tif err := bm.Validate(); err != nil {\n"
+ repro += "\t\tt.Errorf(\"Initial Validate failed: %v\", err)\n"
+ repro += "\t}\n"
+ // perform the action
+ if strings.Contains(lastAction.Name, "setBit") {
+ repro += fmt.Sprintf("\tbm.AddInt(%d)\n", lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "removeBit") {
+ repro += fmt.Sprintf("\tbm.Remove(%d)\n", lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "flip") {
+ repro += fmt.Sprintf("\tbm.Flip(uint64(%d), uint64(%d)+1)\n", lastAction.Y, lastAction.Y)
+ } else if strings.Contains(lastAction.Name, "runOptimize") {
+ repro += "\tbm.RunOptimize()\n"
+ } else if strings.Contains(lastAction.Name, "clear") {
+ repro += "\tbm.Clear()\n"
+ } else if lastAction.Name == " or" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.Or(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " and" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.And(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " difference" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.AndNot(bm2)\n"
+ }
+ }
+ } else if lastAction.Name == " xor" {
+ pairIndexY := lastAction.Y % len(lastAction.PairSnapshots)
+ if pairIndexY < len(lastAction.PairSnapshots) {
+ snapshotY := lastAction.PairSnapshots[pairIndexY]
+ if snapshotY != "" && !strings.HasPrefix(snapshotY, "<") {
+ repro += fmt.Sprintf("\tb2, _ := base64.StdEncoding.DecodeString(\"%s\")\n", snapshotY)
+ repro += "\tbm2 := NewBitmap()\n"
+ repro += "\tbm2.UnmarshalBinary(b2)\n"
+ repro += "\tbm.Xor(bm2)\n"
+ }
+ }
+ } else {
+ repro += fmt.Sprintf("\t// Unhandled action: %s\n", lastAction.Name)
+ }
+ repro += "\tif err := bm.Validate(); err != nil {\n"
+ repro += "\t\tt.Errorf(\"Validate failed: %v\", err)\n"
+ repro += "\t} else {\n"
+ repro += "\t\tt.Logf(\"Validate succeeded\")\n"
+ repro += "\t}\n"
+ } else {
+ repro += "\t// invalid snapshot\n"
+ }
+ }
+ }
+ repro += "}\n"
+
+ // print the repro snippet for the developer
+ fmt.Println()
+ fmt.Println("=== SMAT REPRODUCER SNIPPET ===")
+ if len(repro) > 10000 {
+ fmt.Println("// Reproducer too large, skipping full print")
+ } else {
+ fmt.Println(repro)
+ }
+
+ // also write the repro snippet to a timestamped file in workdir/
+ if len(repro) > 10000 {
+ repro = "// Reproducer too large, skipping\n"
+ }
+ if err := os.MkdirAll("workdir", 0o755); err == nil {
+ fname := fmt.Sprintf("workdir/smat_repro_%d_test.go", ts)
+ if werr := os.WriteFile(fname, []byte(repro), 0o644); werr == nil {
+ fmt.Printf("Wrote repro to %s\n", fname)
+ } else {
+ fmt.Printf("Failed writing repro file: %v\n", werr)
+ }
+ } else {
+ fmt.Printf("Failed creating workdir: %v\n", err)
+ }
+
+ panic(fmt.Sprintf("[checkEquals] bitmap invalid: %v\ncurrentBase64:%s\nlastAction:%s\n", valid, curSnap, last))
+ }
if !p.equalsBitSet(p.bs, p.bm) {
panic("bitset mismatch")
}
}
+func (p *smatPair) Validate() {
+ valid := p.bm.Validate()
+ if valid != nil {
+ panic(fmt.Sprintf("[Validate] bitmap invalid: %v", valid))
+ }
+}
+
func (p *smatPair) equalsBitSet(a *bitset.BitSet, b *Bitmap) bool {
for i, e := a.NextSet(0); e; i, e = a.NextSet(i + 1) {
if !b.ContainsInt(int(i)) {
diff --git a/vendor/github.com/RoaringBitmap/roaring/v2/util.go b/vendor/github.com/RoaringBitmap/roaring/v2/util.go
index f58a86b2ed..031dfa307d 100644
--- a/vendor/github.com/RoaringBitmap/roaring/v2/util.go
+++ b/vendor/github.com/RoaringBitmap/roaring/v2/util.go
@@ -1,9 +1,10 @@
package roaring
import (
+ "cmp"
"math"
"math/rand"
- "sort"
+ "slices"
)
const (
@@ -123,7 +124,7 @@ func combineLoHi16(lob uint16, hob uint16) uint32 {
}
func combineLoHi32(lob uint32, hob uint32) uint32 {
- return uint32(lob) | (hob << 16)
+ return lob | (hob << 16)
}
const maxLowBit = 0xFFFF
@@ -264,19 +265,13 @@ type ph struct {
rand int
}
-type pha []ph
-
-func (p pha) Len() int { return len(p) }
-func (p pha) Less(i, j int) bool { return p[i].rand < p[j].rand }
-func (p pha) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
-
func getRandomPermutation(n int) []int {
r := make([]ph, n)
for i := 0; i < n; i++ {
r[i].orig = i
r[i].rand = rand.Intn(1 << 29)
}
- sort.Sort(pha(r))
+ slices.SortFunc(r, func(a, b ph) int { return cmp.Compare(a.rand, b.rand) })
m := make([]int, n)
for i := range m {
m[i] = r[i].orig
diff --git a/vendor/github.com/bits-and-blooms/bitset/README.md b/vendor/github.com/bits-and-blooms/bitset/README.md
index b245facb7e..599982f2ee 100644
--- a/vendor/github.com/bits-and-blooms/bitset/README.md
+++ b/vendor/github.com/bits-and-blooms/bitset/README.md
@@ -164,3 +164,13 @@ Before committing the code, please check if it passes tests, has adequate covera
go test
go test -cover
```
+
+## Stars
+
+
+[](https://www.star-history.com/#bits-and-blooms/bitset&Date)
+
+## Further reading
+
+Mastering Programming: From Testing to Performance in Go
+
diff --git a/vendor/github.com/bits-and-blooms/bitset/bitset.go b/vendor/github.com/bits-and-blooms/bitset/bitset.go
index 46d05b9ed8..c8d8ddba8c 100644
--- a/vendor/github.com/bits-and-blooms/bitset/bitset.go
+++ b/vendor/github.com/bits-and-blooms/bitset/bitset.go
@@ -905,7 +905,9 @@ func (b *BitSet) DifferenceCardinality(compare *BitSet) uint {
l = b.wordCount()
}
cnt := uint64(0)
- cnt += popcntMaskSlice(b.set[:l], compare.set[:l])
+ if l > 0 {
+ cnt += popcntMaskSlice(b.set[:l], compare.set[:l])
+ }
cnt += popcntSlice(b.set[l:])
return uint(cnt)
}
@@ -960,6 +962,9 @@ func (b *BitSet) Intersection(compare *BitSet) (result *BitSet) {
func (b *BitSet) IntersectionCardinality(compare *BitSet) uint {
panicIfNull(b)
panicIfNull(compare)
+ if b.length == 0 || compare.length == 0 {
+ return 0
+ }
b, compare = sortByLength(b, compare)
cnt := popcntAndSlice(b.set, compare.set)
return uint(cnt)
@@ -1016,7 +1021,10 @@ func (b *BitSet) UnionCardinality(compare *BitSet) uint {
panicIfNull(b)
panicIfNull(compare)
b, compare = sortByLength(b, compare)
- cnt := popcntOrSlice(b.set, compare.set)
+ cnt := uint64(0)
+ if len(b.set) > 0 {
+ cnt += popcntOrSlice(b.set, compare.set)
+ }
if len(compare.set) > len(b.set) {
cnt += popcntSlice(compare.set[len(b.set):])
}
@@ -1071,7 +1079,10 @@ func (b *BitSet) SymmetricDifferenceCardinality(compare *BitSet) uint {
panicIfNull(b)
panicIfNull(compare)
b, compare = sortByLength(b, compare)
- cnt := popcntXorSlice(b.set, compare.set)
+ cnt := uint64(0)
+ if len(b.set) > 0 {
+ cnt += popcntXorSlice(b.set, compare.set)
+ }
if len(compare.set) > len(b.set) {
cnt += popcntSlice(compare.set[len(b.set):])
}
@@ -1473,7 +1484,7 @@ func (b *BitSet) ShiftLeft(bits uint) {
dst := b.set
// not using extendSet() to avoid unneeded data copying
- nsize := wordsNeeded(top + bits)
+ nsize := wordsNeeded(top + bits + 1)
if len(b.set) < nsize {
dst = make([]uint64, nsize)
}
@@ -1520,7 +1531,7 @@ func (b *BitSet) ShiftRight(bits uint) {
return
}
- if bits >= top {
+ if bits > top {
b.set = make([]uint64, wordsNeeded(b.length))
return
}
diff --git a/vendor/github.com/bits-and-blooms/bitset/popcnt.go b/vendor/github.com/bits-and-blooms/bitset/popcnt.go
index 93492390bf..a6d62f7239 100644
--- a/vendor/github.com/bits-and-blooms/bitset/popcnt.go
+++ b/vendor/github.com/bits-and-blooms/bitset/popcnt.go
@@ -2,58 +2,51 @@ package bitset
import "math/bits"
-func popcntSlice(s []uint64) uint64 {
- var cnt int
+func popcntSlice(s []uint64) (cnt uint64) {
for _, x := range s {
- cnt += bits.OnesCount64(x)
+ cnt += uint64(bits.OnesCount64(x))
}
- return uint64(cnt)
+ return
}
-func popcntMaskSlice(s, m []uint64) uint64 {
- var cnt int
- // this explicit check eliminates a bounds check in the loop
- if len(m) < len(s) {
- panic("mask slice is too short")
- }
+func popcntMaskSlice(s, m []uint64) (cnt uint64) {
+ // The next line is to help the bounds checker, it matters!
+ _ = m[len(s)-1] // BCE
for i := range s {
- cnt += bits.OnesCount64(s[i] &^ m[i])
+ cnt += uint64(bits.OnesCount64(s[i] &^ m[i]))
}
- return uint64(cnt)
+ return
}
-func popcntAndSlice(s, m []uint64) uint64 {
- var cnt int
- // this explicit check eliminates a bounds check in the loop
- if len(m) < len(s) {
- panic("mask slice is too short")
- }
+// popcntAndSlice computes the population count of the AND of two slices.
+// It assumes that len(m) >= len(s) > 0.
+func popcntAndSlice(s, m []uint64) (cnt uint64) {
+ // The next line is to help the bounds checker, it matters!
+ _ = m[len(s)-1] // BCE
for i := range s {
- cnt += bits.OnesCount64(s[i] & m[i])
+ cnt += uint64(bits.OnesCount64(s[i] & m[i]))
}
- return uint64(cnt)
+ return
}
-func popcntOrSlice(s, m []uint64) uint64 {
- var cnt int
- // this explicit check eliminates a bounds check in the loop
- if len(m) < len(s) {
- panic("mask slice is too short")
- }
+// popcntOrSlice computes the population count of the OR of two slices.
+// It assumes that len(m) >= len(s) > 0.
+func popcntOrSlice(s, m []uint64) (cnt uint64) {
+ // The next line is to help the bounds checker, it matters!
+ _ = m[len(s)-1] // BCE
for i := range s {
- cnt += bits.OnesCount64(s[i] | m[i])
+ cnt += uint64(bits.OnesCount64(s[i] | m[i]))
}
- return uint64(cnt)
+ return
}
-func popcntXorSlice(s, m []uint64) uint64 {
- var cnt int
- // this explicit check eliminates a bounds check in the loop
- if len(m) < len(s) {
- panic("mask slice is too short")
- }
+// popcntXorSlice computes the population count of the XOR of two slices.
+// It assumes that len(m) >= len(s) > 0.
+func popcntXorSlice(s, m []uint64) (cnt uint64) {
+ // The next line is to help the bounds checker, it matters!
+ _ = m[len(s)-1] // BCE
for i := range s {
- cnt += bits.OnesCount64(s[i] ^ m[i])
+ cnt += uint64(bits.OnesCount64(s[i] ^ m[i]))
}
- return uint64(cnt)
+ return
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/README.md b/vendor/github.com/blevesearch/bleve/v2/README.md
index 47ff007732..c0e85e53a3 100644
--- a/vendor/github.com/blevesearch/bleve/v2/README.md
+++ b/vendor/github.com/blevesearch/bleve/v2/README.md
@@ -24,6 +24,7 @@ A modern indexing + search library in GO
* [geo spatial search](https://github.com/blevesearch/bleve/blob/master/geo/README.md)
* approximate k-nearest neighbors via [vector search](https://github.com/blevesearch/bleve/blob/master/docs/vectors.md)
* [synonym search](https://github.com/blevesearch/bleve/blob/master/docs/synonyms.md)
+ * [hierarchical nested search](https://github.com/blevesearch/bleve/blob/master/docs/hierarchy.md)
* [tf-idf](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#tf-idf) / [bm25](https://github.com/blevesearch/bleve/blob/master/docs/scoring.md#bm25) scoring models
* Hybrid search: exact + semantic
* Supports [RRF (Reciprocal Rank Fusion) and RSF (Relative Score Fusion)](docs/score_fusion.md)
diff --git a/vendor/github.com/blevesearch/bleve/v2/analysis/analyzer/custom/custom.go b/vendor/github.com/blevesearch/bleve/v2/analysis/analyzer/custom/custom.go
index 5df940e5ee..9040e02830 100644
--- a/vendor/github.com/blevesearch/bleve/v2/analysis/analyzer/custom/custom.go
+++ b/vendor/github.com/blevesearch/bleve/v2/analysis/analyzer/custom/custom.go
@@ -140,7 +140,7 @@ func convertInterfaceSliceToStringSlice(interfaceSlice []interface{}, objType st
if ok {
stringSlice[i] = stringObj
} else {
- return nil, fmt.Errorf(objType + " name must be a string")
+ return nil, fmt.Errorf("%s name must be a string", objType)
}
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/builder.go b/vendor/github.com/blevesearch/bleve/v2/builder.go
index f170317ee2..5398739f52 100644
--- a/vendor/github.com/blevesearch/bleve/v2/builder.go
+++ b/vendor/github.com/blevesearch/bleve/v2/builder.go
@@ -73,7 +73,10 @@ func newBuilder(path string, mapping mapping.IndexMapping, config map[string]int
// do not use real config, as these are options for the builder,
// not the resulting index
- meta := newIndexMeta(scorch.Name, scorch.Name, map[string]interface{}{})
+ meta, err := newIndexMeta(scorch.Name, scorch.Name, map[string]interface{}{}, path)
+ if err != nil {
+ return nil, err
+ }
err = meta.Save(path)
if err != nil {
return nil, err
diff --git a/vendor/github.com/blevesearch/bleve/v2/document/document.go b/vendor/github.com/blevesearch/bleve/v2/document/document.go
index 569d57bd68..cb3f537870 100644
--- a/vendor/github.com/blevesearch/bleve/v2/document/document.go
+++ b/vendor/github.com/blevesearch/bleve/v2/document/document.go
@@ -30,8 +30,9 @@ func init() {
}
type Document struct {
- id string `json:"id"`
- Fields []Field `json:"fields"`
+ id string
+ Fields []Field `json:"fields"`
+ NestedDocuments []*Document `json:"nested_documents"`
CompositeFields []*CompositeField
StoredFieldsSize uint64
indexed bool
@@ -68,6 +69,12 @@ func (d *Document) Size() int {
sizeInBytes += entry.Size()
}
+ for _, entry := range d.NestedDocuments {
+ if entry != nil {
+ sizeInBytes += entry.Size()
+ }
+ }
+
return sizeInBytes
}
@@ -111,6 +118,11 @@ func (d *Document) NumPlainTextBytes() uint64 {
}
}
}
+ for _, nestedDoc := range d.NestedDocuments {
+ if nestedDoc != nil {
+ rv += nestedDoc.NumPlainTextBytes()
+ }
+ }
return rv
}
@@ -157,3 +169,13 @@ func (d *Document) SetIndexed() {
func (d *Document) Indexed() bool {
return d.indexed
}
+
+func (d *Document) AddNestedDocument(doc *Document) {
+ d.NestedDocuments = append(d.NestedDocuments, doc)
+}
+
+func (d *Document) VisitNestedDocuments(visitor func(doc index.Document)) {
+ for _, doc := range d.NestedDocuments {
+ visitor(doc)
+ }
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/document/field_geopoint.go b/vendor/github.com/blevesearch/bleve/v2/document/field_geopoint.go
index 5795043f2f..ef8938f70d 100644
--- a/vendor/github.com/blevesearch/bleve/v2/document/field_geopoint.go
+++ b/vendor/github.com/blevesearch/bleve/v2/document/field_geopoint.go
@@ -180,6 +180,15 @@ func NewGeoPointField(name string, arrayPositions []uint64, lon, lat float64) *G
func NewGeoPointFieldWithIndexingOptions(name string, arrayPositions []uint64, lon, lat float64, options index.FieldIndexingOptions) *GeoPointField {
mhash := geo.MortonHash(lon, lat)
prefixCoded := numeric.MustNewPrefixCodedInt64(int64(mhash), 0)
+
+ // docvalues are always enabled for geopoint fields, even if the
+ // indexing options are set to not include docvalues.
+ // snappy compression and chunking are always skipped for geopoint
+ // to avoid mem copies and faster lookups.
+ options |= index.DocValues
+ options |= index.SkipDVChunking
+ options |= index.SkipDVCompression
+
return &GeoPointField{
name: name,
arrayPositions: arrayPositions,
diff --git a/vendor/github.com/blevesearch/bleve/v2/document/field_geoshape.go b/vendor/github.com/blevesearch/bleve/v2/document/field_geoshape.go
index 6282ff12b3..2eb7aa3f2b 100644
--- a/vendor/github.com/blevesearch/bleve/v2/document/field_geoshape.go
+++ b/vendor/github.com/blevesearch/bleve/v2/document/field_geoshape.go
@@ -180,7 +180,11 @@ func NewGeoShapeFieldFromShapeWithIndexingOptions(name string, arrayPositions []
// docvalues are always enabled for geoshape fields, even if the
// indexing options are set to not include docvalues.
+ // snappy compression and chunking are always skipped for geoshape
+ // to avoid mem copies and faster lookups.
options |= index.DocValues
+ options |= index.SkipDVChunking
+ options |= index.SkipDVCompression
return &GeoShapeField{
shape: shape,
@@ -232,7 +236,11 @@ func NewGeometryCollectionFieldFromShapesWithIndexingOptions(name string,
// docvalues are always enabled for geoshape fields, even if the
// indexing options are set to not include docvalues.
+ // snappy compression and chunking are always skipped for geoshape
+ // to avoid mem copies and faster lookups.
options |= index.DocValues
+ options |= index.SkipDVChunking
+ options |= index.SkipDVCompression
return &GeoShapeField{
shape: shape,
diff --git a/vendor/github.com/blevesearch/bleve/v2/document/field_vector.go b/vendor/github.com/blevesearch/bleve/v2/document/field_vector.go
index 4c20013c77..136b79bbcb 100644
--- a/vendor/github.com/blevesearch/bleve/v2/document/field_vector.go
+++ b/vendor/github.com/blevesearch/bleve/v2/document/field_vector.go
@@ -114,6 +114,13 @@ func NewVectorFieldWithIndexingOptions(name string, arrayPositions []uint64,
// skip freq/norms for vector field
options |= index.SkipFreqNorm
+ // bivf-sq8 indexes only supports hamming distance for the primary
+ // binary index. Similarity here is used for the backing flat index,
+ // which is set to cosine similarity for recall reasons
+ if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) {
+ similarity = index.CosineSimilarity
+ }
+
return &VectorField{
name: name,
dims: dims,
diff --git a/vendor/github.com/blevesearch/bleve/v2/error.go b/vendor/github.com/blevesearch/bleve/v2/error.go
index b57a61543d..e5ffe07e97 100644
--- a/vendor/github.com/blevesearch/bleve/v2/error.go
+++ b/vendor/github.com/blevesearch/bleve/v2/error.go
@@ -28,6 +28,7 @@ const (
ErrorIndexReadInconsistency
ErrorTwoPhaseSearchInconsistency
ErrorSynonymSearchNotSupported
+ ErrorTrainingNotSupported
)
// Error represents a more strongly typed bleve error for detecting
diff --git a/vendor/github.com/blevesearch/bleve/v2/index.go b/vendor/github.com/blevesearch/bleve/v2/index.go
index 2f1ba5fbf1..889da1faea 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index.go
@@ -389,6 +389,11 @@ type SynonymIndex interface {
IndexSynonym(id string, collection string, definition *SynonymDefinition) error
}
+type IndexWithCallbacks interface {
+ FileWriterIDsInUse() (map[string]struct{}, error)
+ DropFileWriterIDs(ids map[string]struct{}) error
+}
+
type InsightsIndex interface {
Index
// TermFrequencies returns the tokens ordered by frequencies for the field index.
@@ -396,3 +401,13 @@ type InsightsIndex interface {
// CentroidCardinalities returns the centroids (clusters) from IVF indexes ordered by data density.
CentroidCardinalities(field string, limit int, desceding bool) ([]index.CentroidCardinality, error)
}
+
+type TrainableIndex interface {
+ Index
+ Train(*Batch) error
+}
+
+type IndexFileCopyable interface {
+ SetPathInBolt(key []byte, value []byte) error //dest index
+ CopyFile(file string, d index.IndexDirectory) error // source index
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/builder.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/builder.go
index d4d8e9c075..bd78f51e99 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/builder.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/builder.go
@@ -20,9 +20,9 @@ import (
"sync"
"github.com/RoaringBitmap/roaring/v2"
+ "github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
segment "github.com/blevesearch/scorch_segment_api/v2"
- bolt "go.etcd.io/bbolt"
)
const DefaultBuilderBatchSize = 1000
@@ -291,7 +291,7 @@ func (o *Builder) Close() error {
// create the root bolt
rootBoltPath := o.path + string(os.PathSeparator) + "root.bolt"
- rootBolt, err := bolt.Open(rootBoltPath, 0600, nil)
+ rootBolt, err := util.OpenBolt(rootBoltPath, 0600, nil)
if err != nil {
return err
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/introducer.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/introducer.go
index cb11d5072d..5f0ea19952 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/introducer.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/introducer.go
@@ -154,22 +154,24 @@ func (s *Scorch) introduceSegment(next *segmentIntroduction) error {
cachedDocs: root.segment[i].cachedDocs,
cachedMeta: root.segment[i].cachedMeta,
creator: root.segment[i].creator,
+ internal: root.segment[i].internal,
}
// apply new obsoletions
if root.segment[i].deleted == nil {
newss.deleted = delta
} else {
- if delta.IsEmpty() {
- newss.deleted = root.segment[i].deleted
- } else {
- newss.deleted = roaring.Or(root.segment[i].deleted, delta)
- }
+ newss.deleted = roaring.Or(root.segment[i].deleted, delta)
}
if newss.deleted.IsEmpty() {
newss.deleted = nil
}
+ // update the deleted bitmap to include any nested/sub-documents as well
+ // if the segment supports that
+ if ns, ok := newss.segment.(segment.NestedSegment); ok {
+ newss.deleted = ns.AddNestedDocuments(newss.deleted)
+ }
// check for live size before copying
if newss.LiveSize() > 0 {
newSnapshot.segment = append(newSnapshot.segment, newss)
@@ -201,6 +203,7 @@ func (s *Scorch) introduceSegment(next *segmentIntroduction) error {
stats: next.stats,
cachedDocs: &cachedDocs{cache: nil},
cachedMeta: &cachedMeta{meta: nil},
+ internal: make(map[string][]byte),
creator: "introduceSegment",
}
newSnapshot.segment = append(newSnapshot.segment, newSegmentSnapshot)
@@ -210,6 +213,12 @@ func (s *Scorch) introduceSegment(next *segmentIntroduction) error {
// queued for persistence.
atomic.AddUint64(&s.stats.TotIntroducedItems, newSegmentSnapshot.Count())
atomic.AddUint64(&s.stats.TotIntroducedSegmentsBatch, 1)
+
+ // track the internal values of this segment so that when we update the
+ // bolt we keep the internal values in sync with the segments on disk, and
+ // if this segment didn't get persisted we need to undo that info from the
+ // indexSnapshot's internal map as part of the bolt update.
+ newSegmentSnapshot.internal = next.internal
}
// copy old values
for key, oldVal := range root.internal {
@@ -398,6 +407,7 @@ func (s *Scorch) introduceMerge(nextMerge *segmentMerge) {
cachedDocs: root.segment[i].cachedDocs,
cachedMeta: root.segment[i].cachedMeta,
creator: root.segment[i].creator,
+ internal: root.segment[i].internal,
})
root.segment[i].segment.AddRef()
newSnapshot.offsets = append(newSnapshot.offsets, running)
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/merge.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/merge.go
index e17288410e..0101dd1e36 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/merge.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/merge.go
@@ -31,6 +31,19 @@ import (
const merger = "merger"
+// used in the context of mergerCtrl to provide a way to verify
+// the completion of a merge operation
+const mergeDoneKey = "mergeDone"
+
+type mergeDoneChan chan error
+
+// used in the context of mergerCtrl to provide a way to use
+// a custom merge plan instead of the one generated by the
+// default merge planner
+const mergePlanFuncKey = "mergePlanFunc"
+
+type mergePlanFunc func(*IndexSnapshot) (*mergeplan.MergePlan, error)
+
func (s *Scorch) mergerLoop() {
defer func() {
if r := recover(); r != nil {
@@ -95,11 +108,9 @@ OUTER:
continue OUTER
}
- startTime := time.Now()
-
// lets get started
- err := s.planMergeAtSnapshot(ctrlMsg.ctx, ctrlMsg.options,
- ourSnapshot)
+ startTime := time.Now()
+ err := s.planMergeAtSnapshot(ctrlMsg, ourSnapshot)
if err != nil {
atomic.StoreUint64(&s.iStats.mergeEpoch, 0)
if err == segment.ErrClosed {
@@ -286,42 +297,64 @@ func (w *closeChWrapper) listen() {
}
}
-func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
- options *mergeplan.MergePlanOptions, ourSnapshot *IndexSnapshot) error {
- // build list of persisted segments in this snapshot
- var onlyPersistedSnapshots []mergeplan.Segment
- for _, segmentSnapshot := range ourSnapshot.segment {
- if _, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok {
- onlyPersistedSnapshots = append(onlyPersistedSnapshots, segmentSnapshot)
+// planMergeAtSnapshot plans and executes the merge operations for a given snapshot
+// if there is a custom merge plan function provided, it uses that to get the merge plan
+// otherwise, it builds the merge plan using the default planner and executes the merge tasks in the plan.
+func (s *Scorch) planMergeAtSnapshot(ctrlMsg *mergerCtrl, ourSnapshot *IndexSnapshot) error {
+ var mergePlan *mergeplan.MergePlan
+ // if a merge plan function is provided in the context, use it to get the merge plan
+ if mergePlanFunc, ok := ctrlMsg.ctx.Value(mergePlanFuncKey).(mergePlanFunc); ok {
+ var err error
+ mergePlan, err = mergePlanFunc(ourSnapshot)
+ if err != nil {
+ atomic.AddUint64(&s.stats.TotFileMergePlanErr, 1)
+ return fmt.Errorf("merge planning err: %v", err)
}
}
- atomic.AddUint64(&s.stats.TotFileMergePlan, 1)
+ // default to making a merge plan if a custom one is not provided
+ if mergePlan == nil {
+ // build list of persisted segments in this snapshot
+ var onlyPersistedSnapshots []mergeplan.Segment
+ for _, segmentSnapshot := range ourSnapshot.segment {
+ if _, ok := segmentSnapshot.segment.(segment.PersistedSegment); ok {
+ onlyPersistedSnapshots = append(onlyPersistedSnapshots, segmentSnapshot)
+ }
+ }
- // give this list to the planner
- resultMergePlan, err := mergeplan.Plan(onlyPersistedSnapshots, options)
- if err != nil {
- atomic.AddUint64(&s.stats.TotFileMergePlanErr, 1)
- return fmt.Errorf("merge planning err: %v", err)
- }
- if resultMergePlan == nil {
- // nothing to do
- atomic.AddUint64(&s.stats.TotFileMergePlanNone, 1)
- return nil
+ atomic.AddUint64(&s.stats.TotFileMergePlan, 1)
+
+ // give this list to the planner
+ var err error
+ mergePlan, err = mergeplan.Plan(onlyPersistedSnapshots, ctrlMsg.options)
+ if err != nil {
+ atomic.AddUint64(&s.stats.TotFileMergePlanErr, 1)
+ return fmt.Errorf("merge planning err: %v", err)
+ }
+ if mergePlan == nil {
+ // nothing to do
+ atomic.AddUint64(&s.stats.TotFileMergePlanNone, 1)
+ return nil
+ }
}
+
atomic.AddUint64(&s.stats.TotFileMergePlanOk, 1)
+ atomic.AddUint64(&s.stats.TotFileMergePlanTasks, uint64(len(mergePlan.Tasks)))
- atomic.AddUint64(&s.stats.TotFileMergePlanTasks, uint64(len(resultMergePlan.Tasks)))
-
- // process tasks in serial for now
- var filenames []string
-
- cw := newCloseChWrapper(s.closeCh, ctx)
+ cw := newCloseChWrapper(s.closeCh, ctrlMsg.ctx)
defer cw.close()
-
go cw.listen()
- for _, task := range resultMergePlan.Tasks {
+ var filenames []string
+ var err error
+ defer func() {
+ // send error to done channel if present
+ if done, ok := cw.ctx.Value(mergeDoneKey).(chan error); ok {
+ done <- err
+ }
+ }()
+
+ for _, task := range mergePlan.Tasks {
if len(task.Segments) == 0 {
atomic.AddUint64(&s.stats.TotFileMergePlanTasksSegmentsEmpty, 1)
continue
@@ -329,7 +362,6 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
atomic.AddUint64(&s.stats.TotFileMergePlanTasksSegments, uint64(len(task.Segments)))
- oldMap := make(map[uint64]*SegmentSnapshot, len(task.Segments))
newSegmentID := atomic.AddUint64(&s.nextSegmentID, 1)
segmentsToMerge := make([]segment.Segment, 0, len(task.Segments))
docsToDrop := make([]*roaring.Bitmap, 0, len(task.Segments))
@@ -337,7 +369,6 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
for _, planSegment := range task.Segments {
if segSnapshot, ok := planSegment.(*SegmentSnapshot); ok {
- oldMap[segSnapshot.id] = segSnapshot
mergedSegHistory[segSnapshot.id] = &mergedSegmentHistory{
workerID: 0,
oldSegment: segSnapshot,
@@ -345,7 +376,6 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
if persistedSeg, ok := segSnapshot.segment.(segment.PersistedSegment); ok {
if segSnapshot.LiveSize() == 0 {
atomic.AddUint64(&s.stats.TotFileMergeSegmentsEmpty, 1)
- oldMap[segSnapshot.id] = nil
delete(mergedSegHistory, segSnapshot.id)
} else {
segmentsToMerge = append(segmentsToMerge, segSnapshot.segment)
@@ -372,8 +402,9 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
atomic.AddUint64(&s.stats.TotFileMergeZapBeg, 1)
prevBytesReadTotal := cumulateBytesRead(segmentsToMerge)
- newDocNums, _, err := s.segPlugin.Merge(segmentsToMerge, docsToDrop, path,
- cw.cancelCh, s)
+ var newDocNums [][]uint64
+ newDocNums, _, err = s.segPlugin.MergeUsing(segmentsToMerge, docsToDrop, path,
+ cw.cancelCh, s, s.segmentConfig)
atomic.AddUint64(&s.stats.TotFileMergeZapEnd, 1)
fileMergeZapTime := uint64(time.Since(fileMergeZapStartTime))
@@ -391,7 +422,7 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
return fmt.Errorf("merging failed: %v", err)
}
- seg, err = s.segPlugin.Open(path)
+ seg, err = s.segPlugin.OpenUsing(path, s.segmentConfig)
if err != nil {
s.unmarkIneligibleForRemoval(filename)
atomic.AddUint64(&s.stats.TotFileMergePlanTasksErr, 1)
@@ -425,7 +456,8 @@ func (s *Scorch) planMergeAtSnapshot(ctx context.Context,
select {
case <-s.closeCh:
_ = seg.Close()
- return segment.ErrClosed
+ err = segment.ErrClosed
+ return err
case s.merges <- sm:
atomic.AddUint64(&s.stats.TotFileMergeIntroductions, 1)
}
@@ -540,7 +572,7 @@ func (s *Scorch) mergeAndPersistInMemorySegments(snapshot *IndexSnapshot,
// the newly merged segment is already flushed out to disk, just needs
// to be opened using mmap.
newDocIDs, _, err :=
- s.segPlugin.Merge(segsBatch, dropsBatch, path, s.closeCh, s)
+ s.segPlugin.MergeUsing(segsBatch, dropsBatch, path, s.closeCh, s, s.segmentConfig)
if err != nil {
em.Lock()
errs = append(errs, err)
@@ -555,7 +587,7 @@ func (s *Scorch) mergeAndPersistInMemorySegments(snapshot *IndexSnapshot,
s.markIneligibleForRemoval(filename)
newMergedSegmentIDs[id] = newSegmentID
newDocIDsSet[id] = newDocIDs
- newMergedSegments[id], err = s.segPlugin.Open(path)
+ newMergedSegments[id], err = s.segPlugin.OpenUsing(path, s.segmentConfig)
if err != nil {
em.Lock()
errs = append(errs, err)
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize.go
index 20a0706ef9..658fb08dd6 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize.go
@@ -395,5 +395,7 @@ func (i *IndexSnapshot) unadornedTermFieldReader(
recycle: false,
// signal downstream that this is a special unadorned termFieldReader
unadorned: true,
+ // unadorned TFRs do not require bytes read tracking
+ updateBytesRead: false,
}
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize_knn.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize_knn.go
index affb4ff13b..29f80b4ccb 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize_knn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/optimize_knn.go
@@ -34,8 +34,6 @@ type OptimizeVR struct {
totalCost uint64
// maps field to vector readers
vrs map[string][]*IndexSnapshotVectorReader
- // if at least one of the vector readers requires filtered kNN.
- requiresFiltering bool
}
// This setting _MUST_ only be changed during init and not after.
@@ -85,8 +83,7 @@ func (o *OptimizeVR) Finish() error {
continue
}
- vecIndex, err := segment.InterpretVectorIndex(field,
- o.requiresFiltering, origSeg.deleted)
+ vecIndex, err := segment.InterpretVectorIndex(field, origSeg.deleted)
if err != nil {
errorsM.Lock()
errors = append(errors, err)
@@ -109,7 +106,7 @@ func (o *OptimizeVR) Finish() error {
// kNN search.
if vr.eligibleSelector != nil {
pl, err = vecIndex.SearchWithFilter(vr.vector, vr.k,
- vr.eligibleSelector.SegmentEligibleDocs(index), vr.searchParams)
+ vr.eligibleSelector.SegmentEligibleDocuments(index), vr.searchParams)
} else {
pl, err = vecIndex.Search(vr.vector, vr.k, vr.searchParams)
}
@@ -163,9 +160,6 @@ func (s *IndexSnapshotVectorReader) VectorOptimize(ctx context.Context,
return octx, nil
}
o.ctx = ctx
- if !o.requiresFiltering {
- o.requiresFiltering = s.eligibleSelector != nil
- }
if o.snapshot != s.snapshot {
o.invokeSearcherEndCallback()
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/persister.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/persister.go
index d0c013a1d4..6b7eee5963 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/persister.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/persister.go
@@ -425,7 +425,6 @@ func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persiste
var totSize int
var numSegsToFlushOut int
var totDocs uint64
-
// legacy behaviour of merge + flush of all in-memory segments in one-shot
if legacyFlushBehaviour(po.MaxSizeInMemoryMergePerWorker, po.NumPersisterWorkers) {
val := &flushable{
@@ -538,10 +537,15 @@ func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persiste
exclude := make(map[uint64]struct{})
// copy to the equiv the segments that weren't replaced
- for _, segment := range snapshot.segment {
- if _, wasMerged := mergedSegmentIDs[segment.id]; !wasMerged {
- equiv.segment = append(equiv.segment, segment)
- exclude[segment.id] = struct{}{}
+ for _, ss := range snapshot.segment {
+ if _, wasMerged := mergedSegmentIDs[ss.id]; !wasMerged {
+ equiv.segment = append(equiv.segment, ss)
+ // this can be either in-memory or persisted segment, but while
+ // preparing the bolt snapshot we avoid the in-memory segments to be
+ // flushed out
+ if _, ok := ss.segment.(segment.PersistedSegment); !ok {
+ exclude[ss.id] = struct{}{}
+ }
}
}
@@ -549,10 +553,11 @@ func (s *Scorch) persistSnapshotMaybeMerge(snapshot *IndexSnapshot, po *persiste
for _, segment := range newSnapshot.segment {
if _, ok := newMergedSegmentIDs[segment.id]; ok {
equiv.segment = append(equiv.segment, &SegmentSnapshot{
- id: segment.id,
- segment: segment.segment,
- deleted: nil, // nil since merging handled deletions
- stats: nil,
+ id: segment.id,
+ segment: segment.segment,
+ deleted: nil, // nil since merging handled deletions
+ stats: nil,
+ internal: nil, // segment is persisted and equiv is already updated
})
}
}
@@ -575,6 +580,11 @@ func copyToDirectory(srcPath string, d index.Directory) (int64, error) {
return 0, fmt.Errorf("GetWriter err: %v", err)
}
+ // skip
+ if dest == nil {
+ return 0, nil
+ }
+
sourceFileStat, err := os.Stat(srcPath)
if err != nil {
return 0, err
@@ -616,9 +626,8 @@ func persistToDirectory(seg segment.UnpersistedSegment, d index.Directory,
return err
}
-func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
- segPlugin SegmentPlugin, exclude map[uint64]struct{}, d index.Directory) (
- []string, map[uint64]string, error) {
+func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *util.BoltTxImpl, path string, segPlugin SegmentPlugin,
+ exclude map[uint64]struct{}, d index.Directory) ([]string, map[uint64]string, error) {
snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
if err != nil {
return nil, nil, err
@@ -634,13 +643,29 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, err
}
- err = metaBucket.Put(util.BoltMetaDataSegmentTypeKey, []byte(segPlugin.Type()))
+ err = metaBucket.Put(util.BoltMetaDataSegmentTypeKey, []byte(segPlugin.Type()), nil)
if err != nil {
return nil, nil, err
}
buf := make([]byte, binary.MaxVarintLen32)
binary.BigEndian.PutUint32(buf, segPlugin.Version())
- err = metaBucket.Put(util.BoltMetaDataSegmentVersionKey, buf)
+ err = metaBucket.Put(util.BoltMetaDataSegmentVersionKey, buf, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ // always obtain the path from the parent snapshot if available
+ // since that is the primary source of truth for context
+ if snapshot.parent != nil {
+ path = snapshot.parent.path
+ }
+ writer, err := util.NewFileWriter(
+ []byte(path + string(os.PathSeparator) + "root.bolt"))
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // persist the writer ID used for the bolt snapshot
+ err = metaBucket.Put(util.BoltMetaDataFileWriterIDKey, []byte(writer.Id()), writer)
if err != nil {
return nil, nil, err
}
@@ -654,7 +679,7 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, err
}
- err = metaBucket.Put(util.BoltMetaDataTimeStamp, timeStampBinary)
+ err = metaBucket.Put(util.BoltMetaDataTimeStamp, timeStampBinary, writer)
if err != nil {
return nil, nil, err
}
@@ -664,19 +689,19 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
if err != nil {
return nil, nil, err
}
- // TODO optimize writing these in order?
+
+ // deep copy the internal map since we'll be keeping only the persisted info
+ // in bolt and some of the information might be deleted
+ internal := make(map[string][]byte, len(snapshot.internal))
for k, v := range snapshot.internal {
- err = internalBucket.Put([]byte(k), v)
- if err != nil {
- return nil, nil, err
- }
+ internal[k] = v
}
if snapshot.parent != nil {
val := make([]byte, 8)
bytesWritten := atomic.LoadUint64(&snapshot.parent.stats.TotBytesWrittenAtIndexTime)
binary.LittleEndian.PutUint64(val, bytesWritten)
- err = internalBucket.Put(util.TotBytesWrittenKey, val)
+ err = internalBucket.Put(util.TotBytesWrittenKey, val, writer)
if err != nil {
return nil, nil, err
}
@@ -687,79 +712,112 @@ func prepareBoltSnapshot(snapshot *IndexSnapshot, tx *bolt.Tx, path string,
// first ensure that each segment in this snapshot has been persisted
for _, segmentSnapshot := range snapshot.segment {
- snapshotSegmentKey := encodeUvarintAscending(nil, segmentSnapshot.id)
- snapshotSegmentBucket, err := snapshotBucket.CreateBucketIfNotExists(snapshotSegmentKey)
- if err != nil {
- return nil, nil, err
- }
+ var persistedSeg bool
+ var snapshotSegmentBucket *util.BoltBucketImpl
switch seg := segmentSnapshot.segment.(type) {
case segment.PersistedSegment:
+ snapshotSegmentKey := encodeUvarintAscending(nil, segmentSnapshot.id)
+ snapshotSegmentBucket, err = snapshotBucket.CreateBucketIfNotExists(snapshotSegmentKey)
+ if err != nil {
+ return nil, nil, err
+ }
segPath := seg.Path()
_, err = copyToDirectory(segPath, d)
if err != nil {
return nil, nil, fmt.Errorf("segment: %s copy err: %v", segPath, err)
}
filename := filepath.Base(segPath)
- err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename))
+ err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename), writer)
if err != nil {
return nil, nil, err
}
filenames = append(filenames, filename)
+ persistedSeg = true
case segment.UnpersistedSegment:
// need to persist this to disk if its not part of exclude list (which
// restricts which in-memory segment to be persisted to disk)
if _, ok := exclude[segmentSnapshot.id]; !ok {
+ snapshotSegmentKey := encodeUvarintAscending(nil, segmentSnapshot.id)
+ snapshotSegmentBucket, err = snapshotBucket.CreateBucketIfNotExists(snapshotSegmentKey)
+ if err != nil {
+ return nil, nil, err
+ }
filename := zapFileName(segmentSnapshot.id)
path := filepath.Join(path, filename)
- err := persistToDirectory(seg, d, path)
+ err = persistToDirectory(seg, d, path)
if err != nil {
return nil, nil, fmt.Errorf("segment: %s persist err: %v", path, err)
}
newSegmentPaths[segmentSnapshot.id] = path
- err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename))
+ err = snapshotSegmentBucket.Put(util.BoltPathKey, []byte(filename), nil)
if err != nil {
return nil, nil, err
}
filenames = append(filenames, filename)
+ persistedSeg = true
+ } else {
+ // this segment is not going to be persisted in this cycle, so any
+ // of the corresponding internal values need to be removed since
+ // on recovery they shouldn't be loaded as part of the indexSnapshot
+ for k, v := range segmentSnapshot.internal {
+ if v != nil {
+ delete(internal, k)
+ }
+ }
}
default:
return nil, nil, fmt.Errorf("unknown segment type: %T", seg)
}
- // store current deleted bits
- var roaringBuf bytes.Buffer
- if segmentSnapshot.deleted != nil {
- _, err = segmentSnapshot.deleted.WriteTo(&roaringBuf)
- if err != nil {
- return nil, nil, fmt.Errorf("error persisting roaring bytes: %v", err)
+
+ // if the segment was excluded from persistence, then skip updating the metadata
+ // or helper data corresponding to it - we need to keep things in-line with
+ // the on-disk information
+ if persistedSeg {
+ // store current deleted bits
+ var roaringBuf bytes.Buffer
+ if segmentSnapshot.deleted != nil {
+ _, err = segmentSnapshot.deleted.WriteTo(&roaringBuf)
+ if err != nil {
+ return nil, nil, fmt.Errorf("error persisting roaring bytes: %v", err)
+ }
+ err = snapshotSegmentBucket.Put(util.BoltDeletedKey, roaringBuf.Bytes(), writer)
+ if err != nil {
+ return nil, nil, err
+ }
}
- err = snapshotSegmentBucket.Put(util.BoltDeletedKey, roaringBuf.Bytes())
- if err != nil {
- return nil, nil, err
+
+ // store segment stats
+ if segmentSnapshot.stats != nil {
+ statsBytes, err := json.Marshal(segmentSnapshot.stats.Fetch())
+ if err != nil {
+ return nil, nil, err
+ }
+ err = snapshotSegmentBucket.Put(util.BoltStatsKey, statsBytes, writer)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // store updated field info
+ if segmentSnapshot.updatedFields != nil {
+ updatedFieldsBytes, err := json.Marshal(segmentSnapshot.updatedFields)
+ if err != nil {
+ return nil, nil, err
+ }
+ err = snapshotSegmentBucket.Put(
+ util.BoltUpdatedFieldsKey, updatedFieldsBytes, writer)
+ if err != nil {
+ return nil, nil, err
+ }
}
}
+ }
- // store segment stats
- if segmentSnapshot.stats != nil {
- b, err := json.Marshal(segmentSnapshot.stats.Fetch())
- if err != nil {
- return nil, nil, err
- }
- err = snapshotSegmentBucket.Put(util.BoltStatsKey, b)
- if err != nil {
- return nil, nil, err
- }
- }
-
- // store updated field info
- if segmentSnapshot.updatedFields != nil {
- b, err := json.Marshal(segmentSnapshot.updatedFields)
- if err != nil {
- return nil, nil, err
- }
- err = snapshotSegmentBucket.Put(util.BoltUpdatedFieldsKey, b)
- if err != nil {
- return nil, nil, err
- }
+ // now the internal values are reflective of the on-disk data, update in bolt
+ for k, v := range internal {
+ err = internalBucket.Put([]byte(k), v, writer)
+ if err != nil {
+ return nil, nil, err
}
}
@@ -804,7 +862,7 @@ func (s *Scorch) persistSnapshotDirect(snapshot *IndexSnapshot, exclude map[uint
}
}()
for segmentID, path := range newSegmentPaths {
- newSegments[segmentID], err = s.segPlugin.Open(path)
+ newSegments[segmentID], err = s.segPlugin.OpenUsing(path, s.segmentConfig)
if err != nil {
return fmt.Errorf("error opening new segment at %s, %v", path, err)
}
@@ -854,9 +912,8 @@ func zapFileName(epoch uint64) string {
}
// bolt snapshot code
-
func (s *Scorch) loadFromBolt() error {
- err := s.rootBolt.View(func(tx *bolt.Tx) error {
+ err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
@@ -873,7 +930,7 @@ func (s *Scorch) loadFromBolt() error {
s.AddEligibleForRemoval(snapshotEpoch)
continue
}
- snapshot := snapshots.Bucket(k)
+ snapshot := snapshots.GetBucket(k)
if snapshot == nil {
log.Printf("snapshot key, but bucket missing %x, continuing", k)
s.AddEligibleForRemoval(snapshotEpoch)
@@ -904,6 +961,17 @@ func (s *Scorch) loadFromBolt() error {
foundRoot = true
}
+
+ // try init trainer and load the trained data
+ if trainer := initTrainer(s, s.config); trainer != nil {
+ s.trainer = trainer
+ trainerBucket := snapshots.GetBucket(util.BoltTrainerKey)
+ err := s.trainer.loadTrainedData(trainerBucket)
+ if err != nil {
+ return err
+ }
+ }
+
return nil
})
if err != nil {
@@ -921,13 +989,13 @@ func (s *Scorch) loadFromBolt() error {
// LoadSnapshot loads the segment with the specified epoch
// NOTE: this is currently ONLY intended to be used by the command-line tool
func (s *Scorch) LoadSnapshot(epoch uint64) (rv *IndexSnapshot, err error) {
- err = s.rootBolt.View(func(tx *bolt.Tx) error {
+ err = s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
snapshotKey := encodeUvarintAscending(nil, epoch)
- snapshot := snapshots.Bucket(snapshotKey)
+ snapshot := snapshots.GetBucket(snapshotKey)
if snapshot == nil {
return fmt.Errorf("snapshot with epoch: %v - doesn't exist", epoch)
}
@@ -940,7 +1008,7 @@ func (s *Scorch) LoadSnapshot(epoch uint64) (rv *IndexSnapshot, err error) {
return rv, nil
}
-func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
+func (s *Scorch) loadSnapshot(snapshot *util.BoltBucketImpl) (*IndexSnapshot, error) {
rv := &IndexSnapshot{
parent: s,
internal: make(map[string][]byte),
@@ -950,45 +1018,64 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
// first we look for the meta-data bucket, this will tell us
// which segment type/version was used for this snapshot
// all operations for this scorch will use this type/version
- metaBucket := snapshot.Bucket(util.BoltMetaDataKey)
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("meta-data bucket missing")
}
- segmentType := string(metaBucket.Get(util.BoltMetaDataSegmentTypeKey))
- segmentVersion := binary.BigEndian.Uint32(
- metaBucket.Get(util.BoltMetaDataSegmentVersionKey))
- err := s.loadSegmentPlugin(segmentType, segmentVersion)
+ segmentType, err := metaBucket.Get(util.BoltMetaDataSegmentTypeKey, nil)
+ if err != nil {
+ _ = rv.DecRef()
+ return nil, fmt.Errorf("segment type missing: %v", err)
+ }
+ segmentVersionBytes, err := metaBucket.Get(util.BoltMetaDataSegmentVersionKey, nil)
+ if err != nil {
+ _ = rv.DecRef()
+ return nil, fmt.Errorf("segment version missing: %v", err)
+ }
+ segmentVersion := binary.BigEndian.Uint32(segmentVersionBytes)
+ err = s.loadSegmentPlugin(string(segmentType), segmentVersion)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf(
"unable to load correct segment wrapper: %v", err)
}
+ fileWriterID, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ _ = rv.DecRef()
+ return nil, fmt.Errorf("file writer id missing: %v", err)
+ }
+ reader, err := util.NewFileReader(
+ string(fileWriterID), []byte(s.path+string(os.PathSeparator)+"root.bolt"))
+ if err != nil {
+ _ = rv.DecRef()
+ return nil, fmt.Errorf("unable to load correct reader: %v", err)
+ }
+
var running uint64
c := snapshot.Cursor()
for k, _ := c.First(); k != nil; k, _ = c.Next() {
if k[0] == util.BoltInternalKey[0] {
- internalBucket := snapshot.Bucket(k)
+ internalBucket := snapshot.GetBucket(k)
if internalBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("internal bucket missing")
}
err := internalBucket.ForEach(func(key []byte, val []byte) error {
- copiedVal := append([]byte(nil), val...)
- rv.internal[string(key)] = copiedVal
+ rv.internal[string(key)] = val
return nil
- })
+ }, reader)
if err != nil {
_ = rv.DecRef()
return nil, err
}
} else if k[0] != util.BoltMetaDataKey[0] {
- segmentBucket := snapshot.Bucket(k)
+ segmentBucket := snapshot.GetBucket(k)
if segmentBucket == nil {
_ = rv.DecRef()
return nil, fmt.Errorf("segment key, but bucket missing %x", k)
}
- segmentSnapshot, err := s.loadSegment(segmentBucket)
+ segmentSnapshot, err := s.loadSegment(segmentBucket, reader)
if err != nil {
_ = rv.DecRef()
return nil, fmt.Errorf("failed to load segment: %v", err)
@@ -1010,13 +1097,14 @@ func (s *Scorch) loadSnapshot(snapshot *bolt.Bucket) (*IndexSnapshot, error) {
return rv, nil
}
-func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, error) {
- pathBytes := segmentBucket.Get(util.BoltPathKey)
+func (s *Scorch) loadSegment(segmentBucket *util.BoltBucketImpl, reader util.FileReader) (
+ *SegmentSnapshot, error) {
+ pathBytes, err := segmentBucket.Get(util.BoltPathKey, nil)
if pathBytes == nil {
return nil, fmt.Errorf("segment path missing")
}
segmentPath := s.path + string(os.PathSeparator) + string(pathBytes)
- seg, err := s.segPlugin.Open(segmentPath)
+ seg, err := s.segPlugin.OpenUsing(segmentPath, s.segmentConfig)
if err != nil {
return nil, fmt.Errorf("error opening bolt segment: %v", err)
}
@@ -1026,7 +1114,11 @@ func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, erro
cachedDocs: &cachedDocs{cache: nil},
cachedMeta: &cachedMeta{meta: nil},
}
- deletedBytes := segmentBucket.Get(util.BoltDeletedKey)
+ deletedBytes, err := segmentBucket.Get(util.BoltDeletedKey, reader)
+ if err != nil {
+ _ = seg.Close()
+ return nil, fmt.Errorf("error getting deleted bytes: %v", err)
+ }
if deletedBytes != nil {
deletedBitmap := roaring.NewBitmap()
r := bytes.NewReader(deletedBytes)
@@ -1039,23 +1131,28 @@ func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, erro
rv.deleted = deletedBitmap
}
}
- statBytes := segmentBucket.Get(util.BoltStatsKey)
+ statBytes, err := segmentBucket.Get(util.BoltStatsKey, reader)
+ if err != nil {
+ _ = seg.Close()
+ return nil, fmt.Errorf("error getting stat bytes: %v", err)
+ }
if statBytes != nil {
var statsMap map[string]map[string]uint64
-
err := json.Unmarshal(statBytes, &statsMap)
- stats := &fieldStats{statMap: statsMap}
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading stat bytes: %v", err)
}
- rv.stats = stats
+ rv.stats = &fieldStats{statMap: statsMap}
+ }
+ updatedFieldBytes, err := segmentBucket.Get(util.BoltUpdatedFieldsKey, reader)
+ if err != nil {
+ _ = seg.Close()
+ return nil, fmt.Errorf("error getting updated field bytes: %v", err)
}
- updatedFieldBytes := segmentBucket.Get(util.BoltUpdatedFieldsKey)
if updatedFieldBytes != nil {
var updatedFields map[string]*index.UpdateFieldInfo
-
- err := json.Unmarshal(updatedFieldBytes, &updatedFields)
+ err = json.Unmarshal(updatedFieldBytes, &updatedFields)
if err != nil {
_ = seg.Close()
return nil, fmt.Errorf("error reading updated field bytes: %v", err)
@@ -1068,6 +1165,152 @@ func (s *Scorch) loadSegment(segmentBucket *bolt.Bucket) (*SegmentSnapshot, erro
return rv, nil
}
+// identify all the file callback writer ids that are in use by boltdb
+func (s *Scorch) boltFileWriterIDsInUse() (map[string]struct{}, error) {
+ idMap := make(map[string]struct{})
+ err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
+ snapshots := tx.Bucket(util.BoltSnapshotsBucket)
+ if snapshots == nil {
+ return nil
+ }
+ c := snapshots.Cursor()
+ for k, _ := c.First(); k != nil; k, _ = c.Next() {
+ snapshot := snapshots.GetBucket(k)
+ if snapshot == nil {
+ continue
+ }
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
+ if metaBucket == nil {
+ continue
+ }
+ id, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return err
+ }
+ idMap[string(id)] = struct{}{}
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ return idMap, nil
+}
+
+// remove all content in boltdb associated with the file callback
+// writer ids and process the data using the latest file writer
+func (s *Scorch) removeBoltFileWriterIDs(ids map[string]struct{}) error {
+ filePath := s.path + string(os.PathSeparator) + "root.bolt"
+ writer, err := util.NewFileWriter([]byte(filePath))
+ if err != nil {
+ return err
+ }
+
+ err = s.rootBolt.Update(func(tx *util.BoltTxImpl) error {
+ snapshots := tx.Bucket(util.BoltSnapshotsBucket)
+ if snapshots == nil {
+ return nil
+ }
+ c := snapshots.Cursor()
+ for k, _ := c.First(); k != nil; k, _ = c.Next() {
+ snapshot := snapshots.GetBucket(k)
+ if snapshot == nil {
+ continue
+ }
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
+ if metaBucket == nil {
+ continue
+ }
+ fileWriterIDBytes, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return err
+ }
+ fileWriterID := string(fileWriterIDBytes)
+ if _, ok := ids[fileWriterID]; ok {
+ reader, err := util.NewFileReader(fileWriterID, []byte(filePath))
+ if err != nil {
+ return fmt.Errorf("unable to load correct reader: %v", err)
+ }
+
+ cc := snapshot.Cursor()
+ for kk, _ := cc.First(); kk != nil; kk, _ = cc.Next() {
+ if kk[0] == util.BoltInternalKey[0] {
+ internalBucket := snapshot.GetBucket(kk)
+ if internalBucket == nil {
+ continue
+ }
+ // process all of the internal values and replace them with new values
+ internalBucketVals := make(map[string][]byte)
+ err := internalBucket.ForEach(func(key []byte, val []byte) error {
+ internalBucketVals[string(key)] = val
+ return nil
+ }, reader)
+ if err != nil {
+ return err
+ }
+ for key, val := range internalBucketVals {
+ err = internalBucket.Put([]byte(key), val, writer)
+ if err != nil {
+ return err
+ }
+ }
+ } else if kk[0] != util.BoltMetaDataKey[0] {
+ segmentBucket := snapshot.GetBucket(kk)
+ if segmentBucket == nil {
+ continue
+ }
+ // process the updated field key
+ updatedFieldBytes, err := segmentBucket.Get(util.BoltUpdatedFieldsKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting updated field bytes: %v", err)
+ }
+ if updatedFieldBytes != nil {
+ err = segmentBucket.Put(util.BoltUpdatedFieldsKey, updatedFieldBytes, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ // process the deleted key
+ deletedBytes, err := segmentBucket.Get(util.BoltDeletedKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting deleted bytes: %v", err)
+ }
+ if deletedBytes != nil {
+ err = segmentBucket.Put(util.BoltDeletedKey, deletedBytes, writer)
+ if err != nil {
+ return err
+ }
+ }
+ // process the stats key
+ statsBytes, err := segmentBucket.Get(util.BoltStatsKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting stats bytes: %v", err)
+ }
+ if statsBytes != nil {
+ err = segmentBucket.Put(util.BoltStatsKey, statsBytes, writer)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+ err = metaBucket.Put(util.BoltMetaDataFileWriterIDKey,
+ []byte(writer.Id()), writer)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
func (s *Scorch) removeOldData() {
removed, err := s.removeOldBoltSnapshots()
if err != nil {
@@ -1359,7 +1602,7 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
// for eg for n = 3 the checkpoints preserved should be tc, tc - d, tc - 2d
expirationDuration := time.Duration(s.numSnapshotsToKeep-1) * s.rollbackSamplingInterval
- err := s.rootBolt.View(func(tx *bolt.Tx) error {
+ err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
@@ -1380,15 +1623,18 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
continue
}
- snapshot := snapshots.Bucket(sk)
+ snapshot := snapshots.GetBucket(sk)
if snapshot == nil {
continue
}
- metaBucket := snapshot.Bucket(util.BoltMetaDataKey)
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
if metaBucket == nil {
continue
}
- timeStampBytes := metaBucket.Get(util.BoltMetaDataTimeStamp)
+ timeStampBytes, err := metaBucket.Get(util.BoltMetaDataTimeStamp, nil)
+ if err != nil {
+ continue
+ }
var timeStamp time.Time
err = timeStamp.UnmarshalText(timeStampBytes)
if err != nil {
@@ -1424,7 +1670,7 @@ func (s *Scorch) rootBoltSnapshotMetaData() ([]*snapshotMetaData, error) {
func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) {
var rv []uint64
- err := s.rootBolt.View(func(tx *bolt.Tx) error {
+ err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
@@ -1445,14 +1691,14 @@ func (s *Scorch) RootBoltSnapshotEpochs() ([]uint64, error) {
// Returns the *.zap file names that are listed in the rootBolt.
func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) {
rv := map[string]struct{}{}
- err := s.rootBolt.View(func(tx *bolt.Tx) error {
+ err := s.rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
}
sc := snapshots.Cursor()
for sk, _ := sc.First(); sk != nil; sk, _ = sc.Next() {
- snapshot := snapshots.Bucket(sk)
+ snapshot := snapshots.GetBucket(sk)
if snapshot == nil {
continue
}
@@ -1461,11 +1707,14 @@ func (s *Scorch) loadZapFileNames() (map[string]struct{}, error) {
if segk[0] == util.BoltInternalKey[0] {
continue
}
- segmentBucket := snapshot.Bucket(segk)
+ segmentBucket := snapshot.GetBucket(segk)
if segmentBucket == nil {
continue
}
- pathBytes := segmentBucket.Get(util.BoltPathKey)
+ pathBytes, err := segmentBucket.Get(util.BoltPathKey, nil)
+ if err != nil {
+ continue
+ }
if pathBytes == nil {
continue
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/rollback.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/rollback.go
index f047762fac..008b364082 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/rollback.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/rollback.go
@@ -44,7 +44,7 @@ func RollbackPoints(path string) ([]*RollbackPoint, error) {
rootBoltOpt := &bolt.Options{
ReadOnly: true,
}
- rootBolt, err := bolt.Open(rootBoltPath, 0600, rootBoltOpt)
+ rootBolt, err := util.OpenBolt(rootBoltPath, 0600, rootBoltOpt)
if err != nil || rootBolt == nil {
return nil, err
}
@@ -78,27 +78,40 @@ func RollbackPoints(path string) ([]*RollbackPoint, error) {
continue
}
- snapshot := snapshots.Bucket(k)
+ snapshot := snapshots.GetBucket(k)
if snapshot == nil {
log.Printf("RollbackPoints:"+
" snapshot key, but bucket missing %x, continuing", k)
continue
}
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
+ if metaBucket == nil {
+ return nil, fmt.Errorf("meta-data bucket missing")
+ }
+
+ fileWriterID, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return nil, fmt.Errorf("unable to load file writer ID: %v", err)
+ }
+ reader, err := util.NewFileReader(string(fileWriterID), []byte(rootBoltPath))
+ if err != nil {
+ return nil, fmt.Errorf("unable to load correct reader: %v", err)
+ }
+
meta := map[string][]byte{}
c2 := snapshot.Cursor()
for j, _ := c2.First(); j != nil; j, _ = c2.Next() {
if j[0] == util.BoltInternalKey[0] {
- internalBucket := snapshot.Bucket(j)
+ internalBucket := snapshot.GetBucket(j)
if internalBucket == nil {
err = fmt.Errorf("internal bucket missing")
break
}
err = internalBucket.ForEach(func(key []byte, val []byte) error {
- copiedVal := append([]byte(nil), val...)
- meta[string(key)] = copiedVal
+ meta[string(key)] = val
return nil
- })
+ }, reader)
if err != nil {
break
}
@@ -136,7 +149,7 @@ func Rollback(path string, to *RollbackPoint) error {
rootBoltOpt := &bolt.Options{
ReadOnly: false,
}
- rootBolt, err := bolt.Open(rootBoltPath, 0600, rootBoltOpt)
+ rootBolt, err := util.OpenBolt(rootBoltPath, 0600, rootBoltOpt)
if err != nil || rootBolt == nil {
return err
}
@@ -151,7 +164,7 @@ func Rollback(path string, to *RollbackPoint) error {
// including the target one.
var found bool
var eligibleEpochs []uint64
- err = rootBolt.View(func(tx *bolt.Tx) error {
+ err = rootBolt.View(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/scorch.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/scorch.go
index 287d8e07fd..4f35483d88 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/scorch.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/scorch.go
@@ -15,15 +15,19 @@
package scorch
import (
+ "context"
"encoding/json"
"fmt"
+ "io"
"os"
"path/filepath"
+ "strconv"
"sync"
"sync/atomic"
"time"
"github.com/RoaringBitmap/roaring/v2"
+ "github.com/blevesearch/bleve/v2/index/scorch/mergeplan"
"github.com/blevesearch/bleve/v2/registry"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
@@ -45,6 +49,7 @@ type Scorch struct {
readOnly bool
version uint8
config map[string]interface{}
+ segmentConfig map[string]interface{}
analysisQueue *index.AnalysisQueue
path string
@@ -75,9 +80,11 @@ type Scorch struct {
merges chan *segmentMerge
introducerNotifier chan *epochWatcher
persisterNotifier chan *epochWatcher
- rootBolt *bolt.DB
+ rootBolt *util.RootBoltImpl
asyncTasks sync.WaitGroup
+ trainer trainer
+
onEvent func(event Event) bool
onAsyncError func(err error, path string)
@@ -88,6 +95,33 @@ type Scorch struct {
spatialPlugin index.SpatialAnalyzerPlugin
}
+// trainer interface is used for training an index that has the concept
+// of "learning". Naturally, a vector index is one such thing that would
+// implement this interface. There can be multiple implementations of the
+// training itself even for the same index type.
+//
+// this component is not supposed to interact with the other master routines
+// of scorch and will be used only for training the index before the actual data
+// ingestion starts. The routine should also be released once the
+// training is marked as complete - which can be done using the BoltTrainCompleteKey
+// key and a bool value. However the struct is still maintained for the pointer to
+// the instance so that we can use in the later stages of the index lifecycle.
+type trainer interface {
+ // ephemeral
+ trainLoop()
+ // for the training state and the ingestion of the samples
+ train(batch *index.Batch) error
+
+ // to load the metadata from the bolt under the BoltTrainerKey
+ loadTrainedData(*util.BoltBucketImpl) error
+ // to fetch the internal data from the component
+ getInternal(key []byte) ([]byte, error)
+
+ // trainer specific file transfer operations
+ copyFileLOCKED(file string, d index.IndexDirectory) error
+ updateBolt(snapshotsBucket *util.BoltBucketImpl, key []byte, value []byte) error
+}
+
type ScorchErrorType string
func (t ScorchErrorType) Error() string {
@@ -154,6 +188,7 @@ func NewScorch(storeName string,
forceMergeRequestCh: make(chan *mergerCtrl, 1),
segPlugin: defaultSegmentPlugin,
copyScheduled: map[string]int{},
+ segmentConfig: make(map[string]interface{}),
}
forcedSegmentType, forcedSegmentVersion, err := configForceSegmentTypeVersion(config)
@@ -168,6 +203,11 @@ func NewScorch(storeName string,
}
}
+ segConfig, ok := config["segmentConfig"].(map[string]interface{})
+ if ok {
+ rv.segmentConfig = segConfig
+ }
+
typ, ok := config["spatialPlugin"].(string)
if ok {
if err := rv.loadSpatialAnalyzerPlugin(typ); err != nil {
@@ -205,6 +245,10 @@ func NewScorch(storeName string,
return nil, err
}
+ if trainer := initTrainer(rv, config); trainer != nil {
+ rv.trainer = trainer
+ }
+
return rv, nil
}
@@ -259,6 +303,11 @@ func (s *Scorch) Open() error {
s.asyncTasks.Add(1)
go s.introducerLoop()
+ if s.trainer != nil {
+ s.asyncTasks.Add(1)
+ go s.trainer.trainLoop()
+ }
+
if !s.readOnly && s.path != "" {
s.asyncTasks.Add(1)
go s.persisterLoop()
@@ -312,7 +361,7 @@ func (s *Scorch) openBolt() error {
rootBoltPath := s.path + string(os.PathSeparator) + "root.bolt"
var err error
if s.path != "" {
- s.rootBolt, err = bolt.Open(rootBoltPath, 0o600, &rootBoltOpt)
+ s.rootBolt, err = util.OpenBolt(rootBoltPath, 0o600, &rootBoltOpt)
if err != nil {
return err
}
@@ -424,6 +473,24 @@ func (s *Scorch) Delete(id string) error {
return s.Batch(b)
}
+func (s *Scorch) isTrained(batch *index.Batch) (bool, error) {
+ trained := true
+ if len(batch.IndexOps) > 0 && s.trainer != nil {
+ val, err := s.getInternal(util.BoltTrainCompleteKey)
+ if err != nil {
+ return false, err
+ }
+
+ if val != nil {
+ trained, err = strconv.ParseBool(string(val))
+ if err != nil {
+ return false, err
+ }
+ }
+ }
+ return trained, nil
+}
+
// Batch applices a batch of changes to the index atomically
func (s *Scorch) Batch(batch *index.Batch) (err error) {
start := time.Now()
@@ -434,6 +501,15 @@ func (s *Scorch) Batch(batch *index.Batch) (err error) {
s.fireEvent(EventKindBatchIntroduction, time.Since(start))
}()
+ trained, err := s.isTrained(batch)
+ if err != nil {
+ return err
+ }
+
+ if !trained {
+ return fmt.Errorf("index is not trained yet")
+ }
+
resultChan := make(chan index.Document, len(batch.IndexOps))
var numUpdates uint64
@@ -497,7 +573,7 @@ func (s *Scorch) Batch(batch *index.Batch) (err error) {
stats := newFieldStats()
if len(analysisResults) > 0 {
- newSegment, bufBytes, err = s.segPlugin.New(analysisResults)
+ newSegment, bufBytes, err = s.segPlugin.NewUsing(analysisResults, s.segmentConfig)
if err != nil {
return err
}
@@ -532,6 +608,29 @@ func (s *Scorch) Batch(batch *index.Batch) (err error) {
return err
}
+func (s *Scorch) getInternal(key []byte) ([]byte, error) {
+ s.rootLock.RLock()
+ defer s.rootLock.RUnlock()
+
+ switch string(key) {
+ case string(util.BoltTrainCompleteKey):
+ if s.trainer != nil {
+ return s.trainer.getInternal(key)
+ } else {
+ return nil, fmt.Errorf("get on BoltTrainCompleteKey is not supported" +
+ " with this build")
+ }
+ }
+ return nil, nil
+}
+
+func (s *Scorch) Train(batch *index.Batch) error {
+ if s.trainer != nil {
+ return s.trainer.train(batch)
+ }
+ return fmt.Errorf("training is not supported with this build")
+}
+
func (s *Scorch) prepareSegment(newSegment segment.Segment, ids []string,
internalOps map[string][]byte, persistedCallback index.BatchCallback, stats *fieldStats,
) error {
@@ -741,6 +840,20 @@ func (s *Scorch) StatsMap() map[string]interface{} {
m["field:"+fieldName+":"+statName] = val
}
}
+
+ aggVectorStats := newFieldStats()
+ for _, segmentSnapshot := range indexSnapshot.Segments() {
+ if vsr, ok := segmentSnapshot.Segment().(segment.VectorFieldStatsReporter); ok {
+ segStats := newFieldStats()
+ vsr.UpdateVectorFieldStats(segStats)
+ aggVectorStats.Aggregate(segStats)
+ }
+ }
+ for statName, stats := range aggVectorStats.Fetch() {
+ for fieldName, val := range stats {
+ m["field:"+fieldName+":"+statName] = val
+ }
+ }
return m
}
@@ -799,6 +912,12 @@ func analyze(d index.Document, fn customAnalyzerPluginInitFunc) {
}
}
})
+ if nd, ok := d.(index.NestedDocument); ok {
+ nd.VisitNestedDocuments(func(doc index.Document) {
+ doc.AddIDField()
+ analyze(doc, fn)
+ })
+ }
}
func (s *Scorch) AddEligibleForRemoval(epoch uint64) {
@@ -971,6 +1090,65 @@ func (s *Scorch) CopyReader() index.CopyReader {
return rv
}
+func (s *Scorch) SetPathInBolt(key []byte, value []byte) error {
+ tx, err := s.rootBolt.Begin(true)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if err != nil {
+ _ = tx.Rollback()
+ }
+ }()
+
+ snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
+ if err != nil {
+ return err
+ }
+
+ // currently this is specific to trained index file update
+ err = s.trainer.updateBolt(snapshotsBucket, key, value)
+ if err != nil {
+ return err
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return err
+ }
+
+ return s.rootBolt.Sync()
+}
+
+// CopyFile copies a specific file to a destination directory which has an access to a bleve index
+// doing a io.Copy() isn't enough because the file needs to be tracked in bolt file as well
+func (s *Scorch) CopyFile(file string, d index.IndexDirectory) error {
+ s.rootLock.Lock()
+ defer s.rootLock.Unlock()
+
+ dest, err := d.GetWriter(filepath.Join("store", file))
+ if err != nil {
+ return err
+ }
+
+ source, err := os.Open(filepath.Join(s.path, file))
+ if err != nil {
+ return err
+ }
+
+ defer source.Close()
+ defer dest.Close()
+ _, err = io.Copy(dest, source)
+ if err != nil {
+ return err
+ }
+
+ // this code is currently specific to copying trained data but is future proofed for other files
+ // to be updated in the dest's bolt
+ err = s.trainer.copyFileLOCKED(file, d)
+ return err
+}
+
// external API to fire a scorch event (EventKindIndexStart) externally from bleve
func (s *Scorch) FireIndexEvent() {
s.fireEvent(EventKindIndexStart, 0)
@@ -1002,7 +1180,8 @@ func (s *Scorch) OpenMeta() error {
// Merge and update deleted field info and rewrite index mapping
func (s *Scorch) updateBolt(fieldInfo map[string]*index.UpdateFieldInfo, mappingBytes []byte) error {
- return s.rootBolt.Update(func(tx *bolt.Tx) error {
+ filePath := s.path + string(os.PathSeparator) + "root.bolt"
+ return s.rootBolt.Update(func(tx *util.BoltTxImpl) error {
snapshots := tx.Bucket(util.BoltSnapshotsBucket)
if snapshots == nil {
return nil
@@ -1015,27 +1194,69 @@ func (s *Scorch) updateBolt(fieldInfo map[string]*index.UpdateFieldInfo, mapping
fmt.Printf("unable to parse segment epoch %x, continuing", k)
continue
}
- snapshot := snapshots.Bucket(k)
+ snapshot := snapshots.GetBucket(k)
+ metaBucket := snapshot.GetBucket(util.BoltMetaDataKey)
+ if metaBucket == nil {
+ return fmt.Errorf("meta-data bucket missing")
+ }
+
+ writer, err := util.NewFileWriter([]byte(filePath))
+ if err != nil {
+ return fmt.Errorf("unable to load correct writer: %v", err)
+ }
+
+ fileWriterID, err := metaBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return fmt.Errorf("unable to get file writer id: %v", err)
+ }
+ if fileWriterID == nil {
+ return fmt.Errorf("file writer id missing in meta data")
+ }
+ reader, err := util.NewFileReader(string(fileWriterID), []byte(filePath))
+ if err != nil {
+ return fmt.Errorf("unable to load correct reader: %v", err)
+ }
+
+ err = metaBucket.Put(util.BoltMetaDataFileWriterIDKey, []byte(writer.Id()), writer)
+ if err != nil {
+ return err
+ }
+
cc := snapshot.Cursor()
for kk, _ := cc.First(); kk != nil; kk, _ = cc.Next() {
if kk[0] == util.BoltInternalKey[0] {
- internalBucket := snapshot.Bucket(kk)
+ internalBucket := snapshot.GetBucket(kk)
if internalBucket == nil {
return fmt.Errorf("segment key, but bucket missing %x", kk)
}
- err = internalBucket.Put(util.MappingInternalKey, mappingBytes)
+
+ internalVals := make(map[string][]byte)
+ err := internalBucket.ForEach(func(key []byte, val []byte) error {
+ internalVals[string(key)] = val
+ return nil
+ }, reader)
if err != nil {
return err
}
+
+ for key, val := range internalVals {
+ err = internalBucket.Put([]byte(key), val, writer)
+ if err != nil {
+ return err
+ }
+ }
} else if kk[0] != util.BoltMetaDataKey[0] {
- segmentBucket := snapshot.Bucket(kk)
+ segmentBucket := snapshot.GetBucket(kk)
if segmentBucket == nil {
return fmt.Errorf("segment key, but bucket missing %x", kk)
}
var updatedFields map[string]*index.UpdateFieldInfo
- updatedFieldBytes := segmentBucket.Get(util.BoltUpdatedFieldsKey)
+ updatedFieldBytes, err := segmentBucket.Get(util.BoltUpdatedFieldsKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting updated field bytes: %v", err)
+ }
if updatedFieldBytes != nil {
- err := json.Unmarshal(updatedFieldBytes, &updatedFields)
+ err = json.Unmarshal(updatedFieldBytes, &updatedFields)
if err != nil {
return fmt.Errorf("error reading updated field bytes: %v", err)
}
@@ -1054,17 +1275,218 @@ func (s *Scorch) updateBolt(fieldInfo map[string]*index.UpdateFieldInfo, mapping
} else {
updatedFields = fieldInfo
}
- b, err := json.Marshal(updatedFields)
+ buf, err := json.Marshal(updatedFields)
if err != nil {
return err
}
- err = segmentBucket.Put(util.BoltUpdatedFieldsKey, b)
+ err = segmentBucket.Put(util.BoltUpdatedFieldsKey, buf, writer)
if err != nil {
return err
}
+
+ deletedBytes, err := segmentBucket.Get(util.BoltDeletedKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting deleted bytes: %v", err)
+ }
+ if deletedBytes != nil {
+ err = segmentBucket.Put(util.BoltDeletedKey, deletedBytes, writer)
+ if err != nil {
+ return err
+ }
+ }
+
+ statBytes, err := segmentBucket.Get(util.BoltStatsKey, reader)
+ if err != nil {
+ return fmt.Errorf("error getting stats bytes: %v", err)
+ }
+ if statBytes != nil {
+ err = segmentBucket.Put(util.BoltStatsKey, statBytes, writer)
+ if err != nil {
+ return err
+ }
+ }
}
}
}
return nil
})
}
+
+// returns the set of file callback writer ids in use by all of the segments and boltdb
+func (s *Scorch) FileWriterIDsInUse() (map[string]struct{}, error) {
+ s.rootLock.RLock()
+ keyMap := make(map[string]struct{})
+ for _, segmentSnapShot := range s.root.segment {
+ if seg, ok := segmentSnapShot.segment.(segment.SegmentWithCallbacks); ok {
+ keyMap[seg.CallbackId()] = struct{}{}
+ }
+ }
+
+ boltKeys, err := s.boltFileWriterIDsInUse()
+ if err != nil {
+ return nil, err
+ }
+
+ for k, _ := range boltKeys {
+ keyMap[k] = struct{}{}
+ }
+ s.rootLock.RUnlock()
+
+ return keyMap, nil
+}
+
+// removes all file callback writer ids in use from all of the segments and boltdb
+// boltdb is updated with the latest callback writer while segments are force
+// merged blockingly until snapshot is persisted with the latest callback writer
+func (s *Scorch) DropFileWriterIDs(ids map[string]struct{}) error {
+ err := s.removeBoltFileWriterIDs(ids)
+ if err != nil {
+ return err
+ }
+
+ s.rootLock.Lock()
+ // create a done channel to ensure success of merge
+ ctx := context.Background()
+ doneCh := make(chan error)
+ ctx = context.WithValue(ctx, mergeDoneKey, doneCh)
+
+ // PARTIAL ROLLBACK WILL NOT BE SUPPORTED DURING THIS OPERATION
+ // this is done because all of the rollback snapshots
+ // are likely to have the same sequence numbers and
+ // morever, it is not functionally correct to hold
+ // data with writer ids that have been removed
+ prevNumSnapshotsToKeep := s.numSnapshotsToKeep
+ s.numSnapshotsToKeep = 1
+
+ // track the zapx files that are expected to be removed after
+ // the merge so that we can block until they are removed by the persister
+ filePaths := make([]string, 0)
+
+ var mergePlanner mergePlanFunc = func(ourSnapshot *IndexSnapshot) (*mergeplan.MergePlan, error) {
+ // Create a merge plan with the filtered segments and force a merge
+ // to remove the callback from the segments.
+ mergePlannerOptions, err := s.parseMergePlannerOptions()
+ if err != nil {
+ return nil, fmt.Errorf("mergePlannerOption json parsing err: %v", err)
+ }
+ atomic.AddUint64(&s.stats.TotFileMergePlan, 1)
+
+ // filter all segments that have callbacks that need to be removed
+ // and add them to the list of segments to compact
+ segsToCompact := make([]mergeplan.Segment, 0)
+ for _, ss := range ourSnapshot.segment {
+ // only persisted segments needs to be checked
+ if _, ok := ss.segment.(segment.PersistedSegment); ok {
+ if segWithCallbacks, ok := ss.segment.(segment.SegmentWithCallbacks); ok {
+ if _, ok := ids[segWithCallbacks.CallbackId()]; ok {
+ segsToCompact = append(segsToCompact, ss)
+ filePaths = append(filePaths, zapFileName(ss.id))
+ }
+ }
+ }
+ }
+
+ // attempt a merge plan with the default merge planner options
+ mergePlan, err := mergeplan.Plan(segsToCompact, mergePlannerOptions)
+ if err != nil {
+ atomic.AddUint64(&s.stats.TotFileMergePlanErr, 1)
+ return nil, fmt.Errorf("merge plan creation err: %v", err)
+ }
+
+ // create a map to track segments included in the default merge plan
+ segDictionary := make(map[uint64]bool)
+ for _, seg := range segsToCompact {
+ segDictionary[seg.Id()] = true
+ }
+
+ // create a merge plan if the default merge planner is unable
+ // to create one with the given segments
+ if mergePlan == nil {
+ mergePlan = &mergeplan.MergePlan{
+ Tasks: make([]*mergeplan.MergeTask, 0),
+ }
+ }
+
+ // mark all segments included in the default merge plan
+ for _, task := range mergePlan.Tasks {
+ for _, seg := range task.Segments {
+ segDictionary[seg.Id()] = false
+ }
+ }
+
+ // Create additional merge tasks for segments that are unable to be merged
+ for _, seg := range segsToCompact {
+ if segDictionary[seg.Id()] {
+ mergePlan.Tasks = append(mergePlan.Tasks, &mergeplan.MergeTask{
+ Segments: []mergeplan.Segment{seg},
+ })
+ }
+ }
+
+ return mergePlan, nil
+ }
+
+ // set the merge plan func in the context for the merger to use when it receives the merge request
+ // this is to ensure that the merge request is triggered with the latest snapshot, thus avoiding
+ // any races
+ ctx = context.WithValue(ctx, mergePlanFuncKey, mergePlanner)
+
+ // trigger the merge with the force merge plan
+ s.forceMergeRequestCh <- &mergerCtrl{
+ ctx: ctx,
+ }
+ s.rootLock.Unlock()
+
+ // blockingly wait for merge to complete
+ err = <-doneCh
+ close(doneCh)
+ if err != nil {
+ return err
+ }
+
+ // wait for files to be cleaned up by persister
+ err = s.waitTillFileCleanup(filePaths)
+ if err != nil {
+ return err
+ }
+
+ // reset rollback snapshot retention
+ s.rootLock.Lock()
+ s.numSnapshotsToKeep = prevNumSnapshotsToKeep
+ s.rootLock.Unlock()
+
+ return nil
+}
+
+// waitTillFileCleanup blocks until the given files are cleaned up by the persister or merger or
+// returns an error after a timeout. It does so by checking the index directory every 5 seconds
+// for the presence of the given files, and returns once they are no longer present.
+func (s *Scorch) waitTillFileCleanup(filePaths []string) error {
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ timeout := time.After(5 * time.Minute)
+
+ for {
+ select {
+ case <-ticker.C:
+ files, err := os.ReadDir(s.path)
+ if err != nil {
+ return err
+ }
+ for _, f := range files {
+ fname := f.Name()
+ if filepath.Ext(fname) == ".zap" {
+ for _, filePath := range filePaths {
+ if fname == filePath {
+ continue
+ }
+ }
+ }
+ }
+ return nil
+ case <-timeout:
+ return fmt.Errorf("timeout waiting for file cleanup for files: %v", filePaths)
+ }
+ }
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/segment_plugin.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/segment_plugin.go
index 790a8008a3..16be8e440d 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/segment_plugin.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/segment_plugin.go
@@ -28,6 +28,7 @@ import (
zapv14 "github.com/blevesearch/zapx/v14"
zapv15 "github.com/blevesearch/zapx/v15"
zapv16 "github.com/blevesearch/zapx/v16"
+ zapv17 "github.com/blevesearch/zapx/v17"
)
// SegmentPlugin represents the essential functions required by a package to plug in
@@ -45,10 +46,14 @@ type SegmentPlugin interface {
// New takes a set of Documents and turns them into a new Segment
New(results []index.Document) (segment.Segment, uint64, error)
+ NewUsing(results []index.Document, config map[string]interface{}) (segment.Segment, uint64, error)
+
// Open attempts to open the file at the specified path and
// return the corresponding Segment
Open(path string) (segment.Segment, error)
+ OpenUsing(path string, config map[string]interface{}) (segment.Segment, error)
+
// Merge takes a set of Segments, and creates a new segment on disk at
// the specified path.
// Drops is a set of bitmaps (one for each segment) indicating which
@@ -66,6 +71,10 @@ type SegmentPlugin interface {
Merge(segments []segment.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s segment.StatsReporter) (
[][]uint64, uint64, error)
+
+ MergeUsing(segments []segment.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s segment.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error)
}
var supportedSegmentPlugins map[string]map[uint32]SegmentPlugin
@@ -73,7 +82,8 @@ var defaultSegmentPlugin SegmentPlugin
func init() {
ResetSegmentPlugins()
- RegisterSegmentPlugin(&zapv16.ZapPlugin{}, true)
+ RegisterSegmentPlugin(&zapv17.ZapPlugin{}, true)
+ RegisterSegmentPlugin(&zapv16.ZapPlugin{}, false)
RegisterSegmentPlugin(&zapv15.ZapPlugin{}, false)
RegisterSegmentPlugin(&zapv14.ZapPlugin{}, false)
RegisterSegmentPlugin(&zapv13.ZapPlugin{}, false)
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index.go
index 3f2a330c5c..4e9aff009b 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index.go
@@ -17,7 +17,6 @@ package scorch
import (
"container/heap"
"context"
- "encoding/binary"
"fmt"
"os"
"path/filepath"
@@ -28,11 +27,11 @@ import (
"github.com/RoaringBitmap/roaring/v2"
"github.com/blevesearch/bleve/v2/document"
+ "github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
segment "github.com/blevesearch/scorch_segment_api/v2"
"github.com/blevesearch/vellum"
lev "github.com/blevesearch/vellum/levenshtein"
- bolt "go.etcd.io/bbolt"
)
// re usable, threadsafe levenshtein builders
@@ -42,9 +41,8 @@ type asynchSegmentResult struct {
dict segment.TermDictionary
dictItr segment.DictionaryIterator
- cardinality int
- index int
- docs *roaring.Bitmap
+ index int
+ docs *roaring.Bitmap
thesItr segment.ThesaurusIterator
@@ -59,11 +57,11 @@ func init() {
var err error
lb1, err = lev.NewLevenshteinAutomatonBuilder(1, true)
if err != nil {
- panic(fmt.Errorf("Levenshtein automaton ed1 builder err: %v", err))
+ panic(fmt.Errorf("levenshtein automaton ed1 builder err: %v", err))
}
lb2, err = lev.NewLevenshteinAutomatonBuilder(2, true)
if err != nil {
- panic(fmt.Errorf("Levenshtein automaton ed2 builder err: %v", err))
+ panic(fmt.Errorf("levenshtein automaton ed2 builder err: %v", err))
}
}
@@ -91,6 +89,8 @@ type IndexSnapshot struct {
// UpdateFieldInfo.Index or .Store or .DocValues).
// Used to short circuit queries trying to read stale data
updatedFields map[string]*index.UpdateFieldInfo
+
+ fileWriterID string // the file callback writer id associated with this snapshot
}
func (i *IndexSnapshot) Segments() []*SegmentSnapshot {
@@ -468,13 +468,17 @@ func (is *IndexSnapshot) Fields() ([]string, error) {
}
func (is *IndexSnapshot) GetInternal(key []byte) ([]byte, error) {
+ _, ok := is.internal[string(key)]
+ if !ok {
+ return is.parent.getInternal(key)
+ }
return is.internal[string(key)], nil
}
func (is *IndexSnapshot) DocCount() (uint64, error) {
var rv uint64
for _, segment := range is.segment {
- rv += segment.Count()
+ rv += segment.CountRoot()
}
return rv, nil
}
@@ -501,7 +505,7 @@ func (is *IndexSnapshot) Document(id string) (rv index.Document, err error) {
return nil, nil
}
- docNum, err := docInternalToNumber(next.ID)
+ docNum, err := next.ID.Value()
if err != nil {
return nil, err
}
@@ -571,7 +575,7 @@ func (is *IndexSnapshot) segmentIndexAndLocalDocNumFromGlobal(docNum uint64) (in
}
func (is *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) {
- docNum, err := docInternalToNumber(id)
+ docNum, err := id.Value()
if err != nil {
return "", err
}
@@ -589,7 +593,7 @@ func (is *IndexSnapshot) ExternalID(id index.IndexInternalID) (string, error) {
}
func (is *IndexSnapshot) segmentIndexAndLocalDocNum(id index.IndexInternalID) (int, uint64, error) {
- docNum, err := docInternalToNumber(id)
+ docNum, err := id.Value()
if err != nil {
return 0, 0, err
}
@@ -700,6 +704,8 @@ func (is *IndexSnapshot) TermFieldReader(ctx context.Context, term []byte, field
rv.incrementBytesRead(bytesRead - prevBytesReadItr)
}
}
+ // ONLY update the bytes read value beyond this point for this TFR if scoring is enabled
+ rv.updateBytesRead = rv.includeFreq || rv.includeNorm || rv.includeTermVectors
atomic.AddUint64(&is.parent.stats.TotTermSearchersStarted, uint64(1))
return rv, nil
}
@@ -776,25 +782,6 @@ func (is *IndexSnapshot) recycleTermFieldReader(tfr *IndexSnapshotTermFieldReade
is.m2.Unlock()
}
-func docNumberToBytes(buf []byte, in uint64) []byte {
- if len(buf) != 8 {
- if cap(buf) >= 8 {
- buf = buf[0:8]
- } else {
- buf = make([]byte, 8)
- }
- }
- binary.BigEndian.PutUint64(buf, in)
- return buf
-}
-
-func docInternalToNumber(in index.IndexInternalID) (uint64, error) {
- if len(in) != 8 {
- return 0, fmt.Errorf("wrong len for IndexInternalID: %q", in)
- }
- return binary.BigEndian.Uint64(in), nil
-}
-
func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
segmentIndex int, localDocNum uint64, fields []string, cFields []string,
visitor index.DocValueVisitor, dvs segment.DocVisitState) (
@@ -826,8 +813,10 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
return filteredFields
}
- fieldsFiltered := filterUpdatedFields(fields)
- vFieldsFiltered := filterUpdatedFields(vFields)
+ if len(is.updatedFields) > 0 {
+ fields = filterUpdatedFields(fields)
+ vFields = filterUpdatedFields(vFields)
+ }
var errCh chan error
@@ -836,9 +825,9 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
// if the caller happens to know we're on the same segmentIndex
// from a previous invocation
if cFields == nil {
- cFields = subtractStrings(fieldsFiltered, vFieldsFiltered)
+ cFields = subtractStrings(fields, vFields)
- if !ss.cachedDocs.hasFields(cFields) {
+ if len(cFields) > 0 && !ss.cachedDocs.hasFields(cFields) {
errCh = make(chan error, 1)
go func() {
@@ -851,8 +840,8 @@ func (is *IndexSnapshot) documentVisitFieldTermsOnSegment(
}
}
- if ssvOk && ssv != nil && len(vFieldsFiltered) > 0 {
- dvs, err = ssv.VisitDocValues(localDocNum, fieldsFiltered, visitor, dvs)
+ if ssvOk && ssv != nil && len(vFields) > 0 {
+ dvs, err = ssv.VisitDocValues(localDocNum, fields, visitor, dvs)
if err != nil {
return nil, nil, err
}
@@ -897,7 +886,7 @@ func (dvr *DocValueReader) BytesRead() uint64 {
func (dvr *DocValueReader) VisitDocValues(id index.IndexInternalID,
visitor index.DocValueVisitor,
) (err error) {
- docNum, err := docInternalToNumber(id)
+ docNum, err := id.Value()
if err != nil {
return err
}
@@ -980,17 +969,15 @@ func subtractStrings(a, b []string) []string {
return a
}
- // Create a map for O(1) lookups
- bMap := make(map[string]struct{}, len(b))
- for _, bs := range b {
- bMap[bs] = struct{}{}
- }
-
rv := make([]string, 0, len(a))
+OUTER:
for _, as := range a {
- if _, exists := bMap[as]; !exists {
- rv = append(rv, as)
+ for _, bs := range b {
+ if as == bs {
+ continue OUTER
+ }
}
+ rv = append(rv, as)
}
return rv
}
@@ -1006,7 +993,7 @@ func (is *IndexSnapshot) CopyTo(d index.Directory) error {
return fmt.Errorf("invalid root.bolt file found")
}
- copyBolt, err := bolt.Open(rootFile.Name(), 0o600, nil)
+ copyBolt, err := util.OpenBolt(rootFile.Name(), 0o600, nil)
if err != nil {
return err
}
@@ -1297,3 +1284,23 @@ func (is *IndexSnapshot) TermFrequencies(field string, limit int, descending boo
return termFreqs[:limit], nil
}
+
+// Ancestors returns the ancestor IDs for the given document ID. The prealloc
+// slice can be provided to avoid allocations downstream, and MUST be empty.
+func (i *IndexSnapshot) Ancestors(ID index.IndexInternalID, prealloc []index.AncestorID) ([]index.AncestorID, error) {
+ // get segment and local doc num for the ID
+ seg, ldoc, err := i.segmentIndexAndLocalDocNum(ID)
+ if err != nil {
+ return nil, err
+ }
+ // get ancestors from the segment
+ prealloc = i.segment[seg].Ancestors(ldoc, prealloc)
+ // get global offset for the segment (correcting factor for multi-segment indexes)
+ globalOffset := i.offsets[seg]
+ // adjust ancestors to global doc numbers, not local to segment
+ for idx := range prealloc {
+ prealloc[idx] = prealloc[idx].Add(globalOffset)
+ }
+ // return adjusted ancestors
+ return prealloc, nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_doc.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_doc.go
index 0a979bfb5f..4048a199b8 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_doc.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_doc.go
@@ -15,7 +15,6 @@
package scorch
import (
- "bytes"
"reflect"
"github.com/RoaringBitmap/roaring/v2"
@@ -49,7 +48,7 @@ func (i *IndexSnapshotDocIDReader) Next() (index.IndexInternalID, error) {
next := i.iterators[i.segmentOffset].Next()
// make segment number into global number by adding offset
globalOffset := i.snapshot.offsets[i.segmentOffset]
- return docNumberToBytes(nil, uint64(next)+globalOffset), nil
+ return index.NewIndexInternalID(nil, uint64(next)+globalOffset), nil
}
return nil, nil
}
@@ -63,7 +62,7 @@ func (i *IndexSnapshotDocIDReader) Advance(ID index.IndexInternalID) (index.Inde
if next == nil {
return nil, nil
}
- for bytes.Compare(next, ID) < 0 {
+ for next.Compare(ID) < 0 {
next, err = i.Next()
if err != nil {
return nil, err
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_tfr.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_tfr.go
index cd4d82dce2..f31f213e71 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_tfr.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_tfr.go
@@ -15,7 +15,6 @@
package scorch
import (
- "bytes"
"context"
"fmt"
"reflect"
@@ -51,6 +50,10 @@ type IndexSnapshotTermFieldReader struct {
bytesRead uint64
ctx context.Context
unadorned bool
+ // flag to indicate whether to increment our bytesRead
+ // value after creation of the TFR while iterating our postings
+ // lists
+ updateBytesRead bool
}
func (i *IndexSnapshotTermFieldReader) incrementBytesRead(val uint64) {
@@ -83,10 +86,15 @@ func (i *IndexSnapshotTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*in
if rv == nil {
rv = &index.TermFieldDoc{}
}
+ var prevBytesRead uint64
// find the next hit
for i.segmentOffset < len(i.iterators) {
- prevBytesRead := i.iterators[i.segmentOffset].BytesRead()
- next, err := i.iterators[i.segmentOffset].Next()
+ // get our current postings iterator
+ curItr := i.iterators[i.segmentOffset]
+ if i.updateBytesRead {
+ prevBytesRead = curItr.BytesRead()
+ }
+ next, err := curItr.Next()
if err != nil {
return nil, err
}
@@ -94,18 +102,20 @@ func (i *IndexSnapshotTermFieldReader) Next(preAlloced *index.TermFieldDoc) (*in
// make segment number into global number by adding offset
globalOffset := i.snapshot.offsets[i.segmentOffset]
nnum := next.Number()
- rv.ID = docNumberToBytes(rv.ID, nnum+globalOffset)
+ rv.ID = index.NewIndexInternalID(rv.ID, nnum+globalOffset)
i.postingToTermFieldDoc(next, rv)
i.currID = rv.ID
i.currPosting = next
- // postingsIterators is maintain the bytesRead stat in a cumulative fashion.
- // this is because there are chances of having a series of loadChunk calls,
- // and they have to be added together before sending the bytesRead at this point
- // upstream.
- bytesRead := i.iterators[i.segmentOffset].BytesRead()
- if bytesRead > prevBytesRead {
- i.incrementBytesRead(bytesRead - prevBytesRead)
+ if i.updateBytesRead {
+ // postingsIterators maintains the bytesRead stat in a cumulative fashion.
+ // this is because there are chances of having a series of loadChunk calls,
+ // and they have to be added together before sending the bytesRead at this point
+ // upstream.
+ bytesRead := curItr.BytesRead()
+ if bytesRead > prevBytesRead {
+ i.incrementBytesRead(bytesRead - prevBytesRead)
+ }
}
return rv, nil
}
@@ -146,7 +156,7 @@ func (i *IndexSnapshotTermFieldReader) postingToTermFieldDoc(next segment.Postin
func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAlloced *index.TermFieldDoc) (*index.TermFieldDoc, error) {
// FIXME do something better
// for now, if we need to seek backwards, then restart from the beginning
- if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 {
+ if i.currPosting != nil && i.currID.Compare(ID) >= 0 {
// Check if the TFR is a special unadorned composite optimization.
// Such a TFR will NOT have a valid `term` or `field` set, making it
// impossible for the TFR to replace itself with a new one.
@@ -171,7 +181,7 @@ func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAllo
}
}
}
- num, err := docInternalToNumber(ID)
+ num, err := ID.Value()
if err != nil {
return nil, fmt.Errorf("error converting to doc number % x - %v", ID, err)
}
@@ -196,7 +206,7 @@ func (i *IndexSnapshotTermFieldReader) Advance(ID index.IndexInternalID, preAllo
if preAlloced == nil {
preAlloced = &index.TermFieldDoc{}
}
- preAlloced.ID = docNumberToBytes(preAlloced.ID, next.Number()+
+ preAlloced.ID = index.NewIndexInternalID(preAlloced.ID, next.Number()+
i.snapshot.offsets[segIndex])
i.postingToTermFieldDoc(next, preAlloced)
i.currID = preAlloced.ID
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_vr.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_vr.go
index bd57ad3e06..e572509e91 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_vr.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_index_vr.go
@@ -18,7 +18,6 @@
package scorch
import (
- "bytes"
"context"
"encoding/json"
"fmt"
@@ -96,7 +95,7 @@ func (i *IndexSnapshotVectorReader) Next(preAlloced *index.VectorDoc) (
// make segment number into global number by adding offset
globalOffset := i.snapshot.offsets[i.segmentOffset]
nnum := next.Number()
- rv.ID = docNumberToBytes(rv.ID, nnum+globalOffset)
+ rv.ID = index.NewIndexInternalID(rv.ID, nnum+globalOffset)
rv.Score = float64(next.Score())
i.currID = rv.ID
@@ -113,7 +112,7 @@ func (i *IndexSnapshotVectorReader) Next(preAlloced *index.VectorDoc) (
func (i *IndexSnapshotVectorReader) Advance(ID index.IndexInternalID,
preAlloced *index.VectorDoc) (*index.VectorDoc, error) {
- if i.currPosting != nil && bytes.Compare(i.currID, ID) >= 0 {
+ if i.currPosting != nil && i.currID.Compare(ID) >= 0 {
i2, err := i.snapshot.VectorReader(i.ctx, i.vector, i.field, i.k,
i.searchParams, i.eligibleSelector)
if err != nil {
@@ -124,7 +123,7 @@ func (i *IndexSnapshotVectorReader) Advance(ID index.IndexInternalID,
*i = *(i2.(*IndexSnapshotVectorReader))
}
- num, err := docInternalToNumber(ID)
+ num, err := ID.Value()
if err != nil {
return nil, fmt.Errorf("error converting to doc number % x - %v", ID, err)
}
@@ -149,7 +148,7 @@ func (i *IndexSnapshotVectorReader) Advance(ID index.IndexInternalID,
if preAlloced == nil {
preAlloced = &index.VectorDoc{}
}
- preAlloced.ID = docNumberToBytes(preAlloced.ID, next.Number()+
+ preAlloced.ID = index.NewIndexInternalID(preAlloced.ID, next.Number()+
i.snapshot.offsets[segIndex])
i.currID = preAlloced.ID
i.currPosting = next
@@ -183,8 +182,7 @@ func (i *IndexSnapshot) CentroidCardinalities(field string, limit int, descendin
for _, segment := range i.segment {
if sv, ok := segment.segment.(segment_api.VectorSegment); ok {
- vecIndex, err := sv.InterpretVectorIndex(field,
- false /* does not require filtering */, segment.deleted)
+ vecIndex, err := sv.InterpretVectorIndex(field, segment.deleted)
if err != nil {
return nil, fmt.Errorf("failed to interpret vector index for field %s in segment: %v", field, err)
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_segment.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_segment.go
index c6f3584cc8..136b9f344c 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_segment.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_segment.go
@@ -26,21 +26,23 @@ import (
segment "github.com/blevesearch/scorch_segment_api/v2"
)
-var TermSeparator byte = 0xff
-
-var TermSeparatorSplitSlice = []byte{TermSeparator}
-
type SegmentSnapshot struct {
// this flag is needed to identify whether this
// segment was mmaped recently, in which case
// we consider the loading cost of the metadata
// as part of IO stats.
- mmaped uint32
- id uint64
- segment segment.Segment
- deleted *roaring.Bitmap
- creator string
- stats *fieldStats
+ mmaped uint32
+ id uint64
+ segment segment.Segment
+ deleted *roaring.Bitmap
+ creator string
+ stats *fieldStats
+
+ // if this segment is in-memory then we'll try to undo the internal values
+ // in the indexSnapshot internal map before updating the bolt, since its
+ // supposed to be reflective of the on-disk data.
+ internal map[string][]byte
+
updatedFields map[string]*index.UpdateFieldInfo
cachedMeta *cachedMeta
@@ -113,6 +115,19 @@ func (s *SegmentSnapshot) Count() uint64 {
return rv
}
+// this counts the root documents in the segment this differs from Count() in that
+// Count() counts all live documents including nested children, whereas this method
+// counts only root live documents
+func (s *SegmentSnapshot) CountRoot() uint64 {
+ var rv uint64
+ if nsb, ok := s.segment.(segment.NestedSegment); ok {
+ rv = nsb.CountRoot(s.deleted)
+ } else {
+ rv = s.Count()
+ }
+ return rv
+}
+
func (s *SegmentSnapshot) DocNumbers(docIDs []string) (*roaring.Bitmap, error) {
rv, err := s.segment.DocNumbers(docIDs)
if err != nil {
@@ -220,7 +235,7 @@ func (cfd *cachedFieldDocs) prepareField(field string, ss *SegmentSnapshot) {
for err2 == nil && nextPosting != nil {
docNum := nextPosting.Number()
cfd.docs[docNum] = append(cfd.docs[docNum], []byte(next.Term)...)
- cfd.docs[docNum] = append(cfd.docs[docNum], TermSeparator)
+ cfd.docs[docNum] = append(cfd.docs[docNum], index.DocValueTermSeparator)
cfd.size += uint64(len(next.Term) + 1) // map value
nextPosting, err2 = postingsItr.Next()
}
@@ -241,7 +256,7 @@ func (cfd *cachedFieldDocs) prepareField(field string, ss *SegmentSnapshot) {
type cachedDocs struct {
size uint64
- m sync.Mutex // As the cache is asynchronously prepared, need a lock
+ m sync.RWMutex // As the cache is asynchronously prepared, need a lock
cache map[string]*cachedFieldDocs // Keyed by field
}
@@ -283,14 +298,14 @@ func (c *cachedDocs) prepareFields(wantedFields []string, ss *SegmentSnapshot) e
// hasFields returns true if the cache has all the given fields
func (c *cachedDocs) hasFields(fields []string) bool {
- c.m.Lock()
+ c.m.RLock()
for _, field := range fields {
if _, exists := c.cache[field]; !exists {
- c.m.Unlock()
+ c.m.RUnlock()
return false // found a field not in cache
}
}
- c.m.Unlock()
+ c.m.RUnlock()
return true
}
@@ -311,17 +326,17 @@ func (c *cachedDocs) updateSizeLOCKED() {
func (c *cachedDocs) visitDoc(localDocNum uint64,
fields []string, visitor index.DocValueVisitor) {
- c.m.Lock()
+ c.m.RLock()
for _, field := range fields {
if cachedFieldDocs, exists := c.cache[field]; exists {
- c.m.Unlock()
+ c.m.RUnlock()
<-cachedFieldDocs.readyCh
- c.m.Lock()
+ c.m.RLock()
if tlist, exists := cachedFieldDocs.docs[localDocNum]; exists {
for {
- i := bytes.Index(tlist, TermSeparatorSplitSlice)
+ i := bytes.IndexByte(tlist, index.DocValueTermSeparator)
if i < 0 {
break
}
@@ -332,7 +347,7 @@ func (c *cachedDocs) visitDoc(localDocNum uint64,
}
}
- c.m.Unlock()
+ c.m.RUnlock()
}
// the purpose of the cachedMeta is to simply allow the user of this type to record
@@ -357,7 +372,18 @@ func (c *cachedMeta) updateMeta(field string, val interface{}) {
func (c *cachedMeta) fetchMeta(field string) (rv interface{}) {
c.m.RLock()
+ defer c.m.RUnlock()
+ if c.meta == nil {
+ return nil
+ }
rv = c.meta[field]
- c.m.RUnlock()
return rv
}
+
+func (s *SegmentSnapshot) Ancestors(docNum uint64, prealloc []index.AncestorID) []index.AncestorID {
+ nsb, ok := s.segment.(segment.NestedSegment)
+ if !ok {
+ return append(prealloc, index.NewAncestorID(docNum))
+ }
+ return nsb.Ancestors(docNum, prealloc)
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_vector_index.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_vector_index.go
index 4fbb8441e5..1d0c03b949 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_vector_index.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/snapshot_vector_index.go
@@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
+ "github.com/bits-and-blooms/bitset"
index "github.com/blevesearch/bleve_index_api"
segment_api "github.com/blevesearch/scorch_segment_api/v2"
)
@@ -45,17 +46,82 @@ func (is *IndexSnapshot) VectorReader(ctx context.Context, vector []float32,
return rv, nil
}
+// eligibleDocumentList represents the list of eligible documents within a segment.
+type eligibleDocumentList struct {
+ bs *bitset.BitSet
+}
+
+// Iterator returns an iterator for the eligible document IDs.
+func (edl *eligibleDocumentList) Iterator() index.EligibleDocumentIterator {
+ if edl.bs == nil {
+ // no eligible documents
+ return emptyEligibleIterator
+ }
+ // return the iterator
+ return &eligibleDocumentIterator{
+ bs: edl.bs,
+ }
+}
+
+// Count returns the number of eligible document IDs.
+func (edl *eligibleDocumentList) Count() uint64 {
+ if edl.bs == nil {
+ return 0
+ }
+ return uint64(edl.bs.Count())
+}
+
+// emptyEligibleDocumentList is a reusable empty eligible document list.
+var emptyEligibleDocumentList = &eligibleDocumentList{}
+
+// eligibleDocumentIterator iterates over eligible document IDs within a segment.
+type eligibleDocumentIterator struct {
+ bs *bitset.BitSet
+ current uint
+}
+
+// Next returns the next eligible document ID and whether it exists.
+func (it *eligibleDocumentIterator) Next() (id uint64, ok bool) {
+ next, found := it.bs.NextSet(it.current)
+ if !found {
+ return 0, false
+ }
+ it.current = next + 1
+ return uint64(next), true
+}
+
+// emptyEligibleIterator is a reusable empty eligible document iterator.
+var emptyEligibleIterator = &emptyEligibleDocumentIterator{}
+
+// emptyEligibleDocumentIterator is an iterator that always returns no documents.
+type emptyEligibleDocumentIterator struct{}
+
+// Next always returns false for empty iterator.
+func (it *emptyEligibleDocumentIterator) Next() (id uint64, ok bool) {
+ return 0, false
+}
+
// eligibleDocumentSelector is used to filter out documents that are eligible for
// the KNN search from a pre-filter query.
type eligibleDocumentSelector struct {
- // segment ID -> segment local doc nums
- eligibleDocNums map[int][]uint64
+ // segment ID -> segment local doc nums in a bitset
+ eligibleDocNums []*bitset.BitSet
is *IndexSnapshot
}
-// SegmentEligibleDocs returns the list of eligible local doc numbers for the given segment.
-func (eds *eligibleDocumentSelector) SegmentEligibleDocs(segmentID int) []uint64 {
- return eds.eligibleDocNums[segmentID]
+// SegmentEligibleDocuments returns an EligibleDocumentList for the specified segment ID.
+func (eds *eligibleDocumentSelector) SegmentEligibleDocuments(segmentID int) index.EligibleDocumentList {
+ if eds.eligibleDocNums == nil || segmentID < 0 || segmentID >= len(eds.eligibleDocNums) {
+ return emptyEligibleDocumentList
+ }
+ bs := eds.eligibleDocNums[segmentID]
+ if bs == nil {
+ // no eligible documents for this segment
+ return emptyEligibleDocumentList
+ }
+ return &eligibleDocumentList{
+ bs: bs,
+ }
}
// AddEligibleDocumentMatch adds a document match to the list of eligible documents.
@@ -68,14 +134,19 @@ func (eds *eligibleDocumentSelector) AddEligibleDocumentMatch(id index.IndexInte
if err != nil {
return err
}
+ // allocate a bitset for this segment if needed
+ if eds.eligibleDocNums[segIdx] == nil {
+ // the size of the bitset is the full size of the segment (which is the max local doc num + 1)
+ eds.eligibleDocNums[segIdx] = bitset.New(uint(eds.is.segment[segIdx].FullSize()))
+ }
// Add the local doc number to the list of eligible doc numbers for this segment.
- eds.eligibleDocNums[segIdx] = append(eds.eligibleDocNums[segIdx], docNum)
+ eds.eligibleDocNums[segIdx].Set(uint(docNum))
return nil
}
func (is *IndexSnapshot) NewEligibleDocumentSelector() index.EligibleDocumentSelector {
return &eligibleDocumentSelector{
- eligibleDocNums: map[int][]uint64{},
+ eligibleDocNums: make([]*bitset.BitSet, len(is.segment)),
is: is,
}
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/stats.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/stats.go
index 9abc8ba96f..c4bf2a8557 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/stats.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/stats.go
@@ -136,6 +136,9 @@ type Stats struct {
MaxMemMergeZapTime uint64
TotMemMergeSegments uint64
TotMemorySegmentsAtRoot uint64
+
+ TotTrainedSamples uint64
+ TotTrainTime uint64
}
// atomically populates the returned map
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_noop.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_noop.go
new file mode 100644
index 0000000000..36752e1e13
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_noop.go
@@ -0,0 +1,55 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !vectors
+// +build !vectors
+
+package scorch
+
+import (
+ "fmt"
+
+ "github.com/blevesearch/bleve/v2/util"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+func initTrainer(s *Scorch, config map[string]interface{}) *noopTrainer {
+ return nil
+}
+
+type noopTrainer struct {
+}
+
+func (t *noopTrainer) trainLoop() {}
+
+func (t *noopTrainer) train(batch *index.Batch) error {
+ return fmt.Errorf("training is not supported with this build")
+}
+
+func (t *noopTrainer) loadTrainedData(bucket *util.BoltBucketImpl) error {
+ // noop
+ return nil
+}
+
+func (t *noopTrainer) getInternal(key []byte) ([]byte, error) {
+ return nil, nil
+}
+
+func (t *noopTrainer) copyFileLOCKED(file string, d index.IndexDirectory) error {
+ return nil
+}
+
+func (t *noopTrainer) updateBolt(snapshotsBucket *util.BoltBucketImpl, key []byte, value []byte) error {
+ return nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_vector.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_vector.go
new file mode 100644
index 0000000000..4c3b15ca8e
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/train_vector.go
@@ -0,0 +1,397 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package scorch
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "maps"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ "github.com/blevesearch/bleve/v2/util"
+ index "github.com/blevesearch/bleve_index_api"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+type trainRequest struct {
+ finalSample bool
+ sampleSize int
+ ackCh chan error
+ sample segment.Segment
+}
+
+type vectorTrainer struct {
+ trainingComplete atomic.Bool
+ trainedSamples uint64
+ parent *Scorch
+ config map[string]interface{}
+
+ m sync.RWMutex
+ // not a searchable segment in the sense that it won't return
+ // the data vectors, returns trained centroid layout
+ trainedIndex *SegmentSnapshot
+ trainCh chan *trainRequest
+}
+
+const IndexTrainedWithFastMerge = "vector_index_fast_merge"
+
+func initTrainer(s *Scorch, config map[string]interface{}) *vectorTrainer {
+ if f, ok := config[IndexTrainedWithFastMerge]; ok {
+ feature, ok := f.(bool)
+ if ok && feature {
+ trainer := vectorTrainer{
+ parent: s,
+ config: maps.Clone(s.config),
+ trainCh: make(chan *trainRequest, 1),
+ }
+ // update the parent scorch config with the trainer's callback to fetch the trained index
+ s.segmentConfig[index.TrainedIndexCallback] = index.TrainedIndexCallbackFn(trainer.getTrainedIndex)
+ return &trainer
+ }
+ }
+ return nil
+}
+
+func moveFile(sourcePath, destPath string) error {
+ // rename is supposed to be atomic on the same filesystem
+ err := os.Rename(sourcePath, destPath)
+ if err != nil {
+ return fmt.Errorf("error renaming file: %v", err)
+ }
+ return nil
+}
+
+func (t *vectorTrainer) persistToBolt(trainReq *trainRequest) error {
+ tx, err := t.parent.rootBolt.Begin(true)
+ if err != nil {
+ return fmt.Errorf("error starting bolt transaction: %v", err)
+ }
+ defer tx.Rollback()
+
+ snapshotsBucket, err := tx.CreateBucketIfNotExists(util.BoltSnapshotsBucket)
+ if err != nil {
+ return fmt.Errorf("error creating snapshots bucket: %v", err)
+ }
+
+ trainerBucket, err := snapshotsBucket.CreateBucketIfNotExists(util.BoltTrainerKey)
+ if err != nil {
+ return fmt.Errorf("error creating trained index bucket: %v", err)
+ }
+ err = trainerBucket.Put(util.BoltPathKey, []byte(index.TrainedIndexFileName), nil)
+ if err != nil {
+ return fmt.Errorf("error updating trained index bucket: %v", err)
+ }
+
+ t.trainingComplete.Store(trainReq.finalSample)
+ err = trainerBucket.Put(util.BoltTrainCompleteKey, []byte(strconv.FormatBool(trainReq.finalSample)), nil)
+ if err != nil {
+ return fmt.Errorf("error updating train complete key: %v", err)
+ }
+
+ totSamples := atomic.AddUint64(&t.trainedSamples, uint64(trainReq.sampleSize))
+ err = trainerBucket.Put(util.BoltTrainedSamplesKey, binary.LittleEndian.AppendUint64(nil, totSamples), nil)
+ if err != nil {
+ return fmt.Errorf("error updating trained samples key: %v", err)
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return fmt.Errorf("error committing bolt transaction: %v", err)
+ }
+
+ return t.parent.rootBolt.Sync()
+}
+
+// this is not a routine that will be running throughout the lifetime of the index. It's purpose
+// is to only train the vector index before the data ingestion starts.
+func (t *vectorTrainer) trainLoop() {
+ defer t.parent.asyncTasks.Done()
+
+ trainLoopStartTime := time.Now()
+ path := filepath.Join(t.parent.path, index.TrainedIndexFileName)
+ for {
+ // exit once the final sample set has been ingested and training is complete.
+ if t.trainingComplete.Load() {
+ atomic.StoreUint64(&t.parent.stats.TotTrainedSamples, t.trainedSamples)
+ atomic.StoreUint64(&t.parent.stats.TotTrainTime, uint64(time.Since(trainLoopStartTime).Milliseconds()))
+ return
+ }
+ select {
+ case <-t.parent.closeCh:
+ select {
+ case req := <-t.trainCh:
+ req.ackCh <- fmt.Errorf("trainer is closed")
+ close(req.ackCh)
+ default:
+ }
+ return
+ case trainReq := <-t.trainCh:
+ sampleSeg := trainReq.sample
+ // no sample segment: just persist state if this is the final sample and move on.
+ if sampleSeg == nil {
+ if trainReq.finalSample {
+ if err := t.persistToBolt(trainReq); err != nil {
+ trainReq.ackCh <- fmt.Errorf("error persisting to bolt: %v", err)
+ close(trainReq.ackCh)
+ return
+ }
+ }
+ close(trainReq.ackCh)
+ continue
+ }
+
+ if t.trainedIndex == nil {
+ switch seg := sampleSeg.(type) {
+ case segment.UnpersistedSegment:
+ if err := persistToDirectory(seg, nil, path); err != nil {
+ trainReq.ackCh <- fmt.Errorf("error persisting segment: %v", err)
+ close(trainReq.ackCh)
+ continue
+ }
+ }
+ } else {
+ // merge the new segment with the existing one into a .tmp file, then
+ // atomically rename it into place (Os.Open on the live path is unsafe
+ // during the merge).
+ t.config[index.TrainingKey] = true
+ _, _, err := t.parent.segPlugin.MergeUsing([]segment.Segment{t.trainedIndex.segment, sampleSeg},
+ []*roaring.Bitmap{nil, nil}, path+".tmp", t.parent.closeCh, nil, t.config)
+ t.config[index.TrainingKey] = false
+ if err != nil {
+ trainReq.ackCh <- fmt.Errorf("error merging trained index: %v", err)
+ close(trainReq.ackCh)
+ return
+ }
+
+ t.trainedIndex.segment.Close()
+ if err = moveFile(path+".tmp", path); err != nil {
+ trainReq.ackCh <- fmt.Errorf("error renaming trained index: %v", err)
+ close(trainReq.ackCh)
+ return
+ }
+ }
+
+ // bolt write acts as a checkpoint for failover-recovery: callers downstream
+ // can rely on the trained index being available once this completes.
+ // todo: rethink the frequency of bolt writes
+ if err := t.persistToBolt(trainReq); err != nil {
+ trainReq.ackCh <- fmt.Errorf("error persisting to bolt: %v", err)
+ close(trainReq.ackCh)
+ return
+ }
+
+ trainedIndex, err := t.parent.segPlugin.OpenUsing(path, t.parent.segmentConfig)
+ if err != nil {
+ trainReq.ackCh <- fmt.Errorf("error opening trained index: %v", err)
+ close(trainReq.ackCh)
+ return
+ }
+
+ t.m.Lock()
+ t.trainedIndex = &SegmentSnapshot{segment: trainedIndex}
+ t.m.Unlock()
+ close(trainReq.ackCh)
+ }
+ }
+}
+
+// loads the metadata specific to the trained index from boltdb, happens during init
+// no lock needed
+func (t *vectorTrainer) loadTrainedData(bucket *util.BoltBucketImpl) error {
+ if bucket == nil {
+ return nil
+ }
+ writerID, err := bucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return fmt.Errorf("error getting writer id: %v", err)
+ }
+ reader, err := util.NewFileReader(string(writerID), nil)
+ if err != nil {
+ return fmt.Errorf("error creating file reader: %v", err)
+ }
+
+ segmentSnapshot, err := t.parent.loadSegment(bucket, reader)
+ if err != nil {
+ return err
+ }
+
+ // get the training status out of bolt
+ trainComplete, err := bucket.Get(util.BoltTrainCompleteKey, nil)
+ if err != nil {
+ return fmt.Errorf("error getting train complete: %v", err)
+ }
+ trainedSamples, err := bucket.Get(util.BoltTrainedSamplesKey, nil)
+ if err != nil {
+ return fmt.Errorf("error getting trained samples: %v", err)
+ }
+ atomic.StoreUint64(&t.trainedSamples, binary.LittleEndian.Uint64(trainedSamples))
+ comp, err := strconv.ParseBool(string(trainComplete))
+ if err != nil {
+ return fmt.Errorf("error parsing train complete: %v", err)
+ }
+ t.trainingComplete.Store(comp)
+
+ t.m.Lock()
+ defer t.m.Unlock()
+ t.trainedIndex = segmentSnapshot
+ return nil
+}
+
+func (t *vectorTrainer) train(batch *index.Batch) error {
+ // regulate the Train function
+ t.parent.FireIndexEvent()
+
+ var trainData []index.Document
+ for _, doc := range batch.IndexOps {
+ if doc != nil {
+ // insert _id field
+ // no need to track updates/deletes over here since
+ // the API is singleton
+ doc.AddIDField()
+ }
+ trainData = append(trainData, doc)
+ }
+
+ trainComplete := batch.InternalOps[string(util.BoltTrainCompleteKey)]
+ if trainComplete == nil {
+ trainComplete = []byte("false")
+ }
+ fin, err := strconv.ParseBool(string(trainComplete))
+ if err != nil {
+ return fmt.Errorf("error parsing train complete: %v", err)
+ }
+
+ trainReq := &trainRequest{
+ finalSample: fin,
+ sampleSize: len(trainData),
+ ackCh: make(chan error),
+ }
+ // just builds a new vector index out of the train data provided
+ // this is not necessarily the final train data since this is submitted
+ // as a request to the trainer component to be merged. once the training
+ // is complete, the template will be used for other operations down the line
+ // like merge and search.
+ //
+ // note: this might index text data too, how to handle this? s.segmentConfig?
+ // todo: updates/deletes -> data drift detection
+ if len(trainData) > 0 {
+ trainReq.sample, _, err = t.parent.segPlugin.NewUsing(trainData, t.parent.segmentConfig)
+ if err != nil {
+ return err
+ }
+ }
+
+ t.trainCh <- trainReq
+ err = <-trainReq.ackCh
+ if err != nil {
+ return fmt.Errorf("train_vector: train() err'd out with: %w", err)
+ }
+
+ return err
+}
+
+func (t *vectorTrainer) getInternal(key []byte) ([]byte, error) {
+ switch string(key) {
+ case string(util.BoltTrainCompleteKey):
+ return []byte(strconv.FormatBool(t.trainingComplete.Load())), nil
+ }
+ return nil, nil
+}
+
+func (t *vectorTrainer) getTrainedIndex(field string) (interface{}, error) {
+ // return the coarse quantizer of the trained faiss index belonging to the field
+ // if its not available then zap performs naive merge
+ t.m.RLock()
+ defer t.m.RUnlock()
+ if t.trainedIndex != nil {
+ trainedSegment, ok := t.trainedIndex.segment.(segment.TrainedSegment)
+ if !ok {
+ return nil, fmt.Errorf("segment is not a trained index segment")
+ }
+
+ coarseQuantizer, err := trainedSegment.GetCoarseQuantizer(field)
+ if err != nil {
+ return nil, err
+ }
+ return coarseQuantizer, nil
+ }
+ return nil, nil
+}
+
+func (t *vectorTrainer) copyFileLOCKED(file string, d index.IndexDirectory) error {
+ if strings.HasSuffix(file, index.TrainedIndexFileName) {
+ // trained index file - this is outside the snapshots domain so the bolt update is different
+ err := d.SetPathInBolt(util.BoltTrainerKey, []byte(file))
+ if err != nil {
+ return fmt.Errorf("error updating dest index bolt: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func (t *vectorTrainer) updateBolt(snapshotsBucket *util.BoltBucketImpl, key []byte, value []byte) error {
+ if bytes.Equal(key, util.BoltTrainerKey) {
+ trainerBucket, err := snapshotsBucket.CreateBucketIfNotExists(util.BoltTrainerKey)
+ if err != nil {
+ return err
+ }
+ if trainerBucket == nil {
+ return fmt.Errorf("trainer bucket not found")
+ }
+
+ // guard against duplicate updates
+ existingValue, err := trainerBucket.Get(util.BoltPathKey, nil)
+ if err != nil {
+ return fmt.Errorf("error checking existing value: %v", err)
+ }
+ if existingValue != nil {
+ return fmt.Errorf("key already exists %v %v", t.parent.path, string(existingValue))
+ }
+
+ err = trainerBucket.Put(util.BoltPathKey, value, nil)
+ if err != nil {
+ return err
+ }
+
+ writerID, err := trainerBucket.Get(util.BoltMetaDataFileWriterIDKey, nil)
+ if err != nil {
+ return fmt.Errorf("error getting writer id: %v", err)
+ }
+ reader, err := util.NewFileReader(string(writerID), nil)
+ if err != nil {
+ return fmt.Errorf("error creating file reader: %v", err)
+ }
+
+ // update the centroid index pointer
+ t.trainedIndex, err = t.parent.loadSegment(trainerBucket, reader)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index/scorch/unadorned.go b/vendor/github.com/blevesearch/bleve/v2/index/scorch/unadorned.go
index 18ce1c5823..a37fb37ff1 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index/scorch/unadorned.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index/scorch/unadorned.go
@@ -38,6 +38,7 @@ func init() {
type unadornedPostingsIteratorBitmap struct {
actual roaring.IntPeekable
actualBM *roaring.Bitmap
+ next UnadornedPosting // reused across Next() calls
}
func (i *unadornedPostingsIteratorBitmap) Next() (segment.Posting, error) {
@@ -53,7 +54,10 @@ func (i *unadornedPostingsIteratorBitmap) nextAtOrAfter(atOrAfter uint64) (segme
if !exists {
return nil, nil
}
- return UnadornedPosting(docNum), nil
+ i.next = UnadornedPosting{} // clear the struct
+ rv := &i.next
+ rv.docNum = docNum
+ return rv, nil
}
func (i *unadornedPostingsIteratorBitmap) nextDocNumAtOrAfter(atOrAfter uint64) (uint64, bool) {
@@ -112,8 +116,9 @@ func newUnadornedPostingsIteratorFromBitmap(bm *roaring.Bitmap) segment.Postings
const docNum1HitFinished = math.MaxUint64
type unadornedPostingsIterator1Hit struct {
- docNumOrig uint64 // original 1-hit docNum used to create this iterator
- docNum uint64 // current docNum
+ docNumOrig uint64 // original 1-hit docNum used to create this iterator
+ docNum uint64 // current docNum
+ next UnadornedPosting // reused across Next() calls
}
func (i *unadornedPostingsIterator1Hit) Next() (segment.Posting, error) {
@@ -129,7 +134,10 @@ func (i *unadornedPostingsIterator1Hit) nextAtOrAfter(atOrAfter uint64) (segment
if !exists {
return nil, nil
}
- return UnadornedPosting(docNum), nil
+ i.next = UnadornedPosting{} // clear the struct
+ rv := &i.next
+ rv.docNum = docNum
+ return rv, nil
}
func (i *unadornedPostingsIterator1Hit) nextDocNumAtOrAfter(atOrAfter uint64) (uint64, bool) {
@@ -176,24 +184,26 @@ type ResetablePostingsIterator interface {
ResetIterator()
}
-type UnadornedPosting uint64
-
-func (p UnadornedPosting) Number() uint64 {
- return uint64(p)
+type UnadornedPosting struct {
+ docNum uint64
}
-func (p UnadornedPosting) Frequency() uint64 {
+func (p *UnadornedPosting) Number() uint64 {
+ return p.docNum
+}
+
+func (p *UnadornedPosting) Frequency() uint64 {
return 0
}
-func (p UnadornedPosting) Norm() float64 {
+func (p *UnadornedPosting) Norm() float64 {
return 0
}
-func (p UnadornedPosting) Locations() []segment.Location {
+func (p *UnadornedPosting) Locations() []segment.Location {
return nil
}
-func (p UnadornedPosting) Size() int {
+func (p *UnadornedPosting) Size() int {
return reflectStaticSizeUnadornedPosting
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index_alias_impl.go b/vendor/github.com/blevesearch/bleve/v2/index_alias_impl.go
index 8212c74b92..c58891d664 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index_alias_impl.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index_alias_impl.go
@@ -103,6 +103,24 @@ func (i *indexAliasImpl) IndexSynonym(id string, collection string, definition *
return ErrorSynonymSearchNotSupported
}
+func (i *indexAliasImpl) Train(batch *Batch) error {
+ i.mutex.RLock()
+ defer i.mutex.RUnlock()
+ if !i.open {
+ return ErrorIndexClosed
+ }
+
+ err := i.isAliasToSingleIndex()
+ if err != nil {
+ return err
+ }
+
+ if vi, ok := i.indexes[0].(TrainableIndex); ok {
+ return vi.Train(batch)
+ }
+ return ErrorTrainingNotSupported
+}
+
func (i *indexAliasImpl) Delete(id string) error {
i.mutex.RLock()
defer i.mutex.RUnlock()
@@ -985,6 +1003,15 @@ func MultiSearch(ctx context.Context, req *SearchRequest, params *multiSearchPar
searchStart := time.Now()
asyncResults := make(chan *asyncSearchResult, len(indexes))
+ var preSearchData map[string]map[string]interface{}
+ var rescorer *rescorer
+ var fusionKnnHits search.DocumentMatchCollection
+ if params != nil {
+ preSearchData = params.preSearchData
+ rescorer = params.rescorer
+ fusionKnnHits = params.fusionKnnHits
+ }
+
var reverseQueryExecution bool
if req.SearchBefore != nil {
reverseQueryExecution = true
@@ -1006,8 +1033,8 @@ func MultiSearch(ctx context.Context, req *SearchRequest, params *multiSearchPar
waitGroup.Add(len(indexes))
for _, in := range indexes {
var payload map[string]interface{}
- if params.preSearchData != nil {
- payload = params.preSearchData[in.Name()]
+ if preSearchData != nil {
+ payload = preSearchData[in.Name()]
}
go searchChildIndex(in, createChildSearchRequest(req, payload))
}
@@ -1047,9 +1074,9 @@ func MultiSearch(ctx context.Context, req *SearchRequest, params *multiSearchPar
}
}
- if params.rescorer != nil {
- sr.Hits, sr.Total, sr.MaxScore = params.rescorer.rescore(sr.Hits, params.fusionKnnHits)
- params.rescorer.restoreSearchRequest()
+ if rescorer != nil {
+ sr.Hits, sr.Total, sr.MaxScore = rescorer.rescore(sr.Hits, fusionKnnHits)
+ rescorer.restoreSearchRequest()
}
sr.Hits = hitsInCurrentPage(req, sr.Hits)
diff --git a/vendor/github.com/blevesearch/bleve/v2/index_impl.go b/vendor/github.com/blevesearch/bleve/v2/index_impl.go
index 8065d9c1e8..2545a47a3b 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index_impl.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index_impl.go
@@ -91,7 +91,10 @@ func newIndexUsing(path string, mapping mapping.IndexMapping, indexType string,
path: path,
name: path,
m: mapping,
- meta: newIndexMeta(indexType, kvstore, kvconfig),
+ }
+ rv.meta, err = newIndexMeta(indexType, kvstore, kvconfig, path)
+ if err != nil {
+ return nil, err
}
rv.stats = &IndexStat{i: &rv}
// at this point there is hope that we can be successful, so save index meta
@@ -369,6 +372,20 @@ func (i *indexImpl) IndexSynonym(id string, collection string, definition *Synon
return err
}
+func (i *indexImpl) Train(batch *Batch) error {
+ i.mutex.RLock()
+ defer i.mutex.RUnlock()
+
+ if !i.open {
+ return ErrorIndexClosed
+ }
+
+ if vi, ok := i.i.(index.TrainableIndex); ok {
+ return vi.Train(batch.internal)
+ }
+ return ErrorTrainingNotSupported
+}
+
// IndexAdvanced takes a document.Document object
// skips the mapping and indexes it.
func (i *indexImpl) IndexAdvanced(doc *document.Document) (err error) {
@@ -479,6 +496,55 @@ func (i *indexImpl) Search(req *SearchRequest) (sr *SearchResult, err error) {
return i.SearchInContext(context.Background(), req)
}
+// returns the set of file callback writer ids in use by the index
+func (i *indexImpl) FileWriterIDsInUse() (map[string]struct{}, error) {
+ ids := map[string]struct{}{i.meta.fileReader.Id(): {}}
+
+ if cidx, ok := i.i.(IndexWithCallbacks); ok {
+ cIds, err := cidx.FileWriterIDsInUse()
+ if err != nil {
+ return nil, err
+ }
+ for k := range cIds {
+ ids[k] = struct{}{}
+ }
+ } else {
+ // if the underlying index does not support callbacks, we
+ // assume that the data being written is with the default
+ // writer id which is the empty string
+ ids[util.DefaultFileCallbackId] = struct{}{}
+ }
+
+ return ids, nil
+}
+
+// drops the file callback writer ids from the index and
+// re-processes data with the latest file callback writer id
+func (i *indexImpl) DropFileWriterIDs(ids map[string]struct{}) error {
+ i.mutex.Lock()
+ if _, ok := ids[i.meta.fileReader.Id()]; ok {
+ var err error
+ err = i.meta.UpdateWriter(i.path)
+ if err != nil {
+ return err
+ }
+ }
+ i.mutex.Unlock()
+
+ if cidx, ok := i.i.(IndexWithCallbacks); ok {
+ return cidx.DropFileWriterIDs(ids)
+ } else {
+ // if the underlying index does not support callbacks and the request is
+ // to drop the empty id, which is the default id, we return an error
+ // because it is not possible to drop it
+ if _, ok := ids[util.DefaultFileCallbackId]; ok {
+ return fmt.Errorf("underlying index does not support DropFileWriterIDs")
+ }
+ }
+
+ return nil
+}
+
var (
documentMatchEmptySize int
searchContextEmptySize int
@@ -572,8 +638,7 @@ func (i *indexImpl) preSearch(ctx context.Context, req *SearchRequest, reader in
return nil, err
}
- fs := make(query.FieldSet)
- fs, err := query.ExtractFields(req.Query, i.m, fs)
+ fs, err := query.ExtractFields(req.Query, i.m, search.NewFieldSet())
if err != nil {
return nil, err
}
@@ -642,7 +707,7 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
// ------------------------------------------------------------------------------------------
// set up additional contexts for any search operation that will proceed from
- // here, such as presearch, collectors etc.
+ // here, such as presearch, knn collector, topn collector etc.
// Scoring model callback to be used to get scoring model
scoringModelCallback := func() string {
@@ -687,6 +752,13 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
}
ctx = context.WithValue(ctx, search.GeoBufferPoolCallbackKey, search.GeoBufferPoolCallbackFunc(getBufferPool))
+ // check if the index mapping has any nested fields, which should force
+ // all collectors and searchers to be run in nested mode
+ if nm, ok := i.m.(mapping.NestedMapping); ok {
+ if nm.CountNested() > 0 {
+ ctx = context.WithValue(ctx, search.NestedSearchKey, true)
+ }
+ }
// ------------------------------------------------------------------------------------------
if _, ok := ctx.Value(search.PreSearchKey).(bool); ok {
@@ -716,11 +788,9 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
req.SearchBefore = nil
}
- var coll *collector.TopNCollector
- if req.SearchAfter != nil {
- coll = collector.NewTopNCollectorAfter(req.Size, req.Sort, req.SearchAfter)
- } else {
- coll = collector.NewTopNCollector(req.Size, req.From, req.Sort)
+ coll, err := i.buildTopNCollector(ctx, req, indexReader)
+ if err != nil {
+ return nil, err
}
var knnHits []*search.DocumentMatch
@@ -795,7 +865,7 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
// if score fusion, no faceting for knn hits is done
// hence we can skip setting the knn hits in the collector
if !contextScoreFusionKeyExists {
- setKnnHitsInCollector(knnHits, req, coll)
+ setKnnHitsInCollector(knnHits, coll)
}
if fts != nil {
@@ -937,7 +1007,7 @@ func (i *indexImpl) SearchInContext(ctx context.Context, req *SearchRequest) (sr
if i.name != "" && hit.Index == "" {
hit.Index = i.name
}
- err, storedFieldsBytes := LoadAndHighlightFields(hit, req, i.name, indexReader, highlighter)
+ err, storedFieldsBytes := LoadAndHighlightAllFields(hit, req, i.name, indexReader, highlighter)
if err != nil {
return nil, err
}
@@ -1105,6 +1175,56 @@ func LoadAndHighlightFields(hit *search.DocumentMatch, req *SearchRequest,
return nil, totalStoredFieldsBytes
}
+const NestedDocumentKey = "_$nested"
+
+// LoadAndHighlightAllFields loads stored fields + highlights for root and its descendants.
+// All descendant documents are collected into a _$nested array in the root DocumentMatch.
+func LoadAndHighlightAllFields(
+ root *search.DocumentMatch,
+ req *SearchRequest,
+ indexName string,
+ r index.IndexReader,
+ highlighter highlight.Highlighter,
+) (error, uint64) {
+ var totalStoredFieldsBytes uint64
+ // load root fields/highlights
+ err, bytes := LoadAndHighlightFields(root, req, indexName, r, highlighter)
+ totalStoredFieldsBytes += bytes
+ if err != nil {
+ return err, totalStoredFieldsBytes
+ }
+ // collect all descendant documents
+ nestedDocs := make([]*search.NestedDocumentMatch, 0, len(root.Descendants))
+ // create a dummy desc DocumentMatch to reuse LoadAndHighlightFields
+ desc := &search.DocumentMatch{}
+ for _, descID := range root.Descendants {
+ extID, err := r.ExternalID(descID)
+ if err != nil {
+ return err, totalStoredFieldsBytes
+ }
+ // reset desc for reuse
+ desc.ID = extID
+ desc.IndexInternalID = descID
+ desc.Locations = root.Locations
+ err, bytes := LoadAndHighlightFields(desc, req, indexName, r, highlighter)
+ totalStoredFieldsBytes += bytes
+ if err != nil {
+ return err, totalStoredFieldsBytes
+ }
+ // copy fields to nested doc and append
+ if len(desc.Fields) != 0 || len(desc.Fragments) != 0 {
+ nestedDocs = append(nestedDocs, search.NewNestedDocumentMatch(desc.Fields, desc.Fragments))
+ }
+ desc.Fields = nil
+ desc.Fragments = nil
+ }
+ // add nested documents to root under _$nested key
+ if len(nestedDocs) > 0 {
+ root.AddFieldValue(NestedDocumentKey, nestedDocs)
+ }
+ return nil, totalStoredFieldsBytes
+}
+
// Fields returns the name of all the fields this
// Index has operated on.
func (i *indexImpl) Fields() (fields []string, err error) {
@@ -1388,11 +1508,43 @@ func (i *indexImpl) CopyTo(d index.Directory) (err error) {
err = copyReader.CopyTo(d)
if err != nil {
- return fmt.Errorf("error copying index metadata: %v", err)
+ return fmt.Errorf("error copying index data: %v", err)
}
// copy the metadata
- return i.meta.CopyTo(d)
+ return i.meta.CopyTo(i.path, d)
+}
+
+func (i *indexImpl) CopyFile(file string, d index.IndexDirectory) (err error) {
+ i.mutex.RLock()
+ defer i.mutex.RUnlock()
+
+ if !i.open {
+ return ErrorIndexClosed
+ }
+
+ fileCopyIndex, ok := i.i.(IndexFileCopyable)
+ if !ok {
+ return fmt.Errorf("index implementation does not support file copy reader")
+ }
+
+ return fileCopyIndex.CopyFile(file, d)
+}
+
+func (i *indexImpl) SetPathInBolt(key []byte, value []byte) error {
+ i.mutex.RLock()
+ defer i.mutex.RUnlock()
+
+ if !i.open {
+ return ErrorIndexClosed
+ }
+
+ fileCopyIndex, ok := i.i.(IndexFileCopyable)
+ if !ok {
+ return fmt.Errorf("index implementation does not support file copy")
+ }
+
+ return fileCopyIndex.SetPathInBolt(key, value)
}
func (f FileSystemDirectory) GetWriter(filePath string) (io.WriteCloser,
@@ -1487,3 +1639,39 @@ func (i *indexImpl) CentroidCardinalities(field string, limit int, descending bo
return centroidCardinalities, nil
}
+
+func (i *indexImpl) buildTopNCollector(ctx context.Context, req *SearchRequest, reader index.IndexReader) (*collector.TopNCollector, error) {
+ newCollector := func() *collector.TopNCollector {
+ if req.SearchAfter != nil {
+ return collector.NewTopNCollectorAfter(req.Size, req.Sort, req.SearchAfter)
+ }
+ return collector.NewTopNCollector(req.Size, req.From, req.Sort)
+ }
+
+ newNestedCollector := func(nr index.NestedReader) *collector.TopNCollector {
+ if req.SearchAfter != nil {
+ return collector.NewNestedTopNCollectorAfter(req.Size, req.Sort, req.SearchAfter, nr)
+ }
+ return collector.NewNestedTopNCollector(req.Size, req.From, req.Sort, nr)
+ }
+
+ // check if we are in nested mode
+ if nestedMode, ok := ctx.Value(search.NestedSearchKey).(bool); ok && nestedMode {
+ // get the nested reader from the index reader
+ if nr, ok := reader.(index.NestedReader); ok {
+ // check if the mapping has any nested fields that intersect
+ if nm, ok := i.m.(mapping.NestedMapping); ok {
+ var fs search.FieldSet
+ var err error
+ fs, err = query.ExtractFields(req.Query, i.m, fs)
+ if err != nil {
+ return nil, err
+ }
+ if fs.HasID() || nm.IntersectsPrefix(fs) {
+ return newNestedCollector(nr), nil
+ }
+ }
+ }
+ }
+ return newCollector(), nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index_meta.go b/vendor/github.com/blevesearch/bleve/v2/index_meta.go
index 14b88dcbc0..ca6c7c7ad1 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index_meta.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index_meta.go
@@ -15,6 +15,7 @@
package bleve
import (
+ "encoding/binary"
"fmt"
"os"
"path/filepath"
@@ -27,17 +28,30 @@ import (
const metaFilename = "index_meta.json"
type indexMeta struct {
- Storage string `json:"storage"`
- IndexType string `json:"index_type"`
- Config map[string]interface{} `json:"config,omitempty"`
+ Storage string `json:"storage"`
+ IndexType string `json:"index_type"`
+ Config map[string]interface{} `json:"config,omitempty"`
+ fileWriter util.FileWriter
+ fileReader util.FileReader
}
-func newIndexMeta(indexType string, storage string, config map[string]interface{}) *indexMeta {
- return &indexMeta{
- IndexType: indexType,
- Storage: storage,
- Config: config,
+func newIndexMeta(indexType string, storage string, config map[string]interface{}, path string) (*indexMeta, error) {
+ indexMetaPath := indexMetaPath(path)
+ fileWriter, err := util.NewFileWriter([]byte(indexMetaPath))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create file writer for index meta: %w", err)
}
+ fileReader, err := util.NewFileReader(fileWriter.Id(), []byte(indexMetaPath))
+ if err != nil {
+ return nil, fmt.Errorf("failed to create file reader for index meta: %w", err)
+ }
+ return &indexMeta{
+ IndexType: indexType,
+ Storage: storage,
+ Config: config,
+ fileWriter: fileWriter,
+ fileReader: fileReader,
+ }, nil
}
func openIndexMeta(path string) (*indexMeta, error) {
@@ -49,11 +63,60 @@ func openIndexMeta(path string) (*indexMeta, error) {
if err != nil {
return nil, ErrorIndexMetaMissing
}
+
+ // check if indexMetaPath+_temp exists, if so, this means a writer update was in progress
+ // and we should attempt to recover using the temp file
+ if _, err := os.Stat(indexMetaPath + "_temp"); err == nil {
+ tempBytes, err := os.ReadFile(indexMetaPath + "_temp")
+ if err == nil {
+ err = os.Rename(indexMetaPath+"_temp", indexMetaPath)
+ if err != nil {
+ return nil, err
+ }
+ metaBytes = tempBytes
+ }
+ }
+
var im indexMeta
+ var fileReader util.FileReader
+ // attempt to unmarshal metabytes directly. If this succeeds,
+ // then we know there was no file callback writer used and we can
+ // proceed as normal.
err = util.UnmarshalJSON(metaBytes, &im)
if err != nil {
- return nil, ErrorIndexMetaCorrupt
+ // on failure, we expect the last 4 bytes to be the length of the file
+ // callback id and the preceding bytes to be the file callback id, which
+ // we can use to obtain the file reader to read the actual meta data bytes
+ if len(metaBytes) < 4 {
+ return nil, ErrorIndexMetaCorrupt
+ }
+
+ // read the length of the file callback id from the last 4 bytes
+ pos := len(metaBytes) - 4
+ fileWriterIDLen := int(binary.BigEndian.Uint32(metaBytes[pos:]))
+ pos -= fileWriterIDLen
+ if pos < 0 {
+ return nil, ErrorIndexMetaCorrupt
+ }
+
+ // read and initialize the file reader using the file callback id
+ fileWriterID := metaBytes[pos : pos+fileWriterIDLen]
+ fileReader, err = util.NewFileReader(string(fileWriterID), []byte(indexMetaPath))
+ if err != nil {
+ return nil, err
+ }
+
+ buf, err := fileReader.Process(metaBytes[0:pos])
+ if err != nil {
+ return nil, err
+ }
+ err = util.UnmarshalJSON(buf, &im)
+ if err != nil {
+ return nil, ErrorIndexMetaCorrupt
+ }
}
+ im.fileReader = fileReader
+
if im.IndexType == "" {
im.IndexType = upsidedown.Name
}
@@ -86,15 +149,29 @@ func (i *indexMeta) Save(path string) (err error) {
err = ierr
}
}()
+
+ metaBytes = i.fileWriter.Process(metaBytes)
+
_, err = indexMetaFile.Write(metaBytes)
if err != nil {
return err
}
+
+ _, err = indexMetaFile.Write([]byte(i.fileWriter.Id()))
+ if err != nil {
+ return err
+ }
+
+ err = binary.Write(indexMetaFile, binary.BigEndian, uint32(len(i.fileWriter.Id())))
+ if err != nil {
+ return err
+ }
+
return nil
}
-func (i *indexMeta) CopyTo(d index.Directory) (err error) {
- metaBytes, err := util.MarshalJSON(i)
+func (i *indexMeta) CopyTo(path string, d index.Directory) (err error) {
+ metaBytes, err := os.ReadFile(indexMetaPath(path))
if err != nil {
return err
}
@@ -110,6 +187,69 @@ func (i *indexMeta) CopyTo(d index.Directory) (err error) {
return err
}
+// updates the file callback writer id in the index meta,
+// and re-processes data with the latest file callback writer
+// returns the new file callback writer and reader to be used for
+// future processing of index meta data
+func (i *indexMeta) UpdateWriter(path string) error {
+ indexMetaPath := indexMetaPath(path)
+ metaBytes, err := util.MarshalJSON(i)
+ if err != nil {
+ return err
+ }
+
+ i.fileWriter, err = util.NewFileWriter([]byte(indexMetaPath))
+ if err != nil {
+ return err
+ }
+ metaBytes = i.fileWriter.Process(metaBytes)
+
+ // write out new meta with new writer id, using temp file and rename to ensure atomicity
+ // if we crash in the middle of this, on next open we will see the temp file and recover using it
+ tempMetaPath := indexMetaPath + "_temp"
+ tempMetaFile, err := os.OpenFile(tempMetaPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0666)
+ if err != nil {
+ if os.IsExist(err) {
+ return ErrorIndexPathExists
+ }
+ return err
+ }
+
+ // write the meta bytes
+ _, err = tempMetaFile.Write(metaBytes)
+ if err != nil {
+ return err
+ }
+ // write the file callback id
+ _, err = tempMetaFile.Write([]byte(i.fileWriter.Id()))
+ if err != nil {
+ return err
+ }
+ // write the length of the file callback id
+ err = binary.Write(tempMetaFile, binary.BigEndian, uint32(len(i.fileWriter.Id())))
+ if err != nil {
+ return err
+ }
+ // close file before renaming
+ err = tempMetaFile.Close()
+ if err != nil {
+ return err
+ }
+ // atomically rename temp file to index meta file
+ err = os.Rename(tempMetaPath, indexMetaPath)
+ if err != nil {
+ return err
+ }
+
+ // initialize the new file reader for index meta
+ i.fileReader, err = util.NewFileReader(string(i.fileWriter.Id()), []byte(indexMetaPath))
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
func indexMetaPath(path string) string {
return filepath.Join(path, metaFilename)
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/index_update.go b/vendor/github.com/blevesearch/bleve/v2/index_update.go
index 5666d035be..f16b640d63 100644
--- a/vendor/github.com/blevesearch/bleve/v2/index_update.go
+++ b/vendor/github.com/blevesearch/bleve/v2/index_update.go
@@ -180,6 +180,10 @@ func checkUpdatedMapping(ori, upd *mapping.DocumentMapping) error {
return nil
}
+ if ori.Nested != upd.Nested {
+ return fmt.Errorf("nested property cannot be changed")
+ }
+
var err error
// Recursively go through the child mappings
for name, updDMapping := range upd.Properties {
@@ -507,6 +511,9 @@ func compareFieldMapping(original, updated *mapping.FieldMapping) (*index.Update
if original.VectorIndexOptimizedFor != updated.VectorIndexOptimizedFor {
return nil, fmt.Errorf("vectorIndexOptimizedFor cannot be updated for vector and vector_base64 fields")
}
+ if original.GPU != updated.GPU {
+ return nil, fmt.Errorf("gpu cannot be updated for vector and vector_base64 fields")
+ }
}
if original.IncludeInAll != updated.IncludeInAll {
return nil, fmt.Errorf("includeInAll cannot be changed")
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping.go b/vendor/github.com/blevesearch/bleve/v2/mapping.go
index 723105a294..af02db386a 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping.go
@@ -34,6 +34,20 @@ func NewDocumentStaticMapping() *mapping.DocumentMapping {
return mapping.NewDocumentStaticMapping()
}
+// NewNestedDocumentMapping returns a new document mapping
+// that will treat all objects as nested documents.
+func NewNestedDocumentMapping() *mapping.DocumentMapping {
+ return mapping.NewNestedDocumentMapping()
+}
+
+// NewNestedDocumentStaticMapping returns a new document mapping
+// that will treat all objects as nested documents and
+// will not automatically index parts of a nested document
+// without an explicit mapping.
+func NewNestedDocumentStaticMapping() *mapping.DocumentMapping {
+ return mapping.NewNestedDocumentStaticMapping()
+}
+
// NewDocumentDisabledMapping returns a new document
// mapping that will not perform any indexing.
func NewDocumentDisabledMapping() *mapping.DocumentMapping {
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping/document.go b/vendor/github.com/blevesearch/bleve/v2/mapping/document.go
index a78b27e11d..3da925038b 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping/document.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping/document.go
@@ -22,6 +22,7 @@ import (
"reflect"
"time"
+ "github.com/blevesearch/bleve/v2/document"
"github.com/blevesearch/bleve/v2/registry"
"github.com/blevesearch/bleve/v2/util"
)
@@ -44,6 +45,7 @@ type DocumentMapping struct {
Dynamic bool `json:"dynamic"`
Properties map[string]*DocumentMapping `json:"properties,omitempty"`
Fields []*FieldMapping `json:"fields,omitempty"`
+ Nested bool `json:"nested,omitempty"`
DefaultAnalyzer string `json:"default_analyzer,omitempty"`
DefaultSynonymSource string `json:"default_synonym_source,omitempty"`
@@ -230,6 +232,17 @@ func NewDocumentMapping() *DocumentMapping {
}
}
+// NewNestedDocumentMapping returns a new document
+// mapping that treats sub-documents as nested
+// objects.
+func NewNestedDocumentMapping() *DocumentMapping {
+ return &DocumentMapping{
+ Nested: true,
+ Enabled: true,
+ Dynamic: true,
+ }
+}
+
// NewDocumentStaticMapping returns a new document
// mapping that will not automatically index parts
// of a document without an explicit mapping.
@@ -239,6 +252,17 @@ func NewDocumentStaticMapping() *DocumentMapping {
}
}
+// NewNestedDocumentStaticMapping returns a new document
+// mapping that treats sub-documents as nested
+// objects and will not automatically index parts
+// of the nested document without an explicit mapping.
+func NewNestedDocumentStaticMapping() *DocumentMapping {
+ return &DocumentMapping{
+ Enabled: true,
+ Nested: true,
+ }
+}
+
// NewDocumentDisabledMapping returns a new document
// mapping that will not perform any indexing.
func NewDocumentDisabledMapping() *DocumentMapping {
@@ -312,6 +336,11 @@ func (dm *DocumentMapping) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
+ case "nested":
+ err := util.UnmarshalJSON(v, &dm.Nested)
+ if err != nil {
+ return err
+ }
case "default_analyzer":
err := util.UnmarshalJSON(v, &dm.DefaultAnalyzer)
if err != nil {
@@ -381,6 +410,18 @@ func (dm *DocumentMapping) defaultSynonymSource(path []string) string {
return rv
}
+// baseType returns the base type of v by dereferencing pointers
+func baseType(v interface{}) reflect.Type {
+ if v == nil {
+ return nil
+ }
+ t := reflect.TypeOf(v)
+ for t.Kind() == reflect.Pointer {
+ t = t.Elem()
+ }
+ return t
+}
+
func (dm *DocumentMapping) walkDocument(data interface{}, path []string, indexes []uint64, context *walkContext) {
// allow default "json" tag to be overridden
structTagKey := dm.StructTagKey
@@ -434,11 +475,39 @@ func (dm *DocumentMapping) walkDocument(data interface{}, path []string, indexes
}
}
case reflect.Slice, reflect.Array:
+ subDocMapping, _ := dm.documentMappingForPathElements(path)
+ allowNested := subDocMapping != nil && subDocMapping.Nested
for i := 0; i < val.Len(); i++ {
- if val.Index(i).CanInterface() {
- fieldVal := val.Index(i).Interface()
- dm.processProperty(fieldVal, path, append(indexes, uint64(i)), context)
+ // for each array element, check if it can be represented as an interface
+ idxVal := val.Index(i)
+ // skip invalid values
+ if !idxVal.CanInterface() {
+ continue
}
+ // get the actual value in interface form
+ actual := idxVal.Interface()
+ // if nested mapping, only create nested document for object elements
+ if allowNested && actual != nil {
+ // check the kind of the actual value, is it an object (struct or map)?
+ typ := baseType(actual)
+ if typ == nil {
+ continue
+ }
+ kind := typ.Kind()
+ // only create nested docs for real JSON objects
+ if kind == reflect.Struct || kind == reflect.Map {
+ // Create nested document only for only object elements
+ nestedDocument := document.NewDocument(
+ fmt.Sprintf("%s_$%s_$%d", context.doc.ID(), encodePath(path), i))
+ nestedContext := context.im.newWalkContext(nestedDocument, dm)
+ dm.processProperty(actual, path, append(indexes, uint64(i)), nestedContext)
+ context.doc.AddNestedDocument(nestedDocument)
+ continue
+ }
+ }
+ // non-nested mapping, or non-object element in nested mapping
+ // process the element normally
+ dm.processProperty(actual, path, append(indexes, uint64(i)), context)
}
case reflect.Ptr:
ptrElem := val.Elem()
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping/field.go b/vendor/github.com/blevesearch/bleve/v2/mapping/field.go
index 0b60749102..53c8dc61d9 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping/field.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping/field.go
@@ -83,6 +83,9 @@ type FieldMapping struct {
VectorIndexOptimizedFor string `json:"vector_index_optimized_for,omitempty"`
SynonymSource string `json:"synonym_source,omitempty"`
+
+ // Applicable to vector fields only - enables GPU acceleration for indexing and searching
+ GPU bool `json:"gpu,omitempty"`
}
// NewTextFieldMapping returns a default field mapping for text
@@ -226,6 +229,9 @@ func (fm *FieldMapping) Options() index.FieldIndexingOptions {
if fm.SkipFreqNorm {
rv |= index.SkipFreqNorm
}
+ if fm.GPU {
+ rv |= index.GPU
+ }
return rv
}
@@ -479,6 +485,11 @@ func (fm *FieldMapping) UnmarshalJSON(data []byte) error {
if err != nil {
return err
}
+ case "gpu":
+ err := util.UnmarshalJSON(v, &fm.GPU)
+ if err != nil {
+ return err
+ }
default:
invalidKeys = append(invalidKeys, k)
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping/index.go b/vendor/github.com/blevesearch/bleve/v2/mapping/index.go
index 7878cce8bb..143ff5a313 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping/index.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping/index.go
@@ -17,12 +17,14 @@ package mapping
import (
"encoding/json"
"fmt"
+ "strings"
"github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/analysis/analyzer/standard"
"github.com/blevesearch/bleve/v2/analysis/datetime/optional"
"github.com/blevesearch/bleve/v2/document"
"github.com/blevesearch/bleve/v2/registry"
+ "github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
)
@@ -195,11 +197,19 @@ func (im *IndexMappingImpl) Validate() error {
// the map will hold the fully qualified field name to FieldMapping, so we can
// check for conflicts as we validate each DocumentMapping.
fieldAliasCtx := make(map[string]*FieldMapping)
+ // ensure that the nested property is not set for top-level default mapping
+ if im.DefaultMapping.Nested {
+ return fmt.Errorf("default mapping cannot be nested")
+ }
err = im.DefaultMapping.Validate(im.cache, []string{}, fieldAliasCtx)
if err != nil {
return err
}
- for _, docMapping := range im.TypeMapping {
+ for name, docMapping := range im.TypeMapping {
+ // ensure that the nested property is not set for top-level mappings
+ if docMapping.Nested {
+ return fmt.Errorf("type mapping named: %s cannot be nested", name)
+ }
err = docMapping.Validate(im.cache, []string{}, fieldAliasCtx)
if err != nil {
return err
@@ -574,3 +584,70 @@ func (im *IndexMappingImpl) SynonymSourceVisitor(visitor analysis.SynonymSourceV
}
return nil
}
+
+func (im *IndexMappingImpl) buildNestedPrefixes() map[string]int {
+ prefixDepth := make(map[string]int)
+ var collectNestedFields func(dm *DocumentMapping, pathComponents []string, currentDepth int)
+ collectNestedFields = func(dm *DocumentMapping, pathComponents []string, currentDepth int) {
+ for name, docMapping := range dm.Properties {
+ newPathComponents := append(pathComponents, name)
+ if docMapping.Nested {
+ // This is a nested field boundary
+ newDepth := currentDepth + 1
+ prefixDepth[strings.Join(newPathComponents, pathSeparator)] = newDepth
+ // Continue deeper with incremented depth
+ collectNestedFields(docMapping, newPathComponents, newDepth)
+ } else {
+ // Not nested, continue with same depth
+ collectNestedFields(docMapping, newPathComponents, currentDepth)
+ }
+ }
+ }
+ // Start from depth 0 (root)
+ if im.DefaultMapping != nil && im.DefaultMapping.Enabled {
+ collectNestedFields(im.DefaultMapping, []string{}, 0)
+ }
+ // Now do this for each type mapping
+ for _, docMapping := range im.TypeMapping {
+ if docMapping.Enabled {
+ collectNestedFields(docMapping, []string{}, 0)
+ }
+ }
+ return prefixDepth
+}
+
+func (im *IndexMappingImpl) NestedDepth(fs search.FieldSet) (int, int) {
+ if im.cache == nil || im.cache.NestedPrefixes == nil {
+ return 0, 0
+ }
+
+ im.cache.NestedPrefixes.InitOnce(func() map[string]int {
+ return im.buildNestedPrefixes()
+ })
+
+ return im.cache.NestedPrefixes.NestedDepth(fs)
+}
+
+func (im *IndexMappingImpl) CountNested() int {
+ if im.cache == nil || im.cache.NestedPrefixes == nil {
+ return 0
+ }
+
+ im.cache.NestedPrefixes.InitOnce(func() map[string]int {
+ return im.buildNestedPrefixes()
+ })
+
+ return im.cache.NestedPrefixes.CountNested()
+}
+
+func (im *IndexMappingImpl) IntersectsPrefix(fs search.FieldSet) bool {
+ if im.cache == nil || im.cache.NestedPrefixes == nil {
+ return false
+ }
+
+ im.cache.NestedPrefixes.InitOnce(func() map[string]int {
+ return im.buildNestedPrefixes()
+ })
+
+ return im.cache.NestedPrefixes.IntersectsPrefix(fs)
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping/mapping.go b/vendor/github.com/blevesearch/bleve/v2/mapping/mapping.go
index a6c1591b88..7ff2f99278 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping/mapping.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping/mapping.go
@@ -20,6 +20,7 @@ import (
"github.com/blevesearch/bleve/v2/analysis"
"github.com/blevesearch/bleve/v2/document"
+ "github.com/blevesearch/bleve/v2/search"
)
// A Classifier is an interface describing any object which knows how to
@@ -74,3 +75,21 @@ type SynonymMapping interface {
SynonymSourceVisitor(visitor analysis.SynonymSourceVisitor) error
}
+
+// A NestedMapping extends the IndexMapping interface to provide
+// additional methods for working with nested object mappings.
+type NestedMapping interface {
+ // NestedDepth returns two values:
+ // - common: the highest nested level that is common to all given field paths,
+ // if 0 then there is no common nested level among the given field paths
+ // - max: the highest nested level that applies to at least one of the given field paths
+ // if 0 then none of the given field paths are nested
+ NestedDepth(fieldPaths search.FieldSet) (int, int)
+
+ // IntersectsPrefix returns true if any of the given
+ // field paths intersect with a known nested prefix
+ IntersectsPrefix(fieldPaths search.FieldSet) bool
+
+ // CountNested returns the number of nested object mappings
+ CountNested() int
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/mapping/mapping_vectors.go b/vendor/github.com/blevesearch/bleve/v2/mapping/mapping_vectors.go
index 393262b357..81d2cb9a3e 100644
--- a/vendor/github.com/blevesearch/bleve/v2/mapping/mapping_vectors.go
+++ b/vendor/github.com/blevesearch/bleve/v2/mapping/mapping_vectors.go
@@ -151,6 +151,12 @@ func (fm *FieldMapping) processVector(propertyMightBeVector interface{},
if vectorIndexOptimizedFor == "" {
vectorIndexOptimizedFor = index.DefaultIndexOptimization
}
+ // bivf indexes only supports hamming distance for the primary
+ // binary index. Similarity here is used for the backing flat index,
+ // which is set to cosine similarity for recall reasons
+ if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) {
+ similarity = index.CosineSimilarity
+ }
// normalize raw vector if similarity is cosine
// Since the vector can be multi-vector (flattened array of multiple vectors),
// we use NormalizeMultiVector to normalize each sub-vector independently.
@@ -185,6 +191,12 @@ func (fm *FieldMapping) processVectorBase64(propertyMightBeVectorBase64 interfac
if vectorIndexOptimizedFor == "" {
vectorIndexOptimizedFor = index.DefaultIndexOptimization
}
+ // bivf indexes only supports hamming distance for the primary
+ // binary index. Similarity here is used for the backing flat index,
+ // which is set to cosine similarity for recall reasons
+ if index.OptimizationRequiresBinaryIndex(vectorIndexOptimizedFor) {
+ similarity = index.CosineSimilarity
+ }
decodedVector, err := document.DecodeVector(encodedString)
if err != nil || len(decodedVector) != fm.Dims {
return
@@ -197,6 +209,7 @@ func (fm *FieldMapping) processVectorBase64(propertyMightBeVectorBase64 interfac
fieldName := getFieldName(pathString, path, fm)
options := fm.Options()
+
field := document.NewVectorFieldWithIndexingOptions(fieldName, indexes, decodedVector,
fm.Dims, similarity, vectorIndexOptimizedFor, options)
context.doc.AddField(field)
@@ -264,6 +277,11 @@ func validateVectorFieldAlias(field *FieldMapping, path []string,
"(different vector index optimization values %s and %s)", effectiveFieldName,
effectiveOptimizedFor, aliasOptimizedFor)
}
+ if field.GPU != fieldAlias.GPU {
+ return fmt.Errorf("field: '%s', invalid alias "+
+ "(different gpu values %v and %v)", effectiveFieldName,
+ field.GPU, fieldAlias.GPU)
+ }
return nil
}
@@ -288,6 +306,11 @@ func validateVectorFieldAlias(field *FieldMapping, path []string,
effectiveOptimizedFor,
reflect.ValueOf(index.SupportedVectorIndexOptimizations).MapKeys())
}
+ // bivf indexes requires vector dimensionality to be a multiple of 8
+ if index.OptimizationRequiresBinaryIndex(effectiveOptimizedFor) && field.Dims%8 != 0 {
+ return fmt.Errorf("field: '%s', incompatible vector dimensionality for BIVF: %d,"+
+ " dimension should be a multiple of 8", effectiveFieldName, field.Dims)
+ }
if fieldAliasCtx != nil { // writing to a nil map is unsafe
fieldAliasCtx[effectiveFieldName] = field
diff --git a/vendor/github.com/blevesearch/bleve/v2/numeric/prefix_coded.go b/vendor/github.com/blevesearch/bleve/v2/numeric/prefix_coded.go
index 29bd0fc5c1..03ba043e37 100644
--- a/vendor/github.com/blevesearch/bleve/v2/numeric/prefix_coded.go
+++ b/vendor/github.com/blevesearch/bleve/v2/numeric/prefix_coded.go
@@ -66,6 +66,14 @@ func MustNewPrefixCodedInt64(in int64, shift uint) PrefixCoded {
return rv
}
+func MustNewPrefixCodedInt64Prealloc(in int64, shift uint, prealloc []byte) PrefixCoded {
+ rv, _, err := NewPrefixCodedInt64Prealloc(in, shift, prealloc)
+ if err != nil {
+ panic(err)
+ }
+ return rv
+}
+
// Shift returns the number of bits shifted
// returns 0 if in uninitialized state
func (p PrefixCoded) Shift() (uint, error) {
diff --git a/vendor/github.com/blevesearch/bleve/v2/registry/nested.go b/vendor/github.com/blevesearch/bleve/v2/registry/nested.go
new file mode 100644
index 0000000000..fee7fda62f
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/registry/nested.go
@@ -0,0 +1,136 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package registry
+
+import (
+ "strings"
+ "sync"
+
+ "github.com/blevesearch/bleve/v2/search"
+)
+
+// NestedFieldCache caches nested field prefixes and their corresponding nesting levels.
+// A nested field prefix is a field path prefix that indicates the start of a nested document.
+// The nesting level indicates how deep the nested document is in the overall document structure.
+type NestedFieldCache struct {
+ // nested prefix -> nested level
+ prefixDepth map[string]int
+ once sync.Once
+ m sync.RWMutex
+}
+
+func NewNestedFieldCache() *NestedFieldCache {
+ return &NestedFieldCache{}
+}
+
+func (nfc *NestedFieldCache) InitOnce(buildFunc func() map[string]int) {
+ nfc.once.Do(func() {
+ nfc.m.Lock()
+ defer nfc.m.Unlock()
+ nfc.prefixDepth = buildFunc()
+ })
+}
+
+// NestedDepth returns two values:
+// - common: The nesting level of the longest prefix that applies to every field path
+// in the provided FieldSet. A value of 0 means no nested prefix is shared
+// across all field paths.
+// - max: The nesting level of the longest prefix that applies to at least one
+// field path in the provided FieldSet. A value of 0 means none of the
+// field paths match any nested prefix.
+func (nfc *NestedFieldCache) NestedDepth(fieldPaths search.FieldSet) (common int, max int) {
+ // if no field paths, no nested depth
+ if len(fieldPaths) == 0 {
+ return
+ }
+ nfc.m.RLock()
+ defer nfc.m.RUnlock()
+ // if no cached prefixes, no nested depth
+ if len(nfc.prefixDepth) == 0 {
+ return
+ }
+ // for each prefix, check if its a common prefix or matches any path
+ // update common and max accordingly with the highest nesting level
+ // possible for each respective case
+ for prefix, level := range nfc.prefixDepth {
+ // only check prefixes that could increase one of the results
+ if level <= common && level <= max {
+ continue
+ }
+ // check prefix against field paths, getting whether it matches all paths (common)
+ // and whether it matches at least one path (any)
+ matchAll, matchAny := nfc.prefixMatch(prefix, fieldPaths)
+ // if it matches all paths, update common
+ if matchAll && level > common {
+ common = level
+ }
+ // if it matches any path, update max
+ if matchAny && level > max {
+ max = level
+ }
+ }
+ return common, max
+}
+
+// CountNested returns the number of nested prefixes
+func (nfc *NestedFieldCache) CountNested() int {
+ nfc.m.RLock()
+ defer nfc.m.RUnlock()
+
+ return len(nfc.prefixDepth)
+}
+
+// IntersectsPrefix returns true if any of the given
+// field paths have a nested prefix
+func (nfc *NestedFieldCache) IntersectsPrefix(fieldPaths search.FieldSet) bool {
+ // if no field paths, no intersection
+ if len(fieldPaths) == 0 {
+ return false
+ }
+ nfc.m.RLock()
+ defer nfc.m.RUnlock()
+ // if no cached prefixes, no intersection
+ if len(nfc.prefixDepth) == 0 {
+ return false
+ }
+ // Check each cached nested prefix to see if it intersects with any path
+ for prefix := range nfc.prefixDepth {
+ _, matchAny := nfc.prefixMatch(prefix, fieldPaths)
+ if matchAny {
+ return true
+ }
+ }
+ return false
+}
+
+// prefixMatch checks whether the prefix matches all paths (common) and whether it matches at least one path (any)
+// Caller must hold the read lock.
+func (nfc *NestedFieldCache) prefixMatch(prefix string, fieldPaths search.FieldSet) (common bool, any bool) {
+ common = true
+ any = false
+ for path := range fieldPaths {
+ has := strings.HasPrefix(path, prefix)
+ if has {
+ any = true
+ } else {
+ common = false
+ }
+ // early exit if we have determined both values
+ if any && !common {
+ break
+ }
+ }
+ return common, any
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/registry/registry.go b/vendor/github.com/blevesearch/bleve/v2/registry/registry.go
index 69ee8dd86a..36f209d4f0 100644
--- a/vendor/github.com/blevesearch/bleve/v2/registry/registry.go
+++ b/vendor/github.com/blevesearch/bleve/v2/registry/registry.go
@@ -49,6 +49,7 @@ type Cache struct {
Fragmenters *FragmenterCache
Highlighters *HighlighterCache
SynonymSources *SynonymSourceCache
+ NestedPrefixes *NestedFieldCache
}
func NewCache() *Cache {
@@ -63,6 +64,7 @@ func NewCache() *Cache {
Fragmenters: NewFragmenterCache(),
Highlighters: NewHighlighterCache(),
SynonymSources: NewSynonymSourceCache(),
+ NestedPrefixes: NewNestedFieldCache(),
}
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search.go b/vendor/github.com/blevesearch/bleve/v2/search.go
index ee53ac6e2a..708be0871e 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search.go
@@ -15,9 +15,12 @@
package bleve
import (
+ "bytes"
+ "encoding/json"
"fmt"
"reflect"
"regexp"
+ "slices"
"sort"
"strconv"
"strings"
@@ -625,11 +628,35 @@ func formatHit(rv *strings.Builder, hit *search.DocumentMatch, hitNumber int) *s
}
}
for otherFieldName, otherFieldValue := range hit.Fields {
+ if otherFieldName == NestedDocumentKey {
+ continue
+ }
if _, ok := hit.Fragments[otherFieldName]; !ok {
fmt.Fprintf(rv, "\t%s\n", otherFieldName)
fmt.Fprintf(rv, "\t\t%v\n", otherFieldValue)
}
}
+ // nested documents
+ if nested, ok := hit.Fields[NestedDocumentKey]; ok {
+ if list, ok := nested.([]*search.NestedDocumentMatch); ok {
+ fmt.Fprintf(rv, "\t%s (%d nested documents)\n", NestedDocumentKey, len(list))
+ for ni, nd := range list {
+ fmt.Fprintf(rv, "\t\tNested #%d:\n", ni+1)
+ for f, frags := range nd.Fragments {
+ fmt.Fprintf(rv, "\t\t\t%s\n", f)
+ for _, frag := range frags {
+ fmt.Fprintf(rv, "\t\t\t\t%s\n", frag)
+ }
+ }
+ for f, v := range nd.Fields {
+ if _, ok := nd.Fragments[f]; !ok {
+ fmt.Fprintf(rv, "\t\t\t%s\n", f)
+ fmt.Fprintf(rv, "\t\t\t\t%v\n", v)
+ }
+ }
+ }
+ }
+ }
if len(hit.DecodedSort) > 0 {
fmt.Fprintf(rv, "\t_sort: [")
for k, v := range hit.DecodedSort {
@@ -806,3 +833,22 @@ func ParseParams(r *SearchRequest, input []byte) (*RequestParams, error) {
return params, nil
}
+
+// OptionalRawMessage is a wrapper around json.RawMessage that treats empty or `null` JSON as nil.
+type OptionalRawMessage json.RawMessage
+
+func (n *OptionalRawMessage) UnmarshalJSON(data []byte) error {
+ if len(data) == 0 || bytes.Equal(data, []byte("null")) {
+ *n = nil
+ return nil
+ }
+ *n = slices.Clone(data)
+ return nil
+}
+
+func (n OptionalRawMessage) MarshalJSON() ([]byte, error) {
+ if len(n) == 0 {
+ return []byte("null"), nil
+ }
+ return n, nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/collector/nested.go b/vendor/github.com/blevesearch/bleve/v2/search/collector/nested.go
new file mode 100644
index 0000000000..ce2f790908
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/collector/nested.go
@@ -0,0 +1,103 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package collector
+
+import (
+ "github.com/blevesearch/bleve/v2/search"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+type collectStoreNested struct {
+ // descAdder is used to customize how descendants are merged into their parent
+ descAdder search.DescendantAdderCallbackFn
+ // nested reader to retrieve ancestor information
+ nr index.NestedReader
+ // the current root document match being built
+ currRoot *search.DocumentMatch
+ // the ancestor ID of the current root document being built
+ currRootAncestorID index.AncestorID
+ // prealloc slice for ancestor IDs
+ ancestors []index.AncestorID
+}
+
+func newStoreNested(nr index.NestedReader, descAdder search.DescendantAdderCallbackFn) *collectStoreNested {
+ rv := &collectStoreNested{
+ descAdder: descAdder,
+ nr: nr,
+ }
+ return rv
+}
+
+// ProcessNestedDocument adds a document to the nested store, merging it into its root document
+// as needed. If the returned DocumentMatch is nil, the incoming doc has been merged
+// into its parent and should not be processed further. If the returned DocumentMatch
+// is non-nil, it represents a complete root document that should be processed further.
+// NOTE: This implementation assumes that documents are added in increasing order of their internal IDs
+// which is guaranteed by all searchers in bleve.
+func (c *collectStoreNested) ProcessNestedDocument(ctx *search.SearchContext, doc *search.DocumentMatch) (*search.DocumentMatch, error) {
+ // find ancestors for the doc
+ var err error
+ c.ancestors, err = c.nr.Ancestors(doc.IndexInternalID, c.ancestors[:0])
+ if err != nil {
+ return nil, err
+ }
+ if len(c.ancestors) == 0 {
+ // should not happen, every doc should have at least itself as ancestor
+ return nil, nil
+ }
+ // root docID is the last ancestor
+ rootID := c.ancestors[len(c.ancestors)-1]
+ // check if there is an interim root already and if the incoming doc belongs to it
+ if c.currRoot != nil && c.currRootAncestorID.Equals(rootID) {
+ // there is an interim root already, and the incoming doc belongs to it
+ if err := c.descAdder(c.currRoot, doc); err != nil {
+ return nil, err
+ }
+ // recycle the child document now that it's merged into the interim root
+ ctx.DocumentMatchPool.Put(doc)
+ return nil, nil
+ }
+ // completedRoot is the root document match to return, if any
+ var completedRoot *search.DocumentMatch
+ if c.currRoot != nil {
+ // we have an existing interim root, return it for processing
+ completedRoot = c.currRoot
+ }
+ // no interim root for now so either we have a root document incoming
+ // or we have a child doc and need to create an interim root
+ if len(c.ancestors) == 1 {
+ // incoming doc is the root itself
+ c.currRoot = doc
+ c.currRootAncestorID = rootID
+ return completedRoot, nil
+ }
+ // this is a child doc, create interim root
+ newDM := ctx.DocumentMatchPool.Get()
+ newDM.IndexInternalID = rootID.ToIndexInternalID(newDM.IndexInternalID)
+ // merge the incoming doc into the new interim root
+ c.currRoot = newDM
+ c.currRootAncestorID = rootID
+ if err := c.descAdder(c.currRoot, doc); err != nil {
+ return nil, err
+ }
+ // recycle the child document now that it's merged into the interim root
+ ctx.DocumentMatchPool.Put(doc)
+ return completedRoot, nil
+}
+
+// Current returns the current interim root document match being built, if any
+func (c *collectStoreNested) Current() *search.DocumentMatch {
+ return c.currRoot
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/collector/topn.go b/vendor/github.com/blevesearch/bleve/v2/search/collector/topn.go
index 739dd8348d..bab318d5c8 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/collector/topn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/collector/topn.go
@@ -78,7 +78,9 @@ type TopNCollector struct {
searchAfter *search.DocumentMatch
knnHits map[string]*search.DocumentMatch
- computeNewScoreExpl search.ScoreExplCorrectionCallbackFunc
+ hybridMergeCallback search.HybridMergeCallbackFn
+
+ nestedStore *collectStoreNested
}
// CheckDoneEvery controls how frequently we check the context deadline
@@ -88,25 +90,74 @@ const CheckDoneEvery = uint64(1024)
// skipping over the first 'skip' hits
// ordering hits by the provided sort order
func NewTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector {
- return newTopNCollector(size, skip, sort)
+ return newTopNCollector(size, skip, sort, nil)
}
// NewTopNCollectorAfter builds a collector to find the top 'size' hits
// skipping over the first 'skip' hits
// ordering hits by the provided sort order
+// starting after the provided 'after' sort values
func NewTopNCollectorAfter(size int, sort search.SortOrder, after []string) *TopNCollector {
- rv := newTopNCollector(size, 0, sort)
+ rv := newTopNCollector(size, 0, sort, nil)
rv.searchAfter = createSearchAfterDocument(sort, after)
return rv
}
-func newTopNCollector(size int, skip int, sort search.SortOrder) *TopNCollector {
+// NewNestedTopNCollector builds a collector to find the top 'size' hits
+// skipping over the first 'skip' hits
+// ordering hits by the provided sort order
+// while ensuring the nested documents are handled correctly
+// (i.e. parent document is returned instead of nested document)
+func NewNestedTopNCollector(size int, skip int, sort search.SortOrder, nr index.NestedReader) *TopNCollector {
+ return newTopNCollector(size, skip, sort, nr)
+}
+
+// NewNestedTopNCollectorAfter builds a collector to find the top 'size' hits
+// skipping over the first 'skip' hits
+// ordering hits by the provided sort order
+// starting after the provided 'after' sort values
+// while ensuring the nested documents are handled correctly
+// (i.e. parent document is returned instead of nested document)
+func NewNestedTopNCollectorAfter(size int, sort search.SortOrder, after []string, nr index.NestedReader) *TopNCollector {
+ rv := newTopNCollector(size, 0, sort, nr)
+ rv.searchAfter = createSearchAfterDocument(sort, after)
+ return rv
+}
+
+func newTopNCollector(size int, skip int, sort search.SortOrder, nr index.NestedReader) *TopNCollector {
hc := &TopNCollector{size: size, skip: skip, sort: sort}
hc.store = getOptimalCollectorStore(size, skip, func(i, j *search.DocumentMatch) int {
return hc.sort.Compare(hc.cachedScoring, hc.cachedDesc, i, j)
})
+ if nr != nil {
+ descAdder := func(parent, child *search.DocumentMatch) error {
+ // add descendant score to parent score
+ parent.Score += child.Score
+ // merge explanations
+ parent.Expl = parent.Expl.MergeWith(child.Expl)
+ // merge field term locations
+ parent.FieldTermLocations = search.MergeFieldTermLocationsFromMatch(parent.FieldTermLocations, child)
+ // add child's ID to parent's Descendants
+ // add other as descendant only if it is not the same document
+ if !parent.IndexInternalID.Equals(child.IndexInternalID) {
+ // Add a copy of child.IndexInternalID to descendants, because
+ // child.IndexInternalID will be reset when 'child' is recycled.
+ var descendantID index.IndexInternalID
+ // first check if parent's descendants slice has capacity to reuse
+ if len(parent.Descendants) < cap(parent.Descendants) {
+ // reuse the buffer element at len(parent.Descendants)
+ descendantID = parent.Descendants[:len(parent.Descendants)+1][len(parent.Descendants)]
+ }
+ // copy the contents of id into descendantID, allocating if needed
+ parent.Descendants = append(parent.Descendants, index.NewIndexInternalIDFrom(descendantID, child.IndexInternalID))
+ }
+ return nil
+ }
+ hc.nestedStore = newStoreNested(nr, search.DescendantAdderCallbackFn(descAdder))
+ }
+
// these lookups traverse an interface, so do once up-front
if sort.RequiresDocID() {
hc.needDocIds = true
@@ -283,8 +334,13 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
default:
next, err = searcher.Next(searchContext)
}
+ // use a local totalDocs for counting total docs seen
+ // for context deadline checking, as hc.total is only
+ // incremented for actual(root) collected documents, and
+ // we need to check deadline for every document seen (root or nested)
+ var totalDocs uint64
for err == nil && next != nil {
- if hc.total%CheckDoneEvery == 0 {
+ if totalDocs%CheckDoneEvery == 0 {
select {
case <-ctx.Done():
search.RecordSearchCost(ctx, search.AbortM, 0)
@@ -292,27 +348,60 @@ func (hc *TopNCollector) Collect(ctx context.Context, searcher search.Searcher,
default:
}
}
-
- err = hc.adjustDocumentMatch(searchContext, reader, next)
- if err != nil {
- break
+ totalDocs++
+ if hc.nestedStore != nil {
+ // This may be a nested document — add it to the nested store first.
+ // If the nested store returns nil, the document was merged into its parent
+ // and should not be processed further.
+ // If it returns a non-nil document, it represents a complete root document
+ // and should be processed further.
+ next, err = hc.nestedStore.ProcessNestedDocument(searchContext, next)
+ if err != nil {
+ break
+ }
}
-
- err = hc.prepareDocumentMatch(searchContext, reader, next, false)
- if err != nil {
- break
+ if next != nil {
+ err = hc.adjustDocumentMatch(searchContext, reader, next)
+ if err != nil {
+ break
+ }
+ err = hc.prepareDocumentMatch(searchContext, reader, next, false)
+ if err != nil {
+ break
+ }
+ err = dmHandler(next)
+ if err != nil {
+ break
+ }
}
-
- err = dmHandler(next)
- if err != nil {
- break
- }
-
next, err = searcher.Next(searchContext)
}
if err != nil {
return err
}
+
+ // if we have a nested store, we may have an interim root
+ // that needs to be returned for processing
+ if hc.nestedStore != nil {
+ currRoot := hc.nestedStore.Current()
+ if currRoot != nil {
+ err = hc.adjustDocumentMatch(searchContext, reader, currRoot)
+ if err != nil {
+ return err
+ }
+ // no descendants at this point
+ err = hc.prepareDocumentMatch(searchContext, reader, currRoot, false)
+ if err != nil {
+ return err
+ }
+
+ err = dmHandler(currRoot)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
if hc.knnHits != nil {
// we may have some knn hits left that did not match any of the top N tf-idf hits
// we need to add them to the collector store to consider them as well.
@@ -366,7 +455,10 @@ func (hc *TopNCollector) adjustDocumentMatch(ctx *search.SearchContext,
return err
}
if knnHit, ok := hc.knnHits[d.ID]; ok {
- d.Score, d.Expl = hc.computeNewScoreExpl(d, knnHit)
+ // we have a knn hit corresponding to this document
+ hc.hybridMergeCallback(d, knnHit)
+ // remove this knn hit from the map as it's already
+ // been merged
delete(hc.knnHits, d.ID)
}
}
@@ -501,6 +593,14 @@ func (hc *TopNCollector) visitFieldTerms(reader index.IndexReader, d *search.Doc
}
}
+ // first visit descendants if any
+ for _, descID := range d.Descendants {
+ err := hc.dvReader.VisitDocValues(descID, v)
+ if err != nil {
+ return err
+ }
+ }
+ // now visit the doc values for this document
err := hc.dvReader.VisitDocValues(d.IndexInternalID, v)
if hc.facetsBuilder != nil {
hc.facetsBuilder.EndDoc()
@@ -579,10 +679,10 @@ func (hc *TopNCollector) FacetResults() search.FacetResults {
return nil
}
-func (hc *TopNCollector) SetKNNHits(knnHits search.DocumentMatchCollection, newScoreExplComputer search.ScoreExplCorrectionCallbackFunc) {
+func (hc *TopNCollector) SetKNNHits(knnHits search.DocumentMatchCollection, hybridMergeCallback search.HybridMergeCallbackFn) {
hc.knnHits = make(map[string]*search.DocumentMatch, len(knnHits))
for _, hit := range knnHits {
hc.knnHits[hit.ID] = hit
}
- hc.computeNewScoreExpl = newScoreExplComputer
+ hc.hybridMergeCallback = hybridMergeCallback
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/explanation.go b/vendor/github.com/blevesearch/bleve/v2/search/explanation.go
index 924050016c..98c5e099db 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/explanation.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/explanation.go
@@ -29,6 +29,8 @@ func init() {
reflectStaticSizeExplanation = int(reflect.TypeOf(e).Size())
}
+const MergedExplMessage = "sum of merged explanations:"
+
type Explanation struct {
Value float64 `json:"value"`
Message string `json:"message"`
@@ -54,3 +56,50 @@ func (expl *Explanation) Size() int {
return sizeInBytes
}
+
+// MergeExpl merges two explanations into one.
+// If either explanation is nil, the other is returned.
+// If the first explanation is already a merged explanation,
+// the second explanation is appended to its children.
+// Otherwise, a new merged explanation is created
+// with the two explanations as its children.
+func (expl *Explanation) MergeWith(other *Explanation) *Explanation {
+ if expl == nil {
+ return other
+ }
+ if other == nil || expl == other {
+ return expl
+ }
+
+ newScore := expl.Value + other.Value
+
+ // if both are merged explanations, combine children
+ if expl.Message == MergedExplMessage && other.Message == MergedExplMessage {
+ expl.Value = newScore
+ expl.Children = append(expl.Children, other.Children...)
+ return expl
+ }
+
+ // atleast one is not a merged explanation see which one it is
+ // if expl is merged, append other
+ if expl.Message == MergedExplMessage {
+ // append other as a child to first
+ expl.Value = newScore
+ expl.Children = append(expl.Children, other)
+ return expl
+ }
+
+ // if other is merged, append expl
+ if other.Message == MergedExplMessage {
+ other.Value = newScore
+ other.Children = append(other.Children, expl)
+ return other
+ }
+ // create a new explanation to hold the merged one
+ rv := &Explanation{
+ Value: expl.Value + other.Value,
+ Message: MergedExplMessage,
+ Children: []*Explanation{expl, other},
+ }
+ return rv
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple/highlighter_simple.go b/vendor/github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple/highlighter_simple.go
index e898a1e61c..d0adfa81fb 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple/highlighter_simple.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/highlight/highlighter/simple/highlighter_simple.go
@@ -146,12 +146,8 @@ func (s *Highlighter) BestFragmentsInField(dm *search.DocumentMatch, doc index.D
formattedFragments[i] += s.sep
}
}
-
- if dm.Fragments == nil {
- dm.Fragments = make(search.FieldFragmentMap, 0)
- }
if len(formattedFragments) > 0 {
- dm.Fragments[field] = formattedFragments
+ dm.AddFragments(field, formattedFragments)
}
return formattedFragments
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/boolean.go b/vendor/github.com/blevesearch/bleve/v2/search/query/boolean.go
index 3bf6f91456..96df4a6d85 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/query/boolean.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/boolean.go
@@ -204,6 +204,8 @@ func (q *BooleanQuery) Searcher(ctx context.Context, i index.IndexReader, m mapp
// Compare document IDs
cmp := refDoc.IndexInternalID.Compare(d.IndexInternalID)
if cmp < 0 {
+ // recycle refDoc now that we do not need it
+ sctx.DocumentMatchPool.Put(refDoc)
// filterSearcher is behind the current document, Advance() it
refDoc, err = filterSearcher.Advance(sctx, d.IndexInternalID)
if err != nil || refDoc == nil {
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/conjunction.go b/vendor/github.com/blevesearch/bleve/v2/search/query/conjunction.go
index a2043720a9..631956dca8 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/query/conjunction.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/conjunction.go
@@ -54,14 +54,39 @@ func (q *ConjunctionQuery) AddQuery(aq ...Query) {
func (q *ConjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
ss := make([]search.Searcher, 0, len(q.Conjuncts))
+ cleanup := func() {
+ for _, searcher := range ss {
+ if searcher != nil {
+ _ = searcher.Close()
+ }
+ }
+ }
+ nestedMode, _ := ctx.Value(search.NestedSearchKey).(bool)
+ var nm mapping.NestedMapping
+ if nestedMode {
+ var ok bool
+ // get the nested mapping
+ if nm, ok = m.(mapping.NestedMapping); !ok {
+ // shouldn't be in nested mode if no nested mapping
+ nestedMode = false
+ }
+ }
+ // set of fields used in this query
+ var qfs search.FieldSet
+ var err error
+
for _, conjunct := range q.Conjuncts {
+ // Gather fields when nested mode is enabled
+ if nestedMode {
+ qfs, err = ExtractFields(conjunct, m, qfs)
+ if err != nil {
+ cleanup()
+ return nil, err
+ }
+ }
sr, err := conjunct.Searcher(ctx, i, m, options)
if err != nil {
- for _, searcher := range ss {
- if searcher != nil {
- _ = searcher.Close()
- }
- }
+ cleanup()
return nil, err
}
if _, ok := sr.(*searcher.MatchNoneSearcher); ok && q.queryStringMode {
@@ -75,6 +100,23 @@ func (q *ConjunctionQuery) Searcher(ctx context.Context, i index.IndexReader, m
return searcher.NewMatchNoneSearcher(i)
}
+ if nestedMode {
+ // first determine the nested depth info for the query fields
+ commonDepth, maxDepth := nm.NestedDepth(qfs)
+ // if we have common depth == max depth then we can just use
+ // the normal conjunction searcher, as all fields share the same
+ // nested context, otherwise we need to use the nested conjunction searcher
+ // also, if we are querying the _all or _id fields, we need to use
+ // the nested conjunction searcher as well, with common depth 0
+ // indicating matches happen only at the root level
+ if qfs.HasAll() || qfs.HasID() {
+ commonDepth = 0
+ }
+ if commonDepth < maxDepth {
+ return searcher.NewNestedConjunctionSearcher(ctx, i, ss, commonDepth, options)
+ }
+ }
+
return searcher.NewConjunctionSearcher(ctx, i, ss, options)
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/custom_filter.go b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_filter.go
new file mode 100644
index 0000000000..07031312fc
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_filter.go
@@ -0,0 +1,130 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package query
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/blevesearch/bleve/v2/mapping"
+ "github.com/blevesearch/bleve/v2/search"
+ "github.com/blevesearch/bleve/v2/search/searcher"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// CustomFilterQuery wraps a child query and filters its candidate matches via
+// an embedder-provided per-hit callback.
+type CustomFilterQuery struct {
+ Query Query `json:"query"`
+ Fields []string `json:"fields,omitempty"`
+
+ filterFunc searcher.CustomFilterFunc
+ payload map[string]interface{}
+}
+
+// CustomFilterQueryParser lets an embedder override parsing of
+// {"custom_filter": ...} nodes. It is intended to be assigned once during
+// process startup or init, before any queries are parsed; callers must not
+// mutate it concurrently with ParseQuery(). For example:
+//
+// func init() {
+// query.CustomFilterQueryParser = parseCustomFilterQuery
+// }
+var CustomFilterQueryParser func([]byte) (Query, error)
+
+func NewCustomFilterQueryWithFilter(query Query, filter searcher.CustomFilterFunc, fields []string, payload map[string]interface{}) *CustomFilterQuery {
+ return &CustomFilterQuery{
+ Query: query,
+ Fields: fields,
+ filterFunc: filter,
+ payload: payload,
+ }
+}
+
+func (q *CustomFilterQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+ if q == nil {
+ return nil, fmt.Errorf("custom filter query is nil")
+ }
+ if q.Query == nil {
+ return nil, fmt.Errorf("custom filter query must have a query")
+ }
+ if q.filterFunc == nil {
+ return nil, fmt.Errorf("custom filter query must have a filter callback")
+ }
+
+ // Build the inner searcher first; custom filtering wraps its output.
+ childSearcher, err := q.Query.Searcher(ctx, i, m, options)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create a doc value reader for the requested fields (if any) so the
+ // searcher can populate d.Fields before invoking the callback.
+ var dvReader index.DocValueReader
+ var fieldTypes map[string]string
+ if len(q.Fields) > 0 {
+ var err2 error
+ dvReader, err2 = i.DocValueReader(q.Fields)
+ if err2 != nil {
+ _ = childSearcher.Close()
+ return nil, err2
+ }
+ fieldTypes = resolveFieldTypes(q.Fields, m)
+ }
+
+ return searcher.NewCustomFilterSearcher(ctx, childSearcher, q.filterFunc, dvReader, i, fieldTypes), nil
+}
+
+func (q *CustomFilterQuery) Validate() error {
+ if q == nil {
+ return fmt.Errorf("custom filter query is nil")
+ }
+ if q.Query == nil {
+ return fmt.Errorf("custom filter query must have a query")
+ }
+ if q.filterFunc == nil {
+ return fmt.Errorf("custom filter query must have a filter callback")
+ }
+ if vq, ok := q.Query.(ValidatableQuery); ok {
+ return vq.Validate()
+ }
+ return nil
+}
+
+func (q *CustomFilterQuery) MarshalJSON() ([]byte, error) {
+ inner := make(map[string]interface{}, len(q.payload)+2)
+ for k, v := range q.payload {
+ inner[k] = v
+ }
+ inner["query"] = q.Query
+ if len(q.Fields) > 0 {
+ inner["fields"] = q.Fields
+ }
+ return json.Marshal(map[string]interface{}{
+ "custom_filter": inner,
+ })
+}
+
+func (q *CustomFilterQuery) UnmarshalJSON(data []byte) error {
+ child, fields, payload, err := unmarshalCustomQueryPayload(data, "custom_filter")
+ if err != nil {
+ return err
+ }
+ q.Query = child
+ q.Fields = fields
+ q.payload = payload
+ return nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/custom_payload.go b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_payload.go
new file mode 100644
index 0000000000..e220061b57
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_payload.go
@@ -0,0 +1,96 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package query
+
+import (
+ "encoding/json"
+ "fmt"
+
+ "github.com/blevesearch/bleve/v2/mapping"
+ "github.com/blevesearch/bleve/v2/util"
+)
+
+func unmarshalCustomQueryPayload(data []byte, key string) (Query, []string, map[string]interface{}, error) {
+ tmp := map[string]json.RawMessage{}
+ err := util.UnmarshalJSON(data, &tmp)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ innerRaw, ok := tmp[key]
+ if !ok || innerRaw == nil {
+ return nil, nil, nil, nil
+ }
+
+ var inner map[string]json.RawMessage
+ err = util.UnmarshalJSON(innerRaw, &inner)
+ if err != nil || inner == nil {
+ return nil, nil, nil, fmt.Errorf("%s query must be a JSON object", key)
+ }
+
+ var child Query
+ if childQuery, ok := inner["query"]; ok && childQuery != nil {
+ child, err = ParseQuery(childQuery)
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ }
+
+ var fields []string
+ if rawFields, ok := inner["fields"]; ok && rawFields != nil {
+ if err := util.UnmarshalJSON(rawFields, &fields); err != nil {
+ return nil, nil, nil, fmt.Errorf("%s query has invalid %q: %w",
+ key, "fields", err)
+ }
+ }
+
+ payload := make(map[string]interface{}, len(inner))
+ for k, raw := range inner {
+ if k == "query" || k == "fields" {
+ continue
+ }
+ var v interface{}
+ if raw != nil {
+ err = util.UnmarshalJSON(raw, &v)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("%s query has invalid %q payload: %w",
+ key, k, err)
+ }
+ }
+ payload[k] = v
+ }
+
+ return child, fields, payload, nil
+}
+
+// resolveFieldTypes looks up each field name in the index mapping and returns
+// a map of field name → mapping type (e.g. "datetime", "number", "text").
+// This is used by the searcher layer to correctly decode doc value bytes.
+func resolveFieldTypes(fields []string, m mapping.IndexMapping) map[string]string {
+ if m == nil || len(fields) == 0 {
+ return nil
+ }
+ types := make(map[string]string, len(fields))
+ for _, f := range fields {
+ fm := m.FieldMappingForPath(f)
+ if fm.Type != "" {
+ types[f] = fm.Type
+ }
+ }
+ if len(types) == 0 {
+ return nil
+ }
+ return types
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/custom_score.go b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_score.go
new file mode 100644
index 0000000000..e7f9d01edc
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/custom_score.go
@@ -0,0 +1,130 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package query
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+
+ "github.com/blevesearch/bleve/v2/mapping"
+ "github.com/blevesearch/bleve/v2/search"
+ "github.com/blevesearch/bleve/v2/search/searcher"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// CustomScoreQuery wraps a child query and re-scores its candidate matches via
+// an embedder-provided per-hit callback.
+type CustomScoreQuery struct {
+ Query Query `json:"query"`
+ Fields []string `json:"fields,omitempty"`
+
+ scoreFunc searcher.CustomScoreFunc
+ payload map[string]interface{}
+}
+
+// CustomScoreQueryParser lets an embedder override parsing of
+// {"custom_score": ...} nodes. It is intended to be assigned once during
+// process startup or init, before any queries are parsed; callers must not
+// mutate it concurrently with ParseQuery(). For example:
+//
+// func init() {
+// query.CustomScoreQueryParser = parseCustomScoreQuery
+// }
+var CustomScoreQueryParser func([]byte) (Query, error)
+
+func NewCustomScoreQueryWithScorer(query Query, score searcher.CustomScoreFunc, fields []string, payload map[string]interface{}) *CustomScoreQuery {
+ return &CustomScoreQuery{
+ Query: query,
+ Fields: fields,
+ scoreFunc: score,
+ payload: payload,
+ }
+}
+
+func (q *CustomScoreQuery) Searcher(ctx context.Context, i index.IndexReader, m mapping.IndexMapping, options search.SearcherOptions) (search.Searcher, error) {
+ if q == nil {
+ return nil, fmt.Errorf("custom score query is nil")
+ }
+ if q.Query == nil {
+ return nil, fmt.Errorf("custom score query must have a query")
+ }
+ if q.scoreFunc == nil {
+ return nil, fmt.Errorf("custom score query must have a score callback")
+ }
+
+ // Build the inner searcher first; custom scoring wraps its output.
+ childSearcher, err := q.Query.Searcher(ctx, i, m, options)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create a doc value reader for the requested fields (if any) so the
+ // searcher can populate d.Fields before invoking the callback.
+ var dvReader index.DocValueReader
+ var fieldTypes map[string]string
+ if len(q.Fields) > 0 {
+ var err2 error
+ dvReader, err2 = i.DocValueReader(q.Fields)
+ if err2 != nil {
+ _ = childSearcher.Close()
+ return nil, err2
+ }
+ fieldTypes = resolveFieldTypes(q.Fields, m)
+ }
+
+ return searcher.NewCustomScoreSearcher(ctx, childSearcher, q.scoreFunc, dvReader, i, fieldTypes), nil
+}
+
+func (q *CustomScoreQuery) Validate() error {
+ if q == nil {
+ return fmt.Errorf("custom score query is nil")
+ }
+ if q.Query == nil {
+ return fmt.Errorf("custom score query must have a query")
+ }
+ if q.scoreFunc == nil {
+ return fmt.Errorf("custom score query must have a score callback")
+ }
+ if vq, ok := q.Query.(ValidatableQuery); ok {
+ return vq.Validate()
+ }
+ return nil
+}
+
+func (q *CustomScoreQuery) MarshalJSON() ([]byte, error) {
+ inner := make(map[string]interface{}, len(q.payload)+2)
+ for k, v := range q.payload {
+ inner[k] = v
+ }
+ inner["query"] = q.Query
+ if len(q.Fields) > 0 {
+ inner["fields"] = q.Fields
+ }
+ return json.Marshal(map[string]interface{}{
+ "custom_score": inner,
+ })
+}
+
+func (q *CustomScoreQuery) UnmarshalJSON(data []byte) error {
+ child, fields, payload, err := unmarshalCustomQueryPayload(data, "custom_score")
+ if err != nil {
+ return err
+ }
+ q.Query = child
+ q.Fields = fields
+ q.payload = payload
+ return nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/knn.go b/vendor/github.com/blevesearch/bleve/v2/search/query/knn.go
index ea8780a417..5282a740f3 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/query/knn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/knn.go
@@ -84,6 +84,12 @@ func (q *KNNQuery) Searcher(ctx context.Context, i index.IndexReader,
if q.K <= 0 || len(q.Vector) == 0 {
return nil, fmt.Errorf("k must be greater than 0 and vector must be non-empty")
}
+ // bivf-sq8 indexes only supports hamming distance for the primary
+ // binary index. Similarity here is used for the backing flat index,
+ // which is set to cosine similarity for recall reasons
+ if index.OptimizationRequiresBinaryIndex(fieldMapping.VectorIndexOptimizedFor) {
+ similarityMetric = index.CosineSimilarity
+ }
if similarityMetric == index.CosineSimilarity {
// normalize the vector
q.Vector = mapping.NormalizeVector(q.Vector)
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/query.go b/vendor/github.com/blevesearch/bleve/v2/search/query/query.go
index 27c3978b17..cf64189b29 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/query/query.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/query.go
@@ -308,6 +308,20 @@ func ParseQuery(input []byte) (Query, error) {
}
return &rv, nil
}
+ _, hasCustomFilter := tmp["custom_filter"]
+ if hasCustomFilter {
+ if CustomFilterQueryParser == nil {
+ return nil, fmt.Errorf("custom filter query parser is not registered")
+ }
+ return CustomFilterQueryParser(input)
+ }
+ _, hasCustomScore := tmp["custom_score"]
+ if hasCustomScore {
+ if CustomScoreQueryParser == nil {
+ return nil, fmt.Errorf("custom score query parser is not registered")
+ }
+ return CustomScoreQueryParser(input)
+ }
_, hasDocIds := tmp["ids"]
if hasDocIds {
var rv DocIDQuery
@@ -455,13 +469,10 @@ func DumpQuery(m mapping.IndexMapping, query Query) (string, error) {
return string(data), err
}
-// FieldSet represents a set of queried fields.
-type FieldSet map[string]struct{}
-
// ExtractFields returns a set of fields referenced by the query.
// The returned set may be nil if the query does not explicitly reference any field
// and the DefaultSearchField is unset in the index mapping.
-func ExtractFields(q Query, m mapping.IndexMapping, fs FieldSet) (FieldSet, error) {
+func ExtractFields(q Query, m mapping.IndexMapping, fs search.FieldSet) (search.FieldSet, error) {
if q == nil || m == nil {
return fs, nil
}
@@ -474,9 +485,9 @@ func ExtractFields(q Query, m mapping.IndexMapping, fs FieldSet) (FieldSet, erro
}
if f != "" {
if fs == nil {
- fs = make(FieldSet)
+ fs = search.NewFieldSet()
}
- fs[f] = struct{}{}
+ fs.AddField(f)
}
case *QueryStringQuery:
var expandedQuery Query
@@ -505,6 +516,11 @@ func ExtractFields(q Query, m mapping.IndexMapping, fs FieldSet) (FieldSet, erro
break
}
}
+ case *DocIDQuery, *MatchAllQuery:
+ if fs == nil {
+ fs = search.NewFieldSet()
+ }
+ fs.AddField("_id")
}
return fs, err
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/query/query_string_lex.go b/vendor/github.com/blevesearch/bleve/v2/search/query/query_string_lex.go
index c01fa6fc29..b7a1283393 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/query/query_string_lex.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/query/query_string_lex.go
@@ -18,9 +18,40 @@ import (
"bufio"
"io"
"strings"
+ "sync"
"unicode"
)
+var queryStringLexPool = sync.Pool{
+ New: func() interface{} {
+ return &queryStringLex{
+ in: bufio.NewReader(strings.NewReader("")),
+ }
+ },
+}
+
+func getQueryStringLex(in io.Reader) *queryStringLex {
+ l := queryStringLexPool.Get().(*queryStringLex)
+ l.in.Reset(in)
+ l.currState = startState
+ l.currConsumed = true
+ l.buf = ""
+ l.inEscape = false
+ l.nextToken = nil
+ l.nextTokenType = 0
+ l.seenDot = false
+ l.nextRune = 0
+ l.nextRuneSize = 0
+ l.atEOF = false
+ return l
+}
+
+func putQueryStringLex(l *queryStringLex) {
+ l.in.Reset(strings.NewReader(""))
+ l.nextToken = nil
+ queryStringLexPool.Put(l)
+}
+
const reservedChars = "+-=&|> 0 {
- return nil, fmt.Errorf(strings.Join(lex.errs, "\n"))
+ return nil, fmt.Errorf("%s", strings.Join(lex.errs, "\n"))
}
return lex.query, nil
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_knn.go b/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_knn.go
index 8d90434271..06f50cd4a7 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_knn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_knn.go
@@ -123,7 +123,7 @@ func (sqs *KNNQueryScorer) Score(ctx *search.SearchContext,
if sqs.options.Explain {
rv.Expl = scoreExplanation
}
- rv.IndexInternalID = append(rv.IndexInternalID, knnMatch.ID...)
+ rv.IndexInternalID = index.NewIndexInternalIDFrom(rv.IndexInternalID, knnMatch.ID)
return rv
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_term.go b/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_term.go
index f5f8ec9356..d7e77f9779 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_term.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/scorer/scorer_term.go
@@ -243,7 +243,7 @@ func (s *TermQueryScorer) Score(ctx *search.SearchContext, termMatch *index.Term
}
}
- rv.IndexInternalID = append(rv.IndexInternalID, termMatch.ID...)
+ rv.IndexInternalID = index.NewIndexInternalIDFrom(rv.IndexInternalID, termMatch.ID)
if len(termMatch.Vectors) > 0 {
if cap(rv.FieldTermLocations) < len(termMatch.Vectors) {
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/search.go b/vendor/github.com/blevesearch/bleve/v2/search/search.go
index 7240257877..541bbe42a1 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/search.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/search.go
@@ -165,9 +165,9 @@ type DocumentMatch struct {
// used to indicate the sub-scores that combined to form the
// final score for this document match. This is only populated
- // when the search request's query is a DisjunctionQuery
- // or a ConjunctionQuery. The map key is the index of the sub-query
- // in the DisjunctionQuery or ConjunctionQuery. The map value is the
+ // when the search request's query is a DisjunctionQuery.
+ // The map key is the index of the sub-query
+ // in the DisjunctionQuery. The map value is the
// sub-score for that sub-query.
ScoreBreakdown map[int]float64 `json:"score_breakdown,omitempty"`
@@ -178,6 +178,10 @@ type DocumentMatch struct {
// of the index that this match came from
// of the current alias view, used in alias of aliases scenario
IndexNames []string `json:"index_names,omitempty"`
+
+ // Descendants holds the IDs of any child/descendant document that contributed
+ // to this root DocumentMatch.
+ Descendants []index.IndexInternalID `json:"-"`
}
func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) {
@@ -201,6 +205,21 @@ func (dm *DocumentMatch) AddFieldValue(name string, value interface{}) {
dm.Fields[name] = valSlice
}
+func (dm *DocumentMatch) AddFragments(field string, fragments []string) {
+ if dm.Fragments == nil {
+ dm.Fragments = make(FieldFragmentMap)
+ }
+OUTER:
+ for _, newFrag := range fragments {
+ for _, existingFrag := range dm.Fragments[field] {
+ if existingFrag == newFrag {
+ continue OUTER // no duplicates allowed
+ }
+ }
+ dm.Fragments[field] = append(dm.Fragments[field], newFrag)
+ }
+}
+
// Reset allows an already allocated DocumentMatch to be reused
func (dm *DocumentMatch) Reset() *DocumentMatch {
// remember the []byte used for the IndexInternalID
@@ -218,6 +237,11 @@ func (dm *DocumentMatch) Reset() *DocumentMatch {
scoreBreakdown := dm.ScoreBreakdown
// clear out the score breakdown map
clear(scoreBreakdown)
+ // remember the Descendants backing array
+ descendants := dm.Descendants
+ for i := range descendants { // recycle each IndexInternalID
+ descendants[i] = descendants[i][:0]
+ }
// idiom to copy over from empty DocumentMatch (0 allocations)
*dm = DocumentMatch{}
// reuse the []byte already allocated (and reset len to 0)
@@ -228,6 +252,8 @@ func (dm *DocumentMatch) Reset() *DocumentMatch {
dm.DecodedSort = decodedSort[:0]
// reuse the FieldTermLocations already allocated (and reset len to 0)
dm.FieldTermLocations = ftls[:0]
+ // reuse the Descendants already allocated (and reset len to 0)
+ dm.Descendants = descendants[:0]
// reuse the score breakdown map already allocated (after clearing it)
dm.ScoreBreakdown = scoreBreakdown
return dm
@@ -402,3 +428,20 @@ func (sc *SearchContext) Size() int {
return sizeInBytes
}
+
+// A NestedDocumentMatch is like a DocumentMatch but used for nested documents
+// and does not have score or locations, or a score and is mainly used to
+// hold field values and fragments, to be embedded in the parent DocumentMatch
+type NestedDocumentMatch struct {
+ Fields map[string]interface{} `json:"fields,omitempty"`
+ Fragments FieldFragmentMap `json:"fragments,omitempty"`
+}
+
+// NewNestedDocumentMatch creates a new NestedDocumentMatch instance
+// with the given fields and fragments
+func NewNestedDocumentMatch(fields map[string]interface{}, fragments FieldFragmentMap) *NestedDocumentMatch {
+ return &NestedDocumentMatch{
+ Fields: fields,
+ Fragments: fragments,
+ }
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_conjunction_nested.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_conjunction_nested.go
new file mode 100644
index 0000000000..9ab18dabe9
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_conjunction_nested.go
@@ -0,0 +1,480 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package searcher
+
+import (
+ "context"
+ "fmt"
+ "math"
+ "reflect"
+ "slices"
+
+ "github.com/blevesearch/bleve/v2/search"
+ "github.com/blevesearch/bleve/v2/size"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+var reflectStaticSizeNestedConjunctionSearcher int
+
+func init() {
+ var ncs NestedConjunctionSearcher
+ reflectStaticSizeNestedConjunctionSearcher = int(reflect.TypeOf(ncs).Size())
+}
+
+type NestedConjunctionSearcher struct {
+ nestedReader index.NestedReader
+ searchers []search.Searcher
+ queryNorm float64
+ currs []*search.DocumentMatch
+ currAncestors [][]index.AncestorID
+ currKeys []index.AncestorID
+ initialized bool
+ joinIdx int
+ options search.SearcherOptions
+ docQueue *CoalesceQueue
+ // reusable ID buffer for Advance() calls
+ advanceID index.IndexInternalID
+ // reusable buffer for Advance() calls
+ ancestors []index.AncestorID
+}
+
+func NewNestedConjunctionSearcher(ctx context.Context, indexReader index.IndexReader,
+ searchers []search.Searcher, joinIdx int, options search.SearcherOptions) (search.Searcher, error) {
+
+ var nr index.NestedReader
+ var ok bool
+ if nr, ok = indexReader.(index.NestedReader); !ok {
+ return nil, fmt.Errorf("indexReader does not support nested documents")
+ }
+
+ // build our searcher
+ rv := NestedConjunctionSearcher{
+ nestedReader: nr,
+ options: options,
+ searchers: searchers,
+ currs: make([]*search.DocumentMatch, len(searchers)),
+ currAncestors: make([][]index.AncestorID, len(searchers)),
+ currKeys: make([]index.AncestorID, len(searchers)),
+ joinIdx: joinIdx,
+ docQueue: NewCoalesceQueue(),
+ }
+ rv.computeQueryNorm()
+
+ return &rv, nil
+}
+
+func (s *NestedConjunctionSearcher) computeQueryNorm() {
+ // first calculate sum of squared weights
+ sumOfSquaredWeights := 0.0
+ for _, searcher := range s.searchers {
+ sumOfSquaredWeights += searcher.Weight()
+ }
+ // now compute query norm from this
+ s.queryNorm = 1.0 / math.Sqrt(sumOfSquaredWeights)
+ // finally tell all the downstream searchers the norm
+ for _, searcher := range s.searchers {
+ searcher.SetQueryNorm(s.queryNorm)
+ }
+}
+
+func (s *NestedConjunctionSearcher) Size() int {
+ sizeInBytes := reflectStaticSizeNestedConjunctionSearcher + size.SizeOfPtr
+
+ for _, entry := range s.searchers {
+ sizeInBytes += entry.Size()
+ }
+
+ for _, entry := range s.currs {
+ if entry != nil {
+ sizeInBytes += entry.Size()
+ }
+ }
+
+ return sizeInBytes
+}
+
+func (s *NestedConjunctionSearcher) Weight() float64 {
+ var rv float64
+ for _, searcher := range s.searchers {
+ rv += searcher.Weight()
+ }
+ return rv
+}
+
+func (s *NestedConjunctionSearcher) SetQueryNorm(qnorm float64) {
+ for _, searcher := range s.searchers {
+ searcher.SetQueryNorm(qnorm)
+ }
+}
+
+func (s *NestedConjunctionSearcher) Count() uint64 {
+ // for now return a worst case
+ var sum uint64
+ for _, searcher := range s.searchers {
+ sum += searcher.Count()
+ }
+ return sum
+}
+
+func (s *NestedConjunctionSearcher) Close() (rv error) {
+ for _, searcher := range s.searchers {
+ err := searcher.Close()
+ if err != nil && rv == nil {
+ rv = err
+ }
+ }
+ return rv
+}
+
+func (s *NestedConjunctionSearcher) Min() int {
+ return 0
+}
+
+func (s *NestedConjunctionSearcher) DocumentMatchPoolSize() int {
+ rv := len(s.currs)
+ for _, s := range s.searchers {
+ rv += s.DocumentMatchPoolSize()
+ }
+ return rv
+}
+
+func (s *NestedConjunctionSearcher) initialize(ctx *search.SearchContext) (bool, error) {
+ var err error
+ for i, searcher := range s.searchers {
+ if s.currs[i] != nil {
+ ctx.DocumentMatchPool.Put(s.currs[i])
+ }
+ s.currs[i], err = searcher.Next(ctx)
+ if err != nil {
+ return false, err
+ }
+ if s.currs[i] == nil {
+ // one of the searchers is exhausted, so we are done
+ return true, nil
+ }
+ // get the ancestry chain for this match
+ s.currAncestors[i], err = s.nestedReader.Ancestors(s.currs[i].IndexInternalID, s.currAncestors[i][:0])
+ if err != nil {
+ return false, err
+ }
+ // check if the ancestry chain is > joinIdx, if not we reset the joinIdx
+ // to the minimum possible value across all searchers, ideally this will be
+ // done in query construction time itself, by using the covering depth across
+ // all sub-queries, but we do this here as a fallback
+ if s.joinIdx >= len(s.currAncestors[i]) {
+ s.joinIdx = len(s.currAncestors[i]) - 1
+ }
+ }
+ // build currKeys for each searcher, do it here as we may have adjusted joinIdx
+ for i := range s.searchers {
+ s.currKeys[i] = ancestorFromRoot(s.currAncestors[i], s.joinIdx)
+ }
+ s.initialized = true
+ return false, nil
+}
+
+func (s *NestedConjunctionSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) {
+ // initialize on first call to Next, by getting first match
+ // from each searcher and their ancestry chains
+ if !s.initialized {
+ done, err := s.initialize(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if done {
+ return nil, nil
+ }
+ }
+ // check if the docQueue has any buffered matches
+ if s.docQueue.Len() > 0 {
+ return s.docQueue.Dequeue(ctx), nil
+ }
+ // now enter the main alignment loop
+ n := len(s.searchers)
+OUTER:
+ for {
+ // pick the pivot searcher with the highest key (ancestor at joinIdx level)
+ if s.currs[0] == nil {
+ return nil, nil
+ }
+ maxKey := s.currKeys[0]
+ for i := 1; i < n; i++ {
+ // currs[i] is nil means one of the searchers is exhausted
+ if s.currs[i] == nil {
+ return nil, nil
+ }
+ currKey := s.currKeys[i]
+ if maxKey.Compare(currKey) < 0 {
+ maxKey = currKey
+ }
+ }
+ // store maxkey as advanceID only once only if needed
+ var advanceID index.IndexInternalID
+ // flag to track if all searchers are aligned
+ var aligned bool = true
+ // now try to align all other searchers to the
+ // we check if the a searchers key matches maxKey
+ // if not, we advance the pivot searcher to maxKey
+ // else do nothing and move to the next searcher
+ for i := 0; i < n; i++ {
+ cmp := s.currKeys[i].Compare(maxKey)
+ if cmp < 0 {
+ // not aligned, so advance this searcher to maxKey
+ // convert maxKey to advanceID only once
+ if advanceID == nil {
+ advanceID = s.toAdvanceID(maxKey)
+ }
+ var err error
+ ctx.DocumentMatchPool.Put(s.currs[i])
+ s.currs[i], err = s.searchers[i].Advance(ctx, advanceID)
+ if err != nil {
+ return nil, err
+ }
+ if s.currs[i] == nil {
+ // one of the searchers is exhausted, so we are done
+ return nil, nil
+ }
+ // recalc ancestors
+ s.currAncestors[i], err = s.nestedReader.Ancestors(s.currs[i].IndexInternalID, s.currAncestors[i][:0])
+ if err != nil {
+ return nil, err
+ }
+ // recalc key
+ s.currKeys[i] = ancestorFromRoot(s.currAncestors[i], s.joinIdx)
+ // recalc cmp
+ cmp = s.currKeys[i].Compare(maxKey)
+ }
+ if cmp != 0 {
+ // not aligned
+ aligned = false
+ }
+ }
+ // now check if all the searchers are aligned at the same maxKey
+ // if they are not aligned, we need to restart the loop of picking
+ // the pivot searcher with the highest key
+ if !aligned {
+ continue OUTER
+ }
+ // if we are here, all the searchers are aligned at maxKey
+ // now we need to buffer all the intermediate matches for every
+ // searcher at this key, until either the searcher's key changes
+ // or the searcher is exhausted
+ var err error
+ for i := 0; i < n; i++ {
+ for {
+ // buffer the current match
+ s.docQueue.Enqueue(s.currs[i])
+ // advance to next match
+ s.currs[i], err = s.searchers[i].Next(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if s.currs[i] == nil {
+ // searcher exhausted, break out
+ break
+ }
+ // recalc ancestors
+ s.currAncestors[i], err = s.nestedReader.Ancestors(s.currs[i].IndexInternalID, s.currAncestors[i][:0])
+ if err != nil {
+ return nil, err
+ }
+ // recalc key
+ s.currKeys[i] = ancestorFromRoot(s.currAncestors[i], s.joinIdx)
+ // check if key has changed
+ if !s.currKeys[i].Equals(maxKey) {
+ // key changed, break out
+ break
+ }
+ }
+ }
+ // finalize the docQueue for dequeueing
+ s.docQueue.Finalize()
+ // finally return the first buffered match
+ return s.docQueue.Dequeue(ctx), nil
+ }
+}
+
+// ancestorFromRoot gets the AncestorID at the given position from the root
+// if pos is 0, it returns the root AncestorID, and so on
+func ancestorFromRoot(ancestors []index.AncestorID, pos int) index.AncestorID {
+ return ancestors[len(ancestors)-pos-1]
+}
+
+// toAdvanceID converts an AncestorID to IndexInternalID, reusing the advanceID buffer.
+// The returned ID is safe to pass to Advance() since Advance() never retains references.
+func (s *NestedConjunctionSearcher) toAdvanceID(key index.AncestorID) index.IndexInternalID {
+ // Reset length to 0 while preserving capacity for buffer reuse
+ s.advanceID = s.advanceID[:0]
+ // Convert key to IndexInternalID, reusing the underlying buffer
+ s.advanceID = key.ToIndexInternalID(s.advanceID)
+ return s.advanceID
+}
+
+func (s *NestedConjunctionSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) {
+ if !s.initialized {
+ done, err := s.initialize(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if done {
+ return nil, nil
+ }
+ }
+ // first check if the docQueue has any buffered matches
+ // if so we first check if any of them can satisfy the Advance(ID)
+ for s.docQueue.Len() > 0 {
+ dm := s.docQueue.Dequeue(ctx)
+ if dm.IndexInternalID.Compare(ID) >= 0 {
+ return dm, nil
+ }
+ // otherwise recycle this match
+ ctx.DocumentMatchPool.Put(dm)
+ }
+ var err error
+ // now we first get the ancestry chain for the given ID
+ s.ancestors, err = s.nestedReader.Ancestors(ID, s.ancestors[:0])
+ if err != nil {
+ return nil, err
+ }
+ // we now follow the the following logic for each searcher:
+ // let S be the length of the ancestry chain for the searcher
+ // let I be the length of the ancestry chain for the given ID
+ // 1. if S > I:
+ // then we just Advance() the searcher to the given ID if required
+ // 2. else if S <= I:
+ // then we get the AncestorID at position (S - 1) from the root of
+ // the given ID's ancestry chain, and Advance() the searcher to
+ // it if required
+ for i, searcher := range s.searchers {
+ if s.currs[i] == nil {
+ return nil, nil // already exhausted, nothing to do
+ }
+ var targetID index.IndexInternalID
+ S := len(s.currAncestors[i])
+ I := len(s.ancestors)
+ if S > I {
+ // case 1: S > I
+ targetID = ID
+ } else {
+ // case 2: S <= I
+ targetID = s.toAdvanceID(ancestorFromRoot(s.ancestors, S-1))
+ }
+ if s.currs[i].IndexInternalID.Compare(targetID) < 0 {
+ // need to advance this searcher
+ ctx.DocumentMatchPool.Put(s.currs[i])
+ s.currs[i], err = searcher.Advance(ctx, targetID)
+ if err != nil {
+ return nil, err
+ }
+ if s.currs[i] == nil {
+ // one of the searchers is exhausted, so we are done
+ return nil, nil
+ }
+ // recalc ancestors
+ s.currAncestors[i], err = s.nestedReader.Ancestors(s.currs[i].IndexInternalID, s.currAncestors[i][:0])
+ if err != nil {
+ return nil, err
+ }
+ // recalc key
+ s.currKeys[i] = ancestorFromRoot(s.currAncestors[i], s.joinIdx)
+ }
+ }
+ // we need to call Next() in a loop until we reach or exceed the given ID
+ // the Next() call basically gives us a match that is aligned correctly, but
+ // if joinIdx < I, we can have multiple matches for the same joinIdx ancestor
+ // and they may be < ID, so we need to loop
+ for {
+ next, err := s.Next(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if next == nil {
+ return nil, nil
+ }
+ if next.IndexInternalID.Compare(ID) >= 0 {
+ return next, nil
+ }
+ ctx.DocumentMatchPool.Put(next)
+ }
+}
+
+// ------------------------------------------------------------------------------------------
+type CoalesceQueue struct {
+ order []*search.DocumentMatch // queue of DocumentMatch
+}
+
+func NewCoalesceQueue() *CoalesceQueue {
+ cq := &CoalesceQueue{
+ order: make([]*search.DocumentMatch, 0),
+ }
+ return cq
+}
+
+// Enqueue appends the given DocumentMatch to the queue. Coalescing of duplicates
+// is deferred until Dequeue, after Finalize has sorted items by IndexInternalID.
+func (cq *CoalesceQueue) Enqueue(it *search.DocumentMatch) {
+ // append to order slice (this is a stack)
+ cq.order = append(cq.order, it)
+}
+
+// Finalize prepares the queue for dequeue operations by sorting the items based on
+// their IndexInternalID values. This MUST be called before any Dequeue operations,
+// and after all Enqueue operations are complete. The sort is done in descending order
+// so that dequeueing will basically be popping from the end of the slice, allowing for
+// slice reuse.
+func (cq *CoalesceQueue) Finalize() {
+ slices.SortFunc(cq.order, func(a, b *search.DocumentMatch) int {
+ return b.IndexInternalID.Compare(a.IndexInternalID)
+ })
+}
+
+// Dequeue removes and returns the next DocumentMatch in sorted order, merging any
+// consecutive duplicates. Merged items are recycled via ctx.DocumentMatchPool.
+// Returns nil when the queue is empty.
+func (cq *CoalesceQueue) Dequeue(ctx *search.SearchContext) *search.DocumentMatch {
+ if cq.Len() == 0 {
+ return nil
+ }
+
+ // pop from end of slice
+ rv := cq.order[len(cq.order)-1]
+ cq.order = cq.order[:len(cq.order)-1]
+
+ // merge duplicates
+ for cq.Len() > 0 {
+ // peek at next item
+ next := cq.order[len(cq.order)-1]
+ if !rv.IndexInternalID.Equals(next.IndexInternalID) {
+ // different ID, stop merging
+ break
+ }
+ // pop the next item
+ cq.order = cq.order[:len(cq.order)-1]
+ // same ID, merge
+ rv.Score += next.Score
+ rv.Expl = rv.Expl.MergeWith(next.Expl)
+ rv.FieldTermLocations = search.MergeFieldTermLocationsFromMatch(
+ rv.FieldTermLocations, next)
+ // recycle the merged item
+ ctx.DocumentMatchPool.Put(next)
+ }
+
+ return rv
+}
+
+// Len returns the number of DocumentMatch items currently in the queue.
+func (cq *CoalesceQueue) Len() int {
+ return len(cq.order)
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_fields.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_fields.go
new file mode 100644
index 0000000000..5b4ff93dd6
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_fields.go
@@ -0,0 +1,111 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package searcher
+
+import (
+ "time"
+
+ "github.com/blevesearch/bleve/v2/numeric"
+ "github.com/blevesearch/bleve/v2/search"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// loadDocValuesOnHit uses the supplied DocValueReader to visit doc values
+// for the given hit and populate hit.Fields. It also resolves hit.ID if empty.
+// It is a no-op when dvReader is nil.
+//
+// fieldTypes maps field name → mapping type (e.g. "datetime", "number").
+// When provided, datetime fields are decoded from their stored nanosecond
+// int64 into an RFC3339Nano string, while numeric fields use IEEE 754 bit
+// reinterpretation to recover the original float64. When nil, all prefix-coded
+// values use the numeric (float64) path.
+func loadDocValuesOnHit(hit *search.DocumentMatch, dvReader index.DocValueReader,
+ r index.IndexReader) error {
+ return loadDocValuesOnHitWithTypes(hit, dvReader, r, nil)
+}
+
+func loadDocValuesOnHitWithTypes(hit *search.DocumentMatch, dvReader index.DocValueReader,
+ r index.IndexReader, fieldTypes map[string]string) error {
+ // Always resolve external ID so the callback can read hit.ID.
+ if hit.ID == "" && r != nil {
+ extID, err := r.ExternalID(hit.IndexInternalID)
+ if err != nil {
+ return err
+ }
+ hit.ID = extID
+ }
+
+ if dvReader == nil {
+ return nil
+ }
+
+ err := dvReader.VisitDocValues(hit.IndexInternalID, func(field string, term []byte) {
+ value := decodeDocValueTerm(term, fieldTypes[field])
+ if value != nil {
+ hit.AddFieldValue(field, value)
+ }
+ })
+
+ return err
+}
+
+// decodeDocValueTerm converts raw doc value bytes into a typed Go value.
+// Numeric fields are prefix-coded int64s (only shift-0 terms carry values).
+// Boolean fields are stored as "T" or "F".
+// Everything else (text/keyword) is returned as a string.
+//
+// fieldType is the mapping type string for the field (e.g. "datetime",
+// "number"). When fieldType is "datetime", the prefix-coded int64 is
+// treated as raw nanoseconds (time.UnixNano()) and converted to a UTC
+// RFC3339Nano-formatted string. For numeric fields the int64 is decoded via
+// Int64ToFloat64 (IEEE 754 bit reinterpretation).
+func decodeDocValueTerm(term []byte, fieldType string) interface{} {
+ if len(term) == 0 {
+ return nil
+ }
+
+ // Check if it's a prefix-coded numeric term.
+ if valid, shift := numeric.ValidPrefixCodedTermBytes(term); valid {
+ // Only shift-0 terms carry the actual value.
+ if shift != 0 {
+ return nil
+ }
+ i64, err := numeric.PrefixCoded(term).Int64()
+ if err != nil {
+ return nil
+ }
+ if fieldType == "datetime" {
+ // Datetime doc values store time.UnixNano() directly as int64.
+ // Convert back to a formatted string so callbacks (including
+ // JS UDFs) receive a human-readable date like "2022-03-10T00:00:00Z".
+ return time.Unix(0, i64).UTC().Format(time.RFC3339Nano)
+ }
+ // Numeric float64 fields use Float64ToInt64 bit manipulation encoding.
+ return numeric.Int64ToFloat64(i64)
+ }
+
+ // Boolean fields are stored as "T" or "F".
+ if len(term) == 1 {
+ if term[0] == 'T' {
+ return true
+ }
+ if term[0] == 'F' {
+ return false
+ }
+ }
+
+ // Default: text/keyword — return as string.
+ return string(term)
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_filter.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_filter.go
new file mode 100644
index 0000000000..dcf1f7c056
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_filter.go
@@ -0,0 +1,121 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package searcher
+
+import (
+ "context"
+ "reflect"
+
+ "github.com/blevesearch/bleve/v2/search"
+ "github.com/blevesearch/bleve/v2/size"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+var reflectStaticSizeCustomFilterSearcher int
+
+func init() {
+ var cfs CustomFilterSearcher
+ reflectStaticSizeCustomFilterSearcher = int(reflect.TypeOf(cfs).Size())
+}
+
+// CustomFilterFunc decides whether a hit (with doc-value fields populated)
+// should be kept. Unlike FilterFunc it does not receive a SearchContext since
+// custom-query callbacks only need the DocumentMatch.
+type CustomFilterFunc func(d *search.DocumentMatch) bool
+
+// CustomFilterSearcher wraps a child searcher, optionally loads doc values
+// into each DocumentMatch, then applies a CustomFilterFunc to decide whether
+// to keep the hit. Unlike FilteringSearcher this variant is purpose-built for
+// custom queries that need field values at callback time.
+type CustomFilterSearcher struct {
+ child search.Searcher
+ accept CustomFilterFunc
+ dvReader index.DocValueReader
+ indexReader index.IndexReader
+ fieldTypes map[string]string
+}
+
+func NewCustomFilterSearcher(ctx context.Context, child search.Searcher,
+ filter CustomFilterFunc, dvReader index.DocValueReader,
+ indexReader index.IndexReader,
+ fieldTypes map[string]string) *CustomFilterSearcher {
+ return &CustomFilterSearcher{
+ child: child,
+ accept: filter,
+ dvReader: dvReader,
+ indexReader: indexReader,
+ fieldTypes: fieldTypes,
+ }
+}
+
+func (f *CustomFilterSearcher) Size() int {
+ return reflectStaticSizeCustomFilterSearcher + size.SizeOfPtr +
+ f.child.Size()
+}
+
+func (f *CustomFilterSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) {
+ next, err := f.child.Next(ctx)
+ for next != nil && err == nil {
+ if err = loadDocValuesOnHitWithTypes(next, f.dvReader, f.indexReader, f.fieldTypes); err != nil {
+ return nil, err
+ }
+ if f.accept(next) {
+ return next, nil
+ }
+ next, err = f.child.Next(ctx)
+ }
+ return nil, err
+}
+
+func (f *CustomFilterSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) {
+ adv, err := f.child.Advance(ctx, ID)
+ if err != nil {
+ return nil, err
+ }
+ if adv == nil {
+ return nil, nil
+ }
+ if err = loadDocValuesOnHitWithTypes(adv, f.dvReader, f.indexReader, f.fieldTypes); err != nil {
+ return nil, err
+ }
+ if f.accept(adv) {
+ return adv, nil
+ }
+ return f.Next(ctx)
+}
+
+func (f *CustomFilterSearcher) Close() error {
+ return f.child.Close()
+}
+
+func (f *CustomFilterSearcher) Weight() float64 {
+ return f.child.Weight()
+}
+
+func (f *CustomFilterSearcher) SetQueryNorm(n float64) {
+ f.child.SetQueryNorm(n)
+}
+
+func (f *CustomFilterSearcher) Count() uint64 {
+ return f.child.Count()
+}
+
+func (f *CustomFilterSearcher) Min() int {
+ return f.child.Min()
+}
+
+func (f *CustomFilterSearcher) DocumentMatchPoolSize() int {
+ return f.child.DocumentMatchPoolSize()
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_score.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_score.go
new file mode 100644
index 0000000000..15eb121266
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_custom_score.go
@@ -0,0 +1,114 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package searcher
+
+import (
+ "context"
+ "reflect"
+
+ "github.com/blevesearch/bleve/v2/search"
+ "github.com/blevesearch/bleve/v2/size"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+var reflectStaticSizeCustomScoreSearcher int
+
+func init() {
+ var sfs CustomScoreSearcher
+ reflectStaticSizeCustomScoreSearcher = int(reflect.TypeOf(sfs).Size())
+}
+
+// CustomScoreFunc defines a function which can mutate document scores.
+type CustomScoreFunc func(d *search.DocumentMatch) float64
+
+// CustomScoreSearcher wraps any other searcher, optionally loads doc values
+// into each DocumentMatch, then mutates the score using the supplied
+// CustomScoreFunc.
+type CustomScoreSearcher struct {
+ child search.Searcher
+ mutate CustomScoreFunc
+ dvReader index.DocValueReader
+ indexReader index.IndexReader
+ fieldTypes map[string]string
+}
+
+func NewCustomScoreSearcher(ctx context.Context, s search.Searcher, mutate CustomScoreFunc,
+ dvReader index.DocValueReader, indexReader index.IndexReader,
+ fieldTypes map[string]string) *CustomScoreSearcher {
+ return &CustomScoreSearcher{
+ child: s,
+ mutate: mutate,
+ dvReader: dvReader,
+ indexReader: indexReader,
+ fieldTypes: fieldTypes,
+ }
+}
+
+func (f *CustomScoreSearcher) Size() int {
+ return reflectStaticSizeCustomScoreSearcher + size.SizeOfPtr +
+ f.child.Size()
+}
+
+func (f *CustomScoreSearcher) Next(ctx *search.SearchContext) (*search.DocumentMatch, error) {
+ next, err := f.child.Next(ctx)
+ if err != nil {
+ return nil, err
+ }
+ if next != nil {
+ if err = loadDocValuesOnHitWithTypes(next, f.dvReader, f.indexReader, f.fieldTypes); err != nil {
+ return nil, err
+ }
+ next.Score = f.mutate(next)
+ }
+ return next, nil
+}
+
+func (f *CustomScoreSearcher) Advance(ctx *search.SearchContext, ID index.IndexInternalID) (*search.DocumentMatch, error) {
+ adv, err := f.child.Advance(ctx, ID)
+ if err != nil {
+ return nil, err
+ }
+ if adv != nil {
+ if err = loadDocValuesOnHitWithTypes(adv, f.dvReader, f.indexReader, f.fieldTypes); err != nil {
+ return nil, err
+ }
+ adv.Score = f.mutate(adv)
+ }
+ return adv, nil
+}
+
+func (f *CustomScoreSearcher) Close() error {
+ return f.child.Close()
+}
+
+func (f *CustomScoreSearcher) Weight() float64 {
+ return f.child.Weight()
+}
+
+func (f *CustomScoreSearcher) SetQueryNorm(n float64) {
+ f.child.SetQueryNorm(n)
+}
+
+func (f *CustomScoreSearcher) Count() uint64 {
+ return f.child.Count()
+}
+
+func (f *CustomScoreSearcher) Min() int {
+ return f.child.Min()
+}
+
+func (f *CustomScoreSearcher) DocumentMatchPoolSize() int {
+ return f.child.DocumentMatchPoolSize()
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_disjunction_heap.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_disjunction_heap.go
index 3da876bd35..4c68e5691d 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_disjunction_heap.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_disjunction_heap.go
@@ -15,7 +15,6 @@
package searcher
import (
- "bytes"
"container/heap"
"context"
"math"
@@ -169,7 +168,7 @@ func (s *DisjunctionHeapSearcher) updateMatches() error {
matchingIdxs = append(matchingIdxs, next.matchingIdx)
// now as long as top of heap matches, keep popping
- for len(s.heap) > 0 && bytes.Compare(next.curr.IndexInternalID, s.heap[0].curr.IndexInternalID) == 0 {
+ for len(s.heap) > 0 && next.curr.IndexInternalID.Equals(s.heap[0].curr.IndexInternalID) {
next = heap.Pop(s).(*SearcherCurr)
matching = append(matching, next.curr)
matchingCurrs = append(matchingCurrs, next)
@@ -264,7 +263,7 @@ func (s *DisjunctionHeapSearcher) Advance(ctx *search.SearchContext,
// find all searchers that actually need to be advanced
// advance them, using s.matchingCurrs as temp storage
- for len(s.heap) > 0 && bytes.Compare(s.heap[0].curr.IndexInternalID, ID) < 0 {
+ for len(s.heap) > 0 && s.heap[0].curr.IndexInternalID.Compare(ID) < 0 {
searcherCurr := heap.Pop(s).(*SearcherCurr)
ctx.DocumentMatchPool.Put(searcherCurr.curr)
curr, err := searcherCurr.searcher.Advance(ctx, ID)
@@ -347,7 +346,7 @@ func (s *DisjunctionHeapSearcher) Less(i, j int) bool {
} else if s.heap[j].curr == nil {
return false
}
- return bytes.Compare(s.heap[i].curr.IndexInternalID, s.heap[j].curr.IndexInternalID) < 0
+ return s.heap[i].curr.IndexInternalID.Compare(s.heap[j].curr.IndexInternalID) < 0
}
func (s *DisjunctionHeapSearcher) Swap(i, j int) {
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_filter.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_filter.go
index 97d706b5f9..ef070c73f4 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_filter.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_filter.go
@@ -60,6 +60,9 @@ func (f *FilteringSearcher) Next(ctx *search.SearchContext) (*search.DocumentMat
if f.accept(ctx, next) {
return next, nil
}
+ // recycle this document match now, since
+ // we do not need it anymore
+ ctx.DocumentMatchPool.Put(next)
next, err = f.child.Next(ctx)
}
return nil, err
@@ -76,6 +79,9 @@ func (f *FilteringSearcher) Advance(ctx *search.SearchContext, ID index.IndexInt
if f.accept(ctx, adv) {
return adv, nil
}
+ // recycle this document match now, since
+ // we do not need it anymore
+ ctx.DocumentMatchPool.Put(adv)
return f.Next(ctx)
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoboundingbox.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoboundingbox.go
index c2551a8718..a146fa654d 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoboundingbox.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoboundingbox.go
@@ -53,7 +53,7 @@ func NewGeoBoundingBoxSearcher(ctx context.Context, indexReader index.IndexReade
}
return NewFilteringSearcher(ctx, boxSearcher, buildRectFilter(ctx, dvReader,
- field, minLon, minLat, maxLon, maxLat)), nil
+ minLon, minLat, maxLon, maxLat)), nil
}
}
@@ -88,7 +88,7 @@ func NewGeoBoundingBoxSearcher(ctx context.Context, indexReader index.IndexReade
}
// add filter to check points near the boundary
onBoundarySearcher = NewFilteringSearcher(ctx, rawOnBoundarySearcher,
- buildRectFilter(ctx, dvReader, field, minLon, minLat, maxLon, maxLat))
+ buildRectFilter(ctx, dvReader, minLon, minLat, maxLon, maxLat))
openedSearchers = append(openedSearchers, onBoundarySearcher)
}
@@ -205,28 +205,35 @@ func buildIsIndexedFunc(ctx context.Context, indexReader index.IndexReader, fiel
return isIndexed, closeF, err
}
-func buildRectFilter(ctx context.Context, dvReader index.DocValueReader, field string,
+func buildRectFilter(ctx context.Context, dvReader index.DocValueReader,
minLon, minLat, maxLon, maxLat float64,
) FilterFunc {
+ // reuse the following for each document match that is checked using the filter
+ var lons, lats []float64
+ var found bool
+ dvVisitor := func(_ string, term []byte) {
+ if found {
+ // avoid redundant work if already found
+ return
+ }
+ // only consider the values which are shifted 0
+ prefixCoded := numeric.PrefixCoded(term)
+ shift, err := prefixCoded.Shift()
+ if err == nil && shift == 0 {
+ var i64 int64
+ i64, err = prefixCoded.Int64()
+ if err == nil {
+ lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
+ lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
+ found = true
+ }
+ }
+ }
return func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
// check geo matches against all numeric type terms indexed
- var lons, lats []float64
- var found bool
- err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) {
- // only consider the values which are shifted 0
- prefixCoded := numeric.PrefixCoded(term)
- shift, err := prefixCoded.Shift()
- if err == nil && shift == 0 {
- var i64 int64
- i64, err = prefixCoded.Int64()
- if err == nil {
- lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
- lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
- found = true
- }
- }
- })
- if err == nil && found {
+ lons, lats = lons[:0], lats[:0]
+ found = false
+ if err := dvReader.VisitDocValues(d.IndexInternalID, dvVisitor); err == nil && found {
bytes := dvReader.BytesRead()
if bytes > 0 {
reportIOStats(ctx, bytes)
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopointdistance.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopointdistance.go
index 357ac4de35..7591bcc608 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopointdistance.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopointdistance.go
@@ -66,7 +66,7 @@ func NewGeoPointDistanceSearcher(ctx context.Context, indexReader index.IndexRea
// wrap it in a filtering searcher which checks the actual distance
return NewFilteringSearcher(ctx, rectSearcher,
- buildDistFilter(ctx, dvReader, field, centerLon, centerLat, dist)), nil
+ buildDistFilter(ctx, dvReader, centerLon, centerLat, dist)), nil
}
// boxSearcher builds a searcher for the described bounding box
@@ -113,27 +113,33 @@ func boxSearcher(ctx context.Context, indexReader index.IndexReader,
return boxSearcher, nil
}
-func buildDistFilter(ctx context.Context, dvReader index.DocValueReader, field string,
+func buildDistFilter(ctx context.Context, dvReader index.DocValueReader,
centerLon, centerLat, maxDist float64) FilterFunc {
+ // reuse the following for each document match that is checked using the filter
+ var lons, lats []float64
+ var found bool
+ dvVisitor := func(_ string, term []byte) {
+ if found {
+ // avoid redundant work if already found
+ return
+ }
+ // only consider the values which are shifted 0
+ prefixCoded := numeric.PrefixCoded(term)
+ shift, err := prefixCoded.Shift()
+ if err == nil && shift == 0 {
+ i64, err := prefixCoded.Int64()
+ if err == nil {
+ lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
+ lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
+ found = true
+ }
+ }
+ }
return func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
// check geo matches against all numeric type terms indexed
- var lons, lats []float64
- var found bool
-
- err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) {
- // only consider the values which are shifted 0
- prefixCoded := numeric.PrefixCoded(term)
- shift, err := prefixCoded.Shift()
- if err == nil && shift == 0 {
- i64, err := prefixCoded.Int64()
- if err == nil {
- lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
- lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
- found = true
- }
- }
- })
- if err == nil && found {
+ lons, lats = lons[:0], lats[:0]
+ found = false
+ if err := dvReader.VisitDocValues(d.IndexInternalID, dvVisitor); err == nil && found {
bytes := dvReader.BytesRead()
if bytes > 0 {
reportIOStats(ctx, bytes)
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopolygon.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopolygon.go
index dc04bb66a0..fb6e09be49 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopolygon.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geopolygon.go
@@ -85,28 +85,37 @@ func almostEqual(a, b float64) bool {
// here: https://wrf.ecse.rpi.edu/nikola/pubdetails/pnpoly.html
func buildPolygonFilter(ctx context.Context, dvReader index.DocValueReader, field string,
coordinates []geo.Point) FilterFunc {
+ // reuse the following for each document match that is checked using the filter
+ var lons, lats []float64
+ var found bool
+ dvVisitor := func(_ string, term []byte) {
+ if found {
+ // avoid redundant work if already found
+ return
+ }
+ // only consider the values which are shifted 0
+ prefixCoded := numeric.PrefixCoded(term)
+ shift, err := prefixCoded.Shift()
+ if err == nil && shift == 0 {
+ i64, err := prefixCoded.Int64()
+ if err == nil {
+ lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
+ lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
+ found = true
+ }
+ }
+ }
+ rayIntersectsSegment := func(point, a, b geo.Point) bool {
+ return (a.Lat > point.Lat) != (b.Lat > point.Lat) &&
+ point.Lon < (b.Lon-a.Lon)*(point.Lat-a.Lat)/(b.Lat-a.Lat)+a.Lon
+ }
return func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
// check geo matches against all numeric type terms indexed
- var lons, lats []float64
- var found bool
-
- err := dvReader.VisitDocValues(d.IndexInternalID, func(field string, term []byte) {
- // only consider the values which are shifted 0
- prefixCoded := numeric.PrefixCoded(term)
- shift, err := prefixCoded.Shift()
- if err == nil && shift == 0 {
- i64, err := prefixCoded.Int64()
- if err == nil {
- lons = append(lons, geo.MortonUnhashLon(uint64(i64)))
- lats = append(lats, geo.MortonUnhashLat(uint64(i64)))
- found = true
- }
- }
- })
-
+ lons, lats = lons[:0], lats[:0]
+ found = false
// Note: this approach works for points which are strictly inside
// the polygon. ie it might fail for certain points on the polygon boundaries.
- if err == nil && found {
+ if err := dvReader.VisitDocValues(d.IndexInternalID, dvVisitor); err == nil && found {
bytes := dvReader.BytesRead()
if bytes > 0 {
reportIOStats(ctx, bytes)
@@ -116,10 +125,6 @@ func buildPolygonFilter(ctx context.Context, dvReader index.DocValueReader, fiel
if len(coordinates) < 3 {
return false
}
- rayIntersectsSegment := func(point, a, b geo.Point) bool {
- return (a.Lat > point.Lat) != (b.Lat > point.Lat) &&
- point.Lon < (b.Lon-a.Lon)*(point.Lat-a.Lat)/(b.Lat-a.Lat)+a.Lon
- }
for i := range lons {
pt := geo.Point{Lon: lons[i], Lat: lats[i]}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoshape.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoshape.go
index 703693d781..cd020fafc4 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoshape.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_geoshape.go
@@ -58,18 +58,13 @@ func NewGeoShapeSearcher(ctx context.Context, indexReader index.IndexReader, sha
return NewFilteringSearcher(ctx, mSearcher, buildRelationFilterOnShapes(ctx, dvReader, field, relation, shape)), nil
}
-// Using the same term splitter slice used in the doc values in zap.
-// TODO: This needs to be revisited whenever we change the zap
-// implementation of doc values.
-var termSeparatorSplitSlice = []byte{0xff}
-
func buildRelationFilterOnShapes(ctx context.Context, dvReader index.DocValueReader, field string,
relation string, shape index.GeoJSON,
) FilterFunc {
// this is for accumulating the shape's actual complete value
// spread across multiple docvalue visitor callbacks.
var dvShapeValue []byte
- var startReading, finishReading bool
+ var startReading, finishReading, found bool
var reader *bytes.Reader
var bufPool *s2.GeoBufferPool
@@ -77,51 +72,58 @@ func buildRelationFilterOnShapes(ctx context.Context, dvReader index.DocValueRea
bufPool = bufPoolCallback()
}
- return func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
- var found bool
+ dvVisitor := func(_ string, term []byte) {
+ if found {
+ // avoid redundant work if already found
+ return
+ }
+ tl := len(term)
+ // only consider the values which are GlueBytes prefixed or
+ // if it had already started reading the shape bytes from previous callbacks.
+ if startReading || tl > geo.GlueBytesOffset {
- err := dvReader.VisitDocValues(d.IndexInternalID,
- func(field string, term []byte) {
- // only consider the values which are GlueBytes prefixed or
- // if it had already started reading the shape bytes from previous callbacks.
- if startReading || len(term) > geo.GlueBytesOffset {
+ if !startReading && bytes.Equal(geo.GlueBytes, term[:geo.GlueBytesOffset]) {
+ startReading = true
- if !startReading && bytes.Equal(geo.GlueBytes, term[:geo.GlueBytesOffset]) {
- startReading = true
-
- if bytes.Equal(geo.GlueBytes, term[len(term)-geo.GlueBytesOffset:]) {
- term = term[:len(term)-geo.GlueBytesOffset]
- finishReading = true
- }
-
- dvShapeValue = append(dvShapeValue, term[geo.GlueBytesOffset:]...)
-
- } else if startReading && !finishReading {
- if len(term) > geo.GlueBytesOffset &&
- bytes.Equal(geo.GlueBytes, term[len(term)-geo.GlueBytesOffset:]) {
- term = term[:len(term)-geo.GlueBytesOffset]
- finishReading = true
- }
-
- term = append(termSeparatorSplitSlice, term...)
- dvShapeValue = append(dvShapeValue, term...)
- }
-
- // apply the filter once the entire docvalue is finished reading.
- if finishReading {
- v, err := geojson.FilterGeoShapesOnRelation(shape, dvShapeValue, relation, &reader, bufPool)
- if err == nil && v {
- found = true
- }
-
- dvShapeValue = dvShapeValue[:0]
- startReading = false
- finishReading = false
- }
+ if bytes.Equal(geo.GlueBytes, term[tl-geo.GlueBytesOffset:]) {
+ term = term[:tl-geo.GlueBytesOffset]
+ finishReading = true
}
- })
- if err == nil && found {
+ dvShapeValue = append(dvShapeValue, term[geo.GlueBytesOffset:]...)
+
+ } else if startReading && !finishReading {
+ if tl > geo.GlueBytesOffset &&
+ bytes.Equal(geo.GlueBytes, term[tl-geo.GlueBytesOffset:]) {
+ term = term[:tl-geo.GlueBytesOffset]
+ finishReading = true
+ }
+
+ dvShapeValue = append(dvShapeValue, index.DocValueTermSeparator)
+ dvShapeValue = append(dvShapeValue, term...)
+ }
+
+ // apply the filter once the entire docvalue is finished reading.
+ if finishReading {
+ v, err := geojson.FilterGeoShapesOnRelation(shape, dvShapeValue, relation, &reader, bufPool)
+ if err == nil && v {
+ found = true
+ }
+
+ dvShapeValue = dvShapeValue[:0]
+ startReading = false
+ finishReading = false
+ }
+ }
+ }
+
+ return func(sctx *search.SearchContext, d *search.DocumentMatch) bool {
+ // reset state variables for each document
+ found = false
+ startReading = false
+ finishReading = false
+ dvShapeValue = dvShapeValue[:0]
+ if err := dvReader.VisitDocValues(d.IndexInternalID, dvVisitor); err == nil && found {
bytes := dvReader.BytesRead()
if bytes > 0 {
reportIOStats(ctx, bytes)
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_numeric_range.go b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_numeric_range.go
index f086051c11..cd8f007196 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_numeric_range.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/searcher/search_numeric_range.go
@@ -132,7 +132,7 @@ func filterCandidateTerms(indexReader index.IndexReader,
for err == nil && tfd != nil {
termBytes := []byte(tfd.Term)
i := sort.Search(len(terms), func(i int) bool { return bytes.Compare(terms[i], termBytes) >= 0 })
- if i < len(terms) && bytes.Compare(terms[i], termBytes) == 0 {
+ if i < len(terms) && bytes.Equal(terms[i], termBytes) {
rv = append(rv, terms[i])
}
terms = terms[i:]
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/sort.go b/vendor/github.com/blevesearch/bleve/v2/search/sort.go
index 44e5cd91c9..64230c1165 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/sort.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/sort.go
@@ -683,29 +683,29 @@ type SortGeoDistance struct {
Field string
Desc bool
Unit string
- values []string
+ values [][]byte
Lon float64
Lat float64
unitMult float64
+ tmp []byte
}
// UpdateVisitor notifies this sort field that in this document
// this field has the specified term
func (s *SortGeoDistance) UpdateVisitor(field string, term []byte) {
if field == s.Field {
- s.values = append(s.values, string(term))
+ s.values = append(s.values, term)
}
}
// Value returns the sort value of the DocumentMatch
-// it also resets the state of this SortField for
+// it also resets the state of this SortGeoDistance for
// processing the next document
func (s *SortGeoDistance) Value(i *DocumentMatch) string {
- iTerms := s.filterTermsByType(s.values)
- iTerm := s.filterTermsByMode(iTerms)
+ iTerm := s.findPrefixCodedNumericTerm(s.values)
s.values = s.values[:0]
- if iTerm == "" {
+ if iTerm == nil {
return maxDistance
}
@@ -723,7 +723,8 @@ func (s *SortGeoDistance) Value(i *DocumentMatch) string {
dist /= s.unitMult
}
distInt64 := numeric.Float64ToInt64(dist)
- return string(numeric.MustNewPrefixCodedInt64(distInt64, 0))
+ s.tmp = numeric.MustNewPrefixCodedInt64Prealloc(distInt64, 0, s.tmp)
+ return string(s.tmp)
}
func (s *SortGeoDistance) DecodeValue(value string) string {
@@ -739,25 +740,16 @@ func (s *SortGeoDistance) Descending() bool {
return s.Desc
}
-func (s *SortGeoDistance) filterTermsByMode(terms []string) string {
- if len(terms) >= 1 {
- return terms[0]
- }
-
- return ""
-}
-
-// filterTermsByType attempts to make one pass on the terms
-// return only valid prefix coded numbers with shift of 0
-func (s *SortGeoDistance) filterTermsByType(terms []string) []string {
- var termsWithShiftZero []string
+// findPrefixCodedNumericTerm looks through the provided terms
+// and returns the first valid prefix coded numeric term with shift of 0
+func (s *SortGeoDistance) findPrefixCodedNumericTerm(terms [][]byte) []byte {
for _, term := range terms {
- valid, shift := numeric.ValidPrefixCodedTerm(term)
+ valid, shift := numeric.ValidPrefixCodedTermBytes(term)
if valid && shift == 0 {
- termsWithShiftZero = append(termsWithShiftZero, term)
+ return term
}
}
- return termsWithShiftZero
+ return nil
}
// RequiresDocID says this SearchSort does not require the DocID be loaded
diff --git a/vendor/github.com/blevesearch/bleve/v2/search/util.go b/vendor/github.com/blevesearch/bleve/v2/search/util.go
index 005fda67df..81f22768a9 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search/util.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search/util.go
@@ -50,41 +50,54 @@ func MergeTermLocationMaps(rv, other TermLocationMap) TermLocationMap {
func MergeFieldTermLocations(dest []FieldTermLocation, matches []*DocumentMatch) []FieldTermLocation {
n := len(dest)
for _, dm := range matches {
- n += len(dm.FieldTermLocations)
+ if dm != nil {
+ n += len(dm.FieldTermLocations)
+ }
}
if cap(dest) < n {
dest = append(make([]FieldTermLocation, 0, n), dest...)
}
for _, dm := range matches {
- for _, ftl := range dm.FieldTermLocations {
- dest = append(dest, FieldTermLocation{
- Field: ftl.Field,
- Term: ftl.Term,
- Location: Location{
- Pos: ftl.Location.Pos,
- Start: ftl.Location.Start,
- End: ftl.Location.End,
- ArrayPositions: append(ArrayPositions(nil), ftl.Location.ArrayPositions...),
- },
- })
+ if dm != nil {
+ dest = mergeFieldTermLocationFromMatch(dest, dm)
}
}
return dest
}
-type SearchIOStatsCallbackFunc func(uint64)
+// MergeFieldTermLocationsFromMatch merges field term locations from a single DocumentMatch
+// into dest, returning the updated slice.
+func MergeFieldTermLocationsFromMatch(dest []FieldTermLocation, match *DocumentMatch) []FieldTermLocation {
+ if match == nil {
+ return dest
+ }
+ n := len(dest) + len(match.FieldTermLocations)
+ if cap(dest) < n {
+ dest = append(make([]FieldTermLocation, 0, n), dest...)
+ }
+ return mergeFieldTermLocationFromMatch(dest, match)
+}
-// Implementation of SearchIncrementalCostCallbackFn should handle the following messages
-// - add: increment the cost of a search operation
-// (which can be specific to a query type as well)
-// - abort: query was aborted due to a cancel of search's context (for eg),
-// which can be handled differently as well
-// - done: indicates that a search was complete and the tracked cost can be
-// handled safely by the implementation.
-type SearchIncrementalCostCallbackFn func(SearchIncrementalCostCallbackMsg,
- SearchQueryType, uint64)
+// mergeFieldTermLocationFromMatch appends field term locations from a DocumentMatch into dest.
+// Assumes dest has sufficient capacity.
+func mergeFieldTermLocationFromMatch(dest []FieldTermLocation, dm *DocumentMatch) []FieldTermLocation {
+ for _, ftl := range dm.FieldTermLocations {
+ dest = append(dest, FieldTermLocation{
+ Field: ftl.Field,
+ Term: ftl.Term,
+ Location: Location{
+ Pos: ftl.Location.Pos,
+ Start: ftl.Location.Start,
+ End: ftl.Location.End,
+ ArrayPositions: append(ArrayPositions(nil), ftl.Location.ArrayPositions...),
+ },
+ })
+ }
+
+ return dest
+}
type (
SearchIncrementalCostCallbackMsg uint
@@ -156,6 +169,10 @@ const (
// ScoreFusionKey is used to communicate whether KNN hits need to be preserved for
// hybrid search algorithms (like RRF)
ScoreFusionKey ContextKey = "_fusion_rescoring_key"
+
+ // NestedSearchKey is used to communicate whether the search is performed
+ // in an index with nested documents
+ NestedSearchKey ContextKey = "_nested_search_key"
)
func RecordSearchCost(ctx context.Context,
@@ -184,9 +201,7 @@ const (
MinGeoBufPoolSize = 24
)
-type GeoBufferPoolCallbackFunc func() *s2.GeoBufferPool
-
-// *PreSearchDataKey are used to store the data gathered during the presearch phase
+// PreSearchDataKey are used to store the data gathered during the presearch phase
// which would be use in the actual search phase.
const (
KnnPreSearchDataKey = "_knn_pre_search_data_key"
@@ -197,14 +212,39 @@ const (
const GlobalScoring = "_global_scoring"
type (
+ // SearcherStartCallbackFn is a callback function type used to signal the start of
+ // searcher creation phase.
SearcherStartCallbackFn func(size uint64) error
- SearcherEndCallbackFn func(size uint64) error
+ // SearcherEndCallbackFn is a callback function type used to signal the end of
+ // a searcher creation phase.
+ SearcherEndCallbackFn func(size uint64) error
+ // GetScoringModelCallbackFn is a callback function type used to get the scoring model
+ // to be used for scoring documents during search.
+ GetScoringModelCallbackFn func() string
+ // HybridMergeCallbackFn is a callback function type used to merge a KNN document match
+ // into a full text search document match, of the same docID as part of hybrid search.
+ HybridMergeCallbackFn func(ftsMatch *DocumentMatch, knnMatch *DocumentMatch)
+ // DescendantAdderCallback is a callback function type used to customize how a descendant
+ // DocumentMatch is merged into its parent. This allows different descendant addition strategies for
+ // different use cases (e.g., TopN vs KNN collection).
+ DescendantAdderCallbackFn func(parent *DocumentMatch, descendant *DocumentMatch) error
+ // GeoBufferPoolCallbackFunc is a callback function type used to get the geo buffer pool
+ // to be used during geo searches.
+ GeoBufferPoolCallbackFunc func() *s2.GeoBufferPool
+ // SearchIOStatsCallbackFunc is a callback function type used to report search IO stats
+ // during search.
+ SearchIOStatsCallbackFunc func(uint64)
+ // Implementation of SearchIncrementalCostCallbackFn should handle the following messages
+ // - add: increment the cost of a search operation
+ // (which can be specific to a query type as well)
+ // - abort: query was aborted due to a cancel of search's context (for eg),
+ // which can be handled differently as well
+ // - done: indicates that a search was complete and the tracked cost can be
+ // handled safely by the implementation.
+ SearchIncrementalCostCallbackFn func(SearchIncrementalCostCallbackMsg,
+ SearchQueryType, uint64)
)
-type GetScoringModelCallbackFn func() string
-
-type ScoreExplCorrectionCallbackFunc func(queryMatch *DocumentMatch, knnMatch *DocumentMatch) (float64, *Explanation)
-
// field -> term -> synonyms
type FieldTermSynonymMap map[string]map[string][]string
@@ -237,3 +277,28 @@ type BM25Stats struct {
DocCount float64 `json:"doc_count"`
FieldCardinality map[string]int `json:"field_cardinality"`
}
+
+// FieldSet represents a set of queried fields.
+type FieldSet map[string]struct{}
+
+// NewFieldSet creates a new FieldSet.
+func NewFieldSet() FieldSet {
+ return make(map[string]struct{})
+}
+
+// Add adds a field to the set.
+func (fs FieldSet) AddField(field string) {
+ fs[field] = struct{}{}
+}
+
+// HasID returns true if the field set contains the "_id" field.
+func (fs FieldSet) HasID() bool {
+ _, ok := fs["_id"]
+ return ok
+}
+
+// HasAll returns true if the field set contains the "_all" field.
+func (fs FieldSet) HasAll() bool {
+ _, ok := fs["_all"]
+ return ok
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search_knn.go b/vendor/github.com/blevesearch/bleve/v2/search_knn.go
index 54771ede01..fa119f48cb 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search_knn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search_knn.go
@@ -27,6 +27,7 @@ import (
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/search/collector"
"github.com/blevesearch/bleve/v2/search/query"
+ "github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
)
@@ -42,18 +43,18 @@ type SearchRequest struct {
Query query.Query `json:"query"`
Size int `json:"size"`
From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
+ Highlight *HighlightRequest `json:"highlight,omitempty"`
+ Fields []string `json:"fields,omitempty"`
+ Facets FacetsRequest `json:"facets,omitempty"`
Explain bool `json:"explain"`
Sort search.SortOrder `json:"sort"`
IncludeLocations bool `json:"includeLocations"`
Score string `json:"score,omitempty"`
- SearchAfter []string `json:"search_after"`
- SearchBefore []string `json:"search_before"`
+ SearchAfter []string `json:"search_after,omitempty"`
+ SearchBefore []string `json:"search_before,omitempty"`
- KNN []*KNNRequest `json:"knn"`
- KNNOperator knnOperator `json:"knn_operator"`
+ KNN []*KNNRequest `json:"knn,omitempty"`
+ KNNOperator knnOperator `json:"knn_operator,omitempty"`
// PreSearchData will be a map that will be used
// in the second phase of any 2-phase search, to provide additional
@@ -125,35 +126,35 @@ func (r *SearchRequest) AddKNNOperator(operator knnOperator) {
// a SearchRequest
func (r *SearchRequest) UnmarshalJSON(input []byte) error {
type tempKNNReq struct {
- Field string `json:"field"`
- Vector []float32 `json:"vector"`
- VectorBase64 string `json:"vector_base64"`
- K int64 `json:"k"`
- Boost *query.Boost `json:"boost,omitempty"`
- Params json.RawMessage `json:"params"`
- FilterQuery json.RawMessage `json:"filter,omitempty"`
+ Field string `json:"field"`
+ Vector []float32 `json:"vector"`
+ VectorBase64 string `json:"vector_base64"`
+ K int64 `json:"k"`
+ Boost *query.Boost `json:"boost,omitempty"`
+ Params OptionalRawMessage `json:"params"`
+ FilterQuery OptionalRawMessage `json:"filter,omitempty"`
}
var temp struct {
- Q json.RawMessage `json:"query"`
- Size *int `json:"size"`
- From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
- Explain bool `json:"explain"`
- Sort []json.RawMessage `json:"sort"`
- IncludeLocations bool `json:"includeLocations"`
- Score string `json:"score"`
- SearchAfter []string `json:"search_after"`
- SearchBefore []string `json:"search_before"`
- KNN []*tempKNNReq `json:"knn"`
- KNNOperator knnOperator `json:"knn_operator"`
- PreSearchData json.RawMessage `json:"pre_search_data"`
- Params json.RawMessage `json:"params"`
+ Q json.RawMessage `json:"query"`
+ Size *int `json:"size"`
+ From int `json:"from"`
+ Highlight *HighlightRequest `json:"highlight"`
+ Fields []string `json:"fields"`
+ Facets FacetsRequest `json:"facets"`
+ Explain bool `json:"explain"`
+ Sort []json.RawMessage `json:"sort"`
+ IncludeLocations bool `json:"includeLocations"`
+ Score string `json:"score"`
+ SearchAfter []string `json:"search_after"`
+ SearchBefore []string `json:"search_before"`
+ KNN []*tempKNNReq `json:"knn"`
+ KNNOperator knnOperator `json:"knn_operator"`
+ PreSearchData OptionalRawMessage `json:"pre_search_data"`
+ Params OptionalRawMessage `json:"params"`
}
- err := json.Unmarshal(input, &temp)
+ err := util.UnmarshalJSON(input, &temp)
if err != nil {
return err
}
@@ -216,11 +217,10 @@ func (r *SearchRequest) UnmarshalJSON(input []byte) error {
r.KNN[i].VectorBase64 = temp.KNN[i].VectorBase64
r.KNN[i].K = temp.KNN[i].K
r.KNN[i].Boost = temp.KNN[i].Boost
- r.KNN[i].Params = temp.KNN[i].Params
- if len(knnReq.FilterQuery) == 0 {
- // Setting this to nil to avoid ParseQuery() setting it to a match none
- r.KNN[i].FilterQuery = nil
- } else {
+ if len(temp.KNN[i].Params) > 0 {
+ r.KNN[i].Params = json.RawMessage(temp.KNN[i].Params)
+ }
+ if len(temp.KNN[i].FilterQuery) > 0 {
r.KNN[i].FilterQuery, err = query.ParseQuery(knnReq.FilterQuery)
if err != nil {
return err
@@ -377,7 +377,7 @@ func addSortAndFieldsToKNNHits(req *SearchRequest, knnHits []*search.DocumentMat
}
}
req.Sort.Value(hit)
- err, _ = LoadAndHighlightFields(hit, req, "", reader, nil)
+ err, _ = LoadAndHighlightAllFields(hit, req, "", reader, nil)
if err != nil {
return err
}
@@ -474,17 +474,15 @@ func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, rea
return knnHits, nil
}
-func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) {
+func setKnnHitsInCollector(knnHits []*search.DocumentMatch, coll *collector.TopNCollector) {
if len(knnHits) > 0 {
- newScoreExplComputer := func(queryMatch *search.DocumentMatch, knnMatch *search.DocumentMatch) (float64, *search.Explanation) {
- totalScore := queryMatch.Score + knnMatch.Score
- if !req.Explain {
- // exit early as we don't need to compute the explanation
- return totalScore, nil
- }
- return totalScore, &search.Explanation{Value: totalScore, Message: "sum of:", Children: []*search.Explanation{queryMatch.Expl, knnMatch.Expl}}
+ mergeFn := func(ftsMatch *search.DocumentMatch, knnMatch *search.DocumentMatch) {
+ // Boost the FTS score using the KNN score
+ ftsMatch.Score += knnMatch.Score
+ // Combine the FTS explanation with the KNN explanation, if present
+ ftsMatch.Expl.MergeWith(knnMatch.Expl)
}
- coll.SetKNNHits(knnHits, search.ScoreExplCorrectionCallbackFunc(newScoreExplComputer))
+ coll.SetKNNHits(knnHits, search.HybridMergeCallbackFn(mergeFn))
}
}
diff --git a/vendor/github.com/blevesearch/bleve/v2/search_no_knn.go b/vendor/github.com/blevesearch/bleve/v2/search_no_knn.go
index 172f258ec7..f294a476f3 100644
--- a/vendor/github.com/blevesearch/bleve/v2/search_no_knn.go
+++ b/vendor/github.com/blevesearch/bleve/v2/search_no_knn.go
@@ -25,6 +25,7 @@ import (
"github.com/blevesearch/bleve/v2/search"
"github.com/blevesearch/bleve/v2/search/collector"
"github.com/blevesearch/bleve/v2/search/query"
+ "github.com/blevesearch/bleve/v2/util"
index "github.com/blevesearch/bleve_index_api"
)
@@ -55,15 +56,15 @@ type SearchRequest struct {
Query query.Query `json:"query"`
Size int `json:"size"`
From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
+ Highlight *HighlightRequest `json:"highlight,omitempty"`
+ Fields []string `json:"fields,omitempty"`
+ Facets FacetsRequest `json:"facets,omitempty"`
Explain bool `json:"explain"`
Sort search.SortOrder `json:"sort"`
IncludeLocations bool `json:"includeLocations"`
Score string `json:"score,omitempty"`
- SearchAfter []string `json:"search_after"`
- SearchBefore []string `json:"search_before"`
+ SearchAfter []string `json:"search_after,omitempty"`
+ SearchBefore []string `json:"search_before,omitempty"`
// PreSearchData will be a map that will be used
// in the second phase of any 2-phase search, to provide additional
@@ -86,23 +87,23 @@ type SearchRequest struct {
// a SearchRequest
func (r *SearchRequest) UnmarshalJSON(input []byte) error {
var temp struct {
- Q json.RawMessage `json:"query"`
- Size *int `json:"size"`
- From int `json:"from"`
- Highlight *HighlightRequest `json:"highlight"`
- Fields []string `json:"fields"`
- Facets FacetsRequest `json:"facets"`
- Explain bool `json:"explain"`
- Sort []json.RawMessage `json:"sort"`
- IncludeLocations bool `json:"includeLocations"`
- Score string `json:"score"`
- SearchAfter []string `json:"search_after"`
- SearchBefore []string `json:"search_before"`
- PreSearchData json.RawMessage `json:"pre_search_data"`
- Params json.RawMessage `json:"params"`
+ Q json.RawMessage `json:"query"`
+ Size *int `json:"size"`
+ From int `json:"from"`
+ Highlight *HighlightRequest `json:"highlight"`
+ Fields []string `json:"fields"`
+ Facets FacetsRequest `json:"facets"`
+ Explain bool `json:"explain"`
+ Sort []json.RawMessage `json:"sort"`
+ IncludeLocations bool `json:"includeLocations"`
+ Score string `json:"score"`
+ SearchAfter []string `json:"search_after"`
+ SearchBefore []string `json:"search_before"`
+ PreSearchData OptionalRawMessage `json:"pre_search_data"`
+ Params OptionalRawMessage `json:"params"`
}
- err := json.Unmarshal(input, &temp)
+ err := util.UnmarshalJSON(input, &temp)
if err != nil {
return err
}
@@ -197,7 +198,7 @@ func (i *indexImpl) runKnnCollector(ctx context.Context, req *SearchRequest, rea
return nil, nil
}
-func setKnnHitsInCollector(knnHits []*search.DocumentMatch, req *SearchRequest, coll *collector.TopNCollector) {
+func setKnnHitsInCollector(knnHits []*search.DocumentMatch, coll *collector.TopNCollector) {
}
func requestHasKNN(req *SearchRequest) bool {
diff --git a/vendor/github.com/blevesearch/bleve/v2/util/bolt.go b/vendor/github.com/blevesearch/bleve/v2/util/bolt.go
new file mode 100644
index 0000000000..d05500b7d0
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/util/bolt.go
@@ -0,0 +1,170 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package util
+
+import (
+ "fmt"
+ "os"
+
+ bolt "go.etcd.io/bbolt"
+)
+
+// All of the bolt impls provide a layer of indirection to allow for processing
+// of values as they are read/written to bolt depending on the key or bucket name
+// This is used to allow better support for file callbacks
+
+// wrapper around bolt.DB
+type RootBoltImpl struct {
+ *bolt.DB
+}
+
+// wrapper around bolt.Tx
+type BoltTxImpl struct {
+ *bolt.Tx
+}
+
+// wrapper around bolt.Bucket
+type BoltBucketImpl struct {
+ *bolt.Bucket
+
+ name string // store the name of the bucket during creation
+}
+
+func OpenBolt(path string, mode os.FileMode, options *bolt.Options) (*RootBoltImpl, error) {
+ db, err := bolt.Open(path, mode, options)
+ if err != nil {
+ return nil, err
+ }
+ return &RootBoltImpl{DB: db}, nil
+}
+
+func (r *RootBoltImpl) Begin(writable bool) (*BoltTxImpl, error) {
+ tx, err := r.DB.Begin(writable)
+ if err != nil {
+ return nil, err
+ }
+ return &BoltTxImpl{Tx: tx}, nil
+}
+
+func (r *RootBoltImpl) View(fn func(*BoltTxImpl) error) error {
+ return r.DB.View(func(tx *bolt.Tx) error {
+ return fn(&BoltTxImpl{Tx: tx})
+ })
+}
+
+func (r *RootBoltImpl) Update(fn func(*BoltTxImpl) error) error {
+ return r.DB.Update(func(tx *bolt.Tx) error {
+ return fn(&BoltTxImpl{Tx: tx})
+ })
+}
+
+func (tx *BoltTxImpl) CreateBucketIfNotExists(name []byte) (*BoltBucketImpl, error) {
+ bucket, err := tx.Tx.CreateBucketIfNotExists(name)
+ if err != nil {
+ return nil, err
+ }
+ return &BoltBucketImpl{
+ name: string(name),
+ Bucket: bucket,
+ }, nil
+}
+
+func (tx *BoltTxImpl) Bucket(name []byte) *BoltBucketImpl {
+ bucket := tx.Tx.Bucket(name)
+ if bucket == nil {
+ return nil
+ }
+ return &BoltBucketImpl{
+ name: string(name),
+ Bucket: bucket,
+ }
+}
+
+func (b *BoltBucketImpl) GetBucket(name []byte) *BoltBucketImpl {
+ bucket := b.Bucket.Bucket(name)
+ if bucket == nil {
+ return nil
+ }
+ return &BoltBucketImpl{
+ name: string(name),
+ Bucket: bucket,
+ }
+}
+
+func (b *BoltBucketImpl) CreateBucketIfNotExists(name []byte) (*BoltBucketImpl, error) {
+ bucket, err := b.Bucket.CreateBucketIfNotExists(name)
+ if err != nil {
+ return nil, err
+ }
+ return &BoltBucketImpl{
+ name: string(name),
+ Bucket: bucket,
+ }, nil
+}
+
+// Process values during ForEach if the bucket name or key is in the boltKeysProcessed map
+func (b *BoltBucketImpl) ForEach(fn func(key []byte, value []byte) error, reader FileReader) error {
+ _, ok1 := boltKeysProcessed[b.name]
+ return b.Bucket.ForEach(func(k, v []byte) error {
+ v = append([]byte(nil), v...)
+ if _, ok2 := boltKeysProcessed[string(k)]; ok1 || ok2 {
+ if reader == nil {
+ return fmt.Errorf("reader callback is required for bucket %s", b.name)
+ }
+ processedValue, err := reader.Process(v)
+ if err != nil {
+ return err
+ }
+ return fn(k, processedValue)
+ }
+ return fn(k, v)
+ })
+}
+
+// Process values during Put/Get if the bucket name or key is in the boltKeysProcessed map
+func (b *BoltBucketImpl) Put(key []byte, value []byte, writer FileWriter) error {
+ _, ok1 := boltKeysProcessed[string(key)]
+ _, ok2 := boltKeysProcessed[b.name]
+ value = append([]byte(nil), value...)
+ if ok1 || ok2 {
+ if writer == nil {
+ return fmt.Errorf("writer callback is required for key %s", string(key))
+ }
+ processedValue := writer.Process(value)
+ return b.Bucket.Put(key, processedValue)
+ }
+ return b.Bucket.Put(key, value)
+}
+
+// Process values during Put/Get if the bucket name or key is in the boltKeysProcessed map
+func (b *BoltBucketImpl) Get(key []byte, reader FileReader) ([]byte, error) {
+ _, ok1 := boltKeysProcessed[string(key)]
+ _, ok2 := boltKeysProcessed[b.name]
+ if ok1 || ok2 {
+ if reader == nil {
+ return nil, fmt.Errorf("reader callback is required for key %s", string(key))
+ }
+ val := b.Bucket.Get(key)
+ if val == nil {
+ return nil, nil
+ }
+ processedVal, err := reader.Process(val)
+ if err != nil {
+ return nil, err
+ }
+ return processedVal, nil
+ }
+ return b.Bucket.Get(key), nil
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/util/file_callbacks.go b/vendor/github.com/blevesearch/bleve/v2/util/file_callbacks.go
new file mode 100644
index 0000000000..c485006e24
--- /dev/null
+++ b/vendor/github.com/blevesearch/bleve/v2/util/file_callbacks.go
@@ -0,0 +1,129 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package util
+
+import (
+ "fmt"
+
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// This file provides a mechanism for users of bleve to provide callbacks
+// that can process data before it is written to disk, and after it is read
+// from disk. This can be used for things like encryption, compression, etc.
+
+// The user is responsible for ensuring that the writer and reader callbacks
+// are compatible with each other, and that any state needed by the callbacks
+// is managed appropriately. For example, if the writer callback uses a
+// unique key or nonce per write, the reader callback must be able to
+// determine the correct key or nonce to use for each read.
+
+// The callbacks are identified by an id string, which is returned by the
+// WriterHook. The same id string is passed to the ReaderHook when creating a reader.
+// This allows the reader to determine which callback to use for a given file.
+
+// Support for identifying all callbacks used by a given index and to remove
+// selected callbacks associated with ids is provided via index.WriterIdsInUse()
+// and index.DropWriterIds().
+
+const DefaultFileCallbackId = ""
+
+// FileWriter and FileReader interfaces are wrappers around the callback functions
+// provided by the user. They provide a convenient way to apply the callbacks to data
+// being written to or read from a file. They also store the id the callbacks,
+// which can be useful for managing state across multiple reads and writes.
+type FileWriter interface {
+ Process(data []byte) []byte
+ Id() string
+}
+type fileWriterImpl struct {
+ id string
+ processor func(data []byte) []byte
+}
+
+func NewFileWriter(context []byte) (FileWriter, error) {
+ rv := &fileWriterImpl{}
+
+ if index.WriterHook != nil {
+ var err error
+ rv.id, rv.processor, err = index.WriterHook(context)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return rv, nil
+}
+
+func (w *fileWriterImpl) Process(data []byte) []byte {
+ if w.processor != nil {
+ return w.processor(data)
+ }
+ return data
+}
+
+func (w *fileWriterImpl) Id() string {
+ return w.id
+}
+
+type FileReader interface {
+ Process(data []byte) ([]byte, error)
+ Id() string
+}
+
+type fileReaderImpl struct {
+ id string
+ processor func(data []byte) ([]byte, error)
+}
+
+func NewFileReader(id string, context []byte) (FileReader, error) {
+ rv := &fileReaderImpl{
+ id: id,
+ }
+
+ if index.ReaderHook != nil {
+ var err error
+ rv.processor, err = index.ReaderHook(id, context)
+ if err != nil {
+ return nil, err
+ }
+ } else if id != "" {
+ return nil, fmt.Errorf("reader callback id %s provided but no ReaderHook is set", id)
+ }
+
+ return rv, nil
+}
+
+func (r *fileReaderImpl) Process(data []byte) ([]byte, error) {
+ if r.processor != nil {
+ return r.processor(data)
+ }
+ return data, nil
+}
+
+func (r *fileReaderImpl) Id() string {
+ return r.id
+}
+
+// -----------------------------------------------------------------------
+
+// set of bolt keys and bucket names that require processing by the reader
+// and writer callbacks.
+var boltKeysProcessed = map[string]struct{}{
+ string(BoltDeletedKey): {},
+ string(BoltInternalKey): {},
+ string(BoltStatsKey): {},
+ string(BoltUpdatedFieldsKey): {},
+}
diff --git a/vendor/github.com/blevesearch/bleve/v2/util/keys.go b/vendor/github.com/blevesearch/bleve/v2/util/keys.go
index b71a7f48ba..fb321eba5f 100644
--- a/vendor/github.com/blevesearch/bleve/v2/util/keys.go
+++ b/vendor/github.com/blevesearch/bleve/v2/util/keys.go
@@ -17,6 +17,9 @@ package util
var (
// Bolt keys
BoltSnapshotsBucket = []byte{'s'}
+ BoltTrainerKey = []byte{'t'}
+ BoltTrainCompleteKey = []byte{'c'}
+ BoltTrainedSamplesKey = []byte{'n'}
BoltPathKey = []byte{'p'}
BoltDeletedKey = []byte{'d'}
BoltInternalKey = []byte{'i'}
@@ -27,6 +30,7 @@ var (
BoltStatsKey = []byte("stats")
BoltUpdatedFieldsKey = []byte("fields")
TotBytesWrittenKey = []byte("TotBytesWritten")
+ BoltMetaDataFileWriterIDKey = []byte("fileWriterID")
MappingInternalKey = []byte("_mapping")
)
diff --git a/vendor/github.com/blevesearch/bleve_index_api/.golangci.yml b/vendor/github.com/blevesearch/bleve_index_api/.golangci.yml
deleted file mode 100644
index a00f6c57e4..0000000000
--- a/vendor/github.com/blevesearch/bleve_index_api/.golangci.yml
+++ /dev/null
@@ -1,37 +0,0 @@
-linters:
- # please, do not use `enable-all`: it's deprecated and will be removed soon.
- # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
- disable-all: true
- enable:
- - bodyclose
- - deadcode
- - depguard
- - dogsled
- - dupl
- - errcheck
- - goconst
- - gocritic
- - gocyclo
- - gofmt
- - goimports
- - gomnd
- - goprintffuncname
- - gosimple
- - govet
- - ineffassign
- - interfacer
- - lll
- - misspell
- - nakedret
- - nolintlint
- - rowserrcheck
- - scopelint
- - staticcheck
- - structcheck
- - stylecheck
- - typecheck
- - unconvert
- - unparam
- - unused
- - varcheck
- - whitespace
diff --git a/vendor/github.com/blevesearch/bleve_index_api/README.md b/vendor/github.com/blevesearch/bleve_index_api/README.md
index 46daa68322..84d05e3f9f 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/README.md
+++ b/vendor/github.com/blevesearch/bleve_index_api/README.md
@@ -1,11 +1,10 @@
# Bleve Index API
-[](https://pkg.go.dev/github.com/blevesearch/bleve_index_api)
-[](https://github.com/blevesearch/bleve_index_api/actions?query=workflow%3ATests+event%3Apush+branch%3Amaster)
-[](https://github.com/blevesearch/bleve_index_api/actions?query=workflow%3ALint+event%3Apush+branch%3Amaster)
+[](https://pkg.go.dev/github.com/blevesearch/bleve_index_api)
+[](https://github.com/blevesearch/bleve_index_api/actions/workflows/tests.yml?query=event%3Apush+branch%3Amaster)
Bleve supports a pluggable Index interface.
By placing these interfaces in their own, *hopefully* slowly evolving module, it frees up Bleve and the underlying index to each introduce new major versions without interfering with one another.
-With that in mind, we anticipate introducing non-breaking changes only to this module, and keeping the major version at 1.x for some time.
\ No newline at end of file
+With that in mind, we anticipate introducing non-breaking changes only to this module, and keeping the major version at 1.x for some time.
diff --git a/vendor/github.com/blevesearch/bleve_index_api/directory.go b/vendor/github.com/blevesearch/bleve_index_api/directory.go
index 709a384565..4a9df17f21 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/directory.go
+++ b/vendor/github.com/blevesearch/bleve_index_api/directory.go
@@ -21,3 +21,8 @@ import (
type Directory interface {
GetWriter(filePath string) (io.WriteCloser, error)
}
+
+type IndexDirectory interface {
+ Directory
+ SetPathInBolt(key []byte, value []byte) error
+}
diff --git a/vendor/github.com/blevesearch/bleve_index_api/document.go b/vendor/github.com/blevesearch/bleve_index_api/document.go
index bc91c6c4cc..e3fd48841b 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/document.go
+++ b/vendor/github.com/blevesearch/bleve_index_api/document.go
@@ -124,3 +124,11 @@ type SynonymDocument interface {
// The provided visitor function is called for each synonym field.
VisitSynonymFields(visitor SynonymFieldVisitor)
}
+
+// NestedDocument is a document that contains other documents inside it.
+type NestedDocument interface {
+ Document
+ // VisitNestedDocuments allows iteration over all nested documents in the document.
+ // The provided visitor function is called for each nested document.
+ VisitNestedDocuments(visitor func(doc Document))
+}
diff --git a/vendor/github.com/blevesearch/bleve_index_api/index.go b/vendor/github.com/blevesearch/bleve_index_api/index.go
index 12d907e590..679dd4ff0f 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/index.go
+++ b/vendor/github.com/blevesearch/bleve_index_api/index.go
@@ -17,6 +17,8 @@ package index
import (
"bytes"
"context"
+ "encoding/binary"
+ "fmt"
"reflect"
)
@@ -57,6 +59,11 @@ type CopyIndex interface {
CopyReader() CopyReader
}
+type TrainableIndex interface {
+ Index
+ Train(*Batch) error
+}
+
// EventIndex is an optional interface for exposing the support for firing event
// callbacks for various events in the index.
type EventIndex interface {
@@ -185,17 +192,46 @@ func (tfv *TermFieldVector) Size() int {
len(tfv.Field) + len(tfv.ArrayPositions)*sizeOfUint64
}
-// IndexInternalID is an opaque document identifier interal to the index impl
+// IndexInternalID is an opaque document identifier internal to the index impl
type IndexInternalID []byte
+// NewIndexInternalID encodes a uint64 into an 8-byte big-endian ID, reusing `buf` when possible.
+func NewIndexInternalID(buf []byte, in uint64) IndexInternalID {
+ if len(buf) != 8 {
+ if cap(buf) >= 8 {
+ buf = buf[0:8]
+ } else {
+ buf = make([]byte, 8)
+ }
+ }
+ binary.BigEndian.PutUint64(buf, in)
+ return buf
+}
+
+// NewIndexInternalIDFrom creates a new IndexInternalID by copying from `other`, reusing `buf` when possible.
+func NewIndexInternalIDFrom(buf IndexInternalID, other IndexInternalID) IndexInternalID {
+ buf = buf[:0]
+ return append(buf, other...)
+}
+
+// Equals checks if two IndexInternalID values are equal.
func (id IndexInternalID) Equals(other IndexInternalID) bool {
return id.Compare(other) == 0
}
+// Compare compares two IndexInternalID values, inherently comparing the encoded uint64 values.
func (id IndexInternalID) Compare(other IndexInternalID) int {
return bytes.Compare(id, other)
}
+// Value returns the uint64 value encoded in the IndexInternalID.
+func (id IndexInternalID) Value() (uint64, error) {
+ if len(id) != 8 {
+ return 0, fmt.Errorf("wrong len for IndexInternalID: %q", id)
+ }
+ return binary.BigEndian.Uint64(id), nil
+}
+
type TermFieldDoc struct {
Term string
ID IndexInternalID
@@ -353,6 +389,21 @@ type ThesaurusReader interface {
ThesaurusKeysPrefix(name string, termPrefix []byte) (ThesaurusKeys, error)
}
+// EligibleDocumentIterator provides an interface to iterate over eligible document IDs.
+type EligibleDocumentIterator interface {
+ // Next returns the next document ID and whether it exists.
+ // When ok is false, iteration is complete.
+ Next() (id uint64, ok bool)
+}
+
+// EligibleDocumentList represents a list of eligible document IDs for filtering.
+type EligibleDocumentList interface {
+ // Iterator returns an iterator for the eligible document IDs.
+ Iterator() EligibleDocumentIterator
+ // Count returns the number of eligible document IDs.
+ Count() uint64
+}
+
// EligibleDocumentSelector filters documents based on specific eligibility criteria.
// It can be extended with additional methods for filtering and retrieval.
type EligibleDocumentSelector interface {
@@ -360,10 +411,9 @@ type EligibleDocumentSelector interface {
// id is the internal identifier of the document to be added.
AddEligibleDocumentMatch(id IndexInternalID) error
- // SegmentEligibleDocs returns a list of eligible document IDs within a given segment.
- // segmentID identifies the segment for which eligible documents are retrieved.
- // This must be called after all eligible documents have been added.
- SegmentEligibleDocs(segmentID int) []uint64
+ // SegmentEligibleDocuments returns an EligibleDocumentList for the specified segment.
+ // This must be called after all eligible documents have been added via AddEligibleDocumentMatch.
+ SegmentEligibleDocuments(segmentID int) EligibleDocumentList
}
// -----------------------------------------------------------------------------
@@ -391,3 +441,55 @@ type IndexInsightsReader interface {
// cluster densities (or cardinalities)
CentroidCardinalities(field string, limit int, descending bool) (cenCards []CentroidCardinality, err error)
}
+
+// -----------------------------------------------------------------------------
+// NestedReader is an extended index reader that supports hierarchical document structures.
+type NestedReader interface {
+ IndexReader
+ // Ancestors returns the ancestral chain for a given document ID in the index.
+ // For nested documents, this method retrieves all parent documents in the hierarchy
+ // leading up to the root document ID.
+ Ancestors(id IndexInternalID, prealloc []AncestorID) ([]AncestorID, error)
+}
+
+// AncestorID represents the identifier of an ancestor document in an ancestor chain.
+type AncestorID uint64
+
+// NewAncestorID creates a new AncestorID from the given uint64 value.
+func NewAncestorID(val uint64) AncestorID {
+ return AncestorID(val)
+}
+
+// Compare compares two AncestorID values.
+func (a AncestorID) Compare(b AncestorID) int {
+ switch {
+ case a < b:
+ return -1
+ case a > b:
+ return 1
+ default:
+ return 0
+ }
+}
+
+// Equals checks if two AncestorID values are equal.
+func (a AncestorID) Equals(b AncestorID) bool {
+ return a == b
+}
+
+// Add returns a new AncestorID by adding the given uint64 value to the current AncestorID.
+func (a AncestorID) Add(n uint64) AncestorID {
+ return AncestorID(uint64(a) + n)
+}
+
+// ToIndexInternalID converts the AncestorID to an IndexInternalID.
+func (a AncestorID) ToIndexInternalID(prealloc IndexInternalID) IndexInternalID {
+ return NewIndexInternalID(prealloc, uint64(a))
+}
+
+// Default no-op implementation. Is called before writing any user data to a file.
+var WriterHook func(context []byte) (string, func(data []byte) []byte, error)
+
+// Default no-op implementation. Is called after reading any user data from a file.
+var ReaderHook func(id string, context []byte) (
+ func(data []byte) ([]byte, error), error)
diff --git a/vendor/github.com/blevesearch/bleve_index_api/indexing_options.go b/vendor/github.com/blevesearch/bleve_index_api/indexing_options.go
index 4e92024b9b..c77dd5a559 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/indexing_options.go
+++ b/vendor/github.com/blevesearch/bleve_index_api/indexing_options.go
@@ -14,7 +14,7 @@
package index
-type FieldIndexingOptions int
+type FieldIndexingOptions uint64
const (
IndexField FieldIndexingOptions = 1 << iota
@@ -22,6 +22,9 @@ const (
IncludeTermVectors
DocValues
SkipFreqNorm
+ SkipDVCompression
+ SkipDVChunking
+ GPU
)
const (
@@ -33,6 +36,9 @@ const (
// for a query performed on a text field.
const DefaultScoringModel = TFIDFScoring
+// Sentinel value used to separate terms in doc values encoding
+const DocValueTermSeparator byte = 0xff
+
// Supported similarity models
var SupportedScoringModels = map[string]struct{}{
BM25Scoring: {},
@@ -59,6 +65,18 @@ func (o FieldIndexingOptions) SkipFreqNorm() bool {
return o&SkipFreqNorm != 0
}
+func (o FieldIndexingOptions) SkipDVCompression() bool {
+ return o&SkipDVCompression != 0
+}
+
+func (o FieldIndexingOptions) SkipDVChunking() bool {
+ return o&SkipDVChunking != 0
+}
+
+func (o FieldIndexingOptions) UseGPU() bool {
+ return o&GPU != 0
+}
+
func (o FieldIndexingOptions) String() string {
rv := ""
if o.IsIndexed() {
@@ -88,5 +106,23 @@ func (o FieldIndexingOptions) String() string {
}
rv += "FN"
}
+ if !o.SkipDVCompression() {
+ if rv != "" {
+ rv += ", "
+ }
+ rv += "DV_COMPRESSION"
+ }
+ if !o.SkipDVChunking() {
+ if rv != "" {
+ rv += ", "
+ }
+ rv += "DV_CHUNKING"
+ }
+ if o.UseGPU() {
+ if rv != "" {
+ rv += ", "
+ }
+ rv += "GPU"
+ }
return rv
}
diff --git a/vendor/github.com/blevesearch/bleve_index_api/vector.go b/vendor/github.com/blevesearch/bleve_index_api/vector.go
index 1057cf980a..a19f463032 100644
--- a/vendor/github.com/blevesearch/bleve_index_api/vector.go
+++ b/vendor/github.com/blevesearch/bleve_index_api/vector.go
@@ -18,6 +18,9 @@
package index
type VectorField interface {
+ // Name of the field
+ Name() string
+ // The vector data
Vector() []float32
// Dimensionality of the vector
Dims() int
@@ -25,6 +28,8 @@ type VectorField interface {
Similarity() string
// nlist/nprobe config (recall/latency) the index is optimized for
IndexOptimizedFor() string
+ // Field indexing options
+ Options() FieldIndexingOptions
}
// -----------------------------------------------------------------------------
@@ -49,9 +54,12 @@ var SupportedVectorSimilarityMetrics = map[string]struct{}{
// -----------------------------------------------------------------------------
const (
- IndexOptimizedForRecall = "recall"
- IndexOptimizedForLatency = "latency"
- IndexOptimizedForMemoryEfficient = "memory-efficient"
+ IndexOptimizedForRecall = "recall" // Flat or IVF,SQ8 indexes
+ IndexOptimizedForLatency = "latency" // Flat or IVF,SQ8 indexes; nprobe halved
+ IndexOptimizedForMemoryEfficient = "memory-efficient" // Flat or IVF,SQ4 indexes
+ IndexBIVFWithBackingFlat = "bivf-flat" // BFlat or BIVF with Flat backing index
+ IndexBIVFWithBackingSQ8 = "bivf-sq8" // BFlat or BIVF with SQ8 backing index
+ IndexIVFRaBitQ = "ivf,rabitq" // Flat or IVF,RaBitQ indexes
)
const DefaultIndexOptimization = IndexOptimizedForRecall
@@ -60,6 +68,9 @@ var SupportedVectorIndexOptimizations = map[string]int{
IndexOptimizedForRecall: 0,
IndexOptimizedForLatency: 1,
IndexOptimizedForMemoryEfficient: 2,
+ IndexBIVFWithBackingFlat: 3,
+ IndexBIVFWithBackingSQ8: 4,
+ IndexIVFRaBitQ: 5,
}
// Reverse maps vector index optimizations': int -> string
@@ -67,4 +78,23 @@ var VectorIndexOptimizationsReverseLookup = map[int]string{
0: IndexOptimizedForRecall,
1: IndexOptimizedForLatency,
2: IndexOptimizedForMemoryEfficient,
+ 3: IndexBIVFWithBackingFlat,
+ 4: IndexBIVFWithBackingSQ8,
+ 5: IndexIVFRaBitQ,
}
+
+func OptimizationRequiresBinaryIndex(optimization string) bool {
+ switch optimization {
+ case IndexBIVFWithBackingFlat, IndexBIVFWithBackingSQ8:
+ return true
+ default:
+ return false
+ }
+}
+
+const TrainedIndexFileName = "trained_index"
+const TrainingKey = "_training"
+
+const TrainedIndexCallback = "_trained_index_callback"
+
+type TrainedIndexCallbackFn func(string) (interface{}, error)
diff --git a/vendor/github.com/blevesearch/geo/geojson/geojson_s2_util.go b/vendor/github.com/blevesearch/geo/geojson/geojson_s2_util.go
index 91ad975e30..8afbbffba4 100644
--- a/vendor/github.com/blevesearch/geo/geojson/geojson_s2_util.go
+++ b/vendor/github.com/blevesearch/geo/geojson/geojson_s2_util.go
@@ -136,44 +136,53 @@ func geometryCollectionIntersectsShape(gc *GeometryCollection,
func polygonsContainsLineStrings(s2pgns []*s2.Polygon,
pls []*s2.Polyline) bool {
- linesWithIn := make(map[int]struct{})
checker := s2.NewCrossingEdgeQuery(s2.NewShapeIndex())
-nextLine:
- for lineIndex, pl := range pls {
+
+ // Every line segment in every linestring must be
+ // fully contained in atleast one of the polygons
+ for _, pl := range pls {
for i := 0; i < len(*pl)-1; i++ {
start := (*pl)[i]
end := (*pl)[i+1]
+ contains := false
for _, s2pgn := range s2pgns {
containsStart := s2pgn.ContainsPoint(start)
containsEnd := s2pgn.ContainsPoint(end)
+ // check if both end points are contained and if so,
+ // check if the line segment between them crosses the boundary of the polygon
if containsStart && containsEnd {
crossings := checker.Crossings(start, end, s2pgn, s2.CrossingTypeInterior)
if len(crossings) > 0 {
- continue nextLine
+ continue
}
- linesWithIn[lineIndex] = struct{}{}
- continue nextLine
+ contains = true
+ break
} else {
+ // else we check if the line segment is an edge of the polygon
for _, loop := range s2pgn.Loops() {
for i := 0; i < loop.NumVertices(); i++ {
if !containsStart && start.ApproxEqual(loop.Vertex(i)) {
containsStart = true
- } else if !containsEnd && end.ApproxEqual(loop.Vertex(i)) {
+ }
+ if !containsEnd && end.ApproxEqual(loop.Vertex(i)) {
containsEnd = true
}
if containsStart && containsEnd {
- linesWithIn[lineIndex] = struct{}{}
- continue nextLine
+ contains = true
+ break
}
}
}
}
}
+ if !contains {
+ return false
+ }
}
}
- return len(pls) == len(linesWithIn)
+ return true
}
func rectangleIntersectsWithPolygons(s2rect *s2.Rect,
diff --git a/vendor/github.com/blevesearch/geo/geojson/geojson_shapes_impl.go b/vendor/github.com/blevesearch/geo/geojson/geojson_shapes_impl.go
index 7d8e096738..5f80030473 100644
--- a/vendor/github.com/blevesearch/geo/geojson/geojson_shapes_impl.go
+++ b/vendor/github.com/blevesearch/geo/geojson/geojson_shapes_impl.go
@@ -1717,11 +1717,14 @@ func checkEnvelopeIntersectsShape(s2rect *s2.Rect, shapeIn,
// check if the other shape is a circle.
if c, ok := other.(*Circle); ok {
- s2pgn := s2PolygonFromS2Rectangle(s2rect)
- cp := c.s2cap.Center()
- projected := s2pgn.Project(&cp)
- distance := projected.Distance(cp)
- return distance <= c.s2cap.Radius(), nil
+ // check if the distance of the center of the circle from the
+ // rectangle is less than the radius of the circle.
+ if s2rect.DistanceToLatLng(s2.LatLngFromPoint(c.s2cap.Center())) <=
+ c.s2cap.Radius() {
+ return true, nil
+ }
+
+ return false, nil
}
// check if the other shape is a envelope.
diff --git a/vendor/github.com/blevesearch/go-faiss/faiss.go b/vendor/github.com/blevesearch/go-faiss/faiss.go
index a7087e7459..f151b8576b 100644
--- a/vendor/github.com/blevesearch/go-faiss/faiss.go
+++ b/vendor/github.com/blevesearch/go-faiss/faiss.go
@@ -12,7 +12,10 @@ package faiss
#include
*/
import "C"
-import "errors"
+import (
+ "errors"
+ "fmt"
+)
func getLastError() error {
return errors.New(C.GoString(C.faiss_get_last_error()))
@@ -39,3 +42,11 @@ func NormalizeVector(vector []float32) []float32 {
return vector
}
+
+var (
+ errNotIVFIndex = fmt.Errorf("index is not of ivf type")
+ errMergeFromNotSupported = fmt.Errorf("merge api not supported")
+ errNotBIVFIndex = fmt.Errorf("index is not of bivf type")
+ errFailedToSetQuantizers = fmt.Errorf("couldn't set the quantizers")
+ errSourceIndexNil = fmt.Errorf("source index is nil")
+)
diff --git a/vendor/github.com/blevesearch/go-faiss/gpu.go b/vendor/github.com/blevesearch/go-faiss/gpu.go
new file mode 100644
index 0000000000..b305f6b613
--- /dev/null
+++ b/vendor/github.com/blevesearch/go-faiss/gpu.go
@@ -0,0 +1,309 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build gpu
+
+package faiss
+
+/*
+#include
+#include
+#include
+#include
+#include
+*/
+import "C"
+import (
+ "errors"
+ "fmt"
+ "math/rand"
+ "sort"
+ "sync"
+ "sync/atomic"
+ "time"
+ "unsafe"
+)
+
+var (
+ errAccessingGPUDevices = errors.New("error accessing GPU devices")
+ errNilIndex = errors.New("index is nil")
+ errNoGPUDevices = errors.New("no GPU devices available")
+)
+
+// memorySpace controls where GPU index data is allocated.
+type memorySpace int
+
+const (
+ // memorySpaceDevice uses standard GPU memory (cudaMalloc).
+ memorySpaceDevice memorySpace = 1
+ // memorySpaceUnified uses CUDA managed memory (cudaMallocManaged),
+ // allowing the index to exceed GPU memory on Pascal+ (CC 6.0+) GPUs.
+ memorySpaceUnified memorySpace = 2
+)
+
+const (
+ // the minimum amount of free memory that must be available on a GPU to be considered for index cloning.
+ minGPUFreeMemory = 512 * 1024 * 1024 // 512 MiB
+ // the default memory space to use for GPU indices
+ defaultGPUMemoryMode = memorySpaceUnified
+)
+
+var (
+ gpuCount int
+ loadBalancer *gpuLoadBalancer
+)
+
+func init() {
+ var err error
+ gpuCount, err = numGPUs()
+ if err != nil || gpuCount <= 0 {
+ gpuCount = 0
+ }
+
+ // With exactly one GPU there is nothing to balance; getBestGPUDevice()
+ // returns device 0 directly when loadBalancer is nil.
+ // TODO: verify if 500 milliseconds is a good interval
+ if gpuCount > 1 {
+ loadBalancer = newGPULoadBalancer(500 * time.Millisecond)
+ go loadBalancer.monitor()
+ }
+}
+
+// numGPUs returns the number of available GPU devices.
+func numGPUs() (int, error) {
+ var rv C.int
+ c := C.faiss_get_num_gpus(&rv)
+ if c != 0 {
+ return 0, fmt.Errorf("error getting number of GPUs, err: %v", getLastError())
+ }
+ return int(rv), nil
+}
+
+// gpuLoadBalancer monitors GPU free memory on a fixed interval, keeps a
+// memory-sorted list of devices, and hands them out in round-robin order.
+// At each interval the list is re-sorted and the round-robin counter resets
+// to 0, so the next cycle always starts from the GPU with the most free memory.
+type gpuLoadBalancer struct {
+ mu sync.RWMutex
+ sortedDevices []int
+ idx atomic.Uint32
+ interval time.Duration
+ // scratch buffers reused across refresh calls; only accessed by the monitor goroutine
+ freeMemory []uint64
+ scratchDevs []int
+}
+
+func newGPULoadBalancer(interval time.Duration) *gpuLoadBalancer {
+ lb := &gpuLoadBalancer{
+ interval: interval,
+ freeMemory: make([]uint64, gpuCount),
+ scratchDevs: make([]int, 0, gpuCount),
+ sortedDevices: make([]int, 0, gpuCount),
+ }
+ return lb
+}
+
+func (lb *gpuLoadBalancer) monitor() {
+ ticker := time.NewTicker(lb.interval)
+ defer ticker.Stop()
+
+ // Perform an initial sort before any requests come in.
+ lb.refresh()
+
+ for range ticker.C {
+ lb.refresh()
+ }
+}
+
+// refresh queries every GPU for free memory, sorts the device list in descending
+// order of free memory, and resets the round-robin counter to 0.
+// If all queries fail the sorted list becomes empty, causing nextDevice to error.
+func (lb *gpuLoadBalancer) refresh() {
+ // Zero freeMemory before querying; failed queries leave their slot as 0,
+ // which naturally excludes those devices from selection.
+ clear(lb.freeMemory)
+ lb.scratchDevs = lb.scratchDevs[:0]
+
+ var wg sync.WaitGroup
+ wg.Add(gpuCount)
+ for i := 0; i < gpuCount; i++ {
+ go func(device int) {
+ defer wg.Done()
+ var freeBytes C.size_t
+ if C.faiss_gpu_free_memory(C.int(device), &freeBytes) == 0 {
+ lb.freeMemory[device] = uint64(freeBytes)
+ }
+ }(i)
+ }
+ wg.Wait()
+
+ // Only include devices that reported non-zero free memory, and have at least minGPUFreeMemory free.
+ for i, mem := range lb.freeMemory {
+ if mem > minGPUFreeMemory {
+ lb.scratchDevs = append(lb.scratchDevs, i)
+ }
+ }
+
+ // Shuffle first, then sort descending by free memory to make the
+ // sort as "unstable" as possible
+ // This is useful to add fairness between GPUs with the same memory
+ rand.Shuffle(len(lb.scratchDevs), func(i, j int) {
+ lb.scratchDevs[i], lb.scratchDevs[j] = lb.scratchDevs[j], lb.scratchDevs[i]
+ })
+ // Sort in a descending order by free memory so index 0 is the most appealing GPU.
+ sort.Slice(lb.scratchDevs, func(i, j int) bool {
+ return lb.freeMemory[lb.scratchDevs[i]] > lb.freeMemory[lb.scratchDevs[j]]
+ })
+
+ lb.mu.Lock()
+ old := lb.sortedDevices
+ lb.sortedDevices = lb.scratchDevs
+ lb.scratchDevs = old[:0]
+ lb.idx.Store(0)
+ lb.mu.Unlock()
+}
+
+// nextDevice returns the next GPU device in round-robin order.
+// Returns an error if no devices are currently available.
+func (lb *gpuLoadBalancer) nextDevice() (int, error) {
+ lb.mu.RLock()
+ defer lb.mu.RUnlock()
+
+ devices := lb.sortedDevices
+ n := len(devices)
+ if n == 0 {
+ return 0, errAccessingGPUDevices
+ }
+
+ // atomically allocates the GPU. Minus 1 for zero based index
+ idx := lb.idx.Add(1) - 1
+ return devices[int(idx%uint32(n))], nil
+}
+
+func getBestGPUDevice() (int, error) {
+ if gpuCount == 0 {
+ return 0, errNoGPUDevices
+ }
+ // With exactly one GPU there is nothing to balance; always use device 0.
+ if loadBalancer == nil {
+ return 0, nil
+ }
+ return loadBalancer.nextDevice()
+}
+
+// only expose API used by zapx
+type GPUIndexImpl struct {
+ idx *faissIndex
+ gpuResource *C.FaissStandardGpuResources
+}
+
+func (g *GPUIndexImpl) cPtr() *C.FaissIndex {
+ return g.idx.idx
+}
+
+func (g *GPUIndexImpl) Train(x []float32) error {
+ return g.idx.Train(x)
+}
+
+func (g *GPUIndexImpl) Add(x []float32) error {
+ return g.idx.Add(x)
+}
+
+func (g *GPUIndexImpl) Search(x []float32, k int64) ([]float32, []int64, error) {
+ return g.idx.Search(x, k)
+}
+
+func (g *GPUIndexImpl) Close() {
+ if g.idx != nil {
+ g.idx.Close()
+ g.idx = nil
+ }
+ if g.gpuResource != nil {
+ C.faiss_StandardGpuResources_free(g.gpuResource)
+ g.gpuResource = nil
+ }
+}
+
+// CloneToGPU transfers a CPU index to the best available GPU based on free memory.
+func CloneToGPU(cpuIndex *IndexImpl) (*GPUIndexImpl, error) {
+ if cpuIndex == nil {
+ return nil, errNilIndex
+ }
+
+ // Use the load balancer to select the best GPU device
+ device, err := getBestGPUDevice()
+ if err != nil {
+ return nil, err
+ }
+
+ var gpuResource *C.FaissStandardGpuResources
+ if code := C.faiss_StandardGpuResources_new(&gpuResource); code != 0 {
+ return nil, fmt.Errorf("failed to initialize GPU resources: error code %d, err: %v", code, getLastError())
+ }
+
+ // Disable the pre-allocated temp memory pool so that all GPU memory is
+ // available for index data; unified memory mode handles intermediate
+ // allocations via cudaMalloc/cudaFree on demand.
+ if code := C.faiss_StandardGpuResources_noTempMemory(gpuResource); code != 0 {
+ C.faiss_StandardGpuResources_free(gpuResource)
+ return nil, fmt.Errorf("failed to disable GPU temp memory: error code %d, err: %v", code, getLastError())
+ }
+
+ var clonerOpts *C.FaissGpuClonerOptions
+ if code := C.faiss_GpuClonerOptions_new(&clonerOpts); code != 0 {
+ C.faiss_StandardGpuResources_free(gpuResource)
+ return nil, fmt.Errorf("failed to create cloner options: error code %d, err: %v", code, getLastError())
+ }
+ defer C.faiss_GpuClonerOptions_free(clonerOpts)
+
+ C.faiss_GpuClonerOptions_set_memorySpace(clonerOpts, C.int(defaultGPUMemoryMode))
+
+ var gpuIdx *C.FaissGpuIndex
+ code := C.faiss_index_cpu_to_gpu_with_options(
+ gpuResource,
+ C.int(device),
+ cpuIndex.cPtr(),
+ clonerOpts,
+ &gpuIdx,
+ )
+ if code != 0 {
+ C.faiss_StandardGpuResources_free(gpuResource)
+ return nil, fmt.Errorf("failed to transfer index to GPU device %d: error code %d, err: %v", device, code, getLastError())
+ }
+
+ idx := &faissIndex{
+ idx: (*C.FaissIndex)(unsafe.Pointer(gpuIdx)),
+ }
+
+ return &GPUIndexImpl{
+ idx: idx,
+ gpuResource: gpuResource,
+ }, nil
+}
+
+func CloneToCPU(gpuIndex *GPUIndexImpl) (*IndexImpl, error) {
+ if gpuIndex == nil {
+ return nil, errNilIndex
+ }
+
+ var cpuIdx *C.FaissIndex
+ code := C.faiss_index_gpu_to_cpu(
+ gpuIndex.cPtr(),
+ &cpuIdx,
+ )
+ if code != 0 {
+ return nil, fmt.Errorf("failed to transfer index to CPU: %v", getLastError())
+ }
+ return &IndexImpl{&faissIndex{idx: cpuIdx}}, nil
+}
diff --git a/vendor/github.com/blevesearch/go-faiss/gpu_stub.go b/vendor/github.com/blevesearch/go-faiss/gpu_stub.go
new file mode 100644
index 0000000000..427f8b65f4
--- /dev/null
+++ b/vendor/github.com/blevesearch/go-faiss/gpu_stub.go
@@ -0,0 +1,41 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !gpu
+
+package faiss
+
+import "errors"
+
+// GPUIndexImpl is an opaque type when not built with GPU support.
+type GPUIndexImpl struct{}
+
+func (g *GPUIndexImpl) Train(x []float32) error { return errGPUNotBuilt }
+func (g *GPUIndexImpl) Add(x []float32) error { return errGPUNotBuilt }
+func (g *GPUIndexImpl) Search(x []float32, k int64) ([]float32, []int64, error) {
+ return nil, nil, errGPUNotBuilt
+}
+func (g *GPUIndexImpl) Close() {}
+
+var errGPUNotBuilt = errors.New("not built with GPU support (requires -tags gpu)")
+
+// CloneToGPU is not available without the gpu build tag.
+func CloneToGPU(_ *IndexImpl) (*GPUIndexImpl, error) {
+ return nil, errGPUNotBuilt
+}
+
+// CloneToCPU is not available without the gpu build tag.
+func CloneToCPU(_ *GPUIndexImpl) (*IndexImpl, error) {
+ return nil, errGPUNotBuilt
+}
diff --git a/vendor/github.com/blevesearch/go-faiss/index.go b/vendor/github.com/blevesearch/go-faiss/index.go
index 3a399e5b63..e716a1f881 100644
--- a/vendor/github.com/blevesearch/go-faiss/index.go
+++ b/vendor/github.com/blevesearch/go-faiss/index.go
@@ -33,6 +33,15 @@ type Index interface {
// Ntotal returns the number of indexed vectors.
Ntotal() int64
+ // set the direct map type for IVF indexes.
+ // 0 for No Map
+ // 1 for Array
+ // 2 for Hash
+ SetDirectMap(maptype int) error
+
+ // set the number of probes for IVF indexes
+ SetNProbe(nprobe int32)
+
// MetricType returns the metric type of the index.
MetricType() int
@@ -48,42 +57,45 @@ type Index interface {
// Returns true if the index is an IVF index.
IsIVFIndex() bool
- // Applicable only to IVF indexes: Returns a map where the keys
- // are cluster IDs and the values represent the count of input vectors that belong
- // to each cluster.
- // This method only considers the given vecIDs and does not account for all
- // vectors in the index.
- // Example:
- // If vecIDs = [1, 2, 3, 4, 5], and:
- // - Vectors 1 and 2 belong to cluster 1
- // - Vectors 3, 4, and 5 belong to cluster 2
- // The output will be: map[1:2, 2:3]
- ObtainClusterVectorCountsFromIVFIndex(vecIDs []int64) (map[int64]int64, error)
+ // Returns true if the index is a scalar quantization (SQ) index.
+ IsSQIndex() bool
- // Applicable only to IVF indexes: Returns the centroid IDs in decreasing order
- // of proximity to query 'x' and their distance from 'x'
- ObtainClustersWithDistancesFromIVFIndex(x []float32, centroidIDs []int64) (
+ // Returns true if the index has RaBitQ
+ HasRaBitQ() bool
+
+ // Returns the IVF parameters nprobe and nlist for IVF indexes.
+ IVFParams() (nprobe, nlist int)
+
+ // Applicable only to IVF indexes: Returns a slice where each index represents
+ // a cluster (list) ID and the value is the count of selected vectors belonging
+ // to that cluster. Only vectors specified by the given Selector are considered.
+ ObtainClusterVectorCountsFromIVFIndex(include Selector, nlist int) ([]int64, error)
+
+ // Applicable only to IVF indexes: Returns the centroid IDs in the selector in
+ // decreasing order of proximity to query 'x' and their distance from 'x'
+ ObtainClustersWithDistancesFromIVFIndex(x []float32, centroids Selector, numCentroids int64) (
[]int64, []float32, error)
// Applicable only to IVF indexes: Returns the top k centroid cardinalities and
// their vectors in chosen order (descending or ascending)
ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) ([]uint64, [][]float32, error)
+ // fetch centroid count
+ Nlist() int
+
// Search queries the index with the vectors in x.
// Returns the IDs of the k nearest neighbors for each query vector and the
// corresponding distances.
Search(x []float32, k int64) (distances []float32, labels []int64, err error)
- SearchWithoutIDs(x []float32, k int64, exclude []int64, params json.RawMessage) (distances []float32,
- labels []int64, err error)
-
- SearchWithIDs(x []float32, k int64, include []int64, params json.RawMessage) (distances []float32,
- labels []int64, err error)
+ // SearchWithOptions performs a search with additional optional constraints.
+ // - Selector can be used to restrict the search to a subset of the indexed vectors based on their IDs.
+ // - params is a JSON object that can contain additional search parameters specific to the index type, such as IVF search parameters.
+ SearchWithOptions(x []float32, k int64, sel Selector, params json.RawMessage) (distances []float32, labels []int64, err error)
// Applicable only to IVF indexes: Search clusters whose IDs are in eligibleCentroidIDs
- SearchClustersFromIVFIndex(selector Selector, eligibleCentroidIDs []int64,
- minEligibleCentroids int, k int64, x, centroidDis []float32,
- params json.RawMessage) ([]float32, []int64, error)
+ SearchClustersFromIVFIndex(eligibleCentroidIDs []int64, centroidDis []float32, centroidsToProbe int,
+ x []float32, k int64, include Selector, params json.RawMessage) ([]float32, []int64, error)
Reconstruct(key int64) ([]float32, error)
@@ -95,6 +107,9 @@ type Index interface {
// Returns all vectors with distance < radius.
RangeSearch(x []float32, radius float32) (*RangeSearchResult, error)
+ // DistCompute computes the distance between the query vector and the vectors specified by ids.
+ DistCompute(x []float32, labels []int64) ([]float32, error)
+
// Reset removes all vectors from the index.
Reset() error
@@ -109,6 +124,10 @@ type Index interface {
Size() uint64
cPtr() *C.FaissIndex
+
+ // set the quantizers from a source index into this index, applicable only
+ // for IVF indexes
+ SetQuantizers(source Index) error
}
type faissIndex struct {
@@ -156,24 +175,34 @@ func (idx *faissIndex) Add(x []float32) error {
return nil
}
-func (idx *faissIndex) ObtainClusterVectorCountsFromIVFIndex(vecIDs []int64) (map[int64]int64, error) {
- if !idx.IsIVFIndex() {
- return nil, fmt.Errorf("index is not an IVF index")
+func (idx *faissIndex) ObtainClusterVectorCountsFromIVFIndex(includedVectors Selector, nlist int) ([]int64, error) {
+ // Applicable only to IVF indexes
+ ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
+ if ivfPtr == nil {
+ return nil, errNotIVFIndex
}
- clusterIDs := make([]int64, len(vecIDs))
- if c := C.faiss_get_lists_for_keys(
- idx.idx,
- (*C.idx_t)(unsafe.Pointer(&vecIDs[0])),
- (C.size_t)(len(vecIDs)),
- (*C.idx_t)(unsafe.Pointer(&clusterIDs[0])),
+ // Creating a slice to hold the count of vectors per cluster
+ // Since we have nlist clusters, we create a slice of size nlist
+ // listCount[i] will hold the count of vectors in cluster i
+ listCount := make([]int64, nlist)
+ // Creating a FAISS selector based on the include bitmap.
+ params, err := NewStandardSearchParams(includedVectors)
+ if err != nil {
+ return nil, err
+ }
+ defer params.Delete()
+ // Calling the C function to populate listCount
+ // with the count of vectors per cluster, considering only
+ // the vectors specified in the include selector.
+ if c := C.faiss_IndexIVF_list_vector_count(
+ ivfPtr,
+ (*C.idx_t)(unsafe.Pointer(&listCount[0])),
+ C.size_t(nlist),
+ params.sp,
); c != 0 {
return nil, getLastError()
}
- rv := make(map[int64]int64, len(vecIDs))
- for _, v := range clusterIDs {
- rv[v]++
- }
- return rv, nil
+ return listCount, nil
}
func (idx *faissIndex) IsIVFIndex() bool {
@@ -183,36 +212,38 @@ func (idx *faissIndex) IsIVFIndex() bool {
return true
}
-func (idx *faissIndex) ObtainClustersWithDistancesFromIVFIndex(x []float32, centroidIDs []int64) (
- []int64, []float32, error) {
- // Selector to include only the centroids whose IDs are part of 'centroidIDs'.
- includeSelector, err := NewIDSelectorBatch(centroidIDs)
- if err != nil {
- return nil, nil, err
- }
- defer includeSelector.Delete()
+func (idx *faissIndex) HasRaBitQ() bool {
+ return C.faiss_IndexIVF_has_RaBitQ(idx.idx) == 0
+}
- params, err := NewSearchParams(idx, json.RawMessage{}, includeSelector.Get(), nil)
+func (idx *faissIndex) ObtainClustersWithDistancesFromIVFIndex(x []float32, includedCentroids Selector, numCentroids int64) (
+ []int64, []float32, error) {
+ // Applicable only to IVF indexes
+ ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
+ if ivfPtr == nil {
+ return nil, nil, errNotIVFIndex
+ }
+ params, err := NewStandardSearchParams(includedCentroids)
if err != nil {
return nil, nil, err
}
defer params.Delete()
// Populate these with the centroids and their distances.
- centroids := make([]int64, len(centroidIDs))
- centroidDistances := make([]float32, len(centroidIDs))
+ centroids := make([]int64, numCentroids)
+ centroidDistances := make([]float32, numCentroids)
n := len(x) / idx.D()
- c := C.faiss_Search_closest_eligible_centroids(
- idx.idx,
+ if c := C.faiss_IndexIVF_search_closest_eligible_centroids(
+ ivfPtr,
(C.idx_t)(n),
(*C.float)(&x[0]),
- (C.idx_t)(len(centroidIDs)),
+ (C.idx_t)(numCentroids),
(*C.float)(¢roidDistances[0]),
(*C.idx_t)(¢roids[0]),
- params.sp)
- if c != 0 {
+ params.sp,
+ ); c != 0 {
return nil, nil, getLastError()
}
@@ -284,19 +315,38 @@ func getIndicesOfKCentroidCardinalities(cardinalities []C.size_t, k int, descend
return indices[:k]
}
+func (idx *faissIndex) Nlist() int {
+ ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
+ if ivfPtr == nil {
+ return 0
+ }
+ return int(C.faiss_IndexIVF_nlist(idx.idx))
+}
-func (idx *faissIndex) SearchClustersFromIVFIndex(selector Selector,
- eligibleCentroidIDs []int64, minEligibleCentroids int, k int64, x,
- centroidDis []float32, params json.RawMessage) ([]float32, []int64, error) {
-
+func (idx *faissIndex) SearchClustersFromIVFIndex(eligibleCentroidIDs []int64, centroidDis []float32, centroidsToProbe int,
+ x []float32, k int64, include Selector, params json.RawMessage) ([]float32, []int64, error) {
+ // Applicable only to IVF indexes
+ ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
+ if ivfPtr == nil {
+ return nil, nil, errNotIVFIndex
+ }
+ // If no include selector is provided, we have no results to return.
+ // return an error indicating that the SearchClustersFromIVFIndex requires a valid selector.
+ if include == nil {
+ return nil, nil, fmt.Errorf("SearchClustersFromIVFIndex requires a valid include selector")
+ }
+ // create a temporary search params object to set nprobe, this will override
+ // the nprobe and the nlist set at index time, this will allow the search to
+ // probe only the clusters specified in eligibleCentroidIDs
tempParams := &defaultSearchParamsIVF{
+ // Nlist is set to the number of eligible centroids, which will override
+ // the nlist set at index time.
Nlist: len(eligibleCentroidIDs),
// Have to override nprobe so that more clusters will be searched for this
// query, if required.
- Nprobe: minEligibleCentroids,
+ Nprobe: centroidsToProbe,
}
-
- searchParams, err := NewSearchParams(idx, params, selector.Get(), tempParams)
+ searchParams, err := NewSearchParams(idx, params, include, tempParams)
if err != nil {
return nil, nil, err
}
@@ -306,13 +356,17 @@ func (idx *faissIndex) SearchClustersFromIVFIndex(selector Selector,
distances := make([]float32, int64(n)*k)
labels := make([]int64, int64(n)*k)
-
- effectiveNprobe := getNProbeFromSearchParams(searchParams)
+ // Adjust the slices to match the effective nprobe set in searchParams, as the input
+ // parameters may have different nprobe value, which will be a hard override, over the
+ // centroidsToProbe value passed to this function.
+ // If the effective nprobe is greater than the length of eligibleCentroidIDs,
+ // we limit it to the length of eligibleCentroidIDs.
+ effectiveNprobe := min(getNProbeFromSearchParams(searchParams), int32(len(eligibleCentroidIDs)))
eligibleCentroidIDs = eligibleCentroidIDs[:effectiveNprobe]
centroidDis = centroidDis[:effectiveNprobe]
if c := C.faiss_IndexIVF_search_preassigned_with_params(
- idx.idx,
+ ivfPtr,
(C.idx_t)(n),
(*C.float)(&x[0]),
(C.idx_t)(k),
@@ -321,7 +375,8 @@ func (idx *faissIndex) SearchClustersFromIVFIndex(selector Selector,
(*C.float)(&distances[0]),
(*C.idx_t)(&labels[0]),
(C.int)(0),
- searchParams.sp); c != 0 {
+ searchParams.sp,
+ ); c != 0 {
return nil, nil, getLastError()
}
@@ -341,6 +396,9 @@ func (idx *faissIndex) AddWithIDs(x []float32, xids []int64) error {
return nil
}
+// Always use SearchWithOptions for indexes involving RaBitQ, as
+// simple Search is highly unoptimized for RaBitQ indexes and
+// will not leverage the quantizer for search.
func (idx *faissIndex) Search(x []float32, k int64) (
distances []float32, labels []int64, err error,
) {
@@ -361,51 +419,11 @@ func (idx *faissIndex) Search(x []float32, k int64) (
return
}
-func (idx *faissIndex) SearchWithoutIDs(x []float32, k int64, exclude []int64, params json.RawMessage) (
- distances []float32, labels []int64, err error,
-) {
- if params == nil && len(exclude) == 0 {
+func (idx *faissIndex) SearchWithOptions(x []float32, k int64, sel Selector, params json.RawMessage) ([]float32, []int64, error) {
+ if sel == nil && params == nil && !idx.HasRaBitQ() {
return idx.Search(x, k)
}
-
- var selector *C.FaissIDSelector
- if len(exclude) > 0 {
- excludeSelector, err := NewIDSelectorNot(exclude)
- if err != nil {
- return nil, nil, err
- }
- selector = excludeSelector.Get()
- defer excludeSelector.Delete()
- }
-
- searchParams, err := NewSearchParams(idx, params, selector, nil)
- if err != nil {
- return nil, nil, err
- }
- defer searchParams.Delete()
-
- distances, labels, err = idx.searchWithParams(x, k, searchParams.sp)
-
- return
-}
-
-func (idx *faissIndex) SearchWithIDs(x []float32, k int64, include []int64,
- params json.RawMessage) (distances []float32, labels []int64, err error,
-) {
- includeSelector, err := NewIDSelectorBatch(include)
- if err != nil {
- return nil, nil, err
- }
- defer includeSelector.Delete()
-
- searchParams, err := NewSearchParams(idx, params, includeSelector.Get(), nil)
- if err != nil {
- return nil, nil, err
- }
- defer searchParams.Delete()
-
- distances, labels, err = idx.searchWithParams(x, k, searchParams.sp)
- return
+ return idx.searchWithOptions(x, k, sel, params)
}
func (idx *faissIndex) Reconstruct(key int64) (recons []float32, err error) {
@@ -436,22 +454,17 @@ func (idx *faissIndex) ReconstructBatch(keys []int64, recons []float32) ([]float
return recons, err
}
-func (i *IndexImpl) MergeFrom(other Index, add_id int64) error {
- if impl, ok := other.(*IndexImpl); ok {
- return i.Index.MergeFrom(impl.Index, add_id)
- }
- return fmt.Errorf("merge not support")
-}
-
func (idx *faissIndex) MergeFrom(other Index, add_id int64) (err error) {
- otherIdx, ok := other.(*faissIndex)
- if !ok {
- return fmt.Errorf("merge api not supported")
+ // currrently we support the mergeFrom API only for IVF and SQ indexes
+ // todo: support on Flat index as well
+ if !(idx.IsIVFIndex() && other.IsIVFIndex()) &&
+ !(idx.IsSQIndex() && other.IsSQIndex()) {
+ return fmt.Errorf("faissIndex MergeFrom err: %w", errMergeFromNotSupported)
}
if c := C.faiss_Index_merge_from(
- idx.idx,
- otherIdx.idx,
+ idx.cPtr(),
+ other.cPtr(),
(C.idx_t)(add_id),
); c != 0 {
err = getLastError()
@@ -480,6 +493,16 @@ func (idx *faissIndex) RangeSearch(x []float32, radius float32) (
return &RangeSearchResult{rsr}, nil
}
+func (idx *faissIndex) DistCompute(queryData []float32, ids []int64) ([]float32, error) {
+ distances := make([]float32, len(ids))
+ if c := C.faiss_Index_dist_compute(idx.idx, (*C.float)(&queryData[0]),
+ (*C.idx_t)(&ids[0]), (C.size_t)(len(ids)), (*C.float)(&distances[0])); c != 0 {
+ return nil, getLastError()
+ }
+
+ return distances, nil
+}
+
func (idx *faissIndex) Reset() error {
if c := C.faiss_Index_reset(idx.idx); c != 0 {
return getLastError()
@@ -499,26 +522,30 @@ func (idx *faissIndex) Close() {
C.faiss_Index_free(idx.idx)
}
-func (idx *faissIndex) searchWithParams(x []float32, k int64, searchParams *C.FaissSearchParameters) (
- distances []float32, labels []int64, err error,
-) {
+func (idx *faissIndex) searchWithOptions(x []float32, k int64, sel Selector, params json.RawMessage) ([]float32, []int64, error) {
+ // Build a search params object to contain either the selector, the additional params, or both.
+ searchParams, err := NewSearchParams(idx, params, sel, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer searchParams.Delete()
+
n := len(x) / idx.D()
- distances = make([]float32, int64(n)*k)
- labels = make([]int64, int64(n)*k)
+ distances := make([]float32, int64(n)*k)
+ labels := make([]int64, int64(n)*k)
if c := C.faiss_Index_search_with_params(
idx.idx,
C.idx_t(n),
(*C.float)(&x[0]),
C.idx_t(k),
- searchParams,
+ searchParams.sp,
(*C.float)(&distances[0]),
(*C.idx_t)(&labels[0]),
); c != 0 {
- err = getLastError()
+ return nil, nil, getLastError()
}
-
- return
+ return distances, labels, nil
}
// -----------------------------------------------------------------------------
diff --git a/vendor/github.com/blevesearch/go-faiss/index_binary.go b/vendor/github.com/blevesearch/go-faiss/index_binary.go
new file mode 100644
index 0000000000..83f6dfe53a
--- /dev/null
+++ b/vendor/github.com/blevesearch/go-faiss/index_binary.go
@@ -0,0 +1,465 @@
+package faiss
+
+/*
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+*/
+import "C"
+import (
+ "encoding/json"
+ "fmt"
+ "unsafe"
+)
+
+type BinaryIndex interface {
+ // D returns the dimension of the indexed vectors.
+ D() int
+
+ // MetricType returns the metric type of the index.
+ MetricType() int
+
+ // Ntotal returns the total number of vectors currently stored in the index.
+ Ntotal() int64
+
+ // set the direct map type for IVF indexes.
+ // 0 for No Map
+ // 1 for Array
+ // 2 for Hash
+ SetDirectMap(maptype int) error
+
+ // set the number of probes for IVF indexes
+ SetNProbe(nprobe int32)
+
+ // returns true if the underlying index is an IVF index
+ IsIVFIndex() bool
+
+ // IVFParams returns the nlist and nprobe parameters for IVF indexes
+ IVFParams() (nprobe int, nlist int)
+
+ // trains the index on a representative set of vectors
+ Train(xb []uint8) error
+
+ // adds vectors to the index
+ Add(xb []uint8) error
+
+ // sets the qunatizers from the source index, supposed to be used only for
+ // BIVF indexes and returns error otherwise
+ SetQuantizers(srcIndex BinaryIndex) error
+
+ // merges another binary index into this one, currently applicable only for
+ // IVF indexes returns an error
+ MergeFrom(other BinaryIndex, add_id int64) error
+
+ // queries the index with the vectors in xb
+ // returns the IDs of the k nearest neighbors for each query vector and
+ // their corresponding distances
+ Search(xb []uint8, k int64) (distances []int32, labels []int64, err error)
+
+ // SearchWithOptions performs a search with additional optional constraints.
+ // - Selector can be used to restrict the search to a subset of the indexed vectors based on their IDs.
+ // - params is a JSON object that can contain additional search parameters specific to the index type, such as IVF search parameters.
+ SearchWithOptions(xb []uint8, k int64, sel Selector, params json.RawMessage) (distances []int32, labels []int64, err error)
+
+ // returns a slice where each index corresponds to a cluster in an IVF
+ // index, and the value at each index is the count of vectors in that
+ // cluster, considering only the vectors specified in the include selector.
+ ObtainClusterVectorCountsFromIVFIndex(include Selector, nlist int) (
+ []int64, error)
+
+ // returns the IDs and distances of the closest numCentroids centroids to
+ // the query vector xb, considering only the centroids specified in the
+ // includedCentroids selector.
+ ObtainClustersWithDistancesFromIVFIndex(xb []uint8, includedCentroids Selector,
+ numCentroids int64) ([]int64, []int32, error)
+
+ // Applicable only to IVF indexes: Returns the top k centroid cardinalities and
+ // their vectors in chosen order (descending or ascending)
+ ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) ([]uint64, [][]uint8, error)
+
+ // searches the specified clusters in an IVF index for the k nearest neighbors
+ // of the query vector xb, considering only the vectors specified in the include selector
+ // and additional search parameters passed as a JSON object.
+ SearchClustersFromIVFIndex(eligibleCentroidIDs []int64, centroidDis []int32,
+ centroidsToProbe int, xb []uint8, k int64, include Selector,
+ params json.RawMessage) ([]int32, []int64, error)
+
+ // returns the total size of the index in bytes
+ Size() uint64
+
+ // frees the memory associated with the index
+ Close()
+
+ bPtr() *C.FaissIndexBinary
+}
+
+type faissBinaryIndex struct {
+ bIdx *C.FaissIndexBinary
+}
+
+func (b *faissBinaryIndex) bPtr() *C.FaissIndexBinary {
+ return b.bIdx
+}
+
+func (b *faissBinaryIndex) D() int {
+ return int(C.faiss_IndexBinary_d(b.bIdx))
+}
+
+func (b *faissBinaryIndex) MetricType() int {
+ return int(C.faiss_IndexBinary_metric_type(b.bIdx))
+}
+
+func (b *faissBinaryIndex) Ntotal() int64 {
+ return int64(C.faiss_IndexBinary_ntotal(b.bIdx))
+}
+
+func (b *faissBinaryIndex) SetDirectMap(mapType int) (err error) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return errNotBIVFIndex
+ }
+ if c := C.faiss_IndexBinaryIVF_set_direct_map(
+ ivfPtrBinary,
+ C.int(mapType),
+ ); c != 0 {
+ err = getLastError()
+ }
+ return err
+}
+
+func (b *faissBinaryIndex) SetNProbe(nprobe int32) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return
+ }
+ C.faiss_IndexBinaryIVF_set_nprobe(ivfPtrBinary, C.size_t(nprobe))
+}
+
+func (b *faissBinaryIndex) IsIVFIndex() bool {
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ return ivfPtrBinary != nil
+}
+
+func (b *faissBinaryIndex) IVFParams() (nprobe int, nlist int) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return 0, 0
+ }
+ nlist = int(C.faiss_IndexBinaryIVF_nlist(ivfPtrBinary))
+ nprobe = int(C.faiss_IndexBinaryIVF_nprobe(ivfPtrBinary))
+ return nprobe, nlist
+}
+
+func (b *faissBinaryIndex) Train(x []uint8) error {
+ n := (len(x) * 8) / b.D()
+ if c := C.faiss_IndexBinary_train(b.bIdx, C.idx_t(n),
+ (*C.uint8_t)(&x[0])); c != 0 {
+ return getLastError()
+ }
+ return nil
+}
+
+func (b *faissBinaryIndex) Add(x []uint8) error {
+ n := (len(x) * 8) / b.D()
+ if c := C.faiss_IndexBinary_add(b.bIdx, C.idx_t(n),
+ (*C.uint8_t)(&x[0])); c != 0 {
+ return getLastError()
+ }
+ return nil
+}
+
+func (b *faissBinaryIndex) Search(xb []uint8, k int64) (
+ []int32, []int64, error) {
+ nq := (len(xb) * 8) / b.D()
+ distances := make([]int32, int64(nq)*k)
+ labels := make([]int64, int64(nq)*k)
+
+ if c := C.faiss_IndexBinary_search(
+ b.bIdx,
+ C.idx_t(nq),
+ (*C.uint8_t)(&xb[0]),
+ C.idx_t(k),
+ (*C.int32_t)(&distances[0]),
+ (*C.idx_t)(&labels[0]),
+ ); c != 0 {
+ return nil, nil, getLastError()
+ }
+ return distances, labels, nil
+}
+
+func (b *faissBinaryIndex) SearchWithOptions(xb []uint8, k int64, sel Selector, params json.RawMessage) ([]int32, []int64, error) {
+ if sel == nil && params == nil {
+ return b.Search(xb, k)
+ }
+ return b.searchWithOptions(xb, k, sel, params)
+}
+
+func (b *faissBinaryIndex) searchWithOptions(xb []uint8, k int64, selector Selector,
+ params json.RawMessage) ([]int32, []int64, error) {
+ // Build a binary search params object to contain either the selector, the additional params, or both.
+ searchParams, err := NewBinarySearchParams(b, params, selector, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer searchParams.Delete()
+
+ nq := (len(xb) * 8) / b.D()
+ distances := make([]int32, int64(nq)*k)
+ labels := make([]int64, int64(nq)*k)
+
+ if c := C.faiss_IndexBinary_search_with_params(
+ b.bIdx,
+ C.idx_t(nq),
+ (*C.uint8_t)(&xb[0]),
+ C.idx_t(k),
+ searchParams.sp,
+ (*C.int32_t)(&distances[0]),
+ (*C.idx_t)(&labels[0]),
+ ); c != 0 {
+ return nil, nil, getLastError()
+ }
+ return distances, labels, nil
+}
+
+func (b *faissBinaryIndex) ObtainClusterVectorCountsFromIVFIndex(includedVectors Selector, nlist int) ([]int64, error) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return nil, errNotBIVFIndex
+ }
+ // Creating a slice to hold the count of vectors per cluster
+ // Since we have nlist clusters, we create a slice of size nlist
+ // listCount[i] will hold the count of vectors in cluster i
+ listCount := make([]int64, nlist)
+ // Creating a FAISS selector based on the include bitmap.
+ params, err := NewStandardSearchParams(includedVectors)
+ if err != nil {
+ return nil, err
+ }
+ defer params.Delete()
+ // Calling the C function to populate listCount
+ // with the count of vectors per cluster, considering only
+ // the vectors specified in the include selector.
+ if c := C.faiss_IndexBinaryIVF_list_vector_count(
+ ivfPtrBinary,
+ (*C.idx_t)(unsafe.Pointer(&listCount[0])),
+ C.size_t(nlist),
+ params.sp,
+ ); c != 0 {
+ return nil, getLastError()
+ }
+ return listCount, nil
+}
+
+func (b *faissBinaryIndex) ObtainClustersWithDistancesFromIVFIndex(xb []uint8, includedCentroids Selector, numCentroids int64) ([]int64, []int32, error) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return nil, nil, errNotBIVFIndex
+ }
+ params, err := NewStandardSearchParams(includedCentroids)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer params.Delete()
+
+ // Populate these with the centroids and their distances.
+ centroids := make([]int64, numCentroids)
+ centroidDistances := make([]int32, numCentroids)
+
+ n := (len(xb) * 8) / b.D()
+
+ if c := C.faiss_IndexBinaryIVF_search_closest_eligible_centroids(
+ ivfPtrBinary,
+ (C.idx_t)(n),
+ (*C.uint8_t)(&xb[0]),
+ (C.idx_t)(numCentroids),
+ (*C.int32_t)(¢roidDistances[0]),
+ (*C.idx_t)(¢roids[0]),
+ params.sp,
+ ); c != 0 {
+ return nil, nil, getLastError()
+ }
+
+ return centroids, centroidDistances, nil
+}
+
+func (b *faissBinaryIndex) ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) (
+ []uint64, [][]uint8, error) {
+ if limit <= 0 {
+ return nil, nil, nil
+ }
+
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return nil, nil, errNotBIVFIndex
+ }
+
+ nlist := int(C.faiss_IndexBinaryIVF_nlist(ivfPtrBinary))
+ if nlist == 0 {
+ return nil, nil, nil
+ }
+
+ centroidCardinalities := make([]C.size_t, nlist)
+
+ // Allocate a flat buffer for all centroids, then slice it per centroid
+ d := b.D()
+ flatCentroids := make([]uint8, nlist*d/8)
+
+ // Call the C function to fill centroid vectors and cardinalities
+ c := C.faiss_IndexBinaryIVF_get_centroids_and_cardinality(
+ ivfPtrBinary,
+ (*C.uint8_t)(&flatCentroids[0]),
+ (*C.size_t)(¢roidCardinalities[0]),
+ nil,
+ )
+ if c != 0 {
+ return nil, nil, getLastError()
+ }
+
+ topIndices := getIndicesOfKCentroidCardinalities(
+ centroidCardinalities,
+ min(limit, nlist),
+ descending)
+
+ rvCardinalities := make([]uint64, len(topIndices))
+ rvCentroids := make([][]uint8, len(topIndices))
+
+ for i, idx := range topIndices {
+ rvCardinalities[i] = uint64(centroidCardinalities[idx])
+ rvCentroids[i] = flatCentroids[idx*d : (idx+1)*d]
+ }
+
+ return rvCardinalities, rvCentroids, nil
+
+}
+
+func (b *faissBinaryIndex) SearchClustersFromIVFIndex(eligibleCentroidIDs []int64, centroidDis []int32, centroidsToProbe int,
+ xb []uint8, k int64, include Selector, params json.RawMessage) ([]int32, []int64, error) {
+ // Applicable only to IVF indexes
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(b.bIdx)
+ if ivfPtrBinary == nil {
+ return nil, nil, errNotBIVFIndex
+ }
+ // If no include selector is provided, we have no results to return.
+ // return an error indicating that the SearchClustersFromIVFIndex requires a valid selector.
+ if include == nil {
+ return nil, nil, fmt.Errorf("SearchClustersFromIVFIndex requires a valid include selector")
+ }
+ // create a temporary search params object to set nprobe, this will override
+ // the nprobe and the nlist set at index time, this will allow the search to
+ // probe only the clusters specified in eligibleCentroidIDs
+ tempParams := &defaultSearchParamsIVF{
+ // Nlist is set to the number of eligible centroids, which will override
+ // the nlist set at index time.
+ Nlist: len(eligibleCentroidIDs),
+ // Have to override nprobe so that more clusters will be searched for this
+ // query, if required.
+ Nprobe: centroidsToProbe,
+ }
+ searchParams, err := NewBinarySearchParams(b, params, include, tempParams)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer searchParams.Delete()
+
+ n := (len(xb) * 8) / b.D()
+
+ distances := make([]int32, int64(n)*k)
+ labels := make([]int64, int64(n)*k)
+ // Adjust the slices to match the effective nprobe set in searchParams, as the input
+ // parameters may have different nprobe value, which will be a hard override, over the
+ // centroidsToProbe value passed to this function.
+ // If the effective nprobe is greater than the length of eligibleCentroidIDs,
+ // we limit it to the length of eligibleCentroidIDs.
+ effectiveNprobe := min(getNProbeFromSearchParams(searchParams), int32(len(eligibleCentroidIDs)))
+ eligibleCentroidIDs = eligibleCentroidIDs[:effectiveNprobe]
+ centroidDis = centroidDis[:effectiveNprobe]
+
+ if c := C.faiss_IndexBinaryIVF_search_preassigned_with_params(
+ ivfPtrBinary,
+ (C.idx_t)(n),
+ (*C.uint8_t)(&xb[0]),
+ (C.idx_t)(k),
+ (*C.idx_t)(&eligibleCentroidIDs[0]),
+ (*C.int32_t)(¢roidDis[0]),
+ (*C.int32_t)(&distances[0]),
+ (*C.idx_t)(&labels[0]),
+ (C.int)(0),
+ searchParams.sp,
+ ); c != 0 {
+ return nil, nil, getLastError()
+ }
+
+ return distances, labels, nil
+}
+
+func (b *faissBinaryIndex) Size() uint64 {
+ size := C.faiss_IndexBinary_size(b.bIdx)
+ return uint64(size)
+}
+
+func (idx *faissBinaryIndex) Close() {
+ C.faiss_IndexBinary_free(idx.bIdx)
+}
+
+type BinaryIndexImpl struct {
+ BinaryIndex
+}
+
+func BinaryIndexFactory(dims int, description string) (*BinaryIndexImpl, error) {
+ var cDescription *C.char
+ if description != "" {
+ cDescription = C.CString(description)
+ defer C.free(unsafe.Pointer(cDescription))
+ }
+ var idx faissBinaryIndex
+ if c := C.faiss_index_binary_factory(&idx.bIdx, C.int(dims), cDescription); c != 0 {
+ return nil, getLastError()
+ }
+
+ return &BinaryIndexImpl{&idx}, nil
+}
+
+func (idx *faissBinaryIndex) SetQuantizers(srcIndex BinaryIndex) error {
+ bivf := C.faiss_IndexBinaryIVF_cast(idx.bPtr())
+ if bivf == nil {
+ return errNotBIVFIndex
+ }
+
+ srcIndexPtr := srcIndex.bPtr()
+ if srcIndexPtr == nil {
+ return fmt.Errorf("coarse quantizer is not valid")
+ }
+
+ err := C.faiss_Set_quantizers_binary(idx.bIdx, srcIndexPtr)
+ if err != 0 {
+ return fmt.Errorf("faissBinaryIndex err: %w", errFailedToSetQuantizers)
+ }
+
+ return nil
+}
+
+func (idx *faissBinaryIndex) MergeFrom(other BinaryIndex, add_id int64) (err error) {
+ if !idx.IsIVFIndex() && !other.IsIVFIndex() {
+ return fmt.Errorf("faissBinaryIndex err: %w", errNotBIVFIndex)
+ }
+
+ if c := C.faiss_IndexBinaryIVF_merge_from(
+ idx.bPtr(),
+ other.bPtr(),
+ (C.idx_t)(add_id),
+ ); c != 0 {
+ err = getLastError()
+ }
+
+ return err
+}
diff --git a/vendor/github.com/blevesearch/go-faiss/index_flat.go b/vendor/github.com/blevesearch/go-faiss/index_flat.go
index b8a3c03880..65d3bb64ee 100644
--- a/vendor/github.com/blevesearch/go-faiss/index_flat.go
+++ b/vendor/github.com/blevesearch/go-faiss/index_flat.go
@@ -44,13 +44,3 @@ func (idx *IndexFlat) Xb() []float32 {
C.faiss_IndexFlat_xb(idx.cPtr(), &ptr, &size)
return (*[1 << 30]float32)(unsafe.Pointer(ptr))[:size:size]
}
-
-// AsFlat casts idx to a flat index.
-// AsFlat panics if idx is not a flat index.
-func (idx *IndexImpl) AsFlat() *IndexFlat {
- ptr := C.faiss_IndexFlat_cast(idx.cPtr())
- if ptr == nil {
- panic("index is not a flat index")
- }
- return &IndexFlat{&faissIndex{ptr}}
-}
diff --git a/vendor/github.com/blevesearch/go-faiss/index_io.go b/vendor/github.com/blevesearch/go-faiss/index_io.go
index 608f4d75fe..830877f6f0 100644
--- a/vendor/github.com/blevesearch/go-faiss/index_io.go
+++ b/vendor/github.com/blevesearch/go-faiss/index_io.go
@@ -118,3 +118,43 @@ func ReadIndex(filename string, ioflags int) (*IndexImpl, error) {
}
return &IndexImpl{&idx}, nil
}
+
+func WriteBinaryIndexIntoBuffer(idx BinaryIndex) ([]byte, error) {
+ // the values to be returned by the faiss APIs
+ tempBuf := (*C.uchar)(nil)
+ bufSize := C.size_t(0)
+
+ if c := C.faiss_write_index_binary_buf(
+ idx.bPtr(),
+ &bufSize,
+ &tempBuf,
+ ); c != 0 {
+ C.faiss_free_buf(&tempBuf)
+ return nil, getLastError()
+ }
+
+ val := unsafe.Slice((*byte)(unsafe.Pointer(tempBuf)), uint(bufSize))
+
+ rv := make([]byte, uint(bufSize))
+ copy(rv, val)
+
+ C.faiss_free_buf(&tempBuf)
+ val = nil
+
+ return rv, nil
+}
+
+func ReadBinaryIndexFromBuffer(buf []byte, ioflags int) (*BinaryIndexImpl, error) {
+ ptr := (*C.uchar)(unsafe.Pointer(&buf[0]))
+ size := C.size_t(len(buf))
+
+ var bIdx faissBinaryIndex
+ if c := C.faiss_read_index_binary_buf(ptr,
+ size,
+ C.int(ioflags),
+ &bIdx.bIdx); c != 0 {
+ return nil, getLastError()
+ }
+
+ return &BinaryIndexImpl{&bIdx}, nil
+}
diff --git a/vendor/github.com/blevesearch/go-faiss/index_ivf.go b/vendor/github.com/blevesearch/go-faiss/index_ivf.go
index 38f023aa90..0ae0fc737b 100644
--- a/vendor/github.com/blevesearch/go-faiss/index_ivf.go
+++ b/vendor/github.com/blevesearch/go-faiss/index_ivf.go
@@ -6,17 +6,18 @@ package faiss
#include
#include
#include
+#include
*/
import "C"
import (
"fmt"
)
-func (idx *IndexImpl) SetDirectMap(mapType int) (err error) {
+func (idx *faissIndex) SetDirectMap(mapType int) (err error) {
ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
if ivfPtr == nil {
- return fmt.Errorf("index is not of ivf type")
+ return errNotIVFIndex
}
if c := C.faiss_IndexIVF_set_direct_map(
ivfPtr,
@@ -27,7 +28,7 @@ func (idx *IndexImpl) SetDirectMap(mapType int) (err error) {
return err
}
-func (idx *IndexImpl) GetSubIndex() (*IndexImpl, error) {
+func (idx *faissIndex) GetSubIndex() (Index, error) {
ptr := C.faiss_IndexIDMap2_cast(idx.cPtr())
if ptr == nil {
@@ -44,7 +45,7 @@ func (idx *IndexImpl) GetSubIndex() (*IndexImpl, error) {
// pass nprobe to be set as index time option for IVF indexes only.
// varying nprobe impacts recall but with an increase in latency.
-func (idx *IndexImpl) SetNProbe(nprobe int32) {
+func (idx *faissIndex) SetNProbe(nprobe int32) {
ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
if ivfPtr == nil {
return
@@ -52,10 +53,35 @@ func (idx *IndexImpl) SetNProbe(nprobe int32) {
C.faiss_IndexIVF_set_nprobe(ivfPtr, C.size_t(nprobe))
}
-func (idx *IndexImpl) GetNProbe() int32 {
+func (idx *faissIndex) IVFParams() (nprobe, nlist int) {
ivfPtr := C.faiss_IndexIVF_cast(idx.cPtr())
if ivfPtr == nil {
- return 0
+ return 0, 0
}
- return int32(C.faiss_IndexIVF_nprobe(ivfPtr))
+ return int(C.faiss_IndexIVF_nprobe(ivfPtr)),
+ int(C.faiss_IndexIVF_nlist(ivfPtr))
+}
+
+func (idx *faissIndex) IsSQIndex() bool {
+ sqPtr := C.faiss_IndexScalarQuantizer_cast(idx.cPtr())
+ return sqPtr != nil
+}
+
+func (idx *faissIndex) SetQuantizers(srcIndex Index) error {
+ if !(idx.IsIVFIndex() && srcIndex.IsIVFIndex()) &&
+ !(idx.IsSQIndex() && srcIndex.IsSQIndex()) {
+ return fmt.Errorf("faissIndex SetQuantizers: %w, index type not supported", errFailedToSetQuantizers)
+ }
+
+ srcIndexPtr := srcIndex.cPtr()
+ if srcIndexPtr == nil {
+ return fmt.Errorf("coarse quantizer is not valid")
+ }
+
+ err := C.faiss_Set_quantizers(idx.idx, srcIndexPtr)
+ if err != 0 {
+ return fmt.Errorf("faissIndex SetQuantizers: %w", errFailedToSetQuantizers)
+ }
+
+ return nil
}
diff --git a/vendor/github.com/blevesearch/go-faiss/search_params.go b/vendor/github.com/blevesearch/go-faiss/search_params.go
index 6086073823..baa028e458 100644
--- a/vendor/github.com/blevesearch/go-faiss/search_params.go
+++ b/vendor/github.com/blevesearch/go-faiss/search_params.go
@@ -3,6 +3,8 @@ package faiss
/*
#include
#include
+#include
+#include
#include
*/
import "C"
@@ -54,60 +56,148 @@ func getNProbeFromSearchParams(params *SearchParams) int32 {
return int32(C.faiss_SearchParametersIVF_nprobe(params.sp))
}
-// Returns a valid SearchParams object,
-// thus caller must clean up the object
-// by invoking Delete() method.
-func NewSearchParams(idx Index, params json.RawMessage, sel *C.FaissIDSelector,
+// Returns a valid SearchParams object, configured according to the provided
+// parameters and selector. The returned SearchParams object is allocated,
+// thus caller must clean up the object by invoking Delete() method.
+func NewSearchParams(idx Index, params json.RawMessage, selector Selector,
defaultParams *defaultSearchParamsIVF) (*SearchParams, error) {
+ // Get the selector C pointer, if any.
+ // A nil selector indicates no ID filtering, and it is valid
+ // to send a nil pointer to Faiss.
+ var sel *C.FaissIDSelector
+ if selector != nil {
+ sel = selector.Get()
+ }
+
+ ivfIdx := C.faiss_IndexIVF_cast(idx.cPtr())
+ // if the index is not an IVF index, create a standard SearchParameters object
+ if ivfIdx == nil {
+ rv := &SearchParams{}
+ // Create standard SearchParameters for non-IVF index
+ if c := C.faiss_SearchParameters_new(&rv.sp, sel); c != 0 {
+ return nil, fmt.Errorf("failed to create faiss search params")
+ }
+ return rv, nil
+ }
+
+ nlist := int(C.faiss_IndexIVF_nlist(ivfIdx))
+ nprobe := int(C.faiss_IndexIVF_nprobe(ivfIdx))
+ nvecs := int(C.faiss_Index_ntotal(idx.cPtr()))
+
+ maxCodes, nprobe, err := resolveSearchParams(params, defaultParams, nlist, nprobe, nvecs)
+ if err != nil {
+ return nil, err
+ }
+
+ if idx.HasRaBitQ() {
+ return buildRaBitQSearchParams(maxCodes, nprobe, sel)
+ }
+ return buildIVFSearchParams(maxCodes, nprobe, sel)
+}
+
+func resolveSearchParams(params json.RawMessage, defaultParams *defaultSearchParamsIVF,
+ nlist, nprobe, nvecs int) (int, int, error) {
+ if defaultParams != nil {
+ if defaultParams.Nlist > 0 {
+ nlist = defaultParams.Nlist
+ }
+ if defaultParams.Nprobe > 0 {
+ nprobe = defaultParams.Nprobe
+ }
+ }
+ var ivfParams searchParamsIVF
+ if len(params) > 0 {
+ if err := json.Unmarshal(params, &ivfParams); err != nil {
+ return 0, 0, fmt.Errorf("failed to unmarshal IVF search params, "+
+ "err:%v", err)
+ }
+ if err := ivfParams.Validate(); err != nil {
+ return 0, 0, err
+ }
+ }
+ if ivfParams.NprobePct > 0 {
+ nprobe = max(int(float32(nlist)*(ivfParams.NprobePct/100)), 1)
+ }
+ var maxCodes int
+ if ivfParams.MaxCodesPct > 0 {
+ maxCodes = int(float32(nvecs) * (ivfParams.MaxCodesPct / 100))
+ } // else, maxCodes will be set to the default value of 0, which means no limit
+ return maxCodes, nprobe, nil
+}
+
+func buildIVFSearchParams(maxCodes, nprobe int, sel *C.FaissIDSelector) (*SearchParams, error) {
+ sp := &SearchParams{}
+ if c := C.faiss_SearchParametersIVF_new_with(
+ &sp.sp,
+ sel,
+ C.size_t(nprobe),
+ C.size_t(maxCodes),
+ ); c != 0 {
+ return nil, fmt.Errorf("failed to create faiss IVF search params")
+ }
+
+ return sp, nil
+}
+
+func buildRaBitQSearchParams(maxCodes, nprobe int, sel *C.FaissIDSelector) (*SearchParams, error) {
+ sp := &SearchParams{}
+ if c := C.faiss_SearchParametersRaBitQ_new_with(
+ &sp.sp,
+ sel,
+ C.size_t(nprobe),
+ C.size_t(maxCodes),
+ ); c != 0 {
+ return nil, fmt.Errorf("failed to create faiss IVF RaBitQ search params")
+ }
+
+ return sp, nil
+}
+
+// Returns a standard SearchParams object without any special settings with
+// the provided selector. The returned SearchParams object is allocated,
+// thus caller must clean up the object by invoking Delete() method.
+func NewStandardSearchParams(selector Selector) (*SearchParams, error) {
+ var sel *C.FaissIDSelector
+ if selector != nil {
+ sel = selector.Get()
+ }
rv := &SearchParams{}
if c := C.faiss_SearchParameters_new(&rv.sp, sel); c != 0 {
return nil, fmt.Errorf("failed to create faiss search params")
}
- // check if the index is IVF and set the search params
- if ivfIdx := C.faiss_IndexIVF_cast(idx.cPtr()); ivfIdx != nil {
- rv.sp = C.faiss_SearchParametersIVF_cast(rv.sp)
- if len(params) == 0 && sel == nil {
- return rv, nil
- }
- var nlist, nprobe, nvecs, maxCodes int
- nlist = int(C.faiss_IndexIVF_nlist(ivfIdx))
- nprobe = int(C.faiss_IndexIVF_nprobe(ivfIdx))
- nvecs = int(C.faiss_Index_ntotal(idx.cPtr()))
- if defaultParams != nil {
- if defaultParams.Nlist > 0 {
- nlist = defaultParams.Nlist
- }
- if defaultParams.Nprobe > 0 {
- nprobe = defaultParams.Nprobe
- }
- }
- var ivfParams searchParamsIVF
- if len(params) > 0 {
- if err := json.Unmarshal(params, &ivfParams); err != nil {
- rv.Delete()
- return nil, fmt.Errorf("failed to unmarshal IVF search params, "+
- "err:%v", err)
- }
- if err := ivfParams.Validate(); err != nil {
- rv.Delete()
- return nil, err
- }
- }
- if ivfParams.NprobePct > 0 {
- nprobe = max(int(float32(nlist)*(ivfParams.NprobePct/100)), 1)
- }
- if ivfParams.MaxCodesPct > 0 {
- maxCodes = int(float32(nvecs) * (ivfParams.MaxCodesPct / 100))
- } // else, maxCodes will be set to the default value of 0, which means no limit
- if c := C.faiss_SearchParametersIVF_new_with(
- &rv.sp,
- sel,
- C.size_t(nprobe),
- C.size_t(maxCodes),
- ); c != 0 {
- rv.Delete()
- return nil, fmt.Errorf("failed to create faiss IVF search params")
- }
- }
return rv, nil
}
+
+func NewBinarySearchParams(idx BinaryIndex, params json.RawMessage, selector Selector,
+ defaultParams *defaultSearchParamsIVF) (*SearchParams, error) {
+ // Get the selector C pointer, if any.
+ // A nil selector indicates no ID filtering, and it is valid
+ // to send a nil pointer to Faiss.
+ var sel *C.FaissIDSelector
+ if selector != nil {
+ sel = selector.Get()
+ }
+
+ ivfPtrBinary := C.faiss_IndexBinaryIVF_cast(idx.bPtr())
+
+ // if the index is not an IVF index, create a standard SearchParameters object
+ if ivfPtrBinary == nil {
+ rv := &SearchParams{}
+ // Create standard SearchParameters for non-IVF index
+ if c := C.faiss_SearchParameters_new(&rv.sp, sel); c != 0 {
+ return nil, fmt.Errorf("failed to create faiss search params")
+ }
+ return rv, nil
+ }
+
+ nlist := int(C.faiss_IndexBinaryIVF_nlist(ivfPtrBinary))
+ nprobe := int(C.faiss_IndexBinaryIVF_nprobe(ivfPtrBinary))
+ nvecs := int(C.faiss_IndexBinary_ntotal(idx.bPtr()))
+
+ maxCodes, nprobe, err := resolveSearchParams(params, defaultParams, nlist, nprobe, nvecs)
+ if err != nil {
+ return nil, err
+ }
+
+ return buildIVFSearchParams(maxCodes, nprobe, sel)
+}
diff --git a/vendor/github.com/blevesearch/go-faiss/selector.go b/vendor/github.com/blevesearch/go-faiss/selector.go
index 8e95c4618f..d250096d41 100644
--- a/vendor/github.com/blevesearch/go-faiss/selector.go
+++ b/vendor/github.com/blevesearch/go-faiss/selector.go
@@ -5,36 +5,30 @@ package faiss
*/
import "C"
+// Note: currently we have only one implementation, but we keep the interface for future extensibility
type Selector interface {
+ ExcludeFilter() bool
Get() *C.FaissIDSelector
Delete()
}
// IDSelector represents a set of IDs to remove.
type IDSelector struct {
- sel *C.FaissIDSelector
-}
-
-// Delete frees the memory associated with s.
-func (s *IDSelector) Delete() {
- if s == nil || s.sel == nil {
- return
- }
-
- C.faiss_IDSelector_free(s.sel)
+ exclude bool
+ sel *C.FaissIDSelector
+ inner *C.FaissIDSelector
}
func (s *IDSelector) Get() *C.FaissIDSelector {
return s.sel
}
-type IDSelectorNot struct {
- sel *C.FaissIDSelector
- batchSel *C.FaissIDSelector
+func (s *IDSelector) ExcludeFilter() bool {
+ return s.exclude
}
// Delete frees the memory associated with s.
-func (s *IDSelectorNot) Delete() {
+func (s *IDSelector) Delete() {
if s == nil {
return
}
@@ -42,15 +36,11 @@ func (s *IDSelectorNot) Delete() {
if s.sel != nil {
C.faiss_IDSelector_free(s.sel)
}
- if s.batchSel != nil {
- C.faiss_IDSelector_free(s.batchSel)
+ if s.inner != nil {
+ C.faiss_IDSelector_free(s.inner)
}
}
-func (s *IDSelectorNot) Get() *C.FaissIDSelector {
- return s.sel
-}
-
// NewIDSelectorRange creates a selector that removes IDs on [imin, imax).
func NewIDSelectorRange(imin, imax int64) (Selector, error) {
var sel *C.FaissIDSelectorRange
@@ -58,7 +48,7 @@ func NewIDSelectorRange(imin, imax int64) (Selector, error) {
if c != 0 {
return nil, getLastError()
}
- return &IDSelector{(*C.FaissIDSelector)(sel)}, nil
+ return &IDSelector{sel: (*C.FaissIDSelector)(sel)}, nil
}
// NewIDSelectorBatch creates a new batch selector.
@@ -71,12 +61,12 @@ func NewIDSelectorBatch(indices []int64) (Selector, error) {
); c != 0 {
return nil, getLastError()
}
- return &IDSelector{(*C.FaissIDSelector)(sel)}, nil
+ return &IDSelector{sel: (*C.FaissIDSelector)(sel)}, nil
}
-// NewIDSelectorNot creates a new Not selector, wrapped around a
+// NewIDSelectorBatchNot creates a new Not selector, wrapped around a
// batch selector, with the IDs in 'exclude'.
-func NewIDSelectorNot(exclude []int64) (Selector, error) {
+func NewIDSelectorBatchNot(exclude []int64) (Selector, error) {
batchSelector, err := NewIDSelectorBatch(exclude)
if err != nil {
return nil, err
@@ -90,6 +80,49 @@ func NewIDSelectorNot(exclude []int64) (Selector, error) {
batchSelector.Delete()
return nil, getLastError()
}
- return &IDSelectorNot{sel: (*C.FaissIDSelector)(sel),
- batchSel: batchSelector.Get()}, nil
+ return &IDSelector{exclude: true,
+ sel: (*C.FaissIDSelector)(sel),
+ inner: batchSelector.Get()}, nil
+}
+
+// NewIDSelectorBitmap creates a selector using a bitset, where each bit
+// indicates whether the corresponding ID is to be selected.
+// NOTE: This function assumes that len(bitmap)*8 covers the full range of IDs
+// in the index, and only works when we have vector IDs ranging from 0 to N-1,
+// where N is the number of vectors in the index.
+// The length of the bitmap should be at least ceil(N/8).
+func NewIDSelectorBitmap(bitmap []byte) (Selector, error) {
+ var sel *C.FaissIDSelectorBitmap
+ if c := C.faiss_IDSelectorBitmap_new(
+ &sel,
+ C.size_t(len(bitmap)),
+ (*C.uint8_t)(&bitmap[0]),
+ ); c != 0 {
+ return nil, getLastError()
+ }
+ return &IDSelector{sel: (*C.FaissIDSelector)(sel)}, nil
+}
+
+// NewIDSelectorBitmapNot creates a NOT selector using a bitset, where each bit
+// indicates whether the corresponding ID is NOT to be selected.
+// NOTE: This function assumes that len(bitmap)*8 covers the full range of IDs
+// in the index, and only works when we have vector IDs ranging from 0 to N-1,
+// where N is the number of vectors in the index.
+// The length of the bitmap should be at least ceil(N/8).
+func NewIDSelectorBitmapNot(bitmap []byte) (Selector, error) {
+ bitmapSelector, err := NewIDSelectorBitmap(bitmap)
+ if err != nil {
+ return nil, err
+ }
+ var sel *C.FaissIDSelectorNot
+ if c := C.faiss_IDSelectorNot_new(
+ &sel,
+ bitmapSelector.Get(),
+ ); c != 0 {
+ bitmapSelector.Delete()
+ return nil, getLastError()
+ }
+ return &IDSelector{exclude: true,
+ sel: (*C.FaissIDSelector)(sel),
+ inner: bitmapSelector.Get()}, nil
}
diff --git a/vendor/github.com/blevesearch/mmap-go/.gitignore b/vendor/github.com/blevesearch/mmap-go/.gitignore
index 0c0a5e4916..6c694e4b7d 100644
--- a/vendor/github.com/blevesearch/mmap-go/.gitignore
+++ b/vendor/github.com/blevesearch/mmap-go/.gitignore
@@ -7,4 +7,5 @@ _obj
_test
testdata
/.idea
-*.iml
\ No newline at end of file
+*.iml
+/notes.txt
diff --git a/vendor/github.com/blevesearch/mmap-go/.travis.yml b/vendor/github.com/blevesearch/mmap-go/.travis.yml
deleted file mode 100644
index 169eb1f354..0000000000
--- a/vendor/github.com/blevesearch/mmap-go/.travis.yml
+++ /dev/null
@@ -1,16 +0,0 @@
-language: go
-os:
- - linux
- - osx
- - windows
-go:
- - 1.11.4
-env:
- global:
- - GO111MODULE=on
-install:
- - go mod download
- - go get github.com/mattn/goveralls
-script:
- - go test -v -covermode=count -coverprofile=coverage.out -bench . -cpu 1,4
- - '[ "${TRAVIS_PULL_REQUEST}" = "false" ] && $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci -repotoken $COVERALLS_TOKEN || true'
diff --git a/vendor/github.com/blevesearch/mmap-go/README.md b/vendor/github.com/blevesearch/mmap-go/README.md
index 4cc2bfe1c8..30166aa4a9 100644
--- a/vendor/github.com/blevesearch/mmap-go/README.md
+++ b/vendor/github.com/blevesearch/mmap-go/README.md
@@ -1,12 +1,14 @@
mmap-go
=======
+[](https://github.com/blevesearch/mmap-go/actions/workflows/tests.yml?query=event%3Apush+branch%3Amaster)
+[](https://pkg.go.dev/github.com/blevesearch/mmap-go)
mmap-go is a portable mmap package for the [Go programming language](http://golang.org).
-It has been tested on Linux (386, amd64), OS X, and Windows (386). It should also
-work on other Unix-like platforms, but hasn't been tested with them. I'm interested
-to hear about the results.
-I haven't been able to add more features without adding significant complexity,
-so mmap-go doesn't support mprotect, mincore, and maybe a few other things.
-If you're running on a Unix-like platform and need some of these features,
-I suggest Gustavo Niemeyer's [gommap](http://labix.org/gommap).
+Operating System Support
+========================
+This package is tested using GitHub Actions on Linux, macOS, and Windows. It should also work on other Unix-like platforms, but hasn't been tested with them. I'm interested to hear about the results.
+
+This package compiles for Plan 9 and WebAssembly, but its functions always return errors.
+
+Related functions such as `mprotect` and `mincore` aren't included. I haven't found a way to implement them on Windows without introducing significant complexity. If you're running on a Unix-like platform and really need these features, it should still be possible to implement them on top of this package via `syscall`.
diff --git a/vendor/github.com/blevesearch/mmap-go/mmap.go b/vendor/github.com/blevesearch/mmap-go/mmap.go
index 29655bd222..736f29a2d7 100644
--- a/vendor/github.com/blevesearch/mmap-go/mmap.go
+++ b/vendor/github.com/blevesearch/mmap-go/mmap.go
@@ -8,10 +8,10 @@
// Package mmap allows mapping files into memory. It tries to provide a simple, reasonably portable interface,
// but doesn't go out of its way to abstract away every little platform detail.
// This specifically means:
-// * forked processes may or may not inherit mappings
-// * a file's timestamp may or may not be updated by writes through mappings
-// * specifying a size larger than the file's actual size can increase the file's size
-// * If the mapped file is being modified by another process while your program's running, don't expect consistent results between platforms
+// - forked processes may or may not inherit mappings
+// - a file's timestamp may or may not be updated by writes through mappings
+// - specifying a size larger than the file's actual size can increase the file's size
+// - If the mapped file is being modified by another process while your program's running, don't expect consistent results between platforms
package mmap
import (
diff --git a/vendor/github.com/blevesearch/mmap-go/mmap_plan9.go b/vendor/github.com/blevesearch/mmap-go/mmap_plan9.go
new file mode 100644
index 0000000000..e4c33d39b8
--- /dev/null
+++ b/vendor/github.com/blevesearch/mmap-go/mmap_plan9.go
@@ -0,0 +1,27 @@
+// Copyright 2020 Evan Shaw. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package mmap
+
+import "syscall"
+
+func mmap(len int, inprot, inflags, fd uintptr, off int64) ([]byte, error) {
+ return nil, syscall.EPLAN9
+}
+
+func (m MMap) flush() error {
+ return syscall.EPLAN9
+}
+
+func (m MMap) lock() error {
+ return syscall.EPLAN9
+}
+
+func (m MMap) unlock() error {
+ return syscall.EPLAN9
+}
+
+func (m MMap) unmap() error {
+ return syscall.EPLAN9
+}
diff --git a/vendor/github.com/blevesearch/mmap-go/mmap_unix.go b/vendor/github.com/blevesearch/mmap-go/mmap_unix.go
index 25b13e51fd..62d0aef6cd 100644
--- a/vendor/github.com/blevesearch/mmap-go/mmap_unix.go
+++ b/vendor/github.com/blevesearch/mmap-go/mmap_unix.go
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
+//go:build darwin || dragonfly || freebsd || linux || openbsd || solaris || netbsd
// +build darwin dragonfly freebsd linux openbsd solaris netbsd
package mmap
diff --git a/vendor/github.com/blevesearch/mmap-go/mmap_wasm.go b/vendor/github.com/blevesearch/mmap-go/mmap_wasm.go
new file mode 100644
index 0000000000..cfe1c50b03
--- /dev/null
+++ b/vendor/github.com/blevesearch/mmap-go/mmap_wasm.go
@@ -0,0 +1,27 @@
+// Copyright 2024 Evan Shaw. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package mmap
+
+import "syscall"
+
+func mmap(len int, inprot, inflags, fd uintptr, off int64) ([]byte, error) {
+ return nil, syscall.ENOTSUP
+}
+
+func (m MMap) flush() error {
+ return syscall.ENOTSUP
+}
+
+func (m MMap) lock() error {
+ return syscall.ENOTSUP
+}
+
+func (m MMap) unlock() error {
+ return syscall.ENOTSUP
+}
+
+func (m MMap) unmap() error {
+ return syscall.ENOTSUP
+}
diff --git a/vendor/github.com/blevesearch/mmap-go/mmap_windows.go b/vendor/github.com/blevesearch/mmap-go/mmap_windows.go
index 631b3825f9..e0d986f70a 100644
--- a/vendor/github.com/blevesearch/mmap-go/mmap_windows.go
+++ b/vendor/github.com/blevesearch/mmap-go/mmap_windows.go
@@ -67,6 +67,7 @@ func mmap(len int, prot, flags, hfile uintptr, off int64) ([]byte, error) {
fileOffsetLow := uint32(off & 0xFFFFFFFF)
addr, errno := windows.MapViewOfFile(h, dwDesiredAccess, fileOffsetHigh, fileOffsetLow, uintptr(len))
if addr == 0 {
+ windows.CloseHandle(windows.Handle(h))
return nil, os.NewSyscallError("MapViewOfFile", errno)
}
handleLock.Lock()
@@ -101,7 +102,7 @@ func (m MMap) flush() error {
return errors.New("unknown base address")
}
- if handle.writable {
+ if handle.writable && handle.file != windows.Handle(^uintptr(0)) {
if err := windows.FlushFileBuffers(handle.file); err != nil {
return os.NewSyscallError("FlushFileBuffers", err)
}
diff --git a/vendor/github.com/blevesearch/scorch_segment_api/v2/.golangci.yml b/vendor/github.com/blevesearch/scorch_segment_api/v2/.golangci.yml
deleted file mode 100644
index 664f35f27e..0000000000
--- a/vendor/github.com/blevesearch/scorch_segment_api/v2/.golangci.yml
+++ /dev/null
@@ -1,42 +0,0 @@
-linters:
- # please, do not use `enable-all`: it's deprecated and will be removed soon.
- # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
- disable-all: true
- enable:
- - bodyclose
- - deadcode
- - depguard
- - dogsled
- - dupl
- - errcheck
- - funlen
- - gochecknoinits
- - goconst
- - gocritic
- - gocyclo
- - gofmt
- - goimports
- - golint
- - gomnd
- - goprintffuncname
- - gosec
- - gosimple
- - govet
- - ineffassign
- - interfacer
- - lll
- - misspell
- - nakedret
- - nolintlint
- - rowserrcheck
- - scopelint
- - staticcheck
- - structcheck
- - stylecheck
- - typecheck
- - unconvert
- - unparam
- - unused
- - varcheck
- - whitespace
-
diff --git a/vendor/github.com/blevesearch/scorch_segment_api/v2/README.md b/vendor/github.com/blevesearch/scorch_segment_api/v2/README.md
index dc33b004ed..76a994fe9e 100644
--- a/vendor/github.com/blevesearch/scorch_segment_api/v2/README.md
+++ b/vendor/github.com/blevesearch/scorch_segment_api/v2/README.md
@@ -1,8 +1,7 @@
# Scorch Segment API
-[](https://pkg.go.dev/github.com/blevesearch/scorch_segment_api)
-[](https://github.com/blevesearch/scorch_segment_api/actions?query=workflow%3ATests+event%3Apush+branch%3Amaster)
-[](https://github.com/blevesearch/scorch_segment_api/actions?query=workflow%3ALint+event%3Apush+branch%3Amaster)
+[](https://pkg.go.dev/github.com/blevesearch/scorch_segment_api/v2)
+[](https://github.com/blevesearch/scorch_segment_api/actions/workflows/tests.yml?query=event%3Apush+branch%3Amaster)
Scorch supports a pluggable Segment interface.
diff --git a/vendor/github.com/blevesearch/scorch_segment_api/v2/segment.go b/vendor/github.com/blevesearch/scorch_segment_api/v2/segment.go
index 122a28d793..00b64a8cf8 100644
--- a/vendor/github.com/blevesearch/scorch_segment_api/v2/segment.go
+++ b/vendor/github.com/blevesearch/scorch_segment_api/v2/segment.go
@@ -67,6 +67,11 @@ type UpdatableSegment interface {
SetUpdatedFields(fieldInfo map[string]*index.UpdateFieldInfo)
}
+type SegmentWithCallbacks interface {
+ Segment
+ CallbackId() string
+}
+
type TermDictionary interface {
PostingsList(term []byte, except *roaring.Bitmap, prealloc PostingsList) (PostingsList, error)
@@ -182,6 +187,10 @@ type FieldStatsReporter interface {
UpdateFieldStats(FieldStats)
}
+type VectorFieldStatsReporter interface {
+ UpdateVectorFieldStats(FieldStats)
+}
+
type FieldStats interface {
Store(statName, fieldName string, value uint64)
Aggregate(stats FieldStats)
@@ -243,3 +252,24 @@ type Synonym interface {
Size() int
}
+
+// NestedSegment is an optional interface that a Segment may implement
+// to provide access to nested document relationships within that segment.
+type NestedSegment interface {
+ Segment
+ // Ancestors returns a slice of ancestor IDs for the given document ID.
+ // If the document has no ancestors or if the segment does not support nested documents,
+ // a slice containing only the document ID itself is returned.
+ Ancestors(docID uint64, prealloc []index.AncestorID) []index.AncestorID
+
+ // CountRoot returns the number of root documents in the segment, excluding any documents
+ // that are marked as deleted in the provided bitmap. If the segment does not support nested
+ // documents, it returns the total document count minus the count of deleted documents.
+ // A root document is defined as a document that is not a child of any other document.
+ CountRoot(deleted *roaring.Bitmap) uint64
+
+ // AddNestedDocuments updates the provided bitmap to include all nested documents
+ // associated with documents marked as deleted in the bitmap. This ensures that when
+ // a parent document is deleted, all its nested child documents are also considered deleted.
+ AddNestedDocuments(deleted *roaring.Bitmap) *roaring.Bitmap
+}
diff --git a/vendor/github.com/blevesearch/scorch_segment_api/v2/segment_vector.go b/vendor/github.com/blevesearch/scorch_segment_api/v2/segment_vector.go
index 7e50ce46f2..430afb36dd 100644
--- a/vendor/github.com/blevesearch/scorch_segment_api/v2/segment_vector.go
+++ b/vendor/github.com/blevesearch/scorch_segment_api/v2/segment_vector.go
@@ -20,8 +20,8 @@ package segment
import (
"encoding/json"
- index "github.com/blevesearch/bleve_index_api"
"github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
)
type VecPostingsList interface {
@@ -58,21 +58,32 @@ type VecPostingsIterator interface {
}
type VectorIndex interface {
- // @params: Search params for backing vector index (like IVF, HNSW, etc.)
+ // Search performs a kNN search for the given query vector and returns a postings list.
+ // - qVector: the query vector
+ // - k: the number of similar vectors to return
+ // - params: additional search parameters
Search(qVector []float32, k int64, params json.RawMessage) (VecPostingsList, error)
- // @eligibleDocIDs: DocIDs in the segment eligible for the kNN query.
- SearchWithFilter(qVector []float32, k int64, eligibleDocIDs []uint64,
- params json.RawMessage) (VecPostingsList, error)
+ // SearchWithFilter performs a kNN search for the given query vector, filtering results based on eligible documents
+ // - qVector: the query vector
+ // - k: the number of similar vectors to return
+ // - eligibleList: list of eligible documents to consider
+ // - params: additional search parameters
+ SearchWithFilter(qVector []float32, k int64, eligibleList index.EligibleDocumentList, params json.RawMessage) (VecPostingsList, error)
+ // Close releases any resources held by the VectorIndex.
Close()
Size() uint64
ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) ([]index.CentroidCardinality, error)
}
+type TrainedSegment interface {
+ Segment
+ GetCoarseQuantizer(field string) (interface{}, error)
+}
+
type VectorSegment interface {
Segment
- InterpretVectorIndex(field string, requiresFiltering bool, except *roaring.Bitmap) (
- VectorIndex, error)
+ InterpretVectorIndex(field string, except *roaring.Bitmap) (VectorIndex, error)
}
type VecPosting interface {
diff --git a/vendor/github.com/blevesearch/vellum/README.md b/vendor/github.com/blevesearch/vellum/README.md
index e5c4a8bce8..1357f9d038 100644
--- a/vendor/github.com/blevesearch/vellum/README.md
+++ b/vendor/github.com/blevesearch/vellum/README.md
@@ -1,19 +1,20 @@
#  vellum
-[](https://github.com/couchbase/vellum/actions?query=workflow%3ATests+event%3Apush+branch%3Amaster)
-[](https://coveralls.io/github/couchbase/vellum?branch=master)
-[](https://godoc.org/github.com/couchbase/vellum)
-[](https://goreportcard.com/report/github.com/couchbase/vellum)
+[](https://github.com/blevesearch/vellum/actions/workflows/tests.yml?query=event%3Apush+branch%3Amaster)
+[](https://pkg.go.dev/github.com/blevesearch/vellum)
+[](https://goreportcard.com/report/github.com/blevesearch/vellum)
[](https://opensource.org/licenses/Apache-2.0)
A Go library implementing an FST (finite state transducer) capable of:
- - mapping between keys ([]byte) and a value (uint64)
- - enumerating keys in lexicographic order
+
+- mapping between keys ([]byte) and a value (uint64)
+- enumerating keys in lexicographic order
Some additional goals of this implementation:
- - bounded memory use while building the FST
- - streaming out FST data while building
- - mmap FST runtime to support very large FTSs (optional)
+
+- bounded memory use while building the FST
+- streaming out FST data while building
+- mmap FST runtime to support very large FTSs (optional)
## Usage
@@ -22,27 +23,30 @@ Some additional goals of this implementation:
To build an FST, create a new builder using the `New()` method. This method takes an `io.Writer` as an argument. As the FST is being built, data will be streamed to the writer as soon as possible. With this builder you **MUST** insert keys in lexicographic order. Inserting keys out of order will result in an error. After inserting the last key into the builder, you **MUST** call `Close()` on the builder. This will flush all remaining data to the underlying writer.
In memory:
+
```go
- var buf bytes.Buffer
- builder, err := vellum.New(&buf, nil)
- if err != nil {
- log.Fatal(err)
- }
+var buf bytes.Buffer
+builder, err := vellum.New(&buf, nil)
+if err != nil {
+ log.Fatal(err)
+}
```
To disk:
+
```go
- f, err := os.Create("/tmp/vellum.fst")
- if err != nil {
- log.Fatal(err)
- }
- builder, err := vellum.New(f, nil)
- if err != nil {
- log.Fatal(err)
- }
+f, err := os.Create("/tmp/vellum.fst")
+if err != nil {
+ log.Fatal(err)
+}
+builder, err := vellum.New(f, nil)
+if err != nil {
+ log.Fatal(err)
+}
```
**MUST** insert keys in lexicographic order:
+
```go
err = builder.Insert([]byte("cat"), 1)
if err != nil {
@@ -70,45 +74,49 @@ if err != nil {
After closing the builder, the data can be used to instantiate an FST. If the data was written to disk, you can use the `Open()` method to mmap the file. If the data is already in memory, or you wish to load/mmap the data yourself, you can instantiate the FST with the `Load()` method.
Load in memory:
+
```go
- fst, err := vellum.Load(buf.Bytes())
- if err != nil {
- log.Fatal(err)
- }
+fst, err := vellum.Load(buf.Bytes())
+if err != nil {
+ log.Fatal(err)
+}
```
Open from disk:
+
```go
- fst, err := vellum.Open("/tmp/vellum.fst")
- if err != nil {
- log.Fatal(err)
- }
+fst, err := vellum.Open("/tmp/vellum.fst")
+if err != nil {
+ log.Fatal(err)
+}
```
Get key/value:
+
```go
- val, exists, err = fst.Get([]byte("dog"))
- if err != nil {
- log.Fatal(err)
- }
- if exists {
- fmt.Printf("contains dog with val: %d\n", val)
- } else {
- fmt.Printf("does not contain dog")
- }
+val, exists, err = fst.Get([]byte("dog"))
+if err != nil {
+ log.Fatal(err)
+}
+if exists {
+ fmt.Printf("contains dog with val: %d\n", val)
+} else {
+ fmt.Printf("does not contain dog")
+}
```
Iterate key/values:
+
```go
- itr, err := fst.Iterator(startKeyInclusive, endKeyExclusive)
- for err == nil {
- key, val := itr.Current()
- fmt.Printf("contains key: %s val: %d", key, val)
- err = itr.Next()
- }
- if err != nil {
- log.Fatal(err)
- }
+itr, err := fst.Iterator(startKeyInclusive, endKeyExclusive)
+for err == nil {
+ key, val := itr.Current()
+ fmt.Printf("contains key: %s val: %d", key, val)
+ err = itr.Next()
+}
+if err != nil {
+ log.Fatal(err)
+}
```
### How does the FST get built?
@@ -169,14 +177,17 @@ The vellum command-line tool has a "dot" subcommand that can emit
graphviz dot output data from an input vellum file. The dot file can
in turn be converted into an image using graphviz tools. Example...
- $ vellum dot myFile.vellum > output.dot
- $ dot -Tpng output.dot -o output.png
+```shell
+vellum dot myFile.vellum > output.dot
+dot -Tpng output.dot -o output.png
+```
## Related Work
Much credit goes to two existing projects:
- - [mafsa](https://github.com/smartystreets/mafsa)
- - [BurntSushi/fst](https://github.com/BurntSushi/fst)
+
+- [mafsa](https://github.com/smartystreets/mafsa)
+- [BurntSushi/fst](https://github.com/BurntSushi/fst)
Most of the original implementation here started with my digging into the internals of mafsa. As the implementation progressed, I continued to borrow ideas/approaches from the BurntSushi/fst library as well.
diff --git a/vendor/github.com/blevesearch/vellum/builder.go b/vendor/github.com/blevesearch/vellum/builder.go
index 7e545cbec1..b9012fd31a 100644
--- a/vendor/github.com/blevesearch/vellum/builder.go
+++ b/vendor/github.com/blevesearch/vellum/builder.go
@@ -415,15 +415,17 @@ func outputCat(l, r uint64) uint64 {
//
// NB: builderNode lifecylce is described by the following interactions -
// +------------------------+ +----------------------+
-// | Unfinished Nodes | Transfer once | Registry |
+// | Unfinished Nodes | Transfer once | Registry |
// |(not frozen builderNode)|-----builderNode is ------->| (frozen builderNode) |
// +------------------------+ marked frozen +----------------------+
-// ^ |
-// | |
-// | Put()
-// | Get() on +-------------------+ when
-// +-new char--------| builderNode Pool |<-----------evicted
-// +-------------------+
+//
+// ^ ^
+// | |
+// | |
+// | Put()
+// | Get() on +-------------------+ when
+// +-new char--------| builderNode Pool |<-----------evicted
+// +-------------------+
type builderNodePool struct {
head *builderNode
}
diff --git a/vendor/github.com/blevesearch/vellum/levenshtein/levenshtein_nfa.go b/vendor/github.com/blevesearch/vellum/levenshtein/levenshtein_nfa.go
index 68db5d191c..82655fbce6 100644
--- a/vendor/github.com/blevesearch/vellum/levenshtein/levenshtein_nfa.go
+++ b/vendor/github.com/blevesearch/vellum/levenshtein/levenshtein_nfa.go
@@ -19,13 +19,13 @@ import (
"sort"
)
-/// Levenshtein Distance computed by a Levenshtein Automaton.
-///
-/// Levenshtein automata can only compute the exact Levenshtein distance
-/// up to a given `max_distance`.
-///
-/// Over this distance, the automaton will invariably
-/// return `Distance::AtLeast(max_distance + 1)`.
+// Levenshtein Distance computed by a Levenshtein Automaton.
+//
+// Levenshtein automata can only compute the exact Levenshtein distance
+// up to a given `max_distance`.
+//
+// Over this distance, the automaton will invariably
+// return `Distance::AtLeast(max_distance + 1)`.
type Distance interface {
distance() uint8
}
diff --git a/vendor/github.com/blevesearch/vellum/levenshtein/parametric_dfa.go b/vendor/github.com/blevesearch/vellum/levenshtein/parametric_dfa.go
index d08e5da639..41d2fcf632 100644
--- a/vendor/github.com/blevesearch/vellum/levenshtein/parametric_dfa.go
+++ b/vendor/github.com/blevesearch/vellum/levenshtein/parametric_dfa.go
@@ -15,7 +15,7 @@
package levenshtein
import (
- "crypto/md5"
+ "crypto/sha256"
"encoding/json"
"fmt"
"math"
@@ -311,13 +311,13 @@ func fromNfa(nfa *LevenshteinNFA) (*ParametricDFA, error) {
}
type hash struct {
- index map[[16]byte]int
+ index map[[32]byte]int
items []MultiState
}
func newHash() *hash {
return &hash{
- index: make(map[[16]byte]int, 100),
+ index: make(map[[32]byte]int, 100),
items: make([]MultiState, 0, 100),
}
}
@@ -326,9 +326,9 @@ func (h *hash) getOrAllocate(m MultiState) int {
size := len(h.items)
var exists bool
var pos int
- md5 := getHash(&m)
- if pos, exists = h.index[md5]; !exists {
- h.index[md5] = size
+ sha := getHash(&m)
+ if pos, exists = h.index[sha]; !exists {
+ h.index[sha] = size
pos = size
h.items = append(h.items, m)
}
@@ -339,11 +339,11 @@ func (h *hash) getFromID(id int) *MultiState {
return &h.items[id]
}
-func getHash(ms *MultiState) [16]byte {
+func getHash(ms *MultiState) [32]byte {
msBytes := []byte{}
for _, state := range ms.states {
jsonBytes, _ := json.Marshal(&state)
msBytes = append(msBytes, jsonBytes...)
}
- return md5.Sum(msBytes)
+ return sha256.Sum256(msBytes)
}
diff --git a/vendor/github.com/blevesearch/vellum/vellum.go b/vendor/github.com/blevesearch/vellum/vellum.go
index b2537b3f00..699d2d0986 100644
--- a/vendor/github.com/blevesearch/vellum/vellum.go
+++ b/vendor/github.com/blevesearch/vellum/vellum.go
@@ -32,7 +32,6 @@ Once the FST is ready, you can use the Contains() method to see if a keys is
in the FST. You can use the Get() method to see if a key is in the FST and
retrieve it's associated value. And, you can use the Iterator method to
enumerate key/value pairs within a specified range.
-
*/
package vellum
diff --git a/vendor/github.com/blevesearch/vellum/vellum_mmap.go b/vendor/github.com/blevesearch/vellum/vellum_mmap.go
index 81ea165091..789bf5b421 100644
--- a/vendor/github.com/blevesearch/vellum/vellum_mmap.go
+++ b/vendor/github.com/blevesearch/vellum/vellum_mmap.go
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build !nommap
// +build !nommap
package vellum
diff --git a/vendor/github.com/blevesearch/vellum/vellum_nommap.go b/vendor/github.com/blevesearch/vellum/vellum_nommap.go
index e985272872..0b744aa431 100644
--- a/vendor/github.com/blevesearch/vellum/vellum_nommap.go
+++ b/vendor/github.com/blevesearch/vellum/vellum_nommap.go
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+//go:build nommap
// +build nommap
package vellum
diff --git a/vendor/github.com/blevesearch/zapx/v11/build.go b/vendor/github.com/blevesearch/zapx/v11/build.go
index 3f13a2a6c6..8992b50fb6 100644
--- a/vendor/github.com/blevesearch/zapx/v11/build.go
+++ b/vendor/github.com/blevesearch/zapx/v11/build.go
@@ -160,7 +160,7 @@ func persistStoredFieldValues(fieldID int,
func InitSegmentBase(mem []byte, memCRC uint32, chunkFactor uint32,
fieldsMap map[string]uint16, fieldsInv []string, numDocs uint64,
storedIndexOffset uint64, fieldsIndexOffset uint64, docValueOffset uint64,
- dictLocs []uint64) (*SegmentBase, error) {
+ dictLocs []uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -174,6 +174,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkFactor uint32,
dictLocs: dictLocs,
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v11/merge.go b/vendor/github.com/blevesearch/zapx/v11/merge.go
index 50bb2ba544..1b8758692a 100644
--- a/vendor/github.com/blevesearch/zapx/v11/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v11/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v11/new.go b/vendor/github.com/blevesearch/zapx/v11/new.go
index 095388d51d..c34a209138 100644
--- a/vendor/github.com/blevesearch/zapx/v11/new.go
+++ b/vendor/github.com/blevesearch/zapx/v11/new.go
@@ -45,11 +45,16 @@ var defaultChunkFactor uint32 = 1024
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkFactor(results, defaultChunkFactor)
+ return z.newWithChunkFactor(results, defaultChunkFactor, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkFactor(results, defaultChunkFactor, config)
}
func (*ZapPlugin) newWithChunkFactor(results []index.Document,
- chunkFactor uint32) (segment.Segment, uint64, error) {
+ chunkFactor uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -77,7 +82,7 @@ func (*ZapPlugin) newWithChunkFactor(results []index.Document,
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkFactor,
s.FieldsMap, s.FieldsInv, uint64(len(results)),
- storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets)
+ storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets, config)
if err == nil && s.reset() == nil {
s.lastNumDocs = len(results)
diff --git a/vendor/github.com/blevesearch/zapx/v11/segment.go b/vendor/github.com/blevesearch/zapx/v11/segment.go
index 7465dac15c..3353a3ffce 100644
--- a/vendor/github.com/blevesearch/zapx/v11/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v11/segment.go
@@ -37,8 +37,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -56,6 +66,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
fieldsMap: make(map[string]uint16),
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
},
f: f,
mm: mm,
@@ -104,6 +115,8 @@ type SegmentBase struct {
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
+
+ config map[string]interface{} // config for the segment
}
func (sb *SegmentBase) Size() int {
diff --git a/vendor/github.com/blevesearch/zapx/v12/build.go b/vendor/github.com/blevesearch/zapx/v12/build.go
index de8265c140..daac3d466d 100644
--- a/vendor/github.com/blevesearch/zapx/v12/build.go
+++ b/vendor/github.com/blevesearch/zapx/v12/build.go
@@ -160,7 +160,7 @@ func persistStoredFieldValues(fieldID int,
func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
fieldsMap map[string]uint16, fieldsInv []string, numDocs uint64,
storedIndexOffset uint64, fieldsIndexOffset uint64, docValueOffset uint64,
- dictLocs []uint64) (*SegmentBase, error) {
+ dictLocs []uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -174,6 +174,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
dictLocs: dictLocs,
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v12/merge.go b/vendor/github.com/blevesearch/zapx/v12/merge.go
index e962c6ec17..aace047b1e 100644
--- a/vendor/github.com/blevesearch/zapx/v12/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v12/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v12/new.go b/vendor/github.com/blevesearch/zapx/v12/new.go
index 94322c8e2d..48e13646d6 100644
--- a/vendor/github.com/blevesearch/zapx/v12/new.go
+++ b/vendor/github.com/blevesearch/zapx/v12/new.go
@@ -43,11 +43,16 @@ var ValidateDocFields = func(field index.Field) error {
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkMode(results, DefaultChunkMode)
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
}
func (*ZapPlugin) newWithChunkMode(results []index.Document,
- chunkMode uint32) (segment.Segment, uint64, error) {
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -75,7 +80,7 @@ func (*ZapPlugin) newWithChunkMode(results []index.Document,
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
s.FieldsMap, s.FieldsInv, uint64(len(results)),
- storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets)
+ storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets, config)
if err == nil && s.reset() == nil {
s.lastNumDocs = len(results)
diff --git a/vendor/github.com/blevesearch/zapx/v12/segment.go b/vendor/github.com/blevesearch/zapx/v12/segment.go
index 936b63836f..04b1af18c2 100644
--- a/vendor/github.com/blevesearch/zapx/v12/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v12/segment.go
@@ -37,8 +37,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -56,6 +66,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
fieldsMap: make(map[string]uint16),
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
},
f: f,
mm: mm,
@@ -104,6 +115,8 @@ type SegmentBase struct {
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
+
+ config map[string]interface{} // config for the segment
}
func (sb *SegmentBase) Size() int {
diff --git a/vendor/github.com/blevesearch/zapx/v13/build.go b/vendor/github.com/blevesearch/zapx/v13/build.go
index 827e5c47e8..5f9a30377e 100644
--- a/vendor/github.com/blevesearch/zapx/v13/build.go
+++ b/vendor/github.com/blevesearch/zapx/v13/build.go
@@ -160,7 +160,7 @@ func persistStoredFieldValues(fieldID int,
func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
fieldsMap map[string]uint16, fieldsInv []string, numDocs uint64,
storedIndexOffset uint64, fieldsIndexOffset uint64, docValueOffset uint64,
- dictLocs []uint64) (*SegmentBase, error) {
+ dictLocs []uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -174,6 +174,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
dictLocs: dictLocs,
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v13/merge.go b/vendor/github.com/blevesearch/zapx/v13/merge.go
index e962c6ec17..aace047b1e 100644
--- a/vendor/github.com/blevesearch/zapx/v13/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v13/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v13/new.go b/vendor/github.com/blevesearch/zapx/v13/new.go
index 94322c8e2d..48e13646d6 100644
--- a/vendor/github.com/blevesearch/zapx/v13/new.go
+++ b/vendor/github.com/blevesearch/zapx/v13/new.go
@@ -43,11 +43,16 @@ var ValidateDocFields = func(field index.Field) error {
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkMode(results, DefaultChunkMode)
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
}
func (*ZapPlugin) newWithChunkMode(results []index.Document,
- chunkMode uint32) (segment.Segment, uint64, error) {
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -75,7 +80,7 @@ func (*ZapPlugin) newWithChunkMode(results []index.Document,
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
s.FieldsMap, s.FieldsInv, uint64(len(results)),
- storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets)
+ storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets, config)
if err == nil && s.reset() == nil {
s.lastNumDocs = len(results)
diff --git a/vendor/github.com/blevesearch/zapx/v13/segment.go b/vendor/github.com/blevesearch/zapx/v13/segment.go
index 936b63836f..04b1af18c2 100644
--- a/vendor/github.com/blevesearch/zapx/v13/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v13/segment.go
@@ -37,8 +37,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -56,6 +66,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
fieldsMap: make(map[string]uint16),
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
},
f: f,
mm: mm,
@@ -104,6 +115,8 @@ type SegmentBase struct {
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
+
+ config map[string]interface{} // config for the segment
}
func (sb *SegmentBase) Size() int {
diff --git a/vendor/github.com/blevesearch/zapx/v14/build.go b/vendor/github.com/blevesearch/zapx/v14/build.go
index b36878abbb..9daf0a5316 100644
--- a/vendor/github.com/blevesearch/zapx/v14/build.go
+++ b/vendor/github.com/blevesearch/zapx/v14/build.go
@@ -160,7 +160,7 @@ func persistStoredFieldValues(fieldID int,
func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
fieldsMap map[string]uint16, fieldsInv []string, numDocs uint64,
storedIndexOffset uint64, fieldsIndexOffset uint64, docValueOffset uint64,
- dictLocs []uint64) (*SegmentBase, error) {
+ dictLocs []uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -174,6 +174,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
dictLocs: dictLocs,
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v14/merge.go b/vendor/github.com/blevesearch/zapx/v14/merge.go
index e962c6ec17..aace047b1e 100644
--- a/vendor/github.com/blevesearch/zapx/v14/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v14/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v14/new.go b/vendor/github.com/blevesearch/zapx/v14/new.go
index 94322c8e2d..48e13646d6 100644
--- a/vendor/github.com/blevesearch/zapx/v14/new.go
+++ b/vendor/github.com/blevesearch/zapx/v14/new.go
@@ -43,11 +43,16 @@ var ValidateDocFields = func(field index.Field) error {
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkMode(results, DefaultChunkMode)
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
}
func (*ZapPlugin) newWithChunkMode(results []index.Document,
- chunkMode uint32) (segment.Segment, uint64, error) {
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -75,7 +80,7 @@ func (*ZapPlugin) newWithChunkMode(results []index.Document,
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
s.FieldsMap, s.FieldsInv, uint64(len(results)),
- storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets)
+ storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets, config)
if err == nil && s.reset() == nil {
s.lastNumDocs = len(results)
diff --git a/vendor/github.com/blevesearch/zapx/v14/segment.go b/vendor/github.com/blevesearch/zapx/v14/segment.go
index 936b63836f..04b1af18c2 100644
--- a/vendor/github.com/blevesearch/zapx/v14/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v14/segment.go
@@ -37,8 +37,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -56,6 +66,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
fieldsMap: make(map[string]uint16),
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
},
f: f,
mm: mm,
@@ -104,6 +115,8 @@ type SegmentBase struct {
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
+
+ config map[string]interface{} // config for the segment
}
func (sb *SegmentBase) Size() int {
diff --git a/vendor/github.com/blevesearch/zapx/v15/build.go b/vendor/github.com/blevesearch/zapx/v15/build.go
index 5db1d9ee24..1e6d905299 100644
--- a/vendor/github.com/blevesearch/zapx/v15/build.go
+++ b/vendor/github.com/blevesearch/zapx/v15/build.go
@@ -160,7 +160,7 @@ func persistStoredFieldValues(fieldID int,
func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
fieldsMap map[string]uint16, fieldsInv []string, numDocs uint64,
storedIndexOffset uint64, fieldsIndexOffset uint64, docValueOffset uint64,
- dictLocs []uint64) (*SegmentBase, error) {
+ dictLocs []uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -174,6 +174,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32,
dictLocs: dictLocs,
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v15/merge.go b/vendor/github.com/blevesearch/zapx/v15/merge.go
index 738c24d6b8..f2af1834d4 100644
--- a/vendor/github.com/blevesearch/zapx/v15/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v15/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v15/new.go b/vendor/github.com/blevesearch/zapx/v15/new.go
index 9da48fa350..594afd6865 100644
--- a/vendor/github.com/blevesearch/zapx/v15/new.go
+++ b/vendor/github.com/blevesearch/zapx/v15/new.go
@@ -44,11 +44,16 @@ var ValidateDocFields = func(field index.Field) error {
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkMode(results, DefaultChunkMode)
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
}
func (*ZapPlugin) newWithChunkMode(results []index.Document,
- chunkMode uint32) (segment.Segment, uint64, error) {
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -76,7 +81,7 @@ func (*ZapPlugin) newWithChunkMode(results []index.Document,
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
s.FieldsMap, s.FieldsInv, uint64(len(results)),
- storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets)
+ storedIndexOffset, fieldsIndexOffset, fdvIndexOffset, dictOffsets, config)
// get the bytes written before the interim's reset() call
// write it to the newly formed segment base.
diff --git a/vendor/github.com/blevesearch/zapx/v15/segment.go b/vendor/github.com/blevesearch/zapx/v15/segment.go
index a4938b4ba6..842aaf8092 100644
--- a/vendor/github.com/blevesearch/zapx/v15/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v15/segment.go
@@ -38,8 +38,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -57,6 +67,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
fieldsMap: make(map[string]uint16),
fieldDvReaders: make(map[uint16]*docValueReader),
fieldFSTs: make(map[uint16]*vellum.FST),
+ config: config,
},
f: f,
mm: mm,
@@ -109,6 +120,8 @@ type SegmentBase struct {
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
+
+ config map[string]interface{} // config for the segment
}
func (sb *SegmentBase) Size() int {
diff --git a/vendor/github.com/blevesearch/zapx/v16/build.go b/vendor/github.com/blevesearch/zapx/v16/build.go
index 7843653af5..6426e57c9e 100644
--- a/vendor/github.com/blevesearch/zapx/v16/build.go
+++ b/vendor/github.com/blevesearch/zapx/v16/build.go
@@ -159,7 +159,7 @@ func persistStoredFieldValues(fieldID int,
}
func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32, numDocs uint64,
- storedIndexOffset uint64, sectionsIndexOffset uint64) (*SegmentBase, error) {
+ storedIndexOffset uint64, sectionsIndexOffset uint64, config map[string]interface{}) (*SegmentBase, error) {
sb := &SegmentBase{
mem: mem,
memCRC: memCRC,
@@ -178,6 +178,7 @@ func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32, numDocs uint64
fieldsMap: make(map[string]uint16),
dictLocs: make([]uint64, 0),
fieldsInv: make([]string, 0),
+ config: config,
}
sb.updateSize()
diff --git a/vendor/github.com/blevesearch/zapx/v16/faiss_vector_posting.go b/vendor/github.com/blevesearch/zapx/v16/faiss_vector_posting.go
index 3155bc220f..43e13a6ab0 100644
--- a/vendor/github.com/blevesearch/zapx/v16/faiss_vector_posting.go
+++ b/vendor/github.com/blevesearch/zapx/v16/faiss_vector_posting.go
@@ -274,8 +274,7 @@ func (vpItr *VecPostingsIterator) BytesWritten() uint64 {
// (2) search limited to a subset of documents within an attached vector index
// (3) close attached vector index
// (4) get the size of the attached vector index
-func (sb *SegmentBase) InterpretVectorIndex(field string, requiresFiltering bool,
- except *roaring.Bitmap) (
+func (sb *SegmentBase) InterpretVectorIndex(field string, except *roaring.Bitmap) (
segment.VectorIndex, error) {
rv := &vectorIndexWrapper{sb: sb}
@@ -304,7 +303,7 @@ func (sb *SegmentBase) InterpretVectorIndex(field string, requiresFiltering bool
var err error
rv.vecIndex, rv.vecDocIDMap, rv.docVecIDMap, rv.vectorIDsToExclude, err =
- sb.vecIndexCache.loadOrCreate(fieldIDPlus1, sb.mem[pos:], requiresFiltering,
+ sb.vecIndexCache.loadOrCreate(fieldIDPlus1, sb.mem[pos:], true, // always load docVecIDMap
except)
if err != nil {
return nil, err
diff --git a/vendor/github.com/blevesearch/zapx/v16/faiss_vector_wrapper.go b/vendor/github.com/blevesearch/zapx/v16/faiss_vector_wrapper.go
index 47c269c50e..e85b7d4be1 100644
--- a/vendor/github.com/blevesearch/zapx/v16/faiss_vector_wrapper.go
+++ b/vendor/github.com/blevesearch/zapx/v16/faiss_vector_wrapper.go
@@ -81,13 +81,8 @@ func (v *vectorIndexWrapper) Search(qVector []float32, k int64,
}
func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
- eligibleDocIDs []uint64, params json.RawMessage) (
+ eligibleList index.EligibleDocumentList, params json.RawMessage) (
segment.VecPostingsList, error) {
- // If every element in the index is eligible (full selectivity),
- // then this can basically be considered unfiltered kNN.
- if len(eligibleDocIDs) == int(v.sb.numDocs) {
- return v.Search(qVector, k, params)
- }
// 1. returned postings list (of type PostingsList) has two types of information - docNum and its score.
// 2. both the values can be represented using roaring bitmaps.
// 3. the Iterator (of type PostingsIterator) returned would operate in terms of VecPostings.
@@ -102,10 +97,32 @@ func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
// vector index not found or dimensionality mismatched
return rv, nil
}
- // Check and proceed only if non-zero documents eligible per the filter query.
- if len(eligibleDocIDs) == 0 {
+ if eligibleList == nil {
+ // no eligible documents
return rv, nil
}
+ numEligible := eligibleList.Count()
+ // Check and proceed only if non-zero documents eligible per the filter query.
+ if numEligible == 0 {
+ // no eligible documents
+ return rv, nil
+ }
+ // If every element in the index is eligible (full selectivity),
+ // then this can basically be considered unfiltered kNN.
+ if numEligible == v.sb.numDocs {
+ // all documents eligible, no filtering needed
+ return v.Search(qVector, k, params)
+ }
+ eligibleDocIDs := make([]uint32, 0, numEligible)
+ // get eligible iterator
+ eligibleItr := eligibleList.Iterator()
+ for {
+ docID, ok := eligibleItr.Next()
+ if !ok {
+ break
+ }
+ eligibleDocIDs = append(eligibleDocIDs, uint32(docID))
+ }
// vector IDs corresponding to the local doc numbers to be
// considered for the search
@@ -128,6 +145,10 @@ func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
if len(vectorIDsToInclude) == 0 {
return rv, nil
}
+ // If all vectors are eligible, treat as unfiltered search.
+ if len(vectorIDsToInclude) == len(v.vecDocIDMap) {
+ return v.Search(qVector, k, params)
+ }
// If the index is not an IVF index, then the search can be
// performed directly, using the Flat index.
if !v.vecIndex.IsIVFIndex() {
@@ -141,10 +162,18 @@ func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
v.addIDsToPostingsList(rv, rs)
return rv, nil
}
+ // Getting the nprobe value set at index time.
+ nprobe, nlist := v.vecIndex.IVFParams()
+ // include selector for the vector IDs to be considered
+ includeSelector, err := v.getSelector(vectorIDsToInclude, true)
+ if err != nil {
+ return nil, err
+ }
+ defer includeSelector.Delete()
// Determining which clusters, identified by centroid ID,
// have at least one eligible vector and hence, ought to be
// probed.
- clusterVectorCounts, err := v.vecIndex.ObtainClusterVectorCountsFromIVFIndex(vectorIDsToInclude)
+ clusterVectorCounts, err := v.vecIndex.ObtainClusterVectorCountsFromIVFIndex(includeSelector, nlist)
if err != nil {
return nil, err
}
@@ -185,35 +214,64 @@ func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
// Ordering the retrieved centroid IDs by increasing order
// of distance i.e. decreasing order of proximity to query vector.
centroidIDs := make([]int64, 0, len(clusterVectorCounts))
- for centroidID := range clusterVectorCounts {
- centroidIDs = append(centroidIDs, centroidID)
+ for centroidID, vectorCount := range clusterVectorCounts {
+ // Only centroids with at least one eligible vector are considered.
+ if vectorCount > 0 {
+ // since we are adding only unique centroid IDs, this is simply an increment
+ // and we can avoid a population count at the end
+ centroidIDs = append(centroidIDs, int64(centroidID))
+ }
}
- closestCentroidIDs, centroidDistances, err :=
- v.vecIndex.ObtainClustersWithDistancesFromIVFIndex(qVector, centroidIDs)
+ if len(centroidIDs) == 0 {
+ // no eligible centroids found
+ return rv, nil
+ }
+ // get centroid selector
+ centroidSelector, err := v.getSelector(centroidIDs, true)
+ if err != nil {
+ return nil, err
+ }
+ defer centroidSelector.Delete()
+
+ eligibleCentroidIDs, centroidDistances, err :=
+ v.vecIndex.ObtainClustersWithDistancesFromIVFIndex(qVector, centroidSelector, int64(len(centroidIDs)))
if err != nil {
return nil, err
}
- // Getting the nprobe value set at index time.
- nprobe := int(v.vecIndex.GetNProbe())
// Determining the minimum number of centroids to be probed
// to ensure that at least 'k' vectors are collected while
// examining at least 'nprobe' centroids.
// centroidsToProbe range: [nprobe, number of eligible centroids]
var eligibleVecsTillNow int64
- centroidsToProbe := len(closestCentroidIDs)
- for i, centroidID := range closestCentroidIDs {
+ var eligibleCentroidsTillNow int
+ centroidsToProbe := len(eligibleCentroidIDs)
+ for i, centroidID := range eligibleCentroidIDs {
+ // if we get a -1 somehow here, it means no more centroids
+ // need to reslice the eligibleCentroidIDs and distances
+ // accordingly, just a safeguard check as this does not
+ // really happen. FAISS can pad with -1s if there are not enough
+ // eligible centroids, but we have already counted the cardinality so
+ // we should not see -1s here.
+ if centroidID == -1 {
+ centroidsToProbe = i
+ // reslice to only valid centroids
+ eligibleCentroidIDs = eligibleCentroidIDs[:centroidsToProbe]
+ centroidDistances = centroidDistances[:centroidsToProbe]
+ break
+ }
eligibleVecsTillNow += clusterVectorCounts[centroidID]
+ eligibleCentroidsTillNow = i + 1
// Stop once we've examined at least 'nprobe' centroids and
// collected at least 'k' vectors.
- if eligibleVecsTillNow >= k && i+1 >= nprobe {
- centroidsToProbe = i + 1
+ if eligibleVecsTillNow >= k && eligibleCentroidsTillNow >= nprobe {
+ centroidsToProbe = eligibleCentroidsTillNow
break
}
}
- // Search the clusters specified by 'closestCentroidIDs' for
+ // Search the clusters specified by 'eligibleCentroidIDs' for
// vectors whose IDs are present in 'vectorIDsToInclude'
rs, err := v.searchClustersFromIVFIndex(
- ids, include, closestCentroidIDs, centroidsToProbe,
+ ids, include, eligibleCentroidIDs, centroidsToProbe,
k, qVector, centroidDistances, params)
if err != nil {
return nil, err
@@ -356,7 +414,16 @@ func (v *vectorIndexWrapper) searchWithoutIDs(qVector []float32, k int64, exclud
resultSet, error) {
return v.docSearch(k, v.sb.numDocs,
func() ([]float32, []int64, error) {
- return v.vecIndex.SearchWithoutIDs(qVector, k, exclude, params)
+ var sel faiss.Selector
+ var err error
+ if len(exclude) > 0 {
+ sel, err = v.getSelector(exclude, false)
+ if err != nil {
+ return nil, nil, err
+ }
+ defer sel.Delete()
+ }
+ return v.vecIndex.SearchWithOptions(qVector, k, sel, params)
},
func(numIter int, labels []int64) bool {
// if this is the first loop iteration and we have < k unique docIDs,
@@ -384,7 +451,15 @@ func (v *vectorIndexWrapper) searchWithIDs(qVector []float32, k int64, include [
var includeSet map[int64]struct{}
return v.docSearch(k, v.sb.numDocs,
func() ([]float32, []int64, error) {
- return v.vecIndex.SearchWithIDs(qVector, k, include, params)
+ // build the selector based on whatever ids is as of now
+ selector, err := v.getSelector(include, true)
+ if err != nil {
+ return nil, nil, err
+ }
+ // once the main search is done we must free the selector
+ defer selector.Delete()
+
+ return v.vecIndex.SearchWithOptions(qVector, k, selector, params)
},
func(numIter int, labels []int64) bool {
// if this is the first loop iteration and we have < k unique docIDs,
@@ -441,8 +516,8 @@ func (v *vectorIndexWrapper) searchClustersFromIVFIndex(ids []int64, include boo
}
// once the main search is done we must free the selector
defer selector.Delete()
- return v.vecIndex.SearchClustersFromIVFIndex(selector, eligibleCentroidIDs,
- centroidsToProbe, k, x, centroidDis, params)
+ return v.vecIndex.SearchClustersFromIVFIndex(eligibleCentroidIDs, centroidDis,
+ centroidsToProbe, x, k, selector, params)
},
func(numIter int, labels []int64) bool {
// if this is the first loop iteration and we have < k unique docIDs,
@@ -463,12 +538,12 @@ func (v *vectorIndexWrapper) searchClustersFromIVFIndex(ids []int64, include boo
// and still have not found enough unique docIDs, we increase
// the number of centroids to probe for the next iteration
// to try and find more vectors/documents
- if numIter >= nprobeIncreaseThreshold && centroidsToProbe < len(eligibleCentroidIDs) {
+ if numIter >= nprobeIncreaseThreshold && centroidsToProbe < totalEligibleCentroids {
// Calculate how much to increase: increase by 50% of the remaining centroids to probe,
// but at least by 1 to ensure progress.
increaseAmount := max((totalEligibleCentroids-centroidsToProbe)/2, 1)
// Update centroidsToProbe, ensuring it does not exceed the total eligible centroids
- centroidsToProbe = min(centroidsToProbe+increaseAmount, len(eligibleCentroidIDs))
+ centroidsToProbe = min(centroidsToProbe+increaseAmount, totalEligibleCentroids)
}
// prepare the exclude/include list for the next iteration
if include {
@@ -502,7 +577,7 @@ func (v *vectorIndexWrapper) getSelector(ids []int64, include bool) (selector fa
if include {
selector, err = faiss.NewIDSelectorBatch(ids)
} else {
- selector, err = faiss.NewIDSelectorNot(ids)
+ selector, err = faiss.NewIDSelectorBatchNot(ids)
}
if err != nil {
return nil, err
diff --git a/vendor/github.com/blevesearch/zapx/v16/merge.go b/vendor/github.com/blevesearch/zapx/v16/merge.go
index 6197af1178..69b827f984 100644
--- a/vendor/github.com/blevesearch/zapx/v16/merge.go
+++ b/vendor/github.com/blevesearch/zapx/v16/merge.go
@@ -36,9 +36,21 @@ const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
// Merge takes a slice of segments and bit masks describing which
// documents may be dropped, and creates a new segment containing the
// remaining data. This new segment is built at the specified path.
-func (*ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
closeCh chan struct{}, s seg.StatsReporter) (
[][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
segmentBases := make([]*SegmentBase, len(segments))
for segmenti, segment := range segments {
switch segmentx := segment.(type) {
diff --git a/vendor/github.com/blevesearch/zapx/v16/new.go b/vendor/github.com/blevesearch/zapx/v16/new.go
index c99b933d7b..41fcd23af6 100644
--- a/vendor/github.com/blevesearch/zapx/v16/new.go
+++ b/vendor/github.com/blevesearch/zapx/v16/new.go
@@ -42,11 +42,16 @@ var ValidateDocFields = func(field index.Field) error {
// New creates an in-memory zap-encoded SegmentBase from a set of Documents
func (z *ZapPlugin) New(results []index.Document) (
segment.Segment, uint64, error) {
- return z.newWithChunkMode(results, DefaultChunkMode)
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
}
func (*ZapPlugin) newWithChunkMode(results []index.Document,
- chunkMode uint32) (segment.Segment, uint64, error) {
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
s := interimPool.Get().(*interim)
var br bytes.Buffer
@@ -72,7 +77,7 @@ func (*ZapPlugin) newWithChunkMode(results []index.Document,
}
sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
- uint64(len(results)), storedIndexOffset, sectionsIndexOffset)
+ uint64(len(results)), storedIndexOffset, sectionsIndexOffset, config)
// get the bytes written before the interim's reset() call
// write it to the newly formed segment base.
diff --git a/vendor/github.com/blevesearch/zapx/v16/segment.go b/vendor/github.com/blevesearch/zapx/v16/segment.go
index 461fdf5add..2757b66750 100644
--- a/vendor/github.com/blevesearch/zapx/v16/segment.go
+++ b/vendor/github.com/blevesearch/zapx/v16/segment.go
@@ -39,8 +39,18 @@ func init() {
reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
}
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
// Open returns a zap impl of a segment
-func (*ZapPlugin) Open(path string) (segment.Segment, error) {
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
@@ -59,6 +69,7 @@ func (*ZapPlugin) Open(path string) (segment.Segment, error) {
vecIndexCache: newVectorIndexCache(),
synIndexCache: newSynonymIndexCache(),
fieldDvReaders: make([]map[uint16]*docValueReader, len(segmentSections)),
+ config: config,
},
f: f,
mm: mm,
@@ -111,6 +122,7 @@ type SegmentBase struct {
size uint64
updatedFields map[string]*index.UpdateFieldInfo
+ config map[string]interface{} // config for the segment
m sync.Mutex
fieldFSTs map[uint16]*vellum.FST
diff --git a/vendor/github.com/blevesearch/zapx/v17/.gitignore b/vendor/github.com/blevesearch/zapx/v17/.gitignore
new file mode 100644
index 0000000000..46d1cfad54
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/.gitignore
@@ -0,0 +1,12 @@
+#*
+*.sublime-*
+*~
+.#*
+.project
+.settings
+**/.idea/
+**/*.iml
+.DS_Store
+/cmd/zap/zap
+*.test
+tags
diff --git a/vendor/github.com/blevesearch/zapx/v17/LICENSE b/vendor/github.com/blevesearch/zapx/v17/LICENSE
new file mode 100644
index 0000000000..7a4a3ea242
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
\ No newline at end of file
diff --git a/vendor/github.com/blevesearch/zapx/v17/README.md b/vendor/github.com/blevesearch/zapx/v17/README.md
new file mode 100644
index 0000000000..4cbf1a145b
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/README.md
@@ -0,0 +1,163 @@
+# zapx file format
+
+The zapx module is fork of [zap](https://github.com/blevesearch/zap) module which maintains file format compatibility, but removes dependency on bleve, and instead depends only on the indepenent interface modules:
+
+- [bleve_index_api](https://github.com/blevesearch/scorch_segment_api)
+- [scorch_segment_api](https://github.com/blevesearch/scorch_segment_api)
+
+Advanced ZAP File Format Documentation is [here](zap.md).
+
+The file is written in the reverse order that we typically access data. This helps us write in one pass since later sections of the file require file offsets of things we've already written.
+
+Current usage:
+
+- mmap the entire file
+- crc-32 bytes and version are in fixed position at end of the file
+- reading remainder of footer could be version specific
+- remainder of footer gives us:
+ - 3 important offsets (docValue , fields index and stored data index)
+ - 2 important values (number of docs and chunk factor)
+- field data is processed once and memoized onto the heap so that we never have to go back to disk for it
+- access to stored data by doc number means first navigating to the stored data index, then accessing a fixed position offset into that slice, which gives us the actual address of the data. the first bytes of that section tell us the size of data so that we know where it ends.
+- access to all other indexed data follows the following pattern:
+ - first know the field name -> convert to id
+ - next navigate to term dictionary for that field
+ - some operations stop here and do dictionary ops
+ - next use dictionary to navigate to posting list for a specific term
+ - walk posting list
+ - if necessary, walk posting details as we go
+ - if location info is desired, consult location bitmap to see if it is there
+
+## stored fields section
+
+- for each document
+ - preparation phase:
+ - produce a slice of metadata bytes and data bytes
+ - produce these slices in field id order
+ - field value is appended to the data slice
+ - metadata slice is varint encoded with the following values for each field value
+ - field id (uint16)
+ - field type (byte)
+ - field value start offset in uncompressed data slice (uint64)
+ - field value length (uint64)
+ - field number of array positions (uint64)
+ - one additional value for each array position (uint64)
+ - compress the data slice using snappy
+ - file writing phase:
+ - remember the start offset for this document
+ - write out meta data length (varint uint64)
+ - write out compressed data length (varint uint64)
+ - write out the metadata bytes
+ - write out the compressed data bytes
+
+## stored fields idx
+
+- for each document
+ - write start offset (remembered from previous section) of stored data (big endian uint64)
+
+With this index and a known document number, we have direct access to all the stored field data.
+
+## posting details (freq/norm) section
+
+- for each posting list
+ - produce a slice containing multiple consecutive chunks (each chunk is varint stream)
+ - produce a slice remembering offsets of where each chunk starts
+ - preparation phase:
+ - for each hit in the posting list
+ - if this hit is in next chunk close out encoding of last chunk and record offset start of next
+ - encode term frequency (uint64)
+ - encode norm factor (float32)
+ - file writing phase:
+ - remember start position for this posting list details
+ - write out number of chunks that follow (varint uint64)
+ - write out length of each chunk (each a varint uint64)
+ - write out the byte slice containing all the chunk data
+
+If you know the doc number you're interested in, this format lets you jump to the correct chunk (docNum/chunkFactor) directly and then seek within that chunk until you find it.
+
+## posting details (location) section
+
+- for each posting list
+ - produce a slice containing multiple consecutive chunks (each chunk is varint stream)
+ - produce a slice remembering offsets of where each chunk starts
+ - preparation phase:
+ - for each hit in the posting list
+ - if this hit is in next chunk close out encoding of last chunk and record offset start of next
+ - encode field (uint16)
+ - encode field pos (uint64)
+ - encode field start (uint64)
+ - encode field end (uint64)
+ - encode number of array positions to follow (uint64)
+ - encode each array position (each uint64)
+ - file writing phase:
+ - remember start position for this posting list details
+ - write out number of chunks that follow (varint uint64)
+ - write out length of each chunk (each a varint uint64)
+ - write out the byte slice containing all the chunk data
+
+If you know the doc number you're interested in, this format lets you jump to the correct chunk (docNum/chunkFactor) directly and then seek within that chunk until you find it.
+
+## postings list section
+
+- for each posting list
+ - preparation phase:
+ - encode roaring bitmap posting list to bytes (so we know the length)
+ - file writing phase:
+ - remember the start position for this posting list
+ - write freq/norm details offset (remembered from previous, as varint uint64)
+ - write location details offset (remembered from previous, as varint uint64)
+ - write length of encoded roaring bitmap
+ - write the serialized roaring bitmap data
+
+## dictionary
+
+- for each field
+ - preparation phase:
+ - encode vellum FST with dictionary data pointing to file offset of posting list (remembered from previous)
+ - file writing phase:
+ - remember the start position of this persistDictionary
+ - write length of vellum data (varint uint64)
+ - write out vellum data
+
+## fields section
+
+- for each field
+ - file writing phase:
+ - remember start offset for each field
+ - write dictionary address (remembered from previous) (varint uint64)
+ - write length of field name (varint uint64)
+ - write field name bytes
+
+## fields idx
+
+- for each field
+ - file writing phase:
+ - write big endian uint64 of start offset for each field
+
+NOTE: currently we don't know or record the length of this fields index. Instead we rely on the fact that we know it immediately precedes a footer of known size.
+
+## fields DocValue
+
+- for each field
+ - preparation phase:
+ - produce a slice containing multiple consecutive chunks, where each chunk is composed of a meta section followed by compressed columnar field data
+ - produce a slice remembering the length of each chunk
+ - file writing phase:
+ - remember the start position of this first field DocValue offset in the footer
+ - write out number of chunks that follow (varint uint64)
+ - write out length of each chunk (each a varint uint64)
+ - write out the byte slice containing all the chunk data
+
+NOTE: currently the meta header inside each chunk gives clue to the location offsets and size of the data pertaining to a given docID and any
+read operation leverage that meta information to extract the document specific data from the file.
+
+## footer
+
+- file writing phase
+ - write number of docs (big endian uint64)
+ - write stored field index location (big endian uint64)
+ - write field index location (big endian uint64)
+ - write field docValue location (big endian uint64)
+ - write out chunk factor (big endian uint32)
+ - write out version (big endian uint32)
+ - write out file CRC of everything preceding this (big endian uint32)
diff --git a/vendor/github.com/blevesearch/zapx/v17/build.go b/vendor/github.com/blevesearch/zapx/v17/build.go
new file mode 100644
index 0000000000..f04cae0baf
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/build.go
@@ -0,0 +1,239 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "math"
+ "os"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+const Version uint32 = 17
+
+const Type string = "zap"
+
+const fieldNotUninverted uint64 = math.MaxUint64
+
+func (sb *SegmentBase) Persist(path string) error {
+ return PersistSegmentBase(sb, path)
+}
+
+// WriteTo is an implementation of io.WriterTo interface.
+func (sb *SegmentBase) WriteTo(w io.Writer) (int64, error) {
+ if w == nil {
+ return 0, fmt.Errorf("invalid writer found")
+ }
+
+ n, err := persistSegmentBaseToWriter(sb, w)
+ return int64(n), err
+}
+
+// PersistSegmentBase persists SegmentBase in the zap file format.
+func PersistSegmentBase(sb *SegmentBase, path string) error {
+ // since in-memory data is not processed by any writer callback,
+ // check with the latest writer to see if data needs to be processed
+ writer, err := NewFileWriter(nil, []byte(path))
+ if err != nil {
+ return err
+ }
+ if writer.id != sb.fileReader.id {
+ // rewrite the segment base with the latest writer callback;
+ // the rewrite will persist the segment to the given path (upon
+ // success), so we should return early to avoid overwriting again.
+ return rewriteSegmentBase(sb, path)
+ }
+
+ flag := os.O_RDWR | os.O_CREATE
+
+ f, err := os.OpenFile(path, flag, 0600)
+ if err != nil {
+ return err
+ }
+
+ cleanup := func() {
+ _ = f.Close()
+ _ = os.Remove(path)
+ }
+
+ _, err = persistSegmentBaseToWriter(sb, f)
+ if err != nil {
+ cleanup()
+ return err
+ }
+
+ err = f.Sync()
+ if err != nil {
+ cleanup()
+ return err
+ }
+
+ err = f.Close()
+ if err != nil {
+ cleanup()
+ return err
+ }
+
+ return err
+}
+
+// rewrites the segment base with the latest writer callback by leveraging
+// the merge path
+func rewriteSegmentBase(sb *SegmentBase, path string) error {
+ closeCh := make(chan struct{})
+ defer close(closeCh)
+ _, _, err := mergeSegmentBases([]*SegmentBase{sb}, []*roaring.Bitmap{nil},
+ path, DefaultChunkMode, closeCh, nil, nil)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+type bufWriter struct {
+ w *bufio.Writer
+ n int
+}
+
+func (br *bufWriter) Write(in []byte) (int, error) {
+ n, err := br.w.Write(in)
+ br.n += n
+ return n, err
+}
+
+func persistSegmentBaseToWriter(sb *SegmentBase, w io.Writer) (int, error) {
+ br := &bufWriter{w: bufio.NewWriter(w)}
+
+ _, err := br.Write(sb.mem)
+ if err != nil {
+ return 0, err
+ }
+
+ err = persistFooter(sb.numDocs, sb.storedIndexOffset, sb.sectionsIndexOffset,
+ sb.chunkMode, sb.memCRC, br, sb.fileReader.id)
+ if err != nil {
+ return 0, err
+ }
+
+ err = br.w.Flush()
+ if err != nil {
+ return 0, err
+ }
+
+ return br.n, nil
+}
+
+func persistStoredFieldValues(fieldID int,
+ storedFieldValues [][]byte, stf []byte, spf [][]uint64,
+ curr int, metaEncode varintEncoder, data []byte) (
+ int, []byte, error) {
+ for i := 0; i < len(storedFieldValues); i++ {
+ // encode field
+ _, err := metaEncode(uint64(fieldID))
+ if err != nil {
+ return 0, nil, err
+ }
+ // encode type
+ _, err = metaEncode(uint64(stf[i]))
+ if err != nil {
+ return 0, nil, err
+ }
+ // encode start offset
+ _, err = metaEncode(uint64(curr))
+ if err != nil {
+ return 0, nil, err
+ }
+ // end len
+ _, err = metaEncode(uint64(len(storedFieldValues[i])))
+ if err != nil {
+ return 0, nil, err
+ }
+ // encode number of array pos
+ _, err = metaEncode(uint64(len(spf[i])))
+ if err != nil {
+ return 0, nil, err
+ }
+ // encode all array positions
+ for _, pos := range spf[i] {
+ _, err = metaEncode(pos)
+ if err != nil {
+ return 0, nil, err
+ }
+ }
+
+ data = append(data, storedFieldValues[i]...)
+ curr += len(storedFieldValues[i])
+ }
+
+ return curr, data, nil
+}
+
+func InitSegmentBase(mem []byte, memCRC uint32, chunkMode uint32, numDocs uint64,
+ storedIndexOffset uint64, sectionsIndexOffset uint64,
+ config map[string]interface{}) (*SegmentBase, error) {
+ sb := &SegmentBase{
+ mem: mem,
+ memCRC: memCRC,
+ chunkMode: chunkMode,
+ numDocs: numDocs,
+ storedIndexOffset: storedIndexOffset,
+ sectionsIndexOffset: sectionsIndexOffset,
+ fieldDvReaders: make([][]*docValueReader, len(segmentSections)),
+ updatedFields: make(map[string]*index.UpdateFieldInfo),
+ invIndexCache: newInvertedIndexCache(),
+ vecIndexCache: newVectorIndexCache(),
+ synIndexCache: newSynonymIndexCache(),
+ nstIndexCache: newNestedIndexCache(),
+ // following fields gets populated by loadFields
+ fieldsMap: make(map[string]uint16),
+ fieldsOptions: make(map[string]index.FieldIndexingOptions),
+ fieldsInv: make([]string, 0),
+ config: config,
+ }
+ sb.updateSize()
+
+ // initialize the file reader with an empty callback
+ // since the data is not yet persisted, the data has also
+ // not been processed by any writer callback
+ fileReader, err := NewFileReader("", nil)
+ if err != nil {
+ return nil, err
+ }
+ sb.fileReader = fileReader
+
+ // load the data/section starting offsets for each field
+ // by via the sectionsIndexOffset as starting point.
+ err = sb.loadFields()
+ if err != nil {
+ return nil, err
+ }
+
+ err = sb.loadDvReaders()
+ if err != nil {
+ return nil, err
+ }
+
+ // initialize any of the caches if needed
+ err = sb.nstIndexCache.initialize(sb.numDocs, sb.getEdgeListOffset(), sb.mem)
+ if err != nil {
+ return nil, err
+ }
+
+ return sb, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/centroid_index.go b/vendor/github.com/blevesearch/zapx/v17/centroid_index.go
new file mode 100644
index 0000000000..cdec662e22
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/centroid_index.go
@@ -0,0 +1,82 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+
+ faiss "github.com/blevesearch/go-faiss"
+)
+
+func (sb *SegmentBase) GetCoarseQuantizer(field string) (interface{}, error) {
+ fieldIDPlus1 := sb.fieldsMap[field]
+ if fieldIDPlus1 <= 0 {
+ return nil, fmt.Errorf("field %s does not exist in segment", field)
+ }
+
+ vectorSection := sb.fieldsSectionsMap[fieldIDPlus1-1][SectionFaissVectorIndex]
+ // check if the field has a vector section in the segment.
+ if vectorSection <= 0 {
+ return nil, fmt.Errorf("field %s does not have a vector section in the segment", field)
+ }
+
+ pos := int(vectorSection)
+ // doc values and vector optimization type
+ for i := 0; i < 3; i++ {
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ }
+
+ numVecs, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+
+ // length of the vector to docID map
+ _, n = binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+
+ // vector to docID mapping
+ for i := 0; i < int(numVecs); i++ {
+ _, n = binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ }
+
+ // type of index
+ indexType, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ indexSize, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+
+ // todo: might wanna use the vector cache here, early tests didn't show a big diff
+ faissIndex, err := faiss.ReadIndexFromBuffer(sb.mem[pos:pos+int(indexSize)], faissIOFlags)
+ if err != nil {
+ return nil, err
+ }
+ pos += int(indexSize)
+
+ if faissIndexType(indexType) == faissBIVFIndex {
+ binaryIndexSize, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ binaryIndex, err := faiss.ReadBinaryIndexFromBuffer(sb.mem[pos:pos+int(binaryIndexSize)], faissIOFlags)
+ if err != nil {
+ return nil, err
+ }
+ return newFaissBinaryIndex(binaryIndex, faissIndex)
+ }
+ return newFaissFloat32Index(faissIndex)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/chunk.go b/vendor/github.com/blevesearch/zapx/v17/chunk.go
new file mode 100644
index 0000000000..53d124f063
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/chunk.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2019 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "errors"
+ "fmt"
+)
+
+// LegacyChunkMode was the original chunk mode (always chunk size 1024)
+// this mode is still used for chunking doc values.
+var LegacyChunkMode uint32 = 1024
+
+// DefaultChunkMode is the most recent improvement to chunking and should
+// be used by default.
+var DefaultChunkMode uint32 = 1026
+
+var ErrChunkSizeZero = errors.New("chunk size is zero")
+
+// getChunkSize returns the chunk size for the given chunkMode, cardinality, and
+// maxDocs.
+//
+// In error cases, the returned chunk size will be 0. Caller can differentiate
+// between a valid chunk size of 0 and an error by checking for ErrChunkSizeZero.
+func getChunkSize(chunkMode uint32, cardinality uint64, maxDocs uint64) (uint64, error) {
+ switch {
+ case chunkMode == 0:
+ return 0, ErrChunkSizeZero
+
+ // any chunkMode <= 1024 will always chunk with chunkSize=chunkMode
+ case chunkMode <= 1024:
+ // legacy chunk size
+ return uint64(chunkMode), nil
+
+ case chunkMode == 1025:
+ // attempt at simple improvement
+ // theory - the point of chunking is to put a bound on the maximum number of
+ // calls to Next() needed to find a random document. ie, you should be able
+ // to do one jump to the correct chunk, and then walk through at most
+ // chunk-size items
+ // previously 1024 was chosen as the chunk size, but this is particularly
+ // wasteful for low cardinality terms. the observation is that if there
+ // are less than 1024 items, why not put them all in one chunk,
+ // this way you'll still achieve the same goal of visiting at most
+ // chunk-size items.
+ // no attempt is made to tweak any other case
+ if cardinality <= 1024 {
+ if maxDocs == 0 {
+ return 0, ErrChunkSizeZero
+ }
+ return maxDocs, nil
+ }
+ return 1024, nil
+
+ case chunkMode == 1026:
+ // improve upon the ideas tested in chunkMode 1025
+ // the observation that the fewest number of dense chunks is the most
+ // desirable layout, given the built-in assumptions of chunking
+ // (that we want to put an upper-bound on the number of items you must
+ // walk over without skipping, currently tuned to 1024)
+ //
+ // 1. compute the number of chunks needed (max 1024/chunk)
+ // 2. convert to chunkSize, dividing into maxDocs
+ numChunks := (cardinality / 1024) + 1
+ chunkSize := maxDocs / numChunks
+ if chunkSize == 0 {
+ return 0, ErrChunkSizeZero
+ }
+ return chunkSize, nil
+ }
+ return 0, fmt.Errorf("unknown chunk mode %d", chunkMode)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/contentcoder.go b/vendor/github.com/blevesearch/zapx/v17/contentcoder.go
new file mode 100644
index 0000000000..65c784ca40
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/contentcoder.go
@@ -0,0 +1,281 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "io"
+ "reflect"
+
+ "github.com/golang/snappy"
+)
+
+var reflectStaticSizeMetaData int
+
+func init() {
+ var md MetaData
+ reflectStaticSizeMetaData = int(reflect.TypeOf(md).Size())
+}
+
+type chunkedContentCoder struct {
+ bytesWritten uint64 // moved to top to correct alignment issues on ARM, 386 and 32-bit MIPS.
+
+ final []byte
+ chunkSize uint64
+ currChunk uint64
+ chunkLens []uint64
+
+ compressed []byte // temp buf for snappy compression
+
+ w io.Writer
+ progressiveWrite bool
+ skipCompression bool
+
+ chunkMeta []MetaData
+ chunkMetaBuf bytes.Buffer
+ chunkBuf bytes.Buffer
+}
+
+// MetaData represents the data information inside a
+// chunk.
+type MetaData struct {
+ DocNum uint64 // docNum of the data inside the chunk
+ DocDvOffset uint64 // offset of data inside the chunk for the given docid
+}
+
+// newChunkedContentCoder returns a new chunk content coder which
+// packs data into chunks based on the provided chunkSize
+func newChunkedContentCoder(chunkSize uint64, maxDocNum uint64,
+ w io.Writer, progressiveWrite bool, skipCompression bool,
+) *chunkedContentCoder {
+ total := maxDocNum/chunkSize + 1
+ rv := &chunkedContentCoder{
+ chunkSize: chunkSize,
+ chunkLens: make([]uint64, total),
+ chunkMeta: make([]MetaData, 0, total),
+ w: w,
+ progressiveWrite: progressiveWrite,
+ skipCompression: skipCompression,
+ }
+
+ return rv
+}
+
+// Reset lets you reuse this chunked content coder. Buffers are reset
+// and re used. You cannot change the chunk size.
+func (c *chunkedContentCoder) Reset() {
+ c.currChunk = 0
+ c.bytesWritten = 0
+ c.final = c.final[:0]
+ c.chunkBuf.Reset()
+ c.chunkMetaBuf.Reset()
+ for i := range c.chunkLens {
+ c.chunkLens[i] = 0
+ }
+ c.chunkMeta = c.chunkMeta[:0]
+}
+
+func (c *chunkedContentCoder) SetChunkSize(chunkSize uint64, maxDocNum uint64) {
+ total := int(maxDocNum/chunkSize + 1)
+ c.chunkSize = chunkSize
+ if cap(c.chunkLens) < total {
+ c.chunkLens = make([]uint64, total)
+ } else {
+ c.chunkLens = c.chunkLens[:total]
+ }
+ if cap(c.chunkMeta) < total {
+ c.chunkMeta = make([]MetaData, 0, total)
+ }
+}
+
+// Close indicates you are done calling Add() this allows
+// the final chunk to be encoded.
+func (c *chunkedContentCoder) Close() error {
+ return c.flushContents()
+}
+
+func (c *chunkedContentCoder) incrementBytesWritten(val uint64) {
+ c.bytesWritten += val
+}
+
+func (c *chunkedContentCoder) getBytesWritten() uint64 {
+ return c.bytesWritten
+}
+
+func (c *chunkedContentCoder) writeChunkMeta() ([]byte, error) {
+
+ // flush the contents, with meta information at first
+ buf := make([]byte, binary.MaxVarintLen64)
+ var metaData []byte
+ n := binary.PutUvarint(buf, uint64(len(c.chunkMeta)))
+ _, err := c.chunkMetaBuf.Write(buf[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ // write out the metaData slice
+ for _, meta := range c.chunkMeta {
+ _, err := writeUvarints(&c.chunkMetaBuf, meta.DocNum, meta.DocDvOffset)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // write the metadata to final data
+ metaData = c.chunkMetaBuf.Bytes()
+ c.final = append(c.final, metaData...)
+
+ return metaData, nil
+}
+
+func (c *chunkedContentCoder) flushContents() error {
+ var metaData []byte
+ var err error
+ // Meta data is only needed if we have more than 1 doc in the chunk,
+ // otherwise we can just write the doc value directly
+ if c.chunkSize != 1 {
+ metaData, err = c.writeChunkMeta()
+ if err != nil {
+ return err
+ }
+ }
+
+ // write the compressed data to the final data
+ if c.skipCompression {
+ c.compressed = c.chunkBuf.Bytes()
+ } else {
+ c.compressed = snappy.Encode(c.compressed[:cap(c.compressed)], c.chunkBuf.Bytes())
+ }
+
+ // process the compressed data using the callback
+ if fw, ok := c.w.(*FileWriter); ok && fw != nil {
+ c.compressed = fw.process(c.compressed)
+ }
+
+ c.incrementBytesWritten(uint64(len(c.compressed)))
+ c.final = append(c.final, c.compressed...)
+
+ c.chunkLens[c.currChunk] = uint64(len(c.compressed) + len(metaData))
+
+ if c.progressiveWrite {
+ _, err := c.w.Write(c.final)
+ if err != nil {
+ return err
+ }
+ c.final = c.final[:0]
+ }
+
+ return nil
+}
+
+// Add encodes the provided byte slice into the correct chunk for the provided
+// doc num. You MUST call Add() with increasing docNums.
+func (c *chunkedContentCoder) Add(docNum uint64, vals []byte) error {
+ chunk := docNum / c.chunkSize
+ if chunk != c.currChunk {
+ // flush out the previous chunk details
+ err := c.flushContents()
+ if err != nil {
+ return err
+ }
+ // clearing the chunk specific meta for next chunk
+ c.chunkBuf.Reset()
+ c.chunkMetaBuf.Reset()
+ c.chunkMeta = c.chunkMeta[:0]
+ c.currChunk = chunk
+ }
+
+ // get the starting offset for this doc
+ dvOffset := c.chunkBuf.Len()
+ dvSize, err := c.chunkBuf.Write(vals)
+ if err != nil {
+ return err
+ }
+
+ c.chunkMeta = append(c.chunkMeta, MetaData{
+ DocNum: docNum,
+ DocDvOffset: uint64(dvOffset + dvSize),
+ })
+ return nil
+}
+
+// Write commits all the encoded chunked contents to the provided writer.
+//
+// | ..... data ..... | chunk offsets (varints)
+// | position of chunk offsets (uint64) | number of offsets (uint64) |
+func (c *chunkedContentCoder) Write() (int, error) {
+ var tw int
+
+ if c.final != nil {
+ // write out the data section first
+ nw, err := c.w.Write(c.final)
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+ }
+
+ chunkOffsetsStart := uint64(tw)
+
+ if cap(c.final) < binary.MaxVarintLen64 {
+ c.final = make([]byte, binary.MaxVarintLen64)
+ } else {
+ c.final = c.final[0:binary.MaxVarintLen64]
+ }
+ chunkOffsets := modifyLengthsToEndOffsets(c.chunkLens)
+ // write out the chunk offsets
+ for _, chunkOffset := range chunkOffsets {
+ n := binary.PutUvarint(c.final, chunkOffset)
+ nw, err := c.w.Write(c.final[:n])
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+ }
+
+ chunkOffsetsLen := uint64(tw) - chunkOffsetsStart
+
+ c.final = c.final[0:8]
+ // write out the length of chunk offsets
+ binary.BigEndian.PutUint64(c.final, chunkOffsetsLen)
+ nw, err := c.w.Write(c.final)
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+
+ // write out the number of chunks
+ binary.BigEndian.PutUint64(c.final, uint64(len(c.chunkLens)))
+ nw, err = c.w.Write(c.final)
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+
+ c.final = c.final[:0]
+
+ return tw, nil
+}
+
+// ReadDocValueBoundary elicits the start, end offsets from a
+// metaData header slice
+func ReadDocValueBoundary(chunk int, metaHeaders []MetaData) (uint64, uint64) {
+ var start uint64
+ if chunk > 0 {
+ start = metaHeaders[chunk-1].DocDvOffset
+ }
+ return start, metaHeaders[chunk].DocDvOffset
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/count.go b/vendor/github.com/blevesearch/zapx/v17/count.go
new file mode 100644
index 0000000000..b6135359fb
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/count.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "hash/crc32"
+ "io"
+
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+// CountHashWriter is a wrapper around a Writer which counts the number of
+// bytes which have been written and computes a crc32 hash
+type CountHashWriter struct {
+ w io.Writer
+ crc uint32
+ n int
+ s segment.StatsReporter
+}
+
+// NewCountHashWriter returns a CountHashWriter which wraps the provided Writer
+func NewCountHashWriter(w io.Writer) *CountHashWriter {
+ return &CountHashWriter{w: w}
+}
+
+func NewCountHashWriterWithStatsReporter(w io.Writer, s segment.StatsReporter) *CountHashWriter {
+ return &CountHashWriter{w: w, s: s}
+}
+
+// Write writes the provided bytes to the wrapped writer and counts the bytes
+func (c *CountHashWriter) Write(b []byte) (int, error) {
+ n, err := c.w.Write(b)
+ c.crc = crc32.Update(c.crc, crc32.IEEETable, b[:n])
+ c.n += n
+ if c.s != nil {
+ c.s.ReportBytesWritten(uint64(n))
+ }
+ return n, err
+}
+
+// Count returns the number of bytes written
+func (c *CountHashWriter) Count() int {
+ return c.n
+}
+
+// Sum32 returns the CRC-32 hash of the content written to this writer
+func (c *CountHashWriter) Sum32() uint32 {
+ return c.crc
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/dict.go b/vendor/github.com/blevesearch/zapx/v17/dict.go
new file mode 100644
index 0000000000..5ec7e27fda
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/dict.go
@@ -0,0 +1,188 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "fmt"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/blevesearch/vellum"
+)
+
+// Dictionary is the zap representation of the term dictionary
+type Dictionary struct {
+ sb *SegmentBase
+ field string
+ fieldID uint16
+ fst *vellum.FST
+
+ fstReader *vellum.Reader
+
+ bytesRead uint64
+}
+
+// represents an immutable, empty dictionary
+var emptyDictionary = &Dictionary{}
+
+func (d *Dictionary) Cardinality() int {
+ if d.fst != nil {
+ return d.fst.Len()
+ }
+ return 0
+}
+
+// PostingsList returns the postings list for the specified term
+func (d *Dictionary) PostingsList(term []byte, except *roaring.Bitmap,
+ prealloc segment.PostingsList) (segment.PostingsList, error) {
+ var preallocPL *PostingsList
+ pl, ok := prealloc.(*PostingsList)
+ if ok && pl != nil {
+ preallocPL = pl
+ }
+ return d.postingsList(term, except, preallocPL)
+}
+
+func (d *Dictionary) postingsList(term []byte, except *roaring.Bitmap, rv *PostingsList) (*PostingsList, error) {
+ if d.fstReader == nil {
+ if rv == nil || rv == emptyPostingsList {
+ return emptyPostingsList, nil
+ }
+ return d.postingsListInit(rv, except), nil
+ }
+
+ postingsOffset, exists, err := d.fstReader.Get(term)
+
+ if err != nil {
+ return nil, fmt.Errorf("vellum err: %v", err)
+ }
+ if !exists {
+ if rv == nil || rv == emptyPostingsList {
+ return emptyPostingsList, nil
+ }
+ return d.postingsListInit(rv, except), nil
+ }
+
+ return d.postingsListFromOffset(postingsOffset, except, rv)
+}
+
+func (d *Dictionary) postingsListFromOffset(postingsOffset uint64, except *roaring.Bitmap, rv *PostingsList) (*PostingsList, error) {
+ rv = d.postingsListInit(rv, except)
+
+ err := rv.read(postingsOffset, d)
+ if err != nil {
+ return nil, err
+ }
+
+ return rv, nil
+}
+
+func (d *Dictionary) postingsListInit(rv *PostingsList, except *roaring.Bitmap) *PostingsList {
+ if rv == nil || rv == emptyPostingsList {
+ rv = &PostingsList{}
+ } else {
+ postings := rv.postings
+ if postings != nil {
+ postings.Clear()
+ }
+
+ *rv = PostingsList{} // clear the struct
+
+ rv.postings = postings
+ }
+ rv.sb = d.sb
+ rv.except = except
+ return rv
+}
+
+func (d *Dictionary) Contains(key []byte) (bool, error) {
+ if d.fst != nil {
+ return d.fst.Contains(key)
+ }
+ return false, nil
+}
+
+// AutomatonIterator returns an iterator which only visits terms
+// having the the vellum automaton and start/end key range
+func (d *Dictionary) AutomatonIterator(a segment.Automaton,
+ startKeyInclusive, endKeyExclusive []byte) segment.DictionaryIterator {
+ if d.fst != nil {
+ rv := &DictionaryIterator{
+ d: d,
+ }
+
+ itr, err := d.fst.Search(a, startKeyInclusive, endKeyExclusive)
+ if err == nil {
+ rv.itr = itr
+ } else if err != vellum.ErrIteratorDone {
+ rv.err = err
+ }
+
+ return rv
+ }
+ return emptyDictionaryIterator
+}
+
+func (d *Dictionary) incrementBytesRead(val uint64) {
+ d.bytesRead += val
+}
+
+func (d *Dictionary) BytesRead() uint64 {
+ return d.bytesRead
+}
+
+func (d *Dictionary) ResetBytesRead(val uint64) {
+ d.bytesRead = val
+}
+
+func (d *Dictionary) BytesWritten() uint64 {
+ return 0
+}
+
+// DictionaryIterator is an iterator for term dictionary
+type DictionaryIterator struct {
+ d *Dictionary
+ itr vellum.Iterator
+ err error
+ tmp PostingsList
+ entry index.DictEntry
+ omitCount bool
+}
+
+var emptyDictionaryIterator = &DictionaryIterator{}
+
+// Next returns the next entry in the dictionary
+func (i *DictionaryIterator) Next() (*index.DictEntry, error) {
+ if i.err != nil && i.err != vellum.ErrIteratorDone {
+ return nil, i.err
+ } else if i.itr == nil || i.err == vellum.ErrIteratorDone {
+ return nil, nil
+ }
+ term, postingsOffset := i.itr.Current()
+ if fitr, ok := i.itr.(vellum.FuzzyIterator); ok {
+ i.entry.EditDistance = fitr.EditDistance()
+ }
+ i.entry.Term = string(term)
+ if !i.omitCount {
+ i.err = i.tmp.read(postingsOffset, i.d)
+ if i.err != nil {
+ return nil, i.err
+ }
+ i.entry.Count = i.tmp.Count()
+ }
+ i.err = i.itr.Next()
+ return &i.entry, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/docvalues.go b/vendor/github.com/blevesearch/zapx/v17/docvalues.go
new file mode 100644
index 0000000000..688bda8926
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/docvalues.go
@@ -0,0 +1,409 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "reflect"
+ "sort"
+
+ index "github.com/blevesearch/bleve_index_api"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/golang/snappy"
+)
+
+var reflectStaticSizedocValueReader int
+
+func init() {
+ var dvi docValueReader
+ reflectStaticSizedocValueReader = int(reflect.TypeOf(dvi).Size())
+}
+
+type docNumTermsVisitor func(docNum uint64, terms []byte) error
+
+type docVisitState struct {
+ dvrs []*docValueReader
+ segment *SegmentBase
+
+ bytesRead uint64
+}
+
+// Implements the segment.DiskStatsReporter interface
+// The purpose of this implementation is to get
+// the bytes read from the disk (pertaining to the
+// docvalues) while querying.
+// the loadDvChunk retrieves the next chunk of docvalues
+// and the bytes retrieved off the disk pertaining to that
+// is accounted as well.
+func (d *docVisitState) incrementBytesRead(val uint64) {
+ d.bytesRead += val
+}
+
+func (d *docVisitState) BytesRead() uint64 {
+ return d.bytesRead
+}
+
+func (d *docVisitState) BytesWritten() uint64 {
+ return 0
+}
+
+func (d *docVisitState) ResetBytesRead(val uint64) {
+ d.bytesRead = val
+}
+
+type docValueReader struct {
+ field string
+ indexOptions index.FieldIndexingOptions
+ curChunkNum uint64
+ chunkOffsets []uint64
+ dvDataLoc uint64
+ curChunkHeader []MetaData // Only populated when chunking is enabled
+ curChunkData []byte // compressed data cache
+ uncompressed []byte // temp buf for snappy decompression
+
+ bytesRead uint64
+}
+
+func (di *docValueReader) size() int {
+ return reflectStaticSizedocValueReader + SizeOfPtr +
+ len(di.field) +
+ len(di.chunkOffsets)*SizeOfUint64 +
+ len(di.curChunkHeader)*reflectStaticSizeMetaData +
+ len(di.curChunkData)
+}
+
+func (di *docValueReader) cloneInto(rv *docValueReader) *docValueReader {
+ if rv == nil {
+ rv = &docValueReader{}
+ }
+
+ rv.field = di.field
+ rv.indexOptions = di.indexOptions
+ rv.curChunkNum = math.MaxUint64
+ rv.chunkOffsets = di.chunkOffsets // immutable, so it's sharable
+ rv.dvDataLoc = di.dvDataLoc
+ rv.curChunkHeader = rv.curChunkHeader[:0]
+ rv.curChunkData = nil
+ rv.uncompressed = rv.uncompressed[:0]
+
+ return rv
+}
+
+func (di *docValueReader) curChunkNumber() uint64 {
+ return di.curChunkNum
+}
+
+func (sb *SegmentBase) loadFieldDocValueReader(field string,
+ fieldDvLocStart, fieldDvLocEnd uint64) (*docValueReader, error) {
+ // get the docValue offset for the given fields
+ if fieldDvLocStart == fieldNotUninverted {
+ // no docValues found, nothing to do
+ return nil, nil
+ }
+
+ // read the number of chunks, and chunk offsets position
+ var numChunks, chunkOffsetsPosition uint64
+
+ if fieldDvLocEnd-fieldDvLocStart > 16 {
+ numChunks = binary.BigEndian.Uint64(sb.mem[fieldDvLocEnd-8 : fieldDvLocEnd])
+ // read the length of chunk offsets
+ chunkOffsetsLen := binary.BigEndian.Uint64(sb.mem[fieldDvLocEnd-16 : fieldDvLocEnd-8])
+ // acquire position of chunk offsets
+ chunkOffsetsPosition = (fieldDvLocEnd - 16) - chunkOffsetsLen
+
+ // 16 bytes since it corresponds to the length
+ // of chunk offsets and the position of the offsets
+ sb.incrementBytesRead(16)
+ } else {
+ return nil, fmt.Errorf("loadFieldDocValueReader: fieldDvLoc too small: %d-%d", fieldDvLocEnd, fieldDvLocStart)
+ }
+
+ fdvIter := &docValueReader{
+ curChunkNum: math.MaxUint64,
+ field: field,
+ indexOptions: sb.fieldsOptions[field],
+ chunkOffsets: make([]uint64, int(numChunks)),
+ }
+
+ // read the chunk offsets
+ var offset uint64
+ for i := 0; i < int(numChunks); i++ {
+ loc, read := binary.Uvarint(sb.mem[chunkOffsetsPosition+offset : chunkOffsetsPosition+offset+binary.MaxVarintLen64])
+ if read <= 0 {
+ return nil, fmt.Errorf("corrupted chunk offset during segment load")
+ }
+ fdvIter.chunkOffsets[i] = loc
+ offset += uint64(read)
+ }
+ sb.incrementBytesRead(offset)
+ // set the data offset
+ fdvIter.dvDataLoc = fieldDvLocStart
+ return fdvIter, nil
+}
+
+func (d *docValueReader) getBytesRead() uint64 {
+ return d.bytesRead
+}
+
+func (d *docValueReader) incrementBytesRead(val uint64) {
+ d.bytesRead += val
+}
+
+func (di *docValueReader) loadDvChunk(chunkNumber uint64, s *SegmentBase) error {
+ // advance to the chunk where the docValues
+ // reside for the given docNum
+ destChunkDataLoc, curChunkEnd := di.dvDataLoc, di.dvDataLoc
+ start, end := readChunkBoundary(int(chunkNumber), di.chunkOffsets)
+ if start >= end {
+ di.curChunkHeader = di.curChunkHeader[:0]
+ di.curChunkData = nil
+ di.curChunkNum = chunkNumber
+ di.uncompressed = di.uncompressed[:0]
+ return nil
+ }
+
+ destChunkDataLoc += start
+ curChunkEnd += end
+
+ var err error
+ // if skip chunking is enabled, each chunk has 1 document's docValues
+ if di.indexOptions.SkipDVChunking() {
+ di.curChunkData, err = s.fileReader.process(s.mem[destChunkDataLoc:curChunkEnd])
+ if err != nil {
+ return err
+ }
+ di.curChunkNum = chunkNumber
+ di.uncompressed = di.uncompressed[:0]
+ return nil
+ }
+
+ // read the number of docs reside in the chunk
+ numDocs, read := binary.Uvarint(s.mem[destChunkDataLoc : destChunkDataLoc+binary.MaxVarintLen64])
+ if read <= 0 {
+ return fmt.Errorf("failed to read the chunk")
+ }
+ chunkMetaLoc := destChunkDataLoc + uint64(read)
+ di.incrementBytesRead(uint64(read))
+ offset := uint64(0)
+ if cap(di.curChunkHeader) < int(numDocs) {
+ di.curChunkHeader = make([]MetaData, int(numDocs))
+ } else {
+ di.curChunkHeader = di.curChunkHeader[:int(numDocs)]
+ }
+ for i := 0; i < int(numDocs); i++ {
+ di.curChunkHeader[i].DocNum, read = binary.Uvarint(s.mem[chunkMetaLoc+offset : chunkMetaLoc+offset+binary.MaxVarintLen64])
+ offset += uint64(read)
+ di.curChunkHeader[i].DocDvOffset, read = binary.Uvarint(s.mem[chunkMetaLoc+offset : chunkMetaLoc+offset+binary.MaxVarintLen64])
+ offset += uint64(read)
+ }
+
+ compressedDataLoc := chunkMetaLoc + offset
+ dataLength := curChunkEnd - compressedDataLoc
+ di.incrementBytesRead(uint64(dataLength + offset))
+ di.curChunkData, err = s.fileReader.process(s.mem[compressedDataLoc : compressedDataLoc+dataLength])
+ if err != nil {
+ return err
+ }
+ di.curChunkNum = chunkNumber
+ di.uncompressed = di.uncompressed[:0]
+ return nil
+}
+
+func (di *docValueReader) iterateAllDocValues(s *SegmentBase, visitor docNumTermsVisitor) error {
+ for i := 0; i < len(di.chunkOffsets); i++ {
+ err := di.loadDvChunk(uint64(i), s)
+ if err != nil {
+ return err
+ }
+
+ // if chunkdate is missing or chunk header is missing (when chunking is enabled), ignore chunk
+ if di.curChunkData == nil || (len(di.curChunkHeader) == 0 && !di.indexOptions.SkipDVChunking()) {
+ continue
+ }
+
+ var uncompressed []byte
+ if di.indexOptions.SkipDVCompression() {
+ uncompressed = di.curChunkData
+ } else {
+ // uncompress the already loaded data
+ uncompressed, err = snappy.Decode(di.uncompressed[:cap(di.uncompressed)], di.curChunkData)
+ if err != nil {
+ return err
+ }
+ }
+ di.uncompressed = uncompressed
+
+ // if chunking is skipped, then all docValues
+ // for the chunk belong to a single docNum
+ if di.indexOptions.SkipDVCompression() {
+ err = visitor(uint64(i), uncompressed)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+
+ start := uint64(0)
+ for _, entry := range di.curChunkHeader {
+ err = visitor(entry.DocNum, uncompressed[start:entry.DocDvOffset])
+ if err != nil {
+ return err
+ }
+
+ start = entry.DocDvOffset
+ }
+ }
+
+ return nil
+}
+
+func (di *docValueReader) visitDocValues(docNum uint64,
+ visitor index.DocValueVisitor) error {
+
+ var start, end uint64
+ if di.indexOptions.SkipDVChunking() {
+ // docNum directly maps to the chunk number
+ start = 0
+ end = uint64(len(di.curChunkData))
+ } else {
+ // binary search the term locations for the docNum
+ start, end = di.getDocValueLocs(docNum)
+ if start == math.MaxUint64 || end == math.MaxUint64 || start == end {
+ return nil
+ }
+ }
+
+ var uncompressed []byte
+ var err error
+ // use the uncompressed copy if available
+ if len(di.uncompressed) > 0 {
+ uncompressed = di.uncompressed
+ } else {
+ if di.indexOptions.SkipDVCompression() {
+ uncompressed = di.curChunkData
+ } else {
+ // uncompress the already loaded data
+ uncompressed, err = snappy.Decode(di.uncompressed[:cap(di.uncompressed)], di.curChunkData)
+ if err != nil {
+ return err
+ }
+ }
+ di.uncompressed = uncompressed
+ }
+
+ // pick the terms for the given docNum
+ uncompressed = uncompressed[start:end]
+ for {
+ i := bytes.IndexByte(uncompressed, index.DocValueTermSeparator)
+ if i < 0 {
+ break
+ }
+
+ visitor(di.field, uncompressed[0:i])
+ uncompressed = uncompressed[i+1:]
+ }
+
+ return nil
+}
+
+func (di *docValueReader) getDocValueLocs(docNum uint64) (uint64, uint64) {
+ i := sort.Search(len(di.curChunkHeader), func(i int) bool {
+ return di.curChunkHeader[i].DocNum >= docNum
+ })
+ if i < len(di.curChunkHeader) && di.curChunkHeader[i].DocNum == docNum {
+ return ReadDocValueBoundary(i, di.curChunkHeader)
+ }
+ return math.MaxUint64, math.MaxUint64
+}
+
+// VisitDocValues is an implementation of the
+// DocValueVisitable interface
+func (sb *SegmentBase) VisitDocValues(localDocNum uint64, fields []string,
+ visitor index.DocValueVisitor, dvsIn segment.DocVisitState) (
+ segment.DocVisitState, error) {
+ dvs, ok := dvsIn.(*docVisitState)
+ if !ok || dvs == nil {
+ dvs = &docVisitState{}
+ } else {
+ if dvs.segment != sb {
+ dvs.segment = sb
+ dvs.dvrs = nil
+ dvs.bytesRead = 0
+ }
+ }
+
+ var initDvReaders bool
+ if dvs.dvrs == nil {
+ dvs.dvrs = make([]*docValueReader, len(sb.fieldsInv))
+ initDvReaders = true
+ }
+
+ // find the chunkNumber where the docValues are stored
+ // NOTE: doc values continue to use legacy chunk mode
+ chunkFactor, err := getChunkSize(LegacyChunkMode, 0, 0)
+ if err != nil {
+ return nil, err
+ }
+ var fieldIDPlus1, fieldID uint16
+ var dvr, dvIter *docValueReader
+ var docInChunk uint64
+ for _, field := range fields {
+ if fieldIDPlus1, ok = sb.fieldsMap[field]; !ok {
+ continue
+ }
+ fieldID = fieldIDPlus1 - 1
+
+ if sb.fieldsOptions[field].SkipDVChunking() {
+ docInChunk = localDocNum
+ } else {
+ docInChunk = localDocNum / chunkFactor
+ }
+
+ // initialize the docValueReader for the field if needed
+ if initDvReaders {
+ dvIter = sb.fieldDvReaders[SectionInvertedTextIndex][fieldID]
+ if dvIter != nil {
+ dvs.dvrs[fieldID] = dvIter.cloneInto(dvs.dvrs[fieldID])
+ }
+ }
+
+ dvr = dvs.dvrs[fieldID]
+ if dvr != nil {
+ // check if the chunk is already loaded
+ if docInChunk != dvr.curChunkNumber() {
+ err := dvr.loadDvChunk(docInChunk, sb)
+ if err != nil {
+ return dvs, err
+ }
+ dvs.ResetBytesRead(dvr.getBytesRead())
+ } else {
+ dvs.ResetBytesRead(0)
+ }
+
+ _ = dvr.visitDocValues(localDocNum, visitor)
+ }
+ }
+ return dvs, nil
+}
+
+// VisitableDocValueFields returns the list of fields with
+// persisted doc value terms ready to be visitable using the
+// VisitDocumentFieldTerms method.
+func (sb *SegmentBase) VisitableDocValueFields() ([]string, error) {
+ return sb.fieldDvNames, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/enumerator.go b/vendor/github.com/blevesearch/zapx/v17/enumerator.go
new file mode 100644
index 0000000000..972a224165
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/enumerator.go
@@ -0,0 +1,138 @@
+// Copyright (c) 2018 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+
+ "github.com/blevesearch/vellum"
+)
+
+// enumerator provides an ordered traversal of multiple vellum
+// iterators. Like JOIN of iterators, the enumerator produces a
+// sequence of (key, iteratorIndex, value) tuples, sorted by key ASC,
+// then iteratorIndex ASC, where the same key might be seen or
+// repeated across multiple child iterators.
+type enumerator struct {
+ itrs []vellum.Iterator
+ currKs [][]byte
+ currVs []uint64
+
+ lowK []byte
+ lowIdxs []int
+ lowCurr int
+}
+
+// newEnumerator returns a new enumerator over the vellum Iterators
+func newEnumerator(itrs []vellum.Iterator) (*enumerator, error) {
+ rv := &enumerator{
+ itrs: itrs,
+ currKs: make([][]byte, len(itrs)),
+ currVs: make([]uint64, len(itrs)),
+ lowIdxs: make([]int, 0, len(itrs)),
+ }
+ for i, itr := range rv.itrs {
+ rv.currKs[i], rv.currVs[i] = itr.Current()
+ }
+ rv.updateMatches(false)
+ if rv.lowK == nil && len(rv.lowIdxs) == 0 {
+ return rv, vellum.ErrIteratorDone
+ }
+ return rv, nil
+}
+
+// updateMatches maintains the low key matches based on the currKs
+func (m *enumerator) updateMatches(skipEmptyKey bool) {
+ m.lowK = nil
+ m.lowIdxs = m.lowIdxs[:0]
+ m.lowCurr = 0
+
+ for i, key := range m.currKs {
+ if (key == nil && m.currVs[i] == 0) || // in case of empty iterator
+ (len(key) == 0 && skipEmptyKey) { // skip empty keys
+ continue
+ }
+
+ cmp := bytes.Compare(key, m.lowK)
+ if cmp < 0 || len(m.lowIdxs) == 0 {
+ // reached a new low
+ m.lowK = key
+ m.lowIdxs = m.lowIdxs[:0]
+ m.lowIdxs = append(m.lowIdxs, i)
+ } else if cmp == 0 {
+ m.lowIdxs = append(m.lowIdxs, i)
+ }
+ }
+}
+
+// Current returns the enumerator's current key, iterator-index, and
+// value. If the enumerator is not pointing at a valid value (because
+// Next returned an error previously), Current will return nil,0,0.
+func (m *enumerator) Current() ([]byte, int, uint64) {
+ var i int
+ var v uint64
+ if m.lowCurr < len(m.lowIdxs) {
+ i = m.lowIdxs[m.lowCurr]
+ v = m.currVs[i]
+ }
+ return m.lowK, i, v
+}
+
+// GetLowIdxsAndValues will return all of the iterator indices
+// which point to the current key, and their corresponding
+// values. This can be used by advanced caller which may need
+// to peek into these other sets of data before processing.
+func (m *enumerator) GetLowIdxsAndValues() ([]int, []uint64) {
+ values := make([]uint64, 0, len(m.lowIdxs))
+ for _, idx := range m.lowIdxs {
+ values = append(values, m.currVs[idx])
+ }
+ return m.lowIdxs, values
+}
+
+// Next advances the enumerator to the next key/iterator/value result,
+// else vellum.ErrIteratorDone is returned.
+func (m *enumerator) Next() error {
+ m.lowCurr += 1
+ if m.lowCurr >= len(m.lowIdxs) {
+ // move all the current low iterators forwards
+ for _, vi := range m.lowIdxs {
+ err := m.itrs[vi].Next()
+ if err != nil && err != vellum.ErrIteratorDone {
+ return err
+ }
+ m.currKs[vi], m.currVs[vi] = m.itrs[vi].Current()
+ }
+ // can skip any empty keys encountered at this point
+ m.updateMatches(true)
+ }
+ if m.lowK == nil && len(m.lowIdxs) == 0 {
+ return vellum.ErrIteratorDone
+ }
+ return nil
+}
+
+// Close all the underlying Iterators. The first error, if any, will
+// be returned.
+func (m *enumerator) Close() error {
+ var rv error
+ for _, itr := range m.itrs {
+ err := itr.Close()
+ if rv == nil {
+ rv = err
+ }
+ }
+ return rv
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache.go
new file mode 100644
index 0000000000..863c105b8b
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache.go
@@ -0,0 +1,413 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ faiss "github.com/blevesearch/go-faiss"
+)
+
+// -----------------------------------------------------------------------------
+
+func newVectorIndexCache() *vectorIndexCache {
+ return &vectorIndexCache{
+ cache: make(map[uint16]*cacheEntry),
+ closeCh: make(chan struct{}),
+ }
+}
+
+type vectorIndexCache struct {
+ closeCh chan struct{}
+ m sync.RWMutex
+ cache map[uint16]*cacheEntry
+ isClosed bool
+}
+
+// Clear clears the entire vector index cache.
+func (vc *vectorIndexCache) Clear() {
+ vc.m.Lock()
+ // if already closed, no-op
+ if vc.isClosed {
+ vc.m.Unlock()
+ return
+ }
+ vc.isClosed = true
+ close(vc.closeCh)
+
+ // forcing a close on all indexes to avoid memory leaks.
+ for _, entry := range vc.cache {
+ entry.close()
+ }
+ vc.cache = nil
+ vc.m.Unlock()
+}
+
+// loadOrCreate obtains the vector index from the cache or creates it if it's not present.
+// useGPU indicates whether the field mapping requires GPU acceleration for this index.
+func (vc *vectorIndexCache) loadOrCreate(fieldID uint16, mem []byte, numDocs uint32, except *roaring.Bitmap, useGPU bool, r *FileReader) (
+ index faissIndex, mapping *idMapping, exclude *bitmap, err error) {
+ // first try to read from the cache with a read lock
+ vc.m.RLock()
+ if vc.isClosed {
+ // if cache is closed, no-op
+ vc.m.RUnlock()
+ return nil, nil, nil, nil
+ }
+ entry, ok := vc.cache[fieldID]
+ if ok {
+ vc.m.RUnlock()
+ return entry.load(except)
+ }
+ vc.m.RUnlock()
+ // cache miss, rebuild the cache entry under a write lock
+ vc.m.Lock()
+ defer vc.m.Unlock()
+ if vc.isClosed {
+ // if cache is closed, no-op
+ return nil, nil, nil, nil
+ }
+ // check again if we have the entry now
+ entry, ok = vc.cache[fieldID]
+ if ok {
+ return entry.load(except)
+ }
+ // still not present, create and cache it
+ return vc.createAndCacheLOCKED(fieldID, mem, numDocs, except, useGPU, r)
+}
+
+// Rebuilding the cache on a miss.
+func (vc *vectorIndexCache) createAndCacheLOCKED(fieldID uint16, mem []byte,
+ numDocs uint32, except *roaring.Bitmap, useGPU bool, r *FileReader) (index faissIndex,
+ mapping *idMapping, exclude *bitmap, err error) {
+ // if the cache doesn't have the entry, construct the vector to doc id map and
+ // the vector index out of the mem bytes and update the cache under lock.
+ pos := 0
+ numVecs, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return nil, nil, nil, fmt.Errorf("could not read numVecs")
+ }
+ // if no vectors or no documents, return empty cache entry
+ if numVecs == 0 || numDocs == 0 {
+ return nil, nil, nil, nil
+ }
+ pos += n
+ // read the length of the docID list
+ listLen, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return nil, nil, nil, fmt.Errorf("could not read docID list length")
+ }
+ pos += n
+ // read the entierity of the docID list through the file reader
+ buf, err := r.process(mem[pos : pos+int(listLen)])
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("could not process docID list: %v", err)
+ }
+ pos += int(listLen)
+ bufPos := 0
+ bufLen := len(buf)
+ // create a mapping using the numVecs and numDocs
+ mapping = newIDMapping(uint32(numVecs), numDocs)
+ for vecID := uint32(0); vecID < uint32(numVecs); vecID++ {
+ docID, n := binary.Uvarint(buf[bufPos:min(bufPos+binary.MaxVarintLen64, bufLen)])
+ if n <= 0 {
+ return nil, nil, nil, fmt.Errorf("could not read docID for vecID %d", vecID)
+ }
+ bufPos += n
+ mapping.add(vecID, uint32(docID))
+ }
+ // read the type of the vector index
+ indexType, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return nil, nil, nil, fmt.Errorf("could not read faiss index type")
+ }
+ pos += n
+ // read the faiss index size
+ indexSize, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return nil, nil, nil, fmt.Errorf("could not read faiss index size")
+ }
+ pos += n
+
+ // read the index bytes through the file reader
+ buf, err = r.process(mem[pos : pos+int(indexSize)])
+ if err != nil {
+ return nil, nil, nil, err
+ }
+
+ // read the serialized vector index
+ fIndex, err := faiss.ReadIndexFromBuffer(buf, faissIOFlagsReadOnly)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("faiss index load error: %v", err)
+ }
+ pos += int(indexSize)
+ if faissIndexType(indexType) == faissBIVFIndex {
+ // read the faiss binary index size
+ binSize, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ // read the index bytes through the file reader
+ buf, err = r.process(mem[pos : pos+int(binSize)])
+ if err != nil {
+ return nil, nil, nil, err
+ }
+ // read the serialized binary vector index
+ bIndex, err := faiss.ReadBinaryIndexFromBuffer(buf, faissIOFlagsReadOnly)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("faiss binary index load error: %v", err)
+ }
+ pos += int(binSize)
+ index, err = newFaissBinaryIndex(bIndex, fIndex)
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("faiss binary index creation error: %v", err)
+ }
+ } else {
+ if useGPU {
+ index, err = newFaissGPUFloat32Index(fIndex)
+ } else {
+ index, err = newFaissFloat32Index(fIndex)
+ }
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("faiss float32 index creation error: %v", err)
+ }
+ }
+ // update the cache
+ vc.insertLOCKED(fieldID, index, mapping)
+ return index, mapping, getExcludedVectors(mapping, except), nil
+}
+
+func (vc *vectorIndexCache) insertLOCKED(fieldID uint16,
+ index faissIndex, mapping *idMapping) {
+ // the first time we've hit the cache, try to spawn a monitoring routine
+ // which will reconcile the moving averages for all the fields being hit
+ if len(vc.cache) == 0 {
+ go vc.monitor()
+ }
+ // initializing the alpha with 0.4 essentially means that we are favoring
+ // the history a little bit more relative to the current sample value.
+ // this makes the average to be kept above the threshold value for a
+ // longer time and thereby the index to be resident in the cache
+ // for longer time.
+ vc.cache[fieldID] = createCacheEntry(index, mapping, 0.4)
+}
+
+func (vc *vectorIndexCache) incHit(fieldID uint16) {
+ vc.m.RLock()
+ entry, ok := vc.cache[fieldID]
+ if ok {
+ entry.incHit()
+ }
+ vc.m.RUnlock()
+}
+
+func (vc *vectorIndexCache) decRef(fieldID uint16) {
+ vc.m.RLock()
+ entry, ok := vc.cache[fieldID]
+ if ok {
+ entry.decRef()
+ }
+ vc.m.RUnlock()
+}
+
+// vectorIndexLocation describes where a cached vector index currently resides.
+type vectorIndexLocation uint8
+
+const (
+ vectorIndexNotCached vectorIndexLocation = iota // not present in the cache
+ vectorIndexInCPU // loaded in CPU memory
+ vectorIndexInGPU // loaded in GPU memory
+)
+
+// indexLocation reports where the vector index for fieldID currently resides.
+func (vc *vectorIndexCache) indexLocation(fieldID uint16) vectorIndexLocation {
+ vc.m.RLock()
+ defer vc.m.RUnlock()
+ if vc.isClosed {
+ return vectorIndexNotCached
+ }
+ entry, ok := vc.cache[fieldID]
+ if !ok {
+ return vectorIndexNotCached
+ }
+ if gpuIdx, ok := entry.index.(faissIndexGPU); ok && gpuIdx.inGPURam() {
+ return vectorIndexInGPU
+ }
+ return vectorIndexInCPU
+}
+
+func (vc *vectorIndexCache) cleanup() bool {
+ vc.m.Lock()
+ cache := vc.cache
+
+ // for every field reconcile the average with the current sample values
+ for fieldID, entry := range cache {
+ sample := atomic.LoadUint64(&entry.tracker.sample)
+ entry.tracker.add(sample)
+
+ refCount := atomic.LoadInt64(&entry.refs)
+ // the comparison threshold as of now is (1 - a). mathematically it
+ // means that there is only 1 query per second on average as per history.
+ // and in the current second, there were no queries performed against
+ // this index.
+ if entry.tracker.avg <= (1-entry.tracker.alpha) && refCount <= 0 {
+ atomic.StoreUint64(&entry.tracker.sample, 0)
+ delete(vc.cache, fieldID)
+ entry.close()
+ continue
+ }
+ atomic.StoreUint64(&entry.tracker.sample, 0)
+ }
+
+ rv := len(vc.cache) == 0
+ vc.m.Unlock()
+ return rv
+}
+
+var monitorFreq = 1 * time.Second
+
+func (vc *vectorIndexCache) monitor() {
+ ticker := time.NewTicker(monitorFreq)
+ defer ticker.Stop()
+ for {
+ select {
+ case <-vc.closeCh:
+ return
+ case <-ticker.C:
+ exit := vc.cleanup()
+ if exit {
+ // no entries to be monitored, exit
+ return
+ }
+ }
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+type ewma struct {
+ alpha float64
+ avg float64
+ // every hit to the cache entry is recorded as part of a sample
+ // which will be used to calculate the average in the next cycle of average
+ // computation (which is average traffic for the field till now). this is
+ // used to track the per second hits to the cache entries.
+ sample uint64
+}
+
+func (e *ewma) add(val uint64) {
+ if e.avg == 0.0 {
+ e.avg = float64(val)
+ } else {
+ // the exponentially weighted moving average
+ // X(t) = a.v + (1 - a).X(t-1)
+ e.avg = e.alpha*float64(val) + (1-e.alpha)*e.avg
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+func createCacheEntry(index faissIndex, mapping *idMapping, alpha float64) *cacheEntry {
+ ce := &cacheEntry{
+ index: index,
+ mapping: mapping,
+ tracker: &ewma{
+ alpha: alpha,
+ sample: 1,
+ },
+ refs: 1,
+ }
+ return ce
+}
+
+type cacheEntry struct {
+ tracker *ewma
+
+ // this is used to track the live references to the cache entry,
+ // such that while we do a cleanup() and we see that the avg is below a
+ // threshold we close/cleanup only if the live refs to the cache entry is 0.
+ refs int64
+
+ index faissIndex
+ mapping *idMapping
+}
+
+func (ce *cacheEntry) incHit() {
+ atomic.AddUint64(&ce.tracker.sample, 1)
+}
+
+func (ce *cacheEntry) addRef() {
+ atomic.AddInt64(&ce.refs, 1)
+}
+
+func (ce *cacheEntry) decRef() {
+ atomic.AddInt64(&ce.refs, -1)
+}
+
+func (ce *cacheEntry) load(except *roaring.Bitmap) (faissIndex, *idMapping, *bitmap, error) {
+ ce.incHit()
+ ce.addRef()
+ return ce.index, ce.mapping, getExcludedVectors(ce.mapping, except), nil
+}
+
+func (ce *cacheEntry) close() {
+ go func() {
+ if ce.index != nil {
+ ce.index.close()
+ }
+ ce.mapping = nil
+ }()
+}
+
+// -----------------------------------------------------------------------------
+
+func getExcludedVectors(idMap *idMapping, except *roaring.Bitmap) (exclude *bitmap) {
+ if except != nil && !except.IsEmpty() && idMap != nil {
+ numVecs := idMap.numVectors()
+ // if there are no vectors, nothing to exclude
+ if numVecs == 0 {
+ return exclude
+ }
+ // iterate over the docs present in the except bitmap to
+ // construct the vector exclude bitmap. we can guarantee that
+ // this except bitmap is immutable and derived from the segment
+ // snapshot, but the vector exclude bitmap is part of the
+ // SegmentBase's cache, because of which it is necessary to create
+ // a new vector exclude bitmap per cache load operation
+ // get an iterator over the except bitmap
+ exceptItr := except.Iterator()
+ // as we iterate over the except docIDs, get the vector IDs
+ // for those docIDs and set them in our exclude bitmap
+ for exceptItr.HasNext() {
+ docID := exceptItr.Next()
+ vecs, ok := idMap.vecsForDoc(docID)
+ if ok && len(vecs) > 0 {
+ if exclude == nil {
+ exclude = newBitmap(numVecs)
+ }
+ for _, vecID := range vecs {
+ exclude.set(vecID)
+ }
+ }
+ }
+ }
+ return exclude
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache_nosup.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache_nosup.go
new file mode 100644
index 0000000000..ff152f95c4
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_cache_nosup.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !vectors
+// +build !vectors
+
+package zap
+
+type vectorIndexCache struct {
+}
+
+func newVectorIndexCache() *vectorIndexCache {
+ return nil
+}
+
+func (v *vectorIndexCache) Clear() {}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index.go
new file mode 100644
index 0000000000..77f0104bf5
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index.go
@@ -0,0 +1,115 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/json"
+ "errors"
+
+ "github.com/blevesearch/go-faiss"
+)
+
+var (
+ errNilConfig error = errors.New("faiss index config is nil")
+ errNilIndex error = errors.New("faiss index is nil")
+ errNotSupported error = errors.New("operation not supported")
+)
+
+// Abstract interface for Faiss vector indices, which are returned by the go-faiss library.
+type faissIndex interface {
+ // adds the given vectors to the index.
+ add(vecs *vectorSet) error
+ // closes the index and releases any associated resources.
+ close()
+ // returns the dimensionality of the vectors in the index.
+ dim() int
+ // returns the metric type used by the index, which determines how distances between vectors are computed during search.
+ metricType() int
+ // ntotal returns the total number of vectors currently stored in the index.
+ ntotal() int64
+ // reconstructBatch reconstructs the original vectors for the given vector IDs in the index.
+ reconstructBatch(vecIDs []int64, prealloc []float32) ([]float32, error)
+ // performs a search on the index using the provided query vector and and retrieves the top K nearest neighbors.
+ // Optional search constraints can be applied using the selector and additional search parameters.
+ search(qVector *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error)
+ // write out the index content into the provide fileWriter using a reusable buffer
+ // returns any error encountered during the write process.
+ write(buf []byte, w *FileWriter) error
+ // returns the size of the index in bytes.
+ size() uint64
+ // -----------------------------------------------------------------
+ // casting methods to access index-specific operations below
+ // -----------------------------------------------------------------
+ // returns the underlying IVF index if this is an IVF index,
+ // and a boolean indicating whether the cast was successful.
+ castIVF() faissIndexIVF
+}
+
+// Interface for IVF-specific operations on Faiss vector indices.
+type faissIndexIVF interface {
+ faissIndex
+ // returns the count of the selected vector IDs in each
+ // cluster of the IVF index, based on the provided selector.
+ clusterVectorCounts(sel faiss.Selector, nlist int) ([]int64, error)
+ // returns the top K cardinalities (number of vectors) of the centroids in the IVF index.
+ centroidCardinalities(limit int, descending bool) ([]uint64, [][]float32, error)
+ // returns the IVF index parameters, nprobe and nlist from the ivf index.
+ ivfParams() (nprobe, nlist int)
+ // performs a search on the flat index quantizer of the IVF index, considering only the
+ // clusters selected by the centroidSelector and returns the search results.
+ searchQuantizer(qVector *vectorSet, centroidSelector faiss.Selector, centroidCount int64) ([]int64, []float32, error)
+ // performs a search on the IVF index by probing the specified clusters and returns the search results.
+ // We restrict the search to a caller-supplied set of pre-assigned clusters rather than probing internally.
+ searchClusters(eligibleCentroidIDs []int64, centroidDis []float32,
+ centroidsToProbe int, qVecSet *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error)
+ // sets the direct map type for the IVF index. The direct map is essential for
+ // reconstructing vectors based on their sequential vector IDs in future merges.
+ setDirectMap(directMapType int) error
+ // sets the number of probes (nprobe) for the IVF index. nprobe determines how many
+ // inverted lists are probed during search, and is a key parameter that controls the
+ // trade-off between search accuracy and latency.
+ setNProbe(nprobe int32)
+ // trains the IVF index on the provided training data and adds the vectors to
+ // the trained index. The training step performs k-means clustering to partition
+ // the data space, which enables efficient non-exhaustive search during query time.
+ // directMap and nprobe must be set after this call (GPU sync clears them).
+ trainAndAdd(trainingData *vectorSet, vecsToAdd *vectorSet) error
+ // sets the quantizers for the IVF index. The quantizer is a separate
+ // IVF index that is trained on the same data and used to assign vectors
+ // to clusters in the IVF index.
+ setQuantizers(trainedIndex faissIndexIVF) error
+ // returns whether the participating index is eligible for fast merge
+ isMergeable() bool
+ // merged another faiss index into the current IVF index,
+ // with an offset to adjust vector IDs from the other index.
+ mergeFrom(other faissIndex, offset int64) error
+}
+
+// faissIndexGPU is implemented by any index type that can reside in GPU memory.
+type faissIndexGPU interface {
+ // inGPURam reports whether the index is currently loaded in GPU memory.
+ inGPURam() bool
+}
+
+// Interface for batched search operations on Faiss vector indices.
+type faissQueryBatch interface {
+ // performs a batch search on the index using the provided query vector and parameters,
+ // and returns the distances and corresponding vector IDs of the top k results.
+ // NOTE: only vector search requests with the same `k` are batched together.
+ batchSearch(qVector *vectorSet, k int64) ([]float32, []int64, error)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_bivf.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_bivf.go
new file mode 100644
index 0000000000..596e19d3f6
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_bivf.go
@@ -0,0 +1,323 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "encoding/json"
+
+ index "github.com/blevesearch/bleve_index_api"
+ faiss "github.com/blevesearch/go-faiss"
+)
+
+// ---------------------------------
+// Faiss Binary IVF Index
+// ---------------------------------
+type faissBinaryIndex struct {
+ cfg *faissIndexConfig
+ backing *faiss.IndexImpl
+ binary *faiss.BinaryIndexImpl
+}
+
+func newFaissBinaryIndex(binary *faiss.BinaryIndexImpl, backing *faiss.IndexImpl) (index faissIndex, err error) {
+ // we always create this object only with valid backing and binary indexes
+ if binary == nil || backing == nil {
+ return nil, errNilIndex
+ }
+ return &faissBinaryIndex{
+ backing: backing,
+ binary: binary,
+ }, nil
+}
+
+func newFaissBinaryIndexWithConfig(binary *faiss.BinaryIndexImpl, backing *faiss.IndexImpl, cfg *faissIndexConfig) (index faissIndex, err error) {
+ if binary == nil || backing == nil {
+ return nil, errNilIndex
+ }
+ if cfg == nil {
+ return nil, errNilConfig
+ }
+
+ return &faissBinaryIndex{
+ cfg: cfg,
+ backing: backing,
+ binary: binary,
+ }, nil
+}
+
+func (b *faissBinaryIndex) add(vecs *vectorSet) error {
+ // add float data to backing index and the binary data to binary index
+ err := b.backing.Add(vecs.floatData)
+ if err != nil {
+ return err
+ }
+ return b.binary.Add(vecs.binaryData)
+}
+
+func (b *faissBinaryIndex) close() {
+ b.binary.Close()
+ b.backing.Close()
+}
+
+func (b *faissBinaryIndex) dim() int {
+ return b.binary.D()
+}
+
+func (b *faissBinaryIndex) metricType() int {
+ return b.backing.MetricType()
+}
+
+func (b *faissBinaryIndex) ntotal() int64 {
+ return b.binary.Ntotal()
+}
+
+func (b *faissBinaryIndex) reconstructBatch(vecIDs []int64, prealloc []float32) ([]float32, error) {
+ // reconstruct vectors from backing index
+ return b.backing.ReconstructBatch(vecIDs, prealloc)
+}
+
+func (b *faissBinaryIndex) search(qVector *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ // search the binary index with oversampling and then do a re-ranking on the
+ // FAISS index to get the top K results
+ // first binarize the query vector if not already done
+ qVector.binarize()
+ // search the binary index with oversampling to get a larger set of candidate binary IDs for re-ranking
+ _, binIDs, err := b.binary.SearchWithOptions(qVector.binaryData, binaryOversampleValue*k,
+ selector, params)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // use backing index for re-ranking, compute the distances/scores for the
+ // retrieved binary IDs and then get the top K results based on those distances/scores.
+ distances, err := b.backing.DistCompute(qVector.floatData, binIDs)
+ if err != nil {
+ return nil, nil, err
+ }
+ // quick select algorithm for inplace partial sorting to get top K results
+ // based on distances/scores
+ scores, labels := topNIDsByDistance(distances, binIDs, int(k))
+ return scores, labels, nil
+}
+
+func (b *faissBinaryIndex) write(buf []byte, w *FileWriter) error {
+ backingBytes, err := faiss.WriteIndexIntoBuffer(b.backing)
+ if err != nil {
+ return err
+ }
+ backingBytes = w.process(backingBytes)
+
+ // write the length of the serialized vector index bytes
+ n := binary.PutUvarint(buf, uint64(len(backingBytes)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(backingBytes)
+ if err != nil {
+ return err
+ }
+
+ binaryBytes, err := faiss.WriteBinaryIndexIntoBuffer(b.binary)
+ if err != nil {
+ return err
+ }
+ binaryBytes = w.process(binaryBytes)
+
+ // write the length of the serialized vector index bytes
+ n = binary.PutUvarint(buf, uint64(len(binaryBytes)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(binaryBytes)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (b *faissBinaryIndex) size() uint64 {
+ return b.binary.Size() + b.backing.Size()
+}
+
+// -----------------------------------------------------------------
+// casting methods to access index-specific operations below
+// -----------------------------------------------------------------
+func (b *faissBinaryIndex) castIVF() faissIndexIVF {
+ if b.binary.IsIVFIndex() {
+ // return b itself, as the IVF interface is implemented by the same
+ // struct as the non-IVF interface in go-faiss.
+ return b
+ }
+ // not an IVF index, return nil.
+ return nil
+}
+
+// -----------------------------------------------------------------
+// IVF-Index specific operations
+// -----------------------------------------------------------------
+
+func (b *faissBinaryIndex) centroidCardinalities(limit int, descending bool) ([]uint64, [][]float32, error) {
+ cardinalites, bCentroids, err := b.binary.ObtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
+ if err != nil {
+ return nil, nil, err
+ }
+ centroids := make([][]float32, len(bCentroids))
+ for i := range bCentroids {
+ centroids[i] = make([]float32, len(bCentroids[i]))
+ for j := range bCentroids[i] {
+ centroids[i][j] = float32(bCentroids[i][j])
+ }
+ }
+ return cardinalites, centroids, nil
+}
+
+func (b *faissBinaryIndex) clusterVectorCounts(sel faiss.Selector, nlist int) ([]int64, error) {
+ return b.binary.ObtainClusterVectorCountsFromIVFIndex(sel, nlist)
+}
+
+func (b *faissBinaryIndex) ivfParams() (nprobe, nlist int) {
+ return b.binary.IVFParams()
+}
+
+func (b *faissBinaryIndex) searchQuantizer(qVector *vectorSet, centroidSelector faiss.Selector, centroidCount int64) ([]int64, []float32, error) {
+ // binarize the query vector if not already done
+ qVector.binarize()
+ ids, dis, err := b.binary.ObtainClustersWithDistancesFromIVFIndex(qVector.binaryData, centroidSelector, centroidCount)
+ if err != nil {
+ return nil, nil, err
+ }
+ distances := make([]float32, len(dis))
+ for i, d := range dis {
+ distances[i] = float32(d)
+ }
+ return ids, distances, nil
+}
+
+func (b *faissBinaryIndex) searchClusters(eligibleCentroidIDs []int64, centroidDis []float32,
+ centroidsToProbe int, qVector *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ // binarize the query vector if not already done
+ qVector.binarize()
+ // convert the float distances to binary distances for the binary index search
+ binaryCentroidDis := make([]int32, len(centroidDis))
+ for i, d := range centroidDis {
+ binaryCentroidDis[i] = int32(d)
+ }
+ // search the binary index without oversampling, since we are already searching a
+ // limited number of centroids specified by centroidsToProbe
+ _, binIDs, err := b.binary.SearchClustersFromIVFIndex(eligibleCentroidIDs, binaryCentroidDis,
+ centroidsToProbe, qVector.binaryData, k, selector, params)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // use backing index for re-ranking, compute the distances/scores for the
+ // retrieved binary IDs and then get the top K results based on those distances/scores.
+ // reranking is still necessary since hamming distance has a lot of collisions
+ distances, err := b.backing.DistCompute(qVector.floatData, binIDs)
+ if err != nil {
+ return nil, nil, err
+ }
+ // quick select algorithm for inplace partial sorting to get top K results
+ // based on distances/scores
+ scores, labels := topNIDsByDistance(distances, binIDs, int(k))
+ return scores, labels, nil
+}
+
+func (b *faissBinaryIndex) setDirectMap(directMapType int) error {
+ return b.binary.SetDirectMap(directMapType)
+}
+
+func (b *faissBinaryIndex) setNProbe(nprobe int32) {
+ b.binary.SetNProbe(nprobe)
+}
+
+func (b *faissBinaryIndex) trainAndAdd(trainingData *vectorSet, vecsToAdd *vectorSet) error {
+ // train the backing index with the floatData
+ var err error
+ if b.backing.IsSQIndex() {
+ err = b.backing.Train(trainingData.floatData)
+ if err != nil {
+ return err
+ }
+ }
+
+ err = b.binary.Train(trainingData.binaryData)
+ if err != nil {
+ return err
+ }
+ return b.add(vecsToAdd)
+}
+
+func (b *faissBinaryIndex) setQuantizers(trainedIndex faissIndexIVF) error {
+ if idx, ok := trainedIndex.(*faissBinaryIndex); ok {
+ // set quantizers for the binary and the backing index if its an SQ8 index
+ var err error
+ if idx.backing.IsSQIndex() {
+ err = b.backing.SetQuantizers(idx.backing)
+ if err != nil {
+ return err
+ }
+ }
+ err = b.binary.SetQuantizers(idx.binary)
+ if err != nil {
+ return err
+ }
+ return nil
+ }
+ return errNotSupported
+}
+
+func (b *faissBinaryIndex) isMergeable() bool {
+ if b.cfg != nil {
+ switch b.cfg.optimizationType {
+ case index.IndexBIVFWithBackingFlat:
+ // the flat backing index currently doesn't support merge_from
+ return false
+ case index.IndexBIVFWithBackingSQ8:
+ return b.backing.Ntotal() > ivfThreshold
+ }
+ }
+ return false
+}
+
+func (b *faissBinaryIndex) mergeFrom(other faissIndex, offset int64) error {
+ if idx, ok := other.(*faissBinaryIndex); ok {
+ if !idx.isMergeable() {
+ return errNotSupported
+ }
+ // merge the binary and the backing index, both flat and SQ8 indexes support
+ // merge_from API underneath the hood. the add_id is kept to 0 since we will
+ // be merging the largest set of indexes which will be sequential in the list
+ // of segments being merged, so there won't be any ID conflicts.
+ err := b.backing.MergeFrom(idx.backing, 0)
+ if err != nil {
+ return err
+ }
+ err = b.binary.MergeFrom(idx.binary, offset)
+ if err != nil {
+ return err
+ }
+
+ return nil
+ }
+ return errNotSupported
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_float32.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_float32.go
new file mode 100644
index 0000000000..bfcd7267c5
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_float32.go
@@ -0,0 +1,199 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "encoding/json"
+
+ index "github.com/blevesearch/bleve_index_api"
+ faiss "github.com/blevesearch/go-faiss"
+)
+
+// ---------------------------------
+// Faiss Float32 Index
+// ---------------------------------
+type faissFloat32Index struct {
+ cfg *faissIndexConfig
+ idx *faiss.IndexImpl
+}
+
+func newFaissFloat32Index(idx *faiss.IndexImpl) (index faissIndex, err error) {
+ if idx == nil {
+ return nil, errNilIndex
+ }
+ return &faissFloat32Index{
+ idx: idx,
+ }, nil
+}
+
+func newFaissFloat32IndexWithConfig(idx *faiss.IndexImpl, cfg *faissIndexConfig) (index faissIndex, err error) {
+ if idx == nil {
+ return nil, errNilIndex
+ }
+ if cfg == nil {
+ return nil, errNilConfig
+ }
+
+ return &faissFloat32Index{
+ idx: idx,
+ cfg: cfg,
+ }, nil
+}
+
+func (f *faissFloat32Index) add(vecs *vectorSet) error {
+ return f.idx.Add(vecs.floatData)
+}
+
+func (f *faissFloat32Index) close() {
+ f.idx.Close()
+}
+
+func (f *faissFloat32Index) dim() int {
+ return f.idx.D()
+}
+
+func (f *faissFloat32Index) metricType() int {
+ return f.idx.MetricType()
+}
+
+func (f *faissFloat32Index) ntotal() int64 {
+ return f.idx.Ntotal()
+}
+
+func (f *faissFloat32Index) reconstructBatch(vecIDs []int64, prealloc []float32) ([]float32, error) {
+ return f.idx.ReconstructBatch(vecIDs, prealloc)
+}
+
+func (f *faissFloat32Index) search(qVector *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ return f.idx.SearchWithOptions(qVector.floatData, k, selector, params)
+}
+
+func (f *faissFloat32Index) write(buf []byte, w *FileWriter) error {
+ idxBytes, err := faiss.WriteIndexIntoBuffer(f.idx)
+ if err != nil {
+ return err
+ }
+ idxBytes = w.process(idxBytes)
+
+ // write the length of the serialized vector index bytes
+ n := binary.PutUvarint(buf, uint64(len(idxBytes)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(idxBytes)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (f *faissFloat32Index) size() uint64 {
+ return f.idx.Size()
+}
+
+// -----------------------------------------------------------------
+// casting methods to access index-specific operations below
+// -----------------------------------------------------------------
+func (f *faissFloat32Index) castIVF() faissIndexIVF {
+ if f.idx.IsIVFIndex() {
+ // return f itself, as the IVF interface is implemented by the same
+ // struct as the non-IVF interface in go-faiss.
+ return f
+ }
+ // not an IVF index, return nil.
+ return nil
+}
+
+// -----------------------------------------------------------------
+// IVF-Index specific operations
+// -----------------------------------------------------------------
+func (f *faissFloat32Index) clusterVectorCounts(sel faiss.Selector, nlist int) ([]int64, error) {
+ return f.idx.ObtainClusterVectorCountsFromIVFIndex(sel, nlist)
+}
+
+func (f *faissFloat32Index) centroidCardinalities(limit int, descending bool) ([]uint64, [][]float32, error) {
+ return f.idx.ObtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
+}
+
+func (f *faissFloat32Index) ivfParams() (nprobe, nlist int) {
+ return f.idx.IVFParams()
+}
+
+func (f *faissFloat32Index) searchQuantizer(qVector *vectorSet, centroidSelector faiss.Selector, centroidCount int64) ([]int64, []float32, error) {
+ return f.idx.ObtainClustersWithDistancesFromIVFIndex(qVector.floatData, centroidSelector, centroidCount)
+}
+
+func (f *faissFloat32Index) searchClusters(eligibleCentroidIDs []int64, centroidDis []float32,
+ centroidsToProbe int, qVecSet *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ return f.idx.SearchClustersFromIVFIndex(eligibleCentroidIDs, centroidDis, centroidsToProbe, qVecSet.floatData, k, selector, params)
+}
+
+func (f *faissFloat32Index) setDirectMap(directMapType int) error {
+ return f.idx.SetDirectMap(directMapType)
+}
+
+func (f *faissFloat32Index) setNProbe(nprobe int32) {
+ f.idx.SetNProbe(nprobe)
+}
+
+func (f *faissFloat32Index) trainAndAdd(trainingData *vectorSet, vecsToAdd *vectorSet) error {
+ err := f.idx.Train(trainingData.floatData)
+ if err != nil {
+ return err
+ }
+ return f.add(vecsToAdd)
+}
+
+func (f *faissFloat32Index) setQuantizers(trainedIndex faissIndexIVF) error {
+ centroidFaissIndex, ok := trainedIndex.(*faissFloat32Index)
+ if !ok {
+ // if not a float32 trained index, we cannot set it as the quantizer
+ // for the current index, return an error.
+ return errNotSupported
+ }
+ return f.idx.SetQuantizers(centroidFaissIndex.idx)
+}
+
+func (f *faissFloat32Index) isMergeable() bool {
+ if f.cfg != nil {
+ switch f.cfg.optimizationType {
+ case index.IndexOptimizedForLatency, index.IndexOptimizedForRecall:
+ return f.ntotal() > ivfSq8Threshold
+ case index.IndexOptimizedForMemoryEfficient, index.IndexIVFRaBitQ:
+ return f.ntotal() > ivfThreshold
+ default:
+ return false
+ }
+ }
+ return false
+}
+
+func (f *faissFloat32Index) mergeFrom(other faissIndex, offset int64) error {
+ otherFaissIndex, ok := other.(*faissFloat32Index)
+ if !ok {
+ return errNotSupported
+ }
+
+ if otherFaissIndex.isMergeable() {
+ return f.idx.MergeFrom(otherFaissIndex.idx, offset)
+ }
+ return errNotSupported
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_gpu_float32.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_gpu_float32.go
new file mode 100644
index 0000000000..dfba5cd1ba
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_index_gpu_float32.go
@@ -0,0 +1,304 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "encoding/json"
+ "sync/atomic"
+
+ faiss "github.com/blevesearch/go-faiss"
+)
+
+// gpuState holds all the resources related to gpu vector search,
+// the gpu index and the request batcher to the gpu
+type gpuState struct {
+ idx *faiss.GPUIndexImpl
+ batcher *requestBatcher
+}
+
+// batchSearch implements faissIndexBatch directly on gpuState, so the batcher
+// holds a reference to the index without going through the atomic pointer.
+func (gs *gpuState) batchSearch(qVector *vectorSet, k int64) ([]float32, []int64, error) {
+ return gs.idx.Search(qVector.floatData, k)
+}
+
+// ---------------------------------
+// Faiss GPU Float32 Index
+// ---------------------------------
+// faissGPUFloat32Index wraps a CPU float32 index alongside a GPU index.
+// The GPU is used for unfiltered searches (no selector), while all
+// other operations (filtered searches, IVF cluster searches, SQ/IVF
+// operations, serialization, etc.) are delegated to the CPU index.
+type faissGPUFloat32Index struct {
+ cpuIdx *faiss.IndexImpl
+
+ // doneCh is closed when initGPU completes.
+ doneCh chan struct{}
+
+ // gpu holds both the GPU index and its request batcher as a single
+ // atomic pointer; a nil load means the GPU is not yet available or has
+ // been torn down.
+ gpu atomic.Pointer[gpuState]
+}
+
+// newFaissGPUFloat32Index creates a GPU-backed float32 index. The GPU clone is
+// always performed asynchronously; search falls back to CPU until it
+// completes. All other GPU-operating methods block on doneCh before proceeding.
+func newFaissGPUFloat32Index(cpuIdx *faiss.IndexImpl) (faissIndex, error) {
+ if cpuIdx == nil {
+ return nil, errNilIndex
+ }
+ f := &faissGPUFloat32Index{
+ cpuIdx: cpuIdx,
+ doneCh: make(chan struct{}),
+ }
+ go f.initGPU()
+ return f, nil
+}
+
+// waitGPU blocks until initGPU has completed
+func (f *faissGPUFloat32Index) waitGPU() {
+ <-f.doneCh
+}
+
+// initGPU clones the CPU index to the GPU and sets up the request batcher.
+// It always closes doneCh when it returns, signalling completion to waiters.
+func (f *faissGPUFloat32Index) initGPU() {
+ defer close(f.doneCh)
+ gpuIdx, err := faiss.CloneToGPU(f.cpuIdx)
+ if err != nil || gpuIdx == nil {
+ return
+ }
+ gs := &gpuState{idx: gpuIdx}
+ gs.batcher = newRequestBatcher(gs)
+ f.gpu.Store(gs)
+}
+
+// attempt to add the vectors to the GPU index. If it fails,
+// fallback to the CPU index
+func (f *faissGPUFloat32Index) add(vecs *vectorSet) error {
+ f.waitGPU()
+ gpuState := f.gpu.Load()
+ if gpuState == nil {
+ return f.cpuIdx.Add(vecs.floatData)
+ }
+
+ err := gpuState.idx.Add(vecs.floatData)
+ if err != nil {
+ f.teardownGPU()
+ return f.cpuIdx.Add(vecs.floatData)
+ }
+
+ err = f.syncGPUToCPU()
+ if err != nil {
+ f.teardownGPU()
+ return f.cpuIdx.Add(vecs.floatData)
+ }
+
+ return nil
+}
+
+func (f *faissGPUFloat32Index) close() {
+ f.waitGPU()
+ f.teardownGPU()
+ f.cpuIdx.Close()
+}
+
+// teardownGPU stops the batcher first (while gpuIdx is still live so that
+// the final flush can complete on the GPU), then nils and closes the GPU index.
+func (f *faissGPUFloat32Index) teardownGPU() {
+ f.waitGPU()
+ // Swap to nil first so new searches fall through to CPU immediately.
+ // The batcher holds a direct reference to gpuState.idx via gpuState.batchSearch,
+ // so the final flush completes safely without touching f.gpu.
+ gpuState := f.gpu.Swap(nil)
+ if gpuState == nil {
+ return
+ }
+ gpuState.batcher.stop()
+ gpuState.idx.Close()
+}
+
+func (f *faissGPUFloat32Index) dim() int {
+ return f.cpuIdx.D()
+}
+
+func (f *faissGPUFloat32Index) metricType() int {
+ return f.cpuIdx.MetricType()
+}
+
+func (f *faissGPUFloat32Index) ntotal() int64 {
+ return f.cpuIdx.Ntotal()
+}
+
+func (f *faissGPUFloat32Index) reconstructBatch(vecIDs []int64, prealloc []float32) ([]float32, error) {
+ return f.cpuIdx.ReconstructBatch(vecIDs, prealloc)
+}
+
+func (f *faissGPUFloat32Index) search(qVector *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ if selector == nil && len(params) == 0 {
+ if gpuState := f.gpu.Load(); gpuState != nil {
+ return gpuState.batcher.search(qVector, k)
+ }
+ }
+ // GPU not ready, filtered search, or non-empty params — fall back to CPU
+ return f.cpuIdx.SearchWithOptions(qVector.floatData, k, selector, params)
+}
+
+func (f *faissGPUFloat32Index) write(buf []byte, w *FileWriter) error {
+ idxBytes, err := faiss.WriteIndexIntoBuffer(f.cpuIdx)
+ if err != nil {
+ return err
+ }
+ idxBytes = w.process(idxBytes)
+
+ // write the length of the serialized vector index bytes
+ n := binary.PutUvarint(buf, uint64(len(idxBytes)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(idxBytes)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func (f *faissGPUFloat32Index) size() uint64 {
+ return f.cpuIdx.Size()
+}
+
+// inGPURam reports if the index is currently running on the GPU.
+// returns false if the async clone is not yet done or the clone fails.
+func (f *faissGPUFloat32Index) inGPURam() bool {
+ return f.gpu.Load() != nil
+}
+
+// -----------------------------------------------------------------
+// casting methods to access index-specific operations below
+// -----------------------------------------------------------------
+func (f *faissGPUFloat32Index) castIVF() faissIndexIVF {
+ if f.cpuIdx.IsIVFIndex() {
+ return f
+ }
+ return nil
+}
+
+// -----------------------------------------------------------------
+// IVF-Index specific operations (delegate to CPU index)
+// -----------------------------------------------------------------
+func (f *faissGPUFloat32Index) clusterVectorCounts(sel faiss.Selector, nlist int) ([]int64, error) {
+ return f.cpuIdx.ObtainClusterVectorCountsFromIVFIndex(sel, nlist)
+}
+
+func (f *faissGPUFloat32Index) centroidCardinalities(limit int, descending bool) ([]uint64, [][]float32, error) {
+ return f.cpuIdx.ObtainKCentroidCardinalitiesFromIVFIndex(limit, descending)
+}
+
+func (f *faissGPUFloat32Index) ivfParams() (nprobe, nlist int) {
+ return f.cpuIdx.IVFParams()
+}
+
+func (f *faissGPUFloat32Index) searchQuantizer(qVector *vectorSet, centroidSelector faiss.Selector, centroidCount int64) ([]int64, []float32, error) {
+ return f.cpuIdx.ObtainClustersWithDistancesFromIVFIndex(qVector.floatData, centroidSelector, centroidCount)
+}
+
+func (f *faissGPUFloat32Index) searchClusters(eligibleCentroidIDs []int64, centroidDis []float32,
+ centroidsToProbe int, qVecSet *vectorSet, k int64, selector faiss.Selector, params json.RawMessage) ([]float32, []int64, error) {
+ return f.cpuIdx.SearchClustersFromIVFIndex(eligibleCentroidIDs, centroidDis, centroidsToProbe, qVecSet.floatData, k, selector, params)
+}
+
+func (f *faissGPUFloat32Index) setDirectMap(directMapType int) error {
+ return f.cpuIdx.SetDirectMap(directMapType)
+}
+
+func (f *faissGPUFloat32Index) setNProbe(nprobe int32) {
+ f.cpuIdx.SetNProbe(nprobe)
+}
+
+// attempt to train and add the vectors to the GPU index. If it fails,
+// fallback to the CPU index
+func (f *faissGPUFloat32Index) trainAndAdd(trainingData *vectorSet, vecsToAdd *vectorSet) error {
+ f.waitGPU()
+ gpuState := f.gpu.Load()
+ if gpuState == nil {
+ return f.trainAndAddCPU(trainingData, vecsToAdd)
+ }
+
+ err := gpuState.idx.Train(trainingData.floatData)
+ if err != nil {
+ f.teardownGPU()
+ return f.trainAndAddCPU(trainingData, vecsToAdd)
+ }
+
+ err = gpuState.idx.Add(vecsToAdd.floatData)
+ if err != nil {
+ f.teardownGPU()
+ return f.trainAndAddCPU(trainingData, vecsToAdd)
+ }
+
+ err = f.syncGPUToCPU()
+ if err != nil {
+ f.teardownGPU()
+ return f.trainAndAddCPU(trainingData, vecsToAdd)
+ }
+
+ return nil
+}
+
+func (f *faissGPUFloat32Index) trainAndAddCPU(trainingData *vectorSet, vecsToAdd *vectorSet) error {
+ err := f.cpuIdx.Train(trainingData.floatData)
+ if err != nil {
+ return err
+ }
+ return f.cpuIdx.Add(vecsToAdd.floatData)
+}
+
+func (f *faissGPUFloat32Index) setQuantizers(trainedIndex faissIndexIVF) error {
+ return errNotSupported
+}
+
+func (f *faissGPUFloat32Index) isMergeable() bool {
+ return false
+}
+
+func (f *faissGPUFloat32Index) mergeFrom(other faissIndex, offset int64) error {
+ return errNotSupported
+}
+
+// syncGPUToCPU clones the current GPU index state back to the CPU index,
+// replacing the old CPU index.
+func (f *faissGPUFloat32Index) syncGPUToCPU() error {
+ gpuState := f.gpu.Load()
+ if gpuState == nil {
+ return nil
+ }
+
+ cpuIdx, err := faiss.CloneToCPU(gpuState.idx)
+ if err != nil {
+ return err
+ }
+
+ oldCPUIdx := f.cpuIdx
+ f.cpuIdx = cpuIdx
+ oldCPUIdx.Close()
+ return nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_unix.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_unix.go
new file mode 100644
index 0000000000..116e45a709
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_unix.go
@@ -0,0 +1,25 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors && !windows
+// +build vectors,!windows
+
+package zap
+
+import faiss "github.com/blevesearch/go-faiss"
+
+const (
+ faissIOFlags = faiss.IOFlagReadMmap | faiss.IOFlagSkipPrefetch
+ faissIOFlagsReadOnly = faissIOFlags | faiss.IOFlagReadOnly
+)
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_win.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_win.go
new file mode 100644
index 0000000000..a7bd7d657b
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_io_flags_win.go
@@ -0,0 +1,25 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors && windows
+// +build vectors,windows
+
+package zap
+
+import faiss "github.com/blevesearch/go-faiss"
+
+const (
+ faissIOFlags = faiss.IOFlagReadOnly
+ faissIOFlagsReadOnly = faiss.IOFlagReadOnly
+)
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_posting.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_posting.go
new file mode 100644
index 0000000000..d974616b68
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_posting.go
@@ -0,0 +1,355 @@
+// Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "math"
+ "reflect"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ "github.com/RoaringBitmap/roaring/v2/roaring64"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+var reflectStaticSizeVecPostingsList int
+var reflectStaticSizeVecPostingsIterator int
+var reflectStaticSizeVecPosting int
+
+func init() {
+ var pl VecPostingsList
+ reflectStaticSizeVecPostingsList = int(reflect.TypeOf(pl).Size())
+ var pi VecPostingsIterator
+ reflectStaticSizeVecPostingsIterator = int(reflect.TypeOf(pi).Size())
+ var p VecPosting
+ reflectStaticSizeVecPosting = int(reflect.TypeOf(p).Size())
+}
+
+type VecPosting struct {
+ docNum uint64
+ score float32
+}
+
+func (vp *VecPosting) Number() uint64 {
+ return vp.docNum
+}
+
+func (vp *VecPosting) Score() float32 {
+ return vp.score
+}
+
+func (vp *VecPosting) Size() int {
+ sizeInBytes := reflectStaticSizePosting
+
+ return sizeInBytes
+}
+
+// =============================================================================
+
+// the vector postings list is supposed to store the docNum and its similarity
+// score as a vector postings entry in it.
+// The way in which is it stored is using a roaring64 bitmap.
+// the docNum is stored in high 32 and the lower 32 bits contains the score value.
+// the score is actually a float32 value and in order to store it as a uint32 in
+// the bitmap, we use the IEEE 754 floating point format.
+//
+// each entry in the roaring64 bitmap of the vector postings list is a 64 bit
+// number which looks like this:
+// MSB LSB
+// |64 63 62 ... 32| 31 30 ... 0|
+// | | |
+type VecPostingsList struct {
+ // todo: perhaps we don't even need to store a bitmap if there is only
+ // one similar vector the query, but rather store it as a field value
+ // in the struct
+ except *roaring64.Bitmap
+ postings *roaring64.Bitmap
+}
+
+var emptyVecPostingsIterator = &VecPostingsIterator{}
+var emptyVecPostingsList = &VecPostingsList{}
+
+func (vpl *VecPostingsList) Iterator(prealloc segment.VecPostingsIterator) segment.VecPostingsIterator {
+ if vpl.postings == nil {
+ return emptyVecPostingsIterator
+ }
+ // tbd: do we check the cardinality of postings and scores?
+ var preallocPI *VecPostingsIterator
+ pi, ok := prealloc.(*VecPostingsIterator)
+ if ok && pi != nil {
+ preallocPI = pi
+ }
+ if preallocPI == emptyVecPostingsIterator {
+ preallocPI = nil
+ }
+
+ return vpl.iterator(preallocPI)
+}
+
+func (vpl *VecPostingsList) iterator(rv *VecPostingsIterator) *VecPostingsIterator {
+ if rv == nil {
+ rv = &VecPostingsIterator{}
+ } else {
+ *rv = VecPostingsIterator{} // clear the struct
+ }
+ // think on some of the edge cases over here.
+ if vpl.postings == nil {
+ return rv
+ }
+ rv.postings = vpl
+ rv.all = vpl.postings.Iterator()
+ if vpl.except != nil {
+ rv.ActualBM = roaring64.AndNot(vpl.postings, vpl.except)
+ rv.Actual = rv.ActualBM.Iterator()
+ } else {
+ rv.ActualBM = vpl.postings
+ rv.Actual = rv.all // Optimize to use same iterator for all & Actual.
+ }
+ return rv
+}
+
+func (vpl *VecPostingsList) Size() int {
+ sizeInBytes := reflectStaticSizeVecPostingsList + SizeOfPtr
+
+ if vpl.except != nil {
+ sizeInBytes += int(vpl.except.GetSizeInBytes())
+ }
+
+ return sizeInBytes
+}
+
+func (vpl *VecPostingsList) Count() uint64 {
+ if vpl.postings != nil {
+ n := vpl.postings.GetCardinality()
+ var e uint64
+ if vpl.except != nil {
+ e = vpl.postings.AndCardinality(vpl.except)
+ }
+ return n - e
+ }
+ return 0
+}
+
+func (vpl *VecPostingsList) ResetBytesRead(val uint64) {
+
+}
+
+func (vpl *VecPostingsList) BytesRead() uint64 {
+ return 0
+}
+
+func (vpl *VecPostingsList) BytesWritten() uint64 {
+ return 0
+}
+
+// =============================================================================
+
+type VecPostingsIterator struct {
+ postings *VecPostingsList
+ all roaring64.IntPeekable64
+ Actual roaring64.IntPeekable64
+ ActualBM *roaring64.Bitmap
+
+ next VecPosting // reused across Next() calls
+}
+
+func (vpItr *VecPostingsIterator) nextCodeAtOrAfterClean(atOrAfter uint64) (uint64, bool, error) {
+ vpItr.Actual.AdvanceIfNeeded(atOrAfter)
+
+ if !vpItr.Actual.HasNext() {
+ return 0, false, nil // couldn't find anything
+ }
+
+ return vpItr.Actual.Next(), true, nil
+}
+
+func (vpItr *VecPostingsIterator) nextCodeAtOrAfter(atOrAfter uint64) (uint64, bool, error) {
+ if vpItr.Actual == nil || !vpItr.Actual.HasNext() {
+ return 0, false, nil
+ }
+
+ if vpItr.postings == nil || vpItr.postings == emptyVecPostingsList {
+ // couldn't find anything
+ return 0, false, nil
+ }
+
+ if vpItr.postings.postings == vpItr.ActualBM {
+ return vpItr.nextCodeAtOrAfterClean(atOrAfter)
+ }
+
+ vpItr.Actual.AdvanceIfNeeded(atOrAfter)
+
+ if !vpItr.Actual.HasNext() || !vpItr.all.HasNext() {
+ // couldn't find anything
+ return 0, false, nil
+ }
+
+ n := vpItr.Actual.Next()
+ allN := vpItr.all.Next()
+
+ // n is the next actual hit (excluding some postings), and
+ // allN is the next hit in the full postings, and
+ // if they don't match, move 'all' forwards until they do.
+ for allN != n {
+ if !vpItr.all.HasNext() {
+ return 0, false, nil
+ }
+ allN = vpItr.all.Next()
+ }
+
+ return n, true, nil
+}
+
+// a transformation function which stores both the score and the docNum as a single
+// entry which is a uint64 number.
+func getVectorCode(docNum uint32, score float32) uint64 {
+ return uint64(docNum)<<32 | uint64(math.Float32bits(score))
+}
+
+// Next returns the next posting on the vector postings list, or nil at the end
+func (vpItr *VecPostingsIterator) nextAtOrAfter(atOrAfter uint64) (segment.VecPosting, error) {
+ // transform the docNum provided to the vector code format and use that to
+ // get the next entry. the comparison still happens docNum wise since after
+ // the transformation, the docNum occupies the upper 32 bits just an entry in
+ // the postings list
+ atOrAfter = getVectorCode(uint32(atOrAfter), 0)
+ code, exists, err := vpItr.nextCodeAtOrAfter(atOrAfter)
+ if err != nil || !exists {
+ return nil, err
+ }
+
+ vpItr.next = VecPosting{} // clear the struct
+ rv := &vpItr.next
+ rv.score = math.Float32frombits(uint32(code))
+ rv.docNum = code >> 32
+
+ return rv, nil
+}
+
+func (vpItr *VecPostingsIterator) Next() (segment.VecPosting, error) {
+ return vpItr.nextAtOrAfter(0)
+}
+
+func (vpItr *VecPostingsIterator) Advance(docNum uint64) (segment.VecPosting, error) {
+ return vpItr.nextAtOrAfter(docNum)
+}
+
+func (vpItr *VecPostingsIterator) Size() int {
+ sizeInBytes := reflectStaticSizePostingsIterator + SizeOfPtr +
+ vpItr.next.Size()
+
+ return sizeInBytes
+}
+
+func (vpItr *VecPostingsIterator) ResetBytesRead(val uint64) {
+
+}
+
+func (vpItr *VecPostingsIterator) BytesRead() uint64 {
+ return 0
+}
+
+func (vpItr *VecPostingsIterator) BytesWritten() uint64 {
+ return 0
+}
+
+// InterpretVectorIndex returns a struct based implementation (vectorIndexWrapper)
+// that will allow the caller to -
+// (1) search within an attached vector index
+// (2) search limited to a subset of documents within an attached vector index
+// (3) close attached vector index
+// (4) get the size of the attached vector index
+func (sb *SegmentBase) InterpretVectorIndex(field string, except *roaring.Bitmap) (segment.VectorIndex, error) {
+ rv := &vectorIndexWrapper{sb: sb}
+ fieldIDPlus1 := sb.fieldsMap[field]
+ if fieldIDPlus1 <= 0 {
+ return rv, nil
+ }
+ // adjust to get the actual fieldID
+ fieldID := fieldIDPlus1 - 1
+ rv.fieldID = fieldID
+ // get the position of the vector section for the field
+ pos := sb.fieldsSectionsMap[fieldID][SectionFaissVectorIndex]
+ // check if the field has a vector section in the segment.
+ if pos <= 0 {
+ return rv, nil
+ }
+ // the below loop loads the following:
+ // 1. doc values(first 2 iterations) - adhering to the sections format. never
+ // valid values for vector section
+ // 2. index optimization type.
+ for i := 0; i < 3; i++ {
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ }
+
+ // create the vector index wrapper by loading (or creating) the vector index
+ // and the vector to docID mapping
+ useGPU := sb.fieldsOptions[field].UseGPU()
+ var err error
+ rv.index, rv.mapping, rv.exclude, err = sb.vecIndexCache.loadOrCreate(fieldID, sb.mem[pos:], uint32(sb.numDocs), except, useGPU, sb.fileReader)
+ if err != nil {
+ return nil, err
+ }
+ // get the size of the vector index
+ if rv.index != nil {
+ rv.vecIndexSize = rv.index.size()
+ }
+
+ // get the number of nested documents in this segment, if any
+ // to determine if the wrapper needs to handle nested documents
+ rv.nestedMode = sb.countNested() > 0
+
+ return rv, nil
+}
+
+func (sb *SegmentBase) UpdateFieldStats(stats segment.FieldStats) {
+ for _, fieldName := range sb.fieldsInv {
+ pos := int(sb.fieldsSectionsMap[sb.fieldsMap[fieldName]-1][SectionFaissVectorIndex])
+ if pos == 0 {
+ continue
+ }
+
+ for i := 0; i < 3; i++ {
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ }
+ numVecs, _ := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+
+ stats.Store("num_vectors", fieldName, numVecs)
+ }
+}
+
+func (sb *SegmentBase) UpdateVectorFieldStats(stats segment.FieldStats) {
+ if sb.vecIndexCache == nil {
+ return
+ }
+ for _, fieldName := range sb.fieldsInv {
+ pos := int(sb.fieldsSectionsMap[sb.fieldsMap[fieldName]-1][SectionFaissVectorIndex])
+ if pos == 0 {
+ continue
+ }
+ fieldID := sb.fieldsMap[fieldName] - 1
+ switch sb.vecIndexCache.indexLocation(fieldID) {
+ case vectorIndexInGPU:
+ stats.Store("num_vector_indexes_in_gpu", fieldName, 1)
+ case vectorIndexInCPU:
+ stats.Store("num_vector_indexes_in_cpu", fieldName, 1)
+ }
+ }
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_request_batcher.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_request_batcher.go
new file mode 100644
index 0000000000..a5614dd823
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_request_batcher.go
@@ -0,0 +1,292 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "errors"
+ "sync"
+)
+
+var (
+ errBatcherStopped error = errors.New("batcher has been stopped")
+)
+
+// The requestBatcher is responsible for batching search requests to a Faiss index.
+// It will accumulate incoming search requests and execute them in batches to improve performance.
+// The batcher will use the provided Faiss index to perform the searches, and it
+// will manage the batching logic, including timing and concurrency control.
+type requestBatcher struct {
+ // the coalesce queue that manages the batching of incoming search requests.
+ cq *coalesceQueue
+}
+
+func newRequestBatcher(idx faissQueryBatch) *requestBatcher {
+ b := &requestBatcher{
+ cq: newCoalesceQueue(idx),
+ }
+ return b
+}
+
+// search performs a search on the Faiss index using the provided query vector and k value.
+// NOTE: it must be ensured that every query vector passed to this method has the same dimensionality
+// as the vectors in the Faiss index, this is considered as an invariant to be upheld by the caller,
+// and is not checked within this method for performance reasons.
+func (b *requestBatcher) search(qVector *vectorSet, k int64) ([]float32, []int64, error) {
+ // create a new batch request for this search query.
+ req, respCh := newBatchRequest(qVector, k)
+ // check if the batcher has been stopped before processing the search request.
+ select {
+ case b.cq.enqueueCh <- req:
+ case <-b.cq.stopCh:
+ return nil, nil, errBatcherStopped
+ }
+ // wait for the search results to be sent back through the response channel,
+ // and return those results to the caller.
+ resp := <-respCh
+ return resp.distances, resp.ids, resp.err
+}
+
+func (b *requestBatcher) stop() {
+ b.cq.stop()
+}
+
+// --------------------------------------------------
+// batch request
+// --------------------------------------------------
+
+type batchRequest struct {
+ qVector *vectorSet
+ k int64
+ respCh []chan *batchResponse
+}
+
+func newBatchRequest(qVector *vectorSet, k int64) (*batchRequest, chan *batchResponse) {
+ // response channel for sending the search results back to the requester.
+ respChan := make(chan *batchResponse, 1)
+ return &batchRequest{
+ qVector: qVector,
+ k: k,
+ respCh: []chan *batchResponse{respChan},
+ }, respChan
+}
+
+// canMerge checks if this batch request can be merged with another request.
+// For now, we can only merge requests that have the same k value.
+func (r *batchRequest) canMerge(other *batchRequest) bool {
+ // for now, we can only merge requests that have the same k value,
+ // since the Faiss search API requires a single k value for each search.
+ return r.k == other.k
+}
+
+// mergeWith combines another batch request into this one by concatenating their query vectors and response channels.
+// NOTE: must only be called after veryfing that canMerge() returns true for these two requests.
+func (r *batchRequest) mergeWith(other *batchRequest) {
+ // merge the query vectors of the two requests by concatenating them together.
+ r.qVector.mergeWith(other.qVector)
+ // append the response channels from the other request to this request, so that when the search results are ready,
+ // we can send the results back to all requesters that were merged into this batch.
+ r.respCh = append(r.respCh, other.respCh...)
+}
+
+func (r *batchRequest) sendResponse(distances []float32, ids []int64, err error) {
+ // we may have multiple batches merged together, so we need to segregate the results for each original request
+ // and send them back to the appropriate response channels.
+ if err != nil {
+ // if there was an error during the search, send the error back to all requesters in this batch.
+ for _, respCh := range r.respCh {
+ respCh <- newBatchResponse(nil, nil, err)
+ close(respCh)
+ }
+ return
+ }
+ // if the search was successful, we need to split the combined results back into individual responses for each original request.
+ for i, respCh := range r.respCh {
+ offset := int64(i) * r.k
+ // calculate the start and end indices for the results corresponding to this response channel.
+ curDistances := distances[offset : offset+r.k]
+ curIDs := ids[offset : offset+r.k]
+ // send the results back to the requester through the response channel.
+ respCh <- newBatchResponse(curDistances, curIDs, nil)
+ // close the response channel to signal that the response has been sent and there will be no more data.
+ close(respCh)
+ }
+}
+
+// --------------------------------------------------
+// batch response
+// --------------------------------------------------
+
+type batchResponse struct {
+ distances []float32
+ ids []int64
+ err error
+}
+
+func newBatchResponse(distances []float32, ids []int64, err error) *batchResponse {
+ return &batchResponse{
+ distances: distances,
+ ids: ids,
+ err: err,
+ }
+}
+
+// ---------------------------------------------------
+// batch manager
+// ---------------------------------------------------
+type batchManager struct {
+ batchPool sync.Pool
+}
+
+func newBatchManager() *batchManager {
+ return &batchManager{
+ batchPool: sync.Pool{
+ New: func() any {
+ return make([]*batchRequest, 0, 16)
+ },
+ },
+ }
+}
+
+func (m *batchManager) getBatch() []*batchRequest {
+ return m.batchPool.Get().([]*batchRequest)[:0]
+}
+
+func (m *batchManager) putBatch(batch []*batchRequest) {
+ clear(batch)
+ m.batchPool.Put(batch[:0])
+}
+
+// --------------------------------------------------
+// coalesceQueue
+// --------------------------------------------------
+// Implements Nagle's algorithm for coalescing search requests:
+// - The coalesce goroutine continuously receives and coalesces incoming requests.
+// - When the flusher is idle, the coalesce goroutine hands off the coalesced batch.
+// - While the flusher is busy executing a batch, the coalesce goroutine keeps coalescing new requests.
+// - Once the flusher completes, the coalesce goroutine hands off any accumulated requests right away.
+type coalesceQueue struct {
+ // the Faiss index that this coalesce queue will execute search requests against.
+ idx faissQueryBatch
+ // channel for enqueuing new batch requests into the queue.
+ enqueueCh chan *batchRequest
+ // channel for handing off coalesced batches to the flusher goroutine for execution.
+ flushCh chan []*batchRequest
+ // safeguard to ensure that the stop() method is thread-safe and can only be called once,
+ // preventing multiple close operations on the stopCh.
+ stopOnce sync.Once
+ // channel for signaling the batcher to stop processing requests and shut down.
+ stopCh chan struct{}
+ // closed when filler goroutine has exited after receiving a stop signal.
+ fillerDoneCh chan struct{}
+ // closed when flusher goroutine has exited after receiving a stop signal.
+ flusherDoneCh chan struct{}
+ // a sync.Pool for reusing batch slices to reduce allocations and GC overhead.
+ batchManager *batchManager
+}
+
+func newCoalesceQueue(idx faissQueryBatch) *coalesceQueue {
+ q := &coalesceQueue{
+ idx: idx,
+ enqueueCh: make(chan *batchRequest),
+ flushCh: make(chan []*batchRequest),
+ stopCh: make(chan struct{}),
+ fillerDoneCh: make(chan struct{}),
+ flusherDoneCh: make(chan struct{}),
+ batchManager: newBatchManager(),
+ }
+ go q.filler()
+ go q.flusher()
+ return q
+}
+
+func (q *coalesceQueue) stop() {
+ q.stopOnce.Do(func() {
+ close(q.stopCh)
+ })
+ // wait for all goroutines to exit
+ <-q.fillerDoneCh
+ <-q.flusherDoneCh
+}
+
+// filler is the enqueuer goroutine. It receives incoming search requests,
+// coalesces them into batches, and hands them off to the flusher when it is idle.
+func (q *coalesceQueue) filler() {
+ defer close(q.fillerDoneCh)
+ var pendingBatch []*batchRequest
+ for {
+ if len(pendingBatch) > 0 {
+ select {
+ case req := <-q.enqueueCh:
+ pendingBatch = q.coalesce(pendingBatch, req)
+ case q.flushCh <- pendingBatch:
+ pendingBatch = nil
+ case <-q.stopCh:
+ q.flushCh <- pendingBatch
+ return
+ }
+ } else {
+ select {
+ case req := <-q.enqueueCh:
+ pendingBatch = q.coalesce(pendingBatch, req)
+ case <-q.stopCh:
+ return
+ }
+ }
+ }
+}
+
+// flusher is the background goroutine that executes batches handed off by the monitor.
+func (q *coalesceQueue) flusher() {
+ defer close(q.flusherDoneCh)
+ for {
+ select {
+ case batch := <-q.flushCh:
+ q.executeBatch(batch)
+ case <-q.fillerDoneCh:
+ return
+ }
+ }
+}
+
+// coalesce merges req into the queue, either by finding a compatible pending
+// request to merge with or by appending a new entry.
+func (q *coalesceQueue) coalesce(queue []*batchRequest, req *batchRequest) []*batchRequest {
+ for _, pendingReq := range queue {
+ if pendingReq.canMerge(req) {
+ pendingReq.mergeWith(req)
+ return queue
+ }
+ }
+ // No compatible request found; clone the query vector so that future
+ // merges into this entry do not mutate the caller's data.
+ req.qVector = req.qVector.clone()
+ if queue == nil {
+ queue = q.batchManager.getBatch()
+ }
+ return append(queue, req)
+}
+
+// executeBatch runs all coalesced requests against the Faiss index and delivers results.
+func (q *coalesceQueue) executeBatch(batch []*batchRequest) {
+ for _, req := range batch {
+ distances, ids, err := q.idx.batchSearch(req.qVector, req.k)
+ req.sendResponse(distances, ids, err)
+ }
+ // recycle the batch slice back into the pool
+ q.batchManager.putBatch(batch)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/faiss_vector_wrapper.go b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_wrapper.go
new file mode 100644
index 0000000000..0e15e295a5
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/faiss_vector_wrapper.go
@@ -0,0 +1,1060 @@
+// Copyright (c) 2025 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/json"
+ "fmt"
+ "math"
+ "math/bits"
+ "slices"
+
+ "github.com/RoaringBitmap/roaring/v2/roaring64"
+ index "github.com/blevesearch/bleve_index_api"
+ faiss "github.com/blevesearch/go-faiss"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+const (
+ // maxMultiVectorDocSearchRetries limits repeated searches when deduplicating
+ // multi-vector documents. Each retry excludes previously seen vectors to find
+ // new unique documents. Acts as a safeguard against pathological data distributions.
+ maxMultiVectorDocSearchRetries = 100
+
+ // Pre-Filtered IVF Index search: Threshold for when to start increasing: after 2 iterations without
+ // finding enough documents, we start increasing up to the number of centroidsToProbe
+ // up to the total number of eligible centroids available
+ nprobeIncreaseThreshold = 2
+
+ // binaryOversampleValue is the multiplier used to determine how many additional vectors to retrieve
+ // from the binary index as an oversampling strategy to improve recall.
+ binaryOversampleValue = 4
+)
+
+// vectorIndexWrapper conforms to scorch_segment_api's VectorIndex interface
+type vectorIndexWrapper struct {
+ index faissIndex
+ mapping *idMapping
+ exclude *bitmap
+ fieldID uint16
+ vecIndexSize uint64
+
+ // nestedMode indicates if the vector index is operating in nested document mode.
+ // if so we have a reusable ancestry slice to help with docID lookups
+ nestedMode bool
+ ancestry []index.AncestorID
+
+ sb *SegmentBase
+}
+
+func (v *vectorIndexWrapper) Search(qVector []float32, k int64, params json.RawMessage) (segment.VecPostingsList, error) {
+ if v.index == nil {
+ // vector index not found, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ if v.index.dim() != len(qVector) {
+ // dimensionality mismatch, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ // check if number of docs or number of vectors is zero
+ if v.mapping == nil || v.mapping.numVectors() == 0 || v.mapping.numDocuments() == 0 {
+ // no vectors or no documents indexed, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ // check if all the vectors are excluded
+ if v.exclude != nil && v.exclude.cardinality() == v.mapping.numVectors() {
+ // all vectors excluded, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ // create a vector set using the query vector
+ qVecSet, err := newVectorSet(len(qVector), qVector)
+ if err != nil {
+ return nil, err
+ }
+ rs, err := v.searchWithoutIDs(qVecSet, k, v.exclude, params)
+ if err != nil {
+ return nil, err
+ }
+ // populate the postings list from the result set
+ return getPostingsList(rs), nil
+}
+
+func (v *vectorIndexWrapper) SearchWithFilter(qVector []float32, k int64,
+ eligibleList index.EligibleDocumentList, params json.RawMessage) (
+ segment.VecPostingsList, error) {
+ // if no eligible documents, return empty postings list
+ if eligibleList == nil || eligibleList.Count() == 0 {
+ return emptyVecPostingsList, nil
+ }
+ if v.index == nil {
+ // vector index not found, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ if v.index.dim() != len(qVector) {
+ // dimensionality mismatch, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ // check if number of docs or number of vectors is zero
+ if v.mapping == nil || v.mapping.numVectors() == 0 || v.mapping.numDocuments() == 0 {
+ // no vectors or no documents indexed, so return empty postings list
+ return emptyVecPostingsList, nil
+ }
+ // if all documents are eligible, do a normal search
+ if eligibleList.Count() == uint64(v.mapping.numDocuments()) {
+ return v.Search(qVector, k, params)
+ }
+ // get the eligible document iterator
+ eligibleIterator := eligibleList.Iterator()
+ // vector IDs corresponding to the local doc numbers to be
+ // considered for the search
+ // create a bitmap for the vector IDs to include in the search
+ includeBM := newBitmap(v.mapping.numVectors())
+ includeCardinality := 0
+ for {
+ // get the next eligible document ID
+ id, ok := eligibleIterator.Next()
+ if !ok {
+ // exhausted all eligible document IDs
+ break
+ }
+ // get the vector IDs for this document ID
+ vecIDs, exists := v.mapping.vecsForDoc(uint32(id))
+ if !exists {
+ continue
+ }
+ // since a vector can never belong to multiple documents, we calculate
+ // the cardinality by simply adding the number of vectors for each document
+ // we include, without worrying about duplicates and avoiding a potential
+ // costly population count on the bitmap at the end
+ includeCardinality += len(vecIDs)
+ for _, vecID := range vecIDs {
+ // add all vector IDs for this document to the inclusion bitmap
+ includeBM.set(vecID)
+ }
+ }
+ // In case a doc has invalid vector fields but valid non-vector fields,
+ // filter hit IDs may be ineligible for the kNN since the document does
+ // not have any/valid vectors. Also can happen if no documents have vectors
+ numSelected := uint32(includeCardinality)
+ if numSelected == 0 {
+ return emptyVecPostingsList, nil
+ }
+ // if we have included all vectors, then we can do a normal search
+ // with full selectivity (no filtering)
+ if numSelected == v.mapping.numVectors() {
+ return v.Search(qVector, k, params)
+ }
+ // get a vector set using the query vector
+ qVecSet, err := newVectorSet(len(qVector), qVector)
+ if err != nil {
+ return nil, err
+ }
+ // try to cast the index to an IVF index
+ ivfPtr := v.index.castIVF()
+ if ivfPtr == nil {
+ // perform search with included IDs in the bitmap, since
+ // this is not an IVF index
+ rs, err := v.searchWithIDs(qVecSet, k, includeBM, params)
+ if err != nil {
+ return nil, err
+ }
+ // populate the postings list from the result set
+ return getPostingsList(rs), nil
+ }
+ // Getting the IVF index parameters, nprobe and nlist, set at index time.
+ nprobe, nlist := ivfPtr.ivfParams()
+ // Create a FAISS selector based on the include bitmap.
+ includeSelector, err := getIncludeSelector(includeBM)
+ if err != nil {
+ return nil, err
+ }
+ // Ensure the selector is deleted after use, this does NOT free the inner includeBM bitmap.
+ // We control its lifecycle in GO.
+ defer includeSelector.Delete()
+ // Determining which clusters, identified by centroid ID,
+ // have at least one eligible vector and hence, ought to be
+ // probed.
+ clusterVectorCounts, err := ivfPtr.clusterVectorCounts(includeSelector, nlist)
+ if err != nil {
+ return nil, err
+ }
+ // Create a bitmap for the eligible centroids to be considered for probing.
+ centroidBM := newBitmap(uint32(nlist))
+ centroidCount := 0
+ for centroidID, vectorCount := range clusterVectorCounts {
+ // Only centroids with at least one eligible vector are considered.
+ if vectorCount > 0 {
+ // since we are adding only unique centroid IDs, this is simply an increment
+ // and we can avoid a population count at the end
+ centroidCount++
+ centroidBM.set(uint32(centroidID))
+ }
+ }
+ if centroidCount == 0 {
+ // No centroids have any eligible vectors, so return empty postings list.
+ return emptyVecPostingsList, nil
+ }
+ // create a FAISS selector based on the centroid bitmap
+ centroidSelector, err := getIncludeSelector(centroidBM)
+ if err != nil {
+ return nil, err
+ }
+ defer centroidSelector.Delete()
+ // Search the coarse quantizer to order the centroids based on proximity
+ // to the query vector.
+ eligibleCentroidIDs, centroidDistances, err := ivfPtr.searchQuantizer(qVecSet, centroidSelector, int64(centroidCount))
+ if err != nil {
+ return nil, err
+ }
+ // Determining the minimum number of centroids to be probed
+ // to ensure that at least 'k' vectors are collected while
+ // examining at least 'nprobe' centroids.
+ // centroidsToProbe range: [nprobe, number of eligible centroids]
+ var eligibleVecsTillNow int64
+ var eligibleCentroidsTillNow int
+ centroidsToProbe := len(eligibleCentroidIDs)
+ for i, centroidID := range eligibleCentroidIDs {
+ // if we get a -1 somehow here, it means no more centroids
+ // need to reslice the eligibleCentroidIDs and distances
+ // accordingly, just a safeguard check as this does not
+ // really happen. FAISS can pad with -1s if there are not enough
+ // eligible centroids, but we have already counted the cardinality so
+ // we should not see -1s here.
+ if centroidID == -1 {
+ centroidsToProbe = i
+ // reslice to only valid centroids
+ eligibleCentroidIDs = eligibleCentroidIDs[:centroidsToProbe]
+ centroidDistances = centroidDistances[:centroidsToProbe]
+ break
+ }
+ eligibleVecsTillNow += clusterVectorCounts[centroidID]
+ eligibleCentroidsTillNow = i + 1
+ // Stop once we've examined at least 'nprobe' centroids and
+ // collected at least 'k' vectors.
+ if eligibleVecsTillNow >= k && eligibleCentroidsTillNow >= nprobe {
+ centroidsToProbe = eligibleCentroidsTillNow
+ break
+ }
+ }
+ // Search the clusters specified by 'eligibleCentroidIDs' for
+ // vectors whose IDs are present in the includeBM bitmap.
+ // This is done while probing only 'centroidsToProbe' clusters.
+ // unless overridden dynamically, either by the search parameters
+ // or by the deduplication logic in searchClustersFromIVFIndex.
+ rs, err := v.searchClustersFromIVFIndex(
+ eligibleCentroidIDs, centroidDistances, centroidsToProbe,
+ qVecSet, k, includeBM, params)
+ if err != nil {
+ return nil, err
+ }
+ // populate the postings list from the result set
+ return getPostingsList(rs), nil
+}
+func (v *vectorIndexWrapper) Close() {
+ // skipping the closing because the index is cached and it's being
+ // deferred to a later point of time.
+ v.sb.vecIndexCache.decRef(v.fieldID)
+}
+
+func (v *vectorIndexWrapper) Size() uint64 {
+ return v.vecIndexSize
+}
+
+func (v *vectorIndexWrapper) ObtainKCentroidCardinalitiesFromIVFIndex(limit int, descending bool) (
+ []index.CentroidCardinality, error) {
+ if v.index == nil {
+ return nil, nil
+ }
+ var ivfIdx faissIndexIVF
+ if ivfIdx = v.index.castIVF(); ivfIdx == nil {
+ return nil, nil
+ }
+ cardinalities, centroids, err := ivfIdx.centroidCardinalities(limit, descending)
+ if err != nil {
+ return nil, err
+ }
+ centroidCardinalities := make([]index.CentroidCardinality, len(cardinalities))
+ for i, cardinality := range cardinalities {
+ centroidCardinalities[i] = index.CentroidCardinality{
+ Centroid: centroids[i],
+ Cardinality: cardinality,
+ }
+ }
+ return centroidCardinalities, nil
+}
+
+// docSearch performs a search on the vector index to retrieve
+// top k documents based on the provided search function.
+// It handles deduplication of documents that may have multiple
+// vectors associated with them.
+// The prepareNextIter function is used to set up the state
+// for the next iteration, if more searches are needed to find
+// k unique documents. The callback recieves the number of iterations
+// done so far and the vector ids retrieved in the last search. While preparing
+// the next iteration, if its decided that no further searches are needed,
+// the prepareNextIter function can decide whether to continue searching or not
+func (v *vectorIndexWrapper) docSearch(k int64, numDocs uint64,
+ search func() (scores []float32, labels []int64, err error),
+ prepareNextIter func(numIter int, labels []int64) bool) (resultSet, error) {
+ // create a result set to hold top K docIDs and their scores
+ rs := newResultSet(k, numDocs)
+ // flag to indicate if we have exhausted the vector index
+ var exhausted bool
+ // keep track of number of iterations done, we execute the loop more than once only when
+ // we have multi-vector documents leading to duplicates in docIDs retrieved
+ numIter := 0
+ // get the metric type of the index to help with deduplication logic
+ metricType := v.index.metricType()
+ // we keep searching until we have k unique docIDs or we have exhausted the vector index
+ // or we have reached the maximum number of deduplication iterations allowed
+ for numIter < maxMultiVectorDocSearchRetries && rs.size() < k && !exhausted {
+ // search the vector index
+ numIter++
+ scores, labels, err := search()
+ if err != nil {
+ return nil, err
+ }
+ // process the retrieved ids and scores, getting the corresponding docIDs
+ // for each vector id retrieved, and storing the best score for each unique docID
+ for i, vecID := range labels {
+ // a vecID of -1 indicates that all valid vectors in the index have been exhausted,
+ // so we set the flag to prevent further iterations. However, the current iteration
+ // may still contain valid results, so we process them before stopping.
+ if vecID == -1 {
+ exhausted = true
+ continue
+ }
+ docID, exists := v.getDocIDForVectorID(vecID)
+ if !exists {
+ continue
+ }
+ score := scores[i]
+ prevScore, exists := rs.get(docID)
+ if !exists {
+ // first time seeing this docID, so just store it
+ rs.put(docID, score)
+ continue
+ }
+ // we have seen this docID before, so we must compare scores
+ // check the index metric type first to check how we compare distances/scores
+ // and store the best score for the docID accordingly
+ // for inner product, higher the score, better the match
+ // for euclidean distance, lower the score/distance, better the match
+ // so we invert the comparison accordingly
+ switch metricType {
+ case faiss.MetricInnerProduct: // similarity metrics like dot product => higher is better
+ if score > prevScore {
+ rs.put(docID, score)
+ }
+ case faiss.MetricL2:
+ fallthrough
+ default: // distance metrics like euclidean distance => lower is better
+ if score < prevScore {
+ rs.put(docID, score)
+ }
+ }
+ }
+ // if we still have less than k unique docIDs, prepare for the next iteration, provided
+ // we have not exhausted the index
+ if rs.size() < k && !exhausted {
+ // prepare state for next iteration
+ shouldContinue := prepareNextIter(numIter, labels)
+ if !shouldContinue {
+ break
+ }
+ }
+ }
+ // at this point we either have k unique docIDs or we have exhausted
+ // the vector index or we have reached the maximum number of deduplication iterations allowed
+ // or the prepareNextIter function decided to break out of the loop
+ return rs, nil
+}
+
+// searchWithoutIDs performs a search on the vector index to retrieve the top K documents
+// while excluding any vector IDs specified in the exclude bitmap.
+func (v *vectorIndexWrapper) searchWithoutIDs(qVector *vectorSet, k int64,
+ exclude *bitmap, params json.RawMessage) (resultSet, error) {
+ return v.docSearch(k, v.sb.numDocs,
+ func() ([]float32, []int64, error) {
+ // build the FAISS selector based on the exclude bitmap, if any.
+ // The exclude bitmap can be nil, indicating no exclusions, in that
+ // case we can pass a nil selector to FAISS.
+ // NOTE: The bitmap selector is just a wrapper over the exclude bitmap
+ // which is shared across the CGO layer.
+ sel, err := getExcludeSelector(exclude)
+ if err != nil {
+ return nil, nil, err
+ }
+ // NOTE: the selector being freed does NOT free the inner bitmap, as we control
+ // its lifecycle in GO, to reuse the bitmap across iterations, if needed, for
+ // multi-vector document retrieval.
+ if sel != nil {
+ // The selector can be nil here as we may not be excluding any vectors
+ // in which case we can just pass a nil selector to FAISS.
+ defer sel.Delete()
+ }
+ return v.index.search(qVector, k, sel, params)
+ },
+ func(numIter int, labels []int64) bool {
+ // if this is the first loop iteration and we have < k unique docIDs,
+ // we must clone the existing exclude bitmap before modifying it
+ // to avoid modifying the original bitmap passed in by the caller
+ if numIter == 1 {
+ // if we do not have an exclude bitmap yet, create a new one
+ if exclude == nil {
+ exclude = newBitmap(v.mapping.numVectors())
+ } else {
+ // clone the existing exclude bitmap
+ exclude = exclude.clone()
+ }
+ }
+ // prepare the exclude list for the next iteration by adding
+ // the vector ids retrieved in this iteration
+ for _, vecID := range labels {
+ // should not happen, but just a safeguard, as we catch -1
+ // in the main loop
+ if vecID == -1 {
+ continue
+ }
+ exclude.set(uint32(vecID))
+ }
+ // with exclude bitmap updated, we can proceed to the next iteration
+ // fast check if the exclude bitmap has all vectors excluded, in which case
+ // we can stop searching further
+ return exclude.cardinality() != v.mapping.numVectors()
+ })
+}
+
+// searchWithIDs performs a search on the vector index to retrieve the top K documents while only
+// considering the vector IDs specified in the include bitmap.
+// NOTE: The include bitmap must NOT be nil and must have at least one vector ID set.
+func (v *vectorIndexWrapper) searchWithIDs(vecSet *vectorSet, k int64, include *bitmap, params json.RawMessage) (resultSet, error) {
+ return v.docSearch(k, v.sb.numDocs,
+ func() ([]float32, []int64, error) {
+ // build the FAISS selector based on the include bitmap.
+ // NOTE: The bitmap selector is just a wrapper over the include bitmap
+ // which is shared across the CGO layer.
+ sel, err := getIncludeSelector(include)
+ if err != nil {
+ return nil, nil, err
+ }
+ // NOTE: the selector being freed does NOT free the inner bitmap, as we control
+ // its lifecycle in GO, to reuse the bitmap across iterations, if needed, for
+ // multi-vector document retrieval.
+ defer sel.Delete()
+ return v.index.search(vecSet, k, sel, params)
+ },
+ func(numIter int, labels []int64) bool {
+ // if this is the first loop iteration and we have < k unique docIDs,
+ // we clone the existing include slice before modifying it
+ if numIter == 1 {
+ if include == nil {
+ // should not happen, but just a safeguard
+ include = newBitmap(v.mapping.numVectors())
+ } else {
+ // clone the existing include bitmap
+ include = include.clone()
+ }
+ }
+ // removing the vector ids retrieved in this iteration
+ // from the include set
+ for _, vecID := range labels {
+ // should not happen, but just a safeguard, as we catch -1
+ // in the main loop
+ if vecID == -1 {
+ continue
+ }
+ include.clear(uint32(vecID))
+ }
+ // only continue searching if we still have vector ids to include
+ return !include.isEmpty()
+ })
+}
+
+// searchClustersFromIVFIndex performs a search on the IVF vector index to retrieve the top K documents
+// while including only the vectors present in the includeBM bitmap.
+// It takes into account the eligible centroid IDs and ensures that at least centroidsToProbe are probed.
+// If after a few iterations we haven't found enough documents, it dynamically increases the number of
+// clusters searched (up to the number of eligible centroids) to ensure we can find k unique documents.
+func (v *vectorIndexWrapper) searchClustersFromIVFIndex(eligibleCentroidIDs []int64, centroidDis []float32,
+ centroidsToProbe int, qVecSet *vectorSet, k int64, include *bitmap, params json.RawMessage) (
+ resultSet, error) {
+ // get ivf index pointer, should not be nil at this point since this method is only called after confirming its an ivf index
+ ivfPtr := v.index.castIVF()
+ var totalEligibleCentroids = len(eligibleCentroidIDs)
+ return v.docSearch(k, v.sb.numDocs,
+ func() ([]float32, []int64, error) {
+ // build the FAISS selector based on the include bitmap.
+ // NOTE: The bitmap selector is just a wrapper over the include bitmap
+ // which is shared across the CGO layer.
+ sel, err := getIncludeSelector(include)
+ if err != nil {
+ return nil, nil, err
+ }
+ // NOTE: the selector being freed does NOT free the inner bitmap, as we control
+ // its lifecycle in GO, to reuse the bitmap across iterations, if needed, for
+ // multi-vector document retrieval.
+ if sel != nil {
+ defer sel.Delete()
+ }
+ return ivfPtr.searchClusters(eligibleCentroidIDs, centroidDis, centroidsToProbe,
+ qVecSet, k, sel, params)
+ },
+ func(numIter int, labels []int64) bool {
+ // if this is the first loop iteration and we have < k unique docIDs,
+ // we must clone the existing ids slice before modifying it to avoid
+ // modifying the original slice passed in by the caller
+ if numIter == 1 {
+ if include == nil {
+ // should not happen, but just a safeguard
+ include = newBitmap(v.mapping.numVectors())
+ } else {
+ // clone the existing include bitmap
+ include = include.clone()
+ }
+ }
+ // if we have iterated atleast nprobeIncreaseThreshold times
+ // and still have not found enough unique docIDs, we increase
+ // the number of centroids to probe for the next iteration
+ // to try and find more vectors/documents
+ if numIter >= nprobeIncreaseThreshold && centroidsToProbe < totalEligibleCentroids {
+ // Calculate how much to increase: increase by 50% of the remaining centroids to probe,
+ // but at least by 1 to ensure progress.
+ increaseAmount := max((totalEligibleCentroids-centroidsToProbe)/2, 1)
+ // Update centroidsToProbe, ensuring it does not exceed the total eligible centroids
+ centroidsToProbe = min(centroidsToProbe+increaseAmount, totalEligibleCentroids)
+ }
+ // removing the vector ids retrieved in this iteration
+ // from the include set
+ for _, vecID := range labels {
+ // should not happen, but just a safeguard, as we catch -1
+ // in the main loop
+ if vecID == -1 {
+ continue
+ }
+ include.clear(uint32(vecID))
+ }
+ // only continue searching if we still have vector ids to include
+ return !include.isEmpty()
+ })
+}
+
+// Utility function to get the docID for a given vectorID, used for the
+// deduplication logic, to map vectorIDs back to their corresponding docIDs
+// if we are in nested mode, this method returns the root docID instead of
+// the nested docID, by consulting the edge list. This ensures that kNN searches
+// return unique root documents when nested documents are involved.
+func (v *vectorIndexWrapper) getDocIDForVectorID(vecID int64) (uint32, bool) {
+ docID, exists := v.mapping.docForVec(uint32(vecID))
+ if !v.nestedMode || !exists {
+ // either not in nested mode, or docID does not exist
+ //for the vectorID, so just return the docID as is
+ return docID, exists
+ }
+ // in nested mode and docID exists, so we must get the root docID from the edge list
+ // reuse the wrapper's ancestry slice to avoid allocations
+ v.ancestry = v.sb.Ancestors(uint64(docID), v.ancestry[:0])
+ if len(v.ancestry) == 0 {
+ // should not happen, but just in case, return the docID as is
+ return docID, exists
+ }
+ // return the root docID, which is the last element in the ancestry slice
+ // in case the docID is a root doc, the ancestry slice would have
+ // just one element, which is the docID itself
+ return uint32(v.ancestry[len(v.ancestry)-1]), true
+}
+
+// ------------------------------------------------------------------------------
+// Utility functions not tied to vector index wrapper
+// ------------------------------------------------------------------------------
+
+// Utility function to get a faiss.BitmapSelector to include the IDs specified in the bitmap
+// The caller must ensure to free the selector by calling selector.Delete() when done using it.
+func getIncludeSelector(bm *bitmap) (selector faiss.Selector, err error) {
+ if bm == nil {
+ // no bitmap provided, so return an error as we expect at least one ID to include
+ return nil, fmt.Errorf("include bitmap is nil or empty")
+ }
+ // create a bitmap inclusion selector
+ selector, err = faiss.NewIDSelectorBitmap(bm.bytes())
+ if err != nil {
+ return nil, err
+ }
+ return selector, nil
+}
+
+// Utility function to get a faiss.BitmapSelector to exclude the IDs specified in the bitmap
+// The caller must ensure to free the selector by calling selector.Delete() when done using it.
+func getExcludeSelector(bm *bitmap) (selector faiss.Selector, err error) {
+ if bm == nil {
+ // no bitmap provided, so return nil selector indicating no exclusions
+ return nil, nil
+ }
+ // create a bitmap exclusion selector
+ selector, err = faiss.NewIDSelectorBitmapNot(bm.bytes())
+ if err != nil {
+ return nil, err
+ }
+ return selector, nil
+}
+
+// Utility function to create a vector postings list from the corresponding docID and scores for each
+// unique docID retrieved from the vector index
+func getPostingsList(rs resultSet) segment.VecPostingsList {
+ // 1. returned postings list (of type PostingsList) has two types of information - docNum and its score.
+ // 2. both the values can be represented using roaring bitmaps.
+ // 3. the Iterator (of type VecPostingsIterator) returned would operate in terms of VecPostings.
+ // 4. VecPostings would just have the docNum and the score. Every call of Next()
+ // and just returns the next VecPostings. The caller would do a vp.Number()
+ // and the Score() to get the corresponding values
+ rv := &VecPostingsList{
+ postings: roaring64.New(),
+ }
+ rs.iterate(func(docID uint32, score float32) {
+ // transform the docID and score to vector code format
+ code := getVectorCode(docID, score)
+ // add to postings list, this ensures ordered storage
+ // based on the docID since it occupies the upper 32 bits
+ rv.postings.Add(code)
+ })
+ return rv
+}
+
+// ------------------------------------------------------------------------------
+// ResultSet
+// ------------------------------------------------------------------------------
+
+// resultSet is a data structure to hold (docID, score) pairs while ensuring
+// that each docID is unique. It supports efficient insertion, retrieval,
+// and iteration over the stored pairs.
+type resultSet interface {
+ // Add a (docID, score) pair to the result set.
+ put(docID uint32, score float32)
+ // Get the score for a given docID. Returns false if docID not present.
+ get(docID uint32) (float32, bool)
+ // Iterate over all (docID, score) pairs in the result set.
+ iterate(func(docID uint32, score float32))
+ // Get the size of the result set.
+ size() int64
+}
+
+// resultSetSliceThreshold defines the threshold ratio of k to total documents
+// in the index, below which a map-based resultSet is used, and above which
+// a slice-based resultSet is used.
+// It is derived using the following reasoning:
+//
+// Let N = total number of documents
+// Let K = number of top K documents to retrieve
+//
+// Memory usage if the Result Set uses a map[uint32]float32 of size K underneath:
+//
+// ~20 bytes per entry (key + value + map overhead)
+// Total ≈ 20 * K bytes
+//
+// Memory usage if the Result Set uses a slice of float32 of size N underneath:
+//
+// 4 bytes per entry
+// Total ≈ 4 * N bytes
+//
+// We want the threshold below which a map is more memory-efficient than a slice:
+//
+// 20K < 4N
+// K/N < 4/20
+//
+// Therefore, if the ratio of K to N is less than 0.2 (4/20), we use a map-based resultSet.
+const resultSetSliceThreshold float64 = 0.2
+
+// newResultSet creates a new resultSet
+func newResultSet(k int64, numDocs uint64) resultSet {
+ // if numDocs is zero (empty index), just use map-based resultSet as its a no-op
+ // else decide based the percent of documents being retrieved. If we require
+ // greater than 20% of total documents, use slice-based resultSet for better memory efficiency
+ // else use map-based resultSet
+ if numDocs == 0 || float64(k)/float64(numDocs) < resultSetSliceThreshold {
+ return newResultSetMap(k)
+ }
+ return newResultSetSlice(numDocs)
+}
+
+type resultSetMap struct {
+ data map[uint32]float32
+}
+
+func newResultSetMap(k int64) resultSet {
+ return &resultSetMap{
+ data: make(map[uint32]float32, k),
+ }
+}
+
+func (rs *resultSetMap) put(docID uint32, score float32) {
+ rs.data[docID] = score
+}
+
+func (rs *resultSetMap) get(docID uint32) (float32, bool) {
+ score, exists := rs.data[docID]
+ return score, exists
+}
+
+func (rs *resultSetMap) iterate(f func(docID uint32, score float32)) {
+ for docID, score := range rs.data {
+ f(docID, score)
+ }
+}
+
+func (rs *resultSetMap) size() int64 {
+ return int64(len(rs.data))
+}
+
+type resultSetSlice struct {
+ count int64
+ data []float32
+}
+
+func newResultSetSlice(numDocs uint64) resultSet {
+ data := make([]float32, numDocs)
+ // scores can be negative, so initialize to a sentinel value which is NaN
+ sentinel := float32(math.NaN())
+ for i := range data {
+ data[i] = sentinel
+ }
+ return &resultSetSlice{
+ count: 0,
+ data: data,
+ }
+}
+
+func (rs *resultSetSlice) put(docID uint32, score float32) {
+ // only increment count if this docID was not already present
+ if math.IsNaN(float64(rs.data[docID])) {
+ rs.count++
+ }
+ rs.data[docID] = score
+}
+
+func (rs *resultSetSlice) get(docID uint32) (float32, bool) {
+ score := rs.data[docID]
+ if math.IsNaN(float64(score)) {
+ return 0, false
+ }
+ return score, true
+}
+
+func (rs *resultSetSlice) iterate(f func(docID uint32, score float32)) {
+ for docID, score := range rs.data {
+ if !math.IsNaN(float64(score)) {
+ f(uint32(docID), score)
+ }
+ }
+}
+
+func (rs *resultSetSlice) size() int64 {
+ return rs.count
+}
+
+// -----------------------------------------------------------------------------
+// Bitmap
+// -----------------------------------------------------------------------------
+
+// bitmap is a simple, fixed-size bitmap.
+type bitmap struct {
+ bits []byte
+ size uint32
+}
+
+// newBitmap creates a new bitmap with the given number of bits
+func newBitmap(numBits uint32) *bitmap {
+ bitsetSize := (numBits + 7) / 8
+ return &bitmap{
+ bits: make([]byte, bitsetSize),
+ size: numBits,
+ }
+}
+
+// set the bit at the given position
+func (b *bitmap) set(pos uint32) {
+ if pos >= b.size {
+ return
+ }
+ // set the bit in the byte slice
+ // the byte index is pos / 8, which is equivalent to pos >> 3
+ // the bit index within that byte is pos % 8, which is equivalent to pos & 7
+ // and is from the LSB side of the byte
+ b.bits[pos>>3] |= 1 << (pos & 7)
+}
+
+// clear the bit at the given position
+func (b *bitmap) clear(pos uint32) {
+ if pos >= b.size {
+ return
+ }
+ // clear the bit in the byte slice
+ // the byte index is pos / 8, which is equivalent to pos >> 3
+ // the bit index within that byte is pos % 8, which is equivalent to pos & 7
+ // and is from the LSB side of the byte
+ b.bits[pos>>3] &^= 1 << (pos & 7)
+}
+
+// test if the bit at the given position is set
+func (b *bitmap) test(pos uint32) bool {
+ if pos >= b.size {
+ return false
+ }
+ return (b.bits[pos>>3]>>(pos&7))&1 != 0
+}
+
+// return the underlying byte slice
+func (b *bitmap) bytes() []byte {
+ return b.bits
+}
+
+// returns the number of bits currently set
+func (b *bitmap) cardinality() uint32 {
+ var count int
+ for _, byteVal := range b.bits {
+ // count the number of set bits in the byte
+ count += bits.OnesCount8(byteVal)
+ }
+ return uint32(count)
+}
+
+// isEmpty checks if the bitmap has no bits set
+// or if the cardinality (population count) is zero
+func (b *bitmap) isEmpty() bool {
+ for _, byteVal := range b.bits {
+ if byteVal != 0 {
+ return false
+ }
+ }
+ return true
+}
+
+// creates a clone of the bitmap
+func (b *bitmap) clone() *bitmap {
+ newB := &bitmap{}
+ newB.bits = slices.Clone(b.bits)
+ newB.size = b.size
+ return newB
+}
+
+// -----------------------------------------------------------------------------
+// ID Mapping
+// -----------------------------------------------------------------------------
+
+// idMapping maintains a bidirectional mapping between vector IDs and document IDs.
+// It allows efficient retrieval of document IDs for given vector IDs and vice versa.
+// The mapping assumes that vector IDs and document IDs are ordered sequentially starting from 0
+// up to numVecs-1 and numDocs-1 respectively.
+type idMapping struct {
+ vecToDoc []uint32 // vector ID -> document ID (size = numVecs)
+ docToVec [][]uint32 // document ID -> vector IDs (size = numDocs)
+
+ // keep track of sizes for convenience
+ numVecs uint32
+ numDocs uint32
+}
+
+// newIDMapping creates a new idMapping with the specified sizes
+// numVecs: number of vectors (for vecToDoc mapping)
+// numDocs: number of documents (for docToVec mapping)
+func newIDMapping(numVecs, numDocs uint32) *idMapping {
+ return &idMapping{
+ vecToDoc: make([]uint32, numVecs),
+ docToVec: make([][]uint32, numDocs),
+ numVecs: numVecs,
+ numDocs: numDocs,
+ }
+}
+
+// add a mapping from vector ID to document ID and vice versa
+func (m *idMapping) add(vecID uint32, docID uint32) {
+ // safety check to avoid out of bounds access
+ if vecID >= m.numVecs || docID >= m.numDocs {
+ return
+ }
+ m.vecToDoc[vecID] = docID
+ m.docToVec[docID] = append(m.docToVec[docID], vecID)
+}
+
+// return the number of vectors in the mapping
+func (m *idMapping) numVectors() uint32 {
+ return m.numVecs
+}
+
+// return the number of documents in the mapping
+func (m *idMapping) numDocuments() uint32 {
+ return m.numDocs
+}
+
+// retrieve the document ID for a given vector ID
+func (m *idMapping) docForVec(vecID uint32) (uint32, bool) {
+ if vecID >= m.numVecs {
+ return 0, false
+ }
+ return m.vecToDoc[vecID], true
+}
+
+// retrieve the vector IDs for a given document ID
+func (m *idMapping) vecsForDoc(docID uint32) ([]uint32, bool) {
+ if docID >= m.numDocs {
+ return nil, false
+ }
+ return m.docToVec[docID], true
+}
+
+// ------------------------------------------------------------------------------
+// Quick Select
+// ------------------------------------------------------------------------------
+
+// topNIDsByDistance performs an in-place Quickselect on the dist slice (while
+// keeping ids aligned with their corresponding distances) to find the N largest
+// distances without fully sorting the data. It partitions the array such that
+// the element at index len(dist)-n is the pivot separating the top-N largest
+// values from the rest, and then returns the last N elements of both dist and
+// ids (unordered)
+func topNIDsByDistance(dist []float32, ids []int64, n int) ([]float32, []int64) {
+ if n <= 0 || n > len(dist) {
+ return nil, nil
+ }
+
+ // We want the N largest distances
+ target := len(dist) - n
+
+ left := 0
+ right := len(dist) - 1
+ for left < right {
+ pivotVal := dist[right]
+ store := left
+
+ for i := left; i < right; i++ {
+ // We want largest distances ⇒ partition small ones left
+ if dist[i] < pivotVal {
+ dist[i], dist[store] = dist[store], dist[i]
+ ids[i], ids[store] = ids[store], ids[i]
+ store++
+ }
+ }
+
+ dist[store], dist[right] = dist[right], dist[store]
+ ids[store], ids[right] = ids[right], ids[store]
+ if store == target {
+ break
+ } else if store < target {
+ left = store + 1
+ } else {
+ right = store - 1
+ }
+ }
+
+ // Return top-N IDs (unordered)
+ return dist[target:], ids[target:]
+}
+
+// -----------------------------------------------------------------------------
+// vectorSet
+// -----------------------------------------------------------------------------
+type vectorSet struct {
+ // dimensionality of each vector
+ dim int
+ // number of vectors represented
+ nvecs int
+ // float vectors stored in row-major format,
+ // i.e. for N vectors of D dimensions,
+ // the length of this slice is N*D,
+ floatData []float32
+ // row-major binary representation of the float vectors,
+ // where each bit represents the sign bit
+ // of the corresponding float value.
+ binaryData []uint8
+}
+
+func newVectorSet(dim int, data []float32) (*vectorSet, error) {
+ if len(data) == 0 || dim <= 0 || len(data)%dim != 0 {
+ return nil, fmt.Errorf("invalid vector data: dims %d, data length %d", dim, len(data))
+ }
+ nvecs := len(data) / dim
+ return &vectorSet{
+ dim: dim,
+ nvecs: nvecs,
+ floatData: data,
+ }, nil
+}
+
+// converts float32 vectors into binary format based on the sign bit
+// of the float32 values.
+func convertToBinary(vecs []float32, dims int) []uint8 {
+ nvecs := len(vecs) / dims
+ packed := make([]uint8, 0, nvecs*(dims+7)/8)
+ var cur uint8
+ var count int
+ for i := 0; i < nvecs; i++ {
+ count = 0
+ for j := 0; j < dims; j++ {
+ value := vecs[i*dims+j]
+ // Apply the threshold: convert the float32 to 1 or 0 based on threshold
+ if value >= 0.0 {
+ // Shift the bit into the correct position in the byte
+ cur |= (1 << (7 - count))
+ }
+ count++
+ // When we have 8 bits, store the byte and reset for the next byte
+ if count == 8 {
+ packed = append(packed, cur)
+ cur = 0
+ count = 0
+ }
+ }
+ // If there are any remaining bits, pack them into a byte and append
+ if count > 0 {
+ cur <<= (8 - count)
+ packed = append(packed, cur)
+ }
+ }
+ return packed
+}
+
+func (v *vectorSet) binarize() {
+ // if binaryData is already populated, no need to convert again
+ if v.binaryData != nil {
+ return
+ }
+ // convert the floatData to binary format and store in binaryData
+ v.binaryData = convertToBinary(v.floatData, v.dim)
+}
+
+func (v *vectorSet) clone() *vectorSet {
+ // create a new vectorSet with the same dimensions and number of vectors
+ clone := &vectorSet{
+ dim: v.dim,
+ nvecs: v.nvecs,
+ floatData: slices.Clone(v.floatData),
+ binaryData: slices.Clone(v.binaryData),
+ }
+ return clone
+}
+
+func (v *vectorSet) mergeWith(other *vectorSet) {
+ // sanity check to ensure the two vector sets are compatible for merging
+ if v.dim != other.dim {
+ return
+ }
+ // merge the float data
+ v.floatData = append(v.floatData, other.floatData...)
+ v.nvecs += other.nvecs
+ // invalidate the binary data as the float data has changed
+ v.binaryData = nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/file_callbacks.go b/vendor/github.com/blevesearch/zapx/v17/file_callbacks.go
new file mode 100644
index 0000000000..f3418a5cb0
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/file_callbacks.go
@@ -0,0 +1,129 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+// implied. See the License for the specific language governing
+// permissions and limitations under the License.
+
+package zap
+
+import (
+ "fmt"
+
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// This file provides a mechanism for users of zap to provide callbacks
+// that can process data before it is written to disk, and after it is read
+// from disk. This can be used for things like encryption, compression, etc.
+
+// The user is responsible for ensuring that the writer and reader callbacks
+// are compatible with each other, and that any state needed by the callbacks
+// is managed appropriately. For example, if the writer callback uses a
+// unique key or nonce per write, the reader callback must be able to
+// determine the correct key or nonce to use for each read.
+
+// The callbacks are identified by an id string, which is returned by the
+// WriterCallbackGetter. The same id string is passed to the ReaderCallbackGetter
+// when creating a reader. This allows the reader to determine which
+// callback to use for a given file.
+
+// An example implementation using AES-GCM encryption is provided in
+// file_callbacks_test.go within initFileCallbacks().
+
+// FileWriter wraps a CountHashWriter and applies a user provided
+// writer callback to the data being written.
+type FileWriter struct {
+ id string
+ c *CountHashWriter
+ processor func(data []byte) []byte
+}
+
+// creates an empty FileWriter with no callback. Used
+// when we are writing data that is not going to be persisted
+func NewFileWriterEmpty(c *CountHashWriter) *FileWriter {
+ rv := &FileWriter{
+ c: c,
+ }
+
+ return rv
+}
+
+// NewFileWriter creates a FileWriter with the provided CountHashWriter and applies
+// the writer callback identified by the context.
+func NewFileWriter(c *CountHashWriter, context []byte) (*FileWriter, error) {
+ rv := &FileWriter{
+ c: c,
+ }
+
+ if index.WriterHook != nil {
+ var err error
+ rv.id, rv.processor, err = index.WriterHook(context)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return rv, nil
+}
+
+func (w *FileWriter) Write(data []byte) (int, error) {
+ return w.c.Write(data)
+}
+
+// process applies the writer callback to the data, if one is set
+func (w *FileWriter) process(data []byte) []byte {
+ if w.processor != nil {
+ return w.processor(data)
+ }
+ return data
+}
+
+func (w *FileWriter) Count() int {
+ return w.c.Count()
+}
+
+func (w *FileWriter) Sum32() uint32 {
+ return w.c.Sum32()
+}
+
+// FileReader wraps a reader callback to be applied to data read from a file.
+type FileReader struct {
+ id string
+ processor func(data []byte) ([]byte, error)
+}
+
+// NewFileReader creates a FileReader with the reader callback identified by the context.
+// The id is used to identify which callback to use when reading data.
+func NewFileReader(id string, context []byte) (*FileReader, error) {
+ rv := &FileReader{
+ id: id,
+ }
+
+ if index.ReaderHook != nil {
+ var err error
+ rv.processor, err = index.ReaderHook(id, context)
+ if err != nil {
+ return nil, err
+ }
+ } else if id != "" {
+ return nil, fmt.Errorf("reader callback id %s provided but no ReaderHook is set", id)
+ }
+
+ return rv, nil
+}
+
+// process applies the reader callback to the data, if one is set
+func (r *FileReader) process(data []byte) ([]byte, error) {
+ if r.processor != nil {
+ return r.processor(data)
+ }
+ return data, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/intDecoder.go b/vendor/github.com/blevesearch/zapx/v17/intDecoder.go
new file mode 100644
index 0000000000..9c54b28cb7
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/intDecoder.go
@@ -0,0 +1,145 @@
+// Copyright (c) 2019 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+type chunkedIntDecoder struct {
+ startOffset uint64
+ dataStartOffset uint64
+ chunkOffsets []uint64
+ curChunkBytes []byte
+ data []byte
+ r *memUvarintReader
+ fr *FileReader
+
+ bytesRead uint64
+}
+
+// newChunkedIntDecoder expects an optional or reset chunkedIntDecoder for better reuse.
+func newChunkedIntDecoder(buf []byte, offset uint64, rv *chunkedIntDecoder, fr *FileReader) *chunkedIntDecoder {
+ if rv == nil {
+ rv = &chunkedIntDecoder{startOffset: offset, data: buf}
+ } else {
+ rv.startOffset = offset
+ rv.data = buf
+ }
+
+ var n, numChunks uint64
+ var read int
+ if offset == termNotEncoded {
+ numChunks = 0
+ } else {
+ numChunks, read = binary.Uvarint(buf[offset+n : offset+n+binary.MaxVarintLen64])
+ }
+
+ n += uint64(read)
+ if cap(rv.chunkOffsets) >= int(numChunks) {
+ rv.chunkOffsets = rv.chunkOffsets[:int(numChunks)]
+ } else {
+ rv.chunkOffsets = make([]uint64, int(numChunks))
+ }
+ for i := 0; i < int(numChunks); i++ {
+ rv.chunkOffsets[i], read = binary.Uvarint(buf[offset+n : offset+n+binary.MaxVarintLen64])
+ n += uint64(read)
+ }
+ rv.bytesRead += n
+ rv.dataStartOffset = offset + n
+ rv.fr = fr
+ return rv
+}
+
+// A util function which fetches the query time
+// specific bytes encoded by intcoder (for eg the
+// freqNorm and location details of a term in document)
+// the loadChunk retrieves the next chunk and the
+// number of bytes retrieve in that operation is accounted
+func (d *chunkedIntDecoder) getBytesRead() uint64 {
+ return d.bytesRead
+}
+
+func (d *chunkedIntDecoder) loadChunk(chunk int) error {
+ if d.startOffset == termNotEncoded {
+ d.r = newMemUvarintReader([]byte(nil))
+ return nil
+ }
+
+ if chunk >= len(d.chunkOffsets) {
+ return fmt.Errorf("tried to load freq chunk that doesn't exist %d/(%d)",
+ chunk, len(d.chunkOffsets))
+ }
+
+ end, start := d.dataStartOffset, d.dataStartOffset
+ s, e := readChunkBoundary(chunk, d.chunkOffsets)
+ start += s
+ end += e
+
+ var err error
+ d.curChunkBytes, err = d.fr.process(d.data[start:end])
+ if err != nil {
+ return fmt.Errorf("error processing chunk %d: %w", chunk, err)
+ }
+ d.bytesRead += end - start
+ if d.r == nil {
+ d.r = newMemUvarintReader(d.curChunkBytes)
+ } else {
+ d.r.Reset(d.curChunkBytes)
+ }
+
+ return nil
+}
+
+func (d *chunkedIntDecoder) reset() {
+ d.startOffset = 0
+ d.dataStartOffset = 0
+ d.chunkOffsets = d.chunkOffsets[:0]
+ d.curChunkBytes = d.curChunkBytes[:0]
+ d.bytesRead = 0
+ d.data = d.data[:0]
+ if d.r != nil {
+ d.r.Reset([]byte(nil))
+ }
+}
+
+func (d *chunkedIntDecoder) isNil() bool {
+ return d.curChunkBytes == nil || len(d.curChunkBytes) == 0
+}
+
+func (d *chunkedIntDecoder) readUvarint() (uint64, error) {
+ return d.r.ReadUvarint()
+}
+
+func (d *chunkedIntDecoder) readBytes(start, end int) []byte {
+ return d.curChunkBytes[start:end]
+}
+
+func (d *chunkedIntDecoder) SkipUvarint() {
+ d.r.SkipUvarint()
+}
+
+func (d *chunkedIntDecoder) SkipBytes(count int) {
+ d.r.SkipBytes(count)
+}
+
+func (d *chunkedIntDecoder) Len() int {
+ return d.r.Len()
+}
+
+func (d *chunkedIntDecoder) remainingLen() int {
+ return len(d.curChunkBytes) - d.r.Len()
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/intcoder.go b/vendor/github.com/blevesearch/zapx/v17/intcoder.go
new file mode 100644
index 0000000000..d3d354577b
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/intcoder.go
@@ -0,0 +1,236 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "io"
+)
+
+// We can safely use 0 to represent termNotEncoded since 0
+// could never be a valid address for term location information.
+// (stored field index is always non-empty and earlier in the
+// file)
+const termNotEncoded = 0
+
+type chunkedIntCoder struct {
+ final []byte
+ chunkSize uint64
+ chunkBuf bytes.Buffer
+ chunkLens []uint64
+ currChunk uint64
+
+ buf []byte
+
+ bytesWritten uint64
+}
+
+// newChunkedIntCoder returns a new chunk int coder which packs data into
+// chunks based on the provided chunkSize and supports up to the specified
+// maxDocNum
+func newChunkedIntCoder(chunkSize uint64, maxDocNum uint64) *chunkedIntCoder {
+ total := maxDocNum/chunkSize + 1
+ rv := &chunkedIntCoder{
+ chunkSize: chunkSize,
+ chunkLens: make([]uint64, total),
+ final: make([]byte, 0, 64),
+ }
+
+ return rv
+}
+
+// Reset lets you reuse this chunked int coder. buffers are reset and reused
+// from previous use. you cannot change the chunk size or max doc num.
+func (c *chunkedIntCoder) Reset() {
+ c.final = c.final[:0]
+ c.bytesWritten = 0
+ c.chunkBuf.Reset()
+ c.currChunk = 0
+ for i := range c.chunkLens {
+ c.chunkLens[i] = 0
+ }
+}
+
+// SetChunkSize changes the chunk size. It is only valid to do so
+// with a new chunkedIntCoder, or immediately after calling Reset()
+func (c *chunkedIntCoder) SetChunkSize(chunkSize uint64, maxDocNum uint64) {
+ total := int(maxDocNum/chunkSize + 1)
+ c.chunkSize = chunkSize
+ if cap(c.chunkLens) < total {
+ c.chunkLens = make([]uint64, total)
+ } else {
+ c.chunkLens = c.chunkLens[:total]
+ }
+}
+
+func (c *chunkedIntCoder) incrementBytesWritten(val uint64) {
+ c.bytesWritten += val
+}
+
+func (c *chunkedIntCoder) getBytesWritten() uint64 {
+ return c.bytesWritten
+}
+
+// Add encodes the provided integers into the correct chunk for the provided
+// doc num. You MUST call Add() with increasing docNums.
+func (c *chunkedIntCoder) Add(docNum uint64, vals ...uint64) error {
+ chunk := docNum / c.chunkSize
+ if chunk != c.currChunk {
+ // starting a new chunk
+ c.Close()
+ c.chunkBuf.Reset()
+ c.currChunk = chunk
+ }
+
+ if len(c.buf) < binary.MaxVarintLen64 {
+ c.buf = make([]byte, binary.MaxVarintLen64)
+ }
+
+ for _, val := range vals {
+ wb := binary.PutUvarint(c.buf, val)
+ _, err := c.chunkBuf.Write(c.buf[:wb])
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *chunkedIntCoder) AddBytes(docNum uint64, buf []byte) error {
+ chunk := docNum / c.chunkSize
+ if chunk != c.currChunk {
+ // starting a new chunk
+ c.Close()
+ c.chunkBuf.Reset()
+ c.currChunk = chunk
+ }
+
+ _, err := c.chunkBuf.Write(buf)
+ return err
+}
+
+// Close indicates you are done calling Add() this allows the final chunk
+// to be encoded.
+func (c *chunkedIntCoder) Close() {
+ encodingBytes := c.chunkBuf.Bytes()
+ c.incrementBytesWritten(uint64(len(encodingBytes)))
+ c.chunkLens[c.currChunk] = uint64(len(encodingBytes))
+ c.final = append(c.final, encodingBytes...)
+ c.currChunk = uint64(cap(c.chunkLens)) // sentinel to detect double close
+}
+
+// Write commits all the encoded chunked integers to the provided writer.
+func (c *chunkedIntCoder) Write(w io.Writer) (int, error) {
+ bufNeeded := binary.MaxVarintLen64 * (1 + len(c.chunkLens))
+ if len(c.buf) < bufNeeded {
+ c.buf = make([]byte, bufNeeded)
+ }
+ buf := c.buf
+
+ // convert the chunk lengths into chunk offsets
+ chunkOffsets := modifyLengthsToEndOffsets(c.chunkLens)
+
+ // process each chunk's data individually and recalculate the chunk
+ // boundaries if necessary.
+ if fw, ok := w.(*FileWriter); ok && fw != nil {
+ var prevOffset int
+ processedBuf := make([]byte, 0)
+ for i := 0; i < len(chunkOffsets); i++ {
+ if chunkOffsets[i] == uint64(prevOffset) {
+ continue
+ }
+ buf := fw.process(c.final[prevOffset:chunkOffsets[i]])
+ processedBuf = append(processedBuf, buf...)
+ prevOffset = int(chunkOffsets[i])
+ c.chunkLens[i] = uint64(len(buf))
+ }
+ c.final = processedBuf
+ chunkOffsets = modifyLengthsToEndOffsets(c.chunkLens)
+ }
+
+ // write out the number of chunks & each chunk offsets
+ n := binary.PutUvarint(buf, uint64(len(chunkOffsets)))
+ for _, chunkOffset := range chunkOffsets {
+ n += binary.PutUvarint(buf[n:], chunkOffset)
+ }
+
+ tw, err := w.Write(buf[:n])
+ if err != nil {
+ return tw, err
+ }
+
+ // write out the data
+ nw, err := w.Write(c.final)
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+ return tw, nil
+}
+
+// writeAt commits all the encoded chunked integers to the provided writer
+// and returns the starting offset, total bytes written and an error
+func (c *chunkedIntCoder) writeAt(w io.Writer) (uint64, int, error) {
+ startOffset := uint64(termNotEncoded)
+ if len(c.final) <= 0 {
+ return startOffset, 0, nil
+ }
+
+ if fw, ok := w.(*FileWriter); ok && fw != nil {
+ startOffset = uint64(fw.Count())
+ }
+
+ tw, err := c.Write(w)
+ return startOffset, tw, err
+}
+
+func (c *chunkedIntCoder) FinalSize() int {
+ return len(c.final)
+}
+
+// modifyLengthsToEndOffsets converts the chunk length array
+// to a chunk offset array. The readChunkBoundary
+// will figure out the start and end of every chunk from
+// these offsets. Starting offset of i'th index is stored
+// in i-1'th position except for 0'th index and ending offset
+// is stored at i'th index position.
+// For 0'th element, starting position is always zero.
+// eg:
+// Lens -> 5 5 5 5 => 5 10 15 20
+// Lens -> 0 5 0 5 => 0 5 5 10
+// Lens -> 0 0 0 5 => 0 0 0 5
+// Lens -> 5 0 0 0 => 5 5 5 5
+// Lens -> 0 5 0 0 => 0 5 5 5
+// Lens -> 0 0 5 0 => 0 0 5 5
+func modifyLengthsToEndOffsets(lengths []uint64) []uint64 {
+ var runningOffset uint64
+ var index, i int
+ for i = 1; i <= len(lengths); i++ {
+ runningOffset += lengths[i-1]
+ lengths[index] = runningOffset
+ index++
+ }
+ return lengths
+}
+
+func readChunkBoundary(chunk int, offsets []uint64) (uint64, uint64) {
+ var start uint64
+ if chunk > 0 {
+ start = offsets[chunk-1]
+ }
+ return start, offsets[chunk]
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/inverted_text_cache.go b/vendor/github.com/blevesearch/zapx/v17/inverted_text_cache.go
new file mode 100644
index 0000000000..32b9f7bd34
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/inverted_text_cache.go
@@ -0,0 +1,107 @@
+// Copyright (c) 2025 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+ "sync"
+
+ "github.com/blevesearch/vellum"
+)
+
+func newInvertedIndexCache() *invertedIndexCache {
+ return &invertedIndexCache{
+ cache: make(map[uint16]*invertedCacheEntry),
+ }
+}
+
+type invertedIndexCache struct {
+ m sync.RWMutex
+
+ cache map[uint16]*invertedCacheEntry
+}
+
+func (sc *invertedIndexCache) Clear() {
+ sc.m.Lock()
+ sc.cache = nil
+ sc.m.Unlock()
+}
+
+// loadOrCreate loads the inverted index cache for the specified fieldID if it is already present,
+// or creates it if not. The inverted index cache for a fieldID consists of an FST (Finite State Transducer):
+// - A Vellum FST (Finite State Transducer) representing the TermDictionary.
+// This function returns the loaded or newly created FST, and the number of bytes read from the provided memory slice,
+// if the cache was created.
+func (sc *invertedIndexCache) loadOrCreate(fieldID uint16, mem []byte, fr *FileReader) (*vellum.FST, uint64, error) {
+ sc.m.RLock()
+ entry, ok := sc.cache[fieldID]
+ if ok {
+ sc.m.RUnlock()
+ return entry.load()
+ }
+
+ sc.m.RUnlock()
+
+ sc.m.Lock()
+ defer sc.m.Unlock()
+
+ entry, ok = sc.cache[fieldID]
+ if ok {
+ return entry.load()
+ }
+
+ return sc.createAndCacheLOCKED(fieldID, mem, fr)
+}
+
+// createAndCacheLOCKED creates the inverted index cache for the specified fieldID and caches it.
+func (sc *invertedIndexCache) createAndCacheLOCKED(fieldID uint16, mem []byte, fr *FileReader) (*vellum.FST, uint64, error) {
+ var pos uint64
+ vellumLen, read := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if vellumLen == 0 || read <= 0 {
+ return nil, 0, fmt.Errorf("vellum length is 0")
+ }
+ pos += uint64(read)
+ fstBytes, err := fr.process(mem[pos : pos+vellumLen])
+ if err != nil {
+ return nil, 0, fmt.Errorf("error processing vellum bytes: %v", err)
+ }
+ fst, err := vellum.Load(fstBytes)
+ if err != nil {
+ return nil, 0, fmt.Errorf("vellum err: %v", err)
+ }
+ pos += vellumLen
+ sc.insertLOCKED(fieldID, fst)
+ return fst, pos, nil
+}
+
+// insertLOCKED inserts the vellum FST into the cache for the specified fieldID.
+func (sc *invertedIndexCache) insertLOCKED(fieldID uint16, fst *vellum.FST) {
+ _, ok := sc.cache[fieldID]
+ if !ok {
+ sc.cache[fieldID] = &invertedCacheEntry{
+ fst: fst,
+ }
+ }
+}
+
+// invertedCacheEntry is the vellum FST and is the value stored in the invertedIndexCache cache, for a given fieldID.
+type invertedCacheEntry struct {
+ fst *vellum.FST
+}
+
+func (ce *invertedCacheEntry) load() (*vellum.FST, uint64, error) {
+ return ce.fst, 0, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/memuvarint.go b/vendor/github.com/blevesearch/zapx/v17/memuvarint.go
new file mode 100644
index 0000000000..48a57f9c85
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/memuvarint.go
@@ -0,0 +1,103 @@
+// Copyright (c) 2020 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "fmt"
+)
+
+type memUvarintReader struct {
+ C int // index of next byte to read from S
+ S []byte
+}
+
+func newMemUvarintReader(s []byte) *memUvarintReader {
+ return &memUvarintReader{S: s}
+}
+
+// Len returns the number of unread bytes.
+func (r *memUvarintReader) Len() int {
+ n := len(r.S) - r.C
+ if n < 0 {
+ return 0
+ }
+ return n
+}
+
+// ReadUvarint reads an encoded uint64. The original code this was
+// based on is at encoding/binary/ReadUvarint().
+func (r *memUvarintReader) ReadUvarint() (uint64, error) {
+ if r.C >= len(r.S) {
+ // nothing else to read
+ return 0, nil
+ }
+
+ var x uint64
+ var s uint
+ var C = r.C
+ var S = r.S
+
+ for {
+ b := S[C]
+ C++
+
+ if b < 0x80 {
+ r.C = C
+
+ // why 63? The original code had an 'i += 1' loop var and
+ // checked for i > 9 || i == 9 ...; but, we no longer
+ // check for the i var, but instead check here for s,
+ // which is incremented by 7. So, 7*9 == 63.
+ //
+ // why the "extra" >= check? The normal case is that s <
+ // 63, so we check this single >= guard first so that we
+ // hit the normal, nil-error return pathway sooner.
+ if s >= 63 && (s > 63 || b > 1) {
+ return 0, fmt.Errorf("memUvarintReader overflow")
+ }
+
+ return x | uint64(b)<= len(r.S) {
+ return
+ }
+
+ b := r.S[r.C]
+ r.C++
+
+ if b < 0x80 {
+ return
+ }
+ }
+}
+
+// SkipBytes skips a count number of bytes.
+func (r *memUvarintReader) SkipBytes(count int) {
+ r.C = r.C + count
+}
+
+func (r *memUvarintReader) Reset(s []byte) {
+ r.C = 0
+ r.S = s
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/merge.go b/vendor/github.com/blevesearch/zapx/v17/merge.go
new file mode 100644
index 0000000000..a029aea0e5
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/merge.go
@@ -0,0 +1,821 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bufio"
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "os"
+ "sort"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ seg "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/golang/snappy"
+)
+
+var DefaultFileMergerBufferSize = 1024 * 1024
+
+const docDropped = math.MaxUint64 // sentinel docNum to represent a deleted doc
+
+// Merge takes a slice of segments and bit masks describing which
+// documents may be dropped, and creates a new segment containing the
+// remaining data. This new segment is built at the specified path.
+func (z *ZapPlugin) Merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, nil)
+}
+
+func (z *ZapPlugin) MergeUsing(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ return z.merge(segments, drops, path, closeCh, s, config)
+}
+
+func (*ZapPlugin) merge(segments []seg.Segment, drops []*roaring.Bitmap, path string,
+ closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ segmentBases := make([]*SegmentBase, len(segments))
+ for segmenti, segment := range segments {
+ switch segmentx := segment.(type) {
+ case *Segment:
+ segmentBases[segmenti] = &segmentx.SegmentBase
+ case *SegmentBase:
+ segmentBases[segmenti] = segmentx
+ default:
+ panic(fmt.Sprintf("oops, unexpected segment type: %T", segment))
+ }
+ }
+ return mergeSegmentBases(segmentBases, drops, path, DefaultChunkMode, closeCh, s, config)
+}
+
+func mergeSegmentBases(segmentBases []*SegmentBase, drops []*roaring.Bitmap, path string,
+ chunkMode uint32, closeCh chan struct{}, s seg.StatsReporter, config map[string]interface{}) (
+ [][]uint64, uint64, error) {
+ flag := os.O_RDWR | os.O_CREATE
+
+ f, err := os.OpenFile(path, flag, 0600)
+ if err != nil {
+ return nil, 0, err
+ }
+
+ cleanup := func() {
+ _ = f.Close()
+ _ = os.Remove(path)
+ }
+
+ // buffer the output
+ br := bufio.NewWriterSize(f, DefaultFileMergerBufferSize)
+
+ // wrap it for counting (tracking offsets)
+ cr := NewCountHashWriterWithStatsReporter(br, s)
+ w, err := NewFileWriter(cr, []byte(path))
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ newDocNums, numDocs, storedIndexOffset, _, _, sectionsIndexOffset, err :=
+ mergeToWriter(segmentBases, drops, chunkMode, w, closeCh, config)
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ err = persistFooter(numDocs, storedIndexOffset, sectionsIndexOffset, chunkMode, cr.Sum32(), w, w.id)
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ err = br.Flush()
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ err = f.Sync()
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ err = f.Close()
+ if err != nil {
+ cleanup()
+ return nil, 0, err
+ }
+
+ return newDocNums, uint64(cr.Count()), nil
+}
+
+// Remove fields that have been completely deleted from fieldsInv
+func filterFields(fieldsInv []string, fieldInfo map[string]*index.UpdateFieldInfo) []string {
+ idx := 0
+ for _, field := range fieldsInv {
+ if val, ok := fieldInfo[field]; ok && val.Deleted {
+ continue
+ }
+ fieldsInv[idx] = field
+ idx++
+ }
+ return fieldsInv[:idx]
+}
+
+// Update field options using updateFieldInfo to override the options
+// selected during mergeFields, if needed. This includes removing field
+// options for deleted fields and updating options for fields with changes
+// that have not yet been propagated because a new segment has not been created.
+func finalizeFieldOptions(fieldOptions map[string]index.FieldIndexingOptions,
+ updatedFields map[string]*index.UpdateFieldInfo) map[string]index.FieldIndexingOptions {
+ for field, opts := range fieldOptions {
+ if info, ok := updatedFields[field]; ok {
+ // if field is deleted, remove its options
+ if info.Deleted {
+ delete(fieldOptions, field)
+ continue
+ }
+ // otherwise, update options based on info
+ if info.Index {
+ // ensure indexing is disabled
+ opts &^= index.IndexField
+ }
+ if info.Store {
+ // ensure storing is disabled
+ opts &^= index.StoreField
+ }
+ if info.DocValues {
+ // ensure doc values is disabled
+ opts &^= index.DocValues
+ }
+ fieldOptions[field] = opts
+ }
+ }
+ return fieldOptions
+}
+
+func mergeToWriter(segments []*SegmentBase, drops []*roaring.Bitmap,
+ chunkMode uint32, w *FileWriter, closeCh chan struct{}, config map[string]interface{}) (
+ newDocNums [][]uint64, numDocs, storedIndexOffset uint64,
+ fieldsInv []string, fieldsMap map[string]uint16, sectionsIndexOffset uint64,
+ err error) {
+
+ var fieldsSame bool
+ var fieldsOptions map[string]index.FieldIndexingOptions
+ fieldsSame, fieldsInv, fieldsOptions = mergeFields(segments)
+ updatedFields := mergeUpdatedFields(segments)
+ fieldsInv = filterFields(fieldsInv, updatedFields)
+ fieldsMap = mapFields(fieldsInv)
+ if len(updatedFields) > 0 {
+ // finalize field options based on updated field info
+ fieldsOptions = finalizeFieldOptions(fieldsOptions, updatedFields)
+ // fieldsSame cannot be true if fields were deleted
+ fieldsSame = false
+ }
+
+ numDocs = computeNewDocCount(segments, drops)
+
+ if isClosed(closeCh) {
+ return nil, 0, 0, nil, nil, 0, seg.ErrClosed
+ }
+
+ // the merge opaque is especially important when it comes to tracking the file
+ // offset a field of a particular section is at. This will be used to write the
+ // offsets in the fields section index of the file (the final merged file).
+ mergeOpaque := map[int]resetable{}
+ args := map[string]interface{}{
+ "chunkMode": chunkMode,
+ "fieldsSame": fieldsSame,
+ "fieldsMap": fieldsMap,
+ "numDocs": numDocs,
+ "fieldsOptions": fieldsOptions,
+ "config": config,
+ }
+
+ if numDocs > 0 {
+ storedIndexOffset, newDocNums, err = mergeStoredAndRemap(segments, drops,
+ fieldsMap, fieldsInv, fieldsOptions, fieldsSame, numDocs, w, closeCh)
+ if err != nil {
+ return nil, 0, 0, nil, nil, 0, err
+ }
+
+ // at this point, ask each section implementation to merge itself
+ for i, x := range segmentSections {
+ mergeOpaque[int(i)] = x.InitOpaque(args)
+ err = x.Merge(mergeOpaque, segments, drops, fieldsInv, newDocNums, w, closeCh)
+ if err != nil {
+ return nil, 0, 0, nil, nil, 0, err
+ }
+ }
+ }
+
+ // we can persist the fields section index now, this will point
+ // to the various indexes (each in different section) available for a field.
+ sectionsIndexOffset, err = persistFieldsSection(fieldsInv, fieldsOptions, w, mergeOpaque)
+ if err != nil {
+ return nil, 0, 0, nil, nil, 0, err
+ }
+
+ return newDocNums, numDocs, storedIndexOffset, fieldsInv, fieldsMap, sectionsIndexOffset, nil
+}
+
+// mapFields takes the fieldsInv list and returns a map of fieldName
+// to fieldID+1
+func mapFields(fields []string) map[string]uint16 {
+ rv := make(map[string]uint16, len(fields))
+ for i, fieldName := range fields {
+ rv[fieldName] = uint16(i) + 1
+ }
+ return rv
+}
+
+// computeNewDocCount determines how many documents will be in the newly
+// merged segment when obsoleted docs are dropped
+func computeNewDocCount(segments []*SegmentBase, drops []*roaring.Bitmap) uint64 {
+ var newDocCount uint64
+ for segI, segment := range segments {
+ newDocCount += segment.numDocs
+ if drops[segI] != nil {
+ newDocCount -= drops[segI].GetCardinality()
+ }
+ }
+ return newDocCount
+}
+
+func mergeTermFreqNormLocsByCopying(term []byte, postItr *PostingsIterator,
+ newDocNums []uint64, newRoaring *roaring.Bitmap,
+ tfEncoder *chunkedIntCoder, locEncoder *chunkedIntCoder) (
+ lastDocNum uint64, lastFreq uint64, lastNorm uint64, err error) {
+ nextDocNum, nextFreq, nextNorm, nextFreqNormBytes, nextLocBytes, err :=
+ postItr.nextBytes()
+ for err == nil && len(nextFreqNormBytes) > 0 {
+ hitNewDocNum := newDocNums[nextDocNum]
+ if hitNewDocNum == docDropped {
+ return 0, 0, 0, fmt.Errorf("see hit with dropped doc num")
+ }
+
+ newRoaring.Add(uint32(hitNewDocNum))
+
+ err = tfEncoder.AddBytes(hitNewDocNum, nextFreqNormBytes)
+ if err != nil {
+ return 0, 0, 0, err
+ }
+
+ if len(nextLocBytes) > 0 {
+ err = locEncoder.AddBytes(hitNewDocNum, nextLocBytes)
+ if err != nil {
+ return 0, 0, 0, err
+ }
+ }
+
+ lastDocNum = hitNewDocNum
+ lastFreq = nextFreq
+ lastNorm = nextNorm
+
+ nextDocNum, nextFreq, nextNorm, nextFreqNormBytes, nextLocBytes, err =
+ postItr.nextBytes()
+ }
+
+ return lastDocNum, lastFreq, lastNorm, err
+}
+
+func mergeTermFreqNormLocs(fieldsMap map[string]uint16, term []byte, postItr *PostingsIterator,
+ newDocNums []uint64, newRoaring *roaring.Bitmap,
+ tfEncoder *chunkedIntCoder, locEncoder *chunkedIntCoder, bufLoc []uint64) (
+ lastDocNum uint64, lastFreq uint64, lastNorm uint64, bufLocOut []uint64, err error) {
+ next, err := postItr.Next()
+ for next != nil && err == nil {
+ hitNewDocNum := newDocNums[next.Number()]
+ if hitNewDocNum == docDropped {
+ return 0, 0, 0, nil, fmt.Errorf("see hit with dropped docNum")
+ }
+
+ newRoaring.Add(uint32(hitNewDocNum))
+
+ nextFreq := next.Frequency()
+ var nextNorm uint64
+ if pi, ok := next.(*Posting); ok {
+ nextNorm = pi.NormUint64()
+ } else {
+ return 0, 0, 0, nil, fmt.Errorf("unexpected posting type %T", next)
+ }
+
+ locs := next.Locations()
+
+ if nextFreq > 0 {
+ err = tfEncoder.Add(hitNewDocNum,
+ encodeFreqHasLocs(nextFreq, len(locs) > 0), nextNorm)
+ } else {
+ err = tfEncoder.Add(hitNewDocNum,
+ encodeFreqHasLocs(nextFreq, len(locs) > 0))
+ }
+ if err != nil {
+ return 0, 0, 0, nil, err
+ }
+
+ if len(locs) > 0 {
+ numBytesLocs := 0
+ for _, loc := range locs {
+ ap := loc.ArrayPositions()
+ numBytesLocs += totalUvarintBytes(uint64(fieldsMap[loc.Field()]-1),
+ loc.Pos(), loc.Start(), loc.End(), uint64(len(ap)), ap)
+ }
+
+ err = locEncoder.Add(hitNewDocNum, uint64(numBytesLocs))
+ if err != nil {
+ return 0, 0, 0, nil, err
+ }
+
+ for _, loc := range locs {
+ ap := loc.ArrayPositions()
+ if cap(bufLoc) < 5+len(ap) {
+ bufLoc = make([]uint64, 0, 5+len(ap))
+ }
+ args := bufLoc[0:5]
+ args[0] = uint64(fieldsMap[loc.Field()] - 1)
+ args[1] = loc.Pos()
+ args[2] = loc.Start()
+ args[3] = loc.End()
+ args[4] = uint64(len(ap))
+ args = append(args, ap...)
+ err = locEncoder.Add(hitNewDocNum, args...)
+ if err != nil {
+ return 0, 0, 0, nil, err
+ }
+ }
+ }
+
+ lastDocNum = hitNewDocNum
+ lastFreq = nextFreq
+ lastNorm = nextNorm
+
+ next, err = postItr.Next()
+ }
+
+ return lastDocNum, lastFreq, lastNorm, bufLoc, err
+}
+
+func writePostings(postings *roaring.Bitmap, tfEncoder, locEncoder *chunkedIntCoder,
+ use1HitEncoding func(uint64) (bool, uint64, uint64),
+ w *FileWriter, bufMaxVarintLen64 []byte) (
+ offset uint64, err error) {
+ if postings == nil {
+ return 0, nil
+ }
+
+ termCardinality := postings.GetCardinality()
+ if termCardinality <= 0 {
+ return 0, nil
+ }
+
+ if use1HitEncoding != nil {
+ encodeAs1Hit, docNum1Hit, normBits1Hit := use1HitEncoding(termCardinality)
+ if encodeAs1Hit {
+ return FSTValEncode1Hit(docNum1Hit, normBits1Hit), nil
+ }
+ }
+
+ var tfOffset uint64
+ tfOffset, _, err = tfEncoder.writeAt(w)
+ if err != nil {
+ return 0, err
+ }
+
+ var locOffset uint64
+ locOffset, _, err = locEncoder.writeAt(w)
+ if err != nil {
+ return 0, err
+ }
+
+ postingsOffset := uint64(w.Count())
+
+ n := binary.PutUvarint(bufMaxVarintLen64, tfOffset)
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return 0, err
+ }
+
+ n = binary.PutUvarint(bufMaxVarintLen64, locOffset)
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return 0, err
+ }
+
+ _, err = writeRoaringWithLen(postings, w, bufMaxVarintLen64)
+ if err != nil {
+ return 0, err
+ }
+
+ return postingsOffset, nil
+}
+
+type varintEncoder func(uint64) (int, error)
+
+func mergeStoredAndRemap(segments []*SegmentBase, drops []*roaring.Bitmap,
+ fieldsMap map[string]uint16, fieldsInv []string,
+ fieldsOptions map[string]index.FieldIndexingOptions,
+ fieldsSame bool, newSegDocCount uint64,
+ w *FileWriter, closeCh chan struct{}) (uint64, [][]uint64, error) {
+ var rv [][]uint64 // The remapped or newDocNums for each segment.
+
+ var newDocNum uint64
+
+ var curr int
+ var data, compressed []byte
+ var metaBuf bytes.Buffer
+ varBuf := make([]byte, binary.MaxVarintLen64)
+ metaEncode := func(val uint64) (int, error) {
+ wb := binary.PutUvarint(varBuf, val)
+ return metaBuf.Write(varBuf[:wb])
+ }
+
+ vals := make([][][]byte, len(fieldsInv))
+ typs := make([][]byte, len(fieldsInv))
+ poss := make([][][]uint64, len(fieldsInv))
+
+ // copying data directly is safe only if there are no
+ // file callbacks that might modify the data in all
+ // of the involved segments and the current writer
+ copyFlag := true
+ for _, segment := range segments {
+ if segment.fileReader.id != "" {
+ copyFlag = false
+ break
+ }
+ }
+ if w.id != "" {
+ copyFlag = false
+ }
+
+ var posBuf []uint64
+
+ docNumOffsets := make([]uint64, newSegDocCount)
+
+ vdc := visitDocumentCtxPool.Get().(*visitDocumentCtx)
+ defer visitDocumentCtxPool.Put(vdc)
+
+ // for each segment
+ for segI, segment := range segments {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return 0, nil, seg.ErrClosed
+ }
+
+ segNewDocNums := make([]uint64, segment.numDocs)
+
+ dropsI := drops[segI]
+
+ // optimize when the field mapping is the same across all
+ // segments and there are no deletions, via byte-copying
+ // of stored docs bytes directly to the writer
+ // cannot copy directly if fields might have been deleted
+ if fieldsSame && (dropsI == nil || dropsI.GetCardinality() == 0) && copyFlag {
+ err := segment.copyStoredDocs(newDocNum, docNumOffsets, w)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ for i := uint64(0); i < segment.numDocs; i++ {
+ segNewDocNums[i] = newDocNum
+ newDocNum++
+ }
+ rv = append(rv, segNewDocNums)
+
+ continue
+ }
+
+ // for each doc num
+ for docNum := uint64(0); docNum < segment.numDocs; docNum++ {
+ // TODO: roaring's API limits docNums to 32-bits?
+ if dropsI != nil && dropsI.Contains(uint32(docNum)) {
+ segNewDocNums[docNum] = docDropped
+ continue
+ }
+
+ segNewDocNums[docNum] = newDocNum
+
+ curr = 0
+ metaBuf.Reset()
+ data = data[:0]
+
+ posTemp := posBuf
+
+ // collect all the data
+ for i := 0; i < len(fieldsInv); i++ {
+ vals[i] = vals[i][:0]
+ typs[i] = typs[i][:0]
+ poss[i] = poss[i][:0]
+ }
+ err := segment.visitStoredFields(vdc, docNum, func(field string, typ byte, value []byte, pos []uint64) bool {
+ fieldID := int(fieldsMap[field]) - 1
+ if fieldID < 0 {
+ // no entry for field in fieldsMap
+ return false
+ }
+ // early exit if the store is not wanted for this field
+ if !fieldsOptions[field].IsStored() {
+ return true
+ }
+ vals[fieldID] = append(vals[fieldID], value)
+ typs[fieldID] = append(typs[fieldID], typ)
+
+ // copy array positions to preserve them beyond the scope of this callback
+ var curPos []uint64
+ if len(pos) > 0 {
+ if cap(posTemp) < len(pos) {
+ posBuf = make([]uint64, len(pos)*len(fieldsInv))
+ posTemp = posBuf
+ }
+ curPos = posTemp[0:len(pos)]
+ copy(curPos, pos)
+ posTemp = posTemp[len(pos):]
+ }
+ poss[fieldID] = append(poss[fieldID], curPos)
+
+ return true
+ })
+ if err != nil {
+ return 0, nil, err
+ }
+
+ // _id field special case optimizes ExternalID() lookups
+ idFieldVal := vals[uint16(0)][0]
+ _, err = metaEncode(uint64(len(idFieldVal)))
+ if err != nil {
+ return 0, nil, err
+ }
+
+ // now walk the non-"_id" fields in order
+ for fieldID := 1; fieldID < len(fieldsInv); fieldID++ {
+ // early exit if the store is not wanted for this field
+ if !fieldsOptions[fieldsInv[fieldID]].IsStored() {
+ continue
+ }
+ // early exit if no stored values for this field
+ if len(vals[fieldID]) == 0 {
+ continue
+ }
+ storedFieldValues := vals[fieldID]
+
+ stf := typs[fieldID]
+ spf := poss[fieldID]
+
+ var err2 error
+ curr, data, err2 = persistStoredFieldValues(fieldID,
+ storedFieldValues, stf, spf, curr, metaEncode, data)
+ if err2 != nil {
+ return 0, nil, err2
+ }
+ }
+
+ metaBytes := metaBuf.Bytes()
+
+ compressed = snappy.Encode(compressed[:cap(compressed)], data)
+
+ // record where we're about to start writing
+ docNumOffsets[newDocNum] = uint64(w.Count())
+
+ bufMeta := w.process(metaBytes)
+
+ // idFieldVal is a pointer to a mem mapped byte slice, so we copy
+ // before merging it with the compressed data
+ buf := make([]byte, 0, len(idFieldVal)+len(compressed))
+ buf = append(buf, idFieldVal...)
+ buf = append(buf, compressed...)
+
+ bufCompressed := w.process(buf)
+
+ // write out the meta len and compressed data len
+ _, err = writeUvarints(w,
+ uint64(len(bufMeta)),
+ uint64(len(bufCompressed)))
+ if err != nil {
+ return 0, nil, err
+ }
+ // now write the meta
+ _, err = w.Write(bufMeta)
+ if err != nil {
+ return 0, nil, err
+ }
+ // now write the compressed data
+ _, err = w.Write(bufCompressed)
+ if err != nil {
+ return 0, nil, err
+ }
+
+ newDocNum++
+ }
+
+ rv = append(rv, segNewDocNums)
+ }
+
+ // return value is the start of the stored index
+ storedIndexOffset := uint64(w.Count())
+
+ // now write out the stored doc index
+ for _, docNumOffset := range docNumOffsets {
+ err := binary.Write(w, binary.BigEndian, docNumOffset)
+ if err != nil {
+ return 0, nil, err
+ }
+ }
+
+ // calculate new edge list if applicable
+ var newEdgeList map[uint64]uint64
+
+ for segI, segment := range segments {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return 0, nil, seg.ErrClosed
+ }
+ // get the edgeList for this segment
+ edgeList := segment.EdgeList()
+ // if no edgeList, nothing to do
+ if edgeList == nil {
+ continue
+ }
+ newSegDocNums := rv[segI]
+ edgeList.Iterate(func(oldChild uint64, oldParent uint64) bool {
+ newParent := newSegDocNums[oldParent]
+ newChild := newSegDocNums[oldChild]
+ if newParent != docDropped &&
+ newChild != docDropped {
+ if newEdgeList == nil {
+ newEdgeList = make(map[uint64]uint64)
+ }
+ newEdgeList[newChild] = newParent
+ }
+ return true
+ })
+ }
+
+ // write out the new edge list
+ // first write out the number of entries
+ // which is also the number of valid subDocs
+ // in the merged segment
+ buf := make([]byte, binary.MaxVarintLen64)
+ n := binary.PutUvarint(buf, uint64(len(newEdgeList)))
+ _, err := w.Write(buf[:n])
+ if err != nil {
+ return 0, nil, err
+ }
+ // write the child -> parent edge list
+ // child and parent are both flattened doc ids
+ for child, parent := range newEdgeList {
+ n = binary.PutUvarint(buf, child)
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return 0, nil, err
+ }
+ n = binary.PutUvarint(buf, parent)
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return 0, nil, err
+ }
+ }
+
+ return storedIndexOffset, rv, nil
+}
+
+// copyStoredDocs writes out a segment's stored doc info, optimized by
+// using a single Write() call for the entire set of bytes. The
+// newDocNumOffsets is filled with the new offsets for each doc.
+func (sb *SegmentBase) copyStoredDocs(newDocNum uint64, newDocNumOffsets []uint64,
+ w *FileWriter) error {
+ if sb.numDocs <= 0 {
+ return nil
+ }
+
+ indexOffset0, storedOffset0, _, _, _ :=
+ sb.getDocStoredOffsets(0) // the segment's first doc
+
+ indexOffsetN, storedOffsetN, readN, metaLenN, dataLenN :=
+ sb.getDocStoredOffsets(sb.numDocs - 1) // the segment's last doc
+
+ storedOffset0New := uint64(w.Count())
+
+ storedBytes := sb.mem[storedOffset0 : storedOffsetN+readN+metaLenN+dataLenN]
+ _, err := w.Write(storedBytes)
+ if err != nil {
+ return err
+ }
+
+ // remap the storedOffset's for the docs into new offsets relative
+ // to storedOffset0New, filling the given docNumOffsetsOut array
+ for indexOffset := indexOffset0; indexOffset <= indexOffsetN; indexOffset += 8 {
+ storedOffset := binary.BigEndian.Uint64(sb.mem[indexOffset : indexOffset+8])
+ storedOffsetNew := storedOffset - storedOffset0 + storedOffset0New
+ newDocNumOffsets[newDocNum] = storedOffsetNew
+ newDocNum += 1
+ }
+
+ return nil
+}
+
+// mergeFields builds a unified list of fields used across all the
+// input segments, and computes whether the fields are the same across
+// segments (which depends on fields to be sorted in the same way
+// across segments)
+func mergeFields(segments []*SegmentBase) (bool, []string, map[string]index.FieldIndexingOptions) {
+ fieldsSame := true
+
+ var segment0Fields []string
+ if len(segments) > 0 {
+ segment0Fields = segments[0].Fields()
+ }
+
+ fieldsExist := map[string]struct{}{}
+ fieldOptions := map[string]index.FieldIndexingOptions{}
+ for _, segment := range segments {
+ fields := segment.Fields()
+ for fieldi, field := range fields {
+ fieldsExist[field] = struct{}{}
+
+ if prev, ok := fieldOptions[field]; ok {
+ // Merge options conservatively: once a field option is disabled (bit cleared)
+ // in any segment, it remains disabled. This ensures deterministic behavior
+ // when options can only transition from true -> false.
+ fieldOptions[field] = prev & segment.fieldsOptions[field]
+ // check if any bits were cleared
+ if fieldOptions[field] != prev {
+ // Some bits were cleared (option changed from true -> false)
+ fieldsSame = false
+ }
+ } else {
+ // first occurrence of the field
+ fieldOptions[field] = segment.fieldsOptions[field]
+ }
+ if len(segment0Fields) != len(fields) || segment0Fields[fieldi] != field {
+ fieldsSame = false
+ }
+ }
+ }
+
+ rv := make([]string, 0, len(fieldsExist))
+ // ensure _id stays first
+ rv = append(rv, "_id")
+ for k := range fieldsExist {
+ if k != "_id" {
+ rv = append(rv, k)
+ }
+ }
+
+ sort.Strings(rv[1:]) // leave _id as first
+
+ return fieldsSame, rv, fieldOptions
+}
+
+// Combine updateFieldInfo from all segments
+func mergeUpdatedFields(segments []*SegmentBase) map[string]*index.UpdateFieldInfo {
+ var fieldInfo map[string]*index.UpdateFieldInfo
+
+ for _, segment := range segments {
+ for field, info := range segment.updatedFields {
+ if fieldInfo == nil {
+ fieldInfo = make(map[string]*index.UpdateFieldInfo)
+ }
+ // if field not present, add it
+ if _, ok := fieldInfo[field]; !ok {
+ fieldInfo[field] = &index.UpdateFieldInfo{
+ // mark whether field is deleted in any segment
+ Deleted: info.Deleted,
+ Index: info.Index,
+ Store: info.Store,
+ DocValues: info.DocValues,
+ }
+ } else {
+ fieldInfo[field].Deleted = fieldInfo[field].Deleted || info.Deleted
+ fieldInfo[field].Index = fieldInfo[field].Index || info.Index
+ fieldInfo[field].Store = fieldInfo[field].Store || info.Store
+ fieldInfo[field].DocValues = fieldInfo[field].DocValues || info.DocValues
+ }
+ }
+
+ }
+ return fieldInfo
+}
+
+func isClosed(closeCh chan struct{}) bool {
+ select {
+ case <-closeCh:
+ return true
+ default:
+ return false
+ }
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/nested_cache.go b/vendor/github.com/blevesearch/zapx/v17/nested_cache.go
new file mode 100644
index 0000000000..2d273ba235
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/nested_cache.go
@@ -0,0 +1,290 @@
+// Copyright (c) 2026 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+ "math"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+type nestedIndexCache struct {
+ cache *nestedCacheEntry
+}
+
+// newNestedIndexCache creates a new nested index cache
+// instance, which contains cached edge list
+// for a nested segment
+func newNestedIndexCache() *nestedIndexCache {
+ return &nestedIndexCache{}
+}
+
+// Clear clears the nested index cache, removing the cached edge list
+func (nc *nestedIndexCache) Clear() {
+ nc.cache = nil
+}
+
+func (nc *nestedIndexCache) initialize(numDocs uint64, edgeListOffset uint64, mem []byte) error {
+ // pos stores the current read position
+ pos := edgeListOffset
+ if pos == 0 {
+ // no edge list
+ return nil
+ }
+ // read number of edges in the edge list
+ numEdges, read := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if read <= 0 {
+ return fmt.Errorf("error reading number of edges in nested edge list")
+ }
+ pos += uint64(read)
+ // if no documents or edges/nested documents, return
+ if numDocs == 0 || numEdges == 0 {
+ return nil
+ }
+ edgeList := NewEdgeList(numDocs, numEdges)
+ for i := uint64(0); i < numEdges; i++ {
+ child, read := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if read <= 0 {
+ return fmt.Errorf("error reading child doc id in nested edge list")
+ }
+ pos += uint64(read)
+ parent, read := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if read <= 0 {
+ return fmt.Errorf("error reading parent doc id in nested edge list")
+ }
+ pos += uint64(read)
+ edgeList.AddEdge(child, parent)
+ }
+ nc.cache = &nestedCacheEntry{
+ el: edgeList,
+ }
+ return nil
+}
+
+type nestedCacheEntry struct {
+ // edgeList[child] = parent
+ el EdgeList
+}
+
+func (nc *nestedIndexCache) ancestry(docNum uint64, prealloc []index.AncestorID) []index.AncestorID {
+ cache := nc.cache
+ // add self as first ancestor
+ prealloc = append(prealloc, index.NewAncestorID(docNum))
+ if cache == nil || cache.el == nil {
+ return prealloc
+ }
+ current := docNum
+ for {
+ parent, ok := cache.el.Parent(current)
+ if !ok {
+ break
+ }
+ prealloc = append(prealloc, index.NewAncestorID(parent))
+ current = parent
+ }
+ return prealloc
+}
+
+func (nc *nestedIndexCache) edgeList() EdgeList {
+ cache := nc.cache
+ if cache == nil || cache.el == nil {
+ return nil
+ }
+ return cache.el
+}
+
+func (nc *nestedIndexCache) countNested() uint64 {
+ cache := nc.cache
+ if cache == nil || cache.el == nil {
+ return 0
+ }
+ return cache.el.Count()
+}
+
+// countRoot returns the number of root documents in the given bitmap
+func (nc *nestedIndexCache) countRoot(bm *roaring.Bitmap) uint64 {
+ var totalDocs uint64
+ if bm == nil {
+ // if bitmap is empty, return 0
+ return totalDocs
+ }
+ totalDocs = bm.GetCardinality()
+ cache := nc.cache
+ if cache == nil || cache.el == nil {
+ // if cache is nil, no nested docs, so all docs are root docs
+ // so just return the cardinality of the bitmap
+ return totalDocs
+ }
+ // count nested documents in the bitmap, a nested doc is one that has a parent in the edge list
+ var nestedDocCount uint64
+ bm.Iterate(func(docNum uint32) bool {
+ if _, ok := cache.el.Parent(uint64(docNum)); ok {
+ nestedDocCount++
+ }
+ return true
+ })
+ // root docs = total docs - nested docs
+ if totalDocs < nestedDocCount {
+ // should not happen, but just in case
+ return 0
+ }
+ return totalDocs - nestedDocCount
+}
+
+// -------------------------------------------------------
+
+// EdgeList provides an interface to access parent of a child document
+type EdgeList interface {
+ // Parent returns the parent of the given child document ID,
+ // and a boolean indicating if the parent exists.
+ Parent(child uint64) (uint64, bool)
+
+ // AddEdge adds an edge from child to parent in the edge list.
+ AddEdge(child uint64, parent uint64)
+
+ // Count returns the number of edges in the edge list.
+ Count() uint64
+
+ // Iterate iterates over all edges in the edge list, calling the provided function
+ // with each child-parent pair. If the function returns false, iteration stops.
+ Iterate(func(child uint64, parent uint64) bool)
+}
+
+type edgeListMap struct {
+ edges map[uint64]uint64
+}
+
+func newEdgeListMap(numEdges uint64) *edgeListMap {
+ return &edgeListMap{
+ edges: make(map[uint64]uint64, numEdges),
+ }
+}
+
+func (elm *edgeListMap) Parent(child uint64) (uint64, bool) {
+ parent, ok := elm.edges[child]
+ return parent, ok
+}
+
+func (elm *edgeListMap) AddEdge(child uint64, parent uint64) {
+ elm.edges[child] = parent
+}
+
+func (elm *edgeListMap) Count() uint64 {
+ return uint64(len(elm.edges))
+}
+
+func (elm *edgeListMap) Iterate(f func(child uint64, parent uint64) bool) {
+ for child, parent := range elm.edges {
+ if !f(child, parent) {
+ return
+ }
+ }
+}
+
+type edgeListSlice struct {
+ count uint64
+ sentinel uint64
+ edges []uint64
+}
+
+func newEdgeListSlice(numDocs uint64, numEdges uint64) *edgeListSlice {
+ var sentinel uint64 = math.MaxUint64
+ edges := make([]uint64, numDocs)
+ for i := range edges {
+ edges[i] = sentinel
+ }
+ return &edgeListSlice{
+ count: numEdges,
+ sentinel: sentinel,
+ edges: edges,
+ }
+}
+
+func (els *edgeListSlice) Parent(child uint64) (uint64, bool) {
+ if child >= uint64(len(els.edges)) {
+ return 0, false
+ }
+ parent := els.edges[child]
+ if parent == els.sentinel {
+ return 0, false
+ }
+ return parent, true
+}
+
+func (el *edgeListSlice) AddEdge(child uint64, parent uint64) {
+ if child >= uint64(len(el.edges)) {
+ // out of bounds, ignore as this should not happen
+ return
+ }
+ el.edges[child] = parent
+}
+
+func (el *edgeListSlice) Count() uint64 {
+ return el.count
+}
+
+func (el *edgeListSlice) Iterate(f func(child uint64, parent uint64) bool) {
+ for child, parent := range el.edges {
+ if parent != el.sentinel {
+ if !f(uint64(child), parent) {
+ return
+ }
+ }
+ }
+}
+
+// nestedCacheRatio defines the threshold ratio of nested documents to total documents.
+// It is derived using the following reasoning:
+//
+// Let N = number of nested documents (i.e., edges in the edge list)
+// Let T = total number of documents
+//
+// Memory usage if the edge list is stored as a map[uint64]uint64:
+//
+// ~30 bytes per entry (key + value + map overhead)
+// Total ≈ 30 * N bytes
+//
+// Memory usage if the edge list is stored as a []uint64:
+//
+// 8 bytes per entry
+// Total ≈ 8 * T bytes
+//
+// We want the threshold at which a map becomes more memory-efficient than a slice:
+//
+// 30N < 8T
+// N/T < 8/30
+//
+// Therefore, if the ratio of nested documents to total documents is less than 8/30,
+// we use a map for the edge list; otherwise, we use a slice.
+var edgeListMapThreshold = 8.0 / 30.0
+
+// NewEdgeList creates a new EdgeList instance based on the provided
+// constants, the total number of documents and the number of nested documents/edges.
+func NewEdgeList(numDocs uint64, numEdges uint64) EdgeList {
+ if numDocs == 0 || numEdges == 0 {
+ // no edges, return nil
+ return nil
+ }
+ ratio := float64(numEdges) / float64(numDocs)
+ if ratio < edgeListMapThreshold {
+ // use map representation
+ return newEdgeListMap(numEdges)
+ }
+ // use slice representation
+ return newEdgeListSlice(numDocs, numEdges)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/new.go b/vendor/github.com/blevesearch/zapx/v17/new.go
new file mode 100644
index 0000000000..a1d0b10636
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/new.go
@@ -0,0 +1,555 @@
+// Copyright (c) 2018 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "math"
+ "sort"
+ "sync"
+ "sync/atomic"
+
+ index "github.com/blevesearch/bleve_index_api"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/golang/snappy"
+)
+
+var NewSegmentBufferNumResultsBump int = 100
+var NewSegmentBufferNumResultsFactor float64 = 1.0
+var NewSegmentBufferAvgBytesPerDocFactor float64 = 1.0
+
+// ValidateDocFields can be set by applications to perform additional checks
+// on fields in a document being added to a new segment, by default it does
+// nothing.
+// This API is experimental and may be removed at any time.
+var ValidateDocFields = func(field index.Field) error {
+ return nil
+}
+
+// New creates an in-memory zap-encoded SegmentBase from a set of Documents
+func (z *ZapPlugin) New(results []index.Document) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, nil)
+}
+
+func (z *ZapPlugin) NewUsing(results []index.Document, config map[string]interface{}) (
+ segment.Segment, uint64, error) {
+ return z.newWithChunkMode(results, DefaultChunkMode, config)
+}
+
+func (*ZapPlugin) newWithChunkMode(results []index.Document,
+ chunkMode uint32, config map[string]interface{}) (segment.Segment, uint64, error) {
+ s := interimPool.Get().(*interim)
+
+ var br bytes.Buffer
+ if s.lastNumDocs > 0 {
+ // use previous results to initialize the buf with an estimate
+ // size, but note that the interim instance comes from a
+ // global interimPool, so multiple scorch instances indexing
+ // different docs can lead to low quality estimates
+ estimateAvgBytesPerDoc := int(float64(s.lastOutSize/s.lastNumDocs) *
+ NewSegmentBufferNumResultsFactor)
+ estimateNumResults := int(float64(len(results)+NewSegmentBufferNumResultsBump) *
+ NewSegmentBufferAvgBytesPerDocFactor)
+ br.Grow(estimateAvgBytesPerDoc * estimateNumResults)
+ }
+
+ var err error
+ s.results, s.edgeList = flattenNestedDocuments(results, s.edgeList)
+ s.config = config
+ s.chunkMode = chunkMode
+
+ s.w = NewFileWriterEmpty(NewCountHashWriter(&br))
+
+ storedIndexOffset, sectionsIndexOffset, err := s.convert()
+ if err != nil {
+ return nil, uint64(0), err
+ }
+
+ sb, err := InitSegmentBase(br.Bytes(), s.w.Sum32(), chunkMode,
+ uint64(len(s.results)), storedIndexOffset, sectionsIndexOffset, config)
+
+ // get the bytes written before the interim's reset() call
+ // write it to the newly formed segment base.
+ totalBytesWritten := s.getBytesWritten()
+ if err == nil && s.reset() == nil {
+ s.lastNumDocs = len(results)
+ s.lastOutSize = len(br.Bytes())
+ sb.setBytesWritten(totalBytesWritten)
+ interimPool.Put(s)
+ }
+
+ return sb, uint64(len(br.Bytes())), err
+}
+
+var interimPool = sync.Pool{New: func() interface{} { return &interim{} }}
+
+// interim holds temporary working data used while converting from
+// analysis results to a zap-encoded segment
+type interim struct {
+ results []index.Document
+
+ // edge list for nested documents: child -> parent
+ edgeList map[uint64]uint64
+
+ chunkMode uint32
+
+ w *FileWriter
+
+ config map[string]interface{}
+
+ // FieldsMap adds 1 to field id to avoid zero value issues
+ // name -> field id + 1
+ FieldsMap map[string]uint16
+
+ // FieldsOptions holds the indexing options for each field
+ FieldsOptions map[string]index.FieldIndexingOptions
+
+ // FieldsInv is the inverse of FieldsMap
+ // field id -> name
+ FieldsInv []string
+
+ metaBuf bytes.Buffer
+
+ tmp0 []byte
+ tmp1 []byte
+
+ lastNumDocs int
+ lastOutSize int
+
+ // atomic access to this variable
+ bytesWritten uint64
+
+ opaque map[int]resetable
+}
+
+func (s *interim) reset() (err error) {
+ s.results = nil
+ s.chunkMode = 0
+ s.w = nil
+ clear(s.edgeList)
+ clear(s.FieldsMap)
+ clear(s.FieldsOptions)
+ s.FieldsInv = s.FieldsInv[:0]
+ s.metaBuf.Reset()
+ s.tmp0 = s.tmp0[:0]
+ s.tmp1 = s.tmp1[:0]
+ s.lastNumDocs = 0
+ s.lastOutSize = 0
+
+ // reset the bytes written stat count
+ // to avoid leaking of bytesWritten across reuse cycles.
+ s.setBytesWritten(0)
+
+ if s.opaque != nil {
+ for _, v := range s.opaque {
+ err = v.Reset()
+ }
+ } else {
+ s.opaque = map[int]resetable{}
+ }
+
+ return err
+}
+
+type interimStoredField struct {
+ vals [][]byte
+ typs []byte
+ arrayposs [][]uint64 // array positions
+}
+
+type interimFreqNorm struct {
+ freq uint64
+ norm float32
+ numLocs int
+}
+
+type interimLoc struct {
+ fieldID uint16
+ pos uint64
+ start uint64
+ end uint64
+ arrayposs []uint64
+}
+
+func (s *interim) convert() (uint64, uint64, error) {
+ if s.FieldsMap == nil {
+ s.FieldsMap = map[string]uint16{}
+ }
+ if s.FieldsOptions == nil {
+ s.FieldsOptions = map[string]index.FieldIndexingOptions{}
+ }
+
+ s.getOrDefineField("_id") // _id field is fieldID 0
+ // special case _id field options: the _id is the canonical document identifier and
+ // must always be both indexed and stored so that it can be used for lookups/queries
+ // and retrieved back from the stored fields, regardless of user-specified field options.
+ s.FieldsOptions["_id"] = index.IndexField | index.StoreField
+
+ var fName string
+ for _, result := range s.results {
+ result.VisitComposite(func(field index.CompositeField) {
+ fName = field.Name()
+ s.getOrDefineField(fName)
+ s.FieldsOptions[fName] = field.Options()
+ })
+ result.VisitFields(func(field index.Field) {
+ fName = field.Name()
+ s.getOrDefineField(fName)
+ s.FieldsOptions[fName] = field.Options()
+ })
+ }
+
+ sort.Strings(s.FieldsInv[1:]) // keep _id as first field
+
+ for fieldID, fieldName := range s.FieldsInv {
+ s.FieldsMap[fieldName] = uint16(fieldID + 1)
+ }
+
+ args := map[string]interface{}{
+ "results": s.results,
+ "chunkMode": s.chunkMode,
+ "fieldsMap": s.FieldsMap,
+ "fieldsInv": s.FieldsInv,
+ "config": s.config,
+ "fieldsOptions": s.FieldsOptions,
+ }
+ if s.opaque == nil {
+ s.opaque = map[int]resetable{}
+ for i, x := range segmentSections {
+ s.opaque[int(i)] = x.InitOpaque(args)
+ }
+ } else {
+ for k, v := range args {
+ for _, op := range s.opaque {
+ op.Set(k, v)
+ }
+ }
+ }
+
+ s.processDocuments()
+
+ storedIndexOffset, err := s.writeStoredFields()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ // we can persist the various sections at this point.
+ // the rule of thumb here is that each section must persist field wise.
+ for _, x := range segmentSections {
+ err = x.Persist(s.opaque, s.w)
+ if err != nil {
+ return 0, 0, err
+ }
+ }
+
+ // after persisting the sections to the writer, account corresponding
+ for _, opaque := range s.opaque {
+ opaqueIO, ok := opaque.(segment.DiskStatsReporter)
+ if ok {
+ s.incrementBytesWritten(opaqueIO.BytesWritten())
+ }
+ }
+
+ // we can persist a new fields section here
+ // this new fields section will point to the various indexes available
+ sectionsIndexOffset, err := persistFieldsSection(s.FieldsInv, s.FieldsOptions, s.w, s.opaque)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return storedIndexOffset, sectionsIndexOffset, nil
+}
+
+func (s *interim) getOrDefineField(fieldName string) int {
+ fieldIDPlus1, exists := s.FieldsMap[fieldName]
+ if !exists {
+ fieldIDPlus1 = uint16(len(s.FieldsInv) + 1)
+ s.FieldsMap[fieldName] = fieldIDPlus1
+ s.FieldsInv = append(s.FieldsInv, fieldName)
+ }
+
+ return int(fieldIDPlus1 - 1)
+}
+
+func (s *interim) processDocuments() {
+ for docNum, result := range s.results {
+ s.processDocument(uint32(docNum), result)
+ }
+}
+
+func (s *interim) processDocument(docNum uint32,
+ result index.Document) {
+ // this callback is essentially going to be invoked on each field,
+ // as part of which preprocessing, cumulation etc. of the doc's data
+ // will take place.
+ visitField := func(field index.Field) {
+ fieldID := uint16(s.getOrDefineField(field.Name()))
+
+ // section specific processing of the field
+ for _, section := range segmentSections {
+ section.Process(s.opaque, docNum, field, fieldID)
+ }
+ }
+
+ // walk each composite field
+ result.VisitComposite(func(field index.CompositeField) {
+ visitField(field)
+ })
+
+ // walk each field
+ result.VisitFields(visitField)
+
+ // given that as part of visiting each field, there may some kind of totalling
+ // or accumulation that can be updated, it becomes necessary to commit or
+ // put that totalling/accumulation into effect. However, for certain section
+ // types this particular step need not be valid, in which case it would be a
+ // no-op in the implmentation of the section's process API.
+ for _, section := range segmentSections {
+ section.Process(s.opaque, docNum, nil, math.MaxUint16)
+ }
+
+}
+
+func (s *interim) getBytesWritten() uint64 {
+ return atomic.LoadUint64(&s.bytesWritten)
+}
+
+func (s *interim) incrementBytesWritten(val uint64) {
+ atomic.AddUint64(&s.bytesWritten, val)
+}
+
+func (s *interim) writeStoredFields() (
+ storedIndexOffset uint64, err error) {
+ varBuf := make([]byte, binary.MaxVarintLen64)
+ metaEncode := func(val uint64) (int, error) {
+ wb := binary.PutUvarint(varBuf, val)
+ return s.metaBuf.Write(varBuf[:wb])
+ }
+
+ data, compressed := s.tmp0[:0], s.tmp1[:0]
+ defer func() { s.tmp0, s.tmp1 = data, compressed }()
+
+ // keyed by docNum
+ docStoredOffsets := make([]uint64, len(s.results))
+
+ // keyed by fieldID, for the current doc in the loop
+ docStoredFields := map[uint16]interimStoredField{}
+
+ for docNum, result := range s.results {
+ for fieldID := range docStoredFields { // reset for next doc
+ delete(docStoredFields, fieldID)
+ }
+
+ var validationErr error
+ result.VisitFields(func(field index.Field) {
+ fieldID := uint16(s.getOrDefineField(field.Name()))
+
+ if field.Options().IsStored() {
+ isf := docStoredFields[fieldID]
+ isf.vals = append(isf.vals, field.Value())
+ isf.typs = append(isf.typs, field.EncodedFieldType())
+ isf.arrayposs = append(isf.arrayposs, field.ArrayPositions())
+ docStoredFields[fieldID] = isf
+ }
+
+ err := ValidateDocFields(field)
+ if err != nil && validationErr == nil {
+ validationErr = err
+ }
+ })
+ if validationErr != nil {
+ return 0, validationErr
+ }
+
+ var curr int
+
+ s.metaBuf.Reset()
+ data = data[:0]
+
+ // _id field special case optimizes ExternalID() lookups
+ idFieldVal := docStoredFields[uint16(0)].vals[0]
+ _, err = metaEncode(uint64(len(idFieldVal)))
+ if err != nil {
+ return 0, err
+ }
+
+ // handle non-"_id" fields
+ for fieldID := 1; fieldID < len(s.FieldsInv); fieldID++ {
+ isf, exists := docStoredFields[uint16(fieldID)]
+ if exists {
+ curr, data, err = persistStoredFieldValues(
+ fieldID, isf.vals, isf.typs, isf.arrayposs,
+ curr, metaEncode, data)
+ if err != nil {
+ return 0, err
+ }
+ }
+ }
+
+ metaBytes := s.metaBuf.Bytes()
+
+ compressed = snappy.Encode(compressed[:cap(compressed)], data)
+ s.incrementBytesWritten(uint64(len(compressed)))
+ docStoredOffsets[docNum] = uint64(s.w.Count())
+
+ combined := make([]byte, len(idFieldVal)+len(compressed))
+ copy(combined, idFieldVal)
+ copy(combined[len(idFieldVal):], compressed)
+ bufMeta := s.w.process(metaBytes)
+ bufCompressed := s.w.process(combined)
+
+ _, err = writeUvarints(s.w,
+ uint64(len(bufMeta)),
+ uint64(len(bufCompressed)))
+ if err != nil {
+ return 0, err
+ }
+
+ _, err = s.w.Write(bufMeta)
+ if err != nil {
+ return 0, err
+ }
+
+ _, err = s.w.Write(bufCompressed)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ storedIndexOffset = uint64(s.w.Count())
+
+ for _, docStoredOffset := range docStoredOffsets {
+ err = binary.Write(s.w, binary.BigEndian, docStoredOffset)
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ // write the number of edges in the child -> parent edge list
+ // this will be zero if there are no nested documents
+ // and this number also reflects the number of nested documents
+ // in the segment
+ buf := make([]byte, binary.MaxVarintLen64)
+ n := binary.PutUvarint(buf, uint64(len(s.edgeList)))
+ _, err = s.w.Write(buf[:n])
+ if err != nil {
+ return 0, err
+ }
+ // write the child -> parent edge list
+ // child and parent are both flattened doc ids
+ for child, parent := range s.edgeList {
+ n = binary.PutUvarint(buf, child)
+ _, err = s.w.Write(buf[:n])
+ if err != nil {
+ return 0, err
+ }
+ n = binary.PutUvarint(buf, parent)
+ _, err = s.w.Write(buf[:n])
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ return storedIndexOffset, nil
+}
+
+func (s *interim) setBytesWritten(val uint64) {
+ atomic.StoreUint64(&s.bytesWritten, val)
+}
+
+// returns the total # of bytes needed to encode the given uint64's
+// into binary.PutUVarint() encoding
+func totalUvarintBytes(a, b, c, d, e uint64, more []uint64) (n int) {
+ n = numUvarintBytes(a)
+ n += numUvarintBytes(b)
+ n += numUvarintBytes(c)
+ n += numUvarintBytes(d)
+ n += numUvarintBytes(e)
+ for _, v := range more {
+ n += numUvarintBytes(v)
+ }
+ return n
+}
+
+// returns # of bytes needed to encode x in binary.PutUvarint() encoding
+func numUvarintBytes(x uint64) (n int) {
+ for x >= 0x80 {
+ x >>= 7
+ n++
+ }
+ return n + 1
+}
+
+// flattenNestedDocuments returns a preorder list of the given documents and
+// all their nested documents, along with a map mapping each flattened index
+// to its parent index (excluding root docs entirely).
+// The edge list is represented as a map[child]parent, where both child and
+// parent are flattened document indices.
+// Root documents (those without a parent) are not included in the edge list,
+// as they have no parent. The order of documents in the returned slice is
+// such that parents always appear before their children. A reusable edgeList
+// can be provided to avoid allocations across multiple calls.
+func flattenNestedDocuments(docs []index.Document, edgeList map[uint64]uint64) (
+ []index.Document, map[uint64]uint64) {
+ totalCount := 0
+ for _, doc := range docs {
+ totalCount += countNestedDocuments(doc)
+ }
+
+ if totalCount == len(docs) {
+ // no nested documents, return early
+ return docs, nil
+ }
+
+ flattened := make([]index.Document, 0, totalCount)
+ if edgeList == nil {
+ edgeList = make(map[uint64]uint64, totalCount-len(docs))
+ }
+
+ var traverse func(doc index.Document, hasParent bool, parentIdx uint64)
+ traverse = func(d index.Document, hasParent bool, parentIdx uint64) {
+ curIdx := uint64(len(flattened))
+ flattened = append(flattened, d)
+
+ if hasParent {
+ edgeList[curIdx] = parentIdx
+ }
+
+ if nestedDoc, ok := d.(index.NestedDocument); ok {
+ nestedDoc.VisitNestedDocuments(func(child index.Document) {
+ traverse(child, true, curIdx)
+ })
+ }
+ }
+ // Top-level docs have no parent
+ for _, doc := range docs {
+ traverse(doc, false, 0)
+ }
+ return flattened, edgeList
+}
+
+// countNestedDocuments returns the total number of docs in preorder,
+// including the parent and all descendants.
+func countNestedDocuments(doc index.Document) int {
+ count := 1 // include this doc
+ if nd, ok := doc.(index.NestedDocument); ok {
+ nd.VisitNestedDocuments(func(child index.Document) {
+ count += countNestedDocuments(child)
+ })
+ }
+ return count
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/plugin.go b/vendor/github.com/blevesearch/zapx/v17/plugin.go
new file mode 100644
index 0000000000..f67297ec2f
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/plugin.go
@@ -0,0 +1,27 @@
+// Copyright (c) 2020 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+// ZapPlugin implements the Plugin interface of
+// the blevesearch/scorch_segment_api pkg
+type ZapPlugin struct{}
+
+func (*ZapPlugin) Type() string {
+ return Type
+}
+
+func (*ZapPlugin) Version() uint32 {
+ return Version
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/posting.go b/vendor/github.com/blevesearch/zapx/v17/posting.go
new file mode 100644
index 0000000000..0e63bb1cd5
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/posting.go
@@ -0,0 +1,947 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+ "math"
+ "reflect"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+var reflectStaticSizePostingsList int
+var reflectStaticSizePostingsIterator int
+var reflectStaticSizePosting int
+var reflectStaticSizeLocation int
+
+func init() {
+ var pl PostingsList
+ reflectStaticSizePostingsList = int(reflect.TypeOf(pl).Size())
+ var pi PostingsIterator
+ reflectStaticSizePostingsIterator = int(reflect.TypeOf(pi).Size())
+ var p Posting
+ reflectStaticSizePosting = int(reflect.TypeOf(p).Size())
+ var l Location
+ reflectStaticSizeLocation = int(reflect.TypeOf(l).Size())
+}
+
+// FST or vellum value (uint64) encoding is determined by the top two
+// highest-order or most significant bits...
+//
+// encoding : MSB
+// name : 63 62 61...to...bit #0 (LSB)
+// ----------+---+---+---------------------------------------------------
+// general : 0 | 0 | 62-bits of postingsOffset.
+// ~ : 0 | 1 | reserved for future.
+// 1-hit : 1 | 0 | 31-bits of positive float31 norm | 31-bits docNum.
+// ~ : 1 | 1 | reserved for future.
+//
+// Encoding "general" is able to handle all cases, where the
+// postingsOffset points to more information about the postings for
+// the term.
+//
+// Encoding "1-hit" is used to optimize a commonly seen case when a
+// term has only a single hit. For example, a term in the _id field
+// will have only 1 hit. The "1-hit" encoding is used for a term
+// in a field when...
+//
+// - term vector info is disabled for that field;
+// - and, the term appears in only a single doc for that field;
+// - and, the term's freq is exactly 1 in that single doc for that field;
+// - and, the docNum must fit into 31-bits;
+//
+// Otherwise, the "general" encoding is used instead.
+//
+// In the "1-hit" encoding, the field in that single doc may have
+// other terms, which is supported in the "1-hit" encoding by the
+// positive float31 norm.
+
+const FSTValEncodingMask = uint64(0xc000000000000000)
+const FSTValEncodingGeneral = uint64(0x0000000000000000)
+const FSTValEncoding1Hit = uint64(0x8000000000000000)
+
+func FSTValEncode1Hit(docNum uint64, normBits uint64) uint64 {
+ return FSTValEncoding1Hit | ((mask31Bits & normBits) << 31) | (mask31Bits & docNum)
+}
+
+func FSTValDecode1Hit(v uint64) (docNum uint64, normBits uint64) {
+ return (mask31Bits & v), (mask31Bits & (v >> 31))
+}
+
+const mask31Bits = uint64(0x000000007fffffff)
+
+func under32Bits(x uint64) bool {
+ return x <= mask31Bits
+}
+
+const DocNum1HitFinished = math.MaxUint64
+
+var NormBits1Hit = uint64(1)
+
+// PostingsList is an in-memory representation of a postings list
+type PostingsList struct {
+ sb *SegmentBase
+ postingsOffset uint64
+ freqOffset uint64
+ locOffset uint64
+ postings *roaring.Bitmap
+ except *roaring.Bitmap
+
+ // when normBits1Hit != 0, then this postings list came from a
+ // 1-hit encoding, and only the docNum1Hit & normBits1Hit apply
+ docNum1Hit uint64
+ normBits1Hit uint64
+
+ chunkSize uint64
+
+ bytesRead uint64
+}
+
+// represents an immutable, empty postings list
+var emptyPostingsList = &PostingsList{}
+
+func (p *PostingsList) Size() int {
+ sizeInBytes := reflectStaticSizePostingsList + SizeOfPtr
+
+ if p.except != nil {
+ sizeInBytes += int(p.except.GetSizeInBytes())
+ }
+
+ return sizeInBytes
+}
+
+func (p *PostingsList) OrInto(receiver *roaring.Bitmap) {
+ if p.normBits1Hit != 0 {
+ receiver.Add(uint32(p.docNum1Hit))
+ return
+ }
+
+ if p.postings != nil {
+ receiver.Or(p.postings)
+ }
+}
+
+// Iterator returns an iterator for this postings list
+func (p *PostingsList) Iterator(includeFreq, includeNorm, includeLocs bool,
+ prealloc segment.PostingsIterator) segment.PostingsIterator {
+ if p.normBits1Hit == 0 && p.postings == nil {
+ return emptyPostingsIterator
+ }
+
+ var preallocPI *PostingsIterator
+ pi, ok := prealloc.(*PostingsIterator)
+ if ok && pi != nil {
+ preallocPI = pi
+ }
+ if preallocPI == emptyPostingsIterator {
+ preallocPI = nil
+ }
+
+ return p.iterator(includeFreq, includeNorm, includeLocs, preallocPI)
+}
+
+func (p *PostingsList) iterator(includeFreq, includeNorm, includeLocs bool,
+ rv *PostingsIterator) *PostingsIterator {
+ if rv == nil {
+ rv = &PostingsIterator{}
+ } else {
+ freqNormReader := rv.freqNormReader
+ if freqNormReader != nil {
+ freqNormReader.reset()
+ }
+
+ locReader := rv.locReader
+ if locReader != nil {
+ locReader.reset()
+ }
+
+ nextLocs := rv.nextLocs[:0]
+ nextSegmentLocs := rv.nextSegmentLocs[:0]
+
+ buf := rv.buf
+
+ *rv = PostingsIterator{} // clear the struct
+
+ rv.freqNormReader = freqNormReader
+ rv.locReader = locReader
+
+ rv.nextLocs = nextLocs
+ rv.nextSegmentLocs = nextSegmentLocs
+
+ rv.buf = buf
+ }
+
+ rv.postings = p
+ rv.includeFreqNorm = includeFreq || includeNorm || includeLocs
+ rv.includeLocs = includeLocs
+
+ if p.normBits1Hit != 0 {
+ // "1-hit" encoding
+ rv.docNum1Hit = p.docNum1Hit
+ rv.normBits1Hit = p.normBits1Hit
+
+ if p.except != nil && p.except.Contains(uint32(rv.docNum1Hit)) {
+ rv.docNum1Hit = DocNum1HitFinished
+ }
+
+ return rv
+ }
+
+ // "general" encoding, check if empty
+ if p.postings == nil {
+ return rv
+ }
+
+ // initialize freq chunk reader
+ if rv.includeFreqNorm {
+ rv.freqNormReader = newChunkedIntDecoder(p.sb.mem, p.freqOffset, rv.freqNormReader, p.sb.fileReader)
+ rv.incrementBytesRead(rv.freqNormReader.getBytesRead())
+ }
+
+ // initialize the loc chunk reader
+ if rv.includeLocs {
+ rv.locReader = newChunkedIntDecoder(p.sb.mem, p.locOffset, rv.locReader, p.sb.fileReader)
+ rv.incrementBytesRead(rv.locReader.getBytesRead())
+ }
+
+ rv.all = p.postings.Iterator()
+ if p.except != nil {
+ rv.ActualBM = roaring.AndNot(p.postings, p.except)
+ rv.Actual = rv.ActualBM.Iterator()
+ } else {
+ rv.ActualBM = p.postings
+ rv.Actual = rv.all // Optimize to use same iterator for all & Actual.
+ }
+
+ return rv
+}
+
+// Count returns the number of items on this postings list
+func (p *PostingsList) Count() uint64 {
+ var n, e uint64
+ if p.normBits1Hit != 0 {
+ n = 1
+ if p.except != nil && p.except.Contains(uint32(p.docNum1Hit)) {
+ e = 1
+ }
+ } else if p.postings != nil {
+ n = p.postings.GetCardinality()
+ if p.except != nil {
+ e = p.postings.AndCardinality(p.except)
+ }
+ }
+ return n - e
+}
+
+// Implements the segment.DiskStatsReporter interface
+// The purpose of this implementation is to get
+// the bytes read from the postings lists stored
+// on disk, while querying
+func (p *PostingsList) ResetBytesRead(val uint64) {
+ p.bytesRead = val
+}
+
+func (p *PostingsList) BytesRead() uint64 {
+ return p.bytesRead
+}
+
+func (p *PostingsList) incrementBytesRead(val uint64) {
+ p.bytesRead += val
+}
+
+func (p *PostingsList) BytesWritten() uint64 {
+ return 0
+}
+
+func (rv *PostingsList) read(postingsOffset uint64, d *Dictionary) error {
+ rv.postingsOffset = postingsOffset
+
+ // handle "1-hit" encoding special case
+ if rv.postingsOffset&FSTValEncodingMask == FSTValEncoding1Hit {
+ return rv.init1Hit(postingsOffset)
+ }
+
+ // read the location of the freq/norm details
+ var n uint64
+ var read int
+
+ rv.freqOffset, read = binary.Uvarint(d.sb.mem[postingsOffset+n : postingsOffset+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ rv.locOffset, read = binary.Uvarint(d.sb.mem[postingsOffset+n : postingsOffset+n+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ var postingsLen uint64
+ postingsLen, read = binary.Uvarint(d.sb.mem[postingsOffset+n : postingsOffset+n+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ roaringBytes, err := d.sb.fileReader.process(d.sb.mem[postingsOffset+n : postingsOffset+n+postingsLen])
+ if err != nil {
+ return err
+ }
+
+ rv.incrementBytesRead(n + postingsLen)
+
+ if rv.postings == nil {
+ rv.postings = roaring.NewBitmap()
+ }
+ _, err = rv.postings.FromBuffer(roaringBytes)
+ if err != nil {
+ return fmt.Errorf("error loading roaring bitmap: %v", err)
+ }
+
+ chunkSize, err := getChunkSize(d.sb.chunkMode,
+ rv.postings.GetCardinality(), d.sb.numDocs)
+ if err != nil {
+ return fmt.Errorf("failed to get chunk size: %v", err)
+ }
+
+ rv.chunkSize = chunkSize
+
+ return nil
+}
+
+func (rv *PostingsList) init1Hit(fstVal uint64) error {
+ docNum, normBits := FSTValDecode1Hit(fstVal)
+
+ rv.docNum1Hit = docNum
+ rv.normBits1Hit = normBits
+
+ return nil
+}
+
+// PostingsIterator provides a way to iterate through the postings list
+type PostingsIterator struct {
+ postings *PostingsList
+ all roaring.IntPeekable
+ Actual roaring.IntPeekable
+ ActualBM *roaring.Bitmap
+
+ currChunk uint32
+ freqNormReader *chunkedIntDecoder
+ locReader *chunkedIntDecoder
+
+ next Posting // reused across Next() calls
+ nextLocs []Location // reused across Next() calls
+ nextSegmentLocs []segment.Location // reused across Next() calls
+
+ docNum1Hit uint64
+ normBits1Hit uint64
+
+ buf []byte
+
+ includeFreqNorm bool
+ includeLocs bool
+
+ bytesRead uint64
+}
+
+var emptyPostingsIterator = &PostingsIterator{}
+
+func (i *PostingsIterator) Size() int {
+ sizeInBytes := reflectStaticSizePostingsIterator + SizeOfPtr +
+ i.next.Size()
+ // account for freqNormReader, locReader if we start using this.
+ for _, entry := range i.nextLocs {
+ sizeInBytes += entry.Size()
+ }
+
+ return sizeInBytes
+}
+
+// Implements the segment.DiskStatsReporter interface
+// The purpose of this implementation is to get
+// the bytes read from the disk which includes
+// the freqNorm and location specific information
+// of a hit
+func (i *PostingsIterator) ResetBytesRead(val uint64) {
+ i.bytesRead = val
+}
+
+func (i *PostingsIterator) BytesRead() uint64 {
+ return i.bytesRead
+}
+
+func (i *PostingsIterator) incrementBytesRead(val uint64) {
+ i.bytesRead += val
+}
+
+func (i *PostingsIterator) BytesWritten() uint64 {
+ return 0
+}
+
+func (i *PostingsIterator) loadChunk(chunk int) error {
+ if i.includeFreqNorm {
+ err := i.freqNormReader.loadChunk(chunk)
+ if err != nil {
+ return err
+ }
+
+ // assign the bytes read at this point, since
+ // the postingsIterator is tracking only the chunk loaded
+ // and the cumulation is tracked correctly in the downstream
+ // intDecoder
+ i.ResetBytesRead(i.freqNormReader.getBytesRead())
+ }
+
+ if i.includeLocs {
+ err := i.locReader.loadChunk(chunk)
+ if err != nil {
+ return err
+ }
+ i.ResetBytesRead(i.locReader.getBytesRead())
+ }
+
+ i.currChunk = uint32(chunk)
+ return nil
+}
+
+func (i *PostingsIterator) readFreqNormHasLocs() (uint64, uint64, bool, error) {
+ if i.normBits1Hit != 0 {
+ return 1, i.normBits1Hit, false, nil
+ }
+
+ freqHasLocs, err := i.freqNormReader.readUvarint()
+ if err != nil {
+ return 0, 0, false, fmt.Errorf("error reading frequency: %v", err)
+ }
+
+ freq, hasLocs := decodeFreqHasLocs(freqHasLocs)
+ if freq == 0 {
+ return freq, 0, hasLocs, nil
+ }
+
+ normBits, err := i.freqNormReader.readUvarint()
+ if err != nil {
+ return 0, 0, false, fmt.Errorf("error reading norm: %v", err)
+ }
+
+ return freq, normBits, hasLocs, nil
+}
+
+func (i *PostingsIterator) skipFreqNormReadHasLocs() (bool, error) {
+ if i.normBits1Hit != 0 {
+ return false, nil
+ }
+
+ freqHasLocs, err := i.freqNormReader.readUvarint()
+ if err != nil {
+ return false, fmt.Errorf("error reading freqHasLocs: %v", err)
+ }
+
+ freq, hasLocs := decodeFreqHasLocs(freqHasLocs)
+ if freq == 0 {
+ return hasLocs, nil
+ }
+
+ i.freqNormReader.SkipUvarint() // Skip normBits.
+
+ return hasLocs, nil // See decodeFreqHasLocs() / hasLocs.
+}
+
+func encodeFreqHasLocs(freq uint64, hasLocs bool) uint64 {
+ rv := freq << 1
+ if hasLocs {
+ rv = rv | 0x01 // 0'th LSB encodes whether there are locations
+ }
+ return rv
+}
+
+func decodeFreqHasLocs(freqHasLocs uint64) (uint64, bool) {
+ freq := freqHasLocs >> 1
+ hasLocs := freqHasLocs&0x01 != 0
+ return freq, hasLocs
+}
+
+// readLocation processes all the integers on the stream representing a single
+// location.
+func (i *PostingsIterator) readLocation(l *Location) error {
+ // read off field
+ fieldID, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location field: %v", err)
+ }
+ // read off pos
+ pos, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location pos: %v", err)
+ }
+ // read off start
+ start, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location start: %v", err)
+ }
+ // read off end
+ end, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location end: %v", err)
+ }
+ // read off num array pos
+ numArrayPos, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location num array pos: %v", err)
+ }
+
+ l.field = i.postings.sb.fieldsInv[fieldID]
+ l.pos = pos
+ l.start = start
+ l.end = end
+
+ if cap(l.ap) < int(numArrayPos) {
+ l.ap = make([]uint64, int(numArrayPos))
+ } else {
+ l.ap = l.ap[:int(numArrayPos)]
+ }
+
+ // read off array positions
+ for k := 0; k < int(numArrayPos); k++ {
+ ap, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading array position: %v", err)
+ }
+
+ l.ap[k] = ap
+ }
+
+ return nil
+}
+
+// Next returns the next posting on the postings list, or nil at the end
+func (i *PostingsIterator) Next() (segment.Posting, error) {
+ return i.nextAtOrAfter(0)
+}
+
+// Advance returns the posting at the specified docNum or it is not present
+// the next posting, or if the end is reached, nil
+func (i *PostingsIterator) Advance(docNum uint64) (segment.Posting, error) {
+ return i.nextAtOrAfter(docNum)
+}
+
+// Next returns the next posting on the postings list, or nil at the end
+func (i *PostingsIterator) nextAtOrAfter(atOrAfter uint64) (segment.Posting, error) {
+ docNum, exists, err := i.nextDocNumAtOrAfter(atOrAfter)
+ if err != nil || !exists {
+ return nil, err
+ }
+
+ i.next = Posting{} // clear the struct
+ rv := &i.next
+ rv.docNum = docNum
+
+ if !i.includeFreqNorm {
+ return rv, nil
+ }
+
+ var normBits uint64
+ var hasLocs bool
+
+ rv.freq, normBits, hasLocs, err = i.readFreqNormHasLocs()
+ if err != nil {
+ return nil, err
+ }
+
+ rv.norm = math.Float32frombits(uint32(normBits))
+
+ if i.includeLocs && hasLocs {
+ // prepare locations into reused slices, where we assume
+ // rv.freq >= "number of locs", since in a composite field,
+ // some component fields might have their IncludeTermVector
+ // flags disabled while other component fields are enabled
+ if rv.freq > 0 {
+ if cap(i.nextLocs) >= int(rv.freq) {
+ i.nextLocs = i.nextLocs[0:rv.freq]
+ } else {
+ i.nextLocs = make([]Location, rv.freq, rv.freq*2)
+ }
+ if cap(i.nextSegmentLocs) < int(rv.freq) {
+ i.nextSegmentLocs = make([]segment.Location, rv.freq, rv.freq*2)
+ }
+ rv.locs = i.nextSegmentLocs[:0]
+ }
+
+ numLocsBytes, err := i.locReader.readUvarint()
+ if err != nil {
+ return nil, fmt.Errorf("error reading location numLocsBytes: %v", err)
+ }
+
+ j := 0
+ var nextLoc *Location
+ startBytesRemaining := i.locReader.Len() // # bytes remaining in the locReader
+ for startBytesRemaining-i.locReader.Len() < int(numLocsBytes) {
+ if len(i.nextLocs) > j {
+ nextLoc = &i.nextLocs[j]
+ } else {
+ nextLoc = &Location{}
+ }
+
+ err := i.readLocation(nextLoc)
+ if err != nil {
+ return nil, err
+ }
+
+ rv.locs = append(rv.locs, nextLoc)
+ j++
+ }
+ }
+
+ return rv, nil
+}
+
+// nextDocNum returns the next docNum on the postings list, and also
+// sets up the currChunk / loc related fields of the iterator.
+func (i *PostingsIterator) nextDocNumAtOrAfter(atOrAfter uint64) (uint64, bool, error) {
+ if i.normBits1Hit != 0 {
+ if i.docNum1Hit == DocNum1HitFinished {
+ return 0, false, nil
+ }
+ if i.docNum1Hit < atOrAfter {
+ // advanced past our 1-hit
+ i.docNum1Hit = DocNum1HitFinished // consume our 1-hit docNum
+ return 0, false, nil
+ }
+ docNum := i.docNum1Hit
+ i.docNum1Hit = DocNum1HitFinished // consume our 1-hit docNum
+ return docNum, true, nil
+ }
+
+ if i.Actual == nil || !i.Actual.HasNext() {
+ return 0, false, nil
+ }
+
+ if i.postings == nil || i.postings == emptyPostingsList {
+ // couldn't find anything
+ return 0, false, nil
+ }
+
+ if i.postings.postings == i.ActualBM {
+ return i.nextDocNumAtOrAfterClean(atOrAfter)
+ }
+
+ if i.postings.chunkSize == 0 {
+ return 0, false, ErrChunkSizeZero
+ }
+
+ i.Actual.AdvanceIfNeeded(uint32(atOrAfter))
+
+ if !i.Actual.HasNext() || !i.all.HasNext() {
+ // couldn't find anything
+ return 0, false, nil
+ }
+
+ n := i.Actual.Next()
+ allN := i.all.Next()
+ nChunk := n / uint32(i.postings.chunkSize)
+
+ // when allN becomes >= to here, then allN is in the same chunk as nChunk.
+ allNReachesNChunk := nChunk * uint32(i.postings.chunkSize)
+
+ // n is the next actual hit (excluding some postings), and
+ // allN is the next hit in the full postings, and
+ // if they don't match, move 'all' forwards until they do
+ for allN != n {
+ // we've reached same chunk, so move the freq/norm/loc decoders forward
+ if i.includeFreqNorm && allN >= allNReachesNChunk {
+ err := i.currChunkNext(nChunk)
+ if err != nil {
+ return 0, false, err
+ }
+ }
+
+ if !i.all.HasNext() {
+ return 0, false, nil
+ }
+
+ allN = i.all.Next()
+ }
+
+ if i.includeFreqNorm && (i.currChunk != nChunk || i.freqNormReader.isNil()) {
+ err := i.loadChunk(int(nChunk))
+ if err != nil {
+ return 0, false, fmt.Errorf("error loading chunk: %v", err)
+ }
+ }
+
+ return uint64(n), true, nil
+}
+
+var freqHasLocs1Hit = encodeFreqHasLocs(1, false)
+
+// nextBytes returns the docNum and the encoded freq & loc bytes for
+// the next posting
+func (i *PostingsIterator) nextBytes() (
+ docNumOut uint64, freq uint64, normBits uint64,
+ bytesFreqNorm []byte, bytesLoc []byte, err error) {
+ docNum, exists, err := i.nextDocNumAtOrAfter(0)
+ if err != nil || !exists {
+ return 0, 0, 0, nil, nil, err
+ }
+
+ if i.normBits1Hit != 0 {
+ if i.buf == nil {
+ i.buf = make([]byte, binary.MaxVarintLen64*2)
+ }
+ n := binary.PutUvarint(i.buf, freqHasLocs1Hit)
+ n += binary.PutUvarint(i.buf[n:], i.normBits1Hit)
+ return docNum, uint64(1), i.normBits1Hit, i.buf[:n], nil, nil
+ }
+
+ startFreqNorm := i.freqNormReader.remainingLen()
+
+ var hasLocs bool
+
+ freq, normBits, hasLocs, err = i.readFreqNormHasLocs()
+ if err != nil {
+ return 0, 0, 0, nil, nil, err
+ }
+
+ endFreqNorm := i.freqNormReader.remainingLen()
+ bytesFreqNorm = i.freqNormReader.readBytes(startFreqNorm, endFreqNorm)
+
+ if hasLocs {
+ startLoc := i.locReader.remainingLen()
+
+ numLocsBytes, err := i.locReader.readUvarint()
+ if err != nil {
+ return 0, 0, 0, nil, nil,
+ fmt.Errorf("error reading location nextBytes numLocs: %v", err)
+ }
+
+ // skip over all the location bytes
+ i.locReader.SkipBytes(int(numLocsBytes))
+
+ endLoc := i.locReader.remainingLen()
+ bytesLoc = i.locReader.readBytes(startLoc, endLoc)
+ }
+
+ return docNum, freq, normBits, bytesFreqNorm, bytesLoc, nil
+}
+
+// optimization when the postings list is "clean" (e.g., no updates &
+// no deletions) where the all bitmap is the same as the actual bitmap
+func (i *PostingsIterator) nextDocNumAtOrAfterClean(
+ atOrAfter uint64) (uint64, bool, error) {
+ if !i.includeFreqNorm {
+ i.Actual.AdvanceIfNeeded(uint32(atOrAfter))
+
+ if !i.Actual.HasNext() {
+ return 0, false, nil // couldn't find anything
+ }
+
+ return uint64(i.Actual.Next()), true, nil
+ }
+
+ if i.postings != nil && i.postings.chunkSize == 0 {
+ return 0, false, ErrChunkSizeZero
+ }
+
+ // freq-norm's needed, so maintain freq-norm chunk reader
+ sameChunkNexts := 0 // # of times we called Next() in the same chunk
+ n := i.Actual.Next()
+ nChunk := n / uint32(i.postings.chunkSize)
+
+ for uint64(n) < atOrAfter && i.Actual.HasNext() {
+ n = i.Actual.Next()
+
+ nChunkPrev := nChunk
+ nChunk = n / uint32(i.postings.chunkSize)
+
+ if nChunk != nChunkPrev {
+ sameChunkNexts = 0
+ } else {
+ sameChunkNexts += 1
+ }
+ }
+
+ if uint64(n) < atOrAfter {
+ // couldn't find anything
+ return 0, false, nil
+ }
+
+ for j := 0; j < sameChunkNexts; j++ {
+ err := i.currChunkNext(nChunk)
+ if err != nil {
+ return 0, false, fmt.Errorf("error optimized currChunkNext: %v", err)
+ }
+ }
+
+ if i.currChunk != nChunk || i.freqNormReader.isNil() {
+ err := i.loadChunk(int(nChunk))
+ if err != nil {
+ return 0, false, fmt.Errorf("error loading chunk: %v", err)
+ }
+ }
+
+ return uint64(n), true, nil
+}
+
+func (i *PostingsIterator) currChunkNext(nChunk uint32) error {
+ if i.currChunk != nChunk || i.freqNormReader.isNil() {
+ err := i.loadChunk(int(nChunk))
+ if err != nil {
+ return fmt.Errorf("error loading chunk: %v", err)
+ }
+ }
+
+ // read off freq/offsets even though we don't care about them
+ hasLocs, err := i.skipFreqNormReadHasLocs()
+ if err != nil {
+ return err
+ }
+
+ if i.includeLocs && hasLocs {
+ numLocsBytes, err := i.locReader.readUvarint()
+ if err != nil {
+ return fmt.Errorf("error reading location numLocsBytes: %v", err)
+ }
+
+ // skip over all the location bytes
+ i.locReader.SkipBytes(int(numLocsBytes))
+ }
+
+ return nil
+}
+
+// DocNum1Hit returns the docNum and true if this is "1-hit" optimized
+// and the docNum is available.
+func (p *PostingsIterator) DocNum1Hit() (uint64, bool) {
+ if p.normBits1Hit != 0 && p.docNum1Hit != DocNum1HitFinished {
+ return p.docNum1Hit, true
+ }
+ return 0, false
+}
+
+// ActualBitmap returns the underlying actual bitmap
+// which can be used up the stack for optimizations
+func (p *PostingsIterator) ActualBitmap() *roaring.Bitmap {
+ return p.ActualBM
+}
+
+// ReplaceActual replaces the ActualBM with the provided
+// bitmap
+func (p *PostingsIterator) ReplaceActual(abm *roaring.Bitmap) {
+ p.ActualBM = abm
+ p.Actual = abm.Iterator()
+}
+
+// PostingsIteratorFromBitmap constructs a PostingsIterator given an
+// "actual" bitmap.
+func PostingsIteratorFromBitmap(bm *roaring.Bitmap,
+ includeFreqNorm, includeLocs bool) (segment.PostingsIterator, error) {
+ return &PostingsIterator{
+ ActualBM: bm,
+ Actual: bm.Iterator(),
+ includeFreqNorm: includeFreqNorm,
+ includeLocs: includeLocs,
+ }, nil
+}
+
+// PostingsIteratorFrom1Hit constructs a PostingsIterator given a
+// 1-hit docNum.
+func PostingsIteratorFrom1Hit(docNum1Hit uint64,
+ includeFreqNorm, includeLocs bool) (segment.PostingsIterator, error) {
+ return &PostingsIterator{
+ docNum1Hit: docNum1Hit,
+ normBits1Hit: NormBits1Hit,
+ includeFreqNorm: includeFreqNorm,
+ includeLocs: includeLocs,
+ }, nil
+}
+
+// Posting is a single entry in a postings list
+type Posting struct {
+ docNum uint64
+ freq uint64
+ norm float32
+ locs []segment.Location
+}
+
+func (p *Posting) Size() int {
+ sizeInBytes := reflectStaticSizePosting
+
+ for _, entry := range p.locs {
+ sizeInBytes += entry.Size()
+ }
+
+ return sizeInBytes
+}
+
+// Number returns the document number of this posting in this segment
+func (p *Posting) Number() uint64 {
+ return p.docNum
+}
+
+// Frequency returns the frequencies of occurrence of this term in this doc/field
+func (p *Posting) Frequency() uint64 {
+ return p.freq
+}
+
+// Norm returns the normalization factor for this posting
+func (p *Posting) Norm() float64 {
+ return float64(float32(1.0 / math.Sqrt(float64(math.Float32bits(p.norm)))))
+}
+
+// Locations returns the location information for each occurrence
+func (p *Posting) Locations() []segment.Location {
+ return p.locs
+}
+
+// NormUint64 returns the norm value as uint64
+func (p *Posting) NormUint64() uint64 {
+ return uint64(math.Float32bits(p.norm))
+}
+
+// Location represents the location of a single occurrence
+type Location struct {
+ field string
+ pos uint64
+ start uint64
+ end uint64
+ ap []uint64
+}
+
+func (l *Location) Size() int {
+ return reflectStaticSizeLocation +
+ len(l.field) +
+ len(l.ap)*SizeOfUint64
+}
+
+// Field returns the name of the field (useful in composite fields to know
+// which original field the value came from)
+func (l *Location) Field() string {
+ return l.field
+}
+
+// Start returns the start byte offset of this occurrence
+func (l *Location) Start() uint64 {
+ return l.start
+}
+
+// End returns the end byte offset of this occurrence
+func (l *Location) End() uint64 {
+ return l.end
+}
+
+// Pos returns the 1-based phrase position of this occurrence
+func (l *Location) Pos() uint64 {
+ return l.pos
+}
+
+// ArrayPositions returns the array position vector associated with this occurrence
+func (l *Location) ArrayPositions() []uint64 {
+ return l.ap
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/read.go b/vendor/github.com/blevesearch/zapx/v17/read.go
new file mode 100644
index 0000000000..690705596d
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/read.go
@@ -0,0 +1,65 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import "encoding/binary"
+
+func (sb *SegmentBase) getDocStoredMetaAndCompressed(docNum uint64) ([]byte, []byte, error) {
+ _, storedOffset, n, metaLen, dataLen := sb.getDocStoredOffsets(docNum)
+
+ meta := sb.mem[storedOffset+n : storedOffset+n+metaLen]
+ data := sb.mem[storedOffset+n+metaLen : storedOffset+n+metaLen+dataLen]
+
+ meta, err := sb.fileReader.process(meta)
+ if err != nil {
+ return nil, nil, err
+ }
+ data, err = sb.fileReader.process(data)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return meta, data, nil
+}
+
+func (sb *SegmentBase) getDocStoredOffsets(docNum uint64) (
+ uint64, uint64, uint64, uint64, uint64) {
+ indexOffset := sb.storedIndexOffset + (8 * docNum)
+
+ storedOffset := binary.BigEndian.Uint64(sb.mem[indexOffset : indexOffset+8])
+
+ var n uint64
+
+ metaLen, read := binary.Uvarint(sb.mem[storedOffset : storedOffset+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ dataLen, read := binary.Uvarint(sb.mem[storedOffset+n : storedOffset+n+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ return indexOffset, storedOffset, n, metaLen, dataLen
+}
+
+func (sb *SegmentBase) getEdgeListOffset() uint64 {
+ // if no stored index, then no edge list
+ if sb.storedIndexOffset == 0 {
+ return 0
+ }
+ // Edge list comes after the stored fields index (doc stored offsets)
+ // The stored index offset points to where the doc offsets start
+ // So edge list starts right after the last document offset
+ // which is at sb.storedIndexOffset + (8 * sb.numDocs)
+ // since each doc offset is 8 bytes
+ return sb.storedIndexOffset + (8 * sb.numDocs)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/section.go b/vendor/github.com/blevesearch/zapx/v17/section.go
new file mode 100644
index 0000000000..8d431644f3
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/section.go
@@ -0,0 +1,83 @@
+// Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "sync"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+type section interface {
+ // process is essentially parsing of a specific field's content in a specific
+ // document. any tracking of processed data *specific to this section* should
+ // be done in opaque which will be passed to the Persist() API.
+ Process(opaque map[int]resetable, docNum uint32, f index.Field, fieldID uint16)
+
+ // flush the processed data in the opaque to the writer.
+ Persist(opaque map[int]resetable, w *FileWriter) error
+
+ // this API is used to fetch the file offset of the field for this section.
+ // this is used during search time to parse the section, and fetch results
+ // for the specific "index" thats part of the section.
+ AddrForField(opaque map[int]resetable, fieldID int) int
+
+ // for every field in the fieldsInv (relevant to this section) merge the section
+ // contents from all the segments into a single section data for the field.
+ // as part of the merge API, write the merged data to the writer and also track
+ // the starting offset of this newly merged section data.
+ Merge(opaque map[int]resetable, segments []*SegmentBase, drops []*roaring.Bitmap, fieldsInv []string,
+ newDocNumsIn [][]uint64, w *FileWriter, closeCh chan struct{}) error
+
+ // opaque is used to track the data specific to this section. its not visible
+ // to the other sections and is only visible and freely modifiable by this specifc
+ // section.
+ InitOpaque(args map[string]interface{}) resetable
+}
+
+type resetable interface {
+ Reset() error
+ Set(key string, value interface{})
+}
+
+// -----------------------------------------------------------------------------
+
+const (
+ SectionInvertedTextIndex = iota
+ SectionFaissVectorIndex
+ SectionSynonymIndex
+
+ // Add new sections above this line.
+ // NumSections automatically reflects the total number of sections
+ // and is used to track how many sections can be registered.
+ NumSections
+)
+
+// -----------------------------------------------------------------------------
+
+var (
+ segmentSectionsMutex sync.Mutex
+ // writes to segmentSections within init()s ONLY within lock,
+ // reads will not require lock access
+ segmentSections = make(map[uint16]section)
+)
+
+// Method to be invoked within init()s ONLY.
+func registerSegmentSection(key uint16, val section) {
+ segmentSectionsMutex.Lock()
+ segmentSections[key] = val
+ segmentSectionsMutex.Unlock()
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/section_faiss_vector_index.go b/vendor/github.com/blevesearch/zapx/v17/section_faiss_vector_index.go
new file mode 100644
index 0000000000..b3492574d0
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/section_faiss_vector_index.go
@@ -0,0 +1,1052 @@
+// Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build vectors
+// +build vectors
+
+package zap
+
+import (
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "math"
+ "sync/atomic"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ faiss "github.com/blevesearch/go-faiss"
+ seg "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+func init() {
+ registerSegmentSection(SectionFaissVectorIndex, &faissVectorIndexSection{})
+ invertedTextIndexSectionExclusionChecks = append(invertedTextIndexSectionExclusionChecks, func(field index.Field) bool {
+ _, ok := field.(index.VectorField)
+ return ok
+ })
+ faiss.SetOMPThreads(defaultFaissOMPThreads)
+}
+
+const (
+ // Set the default number of OMP threads to be used by FAISS
+ // to 1 since openMP does not support goroutine based threading well.
+ defaultFaissOMPThreads = 1
+ // Divide the estimated nprobe with this value to optimize
+ // for latency.
+ nprobeLatencyOptimization = 2
+ // The threshold for number of vectors beyond which we start building the ivf class
+ // of indexes
+ ivfThreshold = 1000
+ // The threshold for number of vectors beyond which we consider fast merging
+ // using faiss's native merge capabilities, instead of reconstructing and adding
+ // vectors one by one
+ ivfSq8Threshold = 10000
+)
+
+// Vector index types supported.
+type faissIndexType uint64
+
+const (
+ // faissFP32Index represents the standard float32 index in Faiss,
+ // which stores vectors in 32-bit floating point format.
+ faissFP32Index faissIndexType = iota
+ // faissBIVFIndex represents the binary IVF index in Faiss,
+ // which stores vectors in a 8-bit binary format.
+ faissBIVFIndex
+)
+
+// Errors for invariant violations related to fast merge and trained index retrieval.
+var (
+ ErrorTrainedIndexNotIVF error = errors.New("trained index is not an IVF index, which is required for fast merge")
+ ErrorFastMergeIndexNotIVF error = errors.New("fast merge is only supported for IVF indexes")
+)
+
+type faissVectorIndexSection struct {
+}
+
+func (v *faissVectorIndexSection) Process(opaque map[int]resetable, docNum uint32, field index.Field, fieldID uint16) {
+ if fieldID == math.MaxUint16 {
+ return
+ }
+ if vf, ok := field.(index.VectorField); ok {
+ vo := v.getVectorIndexOpaque(opaque)
+ vo.process(vf, field.Name(), fieldID, docNum)
+ }
+}
+
+func (v *faissVectorIndexSection) Persist(opaque map[int]resetable, w *FileWriter) error {
+ vo := v.getVectorIndexOpaque(opaque)
+ return vo.writeVectorIndexes(w)
+}
+
+func (v *faissVectorIndexSection) AddrForField(opaque map[int]resetable, fieldID int) int {
+ vo := v.getVectorIndexOpaque(opaque)
+ return vo.fieldAddrs[uint16(fieldID)]
+}
+
+// vecIndexInfo contains information specific to a vector index,
+// including metadata and the faiss index pointer itself.
+type vecIndexInfo struct {
+ startOffset int
+ indexSize uint64
+ vecIds []int64
+ indexOptimizedFor string
+ indexType faissIndexType
+ index faissIndex
+}
+
+// Merge merges vector indexes from multiple segments into a single index.
+func (v *faissVectorIndexSection) Merge(opaque map[int]resetable, segments []*SegmentBase,
+ drops []*roaring.Bitmap, fieldsInv []string, newDocNumsIn [][]uint64, w *FileWriter,
+ closeCh chan struct{}) error {
+ vo := v.getVectorIndexOpaque(opaque)
+ // preallocating the space over here, if there are too many fields
+ // in the segment this will help by avoiding multiple allocation
+ // calls.
+ // the segments with valid vector sections in them
+ vecSegs := make([]*SegmentBase, 0, len(segments))
+ // vector index information from those segments
+ indexes := make([]*vecIndexInfo, 0, len(segments))
+ // mapping from vector IDs to docIDs across segments
+ vecToDocID := make([]uint64, 0, len(segments))
+ // for every field, gather the vector indexes from the segments
+ // that have them, merge them and write them out to the writer.
+ for fieldID, fieldName := range fieldsInv {
+ // continue if field is not required to be indexed
+ if !vo.fieldsOptions[fieldName].IsIndexed() {
+ continue
+ }
+ indexes = indexes[:0] // resizing the slices
+ vecSegs = vecSegs[:0]
+ vecToDocID = vecToDocID[:0]
+ // flag to indicate if there are deleted/updated vectors
+ // in any of the vector indexes being merged.
+ var drops bool
+ for segI, sb := range segments {
+ if isClosed(closeCh) {
+ return seg.ErrClosed
+ }
+ if _, ok := sb.fieldsMap[fieldName]; !ok {
+ continue
+ }
+ // check if the section address is a valid one for "fieldName" in the
+ // segment sb. the local fieldID (fetched by the fieldsMap of the sb)
+ // is to be used while consulting the fieldsSectionsMap
+ pos := int(sb.fieldsSectionsMap[sb.fieldsMap[fieldName]-1][SectionFaissVectorIndex])
+ if pos == 0 {
+ continue
+ }
+
+ // loading doc values - adhering to the sections format. never
+ // valid values for vector section
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ _, n = binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+
+ // read the vector index optimization type represented as an int
+ indexOptimizationTypeInt, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ // read the number of vectors
+ numVecs, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+
+ // track the valid vectors to be reconstructed for this segment
+ // during the merge operation.
+ newIndexInfo := &vecIndexInfo{
+ indexOptimizedFor: index.VectorIndexOptimizationsReverseLookup[int(indexOptimizationTypeInt)],
+ vecIds: make([]int64, 0, numVecs),
+ }
+
+ // read the length of the docID list
+ listLen, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ buf, err := sb.fileReader.process(sb.mem[pos : pos+int(listLen)])
+ if err != nil {
+ return err
+ }
+ pos += int(listLen)
+
+ bufPos := 0
+ bufLen := len(buf)
+ for vecID := 0; vecID < int(numVecs); vecID++ {
+ docID, n := binary.Uvarint(buf[bufPos:min(bufPos+binary.MaxVarintLen64, bufLen)])
+ bufPos += n
+ // check if this docID is dropped in the new segment
+ newDocID := newDocNumsIn[segI][uint32(docID)]
+ if newDocID != docDropped {
+ // valid docID, track the mapping
+ vecToDocID = append(vecToDocID, newDocID)
+ // if the remapped doc ID is valid, track it
+ // as part of vecs to be reconstructed (for larger indexes).
+ // This accounts only for valid vector IDs, so deleted
+ // ones won't be reconstructed in the final index.
+ newIndexInfo.vecIds = append(newIndexInfo.vecIds, int64(vecID))
+ } else {
+ // some vectors are dropped, so we can't do a fast merge using faiss's
+ // native merge capabilities, because of the data drift issue.
+ drops = true
+ }
+ }
+
+ if len(newIndexInfo.vecIds) == 0 {
+ // no valid vectors to be merged from this segment
+ continue
+ }
+
+ // read the type of vector index
+ indexType, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ // read the size of the vector index
+ indexSize, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ // record the start offset and size of the vector index
+ newIndexInfo.startOffset = pos
+ newIndexInfo.indexSize = indexSize
+ newIndexInfo.indexType = faissIndexType(indexType)
+ vecSegs = append(vecSegs, sb)
+ indexes = append(indexes, newIndexInfo)
+ pos += int(indexSize)
+ }
+ // continue if there are absolutely no valid vectors present in the segment
+ // for this field and crucially don't store the section start offset in it
+ if len(indexes) == 0 || len(vecToDocID) == 0 {
+ continue
+ }
+
+ err := vo.flushSectionMetadata(fieldID, w, vecToDocID, indexes)
+ if err != nil {
+ return err
+ }
+
+ // we're going to use the trained index template regardless of whether there's
+ // a update/delete in the segments being merged and we let the fast merge
+ // path handle what exactly to do in such cases.
+ trainedIndex, err := trainedIndexFromConfig(vo.config, fieldName)
+ if err != nil {
+ return err
+ }
+
+ useGPU := vo.fieldsOptions[fieldName].UseGPU()
+ err = vo.mergeAndWriteVectorIndexes(trainedIndex, vecSegs, indexes, w, closeCh, useGPU, drops)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func trainedIndexFromConfig(config map[string]interface{}, fieldName string) (faissIndexIVF, error) {
+ var trainedIndexFor index.TrainedIndexCallbackFn
+ var training bool
+ var rv faissIndex
+ if cb, ok := config[index.TrainedIndexCallback]; ok {
+ trainedIndexFor = cb.(index.TrainedIndexCallbackFn)
+ }
+ if tf, ok := config[index.TrainingKey]; ok {
+ training = tf.(bool)
+ }
+ // if we have a callback registered AND if the training flag is not set:
+ // - fastmerge is supported for this index
+ // - we're not in the training phase of index creation where you want to be
+ // able to reconstruct the vectors for training
+ if trainedIndexFor != nil && !training {
+ trainedIndex, err := trainedIndexFor(fieldName)
+ if err != nil {
+ return nil, err
+ }
+ if trainedIndex != nil {
+ rv = trainedIndex.(faissIndex)
+ }
+ }
+ if rv == nil {
+ return nil, nil
+ }
+
+ trainedIndex := rv.castIVF()
+ if trainedIndex == nil {
+ return nil, ErrorTrainedIndexNotIVF
+ }
+ return trainedIndex, nil
+}
+
+func (v *vectorIndexOpaque) flushSectionMetadata(fieldID int, w *FileWriter,
+ vecToDocID []uint64, indexes []*vecIndexInfo) error {
+ tempBuf := v.grabBuf(binary.MaxVarintLen64)
+ fieldStart := w.Count()
+
+ // marking the fact that for vector index, doc values are not valid by
+ // storing fieldNotUninverted values.
+ n := binary.PutUvarint(tempBuf, fieldNotUninverted)
+ _, err := w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ n = binary.PutUvarint(tempBuf, fieldNotUninverted)
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ // write the index optimization type
+ n = binary.PutUvarint(tempBuf, uint64(index.SupportedVectorIndexOptimizations[indexes[0].indexOptimizedFor]))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ // write the number of vectors
+ n = binary.PutUvarint(tempBuf, uint64(len(vecToDocID)))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ buf := make([]byte, binary.MaxVarintLen64*len(vecToDocID))
+ bufPos := 0
+ for _, docID := range vecToDocID {
+ n = binary.PutUvarint(buf[bufPos:], docID)
+ bufPos += n
+ }
+ buf = w.process(buf[:bufPos])
+
+ // write the size of the vector to docID map
+ n = binary.PutUvarint(tempBuf, uint64(len(buf)))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ // write the vecID -> docID mapping
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ // record the fieldStart value for this section.
+ v.fieldAddrs[uint16(fieldID)] = fieldStart
+ return nil
+}
+
+// Calculates the nprobe count, given nlist(number of centroids) based on
+// the metric the index is optimized for.
+func calculateNprobe(nlist int, indexOptimizedFor string) int32 {
+ nprobe := int32(math.Sqrt(float64(nlist)))
+ if indexOptimizedFor == index.IndexOptimizedForLatency {
+ nprobe /= nprobeLatencyOptimization
+ if nprobe < 1 {
+ nprobe = 1
+ }
+ }
+ return nprobe
+}
+
+// todo: need to detect and handle data drift in a more intelligent way
+func (v *vectorIndexOpaque) fastMergeIndexes(trainedIndex faissIndexIVF, cfg *faissIndexConfig,
+ drops bool, vecIndexes []*vecIndexInfo, w *FileWriter, closeCh chan struct{}) error {
+ // create a faissIndex for merged index using nlist and nprobe from trained index's
+ // config and we're hitting the fast merge path only if we've not enabled GPU
+ nprobe, nlist := trainedIndex.ivfParams()
+ cfg.nlist = nlist
+ mergedIdx, err := faissIndexFactory(cfg)
+ if err != nil {
+ return err
+ }
+ defer mergedIdx.close()
+
+ // cast to IVF index to be able to set the quantizer for the fast merge
+ ivfMergedIdx := mergedIdx.castIVF()
+ if ivfMergedIdx == nil {
+ return ErrorFastMergeIndexNotIVF
+ }
+ err = ivfMergedIdx.setDirectMap(1)
+ if err != nil {
+ return err
+ }
+ // setting the same nprobe value in the merged index as the centroid
+ // index to ensure that we probe the same number of clusters
+ ivfMergedIdx.setNProbe(int32(nprobe))
+ // using the trained index to copy the quantizers and set them in the final
+ // merged index if possible
+ err = ivfMergedIdx.setQuantizers(trainedIndex)
+ if err != nil {
+ return err
+ }
+
+ var reconsVecs []float32
+ reconsAndAddFrom := func(vecIDs []int64, srcIdx faissIndex) error {
+ neededReconsLen := len(vecIDs) * cfg.dimension
+ if cap(reconsVecs) < neededReconsLen {
+ reconsVecs = make([]float32, neededReconsLen)
+ }
+ reconsVecs = reconsVecs[:neededReconsLen]
+ reconsVecs, err = srcIdx.reconstructBatch(vecIDs, reconsVecs)
+ if err != nil {
+ return err
+ }
+
+ vecSet, err := newVectorSet(cfg.dimension, reconsVecs)
+ if err != nil {
+ return err
+ }
+ if cfg.indexType == faissBIVFIndex {
+ vecSet.binarize()
+ }
+ // add to target index the reconstructed vectors for the valid vector IDs from the source index.
+ err = mergedIdx.add(vecSet)
+ if err != nil {
+ return err
+ }
+ reconsVecs = reconsVecs[:0]
+ return nil
+ }
+
+ for _, vi := range vecIndexes {
+ if isClosed(closeCh) {
+ return seg.ErrClosed
+ }
+ childIdx := vi.index
+ if drops {
+ // if there are some deletes or updates in the segments being merged,
+ // we can't say definitely which mutation can cause a data drift solely
+ // by the vector count - as a fallback mechanism we reconstruct + add
+ // the vectors in this scenario using the trained template since we can't
+ // use merge_from.
+ err = reconsAndAddFrom(vi.vecIds, childIdx)
+ if err != nil {
+ return err
+ }
+ } else {
+ if err = ivfMergedIdx.mergeFrom(childIdx, mergedIdx.ntotal()); err != nil {
+ // either the childIdx isn't compatible for fast merge or merge_from failed
+ // so, in either case we can fallback to reconstructing and adding the vectors
+ // one by one from the source index to the target index as a error handling mechanism.
+ err = reconsAndAddFrom(vi.vecIds, childIdx)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ }
+
+ tempBuf := v.grabBuf(binary.MaxVarintLen64)
+ // write the type of the vector index
+ n := binary.PutUvarint(tempBuf, uint64(cfg.indexType))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ return mergedIdx.write(tempBuf, w)
+}
+
+func (v *vectorIndexOpaque) mergeAndWriteVectorIndexes(trainedIndex faissIndexIVF, sbs []*SegmentBase,
+ vecIndexes []*vecIndexInfo, w *FileWriter, closeCh chan struct{}, useGPU, drops bool) error {
+ // safe to assume that all the indexes are of the same config values, given
+ // that they are extracted from the field mapping info.
+ var dims, metric, indexDataCap, reconsCap, nvecs int
+ var indexOptimizedFor string
+ var indexType faissIndexType
+ var validMerge bool
+
+ for segI, segBase := range sbs {
+ // Considering merge operations on vector indexes are expensive, it is
+ // worth including an early exit if the merge is aborted, saving us
+ // the resource spikes, even if temporary.
+ if isClosed(closeCh) {
+ freeReconstructedIndexes(vecIndexes)
+ return seg.ErrClosed
+ }
+ // track which index we are currently processing
+ currVecIndex := vecIndexes[segI]
+ currNumVecs := len(currVecIndex.vecIds)
+ // if no valid vectors for this index, don't bring it into memory
+ if currNumVecs == 0 {
+ continue
+ }
+
+ // read the serialized index bytes
+ indexBytes, err := segBase.fileReader.process(segBase.mem[currVecIndex.startOffset : currVecIndex.startOffset+int(currVecIndex.indexSize)])
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+ ioFlags := faissIOFlags
+ if trainedIndex == nil {
+ ioFlags = faissIOFlagsReadOnly
+ }
+ // reconstruct the faiss index from the bytes
+ faissIndex, err := faiss.ReadIndexFromBuffer(indexBytes, ioFlags)
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+
+ // set the dims and metric values from the constructed index.
+ dims = faissIndex.D()
+ // at least one valid index to be merged, mark the merge as valid.
+ validMerge = true
+ metric = faissIndex.MetricType()
+ indexOptimizedFor = currVecIndex.indexOptimizedFor
+ indexType = currVecIndex.indexType
+ // update trackers for buffer capacities
+ indexReconsLen := currNumVecs * dims
+ if indexReconsLen > reconsCap {
+ reconsCap = indexReconsLen
+ }
+ indexDataCap += indexReconsLen
+
+ // track the reconstruct index for this vector index, which will be used
+ // to reconstruct the vectors corresponding to the valid vector IDs for this index.
+ config := newFaissIndexConfig(indexType, indexOptimizedFor, dims, metric, currNumVecs, determineCentroids(currNumVecs), useGPU)
+ fIndex, err := newFaissFloat32IndexWithConfig(faissIndex, config)
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+ vecIndexes[segI].index = fIndex
+
+ // load binary index from disk if present
+ if currVecIndex.indexType == faissBIVFIndex {
+ // get to the bivf part of the vector index section
+ pos := currVecIndex.startOffset + int(currVecIndex.indexSize)
+ binSize, n := binary.Uvarint(segBase.mem[pos : pos+binary.MaxVarintLen64])
+ pos += n
+ indexBytes, err = segBase.fileReader.process(segBase.mem[pos : pos+int(binSize)])
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+
+ binaryIndex, err := faiss.ReadBinaryIndexFromBuffer(indexBytes, ioFlags)
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+ vecIndexes[segI].index, err = newFaissBinaryIndexWithConfig(binaryIndex, faissIndex, config)
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+ }
+ nvecs += currNumVecs
+ }
+
+ // not a valid merge operation as there are no valid indexes to merge.
+ if !validMerge {
+ return nil
+ }
+ // if no valid vectors after merge, nothing to do
+ if nvecs == 0 {
+ // no valid vectors for this index, so we don't even have to
+ // record it in the section
+ freeReconstructedIndexes(vecIndexes)
+ return nil
+ }
+
+ // create the faiss index to hold the merged data, either via fast merge or reconstruction
+ config := newFaissIndexConfig(indexType, indexOptimizedFor, dims, metric, nvecs, determineCentroids(nvecs), useGPU)
+ // we perform fast merge if we're not using the GPU and if the trained index
+ // is compatible to be used for fast merge
+ if !useGPU && canFastMerge(trainedIndex, indexOptimizedFor, nvecs) {
+ err := v.fastMergeIndexes(trainedIndex, config, drops, vecIndexes, w, closeCh)
+ if err != nil {
+ return err
+ }
+ // free the indexes as we won't need them anymore after the fast merge
+ freeReconstructedIndexes(vecIndexes)
+ return nil
+ }
+
+ // Reconstruct Merge Path:
+ // merging of indexes with reconstruction method.
+ // the vecIds in each index contain only the valid vectors,
+ // so we reconstruct only those.
+ indexData := make([]float32, 0, indexDataCap)
+ // reusable buffer for reconstruction
+ recons := make([]float32, 0, reconsCap)
+ for idx, currVecIndex := range vecIndexes {
+ if isClosed(closeCh) {
+ freeReconstructedIndexes(vecIndexes)
+ return seg.ErrClosed
+ }
+ currNumVecs := len(currVecIndex.vecIds)
+ // reconstruct the vectors only if present, it could be that
+ // some of the indexes had all of their vectors updated/deleted.
+ if currNumVecs > 0 && vecIndexes[idx] != nil {
+ neededReconsLen := currNumVecs * config.dimension
+ recons = recons[:neededReconsLen]
+ var err error
+ fIndex := vecIndexes[idx].index
+ recons, err = fIndex.reconstructBatch(currVecIndex.vecIds, recons)
+ if err != nil {
+ freeReconstructedIndexes(vecIndexes)
+ return err
+ }
+ indexData = append(indexData, recons...)
+ }
+ }
+
+ // freeing the reconstructed indexes immediately - waiting till the end
+ // to do the same is not needed because the following operations don't need
+ // the reconstructed ones anymore and doing so will hold up memory which can
+ // be detrimental while creating indexes during introduction.
+ freeReconstructedIndexes(vecIndexes)
+
+ vecSet, err := newVectorSet(config.dimension, indexData)
+ if err != nil {
+ return err
+ }
+ return v.writeFaissIndex(vecSet, config, w)
+}
+
+// constructs a faiss on the vectors according to the provided config and writes it out
+// the given writer
+func (v *vectorIndexOpaque) writeFaissIndex(vecs *vectorSet, config *faissIndexConfig, w *FileWriter) error {
+ // create the faiss index based on the provided description string, and the metric type.
+ index, err := faissIndexFactory(config)
+ if err != nil {
+ return err
+ }
+ // ensure the faiss index is closed after use
+ defer index.close()
+
+ // binarize the vectors for BIVF indexes
+ if config.indexType == faissBIVFIndex {
+ vecs.binarize()
+ }
+ // if we are using an IVF index, train and add first, then set the direct map
+ // and nprobe. The order matters for GPU indexes: CloneToCPU (done inside
+ // trainAndAdd) clears the direct map and nprobe, so they must be set after.
+ if ivfIndex := index.castIVF(); ivfIndex != nil {
+ // train the vector index and add the vectors to it. The training step
+ // performs k-means clustering to partition the data space such that during
+ // search time we probe only a subset of vectors (non-exhaustive search).
+ err = ivfIndex.trainAndAdd(vecs, vecs)
+ if err != nil {
+ return err
+ }
+ // the direct map maintained in the IVF index is essential for the
+ // reconstruction of vectors based on the sequential vector IDs in the
+ // future merges use direct map type 1 -> array based direct map, since
+ // we have sequential vector IDs starting from 0 to N-1.
+ err = ivfIndex.setDirectMap(1)
+ if err != nil {
+ return err
+ }
+ // calculate nprobe using a heuristic.
+ nprobe := calculateNprobe(config.nlist, config.optimizationType)
+ ivfIndex.setNProbe(nprobe)
+ } else {
+ // add the vectors to the index using sequential vector IDs starting
+ // from 0 to N-1
+ err = index.add(vecs)
+ if err != nil {
+ return err
+ }
+ }
+
+ // get a temporary buffer for writing out the index
+ tempBuf := v.grabBuf(binary.MaxVarintLen64)
+ // write the type of the vector index
+ n := binary.PutUvarint(tempBuf, uint64(config.indexType))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ // serialize the merged index into a byte slice, and write it out
+ err = index.write(tempBuf, w)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// returns the index description string and index type constant for the binary
+// index to be created based on the number of vectors and centroids.
+func determineBinaryIndexToUse(nvecs, nlist int) string {
+ switch {
+ case nvecs >= ivfThreshold:
+ return fmt.Sprintf("BIVF%d", nlist)
+ default:
+ return "BFlat"
+ }
+}
+
+// returns the index type constant for the vector index to be created based on the
+// index optimization type specified in the field mapping.
+func determineIndexTypeFromOptimization(indexOptimizedFor string) faissIndexType {
+ if index.OptimizationRequiresBinaryIndex(indexOptimizedFor) {
+ return faissBIVFIndex
+ }
+ return faissFP32Index
+}
+
+// freeReconstructedIndexes closes all faiss indexes in the provided slice.
+func freeReconstructedIndexes(vecIndexes []*vecIndexInfo) {
+ for _, entry := range vecIndexes {
+ if entry != nil && entry.index != nil {
+ entry.index.close()
+ }
+ }
+}
+
+// grabBuf returns a reusable buffer of the given size, allocating a new one if needed.
+func (v *vectorIndexOpaque) grabBuf(size int) []byte {
+ buf := v.tmp0
+ if cap(buf) < size {
+ buf = make([]byte, size)
+ v.tmp0 = buf
+ }
+ return buf[:size]
+}
+
+// determineCentroids determines the number of centroids to use for an IVF index.
+func determineCentroids(nvecs int) int {
+ var nlist int
+ switch {
+ case nvecs >= 200000:
+ nlist = int(4 * math.Sqrt(float64(nvecs)))
+ case nvecs >= ivfThreshold:
+ // 100 points per cluster is a reasonable default, considering the default
+ // minimum and maximum points per cluster is 39 and 256 respectively.
+ // Since it's a recommendation to have a minimum of 10 clusters, 1000(100 * 10)
+ // was chosen as the lower threshold.
+ nlist = nvecs / 100
+ }
+ return nlist
+}
+
+// determineFloat32IndexToUse returns a description string for the float32
+// index and quantizer type, and an index type constant.
+func determineFloat32IndexToUse(nvecs, nlist int, optimizationType string) string {
+ if nvecs < ivfThreshold {
+ return "Flat"
+ }
+ switch optimizationType {
+ case index.IndexBIVFWithBackingFlat:
+ return "Flat"
+ case index.IndexBIVFWithBackingSQ8:
+ return "SQ8"
+ case index.IndexOptimizedForMemoryEfficient:
+ return fmt.Sprintf("IVF%d,SQ4", nlist)
+ case index.IndexIVFRaBitQ:
+ return fmt.Sprintf("IVF%d,RaBitQ", nlist)
+ default:
+ switch {
+ case nvecs >= ivfSq8Threshold:
+ return fmt.Sprintf("IVF%d,SQ8", nlist)
+ default:
+ return fmt.Sprintf("IVF%d,Flat", nlist)
+ }
+ }
+}
+
+func (vo *vectorIndexOpaque) writeVectorIndexes(w *FileWriter) error {
+ // for every fieldID, contents to store over here are:
+ // 1. the serialized representation of the dense vector index.
+ // 2. its constituent metadata like:
+ // a. number of vectors
+ // b. dimension of vectors
+ // c. distance metric
+ // d. index optimization type
+ // e. vectorID -> docID mapping
+ tempBuf := vo.grabBuf(binary.MaxVarintLen64)
+ for fieldID, content := range vo.fieldVectorIndex {
+ // number of vectors to be indexed for this field
+ nvecs := len(content.vecDocIDs)
+ // Set the faiss metric type (default is Euclidean Distance or l2_norm)
+ metric := faiss.MetricL2
+ if content.metric == index.InnerProduct || content.metric == index.CosineSimilarity {
+ // use the same FAISS metric for inner product and cosine similarity
+ metric = faiss.MetricInnerProduct
+ }
+
+ // create a vector set wrapping the vector data
+ vecSet, err := newVectorSet(content.dimension, content.vectors)
+ if err != nil {
+ return err
+ }
+
+ // record the fieldStart value for this section.
+ fieldStart := w.Count()
+ // writing out two offset values to indicate that the current field's
+ // vector section doesn't have valid doc value content within it.
+ n := binary.PutUvarint(tempBuf, fieldNotUninverted)
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ n = binary.PutUvarint(tempBuf, fieldNotUninverted)
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ // write the index optimization type
+ n = binary.PutUvarint(tempBuf, uint64(index.SupportedVectorIndexOptimizations[content.optimizedFor]))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ // write the number of vectors
+ n = binary.PutUvarint(tempBuf, uint64(nvecs))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+
+ buf := make([]byte, binary.MaxVarintLen64*len(content.vecDocIDs))
+ bufPos := 0
+ for _, docID := range content.vecDocIDs {
+ n = binary.PutUvarint(buf[bufPos:], uint64(docID))
+ bufPos += n
+ }
+ buf = w.process(buf[:bufPos])
+
+ // write the size of the vector to docID map
+ n = binary.PutUvarint(tempBuf, uint64(len(buf)))
+ _, err = w.Write(tempBuf[:n])
+ if err != nil {
+ return err
+ }
+ // write the vecID -> docID mapping
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ // determine the type of vector index to be created based on the index optimization
+ // and create the faiss index for the vectors associated with this field and
+ // write out the index into the segment writer.
+ indexType := determineIndexTypeFromOptimization(content.optimizedFor)
+ config := newFaissIndexConfig(indexType, content.optimizedFor, content.dimension, metric, nvecs, determineCentroids(nvecs), false)
+ err = vo.writeFaissIndex(vecSet, config, w)
+ if err != nil {
+ return err
+ }
+
+ // accounts for whatever data has been written out to the writer.
+ vo.incrementBytesWritten(uint64(w.Count() - fieldStart))
+ vo.fieldAddrs[fieldID] = fieldStart
+ }
+ return nil
+}
+
+func (vo *vectorIndexOpaque) process(field index.VectorField, fieldName string, fieldID uint16, docNum uint32) {
+ if fieldID == math.MaxUint16 {
+ // doc processing checkpoint - no action needed
+ return
+ }
+ vec := field.Vector()
+ dim := field.Dims()
+ metric := field.Similarity()
+ indexOptimizedFor := field.IndexOptimizedFor()
+ // caller is supposed to make sure len(vec) is a multiple of dim.
+ // Not double checking it here to avoid the overhead.
+ // This accounts for multi-vector fields, where a field can have
+ // multiple vectors associated with it. In this case we process all
+ // vectors associated with the field as separate vectors.
+ numVectors := len(vec) / dim
+ for i := 0; i < numVectors; i++ {
+ vector := vec[i*dim : (i+1)*dim]
+ // check if we have content for this fieldID already
+ content, ok := vo.fieldVectorIndex[fieldID]
+ if !ok {
+ // create an entry for this fieldID as this is the first time
+ // we are seeing this field
+ content = &vectorIndexContent{
+ dimension: dim,
+ metric: metric,
+ optimizedFor: indexOptimizedFor,
+ vectors: make([]float32, 0, dim*numVectors),
+ vecDocIDs: make([]uint32, 0, numVectors),
+ useGPU: vo.fieldsOptions[fieldName].UseGPU(),
+ }
+ vo.fieldVectorIndex[fieldID] = content
+ }
+ // track the vector data and docIDs
+ content.vectors = append(content.vectors, vector...)
+ content.vecDocIDs = append(content.vecDocIDs, docNum)
+ }
+}
+
+func (v *faissVectorIndexSection) getVectorIndexOpaque(opaque map[int]resetable) *vectorIndexOpaque {
+ if _, ok := opaque[SectionFaissVectorIndex]; !ok {
+ opaque[SectionFaissVectorIndex] = v.InitOpaque(nil)
+ }
+ return opaque[SectionFaissVectorIndex].(*vectorIndexOpaque)
+}
+
+func (v *faissVectorIndexSection) InitOpaque(args map[string]interface{}) resetable {
+ rv := &vectorIndexOpaque{
+ fieldAddrs: make(map[uint16]int),
+ fieldVectorIndex: make(map[uint16]*vectorIndexContent),
+ }
+ for k, v := range args {
+ rv.Set(k, v)
+ }
+
+ return rv
+}
+
+// vectorIndexContent contains the information required to create a vector index for a vector field.
+type vectorIndexContent struct {
+ // vectors stores flattened vectors in a row-major order
+ vectors []float32
+ // vecDocIDs corresponding to each vector
+ vecDocIDs []uint32
+ // dimension is the dimension of all vectors
+ dimension int
+ // metric is the distance metric to be used
+ metric string
+ // optimizedFor is the optimization type for the index
+ optimizedFor string
+ // useGPU indicates whether the index should be created on the GPU
+ useGPU bool
+}
+
+// vectorIndexOpaque holds the internal state for vector index processing.
+type vectorIndexOpaque struct {
+ // external config values passed in, which controls the behavior of vector index creation and merging
+ config map[string]interface{}
+ // number of bytes written out for the vector index section, used for metrics and tracking
+ bytesWritten uint64
+ // fieldAddrs maps fieldID to the address of its vector section
+ fieldAddrs map[uint16]int
+ // fieldVectorIndex maps fieldID to its vector index content
+ fieldVectorIndex map[uint16]*vectorIndexContent
+ // fieldsOptions contains field indexing options
+ fieldsOptions map[string]index.FieldIndexingOptions
+ // tmp0 is a reusable buffer
+ tmp0 []byte
+}
+
+func (vo *vectorIndexOpaque) incrementBytesWritten(val uint64) {
+ atomic.AddUint64(&vo.bytesWritten, val)
+}
+
+func (vo *vectorIndexOpaque) BytesWritten() uint64 {
+ return atomic.LoadUint64(&vo.bytesWritten)
+}
+
+func (vo *vectorIndexOpaque) BytesRead() uint64 {
+ return 0
+}
+
+func (vo *vectorIndexOpaque) ResetBytesRead(uint64) {
+}
+
+// Reset clears all state in the vectorIndexOpaque for reuse.
+func (vo *vectorIndexOpaque) Reset() error {
+ clear(vo.fieldAddrs)
+ clear(vo.fieldVectorIndex)
+ vo.tmp0 = vo.tmp0[:0]
+ vo.fieldsOptions = nil
+ vo.config = nil
+ atomic.StoreUint64(&vo.bytesWritten, 0)
+ return nil
+}
+
+func (v *vectorIndexOpaque) Set(key string, val interface{}) {
+ switch key {
+ case "fieldsOptions":
+ v.fieldsOptions = val.(map[string]index.FieldIndexingOptions)
+ case "config":
+ v.config = val.(map[string]interface{})
+ }
+}
+
+// ---------------------------------
+// Faiss Index Factory
+// ---------------------------------
+type faissIndexConfig struct {
+ indexType faissIndexType
+ dimension int
+ metricType int
+ numVecs int
+ optimizationType string
+ nlist int
+ useGPU bool
+}
+
+func newFaissIndexConfig(idxType faissIndexType, optimizationType string, dimension, metricType, numVecs, nlist int, useGPU bool) *faissIndexConfig {
+ return &faissIndexConfig{
+ indexType: idxType,
+ dimension: dimension,
+ metricType: metricType,
+ numVecs: numVecs,
+ nlist: nlist,
+ optimizationType: optimizationType,
+ useGPU: useGPU,
+ }
+}
+
+// Factory function to create a faissIndex for the given index config.
+func faissIndexFactory(cfg *faissIndexConfig) (faissIndex, error) {
+ switch cfg.indexType {
+ case faissFP32Index:
+ description := determineFloat32IndexToUse(cfg.numVecs, cfg.nlist, cfg.optimizationType)
+ idx, err := faiss.IndexFactory(cfg.dimension, description, cfg.metricType)
+ if err != nil {
+ return nil, err
+ }
+ // we restrict GPU to IVF indexes only; flat and SQ indexes do not get a noticeable speedup
+ // when run on GPU, and the GPU overhead can actually make them slower than CPU.
+ if cfg.useGPU && idx.IsIVFIndex() {
+ return newFaissGPUFloat32Index(idx)
+ }
+ return newFaissFloat32Index(idx)
+ case faissBIVFIndex:
+ description := determineBinaryIndexToUse(cfg.numVecs, cfg.nlist)
+ binaryIdx, err := faiss.BinaryIndexFactory(cfg.dimension, description)
+ if err != nil {
+ return nil, err
+ }
+
+ description = determineFloat32IndexToUse(cfg.numVecs, cfg.nlist, cfg.optimizationType)
+ backingIdx, err := faiss.IndexFactory(cfg.dimension, description, cfg.metricType)
+ if err != nil {
+ return nil, err
+ }
+ return newFaissBinaryIndex(binaryIdx, backingIdx)
+ default:
+ return nil, errNotSupported
+ }
+}
+
+// canFastMerge determines whether we can use the fast merge capabilities of faiss based on
+// - the presence of a trained index
+// - the optimization type of the index.
+// - the total number of vectors being merged.
+func canFastMerge(trainedIndex faissIndexIVF, opt string, totalVecs int) bool {
+ // if the trained index isn't IVF or not available, fallback to naive merge
+ if trainedIndex == nil {
+ return false
+ }
+
+ var minVecsForFastMerge int
+ switch opt {
+ case index.IndexBIVFWithBackingFlat, index.IndexBIVFWithBackingSQ8:
+ fallthrough
+ case index.IndexOptimizedForMemoryEfficient:
+ fallthrough
+ case index.IndexIVFRaBitQ:
+ minVecsForFastMerge = ivfThreshold
+ default:
+ minVecsForFastMerge = ivfSq8Threshold
+ }
+ return trainedIndex.ntotal() > int64(minVecsForFastMerge) && totalVecs > minVecsForFastMerge
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/section_inverted_text_index.go b/vendor/github.com/blevesearch/zapx/v17/section_inverted_text_index.go
new file mode 100644
index 0000000000..a2a82de861
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/section_inverted_text_index.go
@@ -0,0 +1,1121 @@
+// Copyright (c) 2023 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "math"
+ "sort"
+ "sync/atomic"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ seg "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/blevesearch/vellum"
+)
+
+func init() {
+ registerSegmentSection(SectionInvertedTextIndex, &invertedTextIndexSection{})
+}
+
+type invertedTextIndexSection struct {
+}
+
+// This function checks whether the inverted text index section should avoid processing
+// a particular field, preventing unnecessary work if another section will handle it.
+//
+// NOTE: The exclusion check is applicable only to the InvertedTextIndexSection
+// because it serves as a catch-all section. This section processes every field
+// unless explicitly excluded, similar to a "default" case in a switch statement.
+// Other sections, such as VectorSection and SynonymSection, rely on inclusion
+// checks to process only specific field types (e.g., index.VectorField or
+// index.SynonymField). Any new section added in the future must define its
+// special field type and inclusion logic explicitly.
+var isFieldExcludedFromInvertedTextIndexSection = func(field index.Field) bool {
+ for _, excludeField := range invertedTextIndexSectionExclusionChecks {
+ if excludeField(field) {
+ // atleast one section has agreed to exclude this field
+ // from inverted text index section processing and has
+ // agreed to process it independently
+ return true
+ }
+ }
+ // no section has excluded this field from inverted index processing
+ // so it should be processed by the inverted index section
+ return false
+}
+
+// List of checks to determine if a field is excluded from the inverted text index section
+var invertedTextIndexSectionExclusionChecks = make([]func(field index.Field) bool, 0)
+
+func (i *invertedTextIndexSection) Process(opaque map[int]resetable, docNum uint32, field index.Field, fieldID uint16) {
+ if !isFieldExcludedFromInvertedTextIndexSection(field) {
+ io := i.getInvertedIndexOpaque(opaque)
+ io.process(field, fieldID, docNum)
+ }
+}
+
+func (i *invertedTextIndexSection) Persist(opaque map[int]resetable, w *FileWriter) error {
+ io := i.getInvertedIndexOpaque(opaque)
+ return io.writeDicts(w)
+}
+
+func (i *invertedTextIndexSection) AddrForField(opaque map[int]resetable, fieldID int) int {
+ io := i.getInvertedIndexOpaque(opaque)
+ return io.fieldAddrs[fieldID]
+}
+
+func mergeAndPersistInvertedSection(segments []*SegmentBase, dropsIn []*roaring.Bitmap,
+ fieldsInv []string, fieldsMap map[string]uint16, fieldsOptions map[string]index.FieldIndexingOptions,
+ fieldsSame bool, newDocNumsIn [][]uint64, newSegDocCount uint64, chunkMode uint32, w *FileWriter,
+ closeCh chan struct{}) (map[int]int, error) {
+ var bufMaxVarintLen64 []byte = make([]byte, binary.MaxVarintLen64)
+ var bufLoc []uint64
+
+ var postings *PostingsList
+ var postItr *PostingsIterator
+
+ fieldAddrs := make(map[int]int)
+ dictOffsets := make([]uint64, len(fieldsInv))
+ fieldDvLocsStart := make([]uint64, len(fieldsInv))
+ fieldDvLocsEnd := make([]uint64, len(fieldsInv))
+
+ // copying data directly is safe only if there are no
+ // file callbacks that might modify the data in all
+ // of the involved segments and the current writer
+ copyFlag := true
+ for _, segment := range segments {
+ if segment.fileReader.id != "" {
+ copyFlag = false
+ break
+ }
+ }
+ if w.id != "" {
+ copyFlag = false
+ }
+
+ // these int coders are initialized with chunk size 1024
+ // however this will be reset to the correct chunk size
+ // while processing each individual field-term section
+ tfEncoder := newChunkedIntCoder(1024, newSegDocCount-1)
+ locEncoder := newChunkedIntCoder(1024, newSegDocCount-1)
+
+ var vellumBuf bytes.Buffer
+ newVellum, err := vellum.New(&vellumBuf, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ newRoaring := roaring.NewBitmap()
+ newDocNums := make([][]uint64, 0, len(segments))
+ drops := make([]*roaring.Bitmap, 0, len(segments))
+ dicts := make([]*Dictionary, 0, len(segments))
+ itrs := make([]vellum.Iterator, 0, len(segments))
+ segmentsInFocus := make([]*SegmentBase, 0, len(segments))
+ // for each field
+ for fieldID, fieldName := range fieldsInv {
+ // collect FST iterators from all active segments for this field
+ newDocNums = newDocNums[:0]
+ drops = drops[:0]
+ dicts = dicts[:0]
+ itrs = itrs[:0]
+ segmentsInFocus = segmentsInFocus[:0]
+ for segmentI, segment := range segments {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return nil, seg.ErrClosed
+ }
+ // early exit if the field's index option is false
+ if !fieldsOptions[fieldName].IsIndexed() {
+ continue
+ }
+
+ dict, err2 := segment.dictionary(fieldName)
+ if err2 != nil {
+ return nil, err2
+ }
+ if dict != nil && dict.fst != nil {
+ itr, err2 := dict.fst.Iterator(nil, nil)
+ if err2 != nil && err2 != vellum.ErrIteratorDone {
+ return nil, err2
+ }
+ if itr != nil {
+ newDocNums = append(newDocNums, newDocNumsIn[segmentI])
+ if dropsIn[segmentI] != nil && !dropsIn[segmentI].IsEmpty() {
+ drops = append(drops, dropsIn[segmentI])
+ } else {
+ drops = append(drops, nil)
+ }
+ dicts = append(dicts, dict)
+ itrs = append(itrs, itr)
+ segmentsInFocus = append(segmentsInFocus, segment)
+ }
+ }
+ }
+
+ var prevTerm []byte
+
+ newRoaring.Clear()
+
+ var lastDocNum, lastFreq, lastNorm uint64
+
+ // determines whether to use "1-hit" encoding optimization
+ // when a term appears in only 1 doc, with no loc info,
+ // has freq of 1, and the docNum fits into 31-bits
+ use1HitEncoding := func(termCardinality uint64) (bool, uint64, uint64) {
+ if termCardinality == uint64(1) && locEncoder.FinalSize() <= 0 {
+ docNum := uint64(newRoaring.Minimum())
+ if under32Bits(docNum) && docNum == lastDocNum && lastFreq == 1 {
+ return true, docNum, lastNorm
+ }
+ }
+ return false, 0, 0
+ }
+
+ finishTerm := func(term []byte) error {
+ tfEncoder.Close()
+ locEncoder.Close()
+
+ postingsOffset, err := writePostings(newRoaring,
+ tfEncoder, locEncoder, use1HitEncoding, w, bufMaxVarintLen64)
+ if err != nil {
+ return err
+ }
+
+ if postingsOffset > 0 {
+ err = newVellum.Insert(term, postingsOffset)
+ if err != nil {
+ return err
+ }
+ }
+
+ newRoaring.Clear()
+
+ tfEncoder.Reset()
+ locEncoder.Reset()
+
+ lastDocNum = 0
+ lastFreq = 0
+ lastNorm = 0
+
+ return nil
+ }
+
+ enumerator, err := newEnumerator(itrs)
+
+ for err == nil {
+ term, itrI, postingsOffset := enumerator.Current()
+
+ if !bytes.Equal(prevTerm, term) {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return nil, seg.ErrClosed
+ }
+
+ // if the term changed, write out the info collected
+ // for the previous term
+ err = finishTerm(prevTerm)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if !bytes.Equal(prevTerm, term) || prevTerm == nil {
+ // compute cardinality of field-term in new seg
+ var newCard uint64
+ lowItrIdxs, lowItrVals := enumerator.GetLowIdxsAndValues()
+ for i, idx := range lowItrIdxs {
+ pl, err := dicts[idx].postingsListFromOffset(lowItrVals[i], drops[idx], nil)
+ if err != nil {
+ return nil, err
+ }
+ newCard += pl.Count()
+ }
+ // compute correct chunk size with this
+ chunkSize, err := getChunkSize(chunkMode, newCard, newSegDocCount)
+ if err != nil {
+ return nil, err
+ }
+ // update encoders chunk
+ tfEncoder.SetChunkSize(chunkSize, newSegDocCount-1)
+ locEncoder.SetChunkSize(chunkSize, newSegDocCount-1)
+ }
+
+ postings, err = dicts[itrI].postingsListFromOffset(
+ postingsOffset, drops[itrI], postings)
+ if err != nil {
+ return nil, err
+ }
+
+ postItr = postings.iterator(true, true, true, postItr)
+
+ // can only safely copy data if all segments have same fields and all have an empty
+ // writer id (i.e. no callbacks)
+ if fieldsSame && copyFlag {
+ // can optimize by copying freq/norm/loc bytes directly
+ lastDocNum, lastFreq, lastNorm, err = mergeTermFreqNormLocsByCopying(
+ term, postItr, newDocNums[itrI], newRoaring,
+ tfEncoder, locEncoder)
+ } else {
+ lastDocNum, lastFreq, lastNorm, bufLoc, err = mergeTermFreqNormLocs(
+ fieldsMap, term, postItr, newDocNums[itrI], newRoaring,
+ tfEncoder, locEncoder, bufLoc)
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ prevTerm = prevTerm[:0] // copy to prevTerm in case Next() reuses term mem
+ prevTerm = append(prevTerm, term...)
+
+ err = enumerator.Next()
+ }
+ if err != vellum.ErrIteratorDone {
+ return nil, err
+ }
+ // close the enumerator to free the underlying iterators
+ err = enumerator.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ err = finishTerm(prevTerm)
+ if err != nil {
+ return nil, err
+ }
+
+ dictOffset := uint64(w.Count())
+
+ err = newVellum.Close()
+ if err != nil {
+ return nil, err
+ }
+ vellumData := w.process(vellumBuf.Bytes())
+
+ // write out the length of the vellum data
+ n := binary.PutUvarint(bufMaxVarintLen64, uint64(len(vellumData)))
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ // write this vellum to disk
+ _, err = w.Write(vellumData)
+ if err != nil {
+ return nil, err
+ }
+
+ dictOffsets[fieldID] = dictOffset
+
+ fieldDvLocsStart[fieldID] = uint64(w.Count())
+
+ // update the field doc values
+ // NOTE: doc values continue to use legacy chunk mode
+ chunkSize, err := getChunkSize(LegacyChunkMode, 0, 0)
+ if err != nil {
+ return nil, err
+ }
+ if fieldsOptions[fieldName].SkipDVChunking() {
+ chunkSize = 1
+ }
+ fdvEncoder := newChunkedContentCoder(chunkSize, newSegDocCount-1, w, true, fieldsOptions[fieldName].SkipDVCompression())
+
+ fdvReadersAvailable := false
+ var dvIterClone *docValueReader
+ var dvIter *docValueReader
+ for segmentI, segment := range segmentsInFocus {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return nil, seg.ErrClosed
+ }
+ // early exit if docvalues are not wanted for this field
+ if !fieldsOptions[fieldName].IncludeDocValues() {
+ continue
+ }
+ fieldIDPlus1 := uint16(segment.fieldsMap[fieldName])
+ dvIter = segment.fieldDvReaders[SectionInvertedTextIndex][fieldIDPlus1-1]
+ if dvIter != nil {
+ fdvReadersAvailable = true
+ dvIterClone = dvIter.cloneInto(dvIterClone)
+ err = dvIterClone.iterateAllDocValues(segment, func(docNum uint64, terms []byte) error {
+ if newDocNums[segmentI][docNum] == docDropped {
+ return nil
+ }
+ err := fdvEncoder.Add(newDocNums[segmentI][docNum], terms)
+ if err != nil {
+ return err
+ }
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+ }
+ }
+
+ if fdvReadersAvailable {
+ err = fdvEncoder.Close()
+ if err != nil {
+ return nil, err
+ }
+
+ // persist the doc value details for this field
+ _, err = fdvEncoder.Write()
+ if err != nil {
+ return nil, err
+ }
+
+ // get the field doc value offset (end)
+ fieldDvLocsEnd[fieldID] = uint64(w.Count())
+ } else {
+ fieldDvLocsStart[fieldID] = fieldNotUninverted
+ fieldDvLocsEnd[fieldID] = fieldNotUninverted
+ }
+
+ fieldStart := w.Count()
+
+ n = binary.PutUvarint(bufMaxVarintLen64, fieldDvLocsStart[fieldID])
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ n = binary.PutUvarint(bufMaxVarintLen64, fieldDvLocsEnd[fieldID])
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ n = binary.PutUvarint(bufMaxVarintLen64, dictOffsets[fieldID])
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, err
+ }
+
+ fieldAddrs[fieldID] = fieldStart
+
+ // reset vellum buffer and vellum builder
+ vellumBuf.Reset()
+ err = newVellum.Reset(&vellumBuf)
+ if err != nil {
+ return nil, err
+ }
+ }
+ return fieldAddrs, nil
+}
+
+func (i *invertedTextIndexSection) Merge(opaque map[int]resetable, segments []*SegmentBase,
+ drops []*roaring.Bitmap, fieldsInv []string, newDocNumsIn [][]uint64,
+ w *FileWriter, closeCh chan struct{}) error {
+ io := i.getInvertedIndexOpaque(opaque)
+ fieldAddrs, err := mergeAndPersistInvertedSection(segments, drops, fieldsInv,
+ io.FieldsMap, io.FieldsOptions, io.fieldsSame, newDocNumsIn, io.numDocs, io.chunkMode, w, closeCh)
+ if err != nil {
+ return err
+ }
+
+ io.fieldAddrs = fieldAddrs
+ return nil
+}
+
+func (i *invertedIndexOpaque) grabBuf(size int) []byte {
+ buf := i.tmp0
+ if cap(buf) < size {
+ buf = make([]byte, size)
+ i.tmp0 = buf
+ }
+ return buf[:size]
+}
+
+func (i *invertedIndexOpaque) incrementBytesWritten(bytes uint64) {
+ i.bytesWritten += bytes
+}
+
+func (i *invertedIndexOpaque) BytesWritten() uint64 {
+ return i.bytesWritten
+}
+
+func (i *invertedIndexOpaque) BytesRead() uint64 {
+ return 0
+}
+
+func (i *invertedIndexOpaque) ResetBytesRead(uint64) {}
+
+func (io *invertedIndexOpaque) writeDicts(w *FileWriter) error {
+ if len(io.results) == 0 {
+ return nil
+ }
+
+ dictOffsets := make([]uint64, len(io.FieldsInv))
+ var err error
+
+ fdvOffsetsStart := make([]uint64, len(io.FieldsInv))
+ fdvOffsetsEnd := make([]uint64, len(io.FieldsInv))
+
+ buf := io.grabBuf(binary.MaxVarintLen64)
+
+ // these int coders are initialized with chunk size 1024
+ // however this will be reset to the correct chunk size
+ // while processing each individual field-term section
+ tfEncoder := newChunkedIntCoder(1024, uint64(len(io.results)-1))
+ locEncoder := newChunkedIntCoder(1024, uint64(len(io.results)-1))
+
+ var docTermMap [][]byte
+
+ if io.builder == nil {
+ io.builder, err = vellum.New(&io.builderBuf, nil)
+ if err != nil {
+ return err
+ }
+ }
+
+ for fieldID, terms := range io.DictKeys {
+ if cap(docTermMap) < len(io.results) {
+ docTermMap = make([][]byte, len(io.results))
+ } else {
+ docTermMap = docTermMap[:len(io.results)]
+ for docNum := range docTermMap { // reset the docTermMap
+ docTermMap[docNum] = docTermMap[docNum][:0]
+ }
+ }
+
+ dict := io.Dicts[fieldID]
+
+ for _, term := range terms { // terms are already sorted
+ pid := dict[term] - 1
+
+ postingsBS := io.Postings[pid]
+
+ freqNorms := io.FreqNorms[pid]
+ freqNormOffset := 0
+
+ locs := io.Locs[pid]
+ locOffset := 0
+
+ var cardinality uint64
+ if postingsBS != nil {
+ cardinality = postingsBS.GetCardinality()
+ }
+ chunkSize, err := getChunkSize(io.chunkMode, cardinality, uint64(len(io.results)))
+ if err != nil {
+ return err
+ }
+ tfEncoder.SetChunkSize(chunkSize, uint64(len(io.results)-1))
+ locEncoder.SetChunkSize(chunkSize, uint64(len(io.results)-1))
+
+ postingsItr := postingsBS.Iterator()
+ for postingsItr.HasNext() {
+ docNum := uint64(postingsItr.Next())
+
+ freqNorm := freqNorms[freqNormOffset]
+
+ // check if freq/norm is enabled
+ if freqNorm.freq > 0 {
+ err = tfEncoder.Add(docNum,
+ encodeFreqHasLocs(freqNorm.freq, freqNorm.numLocs > 0),
+ uint64(math.Float32bits(freqNorm.norm)))
+ } else {
+ // if disabled, then skip the norm part
+ err = tfEncoder.Add(docNum,
+ encodeFreqHasLocs(freqNorm.freq, freqNorm.numLocs > 0))
+ }
+ if err != nil {
+ return err
+ }
+
+ if freqNorm.numLocs > 0 {
+ numBytesLocs := 0
+ for _, loc := range locs[locOffset : locOffset+freqNorm.numLocs] {
+ numBytesLocs += totalUvarintBytes(
+ uint64(loc.fieldID), loc.pos, loc.start, loc.end,
+ uint64(len(loc.arrayposs)), loc.arrayposs)
+ }
+
+ err = locEncoder.Add(docNum, uint64(numBytesLocs))
+ if err != nil {
+ return err
+ }
+ for _, loc := range locs[locOffset : locOffset+freqNorm.numLocs] {
+ err = locEncoder.Add(docNum,
+ uint64(loc.fieldID), loc.pos, loc.start, loc.end,
+ uint64(len(loc.arrayposs)))
+ if err != nil {
+ return err
+ }
+
+ err = locEncoder.Add(docNum, loc.arrayposs...)
+ if err != nil {
+ return err
+ }
+ }
+ locOffset += freqNorm.numLocs
+ }
+
+ freqNormOffset++
+
+ docTermMap[docNum] = append(
+ append(docTermMap[docNum], term...),
+ index.DocValueTermSeparator)
+ }
+
+ tfEncoder.Close()
+ locEncoder.Close()
+ io.incrementBytesWritten(locEncoder.getBytesWritten())
+ io.incrementBytesWritten(tfEncoder.getBytesWritten())
+
+ postingsOffset, err :=
+ writePostings(postingsBS, tfEncoder, locEncoder, nil, w, buf)
+ if err != nil {
+ return err
+ }
+
+ if postingsOffset > uint64(0) {
+ err = io.builder.Insert([]byte(term), postingsOffset)
+ if err != nil {
+ return err
+ }
+ }
+
+ tfEncoder.Reset()
+ locEncoder.Reset()
+ }
+
+ err = io.builder.Close()
+ if err != nil {
+ return err
+ }
+
+ // record where this dictionary starts
+ dictOffsets[fieldID] = uint64(w.Count())
+
+ vellumData := w.process(io.builderBuf.Bytes())
+
+ // write out the length of the vellum data
+ n := binary.PutUvarint(buf, uint64(len(vellumData)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ io.incrementBytesWritten(uint64(len(vellumData)))
+
+ // write this vellum to disk
+ _, err = w.Write(vellumData)
+ if err != nil {
+ return err
+ }
+
+ // reset vellum for reuse
+ io.builderBuf.Reset()
+
+ err = io.builder.Reset(&io.builderBuf)
+ if err != nil {
+ return err
+ }
+
+ // write the field doc values
+ // NOTE: doc values continue to use legacy chunk mode
+ chunkSize, err := getChunkSize(LegacyChunkMode, 0, 0)
+ if err != nil {
+ return err
+ }
+ if io.FieldsOptions[io.FieldsInv[fieldID]].SkipDVChunking() {
+ chunkSize = 1
+ }
+ fdvEncoder := newChunkedContentCoder(chunkSize, uint64(len(io.results)-1), w, false, io.FieldsOptions[io.FieldsInv[fieldID]].SkipDVCompression())
+ if io.IncludeDocValues[fieldID] {
+ for docNum, docTerms := range docTermMap {
+ if fieldTermMap, ok := io.extraDocValues[docNum]; ok {
+ if sTerms, ok := fieldTermMap[uint16(fieldID)]; ok {
+ for _, sTerm := range sTerms {
+ docTerms = append(append(docTerms, sTerm...), index.DocValueTermSeparator)
+ }
+ }
+ }
+ if len(docTerms) > 0 {
+ err = fdvEncoder.Add(uint64(docNum), docTerms)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ err = fdvEncoder.Close()
+ if err != nil {
+ return err
+ }
+
+ io.incrementBytesWritten(fdvEncoder.getBytesWritten())
+
+ fdvOffsetsStart[fieldID] = uint64(w.Count())
+
+ _, err = fdvEncoder.Write()
+ if err != nil {
+ return err
+ }
+
+ fdvOffsetsEnd[fieldID] = uint64(w.Count())
+ fdvEncoder.Reset()
+ } else {
+ fdvOffsetsStart[fieldID] = fieldNotUninverted
+ fdvOffsetsEnd[fieldID] = fieldNotUninverted
+ }
+
+ fieldStart := w.Count()
+
+ n = binary.PutUvarint(buf, fdvOffsetsStart[fieldID])
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ n = binary.PutUvarint(buf, fdvOffsetsEnd[fieldID])
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ n = binary.PutUvarint(buf, dictOffsets[fieldID])
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ io.fieldAddrs[fieldID] = fieldStart
+ }
+
+ return nil
+}
+
+func (io *invertedIndexOpaque) process(field index.Field, fieldID uint16, docNum uint32) {
+ if !io.init && io.results != nil {
+ io.realloc()
+ io.init = true
+ }
+
+ // if the fieldID is MaxUint16, it's mainly indicated that the caller has
+ // finished invoking the process() for every field on that doc.
+ if fieldID == math.MaxUint16 {
+ for fid, tfs := range io.reusableFieldTFs {
+ dict := io.Dicts[fid]
+ norm := math.Float32frombits(uint32(io.reusableFieldLens[fid]))
+
+ for term, tf := range tfs {
+ pid := dict[term] - 1
+ bs := io.Postings[pid]
+ bs.Add(uint32(docNum))
+
+ io.FreqNorms[pid] = append(io.FreqNorms[pid],
+ interimFreqNorm{
+ freq: uint64(tf.Frequency()),
+ norm: norm,
+ numLocs: len(tf.Locations),
+ })
+
+ if len(tf.Locations) > 0 {
+ locs := io.Locs[pid]
+
+ for _, loc := range tf.Locations {
+ var locf = uint16(fid)
+ if loc.Field != "" {
+ locf = uint16(io.getOrDefineField(loc.Field))
+ }
+ var arrayposs []uint64
+ if len(loc.ArrayPositions) > 0 {
+ arrayposs = loc.ArrayPositions
+ }
+ locs = append(locs, interimLoc{
+ fieldID: locf,
+ pos: uint64(loc.Position),
+ start: uint64(loc.Start),
+ end: uint64(loc.End),
+ arrayposs: arrayposs,
+ })
+ }
+
+ io.Locs[pid] = locs
+ }
+ }
+ }
+ for i := 0; i < len(io.FieldsInv); i++ { // clear these for reuse
+ io.reusableFieldLens[i] = 0
+ io.reusableFieldTFs[i] = nil
+ }
+ return
+ }
+
+ io.reusableFieldLens[fieldID] += field.AnalyzedLength()
+ existingFreqs := io.reusableFieldTFs[fieldID]
+ if existingFreqs != nil {
+ existingFreqs.MergeAll(field.Name(), field.AnalyzedTokenFrequencies())
+ } else {
+ io.reusableFieldTFs[fieldID] = field.AnalyzedTokenFrequencies()
+ }
+}
+
+func (i *invertedIndexOpaque) initDictsAndKeysFromFields() {
+ numFields := len(i.FieldsInv)
+
+ // Resize or allocate Dicts
+ if cap(i.Dicts) >= numFields {
+ i.Dicts = i.Dicts[:numFields]
+ } else {
+ i.Dicts = make([]map[string]uint64, numFields)
+ }
+
+ // Resize or allocate DictKeys
+ if cap(i.DictKeys) >= numFields {
+ i.DictKeys = i.DictKeys[:numFields]
+ } else {
+ i.DictKeys = make([][]string, numFields)
+ }
+
+ for idx := 0; idx < numFields; idx++ {
+ // --- Dicts ---
+ if i.Dicts[idx] == nil {
+ i.Dicts[idx] = make(map[string]uint64)
+ } else {
+ clear(i.Dicts[idx])
+ }
+
+ // --- DictKeys ---
+ if i.DictKeys[idx] != nil {
+ i.DictKeys[idx] = i.DictKeys[idx][:0]
+ } else {
+ i.DictKeys[idx] = nil
+ }
+ }
+}
+
+func (i *invertedIndexOpaque) realloc() {
+ var pidNext int
+
+ var totTFs int
+ var totLocs int
+
+ // initialize dicts and dict keys from fieldsMap
+ i.initDictsAndKeysFromFields()
+
+ visitField := func(field index.Field, docNum int) {
+ fieldID := uint16(i.getOrDefineField(field.Name()))
+
+ dict := i.Dicts[fieldID]
+ dictKeys := i.DictKeys[fieldID]
+
+ tfs := field.AnalyzedTokenFrequencies()
+ for term, tf := range tfs {
+ pidPlus1, exists := dict[term]
+ if !exists {
+ pidNext++
+ pidPlus1 = uint64(pidNext)
+
+ dict[term] = pidPlus1
+ dictKeys = append(dictKeys, term)
+
+ i.numTermsPerPostingsList = append(i.numTermsPerPostingsList, 0)
+ i.numLocsPerPostingsList = append(i.numLocsPerPostingsList, 0)
+ }
+
+ pid := pidPlus1 - 1
+
+ i.numTermsPerPostingsList[pid] += 1
+ i.numLocsPerPostingsList[pid] += len(tf.Locations)
+
+ totLocs += len(tf.Locations)
+ }
+
+ totTFs += len(tfs)
+
+ i.DictKeys[fieldID] = dictKeys
+ if field.Options().IncludeDocValues() {
+ i.IncludeDocValues[fieldID] = true
+ }
+
+ if f, ok := field.(index.GeoShapeField); ok {
+ if _, exists := i.extraDocValues[docNum]; !exists {
+ i.extraDocValues[docNum] = make(map[uint16][][]byte)
+ }
+ i.extraDocValues[docNum][fieldID] = append(i.extraDocValues[docNum][fieldID], f.EncodedShape())
+ }
+ }
+
+ if cap(i.IncludeDocValues) >= len(i.FieldsInv) {
+ i.IncludeDocValues = i.IncludeDocValues[:len(i.FieldsInv)]
+ } else {
+ i.IncludeDocValues = make([]bool, len(i.FieldsInv))
+ }
+
+ if i.extraDocValues == nil {
+ i.extraDocValues = map[int]map[uint16][][]byte{}
+ }
+
+ for docNum, result := range i.results {
+ // walk each composite field
+ result.VisitComposite(func(field index.CompositeField) {
+ visitField(field, docNum)
+ })
+
+ // walk each field
+ result.VisitFields(func(field index.Field) {
+ visitField(field, docNum)
+ })
+ }
+
+ numPostingsLists := pidNext
+
+ if cap(i.Postings) >= numPostingsLists {
+ i.Postings = i.Postings[:numPostingsLists]
+ } else {
+ postings := make([]*roaring.Bitmap, numPostingsLists)
+ copy(postings, i.Postings[:cap(i.Postings)])
+ for i := 0; i < numPostingsLists; i++ {
+ if postings[i] == nil {
+ postings[i] = roaring.New()
+ }
+ }
+ i.Postings = postings
+ }
+
+ if cap(i.FreqNorms) >= numPostingsLists {
+ i.FreqNorms = i.FreqNorms[:numPostingsLists]
+ } else {
+ i.FreqNorms = make([][]interimFreqNorm, numPostingsLists)
+ }
+
+ if cap(i.freqNormsBacking) >= totTFs {
+ i.freqNormsBacking = i.freqNormsBacking[:totTFs]
+ } else {
+ i.freqNormsBacking = make([]interimFreqNorm, totTFs)
+ }
+
+ freqNormsBacking := i.freqNormsBacking
+ for pid, numTerms := range i.numTermsPerPostingsList {
+ i.FreqNorms[pid] = freqNormsBacking[0:0]
+ freqNormsBacking = freqNormsBacking[numTerms:]
+ }
+
+ if cap(i.Locs) >= numPostingsLists {
+ i.Locs = i.Locs[:numPostingsLists]
+ } else {
+ i.Locs = make([][]interimLoc, numPostingsLists)
+ }
+
+ if cap(i.locsBacking) >= totLocs {
+ i.locsBacking = i.locsBacking[:totLocs]
+ } else {
+ i.locsBacking = make([]interimLoc, totLocs)
+ }
+
+ locsBacking := i.locsBacking
+ for pid, numLocs := range i.numLocsPerPostingsList {
+ i.Locs[pid] = locsBacking[0:0]
+ locsBacking = locsBacking[numLocs:]
+ }
+
+ for _, dict := range i.DictKeys {
+ sort.Strings(dict)
+ }
+
+ if cap(i.reusableFieldTFs) >= len(i.FieldsInv) {
+ i.reusableFieldTFs = i.reusableFieldTFs[:len(i.FieldsInv)]
+ } else {
+ i.reusableFieldTFs = make([]index.TokenFrequencies, len(i.FieldsInv))
+ }
+
+ if cap(i.reusableFieldLens) >= len(i.FieldsInv) {
+ i.reusableFieldLens = i.reusableFieldLens[:len(i.FieldsInv)]
+ } else {
+ i.reusableFieldLens = make([]int, len(i.FieldsInv))
+ }
+}
+
+func (i *invertedTextIndexSection) getInvertedIndexOpaque(opaque map[int]resetable) *invertedIndexOpaque {
+ if _, ok := opaque[SectionInvertedTextIndex]; !ok {
+ opaque[SectionInvertedTextIndex] = i.InitOpaque(nil)
+ }
+ return opaque[SectionInvertedTextIndex].(*invertedIndexOpaque)
+}
+
+func (i *invertedIndexOpaque) getOrDefineField(fieldName string) int {
+ fieldIDPlus1, exists := i.FieldsMap[fieldName]
+ if !exists {
+ fieldIDPlus1 = uint16(len(i.FieldsInv) + 1)
+ i.FieldsMap[fieldName] = fieldIDPlus1
+ i.FieldsInv = append(i.FieldsInv, fieldName)
+
+ i.Dicts = append(i.Dicts, make(map[string]uint64))
+
+ n := len(i.DictKeys)
+ if n < cap(i.DictKeys) {
+ i.DictKeys = i.DictKeys[:n+1]
+ i.DictKeys[n] = i.DictKeys[n][:0]
+ } else {
+ i.DictKeys = append(i.DictKeys, []string(nil))
+ }
+ }
+
+ return int(fieldIDPlus1 - 1)
+}
+
+func (i *invertedTextIndexSection) InitOpaque(args map[string]interface{}) resetable {
+ rv := &invertedIndexOpaque{
+ fieldAddrs: map[int]int{},
+ }
+ for k, v := range args {
+ rv.Set(k, v)
+ }
+
+ return rv
+}
+
+type invertedIndexOpaque struct {
+ bytesWritten uint64 // atomic access to this variable, moved to top to correct alignment issues on ARM, 386 and 32-bit MIPS.
+
+ results []index.Document
+
+ chunkMode uint32
+
+ // indicates whethere the following structs are initialized
+ init bool
+
+ // FieldsMap adds 1 to field id to avoid zero value issues
+ // name -> field id + 1
+ FieldsMap map[string]uint16
+
+ // FieldsInv is the inverse of FieldsMap
+ // field id -> name
+ FieldsInv []string
+
+ // Field indexing options
+ // field name -> options
+ FieldsOptions map[string]index.FieldIndexingOptions
+
+ // Term dictionaries for each field
+ // field id -> term -> postings list id + 1
+ Dicts []map[string]uint64
+
+ // Terms for each field, where terms are sorted ascending
+ // field id -> []term
+ DictKeys [][]string
+
+ // Fields whose IncludeDocValues is true
+ // field id -> bool
+ IncludeDocValues []bool
+
+ // postings id -> bitmap of docNums
+ Postings []*roaring.Bitmap
+
+ // postings id -> freq/norm's, one for each docNum in postings
+ FreqNorms [][]interimFreqNorm
+ freqNormsBacking []interimFreqNorm
+
+ // postings id -> locs, one for each freq
+ Locs [][]interimLoc
+ locsBacking []interimLoc
+
+ numTermsPerPostingsList []int // key is postings list id
+ numLocsPerPostingsList []int // key is postings list id
+
+ // store terms that are unnecessary for the term dictionaries but needed in doc values
+ // eg - encoded geoshapes
+ // docNum -> fieldID -> terms
+ extraDocValues map[int]map[uint16][][]byte
+
+ builder *vellum.Builder
+ builderBuf bytes.Buffer
+
+ // reusable stuff for processing fields etc.
+ reusableFieldLens []int
+ reusableFieldTFs []index.TokenFrequencies
+
+ tmp0 []byte
+
+ fieldAddrs map[int]int
+
+ fieldsSame bool
+ numDocs uint64
+}
+
+func (io *invertedIndexOpaque) Reset() (err error) {
+ // cleanup stuff over here
+ io.results = nil
+ io.init = false
+ io.chunkMode = 0
+ io.FieldsMap = nil
+ io.FieldsOptions = nil
+ io.FieldsInv = nil
+ for i := range io.Dicts {
+ io.Dicts[i] = nil
+ }
+ io.Dicts = io.Dicts[:0]
+ for i := range io.DictKeys {
+ io.DictKeys[i] = io.DictKeys[i][:0]
+ }
+ io.DictKeys = io.DictKeys[:0]
+ for i := range io.IncludeDocValues {
+ io.IncludeDocValues[i] = false
+ }
+ io.IncludeDocValues = io.IncludeDocValues[:0]
+ for _, idn := range io.Postings {
+ idn.Clear()
+ }
+ io.Postings = io.Postings[:0]
+ io.FreqNorms = io.FreqNorms[:0]
+ for i := range io.freqNormsBacking {
+ io.freqNormsBacking[i] = interimFreqNorm{}
+ }
+ io.freqNormsBacking = io.freqNormsBacking[:0]
+ io.Locs = io.Locs[:0]
+ for i := range io.locsBacking {
+ io.locsBacking[i] = interimLoc{}
+ }
+ io.locsBacking = io.locsBacking[:0]
+ io.numTermsPerPostingsList = io.numTermsPerPostingsList[:0]
+ io.numLocsPerPostingsList = io.numLocsPerPostingsList[:0]
+ io.builderBuf.Reset()
+ if io.builder != nil {
+ err = io.builder.Reset(&io.builderBuf)
+ }
+
+ io.reusableFieldLens = io.reusableFieldLens[:0]
+ io.reusableFieldTFs = io.reusableFieldTFs[:0]
+
+ io.tmp0 = io.tmp0[:0]
+ io.extraDocValues = nil
+ atomic.StoreUint64(&io.bytesWritten, 0)
+ io.fieldsSame = false
+ io.numDocs = 0
+
+ clear(io.fieldAddrs)
+
+ return err
+}
+func (i *invertedIndexOpaque) Set(key string, val interface{}) {
+ switch key {
+ case "results":
+ i.results = val.([]index.Document)
+ case "chunkMode":
+ i.chunkMode = val.(uint32)
+ case "fieldsSame":
+ i.fieldsSame = val.(bool)
+ case "fieldsMap":
+ i.FieldsMap = val.(map[string]uint16)
+ case "fieldsOptions":
+ i.FieldsOptions = val.(map[string]index.FieldIndexingOptions)
+ case "fieldsInv":
+ i.FieldsInv = val.([]string)
+ case "numDocs":
+ i.numDocs = val.(uint64)
+ }
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/section_synonym_index.go b/vendor/github.com/blevesearch/zapx/v17/section_synonym_index.go
new file mode 100644
index 0000000000..e15b2e17a1
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/section_synonym_index.go
@@ -0,0 +1,795 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "math"
+ "sort"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ "github.com/RoaringBitmap/roaring/v2/roaring64"
+ index "github.com/blevesearch/bleve_index_api"
+ seg "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/blevesearch/vellum"
+)
+
+func init() {
+ registerSegmentSection(SectionSynonymIndex, &synonymIndexSection{})
+ invertedTextIndexSectionExclusionChecks = append(invertedTextIndexSectionExclusionChecks, func(field index.Field) bool {
+ _, ok := field.(index.SynonymField)
+ return ok
+ })
+}
+
+// -----------------------------------------------------------------------------
+
+type synonymIndexOpaque struct {
+ results []index.Document
+
+ // indicates whether the following structs are initialized
+ init bool
+
+ // FieldsMap maps field name to field id and must be set in
+ // the index opaque using the key "fieldsMap"
+ // used for ensuring accurate mapping between fieldID and
+ // thesaurusID
+ // name -> field id + 1
+ FieldsMap map[string]uint16
+
+ // ThesaurusMap adds 1 to thesaurus id to avoid zero value issues
+ // name -> thesaurus id + 1
+ ThesaurusMap map[string]uint16
+
+ // ThesaurusMapInv is the inverse of ThesaurusMap
+ // thesaurus id + 1 -> name
+ ThesaurusInv []string
+
+ // Thesaurus for each thesaurus ID
+ // thesaurus id -> LHS term -> synonym postings list id + 1
+ Thesauri []map[string]uint64
+
+ // LHS Terms for each thesaurus ID, where terms are sorted ascending
+ // thesaurus id -> []term
+ ThesaurusKeys [][]string
+
+ // FieldIDtoThesaurusID maps the field id to the thesaurus id
+ // field id -> thesaurus id
+ FieldIDtoThesaurusID map[uint16]int
+
+ // SynonymIDtoTerm maps synonym id to term for each thesaurus
+ // thesaurus id -> synonym id -> term
+ SynonymTermToID []map[string]uint32
+
+ // SynonymTermToID maps term to synonym id for each thesaurus
+ // thesaurus id -> term -> synonym id
+ // this is the inverse of SynonymIDtoTerm for each thesaurus
+ SynonymIDtoTerm []map[uint32]string
+
+ // synonym postings list -> synonym bitmap
+ Synonyms []*roaring64.Bitmap
+
+ // A reusable vellum FST builder that will be stored in the synonym opaque
+ // and reused across multiple document batches during the persist phase
+ // of the synonym index section, the FST builder is used to build the
+ // FST for each thesaurus, which maps terms to their corresponding synonym bitmaps.
+ builder *vellum.Builder
+
+ // A reusable buffer for the vellum FST builder. It streams data written
+ // into the builder into a byte slice. The final byte slice represents
+ // the serialized vellum FST, which will be written to disk
+ builderBuf bytes.Buffer
+
+ // A reusable buffer for temporary use within the synonym index opaque
+ tmp0 []byte
+
+ // A map linking thesaurus IDs to their corresponding thesaurus' file offsets
+ thesaurusAddrs map[int]int
+}
+
+// Set the fieldsMap and results in the synonym index opaque before the section processes a synonym field.
+func (so *synonymIndexOpaque) Set(key string, value interface{}) {
+ switch key {
+ case "results":
+ so.results = value.([]index.Document)
+ case "fieldsMap":
+ so.FieldsMap = value.(map[string]uint16)
+ }
+}
+
+// Reset the synonym index opaque after a batch of documents have been processed into a segment.
+func (so *synonymIndexOpaque) Reset() (err error) {
+ // cleanup stuff over here
+ so.results = nil
+ so.init = false
+ so.FieldsMap = nil
+ clear(so.ThesaurusMap)
+ so.ThesaurusInv = so.ThesaurusInv[:0]
+ for i := range so.Thesauri {
+ so.Thesauri[i] = nil
+ }
+ so.Thesauri = so.Thesauri[:0]
+ for i := range so.ThesaurusKeys {
+ so.ThesaurusKeys[i] = so.ThesaurusKeys[i][:0]
+ }
+ so.ThesaurusKeys = so.ThesaurusKeys[:0]
+ for _, idn := range so.Synonyms {
+ idn.Clear()
+ }
+ so.Synonyms = so.Synonyms[:0]
+ so.builderBuf.Reset()
+ if so.builder != nil {
+ err = so.builder.Reset(&so.builderBuf)
+ }
+ clear(so.FieldIDtoThesaurusID)
+ so.SynonymTermToID = so.SynonymTermToID[:0]
+ so.SynonymIDtoTerm = so.SynonymIDtoTerm[:0]
+ clear(so.thesaurusAddrs)
+
+ so.tmp0 = so.tmp0[:0]
+ return err
+}
+
+func (so *synonymIndexOpaque) process(field index.SynonymField, fieldID uint16, docNum uint32) {
+ // if this is the first time we are processing a synonym field in this batch
+ // we need to allocate memory for the thesauri and related data structures
+ if !so.init {
+ so.realloc()
+ so.init = true
+ }
+
+ // get the thesaurus id for this field
+ tid := so.FieldIDtoThesaurusID[fieldID]
+
+ // get the thesaurus for this field
+ thesaurus := so.Thesauri[tid]
+
+ termSynMap := so.SynonymTermToID[tid]
+
+ field.IterateSynonyms(func(term string, synonyms []string) {
+ pid := thesaurus[term] - 1
+
+ bs := so.Synonyms[pid]
+
+ for _, syn := range synonyms {
+ code := encodeSynonym(termSynMap[syn], docNum)
+ bs.Add(code)
+ }
+ })
+}
+
+// a one-time call to allocate memory for the thesauri and synonyms which takes
+// all the documents in the result batch and the fieldsMap and predetermines the
+// size of the data structures in the synonymIndexOpaque
+func (so *synonymIndexOpaque) realloc() {
+ var pidNext int
+ var sidNext uint32
+
+ // count the number of unique thesauri from the batch of documents
+ for _, result := range so.results {
+ if synDoc, ok := result.(index.SynonymDocument); ok {
+ synDoc.VisitSynonymFields(func(synField index.SynonymField) {
+ fieldIDPlus1 := so.FieldsMap[synField.Name()]
+ so.getOrDefineThesaurus(fieldIDPlus1-1, synField.Name())
+ })
+ }
+ }
+
+ for _, result := range so.results {
+ if synDoc, ok := result.(index.SynonymDocument); ok {
+ synDoc.VisitSynonymFields(func(synField index.SynonymField) {
+ fieldIDPlus1 := so.FieldsMap[synField.Name()]
+ thesaurusID := so.getOrDefineThesaurus(fieldIDPlus1-1, synField.Name())
+
+ thesaurus := so.Thesauri[thesaurusID]
+ thesaurusKeys := so.ThesaurusKeys[thesaurusID]
+
+ synTermMap := so.SynonymIDtoTerm[thesaurusID]
+
+ termSynMap := so.SynonymTermToID[thesaurusID]
+
+ // iterate over all the term-synonyms pair from the field
+ synField.IterateSynonyms(func(term string, synonyms []string) {
+ _, exists := thesaurus[term]
+ if !exists {
+ pidNext++
+ pidPlus1 := uint64(pidNext)
+
+ thesaurus[term] = pidPlus1
+ thesaurusKeys = append(thesaurusKeys, term)
+ }
+ for _, syn := range synonyms {
+ _, exists := termSynMap[syn]
+ if !exists {
+ termSynMap[syn] = sidNext
+ synTermMap[sidNext] = syn
+ sidNext++
+ }
+ }
+ })
+ so.ThesaurusKeys[thesaurusID] = thesaurusKeys
+ })
+ }
+ }
+
+ numSynonymsLists := pidNext
+
+ if cap(so.Synonyms) >= numSynonymsLists {
+ so.Synonyms = so.Synonyms[:numSynonymsLists]
+ } else {
+ synonyms := make([]*roaring64.Bitmap, numSynonymsLists)
+ copy(synonyms, so.Synonyms[:cap(so.Synonyms)])
+ for i := 0; i < numSynonymsLists; i++ {
+ if synonyms[i] == nil {
+ synonyms[i] = roaring64.New()
+ }
+ }
+ so.Synonyms = synonyms
+ }
+
+ for _, thes := range so.ThesaurusKeys {
+ sort.Strings(thes)
+ }
+}
+
+// getOrDefineThesaurus returns the thesaurus id for the given field id and thesaurus name.
+func (so *synonymIndexOpaque) getOrDefineThesaurus(fieldID uint16, thesaurusName string) int {
+ thesaurusIDPlus1, exists := so.ThesaurusMap[thesaurusName]
+ if !exists {
+ // need to create a new thesaurusID for this thesaurusName and
+ thesaurusIDPlus1 = uint16(len(so.ThesaurusInv) + 1)
+ so.ThesaurusMap[thesaurusName] = thesaurusIDPlus1
+ so.ThesaurusInv = append(so.ThesaurusInv, thesaurusName)
+
+ so.Thesauri = append(so.Thesauri, make(map[string]uint64))
+
+ so.SynonymIDtoTerm = append(so.SynonymIDtoTerm, make(map[uint32]string))
+
+ so.SynonymTermToID = append(so.SynonymTermToID, make(map[string]uint32))
+
+ // map the fieldID to the thesaurusID
+ so.FieldIDtoThesaurusID[fieldID] = int(thesaurusIDPlus1 - 1)
+
+ n := len(so.ThesaurusKeys)
+ if n < cap(so.ThesaurusKeys) {
+ so.ThesaurusKeys = so.ThesaurusKeys[:n+1]
+ so.ThesaurusKeys[n] = so.ThesaurusKeys[n][:0]
+ } else {
+ so.ThesaurusKeys = append(so.ThesaurusKeys, []string(nil))
+ }
+ }
+
+ return int(thesaurusIDPlus1 - 1)
+}
+
+// grabBuf returns a reusable buffer of the given size from the synonymIndexOpaque.
+func (so *synonymIndexOpaque) grabBuf(size int) []byte {
+ buf := so.tmp0
+ if cap(buf) < size {
+ buf = make([]byte, size)
+ so.tmp0 = buf
+ }
+ return buf[:size]
+}
+
+func (so *synonymIndexOpaque) writeThesauri(w *FileWriter) error {
+
+ if len(so.results) == 0 {
+ return nil
+ }
+
+ thesOffsets := make([]uint64, len(so.ThesaurusInv))
+ var err error
+
+ buf := so.grabBuf(binary.MaxVarintLen64)
+
+ if so.builder == nil {
+ so.builder, err = vellum.New(&so.builderBuf, nil)
+ if err != nil {
+ return err
+ }
+ }
+
+ for thesaurusID, terms := range so.ThesaurusKeys {
+ thes := so.Thesauri[thesaurusID]
+ for _, term := range terms { // terms are already sorted
+ pid := thes[term] - 1
+ postingsBS := so.Synonyms[pid]
+ postingsOffset, err := writeSynonyms(postingsBS, w, buf)
+ if err != nil {
+ return err
+ }
+
+ if postingsOffset > uint64(0) {
+ err = so.builder.Insert([]byte(term), postingsOffset)
+ if err != nil {
+ return err
+ }
+ }
+ }
+
+ err = so.builder.Close()
+ if err != nil {
+ return err
+ }
+
+ thesOffsets[thesaurusID] = uint64(w.Count())
+
+ vellumData := w.process(so.builderBuf.Bytes())
+
+ // write out the length of the vellum data
+ n := binary.PutUvarint(buf, uint64(len(vellumData)))
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ // write this vellum to disk
+ _, err = w.Write(vellumData)
+ if err != nil {
+ return err
+ }
+
+ // reset vellum for reuse
+ so.builderBuf.Reset()
+
+ err = so.builder.Reset(&so.builderBuf)
+ if err != nil {
+ return err
+ }
+
+ // write out the synTermMap for this thesaurus
+ err = writeSynTermMap(so.SynonymIDtoTerm[thesaurusID], w, buf)
+ if err != nil {
+ return err
+ }
+
+ thesaurusStart := w.Count()
+
+ n = binary.PutUvarint(buf, fieldNotUninverted)
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ n = binary.PutUvarint(buf, fieldNotUninverted)
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+
+ n = binary.PutUvarint(buf, thesOffsets[thesaurusID])
+ _, err = w.Write(buf[:n])
+ if err != nil {
+ return err
+ }
+ so.thesaurusAddrs[thesaurusID] = thesaurusStart
+ }
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+
+type synonymIndexSection struct {
+}
+
+func (s *synonymIndexSection) getSynonymIndexOpaque(opaque map[int]resetable) *synonymIndexOpaque {
+ if _, ok := opaque[SectionSynonymIndex]; !ok {
+ opaque[SectionSynonymIndex] = s.InitOpaque(nil)
+ }
+ return opaque[SectionSynonymIndex].(*synonymIndexOpaque)
+}
+
+// Implementations of the Section interface for the synonym index section.
+// InitOpaque initializes the synonym index opaque, which sets the FieldsMap and
+// results in the opaque before the section processes a synonym field.
+func (s *synonymIndexSection) InitOpaque(args map[string]interface{}) resetable {
+ rv := &synonymIndexOpaque{
+ ThesaurusMap: map[string]uint16{},
+ FieldIDtoThesaurusID: map[uint16]int{},
+ thesaurusAddrs: map[int]int{},
+ }
+ for k, v := range args {
+ rv.Set(k, v)
+ }
+
+ return rv
+}
+
+// Process processes a synonym field by adding the synonyms to the thesaurus
+// pointed to by the fieldID, implements the Process API for the synonym index section.
+func (s *synonymIndexSection) Process(opaque map[int]resetable, docNum uint32, field index.Field, fieldID uint16) {
+ if fieldID == math.MaxUint16 {
+ return
+ }
+ if sf, ok := field.(index.SynonymField); ok {
+ so := s.getSynonymIndexOpaque(opaque)
+ so.process(sf, fieldID, docNum)
+ }
+}
+
+// Persist serializes and writes the thesauri processed to the writer, along
+// with the synonym postings lists, and the synonym term map. Implements the
+// Persist API for the synonym index section.
+func (s *synonymIndexSection) Persist(opaque map[int]resetable, w *FileWriter) error {
+ so := s.getSynonymIndexOpaque(opaque)
+ return so.writeThesauri(w)
+}
+
+// AddrForField returns the file offset of the thesaurus for the given fieldID,
+// it uses the FieldIDtoThesaurusID map to translate the fieldID to the thesaurusID,
+// and returns the corresponding thesaurus offset from the thesaurusAddrs map.
+// Implements the AddrForField API for the synonym index section.
+func (s *synonymIndexSection) AddrForField(opaque map[int]resetable, fieldID int) int {
+ so := s.getSynonymIndexOpaque(opaque)
+ if so == nil || so.FieldIDtoThesaurusID == nil {
+ return 0
+ }
+ tid, exists := so.FieldIDtoThesaurusID[uint16(fieldID)]
+ if !exists {
+ return 0
+ }
+ return so.thesaurusAddrs[tid]
+}
+
+// Merge merges the thesauri, synonym postings lists and synonym term maps from
+// the segments into a single thesaurus and serializes and writes the merged
+// thesaurus and associated data to the writer. Implements the Merge API for the
+// synonym index section.
+func (s *synonymIndexSection) Merge(opaque map[int]resetable, segments []*SegmentBase,
+ drops []*roaring.Bitmap, fieldsInv []string, newDocNumsIn [][]uint64,
+ w *FileWriter, closeCh chan struct{}) error {
+ so := s.getSynonymIndexOpaque(opaque)
+ thesaurusAddrs, fieldIDtoThesaurusID, err := mergeAndPersistSynonymSection(segments, drops, fieldsInv, newDocNumsIn, w, closeCh)
+ if err != nil {
+ return err
+ }
+
+ so.thesaurusAddrs = thesaurusAddrs
+ so.FieldIDtoThesaurusID = fieldIDtoThesaurusID
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+
+// encodeSynonym encodes a synonymID and a docID into a single uint64 value.
+// The encoding format splits the 64 bits as follows:
+//
+// 63 32 31 0
+// +-----------+----------+
+// | synonymID | docNum |
+// +-----------+----------+
+//
+// The upper 32 bits (63-32) store the synonymID, and the lower 32 bits (31-0) store the docID.
+//
+// Parameters:
+//
+// synonymID - A 32-bit unsigned integer representing the ID of the synonym.
+// docID - A 32-bit unsigned integer representing the document ID.
+//
+// Returns:
+//
+// A 64-bit unsigned integer that combines the synonymID and docID.
+func encodeSynonym(synonymID uint32, docID uint32) uint64 {
+ return uint64(synonymID)<<32 | uint64(docID)
+}
+
+// writeSynonyms serilizes and writes the synonym postings list to the writer, by first
+// serializing the postings list to a byte slice and then writing the length
+// of the byte slice followed by the byte slice itself.
+func writeSynonyms(postings *roaring64.Bitmap, w *FileWriter, bufMaxVarintLen64 []byte) (
+ offset uint64, err error) {
+ termCardinality := postings.GetCardinality()
+ if termCardinality <= 0 {
+ return 0, nil
+ }
+
+ postingsOffset := uint64(w.Count())
+
+ buf, err := postings.ToBytes()
+ if err != nil {
+ return 0, err
+ }
+ buf = w.process(buf)
+
+ // write out the length
+ n := binary.PutUvarint(bufMaxVarintLen64, uint64(len(buf)))
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return 0, err
+ }
+ // write out the roaring bytes
+ _, err = w.Write(buf)
+ if err != nil {
+ return 0, err
+ }
+
+ return postingsOffset, nil
+}
+
+// writeSynTermMap serializes and writes the synonym term map to the writer, by first
+// writing the length of the map followed by the map entries, where each entry
+// consists of the synonym ID, the length of the term, and the term itself.
+func writeSynTermMap(synTermMap map[uint32]string, w *FileWriter, bufMaxVarintLen64 []byte) error {
+ n := binary.PutUvarint(bufMaxVarintLen64, uint64(len(synTermMap)))
+ _, err := w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return err
+ }
+
+ lenTerms := 0
+ for _, term := range synTermMap {
+ lenTerms += len(term)
+ }
+
+ buf := make([]byte, lenTerms+binary.MaxVarintLen64*(2*len(synTermMap)))
+ bufPos := 0
+ for sid, term := range synTermMap {
+ bufPos += binary.PutUvarint(buf[bufPos:], uint64(sid))
+ bufPos += binary.PutUvarint(buf[bufPos:], uint64(len(term)))
+ copy(buf[bufPos:], term)
+ bufPos += len(term)
+ }
+ buf = w.process(buf[:bufPos])
+
+ // write out the length of the map
+ n = binary.PutUvarint(bufMaxVarintLen64, uint64(len(buf)))
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return err
+ }
+
+ _, err = w.Write(buf)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func mergeAndPersistSynonymSection(segments []*SegmentBase, dropsIn []*roaring.Bitmap,
+ fieldsInv []string, newDocNumsIn [][]uint64, w *FileWriter,
+ closeCh chan struct{}) (map[int]int, map[uint16]int, error) {
+
+ var bufMaxVarintLen64 []byte = make([]byte, binary.MaxVarintLen64)
+
+ var synonyms *SynonymsList
+ var synItr *SynonymsIterator
+
+ thesaurusAddrs := make(map[int]int)
+
+ var vellumBuf bytes.Buffer
+ newVellum, err := vellum.New(&vellumBuf, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ newRoaring := roaring64.NewBitmap()
+
+ newDocNums := make([][]uint64, 0, len(segments))
+
+ drops := make([]*roaring.Bitmap, 0, len(segments))
+
+ thesauri := make([]*Thesaurus, 0, len(segments))
+
+ itrs := make([]vellum.Iterator, 0, len(segments))
+
+ fieldIDtoThesaurusID := make(map[uint16]int)
+
+ var thesaurusID int
+ var newSynonymID uint32
+
+ // for each field
+ for fieldID, fieldName := range fieldsInv {
+ // collect FST iterators from all active segments for this field
+ newDocNums = newDocNums[:0]
+ drops = drops[:0]
+ thesauri = thesauri[:0]
+ itrs = itrs[:0]
+ newSynonymID = 0
+ synTermMap := make(map[uint32]string)
+ termSynMap := make(map[string]uint32)
+
+ for segmentI, segment := range segments {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return nil, nil, seg.ErrClosed
+ }
+
+ thes, err2 := segment.thesaurus(fieldName)
+ if err2 != nil {
+ return nil, nil, err2
+ }
+ if thes != nil && thes.fst != nil {
+ itr, err2 := thes.fst.Iterator(nil, nil)
+ if err2 != nil && err2 != vellum.ErrIteratorDone {
+ return nil, nil, err2
+ }
+ if itr != nil {
+ newDocNums = append(newDocNums, newDocNumsIn[segmentI])
+ if dropsIn[segmentI] != nil && !dropsIn[segmentI].IsEmpty() {
+ drops = append(drops, dropsIn[segmentI])
+ } else {
+ drops = append(drops, nil)
+ }
+ thesauri = append(thesauri, thes)
+ itrs = append(itrs, itr)
+ }
+ }
+ }
+
+ // if no iterators, skip this field
+ if len(itrs) == 0 {
+ continue
+ }
+
+ var prevTerm []byte
+
+ newRoaring.Clear()
+
+ finishTerm := func(term []byte) error {
+ postingsOffset, err := writeSynonyms(newRoaring, w, bufMaxVarintLen64)
+ if err != nil {
+ return err
+ }
+ if postingsOffset > 0 {
+ err = newVellum.Insert(term, postingsOffset)
+ if err != nil {
+ return err
+ }
+ }
+ newRoaring.Clear()
+ return nil
+ }
+
+ enumerator, err := newEnumerator(itrs)
+
+ for err == nil {
+ term, itrI, postingsOffset := enumerator.Current()
+
+ if prevTerm != nil && !bytes.Equal(prevTerm, term) {
+ // check for the closure in meantime
+ if isClosed(closeCh) {
+ return nil, nil, seg.ErrClosed
+ }
+
+ // if the term changed, write out the info collected
+ // for the previous term
+ err = finishTerm(prevTerm)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ synonyms, err = thesauri[itrI].synonymsListFromOffset(
+ postingsOffset, drops[itrI], synonyms)
+ if err != nil {
+ return nil, nil, err
+ }
+ synItr = synonyms.iterator(synItr)
+
+ var next seg.Synonym
+ next, err = synItr.Next()
+ for next != nil && err == nil {
+ synNewDocNum := newDocNums[itrI][next.Number()]
+ if synNewDocNum == docDropped {
+ return nil, nil, fmt.Errorf("see hit with dropped docNum")
+ }
+ nextTerm := next.Term()
+ var synNewID uint32
+ if synID, ok := termSynMap[nextTerm]; ok {
+ synNewID = synID
+ } else {
+ synNewID = newSynonymID
+ termSynMap[nextTerm] = newSynonymID
+ synTermMap[newSynonymID] = nextTerm
+ newSynonymID++
+ }
+ synNewCode := encodeSynonym(synNewID, uint32(synNewDocNum))
+ newRoaring.Add(synNewCode)
+ next, err = synItr.Next()
+ }
+ if err != nil {
+ return nil, nil, err
+ }
+
+ prevTerm = prevTerm[:0] // copy to prevTerm in case Next() reuses term mem
+ prevTerm = append(prevTerm, term...)
+ err = enumerator.Next()
+ }
+ if err != vellum.ErrIteratorDone {
+ return nil, nil, err
+ }
+ // close the enumerator to free the underlying iterators
+ err = enumerator.Close()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ if prevTerm != nil {
+ err = finishTerm(prevTerm)
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+
+ err = newVellum.Close()
+ if err != nil {
+ return nil, nil, err
+ }
+ vellumData := w.process(vellumBuf.Bytes())
+
+ thesOffset := uint64(w.Count())
+
+ // write out the length of the vellum data
+ n := binary.PutUvarint(bufMaxVarintLen64, uint64(len(vellumData)))
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // write this vellum to disk
+ _, err = w.Write(vellumData)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // reset vellum buffer and vellum builder
+ vellumBuf.Reset()
+ err = newVellum.Reset(&vellumBuf)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // write out the synTermMap for this thesaurus
+ err = writeSynTermMap(synTermMap, w, bufMaxVarintLen64)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ thesStart := w.Count()
+
+ // the synonym index section does not have any doc value data
+ // so we write two special entries to indicate that
+ // the field is not uninverted and the thesaurus offset
+ n = binary.PutUvarint(bufMaxVarintLen64, fieldNotUninverted)
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ n = binary.PutUvarint(bufMaxVarintLen64, fieldNotUninverted)
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // write out the thesaurus offset from which the vellum data starts
+ n = binary.PutUvarint(bufMaxVarintLen64, thesOffset)
+ _, err = w.Write(bufMaxVarintLen64[:n])
+ if err != nil {
+ return nil, nil, err
+ }
+
+ // if we have a new thesaurus, add it to the thesaurus map
+ fieldIDtoThesaurusID[uint16(fieldID)] = thesaurusID
+ thesaurusAddrs[thesaurusID] = thesStart
+ thesaurusID++
+ }
+
+ return thesaurusAddrs, fieldIDtoThesaurusID, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/segment.go b/vendor/github.com/blevesearch/zapx/v17/segment.go
new file mode 100644
index 0000000000..21d7bedd2a
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/segment.go
@@ -0,0 +1,932 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "io"
+ "os"
+ "sync"
+ "sync/atomic"
+ "unsafe"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ mmap "github.com/blevesearch/mmap-go"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/golang/snappy"
+)
+
+var reflectStaticSizeSegmentBase int
+
+func init() {
+ var sb SegmentBase
+ reflectStaticSizeSegmentBase = int(unsafe.Sizeof(sb))
+}
+
+// OpenUsing returns a zap impl of a segment which tracks some config values during
+// the its lifetime.
+func (z *ZapPlugin) OpenUsing(path string, config map[string]interface{}) (segment.Segment, error) {
+ return z.open(path, config)
+}
+
+// Open returns a zap impl of a segment
+func (z *ZapPlugin) Open(path string) (segment.Segment, error) {
+ return z.open(path, nil)
+}
+
+func (*ZapPlugin) open(path string, config map[string]interface{}) (segment.Segment, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ mm, err := mmap.Map(f, mmap.RDONLY, 0)
+ if err != nil {
+ // mmap failed, try to close the file
+ _ = f.Close()
+ return nil, err
+ }
+
+ rv := &Segment{
+ SegmentBase: SegmentBase{
+ fieldsMap: make(map[string]uint16),
+ fieldsOptions: make(map[string]index.FieldIndexingOptions),
+ invIndexCache: newInvertedIndexCache(),
+ vecIndexCache: newVectorIndexCache(),
+ synIndexCache: newSynonymIndexCache(),
+ nstIndexCache: newNestedIndexCache(),
+ fieldDvReaders: make([][]*docValueReader, len(segmentSections)),
+ config: config,
+ },
+ f: f,
+ mm: mm,
+ path: path,
+ refs: 1,
+ }
+ rv.SegmentBase.updateSize()
+
+ err = rv.loadConfig()
+ if err != nil {
+ _ = rv.Close()
+ return nil, err
+ }
+
+ err = rv.loadFields()
+ if err != nil {
+ _ = rv.Close()
+ return nil, err
+ }
+
+ err = rv.loadDvReaders()
+ if err != nil {
+ _ = rv.Close()
+ return nil, err
+ }
+
+ // initialize any of the caches if needed
+ err = rv.nstIndexCache.initialize(rv.numDocs, rv.getEdgeListOffset(), rv.mem)
+ if err != nil {
+ _ = rv.Close()
+ return nil, err
+ }
+
+ return rv, nil
+}
+
+// SegmentBase is a memory only, read-only implementation of the
+// segment.Segment interface, using zap's data representation.
+type SegmentBase struct {
+ // atomic access to these variables, moved to top to correct alignment issues on ARM, 386 and 32-bit MIPS.
+ bytesRead uint64
+ bytesWritten uint64
+
+ mem []byte
+ memCRC uint32
+ chunkMode uint32
+ fieldsMap map[string]uint16 // fieldName -> fieldID+1
+ fieldsOptions map[string]index.FieldIndexingOptions // fieldName -> fieldOptions
+ fieldsInv []string // fieldID -> fieldName
+ fieldsSectionsMap [][]uint64 // fieldID -> section -> address
+ numDocs uint64
+ storedIndexOffset uint64
+ sectionsIndexOffset uint64
+ fieldDvReaders [][]*docValueReader // naive chunk cache per field; section->fieldID->reader
+ fieldDvNames []string // field names cached in fieldDvReaders
+ size uint64
+
+ // file reader initialised with the writer callback id used by the segment
+ fileReader *FileReader
+
+ // index update specific tracking
+ updatedFields map[string]*index.UpdateFieldInfo
+ config map[string]interface{} // config for the segment
+
+ // section-specific caches
+ invIndexCache *invertedIndexCache
+ vecIndexCache *vectorIndexCache
+ synIndexCache *synonymIndexCache
+ nstIndexCache *nestedIndexCache
+}
+
+func (sb *SegmentBase) Size() int {
+ return int(sb.size)
+}
+
+func (sb *SegmentBase) updateSize() {
+ sizeInBytes := reflectStaticSizeSegmentBase +
+ cap(sb.mem)
+
+ // fieldsMap
+ for k := range sb.fieldsMap {
+ sizeInBytes += (len(k) + SizeOfString) + SizeOfUint16
+ }
+
+ // fieldsOptions
+ for k := range sb.fieldsOptions {
+ sizeInBytes += (len(k) + SizeOfString) + SizeOfUint64
+ }
+
+ // fieldsInv
+ for _, entry := range sb.fieldsInv {
+ sizeInBytes += len(entry) + SizeOfString
+ }
+
+ // fieldDvReaders
+ for _, secDvReaders := range sb.fieldDvReaders {
+ for _, v := range secDvReaders {
+ sizeInBytes += SizeOfUint16 + SizeOfPtr
+ if v != nil {
+ sizeInBytes += v.size()
+ }
+ }
+ }
+
+ sb.size = uint64(sizeInBytes)
+}
+
+func (sb *SegmentBase) AddRef() {}
+func (sb *SegmentBase) DecRef() (err error) { return nil }
+func (sb *SegmentBase) Close() (err error) {
+ sb.invIndexCache.Clear()
+ sb.vecIndexCache.Clear()
+ sb.synIndexCache.Clear()
+ sb.nstIndexCache.Clear()
+ return nil
+}
+
+// Segment implements a persisted segment.Segment interface, by
+// embedding an mmap()'ed SegmentBase.
+type Segment struct {
+ SegmentBase
+
+ f *os.File
+ mm mmap.MMap
+ path string
+ version uint32
+ crc uint32
+
+ m sync.Mutex // Protects the fields that follow.
+ refs int64
+}
+
+func (s *Segment) Size() int {
+ // 8 /* size of file pointer */
+ // 4 /* size of version -> uint32 */
+ // 4 /* size of crc -> uint32 */
+ sizeOfUints := 16
+
+ sizeInBytes := (len(s.path) + SizeOfString) + sizeOfUints
+
+ // mutex, refs -> int64
+ sizeInBytes += 16
+
+ // do not include the mmap'ed part
+ return sizeInBytes + s.SegmentBase.Size() - cap(s.mem)
+}
+
+func (s *Segment) AddRef() {
+ s.m.Lock()
+ s.refs++
+ s.m.Unlock()
+}
+
+func (s *Segment) DecRef() (err error) {
+ s.m.Lock()
+ s.refs--
+ if s.refs == 0 {
+ err = s.closeActual()
+ }
+ s.m.Unlock()
+ return err
+}
+
+func (s *Segment) loadConfig() error {
+ // read offsets of 32 bit values - crc, ver, chunk
+ crcOffset := len(s.mm) - 4
+ verOffset := crcOffset - 4
+ chunkOffset := verOffset - 4
+
+ // read offsets of 64 bit values - sectionsIndexOffset, storedIndexOffset, numDocsOffset
+ sectionsIndexOffset := chunkOffset - 8
+ storedIndexOffset := sectionsIndexOffset - 8
+ numDocsOffset := storedIndexOffset - 8
+
+ // read offsets for the writer id length
+ idLenOffset := numDocsOffset - 4
+
+ // read 32-bit crc
+ s.crc = binary.BigEndian.Uint32(s.mm[crcOffset : crcOffset+4])
+
+ // read 32-bit version
+ s.version = binary.BigEndian.Uint32(s.mm[verOffset : verOffset+4])
+ if s.version != Version {
+ return fmt.Errorf("unsupported version %d != %d", s.version, Version)
+ }
+
+ // read 32-bit chunk mode
+ s.chunkMode = binary.BigEndian.Uint32(s.mm[chunkOffset : chunkOffset+4])
+
+ // read 64-bit sections index offset
+ s.sectionsIndexOffset = binary.BigEndian.Uint64(s.mm[sectionsIndexOffset : sectionsIndexOffset+8])
+
+ // read 64-bit stored index offset
+ s.storedIndexOffset = binary.BigEndian.Uint64(s.mm[storedIndexOffset : storedIndexOffset+8])
+
+ // read 64-bit num docs
+ s.numDocs = binary.BigEndian.Uint64(s.mm[numDocsOffset : numDocsOffset+8])
+
+ // read the length of the id
+ idLen := binary.BigEndian.Uint32(s.mm[idLenOffset : idLenOffset+4])
+ idOffset := idLenOffset - int(idLen)
+
+ // read the file writer callback id and initialize the file reader with the same id
+ fileWriterID := string(s.mm[idOffset : idOffset+int(idLen)])
+ var err error
+ s.fileReader, err = NewFileReader(fileWriterID, []byte(s.path))
+ if err != nil {
+ return err
+ }
+
+ footerSize := FooterSize + int(idLen)
+ s.incrementBytesRead(uint64(footerSize))
+ s.SegmentBase.mem = s.mm[:len(s.mm)-footerSize]
+ return nil
+}
+
+// Implements the segment.DiskStatsReporter interface
+// Only the persistedSegment type implments the
+// interface, as the intention is to retrieve the bytes
+// read from the on-disk segment as part of the current
+// query.
+func (s *Segment) ResetBytesRead(val uint64) {
+ atomic.StoreUint64(&s.SegmentBase.bytesRead, val)
+}
+
+func (s *Segment) BytesRead() uint64 {
+ return atomic.LoadUint64(&s.bytesRead)
+}
+
+func (s *Segment) BytesWritten() uint64 {
+ return 0
+}
+
+func (s *Segment) incrementBytesRead(val uint64) {
+ atomic.AddUint64(&s.bytesRead, val)
+}
+
+func (sb *SegmentBase) BytesWritten() uint64 {
+ return atomic.LoadUint64(&sb.bytesWritten)
+}
+
+func (sb *SegmentBase) setBytesWritten(val uint64) {
+ atomic.AddUint64(&sb.bytesWritten, val)
+}
+
+func (sb *SegmentBase) BytesRead() uint64 {
+ return 0
+}
+
+func (sb *SegmentBase) ResetBytesRead(val uint64) {}
+
+func (sb *SegmentBase) incrementBytesRead(val uint64) {
+ atomic.AddUint64(&sb.bytesRead, val)
+}
+
+func (sb *SegmentBase) loadFields() error {
+ pos := sb.sectionsIndexOffset
+
+ if pos == 0 {
+ return fmt.Errorf("no sections index present")
+ }
+
+ seek := pos + binary.MaxVarintLen64
+ if seek > uint64(len(sb.mem)) {
+ // handling a buffer overflow case.
+ // a rare case where the backing buffer is not large enough to be read directly via
+ // a pos+binary.MaxVarintLen64 seek. For eg, this can happen when there is only
+ // one field to be indexed in the entire batch of data and while writing out
+ // these fields metadata, you write 1 + 8 bytes whereas the MaxVarintLen64 = 10.
+ seek = uint64(len(sb.mem))
+ }
+
+ // read the number of fields
+ numFields, sz := binary.Uvarint(sb.mem[pos:seek])
+ // here, the pos is incremented by the valid number bytes read from the buffer
+ // so in the edge case pointed out above the numFields = 1, the sz = 1 as well.
+ pos += uint64(sz)
+ sb.incrementBytesRead(uint64(sz))
+
+ // the following loop will be executed only once in the edge case pointed out above
+ // since there is only field's offset store which occupies 8 bytes.
+ // the pointer then seeks to a position preceding the sectionsIndexOffset, at
+ // which point the responsibility of handling the out-of-bounds cases shifts to
+ // the specific section's parsing logic.
+ var fieldID uint64
+ for fieldID < numFields {
+ addr := binary.BigEndian.Uint64(sb.mem[pos : pos+8])
+ sb.incrementBytesRead(8)
+
+ fieldSectionMap, err := sb.loadField(uint16(fieldID), addr)
+ if err != nil {
+ return err
+ }
+
+ sb.fieldsSectionsMap = append(sb.fieldsSectionsMap, fieldSectionMap)
+
+ fieldID++
+ pos += 8
+ }
+
+ return nil
+}
+
+// loadField loads the field metadata for the given fieldID at the given position
+func (sb *SegmentBase) loadField(fieldID uint16, pos uint64) ([]uint64, error) {
+ if pos == 0 {
+ // there is no indexing structure present for this field/section
+ return nil, nil
+ }
+
+ fieldStartPos := pos // to track the number of bytes read
+ fieldNameLen, sz := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(sz)
+
+ fieldName, err := sb.fileReader.process(sb.mem[pos : pos+fieldNameLen])
+ if err != nil {
+ return nil, err
+ }
+ pos += fieldNameLen
+
+ // read field options
+ fieldOptions, sz := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(sz)
+
+ sb.fieldsInv = append(sb.fieldsInv, string(fieldName))
+ sb.fieldsMap[string(fieldName)] = fieldID + 1
+ sb.fieldsOptions[string(fieldName)] = index.FieldIndexingOptions(fieldOptions)
+
+ fieldNumSections, sz := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(sz)
+ // create an address mapping array for each of the segment sections
+ // if the field has a valid section index, then the address will be non-zero
+ // else it will be zero.
+ fieldSectionMap := make([]uint64, NumSections)
+ for sectionIdx := uint64(0); sectionIdx < fieldNumSections; sectionIdx++ {
+ // read section id
+ fieldSectionType := binary.BigEndian.Uint16(sb.mem[pos : pos+2])
+ pos += 2
+ fieldSectionAddr := binary.BigEndian.Uint64(sb.mem[pos : pos+8])
+ pos += 8
+ fieldSectionMap[fieldSectionType] = fieldSectionAddr
+ }
+
+ // account the bytes read while parsing the sections field index.
+ sb.incrementBytesRead((pos - uint64(fieldStartPos)) + fieldNameLen)
+ return fieldSectionMap, nil
+}
+
+// Dictionary returns the term dictionary for the specified field
+func (sb *SegmentBase) Dictionary(field string) (segment.TermDictionary, error) {
+ dict, err := sb.dictionary(field)
+ if err == nil && dict == nil {
+ return emptyDictionary, nil
+ }
+ return dict, err
+}
+
+func (sb *SegmentBase) dictionary(field string) (rv *Dictionary, err error) {
+ fieldIDPlus1 := sb.fieldsMap[field]
+ if fieldIDPlus1 == 0 {
+ return nil, nil
+ }
+ pos := sb.fieldsSectionsMap[fieldIDPlus1-1][SectionInvertedTextIndex]
+ if pos > 0 {
+ rv = &Dictionary{
+ sb: sb,
+ field: field,
+ fieldID: fieldIDPlus1 - 1,
+ }
+ // skip the doc value offsets to get to the dictionary portion
+ for i := 0; i < 2; i++ {
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ }
+ dictLoc, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ fst, bytesRead, err := sb.invIndexCache.loadOrCreate(rv.fieldID, sb.mem[dictLoc:], sb.fileReader)
+ if err != nil {
+ return nil, fmt.Errorf("dictionary for field %s err: %v", field, err)
+ }
+ rv.fst = fst
+ rv.fstReader, err = rv.fst.Reader()
+ if err != nil {
+ return nil, fmt.Errorf("dictionary for field %s, vellum reader err: %v", field, err)
+ }
+ rv.bytesRead += bytesRead
+ }
+
+ return rv, nil
+}
+
+// Thesaurus returns the thesaurus with the specified name, or an empty thesaurus if not found.
+func (sb *SegmentBase) Thesaurus(name string) (segment.Thesaurus, error) {
+ thesaurus, err := sb.thesaurus(name)
+ if err == nil && thesaurus == nil {
+ return emptyThesaurus, nil
+ }
+ return thesaurus, err
+}
+
+func (sb *SegmentBase) thesaurus(name string) (rv *Thesaurus, err error) {
+ fieldIDPlus1 := sb.fieldsMap[name]
+ if fieldIDPlus1 == 0 {
+ return nil, nil
+ }
+ pos := sb.fieldsSectionsMap[fieldIDPlus1-1][SectionSynonymIndex]
+ if pos > 0 {
+ rv = &Thesaurus{
+ sb: sb,
+ name: name,
+ fieldID: fieldIDPlus1 - 1,
+ }
+ // skip the doc value offsets as doc values are not supported in thesaurus
+ for i := 0; i < 2; i++ {
+ _, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ }
+ thesLoc, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ fst, synTermMap, bytesRead, err := sb.synIndexCache.loadOrCreate(rv.fieldID, sb.mem[thesLoc:], sb.fileReader)
+ if err != nil {
+ return nil, fmt.Errorf("thesaurus name %s err: %v", name, err)
+ }
+ rv.fst = fst
+ rv.synIDTermMap = synTermMap
+ rv.fstReader, err = rv.fst.Reader()
+ if err != nil {
+ return nil, fmt.Errorf("thesaurus name %s vellum reader err: %v", name, err)
+ }
+ rv.bytesRead += bytesRead
+ }
+ return rv, nil
+}
+
+// visitDocumentCtx holds data structures that are reusable across
+// multiple VisitDocument() calls to avoid memory allocations
+type visitDocumentCtx struct {
+ buf []byte
+ reader bytes.Reader
+ arrayPos []uint64
+}
+
+var visitDocumentCtxPool = sync.Pool{
+ New: func() interface{} {
+ reuse := &visitDocumentCtx{}
+ return reuse
+ },
+}
+
+// VisitStoredFields invokes the StoredFieldValueVisitor for each stored field
+// for the specified doc number
+func (sb *SegmentBase) VisitStoredFields(num uint64, visitor segment.StoredFieldValueVisitor) error {
+ vdc := visitDocumentCtxPool.Get().(*visitDocumentCtx)
+ defer visitDocumentCtxPool.Put(vdc)
+ return sb.visitStoredFields(vdc, num, visitor)
+}
+
+func (sb *SegmentBase) visitStoredFields(vdc *visitDocumentCtx, num uint64,
+ visitor segment.StoredFieldValueVisitor) error {
+ // first make sure this is a valid number in this segment
+ if num < sb.numDocs {
+ meta, compressed, err := sb.getDocStoredMetaAndCompressed(num)
+ if err != nil {
+ return err
+ }
+
+ vdc.reader.Reset(meta)
+
+ // handle _id field special case
+ idFieldValLen, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ idFieldVal := compressed[:idFieldValLen]
+
+ keepGoing := visitor("_id", byte('t'), idFieldVal, nil)
+ if !keepGoing {
+ visitDocumentCtxPool.Put(vdc)
+ return nil
+ }
+
+ // handle non-"_id" fields
+ compressed = compressed[idFieldValLen:]
+
+ uncompressed, err := snappy.Decode(vdc.buf[:cap(vdc.buf)], compressed)
+ if err != nil {
+ return err
+ }
+
+ for keepGoing {
+ field, err := binary.ReadUvarint(&vdc.reader)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return err
+ }
+ typ, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ offset, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ l, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ numap, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ var arrayPos []uint64
+ if numap > 0 {
+ if cap(vdc.arrayPos) < int(numap) {
+ vdc.arrayPos = make([]uint64, numap)
+ }
+ arrayPos = vdc.arrayPos[:numap]
+ for i := 0; i < int(numap); i++ {
+ ap, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return err
+ }
+ arrayPos[i] = ap
+ }
+ }
+ value := uncompressed[offset : offset+l]
+ keepGoing = visitor(sb.fieldsInv[field], byte(typ), value, arrayPos)
+ }
+
+ vdc.buf = uncompressed
+ }
+ return nil
+}
+
+// DocID returns the value of the _id field for the given docNum
+func (sb *SegmentBase) DocID(num uint64) ([]byte, error) {
+ if num >= sb.numDocs {
+ return nil, nil
+ }
+
+ vdc := visitDocumentCtxPool.Get().(*visitDocumentCtx)
+
+ meta, compressed, err := sb.getDocStoredMetaAndCompressed(num)
+ if err != nil {
+ return nil, err
+ }
+
+ vdc.reader.Reset(meta)
+
+ // handle _id field special case
+ idFieldValLen, err := binary.ReadUvarint(&vdc.reader)
+ if err != nil {
+ return nil, err
+ }
+ idFieldVal := compressed[:idFieldValLen]
+
+ visitDocumentCtxPool.Put(vdc)
+
+ return idFieldVal, nil
+}
+
+// Count returns the number of documents in this segment.
+func (sb *SegmentBase) Count() uint64 {
+ return sb.numDocs
+}
+
+// DocNumbers returns a bitset corresponding to the doc numbers of all the
+// provided _id strings
+func (sb *SegmentBase) DocNumbers(ids []string) (*roaring.Bitmap, error) {
+ rv := roaring.New()
+
+ if len(sb.fieldsMap) > 0 {
+ idDict, err := sb.dictionary("_id")
+ if err != nil {
+ return nil, err
+ }
+
+ postingsList := emptyPostingsList
+
+ sMax, err := idDict.fst.GetMaxKey()
+ if err != nil {
+ return nil, err
+ }
+ sMaxStr := string(sMax)
+ for _, id := range ids {
+ if id <= sMaxStr {
+ postingsList, err = idDict.postingsList([]byte(id), nil, postingsList)
+ if err != nil {
+ return nil, err
+ }
+ postingsList.OrInto(rv)
+ }
+ }
+ }
+
+ return rv, nil
+}
+
+// Fields returns the field names used in this segment
+func (sb *SegmentBase) Fields() []string {
+ return sb.fieldsInv
+}
+
+// Path returns the path of this segment on disk
+func (s *Segment) Path() string {
+ return s.path
+}
+
+// Close releases all resources associated with this segment
+func (s *Segment) Close() (err error) {
+ return s.DecRef()
+}
+
+func (s *Segment) closeActual() (err error) {
+ // clear contents from all caches before un-mmapping
+ s.invIndexCache.Clear()
+ s.vecIndexCache.Clear()
+ s.synIndexCache.Clear()
+ s.nstIndexCache.Clear()
+
+ if s.mm != nil {
+ err = s.mm.Unmap()
+ }
+ // try to close file even if unmap failed
+ if s.f != nil {
+ err2 := s.f.Close()
+ if err == nil {
+ // try to return first error
+ err = err2
+ }
+ }
+
+ return
+}
+
+// some helpers i started adding for the command-line utility
+
+// Data returns the underlying mmaped data slice
+func (s *Segment) Data() []byte {
+ return s.mm
+}
+
+// CRC returns the CRC value stored in the file footer
+func (s *Segment) CRC() uint32 {
+ return s.crc
+}
+
+// Version returns the file version in the file footer
+func (s *Segment) Version() uint32 {
+ return s.version
+}
+
+// ChunkFactor returns the chunk factor in the file footer
+func (s *Segment) ChunkMode() uint32 {
+ return s.chunkMode
+}
+
+// SectionsIndexOffset returns the sections index offset in the file footer
+func (s *Segment) SectionsIndexOffset() uint64 {
+ return s.sectionsIndexOffset
+}
+
+// StoredIndexOffset returns the stored value index offset in the file footer
+func (s *Segment) StoredIndexOffset() uint64 {
+ return s.storedIndexOffset
+}
+
+// NumDocs returns the number of documents in the file footer
+func (s *Segment) NumDocs() uint64 {
+ return s.numDocs
+}
+
+// DictAddr is a helper function to compute the file offset where the
+// dictionary is stored for the specified field.
+func (s *Segment) DictAddr(field string) (uint64, error) {
+ fieldIDPlus1, ok := s.fieldsMap[field]
+ if !ok {
+ return 0, fmt.Errorf("no such field '%s'", field)
+ }
+ dictStart := s.fieldsSectionsMap[fieldIDPlus1-1][SectionInvertedTextIndex]
+ if dictStart == 0 {
+ return 0, fmt.Errorf("no dictionary for field '%s'", field)
+ }
+ for i := 0; i < 2; i++ {
+ _, n := binary.Uvarint(s.mem[dictStart : dictStart+binary.MaxVarintLen64])
+ dictStart += uint64(n)
+ }
+ dictLoc, _ := binary.Uvarint(s.mem[dictStart : dictStart+binary.MaxVarintLen64])
+ return dictLoc, nil
+}
+
+// VectorAddr is a helper function to compute the file offset where the
+// vector index is stored for the specified field.
+func (s *Segment) VectorAddr(name string) (uint64, error) {
+ fieldIDPlus1, ok := s.fieldsMap[name]
+ if !ok {
+ return 0, fmt.Errorf("no such field '%s'", name)
+ }
+ vectorStart := s.fieldsSectionsMap[fieldIDPlus1-1][SectionFaissVectorIndex]
+ if vectorStart == 0 {
+ return 0, fmt.Errorf("no vector index for field '%s'", name)
+ }
+ for i := 0; i < 2; i++ {
+ _, n := binary.Uvarint(s.mem[vectorStart : vectorStart+binary.MaxVarintLen64])
+ vectorStart += uint64(n)
+ }
+ vectorLoc, _ := binary.Uvarint(s.mem[vectorStart : vectorStart+binary.MaxVarintLen64])
+ return vectorLoc, nil
+}
+
+// ThesaurusAddr is a helper function to compute the file offset where the
+// thesaurus is stored with the specified name.
+func (s *Segment) ThesaurusAddr(name string) (uint64, error) {
+ fieldIDPlus1, ok := s.fieldsMap[name]
+ if !ok {
+ return 0, fmt.Errorf("no such thesaurus '%s'", name)
+ }
+ thesaurusStart := s.fieldsSectionsMap[fieldIDPlus1-1][SectionSynonymIndex]
+ if thesaurusStart == 0 {
+ return 0, fmt.Errorf("no such thesaurus '%s'", name)
+ }
+ for i := 0; i < 2; i++ {
+ _, n := binary.Uvarint(s.mem[thesaurusStart : thesaurusStart+binary.MaxVarintLen64])
+ thesaurusStart += uint64(n)
+ }
+ thesLoc, _ := binary.Uvarint(s.mem[thesaurusStart : thesaurusStart+binary.MaxVarintLen64])
+ return thesLoc, nil
+}
+
+// EdgeListAddr is the exported helper function to compute the
+// file offset where the edge list is stored.
+func (s *Segment) EdgeListAddr() (uint64, error) {
+ return s.getEdgeListOffset(), nil
+}
+
+func (sb *SegmentBase) loadDvReaders() error {
+ if sb.numDocs == 0 {
+ return nil
+ }
+ for fieldID, sections := range sb.fieldsSectionsMap {
+ for secID, secOffset := range sections {
+ if secOffset > 0 {
+ pos := secOffset
+ var read uint64
+ fieldLocStart, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return fmt.Errorf("loadDvReaders: failed to read the docvalue offset start for field %v", sb.fieldsInv[fieldID])
+ }
+ pos += uint64(n)
+ read += uint64(n)
+ fieldLocEnd, n := binary.Uvarint(sb.mem[pos : pos+binary.MaxVarintLen64])
+ if n <= 0 {
+ return fmt.Errorf("loadDvReaders: failed to read the docvalue offset end for field %v", sb.fieldsInv[fieldID])
+ }
+ pos += uint64(n)
+ read += uint64(n)
+
+ sb.incrementBytesRead(read)
+
+ fieldDvReader, err := sb.loadFieldDocValueReader(sb.fieldsInv[fieldID], fieldLocStart, fieldLocEnd)
+ if err != nil {
+ return err
+ }
+ if fieldDvReader != nil {
+ if sb.fieldDvReaders[secID] == nil {
+ sb.fieldDvReaders[secID] = make([]*docValueReader, len(sb.fieldsInv))
+ }
+ sb.fieldDvReaders[secID][uint16(fieldID)] = fieldDvReader
+ sb.fieldDvNames = append(sb.fieldDvNames, sb.fieldsInv[fieldID])
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+// Getter method to retrieve updateFieldInfo within segment base
+func (s *SegmentBase) GetUpdatedFields() map[string]*index.UpdateFieldInfo {
+ return s.updatedFields
+}
+
+// Setter method to store updateFieldInfo within segment base
+func (s *SegmentBase) SetUpdatedFields(updatedFields map[string]*index.UpdateFieldInfo) {
+ s.updatedFields = updatedFields
+}
+
+// Ancestors returns a slice of document numbers representing the ancestors of the
+// specified document (docNum) within the segment. If the document has no ancestors,
+// a slice containing only the document number itself is returned. The prealloc
+// parameter allows for reusing a preallocated slice to avoid additional allocations.
+func (sb *SegmentBase) Ancestors(docNum uint64, prealloc []index.AncestorID) []index.AncestorID {
+ return sb.nstIndexCache.ancestry(docNum, prealloc)
+}
+
+// CountRoot returns the number of root documents in the segment, excluding any
+// documents that are marked as deleted in the provided bitmap. The deleted bitmap
+// may contain both root and sub-document numbers, and the method ensures that
+// only root documents are counted.
+func (sb *SegmentBase) CountRoot(deleted *roaring.Bitmap) uint64 {
+ // the formula is as follows:
+ // Total Docs (T) = Root Docs (R) + Sub Docs (S)
+ // R = T - S
+ // Now if we have D deleted docs, some of which may be sub-docs, we need to exclude
+ // those from the root doc count. Let D = dR + dS, where dR is the number of deleted
+ // root docs and dS is the number of deleted sub docs.
+ // dR = D - dS
+ // Therefore, the count of root docs excluding deleted ones is:
+ // R - dR = (T - S) - (D - dS)
+ return (sb.Count() - sb.countNested()) - (sb.nstIndexCache.countRoot(deleted))
+}
+
+// AddNestedDocuments returns a bitmap containing the original document numbers in drops,
+// plus any descendant document numbers for each dropped document. The drops
+// parameter represents a set of document numbers to be dropped, and the returned
+// bitmap includes both the original drops and all their descendants (if any).
+func (sb *SegmentBase) AddNestedDocuments(drops *roaring.Bitmap) *roaring.Bitmap {
+ // If no drops or no subDocs, nothing to do
+ if drops == nil || drops.GetCardinality() == 0 || sb.countNested() == 0 {
+ return drops
+ }
+ // Get the edge list for this segment
+ el := sb.EdgeList()
+ // Algorithm => iterate through each child->parent mapping in the edge list,
+ // and for each pair, check if the parent is in the drops bitmap.
+ // If it is, and the child is also not already in the drops bitmap,
+ // add the child to the drops. Repeat this process until no
+ // new additions are made in an iteration.
+ changed := true
+ for changed {
+ changed = false
+ el.Iterate(func(child uint64, parent uint64) bool {
+ if drops.Contains(uint32(parent)) && !drops.Contains(uint32(child)) {
+ drops.Add(uint32(child))
+ changed = true
+ }
+ return true
+ })
+ }
+ return drops
+}
+
+// EdgeList returns an EdgeList interface representing the parent-child relationships between documents in the segment.
+// The EdgeList interface allows iteration over child-parent document pairs, enabling navigation of document hierarchies.
+// The underlying implementation may use a map or a slice, but callers should rely on the interface methods.
+func (sb *SegmentBase) EdgeList() EdgeList {
+ return sb.nstIndexCache.edgeList()
+}
+
+// Utility method to count the number of nested documents in the segment, not exported.
+func (sb *SegmentBase) countNested() uint64 {
+ return sb.nstIndexCache.countNested()
+}
+
+func (sb *SegmentBase) CallbackId() string {
+ return sb.fileReader.id
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/sizes.go b/vendor/github.com/blevesearch/zapx/v17/sizes.go
new file mode 100644
index 0000000000..34166ea330
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/sizes.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2020 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "reflect"
+)
+
+func init() {
+ var b bool
+ SizeOfBool = int(reflect.TypeOf(b).Size())
+ var f32 float32
+ SizeOfFloat32 = int(reflect.TypeOf(f32).Size())
+ var f64 float64
+ SizeOfFloat64 = int(reflect.TypeOf(f64).Size())
+ var i int
+ SizeOfInt = int(reflect.TypeOf(i).Size())
+ var m map[int]int
+ SizeOfMap = int(reflect.TypeOf(m).Size())
+ var ptr *int
+ SizeOfPtr = int(reflect.TypeOf(ptr).Size())
+ var slice []int
+ SizeOfSlice = int(reflect.TypeOf(slice).Size())
+ var str string
+ SizeOfString = int(reflect.TypeOf(str).Size())
+ var u8 uint8
+ SizeOfUint8 = int(reflect.TypeOf(u8).Size())
+ var u16 uint16
+ SizeOfUint16 = int(reflect.TypeOf(u16).Size())
+ var u32 uint32
+ SizeOfUint32 = int(reflect.TypeOf(u32).Size())
+ var u64 uint64
+ SizeOfUint64 = int(reflect.TypeOf(u64).Size())
+}
+
+var SizeOfBool int
+var SizeOfFloat32 int
+var SizeOfFloat64 int
+var SizeOfInt int
+var SizeOfMap int
+var SizeOfPtr int
+var SizeOfSlice int
+var SizeOfString int
+var SizeOfUint8 int
+var SizeOfUint16 int
+var SizeOfUint32 int
+var SizeOfUint64 int
diff --git a/vendor/github.com/blevesearch/zapx/v17/synonym_cache.go b/vendor/github.com/blevesearch/zapx/v17/synonym_cache.go
new file mode 100644
index 0000000000..4c0e872e37
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/synonym_cache.go
@@ -0,0 +1,141 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "fmt"
+ "sync"
+
+ "github.com/blevesearch/vellum"
+)
+
+func newSynonymIndexCache() *synonymIndexCache {
+ return &synonymIndexCache{
+ cache: make(map[uint16]*synonymCacheEntry),
+ }
+}
+
+type synonymIndexCache struct {
+ m sync.RWMutex
+
+ cache map[uint16]*synonymCacheEntry
+}
+
+// Clear clears the synonym cache which would mean tha the termID to term map would no longer be available.
+func (sc *synonymIndexCache) Clear() {
+ sc.m.Lock()
+ sc.cache = nil
+ sc.m.Unlock()
+}
+
+// loadOrCreate loads the synonym index cache for the specified fieldID if it is already present,
+// or creates it if not. The synonym index cache for a fieldID consists of a tuple:
+// - A Vellum FST (Finite State Transducer) representing the thesaurus.
+// - A map associating synonym IDs to their corresponding terms.
+// This function returns the loaded or newly created tuple (FST and map).
+func (sc *synonymIndexCache) loadOrCreate(fieldID uint16, mem []byte, r *FileReader) (*vellum.FST, map[uint32][]byte, uint64, error) {
+ sc.m.RLock()
+ entry, ok := sc.cache[fieldID]
+ if ok {
+ sc.m.RUnlock()
+ return entry.load()
+ }
+
+ sc.m.RUnlock()
+
+ sc.m.Lock()
+ defer sc.m.Unlock()
+
+ entry, ok = sc.cache[fieldID]
+ if ok {
+ return entry.load()
+ }
+
+ return sc.createAndCacheLOCKED(fieldID, mem, r)
+}
+
+// createAndCacheLOCKED creates the synonym index cache for the specified fieldID and caches it.
+func (sc *synonymIndexCache) createAndCacheLOCKED(fieldID uint16, mem []byte, r *FileReader) (*vellum.FST, map[uint32][]byte, uint64, error) {
+ var pos uint64
+ vellumLen, read := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ if vellumLen == 0 || read <= 0 {
+ return nil, nil, 0, fmt.Errorf("vellum length is 0")
+ }
+ pos += uint64(read)
+ fstBytes, err := r.process(mem[pos : pos+vellumLen])
+ if err != nil {
+ return nil, nil, 0, err
+ }
+ fst, err := vellum.Load(fstBytes)
+ if err != nil {
+ return nil, nil, 0, fmt.Errorf("vellum err: %v", err)
+ }
+ pos += vellumLen
+ numSyns, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ if numSyns == 0 {
+ return nil, nil, 0, fmt.Errorf("no synonyms found")
+ }
+ mapLen, n := binary.Uvarint(mem[pos : pos+binary.MaxVarintLen64])
+ pos += uint64(n)
+ if mapLen == 0 {
+ return nil, nil, 0, fmt.Errorf("synonym term map length is 0")
+ }
+ buf, err := r.process(mem[pos : pos+mapLen])
+ if err != nil {
+ return nil, nil, 0, err
+ }
+ pos += mapLen
+ bufLen := uint64(len(buf))
+ var bufPos uint64
+ synTermMap := make(map[uint32][]byte, numSyns)
+ for i := 0; i < int(numSyns); i++ {
+ synID, n := binary.Uvarint(buf[bufPos:min(bufPos+binary.MaxVarintLen64, bufLen)])
+ bufPos += uint64(n)
+ termLen, n := binary.Uvarint(buf[bufPos:min(bufPos+binary.MaxVarintLen64, bufLen)])
+ bufPos += uint64(n)
+ if termLen == 0 {
+ return nil, nil, 0, fmt.Errorf("term length is 0")
+ }
+ term := buf[bufPos : bufPos+uint64(termLen)]
+ bufPos += uint64(termLen)
+ synTermMap[uint32(synID)] = term
+ }
+ sc.insertLOCKED(fieldID, fst, synTermMap)
+ return fst, synTermMap, pos, nil
+}
+
+// insertLOCKED inserts the vellum FST and the map of synonymID to term into the cache for the specified fieldID.
+func (sc *synonymIndexCache) insertLOCKED(fieldID uint16, fst *vellum.FST, synTermMap map[uint32][]byte) {
+ _, ok := sc.cache[fieldID]
+ if !ok {
+ sc.cache[fieldID] = &synonymCacheEntry{
+ fst: fst,
+ synTermMap: synTermMap,
+ }
+ }
+}
+
+// synonymCacheEntry is a tuple of the vellum FST and the map of synonymID to term,
+// and is the value stored in the synonym cache, for a given fieldID.
+type synonymCacheEntry struct {
+ fst *vellum.FST
+ synTermMap map[uint32][]byte
+}
+
+func (ce *synonymCacheEntry) load() (*vellum.FST, map[uint32][]byte, uint64, error) {
+ return ce.fst, ce.synTermMap, 0, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/synonym_posting.go b/vendor/github.com/blevesearch/zapx/v17/synonym_posting.go
new file mode 100644
index 0000000000..9424dd5643
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/synonym_posting.go
@@ -0,0 +1,242 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "reflect"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ "github.com/RoaringBitmap/roaring/v2/roaring64"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+)
+
+var reflectStaticSizeSynonymsList int
+var reflectStaticSizeSynonymsIterator int
+var reflectStaticSizeSynonym int
+
+func init() {
+ var sl SynonymsList
+ reflectStaticSizeSynonymsList = int(reflect.TypeOf(sl).Size())
+ var si SynonymsIterator
+ reflectStaticSizeSynonymsIterator = int(reflect.TypeOf(si).Size())
+ var s Synonym
+ reflectStaticSizeSynonym = int(reflect.TypeOf(s).Size())
+}
+
+// SynonymsList represents a list of synonyms for a term, stored in a Roaring64 bitmap.
+type SynonymsList struct {
+ sb *SegmentBase
+ synonymsOffset uint64
+ synonyms *roaring64.Bitmap
+ except *roaring.Bitmap
+
+ synIDTermMap map[uint32][]byte
+
+ buffer *bytes.Reader
+}
+
+// immutable, empty synonyms list
+var emptySynonymsList = &SynonymsList{}
+
+func (p *SynonymsList) Size() int {
+ sizeInBytes := reflectStaticSizeSynonymsList + SizeOfPtr
+
+ if p.except != nil {
+ sizeInBytes += int(p.except.GetSizeInBytes())
+ }
+
+ return sizeInBytes
+}
+
+// Iterator creates and returns a SynonymsIterator for the SynonymsList.
+// If the synonyms bitmap is nil, it returns an empty iterator.
+func (s *SynonymsList) Iterator(prealloc segment.SynonymsIterator) segment.SynonymsIterator {
+ if s.synonyms == nil {
+ return emptySynonymsIterator
+ }
+
+ var preallocSI *SynonymsIterator
+ pi, ok := prealloc.(*SynonymsIterator)
+ if ok && pi != nil {
+ preallocSI = pi
+ }
+ if preallocSI == emptySynonymsIterator {
+ preallocSI = nil
+ }
+
+ return s.iterator(preallocSI)
+}
+
+// iterator initializes a SynonymsIterator for the SynonymsList and returns it.
+// If a preallocated iterator is provided, it resets and reuses it; otherwise, it creates a new one.
+func (s *SynonymsList) iterator(rv *SynonymsIterator) *SynonymsIterator {
+ if rv == nil {
+ rv = &SynonymsIterator{}
+ } else {
+ *rv = SynonymsIterator{} // clear the struct
+ }
+ rv.synonyms = s
+ rv.except = s.except
+ rv.Actual = s.synonyms.Iterator()
+ rv.ActualBM = s.synonyms
+ rv.synIDTermMap = s.synIDTermMap
+ return rv
+}
+
+// read initializes a SynonymsList by reading data from the given synonymsOffset in the Thesaurus.
+// It reads and parses the Roaring64 bitmap that represents the synonyms.
+func (rv *SynonymsList) read(synonymsOffset uint64, t *Thesaurus) error {
+ rv.synonymsOffset = synonymsOffset
+
+ var n uint64
+ var read int
+
+ var synonymsLen uint64
+ synonymsLen, read = binary.Uvarint(t.sb.mem[synonymsOffset+n : synonymsOffset+n+binary.MaxVarintLen64])
+ n += uint64(read)
+
+ roaringBytes, err := t.sb.fileReader.process(t.sb.mem[synonymsOffset+n : synonymsOffset+n+synonymsLen])
+ if err != nil {
+ return err
+ }
+
+ if rv.synonyms == nil {
+ rv.synonyms = roaring64.NewBitmap()
+ }
+
+ rv.buffer.Reset(roaringBytes)
+
+ _, err = rv.synonyms.ReadFrom(rv.buffer)
+ if err != nil {
+ return fmt.Errorf("error loading roaring bitmap: %v", err)
+ }
+
+ return nil
+}
+
+// -----------------------------------------------------------------------------
+
+// SynonymsIterator provides a way to iterate through the synonyms list.
+type SynonymsIterator struct {
+ synonyms *SynonymsList
+ except *roaring.Bitmap
+
+ Actual roaring64.IntPeekable64
+ ActualBM *roaring64.Bitmap
+
+ synIDTermMap map[uint32][]byte
+ nextSyn Synonym
+}
+
+// immutable, empty synonyms iterator
+var emptySynonymsIterator = &SynonymsIterator{}
+
+func (i *SynonymsIterator) Size() int {
+ sizeInBytes := reflectStaticSizeSynonymsIterator + SizeOfPtr +
+ i.nextSyn.Size()
+
+ return sizeInBytes
+}
+
+// Next returns the next Synonym in the iteration or an error if the end is reached.
+func (i *SynonymsIterator) Next() (segment.Synonym, error) {
+ return i.next()
+}
+
+// next retrieves the next synonym from the iterator, populates the nextSyn field,
+// and returns it. If no valid synonym is found, it returns an error.
+func (i *SynonymsIterator) next() (segment.Synonym, error) {
+ synID, docNum, exists, err := i.nextSynonym()
+ if err != nil || !exists {
+ return nil, err
+ }
+
+ if i.synIDTermMap == nil {
+ return nil, fmt.Errorf("synIDTermMap is nil")
+ }
+
+ // If the synonymID is not found in the map, return an error
+ term, exists := i.synIDTermMap[synID]
+ if !exists {
+ return nil, fmt.Errorf("synonymID %d not found in map", synID)
+ }
+
+ i.nextSyn = Synonym{} // clear the struct
+ rv := &i.nextSyn
+ rv.term = string(term)
+ rv.docNum = docNum
+
+ return rv, nil
+}
+
+// nextSynonym decodes the next synonym from the roaring bitmap iterator,
+// ensuring it is not in the "except" set. Returns the synonymID, docNum,
+// and a flag indicating success.
+func (i *SynonymsIterator) nextSynonym() (uint32, uint32, bool, error) {
+ // If no synonyms are available, return early
+ if i.Actual == nil || i.synonyms == nil || i.synonyms == emptySynonymsList {
+ return 0, 0, false, nil
+ }
+
+ var code uint64
+ var docNum uint32
+ var synID uint32
+
+ // Loop to find the next valid docNum, checking against the except
+ for i.Actual.HasNext() {
+ code = i.Actual.Next()
+ synID, docNum = decodeSynonym(code)
+
+ // If docNum is not in the 'except' set, it's a valid result
+ if i.except == nil || !i.except.Contains(docNum) {
+ return synID, docNum, true, nil
+ }
+ }
+
+ // If no valid docNum is found, return false
+ return 0, 0, false, nil
+}
+
+// Synonym represents a single synonym, containing the term, synonymID, and document number.
+type Synonym struct {
+ term string
+ docNum uint32
+}
+
+// Size returns the memory size of the Synonym, including the length of the term string.
+func (p *Synonym) Size() int {
+ sizeInBytes := reflectStaticSizeSynonym + SizeOfPtr +
+ len(p.term)
+
+ return sizeInBytes
+}
+
+// Term returns the term of the Synonym.
+func (s *Synonym) Term() string {
+ return s.term
+}
+
+// Number returns the document number of the Synonym.
+func (s *Synonym) Number() uint32 {
+ return s.docNum
+}
+
+// decodeSynonym decodes a synonymCode into its synonymID and document ID components.
+func decodeSynonym(synonymCode uint64) (synonymID uint32, docID uint32) {
+ return uint32(synonymCode >> 32), uint32(synonymCode)
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/thesaurus.go b/vendor/github.com/blevesearch/zapx/v17/thesaurus.go
new file mode 100644
index 0000000000..f97aaf293b
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/thesaurus.go
@@ -0,0 +1,161 @@
+// Copyright (c) 2024 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "bytes"
+ "fmt"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+ segment "github.com/blevesearch/scorch_segment_api/v2"
+ "github.com/blevesearch/vellum"
+)
+
+// Thesaurus is the zap representation of a Thesaurus
+type Thesaurus struct {
+ sb *SegmentBase
+ name string
+ fieldID uint16
+ synIDTermMap map[uint32][]byte
+ fst *vellum.FST
+
+ fstReader *vellum.Reader
+
+ bytesRead uint64
+}
+
+// represents an immutable, empty Thesaurus
+var emptyThesaurus = &Thesaurus{}
+
+// SynonymsList returns the synonyms list for the specified term
+func (t *Thesaurus) SynonymsList(term []byte, except *roaring.Bitmap, prealloc segment.SynonymsList) (segment.SynonymsList, error) {
+ var preallocSL *SynonymsList
+ sl, ok := prealloc.(*SynonymsList)
+ if ok && sl != nil {
+ preallocSL = sl
+ }
+ return t.synonymsList(term, except, preallocSL)
+}
+
+func (t *Thesaurus) synonymsList(term []byte, except *roaring.Bitmap, rv *SynonymsList) (*SynonymsList, error) {
+ if t.fstReader == nil {
+ if rv == nil || rv == emptySynonymsList {
+ return emptySynonymsList, nil
+ }
+ return t.synonymsListInit(rv, except), nil
+ }
+
+ synonymsOffset, exists, err := t.fstReader.Get(term)
+
+ if err != nil {
+ return nil, fmt.Errorf("vellum err: %v", err)
+ }
+ if !exists {
+ if rv == nil || rv == emptySynonymsList {
+ return emptySynonymsList, nil
+ }
+ return t.synonymsListInit(rv, except), nil
+ }
+
+ return t.synonymsListFromOffset(synonymsOffset, except, rv)
+}
+
+func (t *Thesaurus) synonymsListFromOffset(synonymsOffset uint64, except *roaring.Bitmap, rv *SynonymsList) (*SynonymsList, error) {
+ rv = t.synonymsListInit(rv, except)
+
+ err := rv.read(synonymsOffset, t)
+ if err != nil {
+ return nil, err
+ }
+
+ return rv, nil
+}
+
+func (t *Thesaurus) synonymsListInit(rv *SynonymsList, except *roaring.Bitmap) *SynonymsList {
+ if rv == nil || rv == emptySynonymsList {
+ rv = &SynonymsList{}
+ rv.buffer = bytes.NewReader(nil)
+ } else {
+ synonyms := rv.synonyms
+ buf := rv.buffer
+ if synonyms != nil {
+ synonyms.Clear()
+ }
+ if buf != nil {
+ buf.Reset(nil)
+ }
+
+ *rv = SynonymsList{} // clear the struct
+
+ rv.synonyms = synonyms
+ rv.buffer = buf
+ }
+ rv.sb = t.sb
+ rv.except = except
+ rv.synIDTermMap = t.synIDTermMap
+ return rv
+}
+
+func (t *Thesaurus) Contains(key []byte) (bool, error) {
+ if t.fst != nil {
+ return t.fst.Contains(key)
+ }
+ return false, nil
+}
+
+// AutomatonIterator returns an iterator which only visits terms
+// having the the vellum automaton and start/end key range
+func (t *Thesaurus) AutomatonIterator(a segment.Automaton,
+ startKeyInclusive, endKeyExclusive []byte) segment.ThesaurusIterator {
+ if t.fst != nil {
+ rv := &ThesaurusIterator{
+ t: t,
+ }
+
+ itr, err := t.fst.Search(a, startKeyInclusive, endKeyExclusive)
+ if err == nil {
+ rv.itr = itr
+ } else if err != vellum.ErrIteratorDone {
+ rv.err = err
+ }
+
+ return rv
+ }
+ return emptyThesaurusIterator
+}
+
+var emptyThesaurusIterator = &ThesaurusIterator{}
+
+// ThesaurusIterator is an iterator for term dictionary
+type ThesaurusIterator struct {
+ t *Thesaurus
+ itr vellum.Iterator
+ err error
+ entry index.ThesaurusEntry
+}
+
+// Next returns the next entry in the dictionary
+func (i *ThesaurusIterator) Next() (*index.ThesaurusEntry, error) {
+ if i.err != nil && i.err != vellum.ErrIteratorDone {
+ return nil, i.err
+ } else if i.itr == nil || i.err == vellum.ErrIteratorDone {
+ return nil, nil
+ }
+ term, _ := i.itr.Current()
+ i.entry.Term = string(term)
+ i.err = i.itr.Next()
+ return &i.entry, nil
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/write.go b/vendor/github.com/blevesearch/zapx/v17/write.go
new file mode 100644
index 0000000000..10065134ac
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/write.go
@@ -0,0 +1,192 @@
+// Copyright (c) 2017 Couchbase, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package zap
+
+import (
+ "encoding/binary"
+ "io"
+
+ "github.com/RoaringBitmap/roaring/v2"
+ index "github.com/blevesearch/bleve_index_api"
+)
+
+// writes out the length of the roaring bitmap in bytes as varint
+// then writes out the roaring bitmap itself
+func writeRoaringWithLen(r *roaring.Bitmap, w io.Writer,
+ reuseBufVarint []byte) (int, error) {
+ buf, err := r.ToBytes()
+ if err != nil {
+ return 0, err
+ }
+
+ var tw int
+
+ if fw, ok := w.(*FileWriter); ok && fw != nil {
+ buf = fw.process(buf)
+ }
+
+ // write out the length
+ n := binary.PutUvarint(reuseBufVarint, uint64(len(buf)))
+ nw, err := w.Write(reuseBufVarint[:n])
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+
+ // write out the roaring bytes
+ nw, err = w.Write(buf)
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+
+ return tw, nil
+}
+
+func persistFieldsSection(fieldsInv []string,
+ fieldsOptions map[string]index.FieldIndexingOptions, w *FileWriter,
+ opaque map[int]resetable) (uint64, error) {
+ var rv uint64
+ fieldsOffsets := make([]uint64, 0, len(fieldsInv))
+
+ for fieldID, fieldName := range fieldsInv {
+ // record start of this field
+ fieldsOffsets = append(fieldsOffsets, uint64(w.Count()))
+ fieldOpts := fieldsOptions[fieldName]
+ fieldName := w.process([]byte(fieldName))
+
+ // write field name length
+ _, err := writeUvarints(w, uint64(len(fieldName)))
+ if err != nil {
+ return 0, err
+ }
+
+ // write out the field name
+ _, err = w.Write(fieldName)
+ if err != nil {
+ return 0, err
+ }
+
+ // write out the field options
+ _, err = writeUvarints(w, uint64(fieldOpts))
+ if err != nil {
+ return 0, err
+ }
+
+ // write out the number of field-specific indexes
+ _, err = writeUvarints(w, uint64(len(segmentSections)))
+ if err != nil {
+ return 0, err
+ }
+
+ // now write pairs of index section ids, and start addresses for each field
+ // which has a specific section's data. this serves as the starting point
+ // using which a field's section data can be read and parsed.
+ for segmentSectionType, segmentSectionImpl := range segmentSections {
+ binary.Write(w, binary.BigEndian, segmentSectionType)
+ binary.Write(w, binary.BigEndian, uint64(segmentSectionImpl.AddrForField(opaque, fieldID)))
+ }
+ }
+
+ rv = uint64(w.Count())
+ // write out number of fields
+ _, err := writeUvarints(w, uint64(len(fieldsInv)))
+ if err != nil {
+ return 0, err
+ }
+ // now write out the fields index
+ for fieldID := range fieldsInv {
+ err := binary.Write(w, binary.BigEndian, fieldsOffsets[fieldID])
+ if err != nil {
+ return 0, err
+ }
+ }
+
+ return rv, nil
+}
+
+// FooterSize is the size of the footer record in bytes
+// crc + id length + ver + chunk + sectionsIndexOffset + stored offset + num docs
+// Does not include the length of the id because it is variable length
+const FooterSize = 4 + 4 + 4 + 4 + 8 + 8 + 8
+
+func persistFooter(numDocs, storedIndexOffset, sectionsIndexOffset uint64,
+ chunkMode, crcBeforeFooter uint32, writerIn io.Writer, fileWriterID string) error {
+ w := NewCountHashWriter(writerIn)
+ w.crc = crcBeforeFooter
+
+ // Write the writer id
+ _, err := w.Write([]byte(fileWriterID))
+ if err != nil {
+ return err
+ }
+
+ // Write the length of the writer id
+ err = binary.Write(w, binary.BigEndian, uint32(len(fileWriterID)))
+ if err != nil {
+ return err
+ }
+
+ // write out the number of docs
+ err = binary.Write(w, binary.BigEndian, numDocs)
+ if err != nil {
+ return err
+ }
+
+ // write out the stored field index location:
+ err = binary.Write(w, binary.BigEndian, storedIndexOffset)
+ if err != nil {
+ return err
+ }
+
+ // write out the sections index location
+ err = binary.Write(w, binary.BigEndian, sectionsIndexOffset)
+ if err != nil {
+ return err
+ }
+
+ // write out 32-bit chunk factor
+ err = binary.Write(w, binary.BigEndian, chunkMode)
+ if err != nil {
+ return err
+ }
+
+ // write out 32-bit version
+ err = binary.Write(w, binary.BigEndian, Version)
+ if err != nil {
+ return err
+ }
+
+ // write out CRC-32 of everything upto but not including this CRC
+ err = binary.Write(w, binary.BigEndian, w.crc)
+ if err != nil {
+ return err
+ }
+ return nil
+}
+
+func writeUvarints(w io.Writer, vals ...uint64) (tw int, err error) {
+ buf := make([]byte, binary.MaxVarintLen64)
+ for _, val := range vals {
+ n := binary.PutUvarint(buf, val)
+ var nw int
+ nw, err = w.Write(buf[:n])
+ tw += nw
+ if err != nil {
+ return tw, err
+ }
+ }
+ return tw, err
+}
diff --git a/vendor/github.com/blevesearch/zapx/v17/zap.md b/vendor/github.com/blevesearch/zapx/v17/zap.md
new file mode 100644
index 0000000000..f082dc8d60
--- /dev/null
+++ b/vendor/github.com/blevesearch/zapx/v17/zap.md
@@ -0,0 +1,338 @@
+# ZAP File Format
+
+## Legend
+
+### File Sections
+
+ |========|
+ | | file section
+ |========|
+
+### Fixed-size fields
+
+ |--------| |----| |--| |-|
+ | | uint64 | | uint32 | | uint16 | | uint8
+ |--------| |----| |--| |-|
+
+### Varints
+
+ |~~~~~~~~|
+ | | varint(up to uint64)
+ |~~~~~~~~|
+
+### Arbitrary-length fields
+
+ |--------...---|
+ | | arbitrary-length field (string, vellum, roaring bitmap)
+ |--------...---|
+
+### Chunked data
+
+ [--------]
+ [ ]
+ [--------]
+
+## Overview
+
+Footer section describes the configuration of particular ZAP file. The format of footer is version-dependent, so it is necessary to check `V` field before the parsing.
+
+ +=================================================================+
+ | Stored Fields |
+ |=================================================================|
+ +-----> | Stored Fields Index |
+ | |=================================================================|
+ | | Inverted Text Index Section |
+ | |=================================================================|
+ | | Vector Index Section |
+ | |=================================================================|
+ | | Synonym Index Section |
+ | |=================================================================|
+ | | Sections Info |
+ | |=================================================================|
+ | +-> | Sections Index |
+ | | |==..==+=======+======+======+=====+======+=====+======+==========|
+ | | | ID | IDL | D# | SF | S | CF | V | CC | (Footer) |
+ | | +==..==+=======+======+======+=====+======+=====+======+==========+
+ | | | |
+ +---------------------------------+ |
+ | |
+ +-----------------------------------+
+
+ ID. ID of the Writer Used.
+ IDL. Length of the Writer ID.
+ D#. Number of Docs.
+ SF. Stored Fields Index Offset.
+ S. Sections Index Offset
+ CF. Chunk Factor.
+ V. Version.
+ CC. CRC32.
+
+## Stored Fields
+
+Stored Fields Index is `D#` consecutive 64-bit unsigned integers - offsets, where relevant Stored Fields Data records are located.
+We also store the EdgeList for nested documents, if present in the segment, to preserve hierarchical relationships.
+If there are NE edges, it means there are NE nested or sub-documents, with each edge representing a child -> parent relationship.
+
+ 0 [SF] [SF + D# * 8]
+ | Stored Fields | Stored Fields Index | Edge List Information |
+ |================================|==================================|========================================================================|
+ | | | |
+ | |--------------------| ||--------|--------|. . .|--------|||~~~~~~~~|~~~~~~~~|~~~~~~~~|~~~~~~~~|~~~~~~~~|. . .|~~~~~~~~~|~~~~~~~~~||
+ | |-> | Stored Fields Data | || 0 | 1 | | D# - 1 ||| NE | C1 | P1 | C2 | P2 | | CNE | PNE ||
+ | | |--------------------| ||--------|----|---|. . .|--------|||~~~~~~~~|~~~~~~~~|~~~~~~~~|~~~~~~~~|~~~~~~~~|. . .|~~~~~~~~~|~~~~~~~~~||
+ | | | | | |
+ |===|============================|==============|===================|========================================================================|
+
+ NE. Number of edges in the edge list.
+ Ci. Child Document Number for edge i.
+ Pi. Parent Document Number for edge i.
+
+Stored Fields Data is an arbitrary size record, which consists of metadata and [Snappy](https://github.com/golang/snappy)-compressed data.
+
+ Stored Fields Data
+ |~~~~~~~~|~~~~~~~~|~~~~~~~~...~~~~~~~~|~~~~~~~~...~~~~~~~~|
+ | MDS | CDS | MD | CD |
+ |~~~~~~~~|~~~~~~~~|~~~~~~~~...~~~~~~~~|~~~~~~~~...~~~~~~~~|
+
+ MDS. Metadata size.
+ CDS. Compressed data size.
+ MD. Metadata.
+ CD. Snappy-compressed data.
+
+## Index Sections
+
+Sections Index is a set of NF uint64 addresses (0 through F# - 1) each of which are offsets to the records in the Sections Info. Inside the sections info, we have further offsets to specific type of index section for that particular field in the segment file. For example, field 0 may correspond to Vector Indexing and its records would have offsets to the Vector Index Section whereas a field 1 may correspond to Text Indexing and its records would rather point to somewhere within the Inverted Text Index Section.
+
+ (...) [F] [F + F#]
+ + Sections Info + Sections Index +
+ |===========================================================================|=================================|
+ | | |
+ | +--------+------+---+----+---------+---------+~~~~~+--+...+--+~~~~~~~~~+ | +------+------+...+------+----+ |
+ +---->| Length | Name | O | NS | S1 Type | S1 Addr | ... | Sn Type | Sn Addr | | | 0 | 1 | | F#-1 | NF | |
+ | | +--------+------+---+----+---------+---------+~~~~~+--+...+--+~~~~~~~~~+ | +------+----+-+...+------+----+ |
+ | | | | |
+ | +===========================================================================+=============|===================+
+ | |
+ +--------------------------------------------------------------------------------------------+
+
+ NF. Number of fields
+ NS. Number of index sections
+ O. Field Indexing Options
+ Sn. nth index section
+
+## Inverted Text Index Section
+
+Each field has its own types of indexes in separate sections as indicated above. This can be a vector index or inverted text index.
+
+In case of inverted text index, the dictionary is encoded in [Vellum](https://github.com/couchbase/vellum) format. Dictionary consists of pairs `(term, offset)`, where `offset` indicates the position of postings (list of documents) for this particular term.
+
+ +================================================================+- Inverted Text
+ | | Index Section
+ | |
+ | Freq/Norm (chunked) |
+ | [~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~] |
+ | +->[ Freq | Norm (float32 under varint) ] |
+ | | [~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~] |
+ | | |
+ | +------------------------------------------------------------+ |
+ | Location Details (chunked) | |
+ | [~~~~~~+~~~~~+~~~~~~~+~~~~~+~~~~~~+~~~~~~~~+~~~~~] | |
+ | +->[ Size | Pos | Start | End | Arr# | ArrPos | ... ] | |
+ | | [~~~~~~+~~~~~+~~~~~~~+~~~~~+~~~~~~+~~~~~~~~+~~~~~] | |
+ | | | |
+ | +----------------------+ | |
+ | Postings List | | |
+ | +~~~~~~~~+~~~~~+~~+~~~~~~~~+----------+...+-+ | |
+ | +->+ F/N | LD | Length | ROARING BITMAP | | |
+ | | +~~~~~+~~|~~~~~~~~|~~~~~~~~+----------+...+-+ | |
+ | | +----------------------------------------------+ |
+ | +-------------------------------------------------+ |
+ | | |
+ | Dictionary | |
+ | +~~~~~~~~~~+~~~~~~~+~~~~~~~~+--------------------------+-...-+ |
+ +-----> DV Start | DV End| Length | VELLUM DATA : (TERM -> OFFSET) | |
+ | | +~~~~~~~~~~+~~~~~~~+~~~~~~~~+----------------------------...-+ |
+ | | |
+ | | |
+ | |================================================================+- Vector Index Section
+ | | |
+ | +================================================================+- Synonym Index Section
+ | | |
+ | |================================================================+- Sections Info
+ +-----------------------------+ |
+ | | |
+ | +-------+-----+-----+------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ | | ... | ITI | ITI ADDR | NS | Length | Name | |
+ | +-------+-----+------------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ +================================================================+
+
+
+ ITI - Inverted Text Index
+
+## Vector Index Section
+
+In a vector index, each vector is assigned a unique, monotonically increasing ID ranging from `0` to `N-1`, where `N` is the total number of vectors in the index. This ID is used internally by the [Faiss](https://github.com/blevesearch/faiss) index. Each vector ID maps to a document ID within the segment, and this mapping is stored as an array of size `N`.
+
+ |================================================================+- Inverted Text Index Section
+ | |
+ |================================================================+- Vector Index Section
+ | |
+ | +~~~~~~~~~~+~~~~~~~~+~~~~~+~~~~~~+~~~~~~+ |
+ +-------> DV Start | DV End | VIO | NVEC | ML | |
+ | | +~~~~~~~~~~+~~~~~~~~+~~~~~+~~~~~~+~~~~~~+ |
+ | | |
+ | | +~~~~~~~~~~~~~+ |
+ | | | DocID_1 | |
+ | | +~~~~~~~~~~~~~+ |
+ | | | DocID_2 | |
+ | | +~~~~~~~~~~~~~+ |
+ | | | ... | |
+ | | +~~~~~~~~~~~~~+ |
+ | | | DocID_N | |
+ | | +~~~~~~~~~~~~~+ |
+ | | |
+ | | +~~~~~~~~~~~~~+ |
+ | | | INDEX TYPE | |
+ | | +~~~~~~~~~~~~~+ |
+ | | +~~~~~~~~~~~~~+ |
+ | | | INDEX DATA | |
+ | | +~~~~~~~~~~~~~+ |
+ | | |
+ | |================================================================+- Synonym Index Section
+ | | |
+ | |================================================================+- Sections Info
+ +-----------------------------+ |
+ | | |
+ | +-------+-----+-----+------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ | | ... | VI | VI ADDR | NS | Length | Name | |
+ | +-------+-----+------------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ +================================================================+
+
+ VI - Vector Index
+ VIO - Vector Index Optimized for
+ NVEC - Number of vectors
+ ML - Length of the vector to document ID map
+ INDEX TYPE - Type of the vector index
+ INDEX DATA - Vector index data
+
+### Vector Index Type - FP32
+
+FP32 vector indexes stores a singular FAISS index to perform search
+
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | | FAISS LEN | |
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | |
+ | +----------+...+-----------+ |
+ | | SERIALIZED FAISS INDEX | |
+ | +----------+...+-----------+ |
+
+ FAISS LEN - Length of the serialized faiss index
+
+### Vector Index Type - Binary
+
+Binary vector indexes stores two separate FAISS indexes to perform search. The first is a primary binary index and the second is a backing FP32 index
+
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | | PRIMARY FAISS LEN | |
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | |
+ | +---------------+...+--------------+ |
+ | | SERIALIZED PRIMARY FAISS INDEX | |
+ | +---------------+...+--------------+ |
+ | |
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | | BACKING FAISS LEN | |
+ | +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+ |
+ | |
+ | +---------------+...+--------------+ |
+ | | SERIALIZED BACKING FAISS INDEX | |
+ | +---------------+...+--------------+ |
+
+ PRIMARY FAISS LEN - Length of the serialized primary faiss index
+ BACKING FAISS LEN - Length of the serialized backing faiss index
+
+## Synonym Index Section
+
+In a synonyms index, the relationship between a term and its synonyms is represented using a Thesaurus. The Thesaurus is encoded in the [Vellum](https://github.com/couchbase/vellum) format and consists of pairs in the form `(term, offset)`. Here, the offset specifies the position of the postings list containing the synonyms for the given term. The postings list is stored as a Roaring64 bitmap, with each entry representing an encoded synonym for the term.
+
+ |================================================================+- Inverted Text Index Section
+ | |
+ |================================================================+- Vector Index Section
+ | |
+ +================================================================+- Synonym Index Section
+ | |
+ | (Offset) +~~~~~+----------+...+---+ |
+ | +--------->| RL | ROARING64 BITMAP | |
+ | | +~~~~~+----------+...+---+ +------------------------+
+ | |(Term -> Offset) |
+ | | |
+ | +--------+ |
+ | | Term ID to Term map (NST Entries) |
+ | +~~~~+~~~~+~~~~~+~~~~[{~~~~~+~~~~+~~~~~~}{~~~~~+~~~~+~~~~~~}...{~~~~~+~~~~+~~~~~~}] |
+ | +->| VL | VD | NST | ML || TID | TL | Term || TID | TL | Term | | TID | TL | Term | |
+ | | +~~~~+~~~~+~~~~~+~~~~[{~~~~~+~~~~+~~~~~~}{~~~~~+~~~~+~~~~~~}...{~~~~~+~~~~+~~~~~~}] |
+ | | |
+ | +----------------------------+ |
+ | | |
+ | +~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~+ |
+ +-----> DV Start | DV End | ThesaurusOffset | |
+ | | +~~~~~~~~~~+~~~~~~~~+~~~~~~~~~~~~~~~~~+ +------------------------+
+ | | |
+ | | |
+ | |================================================================+- Sections Info
+ +-----------------------------+ |
+ | | |
+ | +-------+-----+-----+------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ | | ... | SI | SI ADDR | NS | Length | Name | |
+ | +-------+-----+------------+~~~~~~~~+~~~~~~~~+--+...+--+ |
+ +================================================================+
+
+ SI - Synonym Index
+ VL - Vellum Length
+ VD - Vellum Data (Term -> Offset)
+ RL - Roaring64 Length
+ NST - Number of entries in the term ID to term map
+ ML - Length of the term ID to term map
+ TID - Term ID (32-bit)
+ TL - Term Length
+
+### Synonym Encoding
+
+ ROARING64 BITMAP
+
+ Each 64-bit entry consists of two parts: the first 32 bits represent the Term ID (TID),
+ and the next 32 bits represent the Document Number (DN).
+
+ [{~~~~~+~~~~}{~~~~~+~~~~}...{~~~~~+~~~~}]
+ | TID | DN || TID | DN | | TID | DN |
+ [{~~~~~+~~~~}{~~~~~+~~~~}...{~~~~~+~~~~}]
+
+ TID - Term ID (32-bit)
+ DN - Document Number (32-bit)
+
+## Doc Values
+
+DocValue start and end offsets are stored within the section content of each field. This allows each field having its own type of index to choose whether to store the doc values or not. For example, it may not make sense to store doc values for vector indexing and so, the offsets can be invalid ones for it whereas the fields having text indexing may have valid doc values offsets.
+
+ +================================================================+
+ | +------...--+ |
+ | +->+ DocValues +<-+ |
+ | | +------...--+ | |
+ |==|=================|===========================================+- Inverted Text
+ ++~+~~~~~~~~~+~~~~~~~+~~+~~~~~~~~+-----------------------...--+ | Index Section
+ || DV START | DV END | LENGTH | VELLUM DATA: TERM -> OFFSET| |
+ ++~~~~~~~~~~~+~~~~~~~~~~+~~~~~~~~+-----------------------...--+ |
+ +================================================================+
+
+DocValues is chunked Snappy-compressed values for each document and field.
+
+ [~~~~~~~~~~~~~~~|~~~~~~|~~~~~~~~~|-...-|~~~~~~|~~~~~~~~~|--------------------...-]
+ [ Doc# in Chunk | Doc1 | Offset1 | ... | DocN | OffsetN | SNAPPY COMPRESSED DATA ]
+ [~~~~~~~~~~~~~~~|~~~~~~|~~~~~~~~~|-...-|~~~~~~|~~~~~~~~~|--------------------...-]
+
+Last 16 bytes are description of chunks.
+
+ |~~~~~~~~~~~~...~|----------------|----------------|
+ | Chunk Sizes | Chunk Size Arr | Chunk# |
+ |~~~~~~~~~~~~...~|----------------|----------------|
diff --git a/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go b/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go
index a3a45af317..1d22f0a891 100644
--- a/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go
+++ b/vendor/github.com/decred/dcrd/dcrec/secp256k1/v4/ellipticadaptor.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2022 The Decred developers
+// Copyright 2020-2026 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.
@@ -63,8 +63,8 @@ type KoblitzCurve struct {
// bigAffineToJacobian takes an affine point (x, y) as big integers and converts
// it to Jacobian point with Z=1.
func bigAffineToJacobian(x, y *big.Int, result *JacobianPoint) {
- result.X.SetByteSlice(x.Bytes())
- result.Y.SetByteSlice(y.Bytes())
+ result.X.SetByteSlice(new(big.Int).Mod(x, curveParams.P).Bytes())
+ result.Y.SetByteSlice(new(big.Int).Mod(y, curveParams.P).Bytes())
result.Z.SetInt(1)
}
@@ -91,6 +91,15 @@ func (curve *KoblitzCurve) Params() *elliptic.CurveParams {
//
// This is part of the elliptic.Curve interface implementation. This function
// differs from the crypto/elliptic algorithm since a = 0 not -3.
+//
+// NOTE: Unfortunately, the Go stdlib elliptic.Curve interface requires that the
+// conventional point at infinity (0, 0) is not considered on the curve which is
+// contrary to what is typically expected since the point at infinity is in fact
+// is a valid curve point.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func (curve *KoblitzCurve) IsOnCurve(x, y *big.Int) bool {
// Convert big ints to a Jacobian point for faster arithmetic.
var point JacobianPoint
@@ -101,6 +110,14 @@ func (curve *KoblitzCurve) IsOnCurve(x, y *big.Int) bool {
// Add returns the sum of (x1,y1) and (x2,y2).
//
// This is part of the elliptic.Curve interface implementation.
+//
+// NOTE: Per the documentation of the elliptic.Curve interface, the behavior
+// when the input is not a point on the curve is undefined. Callers must ensure
+// they are calling this method with valid points.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func (curve *KoblitzCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) {
// The point at infinity is the identity according to the group law for
// elliptic curve cryptography. Thus, ∞ + P = P and P + ∞ = P.
@@ -124,6 +141,14 @@ func (curve *KoblitzCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) {
// Double returns 2*(x1,y1).
//
// This is part of the elliptic.Curve interface implementation.
+//
+// NOTE: Per the documentation of the elliptic.Curve interface, the behavior
+// when the input is not a point on the curve is undefined. Callers must ensure
+// they are calling this method with valid points.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func (curve *KoblitzCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) {
if y1.Sign() == 0 {
return new(big.Int), new(big.Int)
@@ -156,6 +181,14 @@ func moduloReduce(k []byte) []byte {
// ScalarMult returns k*(bx, by) where k is a big endian integer.
//
// This is part of the elliptic.Curve interface implementation.
+//
+// NOTE: Per the documentation of the elliptic.Curve interface, the behavior
+// when the input is not a point on the curve is undefined. Callers must ensure
+// they are calling this method with valid points.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func (curve *KoblitzCurve) ScalarMult(bx, by *big.Int, k []byte) (*big.Int, *big.Int) {
// Convert the affine coordinates from big integers to Jacobian points,
// do the multiplication in Jacobian projective space, and convert the
@@ -172,6 +205,10 @@ func (curve *KoblitzCurve) ScalarMult(bx, by *big.Int, k []byte) (*big.Int, *big
// big endian integer.
//
// This is part of the elliptic.Curve interface implementation.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func (curve *KoblitzCurve) ScalarBaseMult(k []byte) (*big.Int, *big.Int) {
// Perform the multiplication and convert the Jacobian point back to affine
// big.Ints.
@@ -250,6 +287,10 @@ var secp256k1 = &KoblitzCurve{
}
// S256 returns an elliptic.Curve which implements secp256k1.
+//
+// Deprecated: The standard library elliptic.Curve interface is now deprecated
+// and callers should interact with the safer, and much faster, specialized
+// methods instead.
func S256() *KoblitzCurve {
return secp256k1
}
diff --git a/vendor/github.com/fsnotify/fsnotify/.cirrus.yml b/vendor/github.com/fsnotify/fsnotify/.cirrus.yml
deleted file mode 100644
index 7f257e99ac..0000000000
--- a/vendor/github.com/fsnotify/fsnotify/.cirrus.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-freebsd_task:
- name: 'FreeBSD'
- freebsd_instance:
- image_family: freebsd-14-2
- install_script:
- - pkg update -f
- - pkg install -y go
- test_script:
- # run tests as user "cirrus" instead of root
- - pw useradd cirrus -m
- - chown -R cirrus:cirrus .
- - FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- - sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- - FSNOTIFY_DEBUG=1 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race -v ./...
diff --git a/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md b/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md
index 6468d2cf40..3027f3c67a 100644
--- a/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md
+++ b/vendor/github.com/fsnotify/fsnotify/CHANGELOG.md
@@ -1,5 +1,54 @@
# Changelog
+1.10.1 2026-05-04
+-----------------
+
+### Changes and fixes
+
+- inotify: don't remove sibling watches sharing a path prefix ([#754])
+
+- inotify, windows: don't rename sibling watches sharing a path prefix
+ ([#755])
+
+
+[#754]: https://github.com/fsnotify/fsnotify/pull/754
+[#755]: https://github.com/fsnotify/fsnotify/pull/755
+
+
+1.10.0 2026-04-30
+-----------------
+This version of fsnotify needs Go 1.23.
+
+### Changes and fixes
+
+- inotify: improve initialization error message ([#731])
+
+- inotify: send Rename event if recursive watch is renamed ([#696])
+
+- inotify: avoid copying event buffers when reading names ([#741])
+
+- kqueue: skip dangling symlinks (ENOENT) in watchDirectoryFiles, so a
+ bad entry no longer aborts Watcher.Add for the whole directory ([#748])
+
+- kqueue: drop watches directly in Close() to fix a file descriptor leak
+ when recycling watchers ([#740])
+
+- windows: fix nil pointer dereference in remWatch ([#736])
+
+- windows: lock watch field updates against concurrent WatchList to fix
+ a race introduced in v1.9.0 ([#709], [#749])
+
+
+[#696]: https://github.com/fsnotify/fsnotify/pull/696
+[#709]: https://github.com/fsnotify/fsnotify/pull/709
+[#731]: https://github.com/fsnotify/fsnotify/pull/731
+[#736]: https://github.com/fsnotify/fsnotify/pull/736
+[#740]: https://github.com/fsnotify/fsnotify/pull/740
+[#741]: https://github.com/fsnotify/fsnotify/pull/741
+[#748]: https://github.com/fsnotify/fsnotify/pull/748
+[#749]: https://github.com/fsnotify/fsnotify/pull/749
+
+
1.9.0 2024-04-04
----------------
diff --git a/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md b/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md
index 4cc40fa597..cd0ee612da 100644
--- a/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md
+++ b/vendor/github.com/fsnotify/fsnotify/CONTRIBUTING.md
@@ -77,6 +77,8 @@ End-of-line escapes with `\` are not supported.
debug [yes/no] # Enable/disable FSNOTIFY_DEBUG (tests are run in
parallel by default, so -parallel=1 is probably a good
idea).
+ state # Print internal state to stderr (exact output differs
+ # per backend).
print [any strings] # Print text to stdout; for debugging.
touch path
diff --git a/vendor/github.com/fsnotify/fsnotify/README.md b/vendor/github.com/fsnotify/fsnotify/README.md
index 1f4eb583d5..2e56ef4c9a 100644
--- a/vendor/github.com/fsnotify/fsnotify/README.md
+++ b/vendor/github.com/fsnotify/fsnotify/README.md
@@ -1,7 +1,7 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, BSD, and illumos.
-Go 1.17 or newer is required; the full documentation is at
+Go 1.23 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify
---
@@ -12,7 +12,7 @@ Platform support:
| :-------------------- | :--------- | :------------------------------------------------------------------------ |
| inotify | Linux | Supported |
| kqueue | BSD, macOS | Supported |
-| ReadDirectoryChangesW | Windows | Supported |
+| ReadDirectoryChangesW | Windows | Supported ([excluding `Chmod` operations][#487]) |
| FEN | illumos | Supported |
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
@@ -22,6 +22,7 @@ Platform support:
Linux and illumos should include Android and Solaris, but these are currently
untested.
+[#487]: https://github.com/fsnotify/fsnotify/issues/487
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
@@ -126,7 +127,7 @@ settings* until we have a native FSEvents implementation (see [#11]).
### Watching a file doesn't work well
Watching individual files (rather than directories) is generally not recommended
as many programs (especially editors) update files atomically: it will write to
-a temporary file which is then moved to to destination, overwriting the original
+a temporary file which is then moved to a destination, overwriting the original
(or some variant thereof). The watcher on the original file is now lost, as that
no longer exists.
@@ -151,26 +152,57 @@ This is the event that inotify sends, so not much can be changed about this.
The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for
the number of watches per user, and `fs.inotify.max_user_instances` specifies
the maximum number of inotify instances per user. Every Watcher you create is an
-"instance", and every path you add is a "watch".
+"instance", and every path you add is a "watch". Reaching the limit will result
+in a "no space left on device" or "too many open files" error.
These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and
-`/proc/sys/fs/inotify/max_user_instances`
+`/proc/sys/fs/inotify/max_user_instances`. The default values differ per distro
+and available memory.
To increase them you can use `sysctl` or write the value to proc file:
- # The default values on Linux 5.18
- sysctl fs.inotify.max_user_watches=124983
- sysctl fs.inotify.max_user_instances=128
+ sysctl fs.inotify.max_user_watches=200000
+ sysctl fs.inotify.max_user_instances=256
To make the changes persist on reboot edit `/etc/sysctl.conf` or
`/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your
distro's documentation):
- fs.inotify.max_user_watches=124983
- fs.inotify.max_user_instances=128
+ fs.inotify.max_user_watches=200000
+ fs.inotify.max_user_instances=256
+
+### Windows
+Recursive watching is not currently enabled through fsnotify's public API
+(see the FAQ "Are subdirectories watched?" above). The notes below
+describe Windows backend behavior observed when recursive watching is
+enabled internally (for example, in fsnotify's own tests). They are kept
+here as a reference for maintainers and contributors who encounter the
+behavior, since the recursive code path still exists in the backend.
+
+When recursive watching is enabled and you watch a directory, you may
+receive a `Write` event for an intermediate directory whenever a child
+entry inside it is created, renamed, or removed. For example, with a
+recursive watch on `/a` and a new file `/a/b/c`, you will receive
+`Create /a/b/c` and may also receive `Write /a/b`.
+
+This happens because, on NTFS-backed volumes, modifying the entries of a
+directory updates that directory's last-write time, and the Windows
+backend requests `FILE_NOTIFY_CHANGE_LAST_WRITE` to support `Write` events
+on files. The same `Write` filter therefore picks up the directory's
+metadata update.
+
+kqueue has the same "directory `Write` = directory contents changed"
+semantics, so portable code that treats `Write` on a directory as
+"something inside it changed" works on Windows and BSD/macOS, but not on
+Linux (inotify uses `Write` only for file-content changes). If you only
+care about file content, filter out `Write` events whose path refers to a
+directory.
+
+Whether the directory `Write` is actually delivered alongside the child
+events is not guaranteed: it depends on `ReadDirectoryChangesW` buffering,
+NTFS metadata update timing, and event coalescing, none of which fsnotify
+controls.
-Reaching the limit will result in a "no space left on device" or "too many open
-files" error.
### kqueue (macOS, all BSD systems)
kqueue requires opening a file descriptor for every file that's being watched;
diff --git a/vendor/github.com/fsnotify/fsnotify/backend_fen.go b/vendor/github.com/fsnotify/fsnotify/backend_fen.go
index 57fc692848..e43c6d088c 100644
--- a/vendor/github.com/fsnotify/fsnotify/backend_fen.go
+++ b/vendor/github.com/fsnotify/fsnotify/backend_fen.go
@@ -158,7 +158,9 @@ func (w *fen) readEvents() {
pevents := make([]unix.PortEvent, 8)
for {
- count, err := w.port.Get(pevents, 1, nil)
+ count, err := internal.IgnoringEINTR(func() (int, error) {
+ return w.port.Get(pevents, 1, nil)
+ })
if err != nil && err != unix.ETIME {
// Interrupted system call (count should be 0) ignore and continue
if errors.Is(err, unix.EINTR) && count == 0 {
diff --git a/vendor/github.com/fsnotify/fsnotify/backend_inotify.go b/vendor/github.com/fsnotify/fsnotify/backend_inotify.go
index a36cb89d73..4c3f6f7c28 100644
--- a/vendor/github.com/fsnotify/fsnotify/backend_inotify.go
+++ b/vendor/github.com/fsnotify/fsnotify/backend_inotify.go
@@ -55,10 +55,10 @@ type (
path map[string]uint32 // pathname → wd
}
watch struct {
- wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
- flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
- path string // Watch path.
- recurse bool // Recursion with ./...?
+ wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
+ flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
+ path string // Watch path.
+ watchFlags watchFlag
}
koekje struct {
cookie uint32
@@ -66,6 +66,9 @@ type (
}
)
+func (w watch) byUser() bool { return w.watchFlags&flagByUser != 0 }
+func (w watch) recurse() bool { return w.watchFlags&flagRecurse != 0 }
+
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
@@ -79,6 +82,13 @@ func (w *watches) len() int { return len(w.wd) }
func (w *watches) add(ww *watch) { w.wd[ww.wd] = ww; w.path[ww.path] = ww.wd }
func (w *watches) remove(watch *watch) { delete(w.path, watch.path); delete(w.wd, watch.wd) }
+func isSameOrDescendantPath(path, root string) bool {
+ if path == root {
+ return true
+ }
+ return strings.HasPrefix(path, root+string(os.PathSeparator))
+}
+
func (w *watches) removePath(path string) ([]uint32, error) {
path, recurse := recursivePath(path)
wd, ok := w.path[path]
@@ -87,20 +97,20 @@ func (w *watches) removePath(path string) ([]uint32, error) {
}
watch := w.wd[wd]
- if recurse && !watch.recurse {
+ if recurse && !watch.recurse() {
return nil, fmt.Errorf("can't use /... with non-recursive watch %q", path)
}
delete(w.path, path)
delete(w.wd, wd)
- if !watch.recurse {
+ if !watch.recurse() {
return []uint32{wd}, nil
}
wds := make([]uint32, 0, 8)
wds = append(wds, wd)
for p, rwd := range w.path {
- if strings.HasPrefix(p, path) {
+ if isSameOrDescendantPath(p, path) {
delete(w.path, p)
delete(w.wd, rwd)
wds = append(wds, rwd)
@@ -139,7 +149,7 @@ func newBackend(ev chan Event, errs chan error) (backend, error) {
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 {
- return nil, errno
+ return nil, fmt.Errorf("couldn't initialize inotify: %w", errno)
}
w := &inotify{
@@ -188,11 +198,8 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
return fmt.Errorf("%w: %s", xErrUnsupported, with.op)
}
- add := func(path string, with withOpts, recurse bool) error {
+ add := func(path string, with withOpts, wf watchFlag) error {
var flags uint32
- if with.noFollow {
- flags |= unix.IN_DONT_FOLLOW
- }
if with.op.Has(Create) {
flags |= unix.IN_CREATE
}
@@ -220,7 +227,7 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
if with.op.Has(xUnportableCloseRead) {
flags |= unix.IN_CLOSE_NOWRITE
}
- return w.register(path, flags, recurse)
+ return w.register(path, flags, wf)
}
w.mu.Lock()
@@ -248,14 +255,18 @@ func (w *inotify) AddWith(path string, opts ...addOpt) error {
w.sendEvent(Event{Name: root, Op: Create})
}
- return add(root, with, true)
+ wf := flagRecurse
+ if root == path {
+ wf |= flagByUser
+ }
+ return add(root, with, wf)
})
}
- return add(path, with, false)
+ return add(path, with, 0)
}
-func (w *inotify) register(path string, flags uint32, recurse bool) error {
+func (w *inotify) register(path string, flags uint32, wf watchFlag) error {
return w.watches.updatePath(path, func(existing *watch) (*watch, error) {
if existing != nil {
flags |= existing.flags | unix.IN_MASK_ADD
@@ -272,10 +283,10 @@ func (w *inotify) register(path string, flags uint32, recurse bool) error {
if existing == nil {
return &watch{
- wd: uint32(wd),
- path: path,
- flags: flags,
- recurse: recurse,
+ wd: uint32(wd),
+ path: path,
+ flags: flags,
+ watchFlags: wf,
}, nil
}
@@ -425,11 +436,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
nameLen = uint32(inEvent.Len)
)
if nameLen > 0 {
- /// Point "bytes" at the first byte of the filename
- bb := *buf
- bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&bb[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
- /// The filename is padded with NULL bytes. TrimRight() gets rid of those.
- name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\x00")
+ name += "/" + inotifyEventName(buf, offset, nameLen)
}
if debug {
@@ -450,7 +457,9 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
// We can't really update the state when a watched path is moved; only
// IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove the watch.
if inEvent.Mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
- if watch.recurse { // Do nothing
+ // Watch is set up as part of recurse: do nothing as the move gets
+ // registered from the parent directory.
+ if watch.recurse() && !watch.byUser() {
return Event{}, true
}
@@ -460,6 +469,10 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
return Event{}, false
}
}
+
+ if watch.recurse() {
+ return Event{Name: watch.path, Op: Rename}, true
+ }
}
/// Skip if we're watching both this path and the parent; the parent will
@@ -473,11 +486,11 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
ev := w.newEvent(name, inEvent.Mask, inEvent.Cookie)
// Need to update watch path for recurse.
- if watch.recurse {
+ if watch.recurse() {
isDir := inEvent.Mask&unix.IN_ISDIR == unix.IN_ISDIR
/// New directory created: set up watch on it.
if isDir && ev.Has(Create) {
- err := w.register(ev.Name, watch.flags, true)
+ err := w.register(ev.Name, watch.flags, flagRecurse)
if !w.sendError(err) {
return Event{}, false
}
@@ -495,7 +508,7 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
if k == watch.wd || ww.path == ev.Name {
continue
}
- if strings.HasPrefix(ww.path, ev.renamedFrom) {
+ if isSameOrDescendantPath(ww.path, ev.renamedFrom) {
ww.path = strings.Replace(ww.path, ev.renamedFrom, ev.Name, 1)
w.watches.wd[k] = ww
}
@@ -507,12 +520,13 @@ func (w *inotify) handleEvent(inEvent *unix.InotifyEvent, buf *[65536]byte, offs
return ev, true
}
-func (w *inotify) isRecursive(path string) bool {
- ww := w.watches.byPath(path)
- if ww == nil { // path could be a file, so also check the Dir.
- ww = w.watches.byPath(filepath.Dir(path))
+func inotifyEventName(buf *[65536]byte, offset, nameLen uint32) string {
+ start := int(offset + unix.SizeofInotifyEvent)
+ bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[start]))[:nameLen:nameLen]
+ for nameLen > 0 && bytes[nameLen-1] == 0 {
+ nameLen--
}
- return ww != nil && ww.recurse
+ return string(bytes[:nameLen])
}
func (w *inotify) newEvent(name string, mask, cookie uint32) Event {
@@ -578,6 +592,6 @@ func (w *inotify) state() {
w.mu.Lock()
defer w.mu.Unlock()
for wd, ww := range w.watches.wd {
- fmt.Fprintf(os.Stderr, "%4d: recurse=%t %q\n", wd, ww.recurse, ww.path)
+ fmt.Fprintf(os.Stderr, "%4d: %q watchFlags=0x%x\n", wd, ww.path, ww.watchFlags)
}
}
diff --git a/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go b/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go
index 340aeec061..d2c8cfb6c6 100644
--- a/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go
+++ b/vendor/github.com/fsnotify/fsnotify/backend_kqueue.go
@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"runtime"
+ "sort"
"sync"
"time"
@@ -245,9 +246,26 @@ func (w *kqueue) Close() error {
return nil
}
+ // Snapshot and drop all watches directly. w.Remove -> w.remove
+ // short-circuits on isClosed() (which is already true after
+ // w.shared.close() above), so calling Remove here in the happy path
+ // leaked every watched directory + file descriptor. On macOS a
+ // single directory watch opens an fd for every file in the dir, so
+ // long-running processes that recreate watchers (hot-reload dev
+ // servers, etc.) ran out of fds with EMFILE (#732).
pathsToRemove := w.watches.listPaths(false)
for _, name := range pathsToRemove {
- w.Remove(name)
+ info, ok := w.watches.byPath(name)
+ if !ok {
+ // w.path has an entry for name but w.wd doesn't --
+ // drop the stale lookup entry so the map state is
+ // consistent after Close.
+ w.watches.remove(0, name)
+ continue
+ }
+ _ = w.register([]int{info.wd}, unix.EV_DELETE, 0)
+ unix.Close(info.wd)
+ w.watches.remove(info.wd, name)
}
unix.Close(w.closepipe[1]) // Send "quit" message to readEvents
@@ -376,19 +394,12 @@ func (w *kqueue) addWatch(name string, flags uint32, listDir bool) (string, erro
}
}
- // Retry on EINTR; open() can return EINTR in practice on macOS.
- // See #354, and Go issues 11180 and 39237.
- for {
- info.wd, err = unix.Open(name, openMode, 0)
- if err == nil {
- break
- }
- if errors.Is(err, unix.EINTR) {
- continue
- }
+ info.wd, err = internal.IgnoringEINTR(func() (int, error) {
+ return unix.Open(name, openMode, 0)
+ })
+ if err != nil {
return "", err
}
-
info.isDir = fi.IsDir()
}
@@ -436,9 +447,10 @@ func (w *kqueue) readEvents() {
eventBuffer := make([]unix.Kevent_t, 10)
for {
- kevents, err := w.read(eventBuffer)
- // EINTR is okay, the syscall was interrupted before timeout expired.
- if err != nil && err != unix.EINTR {
+ kevents, err := internal.IgnoringEINTR(func() ([]unix.Kevent_t, error) {
+ return w.read(eventBuffer)
+ })
+ if err != nil {
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
return
}
@@ -583,12 +595,14 @@ func (w *kqueue) watchDirectoryFiles(dirPath string) error {
cleanPath, err := w.internalWatch(path, fi)
if err != nil {
- // No permission to read the file; that's not a problem: just skip.
- // But do add it to w.fileExists to prevent it from being picked up
- // as a "new" file later (it still shows up in the directory
+ // No permission, or the entry resolved to a missing target
+ // (e.g. a dangling symlink): not a problem, just skip. But
+ // do mark it as seen to prevent it from being picked up as
+ // a "new" file later (it still shows up in the directory
// listing).
switch {
- case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
+ case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) ||
+ errors.Is(err, os.ErrNotExist):
cleanPath = filepath.Clean(path)
default:
return fmt.Errorf("%q: %w", path, err)
@@ -703,3 +717,19 @@ func (w *kqueue) xSupports(op Op) bool {
}
return true
}
+
+func (w *kqueue) state() {
+ w.watches.mu.Lock()
+ defer w.watches.mu.Unlock()
+
+ all := make([]int, 0, len(w.watches.wd))
+ for wd := range w.watches.wd {
+ all = append(all, wd)
+ }
+ sort.Ints(all)
+
+ for _, wd := range all {
+ ww := w.watches.wd[wd]
+ fmt.Fprintf(os.Stderr, "%4d %q linkname=%q\n", wd, ww.name, ww.linkName)
+ }
+}
diff --git a/vendor/github.com/fsnotify/fsnotify/backend_windows.go b/vendor/github.com/fsnotify/fsnotify/backend_windows.go
index 3433642d64..fb9210f24e 100644
--- a/vendor/github.com/fsnotify/fsnotify/backend_windows.go
+++ b/vendor/github.com/fsnotify/fsnotify/backend_windows.go
@@ -11,7 +11,6 @@ import (
"fmt"
"os"
"path/filepath"
- "reflect"
"runtime"
"strings"
"sync"
@@ -37,6 +36,13 @@ type readDirChangesW struct {
var defaultBufferSize = 50
+func isSameOrDescendantPath(path, root string) bool {
+ if path == root {
+ return true
+ }
+ return strings.HasPrefix(path, root+string(os.PathSeparator))
+}
+
func newBackend(ev chan Event, errs chan error) (backend, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil {
@@ -359,22 +365,26 @@ func (w *readDirChangesW) addWatch(pathname string, flags uint64, bufsize int) e
} else {
windows.CloseHandle(ino.handle)
}
+ w.mu.Lock()
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
+ w.mu.Unlock()
err = w.startRead(watchEntry)
if err != nil {
return err
}
+ w.mu.Lock()
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
+ w.mu.Unlock()
return nil
}
@@ -394,8 +404,13 @@ func (w *readDirChangesW) remWatch(pathname string) error {
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
+ if watch == nil {
+ windows.CloseHandle(ino.handle)
+ return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
+ }
if recurse && !watch.recurse {
+ windows.CloseHandle(ino.handle)
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
}
@@ -403,16 +418,19 @@ func (w *readDirChangesW) remWatch(pathname string) error {
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
- if watch == nil {
- return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
- }
if pathname == dir {
- w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
+ w.mu.Lock()
+ mask := watch.mask
watch.mask = 0
+ w.mu.Unlock()
+ w.sendEvent(watch.path, "", mask&sysFSIGNORED)
} else {
name := filepath.Base(pathname)
- w.sendEvent(filepath.Join(watch.path, name), "", watch.names[name]&sysFSIGNORED)
+ w.mu.Lock()
+ mask := watch.names[name]
delete(watch.names, name)
+ w.mu.Unlock()
+ w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
}
return w.startRead(watch)
@@ -420,17 +438,23 @@ func (w *readDirChangesW) remWatch(pathname string) error {
// Must run within the I/O thread.
func (w *readDirChangesW) deleteWatch(watch *watch) {
- for name, mask := range watch.names {
- if mask&provisional == 0 {
- w.sendEvent(filepath.Join(watch.path, name), "", mask&sysFSIGNORED)
+ // Snapshot+clear under the lock so concurrent WatchList() readers see a
+ // consistent state. sendEvent must run outside the lock since it can
+ // block on the user-facing Events channel.
+ w.mu.Lock()
+ names := watch.names
+ watch.names = make(map[string]uint64)
+ mask := watch.mask
+ watch.mask = 0
+ w.mu.Unlock()
+
+ for name, m := range names {
+ if m&provisional == 0 {
+ w.sendEvent(filepath.Join(watch.path, name), "", m&sysFSIGNORED)
}
- delete(watch.names, name)
}
- if watch.mask != 0 {
- if watch.mask&provisional == 0 {
- w.sendEvent(watch.path, "", watch.mask&sysFSIGNORED)
- }
- watch.mask = 0
+ if mask != 0 && mask&provisional == 0 {
+ w.sendEvent(watch.path, "", mask&sysFSIGNORED)
}
}
@@ -457,9 +481,8 @@ func (w *readDirChangesW) startRead(watch *watch) error {
}
// We need to pass the array, rather than the slice.
- hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
- (*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
+ unsafe.SliceData(watch.buf), uint32(len(watch.buf)),
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
@@ -565,12 +588,7 @@ func (w *readDirChangesW) readEvents() {
// Create a buf that is the size of the path name
size := int(raw.FileNameLength / 2)
- var buf []uint16
- // TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
- sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
- sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
- sh.Len = size
- sh.Cap = size
+ buf := unsafe.Slice(&raw.FileName, size)
name := windows.UTF16ToString(buf)
fullname := filepath.Join(watch.path, name)
@@ -587,31 +605,35 @@ func (w *readDirChangesW) readEvents() {
case windows.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case windows.FILE_ACTION_RENAMED_NEW_NAME:
- // Update saved path of all sub-watches.
+ // Update saved path of all sub-watches and rename the
+ // names entry under the lock so WatchList() can't observe
+ // a torn state.
old := filepath.Join(watch.path, watch.rename)
w.mu.Lock()
for _, watchMap := range w.watches {
for _, ww := range watchMap {
- if strings.HasPrefix(ww.path, old) {
+ if isSameOrDescendantPath(ww.path, old) {
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
}
}
}
- w.mu.Unlock()
-
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sysFSMOVESELF
}
+ w.mu.Unlock()
}
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
w.sendEvent(fullname, "", watch.names[name]&mask)
}
if raw.Action == windows.FILE_ACTION_REMOVED {
- w.sendEvent(fullname, "", watch.names[name]&sysFSIGNORED)
+ w.mu.Lock()
+ ignored := watch.names[name] & sysFSIGNORED
delete(watch.names, name)
+ w.mu.Unlock()
+ w.sendEvent(fullname, "", ignored)
}
if watch.rename != "" && raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
diff --git a/vendor/github.com/fsnotify/fsnotify/fsnotify.go b/vendor/github.com/fsnotify/fsnotify/fsnotify.go
index f64be4bf98..38cb4dd481 100644
--- a/vendor/github.com/fsnotify/fsnotify/fsnotify.go
+++ b/vendor/github.com/fsnotify/fsnotify/fsnotify.go
@@ -51,26 +51,25 @@ import (
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
-// create is an "instance", and every path you add is a "watch".
+// create is an "instance", and every path you add is a "watch". Reaching the
+// limit will result in a "no space left on device" or "too many open files"
+// error.
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
-// /proc/sys/fs/inotify/max_user_instances
+// /proc/sys/fs/inotify/max_user_instances. The default values differ per distro
+// and available memory.
//
// To increase them you can use sysctl or write the value to the /proc file:
//
-// # Default values on Linux 5.18
-// sysctl fs.inotify.max_user_watches=124983
-// sysctl fs.inotify.max_user_instances=128
+// sysctl fs.inotify.max_user_watches=200000
+// sysctl fs.inotify.max_user_instances=256
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
-// fs.inotify.max_user_watches=124983
-// fs.inotify.max_user_instances=128
-//
-// Reaching the limit will result in a "no space left on device" or "too many open
-// files" error.
+// fs.inotify.max_user_watches=200000
+// fs.inotify.max_user_instances=256
//
// # kqueue notes (macOS, BSD)
//
@@ -93,6 +92,28 @@ import (
// Sometimes it will send events for all files, sometimes it will send no
// events, and often only for some files.
//
+// Recursive watching is not currently enabled through fsnotify's public
+// API; the recursive code path is gated and only exercised by fsnotify's
+// own tests. The note below describes backend behavior observed when
+// recursive watching is enabled internally, and is kept here as a
+// reference for maintainers and contributors who encounter it.
+//
+// When recursive watching is enabled and you watch a directory, you may
+// receive a Write event for an intermediate directory whenever a child
+// entry inside it is created, renamed, or removed. For example, with a
+// recursive watch on /a and a new file /a/b/c, you will receive
+// Create /a/b/c and may also receive Write /a/b.
+//
+// This happens because, on NTFS-backed volumes, modifying the entries of a
+// directory updates that directory's last-write time, and the Windows
+// backend requests FILE_NOTIFY_CHANGE_LAST_WRITE to support Write events
+// on files. The same Write filter therefore picks up the directory's
+// metadata update.
+//
+// Whether the directory Write is actually delivered alongside the child
+// events is not guaranteed; it depends on ReadDirectoryChangesW buffering,
+// NTFS metadata update timing, and event coalescing.
+//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
@@ -129,8 +150,12 @@ type Watcher struct {
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
- // Some systems may send Write event for directories
- // when the directory content changes.
+ // Some systems also send Write events for directories
+ // when the directory contents change. This is the
+ // case for kqueue, and on Windows for the directory
+ // that contains a created, renamed, or removed child
+ // entry. It does not happen on inotify. See the
+ // per-platform notes on [Watcher].
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
@@ -179,7 +204,9 @@ const (
Create Op = 1 << iota
// The pathname was written to; this does *not* mean the write has finished,
- // and a write can be followed by more writes.
+ // and a write can be followed by more writes. On Windows and kqueue, a
+ // Write on a directory can also indicate that its contents changed; see
+ // the per-platform notes on [Watcher].
Write
// The path was removed; any watches on it will be removed. Some "remove"
@@ -220,7 +247,7 @@ const (
// File opened for reading was closed.
//
- // Only works on Linux and FreeBSD.
+ // Only works on Linux.
xUnportableCloseRead
)
@@ -410,7 +437,6 @@ type (
withOpts struct {
bufsize int
op Op
- noFollow bool
sendCreate bool
}
)
@@ -469,12 +495,6 @@ func withOps(op Op) addOpt {
return func(opt *withOpts) { opt.op = op }
}
-// WithNoFollow disables following symlinks, so the symlinks themselves are
-// watched.
-func withNoFollow() addOpt {
- return func(opt *withOpts) { opt.noFollow = true }
-}
-
// "Internal" option for recursive watches on inotify.
func withCreate() addOpt {
return func(opt *withOpts) { opt.sendCreate = true }
@@ -494,3 +514,13 @@ func recursivePath(path string) (string, bool) {
}
return path, false
}
+
+type watchFlag uint8
+
+const (
+ // Added by user with Add(), rather than an internal watch.
+ flagByUser = watchFlag(0x01)
+ // Part of recursive watch; as the top-level path added by the user or an
+ // "internal" watch.
+ flagRecurse = watchFlag(0x02)
+)
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/darwin.go b/vendor/github.com/fsnotify/fsnotify/internal/darwin.go
index 0b01bc182a..6721aa6096 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/darwin.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/darwin.go
@@ -15,25 +15,6 @@ var (
var maxfiles uint64
-func SetRlimit() {
- // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
- var l syscall.Rlimit
- err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
- if err == nil && l.Cur != l.Max {
- l.Cur = l.Max
- syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
- }
- maxfiles = l.Cur
-
- if n, err := syscall.SysctlUint32("kern.maxfiles"); err == nil && uint64(n) < maxfiles {
- maxfiles = uint64(n)
- }
-
- if n, err := syscall.SysctlUint32("kern.maxfilesperproc"); err == nil && uint64(n) < maxfiles {
- maxfiles = uint64(n)
- }
-}
-
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go
index 928319fb09..7600180742 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_darwin.go
@@ -6,52 +6,10 @@ var names = []struct {
n string
m uint32
}{
- {"NOTE_ABSOLUTE", unix.NOTE_ABSOLUTE},
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
- {"NOTE_BACKGROUND", unix.NOTE_BACKGROUND},
- {"NOTE_CHILD", unix.NOTE_CHILD},
- {"NOTE_CRITICAL", unix.NOTE_CRITICAL},
{"NOTE_DELETE", unix.NOTE_DELETE},
- {"NOTE_EXEC", unix.NOTE_EXEC},
- {"NOTE_EXIT", unix.NOTE_EXIT},
- {"NOTE_EXITSTATUS", unix.NOTE_EXITSTATUS},
- {"NOTE_EXIT_CSERROR", unix.NOTE_EXIT_CSERROR},
- {"NOTE_EXIT_DECRYPTFAIL", unix.NOTE_EXIT_DECRYPTFAIL},
- {"NOTE_EXIT_DETAIL", unix.NOTE_EXIT_DETAIL},
- {"NOTE_EXIT_DETAIL_MASK", unix.NOTE_EXIT_DETAIL_MASK},
- {"NOTE_EXIT_MEMORY", unix.NOTE_EXIT_MEMORY},
- {"NOTE_EXIT_REPARENTED", unix.NOTE_EXIT_REPARENTED},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
- {"NOTE_FFAND", unix.NOTE_FFAND},
- {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
- {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
- {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
- {"NOTE_FFNOP", unix.NOTE_FFNOP},
- {"NOTE_FFOR", unix.NOTE_FFOR},
- {"NOTE_FORK", unix.NOTE_FORK},
- {"NOTE_FUNLOCK", unix.NOTE_FUNLOCK},
- {"NOTE_LEEWAY", unix.NOTE_LEEWAY},
{"NOTE_LINK", unix.NOTE_LINK},
- {"NOTE_LOWAT", unix.NOTE_LOWAT},
- {"NOTE_MACHTIME", unix.NOTE_MACHTIME},
- {"NOTE_MACH_CONTINUOUS_TIME", unix.NOTE_MACH_CONTINUOUS_TIME},
- {"NOTE_NONE", unix.NOTE_NONE},
- {"NOTE_NSECONDS", unix.NOTE_NSECONDS},
- {"NOTE_OOB", unix.NOTE_OOB},
- //{"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK}, -0x100000 (?!)
- {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
- {"NOTE_REAP", unix.NOTE_REAP},
{"NOTE_RENAME", unix.NOTE_RENAME},
- {"NOTE_REVOKE", unix.NOTE_REVOKE},
- {"NOTE_SECONDS", unix.NOTE_SECONDS},
- {"NOTE_SIGNAL", unix.NOTE_SIGNAL},
- {"NOTE_TRACK", unix.NOTE_TRACK},
- {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
- {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
- {"NOTE_USECONDS", unix.NOTE_USECONDS},
- {"NOTE_VM_ERROR", unix.NOTE_VM_ERROR},
- {"NOTE_VM_PRESSURE", unix.NOTE_VM_PRESSURE},
- {"NOTE_VM_PRESSURE_SUDDEN_TERMINATE", unix.NOTE_VM_PRESSURE_SUDDEN_TERMINATE},
- {"NOTE_VM_PRESSURE_TERMINATE", unix.NOTE_VM_PRESSURE_TERMINATE},
{"NOTE_WRITE", unix.NOTE_WRITE},
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go
index 3186b0c349..7600180742 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_dragonfly.go
@@ -7,27 +7,9 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
- {"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
- {"NOTE_EXEC", unix.NOTE_EXEC},
- {"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
- {"NOTE_FFAND", unix.NOTE_FFAND},
- {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
- {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
- {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
- {"NOTE_FFNOP", unix.NOTE_FFNOP},
- {"NOTE_FFOR", unix.NOTE_FFOR},
- {"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
- {"NOTE_LOWAT", unix.NOTE_LOWAT},
- {"NOTE_OOB", unix.NOTE_OOB},
- {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
- {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
- {"NOTE_REVOKE", unix.NOTE_REVOKE},
- {"NOTE_TRACK", unix.NOTE_TRACK},
- {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
- {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
{"NOTE_WRITE", unix.NOTE_WRITE},
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go
index f69fdb930f..b9e45f5551 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_freebsd.go
@@ -6,37 +6,15 @@ var names = []struct {
n string
m uint32
}{
- {"NOTE_ABSTIME", unix.NOTE_ABSTIME},
- {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
- {"NOTE_CHILD", unix.NOTE_CHILD},
- {"NOTE_CLOSE", unix.NOTE_CLOSE},
- {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE},
{"NOTE_DELETE", unix.NOTE_DELETE},
- {"NOTE_EXEC", unix.NOTE_EXEC},
- {"NOTE_EXIT", unix.NOTE_EXIT},
+ {"NOTE_WRITE", unix.NOTE_WRITE},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
- {"NOTE_FFAND", unix.NOTE_FFAND},
- {"NOTE_FFCOPY", unix.NOTE_FFCOPY},
- {"NOTE_FFCTRLMASK", unix.NOTE_FFCTRLMASK},
- {"NOTE_FFLAGSMASK", unix.NOTE_FFLAGSMASK},
- {"NOTE_FFNOP", unix.NOTE_FFNOP},
- {"NOTE_FFOR", unix.NOTE_FFOR},
- {"NOTE_FILE_POLL", unix.NOTE_FILE_POLL},
- {"NOTE_FORK", unix.NOTE_FORK},
+ {"NOTE_ATTRIB", unix.NOTE_ATTRIB},
{"NOTE_LINK", unix.NOTE_LINK},
- {"NOTE_LOWAT", unix.NOTE_LOWAT},
- {"NOTE_MSECONDS", unix.NOTE_MSECONDS},
- {"NOTE_NSECONDS", unix.NOTE_NSECONDS},
- {"NOTE_OPEN", unix.NOTE_OPEN},
- {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
- {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
- {"NOTE_READ", unix.NOTE_READ},
{"NOTE_RENAME", unix.NOTE_RENAME},
{"NOTE_REVOKE", unix.NOTE_REVOKE},
- {"NOTE_SECONDS", unix.NOTE_SECONDS},
- {"NOTE_TRACK", unix.NOTE_TRACK},
- {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
- {"NOTE_TRIGGER", unix.NOTE_TRIGGER},
- {"NOTE_USECONDS", unix.NOTE_USECONDS},
- {"NOTE_WRITE", unix.NOTE_WRITE},
+ {"NOTE_OPEN", unix.NOTE_OPEN},
+ {"NOTE_CLOSE", unix.NOTE_CLOSE},
+ {"NOTE_CLOSE_WRITE", unix.NOTE_CLOSE_WRITE},
+ {"NOTE_READ", unix.NOTE_READ},
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go
index 607e683bd7..5d8116436d 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_kqueue.go
@@ -27,6 +27,6 @@ func Debug(name string, kevent *unix.Kevent_t) {
if unknown > 0 {
l = append(l, fmt.Sprintf("0x%x", unknown))
}
- fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-60s → %q\n",
+ fmt.Fprintf(os.Stderr, "FSNOTIFY_DEBUG: %s %10d:%-20s → %q\n",
time.Now().Format("15:04:05.000000000"), mask, strings.Join(l, " | "), name)
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go
index e5b3b6f694..7600180742 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_netbsd.go
@@ -7,19 +7,9 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
- {"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
- {"NOTE_EXEC", unix.NOTE_EXEC},
- {"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
- {"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
- {"NOTE_LOWAT", unix.NOTE_LOWAT},
- {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
- {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
- {"NOTE_REVOKE", unix.NOTE_REVOKE},
- {"NOTE_TRACK", unix.NOTE_TRACK},
- {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_WRITE", unix.NOTE_WRITE},
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go b/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go
index 1dd455bc5a..871766d699 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/debug_openbsd.go
@@ -7,22 +7,10 @@ var names = []struct {
m uint32
}{
{"NOTE_ATTRIB", unix.NOTE_ATTRIB},
- // {"NOTE_CHANGE", unix.NOTE_CHANGE}, // Not on 386?
- {"NOTE_CHILD", unix.NOTE_CHILD},
{"NOTE_DELETE", unix.NOTE_DELETE},
- {"NOTE_EOF", unix.NOTE_EOF},
- {"NOTE_EXEC", unix.NOTE_EXEC},
- {"NOTE_EXIT", unix.NOTE_EXIT},
{"NOTE_EXTEND", unix.NOTE_EXTEND},
- {"NOTE_FORK", unix.NOTE_FORK},
{"NOTE_LINK", unix.NOTE_LINK},
- {"NOTE_LOWAT", unix.NOTE_LOWAT},
- {"NOTE_PCTRLMASK", unix.NOTE_PCTRLMASK},
- {"NOTE_PDATAMASK", unix.NOTE_PDATAMASK},
{"NOTE_RENAME", unix.NOTE_RENAME},
- {"NOTE_REVOKE", unix.NOTE_REVOKE},
- {"NOTE_TRACK", unix.NOTE_TRACK},
- {"NOTE_TRACKERR", unix.NOTE_TRACKERR},
{"NOTE_TRUNCATE", unix.NOTE_TRUNCATE},
{"NOTE_WRITE", unix.NOTE_WRITE},
}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go b/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go
index 5ac8b50797..758a249052 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/freebsd.go
@@ -15,17 +15,6 @@ var (
var maxfiles uint64
-func SetRlimit() {
- // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
- var l syscall.Rlimit
- err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
- if err == nil && l.Cur != l.Max {
- l.Cur = l.Max
- syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
- }
- maxfiles = uint64(l.Cur)
-}
-
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, uint64(dev)) }
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/unix.go b/vendor/github.com/fsnotify/fsnotify/internal/unix.go
index b251fb8038..9c66f5d30d 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/unix.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/unix.go
@@ -15,17 +15,6 @@ var (
var maxfiles uint64
-func SetRlimit() {
- // Go 1.19 will do this automatically: https://go-review.googlesource.com/c/go/+/393354/
- var l syscall.Rlimit
- err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &l)
- if err == nil && l.Cur != l.Max {
- l.Cur = l.Max
- syscall.Setrlimit(syscall.RLIMIT_NOFILE, &l)
- }
- maxfiles = uint64(l.Cur)
-}
-
func Maxfiles() uint64 { return maxfiles }
func Mkfifo(path string, mode uint32) error { return unix.Mkfifo(path, mode) }
func Mknod(path string, mode uint32, dev int) error { return unix.Mknod(path, mode, dev) }
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/unix2.go b/vendor/github.com/fsnotify/fsnotify/internal/unix2.go
index 37dfeddc28..b2d89592f7 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/unix2.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/unix2.go
@@ -2,6 +2,24 @@
package internal
+import "syscall"
+
func HasPrivilegesForSymlink() bool {
return true
}
+
+// IgnoringEINTR makes a function call and repeats it if it returns an
+// EINTR error. This appears to be required even though we install all
+// signal handlers with SA_RESTART: see #22838, #38033, #38836, #40846.
+// Also #20400 and #36644 are issues in which a signal handler is
+// installed without setting SA_RESTART. None of these are the common case,
+// but there are enough of them that it seems that we can't avoid
+// an EINTR loop.
+func IgnoringEINTR[T any](fn func() (T, error)) (T, error) {
+ for {
+ v, err := fn()
+ if err != syscall.EINTR {
+ return v, err
+ }
+ }
+}
diff --git a/vendor/github.com/fsnotify/fsnotify/internal/windows.go b/vendor/github.com/fsnotify/fsnotify/internal/windows.go
index 896bc2e5a2..e24d569264 100644
--- a/vendor/github.com/fsnotify/fsnotify/internal/windows.go
+++ b/vendor/github.com/fsnotify/fsnotify/internal/windows.go
@@ -14,7 +14,6 @@ var (
ErrUnixEACCES = errors.New("dummy")
)
-func SetRlimit() {}
func Maxfiles() uint64 { return 1<<64 - 1 }
func Mkfifo(path string, mode uint32) error { return errors.New("no FIFOs on Windows") }
func Mknod(path string, mode uint32, dev int) error { return errors.New("no device nodes on Windows") }
diff --git a/vendor/github.com/goccy/go-json/internal/encoder/code.go b/vendor/github.com/goccy/go-json/internal/encoder/code.go
index 5b08faefc7..fec45a4b89 100644
--- a/vendor/github.com/goccy/go-json/internal/encoder/code.go
+++ b/vendor/github.com/goccy/go-json/internal/encoder/code.go
@@ -518,6 +518,7 @@ func (c *StructCode) ToAnonymousOpcode(ctx *compileContext) Opcodes {
prevField = firstField
codes = codes.Add(fieldCodes...)
}
+ ctx.structTypeToCodes[uintptr(unsafe.Pointer(c.typ))] = codes
return codes
}
diff --git a/vendor/github.com/golang/snappy/README b/vendor/github.com/golang/snappy/README
index cea12879a0..fd191f78c7 100644
--- a/vendor/github.com/golang/snappy/README
+++ b/vendor/github.com/golang/snappy/README
@@ -1,8 +1,13 @@
The Snappy compression format in the Go programming language.
-To download and install from source:
+To use as a library:
$ go get github.com/golang/snappy
+To use as a binary:
+$ go install github.com/golang/snappy/cmd/snappytool@latest
+$ cat decoded | ~/go/bin/snappytool -e > encoded
+$ cat encoded | ~/go/bin/snappytool -d > decoded
+
Unless otherwise noted, the Snappy-Go source files are distributed
under the BSD-style license found in the LICENSE file.
diff --git a/vendor/github.com/golang/snappy/encode_arm64.s b/vendor/github.com/golang/snappy/encode_arm64.s
index f8d54adfc5..f0c876a248 100644
--- a/vendor/github.com/golang/snappy/encode_arm64.s
+++ b/vendor/github.com/golang/snappy/encode_arm64.s
@@ -27,7 +27,7 @@
// The unusual register allocation of local variables, such as R10 for the
// source pointer, matches the allocation used at the call site in encodeBlock,
// which makes it easier to manually inline this function.
-TEXT ·emitLiteral(SB), NOSPLIT, $32-56
+TEXT ·emitLiteral(SB), NOSPLIT, $40-56
MOVD dst_base+0(FP), R8
MOVD lit_base+24(FP), R10
MOVD lit_len+32(FP), R3
@@ -261,7 +261,7 @@ extendMatchEnd:
// "var table [maxTableSize]uint16" takes up 32768 bytes of stack space. An
// extra 64 bytes, to call other functions, and an extra 64 bytes, to spill
// local variables (registers) during calls gives 32768 + 64 + 64 = 32896.
-TEXT ·encodeBlock(SB), 0, $32896-56
+TEXT ·encodeBlock(SB), 0, $32904-56
MOVD dst_base+0(FP), R8
MOVD src_base+24(FP), R7
MOVD src_len+32(FP), R14
diff --git a/vendor/github.com/klauspost/compress/.gitattributes b/vendor/github.com/klauspost/compress/.gitattributes
index 402433593c..57aa6487c5 100644
--- a/vendor/github.com/klauspost/compress/.gitattributes
+++ b/vendor/github.com/klauspost/compress/.gitattributes
@@ -1,2 +1,3 @@
* -text
*.bin -text -diff
+*.md text eol=lf
diff --git a/vendor/github.com/klauspost/compress/README.md b/vendor/github.com/klauspost/compress/README.md
index e839fe9c60..fb023f2cf2 100644
--- a/vendor/github.com/klauspost/compress/README.md
+++ b/vendor/github.com/klauspost/compress/README.md
@@ -1,700 +1,700 @@
-# compress
-
-This package provides various compression algorithms.
-
-* [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression in pure Go.
-* [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy.
-* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
-* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
-* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
-* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
-* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
-
-[](https://pkg.go.dev/github.com/klauspost/compress?tab=subdirectories)
-[](https://github.com/klauspost/compress/actions/workflows/go.yml)
-[](https://sourcegraph.com/github.com/klauspost/compress?badge)
-
-# package usage
-
-Use `go get github.com/klauspost/compress@latest` to add it to your project.
-
-This package will support the current Go version and 2 versions back.
-
-* Use the `nounsafe` tag to disable all use of the "unsafe" package.
-* Use the `noasm` tag to disable all assembly across packages.
-
-Use the links above for more information on each.
-
-# changelog
-
-* Feb 9th, 2026 [1.18.4](https://github.com/klauspost/compress/releases/tag/v1.18.4)
- * gzhttp: Add zstandard to server handler wrapper https://github.com/klauspost/compress/pull/1121
- * zstd: Add ResetWithOptions to encoder/decoder https://github.com/klauspost/compress/pull/1122
- * gzhttp: preserve qvalue when extra parameters follow in Accept-Encoding by @analytically in https://github.com/klauspost/compress/pull/1116
-
-* Jan 16th, 2026 [1.18.3](https://github.com/klauspost/compress/releases/tag/v1.18.3)
- * Downstream CVE-2025-61728. See [golang/go#77102](https://github.com/golang/go/issues/77102).
-
-* Dec 1st, 2025 - [1.18.2](https://github.com/klauspost/compress/releases/tag/v1.18.2)
- * flate: Fix invalid encoding on level 9 with single value input in https://github.com/klauspost/compress/pull/1115
- * flate: reduce stateless allocations by @RXamzin in https://github.com/klauspost/compress/pull/1106
-
-* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1) - RETRACTED
- * zstd: Add simple zstd EncodeTo/DecodeTo functions https://github.com/klauspost/compress/pull/1079
- * zstd: Fix incorrect buffer size in dictionary encodes https://github.com/klauspost/compress/pull/1059
- * s2: check for cap, not len of buffer in EncodeBetter/Best by @vdarulis in https://github.com/klauspost/compress/pull/1080
- * zlib: Avoiding extra allocation in zlib.reader.Reset by @travelpolicy in https://github.com/klauspost/compress/pull/1086
- * gzhttp: remove redundant err check in zstdReader by @ryanfowler in https://github.com/klauspost/compress/pull/1090
- * flate: Faster load+store https://github.com/klauspost/compress/pull/1104
- * flate: Simplify matchlen https://github.com/klauspost/compress/pull/1101
- * flate: Use exact sizes for huffman tables https://github.com/klauspost/compress/pull/1103
-
-* Feb 19th, 2025 - [1.18.0](https://github.com/klauspost/compress/releases/tag/v1.18.0)
- * Add unsafe little endian loaders https://github.com/klauspost/compress/pull/1036
- * fix: check `r.err != nil` but return a nil value error `err` by @alingse in https://github.com/klauspost/compress/pull/1028
- * flate: Simplify L4-6 loading https://github.com/klauspost/compress/pull/1043
- * flate: Simplify matchlen (remove asm) https://github.com/klauspost/compress/pull/1045
- * s2: Improve small block compression speed w/o asm https://github.com/klauspost/compress/pull/1048
- * flate: Fix matchlen L5+L6 https://github.com/klauspost/compress/pull/1049
- * flate: Cleanup & reduce casts https://github.com/klauspost/compress/pull/1050
-
-
- See changes to v1.17.x
-
-* Oct 11th, 2024 - [1.17.11](https://github.com/klauspost/compress/releases/tag/v1.17.11)
- * zstd: Fix extra CRC written with multiple Close calls https://github.com/klauspost/compress/pull/1017
- * s2: Don't use stack for index tables https://github.com/klauspost/compress/pull/1014
- * gzhttp: No content-type on no body response code by @juliens in https://github.com/klauspost/compress/pull/1011
- * gzhttp: Do not set the content-type when response has no body by @kevinpollet in https://github.com/klauspost/compress/pull/1013
-
-* Sep 23rd, 2024 - [1.17.10](https://github.com/klauspost/compress/releases/tag/v1.17.10)
- * gzhttp: Add TransportAlwaysDecompress option. https://github.com/klauspost/compress/pull/978
- * gzhttp: Add supported decompress request body by @mirecl in https://github.com/klauspost/compress/pull/1002
- * s2: Add EncodeBuffer buffer recycling callback https://github.com/klauspost/compress/pull/982
- * zstd: Improve memory usage on small streaming encodes https://github.com/klauspost/compress/pull/1007
- * flate: read data written with partial flush by @vajexal in https://github.com/klauspost/compress/pull/996
-
-* Jun 12th, 2024 - [1.17.9](https://github.com/klauspost/compress/releases/tag/v1.17.9)
- * s2: Reduce ReadFrom temporary allocations https://github.com/klauspost/compress/pull/949
- * flate, zstd: Shave some bytes off amd64 matchLen by @greatroar in https://github.com/klauspost/compress/pull/963
- * Upgrade zip/zlib to 1.22.4 upstream https://github.com/klauspost/compress/pull/970 https://github.com/klauspost/compress/pull/971
- * zstd: BuildDict fails with RLE table https://github.com/klauspost/compress/pull/951
-
-* Apr 9th, 2024 - [1.17.8](https://github.com/klauspost/compress/releases/tag/v1.17.8)
- * zstd: Reject blocks where reserved values are not 0 https://github.com/klauspost/compress/pull/885
- * zstd: Add RLE detection+encoding https://github.com/klauspost/compress/pull/938
-
-* Feb 21st, 2024 - [1.17.7](https://github.com/klauspost/compress/releases/tag/v1.17.7)
- * s2: Add AsyncFlush method: Complete the block without flushing by @Jille in https://github.com/klauspost/compress/pull/927
- * s2: Fix literal+repeat exceeds dst crash https://github.com/klauspost/compress/pull/930
-
-* Feb 5th, 2024 - [1.17.6](https://github.com/klauspost/compress/releases/tag/v1.17.6)
- * zstd: Fix incorrect repeat coding in best mode https://github.com/klauspost/compress/pull/923
- * s2: Fix DecodeConcurrent deadlock on errors https://github.com/klauspost/compress/pull/925
-
-* Jan 26th, 2024 - [v1.17.5](https://github.com/klauspost/compress/releases/tag/v1.17.5)
- * flate: Fix reset with dictionary on custom window encodes https://github.com/klauspost/compress/pull/912
- * zstd: Add Frame header encoding and stripping https://github.com/klauspost/compress/pull/908
- * zstd: Limit better/best default window to 8MB https://github.com/klauspost/compress/pull/913
- * zstd: Speed improvements by @greatroar in https://github.com/klauspost/compress/pull/896 https://github.com/klauspost/compress/pull/910
- * s2: Fix callbacks for skippable blocks and disallow 0xfe (Padding) by @Jille in https://github.com/klauspost/compress/pull/916 https://github.com/klauspost/compress/pull/917
-https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/compress/pull/918
-
-* Dec 1st, 2023 - [v1.17.4](https://github.com/klauspost/compress/releases/tag/v1.17.4)
- * huff0: Speed up symbol counting by @greatroar in https://github.com/klauspost/compress/pull/887
- * huff0: Remove byteReader by @greatroar in https://github.com/klauspost/compress/pull/886
- * gzhttp: Allow overriding decompression on transport https://github.com/klauspost/compress/pull/892
- * gzhttp: Clamp compression level https://github.com/klauspost/compress/pull/890
- * gzip: Error out if reserved bits are set https://github.com/klauspost/compress/pull/891
-
-* Nov 15th, 2023 - [v1.17.3](https://github.com/klauspost/compress/releases/tag/v1.17.3)
- * fse: Fix max header size https://github.com/klauspost/compress/pull/881
- * zstd: Improve better/best compression https://github.com/klauspost/compress/pull/877
- * gzhttp: Fix missing content type on Close https://github.com/klauspost/compress/pull/883
-
-* Oct 22nd, 2023 - [v1.17.2](https://github.com/klauspost/compress/releases/tag/v1.17.2)
- * zstd: Fix rare *CORRUPTION* output in "best" mode. See https://github.com/klauspost/compress/pull/876
-
-* Oct 14th, 2023 - [v1.17.1](https://github.com/klauspost/compress/releases/tag/v1.17.1)
- * s2: Fix S2 "best" dictionary wrong encoding https://github.com/klauspost/compress/pull/871
- * flate: Reduce allocations in decompressor and minor code improvements by @fakefloordiv in https://github.com/klauspost/compress/pull/869
- * s2: Fix EstimateBlockSize on 6&7 length input https://github.com/klauspost/compress/pull/867
-
-* Sept 19th, 2023 - [v1.17.0](https://github.com/klauspost/compress/releases/tag/v1.17.0)
- * Add experimental dictionary builder https://github.com/klauspost/compress/pull/853
- * Add xerial snappy read/writer https://github.com/klauspost/compress/pull/838
- * flate: Add limited window compression https://github.com/klauspost/compress/pull/843
- * s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
- * flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
- * gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
-
-
-
- See changes to v1.16.x
-
-
-* July 1st, 2023 - [v1.16.7](https://github.com/klauspost/compress/releases/tag/v1.16.7)
- * zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
- * s2: add GetBufferCapacity() method by @GiedriusS in https://github.com/klauspost/compress/pull/832
-
-* June 13, 2023 - [v1.16.6](https://github.com/klauspost/compress/releases/tag/v1.16.6)
- * zstd: correctly ignore WithEncoderPadding(1) by @ianlancetaylor in https://github.com/klauspost/compress/pull/806
- * zstd: Add amd64 match length assembly https://github.com/klauspost/compress/pull/824
- * gzhttp: Handle informational headers by @rtribotte in https://github.com/klauspost/compress/pull/815
- * s2: Improve Better compression slightly https://github.com/klauspost/compress/pull/663
-
-* Apr 16, 2023 - [v1.16.5](https://github.com/klauspost/compress/releases/tag/v1.16.5)
- * zstd: readByte needs to use io.ReadFull by @jnoxon in https://github.com/klauspost/compress/pull/802
- * gzip: Fix WriterTo after initial read https://github.com/klauspost/compress/pull/804
-
-* Apr 5, 2023 - [v1.16.4](https://github.com/klauspost/compress/releases/tag/v1.16.4)
- * zstd: Improve zstd best efficiency by @greatroar and @klauspost in https://github.com/klauspost/compress/pull/784
- * zstd: Respect WithAllLitEntropyCompression https://github.com/klauspost/compress/pull/792
- * zstd: Fix amd64 not always detecting corrupt data https://github.com/klauspost/compress/pull/785
- * zstd: Various minor improvements by @greatroar in https://github.com/klauspost/compress/pull/788 https://github.com/klauspost/compress/pull/794 https://github.com/klauspost/compress/pull/795
- * s2: Fix huge block overflow https://github.com/klauspost/compress/pull/779
- * s2: Allow CustomEncoder fallback https://github.com/klauspost/compress/pull/780
- * gzhttp: Support ResponseWriter Unwrap() in gzhttp handler by @jgimenez in https://github.com/klauspost/compress/pull/799
-
-* Mar 13, 2023 - [v1.16.1](https://github.com/klauspost/compress/releases/tag/v1.16.1)
- * zstd: Speed up + improve best encoder by @greatroar in https://github.com/klauspost/compress/pull/776
- * gzhttp: Add optional [BREACH mitigation](https://github.com/klauspost/compress/tree/master/gzhttp#breach-mitigation). https://github.com/klauspost/compress/pull/762 https://github.com/klauspost/compress/pull/768 https://github.com/klauspost/compress/pull/769 https://github.com/klauspost/compress/pull/770 https://github.com/klauspost/compress/pull/767
- * s2: Add Intel LZ4s converter https://github.com/klauspost/compress/pull/766
- * zstd: Minor bug fixes https://github.com/klauspost/compress/pull/771 https://github.com/klauspost/compress/pull/772 https://github.com/klauspost/compress/pull/773
- * huff0: Speed up compress1xDo by @greatroar in https://github.com/klauspost/compress/pull/774
-
-* Feb 26, 2023 - [v1.16.0](https://github.com/klauspost/compress/releases/tag/v1.16.0)
- * s2: Add [Dictionary](https://github.com/klauspost/compress/tree/master/s2#dictionaries) support. https://github.com/klauspost/compress/pull/685
- * s2: Add Compression Size Estimate. https://github.com/klauspost/compress/pull/752
- * s2: Add support for custom stream encoder. https://github.com/klauspost/compress/pull/755
- * s2: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
- * s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747
- * s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
-
-
-
- See changes to v1.15.x
-
-* Jan 21st, 2023 (v1.15.15)
- * deflate: Improve level 7-9 https://github.com/klauspost/compress/pull/739
- * zstd: Add delta encoding support by @greatroar in https://github.com/klauspost/compress/pull/728
- * zstd: Various speed improvements by @greatroar https://github.com/klauspost/compress/pull/741 https://github.com/klauspost/compress/pull/734 https://github.com/klauspost/compress/pull/736 https://github.com/klauspost/compress/pull/744 https://github.com/klauspost/compress/pull/743 https://github.com/klauspost/compress/pull/745
- * gzhttp: Add SuffixETag() and DropETag() options to prevent ETag collisions on compressed responses by @willbicks in https://github.com/klauspost/compress/pull/740
-
-* Jan 3rd, 2023 (v1.15.14)
-
- * flate: Improve speed in big stateless blocks https://github.com/klauspost/compress/pull/718
- * zstd: Minor speed tweaks by @greatroar in https://github.com/klauspost/compress/pull/716 https://github.com/klauspost/compress/pull/720
- * export NoGzipResponseWriter for custom ResponseWriter wrappers by @harshavardhana in https://github.com/klauspost/compress/pull/722
- * s2: Add example for indexing and existing stream https://github.com/klauspost/compress/pull/723
-
-* Dec 11, 2022 (v1.15.13)
- * zstd: Add [MaxEncodedSize](https://pkg.go.dev/github.com/klauspost/compress@v1.15.13/zstd#Encoder.MaxEncodedSize) to encoder https://github.com/klauspost/compress/pull/691
- * zstd: Various tweaks and improvements https://github.com/klauspost/compress/pull/693 https://github.com/klauspost/compress/pull/695 https://github.com/klauspost/compress/pull/696 https://github.com/klauspost/compress/pull/701 https://github.com/klauspost/compress/pull/702 https://github.com/klauspost/compress/pull/703 https://github.com/klauspost/compress/pull/704 https://github.com/klauspost/compress/pull/705 https://github.com/klauspost/compress/pull/706 https://github.com/klauspost/compress/pull/707 https://github.com/klauspost/compress/pull/708
-
-* Oct 26, 2022 (v1.15.12)
-
- * zstd: Tweak decoder allocs. https://github.com/klauspost/compress/pull/680
- * gzhttp: Always delete `HeaderNoCompression` https://github.com/klauspost/compress/pull/683
-
-* Sept 26, 2022 (v1.15.11)
-
- * flate: Improve level 1-3 compression https://github.com/klauspost/compress/pull/678
- * zstd: Improve "best" compression by @nightwolfz in https://github.com/klauspost/compress/pull/677
- * zstd: Fix+reduce decompression allocations https://github.com/klauspost/compress/pull/668
- * zstd: Fix non-effective noescape tag https://github.com/klauspost/compress/pull/667
-
-* Sept 16, 2022 (v1.15.10)
-
- * zstd: Add [WithDecodeAllCapLimit](https://pkg.go.dev/github.com/klauspost/compress@v1.15.10/zstd#WithDecodeAllCapLimit) https://github.com/klauspost/compress/pull/649
- * Add Go 1.19 - deprecate Go 1.16 https://github.com/klauspost/compress/pull/651
- * flate: Improve level 5+6 compression https://github.com/klauspost/compress/pull/656
- * zstd: Improve "better" compression https://github.com/klauspost/compress/pull/657
- * s2: Improve "best" compression https://github.com/klauspost/compress/pull/658
- * s2: Improve "better" compression. https://github.com/klauspost/compress/pull/635
- * s2: Slightly faster non-assembly decompression https://github.com/klauspost/compress/pull/646
- * Use arrays for constant size copies https://github.com/klauspost/compress/pull/659
-
-* July 21, 2022 (v1.15.9)
-
- * zstd: Fix decoder crash on amd64 (no BMI) on invalid input https://github.com/klauspost/compress/pull/645
- * zstd: Disable decoder extended memory copies (amd64) due to possible crashes https://github.com/klauspost/compress/pull/644
- * zstd: Allow single segments up to "max decoded size" https://github.com/klauspost/compress/pull/643
-
-* July 13, 2022 (v1.15.8)
-
- * gzip: fix stack exhaustion bug in Reader.Read https://github.com/klauspost/compress/pull/641
- * s2: Add Index header trim/restore https://github.com/klauspost/compress/pull/638
- * zstd: Optimize seqdeq amd64 asm by @greatroar in https://github.com/klauspost/compress/pull/636
- * zstd: Improve decoder memcopy https://github.com/klauspost/compress/pull/637
- * huff0: Pass a single bitReader pointer to asm by @greatroar in https://github.com/klauspost/compress/pull/634
- * zstd: Branchless getBits for amd64 w/o BMI2 by @greatroar in https://github.com/klauspost/compress/pull/640
- * gzhttp: Remove header before writing https://github.com/klauspost/compress/pull/639
-
-* June 29, 2022 (v1.15.7)
-
- * s2: Fix absolute forward seeks https://github.com/klauspost/compress/pull/633
- * zip: Merge upstream https://github.com/klauspost/compress/pull/631
- * zip: Re-add zip64 fix https://github.com/klauspost/compress/pull/624
- * zstd: translate fseDecoder.buildDtable into asm by @WojciechMula in https://github.com/klauspost/compress/pull/598
- * flate: Faster histograms https://github.com/klauspost/compress/pull/620
- * deflate: Use compound hcode https://github.com/klauspost/compress/pull/622
-
-* June 3, 2022 (v1.15.6)
- * s2: Improve coding for long, close matches https://github.com/klauspost/compress/pull/613
- * s2c: Add Snappy/S2 stream recompression https://github.com/klauspost/compress/pull/611
- * zstd: Always use configured block size https://github.com/klauspost/compress/pull/605
- * zstd: Fix incorrect hash table placement for dict encoding in default https://github.com/klauspost/compress/pull/606
- * zstd: Apply default config to ZipDecompressor without options https://github.com/klauspost/compress/pull/608
- * gzhttp: Exclude more common archive formats https://github.com/klauspost/compress/pull/612
- * s2: Add ReaderIgnoreCRC https://github.com/klauspost/compress/pull/609
- * s2: Remove sanity load on index creation https://github.com/klauspost/compress/pull/607
- * snappy: Use dedicated function for scoring https://github.com/klauspost/compress/pull/614
- * s2c+s2d: Use official snappy framed extension https://github.com/klauspost/compress/pull/610
-
-* May 25, 2022 (v1.15.5)
- * s2: Add concurrent stream decompression https://github.com/klauspost/compress/pull/602
- * s2: Fix final emit oob read crash on amd64 https://github.com/klauspost/compress/pull/601
- * huff0: asm implementation of Decompress1X by @WojciechMula https://github.com/klauspost/compress/pull/596
- * zstd: Use 1 less goroutine for stream decoding https://github.com/klauspost/compress/pull/588
- * zstd: Copy literal in 16 byte blocks when possible https://github.com/klauspost/compress/pull/592
- * zstd: Speed up when WithDecoderLowmem(false) https://github.com/klauspost/compress/pull/599
- * zstd: faster next state update in BMI2 version of decode by @WojciechMula in https://github.com/klauspost/compress/pull/593
- * huff0: Do not check max size when reading table. https://github.com/klauspost/compress/pull/586
- * flate: Inplace hashing for level 7-9 https://github.com/klauspost/compress/pull/590
-
-
-* May 11, 2022 (v1.15.4)
- * huff0: decompress directly into output by @WojciechMula in [#577](https://github.com/klauspost/compress/pull/577)
- * inflate: Keep dict on stack [#581](https://github.com/klauspost/compress/pull/581)
- * zstd: Faster decoding memcopy in asm [#583](https://github.com/klauspost/compress/pull/583)
- * zstd: Fix ignored crc [#580](https://github.com/klauspost/compress/pull/580)
-
-* May 5, 2022 (v1.15.3)
- * zstd: Allow to ignore checksum checking by @WojciechMula [#572](https://github.com/klauspost/compress/pull/572)
- * s2: Fix incorrect seek for io.SeekEnd in [#575](https://github.com/klauspost/compress/pull/575)
-
-* Apr 26, 2022 (v1.15.2)
- * zstd: Add x86-64 assembly for decompression on streams and blocks. Contributed by [@WojciechMula](https://github.com/WojciechMula). Typically 2x faster. [#528](https://github.com/klauspost/compress/pull/528) [#531](https://github.com/klauspost/compress/pull/531) [#545](https://github.com/klauspost/compress/pull/545) [#537](https://github.com/klauspost/compress/pull/537)
- * zstd: Add options to ZipDecompressor and fixes [#539](https://github.com/klauspost/compress/pull/539)
- * s2: Use sorted search for index [#555](https://github.com/klauspost/compress/pull/555)
- * Minimum version is Go 1.16, added CI test on 1.18.
-
-* Mar 11, 2022 (v1.15.1)
- * huff0: Add x86 assembly of Decode4X by @WojciechMula in [#512](https://github.com/klauspost/compress/pull/512)
- * zstd: Reuse zip decoders in [#514](https://github.com/klauspost/compress/pull/514)
- * zstd: Detect extra block data and report as corrupted in [#520](https://github.com/klauspost/compress/pull/520)
- * zstd: Handle zero sized frame content size stricter in [#521](https://github.com/klauspost/compress/pull/521)
- * zstd: Add stricter block size checks in [#523](https://github.com/klauspost/compress/pull/523)
-
-* Mar 3, 2022 (v1.15.0)
- * zstd: Refactor decoder [#498](https://github.com/klauspost/compress/pull/498)
- * zstd: Add stream encoding without goroutines [#505](https://github.com/klauspost/compress/pull/505)
- * huff0: Prevent single blocks exceeding 16 bits by @klauspost in[#507](https://github.com/klauspost/compress/pull/507)
- * flate: Inline literal emission [#509](https://github.com/klauspost/compress/pull/509)
- * gzhttp: Add zstd to transport [#400](https://github.com/klauspost/compress/pull/400)
- * gzhttp: Make content-type optional [#510](https://github.com/klauspost/compress/pull/510)
-
-Both compression and decompression now supports "synchronous" stream operations. This means that whenever "concurrency" is set to 1, they will operate without spawning goroutines.
-
-Stream decompression is now faster on asynchronous, since the goroutine allocation much more effectively splits the workload. On typical streams this will typically use 2 cores fully for decompression. When a stream has finished decoding no goroutines will be left over, so decoders can now safely be pooled and still be garbage collected.
-
-While the release has been extensively tested, it is recommended to testing when upgrading.
-
-
-
-
- See changes to v1.14.x
-
-* Feb 22, 2022 (v1.14.4)
- * flate: Fix rare huffman only (-2) corruption. [#503](https://github.com/klauspost/compress/pull/503)
- * zip: Update deprecated CreateHeaderRaw to correctly call CreateRaw by @saracen in [#502](https://github.com/klauspost/compress/pull/502)
- * zip: don't read data descriptor early by @saracen in [#501](https://github.com/klauspost/compress/pull/501) #501
- * huff0: Use static decompression buffer up to 30% faster [#499](https://github.com/klauspost/compress/pull/499) [#500](https://github.com/klauspost/compress/pull/500)
-
-* Feb 17, 2022 (v1.14.3)
- * flate: Improve fastest levels compression speed ~10% more throughput. [#482](https://github.com/klauspost/compress/pull/482) [#489](https://github.com/klauspost/compress/pull/489) [#490](https://github.com/klauspost/compress/pull/490) [#491](https://github.com/klauspost/compress/pull/491) [#494](https://github.com/klauspost/compress/pull/494) [#478](https://github.com/klauspost/compress/pull/478)
- * flate: Faster decompression speed, ~5-10%. [#483](https://github.com/klauspost/compress/pull/483)
- * s2: Faster compression with Go v1.18 and amd64 microarch level 3+. [#484](https://github.com/klauspost/compress/pull/484) [#486](https://github.com/klauspost/compress/pull/486)
-
-* Jan 25, 2022 (v1.14.2)
- * zstd: improve header decoder by @dsnet [#476](https://github.com/klauspost/compress/pull/476)
- * zstd: Add bigger default blocks [#469](https://github.com/klauspost/compress/pull/469)
- * zstd: Remove unused decompression buffer [#470](https://github.com/klauspost/compress/pull/470)
- * zstd: Fix logically dead code by @ningmingxiao [#472](https://github.com/klauspost/compress/pull/472)
- * flate: Improve level 7-9 [#471](https://github.com/klauspost/compress/pull/471) [#473](https://github.com/klauspost/compress/pull/473)
- * zstd: Add noasm tag for xxhash [#475](https://github.com/klauspost/compress/pull/475)
-
-* Jan 11, 2022 (v1.14.1)
- * s2: Add stream index in [#462](https://github.com/klauspost/compress/pull/462)
- * flate: Speed and efficiency improvements in [#439](https://github.com/klauspost/compress/pull/439) [#461](https://github.com/klauspost/compress/pull/461) [#455](https://github.com/klauspost/compress/pull/455) [#452](https://github.com/klauspost/compress/pull/452) [#458](https://github.com/klauspost/compress/pull/458)
- * zstd: Performance improvement in [#420]( https://github.com/klauspost/compress/pull/420) [#456](https://github.com/klauspost/compress/pull/456) [#437](https://github.com/klauspost/compress/pull/437) [#467](https://github.com/klauspost/compress/pull/467) [#468](https://github.com/klauspost/compress/pull/468)
- * zstd: add arm64 xxhash assembly in [#464](https://github.com/klauspost/compress/pull/464)
- * Add garbled for binaries for s2 in [#445](https://github.com/klauspost/compress/pull/445)
-
-
-
- See changes to v1.13.x
-
-* Aug 30, 2021 (v1.13.5)
- * gz/zlib/flate: Alias stdlib errors [#425](https://github.com/klauspost/compress/pull/425)
- * s2: Add block support to commandline tools [#413](https://github.com/klauspost/compress/pull/413)
- * zstd: pooledZipWriter should return Writers to the same pool [#426](https://github.com/klauspost/compress/pull/426)
- * Removed golang/snappy as external dependency for tests [#421](https://github.com/klauspost/compress/pull/421)
-
-* Aug 12, 2021 (v1.13.4)
- * Add [snappy replacement package](https://github.com/klauspost/compress/tree/master/snappy).
- * zstd: Fix incorrect encoding in "best" mode [#415](https://github.com/klauspost/compress/pull/415)
-
-* Aug 3, 2021 (v1.13.3)
- * zstd: Improve Best compression [#404](https://github.com/klauspost/compress/pull/404)
- * zstd: Fix WriteTo error forwarding [#411](https://github.com/klauspost/compress/pull/411)
- * gzhttp: Return http.HandlerFunc instead of http.Handler. Unlikely breaking change. [#406](https://github.com/klauspost/compress/pull/406)
- * s2sx: Fix max size error [#399](https://github.com/klauspost/compress/pull/399)
- * zstd: Add optional stream content size on reset [#401](https://github.com/klauspost/compress/pull/401)
- * zstd: use SpeedBestCompression for level >= 10 [#410](https://github.com/klauspost/compress/pull/410)
-
-* Jun 14, 2021 (v1.13.1)
- * s2: Add full Snappy output support [#396](https://github.com/klauspost/compress/pull/396)
- * zstd: Add configurable [Decoder window](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithDecoderMaxWindow) size [#394](https://github.com/klauspost/compress/pull/394)
- * gzhttp: Add header to skip compression [#389](https://github.com/klauspost/compress/pull/389)
- * s2: Improve speed with bigger output margin [#395](https://github.com/klauspost/compress/pull/395)
-
-* Jun 3, 2021 (v1.13.0)
- * Added [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp#gzip-handler) which allows wrapping HTTP servers and clients with GZIP compressors.
- * zstd: Detect short invalid signatures [#382](https://github.com/klauspost/compress/pull/382)
- * zstd: Spawn decoder goroutine only if needed. [#380](https://github.com/klauspost/compress/pull/380)
-
-
-
-
- See changes to v1.12.x
-
-* May 25, 2021 (v1.12.3)
- * deflate: Better/faster Huffman encoding [#374](https://github.com/klauspost/compress/pull/374)
- * deflate: Allocate less for history. [#375](https://github.com/klauspost/compress/pull/375)
- * zstd: Forward read errors [#373](https://github.com/klauspost/compress/pull/373)
-
-* Apr 27, 2021 (v1.12.2)
- * zstd: Improve better/best compression [#360](https://github.com/klauspost/compress/pull/360) [#364](https://github.com/klauspost/compress/pull/364) [#365](https://github.com/klauspost/compress/pull/365)
- * zstd: Add helpers to compress/decompress zstd inside zip files [#363](https://github.com/klauspost/compress/pull/363)
- * deflate: Improve level 5+6 compression [#367](https://github.com/klauspost/compress/pull/367)
- * s2: Improve better/best compression [#358](https://github.com/klauspost/compress/pull/358) [#359](https://github.com/klauspost/compress/pull/358)
- * s2: Load after checking src limit on amd64. [#362](https://github.com/klauspost/compress/pull/362)
- * s2sx: Limit max executable size [#368](https://github.com/klauspost/compress/pull/368)
-
-* Apr 14, 2021 (v1.12.1)
- * snappy package removed. Upstream added as dependency.
- * s2: Better compression in "best" mode [#353](https://github.com/klauspost/compress/pull/353)
- * s2sx: Add stdin input and detect pre-compressed from signature [#352](https://github.com/klauspost/compress/pull/352)
- * s2c/s2d: Add http as possible input [#348](https://github.com/klauspost/compress/pull/348)
- * s2c/s2d/s2sx: Always truncate when writing files [#352](https://github.com/klauspost/compress/pull/352)
- * zstd: Reduce memory usage further when using [WithLowerEncoderMem](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithLowerEncoderMem) [#346](https://github.com/klauspost/compress/pull/346)
- * s2: Fix potential problem with amd64 assembly and profilers [#349](https://github.com/klauspost/compress/pull/349)
-
-
-
- See changes to v1.11.x
-
-* Mar 26, 2021 (v1.11.13)
- * zstd: Big speedup on small dictionary encodes [#344](https://github.com/klauspost/compress/pull/344) [#345](https://github.com/klauspost/compress/pull/345)
- * zstd: Add [WithLowerEncoderMem](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithLowerEncoderMem) encoder option [#336](https://github.com/klauspost/compress/pull/336)
- * deflate: Improve entropy compression [#338](https://github.com/klauspost/compress/pull/338)
- * s2: Clean up and minor performance improvement in best [#341](https://github.com/klauspost/compress/pull/341)
-
-* Mar 5, 2021 (v1.11.12)
- * s2: Add `s2sx` binary that creates [self extracting archives](https://github.com/klauspost/compress/tree/master/s2#s2sx-self-extracting-archives).
- * s2: Speed up decompression on non-assembly platforms [#328](https://github.com/klauspost/compress/pull/328)
-
-* Mar 1, 2021 (v1.11.9)
- * s2: Add ARM64 decompression assembly. Around 2x output speed. [#324](https://github.com/klauspost/compress/pull/324)
- * s2: Improve "better" speed and efficiency. [#325](https://github.com/klauspost/compress/pull/325)
- * s2: Fix binaries.
-
-* Feb 25, 2021 (v1.11.8)
- * s2: Fixed occasional out-of-bounds write on amd64. Upgrade recommended.
- * s2: Add AMD64 assembly for better mode. 25-50% faster. [#315](https://github.com/klauspost/compress/pull/315)
- * s2: Less upfront decoder allocation. [#322](https://github.com/klauspost/compress/pull/322)
- * zstd: Faster "compression" of incompressible data. [#314](https://github.com/klauspost/compress/pull/314)
- * zip: Fix zip64 headers. [#313](https://github.com/klauspost/compress/pull/313)
-
-* Jan 14, 2021 (v1.11.7)
- * Use Bytes() interface to get bytes across packages. [#309](https://github.com/klauspost/compress/pull/309)
- * s2: Add 'best' compression option. [#310](https://github.com/klauspost/compress/pull/310)
- * s2: Add ReaderMaxBlockSize, changes `s2.NewReader` signature to include varargs. [#311](https://github.com/klauspost/compress/pull/311)
- * s2: Fix crash on small better buffers. [#308](https://github.com/klauspost/compress/pull/308)
- * s2: Clean up decoder. [#312](https://github.com/klauspost/compress/pull/312)
-
-* Jan 7, 2021 (v1.11.6)
- * zstd: Make decoder allocations smaller [#306](https://github.com/klauspost/compress/pull/306)
- * zstd: Free Decoder resources when Reset is called with a nil io.Reader [#305](https://github.com/klauspost/compress/pull/305)
-
-* Dec 20, 2020 (v1.11.4)
- * zstd: Add Best compression mode [#304](https://github.com/klauspost/compress/pull/304)
- * Add header decoder [#299](https://github.com/klauspost/compress/pull/299)
- * s2: Add uncompressed stream option [#297](https://github.com/klauspost/compress/pull/297)
- * Simplify/speed up small blocks with known max size. [#300](https://github.com/klauspost/compress/pull/300)
- * zstd: Always reset literal dict encoder [#303](https://github.com/klauspost/compress/pull/303)
-
-* Nov 15, 2020 (v1.11.3)
- * inflate: 10-15% faster decompression [#293](https://github.com/klauspost/compress/pull/293)
- * zstd: Tweak DecodeAll default allocation [#295](https://github.com/klauspost/compress/pull/295)
-
-* Oct 11, 2020 (v1.11.2)
- * s2: Fix out of bounds read in "better" block compression [#291](https://github.com/klauspost/compress/pull/291)
-
-* Oct 1, 2020 (v1.11.1)
- * zstd: Set allLitEntropy true in default configuration [#286](https://github.com/klauspost/compress/pull/286)
-
-* Sept 8, 2020 (v1.11.0)
- * zstd: Add experimental compression [dictionaries](https://github.com/klauspost/compress/tree/master/zstd#dictionaries) [#281](https://github.com/klauspost/compress/pull/281)
- * zstd: Fix mixed Write and ReadFrom calls [#282](https://github.com/klauspost/compress/pull/282)
- * inflate/gz: Limit variable shifts, ~5% faster decompression [#274](https://github.com/klauspost/compress/pull/274)
-
-
-
- See changes to v1.10.x
-
-* July 8, 2020 (v1.10.11)
- * zstd: Fix extra block when compressing with ReadFrom. [#278](https://github.com/klauspost/compress/pull/278)
- * huff0: Also populate compression table when reading decoding table. [#275](https://github.com/klauspost/compress/pull/275)
-
-* June 23, 2020 (v1.10.10)
- * zstd: Skip entropy compression in fastest mode when no matches. [#270](https://github.com/klauspost/compress/pull/270)
-
-* June 16, 2020 (v1.10.9):
- * zstd: API change for specifying dictionaries. See [#268](https://github.com/klauspost/compress/pull/268)
- * zip: update CreateHeaderRaw to handle zip64 fields. [#266](https://github.com/klauspost/compress/pull/266)
- * Fuzzit tests removed. The service has been purchased and is no longer available.
-
-* June 5, 2020 (v1.10.8):
- * 1.15x faster zstd block decompression. [#265](https://github.com/klauspost/compress/pull/265)
-
-* June 1, 2020 (v1.10.7):
- * Added zstd decompression [dictionary support](https://github.com/klauspost/compress/tree/master/zstd#dictionaries)
- * Increase zstd decompression speed up to 1.19x. [#259](https://github.com/klauspost/compress/pull/259)
- * Remove internal reset call in zstd compression and reduce allocations. [#263](https://github.com/klauspost/compress/pull/263)
-
-* May 21, 2020: (v1.10.6)
- * zstd: Reduce allocations while decoding. [#258](https://github.com/klauspost/compress/pull/258), [#252](https://github.com/klauspost/compress/pull/252)
- * zstd: Stricter decompression checks.
-
-* April 12, 2020: (v1.10.5)
- * s2-commands: Flush output when receiving SIGINT. [#239](https://github.com/klauspost/compress/pull/239)
-
-* Apr 8, 2020: (v1.10.4)
- * zstd: Minor/special case optimizations. [#251](https://github.com/klauspost/compress/pull/251), [#250](https://github.com/klauspost/compress/pull/250), [#249](https://github.com/klauspost/compress/pull/249), [#247](https://github.com/klauspost/compress/pull/247)
-* Mar 11, 2020: (v1.10.3)
- * s2: Use S2 encoder in pure Go mode for Snappy output as well. [#245](https://github.com/klauspost/compress/pull/245)
- * s2: Fix pure Go block encoder. [#244](https://github.com/klauspost/compress/pull/244)
- * zstd: Added "better compression" mode. [#240](https://github.com/klauspost/compress/pull/240)
- * zstd: Improve speed of fastest compression mode by 5-10% [#241](https://github.com/klauspost/compress/pull/241)
- * zstd: Skip creating encoders when not needed. [#238](https://github.com/klauspost/compress/pull/238)
-
-* Feb 27, 2020: (v1.10.2)
- * Close to 50% speedup in inflate (gzip/zip decompression). [#236](https://github.com/klauspost/compress/pull/236) [#234](https://github.com/klauspost/compress/pull/234) [#232](https://github.com/klauspost/compress/pull/232)
- * Reduce deflate level 1-6 memory usage up to 59%. [#227](https://github.com/klauspost/compress/pull/227)
-
-* Feb 18, 2020: (v1.10.1)
- * Fix zstd crash when resetting multiple times without sending data. [#226](https://github.com/klauspost/compress/pull/226)
- * deflate: Fix dictionary use on level 1-6. [#224](https://github.com/klauspost/compress/pull/224)
- * Remove deflate writer reference when closing. [#224](https://github.com/klauspost/compress/pull/224)
-
-* Feb 4, 2020: (v1.10.0)
- * Add optional dictionary to [stateless deflate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc#StatelessDeflate). Breaking change, send `nil` for previous behaviour. [#216](https://github.com/klauspost/compress/pull/216)
- * Fix buffer overflow on repeated small block deflate. [#218](https://github.com/klauspost/compress/pull/218)
- * Allow copying content from an existing ZIP file without decompressing+compressing. [#214](https://github.com/klauspost/compress/pull/214)
- * Added [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) AMD64 assembler and various optimizations. Stream speed >10GB/s. [#186](https://github.com/klauspost/compress/pull/186)
-
-
-
-
- See changes prior to v1.10.0
-
-* Jan 20,2020 (v1.9.8) Optimize gzip/deflate with better size estimates and faster table generation. [#207](https://github.com/klauspost/compress/pull/207) by [luyu6056](https://github.com/luyu6056), [#206](https://github.com/klauspost/compress/pull/206).
-* Jan 11, 2020: S2 Encode/Decode will use provided buffer if capacity is big enough. [#204](https://github.com/klauspost/compress/pull/204)
-* Jan 5, 2020: (v1.9.7) Fix another zstd regression in v1.9.5 - v1.9.6 removed.
-* Jan 4, 2020: (v1.9.6) Regression in v1.9.5 fixed causing corrupt zstd encodes in rare cases.
-* Jan 4, 2020: Faster IO in [s2c + s2d commandline tools](https://github.com/klauspost/compress/tree/master/s2#commandline-tools) compression/decompression. [#192](https://github.com/klauspost/compress/pull/192)
-* Dec 29, 2019: Removed v1.9.5 since fuzz tests showed a compatibility problem with the reference zstandard decoder.
-* Dec 29, 2019: (v1.9.5) zstd: 10-20% faster block compression. [#199](https://github.com/klauspost/compress/pull/199)
-* Dec 29, 2019: [zip](https://godoc.org/github.com/klauspost/compress/zip) package updated with latest Go features
-* Dec 29, 2019: zstd: Single segment flag condintions tweaked. [#197](https://github.com/klauspost/compress/pull/197)
-* Dec 18, 2019: s2: Faster compression when ReadFrom is used. [#198](https://github.com/klauspost/compress/pull/198)
-* Dec 10, 2019: s2: Fix repeat length output when just above at 16MB limit.
-* Dec 10, 2019: zstd: Add function to get decoder as io.ReadCloser. [#191](https://github.com/klauspost/compress/pull/191)
-* Dec 3, 2019: (v1.9.4) S2: limit max repeat length. [#188](https://github.com/klauspost/compress/pull/188)
-* Dec 3, 2019: Add [WithNoEntropyCompression](https://godoc.org/github.com/klauspost/compress/zstd#WithNoEntropyCompression) to zstd [#187](https://github.com/klauspost/compress/pull/187)
-* Dec 3, 2019: Reduce memory use for tests. Check for leaked goroutines.
-* Nov 28, 2019 (v1.9.3) Less allocations in stateless deflate.
-* Nov 28, 2019: 5-20% Faster huff0 decode. Impacts zstd as well. [#184](https://github.com/klauspost/compress/pull/184)
-* Nov 12, 2019 (v1.9.2) Added [Stateless Compression](#stateless-compression) for gzip/deflate.
-* Nov 12, 2019: Fixed zstd decompression of large single blocks. [#180](https://github.com/klauspost/compress/pull/180)
-* Nov 11, 2019: Set default [s2c](https://github.com/klauspost/compress/tree/master/s2#commandline-tools) block size to 4MB.
-* Nov 11, 2019: Reduce inflate memory use by 1KB.
-* Nov 10, 2019: Less allocations in deflate bit writer.
-* Nov 10, 2019: Fix inconsistent error returned by zstd decoder.
-* Oct 28, 2019 (v1.9.1) ztsd: Fix crash when compressing blocks. [#174](https://github.com/klauspost/compress/pull/174)
-* Oct 24, 2019 (v1.9.0) zstd: Fix rare data corruption [#173](https://github.com/klauspost/compress/pull/173)
-* Oct 24, 2019 zstd: Fix huff0 out of buffer write [#171](https://github.com/klauspost/compress/pull/171) and always return errors [#172](https://github.com/klauspost/compress/pull/172)
-* Oct 10, 2019: Big deflate rewrite, 30-40% faster with better compression [#105](https://github.com/klauspost/compress/pull/105)
-
-
-
-
- See changes prior to v1.9.0
-
-* Oct 10, 2019: (v1.8.6) zstd: Allow partial reads to get flushed data. [#169](https://github.com/klauspost/compress/pull/169)
-* Oct 3, 2019: Fix inconsistent results on broken zstd streams.
-* Sep 25, 2019: Added `-rm` (remove source files) and `-q` (no output except errors) to `s2c` and `s2d` [commands](https://github.com/klauspost/compress/tree/master/s2#commandline-tools)
-* Sep 16, 2019: (v1.8.4) Add `s2c` and `s2d` [commandline tools](https://github.com/klauspost/compress/tree/master/s2#commandline-tools).
-* Sep 10, 2019: (v1.8.3) Fix s2 decoder [Skip](https://godoc.org/github.com/klauspost/compress/s2#Reader.Skip).
-* Sep 7, 2019: zstd: Added [WithWindowSize](https://godoc.org/github.com/klauspost/compress/zstd#WithWindowSize), contributed by [ianwilkes](https://github.com/ianwilkes).
-* Sep 5, 2019: (v1.8.2) Add [WithZeroFrames](https://godoc.org/github.com/klauspost/compress/zstd#WithZeroFrames) which adds full zero payload block encoding option.
-* Sep 5, 2019: Lazy initialization of zstandard predefined en/decoder tables.
-* Aug 26, 2019: (v1.8.1) S2: 1-2% compression increase in "better" compression mode.
-* Aug 26, 2019: zstd: Check maximum size of Huffman 1X compressed literals while decoding.
-* Aug 24, 2019: (v1.8.0) Added [S2 compression](https://github.com/klauspost/compress/tree/master/s2#s2-compression), a high performance replacement for Snappy.
-* Aug 21, 2019: (v1.7.6) Fixed minor issues found by fuzzer. One could lead to zstd not decompressing.
-* Aug 18, 2019: Add [fuzzit](https://fuzzit.dev/) continuous fuzzing.
-* Aug 14, 2019: zstd: Skip incompressible data 2x faster. [#147](https://github.com/klauspost/compress/pull/147)
-* Aug 4, 2019 (v1.7.5): Better literal compression. [#146](https://github.com/klauspost/compress/pull/146)
-* Aug 4, 2019: Faster zstd compression. [#143](https://github.com/klauspost/compress/pull/143) [#144](https://github.com/klauspost/compress/pull/144)
-* Aug 4, 2019: Faster zstd decompression. [#145](https://github.com/klauspost/compress/pull/145) [#143](https://github.com/klauspost/compress/pull/143) [#142](https://github.com/klauspost/compress/pull/142)
-* July 15, 2019 (v1.7.4): Fix double EOF block in rare cases on zstd encoder.
-* July 15, 2019 (v1.7.3): Minor speedup/compression increase in default zstd encoder.
-* July 14, 2019: zstd decoder: Fix decompression error on multiple uses with mixed content.
-* July 7, 2019 (v1.7.2): Snappy update, zstd decoder potential race fix.
-* June 17, 2019: zstd decompression bugfix.
-* June 17, 2019: fix 32 bit builds.
-* June 17, 2019: Easier use in modules (less dependencies).
-* June 9, 2019: New stronger "default" [zstd](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression mode. Matches zstd default compression ratio.
-* June 5, 2019: 20-40% throughput in [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and better compression.
-* June 5, 2019: deflate/gzip compression: Reduce memory usage of lower compression levels.
-* June 2, 2019: Added [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression!
-* May 25, 2019: deflate/gzip: 10% faster bit writer, mostly visible in lower levels.
-* Apr 22, 2019: [zstd](https://github.com/klauspost/compress/tree/master/zstd#zstd) decompression added.
-* Aug 1, 2018: Added [huff0 README](https://github.com/klauspost/compress/tree/master/huff0#huff0-entropy-compression).
-* Jul 8, 2018: Added [Performance Update 2018](#performance-update-2018) below.
-* Jun 23, 2018: Merged [Go 1.11 inflate optimizations](https://go-review.googlesource.com/c/go/+/102235). Go 1.9 is now required. Backwards compatible version tagged with [v1.3.0](https://github.com/klauspost/compress/releases/tag/v1.3.0).
-* Apr 2, 2018: Added [huff0](https://godoc.org/github.com/klauspost/compress/huff0) en/decoder. Experimental for now, API may change.
-* Mar 4, 2018: Added [FSE Entropy](https://godoc.org/github.com/klauspost/compress/fse) en/decoder. Experimental for now, API may change.
-* Nov 3, 2017: Add compression [Estimate](https://godoc.org/github.com/klauspost/compress#Estimate) function.
-* May 28, 2017: Reduce allocations when resetting decoder.
-* Apr 02, 2017: Change back to official crc32, since changes were merged in Go 1.7.
-* Jan 14, 2017: Reduce stack pressure due to array copies. See [Issue #18625](https://github.com/golang/go/issues/18625).
-* Oct 25, 2016: Level 2-4 have been rewritten and now offers significantly better performance than before.
-* Oct 20, 2016: Port zlib changes from Go 1.7 to fix zlib writer issue. Please update.
-* Oct 16, 2016: Go 1.7 changes merged. Apples to apples this package is a few percent faster, but has a significantly better balance between speed and compression per level.
-* Mar 24, 2016: Always attempt Huffman encoding on level 4-7. This improves base 64 encoded data compression.
-* Mar 24, 2016: Small speedup for level 1-3.
-* Feb 19, 2016: Faster bit writer, level -2 is 15% faster, level 1 is 4% faster.
-* Feb 19, 2016: Handle small payloads faster in level 1-3.
-* Feb 19, 2016: Added faster level 2 + 3 compression modes.
-* Feb 19, 2016: [Rebalanced compression levels](https://blog.klauspost.com/rebalancing-deflate-compression-levels/), so there is a more even progression in terms of compression. New default level is 5.
-* Feb 14, 2016: Snappy: Merge upstream changes.
-* Feb 14, 2016: Snappy: Fix aggressive skipping.
-* Feb 14, 2016: Snappy: Update benchmark.
-* Feb 13, 2016: Deflate: Fixed assembler problem that could lead to sub-optimal compression.
-* Feb 12, 2016: Snappy: Added AMD64 SSE 4.2 optimizations to matching, which makes easy to compress material run faster. Typical speedup is around 25%.
-* Feb 9, 2016: Added Snappy package fork. This version is 5-7% faster, much more on hard to compress content.
-* Jan 30, 2016: Optimize level 1 to 3 by not considering static dictionary or storing uncompressed. ~4-5% speedup.
-* Jan 16, 2016: Optimization on deflate level 1,2,3 compression.
-* Jan 8 2016: Merge [CL 18317](https://go-review.googlesource.com/#/c/18317): fix reading, writing of zip64 archives.
-* Dec 8 2015: Make level 1 and -2 deterministic even if write size differs.
-* Dec 8 2015: Split encoding functions, so hashing and matching can potentially be inlined. 1-3% faster on AMD64. 5% faster on other platforms.
-* Dec 8 2015: Fixed rare [one byte out-of bounds read](https://github.com/klauspost/compress/issues/20). Please update!
-* Nov 23 2015: Optimization on token writer. ~2-4% faster. Contributed by [@dsnet](https://github.com/dsnet).
-* Nov 20 2015: Small optimization to bit writer on 64 bit systems.
-* Nov 17 2015: Fixed out-of-bound errors if the underlying Writer returned an error. See [#15](https://github.com/klauspost/compress/issues/15).
-* Nov 12 2015: Added [io.WriterTo](https://golang.org/pkg/io/#WriterTo) support to gzip/inflate.
-* Nov 11 2015: Merged [CL 16669](https://go-review.googlesource.com/#/c/16669/4): archive/zip: enable overriding (de)compressors per file
-* Oct 15 2015: Added skipping on uncompressible data. Random data speed up >5x.
-
-
-
-# deflate usage
-
-The packages are drop-in replacements for standard library [deflate](https://godoc.org/github.com/klauspost/compress/flate), [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip), and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). Simply replace the import path to use them:
-
-Typical speed is about 2x of the standard library packages.
-
-| old import | new import | Documentation |
-|------------------|---------------------------------------|-------------------------------------------------------------------------|
-| `compress/gzip` | `github.com/klauspost/compress/gzip` | [gzip](https://pkg.go.dev/github.com/klauspost/compress/gzip?tab=doc) |
-| `compress/zlib` | `github.com/klauspost/compress/zlib` | [zlib](https://pkg.go.dev/github.com/klauspost/compress/zlib?tab=doc) |
-| `archive/zip` | `github.com/klauspost/compress/zip` | [zip](https://pkg.go.dev/github.com/klauspost/compress/zip?tab=doc) |
-| `compress/flate` | `github.com/klauspost/compress/flate` | [flate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc) |
-
-You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop-in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
-
-The packages implement the same API as the standard library, so you can use the original godoc documentation: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
-
-Currently there is only minor speedup on decompression (mostly CRC32 calculation).
-
-Memory usage is typically 1MB for a Writer. stdlib is in the same range.
-If you expect to have a lot of concurrently allocated Writers consider using
-the stateless compression described below.
-
-For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
-
-To disable all assembly add `-tags=noasm`. This works across all packages.
-
-# Stateless compression
-
-This package offers stateless compression as a special option for gzip/deflate.
-It will do compression but without maintaining any state between Write calls.
-
-This means there will be no memory kept between Write calls, but compression and speed will be suboptimal.
-
-This is only relevant in cases where you expect to run many thousands of compressors concurrently,
-but with very little activity. This is *not* intended for regular web servers serving individual requests.
-
-Because of this, the size of actual Write calls will affect output size.
-
-In gzip, specify level `-3` / `gzip.StatelessCompression` to enable.
-
-For direct deflate use, NewStatelessWriter and StatelessDeflate are available. See [documentation](https://godoc.org/github.com/klauspost/compress/flate#NewStatelessWriter)
-
-A `bufio.Writer` can of course be used to control write sizes. For example, to use a 4KB buffer:
-
-```go
- // replace 'ioutil.Discard' with your output.
- gzw, err := gzip.NewWriterLevel(ioutil.Discard, gzip.StatelessCompression)
- if err != nil {
- return err
- }
- defer gzw.Close()
-
- w := bufio.NewWriterSize(gzw, 4096)
- defer w.Flush()
-
- // Write to 'w'
-```
-
-This will only use up to 4KB in memory when the writer is idle.
-
-Compression is almost always worse than the fastest compression level
-and each write will allocate (a little) memory.
-
-
-# Other packages
-
-Here are other packages of good quality and pure Go (no cgo wrappers or autoconverted code):
-
-* [github.com/pierrec/lz4](https://github.com/pierrec/lz4) - strong multithreaded LZ4 compression.
-* [github.com/cosnicolaou/pbzip2](https://github.com/cosnicolaou/pbzip2) - multithreaded bzip2 decompression.
-* [github.com/dsnet/compress](https://github.com/dsnet/compress) - brotli decompression, bzip2 writer.
-* [github.com/ronanh/intcomp](https://github.com/ronanh/intcomp) - Integer compression.
-* [github.com/spenczar/fpc](https://github.com/spenczar/fpc) - Float compression.
-* [github.com/minio/zipindex](https://github.com/minio/zipindex) - External ZIP directory index.
-* [github.com/ybirader/pzip](https://github.com/ybirader/pzip) - Fast concurrent zip archiver and extractor.
-
-# license
-
-This code is licensed under the same conditions as the original Go code. See LICENSE file.
-
-
-
-
-
+# compress
+
+This package provides various compression algorithms.
+
+* [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression in pure Go.
+* [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) is a high performance replacement for Snappy.
+* Optimized [deflate](https://godoc.org/github.com/klauspost/compress/flate) packages which can be used as a dropin replacement for [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip) and [zlib](https://godoc.org/github.com/klauspost/compress/zlib).
+* [snappy](https://github.com/klauspost/compress/tree/master/snappy) is a drop-in replacement for `github.com/golang/snappy` offering better compression and concurrent streams.
+* [huff0](https://github.com/klauspost/compress/tree/master/huff0) and [FSE](https://github.com/klauspost/compress/tree/master/fse) implementations for raw entropy encoding.
+* [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp) Provides client and server wrappers for handling gzipped/zstd HTTP requests efficiently.
+* [pgzip](https://github.com/klauspost/pgzip) is a separate package that provides a very fast parallel gzip implementation.
+
+[](https://pkg.go.dev/github.com/klauspost/compress?tab=subdirectories)
+[](https://github.com/klauspost/compress/actions/workflows/go.yml)
+[](https://sourcegraph.com/github.com/klauspost/compress?badge)
+
+# package usage
+
+Use `go get github.com/klauspost/compress@latest` to add it to your project.
+
+This package will support the current Go version and 2 versions back.
+
+* Use the `nounsafe` tag to disable all use of the "unsafe" package.
+* Use the `noasm` tag to disable all assembly across packages.
+
+Use the links above for more information on each.
+
+# changelog
+
+* Feb 9th, 2026 [1.18.4](https://github.com/klauspost/compress/releases/tag/v1.18.4)
+ * gzhttp: Add zstandard to server handler wrapper https://github.com/klauspost/compress/pull/1121
+ * zstd: Add ResetWithOptions to encoder/decoder https://github.com/klauspost/compress/pull/1122
+ * gzhttp: preserve qvalue when extra parameters follow in Accept-Encoding by @analytically in https://github.com/klauspost/compress/pull/1116
+
+* Jan 16th, 2026 [1.18.3](https://github.com/klauspost/compress/releases/tag/v1.18.3)
+ * Downstream CVE-2025-61728. See [golang/go#77102](https://github.com/golang/go/issues/77102).
+
+* Dec 1st, 2025 - [1.18.2](https://github.com/klauspost/compress/releases/tag/v1.18.2)
+ * flate: Fix invalid encoding on level 9 with single value input in https://github.com/klauspost/compress/pull/1115
+ * flate: reduce stateless allocations by @RXamzin in https://github.com/klauspost/compress/pull/1106
+
+* Oct 20, 2025 - [1.18.1](https://github.com/klauspost/compress/releases/tag/v1.18.1) - RETRACTED
+ * zstd: Add simple zstd EncodeTo/DecodeTo functions https://github.com/klauspost/compress/pull/1079
+ * zstd: Fix incorrect buffer size in dictionary encodes https://github.com/klauspost/compress/pull/1059
+ * s2: check for cap, not len of buffer in EncodeBetter/Best by @vdarulis in https://github.com/klauspost/compress/pull/1080
+ * zlib: Avoiding extra allocation in zlib.reader.Reset by @travelpolicy in https://github.com/klauspost/compress/pull/1086
+ * gzhttp: remove redundant err check in zstdReader by @ryanfowler in https://github.com/klauspost/compress/pull/1090
+ * flate: Faster load+store https://github.com/klauspost/compress/pull/1104
+ * flate: Simplify matchlen https://github.com/klauspost/compress/pull/1101
+ * flate: Use exact sizes for huffman tables https://github.com/klauspost/compress/pull/1103
+
+* Feb 19th, 2025 - [1.18.0](https://github.com/klauspost/compress/releases/tag/v1.18.0)
+ * Add unsafe little endian loaders https://github.com/klauspost/compress/pull/1036
+ * fix: check `r.err != nil` but return a nil value error `err` by @alingse in https://github.com/klauspost/compress/pull/1028
+ * flate: Simplify L4-6 loading https://github.com/klauspost/compress/pull/1043
+ * flate: Simplify matchlen (remove asm) https://github.com/klauspost/compress/pull/1045
+ * s2: Improve small block compression speed w/o asm https://github.com/klauspost/compress/pull/1048
+ * flate: Fix matchlen L5+L6 https://github.com/klauspost/compress/pull/1049
+ * flate: Cleanup & reduce casts https://github.com/klauspost/compress/pull/1050
+
+
+ See changes to v1.17.x
+
+* Oct 11th, 2024 - [1.17.11](https://github.com/klauspost/compress/releases/tag/v1.17.11)
+ * zstd: Fix extra CRC written with multiple Close calls https://github.com/klauspost/compress/pull/1017
+ * s2: Don't use stack for index tables https://github.com/klauspost/compress/pull/1014
+ * gzhttp: No content-type on no body response code by @juliens in https://github.com/klauspost/compress/pull/1011
+ * gzhttp: Do not set the content-type when response has no body by @kevinpollet in https://github.com/klauspost/compress/pull/1013
+
+* Sep 23rd, 2024 - [1.17.10](https://github.com/klauspost/compress/releases/tag/v1.17.10)
+ * gzhttp: Add TransportAlwaysDecompress option. https://github.com/klauspost/compress/pull/978
+ * gzhttp: Add supported decompress request body by @mirecl in https://github.com/klauspost/compress/pull/1002
+ * s2: Add EncodeBuffer buffer recycling callback https://github.com/klauspost/compress/pull/982
+ * zstd: Improve memory usage on small streaming encodes https://github.com/klauspost/compress/pull/1007
+ * flate: read data written with partial flush by @vajexal in https://github.com/klauspost/compress/pull/996
+
+* Jun 12th, 2024 - [1.17.9](https://github.com/klauspost/compress/releases/tag/v1.17.9)
+ * s2: Reduce ReadFrom temporary allocations https://github.com/klauspost/compress/pull/949
+ * flate, zstd: Shave some bytes off amd64 matchLen by @greatroar in https://github.com/klauspost/compress/pull/963
+ * Upgrade zip/zlib to 1.22.4 upstream https://github.com/klauspost/compress/pull/970 https://github.com/klauspost/compress/pull/971
+ * zstd: BuildDict fails with RLE table https://github.com/klauspost/compress/pull/951
+
+* Apr 9th, 2024 - [1.17.8](https://github.com/klauspost/compress/releases/tag/v1.17.8)
+ * zstd: Reject blocks where reserved values are not 0 https://github.com/klauspost/compress/pull/885
+ * zstd: Add RLE detection+encoding https://github.com/klauspost/compress/pull/938
+
+* Feb 21st, 2024 - [1.17.7](https://github.com/klauspost/compress/releases/tag/v1.17.7)
+ * s2: Add AsyncFlush method: Complete the block without flushing by @Jille in https://github.com/klauspost/compress/pull/927
+ * s2: Fix literal+repeat exceeds dst crash https://github.com/klauspost/compress/pull/930
+
+* Feb 5th, 2024 - [1.17.6](https://github.com/klauspost/compress/releases/tag/v1.17.6)
+ * zstd: Fix incorrect repeat coding in best mode https://github.com/klauspost/compress/pull/923
+ * s2: Fix DecodeConcurrent deadlock on errors https://github.com/klauspost/compress/pull/925
+
+* Jan 26th, 2024 - [v1.17.5](https://github.com/klauspost/compress/releases/tag/v1.17.5)
+ * flate: Fix reset with dictionary on custom window encodes https://github.com/klauspost/compress/pull/912
+ * zstd: Add Frame header encoding and stripping https://github.com/klauspost/compress/pull/908
+ * zstd: Limit better/best default window to 8MB https://github.com/klauspost/compress/pull/913
+ * zstd: Speed improvements by @greatroar in https://github.com/klauspost/compress/pull/896 https://github.com/klauspost/compress/pull/910
+ * s2: Fix callbacks for skippable blocks and disallow 0xfe (Padding) by @Jille in https://github.com/klauspost/compress/pull/916 https://github.com/klauspost/compress/pull/917
+https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/compress/pull/918
+
+* Dec 1st, 2023 - [v1.17.4](https://github.com/klauspost/compress/releases/tag/v1.17.4)
+ * huff0: Speed up symbol counting by @greatroar in https://github.com/klauspost/compress/pull/887
+ * huff0: Remove byteReader by @greatroar in https://github.com/klauspost/compress/pull/886
+ * gzhttp: Allow overriding decompression on transport https://github.com/klauspost/compress/pull/892
+ * gzhttp: Clamp compression level https://github.com/klauspost/compress/pull/890
+ * gzip: Error out if reserved bits are set https://github.com/klauspost/compress/pull/891
+
+* Nov 15th, 2023 - [v1.17.3](https://github.com/klauspost/compress/releases/tag/v1.17.3)
+ * fse: Fix max header size https://github.com/klauspost/compress/pull/881
+ * zstd: Improve better/best compression https://github.com/klauspost/compress/pull/877
+ * gzhttp: Fix missing content type on Close https://github.com/klauspost/compress/pull/883
+
+* Oct 22nd, 2023 - [v1.17.2](https://github.com/klauspost/compress/releases/tag/v1.17.2)
+ * zstd: Fix rare *CORRUPTION* output in "best" mode. See https://github.com/klauspost/compress/pull/876
+
+* Oct 14th, 2023 - [v1.17.1](https://github.com/klauspost/compress/releases/tag/v1.17.1)
+ * s2: Fix S2 "best" dictionary wrong encoding https://github.com/klauspost/compress/pull/871
+ * flate: Reduce allocations in decompressor and minor code improvements by @fakefloordiv in https://github.com/klauspost/compress/pull/869
+ * s2: Fix EstimateBlockSize on 6&7 length input https://github.com/klauspost/compress/pull/867
+
+* Sept 19th, 2023 - [v1.17.0](https://github.com/klauspost/compress/releases/tag/v1.17.0)
+ * Add experimental dictionary builder https://github.com/klauspost/compress/pull/853
+ * Add xerial snappy read/writer https://github.com/klauspost/compress/pull/838
+ * flate: Add limited window compression https://github.com/klauspost/compress/pull/843
+ * s2: Do 2 overlapping match checks https://github.com/klauspost/compress/pull/839
+ * flate: Add amd64 assembly matchlen https://github.com/klauspost/compress/pull/837
+ * gzip: Copy bufio.Reader on Reset by @thatguystone in https://github.com/klauspost/compress/pull/860
+
+
+
+ See changes to v1.16.x
+
+
+* July 1st, 2023 - [v1.16.7](https://github.com/klauspost/compress/releases/tag/v1.16.7)
+ * zstd: Fix default level first dictionary encode https://github.com/klauspost/compress/pull/829
+ * s2: add GetBufferCapacity() method by @GiedriusS in https://github.com/klauspost/compress/pull/832
+
+* June 13, 2023 - [v1.16.6](https://github.com/klauspost/compress/releases/tag/v1.16.6)
+ * zstd: correctly ignore WithEncoderPadding(1) by @ianlancetaylor in https://github.com/klauspost/compress/pull/806
+ * zstd: Add amd64 match length assembly https://github.com/klauspost/compress/pull/824
+ * gzhttp: Handle informational headers by @rtribotte in https://github.com/klauspost/compress/pull/815
+ * s2: Improve Better compression slightly https://github.com/klauspost/compress/pull/663
+
+* Apr 16, 2023 - [v1.16.5](https://github.com/klauspost/compress/releases/tag/v1.16.5)
+ * zstd: readByte needs to use io.ReadFull by @jnoxon in https://github.com/klauspost/compress/pull/802
+ * gzip: Fix WriterTo after initial read https://github.com/klauspost/compress/pull/804
+
+* Apr 5, 2023 - [v1.16.4](https://github.com/klauspost/compress/releases/tag/v1.16.4)
+ * zstd: Improve zstd best efficiency by @greatroar and @klauspost in https://github.com/klauspost/compress/pull/784
+ * zstd: Respect WithAllLitEntropyCompression https://github.com/klauspost/compress/pull/792
+ * zstd: Fix amd64 not always detecting corrupt data https://github.com/klauspost/compress/pull/785
+ * zstd: Various minor improvements by @greatroar in https://github.com/klauspost/compress/pull/788 https://github.com/klauspost/compress/pull/794 https://github.com/klauspost/compress/pull/795
+ * s2: Fix huge block overflow https://github.com/klauspost/compress/pull/779
+ * s2: Allow CustomEncoder fallback https://github.com/klauspost/compress/pull/780
+ * gzhttp: Support ResponseWriter Unwrap() in gzhttp handler by @jgimenez in https://github.com/klauspost/compress/pull/799
+
+* Mar 13, 2023 - [v1.16.1](https://github.com/klauspost/compress/releases/tag/v1.16.1)
+ * zstd: Speed up + improve best encoder by @greatroar in https://github.com/klauspost/compress/pull/776
+ * gzhttp: Add optional [BREACH mitigation](https://github.com/klauspost/compress/tree/master/gzhttp#breach-mitigation). https://github.com/klauspost/compress/pull/762 https://github.com/klauspost/compress/pull/768 https://github.com/klauspost/compress/pull/769 https://github.com/klauspost/compress/pull/770 https://github.com/klauspost/compress/pull/767
+ * s2: Add Intel LZ4s converter https://github.com/klauspost/compress/pull/766
+ * zstd: Minor bug fixes https://github.com/klauspost/compress/pull/771 https://github.com/klauspost/compress/pull/772 https://github.com/klauspost/compress/pull/773
+ * huff0: Speed up compress1xDo by @greatroar in https://github.com/klauspost/compress/pull/774
+
+* Feb 26, 2023 - [v1.16.0](https://github.com/klauspost/compress/releases/tag/v1.16.0)
+ * s2: Add [Dictionary](https://github.com/klauspost/compress/tree/master/s2#dictionaries) support. https://github.com/klauspost/compress/pull/685
+ * s2: Add Compression Size Estimate. https://github.com/klauspost/compress/pull/752
+ * s2: Add support for custom stream encoder. https://github.com/klauspost/compress/pull/755
+ * s2: Add LZ4 block converter. https://github.com/klauspost/compress/pull/748
+ * s2: Support io.ReaderAt in ReadSeeker. https://github.com/klauspost/compress/pull/747
+ * s2c/s2sx: Use concurrent decoding. https://github.com/klauspost/compress/pull/746
+
+
+
+ See changes to v1.15.x
+
+* Jan 21st, 2023 (v1.15.15)
+ * deflate: Improve level 7-9 https://github.com/klauspost/compress/pull/739
+ * zstd: Add delta encoding support by @greatroar in https://github.com/klauspost/compress/pull/728
+ * zstd: Various speed improvements by @greatroar https://github.com/klauspost/compress/pull/741 https://github.com/klauspost/compress/pull/734 https://github.com/klauspost/compress/pull/736 https://github.com/klauspost/compress/pull/744 https://github.com/klauspost/compress/pull/743 https://github.com/klauspost/compress/pull/745
+ * gzhttp: Add SuffixETag() and DropETag() options to prevent ETag collisions on compressed responses by @willbicks in https://github.com/klauspost/compress/pull/740
+
+* Jan 3rd, 2023 (v1.15.14)
+
+ * flate: Improve speed in big stateless blocks https://github.com/klauspost/compress/pull/718
+ * zstd: Minor speed tweaks by @greatroar in https://github.com/klauspost/compress/pull/716 https://github.com/klauspost/compress/pull/720
+ * export NoGzipResponseWriter for custom ResponseWriter wrappers by @harshavardhana in https://github.com/klauspost/compress/pull/722
+ * s2: Add example for indexing and existing stream https://github.com/klauspost/compress/pull/723
+
+* Dec 11, 2022 (v1.15.13)
+ * zstd: Add [MaxEncodedSize](https://pkg.go.dev/github.com/klauspost/compress@v1.15.13/zstd#Encoder.MaxEncodedSize) to encoder https://github.com/klauspost/compress/pull/691
+ * zstd: Various tweaks and improvements https://github.com/klauspost/compress/pull/693 https://github.com/klauspost/compress/pull/695 https://github.com/klauspost/compress/pull/696 https://github.com/klauspost/compress/pull/701 https://github.com/klauspost/compress/pull/702 https://github.com/klauspost/compress/pull/703 https://github.com/klauspost/compress/pull/704 https://github.com/klauspost/compress/pull/705 https://github.com/klauspost/compress/pull/706 https://github.com/klauspost/compress/pull/707 https://github.com/klauspost/compress/pull/708
+
+* Oct 26, 2022 (v1.15.12)
+
+ * zstd: Tweak decoder allocs. https://github.com/klauspost/compress/pull/680
+ * gzhttp: Always delete `HeaderNoCompression` https://github.com/klauspost/compress/pull/683
+
+* Sept 26, 2022 (v1.15.11)
+
+ * flate: Improve level 1-3 compression https://github.com/klauspost/compress/pull/678
+ * zstd: Improve "best" compression by @nightwolfz in https://github.com/klauspost/compress/pull/677
+ * zstd: Fix+reduce decompression allocations https://github.com/klauspost/compress/pull/668
+ * zstd: Fix non-effective noescape tag https://github.com/klauspost/compress/pull/667
+
+* Sept 16, 2022 (v1.15.10)
+
+ * zstd: Add [WithDecodeAllCapLimit](https://pkg.go.dev/github.com/klauspost/compress@v1.15.10/zstd#WithDecodeAllCapLimit) https://github.com/klauspost/compress/pull/649
+ * Add Go 1.19 - deprecate Go 1.16 https://github.com/klauspost/compress/pull/651
+ * flate: Improve level 5+6 compression https://github.com/klauspost/compress/pull/656
+ * zstd: Improve "better" compression https://github.com/klauspost/compress/pull/657
+ * s2: Improve "best" compression https://github.com/klauspost/compress/pull/658
+ * s2: Improve "better" compression. https://github.com/klauspost/compress/pull/635
+ * s2: Slightly faster non-assembly decompression https://github.com/klauspost/compress/pull/646
+ * Use arrays for constant size copies https://github.com/klauspost/compress/pull/659
+
+* July 21, 2022 (v1.15.9)
+
+ * zstd: Fix decoder crash on amd64 (no BMI) on invalid input https://github.com/klauspost/compress/pull/645
+ * zstd: Disable decoder extended memory copies (amd64) due to possible crashes https://github.com/klauspost/compress/pull/644
+ * zstd: Allow single segments up to "max decoded size" https://github.com/klauspost/compress/pull/643
+
+* July 13, 2022 (v1.15.8)
+
+ * gzip: fix stack exhaustion bug in Reader.Read https://github.com/klauspost/compress/pull/641
+ * s2: Add Index header trim/restore https://github.com/klauspost/compress/pull/638
+ * zstd: Optimize seqdeq amd64 asm by @greatroar in https://github.com/klauspost/compress/pull/636
+ * zstd: Improve decoder memcopy https://github.com/klauspost/compress/pull/637
+ * huff0: Pass a single bitReader pointer to asm by @greatroar in https://github.com/klauspost/compress/pull/634
+ * zstd: Branchless getBits for amd64 w/o BMI2 by @greatroar in https://github.com/klauspost/compress/pull/640
+ * gzhttp: Remove header before writing https://github.com/klauspost/compress/pull/639
+
+* June 29, 2022 (v1.15.7)
+
+ * s2: Fix absolute forward seeks https://github.com/klauspost/compress/pull/633
+ * zip: Merge upstream https://github.com/klauspost/compress/pull/631
+ * zip: Re-add zip64 fix https://github.com/klauspost/compress/pull/624
+ * zstd: translate fseDecoder.buildDtable into asm by @WojciechMula in https://github.com/klauspost/compress/pull/598
+ * flate: Faster histograms https://github.com/klauspost/compress/pull/620
+ * deflate: Use compound hcode https://github.com/klauspost/compress/pull/622
+
+* June 3, 2022 (v1.15.6)
+ * s2: Improve coding for long, close matches https://github.com/klauspost/compress/pull/613
+ * s2c: Add Snappy/S2 stream recompression https://github.com/klauspost/compress/pull/611
+ * zstd: Always use configured block size https://github.com/klauspost/compress/pull/605
+ * zstd: Fix incorrect hash table placement for dict encoding in default https://github.com/klauspost/compress/pull/606
+ * zstd: Apply default config to ZipDecompressor without options https://github.com/klauspost/compress/pull/608
+ * gzhttp: Exclude more common archive formats https://github.com/klauspost/compress/pull/612
+ * s2: Add ReaderIgnoreCRC https://github.com/klauspost/compress/pull/609
+ * s2: Remove sanity load on index creation https://github.com/klauspost/compress/pull/607
+ * snappy: Use dedicated function for scoring https://github.com/klauspost/compress/pull/614
+ * s2c+s2d: Use official snappy framed extension https://github.com/klauspost/compress/pull/610
+
+* May 25, 2022 (v1.15.5)
+ * s2: Add concurrent stream decompression https://github.com/klauspost/compress/pull/602
+ * s2: Fix final emit oob read crash on amd64 https://github.com/klauspost/compress/pull/601
+ * huff0: asm implementation of Decompress1X by @WojciechMula https://github.com/klauspost/compress/pull/596
+ * zstd: Use 1 less goroutine for stream decoding https://github.com/klauspost/compress/pull/588
+ * zstd: Copy literal in 16 byte blocks when possible https://github.com/klauspost/compress/pull/592
+ * zstd: Speed up when WithDecoderLowmem(false) https://github.com/klauspost/compress/pull/599
+ * zstd: faster next state update in BMI2 version of decode by @WojciechMula in https://github.com/klauspost/compress/pull/593
+ * huff0: Do not check max size when reading table. https://github.com/klauspost/compress/pull/586
+ * flate: Inplace hashing for level 7-9 https://github.com/klauspost/compress/pull/590
+
+
+* May 11, 2022 (v1.15.4)
+ * huff0: decompress directly into output by @WojciechMula in [#577](https://github.com/klauspost/compress/pull/577)
+ * inflate: Keep dict on stack [#581](https://github.com/klauspost/compress/pull/581)
+ * zstd: Faster decoding memcopy in asm [#583](https://github.com/klauspost/compress/pull/583)
+ * zstd: Fix ignored crc [#580](https://github.com/klauspost/compress/pull/580)
+
+* May 5, 2022 (v1.15.3)
+ * zstd: Allow to ignore checksum checking by @WojciechMula [#572](https://github.com/klauspost/compress/pull/572)
+ * s2: Fix incorrect seek for io.SeekEnd in [#575](https://github.com/klauspost/compress/pull/575)
+
+* Apr 26, 2022 (v1.15.2)
+ * zstd: Add x86-64 assembly for decompression on streams and blocks. Contributed by [@WojciechMula](https://github.com/WojciechMula). Typically 2x faster. [#528](https://github.com/klauspost/compress/pull/528) [#531](https://github.com/klauspost/compress/pull/531) [#545](https://github.com/klauspost/compress/pull/545) [#537](https://github.com/klauspost/compress/pull/537)
+ * zstd: Add options to ZipDecompressor and fixes [#539](https://github.com/klauspost/compress/pull/539)
+ * s2: Use sorted search for index [#555](https://github.com/klauspost/compress/pull/555)
+ * Minimum version is Go 1.16, added CI test on 1.18.
+
+* Mar 11, 2022 (v1.15.1)
+ * huff0: Add x86 assembly of Decode4X by @WojciechMula in [#512](https://github.com/klauspost/compress/pull/512)
+ * zstd: Reuse zip decoders in [#514](https://github.com/klauspost/compress/pull/514)
+ * zstd: Detect extra block data and report as corrupted in [#520](https://github.com/klauspost/compress/pull/520)
+ * zstd: Handle zero sized frame content size stricter in [#521](https://github.com/klauspost/compress/pull/521)
+ * zstd: Add stricter block size checks in [#523](https://github.com/klauspost/compress/pull/523)
+
+* Mar 3, 2022 (v1.15.0)
+ * zstd: Refactor decoder [#498](https://github.com/klauspost/compress/pull/498)
+ * zstd: Add stream encoding without goroutines [#505](https://github.com/klauspost/compress/pull/505)
+ * huff0: Prevent single blocks exceeding 16 bits by @klauspost in[#507](https://github.com/klauspost/compress/pull/507)
+ * flate: Inline literal emission [#509](https://github.com/klauspost/compress/pull/509)
+ * gzhttp: Add zstd to transport [#400](https://github.com/klauspost/compress/pull/400)
+ * gzhttp: Make content-type optional [#510](https://github.com/klauspost/compress/pull/510)
+
+Both compression and decompression now supports "synchronous" stream operations. This means that whenever "concurrency" is set to 1, they will operate without spawning goroutines.
+
+Stream decompression is now faster on asynchronous, since the goroutine allocation much more effectively splits the workload. On typical streams this will typically use 2 cores fully for decompression. When a stream has finished decoding no goroutines will be left over, so decoders can now safely be pooled and still be garbage collected.
+
+While the release has been extensively tested, it is recommended to testing when upgrading.
+
+
+
+
+ See changes to v1.14.x
+
+* Feb 22, 2022 (v1.14.4)
+ * flate: Fix rare huffman only (-2) corruption. [#503](https://github.com/klauspost/compress/pull/503)
+ * zip: Update deprecated CreateHeaderRaw to correctly call CreateRaw by @saracen in [#502](https://github.com/klauspost/compress/pull/502)
+ * zip: don't read data descriptor early by @saracen in [#501](https://github.com/klauspost/compress/pull/501) #501
+ * huff0: Use static decompression buffer up to 30% faster [#499](https://github.com/klauspost/compress/pull/499) [#500](https://github.com/klauspost/compress/pull/500)
+
+* Feb 17, 2022 (v1.14.3)
+ * flate: Improve fastest levels compression speed ~10% more throughput. [#482](https://github.com/klauspost/compress/pull/482) [#489](https://github.com/klauspost/compress/pull/489) [#490](https://github.com/klauspost/compress/pull/490) [#491](https://github.com/klauspost/compress/pull/491) [#494](https://github.com/klauspost/compress/pull/494) [#478](https://github.com/klauspost/compress/pull/478)
+ * flate: Faster decompression speed, ~5-10%. [#483](https://github.com/klauspost/compress/pull/483)
+ * s2: Faster compression with Go v1.18 and amd64 microarch level 3+. [#484](https://github.com/klauspost/compress/pull/484) [#486](https://github.com/klauspost/compress/pull/486)
+
+* Jan 25, 2022 (v1.14.2)
+ * zstd: improve header decoder by @dsnet [#476](https://github.com/klauspost/compress/pull/476)
+ * zstd: Add bigger default blocks [#469](https://github.com/klauspost/compress/pull/469)
+ * zstd: Remove unused decompression buffer [#470](https://github.com/klauspost/compress/pull/470)
+ * zstd: Fix logically dead code by @ningmingxiao [#472](https://github.com/klauspost/compress/pull/472)
+ * flate: Improve level 7-9 [#471](https://github.com/klauspost/compress/pull/471) [#473](https://github.com/klauspost/compress/pull/473)
+ * zstd: Add noasm tag for xxhash [#475](https://github.com/klauspost/compress/pull/475)
+
+* Jan 11, 2022 (v1.14.1)
+ * s2: Add stream index in [#462](https://github.com/klauspost/compress/pull/462)
+ * flate: Speed and efficiency improvements in [#439](https://github.com/klauspost/compress/pull/439) [#461](https://github.com/klauspost/compress/pull/461) [#455](https://github.com/klauspost/compress/pull/455) [#452](https://github.com/klauspost/compress/pull/452) [#458](https://github.com/klauspost/compress/pull/458)
+ * zstd: Performance improvement in [#420]( https://github.com/klauspost/compress/pull/420) [#456](https://github.com/klauspost/compress/pull/456) [#437](https://github.com/klauspost/compress/pull/437) [#467](https://github.com/klauspost/compress/pull/467) [#468](https://github.com/klauspost/compress/pull/468)
+ * zstd: add arm64 xxhash assembly in [#464](https://github.com/klauspost/compress/pull/464)
+ * Add garbled for binaries for s2 in [#445](https://github.com/klauspost/compress/pull/445)
+
+
+
+ See changes to v1.13.x
+
+* Aug 30, 2021 (v1.13.5)
+ * gz/zlib/flate: Alias stdlib errors [#425](https://github.com/klauspost/compress/pull/425)
+ * s2: Add block support to commandline tools [#413](https://github.com/klauspost/compress/pull/413)
+ * zstd: pooledZipWriter should return Writers to the same pool [#426](https://github.com/klauspost/compress/pull/426)
+ * Removed golang/snappy as external dependency for tests [#421](https://github.com/klauspost/compress/pull/421)
+
+* Aug 12, 2021 (v1.13.4)
+ * Add [snappy replacement package](https://github.com/klauspost/compress/tree/master/snappy).
+ * zstd: Fix incorrect encoding in "best" mode [#415](https://github.com/klauspost/compress/pull/415)
+
+* Aug 3, 2021 (v1.13.3)
+ * zstd: Improve Best compression [#404](https://github.com/klauspost/compress/pull/404)
+ * zstd: Fix WriteTo error forwarding [#411](https://github.com/klauspost/compress/pull/411)
+ * gzhttp: Return http.HandlerFunc instead of http.Handler. Unlikely breaking change. [#406](https://github.com/klauspost/compress/pull/406)
+ * s2sx: Fix max size error [#399](https://github.com/klauspost/compress/pull/399)
+ * zstd: Add optional stream content size on reset [#401](https://github.com/klauspost/compress/pull/401)
+ * zstd: use SpeedBestCompression for level >= 10 [#410](https://github.com/klauspost/compress/pull/410)
+
+* Jun 14, 2021 (v1.13.1)
+ * s2: Add full Snappy output support [#396](https://github.com/klauspost/compress/pull/396)
+ * zstd: Add configurable [Decoder window](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithDecoderMaxWindow) size [#394](https://github.com/klauspost/compress/pull/394)
+ * gzhttp: Add header to skip compression [#389](https://github.com/klauspost/compress/pull/389)
+ * s2: Improve speed with bigger output margin [#395](https://github.com/klauspost/compress/pull/395)
+
+* Jun 3, 2021 (v1.13.0)
+ * Added [gzhttp](https://github.com/klauspost/compress/tree/master/gzhttp#gzip-handler) which allows wrapping HTTP servers and clients with GZIP compressors.
+ * zstd: Detect short invalid signatures [#382](https://github.com/klauspost/compress/pull/382)
+ * zstd: Spawn decoder goroutine only if needed. [#380](https://github.com/klauspost/compress/pull/380)
+
+
+
+
+ See changes to v1.12.x
+
+* May 25, 2021 (v1.12.3)
+ * deflate: Better/faster Huffman encoding [#374](https://github.com/klauspost/compress/pull/374)
+ * deflate: Allocate less for history. [#375](https://github.com/klauspost/compress/pull/375)
+ * zstd: Forward read errors [#373](https://github.com/klauspost/compress/pull/373)
+
+* Apr 27, 2021 (v1.12.2)
+ * zstd: Improve better/best compression [#360](https://github.com/klauspost/compress/pull/360) [#364](https://github.com/klauspost/compress/pull/364) [#365](https://github.com/klauspost/compress/pull/365)
+ * zstd: Add helpers to compress/decompress zstd inside zip files [#363](https://github.com/klauspost/compress/pull/363)
+ * deflate: Improve level 5+6 compression [#367](https://github.com/klauspost/compress/pull/367)
+ * s2: Improve better/best compression [#358](https://github.com/klauspost/compress/pull/358) [#359](https://github.com/klauspost/compress/pull/358)
+ * s2: Load after checking src limit on amd64. [#362](https://github.com/klauspost/compress/pull/362)
+ * s2sx: Limit max executable size [#368](https://github.com/klauspost/compress/pull/368)
+
+* Apr 14, 2021 (v1.12.1)
+ * snappy package removed. Upstream added as dependency.
+ * s2: Better compression in "best" mode [#353](https://github.com/klauspost/compress/pull/353)
+ * s2sx: Add stdin input and detect pre-compressed from signature [#352](https://github.com/klauspost/compress/pull/352)
+ * s2c/s2d: Add http as possible input [#348](https://github.com/klauspost/compress/pull/348)
+ * s2c/s2d/s2sx: Always truncate when writing files [#352](https://github.com/klauspost/compress/pull/352)
+ * zstd: Reduce memory usage further when using [WithLowerEncoderMem](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithLowerEncoderMem) [#346](https://github.com/klauspost/compress/pull/346)
+ * s2: Fix potential problem with amd64 assembly and profilers [#349](https://github.com/klauspost/compress/pull/349)
+
+
+
+ See changes to v1.11.x
+
+* Mar 26, 2021 (v1.11.13)
+ * zstd: Big speedup on small dictionary encodes [#344](https://github.com/klauspost/compress/pull/344) [#345](https://github.com/klauspost/compress/pull/345)
+ * zstd: Add [WithLowerEncoderMem](https://pkg.go.dev/github.com/klauspost/compress/zstd#WithLowerEncoderMem) encoder option [#336](https://github.com/klauspost/compress/pull/336)
+ * deflate: Improve entropy compression [#338](https://github.com/klauspost/compress/pull/338)
+ * s2: Clean up and minor performance improvement in best [#341](https://github.com/klauspost/compress/pull/341)
+
+* Mar 5, 2021 (v1.11.12)
+ * s2: Add `s2sx` binary that creates [self extracting archives](https://github.com/klauspost/compress/tree/master/s2#s2sx-self-extracting-archives).
+ * s2: Speed up decompression on non-assembly platforms [#328](https://github.com/klauspost/compress/pull/328)
+
+* Mar 1, 2021 (v1.11.9)
+ * s2: Add ARM64 decompression assembly. Around 2x output speed. [#324](https://github.com/klauspost/compress/pull/324)
+ * s2: Improve "better" speed and efficiency. [#325](https://github.com/klauspost/compress/pull/325)
+ * s2: Fix binaries.
+
+* Feb 25, 2021 (v1.11.8)
+ * s2: Fixed occasional out-of-bounds write on amd64. Upgrade recommended.
+ * s2: Add AMD64 assembly for better mode. 25-50% faster. [#315](https://github.com/klauspost/compress/pull/315)
+ * s2: Less upfront decoder allocation. [#322](https://github.com/klauspost/compress/pull/322)
+ * zstd: Faster "compression" of incompressible data. [#314](https://github.com/klauspost/compress/pull/314)
+ * zip: Fix zip64 headers. [#313](https://github.com/klauspost/compress/pull/313)
+
+* Jan 14, 2021 (v1.11.7)
+ * Use Bytes() interface to get bytes across packages. [#309](https://github.com/klauspost/compress/pull/309)
+ * s2: Add 'best' compression option. [#310](https://github.com/klauspost/compress/pull/310)
+ * s2: Add ReaderMaxBlockSize, changes `s2.NewReader` signature to include varargs. [#311](https://github.com/klauspost/compress/pull/311)
+ * s2: Fix crash on small better buffers. [#308](https://github.com/klauspost/compress/pull/308)
+ * s2: Clean up decoder. [#312](https://github.com/klauspost/compress/pull/312)
+
+* Jan 7, 2021 (v1.11.6)
+ * zstd: Make decoder allocations smaller [#306](https://github.com/klauspost/compress/pull/306)
+ * zstd: Free Decoder resources when Reset is called with a nil io.Reader [#305](https://github.com/klauspost/compress/pull/305)
+
+* Dec 20, 2020 (v1.11.4)
+ * zstd: Add Best compression mode [#304](https://github.com/klauspost/compress/pull/304)
+ * Add header decoder [#299](https://github.com/klauspost/compress/pull/299)
+ * s2: Add uncompressed stream option [#297](https://github.com/klauspost/compress/pull/297)
+ * Simplify/speed up small blocks with known max size. [#300](https://github.com/klauspost/compress/pull/300)
+ * zstd: Always reset literal dict encoder [#303](https://github.com/klauspost/compress/pull/303)
+
+* Nov 15, 2020 (v1.11.3)
+ * inflate: 10-15% faster decompression [#293](https://github.com/klauspost/compress/pull/293)
+ * zstd: Tweak DecodeAll default allocation [#295](https://github.com/klauspost/compress/pull/295)
+
+* Oct 11, 2020 (v1.11.2)
+ * s2: Fix out of bounds read in "better" block compression [#291](https://github.com/klauspost/compress/pull/291)
+
+* Oct 1, 2020 (v1.11.1)
+ * zstd: Set allLitEntropy true in default configuration [#286](https://github.com/klauspost/compress/pull/286)
+
+* Sept 8, 2020 (v1.11.0)
+ * zstd: Add experimental compression [dictionaries](https://github.com/klauspost/compress/tree/master/zstd#dictionaries) [#281](https://github.com/klauspost/compress/pull/281)
+ * zstd: Fix mixed Write and ReadFrom calls [#282](https://github.com/klauspost/compress/pull/282)
+ * inflate/gz: Limit variable shifts, ~5% faster decompression [#274](https://github.com/klauspost/compress/pull/274)
+
+
+
+ See changes to v1.10.x
+
+* July 8, 2020 (v1.10.11)
+ * zstd: Fix extra block when compressing with ReadFrom. [#278](https://github.com/klauspost/compress/pull/278)
+ * huff0: Also populate compression table when reading decoding table. [#275](https://github.com/klauspost/compress/pull/275)
+
+* June 23, 2020 (v1.10.10)
+ * zstd: Skip entropy compression in fastest mode when no matches. [#270](https://github.com/klauspost/compress/pull/270)
+
+* June 16, 2020 (v1.10.9):
+ * zstd: API change for specifying dictionaries. See [#268](https://github.com/klauspost/compress/pull/268)
+ * zip: update CreateHeaderRaw to handle zip64 fields. [#266](https://github.com/klauspost/compress/pull/266)
+ * Fuzzit tests removed. The service has been purchased and is no longer available.
+
+* June 5, 2020 (v1.10.8):
+ * 1.15x faster zstd block decompression. [#265](https://github.com/klauspost/compress/pull/265)
+
+* June 1, 2020 (v1.10.7):
+ * Added zstd decompression [dictionary support](https://github.com/klauspost/compress/tree/master/zstd#dictionaries)
+ * Increase zstd decompression speed up to 1.19x. [#259](https://github.com/klauspost/compress/pull/259)
+ * Remove internal reset call in zstd compression and reduce allocations. [#263](https://github.com/klauspost/compress/pull/263)
+
+* May 21, 2020: (v1.10.6)
+ * zstd: Reduce allocations while decoding. [#258](https://github.com/klauspost/compress/pull/258), [#252](https://github.com/klauspost/compress/pull/252)
+ * zstd: Stricter decompression checks.
+
+* April 12, 2020: (v1.10.5)
+ * s2-commands: Flush output when receiving SIGINT. [#239](https://github.com/klauspost/compress/pull/239)
+
+* Apr 8, 2020: (v1.10.4)
+ * zstd: Minor/special case optimizations. [#251](https://github.com/klauspost/compress/pull/251), [#250](https://github.com/klauspost/compress/pull/250), [#249](https://github.com/klauspost/compress/pull/249), [#247](https://github.com/klauspost/compress/pull/247)
+* Mar 11, 2020: (v1.10.3)
+ * s2: Use S2 encoder in pure Go mode for Snappy output as well. [#245](https://github.com/klauspost/compress/pull/245)
+ * s2: Fix pure Go block encoder. [#244](https://github.com/klauspost/compress/pull/244)
+ * zstd: Added "better compression" mode. [#240](https://github.com/klauspost/compress/pull/240)
+ * zstd: Improve speed of fastest compression mode by 5-10% [#241](https://github.com/klauspost/compress/pull/241)
+ * zstd: Skip creating encoders when not needed. [#238](https://github.com/klauspost/compress/pull/238)
+
+* Feb 27, 2020: (v1.10.2)
+ * Close to 50% speedup in inflate (gzip/zip decompression). [#236](https://github.com/klauspost/compress/pull/236) [#234](https://github.com/klauspost/compress/pull/234) [#232](https://github.com/klauspost/compress/pull/232)
+ * Reduce deflate level 1-6 memory usage up to 59%. [#227](https://github.com/klauspost/compress/pull/227)
+
+* Feb 18, 2020: (v1.10.1)
+ * Fix zstd crash when resetting multiple times without sending data. [#226](https://github.com/klauspost/compress/pull/226)
+ * deflate: Fix dictionary use on level 1-6. [#224](https://github.com/klauspost/compress/pull/224)
+ * Remove deflate writer reference when closing. [#224](https://github.com/klauspost/compress/pull/224)
+
+* Feb 4, 2020: (v1.10.0)
+ * Add optional dictionary to [stateless deflate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc#StatelessDeflate). Breaking change, send `nil` for previous behaviour. [#216](https://github.com/klauspost/compress/pull/216)
+ * Fix buffer overflow on repeated small block deflate. [#218](https://github.com/klauspost/compress/pull/218)
+ * Allow copying content from an existing ZIP file without decompressing+compressing. [#214](https://github.com/klauspost/compress/pull/214)
+ * Added [S2](https://github.com/klauspost/compress/tree/master/s2#s2-compression) AMD64 assembler and various optimizations. Stream speed >10GB/s. [#186](https://github.com/klauspost/compress/pull/186)
+
+
+
+
+ See changes prior to v1.10.0
+
+* Jan 20,2020 (v1.9.8) Optimize gzip/deflate with better size estimates and faster table generation. [#207](https://github.com/klauspost/compress/pull/207) by [luyu6056](https://github.com/luyu6056), [#206](https://github.com/klauspost/compress/pull/206).
+* Jan 11, 2020: S2 Encode/Decode will use provided buffer if capacity is big enough. [#204](https://github.com/klauspost/compress/pull/204)
+* Jan 5, 2020: (v1.9.7) Fix another zstd regression in v1.9.5 - v1.9.6 removed.
+* Jan 4, 2020: (v1.9.6) Regression in v1.9.5 fixed causing corrupt zstd encodes in rare cases.
+* Jan 4, 2020: Faster IO in [s2c + s2d commandline tools](https://github.com/klauspost/compress/tree/master/s2#commandline-tools) compression/decompression. [#192](https://github.com/klauspost/compress/pull/192)
+* Dec 29, 2019: Removed v1.9.5 since fuzz tests showed a compatibility problem with the reference zstandard decoder.
+* Dec 29, 2019: (v1.9.5) zstd: 10-20% faster block compression. [#199](https://github.com/klauspost/compress/pull/199)
+* Dec 29, 2019: [zip](https://godoc.org/github.com/klauspost/compress/zip) package updated with latest Go features
+* Dec 29, 2019: zstd: Single segment flag condintions tweaked. [#197](https://github.com/klauspost/compress/pull/197)
+* Dec 18, 2019: s2: Faster compression when ReadFrom is used. [#198](https://github.com/klauspost/compress/pull/198)
+* Dec 10, 2019: s2: Fix repeat length output when just above at 16MB limit.
+* Dec 10, 2019: zstd: Add function to get decoder as io.ReadCloser. [#191](https://github.com/klauspost/compress/pull/191)
+* Dec 3, 2019: (v1.9.4) S2: limit max repeat length. [#188](https://github.com/klauspost/compress/pull/188)
+* Dec 3, 2019: Add [WithNoEntropyCompression](https://godoc.org/github.com/klauspost/compress/zstd#WithNoEntropyCompression) to zstd [#187](https://github.com/klauspost/compress/pull/187)
+* Dec 3, 2019: Reduce memory use for tests. Check for leaked goroutines.
+* Nov 28, 2019 (v1.9.3) Less allocations in stateless deflate.
+* Nov 28, 2019: 5-20% Faster huff0 decode. Impacts zstd as well. [#184](https://github.com/klauspost/compress/pull/184)
+* Nov 12, 2019 (v1.9.2) Added [Stateless Compression](#stateless-compression) for gzip/deflate.
+* Nov 12, 2019: Fixed zstd decompression of large single blocks. [#180](https://github.com/klauspost/compress/pull/180)
+* Nov 11, 2019: Set default [s2c](https://github.com/klauspost/compress/tree/master/s2#commandline-tools) block size to 4MB.
+* Nov 11, 2019: Reduce inflate memory use by 1KB.
+* Nov 10, 2019: Less allocations in deflate bit writer.
+* Nov 10, 2019: Fix inconsistent error returned by zstd decoder.
+* Oct 28, 2019 (v1.9.1) ztsd: Fix crash when compressing blocks. [#174](https://github.com/klauspost/compress/pull/174)
+* Oct 24, 2019 (v1.9.0) zstd: Fix rare data corruption [#173](https://github.com/klauspost/compress/pull/173)
+* Oct 24, 2019 zstd: Fix huff0 out of buffer write [#171](https://github.com/klauspost/compress/pull/171) and always return errors [#172](https://github.com/klauspost/compress/pull/172)
+* Oct 10, 2019: Big deflate rewrite, 30-40% faster with better compression [#105](https://github.com/klauspost/compress/pull/105)
+
+
+
+
+ See changes prior to v1.9.0
+
+* Oct 10, 2019: (v1.8.6) zstd: Allow partial reads to get flushed data. [#169](https://github.com/klauspost/compress/pull/169)
+* Oct 3, 2019: Fix inconsistent results on broken zstd streams.
+* Sep 25, 2019: Added `-rm` (remove source files) and `-q` (no output except errors) to `s2c` and `s2d` [commands](https://github.com/klauspost/compress/tree/master/s2#commandline-tools)
+* Sep 16, 2019: (v1.8.4) Add `s2c` and `s2d` [commandline tools](https://github.com/klauspost/compress/tree/master/s2#commandline-tools).
+* Sep 10, 2019: (v1.8.3) Fix s2 decoder [Skip](https://godoc.org/github.com/klauspost/compress/s2#Reader.Skip).
+* Sep 7, 2019: zstd: Added [WithWindowSize](https://godoc.org/github.com/klauspost/compress/zstd#WithWindowSize), contributed by [ianwilkes](https://github.com/ianwilkes).
+* Sep 5, 2019: (v1.8.2) Add [WithZeroFrames](https://godoc.org/github.com/klauspost/compress/zstd#WithZeroFrames) which adds full zero payload block encoding option.
+* Sep 5, 2019: Lazy initialization of zstandard predefined en/decoder tables.
+* Aug 26, 2019: (v1.8.1) S2: 1-2% compression increase in "better" compression mode.
+* Aug 26, 2019: zstd: Check maximum size of Huffman 1X compressed literals while decoding.
+* Aug 24, 2019: (v1.8.0) Added [S2 compression](https://github.com/klauspost/compress/tree/master/s2#s2-compression), a high performance replacement for Snappy.
+* Aug 21, 2019: (v1.7.6) Fixed minor issues found by fuzzer. One could lead to zstd not decompressing.
+* Aug 18, 2019: Add [fuzzit](https://fuzzit.dev/) continuous fuzzing.
+* Aug 14, 2019: zstd: Skip incompressible data 2x faster. [#147](https://github.com/klauspost/compress/pull/147)
+* Aug 4, 2019 (v1.7.5): Better literal compression. [#146](https://github.com/klauspost/compress/pull/146)
+* Aug 4, 2019: Faster zstd compression. [#143](https://github.com/klauspost/compress/pull/143) [#144](https://github.com/klauspost/compress/pull/144)
+* Aug 4, 2019: Faster zstd decompression. [#145](https://github.com/klauspost/compress/pull/145) [#143](https://github.com/klauspost/compress/pull/143) [#142](https://github.com/klauspost/compress/pull/142)
+* July 15, 2019 (v1.7.4): Fix double EOF block in rare cases on zstd encoder.
+* July 15, 2019 (v1.7.3): Minor speedup/compression increase in default zstd encoder.
+* July 14, 2019: zstd decoder: Fix decompression error on multiple uses with mixed content.
+* July 7, 2019 (v1.7.2): Snappy update, zstd decoder potential race fix.
+* June 17, 2019: zstd decompression bugfix.
+* June 17, 2019: fix 32 bit builds.
+* June 17, 2019: Easier use in modules (less dependencies).
+* June 9, 2019: New stronger "default" [zstd](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression mode. Matches zstd default compression ratio.
+* June 5, 2019: 20-40% throughput in [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and better compression.
+* June 5, 2019: deflate/gzip compression: Reduce memory usage of lower compression levels.
+* June 2, 2019: Added [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression!
+* May 25, 2019: deflate/gzip: 10% faster bit writer, mostly visible in lower levels.
+* Apr 22, 2019: [zstd](https://github.com/klauspost/compress/tree/master/zstd#zstd) decompression added.
+* Aug 1, 2018: Added [huff0 README](https://github.com/klauspost/compress/tree/master/huff0#huff0-entropy-compression).
+* Jul 8, 2018: Added [Performance Update 2018](#performance-update-2018) below.
+* Jun 23, 2018: Merged [Go 1.11 inflate optimizations](https://go-review.googlesource.com/c/go/+/102235). Go 1.9 is now required. Backwards compatible version tagged with [v1.3.0](https://github.com/klauspost/compress/releases/tag/v1.3.0).
+* Apr 2, 2018: Added [huff0](https://godoc.org/github.com/klauspost/compress/huff0) en/decoder. Experimental for now, API may change.
+* Mar 4, 2018: Added [FSE Entropy](https://godoc.org/github.com/klauspost/compress/fse) en/decoder. Experimental for now, API may change.
+* Nov 3, 2017: Add compression [Estimate](https://godoc.org/github.com/klauspost/compress#Estimate) function.
+* May 28, 2017: Reduce allocations when resetting decoder.
+* Apr 02, 2017: Change back to official crc32, since changes were merged in Go 1.7.
+* Jan 14, 2017: Reduce stack pressure due to array copies. See [Issue #18625](https://github.com/golang/go/issues/18625).
+* Oct 25, 2016: Level 2-4 have been rewritten and now offers significantly better performance than before.
+* Oct 20, 2016: Port zlib changes from Go 1.7 to fix zlib writer issue. Please update.
+* Oct 16, 2016: Go 1.7 changes merged. Apples to apples this package is a few percent faster, but has a significantly better balance between speed and compression per level.
+* Mar 24, 2016: Always attempt Huffman encoding on level 4-7. This improves base 64 encoded data compression.
+* Mar 24, 2016: Small speedup for level 1-3.
+* Feb 19, 2016: Faster bit writer, level -2 is 15% faster, level 1 is 4% faster.
+* Feb 19, 2016: Handle small payloads faster in level 1-3.
+* Feb 19, 2016: Added faster level 2 + 3 compression modes.
+* Feb 19, 2016: [Rebalanced compression levels](https://blog.klauspost.com/rebalancing-deflate-compression-levels/), so there is a more even progression in terms of compression. New default level is 5.
+* Feb 14, 2016: Snappy: Merge upstream changes.
+* Feb 14, 2016: Snappy: Fix aggressive skipping.
+* Feb 14, 2016: Snappy: Update benchmark.
+* Feb 13, 2016: Deflate: Fixed assembler problem that could lead to sub-optimal compression.
+* Feb 12, 2016: Snappy: Added AMD64 SSE 4.2 optimizations to matching, which makes easy to compress material run faster. Typical speedup is around 25%.
+* Feb 9, 2016: Added Snappy package fork. This version is 5-7% faster, much more on hard to compress content.
+* Jan 30, 2016: Optimize level 1 to 3 by not considering static dictionary or storing uncompressed. ~4-5% speedup.
+* Jan 16, 2016: Optimization on deflate level 1,2,3 compression.
+* Jan 8 2016: Merge [CL 18317](https://go-review.googlesource.com/#/c/18317): fix reading, writing of zip64 archives.
+* Dec 8 2015: Make level 1 and -2 deterministic even if write size differs.
+* Dec 8 2015: Split encoding functions, so hashing and matching can potentially be inlined. 1-3% faster on AMD64. 5% faster on other platforms.
+* Dec 8 2015: Fixed rare [one byte out-of bounds read](https://github.com/klauspost/compress/issues/20). Please update!
+* Nov 23 2015: Optimization on token writer. ~2-4% faster. Contributed by [@dsnet](https://github.com/dsnet).
+* Nov 20 2015: Small optimization to bit writer on 64 bit systems.
+* Nov 17 2015: Fixed out-of-bound errors if the underlying Writer returned an error. See [#15](https://github.com/klauspost/compress/issues/15).
+* Nov 12 2015: Added [io.WriterTo](https://golang.org/pkg/io/#WriterTo) support to gzip/inflate.
+* Nov 11 2015: Merged [CL 16669](https://go-review.googlesource.com/#/c/16669/4): archive/zip: enable overriding (de)compressors per file
+* Oct 15 2015: Added skipping on uncompressible data. Random data speed up >5x.
+
+
+
+# deflate usage
+
+The packages are drop-in replacements for standard library [deflate](https://godoc.org/github.com/klauspost/compress/flate), [gzip](https://godoc.org/github.com/klauspost/compress/gzip), [zip](https://godoc.org/github.com/klauspost/compress/zip), and [zlib](https://godoc.org/github.com/klauspost/compress/zlib). Simply replace the import path to use them:
+
+Typical speed is about 2x of the standard library packages.
+
+| old import | new import | Documentation |
+|------------------|---------------------------------------|-------------------------------------------------------------------------|
+| `compress/gzip` | `github.com/klauspost/compress/gzip` | [gzip](https://pkg.go.dev/github.com/klauspost/compress/gzip?tab=doc) |
+| `compress/zlib` | `github.com/klauspost/compress/zlib` | [zlib](https://pkg.go.dev/github.com/klauspost/compress/zlib?tab=doc) |
+| `archive/zip` | `github.com/klauspost/compress/zip` | [zip](https://pkg.go.dev/github.com/klauspost/compress/zip?tab=doc) |
+| `compress/flate` | `github.com/klauspost/compress/flate` | [flate](https://pkg.go.dev/github.com/klauspost/compress/flate?tab=doc) |
+
+You may also be interested in [pgzip](https://github.com/klauspost/pgzip), which is a drop-in replacement for gzip, which support multithreaded compression on big files and the optimized [crc32](https://github.com/klauspost/crc32) package used by these packages.
+
+The packages implement the same API as the standard library, so you can use the original godoc documentation: [gzip](http://golang.org/pkg/compress/gzip/), [zip](http://golang.org/pkg/archive/zip/), [zlib](http://golang.org/pkg/compress/zlib/), [flate](http://golang.org/pkg/compress/flate/).
+
+Currently there is only minor speedup on decompression (mostly CRC32 calculation).
+
+Memory usage is typically 1MB for a Writer. stdlib is in the same range.
+If you expect to have a lot of concurrently allocated Writers consider using
+the stateless compression described below.
+
+For compression performance, see: [this spreadsheet](https://docs.google.com/spreadsheets/d/1nuNE2nPfuINCZJRMt6wFWhKpToF95I47XjSsc-1rbPQ/edit?usp=sharing).
+
+To disable all assembly add `-tags=noasm`. This works across all packages.
+
+# Stateless compression
+
+This package offers stateless compression as a special option for gzip/deflate.
+It will do compression but without maintaining any state between Write calls.
+
+This means there will be no memory kept between Write calls, but compression and speed will be suboptimal.
+
+This is only relevant in cases where you expect to run many thousands of compressors concurrently,
+but with very little activity. This is *not* intended for regular web servers serving individual requests.
+
+Because of this, the size of actual Write calls will affect output size.
+
+In gzip, specify level `-3` / `gzip.StatelessCompression` to enable.
+
+For direct deflate use, NewStatelessWriter and StatelessDeflate are available. See [documentation](https://godoc.org/github.com/klauspost/compress/flate#NewStatelessWriter)
+
+A `bufio.Writer` can of course be used to control write sizes. For example, to use a 4KB buffer:
+
+```go
+ // replace 'ioutil.Discard' with your output.
+ gzw, err := gzip.NewWriterLevel(ioutil.Discard, gzip.StatelessCompression)
+ if err != nil {
+ return err
+ }
+ defer gzw.Close()
+
+ w := bufio.NewWriterSize(gzw, 4096)
+ defer w.Flush()
+
+ // Write to 'w'
+```
+
+This will only use up to 4KB in memory when the writer is idle.
+
+Compression is almost always worse than the fastest compression level
+and each write will allocate (a little) memory.
+
+
+# Other packages
+
+Here are other packages of good quality and pure Go (no cgo wrappers or autoconverted code):
+
+* [github.com/pierrec/lz4](https://github.com/pierrec/lz4) - strong multithreaded LZ4 compression.
+* [github.com/cosnicolaou/pbzip2](https://github.com/cosnicolaou/pbzip2) - multithreaded bzip2 decompression.
+* [github.com/dsnet/compress](https://github.com/dsnet/compress) - brotli decompression, bzip2 writer.
+* [github.com/ronanh/intcomp](https://github.com/ronanh/intcomp) - Integer compression.
+* [github.com/spenczar/fpc](https://github.com/spenczar/fpc) - Float compression.
+* [github.com/minio/zipindex](https://github.com/minio/zipindex) - External ZIP directory index.
+* [github.com/ybirader/pzip](https://github.com/ybirader/pzip) - Fast concurrent zip archiver and extractor.
+
+# license
+
+This code is licensed under the same conditions as the original Go code. See LICENSE file.
+
+
+
+
+
diff --git a/vendor/github.com/klauspost/compress/fse/README.md b/vendor/github.com/klauspost/compress/fse/README.md
index ea7324da67..27d8ed56fc 100644
--- a/vendor/github.com/klauspost/compress/fse/README.md
+++ b/vendor/github.com/klauspost/compress/fse/README.md
@@ -1,79 +1,79 @@
-# Finite State Entropy
-
-This package provides Finite State Entropy encoding and decoding.
-
-Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
-encoding provides a fast near-optimal symbol encoding/decoding
-for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
-
-This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
-This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
-but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
-
-* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
-
-## News
-
- * Feb 2018: First implementation released. Consider this beta software for now.
-
-# Usage
-
-This package provides a low level interface that allows to compress single independent blocks.
-
-Each block is separate, and there is no built in integrity checks.
-This means that the caller should keep track of block sizes and also do checksums if needed.
-
-Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
-You must provide input and will receive the output and maybe an error.
-
-These error values can be returned:
-
-| Error | Description |
-|---------------------|-----------------------------------------------------------------------------|
-| `` | Everything ok, output is returned |
-| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
-| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
-| `(error)` | An internal error occurred. |
-
-As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
-
-To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
-that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
-object can be used for both.
-
-Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
-you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
-
-Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
-You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
-your input was likely corrupted.
-
-It is important to note that a successful decoding does *not* mean your output matches your original input.
-There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
-
-For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
-
-# Performance
-
-A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
-All compression functions are currently only running on the calling goroutine so only one core will be used per block.
-
-The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
-is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
-beneficial to transpose all your input values down by 64.
-
-With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
-around 300MB/s decompression speed.
-
-The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
-
-# Plans
-
-At one point, more internals will be exposed to facilitate more "expert" usage of the components.
-
-A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
-
-# Contributing
-
-Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
+# Finite State Entropy
+
+This package provides Finite State Entropy encoding and decoding.
+
+Finite State Entropy (also referenced as [tANS](https://en.wikipedia.org/wiki/Asymmetric_numeral_systems#tANS))
+encoding provides a fast near-optimal symbol encoding/decoding
+for byte blocks as implemented in [zstandard](https://github.com/facebook/zstd).
+
+This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
+This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
+but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
+
+* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/fse)
+
+## News
+
+ * Feb 2018: First implementation released. Consider this beta software for now.
+
+# Usage
+
+This package provides a low level interface that allows to compress single independent blocks.
+
+Each block is separate, and there is no built in integrity checks.
+This means that the caller should keep track of block sizes and also do checksums if needed.
+
+Compressing a block is done via the [`Compress`](https://godoc.org/github.com/klauspost/compress/fse#Compress) function.
+You must provide input and will receive the output and maybe an error.
+
+These error values can be returned:
+
+| Error | Description |
+|---------------------|-----------------------------------------------------------------------------|
+| `` | Everything ok, output is returned |
+| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
+| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
+| `(error)` | An internal error occurred. |
+
+As can be seen above there are errors that will be returned even under normal operation so it is important to handle these.
+
+To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/fse#Scratch) object
+that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
+object can be used for both.
+
+Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
+you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
+
+Decompressing is done by calling the [`Decompress`](https://godoc.org/github.com/klauspost/compress/fse#Decompress) function.
+You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
+your input was likely corrupted.
+
+It is important to note that a successful decoding does *not* mean your output matches your original input.
+There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
+
+For more detailed usage, see examples in the [godoc documentation](https://godoc.org/github.com/klauspost/compress/fse#pkg-examples).
+
+# Performance
+
+A lot of factors are affecting speed. Block sizes and compressibility of the material are primary factors.
+All compression functions are currently only running on the calling goroutine so only one core will be used per block.
+
+The compressor is significantly faster if symbols are kept as small as possible. The highest byte value of the input
+is used to reduce some of the processing, so if all your input is above byte value 64 for instance, it may be
+beneficial to transpose all your input values down by 64.
+
+With moderate block sizes around 64k speed are typically 200MB/s per core for compression and
+around 300MB/s decompression speed.
+
+The same hardware typically does Huffman (deflate) encoding at 125MB/s and decompression at 100MB/s.
+
+# Plans
+
+At one point, more internals will be exposed to facilitate more "expert" usage of the components.
+
+A streaming interface is also likely to be implemented. Likely compatible with [FSE stream format](https://github.com/Cyan4973/FiniteStateEntropy/blob/dev/programs/fileio.c#L261).
+
+# Contributing
+
+Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
changes will likely not be accepted. If in doubt open an issue before writing the PR.
\ No newline at end of file
diff --git a/vendor/github.com/klauspost/compress/huff0/README.md b/vendor/github.com/klauspost/compress/huff0/README.md
index 8b6e5c6638..26d5101b36 100644
--- a/vendor/github.com/klauspost/compress/huff0/README.md
+++ b/vendor/github.com/klauspost/compress/huff0/README.md
@@ -1,89 +1,89 @@
-# Huff0 entropy compression
-
-This package provides Huff0 encoding and decoding as used in zstd.
-
-[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
-a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
-(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
-
-This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
-This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
-but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
-
-* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
-
-## News
-
-This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
-
-This ensures that most functionality is well tested.
-
-# Usage
-
-This package provides a low level interface that allows to compress single independent blocks.
-
-Each block is separate, and there is no built in integrity checks.
-This means that the caller should keep track of block sizes and also do checksums if needed.
-
-Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
-[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
-You must provide input and will receive the output and maybe an error.
-
-These error values can be returned:
-
-| Error | Description |
-|---------------------|-----------------------------------------------------------------------------|
-| `` | Everything ok, output is returned |
-| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
-| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
-| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
-| `(error)` | An internal error occurred. |
-
-
-As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
-
-To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
-that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
-object can be used for both.
-
-Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
-you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
-
-The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
-
-## Tables and re-use
-
-Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
-
-The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
-that controls this behaviour. See the documentation for details. This can be altered between each block.
-
-Do however note that this information is *not* stored in the output block and it is up to the users of the package to
-record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
-based on the boolean reported back from the CompressXX call.
-
-If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
-[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
-
-## Decompressing
-
-The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
-This will initialize the decoding tables.
-You can supply the complete block to `ReadTable` and it will return the data part of the block
-which can be given to the decompressor.
-
-Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
-or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
-
-For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
-
-You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
-your input was likely corrupted.
-
-It is important to note that a successful decoding does *not* mean your output matches your original input.
-There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
-
-# Contributing
-
-Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
-changes will likely not be accepted. If in doubt open an issue before writing the PR.
+# Huff0 entropy compression
+
+This package provides Huff0 encoding and decoding as used in zstd.
+
+[Huff0](https://github.com/Cyan4973/FiniteStateEntropy#new-generation-entropy-coders),
+a Huffman codec designed for modern CPU, featuring OoO (Out of Order) operations on multiple ALU
+(Arithmetic Logic Unit), achieving extremely fast compression and decompression speeds.
+
+This can be used for compressing input with a lot of similar input values to the smallest number of bytes.
+This does not perform any multi-byte [dictionary coding](https://en.wikipedia.org/wiki/Dictionary_coder) as LZ coders,
+but it can be used as a secondary step to compressors (like Snappy) that does not do entropy encoding.
+
+* [Godoc documentation](https://godoc.org/github.com/klauspost/compress/huff0)
+
+## News
+
+This is used as part of the [zstandard](https://github.com/klauspost/compress/tree/master/zstd#zstd) compression and decompression package.
+
+This ensures that most functionality is well tested.
+
+# Usage
+
+This package provides a low level interface that allows to compress single independent blocks.
+
+Each block is separate, and there is no built in integrity checks.
+This means that the caller should keep track of block sizes and also do checksums if needed.
+
+Compressing a block is done via the [`Compress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress1X) and
+[`Compress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Compress4X) functions.
+You must provide input and will receive the output and maybe an error.
+
+These error values can be returned:
+
+| Error | Description |
+|---------------------|-----------------------------------------------------------------------------|
+| `` | Everything ok, output is returned |
+| `ErrIncompressible` | Returned when input is judged to be too hard to compress |
+| `ErrUseRLE` | Returned from the compressor when the input is a single byte value repeated |
+| `ErrTooBig` | Returned if the input block exceeds the maximum allowed size (128 Kib) |
+| `(error)` | An internal error occurred. |
+
+
+As can be seen above some of there are errors that will be returned even under normal operation so it is important to handle these.
+
+To reduce allocations you can provide a [`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object
+that can be re-used for successive calls. Both compression and decompression accepts a `Scratch` object, and the same
+object can be used for both.
+
+Be aware, that when re-using a `Scratch` object that the *output* buffer is also re-used, so if you are still using this
+you must set the `Out` field in the scratch to nil. The same buffer is used for compression and decompression output.
+
+The `Scratch` object will retain state that allows to re-use previous tables for encoding and decoding.
+
+## Tables and re-use
+
+Huff0 allows for reusing tables from the previous block to save space if that is expected to give better/faster results.
+
+The Scratch object allows you to set a [`ReusePolicy`](https://godoc.org/github.com/klauspost/compress/huff0#ReusePolicy)
+that controls this behaviour. See the documentation for details. This can be altered between each block.
+
+Do however note that this information is *not* stored in the output block and it is up to the users of the package to
+record whether [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable) should be called,
+based on the boolean reported back from the CompressXX call.
+
+If you want to store the table separate from the data, you can access them as `OutData` and `OutTable` on the
+[`Scratch`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch) object.
+
+## Decompressing
+
+The first part of decoding is to initialize the decoding table through [`ReadTable`](https://godoc.org/github.com/klauspost/compress/huff0#ReadTable).
+This will initialize the decoding tables.
+You can supply the complete block to `ReadTable` and it will return the data part of the block
+which can be given to the decompressor.
+
+Decompressing is done by calling the [`Decompress1X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress1X)
+or [`Decompress4X`](https://godoc.org/github.com/klauspost/compress/huff0#Scratch.Decompress4X) function.
+
+For concurrently decompressing content with a fixed table a stateless [`Decoder`](https://godoc.org/github.com/klauspost/compress/huff0#Decoder) can be requested which will remain correct as long as the scratch is unchanged. The capacity of the provided slice indicates the expected output size.
+
+You must provide the output from the compression stage, at exactly the size you got back. If you receive an error back
+your input was likely corrupted.
+
+It is important to note that a successful decoding does *not* mean your output matches your original input.
+There are no integrity checks, so relying on errors from the decompressor does not assure your data is valid.
+
+# Contributing
+
+Contributions are always welcome. Be aware that adding public functions will require good justification and breaking
+changes will likely not be accepted. If in doubt open an issue before writing the PR.
diff --git a/vendor/github.com/klauspost/compress/s2/decode_amd64.s b/vendor/github.com/klauspost/compress/s2/decode_amd64.s
index 9b105e03c5..1216df78f5 100644
--- a/vendor/github.com/klauspost/compress/s2/decode_amd64.s
+++ b/vendor/github.com/klauspost/compress/s2/decode_amd64.s
@@ -51,7 +51,7 @@
//
// The d variable is implicitly R_DST - R_DBASE, and len(dst)-d is R_DEND - R_DST.
// The s variable is implicitly R_SRC - R_SBASE, and len(src)-s is R_SEND - R_SRC.
-TEXT ·s2Decode(SB), NOSPLIT, $48-56
+TEXT ·s2Decode(SB), NOSPLIT, $56-56
// Initialize R_SRC, R_DST and R_DBASE-R_SEND.
MOVQ dst_base+0(FP), R_DBASE
MOVQ dst_len+8(FP), R_DLEN
diff --git a/vendor/github.com/klauspost/compress/s2/encode_best.go b/vendor/github.com/klauspost/compress/s2/encode_best.go
index c857c5c283..49f2166ec1 100644
--- a/vendor/github.com/klauspost/compress/s2/encode_best.go
+++ b/vendor/github.com/klauspost/compress/s2/encode_best.go
@@ -23,12 +23,12 @@ func encodeBlockBest(dst, src []byte, dict *Dict) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
- lTableBits = 19
- maxLTableSize = 1 << lTableBits
+ lTableBits = bestLongTableBits
+ maxLTableSize = bestLongTableSize
// Short hash matches.
- sTableBits = 16
- maxSTableSize = 1 << sTableBits
+ sTableBits = bestShortTableBits
+ maxSTableSize = bestShortTableSize
inputMargin = 8 + 2
@@ -44,8 +44,10 @@ func encodeBlockBest(dst, src []byte, dict *Dict) (d int) {
}
sLimitDict := min(len(src)-inputMargin, MaxDictSrcOffset-inputMargin)
- var lTable [maxLTableSize]uint64
- var sTable [maxSTableSize]uint64
+ tbl := getBestTables()
+ lTable := &tbl.lTable
+ sTable := &tbl.sTable
+ defer bestTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - 5
@@ -456,12 +458,12 @@ func encodeBlockBestSnappy(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
- lTableBits = 19
- maxLTableSize = 1 << lTableBits
+ lTableBits = bestLongTableBits
+ maxLTableSize = bestLongTableSize
// Short hash matches.
- sTableBits = 16
- maxSTableSize = 1 << sTableBits
+ sTableBits = bestShortTableBits
+ maxSTableSize = bestShortTableSize
inputMargin = 8 + 2
)
@@ -474,8 +476,10 @@ func encodeBlockBestSnappy(dst, src []byte) (d int) {
return 0
}
- var lTable [maxLTableSize]uint64
- var sTable [maxSTableSize]uint64
+ tbl := getBestTables()
+ lTable := &tbl.lTable
+ sTable := &tbl.sTable
+ defer bestTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - 5
diff --git a/vendor/github.com/klauspost/compress/s2/encode_better.go b/vendor/github.com/klauspost/compress/s2/encode_better.go
index 1e30fb7317..adbc57c4d2 100644
--- a/vendor/github.com/klauspost/compress/s2/encode_better.go
+++ b/vendor/github.com/klauspost/compress/s2/encode_better.go
@@ -59,16 +59,18 @@ func encodeBlockBetterGo(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
- lTableBits = 17
- maxLTableSize = 1 << lTableBits
+ lTableBits = betterLongTableBits
+ maxLTableSize = betterLongTableSize
// Short hash matches.
- sTableBits = 14
- maxSTableSize = 1 << sTableBits
+ sTableBits = betterShortTableBits
+ maxSTableSize = betterShortTableSize
)
- var lTable [maxLTableSize]uint32
- var sTable [maxSTableSize]uint32
+ tbl := getBetterTables()
+ lTable := &tbl.lTable
+ sTable := &tbl.sTable
+ defer betterTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
@@ -317,16 +319,18 @@ func encodeBlockBetterSnappyGo(dst, src []byte) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
- lTableBits = 16
- maxLTableSize = 1 << lTableBits
+ lTableBits = betterSnappyLongTableBits
+ maxLTableSize = betterSnappyLongTableSize
// Short hash matches.
- sTableBits = 14
- maxSTableSize = 1 << sTableBits
+ sTableBits = betterShortTableBits
+ maxSTableSize = betterShortTableSize
)
- var lTable [maxLTableSize]uint32
- var sTable [maxSTableSize]uint32
+ tbl := getBetterSnappyTables()
+ lTable := &tbl.lTable
+ sTable := &tbl.sTable
+ defer betterSnappyTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
@@ -902,12 +906,12 @@ func encodeBlockBetterDict(dst, src []byte, dict *Dict) (d int) {
// Initialize the hash tables.
const (
// Long hash matches.
- lTableBits = 17
- maxLTableSize = 1 << lTableBits
+ lTableBits = betterLongTableBits
+ maxLTableSize = betterLongTableSize
// Short hash matches.
- sTableBits = 14
- maxSTableSize = 1 << sTableBits
+ sTableBits = betterShortTableBits
+ maxSTableSize = betterShortTableSize
maxAhead = 8 // maximum bytes ahead without checking sLimit
@@ -921,8 +925,10 @@ func encodeBlockBetterDict(dst, src []byte, dict *Dict) (d int) {
dict.initBetter()
- var lTable [maxLTableSize]uint32
- var sTable [maxSTableSize]uint32
+ tbl := getBetterTables()
+ lTable := &tbl.lTable
+ sTable := &tbl.sTable
+ defer betterTablePool.Put(tbl)
// Bail if we can't compress to at least this.
dstLimit := len(src) - len(src)>>5 - 6
diff --git a/vendor/github.com/klauspost/compress/s2/hashtable_pool.go b/vendor/github.com/klauspost/compress/s2/hashtable_pool.go
new file mode 100644
index 0000000000..bc7cabd5c5
--- /dev/null
+++ b/vendor/github.com/klauspost/compress/s2/hashtable_pool.go
@@ -0,0 +1,65 @@
+package s2
+
+import "sync"
+
+// Table size constants
+const (
+ betterLongTableBits = 17
+ betterLongTableSize = 1 << betterLongTableBits // 131072
+
+ betterShortTableBits = 14
+ betterShortTableSize = 1 << betterShortTableBits // 16384
+
+ betterSnappyLongTableBits = 16
+ betterSnappyLongTableSize = 1 << betterSnappyLongTableBits // 65536
+
+ bestLongTableBits = 19
+ bestLongTableSize = 1 << bestLongTableBits // 524288
+
+ bestShortTableBits = 16
+ bestShortTableSize = 1 << bestShortTableBits // 65536
+)
+
+type betterTables struct {
+ lTable [betterLongTableSize]uint32
+ sTable [betterShortTableSize]uint32
+}
+
+var betterTablePool = sync.Pool{New: func() interface{} { return &betterTables{} }}
+
+// betterSnappyTables holds better-snappy compression hash tables.
+type betterSnappyTables struct {
+ lTable [betterSnappyLongTableSize]uint32
+ sTable [betterShortTableSize]uint32
+}
+
+var betterSnappyTablePool = sync.Pool{New: func() interface{} { return &betterSnappyTables{} }}
+
+// bestTables holds best compression hash tables.
+type bestTables struct {
+ lTable [bestLongTableSize]uint64
+ sTable [bestShortTableSize]uint64
+}
+
+var bestTablePool = sync.Pool{New: func() interface{} { return &bestTables{} }}
+
+// getBetterTables gets a zeroed betterTables from the pool.
+func getBetterTables() *betterTables {
+ t := betterTablePool.Get().(*betterTables)
+ *t = betterTables{}
+ return t
+}
+
+// getBetterSnappyTables gets a zeroed betterSnappyTables from the pool.
+func getBetterSnappyTables() *betterSnappyTables {
+ t := betterSnappyTablePool.Get().(*betterSnappyTables)
+ *t = betterSnappyTables{}
+ return t
+}
+
+// getBestTables gets a zeroed bestTables from the pool.
+func getBestTables() *bestTables {
+ t := bestTablePool.Get().(*bestTables)
+ *t = bestTables{}
+ return t
+}
diff --git a/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml b/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml
new file mode 100644
index 0000000000..b231abdf2e
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/dsig/.goreleaser.yml
@@ -0,0 +1,7 @@
+version: 2
+
+builds:
+ - skip: true
+
+changelog:
+ use: github-native
diff --git a/vendor/github.com/lestrrat-go/dsig/Changes b/vendor/github.com/lestrrat-go/dsig/Changes
index bccce97613..5e7a522cd1 100644
--- a/vendor/github.com/lestrrat-go/dsig/Changes
+++ b/vendor/github.com/lestrrat-go/dsig/Changes
@@ -1,5 +1,28 @@
Changes
=======
+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.
+
+v1.2.0 6 Apr 2026
+ * Add `VerifyDigest()` for verifying signatures against pre-computed digests.
+ Supported for HMAC, RSA, and ECDSA families. EdDSA and Custom return an error.
+
+ * Add low-level digest verification functions: `VerifyHMACDigest()`,
+ `VerifyRSADigest()`, `VerifyECDSADigest()`.
+
+v1.1.0 2 Apr 2026
+ * Add `Custom` algorithm family for registering user-defined sign/verify
+ implementations. For the `Custom` family, `AlgorithmInfo.Meta` must implement
+ the `Signer` and/or `Verifier` interfaces. The implementation struct can
+ carry any additional metadata it needs (hash functions, curves, etc.).
+
+ * Add `UnregisterAlgorithm()` for removing previously registered custom
+ algorithms. Built-in algorithms are protected and cannot be unregistered.
+
+ * `RegisterAlgorithm()` now rejects re-registration of an already-registered
+ algorithm name. Use `UnregisterAlgorithm()` first if you need to replace it.
+
v1.0.0 - 18 Aug 2025
-* Initial release
\ No newline at end of file
+ * Initial release
\ No newline at end of file
diff --git a/vendor/github.com/lestrrat-go/dsig/README.md b/vendor/github.com/lestrrat-go/dsig/README.md
index 37c194579e..b52b998f8f 100644
--- a/vendor/github.com/lestrrat-go/dsig/README.md
+++ b/vendor/github.com/lestrrat-go/dsig/README.md
@@ -11,7 +11,7 @@ While there are many standards for generating and verifying digital signatures,
* EdDSA signatures (Ed25519, Ed448)
* HMAC signatures (SHA-256, SHA-384, SHA-512)
* Support for crypto.Signer interface
-* Allows for dynamic additions of algorithms in limited cases.
+* Custom algorithm registration via `Signer`/`Verifier` interfaces
# SYNOPSIS
diff --git a/vendor/github.com/lestrrat-go/dsig/dsig.go b/vendor/github.com/lestrrat-go/dsig/dsig.go
index de6cbdec45..a6b54418a7 100644
--- a/vendor/github.com/lestrrat-go/dsig/dsig.go
+++ b/vendor/github.com/lestrrat-go/dsig/dsig.go
@@ -14,6 +14,7 @@ import (
"crypto/sha512"
"fmt"
"hash"
+ "io"
"sync"
)
@@ -26,6 +27,7 @@ const (
RSA
ECDSA
EdDSAFamily
+ Custom
maxFamily
)
@@ -40,6 +42,8 @@ func (f Family) String() string {
return "ECDSA"
case EdDSAFamily:
return "EdDSA"
+ case Custom:
+ return "Custom"
default:
return "InvalidFamily"
}
@@ -73,18 +77,43 @@ type EdDSAFamilyMeta struct {
// Reserved for future use
}
+// Signer is an interface for custom signing implementations.
+// For the Custom algorithm family, info.Meta must implement this interface
+// to support signing. The implementation struct can carry any additional
+// metadata it needs (hash functions, curves, etc.).
+type Signer interface {
+ Sign(key any, payload []byte, 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
+// metadata it needs (hash functions, curves, etc.).
+type Verifier interface {
+ Verify(key any, payload, signature []byte) error
+}
+
var algorithms = make(map[string]AlgorithmInfo)
+var builtinAlgorithms = make(map[string]struct{})
var muAlgorithms sync.RWMutex
// RegisterAlgorithm registers a new digital signature algorithm with the specified family and metadata.
//
-// info.Meta should contain extra metadata for some algorithms. Currently HMAC, RSA,
-// and ECDSA family of algorithms need their respective metadata (HMACFamilyMeta,
-// RSAFamilyMeta, and ECDSAFamilyMeta). Metadata for other families are ignored.
+// 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.
+//
+// Re-registration of an already-registered algorithm name is rejected. Use
+// UnregisterAlgorithm to remove it first if you need to replace it.
func RegisterAlgorithm(name string, info AlgorithmInfo) error {
muAlgorithms.Lock()
defer muAlgorithms.Unlock()
+ if _, exists := algorithms[name]; exists {
+ return fmt.Errorf("algorithm %s is already registered", name)
+ }
+
// Validate the metadata matches the family
switch info.Family {
case HMAC:
@@ -101,6 +130,12 @@ func RegisterAlgorithm(name string, info AlgorithmInfo) error {
}
case EdDSAFamily:
// EdDSA metadata is optional for now
+ case Custom:
+ _, isSigner := info.Meta.(Signer)
+ _, isVerifier := info.Meta.(Verifier)
+ if !isSigner && !isVerifier {
+ return fmt.Errorf("custom algorithm %s: Meta must implement Signer and/or Verifier", name)
+ }
default:
return fmt.Errorf("unsupported algorithm family %s for algorithm %s", info.Family, name)
}
@@ -109,6 +144,21 @@ func RegisterAlgorithm(name string, info AlgorithmInfo) error {
return nil
}
+// UnregisterAlgorithm removes a previously registered algorithm by name.
+// Built-in algorithms cannot be unregistered.
+// It is a no-op if the algorithm is not registered.
+func UnregisterAlgorithm(name string) error {
+ muAlgorithms.Lock()
+ defer muAlgorithms.Unlock()
+
+ if _, ok := builtinAlgorithms[name]; ok {
+ return fmt.Errorf("algorithm %s is a built-in algorithm and cannot be unregistered", name)
+ }
+
+ delete(algorithms, name)
+ return nil
+}
+
// GetAlgorithmInfo retrieves the algorithm information for a given algorithm name.
// Returns the info and true if found, zero value and false if not found.
func GetAlgorithmInfo(name string) (AlgorithmInfo, bool) {
@@ -219,6 +269,7 @@ func init() {
if err := RegisterAlgorithm(name, info); err != nil {
panic(fmt.Sprintf("failed to register algorithm %s: %v", name, err))
}
+ builtinAlgorithms[name] = struct{}{}
}
}
diff --git a/vendor/github.com/lestrrat-go/dsig/ecdsa.go b/vendor/github.com/lestrrat-go/dsig/ecdsa.go
index a04a266919..4041d9c53d 100644
--- a/vendor/github.com/lestrrat-go/dsig/ecdsa.go
+++ b/vendor/github.com/lestrrat-go/dsig/ecdsa.go
@@ -176,6 +176,21 @@ func VerifyECDSA(key *ecdsa.PublicKey, payload, signature []byte, h crypto.Hash)
return ecdsaVerify(key, payload, h, &r, &s)
}
+// VerifyECDSADigest verifies an ECDSA signature given a pre-computed digest.
+// The caller is responsible for hashing the signing input with the correct
+// hash function for the algorithm (e.g. SHA-256 for ES256). This function
+// does not validate the digest length.
+func VerifyECDSADigest(key *ecdsa.PublicKey, digest, signature []byte) error {
+ var r, s big.Int
+ if err := UnpackECDSASignature(signature, key, &r, &s); err != nil {
+ return fmt.Errorf("dsig.VerifyECDSADigest: %w", err)
+ }
+ if !ecdsa.Verify(key, digest, &r, &s) {
+ return NewVerificationError("invalid ECDSA signature")
+ }
+ return nil
+}
+
// VerifyECDSACryptoSigner verifies an ECDSA signature for crypto.Signer implementations.
// This function is useful for verifying signatures created by hardware security modules
// or other implementations of the crypto.Signer interface.
diff --git a/vendor/github.com/lestrrat-go/dsig/hmac.go b/vendor/github.com/lestrrat-go/dsig/hmac.go
index 8b2612279d..ad3d86c62a 100644
--- a/vendor/github.com/lestrrat-go/dsig/hmac.go
+++ b/vendor/github.com/lestrrat-go/dsig/hmac.go
@@ -30,6 +30,14 @@ func SignHMAC(key, payload []byte, hfunc func() hash.Hash) ([]byte, error) {
return h.Sum(nil), nil
}
+// VerifyHMACDigest verifies an HMAC signature given a pre-computed MAC.
+func VerifyHMACDigest(computedMAC, signature []byte) error {
+ if !hmac.Equal(computedMAC, signature) {
+ return NewVerificationError("invalid HMAC signature")
+ }
+ return nil
+}
+
// VerifyHMAC verifies an HMAC signature for the given payload.
// This function verifies the signature using the specified key and hash function.
// The payload parameter should be the pre-computed signing input (typically header.payload).
diff --git a/vendor/github.com/lestrrat-go/dsig/rsa.go b/vendor/github.com/lestrrat-go/dsig/rsa.go
index a339fe5b78..b052066944 100644
--- a/vendor/github.com/lestrrat-go/dsig/rsa.go
+++ b/vendor/github.com/lestrrat-go/dsig/rsa.go
@@ -61,3 +61,17 @@ func VerifyRSA(key *rsa.PublicKey, payload, signature []byte, h crypto.Hash, pss
}
return rsa.VerifyPKCS1v15(key, h, digest, signature)
}
+
+// VerifyRSADigest verifies an RSA signature given a pre-computed digest.
+// If pss is true, RSA-PSS verification is used; otherwise, PKCS#1 v1.5 is used.
+func VerifyRSADigest(key *rsa.PublicKey, digest, signature []byte, h crypto.Hash, pss bool) error {
+ // isValidRSAKey only rejects non-RSA private key types, so this check is
+ // a no-op for *rsa.PublicKey. Kept for consistency with VerifyRSA.
+ if !isValidRSAKey(key) {
+ return fmt.Errorf(`invalid key type %T for RSA algorithm`, key)
+ }
+ if pss {
+ return rsa.VerifyPSS(key, h, digest, signature, &rsa.PSSOptions{Hash: h, SaltLength: rsa.PSSSaltLengthEqualsHash})
+ }
+ return rsa.VerifyPKCS1v15(key, h, digest, signature)
+}
diff --git a/vendor/github.com/lestrrat-go/dsig/sign.go b/vendor/github.com/lestrrat-go/dsig/sign.go
index e2a6bde290..eb57f5eec6 100644
--- a/vendor/github.com/lestrrat-go/dsig/sign.go
+++ b/vendor/github.com/lestrrat-go/dsig/sign.go
@@ -2,6 +2,8 @@ package dsig
import (
"crypto"
+ "crypto/ecdsa"
+ "crypto/rand"
"crypto/rsa"
"fmt"
"io"
@@ -9,9 +11,6 @@ import (
// Sign generates a digital signature using the specified key and algorithm.
//
-// This function loads the signer registered in the dsig package _ONLY_.
-// It does not support custom signers that the user might have registered.
-//
// rr is an io.Reader that provides randomness for signing. If rr is nil, it defaults to rand.Reader.
// Not all algorithms require this parameter, but it is included for consistency.
// 99% of the time, you can pass nil for rr, and it will work fine.
@@ -30,6 +29,8 @@ 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)
default:
return nil, fmt.Errorf(`dsig.Sign: unsupported signature family %q`, info.Family)
}
@@ -98,3 +99,112 @@ 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) {
+ signer, ok := info.Meta.(Signer)
+ if !ok {
+ return nil, fmt.Errorf(`dsig.Sign: algorithm has no signer registered`)
+ }
+ return signer.Sign(key, payload, rr)
+}
+
+// SignDigest generates a digital signature from a pre-computed digest.
+//
+// For RSA/ECDSA, digest is the hash of the signing input and key is the
+// private key used for signing.
+//
+// For HMAC, the digest must be the pre-computed MAC (i.e. the output of
+// hmac.New(hashFunc, key) after writing the signing input). The digest IS
+// the signature, so it is returned as-is.
+//
+// EdDSA and Custom families are not supported and return an error.
+//
+// rr is an io.Reader that provides randomness for signing. If rr is nil,
+// it defaults to rand.Reader.
+func SignDigest(key any, alg string, digest []byte, rr io.Reader) ([]byte, error) {
+ info, ok := GetAlgorithmInfo(alg)
+ if !ok {
+ return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature algorithm %q`, alg)
+ }
+
+ switch info.Family {
+ case HMAC:
+ // The caller already computed the HMAC (which incorporates the key)
+ // and passed it as digest. The digest IS the signature.
+ return digest, nil
+ case RSA:
+ return dispatchRSASignDigest(key, info, digest, rr)
+ case ECDSA:
+ return dispatchECDSASignDigest(key, info, digest, rr)
+ case EdDSAFamily:
+ 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`)
+ default:
+ return nil, fmt.Errorf(`dsig.SignDigest: unsupported signature family %q`, info.Family)
+ }
+}
+
+func dispatchRSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) {
+ meta, ok := info.Meta.(RSAFamilyMeta)
+ if !ok {
+ return nil, fmt.Errorf(`dsig.SignDigest: invalid RSA metadata`)
+ }
+
+ if rr == nil {
+ rr = rand.Reader
+ }
+
+ cs, isCryptoSigner, err := rsaGetSignerCryptoSignerKey(key)
+ if err != nil {
+ return nil, fmt.Errorf(`dsig.SignDigest: %w`, err)
+ }
+ if isCryptoSigner {
+ var opts crypto.SignerOpts = meta.Hash
+ if meta.PSS {
+ rsaopts := rsaPSSOptions(meta.Hash)
+ opts = &rsaopts
+ }
+ return cs.Sign(rr, digest, opts)
+ }
+
+ privkey, ok := key.(*rsa.PrivateKey)
+ if !ok {
+ return nil, fmt.Errorf(`dsig.SignDigest: invalid key type %T. *rsa.PrivateKey is required`, key)
+ }
+ if meta.PSS {
+ rsaopts := rsaPSSOptions(meta.Hash)
+ return rsa.SignPSS(rr, privkey, meta.Hash, digest, &rsaopts)
+ }
+ return rsa.SignPKCS1v15(rr, privkey, meta.Hash, digest)
+}
+
+func dispatchECDSASignDigest(key any, info AlgorithmInfo, digest []byte, rr io.Reader) ([]byte, error) {
+ meta, ok := info.Meta.(ECDSAFamilyMeta)
+ if !ok {
+ return nil, fmt.Errorf(`dsig.SignDigest: invalid ECDSA metadata`)
+ }
+
+ if rr == nil {
+ rr = rand.Reader
+ }
+
+ privkey, cs, isCryptoSigner, err := ecdsaGetSignerKey(key)
+ if err != nil {
+ return nil, fmt.Errorf(`dsig.SignDigest: %w`, err)
+ }
+ if isCryptoSigner {
+ signed, err := cs.Sign(rr, digest, meta.Hash)
+ if err != nil {
+ return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using crypto.Signer: %w`, err)
+ }
+ return signECDSACryptoSigner(cs, signed)
+ }
+
+ r, s, err := ecdsa.Sign(rr, privkey, digest)
+ if err != nil {
+ return nil, fmt.Errorf(`dsig.SignDigest: failed to sign digest using ecdsa: %w`, err)
+ }
+ return PackECDSASignature(r, s, privkey.Curve.Params().BitSize)
+}
+
diff --git a/vendor/github.com/lestrrat-go/dsig/verify.go b/vendor/github.com/lestrrat-go/dsig/verify.go
index 86085b0a37..05ffe8e94f 100644
--- a/vendor/github.com/lestrrat-go/dsig/verify.go
+++ b/vendor/github.com/lestrrat-go/dsig/verify.go
@@ -9,9 +9,6 @@ import (
)
// Verify verifies a digital signature using the specified key and algorithm.
-//
-// This function loads the verifier registered in the dsig package _ONLY_.
-// It does not support custom verifiers that the user might have registered.
func Verify(key any, alg string, payload, signature []byte) error {
info, ok := GetAlgorithmInfo(alg)
if !ok {
@@ -27,11 +24,100 @@ 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)
default:
return fmt.Errorf(`dsig.Verify: unsupported signature family %q`, info.Family)
}
}
+// VerifyDigest verifies a signature given a pre-computed digest.
+//
+// For RSA/ECDSA, digest is the hash of the signing input and key is the
+// public key used for verification.
+//
+// For HMAC, digest must be the pre-computed MAC (i.e. the output of
+// hmac.New(hashFunc, key) after writing the signing input). The key
+// parameter is not used because it is already incorporated into the MAC.
+//
+// EdDSA and Custom families are not supported and return an error.
+func VerifyDigest(key any, alg string, digest, signature []byte) error {
+ info, ok := GetAlgorithmInfo(alg)
+ if !ok {
+ return fmt.Errorf(`dsig.VerifyDigest: unsupported signature algorithm %q`, alg)
+ }
+
+ switch info.Family {
+ case HMAC:
+ // key is not used here: the caller has already computed the HMAC
+ // (which incorporates the key) and passed it as digest.
+ return VerifyHMACDigest(digest, signature)
+ case RSA:
+ return dispatchRSAVerifyDigest(key, info, digest, signature)
+ case ECDSA:
+ return dispatchECDSAVerifyDigest(key, info, digest, signature)
+ case EdDSAFamily:
+ return fmt.Errorf(`dsig.VerifyDigest: EdDSA does not support digest-based verification`)
+ case Custom:
+ // 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`)
+ default:
+ return fmt.Errorf(`dsig.VerifyDigest: unsupported signature family %q`, info.Family)
+ }
+}
+
+func dispatchRSAVerifyDigest(key any, info AlgorithmInfo, digest, signature []byte) error {
+ meta, ok := info.Meta.(RSAFamilyMeta)
+ if !ok {
+ return fmt.Errorf(`dsig.VerifyDigest: invalid RSA metadata`)
+ }
+
+ var pubkey *rsa.PublicKey
+
+ if cs, ok := key.(crypto.Signer); ok {
+ cpub := cs.Public()
+ switch cpub := cpub.(type) {
+ case rsa.PublicKey:
+ pubkey = &cpub
+ case *rsa.PublicKey:
+ pubkey = cpub
+ default:
+ return fmt.Errorf(`dsig.VerifyDigest: failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key)
+ }
+ } else {
+ var ok bool
+ pubkey, ok = key.(*rsa.PublicKey)
+ if !ok {
+ return fmt.Errorf(`dsig.VerifyDigest: failed to retrieve *rsa.PublicKey out of %T`, key)
+ }
+ }
+
+ return VerifyRSADigest(pubkey, digest, signature, meta.Hash, meta.PSS)
+}
+
+// Note: the crypto.Signer → *ecdsa.PublicKey extraction below duplicates
+// logic in VerifyECDSACryptoSigner. We can't call that function because it
+// hashes the payload internally. If the extraction logic changes, update both.
+func dispatchECDSAVerifyDigest(key any, info AlgorithmInfo, digest, signature []byte) error {
+ pubkey, cs, isCryptoSigner, err := ecdsaGetVerifierKey(key)
+ if err != nil {
+ return fmt.Errorf(`dsig.VerifyDigest: %w`, err)
+ }
+ if isCryptoSigner {
+ cpub := cs.Public()
+ switch cpub := cpub.(type) {
+ case ecdsa.PublicKey:
+ pubkey = &cpub
+ case *ecdsa.PublicKey:
+ pubkey = cpub
+ default:
+ return fmt.Errorf(`dsig.VerifyDigest: expected *ecdsa.PublicKey from crypto.Signer, got %T`, cpub)
+ }
+ }
+ return VerifyECDSADigest(pubkey, digest, signature)
+}
+
func dispatchHMACVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
meta, ok := info.Meta.(HMACFamilyMeta)
if !ok {
@@ -110,6 +196,14 @@ func dispatchEdDSAVerify(key any, _ AlgorithmInfo, payload, signature []byte) er
return VerifyEdDSA(pubkey, payload, signature)
}
+func dispatchCustomVerify(key any, info AlgorithmInfo, payload, signature []byte) error {
+ verifier, ok := info.Meta.(Verifier)
+ if !ok {
+ return fmt.Errorf(`dsig.Verify: algorithm has no verifier registered`)
+ }
+ return verifier.Verify(key, payload, signature)
+}
+
func ecdsaGetVerifierKey(key any) (*ecdsa.PublicKey, crypto.Signer, bool, error) {
cs, isCryptoSigner := key.(crypto.Signer)
if isCryptoSigner {
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/Changes b/vendor/github.com/lestrrat-go/httprc/v3/Changes
index 6a5eb8064a..001c8c5444 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/Changes
+++ b/vendor/github.com/lestrrat-go/httprc/v3/Changes
@@ -1,6 +1,22 @@
Changes
=======
+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
+ and worker goroutines (#113, #116)
+ * Fix proxysink self-deadlock caused by missing mutex unlock on context
+ cancellation path
+
+v3.0.4 08 Feb 2026
+ * Fix worker goroutine dying on sync refresh failure, which could cause
+ deadlocks after repeated failures (lestrrat-go/jwx#1551)
+ * Move ErrNotReady example functions out of client_example_test.go into
+ separate files to avoid triggering autodoc workflow
+
+v3.0.3 23 Dec 2025
+ * Add ErrNotReady error state to avoid waiting for unstable URLs
+
v3.0.2 05 Dev 2025
* Code changes mainly due to upgraded linter.
* github.com/lestrrat-go/option upgraded to v2
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/backend.go b/vendor/github.com/lestrrat-go/httprc/v3/backend.go
index 31f9fc07d3..713de23de2 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/backend.go
+++ b/vendor/github.com/lestrrat-go/httprc/v3/backend.go
@@ -118,14 +118,6 @@ func (c *ctrlBackend) handleRequest(ctx context.Context, req any) {
}
}
-func sendWorker(ctx context.Context, ch chan Resource, r Resource) {
- r.SetBusy(true)
- select {
- case <-ctx.Done():
- case ch <- r:
- }
-}
-
func sendWorkerSynchronous(ctx context.Context, ch chan synchronousRequest, r synchronousRequest) {
r.resource.SetBusy(true)
select {
@@ -159,26 +151,67 @@ func (c *ctrlBackend) loop(ctx context.Context, readywg, donewg *sync.WaitGroup)
readywg.Done()
defer c.traceSink.Put(ctx, "httprc controller: stopping main controller loop")
defer donewg.Done()
+
+ var pending []Resource
for {
- c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for request or tick (tick interval=%s)", c.tickInterval))
- select {
- case req := <-c.incoming:
- c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T", req))
- c.handleRequest(ctx, req)
- case t := <-c.check.C:
- c.periodicCheck(ctx, t)
- case <-ctx.Done():
- return
+ if len(pending) > 0 {
+ // Dispatch pending items while remaining responsive to incoming
+ // requests. This prevents a deadlock where periodicCheck blocks
+ // on c.outgoing while a worker blocks on c.incoming (issue #113).
+
+ // Skip resources that were removed (or replaced) after periodicCheck
+ // queued them. Without this check, a stale resource could be sent to
+ // a worker, causing an unnecessary fetch and a subsequent
+ // adjustIntervalRequest for a resource that is no longer registered.
+ r := pending[0]
+ // Compare interface values directly. This is safe because all
+ // Resource implementations are pointer types (*ResourceBase[T]),
+ // so the comparison is a pointer identity check.
+ if cur, ok := c.items[r.URL()]; !ok || cur != r {
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: skipping pending resource %q (no longer registered or replaced)", r.URL()))
+ r.SetBusy(false)
+ pending = pending[1:]
+ continue
+ }
+
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatching pending resource %q to worker pool (%d remaining)", pending[0].URL(), len(pending)))
+ select {
+ case req := <-c.incoming:
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T (while dispatching)", req))
+ c.handleRequest(ctx, req)
+ case c.outgoing <- pending[0]:
+ pending = pending[1:]
+ case t := <-c.check.C:
+ pending = append(pending, c.periodicCheck(ctx, t)...)
+ case <-ctx.Done():
+ return
+ }
+ } else {
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for request or tick (tick interval=%s)", c.tickInterval))
+ select {
+ case req := <-c.incoming:
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: got request %T", req))
+ c.handleRequest(ctx, req)
+ case t := <-c.check.C:
+ pending = c.periodicCheck(ctx, t)
+ case <-ctx.Done():
+ return
+ }
}
}
}
-func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) {
+// periodicCheck examines all registered resources and returns those that are
+// due for refresh. Items are marked busy here so they won't be selected again
+// on the next tick. The caller (loop) is responsible for dispatching them to
+// the worker pool, interleaved with incoming request handling, to avoid the
+// deadlock described in https://github.com/lestrrat-go/httprc/issues/113.
+func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) []Resource {
c.traceSink.Put(ctx, "httprc controller: START periodic check")
defer c.traceSink.Put(ctx, "httprc controller: END periodic check")
var minNext time.Time
- var dispatched int
minInterval := -1 * time.Second
+ var toDispatch []Resource
for _, item := range c.items {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: checking resource %q", item.URL()))
@@ -196,14 +229,13 @@ func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is busy or not ready yet, skipping", item.URL()))
continue
}
- c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is ready, dispatching to worker pool", item.URL()))
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: resource %q is ready, queuing for dispatch", item.URL()))
- dispatched++
- c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatching resource %q to worker pool", item.URL()))
- sendWorker(ctx, c.outgoing, item)
+ item.SetBusy(true)
+ toDispatch = append(toDispatch, item)
}
- c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: dispatched %d resources", dispatched))
+ c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: queued %d resources for dispatch", len(toDispatch)))
// Next check is always at the earliest next check + 1 second.
// The extra second makes sure that we are _past_ the actual next check time
@@ -223,6 +255,7 @@ func (c *ctrlBackend) periodicCheck(ctx context.Context, t time.Time) {
}
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: next check in %s", c.tickInterval))
+ return toDispatch
}
func (c *ctrlBackend) SetTickInterval(d time.Duration) {
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/controller.go b/vendor/github.com/lestrrat-go/httprc/v3/controller.go
index 1ad9d7b6c6..ffca6a590d 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/controller.go
+++ b/vendor/github.com/lestrrat-go/httprc/v3/controller.go
@@ -142,14 +142,23 @@ func (c *controller) Add(ctx context.Context, r Resource, options ...AddOption)
resource: r,
}
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: sending add request for %q to backend", r.URL()))
+ // Send to backend and wait for registration confirmation.
+ // If this succeeds, the resource is in the backend.
if _, err := sendBackend[addRequest, struct{}](ctx, c.incoming, req, reply); err != nil {
return err
}
+ // IMPORTANT: At this point, the resource has been successfully registered
+ // in the backend (stored in c.items map). The backend worker will fetch
+ // this resource periodically.
if waitReady {
c.traceSink.Put(ctx, fmt.Sprintf("httprc controller: waiting for resource %q to be ready", r.URL()))
if err := r.Ready(ctx); err != nil {
- return err
+ // CHANGE: Wrap Ready() errors with errNotReady to indicate that
+ // registration succeeded but the first fetch hasn't completed.
+ // Using %w twice creates a multi-error chain (Go 1.20+), allowing
+ // errors.Is() to check both errNotReady and the underlying error.
+ return fmt.Errorf("%w: %w", errNotReady, err)
}
}
return nil
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/errors.go b/vendor/github.com/lestrrat-go/httprc/v3/errors.go
index 1152ba947f..474c790f5d 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/errors.go
+++ b/vendor/github.com/lestrrat-go/httprc/v3/errors.go
@@ -55,3 +55,51 @@ var errBlockedByWhitelist = errors.New(`blocked by whitelist`)
func ErrBlockedByWhitelist() error {
return errBlockedByWhitelist
}
+
+var errNotReady = errors.New(`resource registered but not ready`)
+
+// ErrNotReady returns a sentinel error indicating that the resource was
+// successfully registered with the backend and is being actively managed,
+// but the first fetch and transformation has not completed successfully yet.
+//
+// This error is returned by Add() when:
+// - The resource was successfully added to the backend (registration succeeded)
+// - WithWaitReady(true) was specified (the default)
+// - The Ready() call failed (timeout, transform error, context cancelled, etc.)
+//
+// When Add() returns this error, the resource IS in the backend's resource map
+// and will continue to be fetched periodically in the background according to
+// the refresh interval. The application can safely proceed - the resource data
+// may become available later when a fetch succeeds.
+//
+// IMPORTANT: "Not ready" means the first fetch and transformation has not completed
+// successfully. The resource may eventually become ready (if the transformation
+// succeeds on a subsequent retry), or it may never become ready (if the data is
+// permanently invalid or the server is unreachable). The backend will continue
+// retrying according to the configured refresh interval.
+//
+// The underlying error (context deadline, transform failure, etc.) is wrapped
+// using Go 1.20+ multiple error wrapping and can be examined with errors.Is()
+// or errors.As(). You do not need to manually unwrap the error.
+//
+// Example:
+//
+// err := ctrl.Add(ctx, resource)
+// if err != nil {
+// if errors.Is(err, httprc.ErrNotReady()) {
+// // Resource registered, will fetch in background
+// log.Print("Resource not ready yet, continuing startup")
+//
+// // Can also check the underlying cause
+// if errors.Is(err, context.DeadlineExceeded) {
+// log.Print("Timed out waiting for first fetch")
+// }
+// return nil
+// }
+// // Registration failed
+// return fmt.Errorf("failed to register resource: %w", err)
+// }
+// // Resource registered AND ready with data
+func ErrNotReady() error {
+ return errNotReady
+}
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go b/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go
index f290422d6c..942e654c2e 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go
+++ b/vendor/github.com/lestrrat-go/httprc/v3/proxysink/proxysink.go
@@ -72,6 +72,7 @@ func (p *Proxy[T]) flushloop(ctx context.Context) {
p.mu.Unlock()
return
}
+ p.mu.Unlock()
default:
}
diff --git a/vendor/github.com/lestrrat-go/httprc/v3/worker.go b/vendor/github.com/lestrrat-go/httprc/v3/worker.go
index d11477dadc..1b74164159 100644
--- a/vendor/github.com/lestrrat-go/httprc/v3/worker.go
+++ b/vendor/github.com/lestrrat-go/httprc/v3/worker.go
@@ -42,7 +42,7 @@ func (w worker) Run(ctx context.Context, readywg *sync.WaitGroup, donewg *sync.W
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: FAILED to sync %q (synchronous): %s", sr.resource.URL(), err))
sendReply(ctx, sr.reply, struct{}{}, err)
sr.resource.SetBusy(false)
- return
+ continue
}
w.traceSink.Put(ctx, fmt.Sprintf("httprc worker: SUCCESS syncing %q (synchronous)", sr.resource.URL()))
sr.resource.SetBusy(false)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/.gitignore b/vendor/github.com/lestrrat-go/jwx/v3/.gitignore
index c4c0ebff32..09d70ac9eb 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/.gitignore
+++ b/vendor/github.com/lestrrat-go/jwx/v3/.gitignore
@@ -37,3 +37,7 @@ out
cmd/jwx/jwx
bazel-*
+
+# Go workspace files (local development only)
+go.work
+go.work.sum
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml b/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml
index 30dc4c519b..5e67fa0d1f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml
+++ b/vendor/github.com/lestrrat-go/jwx/v3/.golangci.yml
@@ -106,6 +106,18 @@ linters:
- revive
path: jwt/internal/types/
text: "var-naming: avoid meaningless package names"
+ - linters:
+ - revive
+ path: internal/json/
+ text: "var-naming: avoid package names"
+ - linters:
+ - revive
+ path: jwe/internal/cipher/
+ text: "var-naming: avoid package names that conflict with Go standard library package names"
+ - linters:
+ - revive
+ path: jwk/ecdsa/
+ text: "var-naming: avoid package names that conflict with Go standard library package names"
- linters:
- godoclint
path: (^|/)internal/
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md b/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md
new file mode 100644
index 0000000000..5be5c559bf
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/AGENTS.md
@@ -0,0 +1,266 @@
+# AGENTS.md
+
+## For Module Consumers
+
+If you are writing code that *uses* jwx (not developing jwx itself):
+
+- **Examples**: See `examples/` directory for runnable usage patterns
+- **Documentation**: See `docs/` directory and package READMEs
+- **API Reference**: Use `go doc` or https://pkg.go.dev/github.com/lestrrat-go/jwx/v3
+
+The rest of this document focuses on developing the jwx library itself.
+
+---
+
+## Go Version
+
+This project requires **Go 1.25.0** or later. Check `go.mod` for the exact version.
+
+## Module Path vs Physical Layout
+
+This repository uses a **flat layout** with vanity import paths. There is no physical `v3/` directory.
+
+| Branch | Module Path | Physical Root |
+|--------|-------------|---------------|
+| `develop/v3` | `github.com/lestrrat-go/jwx/v3` | `/` (repo root) |
+
+`import "github.com/lestrrat-go/jwx/v3/jwt"` → files are at `./jwt/`, not `./v3/jwt/`.
+
+## Code Generation
+
+### Immutable Rule
+
+**NEVER edit files ending in `_gen.go` directly.** These are generated files. Edit the generator sources instead.
+
+### Generated Files Pattern
+
+Files matching `*_gen.go` are generated. Examples:
+- `jwt/options_gen.go`
+- `jwt/token_gen.go`
+- `jws/headers_gen.go`
+- `jwk/rsa_gen.go`
+- `jwa/signature_gen.go`
+
+### Generator Locations
+
+| Generator | Location | Input Files | Output |
+|-----------|----------|-------------|--------|
+| `genoptions` | `tools/cmd/genoptions/` | `{jwa,jwe,jwk,jws,jwt}/options.yaml` | `*/options_gen.go` |
+| `genjwt` | `tools/cmd/genjwt/` | `tools/cmd/genjwt/objects.yml` | `jwt/*_gen.go` |
+| `genjws` | `tools/cmd/genjws/` | `tools/cmd/genjws/objects.yml` | `jws/*_gen.go` |
+| `genjwe` | `tools/cmd/genjwe/` | `tools/cmd/genjwe/objects.yml` | `jwe/*_gen.go` |
+| `genjwk` | `tools/cmd/genjwk/` | `tools/cmd/genjwk/objects.yml` | `jwk/*_gen.go` |
+| `genjwa` | `tools/cmd/genjwa/` | `tools/cmd/genjwa/objects.yml` | `jwa/*_gen.go` |
+| `genreadfile` | `tools/cmd/genreadfile/` | - | ReadFile helpers |
+
+### Regeneration Commands
+
+```bash
+# Regenerate all code (includes options via `go generate .`)
+make generate
+
+# Regenerate specific package (objects/types only, NOT options)
+make generate-jwt
+make generate-jws
+make generate-jwe
+make generate-jwk
+make generate-jwa
+
+# Regenerate options only (options.yaml → options_gen.go for all packages)
+go generate .
+# or directly:
+./tools/cmd/genoptions.sh
+```
+
+**Important:** `make generate-` does **not** regenerate options. If you
+edit an `options.yaml` file, run `make generate` or `go generate .`.
+
+## Functional Options Pattern
+
+Options are defined in `{package}/options.yaml` and generated into `{package}/options_gen.go`.
+
+Example `options.yaml` entry:
+
+```yaml
+options:
+ - ident: Token
+ interface: ParseOption
+ argument_type: Token
+ comment: |
+ WithToken specifies the token instance...
+```
+
+Generates `WithToken(v Token) ParseOption` function.
+
+## Multi-Module Structure
+
+This repository contains multiple Go modules. The nested modules use `replace` directives for local development.
+
+| Module | Path | Purpose |
+|--------|------|---------|
+| Main | `./go.mod` | Core library |
+| Examples | `./examples/go.mod` | Usage examples |
+| CLI | `./cmd/jwx/go.mod` | Command-line tool |
+| Perf Bench | `./bench/performance/go.mod` | Performance benchmarks |
+| Comparison | `./bench/comparison/go.mod` | Library comparison |
+| Generators | `./tools/cmd/*/go.mod` | Code generators |
+
+### Local Development
+
+The `examples/go.mod` contains:
+```go
+replace github.com/lestrrat-go/jwx/v3 v3.0.0 => ../
+```
+
+No `go.work` file is committed. When working across modules, either:
+1. Create a temporary `go.work` file (it is .gitignored)
+2. Rely on the `replace` directives already in place
+
+## Development Commands
+
+```bash
+# Run all tests
+make test
+
+# Run tests with specific build tags
+make test-goccy # Use goccy/go-json
+make test-es256k # Enable ES256K support
+make test-alltags # All optional features
+
+# Run short/smoke tests
+make smoke
+
+# Generate coverage report
+make cover
+make viewcover
+
+# Lint
+make lint
+
+# Format and tidy
+make imports
+make tidy
+```
+
+### Test Script Details
+
+Tests are run via `./tools/test.sh` which iterates over:
+- `.` (main module)
+- `./examples`
+- `./bench/performance`
+- `./cmd/jwx`
+
+## Package Directory Map
+
+| Package | Responsibility |
+|---------|----------------|
+| `jwa/` | Algorithm identifiers (e.g., `RS256`, `ES384`, `A128GCM`) |
+| `jwk/` | JSON Web Keys - key representation and management |
+| `jws/` | JSON Web Signatures - `Sign()` and `Verify()` |
+| `jwe/` | JSON Web Encryption - `Encrypt()` and `Decrypt()` |
+| `jwt/` | JSON Web Tokens - claims and validation |
+| `jwt/openid/` | OpenID Connect ID tokens |
+| `transform/` | Token transformation utilities |
+
+## Relevant RFCs
+
+- RFC 7515 - JWS (JSON Web Signature)
+- RFC 7516 - JWE (JSON Web Encryption)
+- RFC 7517 - JWK (JSON Web Key)
+- RFC 7518 - JWA (JSON Web Algorithms)
+- RFC 7519 - JWT (JSON Web Token)
+- OpenID Connect Core 1.0
+
+## Error Handling
+
+Sentinel errors are exposed via functions. Use `errors.Is()`:
+
+```go
+if errors.Is(err, jwt.TokenExpiredError()) { ... }
+```
+
+| Package | Function | Meaning |
+|---------|----------|---------|
+| `jwt` | `TokenExpiredError()` | `exp` claim not satisfied |
+| `jwt` | `TokenNotYetValidError()` | `nbf` claim not satisfied |
+| `jwt` | `InvalidIssuerError()` | `iss` claim not satisfied |
+| `jwt` | `InvalidAudienceError()` | `aud` claim not satisfied |
+| `jwt` | `ValidateError()` | Generic validation failure |
+| `jwt` | `ParseError()` | Parse failed |
+| `jws` | `VerificationError()` | Signature verification failed |
+| `jwe` | `DecryptError()` | Decryption failed |
+
+## Testing
+
+Use `github.com/stretchr/testify/require` for assertions (not `assert`).
+
+## Build Tags
+
+| Tag | Effect |
+|-----|--------|
+| `jwx_goccy` | Use `goccy/go-json` instead of `encoding/json` |
+| `jwx_es256k` | Enable secp256k1/ES256K algorithm support |
+| `jwx_secp256k1_pem` | Enable PEM encoding for secp256k1 keys |
+| `jwx_asmbase64` | Use assembly-optimized base64 |
+
+## Quick Reference: Common Modifications
+
+| Task | Edit This | Then Run |
+|------|-----------|----------|
+| Add/edit any option | `{pkg}/options.yaml` | `make generate` or `go generate .` |
+| Add new JWS header field | `tools/cmd/genjws/objects.yml` | `make generate-jws` |
+| Add new JWK key field | `tools/cmd/genjwk/objects.yml` | `make generate-jwk` |
+| Add new algorithm | `tools/cmd/genjwa/objects.yml` | `make generate-jwa` |
+| Modify token fields | `tools/cmd/genjwt/objects.yml` | `make generate-jwt` |
+
+## File Naming Conventions
+
+| Pattern | Meaning |
+|---------|---------|
+| `*_gen.go` | Generated code - DO NOT EDIT |
+| `*_test.go` | Test files |
+| `*_gen_test.go` | Generated tests - DO NOT EDIT |
+| `options.yaml` | Option definitions (input to genoptions) |
+| `objects.yml` | Object definitions (input to package-specific generators) |
+
+## Examples Directory
+
+Naming convention: `{package}_xxx_example_test.go`
+- `jwt_parse_example_test.go`
+- `jws_sign_example_test.go`
+- `jwx_example_test.go` (cross-package)
+- `jwx_readme_example_test.go` (cross-package, used in README)
+- `jwx_register_ec_and_key_example_test.go` (cross-package, key registration)
+
+Examples are included in `docs/` via autodoc markers:
+```markdown
+
+
+```
+
+## Pre-Read Rules
+
+Read linked doc BEFORE working in that area. No exceptions.
+
+| Trigger | Doc |
+|---------|-----|
+| Looking up package APIs, types, functions | `.claude/docs/packages.md` |
+| Running or writing tests, fuzz tests | `.claude/docs/testing.md` |
+| Understanding package relationships, imports | `.claude/docs/dependencies.md` |
+| Working with errors, error handling patterns | `.claude/docs/error-formatting.md` |
+| Code generation, options pattern, extension points, JSON/base64 backends | `.claude/docs/internals.md` |
+
+## Cache Maintenance
+
+These docs cache repository state. Still read source before modifying code.
+
+1. When your changes affect a doc below, update it in the same commit.
+2. If you notice any doc is wrong or stale — even on an unrelated task — fix it immediately.
+
+| Doc | Update trigger |
+|-----|----------------|
+| `.claude/docs/packages.md` | New/renamed/removed exported functions, types, or packages |
+| `.claude/docs/testing.md` | Changes to test infrastructure, build tags, test helpers, fuzz targets |
+| `.claude/docs/dependencies.md` | New internal imports between packages, new external dependencies |
+| `.claude/docs/error-formatting.md` | New sentinel errors, changes to error wrapping patterns |
+| `.claude/docs/internals.md` | Changes to generators, options YAML schema, registration points, multi-module layout |
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/Changes b/vendor/github.com/lestrrat-go/jwx/v3/Changes
index 4df33b756c..c5eeebf6dd 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/Changes
+++ b/vendor/github.com/lestrrat-go/jwx/v3/Changes
@@ -4,6 +4,337 @@ 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.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
+ `crit`; `jws.Sign` auto-declares `b64` in `crit` when emitting
+ `b64=false`; `Message.MarshalJSON` honors `b64=false` instead of
+ silently re-encoding; `jws.VerifyCompactFast` refuses any compact
+ JWS carrying `b64` (the fast path doesn't process extension
+ headers); and `b64` is now declared as a typed boolean header
+ field rather than handled ad-hoc.
+ (#2081, #2087, #2102, #2104, #2106)
+
+ * [jws] Reject malformed general-form JSON-serialized JWS: inputs
+ with a top-level `header` member as a sibling of `signatures` are
+ rejected (the spec only permits `header` inside per-signature
+ objects), as are inputs whose `protected` member is a literal
+ JSON object instead of a base64url-encoded string.
+ (#2089, #2108)
+
+ * [jws] `jws.AlgorithmsForKey` failures from unclassifiable keys
+ are now wrapped in a typed sentinel so callers can branch on
+ "couldn't categorize this key" without string matching the error
+ message. (#2110)
+
+ * [jws] Verify error-shape consistency: `VerifyCompactFast`
+ refusals now match the `jws.VerifyError()` taxonomy used by the
+ slow path, fan-out verify errors name the loose `WithKeySet`
+ options that were tried, multi-signature `b64` mismatches name
+ the offending signature index and conflicting value, and the
+ compact `b64=false`+payload-contains-`.` error references RFC
+ 7797 §5.2 and points at `WithDetachedPayload`.
+ (#2083, #2085, #2114)
+
+ * [jws] Keys fetched via the `jku` header are no longer accepted
+ for signature verification when the JWK declares `use=enc`.
+ (#2060)
+
+ * [jws][jwe] `jws.VerifyMessage` and `jwe.DecryptMessage` observe
+ context cancellation between loop iterations rather than only at
+ boundaries. Long fan-out verify/decrypt loops now respond to a
+ cancelled context promptly. (#2112, #2117)
+
+ * [jwe] Reject PBES2 messages whose `p2c` (iteration count) does
+ not parse cleanly into int64 or violates the configured bound.
+ The error now names the violated bound (min vs max) instead of
+ the generic "out of range". (#2119)
+
+ * [jwe] `jwe.WithKey()` validates the alg-vs-key shape at option
+ construction time rather than during encryption, so misuse
+ surfaces at the call site instead of inside the encrypt loop.
+ (#2121)
+
+ * [jwe] Decrypt error-path cleanup: per-key failures from key-set
+ providers are surfaced via `errors.Join` so each underlying
+ error remains inspectable; the joined error count is bounded to
+ keep diagnostics readable; the redundant outer `Decrypt:` prefix
+ is dropped; and the compression-cap error names the
+ "decompressed" payload, the option, and the size.
+ (#2123, #2125, #2127)
+
+ * [jwe] Add `jwe.WithDisabledKeyAlgorithms(...)` as a global
+ policy hook (`jwe.Configure(...)` or per-call) for refusing
+ specific key-management algorithms across all `jwe.Decrypt`
+ calls. (#2129)
+
+ * [jwe] Reject messages whose protected-header `alg` conflicts
+ with a per-recipient `alg`. The previous behavior silently
+ preferred the protected-header value. (#2052)
+
+ * [jwk] Stop duplicating JWK fields at the JWKS top level on
+ parse. A JWKS whose top-level object carries fields with the
+ same names as JWK members no longer copies those values into
+ every key in `keys`. (#2133)
+
+ * [jwk] A custom `jwk.KeyParser` returning `(nil, nil)` now
+ signals "continue to the next parser" rather than "successfully
+ produced a nil key". Callers no longer end up with a nil
+ `jwk.Key` from a successful parse when an extension returns the
+ empty pair. (#2140)
+
+ * [jwk] Stream the JWKS `keys` array with a cap-before-allocate
+ strategy. Inputs respect `WithMaxKeys` before any unbounded
+ slice growth, and bounded-size JWKS no longer over-allocate
+ based on attacker-controlled length hints. (#2137)
+
+ * [jwk] Wrap `jwk.ParseKey` errors with the `jwk.ParseError`
+ sentinel so callers can branch on parse-vs-other failures with
+ `errors.Is(err, jwk.ParseError())`. (#2135)
+
+ * [jwk] ECDSA public keys whose X / Y coordinates exceed the
+ curve's byte length are rejected with a typed error instead of
+ reaching curve arithmetic with an out-of-range `big.Int`.
+ (#2050)
+
+ * [jwt] `jwt.ParseRequest` no longer skips the request body when
+ the request uses chunked transfer encoding. The
+ Content-Length-based fast path previously bypassed `ParseForm`
+ for chunked requests even when `WithFormKey` was supplied.
+ (#2091)
+
+ * [jwt] Pedantic mode (`jwt.WithPedanticParse(true)`) enforces
+ that nested-envelope JWTs declare `cty=JWT`. Tokens missing
+ `cty` or carrying a different value are rejected. (#2094)
+
+ * [jwt] `jwt.ParseInsecure` parses the loop-local payload split
+ from the input rather than the original input bytes. Fixes a
+ case where `ParseInsecure` could return a token assembled from
+ a different signature segment than the one being inspected.
+ (#2097)
+
+ * [jwt] `jwt.WithMaxDeltaIs(...)` and `jwt.WithMinDeltaIs(...)`
+ reject tokens whose compared claim is missing rather than
+ treating it as zero. Validation no longer silently succeeds
+ when the claim isn't present on the token. (#2099)
+
+ * [jwt] `jwt.Parse` / `jwt.ParseRequest` only call `ParseForm`
+ when `WithFormKey` is supplied. Previously the form body could
+ be consumed even when the caller did not opt in to form-source
+ extraction. (#2058)
+
+ * [jwt] Fix `AddressClaim.MarshalJSON` to handle non-printable
+ bytes correctly. (#2056)
+
+ * [jwa] Unify the signature, key-encryption, and
+ content-encryption algorithm tables behind a single
+ registration entry point. Extension algorithms register once
+ rather than via three parallel APIs. (#2066)
+
+ * [cmd/jwx] Warn before writing a private key to a TTY; reject
+ `keysize <= 0` for `oct` key generation. (#2071)
+
+v3.1.0 19 Apr 2026
+ * [jwk] Add `jwk.WithRejectDuplicateKID(bool)` — when enabled, `jwk.Parse` /
+ `jwk.ParseReader` / `jwk.ParseString` return an error if the input JWKS
+ contains more than one key sharing the same non-empty `kid`. Usable as a
+ `jwk.Configure()` global or a per-call override. Default behavior
+ (first-match-wins) is unchanged.
+
+ * [jwk] Add `jwk.WithMaxKeys(int)` — caps the number of keys accepted
+ by `jwk.Parse` / `jwk.ParseReader` / `jwk.ParseString` in both the
+ JSON `"keys"` array and the PEM/X.509 block stream (default 1000).
+ Usable as a `jwk.Configure()` global or a per-call override.
+ Replaces the hardcoded internal PEM cap of the same value, so the
+ default behavior is unchanged. Backport of the v4 amplification
+ cap that mirrors `jws.WithMaxSignatures` and
+ `jwe.WithMaxRecipients`.
+
+ * [jws] Add `jws.WithDetachedPayloadReader(io.Reader)` — a streaming
+ variant of `jws.WithDetachedPayload([]byte)` that consumes the
+ payload from an `io.Reader` instead of a byte slice, so the payload
+ is never materialized in memory. It is a `jws.Sign()` / `jws.Verify()`
+ option; the two remain the default entry points. Only HMAC/RSA/ECDSA
+ algorithms are supported; EdDSA, custom-family algorithms, and
+ algorithms registered via `jws.RegisterSigner()` / `jws.RegisterVerifier()`
+ are rejected with a clear error pointing at `jws.WithDetachedPayload()`.
+ On sign, multiple `jws.WithKey()` options combined with `jws.WithJSON()`
+ produce a general-form multi-signature JWS (the payload is streamed
+ once and fanned out to each signer). On verify, only single-signature
+ JWS input is supported; `jws.WithKeySet()`, `jws.WithKeyProvider()`,
+ and `jws.WithVerifyAuto()` are not accepted. (#1663)
+
+ * [jws] Add `jws.Base64StreamEncoder` — the stream-capable extension
+ of `jws.Base64Encoder`. The default encoder and `*base64.Encoding`
+ values supplied via `jws.WithBase64Encoder()` are auto-wrapped, so
+ typical callers see no change. Custom encoders only need to
+ implement this additional interface if they want to be usable with
+ `jws.WithDetachedPayloadReader()`. (#1663)
+
+ * [jws] Fix `jws.Sign` with `WithDetachedPayload` + `WithJSON` to
+ omit the `"payload"` member from the output per RFC 7515
+ Appendix F. Previously the payload was still emitted, producing
+ non-detached JSON. (#1663)
+
+ * [jwk] BREAKING: `jwk.PublicSetOf` now returns an error when the input
+ set contains a symmetric (oct) key. Previously, symmetric keys were
+ silently passed through — which meant callers following the documented
+ "publish my public JWKS" pattern could leak HMAC secret material.
+ Callers who genuinely want the legacy pass-through behavior can opt in
+ with `jwk.WithAllowSymmetric(true)`. The signature is now variadic
+ (`PublicSetOf(v Set, options ...PublicSetOption)`), so existing call
+ sites compile unchanged. The minor version is bumped from v3.0.x →
+ v3.1.0 to reflect this deliberate behavior change.
+
+ `jwk.PublicKeyOf` on a single symmetric key is unchanged — it still
+ returns the key as-is, matching its documented behavior.
+
+ * [jws][jwe][jwk] Replace intermediate map[string]any allocation in
+ MarshalJSON with a pair-slice + sync.Pool pattern, matching the approach
+ already used in jwt. Eliminates per-call map and key-slice allocations
+ in the serialization hot path.
+
+ * [jwt][jwe][jws][jwk] Fix inconsistent mutex locking across main data
+ structures. Named getters on JWK key types, MarshalJSON on JWK keys,
+ UnmarshalJSON on JWE headers, makePairs/MarshalJSON on JWT tokens,
+ rawBuffer on JWS headers, and Set/Keys on jwk.Set were missing proper
+ lock protection. Switch all mutex fields from *sync.RWMutex (pointer)
+ to sync.RWMutex (value) so go vet -copylocks catches accidental copies,
+ and convert affected value-receiver methods to pointer receivers.
+
+ * [jwe] Add `WithMaxRecipients(int)` to reject JWE messages with more recipients
+ than the configured limit. Default is 100. Can be set globally via
+ `jwe.Settings()` or per-call in `jwe.Decrypt()` / `jwe.Parse()`. (#1633)
+
+ * [jws] Add `WithMaxSignatures(int)` to reject JWS JSON-serialized messages with
+ more signatures than the configured limit. Default is 100. Can be set globally
+ via `jws.Settings()` or per-call in `jws.Parse()`. (#1636)
+
+ * [jwk] The default HTTP client used by `jwk.Fetch()` and `jwk.Cache` now
+ enforces a 30-second timeout, blocks HTTPS-to-HTTP redirect downgrades at
+ every hop, and limits redirect chains to 5 hops. This mitigates SSRF via
+ redirect chains and slowloris-style DoS from unresponsive JWKS endpoints.
+ Callers who provide their own `http.Client` via `jwk.WithHTTPClient()` are
+ not affected. (#1634, #1637, #1639, #1640)
+
+ * [jwk] Add `jwk.DefaultHTTPClient()` which returns a new `*http.Client`
+ configured with the library's default protections. Useful for restoring
+ defaults after calling `jwk.Configure(jwk.WithHTTPClient(...))`. (#1638)
+
+ * [jwk] `WithMaxFetchBodySize(int64)` can now be set globally via
+ `jwk.Configure()` in addition to per-call. (#1631)
+
+ * [jwk] Add `jwk.WrapHTTPClientDefaults()` to apply the library's default
+ safety behaviors (timeout, redirect policy) to a caller-provided
+ `*http.Client`. Existing client settings (Transport, Jar, etc.) are
+ preserved; CheckRedirect is wrapped rather than overwritten.
+
+ * [jwt] Add `jwt.WithStrictStringClaims(true)` option for `jwt.Parse()` and
+ `jwt.ReadFile()` to reject JSON `null` for string registered claims
+ (`iss`, `sub`, `jti`). By default, null is silently accepted as an empty
+ string. (#1484)
+
+ * [jwa] Add fully-specified EdDSA signature algorithms `Ed25519` and `Ed448` per
+ RFC 9864. The polymorphic `EdDSA` algorithm is now marked as deprecated.
+ New Go accessors: `jwa.EdDSAEd25519()` and `jwa.EdDSAEd448()` (function names
+ are tentative and may change in future releases).
+ Ed448 signing/verification requires `github.com/cloudflare/circl` because
+ Go's standard library does not support Ed448. To avoid pulling in this
+ extra dependency for all users, Ed448 support is provided as a separate
+ module (`github.com/lestrrat-go/jwx-circl-ed448`). Import it for side
+ effects to enable Ed448:
+ import _ "github.com/lestrrat-go/jwx-circl-ed448"
+ Without this import, Ed448 is registered as an algorithm identifier but
+ will return an error at sign/verify time.
+
+ * [jwk] Add `jwk/jwkunsafe` package with `NewKey(kty)` and `NewPublicKey(kty)`
+ functions for creating empty, unpopulated JWK key objects. This is intended
+ for extension module authors who need to register custom KeyImporter
+ implementations for new key types.
+
+ * [jws] Add `jws.RegisterAlgorithmForKeyType()` for external modules to register
+ additional algorithm-to-key-type mappings.
+
+ * [jws/jwsbb] Add `jwsbb.RegisterAlgorithm()` for external modules to
+ register custom algorithm implementations (e.g. Ed448).
+
+ * [jws] `jws.Verify()` and `jws.VerifyCompactFast()` now validate the "crit"
+ (Critical) header parameter per RFC 7515 Section 4.1.11. Signatures with an
+ empty "crit" array, standard JOSE header names in "crit", or "crit"-listed
+ extensions not present in the protected header are now rejected. To disable
+ this validation on a per-call basis, pass
+ `jws.WithCritValidation(false)` to `jws.Verify()`.
+
+ * [jwe] `jwe.Decrypt()` now validates the "crit" (Critical) header parameter
+ per RFC 7516 Section 4.1.13, matching the jws behavior above. Messages with
+ an empty "crit" array, standard JOSE header names in "crit", or "crit"-listed
+ extensions not present in the protected header are now rejected. Declare
+ extensions with `jwe.WithCritExtension()`, or disable validation on a
+ per-call basis with `jwe.WithCritValidation(false)`. (#1735)
+
+ * [jwk] BREAKING (extension modules): `jwk.RegisterKeyExporter` now takes a
+ `jwk.KeyKind` instead of `jwa.KeyType`. Call sites migrate with
+ `jwk.KeyKind(kty.String())`; for curve-specific exporters, use a compound
+ identity like `jwk.KeyKind("OKP:Ed448")`. Only affects extension-module
+ authors registering custom exporters; library users calling `jwk.Export`
+ are unaffected.
+
+ * [jwk] Fixed inverted rlocker condition in RSA key export. (#1576)
+
+ * [jwe] Fixed X25519 ECDH-ES key agreement to include `apu` and `apv` parameters
+ in the Concat KDF derivation, matching the ECDSA path. Previously these values
+ were silently discarded during encryption, weakening the key derivation per
+ RFC 7518 Section 4.6.2.
+
+ * [jwe] POTENTIALLY BREAKING: `jwe.Decrypt()` now rejects PBES2 messages with
+ a `p2c` (iteration count) below 1,000 by default. This prevents accepting
+ tokens with trivially low iteration counts that eliminate PBKDF2 brute-force
+ protection. To restore the previous behavior or adjust the threshold, call
+ `jwe.Settings(jwe.WithMinPBES2Count(0))`.
+
+ * [jwk] Fixed a deadlock in `jwk.Cache` that occurred when repeated `Refresh()` calls
+ failed (e.g. HTTP 500 responses). Each failure killed an httprc worker goroutine,
+ and after all workers were exhausted, subsequent `Refresh()` calls would block forever. (#1551)
+
+ * [jws] Add `jws.RegisterAlgorithmForCurve()` for external modules to register
+ algorithm-to-curve mappings. `jws.AlgorithmsForKey()` now filters results
+ by the key's curve when applicable. (#1620)
+
+ * [jwk] Add `jwk.WithMaxFetchBodySize()` option to limit the response body size
+ when fetching remote JWK Sets via `jwk.Fetch()` and `jwk.Cache`. (#1622)
+
+ * [jwe] Add `jwe.WithMaxPBES2Count()` and `jwe.WithMinPBES2Count()` as per-call
+ options to `jwe.Decrypt()`, allowing callers to override the global PBES2
+ iteration count limits on a per-decryption basis. (#1623)
+
+ * [jwk] `jwk.X509CertChain()` now correctly returns `false` as the second return
+ value when the certificate chain is nil. (#1624)
+
+ * [jwk] Fixed a data race in the x509 decoder registry iteration. (#1625)
+
+ * [jwk] `jwk.Parse()` now limits PEM input to 1,000 blocks maximum to prevent
+ resource exhaustion from inputs containing thousands of small PEM blocks. (#1626)
+
+ * [jwk] RSA JWK validation is now enforced consistently across JSON parse,
+ JWKS parse, PEM/X.509 parse, and `jwk.Import()`. Keys with moduli smaller
+ than 2048 bits or unsafe public exponents are now rejected by default.
+ Compatibility knobs were added to `jwk.Configure()` via
+ `jwk.WithMinRSAModulusBits(...)` and `jwk.WithMinRSAPublicExponent(...)`.
+
+ * [jwt] `jwt.Validate()` now rejects negative durations passed to
+ `jwt.WithAcceptableSkew()`. (#1627)
+
+ * [jwt/openid] `openid.Birthdate` now accepts year `0000` as a valid value per
+ the OpenID Connect Core specification. (#1628)
+
+ * [jwk][jws][jwe] Generated `Set()` methods now deep-copy slice and byte-slice
+ values so callers cannot mutate internal header or key state after setting
+ a field. (#1659)
+
+ * [jwk] When `UnmarshalJSON` fails on a private key (RSA, EC, OKP, Symmetric),
+ all sensitive fields are now zeroed before the error is returned. This
+ prevents leaking partial key material through half-constructed objects. (#1660)
+
v3.0.13 12 Jan 2026
* [jwt] The `jwt.WithContext()` option is now properly being passed to `jws.Verify()` from
`jwt.Parse()`.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel
index c9bdc9b730..61947500b4 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel
@@ -5,7 +5,7 @@ module(
)
bazel_dep(name = "bazel_skylib", version = "1.7.1")
-bazel_dep(name = "rules_go", version = "0.55.1")
+bazel_dep(name = "rules_go", version = "0.57.0")
bazel_dep(name = "gazelle", version = "0.44.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.11.0")
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock
index 2848e8716d..7d196c3671 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock
+++ b/vendor/github.com/lestrrat-go/jwx/v3/MODULE.bazel.lock
@@ -56,12 +56,13 @@
"https://bcr.bazel.build/modules/package_metadata/0.0.2/source.json": "e53a759a72488d2c0576f57491ef2da0cf4aab05ac0997314012495935531b73",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
- "https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
"https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
"https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
+ "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580",
+ "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96",
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
"https://bcr.bazel.build/modules/protobuf/27.1/MODULE.bazel": "703a7b614728bb06647f965264967a8ef1c39e09e8f167b3ca0bb1fd80449c0d",
@@ -97,8 +98,8 @@
"https://bcr.bazel.build/modules/rules_go/0.42.0/MODULE.bazel": "8cfa875b9aa8c6fce2b2e5925e73c1388173ea3c32a0db4d2b4804b453c14270",
"https://bcr.bazel.build/modules/rules_go/0.46.0/MODULE.bazel": "3477df8bdcc49e698b9d25f734c4f3a9f5931ff34ee48a2c662be168f5f2d3fd",
"https://bcr.bazel.build/modules/rules_go/0.51.0/MODULE.bazel": "b6920f505935bfd69381651c942496d99b16e2a12f3dd5263b90ded16f3b4d0f",
- "https://bcr.bazel.build/modules/rules_go/0.55.1/MODULE.bazel": "a57a6fc59a74326c0b440d07cca209edf13c7d1a641e48cfbeab56e79f873609",
- "https://bcr.bazel.build/modules/rules_go/0.55.1/source.json": "827a740c8959c9d20616889e7746cde4dcc6ee80d25146943627ccea0736328f",
+ "https://bcr.bazel.build/modules/rules_go/0.57.0/MODULE.bazel": "bee44028b527cd6d1b7699a2c78714bba237b40ee21f90a83b472c94bc53159d",
+ "https://bcr.bazel.build/modules/rules_go/0.57.0/source.json": "a782b756d87c68a223a48848eda4b0dac1c5fd1d925d648d7598b68aa1fb6d6d",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/6.0.0/MODULE.bazel": "8a43b7df601a7ec1af61d79345c17b31ea1fedc6711fd4abfd013ea612978e39",
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/Makefile b/vendor/github.com/lestrrat-go/jwx/v3/Makefile
index 672c007b29..9f96115057 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/Makefile
+++ b/vendor/github.com/lestrrat-go/jwx/v3/Makefile
@@ -1,4 +1,4 @@
-.PHONY: generate realclean cover viewcover test lint check_diffs imports tidy jwx
+.PHONY: generate realclean cover viewcover test lint check_diffs imports tidy jwx fuzz fuzz-jwt fuzz-jws fuzz-jwe fuzz-jwk
generate:
@go generate
@$(MAKE) generate-jwa generate-jwe generate-jwk generate-jws generate-jwt
@@ -94,5 +94,26 @@ imports:
tidy:
./scripts/tidy.sh
+FUZZTIME ?= 30s
+
+fuzz: fuzz-jwt fuzz-jws fuzz-jwe fuzz-jwk
+
+fuzz-jwt:
+ go test ./jwt/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME)
+ go test ./jwt/ -run "^$$" -fuzz FuzzSignAndParse -fuzztime $(FUZZTIME)
+
+fuzz-jws:
+ go test ./jws/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME)
+ go test ./jws/ -run "^$$" -fuzz FuzzSignAndVerify -fuzztime $(FUZZTIME)
+
+fuzz-jwe:
+ go test ./jwe/ -run "^$$" -fuzz FuzzParse -fuzztime $(FUZZTIME)
+ go test ./jwe/ -run "^$$" -fuzz FuzzEncryptAndDecrypt -fuzztime $(FUZZTIME)
+
+fuzz-jwk:
+ go test ./jwk/ -run "^$$" -fuzz "^FuzzParseKey$$" -fuzztime $(FUZZTIME)
+ go test ./jwk/ -run "^$$" -fuzz "^FuzzParse$$" -fuzztime $(FUZZTIME)
+ go test ./jwk/ -run "^$$" -fuzz FuzzParseKeyRoundtrip -fuzztime $(FUZZTIME)
+
jwx:
@./tools/cmd/install-jwx.sh
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel
index f308530bcf..8ec85d00be 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/BUILD.bazel
@@ -5,12 +5,15 @@ go_library(
srcs = [
"cert.go",
"chain.go",
+ "options.go",
+ "settings.go",
],
importpath = "github.com/lestrrat-go/jwx/v3/cert",
visibility = ["//visibility:public"],
deps = [
- "//internal/base64",
- "//internal/tokens",
+ "//internal/base64",
+ "//internal/tokens",
+ "@com_github_lestrrat_go_option_v2//:option",
],
)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go
index efefbcb417..8007c74133 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/cert.go
@@ -1,6 +1,7 @@
package cert
import (
+ "bytes"
"crypto/x509"
stdlibb64 "encoding/base64"
"fmt"
@@ -31,18 +32,81 @@ func EncodeBase64(der []byte) ([]byte, error) {
return dst, nil
}
-// Parse is a utility function to decode a base64 encoded
-// ASN.1 DER format certificate, and to parse the byte sequence.
-// The certificate must be in PKIX format, and it must not contain PEM markers
+// Parse decodes a base64-encoded ASN.1 DER certificate and validates that it
+// parses as X.509.
+//
+// The certificate must be in PKIX format and it must not contain PEM markers.
+// The maximum decoded certificate size is controlled by `cert.Settings()`.
func Parse(src []byte) (*x509.Certificate, error) {
+ src = stripASCIIWhitespace(bytes.TrimSpace(src))
+ if err := validateEncodedCertificateSize(src); err != nil {
+ return nil, err
+ }
+
dst, err := base64.Decode(src)
if err != nil {
return nil, fmt.Errorf(`failed to base64 decode the certificate: %w`, err)
}
- cert, err := x509.ParseCertificate(dst)
+ return validateDERCertificate(dst)
+}
+
+func validateDERCertificate(der []byte) (*x509.Certificate, error) {
+ if err := validateCertificateSize(len(der)); err != nil {
+ return nil, err
+ }
+
+ cert, err := x509.ParseCertificate(der)
if err != nil {
return nil, fmt.Errorf(`failed to parse x509 certificate: %w`, err)
}
return cert, nil
}
+
+func validateEncodedCertificateSize(src []byte) error {
+ return validateCertificateSize(decodedCertificateSize(src))
+}
+
+func decodedCertificateSize(src []byte) int {
+ n := len(src)
+ if n == 0 {
+ return 0
+ }
+
+ size := n / 4 * 3
+ switch n % 4 {
+ case 2:
+ size++
+ case 3:
+ size += 2
+ }
+
+ if n%4 == 0 && src[n-1] == '=' {
+ size--
+ if n > 1 && src[n-2] == '=' {
+ size--
+ }
+ }
+
+ if size < 0 {
+ return 0
+ }
+ return size
+}
+
+func normalizeAndValidateChainCertificate(src []byte) ([]byte, error) {
+ normalized := stripASCIIWhitespace(src)
+ if err := validateEncodedCertificateSize(normalized); err != nil {
+ return nil, err
+ }
+
+ der, err := stdlibb64.StdEncoding.DecodeString(string(normalized))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to base64 decode the certificate: %w`, err)
+ }
+
+ if _, err := validateDERCertificate(der); err != nil {
+ return nil, err
+ }
+ return normalized, nil
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go
index 112274669a..30e48296f0 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/chain.go
@@ -2,8 +2,11 @@ package cert
import (
"bytes"
+ "encoding/base64"
"encoding/json"
+ "encoding/pem"
"fmt"
+ "io"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
@@ -11,8 +14,9 @@ import (
// Chain represents a certificate chain as used in the `x5c` field of
// various objects within JOSE.
//
-// It stores the certificates as a list of base64 encoded []byte
-// sequence. By definition these values must PKIX encoded.
+// It stores the certificates as a list of base64-encoded byte sequences. Every
+// certificate added to or decoded into the chain must parse as X.509 and is
+// subject to the global limits configured by `cert.Settings()`.
type Chain struct {
certificates [][]byte
}
@@ -24,24 +28,65 @@ func (cc Chain) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
- buf.WriteByte('"')
- buf.Write(cert)
- buf.WriteByte('"')
+ encoded, err := json.Marshal(string(cert))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to encode certificate at index %d: %w`, i, err)
+ }
+ buf.Write(encoded)
}
buf.WriteByte(tokens.CloseSquareBracket)
return buf.Bytes(), nil
}
+// UnmarshalJSON decodes an `x5c` JSON array and validates each entry as a
+// base64-encoded X.509 certificate.
func (cc *Chain) UnmarshalJSON(data []byte) error {
- var tmp []string
- if err := json.Unmarshal(data, &tmp); err != nil {
+ dec := json.NewDecoder(bytes.NewReader(data))
+ tok, err := dec.Token()
+ if err != nil {
return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err)
}
- certs := make([][]byte, len(tmp))
- for i, cert := range tmp {
- certs[i] = []byte(cert)
+ delim, ok := tok.(json.Delim)
+ if !ok || delim != '[' {
+ return fmt.Errorf(`failed to unmarshal certificate chain: expected JSON array`)
}
+
+ var certs [][]byte
+ for dec.More() {
+ if err := validateChainLength(len(certs) + 1); err != nil {
+ return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err)
+ }
+
+ var cert string
+ if err := dec.Decode(&cert); err != nil {
+ return fmt.Errorf(`failed to decode certificate at index %d: %w`, len(certs), err)
+ }
+
+ normalized, err := normalizeAndValidateChainCertificate([]byte(cert))
+ if err != nil {
+ return fmt.Errorf(`failed to decode certificate at index %d: %w`, len(certs), err)
+ }
+ certs = append(certs, normalized)
+ }
+
+ tok, err = dec.Token()
+ if err != nil {
+ return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err)
+ }
+
+ delim, ok = tok.(json.Delim)
+ if !ok || delim != ']' {
+ return fmt.Errorf(`failed to unmarshal certificate chain: expected closing array`)
+ }
+
+ if _, err := dec.Token(); err != io.EOF {
+ if err != nil {
+ return fmt.Errorf(`failed to unmarshal certificate chain: %w`, err)
+ }
+ return fmt.Errorf(`failed to unmarshal certificate chain: unexpected trailing data`)
+ }
+
cc.certificates = certs
return nil
}
@@ -62,19 +107,52 @@ func (cc *Chain) Len() int {
return len(cc.certificates)
}
-var pemStart = []byte("----- BEGIN CERTIFICATE -----")
-var pemEnd = []byte("----- END CERTIFICATE -----")
-
func (cc *Chain) AddString(der string) error {
return cc.Add([]byte(der))
}
+// Add appends a certificate to the chain.
+//
+// Input may be either a PEM `CERTIFICATE` block or a base64-encoded DER value
+// as stored in JOSE `x5c` fields. The certificate is validated as X.509 and is
+// subject to the global limits configured by `cert.Settings()`.
func (cc *Chain) Add(der []byte) error {
- // We're going to be nice and remove marker lines if they
- // give it to us
- der = bytes.TrimPrefix(der, pemStart)
- der = bytes.TrimSuffix(der, pemEnd)
+ if err := validateChainLength(len(cc.certificates) + 1); err != nil {
+ return fmt.Errorf(`cert.Chain.Add: %w`, err)
+ }
+
der = bytes.TrimSpace(der)
- cc.certificates = append(cc.certificates, der)
+ // Accept a PEM-encoded CERTIFICATE block and convert it to the
+ // base64(DER) form that x5c requires.
+ if block, _ := pem.Decode(der); block != nil && block.Type == "CERTIFICATE" {
+ if _, err := validateDERCertificate(block.Bytes); err != nil {
+ return fmt.Errorf(`cert.Chain.Add: %w`, err)
+ }
+
+ encoded := make([]byte, base64.StdEncoding.EncodedLen(len(block.Bytes)))
+ base64.StdEncoding.Encode(encoded, block.Bytes)
+ cc.certificates = append(cc.certificates, encoded)
+ return nil
+ }
+
+ // Non-PEM input must be base64(DER). Strip any internal whitespace
+ // (callers commonly pass multi-line base64 literals) and validate.
+ normalized, err := normalizeAndValidateChainCertificate(der)
+ if err != nil {
+ return fmt.Errorf(`cert.Chain.Add: %w`, err)
+ }
+ cc.certificates = append(cc.certificates, normalized)
return nil
}
+
+func stripASCIIWhitespace(src []byte) []byte {
+ dst := make([]byte, 0, len(src))
+ for _, b := range src {
+ switch b {
+ case ' ', '\t', '\r', '\n', '\v', '\f':
+ continue
+ }
+ dst = append(dst, b)
+ }
+ return dst
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go
new file mode 100644
index 0000000000..b374ae18cc
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/options.go
@@ -0,0 +1,34 @@
+package cert
+
+import "github.com/lestrrat-go/option/v2"
+
+// GlobalOption describes an option that can be passed to `cert.Settings()`.
+type GlobalOption interface {
+ option.Interface
+ globalOption()
+}
+
+type globalOption struct {
+ option.Interface
+}
+
+func (*globalOption) globalOption() {}
+
+type identMaxChainLength struct{}
+type identMaxCertificateSize struct{}
+
+// WithMaxChainLength specifies the maximum number of certificates allowed in
+// a certificate chain handled by `cert.Chain`.
+//
+// The default is 10. Set to 0 to disable the limit.
+func WithMaxChainLength(v int) GlobalOption {
+ return &globalOption{option.New(identMaxChainLength{}, v)}
+}
+
+// WithMaxCertificateSize specifies the maximum decoded DER size, in bytes,
+// accepted by `cert.Parse()` and `cert.Chain` ingestion.
+//
+// The default is 256 KiB. Set to 0 to disable the limit.
+func WithMaxCertificateSize(v int64) GlobalOption {
+ return &globalOption{option.New(identMaxCertificateSize{}, v)}
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go b/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go
new file mode 100644
index 0000000000..50c90800de
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/cert/settings.go
@@ -0,0 +1,76 @@
+package cert
+
+import (
+ "fmt"
+ "sync/atomic"
+)
+
+const (
+ defaultMaxChainLength = 10
+ defaultMaxCertificateSize = 256 * 1024
+)
+
+var maxChainLength atomic.Int64
+var maxCertificateSize atomic.Int64
+
+func init() {
+ maxChainLength.Store(defaultMaxChainLength)
+ maxCertificateSize.Store(defaultMaxCertificateSize)
+}
+
+// Settings configures process-global validation limits for `cert.Parse()` and
+// `cert.Chain` ingestion.
+//
+// These settings are read atomically, so changing them at runtime is race-free.
+// However, concurrent parses may observe a mix of old and new values. Configure
+// them once at program startup when possible.
+func Settings(options ...GlobalOption) {
+ for _, opt := range options {
+ switch opt.Ident() {
+ case identMaxChainLength{}:
+ var v int
+ if err := opt.Value(&v); err != nil {
+ panic(fmt.Sprintf("cert.Settings: value for option WithMaxChainLength must be an int: %s", err))
+ }
+ if v < 0 {
+ panic("cert.Settings: WithMaxChainLength must be greater than or equal to zero")
+ }
+ maxChainLength.Store(int64(v))
+ case identMaxCertificateSize{}:
+ var v int64
+ if err := opt.Value(&v); err != nil {
+ panic(fmt.Sprintf("cert.Settings: value for option WithMaxCertificateSize must be an int64: %s", err))
+ }
+ if v < 0 {
+ panic("cert.Settings: WithMaxCertificateSize must be greater than or equal to zero")
+ }
+ maxCertificateSize.Store(v)
+ }
+ }
+}
+
+func currentMaxChainLength() int64 {
+ return maxChainLength.Load()
+}
+
+func currentMaxCertificateSize() int64 {
+ return maxCertificateSize.Load()
+}
+
+func validateChainLength(n int) error {
+ limit := currentMaxChainLength()
+ if limit == 0 || int64(n) <= limit {
+ return nil
+ }
+
+ return fmt.Errorf(`certificate chain length %d exceeds maximum allowed length of %d`, n, limit)
+}
+
+func validateCertificateSize(n int) error {
+ limit := currentMaxCertificateSize()
+ if limit == 0 || int64(n) <= limit {
+ return nil
+ }
+
+ return fmt.Errorf(`certificate size %d exceeds maximum allowed size of %d bytes`, n, limit)
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go
index 6e83ecc4a5..f06f6f6c74 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/asmbase64.go
@@ -3,7 +3,9 @@
package base64
import (
+ stdbase64 "encoding/base64"
"fmt"
+ "io"
"slices"
asmbase64 "github.com/segmentio/asm/base64"
@@ -25,6 +27,15 @@ func (e asmEncoder) AppendEncode(dst, src []byte) []byte {
return dst[:len(dst)+n]
}
+// NewEncoder satisfies [StreamEncoder]. segmentio/asm's base64 package
+// does not provide a streaming encoder, so this falls back to the
+// stdlib's RawURLEncoding streaming encoder — output is byte-identical
+// for RFC 4648 raw URL encoding, so the signing prefix (produced via
+// asm) and the streamed payload remain consistent.
+func (e asmEncoder) NewEncoder(w io.Writer) io.WriteCloser {
+ return stdbase64.NewEncoder(stdbase64.RawURLEncoding, w)
+}
+
type asmDecoder struct{}
func (d asmDecoder) Decode(src []byte) ([]byte, error) {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go
index 5ed8e35006..356131da19 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/base64/base64.go
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/binary"
"fmt"
+ "io"
"sync"
)
@@ -19,6 +20,55 @@ type Encoder interface {
AppendEncode([]byte, []byte) []byte
}
+// StreamEncoder is an [Encoder] that can also produce an incremental
+// [io.WriteCloser] for encoding a byte stream directly into a downstream
+// writer. This is the shape the jws streaming detached-payload path
+// needs to avoid materializing the payload in memory.
+//
+// The stdlib *[encoding/base64.Encoding] satisfies this interface
+// automatically via [AsStreamEncoder] (the stdlib exposes NewEncoder as
+// a package-level function rather than a method, so a small wrapper is
+// applied on lookup). Extension modules providing custom encoders
+// should implement [io.WriteCloser]-returning NewEncoder if they want
+// their encoder honored by the streaming path.
+type StreamEncoder interface {
+ Encoder
+ // NewEncoder returns a new [io.WriteCloser] that encodes bytes
+ // written to it and forwards the encoded output to w. Close must
+ // be called to flush any partial final block.
+ NewEncoder(w io.Writer) io.WriteCloser
+}
+
+// AsStreamEncoder reports whether e can be used as a [StreamEncoder]
+// and returns the stream-capable view. Callers should error out when
+// the second return value is false rather than silently falling back
+// to a different encoder, to avoid mixing encodings within a single
+// signing operation.
+//
+// The stdlib [*encoding/base64.Encoding] is supported as a special
+// case (its streaming form is a top-level function rather than a
+// method, so it does not directly satisfy the interface).
+func AsStreamEncoder(e Encoder) (StreamEncoder, bool) {
+ if s, ok := e.(StreamEncoder); ok {
+ return s, true
+ }
+ if enc, ok := e.(*base64.Encoding); ok {
+ return stdStreamEncoder{Encoding: enc}, true
+ }
+ return nil, false
+}
+
+// stdStreamEncoder wraps the stdlib [*base64.Encoding] so it satisfies
+// [StreamEncoder]. It is used as the fallback in [AsStreamEncoder]
+// when a caller passes a raw [*base64.Encoding].
+type stdStreamEncoder struct {
+ *base64.Encoding
+}
+
+func (e stdStreamEncoder) NewEncoder(w io.Writer) io.WriteCloser {
+ return base64.NewEncoder(e.Encoding, w)
+}
+
var muEncoder sync.RWMutex
var encoder Encoder = base64.RawURLEncoding
var muDecoder sync.RWMutex
@@ -59,6 +109,14 @@ func Encode(src []byte) []byte {
return dst
}
+func AppendEncode(dst, src []byte) []byte {
+ return getEncoder().AppendEncode(dst, src)
+}
+
+func EncodedLen(n int) int {
+ return getEncoder().EncodedLen(n)
+}
+
func EncodeToString(src []byte) string {
return getEncoder().EncodeToString(src)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go
index c1917ef27a..4dec2b806c 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/json.go
@@ -3,16 +3,16 @@ package json
import (
"bytes"
"fmt"
- "os"
+
"sync/atomic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
)
-var useNumber uint32 // TODO: at some point, change to atomic.Bool
+var useNumber atomic.Uint32
func UseNumber() bool {
- return atomic.LoadUint32(&useNumber) == 1
+ return useNumber.Load() == 1
}
// Sets the global configuration for json decoding
@@ -21,7 +21,7 @@ func DecoderSettings(inUseNumber bool) {
if inUseNumber {
val = 1
}
- atomic.StoreUint32(&useNumber, val)
+ useNumber.Store(val)
}
// Unmarshal respects the values specified in DecoderSettings,
@@ -45,7 +45,35 @@ func AssignNextBytesToken(dst *[]byte, dec *Decoder) error {
return nil
}
-func ReadNextStringToken(dec *Decoder) (string, error) {
+func shouldRejectNullStrings(dc DecodeCtx) bool {
+ if dc != nil {
+ if sdc, ok := dc.(StrictStringDecodeCtx); ok {
+ return sdc.StrictStrings()
+ }
+ }
+ return false
+}
+
+// ReadNextStringToken reads the next JSON token from the decoder and
+// returns it as a string. By default, JSON null is silently accepted as "".
+// When the given DecodeCtx implements StrictStringDecodeCtx and StrictStrings()
+// returns true, null values are rejected.
+func ReadNextStringToken(dec *Decoder, dc DecodeCtx) (string, error) {
+ if shouldRejectNullStrings(dc) {
+ var val any
+ if err := dec.Decode(&val); err != nil {
+ return "", fmt.Errorf(`error reading next value: %w`, err)
+ }
+ if val == nil {
+ return "", fmt.Errorf(`error reading next value: expected string, got null`)
+ }
+ s, ok := val.(string)
+ if !ok {
+ return "", fmt.Errorf(`error reading next value: expected string, got %T`, val)
+ }
+ return s, nil
+ }
+
var val string
if err := dec.Decode(&val); err != nil {
return "", fmt.Errorf(`error reading next value: %w`, err)
@@ -53,8 +81,8 @@ func ReadNextStringToken(dec *Decoder) (string, error) {
return val, nil
}
-func AssignNextStringToken(dst **string, dec *Decoder) error {
- val, err := ReadNextStringToken(dec)
+func AssignNextStringToken(dst **string, dec *Decoder, dc DecodeCtx) error {
+ val, err := ReadNextStringToken(dec, dc)
if err != nil {
return err
}
@@ -106,22 +134,33 @@ type DecodeCtxContainer interface {
SetDecodeCtx(DecodeCtx)
}
-// stock decodeCtx. should cover 80% of the cases
-type decodeCtx struct {
- registry *Registry
+// StrictStringDecodeCtx is an optional interface that DecodeCtx implementations
+// can satisfy to control per-call null string rejection.
+type StrictStringDecodeCtx interface {
+ StrictStrings() bool
}
+// stock decodeCtx. should cover 80% of the cases
+type decodeCtx struct {
+ registry *Registry
+ strictStrings bool
+}
+
+// NewDecodeCtx creates a new DecodeCtx with the given registry.
func NewDecodeCtx(r *Registry) DecodeCtx {
return &decodeCtx{registry: r}
}
+// NewDecodeCtxStrictStrings creates a new DecodeCtx with the given registry
+// and strict string rejection flag.
+func NewDecodeCtxStrictStrings(r *Registry, strict bool) DecodeCtx {
+ return &decodeCtx{registry: r, strictStrings: strict}
+}
+
func (dc *decodeCtx) Registry() *Registry {
return dc.registry
}
-func Dump(v any) {
- enc := NewEncoder(os.Stdout)
- enc.SetIndent("", " ")
- //nolint:errchkjson
- _ = enc.Encode(v)
+func (dc *decodeCtx) StrictStrings() bool {
+ return dc.strictStrings
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go
index 9e51fa7fe9..f249d8f60a 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/json/stdlib.go
@@ -1,6 +1,5 @@
//go:build !jwx_goccy
-//nolint:revive
package json
import (
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel
deleted file mode 100644
index c70c4d2871..0000000000
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/BUILD.bazel
+++ /dev/null
@@ -1,8 +0,0 @@
-load("@rules_go//go:def.bzl", "go_library")
-
-go_library(
- name = "jwxio",
- srcs = ["jwxio.go"],
- importpath = "github.com/lestrrat-go/jwx/v3/internal/jwxio",
- visibility = ["//:__subpackages__"],
-)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go
deleted file mode 100644
index 8396417a9d..0000000000
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/jwxio/jwxio.go
+++ /dev/null
@@ -1,29 +0,0 @@
-package jwxio
-
-import (
- "bytes"
- "errors"
- "io"
- "strings"
-)
-
-var errNonFiniteSource = errors.New(`cannot read from non-finite source`)
-
-func NonFiniteSourceError() error {
- return errNonFiniteSource
-}
-
-// ReadAllFromFiniteSource reads all data from a io.Reader _if_ it comes from a
-// finite source.
-func ReadAllFromFiniteSource(rdr io.Reader) ([]byte, error) {
- switch rdr.(type) {
- case *bytes.Reader, *bytes.Buffer, *strings.Reader:
- data, err := io.ReadAll(rdr)
- if err != nil {
- return nil, err
- }
- return data, nil
- default:
- return nil, errNonFiniteSource
- }
-}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel
index cebc269330..c48330e278 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/BUILD.bazel
@@ -3,7 +3,6 @@ load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "pool",
srcs = [
- "big_int.go",
"byte_slice.go",
"bytes_buffer.go",
"error_slice.go",
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go
deleted file mode 100644
index 57c446d4d2..0000000000
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/big_int.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package pool
-
-import "math/big"
-
-var bigIntPool = New[*big.Int](allocBigInt, freeBigInt)
-
-func allocBigInt() *big.Int {
- return &big.Int{}
-}
-
-func freeBigInt(b *big.Int) *big.Int {
- b.SetInt64(0) // Reset the value to zero
- return b
-}
-
-// BigInt returns a pool of *big.Int instances.
-func BigInt() *Pool[*big.Int] {
- return bigIntPool
-}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go
index 46f1028343..857d41ca38 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/byte_slice.go
@@ -9,9 +9,14 @@ func allocByteSlice() []byte {
}
func freeByteSlice(b []byte) []byte {
+ // Defensive: scrub the entire backing array, not just b[:len(b)]. No
+ // current caller is known to reslice past len(b) and observe stale
+ // bytes, but a defer Put(buf) that captures buf at len=0 (before a
+ // subsequent buf = buf[:n]) would otherwise leave plaintext resident
+ // in the pool's backing storage.
+ b = b[:cap(b)]
clear(b)
- b = b[:0] // Reset the slice to zero length
- return b
+ return b[:0]
}
func ByteSlice() SlicePool[byte] {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go
index a877f73ff8..090c83400d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/internal/pool/bytes_buffer.go
@@ -9,6 +9,13 @@ func allocBytesBuffer() *bytes.Buffer {
}
func freeBytesBuffer(b *bytes.Buffer) *bytes.Buffer {
+ // Zero the backing array before returning to pool — the buffer
+ // may hold private-key material, plaintext, or HMAC input.
+ // b.Bytes() shares the internal slice (offset is always 0 in
+ // our write-only usage); reslicing to cap reaches all residual bytes.
+ if buf := b.Bytes(); cap(buf) > 0 {
+ clear(buf[:cap(buf)])
+ }
b.Reset()
return b
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel
index cfb7af02a0..6d0af7efb3 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/BUILD.bazel
@@ -5,6 +5,7 @@ go_library(
srcs = [
"compression_gen.go",
"content_encryption_gen.go",
+ "ed448.go",
"elliptic_gen.go",
"jwa.go",
"key_encryption_gen.go",
@@ -29,10 +30,11 @@ go_test(
"jwa_test.go",
"key_encryption_gen_test.go",
"key_type_gen_test.go",
+ "options_gen_test.go",
"signature_gen_test.go",
],
+ embed = [":jwa"],
deps = [
- ":jwa",
"@com_github_stretchr_testify//require",
"@com_github_lestrrat_go_option_v2//:option",
],
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go
index a7a2451afa..42e5006a9c 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/compression_gen.go
@@ -22,6 +22,9 @@ func init() {
algorithms[1] = NewCompressionAlgorithm("")
RegisterCompressionAlgorithm(algorithms...)
+ for _, alg := range algorithms {
+ builtinCompressionAlgorithm[alg.String()] = struct{}{}
+ }
}
// Deflate returns an object representing the "DEF" content compression algorithm value. Using this value specifies that the content should be compressed using DEFLATE (RFC 1951).
@@ -88,36 +91,44 @@ func LookupCompressionAlgorithm(name string) (CompressionAlgorithm, bool) {
// RegisterCompressionAlgorithm registers a new CompressionAlgorithm. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
func RegisterCompressionAlgorithm(algorithms ...CompressionAlgorithm) {
muAllCompressionAlgorithm.Lock()
+ defer muAllCompressionAlgorithm.Unlock()
for _, alg := range algorithms {
+ if _, ok := builtinCompressionAlgorithm[alg.String()]; ok {
+ if existing, ok := allCompressionAlgorithm[alg.String()]; ok && existing != alg {
+ continue
+ }
+ continue
+ }
allCompressionAlgorithm[alg.String()] = alg
}
- muAllCompressionAlgorithm.Unlock()
- rebuildCompressionAlgorithm()
+ rebuildCompressionAlgorithmLocked()
}
// UnregisterCompressionAlgorithm unregisters a CompressionAlgorithm from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterCompressionAlgorithm(algorithms ...CompressionAlgorithm) {
muAllCompressionAlgorithm.Lock()
+ defer muAllCompressionAlgorithm.Unlock()
for _, alg := range algorithms {
if _, ok := builtinCompressionAlgorithm[alg.String()]; ok {
continue
}
delete(allCompressionAlgorithm, alg.String())
}
- muAllCompressionAlgorithm.Unlock()
- rebuildCompressionAlgorithm()
+ rebuildCompressionAlgorithmLocked()
}
-func rebuildCompressionAlgorithm() {
+func rebuildCompressionAlgorithmLocked() {
list := make([]CompressionAlgorithm, 0, len(allCompressionAlgorithm))
- muAllCompressionAlgorithm.RLock()
for _, v := range allCompressionAlgorithm {
list = append(list, v)
}
- muAllCompressionAlgorithm.RUnlock()
sort.Slice(list, func(i, j int) bool {
return list[i].String() < list[j].String()
})
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go
index 8ccc47e462..b04b1c755d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/content_encryption_gen.go
@@ -5,18 +5,10 @@ package jwa
import (
"encoding/json"
"fmt"
- "sort"
- "sync"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
-var muAllContentEncryptionAlgorithm sync.RWMutex
-var allContentEncryptionAlgorithm = map[string]ContentEncryptionAlgorithm{}
-var muListContentEncryptionAlgorithm sync.RWMutex
-var listContentEncryptionAlgorithm []ContentEncryptionAlgorithm
-var builtinContentEncryptionAlgorithm = map[string]struct{}{}
-
func init() {
// builtin values for ContentEncryptionAlgorithm
algorithms := make([]ContentEncryptionAlgorithm, 6)
@@ -28,6 +20,9 @@ func init() {
algorithms[5] = NewContentEncryptionAlgorithm(tokens.A256GCM)
RegisterContentEncryptionAlgorithm(algorithms...)
+ for _, alg := range algorithms {
+ markBuiltin(alg.String())
+ }
}
// A128CBC_HS256 returns an object representing A128CBC-HS256. Using this value specifies that the content should be encrypted using AES-CBC + HMAC-SHA256 (128).
@@ -61,13 +56,11 @@ func A256GCM() ContentEncryptionAlgorithm {
}
func lookupBuiltinContentEncryptionAlgorithm(name string) ContentEncryptionAlgorithm {
- muAllContentEncryptionAlgorithm.RLock()
- v, ok := allContentEncryptionAlgorithm[name]
- muAllContentEncryptionAlgorithm.RUnlock()
+ v, ok := lookupAlgorithm(algKindContentEncryption, name)
if !ok {
panic(fmt.Sprintf(`jwa: ContentEncryptionAlgorithm %q not registered`, name))
}
- return v
+ return v.(ContentEncryptionAlgorithm)
}
// ContentEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-5
@@ -106,57 +99,46 @@ func NewContentEncryptionAlgorithm(name string, options ...NewAlgorithmOption) C
// LookupContentEncryptionAlgorithm returns the ContentEncryptionAlgorithm object for the given name.
func LookupContentEncryptionAlgorithm(name string) (ContentEncryptionAlgorithm, bool) {
- muAllContentEncryptionAlgorithm.RLock()
- v, ok := allContentEncryptionAlgorithm[name]
- muAllContentEncryptionAlgorithm.RUnlock()
- return v, ok
+ if v, ok := lookupAlgorithm(algKindContentEncryption, name); ok {
+ return v.(ContentEncryptionAlgorithm), true
+ }
+ var zero ContentEncryptionAlgorithm
+ return zero, false
}
// RegisterContentEncryptionAlgorithm registers a new ContentEncryptionAlgorithm. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
+//
+// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm
+// share a single algorithm-name namespace so that KeyAlgorithmFrom can
+// resolve unambiguously. Registering a name that is already registered as a
+// different kind is a silent no-op (the first Register* call wins).
func RegisterContentEncryptionAlgorithm(algorithms ...ContentEncryptionAlgorithm) {
- muAllContentEncryptionAlgorithm.Lock()
for _, alg := range algorithms {
- allContentEncryptionAlgorithm[alg.String()] = alg
+ registerAlgorithm(algKindContentEncryption, alg)
}
- muAllContentEncryptionAlgorithm.Unlock()
- rebuildContentEncryptionAlgorithm()
}
// UnregisterContentEncryptionAlgorithm unregisters a ContentEncryptionAlgorithm from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterContentEncryptionAlgorithm(algorithms ...ContentEncryptionAlgorithm) {
- muAllContentEncryptionAlgorithm.Lock()
for _, alg := range algorithms {
- if _, ok := builtinContentEncryptionAlgorithm[alg.String()]; ok {
- continue
- }
- delete(allContentEncryptionAlgorithm, alg.String())
+ unregisterAlgorithm(algKindContentEncryption, alg.String())
}
- muAllContentEncryptionAlgorithm.Unlock()
- rebuildContentEncryptionAlgorithm()
-}
-
-func rebuildContentEncryptionAlgorithm() {
- list := make([]ContentEncryptionAlgorithm, 0, len(allContentEncryptionAlgorithm))
- muAllContentEncryptionAlgorithm.RLock()
- for _, v := range allContentEncryptionAlgorithm {
- list = append(list, v)
- }
- muAllContentEncryptionAlgorithm.RUnlock()
- sort.Slice(list, func(i, j int) bool {
- return list[i].String() < list[j].String()
- })
- muListContentEncryptionAlgorithm.Lock()
- listContentEncryptionAlgorithm = list
- muListContentEncryptionAlgorithm.Unlock()
}
// ContentEncryptionAlgorithms returns a list of all available values for ContentEncryptionAlgorithm.
func ContentEncryptionAlgorithms() []ContentEncryptionAlgorithm {
- muListContentEncryptionAlgorithm.RLock()
- defer muListContentEncryptionAlgorithm.RUnlock()
- return listContentEncryptionAlgorithm
+ raw := listAlgorithmsByKind(algKindContentEncryption)
+ out := make([]ContentEncryptionAlgorithm, len(raw))
+ for i, alg := range raw {
+ out[i] = alg.(ContentEncryptionAlgorithm)
+ }
+ return out
}
// MarshalJSON serializes the ContentEncryptionAlgorithm object to a JSON string.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go
new file mode 100644
index 0000000000..f28d7296b3
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/ed448.go
@@ -0,0 +1,14 @@
+package jwa
+
+// EdDSAEd448 returns an object representing the EdDSA signature algorithm
+// using Ed448 (RFC 9864).
+//
+// Unlike built-in algorithms, Ed448 is not registered by default. Import
+// the ed448 module for its side effects to enable Ed448 support:
+//
+// import _ "github.com/lestrrat-go/jwx-circl-ed448"
+//
+// The function name is tentative and may change in future releases.
+func EdDSAEd448() SignatureAlgorithm {
+ return NewSignatureAlgorithm("Ed448")
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go
index 2418efde08..b6d7de5490 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/elliptic_gen.go
@@ -27,6 +27,9 @@ func init() {
algorithms[6] = NewEllipticCurveAlgorithm("X448")
RegisterEllipticCurveAlgorithm(algorithms...)
+ for _, alg := range algorithms {
+ builtinEllipticCurveAlgorithm[alg.String()] = struct{}{}
+ }
}
// Ed25519 returns an object representing Ed25519 algorithm for EdDSA operations.
@@ -125,36 +128,44 @@ func LookupEllipticCurveAlgorithm(name string) (EllipticCurveAlgorithm, bool) {
// RegisterEllipticCurveAlgorithm registers a new EllipticCurveAlgorithm. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
func RegisterEllipticCurveAlgorithm(algorithms ...EllipticCurveAlgorithm) {
muAllEllipticCurveAlgorithm.Lock()
+ defer muAllEllipticCurveAlgorithm.Unlock()
for _, alg := range algorithms {
+ if _, ok := builtinEllipticCurveAlgorithm[alg.String()]; ok {
+ if existing, ok := allEllipticCurveAlgorithm[alg.String()]; ok && existing != alg {
+ continue
+ }
+ continue
+ }
allEllipticCurveAlgorithm[alg.String()] = alg
}
- muAllEllipticCurveAlgorithm.Unlock()
- rebuildEllipticCurveAlgorithm()
+ rebuildEllipticCurveAlgorithmLocked()
}
// UnregisterEllipticCurveAlgorithm unregisters a EllipticCurveAlgorithm from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterEllipticCurveAlgorithm(algorithms ...EllipticCurveAlgorithm) {
muAllEllipticCurveAlgorithm.Lock()
+ defer muAllEllipticCurveAlgorithm.Unlock()
for _, alg := range algorithms {
if _, ok := builtinEllipticCurveAlgorithm[alg.String()]; ok {
continue
}
delete(allEllipticCurveAlgorithm, alg.String())
}
- muAllEllipticCurveAlgorithm.Unlock()
- rebuildEllipticCurveAlgorithm()
+ rebuildEllipticCurveAlgorithmLocked()
}
-func rebuildEllipticCurveAlgorithm() {
+func rebuildEllipticCurveAlgorithmLocked() {
list := make([]EllipticCurveAlgorithm, 0, len(allEllipticCurveAlgorithm))
- muAllEllipticCurveAlgorithm.RLock()
for _, v := range allEllipticCurveAlgorithm {
list = append(list, v)
}
- muAllEllipticCurveAlgorithm.RUnlock()
sort.Slice(list, func(i, j int) bool {
return list[i].String() < list[j].String()
})
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go
index 29ac1dfe76..a363b80e41 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/jwa.go
@@ -6,8 +6,12 @@ package jwa
import (
"errors"
"fmt"
+ "sort"
+ "sync"
)
+const maxKeyAlgorithmErrorPreview = 64
+
// KeyAlgorithm is a workaround for jwk.Key being able to contain different
// types of algorithms in its `alg` field.
//
@@ -30,38 +34,189 @@ func ErrInvalidKeyAlgorithm() error {
return errInvalidKeyAlgorithm
}
+func formatInvalidKeyAlgorithmValue(v string) string {
+ runes := []rune(v)
+ if len(runes) <= maxKeyAlgorithmErrorPreview {
+ return fmt.Sprintf("%q", v)
+ }
+
+ return fmt.Sprintf("%q", string(runes[:maxKeyAlgorithmErrorPreview])+`...`)
+}
+
+// algorithmKind tags entries in the shared algRegistry so the
+// per-kind public Register/Lookup/Unregister/s functions can
+// dispatch through one map without losing the typed identity of each
+// algorithm.
+type algorithmKind uint8
+
+const (
+ algKindUnknown algorithmKind = iota
+ algKindSignature
+ algKindKeyEncryption
+ algKindContentEncryption
+)
+
+func (k algorithmKind) String() string {
+ switch k {
+ case algKindSignature:
+ return "SignatureAlgorithm"
+ case algKindKeyEncryption:
+ return "KeyEncryptionAlgorithm"
+ case algKindContentEncryption:
+ return "ContentEncryptionAlgorithm"
+ default:
+ return "unknown algorithm kind"
+ }
+}
+
+type algRegistryEntry struct {
+ kind algorithmKind
+ alg KeyAlgorithm
+ builtin bool
+}
+
+// algRegistry is the single shared namespace for the three
+// KeyAlgorithm-implementing kinds. Independent per-kind maps would
+// let an extension register the same name as both (say) a
+// SignatureAlgorithm and a KeyEncryptionAlgorithm, after which
+// KeyAlgorithmFrom("X") would resolve to whichever kind was tried
+// first — silently flipping with import order. Funnelling all three
+// through one map fixes that ambiguity at registration time.
+var (
+ muAlgRegistry sync.RWMutex
+ algRegistry = map[string]algRegistryEntry{}
+)
+
+// registerAlgorithm is the shared backend for the three public
+// Register{Signature,KeyEncryption,ContentEncryption}Algorithm
+// functions.
+//
+// Behavior:
+// - Re-registering the exact same value (same kind, same alg) is a
+// no-op.
+// - Cross-kind name reuse is a silent no-op: the first registration
+// wins and the second Register* call has no effect. v3's
+// pre-existing Register* signature returns no error and v3 does
+// not change observable error/panic behavior, so the cross-kind
+// case is silently skipped — KeyAlgorithmFrom now resolves
+// unambiguously to the first-registered kind. (v4 escalates this
+// to a returned error from Register*.)
+// - Built-in replacement is a silent no-op, preserving the
+// pre-unification per-kind v3 Register* behavior.
+// - Same-kind, non-builtin re-registration with a different value
+// silently overwrites. This preserves the pre-unification
+// behavior of the per-kind Register* functions.
+func registerAlgorithm(kind algorithmKind, alg KeyAlgorithm) {
+ name := alg.String()
+ muAlgRegistry.Lock()
+ defer muAlgRegistry.Unlock()
+ if existing, ok := algRegistry[name]; ok {
+ if existing.kind == kind && existing.alg == alg {
+ return
+ }
+ if existing.kind != kind {
+ return
+ }
+ if existing.builtin {
+ return
+ }
+ }
+ algRegistry[name] = algRegistryEntry{kind: kind, alg: alg}
+}
+
+// markBuiltin flips the builtin flag on an already-registered name.
+// Called by the per-kind generated init() after the bulk Register*
+// pass, preserving the existing two-phase init pattern.
+func markBuiltin(name string) {
+ muAlgRegistry.Lock()
+ defer muAlgRegistry.Unlock()
+ if entry, ok := algRegistry[name]; ok {
+ entry.builtin = true
+ algRegistry[name] = entry
+ }
+}
+
+// unregisterAlgorithm is the shared backend for the three public
+// Unregister*Algorithm functions. No-op for built-ins, no-op for a
+// kind mismatch, no-op for unknown names — same surface contract as
+// the pre-unification per-kind Unregister*.
+func unregisterAlgorithm(kind algorithmKind, name string) {
+ muAlgRegistry.Lock()
+ defer muAlgRegistry.Unlock()
+ if entry, ok := algRegistry[name]; ok && entry.kind == kind && !entry.builtin {
+ delete(algRegistry, name)
+ }
+}
+
+// lookupAlgorithm returns the registered KeyAlgorithm for name iff it
+// is registered as the requested kind. Used by the per-kind
+// generated Lookup* wrappers.
+func lookupAlgorithm(kind algorithmKind, name string) (KeyAlgorithm, bool) {
+ muAlgRegistry.RLock()
+ defer muAlgRegistry.RUnlock()
+ if entry, ok := algRegistry[name]; ok && entry.kind == kind {
+ return entry.alg, true
+ }
+ return nil, false
+}
+
+// listAlgorithmsByKind returns every registered algorithm of the
+// given kind, sorted by name. Used by the per-kind generated
+// s() functions.
+func listAlgorithmsByKind(kind algorithmKind) []KeyAlgorithm {
+ muAlgRegistry.RLock()
+ defer muAlgRegistry.RUnlock()
+ out := make([]KeyAlgorithm, 0, len(algRegistry))
+ for _, entry := range algRegistry {
+ if entry.kind == kind {
+ out = append(out, entry.alg)
+ }
+ }
+ sort.Slice(out, func(i, j int) bool { return out[i].String() < out[j].String() })
+ return out
+}
+
// KeyAlgorithmFrom takes either a string, `jwa.SignatureAlgorithm`,
-// `jwa.KeyEncryptionAlgorithm`, or `jwa.ContentEncryptionAlgorithm`.
+// `jwa.KeyEncryptionAlgorithm`, or `jwa.ContentEncryptionAlgorithm`,
// and returns a `jwa.KeyAlgorithm`.
//
-// If the value cannot be handled, it returns an `jwa.InvalidKeyAlgorithm`
-// object instead of returning an error. This design choice was made to allow
-// users to directly pass the return value to functions such as `jws.Sign()`
+// String inputs resolve through the shared algorithm registry: the
+// returned KeyAlgorithm holds the concrete typed value (Signature,
+// KeyEncryption, or ContentEncryption) for whichever kind owns the
+// name. Cross-kind name reuse is structurally avoided — the first
+// Register* wins and subsequent cross-kind registrations are silent
+// no-ops — so KeyAlgorithmFrom no longer needs precedence rules.
+//
+// Typed inputs whose String() is empty (for example a zero-value
+// `var sa jwa.SignatureAlgorithm`) are rejected with
+// ErrInvalidKeyAlgorithm. Without this check the typed arms accepted
+// names that would never resolve through any registry, surfacing as
+// confusing failures far from the call site.
func KeyAlgorithmFrom(v any) (KeyAlgorithm, error) {
switch v := v.(type) {
case SignatureAlgorithm:
+ if v.String() == "" {
+ return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm)
+ }
return v, nil
case KeyEncryptionAlgorithm:
+ if v.String() == "" {
+ return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm)
+ }
return v, nil
case ContentEncryptionAlgorithm:
+ if v.String() == "" {
+ return nil, fmt.Errorf(`invalid key value: zero-value %T: %w`, v, errInvalidKeyAlgorithm)
+ }
return v, nil
case string:
- salg, ok := LookupSignatureAlgorithm(v)
- if ok {
- return salg, nil
+ muAlgRegistry.RLock()
+ entry, ok := algRegistry[v]
+ muAlgRegistry.RUnlock()
+ if !ok {
+ return nil, fmt.Errorf(`invalid key value: %s: %w`, formatInvalidKeyAlgorithmValue(v), errInvalidKeyAlgorithm)
}
-
- kalg, ok := LookupKeyEncryptionAlgorithm(v)
- if ok {
- return kalg, nil
- }
-
- calg, ok := LookupContentEncryptionAlgorithm(v)
- if ok {
- return calg, nil
- }
-
- return nil, fmt.Errorf(`invalid key value: %q: %w`, v, errInvalidKeyAlgorithm)
+ return entry.alg, nil
default:
return nil, fmt.Errorf(`invalid key type: %T: %w`, v, errInvalidKeyAlgorithm)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go
index 716c43cd04..bdb19b0dd3 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_encryption_gen.go
@@ -5,18 +5,10 @@ package jwa
import (
"encoding/json"
"fmt"
- "sort"
- "sync"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
-var muAllKeyEncryptionAlgorithm sync.RWMutex
-var allKeyEncryptionAlgorithm = map[string]KeyEncryptionAlgorithm{}
-var muListKeyEncryptionAlgorithm sync.RWMutex
-var listKeyEncryptionAlgorithm []KeyEncryptionAlgorithm
-var builtinKeyEncryptionAlgorithm = map[string]struct{}{}
-
func init() {
// builtin values for KeyEncryptionAlgorithm
algorithms := make([]KeyEncryptionAlgorithm, 19)
@@ -41,6 +33,9 @@ func init() {
algorithms[18] = NewKeyEncryptionAlgorithm(tokens.RSA_OAEP_512)
RegisterKeyEncryptionAlgorithm(algorithms...)
+ for _, alg := range algorithms {
+ markBuiltin(alg.String())
+ }
}
// A128GCMKW returns an object representing AES-GCM key wrap (128) key encryption algorithm.
@@ -139,13 +134,11 @@ func RSA_OAEP_512() KeyEncryptionAlgorithm {
}
func lookupBuiltinKeyEncryptionAlgorithm(name string) KeyEncryptionAlgorithm {
- muAllKeyEncryptionAlgorithm.RLock()
- v, ok := allKeyEncryptionAlgorithm[name]
- muAllKeyEncryptionAlgorithm.RUnlock()
+ v, ok := lookupAlgorithm(algKindKeyEncryption, name)
if !ok {
panic(fmt.Sprintf(`jwa: KeyEncryptionAlgorithm %q not registered`, name))
}
- return v
+ return v.(KeyEncryptionAlgorithm)
}
// KeyEncryptionAlgorithm represents the various encryption algorithms as described in https://tools.ietf.org/html/rfc7518#section-4.1
@@ -195,57 +188,46 @@ func NewKeyEncryptionAlgorithm(name string, options ...NewKeyEncryptionAlgorithm
// LookupKeyEncryptionAlgorithm returns the KeyEncryptionAlgorithm object for the given name.
func LookupKeyEncryptionAlgorithm(name string) (KeyEncryptionAlgorithm, bool) {
- muAllKeyEncryptionAlgorithm.RLock()
- v, ok := allKeyEncryptionAlgorithm[name]
- muAllKeyEncryptionAlgorithm.RUnlock()
- return v, ok
+ if v, ok := lookupAlgorithm(algKindKeyEncryption, name); ok {
+ return v.(KeyEncryptionAlgorithm), true
+ }
+ var zero KeyEncryptionAlgorithm
+ return zero, false
}
// RegisterKeyEncryptionAlgorithm registers a new KeyEncryptionAlgorithm. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
+//
+// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm
+// share a single algorithm-name namespace so that KeyAlgorithmFrom can
+// resolve unambiguously. Registering a name that is already registered as a
+// different kind is a silent no-op (the first Register* call wins).
func RegisterKeyEncryptionAlgorithm(algorithms ...KeyEncryptionAlgorithm) {
- muAllKeyEncryptionAlgorithm.Lock()
for _, alg := range algorithms {
- allKeyEncryptionAlgorithm[alg.String()] = alg
+ registerAlgorithm(algKindKeyEncryption, alg)
}
- muAllKeyEncryptionAlgorithm.Unlock()
- rebuildKeyEncryptionAlgorithm()
}
// UnregisterKeyEncryptionAlgorithm unregisters a KeyEncryptionAlgorithm from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterKeyEncryptionAlgorithm(algorithms ...KeyEncryptionAlgorithm) {
- muAllKeyEncryptionAlgorithm.Lock()
for _, alg := range algorithms {
- if _, ok := builtinKeyEncryptionAlgorithm[alg.String()]; ok {
- continue
- }
- delete(allKeyEncryptionAlgorithm, alg.String())
+ unregisterAlgorithm(algKindKeyEncryption, alg.String())
}
- muAllKeyEncryptionAlgorithm.Unlock()
- rebuildKeyEncryptionAlgorithm()
-}
-
-func rebuildKeyEncryptionAlgorithm() {
- list := make([]KeyEncryptionAlgorithm, 0, len(allKeyEncryptionAlgorithm))
- muAllKeyEncryptionAlgorithm.RLock()
- for _, v := range allKeyEncryptionAlgorithm {
- list = append(list, v)
- }
- muAllKeyEncryptionAlgorithm.RUnlock()
- sort.Slice(list, func(i, j int) bool {
- return list[i].String() < list[j].String()
- })
- muListKeyEncryptionAlgorithm.Lock()
- listKeyEncryptionAlgorithm = list
- muListKeyEncryptionAlgorithm.Unlock()
}
// KeyEncryptionAlgorithms returns a list of all available values for KeyEncryptionAlgorithm.
func KeyEncryptionAlgorithms() []KeyEncryptionAlgorithm {
- muListKeyEncryptionAlgorithm.RLock()
- defer muListKeyEncryptionAlgorithm.RUnlock()
- return listKeyEncryptionAlgorithm
+ raw := listAlgorithmsByKind(algKindKeyEncryption)
+ out := make([]KeyEncryptionAlgorithm, len(raw))
+ for i, alg := range raw {
+ out[i] = alg.(KeyEncryptionAlgorithm)
+ }
+ return out
}
// MarshalJSON serializes the KeyEncryptionAlgorithm object to a JSON string.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go
index 8bc5ebb5f0..db3580a277 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/key_type_gen.go
@@ -24,6 +24,9 @@ func init() {
algorithms[3] = NewKeyType("RSA")
RegisterKeyType(algorithms...)
+ for _, alg := range algorithms {
+ builtinKeyType[alg.String()] = struct{}{}
+ }
}
// EC returns an object representing EC. Elliptic Curve
@@ -107,36 +110,44 @@ func LookupKeyType(name string) (KeyType, bool) {
// RegisterKeyType registers a new KeyType. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
func RegisterKeyType(algorithms ...KeyType) {
muAllKeyType.Lock()
+ defer muAllKeyType.Unlock()
for _, alg := range algorithms {
+ if _, ok := builtinKeyType[alg.String()]; ok {
+ if existing, ok := allKeyType[alg.String()]; ok && existing != alg {
+ continue
+ }
+ continue
+ }
allKeyType[alg.String()] = alg
}
- muAllKeyType.Unlock()
- rebuildKeyType()
+ rebuildKeyTypeLocked()
}
// UnregisterKeyType unregisters a KeyType from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterKeyType(algorithms ...KeyType) {
muAllKeyType.Lock()
+ defer muAllKeyType.Unlock()
for _, alg := range algorithms {
if _, ok := builtinKeyType[alg.String()]; ok {
continue
}
delete(allKeyType, alg.String())
}
- muAllKeyType.Unlock()
- rebuildKeyType()
+ rebuildKeyTypeLocked()
}
-func rebuildKeyType() {
+func rebuildKeyTypeLocked() {
list := make([]KeyType, 0, len(allKeyType))
- muAllKeyType.RLock()
for _, v := range allKeyType {
list = append(list, v)
}
- muAllKeyType.RUnlock()
sort.Slice(list, func(i, j int) bool {
return list[i].String() < list[j].String()
})
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go
index 653d7d56af..c682b8dd65 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwa/signature_gen.go
@@ -5,36 +5,32 @@ package jwa
import (
"encoding/json"
"fmt"
- "sort"
- "sync"
)
-var muAllSignatureAlgorithm sync.RWMutex
-var allSignatureAlgorithm = map[string]SignatureAlgorithm{}
-var muListSignatureAlgorithm sync.RWMutex
-var listSignatureAlgorithm []SignatureAlgorithm
-var builtinSignatureAlgorithm = map[string]struct{}{}
-
func init() {
// builtin values for SignatureAlgorithm
- algorithms := make([]SignatureAlgorithm, 15)
+ algorithms := make([]SignatureAlgorithm, 16)
algorithms[0] = NewSignatureAlgorithm("ES256")
algorithms[1] = NewSignatureAlgorithm("ES256K")
algorithms[2] = NewSignatureAlgorithm("ES384")
algorithms[3] = NewSignatureAlgorithm("ES512")
- algorithms[4] = NewSignatureAlgorithm("EdDSA")
- algorithms[5] = NewSignatureAlgorithm("HS256", WithIsSymmetric(true))
- algorithms[6] = NewSignatureAlgorithm("HS384", WithIsSymmetric(true))
- algorithms[7] = NewSignatureAlgorithm("HS512", WithIsSymmetric(true))
- algorithms[8] = NewSignatureAlgorithm("none")
- algorithms[9] = NewSignatureAlgorithm("PS256")
- algorithms[10] = NewSignatureAlgorithm("PS384")
- algorithms[11] = NewSignatureAlgorithm("PS512")
- algorithms[12] = NewSignatureAlgorithm("RS256")
- algorithms[13] = NewSignatureAlgorithm("RS384")
- algorithms[14] = NewSignatureAlgorithm("RS512")
+ algorithms[4] = NewSignatureAlgorithm("EdDSA", WithDeprecated(true))
+ algorithms[5] = NewSignatureAlgorithm("Ed25519")
+ algorithms[6] = NewSignatureAlgorithm("HS256", WithIsSymmetric(true))
+ algorithms[7] = NewSignatureAlgorithm("HS384", WithIsSymmetric(true))
+ algorithms[8] = NewSignatureAlgorithm("HS512", WithIsSymmetric(true))
+ algorithms[9] = NewSignatureAlgorithm("none")
+ algorithms[10] = NewSignatureAlgorithm("PS256")
+ algorithms[11] = NewSignatureAlgorithm("PS384")
+ algorithms[12] = NewSignatureAlgorithm("PS512")
+ algorithms[13] = NewSignatureAlgorithm("RS256")
+ algorithms[14] = NewSignatureAlgorithm("RS384")
+ algorithms[15] = NewSignatureAlgorithm("RS512")
RegisterSignatureAlgorithm(algorithms...)
+ for _, alg := range algorithms {
+ markBuiltin(alg.String())
+ }
}
// ES256 returns an object representing ECDSA signature algorithm using P-256 curve and SHA-256.
@@ -57,11 +53,16 @@ func ES512() SignatureAlgorithm {
return lookupBuiltinSignatureAlgorithm("ES512")
}
-// EdDSA returns an object representing EdDSA signature algorithms.
+// EdDSA returns an object representing EdDSA signature algorithms (deprecated by RFC 9864, use EdDSAEd25519 or EdDSAEd448).
func EdDSA() SignatureAlgorithm {
return lookupBuiltinSignatureAlgorithm("EdDSA")
}
+// EdDSAEd25519 returns an object representing EdDSA signature algorithm using Ed25519 (RFC 9864). The function name is tentative and may change in future releases.
+func EdDSAEd25519() SignatureAlgorithm {
+ return lookupBuiltinSignatureAlgorithm("Ed25519")
+}
+
// HS256 returns an object representing HMAC signature algorithm using SHA-256.
func HS256() SignatureAlgorithm {
return lookupBuiltinSignatureAlgorithm("HS256")
@@ -113,13 +114,11 @@ func RS512() SignatureAlgorithm {
}
func lookupBuiltinSignatureAlgorithm(name string) SignatureAlgorithm {
- muAllSignatureAlgorithm.RLock()
- v, ok := allSignatureAlgorithm[name]
- muAllSignatureAlgorithm.RUnlock()
+ v, ok := lookupAlgorithm(algKindSignature, name)
if !ok {
panic(fmt.Sprintf(`jwa: SignatureAlgorithm %q not registered`, name))
}
- return v
+ return v.(SignatureAlgorithm)
}
// SignatureAlgorithm represents the various signature algorithms as described in https://tools.ietf.org/html/rfc7518#section-3.1
@@ -169,57 +168,46 @@ func NewSignatureAlgorithm(name string, options ...NewSignatureAlgorithmOption)
// LookupSignatureAlgorithm returns the SignatureAlgorithm object for the given name.
func LookupSignatureAlgorithm(name string) (SignatureAlgorithm, bool) {
- muAllSignatureAlgorithm.RLock()
- v, ok := allSignatureAlgorithm[name]
- muAllSignatureAlgorithm.RUnlock()
- return v, ok
+ if v, ok := lookupAlgorithm(algKindSignature, name); ok {
+ return v.(SignatureAlgorithm), true
+ }
+ var zero SignatureAlgorithm
+ return zero, false
}
// RegisterSignatureAlgorithm registers a new SignatureAlgorithm. The signature value must be immutable
// and safe to be used by multiple goroutines, as it is going to be shared with all other users of this library.
+//
+// Registration is process-global. Built-in identifiers such as RS256 are
+// reserved and cannot be replaced by callers after init has completed; use a
+// distinct name for third-party algorithms.
+//
+// SignatureAlgorithm, KeyEncryptionAlgorithm, and ContentEncryptionAlgorithm
+// share a single algorithm-name namespace so that KeyAlgorithmFrom can
+// resolve unambiguously. Registering a name that is already registered as a
+// different kind is a silent no-op (the first Register* call wins).
func RegisterSignatureAlgorithm(algorithms ...SignatureAlgorithm) {
- muAllSignatureAlgorithm.Lock()
for _, alg := range algorithms {
- allSignatureAlgorithm[alg.String()] = alg
+ registerAlgorithm(algKindSignature, alg)
}
- muAllSignatureAlgorithm.Unlock()
- rebuildSignatureAlgorithm()
}
// UnregisterSignatureAlgorithm unregisters a SignatureAlgorithm from its known database.
// Non-existent entries, as well as built-in algorithms will silently be ignored.
func UnregisterSignatureAlgorithm(algorithms ...SignatureAlgorithm) {
- muAllSignatureAlgorithm.Lock()
for _, alg := range algorithms {
- if _, ok := builtinSignatureAlgorithm[alg.String()]; ok {
- continue
- }
- delete(allSignatureAlgorithm, alg.String())
+ unregisterAlgorithm(algKindSignature, alg.String())
}
- muAllSignatureAlgorithm.Unlock()
- rebuildSignatureAlgorithm()
-}
-
-func rebuildSignatureAlgorithm() {
- list := make([]SignatureAlgorithm, 0, len(allSignatureAlgorithm))
- muAllSignatureAlgorithm.RLock()
- for _, v := range allSignatureAlgorithm {
- list = append(list, v)
- }
- muAllSignatureAlgorithm.RUnlock()
- sort.Slice(list, func(i, j int) bool {
- return list[i].String() < list[j].String()
- })
- muListSignatureAlgorithm.Lock()
- listSignatureAlgorithm = list
- muListSignatureAlgorithm.Unlock()
}
// SignatureAlgorithms returns a list of all available values for SignatureAlgorithm.
func SignatureAlgorithms() []SignatureAlgorithm {
- muListSignatureAlgorithm.RLock()
- defer muListSignatureAlgorithm.RUnlock()
- return listSignatureAlgorithm
+ raw := listAlgorithmsByKind(algKindSignature)
+ out := make([]SignatureAlgorithm, len(raw))
+ for i, alg := range raw {
+ out[i] = alg.(SignatureAlgorithm)
+ }
+ return out
}
// MarshalJSON serializes the SignatureAlgorithm object to a JSON string.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md b/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md
index c85d05bbbe..982e7454b2 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/README.md
@@ -12,38 +12,38 @@ Examples are located in the examples directory ([jwe_example_test.go](../example
Supported key encryption algorithm:
-| Algorithm | Supported? | Constant in [jwa](../jwa) |
-|:-----------------------------------------|:-----------|:-------------------------|
-| RSA-PKCS1v1.5 | YES | jwa.RSA1_5 |
-| RSA-OAEP-SHA1 | YES | jwa.RSA_OAEP |
-| RSA-OAEP-SHA256 | YES | jwa.RSA_OAEP_256 |
-| AES key wrap (128) | YES | jwa.A128KW |
-| AES key wrap (192) | YES | jwa.A192KW |
-| AES key wrap (256) | YES | jwa.A256KW |
-| Direct encryption | YES (1) | jwa.DIRECT |
-| ECDH-ES | YES (1) | jwa.ECDH_ES |
-| ECDH-ES + AES key wrap (128) | YES | jwa.ECDH_ES_A128KW |
-| ECDH-ES + AES key wrap (192) | YES | jwa.ECDH_ES_A192KW |
-| ECDH-ES + AES key wrap (256) | YES | jwa.ECDH_ES_A256KW |
-| AES-GCM key wrap (128) | YES | jwa.A128GCMKW |
-| AES-GCM key wrap (192) | YES | jwa.A192GCMKW |
-| AES-GCM key wrap (256) | YES | jwa.A256GCMKW |
-| PBES2 + HMAC-SHA256 + AES key wrap (128) | YES | jwa.PBES2_HS256_A128KW |
-| PBES2 + HMAC-SHA384 + AES key wrap (192) | YES | jwa.PBES2_HS384_A192KW |
-| PBES2 + HMAC-SHA512 + AES key wrap (256) | YES | jwa.PBES2_HS512_A256KW |
+| Algorithm | Supported? | Constant in [jwa](../jwa) | Note |
+|:-----------------------------------------|:-----------|:-------------------------|:-----|
+| RSA-PKCS1v1.5 | YES | jwa.RSA1_5 | Legacy interop only; prefer RSA-OAEP for new code |
+| RSA-OAEP-SHA1 | YES | jwa.RSA_OAEP | |
+| RSA-OAEP-SHA256 | YES | jwa.RSA_OAEP_256 | |
+| AES key wrap (128) | YES | jwa.A128KW | |
+| AES key wrap (192) | YES | jwa.A192KW | |
+| AES key wrap (256) | YES | jwa.A256KW | |
+| Direct encryption | YES (1) | jwa.DIRECT | |
+| ECDH-ES | YES (1) | jwa.ECDH_ES | |
+| ECDH-ES + AES key wrap (128) | YES | jwa.ECDH_ES_A128KW | |
+| ECDH-ES + AES key wrap (192) | YES | jwa.ECDH_ES_A192KW | |
+| ECDH-ES + AES key wrap (256) | YES | jwa.ECDH_ES_A256KW | |
+| AES-GCM key wrap (128) | YES | jwa.A128GCMKW | |
+| AES-GCM key wrap (192) | YES | jwa.A192GCMKW | |
+| AES-GCM key wrap (256) | YES | jwa.A256GCMKW | |
+| PBES2 + HMAC-SHA256 + AES key wrap (128) | YES | jwa.PBES2_HS256_A128KW | |
+| PBES2 + HMAC-SHA384 + AES key wrap (192) | YES | jwa.PBES2_HS384_A192KW | |
+| PBES2 + HMAC-SHA512 + AES key wrap (256) | YES | jwa.PBES2_HS512_A256KW | |
* Note 1: Single-recipient only
Supported content encryption algorithm:
-| Algorithm | Supported? | Constant in [jwa](../jwa) |
-|:----------------------------|:-----------|:--------------------------|
-| AES-CBC + HMAC-SHA256 (128) | YES | jwa.A128CBC_HS256 |
-| AES-CBC + HMAC-SHA384 (192) | YES | jwa.A192CBC_HS384 |
-| AES-CBC + HMAC-SHA512 (256) | YES | jwa.A256CBC_HS512 |
-| AES-GCM (128) | YES | jwa.A128GCM |
-| AES-GCM (192) | YES | jwa.A192GCM |
-| AES-GCM (256) | YES | jwa.A256GCM |
+| Algorithm | Supported? | Constant in [jwa](../jwa) | Required CEK length |
+|:----------------------------|:-----------|:--------------------------|:--------------------|
+| AES-CBC + HMAC-SHA256 (128) | YES | jwa.A128CBC_HS256 | 32 bytes |
+| AES-CBC + HMAC-SHA384 (192) | YES | jwa.A192CBC_HS384 | 48 bytes |
+| AES-CBC + HMAC-SHA512 (256) | YES | jwa.A256CBC_HS512 | 64 bytes |
+| AES-GCM (128) | YES | jwa.A128GCM | 16 bytes |
+| AES-GCM (192) | YES | jwa.A192GCM | 24 bytes |
+| AES-GCM (256) | YES | jwa.A256GCM | 32 bytes |
# SYNOPSIS
@@ -59,7 +59,7 @@ func ExampleEncrypt() {
payload := []byte("Lorem Ipsum")
- encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA1_5, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
+ encrypted, err := jwe.Encrypt(payload, jwe.WithKey(jwa.RSA_OAEP, &privkey.PublicKey), jwe.WithContentEncryption(jwa.A128CBC_HS256))
if err != nil {
log.Printf("failed to encrypt payload: %s", err)
return
@@ -79,7 +79,7 @@ func ExampleDecrypt() {
return
}
- decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA1_5, privkey))
+ decrypted, err := jwe.Decrypt(encrypted, jwe.WithKey(jwa.RSA_OAEP, privkey))
if err != nil {
log.Printf("failed to decrypt: %s", err)
return
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go
index a1ed158fb0..2476046a91 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/compress.go
@@ -19,7 +19,7 @@ func uncompress(src []byte, maxBufferSize int64) ([]byte, error) {
n, readErr := r.Read(buf[:])
sofar += int64(n)
if sofar > maxBufferSize {
- return nil, fmt.Errorf(`compressed payload exceeds maximum allowed size`)
+ return nil, fmt.Errorf(`decompressed payload exceeds WithMaxDecompressBufferSize=%d (saw %d bytes after a %d-byte read)`, maxBufferSize, sofar, n)
}
if readErr != nil {
// if we have a read error, and it's not EOF, then we need to stop
@@ -56,7 +56,6 @@ func compress(plaintext []byte) ([]byte, error) {
return nil, fmt.Errorf(`failed to close compression writer: %w`, err)
}
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go
index 9429d84b1b..bc6e6f705c 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/decrypt.go
@@ -3,7 +3,6 @@ package jwe
import (
"fmt"
- "github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt"
"github.com/lestrrat-go/jwx/v3/jwe/jwebb"
@@ -140,10 +139,11 @@ func (d *decrypter) Decrypt(recipient Recipient, ciphertext []byte, msg *Message
return
}
- computedAad := d.computedAad
- if d.aad != nil {
- computedAad = append(append(computedAad, tokens.Period), d.aad...)
- }
+ // When an external aad is present we must NOT append into
+ // d.computedAad's backing array: it aliases msg.rawProtectedHeaders
+ // in the caller, and appending would mutate bytes past its length
+ // in storage still referenced by the Message.
+ computedAad := concatAAD(d.computedAad, d.aad)
plaintext, err = cipher.Decrypt(cek, d.iv, ciphertext, d.tag, computedAad)
if err != nil {
@@ -158,40 +158,43 @@ func (d *decrypter) Decrypt(recipient Recipient, ciphertext []byte, msg *Message
}
func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, err error) {
+ keyalgStr := d.keyalg.String()
+ ctalgStr := d.ctalg.String()
+
recipientKey := recipient.EncryptedKey()
if kd, ok := d.privkey.(KeyDecrypter); ok {
return kd.DecryptKey(d.keyalg, recipientKey, recipient, msg)
}
- if jwebb.IsDirect(d.keyalg.String()) {
+ if jwebb.IsDirect(keyalgStr) {
cek, ok := d.privkey.([]byte)
if !ok {
- return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", d.keyalg, d.privkey)
+ return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", keyalgStr, d.privkey)
}
- return jwebb.KeyDecryptDirect(recipientKey, recipientKey, d.keyalg.String(), cek)
+ return jwebb.KeyDecryptDirect(recipientKey, recipientKey, keyalgStr, cek)
}
- if jwebb.IsPBES2(d.keyalg.String()) {
+ if jwebb.IsPBES2(keyalgStr) {
password, ok := d.privkey.([]byte)
if !ok {
- return nil, fmt.Errorf("decrypt key: []byte is required as the password for %s (got %T)", d.keyalg, d.privkey)
+ return nil, fmt.Errorf("decrypt key: []byte is required as the password for %s (got %T)", keyalgStr, d.privkey)
}
- salt := []byte(d.keyalg.String())
+ salt := []byte(keyalgStr)
salt = append(salt, byte(0))
salt = append(salt, d.keysalt...)
- return jwebb.KeyDecryptPBES2(recipientKey, recipientKey, d.keyalg.String(), password, salt, d.keycount)
+ return jwebb.KeyDecryptPBES2(recipientKey, recipientKey, keyalgStr, password, salt, d.keycount)
}
- if jwebb.IsAESGCMKW(d.keyalg.String()) {
+ if jwebb.IsAESGCMKW(keyalgStr) {
sharedkey, ok := d.privkey.([]byte)
if !ok {
- return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", d.keyalg, d.privkey)
+ return nil, fmt.Errorf("decrypt key: []byte is required as the key for %s (got %T)", keyalgStr, d.privkey)
}
- return jwebb.KeyDecryptAESGCMKW(recipientKey, recipientKey, d.keyalg.String(), sharedkey, d.keyiv, d.keytag)
+ return jwebb.KeyDecryptAESGCMKW(recipientKey, recipientKey, keyalgStr, sharedkey, d.keyiv, d.keytag)
}
- if jwebb.IsECDHES(d.keyalg.String()) {
- alg, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(d.keyalg.String(), d.ctalg.String())
+ if jwebb.IsECDHES(keyalgStr) {
+ alg, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(keyalgStr, ctalgStr)
if err != nil {
return nil, fmt.Errorf(`failed to determine ECDH-ES key size: %w`, err)
}
@@ -199,10 +202,10 @@ func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, e
if !keywrap {
return jwebb.KeyDecryptECDHES(recipientKey, cek, alg, d.apu, d.apv, d.privkey, d.pubkey, keysize)
}
- return jwebb.KeyDecryptECDHESKeyWrap(recipientKey, recipientKey, d.keyalg.String(), d.apu, d.apv, d.privkey, d.pubkey, keysize)
+ return jwebb.KeyDecryptECDHESKeyWrap(recipientKey, recipientKey, keyalgStr, d.apu, d.apv, d.privkey, d.pubkey, keysize)
}
- if jwebb.IsRSA15(d.keyalg.String()) {
+ if jwebb.IsRSA15(keyalgStr) {
cipher, err := d.ContentCipher()
if err != nil {
return nil, fmt.Errorf(`failed to fetch content crypt cipher: %w`, err)
@@ -211,17 +214,17 @@ func (d *decrypter) DecryptKey(recipient Recipient, msg *Message) (cek []byte, e
return jwebb.KeyDecryptRSA15(recipientKey, recipientKey, d.privkey, keysize)
}
- if jwebb.IsRSAOAEP(d.keyalg.String()) {
- return jwebb.KeyDecryptRSAOAEP(recipientKey, recipientKey, d.keyalg.String(), d.privkey)
+ if jwebb.IsRSAOAEP(keyalgStr) {
+ return jwebb.KeyDecryptRSAOAEP(recipientKey, recipientKey, keyalgStr, d.privkey)
}
- if jwebb.IsAESKW(d.keyalg.String()) {
+ if jwebb.IsAESKW(keyalgStr) {
sharedkey, ok := d.privkey.([]byte)
if !ok {
- return nil, fmt.Errorf("[]byte is required as the key to decrypt %s", d.keyalg.String())
+ return nil, fmt.Errorf("[]byte is required as the key to decrypt %s", keyalgStr)
}
- return jwebb.KeyDecryptAESKW(recipientKey, recipientKey, d.keyalg.String(), sharedkey)
+ return jwebb.KeyDecryptAESKW(recipientKey, recipientKey, keyalgStr, sharedkey)
}
- return nil, fmt.Errorf(`unsupported algorithm for key decryption (%s)`, d.keyalg)
+ return nil, fmt.Errorf(`unsupported algorithm for key decryption (%s)`, keyalgStr)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go
index e75f342a3d..16de2aa898 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/encrypt.go
@@ -8,9 +8,9 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
"github.com/lestrrat-go/jwx/v3/jwa"
- "github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt"
"github.com/lestrrat-go/jwx/v3/jwe/internal/keygen"
"github.com/lestrrat-go/jwx/v3/jwe/jwebb"
+ "github.com/lestrrat-go/jwx/v3/jwk"
)
// encrypter is responsible for taking various components to encrypt a key.
@@ -18,13 +18,13 @@ import (
//
//nolint:govet
type encrypter struct {
- apu []byte
- apv []byte
- ctalg jwa.ContentEncryptionAlgorithm
- keyalg jwa.KeyEncryptionAlgorithm
- pubkey any
- rawKey any
- cipher content_crypt.Cipher
+ apu []byte
+ apv []byte
+ ctalg jwa.ContentEncryptionAlgorithm
+ keyalg jwa.KeyEncryptionAlgorithm
+ pubkey any
+ rawKey any
+ pbes2Count int
}
// newEncrypter creates a new Encrypter instance with all required parameters.
@@ -34,24 +34,22 @@ type encrypter struct {
// *rsa.PublicKey, instead of jwk.Key)
//
// You should consider this object immutable once created.
-func newEncrypter(keyalg jwa.KeyEncryptionAlgorithm, ctalg jwa.ContentEncryptionAlgorithm, pubkey any, rawKey any, apu, apv []byte) (*encrypter, error) {
- cipher, err := jwebb.CreateContentCipher(ctalg.String())
- if err != nil {
- return nil, fmt.Errorf(`failed to create content cipher: %w`, err)
- }
-
+func newEncrypter(keyalg jwa.KeyEncryptionAlgorithm, ctalg jwa.ContentEncryptionAlgorithm, pubkey any, rawKey any, apu, apv []byte, pbes2Count int) *encrypter {
return &encrypter{
- apu: apu,
- apv: apv,
- ctalg: ctalg,
- keyalg: keyalg,
- pubkey: pubkey,
- rawKey: rawKey,
- cipher: cipher,
- }, nil
+ apu: apu,
+ apv: apv,
+ ctalg: ctalg,
+ keyalg: keyalg,
+ pubkey: pubkey,
+ rawKey: rawKey,
+ pbes2Count: pbes2Count,
+ }
}
func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
+ keyalgStr := e.keyalg.String()
+ ctalgStr := e.ctalg.String()
+
if ke, ok := e.pubkey.(KeyEncrypter); ok {
encrypted, err := ke.EncryptKey(cek)
if err != nil {
@@ -60,32 +58,32 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
return keygen.ByteKey(encrypted), nil
}
- if jwebb.IsDirect(e.keyalg.String()) {
+ if jwebb.IsDirect(keyalgStr) {
sharedkey, ok := e.rawKey.([]byte)
if !ok {
- return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey)
+ return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", keyalgStr, e.rawKey)
}
- return jwebb.KeyEncryptDirect(cek, e.keyalg.String(), sharedkey)
+ return jwebb.KeyEncryptDirect(cek, keyalgStr, sharedkey)
}
- if jwebb.IsPBES2(e.keyalg.String()) {
+ if jwebb.IsPBES2(keyalgStr) {
password, ok := e.rawKey.([]byte)
if !ok {
- return nil, fmt.Errorf("encrypt key: []byte is required as the password for %s (got %T)", e.keyalg, e.rawKey)
+ return nil, fmt.Errorf("encrypt key: []byte is required as the password for %s (got %T)", keyalgStr, e.rawKey)
}
- return jwebb.KeyEncryptPBES2(cek, e.keyalg.String(), password)
+ return jwebb.KeyEncryptPBES2(cek, keyalgStr, password, e.pbes2Count)
}
- if jwebb.IsAESGCMKW(e.keyalg.String()) {
+ if jwebb.IsAESGCMKW(keyalgStr) {
sharedkey, ok := e.rawKey.([]byte)
if !ok {
- return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey)
+ return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", keyalgStr, e.rawKey)
}
- return jwebb.KeyEncryptAESGCMKW(cek, e.keyalg.String(), sharedkey)
+ return jwebb.KeyEncryptAESGCMKW(cek, keyalgStr, sharedkey)
}
- if jwebb.IsECDHES(e.keyalg.String()) {
- _, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(e.keyalg.String(), e.ctalg.String())
+ if jwebb.IsECDHES(keyalgStr) {
+ _, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(keyalgStr, ctalgStr)
if err != nil {
return nil, fmt.Errorf(`failed to determine ECDH-ES key size: %w`, err)
}
@@ -120,9 +118,9 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
case *ecdh.PublicKey:
if key.Curve() == ecdh.X25519() {
if !keywrap {
- return jwebb.KeyEncryptECDHESX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
+ return jwebb.KeyEncryptECDHESX25519(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr)
}
- return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
+ return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr)
}
var ecdsaKey *ecdsa.PublicKey
@@ -135,15 +133,15 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
switch key := keyToUse.(type) {
case *ecdsa.PublicKey:
if !keywrap {
- return jwebb.KeyEncryptECDHESECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
+ return jwebb.KeyEncryptECDHESECDSA(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr)
}
- return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
+ return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, keyalgStr, e.apu, e.apv, key, keysize, ctalgStr)
default:
return nil, fmt.Errorf(`encrypt: unsupported key type for ECDH-ES: %T`, keyToUse)
}
}
- if jwebb.IsRSA15(e.keyalg.String()) {
+ if jwebb.IsRSA15(keyalgStr) {
keyToUse := e.rawKey
if keyToUse == nil {
keyToUse = e.pubkey
@@ -159,10 +157,10 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err)
}
- return jwebb.KeyEncryptRSA15(cek, e.keyalg.String(), pubkey)
+ return jwebb.KeyEncryptRSA15(cek, keyalgStr, pubkey)
}
- if jwebb.IsRSAOAEP(e.keyalg.String()) {
+ if jwebb.IsRSAOAEP(keyalgStr) {
keyToUse := e.rawKey
if keyToUse == nil {
keyToUse = e.pubkey
@@ -178,16 +176,76 @@ func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err)
}
- return jwebb.KeyEncryptRSAOAEP(cek, e.keyalg.String(), pubkey)
+ return jwebb.KeyEncryptRSAOAEP(cek, keyalgStr, pubkey)
}
- if jwebb.IsAESKW(e.keyalg.String()) {
+ if jwebb.IsAESKW(keyalgStr) {
sharedkey, ok := e.rawKey.([]byte)
if !ok {
- return nil, fmt.Errorf("[]byte is required as the key to encrypt %s", e.keyalg.String())
+ return nil, fmt.Errorf("[]byte is required as the key to encrypt %s", keyalgStr)
}
- return jwebb.KeyEncryptAESKW(cek, e.keyalg.String(), sharedkey)
+ return jwebb.KeyEncryptAESKW(cek, keyalgStr, sharedkey)
}
- return nil, fmt.Errorf(`unsupported algorithm for key encryption (%s)`, e.keyalg)
+ return nil, fmt.Errorf(`unsupported algorithm for key encryption (%s)`, keyalgStr)
+}
+
+// validateAlgorithmForKey checks that alg is family-compatible with
+// key at the WithKey option boundary, surfacing wrong-shape mismatches
+// as crisp `jwe.WithKey: ...` errors instead of nested errors deep in
+// the dispatcher (e.g. `[]byte is required as the key to encrypt ...`
+// from inside the AESKW path).
+//
+// Permissive carve-outs (return nil, deferring validation):
+//
+// - jwk.Key wrappers: kty-vs-alg check happens at jwk.Export time.
+// - Caller-supplied KeyEncrypter / KeyDecrypter implementations:
+// the caller takes responsibility for the key-shape contract.
+// - Nil key: legitimate for `dir` (caller provides CEK separately).
+//
+// All other built-in algorithm families enforce a concrete key-shape
+// expectation here. The error is wrapped by the WithKey site so the
+// caller sees `jwe.WithKey: ...` consistently.
+func validateAlgorithmForKey(alg jwa.KeyEncryptionAlgorithm, key any) error {
+ if key == nil {
+ return nil
+ }
+ if _, ok := key.(jwk.Key); ok {
+ return nil
+ }
+ if _, ok := key.(KeyEncrypter); ok {
+ return nil
+ }
+ if _, ok := key.(KeyDecrypter); ok {
+ return nil
+ }
+
+ algStr := alg.String()
+ switch {
+ case jwebb.IsDirect(algStr):
+ if _, ok := key.([]byte); !ok {
+ return fmt.Errorf(`algorithm %q requires a []byte key (got %T)`, algStr, key)
+ }
+ case jwebb.IsAESKW(algStr) || jwebb.IsAESGCMKW(algStr) || jwebb.IsPBES2(algStr):
+ if _, ok := key.([]byte); !ok {
+ return fmt.Errorf(`algorithm %q requires a []byte key (got %T)`, algStr, key)
+ }
+ case jwebb.IsRSA15(algStr) || jwebb.IsRSAOAEP(algStr):
+ switch key.(type) {
+ case *rsa.PublicKey, rsa.PublicKey, *rsa.PrivateKey, rsa.PrivateKey:
+ default:
+ return fmt.Errorf(`algorithm %q requires an RSA key (got %T)`, algStr, key)
+ }
+ case jwebb.IsECDHES(algStr):
+ switch key.(type) {
+ case *ecdsa.PublicKey, ecdsa.PublicKey, *ecdsa.PrivateKey, ecdsa.PrivateKey,
+ *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey:
+ default:
+ return fmt.Errorf(`algorithm %q requires an ECDSA or ECDH key (got %T)`, algStr, key)
+ }
+ default:
+ // Unknown algorithm family: defer to dispatch.
+ return nil
+ }
+ return nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go
index 89d276fc44..8f65daa4b1 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/errors.go
@@ -1,6 +1,9 @@
package jwe
-import "errors"
+import (
+ "errors"
+ "fmt"
+)
type encryptError struct {
error
@@ -22,6 +25,10 @@ func EncryptError() error {
return errDefaultEncryptError
}
+func makeEncryptError(prefix string, f string, args ...any) error {
+ return encryptError{fmt.Errorf(prefix+": "+f, args...)}
+}
+
type decryptError struct {
error
}
@@ -42,6 +49,10 @@ func DecryptError() error {
return errDefaultDecryptError
}
+func makeDecryptError(f string, args ...any) error {
+ return decryptError{fmt.Errorf("jwe.Decrypt: "+f, args...)}
+}
+
type recipientError struct {
error
}
@@ -68,6 +79,10 @@ func RecipientError() error {
return errDefaultRecipientError
}
+func makeRecipientError(err error) error {
+ return recipientError{err}
+}
+
type parseError struct {
error
}
@@ -88,3 +103,7 @@ var errDefaultParseError = parseError{errors.New(`parse error`)}
func ParseError() error {
return errDefaultParseError
}
+
+func makeParseError(prefix string, f string, args ...any) error {
+ return parseError{fmt.Errorf(prefix+": "+f, args...)}
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go
index 7773a5d812..5390c2be45 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/headers_gen.go
@@ -108,12 +108,11 @@ type stdHeaders struct {
x509CertThumbprintS256 *string
x509URL *string
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
}
func NewHeaders() Headers {
return &stdHeaders{
- mu: &sync.RWMutex{},
privateParams: map[string]any{},
}
}
@@ -430,13 +429,23 @@ func (h *stdHeaders) setNoLock(name string, value any) error {
switch name {
case AgreementPartyUInfoKey:
if v, ok := value.([]byte); ok {
- h.agreementPartyUInfo = v
+ if v == nil {
+ h.agreementPartyUInfo = nil
+ } else {
+ h.agreementPartyUInfo = make([]byte, len(v))
+ copy(h.agreementPartyUInfo, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, AgreementPartyUInfoKey, value)
case AgreementPartyVInfoKey:
if v, ok := value.([]byte); ok {
- h.agreementPartyVInfo = v
+ if v == nil {
+ h.agreementPartyVInfo = nil
+ } else {
+ h.agreementPartyVInfo = make([]byte, len(v))
+ copy(h.agreementPartyVInfo, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, AgreementPartyVInfoKey, value)
@@ -469,7 +478,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, ContentTypeKey, value)
case CriticalKey:
if v, ok := value.([]string); ok {
- h.critical = v
+ if v == nil {
+ h.critical = nil
+ } else {
+ h.critical = make([]string, len(v))
+ copy(h.critical, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, CriticalKey, value)
@@ -579,6 +593,8 @@ func (h *stdHeaders) Remove(key string) error {
}
func (h *stdHeaders) UnmarshalJSON(buf []byte) error {
+ h.mu.Lock()
+ defer h.mu.Unlock()
h.agreementPartyUInfo = nil
h.agreementPartyVInfo = nil
h.algorithm = nil
@@ -640,7 +656,7 @@ LOOP:
}
h.contentEncryption = &decoded
case ContentTypeKey:
- if err := json.AssignNextStringToken(&h.contentType, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.contentType, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, ContentTypeKey, err)
}
case CriticalKey:
@@ -670,15 +686,15 @@ LOOP:
}
h.jwk = key
case JWKSetURLKey:
- if err := json.AssignNextStringToken(&h.jwkSetURL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.jwkSetURL, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, JWKSetURLKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case TypeKey:
- if err := json.AssignNextStringToken(&h.typ, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.typ, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, TypeKey, err)
}
case X509CertChainKey:
@@ -688,15 +704,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -771,108 +787,190 @@ func (h *stdHeaders) Keys() []string {
return keys
}
-func (h stdHeaders) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- keys := make([]string, 0, 16+len(h.privateParams))
+type headerPair struct {
+ Name string
+ Value any
+}
+
+var headerPairPool = sync.Pool{
+ New: func() any {
+ return make([]headerPair, 0, 16)
+ },
+}
+
+func getHeaderPairList() []headerPair {
+ return headerPairPool.Get().([]headerPair)
+}
+
+func putHeaderPairList(list []headerPair) {
+ list = list[:0]
+ headerPairPool.Put(list)
+}
+
+func (h *stdHeaders) makePairs() ([]headerPair, error) {
+ pairs := getHeaderPairList()
h.mu.RLock()
+ defer h.mu.RUnlock()
if h.agreementPartyUInfo != nil {
- data[AgreementPartyUInfoKey] = h.agreementPartyUInfo
- keys = append(keys, AgreementPartyUInfoKey)
+ v, err := json.Marshal(base64.EncodeToString(h.agreementPartyUInfo))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AgreementPartyUInfoKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: AgreementPartyUInfoKey, Value: v})
}
if h.agreementPartyVInfo != nil {
- data[AgreementPartyVInfoKey] = h.agreementPartyVInfo
- keys = append(keys, AgreementPartyVInfoKey)
+ v, err := json.Marshal(base64.EncodeToString(h.agreementPartyVInfo))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AgreementPartyVInfoKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: AgreementPartyVInfoKey, Value: v})
}
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- keys = append(keys, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: AlgorithmKey, Value: v})
}
if h.compression != nil {
- data[CompressionKey] = *(h.compression)
- keys = append(keys, CompressionKey)
+ v, err := json.Marshal(*(h.compression))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, CompressionKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: CompressionKey, Value: v})
}
if h.contentEncryption != nil {
- data[ContentEncryptionKey] = *(h.contentEncryption)
- keys = append(keys, ContentEncryptionKey)
+ v, err := json.Marshal(*(h.contentEncryption))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentEncryptionKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: ContentEncryptionKey, Value: v})
}
if h.contentType != nil {
- data[ContentTypeKey] = *(h.contentType)
- keys = append(keys, ContentTypeKey)
+ v, err := json.Marshal(*(h.contentType))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentTypeKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: ContentTypeKey, Value: v})
}
if h.critical != nil {
- data[CriticalKey] = h.critical
- keys = append(keys, CriticalKey)
+ v, err := json.Marshal(h.critical)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, CriticalKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: CriticalKey, Value: v})
}
if h.ephemeralPublicKey != nil {
- data[EphemeralPublicKeyKey] = h.ephemeralPublicKey
- keys = append(keys, EphemeralPublicKeyKey)
+ v, err := json.Marshal(h.ephemeralPublicKey)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, EphemeralPublicKeyKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: EphemeralPublicKeyKey, Value: v})
}
if h.jwk != nil {
- data[JWKKey] = h.jwk
- keys = append(keys, JWKKey)
+ v, err := json.Marshal(h.jwk)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: JWKKey, Value: v})
}
if h.jwkSetURL != nil {
- data[JWKSetURLKey] = *(h.jwkSetURL)
- keys = append(keys, JWKSetURLKey)
+ v, err := json.Marshal(*(h.jwkSetURL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKSetURLKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: JWKSetURLKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- keys = append(keys, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: KeyIDKey, Value: v})
}
if h.typ != nil {
- data[TypeKey] = *(h.typ)
- keys = append(keys, TypeKey)
+ v, err := json.Marshal(*(h.typ))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, TypeKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: TypeKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- keys = append(keys, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- keys = append(keys, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- keys = append(keys, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- keys = append(keys, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- keys = append(keys, k)
- }
- h.mu.RUnlock()
-
- sort.Strings(keys)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- enc := json.NewEncoder(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- for i, k := range keys {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(k)
- buf.WriteString(`":`)
- v := data[k]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s`, k)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, headerPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *stdHeaders) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putHeaderPairList(pairs)
return ret, nil
}
@@ -894,6 +992,6 @@ func (h *stdHeaders) clear() {
h.x509CertThumbprint = nil
h.x509CertThumbprintS256 = nil
h.x509URL = nil
- h.privateParams = map[string]any{}
+ clear(h.privateParams)
h.mu.Unlock()
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go
index 91ad8cb809..6f0f918de3 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/interface.go
@@ -39,6 +39,25 @@ type KeyIDer interface {
// expose the secret key in memory, for example, when you want to use
// hardware security modules (HSMs) to decrypt the key.
//
+// Library contract for implementers (read carefully):
+//
+// - The library has already verified that the wire-level `alg` is
+// consistent across the protected header and per-recipient header
+// (RFC 7516 §7.2.1 disjointness). Your DecryptKey is invoked with
+// the alg the library has decided to use for this attempt.
+// - The library has NOT validated key-shape-vs-alg compatibility for
+// your custom decrypter. You receive the raw recipient and message;
+// headers are split between protected (signed/integrity-protected)
+// and per-recipient (unprotected). If you read a value from the
+// unprotected per-recipient header for a security decision, you
+// must enforce its consistency with the protected header yourself.
+// - Returning a non-nil error short-circuits this recipient. Returning
+// nil bytes with nil error is treated as "decryption failed" by the
+// dispatcher (use a non-nil error for clarity).
+// - You are responsible for any constant-time considerations relevant
+// to your decryption primitive (e.g. RFC 3218 random-CEK fallback
+// for RSA-PKCS1v1.5; the library does this for the built-in path).
+//
// This API is experimental and may change without notice, even
// in minor releases.
type KeyDecrypter interface {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go
index 4f08c4936f..da22c82235 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/aescbc/aescbc.go
@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"hash"
+ "slices"
"sync/atomic"
"github.com/lestrrat-go/jwx/v3/internal/pool"
@@ -23,6 +24,12 @@ const defaultBufSize int64 = 256 * 1024 * 1024
var maxBufSize atomic.Int64
+// errInvalidCiphertext is the single opaque error returned by Hmac.Open for
+// every failure mode (pre-MAC structural checks and post-MAC tag mismatch).
+// Keeping one value across all paths prevents a structural-vs-cryptographic
+// oracle on remote decrypt endpoints.
+var errInvalidCiphertext = errors.New("invalid ciphertext")
+
func init() {
SetMaxBufferSize(defaultBufSize)
}
@@ -108,7 +115,7 @@ type Hmac struct {
blockCipher cipher.Block
hash func() hash.Hash
keysize int
- tagsize int
+ tlen int
integrityKey []byte
}
@@ -125,14 +132,23 @@ func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) {
return
}
+ // Per RFC 7518 §5.2.2.1, T_LEN is the authentication tag length. For the
+ // three defined AES-CBC-HMAC variants (A128CBC-HS256, A192CBC-HS384,
+ // A256CBC-HS512) T_LEN happens to equal MAC_KEY_LEN (== keysize here),
+ // but we track it independently so a future variant with a different
+ // T_LEN won't silently mis-truncate the HMAC output.
var hfunc func() hash.Hash
+ var tlen int
switch keysize {
- case 16:
+ case 16: // A128CBC-HS256
hfunc = sha256.New
- case 24:
+ tlen = 16
+ case 24: // A192CBC-HS384
hfunc = sha512.New384
- case 32:
+ tlen = 24
+ case 32: // A256CBC-HS512
hfunc = sha512.New
+ tlen = 32
default:
return nil, fmt.Errorf("unsupported key size %d", keysize)
}
@@ -142,11 +158,7 @@ func New(key []byte, f BlockCipherFunc) (hmac *Hmac, err error) {
hash: hfunc,
integrityKey: ikey,
keysize: keysize,
- tagsize: keysize, // NonceSize,
- // While investigating GH #207, I stumbled upon another problem where
- // the computed tags don't match on decrypt. After poking through the
- // code using a bunch of debug statements, I've finally found out that
- // tagsize = keysize makes the whole thing work.
+ tlen: tlen,
}, nil
}
@@ -157,7 +169,7 @@ func (c Hmac) NonceSize() int {
// Overhead fulfills the crypto.AEAD interface
func (c Hmac) Overhead() int {
- return c.blockCipher.BlockSize() + c.tagsize
+ return c.blockCipher.BlockSize() + c.tlen
}
func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) {
@@ -176,20 +188,26 @@ func (c Hmac) ComputeAuthTag(aad, nonce, ciphertext []byte) ([]byte, error) {
h.Write(ciphertext)
h.Write(buf[:])
s := h.Sum(nil)
- return s[:c.tagsize], nil
+ return s[:c.tlen], nil
}
func ensureSize(dst []byte, n int) []byte {
- // if the dst buffer has enough length just copy the relevant parts to it.
- // Otherwise create a new slice that's big enough, and operate on that
- // Note: I think go-jose has a bug in that it checks for cap(), but not len().
- ret := dst
- if diff := n - len(dst); diff > 0 {
- // dst is not big enough
- ret = make([]byte, n)
- copy(ret, dst)
+ // Grow dst by n bytes, preserving its current contents as the prefix.
+ // This matches the crypto.AEAD append contract used by Seal/Open.
+ if n < 0 {
+ panic(fmt.Errorf("failed to allocate buffer"))
}
- return ret
+
+ const maxInt = int64(^uint(0) >> 1)
+ maxAlloc := min(maxBufSize.Load(), maxInt)
+
+ if int64(len(dst)) > maxAlloc-int64(n) {
+ panic(fmt.Errorf("failed to allocate buffer"))
+ }
+
+ retlen := len(dst) + n
+ dst = slices.Grow(dst, n)
+ return dst[:retlen]
}
// Seal fulfills the crypto.AEAD interface
@@ -215,9 +233,7 @@ func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte {
panic(fmt.Errorf("failed to seal on hmac: %v", err))
}
- retlen := len(dst) + len(ciphertext) + len(authtag)
-
- ret := ensureSize(dst, retlen)
+ ret := ensureSize(dst, len(ciphertext)+len(authtag))
out := ret[len(dst):]
n := copy(out, ciphertext)
copy(out[n:], authtag)
@@ -227,17 +243,32 @@ func (c Hmac) Seal(dst, nonce, plaintext, data []byte) []byte {
// Open fulfills the crypto.AEAD interface
func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
- if len(ciphertext) < c.keysize {
- return nil, fmt.Errorf(`invalid ciphertext (too short)`)
+ // Validate the IV length explicitly instead of letting
+ // cipher.NewCBCDecrypter panic on a mismatched nonce. The caller in
+ // jwe/internal/cipher also wraps Open in a defer/recover, and we
+ // intentionally keep BOTH layers: the explicit check turns a malformed
+ // IV into a normal error on the happy path (reviewable, testable, no
+ // stack unwind), while the recover stays as a belt-and-braces guard
+ // against other panics inside the stdlib CBC path (e.g. future
+ // invariants we don't currently enforce). Removing either layer would
+ // mean relying on the other — this way a regression in one is still
+ // caught by the other. See JWE-005 in the v4 security review.
+ // All pre-MAC structural failures return the exact same error value
+ // as the post-MAC failure below. Distinguishing "malformed nonce",
+ // "ciphertext too short", "ciphertext length not block-aligned", and
+ // "MAC mismatch" at the caller would leak whether an attacker probe
+ // is block-aligned vs cryptographically invalid — a structural-vs-MAC
+ // oracle that composes with other leaks. Keep all four paths opaque.
+ if len(nonce) != c.blockCipher.BlockSize() {
+ return nil, errInvalidCiphertext
+ }
+ if len(ciphertext) < c.tlen {
+ return nil, errInvalidCiphertext
}
- tagOffset := len(ciphertext) - c.tagsize
+ tagOffset := len(ciphertext) - c.tlen
if tagOffset%c.blockCipher.BlockSize() != 0 {
- return nil, fmt.Errorf(
- "invalid ciphertext (invalid length: %d %% %d != 0)",
- tagOffset,
- c.blockCipher.BlockSize(),
- )
+ return nil, errInvalidCiphertext
}
tag := ciphertext[tagOffset:]
ciphertext = ciphertext[:tagOffset]
@@ -248,16 +279,15 @@ func (c Hmac) Open(dst, nonce, ciphertext, data []byte) ([]byte, error) {
}
cbc := cipher.NewCBCDecrypter(c.blockCipher, nonce)
- buf := pool.ByteSlice().GetCapacity(tagOffset)
+ buf := pool.ByteSlice().GetCapacity(tagOffset)[:tagOffset]
defer pool.ByteSlice().Put(buf)
- buf = buf[:tagOffset]
cbc.CryptBlocks(buf, ciphertext)
toRemove, good := extractPadding(buf)
cmp := subtle.ConstantTimeCompare(expectedTag, tag) & int(good)
if cmp != 1 {
- return nil, errors.New(`invalid ciphertext`)
+ return nil, errInvalidCiphertext
}
plaintext := buf[:len(buf)-toRemove]
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go
index 9b9a40d00d..9595864c69 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/cipher/cipher.go
@@ -128,9 +128,8 @@ func (c AesContentCipher) Encrypt(cek, plaintext, aad []byte) (iv, ciphertxt, ta
panic(fmt.Sprintf("tag offset is less than 0 (combined len = %d, tagsize = %d)", len(combined), c.TagSize()))
}
+ ciphertxt = combined[:tagoffset:tagoffset]
tag = combined[tagoffset:]
- ciphertxt = make([]byte, tagoffset)
- copy(ciphertxt, combined[:tagoffset])
return
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go
index 3691830a63..18b61afb6d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf/concatkdf.go
@@ -13,22 +13,25 @@ type KDF struct {
hash crypto.Hash
}
-func ndata(src []byte) []byte {
- buf := make([]byte, 4+len(src))
- binary.BigEndian.PutUint32(buf, uint32(len(src)))
- copy(buf[4:], src)
- return buf
-}
-
func New(hash crypto.Hash, alg, Z, apu, apv, pubinfo, privinfo []byte) *KDF {
- algbuf := ndata(alg)
- apubuf := ndata(apu)
- apvbuf := ndata(apv)
+ // Write length-prefixed fields directly into a single buffer,
+ // avoiding intermediate allocations from ndata().
+ totalSize := (4 + len(alg)) + (4 + len(apu)) + (4 + len(apv)) + len(pubinfo) + len(privinfo)
+ concat := make([]byte, totalSize)
+
+ n := 0
+ binary.BigEndian.PutUint32(concat[n:], uint32(len(alg)))
+ n += 4
+ n += copy(concat[n:], alg)
+
+ binary.BigEndian.PutUint32(concat[n:], uint32(len(apu)))
+ n += 4
+ n += copy(concat[n:], apu)
+
+ binary.BigEndian.PutUint32(concat[n:], uint32(len(apv)))
+ n += 4
+ n += copy(concat[n:], apv)
- concat := make([]byte, len(algbuf)+len(apubuf)+len(apvbuf)+len(pubinfo)+len(privinfo))
- n := copy(concat, algbuf)
- n += copy(concat[n:], apubuf)
- n += copy(concat[n:], apvbuf)
n += copy(concat[n:], pubinfo)
copy(concat[n:], privinfo)
@@ -42,11 +45,13 @@ func New(hash crypto.Hash, alg, Z, apu, apv, pubinfo, privinfo []byte) *KDF {
func (k *KDF) Read(out []byte) (int, error) {
var round uint32 = 1
h := k.hash.New()
+ var roundBuf [4]byte
for len(out) > len(k.buf) {
h.Reset()
- if err := binary.Write(h, binary.BigEndian, round); err != nil {
+ binary.BigEndian.PutUint32(roundBuf[:], round)
+ if _, err := h.Write(roundBuf[:]); err != nil {
return 0, fmt.Errorf(`failed to write round using kdf: %w`, err)
}
if _, err := h.Write(k.z); err != nil {
@@ -56,7 +61,7 @@ func (k *KDF) Read(out []byte) (int, error) {
return 0, fmt.Errorf(`failed to write other info using kdf: %w`, err)
}
- k.buf = append(k.buf, h.Sum(nil)...)
+ k.buf = h.Sum(k.buf)
round++
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go
index daa7599d9f..34e6b05c30 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/internal/keygen/keygen.go
@@ -9,7 +9,6 @@ import (
"fmt"
"io"
- "github.com/lestrrat-go/jwx/v3/internal/ecutil"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwe/internal/concatkdf"
"github.com/lestrrat-go/jwx/v3/jwk"
@@ -28,9 +27,27 @@ func Random(n int) (ByteSource, error) {
return ByteKey(buf), nil
}
-// Ecdhes generates a new key using ECDH-ES
+// Ecdhes generates a new key using ECDH-ES.
+//
+// The recipient pubkey is converted to *ecdh.PublicKey via stdlib
+// (*ecdsa.PublicKey).ECDH() before any cryptographic operation. That
+// conversion uses identity matching against the named NIST curves
+// (elliptic.P256/P384/P521) and rejects anything else — including a
+// caller-controlled or tampered elliptic.Curve and the generic big-int
+// CurveParams path. This closes the invalid-curve attack surface that
+// the previous deprecated crypto/elliptic.Curve.ScalarMult code path
+// exposed when the recipient's *ecdsa.PublicKey.Curve field was
+// attacker-influenced.
func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, apv []byte) (ByteSource, error) {
- priv, err := ecdsa.GenerateKey(pubkey.Curve, rand.Reader)
+ if pubkey == nil || pubkey.X == nil || pubkey.Y == nil {
+ return nil, fmt.Errorf(`invalid ECDH-ES public key: nil X or Y`)
+ }
+ ecdhPub, err := pubkey.ECDH()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to convert ECDH-ES public key to *ecdh.PublicKey: %w`, err)
+ }
+
+ priv, err := ecdhPub.Curve().GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf(`failed to generate key for ECDH-ES: %w`, err)
}
@@ -45,12 +62,11 @@ func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, a
pubinfo := make([]byte, 4)
binary.BigEndian.PutUint32(pubinfo, uint32(keysize)*8)
- if !priv.PublicKey.Curve.IsOnCurve(pubkey.X, pubkey.Y) {
- return nil, fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`)
+ zBytes, err := priv.ECDH(ecdhPub)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to compute Z: %w`, err)
}
- z, _ := priv.PublicKey.Curve.ScalarMult(pubkey.X, pubkey.Y, priv.D.Bytes())
- zBytes := ecutil.AllocECPointBuffer(z, priv.PublicKey.Curve)
- defer ecutil.ReleaseECPointBuffer(zBytes)
+
kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, apu, apv, pubinfo, []byte{})
kek := make([]byte, keysize)
if _, err := kdf.Read(kek); err != nil {
@@ -58,13 +74,13 @@ func Ecdhes(alg string, enc string, keysize int, pubkey *ecdsa.PublicKey, apu, a
}
return ByteWithECPublicKey{
- PublicKey: &priv.PublicKey,
+ PublicKey: priv.PublicKey(),
ByteKey: ByteKey(kek),
}, nil
}
// X25519 generates a new key using ECDH-ES with X25519
-func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey) (ByteSource, error) {
+func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey, apu, apv []byte) (ByteSource, error) {
priv, err := ecdh.X25519().GenerateKey(rand.Reader)
if err != nil {
return nil, fmt.Errorf(`failed to generate key for X25519: %w`, err)
@@ -84,7 +100,7 @@ func X25519(alg string, enc string, keysize int, pubkey *ecdh.PublicKey) (ByteSo
if err != nil {
return nil, fmt.Errorf(`failed to compute Z: %w`, err)
}
- kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, []byte{}, []byte{}, pubinfo, []byte{})
+ kdf := concatkdf.New(crypto.SHA256, []byte(algorithm), zBytes, apu, apv, pubinfo, []byte{})
kek := make([]byte, keysize)
if _, err := kdf.Read(kek); err != nil {
return nil, fmt.Errorf(`failed to read kdf: %w`, err)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go
index a5d6aca8a3..5f1a397c68 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/io.go
@@ -15,6 +15,12 @@ func (sysFS) Open(path string) (fs.File, error) {
}
func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
+ var parseOptions []ParseOption
+ for _, option := range options {
+ if po, ok := option.(ParseOption); ok {
+ parseOptions = append(parseOptions, po)
+ }
+ }
var srcFS fs.FS = sysFS{}
for _, option := range options {
@@ -32,5 +38,5 @@ func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
}
defer f.Close()
- return ParseReader(f)
+ return ParseReader(f, parseOptions...)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go
index 5b9c92771a..706efaaa27 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwe.go
@@ -1,6 +1,11 @@
//go:generate ../tools/cmd/genjwe.sh
-// Package jwe implements JWE as described in https://tools.ietf.org/html/rfc7516
+// Package jwe implements JWE as described in https://tools.ietf.org/html/rfc7516.
+//
+// Legacy note: RSA-PKCS1 v1.5 key encryption (`jwa.RSA1_5()`) is supported
+// only for interoperability with existing peers. New applications should
+// prefer an RSA-OAEP variant such as `jwa.RSA_OAEP_256()` because PKCS#1 v1.5
+// decryption is exposed to Bleichenbacher-style oracle attacks.
package jwe
// #region imports
@@ -11,7 +16,9 @@ import (
"errors"
"fmt"
"io"
- "sync"
+ "math"
+ "slices"
+ "sync/atomic"
"github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
@@ -28,33 +35,92 @@ import (
// #region globals
-var muSettings sync.RWMutex
-var maxPBES2Count = 10000
-var maxDecompressBufferSize int64 = 10 * 1024 * 1024 // 10MB
+var maxPBES2Count atomic.Int64
+var minPBES2Count atomic.Int64
+var pbes2Count atomic.Int64
+var maxRecipients atomic.Int64
+var maxDecompressBufferSize atomic.Int64
+var disabledKeyAlgs atomic.Pointer[map[string]struct{}]
+
+func init() {
+ maxPBES2Count.Store(10000)
+ minPBES2Count.Store(1000)
+ pbes2Count.Store(int64(tokens.PBES2DefaultIterations))
+ maxRecipients.Store(100)
+ maxDecompressBufferSize.Store(10 * 1024 * 1024) // 10MB
+}
func Settings(options ...GlobalOption) {
- muSettings.Lock()
- defer muSettings.Unlock()
for _, option := range options {
switch option.Ident() {
case identMaxPBES2Count{}:
- if err := option.Value(&maxPBES2Count); err != nil {
+ var v int
+ if err := option.Value(&v); err != nil {
panic(fmt.Sprintf("jwe.Settings: value for option WithMaxPBES2Count must be an int: %s", err))
}
+ maxPBES2Count.Store(int64(v))
+ case identMinPBES2Count{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ panic(fmt.Sprintf("jwe.Settings: value for option WithMinPBES2Count must be an int: %s", err))
+ }
+ minPBES2Count.Store(int64(v))
+ case identPBES2Count{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ panic(fmt.Sprintf("jwe.Settings: value for option WithPBES2Count must be an int: %s", err))
+ }
+ if v <= 0 {
+ v = tokens.PBES2DefaultIterations
+ }
+ pbes2Count.Store(int64(v))
+ case identMaxRecipients{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ panic(fmt.Sprintf("jwe.Settings: value for option WithMaxRecipients must be an int: %s", err))
+ }
+ maxRecipients.Store(int64(v))
case identMaxDecompressBufferSize{}:
- if err := option.Value(&maxDecompressBufferSize); err != nil {
+ var v int64
+ if err := option.Value(&v); err != nil {
panic(fmt.Sprintf("jwe.Settings: value for option WithMaxDecompressBufferSize must be an int64: %s", err))
}
+ maxDecompressBufferSize.Store(v)
case identCBCBufferSize{}:
var v int64
if err := option.Value(&v); err != nil {
panic(fmt.Sprintf("jwe.Settings: value for option WithCBCBufferSize must be an int64: %s", err))
}
aescbc.SetMaxBufferSize(v)
+ case identDisabledKeyAlgorithms{}:
+ var algs []jwa.KeyEncryptionAlgorithm
+ if err := option.Value(&algs); err != nil {
+ panic(fmt.Sprintf("jwe.Settings: value for option WithDisabledKeyAlgorithms must be []jwa.KeyEncryptionAlgorithm: %s", err))
+ }
+ if len(algs) == 0 {
+ disabledKeyAlgs.Store(nil)
+ continue
+ }
+ m := make(map[string]struct{}, len(algs))
+ for _, alg := range algs {
+ m[alg.String()] = struct{}{}
+ }
+ disabledKeyAlgs.Store(&m)
}
}
}
+// isKeyAlgorithmDisabled reports whether alg is in the global
+// jwe.WithDisabledKeyAlgorithms set.
+func isKeyAlgorithmDisabled(alg jwa.KeyEncryptionAlgorithm) bool {
+ m := disabledKeyAlgs.Load()
+ if m == nil {
+ return false
+ }
+ _, ok := (*m)[alg.String()]
+ return ok
+}
+
const (
fmtInvalid = iota
fmtCompact
@@ -63,18 +129,19 @@ const (
fmtMax
)
-var _ = fmtInvalid
-var _ = fmtMax
-
var registry = json.NewRegistry()
type recipientBuilder struct {
- alg jwa.KeyEncryptionAlgorithm
- key any
- headers Headers
+ alg jwa.KeyEncryptionAlgorithm
+ key any
+ headers Headers
+ pbes2Count int
}
func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryptionAlgorithm, _ *content_crypt.Generic) ([]byte, error) {
+ if isKeyAlgorithmDisabled(b.alg) {
+ return nil, fmt.Errorf(`jwe.Encrypt: key encryption algorithm %q is disabled by jwe.WithDisabledKeyAlgorithms`, b.alg)
+ }
// we need the raw key for later use
rawKey := b.key
@@ -104,7 +171,7 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp
hdr := b.headers
if hdr == nil {
- hdr = NewHeaders()
+ hdr = r.Headers()
}
if val, ok := hdr.AgreementPartyUInfo(); ok {
@@ -116,10 +183,7 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp
}
// Create the encrypter using the new jwebb pattern
- enc, err := newEncrypter(b.alg, calg, b.key, rawKey, apu, apv)
- if err != nil {
- return nil, fmt.Errorf(`jwe.Encrypt: recipientBuilder: failed to create encrypter: %w`, err)
- }
+ enc := newEncrypter(b.alg, calg, b.key, rawKey, apu, apv, b.pbes2Count)
_ = r.SetHeaders(hdr)
@@ -168,9 +232,9 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp
// option.
//
// jwe.Encrypt(payload, jwe.WithKey(alg, key))
-// jwe.Encrypt(payload, jws.WithJSON(), jws.WithKey(alg1, key1), jws.WithKey(alg2, key2))
+// jwe.Encrypt(payload, jwe.WithJSON(), jwe.WithKey(alg1, key1), jwe.WithKey(alg2, key2))
//
-// Note that in the second example the `jws.WithJSON()` option is
+// Note that in the second example the `jwe.WithJSON()` option is
// specified as well. This is because the compact serialization
// format does not support multiple recipients, and users must
// specifically ask for the JSON serialization format.
@@ -178,7 +242,18 @@ func (b *recipientBuilder) Build(r Recipient, cek []byte, calg jwa.ContentEncryp
// Read the documentation for `jwe.WithKey()` to learn more about the
// possible values that can be used for `alg` and `key`.
//
-// Look for options that return `jwe.EncryptOption` or `jws.EncryptDecryptOption`
+// `jwa.RSA1_5()` is supported only for interoperability with legacy peers.
+// New applications should prefer an RSA-OAEP variant such as
+// `jwa.RSA_OAEP_256()` because PKCS#1 v1.5 decryption is exposed to
+// Bleichenbacher-style oracle attacks.
+// If you enable `jwe.WithCompress()`, this library does not enforce a
+// producer-side payload size limit before compression. Callers that accept
+// untrusted or arbitrarily large plaintext must bound the input size before
+// calling `jwe.Encrypt()`. Recipients may also reject compressed messages
+// whose decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize()`
+// setting.
+//
+// Look for options that return `jwe.EncryptOption` or `jwe.EncryptDecryptOption`
// for a complete list of options that can be passed to this function.
//
// As of v3.0.12, users can specify `jwe.WithLegacyHeaderMerging()` to
@@ -188,11 +263,11 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
ec := encryptContextPool.Get()
defer encryptContextPool.Put(ec)
if err := ec.ProcessOptions(options); err != nil {
- return nil, encryptError{fmt.Errorf(`jwe.Encrypt: failed to process options: %w`, err)}
+ return nil, makeEncryptError(`jwe.Encrypt`, `failed to process options: %w`, err)
}
ret, err := ec.EncryptMessage(payload, nil)
if err != nil {
- return nil, encryptError{fmt.Errorf(`jwe.Encrypt: %w`, err)}
+ return nil, makeEncryptError(`jwe.Encrypt`, `%w`, err)
}
return ret, nil
}
@@ -202,6 +277,32 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
// Encrypt function such that the latter does not accidentally use a static
// CEK.
//
+// Unless `jwe.WithContentEncryption()` is provided, `EncryptStatic` uses
+// `jwa.A256GCM()`, which requires a 32-byte CEK.
+//
+// The CEK used to encrypt the payload must match the selected content
+// encryption algorithm:
+//
+// - `jwa.A128GCM()`: 16 bytes
+// - `jwa.A192GCM()`: 24 bytes
+// - `jwa.A256GCM()`: 32 bytes
+// - `jwa.A128CBC_HS256()`: 32 bytes
+// - `jwa.A192CBC_HS384()`: 48 bytes
+// - `jwa.A256CBC_HS512()`: 64 bytes
+//
+// `EncryptStatic` validates the final CEK length before payload encryption
+// and returns an error if it does not match the selected `enc` algorithm.
+//
+// NOTE: when the chosen key-encryption algorithm derives the CEK rather than
+// wrapping it — specifically `jwa.DIRECT()` and bare `jwa.ECDH_ES()` (without
+// a key-wrap suffix) — the `cek` argument supplied here is ignored for
+// content encryption. In those modes the effective CEK is the shared/derived
+// key produced by the `jwe.WithKey()` input, and the byte-length check
+// described above is enforced against that derived CEK, not against the
+// value passed as `cek`. To pin the CEK deterministically, pair
+// `EncryptStatic` only with key-wrapping algorithms such as
+// `jwa.RSA_OAEP()`, `jwa.A256KW()`, or `jwa.ECDH_ES_A256KW()`.
+//
// DO NOT attempt to use this function unless you completely understand the
// security implications to using static CEKs. You have been warned.
//
@@ -209,16 +310,16 @@ func Encrypt(payload []byte, options ...EncryptOption) ([]byte, error) {
// future changes across minor/micro versions.
func EncryptStatic(payload, cek []byte, options ...EncryptOption) ([]byte, error) {
if len(cek) <= 0 {
- return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: empty CEK`)}
+ return nil, makeEncryptError(`jwe.EncryptStatic`, `empty CEK`)
}
ec := encryptContextPool.Get()
defer encryptContextPool.Put(ec)
if err := ec.ProcessOptions(options); err != nil {
- return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: failed to process options: %w`, err)}
+ return nil, makeEncryptError(`jwe.EncryptStatic`, `failed to process options: %w`, err)
}
ret, err := ec.EncryptMessage(payload, cek)
if err != nil {
- return nil, encryptError{fmt.Errorf(`jwe.EncryptStatic: %w`, err)}
+ return nil, makeEncryptError(`jwe.EncryptStatic`, `%w`, err)
}
return ret, nil
}
@@ -229,7 +330,12 @@ type decryptContext struct {
keyUsed any
cek *[]byte
dst *Message
+ maxRecipients int
maxDecompressBufferSize int64
+ maxPBES2Count int
+ minPBES2Count int
+ critValidation bool
+ criticalExtensions []string
//nolint:containedctx
ctx context.Context
}
@@ -247,16 +353,21 @@ func freeDecryptContext(dc *decryptContext) *decryptContext {
dc.keyUsed = nil
dc.cek = nil
dc.dst = nil
+ dc.maxRecipients = 0
dc.maxDecompressBufferSize = 0
+ dc.maxPBES2Count = 0
+ dc.minPBES2Count = 0
+ dc.critValidation = false
+ dc.criticalExtensions = dc.criticalExtensions[:0]
dc.ctx = context.Background()
return dc
}
func (dc *decryptContext) ProcessOptions(options []DecryptOption) error {
- // Set default max decompress buffer size
- muSettings.RLock()
- dc.maxDecompressBufferSize = maxDecompressBufferSize
- muSettings.RUnlock()
+ dc.maxRecipients = int(maxRecipients.Load())
+ dc.maxDecompressBufferSize = maxDecompressBufferSize.Load()
+ dc.maxPBES2Count = int(maxPBES2Count.Load())
+ dc.minPBES2Count = int(minPBES2Count.Load())
for _, option := range options {
switch option.Ident() {
@@ -283,19 +394,44 @@ func (dc *decryptContext) ProcessOptions(options []DecryptOption) error {
if !ok {
return fmt.Errorf("jwe.decrypt: WithKey() option must be specified using jwa.KeyEncryptionAlgorithm (got %T)", pair.alg)
}
+ if err := validateAlgorithmForKey(alg, pair.key); err != nil {
+ return fmt.Errorf("jwe.WithKey: %w", err)
+ }
dc.keyProviders = append(dc.keyProviders, &staticKeyProvider{alg: alg, key: pair.key})
case identCEK{}:
if err := option.Value(&dc.cek); err != nil {
return fmt.Errorf("jwe.decrypt: WithCEK must be a *[]byte: %w", err)
}
+ case identMaxRecipients{}:
+ if err := option.Value(&dc.maxRecipients); err != nil {
+ return fmt.Errorf("jwe.decrypt: WithMaxRecipients must be int: %w", err)
+ }
case identMaxDecompressBufferSize{}:
if err := option.Value(&dc.maxDecompressBufferSize); err != nil {
return fmt.Errorf("jwe.decrypt: WithMaxDecompressBufferSize must be int64: %w", err)
}
+ case identMaxPBES2Count{}:
+ if err := option.Value(&dc.maxPBES2Count); err != nil {
+ return fmt.Errorf("jwe.decrypt: WithMaxPBES2Count must be int: %w", err)
+ }
+ case identMinPBES2Count{}:
+ if err := option.Value(&dc.minPBES2Count); err != nil {
+ return fmt.Errorf("jwe.decrypt: WithMinPBES2Count must be int: %w", err)
+ }
case identContext{}:
if err := option.Value(&dc.ctx); err != nil {
return fmt.Errorf("jwe.decrypt: WithContext must be a context.Context: %w", err)
}
+ case identCritValidation{}:
+ if err := option.Value(&dc.critValidation); err != nil {
+ return fmt.Errorf("jwe.decrypt: WithCritValidation must be a bool: %w", err)
+ }
+ case identCritExtension{}:
+ var names []string
+ if err := option.Value(&names); err != nil {
+ return fmt.Errorf("jwe.decrypt: WithCritExtension must be a string: %w", err)
+ }
+ dc.criticalExtensions = append(dc.criticalExtensions, names...)
}
}
@@ -306,20 +442,107 @@ func (dc *decryptContext) ProcessOptions(options []DecryptOption) error {
return nil
}
-func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
- msg, err := parseJSONOrCompact(buf, true)
- if err != nil {
- return nil, fmt.Errorf(`failed to parse buffer for Decrypt: %w`, err)
+// validateCritical checks the "crit" header per RFC 7516 Section 4.1.13
+// (which references RFC 7515 Section 4.1.11). It enforces:
+// - the list is non-empty
+// - no entry is the empty string
+// - no entry duplicates another
+// - no entry names a standard JOSE/JWE header parameter
+// - every entry appears as a header parameter in the protected header
+// - every entry is in the caller-supplied allowedExtensions allowlist
+//
+// The last check is the central RFC requirement: recipients MUST reject
+// any "crit" extension they do not understand, and the only way the
+// library knows which extensions the caller understands is via the
+// allowlist (populated from jwe.WithCritExtension()).
+func validateCritical(protected Headers, allowedExtensions []string) error {
+ if !protected.Has(CriticalKey) {
+ return nil
}
- // Process things that are common to the message
+ crit, _ := protected.Critical()
+ if len(crit) == 0 {
+ return makeDecryptError(`"crit" header must not be empty`)
+ }
+
+ seen := make(map[string]struct{}, len(crit))
+ for _, name := range crit {
+ if name == "" {
+ return makeDecryptError(`"crit" header must not contain an empty extension name`)
+ }
+ if _, dup := seen[name]; dup {
+ return makeDecryptError(`"crit" header must not contain duplicate extension %q`, name)
+ }
+ seen[name] = struct{}{}
+
+ // RFC 7515 Section 4.1.11: "crit" MUST NOT include names defined
+ // by the JOSE Header specification itself.
+ if slices.Contains(stdHeaderNames, name) {
+ return makeDecryptError(`"crit" header must not contain standard header parameter %q`, name)
+ }
+
+ // The extension must be present in the protected header.
+ if !protected.Has(name) {
+ return makeDecryptError(`"crit" header references extension %q, but it is not present in the protected header`, name)
+ }
+
+ // The recipient must have declared support for the extension.
+ if !slices.Contains(allowedExtensions, name) {
+ return makeDecryptError(`"crit" header references extension %q, but the recipient has not declared support for it (use jwe.WithCritExtension(%q))`, name, name)
+ }
+ }
+
+ return nil
+}
+
+// concatAAD returns the AAD value used to seal or open a JWE payload:
+// the protected-header segment, optionally followed by ASCII '.' and
+// the caller-supplied external aad (RFC 7516 §5.1 step 14 / §5.2
+// step 14). A fresh slice is always allocated so the caller's computed
+// and aad slices are never appended into, which matters because
+// computedAad often aliases a Message field whose backing array is
+// still referenced elsewhere.
+func concatAAD(computed, aad []byte) []byte {
+ if len(aad) == 0 {
+ return computed
+ }
+ out := make([]byte, len(computed)+1+len(aad))
+ n := copy(out, computed)
+ out[n] = tokens.Period
+ copy(out[n+1:], aad)
+ return out
+}
+
+func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
+ msg, err := parseJSONOrCompact(buf, true, dc.maxRecipients)
+ if err != nil {
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to parse buffer: %w`, err)
+ }
+
+ // Validate the "crit" header per RFC 7516 Section 4.1.13. The check
+ // runs against the protected header only — RFC says "crit" MUST live
+ // there — and short-circuits before any key-decrypt or content-decrypt
+ // work happens.
+ if dc.critValidation {
+ if err := validateCritical(msg.protectedHeaders, dc.criticalExtensions); err != nil {
+ return nil, err
+ }
+ }
+
+ // Clone the shared (top-level) protected header as our working copy.
+ // We deliberately do NOT merge msg.unprotectedHeaders (the shared,
+ // top-level *unprotected* header) here: it is never covered by the
+ // AEAD tag, so it must not contribute algorithm parameters.
+ //
+ // Per-recipient unprotected headers are a separate case — RFC 7516
+ // §5.3 explicitly permits them to carry recipient-specific algorithm
+ // parameters (alg, epk, p2s, p2c, iv, tag, apu, apv, …), and
+ // decryptContent merges recipient.Headers() onto this base below.
+ // That merge is bounded by WithMaxRecipients and, for PBES2, by
+ // WithMaxPBES2Count (applied per recipient).
h, err := msg.protectedHeaders.Clone()
if err != nil {
- return nil, fmt.Errorf(`failed to copy protected headers: %w`, err)
- }
- h, err = h.Merge(msg.unprotectedHeaders)
- if err != nil {
- return nil, fmt.Errorf(`failed to merge headers for message decryption: %w`, err)
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to copy protected headers: %w`, err)
}
var aad []byte
@@ -335,7 +558,7 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
var err error
computedAad, err = msg.protectedHeaders.Encode()
if err != nil {
- return nil, fmt.Errorf(`failed to encode protected headers: %w`, err)
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to encode protected headers: %w`, err)
}
}
@@ -345,16 +568,22 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
if len(recipients) == 0 {
r := NewRecipient()
if err := r.SetHeaders(msg.protectedHeaders); err != nil {
- return nil, fmt.Errorf(`failed to set headers to recipient: %w`, err)
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to set headers to recipient: %w`, err)
}
recipients = append(recipients, r)
}
errs := make([]error, 0, len(recipients))
for _, recipient := range recipients {
+ // Honor caller's deadline between recipients. Symmetric with
+ // the per-keyProvider and per-(alg,key) checks in tryRecipient.
+ if err := dc.ctx.Err(); err != nil {
+ return nil, makeDecryptError(`%w`, err)
+ }
+
decrypted, err := dc.tryRecipient(msg, recipient, h, aad, computedAad)
if err != nil {
- errs = append(errs, recipientError{err})
+ errs = append(errs, makeRecipientError(err))
continue
}
if dc.dst != nil {
@@ -364,19 +593,48 @@ func (dc *decryptContext) DecryptMessage(buf []byte) ([]byte, error) {
}
return decrypted, nil
}
- return nil, fmt.Errorf(`failed to decrypt any of the recipients: %w`, errors.Join(errs...))
+ // Bound the joined-error count so a hostile JWE with many recipients
+ // can't produce an unbounded error string. Keep the first
+ // decryptErrorJoinCap entries verbatim and replace the rest with a
+ // single "... and N more" sentinel.
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to decrypt any of the recipients: %w`, joinDecryptErrors(errs))
+}
+
+// decryptErrorJoinCap caps how many per-recipient constituent errors
+// get joined into the final Decrypt error so the resulting err.Error()
+// can't grow unboundedly under a hostile multi-recipient JWE.
+const decryptErrorJoinCap = 10
+
+func joinDecryptErrors(errs []error) error {
+ if len(errs) <= decryptErrorJoinCap {
+ return errors.Join(errs...)
+ }
+ kept := make([]error, decryptErrorJoinCap, decryptErrorJoinCap+1)
+ copy(kept, errs[:decryptErrorJoinCap])
+ kept = append(kept, fmt.Errorf("... and %d more error(s) suppressed", len(errs)-decryptErrorJoinCap))
+ return errors.Join(kept...)
}
func (dc *decryptContext) tryRecipient(msg *Message, recipient Recipient, protectedHeaders Headers, aad, computedAad []byte) ([]byte, error) {
var tried int
var lastError error
for i, kp := range dc.keyProviders {
+ // Honor caller's deadline between key providers.
+ if err := dc.ctx.Err(); err != nil {
+ return nil, err
+ }
+
var sink algKeySink
if err := kp.FetchKeys(dc.ctx, &sink, recipient, msg); err != nil {
return nil, fmt.Errorf(`key provider %d failed: %w`, i, err)
}
for _, pair := range sink.list {
+ // Honor caller's deadline between (alg,key) pairs.
+ if err := dc.ctx.Err(); err != nil {
+ return nil, err
+ }
+
tried++
// alg is converted here because pair.alg is of type jwa.KeyAlgorithm.
// this may seem ugly, but we're trying to avoid declaring separate
@@ -403,6 +661,9 @@ func (dc *decryptContext) tryRecipient(msg *Message, recipient Recipient, protec
}
func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgorithm, key any, recipient Recipient, protectedHeaders Headers, aad, computedAad []byte) ([]byte, error) {
+ if isKeyAlgorithmDisabled(alg) {
+ return nil, makeDecryptError(`key encryption algorithm %q is disabled by jwe.WithDisabledKeyAlgorithms`, alg)
+ }
if jwkKey, ok := key.(jwk.Key); ok {
var raw any
if err := jwk.Export(jwkKey, &raw); err != nil {
@@ -422,9 +683,34 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo
Tag(msg.tag).
CEK(dc.cek)
- // The "alg" header can be in either protected/unprotected headers.
- // prefer per-recipient headers (as it might be the case that the algorithm differs
- // by each recipient), then look at protected headers.
+ // RFC 7516 §7.2.1 requires header parameter names to be disjoint
+ // across the protected, shared-unprotected, and per-recipient
+ // header locations. For "alg" specifically, allowing protected
+ // and per-recipient headers to declare conflicting values is an
+ // algorithm-confusion vector: an attacker who can rewrite the
+ // per-recipient (unprotected) location can claim a different alg
+ // than the integrity-protected one, and the alg-match loop below
+ // would silently break on whichever it sees first.
+ //
+ // Compact-form JWE legitimately has the same alg value in both
+ // places — parseCompact synthesizes a per-recipient header by
+ // cloning the protected header (minus enc), so a strict-disjoint
+ // check would reject every compact JWE. We therefore allow the
+ // duplication when the values agree, and reject only when they
+ // disagree.
+ if rh := recipient.Headers(); rh != nil {
+ if recipAlg, recipHas := rh.Algorithm(); recipHas {
+ if protectedAlg, protectedHas := protectedHeaders.Algorithm(); protectedHas && protectedAlg != recipAlg {
+ return nil, makeDecryptError(`malformed JWE — "alg" header value differs between protected (%q) and per-recipient (%q) headers (RFC 7516 §7.2.1)`, protectedAlg, recipAlg)
+ }
+ }
+ }
+
+ // The "alg" header can be in either protected or per-recipient
+ // headers. With disjointness enforced above, only one location can
+ // have it, so iteration order does not affect security; we keep
+ // per-recipient first to match the historical preference for
+ // recipient-specific algs in multi-recipient JWE.
var algMatched bool
for _, hdr := range []Headers{recipient.Headers(), protectedHeaders} {
v, ok := hdr.Algorithm()
@@ -484,7 +770,10 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo
}
case jwa.A128GCMKW(), jwa.A192GCMKW(), jwa.A256GCMKW():
var ivB64 string
- if err := h2.Get(InitializationVectorKey, &ivB64); err == nil {
+ if h2.Has(InitializationVectorKey) {
+ if err := h2.Get(InitializationVectorKey, &ivB64); err != nil {
+ return nil, fmt.Errorf(`field %q is not a string: %w`, InitializationVectorKey, err)
+ }
iv, err := base64.DecodeString(ivB64)
if err != nil {
return nil, fmt.Errorf(`failed to b64-decode 'iv': %w`, err)
@@ -492,7 +781,10 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo
dec.KeyInitializationVector(iv)
}
var tagB64 string
- if err := h2.Get(TagKey, &tagB64); err == nil {
+ if h2.Has(TagKey) {
+ if err := h2.Get(TagKey, &tagB64); err != nil {
+ return nil, fmt.Errorf(`field %q is not a string: %w`, TagKey, err)
+ }
tag, err := base64.DecodeString(tagB64)
if err != nil {
return nil, fmt.Errorf(`failed to b64-decode 'tag': %w`, err)
@@ -505,39 +797,58 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo
return nil, fmt.Errorf(`failed to get %q field`, SaltKey)
}
- // check if WithUseNumber is effective, because it will change the
- // type of the underlying value (#1140)
- var countFlt float64
+ // Parse p2c into int64 directly. Float64 cannot represent
+ // integers above 2^53 exactly; comparing a parsed value
+ // against a high MaxPBES2Count cap in float-space and then
+ // casting via int(...) lets out-of-range values silently
+ // round into the accepted range when callers raise the cap
+ // past 2^53. int64 keeps the bound check exact.
+ var count int64
if json.UseNumber() {
- var count json.Number
- if err := h2.Get(CountKey, &count); err != nil {
+ var n json.Number
+ if err := h2.Get(CountKey, &n); err != nil {
return nil, fmt.Errorf(`failed to get %q field`, CountKey)
}
- v, err := count.Float64()
+ c, err := n.Int64()
if err != nil {
- return nil, fmt.Errorf("failed to convert 'p2c' to float64: %w", err)
+ return nil, fmt.Errorf(`invalid 'p2c' value: %q is not a valid integer: %w`, n.String(), err)
}
- countFlt = v
+ count = c
} else {
- var count float64
- if err := h2.Get(CountKey, &count); err != nil {
+ var v float64
+ if err := h2.Get(CountKey, &v); err != nil {
return nil, fmt.Errorf(`failed to get %q field`, CountKey)
}
- countFlt = count
+ if math.IsNaN(v) || math.IsInf(v, 0) || math.Trunc(v) != v {
+ return nil, fmt.Errorf(`invalid 'p2c' value: not a positive integer (got %v)`, v)
+ }
+ // Use explicit float-domain bounds (2^63 / -2^63) so
+ // the comparison is platform-independent and does not
+ // go through math.MaxInt64's implicit conversion.
+ const (
+ int64MaxAsFloat = float64(1 << 63) // 2^63, smallest float > MaxInt64
+ int64MinAsFloat = -int64MaxAsFloat // -2^63, exact float = MinInt64
+ )
+ if v >= int64MaxAsFloat || v < int64MinAsFloat {
+ return nil, fmt.Errorf(`invalid 'p2c' value: not representable as int64 (got %v)`, v)
+ }
+ count = int64(v)
}
- muSettings.RLock()
- maxCount := maxPBES2Count
- muSettings.RUnlock()
- if countFlt > float64(maxCount) {
- return nil, fmt.Errorf("invalid 'p2c' value")
+ maxCount := dc.maxPBES2Count
+ minCount := dc.minPBES2Count
+ if count < int64(minCount) {
+ return nil, fmt.Errorf(`invalid 'p2c' value: %d is below WithMinPBES2Count=%d (RFC 7518 §4.8.1.2 floor; loosen via jwe.WithMinPBES2Count)`, count, minCount)
+ }
+ if count > int64(maxCount) {
+ return nil, fmt.Errorf(`invalid 'p2c' value: %d exceeds WithMaxPBES2Count=%d (DoS amplification cap; raise via jwe.WithMaxPBES2Count)`, count, maxCount)
}
salt, err := base64.DecodeString(saltB64)
if err != nil {
return nil, fmt.Errorf(`failed to b64-decode 'salt': %w`, err)
}
dec.KeySalt(salt)
- dec.KeyCount(int(countFlt))
+ dec.KeyCount(int(count))
}
plaintext, err := dec.Decrypt(recipient, msg.cipherText, msg)
@@ -548,7 +859,7 @@ func (dc *decryptContext) decryptContent(msg *Message, alg jwa.KeyEncryptionAlgo
if v, ok := h2.Compression(); ok && v == jwa.Deflate() {
buf, err := uncompress(plaintext, dc.maxDecompressBufferSize)
if err != nil {
- return nil, fmt.Errorf(`jwe.Derypt: failed to uncompress payload: %w`, err)
+ return nil, fmt.Errorf(`jwe.Decrypt: failed to uncompress payload: %w`, err)
}
plaintext = buf
}
@@ -565,6 +876,7 @@ type encryptContext struct {
calg jwa.ContentEncryptionAlgorithm
compression jwa.CompressionAlgorithm
format int
+ pbes2Count int
builders []*recipientBuilder
protected Headers
legacyHeaderMerging bool
@@ -584,6 +896,7 @@ func freeEncryptContext(ec *encryptContext) *encryptContext {
ec.calg = jwa.A256GCM()
ec.compression = jwa.NoCompress()
ec.format = fmtCompact
+ ec.pbes2Count = 0
ec.builders = ec.builders[:0]
ec.protected = nil
return ec
@@ -591,6 +904,7 @@ func freeEncryptContext(ec *encryptContext) *encryptContext {
func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
ec.legacyHeaderMerging = true
+ ec.pbes2Count = int(pbes2Count.Load())
var mergeProtected bool
var useRawCEK bool
for _, option := range options {
@@ -604,6 +918,9 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
if !ok {
return fmt.Errorf("jwe.encrypt: WithKey() option must be specified using jwa.KeyEncryptionAlgorithm (got %T)", wk.alg)
}
+ if err := validateAlgorithmForKey(v, wk.key); err != nil {
+ return fmt.Errorf("jwe.WithKey: %w", err)
+ }
if v == jwa.DIRECT() || v == jwa.ECDH_ES() {
useRawCEK = true
}
@@ -612,6 +929,14 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
key: wk.key,
headers: wk.headers,
})
+ case identPBES2Count{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ return fmt.Errorf("jwe.encrypt: WithPBES2Count must be int: %w", err)
+ }
+ if v > 0 {
+ ec.pbes2Count = v
+ }
case identContentEncryptionAlgorithm{}:
var c jwa.ContentEncryptionAlgorithm
if err := option.Value(&c); err != nil {
@@ -671,7 +996,7 @@ func (ec *encryptContext) ProcessOptions(options []EncryptOption) error {
if useRawCEK {
if len(ec.builders) != 1 {
- return fmt.Errorf(`multiple recipients for ECDH-ES/DIRECT mode supported`)
+ return fmt.Errorf(`multiple recipients for ECDH-ES/DIRECT mode are not supported`)
}
}
@@ -714,10 +1039,14 @@ func freeHeaders(h Headers) Headers {
var recipientPool = pool.New(NewRecipient, freeRecipient)
func freeRecipient(r Recipient) Recipient {
+ // Return the recipient's headers to headerPool and install a fresh
+ // instance so the next recipientPool.Get() never hands out a
+ // pointer the caller may still hold a reference to. This is safe
+ // because WithPerRecipientHeaders clones the caller-supplied
+ // Headers, so anything we receive here is already library-owned.
if h := r.Headers(); h != nil {
- if c, ok := h.(interface{ clear() }); ok {
- c.clear()
- }
+ headerPool.Put(h)
+ _ = r.SetHeaders(headerPool.Get())
}
if sr, ok := r.(*stdRecipient); ok {
@@ -777,6 +1106,7 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
defer recipientSlicePool.Put(recipients)
for i, builder := range ec.builders {
+ builder.pbes2Count = ec.pbes2Count
r := recipientPool.Get()
defer recipientPool.Put(r)
@@ -794,6 +1124,10 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
}
}
+ if len(cek) != contentcrypt.KeySize() {
+ return nil, fmt.Errorf(`content encryption key length %d does not match enc %q (expected %d bytes)`, len(cek), ec.calg.String(), contentcrypt.KeySize())
+ }
+
if err := protected.Set(ContentEncryptionKey, ec.calg); err != nil {
return nil, fmt.Errorf(`failed to set "enc" in protected header: %w`, err)
}
@@ -869,6 +1203,13 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
return nil, fmt.Errorf(`failed to encrypt payload: %w`, err)
}
+ // Fast path for compact serialization: assemble directly from
+ // 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
+ }
+
msg := msgPool.Get()
defer msgPool.Put(msg)
@@ -889,8 +1230,6 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
}
switch ec.format {
- case fmtCompact:
- return Compact(msg)
case fmtJSON:
return json.Marshal(msg)
case fmtJSONPretty:
@@ -904,11 +1243,11 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
// payload (e.g. the key encryption algorithm and the corresponding
// key to decrypt the JWE message) in its optional arguments. See
// the examples and list of options that return a DecryptOption for possible
-// values. Upon successful decryptiond returns the decrypted payload.
+// values. Upon successful decryption returns the decrypted payload.
//
// The JWE message can be either compact or full JSON format.
//
-// When using `jwe.WithKeyEncryptionAlgorithm()`, you can pass a `jwa.KeyAlgorithm`
+// When using `jwe.WithKey()`, you can pass a `jwa.KeyAlgorithm`
// for convenience: this is mainly to allow you to directly pass the result of `(jwk.Key).Algorithm()`.
// However, do note that while `(jwk.Key).Algorithm()` could very well contain key encryption
// algorithms, it could also contain other types of values, such as _signature algorithms_.
@@ -929,17 +1268,31 @@ func (ec *encryptContext) EncryptMessage(payload []byte, cek []byte) ([]byte, er
//
// jwe.Settings(jwe.WithMaxDecompressBufferSize(10*1024*1024)) // changes value globally
// jwe.Decrypt(..., jwe.WithMaxDecompressBufferSize(250*1024)) // changes just for this call
+//
+// PBES2 amplification: PBES2 algorithms (PBES2-HS256+A128KW, etc.)
+// derive the CEK via PBKDF2 with the iteration count taken from the
+// JWE's `p2c` header. An attacker-controlled iteration count multiplied
+// by `WithMaxRecipients` is the major CPU-amplification vector on the
+// decrypt side. Bound it via `WithMaxPBES2Count` (default 1,000,000)
+// and reject too-low counts via `WithMinPBES2Count` (default 1000;
+// RFC 7518 §4.8.1.2 floor — note OWASP 2023 recommends ≥600,000 for
+// production password-derived key material). Both options accept a
+// `Settings()` global or a per-call value.
func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
dc := decryptContextPool.Get()
defer decryptContextPool.Put(dc)
if err := dc.ProcessOptions(options); err != nil {
- return nil, decryptError{fmt.Errorf(`jwe.Decrypt: failed to process options: %w`, err)}
+ return nil, makeDecryptError(`failed to process options: %w`, err)
}
ret, err := dc.DecryptMessage(buf)
if err != nil {
- return nil, decryptError{fmt.Errorf(`jwe.Decrypt: %w`, err)}
+ // DecryptMessage already returns errors prefixed with
+ // "jwe.Decrypt:" — wrap as decryptError without adding a
+ // second prefix, otherwise multi-recipient errors carry
+ // the "jwe.Decrypt:" string multiple times.
+ return nil, decryptError{err}
}
return ret, nil
}
@@ -947,18 +1300,18 @@ func Decrypt(buf []byte, options ...DecryptOption) ([]byte, error) {
// Parse parses the JWE message into a Message object. The JWE message
// can be either compact or full JSON format.
//
-// Parse() currently does not take any options, but the API accepts it
-// in anticipation of future addition.
+// Bounding the input size is the caller's responsibility; this function
+// trusts the caller-provided buf. See docs/13-input-size.md.
func Parse(buf []byte, _ ...ParseOption) (*Message, error) {
- return parseJSONOrCompact(buf, false)
+ return parseJSONOrCompact(buf, false, int(maxRecipients.Load()))
}
// errors are wrapped within this function, because we call it directly
// from Decrypt as well.
-func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool) (*Message, error) {
+func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool, maxR int) (*Message, error) {
buf = bytes.TrimSpace(buf)
if len(buf) == 0 {
- return nil, parseError{fmt.Errorf(`jwe.Parse: empty buffer`)}
+ return nil, makeParseError(`jwe.Parse`, `empty buffer`)
}
var msg *Message
@@ -970,29 +1323,38 @@ func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool) (*Message, error
}
if err != nil {
- return nil, parseError{fmt.Errorf(`jwe.Parse: %w`, err)}
+ return nil, makeParseError(`jwe.Parse`, `%w`, err)
}
+
+ if maxR > 0 && len(msg.recipients) > maxR {
+ return nil, makeParseError(`jwe.Parse`, `too many recipients in JWE message (%d > %d)`, len(msg.recipients), maxR)
+ }
+
return msg, nil
}
// ParseString is the same as Parse, but takes a string.
-func ParseString(s string) (*Message, error) {
+func ParseString(s string, _ ...ParseOption) (*Message, error) {
msg, err := Parse([]byte(s))
if err != nil {
- return nil, parseError{fmt.Errorf(`jwe.ParseString: %w`, err)}
+ return nil, makeParseError(`jwe.ParseString`, `%w`, err)
}
return msg, nil
}
// ParseReader is the same as Parse, but takes an io.Reader.
-func ParseReader(src io.Reader) (*Message, error) {
+//
+// Bounding the input size is the caller's responsibility: wrap src with
+// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See
+// docs/13-input-size.md for the rationale.
+func ParseReader(src io.Reader, _ ...ParseOption) (*Message, error) {
buf, err := io.ReadAll(src)
if err != nil {
- return nil, parseError{fmt.Errorf(`jwe.ParseReader: failed to read from io.Reader: %w`, err)}
+ return nil, makeParseError(`jwe.ParseReader`, `failed to read from io.Reader: %w`, err)
}
msg, err := Parse(buf)
if err != nil {
- return nil, parseError{fmt.Errorf(`jwe.ParseReader: %w`, err)}
+ return nil, makeParseError(`jwe.ParseReader`, `%w`, err)
}
return msg, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go
index ac07993176..a8fa825a57 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_asymmetric.go
@@ -113,8 +113,10 @@ func KeyDecryptRSA15(_, enckey []byte, privkeyif any, keysize int) ([]byte, erro
return nil, fmt.Errorf(`jwebb.KeyDecryptRSA15: %w`, err)
}
- // Perform some input validation.
- expectedlen := privkey.PublicKey.N.BitLen() / tokens.BitsPerByte
+ // Perform some input validation. Use privkey.Size() which applies
+ // ceiling division on the modulus bit length, avoiding silent truncation
+ // if N.BitLen() is not a multiple of 8.
+ expectedlen := privkey.Size()
if expectedlen != len(enckey) {
// Input size is incorrect, the encrypted payload should always match
// the size of the public modulus (e.g. using a 2048 bit key will
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go
index c09e30a34e..cb98801865 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_decrypt_symmetric.go
@@ -78,8 +78,11 @@ func KeyDecryptAESGCMKW(recipientKey, _ []byte, _ string, sharedkey []byte, iv [
return nil, fmt.Errorf(`failed to create new GCM wrap: %w`, err)
}
- // Combine recipient key and tag for GCM decryption
- ciphertext := recipientKey[:]
+ // Combine recipient key and tag for GCM decryption. Allocate a fresh
+ // buffer so we never alias into recipientKey's backing array, which
+ // is owned by the parsed message.
+ ciphertext := make([]byte, 0, len(recipientKey)+len(tag))
+ ciphertext = append(ciphertext, recipientKey...)
ciphertext = append(ciphertext, tag...)
jek, err := aesgcm.Open(nil, iv, ciphertext, nil)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go
index 6f008173c8..4dd6274863 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_asymmetric.go
@@ -65,9 +65,9 @@ func generateECDHESKeyECDSA(alg string, calg string, keysize uint32, pubkey *ecd
}
// generateECDHESKeyX25519 generates the key material for X25519 keys using ECDH-ES
-func generateECDHESKeyX25519(alg string, calg string, keysize uint32, pubkey *ecdh.PublicKey) (keygen.ByteWithECPublicKey, error) {
+func generateECDHESKeyX25519(alg string, calg string, keysize uint32, pubkey *ecdh.PublicKey, apu, apv []byte) (keygen.ByteWithECPublicKey, error) {
// Generate the key directly
- kg, err := keygen.X25519(alg, calg, int(keysize), pubkey)
+ kg, err := keygen.X25519(alg, calg, int(keysize), pubkey, apu, apv)
if err != nil {
return keygen.ByteWithECPublicKey{}, fmt.Errorf(`failed to generate X25519 key: %w`, err)
}
@@ -103,8 +103,8 @@ func KeyEncryptECDHESKeyWrapECDSA(cek []byte, alg string, apu, apv []byte, pubke
}
// KeyEncryptECDHESKeyWrapX25519 encrypts the CEK using ECDH-ES with key wrapping for X25519 keys
-func KeyEncryptECDHESKeyWrapX25519(cek []byte, alg string, _ []byte, _ []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) {
- bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey)
+func KeyEncryptECDHESKeyWrapX25519(cek []byte, alg string, apu []byte, apv []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) {
+ bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey, apu, apv)
if err != nil {
return nil, err
}
@@ -136,8 +136,8 @@ func KeyEncryptECDHESECDSA(_ []byte, alg string, apu, apv []byte, pubkey *ecdsa.
}
// KeyEncryptECDHESX25519 encrypts using ECDH-ES direct (no key wrapping) for X25519 keys
-func KeyEncryptECDHESX25519(_ []byte, alg string, _, _ []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) {
- bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey)
+func KeyEncryptECDHESX25519(_ []byte, alg string, apu, apv []byte, pubkey *ecdh.PublicKey, keysize uint32, calg string) (keygen.ByteSource, error) {
+ bwpk, err := generateECDHESKeyX25519(alg, calg, keysize, pubkey, apu, apv)
if err != nil {
return nil, err
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go
index d489aaba28..e2ee2766fa 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/key_encrypt_symmetric.go
@@ -35,8 +35,11 @@ func KeyEncryptDirect(_ []byte, _ string, sharedkey []byte) (keygen.ByteSource,
return keygen.ByteKey(sharedkey), nil
}
-// KeyEncryptPBES2 encrypts the CEK using PBES2 password-based encryption
-func KeyEncryptPBES2(cek []byte, alg string, password []byte) (keygen.ByteSource, error) {
+// KeyEncryptPBES2 encrypts the CEK using PBES2 password-based encryption.
+// count is the PBKDF2 iteration count. If count <= 0, tokens.PBES2DefaultIterations
+// is used as a safety fallback; public callers go through jwe.Encrypt / jwe.Settings
+// and always provide a positive value via the WithPBES2Count option.
+func KeyEncryptPBES2(cek []byte, alg string, password []byte, count int) (keygen.ByteSource, error) {
var hashFunc func() hash.Hash
var keylen int
@@ -54,7 +57,9 @@ func KeyEncryptPBES2(cek []byte, alg string, password []byte) (keygen.ByteSource
return nil, fmt.Errorf(`unsupported PBES2 algorithm: %s`, alg)
}
- count := tokens.PBES2DefaultIterations
+ if count <= 0 {
+ count = tokens.PBES2DefaultIterations
+ }
salt := make([]byte, keylen)
_, err := io.ReadFull(rand.Reader, salt)
if err != nil {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go
index 0792d6cb8e..b48bcec5f3 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/jwebb/keywrap.go
@@ -13,6 +13,9 @@ import (
var keywrapDefaultIV = []byte{0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6, 0xa6}
func Wrap(kek cipher.Block, cek []byte) ([]byte, error) {
+ if len(cek) < tokens.KeywrapChunkLen {
+ return nil, fmt.Errorf(`keywrap input must be at least %d bytes`, tokens.KeywrapChunkLen)
+ }
if len(cek)%tokens.KeywrapBlockSize != 0 {
return nil, fmt.Errorf(`keywrap input must be %d byte blocks`, tokens.KeywrapBlockSize)
}
@@ -25,15 +28,11 @@ func Wrap(kek cipher.Block, cek []byte) ([]byte, error) {
copy(r[i], cek[i*tokens.KeywrapChunkLen:])
}
- buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)
+ buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)[:tokens.KeywrapChunkLen*2]
defer pool.ByteSlice().Put(buffer)
- // the byte slice has the capacity, but len is 0
- buffer = buffer[:tokens.KeywrapChunkLen*2]
- tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)
+ tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)[:tokens.KeywrapChunkLen]
defer pool.ByteSlice().Put(tBytes)
- // the byte slice has the capacity, but len is 0
- tBytes = tBytes[:tokens.KeywrapChunkLen]
copy(buffer, keywrapDefaultIV)
@@ -60,6 +59,9 @@ func Wrap(kek cipher.Block, cek []byte) ([]byte, error) {
}
func Unwrap(block cipher.Block, ciphertxt []byte) ([]byte, error) {
+ if len(ciphertxt) < 2*tokens.KeywrapChunkLen {
+ return nil, fmt.Errorf(`keyunwrap input must be at least %d bytes`, 2*tokens.KeywrapChunkLen)
+ }
if len(ciphertxt)%tokens.KeywrapChunkLen != 0 {
return nil, fmt.Errorf(`keyunwrap input must be %d byte blocks`, tokens.KeywrapChunkLen)
}
@@ -72,15 +74,11 @@ func Unwrap(block cipher.Block, ciphertxt []byte) ([]byte, error) {
copy(r[i], ciphertxt[(i+1)*tokens.KeywrapChunkLen:])
}
- buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)
+ buffer := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen * 2)[:tokens.KeywrapChunkLen*2]
defer pool.ByteSlice().Put(buffer)
- // the byte slice has the capacity, but len is 0
- buffer = buffer[:tokens.KeywrapChunkLen*2]
- tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)
+ tBytes := pool.ByteSlice().GetCapacity(tokens.KeywrapChunkLen)[:tokens.KeywrapChunkLen]
defer pool.ByteSlice().Put(tBytes)
- // the byte slice has the capacity, but len is 0
- tBytes = tBytes[:tokens.KeywrapChunkLen]
copy(buffer[:tokens.KeywrapChunkLen], ciphertxt[:tokens.KeywrapChunkLen])
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go
index 05adc04517..81bb5b6ec7 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/key_provider.go
@@ -2,6 +2,7 @@ package jwe
import (
"context"
+ "errors"
"fmt"
"sync"
@@ -106,10 +107,11 @@ type keySetProvider struct {
requireKid bool
}
-func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, _ Recipient, _ *Message) error {
+func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, r Recipient, msg *Message) error {
if usage, ok := key.KeyUsage(); ok {
if usage != "" && usage != jwk.ForEncryption.String() {
- return nil
+ kid, _ := key.KeyID()
+ return fmt.Errorf(`key %q has key_use=%q (expected %q for encryption)`, kid, usage, jwk.ForEncryption.String())
}
}
@@ -123,7 +125,30 @@ func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, _ Recipient, _ *M
return nil
}
- return nil
+ // The JWK has no "alg" — common for IdP-published encryption keys.
+ // Fall back to the recipient's declared "alg" (per-recipient header,
+ // then protected header), matching the preference order used when
+ // jwe.Decrypt verifies the chosen key's algorithm against the message.
+ // jwe.Decrypt re-checks agreement before use, so trusting the header
+ // alg here does not widen the attack surface.
+ for _, hdr := range []Headers{r.Headers(), msg.ProtectedHeaders()} {
+ if hdr == nil {
+ continue
+ }
+ v, ok := hdr.Algorithm()
+ if !ok {
+ continue
+ }
+ kalg, ok := jwa.LookupKeyEncryptionAlgorithm(v.String())
+ if !ok {
+ continue
+ }
+ sink.Key(kalg, key)
+ return nil
+ }
+
+ kid, _ := key.KeyID()
+ return fmt.Errorf(`key %q in set has no "alg" field and the JWE message has no recoverable "alg" header; declare "alg" on the JWK or use jwe.WithKey(alg, key) directly`, kid)
}
func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient, msg *Message) error {
@@ -144,11 +169,22 @@ func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, r Recipient
return kp.selectKey(sink, key, r, msg)
}
+ // Collect per-key errors and surface them via errors.Join when
+ // nothing produced a usable (alg, key) pair. Without this, a
+ // caller debugging "why didn't my keyset match" got no signal.
+ var perKeyErrs []error
+ var emitted bool
for i := range kp.set.Len() {
key, _ := kp.set.Key(i)
- if err := kp.selectKey(sink, key, r, msg); err != nil {
+ err := kp.selectKey(sink, key, r, msg)
+ if err != nil {
+ perKeyErrs = append(perKeyErrs, err)
continue
}
+ emitted = true
+ }
+ if !emitted && len(perKeyErrs) > 0 {
+ return fmt.Errorf(`failed to select any usable key from set of %d (no key produced a usable (alg, key) pair): %w`, kp.set.Len(), errors.Join(perKeyErrs...))
}
return nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go
index 7aad833f26..22c6e6660a 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/message.go
@@ -1,6 +1,7 @@
package jwe
import (
+ "bytes"
"fmt"
"sort"
"strings"
@@ -71,8 +72,7 @@ func (r *stdRecipient) MarshalJSON() ([]byte, error) {
buf.WriteString(base64.EncodeToString(r.encryptedKey))
buf.WriteString(`"}`)
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
@@ -248,8 +248,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
if aad := m.AuthenticatedData(); len(aad) > 0 {
aad = base64.Encode(aad)
if encodedProtectedHeaders != nil {
- tmp := append(encodedProtectedHeaders, tokens.Period)
- aad = append(tmp, aad...)
+ aad = concatAAD(encodedProtectedHeaders, aad)
}
buf.Reset()
@@ -344,8 +343,7 @@ func (m *Message) MarshalJSON() ([]byte, error) {
}
fmt.Fprintf(buf, `}`)
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
@@ -422,29 +420,36 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
m.authenticatedData = v
}
- if src := proxy.CipherText; len(src) > 0 {
- v, err := base64.DecodeString(src)
- if err != nil {
- return fmt.Errorf(`failed to decode "ciphertext": %w`, err)
- }
- m.cipherText = v
+ // RFC 7516 §7.2: "ciphertext", "iv", and "tag" MUST be present and
+ // non-empty for any AEAD-protected JWE. Reject missing/empty values
+ // here so that a zero-length authentication tag cannot reach the
+ // AEAD verification code path.
+ if len(proxy.CipherText) == 0 {
+ return fmt.Errorf(`missing or empty "ciphertext" field`)
}
+ ctbuf, err := base64.DecodeString(proxy.CipherText)
+ if err != nil {
+ return fmt.Errorf(`failed to decode "ciphertext": %w`, err)
+ }
+ m.cipherText = ctbuf
- if src := proxy.InitializationVector; len(src) > 0 {
- v, err := base64.DecodeString(src)
- if err != nil {
- return fmt.Errorf(`failed to decode "iv": %w`, err)
- }
- m.initializationVector = v
+ if len(proxy.InitializationVector) == 0 {
+ return fmt.Errorf(`missing or empty "iv" field`)
}
+ ivbuf, err := base64.DecodeString(proxy.InitializationVector)
+ if err != nil {
+ return fmt.Errorf(`failed to decode "iv": %w`, err)
+ }
+ m.initializationVector = ivbuf
- if src := proxy.Tag; len(src) > 0 {
- v, err := base64.DecodeString(src)
- if err != nil {
- return fmt.Errorf(`failed to decode "tag": %w`, err)
- }
- m.tag = v
+ if len(proxy.Tag) == 0 {
+ return fmt.Errorf(`missing or empty "tag" field`)
}
+ tagbuf, err := base64.DecodeString(proxy.Tag)
+ if err != nil {
+ return fmt.Errorf(`failed to decode "tag": %w`, err)
+ }
+ m.tag = tagbuf
m.protectedHeaders = h
if m.storeProtectedHeaders {
@@ -554,7 +559,26 @@ func Compact(m *Message, _ ...CompactOption) ([]byte, error) {
buf.WriteByte(tokens.Period)
buf.Write(tag)
- result := make([]byte, buf.Len())
- copy(result, buf.Bytes())
+ result := bytes.Clone(buf.Bytes())
return result, nil
}
+
+// compactSerialize assembles a JWE compact serialization from pre-encoded
+// protected headers (aad) and raw binary fields. This avoids the redundant
+// Clone/Merge/Encode cycle that Compact() performs.
+func compactSerialize(aad, encryptedKey, iv, ciphertext, tag []byte) []byte {
+ size := len(aad) + base64.EncodedLen(len(encryptedKey)) + base64.EncodedLen(len(iv)) + base64.EncodedLen(len(ciphertext)) + base64.EncodedLen(len(tag)) + 4
+ buf := make([]byte, 0, size)
+
+ buf = append(buf, aad...)
+ buf = append(buf, tokens.Period)
+ buf = base64.AppendEncode(buf, encryptedKey)
+ buf = append(buf, tokens.Period)
+ buf = base64.AppendEncode(buf, iv)
+ buf = append(buf, tokens.Period)
+ buf = base64.AppendEncode(buf, ciphertext)
+ buf = append(buf, tokens.Period)
+ buf = base64.AppendEncode(buf, tag)
+
+ return buf
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go
index 0437ea8733..ab356f5d81 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.go
@@ -6,6 +6,66 @@ import (
"github.com/lestrrat-go/option/v2"
)
+type identCritExtension struct{}
+type identDisabledKeyAlgorithms struct{}
+
+// WithDisabledKeyAlgorithms returns a process-global option for jwe.Settings()
+// that refuses the named key encryption algorithms in both directions. After
+// the call returns, jwe.Encrypt() will not produce a recipient using any
+// listed algorithm, and jwe.Decrypt() will reject any recipient whose "alg"
+// is in the list, before any cryptographic work runs. The check fires per
+// recipient: a multi-recipient JWE is rejected as soon as a disabled "alg"
+// is seen on any recipient.
+//
+// The list is replaced (not unioned) on each Settings() call. To clear the
+// disabled set, call jwe.Settings(jwe.WithDisabledKeyAlgorithms()) with no
+// arguments.
+//
+// This is a deployment-time policy hook for the canonical "disable RSA1_5"
+// case (RFC 8725 §3.1) and similar legacy-algorithm bans. The jwa package
+// does not unregister these algorithms — keeping them registered preserves
+// header parsing for diagnostic logs, while this option blocks any actual
+// crypto use.
+func WithDisabledKeyAlgorithms(algorithms ...jwa.KeyEncryptionAlgorithm) GlobalOption {
+ return &globalOption{option.New(identDisabledKeyAlgorithms{}, algorithms)}
+}
+
+// WithCritExtension declares that the caller understands and will process
+// the named "crit" (Critical) header parameter extension(s) per RFC 7516
+// Section 4.1.13 (which references RFC 7515 Section 4.1.11). The option
+// is variadic and accumulating: a single call may register any number
+// of extension names, and the option may be passed multiple times to add
+// more.
+//
+// This option only takes effect when jwe.WithCritValidation(true) is
+// also passed. With validation enabled, jwe.Decrypt() rejects any JWE
+// whose protected header lists a "crit" extension that has not been
+// declared via this option, satisfying the RFC's requirement that
+// recipients MUST reject any "crit" extension they do not understand.
+//
+// IMPORTANT: declaring an extension here is a promise to the library
+// that the caller knows what the extension means and will perform any
+// validation, side effect, or policy enforcement the extension requires
+// AFTER jwe.Decrypt() returns successfully. The library cannot inspect
+// or enforce the semantics of an extension; it only checks that every
+// "crit" entry in the message has been declared. If you register an
+// extension and then forget to act on its value, you have effectively
+// disabled the protection the producer was trying to obtain by listing
+// the extension as critical.
+//
+// Concretely, the post-decrypt code path for a declared extension must:
+//
+// 1. Read the value of the named header from the decrypted message.
+// 2. Apply whatever check or transformation the extension specifies
+// (e.g. for an "x-tenant-binding" extension, refuse to act on the
+// payload unless the binding matches the current tenant).
+// 3. Treat any failure of that check as a decryption failure for
+// the application's purposes, even though jwe.Decrypt() returned
+// no error.
+func WithCritExtension(names ...string) DecryptOption {
+ return &decryptOption{option.New(identCritExtension{}, names)}
+}
+
// WithProtectedHeaders is used to specify contents of the protected header.
// Some fields such as "enc" and "zip" will be overwritten when encryption is
// performed.
@@ -35,12 +95,22 @@ func (*withKeySuboption) withKeySuboption() {}
// WithPerRecipientHeaders is used to pass header values for each recipient.
// Note that these headers are by definition _unprotected_.
+//
+// The supplied Headers is cloned before being stored in the option, so the
+// caller retains exclusive ownership of the original instance and the
+// library never mutates or pools it.
func WithPerRecipientHeaders(hdr Headers) WithKeySuboption {
+ if hdr != nil {
+ if cloned, err := hdr.Clone(); err == nil {
+ hdr = cloned
+ }
+ }
return &withKeySuboption{option.New(identPerRecipientHeaders{}, hdr)}
}
// WithKey is used to pass a static algorithm/key pair to either `jwe.Encrypt()` or `jwe.Decrypt()`.
-// either a raw key or `jwk.Key` may be passed as `key`.
+// Either a raw key or `jwk.Key` may be passed as `key`. If `key` is a `jwk.Key`,
+// it must export to one of the raw key types described below.
//
// The `alg` parameter is the identifier for the key encryption algorithm that should be used.
// It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.KeyEncryptionAlgorithm`
@@ -48,6 +118,30 @@ func WithPerRecipientHeaders(hdr Headers) WithKeySuboption {
// passed to the option. If you specify other algorithm types such as `jwa.SignatureAlgorithm`,
// then you will get an error when `jwe.Encrypt()` or `jwe.Decrypt()` is executed.
//
+// Built-in algorithm/key pairs are:
+//
+// - `jwa.RSA1_5()` and `jwa.RSA_OAEP*()`: `*rsa.PublicKey` for `jwe.Encrypt()`
+// and the matching `*rsa.PrivateKey` for `jwe.Decrypt()`
+// - `jwa.A128KW()`, `jwa.A192KW()`, `jwa.A256KW()`, `jwa.A128GCMKW()`,
+// `jwa.A192GCMKW()`, and `jwa.A256GCMKW()`: shared symmetric key bytes of
+// the size required by the selected algorithm
+// - `jwa.DIRECT()`: shared symmetric key bytes used as the CEK. The key length
+// must match the selected `enc`, and DIRECT supports only a single recipient
+// - `jwa.ECDH_ES()` and `jwa.ECDH_ES_A*KW()`: recipient public key for
+// `jwe.Encrypt()` and the matching private key for `jwe.Decrypt()`. Built-in
+// support accepts `*ecdsa.PublicKey`, `*ecdsa.PrivateKey`,
+// `*ecdh.PublicKey`, and `*ecdh.PrivateKey`; `jwa.ECDH_ES()` also supports
+// only a single recipient
+// - `jwa.PBES2_*()`: password bytes
+//
+// `jwa.RSA1_5()` is supported only for interoperability with legacy peers.
+// New applications should prefer an RSA-OAEP variant such as
+// `jwa.RSA_OAEP_256()` because PKCS#1 v1.5 decryption is exposed to
+// Bleichenbacher-style oracle attacks.
+//
+// Additional algorithms may be added by extension packages, but the key must
+// still match the contract for the selected `alg`.
+//
// Unlike `jwe.WithKeySet()`, the `kid` field does not need to match for the key
// to be tried.
func WithKey(alg jwa.KeyAlgorithm, key any, options ...WithKeySuboption) EncryptDecryptOption {
@@ -68,6 +162,24 @@ func WithKey(alg jwa.KeyAlgorithm, key any, options ...WithKeySuboption) Encrypt
})}
}
+// WithKeySet specifies a JWKS (jwk.Set) to use for decryption. The
+// recipient's `kid` header selects a key from the set, and the key's
+// `alg` (or, when the JWK lacks `alg`, the recipient's declared `alg`)
+// drives the decrypt-time dispatch.
+//
+// By default WithKeySet requires the JWE to carry a `kid` header that
+// matches a key in the set. Pass `WithRequireKid(false)` to fall back
+// to trying every key in the set (slower, looser; intended for legacy
+// peers that don't emit `kid`). Per-key errors from the set are
+// surfaced via `errors.Join` when nothing matched, so a caller
+// debugging "why didn't my keyset match" sees the per-key reasons.
+//
+// Security note: the recipient's per-recipient header is unprotected.
+// When the selected JWK has no `alg`, the keyset provider falls back
+// to the per-recipient `alg`, then the protected header's `alg`.
+// `jwe.Decrypt` re-checks `alg` against the integrity-protected
+// protected header before any cryptographic call (RFC 7516 §7.2.1
+// disjointness).
func WithKeySet(set jwk.Set, options ...WithKeySetSuboption) DecryptOption {
requireKid := true
for _, option := range options {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml
index 359d80944d..428e73e2b0 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options.yaml
@@ -10,6 +10,12 @@ interfaces:
methods:
- globalOption
- decryptOption
+ - name: GlobalEncryptOption
+ comment: |
+ GlobalEncryptOption describes options that changes global settings and for each call of the `jwe.Encrypt` function
+ methods:
+ - globalOption
+ - encryptOption
- name: CompactOption
comment: |
CompactOption describes options that can be passed to `jwe.Compact`
@@ -37,6 +43,13 @@ interfaces:
- readFileOption
comment: |
ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
+ - name: GlobalParseOption
+ methods:
+ - globalOption
+ - readFileOption
+ comment: |
+ GlobalParseOption describes an Option that can be passed to `jwe.Settings()`
+ and `jwe.ReadFile()`.
- name: ReadFileOption
comment: |
ReadFileOption is a type of `Option` that can be passed to `jwe.ReadFile`
@@ -76,6 +89,17 @@ options:
a payload using `jwe.Encrypt` (Yes, we know it can only be "" or "DEF",
but the way the specification is written it could allow for more options,
and therefore this option takes an argument)
+
+ Compression can leak information about the plaintext through message
+ length, so enable it only when you understand that tradeoff.
+
+ This library does not enforce an encrypt-side plaintext size limit before
+ compression. Callers that accept untrusted or arbitrarily large payloads
+ must bound the input size before calling `jwe.Encrypt` with this option.
+
+ Recipients may independently reject compressed messages whose
+ decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize`
+ setting.
- ident: ContentEncryptionAlgorithm
interface: EncryptOption
option_name: WithContentEncryption
@@ -140,14 +164,77 @@ options:
This option is currently considered EXPERIMENTAL, and is subject to
future changes across minor/micro versions.
- ident: MaxPBES2Count
- interface: GlobalOption
+ interface: GlobalDecryptOption
argument_type: int
comment: |
WithMaxPBES2Count specifies the maximum number of PBES2 iterations
to use when decrypting a message. If not specified, the default
value of 10,000 is used.
- This option has a global effect.
+ The cap is applied per recipient. RFC 7516 §5.3 allows each
+ recipient of a JSON-serialized JWE to carry its own "p2c" iteration
+ count in its per-recipient unprotected header, and jwe.Decrypt
+ honors that. For a JWE with N recipients, the worst-case PBKDF2
+ cost is therefore on the order of N * MaxPBES2Count iterations per
+ decrypt attempt. When accepting multi-recipient JSON JWEs from
+ untrusted senders, also clamp jwe.WithMaxRecipients.
+
+ This option can be used for `jwe.Settings()`, which changes the behavior
+ globally, or for `jwe.Decrypt()`, which changes the behavior for that
+ specific call.
+ - ident: MinPBES2Count
+ interface: GlobalDecryptOption
+ argument_type: int
+ comment: |
+ WithMinPBES2Count specifies the minimum number of PBES2 iterations
+ to accept when decrypting a message. If not specified, the default
+ value of 1,000 is used (RFC 7518 §4.8.1.2 floor). Set to 0 to
+ disable the minimum check (NOT RECOMMENDED for untrusted issuers
+ — without a floor, recipients accept arbitrarily-weak password-
+ derived keys and silently spend the producer-chosen amount of
+ crypto work to verify them).
+
+ Threat model: a malicious or careless issuer signs a PBES2-wrapped
+ JWE with a very low p2c (e.g. 100) so they spend almost no CPU on
+ their side, while the recipient happily derives the wrap key with
+ the same low iteration count. The result is an authenticated
+ message whose key-derivation strength is well below industry
+ practice (OWASP 2023 recommends ≥600,000 PBKDF2-SHA256 iterations
+ for password-derived keys; the RFC floor of 1,000 is far below
+ that). For receiver-side hardening against under-iteration, raise
+ WithMinPBES2Count above 1,000.
+
+ This option can be used for `jwe.Settings()`, which changes the behavior
+ globally, or for `jwe.Decrypt()`, which changes the behavior for that
+ specific call.
+ - ident: PBES2Count
+ interface: GlobalEncryptOption
+ argument_type: int
+ comment: |
+ WithPBES2Count specifies the number of PBKDF2 iterations to use when
+ encrypting a key with the PBES2 family of algorithms. If not specified,
+ the default value of 10,000 is used. Modern guidance (OWASP 2023)
+ recommends 600,000 or more for PBKDF2-HMAC-SHA256.
+
+ This option only affects encryption. Iteration counts on incoming
+ messages are validated separately on decrypt via WithMinPBES2Count
+ and WithMaxPBES2Count.
+
+ This option can be used for `jwe.Settings()`, which changes the behavior
+ globally, or for `jwe.Encrypt()`, which changes the behavior for that
+ specific call.
+ - ident: MaxRecipients
+ interface: GlobalDecryptOption
+ argument_type: int
+ comment: |
+ WithMaxRecipients specifies the maximum number of recipients allowed
+ in a JWE message. If a JWE message contains more recipients than this
+ value, parsing or decryption will return an error. The default value
+ is 100.
+
+ This option can be used for `jwe.Settings()`, which changes the behavior
+ globally, or for `jwe.Decrypt()`, which changes the behavior for that
+ specific call.
- ident: MaxDecompressBufferSize
interface: GlobalDecryptOption
argument_type: int64
@@ -157,6 +244,12 @@ options:
exceeds this amount when decompressed, jwe.Decrypt will return an error.
The default value is 10MB.
+ A non-positive value rejects every compressed JWE: the cap fires on
+ the first byte of inflated output, so any "zip"-compressed message
+ fails with an exceeds-cap error before any payload is delivered. Use
+ this when the deployment refuses to accept compressed JWEs at all.
+ Pass an explicit positive cap when compressed payloads are expected.
+
This option can be used for `jwe.Settings()`, which changes the behavior
globally, or for `jwe.Decrypt()`, which changes the behavior for that
specific call.
@@ -170,6 +263,39 @@ options:
In v2, this option was called MaxBufferSize.
This option has a global effect.
+ - ident: CritValidation
+ interface: DecryptOption
+ argument_type: bool
+ comment: |
+ WithCritValidation enables RFC 7516 Section 4.1.13 (via RFC 7515
+ Section 4.1.11) validation of the "crit" (Critical) header parameter
+ during decryption. The default is false, matching the behavior of
+ v3.0.13 and earlier (the "crit" header is silently ignored).
+
+ When enabled, jwe.Decrypt() will reject any JWE whose protected
+ header lists "crit" entries that the recipient has not declared
+ support for via jwe.WithCritExtension(). It will also reject
+ structurally invalid "crit" lists: empty arrays, duplicate names,
+ empty extension names, names of standard JOSE/JWE header parameters,
+ and names that do not appear as header parameters in the protected
+ header.
+
+ Per RFC 7516 Section 4.1.13 (referencing RFC 7515 Section 4.1.11),
+ recipients MUST reject a JWE whose "crit" list names extensions
+ they do not understand. Enabling this option together with one or
+ more jwe.WithCritExtension() calls is the only way to satisfy that
+ requirement with this library.
+
+ IMPORTANT: enabling this option makes the library check that every
+ "crit" entry has been declared via jwe.WithCritExtension(), but the
+ library cannot perform the actual extension-specific processing on
+ your behalf. After jwe.Decrypt() returns successfully, your code
+ MUST read each declared extension header and apply whatever check
+ or side effect the extension semantics demand. If you declare an
+ extension and then forget to act on its value, you have defeated
+ the protection the producer was trying to obtain by marking that
+ extension critical. See the documentation on jwe.WithCritExtension
+ for details.
- ident: LegacyHeaderMerging
interface: EncryptOption
argument_type: bool
@@ -207,4 +333,4 @@ options:
merged into the protected headers).
In future versions, the new behavior will be the default. New users are
- encouraged to set this option to `false` now to avoid future issues.
\ No newline at end of file
+ encouraged to set this option to `false` now to avoid future issues.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go
index 2d28eecb44..54962c5d04 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwe/options_gen.go
@@ -78,6 +78,21 @@ func (*globalDecryptOption) globalOption() {}
func (*globalDecryptOption) decryptOption() {}
+// GlobalEncryptOption describes options that changes global settings and for each call of the `jwe.Encrypt` function
+type GlobalEncryptOption interface {
+ Option
+ globalOption()
+ encryptOption()
+}
+
+type globalEncryptOption struct {
+ Option
+}
+
+func (*globalEncryptOption) globalOption() {}
+
+func (*globalEncryptOption) encryptOption() {}
+
// GlobalOption describes options that changes global settings for this package
type GlobalOption interface {
Option
@@ -90,6 +105,22 @@ type globalOption struct {
func (*globalOption) globalOption() {}
+// GlobalParseOption describes an Option that can be passed to `jwe.Settings()`
+// and `jwe.ReadFile()`.
+type GlobalParseOption interface {
+ Option
+ globalOption()
+ readFileOption()
+}
+
+type globalParseOption struct {
+ Option
+}
+
+func (*globalParseOption) globalOption() {}
+
+func (*globalParseOption) readFileOption() {}
+
// ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
type ParseOption interface {
Option
@@ -143,6 +174,7 @@ type identCEK struct{}
type identCompress struct{}
type identContentEncryptionAlgorithm struct{}
type identContext struct{}
+type identCritValidation struct{}
type identFS struct{}
type identKey struct{}
type identKeyProvider struct{}
@@ -150,8 +182,11 @@ type identKeyUsed struct{}
type identLegacyHeaderMerging struct{}
type identMaxDecompressBufferSize struct{}
type identMaxPBES2Count struct{}
+type identMaxRecipients struct{}
type identMergeProtectedHeaders struct{}
type identMessage struct{}
+type identMinPBES2Count struct{}
+type identPBES2Count struct{}
type identPerRecipientHeaders struct{}
type identPretty struct{}
type identProtectedHeaders struct{}
@@ -178,6 +213,10 @@ func (identContext) String() string {
return "WithContext"
}
+func (identCritValidation) String() string {
+ return "WithCritValidation"
+}
+
func (identFS) String() string {
return "WithFS"
}
@@ -206,6 +245,10 @@ func (identMaxPBES2Count) String() string {
return "WithMaxPBES2Count"
}
+func (identMaxRecipients) String() string {
+ return "WithMaxRecipients"
+}
+
func (identMergeProtectedHeaders) String() string {
return "WithMergeProtectedHeaders"
}
@@ -214,6 +257,14 @@ func (identMessage) String() string {
return "WithMessage"
}
+func (identMinPBES2Count) String() string {
+ return "WithMinPBES2Count"
+}
+
+func (identPBES2Count) String() string {
+ return "WithPBES2Count"
+}
+
func (identPerRecipientHeaders) String() string {
return "WithPerRecipientHeaders"
}
@@ -258,6 +309,17 @@ func WithCEK(v *[]byte) DecryptOption {
// a payload using `jwe.Encrypt` (Yes, we know it can only be "" or "DEF",
// but the way the specification is written it could allow for more options,
// and therefore this option takes an argument)
+//
+// Compression can leak information about the plaintext through message
+// length, so enable it only when you understand that tradeoff.
+//
+// This library does not enforce an encrypt-side plaintext size limit before
+// compression. Callers that accept untrusted or arbitrarily large payloads
+// must bound the input size before calling `jwe.Encrypt` with this option.
+//
+// Recipients may independently reject compressed messages whose
+// decompressed payload exceeds their `jwe.WithMaxDecompressBufferSize`
+// setting.
func WithCompress(v jwa.CompressionAlgorithm) EncryptOption {
return &encryptOption{option.New(identCompress{}, v)}
}
@@ -274,6 +336,39 @@ func WithContext(v context.Context) DecryptOption {
return &decryptOption{option.New(identContext{}, v)}
}
+// WithCritValidation enables RFC 7516 Section 4.1.13 (via RFC 7515
+// Section 4.1.11) validation of the "crit" (Critical) header parameter
+// during decryption. The default is false, matching the behavior of
+// v3.0.13 and earlier (the "crit" header is silently ignored).
+//
+// When enabled, jwe.Decrypt() will reject any JWE whose protected
+// header lists "crit" entries that the recipient has not declared
+// support for via jwe.WithCritExtension(). It will also reject
+// structurally invalid "crit" lists: empty arrays, duplicate names,
+// empty extension names, names of standard JOSE/JWE header parameters,
+// and names that do not appear as header parameters in the protected
+// header.
+//
+// Per RFC 7516 Section 4.1.13 (referencing RFC 7515 Section 4.1.11),
+// recipients MUST reject a JWE whose "crit" list names extensions
+// they do not understand. Enabling this option together with one or
+// more jwe.WithCritExtension() calls is the only way to satisfy that
+// requirement with this library.
+//
+// IMPORTANT: enabling this option makes the library check that every
+// "crit" entry has been declared via jwe.WithCritExtension(), but the
+// library cannot perform the actual extension-specific processing on
+// your behalf. After jwe.Decrypt() returns successfully, your code
+// MUST read each declared extension header and apply whatever check
+// or side effect the extension semantics demand. If you declare an
+// extension and then forget to act on its value, you have defeated
+// the protection the producer was trying to obtain by marking that
+// extension critical. See the documentation on jwe.WithCritExtension
+// for details.
+func WithCritValidation(v bool) DecryptOption {
+ return &decryptOption{option.New(identCritValidation{}, v)}
+}
+
// WithFS specifies the source `fs.FS` object to read the file from.
func WithFS(v fs.FS) ReadFileOption {
return &readFileOption{option.New(identFS{}, v)}
@@ -339,6 +434,12 @@ func WithLegacyHeaderMerging(v bool) EncryptOption {
// exceeds this amount when decompressed, jwe.Decrypt will return an error.
// The default value is 10MB.
//
+// A non-positive value rejects every compressed JWE: the cap fires on
+// the first byte of inflated output, so any "zip"-compressed message
+// fails with an exceeds-cap error before any payload is delivered. Use
+// this when the deployment refuses to accept compressed JWEs at all.
+// Pass an explicit positive cap when compressed payloads are expected.
+//
// This option can be used for `jwe.Settings()`, which changes the behavior
// globally, or for `jwe.Decrypt()`, which changes the behavior for that
// specific call.
@@ -350,9 +451,31 @@ func WithMaxDecompressBufferSize(v int64) GlobalDecryptOption {
// to use when decrypting a message. If not specified, the default
// value of 10,000 is used.
//
-// This option has a global effect.
-func WithMaxPBES2Count(v int) GlobalOption {
- return &globalOption{option.New(identMaxPBES2Count{}, v)}
+// The cap is applied per recipient. RFC 7516 §5.3 allows each
+// recipient of a JSON-serialized JWE to carry its own "p2c" iteration
+// count in its per-recipient unprotected header, and jwe.Decrypt
+// honors that. For a JWE with N recipients, the worst-case PBKDF2
+// cost is therefore on the order of N * MaxPBES2Count iterations per
+// decrypt attempt. When accepting multi-recipient JSON JWEs from
+// untrusted senders, also clamp jwe.WithMaxRecipients.
+//
+// This option can be used for `jwe.Settings()`, which changes the behavior
+// globally, or for `jwe.Decrypt()`, which changes the behavior for that
+// specific call.
+func WithMaxPBES2Count(v int) GlobalDecryptOption {
+ return &globalDecryptOption{option.New(identMaxPBES2Count{}, v)}
+}
+
+// WithMaxRecipients specifies the maximum number of recipients allowed
+// in a JWE message. If a JWE message contains more recipients than this
+// value, parsing or decryption will return an error. The default value
+// is 100.
+//
+// This option can be used for `jwe.Settings()`, which changes the behavior
+// globally, or for `jwe.Decrypt()`, which changes the behavior for that
+// specific call.
+func WithMaxRecipients(v int) GlobalDecryptOption {
+ return &globalDecryptOption{option.New(identMaxRecipients{}, v)}
}
// WithMergeProtectedHeaders specify that when given multiple headers
@@ -369,6 +492,47 @@ func WithMessage(v *Message) DecryptOption {
return &decryptOption{option.New(identMessage{}, v)}
}
+// WithMinPBES2Count specifies the minimum number of PBES2 iterations
+// to accept when decrypting a message. If not specified, the default
+// value of 1,000 is used (RFC 7518 §4.8.1.2 floor). Set to 0 to
+// disable the minimum check (NOT RECOMMENDED for untrusted issuers
+// — without a floor, recipients accept arbitrarily-weak password-
+// derived keys and silently spend the producer-chosen amount of
+// crypto work to verify them).
+//
+// Threat model: a malicious or careless issuer signs a PBES2-wrapped
+// JWE with a very low p2c (e.g. 100) so they spend almost no CPU on
+// their side, while the recipient happily derives the wrap key with
+// the same low iteration count. The result is an authenticated
+// message whose key-derivation strength is well below industry
+// practice (OWASP 2023 recommends ≥600,000 PBKDF2-SHA256 iterations
+// for password-derived keys; the RFC floor of 1,000 is far below
+// that). For receiver-side hardening against under-iteration, raise
+// WithMinPBES2Count above 1,000.
+//
+// This option can be used for `jwe.Settings()`, which changes the behavior
+// globally, or for `jwe.Decrypt()`, which changes the behavior for that
+// specific call.
+func WithMinPBES2Count(v int) GlobalDecryptOption {
+ return &globalDecryptOption{option.New(identMinPBES2Count{}, v)}
+}
+
+// WithPBES2Count specifies the number of PBKDF2 iterations to use when
+// encrypting a key with the PBES2 family of algorithms. If not specified,
+// the default value of 10,000 is used. Modern guidance (OWASP 2023)
+// recommends 600,000 or more for PBKDF2-HMAC-SHA256.
+//
+// This option only affects encryption. Iteration counts on incoming
+// messages are validated separately on decrypt via WithMinPBES2Count
+// and WithMaxPBES2Count.
+//
+// This option can be used for `jwe.Settings()`, which changes the behavior
+// globally, or for `jwe.Encrypt()`, which changes the behavior for that
+// specific call.
+func WithPBES2Count(v int) GlobalEncryptOption {
+ return &globalEncryptOption{option.New(identPBES2Count{}, v)}
+}
+
// WithPretty specifies whether the JSON output should be formatted and
// indented
func WithPretty(v bool) WithJSONSuboption {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel
index 8e82e1f009..1bcb93a319 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/BUILD.bazel
@@ -41,6 +41,7 @@ go_library(
"//internal/tokens",
"//jwa",
"//jwk/ecdsa",
+ "//jwk/internal/registry",
"//jwk/jwkbb",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
"@com_github_lestrrat_go_httprc_v3//:httprc",
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go
index 6d5b00f056..388c776a81 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/cache.go
@@ -81,14 +81,23 @@ type Cache struct {
// conjection with `httprc.NewResource` to create a `httprc.Resource` object
// to auto-update `jwk.Set` objects.
type Transformer struct {
- parseOptions []ParseOption
+ parseOptions []ParseOption
+ maxFetchBodySize int64
}
func (t Transformer) Transform(_ context.Context, res *http.Response) (Set, error) {
- buf, err := io.ReadAll(res.Body)
+ maxBodySize := t.maxFetchBodySize
+ if maxBodySize <= 0 {
+ maxBodySize = maxFetchBodySize.Load()
+ }
+
+ buf, err := io.ReadAll(io.LimitReader(res.Body, maxBodySize+1))
if err != nil {
return nil, fmt.Errorf(`failed to read response body status: %w`, err)
}
+ if int64(len(buf)) > maxBodySize {
+ return nil, fmt.Errorf(`response body at %q exceeded max size of %d bytes`, res.Request.URL.String(), maxBodySize)
+ }
set, err := Parse(buf, t.parseOptions...)
if err != nil {
@@ -121,10 +130,17 @@ func NewCache(ctx context.Context, client *httprc.Client) (*Cache, error) {
// Register registers a URL to be managed by the cache. URLs must
// be registered before issuing `Get`
//
-// The `Register` method is a thin wrapper around `(httprc.Controller).Add`
+// The `Register` method is a thin wrapper around `(httprc.Controller).Add`.
+//
+// As with `jwk.Fetch`, the default whitelist is `jwk.InsecureWhitelist{}`
+// — every URL is allowed. Supply `jwk.WithFetchWhitelist()` when the URL
+// originates from untrusted input. See `jwk.Fetch` for the full security
+// rationale.
func (c *Cache) Register(ctx context.Context, u string, options ...RegisterOption) error {
var parseOptions []ParseOption
var resourceOptions []httprc.NewResourceOption
+ var maxFetchBodySize int64
+ var hasHTTPClient bool
waitReady := true
for _, option := range options {
switch option := option.(type) {
@@ -144,16 +160,29 @@ func (c *Cache) Register(ctx context.Context, u string, options ...RegisterOptio
return fmt.Errorf(`failed to retrieve HTTPClient option value: %w`, err)
}
resourceOptions = append(resourceOptions, httprc.WithHTTPClient(cli))
+ hasHTTPClient = true
case identWaitReady{}:
if err := option.Value(&waitReady); err != nil {
return fmt.Errorf(`failed to retrieve WaitReady option value: %w`, err)
}
+ case identMaxFetchBodySize{}:
+ if err := option.Value(&maxFetchBodySize); err != nil {
+ return fmt.Errorf(`failed to retrieve MaxFetchBodySize option value: %w`, err)
+ }
}
}
}
+ // If no HTTP client was explicitly provided, use the library's default
+ // client which includes timeout and redirect protections. Without this,
+ // httprc would fall back to http.DefaultClient which has no such protections.
+ if !hasHTTPClient {
+ resourceOptions = append(resourceOptions, httprc.WithHTTPClient(getFetchHTTPClient()))
+ }
+
r, err := httprc.NewResource[Set](u, &Transformer{
- parseOptions: parseOptions,
+ parseOptions: parseOptions,
+ maxFetchBodySize: maxFetchBodySize,
}, resourceOptions...)
if err != nil {
return fmt.Errorf(`failed to create httprc.Resource: %w`, err)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go
index 057f4b02a0..4cf7bb6450 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/convert.go
@@ -14,7 +14,6 @@ import (
"github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
- "github.com/lestrrat-go/jwx/v3/jwa"
)
// # Converting between Raw Keys and `jwk.Key`s
@@ -23,33 +22,72 @@ import (
// A converter that converts from a `jwk.Key` to a raw key is called a KeyExporter.
var keyImporters = make(map[reflect.Type]KeyImporter)
-var keyExporters = make(map[jwa.KeyType][]KeyExporter)
+
+// KeyKind identifies a key for exporter dispatch. Built-in key types
+// use the key type string (e.g. "RSA", "EC", "OKP", "oct"). Keys that
+// implement KeyKinder can return a more specific identity
+// (e.g. "OKP:Ed448") to select a curve-specific exporter.
+type KeyKind string
+
+// KeyKinder is implemented by keys that need exporter dispatch
+// beyond just their key type. For example, OKP keys return
+// "OKP:" so that curve-specific exporters can be registered
+// by external modules.
+//
+// Keys that do not implement this interface are dispatched by
+// KeyType().String() alone.
+type KeyKinder interface {
+ KeyKind() KeyKind
+}
+
+var keyExporters = make(map[KeyKind][]KeyExporter)
var muKeyImporters sync.RWMutex
var muKeyExporters sync.RWMutex
-// RegisterKeyImporter registers a KeyImporter for the given raw key. When `jwk.Import()` is called,
-// the library will look up the appropriate KeyImporter for the given raw key type (via `reflect`)
-// and execute the KeyImporters in succession until either one of them succeeds, or all of them fail.
+// RegisterKeyImporter registers a KeyImporter for the given raw key.
+// When `jwk.Import()` is called, the library looks up the importer for
+// the given raw key type (via `reflect`) and executes it.
+//
+// Importer dispatch is single-valued per Go type: there is exactly
+// one importer registered per `reflect.TypeOf(from)`. Registering a
+// second importer for the same raw-key type silently replaces the
+// previous entry — including built-in importers for
+// `*rsa.PrivateKey`, `*ecdsa.PrivateKey`, and so on. Callers that
+// need to guard against accidental overwrites should keep track of
+// registrations themselves and avoid double-registration at init()
+// time.
+//
+// This deliberately differs from the stacking behavior of
+// [RegisterKeyExporter] (keyed by [KeyKind] strings) and
+// [RegisterKeyParser] (an untyped-JSON fallback chain); importer
+// dispatch is a single-value map keyed by Go type, with no
+// equivalent dimension to try next. v4 turns the overwrite into
+// an error; v3 keeps the frozen silent-overwrite behavior for
+// backward compatibility.
func RegisterKeyImporter(from any, conv KeyImporter) {
muKeyImporters.Lock()
defer muKeyImporters.Unlock()
keyImporters[reflect.TypeOf(from)] = conv
}
-// RegisterKeyExporter registers a KeyExporter for the given key type. When `key.Raw()` is called,
-// the library will look up the appropriate KeyExporter for the given key type and execute the
-// KeyExporters in succession until either one of them succeeds, or all of them fail.
-func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) {
+// RegisterKeyExporter registers a KeyExporter for the given key identity.
+// When `jwk.Export()` is called, the library first tries exporters registered
+// for the key's specific identity (via [KeyKinder]), then falls back to
+// exporters registered for the key type alone.
+//
+// For most key types, pass `KeyKind(kty.String())` (e.g. `KeyKind("RSA")`).
+// For curve-specific exporters, use a compound identity like `KeyKind("OKP:Ed448")`.
+func RegisterKeyExporter(ident KeyKind, conv KeyExporter) {
muKeyExporters.Lock()
defer muKeyExporters.Unlock()
- convs, ok := keyExporters[kty]
+ convs, ok := keyExporters[ident]
if !ok {
convs = []KeyExporter{conv}
} else {
convs = append([]KeyExporter{conv}, convs...)
}
- keyExporters[kty] = convs
+ keyExporters[ident] = convs
}
// KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`,
@@ -376,10 +414,12 @@ func Export(key Key, dst any) error {
if rv.Kind() != reflect.Ptr {
return fmt.Errorf(`jwk.Export: destination object must be a pointer`)
}
+
muKeyExporters.RLock()
- exporters, ok := keyExporters[key.KeyType()]
+ exporters := findExporters(key)
muKeyExporters.RUnlock()
- if !ok {
+
+ if len(exporters) == 0 {
return fmt.Errorf(`jwk.Export: no exporters registered for key type '%T'`, key)
}
for _, conv := range exporters {
@@ -397,3 +437,16 @@ func Export(key Key, dst any) error {
}
return fmt.Errorf(`jwk.Export: no suitable exporter found for key type '%T'`, key)
}
+
+// findExporters returns exporters for the key, trying the specific
+// KeyKind first, then falling back to the key type. Caller must
+// hold muKeyExporters.RLock.
+func findExporters(key Key) []KeyExporter {
+ if ki, ok := key.(KeyKinder); ok {
+ ident := ki.KeyKind()
+ if exporters, ok := keyExporters[ident]; ok {
+ return exporters
+ }
+ }
+ return keyExporters[KeyKind(key.KeyType().String())]
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go
index 7df707521b..b4f6e164e2 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/doc.go
@@ -22,7 +22,7 @@
//
// jwkKey, _ := jwk.Import(rsaPrivateKey)
// var rawKey *rsa.PRrivateKey
-// jwkKey.Raw(&rawKey)
+// jwk.Export(jwkKey, &rawKey)
//
// You can use them to sign/verify/encrypt/decrypt:
//
@@ -177,9 +177,9 @@
// var hint string
// if err := probe.Get("MyHint", &hint); err != nil {
// // if it doesn't have the `my_hint` field, it probably means
-// // it's not for us, so we return ContinueParseError so that
+// // it's not for us, so we return ContinueError so that
// // the next parser can pick it up
-// return nil, jwk.ContinueParseError()
+// return nil, jwk.ContinueError()
// }
//
// // Use hint to determine concrete key type
@@ -190,7 +190,7 @@
// ...
// }
//
-// return unmarshaler.Unmarshal(data, key)
+// return unmarshaler.UnmarshalKey(data, key)
// }
//
// ## Registering KeyImporter/KeyExporter
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go
index 8f76d0508e..7d246afdb3 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa.go
@@ -20,7 +20,7 @@ func init() {
ourecdsa.RegisterCurve(jwa.P384(), elliptic.P384())
ourecdsa.RegisterCurve(jwa.P521(), elliptic.P521())
- RegisterKeyExporter(jwa.EC(), KeyExportFunc(ecdsaJWKToRaw))
+ RegisterKeyExporter(KeyKind(jwa.EC().String()), KeyExportFunc(ecdsaJWKToRaw))
}
func (k *ecdsaPublicKey) Import(rawKey *ecdsa.PublicKey) error {
@@ -35,6 +35,10 @@ func (k *ecdsaPublicKey) Import(rawKey *ecdsa.PublicKey) error {
return fmt.Errorf(`invalid ecdsa.PublicKey`)
}
+ if err := validateECDSAPoint(rawKey.Curve, rawKey.X, rawKey.Y); err != nil {
+ return fmt.Errorf(`jwk: %w`, err)
+ }
+
xbuf := ecutil.AllocECPointBuffer(rawKey.X, rawKey.Curve)
ybuf := ecutil.AllocECPointBuffer(rawKey.Y, rawKey.Curve)
defer ecutil.ReleaseECPointBuffer(xbuf)
@@ -68,6 +72,10 @@ func (k *ecdsaPrivateKey) Import(rawKey *ecdsa.PrivateKey) error {
return fmt.Errorf(`invalid ecdsa.PrivateKey`)
}
+ if err := validateECDSAPoint(rawKey.Curve, rawKey.PublicKey.X, rawKey.PublicKey.Y); err != nil {
+ return fmt.Errorf(`jwk: %w`, err)
+ }
+
xbuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.X, rawKey.Curve)
ybuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.Y, rawKey.Curve)
dbuf := ecutil.AllocECPointBuffer(rawKey.D, rawKey.Curve)
@@ -101,9 +109,121 @@ func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ec
x.SetBytes(xbuf)
y.SetBytes(ybuf)
+ if err := validateECDSAPoint(crv, &x, &y); err != nil {
+ return nil, fmt.Errorf(`jwk: %w`, err)
+ }
+
return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil
}
+// validateECDSAPoint rejects ECDSA public key coordinates that are not
+// safe to use: the identity point (0, 0) and any point that does not lie
+// on the named curve. Without these checks, attacker-supplied JWKs can
+// smuggle off-curve or small-subgroup points into downstream ECDSA/ECDH
+// operations (invalid-curve attacks). See JWK-003.
+//
+// The implementation is split into two branches for a reason:
+//
+// 1. For the NIST P-256/P-384/P-521 curves we route through crypto/ecdh.
+// Go 1.21 deprecated most of crypto/elliptic's Curve methods — not
+// because point-on-curve validation stopped being necessary, but
+// because the generic big.Int implementation in crypto/elliptic had
+// subtle edge cases and the Go team wanted users off it. The blessed
+// replacement for "parse and validate an uncompressed point" on
+// stdlib curves is ecdh.Curve.NewPublicKey, which enforces on-curve
+// membership and rejects the identity as part of parsing the SEC1
+// 0x04 || X || Y encoding. Using ecdh here means we're using exactly
+// the Go team's recommended replacement, and the deprecated stdlib
+// elliptic methods are never reached for any NIST-curve input.
+//
+// 2. For any other curve registered through jwk/ecdsa.RegisterCurve
+// (most importantly secp256k1 via the ES256K extension module),
+// crypto/ecdh has no entry point — it only supports the four curves
+// listed above. The only mechanism available for validating a point
+// on a custom curve is the elliptic.Curve interface's IsOnCurve
+// method. Calling it here is correct despite the staticcheck
+// deprecation notice, for three reasons:
+//
+// a. The deprecation targets the *stdlib* elliptic.Curve
+// implementations (elliptic.P256() etc.). Custom curves such as
+// btcec/secp256k1 ship their own IsOnCurve implementation; the
+// interface dispatch lands in that implementation, not in the
+// deprecated stdlib one. staticcheck cannot see through interface
+// dispatch, so the lint scope is suppressed on just this line.
+//
+// b. The elliptic.Curve interface itself remains part of Go's
+// supported API because crypto/ecdsa.Verify and
+// crypto/ecdsa.Sign continue to take elliptic.Curve-backed keys.
+// Any third-party curve that plugs into crypto/ecdsa is
+// contractually required to implement a working IsOnCurve; that
+// is the only thing crypto/ecdsa has to validate the public point
+// before verification. Calling it from here is the same contract.
+//
+// c. The remaining alternatives are worse: (i) refusing to validate
+// non-stdlib curves at all reintroduces JWK-003 for ES256K users;
+// (ii) refusing to *support* non-stdlib curves is a regression
+// for ES256K users. A cleaner long-term fix is to extend
+// jwk/ecdsa.RegisterCurve so extension modules can register a
+// validator function alongside the curve, letting us drop the
+// IsOnCurve call entirely. That is a deliberate follow-up, not a
+// blocker for this security fix.
+func validateECDSAPoint(crv elliptic.Curve, x, y *big.Int) error {
+ if x.Sign() == 0 && y.Sign() == 0 {
+ return fmt.Errorf(`invalid ECDSA public key: identity point is not a valid public key`)
+ }
+
+ // Coordinates must fit in the curve's field. The NIST P-curve
+ // branch below pads x and y into a fixed-size SEC1 buffer via
+ // big.Int.FillBytes, which panics on oversized input. Bounding
+ // here makes the function safe by construction for every caller,
+ // including jwk.Import handed a hand-built *ecdsa.PublicKey from
+ // raw bytes.
+ bits := crv.Params().BitSize
+ if x.BitLen() > bits {
+ return fmt.Errorf(`invalid ECDSA public key: x coordinate is %d bits, exceeds curve %q field size of %d bits`, x.BitLen(), crv.Params().Name, bits)
+ }
+ if y.BitLen() > bits {
+ return fmt.Errorf(`invalid ECDSA public key: y coordinate is %d bits, exceeds curve %q field size of %d bits`, y.BitLen(), crv.Params().Name, bits)
+ }
+
+ if ecdhCrv, ok := stdlibECDHCurve(crv); ok {
+ size := (crv.Params().BitSize + 7) / 8
+ buf := make([]byte, 1+2*size)
+ buf[0] = 0x04
+ x.FillBytes(buf[1 : 1+size])
+ y.FillBytes(buf[1+size:])
+ if _, err := ecdhCrv.NewPublicKey(buf); err != nil {
+ return fmt.Errorf(`invalid ECDSA public key: %w`, err)
+ }
+ return nil
+ }
+
+ // Custom-curve fallback. See the block comment on validateECDSAPoint
+ // for the full justification of calling a deprecated-marked method;
+ // the short version is that interface dispatch lands in the custom
+ // curve's own IsOnCurve, not in deprecated stdlib code.
+ if !crv.IsOnCurve(x, y) { //nolint:staticcheck // see validateECDSAPoint godoc: only path that validates custom curves
+ return fmt.Errorf(`invalid ECDSA public key: point is not on curve %q`, crv.Params().Name)
+ }
+ return nil
+}
+
+// stdlibECDHCurve maps a crypto/elliptic curve to its crypto/ecdh
+// counterpart when one exists. Only the NIST P-curves supported by both
+// packages are mapped; everything else returns ok=false and falls back
+// to the elliptic.Curve path in validateECDSAPoint.
+func stdlibECDHCurve(crv elliptic.Curve) (ecdh.Curve, bool) {
+ switch crv {
+ case elliptic.P256():
+ return ecdh.P256(), true
+ case elliptic.P384():
+ return ecdh.P384(), true
+ case elliptic.P521():
+ return ecdh.P521(), true
+ }
+ return nil, false
+}
+
func buildECDHPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdh.PublicKey, error) {
var ecdhcrv ecdh.Curve
switch alg {
@@ -170,34 +290,64 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
}
}
- locker, ok := k.(rlocker)
- if ok {
+ // rlocker is unexported with unexported methods, so only our
+ // concrete types implement it. A successful assertion lets us
+ // type-assert to the concrete struct and read fields directly
+ // under a single batch lock. This avoids nested RLock (which
+ // deadlocks when a writer is pending) while preserving an
+ // atomic snapshot of all fields.
+ var crv jwa.EllipticCurveAlgorithm
+ var hasCrv bool
+ var od, ox, oy []byte
+ if locker, ok := k.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := k.(*ecdsaPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ if concrete.crv != nil {
+ crv = *(concrete.crv)
+ hasCrv = true
+ }
+ od, ox, oy = concrete.d, concrete.x, concrete.y
+ locker.runlock()
+ } else {
+ // External implementation — use self-locking interface getters.
+ var ok bool
+ if crv, ok = k.Crv(); !ok {
+ return nil, fmt.Errorf(`missing "crv" field`)
+ }
+ hasCrv = true
+ if od, ok = k.D(); !ok {
+ return nil, fmt.Errorf(`missing "d" field`)
+ }
+ if ox, ok = k.X(); !ok {
+ return nil, fmt.Errorf(`missing "x" field`)
+ }
+ if oy, ok = k.Y(); !ok {
+ return nil, fmt.Errorf(`missing "y" field`)
+ }
}
- crv, ok := k.Crv()
- if !ok {
+ if !hasCrv {
return nil, fmt.Errorf(`missing "crv" field`)
}
if isECDH {
- d, ok := k.D()
- if !ok {
+ if od == nil {
return nil, fmt.Errorf(`missing "d" field`)
}
- return buildECDHPrivateKey(crv, d)
+ return buildECDHPrivateKey(crv, od)
}
- x, ok := k.X()
- if !ok {
+ if ox == nil {
return nil, fmt.Errorf(`missing "x" field`)
}
- y, ok := k.Y()
- if !ok {
+ if oy == nil {
return nil, fmt.Errorf(`missing "y" field`)
}
- pubk, err := buildECDSAPublicKey(crv, x, y)
+ if od == nil {
+ return nil, fmt.Errorf(`missing "d" field`)
+ }
+
+ pubk, err := buildECDSAPublicKey(crv, ox, oy)
if err != nil {
return nil, fmt.Errorf(`failed to build public key: %w`, err)
}
@@ -205,12 +355,7 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
var key ecdsa.PrivateKey
var d big.Int
- origD, ok := k.D()
- if !ok {
- return nil, fmt.Errorf(`missing "d" field`)
- }
-
- d.SetBytes(origD)
+ d.SetBytes(od)
key.D = &d
key.PublicKey = *pubk
@@ -231,24 +376,40 @@ func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
}
}
- locker, ok := k.(rlocker)
- if ok {
+ // See ECDSAPrivateKey case above for explanation of the rlocker pattern.
+ var crv jwa.EllipticCurveAlgorithm
+ var hasCrv bool
+ var x, y []byte
+ if locker, ok := k.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := k.(*ecdsaPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ if concrete.crv != nil {
+ crv = *(concrete.crv)
+ hasCrv = true
+ }
+ x, y = concrete.x, concrete.y
+ locker.runlock()
+ } else {
+ var ok bool
+ if crv, ok = k.Crv(); !ok {
+ return nil, fmt.Errorf(`missing "crv" field`)
+ }
+ hasCrv = true
+ if x, ok = k.X(); !ok {
+ return nil, fmt.Errorf(`missing "x" field`)
+ }
+ if y, ok = k.Y(); !ok {
+ return nil, fmt.Errorf(`missing "y" field`)
+ }
}
- crv, ok := k.Crv()
- if !ok {
+ if !hasCrv {
return nil, fmt.Errorf(`missing "crv" field`)
}
-
- x, ok := k.X()
- if !ok {
+ if x == nil {
return nil, fmt.Errorf(`missing "x" field`)
}
-
- y, ok := k.Y()
- if !ok {
+ if y == nil {
return nil, fmt.Errorf(`missing "y" field`)
}
if isECDH {
@@ -305,12 +466,12 @@ func ecdsaThumbprint(hash crypto.Hash, crv, x, y string) []byte {
// Thumbprint returns the JWK thumbprint using the indicated
// hashing algorithm, according to RFC 7638
-func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
var key ecdsa.PublicKey
- if err := Export(&k, &key); err != nil {
+ if err := Export(k, &key); err != nil {
return nil, fmt.Errorf(`failed to export ecdsa.PublicKey for thumbprint generation: %w`, err)
}
@@ -329,12 +490,12 @@ func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
// Thumbprint returns the JWK thumbprint using the indicated
// hashing algorithm, according to RFC 7638
-func (k ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
var key ecdsa.PrivateKey
- if err := Export(&k, &key); err != nil {
+ if err := Export(k, &key); err != nil {
return nil, fmt.Errorf(`failed to export ecdsa.PrivateKey for thumbprint generation: %w`, err)
}
@@ -367,12 +528,21 @@ func ecdsaValidateKey(k interface {
}
keySize := ecutil.CalculateKeySize(crv)
- if x, ok := k.X(); !ok || len(x) != keySize {
- return fmt.Errorf(`invalid "x" length (%d) for curve %q`, len(x), crv.Params().Name)
+ xbuf, ok := k.X()
+ if !ok || len(xbuf) != keySize {
+ return fmt.Errorf(`invalid "x" length (%d) for curve %q`, len(xbuf), crv.Params().Name)
}
- if y, ok := k.Y(); !ok || len(y) != keySize {
- return fmt.Errorf(`invalid "y" length (%d) for curve %q`, len(y), crv.Params().Name)
+ ybuf, ok := k.Y()
+ if !ok || len(ybuf) != keySize {
+ return fmt.Errorf(`invalid "y" length (%d) for curve %q`, len(ybuf), crv.Params().Name)
+ }
+
+ var x, y big.Int
+ x.SetBytes(xbuf)
+ y.SetBytes(ybuf)
+ if err := validateECDSAPoint(crv, &x, &y); err != nil {
+ return err
}
if checkPrivate {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go
index 3392483218..ef0338413c 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa/ecdsa.go
@@ -45,16 +45,22 @@ func rebuildCurves() {
}
}
-// Algorithms returns the list of registered jwa.EllipticCurveAlgorithms
-// that ca be used for ECDSA keys.
+// Algorithms returns a snapshot of the registered
+// jwa.EllipticCurveAlgorithms that can be used for ECDSA keys.
+//
+// The returned slice is caller-owned. Modifying it does not affect the
+// package registry, and ordering is unspecified.
func Algorithms() []jwa.EllipticCurveAlgorithm {
muCurves.RLock()
defer muCurves.RUnlock()
- return algList
+ return append([]jwa.EllipticCurveAlgorithm(nil), algList...)
}
func AlgorithmFromCurve(crv elliptic.Curve) (jwa.EllipticCurveAlgorithm, error) {
+ muCurves.RLock()
+ defer muCurves.RUnlock()
+
alg, ok := curveToAlgMap[crv]
if !ok {
return jwa.InvalidEllipticCurve(), fmt.Errorf(`unknown elliptic curve: %q`, crv)
@@ -63,6 +69,9 @@ func AlgorithmFromCurve(crv elliptic.Curve) (jwa.EllipticCurveAlgorithm, error)
}
func CurveFromAlgorithm(alg jwa.EllipticCurveAlgorithm) (elliptic.Curve, error) {
+ muCurves.RLock()
+ defer muCurves.RUnlock()
+
crv, ok := algToCurveMap[alg]
if !ok {
return nil, fmt.Errorf(`unknown elliptic curve algorithm: %q`, alg)
@@ -71,6 +80,9 @@ func CurveFromAlgorithm(alg jwa.EllipticCurveAlgorithm) (elliptic.Curve, error)
}
func IsCurveAvailable(alg jwa.EllipticCurveAlgorithm) bool {
+ muCurves.RLock()
+ defer muCurves.RUnlock()
+
_, ok := algToCurveMap[alg]
return ok
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go
index 39536de3d8..a717e24bb9 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/ecdsa_gen.go
@@ -15,6 +15,7 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk/internal/registry"
)
const (
@@ -44,7 +45,7 @@ type ecdsaPublicKey struct {
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
y []byte
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -53,28 +54,29 @@ var _ Key = &ecdsaPublicKey{}
func newECDSAPublicKey() *ecdsaPublicKey {
return &ecdsaPublicKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h ecdsaPublicKey) KeyType() jwa.KeyType {
+func (h *ecdsaPublicKey) KeyType() jwa.KeyType {
return jwa.EC()
}
-func (h ecdsaPublicKey) rlock() {
+func (h *ecdsaPublicKey) rlock() {
h.mu.RLock()
}
-func (h ecdsaPublicKey) runlock() {
+func (h *ecdsaPublicKey) runlock() {
h.mu.RUnlock()
}
-func (h ecdsaPublicKey) IsPrivate() bool {
+func (h *ecdsaPublicKey) IsPrivate() bool {
return false
}
func (h *ecdsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -82,6 +84,8 @@ func (h *ecdsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *ecdsaPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.crv != nil {
return *(h.crv), true
}
@@ -89,6 +93,8 @@ func (h *ecdsaPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
}
func (h *ecdsaPublicKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -96,6 +102,8 @@ func (h *ecdsaPublicKey) KeyID() (string, bool) {
}
func (h *ecdsaPublicKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -103,6 +111,8 @@ func (h *ecdsaPublicKey) KeyOps() (KeyOperationList, bool) {
}
func (h *ecdsaPublicKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -110,6 +120,8 @@ func (h *ecdsaPublicKey) KeyUsage() (string, bool) {
}
func (h *ecdsaPublicKey) X() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x != nil {
return h.x, true
}
@@ -117,10 +129,17 @@ func (h *ecdsaPublicKey) X() ([]byte, bool) {
}
func (h *ecdsaPublicKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *ecdsaPublicKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -128,6 +147,8 @@ func (h *ecdsaPublicKey) X509CertThumbprint() (string, bool) {
}
func (h *ecdsaPublicKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -135,6 +156,8 @@ func (h *ecdsaPublicKey) X509CertThumbprintS256() (string, bool) {
}
func (h *ecdsaPublicKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -142,6 +165,8 @@ func (h *ecdsaPublicKey) X509URL() (string, bool) {
}
func (h *ecdsaPublicKey) Y() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.y != nil {
return h.y, true
}
@@ -348,7 +373,12 @@ func (h *ecdsaPublicKey) setNoLock(name string, value any) error {
}
case ECDSAXKey:
if v, ok := value.([]byte); ok {
- h.x = v
+ if v == nil {
+ h.x = nil
+ } else {
+ h.x = make([]byte, len(v))
+ copy(h.x, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, ECDSAXKey, value)
@@ -378,7 +408,12 @@ func (h *ecdsaPublicKey) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value)
case ECDSAYKey:
if v, ok := value.([]byte); ok {
- h.y = v
+ if v == nil {
+ h.y = nil
+ } else {
+ h.y = make([]byte, len(v))
+ copy(h.y, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, ECDSAYKey, value)
@@ -476,7 +511,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -500,7 +535,7 @@ LOOP:
}
h.crv = &decoded
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -510,7 +545,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case ECDSAXKey:
@@ -524,15 +559,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
case ECDSAYKey:
@@ -549,7 +584,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -572,88 +607,142 @@ LOOP:
return nil
}
-func (h ecdsaPublicKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 11)
- data[KeyTypeKey] = jwa.EC()
- fields = append(fields, KeyTypeKey)
+func (h *ecdsaPublicKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.EC())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.crv != nil {
- data[ECDSACrvKey] = *(h.crv)
- fields = append(fields, ECDSACrvKey)
+ v, err := json.Marshal(*(h.crv))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSACrvKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSACrvKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.x != nil {
- data[ECDSAXKey] = h.x
- fields = append(fields, ECDSAXKey)
+ v, err := json.Marshal(base64.EncodeToString(h.x))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAXKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSAXKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
if h.y != nil {
- data[ECDSAYKey] = h.y
- fields = append(fields, ECDSAYKey)
+ v, err := json.Marshal(base64.EncodeToString(h.y))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAYKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSAYKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *ecdsaPublicKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -723,7 +812,7 @@ type ecdsaPrivateKey struct {
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
y []byte
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -732,28 +821,29 @@ var _ Key = &ecdsaPrivateKey{}
func newECDSAPrivateKey() *ecdsaPrivateKey {
return &ecdsaPrivateKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h ecdsaPrivateKey) KeyType() jwa.KeyType {
+func (h *ecdsaPrivateKey) KeyType() jwa.KeyType {
return jwa.EC()
}
-func (h ecdsaPrivateKey) rlock() {
+func (h *ecdsaPrivateKey) rlock() {
h.mu.RLock()
}
-func (h ecdsaPrivateKey) runlock() {
+func (h *ecdsaPrivateKey) runlock() {
h.mu.RUnlock()
}
-func (h ecdsaPrivateKey) IsPrivate() bool {
+func (h *ecdsaPrivateKey) IsPrivate() bool {
return true
}
func (h *ecdsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -761,6 +851,8 @@ func (h *ecdsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *ecdsaPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.crv != nil {
return *(h.crv), true
}
@@ -768,6 +860,8 @@ func (h *ecdsaPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
}
func (h *ecdsaPrivateKey) D() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.d != nil {
return h.d, true
}
@@ -775,6 +869,8 @@ func (h *ecdsaPrivateKey) D() ([]byte, bool) {
}
func (h *ecdsaPrivateKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -782,6 +878,8 @@ func (h *ecdsaPrivateKey) KeyID() (string, bool) {
}
func (h *ecdsaPrivateKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -789,6 +887,8 @@ func (h *ecdsaPrivateKey) KeyOps() (KeyOperationList, bool) {
}
func (h *ecdsaPrivateKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -796,6 +896,8 @@ func (h *ecdsaPrivateKey) KeyUsage() (string, bool) {
}
func (h *ecdsaPrivateKey) X() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x != nil {
return h.x, true
}
@@ -803,10 +905,17 @@ func (h *ecdsaPrivateKey) X() ([]byte, bool) {
}
func (h *ecdsaPrivateKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *ecdsaPrivateKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -814,6 +923,8 @@ func (h *ecdsaPrivateKey) X509CertThumbprint() (string, bool) {
}
func (h *ecdsaPrivateKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -821,6 +932,8 @@ func (h *ecdsaPrivateKey) X509CertThumbprintS256() (string, bool) {
}
func (h *ecdsaPrivateKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -828,6 +941,8 @@ func (h *ecdsaPrivateKey) X509URL() (string, bool) {
}
func (h *ecdsaPrivateKey) Y() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.y != nil {
return h.y, true
}
@@ -1016,7 +1131,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, ECDSACrvKey, value)
case ECDSADKey:
if v, ok := value.([]byte); ok {
- h.d = v
+ if v == nil {
+ h.d = nil
+ } else {
+ h.d = make([]byte, len(v))
+ copy(h.d, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, ECDSADKey, value)
@@ -1050,7 +1170,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error {
}
case ECDSAXKey:
if v, ok := value.([]byte); ok {
- h.x = v
+ if v == nil {
+ h.x = nil
+ } else {
+ h.x = make([]byte, len(v))
+ copy(h.x, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, ECDSAXKey, value)
@@ -1080,7 +1205,12 @@ func (h *ecdsaPrivateKey) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value)
case ECDSAYKey:
if v, ok := value.([]byte); ok {
- h.y = v
+ if v == nil {
+ h.y = nil
+ } else {
+ h.y = make([]byte, len(v))
+ copy(h.y, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, ECDSAYKey, value)
@@ -1147,7 +1277,7 @@ func (k *ecdsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
k.dc = dc
}
-func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) error {
+func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) {
h.mu.Lock()
defer h.mu.Unlock()
h.algorithm = nil
@@ -1162,6 +1292,16 @@ func (h *ecdsaPrivateKey) UnmarshalJSON(buf []byte) error {
h.x509CertThumbprintS256 = nil
h.x509URL = nil
h.y = nil
+ defer func() {
+ if retErr != nil {
+ clear(h.d)
+ h.d = nil
+ clear(h.x)
+ h.x = nil
+ clear(h.y)
+ h.y = nil
+ }
+ }()
dec := json.NewDecoder(bytes.NewReader(buf))
LOOP:
for {
@@ -1181,7 +1321,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -1209,7 +1349,7 @@ LOOP:
return fmt.Errorf(`failed to decode value for key %s: %w`, ECDSADKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -1219,7 +1359,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case ECDSAXKey:
@@ -1233,15 +1373,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
case ECDSAYKey:
@@ -1258,7 +1398,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -1284,92 +1424,149 @@ LOOP:
return nil
}
-func (h ecdsaPrivateKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 12)
- data[KeyTypeKey] = jwa.EC()
- fields = append(fields, KeyTypeKey)
+func (h *ecdsaPrivateKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.EC())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.crv != nil {
- data[ECDSACrvKey] = *(h.crv)
- fields = append(fields, ECDSACrvKey)
+ v, err := json.Marshal(*(h.crv))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSACrvKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSACrvKey, Value: v})
}
if h.d != nil {
- data[ECDSADKey] = h.d
- fields = append(fields, ECDSADKey)
+ v, err := json.Marshal(base64.EncodeToString(h.d))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSADKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSADKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.x != nil {
- data[ECDSAXKey] = h.x
- fields = append(fields, ECDSAXKey)
+ v, err := json.Marshal(base64.EncodeToString(h.x))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAXKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSAXKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
if h.y != nil {
- data[ECDSAYKey] = h.y
- fields = append(fields, ECDSAYKey)
+ v, err := json.Marshal(base64.EncodeToString(h.y))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ECDSAYKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: ECDSAYKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *ecdsaPrivateKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -1430,3 +1627,10 @@ func init() {
func ECDSAStandardFieldsFilter() KeyFilter {
return ecdsaStandardFields
}
+
+func init() {
+ registry.Register(jwa.EC().String(), registry.Constructor{
+ Public: func() any { return newECDSAPublicKey() },
+ Private: func() any { return newECDSAPrivateKey() },
+ })
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go
index af7e00d952..e7be20c5f5 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/errors.go
@@ -72,6 +72,31 @@ func sparseerr(f string, args ...any) error {
return bparseerr(`jwk.ParseString`, f, args...)
}
+func kparseerr(f string, args ...any) error {
+ return bparseerr(`jwk.ParseKey`, f, args...)
+}
+
+type whitelistError struct {
+ error
+}
+
+func (e whitelistError) Unwrap() error {
+ return e.error
+}
+
+func (whitelistError) Is(err error) bool {
+ _, ok := err.(whitelistError)
+ return ok
+}
+
+var errDefaultWhitelistError = whitelistError{errors.New(`rejected by whitelist`)}
+
+// WhitelistError returns an error that can be passed to `errors.Is` to check
+// if the error is caused by a URL being rejected by a whitelist.
+func WhitelistError() error {
+ return errDefaultWhitelistError
+}
+
var errDefaultParseError = parseError{errors.New(`parse error`)}
func ParseError() error {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go
index 2c80a369dc..79572edf3f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/fetch.go
@@ -5,8 +5,110 @@ import (
"fmt"
"io"
"net/http"
+ "sync"
+ "sync/atomic"
+ "time"
)
+// defaultMaxFetchBodySize is the initial default maximum number of bytes read
+// from an HTTP response body when fetching a JWKS (10 MB).
+const defaultMaxFetchBodySize int64 = 10 * 1024 * 1024
+
+// defaultFetchTimeout is the default timeout for HTTP requests made by
+// jwk.Fetch(). This prevents malicious or unresponsive JWKS endpoints from
+// hanging indefinitely (e.g. slowloris-style DoS).
+const defaultFetchTimeout = 30 * time.Second
+
+// defaultMaxRedirects is the maximum number of HTTP redirects the default
+// fetch client will follow. This is intentionally lower than Go's default
+// of 10 to limit redirect chain abuse.
+const defaultMaxRedirects = 5
+
+var maxFetchBodySize atomic.Int64
+
+var (
+ fetchHTTPClientMu sync.RWMutex
+ fetchHTTPClient HTTPClient
+)
+
+func init() {
+ maxFetchBodySize.Store(defaultMaxFetchBodySize)
+ fetchHTTPClient = DefaultHTTPClient()
+}
+
+// DefaultHTTPClient returns a new http.Client configured with the same
+// defaults used by jwk.Fetch(): a 30-second timeout, a redirect policy
+// that blocks HTTPS-to-HTTP scheme downgrades, and a maximum of 5 redirects.
+//
+// This is useful for callers who need the library's default protections
+// but want to wrap or augment the client (e.g. adding a custom Transport),
+// and for restoring defaults after calling jwk.Configure(jwk.WithHTTPClient(...)).
+func DefaultHTTPClient() *http.Client {
+ return WrapHTTPClientDefaults(&http.Client{})
+}
+
+// WrapHTTPClientDefaults returns a shallow copy of the given http.Client with the
+// library's default safety behaviors applied. Existing client settings
+// (Transport, Jar, etc.) are preserved.
+//
+// - Timeout: applied only when the client has no timeout set (zero value).
+// - CheckRedirect: if the client already has one, the library's redirect
+// policy runs first; if it passes, the original CheckRedirect is called.
+// If the client has no CheckRedirect, the library's policy is used directly.
+//
+// This is useful when you need to bring your own http.Client (e.g. for custom
+// TLS configuration) but still want the library's redirect hardening.
+func WrapHTTPClientDefaults(client *http.Client) *http.Client {
+ cloned := *client
+ if cloned.Timeout == 0 {
+ cloned.Timeout = defaultFetchTimeout
+ }
+ orig := cloned.CheckRedirect
+ if orig == nil {
+ cloned.CheckRedirect = defaultCheckRedirect
+ } else {
+ cloned.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+ if err := defaultCheckRedirect(req, via); err != nil {
+ return err
+ }
+ return orig(req, via)
+ }
+ }
+ return &cloned
+}
+
+// defaultCheckRedirect is the CheckRedirect policy for the default HTTP client
+// used by jwk.Fetch(). It prevents HTTPS-to-HTTP scheme downgrades and limits
+// the total number of redirects.
+//
+// This does NOT protect against redirects to private/internal IP addresses.
+// For full SSRF protection, callers should provide a custom http.Client via
+// WithHTTPClient that validates destination IPs in Transport.DialContext.
+func defaultCheckRedirect(req *http.Request, via []*http.Request) error {
+ if len(via) >= defaultMaxRedirects {
+ return fmt.Errorf("jwk.Fetch: stopped after %d redirects", defaultMaxRedirects)
+ }
+
+ // Prevent HTTPS → HTTP scheme downgrade at any hop.
+ // via[len(via)-1] is the immediately previous request in the chain.
+ if len(via) > 0 && via[len(via)-1].URL.Scheme == "https" && req.URL.Scheme != "https" {
+ return fmt.Errorf("jwk.Fetch: redirect from HTTPS to non-HTTPS URL %q is not allowed", req.URL.Redacted())
+ }
+ return nil
+}
+
+func getFetchHTTPClient() HTTPClient {
+ fetchHTTPClientMu.RLock()
+ defer fetchHTTPClientMu.RUnlock()
+ return fetchHTTPClient
+}
+
+func setFetchHTTPClient(c HTTPClient) {
+ fetchHTTPClientMu.Lock()
+ defer fetchHTTPClientMu.Unlock()
+ fetchHTTPClient = c
+}
+
// Fetcher is an interface that represents an object that fetches a JWKS.
// Currently this is only used in the `jws.WithVerifyAuto` option.
//
@@ -66,11 +168,28 @@ func (f *CachedFetcher) Fetch(ctx context.Context, u string, _ ...FetchOption) (
// contents of the object with the data at the remote resource,
// consider using `jwk.Cache`, which automatically refreshes
// jwk.Set objects asynchronously.
+//
+// # Security
+//
+// By default, jwk.Fetch does not restrict which URLs may be contacted: the
+// URL you pass is fetched as-is, with only the default HTTP client's
+// HTTPS-to-HTTP redirect block applied. This is the right default when the
+// URL is hard-coded or comes from configuration you control.
+//
+// It is NOT safe when the URL is attacker-controllable — most commonly a
+// `jku` header copied out of an untrusted JWS. In that case you MUST pass
+// a jwk.WithFetchWhitelist() option that restricts the reachable URLs via
+// jwk.MapWhitelist, jwk.RegexpWhitelist, or a custom Whitelist.
+//
+// For defense against redirect-to-private-IP and DNS-rebinding attacks,
+// combine WithFetchWhitelist with a custom http.Client (see WithHTTPClient)
+// whose Transport.DialContext validates resolved addresses.
func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
var parseOptions []ParseOption
//nolint:revive // I want to keep the type of `wl` as `Whitelist` instead of `InsecureWhitelist`
var wl Whitelist = InsecureWhitelist{}
- var client HTTPClient = http.DefaultClient
+ var client = getFetchHTTPClient()
+ var maxBodySize = maxFetchBodySize.Load()
for _, option := range options {
if parseOpt, ok := option.(ParseOption); ok {
parseOptions = append(parseOptions, parseOpt)
@@ -86,11 +205,18 @@ func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
if err := option.Value(&wl); err != nil {
return nil, fmt.Errorf(`failed to retrieve fetch whitelist option value: %w`, err)
}
+ case identMaxFetchBodySize{}:
+ if err := option.Value(&maxBodySize); err != nil {
+ return nil, fmt.Errorf(`failed to retrieve MaxFetchBodySize option value: %w`, err)
+ }
+ if maxBodySize <= 0 {
+ return nil, fmt.Errorf(`jwk.Fetch: WithMaxFetchBodySize must be greater than zero`)
+ }
}
}
if !wl.IsAllowed(u) {
- return nil, fmt.Errorf(`jwk.Fetch: url %q has been rejected by whitelist`, u)
+ return nil, whitelistError{fmt.Errorf(`jwk.Fetch: url %q has been rejected by whitelist`, u)}
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
@@ -108,10 +234,18 @@ func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
return nil, fmt.Errorf(`jwk.Fetch: request returned status %d, expected 200`, res.StatusCode)
}
- buf, err := io.ReadAll(res.Body)
+ // LimitReader caps memory at maxBodySize+1; reading +1 byte lets us detect
+ // oversized responses. We intentionally skip a Content-Length pre-check because
+ // the header is untrustworthy (server-controlled, absent in chunked transfers).
+ // Slow-trickle attacks are mitigated by context deadlines and http.Client.Timeout,
+ // not by header inspection.
+ buf, err := io.ReadAll(io.LimitReader(res.Body, maxBodySize+1))
if err != nil {
return nil, fmt.Errorf(`jwk.Fetch: failed to read response body for %q: %w`, u, err)
}
+ if int64(len(buf)) > maxBodySize {
+ return nil, fmt.Errorf(`jwk.Fetch: response body for %q exceeded max size of %d bytes`, u, maxBodySize)
+ }
return Parse(buf, parseOptions...)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go
index c5a22a43fb..db6b8e31d5 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface.go
@@ -41,15 +41,17 @@ const (
// Set represents JWKS object, a collection of jwk.Key objects.
//
-// Sets can be safely converted to and from JSON using the standard
-// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. However,
-// if you do not know if the payload contains a single JWK or a JWK set,
-// consider using `jwk.Parse()` to always get a `jwk.Set` out of it.
+// Sets can be marshaled and unmarshaled with the standard
+// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. The
+// unmarshal path requires JWKS shape (an object with a "keys" field).
+// For input that may be either a single bare JWK or a JWKS, use
+// [Parse], which dispatches between the two shapes and always returns
+// a `jwk.Set`.
//
-// Since v1.2.12, JWK sets with private parameters can be parsed as well.
-// Such private parameters can be accessed via the `Field()` method.
-// If a resource contains a single JWK instead of a JWK set, private parameters
-// are stored in _both_ the resulting `jwk.Set` object and the `jwk.Key` object .
+// JWKS-level extension members (any top-level field other than "keys")
+// are preserved as set-level private parameters and are accessible via
+// the `Field()` method. Per-key extension members live on the
+// individual `jwk.Key` objects, accessible via that key's `Field()`.
//
//nolint:interfacebloat
type Set interface {
@@ -119,10 +121,12 @@ type Set interface {
}
type set struct {
- keys []Key
- mu sync.RWMutex
- dc DecodeCtx
- privateParams map[string]any
+ 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
}
type PublicKeyer interface {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go
index 4f23d96cb0..1bb2d081bf 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/interface_gen.go
@@ -4,6 +4,7 @@ package jwk
import (
"crypto"
+ "sync"
"github.com/lestrrat-go/jwx/v3/cert"
"github.com/lestrrat-go/jwx/v3/jwa"
@@ -21,6 +22,26 @@ const (
X509CertThumbprintS256Key = "x5t#S256"
)
+type fieldPair struct {
+ Name string
+ Value any
+}
+
+var fieldPairPool = sync.Pool{
+ New: func() any {
+ return make([]fieldPair, 0, 17)
+ },
+}
+
+func getFieldPairList() []fieldPair {
+ return fieldPairPool.Get().([]fieldPair)
+}
+
+func putFieldPairList(list []fieldPair) {
+ clear(list) // zero fieldPair entries so pooled values don't retain references across Put/Get
+ fieldPairPool.Put(list[:0])
+}
+
// Key defines the minimal interface for each of the
// key types. Their use and implementation differ significantly
// between each key type, so you should use type assertions
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel
new file mode 100644
index 0000000000..af55d665ba
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "registry",
+ srcs = ["registry.go"],
+ importpath = "github.com/lestrrat-go/jwx/v3/jwk/internal/registry",
+ visibility = ["//jwk:__subpackages__"],
+)
+
+alias(
+ name = "go_default_library",
+ actual = ":registry",
+ visibility = ["//jwk:__subpackages__"],
+)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go
new file mode 100644
index 0000000000..9f09d77283
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/internal/registry/registry.go
@@ -0,0 +1,43 @@
+// Package registry provides an internal registry of JWK key constructors.
+// It exists so that both jwk and jwk/jwkunsafe can access the same set of
+// constructors without exporting them from the jwk package.
+package registry
+
+import "fmt"
+
+// Constructor holds factory functions for creating empty JWK keys.
+// Public may be nil for key types without a public/private distinction
+// (e.g. symmetric keys).
+type Constructor struct {
+ Public func() any // returns jwk.Key
+ Private func() any // returns jwk.Key
+}
+
+var constructors = map[string]Constructor{}
+
+// Register adds a constructor for the given key type name.
+func Register(kty string, c Constructor) {
+ constructors[kty] = c
+}
+
+// NewKey creates a new empty private (or symmetric) key for the given key type.
+func NewKey(kty string) (any, error) {
+ c, ok := constructors[kty]
+ if !ok {
+ return nil, fmt.Errorf(`registry: unknown key type %q`, kty)
+ }
+ return c.Private(), nil
+}
+
+// NewPublicKey creates a new empty public key for the given key type.
+// Returns an error for key types that have no public/private distinction.
+func NewPublicKey(kty string) (any, error) {
+ c, ok := constructors[kty]
+ if !ok {
+ return nil, fmt.Errorf(`registry: unknown key type %q`, kty)
+ }
+ if c.Public == nil {
+ return nil, fmt.Errorf(`registry: key type %q has no public key variant`, kty)
+ }
+ return c.Public(), nil
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go
index 22d4950d8f..ba2db6cb48 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwk.go
@@ -14,12 +14,15 @@ import (
"math/big"
"reflect"
"slices"
+ "sync/atomic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/json"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk/jwkbb"
)
-var registry = json.NewRegistry()
+var fieldRegistry = json.NewRegistry()
func bigIntToBytes(n *big.Int) ([]byte, error) {
if n == nil {
@@ -28,7 +31,22 @@ func bigIntToBytes(n *big.Int) ([]byte, error) {
return n.Bytes(), nil
}
+// maxKeys bounds the number of keys accepted by Parse() from a single
+// input. It applies to both the JSON `keys` array and the PEM/X.509
+// block stream: each entry triggers a probe + unmarshal + validation,
+// and callers cannot predict that amplification from the raw input
+// size alone. Tunable via WithMaxKeys / Configure(WithMaxKeys(...)).
+var maxKeys atomic.Int64
+
+// rejectDuplicateKID makes Parse/UnmarshalJSON fail when the JWKS
+// carries two or more keys with the same non-empty "kid". Default is
+// false (RFC 7517 allows duplicates; LookupKeyID returns the first).
+// Tunable via WithRejectDuplicateKID / Configure(WithRejectDuplicateKID(...)).
+var rejectDuplicateKID atomic.Bool
+
func init() {
+ maxKeys.Store(1000)
+
if err := RegisterProbeField(reflect.StructField{
Name: "Kty",
Type: reflect.TypeFor[string](),
@@ -41,7 +59,7 @@ func init() {
Type: reflect.TypeFor[json.RawMessage](),
Tag: `json:"d,omitempty"`,
}); err != nil {
- panic(fmt.Errorf("failed to register mandatory probe for 'kty' field: %w", err))
+ panic(fmt.Errorf("failed to register mandatory probe for 'd' field: %w", err))
}
}
@@ -67,7 +85,16 @@ func Import(raw any) (Key, error) {
return nil, importerr(`failed to convert %T to jwk.Key: no converters were able to convert`, raw)
}
- return conv.Import(raw)
+ key, err := conv.Import(raw)
+ if err != nil {
+ return nil, err
+ }
+ if v, ok := key.(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return nil, importerr(`key validation failed: %w`, err)
+ }
+ }
+ return key, nil
}
// PublicSetOf returns a new jwk.Set consisting of
@@ -77,9 +104,29 @@ func Import(raw any) (Key, error) {
// you want to generate the corresponding public versions for the
// users to verify with.
//
-// Be aware that all fields will be copied onto the new public key. It is the caller's
-// responsibility to remove any fields, if necessary.
-func PublicSetOf(v Set) (Set, error) {
+// By default, if the input set contains a symmetric (oct) key, this
+// function returns an error: a symmetric key has no public form, and
+// its "public" representation would be the secret itself. Publishing
+// such a set (e.g. as `/.well-known/jwks.json`) would leak secret
+// material. This is a behavior change from earlier v3.0.x releases,
+// where symmetric keys were silently passed through. Callers who
+// explicitly want the legacy pass-through behavior can opt in with
+// `jwk.WithAllowSymmetric(true)`.
+//
+// Be aware that for asymmetric private keys, all fields will be
+// copied onto the new public key. It is the caller's responsibility
+// to remove any fields, if necessary.
+func PublicSetOf(v Set, options ...PublicSetOption) (Set, error) {
+ var allowSymmetric 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)
+ }
+ }
+ }
+
newSet := NewSet()
n := v.Len()
@@ -88,6 +135,10 @@ func PublicSetOf(v Set) (Set, error) {
if !ok {
return nil, fmt.Errorf(`key not found`)
}
+ 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)
+ }
pubKey, err := PublicKeyOf(k)
if err != nil {
return nil, fmt.Errorf(`failed to get public key of %T: %w`, k, err)
@@ -198,6 +249,14 @@ func (ctx *setDecodeCtx) IgnoreParseError() bool {
// are performed for certificate expiration, no checks against missing
// parameters are performed, etc.
func ParseKey(data []byte, options ...ParseOption) (Key, error) {
+ key, err := doParseKey(data, options...)
+ if err != nil {
+ return nil, kparseerr(`%w`, err)
+ }
+ return key, nil
+}
+
+func doParseKey(data []byte, options ...ParseOption) (Key, error) {
var parsePEM bool
var localReg *json.Registry
var pemDecoder PEMDecoder
@@ -263,6 +322,13 @@ func ParseKey(data []byte, options ...ParseOption) (Key, error) {
parser := parsers[i]
key, err := parser.ParseKey(probe, &unmarshaler, data)
if err == nil {
+ // A buggy custom parser may return (nil, nil); treat
+ // that as if it had returned ContinueError so the next
+ // parser runs instead of handing the caller a nil Key
+ // they will dereference.
+ if key == nil {
+ continue
+ }
return key, nil
}
@@ -295,6 +361,8 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
var localReg *json.Registry
var ignoreParseError bool
var pemDecoder PEMDecoder
+ maxK := int(maxKeys.Load())
+ rejectDupKid := rejectDuplicateKID.Load()
for _, option := range options {
switch option.Ident() {
case identPEM{}:
@@ -313,6 +381,19 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
if err := option.Value(&ignoreParseError); err != nil {
return nil, parseerr(`failed to retrieve IgnoreParseError option value: %w`, err)
}
+ case identMaxKeys{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ return nil, parseerr(`failed to retrieve MaxKeys option value: %w`, err)
+ }
+ if v <= 0 {
+ return nil, parseerr(`WithMaxKeys must be greater than zero, got %d`, v)
+ }
+ maxK = v
+ case identRejectDuplicateKID{}:
+ if err := option.Value(&rejectDupKid); err != nil {
+ return nil, parseerr(`failed to retrieve RejectDuplicateKID option value: %w`, err)
+ }
case identTypedField{}:
var pair typedFieldPair // temporary var needed for typed field
if err := option.Value(&pair); err != nil {
@@ -332,6 +413,7 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
pemDecoder = NewPEMDecoder()
}
src = bytes.TrimSpace(src)
+ var keyCount int
for len(src) > 0 {
raw, rest, err := pemDecoder.Decode(src)
if err != nil {
@@ -344,8 +426,17 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
if err := s.AddKey(key); err != nil {
return nil, parseerr(`failed to add jwk.Key to set: %w`, err)
}
+ keyCount++
+ if keyCount > maxK {
+ return nil, parseerr(`too many keys in PEM input: max %d`, maxK)
+ }
src = bytes.TrimSpace(rest)
}
+ if rejectDupKid {
+ if kid, dup := firstDuplicateKID(s); dup {
+ return nil, parseerr(`duplicate "kid" %q in PEM input`, kid)
+ }
+ }
return s, nil
}
@@ -362,13 +453,60 @@ func Parse(src []byte, options ...ParseOption) (Set, error) {
defer func() { dcKs.SetDecodeCtx(nil) }()
}
- if err := json.Unmarshal(src, s); err != nil {
- return nil, parseerr(`failed to unmarshal JWK set: %w`, err)
+ // Propagate the resolved cap to Set.UnmarshalJSON. A scratch field
+ // rather than a ParseOption thread-through keeps json.Unmarshal happy.
+ if setter, ok := s.(interface{ setMaxKeys(int) }); ok {
+ setter.setMaxKeys(maxK)
+ defer setter.setMaxKeys(0)
+ }
+ if setter, ok := s.(interface{ setRejectDuplicateKID(bool) }); ok && rejectDupKid {
+ setter.setRejectDuplicateKID(true)
+ defer setter.setRejectDuplicateKID(false)
+ }
+
+ // Dispatch JWK-vs-JWKS up front. Set.UnmarshalJSON requires JWKS
+ // shape; the bare-JWK convenience lives here.
+ if jwkbb.HeaderHas(jwkbb.HeaderParse(src), "keys") {
+ if err := json.Unmarshal(src, s); err != nil {
+ return nil, parseerr(`failed to unmarshal JWK set: %w`, err)
+ }
+ } else {
+ key, err := ParseKey(src, options...)
+ if err != nil {
+ return nil, parseerr(`failed to parse sole key: %w`, err)
+ }
+ if err := s.AddKey(key); err != nil {
+ return nil, parseerr(`failed to add jwk.Key to set: %w`, err)
+ }
+ }
+
+ if rejectDupKid {
+ if kid, dup := firstDuplicateKID(s); dup {
+ return nil, parseerr(`duplicate "kid" %q`, kid)
+ }
}
return s, nil
}
+// firstDuplicateKID returns the first non-empty kid that appears more
+// than once in s, or ("", false) if every non-empty kid is unique.
+func firstDuplicateKID(s Set) (string, bool) {
+ seen := make(map[string]struct{}, s.Len())
+ for i := range s.Len() {
+ key, _ := s.Key(i)
+ kid, ok := key.KeyID()
+ if !ok || kid == "" {
+ continue
+ }
+ if _, dup := seen[kid]; dup {
+ return kid, true
+ }
+ seen[kid] = struct{}{}
+ }
+ return "", false
+}
+
// ParseReader parses a JWK set from the incoming byte buffer.
func ParseReader(src io.Reader, options ...ParseOption) (Set, error) {
// meh, there's no way to tell if a stream has "ended" a single
@@ -396,22 +534,32 @@ func ParseString(s string, options ...ParseOption) (Set, error) {
// AssignKeyID is a convenience function to automatically assign the "kid"
// section of the key, if it already doesn't have one. It uses Key.Thumbprint
-// method with crypto.SHA256 as the default hashing algorithm
+// method with crypto.SHA256 as the default hashing algorithm.
+//
+// By default, if the key already carries a `kid`, `AssignKeyID` leaves it
+// alone and returns nil. Pass `jwk.WithForceAssign(true)` to force
+// recomputation (for example, when upgrading to a stronger thumbprint hash
+// via `jwk.WithThumbprintHash`).
func AssignKeyID(key Key, options ...AssignKeyIDOption) error {
- if key.Has(KeyIDKey) {
- return nil
- }
-
hash := crypto.SHA256
+ var force bool
for _, option := range options {
switch option.Ident() {
case identThumbprintHash{}:
if err := option.Value(&hash); err != nil {
return fmt.Errorf(`failed to retrieve thumbprint hash option value: %w`, err)
}
+ case identForceAssign{}:
+ if err := option.Value(&force); err != nil {
+ return fmt.Errorf(`failed to retrieve force assign option value: %w`, err)
+ }
}
}
+ if !force && key.Has(KeyIDKey) {
+ return nil
+ }
+
h, err := key.Thumbprint(hash)
if err != nil {
return fmt.Errorf(`failed to generate thumbprint: %w`, err)
@@ -574,7 +722,7 @@ type CustomDecodeFunc = json.CustomDecodeFunc
// that wraps your desired type (in this case `time.Time`) and implement
// MarshalJSON and UnmashalJSON.
func RegisterCustomField(name string, object any) {
- registry.Register(name, object)
+ fieldRegistry.Register(name, object)
}
// Equal compares two keys and returns true if they are equal. The comparison
@@ -639,6 +787,11 @@ func IsKeyValidationError(err error) bool {
// Configure is used to configure global behavior of the jwk package.
func Configure(options ...GlobalOption) {
var strictKeyUsagePtr *bool
+ var maxFetchBodySizePtr *int64
+ var maxKeysPtr *int64
+ var httpClientPtr *HTTPClient
+ var minRSAModulusBitsPtr *int64
+ var minRSAPublicExponentPtr *int64
for _, option := range options {
switch option.Ident() {
case identStrictKeyUsage{}:
@@ -647,12 +800,77 @@ func Configure(options ...GlobalOption) {
continue
}
strictKeyUsagePtr = &v
+ case identMaxFetchBodySize{}:
+ var v int64
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ if v <= 0 {
+ continue
+ }
+ maxFetchBodySizePtr = &v
+ case identMaxKeys{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ if v <= 0 {
+ continue
+ }
+ v64 := int64(v)
+ maxKeysPtr = &v64
+ case identHTTPClient{}:
+ var v HTTPClient
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ httpClientPtr = &v
+ case identMinRSAModulusBits{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ v64 := int64(v)
+ minRSAModulusBitsPtr = &v64
+ case identMinRSAPublicExponent{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ v64 := int64(v)
+ minRSAPublicExponentPtr = &v64
+ case identRejectDuplicateKID{}:
+ var v bool
+ if err := option.Value(&v); err != nil {
+ continue
+ }
+ rejectDuplicateKID.Store(v)
}
}
if strictKeyUsagePtr != nil {
strictKeyUsage.Store(*strictKeyUsagePtr)
}
+
+ if maxFetchBodySizePtr != nil {
+ maxFetchBodySize.Store(*maxFetchBodySizePtr)
+ }
+
+ if maxKeysPtr != nil {
+ maxKeys.Store(*maxKeysPtr)
+ }
+
+ if httpClientPtr != nil {
+ setFetchHTTPClient(*httpClientPtr)
+ }
+
+ if minRSAModulusBitsPtr != nil {
+ rsaMinModulusBits.Store(*minRSAModulusBitsPtr)
+ }
+
+ if minRSAPublicExponentPtr != nil {
+ setMinRSAPublicExponent(int(*minRSAPublicExponentPtr))
+ }
}
// These are used when validating keys.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel
index 68a4ccdc19..baf286688a 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/BUILD.bazel
@@ -1,12 +1,25 @@
-load("@rules_go//go:def.bzl", "go_library")
+load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "jwkbb",
- srcs = ["x509.go"],
+ srcs = [
+ "header.go",
+ "x509.go",
+ ],
importpath = "github.com/lestrrat-go/jwx/v3/jwk/jwkbb",
visibility = ["//visibility:public"],
deps = [
"@com_github_lestrrat_go_blackmagic//:blackmagic",
+ "@com_github_valyala_fastjson//:fastjson",
+ ],
+)
+
+go_test(
+ name = "jwkbb_test",
+ srcs = ["header_test.go"],
+ deps = [
+ ":jwkbb",
+ "@com_github_stretchr_testify//require",
],
)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go
new file mode 100644
index 0000000000..93a1202017
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/header.go
@@ -0,0 +1,130 @@
+package jwkbb
+
+import (
+ "fmt"
+
+ "github.com/valyala/fastjson"
+)
+
+type headerNotFoundError struct {
+ key string
+}
+
+func (e headerNotFoundError) Error() string {
+ return fmt.Sprintf(`jwkbb: field "%s" not found`, e.key)
+}
+
+func (e headerNotFoundError) Is(target error) bool {
+ switch target.(type) {
+ case headerNotFoundError, *headerNotFoundError:
+ return true
+ default:
+ return false
+ }
+}
+
+// ErrHeaderNotFound returns an error that can be passed to errors.Is
+// to check if the error is the result of a field not being found in
+// the parsed JSON.
+func ErrHeaderNotFound() error {
+ return headerNotFoundError{}
+}
+
+// Header is an opaque handle to a parsed JWK or JWKS JSON object.
+// It exists for fast, allocation-light field probing without paying
+// the cost of a full encoding/json unmarshal.
+//
+// Header instances are NOT safe for concurrent use. Create a new one
+// per goroutine. Values returned by HeaderGet* helpers may alias
+// memory owned by the Header; do not retain them past the Header's
+// lifetime unless the helper explicitly copies (HeaderGetString does;
+// HeaderGetStringBytes does not).
+//
+// This type is experimental and may change or be removed in the future.
+type Header interface {
+ // Sealed so callers can't depend on the underlying fastjson type
+ // or substitute their own implementation.
+ jwkbbHeader()
+}
+
+type header struct {
+ v *fastjson.Value
+ err error
+}
+
+func (h *header) jwkbbHeader() {}
+
+// HeaderParse parses a JSON byte slice and returns a Header for fast
+// field access. Parse errors are deferred to the first HeaderGet* /
+// HeaderHas call.
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderParse(buf []byte) Header {
+ var p fastjson.Parser
+ v, err := p.ParseBytes(buf)
+ if err != nil {
+ return &header{err: err}
+ }
+ return &header{v: v}
+}
+
+func headerGet(h Header, key string) (*fastjson.Value, error) {
+ //nolint:forcetypeassert
+ hh := h.(*header) // we _know_ this can't be another type
+ if hh.err != nil {
+ return nil, hh.err
+ }
+
+ v := hh.v.Get(key)
+ if v == nil {
+ return nil, headerNotFoundError{key: key}
+ }
+ return v, nil
+}
+
+// HeaderHas reports whether the given key exists in the parsed JSON object.
+// Returns false on parse errors.
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderHas(h Header, key string) bool {
+ _, err := headerGet(h, key)
+ return err == nil
+}
+
+// HeaderGetString returns the string value for the given key as a
+// freshly-allocated Go string. The returned value remains valid after
+// the Header is garbage collected.
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderGetString(h Header, key string) (string, error) {
+ v, err := headerGet(h, key)
+ if err != nil {
+ return "", err
+ }
+
+ sb, err := v.StringBytes()
+ if err != nil {
+ return "", err
+ }
+
+ return string(sb), nil
+}
+
+// HeaderGetStringBytes returns the JSON string bytes for the given key
+// without copying.
+//
+// WARNING: the returned slice aliases memory owned by h. It becomes
+// invalid as soon as h is reused, re-parsed, or goes out of scope and
+// is garbage collected. Do not retain the slice, share it across
+// goroutines, or use it after any further call on h. If you need a
+// value that outlives h, use [HeaderGetString].
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderGetStringBytes(h Header, key string) ([]byte, error) {
+ v, err := headerGet(h, key)
+ if err != nil {
+ return nil, err
+ }
+
+ return v.StringBytes()
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go
index 3c827cfa6f..338d471bfb 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/jwkbb/x509.go
@@ -100,6 +100,9 @@ func DecodeX509(dst any, block *pem.Block) error {
}
return blackmagic.AssignIfCompatible(dst, key)
case CertificateBlockType:
+ // Only the public key is extracted. Certificate validation (chain,
+ // expiration, CN/SAN, EKU, etc.) is intentionally not performed here;
+ // it is an application-level concern. See jwk.ParseKey documentation.
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf(`failed to parse certificate: %w`, err)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go
index 7cbf66c2d8..ddbda60efa 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp.go
@@ -7,16 +7,26 @@ import (
"crypto/ed25519"
"fmt"
"reflect"
+ "sync"
- "github.com/lestrrat-go/blackmagic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/jwa"
)
func init() {
- RegisterKeyExporter(jwa.OKP(), KeyExportFunc(okpJWKToRaw))
+ RegisterKeyExporter(KeyKind(jwa.OKP().String()), KeyExportFunc(okpJWKToRaw))
}
+func okpKeyKind(crv func() (jwa.EllipticCurveAlgorithm, bool)) KeyKind {
+ if c, ok := crv(); ok {
+ return KeyKind(jwa.OKP().String() + ":" + c.String())
+ }
+ return KeyKind(jwa.OKP().String())
+}
+
+func (k *okpPublicKey) KeyKind() KeyKind { return okpKeyKind(k.Crv) }
+func (k *okpPrivateKey) KeyKind() KeyKind { return okpKeyKind(k.Crv) }
+
// Mental note:
//
// Curve25519 refers to a particular curve, and is represented in its Montgomery form.
@@ -40,14 +50,32 @@ func (k *okpPublicKey) Import(rawKeyIf any) error {
var crv jwa.EllipticCurveAlgorithm
switch rawKey := rawKeyIf.(type) {
case ed25519.PublicKey:
- k.x = rawKey
crv = jwa.Ed25519()
+ if err := validateOKPPublicKeySize(crv, rawKey); err != nil {
+ return err
+ }
+ k.x = rawKey
k.crv = &crv
case *ecdh.PublicKey:
- k.x = rawKey.Bytes()
crv = jwa.X25519()
+ xbuf := rawKey.Bytes()
+ if err := validateOKPPublicKeySize(crv, xbuf); err != nil {
+ return err
+ }
+ k.x = xbuf
k.crv = &crv
default:
+ muOKPRawKeyImporters.RLock()
+ defer muOKPRawKeyImporters.RUnlock()
+ for _, fn := range okpRawKeyImporters {
+ c, x, _, ok := fn(rawKeyIf)
+ if ok {
+ k.x = x
+ crv = c
+ k.crv = &crv
+ return nil
+ }
+ }
return fmt.Errorf(`unknown key type %T`, rawKeyIf)
}
@@ -61,24 +89,92 @@ func (k *okpPrivateKey) Import(rawKeyIf any) error {
var crv jwa.EllipticCurveAlgorithm
switch rawKey := rawKeyIf.(type) {
case ed25519.PrivateKey:
+ crv = jwa.Ed25519()
+ if len(rawKey) != ed25519.PrivateKeySize {
+ return fmt.Errorf(`ed25519: wrong private key size`)
+ }
k.d = rawKey.Seed()
k.x = rawKey.Public().(ed25519.PublicKey) //nolint:forcetypeassert
- crv = jwa.Ed25519()
k.crv = &crv
case *ecdh.PrivateKey:
- // k.d = rawKey.Seed()
- k.d = rawKey.Bytes()
- k.x = rawKey.PublicKey().Bytes()
crv = jwa.X25519()
+ dbuf := rawKey.Bytes()
+ if err := validateOKPPrivateKeySize(crv, dbuf); err != nil {
+ return err
+ }
+ xbuf := rawKey.PublicKey().Bytes()
+ if err := validateOKPPublicKeySize(crv, xbuf); err != nil {
+ return err
+ }
+ k.d = dbuf
+ k.x = xbuf
k.crv = &crv
default:
+ muOKPRawKeyImporters.RLock()
+ defer muOKPRawKeyImporters.RUnlock()
+ for _, fn := range okpRawKeyImporters {
+ c, x, d, ok := fn(rawKeyIf)
+ if ok {
+ k.x = x
+ k.d = d
+ crv = c
+ k.crv = &crv
+ return nil
+ }
+ }
return fmt.Errorf(`unknown key type %T`, rawKeyIf)
}
return nil
}
+// OKPRawKeyImporter tries to import a raw key as an OKP key.
+// Returns the curve, x, d (nil for public), and true if handled.
+type OKPRawKeyImporter func(key any) (crv jwa.EllipticCurveAlgorithm, x, d []byte, ok bool)
+
+var muOKPRawKeyImporters sync.RWMutex
+var okpRawKeyImporters []OKPRawKeyImporter
+
+// RegisterOKPRawKeyImporter registers a function that can import raw keys as OKP keys.
+func RegisterOKPRawKeyImporter(fn OKPRawKeyImporter) {
+ muOKPRawKeyImporters.Lock()
+ defer muOKPRawKeyImporters.Unlock()
+ okpRawKeyImporters = append(okpRawKeyImporters, fn)
+}
+
+func validateOKPPublicKeySize(alg jwa.EllipticCurveAlgorithm, xbuf []byte) error {
+ switch alg {
+ case jwa.Ed25519():
+ if len(xbuf) != ed25519.PublicKeySize {
+ return fmt.Errorf(`ed25519: wrong public key size`)
+ }
+ case jwa.X25519():
+ if len(xbuf) != 32 {
+ return fmt.Errorf(`x25519: wrong public key size`)
+ }
+ }
+ return nil
+}
+
+func validateOKPPrivateKeySize(alg jwa.EllipticCurveAlgorithm, dbuf []byte) error {
+ switch alg {
+ case jwa.Ed25519():
+ if len(dbuf) != ed25519.SeedSize {
+ return fmt.Errorf(`ed25519: wrong private key size`)
+ }
+ case jwa.X25519():
+ if len(dbuf) != 32 {
+ return fmt.Errorf(`x25519: wrong private key size`)
+ }
+ }
+ return nil
+}
+
func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (any, error) {
+ if err := validateOKPPublicKeySize(alg, xbuf); err != nil {
+ return nil, err
+ }
+
switch alg {
case jwa.Ed25519():
return ed25519.PublicKey(xbuf), nil
@@ -93,36 +189,19 @@ func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (any, error)
}
}
-// Raw returns the EC-DSA public key represented by this JWK
-func (k *okpPublicKey) Raw(v any) error {
- k.mu.RLock()
- defer k.mu.RUnlock()
-
- crv, ok := k.Crv()
- if !ok {
- return fmt.Errorf(`missing "crv" field`)
- }
-
- pubk, err := buildOKPPublicKey(crv, k.x)
- if err != nil {
- return fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err)
- }
-
- if err := blackmagic.AssignIfCompatible(v, pubk); err != nil {
- return fmt.Errorf(`jwk.OKPPublicKey: failed to assign to destination variable: %w`, err)
- }
- return nil
-}
-
func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte) (any, error) {
if len(dbuf) == 0 {
return nil, fmt.Errorf(`cannot use empty seed`)
}
+ if err := validateOKPPublicKeySize(alg, xbuf); err != nil {
+ return nil, err
+ }
+ if err := validateOKPPrivateKeySize(alg, dbuf); err != nil {
+ return nil, err
+ }
+
switch alg {
case jwa.Ed25519():
- if len(dbuf) != ed25519.SeedSize {
- return nil, fmt.Errorf(`ed25519: wrong private key size`)
- }
ret := ed25519.NewKeyFromSeed(dbuf)
//nolint:forcetypeassert
if !bytes.Equal(xbuf, ret.Public().(ed25519.PublicKey)) {
@@ -154,24 +233,46 @@ func okpJWKToRaw(key Key, _ any /* this is unused because this is half baked */)
switch key := extracted.(type) {
case OKPPrivateKey:
- locker, ok := key.(rlocker)
- if ok {
+ // rlocker is unexported with unexported methods, so only our
+ // concrete types implement it. A successful assertion lets us
+ // type-assert to the concrete struct and read fields directly
+ // under a single batch lock. This avoids nested RLock (which
+ // deadlocks when a writer is pending) while preserving an
+ // atomic snapshot of all fields.
+ var crv jwa.EllipticCurveAlgorithm
+ var hasCrv bool
+ var x, d []byte
+ if locker, ok := key.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := key.(*okpPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ if concrete.crv != nil {
+ crv = *(concrete.crv)
+ hasCrv = true
+ }
+ x, d = concrete.x, concrete.d
+ locker.runlock()
+ } else {
+ // External implementation — use self-locking interface getters.
+ var ok bool
+ if crv, ok = key.Crv(); !ok {
+ return nil, fmt.Errorf(`missing "crv" field`)
+ }
+ hasCrv = true
+ if x, ok = key.X(); !ok {
+ return nil, fmt.Errorf(`missing "x" field`)
+ }
+ if d, ok = key.D(); !ok {
+ return nil, fmt.Errorf(`missing "d" field`)
+ }
}
- crv, ok := key.Crv()
- if !ok {
+ if !hasCrv {
return nil, fmt.Errorf(`missing "crv" field`)
}
-
- x, ok := key.X()
- if !ok {
+ if x == nil {
return nil, fmt.Errorf(`missing "x" field`)
}
-
- d, ok := key.D()
- if !ok {
+ if d == nil {
return nil, fmt.Errorf(`missing "d" field`)
}
@@ -181,21 +282,37 @@ func okpJWKToRaw(key Key, _ any /* this is unused because this is half baked */)
}
return privk, nil
case OKPPublicKey:
- locker, ok := key.(rlocker)
- if ok {
+ // See OKPPrivateKey case above for explanation of the rlocker pattern.
+ var crv jwa.EllipticCurveAlgorithm
+ var hasCrv bool
+ var x []byte
+ if locker, ok := key.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := key.(*okpPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ if concrete.crv != nil {
+ crv = *(concrete.crv)
+ hasCrv = true
+ }
+ x = concrete.x
+ locker.runlock()
+ } else {
+ var ok bool
+ if crv, ok = key.Crv(); !ok {
+ return nil, fmt.Errorf(`missing "crv" field`)
+ }
+ hasCrv = true
+ if x, ok = key.X(); !ok {
+ return nil, fmt.Errorf(`missing "x" field`)
+ }
}
- crv, ok := key.Crv()
- if !ok {
+ if !hasCrv {
return nil, fmt.Errorf(`missing "crv" field`)
}
-
- x, ok := key.X()
- if !ok {
+ if x == nil {
return nil, fmt.Errorf(`missing "x" field`)
}
+
pubk, err := buildOKPPublicKey(crv, x)
if err != nil {
return nil, fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err)
@@ -249,7 +366,7 @@ func okpThumbprint(hash crypto.Hash, crv, x string) []byte {
// Thumbprint returns the JWK thumbprint using the indicated
// hashing algorithm, according to RFC 7638 / 8037
-func (k okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
@@ -266,7 +383,7 @@ func (k okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
// Thumbprint returns the JWK thumbprint using the indicated
// hashing algorithm, according to RFC 7638 / 8037
-func (k okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
@@ -286,18 +403,27 @@ func validateOKPKey(key interface {
Crv() (jwa.EllipticCurveAlgorithm, bool)
X() ([]byte, bool)
}) error {
- if v, ok := key.Crv(); !ok || v == jwa.InvalidEllipticCurve() {
+ crv, ok := key.Crv()
+ if !ok || crv == jwa.InvalidEllipticCurve() {
return fmt.Errorf(`invalid curve algorithm`)
}
- if v, ok := key.X(); !ok || len(v) == 0 {
+ x, ok := key.X()
+ if !ok || len(x) == 0 {
return fmt.Errorf(`missing "x" field`)
}
+ if err := validateOKPPublicKeySize(crv, x); err != nil {
+ return err
+ }
if priv, ok := key.(keyWithD); ok {
- if d, ok := priv.D(); !ok || len(d) == 0 {
+ d, ok := priv.D()
+ if !ok || len(d) == 0 {
return fmt.Errorf(`missing "d" field`)
}
+ if err := validateOKPPrivateKeySize(crv, d); err != nil {
+ return err
+ }
}
return nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go
index 0bde986147..3cfac02757 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/okp_gen.go
@@ -15,6 +15,7 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk/internal/registry"
)
const (
@@ -41,7 +42,7 @@ type okpPublicKey struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -50,28 +51,29 @@ var _ Key = &okpPublicKey{}
func newOKPPublicKey() *okpPublicKey {
return &okpPublicKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h okpPublicKey) KeyType() jwa.KeyType {
+func (h *okpPublicKey) KeyType() jwa.KeyType {
return jwa.OKP()
}
-func (h okpPublicKey) rlock() {
+func (h *okpPublicKey) rlock() {
h.mu.RLock()
}
-func (h okpPublicKey) runlock() {
+func (h *okpPublicKey) runlock() {
h.mu.RUnlock()
}
-func (h okpPublicKey) IsPrivate() bool {
+func (h *okpPublicKey) IsPrivate() bool {
return false
}
func (h *okpPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -79,6 +81,8 @@ func (h *okpPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *okpPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.crv != nil {
return *(h.crv), true
}
@@ -86,6 +90,8 @@ func (h *okpPublicKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
}
func (h *okpPublicKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -93,6 +99,8 @@ func (h *okpPublicKey) KeyID() (string, bool) {
}
func (h *okpPublicKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -100,6 +108,8 @@ func (h *okpPublicKey) KeyOps() (KeyOperationList, bool) {
}
func (h *okpPublicKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -107,6 +117,8 @@ func (h *okpPublicKey) KeyUsage() (string, bool) {
}
func (h *okpPublicKey) X() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x != nil {
return h.x, true
}
@@ -114,10 +126,17 @@ func (h *okpPublicKey) X() ([]byte, bool) {
}
func (h *okpPublicKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *okpPublicKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -125,6 +144,8 @@ func (h *okpPublicKey) X509CertThumbprint() (string, bool) {
}
func (h *okpPublicKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -132,6 +153,8 @@ func (h *okpPublicKey) X509CertThumbprintS256() (string, bool) {
}
func (h *okpPublicKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -328,7 +351,12 @@ func (h *okpPublicKey) setNoLock(name string, value any) error {
}
case OKPXKey:
if v, ok := value.([]byte); ok {
- h.x = v
+ if v == nil {
+ h.x = nil
+ } else {
+ h.x = make([]byte, len(v))
+ copy(h.x, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, OKPXKey, value)
@@ -447,7 +475,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -471,7 +499,7 @@ LOOP:
}
h.crv = &decoded
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -481,7 +509,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case OKPXKey:
@@ -495,15 +523,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -516,7 +544,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -536,84 +564,135 @@ LOOP:
return nil
}
-func (h okpPublicKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 10)
- data[KeyTypeKey] = jwa.OKP()
- fields = append(fields, KeyTypeKey)
+func (h *okpPublicKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.OKP())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.crv != nil {
- data[OKPCrvKey] = *(h.crv)
- fields = append(fields, OKPCrvKey)
+ v, err := json.Marshal(*(h.crv))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPCrvKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: OKPCrvKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.x != nil {
- data[OKPXKey] = h.x
- fields = append(fields, OKPXKey)
+ v, err := json.Marshal(base64.EncodeToString(h.x))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPXKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: OKPXKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *okpPublicKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -678,7 +757,7 @@ type okpPrivateKey struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -687,28 +766,29 @@ var _ Key = &okpPrivateKey{}
func newOKPPrivateKey() *okpPrivateKey {
return &okpPrivateKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h okpPrivateKey) KeyType() jwa.KeyType {
+func (h *okpPrivateKey) KeyType() jwa.KeyType {
return jwa.OKP()
}
-func (h okpPrivateKey) rlock() {
+func (h *okpPrivateKey) rlock() {
h.mu.RLock()
}
-func (h okpPrivateKey) runlock() {
+func (h *okpPrivateKey) runlock() {
h.mu.RUnlock()
}
-func (h okpPrivateKey) IsPrivate() bool {
+func (h *okpPrivateKey) IsPrivate() bool {
return true
}
func (h *okpPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -716,6 +796,8 @@ func (h *okpPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *okpPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.crv != nil {
return *(h.crv), true
}
@@ -723,6 +805,8 @@ func (h *okpPrivateKey) Crv() (jwa.EllipticCurveAlgorithm, bool) {
}
func (h *okpPrivateKey) D() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.d != nil {
return h.d, true
}
@@ -730,6 +814,8 @@ func (h *okpPrivateKey) D() ([]byte, bool) {
}
func (h *okpPrivateKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -737,6 +823,8 @@ func (h *okpPrivateKey) KeyID() (string, bool) {
}
func (h *okpPrivateKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -744,6 +832,8 @@ func (h *okpPrivateKey) KeyOps() (KeyOperationList, bool) {
}
func (h *okpPrivateKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -751,6 +841,8 @@ func (h *okpPrivateKey) KeyUsage() (string, bool) {
}
func (h *okpPrivateKey) X() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x != nil {
return h.x, true
}
@@ -758,10 +850,17 @@ func (h *okpPrivateKey) X() ([]byte, bool) {
}
func (h *okpPrivateKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *okpPrivateKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -769,6 +868,8 @@ func (h *okpPrivateKey) X509CertThumbprint() (string, bool) {
}
func (h *okpPrivateKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -776,6 +877,8 @@ func (h *okpPrivateKey) X509CertThumbprintS256() (string, bool) {
}
func (h *okpPrivateKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -954,7 +1057,12 @@ func (h *okpPrivateKey) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, OKPCrvKey, value)
case OKPDKey:
if v, ok := value.([]byte); ok {
- h.d = v
+ if v == nil {
+ h.d = nil
+ } else {
+ h.d = make([]byte, len(v))
+ copy(h.d, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, OKPDKey, value)
@@ -988,7 +1096,12 @@ func (h *okpPrivateKey) setNoLock(name string, value any) error {
}
case OKPXKey:
if v, ok := value.([]byte); ok {
- h.x = v
+ if v == nil {
+ h.x = nil
+ } else {
+ h.x = make([]byte, len(v))
+ copy(h.x, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, OKPXKey, value)
@@ -1077,7 +1190,7 @@ func (k *okpPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
k.dc = dc
}
-func (h *okpPrivateKey) UnmarshalJSON(buf []byte) error {
+func (h *okpPrivateKey) UnmarshalJSON(buf []byte) (retErr error) {
h.mu.Lock()
defer h.mu.Unlock()
h.algorithm = nil
@@ -1091,6 +1204,14 @@ func (h *okpPrivateKey) UnmarshalJSON(buf []byte) error {
h.x509CertThumbprint = nil
h.x509CertThumbprintS256 = nil
h.x509URL = nil
+ defer func() {
+ if retErr != nil {
+ clear(h.d)
+ h.d = nil
+ clear(h.x)
+ h.x = nil
+ }
+ }()
dec := json.NewDecoder(bytes.NewReader(buf))
LOOP:
for {
@@ -1110,7 +1231,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -1138,7 +1259,7 @@ LOOP:
return fmt.Errorf(`failed to decode value for key %s: %w`, OKPDKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -1148,7 +1269,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case OKPXKey:
@@ -1162,15 +1283,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -1183,7 +1304,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -1206,88 +1327,142 @@ LOOP:
return nil
}
-func (h okpPrivateKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 11)
- data[KeyTypeKey] = jwa.OKP()
- fields = append(fields, KeyTypeKey)
+func (h *okpPrivateKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.OKP())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.crv != nil {
- data[OKPCrvKey] = *(h.crv)
- fields = append(fields, OKPCrvKey)
+ v, err := json.Marshal(*(h.crv))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPCrvKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: OKPCrvKey, Value: v})
}
if h.d != nil {
- data[OKPDKey] = h.d
- fields = append(fields, OKPDKey)
+ v, err := json.Marshal(base64.EncodeToString(h.d))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: OKPDKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.x != nil {
- data[OKPXKey] = h.x
- fields = append(fields, OKPXKey)
+ v, err := json.Marshal(base64.EncodeToString(h.x))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, OKPXKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: OKPXKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *okpPrivateKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -1345,3 +1520,10 @@ func init() {
func OKPStandardFieldsFilter() KeyFilter {
return okpStandardFields
}
+
+func init() {
+ registry.Register(jwa.OKP().String(), registry.Constructor{
+ Public: func() any { return newOKPPublicKey() },
+ Private: func() any { return newOKPPrivateKey() },
+ })
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml
index 879dcba158..765f3ea8e9 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options.yaml
@@ -45,16 +45,79 @@ interfaces:
comment: |
GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to
change the global configuration of the jwk package.
+ - name: GlobalParseOption
+ methods:
+ - globalOption
+ - fetchOption
+ - registerOption
+ - readFileOption
+ comment: |
+ GlobalParseOption describes an Option that can be passed to both
+ `jwk.Configure()` (to change the default globally) and
+ `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()` (to
+ override per call).
+ - name: PublicSetOption
+ comment: |
+ PublicSetOption is a type of Option that can be passed to `jwk.PublicSetOf()`
+ - name: GlobalFetchOption
+ methods:
+ - globalOption
+ - fetchOption
+ - registerOption
+ - parseOption
+ comment: |
+ GlobalFetchOption describes an Option that can be passed to `jwk.Configure()`,
+ `jwk.Fetch()`, and `(*jwk.Cache).Register()`.
options:
- ident: HTTPClient
- interface: RegisterFetchOption
+ interface: GlobalFetchOption
argument_type: HTTPClient
comment: |
WithHTTPClient allows users to specify the "net/http".Client object that
is used when fetching jwk.Set objects.
+
+ When passed to `jwk.Configure()`, it sets the global default HTTP client
+ used by `jwk.Fetch()`. By default, `jwk.Fetch()` uses an HTTP client with
+ a 30-second timeout and a redirect policy that blocks HTTPS-to-HTTP
+ scheme downgrades (with a maximum of 5 redirects) instead of
+ `http.DefaultClient` (which has no timeout and allows up to 10 redirects).
+
+ The client is used as-is: the library does NOT automatically apply its
+ default timeout or redirect policy to a user-supplied client. If you want
+ to bring your own client (e.g. for custom TLS or proxy settings) while
+ retaining the library's defaults, wrap it with `jwk.WrapHTTPClientDefaults()`
+ before passing it to this option.
+
+ For full SSRF protection (blocking redirects to private IPs, DNS
+ rebinding prevention), provide a custom http.Client with an appropriate
+ Transport.DialContext that validates resolved IP addresses.
+
+ Users can override the client per-call via `jwk.Fetch()` or per-resource
+ via `(*jwk.Cache).Register()`.
+ - ident: MaxFetchBodySize
+ interface: GlobalFetchOption
+ argument_type: int64
+ comment: |
+ WithMaxFetchBodySize specifies the maximum number of bytes to read from
+ an HTTP response body when fetching a JWKS. If the response body exceeds
+ this size, the fetch returns an error. The default value is 10MB (10485760).
+
+ This option can be passed to `jwk.Configure()` to change the default
+ globally, or to `jwk.Fetch()` / `(*jwk.Cache).Register()` for a per-call
+ override.
- ident: ThumbprintHash
interface: AssignKeyIDOption
argument_type: crypto.Hash
+ - ident: ForceAssign
+ interface: AssignKeyIDOption
+ argument_type: bool
+ comment: |
+ WithForceAssign forces `jwk.AssignKeyID` to recompute and overwrite
+ the `kid` header even when the key already has one. The default
+ behavior preserves any existing `kid`; use this option to upgrade
+ the thumbprint hash (e.g. with `jwk.WithThumbprintHash`) or to
+ refresh a `kid` after mutating a key field that invalidates the
+ cached thumbprint.
- ident: LocalRegistry
option_name: withLocalRegistry
interface: ParseOption
@@ -86,9 +149,25 @@ options:
interface: FetchOption
argument_type: Whitelist
comment: |
- WithFetchWhitelist specifies the Whitelist object to use when
- fetching JWKs from a remote source. This option can be passed
- to both `jwk.Fetch()`
+ WithFetchWhitelist specifies the Whitelist applied to the URL passed
+ to `jwk.Fetch()` (and `(*jwk.Cache).Register()`).
+
+ The default when this option is not supplied is `jwk.InsecureWhitelist{}`,
+ which allows every URL. That is the right default for URLs that are
+ hard-coded in your program or loaded from trusted configuration, and
+ keeps first-time usage free of boilerplate.
+
+ It is NOT safe when the URL comes from an untrusted source — most
+ commonly the `jku` header of a JWS handed to you by a peer. For those
+ call sites you MUST supply a restrictive Whitelist: use
+ `jwk.NewMapWhitelist()` for a fixed allow-list, `jwk.RegexpWhitelist`
+ for pattern-based allow-lists, or implement the `jwk.Whitelist`
+ interface yourself.
+
+ Note that a whitelist only constrains the initial URL. For defense
+ against redirect-to-private-IP and DNS-rebinding attacks, also supply
+ a custom `http.Client` via `jwk.WithHTTPClient` whose
+ `Transport.DialContext` validates resolved addresses.
- ident: IgnoreParseError
interface: ParseOption
argument_type: bool
@@ -140,4 +219,80 @@ options:
If this option is set to false, then the "use" field can be any
value. If this options is set to true, then the "use" field must
be one of the registered values, and otherwise an error will be
- reported during parsing / assignment to `jwk.KeyUsageType`
\ No newline at end of file
+ reported during parsing / assignment to `jwk.KeyUsageType`
+ - ident: MinRSAModulusBits
+ interface: GlobalOption
+ argument_type: int
+ comment: |
+ WithMinRSAModulusBits specifies the minimum RSA modulus size, in bits,
+ accepted by JWK validation and raw/PEM/X.509 import.
+
+ The default is 2048. Lower this only for legacy interoperability with
+ older key material. A value of 0 disables the modulus-size floor.
+ - ident: MinRSAPublicExponent
+ interface: GlobalOption
+ argument_type: int
+ comment: |
+ WithMinRSAPublicExponent specifies the minimum RSA public exponent
+ accepted by JWK validation and raw/PEM/X.509 import.
+
+ The default is 3. The exponent must still be odd and fit in a Go `int`.
+ Lower this only for legacy interoperability. A value of 0 disables the
+ minimum-exponent floor.
+ - ident: MaxKeys
+ interface: GlobalParseOption
+ argument_type: int
+ comment: |
+ WithMaxKeys specifies the maximum number of keys allowed in a JWK
+ set passed to `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
+ If the "keys" array of a JSON-encoded JWKS, or the number of PEM
+ blocks in a PEM/X.509-encoded input, exceeds this value, parsing
+ returns an error. The default is 1000.
+
+ This option can be passed to `jwk.Configure()` to change the
+ default globally, or to `jwk.Parse()` / `jwk.ParseReader()` /
+ `jwk.ParseString()` for a per-call override. A non-positive
+ value is rejected.
+
+ The cap defends against amplification: each entry triggers a
+ probe + unmarshal + validation, each of which allocates.
+ Bounding raw input bytes remains the caller's responsibility.
+ - ident: RejectDuplicateKID
+ interface: GlobalParseOption
+ argument_type: bool
+ comment: |
+ WithRejectDuplicateKID instructs `jwk.Parse()` /
+ `jwk.ParseReader()` / `jwk.ParseString()` and
+ `Set.UnmarshalJSON()` to return an error when the JWKS contains
+ two or more keys with the same non-empty `kid`. Keys without a
+ kid are not considered.
+
+ Default is false — first-match-wins is retained for
+ compatibility with RFC 7517 (which permits, but does not
+ mandate, unique kids) and with existing callers that rely on
+ `(jwk.Set).LookupKeyID` returning the first entry. Use this
+ option when your issuer should guarantee kid uniqueness and a
+ duplicate is a sign of misconfiguration worth surfacing at
+ parse time rather than at verify time.
+
+ Can be set globally via `jwk.Configure()` or per-call on
+ `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
+
+ This does not affect `(*Set).AddKey` — programmatic additions
+ remain permissive (AddKey dedupes only by pointer identity).
+ - ident: AllowSymmetric
+ interface: PublicSetOption
+ argument_type: bool
+ comment: |
+ WithAllowSymmetric controls whether `jwk.PublicSetOf` tolerates
+ symmetric (oct) keys in the input set.
+
+ By default this option is false: a symmetric key in the input is
+ an error, because a symmetric key has no public form — its
+ "public" representation is the secret itself. Passing such a set
+ through `PublicSetOf` silently and then publishing the result
+ (e.g. as `/.well-known/jwks.json`) would leak HMAC secret material.
+
+ 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.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go
index 99e66c3e7e..e90a9ee8d4 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/options_gen.go
@@ -56,6 +56,28 @@ func (*fetchOption) parseOption() {}
func (*fetchOption) registerOption() {}
+// GlobalFetchOption describes an Option that can be passed to `jwk.Configure()`,
+// `jwk.Fetch()`, and `(*jwk.Cache).Register()`.
+type GlobalFetchOption interface {
+ Option
+ globalOption()
+ fetchOption()
+ registerOption()
+ parseOption()
+}
+
+type globalFetchOption struct {
+ Option
+}
+
+func (*globalFetchOption) globalOption() {}
+
+func (*globalFetchOption) fetchOption() {}
+
+func (*globalFetchOption) registerOption() {}
+
+func (*globalFetchOption) parseOption() {}
+
// GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to
// change the global configuration of the jwk package.
type GlobalOption interface {
@@ -69,6 +91,30 @@ type globalOption struct {
func (*globalOption) globalOption() {}
+// GlobalParseOption describes an Option that can be passed to both
+// `jwk.Configure()` (to change the default globally) and
+// `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()` (to
+// override per call).
+type GlobalParseOption interface {
+ Option
+ globalOption()
+ fetchOption()
+ registerOption()
+ readFileOption()
+}
+
+type globalParseOption struct {
+ Option
+}
+
+func (*globalParseOption) globalOption() {}
+
+func (*globalParseOption) fetchOption() {}
+
+func (*globalParseOption) registerOption() {}
+
+func (*globalParseOption) readFileOption() {}
+
// ParseOption is a type of Option that can be passed to `jwk.Parse()`
// ParseOption also implements the `ReadFileOption` and `NewCacheOption`,
// and thus safely be passed to `jwk.ReadFile` and `(*jwk.Cache).Configure()`
@@ -89,6 +135,18 @@ func (*parseOption) registerOption() {}
func (*parseOption) readFileOption() {}
+// PublicSetOption is a type of Option that can be passed to `jwk.PublicSetOf()`
+type PublicSetOption interface {
+ Option
+ publicSetOption()
+}
+
+type publicSetOption struct {
+ Option
+}
+
+func (*publicSetOption) publicSetOption() {}
+
// ReadFileOption is a type of `Option` that can be passed to `jwk.ReadFile`
type ReadFileOption interface {
Option
@@ -144,18 +202,29 @@ type resourceOption struct {
func (*resourceOption) resourceOption() {}
+type identAllowSymmetric struct{}
type identFS struct{}
type identFetchWhitelist struct{}
+type identForceAssign struct{}
type identHTTPClient struct{}
type identIgnoreParseError struct{}
type identLocalRegistry struct{}
+type identMaxFetchBodySize struct{}
+type identMaxKeys struct{}
+type identMinRSAModulusBits struct{}
+type identMinRSAPublicExponent struct{}
type identPEM struct{}
type identPEMDecoder struct{}
+type identRejectDuplicateKID struct{}
type identStrictKeyUsage struct{}
type identThumbprintHash struct{}
type identWaitReady struct{}
type identX509 struct{}
+func (identAllowSymmetric) String() string {
+ return "WithAllowSymmetric"
+}
+
func (identFS) String() string {
return "WithFS"
}
@@ -164,6 +233,10 @@ func (identFetchWhitelist) String() string {
return "WithFetchWhitelist"
}
+func (identForceAssign) String() string {
+ return "WithForceAssign"
+}
+
func (identHTTPClient) String() string {
return "WithHTTPClient"
}
@@ -176,6 +249,22 @@ func (identLocalRegistry) String() string {
return "withLocalRegistry"
}
+func (identMaxFetchBodySize) String() string {
+ return "WithMaxFetchBodySize"
+}
+
+func (identMaxKeys) String() string {
+ return "WithMaxKeys"
+}
+
+func (identMinRSAModulusBits) String() string {
+ return "WithMinRSAModulusBits"
+}
+
+func (identMinRSAPublicExponent) String() string {
+ return "WithMinRSAPublicExponent"
+}
+
func (identPEM) String() string {
return "WithPEM"
}
@@ -184,6 +273,10 @@ func (identPEMDecoder) String() string {
return "WithPEMDecoder"
}
+func (identRejectDuplicateKID) String() string {
+ return "WithRejectDuplicateKID"
+}
+
func (identStrictKeyUsage) String() string {
return "WithStrictKeyUsage"
}
@@ -200,22 +293,83 @@ func (identX509) String() string {
return "WithX509"
}
+// WithAllowSymmetric controls whether `jwk.PublicSetOf` tolerates
+// symmetric (oct) keys in the input set.
+//
+// By default this option is false: a symmetric key in the input is
+// an error, because a symmetric key has no public form — its
+// "public" representation is the secret itself. Passing such a set
+// through `PublicSetOf` silently and then publishing the result
+// (e.g. as `/.well-known/jwks.json`) would leak HMAC secret material.
+//
+// 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.
+func WithAllowSymmetric(v bool) PublicSetOption {
+ return &publicSetOption{option.New(identAllowSymmetric{}, v)}
+}
+
// WithFS specifies the source `fs.FS` object to read the file from.
func WithFS(v fs.FS) ReadFileOption {
return &readFileOption{option.New(identFS{}, v)}
}
-// WithFetchWhitelist specifies the Whitelist object to use when
-// fetching JWKs from a remote source. This option can be passed
-// to both `jwk.Fetch()`
+// WithFetchWhitelist specifies the Whitelist applied to the URL passed
+// to `jwk.Fetch()` (and `(*jwk.Cache).Register()`).
+//
+// The default when this option is not supplied is `jwk.InsecureWhitelist{}`,
+// which allows every URL. That is the right default for URLs that are
+// hard-coded in your program or loaded from trusted configuration, and
+// keeps first-time usage free of boilerplate.
+//
+// It is NOT safe when the URL comes from an untrusted source — most
+// commonly the `jku` header of a JWS handed to you by a peer. For those
+// call sites you MUST supply a restrictive Whitelist: use
+// `jwk.NewMapWhitelist()` for a fixed allow-list, `jwk.RegexpWhitelist`
+// for pattern-based allow-lists, or implement the `jwk.Whitelist`
+// interface yourself.
+//
+// Note that a whitelist only constrains the initial URL. For defense
+// against redirect-to-private-IP and DNS-rebinding attacks, also supply
+// a custom `http.Client` via `jwk.WithHTTPClient` whose
+// `Transport.DialContext` validates resolved addresses.
func WithFetchWhitelist(v Whitelist) FetchOption {
return &fetchOption{option.New(identFetchWhitelist{}, v)}
}
+// WithForceAssign forces `jwk.AssignKeyID` to recompute and overwrite
+// the `kid` header even when the key already has one. The default
+// behavior preserves any existing `kid`; use this option to upgrade
+// the thumbprint hash (e.g. with `jwk.WithThumbprintHash`) or to
+// refresh a `kid` after mutating a key field that invalidates the
+// cached thumbprint.
+func WithForceAssign(v bool) AssignKeyIDOption {
+ return &assignKeyIDOption{option.New(identForceAssign{}, v)}
+}
+
// WithHTTPClient allows users to specify the "net/http".Client object that
// is used when fetching jwk.Set objects.
-func WithHTTPClient(v HTTPClient) RegisterFetchOption {
- return ®isterFetchOption{option.New(identHTTPClient{}, v)}
+//
+// When passed to `jwk.Configure()`, it sets the global default HTTP client
+// used by `jwk.Fetch()`. By default, `jwk.Fetch()` uses an HTTP client with
+// a 30-second timeout and a redirect policy that blocks HTTPS-to-HTTP
+// scheme downgrades (with a maximum of 5 redirects) instead of
+// `http.DefaultClient` (which has no timeout and allows up to 10 redirects).
+//
+// The client is used as-is: the library does NOT automatically apply its
+// default timeout or redirect policy to a user-supplied client. If you want
+// to bring your own client (e.g. for custom TLS or proxy settings) while
+// retaining the library's defaults, wrap it with `jwk.WrapHTTPClientDefaults()`
+// before passing it to this option.
+//
+// For full SSRF protection (blocking redirects to private IPs, DNS
+// rebinding prevention), provide a custom http.Client with an appropriate
+// Transport.DialContext that validates resolved IP addresses.
+//
+// Users can override the client per-call via `jwk.Fetch()` or per-resource
+// via `(*jwk.Cache).Register()`.
+func WithHTTPClient(v HTTPClient) GlobalFetchOption {
+ return &globalFetchOption{option.New(identHTTPClient{}, v)}
}
// WithIgnoreParseError is only applicable when used with `jwk.Parse()`
@@ -246,6 +400,54 @@ func withLocalRegistry(v *json.Registry) ParseOption {
return &parseOption{option.New(identLocalRegistry{}, v)}
}
+// WithMaxFetchBodySize specifies the maximum number of bytes to read from
+// an HTTP response body when fetching a JWKS. If the response body exceeds
+// this size, the fetch returns an error. The default value is 10MB (10485760).
+//
+// This option can be passed to `jwk.Configure()` to change the default
+// globally, or to `jwk.Fetch()` / `(*jwk.Cache).Register()` for a per-call
+// override.
+func WithMaxFetchBodySize(v int64) GlobalFetchOption {
+ return &globalFetchOption{option.New(identMaxFetchBodySize{}, v)}
+}
+
+// WithMaxKeys specifies the maximum number of keys allowed in a JWK
+// set passed to `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
+// If the "keys" array of a JSON-encoded JWKS, or the number of PEM
+// blocks in a PEM/X.509-encoded input, exceeds this value, parsing
+// returns an error. The default is 1000.
+//
+// This option can be passed to `jwk.Configure()` to change the
+// default globally, or to `jwk.Parse()` / `jwk.ParseReader()` /
+// `jwk.ParseString()` for a per-call override. A non-positive
+// value is rejected.
+//
+// The cap defends against amplification: each entry triggers a
+// probe + unmarshal + validation, each of which allocates.
+// Bounding raw input bytes remains the caller's responsibility.
+func WithMaxKeys(v int) GlobalParseOption {
+ return &globalParseOption{option.New(identMaxKeys{}, v)}
+}
+
+// WithMinRSAModulusBits specifies the minimum RSA modulus size, in bits,
+// accepted by JWK validation and raw/PEM/X.509 import.
+//
+// The default is 2048. Lower this only for legacy interoperability with
+// older key material. A value of 0 disables the modulus-size floor.
+func WithMinRSAModulusBits(v int) GlobalOption {
+ return &globalOption{option.New(identMinRSAModulusBits{}, v)}
+}
+
+// WithMinRSAPublicExponent specifies the minimum RSA public exponent
+// accepted by JWK validation and raw/PEM/X.509 import.
+//
+// The default is 3. The exponent must still be odd and fit in a Go `int`.
+// Lower this only for legacy interoperability. A value of 0 disables the
+// minimum-exponent floor.
+func WithMinRSAPublicExponent(v int) GlobalOption {
+ return &globalOption{option.New(identMinRSAPublicExponent{}, 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
@@ -263,6 +465,29 @@ func WithPEMDecoder(v PEMDecoder) ParseOption {
return &parseOption{option.New(identPEMDecoder{}, v)}
}
+// WithRejectDuplicateKID instructs `jwk.Parse()` /
+// `jwk.ParseReader()` / `jwk.ParseString()` and
+// `Set.UnmarshalJSON()` to return an error when the JWKS contains
+// two or more keys with the same non-empty `kid`. Keys without a
+// kid are not considered.
+//
+// Default is false — first-match-wins is retained for
+// compatibility with RFC 7517 (which permits, but does not
+// mandate, unique kids) and with existing callers that rely on
+// `(jwk.Set).LookupKeyID` returning the first entry. Use this
+// option when your issuer should guarantee kid uniqueness and a
+// duplicate is a sign of misconfiguration worth surfacing at
+// parse time rather than at verify time.
+//
+// Can be set globally via `jwk.Configure()` or per-call on
+// `jwk.Parse()` / `jwk.ParseReader()` / `jwk.ParseString()`.
+//
+// This does not affect `(*Set).AddKey` — programmatic additions
+// remain permissive (AddKey dedupes only by pointer identity).
+func WithRejectDuplicateKID(v bool) GlobalParseOption {
+ return &globalParseOption{option.New(identRejectDuplicateKID{}, 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
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go
index fa8764ef72..2969fd3078 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/parser.go
@@ -21,7 +21,7 @@ type KeyParser interface {
// If your KeyParser decides that the payload is not something
// you can parse, and you would like to continue parsing with
// the remaining KeyParser instances that are registered,
- // return a `jwk.ContinueParseError`. Any other errors will immediately
+ // return a `jwk.ContinueError()`. Any other errors will immediately
// halt the parsing process.
//
// When unmarshaling JSON, use the unmarshaler object supplied as
@@ -89,6 +89,15 @@ func defaultParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (
if err := unmarshaler.UnmarshalKey(data, key); err != nil {
return nil, fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err)
}
+ // Enforce the trust boundary: a key that fails its own Validate() must
+ // never escape Parse/ParseKey. All built-in key types implement this
+ // interface via NewKeyValidationError, so callers can still use
+ // jwk.IsKeyValidationError on the wrapped error.
+ if v, ok := key.(interface{ Validate() error }); ok {
+ if err := v.Validate(); err != nil {
+ return nil, fmt.Errorf(`jwk.Parse: key validation failed: %w`, err)
+ }
+ }
return key, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go
index ca27681587..ecb74edfc1 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa.go
@@ -6,7 +6,9 @@ import (
"encoding/binary"
"fmt"
"math/big"
+ "math/bits"
"reflect"
+ "sync/atomic"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/pool"
@@ -14,7 +16,27 @@ import (
)
func init() {
- RegisterKeyExporter(jwa.RSA(), KeyExportFunc(rsaJWKToRaw))
+ RegisterKeyExporter(KeyKind(jwa.RSA().String()), KeyExportFunc(rsaJWKToRaw))
+}
+
+const minRSAModulusBits = 2048
+const minRSAPublicExponent = 3
+
+var rsaMinModulusBits = atomic.Int64{}
+var rsaMinPublicExponent atomic.Pointer[big.Int]
+
+func init() {
+ rsaMinModulusBits.Store(minRSAModulusBits)
+ setMinRSAPublicExponent(minRSAPublicExponent)
+}
+
+func setMinRSAPublicExponent(v int) {
+ if v <= 0 {
+ rsaMinPublicExponent.Store(nil)
+ return
+ }
+
+ rsaMinPublicExponent.Store(big.NewInt(int64(v)))
}
func (k *rsaPrivateKey) Import(rawKey *rsa.PrivateKey) error {
@@ -76,6 +98,9 @@ func importRsaPublicKeyByteValues(rawKey *rsa.PublicKey) ([]byte, []byte, error)
if err != nil {
return nil, nil, fmt.Errorf(`invalid rsa.PublicKey: %w`, err)
}
+ if rawKey.E <= 0 {
+ return nil, nil, fmt.Errorf(`invalid rsa.PublicKey: invalid rsa public exponent: must be a positive odd integer`)
+ }
data := make([]byte, 8)
binary.BigEndian.PutUint64(data, uint64(rawKey.E))
@@ -102,16 +127,51 @@ func (k *rsaPublicKey) Import(rawKey *rsa.PublicKey) error {
return nil
}
-func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) {
- bin := pool.BigInt().Get()
- bie := pool.BigInt().Get()
- defer pool.BigInt().Put(bie)
+func validateRSAModulusAndExponent(n, e []byte) (*big.Int, error) {
+ n = trimLeadingZeroBytes(n)
+ if len(n) == 0 {
+ return nil, fmt.Errorf(`missing "n" value`)
+ }
- bin.SetBytes(n)
- bie.SetBytes(e)
+ bigN := new(big.Int).SetBytes(n)
+ minBits := int(rsaMinModulusBits.Load())
+ if minBits > 0 && bigN.BitLen() < minBits {
+ return nil, fmt.Errorf(`rsa modulus too small: got %d bits, need at least %d`, bigN.BitLen(), minBits)
+ }
- key.N = bin
- key.E = int(bie.Int64())
+ e = trimLeadingZeroBytes(e)
+ if len(e) == 0 {
+ return nil, fmt.Errorf(`missing "e" value`)
+ }
+
+ bigE := new(big.Int).SetBytes(e)
+ minExponent := rsaMinPublicExponent.Load()
+ if bigE.Sign() <= 0 || bigE.Bit(0) == 0 {
+ return nil, fmt.Errorf(`invalid rsa public exponent: must be a positive odd integer`)
+ }
+ if minExponent != nil && bigE.Cmp(minExponent) < 0 {
+ return nil, fmt.Errorf(`invalid rsa public exponent: got %s, need at least %s`, bigE.String(), minExponent.String())
+ }
+
+ // rsa.PublicKey.E is a Go int. Reject exponents that do not fit on the
+ // current platform (e.g. GOARCH=386). Without this guard, Int64()/int()
+ // silently truncates, causing the materialized key to disagree with the
+ // JSON bytes and breaking RFC 7638 thumbprint uniqueness.
+ if bigE.BitLen() >= bits.UintSize {
+ return nil, fmt.Errorf(`rsa public exponent too large for this platform: %d bits (max %d)`, bigE.BitLen(), bits.UintSize-1)
+ }
+
+ return bigE, nil
+}
+
+func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) error {
+ bigE, err := validateRSAModulusAndExponent(n, e)
+ if err != nil {
+ return err
+ }
+ key.N = new(big.Int).SetBytes(trimLeadingZeroBytes(n))
+ key.E = int(bigE.Int64())
+ return nil
}
var rsaConvertibleKeys = []reflect.Type{
@@ -132,26 +192,67 @@ func rsaJWKToRaw(key Key, hint any) (any, error) {
return nil, fmt.Errorf(`invalid destination object type %T for private RSA JWK: %w`, hint, ContinueError())
}
- locker, ok := key.(rlocker)
- if !ok {
+ // rlocker is unexported with unexported methods, so only our
+ // concrete types implement it. A successful assertion lets us
+ // type-assert to the concrete struct and read fields directly
+ // under a single batch lock. This avoids nested RLock (which
+ // deadlocks when a writer is pending) while preserving an
+ // atomic snapshot of all fields.
+ var od, oq, op, on, oe []byte
+ var odp, odq, oqi []byte
+ var hasDp, hasDq, hasQi bool
+ if locker, ok := key.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := key.(*rsaPrivateKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ od, oq, op, on, oe = concrete.d, concrete.q, concrete.p, concrete.n, concrete.e
+ if concrete.dp != nil {
+ odp, hasDp = concrete.dp, true
+ }
+ if concrete.dq != nil {
+ odq, hasDq = concrete.dq, true
+ }
+ if concrete.qi != nil {
+ oqi, hasQi = concrete.qi, true
+ }
+ locker.runlock()
+ } else {
+ // External implementation — use self-locking interface getters.
+ var ok bool
+ if od, ok = key.D(); !ok {
+ return nil, fmt.Errorf(`missing "d" value`)
+ }
+ if oq, ok = key.Q(); !ok {
+ return nil, fmt.Errorf(`missing "q" value`)
+ }
+ if op, ok = key.P(); !ok {
+ return nil, fmt.Errorf(`missing "p" value`)
+ }
+ if on, ok = key.N(); !ok {
+ return nil, fmt.Errorf(`missing "n" value`)
+ }
+ if oe, ok = key.E(); !ok {
+ return nil, fmt.Errorf(`missing "e" value`)
+ }
+ odp, hasDp = key.DP()
+ odq, hasDq = key.DQ()
+ oqi, hasQi = key.QI()
}
- od, ok := key.D()
- if !ok {
+ if od == nil {
return nil, fmt.Errorf(`missing "d" value`)
}
-
- oq, ok := key.Q()
- if !ok {
+ if oq == nil {
return nil, fmt.Errorf(`missing "q" value`)
}
-
- op, ok := key.P()
- if !ok {
+ if op == nil {
return nil, fmt.Errorf(`missing "p" value`)
}
+ if on == nil {
+ return nil, fmt.Errorf(`missing "n" value`)
+ }
+ if oe == nil {
+ return nil, fmt.Errorf(`missing "e" value`)
+ }
var d, q, p big.Int // note: do not use from sync.Pool
@@ -159,36 +260,27 @@ func rsaJWKToRaw(key Key, hint any) (any, error) {
q.SetBytes(oq)
p.SetBytes(op)
- // optional fields
var dp, dq, qi *big.Int
- if odp, ok := key.DP(); ok {
+ if hasDp {
dp = &big.Int{} // note: do not use from sync.Pool
dp.SetBytes(odp)
}
- if odq, ok := key.DQ(); ok {
+ if hasDq {
dq = &big.Int{} // note: do not use from sync.Pool
dq.SetBytes(odq)
}
- if oqi, ok := key.QI(); ok {
+ if hasQi {
qi = &big.Int{} // note: do not use from sync.Pool
qi.SetBytes(oqi)
}
- n, ok := key.N()
- if !ok {
- return nil, fmt.Errorf(`missing "n" value`)
- }
-
- e, ok := key.E()
- if !ok {
- return nil, fmt.Errorf(`missing "e" value`)
- }
-
var privkey rsa.PrivateKey
- buildRSAPublicKey(&privkey.PublicKey, n, e)
+ if err := buildRSAPublicKey(&privkey.PublicKey, on, oe); err != nil {
+ return nil, fmt.Errorf(`failed to build rsa.PublicKey: %w`, err)
+ }
privkey.D = &d
privkey.Primes = []*big.Int{&p, &q}
@@ -212,24 +304,34 @@ func rsaJWKToRaw(key Key, hint any) (any, error) {
return nil, fmt.Errorf(`invalid destination object type %T for public RSA JWK: %w`, hint, ContinueError())
}
- locker, ok := key.(rlocker)
- if !ok {
+ var n, e []byte
+ // See RSAPrivateKey case above for explanation of the rlocker pattern.
+ if locker, ok := key.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := key.(*rsaPublicKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ n, e = concrete.n, concrete.e
+ locker.runlock()
+ } else {
+ var ok bool
+ if n, ok = key.N(); !ok {
+ return nil, fmt.Errorf(`missing "n" value`)
+ }
+ if e, ok = key.E(); !ok {
+ return nil, fmt.Errorf(`missing "e" value`)
+ }
}
- n, ok := key.N()
- if !ok {
+ if n == nil {
return nil, fmt.Errorf(`missing "n" value`)
}
-
- e, ok := key.E()
- if !ok {
+ if e == nil {
return nil, fmt.Errorf(`missing "e" value`)
}
var pubkey rsa.PublicKey
- buildRSAPublicKey(&pubkey, n, e)
+ if err := buildRSAPublicKey(&pubkey, n, e); err != nil {
+ return nil, fmt.Errorf(`failed to build rsa.PublicKey: %w`, err)
+ }
return &pubkey, nil
@@ -270,36 +372,46 @@ func (k *rsaPublicKey) PublicKey() (Key, error) {
// Thumbprint returns the JWK thumbprint using the indicated
// hashing algorithm, according to RFC 7638
-func (k rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
- var key rsa.PrivateKey
- if err := Export(&k, &key); err != nil {
- return nil, fmt.Errorf(`failed to export RSA private key: %w`, err)
- }
- return rsaThumbprint(hash, &key.PublicKey)
+ return rsaThumbprint(hash, k.n, k.e)
}
-func (k rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
+func (k *rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
k.mu.RLock()
defer k.mu.RUnlock()
- var key rsa.PublicKey
- if err := Export(&k, &key); err != nil {
- return nil, fmt.Errorf(`failed to export RSA public key: %w`, err)
- }
- return rsaThumbprint(hash, &key)
+ return rsaThumbprint(hash, k.n, k.e)
}
-func rsaThumbprint(hash crypto.Hash, key *rsa.PublicKey) ([]byte, error) {
+// trimLeadingZeroBytes strips leading zero bytes. RFC 7638 requires the
+// minimal big-endian representation of n/e for canonical JSON.
+func trimLeadingZeroBytes(b []byte) []byte {
+ for len(b) > 0 && b[0] == 0 {
+ b = b[1:]
+ }
+ return b
+}
+
+func rsaThumbprint(hash crypto.Hash, n, e []byte) ([]byte, error) {
+ n = trimLeadingZeroBytes(n)
+ e = trimLeadingZeroBytes(e)
+ if len(n) == 0 {
+ return nil, fmt.Errorf(`failed to compute rsa thumbprint: missing "n" value`)
+ }
+ if len(e) == 0 {
+ return nil, fmt.Errorf(`failed to compute rsa thumbprint: missing "e" value`)
+ }
+
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
buf.WriteString(`{"e":"`)
- buf.WriteString(base64.EncodeUint64ToString(uint64(key.E)))
+ buf.WriteString(base64.EncodeToString(e))
buf.WriteString(`","kty":"RSA","n":"`)
- buf.WriteString(base64.EncodeToString(key.N.Bytes()))
+ buf.WriteString(base64.EncodeToString(n))
buf.WriteString(`"}`)
h := hash.New()
@@ -322,15 +434,8 @@ func validateRSAKey(key interface {
if !ok {
return fmt.Errorf(`missing "e" value`)
}
-
- if len(n) == 0 {
- // Ideally we would like to check for the actual length, but unlike
- // EC keys, we have nothing in the key itself that will tell us
- // how many bits this key should have.
- return fmt.Errorf(`missing "n" value`)
- }
- if len(e) == 0 {
- return fmt.Errorf(`missing "e" value`)
+ if _, err := validateRSAModulusAndExponent(n, e); err != nil {
+ return err
}
if checkPrivate {
if priv, ok := key.(keyWithD); ok {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go
index 8e2a4f085b..3ef59aec6f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/rsa_gen.go
@@ -15,6 +15,7 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk/internal/registry"
)
const (
@@ -46,7 +47,7 @@ type rsaPublicKey struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -55,28 +56,29 @@ var _ Key = &rsaPublicKey{}
func newRSAPublicKey() *rsaPublicKey {
return &rsaPublicKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h rsaPublicKey) KeyType() jwa.KeyType {
+func (h *rsaPublicKey) KeyType() jwa.KeyType {
return jwa.RSA()
}
-func (h rsaPublicKey) rlock() {
+func (h *rsaPublicKey) rlock() {
h.mu.RLock()
}
-func (h rsaPublicKey) runlock() {
+func (h *rsaPublicKey) runlock() {
h.mu.RUnlock()
}
-func (h rsaPublicKey) IsPrivate() bool {
+func (h *rsaPublicKey) IsPrivate() bool {
return false
}
func (h *rsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -84,6 +86,8 @@ func (h *rsaPublicKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *rsaPublicKey) E() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.e != nil {
return h.e, true
}
@@ -91,6 +95,8 @@ func (h *rsaPublicKey) E() ([]byte, bool) {
}
func (h *rsaPublicKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -98,6 +104,8 @@ func (h *rsaPublicKey) KeyID() (string, bool) {
}
func (h *rsaPublicKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -105,6 +113,8 @@ func (h *rsaPublicKey) KeyOps() (KeyOperationList, bool) {
}
func (h *rsaPublicKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -112,6 +122,8 @@ func (h *rsaPublicKey) KeyUsage() (string, bool) {
}
func (h *rsaPublicKey) N() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.n != nil {
return h.n, true
}
@@ -119,10 +131,17 @@ func (h *rsaPublicKey) N() ([]byte, bool) {
}
func (h *rsaPublicKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *rsaPublicKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -130,6 +149,8 @@ func (h *rsaPublicKey) X509CertThumbprint() (string, bool) {
}
func (h *rsaPublicKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -137,6 +158,8 @@ func (h *rsaPublicKey) X509CertThumbprintS256() (string, bool) {
}
func (h *rsaPublicKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -299,7 +322,12 @@ func (h *rsaPublicKey) setNoLock(name string, value any) error {
return nil
case RSAEKey:
if v, ok := value.([]byte); ok {
- h.e = v
+ if v == nil {
+ h.e = nil
+ } else {
+ h.e = make([]byte, len(v))
+ copy(h.e, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSAEKey, value)
@@ -333,7 +361,12 @@ func (h *rsaPublicKey) setNoLock(name string, value any) error {
}
case RSANKey:
if v, ok := value.([]byte); ok {
- h.n = v
+ if v == nil {
+ h.n = nil
+ } else {
+ h.n = make([]byte, len(v))
+ copy(h.n, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSANKey, value)
@@ -452,7 +485,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -474,7 +507,7 @@ LOOP:
return fmt.Errorf(`failed to decode value for key %s: %w`, RSAEKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -484,7 +517,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case RSANKey:
@@ -498,15 +531,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -519,7 +552,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -539,84 +572,135 @@ LOOP:
return nil
}
-func (h rsaPublicKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 10)
- data[KeyTypeKey] = jwa.RSA()
- fields = append(fields, KeyTypeKey)
+func (h *rsaPublicKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.RSA())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.e != nil {
- data[RSAEKey] = h.e
- fields = append(fields, RSAEKey)
+ v, err := json.Marshal(base64.EncodeToString(h.e))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAEKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSAEKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.n != nil {
- data[RSANKey] = h.n
- fields = append(fields, RSANKey)
+ v, err := json.Marshal(base64.EncodeToString(h.n))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSANKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSANKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *rsaPublicKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -691,7 +775,7 @@ type rsaPrivateKey struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -700,28 +784,29 @@ var _ Key = &rsaPrivateKey{}
func newRSAPrivateKey() *rsaPrivateKey {
return &rsaPrivateKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h rsaPrivateKey) KeyType() jwa.KeyType {
+func (h *rsaPrivateKey) KeyType() jwa.KeyType {
return jwa.RSA()
}
-func (h rsaPrivateKey) rlock() {
+func (h *rsaPrivateKey) rlock() {
h.mu.RLock()
}
-func (h rsaPrivateKey) runlock() {
+func (h *rsaPrivateKey) runlock() {
h.mu.RUnlock()
}
-func (h rsaPrivateKey) IsPrivate() bool {
+func (h *rsaPrivateKey) IsPrivate() bool {
return true
}
func (h *rsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -729,6 +814,8 @@ func (h *rsaPrivateKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *rsaPrivateKey) D() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.d != nil {
return h.d, true
}
@@ -736,6 +823,8 @@ func (h *rsaPrivateKey) D() ([]byte, bool) {
}
func (h *rsaPrivateKey) DP() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.dp != nil {
return h.dp, true
}
@@ -743,6 +832,8 @@ func (h *rsaPrivateKey) DP() ([]byte, bool) {
}
func (h *rsaPrivateKey) DQ() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.dq != nil {
return h.dq, true
}
@@ -750,6 +841,8 @@ func (h *rsaPrivateKey) DQ() ([]byte, bool) {
}
func (h *rsaPrivateKey) E() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.e != nil {
return h.e, true
}
@@ -757,6 +850,8 @@ func (h *rsaPrivateKey) E() ([]byte, bool) {
}
func (h *rsaPrivateKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -764,6 +859,8 @@ func (h *rsaPrivateKey) KeyID() (string, bool) {
}
func (h *rsaPrivateKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -771,6 +868,8 @@ func (h *rsaPrivateKey) KeyOps() (KeyOperationList, bool) {
}
func (h *rsaPrivateKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -778,6 +877,8 @@ func (h *rsaPrivateKey) KeyUsage() (string, bool) {
}
func (h *rsaPrivateKey) N() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.n != nil {
return h.n, true
}
@@ -785,6 +886,8 @@ func (h *rsaPrivateKey) N() ([]byte, bool) {
}
func (h *rsaPrivateKey) P() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.p != nil {
return h.p, true
}
@@ -792,6 +895,8 @@ func (h *rsaPrivateKey) P() ([]byte, bool) {
}
func (h *rsaPrivateKey) Q() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.q != nil {
return h.q, true
}
@@ -799,6 +904,8 @@ func (h *rsaPrivateKey) Q() ([]byte, bool) {
}
func (h *rsaPrivateKey) QI() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.qi != nil {
return h.qi, true
}
@@ -806,10 +913,17 @@ func (h *rsaPrivateKey) QI() ([]byte, bool) {
}
func (h *rsaPrivateKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *rsaPrivateKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -817,6 +931,8 @@ func (h *rsaPrivateKey) X509CertThumbprint() (string, bool) {
}
func (h *rsaPrivateKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -824,6 +940,8 @@ func (h *rsaPrivateKey) X509CertThumbprintS256() (string, bool) {
}
func (h *rsaPrivateKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -1046,25 +1164,45 @@ func (h *rsaPrivateKey) setNoLock(name string, value any) error {
return nil
case RSADKey:
if v, ok := value.([]byte); ok {
- h.d = v
+ if v == nil {
+ h.d = nil
+ } else {
+ h.d = make([]byte, len(v))
+ copy(h.d, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSADKey, value)
case RSADPKey:
if v, ok := value.([]byte); ok {
- h.dp = v
+ if v == nil {
+ h.dp = nil
+ } else {
+ h.dp = make([]byte, len(v))
+ copy(h.dp, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSADPKey, value)
case RSADQKey:
if v, ok := value.([]byte); ok {
- h.dq = v
+ if v == nil {
+ h.dq = nil
+ } else {
+ h.dq = make([]byte, len(v))
+ copy(h.dq, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSADQKey, value)
case RSAEKey:
if v, ok := value.([]byte); ok {
- h.e = v
+ if v == nil {
+ h.e = nil
+ } else {
+ h.e = make([]byte, len(v))
+ copy(h.e, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSAEKey, value)
@@ -1098,25 +1236,45 @@ func (h *rsaPrivateKey) setNoLock(name string, value any) error {
}
case RSANKey:
if v, ok := value.([]byte); ok {
- h.n = v
+ if v == nil {
+ h.n = nil
+ } else {
+ h.n = make([]byte, len(v))
+ copy(h.n, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSANKey, value)
case RSAPKey:
if v, ok := value.([]byte); ok {
- h.p = v
+ if v == nil {
+ h.p = nil
+ } else {
+ h.p = make([]byte, len(v))
+ copy(h.p, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSAPKey, value)
case RSAQKey:
if v, ok := value.([]byte); ok {
- h.q = v
+ if v == nil {
+ h.q = nil
+ } else {
+ h.q = make([]byte, len(v))
+ copy(h.q, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSAQKey, value)
case RSAQIKey:
if v, ok := value.([]byte); ok {
- h.qi = v
+ if v == nil {
+ h.qi = nil
+ } else {
+ h.qi = make([]byte, len(v))
+ copy(h.qi, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, RSAQIKey, value)
@@ -1215,7 +1373,7 @@ func (k *rsaPrivateKey) SetDecodeCtx(dc json.DecodeCtx) {
k.dc = dc
}
-func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) error {
+func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) (retErr error) {
h.mu.Lock()
defer h.mu.Unlock()
h.algorithm = nil
@@ -1234,6 +1392,26 @@ func (h *rsaPrivateKey) UnmarshalJSON(buf []byte) error {
h.x509CertThumbprint = nil
h.x509CertThumbprintS256 = nil
h.x509URL = nil
+ defer func() {
+ if retErr != nil {
+ clear(h.d)
+ h.d = nil
+ clear(h.dp)
+ h.dp = nil
+ clear(h.dq)
+ h.dq = nil
+ clear(h.e)
+ h.e = nil
+ clear(h.n)
+ h.n = nil
+ clear(h.p)
+ h.p = nil
+ clear(h.q)
+ h.q = nil
+ clear(h.qi)
+ h.qi = nil
+ }
+ }()
dec := json.NewDecoder(bytes.NewReader(buf))
LOOP:
for {
@@ -1253,7 +1431,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -1287,7 +1465,7 @@ LOOP:
return fmt.Errorf(`failed to decode value for key %s: %w`, RSAEKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -1297,7 +1475,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case RSANKey:
@@ -1323,15 +1501,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -1344,7 +1522,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -1367,108 +1545,177 @@ LOOP:
return nil
}
-func (h rsaPrivateKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 16)
- data[KeyTypeKey] = jwa.RSA()
- fields = append(fields, KeyTypeKey)
+func (h *rsaPrivateKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.RSA())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.d != nil {
- data[RSADKey] = h.d
- fields = append(fields, RSADKey)
+ v, err := json.Marshal(base64.EncodeToString(h.d))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSADKey, Value: v})
}
if h.dp != nil {
- data[RSADPKey] = h.dp
- fields = append(fields, RSADPKey)
+ v, err := json.Marshal(base64.EncodeToString(h.dp))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADPKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSADPKey, Value: v})
}
if h.dq != nil {
- data[RSADQKey] = h.dq
- fields = append(fields, RSADQKey)
+ v, err := json.Marshal(base64.EncodeToString(h.dq))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSADQKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSADQKey, Value: v})
}
if h.e != nil {
- data[RSAEKey] = h.e
- fields = append(fields, RSAEKey)
+ v, err := json.Marshal(base64.EncodeToString(h.e))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAEKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSAEKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.n != nil {
- data[RSANKey] = h.n
- fields = append(fields, RSANKey)
+ v, err := json.Marshal(base64.EncodeToString(h.n))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSANKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSANKey, Value: v})
}
if h.p != nil {
- data[RSAPKey] = h.p
- fields = append(fields, RSAPKey)
+ v, err := json.Marshal(base64.EncodeToString(h.p))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAPKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSAPKey, Value: v})
}
if h.q != nil {
- data[RSAQKey] = h.q
- fields = append(fields, RSAQKey)
+ v, err := json.Marshal(base64.EncodeToString(h.q))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAQKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSAQKey, Value: v})
}
if h.qi != nil {
- data[RSAQIKey] = h.qi
- fields = append(fields, RSAQIKey)
+ v, err := json.Marshal(base64.EncodeToString(h.qi))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, RSAQIKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: RSAQIKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *rsaPrivateKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -1541,3 +1788,10 @@ func init() {
func RSAStandardFieldsFilter() KeyFilter {
return rsaStandardFields
}
+
+func init() {
+ registry.Register(jwa.RSA().String(), registry.Constructor{
+ Public: func() any { return newRSAPublicKey() },
+ Private: func() any { return newRSAPrivateKey() },
+ })
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go
index 6f339649a8..6b8c7aa564 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/set.go
@@ -27,8 +27,8 @@ func NewSet() Set {
}
func (s *set) Set(n string, v any) error {
- s.mu.RLock()
- defer s.mu.RUnlock()
+ s.mu.Lock()
+ defer s.mu.Unlock()
if n == keysKey {
vl, ok := v.([]Key)
@@ -95,8 +95,15 @@ func (s *set) AddKey(key Key) error {
s.mu.Lock()
defer s.mu.Unlock()
- if reflect.ValueOf(key).IsNil() {
- panic("nil key")
+ rv := reflect.ValueOf(key)
+ if !rv.IsValid() {
+ return fmt.Errorf(`(jwk.Set).AddKey: nil key`)
+ }
+ switch rv.Kind() {
+ case reflect.Ptr, reflect.Interface, reflect.Chan, reflect.Func, reflect.Map, reflect.Slice:
+ if rv.IsNil() {
+ return fmt.Errorf(`(jwk.Set).AddKey: nil key`)
+ }
}
if i := s.indexNL(key); i > -1 {
@@ -144,6 +151,8 @@ func (s *set) Clear() error {
}
func (s *set) Keys() []string {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
ret := make([]string, len(s.privateParams))
var i int
for k := range s.privateParams {
@@ -161,7 +170,8 @@ func (s *set) MarshalJSON() ([]byte, error) {
defer pool.BytesBuffer().Put(buf)
enc := json.NewEncoder(buf)
- fields := []string{keysKey}
+ fields := make([]string, 0, 1+len(s.privateParams))
+ fields = append(fields, keysKey)
for k := range s.privateParams {
fields = append(fields, k)
}
@@ -197,6 +207,20 @@ func (s *set) MarshalJSON() ([]byte, error) {
return ret, nil
}
+func (s *set) setMaxKeys(n int) {
+ s.maxKeys = n
+}
+
+func (s *set) setRejectDuplicateKID(v bool) {
+ s.rejectDuplicateKID = 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
+// cannot force allocation past the cap. This entry point requires
+// JWKS shape; bare JWK input is rejected here. Callers that don't
+// know the shape ahead of time should use [Parse], which dispatches.
func (s *set) UnmarshalJSON(data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
@@ -213,7 +237,12 @@ func (s *set) UnmarshalJSON(data []byte) error {
ignoreParseError = dc.IgnoreParseError()
}
- var sawKeysField bool
+ maxK := s.maxKeys
+ if maxK <= 0 {
+ maxK = int(maxKeys.Load())
+ }
+ rejectDupKid := s.rejectDuplicateKID || rejectDuplicateKID.Load()
+
dec := json.NewDecoder(bytes.NewReader(data))
LOOP:
for {
@@ -224,9 +253,7 @@ LOOP:
switch tok := tok.(type) {
case json.Delim:
- // Assuming we're doing everything correctly, we should ONLY
- // get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here.
- if tok == tokens.CloseCurlyBracket { // End of object
+ if tok == tokens.CloseCurlyBracket {
break LOOP
} else if tok != tokens.OpenCurlyBracket {
return fmt.Errorf(`expected '%c' but got '%c'`, tokens.OpenCurlyBracket, tok)
@@ -234,21 +261,54 @@ LOOP:
case string:
switch tok {
case "keys":
- sawKeysField = true
- var list []json.RawMessage
- if err := dec.Decode(&list); err != nil {
+ openTok, err := dec.Token()
+ if err != nil {
return fmt.Errorf(`failed to decode "keys": %w`, err)
}
+ openDelim, ok := openTok.(json.Delim)
+ if !ok || openDelim != tokens.OpenSquareBracket {
+ return fmt.Errorf(`failed to decode "keys": expected '%c' but got %v`, tokens.OpenSquareBracket, openTok)
+ }
- for i, keysrc := range list {
- key, err := ParseKey(keysrc, options...)
+ var seenKIDs map[string]struct{}
+ if rejectDupKid {
+ seenKIDs = make(map[string]struct{})
+ }
+ var i int
+ for dec.More() {
+ if i >= maxK {
+ return fmt.Errorf(`too many keys in "keys" array: max %d`, maxK)
+ }
+ var raw json.RawMessage
+ if err := dec.Decode(&raw); err != nil {
+ return fmt.Errorf(`failed to decode "keys": %w`, err)
+ }
+ key, err := ParseKey(raw, options...)
if err != nil {
if !ignoreParseError {
return fmt.Errorf(`failed to decode key #%d in "keys": %w`, i, err)
}
+ i++
continue
}
+ if seenKIDs != nil {
+ if kid, ok := key.KeyID(); ok && kid != "" {
+ if _, dup := seenKIDs[kid]; dup {
+ return fmt.Errorf(`duplicate "kid" %q in "keys" array`, kid)
+ }
+ seenKIDs[kid] = struct{}{}
+ }
+ }
s.keys = append(s.keys, key)
+ i++
+ }
+ closeTok, err := dec.Token()
+ if err != nil {
+ return fmt.Errorf(`failed to decode "keys": %w`, err)
+ }
+ closeDelim, ok := closeTok.(json.Delim)
+ if !ok || closeDelim != tokens.CloseSquareBracket {
+ return fmt.Errorf(`failed to decode "keys": expected '%c' but got %v`, tokens.CloseSquareBracket, closeTok)
}
default:
var v any
@@ -259,19 +319,6 @@ LOOP:
}
}
}
-
- // This is really silly, but we can only detect the
- // lack of the "keys" field after going through the
- // entire object once
- // Not checking for len(s.keys) == 0, because it could be
- // an empty key set
- if !sawKeysField {
- key, err := ParseKey(data, options...)
- if err != nil {
- return fmt.Errorf(`failed to parse sole key in key set`)
- }
- s.keys = append(s.keys, key)
- }
return nil
}
@@ -279,11 +326,7 @@ func (s *set) LookupKeyID(kid string) (Key, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
- for i := range s.Len() {
- key, ok := s.Key(i)
- if !ok {
- return nil, false
- }
+ for _, key := range s.keys {
gotkid, ok := key.KeyID()
if ok && gotkid == kid {
return key, true
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go
index 7db5e1591a..da4bff433b 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric.go
@@ -4,13 +4,14 @@ import (
"crypto"
"fmt"
"reflect"
+ "slices"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/jwa"
)
func init() {
- RegisterKeyExporter(jwa.OctetSeq(), KeyExportFunc(octetSeqToRaw))
+ RegisterKeyExporter(KeyKind(jwa.OctetSeq().String()), KeyExportFunc(octetSeqToRaw))
}
func (k *symmetricKey) Import(rawKey []byte) error {
@@ -21,7 +22,7 @@ func (k *symmetricKey) Import(rawKey []byte) error {
return fmt.Errorf(`non-empty []byte key required`)
}
- k.octets = rawKey
+ k.octets = slices.Clone(rawKey)
return nil
}
@@ -44,14 +45,27 @@ func octetSeqToRaw(key Key, hint any) (any, error) {
return nil, fmt.Errorf(`invalid destination object type %T for symmetric key: %w`, hint, ContinueError())
}
- locker, ok := key.(rlocker)
- if ok {
+ // rlocker is unexported with unexported methods, so only our
+ // concrete types implement it. A successful assertion lets us
+ // type-assert to the concrete struct and read fields directly
+ // under a single batch lock. This avoids nested RLock (which
+ // deadlocks when a writer is pending) while preserving an
+ // atomic snapshot of all fields.
+ var ooctets []byte
+ if locker, ok := key.(rlocker); ok {
locker.rlock()
- defer locker.runlock()
+ concrete := key.(*symmetricKey) //nolint:forcetypeassert // rlocker is unexported; only our concrete types implement it
+ ooctets = concrete.octets
+ locker.runlock()
+ } else {
+ // External implementation — use self-locking interface getters.
+ var ok bool
+ if ooctets, ok = key.Octets(); !ok {
+ return nil, fmt.Errorf(`jwk.SymmetricKey: missing "k" field`)
+ }
}
- ooctets, ok := key.Octets()
- if !ok {
+ if ooctets == nil {
return nil, fmt.Errorf(`jwk.SymmetricKey: missing "k" field`)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go
index bfd2f8497d..900ed6537b 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/symmetric_gen.go
@@ -15,6 +15,7 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk/internal/registry"
)
const (
@@ -37,7 +38,7 @@ type symmetricKey struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc json.DecodeCtx
}
@@ -46,24 +47,25 @@ var _ Key = &symmetricKey{}
func newSymmetricKey() *symmetricKey {
return &symmetricKey{
- mu: &sync.RWMutex{},
privateParams: make(map[string]any),
}
}
-func (h symmetricKey) KeyType() jwa.KeyType {
+func (h *symmetricKey) KeyType() jwa.KeyType {
return jwa.OctetSeq()
}
-func (h symmetricKey) rlock() {
+func (h *symmetricKey) rlock() {
h.mu.RLock()
}
-func (h symmetricKey) runlock() {
+func (h *symmetricKey) runlock() {
h.mu.RUnlock()
}
func (h *symmetricKey) Algorithm() (jwa.KeyAlgorithm, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.algorithm != nil {
return *(h.algorithm), true
}
@@ -71,6 +73,8 @@ func (h *symmetricKey) Algorithm() (jwa.KeyAlgorithm, bool) {
}
func (h *symmetricKey) KeyID() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyID != nil {
return *(h.keyID), true
}
@@ -78,6 +82,8 @@ func (h *symmetricKey) KeyID() (string, bool) {
}
func (h *symmetricKey) KeyOps() (KeyOperationList, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyOps != nil {
return *(h.keyOps), true
}
@@ -85,6 +91,8 @@ func (h *symmetricKey) KeyOps() (KeyOperationList, bool) {
}
func (h *symmetricKey) KeyUsage() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.keyUsage != nil {
return *(h.keyUsage), true
}
@@ -92,6 +100,8 @@ func (h *symmetricKey) KeyUsage() (string, bool) {
}
func (h *symmetricKey) Octets() ([]byte, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.octets != nil {
return h.octets, true
}
@@ -99,10 +109,17 @@ func (h *symmetricKey) Octets() ([]byte, bool) {
}
func (h *symmetricKey) X509CertChain() (*cert.Chain, bool) {
- return h.x509CertChain, true
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.x509CertChain != nil {
+ return h.x509CertChain, true
+ }
+ return nil, false
}
func (h *symmetricKey) X509CertThumbprint() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprint != nil {
return *(h.x509CertThumbprint), true
}
@@ -110,6 +127,8 @@ func (h *symmetricKey) X509CertThumbprint() (string, bool) {
}
func (h *symmetricKey) X509CertThumbprintS256() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509CertThumbprintS256 != nil {
return *(h.x509CertThumbprintS256), true
}
@@ -117,6 +136,8 @@ func (h *symmetricKey) X509CertThumbprintS256() (string, bool) {
}
func (h *symmetricKey) X509URL() (string, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
if h.x509URL != nil {
return *(h.x509URL), true
}
@@ -297,7 +318,12 @@ func (h *symmetricKey) setNoLock(name string, value any) error {
}
case SymmetricOctetsKey:
if v, ok := value.([]byte); ok {
- h.octets = v
+ if v == nil {
+ h.octets = nil
+ } else {
+ h.octets = make([]byte, len(v))
+ copy(h.octets, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, SymmetricOctetsKey, value)
@@ -382,7 +408,7 @@ func (k *symmetricKey) SetDecodeCtx(dc json.DecodeCtx) {
k.dc = dc
}
-func (h *symmetricKey) UnmarshalJSON(buf []byte) error {
+func (h *symmetricKey) UnmarshalJSON(buf []byte) (retErr error) {
h.mu.Lock()
defer h.mu.Unlock()
h.algorithm = nil
@@ -394,6 +420,12 @@ func (h *symmetricKey) UnmarshalJSON(buf []byte) error {
h.x509CertThumbprint = nil
h.x509CertThumbprintS256 = nil
h.x509URL = nil
+ defer func() {
+ if retErr != nil {
+ clear(h.octets)
+ h.octets = nil
+ }
+ }()
dec := json.NewDecoder(bytes.NewReader(buf))
LOOP:
for {
@@ -413,7 +445,7 @@ LOOP:
case string: // Objects can only have string keys
switch tok {
case KeyTypeKey:
- val, err := json.ReadNextStringToken(dec)
+ val, err := json.ReadNextStringToken(dec, h.dc)
if err != nil {
return fmt.Errorf(`error reading token: %w`, err)
}
@@ -431,7 +463,7 @@ LOOP:
}
h.algorithm = &alg
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case KeyOpsKey:
@@ -441,7 +473,7 @@ LOOP:
}
h.keyOps = &decoded
case KeyUsageKey:
- if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyUsage, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
}
case SymmetricOctetsKey:
@@ -455,15 +487,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, h.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -476,7 +508,7 @@ LOOP:
}
}
}
- decoded, err := registry.Decode(dec, tok)
+ decoded, err := fieldRegistry.Decode(dec, tok)
if err == nil {
h.setNoLock(tok, decoded)
continue
@@ -493,80 +525,128 @@ LOOP:
return nil
}
-func (h symmetricKey) MarshalJSON() ([]byte, error) {
- data := make(map[string]any)
- fields := make([]string, 0, 9)
- data[KeyTypeKey] = jwa.OctetSeq()
- fields = append(fields, KeyTypeKey)
+func (h *symmetricKey) makePairs() ([]fieldPair, error) {
+ pairs := getFieldPairList()
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ {
+ v, err := json.Marshal(jwa.OctetSeq())
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyTypeKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyTypeKey, Value: v})
+ }
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- fields = append(fields, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: AlgorithmKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- fields = append(fields, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyIDKey, Value: v})
}
if h.keyOps != nil {
- data[KeyOpsKey] = *(h.keyOps)
- fields = append(fields, KeyOpsKey)
+ v, err := json.Marshal(*(h.keyOps))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyOpsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyOpsKey, Value: v})
}
if h.keyUsage != nil {
- data[KeyUsageKey] = *(h.keyUsage)
- fields = append(fields, KeyUsageKey)
+ v, err := json.Marshal(*(h.keyUsage))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyUsageKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: KeyUsageKey, Value: v})
}
if h.octets != nil {
- data[SymmetricOctetsKey] = h.octets
- fields = append(fields, SymmetricOctetsKey)
+ v, err := json.Marshal(base64.EncodeToString(h.octets))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, SymmetricOctetsKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: SymmetricOctetsKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- fields = append(fields, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- fields = append(fields, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- fields = append(fields, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- fields = append(fields, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, fieldPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- fields = append(fields, k)
- }
-
- sort.Strings(fields)
- buf := pool.BytesBuffer().Get()
- defer pool.BytesBuffer().Put(buf)
- buf.WriteByte(tokens.OpenCurlyBracket)
- enc := json.NewEncoder(buf)
- for i, f := range fields {
- if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(f)
- buf.WriteString(`":`)
- v := data[f]
+ var encoded []byte
switch v := v.(type) {
case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
}
- buf.Truncate(buf.Len() - 1)
}
+ pairs = append(pairs, fieldPair{Name: k, Value: encoded})
+ }
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *symmetricKey) MarshalJSON() ([]byte, error) {
+ buf := pool.BytesBuffer().Get()
+ defer pool.BytesBuffer().Put(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
+ buf.WriteByte(tokens.OpenCurlyBracket)
+
+ for i, pair := range pairs {
+ if i > 0 {
+ buf.WriteByte(tokens.Comma)
+ }
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putFieldPairList(pairs)
return ret, nil
}
@@ -618,3 +698,9 @@ func init() {
func SymmetricStandardFieldsFilter() KeyFilter {
return symmetricStandardFields
}
+
+func init() {
+ registry.Register(jwa.OctetSeq().String(), registry.Constructor{
+ Private: func() any { return newSymmetricKey() },
+ })
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go
index 0b0df701ae..81db97f82e 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/whitelist.go
@@ -5,8 +5,16 @@ import "github.com/lestrrat-go/httprc/v3"
type Whitelist = httprc.Whitelist
type WhitelistFunc = httprc.WhitelistFunc
-// InsecureWhitelist is an alias to httprc.InsecureWhitelist. Use
-// functions in the `httprc` package to interact with this type.
+// InsecureWhitelist is a Whitelist implementation (aliased to
+// httprc.InsecureWhitelist) that allows every URL jwk.Fetch() is asked to
+// retrieve. It is the library's default, which keeps first-time usage
+// simple: callers with a hard-coded JWKS URL do not have to configure
+// anything.
+//
+// Do NOT use InsecureWhitelist in any code path where the URL originates
+// from untrusted input (for example, the `jku` header of a JWS). For
+// those paths, construct a MapWhitelist, RegexpWhitelist, or custom
+// Whitelist and pass it via jwk.WithFetchWhitelist().
type InsecureWhitelist = httprc.InsecureWhitelist
func NewInsecureWhitelist() InsecureWhitelist {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go b/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go
index f06063c6ed..006ead224d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwk/x509.go
@@ -154,8 +154,15 @@ func (f X509DecodeFunc) DecodeX509(dst any, block *pem.Block) error {
return f(dst, block)
}
-var muX509Decoders sync.Mutex
-var x509Decoders = map[any]int{}
+// x509Decoders holds every registered decoder keyed by its caller-supplied
+// ident. x509DecoderIdents keeps the registration order so decodeX509 tries
+// decoders in a stable, deterministic sequence. x509DecoderList is the
+// read-optimized snapshot handed out to readers; every mutation replaces it
+// with a freshly allocated slice so readers can iterate their captured
+// header without any further synchronization.
+var muX509Decoders sync.RWMutex
+var x509Decoders = map[any]X509Decoder{}
+var x509DecoderIdents = []any{}
var x509DecoderList = []X509Decoder{}
type identDefaultX509Decoder struct{}
@@ -180,8 +187,14 @@ func RegisterX509Decoder(ident any, decoder X509Decoder) {
return // already registered
}
- x509Decoders[ident] = len(x509DecoderList)
- x509DecoderList = append(x509DecoderList, decoder)
+ x509Decoders[ident] = decoder
+ x509DecoderIdents = append(x509DecoderIdents, ident)
+ // Publish a fresh slice so any reader that snapshotted the previous
+ // one keeps iterating its own immutable copy.
+ next := make([]X509Decoder, len(x509DecoderList)+1)
+ copy(next, x509DecoderList)
+ next[len(x509DecoderList)] = decoder
+ x509DecoderList = next
}
// UnregisterX509Decoder unregisters the X509Decoder identified by the given identifier.
@@ -191,26 +204,27 @@ func RegisterX509Decoder(ident any, decoder X509Decoder) {
func UnregisterX509Decoder(ident any) {
muX509Decoders.Lock()
defer muX509Decoders.Unlock()
- idx, ok := x509Decoders[ident]
- if !ok {
+ if _, ok := x509Decoders[ident]; !ok {
return // not registered
}
delete(x509Decoders, ident)
- l := len(x509DecoderList)
- switch idx {
- case l - 1:
- // if the last element, just truncate the slice
- x509DecoderList = x509DecoderList[:l-1]
- case 0:
- // if the first element, just shift the slice
- x509DecoderList = x509DecoderList[1:]
- default:
- // if the element is in the middle, remove it by slicing
- // and appending the two slices together
- x509DecoderList = append(x509DecoderList[:idx], x509DecoderList[idx+1:]...)
+ // Rebuild idents and the reader-facing slice as fresh allocations,
+ // preserving registration order and filtering the removed ident.
+ // Mutating the old slices in place would race with readers in
+ // decodeX509 that iterate a captured snapshot without holding RLock.
+ nextIdents := make([]any, 0, len(x509DecoderIdents)-1)
+ nextList := make([]X509Decoder, 0, len(x509DecoderList)-1)
+ for _, id := range x509DecoderIdents {
+ if id == ident {
+ continue
+ }
+ nextIdents = append(nextIdents, id)
+ nextList = append(nextList, x509Decoders[id])
}
+ x509DecoderIdents = nextIdents
+ x509DecoderList = nextList
}
// decodeX509 decodes a PEM encoded ASN.1 DER format into the given destination.
@@ -222,8 +236,12 @@ func decodeX509(dst any, src []byte) error {
return fmt.Errorf(`failed to decode PEM data`)
}
+ muX509Decoders.RLock()
+ decoders := x509DecoderList
+ muX509Decoders.RUnlock()
+
var errs []error
- for _, d := range x509DecoderList {
+ for _, d := range decoders {
if err := d.DecodeX509(dst, block); err != nil {
errs = append(errs, err)
continue
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel
index 920d3f87b1..32dbdd1881 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/BUILD.bazel
@@ -18,6 +18,7 @@ go_library(
"signer.go",
"sign_context.go",
"signature_builder.go",
+ "streaming_detached.go",
"verifier.go",
"verify_context.go",
],
@@ -28,7 +29,6 @@ go_library(
"//internal/base64",
"//internal/ecutil",
"//internal/json",
- "//internal/jwxio",
"//internal/tokens",
"//internal/keyconv",
"//internal/pool",
@@ -39,6 +39,7 @@ go_library(
"//jws/legacy",
"//transform",
"@com_github_lestrrat_go_blackmagic//:blackmagic",
+ "@com_github_lestrrat_go_dsig//:dsig",
"@com_github_lestrrat_go_option_v2//:option",
],
)
@@ -53,6 +54,7 @@ go_test(
"message_test.go",
"options_gen_test.go",
"signer_test.go",
+ "streaming_detached_test.go",
],
embed = [":jws"],
deps = [
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go
index d5e1762a6a..e4445bd547 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/errors.go
@@ -1,14 +1,81 @@
package jws
import (
+ "errors"
"fmt"
)
+// errCritPresent is returned by VerifyCompactFast when the protected
+// header carries a "crit" list. The fast path cannot enforce RFC 7515
+// §4.1.11 (it has no WithCritExtension allowlist), so it refuses rather
+// than silently accepting. The sentinel is wrapped in verifyError at the
+// return site so the resulting error matches BOTH errors.Is(err,
+// jws.ErrCritPresent()) (the specific reason) AND errors.Is(err,
+// jws.VerifyError()) (the general class), letting callers choose the
+// classification granularity that fits their code path.
+var errCritPresent = errors.New("VerifyCompactFast: protected header contains \"crit\"; use jws.Verify")
+
+// ErrCritPresent returns the sentinel error returned by VerifyCompactFast
+// when the protected header contains a "crit" list. The error returned
+// from VerifyCompactFast also matches jws.VerifyError(), so callers that
+// only branch on the general class still classify the refusal correctly.
+func ErrCritPresent() error {
+ return errCritPresent
+}
+
+// errB64Present is returned by VerifyCompactFast when the protected
+// header carries a "b64" entry (typically b64=false per RFC 7797). The
+// fast path assumes the default b64=true encoding for both the
+// signing-input reconstruction and the post-verify payload decode; a
+// b64=false message signed under non-conformant rules (b64 not declared
+// in "crit") would otherwise verify cryptographically while returning
+// a decoded payload that differs from the producer's intent. Refusing
+// here defers such messages to jws.Verify, which has the
+// WithDetachedPayload and WithCritExtension machinery to handle b64=false
+// correctly. As with errCritPresent, the sentinel is wrapped in
+// verifyError at the return site so the resulting error matches both
+// errors.Is(err, jws.ErrB64Present()) and errors.Is(err, jws.VerifyError()).
+var errB64Present = errors.New("VerifyCompactFast: protected header contains \"b64\"; use jws.Verify")
+
+// ErrB64Present returns the sentinel error returned by VerifyCompactFast
+// when the protected header contains a "b64" entry. The error returned
+// from VerifyCompactFast also matches jws.VerifyError(), so callers that
+// only branch on the general class still classify the refusal correctly.
+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.
+func ErrUnclassifiableKey() error {
+ return errUnclassifiableKey
+}
+
type signError struct {
error
}
-var errDefaultSignError = signerr(`unknown error`)
+const (
+ prefixJwsSign = `jws.Sign`
+ prefixJwsCompact = `jws.Compact`
+)
+
+var errDefaultSignError = makeSignError(prefixJwsSign, `unknown error`)
// SignError returns an error that can be passed to `errors.Is` to check if the error is a sign error.
func SignError() error {
@@ -24,8 +91,8 @@ func (signError) Is(err error) bool {
return ok
}
-func signerr(f string, args ...any) error {
- return signError{fmt.Errorf(`jws.Sign: `+f, args...)}
+func makeSignError(prefix string, f string, args ...any) error {
+ return signError{fmt.Errorf(prefix+`: `+f, args...)}
}
// This error is returned when jws.Verify fails, but note that there's another type of
@@ -34,7 +101,7 @@ type verifyError struct {
error
}
-var errDefaultVerifyError = verifyerr(`unknown error`)
+var errDefaultVerifyError = makeVerifyError(`unknown error`)
// VerifyError returns an error that can be passed to `errors.Is` to check if the error is a verify error.
func VerifyError() error {
@@ -50,7 +117,7 @@ func (verifyError) Is(err error) bool {
return ok
}
-func verifyerr(f string, args ...any) error {
+func makeVerifyError(f string, args ...any) error {
return verifyError{fmt.Errorf(`jws.Verify: `+f, args...)}
}
@@ -79,7 +146,7 @@ type parseError struct {
error
}
-var errDefaultParseError = parseerr(`unknown error`)
+var errDefaultParseError = makeParseError(`jws.Parse`, `unknown error`)
// ParseError returns an error that can be passed to `errors.Is` to check if the error is a parse error.
func ParseError() error {
@@ -95,18 +162,6 @@ func (parseError) Is(err error) bool {
return ok
}
-func bparseerr(prefix string, f string, args ...any) error {
+func makeParseError(prefix string, f string, args ...any) error {
return parseError{fmt.Errorf(prefix+": "+f, args...)}
}
-
-func parseerr(f string, args ...any) error {
- return bparseerr(`jws.Parse`, f, args...)
-}
-
-func sparseerr(f string, args ...any) error {
- return bparseerr(`jws.ParseString`, f, args...)
-}
-
-func rparseerr(f string, args ...any) error {
- return bparseerr(`jws.ParseReader`, f, args...)
-}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go
index 28ebd2ea0e..73d746099a 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/es256k.go
@@ -8,5 +8,5 @@ import (
func init() {
// Register ES256K to EC algorithm family
- addAlgorithmForKeyType(jwa.EC(), jwa.ES256K())
+ RegisterAlgorithmForKeyType(jwa.EC(), jwa.ES256K())
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go
index 8465eda2b1..0628e626d2 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/headers_gen.go
@@ -20,6 +20,7 @@ import (
const (
AlgorithmKey = "alg"
+ B64Key = "b64"
ContentTypeKey = "cty"
CriticalKey = "crit"
JWKKey = "jwk"
@@ -41,6 +42,7 @@ const (
// In most cases, you likely want to use the protected headers, as this is part of the signed content.
type Headers interface {
Algorithm() (jwa.SignatureAlgorithm, bool)
+ B64() (bool, bool)
ContentType() (string, bool)
Critical() ([]string, bool)
JWK() (jwk.Key, bool)
@@ -72,10 +74,11 @@ type Headers interface {
}
// stdHeaderNames is a list of all standard header names defined in the JWS specification.
-var stdHeaderNames = []string{AlgorithmKey, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey}
+var stdHeaderNames = []string{AlgorithmKey, B64Key, ContentTypeKey, CriticalKey, JWKKey, JWKSetURLKey, KeyIDKey, TypeKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, X509URLKey}
type stdHeaders struct {
algorithm *jwa.SignatureAlgorithm // https://tools.ietf.org/html/rfc7515#section-4.1.1
+ b64 *bool // https://tools.ietf.org/html/rfc7797#section-3
contentType *string // https://tools.ietf.org/html/rfc7515#section-4.1.10
critical []string // https://tools.ietf.org/html/rfc7515#section-4.1.11
jwk jwk.Key // https://tools.ietf.org/html/rfc7515#section-4.1.3
@@ -87,15 +90,13 @@ type stdHeaders struct {
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
privateParams map[string]any
- mu *sync.RWMutex
+ mu sync.RWMutex
dc DecodeCtx
raw []byte // stores the raw version of the header so it can be used later
}
func NewHeaders() Headers {
- return &stdHeaders{
- mu: &sync.RWMutex{},
- }
+ return &stdHeaders{}
}
func (h *stdHeaders) Algorithm() (jwa.SignatureAlgorithm, bool) {
@@ -107,6 +108,15 @@ func (h *stdHeaders) Algorithm() (jwa.SignatureAlgorithm, bool) {
return *(h.algorithm), true
}
+func (h *stdHeaders) B64() (bool, bool) {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ if h.b64 == nil {
+ return false, false
+ }
+ return *(h.b64), true
+}
+
func (h *stdHeaders) ContentType() (string, bool) {
h.mu.RLock()
defer h.mu.RUnlock()
@@ -119,13 +129,13 @@ func (h *stdHeaders) ContentType() (string, bool) {
func (h *stdHeaders) Critical() ([]string, bool) {
h.mu.RLock()
defer h.mu.RUnlock()
- return h.critical, true
+ return h.critical, h.critical != nil
}
func (h *stdHeaders) JWK() (jwk.Key, bool) {
h.mu.RLock()
defer h.mu.RUnlock()
- return h.jwk, true
+ return h.jwk, h.jwk != nil
}
func (h *stdHeaders) JWKSetURL() (string, bool) {
@@ -158,7 +168,7 @@ func (h *stdHeaders) Type() (string, bool) {
func (h *stdHeaders) X509CertChain() (*cert.Chain, bool) {
h.mu.RLock()
defer h.mu.RUnlock()
- return h.x509CertChain, true
+ return h.x509CertChain, h.x509CertChain != nil
}
func (h *stdHeaders) X509CertThumbprint() (string, bool) {
@@ -190,6 +200,7 @@ func (h *stdHeaders) X509URL() (string, bool) {
func (h *stdHeaders) clear() {
h.algorithm = nil
+ h.b64 = nil
h.contentType = nil
h.critical = nil
h.jwk = nil
@@ -217,6 +228,8 @@ func (h *stdHeaders) SetDecodeCtx(dc DecodeCtx) {
}
func (h *stdHeaders) rawBuffer() []byte {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
return h.raw
}
@@ -232,6 +245,8 @@ func (h *stdHeaders) Has(name string) bool {
switch name {
case AlgorithmKey:
return h.algorithm != nil
+ case B64Key:
+ return h.b64 != nil
case ContentTypeKey:
return h.contentType != nil
case CriticalKey:
@@ -270,6 +285,14 @@ func (h *stdHeaders) Get(name string, dst any) error {
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
}
return nil
+ case B64Key:
+ if h.b64 == nil {
+ return fmt.Errorf(`field %q not found`, name)
+ }
+ if err := blackmagic.AssignIfCompatible(dst, *(h.b64)); err != nil {
+ return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
+ }
+ return nil
case ContentTypeKey:
if h.contentType == nil {
return fmt.Errorf(`field %q not found`, name)
@@ -383,6 +406,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error {
return nil
}
return fmt.Errorf("expecte jwa.SignatureAlgorithm, received %T", alg)
+ case B64Key:
+ if v, ok := value.(bool); ok {
+ h.b64 = &v
+ return nil
+ }
+ return fmt.Errorf(`invalid value for %s key: %T`, B64Key, value)
case ContentTypeKey:
if v, ok := value.(string); ok {
h.contentType = &v
@@ -391,7 +420,12 @@ func (h *stdHeaders) setNoLock(name string, value any) error {
return fmt.Errorf(`invalid value for %s key: %T`, ContentTypeKey, value)
case CriticalKey:
if v, ok := value.([]string); ok {
- h.critical = v
+ if v == nil {
+ h.critical = nil
+ } else {
+ h.critical = make([]string, len(v))
+ copy(h.critical, v)
+ }
return nil
}
return fmt.Errorf(`invalid value for %s key: %T`, CriticalKey, value)
@@ -458,6 +492,8 @@ func (h *stdHeaders) Remove(key string) error {
switch key {
case AlgorithmKey:
h.algorithm = nil
+ case B64Key:
+ h.b64 = nil
case ContentTypeKey:
h.contentType = nil
case CriticalKey:
@@ -512,8 +548,14 @@ LOOP:
return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err)
}
h.algorithm = &decoded
+ case B64Key:
+ var decoded bool
+ if err := dec.Decode(&decoded); err != nil {
+ return fmt.Errorf(`failed to decode value for key %s: %w`, B64Key, err)
+ }
+ h.b64 = &decoded
case ContentTypeKey:
- if err := json.AssignNextStringToken(&h.contentType, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.contentType, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, ContentTypeKey, err)
}
case CriticalKey:
@@ -533,15 +575,15 @@ LOOP:
}
h.jwk = key
case JWKSetURLKey:
- if err := json.AssignNextStringToken(&h.jwkSetURL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.jwkSetURL, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, JWKSetURLKey, err)
}
case KeyIDKey:
- if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.keyID, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
}
case TypeKey:
- if err := json.AssignNextStringToken(&h.typ, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.typ, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, TypeKey, err)
}
case X509CertChainKey:
@@ -551,15 +593,15 @@ LOOP:
}
h.x509CertChain = &decoded
case X509CertThumbprintKey:
- if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
}
case X509CertThumbprintS256Key:
- if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
}
case X509URLKey:
- if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
+ if err := json.AssignNextStringToken(&h.x509URL, dec, nil); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
}
default:
@@ -580,10 +622,13 @@ LOOP:
func (h *stdHeaders) Keys() []string {
h.mu.RLock()
defer h.mu.RUnlock()
- keys := make([]string, 0, 11+len(h.privateParams))
+ keys := make([]string, 0, 12+len(h.privateParams))
if h.algorithm != nil {
keys = append(keys, AlgorithmKey)
}
+ if h.b64 != nil {
+ keys = append(keys, B64Key)
+ }
if h.contentType != nil {
keys = append(keys, ContentTypeKey)
}
@@ -620,85 +665,161 @@ func (h *stdHeaders) Keys() []string {
return keys
}
-func (h stdHeaders) MarshalJSON() ([]byte, error) {
+type headerPair struct {
+ Name string
+ Value any
+}
+
+var headerPairPool = sync.Pool{
+ New: func() any {
+ return make([]headerPair, 0, 12)
+ },
+}
+
+func getHeaderPairList() []headerPair {
+ return headerPairPool.Get().([]headerPair)
+}
+
+func putHeaderPairList(list []headerPair) {
+ list = list[:0]
+ headerPairPool.Put(list)
+}
+
+func (h *stdHeaders) makePairs() ([]headerPair, error) {
+ pairs := getHeaderPairList()
h.mu.RLock()
- data := make(map[string]any)
- keys := make([]string, 0, 11+len(h.privateParams))
+ defer h.mu.RUnlock()
if h.algorithm != nil {
- data[AlgorithmKey] = *(h.algorithm)
- keys = append(keys, AlgorithmKey)
+ v, err := json.Marshal(*(h.algorithm))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, AlgorithmKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: AlgorithmKey, Value: v})
+ }
+ if h.b64 != nil {
+ v, err := json.Marshal(*(h.b64))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, B64Key, err)
+ }
+ pairs = append(pairs, headerPair{Name: B64Key, Value: v})
}
if h.contentType != nil {
- data[ContentTypeKey] = *(h.contentType)
- keys = append(keys, ContentTypeKey)
+ v, err := json.Marshal(*(h.contentType))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, ContentTypeKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: ContentTypeKey, Value: v})
}
if h.critical != nil {
- data[CriticalKey] = h.critical
- keys = append(keys, CriticalKey)
+ v, err := json.Marshal(h.critical)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, CriticalKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: CriticalKey, Value: v})
}
if h.jwk != nil {
- data[JWKKey] = h.jwk
- keys = append(keys, JWKKey)
+ v, err := json.Marshal(h.jwk)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: JWKKey, Value: v})
}
if h.jwkSetURL != nil {
- data[JWKSetURLKey] = *(h.jwkSetURL)
- keys = append(keys, JWKSetURLKey)
+ v, err := json.Marshal(*(h.jwkSetURL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, JWKSetURLKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: JWKSetURLKey, Value: v})
}
if h.keyID != nil {
- data[KeyIDKey] = *(h.keyID)
- keys = append(keys, KeyIDKey)
+ v, err := json.Marshal(*(h.keyID))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, KeyIDKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: KeyIDKey, Value: v})
}
if h.typ != nil {
- data[TypeKey] = *(h.typ)
- keys = append(keys, TypeKey)
+ v, err := json.Marshal(*(h.typ))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, TypeKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: TypeKey, Value: v})
}
if h.x509CertChain != nil {
- data[X509CertChainKey] = h.x509CertChain
- keys = append(keys, X509CertChainKey)
+ v, err := json.Marshal(h.x509CertChain)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertChainKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertChainKey, Value: v})
}
if h.x509CertThumbprint != nil {
- data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
- keys = append(keys, X509CertThumbprintKey)
+ v, err := json.Marshal(*(h.x509CertThumbprint))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertThumbprintKey, Value: v})
}
if h.x509CertThumbprintS256 != nil {
- data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
- keys = append(keys, X509CertThumbprintS256Key)
+ v, err := json.Marshal(*(h.x509CertThumbprintS256))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509CertThumbprintS256Key, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509CertThumbprintS256Key, Value: v})
}
if h.x509URL != nil {
- data[X509URLKey] = *(h.x509URL)
- keys = append(keys, X509URLKey)
+ v, err := json.Marshal(*(h.x509URL))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, X509URLKey, err)
+ }
+ pairs = append(pairs, headerPair{Name: X509URLKey, Value: v})
}
for k, v := range h.privateParams {
- data[k] = v
- keys = append(keys, k)
+ var encoded []byte
+ switch v := v.(type) {
+ case []byte:
+ var err error
+ encoded, err = json.Marshal(base64.EncodeToString(v))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ default:
+ var err error
+ encoded, err = json.Marshal(v)
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal field %q: %w`, k, err)
+ }
+ }
+ pairs = append(pairs, headerPair{Name: k, Value: encoded})
}
- h.mu.RUnlock()
- sort.Strings(keys)
+
+ sort.Slice(pairs, func(i, j int) bool {
+ return pairs[i].Name < pairs[j].Name
+ })
+
+ return pairs, nil
+}
+
+func (h *stdHeaders) MarshalJSON() ([]byte, error) {
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
- enc := json.NewEncoder(buf)
+ pairs, err := h.makePairs()
+ if err != nil {
+ return nil, fmt.Errorf(`failed to make pairs: %w`, err)
+ }
buf.WriteByte(tokens.OpenCurlyBracket)
- for i, k := range keys {
+
+ for i, pair := range pairs {
if i > 0 {
- buf.WriteRune(tokens.Comma)
- }
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(k)
- buf.WriteString(`":`)
- switch v := data[k].(type) {
- case []byte:
- buf.WriteRune(tokens.DoubleQuote)
- buf.WriteString(base64.EncodeToString(v))
- buf.WriteRune(tokens.DoubleQuote)
- default:
- if err := enc.Encode(v); err != nil {
- return nil, fmt.Errorf(`failed to encode value for field %s: %w`, k, err)
- }
- buf.Truncate(buf.Len() - 1)
+ buf.WriteByte(tokens.Comma)
}
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
copy(ret, buf.Bytes())
+ putHeaderPairList(pairs)
return ret, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go
index e3ad296844..c87252344d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/interface.go
@@ -5,9 +5,16 @@ import (
"github.com/lestrrat-go/jwx/v3/jws/legacy"
)
+// Deprecated: Use the legacy package directly. These type aliases will be removed in v4.
type Signer = legacy.Signer
+
+// Deprecated: Use the legacy package directly. These type aliases will be removed in v4.
type Verifier = legacy.Verifier
+
+// Deprecated: Use the legacy package directly. These type aliases will be removed in v4.
type HMACSigner = legacy.HMACSigner
+
+// Deprecated: Use the legacy package directly. These type aliases will be removed in v4.
type HMACVerifier = legacy.HMACVerifier
// Base64Encoder is an interface that can be used when encoding JWS message
@@ -20,6 +27,15 @@ type HMACVerifier = legacy.HMACVerifier
// but it uses a base64 encoding with padding.
type Base64Encoder = base64.Encoder
+// Base64StreamEncoder is the stream-capable extension of
+// [Base64Encoder]. Encoders that satisfy this interface can be used
+// with the streaming detached-payload path
+// ([jws.WithDetachedPayloadReader]). The default encoder
+// (`encoding/base64.RawURLEncoding`) satisfies it. Custom encoders
+// that do not satisfy it cause [jws.Sign] / [jws.Verify] with
+// [jws.WithDetachedPayloadReader] to return an error.
+type Base64StreamEncoder = base64.StreamEncoder
+
type DecodeCtx interface {
CollectRaw() bool
}
@@ -63,11 +79,21 @@ type DecodeCtx interface {
// headers and the signatures don't match.
//
// To sign and verify, use the appropriate `Sign()` and `Verify()` functions.
+//
+// JSON round-trip note: Message.MarshalJSON collapses any single-signature
+// general-form input (one with a top-level "signatures" array of length 1)
+// into the flattened form (with top-level "protected"/"signature" fields)
+// on output. The conversion is cryptographically lossless — the protected,
+// signature, and payload bytes survive — but byte-level identity changes,
+// so callers that hash or dedup JWS messages by their JSON encoding will
+// not recognize a round-tripped message as equal to its input.
type Message struct {
- dc DecodeCtx
- payload []byte
- signatures []*Signature
- b64 bool // true if payload should be base64 encoded
+ dc DecodeCtx
+ payload []byte
+ signatures []*Signature
+ b64 bool // true if payload should be base64 encoded
+ detached bool // true if the JWS is a detached-payload form: JSON output omits the "payload" member per RFC 7515 Appendix F
+ maxSignatures int // scratch cap enforced during UnmarshalJSON; 0 means use global default
}
type Signature struct {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go
index 77a084cfda..894c1195e7 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/io.go
@@ -15,6 +15,12 @@ func (sysFS) Open(path string) (fs.File, error) {
}
func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
+ var parseOptions []ParseOption
+ for _, option := range options {
+ if po, ok := option.(ParseOption); ok {
+ parseOptions = append(parseOptions, po)
+ }
+ }
var srcFS fs.FS = sysFS{}
for _, option := range options {
@@ -32,5 +38,5 @@ func ReadFile(path string, options ...ReadFileOption) (*Message, error) {
}
defer f.Close()
- return ParseReader(f)
+ return ParseReader(f, parseOptions...)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go
index f09e40db2d..99bf78581a 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jws.go
@@ -26,7 +26,7 @@
package jws
import (
- "bufio"
+ "crypto"
"crypto/ecdh"
"crypto/ecdsa"
"crypto/ed25519"
@@ -34,14 +34,14 @@ import (
"errors"
"fmt"
"io"
- "reflect"
+ "slices"
"sync"
+ "sync/atomic"
"unicode"
"unicode/utf8"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/json"
- "github.com/lestrrat-go/jwx/v3/internal/jwxio"
"github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
"github.com/lestrrat-go/jwx/v3/jwa"
@@ -51,6 +51,12 @@ import (
var registry = json.NewRegistry()
+var maxSignatures atomic.Int64
+
+func init() {
+ maxSignatures.Store(100)
+}
+
var signers = make(map[jwa.SignatureAlgorithm]Signer)
var muSigner = &sync.Mutex{}
@@ -92,10 +98,6 @@ const (
fmtMax
)
-// silence linters
-var _ = fmtInvalid
-var _ = fmtMax
-
func validateKeyBeforeUse(key any) error {
jwkKey, ok := key.(jwk.Key)
if !ok {
@@ -143,6 +145,16 @@ func validateKeyBeforeUse(key any) error {
// signing process, as you will likely be required to set the `b64` field
// when using detached payload.
//
+// RFC 7797 note: producing an in-band compact JWS with `b64=false`
+// (i.e. setting the `b64` protected header to `false` without also
+// passing [WithDetachedPayload]) is "NOT RECOMMENDED" per §5.2; strict
+// peers commonly reject such messages. The canonical pairing for
+// `b64=false` is [WithDetachedPayload] (or [WithDetachedPayloadReader]
+// for streaming), which keeps the unencoded payload out of the wire
+// format. Sign auto-declares `"b64"` in `crit` whenever `b64=false`
+// is set, so the produced JWS is at least RFC 7797 §3 conformant on
+// the producer side.
+//
// Look for options that return `jws.SignOption` or `jws.SignVerifyOption`
// for a complete list of options that can be passed to this function.
//
@@ -154,12 +166,12 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
sc.payload = payload
if err := sc.ProcessOptions(options); err != nil {
- return nil, signerr(`failed to process options: %w`, err)
+ return nil, makeSignError(prefixJwsSign, `failed to process options: %w`, err)
}
lsigner := len(sc.sigbuilders)
if lsigner == 0 {
- return nil, signerr(`no signers available. Specify an algorithm and a key using jws.WithKey()`)
+ return nil, makeSignError(prefixJwsSign, `no signers available. Specify an algorithm and a key using jws.WithKey()`)
}
// Design note: while we could have easily set format = fmtJSON when
@@ -171,7 +183,11 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
// Therefore, instead of making implicit format conversions, we force the
// user to spell it out as `jws.Sign(..., jws.WithJSON(), jws.WithKey(...), jws.WithKey(...))`
if sc.format == fmtCompact && lsigner != 1 {
- return nil, signerr(`cannot have multiple signers (keys) specified for compact serialization. Use only one jws.WithKey()`)
+ return nil, makeSignError(prefixJwsSign, `cannot have multiple signers (keys) specified for compact serialization. Use only one jws.WithKey()`)
+ }
+
+ if sc.payloadReader != nil {
+ return sc.signStreaming()
}
// Create a Message object with all the bits and bobs, and we'll
@@ -179,7 +195,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
var result Message
if err := sc.PopulateMessage(&result); err != nil {
- return nil, signerr(`failed to populate message: %w`, err)
+ return nil, makeSignError(prefixJwsSign, `failed to populate message: %w`, err)
}
switch sc.format {
case fmtJSON:
@@ -200,7 +216,7 @@ func Sign(payload []byte, options ...SignOption) ([]byte, error) {
}
return Compact(&result, compactOpts...)
default:
- return nil, signerr(`invalid serialization format`)
+ return nil, makeSignError(prefixJwsSign, `invalid serialization format`)
}
}
@@ -230,27 +246,58 @@ var allowNoneWhitelist = jwk.WhitelistFunc(func(string) bool {
// when the verification process itself fails (e.g. invalid signature, wrong key),
// while the former is returned when any other part of the `jws.Verify()`
// function fails.
+//
+// When `jws.WithDetachedPayloadReader()` is used, the payload is streamed
+// from the caller's `io.Reader` and is not extracted from the JWS envelope.
+// In that case, the returned `[]byte` is a non-nil zero-length slice on
+// success; the verified bytes are whatever the caller read from the Reader.
+// Do not treat the returned slice as "the payload is empty" — callers that
+// need the payload bytes must retain their own copy.
+//
+// Context cancellation is governed by [WithContext]. The slow-path verify
+// loop checks ctx.Err() between each signature, each key provider, and
+// each (alg, key) attempt; jkuProvider passes ctx to its underlying
+// jwk.Fetcher; the streaming path checks ctx between payload Reads.
+// staticKeyProvider and keySetProvider do not consult ctx inside
+// FetchKeys themselves (their backing data is already in memory) — see
+// the [WithContext] godoc for the full per-layer breakdown.
func Verify(buf []byte, options ...VerifyOption) ([]byte, error) {
vc := verifyContextPool.Get()
defer verifyContextPool.Put(vc)
if err := vc.ProcessOptions(options); err != nil {
- return nil, verifyerr(`failed to process options: %w`, err)
+ return nil, makeVerifyError(`failed to process options: %w`, err)
}
return vc.VerifyMessage(buf)
}
-// get the value of b64 header field.
-// If the field does not exist, returns true (default)
-// Otherwise return the value specified by the header field.
+// getB64Value reads the typed "b64" header field and returns its value,
+// or RFC 7797's default of true when the field is unset.
func getB64Value(hdr Headers) bool {
- var b64 bool
- if err := hdr.Get("b64", &b64); err != nil {
- return true // default
+ v, ok := hdr.B64()
+ if !ok {
+ return true // RFC 7797 default
}
+ return v
+}
- return b64
+func detectParseFormat(src []byte) int {
+ for i := 0; i < len(src); {
+ r := rune(src[i])
+ width := 1
+ if r >= utf8.RuneSelf {
+ r, width = utf8.DecodeRune(src[i:])
+ }
+ if !unicode.IsSpace(r) {
+ if r == tokens.OpenCurlyBracket {
+ return fmtJSON
+ }
+ return fmtCompact
+ }
+ i += width
+ }
+ return 0
}
// Parse parses contents from the given source and creates a jws.Message
@@ -261,15 +308,20 @@ func getB64Value(hdr Headers) bool {
// will attempt to autodetect the format. If one or the other is specified,
// only the specified format will be attempted.
//
+// Bounding the input size is the caller's responsibility; this function
+// trusts the caller-provided src. See docs/13-input-size.md.
+//
// On error, returns a jws.ParseError.
func Parse(src []byte, options ...ParseOption) (*Message, error) {
+ maxSigs := int(maxSignatures.Load())
+
var formats int
for _, option := range options {
switch option.Ident() {
case identSerialization{}:
var v int
if err := option.Value(&v); err != nil {
- return nil, parseerr(`failed to retrieve serialization option value: %w`, err)
+ return nil, makeParseError(`jws.Parse`, `failed to retrieve serialization option value: %w`, err)
}
switch v {
case fmtJSON:
@@ -277,53 +329,46 @@ func Parse(src []byte, options ...ParseOption) (*Message, error) {
case fmtCompact:
formats |= fmtCompact
}
+ case identMaxSignatures{}:
+ if err := option.Value(&maxSigs); err != nil {
+ return nil, makeParseError(`jws.Parse`, `failed to retrieve max signatures option value: %w`, err)
+ }
+ if maxSigs <= 0 {
+ return nil, makeParseError(`jws.Parse`, `WithMaxSignatures must be greater than zero`)
+ }
}
}
// if format is 0 or both JSON/Compact, auto detect
if v := formats & (fmtJSON | fmtCompact); v == 0 || v == fmtJSON|fmtCompact {
- CHECKLOOP:
- for i := range src {
- r := rune(src[i])
- if r >= utf8.RuneSelf {
- r, _ = utf8.DecodeRune(src)
- }
- if !unicode.IsSpace(r) {
- if r == tokens.OpenCurlyBracket {
- formats = fmtJSON
- } else {
- formats = fmtCompact
- }
- break CHECKLOOP
- }
- }
+ formats = detectParseFormat(src)
}
if formats&fmtCompact == fmtCompact {
msg, err := parseCompact(src)
if err != nil {
- return nil, parseerr(`failed to parse compact format: %w`, err)
+ return nil, makeParseError(`jws.Parse`, `failed to parse compact format: %w`, err)
}
return msg, nil
} else if formats&fmtJSON == fmtJSON {
- msg, err := parseJSON(src)
+ msg, err := parseJSON(src, maxSigs)
if err != nil {
- return nil, parseerr(`failed to parse JSON format: %w`, err)
+ return nil, makeParseError(`jws.Parse`, `failed to parse JSON format: %w`, err)
}
return msg, nil
}
- return nil, parseerr(`invalid byte sequence`)
+ return nil, makeParseError(`jws.Parse`, `invalid byte sequence`)
}
// ParseString parses contents from the given source and creates a jws.Message
// struct. The input can be in either compact or full JSON serialization.
//
// On error, returns a jws.ParseError.
-func ParseString(src string) (*Message, error) {
- msg, err := Parse([]byte(src))
+func ParseString(src string, options ...ParseOption) (*Message, error) {
+ msg, err := Parse([]byte(src), options...)
if err != nil {
- return nil, sparseerr(`failed to parse string: %w`, err)
+ return nil, makeParseError(`jws.ParseString`, `failed to parse string: %w`, err)
}
return msg, nil
}
@@ -331,59 +376,22 @@ func ParseString(src string) (*Message, error) {
// ParseReader parses contents from the given source and creates a jws.Message
// struct. The input can be in either compact or full JSON serialization.
//
+// Bounding the input size is the caller's responsibility: wrap src with
+// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See
+// docs/13-input-size.md for the rationale.
+//
// On error, returns a jws.ParseError.
-func ParseReader(src io.Reader) (*Message, error) {
- data, err := jwxio.ReadAllFromFiniteSource(src)
- if err == nil {
- return Parse(data)
- }
-
- if !errors.Is(err, jwxio.NonFiniteSourceError()) {
- return nil, rparseerr(`failed to read from finite source: %w`, err)
- }
-
- rdr := bufio.NewReader(src)
- var first rune
- for {
- r, _, err := rdr.ReadRune()
- if err != nil {
- return nil, rparseerr(`failed to read rune: %w`, err)
- }
- if !unicode.IsSpace(r) {
- first = r
- if err := rdr.UnreadRune(); err != nil {
- return nil, rparseerr(`failed to unread rune: %w`, err)
- }
-
- break
- }
- }
-
- var parser func(io.Reader) (*Message, error)
- if first == tokens.OpenCurlyBracket {
- parser = parseJSONReader
- } else {
- parser = parseCompactReader
- }
-
- m, err := parser(rdr)
+func ParseReader(src io.Reader, options ...ParseOption) (*Message, error) {
+ buf, err := io.ReadAll(src)
if err != nil {
- return nil, rparseerr(`failed to parse reader: %w`, err)
+ return nil, makeParseError(`jws.ParseReader`, `failed to read from io.Reader: %w`, err)
}
-
- return m, nil
+ return Parse(buf, options...)
}
-func parseJSONReader(src io.Reader) (result *Message, err error) {
- var m Message
- if err := json.NewDecoder(src).Decode(&m); err != nil {
- return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err)
- }
- return &m, nil
-}
-
-func parseJSON(data []byte) (result *Message, err error) {
+func parseJSON(data []byte, maxSigs int) (result *Message, err error) {
var m Message
+ m.maxSignatures = maxSigs
if err := json.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf(`failed to unmarshal jws message: %w`, err)
}
@@ -394,12 +402,12 @@ func parseJSON(data []byte) (result *Message, err error) {
// separately: protected headers, payload and signature.
// On error, returns a jws.ParseError.
//
-// This function will be deprecated in v4. It is a low-level API, and
-// thus will be available in the `jwsbb` package.
+// Deprecated: This is a low-level API that will be removed in v4.
+// Use the jwsbb package directly instead.
func SplitCompact(src []byte) ([]byte, []byte, []byte, error) {
hdr, payload, signature, err := jwsbb.SplitCompact(src)
if err != nil {
- return nil, nil, nil, parseerr(`%w`, err)
+ return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err)
}
return hdr, payload, signature, nil
}
@@ -408,12 +416,12 @@ func SplitCompact(src []byte) ([]byte, []byte, []byte, error) {
// separately: protected headers, payload and signature.
// On error, returns a jws.ParseError.
//
-// This function will be deprecated in v4. It is a low-level API, and
-// thus will be available in the `jwsbb` package.
+// Deprecated: This is a low-level API that will be removed in v4.
+// Use the jwsbb package directly instead.
func SplitCompactString(src string) ([]byte, []byte, []byte, error) {
hdr, payload, signature, err := jwsbb.SplitCompactString(src)
if err != nil {
- return nil, nil, nil, parseerr(`%w`, err)
+ return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err)
}
return hdr, payload, signature, nil
}
@@ -422,29 +430,20 @@ func SplitCompactString(src string) ([]byte, []byte, []byte, error) {
// separately: protected headers, payload and signature.
// On error, returns a jws.ParseError.
//
-// This function will be deprecated in v4. It is a low-level API, and
-// thus will be available in the `jwsbb` package.
+// Deprecated: This is a low-level API that will be removed in v4.
+// Use the jwsbb package directly instead.
func SplitCompactReader(rdr io.Reader) ([]byte, []byte, []byte, error) {
hdr, payload, signature, err := jwsbb.SplitCompactReader(rdr)
if err != nil {
- return nil, nil, nil, parseerr(`%w`, err)
+ return nil, nil, nil, makeParseError(`jws.Parse`, `%w`, err)
}
return hdr, payload, signature, nil
}
-// parseCompactReader parses a JWS value serialized via compact serialization.
-func parseCompactReader(rdr io.Reader) (m *Message, err error) {
- protected, payload, signature, err := SplitCompactReader(rdr)
- if err != nil {
- return nil, fmt.Errorf(`invalid compact serialization format: %w`, err)
- }
- return parse(protected, payload, signature)
-}
-
func parseCompact(data []byte) (m *Message, err error) {
- protected, payload, signature, err := SplitCompact(data)
+ protected, payload, signature, err := jwsbb.SplitCompact(data)
if err != nil {
- return nil, fmt.Errorf(`invalid compact serialization format: %w`, err)
+ return nil, makeParseError(`jws.Parse`, `invalid compact serialization format: %w`, err)
}
return parse(protected, payload, signature)
}
@@ -476,6 +475,12 @@ func parse(protected, payload, signature []byte) (*Message, error) {
if err != nil {
return nil, fmt.Errorf(`failed to decode signature: %w`, err)
}
+ if len(decodedSignature) == 0 {
+ alg, ok := hdr.Algorithm()
+ if !ok || alg != jwa.NoSignature() {
+ return nil, fmt.Errorf(`empty compact signature requires protected header "alg" to be "none"`)
+ }
+ }
var msg Message
msg.payload = decodedPayload
@@ -530,60 +535,259 @@ func RegisterCustomField(name string, object any) {
registry.Register(name, object)
}
+// curver is implemented by jwk.Key types that carry curve information.
+type curver interface {
+ Crv() (jwa.EllipticCurveAlgorithm, bool)
+}
+
// Helpers for signature verification
-var rawKeyToKeyType = make(map[reflect.Type]jwa.KeyType)
+var muAlgorithmMaps 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() {
- rawKeyToKeyType[reflect.TypeFor[[]byte]()] = jwa.OctetSeq()
- rawKeyToKeyType[reflect.TypeFor[ed25519.PublicKey]()] = jwa.OKP()
- rawKeyToKeyType[reflect.TypeFor[rsa.PublicKey]()] = jwa.RSA()
- rawKeyToKeyType[reflect.TypeFor[*rsa.PublicKey]()] = jwa.RSA()
- rawKeyToKeyType[reflect.TypeFor[ecdsa.PublicKey]()] = jwa.EC()
- rawKeyToKeyType[reflect.TypeFor[*ecdsa.PublicKey]()] = jwa.EC()
-
- addAlgorithmForKeyType(jwa.OKP(), jwa.EdDSA())
+ RegisterAlgorithmForKeyType(jwa.OKP(), jwa.EdDSA())
+ RegisterAlgorithmForCurve(jwa.Ed25519(), jwa.EdDSAEd25519())
for _, alg := range []jwa.SignatureAlgorithm{jwa.HS256(), jwa.HS384(), jwa.HS512()} {
- addAlgorithmForKeyType(jwa.OctetSeq(), alg)
+ RegisterAlgorithmForKeyType(jwa.OctetSeq(), alg)
}
for _, alg := range []jwa.SignatureAlgorithm{jwa.RS256(), jwa.RS384(), jwa.RS512(), jwa.PS256(), jwa.PS384(), jwa.PS512()} {
- addAlgorithmForKeyType(jwa.RSA(), alg)
+ RegisterAlgorithmForKeyType(jwa.RSA(), alg)
}
for _, alg := range []jwa.SignatureAlgorithm{jwa.ES256(), jwa.ES384(), jwa.ES512()} {
- addAlgorithmForKeyType(jwa.EC(), alg)
+ RegisterAlgorithmForKeyType(jwa.EC(), alg)
}
}
-func addAlgorithmForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) {
+// RegisterAlgorithmForKeyType registers an additional algorithm as valid for
+// the given key type. This is used internally by init() and can also be called
+// from external modules that provide support for additional algorithms (e.g. Ed448).
+func RegisterAlgorithmForKeyType(kty jwa.KeyType, alg jwa.SignatureAlgorithm) {
+ muAlgorithmMaps.Lock()
+ defer muAlgorithmMaps.Unlock()
keyTypeToAlgorithms[kty] = append(keyTypeToAlgorithms[kty], alg)
+ if !slices.Contains(algorithmToKeyTypes[alg], kty) {
+ algorithmToKeyTypes[alg] = append(algorithmToKeyTypes[alg], kty)
+ }
+}
+
+// RegisterAlgorithmForCurve registers an algorithm as valid for the given
+// elliptic curve. When [AlgorithmsForKey] can determine the curve of a key,
+// it returns the union of key-type-level algorithms and curve-specific
+// algorithms instead of all algorithms for the key type.
+//
+// This function is append-only and deduplicates entries, so builtin
+// registrations cannot be overwritten by external modules.
+func RegisterAlgorithmForCurve(crv jwa.EllipticCurveAlgorithm, alg jwa.SignatureAlgorithm) {
+ muAlgorithmMaps.Lock()
+ defer muAlgorithmMaps.Unlock()
+ if slices.Contains(curveToAlgorithms[crv], alg) {
+ return
+ }
+ curveToAlgorithms[crv] = append(curveToAlgorithms[crv], alg)
}
// AlgorithmsForKey returns the possible signature algorithms that can
// be used for a given key. It only takes in consideration keys/algorithms
// for verification purposes, as this is the only usage where one may need
// dynamically figure out which method to use.
+//
+// When the key's curve can be determined (via [jwk.Key] Crv() method or
+// inferred from the raw Go type), curve-specific algorithms registered via
+// [RegisterAlgorithmForCurve] are combined with key-type-level algorithms
+// to produce a more precise result.
+//
+// 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 AlgorithmsForKey(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, *ecdh.PublicKey, ecdh.PublicKey, *ecdh.PrivateKey, ecdh.PrivateKey:
+ 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:
- return nil, fmt.Errorf(`unknown key type %T`, key)
+ // 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 := AlgorithmsForKey(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()
+ }
}
- algs, ok := keyTypeToAlgorithms[kty]
+ muAlgorithmMaps.RLock()
+ defer muAlgorithmMaps.RUnlock()
+
+ ktyAlgs, ok := keyTypeToAlgorithms[kty]
if !ok {
- return nil, fmt.Errorf(`unregistered key type %q`, kty)
+ return nil, fmt.Errorf(`%w: unregistered key type %q`, errUnclassifiableKey, kty)
}
- return algs, nil
+
+ // 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 filterAlgorithmsForCurve(ktyAlgs, crvAlgs), nil
+ }
+
+ return ktyAlgs, nil
+}
+
+// filterAlgorithmsForCurve 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 filterAlgorithmsForCurve(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
+}
+
+// validateAlgorithmForKey checks that alg is compatible with key.
+// Three classification failures are intentionally allowed through:
+// (a) a nil key, used by keyless algorithms (see GH910);
+// (b) any key handed to an algorithm with a user-registered custom
+// Signer2/Verifier2 — custom implementations may accept arbitrary key
+// types that AlgorithmsForKey cannot classify; and
+// (c) an opaque crypto.Signer whose .Public() is itself a crypto.Signer,
+// the one case AlgorithmsForKey refuses to recurse into.
+// Every other classification failure is surfaced so callers get a crisp
+// option-boundary rejection instead of a deep-stack error.
+func validateAlgorithmForKey(alg jwa.SignatureAlgorithm, key any) error {
+ if key == nil {
+ return nil
+ }
+ algs, err := AlgorithmsForKey(key)
+ if err != nil {
+ if hasCustomSigVerifier(alg) {
+ return nil
+ }
+ if signer, ok := key.(crypto.Signer); ok {
+ if _, isSigner := signer.Public().(crypto.Signer); isSigner {
+ return nil
+ }
+ }
+ return fmt.Errorf(`jws.WithKey: %w`, err)
+ }
+ if !slices.Contains(algs, alg) {
+ if hasCustomSigVerifier(alg) {
+ return nil
+ }
+ return fmt.Errorf(`jws.WithKey: algorithm %q is not compatible with key type %T`, alg, key)
+ }
+ return nil
+}
+
+// hasCustomSigVerifier reports whether a non-default Signer2 or
+// Verifier2 has been registered for alg. When this is true, key-type
+// validation must be skipped: the custom implementation decides what
+// key types it accepts.
+func hasCustomSigVerifier(alg jwa.SignatureAlgorithm) bool {
+ muSigner2DB.RLock()
+ s, sok := signer2DB[alg]
+ muSigner2DB.RUnlock()
+ if sok {
+ if _, isDefault := s.(defaultSigner); !isDefault {
+ return true
+ }
+ }
+ muVerifier2DB.RLock()
+ v, vok := verifier2DB[alg]
+ muVerifier2DB.RUnlock()
+ if vok {
+ if _, isDefault := v.(defaultVerifier); !isDefault {
+ return true
+ }
+ }
+ return false
}
// Settings allows you to set global settings for this JWS operations.
@@ -594,6 +798,15 @@ func Settings(options ...GlobalOption) {
for _, option := range options {
switch option.Ident() {
case identLegacySigners{}:
+ case identMaxSignatures{}:
+ var v int
+ if err := option.Value(&v); err != nil {
+ panic(fmt.Sprintf("jws.Settings: value for WithMaxSignatures must be an int: %s", err))
+ }
+ if v <= 0 {
+ panic("jws.Settings: WithMaxSignatures must be greater than zero")
+ }
+ maxSignatures.Store(int64(v))
}
}
}
@@ -619,18 +832,90 @@ func Settings(options ...GlobalOption) {
//
// Since this function avoids doing many checks that jws.Verify would perform,
// you must ensure to perform the necessary checks including ensuring that algorithm is safe to use for your payload yourself.
+//
+// VerifyCompactFast cross-checks the protected header's "alg" against
+// the caller-supplied alg: if the header omits "alg" (required by
+// RFC 7515 §4.1.1) or advertises a different value, it returns a
+// verification error. This prevents silently verifying a message
+// under a different discipline than the one its header advertises.
+//
+// VerifyCompactFast refuses messages whose protected header carries a
+// "crit" list. RFC 7515 §4.1.11 requires every critical extension to be
+// understood by the recipient, and the fast path has no WithCritExtension
+// allowlist to consult. On crit-present input it returns a sentinel error
+// that callers can detect with errors.Is(err, jws.ErrCritPresent()) and
+// retry through jws.Verify, which enforces the full validateCritical rule
+// set. Applications that may legitimately receive "crit" headers should
+// call jws.Verify directly.
+//
+// VerifyCompactFast assumes the JWS uses the default "b64":true
+// (base64url-encoded) payload encoding. Any protected header carrying
+// a "b64" entry is refused with jws.ErrB64Present(), regardless of
+// whether "crit" also lists it: the fast path's signing-input
+// reconstruction and post-verify base64 decode both depend on the
+// default encoding, and a non-conformant b64=false producer (one that
+// omits "b64" from "crit") would otherwise verify cryptographically
+// while returning bytes that differ from the producer's intent.
+// Detached-payload callers must use jws.Verify with jws.WithDetachedPayload
+// regardless, since VerifyCompactFast has no way to accept a detached
+// payload.
func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]byte, error) {
+ if err := validateAlgorithmForKey(alg, key); err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+
algstr := alg.String()
- // Split the serialized JWT into its components
+ // Split the serialized JWS into its components
hdr, payload, encodedSig, err := jwsbb.SplitCompact(compact)
if err != nil {
- return nil, fmt.Errorf("jwt.verifyFast: failed to split compact: %w", err)
+ return nil, makeVerifyError("failed to split compact: %w", err)
+ }
+
+ parsedHdr := jwsbb.HeaderParseCompact(hdr)
+
+ // Refuse crit-bearing messages: the fast path has no WithCritExtension
+ // allowlist, so accepting them would silently violate RFC 7515 §4.1.11.
+ // Callers that wrap VerifyCompactFast can detect this via
+ // errors.Is(err, jws.ErrCritPresent()) and fall through to jws.Verify.
+ // The sentinel is wrapped in verifyError so the same error also matches
+ // errors.Is(err, jws.VerifyError()) — fast-path refusals are a verify
+ // error, just one with a more specific classification available.
+ if jwsbb.HeaderHas(parsedHdr, CriticalKey) {
+ return nil, verifyError{errCritPresent}
+ }
+
+ // Refuse "b64"-bearing messages, regardless of whether "crit" also
+ // lists it. The signing-input reconstruction and the post-verify
+ // base64 decode both assume the default b64=true encoding; a
+ // b64=false JWS that the fast path "verified" would either fail the
+ // post-verify base64 decode with a misleading error, or — worse —
+ // return base64-decoded garbage as the payload while the producer's
+ // raw bytes silently disagree. jws.Verify has the WithDetachedPayload
+ // / WithCritExtension machinery to handle b64=false correctly. As with
+ // the crit refusal above, the sentinel is wrapped in verifyError so the
+ // same error matches both jws.ErrB64Present() and jws.VerifyError().
+ if jwsbb.HeaderHas(parsedHdr, "b64") {
+ return nil, verifyError{errB64Present}
+ }
+
+ // Cross-check the protected header "alg" against the caller-supplied
+ // alg. RFC 7515 §4.1.1 makes "alg" mandatory in the protected header
+ // for compact serialization, and a mismatch between what the message
+ // advertises and the discipline under which we verify is the sort of
+ // silent divergence that downstream code (e.g. JWT consumers) should
+ // not be asked to re-discover on its own.
+ hdrAlg, err := jwsbb.HeaderGetString(parsedHdr, AlgorithmKey)
+ if err != nil {
+ return nil, verifyError{verificationError{fmt.Errorf(`jws.Verify: failed to extract %q from protected header: %w`, AlgorithmKey, err)}}
+ }
+ if hdrAlg != algstr {
+ return nil, verifyError{verificationError{fmt.Errorf(`jws.Verify: protected header %q %q does not match caller-supplied algorithm %q`, AlgorithmKey, hdrAlg, algstr)}}
}
signature, err := base64.Decode(encodedSig)
if err != nil {
- return nil, fmt.Errorf("jwt.verifyFast: failed to decode signature: %w", err)
+ return nil, makeVerifyError("failed to decode signature: %w", err)
}
// Instead of appending, copy the data from hdr/payload
@@ -645,21 +930,21 @@ func VerifyCompactFast(key any, compact []byte, alg jwa.SignatureAlgorithm) ([]b
// Verify the signature
if verifier2, err := VerifierFor(alg); err == nil {
if err := verifier2.Verify(key, verifyBuf, signature); err != nil {
- return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}}
+ return nil, verifyError{verificationError{fmt.Errorf("signature verification failed for %s: %w", algstr, err)}}
}
} else {
legacyVerifier, err := NewVerifier(alg)
if err != nil {
- return nil, verifyerr("jwt.VerifyCompact: failed to create verifier for %s: %w", algstr, err)
+ return nil, makeVerifyError("failed to create verifier for %s: %w", algstr, err)
}
if err := legacyVerifier.Verify(verifyBuf, signature, key); err != nil {
- return nil, verifyError{verificationError{fmt.Errorf("jwt.VerifyCompact: signature verification failed for %s: %w", algstr, err)}}
+ return nil, verifyError{verificationError{fmt.Errorf("signature verification failed for %s: %w", algstr, err)}}
}
}
decoded, err := base64.Decode(payload)
if err != nil {
- return nil, verifyerr("jwt.VerifyCompact: failed to decode payload: %w", err)
+ return nil, makeVerifyError("failed to decode payload: %w", err)
}
return decoded, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel
index 0799e81110..54e64265a0 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/BUILD.bazel
@@ -7,6 +7,7 @@ go_library(
"ecdsa.go",
"eddsa.go",
"format.go",
+ "header.go",
"hmac.go",
"jwsbb.go",
"rsa.go",
@@ -18,12 +19,12 @@ go_library(
deps = [
"//internal/base64",
"//internal/ecutil",
- "//internal/jwxio",
"//internal/keyconv",
"//internal/pool",
"//internal/tokens",
"//jws/internal/keytype",
"@com_github_lestrrat_go_dsig//:dsig",
+ "@com_github_valyala_fastjson//:fastjson",
],
)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go
index 430bf625ac..945206c2c9 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/format.go
@@ -6,7 +6,6 @@ import (
"io"
"github.com/lestrrat-go/jwx/v3/internal/base64"
- "github.com/lestrrat-go/jwx/v3/internal/jwxio"
"github.com/lestrrat-go/jwx/v3/internal/tokens"
)
@@ -39,6 +38,26 @@ func SignBuffer(buf, hdr, payload []byte, encoder base64.Encoder, encodePayload
return buf
}
+// SigningPrefix returns base64url(hdr) + "." — the portion of the signing
+// input that precedes the (possibly base64-encoded) payload.
+//
+// Parameters:
+// - buf: Reusable scratch buffer (can be nil for automatic allocation);
+// any prior contents are discarded, matching [SignBuffer]. This is not
+// an append-style API.
+// - hdr: Raw header bytes (will be base64-encoded)
+// - encoder: Base64 encoder to use for encoding the header
+func SigningPrefix(buf, hdr []byte, encoder base64.Encoder) []byte {
+ l := encoder.EncodedLen(len(hdr)) + 1
+ if cap(buf) < l {
+ buf = make([]byte, 0, l)
+ }
+ buf = buf[:0]
+ buf = encoder.AppendEncode(buf, hdr)
+ buf = append(buf, tokens.Period)
+ return buf
+}
+
// AppendSignature appends a base64-encoded signature to a JWS signing input buffer.
// This completes the compact JWS serialization by adding the final signature component.
// The input buffer should contain the signing input (header.payload), and this function
@@ -150,9 +169,11 @@ func SplitCompactString(src string) (protected, payload, signature []byte, err e
}
// SplitCompactReader parses a compact JWS serialization from an io.Reader.
-// This function handles both finite and streaming sources efficiently.
-// For finite sources, it reads all data at once. For streaming sources,
-// it uses a buffer-based approach to find segment boundaries.
+// It reads the entire input from rdr and dispatches to [SplitCompact].
+//
+// Bounding the input size is the caller's responsibility: wrap rdr with
+// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See
+// docs/13-input-size.md for the rationale.
//
// Parameters:
// - rdr: Reader containing the compact JWS data
@@ -165,71 +186,9 @@ func SplitCompactString(src string) (protected, payload, signature []byte, err e
//
// The function validates that exactly 3 segments are present, separated by periods.
func SplitCompactReader(rdr io.Reader) (protected, payload, signature []byte, err error) {
- data, err := jwxio.ReadAllFromFiniteSource(rdr)
- if err == nil {
- return SplitCompact(data)
- }
-
- if !errors.Is(err, jwxio.NonFiniteSourceError()) {
+ data, err := io.ReadAll(rdr)
+ if err != nil {
return nil, nil, nil, err
}
-
- var periods int
- var state int
-
- buf := make([]byte, 4096)
- var sofar []byte
-
- for {
- // read next bytes
- n, err := rdr.Read(buf)
- // return on unexpected read error
- if err != nil && err != io.EOF {
- return nil, nil, nil, io.ErrUnexpectedEOF
- }
-
- // append to current buffer
- sofar = append(sofar, buf[:n]...)
- // loop to capture multiple tokens.Period in current buffer
- for loop := true; loop; {
- var i = bytes.IndexByte(sofar, tokens.Period)
- if i == -1 && err != io.EOF {
- // no tokens.Period found -> exit and read next bytes (outer loop)
- loop = false
- continue
- } else if i == -1 && err == io.EOF {
- // no tokens.Period found -> process rest and exit
- i = len(sofar)
- loop = false
- } else {
- // tokens.Period found
- periods++
- }
-
- // Reaching this point means we have found a tokens.Period or EOF and process the rest of the buffer
- switch state {
- case 0:
- protected = sofar[:i]
- state++
- case 1:
- payload = sofar[:i]
- state++
- case 2:
- signature = sofar[:i]
- }
- // Shorten current buffer
- if len(sofar) > i {
- sofar = sofar[i+1:]
- }
- }
- // Exit on EOF
- if err == io.EOF {
- break
- }
- }
- if periods != 2 {
- return nil, nil, nil, InvalidNumberOfSegmentsError()
- }
-
- return protected, payload, signature, nil
+ return SplitCompact(data)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go
index cac3987ea5..17f5b7b7de 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/header.go
@@ -177,12 +177,16 @@ func HeaderGetInt64(h Header, key string) (int64, error) {
return v.Int64()
}
-// HeaderGetStringBytes returns the byte slice value for the given key from the JWS header.
-// An error is returned if the JSON was not valid, if the key does not exist,
-// or if the value is not a byte slice.
+// HeaderGetStringBytes returns the JSON string bytes for the given key
+// from the JWS header, without copying. An error is returned if the JSON
+// was not valid, if the key does not exist, or if the value is not a
+// JSON string.
//
-// Because of limitations of the underlying library, you cannot use the return value
-// of this function after the parser is garbage collected.
+// WARNING: the returned slice aliases memory owned by h. It becomes
+// invalid as soon as h is reused, re-parsed, or goes out of scope and is
+// garbage collected. Do not retain the slice, share it across
+// goroutines, or use it after any further call on h. If you need a value
+// that outlives h, use [HeaderGetString], which returns a string copy.
//
// This function is experimental and may change or be removed in the future.
func HeaderGetStringBytes(h Header, key string) ([]byte, error) {
@@ -194,6 +198,41 @@ func HeaderGetStringBytes(h Header, key string) ([]byte, error) {
return v.StringBytes()
}
+// HeaderHas returns true if the given key exists in the JWS header.
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderHas(h Header, key string) bool {
+ _, err := headerGet(h, key)
+ return err == nil
+}
+
+// HeaderGetStringArray returns a string array for the given key from the JWS header.
+// An error is returned if the JSON was not valid, if the key does not exist,
+// or if the value is not a JSON array of strings.
+//
+// This function is experimental and may change or be removed in the future.
+func HeaderGetStringArray(h Header, key string) ([]string, error) {
+ v, err := headerGet(h, key)
+ if err != nil {
+ return nil, err
+ }
+
+ arr, err := v.Array()
+ if err != nil {
+ return nil, err
+ }
+
+ result := make([]string, len(arr))
+ for i, item := range arr {
+ sb, err := item.StringBytes()
+ if err != nil {
+ return nil, err
+ }
+ result[i] = string(sb)
+ }
+ return result, nil
+}
+
// HeaderGetUint returns the uint value for the given key from the JWS header.
// An error is returned if the JSON was not valid, if the key does not exist,
// or if the value is not a uint.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go
index 6a67ee8f86..0a36ef1877 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/jwsbb.go
@@ -17,6 +17,10 @@
package jwsbb
import (
+ "crypto"
+ "crypto/ed25519"
+ "fmt"
+
"github.com/lestrrat-go/dsig"
)
@@ -44,22 +48,11 @@ const (
// EdDSA algorithm
edDSA = "EdDSA"
+
+ // Fully-specified EdDSA algorithms (RFC 9864)
+ edDSAEd25519 = "Ed25519"
)
-// Signer is a generic interface that defines the method for signing payloads.
-// The type parameter K represents the key type (e.g., []byte for HMAC keys,
-// *rsa.PrivateKey for RSA keys, *ecdsa.PrivateKey for ECDSA keys).
-type Signer[K any] interface {
- Sign(key K, payload []byte) ([]byte, error)
-}
-
-// Verifier is a generic interface that defines the method for verifying signatures.
-// The type parameter K represents the key type (e.g., []byte for HMAC keys,
-// *rsa.PublicKey for RSA keys, *ecdsa.PublicKey for ECDSA keys).
-type Verifier[K any] interface {
- Verify(key K, buf []byte, signature []byte) error
-}
-
// JWS to dsig algorithm mapping
var jwsToDsigAlgorithm = map[string]string{
// HMAC algorithms
@@ -85,10 +78,31 @@ var jwsToDsigAlgorithm = map[string]string{
// EdDSA algorithm
edDSA: dsig.EdDSA,
+
+ // Fully-specified EdDSA algorithms (RFC 9864)
+ edDSAEd25519: dsig.EdDSA,
}
-// getDsigAlgorithm returns the dsig algorithm name for a JWS algorithm
-func getDsigAlgorithm(jwsAlg string) (string, bool) {
+// GetDsigAlgorithm returns the dsig algorithm name for a JWS algorithm.
+func GetDsigAlgorithm(jwsAlg string) (string, bool) {
dsigAlg, ok := jwsToDsigAlgorithm[jwsAlg]
return dsigAlg, ok
}
+
+// validateEdDSACurve enforces that fully-specified EdDSA algorithms (RFC 9864)
+// are only used with the correct key curve. The polymorphic "EdDSA" algorithm
+// accepts any EdDSA key without curve checks. The pub argument must be the
+// already-extracted public key (after jwk.Key unwrapping / keyconv).
+func validateEdDSACurve(jwsAlg string, pub crypto.PublicKey) error {
+ switch jwsAlg {
+ case edDSAEd25519:
+ if _, ok := pub.(ed25519.PublicKey); !ok {
+ return fmt.Errorf(`algorithm %q requires an Ed25519 key, got %T`, jwsAlg, pub)
+ }
+ case edDSA:
+ // Polymorphic EdDSA: no curve restriction
+ default:
+ return fmt.Errorf(`unsupported fully-specified EdDSA algorithm %q`, jwsAlg)
+ }
+ return nil
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go
index 6f36ab0554..8c0c185c54 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/sign.go
@@ -21,9 +21,10 @@ import (
// Not all algorithms require this parameter, but it is included for consistency.
// 99% of the time, you can pass nil for rr, and it will work fine.
func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
- dsigAlg, ok := getDsigAlgorithm(alg)
+ dsigAlg, ok := GetDsigAlgorithm(alg)
if !ok {
- return nil, fmt.Errorf(`jwsbb.Sign: unsupported signature algorithm %q`, alg)
+ // For custom algorithms registered with dsig, JWS name = dsig name
+ dsigAlg = alg
}
// Get dsig algorithm info to determine key conversion strategy
@@ -40,7 +41,9 @@ func Sign(key any, alg string, payload []byte, rr io.Reader) ([]byte, error) {
case dsig.ECDSA:
return dispatchECDSASign(key, dsigAlg, payload, rr)
case dsig.EdDSAFamily:
- return dispatchEdDSASign(key, dsigAlg, payload, rr)
+ return dispatchEdDSASign(key, alg, dsigAlg, payload, rr)
+ case dsig.Custom:
+ return dsig.Sign(key, dsigAlg, payload, rr)
default:
return nil, fmt.Errorf(`jwsbb.Sign: unsupported dsig algorithm family %q`, dsigInfo.Family)
}
@@ -91,11 +94,17 @@ func dispatchECDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([
return dsig.Sign(privkey, dsigAlg, payload, rr)
}
-func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
+func dispatchEdDSASign(key any, jwsAlg, dsigAlg string, payload []byte, rr io.Reader) ([]byte, error) {
+ // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family,
+ // so they take the dsig.Custom branch in Sign() and never reach this function.
+
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an EdDSA key
- if _, ok := signer.Public().(ed25519.PublicKey); ok {
+ if pub, ok := signer.Public().(ed25519.PublicKey); ok {
+ if err := validateEdDSACurve(jwsAlg, pub); err != nil {
+ return nil, fmt.Errorf(`jwsbb.Sign: %w`, err)
+ }
return dsig.Sign(signer, dsigAlg, payload, rr)
}
}
@@ -106,5 +115,9 @@ func dispatchEdDSASign(key any, dsigAlg string, payload []byte, rr io.Reader) ([
return nil, fmt.Errorf(`jwsbb.Sign: invalid key type %T. ed25519.PrivateKey is required: %w`, key, err)
}
+ if err := validateEdDSACurve(jwsAlg, privkey.Public()); err != nil {
+ return nil, fmt.Errorf(`jwsbb.Sign: %w`, err)
+ }
+
return dsig.Sign(privkey, dsigAlg, payload, rr)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go
index bac3ff487e..f3bfcc6b7f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/jwsbb/verify.go
@@ -16,9 +16,10 @@ import (
// This function loads the verifier registered in the jwsbb package _ONLY_.
// It does not support custom verifiers that the user might have registered.
func Verify(key any, alg string, payload, signature []byte) error {
- dsigAlg, ok := getDsigAlgorithm(alg)
+ dsigAlg, ok := GetDsigAlgorithm(alg)
if !ok {
- return fmt.Errorf(`jwsbb.Verify: unsupported signature algorithm %q`, alg)
+ // For custom algorithms registered with dsig, JWS name = dsig name
+ dsigAlg = alg
}
// Get dsig algorithm info to determine key conversion strategy
@@ -35,7 +36,9 @@ func Verify(key any, alg string, payload, signature []byte) error {
case dsig.ECDSA:
return dispatchECDSAVerify(key, dsigAlg, payload, signature)
case dsig.EdDSAFamily:
- return dispatchEdDSAVerify(key, dsigAlg, payload, signature)
+ return dispatchEdDSAVerify(key, alg, dsigAlg, payload, signature)
+ case dsig.Custom:
+ return dsig.Verify(key, dsigAlg, payload, signature)
default:
return fmt.Errorf(`jwsbb.Verify: unsupported dsig algorithm family %q`, dsigInfo.Family)
}
@@ -86,11 +89,17 @@ func dispatchECDSAVerify(key any, dsigAlg string, payload, signature []byte) err
return dsig.Verify(pubkey, dsigAlg, payload, signature)
}
-func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) error {
+func dispatchEdDSAVerify(key any, jwsAlg, dsigAlg string, payload, signature []byte) error {
+ // Note: Extension algorithms (e.g. Ed448) are registered as dsig.Custom family,
+ // so they take the dsig.Custom branch in Verify() and never reach this function.
+
// Try crypto.Signer first (dsig can handle it directly)
if signer, ok := key.(crypto.Signer); ok {
// Verify it's an EdDSA key
- if _, ok := signer.Public().(ed25519.PublicKey); ok {
+ if pub, ok := signer.Public().(ed25519.PublicKey); ok {
+ if err := validateEdDSACurve(jwsAlg, pub); err != nil {
+ return fmt.Errorf(`jwsbb.Verify: %w`, err)
+ }
return dsig.Verify(signer, dsigAlg, payload, signature)
}
}
@@ -101,5 +110,9 @@ func dispatchEdDSAVerify(key any, dsigAlg string, payload, signature []byte) err
return fmt.Errorf(`jwsbb.Verify: invalid key type %T. ed25519.PublicKey is required: %w`, key, err)
}
+ if err := validateEdDSACurve(jwsAlg, pubkey); err != nil {
+ return fmt.Errorf(`jwsbb.Verify: %w`, err)
+ }
+
return dsig.Verify(pubkey, dsigAlg, payload, signature)
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go
index 84529a1a87..49afd0e19f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/key_provider.go
@@ -2,8 +2,10 @@ package jws
import (
"context"
+ "errors"
"fmt"
"net/url"
+ "slices"
"sync"
"github.com/lestrrat-go/jwx/v3/jwa"
@@ -76,7 +78,7 @@ type KeySink interface {
}
type algKeyPair struct {
- alg jwa.KeyAlgorithm
+ alg jwa.SignatureAlgorithm
key any
}
@@ -109,116 +111,184 @@ type keySetProvider struct {
multipleKeysPerKeyID bool // true if we should attempt to match multiple keys per key ID. if false we assume that only one key exists for a given key ID
}
-func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) error {
+// selectKey examines a single key and, if it is suitable for the given
+// signature, adds one or more (algorithm, key) pairs to the sink.
+// It returns true if at least one pair was added, false if the key was
+// filtered out (e.g. wrong usage, no matching algorithm).
+func (kp *keySetProvider) selectKey(sink KeySink, key jwk.Key, sig *Signature, _ *Message) (bool, error) {
if usage, ok := key.KeyUsage(); ok {
// it's okay if use: "". we'll assume it's "sig"
if usage != "" && usage != jwk.ForSignature.String() {
- return nil
+ kid, _ := key.KeyID()
+ return false, fmt.Errorf(`key with kid %q is marked use=%q, not usable for signature verification (expected %q)`, kid, usage, jwk.ForSignature.String())
}
}
if v, ok := key.Algorithm(); ok {
salg, ok := jwa.LookupSignatureAlgorithm(v.String())
if !ok {
- return fmt.Errorf(`invalid signature algorithm %q`, v)
+ return false, fmt.Errorf(`invalid signature algorithm %q`, v)
}
sink.Key(salg, key)
- return nil
+ return true, nil
}
- if kp.inferAlgorithm {
- algs, err := AlgorithmsForKey(key)
- if err != nil {
- return fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err)
- }
+ if !kp.inferAlgorithm {
+ return false, nil
+ }
- // bail out if the JWT has a `alg` field, and it doesn't match
- if tokAlg, ok := sig.ProtectedHeaders().Algorithm(); ok {
- for _, alg := range algs {
- if tokAlg == alg {
- sink.Key(alg, key)
- return nil
- }
- }
- return fmt.Errorf(`algorithm in the message does not match any of the inferred algorithms`)
- }
+ algs, err := AlgorithmsForKey(key)
+ if err != nil {
+ return false, fmt.Errorf(`failed to get a list of signature methods for key type %s: %w`, key.KeyType(), err)
+ }
- // Yes, you get to try them all!!!!!!!
+ // bail out if the JWT has a `alg` field, and it doesn't match
+ if tokAlg, ok := sig.ProtectedHeaders().Algorithm(); ok {
for _, alg := range algs {
- sink.Key(alg, key)
+ if tokAlg == alg {
+ sink.Key(alg, key)
+ return true, nil
+ }
}
- return nil
+ return false, fmt.Errorf(`algorithm in the message does not match any of the inferred algorithms`)
}
- return nil
+
+ // Yes, you get to try them all!!!!!!!
+ for _, alg := range algs {
+ sink.Key(alg, key)
+ }
+ return len(algs) > 0, nil
}
func (kp *keySetProvider) FetchKeys(_ context.Context, sink KeySink, sig *Signature, msg *Message) error {
if kp.requireKid {
wantedKid, ok := sig.ProtectedHeaders().KeyID()
if !ok {
- // If the kid is NOT specified... kp.useDefault needs to be true, and the
- // JWKs must have exactly one key in it
- if !kp.useDefault {
- return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token`)
- } else if kp.useDefault && kp.set.Len() > 1 {
- return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token but multiple keys available in key set`)
- }
-
- // if we got here, then useDefault == true AND there is exactly
- // one key in the set.
- key, ok := kp.set.Key(0)
- if !ok {
- return fmt.Errorf(`failed to get key at index 0 (empty JWKS?)`)
- }
- return kp.selectKey(sink, key, sig, msg)
+ return kp.fetchDefaultKey(sink, sig, msg)
}
+ return kp.fetchKeysByKid(sink, sig, msg, wantedKid)
+ }
+ return kp.fetchAllKeys(sink, sig, msg)
+}
- // Otherwise we better be able to look up the key.
- // <= v2.0.3 backwards compatible case: only match a single key
- // whose key ID matches `wantedKid`
- if !kp.multipleKeysPerKeyID {
- key, ok := kp.set.LookupKeyID(wantedKid)
- if !ok {
- return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
- }
- return kp.selectKey(sink, key, sig, msg)
- }
+// fetchDefaultKey handles the case where kid is required but the token
+// has no kid field. It uses the sole key in the set when useDefault is true.
+func (kp *keySetProvider) fetchDefaultKey(sink KeySink, sig *Signature, msg *Message) error {
+ if !kp.useDefault {
+ return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token`)
+ }
+ if kp.set.Len() > 1 {
+ return fmt.Errorf(`failed to find matching key: no key ID ("kid") specified in token but multiple keys available in key set`)
+ }
- // if multipleKeysPerKeyID is true, we attempt all keys whose key ID matches
- // the wantedKey
- ok = false
- for i := range kp.set.Len() {
- key, _ := kp.set.Key(i)
- if kid, ok := key.KeyID(); !ok || kid != wantedKid {
- continue
- }
+ key, ok := kp.set.Key(0)
+ if !ok {
+ return fmt.Errorf(`failed to get key at index 0 (empty JWKS?)`)
+ }
+ _, err := kp.selectKey(sink, key, sig, msg)
+ return err
+}
- if err := kp.selectKey(sink, key, sig, msg); err != nil {
- continue
- }
- ok = true
- // continue processing so that we try all keys with the same key ID
- }
+// fetchKeysByKid looks up keys by their key ID and adds matching ones to the sink.
+func (kp *keySetProvider) fetchKeysByKid(sink KeySink, sig *Signature, msg *Message, wantedKid string) error {
+ // <= v2.0.3 backwards compatible case: only match a single key
+ // whose key ID matches `wantedKid`
+ if !kp.multipleKeysPerKeyID {
+ key, ok := kp.set.LookupKeyID(wantedKid)
if !ok {
return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
}
- return nil
+ _, err := kp.selectKey(sink, key, sig, msg)
+ return err
}
- // Otherwise just try all keys
+ // multipleKeysPerKeyID: attempt all keys whose key ID matches
+ found := false
+ var errs []error
+ for i := range kp.set.Len() {
+ key, _ := kp.set.Key(i)
+ if kid, ok := key.KeyID(); !ok || kid != wantedKid {
+ continue
+ }
+
+ added, err := kp.selectKey(sink, key, sig, msg)
+ if err != nil {
+ errs = append(errs, fmt.Errorf(`key #%d: %w`, i, err))
+ continue
+ }
+ if added {
+ found = true
+ }
+ }
+ if !found {
+ if len(errs) > 0 {
+ return fmt.Errorf(`failed to select any key with key ID %q: %w`, wantedKid, errors.Join(errs...))
+ }
+ return fmt.Errorf(`failed to find key with key ID %q in key set`, wantedKid)
+ }
+ return nil
+}
+
+// fetchAllKeys iterates all keys in the set and adds suitable ones to the sink.
+//
+// When the protected header advertises an `alg`, keys whose type cannot
+// produce that algorithm are skipped before reaching selectKey. This
+// bounds verification fan-out to N_keys_of_matching_type instead of
+// N_keys when `WithRequireKid(false)` is used against a heterogeneous
+// JWKS. The skip is semantics-preserving: validateAlgorithmForKey in
+// verify_context would reject the incompatible (alg, key) pair before
+// running any verifier anyway.
+//
+// The allowed-KeyType set is looked up once per FetchKeys call via the
+// precomputed algorithmToKeyTypes inverse map, so the per-key check is
+// a cheap KeyType equality over a tiny slice (typically 1 element).
+// When allowedKtys is nil (no header alg, or alg has no registered
+// key type), the filter is skipped.
+func (kp *keySetProvider) fetchAllKeys(sink KeySink, sig *Signature, msg *Message) error {
+ var allowedKtys []jwa.KeyType
+ if hdrAlg, ok := sig.ProtectedHeaders().Algorithm(); ok {
+ allowedKtys = keyTypesForAlgorithm(hdrAlg)
+ }
+ found := false
+ var errs []error
for i := range kp.set.Len() {
key, ok := kp.set.Key(i)
if !ok {
return fmt.Errorf(`failed to get key at index %d`, i)
}
- if err := kp.selectKey(sink, key, sig, msg); err != nil {
+ if allowedKtys != nil && !slices.Contains(allowedKtys, key.KeyType()) {
continue
}
+ added, err := kp.selectKey(sink, key, sig, msg)
+ if err != nil {
+ errs = append(errs, fmt.Errorf(`key #%d: %w`, i, err))
+ continue
+ }
+ if added {
+ found = true
+ }
+ }
+ if !found && len(errs) > 0 {
+ return fmt.Errorf(`no key in the key set was usable: %w`, errors.Join(errs...))
}
return nil
}
+// keyTypesForAlgorithm returns the registered key types that can
+// produce the given signature algorithm. The inverse map is maintained
+// at registration time so this is an O(1) lookup. Returns nil if no
+// key type is registered for alg, which signals callers to skip the
+// prefilter.
+func keyTypesForAlgorithm(alg jwa.SignatureAlgorithm) []jwa.KeyType {
+ muAlgorithmMaps.RLock()
+ defer muAlgorithmMaps.RUnlock()
+ // Copy so the caller can safely iterate without holding the
+ // lock; RegisterAlgorithmForKeyType may append concurrently
+ // after we return. Typical length is 1.
+ return slices.Clone(algorithmToKeyTypes[alg])
+}
+
type jkuProvider struct {
fetcher jwk.Fetcher
options []jwk.FetchOption
@@ -256,8 +326,13 @@ func (kp jkuProvider) FetchKeys(ctx context.Context, sink KeySink, sig *Signatur
key, ok := set.LookupKeyID(kid)
if !ok {
- // It is not an error if the key with the kid doesn't exist
- return nil
+ return fmt.Errorf(`jku: key with "kid" %q not found in JWKS fetched from %q`, kid, u)
+ }
+
+ if usage, ok := key.KeyUsage(); ok {
+ if usage != "" && usage != jwk.ForSignature.String() {
+ return fmt.Errorf(`key with kid %q is marked use=%q, not usable for signature verification (expected %q)`, kid, usage, jwk.ForSignature.String())
+ }
}
algs, err := AlgorithmsForKey(key)
@@ -266,19 +341,26 @@ func (kp jkuProvider) FetchKeys(ctx context.Context, sink KeySink, sig *Signatur
}
hdrAlg, ok := sig.ProtectedHeaders().Algorithm()
- if ok {
- for _, alg := range algs {
- // if we have an "alg" field in the JWS, we can only proceed if
- // the inferred algorithm matches
- if hdrAlg != alg {
- continue
- }
-
- sink.Key(alg, key)
- break
- }
+ if !ok {
+ // The jku provider routes a key by matching both "kid" and
+ // "alg" against the JWS protected header. With no alg in the
+ // header there's nothing to pin the signature algorithm to,
+ // so reject explicitly rather than returning no keys and
+ // letting the outer verify loop surface a generic "could not
+ // be verified with any of the keys" message.
+ return fmt.Errorf(`use of "jku" requires that the protected header contain an "alg" field`)
}
- return nil
+
+ for _, alg := range algs {
+ if hdrAlg != alg {
+ continue
+ }
+
+ sink.Key(alg, key)
+ return nil
+ }
+
+ return fmt.Errorf(`algorithm %q in JWS header does not match any algorithm for key type %s from jku`, hdrAlg, key.KeyType())
}
// KeyProviderFunc is a type of KeyProvider that is implemented by
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go
index 767ad723a3..eeec25c8a5 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy.go
@@ -61,15 +61,20 @@ func enableLegacySigners() {
}
}
- if err := RegisterSigner(jwa.EdDSA(), SignerFactoryFn(func() (Signer, error) {
- return legacy.NewEdDSASigner(), nil
- })); err != nil {
- panic(fmt.Sprintf("RegisterSigner failed: %v", err))
- }
- if err := RegisterVerifier(jwa.EdDSA(), VerifierFactoryFn(func() (Verifier, error) {
- return legacy.NewEdDSAVerifier(), nil
- })); err != nil {
- panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
+ // Ed448 is intentionally excluded: the legacy EdDSA signer/verifier
+ // only supports ed25519. Ed448 is handled via the defaultSigner/defaultVerifier
+ // path which routes to jwsbb (requires github.com/lestrrat-go/jwx-circl-ed448).
+ for _, alg := range []jwa.SignatureAlgorithm{jwa.EdDSA(), jwa.EdDSAEd25519()} {
+ if err := RegisterSigner(alg, SignerFactoryFn(func() (Signer, error) {
+ return legacy.NewEdDSASigner(), nil
+ })); err != nil {
+ panic(fmt.Sprintf("RegisterSigner failed: %v", err))
+ }
+ if err := RegisterVerifier(alg, VerifierFactoryFn(func() (Verifier, error) {
+ return legacy.NewEdDSAVerifier(), nil
+ })); err != nil {
+ panic(fmt.Sprintf("RegisterVerifier failed: %v", err))
+ }
}
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go
index 0b714a44b8..36c816d79f 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/legacy/ecdsa.go
@@ -10,7 +10,6 @@ import (
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
"github.com/lestrrat-go/jwx/v3/internal/keyconv"
- "github.com/lestrrat-go/jwx/v3/internal/pool"
"github.com/lestrrat-go/jwx/v3/jwa"
"github.com/lestrrat-go/jwx/v3/jws/internal/keytype"
)
@@ -179,18 +178,13 @@ func (v *ecdsaVerifier) Verify(payload []byte, signature []byte, key any) error
return fmt.Errorf(`public key used does not contain a point (X,Y) on the curve`)
}
- r := pool.BigInt().Get()
- s := pool.BigInt().Get()
- defer pool.BigInt().Put(r)
- defer pool.BigInt().Put(s)
-
keySize := ecutil.CalculateKeySize(pubkey.Curve)
if len(signature) != keySize*2 {
return fmt.Errorf(`invalid signature length for curve %q`, pubkey.Curve.Params().Name)
}
- r.SetBytes(signature[:keySize])
- s.SetBytes(signature[keySize:])
+ r := new(big.Int).SetBytes(signature[:keySize])
+ s := new(big.Int).SetBytes(signature[keySize:])
h := v.hash.New()
if _, err := h.Write(payload); err != nil {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go
index e113d1438c..02e4590620 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/message.go
@@ -65,14 +65,18 @@ func (s *Signature) UnmarshalJSON(data []byte) error {
s.headers = sup.Header
if buf := sup.Protected; buf != nil {
- src := []byte(*buf)
- if !bytes.HasPrefix(src, []byte{tokens.OpenCurlyBracket}) {
- decoded, err := base64.Decode(src)
- if err != nil {
- return fmt.Errorf(`failed to base64 decode protected headers: %w`, err)
- }
- src = decoded
+ // RFC 7515 §3 mandates that "protected" be base64url-encoded.
+ // Earlier code carried a relaxed probe that accepted a literal-
+ // JSON form (a JSON string whose content begins with "{") and
+ // skipped base64 decoding — that was asymmetric with the
+ // flattened branch (which only base64-decodes) and gave callers
+ // a non-conforming wire form useful for evading byte-exact JWS
+ // dedup / replay caches.
+ decoded, err := base64.Decode([]byte(*buf))
+ if err != nil {
+ return fmt.Errorf(`failed to base64 decode protected headers: %w`, err)
}
+ src := decoded
prt := NewHeaders()
//nolint:forcetypeassert
@@ -96,16 +100,14 @@ func (s *Signature) UnmarshalJSON(data []byte) error {
}
// Sign populates the signature field, with a signature generated by
-// given the signer object and payload.
+// the given signer object and payload.
//
// The first return value is the raw signature in binary format.
-// The second return value s the full three-segment signature
+// The second return value is the full three-segment signature
// (e.g. "eyXXXX.XXXXX.XXXX")
//
-// This method is deprecated, and will be remove in a future release.
-// Signature objects in the future will only be used as containers,
-// and signing will be done using the `jws.Sign` function, or alternatively
-// you could use jwsbb package to craft the signature manually.
+// Deprecated: Signature objects will only be used as containers in the future.
+// Use [Sign] or the jwsbb package to craft signatures manually.
func (s *Signature) Sign(payload []byte, signer Signer, key any) ([]byte, []byte, error) {
return s.sign2(payload, signer, key)
}
@@ -180,7 +182,7 @@ func (s *Signature) sign2(payload []byte, signer interface{ Algorithm() jwa.Sign
} else {
if !s.detached {
if bytes.Contains(payload, []byte{tokens.Period}) {
- return nil, nil, fmt.Errorf(`payload must not contain a "."`)
+ return nil, nil, fmt.Errorf(`compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`)
}
}
plen = len(payload)
@@ -194,8 +196,7 @@ func (s *Signature) sign2(payload []byte, signer interface{ Algorithm() jwa.Sign
buf.WriteByte(tokens.Period)
buf.WriteString(encoder.EncodeToString(s.signature))
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return s.signature, ret, nil
}
@@ -278,7 +279,7 @@ func (m Message) LookupSignature(kid string) []*Signature {
type messageUnmarshalProbe struct {
Payload *string `json:"payload"`
Signatures []json.RawMessage `json:"signatures,omitempty"`
- Header Headers `json:"header,omitempty"`
+ Header json.RawMessage `json:"header,omitempty"`
Protected *string `json:"protected,omitempty"`
Signature *string `json:"signature,omitempty"`
}
@@ -287,19 +288,42 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
m.payload = nil
m.signatures = nil
m.b64 = true
+ m.detached = false
var mup messageUnmarshalProbe
- mup.Header = NewHeaders()
if err := json.Unmarshal(buf, &mup); err != nil {
return fmt.Errorf(`failed to unmarshal into temporary structure: %w`, err)
}
+ // Enforce the signature cap before we decode any signature entry.
+ // The probe above leaves mup.Signatures as []json.RawMessage, so no
+ // headers/base64 work has happened yet. Doing the check here prevents
+ // a large signatures array from allocating O(input) work before Parse
+ // gets a chance to reject it.
+ maxSigs := m.maxSignatures
+ if maxSigs == 0 {
+ maxSigs = int(maxSignatures.Load())
+ }
+ if maxSigs > 0 && len(mup.Signatures) > maxSigs {
+ return fmt.Errorf(`too many signatures in JWS message (%d > %d)`, len(mup.Signatures), maxSigs)
+ }
+
b64 := true
if mup.Signature == nil { // flattened signature is NOT present
if len(mup.Signatures) == 0 {
return fmt.Errorf(`required field "signatures" not present`)
}
+ // RFC 7515 §7.2.1 places the unprotected JOSE header inside each
+ // signature entry for the general (multi-signature) form; a
+ // top-level "header" sibling of "signatures" is not defined.
+ // Reject rather than silently drop — silent drop hid both typos
+ // and an attacker-controlled trigger surface for any
+ // RegisterCustomDecoder side effects on the dropped contents.
+ if len(mup.Header) > 0 && !bytes.Equal(mup.Header, []byte("null")) {
+ return fmt.Errorf(`general-form JWS must not contain top-level "header" sibling of "signatures" (RFC 7515 §7.2.1 places the unprotected header inside each signature entry)`)
+ }
+
m.signatures = make([]*Signature, 0, len(mup.Signatures))
for i, rawsig := range mup.Signatures {
var sig Signature
@@ -319,8 +343,8 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
b64 = false
}
} else {
- if b64 != getB64Value(sig.protected) {
- return fmt.Errorf(`b64 value must be the same for all signatures`)
+ if got := getB64Value(sig.protected); b64 != got {
+ return fmt.Errorf(`b64 value must be the same for all signatures; signature #%d declared b64=%t but earlier signature(s) declared b64=%t`, i+1, got, b64)
}
}
@@ -332,7 +356,13 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
}
var sig Signature
- sig.headers = mup.Header
+ if len(mup.Header) > 0 && !bytes.Equal(mup.Header, []byte("null")) {
+ hdrs := NewHeaders()
+ if err := json.Unmarshal(mup.Header, hdrs); err != nil {
+ return fmt.Errorf(`failed to unmarshal flattened unprotected header: %w`, err)
+ }
+ sig.headers = hdrs
+ }
if src := mup.Protected; src != nil {
decoded, err := base64.DecodeString(*src)
if err != nil {
@@ -364,7 +394,11 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
b64 = getB64Value(sig.protected)
}
- if mup.Payload != nil {
+ if mup.Payload == nil {
+ // RFC 7515 Appendix F: detached content is signaled in the
+ // JSON Serialization by omitting the "payload" member.
+ m.detached = true
+ } else {
if !b64 { // NOT base64 encoded
m.payload = []byte(*mup.Payload)
} else {
@@ -405,30 +439,54 @@ func (m Message) marshalFlattened() ([]byte, error) {
wrote = true
}
- if wrote {
- buf.WriteRune(tokens.Comma)
+ // RFC 7515 Appendix F: detached content is signaled in the JSON
+ // Serialization by omitting the "payload" member.
+ if !m.detached {
+ if wrote {
+ buf.WriteRune(tokens.Comma)
+ }
+ if !getB64Value(sig.protected) {
+ // RFC 7797 b64=false: emit the raw payload as a JSON string
+ // rather than re-base64-encoding it. json.Marshal handles
+ // any necessary escaping for byte sequences that aren't
+ // JSON-safe as-is.
+ payloadbuf, err := json.Marshal(string(m.payload))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal "payload" (flattened, b64=false): %w`, err)
+ }
+ buf.WriteString(`"payload":`)
+ buf.Write(payloadbuf)
+ } else {
+ buf.WriteString(`"payload":"`)
+ buf.Write(base64.Encode(m.payload))
+ buf.WriteRune('"')
+ }
+ wrote = true
}
- buf.WriteString(`"payload":"`)
- buf.WriteString(base64.EncodeToString(m.payload))
- buf.WriteRune('"')
if protected := sig.protected; protected != nil {
protectedbuf, err := json.Marshal(protected)
if err != nil {
return nil, fmt.Errorf(`failed to marshal "protected" (flattened format): %w`, err)
}
- buf.WriteString(`,"protected":"`)
- buf.WriteString(base64.EncodeToString(protectedbuf))
+ if wrote {
+ buf.WriteRune(tokens.Comma)
+ }
+ buf.WriteString(`"protected":"`)
+ buf.Write(base64.Encode(protectedbuf))
buf.WriteRune('"')
+ wrote = true
}
- buf.WriteString(`,"signature":"`)
- buf.WriteString(base64.EncodeToString(sig.signature))
+ if wrote {
+ buf.WriteRune(tokens.Comma)
+ }
+ buf.WriteString(`"signature":"`)
+ buf.Write(base64.Encode(sig.signature))
buf.WriteRune('"')
buf.WriteRune(tokens.CloseCurlyBracket)
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
@@ -436,9 +494,34 @@ func (m Message) marshalFull() ([]byte, error) {
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
- buf.WriteString(`{"payload":"`)
- buf.WriteString(base64.EncodeToString(m.payload))
- buf.WriteString(`","signatures":[`)
+ // RFC 7515 Appendix F: detached content is signaled in the JSON
+ // Serialization by omitting the "payload" member.
+ if m.detached {
+ buf.WriteString(`{"signatures":[`)
+ } else {
+ // RFC 7797 b64=false: emit the raw payload as a JSON string
+ // rather than re-base64-encoding it. The general JWS form has
+ // one shared payload across signatures; per RFC 7797, all
+ // signers must agree on the b64 flag, so we consult the first
+ // signature's protected header.
+ var b64 = true
+ if len(m.signatures) > 0 {
+ b64 = getB64Value(m.signatures[0].protected)
+ }
+ if !b64 {
+ payloadbuf, err := json.Marshal(string(m.payload))
+ if err != nil {
+ return nil, fmt.Errorf(`failed to marshal "payload" (full, b64=false): %w`, err)
+ }
+ buf.WriteString(`{"payload":`)
+ buf.Write(payloadbuf)
+ buf.WriteString(`,"signatures":[`)
+ } else {
+ buf.WriteString(`{"payload":"`)
+ buf.Write(base64.Encode(m.payload))
+ buf.WriteString(`","signatures":[`)
+ }
+ }
for i, sig := range m.signatures {
if i > 0 {
buf.WriteRune(tokens.Comma)
@@ -465,7 +548,7 @@ func (m Message) marshalFull() ([]byte, error) {
buf.WriteRune(tokens.Comma)
}
buf.WriteString(`"protected":"`)
- buf.WriteString(base64.EncodeToString(protectedbuf))
+ buf.Write(base64.Encode(protectedbuf))
buf.WriteRune('"')
wrote = true
}
@@ -476,15 +559,14 @@ func (m Message) marshalFull() ([]byte, error) {
buf.WriteRune(tokens.Comma)
}
buf.WriteString(`"signature":"`)
- buf.WriteString(base64.EncodeToString(sig.signature))
+ buf.Write(base64.Encode(sig.signature))
buf.WriteString(`"`)
}
buf.WriteString(`}`)
}
buf.WriteString(`]}`)
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
@@ -497,7 +579,7 @@ func (m Message) marshalFull() ([]byte, error) {
// must be passed to the function.
func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
if l := len(msg.signatures); l != 1 {
- return nil, fmt.Errorf(`jws.Compact: cannot serialize message with %d signatures (must be one)`, l)
+ return nil, makeSignError(prefixJwsCompact, `cannot serialize message with %d signatures (must be one)`, l)
}
var detached bool
@@ -506,11 +588,11 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
switch option.Ident() {
case identDetached{}:
if err := option.Value(&detached); err != nil {
- return nil, fmt.Errorf(`jws.Compact: failed to retrieve detached option value: %w`, err)
+ return nil, makeSignError(prefixJwsCompact, `failed to retrieve detached option value: %w`, err)
}
case identBase64Encoder{}:
if err := option.Value(&encoder); err != nil {
- return nil, fmt.Errorf(`jws.Compact: failed to retrieve base64 encoder option value: %w`, err)
+ return nil, makeSignError(prefixJwsCompact, `failed to retrieve base64 encoder option value: %w`, err)
}
}
}
@@ -521,7 +603,7 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
hdrbuf, err := json.Marshal(hdrs)
if err != nil {
- return nil, fmt.Errorf(`jws.Compress: failed to marshal headers: %w`, err)
+ return nil, makeSignError(prefixJwsCompact, `failed to marshal headers: %w`, err)
}
buf := pool.BytesBuffer().Get()
@@ -536,7 +618,7 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
buf.WriteString(encoded)
} else {
if bytes.Contains(msg.payload, []byte{tokens.Period}) {
- return nil, fmt.Errorf(`jws.Compress: payload must not contain a "."`)
+ return nil, makeSignError(prefixJwsCompact, `compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`)
}
buf.Write(msg.payload)
}
@@ -544,7 +626,6 @@ func Compact(msg *Message, options ...CompactOption) ([]byte, error) {
buf.WriteByte(tokens.Period)
buf.WriteString(encoder.EncodeToString(s.signature))
- ret := make([]byte, buf.Len())
- copy(ret, buf.Bytes())
+ ret := bytes.Clone(buf.Bytes())
return ret, nil
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go
index 4c217c3483..cc98abc5be 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.go
@@ -7,6 +7,42 @@ import (
)
type identInsecureNoSignature struct{}
+type identCritExtension struct{}
+
+// WithCritExtension declares that the caller understands and will process
+// the named "crit" (Critical) header parameter extension(s) per RFC 7515
+// Section 4.1.11. The option is variadic and accumulating: a single call
+// may register any number of extension names, and the option may be
+// passed multiple times to add more.
+//
+// This option only takes effect when jws.WithCritValidation(true) is
+// also passed. With validation enabled, jws.Verify() rejects any JWS
+// whose protected header lists a "crit" extension that has not been
+// declared via this option, satisfying the RFC's requirement that
+// recipients MUST reject any "crit" extension they do not understand.
+//
+// IMPORTANT: declaring an extension here is a promise to the library
+// that the caller knows what the extension means and will perform any
+// validation, side effect, or policy enforcement the extension requires
+// AFTER jws.Verify() returns successfully. The library cannot inspect
+// or enforce the semantics of an extension; it only checks that every
+// "crit" entry in the message has been declared. If you register an
+// extension and then forget to act on its value, you have effectively
+// disabled the protection the producer was trying to obtain by listing
+// the extension as critical.
+//
+// Concretely, the post-verify code path for a declared extension must:
+//
+// 1. Read the value of the named header from the verified message.
+// 2. Apply whatever check or transformation the extension specifies
+// (e.g. for an "x-tenant-binding" extension, refuse to act on the
+// payload unless the binding matches the current tenant).
+// 3. Treat any failure of that check as a verification failure for
+// the application's purposes, even though jws.Verify() returned
+// no error.
+func WithCritExtension(names ...string) VerifyOption {
+ return &verifyOption{option.New(identCritExtension{}, names)}
+}
// WithJSON specifies that the result of `jws.Sign()` is serialized in
// JSON format.
@@ -38,7 +74,10 @@ type withKey struct {
public Headers
}
-// Protected exists as an escape hatch to modify the header values after the fact
+// Protected returns the protected headers. If w.protected is nil and v is
+// non-nil, v is stored as the protected headers before returning. This allows
+// callers to provide a default Headers that is used only when none was
+// explicitly configured via WithProtectedHeaders.
func (w *withKey) Protected(v Headers) Headers {
if w.protected == nil && v != nil {
w.protected = v
@@ -48,11 +87,15 @@ func (w *withKey) Protected(v Headers) Headers {
// WithKey is used to pass a static algorithm/key pair to either `jws.Sign()` or `jws.Verify()`.
//
+// IMPORTANT: Although `alg` is typed as `jwa.KeyAlgorithm` for compatibility
+// with `(jwk.Key).Algorithm()`, JWS only accepts `jwa.SignatureAlgorithm`
+// values here. Passing a key-encryption algorithm such as `jwa.A128KW()` to
+// `jws.WithKey()` compiles, but `jws.Sign()` / `jws.Verify()` reject it at runtime.
+//
// The `alg` parameter is the identifier for the signature algorithm that should be used.
-// It is of type `jwa.KeyAlgorithm` but in reality you can only pass `jwa.SignatureAlgorithm`
-// types. It is this way so that the value in `(jwk.Key).Algorithm()` can be directly
-// passed to the option. If you specify other algorithm types such as `jwa.KeyEncryptionAlgorithm`,
-// then you will get an error when `jws.Sign()` or `jws.Verify()` is executed.
+// It is of type `jwa.KeyAlgorithm` so that the value in `(jwk.Key).Algorithm()` can be
+// directly passed to the option, but that is only valid when the JWK is already known to
+// be intended for JWS and its `alg` value is a `jwa.SignatureAlgorithm`.
//
// The `alg` parameter cannot be "none" (jwa.NoSignature) for security reasons.
// You will have to use a separate, more explicit option to allow the use of "none"
@@ -221,7 +264,10 @@ type withInsecureNoSignature struct {
protected Headers
}
-// Protected exists as an escape hatch to modify the header values after the fact
+// Protected returns the protected headers. If w.protected is nil and v is
+// non-nil, v is stored as the protected headers before returning. This allows
+// callers to provide a default Headers that is used only when none was
+// explicitly configured via WithProtectedHeaders.
func (w *withInsecureNoSignature) Protected(v Headers) Headers {
if w.protected == nil && v != nil {
w.protected = v
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml
index 79dbb72500..fb8c88db62 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options.yaml
@@ -54,6 +54,14 @@ interfaces:
- verifyOption
- parseOption
- readFileOption
+ - name: GlobalParseOption
+ methods:
+ - globalOption
+ - parseOption
+ - readFileOption
+ comment: |
+ GlobalParseOption describes an Option that can be passed to `jws.Settings()`,
+ `jws.Parse()`, and `jws.ReadFile()`.
- name: GlobalOption
comment: |
GlobalOption can be passed to `jws.Settings()` to set global options for the JWS package.
@@ -89,11 +97,88 @@ options:
Note that this option does NOT populate the `b64` header, which is sometimes
required by other JWS implementations.
-
+
When this option is used for `jws.Sign()`, the first parameter (normally the payload)
must be set to `nil`.
+
+ When passed to `jws.Verify()` together with `jws.WithCritValidation(true)`,
+ the RFC 7797 `"b64"` extension is automatically added to the
+ caller's allowlist as if `jws.WithCritExtension("b64")` had also
+ been passed. Detached-payload verification is the canonical
+ pairing for `b64=false`, and the jws package implements `b64=false`
+ handling natively, so there is no need to declare it explicitly.
+ Other crit extensions still require explicit declaration.
If you have to verify using this option, you should know exactly how and why this works.
+ - ident: DetachedPayloadReader
+ interface: SignVerifyOption
+ argument_type: io.Reader
+ comment: |
+ WithDetachedPayloadReader is the streaming variant of
+ `jws.WithDetachedPayload()`: the detached payload is consumed from an
+ `io.Reader` instead of a `[]byte`. Use this when the detached payload
+ is too large to comfortably materialize in memory.
+
+ For detached payloads that already fit in memory, prefer
+ `jws.WithDetachedPayload()`; the byte-slice path supports the full
+ option and algorithm surface, while the Reader path is a narrow
+ specialist.
+
+ The Reader is consumed exactly once. On signing or verification
+ failure the Reader cannot be rewound; callers that need retry
+ semantics must buffer the payload themselves.
+
+ The Reader is accessed from the calling goroutine only; do not share
+ a single Reader between concurrent `jws.Sign` / `jws.Verify` calls
+ unless the Reader is itself goroutine-safe and positioned
+ independently for each call.
+
+ Only the HMAC, RSA (PKCS#1 v1.5 and PSS), and ECDSA algorithm
+ families are supported by this option; EdDSA and custom-family
+ algorithms require the full payload in memory and will be rejected
+ with a clear error. The option is mutually exclusive with
+ `jws.WithDetachedPayload()` and `jws.WithInsecureNoSignature()`.
+ Algorithms registered via `jws.RegisterSigner()` /
+ `jws.RegisterVerifier()` are likewise unreachable through this
+ path (the streaming code dispatches directly against the dsig
+ algorithm registry).
+
+ On sign, multiple `jws.WithKey()` options may be combined with
+ `jws.WithJSON()` to produce a general-form multi-signature JWS;
+ the payload is streamed once and fanned out to each signer. All
+ signers must agree on the RFC 7797 `"b64"` flag, since the
+ produced JWS has a single payload segment on the wire. Compact
+ serialization still allows exactly one signature.
+
+ On verify, this option supports only single-signature JWS input
+ (compact or flattened/general-single-signature JSON). To verify
+ one signature out of a multi-signature JWS, use `jws.Verify()`
+ with `jws.WithDetachedPayload()` and a buffered copy of the
+ payload.
+
+ On verify, `jws.Verify()` returns a non-nil zero-length `[]byte` on
+ success when this option is used, because the payload was streamed
+ from the caller rather than extracted from the JWS envelope. Do not
+ read that slice as "the payload is empty" — the verified bytes are
+ whatever the caller read from the Reader, and callers that need
+ them must retain their own copy.
+
+ `jws.WithBase64Encoder()` is honored as long as the supplied
+ encoder implements `jws.Base64StreamEncoder` (i.e. provides a
+ stream-capable `NewEncoder(io.Writer) io.WriteCloser`). The
+ default encoder (`encoding/base64.RawURLEncoding`) satisfies
+ this automatically. Custom encoders that only implement the
+ basic `jws.Base64Encoder` interface cause `jws.Sign()` /
+ `jws.Verify()` to return an error when combined with this
+ option, rather than silently falling back to a different encoder
+ for the payload.
+
+ Like `jws.WithDetachedPayload()`, on verify this option
+ implicitly declares RFC 7797 `"b64"` as a recognized `crit`
+ extension.
+
+ When this option is used with `jws.Sign()`, the first parameter
+ (normally the payload) must be set to `nil`.
- ident: Base64Encoder
interface: SignVerifyCompactOption
argument_type: Base64Encoder
@@ -165,6 +250,17 @@ options:
It is highly recommended that you fix your key to contain a proper `alg`
header field instead of resorting to using this option, but sometimes
it just needs to happen.
+
+ Fan-out and DoS considerations: when combined with `WithRequireKid(false)`
+ against a large JWKS, verification attempts scale with the number of
+ keys in the set. If the JWS protected header advertises an `alg` (as
+ required by RFC 7515 §4.1.1), only keys whose type is compatible with
+ that algorithm are tried, so the cost is bounded by the number of
+ type-compatible keys. If the header has no `alg`, every inferred
+ algorithm is tried against every candidate key, and the cost becomes
+ `N_keys × N_algs_per_keytype`. Operators exposing verification to
+ untrusted input should pair this option with `WithMaxSignatures` and
+ keep their JWKS bounded.
- ident: UseDefault
interface: WithKeySetSuboption
argument_type: bool
@@ -199,14 +295,36 @@ options:
- ident: Context
interface: VerifyOption
argument_type: context.Context
+ comment: |
+ WithContext attaches a context.Context to the verify call. The
+ context is observed at three layers:
+
+ - jws.Verify (slow path) checks ctx.Err() between each signature,
+ each key provider, and each (alg, key) attempt — a deadline
+ fired between iterations short-circuits the loop with the
+ context error.
+ - jkuProvider.FetchKeys passes ctx to jwk.Fetcher.Fetch.
+ - The streaming detached-payload path checks ctx between Reads
+ of the caller's payload reader.
+
+ staticKeyProvider and keySetProvider do not themselves consult
+ ctx inside FetchKeys (the backing data is already in memory);
+ cancellation observation between key candidates is handled by
+ Verify's loop checks above.
- ident: ProtectedHeaders
interface: WithKeySuboption
argument_type: Headers
comment: |
WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()`
to specify a protected header to be attached to the JWS signature.
-
- It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
+
+ It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`.
+
+ kid precedence: if the supplied headers include a "kid" and the
+ `jwk.Key` passed to `jws.WithKey()` also carries one, the
+ `jwk.Key`'s kid wins and the header kid is silently overwritten.
+ Callers that want to keep the header kid must strip it from the
+ key first (e.g. `key.Remove(jwk.KeyIDKey)`).
- ident: PublicHeaders
interface: WithKeySuboption
argument_type: Headers
@@ -228,3 +346,47 @@ options:
constant_value: true
comment: |
WithLegacySigners is a no-op option that exists only for backwards compatibility.
+ - ident: MaxSignatures
+ interface: GlobalParseOption
+ argument_type: int
+ comment: |
+ WithMaxSignatures specifies the maximum number of signatures allowed
+ in a JWS message using JSON serialization. If a JWS message contains
+ more signatures than this value, parsing will return an error.
+ The default value is 100.
+
+ This option can be passed to `jws.Settings()` to change the default
+ globally, or to `jws.Parse()` / `jws.ReadFile()` for a per-call
+ override.
+ - ident: CritValidation
+ interface: VerifyOption
+ argument_type: bool
+ comment: |
+ WithCritValidation enables RFC 7515 Section 4.1.11 validation of the
+ "crit" (Critical) header parameter during verification. The default
+ is false, matching the behavior of v3.0.13 and earlier (the "crit"
+ header is silently ignored).
+
+ When enabled, jws.Verify() will reject any JWS whose protected
+ header lists "crit" entries that the recipient has not declared
+ support for via jws.WithCritExtension(). It will also reject
+ structurally invalid "crit" lists: empty arrays, duplicate names,
+ empty extension names, names of standard JOSE header parameters,
+ and names that do not appear as header parameters in the protected
+ header.
+
+ Per RFC 7515 Section 4.1.11, recipients MUST reject a JWS whose
+ "crit" list names extensions they do not understand. Enabling this
+ option together with one or more jws.WithCritExtension() calls is
+ the only way to satisfy that requirement with this library.
+
+ IMPORTANT: enabling this option makes the library check that every
+ "crit" entry has been declared via jws.WithCritExtension(), but the
+ library cannot perform the actual extension-specific processing on
+ your behalf. After jws.Verify() returns successfully, your code
+ MUST read each declared extension header and apply whatever check
+ or side effect the extension semantics demand. If you declare an
+ extension and then forget to act on its value, you have defeated
+ the protection the producer was trying to obtain by marking that
+ extension critical. See the documentation on jws.WithCritExtension
+ for details.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go
index 7013e86bd7..10dd96e489 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/options_gen.go
@@ -4,6 +4,7 @@ package jws
import (
"context"
+ "io"
"io/fs"
"github.com/lestrrat-go/option/v2"
@@ -35,6 +36,25 @@ type globalOption struct {
func (*globalOption) globalOption() {}
+// GlobalParseOption describes an Option that can be passed to `jws.Settings()`,
+// `jws.Parse()`, and `jws.ReadFile()`.
+type GlobalParseOption interface {
+ Option
+ globalOption()
+ parseOption()
+ readFileOption()
+}
+
+type globalParseOption struct {
+ Option
+}
+
+func (*globalParseOption) globalOption() {}
+
+func (*globalParseOption) parseOption() {}
+
+func (*globalParseOption) readFileOption() {}
+
// ReadFileOption is a type of `Option` that can be passed to `jwe.Parse`
type ParseOption interface {
Option
@@ -185,14 +205,17 @@ func (*withKeySuboption) withKeySuboption() {}
type identBase64Encoder struct{}
type identContext struct{}
+type identCritValidation struct{}
type identDetached struct{}
type identDetachedPayload struct{}
+type identDetachedPayloadReader struct{}
type identFS struct{}
type identInferAlgorithmFromKey struct{}
type identKey struct{}
type identKeyProvider struct{}
type identKeyUsed struct{}
type identLegacySigners struct{}
+type identMaxSignatures struct{}
type identMessage struct{}
type identMultipleKeysPerKeyID struct{}
type identPretty struct{}
@@ -211,6 +234,10 @@ func (identContext) String() string {
return "WithContext"
}
+func (identCritValidation) String() string {
+ return "WithCritValidation"
+}
+
func (identDetached) String() string {
return "WithDetached"
}
@@ -219,6 +246,10 @@ func (identDetachedPayload) String() string {
return "WithDetachedPayload"
}
+func (identDetachedPayloadReader) String() string {
+ return "WithDetachedPayloadReader"
+}
+
func (identFS) String() string {
return "WithFS"
}
@@ -243,6 +274,10 @@ func (identLegacySigners) String() string {
return "WithLegacySigners"
}
+func (identMaxSignatures) String() string {
+ return "WithMaxSignatures"
+}
+
func (identMessage) String() string {
return "WithMessage"
}
@@ -286,10 +321,57 @@ func WithBase64Encoder(v Base64Encoder) SignVerifyCompactOption {
return &signVerifyCompactOption{option.New(identBase64Encoder{}, v)}
}
+// WithContext attaches a context.Context to the verify call. The
+// context is observed at three layers:
+//
+// - jws.Verify (slow path) checks ctx.Err() between each signature,
+// each key provider, and each (alg, key) attempt — a deadline
+// fired between iterations short-circuits the loop with the
+// context error.
+// - jkuProvider.FetchKeys passes ctx to jwk.Fetcher.Fetch.
+// - The streaming detached-payload path checks ctx between Reads
+// of the caller's payload reader.
+//
+// staticKeyProvider and keySetProvider do not themselves consult
+// ctx inside FetchKeys (the backing data is already in memory);
+// cancellation observation between key candidates is handled by
+// Verify's loop checks above.
func WithContext(v context.Context) VerifyOption {
return &verifyOption{option.New(identContext{}, v)}
}
+// WithCritValidation enables RFC 7515 Section 4.1.11 validation of the
+// "crit" (Critical) header parameter during verification. The default
+// is false, matching the behavior of v3.0.13 and earlier (the "crit"
+// header is silently ignored).
+//
+// When enabled, jws.Verify() will reject any JWS whose protected
+// header lists "crit" entries that the recipient has not declared
+// support for via jws.WithCritExtension(). It will also reject
+// structurally invalid "crit" lists: empty arrays, duplicate names,
+// empty extension names, names of standard JOSE header parameters,
+// and names that do not appear as header parameters in the protected
+// header.
+//
+// Per RFC 7515 Section 4.1.11, recipients MUST reject a JWS whose
+// "crit" list names extensions they do not understand. Enabling this
+// option together with one or more jws.WithCritExtension() calls is
+// the only way to satisfy that requirement with this library.
+//
+// IMPORTANT: enabling this option makes the library check that every
+// "crit" entry has been declared via jws.WithCritExtension(), but the
+// library cannot perform the actual extension-specific processing on
+// your behalf. After jws.Verify() returns successfully, your code
+// MUST read each declared extension header and apply whatever check
+// or side effect the extension semantics demand. If you declare an
+// extension and then forget to act on its value, you have defeated
+// the protection the producer was trying to obtain by marking that
+// extension critical. See the documentation on jws.WithCritExtension
+// for details.
+func WithCritValidation(v bool) VerifyOption {
+ return &verifyOption{option.New(identCritValidation{}, v)}
+}
+
// WithDetached specifies that the `jws.Message` should be serialized in
// JWS compact serialization with detached payload. The resulting octet
// sequence will not contain the payload section.
@@ -305,11 +387,88 @@ func WithDetached(v bool) CompactOption {
// When this option is used for `jws.Sign()`, the first parameter (normally the payload)
// must be set to `nil`.
//
+// When passed to `jws.Verify()` together with `jws.WithCritValidation(true)`,
+// the RFC 7797 `"b64"` extension is automatically added to the
+// caller's allowlist as if `jws.WithCritExtension("b64")` had also
+// been passed. Detached-payload verification is the canonical
+// pairing for `b64=false`, and the jws package implements `b64=false`
+// handling natively, so there is no need to declare it explicitly.
+// Other crit extensions still require explicit declaration.
+//
// If you have to verify using this option, you should know exactly how and why this works.
func WithDetachedPayload(v []byte) SignVerifyOption {
return &signVerifyOption{option.New(identDetachedPayload{}, v)}
}
+// WithDetachedPayloadReader is the streaming variant of
+// `jws.WithDetachedPayload()`: the detached payload is consumed from an
+// `io.Reader` instead of a `[]byte`. Use this when the detached payload
+// is too large to comfortably materialize in memory.
+//
+// For detached payloads that already fit in memory, prefer
+// `jws.WithDetachedPayload()`; the byte-slice path supports the full
+// option and algorithm surface, while the Reader path is a narrow
+// specialist.
+//
+// The Reader is consumed exactly once. On signing or verification
+// failure the Reader cannot be rewound; callers that need retry
+// semantics must buffer the payload themselves.
+//
+// The Reader is accessed from the calling goroutine only; do not share
+// a single Reader between concurrent `jws.Sign` / `jws.Verify` calls
+// unless the Reader is itself goroutine-safe and positioned
+// independently for each call.
+//
+// Only the HMAC, RSA (PKCS#1 v1.5 and PSS), and ECDSA algorithm
+// families are supported by this option; EdDSA and custom-family
+// algorithms require the full payload in memory and will be rejected
+// with a clear error. The option is mutually exclusive with
+// `jws.WithDetachedPayload()` and `jws.WithInsecureNoSignature()`.
+// Algorithms registered via `jws.RegisterSigner()` /
+// `jws.RegisterVerifier()` are likewise unreachable through this
+// path (the streaming code dispatches directly against the dsig
+// algorithm registry).
+//
+// On sign, multiple `jws.WithKey()` options may be combined with
+// `jws.WithJSON()` to produce a general-form multi-signature JWS;
+// the payload is streamed once and fanned out to each signer. All
+// signers must agree on the RFC 7797 `"b64"` flag, since the
+// produced JWS has a single payload segment on the wire. Compact
+// serialization still allows exactly one signature.
+//
+// On verify, this option supports only single-signature JWS input
+// (compact or flattened/general-single-signature JSON). To verify
+// one signature out of a multi-signature JWS, use `jws.Verify()`
+// with `jws.WithDetachedPayload()` and a buffered copy of the
+// payload.
+//
+// On verify, `jws.Verify()` returns a non-nil zero-length `[]byte` on
+// success when this option is used, because the payload was streamed
+// from the caller rather than extracted from the JWS envelope. Do not
+// read that slice as "the payload is empty" — the verified bytes are
+// whatever the caller read from the Reader, and callers that need
+// them must retain their own copy.
+//
+// `jws.WithBase64Encoder()` is honored as long as the supplied
+// encoder implements `jws.Base64StreamEncoder` (i.e. provides a
+// stream-capable `NewEncoder(io.Writer) io.WriteCloser`). The
+// default encoder (`encoding/base64.RawURLEncoding`) satisfies
+// this automatically. Custom encoders that only implement the
+// basic `jws.Base64Encoder` interface cause `jws.Sign()` /
+// `jws.Verify()` to return an error when combined with this
+// option, rather than silently falling back to a different encoder
+// for the payload.
+//
+// Like `jws.WithDetachedPayload()`, on verify this option
+// implicitly declares RFC 7797 `"b64"` as a recognized `crit`
+// extension.
+//
+// When this option is used with `jws.Sign()`, the first parameter
+// (normally the payload) must be set to `nil`.
+func WithDetachedPayloadReader(v io.Reader) SignVerifyOption {
+ return &signVerifyOption{option.New(identDetachedPayloadReader{}, v)}
+}
+
// WithFS specifies the source `fs.FS` object to read the file from.
func WithFS(v fs.FS) ReadFileOption {
return &readFileOption{option.New(identFS{}, v)}
@@ -334,6 +493,17 @@ func WithFS(v fs.FS) ReadFileOption {
// It is highly recommended that you fix your key to contain a proper `alg`
// header field instead of resorting to using this option, but sometimes
// it just needs to happen.
+//
+// Fan-out and DoS considerations: when combined with `WithRequireKid(false)`
+// against a large JWKS, verification attempts scale with the number of
+// keys in the set. If the JWS protected header advertises an `alg` (as
+// required by RFC 7515 §4.1.1), only keys whose type is compatible with
+// that algorithm are tried, so the cost is bounded by the number of
+// type-compatible keys. If the header has no `alg`, every inferred
+// algorithm is tried against every candidate key, and the cost becomes
+// `N_keys × N_algs_per_keytype`. Operators exposing verification to
+// untrusted input should pair this option with `WithMaxSignatures` and
+// keep their JWKS bounded.
func WithInferAlgorithmFromKey(v bool) WithKeySetSuboption {
return &withKeySetSuboption{option.New(identInferAlgorithmFromKey{}, v)}
}
@@ -361,6 +531,18 @@ func WithLegacySigners() GlobalOption {
return &globalOption{option.New(identLegacySigners{}, true)}
}
+// WithMaxSignatures specifies the maximum number of signatures allowed
+// in a JWS message using JSON serialization. If a JWS message contains
+// more signatures than this value, parsing will return an error.
+// The default value is 100.
+//
+// This option can be passed to `jws.Settings()` to change the default
+// globally, or to `jws.Parse()` / `jws.ReadFile()` for a per-call
+// override.
+func WithMaxSignatures(v int) GlobalParseOption {
+ return &globalParseOption{option.New(identMaxSignatures{}, v)}
+}
+
// WithMessage can be passed to Verify() to obtain the jws.Message upon
// a successful verification.
func WithMessage(v *Message) VerifyOption {
@@ -385,7 +567,13 @@ func WithPretty(v bool) WithJSONSuboption {
// WithProtected is used with `jws.WithKey()` option when used with `jws.Sign()`
// to specify a protected header to be attached to the JWS signature.
//
-// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`
+// It has no effect if used when `jws.WithKey()` is passed to `jws.Verify()`.
+//
+// kid precedence: if the supplied headers include a "kid" and the
+// `jwk.Key` passed to `jws.WithKey()` also carries one, the
+// `jwk.Key`'s kid wins and the header kid is silently overwritten.
+// Callers that want to keep the header kid must strip it from the
+// key first (e.g. `key.Remove(jwk.KeyIDKey)`).
func WithProtectedHeaders(v Headers) WithKeySuboption {
return &withKeySuboption{option.New(identProtectedHeaders{}, v)}
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go
index 49abe0abca..a6cbd045e9 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/sign_context.go
@@ -2,6 +2,8 @@ package jws
import (
"fmt"
+ "io"
+ "strings"
"github.com/lestrrat-go/jwx/v3/internal/base64"
"github.com/lestrrat-go/jwx/v3/internal/pool"
@@ -9,13 +11,14 @@ import (
)
type signContext struct {
- format int
- detached bool
- validateKey bool
- payload []byte
- encoder Base64Encoder
- none *signatureBuilder // special signature builder
- sigbuilders []*signatureBuilder
+ format int
+ detached bool
+ validateKey bool
+ payload []byte
+ payloadReader io.Reader
+ encoder Base64Encoder
+ none *signatureBuilder // special signature builder
+ sigbuilders []*signatureBuilder
}
var signContextPool = pool.New[*signContext](allocSignContext, freeSignContext)
@@ -39,6 +42,7 @@ func freeSignContext(ctx *signContext) *signContext {
ctx.encoder = base64.DefaultEncoder()
ctx.none = nil
ctx.payload = nil
+ ctx.payloadReader = nil
return ctx
}
@@ -48,12 +52,12 @@ func (sc *signContext) ProcessOptions(options []SignOption) error {
switch option.Ident() {
case identSerialization{}:
if err := option.Value(&sc.format); err != nil {
- return signerr(`failed to retrieve serialization option value: %w`, err)
+ return makeSignError(prefixJwsSign, `failed to retrieve serialization option value: %w`, err)
}
case identInsecureNoSignature{}:
var data withInsecureNoSignature
if err := option.Value(&data); err != nil {
- return signerr(`failed to retrieve insecure-no-signature option value: %w`, err)
+ return makeSignError(prefixJwsSign, `failed to retrieve insecure-no-signature option value: %w`, err)
}
sb := signatureBuilderPool.Get()
sb.alg = jwa.NoSignature()
@@ -64,17 +68,21 @@ func (sc *signContext) ProcessOptions(options []SignOption) error {
case identKey{}:
var data *withKey
if err := option.Value(&data); err != nil {
- return signerr(`jws.Sign: invalid value for WithKey option: %w`, err)
+ return makeSignError(prefixJwsSign, `invalid value for WithKey option: %w`, err)
}
alg, ok := data.alg.(jwa.SignatureAlgorithm)
if !ok {
- return signerr(`expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, data.alg)
+ return makeSignError(prefixJwsSign, `expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, data.alg)
}
// No, we don't accept "none" here.
if alg == jwa.NoSignature() {
- return signerr(`"none" (jwa.NoSignature) cannot be used with jws.WithKey`)
+ return makeSignError(prefixJwsSign, `"none" (jwa.NoSignature) cannot be used with jws.WithKey`)
+ }
+
+ if err := validateAlgorithmForKey(alg, data.key); err != nil {
+ return makeSignError(prefixJwsSign, `%w`, err)
}
sb := signatureBuilderPool.Get()
@@ -97,21 +105,40 @@ func (sc *signContext) ProcessOptions(options []SignOption) error {
sc.sigbuilders = append(sc.sigbuilders, sb)
case identDetachedPayload{}:
+ if sc.payloadReader != nil {
+ return makeSignError(prefixJwsSign, `jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`)
+ }
if sc.payload != nil {
- return signerr(`payload must be nil when jws.WithDetachedPayload() is specified`)
+ return makeSignError(prefixJwsSign, `the first argument to jws.Sign() must be nil when jws.WithDetachedPayload() is used`)
}
if err := option.Value(&sc.payload); err != nil {
- return signerr(`failed to retrieve detached payload option value: %w`, err)
+ return makeSignError(prefixJwsSign, `failed to retrieve detached payload option value: %w`, err)
+ }
+ sc.detached = true
+ case identDetachedPayloadReader{}:
+ if sc.payloadReader != nil {
+ return makeSignError(prefixJwsSign, `jws.WithDetachedPayloadReader() specified more than once`)
+ }
+ if sc.detached {
+ return makeSignError(prefixJwsSign, `jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`)
+ }
+ if sc.payload != nil {
+ return makeSignError(prefixJwsSign, `the first argument to jws.Sign() must be nil when jws.WithDetachedPayloadReader() is used`)
+ }
+ if err := option.Value(&sc.payloadReader); err != nil {
+ return makeSignError(prefixJwsSign, `failed to retrieve detached payload reader option value: %w`, err)
}
sc.detached = true
case identValidateKey{}:
if err := option.Value(&sc.validateKey); err != nil {
- return signerr(`failed to retrieve validate-key option value: %w`, err)
+ return makeSignError(prefixJwsSign, `failed to retrieve validate-key option value: %w`, err)
}
case identBase64Encoder{}:
if err := option.Value(&sc.encoder); err != nil {
- return signerr(`failed to retrieve base64-encoder option value: %w`, err)
+ return makeSignError(prefixJwsSign, `failed to retrieve base64-encoder option value: %w`, err)
}
+ default:
+ return makeSignError(prefixJwsSign, `invalid jws.SignOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
}
}
return nil
@@ -119,6 +146,7 @@ func (sc *signContext) ProcessOptions(options []SignOption) error {
func (sc *signContext) PopulateMessage(m *Message) error {
m.payload = sc.payload
+ m.detached = sc.detached
m.signatures = make([]*Signature, 0, len(sc.sigbuilders))
for i, sb := range sc.sigbuilders {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go
index fc09a69367..2963b6e48d 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/signature_builder.go
@@ -3,6 +3,7 @@ package jws
import (
"bytes"
"fmt"
+ "slices"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/internal/pool"
@@ -52,26 +53,51 @@ func freeSignatureBuilder(sb *signatureBuilder) *signatureBuilder {
}
func (sb *signatureBuilder) Build(sc *signContext, payload []byte) (*Signature, error) {
- protected := sb.protected
- if protected == nil {
+ // Clone caller-provided headers before mutating so that re-using the
+ // same Headers instance across multiple Sign calls does not cause
+ // cross-contamination of alg/kid.
+ var protected Headers
+ if sb.protected != nil {
+ cloned, err := sb.protected.Clone()
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to clone protected headers: %w`, err)
+ }
+ protected = cloned
+ } else {
protected = NewHeaders()
}
if err := protected.Set(AlgorithmKey, sb.alg); err != nil {
- return nil, signerr(`failed to set "alg" header: %w`, err)
+ return nil, makeSignError(prefixJwsSign, `failed to set "alg" header: %w`, err)
}
if key, ok := sb.key.(jwk.Key); ok {
if kid, ok := key.KeyID(); ok && kid != "" {
if err := protected.Set(KeyIDKey, kid); err != nil {
- return nil, signerr(`failed to set "kid" header: %w`, err)
+ return nil, makeSignError(prefixJwsSign, `failed to set "kid" header: %w`, err)
+ }
+ }
+ }
+
+ // RFC 7797 §3 requires producers that set "b64":false to also list
+ // "b64" in "crit". Auto-declare it in the protected header so a
+ // caller who set b64=false but forgot the crit declaration does not
+ // emit a non-conformant stream that strict verifiers refuse.
+ // Idempotent: if "b64" is already in crit, the list is unchanged.
+ // If crit is unset, it is created with just "b64".
+ if !getB64Value(protected) {
+ crit, _ := protected.Critical()
+ if !slices.Contains(crit, "b64") {
+ crit = append(crit, "b64")
+ if err := protected.Set(CriticalKey, crit); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to set "crit" header: %w`, err)
}
}
}
hdrs, err := mergeHeaders(sb.public, protected)
if err != nil {
- return nil, signerr(`failed to merge headers: %w`, err)
+ return nil, makeSignError(prefixJwsSign, `failed to merge headers: %w`, err)
}
// raw, json format headers
@@ -84,7 +110,7 @@ func (sb *signatureBuilder) Build(sc *signContext, payload []byte) (*Signature,
b64 := getB64Value(hdrs)
if !b64 && !sc.detached {
if bytes.IndexByte(payload, tokens.Period) != -1 {
- return nil, fmt.Errorf(`payload must not contain a "."`)
+ return nil, fmt.Errorf(`compact serialization with b64=false requires payload to contain no "." characters per RFC 7797 §5.2; use jws.WithDetachedPayload to keep the payload out of the wire format`)
}
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go
index 99005e859a..af88f5e4b1 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/signer.go
@@ -145,10 +145,9 @@ func UnregisterSigner(alg jwa.SignatureAlgorithm) {
}
// NewSigner creates a signer that signs payloads using the given signature algorithm.
-// This function is deprecated, and will either be removed to re-purposed using
-// a different signature.
//
-// When you want to load a Signer object, you should use `SignerFor()` instead.
+// Deprecated: Use [SignerFor] instead. This function will be removed or
+// repurposed with a different signature in v4.
func NewSigner(alg jwa.SignatureAlgorithm) (Signer, error) {
s, err := newLegacySigner(alg)
if err == nil {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go
new file mode 100644
index 0000000000..871bb6439d
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/streaming_detached.go
@@ -0,0 +1,525 @@
+package jws
+
+import (
+ "crypto/ecdsa"
+ "crypto/hmac"
+ "crypto/rsa"
+ "fmt"
+ "hash"
+ "io"
+
+ "github.com/lestrrat-go/blackmagic"
+ "github.com/lestrrat-go/dsig"
+ "github.com/lestrrat-go/jwx/v3/internal/base64"
+ "github.com/lestrrat-go/jwx/v3/internal/json"
+ "github.com/lestrrat-go/jwx/v3/internal/keyconv"
+ "github.com/lestrrat-go/jwx/v3/internal/tokens"
+ "github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk"
+ "github.com/lestrrat-go/jwx/v3/jws/jwsbb"
+)
+
+// This file implements the streaming detached-payload variant of jws.Sign()
+// and jws.Verify(), reached via the jws.WithDetachedPayloadReader() option.
+// It deliberately bypasses the jws.Signer / jws.Verifier dispatch path and
+// talks to dsig directly, because it needs incremental hashing through a
+// hash.Hash — the Signer interface takes a fully materialized []byte payload.
+//
+// Consequences: algorithms registered via jws.RegisterSigner() /
+// jws.RegisterVerifier() are unreachable here, as are algorithm families
+// that cannot be driven from a digest (EdDSA, custom).
+
+// streamingSigner carries per-signature state through signStreaming:
+// the header prefix already written into hasher, the hasher itself
+// (fed with the prefix, ready to consume payload bytes), the resolved
+// dsig alg info, and the raw key / unprotected header needed for final
+// signing and JSON assembly.
+type streamingSigner struct {
+ dsigInfo streamingAlgorithmInfo
+ rawKey any
+ hasher hash.Hash
+ hdrEncoded string // base64(protected header JSON)
+ public Headers
+}
+
+// signStreaming is invoked from Sign() when sc.payloadReader is set. It
+// assembles the signing input for each configured signer by feeding
+// base64(header) "." base64(payload) (or raw payload when b64=false)
+// into a hash.Hash, then calls dsig.SignDigest once per signer. When
+// more than one signer is registered the payload is streamed once and
+// fanned out to each hasher via [io.MultiWriter]; the general JSON
+// serialization is used for the output.
+func (sc *signContext) signStreaming() ([]byte, error) {
+ if sc.none != nil {
+ return nil, makeSignError(prefixJwsSign, `jws.WithInsecureNoSignature() cannot be combined with jws.WithDetachedPayloadReader(); use jws.Sign with jws.WithInsecureNoSignature() if you really need an unsecured in-memory JWS`)
+ }
+
+ streamEncoder, ok := base64.AsStreamEncoder(sc.encoder)
+ if !ok {
+ return nil, makeSignError(prefixJwsSign, `jws.WithDetachedPayloadReader() requires a base64 encoder with a NewEncoder(io.Writer) io.WriteCloser method (interface jws.Base64StreamEncoder). The configured encoder %T does not provide one. Install a stream-capable encoder via jwx.Settings(jwx.WithBase64Encoder(...)) or jws.WithBase64Encoder(...); the default encoding/base64.RawURLEncoding satisfies this automatically.`, sc.encoder)
+ }
+
+ signers := make([]streamingSigner, 0, len(sc.sigbuilders))
+ var b64 bool
+ var b64Set bool
+
+ for idx, sb := range sc.sigbuilders {
+ alg := sb.alg
+ if alg == jwa.NoSignature() {
+ return nil, makeSignError(prefixJwsSign, `"none" (jwa.NoSignature) cannot be used with jws.WithDetachedPayloadReader()`)
+ }
+
+ dsigInfo, err := resolveStreamingAlgorithm(alg)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `signature %d: %w`, idx, err)
+ }
+
+ if sc.validateKey {
+ if err := validateKeyBeforeUse(sb.key); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to validate key for signature %d: %w`, idx, err)
+ }
+ }
+
+ rawKey, err := convertStreamingSignKey(sb.key, dsigInfo.Family)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to convert key for signature %d: %w`, idx, err)
+ }
+
+ protected, err := cloneOrNewHeaders(sb.protected)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to clone protected headers for signature %d: %w`, idx, err)
+ }
+ if err := protected.Set(AlgorithmKey, alg); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to set "alg" header for signature %d: %w`, idx, err)
+ }
+ if jwkKey, ok := sb.key.(jwk.Key); ok {
+ var kid string
+ if err := jwkKey.Get(jwk.KeyIDKey, &kid); err == nil && kid != "" {
+ if err := protected.Set(KeyIDKey, kid); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to set "kid" header for signature %d: %w`, idx, err)
+ }
+ }
+ }
+
+ // For compact serialization RFC 7515 requires the unprotected
+ // header to be merged into the protected header because there
+ // is no separate slot for it on the wire. JSON serializations
+ // keep them separate.
+ signingHeaders := protected
+ if sc.format == fmtCompact {
+ signingHeaders, err = mergeHeaders(sb.public, protected)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to merge headers for signature %d: %w`, idx, err)
+ }
+ }
+
+ // A multi-signature JWS has a single payload segment on the
+ // wire, so every signer must agree on the RFC 7797 "b64" flag
+ // or the produced JWS is internally inconsistent.
+ thisB64 := getB64Value(signingHeaders)
+ if !b64Set {
+ b64 = thisB64
+ b64Set = true
+ } else if thisB64 != b64 {
+ return nil, makeSignError(prefixJwsSign, `signature %d disagrees with earlier signers on the RFC 7797 "b64" flag; all signers must use the same b64 value`, idx)
+ }
+
+ hdrbuf, err := json.Marshal(signingHeaders)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to marshal headers for signature %d: %w`, idx, err)
+ }
+
+ hasher, err := newStreamingHasher(dsigInfo, rawKey)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to create hasher for signature %d: %w`, idx, err)
+ }
+
+ hdrEncoded := streamEncoder.EncodeToString(hdrbuf)
+ if _, err := hasher.Write([]byte(hdrEncoded)); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to write signing prefix for signature %d: %w`, idx, err)
+ }
+ if _, err := hasher.Write([]byte{tokens.Period}); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to write signing prefix for signature %d: %w`, idx, err)
+ }
+
+ signers = append(signers, streamingSigner{
+ dsigInfo: dsigInfo,
+ rawKey: rawKey,
+ hasher: hasher,
+ hdrEncoded: hdrEncoded,
+ public: sb.public,
+ })
+ }
+
+ hashers := make([]hash.Hash, len(signers))
+ for i := range signers {
+ hashers[i] = signers[i].hasher
+ }
+ if err := streamPayloadIntoHashers(hashers, sc.payloadReader, b64, streamEncoder); err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to stream payload: %w`, err)
+ }
+
+ rawSignatures := make([][]byte, len(signers))
+ for i, st := range signers {
+ sig, err := dsig.SignDigest(st.rawKey, st.dsigInfo.Name, st.hasher.Sum(nil), nil)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to sign digest for signature %d: %w`, i, err)
+ }
+ rawSignatures[i] = sig
+ }
+
+ switch sc.format {
+ case fmtCompact:
+ // Upstream validation in jws.Sign guarantees compact implies
+ // exactly one signer, but guard against future drift.
+ if len(signers) != 1 {
+ return nil, makeSignError(prefixJwsSign, `compact serialization requires exactly one signature, got %d`, len(signers))
+ }
+ sigEncoded := streamEncoder.EncodeToString(rawSignatures[0])
+ buf := make([]byte, 0, len(signers[0].hdrEncoded)+2+len(sigEncoded))
+ buf = append(buf, signers[0].hdrEncoded...)
+ buf = append(buf, tokens.Period, tokens.Period)
+ buf = append(buf, sigEncoded...)
+ return buf, nil
+ case fmtJSON, fmtJSONPretty:
+ return assembleStreamingDetachedJSON(signers, rawSignatures, streamEncoder, sc.format == fmtJSONPretty)
+ default:
+ return nil, makeSignError(prefixJwsSign, `unexpected serialization format %d`, sc.format)
+ }
+}
+
+// verifyStreaming is invoked from VerifyMessage() when vc.payloadReader is
+// set. It parses the JWS envelope through jws.Parse, then re-builds the
+// signing input by feeding base64(header) "." base64(payload) into a
+// hash.Hash fed from the supplied io.Reader and calls dsig.VerifyDigest.
+func (vc *verifyContext) verifyStreaming(buf []byte) ([]byte, error) {
+ if len(vc.keyProviders) != 1 {
+ return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires exactly one jws.WithKey(); jws.WithKeySet(), jws.WithKeyProvider() and jws.WithVerifyAuto() are not supported on the streaming path`)
+ }
+ staticKP, ok := vc.keyProviders[0].(*staticKeyProvider)
+ if !ok {
+ return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires exactly one jws.WithKey(); jws.WithKeySet(), jws.WithKeyProvider() and jws.WithVerifyAuto() are not supported on the streaming path`)
+ }
+ alg := staticKP.alg
+ key := staticKP.key
+
+ if alg == jwa.NoSignature() {
+ return nil, makeVerifyError(`"none" (jwa.NoSignature) cannot be used with jws.WithDetachedPayloadReader(); use jws.Parse if you need to inspect an unsecured JWS`)
+ }
+
+ dsigInfo, err := resolveStreamingAlgorithm(alg)
+ if err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+
+ streamEncoder, ok := base64.AsStreamEncoder(vc.encoder)
+ if !ok {
+ return nil, makeVerifyError(`jws.WithDetachedPayloadReader() requires a base64 encoder with a NewEncoder(io.Writer) io.WriteCloser method (interface jws.Base64StreamEncoder). The configured encoder %T does not provide one. Install a stream-capable encoder via jwx.Settings(jwx.WithBase64Encoder(...)) or jws.WithBase64Encoder(...); the default encoding/base64.RawURLEncoding satisfies this automatically.`, vc.encoder)
+ }
+
+ msg, err := Parse(buf, vc.parseOptions...)
+ if err != nil {
+ return nil, makeVerifyError(`failed to parse jws: %w`, err)
+ }
+ defer msg.clearRaw()
+
+ if len(msg.signatures) != 1 {
+ return nil, makeVerifyError(`jws.WithDetachedPayloadReader() supports only single-signature JWS, got %d`, len(msg.signatures))
+ }
+ if len(msg.payload) != 0 {
+ return nil, makeVerifyError(`JWS must not have an embedded payload when jws.WithDetachedPayloadReader() is used`)
+ }
+
+ sig := msg.signatures[0]
+
+ var rawHeaders []byte
+ if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok {
+ rawHeaders = rbp.rawBuffer()
+ }
+ if rawHeaders == nil {
+ rawHeaders, err = json.Marshal(sig.protected)
+ if err != nil {
+ return nil, makeVerifyError(`failed to marshal "protected": %w`, err)
+ }
+ }
+
+ if vc.critValidation {
+ if err := validateB64InCritIfFalse(sig.protected); err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+ if err := validateCritical(sig.protected, vc.criticalExtensions); err != nil {
+ return nil, makeVerifyError(`invalid "crit" header: %w`, err)
+ }
+ }
+
+ if vc.validateKey {
+ if err := validateKeyBeforeUse(key); err != nil {
+ return nil, makeVerifyError(`failed to validate key before verification: %w`, err)
+ }
+ }
+
+ rawKey, err := convertStreamingVerifyKey(key, dsigInfo.Family)
+ if err != nil {
+ return nil, makeVerifyError(`failed to convert key: %w`, err)
+ }
+
+ hasher, err := newStreamingHasher(dsigInfo, rawKey)
+ if err != nil {
+ return nil, makeVerifyError(`failed to create hasher: %w`, err)
+ }
+ hdrEncoded := streamEncoder.EncodeToString(rawHeaders)
+ if _, err := hasher.Write([]byte(hdrEncoded)); err != nil {
+ return nil, makeVerifyError(`failed to write signing prefix: %w`, err)
+ }
+ if _, err := hasher.Write([]byte{tokens.Period}); err != nil {
+ return nil, makeVerifyError(`failed to write signing prefix: %w`, err)
+ }
+ if err := streamPayloadIntoHashers([]hash.Hash{hasher}, vc.payloadReader, msg.b64, streamEncoder); err != nil {
+ return nil, makeVerifyError(`failed to stream payload: %w`, err)
+ }
+
+ if err := dsig.VerifyDigest(rawKey, dsigInfo.Name, hasher.Sum(nil), sig.signature); err != nil {
+ return nil, makeVerifyError(`failed to verify signature: %w`, verificationError{err})
+ }
+
+ if vc.keyUsed != nil {
+ if err := blackmagic.AssignIfCompatible(vc.keyUsed, key); err != nil {
+ return nil, makeVerifyError(`failed to assign key to keyUsed: %w`, err)
+ }
+ }
+ if vc.dst != nil {
+ *vc.dst = *msg
+ }
+ // Non-nil zero-length slice is the sentinel: the payload was streamed
+ // from the caller so there are no bytes to hand back, but returning
+ // nil would be indistinguishable from "ignored return" and invite
+ // `len(payload) == 0` silent-logic bugs in callers.
+ return []byte{}, nil
+}
+
+// streamingAlgorithmInfo carries the resolved dsig metadata plus the dsig
+// algorithm name, since dsig.AlgorithmInfo itself does not include it.
+type streamingAlgorithmInfo struct {
+ dsig.AlgorithmInfo
+
+ Name string
+}
+
+// resolveStreamingAlgorithm maps a JWS algorithm to its dsig metadata and
+// enforces the family restrictions for the streaming path. It routes through
+// jwsbb.GetDsigAlgorithm so algorithms registered by extension modules work
+// just like algorithms built in to jws.
+func resolveStreamingAlgorithm(alg jwa.SignatureAlgorithm) (streamingAlgorithmInfo, error) {
+ dsigAlg, ok := jwsbb.GetDsigAlgorithm(alg.String())
+ if !ok {
+ // For custom dsig algorithms registered directly with dsig the JWS
+ // name may equal the dsig name.
+ dsigAlg = alg.String()
+ }
+ info, ok := dsig.GetAlgorithmInfo(dsigAlg)
+ if !ok {
+ return streamingAlgorithmInfo{}, fmt.Errorf(`unsupported algorithm %q; use jws.WithDetachedPayload() if you need the general detached path`, alg)
+ }
+ switch info.Family {
+ case dsig.EdDSAFamily:
+ return streamingAlgorithmInfo{}, fmt.Errorf(`algorithm %q is incompatible with streaming detached payloads: RFC 8032 EdDSA signs the full message, not a pre-computed digest, so the payload cannot be streamed; use jws.WithDetachedPayload() if the payload fits in memory, or a digest-based algorithm such as HS256, RS256, or ES256`, alg)
+ case dsig.Custom:
+ return streamingAlgorithmInfo{}, fmt.Errorf(`algorithm %q is a custom-family algorithm and does not support streaming because the library cannot know whether the algorithm pre-hashes the payload; use jws.WithDetachedPayload() if the payload fits in memory`, alg)
+ }
+ return streamingAlgorithmInfo{AlgorithmInfo: info, Name: dsigAlg}, nil
+}
+
+// newStreamingHasher returns a hash.Hash preloaded with the key material
+// the family needs (HMAC is keyed; RSA/ECDSA just hash).
+func newStreamingHasher(info streamingAlgorithmInfo, key any) (hash.Hash, error) {
+ switch info.Family {
+ case dsig.HMAC:
+ meta, ok := info.Meta.(dsig.HMACFamilyMeta)
+ if !ok {
+ return nil, fmt.Errorf(`invalid HMAC metadata`)
+ }
+ keyBytes, ok := key.([]byte)
+ if !ok {
+ // Route through keyconv so the error matches the non-streaming
+ // HMAC path (e.g., passing a string secret surfaces
+ // `keyconv: expected []byte, got string`) instead of the
+ // terser type-assertion failure.
+ if err := keyconv.ByteSliceKey(&keyBytes, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert HMAC key to []byte (streaming path): %w`, err)
+ }
+ }
+ return hmac.New(meta.HashFunc, keyBytes), nil
+ case dsig.RSA:
+ meta, ok := info.Meta.(dsig.RSAFamilyMeta)
+ if !ok {
+ return nil, fmt.Errorf(`invalid RSA metadata`)
+ }
+ return meta.Hash.New(), nil
+ case dsig.ECDSA:
+ meta, ok := info.Meta.(dsig.ECDSAFamilyMeta)
+ if !ok {
+ return nil, fmt.Errorf(`invalid ECDSA metadata`)
+ }
+ return meta.Hash.New(), nil
+ default:
+ return nil, fmt.Errorf(`unsupported algorithm family %q for streaming`, info.Family)
+ }
+}
+
+// streamPayloadIntoHashers copies the payload once and fans it out to
+// every hasher via [io.MultiWriter]. When b64=true the bytes are
+// routed through per-hasher [io.WriteCloser]s returned by the
+// [base64.StreamEncoder] (each encoder keeps an unflushed 3-byte tail,
+// so the wrappers cannot be shared). When b64=false the hashers receive
+// the payload bytes directly.
+func streamPayloadIntoHashers(hashers []hash.Hash, payload io.Reader, encodePayload bool, enc base64.StreamEncoder) error {
+ if len(hashers) == 0 {
+ return fmt.Errorf(`no hashers to stream into`)
+ }
+ if !encodePayload {
+ writers := make([]io.Writer, len(hashers))
+ for i, h := range hashers {
+ writers[i] = h
+ }
+ if _, err := io.Copy(io.MultiWriter(writers...), payload); err != nil {
+ return fmt.Errorf(`failed to stream payload: %w`, err)
+ }
+ return nil
+ }
+
+ encoders := make([]io.WriteCloser, len(hashers))
+ writers := make([]io.Writer, len(hashers))
+ for i, h := range hashers {
+ encoders[i] = enc.NewEncoder(h)
+ writers[i] = encoders[i]
+ }
+ if _, err := io.Copy(io.MultiWriter(writers...), payload); err != nil {
+ for _, w := range encoders {
+ _ = w.Close()
+ }
+ return fmt.Errorf(`failed to stream payload through base64 encoder: %w`, err)
+ }
+ for i, w := range encoders {
+ if err := w.Close(); err != nil {
+ return fmt.Errorf(`failed to close base64 encoder for signature %d: %w`, i, err)
+ }
+ }
+ return nil
+}
+
+type streamingDetachedJSONEntry struct {
+ Header json.RawMessage `json:"header,omitempty"`
+ Protected string `json:"protected"`
+ Signature string `json:"signature"`
+}
+
+type streamingDetachedJSONGeneral struct {
+ Signatures []streamingDetachedJSONEntry `json:"signatures"`
+}
+
+// assembleStreamingDetachedJSON emits the flattened form for a single
+// signature and the general form for multiple. The "payload" member is
+// omitted in both cases per RFC 7515 Appendix F.
+func assembleStreamingDetachedJSON(signers []streamingSigner, rawSignatures [][]byte, enc base64.StreamEncoder, pretty bool) ([]byte, error) {
+ entries := make([]streamingDetachedJSONEntry, len(signers))
+ for i, st := range signers {
+ e := streamingDetachedJSONEntry{
+ Protected: st.hdrEncoded,
+ Signature: enc.EncodeToString(rawSignatures[i]),
+ }
+ if st.public != nil {
+ hdrjs, err := json.Marshal(st.public)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to marshal unprotected header for signature %d: %w`, i, err)
+ }
+ e.Header = hdrjs
+ }
+ entries[i] = e
+ }
+
+ var payload any
+ if len(entries) == 1 {
+ payload = entries[0]
+ } else {
+ payload = streamingDetachedJSONGeneral{Signatures: entries}
+ }
+
+ if pretty {
+ out, err := json.MarshalIndent(payload, "", " ")
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to marshal JSON output: %w`, err)
+ }
+ return out, nil
+ }
+ out, err := json.Marshal(payload)
+ if err != nil {
+ return nil, makeSignError(prefixJwsSign, `failed to marshal JSON output: %w`, err)
+ }
+ return out, nil
+}
+
+// cloneOrNewHeaders returns a defensive copy of hdr, or a fresh empty
+// Headers if hdr is nil. The streaming path mutates the protected headers
+// to set "alg" / "kid", so we never mutate a caller-supplied value.
+func cloneOrNewHeaders(hdr Headers) (Headers, error) {
+ if hdr == nil {
+ return NewHeaders(), nil
+ }
+ return hdr.Clone()
+}
+
+func convertStreamingSignKey(key any, family dsig.Family) (any, error) {
+ if _, ok := key.(jwk.Key); !ok {
+ return key, nil
+ }
+ switch family {
+ case dsig.HMAC:
+ var rawKey []byte
+ if err := keyconv.ByteSliceKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert HMAC key: %w`, err)
+ }
+ return rawKey, nil
+ case dsig.RSA:
+ var rawKey rsa.PrivateKey
+ if err := keyconv.RSAPrivateKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert RSA key: %w`, err)
+ }
+ return &rawKey, nil
+ case dsig.ECDSA:
+ var rawKey ecdsa.PrivateKey
+ if err := keyconv.ECDSAPrivateKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert ECDSA key: %w`, err)
+ }
+ return &rawKey, nil
+ default:
+ return key, nil
+ }
+}
+
+func convertStreamingVerifyKey(key any, family dsig.Family) (any, error) {
+ if _, ok := key.(jwk.Key); !ok {
+ return key, nil
+ }
+ switch family {
+ case dsig.HMAC:
+ var rawKey []byte
+ if err := keyconv.ByteSliceKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert HMAC key: %w`, err)
+ }
+ return rawKey, nil
+ case dsig.RSA:
+ var rawKey rsa.PublicKey
+ if err := keyconv.RSAPublicKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert RSA key: %w`, err)
+ }
+ return &rawKey, nil
+ case dsig.ECDSA:
+ var rawKey ecdsa.PublicKey
+ if err := keyconv.ECDSAPublicKey(&rawKey, key); err != nil {
+ return nil, fmt.Errorf(`failed to convert ECDSA key: %w`, err)
+ }
+ return &rawKey, nil
+ default:
+ return key, nil
+ }
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go
index 70b91c2938..c7838b4ab7 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/verifier.go
@@ -17,10 +17,7 @@ func (v defaultVerifier) Algorithm() jwa.SignatureAlgorithm {
}
func (v defaultVerifier) Verify(key any, payload, signature []byte) error {
- if err := jwsbb.Verify(key, v.alg.String(), payload, signature); err != nil {
- return verifyError{verificationError{err}}
- }
- return nil
+ return jwsbb.Verify(key, v.alg.String(), payload, signature)
}
type Verifier2 interface {
@@ -35,10 +32,7 @@ type verifierAdapter struct {
}
func (v verifierAdapter) Verify(key any, payload, signature []byte) error {
- if err := v.v.Verify(payload, signature, key); err != nil {
- return verifyError{verificationError{err}}
- }
- return nil
+ return v.v.Verify(payload, signature, key)
}
// VerifierFor returns a Verifier2 for the given signature algorithm.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go b/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go
index b4807d569c..f9c2421f34 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jws/verify_context.go
@@ -4,6 +4,8 @@ import (
"context"
"errors"
"fmt"
+ "io"
+ "slices"
"strings"
"github.com/lestrrat-go/blackmagic"
@@ -16,13 +18,16 @@ import (
// verifyContext holds the state during JWS verification
type verifyContext struct {
- parseOptions []ParseOption
- dst *Message
- detachedPayload []byte
- keyProviders []KeyProvider
- keyUsed any
- validateKey bool
- encoder Base64Encoder
+ parseOptions []ParseOption
+ dst *Message
+ detachedPayload []byte
+ payloadReader io.Reader
+ keyProviders []KeyProvider
+ keyUsed any
+ validateKey bool
+ critValidation bool
+ criticalExtensions []string
+ encoder Base64Encoder
//nolint:containedctx
ctx context.Context
}
@@ -40,9 +45,12 @@ func freeVerifyContext(vc *verifyContext) *verifyContext {
vc.parseOptions = vc.parseOptions[:0]
vc.dst = nil
vc.detachedPayload = nil
+ vc.payloadReader = nil
vc.keyProviders = vc.keyProviders[:0]
vc.keyUsed = nil
vc.validateKey = false
+ vc.critValidation = false
+ vc.criticalExtensions = vc.criticalExtensions[:0]
vc.encoder = base64.DefaultEncoder()
vc.ctx = context.Background()
return vc
@@ -54,67 +62,119 @@ func (vc *verifyContext) ProcessOptions(options []VerifyOption) error {
switch option.Ident() {
case identMessage{}:
if err := option.Value(&vc.dst); err != nil {
- return verifyerr(`invalid value for option WithMessage: %w`, err)
+ return makeVerifyError(`invalid value for option WithMessage: %w`, err)
}
case identDetachedPayload{}:
- if err := option.Value(&vc.detachedPayload); err != nil {
- return verifyerr(`invalid value for option WithDetachedPayload: %w`, err)
+ if vc.payloadReader != nil {
+ return makeVerifyError(`jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`)
}
+ if err := option.Value(&vc.detachedPayload); err != nil {
+ return makeVerifyError(`invalid value for option WithDetachedPayload: %w`, err)
+ }
+ // RFC 7797 "b64" auto-declaration. Detached-payload
+ // verification is the canonical use case for b64=false,
+ // and the jws package implements b64=false handling
+ // natively, so requiring callers to also pass
+ // jws.WithCritExtension("b64") is busywork. We declare
+ // it implicitly here so application code stays focused
+ // on its own crit extensions. This does not relax any
+ // other validateCritical check — the b64 header still
+ // has to appear in the protected header, the crit list
+ // still has to be non-empty / no duplicates / no
+ // standard names, etc. Only the "is in the caller's
+ // allowlist" check is short-circuited for "b64", and
+ // only when WithDetachedPayload was passed.
+ vc.criticalExtensions = append(vc.criticalExtensions, "b64")
+ case identDetachedPayloadReader{}:
+ if vc.detachedPayload != nil {
+ return makeVerifyError(`jws.WithDetachedPayload() and jws.WithDetachedPayloadReader() are mutually exclusive`)
+ }
+ if err := option.Value(&vc.payloadReader); err != nil {
+ return makeVerifyError(`invalid value for option WithDetachedPayloadReader: %w`, err)
+ }
+ // Same RFC 7797 "b64" auto-declaration as for
+ // identDetachedPayload; streaming is the other canonical
+ // use case for b64=false.
+ vc.criticalExtensions = append(vc.criticalExtensions, "b64")
case identKey{}:
var pair *withKey
if err := option.Value(&pair); err != nil {
- return verifyerr(`invalid value for option WithKey: %w`, err)
+ return makeVerifyError(`invalid value for option WithKey: %w`, err)
}
+
+ alg, ok := pair.alg.(jwa.SignatureAlgorithm)
+ if !ok {
+ return makeVerifyError(`expected algorithm to be of type jwa.SignatureAlgorithm but got (%[1]q, %[1]T)`, pair.alg)
+ }
+
+ if err := validateAlgorithmForKey(alg, pair.key); err != nil {
+ return makeVerifyError(`%w`, err)
+ }
+
vc.keyProviders = append(vc.keyProviders, &staticKeyProvider{
- alg: pair.alg.(jwa.SignatureAlgorithm),
+ alg: alg,
key: pair.key,
})
case identKeyProvider{}:
var kp KeyProvider
if err := option.Value(&kp); err != nil {
- return verifyerr(`failed to retrieve key-provider option value: %w`, err)
+ return makeVerifyError(`failed to retrieve key-provider option value: %w`, err)
}
vc.keyProviders = append(vc.keyProviders, kp)
case identKeyUsed{}:
if err := option.Value(&vc.keyUsed); err != nil {
- return verifyerr(`failed to retrieve key-used option value: %w`, err)
+ return makeVerifyError(`failed to retrieve key-used option value: %w`, err)
}
case identContext{}:
if err := option.Value(&vc.ctx); err != nil {
- return verifyerr(`failed to retrieve context option value: %w`, err)
+ return makeVerifyError(`failed to retrieve context option value: %w`, err)
}
case identValidateKey{}:
if err := option.Value(&vc.validateKey); err != nil {
- return verifyerr(`failed to retrieve validate-key option value: %w`, err)
+ return makeVerifyError(`failed to retrieve validate-key option value: %w`, err)
}
+ case identCritValidation{}:
+ if err := option.Value(&vc.critValidation); err != nil {
+ return makeVerifyError(`failed to retrieve crit-validation option value: %w`, err)
+ }
+ case identCritExtension{}:
+ var names []string
+ if err := option.Value(&names); err != nil {
+ return makeVerifyError(`failed to retrieve crit-extension option value: %w`, err)
+ }
+ vc.criticalExtensions = append(vc.criticalExtensions, names...)
case identSerialization{}:
vc.parseOptions = append(vc.parseOptions, option.(ParseOption))
case identBase64Encoder{}:
if err := option.Value(&vc.encoder); err != nil {
- return verifyerr(`failed to retrieve base64-encoder option value: %w`, err)
+ return makeVerifyError(`failed to retrieve base64-encoder option value: %w`, err)
}
default:
- return verifyerr(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
+ return makeVerifyError(`invalid jws.VerifyOption %q passed`, `With`+strings.TrimPrefix(fmt.Sprintf(`%T`, option.Ident()), `jws.ident`))
}
}
if len(vc.keyProviders) < 1 {
- return verifyerr(`no key providers have been provided (see jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and jws.WithKeyProvider()`)
+ return makeVerifyError(`no key providers have been provided (see jws.WithKey(), jws.WithKeySet(), jws.WithVerifyAuto(), and jws.WithKeyProvider()`)
}
return nil
}
func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) {
+ if vc.payloadReader != nil {
+ return vc.verifyStreaming(buf)
+ }
+
msg, err := Parse(buf, vc.parseOptions...)
if err != nil {
- return nil, verifyerr(`failed to parse jws: %w`, err)
+ return nil, makeVerifyError(`failed to parse jws: %w`, err)
}
defer msg.clearRaw()
if vc.detachedPayload != nil {
if len(msg.payload) != 0 {
- return nil, verifyerr(`can't specify detached payload for JWS with payload`)
+ return nil, makeVerifyError(`can't specify detached payload for JWS with payload`)
}
msg.payload = vc.detachedPayload
@@ -136,6 +196,15 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) {
pool.ErrorSlice().Put(errs)
}()
for idx, sig := range msg.signatures {
+ // Honor caller's deadline between signatures. Without this
+ // check, a hostile JWS with many signatures keeps the loop
+ // running long after the deadline; only kp.FetchKeys had
+ // visibility into vc.ctx, and not every key provider observes
+ // it. Cheap (~1ns) on the success path.
+ if err := vc.ctx.Err(); err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+
var rawHeaders []byte
if rbp, ok := sig.protected.(interface{ rawBuffer() []byte }); ok {
if raw := rbp.rawBuffer(); raw != nil {
@@ -146,44 +215,81 @@ func (vc *verifyContext) VerifyMessage(buf []byte) ([]byte, error) {
if rawHeaders == nil {
protected, err := json.Marshal(sig.protected)
if err != nil {
- return nil, verifyerr(`failed to marshal "protected" for signature #%d: %w`, idx+1, err)
+ return nil, makeVerifyError(`failed to marshal "protected" for signature #%d: %w`, idx+1, err)
}
rawHeaders = protected
}
+ if vc.critValidation {
+ if err := validateB64InCritIfFalse(sig.protected); err != nil {
+ errs = append(errs, makeVerifyError(`signature #%d: %w`, idx+1, err))
+ continue
+ }
+ if err := validateCritical(sig.protected, vc.criticalExtensions); err != nil {
+ errs = append(errs, makeVerifyError(`signature #%d has invalid "crit" header: %w`, idx+1, err))
+ continue
+ }
+ }
+
verifyBuf = verifyBuf[:0]
verifyBuf = jwsbb.SignBuffer(verifyBuf, rawHeaders, msg.payload, vc.encoder, msg.b64)
+ keysAttempted := 0
for i, kp := range vc.keyProviders {
+ // Honor caller's deadline between key providers.
+ if err := vc.ctx.Err(); err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+
var sink algKeySink
if err := kp.FetchKeys(vc.ctx, &sink, sig, msg); err != nil {
- return nil, verifyerr(`key provider %d failed: %w`, i, err)
+ errs = append(errs, makeVerifyError(`signature #%d: key provider %d failed: %w`, idx+1, i, err))
+ continue
}
for _, pair := range sink.list {
- // alg is converted here because pair.alg is of type jwa.KeyAlgorithm.
- // this may seem ugly, but we're trying to avoid declaring separate
- // structs for `alg jwa.KeyEncryptionAlgorithm` and `alg jwa.SignatureAlgorithm`
- //nolint:forcetypeassert
- alg := pair.alg.(jwa.SignatureAlgorithm)
+ // Honor caller's deadline between (alg,key) pairs.
+ // Under WithRequireKid(false) + WithInferAlgorithmFromKey(true)
+ // + a large JWKS, this inner loop is the dominant
+ // cost — checking ctx between attempts caps the
+ // post-deadline crypto work at one operation.
+ if err := vc.ctx.Err(); err != nil {
+ return nil, makeVerifyError(`%w`, err)
+ }
+
+ alg := pair.alg
key := pair.key
+ keysAttempted++
if err := vc.tryKey(verifyBuf, alg, key, msg, sig); err != nil {
- errs = append(errs, verifyerr(`failed to verify signature #%d with key %T: %w`, idx+1, key, err))
+ errs = append(errs, makeVerifyError(`failed to verify signature #%d with key %T: %w`, idx+1, key, err))
continue
}
return msg.payload, nil
}
}
- errs = append(errs, verifyerr(`signature #%d could not be verified with any of the keys`, idx+1))
+ if keysAttempted == 0 {
+ errs = append(errs, makeVerifyError(`signature #%d: no matching keys were provided by any key provider`, idx+1))
+ } else if looseOpts := vc.namedLooseKeySetOptions(); len(looseOpts) > 0 && keysAttempted > 1 {
+ // When a loose keySet config widened the candidate set, name
+ // the option(s) so the operator can see why a single Verify
+ // call paid N× the cost — the un-attributed message gets
+ // mis-diagnosed by adding more keys instead of fixing the
+ // JWS or tightening the config.
+ errs = append(errs, makeVerifyError(
+ `signature #%d: tried %d (alg,key) pair(s) but none verified successfully; %s widened the candidate set`,
+ idx+1, keysAttempted, strings.Join(looseOpts, " and ")))
+ } else {
+ errs = append(errs, makeVerifyError(`signature #%d: tried %d key(s) but none verified successfully`, idx+1, keysAttempted))
+ }
}
- return nil, verifyerr(`could not verify message using any of the signatures or keys: %w`, errors.Join(errs...))
+ return nil, makeVerifyError(`could not verify message using any of the signatures or keys: %w`, errors.Join(errs...))
}
func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, key any, msg *Message, sig *Signature) error {
if vc.validateKey {
if err := validateKeyBeforeUse(key); err != nil {
- return fmt.Errorf(`failed to validate key before signing: %w`, err)
+ return fmt.Errorf(`failed to validate key before verification: %w`, err)
}
}
@@ -209,3 +315,130 @@ func (vc *verifyContext) tryKey(verifyBuf []byte, alg jwa.SignatureAlgorithm, ke
return nil
}
+
+// validateB64InCritIfFalse enforces RFC 7797 §3: producers that set
+// b64=false in the protected header MUST also list "b64" in the protected
+// header's "crit" array. The check runs alongside (and before)
+// validateCritical so a non-conformant b64=false JWS is rejected up front
+// regardless of whether the caller has supplied a crit allowlist via
+// jws.WithCritExtension. Without this check, jws.Verify silently honors
+// b64=false on the wire and computes its signing input differently from a
+// strictly conformant verifier — exactly the cross-implementation
+// disagreement RFC 7797 §6 was designed to prevent. VerifyCompactFast
+// rejects any b64-bearing message outright via jws.ErrB64Present(); this
+// helper is the slow-path mirror that targets only the non-conformant
+// shape rather than blanket-refusing b64=false.
+func validateB64InCritIfFalse(protected Headers) error {
+ if getB64Value(protected) {
+ return nil
+ }
+ if !protected.Has(CriticalKey) {
+ return makeVerifyError(`protected header has "b64":false but no "crit"; RFC 7797 §3 requires producers that set "b64":false to list "b64" in "crit"`)
+ }
+ crit, _ := protected.Critical()
+ if !slices.Contains(crit, "b64") {
+ return makeVerifyError(`protected header has "b64":false but "crit" does not list "b64"; RFC 7797 §3 requires producers that set "b64":false to list "b64" in "crit"`)
+ }
+ return nil
+}
+
+// validateCritical checks the "crit" header per RFC 7515 Section 4.1.11.
+// It enforces:
+// - the list is non-empty
+// - no entry is the empty string
+// - no entry duplicates another
+// - no entry names a standard JOSE header parameter
+// - every entry appears as a header parameter in the protected header
+// - every entry is in the caller-supplied allowedExtensions allowlist
+//
+// The last check is the central RFC requirement: recipients MUST reject
+// any "crit" extension they do not understand, and the only way the
+// library knows which extensions the caller understands is via the
+// allowlist (populated from jws.WithCritExtension()).
+//
+// As a convenience, the RFC 7797 "b64" extension is auto-declared into
+// allowedExtensions whenever the caller passes jws.WithDetachedPayload
+// — see the identDetachedPayload case in ProcessOptions. The auto-
+// declaration only short-circuits the allowlist check; every other
+// rule above still applies to the "b64" entry.
+func validateCritical(protected Headers, allowedExtensions []string) error {
+ if !protected.Has(CriticalKey) {
+ return nil
+ }
+
+ crit, _ := protected.Critical()
+ if len(crit) == 0 {
+ return makeVerifyError(`"crit" header must not be empty`)
+ }
+
+ seen := make(map[string]struct{}, len(crit))
+ for _, name := range crit {
+ if name == "" {
+ return makeVerifyError(`"crit" header must not contain an empty extension name`)
+ }
+ if _, dup := seen[name]; dup {
+ return makeVerifyError(`"crit" header must not contain duplicate extension %q`, name)
+ }
+ seen[name] = struct{}{}
+
+ // RFC 7515 Section 4.1.11: "crit" MUST NOT include names defined
+ // by the JOSE Header specification itself. The "b64" parameter
+ // is RFC 7797, not RFC 7515 — listing it in "crit" is the
+ // canonical use of the field per RFC 7797 §3 — so exclude it
+ // from this check even though it is a typed field on stdHeaders.
+ if name != B64Key && slices.Contains(stdHeaderNames, name) {
+ return makeVerifyError(`"crit" header must not contain standard header parameter %q`, name)
+ }
+
+ // The extension must be present in the protected header.
+ if !protected.Has(name) {
+ return makeVerifyError(`"crit" header references extension %q, but it is not present in the protected header`, name)
+ }
+
+ // The recipient must have declared support for the extension.
+ if !slices.Contains(allowedExtensions, name) {
+ if name == B64Key {
+ // b64=false is the canonical RFC 7797 case. The
+ // auto-declare only fires for WithDetachedPayload /
+ // WithDetachedPayloadReader; in-band b64=false still
+ // requires the caller to opt in explicitly.
+ return makeVerifyError(`"crit" header references extension "b64", but the recipient has not declared support for it; pass jws.WithCritExtension("b64") to accept in-band b64=false (auto-declare only fires for jws.WithDetachedPayload / jws.WithDetachedPayloadReader)`)
+ }
+ return makeVerifyError(`"crit" header references extension %q, but the recipient has not declared support for it (use jws.WithCritExtension(%q))`, name, name)
+ }
+ }
+
+ return nil
+}
+
+// namedLooseKeySetOptions inspects the registered key providers and
+// returns the human-readable names of the loose-config keySet options
+// in effect for this verify call: jws.WithRequireKid(false) and/or
+// jws.WithInferAlgorithmFromKey(true). These are the options whose
+// presence widens the per-signature (alg,key) candidate set beyond
+// the default "kid + alg pin" of one. The names are used in the final
+// "could not verify" error so an operator sees which options produced
+// the fan-out without grep'ing the source.
+func (vc *verifyContext) namedLooseKeySetOptions() []string {
+ var requireKidFalse, inferAlgorithm bool
+ for _, kp := range vc.keyProviders {
+ ksp, ok := kp.(*keySetProvider)
+ if !ok {
+ continue
+ }
+ if !ksp.requireKid {
+ requireKidFalse = true
+ }
+ if ksp.inferAlgorithm {
+ inferAlgorithm = true
+ }
+ }
+ var names []string
+ if requireKidFalse {
+ names = append(names, "jws.WithRequireKid(false)")
+ }
+ if inferAlgorithm {
+ names = append(names, "jws.WithInferAlgorithmFromKey(true)")
+ }
+ return names
+}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go
new file mode 100644
index 0000000000..53c696762c
--- /dev/null
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/doc.go
@@ -0,0 +1,46 @@
+// Package jwt implements JSON Web Tokens as described in RFC 7519.
+//
+// # Parsing and Verification
+//
+// Parse verifies signed tokens by default. Pass WithKey, WithKeySet,
+// WithKeyProvider, or WithVerifyAuto when the token is signed. A bare
+// Parse call returns an error instead of silently accepting an unverified
+// token.
+//
+// To intentionally skip verification, pass WithVerify(false). Use
+// ParseInsecure only when the input is already trusted: it disables both
+// signature verification and validation, and it rejects key-bearing options
+// so that stray verification settings cannot silently be ignored.
+//
+// # Validation
+//
+// Parse validates registered time claims by default after decoding. In
+// particular, exp, nbf, and iat are checked automatically. Validate can also
+// be called directly on an existing Token. Pass WithValidate(false) to skip
+// automatic validation for a particular parse.
+//
+// # Errors
+//
+// Error checks in v3 use opaque values exposed by helper functions. Use
+// errors.Is with the exported helpers to check for a class of failure:
+//
+// err := jwt.Validate(token)
+// if errors.Is(err, jwt.TokenExpiredError()) { /* ... */ }
+// if errors.Is(err, jwt.InvalidIssuerError()) { /* ... */ }
+//
+// # Time Claims
+//
+// Numeric date claims are decoded into time.Time values. Validation uses
+// exact timestamps by default; configure WithAcceptableSkew for clock skew
+// and WithTruncation when you need truncated comparisons.
+//
+// # OpenID Connect and Nested Tokens
+//
+// Use package github.com/lestrrat-go/jwx/v3/jwt/openid when you want a Token
+// implementation with typed OpenID Connect claim accessors.
+//
+// Parse handles compact JWS JWTs and raw JSON tokens. It does not decrypt
+// JWE envelopes for you. To produce nested JWTs, use
+// NewSerializer().Sign(...).Encrypt(...).Serialize(...), and decrypt any
+// outer JWE before calling Parse.
+package jwt
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go
index 43f7c966da..da2f8acdc2 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/fastpath.go
@@ -12,6 +12,36 @@ import (
"github.com/lestrrat-go/jwx/v3/jws/jwsbb"
)
+// fastPathKidSafe reports whether kid can be concatenated into a
+// hand-built JSON header literal without escaping. Any byte that would
+// require a JSON escape (control bytes, `"`, `\`) or any non-ASCII
+// byte disqualifies the fast path; such kids fall through to the
+// regular jws.Sign path where encoding/json handles escaping.
+func fastPathKidSafe(kid string) bool {
+ for i := range len(kid) {
+ c := kid[i]
+ if c < 0x20 || c >= 0x7f || c == '"' || c == '\\' {
+ return false
+ }
+ }
+ return true
+}
+
+// fastPathAlgSafe reports whether alg can be concatenated into a
+// hand-built JSON header literal without escaping. The byte set
+// mirrors fastPathKidSafe: any control byte, `"`, `\`, or non-ASCII
+// byte disqualifies the value. Unlike kid, an unsafe alg is rejected
+// outright by jwt.Sign rather than silently falling through.
+func fastPathAlgSafe(alg string) bool {
+ for i := range len(alg) {
+ c := alg[i]
+ if c < 0x20 || c >= 0x7f || c == '"' || c == '\\' {
+ return false
+ }
+ }
+ return true
+}
+
// signFast reinvents the wheel a bit to avoid the overhead of
// going through the entire jws.Sign() machinery.
func signFast(t Token, alg jwa.SignatureAlgorithm, key any) ([]byte, error) {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go
index 691c5a0df4..9c02aca7fa 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/http.go
@@ -1,6 +1,7 @@
package jwt
import (
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -19,7 +20,7 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token,
switch option.Ident() {
case identCookie{}:
if err := option.Value(&dst); err != nil {
- return nil, fmt.Errorf(`jws.ParseCookie: value to option WithCookie must be **http.Cookie: %w`, err)
+ return nil, fmt.Errorf(`jwt.ParseCookie: value to option WithCookie must be **http.Cookie: %w`, err)
}
}
}
@@ -30,7 +31,7 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token,
}
tok, err := ParseString(cookie.Value, options...)
if err != nil {
- return nil, fmt.Errorf(`jws.ParseCookie: failed to parse token stored in cookie: %w`, err)
+ return nil, fmt.Errorf(`jwt.ParseCookie: failed to parse token stored in cookie: %w`, err)
}
if dst != nil {
@@ -41,8 +42,10 @@ func ParseCookie(req *http.Request, name string, options ...ParseOption) (Token,
// ParseHeader parses a JWT stored in a http.Header.
//
-// For the header "Authorization", it will strip the prefix "Bearer " and will
-// treat the remaining value as a JWT.
+// For the header "Authorization", it will strip the "Bearer" scheme per
+// RFC 6750 §2.1 (case-insensitive scheme token; space or tab separator
+// required) and treat the remainder as a JWT. If the value does not begin
+// with a well-formed "Bearer ", the full value is parsed as-is.
func ParseHeader(hdr http.Header, name string, options ...ParseOption) (Token, error) {
key := http.CanonicalHeaderKey(name)
v := strings.TrimSpace(hdr.Get(key))
@@ -51,9 +54,9 @@ func ParseHeader(hdr http.Header, name string, options ...ParseOption) (Token, e
}
if key == "Authorization" {
- // Authorization header is an exception. We strip the "Bearer " from
- // the prefix
- v = strings.TrimSpace(strings.TrimPrefix(v, "Bearer"))
+ if len(v) >= 7 && strings.EqualFold(v[:6], "Bearer") && (v[6] == ' ' || v[6] == '\t') {
+ v = strings.TrimSpace(v[7:])
+ }
}
tok, err := ParseString(v, options...)
@@ -84,19 +87,26 @@ func ParseForm(values url.Values, name string, options ...ParseOption) (Token, e
// are specified, you must explicitly re-enable searching for "Authorization" header
// if you also want to search for it.
//
-// # searches for "Authorization"
+// // searches for "Authorization"
// jwt.ParseRequest(req)
//
-// # searches for "x-my-token" ONLY.
+// // searches for "x-my-token" ONLY.
// jwt.ParseRequest(req, jwt.WithHeaderKey("x-my-token"))
//
-// # searches for "Authorization" AND "x-my-token"
+// // searches for "Authorization" AND "x-my-token"
// jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.WithHeaderKey("x-my-token"))
//
// Cookies are searched using (http.Request).Cookie(). If you have multiple
// cookies with the same name, and you want to search for a specific one that
// (http.Request).Cookie() would not return, you will need to implement your
// own logic to extract the cookie and use jwt.ParseString().
+//
+// When (and only when) at least one WithFormKey() option is supplied,
+// ParseRequest will call (*http.Request).ParseForm() to read form fields
+// from the request body. Callers that accept untrusted requests should
+// wrap req.Body with http.MaxBytesReader before calling ParseRequest so
+// that an oversized body does not exhaust memory during form parsing.
+// Without WithFormKey() the request body is left untouched.
func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
var hdrkeys []string
var formkeys []string
@@ -107,19 +117,19 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
case identHeaderKey{}:
var v string
if err := option.Value(&v); err != nil {
- return nil, fmt.Errorf(`jws.ParseRequest: value to option WithHeaderKey must be string: %w`, err)
+ return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithHeaderKey must be string: %w`, err)
}
hdrkeys = append(hdrkeys, v)
case identFormKey{}:
var v string
if err := option.Value(&v); err != nil {
- return nil, fmt.Errorf(`jws.ParseRequest: value to option WithFormKey must be string: %w`, err)
+ return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithFormKey must be string: %w`, err)
}
formkeys = append(formkeys, v)
case identCookieKey{}:
var v string
if err := option.Value(&v); err != nil {
- return nil, fmt.Errorf(`jws.ParseRequest: value to option WithCookieKey must be string: %w`, err)
+ return nil, fmt.Errorf(`jwt.ParseRequest: value to option WithCookieKey must be string: %w`, err)
}
cookiekeys = append(cookiekeys, v)
default:
@@ -165,7 +175,15 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
return tok, nil
}
- if cl := req.ContentLength; cl > 0 {
+ // Only touch the request body when the caller actually asked us
+ // to look at form fields. Without this guard ParseRequest would
+ // call req.ParseForm() on every request — for form-encoded bodies
+ // that drains the body, leaving downstream handlers with an empty
+ // io.Reader; for other Content-Types it is still wasted work on
+ // the URL query. We DO NOT gate on ContentLength: chunked-transfer
+ // requests have ContentLength == -1, and RFC 6750 §2.2 allows
+ // form-borne bearer tokens including under chunked encoding.
+ if len(formkeys) > 0 {
if err := req.ParseForm(); err != nil {
return nil, fmt.Errorf(`failed to parse form: %w`, err)
}
@@ -241,21 +259,35 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
lmhdrs := len(mhdrs)
lmfrms := len(mfrms)
lmcookies := len(mcookies)
- var errors []any
+ // Render display text without fmt verbs. A dynamic fmt.Errorf format
+ // string would be brittle: caller-supplied keys flow through
+ // strconv.Quote, but strconv.Quote does not escape '%', so a key
+ // containing '%s' would otherwise turn into a format verb and mangle
+ // output. Write texts directly and propagate the underlying errors
+ // via errors.Join so errors.Is / errors.As still traverse them.
+ var errs []error
if lmhdrs > 0 || lmfrms > 0 || lmcookies > 0 {
b.WriteString(". Additionally, errors were encountered during attempts to verify using:")
if lmhdrs > 0 {
b.WriteString(" headers: (")
count := 0
- for hdrkey, err := range mhdrs {
+ // Iterate ordered key slices so rendering is deterministic
+ // (map iteration would reorder per run).
+ for _, hdrkey := range hdrkeys {
+ err, ok := mhdrs[hdrkey]
+ if !ok {
+ continue
+ }
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[header key: ")
b.WriteString(strconv.Quote(hdrkey))
- b.WriteString(", error: %w]")
- errors = append(errors, err)
+ b.WriteString(", error: ")
+ b.WriteString(err.Error())
+ b.WriteByte(tokens.CloseSquareBracket)
+ errs = append(errs, err)
count++
}
b.WriteString(")")
@@ -264,32 +296,49 @@ func ParseRequest(req *http.Request, options ...ParseOption) (Token, error) {
if lmcookies > 0 {
count := 0
b.WriteString(" cookies: (")
- for cookiekey, err := range mcookies {
+ for _, cookiekey := range cookiekeys {
+ err, ok := mcookies[cookiekey]
+ if !ok {
+ continue
+ }
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[cookie key: ")
b.WriteString(strconv.Quote(cookiekey))
- b.WriteString(", error: %w]")
- errors = append(errors, err)
+ b.WriteString(", error: ")
+ b.WriteString(err.Error())
+ b.WriteByte(tokens.CloseSquareBracket)
+ errs = append(errs, err)
count++
}
+ b.WriteString(")")
}
if lmfrms > 0 {
count := 0
b.WriteString(" forms: (")
- for formkey, err := range mfrms {
+ for _, formkey := range formkeys {
+ err, ok := mfrms[formkey]
+ if !ok {
+ continue
+ }
if count > 0 {
b.WriteString(", ")
}
b.WriteString("[form key: ")
b.WriteString(strconv.Quote(formkey))
- b.WriteString(", error: %w]")
- errors = append(errors, err)
+ b.WriteString(", error: ")
+ b.WriteString(err.Error())
+ b.WriteByte(tokens.CloseSquareBracket)
+ errs = append(errs, err)
count++
}
+ b.WriteString(")")
}
}
- return nil, fmt.Errorf(b.String(), errors...)
+ if len(errs) == 0 {
+ return nil, errors.New(b.String())
+ }
+ return nil, fmt.Errorf("%s: %w", b.String(), errors.Join(errs...))
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go
index 179763a50d..31bfcd1850 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/errors/errors.go
@@ -173,11 +173,8 @@ type MissingRequiredClaimError struct {
}
func (err *MissingRequiredClaimError) Is(target error) bool {
- err1, ok := target.(*MissingRequiredClaimError)
- if !ok {
- return false
- }
- return err1 == ErrMissingRequiredClaimDefault || err1.claim == err.claim
+ _, ok := target.(*MissingRequiredClaimError)
+ return ok
}
func MissingRequiredClaimErrorf(name string) error {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go
index 3d40a9ed97..959eaeeff1 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/internal/types/date.go
@@ -4,6 +4,7 @@ import (
"fmt"
"strconv"
"strings"
+ "sync/atomic"
"time"
"github.com/lestrrat-go/jwx/v3/internal/json"
@@ -15,9 +16,9 @@ const (
MaxPrecision uint32 = 9 // nanosecond level
)
-var Pedantic uint32
-var ParsePrecision = DefaultPrecision
-var FormatPrecision = DefaultPrecision
+var Pedantic atomic.Uint32
+var ParsePrecision atomic.Uint32
+var FormatPrecision atomic.Uint32
// NumericDate represents the date format used in the 'nbf' claim
type NumericDate struct {
@@ -57,7 +58,7 @@ func parseNumericString(x string) (time.Time, error) {
// Only check for the escape hatch if it's the pedantic
// flag is off
- if Pedantic != 1 {
+ if Pedantic.Load() != 1 {
// This is an escape hatch for non-conformant providers
// that gives us RFC3339 instead of epoch time
for _, r := range x {
@@ -77,12 +78,13 @@ func parseNumericString(x string) (time.Time, error) {
var fractional string
whole := x
+ parsePrecision := ParsePrecision.Load()
if i := strings.IndexRune(x, tokens.Period); i > 0 {
- if ParsePrecision > 0 && len(x) > i+1 {
+ if parsePrecision > 0 && len(x) > i+1 {
fractional = x[i+1:] // everything after the tokens.Period
- if int(ParsePrecision) < len(fractional) {
+ if int(parsePrecision) < len(fractional) {
// Remove insignificant digits
- fractional = fractional[:int(ParsePrecision)]
+ fractional = fractional[:int(parsePrecision)]
}
// Replace missing fractional diits with zeros
for len(fractional) < int(MaxPrecision) {
@@ -146,7 +148,8 @@ func (n *NumericDate) Accept(v any) error {
}
func (n NumericDate) String() string {
- if FormatPrecision == 0 {
+ formatPrecision := FormatPrecision.Load()
+ if formatPrecision == 0 {
return strconv.FormatInt(n.Unix(), 10)
}
@@ -159,7 +162,7 @@ func (n NumericDate) String() string {
}
slwhole := len(s) - int(MaxPrecision)
- s = s[:slwhole] + "." + s[slwhole:slwhole+int(FormatPrecision)]
+ s = s[:slwhole] + "." + s[slwhole:slwhole+int(formatPrecision)]
if s[0] == tokens.Period {
s = "0" + s
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go
index 99b5ef37a6..6229c763cd 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/jwt.go
@@ -1,28 +1,34 @@
//go:generate ../tools/cmd/genjwt.sh
//go:generate stringer -type=TokenOption -output=token_options_gen.go
-// Package jwt implements JSON Web Tokens as described in https://tools.ietf.org/html/rfc7519
package jwt
import (
"bytes"
+ "errors"
"fmt"
"io"
+ "sync"
"sync/atomic"
"time"
"github.com/lestrrat-go/jwx/v3"
"github.com/lestrrat-go/jwx/v3/internal/json"
"github.com/lestrrat-go/jwx/v3/jwa"
+ "github.com/lestrrat-go/jwx/v3/jwk"
"github.com/lestrrat-go/jwx/v3/jws"
jwterrs "github.com/lestrrat-go/jwx/v3/jwt/internal/errors"
"github.com/lestrrat-go/jwx/v3/jwt/internal/types"
)
+var muSettings sync.Mutex
var defaultTruncation atomic.Int64
// Settings controls global settings that are specific to JWTs.
func Settings(options ...GlobalOption) {
+ muSettings.Lock()
+ defer muSettings.Unlock()
+
var flattenAudience bool
var parsePedantic bool
var parsePrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set
@@ -64,38 +70,29 @@ func Settings(options ...GlobalOption) {
}
if parsePrecision <= types.MaxPrecision { // remember we set default to max + 1
- v := atomic.LoadUint32(&types.ParsePrecision)
- if v != parsePrecision {
- atomic.CompareAndSwapUint32(&types.ParsePrecision, v, parsePrecision)
- }
+ types.ParsePrecision.Store(parsePrecision)
}
if formatPrecision <= types.MaxPrecision { // remember we set default to max + 1
- v := atomic.LoadUint32(&types.FormatPrecision)
- if v != formatPrecision {
- atomic.CompareAndSwapUint32(&types.FormatPrecision, v, formatPrecision)
- }
+ types.FormatPrecision.Store(formatPrecision)
}
{
- v := atomic.LoadUint32(&types.Pedantic)
- if (v == 1) != parsePedantic {
- var newVal uint32
- if parsePedantic {
- newVal = 1
- }
- atomic.CompareAndSwapUint32(&types.Pedantic, v, newVal)
+ var newVal uint32
+ if parsePedantic {
+ newVal = 1
}
+ types.Pedantic.Store(newVal)
}
{
- defaultOptionsMu.Lock()
+ opts := TokenOptionSet(defaultOptions.Load())
if flattenAudience {
- defaultOptions.Enable(FlattenAudience)
+ opts.Enable(FlattenAudience)
} else {
- defaultOptions.Disable(FlattenAudience)
+ opts.Disable(FlattenAudience)
}
- defaultOptionsMu.Unlock()
+ defaultOptions.Store(opts.Value())
}
if truncation >= 0 {
@@ -118,27 +115,33 @@ func ParseString(s string, options ...ParseOption) (Token, error) {
// The token must be encoded in JWS compact format, or a raw JSON form of JWT
// without any signatures.
//
-// If you need JWE support on top of JWS, you will need to rollout your
-// own workaround.
+// Signed input is verified by default. Pass `jwt.WithKey()`,
+// `jwt.WithKeySet()`, `jwt.WithKeyProvider()`, or
+// `jwt.WithVerifyAuto(fetcher, fetchOptions...)` when verification is
+// required. A bare `jwt.Parse()` call returns an error; to intentionally
+// skip verification, pass `jwt.WithVerify(false)` or use
+// `jwt.ParseInsecure()`.
//
-// If the token is signed, and you want to verify the payload matches the signature,
-// you must pass the jwt.WithKey(alg, key) or jwt.WithKeySet(jwk.Set) option.
-// If you do not specify these parameters, no verification will be performed.
+// `Parse()` also accepts `ValidateOption` values. Validation runs by default
+// after parsing, so `jwt.WithValidate(true)` is only needed to override a
+// prior `jwt.WithValidate(false)` in the same option set. Pass
+// `jwt.WithValidate(false)` if you need to defer validation and call
+// `Validate()` yourself later.
+//
+// To produce nested JWTs, use
+// `jwt.NewSerializer().Sign(...).Encrypt(...).Serialize(...)`. `Parse()` does
+// not decrypt JWE envelopes; decrypt the outer JWE before calling it.
//
// During verification, if the JWS headers specify a key ID (`kid`), the
// key used for verification must match the specified ID. If you are somehow
// using a key without a `kid` (which is highly unlikely if you are working
-// with a JWT from a well-know provider), you can work around this by modifying
-// the `jwk.Key` and setting the `kid` header.
-//
-// If you also want to assert the validity of the JWT itself (i.e. expiration
-// and such), use the `Validate()` function on the returned token, or pass the
-// `WithValidate(true)` option. Validate options can also be passed to
-// `Parse`
+// with a JWT from a well-known provider), you can work around this by
+// modifying the `jwk.Key` and setting its `kid` field.
//
// This function takes both ParseOption and ValidateOption types:
-// ParseOptions control the parsing behavior, and ValidateOptions are
-// passed to `Validate()` when `jwt.WithValidate` is specified.
+// ParseOptions control parsing and verification behavior, and
+// ValidateOptions are passed to `Validate()` when automatic validation is
+// enabled.
func Parse(s []byte, options ...ParseOption) (Token, error) {
tok, err := parseBytes(s, options...)
if err != nil {
@@ -150,14 +153,19 @@ func Parse(s []byte, options ...ParseOption) (Token, error) {
// ParseInsecure is exactly the same as Parse(), but it disables
// signature verification and token validation.
//
-// You cannot override `jwt.WithVerify()` or `jwt.WithValidate()`
-// using this function. Providing these options would result in
-// an error
+// `jwt.WithVerify()` and `jwt.WithValidate()` may not be specified
+// because they would conflict with the function's purpose. Likewise,
+// the key-bearing options `jwt.WithKey()`, `jwt.WithKeySet()`,
+// `jwt.WithKeyProvider()`, and `jwt.WithVerifyAuto()` are rejected so
+// that typos like `jwt.ParseInsecure(data, jwt.WithKey(...))` cannot
+// silently skip verification. Use `jwt.Parse` when a key is available.
func ParseInsecure(s []byte, options ...ParseOption) (Token, error) {
for _, option := range options {
switch option.Ident() {
case identVerify{}, identValidate{}:
return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `jwt.WithVerify() and jwt.WithValidate() may not be specified`)
+ case identKey{}, identKeySet{}, identKeyProvider{}, identVerifyAuto{}:
+ return nil, jwterrs.ParseErrorf(`jwt.ParseInsecure`, `key-bearing options (jwt.WithKey, jwt.WithKeySet, jwt.WithKeyProvider, jwt.WithVerifyAuto) may not be specified; use jwt.Parse to verify with a key`)
}
}
@@ -169,9 +177,12 @@ func ParseInsecure(s []byte, options ...ParseOption) (Token, error) {
return tok, nil
}
-// ParseReader calls Parse against an io.Reader
+// ParseReader calls Parse against an io.Reader.
+//
+// Bounding the input size is the caller's responsibility: wrap src with
+// [io.LimitReader] or [net/http.MaxBytesReader] before passing it in. See
+// docs/13-input-size.md for the rationale.
func ParseReader(src io.Reader, options ...ParseOption) (Token, error) {
- // We're going to need the raw bytes regardless. Read it.
data, err := io.ReadAll(src)
if err != nil {
return nil, jwterrs.ParseErrorf(`jwt.ParseReader`, `failed to read from token data source: %w`, err)
@@ -184,15 +195,16 @@ func ParseReader(src io.Reader, options ...ParseOption) (Token, error) {
}
type parseCtx struct {
- token Token
- validateOpts []ValidateOption
- verifyOpts []jws.VerifyOption
- localReg *json.Registry
- pedantic bool
- skipVerification bool
- validate bool
- withKeyCount int
- withKey *withKey // this is used to detect if we have a WithKey option
+ token Token
+ validateOpts []ValidateOption
+ verifyOpts []jws.VerifyOption
+ localReg *json.Registry
+ strictStringClaims *bool // per-call override; nil = use global
+ pedantic bool
+ skipVerification bool
+ validate bool
+ withKeyCount int
+ withKey *withKey // this is used to detect if we have a WithKey option
}
func parseBytes(data []byte, options ...ParseOption) (Token, error) {
@@ -262,6 +274,12 @@ func parseBytes(data []byte, options ...ParseOption) (Token, error) {
ctx.localReg = json.NewRegistry()
}
ctx.localReg.Register(pair.Name, pair.Value)
+ case identStrictStringClaims{}:
+ var v bool
+ if err := o.Value(&v); err != nil {
+ return nil, fmt.Errorf("jwt.parseBytes: value for WithStrictStringClaims must be bool: %w", err)
+ }
+ ctx.strictStringClaims = &v
}
}
@@ -306,16 +324,61 @@ func verifyJWS(ctx *parseCtx, payload []byte) ([]byte, int, error) {
alg, ok := wk.alg.(jwa.SignatureAlgorithm)
if ok && len(wk.options) == 0 {
verified, err := jws.VerifyCompactFast(wk.key, payload, alg)
- if err != nil {
- return nil, _JwsVerifyDone, err
+ if err == nil {
+ return verified, peekJWSNestedState(ctx, payload), nil
}
- return verified, _JwsVerifyDone, nil
+ // VerifyCompactFast refuses crit-bearing messages. In v3
+ // jws.Verify defaults critValidation=false, so the generic
+ // fall-through path would still silently accept "crit".
+ // Force the strict path here: jwt.Parse must not be laxer
+ // than jws.Verify + WithCritValidation.
+ if errors.Is(err, jws.ErrCritPresent()) {
+ verifyOpts := append(ctx.verifyOpts, jws.WithCompact(), jws.WithCritValidation(true))
+ verified, err := jws.Verify(payload, verifyOpts...)
+ if err != nil {
+ return nil, _JwsVerifyDone, err
+ }
+ return verified, peekJWSNestedState(ctx, payload), nil
+ }
+ return nil, _JwsVerifyDone, err
}
}
verifyOpts := append(ctx.verifyOpts, jws.WithCompact())
verified, err := jws.Verify(payload, verifyOpts...)
- return verified, _JwsVerifyDone, err
+ if err != nil {
+ return nil, _JwsVerifyDone, err
+ }
+ return verified, peekJWSNestedState(ctx, payload), nil
+}
+
+// peekJWSNestedState returns _JwsVerifyExpectNested when pedantic mode is on
+// and the verified JWS protected header carries cty=JWT (RFC 7519 §5.2 — the
+// payload is itself a Nested JWT; the outer envelope expects another signed/
+// encrypted layer wrapping the JWT, not a raw JWT). Otherwise returns
+// _JwsVerifyDone. The signature has already been verified at this point, so
+// re-parsing the protected header is safe — it operates on bytes the producer
+// signed.
+func peekJWSNestedState(ctx *parseCtx, payload []byte) int {
+ if !ctx.pedantic {
+ return _JwsVerifyDone
+ }
+ msg, err := jws.Parse(payload, jws.WithCompact())
+ if err != nil || len(msg.Signatures()) == 0 {
+ return _JwsVerifyDone
+ }
+ hdr := msg.Signatures()[0].ProtectedHeaders()
+ if hdr == nil {
+ return _JwsVerifyDone
+ }
+ cty, ok := hdr.ContentType()
+ if !ok {
+ return _JwsVerifyDone
+ }
+ if cty == "JWT" {
+ return _JwsVerifyExpectNested
+ }
+ return _JwsVerifyDone
}
// verify parameter exists to make sure that we don't accidentally skip
@@ -399,8 +462,11 @@ OUTER:
}
}
- // No verification.
- m, err := jws.Parse(data, jws.WithCompact())
+ // No verification. Parse the LOOP-LOCAL `payload` (not the
+ // original `data`); for a 2-layer nested JWS, iter 2 must
+ // see the inner JWS bytes that iter 1 produced, not re-
+ // parse the outer envelope.
+ m, err := jws.Parse(payload, jws.WithCompact())
if err != nil {
return nil, fmt.Errorf(`invalid jws message: %w`, err)
}
@@ -415,12 +481,18 @@ OUTER:
ctx.token = New()
}
- if ctx.localReg != nil {
+ if ctx.localReg != nil || ctx.strictStringClaims != nil {
dcToken, ok := ctx.token.(TokenWithDecodeCtx)
if !ok {
- return nil, fmt.Errorf(`typed claim was requested, but the token (%T) does not support DecodeCtx`, ctx.token)
+ return nil, fmt.Errorf(`typed claim or strict string claims was requested, but the token (%T) does not support DecodeCtx`, ctx.token)
}
- dc := json.NewDecodeCtx(ctx.localReg)
+
+ var strict bool
+ if ctx.strictStringClaims != nil {
+ strict = *ctx.strictStringClaims
+ }
+
+ dc := json.NewDecodeCtxStrictStrings(ctx.localReg, strict)
dcToken.SetDecodeCtx(dc)
defer func() { dcToken.SetDecodeCtx(nil) }()
}
@@ -473,10 +545,30 @@ func Sign(t Token, options ...SignOption) ([]byte, error) {
return nil, fmt.Errorf(`jwt.Sign: invalid algorithm type %T. jwa.SignatureAlgorithm is required`, wk.alg)
}
+ // Reject algorithm names that would require JSON escaping
+ // in the protected header. Unlike kid (which may be attacker-
+ // influenced and silently falls through to jws.Sign), an
+ // unsafe alg is almost certainly a caller bug or an injection
+ // attempt, so we fail fast rather than emit any signature.
+ if !fastPathAlgSafe(alg.String()) {
+ return nil, fmt.Errorf(`jwt.Sign: algorithm %q contains bytes that require JSON escaping`, alg.String())
+ }
+
// Check if option contains anything other than alg/key
if len(wk.options) == 0 {
- // yay, we have something we can put in the FAST PATH!
- return signFast(t, alg, wk.key)
+ // If the key carries a kid that would require JSON escaping,
+ // skip the fast path (which concatenates kid raw into the
+ // protected header) and fall through to jws.Sign.
+ fastSafe := true
+ if jwkKey, ok := wk.key.(jwk.Key); ok {
+ if v, ok := jwkKey.KeyID(); ok && !fastPathKidSafe(v) {
+ fastSafe = false
+ }
+ }
+ if fastSafe {
+ // yay, we have something we can put in the FAST PATH!
+ return signFast(t, alg, wk.key)
+ }
}
// fallthrough
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go
index 4a7cfd3e5d..1e263f2e00 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.go
@@ -206,9 +206,6 @@ type withKeySet struct {
// and you do not mind the verification process having to possibly
// attempt using multiple times before succeeding to verify. See
// `jws.InferAlgorithmFromKey` option
-//
-// If you have only one key in the set, and are sure you want to
-// use that key, you can use the `jwt.WithDefaultKey` option.
func WithKeySet(set jwk.Set, options ...any) ParseOption {
return &parseOption{option.New(identKeySet{}, &withKeySet{
set: set,
@@ -242,7 +239,10 @@ func WithAudience(s string) ValidateOption {
return WithValidator(audienceClaimContainsString(s))
}
-// WithClaimValue specifies the expected value for a given claim
+// WithClaimValue specifies the expected value for a given claim.
+// The stored and expected values are compared with reflect.DeepEqual,
+// so slice-, map-, and struct-valued claims are supported in addition
+// to scalars. See [ClaimValueIs] for edge-case semantics.
func WithClaimValue(name string, v any) ValidateOption {
return WithValidator(ClaimValueIs(name, v))
}
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml
index bfcadfac25..fe03bd653e 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options.yaml
@@ -48,6 +48,14 @@ interfaces:
- name: ReadFileOption
comment: |
ReadFileOption is a type of `Option` that can be passed to `jws.ReadFile`
+ - name: GlobalParseOption
+ methods:
+ - globalOption
+ - parseOption
+ - readFileOption
+ comment: |
+ GlobalParseOption describes an Option that can be passed to `jwt.Settings()`,
+ `jwt.Parse()`, and `jwt.ReadFile()`.
- name: GlobalValidateOption
methods:
- globalOption
@@ -125,6 +133,18 @@ options:
See the documentation for `jwt.TokenOptionSet`, `(jwt.Token).Options`, and
`jwt.FlattenAudience` for more details
+ - ident: StrictStringClaims
+ interface: ParseOption
+ argument_type: bool
+ comment: |
+ WithStrictStringClaims controls whether JSON null values for string
+ claims (such as "iss", "sub", "jti") cause a parse error. By default,
+ null is silently accepted as an empty string (matching Go's standard
+ JSON decoding behavior). When set to true, null values are rejected
+ per the RFC type definitions (e.g. StringOrURI).
+
+ This option only affects JWT claims. JWK, JWE, and JWS fields are
+ not subject to this check and will always accept null as an empty string.
- ident: FormKey
interface: ParseOption
argument_type: string
@@ -271,4 +291,4 @@ options:
argument_type: jws.Base64Encoder
comment: |
WithBase64Encoder specifies the base64 encoder to use for signing
- tokens and verifying JWS signatures.
\ No newline at end of file
+ tokens and verifying JWS signatures.
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go
index 3a644a6e4c..7ee6dc3427 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/options_gen.go
@@ -39,6 +39,25 @@ type globalOption struct {
func (*globalOption) globalOption() {}
+// GlobalParseOption describes an Option that can be passed to `jwt.Settings()`,
+// `jwt.Parse()`, and `jwt.ReadFile()`.
+type GlobalParseOption interface {
+ Option
+ globalOption()
+ parseOption()
+ readFileOption()
+}
+
+type globalParseOption struct {
+ Option
+}
+
+func (*globalParseOption) globalOption() {}
+
+func (*globalParseOption) parseOption() {}
+
+func (*globalParseOption) readFileOption() {}
+
// GlobalValidateOption describes an Option that can be passed to `jwt.Settings()` and `jwt.Validate()`
type GlobalValidateOption interface {
Option
@@ -181,6 +200,7 @@ type identNumericDateParsePrecision struct{}
type identPedantic struct{}
type identResetValidators struct{}
type identSignOption struct{}
+type identStrictStringClaims struct{}
type identToken struct{}
type identTruncation struct{}
type identValidate struct{}
@@ -259,6 +279,10 @@ func (identSignOption) String() string {
return "WithSignOption"
}
+func (identStrictStringClaims) String() string {
+ return "WithStrictStringClaims"
+}
+
func (identToken) String() string {
return "WithToken"
}
@@ -432,6 +456,18 @@ func WithSignOption(v jws.SignOption) SignOption {
return &signOption{option.New(identSignOption{}, v)}
}
+// WithStrictStringClaims controls whether JSON null values for string
+// claims (such as "iss", "sub", "jti") cause a parse error. By default,
+// null is silently accepted as an empty string (matching Go's standard
+// JSON decoding behavior). When set to true, null values are rejected
+// per the RFC type definitions (e.g. StringOrURI).
+//
+// This option only affects JWT claims. JWK, JWE, and JWS fields are
+// not subject to this check and will always accept null as an empty string.
+func WithStrictStringClaims(v bool) ParseOption {
+ return &parseOption{option.New(identStrictStringClaims{}, v)}
+}
+
// WithToken specifies the token instance in which the resulting JWT is stored
// when parsing JWT tokens
func WithToken(v Token) ParseOption {
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go
index 2361ff5621..7a057742a9 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_gen.go
@@ -101,7 +101,7 @@ type Token interface {
Keys() []string
}
type stdToken struct {
- mu *sync.RWMutex
+ mu sync.RWMutex
dc DecodeCtx // per-object context for decoding
options TokenOptionSet // per-object option
audience types.StringList // https://tools.ietf.org/html/rfc7519#section-4.1.3
@@ -119,13 +119,14 @@ type stdToken struct {
// Convenience accessors are provided for these standard claims
func New() Token {
return &stdToken{
- mu: &sync.RWMutex{},
privateClaims: make(map[string]any),
options: DefaultOptionSet(),
}
}
func (t *stdToken) Options() *TokenOptionSet {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
return &t.options
}
@@ -440,11 +441,11 @@ LOOP:
}
t.issuedAt = &decoded
case IssuerKey:
- if err := json.AssignNextStringToken(&t.issuer, dec); err != nil {
+ if err := json.AssignNextStringToken(&t.issuer, dec, t.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, IssuerKey, err)
}
case JwtIDKey:
- if err := json.AssignNextStringToken(&t.jwtID, dec); err != nil {
+ if err := json.AssignNextStringToken(&t.jwtID, dec, t.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, JwtIDKey, err)
}
case NotBeforeKey:
@@ -454,7 +455,7 @@ LOOP:
}
t.notBefore = &decoded
case SubjectKey:
- if err := json.AssignNextStringToken(&t.subject, dec); err != nil {
+ if err := json.AssignNextStringToken(&t.subject, dec, t.dc); err != nil {
return fmt.Errorf(`failed to decode value for key %s: %w`, SubjectKey, err)
}
default:
@@ -548,6 +549,8 @@ func putClaimPairList(list []claimPair) {
func (t *stdToken) makePairs() ([]claimPair, error) {
pairs := getClaimPairList()
+ t.mu.RLock()
+ defer t.mu.RUnlock()
if t.audience != nil {
buf, err := json.MarshalAudience(t.audience, t.options.IsEnabled(FlattenAudience))
if err != nil {
@@ -612,7 +615,7 @@ func (t *stdToken) makePairs() ([]claimPair, error) {
return pairs, nil
}
-func (t stdToken) MarshalJSON() ([]byte, error) {
+func (t *stdToken) MarshalJSON() ([]byte, error) {
buf := pool.BytesBuffer().Get()
defer pool.BytesBuffer().Put(buf)
pairs, err := t.makePairs()
@@ -625,7 +628,10 @@ func (t stdToken) MarshalJSON() ([]byte, error) {
if i > 0 {
buf.WriteByte(tokens.Comma)
}
- fmt.Fprintf(buf, "%q: %s", pair.Name, pair.Value)
+ buf.WriteByte('"')
+ buf.WriteString(pair.Name)
+ buf.WriteString(`": `)
+ buf.Write(pair.Value.([]byte))
}
buf.WriteByte(tokens.CloseCurlyBracket)
ret := make([]byte, buf.Len())
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go
index 088c4263be..ef9799e6d4 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/token_options.go
@@ -1,12 +1,11 @@
package jwt
-import "sync"
+import "sync/atomic"
// TokenOptionSet is a bit flag containing per-token options.
type TokenOptionSet uint64
-var defaultOptions TokenOptionSet
-var defaultOptionsMu sync.RWMutex
+var defaultOptions atomic.Uint64
// TokenOption describes a single token option that can be set on
// the per-token option set (TokenOptionSet)
@@ -45,7 +44,7 @@ func (o TokenOptionSet) Value() uint64 {
// option set. This may differ depending on if/when functions that
// change the global state has been called, such as `jwt.Settings`
func DefaultOptionSet() TokenOptionSet {
- return TokenOptionSet(defaultOptions.Value())
+ return TokenOptionSet(defaultOptions.Load())
}
// Clear sets all bits to zero, effectively disabling all options
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go b/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go
index af46868d8b..f398c83bfe 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwt/validate.go
@@ -3,6 +3,7 @@ package jwt
import (
"context"
"fmt"
+ "reflect"
"slices"
"strconv"
"time"
@@ -24,7 +25,7 @@ func isSupportedTimeClaim(c string) error {
case ExpirationKey, IssuedAtKey, NotBeforeKey:
return nil
}
- return fmt.Errorf(`unsupported time claim %s`, strconv.Quote(c))
+ return jwterrs.ValidateErrorf(`unsupported time claim %s in jwt.WithMaxDelta/jwt.WithMinDelta`, strconv.Quote(c))
}
func timeClaim(t Token, clock Clock, c string) time.Time {
@@ -51,6 +52,10 @@ func timeClaim(t Token, clock Clock, c string) time.Time {
// See the various `WithXXX` functions for optional parameters
// that can control the behavior of this method.
func Validate(t Token, options ...ValidateOption) error {
+ if t == nil {
+ return jwterrs.ValidateErrorf(`jwt.Validate: token is nil`)
+ }
+
ctx := context.Background()
trunc := getDefaultTruncation()
@@ -73,6 +78,9 @@ func Validate(t Token, options ...ValidateOption) error {
if err := o.Value(&skew); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithAcceptableSkew() option must be time.Duration: %w`, err)
}
+ if skew < 0 {
+ return fmt.Errorf(`jwt.Validate: WithAcceptableSkew() must not be negative`)
+ }
case identTruncation{}:
if err := o.Value(&trunc); err != nil {
return fmt.Errorf(`jwt.Validate: value for WithTruncation() option must be time.Duration: %w`, err)
@@ -162,10 +170,21 @@ func MinDeltaIs(c1, c2 string, dur time.Duration) Validator {
func (iitr *isInTimeRange) Validate(ctx context.Context, t Token) error {
clock := ValidationCtxClock(ctx) // MUST be populated
skew := ValidationCtxSkew(ctx) // MUST be populated
- // We don't check if the claims already exist, because we already did that
- // by piggybacking on `required` check.
t1 := timeClaim(t, clock, iitr.c1)
t2 := timeClaim(t, clock, iitr.c2)
+ // Defensive: reject zero-value claims before computing delta. The
+ // auto-IsRequired piggyback in WithValidator type-switches on the
+ // concrete *isInTimeRange — wrapping this validator (e.g., in
+ // ValidatorFunc) skips that piggyback, and a missing time claim
+ // would silently produce a hugely-negative delta that trivially
+ // satisfies the upper-bound check. Reject the missing claim
+ // regardless of how the validator was wrapped.
+ if t1.IsZero() {
+ return jwterrs.MissingRequiredClaimErrorf(iitr.c1)
+ }
+ if t2.IsZero() {
+ return jwterrs.MissingRequiredClaimErrorf(iitr.c2)
+ }
if iitr.less { // t1 - t2 <= iitr.dur
// t1 - t2 < iitr.dur + skew
if t1.Sub(t2) > iitr.dur+skew {
@@ -210,21 +229,36 @@ func SetValidationCtxSkew(ctx context.Context, dur time.Duration) context.Contex
}
// ValidationCtxClock returns the Clock object associated with
-// the current validation context. This value will always be available
-// during validation of tokens.
+// the current validation context. When called from within a Validator
+// invoked by [Validate], the value is always populated. If the context
+// was not initialized by [Validate] (for example, a custom validator
+// was invoked with a bare context), a default clock backed by
+// [time.Now] is returned instead of panicking.
func ValidationCtxClock(ctx context.Context) Clock {
- //nolint:forcetypeassert
- return ctx.Value(identValidationCtxClock{}).(Clock)
+ if cl, ok := ctx.Value(identValidationCtxClock{}).(Clock); ok {
+ return cl
+ }
+ return ClockFunc(time.Now)
}
+// ValidationCtxSkew returns the clock skew associated with the current
+// validation context. If the context was not initialized by [Validate],
+// zero is returned instead of panicking.
func ValidationCtxSkew(ctx context.Context) time.Duration {
- //nolint:forcetypeassert
- return ctx.Value(identValidationCtxSkew{}).(time.Duration)
+ if dur, ok := ctx.Value(identValidationCtxSkew{}).(time.Duration); ok {
+ return dur
+ }
+ return 0
}
+// ValidationCtxTruncation returns the truncation granularity associated
+// with the current validation context. If the context was not initialized
+// by [Validate], zero is returned instead of panicking.
func ValidationCtxTruncation(ctx context.Context) time.Duration {
- //nolint:forcetypeassert
- return ctx.Value(identValidationCtxTruncation{}).(time.Duration)
+ if dur, ok := ctx.Value(identValidationCtxTruncation{}).(time.Duration); ok {
+ return dur
+ }
+ return 0
}
// IsExpirationValid is one of the default validators that will be executed.
@@ -368,9 +402,11 @@ type claimValueIs struct {
}
// ClaimValueIs creates a Validator that checks if the value of claim `name`
-// matches `value`. The comparison is done using a simple `==` comparison,
-// and therefore complex comparisons may fail using this code. If you
-// need to do more, use a custom Validator.
+// matches `value`. The comparison is done with reflect.DeepEqual, so
+// slice-, map-, and struct-valued claims are supported in addition to
+// scalars. Function-valued claims follow reflect.DeepEqual semantics
+// (equal only when both sides are nil). If you need finer-grained
+// matching than DeepEqual provides, use a custom Validator.
func ClaimValueIs(name string, value any) Validator {
return &claimValueIs{
name: name,
@@ -384,7 +420,7 @@ func (cv *claimValueIs) Validate(_ context.Context, t Token) error {
if err := t.Get(cv.name, &v); err != nil {
return cv.makeErr(`claim %[1]q does not exist or is not a []string: %[2]w`, cv.name, err)
}
- if v != cv.value {
+ if !reflect.DeepEqual(v, cv.value) {
return cv.makeErr(`claim %[1]q does not have the expected value`, cv.name)
}
return nil
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/jwx.go b/vendor/github.com/lestrrat-go/jwx/v3/jwx.go
index fc394e5137..441518fd56 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/jwx.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/jwx.go
@@ -28,6 +28,12 @@ import (
// DecoderSettings gives you a access to configure the "encoding/json".Decoder
// used to decode JSON objects within the jwx framework.
+//
+// All options accepted here have process-global effect and are intended
+// to be applied exactly once at program startup, before any goroutine
+// begins parsing JWx payloads. See the godoc on individual JSONOption
+// constructors (e.g. [WithUseNumber]) for the concurrency contract of
+// each setting.
func DecoderSettings(options ...JSONOption) {
// XXX We're using this format instead of just passing a single boolean
// in case a new option is to be added some time later
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/options.go b/vendor/github.com/lestrrat-go/jwx/v3/options.go
index b642a199d8..838a1f93e7 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/options.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/options.go
@@ -24,6 +24,15 @@ func newJSONOption(n any, v any) JSONOption {
// WithUseNumber controls whether the jwx package should unmarshal
// JSON objects with the "encoding/json".Decoder.UseNumber feature on.
//
+// This setting has process-global effect and must be applied once
+// at program startup (typically from func init() or early in main())
+// before any goroutine begins parsing JWx payloads. The underlying
+// flag is read atomically, so toggling it at runtime is race-free,
+// but any in-flight or subsequent decoders will observe a mix of
+// float64 and json.Number values in concurrently-decoded custom
+// fields — callers that type-assert on those values will break
+// non-deterministically. There is no per-call override.
+//
// Default is false.
func WithUseNumber(b bool) JSONOption {
return newJSONOption(identUseNumber{}, b)
diff --git a/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go b/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go
index 4eb80cb99f..392d22241b 100644
--- a/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go
+++ b/vendor/github.com/lestrrat-go/jwx/v3/transform/map.go
@@ -23,6 +23,11 @@ type Mappable interface {
// Many objects in jwe, jwk, jws, and jwt packages including
// `jwt.Token`, `jwk.Key`, `jws.Header`, etc.
//
+// The values stored in `dst` are the exact values returned by `m.Get()`.
+// Mutable values such as slices, maps, and pointers may therefore be live
+// aliases to the original object's backing storage. AsMap is not a deep-copy
+// or snapshot helper.
+//
// EXPERIMENTAL: This API is experimental and its interface and behavior is
// subject to change in future releases. This API is not subject to semver
// compatibility guarantees.
diff --git a/vendor/github.com/nats-io/jwt/v2/account_claims.go b/vendor/github.com/nats-io/jwt/v2/account_claims.go
index d143bee5d7..317250623c 100644
--- a/vendor/github.com/nats-io/jwt/v2/account_claims.go
+++ b/vendor/github.com/nats-io/jwt/v2/account_claims.go
@@ -347,9 +347,11 @@ func NewAccountClaims(subject string) *AccountClaims {
c.SigningKeys = make(SigningKeys)
// Set to unlimited to start. We do it this way so we get compiler
// errors if we add to the OperatorLimits.
+ // JetStream is disabled by default by setting MemoryStorage and DiskStorage to zero, instead of NoLimit.
c.Limits = OperatorLimits{
NatsLimits{NoLimit, NoLimit, NoLimit},
AccountLimits{NoLimit, NoLimit, true, false, NoLimit, NoLimit},
+ // Default zeros implies that JetStream is not enabled by default, see OperatorLimits.IsJSEnabled().
JetStreamLimits{0, 0, 0, 0, 0, 0, 0, false},
JetStreamTieredLimits{},
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/accounts.go b/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
index 225f92e726..37db04506a 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/accounts.go
@@ -66,7 +66,7 @@ type Account struct {
claimJWT string
updated time.Time
mu sync.RWMutex
- sqmu sync.Mutex
+ smu sync.Mutex // serializes route interest updates
sl *Sublist
ic *client
sq *sendq
@@ -80,7 +80,7 @@ type Account struct {
nrleafs int32
clients map[*client]struct{}
rm map[string]int32
- lqws map[string]int32
+ lws map[string]int32 // per key, last rm[key] sent to routes; used to dedup sends
usersRevoked map[string]int64
mappings []*mapping
hasMapped atomic.Bool
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/auth.go b/vendor/github.com/nats-io/nats-server/v2/server/auth.go
index 09fad33460..f4c9fa2d46 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/auth.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/auth.go
@@ -56,6 +56,8 @@ type ClientAuthentication interface {
GetNonce() []byte
// Kind indicates what type of connection this is matching defined constants like CLIENT, ROUTER, GATEWAY, LEAF etc
Kind() int
+ //Gets the ID associated with a client
+ GetID() uint64
}
// NkeyUser is for multiple nkey based users
@@ -1283,7 +1285,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
hasEmailAddresses := len(cert.EmailAddresses) > 0
hasSubject := len(cert.Subject.String()) > 0
hasURIs := len(cert.URIs) > 0
- if !hasEmailAddresses && !hasSubject && !hasURIs {
+ if !hasSANs && !hasEmailAddresses && !hasSubject && !hasURIs {
c.Debugf("User required in cert, none found")
return false
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/client.go b/vendor/github.com/nats-io/nats-server/v2/server/client.go
index 162c235007..61a6dc0338 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/client.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/client.go
@@ -30,6 +30,7 @@ import (
"net/url"
"regexp"
"runtime"
+ "slices"
"strconv"
"strings"
"sync"
@@ -270,7 +271,7 @@ type client struct {
mpay int32
msubs int32
mcl int32
- mu sync.Mutex
+ mu sync.RWMutex
cid uint64
start time.Time
nonce []byte
@@ -560,6 +561,13 @@ func (c *client) GetNonce() []byte {
return c.nonce
}
+// GetID returns the client ID
+func (c *client) GetID() uint64 {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.cid
+}
+
// GetName returns the application supplied name for the connection.
func (c *client) GetName() string {
c.mu.Lock()
@@ -998,6 +1006,7 @@ func (c *client) RegisterUser(user *User) {
// Reset perms to nil in case client previously had them.
c.perms = nil
c.mperms = nil
+ c.darray = nil
} else {
c.setPermissions(user.Permissions)
}
@@ -1035,6 +1044,7 @@ func (c *client) RegisterNkeyUser(user *NkeyUser) error {
// Reset perms to nil in case client previously had them.
c.perms = nil
c.mperms = nil
+ c.darray = nil
} else {
c.setPermissions(user.Permissions)
}
@@ -1061,6 +1071,8 @@ func (c *client) setPermissions(perms *Permissions) {
return
}
c.perms = &permissions{}
+ c.mperms = nil
+ c.darray = nil
slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
// Loop over publish permissions
@@ -1092,7 +1104,7 @@ func (c *client) setPermissions(perms *Permissions) {
if perms.Subscribe != nil {
var err error
if len(perms.Subscribe.Allow) > 0 {
- c.perms.sub.allow = NewSublist(slcache)
+ c.perms.sub.allow = NewSublistNoCache()
}
for _, subSubject := range perms.Subscribe.Allow {
sub := &subscription{}
@@ -1104,7 +1116,7 @@ func (c *client) setPermissions(perms *Permissions) {
c.perms.sub.allow.Insert(sub)
}
if len(perms.Subscribe.Deny) > 0 {
- c.perms.sub.deny = NewSublist(slcache)
+ c.perms.sub.deny = NewSublistNoCache()
// Also hold onto this array for later.
c.darray = perms.Subscribe.Deny
}
@@ -1201,38 +1213,40 @@ func (c *client) mergeDenyPermissions(what denyType, denyPubs []string) {
if c.perms == nil {
c.perms = &permissions{}
}
- slcache := c.srv != nil && !c.srv.getOpts().NoSublistCache
- var perms []*perm
- switch what {
- case pub:
- perms = []*perm{&c.perms.pub}
- case sub:
- perms = []*perm{&c.perms.sub}
- case both:
- perms = []*perm{&c.perms.pub, &c.perms.sub}
- }
- for _, p := range perms {
- if p.deny == nil {
- p.deny = NewSublist(slcache)
+ if what == pub || what == both {
+ if c.perms.pub.deny == nil {
+ c.perms.pub.deny = NewSublistForServer(c.srv)
}
- FOR_DENY:
- for _, subj := range denyPubs {
- r := p.deny.Match(subj)
- for _, v := range r.qsubs {
- for _, s := range v {
- if string(s.subject) == subj {
- continue FOR_DENY
- }
- }
- }
- for _, s := range r.psubs {
+ mergeDenyPerm(&c.perms.pub, denyPubs)
+ }
+ if what == sub || what == both {
+ if c.perms.sub.deny == nil {
+ // Avoid sublist cache contention in canSubscribe.
+ c.perms.sub.deny = NewSublistNoCache()
+ }
+ mergeDenyPerm(&c.perms.sub, denyPubs)
+ }
+}
+
+// mergeDenyPerm inserts new deny permissions, skipping subjects that already exist.
+func mergeDenyPerm(p *perm, denyPubs []string) {
+FOR_DENY:
+ for _, subj := range denyPubs {
+ r := p.deny.Match(subj)
+ for _, v := range r.qsubs {
+ for _, s := range v {
if string(s.subject) == subj {
continue FOR_DENY
}
}
- sub := &subscription{subject: []byte(subj)}
- p.deny.Insert(sub)
}
+ for _, s := range r.psubs {
+ if string(s.subject) == subj {
+ continue FOR_DENY
+ }
+ }
+ sub := &subscription{subject: []byte(subj)}
+ p.deny.Insert(sub)
}
}
@@ -1535,6 +1549,11 @@ func (c *client) readLoop(pre []byte) {
acc.stats.Unlock()
}
+ if c.kind == CLIENT {
+ atomic.AddInt64(&s.inClientMsgs, inMsgs)
+ atomic.AddInt64(&s.inClientBytes, inBytes)
+ }
+
atomic.AddInt64(&s.inMsgs, inMsgs)
atomic.AddInt64(&s.inBytes, inBytes)
}
@@ -2684,6 +2703,12 @@ func (c *client) processPing() {
srv.mu.Lock()
info := srv.copyInfo()
c.mu.Lock()
+ // Keep the in-process tls_required override from the initial INFO,
+ // otherwise this async INFO would flip it back to true.
+ if c.iproc && info.TLSRequired && !c.flags.isSet(didTLSFirst) {
+ info.TLSRequired = false
+ info.TLSAvailable = true
+ }
info.RemoteAccount = c.acc.Name
info.IsSystemAccount = c.acc == srv.SystemAccount()
info.ConnectInfo = true
@@ -3240,9 +3265,9 @@ func (c *client) addShadowSub(sub *subscription, ime *ime) (*subscription, error
return &nsub, nil
}
-// canSubscribe determines if the client is authorized to subscribe to the
-// given subject. Assumes caller is holding lock.
-func (c *client) canSubscribe(subject string, optQueue ...string) bool {
+// canSubscribeInternal determines if the client is authorized to subscribe to
+// the given subject. Assumes caller is holding at least a read lock.
+func (c *client) canSubscribeInternal(subject string, optQueue ...string) bool {
if c.perms == nil {
return true
}
@@ -3287,23 +3312,32 @@ func (c *client) canSubscribe(subject string, optQueue ...string) bool {
// If the queue appears in the deny list, then DO NOT allow.
allowed = !queueMatches(queue, r.qsubs)
}
+ }
+ return allowed
+}
- // We use the actual subscription to signal us to spin up the deny mperms
- // and cache. We check if the subject is a wildcard that intersects any of
- // the deny clauses.
- // FIXME(dlc) - We could be smarter and track when these go away and remove.
- if allowed && c.mperms == nil && subjectHasWildcard(subject) {
- // Whip through the deny array and check if this wildcard subject can
- // overlap with any denied deliveries.
- for _, sub := range c.darray {
- if SubjectsCollide(sub, subject) {
- c.loadMsgDenyFilter()
- break
- }
+// canSubscribe determines if the client is authorized to subscribe to the
+// given subject and initializes the delivery-time deny filter when needed.
+// Assumes caller is holding the write lock.
+func (c *client) canSubscribe(subject string, optQueue ...string) bool {
+ if !c.canSubscribeInternal(subject, optQueue...) {
+ return false
+ }
+ // We use the actual subscription to signal us to spin up the deny mperms
+ // and cache. We check if the subject is a wildcard that intersects any of
+ // the deny clauses.
+ // FIXME(dlc) - We could be smarter and track when these go away and remove.
+ if c.mperms == nil && subjectHasWildcard(subject) {
+ // Whip through the deny array and check if this wildcard subject can
+ // overlap with any denied deliveries.
+ for _, sub := range c.darray {
+ if SubjectsCollide(sub, subject) {
+ c.loadMsgDenyFilter()
+ break
}
}
}
- return allowed
+ return true
}
func queueMatches(queue string, qsubs [][]*subscription) bool {
@@ -4419,7 +4453,7 @@ func (c *client) processInboundClientMsg(msg []byte) (bool, bool) {
if c.srv.gateway.enabled {
reply := c.pa.reply
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
- reply = append(reply, '@')
+ reply = append(slices.Clip(reply), '@')
reply = append(reply, c.pa.deliver...)
}
didDeliver = c.sendMsgToGateways(acc, msg, c.pa.subject, reply, qnames, false) || didDeliver
@@ -4467,7 +4501,7 @@ func (c *client) handleGWReplyMap(msg []byte) bool {
if c.srv.gateway.enabled {
reply := c.pa.reply
if len(c.pa.deliver) > 0 && c.kind == JETSTREAM && len(reply) > 0 && !replyHasJSAckSuffix(reply) {
- reply = append(reply, '@')
+ reply = append(slices.Clip(reply), '@')
reply = append(reply, c.pa.deliver...)
}
c.sendMsgToGateways(c.acc, msg, c.pa.subject, reply, nil, false)
@@ -4540,7 +4574,8 @@ func removeHeaderIfPrefixPresent(hdr []byte, prefix string) []byte {
}
index += start
if index < 1 || hdr[index-1] != '\n' {
- return hdr
+ index += len(prefix)
+ continue
}
end := bytes.Index(hdr[index+len(prefix):], []byte(_CRLF_))
@@ -5119,6 +5154,7 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
var dlvExtraSize int64
var dlvRouteMsgs int64
var dlvLeafMsgs int64
+ var dlvClientMsgs int64
// We need to know if this is a MQTT producer because they send messages
// without CR_LF (we otherwise remove the size of CR_LF from message size).
@@ -5132,12 +5168,15 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
totalBytes := dlvMsgs*int64(len(msg)) + dlvExtraSize
routeBytes := dlvRouteMsgs*int64(len(msg)) + dlvExtraSize
leafBytes := dlvLeafMsgs*int64(len(msg)) + dlvExtraSize
+ // dlvExtraSize applies to route/leaf header overhead, not client deliveries
+ clientBytes := dlvClientMsgs * int64(len(msg))
// For non MQTT producers, remove the CR_LF * number of messages
if !prodIsMQTT {
totalBytes -= dlvMsgs * int64(LEN_CR_LF)
routeBytes -= dlvRouteMsgs * int64(LEN_CR_LF)
leafBytes -= dlvLeafMsgs * int64(LEN_CR_LF)
+ clientBytes -= dlvClientMsgs * int64(LEN_CR_LF)
}
if acc != nil {
@@ -5158,6 +5197,9 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
if srv := c.srv; srv != nil {
atomic.AddInt64(&srv.outMsgs, dlvMsgs)
atomic.AddInt64(&srv.outBytes, totalBytes)
+
+ atomic.AddInt64(&srv.outClientMsgs, dlvClientMsgs)
+ atomic.AddInt64(&srv.outClientBytes, clientBytes)
}
}
@@ -5253,6 +5295,9 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
// We don't count internal deliveries, so do only when sub.icb is nil.
if sub.icb == nil {
dlvMsgs++
+ if sub.client.kind == CLIENT {
+ dlvClientMsgs++
+ }
}
didDeliver = true
}
@@ -5480,6 +5525,8 @@ func (c *client) processMsgResults(acc *Account, r *SublistResult, msg, deliver,
dlvRouteMsgs++
case LEAF:
dlvLeafMsgs++
+ case CLIENT:
+ dlvClientMsgs++
}
}
// Do the rest even when message delivery was skipped.
@@ -5518,7 +5565,7 @@ sendToRoutesOrLeafs:
// already performed, otherwise we'd end up with a duplicate '@' suffix
// resulting in a protocol error.
if len(deliver) > 0 && len(reply) > 0 && !remapped && !replyHasJSAckSuffix(reply) {
- reply = append(reply, '@')
+ reply = append(slices.Clip(reply), '@')
reply = append(reply, deliver...)
}
@@ -6538,10 +6585,20 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
if len(subjs) > 0 {
detail = fmt.Sprintf(" (%s)", strings.Join(subjs, "; "))
}
- if kind == CLIENT {
- c.Errorf("TLS handshake error: %v%s", err, detail)
- } else {
+ if kind == ROUTER || kind == GATEWAY {
+ // Always surface these as errors, as these ports shouldn't be behind a load
+ // balancer or regularly probed.
c.Errorf("TLS %s handshake error: %v%s", typ, err, detail)
+ } else {
+ logf := c.Errorf
+ if isClientProbeTLSHandshakeError(err) {
+ logf = c.Debugf
+ }
+ if kind == CLIENT {
+ logf("TLS handshake error: %v%s", err, detail)
+ } else {
+ logf("TLS %s handshake error: %v%s", typ, err, detail)
+ }
}
c.closeConnection(TLSHandshakeError)
@@ -6571,6 +6628,17 @@ func (c *client) doTLSHandshake(typ string, solicit bool, url *url.URL, tlsConfi
return false, err
}
+func isClientProbeTLSHandshakeError(err error) bool {
+ var netErr net.Error
+ if errors.As(err, &netErr) && netErr.Timeout() {
+ return true
+ }
+ var recordHeaderErr tls.RecordHeaderError
+ // Conn is only set by crypto/tls when the invalid record was the peer's
+ // initial handshake bytes, which is the non-TLS probe/load-balancer case.
+ return errors.As(err, &recordHeaderErr) && recordHeaderErr.Conn != nil
+}
+
// getRawAuthUserLock returns the raw auth user for the client.
// Will acquire the client lock.
func (c *client) getRawAuthUserLock() string {
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/const.go b/vendor/github.com/nats-io/nats-server/v2/server/const.go
index b173aa2a78..1eeeee2288 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/const.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/const.go
@@ -66,7 +66,7 @@ func init() {
const (
// VERSION is the current version for the server.
- VERSION = "2.14.0"
+ VERSION = "2.14.2"
// PROTO is the currently supported protocol.
// 0 was the original
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
index dba9517413..0a6f506a67 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/consumer.go
@@ -511,6 +511,7 @@ type consumer struct {
retention RetentionPolicy
monitorWg sync.WaitGroup
+ monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic.
inMonitor bool
// R>1 proposals
@@ -1140,7 +1141,10 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
mset.mu.Unlock()
return nil, NewJSConsumerWQRequiresExplicitAckError()
}
-
+ if config.DeliverPolicy != DeliverAll {
+ mset.mu.Unlock()
+ return nil, NewJSConsumerWQConsumerNotDeliverAllError()
+ }
if mset.numLimitableConsumers() > 0 {
subjects := gatherSubjectFilters(config.FilterSubject, config.FilterSubjects)
if len(subjects) == 0 {
@@ -1168,10 +1172,6 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
}
}
}
- if config.DeliverPolicy != DeliverAll {
- mset.mu.Unlock()
- return nil, NewJSConsumerWQConsumerNotDeliverAllError()
- }
}
// Set name, which will be durable name if set, otherwise we create one at random.
@@ -1336,6 +1336,8 @@ func (mset *stream) addConsumerWithAssignment(config *ConsumerConfig, oname stri
// Clustered non-direct consumers defer this to setLeader so the
// expensive store scans don't block the meta apply goroutine.
if err := o.selectStartingSeqNo(); err != nil {
+ mset.mu.Unlock()
+ o.deleteWithoutAdvisory()
return nil, err
}
}
@@ -1832,11 +1834,14 @@ func (o *consumer) setLeader(isLeader bool) error {
stopAndClearTimer(&o.uptmr)
// Make sure to clear out any re-deliver queues
o.stopAndClearPtmr()
+ o.rdc = nil
o.rdq = nil
o.rdqi.Empty()
o.pending = nil
o.rsm = nil
o.resetPendingDeliveries()
+ // Reset num pending, these are only authoritative on the leader.
+ o.npc, o.npf = 0, 0
// ok if they are nil, we protect inside unsubscribe()
o.unsubscribe(o.ackSubOld)
o.unsubscribe(o.ackSub)
@@ -2138,7 +2143,7 @@ func (o *consumer) deleteNotActive() {
cnaStart := consumerNotActiveStartInterval
o.mu.Lock()
- if o.mset == nil {
+ if o.mset == nil || !o.isLeader() {
o.mu.Unlock()
return
}
@@ -2213,6 +2218,8 @@ func (o *consumer) deleteNotActive() {
s, js := o.mset.srv, o.srv.js.Load()
acc, stream, name, isDirect := o.acc.Name, o.stream, o.name, o.cfg.Direct
+ // Capture our own view of the assignment while we still hold the lock.
+ ca := o.ca
var qch, cqch chan struct{}
if o.srv != nil {
qch = o.srv.quitCh
@@ -2230,9 +2237,6 @@ func (o *consumer) deleteNotActive() {
"consumer": name,
})
- // We will delete locally regardless.
- defer o.delete()
-
// If we are clustered, check if we still have this consumer assigned.
// If we do forward a proposal to delete ourselves to the metacontroller leader.
if !isDirect && s.JetStreamIsClustered() {
@@ -2241,8 +2245,11 @@ func (o *consumer) deleteNotActive() {
meta RaftNode
removeEntry []byte
)
- ca, cc := js.consumerAssignment(acc, stream, name), js.cluster
- if ca != nil && cc != nil {
+ nca := js.consumerAssignment(acc, stream, name)
+ // Only propose the delete if the meta-layer assignment still refers to
+ // the consumer we captured, otherwise we'd be racing a recreated
+ // consumer with the same name.
+ if cc := js.cluster; cc != nil && ca != nil && ca.sameIdentity(nca) {
meta = cc.meta
cca := ca.clone()
cca.Reply = _EMPTY_
@@ -2251,7 +2258,7 @@ func (o *consumer) deleteNotActive() {
}
js.mu.RUnlock()
- if ca != nil && cc != nil {
+ if ca != nil && meta != nil {
// Check to make sure we went away.
// Don't think this needs to be a monitored go routine.
jitter := time.Duration(rand.Int63n(int64(cnaStart)))
@@ -2274,10 +2281,11 @@ func (o *consumer) deleteNotActive() {
js.mu.RUnlock()
return
}
- nca := js.consumerAssignment(acc, stream, name)
- js.mu.RUnlock()
+ nca = js.consumerAssignment(acc, stream, name)
// Make sure this is the same consumer assignment, and not a new consumer with the same name.
- if nca != nil && reflect.DeepEqual(nca, ca) {
+ match := ca.sameIdentity(nca)
+ js.mu.RUnlock()
+ if match {
s.Warnf("Consumer assignment for '%s > %s > %s' not cleaned up, retrying", acc, stream, name)
meta.ForwardProposal(removeEntry)
if interval < cnaMax {
@@ -2290,6 +2298,10 @@ func (o *consumer) deleteNotActive() {
return
}
}
+ } else {
+ // Otherwise, we can delete locally. Either a consumer that's not tracked
+ // by the meta layer (direct), or a standalone non-clustered server.
+ o.delete()
}
}
@@ -2341,7 +2353,9 @@ func (o *consumer) hasMaxDeliveries(seq uint64) bool {
// Make sure to remove from pending.
if p, ok := o.pending[seq]; ok && p != nil {
delete(o.pending, seq)
- o.updateDelivered(p.Sequence, seq, dc, p.Timestamp)
+ // Increment by one, since the delivery count hasn't been increased above.
+ o.updateDelivered(p.Sequence, seq, dc+1, p.Timestamp)
+ o.moveAckFloor(p.Sequence, seq)
}
// Ensure redelivered state is set, if not already.
if o.rdc == nil {
@@ -3245,14 +3259,42 @@ func (o *consumer) ackWait(next time.Duration) time.Duration {
return o.cfg.AckWait + ackWaitDelay
}
-// Due to bug in calculation of sequences on restoring redelivered let's do quick sanity check.
-// Lock should be held.
-func (o *consumer) checkRedelivered() {
- var shouldUpdateState bool
+func (o *consumer) removeRedeliveredBelow(seq uint64) {
+ if seq == 0 {
+ return
+ }
+ o.mu.Lock()
for sseq := range o.rdc {
- if sseq <= o.asflr {
+ if sseq < seq {
delete(o.rdc, sseq)
o.removeFromRedeliverQueue(sseq)
+ }
+ }
+ o.mu.Unlock()
+
+ if o.store != nil {
+ o.store.RemoveRedeliveredBelow(seq)
+ }
+}
+
+// checkRedelivered drops rdq entries at/below asflr or below stream's first sequence.
+// But rdc is kept until the message leaves the stream: needAck relies on rdc to mark
+// messages past MaxDeliver.
+// Lock should be held.
+func (o *consumer) checkRedelivered() {
+ if o.mset == nil {
+ return
+ }
+ var ss StreamState
+ o.mset.store.FastState(&ss)
+
+ var shouldUpdateState bool
+ for sseq := range o.rdc {
+ if sseq <= o.asflr || sseq < ss.FirstSeq {
+ o.removeFromRedeliverQueue(sseq)
+ }
+ if sseq < ss.FirstSeq {
+ delete(o.rdc, sseq)
shouldUpdateState = true
}
}
@@ -3641,22 +3683,7 @@ func (o *consumer) processAckMsgLocked(sseq, dseq, dc uint64, reply string, doSa
delete(o.pending, sseq)
// Use the original deliver sequence from our pending record.
dseq = p.Sequence
-
- // Only move floors if we matched an existing pending.
- if len(o.pending) == 0 {
- o.adflr = o.dseq - 1
- o.asflr = o.sseq - 1
- } else if dseq == o.adflr+1 {
- o.adflr, o.asflr = dseq, sseq
- for ss := sseq + 1; ss < o.sseq; ss++ {
- if p, ok := o.pending[ss]; ok {
- if p.Sequence > 0 {
- o.adflr, o.asflr = p.Sequence-1, ss-1
- }
- break
- }
- }
- }
+ o.moveAckFloor(dseq, sseq)
}
delete(o.rdc, sseq)
o.removeFromRedeliverQueue(sseq)
@@ -3727,6 +3754,25 @@ func (o *consumer) processAckMsgLocked(sseq, dseq, dc uint64, reply string, doSa
return ackInPlace
}
+// Lock should be held.
+func (o *consumer) moveAckFloor(dseq, sseq uint64) {
+ // Only move floors if we matched an existing pending.
+ if len(o.pending) == 0 {
+ o.adflr = o.dseq - 1
+ o.asflr = o.sseq - 1
+ } else if dseq == o.adflr+1 {
+ o.adflr, o.asflr = dseq, sseq
+ for ss := sseq + 1; ss < o.sseq; ss++ {
+ if p, ok := o.pending[ss]; ok {
+ if p.Sequence > 0 {
+ o.adflr, o.asflr = p.Sequence-1, ss-1
+ }
+ break
+ }
+ }
+ }
+}
+
// Determine if this is a truly filtered consumer. Modern clients will place filtered subjects
// even if the stream only has a single non-wildcard subject designation.
// Read lock should be held.
@@ -4763,7 +4809,9 @@ func (o *consumer) getNextMsg() (*jsPubMsg, uint64, error) {
// Make sure to remove from pending.
if p, ok := o.pending[seq]; ok && p != nil {
delete(o.pending, seq)
+ // The delivery count has already been incremented once.
o.updateDelivered(p.Sequence, seq, dc, p.Timestamp)
+ o.moveAckFloor(p.Sequence, seq)
}
continue
}
@@ -5507,11 +5555,11 @@ func (o *consumer) streamNumPendingLocked() (uint64, error) {
return o.streamNumPending()
}
-// Will force a set from the stream store of num pending.
+// Will force a set from the stream store of num pending on the consumer leader.
// Depends on delivery policy, for last per subject we calculate differently.
// Lock should be held.
func (o *consumer) streamNumPending() (uint64, error) {
- if o.mset == nil || o.mset.store == nil {
+ if o.mset == nil || o.mset.store == nil || !o.isLeader() {
o.npc, o.npf = 0, 0
return 0, nil
}
@@ -6252,7 +6300,10 @@ func (o *consumer) selectStartingSeqNo() error {
o.asflr = o.sseq - 1
// Set our starting sequence state.
// But only if we're not clustered, if clustered we propose upon becoming leader.
- if o.store != nil && o.sseq > 0 && o.cfg.replicas(&o.mset.cfg) == 1 {
+ o.mset.cfgMu.RLock()
+ isR1 := o.cfg.replicas(&o.mset.cfg) == 1
+ o.mset.cfgMu.RUnlock()
+ if o.store != nil && o.sseq > 0 && isR1 {
if err := o.store.SetStarting(o.sseq - 1); err != nil {
return err
}
@@ -6405,9 +6456,9 @@ func (o *consumer) purge(sseq uint64, slseq uint64, isWider bool) {
}
delete(o.pending, seq)
delete(o.rdc, seq)
+ o.updateAcks(p.Sequence, seq, _EMPTY_)
// rdq handled below.
- }
- if isWider && store != nil {
+ } else if isWider && store != nil {
// Our filtered subject, which could be all, is wider than the underlying purge.
// We need to check if the pending items left are still valid.
var smv StoreMsg
@@ -6420,6 +6471,7 @@ func (o *consumer) purge(sseq uint64, slseq uint64, isWider bool) {
}
delete(o.pending, seq)
delete(o.rdc, seq)
+ o.updateAcks(p.Sequence, seq, _EMPTY_)
}
}
}
@@ -6778,6 +6830,10 @@ func (o *consumer) decStreamPending(sseq uint64, subj string) {
var rdc uint64
if wasPending {
rdc = o.deliveryCount(sseq)
+ } else if _, ok := o.rdc[sseq]; ok && o.isLeader() {
+ delete(o.rdc, sseq)
+ // Pass 0 as the delivered sequence to only remove the redelivered state.
+ o.updateAcks(0, sseq, _EMPTY_)
}
o.mu.Unlock()
@@ -6878,14 +6934,21 @@ func gatherSubjectFilters(filter string, filters []string) []string {
// shouldStartMonitor will return true if we should start a monitor
// goroutine or will return false if one is already running.
func (o *consumer) shouldStartMonitor() bool {
- o.mu.Lock()
- defer o.mu.Unlock()
+ // monitorMu is held across the monitorWg.Add below so that it cannot race
+ // a concurrent monitorWg.Wait in stopMonitoring. It is taken before o.mu to
+ // keep a consistent lock ordering.
+ o.monitorMu.Lock()
+ defer o.monitorMu.Unlock()
+ o.mu.Lock()
if o.inMonitor {
+ o.mu.Unlock()
return false
}
- o.monitorWg.Add(1)
o.inMonitor = true
+ o.mu.Unlock()
+
+ o.monitorWg.Add(1)
return true
}
@@ -6901,6 +6964,18 @@ func (o *consumer) clearMonitorRunning() {
}
}
+// stopMonitoring signals any running monitor goroutine to quit and waits for
+// it to fully exit.
+func (o *consumer) stopMonitoring() {
+ // monitorMu is held across both the quit signal and the wait so that a
+ // concurrent shouldStartMonitor cannot slip a new monitor generation in
+ // between.
+ o.monitorMu.Lock()
+ defer o.monitorMu.Unlock()
+ o.signalMonitorQuit()
+ o.monitorWg.Wait()
+}
+
// Test whether we are in the monitor routine.
func (o *consumer) isMonitorRunning() bool {
o.mu.RLock()
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/dirstore.go b/vendor/github.com/nats-io/nats-server/v2/server/dirstore.go
index 6ef11bec7d..0fae234217 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/dirstore.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/dirstore.go
@@ -230,7 +230,7 @@ func (store *DirJWTStore) Pack(maxJWTs int) (string, error) {
}
store.Lock()
err := filepath.Walk(store.directory, func(path string, info os.FileInfo, err error) error {
- if !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
+ if info != nil && !info.IsDir() && strings.HasSuffix(path, fileExtension) { // this is a JWT
if count == maxJWTs { // won't match negative
return nil
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/events.go b/vendor/github.com/nats-io/nats-server/v2/server/events.go
index 8bc9bcd511..1c69d0de8e 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/events.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/events.go
@@ -374,7 +374,9 @@ type ServerStats struct {
ActiveAccounts int `json:"active_accounts"`
NumSubs uint32 `json:"subscriptions"`
Sent DataStats `json:"sent"`
+ SentToClients DataStats `json:"sent_to_clients"`
Received DataStats `json:"received"`
+ ReceivedFromClients DataStats `json:"received_from_clients"`
SlowConsumers int64 `json:"slow_consumers"`
SlowConsumersStats *SlowConsumersStats `json:"slow_consumer_stats,omitempty"`
StaleConnections int64 `json:"stale_connections,omitempty"`
@@ -612,7 +614,7 @@ RESET:
// Optional raw header addition.
if pm.hdr != nil {
- b = append(pm.hdr, b...)
+ b = append(pm.hdr[:len(pm.hdr):len(pm.hdr)], b...)
nhdr := len(pm.hdr)
nsize := len(b) - LEN_CR_LF
// MQTT producers don't have CRLF, so add it back.
@@ -948,8 +950,12 @@ func (s *Server) sendStatsz(subj string) {
m.Stats.ActiveAccounts = int(atomic.LoadInt32(&s.activeAccounts))
m.Stats.Received.Msgs = atomic.LoadInt64(&s.inMsgs)
m.Stats.Received.Bytes = atomic.LoadInt64(&s.inBytes)
+ m.Stats.ReceivedFromClients.Msgs = atomic.LoadInt64(&s.inClientMsgs)
+ m.Stats.ReceivedFromClients.Bytes = atomic.LoadInt64(&s.inClientBytes)
m.Stats.Sent.Msgs = atomic.LoadInt64(&s.outMsgs)
m.Stats.Sent.Bytes = atomic.LoadInt64(&s.outBytes)
+ m.Stats.SentToClients.Msgs = atomic.LoadInt64(&s.outClientMsgs)
+ m.Stats.SentToClients.Bytes = atomic.LoadInt64(&s.outClientBytes)
m.Stats.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
// Evaluate the slow consumer stats, but set it only if one of the value is not 0.
scs := &SlowConsumersStats{
@@ -1880,6 +1886,10 @@ func (s *Server) shutdownEventing() {
}
s.mu.Lock()
+ if s.sys == nil || s.sys.resetCh == nil {
+ s.mu.Unlock()
+ return
+ }
clearTimer(&s.sys.sweeper)
clearTimer(&s.sys.stmr)
rc := s.sys.resetCh
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
index c6108ad68b..96abc73ac5 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
@@ -377,6 +377,10 @@ const (
rlBadThresh = 32 * 1024 * 1024
// Checksum size for hash for msg records.
recordHashSize = 8
+
+ // Above this number of subjects, index.db may not be written regularly anymore, and
+ // certain psim optimisations may not be used.
+ highCardinalityThreshold = 1_000_000
)
func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
@@ -739,7 +743,7 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
fs.ageChkTime = 0
}
- if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
+ if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer <= 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
if err := fs.enforceMsgPerSubjectLimit(true); err != nil {
fs.mu.Unlock()
return err
@@ -1425,6 +1429,7 @@ func (mb *msgBlock) convertCipher() error {
// Reset the cache since we just read everything in.
mb.cache = nil
+ mb.ecache.Set(nil)
// Generate new keys. If we error for some reason then we will put
// the old keyfile back.
@@ -1462,10 +1467,19 @@ func (mb *msgBlock) convertToEncrypted() error {
} else if err = mb.indexCacheBuf(buf); err != nil {
// This likely indicates this was already encrypted or corrupt.
mb.cache = nil
+ mb.ecache.Set(nil)
return err
}
// Undo cache from above for later.
mb.cache = nil
+ mb.ecache.Set(nil)
+ // Regenerate mb.bek so that the keystream offset is at zero. This matches
+ // what encryptOrDecryptIfNeeded does on read-back, otherwise re-entering
+ // convertToEncrypted with a previously-used mb.bek would write ciphertext at
+ // the wrong stream offset and silently corrupt the block.
+ if mb.bek, err = genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce); err != nil {
+ return err
+ }
mb.bek.XORKeyStream(buf, buf)
<-dios
err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
@@ -2208,18 +2222,22 @@ func (fs *fileStore) recoverTTLState() error {
// Done.
break
}
- msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
+ mb.mu.Lock()
+ msg, _, err := mb.fetchMsgNoCopyLocked(seq, &sm)
if err != nil {
+ mb.finishedWithCache()
+ mb.mu.Unlock()
fs.warn("Error loading msg seq %d for recovering TTL: %s", seq, err)
continue
}
- if len(msg.hdr) == 0 {
- continue
- }
- if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
- expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
- fs.ttls.Add(seq, int64(expires))
+ if len(msg.hdr) > 0 {
+ if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
+ expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
+ fs.ttls.Add(seq, int64(expires))
+ }
}
+ mb.finishedWithCache()
+ mb.mu.Unlock()
}
}
return nil
@@ -2289,18 +2307,22 @@ func (fs *fileStore) recoverMsgSchedulingState() error {
// Done.
break
}
- msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
+ mb.mu.Lock()
+ msg, _, err := mb.fetchMsgNoCopyLocked(seq, &sm)
if err != nil {
+ mb.finishedWithCache()
+ mb.mu.Unlock()
fs.warn("Error loading msg seq %d for recovering message schedules: %s", seq, err)
continue
}
- if len(msg.hdr) == 0 {
- continue
- }
- if schedule, apiErr := nextMessageSchedule(sm.hdr, sm.ts); apiErr == nil && !schedule.IsZero() {
- // Copy the subject, as it's stored in the scheduling maps and the backing cache could be reused in the meantime.
- fs.scheduling.init(seq, copyString(sm.subj), schedule.UnixNano())
+ if len(msg.hdr) > 0 {
+ if schedule, apiErr := nextMessageSchedule(msg.hdr, msg.ts); apiErr == nil && !schedule.IsZero() {
+ // Copy the subject, as it's stored in the scheduling maps and the backing cache could be reused in the meantime.
+ fs.scheduling.init(seq, copyString(msg.subj), schedule.UnixNano())
+ }
}
+ mb.finishedWithCache()
+ mb.mu.Unlock()
}
}
return nil
@@ -2755,6 +2777,7 @@ func (fs *fileStore) GetSeqFromTime(t time.Time) uint64 {
// Using a binary search, but need to be aware of interior deletes in the block.
seq := lseq + 1
+ mb.mu.Lock()
loop:
for fseq <= lseq {
mid := fseq + (lseq-fseq)/2
@@ -2762,7 +2785,7 @@ loop:
// Potentially skip over gaps. We keep the original middle but keep track of a
// potential delete range with an offset.
for {
- sm, _, err := mb.fetchMsgNoCopy(mid+off, &smv)
+ sm, _, err := mb.fetchMsgNoCopyLocked(mid+off, &smv)
if err != nil || sm == nil {
off++
if mid+off <= lseq {
@@ -2789,6 +2812,8 @@ loop:
fseq = mid + off + 1
}
}
+ mb.finishedWithCache()
+ mb.mu.Unlock()
return seq
}
@@ -2829,14 +2854,11 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// If there are no subject matches then this is effectively no-op.
hseq := uint64(math.MaxUint64)
var ierr error
- stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
- if ierr != nil {
- return
- }
+ stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil {
- return
+ return false
}
}
first := max(start, ss.First)
@@ -2844,12 +2866,12 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// The start cutoff is after the last sequence for this subject,
// or we think we already know of a subject with an earlier msg
// than our first seq for this subject.
- return
+ return true
}
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if ierr = mb.loadMsgsWithLock(); ierr != nil {
- return
+ return false
}
didLoad = true
}
@@ -2863,7 +2885,7 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
sm = fsm
hseq = ss.First
}
- return
+ return true
}
for seq := first; seq <= ss.Last; seq++ {
// Otherwise we have a start floor that intersects where this subject
@@ -2889,6 +2911,7 @@ func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
+ return true
})
if ierr != nil {
return nil, false, ierr
@@ -3126,14 +3149,11 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
// If there are no subject matches then this is effectively no-op.
hseq := uint64(0)
var ierr error
- stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
- if ierr != nil {
- return
- }
+ stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) bool {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if ierr = mb.recalculateForSubj(bytesToString(subj), ss); ierr != nil {
- return
+ return false
}
}
first := min(start, ss.Last)
@@ -3142,7 +3162,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
if first < ss.First || first <= hseq {
// The start cutoff is before the first sequence for this subject,
// or we already know of a subject with a later-or-equal msg.
- return
+ return true
}
if first == ss.Last {
// If the start floor is above where this subject starts then we can
@@ -3151,7 +3171,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
sm = fsm
hseq = ss.Last
}
- return
+ return true
}
for seq := first; seq >= ss.First; seq-- {
// Otherwise we have a start floor that intersects where this subject
@@ -3177,6 +3197,7 @@ func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *S
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
+ return true
})
if ierr != nil {
return nil, false, ierr
@@ -3417,6 +3438,9 @@ func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, e
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
if wc {
+ if fs.psim.Size() > highCardinalityThreshold {
+ return bi + 1, nil
+ }
fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
if psi.fblk < start {
start = psi.fblk
@@ -3438,16 +3462,29 @@ func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, e
// This is used to see if we can selectively jump start blocks based on filter subjects and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlockMulti(sl *gsl.SimpleSublist, bi int) (int, error) {
+ // Don't bother if full wildcard.
+ if sl.MatchesFullWildcard() || fs.psim.Size() > highCardinalityThreshold {
+ return bi + 1, nil
+ }
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
- stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
+ guard := fs.blks[bi].getIndex() + 1
+ stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
if psi.fblk < start {
start = psi.fblk
}
+ if start == guard {
+ // One of the subjects matches the next block, so there's no point in carrying on trying to skip.
+ return false
+ }
if psi.lblk > stop {
stop = psi.lblk
}
+ return true
})
+ if start == guard {
+ return bi + 1, nil
+ }
// Nothing was found.
if start == uint32(math.MaxUint32) {
return -1, ErrStoreEOF
@@ -4322,10 +4359,10 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
mb := fs.blks[seqStart]
bi := mb.index
- stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
+ stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
// If the select blk start is greater than entry's last blk skip.
if bi > psi.lblk {
- return
+ return true
}
total++
// We will track the subjects that are an exact match to the last block.
@@ -4333,6 +4370,7 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
if psi.lblk == bi {
lbm[string(subj)] = true
}
+ return true
})
// Now check if we need to inspect the seqStart block.
@@ -4422,18 +4460,11 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
var ierr error
var havePartial bool
var updateLLTS bool
- stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
- if ierr != nil {
- return
- }
+ stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool {
subj := bytesToString(bsubj)
- if havePartial {
- // If we already found a partial then don't do anything else.
- return
- }
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
if ierr = mb.recalculateForSubj(subj, ss); ierr != nil {
- return
+ return false
}
}
if sseq <= ss.First {
@@ -4441,7 +4472,9 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
+ return false
}
+ return true
})
if ierr != nil {
mb.mu.Unlock()
@@ -4494,12 +4527,13 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
start := uint32(math.MaxUint32)
- stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
+ stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < start {
start = psi.fblk
}
+ return true
})
// See if we were asked for all, if so we are done.
@@ -4545,8 +4579,9 @@ func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPer
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
- stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
+ stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) bool {
adjust += ss.Msgs
+ return true
})
}
} else {
@@ -4716,6 +4751,7 @@ func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) {
}
// If we had a write error before, don't allow continuing into a new block.
if err := lmb.werr; err != nil {
+ lmb.mu.Unlock()
return nil, err
}
// Flush any pending messages.
@@ -4894,15 +4930,17 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
}
// Adjust top level tracking of per subject msg counts.
+ var info *psi
+ var ok bool
if len(subj) > 0 && fs.psim != nil {
index := fs.lmb.index
- if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
+ if info, ok = fs.psim.Find(stringToBytes(subj)); ok {
info.total++
if index > info.lblk {
info.lblk = index
}
} else {
- fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
+ info, _ = fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
fs.tsl += len(subj)
}
}
@@ -4957,6 +4995,10 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
}
}
}
+ // If we only ever store one/last message for a subject, can correct the first block to where we've just written.
+ if info != nil && info.total == 1 && mmp == 1 {
+ info.fblk = info.lblk
+ }
// Limits checks and enforcement.
// If they do any deletions they will update the
@@ -5728,7 +5770,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
lhdr, lmsg int
ttl int64
)
- // We don't use a copy as long as that's possible. When unlocking mb or erasing, we'll copy the subject.
sm, err := mb.cacheLookupNoCopy(seq, &smv)
if err != nil {
finishedWithCache()
@@ -5739,7 +5780,9 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
}
return false, err
} else if sm != nil {
- subj = sm.subj
+ // subj aliases mb.cache.buf; copy now because the cache may be erased or
+ // recycled after we drop mb.mu. The rest are scalars stashed for later use.
+ subj = copyString(sm.subj)
ts = sm.ts
lhdr = len(sm.hdr)
lmsg = len(sm.msg)
@@ -5751,8 +5794,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
// when the last block is empty.
// If not via limits and not empty (empty writes tombstone below if last) write tombstone.
if !viaLimits && !isEmpty && sm != nil {
- // Need to copy the subject since we unlock and re-acquire, and the cache could change.
- subj = copyString(subj)
mb.mu.Unlock() // Only safe way to checkLastBlock is to unlock here...
lmb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
@@ -5785,9 +5826,6 @@ func (fs *fileStore) removeMsgFromBlock(mb *msgBlock, seq uint64, secure, viaLim
mb.mu.Unlock()
return false, err
}
- // Need to copy the subject, as eraseMsg will overwrite the cache and we won't
- // be able to access sm.subj anymore later on.
- subj = copyString(subj)
if err := mb.eraseMsg(seq, int(ri), int(msz), isLastBlock); err != nil {
finishedWithCache()
mb.mu.Unlock()
@@ -6495,10 +6533,9 @@ func (mb *msgBlock) selectNextFirst() {
var smv StoreMsg
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil {
- // Slow path, need to unlock.
- mb.mu.Unlock()
- sm, _, _ = mb.fetchMsgNoCopy(seq, &smv)
- mb.mu.Lock()
+ // Slow path, cache not loaded.
+ sm, _, _ = mb.fetchMsgNoCopyLocked(seq, &smv)
+ mb.finishedWithCache()
}
if sm != nil {
mb.first.ts = sm.ts
@@ -6676,8 +6713,14 @@ func (mb *msgBlock) tryExpireCacheLocked() {
}
// Check for activity on the cache that would prevent us from expiring.
- if tns-bufts <= int64(mb.cexp) {
- mb.resetCacheExpireTimer(mb.cexp - time.Duration(tns-bufts))
+ // Both tns and bufts come from ats.AccessTime(), which means bufts can understate
+ // how recent the last activity actually was by up to one tick.
+ if delta := tns - bufts; delta <= int64(mb.cexp)+int64(ats.TickInterval) {
+ td := mb.cexp - time.Duration(delta)
+ if td <= 0 {
+ td = ats.TickInterval
+ }
+ mb.resetCacheExpireTimer(td)
if strengthened {
mb.finishedWithCache()
}
@@ -7467,6 +7510,11 @@ func (fs *fileStore) writeTombstoneNoFlush(seq uint64, ts int64) error {
// Lock should be held.
func (mb *msgBlock) recompressOnDiskIfNeeded() error {
+ // If the block has been closed in the meantime, skip.
+ if mb.closed {
+ return nil
+ }
+
alg := mb.fs.fcfg.Compression
// Open up the file block and read in the entire contents into memory.
@@ -8459,25 +8507,25 @@ checkCache:
// We assume the block was selected and is correct, so we do not do range checks.
// Lock should not be held.
func (mb *msgBlock) fetchMsg(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
+ mb.mu.Lock()
+ defer mb.mu.Unlock()
+ defer mb.finishedWithCache()
return mb.fetchMsgEx(seq, sm, true)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
-// We will not copy the msg data.
-// Lock should not be held.
-func (mb *msgBlock) fetchMsgNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
+// We will not copy the msg data, the returned StoreMsg's subj/hdr/msg/buf are aliased
+// into mb.cache.buf and are only safe to read while mb.mu is held.
+func (mb *msgBlock) fetchMsgNoCopyLocked(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
return mb.fetchMsgEx(seq, sm, false)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will copy the msg data based on doCopy boolean.
-// Lock should not be held.
+// Lock should be held.
func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, bool, error) {
- mb.mu.Lock()
- defer mb.mu.Unlock()
-
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
if seq < fseq || seq > lseq {
return nil, false, ErrStoreMsgNotFound
@@ -8499,7 +8547,6 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg
return nil, false, err
}
}
- defer mb.finishedWithCache()
llseq := mb.llseq
fsm, err := mb.cacheLookupEx(seq, sm, doCopy)
@@ -8507,7 +8554,7 @@ func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg
return nil, false, err
}
expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
- return fsm, expireOk, err
+ return fsm, expireOk, nil
}
var (
@@ -8675,9 +8722,15 @@ func (fs *fileStore) sizeForSeq(seq uint64) int {
}
var smv StoreMsg
if mb := fs.selectMsgBlock(seq); mb != nil {
- if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
- return int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
+ mb.mu.Lock()
+ sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv)
+ var sz int
+ if sm != nil {
+ sz = int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
}
+ mb.finishedWithCache()
+ mb.mu.Unlock()
+ return sz
}
return 0
}
@@ -8867,9 +8920,17 @@ func (fs *fileStore) SubjectForSeq(seq uint64) (string, error) {
mb := fs.selectMsgBlock(seq)
fs.mu.RUnlock()
if mb != nil {
- if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
+ mb.mu.Lock()
+ sm, _, _ := mb.fetchMsgNoCopyLocked(seq, &smv)
+ var subj string
+ if sm != nil {
// Copy the subject, as it's used elsewhere, and the backing cache could be reused in the meantime.
- return copyString(sm.subj), nil
+ subj = copyString(sm.subj)
+ }
+ mb.finishedWithCache()
+ mb.mu.Unlock()
+ if sm != nil {
+ return subj, nil
}
}
return _EMPTY_, ErrStoreMsgNotFound
@@ -8950,10 +9011,8 @@ func (fs *fileStore) loadLastLocked(subj string, sm *StoreMsg) (lsm *StoreMsg, e
if ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
if err = mb.recalculateForSubj(subj, ss); err != nil {
- if err != nil {
- mb.mu.Unlock()
- return nil, err
- }
+ mb.mu.Unlock()
+ return nil, err
}
}
l = ss.Last
@@ -9028,12 +9087,13 @@ func (fs *fileStore) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *
if start <= fs.state.FirstSeq {
var total uint64
blkStart := uint32(math.MaxUint32)
- stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
+ stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) bool {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < blkStart {
blkStart = psi.fblk
}
+ return true
})
// Nothing available.
if total == 0 {
@@ -9718,6 +9778,10 @@ func compareFn(subject string) func(string, string) bool {
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
+ // sequence == 1 means "purge up to but not including 1", a no-op.
+ if sequence == 1 {
+ return 0, nil
+ }
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return fs.purge(0)
@@ -9725,6 +9789,10 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
if sequence > 1 {
return fs.compact(sequence)
}
+ // Make sure to not leave subject if empty.
+ if subject == _EMPTY_ {
+ subject = fwcs
+ }
}
// Persist any write errors.
@@ -9736,11 +9804,6 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
}
}()
- // Make sure to not leave subject if empty and we reach this spot.
- if subject == _EMPTY_ {
- subject = fwcs
- }
-
eq, wc := compareFn(subject), subjectHasWildcard(subject)
var firstSeqNeedsUpdate bool
var bytes uint64
@@ -9828,7 +9891,10 @@ func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint
continue
}
- if sequence > 1 && sequence <= l {
+ // "Purge up to but not including sequence": sequence == 0 means no
+ // sequence filter; sequence >= 1 clamps the per-block upper bound to
+ // sequence-1 (so sequence == 1 leaves nothing to process).
+ if sequence >= 1 && sequence <= l {
l = sequence - 1
}
@@ -10018,7 +10084,11 @@ func (fs *fileStore) purge(fseq uint64) (purged uint64, rerr error) {
fs.state.Msgs = 0
for _, mb := range fs.blks {
- mb.dirtyClose()
+ // These blocks are being discarded by the purge, so mark them closed.
+ mb.mu.Lock()
+ mb.dirtyCloseWithRemove(false)
+ mb.closed = true
+ mb.mu.Unlock()
}
// Check if we need to set the first seq to a new number.
@@ -10690,11 +10760,18 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
// at the end, after we release the lock.
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
- var err error
- var lsm *StoreMsg
+ var hasLsm bool
+ var lastTime int64
smb := fs.selectMsgBlock(seq)
if smb != nil {
- lsm, _, err = smb.fetchMsgNoCopy(seq, nil)
+ smb.mu.Lock()
+ lsm, _, err := smb.fetchMsgNoCopyLocked(seq, nil)
+ if lsm != nil {
+ hasLsm = true
+ lastTime = lsm.ts
+ }
+ smb.finishedWithCache()
+ smb.mu.Unlock()
if err != nil && err != ErrStoreMsgNotFound && err != errDeletedMsg {
fs.mu.Unlock()
return err
@@ -10702,13 +10779,12 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
}
// Reset last so new block doesn't contain truncated sequences/timestamps.
- var lastTime int64
- if lsm != nil {
- lastTime = lsm.ts
- } else if smb != nil {
- lastTime = smb.last.ts
- } else {
- lastTime = fs.state.LastTime.UnixNano()
+ if !hasLsm {
+ if smb != nil {
+ lastTime = smb.last.ts
+ } else {
+ lastTime = fs.state.LastTime.UnixNano()
+ }
}
fs.state.LastSeq = seq
fs.state.LastTime = time.Unix(0, lastTime).UTC()
@@ -10730,7 +10806,7 @@ func (fs *fileStore) Truncate(seq uint64) (rerr error) {
// If the selected block is not found or the message was deleted, we'll need to write a tombstone
// at the truncated sequence so we don't roll backward on our last sequence and timestamp.
- if lsm == nil || removeSmb {
+ if !hasLsm || removeSmb {
if err = fs.writeTombstone(seq, lastTime); err != nil {
fs.mu.Unlock()
return err
@@ -11078,6 +11154,8 @@ func (mb *msgBlock) dirtyCloseWithRemove(remove bool) error {
}
}
if remove {
+ // The block is being destroyed, so mark it closed.
+ mb.closed = true
// Clear any tracking by subject if we are removing.
mb.fss = nil
if mb.mfn != _EMPTY_ {
@@ -11568,7 +11646,7 @@ func (fs *fileStore) flushStreamStateLoop(qch, done chan struct{}) {
fs.warn("File system permission denied when flushing stream state, disabling JetStream: %v", err)
// messages in block cache could be lost in the worst case.
// In the clustered mode it is very highly unlikely as a result of replication.
- fs.srv.DisableJetStream()
+ fs.srv.ShutdownJetStream()
return
}
@@ -11652,13 +11730,12 @@ func (fs *fileStore) _writeFullState(force bool) error {
// We will base off of number of subjects and interior deletes. A very large number of msg blocks could also
// be used, but for next server version will redo all meta handling to be disk based. So this is temporary.
if !force {
- const numThreshold = 1_000_000
// Calculate interior deletes.
var numDeleted int
if fs.state.LastSeq > fs.state.FirstSeq {
numDeleted = int((fs.state.LastSeq - fs.state.FirstSeq + 1) - fs.state.Msgs)
}
- if numSubjects > numThreshold || numDeleted > numThreshold {
+ if numSubjects > highCardinalityThreshold || numDeleted > highCardinalityThreshold {
fs.mu.RUnlock()
return errStateTooBig
}
@@ -12832,16 +12909,31 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
return ErrNoAckPolicy
}
+ var kick bool
+ defer func() {
+ if kick {
+ o.kickFlusher()
+ }
+ }()
+
+ // We do this regardless.
+ if _, ok := o.state.Redelivered[sseq]; ok {
+ delete(o.state.Redelivered, sseq)
+ kick = true
+ }
+
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
- delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
}
+ // Done with the consistency checks, we'll always kick for below updates.
+ kick = true
+
// Check for AckAll here (or AckFlowControl which functions like AckAll).
if o.cfg.AckPolicy == AckAll || o.cfg.AckPolicy == AckFlowControl {
sgap := sseq - o.state.AckFloor.Stream
@@ -12860,7 +12952,6 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
delete(o.state.Redelivered, seq)
}
}
- o.kickFlusher()
return nil
}
@@ -12892,13 +12983,27 @@ func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
}
}
}
- // We do these regardless.
- delete(o.state.Redelivered, sseq)
-
- o.kickFlusher()
return nil
}
+func (o *consumerFileStore) RemoveRedeliveredBelow(seq uint64) {
+ if seq == 0 {
+ return
+ }
+ o.mu.Lock()
+ defer o.mu.Unlock()
+ var removed bool
+ for s := range o.state.Redelivered {
+ if s < seq {
+ delete(o.state.Redelivered, s)
+ removed = true
+ }
+ }
+ if removed {
+ o.kickFlusher()
+ }
+}
+
const seqsHdrSize = 6*binary.MaxVarintLen64 + hdrLen
// Encode our consumer state, version 2.
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
index 010b170b8d..f686864668 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream.go
@@ -209,9 +209,6 @@ func (s *Server) EnableJetStream(config *JetStreamConfig) error {
maxStore, maxMem = config.MaxStore, config.MaxMemory
}
config = s.dynJetStreamConfig(storeDir, maxStore, maxMem)
- if maxMem > 0 {
- config.MaxMemory = maxMem
- }
if domain != _EMPTY_ {
config.Domain = domain
}
@@ -577,7 +574,7 @@ func (s *Server) restartJetStream() error {
err := s.EnableJetStream(&cfg)
if err != nil {
s.Warnf("Can't start JetStream: %v", err)
- return s.DisableJetStream()
+ return s.ShutdownJetStream()
}
s.updateJetStreamInfoStatus(true)
return nil
@@ -629,7 +626,7 @@ func (s *Server) handleOutOfSpace(mset *stream) {
s.Errorf("JetStream out of resources, will be DISABLED")
}
- go s.DisableJetStream()
+ go s.ShutdownJetStream()
adv := &JSServerOutOfSpaceAdvisory{
TypedEvent: TypedEvent{
@@ -648,8 +645,23 @@ func (s *Server) handleOutOfSpace(mset *stream) {
}
// DisableJetStream will turn off JetStream and signals in clustered mode
-// to have the metacontroller remove us from the peer list.
+// to have the metacontroller remove us from the peer list. Persistent
+// meta-raft state on disk is removed. For transient runtime errors where
+// the server should rejoin its existing meta group on restart, use
+// ShutdownJetStream instead.
func (s *Server) DisableJetStream() error {
+ return s.disableJetStream(true)
+}
+
+// ShutdownJetStream is like DisableJetStream but preserves persistent
+// meta-raft state on disk so the server can rejoin the existing meta
+// group on restart. Use for transient runtime errors that the operator
+// is expected to fix before restarting.
+func (s *Server) ShutdownJetStream() error {
+ return s.disableJetStream(false)
+}
+
+func (s *Server) disableJetStream(deleteState bool) error {
if !s.JetStreamEnabled() {
return nil
}
@@ -680,7 +692,12 @@ func (s *Server) DisableJetStream() error {
s.Warnf("JetStream timeout waiting for meta leader transfer")
}
}
- meta.Delete()
+ if deleteState {
+ meta.Delete()
+ } else {
+ meta.Stop()
+ meta.WaitForStop()
+ }
}
}
@@ -1811,13 +1828,15 @@ func diffCheckedLimits(a, b map[string]JetStreamAccountLimits) map[string]JetStr
// Lock should be held.
func (jsa *jsAccount) reservedStorage(tier string) (mem, store uint64) {
for _, mset := range jsa.streams {
- cfg := &mset.cfg
- if (tier == _EMPTY_ || tier == tierName(cfg.Replicas)) && cfg.MaxBytes > 0 {
- switch cfg.Storage {
+ mset.cfgMu.RLock()
+ storage, replicas, maxBytes := mset.cfg.Storage, mset.cfg.Replicas, mset.cfg.MaxBytes
+ mset.cfgMu.RUnlock()
+ if (tier == _EMPTY_ || tier == tierName(replicas)) && maxBytes > 0 {
+ switch storage {
case FileStorage:
- store += uint64(cfg.MaxBytes)
+ store += uint64(maxBytes)
case MemoryStorage:
- mem += uint64(cfg.MaxBytes)
+ mem += uint64(maxBytes)
}
}
}
@@ -2332,9 +2351,9 @@ func tierName(replicas int) string {
return fmt.Sprintf("R%d", replicas)
}
-func isSameTier(cfgA, cfgB *StreamConfig) bool {
- a := max(1, cfgA.Replicas)
- b := max(1, cfgB.Replicas)
+func isSameTier(replicasA, replicasB int) bool {
+ a := max(1, replicasA)
+ b := max(1, replicasB)
// TODO (mh) this is where we could select based off a placement tag as well "qos:tier"
return a == b
}
@@ -2360,9 +2379,12 @@ func (jsa *jsAccount) selectLimits(replicas int) (JetStreamAccountLimits, string
// Lock should be held.
func (jsa *jsAccount) countStreams(tier string, cfg *StreamConfig) (streams int) {
- for _, sa := range jsa.streams {
+ for _, mset := range jsa.streams {
+ mset.cfgMu.RLock()
+ name, replicas := mset.cfg.Name, mset.cfg.Replicas
+ mset.cfgMu.RUnlock()
// Don't count the stream toward the limit if it already exists.
- if (tier == _EMPTY_ || isSameTier(&sa.cfg, cfg)) && sa.cfg.Name != cfg.Name {
+ if (tier == _EMPTY_ || isSameTier(replicas, cfg.Replicas)) && name != cfg.Name {
streams++
}
}
@@ -2426,53 +2448,69 @@ func (jsa *jsAccount) wouldExceedLimits(storeType StorageType, tierName string,
// Check account limits.
// Read Lock should be held
-func (js *jetStream) checkAccountLimits(selected *JetStreamAccountLimits, config *StreamConfig, currentRes int64) error {
- return js.checkLimits(selected, config, false, currentRes, 0)
+func (js *jetStream) checkAccountLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, currentRes int64) error {
+ return js.checkLimits(selected, tier, config, false, currentRes, 0)
}
// Check account and server limits.
// Read Lock should be held
-func (js *jetStream) checkAllLimits(selected *JetStreamAccountLimits, config *StreamConfig, currentRes, maxBytesOffset int64) error {
- return js.checkLimits(selected, config, true, currentRes, maxBytesOffset)
+func (js *jetStream) checkAllLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, currentRes, maxBytesOffset int64) error {
+ return js.checkLimits(selected, tier, config, true, currentRes, maxBytesOffset)
}
// Check if a new proposed msg set while exceed our account limits.
// Lock should be held.
-func (js *jetStream) checkLimits(selected *JetStreamAccountLimits, config *StreamConfig, checkServer bool, currentRes, maxBytesOffset int64) error {
+func (js *jetStream) checkLimits(selected *JetStreamAccountLimits, tier string, config *StreamConfig, checkServer bool, currentRes, maxBytesOffset int64) error {
// Check MaxConsumers
if config.MaxConsumers > 0 && selected.MaxConsumers > 0 && config.MaxConsumers > selected.MaxConsumers {
return NewJSMaximumConsumersLimitError()
}
// stream limit is checked separately on stream create only!
// Check storage, memory or disk.
- return js.checkBytesLimits(selected, config.MaxBytes, config.Storage, checkServer, currentRes, maxBytesOffset)
+ return js.checkBytesLimits(selected, tier, config.MaxBytes, config.Replicas, config.Storage, checkServer, currentRes, maxBytesOffset)
+}
+
+// accountReservation returns how many bytes count against the account limit
+// for a stream with the given replica count. Un-tiered limits are flat, so R>1
+// is counted as Replicas*bytes; tiered limits already bake in replication.
+func accountReservation(tier string, replicas int, bytes int64) int64 {
+ if bytes <= 0 {
+ return 0
+ }
+ if tier == _EMPTY_ && replicas > 1 {
+ return mulSaturate(int64(replicas), bytes)
+ }
+ return bytes
}
// Check if additional bytes will exceed our account limits and optionally the server itself.
// Read Lock should be held.
-func (js *jetStream) checkBytesLimits(selectedLimits *JetStreamAccountLimits, addBytes int64, storage StorageType, checkServer bool, currentRes, maxBytesOffset int64) error {
+func (js *jetStream) checkBytesLimits(selectedLimits *JetStreamAccountLimits, tier string, addBytes int64, replicas int, storage StorageType, checkServer bool, currentRes, maxBytesOffset int64) error {
if addBytes < 0 {
addBytes = 1
}
- totalBytes := addSaturate(addBytes, maxBytesOffset)
+ // The per-server footprint is a single replica's worth of bytes; the
+ // account footprint additionally accounts for replication in un-tiered setups.
+ serverBytes := addSaturate(addBytes, maxBytesOffset)
+ accountBytes := accountReservation(tier, replicas, serverBytes)
switch storage {
case MemoryStorage:
// Account limits defined.
- if selectedLimits.MaxMemory >= 0 && (currentRes > selectedLimits.MaxMemory || totalBytes > selectedLimits.MaxMemory-currentRes) {
+ if selectedLimits.MaxMemory >= 0 && (currentRes > selectedLimits.MaxMemory || accountBytes > selectedLimits.MaxMemory-currentRes) {
return NewJSMemoryResourcesExceededError()
}
// Check if this server can handle request.
- if checkServer && (js.memReserved > js.config.MaxMemory || totalBytes > js.config.MaxMemory-js.memReserved) {
+ if checkServer && (js.memReserved > js.config.MaxMemory || serverBytes > js.config.MaxMemory-js.memReserved) {
return NewJSMemoryResourcesExceededError()
}
case FileStorage:
// Account limits defined.
- if selectedLimits.MaxStore >= 0 && (currentRes > selectedLimits.MaxStore || totalBytes > selectedLimits.MaxStore-currentRes) {
+ if selectedLimits.MaxStore >= 0 && (currentRes > selectedLimits.MaxStore || accountBytes > selectedLimits.MaxStore-currentRes) {
return NewJSStorageResourcesExceededError()
}
// Check if this server can handle request.
- if checkServer && (js.storeReserved > js.config.MaxStore || totalBytes > js.config.MaxStore-js.storeReserved) {
+ if checkServer && (js.storeReserved > js.config.MaxStore || serverBytes > js.config.MaxStore-js.storeReserved) {
return NewJSStorageResourcesExceededError()
}
}
@@ -2682,13 +2720,13 @@ func (s *Server) dynJetStreamConfig(storeDir string, maxStore, maxMem int64) *Je
jsc.SyncInterval = opts.SyncInterval
jsc.SyncAlways = opts.SyncAlways
- if opts.maxStoreSet && maxStore >= 0 {
+ if maxStore > 0 || (opts.maxStoreSet && maxStore == 0) {
jsc.MaxStore = maxStore
} else {
jsc.MaxStore = diskAvailable(jsc.StoreDir)
}
- if opts.maxMemSet && maxMem >= 0 {
+ if maxMem > 0 || (opts.maxMemSet && maxMem == 0) {
jsc.MaxMemory = maxMem
} else {
// Estimate to 75% of total memory if we can determine system memory.
@@ -2873,7 +2911,7 @@ func (s *Server) handleWritePermissionError() {
if s.JetStreamEnabled() {
s.Errorf("File system permission denied while writing, disabling JetStream")
- go s.DisableJetStream()
+ go s.ShutdownJetStream()
//TODO Send respective advisory if needed, same as in handleOutOfSpace
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go
index 53525a8bca..cd1ace7e1d 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_api.go
@@ -1352,19 +1352,16 @@ func (s *Server) jsonResponse(v any) string {
// Read lock must be held
func (jsa *jsAccount) tieredReservation(tier string, cfg *StreamConfig) int64 {
var reservation int64
- for _, sa := range jsa.streams {
+ for _, mset := range jsa.streams {
+ mset.cfgMu.RLock()
+ name, storage, replicas, maxBytes := mset.cfg.Name, mset.cfg.Storage, mset.cfg.Replicas, mset.cfg.MaxBytes
+ mset.cfgMu.RUnlock()
// Don't count the stream toward the limit if it already exists.
- if sa.cfg.Name == cfg.Name {
+ if name == cfg.Name {
continue
}
- if (tier == _EMPTY_ || isSameTier(&sa.cfg, cfg)) && sa.cfg.MaxBytes > 0 && sa.cfg.Storage == cfg.Storage {
- // If tier is empty, all storage is flat and we should adjust for replicas.
- // Otherwise if tiered, storage replication already taken into consideration.
- if tier == _EMPTY_ && sa.cfg.Replicas > 1 {
- reservation = addSaturate(reservation, mulSaturate(int64(sa.cfg.Replicas), sa.cfg.MaxBytes))
- } else {
- reservation = addSaturate(reservation, sa.cfg.MaxBytes)
- }
+ if (tier == _EMPTY_ || isSameTier(replicas, cfg.Replicas)) && maxBytes > 0 && storage == cfg.Storage {
+ reservation = addSaturate(reservation, accountReservation(tier, replicas, maxBytes))
}
}
return reservation
@@ -1699,19 +1696,23 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account,
resp.Streams = resp.Streams[:JSApiNamesLimit]
}
} else {
+ // Snapshot names once to avoid repeated cfgMu RLocks during sort+append.
msets := acc.filteredStreams(filter)
- // Since we page results order matters.
- if len(msets) > 1 {
- slices.SortFunc(msets, func(i, j *stream) int { return cmp.Compare(i.cfg.Name, j.cfg.Name) })
+ names := make([]string, len(msets))
+ for i, mset := range msets {
+ names[i] = mset.getCfgName()
+ }
+ if len(names) > 1 {
+ slices.Sort(names)
}
- numStreams = len(msets)
+ numStreams = len(names)
if offset > numStreams {
offset = numStreams
}
- for _, mset := range msets[offset:] {
- resp.Streams = append(resp.Streams, mset.cfg.Name)
+ for _, name := range names[offset:] {
+ resp.Streams = append(resp.Streams, name)
if len(resp.Streams) >= JSApiNamesLimit {
break
}
@@ -1805,21 +1806,31 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s
msets = acc.filteredStreams(filter)
}
- slices.SortFunc(msets, func(i, j *stream) int { return cmp.Compare(i.cfg.Name, j.cfg.Name) })
+ // Snapshot names once and sort the parallel slice to avoid repeated cfgMu RLocks.
+ type msetWithName struct {
+ mset *stream
+ name string
+ }
+ named := make([]msetWithName, len(msets))
+ for i, mset := range msets {
+ named[i] = msetWithName{mset, mset.getCfgName()}
+ }
+ slices.SortFunc(named, func(a, b msetWithName) int { return cmp.Compare(a.name, b.name) })
- scnt := len(msets)
+ scnt := len(named)
if offset > scnt {
offset = scnt
}
var missingNames []string
- for _, mset := range msets[offset:] {
+ for _, n := range named[offset:] {
+ mset, name := n.mset, n.name
if mset.offlineReason != _EMPTY_ {
if resp.Offline == nil {
resp.Offline = make(map[string]string, 1)
}
- resp.Offline[mset.getCfgName()] = mset.offlineReason
- missingNames = append(missingNames, mset.getCfgName())
+ resp.Offline[name] = mset.offlineReason
+ missingNames = append(missingNames, name)
continue
}
@@ -3286,12 +3297,15 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
- if mset.cfg.Sealed {
+ mset.cfgMu.RLock()
+ sealed, denyDelete := mset.cfg.Sealed, mset.cfg.DenyDelete
+ mset.cfgMu.RUnlock()
+ if sealed {
resp.Error = NewJSStreamSealedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
- if mset.cfg.DenyDelete {
+ if denyDelete {
resp.Error = NewJSStreamMsgDeleteFailedError(errors.New("message delete not permitted"))
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
@@ -3719,12 +3733,15 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account,
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
- if mset.cfg.Sealed {
+ mset.cfgMu.RLock()
+ sealed, denyPurge := mset.cfg.Sealed, mset.cfg.DenyPurge
+ mset.cfgMu.RUnlock()
+ if sealed {
resp.Error = NewJSStreamSealedError()
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
}
- if mset.cfg.DenyPurge {
+ if denyPurge {
resp.Error = NewJSStreamPurgeFailedError(errors.New("stream purge not permitted"))
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp))
return
@@ -3762,7 +3779,7 @@ func (acc *Account) jsNonClusteredStreamLimitsCheck(cfg *StreamConfig) *ApiError
return NewJSMaximumStreamsLimitError()
}
reserved := jsa.tieredReservation(tier, cfg)
- if err := jsa.js.checkAllLimits(selectedLimits, cfg, reserved, 0); err != nil {
+ if err := jsa.js.checkAllLimits(selectedLimits, tier, cfg, reserved, 0); err != nil {
return NewJSStreamLimitsError(err, Unless(err))
}
return nil
@@ -4297,6 +4314,8 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
var hdr []byte
chunk := make([]byte, chunkSize)
+ ackTimer := time.NewTimer(snapshotAckTimeout)
+ defer stopAndClearTimer(&ackTimer)
for index := 1; ; index++ {
select {
case <-slots:
@@ -4309,7 +4328,7 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
// The snapshotting goroutine has failed for some reason.
hdr = []byte(fmt.Sprintf("NATS/1.0 500 %s\r\n\r\n", err))
goto done
- case <-time.After(snapshotAckTimeout):
+ case <-ackTimer.C:
// It's taking a very long time for the receiver to send us acks,
// they have probably stalled or there is high loss on the link.
hdr = []byte("NATS/1.0 408 No Flow Response\r\n\r\n")
@@ -4328,6 +4347,7 @@ func (s *Server) streamSnapshot(acc *Account, mset *stream, sr *SnapshotResult,
hdr = []byte("NATS/1.0 204\r\n\r\n")
}
mset.outq.send(newJSPubMsg(reply, _EMPTY_, ackReply, nil, chunk, nil, 0))
+ ackTimer.Reset(snapshotAckTimeout)
}
done:
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go
index 4b04241fad..2f4419bbf2 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_batching.go
@@ -451,6 +451,7 @@ func (diff *batchStagedDiff) commit(mset *stream) {
if c, ok := mset.inflight[subj]; ok {
c.bytes += i.bytes
c.ops += i.ops
+ c.schedule = i.schedule
} else {
mset.inflight[subj] = i
}
@@ -530,13 +531,14 @@ func checkMsgHeadersPreClusteredProposal(
discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64,
) ([]byte, []byte, uint64, *ApiError, error) {
var incr *big.Int
+ var hasSchedule bool
// Some header checks must be checked pre proposal.
if len(hdr) > 0 {
// Since we encode header len as u16 make sure we do not exceed.
// Again this works if it goes through but better to be pre-emptive.
if len(hdr) > math.MaxUint16 {
- err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
+ err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, name)
return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err
}
// Counter increments.
@@ -810,6 +812,7 @@ func checkMsgHeadersPreClusteredProposal(
}
return hdr, msg, 0, apiErr, apiErr
} else if !schedule.IsZero() {
+ hasSchedule = true
if !allowMsgSchedules {
apiErr := NewJSMessageSchedulesDisabledError()
return hdr, msg, 0, apiErr, apiErr
@@ -877,6 +880,26 @@ func checkMsgHeadersPreClusteredProposal(
} else if !allowMsgSchedules {
apiErr := NewJSMessageSchedulesDisabledError()
return hdr, msg, 0, apiErr, apiErr
+ } else {
+ // Check that the to-be-purged subject is a schedule message.
+ // We still allow this message through if there exists no message for this subject,
+ // to remain backward-compatible. An "expected at sequence" check can still be
+ // performed to make this stricter.
+ schedSubj := bytesToString(scheduler)
+ var invalid bool
+ if i, ok := diff.inflight[schedSubj]; ok {
+ invalid = !i.schedule
+ } else if i, ok = mset.inflight[schedSubj]; ok {
+ invalid = !i.schedule
+ } else {
+ var smv StoreMsg
+ sm, _ := mset.store.LoadLastMsg(schedSubj, &smv)
+ invalid = sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0
+ }
+ if invalid {
+ apiErr := NewJSMessageSchedulesSchedulerInvalidError()
+ return hdr, msg, 0, apiErr, apiErr
+ }
}
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
@@ -930,8 +953,9 @@ func checkMsgHeadersPreClusteredProposal(
if i, ok = diff.inflight[subject]; ok {
i.bytes += sz
i.ops++
+ i.schedule = hasSchedule
} else {
- i = &inflightSubjectRunningTotal{bytes: sz, ops: 1}
+ i = &inflightSubjectRunningTotal{bytes: sz, ops: 1, schedule: hasSchedule}
diff.inflight[subject] = i
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
index d406c7f1a2..8ca41480ce 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/jetstream_cluster.go
@@ -320,6 +320,21 @@ func (ca *consumerAssignment) clearResponded() {
ca.responded.Store(false)
}
+// sameIdentity reports whether nca refers to the same logical consumer as ca.
+// Only stable identity fields (Name, Stream, Group name, Created time) are
+// compared; request-routing fields like Client/Reply and transient flags are
+// intentionally excluded since processClusterCreateConsumer may set the
+// per-object o.ca to a clone with the original requester's Client/Reply
+// preserved while the meta-layer holds the newer values.
+func (ca *consumerAssignment) sameIdentity(nca *consumerAssignment) bool {
+ return ca != nil && nca != nil &&
+ nca.Name == ca.Name &&
+ nca.Stream == ca.Stream &&
+ nca.Created.Equal(ca.Created) &&
+ nca.Group != nil && ca.Group != nil &&
+ nca.Group.Name == ca.Group.Name
+}
+
// clone returns a copy of ca. Field-explicit (rather than `*ca`) and
// pointer-returning so the embedded atomic.Bool isn't value-copied;
// responded is transferred via Load/Store. Concurrent callers may write
@@ -713,6 +728,14 @@ func (js *jetStream) isStreamHealthy(acc *Account, sa *streamAssignment) error {
js.mu.RUnlock()
return errors.New("stream assignment or group missing")
}
+ // Surface any persisted assignment-level error (e.g. failed create on this
+ // peer due to account limits) so the health check reflects the broken state
+ // instead of falling through to runtime-only checks.
+ if sa.err != nil {
+ err := sa.err
+ js.mu.RUnlock()
+ return fmt.Errorf("stream assignment error: %w", err)
+ }
streamName := sa.Config.Name
node := sa.Group.node
js.mu.RUnlock()
@@ -788,6 +811,14 @@ func (js *jetStream) isConsumerHealthy(mset *stream, consumer string, ca *consum
js.mu.RUnlock()
return errors.New("consumer assignment or group missing")
}
+ // Surface any persisted assignment-level error (e.g. failed create on this
+ // peer) so the health check reflects the broken state instead of falling
+ // through to runtime-only checks.
+ if ca.err != nil {
+ err := ca.err
+ js.mu.RUnlock()
+ return fmt.Errorf("consumer assignment error: %w", err)
+ }
created := ca.Created
node := ca.Group.node
js.mu.RUnlock()
@@ -3612,7 +3643,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
// If we were successful lookup up our stream now.
if err == nil {
if mset, err = acc.lookupStream(sa.Config.Name); mset != nil {
- mset.monitorWg.Add(1)
+ mset.startMonitorWg()
defer mset.monitorWg.Done()
mset.checkInMonitor()
mset.setStreamAssignment(sa)
@@ -3632,6 +3663,7 @@ func (js *jetStream) monitorStream(mset *stream, sa *streamAssignment, sendSnaps
mset.delete()
}
js.mu.Lock()
+ s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
if n != nil {
n.Delete()
@@ -3792,8 +3824,7 @@ func (mset *stream) resetClusteredState(err error) bool {
// Need to do the rest in a separate Go routine.
go func() {
- mset.signalMonitorQuit()
- mset.monitorWg.Wait()
+ mset.stopMonitoring()
mset.resetAndWaitOnConsumers()
// Stop our stream.
mset.stop(shouldDelete, false)
@@ -4526,6 +4557,10 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
return
}
+ // Acquire clMu before ddMu so any inflight proposals finish first, and we can
+ // clean up if they added new dedupe IDs.
+ mset.clMu.Lock()
+
// Clear inflight dedupe IDs, where seq=0.
mset.ddMu.Lock()
var removed int
@@ -4548,7 +4583,6 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
}
mset.ddMu.Unlock()
- mset.clMu.Lock()
// Clear inflight if we have it.
mset.inflight = nil
mset.inflightTransform = nil
@@ -4557,6 +4591,12 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
// Clear expected per subject state.
mset.expectedPerSubjectSequence = nil
mset.expectedPerSubjectInProcess = nil
+
+ // Clear clseq on every leader transition. recalculateClusteredSeq
+ // repopulates it on the next proposal.
+ if mset.clseq > 0 {
+ mset.clseq = 0
+ }
mset.clMu.Unlock()
js.mu.RLock()
@@ -4578,14 +4618,6 @@ func (js *jetStream) processStreamLeaderChange(mset *stream, isLeader bool) {
}
}
- // Clear clseq on every leader transition. recalculateClusteredSeq
- // repopulates it on the next proposal.
- mset.clMu.Lock()
- if mset.clseq > 0 {
- mset.clseq = 0
- }
- mset.clMu.Unlock()
-
// Tell stream to switch leader status.
mset.setLeader(isLeader)
@@ -5041,14 +5073,14 @@ func (s *Server) removeStream(mset *stream, nsa *streamAssignment) {
if js, _ := s.getJetStreamCluster(); js != nil {
js.mu.Lock()
nsa.Group.node = nil
+ nsa.err = nil
isShuttingDown = js.shuttingDown
js.mu.Unlock()
}
if !isShuttingDown {
// wait for monitor to be shutdown.
- mset.signalMonitorQuit()
- mset.monitorWg.Wait()
+ mset.stopMonitoring()
}
mset.stop(true, false)
}
@@ -5068,6 +5100,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
storage, cfg := sa.Config.Storage, sa.Config
recovering := sa.recovering
hasResponded := sa.markResponded()
+ hadErr := sa.err != nil
js.mu.RUnlock()
mset, err := acc.lookupStream(cfg.Name)
@@ -5077,8 +5110,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
s.Warnf("JetStream cluster detected stream remapping for '%s > %s' from %q to %q",
acc, cfg.Name, osa.Group.Name, sa.Group.Name)
mset.removeNode()
- mset.signalMonitorQuit()
- mset.monitorWg.Wait()
+ mset.stopMonitoring()
alreadyRunning, needsNode = false, true
// Make sure to clear from original.
js.mu.Lock()
@@ -5103,7 +5135,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
"stream": mset.name(),
})
}
- mset.monitorWg.Add(1)
+ mset.startMonitorWg()
// Start monitoring..
started := s.startGoRoutine(
func() { js.monitorStream(mset, sa, needsNode) },
@@ -5119,8 +5151,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
} else if numReplicas == 1 && alreadyRunning {
// We downgraded to R1. Make sure we cleanup the raft node and the stream monitor.
mset.removeNode()
- mset.signalMonitorQuit()
- mset.monitorWg.Wait()
+ mset.stopMonitoring()
// In case we need to shutdown the cluster specific subs, etc.
mset.mu.Lock()
// Stop responding to sync requests.
@@ -5137,9 +5168,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
mset.setStreamAssignment(sa)
// Call update.
- if err = mset.updateWithAdvisory(cfg, !recovering, false); err != nil {
- s.Warnf("JetStream cluster error updating stream %q for account %q: %v", cfg.Name, acc.Name, err)
- }
+ err = mset.updateWithAdvisory(cfg, !recovering, false)
}
// If not found we must be expanding into this node since if we are here we know we are a member.
@@ -5150,6 +5179,7 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
if err != nil {
js.mu.Lock()
+ s.Warnf("Stream update failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
result := &streamAssignmentResult{
Account: sa.Client.serviceAccount(),
@@ -5163,6 +5193,10 @@ func (js *jetStream) processClusterUpdateStream(acc *Account, osa, sa *streamAss
// Send response to the metadata leader. They will forward to the user as needed.
s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
return
+ } else if hadErr {
+ js.mu.Lock()
+ sa.err = nil
+ js.mu.Unlock()
}
isLeader := mset.IsLeader()
@@ -5219,6 +5253,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
storage := sa.Config.Storage
restore := sa.Restore
recovering := sa.recovering
+ hadErr := sa.err != nil
js.mu.RUnlock()
// Process the raft group and make sure it's running if needed.
@@ -5310,7 +5345,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
}
} else if err == NewJSStreamNotFoundError() {
// Add in the stream here.
- mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa, false, false)
+ mset, err = acc.addStreamWithAssignment(sa.Config, nil, sa, false, true)
}
if mset != nil {
mset.setCreatedTime(created)
@@ -5327,8 +5362,8 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
return
}
+ s.Warnf("Stream create failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
if IsNatsErr(err, JSStreamStoreFailedF) {
- s.Warnf("Stream create failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
err = errStreamStoreFailed
}
js.mu.Lock()
@@ -5361,6 +5396,10 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
s.sendInternalMsgLocked(streamAssignmentSubj, _EMPTY_, nil, result)
}
return
+ } else if hadErr {
+ js.mu.Lock()
+ sa.err = nil
+ js.mu.Unlock()
}
// Re-capture node.
@@ -5372,7 +5411,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
if node != nil {
if !alreadyRunning {
if mset != nil {
- mset.monitorWg.Add(1)
+ mset.startMonitorWg()
}
started := s.startGoRoutine(
func() { js.monitorStream(mset, sa, false) },
@@ -5408,6 +5447,7 @@ func (js *jetStream) processClusterCreateStream(acc *Account, sa *streamAssignme
mset.delete()
}
js.mu.Lock()
+ s.Warnf("Stream restore failed for '%s > %s': %v", sa.Client.serviceAccount(), sa.Config.Name, err)
sa.err = err
result := &streamAssignmentResult{
Account: sa.Client.serviceAccount(),
@@ -5559,8 +5599,7 @@ func (js *jetStream) processClusterDeleteStream(sa *streamAssignment, isMember,
n.Delete()
}
// wait for monitor to be shut down
- mset.signalMonitorQuit()
- mset.monitorWg.Wait()
+ mset.stopMonitoring()
err = mset.stop(true, wasLeader)
stopped = true
} else if isMember {
@@ -5782,8 +5821,7 @@ func (s *Server) removeConsumer(o *consumer, nca *consumerAssignment) {
if !isShuttingDown {
// wait for monitor to be shutdown.
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
}
o.deleteWithoutAdvisory()
}
@@ -5890,8 +5928,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
s.Warnf("JetStream cluster detected consumer remapping for '%s > %s' from %q to %q",
acc, ca.Name, oca.Group.Name, ca.Group.Name)
o.clearNode()
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
alreadyRunning = false
// Make sure to clear from original.
js.mu.Lock()
@@ -5999,13 +6036,12 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
return
}
+ s.Warnf("Consumer create failed for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err)
if IsNatsErr(err, JSConsumerStoreFailedErrF) {
- s.Warnf("Consumer create failed for '%s > %s > %s': %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, err)
err = errConsumerStoreFailed
}
js.mu.Lock()
-
ca.err = err
hasResponded := ca.hasResponded()
@@ -6048,8 +6084,14 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
}
} else {
js.mu.RLock()
+ hadErr := ca.err != nil
node := rg.node
js.mu.RUnlock()
+ if hadErr {
+ js.mu.Lock()
+ ca.err = nil
+ js.mu.Unlock()
+ }
if didCreate {
o.setCreatedTime(ca.Created)
@@ -6057,8 +6099,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
// Check for scale down to 1..
if node != nil && len(rg.Peers) == 1 {
o.clearNode()
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
// Need to clear from rg too.
js.mu.Lock()
rg.node = nil
@@ -6097,8 +6138,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
if node == nil {
// Wait for the previous routine to stop running.
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
// Single replica consumer, process manually here.
// Force response in case we think this is an update.
if !js.isMetaRecovering() && isConfigUpdate {
@@ -6129,8 +6169,7 @@ func (js *jetStream) processClusterCreateConsumer(oca, ca *consumerAssignment, s
// Start our monitoring routine if needed.
if !alreadyRunning {
// Wait for the previous routine to stop running.
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
if o.shouldStartMonitor() {
started := s.startGoRoutine(
func() { js.monitorConsumer(o, ca) },
@@ -7199,9 +7238,17 @@ func (js *jetStream) processStreamAssignmentResults(sub *subscription, c *client
}
// Remove this assignment if possible.
if canDelete {
+ var apiErr *ApiError
+ if result.Response != nil {
+ apiErr = result.Response.Error
+ } else if result.Restore != nil {
+ apiErr = result.Restore.Error
+ }
+ s.Warnf("Stream assignment for '%s > %s' rejected by assigned member: %v", sa.Client.serviceAccount(), sa.Config.Name, apiErr)
sa.err = NewJSClusterNotAssignedError()
- cc.meta.Propose(encodeDeleteStreamAssignment(sa))
- cc.trackInflightStreamProposal(result.Account, sa, true)
+ if err := cc.meta.Propose(encodeDeleteStreamAssignment(sa)); err == nil {
+ cc.trackInflightStreamProposal(result.Account, sa, true)
+ }
}
}
}
@@ -7236,6 +7283,7 @@ func (js *jetStream) processConsumerAssignmentResults(sub *subscription, c *clie
// Make sure this is recent response.
if result.Response.Error != nil && result.Response.Error != NewJSConsumerNameExistError() && time.Since(ca.Created) < 2*time.Second {
// Do not list in consumer names/lists.
+ s.Warnf("Consumer assignment for '%s > %s > %s' rejected by assigned member: %v", ca.Client.serviceAccount(), ca.Stream, ca.Name, result.Response.Error)
ca.err = NewJSClusterNotAssignedError()
}
}
@@ -7866,16 +7914,10 @@ func (js *jetStream) tieredStreamAndReservationCount(accName, tier string, cfg *
if sa.Config.Name == cfg.Name {
continue
}
- if tier == _EMPTY_ || isSameTier(sa.Config, cfg) {
+ if tier == _EMPTY_ || isSameTier(sa.Config.Replicas, cfg.Replicas) {
numStreams++
if sa.Config.MaxBytes > 0 && sa.Config.Storage == cfg.Storage {
- // If tier is empty, all storage is flat and we should adjust for replicas.
- // Otherwise if tiered, storage replication already taken into consideration.
- if tier == _EMPTY_ && sa.Config.Replicas > 1 {
- reservation = addSaturate(reservation, mulSaturate(int64(sa.Config.Replicas), sa.Config.MaxBytes))
- } else {
- reservation = addSaturate(reservation, sa.Config.MaxBytes)
- }
+ reservation = addSaturate(reservation, accountReservation(tier, sa.Config.Replicas, sa.Config.MaxBytes))
}
}
}
@@ -7951,7 +7993,7 @@ func (js *jetStream) jsClusteredStreamLimitsCheck(acc *Account, cfg *StreamConfi
return NewJSMaximumStreamsLimitError()
}
// Check for account limits here before proposing.
- if err := js.checkAccountLimits(selectedLimits, cfg, reservations); err != nil {
+ if err := js.checkAccountLimits(selectedLimits, tier, cfg, reservations); err != nil {
return NewJSStreamLimitsError(err, Unless(err))
}
return nil
@@ -8298,26 +8340,7 @@ func (s *Server) jsClusteredStreamUpdateRequest(ci *ClientInfo, acc *Account, su
js.mu.Lock()
}
// If we identified a leader make sure its part of the new group.
- selected := make([]string, 0, newCfg.Replicas)
-
- if curLeader != _EMPTY_ {
- selected = append(selected, curLeader)
- }
- for _, peer := range rg.Peers {
- if len(selected) == newCfg.Replicas {
- break
- }
- if peer == curLeader {
- continue
- }
- if si, ok := s.nodeToInfo.Load(peer); ok && si != nil {
- if si.(nodeInfo).offline {
- continue
- }
- selected = append(selected, peer)
- }
- }
- rg.Peers = selected
+ rg.Peers = s.selectScaleDownPeers(rg.Peers, curLeader, newCfg.Replicas)
// Single nodes are not recorded by the NRG layer so we can rename.
// MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies.
if len(rg.Peers) == 1 {
@@ -8639,6 +8662,43 @@ func (s *Server) allPeersOffline(rg *raftGroup) bool {
return true
}
+// Select the peers to keep when scaling a raft group down to replicas.
+// The current leader, if known and in peer set, is kept. Online peers are preferred,
+// but we will fall back to offline peers to honor the requested replica count.
+func (s *Server) selectScaleDownPeers(peers []string, curLeader string, replicas int) []string {
+ selected := make([]string, 0, replicas)
+ if curLeader != _EMPTY_ && slices.Contains(peers, curLeader) {
+ selected = append(selected, curLeader)
+ if len(selected) == replicas {
+ return selected
+ }
+ }
+ // Prefer online peers.
+ for _, peer := range peers {
+ if peer == curLeader {
+ continue
+ }
+ if si, ok := s.nodeToInfo.Load(peer); ok && si != nil && !si.(nodeInfo).offline {
+ selected = append(selected, peer)
+ if len(selected) == replicas {
+ return selected
+ }
+ }
+ }
+
+ // Fall back to offline peers for the remainder.
+ for _, peer := range peers {
+ if slices.Contains(selected, peer) {
+ continue
+ }
+ selected = append(selected, peer)
+ if len(selected) == replicas {
+ break
+ }
+ }
+ return selected
+}
+
// This will do a scatter and gather operation for all streams for this account. This is only called from metadata leader.
// This will be running in a separate Go routine.
func (s *Server) jsClusteredStreamListRequest(acc *Account, ci *ClientInfo, filter string, offset int, subject, reply string, rmsg []byte) {
@@ -9423,6 +9483,11 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
return
}
+ if cfg.DeliverPolicy != DeliverAll {
+ resp.Error = NewJSConsumerWQConsumerNotDeliverAllError()
+ s.sendAPIErrResponse(ci, acc, subject, reply, string(rmsg), s.jsonResponse(&resp))
+ return
+ }
subjects := gatherSubjectFilters(cfg.FilterSubject, cfg.FilterSubjects)
for oca := range js.consumerAssignmentsOrInflightSeq(acc.Name, stream) {
if oca.Name == oname || oca.Config.Direct || oca.Config.Sourcing {
@@ -9479,7 +9544,7 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
if ci, err := sysRequest[ConsumerInfo](s, clusterConsumerInfoT, ci.serviceAccount(), sa.Config.Name, oname); err != nil {
s.Warnf("Did not receive consumer info results for '%s > %s > %s' due to: %s", acc, sa.Config.Name, oname, err)
} else if ci != nil {
- if cl := ci.Cluster; cl != nil {
+ if cl := ci.Cluster; cl != nil && cl.Leader != _EMPTY_ {
curLeader = getHash(cl.Leader)
}
}
@@ -9523,20 +9588,9 @@ func (s *Server) jsClusteredConsumerRequest(ci *ClientInfo, acc *Account, subjec
nca.Group.Preferred = curLeader
nca.Group.ScaleUp = true
} else if rBefore > rAfter {
- newPeerSet := nca.Group.Peers
- // mark leader preferred and move it to end
+ // Mark the current leader as preferred, it will be kept in the new peer set.
nca.Group.Preferred = curLeader
- if nca.Group.Preferred != _EMPTY_ {
- for i, p := range newPeerSet {
- if nca.Group.Preferred == p {
- newPeerSet[i] = newPeerSet[len(newPeerSet)-1]
- newPeerSet[len(newPeerSet)-1] = p
- }
- }
- }
- // scale down by removing peers from the end
- newPeerSet = newPeerSet[len(newPeerSet)-rAfter:]
- nca.Group.Peers = newPeerSet
+ nca.Group.Peers = s.selectScaleDownPeers(nca.Group.Peers, curLeader, rAfter)
// Single nodes are not recorded by the NRG layer so we can rename.
// MUST do this, otherwise a scaleup afterward could potentially lead to inconsistencies.
if len(nca.Group.Peers) == 1 {
@@ -9978,7 +10032,7 @@ func (mset *stream) processClusteredInboundMsg(subject, reply string, hdr, msg [
// Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive.
// Subtract to prevent against overflows.
if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) {
- err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
+ err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, name)
s.RateLimitWarnf("%s", err.Error())
if canRespond {
var resp = &JSPubAckResponse{PubAck: &PubAck{Stream: name}}
@@ -10202,6 +10256,17 @@ var (
errCatchupTooManyRetries = errors.New("catchup failed, too many retries")
)
+// Catchup inactivity timers.
+const (
+ defaultStreamCatchupStartInterval = 5 * time.Second
+ defaultStreamCatchupActivityInterval = 30 * time.Second
+)
+
+var (
+ streamCatchupStartInterval = defaultStreamCatchupStartInterval
+ streamCatchupActivityInterval = defaultStreamCatchupActivityInterval
+)
+
// Process a stream snapshot.
func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (e error) {
// Update any deletes, etc.
@@ -10260,10 +10325,8 @@ func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (
var sub *subscription
var err error
- const (
- startInterval = 5 * time.Second
- activityInterval = 30 * time.Second
- )
+ startInterval := streamCatchupStartInterval
+ activityInterval := streamCatchupActivityInterval
notActive := time.NewTimer(startInterval)
defer notActive.Stop()
@@ -10275,9 +10338,7 @@ func (mset *stream) processSnapshot(snap *StreamReplicatedState, index uint64) (
mset.mu.Lock()
for _, o := range mset.consumers {
o.mu.Lock()
- if o.isLeader() {
- o.streamNumPending()
- }
+ o.streamNumPending()
o.mu.Unlock()
}
mset.mu.Unlock()
@@ -10462,8 +10523,11 @@ RETRY:
return err
} else if err == NewJSInsufficientResourcesError() {
notifyLeaderStopCatchup(mrec, err)
- if mset.js.limitsExceeded(mset.cfg.Storage) {
- s.resourcesExceededError(mset.cfg.Storage)
+ mset.cfgMu.RLock()
+ storage := mset.cfg.Storage
+ mset.cfgMu.RUnlock()
+ if mset.js.limitsExceeded(storage) {
+ s.resourcesExceededError(storage)
} else {
s.Warnf("Catchup for stream '%s > %s' errored, account resources exceeded: %v", mset.account(), mset.name(), err)
}
@@ -10914,7 +10978,7 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
nextBatchC <- struct{}{}
remoteQuitCh := make(chan struct{})
- const activityInterval = 30 * time.Second
+ activityInterval := streamCatchupActivityInterval
notActive := time.NewTimer(activityInterval)
defer notActive.Stop()
@@ -11167,6 +11231,8 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
// Run as long as we are still active and need catchup.
// FIXME(dlc) - Purge event? Stream delete?
+ retryTimer := time.NewTimer(500 * time.Millisecond)
+ defer stopAndClearTimer(&retryTimer)
for {
// Get this each time, will be non-nil if globally blocked and we will close to wake everyone up.
cbKick := s.cbKickChan()
@@ -11181,7 +11247,8 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
return
case <-notActive.C:
s.Warnf("Catchup for stream '%s > %s' stalled", mset.account(), mset.name())
- mset.clearCatchupPeer(sreq.Peer)
+ // Do NOT clear the catchup peer on a transient inactivity stall, this allows the
+ // follower to retry without us losing track of it requiring catchup.
return
case <-nextBatchC:
if !sendNextBatchAndContinue(qch) {
@@ -11193,12 +11260,13 @@ func (mset *stream) runCatchup(sendSubject string, sreq *streamSyncRequest) {
mset.clearCatchupPeer(sreq.Peer)
return
}
- case <-time.After(500 * time.Millisecond):
+ case <-retryTimer.C:
if !sendNextBatchAndContinue(qch) {
mset.clearCatchupPeer(sreq.Peer)
return
}
}
+ retryTimer.Reset(500 * time.Millisecond)
}
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
index be84f7b190..265da58dc5 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/leafnode.go
@@ -304,6 +304,13 @@ func validateLeafNode(o *Options) error {
return fmt.Errorf("remote leaf node configuration cannot have a mix of websocket and non-websocket urls: %q", redactURLList(rcfg.URLs))
}
}
+ if !wsAllowedFIPS() {
+ for _, u := range rcfg.URLs {
+ if isWSURL(u) {
+ return fmt.Errorf("remote leaf node URL %q cannot be used in FIPS-140 mode when built with this Go version, use Go 1.26 or later", redactURLString(u.String()))
+ }
+ }
+ }
// Validate compression settings
if rcfg.Compression.Mode != _EMPTY_ {
if err := validateAndNormalizeCompressionOption(&rcfg.Compression, CompressionS2Auto); err != nil {
@@ -1300,7 +1307,10 @@ func (s *Server) createLeafNode(conn net.Conn, rURL *url.URL, remote *leafNodeCf
info = s.copyLeafNodeInfo()
// For tests that want to simulate old servers, do not set the compression
// on the INFO protocol if configured with CompressionNotSupported.
- if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported {
+ // Also suppress it if WebSocket compression is already in use, otherwise
+ // an old soliciting peer would honor the advertised mode, switch to S2,
+ // and then wait forever for a compressed INFO response from us.
+ if cm := opts.LeafNode.Compression.Mode; cm != CompressionNotSupported && (ws == nil || !ws.compress) {
info.Compression = cm
}
// We always send a nonce for LEAF connections. Do not change that without
@@ -1721,6 +1731,15 @@ func (c *client) processLeafnodeInfo(info *Info) {
}
func (s *Server) negotiateLeafCompression(c *client, didSolicit bool, infoCompression string, co *CompressionOpts) (bool, error) {
+ // If WebSocket compression is already negotiated on this connection then
+ // we shouldn't layer S2 compression on top of it.
+ c.mu.Lock()
+ if c.ws != nil && c.ws.compress {
+ c.leaf.compression = CompressionOff
+ c.mu.Unlock()
+ return false, nil
+ }
+ c.mu.Unlock()
// Negotiate the appropriate compression mode (or no compression)
cm, err := selectCompressionMode(co.Mode, infoCompression)
if err != nil {
@@ -2027,12 +2046,14 @@ func (s *Server) addLeafNodeConnection(c *client, srvName, clusterName string, c
// In an extension use case, pin leadership to server remotes connect to.
// Therefore, server with a remote that are not already in observer mode, need to be put into it.
if solicited && meta != nil && !meta.IsObserver() {
- meta.setObserver(true, extExtended)
c.Debugf("Turning JetStream metadata controller Observer Mode on - System Account Connected")
- // Take note that the domain was not extended to avoid this state next startup.
- writePeerState(js.config.StoreDir, meta.currentPeerState())
- // If this server is the leader already, step down so a new leader can be elected (that is not an observer)
- meta.StepDown()
+ // Discard any local metagroup state accumulated before the SYS-account
+ // leaf came up (e.g. the wrong-hint case where this server bootstrapped
+ // its own metagroup). The parent's view is now authoritative; without
+ // this reset the two raft logs stay forked because the standalone log's
+ // commit prefix short-circuits the follower's AE handling.
+ meta.setObserver(true, extExtended)
+ meta.Reset()
}
} else {
// This deny is needed in all cases (system account shared or not)
@@ -2586,31 +2607,48 @@ func (acc *Account) updateLeafNodesEx(sub *subscription, delta int32, hubOnly bo
// Do this once.
subject := string(sub.subject)
- // Walk the connected leafnodes.
- for _, ln := range acc.lleafs {
+ // Walk the connected leafnodes from a random starting point to avoid
+ // concurrent callers all contending over leafs in the same order.
+ nleafs := len(acc.lleafs)
+ start := 0
+ if nleafs > 1 {
+ start = rand.Intn(nleafs)
+ }
+ for i := 0; i < nleafs; i++ {
+ ln := acc.lleafs[(start+i)%nleafs]
if ln == sub.client {
continue
}
- ln.mu.Lock()
+ ln.mu.RLock()
// Don't advertise interest from leafnodes to other isolated leafnodes.
if sub.client.kind == LEAF && ln.isIsolatedLeafNode() {
- ln.mu.Unlock()
+ ln.mu.RUnlock()
continue
}
// If `hubOnly` is true, it means that we want to update only leafnodes
// that connect to this server (so isHubLeafNode() would return `true`).
if hubOnly && !ln.isHubLeafNode() {
- ln.mu.Unlock()
+ ln.mu.RUnlock()
continue
}
// Check to make sure this sub does not have an origin cluster that matches the leafnode.
// If skipped, make sure that we still let go the "$LDS." subscription that allows
// the detection of loops as long as different cluster.
clusterDifferent := cluster != ln.remoteCluster()
- if (isLDS && clusterDifferent) || ((cluster == _EMPTY_ || clusterDifferent) && (delta <= 0 || ln.canSubscribe(subject))) {
- ln.updateSmap(sub, delta, isLDS)
+ update := (isLDS && clusterDifferent) ||
+ ((cluster == _EMPTY_ || clusterDifferent) && (delta <= 0 || ln.canSubscribeInternal(subject)))
+ ln.mu.RUnlock()
+ if update {
+ ln.mu.Lock()
+ // The leaf role, isolation mode, and remote cluster are stable
+ // for the connection. Recheck canSubscribe here since permissions
+ // can change, and to initializes mperms for wildcard subscriptions
+ // that collide with deny rules.
+ if isLDS || delta <= 0 || ln.canSubscribe(subject) {
+ ln.updateSmap(sub, delta, isLDS)
+ }
+ ln.mu.Unlock()
}
- ln.mu.Unlock()
}
}
@@ -3299,35 +3337,48 @@ func (c *client) leafMsgAllowed() bool {
return true
}
- c.mu.Lock()
- defer c.mu.Unlock()
-
+ c.mu.RLock()
if c.isSpokeLeafNode() {
// Gateway routed replies are forwarded without
// permission checks.
if isGW || c.leafReceiveAllowed(subjectToCheck) {
+ c.mu.RUnlock()
return true
}
} else if c.leafSendAllowed(subjectToCheck) {
+ c.mu.RUnlock()
return true
}
+
+ // If allow_responses is not configured, or there is no tracked reply for
+ // this subject, the answer is "denied" and we can return it while still
+ // holding only the read lock.
+ replySubject := bytesToString(wireSubject)
+ if c.perms == nil || c.perms.resp == nil || c.replies[replySubject] == nil {
+ c.mu.RUnlock()
+ return false
+ }
+ c.mu.RUnlock()
+
// Check tracked reply permissions (allow_responses).
// Use the pre-strip subject since deliverMsg tracks
// replies under the original form, which includes
// the GW routing prefix for routed requests.
- return c.responseAllowed(bytesToString(wireSubject))
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ return c.responseAllowed(replySubject)
}
// Returns true if the leaf side ACLs allow importing this subject,
// based on the permissions received over INFO and any local deny_imports.
-// Lock must be held.
+// At least a read lock must be held.
func (c *client) leafReceiveAllowed(subject []byte) bool {
- return c.canSubscribe(bytesToString(subject))
+ return c.canSubscribeInternal(bytesToString(subject))
}
// Returns true if the hub side ACLs allow the remote leaf to send
// this subject.
-// Lock must be held.
+// At least a read lock must be held.
func (c *client) leafSendAllowed(bsubject []byte) bool {
// Use the original export ACL captured for this accepted leaf.
// The live perms also contain additional JetStream denies used by
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
index 4e3f113b3b..083a98ef79 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/memstore.go
@@ -123,7 +123,7 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
maxp := ms.maxp
ms.maxp = cfg.MaxMsgsPer
// If the value is smaller, or was unset before, we need to enforce that.
- if ms.maxp > 0 && (maxp == 0 || ms.maxp < maxp) {
+ if ms.maxp > 0 && (maxp <= 0 || ms.maxp < maxp) {
lm := uint64(ms.maxp)
ms.fss.IterFast(func(subj []byte, ss *SimpleState) bool {
if ss.Msgs > lm {
@@ -961,7 +961,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
var havePartial bool
var totalSkipped uint64
// We will track start and end sequences as we go.
- stree.IntersectGSL[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) {
+ stree.IntersectGSL[SimpleState](ms.fss, sl, func(subj []byte, fss *SimpleState) bool {
if fss.firstNeedsUpdate || fss.lastNeedsUpdate {
ms.recalculateForSubj(bytesToString(subj), fss)
}
@@ -975,6 +975,7 @@ func (ms *memStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerS
} else {
totalSkipped += fss.Msgs
}
+ return true
})
// If we did not encounter any partials we can return here.
@@ -1426,48 +1427,70 @@ func (ms *memStore) runMsgScheduling() {
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (ms *memStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
+ // sequence == 1 means "purge up to but not including 1", a no-op.
+ if sequence == 1 {
+ return 0, nil
+ }
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return ms.purge(0)
}
if sequence > 1 {
return ms.compact(sequence)
- } else if keep > 0 {
- ms.mu.RLock()
- msgs, lseq := ms.state.Msgs, ms.state.LastSeq
- ms.mu.RUnlock()
- if keep >= msgs {
- return 0, nil
- }
- return ms.compact(lseq - keep + 1)
}
- return 0, nil
-
+ // Make sure to not leave subject if empty.
+ if subject == _EMPTY_ {
+ subject = fwcs
+ }
}
eq := compareFn(subject)
- if ss, _ := ms.FilteredState(1, subject); ss.Msgs > 0 {
- if keep > 0 {
- if keep >= ss.Msgs {
- return 0, nil
- }
- ss.Msgs -= keep
+
+ // FilteredState narrows the search range.
+ ss, _ := ms.FilteredState(1, subject)
+ if ss.Msgs == 0 {
+ return 0, nil
+ }
+ // If we have a "keep" designation need to know how many to purge.
+ var maxp uint64
+ if keep > 0 {
+ if keep >= ss.Msgs {
+ return 0, nil
}
- last := ss.Last
- if sequence > 1 {
- last = sequence - 1
- }
- ms.mu.Lock()
- for seq := ss.First; seq <= last; seq++ {
- if sm, ok := ms.msgs[seq]; ok && eq(sm.subj, subject) {
- if ok := ms.removeMsg(sm.seq, false); ok {
- purged++
- if purged >= ss.Msgs {
- break
- }
+ maxp = ss.Msgs - keep
+ }
+ // "Purge up to but not including sequence": sequence == 0 means no
+ // sequence filter; sequence >= 1 clamps the upper bound to sequence-1
+ // (so sequence == 1 purges nothing).
+ last := ss.Last
+ if sequence >= 1 {
+ last = sequence - 1
+ }
+ var bytes, lowSeq uint64
+ var lowSubj string
+ ms.mu.Lock()
+ for seq := ss.First; seq <= last; seq++ {
+ if sm, ok := ms.msgs[seq]; ok && eq(sm.subj, subject) {
+ if subj, sz, ok := ms.removeMsgNoCB(sm.seq, false); ok {
+ purged++
+ bytes += sz
+ if lowSeq == 0 {
+ lowSeq, lowSubj = sm.seq, subj
+ }
+ if maxp > 0 && purged >= maxp {
+ break
}
}
}
- ms.mu.Unlock()
+ }
+ cb := ms.scb
+ ms.mu.Unlock()
+
+ if cb != nil && purged > 0 {
+ if purged == 1 {
+ cb(-1, -int64(bytes), lowSeq, lowSubj)
+ } else {
+ cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
+ }
}
return purged, nil
}
@@ -2124,20 +2147,38 @@ func (ms *memStore) recalculateForSubj(subj string, ss *SimpleState) {
// Removes the message referenced by seq.
// Lock should be held.
func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
- var ss uint64
- sm, ok := ms.msgs[seq]
+ subj, size, ok := ms.removeMsgNoCB(seq, secure)
if !ok {
return false
}
+ if ms.scb != nil {
+ // We do not want to hold any locks here.
+ ms.mu.Unlock()
+ if ms.scb != nil {
+ ms.scb(-1, -int64(size), seq, subj)
+ }
+ ms.mu.Lock()
+ }
+ return true
+}
- ss = memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
+// Removes the message referenced by seq, but without calling the storage callback.
+// Returns the removed message's subject and size.
+// Lock should be held.
+func (ms *memStore) removeMsgNoCB(seq uint64, secure bool) (subj string, size uint64, ok bool) {
+ sm, ok := ms.msgs[seq]
+ if !ok {
+ return _EMPTY_, 0, false
+ }
+
+ size = memStoreMsgSize(sm.subj, sm.hdr, sm.msg)
if ms.state.Msgs > 0 {
ms.state.Msgs--
- if ss > ms.state.Bytes {
- ss = ms.state.Bytes
+ if size > ms.state.Bytes {
+ size = ms.state.Bytes
}
- ms.state.Bytes -= ss
+ ms.state.Bytes -= size
}
ms.dmap.Insert(seq)
ms.updateFirstSeq(seq)
@@ -2166,17 +2207,7 @@ func (ms *memStore) removeMsg(seq uint64, secure bool) bool {
// Must delete message after updating per-subject info, to be consistent with file store.
delete(ms.msgs, seq)
- if ms.scb != nil {
- // We do not want to hold any locks here.
- ms.mu.Unlock()
- if ms.scb != nil {
- delta := int64(ss)
- ms.scb(-1, -delta, seq, sm.subj)
- }
- ms.mu.Lock()
- }
-
- return ok
+ return sm.subj, size, true
}
// Type returns the type of the underlying store.
@@ -2595,13 +2626,15 @@ func (o *consumerMemStore) UpdateAcks(dseq, sseq uint64) error {
return ErrNoAckPolicy
}
+ // We do this regardless.
+ delete(o.state.Redelivered, sseq)
+
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
- delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
}
@@ -2655,12 +2688,23 @@ func (o *consumerMemStore) UpdateAcks(dseq, sseq uint64) error {
}
}
}
- // We do these regardless.
- delete(o.state.Redelivered, sseq)
return nil
}
+func (o *consumerMemStore) RemoveRedeliveredBelow(seq uint64) {
+ if seq == 0 {
+ return
+ }
+ o.mu.Lock()
+ defer o.mu.Unlock()
+ for s := range o.state.Redelivered {
+ if s < seq {
+ delete(o.state.Redelivered, s)
+ }
+ }
+}
+
func (o *consumerMemStore) UpdateConfig(cfg *ConsumerConfig) error {
o.mu.Lock()
defer o.mu.Unlock()
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
index 6d4460b238..1880f2b4e4 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/monitor.go
@@ -1271,10 +1271,14 @@ type Varz struct {
Routes int `json:"routes"` // Routes is the number of connected route servers
Remotes int `json:"remotes"` // Remotes is the configured route remote endpoints
Leafs int `json:"leafnodes"` // Leafs is the number connected leafnode clients
- InMsgs int64 `json:"in_msgs"` // InMsgs is the number of messages this server received
- OutMsgs int64 `json:"out_msgs"` // OutMsgs is the number of message this server sent
- InBytes int64 `json:"in_bytes"` // InBytes is the number of bytes this server received
- OutBytes int64 `json:"out_bytes"` // OutMsgs is the number of bytes this server sent
+ InMsgs int64 `json:"in_msgs"` // InMsgs is the total number of messages this server received. This includes messages from the clients, routers, gateways and leaf nodes
+ InBytes int64 `json:"in_bytes"` // InBytes is the total number of bytes this server received. This includes messages from the clients, routers, gateways and leaf nodes
+ InClientMsgs int64 `json:"in_client_msgs"` // InClientMsgs is the number of messages this server received from the clients
+ InClientBytes int64 `json:"in_client_bytes"` // InClientBytes is the number of bytes this server received from the clients
+ OutMsgs int64 `json:"out_msgs"` // OutMsgs is the total number of message this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes
+ OutBytes int64 `json:"out_bytes"` // OutBytes is the total number of bytes this server sent. This includes messages sent to the clients, routers, gateways and leaf nodes
+ OutClientMsgs int64 `json:"out_client_msgs"` // OutClientMsgs is the number of messages this server sent to the clients
+ OutClientBytes int64 `json:"out_client_bytes"` // OutClientBytes is the number of bytes this server sent to the clients
SlowConsumers int64 `json:"slow_consumers"` // SlowConsumers is the total count of clients that were disconnected since start due to being slow consumers
StaleConnections int64 `json:"stale_connections"` // StaleConnections is the total count of stale connections that were detected
StalledClients int64 `json:"stalled_clients"` // StalledClients is the total number of times that clients have been stalled.
@@ -1877,6 +1881,10 @@ func (s *Server) updateVarzRuntimeFields(v *Varz, forceUpdate bool, pcpu float64
v.InBytes = atomic.LoadInt64(&s.inBytes)
v.OutMsgs = atomic.LoadInt64(&s.outMsgs)
v.OutBytes = atomic.LoadInt64(&s.outBytes)
+ v.InClientMsgs = atomic.LoadInt64(&s.inClientMsgs)
+ v.InClientBytes = atomic.LoadInt64(&s.inClientBytes)
+ v.OutClientMsgs = atomic.LoadInt64(&s.outClientMsgs)
+ v.OutClientBytes = atomic.LoadInt64(&s.outClientBytes)
v.SlowConsumers = atomic.LoadInt64(&s.slowConsumers)
v.StalledClients = atomic.LoadInt64(&s.stalls)
v.SlowConsumersStats = &SlowConsumersStats{
@@ -2511,7 +2519,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)
s.accounts.Range(func(key, a any) bool {
acc := a.(*Account)
acc.mu.RLock()
- if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 {
+ if (opts != nil && opts.IncludeUnused) || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 {
stz.Accounts = append(stz.Accounts, acc.statz())
}
acc.mu.RUnlock()
@@ -2522,7 +2530,7 @@ func (s *Server) AccountStatz(opts *AccountStatzOptions) (*AccountStatz, error)
if acc, ok := s.accounts.Load(a); ok {
acc := acc.(*Account)
acc.mu.RLock()
- if opts.IncludeUnused || acc.numLocalConnections() != 0 {
+ if opts.IncludeUnused || acc.numLocalConnections() != 0 || acc.numLocalLeafNodes() != 0 {
stz.Accounts = append(stz.Accounts, acc.statz())
}
acc.mu.RUnlock()
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go b/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go
index 7b0e5c2fdb..4ff181b2ad 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/mqtt.go
@@ -239,7 +239,7 @@ var (
errMQTTEmptyUsername = errors.New("empty user name not allowed")
errMQTTTopicIsEmpty = errors.New("topic cannot be empty")
errMQTTPacketIdentifierIsZero = errors.New("packet identifier cannot be 0")
- errMQTTUnsupportedCharacters = errors.New("character ' ' not supported for MQTT topics")
+ errMQTTUnsupportedCharacters = errors.New("character not supported for MQTT topics")
errMQTTInvalidSession = errors.New("invalid MQTT session")
errMQTTInvalidRetainFlags = errors.New("invalid retained message flags")
errMQTTSessionCollision = errors.New("stored session does not match client ID")
@@ -5713,8 +5713,11 @@ func mqttToNATSSubjectConversion(mt []byte, wcOk bool) ([]byte, error) {
}
res = append(res, btsep)
}
- case ' ':
- // As of now, we cannot support ' ' in the MQTT topic/filter.
+ case ' ', '\t', '\n', '\r', '\f':
+ // We cannot support whitespace in the MQTT topic/filter — these
+ // characters would also corrupt the NATS wire protocol when the
+ // subject is forwarded to other connection types (e.g. leaf
+ // nodes) where the resulting control line could be split.
return nil, errMQTTUnsupportedCharacters
case 0x7f:
// SubjectTree uses DEL as an internal pivot marker, so retained
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/raft.go b/vendor/github.com/nats-io/nats-server/v2/server/raft.go
index 5055297e71..5812f33a9b 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/raft.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/raft.go
@@ -22,7 +22,6 @@ import (
"iter"
"math"
"math/rand"
- "net"
"os"
"path/filepath"
"runtime"
@@ -86,6 +85,7 @@ type RaftNode interface {
WaitForStop()
Delete()
IsDeleted() bool
+ Reset()
RecreateInternalSubs() error
IsSystemAccount() bool
GetTrafficAccountName() string
@@ -344,6 +344,7 @@ var (
errNoInternalClient = errors.New("raft: no internal client")
errMembershipChange = errors.New("raft: membership change in progress")
errRemoveLastNode = errors.New("raft: cannot remove the last peer")
+ errPeerNotFound = errors.New("raft: peer not found")
)
// This will bootstrap a raftNode by writing its config into the store directory.
@@ -374,16 +375,13 @@ func (s *Server) bootstrapRaftNode(cfg *RaftConfig, knownPeers []string, allPeer
if gw.Name == cn {
continue
}
- for _, u := range gw.URLs {
- host := u.Hostname()
- // If this is an IP just add one.
- if net.ParseIP(host) != nil {
- ngwps++
- } else {
- addrs, _ := net.LookupHost(host)
- ngwps += len(addrs)
- }
- }
+ // Each configured gateway URL represents one remote endpoint, so
+ // count it as a single peer. We must not resolve the host and add
+ // one per returned address: a hostname on a dual-stack host (e.g.
+ // "localhost" -> 127.0.0.1 + ::1) would then count the same server
+ // multiple times, inflating the expected meta-group size above the
+ // real node count and preventing meta leader election.
+ ngwps += len(gw.URLs)
}
if expected < nrs+ngwps {
@@ -1046,7 +1044,10 @@ func (n *raft) ProposeRemovePeer(peer string) error {
n.RUnlock()
return errMembershipChange
}
-
+ if _, ok := n.peers[peer]; !ok {
+ n.RUnlock()
+ return errPeerNotFound
+ }
if len(n.peers) <= 1 {
n.RUnlock()
return errRemoveLastNode
@@ -1383,6 +1384,11 @@ func (n *raft) installSnapshot(snap *snapshot) error {
return err
}
+ // If installing a snapshot past our commits, clear the cache.
+ if snap.lastIndex > n.commit && len(n.pae) > 0 {
+ n.pae = make(map[uint64]*appendEntry)
+ }
+
var state StreamState
n.wal.FastState(&state)
n.papplied = snap.lastIndex
@@ -1543,12 +1549,20 @@ func (c *checkpoint) InstallSnapshot(data []byte) (uint64, error) {
n.Unlock()
err := writeFileWithSync(c.snapFile, encoded, defaultFilePerms)
n.Lock()
+ // On either failure path, drop the file we just wrote so it doesn't get
+ // picked up by setupLastSnapshot on restart. Skip the remove if it's the
+ // snapshot already adopted into n.snapfile for this term/applied.
if err != nil {
+ if c.snapFile != n.snapfile {
+ os.Remove(c.snapFile)
+ }
// We could set write err here, but if this is a temporary situation, too many open files etc.
// we want to retry and snapshots are not fatal.
return 0, err
} else if !n.snapshotting {
- // The checkpoint can be aborted at any time, don't continue if that happened.
+ if c.snapFile != n.snapfile {
+ os.Remove(c.snapFile)
+ }
return 0, errSnapAborted
}
@@ -1610,6 +1624,9 @@ func termAndIndexFromSnapFile(sn string) (term, index uint64, err error) {
if n, err := fmt.Sscanf(fn, snapFileT, &term, &index); err != nil || n != 2 {
return 0, 0, errBadSnapName
}
+ if fn != fmt.Sprintf(snapFileT, term, index) {
+ return 0, 0, errBadSnapName
+ }
return term, index, nil
}
@@ -2220,6 +2237,64 @@ func (n *raft) shutdown() {
}
}
+// Reset discards this node's local raft state (log, snapshots, peer set,
+// term/vote) so it can be caught up cleanly by another group with the same
+// name. The caller is responsible for parking the node first (typically via
+// SetObserver) if it should not compete for leadership immediately after;
+// Reset itself steps the node down but does not flip observer mode.
+func (n *raft) Reset() {
+ n.Lock()
+ defer n.Unlock()
+
+ n.debug("Resetting Raft state")
+
+ n.stepdownLocked(_EMPTY_)
+
+ // Cancel any in-flight catchup so it does not race the reset.
+ n.cancelCatchup()
+
+ // Drop proposals and inbound entries; they are no longer meaningful
+ // against whatever log this node ends up following.
+ n.prop.drain()
+ n.entry.drain()
+ n.resp.drain()
+ n.apply.drain()
+ n.reqs.drain()
+ n.votes.drain()
+
+ // Remove every snapshot under our snapshots dir, not just the one referenced
+ // by n.snapfile. Orphans (e.g. from a crash between install and the previous
+ // file's removal) would otherwise be picked up by setupLastSnapshot on the
+ // next restart and reseed the state we are discarding here.
+ snapDir := filepath.Join(n.sd, snapshotsDir)
+ if err := os.RemoveAll(snapDir); err != nil {
+ n.warn("Error removing snapshots directory during reset: %v", err)
+ }
+ if err := os.MkdirAll(snapDir, defaultDirPerms); err != nil {
+ n.warn("Error recreating snapshots directory during reset: %v", err)
+ }
+ n.snapfile = _EMPTY_
+
+ // Abort any inflight async snapshot checkpoint.
+ n.snapshotting = false
+
+ // Reset the WAL, but reset these first to not trip the assertion.
+ n.commit, n.hcommit, n.applied, n.processed, n.papplied = 0, 0, 0, 0, 0
+ n.resetWAL()
+
+ // Reset peer set to just ourselves; a new leader will fold us back into
+ // the cluster's membership view via processPeerState.
+ n.peers = map[string]*lps{n.id: {time.Time{}, 0, true}}
+ n.removed = nil
+ n.adjustClusterSizeAndQuorum()
+
+ n.term, n.vote = 0, _EMPTY_
+ n.writeTermVote()
+
+ // Persist the cleared peer state so a restart picks up the reset.
+ n.writePeerState(n.currentPeerStateLocked())
+}
+
const (
raftAllSubj = "$NRG.>"
raftVoteSubj = "$NRG.V.%s"
@@ -2904,6 +2979,16 @@ func (n *raft) handleForwardedRemovePeerProposal(sub *subscription, c *client, _
n.RUnlock()
return
}
+ if _, ok := n.peers[string(msg)]; !ok {
+ n.debug("Ignoring forwarded peer removal proposal, peer not found")
+ n.RUnlock()
+ return
+ }
+ if len(n.peers) <= 1 {
+ n.debug("Ignoring forwarded peer removal proposal, remove last node")
+ n.RUnlock()
+ return
+ }
prop := n.prop
n.RUnlock()
@@ -2966,6 +3051,9 @@ func (n *raft) addPeer(peer string) {
// If we were on the removed list reverse that here.
if n.removed != nil {
delete(n.removed, peer)
+ if len(n.removed) == 0 {
+ n.removed = nil
+ }
}
if lp, ok := n.peers[peer]; !ok {
@@ -3901,6 +3989,19 @@ func (n *raft) truncateWAL(term, index uint64) {
// Set after we know we have truncated properly.
n.pterm, n.pindex = term, index
+ // Invalidate cached entries the WAL no longer has.
+ if index == 0 {
+ if len(n.pae) > 0 {
+ n.pae = make(map[uint64]*appendEntry)
+ }
+ } else {
+ for k := range n.pae {
+ if k > index {
+ delete(n.pae, k)
+ }
+ }
+ }
+
// Check if we're truncating an uncommitted membership change.
if n.membChangeIndex > 0 && n.membChangeIndex > index {
n.membChangeIndex = 0
@@ -4255,13 +4356,10 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
// Inherit state from appendEntry with the leader's snapshot.
hadPreviousSnapshot := n.snapfile != _EMPTY_
- n.pindex = ae.pindex
- n.pterm = ae.pterm
- n.commit = ae.pindex
snap := &snapshot{
- lastTerm: n.pterm,
- lastIndex: n.pindex,
+ lastTerm: ae.pterm,
+ lastIndex: ae.pindex,
peerstate: encodePeerState(&peerState{n.peerNames(), n.csz, n.extSt}),
data: ae.entries[0].Data,
}
@@ -4271,6 +4369,9 @@ func (n *raft) processAppendEntry(ae *appendEntry, sub *subscription) {
n.Unlock()
return
}
+ n.pindex = ae.pindex
+ n.pterm = ae.pterm
+ n.commit = ae.pindex
n.resetInitializing()
if !hadPreviousSnapshot {
@@ -4422,9 +4523,28 @@ func (n *raft) processPeerState(ps *peerState) {
if lp := old[peer]; lp != nil {
lp.kp = true
n.peers[peer] = lp
+ delete(old, peer)
} else {
n.peers[peer] = &lps{time.Time{}, 0, true}
}
+ // If we were on the removed list reverse that here.
+ if n.removed != nil {
+ delete(n.removed, peer)
+ if len(n.removed) == 0 {
+ n.removed = nil
+ }
+ }
+ }
+ // Any remaining old nodes are marked as removed, so they can't be
+ // re-added via automatic peer tracking.
+ if len(old) > 0 {
+ if n.removed == nil {
+ n.removed = map[string]time.Time{}
+ }
+ now := time.Now()
+ for peer := range old {
+ n.removed[peer] = now
+ }
}
n.debug("Update peers from leader to %+v", n.peers)
n.writePeerState(ps)
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/route.go b/vendor/github.com/nats-io/nats-server/v2/server/route.go
index a0384ad35c..26704e93a2 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/route.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/route.go
@@ -2015,7 +2015,7 @@ func (s *Server) createRoute(conn net.Conn, rURL *url.URL, rtype RouteType, goss
pingInterval = opts.Cluster.PingInterval
}
if opts.Cluster.MaxPingsOut > 0 {
- pingMax = opts.MaxPingsOut
+ pingMax = opts.Cluster.MaxPingsOut
}
c.watchForStaleConnection(adjustPingInterval(ROUTER, pingInterval), pingMax)
} else {
@@ -2539,21 +2539,21 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
// queue subscriptions updates (sub/unsub).
// See https://github.com/nats-io/nats-server/pull/1126 for more details.
if isq {
- acc.sqmu.Lock()
+ acc.smu.Lock()
}
acc.mu.Lock()
}
accUnlock := func() {
acc.mu.Unlock()
if isq {
- acc.sqmu.Unlock()
+ acc.smu.Unlock()
}
}
accLock()
// This is non-nil when we know we are in cluster mode.
- rm, lqws := acc.rm, acc.lqws
+ rm, lws := acc.rm, acc.lws
if rm == nil {
accUnlock()
return
@@ -2573,9 +2573,7 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
n += delta
if n <= 0 {
delete(rm, key)
- if isq {
- delete(lqws, key)
- }
+ delete(lws, key)
update = true // Update for deleting (N->0)
} else {
rm[key] = n
@@ -2650,29 +2648,29 @@ func (s *Server) updateRouteSubscriptionMap(acc *Account, sub *subscription, del
trace := atomic.LoadInt32(&s.logging.trace) == 1
s.mu.RUnlock()
- // If we are a queue subscriber we need to make sure our updates are serialized from
- // potential multiple connections. We want to make sure that the order above is preserved
- // here but not necessarily all updates need to be sent. We need to block and recheck the
- // n count with the lock held through sending here. We will suppress duplicate sends of same qw.
- if isq {
- // However, we can't hold the acc.mu lock since we allow client.mu.Lock -> acc.mu.Lock
- // but not the opposite. So use a dedicated lock while holding the route's lock.
- acc.sqmu.Lock()
- defer acc.sqmu.Unlock()
+ // We need to make sure our updates are serialized from potential multiple connections. We want
+ // to make sure that the order above is preserved here but not necessarily all updates need to
+ // be sent. We need to block and recheck the n count with the lock held through sending here.
+ //
+ // However, we can't hold the acc.mu lock since we allow client.mu.Lock -> acc.mu.Lock
+ // but not the opposite. So use a dedicated lock while holding the route's lock.
+ acc.smu.Lock()
+ defer acc.smu.Unlock()
- acc.mu.Lock()
- n = rm[key]
+ acc.mu.Lock()
+ n = rm[key]
+ if isq {
sub.qw = n
- // Check the last sent weight here. If same, then someone
- // beat us to it and we can just return here. Otherwise update
- if ls, ok := lqws[key]; ok && ls == n {
- acc.mu.Unlock()
- return
- } else if n > 0 {
- lqws[key] = n
- }
- acc.mu.Unlock()
}
+ // Check the last sent value here. If same, then someone beat us to it and
+ // we can just return here. Otherwise update.
+ if ls, ok := lws[key]; ok && ls == n {
+ acc.mu.Unlock()
+ return
+ } else if n > 0 {
+ lws[key] = n
+ }
+ acc.mu.Unlock()
// Snapshot into array
subs := []*subscription{sub}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/server.go b/vendor/github.com/nats-io/nats-server/v2/server/server.go
index 9612c868c1..43823e0782 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/server.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/server.go
@@ -402,9 +402,13 @@ type nodeInfo struct {
type stats struct {
inMsgs int64
- outMsgs int64
inBytes int64
+ inClientMsgs int64
+ inClientBytes int64
+ outMsgs int64
outBytes int64
+ outClientMsgs int64
+ outClientBytes int64
slowConsumers int64
staleConnections int64
stalls int64
@@ -1964,7 +1968,7 @@ func (s *Server) registerAccountNoLock(acc *Account) *Account {
// TODO(dlc)- Double check that we need this for GWs.
if acc.rm == nil && s.opts != nil && s.shouldTrackSubscriptions() {
acc.rm = make(map[string]int32)
- acc.lqws = make(map[string]int32)
+ acc.lws = make(map[string]int32)
}
acc.srv = s
acc.updated = time.Now()
@@ -2555,6 +2559,10 @@ func (s *Server) Shutdown() {
if s == nil {
return
}
+ // Prevent issues with multiple calls.
+ if !s.shutdown.CompareAndSwap(false, true) {
+ return
+ }
// This is for JetStream R1 Pull Consumers to allow signaling
// that pending pull requests are invalid.
s.signalPullConsumers()
@@ -2568,11 +2576,6 @@ func (s *Server) Shutdown() {
// eventing items associated with accounts.
s.shutdownEventing()
- // Prevent issues with multiple calls.
- if s.isShuttingDown() {
- return
- }
-
s.mu.Lock()
s.Noticef("Initiating Shutdown...")
@@ -2580,7 +2583,6 @@ func (s *Server) Shutdown() {
opts := s.getOpts()
- s.shutdown.Store(true)
s.running.Store(false)
s.grMu.Lock()
s.grRunning = false
@@ -3411,8 +3413,12 @@ func (s *Server) createClientEx(conn net.Conn, inProcess bool) *client {
}
}
- // Check for proxy protocol if enabled.
- if !isClosed && !tlsRequired && opts.ProxyProtocol {
+ // Check for proxy protocol if enabled. The PROXY header is sent as
+ // plaintext before any TLS handshake per the spec, so we must read it
+ // before doing TLS even when TLS is required. Any bytes read past the
+ // header are kept in `pre` and replayed into the TLS handshake (or the
+ // non-TLS protocol parser) by the tlsMixConn wrapper used below.
+ if !isClosed && opts.ProxyProtocol {
if len(pre) == 0 {
// There has been no pre-read yet, do so so we can work out
// if the client is trying to negotiate PROXY.
@@ -3640,7 +3646,11 @@ func tlsTimeout(c *client, conn *tls.Conn) {
}
cs := conn.ConnectionState()
if !cs.HandshakeComplete {
- c.Errorf("TLS handshake timeout")
+ if c.kind == CLIENT || c.kind == LEAF {
+ c.Debugf("TLS handshake timeout")
+ } else {
+ c.Errorf("TLS handshake timeout")
+ }
c.sendErr("Secure Connection - TLS Required")
c.closeConnection(TLSHandshakeError)
}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/store.go b/vendor/github.com/nats-io/nats-server/v2/server/store.go
index 31a834efac..f30eddcddc 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/store.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/store.go
@@ -364,6 +364,7 @@ type ConsumerStore interface {
HasState() bool
UpdateDelivered(dseq, sseq, dc uint64, ts int64) error
UpdateAcks(dseq, sseq uint64) error
+ RemoveRedeliveredBelow(seq uint64)
UpdateConfig(cfg *ConsumerConfig) error
Update(*ConsumerState) error
ForceUpdate(*ConsumerState) error
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stream.go b/vendor/github.com/nats-io/nats-server/v2/server/stream.go
index 5698f3cb2a..649688ec7b 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/stream.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/stream.go
@@ -264,7 +264,7 @@ type PubAck struct {
Duplicate bool `json:"duplicate,omitempty"`
Value string `json:"val,omitempty"`
BatchId string `json:"batch,omitempty"`
- BatchSize int `json:"count,omitempty"`
+ BatchSize uint64 `json:"count,omitempty"`
}
// CounterValue is the body of a message when used as a counter.
@@ -571,6 +571,7 @@ type stream struct {
mirrorLastBySub *subscription // Mirrors only.
monitorWg sync.WaitGroup // Wait group for the monitor routine.
+ monitorMu sync.Mutex // Serializes monitorWg's Add against Wait to prevent a WaitGroup reuse panic.
// If standalone/single-server, the offline reason needs to be stored directly in the stream.
// Otherwise, if clustered it will be part of the stream assignment.
@@ -582,8 +583,9 @@ type stream struct {
// inflightSubjectRunningTotal stores a running total of inflight messages for a specific subject.
type inflightSubjectRunningTotal struct {
- bytes uint64 // Running total of inflight bytes for inflight messages.
- ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total.
+ bytes uint64 // Running total of inflight bytes for inflight messages.
+ ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total.
+ schedule bool // Marks whether the last message is a schedule.
}
// msgCounterRunningTotal stores a running total and a number of inflight
@@ -816,7 +818,7 @@ func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileSt
if isClustered {
_, reserved = js.tieredStreamAndReservationCount(a.Name, tier, cfg)
}
- if err := js.checkAllLimits(&selected, cfg, reserved, 0); err != nil {
+ if err := js.checkAllLimits(&selected, tier, cfg, reserved, 0); err != nil {
js.mu.RUnlock()
return nil, err
}
@@ -1750,6 +1752,9 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo
if cfg.AllowMsgTTL {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message TTLs"))
}
+ if cfg.AllowMsgSchedules {
+ return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message schedules"))
+ }
if cfg.Retention != LimitsPolicy {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream can only use limits retention"))
}
@@ -1786,6 +1791,9 @@ func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic boo
}
if cfg.AllowMsgSchedules {
+ if cfg.Discard == DiscardNew {
+ return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot use discard new"))
+ }
if !cfg.AllowRollup {
if pedantic {
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot be set if roll-ups are disabled"))
@@ -2340,11 +2348,10 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan
// Save the user configured MaxBytes.
newMaxBytes := cfg.MaxBytes
- maxBytesOffset := int64(0)
// We temporarily set cfg.MaxBytes to maxBytesDiff because checkAllLimits
// adds cfg.MaxBytes to the current reserved limit and checks if we've gone
- // over. However, we don't want an addition cfg.MaxBytes, we only want to
+ // over. However, we don't want an additional cfg.MaxBytes, we only want to
// reserve the difference between the new and the old values.
cfg.MaxBytes = maxBytesDiff
@@ -2371,15 +2378,13 @@ func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedan
if isClustered {
_, reserved = js.tieredStreamAndReservationCount(acc.Name, tier, &cfg)
}
- // reservation does not account for this stream, hence add the old value
- if old.MaxBytes > 0 {
- if tier == _EMPTY_ && old.Replicas > 1 {
- reserved = addSaturate(reserved, mulSaturate(int64(old.Replicas), old.MaxBytes))
- } else {
- reserved = addSaturate(reserved, old.MaxBytes)
- }
- }
- if err := js.checkAllLimits(&selected, &cfg, reserved, maxBytesOffset); err != nil {
+ // reserved covers only the other streams. checkAllLimits adds this stream's
+ // footprint via cfg.MaxBytes, which is currently maxBytesDiff, so it only
+ // adds the diff. Add the remaining (newMaxBytes - maxBytesDiff) here so the
+ // two together equal this stream's true new footprint, even when Replicas
+ // changes on update.
+ reserved = addSaturate(reserved, accountReservation(tier, cfg.Replicas, newMaxBytes-maxBytesDiff))
+ if err := js.checkAllLimits(&selected, tier, &cfg, reserved, 0); err != nil {
return nil, err
}
// Restore the user configured MaxBytes.
@@ -3018,14 +3023,20 @@ func (mset *stream) retryDisconnectedSyncConsumers() {
clientClosed := func(c *client) bool {
return c != nil && (c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed))
}
- // Stale sources need to be reset: we expect a heartbeat every sourceHealthHB, so missing a couple
- // is a strong signal the remote delivery is no longer reaching us and a retry is warranted.
+ // Stale sources need to be reset: if not seen past the health check interval, it's stale.
stale := func(si *sourceInfo) bool {
- return time.Since(time.Unix(0, si.last.Load())) > 2*sourceHealthHB
+ return time.Since(time.Unix(0, si.last.Load())) > sourceHealthCheckInterval
}
shouldRetry := func(si *sourceInfo) bool {
- if si != nil && (si.sip || si.sub == nil || clientClosed(si.sub.client) || stale(si)) {
- si.fails, si.sip = 0, false
+ if si != nil && !si.sip && (si.sub == nil || clientClosed(si.sub.client) || stale(si)) {
+ // Skip if a recreate is already scheduled and we can't cancel it.
+ if t, ok := mset.sourceSetupSchedules[si.iname]; ok {
+ if !t.Stop() {
+ return false
+ }
+ delete(mset.sourceSetupSchedules, si.iname)
+ }
+ si.fails = 0
mset.cancelSourceInfo(si)
return true
}
@@ -3205,7 +3216,13 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
} else {
// If the deliver sequence matches then the upstream stream has expired or deleted messages.
if dseq == mset.mirror.dseq+1 {
- mset.skipMsgs(mset.mirror.sseq+1, sseq-1)
+ if err := mset.skipMsgs(mset.mirror.sseq+1, sseq-1); err != nil {
+ mset.mirror.sseq = osseq
+ mset.mirror.dseq = odseq
+ mset.mu.Unlock()
+ mset.retryMirrorConsumer()
+ return false
+ }
mset.mirror.dseq++
mset.mirror.sseq = sseq
} else {
@@ -3263,7 +3280,7 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
if err != nil {
if strings.Contains(err.Error(), "no space left") {
s.Errorf("JetStream out of space, will be DISABLED")
- s.DisableJetStream()
+ s.ShutdownJetStream()
return false
}
if err != errLastSeqMismatch {
@@ -3274,20 +3291,18 @@ func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
accName, sname, err)
} else {
// We may have missed messages, restart.
- if lseq := mset.lastSeq(); sseq <= lseq {
- mset.mu.Lock()
+ lseq := mset.lastSeq()
+ mset.mu.Lock()
+ if mset.mirror != nil {
mset.mirror.lag = olag
- mset.mirror.sseq = lseq
- mset.mirror.dseq = odseq
- mset.mu.Unlock()
- return false
- } else {
- mset.mu.Lock()
mset.mirror.dseq = odseq
mset.mirror.sseq = osseq
- mset.mu.Unlock()
- mset.retryMirrorConsumer()
+ if sseq <= lseq {
+ mset.mirror.sseq = lseq
+ }
}
+ mset.mu.Unlock()
+ mset.retryMirrorConsumer()
}
}
return err == nil
@@ -3324,20 +3339,21 @@ func (mset *stream) retryMirrorConsumer() error {
}
// Lock should be held.
-func (mset *stream) skipMsgs(start, end uint64) {
+func (mset *stream) skipMsgs(start, end uint64) error {
node, store := mset.node, mset.store
// If we are not clustered we can short circuit now with store.SkipMsgs
if node == nil {
- store.SkipMsgs(start, end-start+1)
+ if err := store.SkipMsgs(start, end-start+1); err != nil {
+ return err
+ }
mset.lseq = end
- return
+ return nil
}
// Must only be enabled once every peer in the cluster supports receiving
// deleteRangeOp in the normal apply path; older peers panic on unknown ops.
if mset.srv.getOpts().getFeatureFlag(FeatureFlagJsRaftDeleteRange) {
- node.Propose(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1}))
- return
+ return node.Propose(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1}))
}
var entries []*Entry
@@ -3345,7 +3361,9 @@ func (mset *stream) skipMsgs(start, end uint64) {
entries = append(entries, newEntry(EntryNormal, encodeStreamMsg(_EMPTY_, _EMPTY_, nil, nil, seq-1, 0, false)))
// So a single message does not get too big.
if len(entries) > 10_000 {
- node.ProposeMulti(entries)
+ if err := node.ProposeMulti(entries); err != nil {
+ return err
+ }
// We need to re-create `entries` because there is a reference
// to it in the node's pae map.
entries = entries[:0]
@@ -3353,8 +3371,9 @@ func (mset *stream) skipMsgs(start, end uint64) {
}
// Send all at once.
if len(entries) > 0 {
- node.ProposeMulti(entries)
+ return node.ProposeMulti(entries)
}
+ return nil
}
const (
@@ -3718,13 +3737,25 @@ func (mset *stream) setupMirrorConsumer() error {
state = StreamState{}
mset.store.FastState(&state)
if state.LastSeq < ccr.ConsumerInfo.Delivered.Stream {
+ // Local helper: abort consumer setup, leaving the mirror in its
+ // pre-setup state so the retry path can re-create it cleanly.
+ failSetup := func(setupErr error) {
+ mset.cancelSourceInfo(mirror)
+ mirror.err = NewJSMirrorConsumerSetupFailedError(setupErr, Unless(setupErr))
+ retry = true
+ mset.mu.Unlock()
+ }
// Check to see if delivered is past our last and we have no msgs. This will help the
// case when mirroring a stream that has a very high starting sequence number.
if state.Msgs == 0 && ccr.ConsumerInfo.Delivered.Stream > state.LastSeq {
- mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0)
+ if _, err := mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0); err != nil {
+ failSetup(err)
+ return
+ }
mset.lseq = ccr.ConsumerInfo.Delivered.Stream
- } else {
- mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream)
+ } else if err := mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream); err != nil {
+ failSetup(err)
+ return
}
}
@@ -4399,7 +4430,7 @@ func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool {
s := mset.srv
if strings.Contains(err.Error(), "no space left") {
s.Errorf("JetStream out of space, will be DISABLED")
- s.DisableJetStream()
+ s.ShutdownJetStream()
} else {
mset.mu.RLock()
accName, sname, iName := mset.acc.Name, mset.cfg.Name, si.iname
@@ -5019,10 +5050,14 @@ func (mset *stream) deleteAtomicBatches(shuttingDown bool) {
// Lock should be held.
func (mset *stream) deleteBatchApplyState() {
if batch := mset.batchApply; batch != nil {
- // Need to return entries (if any) to the pool.
+ // Clear under batch.mu so a stale reference held by the stream monitor
+ // can't re-pool entries we already returned.
+ batch.mu.Lock()
for _, bce := range batch.entries {
bce.ReturnToPool()
}
+ batch.clearBatchStateLocked()
+ batch.mu.Unlock()
mset.batchApply = nil
}
}
@@ -5176,9 +5211,12 @@ func (mset *stream) storeUpdates(md, bd int64, seq uint64, subj string) {
mset.clsMu.RUnlock()
} else if md < 0 {
// Batch decrements we need to force consumers to re-calculate num pending.
+ var ss StreamState
+ mset.store.FastState(&ss)
mset.clsMu.RLock()
for _, o := range mset.cList {
o.streamNumPendingLocked()
+ o.removeRedeliveredBelow(ss.FirstSeq)
}
mset.clsMu.RUnlock()
}
@@ -5558,15 +5596,14 @@ func getFastBatch(reply string, hdr []byte) (*FastBatch, bool) {
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
return nil, true
}
- a := parseInt64(stringToBytes(reply[o+1 : p]))
- if a < 1 {
+ seq, ok := parseUint64(stringToBytes(reply[o+1 : p]))
+ // Reject math.MaxUint64 to prevent b.lseq overflowing on the next b.lseq++.
+ if !ok || seq == 0 || seq == math.MaxUint64 {
return nil, true
}
- b.seq = uint64(a)
+ b.seq = seq
p = o
- if b.seq <= 0 {
- return nil, true
- } else if b.seq == 1 && b.commitEob {
+ if b.seq == 1 && b.commitEob {
return nil, true
}
if op == FastBatchOpStart && b.seq != 1 {
@@ -5590,7 +5627,7 @@ func getFastBatch(reply string, hdr []byte) (*FastBatch, bool) {
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
return nil, true
}
- a = parseInt64(stringToBytes(reply[o+1 : p]))
+ a := parseInt64(stringToBytes(reply[o+1 : p]))
if a <= 0 {
a = 10
} else if a > math.MaxUint16 {
@@ -5613,7 +5650,7 @@ func getBatchSequence(hdr []byte) (uint64, bool) {
if len(bseq) == 0 {
return 0, false
}
- return uint64(parseInt64(bseq)), true
+ return parseUint64(bseq)
}
// Signal if we are clustered. Will acquire rlock.
@@ -6528,6 +6565,23 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
outq.sendMsg(reply, b)
}
return apiErr
+ } else {
+ // Check that the to-be-purged subject is a schedule message.
+ // We still allow this message through if there exists no message for this subject,
+ // to remain backward-compatible. An "expected at sequence" check can still be
+ // performed to make this stricter.
+ var smv StoreMsg
+ sm, _ := store.LoadLastMsg(bytesToString(scheduler), &smv)
+ if sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0 {
+ apiErr := NewJSMessageSchedulesSchedulerInvalidError()
+ if canRespond {
+ resp.PubAck = &PubAck{Stream: name}
+ resp.Error = apiErr
+ b, _ := json.Marshal(resp)
+ outq.sendMsg(reply, b)
+ }
+ return apiErr
+ }
}
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
@@ -6842,6 +6896,10 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
var thdrsOnly bool
if mset.tr != nil {
tsubj, _ = mset.tr.Match(subject)
+ if tsubj != _EMPTY_ && !IsValidPublishSubject(tsubj) {
+ s.RateLimitWarnf("Stream '%s > %s' suppressing republish with invalid subject %q", accName, name, tsubj)
+ tsubj = _EMPTY_ // ... stops the republish.
+ }
if mset.cfg.RePublish != nil {
thdrsOnly = mset.cfg.RePublish.HeadersOnly
}
@@ -6910,7 +6968,7 @@ func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg
if isPermissionError(err) {
// messages in block cache could be lost in the worst case.
// In the clustered mode it is very highly unlikely as a result of replication.
- go mset.srv.DisableJetStream()
+ go mset.srv.ShutdownJetStream()
mset.srv.Warnf("Filesystem permission denied while writing msg, disabling JetStream: %v", err)
return err
}
@@ -8162,10 +8220,7 @@ func (mset *stream) resetAndWaitOnConsumers() {
node.StepDown()
node.Stop()
}
- if o.isMonitorRunning() {
- o.signalMonitorQuit()
- o.monitorWg.Wait()
- }
+ o.stopMonitoring()
}
}
@@ -8269,8 +8324,7 @@ func (mset *stream) stop(deleteFlag, advisory bool) error {
// but should we log?
o.stopWithFlags(deleteFlag, deleteFlag, false, advisory)
if !isShuttingDown {
- o.signalMonitorQuit()
- o.monitorWg.Wait()
+ o.stopMonitoring()
}
}
}
@@ -8515,14 +8569,16 @@ func (mset *stream) setConsumer(o *consumer) {
// Lock should be held.
func (mset *stream) removeConsumer(o *consumer) {
- if o.cfg.FilterSubject != _EMPTY_ && mset.numFilter > 0 {
- mset.numFilter--
- }
- if (o.cfg.Direct || o.cfg.Sourcing) && mset.sourcingConsumers > 0 {
- mset.sourcingConsumers--
- }
- if mset.consumers != nil {
+ if _, ok := mset.consumers[o.name]; ok {
delete(mset.consumers, o.name)
+
+ if o.cfg.FilterSubject != _EMPTY_ && mset.numFilter > 0 {
+ mset.numFilter--
+ }
+ if (o.cfg.Direct || o.cfg.Sourcing) && mset.sourcingConsumers > 0 {
+ mset.sourcingConsumers--
+ }
+
// Now update consumers list as well
mset.clsMu.Lock()
for i, ol := range mset.cList {
@@ -9000,7 +9056,7 @@ func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error
}
bc += hdr.Size
js.mu.RLock()
- err = js.checkAllLimits(&selected, &cfg, reserved, bc)
+ err = js.checkAllLimits(&selected, tier, &cfg, reserved, bc)
js.mu.RUnlock()
if err != nil {
return nil, err
@@ -9176,6 +9232,28 @@ func (mset *stream) checkConsumerReplication() {
}
}
+// startMonitorWg registers a pending monitor goroutine on monitorWg. It is
+// held under monitorMu so that the monitorWg.Add can never race a concurrent
+// monitorWg.Wait in stopMonitoring. The corresponding monitorWg.Done is done by
+// the monitor goroutine directly and must not be wrapped with monitorMu.
+func (mset *stream) startMonitorWg() {
+ mset.monitorMu.Lock()
+ mset.monitorWg.Add(1)
+ mset.monitorMu.Unlock()
+}
+
+// stopMonitoring signals any running monitor goroutine to quit and waits for
+// it to fully exit.
+func (mset *stream) stopMonitoring() {
+ // monitorMu is held across both the quit signal and the wait so that a
+ // concurrent startMonitorWg cannot slip a new monitor generation in
+ // between.
+ mset.monitorMu.Lock()
+ defer mset.monitorMu.Unlock()
+ mset.signalMonitorQuit()
+ mset.monitorWg.Wait()
+}
+
// Will check if we are running in the monitor already and if not set the appropriate flag.
func (mset *stream) checkInMonitor() bool {
mset.mu.Lock()
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go b/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go
index 00ebdc46f2..0c257435dc 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/stree/stree.go
@@ -485,7 +485,7 @@ func LazyIntersect[TL, TR any](tl *SubjectTree[TL], tr *SubjectTree[TR], cb func
// IntersectGSL will match all items in the given subject tree that
// have interest expressed in the given sublist. The callback will only be called
// once for each subject, regardless of overlapping subscriptions in the sublist.
-func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T)) {
+func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T) bool) {
if t == nil || t.root == nil || sl == nil {
return
}
@@ -493,14 +493,14 @@ func IntersectGSL[T any, SL comparable](t *SubjectTree[T], sl *gsl.GenericSublis
_intersectGSL(t.root, _pre[:0], sl, cb)
}
-func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T)) {
+func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSublist[SL], cb func(subject []byte, val *T) bool) bool {
if n.isLeaf() {
ln := n.(*leaf[T])
subj := append(pre, ln.suffix...)
if sl.HasInterest(bytesToString(subj)) {
- cb(subj, &ln.value)
+ return cb(subj, &ln.value)
}
- return
+ return true
}
bn := n.base()
pre = append(pre, bn.prefix...)
@@ -512,8 +512,11 @@ func _intersectGSL[T any, SL comparable](n node, pre []byte, sl *gsl.GenericSubl
if !hasInterestForTokens(sl, subj, len(pre)) {
continue
}
- _intersectGSL(cn, pre, sl, cb)
+ if !_intersectGSL(cn, pre, sl, cb) {
+ return false
+ }
}
+ return true
}
// The subject tree can return partial tokens so we need to check starting interest
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/sublist.go b/vendor/github.com/nats-io/nats-server/v2/server/sublist.go
index 7423be0b20..8428f9dfbe 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/sublist.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/sublist.go
@@ -638,9 +638,10 @@ func (s *Sublist) hasInterest(subject string, doLock bool, np, nq *int) bool {
if doLock {
s.RLock()
}
- var matched bool
+ var matched, ok bool
if s.cache != nil {
- if r, ok := s.cache[subject]; ok {
+ var r *SublistResult
+ if r, ok = s.cache[subject]; ok {
if np != nil && nq != nil {
*np += len(r.psubs)
for _, qsub := range r.qsubs {
@@ -653,9 +654,9 @@ func (s *Sublist) hasInterest(subject string, doLock bool, np, nq *int) bool {
if doLock {
s.RUnlock()
}
- if matched {
+ if ok {
atomic.AddUint64(&s.cacheHits, 1)
- return true
+ return matched
}
tsa := [32]string{}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/util.go b/vendor/github.com/nats-io/nats-server/v2/server/util.go
index ff90838bb8..f2c5f80d39 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/util.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/util.go
@@ -124,6 +124,26 @@ func parseInt64(d []byte) (n int64) {
return n
}
+// parseUint64 expects decimal positive numbers. Returns the value and true on success,
+// or 0 and false on invalid input or overflow.
+func parseUint64(d []byte) (uint64, bool) {
+ if len(d) == 0 {
+ return 0, false
+ }
+ var n uint64
+ for _, dec := range d {
+ if dec < asciiZero || dec > asciiNine {
+ return 0, false
+ }
+ digit := uint64(dec) - asciiZero
+ if n > math.MaxUint64/10 || (n == math.MaxUint64/10 && digit > math.MaxUint64%10) {
+ return 0, false
+ }
+ n = n*10 + digit
+ }
+ return n, true
+}
+
// Helper to move from float seconds to time.Duration
func secondsToDuration(seconds float64) time.Duration {
ttl := seconds * float64(time.Second)
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/websocket.go b/vendor/github.com/nats-io/nats-server/v2/server/websocket.go
index 0ee1ca7db9..5425d67fc4 100644
--- a/vendor/github.com/nats-io/nats-server/v2/server/websocket.go
+++ b/vendor/github.com/nats-io/nats-server/v2/server/websocket.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2025 The NATS Authors
+// Copyright 2020-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,7 +16,6 @@ package server
import (
"bytes"
crand "crypto/rand"
- "crypto/sha1"
"crypto/tls"
"encoding/base64"
"encoding/binary"
@@ -1107,15 +1106,6 @@ func wsGetHostAndPort(tls bool, hostport string) (string, string, error) {
return strings.ToLower(host), port, err
}
-// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
-// and returns it as a based64 encoded string.
-func wsAcceptKey(key string) string {
- h := sha1.New()
- h.Write([]byte(key))
- h.Write(wsGUID)
- return base64.StdEncoding.EncodeToString(h.Sum(nil))
-}
-
func wsMakeChallengeKey() (string, error) {
p := make([]byte, 16)
if _, err := io.ReadFull(crand.Reader, p); err != nil {
@@ -1131,6 +1121,9 @@ func validateWebsocketOptions(o *Options) error {
if wo.Port == 0 {
return nil
}
+ if !wsAllowedFIPS() {
+ return fmt.Errorf("websocket: cannot be used in FIPS-140 mode when built with this Go version, use Go 1.26 or later")
+ }
// Enforce TLS... unless NoTLS is set to true.
if wo.TLSConfig == nil && !wo.NoTLS {
return errors.New("websocket requires TLS configuration")
@@ -1560,10 +1553,11 @@ func (c *client) wsCollapsePtoNB() (net.Buffers, int64) {
if mask {
wsMaskBuf(key, p[:lp])
}
- bufs = append(bufs, fh[:n], p[:lp])
+ bufs = append(bufs, fh[:n], append(nbPoolGet(lp), p[:lp]...))
csz += n + lp
p = p[lp:]
}
+ nbPoolPut(b)
} else {
ol := len(p)
h, key := wsCreateFrameHeader(mask, true, wsBinaryMessage, ol)
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/websocket_go125.go b/vendor/github.com/nats-io/nats-server/v2/server/websocket_go125.go
new file mode 100644
index 0000000000..3bbdfe972c
--- /dev/null
+++ b/vendor/github.com/nats-io/nats-server/v2/server/websocket_go125.go
@@ -0,0 +1,38 @@
+// Copyright 2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !go1.26
+
+package server
+
+import (
+ "crypto/fips140"
+ "crypto/sha1"
+ "encoding/base64"
+)
+
+func wsAllowedFIPS() bool {
+ // SHA-1 is not permitted on Go 1.25 FIPS builds because we cannot avoid its
+ // enforcement for Sec-WebSocket-Key and Sec-WebSocket-Accept, it will result
+ // in a panic.
+ return !fips140.Enabled()
+}
+
+// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
+// and returns it as a based64 encoded string.
+func wsAcceptKey(key string) string {
+ h := sha1.New()
+ h.Write([]byte(key))
+ h.Write(wsGUID)
+ return base64.StdEncoding.EncodeToString(h.Sum(nil))
+}
diff --git a/vendor/github.com/nats-io/nats-server/v2/server/websocket_go126.go b/vendor/github.com/nats-io/nats-server/v2/server/websocket_go126.go
new file mode 100644
index 0000000000..d6f38634e3
--- /dev/null
+++ b/vendor/github.com/nats-io/nats-server/v2/server/websocket_go126.go
@@ -0,0 +1,42 @@
+// Copyright 2026 The NATS Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build go1.26
+
+package server
+
+import (
+ "crypto/fips140"
+ "crypto/sha1"
+ "encoding/base64"
+)
+
+func wsAllowedFIPS() bool {
+ // As SHA-1 is only used for Sec-WebSocket-Key and Sec-WebSocket-Accept, we
+ // can continue to allow it in FIPS builds as long as they are built with
+ // Go 1.26 or later only.
+ return true
+}
+
+// Concatenate the key sent by the client with the GUID, then computes the SHA1 hash
+// and returns it as a based64 encoded string.
+func wsAcceptKey(key string) string {
+ var r []byte
+ fips140.WithoutEnforcement(func() {
+ h := sha1.New()
+ h.Write([]byte(key))
+ h.Write(wsGUID)
+ r = h.Sum(nil)
+ })
+ return base64.StdEncoding.EncodeToString(r)
+}
diff --git a/vendor/github.com/nats-io/nkeys/nkeys.go b/vendor/github.com/nats-io/nkeys/nkeys.go
index 6f1ba20509..968b5e9662 100644
--- a/vendor/github.com/nats-io/nkeys/nkeys.go
+++ b/vendor/github.com/nats-io/nkeys/nkeys.go
@@ -19,7 +19,7 @@ package nkeys
import "io"
// Version is our current version
-const Version = "0.4.7"
+const Version = "0.4.16"
// KeyPair provides the central interface to nkeys.
type KeyPair interface {
@@ -66,15 +66,11 @@ func CreateOperator() (KeyPair, error) {
// FromPublicKey will create a KeyPair capable of verifying signatures.
func FromPublicKey(public string) (KeyPair, error) {
- raw, err := decode([]byte(public))
+ prefix, key, err := decodePublicKey(public)
if err != nil {
return nil, err
}
- pre := PrefixByte(raw[0])
- if err := checkValidPublicPrefixByte(pre); err != nil {
- return nil, ErrInvalidPublicKey
- }
- return &pub{pre, raw[1:]}, nil
+ return &pub{prefix, key}, nil
}
// FromSeed will create a KeyPair capable of signing and verifying signatures.
diff --git a/vendor/github.com/nats-io/nkeys/public.go b/vendor/github.com/nats-io/nkeys/public.go
index a6e88c9ba5..ac1bf60ea8 100644
--- a/vendor/github.com/nats-io/nkeys/public.go
+++ b/vendor/github.com/nats-io/nkeys/public.go
@@ -52,6 +52,9 @@ func (p *pub) Sign(input []byte) ([]byte, error) {
// Verify will verify the input against a signature utilizing the public key.
func (p *pub) Verify(input []byte, sig []byte) error {
+ if len(p.pub) != ed25519.PublicKeySize {
+ return ErrInvalidPublicKey
+ }
if !ed25519.Verify(p.pub, input, sig) {
return ErrInvalidSignature
}
diff --git a/vendor/github.com/nats-io/nkeys/strkey.go b/vendor/github.com/nats-io/nkeys/strkey.go
index 8ae33116c0..5ccd75b719 100644
--- a/vendor/github.com/nats-io/nkeys/strkey.go
+++ b/vendor/github.com/nats-io/nkeys/strkey.go
@@ -15,6 +15,7 @@ package nkeys
import (
"bytes"
+ "crypto/ed25519"
"encoding/base32"
"encoding/binary"
)
@@ -184,6 +185,9 @@ func DecodeSeed(src []byte) (PrefixByte, []byte, error) {
if checkValidPublicPrefixByte(PrefixByte(b2)) != nil {
return PrefixByteSeed, nil, ErrInvalidSeed
}
+ if len(raw[2:]) != seedLen {
+ return PrefixByteSeed, nil, ErrInvalidSeed
+ }
return PrefixByte(b2), raw[2:], nil
}
@@ -208,14 +212,23 @@ func Prefix(src string) PrefixByte {
// IsValidPublicKey will decode and verify that the string is a valid encoded public key.
func IsValidPublicKey(src string) bool {
- b, err := decode([]byte(src))
+ _, _, err := decodePublicKey(src)
+ return err == nil
+}
+
+func decodePublicKey(public string) (PrefixByte, []byte, error) {
+ raw, err := decode([]byte(public))
if err != nil {
- return false
+ return PrefixByteUnknown, nil, err
}
- if prefix := PrefixByte(b[0]); checkValidPublicPrefixByte(prefix) != nil {
- return false
+ pre := PrefixByte(raw[0])
+ if err := checkValidPublicPrefixByte(pre); err != nil {
+ return PrefixByteUnknown, nil, ErrInvalidPublicKey
}
- return true
+ if len(raw[1:]) != ed25519.PublicKeySize {
+ return PrefixByteUnknown, nil, ErrInvalidPublicKey
+ }
+ return pre, raw[1:], nil
}
// IsValidPublicUserKey will decode and verify the string is a valid encoded Public User Key.
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json
index 9eb82c2968..30cef5a5d1 100644
--- a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.1.json
@@ -4913,4 +4913,4 @@
"rego_v1",
"template_strings"
]
-}
+}
\ No newline at end of file
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json
similarity index 99%
rename from vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json
rename to vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json
index 9eb82c2968..b47b68d612 100644
--- a/vendor/github.com/open-policy-agent/opa/capabilities/v1.15.2.json
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.0.json
@@ -4700,6 +4700,42 @@
"type": "function"
}
},
+ {
+ "name": "uri.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
{
"name": "urlquery.decode",
"decl": {
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json
new file mode 100644
index 0000000000..b47b68d612
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.1.json
@@ -0,0 +1,4952 @@
+{
+ "builtins": [
+ {
+ "name": "abs",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "all",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "and",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "\u0026"
+ },
+ {
+ "name": "any",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "array.concat",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.flatten",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.reverse",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.slice",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "assign",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": ":="
+ },
+ {
+ "name": "base64.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode_no_pad",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.and",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.lsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.negate",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.or",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.rsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.xor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "cast_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "null"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "ceil",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "concat",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "count",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.equal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.parse_private_keys",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificate_request",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_keypair",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_rsa_private_key",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "div",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "/"
+ },
+ {
+ "name": "endswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "eq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "="
+ },
+ {
+ "name": "equal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "=="
+ },
+ {
+ "name": "floor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "format_int",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.quote_meta",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable_paths",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_and_verify",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_query",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_schema",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.schema_is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "gt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e"
+ },
+ {
+ "name": "gte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e="
+ },
+ {
+ "name": "hex.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "hex.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "http.send",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "indexof",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "indexof_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.member_2",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.member_3",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.print",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.template_string",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.test_case",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "intersection",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode_verify",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign_raw",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.verify_eddsa",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_number",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "indent",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "prefix",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "pretty",
+ "value": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.match_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "static": [
+ {
+ "key": "desc",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "error",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "field",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "type",
+ "value": {
+ "type": "string"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.patch",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "op",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "path",
+ "value": {
+ "type": "any"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.verify_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lower",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c"
+ },
+ {
+ "name": "lte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c="
+ },
+ {
+ "name": "max",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "min",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "minus",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "function"
+ },
+ "infix": "-"
+ },
+ {
+ "name": "mul",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "*"
+ },
+ {
+ "name": "neq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "!="
+ },
+ {
+ "name": "net.cidr_contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_contains_matches",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "static": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_expand",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_intersects",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_merge",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_overlap",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "net.lookup_ip_addr",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "numbers.range",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "numbers.range_step",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.get",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.keys",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.subset",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "opa.runtime",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "or",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "|"
+ },
+ {
+ "name": "plus",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "+"
+ },
+ {
+ "name": "print",
+ "decl": {
+ "type": "function",
+ "variadic": {
+ "type": "any"
+ }
+ }
+ },
+ {
+ "name": "product",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "providers.aws.sign_req",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rand.intn",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "re_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "regex.find_all_string_submatch_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.find_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.globs_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.template_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.chain",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.rule",
+ "decl": {
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.parse_module",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rem",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "%"
+ },
+ {
+ "name": "replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "round",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.compare",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "set_diff",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "sort",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sprintf",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "startswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_prefix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_suffix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.count",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.render_template",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.replace_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.reverse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "substring",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sum",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.add_date",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.clock",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.date",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.diff",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.format",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.now_ns",
+ "decl": {
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "time.parse_duration_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_rfc3339_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.weekday",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "to_number",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_left",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_prefix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_right",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_space",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_suffix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "type_name",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "union",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse_bytes",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "upper",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode_object",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode_object",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.rfc4122",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "walk",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "relation": true
+ },
+ {
+ "name": "yaml.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ }
+ ],
+ "wasm_abi_versions": [
+ {
+ "version": 1,
+ "minor_version": 1
+ },
+ {
+ "version": 1,
+ "minor_version": 2
+ }
+ ],
+ "features": [
+ "keywords_in_refs",
+ "rego_v1",
+ "template_strings"
+ ]
+}
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json
new file mode 100644
index 0000000000..b47b68d612
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.16.2.json
@@ -0,0 +1,4952 @@
+{
+ "builtins": [
+ {
+ "name": "abs",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "all",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "and",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "\u0026"
+ },
+ {
+ "name": "any",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "array.concat",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.flatten",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.reverse",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.slice",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "assign",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": ":="
+ },
+ {
+ "name": "base64.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode_no_pad",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.and",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.lsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.negate",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.or",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.rsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.xor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "cast_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "null"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "ceil",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "concat",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "count",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.equal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.parse_private_keys",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificate_request",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_keypair",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_rsa_private_key",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "div",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "/"
+ },
+ {
+ "name": "endswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "eq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "="
+ },
+ {
+ "name": "equal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "=="
+ },
+ {
+ "name": "floor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "format_int",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.quote_meta",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable_paths",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_and_verify",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_query",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_schema",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.schema_is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "gt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e"
+ },
+ {
+ "name": "gte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e="
+ },
+ {
+ "name": "hex.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "hex.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "http.send",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "indexof",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "indexof_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.member_2",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.member_3",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.print",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.template_string",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.test_case",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "intersection",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode_verify",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign_raw",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.verify_eddsa",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_number",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "indent",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "prefix",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "pretty",
+ "value": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.match_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "static": [
+ {
+ "key": "desc",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "error",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "field",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "type",
+ "value": {
+ "type": "string"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.patch",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "op",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "path",
+ "value": {
+ "type": "any"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.verify_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lower",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c"
+ },
+ {
+ "name": "lte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c="
+ },
+ {
+ "name": "max",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "min",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "minus",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "function"
+ },
+ "infix": "-"
+ },
+ {
+ "name": "mul",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "*"
+ },
+ {
+ "name": "neq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "!="
+ },
+ {
+ "name": "net.cidr_contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_contains_matches",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "static": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_expand",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_intersects",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_merge",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_overlap",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "net.lookup_ip_addr",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "numbers.range",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "numbers.range_step",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.get",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.keys",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.subset",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "opa.runtime",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "or",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "|"
+ },
+ {
+ "name": "plus",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "+"
+ },
+ {
+ "name": "print",
+ "decl": {
+ "type": "function",
+ "variadic": {
+ "type": "any"
+ }
+ }
+ },
+ {
+ "name": "product",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "providers.aws.sign_req",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rand.intn",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "re_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "regex.find_all_string_submatch_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.find_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.globs_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.template_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.chain",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.rule",
+ "decl": {
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.parse_module",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rem",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "%"
+ },
+ {
+ "name": "replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "round",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.compare",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "set_diff",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "sort",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sprintf",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "startswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_prefix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_suffix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.count",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.render_template",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.replace_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.reverse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "substring",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sum",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.add_date",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.clock",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.date",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.diff",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.format",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.now_ns",
+ "decl": {
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "time.parse_duration_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_rfc3339_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.weekday",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "to_number",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_left",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_prefix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_right",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_space",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_suffix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "type_name",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "union",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse_bytes",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "upper",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode_object",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode_object",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.rfc4122",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "walk",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "relation": true
+ },
+ {
+ "name": "yaml.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ }
+ ],
+ "wasm_abi_versions": [
+ {
+ "version": 1,
+ "minor_version": 1
+ },
+ {
+ "version": 1,
+ "minor_version": 2
+ }
+ ],
+ "features": [
+ "keywords_in_refs",
+ "rego_v1",
+ "template_strings"
+ ]
+}
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json
new file mode 100644
index 0000000000..07b0eb9507
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.0.json
@@ -0,0 +1,4955 @@
+{
+ "builtins": [
+ {
+ "name": "abs",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "all",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "and",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "\u0026"
+ },
+ {
+ "name": "any",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "array.concat",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.flatten",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.reverse",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.slice",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "assign",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": ":="
+ },
+ {
+ "name": "base64.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode_no_pad",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.and",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.lsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.negate",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.or",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.rsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.xor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "cast_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "null"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "ceil",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "concat",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "count",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.equal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.parse_private_keys",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificate_request",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_keypair",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_rsa_private_key",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "div",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "/"
+ },
+ {
+ "name": "endswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "eq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "="
+ },
+ {
+ "name": "equal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "=="
+ },
+ {
+ "name": "floor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "format_int",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.quote_meta",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable_paths",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_and_verify",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_query",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_schema",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.schema_is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "gt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e"
+ },
+ {
+ "name": "gte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e="
+ },
+ {
+ "name": "hex.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "hex.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "http.send",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "indexof",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "indexof_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.member_2",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.member_3",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.print",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.template_string",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.test_case",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "intersection",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode_verify",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign_raw",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.verify_eddsa",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_number",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "indent",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "prefix",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "pretty",
+ "value": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.match_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "static": [
+ {
+ "key": "desc",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "error",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "field",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "type",
+ "value": {
+ "type": "string"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.patch",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "op",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "path",
+ "value": {
+ "type": "any"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.verify_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lower",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c"
+ },
+ {
+ "name": "lte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c="
+ },
+ {
+ "name": "max",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "min",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "minus",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "function"
+ },
+ "infix": "-"
+ },
+ {
+ "name": "mul",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "*"
+ },
+ {
+ "name": "neq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "!="
+ },
+ {
+ "name": "net.cidr_contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_contains_matches",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "static": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_expand",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_intersects",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_merge",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_overlap",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "net.lookup_ip_addr",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "numbers.range",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "numbers.range_step",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.get",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.keys",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.subset",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "opa.runtime",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "or",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "|"
+ },
+ {
+ "name": "plus",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "+"
+ },
+ {
+ "name": "print",
+ "decl": {
+ "type": "function",
+ "variadic": {
+ "type": "any"
+ }
+ }
+ },
+ {
+ "name": "product",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "providers.aws.sign_req",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rand.intn",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "re_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "regex.find_all_string_submatch_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.find_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.globs_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.template_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.chain",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.rule",
+ "decl": {
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.parse_module",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rem",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "%"
+ },
+ {
+ "name": "replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "round",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.compare",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "set_diff",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "sort",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sprintf",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "startswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_prefix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_suffix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.count",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.render_template",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.replace_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.reverse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "substring",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sum",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.add_date",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.clock",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.date",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.diff",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.format",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.now_ns",
+ "decl": {
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "time.parse_duration_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_rfc3339_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.weekday",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "to_number",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_left",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_prefix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_right",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_space",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_suffix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "type_name",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "union",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse_bytes",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "upper",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode_object",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode_object",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.rfc4122",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "walk",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "relation": true
+ },
+ {
+ "name": "yaml.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ }
+ ],
+ "future_keywords": [
+ "not"
+ ],
+ "wasm_abi_versions": [
+ {
+ "version": 1,
+ "minor_version": 1
+ },
+ {
+ "version": 1,
+ "minor_version": 2
+ }
+ ],
+ "features": [
+ "keywords_in_refs",
+ "rego_v1",
+ "template_strings"
+ ]
+}
diff --git a/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.1.json b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.1.json
new file mode 100644
index 0000000000..07b0eb9507
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/capabilities/v1.17.1.json
@@ -0,0 +1,4955 @@
+{
+ "builtins": [
+ {
+ "name": "abs",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "all",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "and",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "\u0026"
+ },
+ {
+ "name": "any",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "array.concat",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.flatten",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.reverse",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "array.slice",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "assign",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": ":="
+ },
+ {
+ "name": "base64.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "base64url.encode_no_pad",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.and",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.lsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.negate",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.or",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.rsh",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "bits.xor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "cast_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "null"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "cast_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "ceil",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "concat",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "count",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.equal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.hmac.sha512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.md5",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.parse_private_keys",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha1",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.sha256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_and_verify_certificates_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificate_request",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_certificates",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_keypair",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "crypto.x509.parse_rsa_private_key",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "div",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "/"
+ },
+ {
+ "name": "endswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "eq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "="
+ },
+ {
+ "name": "equal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "=="
+ },
+ {
+ "name": "floor",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "format_int",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "glob.quote_meta",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graph.reachable_paths",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_and_verify",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_query",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.parse_schema",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "graphql.schema_is_valid",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "gt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e"
+ },
+ {
+ "name": "gte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003e="
+ },
+ {
+ "name": "hex.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "hex.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "http.send",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "indexof",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "indexof_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.member_2",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.member_3",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "in"
+ },
+ {
+ "name": "internal.print",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.template_string",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "internal.test_case",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "function"
+ }
+ },
+ {
+ "name": "intersection",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.decode_verify",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.encode_sign_raw",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "io.jwt.verify_eddsa",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_es512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_hs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_ps512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs256",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs384",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "io.jwt.verify_rs512",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_array",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_boolean",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_null",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_number",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_object",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_set",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "is_string",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.marshal_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "indent",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "prefix",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "pretty",
+ "value": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.match_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "dynamic": {
+ "static": [
+ {
+ "key": "desc",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "error",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "field",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "type",
+ "value": {
+ "type": "string"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.patch",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "op",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "path",
+ "value": {
+ "type": "any"
+ }
+ }
+ ],
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "json.verify_schema",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "boolean"
+ },
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lower",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "lt",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c"
+ },
+ {
+ "name": "lte",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "\u003c="
+ },
+ {
+ "name": "max",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "min",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "minus",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "function"
+ },
+ "infix": "-"
+ },
+ {
+ "name": "mul",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "*"
+ },
+ {
+ "name": "neq",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "infix": "!="
+ },
+ {
+ "name": "net.cidr_contains",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_contains_matches",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "static": [
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_expand",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_intersects",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_merge",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "of": [
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "net.cidr_overlap",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "net.lookup_ip_addr",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "numbers.range",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "numbers.range_step",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.filter",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.get",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "any"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.keys",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.remove",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.subset",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "object.union_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "opa.runtime",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "or",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "infix": "|"
+ },
+ {
+ "name": "plus",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "+"
+ },
+ {
+ "name": "print",
+ "decl": {
+ "type": "function",
+ "variadic": {
+ "type": "any"
+ }
+ }
+ },
+ {
+ "name": "product",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "providers.aws.sign_req",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rand.intn",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "re_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "regex.find_all_string_submatch_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.find_n",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.globs_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "regex.template_match",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.chain",
+ "decl": {
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.metadata.rule",
+ "decl": {
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rego.parse_module",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "rem",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "infix": "%"
+ },
+ {
+ "name": "replace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "round",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.compare",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "semver.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "set_diff",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ },
+ "deprecated": true
+ },
+ {
+ "name": "sort",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "split",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sprintf",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "startswith",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_prefix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.any_suffix_match",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.count",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.render_template",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.replace_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "strings.reverse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "substring",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "sum",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "dynamic": {
+ "type": "number"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "number"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.add_date",
+ "decl": {
+ "args": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.clock",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.date",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.diff",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ },
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "number"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.format",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.now_ns",
+ "decl": {
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "time.parse_duration_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.parse_rfc3339_ns",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "time.weekday",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "number"
+ },
+ {
+ "static": [
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "array"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "to_number",
+ "decl": {
+ "args": [
+ {
+ "of": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trace",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_left",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_prefix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_right",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_space",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "trim_suffix",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "type_name",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "union",
+ "decl": {
+ "args": [
+ {
+ "of": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "set"
+ }
+ ],
+ "result": {
+ "of": {
+ "type": "any"
+ },
+ "type": "set"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "units.parse_bytes",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "number"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "upper",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uri.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.decode_object",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "urlquery.encode_object",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "of": [
+ {
+ "type": "string"
+ },
+ {
+ "dynamic": {
+ "type": "string"
+ },
+ "type": "array"
+ },
+ {
+ "of": {
+ "type": "string"
+ },
+ "type": "set"
+ }
+ ],
+ "type": "any"
+ }
+ },
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.parse",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "uuid.rfc4122",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ },
+ "nondeterministic": true
+ },
+ {
+ "name": "walk",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "static": [
+ {
+ "dynamic": {
+ "type": "any"
+ },
+ "type": "array"
+ },
+ {
+ "type": "any"
+ }
+ ],
+ "type": "array"
+ },
+ "type": "function"
+ },
+ "relation": true
+ },
+ {
+ "name": "yaml.is_valid",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "boolean"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.marshal",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
+ {
+ "name": "yaml.unmarshal",
+ "decl": {
+ "args": [
+ {
+ "type": "string"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ }
+ ],
+ "future_keywords": [
+ "not"
+ ],
+ "wasm_abi_versions": [
+ {
+ "version": 1,
+ "minor_version": 1
+ },
+ {
+ "version": 1,
+ "minor_version": 2
+ }
+ ],
+ "features": [
+ "keywords_in_refs",
+ "rego_v1",
+ "template_strings"
+ ]
+}
diff --git a/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go b/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go
index 98093b774e..0ba5c33269 100644
--- a/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go
+++ b/vendor/github.com/open-policy-agent/opa/internal/bundle/utils.go
@@ -78,7 +78,7 @@ func LoadWasmResolversFromStore(ctx context.Context, store storage.Store, txn st
}
for _, wmf := range resolversToLoad {
- resolver, err := wasm.New(wmf.Entrypoints, wmf.Raw, data)
+ resolver, err := wasm.NewWithContext(ctx, wmf.Entrypoints, wmf.Raw, data)
if err != nil {
return nil, fmt.Errorf("failed to initialize wasm module for entrypoints '%s': %s", wmf.Entrypoints, err)
}
diff --git a/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go b/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go
index c2392b6775..85695b1292 100644
--- a/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go
+++ b/vendor/github.com/open-policy-agent/opa/internal/cidr/merge/merge.go
@@ -293,7 +293,7 @@ func partitionCIDR(targetCIDR net.IPNet, excludeCIDR net.IPNet) ([]*net.IPNet, [
// package expects, as big package doesn't append leading zeroes.
if iUpperBytesLen != net.IPv6len {
numZeroesToAppend := net.IPv6len - iUpperBytesLen
- zeroBytes := make([]byte, numZeroesToAppend)
+ zeroBytes := make([]byte, numZeroesToAppend, numZeroesToAppend+len(iUpper.Bytes()))
iUpperBytes = append(zeroBytes, iUpper.Bytes()...)
} else {
iUpperBytes = iUpper.Bytes()
@@ -303,7 +303,7 @@ func partitionCIDR(targetCIDR net.IPNet, excludeCIDR net.IPNet) ([]*net.IPNet, [
iLowerBytesLen := len(iLower.Bytes())
if iLowerBytesLen != net.IPv6len {
numZeroesToAppend := net.IPv6len - iLowerBytesLen
- zeroBytes := make([]byte, numZeroesToAppend)
+ zeroBytes := make([]byte, numZeroesToAppend, numZeroesToAppend+len(iLower.Bytes()))
iLowerBytes = append(zeroBytes, iLower.Bytes()...)
} else {
iLowerBytes = iLower.Bytes()
diff --git a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go
index e8007ee2b6..f23416e0b5 100644
--- a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go
+++ b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schema.go
@@ -56,6 +56,7 @@ type Schema struct {
RootSchema *SubSchema
Pool *schemaPool
ReferencePool *schemaReferencePool
+ validatePatterns bool
}
func (d *Schema) parse(document any, draft Draft) error {
@@ -516,12 +517,23 @@ func (d *Schema) parseSchema(documentNode any, currentSchema *SubSchema) error {
}
}
- // NOTE: Regex compilation step removed as we don't use "pattern" attribute for
- // type checking, and this would cause schemas to fail if they included patterns
- // that were valid ECMA regex dialect but not known to Go (i.e. the regexp.Compile
- // function), such as patterns with negative lookahead
- if _, err := getString(m, KeyPattern); err != nil {
+ // The "pattern" keyword is only compiled into a regex when
+ // validatePatterns is set. It is off by default because many real-world
+ // schemas use ECMA-262 regex features (such as negative lookahead) that
+ // Go's RE2-based regexp package cannot compile; callers that don't need
+ // runtime pattern enforcement can leave it off and still load such
+ // schemas without error.
+ if pattern, err := getString(m, KeyPattern); err != nil {
return err
+ } else if pattern != nil && d.validatePatterns {
+ regexpObject, err := regexp.Compile(*pattern)
+ if err != nil {
+ return errors.New(formatErrorDescription(
+ Locale.MustBeValidRegex(),
+ ErrorDetails{"key": KeyPattern},
+ ))
+ }
+ currentSchema.pattern = regexpObject
}
if format, err := getString(m, KeyFormat); err != nil {
diff --git a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go
index 88caa65de2..a27113d83f 100644
--- a/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go
+++ b/vendor/github.com/open-policy-agent/opa/internal/gojsonschema/schemaLoader.go
@@ -23,10 +23,11 @@ import (
// SchemaLoader is used to load schemas
type SchemaLoader struct {
- pool *schemaPool
- AutoDetect bool
- Validate bool
- Draft Draft
+ pool *schemaPool
+ AutoDetect bool
+ Validate bool
+ Draft Draft
+ ValidatePatterns bool
}
// NewSchemaLoader creates a new NewSchemaLoader
@@ -157,6 +158,7 @@ func (sl *SchemaLoader) Compile(rootSchema JSONLoader) (*Schema, error) {
d.Pool.jsonLoaderFactory = rootSchema.LoaderFactory()
d.DocumentReference = ref
d.ReferencePool = newSchemaReferencePool()
+ d.validatePatterns = sl.ValidatePatterns
var doc any
if ref.String() != "" {
diff --git a/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go b/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go
index 445b5d7c2a..f3e76ee854 100644
--- a/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go
+++ b/vendor/github.com/open-policy-agent/opa/internal/planner/planner.go
@@ -637,12 +637,12 @@ func (p *Planner) planQuery(q ast.Body, index int, iter planiter) error {
func (p *Planner) planExpr(e *ast.Expr, iter planiter) error {
switch {
- case e.Negated:
- return p.planNot(e, iter)
-
case len(e.With) > 0:
return p.planWith(e, iter)
+ case e.IsNegated():
+ return p.planNot(e, iter)
+
case e.IsCall():
return p.planExprCall(e, iter)
@@ -661,8 +661,27 @@ func (p *Planner) planNot(e *ast.Expr, iter planiter) error {
prev := p.curr
p.curr = not.Block
- if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil {
- return err
+ if n, ok := e.Terms.(*ast.Not); ok {
+ cond := p.newLocal() // success condition
+
+ err := p.planQuery(n.Body, 0, func() error {
+ p.appendStmt(&ir.AssignVarStmt{
+ Source: op(ir.Bool(true)),
+ Target: cond,
+ })
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ p.appendStmt(&ir.IsDefinedStmt{
+ Source: cond,
+ })
+ } else {
+ if err := p.planExpr(e.Complement(), func() error { return nil }); err != nil {
+ return err
+ }
}
p.curr = prev
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go b/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go
index 603ab5cd77..90af23a81c 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/annotations.go
@@ -7,9 +7,13 @@ package ast
import (
"encoding/json"
"fmt"
+ "maps"
"net/url"
+ "runtime"
"slices"
"strings"
+ "sync"
+ "weak"
"github.com/open-policy-agent/opa/internal/deepcopy"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
@@ -36,6 +40,7 @@ type (
Schemas []*SchemaAnnotation `json:"schemas,omitempty"`
Compile *CompileAnnotation `json:"compile,omitempty"`
Custom map[string]any `json:"custom,omitempty"`
+ Labels map[string]any `json:"labels,omitempty"`
Location *Location `json:"location,omitempty"`
comments []*Comment
@@ -65,10 +70,11 @@ type (
}
AnnotationSet struct {
- byRule map[*Rule][]*Annotations
- byPackage map[int]*Annotations
- byPath *annotationTreeNode
- modules []*Module // Modules this set was constructed from
+ byRule map[*Rule][]*Annotations
+ byPackage map[int]*Annotations
+ byPath *annotationTreeNode
+ modules []*Module // Modules this set was constructed from
+ mergedLabels sync.Map // map[weak.Pointer[Rule]]*ruleLabelsEntry; lazily populated, entries cleaned up via runtime.AddCleanup when rules are GC'd
}
annotationTreeNode struct {
@@ -172,6 +178,10 @@ func (a *Annotations) Compare(other *Annotations) int {
return cmp
}
+ if cmp := util.Compare(a.Labels, other.Labels); cmp != 0 {
+ return cmp
+ }
+
return 0
}
@@ -228,6 +238,10 @@ func (a *Annotations) MarshalJSON() ([]byte, error) {
data["custom"] = a.Custom
}
+ if len(a.Labels) > 0 {
+ data["labels"] = a.Labels
+ }
+
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
if a.Location != nil {
data["location"] = a.Location
@@ -419,6 +433,10 @@ func (a *Annotations) Copy(node Node) *Annotations {
cpy.Custom = deepcopy.Map(a.Custom)
}
+ if a.Labels != nil {
+ cpy.Labels = deepcopy.Map(a.Labels)
+ }
+
cpy.node = node
return &cpy
@@ -513,6 +531,14 @@ func (a *Annotations) toObject() (*Object, *Error) {
obj.Insert(InternedTerm("custom"), NewTerm(c))
}
+ if len(a.Labels) > 0 {
+ l, err := InterfaceToValue(a.Labels)
+ if err != nil {
+ return nil, NewError(CompileErr, a.Location, "invalid labels annotation %s", err.Error())
+ }
+ obj.Insert(InternedTerm("labels"), NewTerm(l))
+ }
+
return &obj, nil
}
@@ -882,6 +908,17 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
ruleAnnots := as.GetRuleScope(rule)
+ // Fall back to the rule's own attached annotations when the rule's source
+ // module isn't tracked by this AnnotationSet. This happens for rules
+ // supplied by an ExternalRuleSource that returns []*Rule directly: their
+ // source module is never compiled by the outer Compiler, so the set has no
+ // entries for them at any scope. attachRuleAnnotations (run at parse time)
+ // populates rule.Annotations with rule-scope and document-scope entries,
+ // which is the upper bound of what's reachable for such rules anyway.
+ if len(ruleAnnots) == 0 && len(rule.Annotations) > 0 && !slices.Contains(as.modules, rule.Module) {
+ ruleAnnots = rule.Annotations
+ }
+
if len(ruleAnnots) >= 1 {
for _, a := range ruleAnnots {
refs = append(refs, NewAnnotationsRef(a))
@@ -923,6 +960,62 @@ func (as *AnnotationSet) Chain(rule *Rule) AnnotationsRefSet {
return refs
}
+// ruleLabelsEntry caches the merged labels and dedup key for a single rule.
+type ruleLabelsEntry struct {
+ labels map[string]any
+ key string
+}
+
+// MergedLabels returns the inner-scope-wins merged labels for the given rule
+// along with a stable string suitable for content-based deduplication. The
+// result is computed once per rule and cached on the AnnotationSet; the cache
+// entry is dropped automatically when the rule is garbage-collected.
+//
+// labels is nil when the rule has no labels anywhere in its annotation chain;
+// in that case key is the empty string.
+func (as *AnnotationSet) MergedLabels(rule *Rule) (labels map[string]any, key string) {
+ if as == nil {
+ return nil, ""
+ }
+ k := weak.Make(rule)
+ if v, ok := as.mergedLabels.Load(k); ok {
+ e := v.(*ruleLabelsEntry)
+ return e.labels, e.key
+ }
+ merged := mergeChainLabels(as.Chain(rule))
+ e := &ruleLabelsEntry{labels: merged}
+ if len(merged) > 0 {
+ b, _ := json.Marshal(merged)
+ e.key = string(b)
+ }
+ actual, loaded := as.mergedLabels.LoadOrStore(k, e)
+ if !loaded {
+ // k is a weak.Pointer (value type) — it does not keep rule alive, so
+ // the cleanup will fire once the rule becomes unreachable elsewhere.
+ runtime.AddCleanup(rule, func(k weak.Pointer[Rule]) { as.mergedLabels.Delete(k) }, k)
+ }
+ e = actual.(*ruleLabelsEntry)
+ return e.labels, e.key
+}
+
+// mergeChainLabels folds labels from a rule's annotation chain with inner-wins
+// precedence. AnnotationSet.Chain returns entries in inner-to-outer order, so
+// we iterate in reverse to fold outer-to-inner.
+func mergeChainLabels(chain AnnotationsRefSet) map[string]any {
+ var merged map[string]any
+ for i := len(chain) - 1; i >= 0; i-- {
+ a := chain[i].Annotations
+ if a == nil || len(a.Labels) == 0 {
+ continue
+ }
+ if merged == nil {
+ merged = make(map[string]any, len(a.Labels))
+ }
+ maps.Copy(merged, a.Labels)
+ }
+ return merged
+}
+
func (ars FlatAnnotationsRefSet) Insert(ar *AnnotationsRef) FlatAnnotationsRefSet {
result := make(FlatAnnotationsRefSet, 0, len(ars)+1)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go b/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go
index 7e30a8051c..78bd9a7d87 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/builtins.go
@@ -174,6 +174,8 @@ var DefaultBuiltins = [...]*Builtin{
URLQueryEncode,
URLQueryEncodeObject,
URLQueryDecodeObject,
+ URIParse,
+ URIIsValid,
YAMLMarshal,
YAMLUnmarshal,
YAMLIsValid,
@@ -2043,6 +2045,33 @@ var URLQueryDecodeObject = &Builtin{
CanSkipBctx: true,
}
+var URIParse = &Builtin{
+ Name: "uri.parse",
+ Description: "Parses a URI and returns an object containing its components according to RFC 3986. " +
+ "Empty components are omitted. " +
+ "In addition to the standard components, `raw_query` is returned for use with `urlquery` builtins, " +
+ "and `raw_path` is returned to allow detection of path-based exploits using percent-encoded characters.",
+ Decl: types.NewFunction(
+ types.Args(
+ types.Named("uri", types.S).Description("the URI string to parse"),
+ ),
+ types.Named("output", types.NewObject(nil, types.NewDynamicProperty(types.S, types.S))).Description("object containing URI components"),
+ ),
+ CanSkipBctx: true,
+}
+
+var URIIsValid = &Builtin{
+ Name: "uri.is_valid",
+ Description: "Returns true if the input can be parsed as a URI.",
+ Decl: types.NewFunction(
+ types.Args(
+ types.Named("uri", types.S).Description("the URI string to validate"),
+ ),
+ types.Named("result", types.B).Description("true if `uri` is a valid URI, false otherwise"),
+ ),
+ CanSkipBctx: true,
+}
+
var YAMLMarshal = &Builtin{
Name: "yaml.marshal",
Description: "Serializes the input term to YAML.",
@@ -2416,7 +2445,7 @@ var ParseDurationNanos = &Builtin{
Description: "Returns the duration in nanoseconds represented by a string.",
Decl: types.NewFunction(
types.Args(
- types.Named("duration", types.S).Description("a duration like \"3m\"; see the [Go `time` package documentation](https://golang.org/pkg/time/#ParseDuration) for more details"),
+ types.Named("duration", types.S).Description("a duration like \"3m\"; see the [OPA `Duration Parsing` documentation](https://www.openpolicyagent.org/docs/latest/policy-reference/builtins/time#duration-parsing) for more details"),
),
types.Named("ns", types.N).Description("the `duration` in nanoseconds"),
),
@@ -3048,7 +3077,7 @@ var GraphQLSchemaIsValid = &Builtin{
// and returns error string for all other inputs.
var JSONSchemaVerify = &Builtin{
Name: "json.verify_schema",
- Description: "Checks that the input is a valid JSON schema object. The schema can be either a JSON string or an JSON object.",
+ Description: "Checks that the input is a valid JSON schema object. The schema can be either a JSON string or an JSON object. The `pattern` keyword, if present, is compiled using Go's RE2 regex dialect; schemas relying on ECMA-262 features that RE2 does not support (e.g. negative lookahead) will be rejected.",
Decl: types.NewFunction(
types.Args(
types.Named("schema", types.NewAny(types.S, types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)))).
@@ -3068,7 +3097,7 @@ var JSONSchemaVerify = &Builtin{
// and returns non-empty array with error objects otherwise.
var JSONMatchSchema = &Builtin{
Name: "json.match_schema",
- Description: "Checks that the document matches the JSON schema.",
+ Description: "Checks that the document matches the JSON schema. The `pattern` keyword is enforced using Go's RE2 regex dialect; schemas relying on ECMA-262 features that RE2 does not support (e.g. negative lookahead) will be rejected.",
Decl: types.NewFunction(
types.Args(
types.Named("document", types.NewAny(types.S, types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)))).
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go b/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go
index 170f0bf176..0c37dfb1fe 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/capabilities.go
@@ -108,7 +108,8 @@ type WasmABIVersion struct {
}
type CapabilitiesOptions struct {
- regoVersion RegoVersion
+ regoVersion RegoVersion
+ experimentalKeywords bool
}
func newCapabilitiesOptions(opts []CapabilitiesOption) CapabilitiesOptions {
@@ -127,6 +128,21 @@ func CapabilitiesRegoVersion(regoVersion RegoVersion) CapabilitiesOption {
}
}
+// CapabilitiesExperimentalKeywords controls whether experimental future
+// keywords are included in the returned capabilities. When enabled, the
+// future_keywords list will include keywords that are recognized by the
+// parser but are not yet ready for general use.
+//
+// Experimental keywords are subject to change or removal without notice and
+// are not covered by OPA's compatibility guarantees. Enable this option only
+// when you intentionally want to opt in to in-progress language features
+// (for example, when writing tests for those features).
+func CapabilitiesExperimentalKeywords(yes bool) CapabilitiesOption {
+ return func(o *CapabilitiesOptions) {
+ o.experimentalKeywords = yes
+ }
+}
+
// CapabilitiesForThisVersion returns the capabilities of this version of OPA.
func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities {
co := newCapabilitiesOptions(opts)
@@ -147,6 +163,9 @@ func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities {
switch co.regoVersion {
case RegoV0, RegoV0CompatV1:
for kw := range allFutureKeywords {
+ if _, internal := experimentalFutureKeywords[kw]; internal && !co.experimentalKeywords {
+ continue
+ }
f.FutureKeywords = append(f.FutureKeywords, kw)
}
@@ -159,6 +178,9 @@ func CapabilitiesForThisVersion(opts ...CapabilitiesOption) *Capabilities {
}
default:
for kw := range futureKeywords {
+ if _, internal := experimentalFutureKeywords[kw]; internal && !co.experimentalKeywords {
+ continue
+ }
f.FutureKeywords = append(f.FutureKeywords, kw)
}
@@ -277,6 +299,10 @@ func (c *Capabilities) ContainsBuiltin(name string) bool {
})
}
+func (c *Capabilities) ContainsFutureKeyword(kw string) bool {
+ return slices.Contains(c.FutureKeywords, kw)
+}
+
// addBuiltinSorted inserts a built-in into c in sorted order. An existing built-in with the same name
// will be overwritten.
func (c *Capabilities) addBuiltinSorted(bi *Builtin) {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/check.go b/vendor/github.com/open-policy-agent/opa/v1/ast/check.go
index 6e4d8ddd74..810f38b1d3 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/check.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/check.go
@@ -308,6 +308,17 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
typeK := cpy.GetByValue(rule.Head.Key.Value)
if typeK != nil {
tpe = types.NewSet(typeK)
+ if !path.IsGround() {
+ objPath := path.DynamicSuffix()
+ path = path.GroundPrefix()
+
+ var err error
+ tpe, err = nestedObject(cpy, objPath, tpe)
+ if err != nil {
+ tc.err(NewError(TypeErr, rule.Head.Location, "%s", err.Error()))
+ tpe = nil
+ }
+ }
}
}
}
@@ -419,7 +430,18 @@ func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error {
return newArgError(expr.Location, name, "too few arguments", getArgTypes(env, args), namedFargs)
}
+ pre := getArgTypes(env, args)
+
for i := range args {
+ // Check that pre-existing argument types are compatible with the expected types.
+ // Catching that case here avoids false negatives for builtins like sum([1, a]) where a is known to be a string.
+ if pre[i] != nil && !types.Nil(pre[i]) && !unifies(pre[i], fargs.Arg(i)) {
+ return newArgError(expr.Location, name, "invalid argument(s)", pre, namedFargs)
+ }
+
+ // unify1 infers types for untyped variables and checks resolved parts (constants, refs) inside partially-typed composites.
+ // The unifies pre-check above is skipped when the argument contains untyped variables,
+ // so unify1 is still needed to catch those errors.
if !unify1(env, args[i], fargs.Arg(i), false) {
post := make([]types.Type, len(args))
for i := range args {
@@ -509,6 +531,16 @@ func unify2(env *TypeEnv, a *Term, typeA types.Type, b *Term, typeB types.Type)
}
}
+ // When one side is a Var with no type and the other has partial type
+ // info (e.g. a comprehension with some undetermined components),
+ // assign the known structure to the Var.
+ if _, ok := a.Value.(Var); ok && typeA == nil && typeB != nil {
+ return unify1(env, a, typeB, false)
+ }
+ if _, ok := b.Value.(Var); ok && typeB == nil && typeA != nil {
+ return unify1(env, b, typeA, false)
+ }
+
return false
}
@@ -549,12 +581,20 @@ func unify2Object(env *TypeEnv, a *Term, b *Term) bool {
return false
}
+// unify1 walks into a term's structure (arrays, objects, sets, vars), checks
+// compatibility against the expected type, and infers types for variables by
+// assigning them in env. It uses unifies internally for leaf checks.
func unify1(env *TypeEnv, term *Term, tpe types.Type, union bool) bool {
switch v := term.Value.(type) {
case *Array:
switch tpe := tpe.(type) {
case *types.Array:
return unify1Array(env, v, tpe, union)
+ case *types.Recursive:
+ if arr, ok := tpe.Unwrap().(*types.Array); ok {
+ return unify1Array(env, v, arr, union)
+ }
+ return false
case types.Any:
if types.Compare(tpe, types.A) == 0 {
for i := range v.Len() {
@@ -573,6 +613,11 @@ func unify1(env *TypeEnv, term *Term, tpe types.Type, union bool) bool {
switch tpe := tpe.(type) {
case *types.Object:
return unify1Object(env, v, tpe, union)
+ case *types.Recursive:
+ if obj, ok := tpe.Unwrap().(*types.Object); ok {
+ return unify1Object(env, v, obj, union)
+ }
+ return false
case types.Any:
if types.Compare(tpe, types.A) == 0 {
v.Foreach(func(key, value *Term) {
@@ -710,6 +755,9 @@ func (rc *refChecker) Visit(x any) bool {
case *Term:
NewGenericVisitor(rc.Visit).Walk(terms)
return true
+ case *Not:
+ NewGenericVisitor(rc.Visit).Walk(terms.Body)
+ return true
}
case Ref:
if err := rc.checkApply(rc.env, x); err != nil {
@@ -839,12 +887,21 @@ func (rc *refChecker) checkRefLeaf(tpe types.Type, ref Ref, idx int) *Error {
return rc.checkRefLeaf(types.Values(tpe), ref, idx+1)
}
+// unifies checks whether two types are compatible with each other.
func unifies(a, b types.Type) bool {
if a == nil || b == nil {
return false
}
+ // Unwrap recursive types so they compare as their underlying type.
+ if r, ok := a.(*types.Recursive); ok {
+ a = r.Unwrap()
+ }
+ if r, ok := b.(*types.Recursive); ok {
+ b = r.Unwrap()
+ }
+
anyA, ok1 := a.(types.Any)
if ok1 {
if unifiesAny(anyA, b) {
@@ -962,7 +1019,14 @@ func unifiesObjects(a, b *types.Object) bool {
func unifiesObjectsStatic(a, b *types.Object) bool {
for _, k := range a.Keys() {
- if !unifies(a.Select(k), b.Select(k)) {
+ tpeB := b.Select(k)
+ if tpeB == nil {
+ if a.DynamicValue() != nil || b.DynamicValue() != nil {
+ continue
+ }
+ return false
+ }
+ if !unifies(a.Select(k), tpeB) {
return false
}
}
@@ -1147,6 +1211,9 @@ func getOneOfForType(tpe types.Type) (result []Value) {
result = append(result, v)
}
+ case *types.Recursive:
+ return getOneOfForType(tpe.Unwrap())
+
case types.Any:
for _, object := range tpe {
objRes := getOneOfForType(object)
@@ -1174,7 +1241,7 @@ func removeDuplicate(list []Value) []Value {
func getArgTypes(env *TypeEnv, args []*Term) []types.Type {
pre := make([]types.Type, len(args))
for i := range args {
- pre[i] = env.Get(args[i])
+ pre[i] = env.GetByValue(args[i].Value)
}
return pre
}
@@ -1201,6 +1268,9 @@ var dynamicAnyAny = types.NewDynamicProperty(types.A, types.A)
// override takes a type t and returns a type obtained from t where the path represented by ref within it has type o (overriding the original type of that path)
func override(ref Ref, t types.Type, o types.Type, rule *Rule) (types.Type, *Error) {
var newStaticProps []*types.StaticProperty
+ if r, ok := t.(*types.Recursive); ok {
+ t = r.Unwrap()
+ }
obj, ok := t.(*types.Object)
if !ok {
newType, err := getObjectType(ref, o, rule, dynamicAnyAny)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go b/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go
index 8cd2bf9dc4..0314905670 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/compare.go
@@ -75,6 +75,9 @@ func Compare(a, b any) int {
}
switch a := a.(type) {
+ case *Not:
+ b := b.(*Not)
+ return Compare(a.Body, b.Body)
case Null:
return 0
case Boolean:
@@ -144,6 +147,10 @@ func Compare(a, b any) int {
return a.Compare(b.(*SomeDecl))
case *Every:
return a.Compare(b.(*Every))
+ case *LogicalAnd:
+ return a.Compare(b.(*LogicalAnd))
+ case *LogicalOr:
+ return a.Compare(b.(*LogicalOr))
case *With:
return a.Compare(b.(*With))
case Body:
@@ -210,8 +217,14 @@ func sortOrder(x any) int {
return 101
case *Every:
return 102
+ case *LogicalAnd:
+ return 103
+ case *LogicalOr:
+ return 104
case *With:
return 110
+ case *Not:
+ return 111
case *Head:
return 120
case Body:
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go b/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go
index a9455145ef..6bc705a232 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/compile.go
@@ -5,6 +5,7 @@
package ast
import (
+ "context"
"errors"
"fmt"
"io"
@@ -128,6 +129,7 @@ type Compiler struct {
localvargen *localVarGenerator
moduleLoader ModuleLoader
+ externalSources *util.HasherMap[Ref, ExternalRuleSource]
stages []stage
maxErrs int
errCount uint32
@@ -135,6 +137,7 @@ type Compiler struct {
sorted []string // list of sorted module names
pathExists func([]string) (bool, error)
pathConflictCheckRoots []string
+ injectedVirtual func(Ref) bool // optional custom virtual document checker
after map[string][]CompilerStageDefinition
metrics metrics.Metrics
capabilities *Capabilities // user-supplied capabilities
@@ -419,6 +422,7 @@ func NewCompiler() *Compiler {
Modules: map[string]*Module{},
RewrittenVars: map[Var]Var{},
Required: &Capabilities{},
+ externalSources: util.NewHasherMap[Ref, ExternalRuleSource](RefEqual),
maxErrs: CompileErrorLimitDefault,
mu: &sync.Mutex{},
after: map[string][]CompilerStageDefinition{},
@@ -651,6 +655,14 @@ func (c *Compiler) QueryCompiler() QueryCompiler {
return newQueryCompiler(&c0)
}
+// WithVirtual sets a custom virtual document checker on the compiler.
+// The provided function will be called during rule index building to determine
+// if additional refs should be considered virtual documents.
+func (c *Compiler) WithVirtual(fn func(Ref) bool) *Compiler {
+ c.injectedVirtual = fn
+ return c
+}
+
// Compile runs the compilation process on the input modules. The compiled
// version of the modules and associated data structures are stored on the
// compiler. If the compilation process fails for any reason, the compiler will
@@ -894,9 +906,8 @@ func (c *Compiler) GetRulesDynamic(ref Ref) []*Rule {
// Without the options, it would be excluded.
func (c *Compiler) GetRulesDynamicWithOpts(ref Ref, opts RulesOptions) []*Rule {
node := c.RuleTree
-
set := map[*Rule]struct{}{}
- var walk func(node *TreeNode, i int)
+ var walk func(*TreeNode, int)
walk = func(node *TreeNode, i int) {
switch {
case i >= len(ref):
@@ -1041,6 +1052,19 @@ func (c *Compiler) WithModuleLoader(f ModuleLoader) *Compiler {
return c
}
+// WithExternalSource registers an external rule source for the given package
+// reference. When rules under this package are queried via RuleIndex, the
+// external source will be invoked to fetch all rules for the package. The
+// fetched rules are cached so the external source is only called once per
+// package.
+//
+// The package reference should be a fully qualified path (e.g., data.foo.bar).
+// All rule queries under this package will be handled by the external source.
+func (c *Compiler) WithExternalSource(packageRef Ref, source ExternalRuleSource) *Compiler {
+ c.externalSources.Put(packageRef, source)
+ return c
+}
+
// WithDefaultRegoVersion sets the default Rego version to use when a module doesn't specify one;
// such as when it's hand-crafted instead of parsed.
func (c *Compiler) WithDefaultRegoVersion(regoVersion RegoVersion) *Compiler {
@@ -1113,10 +1137,14 @@ func (c *Compiler) counterAdd(name string, n uint64) {
func (c *Compiler) buildRuleIndices() {
c.RuleTree.DepthFirst(func(node *TreeNode) bool {
- if len(node.Values) == 0 {
+ if len(node.Values) == 0 && node.External == nil {
return false
}
- rules := node.Values
+ if node.External != nil {
+ // Skip external sources - they build indices dynamically
+ return true
+ }
+ rules := node.Values // must be len > 0 here
hasNonGroundRef := false
for _, r := range rules {
hasNonGroundRef = !r.Head.Ref().IsGround()
@@ -1141,9 +1169,7 @@ func (c *Compiler) buildRuleIndices() {
}
}
- index := newBaseDocEqIndex(func(ref Ref) bool {
- return isVirtual(c.RuleTree, ref.GroundPrefix())
- })
+ index := newBaseDocEqIndex(c.isVirtual)
if index.Build(rules) {
node.Index = index
}
@@ -1196,10 +1222,19 @@ func (c *Compiler) buildRequiredCapabilities() {
if len(path) == 2 {
if c.moduleIsRegoV1(c.Modules[name]) {
for kw := range futureKeywords {
+ // Don't output experimental keywords for wildcard imports
+ // TODO: Remove on and/or release
+ if _, internal := experimentalFutureKeywords[kw]; internal {
+ continue
+ }
keywords[kw] = struct{}{}
}
} else {
for kw := range allFutureKeywords {
+ // TODO: Remove on and/or release
+ if _, internal := experimentalFutureKeywords[kw]; internal {
+ continue
+ }
keywords[kw] = struct{}{}
}
}
@@ -1302,15 +1337,19 @@ func (c *Compiler) checkRuleConflicts() {
return false // go deeper
}
- kinds := make(map[RuleKind]struct{}, len(node.Values))
+ rules := node.Values
+ if len(rules) == 0 {
+ return true // ?? right
+ }
+ kinds := make(map[RuleKind]struct{}, len(rules))
completeRules := 0
partialRules := 0
- arities := make(map[int]struct{}, len(node.Values))
+ arities := make(map[int]struct{}, len(rules))
name := ""
var conflicts []Ref
defaultRules := make([]*Rule, 0)
- for _, rule := range node.Values {
+ for _, rule := range rules {
r := rule
ref := r.Ref()
name = rw(ref.CopyNonGround()).String() // varRewriter operates in-place
@@ -1366,12 +1405,27 @@ func (c *Compiler) checkRuleConflicts() {
}
}
+ // Functions cannot exist within a rule's dynamic extent, as there is no valid
+ // evaluation scenario for this right now: it will return an error or panic.
+ // NB(sr): This is something we may overcome later -- but for now, it's better
+ // to return an error than to fail in hard-to-understand ways.
+ if conflicts == nil && len(node.Children) > 0 {
+ for _, rule := range node.Values {
+ if !rule.Ref().IsGround() {
+ if funcConflicts := node.flattenChildFunctions(); len(funcConflicts) > 0 {
+ conflicts = funcConflicts
+ }
+ break
+ }
+ }
+ }
+
switch {
case conflicts != nil:
- return !c.err(NewError(TypeErr, node.Values[0].Loc(), "rule %v conflicts with %v", name, conflicts))
+ return !c.err(NewError(TypeErr, rules[0].Loc(), "rule %v conflicts with %v", name, conflicts))
case len(kinds) > 1 || len(arities) > 1 || (completeRules >= 1 && partialRules >= 1):
- return !c.err(NewError(TypeErr, node.Values[0].Loc(), "conflicting rules %v found", name))
+ return !c.err(NewError(TypeErr, rules[0].Loc(), "conflicting rules %v found", name))
case len(defaultRules) > 1:
buf := append(append(append(make([]byte, 0, 64), "multiple default rules "...), name...), " found at "...)
@@ -1516,6 +1570,7 @@ func (c *Compiler) checkBodySafety(safe VarSet, b Body) Body {
// SafetyCheckVisitorParams defines the AST visitor parameters to use for collecting
// variables during the safety check. This has to be exported because it's relied on
// by the copy propagation implementation in topdown.
+// TODO: deprecate?
var SafetyCheckVisitorParams = VarVisitorParams{
SkipRefCallHead: true,
SkipClosures: true,
@@ -1623,7 +1678,9 @@ type schemaParser struct {
}
type cachedDef struct {
- properties []*types.StaticProperty
+ typ types.Type
+ rec *types.Recursive
+ processing bool
}
func newSchemaParser() *schemaParser {
@@ -1636,7 +1693,7 @@ func (parser *schemaParser) parseSchema(schema any) (types.Type, error) {
return parser.parseSchemaWithPropertyKey(schema, "")
}
-func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey string) (types.Type, error) {
+func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey string) (result types.Type, err error) {
subSchema, ok := schema.(*gojsonschema.SubSchema)
if !ok {
return nil, fmt.Errorf("unexpected schema type %v", subSchema)
@@ -1645,11 +1702,35 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s
// Handle referenced schemas, returns directly when a $ref is found
if subSchema.RefSchema != nil {
if existing, ok := parser.definitionCache[subSchema.Ref.String()]; ok {
- return types.NewObject(existing.properties, nil), nil
+ if existing.processing {
+ if existing.rec == nil {
+ existing.rec = types.NewRecursive(subSchema.Ref.String(), nil)
+ }
+ return existing.rec, nil
+ }
+ return existing.typ, nil
}
return parser.parseSchemaWithPropertyKey(subSchema.RefSchema, subSchema.Ref.String())
}
+ // Cache this $ref definition and finalize it via defer when parsing
+ // completes. This allows recursive $refs to be detected: if a nested
+ // $ref hits a definition that is still processing, we know it's a cycle.
+ var def *cachedDef
+ if propertyKey != "" {
+ def = &cachedDef{processing: true}
+ parser.definitionCache[propertyKey] = def
+ defer func() {
+ def.processing = false
+ if result != nil && err == nil {
+ def.typ = result
+ if def.rec != nil {
+ def.rec.SetType(result)
+ }
+ }
+ }()
+ }
+
// Handle anyOf
if subSchema.AnyOf != nil {
var orType types.Type
@@ -1685,7 +1766,22 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s
}
if subSchema.AllOf != nil {
- subSchemaArray := subSchema.AllOf
+ // Build the list of schemas to merge: resolve $refs and skip pure anyOf
+ // wrappers that carry no explicit type or structure. Such schemas have an
+ // "Undefined" type that would cause a spurious type-mismatch in mergeSchemas.
+ subSchemaArray := make([]*gojsonschema.SubSchema, 0, len(subSchema.AllOf))
+ for _, s := range subSchema.AllOf {
+ for s.RefSchema != nil {
+ s = s.RefSchema
+ }
+ if !s.Types.IsTyped() && s.AnyOf != nil && len(s.PropertiesChildren) == 0 && len(s.ItemsChildren) == 0 {
+ continue
+ }
+ subSchemaArray = append(subSchemaArray, s)
+ }
+ if len(subSchemaArray) == 0 {
+ return types.A, nil
+ }
allOfResult, err := mergeSchemas(subSchemaArray...)
if err != nil {
return nil, err
@@ -1717,28 +1813,15 @@ func (parser *schemaParser) parseSchemaWithPropertyKey(schema any, propertyKey s
} else if subSchema.Types.Contains("object") {
if len(subSchema.PropertiesChildren) > 0 {
- def := &cachedDef{
- properties: make([]*types.StaticProperty, 0, len(subSchema.PropertiesChildren)),
- }
- for _, pSchema := range subSchema.PropertiesChildren {
- def.properties = append(def.properties, types.NewStaticProperty(pSchema.Property, nil))
- }
- if propertyKey != "" {
- parser.definitionCache[propertyKey] = def
- }
+ properties := make([]*types.StaticProperty, 0, len(subSchema.PropertiesChildren))
for _, pSchema := range subSchema.PropertiesChildren {
newtype, err := parser.parseSchema(pSchema)
if err != nil {
return nil, fmt.Errorf("unexpected schema type %v: %w", pSchema, err)
}
- for i, prop := range def.properties {
- if prop.Key == pSchema.Property {
- def.properties[i].Value = newtype
- break
- }
- }
+ properties = append(properties, types.NewStaticProperty(pSchema.Property, newtype))
}
- return types.NewObject(def.properties, nil), nil
+ return types.NewObject(properties, nil), nil
}
return types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)), nil
@@ -2480,6 +2563,7 @@ func rewriteTemplateString(tsr *templateStringRewriter, safe VarSet, loc *Locati
if len(ts.Parts) == 0 {
terms = append(terms, NewTerm(InternedEmptyStringValue).SetLocation(loc))
} else {
+ // Note: we don't care about not exprs here
vis := ClearOrNewVarVisitor(nil).WithParams(SafetyCheckVisitorParams)
for _, p := range ts.Parts {
switch p := p.(type) {
@@ -2518,6 +2602,7 @@ func rewriteTemplateString(tsr *templateStringRewriter, safe VarSet, loc *Locati
continue
}
+ // Note: we don't care about not exprs here
vis = ClearOrNewVarVisitor(vis).WithParams(SafetyCheckVisitorParams)
vis.Walk(t)
vars := vis.Vars()
@@ -2647,6 +2732,22 @@ func rewritePrintCalls(gen *localVarGenerator, getArity func(Ref) int, globals V
case *Every:
safe.Update(x.KeyValueVars())
modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Body)
+ case *Not:
+ modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Body)
+ case *LogicalAnd:
+ var modR bool
+ var errsR Errors
+ modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Lhs)
+ modR, errsR = rewritePrintCalls(gen, getArity, safe, x.Rhs)
+ modrec = modrec || modR
+ errsrec = append(errsrec, errsR...)
+ case *LogicalOr:
+ var modR bool
+ var errsR Errors
+ modrec, errsrec = rewritePrintCalls(gen, getArity, safe, x.Lhs)
+ modR, errsR = rewritePrintCalls(gen, getArity, safe, x.Rhs)
+ modrec = modrec || modR
+ errsrec = append(errsrec, errsR...)
}
if modrec {
modified = true
@@ -2689,6 +2790,7 @@ func rewritePrintCalls(gen *localVarGenerator, getArity func(Ref) int, globals V
}
for j := range args {
+ // Note: we don't care about not exprs here
vis = vis.Clear().WithParams(SafetyCheckVisitorParams)
vis.Walk(args[j])
vars := vis.Vars()
@@ -2736,6 +2838,18 @@ func erasePrintCalls(node any) bool {
modrec, x.Body = erasePrintCallsInBody(x.Body)
case *Every:
modrec, x.Body = erasePrintCallsInBody(x.Body)
+ case *Not:
+ modrec, x.Body = erasePrintCallsInBody(x.Body)
+ case *LogicalAnd:
+ modL, lhs := erasePrintCallsInBody(x.Lhs)
+ modR, rhs := erasePrintCallsInBody(x.Rhs)
+ x.Lhs, x.Rhs = lhs, rhs
+ modrec = modL || modR
+ case *LogicalOr:
+ modL, lhs := erasePrintCallsInBody(x.Lhs)
+ modR, rhs := erasePrintCallsInBody(x.Rhs)
+ x.Lhs, x.Rhs = lhs, rhs
+ modrec = modL || modR
}
if modrec {
modified = true
@@ -3026,6 +3140,14 @@ func rewriteRegoMetadataCalls(metadataChainVar *Var, metadataRuleVar *Var, body
errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars)
case *Every:
errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars)
+ case *Not:
+ errs = rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Body, rewrittenVars)
+ case *LogicalAnd:
+ errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Lhs, rewrittenVars)...)
+ errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Rhs, rewrittenVars)...)
+ case *LogicalOr:
+ errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Lhs, rewrittenVars)...)
+ errs = append(errs, rewriteRegoMetadataCalls(metadataChainVar, metadataRuleVar, x.Rhs, rewrittenVars)...)
}
return true
})
@@ -3417,6 +3539,17 @@ func (c *Compiler) setModuleTree() {
func (c *Compiler) setRuleTree() {
c.RuleTree = NewRuleTree(c.ModuleTree)
+
+ // Add tree nodes for external source paths so evaluation knows to look there
+ c.externalSources.Iter(func(pkgRef Ref, source ExternalRuleSource) bool {
+ ri, err := source.Init(context.TODO(), pkgRef)
+ if err != nil {
+ c.err(NewError(CompileErr, nil, "failed to initialize external rule source for ref %v: %v", pkgRef, err))
+ return true
+ }
+ c.RuleTree.add(pkgRef, ri)
+ return false
+ })
}
func (c *Compiler) setGraph() {
@@ -4085,6 +4218,7 @@ func (n *ModuleTreeNode) DepthFirst(f func(*ModuleTreeNode) bool) {
// rule path.
type TreeNode struct {
Key Value
+ External *ExternalIndex
Values []*Rule
Children map[Value]*TreeNode
Sorted []Value
@@ -4129,20 +4263,104 @@ func NewRuleTree(mtree *ModuleTreeNode) *TreeNode {
return &root
}
-func (n *TreeNode) add(path Ref, rule *Rule) {
+func (n *TreeNode) add(path Ref, val any) {
node, tail := n.find(path)
if len(tail) > 0 {
- sub := treeNodeFromRef(tail, rule)
+ sub := treeNodeFromRef(path, tail, val)
if node.Children == nil {
node.Children = make(map[Value]*TreeNode, 1)
}
node.Children[sub.Key] = sub
node.Sorted = append(node.Sorted, sub.Key)
- } else if rule != nil {
- node.Values = append(node.Values, rule)
+ } else if val != nil {
+ switch val := val.(type) {
+ case *Rule:
+ node.Values = append(node.Values, val)
+ case ExternalRuleIndex:
+ node.External = &ExternalIndex{
+ Index: val,
+ Ref: path,
+ }
+ }
}
}
+type ExternalIndex struct {
+ Index ExternalRuleIndex
+ Ref Ref
+}
+
+func (ei *ExternalIndex) Tree(ctx context.Context, rt *TreeNode, prefix Ref, input *Term, m metrics.Metrics, reqMD map[string]any, respMD map[string]any) (*TreeNode, ExternalRuleIndex, error) {
+ resolver := &termResolver{input: input}
+
+ rules, updatedIndex, err := ei.Index.Lookup(ctx,
+ LookupResolver(resolver),
+ LookupMetrics(m),
+ LookupRequestMetadata(reqMD),
+ LookupResponseMetadata(respMD),
+ )
+ if err != nil {
+ return nil, nil, err
+ }
+ c0 := NewCompiler()
+
+ if o := ei.Index.Opts(); o != nil {
+ if len(o.SkippedStages) > 0 {
+ c0.WithSkipStages(o.SkippedStages...)
+ }
+
+ if len(o.VisibleRefs) > 0 {
+ visible := o.VisibleRefs
+ c0.WithVirtual(func(ref Ref) bool {
+ return slices.ContainsFunc(visible, ref.HasPrefix) && rt.isVirtual(ref)
+ })
+ }
+ }
+
+ modules := make(map[string]*Module)
+ for _, rule := range rules {
+ pkgPathStr := rule.Module.Package.Path.String()
+ if mod, exists := modules[pkgPathStr]; exists {
+ mod.Rules = append(mod.Rules, rule)
+ } else {
+ modules[pkgPathStr] = &Module{
+ Package: &Package{Path: rule.Module.Package.Path},
+ Rules: []*Rule{rule},
+ }
+ }
+ }
+ if m != nil {
+ t := m.Timer("external_lookup_compile_module")
+ t.Start()
+ defer t.Stop()
+ }
+ c0.Compile(modules)
+ if c0.Failed() {
+ return nil, nil, c0.Errors
+ }
+
+ node := c0.RuleTree.Find(prefix)
+ return node, updatedIndex, nil
+}
+
+type termResolver struct {
+ input *Term
+}
+
+func (r *termResolver) Resolve(ref Ref) (Value, error) {
+ if ref.HasPrefix(InputRootRef) {
+ if r.input == nil {
+ return nil, UnknownValueErr{}
+ }
+ v, err := r.input.Value.Find(ref[1:])
+ if err != nil {
+ return nil, UnknownValueErr{}
+ }
+ return v, nil
+ }
+ return nil, UnknownValueErr{}
+}
+
// Size returns the number of rules in the tree.
func (n *TreeNode) Size() (s int) {
for _, c := range n.Children {
@@ -4202,36 +4420,85 @@ func (n *TreeNode) DepthFirst(f func(*TreeNode) bool) {
}
}
-func treeNodeFromRef(ref Ref, rule *Rule) *TreeNode {
- depth := len(ref) - 1
- key := ref[depth].Value
- node := &TreeNode{
- Key: key,
- Children: nil,
+func (c *Compiler) isVirtual(ref Ref) bool {
+ return (c.injectedVirtual != nil && c.injectedVirtual(ref)) ||
+ c.RuleTree.isVirtual(ref.GroundPrefix())
+}
+
+// isVirtual returns true if the ref is virtual (has rules).
+func (n *TreeNode) isVirtual(ref Ref) bool {
+ node := n
+ for i := range ref {
+ child := node.Child(ref[i].Value)
+ if child == nil {
+ return false
+ } else if len(child.Values) > 0 || child.External != nil {
+ return true
+ }
+ node = child
}
- if rule != nil {
- node.Values = []*Rule{rule}
+ return true
+}
+
+func treeNodeFromRef(ref, tail Ref, val any) *TreeNode {
+ if len(tail) == 0 {
+ node := &TreeNode{
+ Children: make(map[Value]*TreeNode),
+ }
+ attachValueToNode(node, ref, val)
+ return node
}
- for i := len(ref) - 2; i >= 0; i-- {
- key := ref[i].Value
+ depth := len(tail) - 1
+ node := &TreeNode{
+ Key: tail[depth].Value,
+ }
+ attachValueToNode(node, ref, val)
+
+ for i := depth - 1; i >= 0; i-- {
+ childKey := tail[i+1].Value
node = &TreeNode{
- Key: key,
- Children: map[Value]*TreeNode{ref[i+1].Value: node},
- Sorted: []Value{ref[i+1].Value},
+ Key: tail[i].Value,
+ Children: map[Value]*TreeNode{childKey: node},
+ Sorted: []Value{childKey},
}
}
return node
}
+func attachValueToNode(node *TreeNode, ref Ref, val any) {
+ if val == nil {
+ return
+ }
+ switch val := val.(type) {
+ case *Rule:
+ node.Values = append(node.Values, val)
+ case ExternalRuleIndex:
+ node.External = &ExternalIndex{
+ Index: val,
+ Ref: ref,
+ }
+ }
+}
+
// flattenChildren flattens all children's rule refs into a sorted array.
func (n *TreeNode) flattenChildren() []Ref {
+ return n.flattenMatchingChildren(func(_ *Rule) bool { return true })
+}
+
+// flattenChildFunctions is like flattenChildren but only collects functions (rules with args).
+func (n *TreeNode) flattenChildFunctions() []Ref {
+ return n.flattenMatchingChildren(func(r *Rule) bool { return r.isFunction() })
+}
+
+func (n *TreeNode) flattenMatchingChildren(f func(*Rule) bool) []Ref {
ret := newRefSet()
for _, sub := range n.Children { // we only want the children, so don't use n.DepthFirst() right away
sub.DepthFirst(func(x *TreeNode) bool {
- for _, r := range x.Values {
- rule := r
- ret.AddPrefix(rule.Ref())
+ for _, rule := range x.Values {
+ if f(rule) {
+ ret.AddPrefix(rule.Ref())
+ }
}
return false
})
@@ -4240,6 +4507,60 @@ func (n *TreeNode) flattenChildren() []Ref {
return util.SortedFunc(ret.s, RefCompare)
}
+// Copy creates a shallow copy of the TreeNode suitable for augmentation.
+// Children map is copied recursively. Values slices are initially shared but
+// reallocated on modification (e.g., by MergeChild's append operation).
+func (n *TreeNode) Copy() *TreeNode {
+ if n == nil {
+ return nil
+ }
+
+ result := &TreeNode{
+ Key: n.Key,
+ External: n.External,
+ Values: n.Values,
+ Hide: n.Hide,
+ Index: n.Index,
+ }
+
+ if n.Children != nil {
+ result.Children = make(map[Value]*TreeNode, len(n.Children))
+ for k, v := range n.Children {
+ result.Children[k] = v.Copy()
+ }
+ }
+
+ if n.Sorted != nil {
+ result.Sorted = make([]Value, len(n.Sorted))
+ copy(result.Sorted, n.Sorted)
+ }
+
+ return result
+}
+
+// MergeChild merges another TreeNode into this node's children.
+func (n *TreeNode) MergeChild(key Value, other *TreeNode) {
+ if other == nil {
+ return
+ }
+
+ existing := n.Child(key)
+ if existing == nil {
+ if n.Children == nil {
+ n.Children = make(map[Value]*TreeNode)
+ }
+ n.Children[key] = other
+ n.Sorted = append(n.Sorted, key)
+ return
+ }
+
+ existing.Values = append(existing.Values, other.Values...)
+
+ for childKey, childNode := range other.Children {
+ existing.MergeChild(childKey, childNode)
+ }
+}
+
// Graph represents the graph of dependencies between rules.
type Graph struct {
adj map[util.T]map[util.T]struct{}
@@ -4251,7 +4572,6 @@ type Graph struct {
// NewGraph returns a new Graph based on modules. The list function must return
// the rules referred to directly by the ref.
func NewGraph(modules map[string]*Module, list func(Ref) []*Rule) *Graph {
-
graph := &Graph{
adj: map[util.T]map[util.T]struct{}{},
radj: map[util.T]map[util.T]struct{}{},
@@ -4492,7 +4812,7 @@ func (vs unsafeVars) Slice() (result []unsafePair) {
// If the body cannot be reordered to ensure safety, the second return value
// contains a mapping of expressions to unsafe variables in those expressions.
func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, globals VarSet, body Body) (Body, unsafeVars) {
- vis := varVisitorPool.Get().WithParams(SafetyCheckVisitorParams)
+ vis := varVisitorPool.Get().WithParams(SafetyCheckVisitorParamsWithArity(arity))
vis.WalkBody(body)
defer varVisitorPool.Put(vis)
@@ -4502,7 +4822,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo
unsafe := make(unsafeVars, len(bodyVars)-len(safe))
for _, e := range body {
- vis = vis.Clear().WithParams(SafetyCheckVisitorParams)
+ vis = vis.Clear().WithParams(SafetyCheckVisitorParamsWithArity(arity))
vis.Walk(e)
for v := range vis.Vars() {
if _, ok := safe[v]; !ok {
@@ -4538,6 +4858,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo
if uv.Equal(ovs) { // special case "closure-self"
continue
}
+ // The expression is closing over variables not yet present in reordered body
unsafe.Set(e, uv)
}
@@ -4570,7 +4891,7 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo
for i, e := range reordered {
if i > 0 {
- vis = vis.Clear().WithParams(SafetyCheckVisitorParams)
+ vis = vis.Clear().WithParams(SafetyCheckVisitorParamsWithArity(arity))
vis.Walk(reordered[i-1])
g.Update(vis.Vars())
}
@@ -4583,6 +4904,138 @@ func reorderBodyForSafety(builtins map[string]*Builtin, arity func(Ref) int, glo
return reordered, unsafe
}
+// SafetyCheckVisitorParamsWithArity installs a customVisit hook on top of
+// SafetyCheckVisitorParams that promotes vars from inside implicit operand
+// bodies of *Not, *LogicalAnd, and *LogicalOr into the visiting set. It
+// has two consumers, both of which depend on this promotion:
+func SafetyCheckVisitorParamsWithArity(arity func(Ref) int) VarVisitorParams {
+ params := SafetyCheckVisitorParams
+ params.customVisit = func(vis *VarVisitor, v any) bool {
+ return promoteUnsafeOperandBodyVars(arity, vis, v)
+ }
+ return params
+}
+
+func promoteUnsafeOperandBodyVars(arity func(Ref) int, vis *VarVisitor, v any) bool {
+ promote := func(body Body) {
+ for v := range unsafeImplicitBodyVars(body, arity) {
+ vis.Add(v)
+ }
+ }
+ switch n := v.(type) {
+ case *Not:
+ if !n.ExplicitBody {
+ promote(n.Body)
+ }
+ return true
+ case *LogicalAnd:
+ if !n.ExplicitLhs {
+ promote(n.Lhs)
+ }
+ if !n.ExplicitRhs {
+ promote(n.Rhs)
+ }
+ return true
+ case *LogicalOr:
+ if !n.ExplicitLhs {
+ promote(n.Lhs)
+ }
+ if !n.ExplicitRhs {
+ promote(n.Rhs)
+ }
+ return true
+ }
+ return false
+}
+
+// unsafeImplicitBodyVars returns the set of vars from an implicit logical
+// operand/not body that are not internally satisfied. The classification
+// rule:
+// - single-expr body: every visible var is returned (no consumer is
+// possible within a single expression).
+// - multi-expr body: a var is returned iff it has no binding within the
+// body, OR it appears in exactly one body expr (no separate consumer).
+func unsafeImplicitBodyVars(body Body, arity func(Ref) int) VarSet {
+ result := NewVarSet()
+ if len(body) == 0 {
+ return result
+ }
+
+ internalVis := varVisitorPool.Get()
+ defer varVisitorPool.Put(internalVis)
+
+ // 1. Collect per-expr occurrences.
+ occurrences := map[Var][]*Expr{}
+ for _, e := range body {
+ internalVis.Clear().WithParams(SafetyCheckVisitorParams)
+ internalVis.Walk(e)
+ for v := range internalVis.Vars() {
+ occurrences[v] = append(occurrences[v], e)
+ }
+ }
+
+ // Single-expr fast path: there is no other expression to consume a
+ // binding, so every visible var must come from outside the body.
+ if len(body) == 1 {
+ for v := range occurrences {
+ result.Add(v)
+ }
+ return result
+ }
+
+ // 2. Collect bindings (eq outputs + trailing call-arg outputs).
+ bindings := map[Var]struct{}{}
+ for _, e := range body {
+ terms, ok := e.Terms.([]*Term)
+ if !ok {
+ continue
+ }
+
+ if e.IsEquality() {
+ for v := range outputVarsForExprEq(e, VarSet{}, VarSet{}) {
+ bindings[v] = struct{}{}
+ }
+ continue
+ }
+
+ operator, ok := terms[0].Value.(Ref)
+ if !ok {
+ continue
+ }
+
+ ar := arity(operator)
+ if ar < 0 {
+ continue
+ }
+
+ numInputTerms := ar + 1
+ if numInputTerms >= len(terms) {
+ continue
+ }
+
+ internalVis.Clear().WithParams(VarVisitorParams{
+ SkipClosures: true,
+ SkipSets: true,
+ SkipObjectKeys: true,
+ SkipRefHead: true,
+ })
+ internalVis.WalkArgs(terms[numInputTerms:])
+ for v := range internalVis.Vars() {
+ bindings[v] = struct{}{}
+ }
+ }
+
+ // 3. Check if each var occurrence is bound by multiple expressions
+ for v, exprs := range occurrences {
+ if _, bound := bindings[v]; !bound {
+ result.Add(v)
+ } else if len(exprs) == 1 {
+ result.Add(v)
+ }
+ }
+ return result
+}
+
type bodySafetyTransformer struct {
builtins map[string]*Builtin
arity func(Ref) int
@@ -4635,9 +5088,21 @@ func (xform *bodySafetyTransformer) Visit(x any) bool {
return true
}
case *Expr:
- if ev, ok := term.Terms.(*Every); ok {
- xform.globals.Update(ev.KeyValueVars())
- ev.Body = xform.reorderComprehensionSafety(NewVarSet(), ev.Body)
+ switch x := term.Terms.(type) {
+ case *Every:
+ xform.globals.Update(x.KeyValueVars())
+ x.Body = xform.reorderComprehensionSafety(NewVarSet(), x.Body)
+ return true
+ case *Not:
+ x.Body = xform.reorderComprehensionSafety(NewVarSet(), x.Body)
+ return true
+ case *LogicalAnd:
+ x.Lhs = xform.reorderComprehensionSafety(NewVarSet(), x.Lhs)
+ x.Rhs = xform.reorderComprehensionSafety(NewVarSet(), x.Rhs)
+ return true
+ case *LogicalOr:
+ x.Lhs = xform.reorderComprehensionSafety(NewVarSet(), x.Lhs)
+ x.Rhs = xform.reorderComprehensionSafety(NewVarSet(), x.Rhs)
return true
}
}
@@ -4645,7 +5110,7 @@ func (xform *bodySafetyTransformer) Visit(x any) bool {
}
func (xform *bodySafetyTransformer) reorderComprehensionSafety(tv VarSet, body Body) Body {
- bv := body.Vars(SafetyCheckVisitorParams)
+ bv := body.Vars(SafetyCheckVisitorParamsWithArity(xform.arity))
bv.Update(xform.globals)
if tv.DiffCount(bv) > 0 {
@@ -4682,11 +5147,18 @@ func (xform *bodySafetyTransformer) reorderSetComprehensionSafety(sc *SetCompreh
// this expression.
func unsafeVarsInClosures(e *Expr, vis *VarVisitor) {
WalkClosures(e, func(x any) bool {
- if ev, ok := x.(*Every); ok {
- vis.WalkBody(ev.Body)
- return true
+ switch x := x.(type) {
+ case *Every:
+ vis.WalkBody(x.Body)
+ case *LogicalAnd:
+ vis.WalkBody(x.Lhs)
+ vis.WalkBody(x.Rhs)
+ case *LogicalOr:
+ vis.WalkBody(x.Lhs)
+ vis.WalkBody(x.Rhs)
+ default:
+ vis.Walk(x)
}
- vis.Walk(x)
return true
})
}
@@ -4719,16 +5191,18 @@ func OutputVarsFromExpr(c *Compiler, expr *Expr, safe VarSet) VarSet {
func outputVarsForExpr(expr *Expr, arity func(Ref) int, safe VarSet, output VarSet, vis *VarVisitor) VarSet {
// Negated expressions must be safe.
- if expr.Negated {
+ if expr.IsNegated() {
return VarSet{}
}
if len(expr.With) > 0 {
+ // Note: we don't care about not exprs here
vis = ClearOrNewVarVisitor(vis).WithParams(SafetyCheckVisitorParams)
}
// With modifier inputs must be safe.
for _, with := range expr.With {
+ // Note: we don't care about not exprs here
vis = vis.Clear().WithParams(SafetyCheckVisitorParams)
vis.Walk(with)
if vis.Vars().DiffCount(safe) > 0 {
@@ -4760,6 +5234,9 @@ func outputVarsForExpr(expr *Expr, arity func(Ref) int, safe VarSet, output VarS
return outputVarsForExprCall(expr, ar, safe, terms, vis, output)
case *Every:
return outputVarsForTerms(terms.Domain, safe, output)
+ case *LogicalAnd, *LogicalOr:
+ // and/or expressions do not contribute bindings to the enclosing body.
+ return VarSet{}
default:
panic("illegal expression")
}
@@ -4922,7 +5399,7 @@ func resolveRef(globals map[Var]*usedRef, ignore *declaredVarStack, ref Ref) Ref
switch v := x.Value.(type) {
case Var:
if g, ok := globals[v]; ok && !ignore.Contains(v) {
- cpy := g.ref.Copy()
+ cpy := g.ref.CopyNonGround()
for i := range cpy {
cpy[i].SetLocation(x.Location)
}
@@ -5059,6 +5536,27 @@ func resolveRefsInExpr(globals map[Var]*usedRef, ignore *declaredVarStack, expr
Body: resolveRefsInBody(globals, ignore, ts.Body),
}
ignore.Pop()
+ case *Not:
+ cpy.Terms = &Not{
+ Body: resolveRefsInBody(globals, ignore, ts.Body),
+ ExplicitBody: ts.ExplicitBody,
+ }
+ case *LogicalAnd:
+ cpy.Terms = &LogicalAnd{
+ Lhs: resolveRefsInBody(globals, ignore, ts.Lhs),
+ Rhs: resolveRefsInBody(globals, ignore, ts.Rhs),
+ ExplicitLhs: ts.ExplicitLhs,
+ ExplicitRhs: ts.ExplicitRhs,
+ Location: ts.Location,
+ }
+ case *LogicalOr:
+ cpy.Terms = &LogicalOr{
+ Lhs: resolveRefsInBody(globals, ignore, ts.Lhs),
+ Rhs: resolveRefsInBody(globals, ignore, ts.Rhs),
+ ExplicitLhs: ts.ExplicitLhs,
+ ExplicitRhs: ts.ExplicitRhs,
+ Location: ts.Location,
+ }
}
for _, w := range cpy.With {
w.Target = resolveRefsInTerm(globals, ignore, w.Target)
@@ -5071,7 +5569,7 @@ func resolveRefsInTerm(globals map[Var]*usedRef, ignore *declaredVarStack, term
switch v := term.Value.(type) {
case Var:
if g, ok := globals[v]; ok && !ignore.Contains(v) {
- cpy := g.ref.Copy()
+ cpy := g.ref.CopyNonGround()
for i := range cpy {
cpy[i].SetLocation(term.Location)
}
@@ -5365,6 +5863,10 @@ func rewriteDynamics(f *equalityFactory, body Body) Body {
result = rewriteDynamicsCallExpr(f, expr, result)
case expr.IsEvery():
result = rewriteDynamicsEveryExpr(f, expr, result)
+ case expr.IsNot():
+ result = rewriteDynamicsNotExpr(f, expr, result)
+ case expr.IsAnd(), expr.IsOr():
+ result = rewriteDynamicsLogicalExpr(f, expr, result)
default:
result = rewriteDynamicsTermExpr(f, expr, result)
}
@@ -5400,6 +5902,26 @@ func rewriteDynamicsEveryExpr(f *equalityFactory, expr *Expr, result Body) Body
return result
}
+func rewriteDynamicsNotExpr(f *equalityFactory, expr *Expr, result Body) Body {
+ n := expr.Terms.(*Not)
+ n.Body = rewriteDynamics(f, n.Body)
+ result.Append(expr)
+ return result
+}
+
+func rewriteDynamicsLogicalExpr(f *equalityFactory, expr *Expr, result Body) Body {
+ switch t := expr.Terms.(type) {
+ case *LogicalAnd:
+ t.Lhs = rewriteDynamics(f, t.Lhs)
+ t.Rhs = rewriteDynamics(f, t.Rhs)
+ case *LogicalOr:
+ t.Lhs = rewriteDynamics(f, t.Lhs)
+ t.Rhs = rewriteDynamics(f, t.Rhs)
+ }
+ result.Append(expr)
+ return result
+}
+
func rewriteDynamicsTermExpr(f *equalityFactory, expr *Expr, result Body) Body {
term := expr.Terms.(*Term)
result, expr.Terms = rewriteDynamicsInTerm(expr, f, term, result)
@@ -5419,6 +5941,8 @@ func rewriteDynamicsInTerm(original *Expr, f *equalityFactory, term *Term, resul
v.Body = rewriteDynamics(f, v.Body)
case *ObjectComprehension:
v.Body = rewriteDynamics(f, v.Body)
+ case *Not:
+ v.Body = rewriteDynamics(f, v.Body)
default:
result, term = rewriteDynamicsOne(original, f, term, result)
}
@@ -5606,6 +6130,17 @@ func expandExpr(gen *localVarGenerator, expr *Expr) (result []*Expr) {
terms.Body = rewriteExprTermsInBody(gen, terms.Body)
result = append(result, extras...)
result = append(result, expr)
+ case *Not:
+ terms.Body = rewriteExprTermsInBody(gen, terms.Body)
+ result = append(result, expr)
+ case *LogicalAnd:
+ terms.Lhs = rewriteExprTermsInBody(gen, terms.Lhs)
+ terms.Rhs = rewriteExprTermsInBody(gen, terms.Rhs)
+ result = append(result, expr)
+ case *LogicalOr:
+ terms.Lhs = rewriteExprTermsInBody(gen, terms.Lhs)
+ terms.Rhs = rewriteExprTermsInBody(gen, terms.Rhs)
+ result = append(result, expr)
}
return
}
@@ -5666,6 +6201,9 @@ func expandExprTerm(gen *localVarGenerator, term *Term) (support []*Expr, output
support, value := expandExprTerm(gen, v.Value)
v.Value = value
v.Body = rewriteExprTermsInBody(gen, appendToBody(v.Body, support...))
+ case *Not:
+ // Note: not strictly needed as long as 'not' can only be a term node, and not a term value
+ v.Body = rewriteExprTermsInBody(gen, appendToBody(v.Body, support...))
}
return
}
@@ -5913,6 +6451,11 @@ func rewriteDeclaredVarsInBody(g *localVarGenerator, stack *localDeclaredVars, u
expr, errs = rewriteSomeDeclStatement(g, stack, body[i], errs, strict)
case body[i].IsEvery():
expr, errs = rewriteEveryStatement(g, stack, body[i], errs, strict)
+ case body[i].IsNot() && body[i].Terms.(*Not).ExplicitBody:
+ // Only explicit not bodies are allowed to declare vars
+ expr, errs = rewriteNotStatement(g, stack, body[i], errs, strict)
+ case body[i].IsAnd() || body[i].IsOr():
+ expr, errs = rewriteLogicalStatement(g, stack, body[i], errs, strict)
default:
expr, errs = rewriteDeclaredVarsInExpr(g, stack, body[i], errs, strict)
}
@@ -6152,9 +6695,54 @@ func rewriteSomeDeclStatement(g *localVarGenerator, stack *localDeclaredVars, ex
return nil, errs
}
+func rewriteNotStatement(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) {
+ e := expr.Copy()
+ not := e.Terms.(*Not)
+
+ stack.Push()
+ defer stack.Pop()
+
+ used := NewVarSet()
+ not.Body, errs = rewriteDeclaredVarsInBody(g, stack, used, not.Body, errs, strict)
+
+ return rewriteDeclaredVarsInExpr(g, stack, e, errs, strict)
+}
+
+func rewriteLogicalStatement(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) {
+ e := expr.Copy()
+
+ switch t := e.Terms.(type) {
+ case *LogicalAnd:
+ if t.ExplicitLhs {
+ t.Lhs, errs = rewriteLogicalOperandBody(g, stack, t.Lhs, errs, strict)
+ }
+ if t.ExplicitRhs {
+ t.Rhs, errs = rewriteLogicalOperandBody(g, stack, t.Rhs, errs, strict)
+ }
+ case *LogicalOr:
+ if t.ExplicitLhs {
+ t.Lhs, errs = rewriteLogicalOperandBody(g, stack, t.Lhs, errs, strict)
+ }
+ if t.ExplicitRhs {
+ t.Rhs, errs = rewriteLogicalOperandBody(g, stack, t.Rhs, errs, strict)
+ }
+ }
+
+ return rewriteDeclaredVarsInExpr(g, stack, e, errs, strict)
+}
+
+func rewriteLogicalOperandBody(g *localVarGenerator, stack *localDeclaredVars, body Body, errs Errors, strict bool) (Body, Errors) {
+ stack.Push()
+ defer stack.Pop()
+
+ used := NewVarSet()
+ return rewriteDeclaredVarsInBody(g, stack, used, body, errs, strict)
+}
+
func rewriteDeclaredVarsInExpr(g *localVarGenerator, stack *localDeclaredVars, expr *Expr, errs Errors, strict bool) (*Expr, Errors) {
vis := NewGenericVisitor(func(x any) bool {
var stop bool
+ // Note: we don't include *Not nodes here, as such bodies are allowed to contain assignments; e.g. 'not {x := input.x; f(x)}'
switch x := x.(type) {
case *Term:
stop, errs = rewriteDeclaredVarsInTerm(g, stack, x, errs, strict)
@@ -6467,8 +7055,8 @@ func validateWith(c *Compiler, unsafeBuiltinsMap map[string]struct{}, expr *Expr
// target is a function. It's probably wrong for arity-0 functions, but those are
// and edge case anyways.
if child := targetNode.Child(ref[len(ref)-1].Value); child != nil {
- for _, v := range child.Values {
- if len(v.Head.Args) > 0 {
+ for _, r := range child.Values {
+ if len(r.Head.Args) > 0 {
if ok, err := validateWithFunctionValue(c.builtins, unsafeBuiltinsMap, c.RuleTree, value); err != nil || ok {
return false, err // err may be nil
}
@@ -6481,8 +7069,8 @@ func validateWith(c *Compiler, unsafeBuiltinsMap map[string]struct{}, expr *Expr
if r, ok := value.Value.(Ref); ok {
// TODO: check that target ref doesn't exist?
if valueNode := c.RuleTree.Find(r); valueNode != nil {
- for _, v := range valueNode.Values {
- if len(v.Head.Args) > 0 {
+ for _, r := range valueNode.Values {
+ if len(r.Head.Args) > 0 {
return false, nil
}
}
@@ -6567,19 +7155,6 @@ func isBuiltinRefOrVar(bs map[string]*Builtin, unsafeBuiltinsMap map[string]stru
return false, nil
}
-func isVirtual(node *TreeNode, ref Ref) bool {
- for i := range ref {
- child := node.Child(ref[i].Value)
- if child == nil {
- return false
- } else if len(child.Values) > 0 {
- return true
- }
- node = child
- }
- return true
-}
-
func safetyErrorSlice(unsafe unsafeVars, rewritten map[Var]Var) (result Errors) {
if len(unsafe) == 0 {
return
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/env.go b/vendor/github.com/open-policy-agent/opa/v1/ast/env.go
index 3a3e793778..c3a5fcef02 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/env.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/env.go
@@ -320,7 +320,11 @@ func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
curr.children.Put(child.key, child)
} else if child.value != nil && i+1 < len(path) {
// If child has an object value, merge the new value into it.
- if o, ok := child.value.(*types.Object); ok {
+ cv := child.value
+ if r, ok := cv.(*types.Recursive); ok {
+ cv = r.Unwrap()
+ }
+ if o, ok := cv.(*types.Object); ok {
var err error
child.value, err = insertIntoObject(o, path[i+1:], tpe, env)
if err != nil {
@@ -334,15 +338,26 @@ func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
curr.value = mergeTypes(curr.value, tpe)
- if _, ok := tpe.(*types.Object); ok && curr.children.Len() > 0 {
+ _, isObj := tpe.(*types.Object)
+ if !isObj {
+ if r, ok := tpe.(*types.Recursive); ok {
+ _, isObj = r.Unwrap().(*types.Object)
+ }
+ }
+ if isObj && curr.children.Len() > 0 {
// merge all leafs into the inserted object
+ cv := curr.value
+ if r, ok := cv.(*types.Recursive); ok {
+ cv = r.Unwrap()
+ }
for p, t := range curr.Leafs() {
var err error
- curr.value, err = insertIntoObject(curr.value.(*types.Object), *p, t, env)
+ cv, err = insertIntoObject(cv.(*types.Object), *p, t, env)
if err != nil {
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
}
}
+ curr.value = cv
}
}
@@ -363,6 +378,14 @@ func mergeTypes(a, b types.Type) types.Type {
return a
}
+ // Unwrap recursive types so they merge as their underlying type.
+ if r, ok := a.(*types.Recursive); ok {
+ a = r.Unwrap()
+ }
+ if r, ok := b.(*types.Recursive); ok {
+ b = r.Unwrap()
+ }
+
switch a := a.(type) {
case *types.Object:
if bObj, ok := b.(*types.Object); ok && len(a.StaticProperties()) == 0 && len(bObj.StaticProperties()) == 0 {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go b/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go
new file mode 100644
index 0000000000..ce3433cce0
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/external_source.go
@@ -0,0 +1,128 @@
+// Copyright 2026 The OPA Authors. All rights reserved.
+// Use of this source code is governed by an Apache2
+// license that can be found in the LICENSE file.
+
+package ast
+
+import (
+ "context"
+
+ "github.com/open-policy-agent/opa/v1/metrics"
+)
+
+type ExternalRuleSource interface {
+ // Refs returns the package refs that this source provides rules for.
+ // A source can provide rules for multiple packages.
+ Refs() []Ref
+
+ // Init returns an initialized [ExternalRuleIndex]. A `Ref` is provided
+ // so we know which package we're preparing if multiple Refs are external.
+ Init(context.Context, Ref) (ExternalRuleIndex, error)
+}
+
+// ExternalRuleIndex mirrors RuleIndex.Lookup(), but add a [context.Context] parameter.
+type ExternalRuleIndex interface {
+ // Opts returns the options for the ExternalRuleIndex. Returns nil if no
+ // options are configured.
+ Opts() *ExternalSourceOptions
+
+ // Lookup returns rules and optionally an updated ExternalRuleIndex instance.
+ // The returned ExternalRuleIndex (if non-nil) will be used for subsequent
+ // Lookup calls within the same evaluation context, allowing plugins to
+ // maintain per-evaluation state.
+ //
+ // Plugins can use two strategies:
+ // 1. Immutable: Return a new ExternalRuleIndex instance with updated state
+ // 2. Mutable: Update internal state and return self
+ //
+ // If the plugin does not need per-evaluation state, it can return nil for
+ // the ExternalRuleIndex, and the original instance will continue to be used.
+ Lookup(context.Context, ...LookupOption) ([]*Rule, ExternalRuleIndex, error)
+}
+
+// ExternalRuleIndexCloser is an optional interface for resource cleanup.
+type ExternalRuleIndexCloser interface {
+ ExternalRuleIndex
+ Close() error
+}
+
+// ExternalSourceOptions contains options for registering an external rule source.
+type ExternalSourceOptions struct {
+ // VisibleRefs controls which parts of the surrounding rule tree the external
+ // source can reference during compilation. By default (nil), the source is
+ // fully isolated and cannot access any surrounding policy. An empty slice
+ // is equivalent to nil (fully isolated).
+ //
+ // To allow access to the entire rule tree, use []Ref{MustParseRef("data")}.
+ // To allow access to specific subtrees only, list them explicitly, e.g.
+ // []Ref{MustParseRef("data.helpers")}. The external source can then
+ // reference rules under those prefixes but nothing else.
+ VisibleRefs []Ref
+
+ // SkippedStages allows external sources to skip stages in the dynamic compiler
+ // used with the externally-provided Rego. If, for example, the `[]*Rule` returned
+ // has already been compiled, we can skip all stages.
+ //
+ // For pre-compiled rules, prefer starting from AllStages() and removing only
+ // the stages you need (e.g. SetModuleTree, SetRuleTree, BuildRuleIndices).
+ // This is forward-compatible: new compiler stages added in future releases
+ // will be skipped automatically rather than running unexpectedly.
+ SkippedStages []StageID
+}
+
+// LookupOption is a functional option for ExternalRuleIndex.Lookup calls.
+type LookupOption func(*LookupOptions)
+
+// LookupOptions contains options for ExternalRuleIndex.Lookup calls.
+type LookupOptions struct {
+ metrics metrics.Metrics
+ resolver ValueResolver
+ requestMetadata map[string]any
+ responseMetadata map[string]any
+}
+
+// Metrics returns the metrics instance from the options, or nil if not set.
+func (o *LookupOptions) Metrics() metrics.Metrics {
+ if o == nil {
+ return nil
+ }
+ return o.metrics
+}
+
+func (o *LookupOptions) Resolver() ValueResolver {
+ return o.resolver
+}
+
+func (o *LookupOptions) RequestMetadata() map[string]any {
+ return o.requestMetadata
+}
+
+func (o *LookupOptions) ResponseMetadata() map[string]any {
+ return o.responseMetadata
+}
+
+// LookupMetrics returns a LookupOption that sets the metrics instance
+// for the Lookup call.
+func LookupMetrics(m metrics.Metrics) LookupOption {
+ return func(opts *LookupOptions) {
+ opts.metrics = m
+ }
+}
+
+func LookupResolver(r ValueResolver) LookupOption {
+ return func(opts *LookupOptions) {
+ opts.resolver = r
+ }
+}
+
+func LookupRequestMetadata(m map[string]any) LookupOption {
+ return func(opts *LookupOptions) {
+ opts.requestMetadata = m
+ }
+}
+
+func LookupResponseMetadata(m map[string]any) LookupOption {
+ return func(opts *LookupOptions) {
+ opts.responseMetadata = m
+ }
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go b/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go
index 2721c3618b..4a32950e96 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/internal/tokens/tokens.go
@@ -76,6 +76,8 @@ const (
Every
Contains
If
+ LogicalAnd
+ LogicalOr
)
var strings = [...]string{
@@ -131,6 +133,8 @@ var strings = [...]string{
Every: "every",
Contains: "contains",
If: "if",
+ LogicalAnd: "and",
+ LogicalOr: "or",
}
var keywords = map[string]Token{
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go b/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go
index 4f454beb96..f1f4db59d1 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/interning.go
@@ -41,9 +41,8 @@ var (
minusOneValue Value = Number("-1")
minusOneTerm = NewTerm(minusOneValue)
- internedStringTerms = map[string]*Term{
- "": InternedEmptyString,
- }
+ internedStringValues = map[string]Value{"": InternedEmptyStringValue}
+ internedStringTerms = map[string]*Term{"": InternedEmptyString}
internedVarValues = map[string]Value{
"input": Var("input"),
@@ -68,11 +67,12 @@ var (
// interned terms are shared globally, and the underlying map is not thread-safe.
func InternStringTerm(str ...string) {
for _, s := range str {
- if _, ok := internedStringTerms[s]; ok {
+ if _, ok := internedStringValues[s]; ok {
continue
}
- internedStringTerms[s] = &Term{Value: String(s)}
+ internedStringValues[s] = String(s)
+ internedStringTerms[s] = &Term{Value: internedStringValues[s]}
}
}
@@ -125,7 +125,7 @@ func HasInternedValue[T internable](v T) bool {
// InternedValue returns an interned Value for scalar v, if the value is
// interned. If the value is not interned, a new Value is returned.
func InternedValue[T internable](v T) Value {
- return InternedValueOr(v, internedTermValue)
+ return InternedValueOr(v, newValue)
}
// InternedVarValue returns an interned Var Value for the given name. If the
@@ -144,6 +144,8 @@ func InternedValueOr[T internable](v T, supplier func(T) Value) Value {
switch value := any(v).(type) {
case bool:
return internedBooleanValue(value)
+ case string:
+ return internedStringValue(value)
case int:
return internedIntNumberValue(value)
case int8:
@@ -168,6 +170,37 @@ func InternedValueOr[T internable](v T, supplier func(T) Value) Value {
return supplier(v)
}
+func newValue[T internable](v T) Value {
+ switch value := any(v).(type) {
+ case bool:
+ return Boolean(value)
+ case string:
+ return String(value)
+ case int:
+ return Number(strconv.Itoa(value))
+ case int8:
+ return Number(strconv.Itoa(int(value)))
+ case int16:
+ return Number(strconv.Itoa(int(value)))
+ case int32:
+ return Number(strconv.Itoa(int(value)))
+ case int64:
+ return Number(strconv.Itoa(int(value)))
+ case uint:
+ return Number(strconv.Itoa(int(value)))
+ case uint8:
+ return Number(strconv.Itoa(int(value)))
+ case uint16:
+ return Number(strconv.Itoa(int(value)))
+ case uint32:
+ return Number(strconv.Itoa(int(value)))
+ case uint64:
+ return Number(strconv.Itoa(int(value)))
+ default:
+ panic("unreachable")
+ }
+}
+
// Interned returns a possibly interned term for the given scalar value.
// If the value is not interned, a new term is created for that value.
func InternedTerm[T internable](v T) *Term {
@@ -267,6 +300,14 @@ func internedBooleanValue(b bool) Value {
return InternedBooleanFalseValue
}
+func internedStringValue(s string) Value {
+ if v, ok := internedStringValues[s]; ok {
+ return v
+ }
+
+ return String(s)
+}
+
// InternedBooleanTerm returns an interned term with the given boolean value.
func internedBooleanTerm(b bool) *Term {
if b {
@@ -300,7 +341,7 @@ func internedIntNumberTerm(i int) *Term {
return minusOneTerm
}
- return &Term{Value: Number(strconv.Itoa(i))}
+ return &Term{Value: internedIntNumberValue(i)}
}
// InternedStringTerm returns an interned term with the given string value. If the
@@ -314,10 +355,6 @@ func internedStringTerm(s string) *Term {
return StringTerm(s)
}
-func internedTermValue[T internable](v T) Value {
- return InternedTerm(v).Value
-}
-
func init() {
InternStringTerm(
// Numbers
@@ -341,6 +378,8 @@ func init() {
"data", "input", "result", "keywords", "path", "v1", "error", "partial",
// HTTP
"code", "message", "status_code", "method", "url", "uri", "body", "raw_body", "headers", "query_params",
+ // URI
+ "scheme", "hostname", "port", "raw_path", "raw_query", "fragment",
// JWT
"enc", "cty", "iss", "exp", "nbf", "aud", "secret", "cert",
// Decisions
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go b/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go
index 9081fe7039..922520e958 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/json/json.go
@@ -54,6 +54,9 @@ type NodeToggle struct {
With bool
Annotations bool
AnnotationsRef bool
+ Not bool
+ And bool
+ Or bool
}
// configuredJSONOptions synchronizes access to the global JSON options
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go b/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go
new file mode 100644
index 0000000000..217947bc83
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/mermaid.go
@@ -0,0 +1,431 @@
+// Copyright 2026 The OPA Authors. All rights reserved.
+// Use of this source code is governed by an Apache2
+// license that can be found in the LICENSE file.
+
+package ast
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+)
+
+// TODO: move/rename?
+
+// mermaidFormatter is implemented by AST nodes that can render themselves as a
+// Mermaid flowchart fragment. Each call writes node and edge declarations to b
+// and returns the ID of the node it emitted.
+type mermaidFormatter interface {
+ mermaidFormat(b *mermaidBuilder) string
+}
+
+// mermaidBuilder accumulates Mermaid flowchart lines and issues unique node IDs.
+type mermaidBuilder struct {
+ buf strings.Builder
+ counter int
+}
+
+func (b *mermaidBuilder) newID() string {
+ b.counter++
+ return fmt.Sprintf("n%d", b.counter)
+}
+
+// node writes a Mermaid node declaration and returns its ID.
+// Supported shapes: "rect" (default), "round", "hex", "cyl", "stadium", "trap".
+func (b *mermaidBuilder) node(shape, label string) string {
+ id := b.newID()
+ label = mermaidEscapeLabel(label)
+ b.buf.WriteString(" ")
+ b.buf.WriteString(id)
+ switch shape {
+ case "round":
+ b.buf.WriteString("(\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\")\n")
+ case "hex":
+ b.buf.WriteString("{{\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\"}}\n")
+ case "cyl":
+ b.buf.WriteString("[(\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\")]\n")
+ case "stadium":
+ b.buf.WriteString("([\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\"])\n")
+ case "trap":
+ b.buf.WriteString("[/\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\"/]\n")
+ default: // "rect"
+ b.buf.WriteString("[\"")
+ b.buf.WriteString(label)
+ b.buf.WriteString("\"]\n")
+ }
+ return id
+}
+
+// edge writes a plain directed edge.
+func (b *mermaidBuilder) edge(from, to string) {
+ b.buf.WriteString(" ")
+ b.buf.WriteString(from)
+ b.buf.WriteString(" --> ")
+ b.buf.WriteString(to)
+ b.buf.WriteString("\n")
+}
+
+// edgeLabeled writes a directed edge with a text label.
+func (b *mermaidBuilder) edgeLabeled(from, to, label string) {
+ b.buf.WriteString(" ")
+ b.buf.WriteString(from)
+ b.buf.WriteString(" -->|")
+ b.buf.WriteString(label)
+ b.buf.WriteString("| ")
+ b.buf.WriteString(to)
+ b.buf.WriteString("\n")
+}
+
+// mermaidEscapeLabel replaces characters that break Mermaid quoted node labels.
+func mermaidEscapeLabel(s string) string {
+ return strings.ReplaceAll(s, `"`, "#quot;")
+}
+
+// mermaidGraph returns a Mermaid flowchart string representing the structure of
+// the given module.
+func mermaidGraph(module *Module) string {
+ b := &mermaidBuilder{}
+ module.mermaidFormat(b)
+ var out strings.Builder
+ out.WriteString("flowchart TD\n")
+ out.WriteString(b.buf.String())
+ return out.String()
+}
+
+// --- Module ---
+
+func (mod *Module) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("rect", "Module")
+ pkgID := mod.Package.mermaidFormat(b)
+ b.edgeLabeled(id, pkgID, "package")
+ for _, imp := range mod.Imports {
+ impID := imp.mermaidFormat(b)
+ b.edgeLabeled(id, impID, "import")
+ }
+ for _, rule := range mod.Rules {
+ ruleID := rule.mermaidFormat(b)
+ b.edgeLabeled(id, ruleID, "rule")
+ }
+ return id
+}
+
+// --- Package ---
+
+func (pkg *Package) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("rect", "Package: "+pkg.Path.String())
+}
+
+// --- Import ---
+
+func (imp *Import) mermaidFormat(b *mermaidBuilder) string {
+ label := "Import: " + imp.Path.String()
+ if imp.Alias != "" {
+ label += " as " + string(imp.Alias)
+ }
+ return b.node("rect", label)
+}
+
+// --- Rule ---
+
+func (rule *Rule) mermaidFormat(b *mermaidBuilder) string {
+ ref := rule.Head.Ref().String()
+ label := "Rule: " + ref
+ if rule.Default {
+ label = "Rule: default " + ref
+ }
+ id := b.node("rect", label)
+
+ headID := mermaidFormatHead(rule.Head, b)
+ b.edge(id, headID)
+
+ if len(rule.Body) > 0 {
+ bodyID := b.node("rect", "Body")
+ b.edge(id, bodyID)
+ for i, expr := range rule.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
+ }
+ }
+
+ if rule.Else != nil {
+ elseID := rule.Else.mermaidFormat(b)
+ b.edgeLabeled(id, elseID, "else")
+ }
+ return id
+}
+
+func mermaidFormatHead(head *Head, b *mermaidBuilder) string {
+ id := b.node("rect", "Head")
+
+ refId := head.Ref().mermaidFormat(b)
+ b.edgeLabeled(id, refId, "ref")
+
+ for i, arg := range head.Args {
+ argID := arg.mermaidFormat(b)
+ b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
+ }
+
+ if head.Key != nil {
+ keyID := head.Key.mermaidFormat(b)
+ b.edgeLabeled(id, keyID, "key")
+ }
+
+ if head.Value != nil {
+ valID := head.Value.mermaidFormat(b)
+ b.edgeLabeled(id, valID, "value")
+ }
+
+ return id
+}
+
+func mermaidFormatExpr(expr *Expr, index int, b *mermaidBuilder) string {
+ label := expr.String()
+
+ id := b.node("hex", label)
+
+ switch terms := expr.Terms.(type) {
+ case *Term:
+ termID := terms.mermaidFormat(b)
+ b.edge(id, termID)
+ case []*Term:
+ // terms[0] is the operator; remaining are arguments.
+ for i, t := range terms {
+ tID := t.mermaidFormat(b)
+ if i == 0 {
+ b.edgeLabeled(id, tID, "op")
+ } else {
+ b.edgeLabeled(id, tID, fmt.Sprintf(`"arg[%d]"`, i-1))
+ }
+ }
+ case *SomeDecl:
+ for _, sym := range terms.Symbols {
+ symID := sym.mermaidFormat(b)
+ b.edgeLabeled(id, symID, "symbol")
+ }
+ case *Every:
+ everyID := mermaidFormatEvery(terms, b)
+ b.edge(id, everyID)
+ case *Not:
+ notID := terms.mermaidFormat(b)
+ b.edge(id, notID)
+ case *LogicalAnd:
+ andID := mermaidFormatLogical("and", terms.Lhs, terms.Rhs, b)
+ b.edge(id, andID)
+ case *LogicalOr:
+ orID := mermaidFormatLogical("or", terms.Lhs, terms.Rhs, b)
+ b.edge(id, orID)
+ }
+
+ for _, w := range expr.With {
+ withID := mermaidFormatWith(w, b)
+ b.edgeLabeled(id, withID, "with")
+ }
+ return id
+}
+
+func mermaidFormatEvery(every *Every, b *mermaidBuilder) string {
+ id := b.node("rect", "every")
+ if every.Key != nil {
+ keyID := every.Key.mermaidFormat(b)
+ b.edgeLabeled(id, keyID, "key")
+ }
+ valID := every.Value.mermaidFormat(b)
+ b.edgeLabeled(id, valID, "value")
+ domainID := every.Domain.mermaidFormat(b)
+ b.edgeLabeled(id, domainID, "domain")
+ bodyID := b.node("rect", "Body")
+ b.edge(id, bodyID)
+ for i, expr := range every.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edge(bodyID, exprID)
+ }
+ return id
+}
+
+func mermaidFormatLogical(op string, lhs, rhs Body, b *mermaidBuilder) string {
+ id := b.node("rect", op)
+ lhsID := b.node("rect", "Lhs")
+ b.edge(id, lhsID)
+ for i, expr := range lhs {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edge(lhsID, exprID)
+ }
+ rhsID := b.node("rect", "Rhs")
+ b.edge(id, rhsID)
+ for i, expr := range rhs {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edge(rhsID, exprID)
+ }
+ return id
+}
+
+func mermaidFormatWith(w *With, b *mermaidBuilder) string {
+ id := b.node("rect", "with")
+ targetID := w.Target.mermaidFormat(b)
+ b.edgeLabeled(id, targetID, "target")
+ valID := w.Value.mermaidFormat(b)
+ b.edgeLabeled(id, valID, "value")
+ return id
+}
+
+// --- Not ---
+
+func (not *Not) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("stadium", "not")
+ for i, expr := range not.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edgeLabeled(id, exprID, strconv.Itoa(i))
+ }
+ return id
+}
+
+// --- Term ---
+
+// mermaidFormat delegates to the underlying Value. Term itself does not emit a
+// node; the Value determines the node shape and label.
+func (term *Term) mermaidFormat(b *mermaidBuilder) string {
+ switch v := term.Value.(type) {
+ case mermaidFormatter:
+ return v.mermaidFormat(b)
+ case Set:
+ return mermaidFormatSet(v, b)
+ case Object:
+ return mermaidFormatObject(v, b)
+ default:
+ return b.node("round", term.String())
+ }
+}
+
+// --- Scalar Values ---
+
+func (Null) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", "null")
+}
+
+func (bol Boolean) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", bol.String())
+}
+
+func (num Number) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", num.String())
+}
+
+func (str String) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", str.String())
+}
+
+func (v Var) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", string(v))
+}
+
+// --- Ref ---
+
+func (ref Ref) mermaidFormat(b *mermaidBuilder) string {
+ return b.node("round", "ref: "+ref.String())
+}
+
+// --- Array ---
+
+func (arr *Array) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("cyl", fmt.Sprintf("array[%d]", arr.Len()))
+ arr.Foreach(func(t *Term) {
+ elemID := t.mermaidFormat(b)
+ b.edge(id, elemID)
+ })
+ return id
+}
+
+// --- Set ---
+
+func mermaidFormatSet(s Set, b *mermaidBuilder) string {
+ id := b.node("cyl", fmt.Sprintf("set{%d}", s.Len()))
+ s.Foreach(func(t *Term) {
+ elemID := t.mermaidFormat(b)
+ b.edge(id, elemID)
+ })
+ return id
+}
+
+// --- Object ---
+
+func mermaidFormatObject(o Object, b *mermaidBuilder) string {
+ id := b.node("cyl", fmt.Sprintf("object{%d}", o.Len()))
+ _ = o.Iter(func(k, v *Term) error {
+ kvID := b.node("rect", "kv")
+ b.edge(id, kvID)
+ kID := k.mermaidFormat(b)
+ b.edgeLabeled(kvID, kID, "key")
+ vID := v.mermaidFormat(b)
+ b.edgeLabeled(kvID, vID, "value")
+ return nil
+ })
+ return id
+}
+
+// --- Comprehensions ---
+
+func (ac *ArrayComprehension) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("stadium", "Array Comprehension")
+ termID := ac.Term.mermaidFormat(b)
+ b.edgeLabeled(id, termID, "term")
+ bodyID := b.node("rect", "Body")
+ b.edge(id, bodyID)
+ for i, expr := range ac.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
+ }
+ return id
+}
+
+func (oc *ObjectComprehension) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("stadium", "Object Comprehension")
+ kID := oc.Key.mermaidFormat(b)
+ b.edgeLabeled(id, kID, "key")
+ vID := oc.Value.mermaidFormat(b)
+ b.edgeLabeled(id, vID, "value")
+ bodyID := b.node("rect", "Body")
+ b.edge(id, bodyID)
+ for i, expr := range oc.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
+ }
+ return id
+}
+
+func (sc *SetComprehension) mermaidFormat(b *mermaidBuilder) string {
+ id := b.node("stadium", "Set Comprehension")
+ termID := sc.Term.mermaidFormat(b)
+ b.edgeLabeled(id, termID, "term")
+ bodyID := b.node("rect", "Body")
+ b.edge(id, bodyID)
+ for i, expr := range sc.Body {
+ exprID := mermaidFormatExpr(expr, i, b)
+ b.edgeLabeled(bodyID, exprID, strconv.Itoa(i))
+ }
+ return id
+}
+
+// --- Call ---
+
+func (c Call) mermaidFormat(b *mermaidBuilder) string {
+ opLabel := "call"
+ if len(c) > 0 {
+ opLabel = "call: " + c[0].String()
+ }
+ id := b.node("trap", opLabel)
+ for i, arg := range c[1:] {
+ argID := arg.mermaidFormat(b)
+ b.edgeLabeled(id, argID, fmt.Sprintf(`"arg[%d]"`, i))
+ }
+ return id
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go b/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go
index 9e52b89a67..a164cea9ff 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/parser.go
@@ -148,6 +148,7 @@ type Parser struct {
cache parsedTermCache
recursionDepth int
maxRecursionDepth int
+ notBodies bool
}
type parsedTermCacheItem struct {
@@ -184,8 +185,7 @@ type ParserOptions struct {
FutureKeywords []string
SkipRules bool
// RegoVersion is the version of Rego to parse for.
- RegoVersion RegoVersion
- unreleasedKeywords bool // TODO(sr): cleanup
+ RegoVersion RegoVersion
}
// EffectiveRegoVersion returns the effective RegoVersion to use for parsing.
@@ -256,14 +256,6 @@ func (p *Parser) WithAllFutureKeywords(yes bool) *Parser {
return p
}
-// withUnreleasedKeywords allows using keywords that haven't surfaced
-// as future keywords (see above) yet, but have tests that require
-// them to be parsed
-func (p *Parser) withUnreleasedKeywords(yes bool) *Parser {
- p.po.unreleasedKeywords = yes
- return p
-}
-
// WithCapabilities sets the capabilities structure on the parser.
func (p *Parser) WithCapabilities(c *Capabilities) *Parser {
p.po.Capabilities = c
@@ -434,9 +426,25 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
}
selected := map[string]tokens.Token{}
- if p.po.AllFutureKeywords || p.po.EffectiveRegoVersion() == RegoV1 {
+ if p.po.AllFutureKeywords {
maps.Copy(selected, allowedFutureKeywords)
} else {
+ if p.po.EffectiveRegoVersion() == RegoV1 {
+ for kw := range futureKeywordsV0 {
+ tok, ok := allowedFutureKeywords[kw]
+ if !ok {
+ return nil, nil, Errors{
+ &Error{
+ Code: ParseErr,
+ Message: fmt.Sprintf("unknown future keyword: %v", kw),
+ Location: nil,
+ },
+ }
+ }
+ selected[kw] = tok
+ }
+ }
+
for _, kw := range p.po.FutureKeywords {
tok, ok := allowedFutureKeywords[kw]
if !ok {
@@ -451,10 +459,15 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
selected[kw] = tok
}
}
+
+ if _, ok := selected["not"]; ok {
+ p.notBodies = true
+ }
+
p.s.s = p.s.s.WithKeywords(selected)
if p.po.EffectiveRegoVersion() == RegoV1 {
- for kw, tok := range allowedFutureKeywords {
+ for kw, tok := range futureKeywordsV0 {
p.s.s.AddKeyword(kw, tok)
}
}
@@ -545,7 +558,7 @@ func (p *Parser) parseAnnotations(stmts []Statement) []Statement {
}
func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) {
- numBlocks := CountFunc(comments, isMetadataComment)
+ numBlocks := CountFunc(comments, IsMetadataComment)
if numBlocks == 0 {
return nil, nil
}
@@ -557,7 +570,7 @@ func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) {
}
for i := range comments {
- if isMetadataComment(comments[i]) { // scan until end of block
+ if IsMetadataComment(comments[i]) { // scan until end of block
mdp.Reset(comments[i].Location)
for i++; i < len(comments) && !blockBuster(comments[i], comments[i-1]); i++ {
mdp.Append(comments[i])
@@ -576,12 +589,12 @@ func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) {
return stmts, errs
}
-func isMetadataComment(c *Comment) bool {
+func IsMetadataComment(c *Comment) bool {
return c.Location.Col == 1 && bytes.HasPrefix(bytes.TrimSpace(c.Text), metadataBytes)
}
func blockBuster(curr, prev *Comment) bool { // or endOfBlock, but the name was too good to pass up
- return curr.Location.Col != 1 || curr.Location.Row-1 != prev.Location.Row
+ return curr.Location.Col != 1 || curr.Location.Row-1 != prev.Location.Row || IsMetadataComment(curr)
}
func (p *Parser) parsePackage() *Package {
@@ -1211,6 +1224,32 @@ func (p *Parser) parseLiteral() (expr *Expr) {
}
}()
+ // LHS explicit-body operand of an `and`/`or` binary: `{ body } and/or ...`.
+ // Speculatively parse `{...}`; if followed by an and/or operator, build the
+ // binary. Otherwise, restore and fall through to regular handling.
+ if p.s.tok == tokens.LBrace && p.logicalKeywordsActive() {
+ s := p.save()
+ bodyLoc := p.s.Loc()
+ p.scan()
+ body := p.parseBody(tokens.RBrace)
+ if body != nil {
+ p.scan() // consume `}`
+ if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
+ outer := p.parseLogicalOrChain(body, true, bodyLoc)
+ if outer == nil {
+ return nil
+ }
+ if p.s.tok == tokens.With {
+ if outer.With = p.parseWith(); outer.With == nil {
+ return nil
+ }
+ }
+ return outer
+ }
+ }
+ p.restore(s)
+ }
+
// Check that we're not parsing a ref
if p.isAllowedRefKeyword(p.s.tok) {
// Scan ahead
@@ -1225,17 +1264,18 @@ func (p *Parser) parseLiteral() (expr *Expr) {
}
}
- var negated bool
- if p.s.tok == tokens.Not {
- s := p.save()
- p.scanWS()
- tok := p.s.tok
- p.restore(s)
+ negated := isNegatedExpression(p)
- if tok != tokens.Dot && tok != tokens.LBrack {
- p.scan()
- negated = true
+ if negated && p.notBodies && p.s.tok == tokens.LBrace {
+ nb := p.parseNotBody()
+
+ if nb != nil && p.s.tok == tokens.With {
+ if nb.With = p.parseWith(); nb.With == nil {
+ return nil
+ }
}
+
+ return nb
}
switch p.s.tok {
@@ -1269,11 +1309,14 @@ func (p *Parser) isAllowedRefKeywordStr(s string) bool {
}
func (p *Parser) parseLiteralExpr(negated bool) *Expr {
+ startOffset := p.s.loc.Offset
+ startLoc := p.s.Loc()
s := p.save()
expr := p.parseExpr()
if expr != nil {
- expr.Negated = negated
+ var withLoc *Location
if p.s.tok == tokens.With {
+ withLoc = p.s.Loc()
if expr.With = p.parseWith(); expr.With == nil {
return nil
}
@@ -1295,6 +1338,42 @@ func (p *Parser) parseLiteralExpr(negated bool) *Expr {
}
}
}
+
+ if negated && p.notBodies {
+ // Move 'with' statement to outer not expr
+ w := expr.With
+ expr.With = nil
+ expr = NewExpr(&Not{Body: NewBody(expr), Location: p.s.Loc()})
+ expr.With = w
+ } else {
+ expr.Negated = negated
+ }
+
+ if p.s.tok == tokens.LogicalAnd || p.s.tok == tokens.LogicalOr {
+ if withLoc != nil {
+ kw := p.s.tok.String()
+ p.errorf(withLoc,
+ "`with` modifier is not allowed on operand of `%s`; wrap the operand in `{...}` to scope, or move `with` after the %s expression to apply it to the whole expression",
+ kw, kw)
+ return nil
+ }
+
+ if expr.Location == nil {
+ startLoc.Text = p.s.Text(startOffset, p.s.lastEnd)
+ expr.SetLoc(startLoc)
+ }
+
+ outer := p.parseLogicalOrChain(NewBody(expr), false, expr.Location)
+ if outer == nil {
+ return nil
+ }
+ if p.s.tok == tokens.With {
+ if outer.With = p.parseWith(); outer.With == nil {
+ return nil
+ }
+ }
+ return outer
+ }
}
return expr
}
@@ -1427,6 +1506,184 @@ func (p *Parser) parseSome() *Expr {
return NewExpr(decl).SetLocation(decl.Location)
}
+func (p *Parser) parseNotBody() *Expr {
+ loc := p.s.Loc()
+ p.scan()
+
+ body := p.parseBody(tokens.RBrace)
+ if body == nil {
+ return nil
+ }
+ p.scan()
+
+ not := &Not{Body: body, ExplicitBody: true, Location: loc}
+ return NewExpr(not).SetLocation(loc)
+}
+
+// logicalKeywordsActive reports whether the scanner currently treats `and` or
+// `or` as keywords.
+func (p *Parser) logicalKeywordsActive() bool {
+ return p.s.s.IsKeyword("and") || p.s.s.IsKeyword("or")
+}
+
+// parseLogicalOrChain folds a left-associative chain of `or` operators on top
+// of the given lhs, with `and`-chains folded in first because `and` binds
+// tighter. The lhs is supplied as a (body, explicit, location) triple so that
+// both implicit single-expression operands and explicit `{...}` operands can
+// be represented.
+func (p *Parser) parseLogicalOrChain(lhsBody Body, lhsExplicit bool, lhsLoc *Location) *Expr {
+ if p.s.tok != tokens.LogicalAnd && p.s.tok != tokens.LogicalOr {
+ panic("expected logical and/or operator at p.s.tok")
+ }
+
+ if !p.enter() {
+ return nil
+ }
+ defer p.leave()
+
+ // Higher precedence first: fold any leading `and`-chain into the lhs.
+ if p.s.tok == tokens.LogicalAnd {
+ andExpr := p.parseLogicalAndChain(lhsBody, lhsExplicit, lhsLoc)
+ if andExpr == nil {
+ return nil
+ }
+ lhsBody = NewBody(andExpr)
+ lhsExplicit = false
+ }
+
+ for p.s.tok == tokens.LogicalOr {
+ p.scan()
+
+ rhsBody, rhsExplicit := p.parseLogicalOperand()
+ if rhsBody == nil {
+ return nil
+ }
+
+ // RHS may extend into a higher-precedence `and`-chain.
+ if p.s.tok == tokens.LogicalAnd {
+ rhsLoc := rhsBody[0].Location
+ andExpr := p.parseLogicalAndChain(rhsBody, rhsExplicit, rhsLoc)
+ if andExpr == nil {
+ return nil
+ }
+ rhsBody = NewBody(andExpr)
+ rhsExplicit = false
+ }
+
+ node := &LogicalOr{
+ Lhs: lhsBody,
+ Rhs: rhsBody,
+ ExplicitLhs: lhsExplicit,
+ ExplicitRhs: rhsExplicit,
+ Location: lhsLoc,
+ }
+ wrapper := NewExpr(node).SetLocation(lhsLoc)
+ lhsBody = NewBody(wrapper)
+ lhsExplicit = false
+ }
+
+ return lhsBody[0]
+}
+
+// parseLogicalAndChain folds a left-associative chain of `and` operators on
+// top of the given lhs.
+func (p *Parser) parseLogicalAndChain(lhsBody Body, lhsExplicit bool, lhsLoc *Location) *Expr {
+ if p.s.tok != tokens.LogicalAnd {
+ panic("expected logical and operator at p.s.tok")
+ }
+
+ if !p.enter() {
+ return nil
+ }
+ defer p.leave()
+
+ for p.s.tok == tokens.LogicalAnd {
+ p.scan()
+
+ rhsBody, rhsExplicit := p.parseLogicalOperand()
+ if rhsBody == nil {
+ return nil
+ }
+
+ node := &LogicalAnd{
+ Lhs: lhsBody,
+ Rhs: rhsBody,
+ ExplicitLhs: lhsExplicit,
+ ExplicitRhs: rhsExplicit,
+ Location: lhsLoc,
+ }
+ wrapper := NewExpr(node).SetLocation(lhsLoc)
+ lhsBody = NewBody(wrapper)
+ lhsExplicit = false
+ }
+
+ return lhsBody[0]
+}
+
+func isNegatedExpression(p *Parser) bool {
+ if p.s.tok == tokens.Not {
+ // Distinguish the `not` keyword from a ref like `not.x`.
+ s := p.save()
+ p.scanWS()
+ tok := p.s.tok
+ p.restore(s)
+ if tok != tokens.Dot && tok != tokens.LBrack {
+ p.scan()
+ return true
+ }
+ }
+ return false
+}
+
+// parseLogicalOperand parses a single operand of an `and`/`or` expression.
+// Returns the operand body and whether it was parsed from an explicit `{...}`
+// body. Returns (nil, false) on parse error.
+func (p *Parser) parseLogicalOperand() (Body, bool) {
+ if p.s.tok == tokens.LBrace {
+ loc := p.s.Loc()
+ p.scan()
+ body := p.parseBody(tokens.RBrace)
+ if body == nil {
+ return nil, false
+ }
+ p.scan()
+ _ = loc
+ return body, true
+ }
+
+ negated := isNegatedExpression(p)
+
+ if negated && p.notBodies && p.s.tok == tokens.LBrace {
+ nb := p.parseNotBody()
+ if nb == nil {
+ return nil, false
+ }
+ return NewBody(nb), false
+ }
+
+ startOffset := p.s.loc.Offset
+ startLoc := p.s.Loc()
+ expr := p.parseExpr()
+ if expr == nil {
+ return nil, false
+ }
+
+ if expr.Location == nil {
+ startLoc.Text = p.s.Text(startOffset, p.s.lastEnd)
+ expr.SetLoc(startLoc)
+ }
+
+ if negated && p.notBodies {
+ // Don't attach any existing 'with' statements, they belong to the and/or, not the negated expression.
+ notNode := &Not{Body: NewBody(expr), Location: expr.Location}
+ expr = NewExpr(notNode).SetLocation(expr.Location)
+ } else if negated {
+ expr.Negated = true
+ }
+
+ return NewBody(expr), false
+}
+
func (p *Parser) parseEvery() *Expr {
qb := &Every{}
qb.SetLoc(p.s.Loc())
@@ -1731,6 +1988,7 @@ func (p *Parser) parseTerm() *Term {
s0 := p.save()
var term *Term
+ var unaryMinusLoc *Location
switch p.s.tok {
case tokens.Null:
term = NullTerm().SetLocation(p.s.Loc())
@@ -1738,7 +1996,21 @@ func (p *Parser) parseTerm() *Term {
term = BooleanTerm(true).SetLocation(p.s.Loc())
case tokens.False:
term = BooleanTerm(false).SetLocation(p.s.Loc())
- case tokens.Sub, tokens.Dot, tokens.Number:
+ case tokens.Sub:
+ loc := p.s.Loc()
+ s := p.save()
+ p.scan()
+ if p.s.tok == tokens.Ident || p.s.tok == tokens.Contains {
+ // Unary minus on a reference: -ref → minus(0, ref).
+ // parseTermFinish below will resolve the full ref (e.g. input.number),
+ // after which we wrap the result in a minus call.
+ unaryMinusLoc = loc
+ term = p.parseVar()
+ } else {
+ p.restore(s)
+ term = p.parseNumber()
+ }
+ case tokens.Dot, tokens.Number:
term = p.parseNumber()
case tokens.String:
term = p.parseString()
@@ -1768,6 +2040,10 @@ func (p *Parser) parseTerm() *Term {
}
term = p.parseTermFinish(term, false)
+ if unaryMinusLoc != nil && term != nil {
+ zero := IntNumberTerm(0).SetLocation(unaryMinusLoc)
+ term = p.setLoc(Minus.Call(zero, term), unaryMinusLoc, unaryMinusLoc.Offset, p.s.lastEnd)
+ }
p.parsedTermCachePush(term, s0)
return term
}
@@ -2572,7 +2848,7 @@ func (p *Parser) illegal(note string, a ...any) {
tok := p.s.tok.String()
tokType := "token"
- if _, ok := allFutureKeywords[tok]; ok || tokens.IsKeyword(p.s.tok) {
+ if tokens.IsKeyword(p.s.tok) || isFutureKeywordToken(p.s.tok) {
tokType = "keyword"
}
@@ -2748,6 +3024,7 @@ type rawAnnotation struct {
Schemas []map[string]any `yaml:"schemas"`
Compile map[string]any `yaml:"compile"`
Custom map[string]any `yaml:"custom"`
+ Labels map[string]any `yaml:"labels"`
}
type metadataParser struct {
@@ -2898,6 +3175,15 @@ func (b *metadataParser) Parse() (result *Annotations, err error) {
}
}
+ if raw.Labels != nil {
+ result.Labels = make(map[string]any, len(raw.Labels))
+ for k, v := range raw.Labels {
+ if result.Labels[k], err = convertYAMLMapKeyTypes(v, nil); err != nil {
+ return nil, err
+ }
+ }
+ }
+
result.Location = b.loc
// recreate original text of entire metadata block for location text attribute
@@ -3101,7 +3387,11 @@ func convertYAMLMapKeyTypes(x any, path []string) (any, error) {
// futureKeywords is the source of truth for future keywords that will
// eventually become standard keywords inside of Rego.
-var futureKeywords = map[string]tokens.Token{}
+var futureKeywords = map[string]tokens.Token{
+ "not": tokens.Not,
+ "and": tokens.LogicalAnd,
+ "or": tokens.LogicalOr,
+}
// futureKeywordsV0 is the source of truth for future keywords that were
// not yet a standard part of Rego in v0, and required importing.
@@ -3114,6 +3404,22 @@ var futureKeywordsV0 = map[string]tokens.Token{
var allFutureKeywords map[string]tokens.Token
+// experimentalFutureKeywords are future keywords that exist in the parser but are
+// intentionally hidden from the default capabilities advertisement.
+// They are only activated when a policy imports them AND the active
+// capabilities explicitly list them.
+var experimentalFutureKeywords = map[string]struct{}{
+ "and": {},
+ "or": {},
+}
+
+var allFutureKeywordTokens map[tokens.Token]struct{}
+
+func isFutureKeywordToken(tok tokens.Token) bool {
+ _, ok := allFutureKeywordTokens[tok]
+ return ok
+}
+
func IsFutureKeyword(s string) bool {
return IsFutureKeywordForRegoVersion(s, RegoV1)
}
@@ -3167,8 +3473,13 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
return
}
- kwds = []string{keyword} // overwrite
+ if keyword == "not" {
+ p.notBodies = true
+ } else {
+ kwds = []string{keyword} // overwrite
+ }
}
+
for _, kw := range kwds {
p.s.s.AddKeyword(kw, allowedFutureKeywords[kw])
}
@@ -3211,6 +3522,11 @@ func init() {
allFutureKeywords = map[string]tokens.Token{}
maps.Copy(allFutureKeywords, futureKeywords)
maps.Copy(allFutureKeywords, futureKeywordsV0)
+
+ allFutureKeywordTokens = make(map[tokens.Token]struct{}, len(allFutureKeywords))
+ for _, tok := range allFutureKeywords {
+ allFutureKeywordTokens[tok] = struct{}{}
+ }
}
// enter increments the recursion depth counter and checks if it exceeds the maximum.
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go b/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go
index ab3de33a1f..8b63182c0f 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/parser_ext.go
@@ -46,6 +46,16 @@ func MustParseExpr(input string) *Expr {
return parsed
}
+// MustParseExprWithOpts returns a parsed expression.
+// If an error occurs during parsing, panic.
+func MustParseExprWithOpts(input string, opts ParserOptions) *Expr {
+ parsed, err := ParseExprWithOpts(input, opts)
+ if err != nil {
+ panic(err)
+ }
+ return parsed
+}
+
// MustParseImports returns a slice of imports.
// If an error occurs during parsing, panic.
func MustParseImports(input string) []*Import {
@@ -523,6 +533,20 @@ func ParseExpr(input string) (*Expr, error) {
return body[0], nil
}
+// ParseExprWithOpts returns exactly one expression.
+// If multiple expressions are parsed, an error is returned.
+// It does _not_ set SkipRules: true on its own, but respects whatever ParserOptions it's been given.
+func ParseExprWithOpts(input string, popts ParserOptions) (*Expr, error) {
+ body, err := ParseBodyWithOpts(input, popts)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse expression: %w", err)
+ }
+ if len(body) != 1 {
+ return nil, fmt.Errorf("expected exactly one expression but got: %v", body)
+ }
+ return body[0], nil
+}
+
// ParsePackage returns exactly one Package.
// If multiple statements are parsed, an error is returned.
func ParsePackage(input string) (*Package, error) {
@@ -632,8 +656,7 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
WithAllFutureKeywords(popts.AllFutureKeywords).
WithCapabilities(popts.Capabilities).
WithSkipRules(popts.SkipRules).
- WithRegoVersion(popts.RegoVersion).
- withUnreleasedKeywords(popts.unreleasedKeywords)
+ WithRegoVersion(popts.RegoVersion)
stmts, comments, errs := parser.Parse()
if len(errs) > 0 {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go b/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go
index 632b5aa6d5..470c6ab6f7 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/policy.go
@@ -289,6 +289,24 @@ type (
Location *Location `json:"location,omitempty"`
}
+ // LogicalAnd represents a logical conjunction (`lhs and rhs`).
+ LogicalAnd struct {
+ Lhs Body `json:"lhs"`
+ Rhs Body `json:"rhs"`
+ ExplicitLhs bool `json:"explicit_lhs,omitempty"`
+ ExplicitRhs bool `json:"explicit_rhs,omitempty"`
+ Location *Location `json:"location,omitempty"`
+ }
+
+ // LogicalOr represents a logical disjunction (`lhs or rhs`).
+ LogicalOr struct {
+ Lhs Body `json:"lhs"`
+ Rhs Body `json:"rhs"`
+ ExplicitLhs bool `json:"explicit_lhs,omitempty"`
+ ExplicitRhs bool `json:"explicit_rhs,omitempty"`
+ Location *Location `json:"location,omitempty"`
+ }
+
// With represents a modifier on an expression.
With struct {
Target *Term `json:"target"`
@@ -989,11 +1007,7 @@ func (head *Head) HasDynamicRef() bool {
// Copy returns a deep copy of a.
func (a Args) Copy() Args {
- cpy := make(Args, 0, len(a))
- for _, t := range a {
- cpy = append(cpy, t.Copy())
- }
- return cpy
+ return termSliceCopy(a)
}
func (a Args) String() string {
@@ -1150,7 +1164,7 @@ func (body Body) Vars(params VarVisitorParams) VarSet {
// NewExpr returns a new Expr object.
func NewExpr(terms any) *Expr {
switch terms.(type) {
- case *SomeDecl, *Every, *Term, []*Term: // ok
+ case *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr, *Term, []*Term: // ok
default:
panic("unreachable")
}
@@ -1163,7 +1177,14 @@ func NewExpr(terms any) *Expr {
}
// Complement returns a copy of this expression with the negation flag flipped.
+// Note: complementing an expression containing an ast.Not term is invalid, as this will create a double negation;
+// ast.Not terms may contain multiple expressions, and can therefore not be un-negated to a single *ast.Expr; use ast.Complement() instead.
+// Passing an expression containing an ast.Not with multiple expressions in its body will cause a panic.
func (expr *Expr) Complement() *Expr {
+ if n, ok := expr.Terms.(*Not); ok && len(n.Body) > 1 {
+ panic(fmt.Errorf("cannot complement %T containing multiple expressions (%s)", n, n))
+ }
+
cpy := *expr
cpy.Negated = !cpy.Negated
return &cpy
@@ -1246,6 +1267,18 @@ func (expr *Expr) Compare(other *Expr) int {
if cmp := Compare(t, other.Terms.(*Every)); cmp != 0 {
return cmp
}
+ case *Not:
+ if cmp := Compare(t, other.Terms.(*Not)); cmp != 0 {
+ return cmp
+ }
+ case *LogicalAnd:
+ if cmp := Compare(t, other.Terms.(*LogicalAnd)); cmp != 0 {
+ return cmp
+ }
+ case *LogicalOr:
+ if cmp := Compare(t, other.Terms.(*LogicalOr)); cmp != 0 {
+ return cmp
+ }
}
return withSliceCompare(expr.With, other.With)
@@ -1261,6 +1294,12 @@ func (expr *Expr) sortOrder() int {
return 2
case *Every:
return 3
+ case *Not:
+ return 4
+ case *LogicalAnd:
+ return 5
+ case *LogicalOr:
+ return 6
}
return -1
}
@@ -1293,6 +1332,12 @@ func (expr *Expr) Copy() *Expr {
cpy.Terms = ts.Copy()
case *Every:
cpy.Terms = ts.Copy()
+ case *Not:
+ cpy.Terms = ts.Copy()
+ case *LogicalAnd:
+ cpy.Terms = ts.Copy()
+ case *LogicalOr:
+ cpy.Terms = ts.Copy()
}
return cpy
@@ -1310,6 +1355,10 @@ func (expr *Expr) Hash() int {
}
case *Term:
s += ts.Value.Hash()
+ case *LogicalAnd:
+ s += ts.Hash()
+ case *LogicalOr:
+ s += ts.Hash()
}
if expr.Negated {
s++
@@ -1356,12 +1405,35 @@ func (expr *Expr) IsEvery() bool {
return ok
}
+// IsNot returns true if this expression is a 'not' expression.
+func (expr *Expr) IsNot() bool {
+ _, ok := expr.Terms.(*Not)
+ return ok
+}
+
+// IsNegated returns true if Negated or IsNot() returns true for this expression
+func (expr *Expr) IsNegated() bool {
+ return expr.Negated || expr.IsNot()
+}
+
// IsSome returns true if this expression is a 'some' expression.
func (expr *Expr) IsSome() bool {
_, ok := expr.Terms.(*SomeDecl)
return ok
}
+// IsAnd returns true if this expression is a logical 'and' expression.
+func (expr *Expr) IsAnd() bool {
+ _, ok := expr.Terms.(*LogicalAnd)
+ return ok
+}
+
+// IsOr returns true if this expression is a logical 'or' expression.
+func (expr *Expr) IsOr() bool {
+ _, ok := expr.Terms.(*LogicalOr)
+ return ok
+}
+
// Operator returns the name of the function or built-in this expression refers
// to. If this expression is not a function call, returns nil.
func (expr *Expr) Operator() Ref {
@@ -1416,6 +1488,12 @@ func (expr *Expr) IsGround() bool {
}
case *Term:
return ts.IsGround()
+ case *Not:
+ return ts.IsGround()
+ case *LogicalAnd:
+ return ts.Lhs.IsGround() && ts.Rhs.IsGround()
+ case *LogicalOr:
+ return ts.Lhs.IsGround() && ts.Rhs.IsGround()
}
return true
}
@@ -1670,6 +1748,182 @@ func (q *Every) MarshalJSON() ([]byte, error) {
return json.Marshal(data)
}
+func (a *LogicalAnd) String() string {
+ return formatBinaryLogical("and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs)
+}
+
+func (a *LogicalAnd) Loc() *Location {
+ return a.Location
+}
+
+func (a *LogicalAnd) SetLoc(l *Location) {
+ a.Location = l
+}
+
+func (a *LogicalAnd) Copy() *LogicalAnd {
+ cpy := *a
+ cpy.Lhs = a.Lhs.Copy()
+ cpy.Rhs = a.Rhs.Copy()
+ return &cpy
+}
+
+// Compare returns an integer indicating whether a is less than, equal to, or
+// greater than other. The ExplicitLhs/ExplicitRhs fields are ignored, as they
+// describe surface syntax rather than semantic content.
+func (a *LogicalAnd) Compare(other *LogicalAnd) int {
+ if cmp := a.Lhs.Compare(other.Lhs); cmp != 0 {
+ return cmp
+ }
+ return a.Rhs.Compare(other.Rhs)
+}
+
+func (a *LogicalAnd) Hash() int {
+ return a.Lhs.Hash() + a.Rhs.Hash()
+}
+
+func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
+ data := map[string]any{
+ "type": "and",
+ "lhs": a.Lhs,
+ "rhs": a.Rhs,
+ }
+ if a.ExplicitLhs {
+ data["explicit_lhs"] = true
+ }
+ if a.ExplicitRhs {
+ data["explicit_rhs"] = true
+ }
+
+ if astJSON.GetOptions().MarshalOptions.IncludeLocation.And {
+ if a.Location != nil {
+ data["location"] = a.Location
+ }
+ }
+
+ return json.Marshal(data)
+}
+
+func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
+ v := map[string]any{}
+ if err := util.UnmarshalJSON(bs, &v); err != nil {
+ return err
+ }
+ return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
+}
+
+func (o *LogicalOr) String() string {
+ return formatBinaryLogical("or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs)
+}
+
+func (o *LogicalOr) Loc() *Location {
+ return o.Location
+}
+
+func (o *LogicalOr) SetLoc(l *Location) {
+ o.Location = l
+}
+
+func (o *LogicalOr) Copy() *LogicalOr {
+ cpy := *o
+ cpy.Lhs = o.Lhs.Copy()
+ cpy.Rhs = o.Rhs.Copy()
+ return &cpy
+}
+
+// Compare returns an integer indicating whether o is less than, equal to, or
+// greater than other. The ExplicitLhs/ExplicitRhs fields are ignored, as they
+// describe surface syntax rather than semantic content.
+func (o *LogicalOr) Compare(other *LogicalOr) int {
+ if cmp := o.Lhs.Compare(other.Lhs); cmp != 0 {
+ return cmp
+ }
+ return o.Rhs.Compare(other.Rhs)
+}
+
+func (o *LogicalOr) Hash() int {
+ return o.Lhs.Hash() + o.Rhs.Hash()
+}
+
+func (o *LogicalOr) MarshalJSON() ([]byte, error) {
+ data := map[string]any{
+ "type": "or",
+ "lhs": o.Lhs,
+ "rhs": o.Rhs,
+ }
+ if o.ExplicitLhs {
+ data["explicit_lhs"] = true
+ }
+ if o.ExplicitRhs {
+ data["explicit_rhs"] = true
+ }
+
+ if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or {
+ if o.Location != nil {
+ data["location"] = o.Location
+ }
+ }
+
+ return json.Marshal(data)
+}
+
+func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
+ v := map[string]any{}
+ if err := util.UnmarshalJSON(bs, &v); err != nil {
+ return err
+ }
+ return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
+}
+
+func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error {
+ lhsRaw, ok := v["lhs"].([]any)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"])
+ }
+ l, err := unmarshalBody(lhsRaw)
+ if err != nil {
+ return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err)
+ }
+ *lhs = l
+
+ rhsRaw, ok := v["rhs"].([]any)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"])
+ }
+ r, err := unmarshalBody(rhsRaw)
+ if err != nil {
+ return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err)
+ }
+ *rhs = r
+
+ if x, ok := v["explicit_lhs"]; ok {
+ b, ok := x.(bool)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x)
+ }
+ *explicitLhs = b
+ }
+ if x, ok := v["explicit_rhs"]; ok {
+ b, ok := x.(bool)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x)
+ }
+ *explicitRhs = b
+ }
+
+ return nil
+}
+
+func formatBinaryLogical(op string, lhs, rhs Body, explicitLhs, explicitRhs bool) string {
+ return formatLogicalOperand(lhs, explicitLhs) + " " + op + " " + formatLogicalOperand(rhs, explicitRhs)
+}
+
+func formatLogicalOperand(b Body, explicit bool) string {
+ if explicit {
+ return "{ " + b.String() + " }"
+ }
+ return b.String()
+}
+
func (w *With) String() string {
buf, _ := w.AppendText(make([]byte, 0, w.StringLength()))
return util.ByteSliceToString(buf)
@@ -1775,6 +2029,8 @@ func Copy(x any) any {
return x.Copy()
case *Every:
return x.Copy()
+ case *Not:
+ return x.Copy()
case *Term:
return x.Copy()
case *ArrayComprehension:
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go b/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go
index 1c9813aa97..cead974b16 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/policy_appenders.go
@@ -325,6 +325,38 @@ func (c *Comment) AppendText(buf []byte) ([]byte, error) {
return append(append(buf, '#'), c.Text...), nil
}
+func (a *LogicalAnd) AppendText(buf []byte) ([]byte, error) {
+ return appendLogical(buf, "and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs)
+}
+
+func (o *LogicalOr) AppendText(buf []byte) ([]byte, error) {
+ return appendLogical(buf, "or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs)
+}
+
+func appendLogical(buf []byte, op string, lhs, rhs Body, explicitLhs, explicitRhs bool) ([]byte, error) {
+ var err error
+ if buf, err = appendLogicalOperand(buf, lhs, explicitLhs); err != nil {
+ return nil, err
+ }
+ buf = append(buf, ' ')
+ buf = append(buf, op...)
+ buf = append(buf, ' ')
+ return appendLogicalOperand(buf, rhs, explicitRhs)
+}
+
+func appendLogicalOperand(buf []byte, b Body, explicit bool) ([]byte, error) {
+ if !explicit && len(b) == 1 {
+ return b.AppendText(buf)
+ }
+
+ buf = append(buf, "{ "...)
+ var err error
+ if buf, err = b.AppendText(buf); err != nil {
+ return nil, err
+ }
+ return append(buf, " }"...), nil
+}
+
// RulePath returns the string representation of the rule's path, i.e. its package path followed by the rule head ref.
func RulePath(r *Rule) string {
if r == nil {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go b/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go
index aa34f37471..1ee32ee555 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/pretty.go
@@ -50,6 +50,8 @@ func (pp *prettyPrinter) Before(x any) bool {
pp.writeIndent("%v %v", TypeName(x), strings.Join(extras, " "))
case Null, Boolean, Number, String, Var:
pp.writeValue(x)
+ case *Not:
+ pp.writeType(x)
default:
pp.writeType(x)
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go b/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go
index fe53227d24..ccdc7dc7a4 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/string_length.go
@@ -125,6 +125,19 @@ func (c Call) StringLength() int {
return c[0].StringLength() + 2 + TermSliceStringLength(c[1:], 2)
}
+// comprehensionTermStringLength returns the string length of a term when
+// rendered inside a comprehension head, where infix operators are rendered
+// in infix notation wrapped in parens.
+func comprehensionTermStringLength(t *Term) int {
+ if call, ok := t.Value.(Call); ok && len(call) == 3 {
+ if bi, found := BuiltinMap[call[0].String()]; found && bi.Infix != "" && bi.Infix != "in" {
+ // "(left op right)" = left + " " + op + " " + right + "()"
+ return comprehensionTermStringLength(call[1]) + len(bi.Infix) + comprehensionTermStringLength(call[2]) + 4
+ }
+ }
+ return t.StringLength()
+}
+
func (r Ref) StringLength() (n int) {
rlen := len(r)
if rlen == 0 {
@@ -165,16 +178,16 @@ func (v Var) StringLength() int {
}
func (s *SetComprehension) StringLength() int {
- return s.Term.StringLength() + s.Body.StringLength() + 5 // {} and " | "
+ return comprehensionTermStringLength(s.Term) + s.Body.StringLength() + 5 // {} and " | "
}
func (a *ArrayComprehension) StringLength() int {
- return a.Term.StringLength() + a.Body.StringLength() + 5 // [] and " | "
+ return comprehensionTermStringLength(a.Term) + a.Body.StringLength() + 5 // [] and " | "
}
func (o *ObjectComprehension) StringLength() (n int) {
- n += o.Key.StringLength()
- n += o.Value.StringLength()
+ n += comprehensionTermStringLength(o.Key)
+ n += comprehensionTermStringLength(o.Value)
n += o.Body.StringLength()
return n + 7 // "{}"", " | ", and ": "
}
@@ -349,3 +362,31 @@ func (s *SomeDecl) StringLength() int {
func (c *Comment) StringLength() int {
return 1 + len(c.Text) // '#' + text
}
+
+func (not *Not) StringLength() int {
+ if !not.ExplicitBody && len(not.Body) == 1 {
+ // "not ..."
+ return 4 + not.Body.StringLength()
+ }
+ // "not {...}"
+ return 6 + not.Body.StringLength()
+}
+
+func (a *LogicalAnd) StringLength() int {
+ return logicalOperandStringLength(a.Lhs, a.ExplicitLhs) +
+ 5 + // " and "
+ logicalOperandStringLength(a.Rhs, a.ExplicitRhs)
+}
+
+func (o *LogicalOr) StringLength() int {
+ return logicalOperandStringLength(o.Lhs, o.ExplicitLhs) +
+ 4 + // " or "
+ logicalOperandStringLength(o.Rhs, o.ExplicitRhs)
+}
+
+func logicalOperandStringLength(b Body, explicit bool) int {
+ if !explicit && len(b) == 1 {
+ return b.StringLength()
+ }
+ return b.StringLength() + 4 // "{ " + body + " }"
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go b/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go
index 72ec03f8cc..8bb7e7ddfd 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/strings.go
@@ -50,6 +50,8 @@ func ValueName(x Value) string {
return "setcomprehension"
case *TemplateString:
return "templatestring"
+ case *Not:
+ return "not"
}
return TypeName(x)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/term.go b/vendor/github.com/open-policy-agent/opa/v1/ast/term.go
index 436b22eb2a..fb1e7aeae5 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/term.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/term.go
@@ -562,6 +562,154 @@ func IsScalar(v Value) bool {
return false
}
+// Not is both a Term and a Node
+type Not struct {
+ Body Body `json:"body"`
+ ExplicitBody bool `json:"explicit_body,omitempty"`
+ Location *Location `json:"location,omitempty"`
+}
+
+func NewNot(exprs ...*Expr) *Not {
+ return &Not{
+ Body: NewBody(exprs...),
+ }
+}
+
+func NotTerm(exprs ...*Expr) *Term {
+ return NewTerm(NewNot(exprs...))
+}
+
+func NotExpr(exprs ...*Expr) *Expr {
+ return NewExpr(&Not{
+ Body: NewBody(exprs...),
+ })
+}
+
+func Complement(expr *Expr) []*Expr {
+ if expr.Negated {
+ // Legacy negation
+ return []*Expr{expr.Complement()}
+ }
+
+ if n, ok := expr.Terms.(*Not); ok {
+ b := make([]*Expr, 0, len(n.Body))
+ for _, e := range n.Body {
+ cpy := *e
+
+ for _, w := range expr.With {
+ cpy.With = append(cpy.With, w.Copy())
+ }
+
+ b = append(b, &cpy)
+ }
+ return b
+ }
+
+ return []*Expr{NotExpr(expr)}
+}
+
+// Copy returns a deep copy of n.
+func (n *Not) Copy() *Not {
+ cpy := *n
+ cpy.Body = n.Body.Copy()
+ return &cpy
+}
+
+func (n *Not) Loc() *Location {
+ return n.Location
+}
+
+func (n *Not) SetLoc(l *Location) {
+ n.Location = l
+}
+
+func (n *Not) Equal(other Value) bool {
+ return n.Compare(other) == 0
+}
+
+func (n *Not) Compare(other Value) int {
+ switch o := other.(type) {
+ case *Not:
+ // We don't consider the ExplicitBody field, as it has no effect on expression negation
+ return n.Body.Compare(o.Body)
+ default:
+ return -1
+ }
+}
+
+func (n *Not) Find(path Ref) (Value, error) {
+ if len(path) == 0 {
+ return n, nil
+ }
+ return nil, errFindNotFound
+}
+
+func (n *Not) Hash() int {
+ return 1 + n.Body.Hash()
+}
+
+func (n *Not) IsGround() bool {
+ return n.Body.IsGround()
+}
+
+func (n *Not) String() string {
+ if !n.ExplicitBody && len(n.Body) == 1 {
+ return "not " + n.Body.String()
+ }
+
+ return "not {" + n.Body.String() + "}"
+}
+
+func (n *Not) MarshalJSON() ([]byte, error) {
+ data := map[string]any{
+ "type": "not",
+ "body": n.Body,
+ "explicit_body": n.ExplicitBody,
+ }
+
+ if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not {
+ if n.Location != nil {
+ data["location"] = n.Location
+ }
+ }
+
+ return json.Marshal(data)
+}
+
+func (n *Not) UnmarshalJSON(bs []byte) error {
+ v := map[string]any{}
+ if err := util.UnmarshalJSON(bs, &v); err != nil {
+ return err
+ }
+
+ return unmarshalNot(n, v)
+}
+
+func unmarshalNot(n *Not, v map[string]any) error {
+ var eb bool
+ if x, ok := v["explicit_body"]; ok {
+ eb, ok = x.(bool)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"])
+ }
+ }
+
+ b, ok := v["body"].([]any)
+ if !ok {
+ return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"])
+ }
+
+ body, err := unmarshalBody(b)
+ if err != nil {
+ return fmt.Errorf("ast: unable to unmarshal not body: %w", err)
+ }
+
+ n.ExplicitBody = eb
+ n.Body = body
+
+ return nil
+}
+
// Null represents the null value defined by JSON.
type Null struct{}
@@ -1173,10 +1321,10 @@ func (ref Ref) Copy() Ref {
return termSliceCopy(ref)
}
-// CopyNonGround returns a new ref with deep copies of the non-ground parts and shallow
-// copies of the ground parts. This is a *much* cheaper operation than Copy for operations
-// that only intend to modify (e.g. plug) the non-ground parts. The head element of the ref
-// is always shallow copied.
+// CopyNonGround returns a new ref with shallow copies of ground parts and deep
+// copies of non-ground parts. The head element is always shallow copied. This is
+// cheaper than Copy for operations that only modify non-ground parts (e.g. plugging)
+// or metadata (e.g. Location).
func (ref Ref) CopyNonGround() Ref {
cpy := make(Ref, len(ref))
cpy[0] = ref[0]
@@ -1729,17 +1877,26 @@ type set struct {
// Copy returns a deep copy of s.
func (s *set) Copy() Set {
+ n := len(s.keys)
cpy := &set{
hash: s.hash,
ground: s.ground,
sortGuard: sync.Once{},
- elems: make(map[int]*Term, len(s.elems)),
- keys: make([]*Term, 0, len(s.keys)),
+ elems: make(map[int]*Term, n),
+ keys: make([]*Term, n),
}
- for hash := range s.elems {
- cpy.elems[hash] = s.elems[hash].Copy()
- cpy.keys = append(cpy.keys, cpy.elems[hash])
+ if n > 0 {
+ // Batch-allocate all Term structs in a single contiguous block.
+ buf := make([]Term, n)
+ i := 0
+ for hash, elem := range s.elems {
+ buf[i] = *elem
+ deepCopyTermValue(&buf[i])
+ cpy.elems[hash] = &buf[i]
+ cpy.keys[i] = &buf[i]
+ i++
+ }
}
return cpy
@@ -2364,10 +2521,41 @@ func (obj *object) IsGround() bool {
// Copy returns a deep copy of obj.
func (obj *object) Copy() Object {
- cpy, _ := obj.Map(func(k, v *Term) (*Term, *Term, error) {
- return k.Copy(), v.Copy(), nil
- })
- cpy.(*object).hash = obj.hash
+ n := len(obj.keys)
+ cpy := &object{
+ elems: make(map[int]*objectElem, n),
+ sortGuard: sync.Once{},
+ hash: obj.hash,
+ ground: obj.ground,
+ }
+
+ if n == 0 {
+ return cpy
+ }
+
+ // Batch-allocate all objectElems, keys, and values in contiguous blocks
+ // (3 allocations instead of 3N).
+ elems := make([]objectElem, n)
+ keys := make([]Term, n)
+ vals := make([]Term, n)
+ cpy.keys = make([]*objectElem, n)
+
+ for i, srcElem := range obj.keys {
+ keys[i] = *srcElem.key
+ deepCopyTermValue(&keys[i])
+ vals[i] = *srcElem.value
+ deepCopyTermValue(&vals[i])
+
+ elems[i] = objectElem{key: &keys[i], value: &vals[i]}
+ cpy.keys[i] = &elems[i]
+
+ hash := keys[i].Hash()
+ if head, ok := cpy.elems[hash]; ok {
+ elems[i].next = head
+ }
+ cpy.elems[hash] = &elems[i]
+ }
+
return cpy
}
@@ -2931,10 +3119,45 @@ func (c Call) String() string {
return util.ByteSliceToString(buf)
}
+// deepCopyTermValue deep copies the Value of term in-place.
+// Scalar values (Null, Boolean, Number, String, Var) are already
+// copied by struct assignment, so only container types need work.
+func deepCopyTermValue(term *Term) {
+ switch v := term.Value.(type) {
+ case Null, Boolean, Number, String, Var:
+ // Already copied by *term = *src struct assignment.
+ case Ref:
+ term.Value = v.Copy()
+ case *Array:
+ term.Value = v.Copy()
+ case Set:
+ term.Value = v.Copy()
+ case *object:
+ term.Value = v.Copy()
+ case *ArrayComprehension:
+ term.Value = v.Copy()
+ case *ObjectComprehension:
+ term.Value = v.Copy()
+ case *SetComprehension:
+ term.Value = v.Copy()
+ case *TemplateString:
+ term.Value = v.Copy()
+ case Call:
+ term.Value = v.Copy()
+ }
+}
+
func termSliceCopy(a []*Term) []*Term {
- cpy := make([]*Term, len(a))
- for i := range a {
- cpy[i] = a[i].Copy()
+ n := len(a)
+ if n == 0 {
+ return make([]*Term, 0)
+ }
+ // Batch allocate all Term structs in a single contiguous slice (2 allocs
+ // instead of N+1) using the same pattern as util.NewPtrSlice.
+ cpy := util.NewPtrSlice[Term](n)
+ for i := range n {
+ *cpy[i] = *a[i] // copy Term struct (Value + Location)
+ deepCopyTermValue(cpy[i]) // deep copy container Values in-place
}
return cpy
}
@@ -3019,11 +3242,32 @@ func unmarshalExpr(expr *Expr, v map[string]any) error {
}
switch ts := v["terms"].(type) {
case map[string]any:
- t, err := unmarshalTerm(ts)
- if err != nil {
- return err
+ switch tt, _ := ts["type"].(string); tt {
+ case "not":
+ n := &Not{}
+ if err := unmarshalNot(n, ts); err != nil {
+ return err
+ }
+ expr.Terms = n
+ case "and":
+ a := &LogicalAnd{}
+ if err := unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, ts); err != nil {
+ return err
+ }
+ expr.Terms = a
+ case "or":
+ o := &LogicalOr{}
+ if err := unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, ts); err != nil {
+ return err
+ }
+ expr.Terms = o
+ default:
+ t, err := unmarshalTerm(ts)
+ if err != nil {
+ return err
+ }
+ expr.Terms = t
}
- expr.Terms = t
case []any:
terms, err := unmarshalTermSlice(ts)
if err != nil {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go b/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go
index 93c0803910..63c3973b3f 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/term_appenders.go
@@ -2,6 +2,7 @@ package ast
import (
"encoding"
+ "fmt"
"strconv"
"strings"
@@ -228,7 +229,7 @@ func (r Ref) AppendText(buf []byte) ([]byte, error) {
func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
- if buf, err = sc.Term.AppendText(buf); err != nil {
+ if buf, err = appendComprehensionTerm(buf, sc.Term); err != nil {
return nil, err
}
if buf, err = sc.Body.AppendText(append(buf, " | "...)); err != nil {
@@ -240,7 +241,7 @@ func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) {
func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '[')
var err error
- if buf, err = ac.Term.AppendText(buf); err != nil {
+ if buf, err = appendComprehensionTerm(buf, ac.Term); err != nil {
return nil, err
}
if buf, err = ac.Body.AppendText(append(buf, " | "...)); err != nil {
@@ -252,11 +253,11 @@ func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) {
func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
- if buf, err = oc.Key.AppendText(buf); err != nil {
+ if buf, err = appendComprehensionTerm(buf, oc.Key); err != nil {
return nil, err
}
buf = append(buf, ": "...)
- if buf, err = oc.Value.AppendText(buf); err != nil {
+ if buf, err = appendComprehensionTerm(buf, oc.Value); err != nil {
return nil, err
}
if buf, err = oc.Body.AppendText(append(buf, " | "...)); err != nil {
@@ -264,3 +265,38 @@ func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) {
}
return append(buf, '}'), nil
}
+
+// appendComprehensionTerm writes a term, rendering infix operator calls
+// in infix notation wrapped in parens. This prevents ambiguity with the
+// comprehension "|" separator and produces more readable output.
+func appendComprehensionTerm(buf []byte, term *Term) ([]byte, error) {
+ if call, ok := term.Value.(Call); ok && len(call) == 3 {
+ if bi, found := BuiltinMap[call[0].String()]; found && bi.Infix != "" && bi.Infix != "in" {
+ buf = append(buf, '(')
+ var err error
+ if buf, err = appendComprehensionTerm(buf, call[1]); err != nil {
+ return nil, err
+ }
+ buf = fmt.Appendf(buf, " %s ", bi.Infix)
+ if buf, err = appendComprehensionTerm(buf, call[2]); err != nil {
+ return nil, err
+ }
+ return append(buf, ')'), nil
+ }
+ }
+ return term.AppendText(buf)
+}
+
+func (not *Not) AppendText(buf []byte) ([]byte, error) {
+ if !not.ExplicitBody && len(not.Body) == 1 {
+ buf = append(buf, "not "...)
+ return not.Body.AppendText(buf)
+ }
+
+ buf = append(buf, "not {"...)
+ var err error
+ if buf, err = not.Body.AppendText(buf); err != nil {
+ return nil, err
+ }
+ return append(buf, '}'), nil
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go b/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go
index a71bc0a77c..7c0c54e3d7 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/transform.go
@@ -194,6 +194,29 @@ func Transform(t Transformer, x any) (any, error) {
return nil, err
}
y.Terms = ts
+ case *Not:
+ ts.Body, err = transformBody(t, ts.Body)
+ if err != nil {
+ return nil, err
+ }
+ case *LogicalAnd:
+ ts.Lhs, err = transformBody(t, ts.Lhs)
+ if err != nil {
+ return nil, err
+ }
+ ts.Rhs, err = transformBody(t, ts.Rhs)
+ if err != nil {
+ return nil, err
+ }
+ case *LogicalOr:
+ ts.Lhs, err = transformBody(t, ts.Lhs)
+ if err != nil {
+ return nil, err
+ }
+ ts.Rhs, err = transformBody(t, ts.Rhs)
+ if err != nil {
+ return nil, err
+ }
}
for i, w := range y.With {
w, err := Transform(t, w)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go b/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go
new file mode 100644
index 0000000000..f22367bc81
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/treenode_dump.go
@@ -0,0 +1,56 @@
+package ast
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+)
+
+// Dump returns a string representation of the tree structure rooted at this node.
+func (n *TreeNode) Dump() string {
+ var sb strings.Builder
+ n.dumpRecursive(&sb, "", "")
+ return sb.String()
+}
+
+func (n *TreeNode) dumpRecursive(sb *strings.Builder, prefix, childPrefix string) {
+ sb.WriteString(prefix)
+ fmt.Fprintf(sb, "%v", n.Key)
+
+ if n.Hide {
+ sb.WriteString(" [hidden]")
+ }
+ if n.External != nil {
+ fmt.Fprintf(sb, " ext:%v", n.External.Ref)
+ }
+ if len(n.Values) > 0 {
+ fmt.Fprintf(sb, " rules:%d", len(n.Values))
+ }
+ sb.WriteString("\n")
+
+ if len(n.Children) == 0 {
+ return
+ }
+
+ keys := make([]Value, 0, len(n.Children))
+ for k := range n.Children {
+ keys = append(keys, k)
+ }
+ sort.Slice(keys, func(i, j int) bool {
+ return Compare(keys[i], keys[j]) < 0
+ })
+
+ for i, key := range keys {
+ child := n.Children[key]
+ isLast := i == len(keys)-1
+ var newPrefix, newChildPrefix string
+ if isLast {
+ newPrefix = childPrefix + "└── "
+ newChildPrefix = childPrefix + " "
+ } else {
+ newPrefix = childPrefix + "├── "
+ newChildPrefix = childPrefix + "│ "
+ }
+ child.dumpRecursive(sb, newPrefix, newChildPrefix)
+ }
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json b/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json
index bd0df82ad6..4e5442604c 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/version_index.json
@@ -970,6 +970,16 @@
"Minor": 17,
"Patch": 0
},
+ "uri.is_valid": {
+ "Major": 1,
+ "Minor": 16,
+ "Patch": 0
+ },
+ "uri.parse": {
+ "Major": 1,
+ "Minor": 16,
+ "Patch": 0
+ },
"urlquery.decode": {
"Major": 0,
"Minor": 17,
@@ -1074,6 +1084,11 @@
"Major": 0,
"Minor": 34,
"Patch": 0
+ },
+ "not": {
+ "Major": 1,
+ "Minor": 17,
+ "Patch": 0
}
}
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go b/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go
index d7725f5a51..c054b92b29 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ast/visit.go
@@ -48,6 +48,7 @@ type (
SkipWithTarget bool
SkipSets bool
SkipTemplateStrings bool
+ customVisit func(vis *VarVisitor, v any) bool
}
// Visitor defines the interface for iterating AST elements. The Visit function
@@ -161,7 +162,7 @@ func walk(v Visitor, x any) {
}
case *Expr:
switch ts := x.Terms.(type) {
- case *Term, *SomeDecl, *Every:
+ case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr:
Walk(w, ts)
case []*Term:
for i := range ts {
@@ -214,6 +215,14 @@ func walk(v Visitor, x any) {
Walk(w, x.Value)
Walk(w, x.Domain)
Walk(w, x.Body)
+ case *Not:
+ Walk(w, x.Body)
+ case *LogicalAnd:
+ Walk(w, x.Lhs)
+ Walk(w, x.Rhs)
+ case *LogicalOr:
+ Walk(w, x.Lhs)
+ Walk(w, x.Rhs)
case *SomeDecl:
for i := range x.Symbols {
Walk(w, x.Symbols[i])
@@ -236,7 +245,7 @@ func WalkVars(x any, f func(Var) bool) {
func WalkClosures(x any, f func(any) bool) {
vis := NewGenericVisitor(func(x any) bool {
switch x := x.(type) {
- case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *Every:
+ case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *Every, *Not, *LogicalAnd, *LogicalOr:
return f(x)
}
return false
@@ -396,7 +405,7 @@ func (tv *typeVisitor[T]) walk(x any, visit func(x T) bool) {
}
case *Expr:
switch ts := x.Terms.(type) {
- case *Term, *SomeDecl, *Every:
+ case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr:
tv.walk(ts, visit)
case []*Term:
for i := range ts {
@@ -463,6 +472,14 @@ func (tv *typeVisitor[T]) walk(x any, visit func(x T) bool) {
for i := range x.Parts {
tv.walk(x.Parts[i], visit)
}
+ case *Not:
+ tv.walk(x.Body, visit)
+ case *LogicalAnd:
+ tv.walkBody(x.Lhs, visit)
+ tv.walkBody(x.Rhs, visit)
+ case *LogicalOr:
+ tv.walkBody(x.Lhs, visit)
+ tv.walkBody(x.Rhs, visit)
}
}
@@ -533,7 +550,7 @@ func (vis *GenericVisitor) Walk(x any) {
}
case *Expr:
switch ts := x.Terms.(type) {
- case *Term, *SomeDecl, *Every:
+ case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr:
vis.Walk(ts)
case []*Term:
for i := range ts {
@@ -600,6 +617,14 @@ func (vis *GenericVisitor) Walk(x any) {
for i := range x.Parts {
vis.Walk(x.Parts[i])
}
+ case *Not:
+ vis.Walk(x.Body)
+ case *LogicalAnd:
+ vis.Walk(x.Lhs)
+ vis.Walk(x.Rhs)
+ case *LogicalOr:
+ vis.Walk(x.Lhs)
+ vis.Walk(x.Rhs)
}
}
@@ -668,7 +693,7 @@ func (vis *BeforeAfterVisitor) Walk(x any) {
}
case *Expr:
switch ts := x.Terms.(type) {
- case *Term, *SomeDecl, *Every:
+ case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr:
vis.Walk(ts)
case []*Term:
for i := range ts {
@@ -731,6 +756,14 @@ func (vis *BeforeAfterVisitor) Walk(x any) {
for i := range x.Symbols {
vis.Walk(x.Symbols[i])
}
+ case *Not:
+ vis.Walk(x.Body)
+ case *LogicalAnd:
+ vis.Walk(x.Lhs)
+ vis.Walk(x.Rhs)
+ case *LogicalOr:
+ vis.Walk(x.Lhs)
+ vis.Walk(x.Rhs)
}
}
@@ -807,7 +840,7 @@ func (vis *VarVisitor) visit(v any) bool {
}
if vis.params.SkipClosures {
switch v := v.(type) {
- case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *TemplateString:
+ case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *TemplateString, *Not, *LogicalAnd, *LogicalOr:
return true
case *Expr:
if ev, ok := v.Terms.(*Every); ok {
@@ -886,6 +919,12 @@ func (vis *VarVisitor) visit(v any) bool {
// Walk iterates the AST by calling the function f on the [VarVisitor] before recursing.
// Contrary to the deprecated [Walk] function, this does not require allocating the visitor from heap.
func (vis *VarVisitor) Walk(x any) {
+ if vis.params.customVisit != nil {
+ if vis.params.customVisit(vis, x) {
+ return
+ }
+ }
+
if vis.visit(x) {
return
}
@@ -927,7 +966,7 @@ func (vis *VarVisitor) Walk(x any) {
vis.WalkArgs(x)
case *Expr:
switch ts := x.Terms.(type) {
- case *Term, *SomeDecl, *Every:
+ case *Term, *SomeDecl, *Every, *Not, *LogicalAnd, *LogicalOr:
vis.Walk(ts)
case []*Term:
for i := range ts {
@@ -992,6 +1031,14 @@ func (vis *VarVisitor) Walk(x any) {
for i := range x.Parts {
vis.Walk(x.Parts[i])
}
+ case *Not:
+ vis.Walk(x.Body)
+ case *LogicalAnd:
+ vis.WalkBody(x.Lhs)
+ vis.WalkBody(x.Rhs)
+ case *LogicalOr:
+ vis.WalkBody(x.Lhs)
+ vis.WalkBody(x.Rhs)
}
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go b/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go
index bf00e96ca2..80bad2090b 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/bundle.go
@@ -1115,6 +1115,10 @@ func (b *Bundle) FormatModulesWithOptions(opts BundleFormatOptions) error {
Capabilities: opts.Capabilities,
}
+ if fmtOpts.Capabilities == nil {
+ fmtOpts.Capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(fmtOpts.RegoVersion))
+ }
+
if module.Parsed != nil {
fmtOpts.ParserOptions = &ast.ParserOptions{
RegoVersion: module.Parsed.RegoVersion(),
@@ -1122,10 +1126,9 @@ func (b *Bundle) FormatModulesWithOptions(opts BundleFormatOptions) error {
if opts.PreserveModuleRegoVersion {
fmtOpts.RegoVersion = module.Parsed.RegoVersion()
}
- }
-
- if fmtOpts.Capabilities == nil {
- fmtOpts.Capabilities = ast.CapabilitiesForThisVersion(ast.CapabilitiesRegoVersion(fmtOpts.RegoVersion))
+ if fmtOpts.ParserOptions.RegoVersion == fmtOpts.RegoVersion {
+ fmtOpts.ParserOptions.Capabilities = fmtOpts.Capabilities
+ }
}
if module.Raw == nil {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json b/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json
new file mode 100644
index 0000000000..ff060aeea4
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/manifest.schema.json
@@ -0,0 +1,63 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openpolicyagent.org/schemas/bundle/v1/manifest.schema.json",
+ "title": "OPA Bundle Manifest",
+ "description": "JSON Schema for the bundle `.manifest` file produced by `opa build`. Generated from v1/bundle/bundle.go.",
+ "$ref": "#/$defs/Manifest",
+ "$defs": {
+ "Manifest": {
+ "type": "object",
+ "properties": {
+ "file_rego_versions": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer"
+ }
+ },
+ "metadata": {
+ "type": "object"
+ },
+ "rego_version": {
+ "type": "integer"
+ },
+ "revision": {
+ "type": "string"
+ },
+ "roots": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "wasm": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/WasmResolver"
+ }
+ }
+ },
+ "required": [
+ "revision"
+ ]
+ },
+ "WasmResolver": {
+ "type": "object",
+ "properties": {
+ "annotations": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "description": "Rego annotations; opaque in this schema. See the OPA documentation for the full annotation shape."
+ }
+ },
+ "entrypoint": {
+ "type": "string"
+ },
+ "module": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go b/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go
index f6c13c75fc..29efe208d2 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/bundle/store.go
@@ -12,7 +12,6 @@ import (
"fmt"
"maps"
"path/filepath"
- "slices"
"sort"
"strings"
"sync"
@@ -359,8 +358,9 @@ type ActivateOpts struct {
TxnCtx *storage.Context
Compiler *ast.Compiler
Metrics metrics.Metrics
- Bundles map[string]*Bundle // Optional
- ExtraModules map[string]*ast.Module // Optional
+ Bundles map[string]*Bundle // Optional
+ ExtraModules map[string]*ast.Module // Optional
+ ExternalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource] // Optional
AuthorizationDecisionRef ast.Ref
ParserOptions ast.ParserOptions
Plugin string
@@ -424,7 +424,6 @@ func Deactivate(opts *DeactivateOpts) error {
}
func activateBundles(opts *ActivateOpts) error {
-
// Build collections of bundle names, modules, and roots to erase
erase := map[string]struct{}{}
names := map[string]struct{}{}
@@ -476,9 +475,7 @@ func activateBundles(opts *ActivateOpts) error {
// Validate data in bundle does not contain paths outside the bundle's roots.
for _, b := range snapshotBundles {
-
if b.lazyLoadingMode {
-
for _, item := range b.Raw {
path := filepath.ToSlash(item.Path)
@@ -535,7 +532,7 @@ func activateBundles(opts *ActivateOpts) error {
maps.Copy(remainingAndExtra, remaining)
maps.Copy(remainingAndExtra, opts.ExtraModules)
- err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef)
+ err = compileModules(opts.Compiler, opts.Metrics, snapshotBundles, remainingAndExtra, opts.legacy, opts.AuthorizationDecisionRef, opts.ExternalSources)
if err != nil {
return err
}
@@ -612,7 +609,6 @@ func doDFS(obj map[string]json.RawMessage, path string, roots []string) error {
}
func activateDeltaBundles(opts *ActivateOpts, bundles map[string]*Bundle) error {
-
// Check that the manifest roots and wasm resolvers in the delta bundle
// match with those currently in the store
for name, b := range bundles {
@@ -685,7 +681,6 @@ func valueToManifest(v any) (Manifest, error) {
// erase bundles by name and roots. This will clear all policies and data at its roots and remove its
// manifest from storage.
func eraseBundles(ctx context.Context, store storage.Store, txn storage.Transaction, parserOpts ast.ParserOptions, names map[string]struct{}, roots map[string]struct{}) (map[string]*ast.Module, error) {
-
if err := eraseData(ctx, store, txn, roots); err != nil {
return nil, err
}
@@ -774,7 +769,6 @@ func readModuleInfoFromStore(ctx context.Context, store storage.Store, txn stora
}
func erasePolicies(ctx context.Context, store storage.Store, txn storage.Transaction, parserOpts ast.ParserOptions, roots map[string]struct{}) (map[string]*ast.Module, []string, error) {
-
ids, err := store.ListPolicies(ctx, txn)
if err != nil {
return nil, nil, err
@@ -861,8 +855,8 @@ func writeEtagToStore(opts *ActivateOpts, name, etag string) error {
}
func writeModuleRegoVersionToStore(ctx context.Context, store storage.Store, txn storage.Transaction, b *Bundle,
- mf ModuleFile, storagePath string, runtimeRegoVersion ast.RegoVersion) error {
-
+ mf ModuleFile, storagePath string, runtimeRegoVersion ast.RegoVersion,
+) error {
var regoVersion ast.RegoVersion
if mf.Parsed != nil {
regoVersion = mf.Parsed.RegoVersion()
@@ -965,11 +959,18 @@ func writeData(ctx context.Context, store storage.Store, txn storage.Transaction
return nil
}
-func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref) error {
-
+func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, authorizationDecisionRef ast.Ref, externalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource]) error {
m.Timer(metrics.RegoModuleCompile).Start()
defer m.Timer(metrics.RegoModuleCompile).Stop()
+ // Apply external sources before compilation
+ if externalSources != nil {
+ externalSources.Iter(func(ref ast.Ref, source ast.ExternalRuleSource) bool {
+ compiler = compiler.WithExternalSource(ref, source)
+ return false
+ })
+ }
+
modules := make(map[string]*ast.Module, len(compiler.Modules)+len(extraModules)+len(bundles))
// preserve any modules already on the compiler
@@ -1000,11 +1001,18 @@ func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[strin
return iCompiler.VerifyAuthorizationPolicySchema(compiler, authorizationDecisionRef)
}
-func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool) error {
-
+func writeModules(ctx context.Context, store storage.Store, txn storage.Transaction, compiler *ast.Compiler, m metrics.Metrics, bundles map[string]*Bundle, extraModules map[string]*ast.Module, legacy bool, externalSources *util.HasherMap[ast.Ref, ast.ExternalRuleSource]) error {
m.Timer(metrics.RegoModuleCompile).Start()
defer m.Timer(metrics.RegoModuleCompile).Stop()
+ // Apply external sources before compilation
+ if externalSources != nil {
+ externalSources.Iter(func(ref ast.Ref, source ast.ExternalRuleSource) bool {
+ compiler = compiler.WithExternalSource(ref, source)
+ return false
+ })
+ }
+
modules := map[string]*ast.Module{}
// preserve any modules already on the compiler
@@ -1066,67 +1074,165 @@ func lookup(path storage.Path, data map[string]any) (any, bool) {
return value, ok
}
+// rootEntry is a (bundle, root) pair, used for the
+// sort-and-scan algorithm in hasRootsOverlap.
+type rootEntry struct {
+ bundle string
+ canonical string // cannonicalized bundle root path
+}
+
+// displayRoot is used for cleaner error messages.
+func (e rootEntry) displayRoot() string {
+ return strings.Trim(e.canonical, "/")
+}
+
+// canonicalRoot normalizes a raw bundle root so that string prefix
+// matching correctly handles path segments.
+// Example: "/a/b/" is a prefix of "/a/b/c/" but not of "/a/bc/".
+func canonicalRoot(raw string) string {
+ trimmed := strings.Trim(raw, "/")
+ if trimmed == "" {
+ return "/"
+ }
+ return "/" + trimmed + "/"
+}
+
+// appendRootEntries appends one rootEntry per root in the `roots` slice
+// to the `entries` slice, returning the updated entries list. Bundles
+// declaring an empty root are tracked in `bundlesWithEmptyRoots` for the
+// empty-root error-message special case.
+func appendRootEntries(entries []rootEntry, bundlesWithEmptyRoots map[string]bool, name string, roots []string) []rootEntry {
+ for _, raw := range roots {
+ if raw == "" {
+ bundlesWithEmptyRoots[name] = true
+ }
+ entries = append(entries, rootEntry{
+ bundle: name,
+ canonical: canonicalRoot(raw),
+ })
+ }
+ return entries
+}
+
+// hasRootsOverlap verifies that the roots declared by the bundles being
+// activated do not collide with each other or with any bundle already
+// present in the store.
+//
+// Intuition: We sort all paths lexicographically, then scan forward for
+// prefix matches. Prefix matches == collisions. This works because
+// shorter path prefixes will always be sorted ahead of any colliding
+// longer paths.
+//
+// We use one loop to scan for exact matches. This forms a group of known
+// conflicting paths. We then scan forward from that group until we hit
+// a path which does not have our root path as the prefix. We then generate
+// the conflict sets for all of the paths in the group against each other,
+// and then against any conflicting longer paths.
+//
+// All conflicts among the post-activation root set are reported.
+// Existing bundles in the store are assumed conflict-free with each
+// other — an invariant maintained because each was checked against the
+// store at its own activation time. Replacing a bundle takes its new
+// roots only; the store's stale view of that bundle is discarded
+// during setup so it can't produce phantom conflicts against the
+// replacement's own new roots.
func hasRootsOverlap(ctx context.Context, store storage.Store, txn storage.Transaction, newBundles map[string]*Bundle) error {
storeBundles, err := ReadBundleNamesFromStore(ctx, store, txn)
if suppressNotFound(err) != nil {
return err
}
- allRoots := map[string][]string{}
+ // Flatten every root (existing + new) into one entry list. A bundle
+ // being re-activated takes its roots from newBundles only; the
+ // store's stale view of that bundle is skipped.
bundlesWithEmptyRoots := map[string]bool{}
+ entries := make([]rootEntry, 0, len(storeBundles)+2*len(newBundles))
- // Build a map of roots for existing bundles already in the system
for _, name := range storeBundles {
+ if _, replaced := newBundles[name]; replaced {
+ continue
+ }
roots, err := ReadBundleRootsFromStore(ctx, store, txn, name)
if suppressNotFound(err) != nil {
return err
}
- allRoots[name] = roots
- if slices.Contains(roots, "") {
- bundlesWithEmptyRoots[name] = true
- }
+ entries = appendRootEntries(entries, bundlesWithEmptyRoots, name, roots)
}
-
- // Add in any bundles that are being activated, overwrite existing roots
- // with new ones where bundles are in both groups.
for name, bundle := range newBundles {
- allRoots[name] = *bundle.Manifest.Roots
- if slices.Contains(*bundle.Manifest.Roots, "") {
- bundlesWithEmptyRoots[name] = true
- }
+ entries = appendRootEntries(entries, bundlesWithEmptyRoots, name, *bundle.Manifest.Roots)
}
- // Now check for each new bundle if it conflicts with any of the others
+ // Sort the bundle roots list.
+ sort.Slice(entries, func(i, j int) bool {
+ if entries[i].canonical != entries[j].canonical {
+ return entries[i].canonical < entries[j].canonical
+ }
+ return entries[i].bundle < entries[j].bundle
+ })
+
collidingBundles := map[string]bool{}
conflictSet := map[string]bool{}
- for name, bundle := range newBundles {
- for otherBundle, otherRoots := range allRoots {
- if name == otherBundle {
- // Skip the current bundle being checked
- continue
+ groupBundles := map[string]bool{} // reused across iterations via clear()
+
+ // Scan through the sorted list of bundle root paths iteratively,
+ // identifying conflict groups for later reporting at the end.
+ // If there's a cluster of exact path matches, we can advance the
+ // groupStart index to the end of that group after we've accounted
+ // for the conflicts.
+ for groupStart := 0; groupStart < len(entries); {
+ // Identify the group of entries sharing the
+ // exact same canonical root.
+ groupEnd := groupStart + 1
+ for groupEnd < len(entries) && entries[groupEnd].canonical == entries[groupStart].canonical {
+ groupEnd++
+ }
+
+ clear(groupBundles) // Reuse the conflict group map.
+ for k := groupStart; k < groupEnd; k++ {
+ groupBundles[entries[k].bundle] = true
+ }
+
+ // Same-root conflict: more than one bundle shares this root.
+ if len(groupBundles) > 1 {
+ for k := groupStart; k < groupEnd; k++ {
+ collidingBundles[entries[k].bundle] = true
+ }
+ conflictSet[fmt.Sprintf("root %s is in multiple bundles", entries[groupStart].displayRoot())] = true
+ }
+
+ groupCanonical := entries[groupStart].canonical
+ groupDisplay := entries[groupStart].displayRoot()
+
+ // Descendant conflicts: forward scan while the group's canonical
+ // root is a prefix of the next entry's canonical root.
+ for d := groupEnd; d < len(entries); d++ {
+ // Break if next entry isn't a conflict.
+ if !strings.HasPrefix(entries[d].canonical, groupCanonical) {
+ break
}
- // Compare the "new" roots with other existing (or a different bundles new roots)
- for _, newRoot := range *bundle.Manifest.Roots {
- for _, otherRoot := range otherRoots {
- if !RootPathsOverlap(newRoot, otherRoot) {
- continue
- }
-
- collidingBundles[name] = true
- collidingBundles[otherBundle] = true
-
- // Different message required if the roots are same
- if newRoot == otherRoot {
- conflictSet[fmt.Sprintf("root %s is in multiple bundles", newRoot)] = true
- } else {
- paths := []string{newRoot, otherRoot}
- sort.Strings(paths)
- conflictSet[fmt.Sprintf("%s overlaps %s", paths[0], paths[1])] = true
- }
+ // Pair the descendant against every group entry from a different bundle.
+ sawCrossBundleConflict := false
+ for k := groupStart; k < groupEnd; k++ {
+ if entries[k].bundle == entries[d].bundle {
+ continue
}
+ collidingBundles[entries[k].bundle] = true
+ sawCrossBundleConflict = true
+ }
+
+ // Only record a conflict if the descendant overlaps with an
+ // ancestor declared by a different bundle. A single bundle
+ // is allowed to declare overlapping roots in its own manifest.
+ if sawCrossBundleConflict {
+ collidingBundles[entries[d].bundle] = true
+ paths := []string{groupDisplay, entries[d].displayRoot()}
+ sort.Strings(paths)
+ conflictSet[fmt.Sprintf("%s overlaps %s", paths[0], paths[1])] = true
}
}
+
+ groupStart = groupEnd
}
if len(collidingBundles) == 0 {
@@ -1192,8 +1298,10 @@ func applyPatches(ctx context.Context, store storage.Store, txn storage.Transact
// LegacyManifestStoragePath is the older unnamed bundle path for manifests to be stored.
//
// Deprecated: Use ManifestStoragePath and named bundles instead.
-var legacyManifestStoragePath = storage.MustParsePath("/system/bundle/manifest")
-var legacyRevisionStoragePath = append(legacyManifestStoragePath, "revision")
+var (
+ legacyManifestStoragePath = storage.MustParsePath("/system/bundle/manifest")
+ legacyRevisionStoragePath = append(legacyManifestStoragePath, "revision")
+)
// LegacyWriteManifestToStore will write the bundle manifest to the older single (unnamed) bundle manifest location.
//
diff --git a/vendor/github.com/open-policy-agent/opa/v1/format/format.go b/vendor/github.com/open-policy-agent/opa/v1/format/format.go
index c14a6ce798..351ae0a600 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/format/format.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/format/format.go
@@ -226,6 +226,8 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) {
extraFutureKeywordImports["in"] = struct{}{}
case n.IsEvery():
extraFutureKeywordImports["every"] = struct{}{}
+ case n.IsNot():
+ extraFutureKeywordImports["not"] = struct{}{}
}
case *ast.Import:
@@ -278,6 +280,7 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) {
}
regoV1Imported := slices.ContainsFunc(x.Imports, isRegoV1Compatible)
+
if regoVersion == ast.RegoV0CompatV1 || regoVersion == ast.RegoV1 || regoV1Imported {
if !opts.DropV0Imports && !regoV1Imported {
for _, kw := range o.futureKeywords {
@@ -286,11 +289,18 @@ func AstWithOpts(x any, opts Opts) ([]byte, error) {
} else {
x.Imports = future.FilterFutureImports(x.Imports)
}
+
+ for kw := range extraFutureKeywordImports {
+ if ast.IsFutureKeywordForRegoVersion(kw, ast.RegoV1) {
+ x.Imports = ensureFutureKeywordImport(x.Imports, kw)
+ }
+ }
} else {
for kw := range extraFutureKeywordImports {
x.Imports = ensureFutureKeywordImport(x.Imports, kw)
}
}
+
err := w.writeModule(x)
if err != nil {
w.errs = append(w.errs, ast.NewError(ast.FormatErr, &ast.Location{}, "%s", err.Error()))
@@ -519,6 +529,7 @@ func (w *writer) writePackage(pkg *ast.Package, comments []*ast.Comment) ([]*ast
}
func (w *writer) writeComments(comments []*ast.Comment) error {
+ var inMetadataBlock bool
for i := range comments {
if i > 0 {
l, err := locCmp(comments[i], comments[i-1])
@@ -527,9 +538,20 @@ func (w *writer) writeComments(comments []*ast.Comment) error {
}
if l > 1 {
w.blankLine()
+ inMetadataBlock = false
+ } else if l == 1 {
+ // if next comment is a metadata header and previous comment
+ // was part of a metadata block, add a blank line to separate them
+ if inMetadataBlock && ast.IsMetadataComment(comments[i]) {
+ w.blankLine()
+ }
}
}
+ if ast.IsMetadataComment(comments[i]) {
+ inMetadataBlock = true
+ }
+
w.writeLine(comments[i].String())
}
@@ -915,7 +937,12 @@ func (w *writer) writeExpr(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comm
return nil, err
}
case *ast.Every:
- comments, err = w.writeEvery(t, comments)
+ comments, err = w.writeEvery(t, expr.Loc(), comments)
+ if err != nil {
+ return nil, err
+ }
+ case *ast.Not:
+ comments, err = w.writeNot(t, expr.Loc(), comments)
if err != nil {
return nil, err
}
@@ -931,32 +958,39 @@ func (w *writer) writeExpr(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comm
}
}
- var indented, down bool
- for i, with := range expr.With {
- if i == 0 || with.Location.Row == expr.With[i-1].Location.Row { // we're on the same line
- comments, err = w.writeWith(with, comments, false)
- if err != nil {
- return nil, err
- }
- } else { // we're on a new line
- if !indented {
- indented = true
-
- w.up()
- down = true
- }
- w.endLine()
- w.startLine()
- comments, err = w.writeWith(with, comments, true)
- if err != nil {
- return nil, err
- }
- }
+ if len(expr.With) == 0 {
+ return comments, nil
}
- if down {
- if err := w.down(); err != nil {
- return nil, err
+ withs := expr.With
+ lastRow := expr.Location.Row
+
+ // Print on same row if already there, otherwise increase indent a print remaining
+ if withs[0].Location.Row == lastRow {
+ if comments, err = w.writeWith(withs[0], comments, false); err != nil {
+ return comments, err
+ }
+ lastRow, withs = withs[0].Location.Row, withs[1:]
+ }
+
+ if len(withs) > 0 {
+ var indented bool
+
+ for _, with := range withs {
+ indent := with.Location.Row > lastRow
+ if indent {
+ if !indented {
+ w.up()
+ defer w.down() //nolint:errcheck
+ indented = true
+ }
+ w.endLine()
+ w.startLine()
+ lastRow = with.Location.Row
+ }
+ if comments, err = w.writeWith(with, comments, indent); err != nil {
+ return comments, err
+ }
}
}
@@ -1004,9 +1038,13 @@ func (w *writer) writeSomeDecl(decl *ast.SomeDecl, comments []*ast.Comment) ([]*
return comments, nil
}
-func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.Comment, error) {
+func (w *writer) writeEvery(every *ast.Every, loc *ast.Location, comments []*ast.Comment) ([]*ast.Comment, error) {
+ if loc == nil {
+ loc = every.Loc()
+ }
+
var err error
- comments, err = w.insertComments(comments, every.Location)
+ comments, err = w.insertComments(comments, loc)
if err != nil {
return nil, err
}
@@ -1028,7 +1066,7 @@ func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.C
return nil, err
}
w.write(" {")
- comments, err = w.writeComprehensionBody('{', '}', every.Body, every.Loc(), every.Loc(), comments)
+ comments, err = w.writeComprehensionBody('{', '}', every.Body, loc, loc, comments)
if err != nil {
// the unexpected comment error is passed up to be handled by writeHead
if !errors.As(err, &unexpectedCommentError{}) {
@@ -1044,6 +1082,45 @@ func (w *writer) writeEvery(every *ast.Every, comments []*ast.Comment) ([]*ast.C
return comments, nil
}
+func (w *writer) writeNot(not *ast.Not, loc *ast.Location, comments []*ast.Comment) ([]*ast.Comment, error) {
+ if loc == nil {
+ loc = not.Loc()
+ }
+
+ var err error
+ comments, err = w.insertComments(comments, loc)
+ if err != nil {
+ return nil, err
+ }
+
+ w.write("not ")
+
+ if not.ExplicitBody || len(not.Body) > 1 {
+ w.write("{")
+ comments, err = w.writeComprehensionBody('{', '}', not.Body, loc, loc, comments)
+ if err != nil {
+ if !errors.As(err, &unexpectedCommentError{}) {
+ return nil, err
+ }
+ }
+
+ if len(not.Body) == 1 &&
+ not.Body[0].Location.Row == loc.Row {
+ w.write(" ")
+ }
+ w.write("}")
+ } else {
+ comments, err = w.writeExpr(not.Body[0], comments)
+ if err != nil {
+ if !errors.As(err, &unexpectedCommentError{}) {
+ return nil, err
+ }
+ }
+ }
+
+ return comments, nil
+}
+
func (w *writer) writeFunctionCall(expr *ast.Expr, comments []*ast.Comment) ([]*ast.Comment, error) {
terms := expr.Terms.([]*ast.Term)
@@ -1135,7 +1212,7 @@ func (w *writer) writeWith(with *ast.With, comments []*ast.Comment, indented boo
w.write(" as ")
comments, err = w.writeTerm(with.Value, comments)
if err != nil {
- return nil, err
+ return comments, err
}
return comments, nil
}
@@ -1158,6 +1235,7 @@ func (w *writer) writeTerm(term *ast.Term, comments []*ast.Comment) ([]*ast.Comm
}
currentLen := w.buf.Len()
+ currentLevel := w.level
currentComments := saveComments(comments)
defer commentsSlicePool.Put(currentComments)
@@ -1165,6 +1243,16 @@ func (w *writer) writeTerm(term *ast.Term, comments []*ast.Comment) ([]*ast.Comm
if err != nil {
if errors.As(err, &unexpectedCommentError{}) {
w.buf.Truncate(currentLen)
+ w.level = currentLevel
+
+ // If beforeEnd refers to a comment within the source text range, clear it
+ // This prevents the comment from being written twice
+ if w.beforeEnd != nil && len(term.Location.Text) > 0 {
+ endRow := term.Location.Row + bytes.Count(term.Location.Text, []byte{'\n'})
+ if w.beforeEnd.Location.Row >= term.Location.Row && w.beforeEnd.Location.Row <= endRow {
+ w.beforeEnd = nil
+ }
+ }
comments, uErr := w.writeUnformatted(term.Location, *currentComments)
if uErr != nil {
@@ -1750,6 +1838,9 @@ func (w *writer) writeImport(imp *ast.Import) error {
// We don't want to wrap future.keywords imports in parens, so we create a new writer that doesn't
w2 := writer{
buf: bytes.Buffer{},
+ fmtOpts: fmtOpts{
+ allowKeywordsInRefs: true,
+ },
}
_, err := w2.writeRef(path, nil)
if err != nil {
@@ -1777,6 +1868,20 @@ func (w *writer) writeIterable(elements []any, last *ast.Location, close *ast.Lo
if err != nil {
return nil, err
}
+
+ // If there are comments within the single line, don't collapse it and keep it as-is
+ // Return an error so that writeTerm will write the original formatting
+ if len(lines) == 1 {
+ for _, c := range comments {
+ if c.Location.Row > last.Row && c.Location.Row < close.Row {
+ return comments, unexpectedCommentError{
+ newComment: truncatedString(c.String(), 100),
+ newCommentRow: c.Location.Row,
+ }
+ }
+ }
+ }
+
if len(lines) > 1 {
w.delayBeforeEnd()
w.startMultilineSeq()
@@ -2295,17 +2400,35 @@ func ensureFutureKeywordImport(imps []*ast.Import, kw string) []*ast.Import {
}
}
imp := &ast.Import{
- // NOTE: This is a hack to not error on the ref containing a keyword already present in v1.
- // A cleaner solution would be to instead allow refs to contain keyword terms.
- // E.g. in v1, `import future.keywords["in"]` is valid, but `import future.keywords.in` is not
- // as it contains a reserved keyword.
- Path: ast.MustParseTerm("future.keywords[\"" + kw + "\"]"),
- //Path: ast.MustParseTerm("future.keywords." + kw),
+ Path: ast.MustParseTerm("future.keywords." + kw),
}
- imp.Location = defaultLocation(imp)
+ imp.Location = nextImportLoc(imps, imp)
return append(imps, imp)
}
+func nextImportLoc(imps []*ast.Import, node ast.Node) *ast.Location {
+ maxRow := 0
+ for _, imp := range imps {
+ if imp.Loc() == nil {
+ continue
+ }
+ if isFutureKeywordsImport(imp) || isRegoV1Compatible(imp) {
+ if imp.Loc().Row > maxRow {
+ maxRow = imp.Loc().Row
+ }
+ }
+ }
+ if maxRow == 0 {
+ return defaultLocation(node)
+ }
+ return ast.NewLocation([]byte(node.String()), defaultLocationFile, maxRow+1, 1)
+}
+
+func isFutureKeywordsImport(imp *ast.Import) bool {
+ path := imp.Path.Value.(ast.Ref)
+ return len(path) >= 2 && ast.FutureRootDocument.Equal(path[0])
+}
+
func ensureRegoV1Import(imps []*ast.Import) []*ast.Import {
return ensureImport(imps, ast.RegoV1CompatibleRef)
}
@@ -2331,7 +2454,7 @@ func ensureImport(imps []*ast.Import, path ast.Ref) []*ast.Import {
imp := &ast.Import{
Path: ast.NewTerm(path),
}
- imp.Location = defaultLocation(imp)
+ imp.Location = nextImportLoc(imps, imp)
return append(imps, imp)
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go b/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go
index f792e2c1b6..f395fb2b62 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/ir/marshal.go
@@ -103,6 +103,53 @@ type typedOperand struct {
Value Val `json:"value"`
}
+// MarshalJSON for MakeNumberRefStmt emits both "index" (the canonical key,
+// matching the casing of every other field in the IR) and "Index" (the
+// historical key, kept for backwards compatibility with consumers that
+// hard-code the original spelling). The "Index" key is deprecated and will
+// be removed in a future major release; new consumers should read "index".
+func (m *MakeNumberRefStmt) MarshalJSON() ([]byte, error) {
+ return json.Marshal(struct {
+ File int `json:"file"`
+ Col int `json:"col"`
+ Row int `json:"row"`
+ Index int `json:"index"`
+ IndexLegacy int `json:"Index"` // deprecated; remove in next major
+ Target Local `json:"target"`
+ }{
+ File: m.File,
+ Col: m.Col,
+ Row: m.Row,
+ Index: m.Index,
+ IndexLegacy: m.Index,
+ Target: m.Target,
+ })
+}
+
+// UnmarshalJSON for MakeNumberRefStmt accepts either the canonical "index"
+// key or the deprecated "Index" key. When both are present, "index" wins.
+func (m *MakeNumberRefStmt) UnmarshalJSON(bs []byte) error {
+ var raw struct {
+ File int `json:"file"`
+ Col int `json:"col"`
+ Row int `json:"row"`
+ Index *int `json:"index"`
+ IndexLegacy *int `json:"Index"`
+ Target Local `json:"target"`
+ }
+ if err := json.Unmarshal(bs, &raw); err != nil {
+ return err
+ }
+ m.File, m.Col, m.Row, m.Target = raw.File, raw.Col, raw.Row, raw.Target
+ switch {
+ case raw.Index != nil:
+ m.Index = *raw.Index
+ case raw.IndexLegacy != nil:
+ m.Index = *raw.IndexLegacy
+ }
+ return nil
+}
+
var stmtFactories = map[string]func() Stmt{
"ReturnLocalStmt": func() Stmt { return &ReturnLocalStmt{} },
"CallStmt": func() Stmt { return &CallStmt{} },
@@ -145,3 +192,25 @@ var valFactories = map[string]func() Val{
"string_index": func() Val { var x StringIndex; return &x },
"local": func() Val { var x Local; return &x },
}
+
+// StmtKinds returns a fresh zero-value instance of every registered Stmt
+// kind, keyed by the discriminator string used in the JSON form. Useful for
+// tools (schema generators, linters, transformers) that need to walk the
+// IR's polymorphic Stmt universe without depending on package internals.
+func StmtKinds() map[string]Stmt {
+ out := make(map[string]Stmt, len(stmtFactories))
+ for k, f := range stmtFactories {
+ out[k] = f()
+ }
+ return out
+}
+
+// ValKinds returns a fresh zero-value instance of every registered Val kind,
+// keyed by the discriminator string. See StmtKinds for usage notes.
+func ValKinds() map[string]Val {
+ out := make(map[string]Val, len(valFactories))
+ for k, f := range valFactories {
+ out[k] = f()
+ }
+ return out
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json b/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json
new file mode 100644
index 0000000000..e4af18ba23
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/ir/plan.schema.json
@@ -0,0 +1,1782 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openpolicyagent.org/schemas/ir/v1/plan.schema.json",
+ "title": "OPA IR Plan",
+ "description": "JSON Schema for the IR plan produced by `opa build -t plan`. Generated from v1/ir/ir.go.",
+ "$ref": "#/$defs/Policy",
+ "$defs": {
+ "ArrayAppendStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "array": {
+ "type": "integer"
+ },
+ "value": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "array",
+ "col",
+ "file",
+ "row",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "AssignIntStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "AssignVarOnceStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "AssignVarStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "Block": {
+ "type": "object",
+ "properties": {
+ "stmts": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/Stmt"
+ }
+ }
+ },
+ "required": [
+ "stmts"
+ ],
+ "additionalProperties": false
+ },
+ "BlockStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "blocks": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Block"
+ }
+ }
+ },
+ "required": [
+ "blocks",
+ "col",
+ "file",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "BreakStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "index": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "index",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "BuiltinFunc": {
+ "type": "object",
+ "properties": {
+ "decl": {
+ "type": [
+ "object",
+ "null"
+ ],
+ "description": "BuiltinFunc declaration; opaque in this schema."
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "decl",
+ "name"
+ ],
+ "additionalProperties": false
+ },
+ "CallDynamicStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "args": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "path": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "result": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "args",
+ "col",
+ "file",
+ "path",
+ "result",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "CallStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "args": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "func": {
+ "type": "string"
+ },
+ "result": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "args",
+ "col",
+ "file",
+ "func",
+ "result",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "DotStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "key": {
+ "$ref": "#/$defs/Operand"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "key",
+ "row",
+ "source",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "EqualStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "a": {
+ "$ref": "#/$defs/Operand"
+ },
+ "b": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "a",
+ "b",
+ "col",
+ "file",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "Func": {
+ "type": "object",
+ "properties": {
+ "blocks": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Block"
+ }
+ },
+ "name": {
+ "type": "string"
+ },
+ "params": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "path": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "return": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "blocks",
+ "name",
+ "params",
+ "return"
+ ],
+ "additionalProperties": false
+ },
+ "Funcs": {
+ "type": "object",
+ "properties": {
+ "funcs": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Func"
+ }
+ }
+ },
+ "required": [
+ "funcs"
+ ],
+ "additionalProperties": false
+ },
+ "IsArrayStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "IsDefinedStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "IsObjectStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "IsSetStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "IsUndefinedStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "LenStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "$ref": "#/$defs/Operand"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "MakeArrayStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "capacity": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "capacity",
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "MakeNullStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "MakeNumberIntStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "MakeNumberRefStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "index": {
+ "type": "integer"
+ },
+ "Index": {
+ "type": "integer",
+ "deprecated": true,
+ "description": "Deprecated alias for `index`. Both keys are emitted by current OPA versions for backwards compatibility; will be removed in a future major release. Read `index` instead."
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "index",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "MakeObjectStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "MakeSetStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "NopStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "NotEqualStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "a": {
+ "$ref": "#/$defs/Operand"
+ },
+ "b": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "a",
+ "b",
+ "col",
+ "file",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "NotStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "block": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Block"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "block",
+ "col",
+ "file",
+ "row"
+ ],
+ "additionalProperties": false
+ },
+ "ObjectInsertOnceStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "key": {
+ "$ref": "#/$defs/Operand"
+ },
+ "object": {
+ "type": "integer"
+ },
+ "value": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "key",
+ "object",
+ "row",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "ObjectInsertStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "key": {
+ "$ref": "#/$defs/Operand"
+ },
+ "object": {
+ "type": "integer"
+ },
+ "value": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "key",
+ "object",
+ "row",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "ObjectMergeStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "a": {
+ "type": "integer"
+ },
+ "b": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "a",
+ "b",
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "Operand": {
+ "$ref": "#/$defs/Val"
+ },
+ "Plan": {
+ "type": "object",
+ "properties": {
+ "blocks": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Block"
+ }
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "blocks",
+ "name"
+ ],
+ "additionalProperties": false
+ },
+ "Plans": {
+ "type": "object",
+ "properties": {
+ "plans": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/$defs/Plan"
+ }
+ }
+ },
+ "required": [
+ "plans"
+ ],
+ "additionalProperties": false
+ },
+ "Policy": {
+ "type": "object",
+ "properties": {
+ "funcs": {
+ "$ref": "#/$defs/Funcs"
+ },
+ "plans": {
+ "$ref": "#/$defs/Plans"
+ },
+ "static": {
+ "$ref": "#/$defs/Static"
+ }
+ },
+ "additionalProperties": false
+ },
+ "ResetLocalStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "target": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "target"
+ ],
+ "additionalProperties": false
+ },
+ "ResultSetAddStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "ReturnLocalStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "source"
+ ],
+ "additionalProperties": false
+ },
+ "ScanStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "block": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Block"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "key": {
+ "type": "integer"
+ },
+ "source": {
+ "type": "integer"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "block",
+ "col",
+ "file",
+ "key",
+ "row",
+ "source",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "SetAddStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "set": {
+ "type": "integer"
+ },
+ "value": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "col",
+ "file",
+ "row",
+ "set",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "Static": {
+ "type": "object",
+ "properties": {
+ "builtin_funcs": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/BuiltinFunc"
+ }
+ },
+ "files": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/StringConst"
+ }
+ },
+ "strings": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/StringConst"
+ }
+ }
+ },
+ "additionalProperties": false
+ },
+ "Stmt": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ArrayAppendStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ArrayAppendStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "AssignIntStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/AssignIntStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "AssignVarOnceStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/AssignVarOnceStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "AssignVarStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/AssignVarStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "BlockStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/BlockStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "BreakStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/BreakStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "CallDynamicStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/CallDynamicStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "CallStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/CallStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "DotStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/DotStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "EqualStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/EqualStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "IsArrayStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/IsArrayStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "IsDefinedStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/IsDefinedStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "IsObjectStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/IsObjectStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "IsSetStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/IsSetStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "IsUndefinedStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/IsUndefinedStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "LenStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/LenStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeArrayStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeArrayStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeNullStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeNullStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeNumberIntStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeNumberIntStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeNumberRefStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeNumberRefStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeObjectStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeObjectStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "MakeSetStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/MakeSetStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "NopStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/NopStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "NotEqualStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/NotEqualStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "NotStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/NotStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ObjectInsertOnceStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ObjectInsertOnceStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ObjectInsertStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ObjectInsertStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ObjectMergeStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ObjectMergeStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ResetLocalStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ResetLocalStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ResultSetAddStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ResultSetAddStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ReturnLocalStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ReturnLocalStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "ScanStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/ScanStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "SetAddStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/SetAddStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "WithStmt"
+ },
+ "stmt": {
+ "$ref": "#/$defs/WithStmt"
+ }
+ },
+ "required": [
+ "type",
+ "stmt"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "StringConst": {
+ "type": "object",
+ "properties": {
+ "value": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ "Val": {
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "bool"
+ },
+ "value": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "type",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "local"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "type",
+ "value"
+ ],
+ "additionalProperties": false
+ },
+ {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "string_index"
+ },
+ "value": {
+ "type": "integer"
+ }
+ },
+ "required": [
+ "type",
+ "value"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "WithStmt": {
+ "type": "object",
+ "properties": {
+ "col": {
+ "type": "integer"
+ },
+ "file": {
+ "type": "integer"
+ },
+ "row": {
+ "type": "integer"
+ },
+ "block": {
+ "oneOf": [
+ {
+ "$ref": "#/$defs/Block"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "local": {
+ "type": "integer"
+ },
+ "path": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "integer"
+ }
+ },
+ "value": {
+ "$ref": "#/$defs/Operand"
+ }
+ },
+ "required": [
+ "block",
+ "col",
+ "file",
+ "local",
+ "path",
+ "row",
+ "value"
+ ],
+ "additionalProperties": false
+ }
+ }
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go b/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go
index 1fbd560035..3aaaac0973 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/logging/buffered_logger.go
@@ -16,12 +16,15 @@ type logEntry struct {
// BufferedLogger captures log entries in memory until Flush is called,
// at which point it replays all buffered entries to the target logger.
-// After Flush() is called, the BufferedLogger should not be used anymore.
+// After Flush, subsequent log calls are forwarded to the flush target.
+// This ensures that code holding a cached reference to the BufferedLogger
+// (or a WithFields derivative) continues to work correctly.
type BufferedLogger struct {
mu sync.Mutex
buffer []logEntry
maxEntries int
currentLevel Level
+ target Logger
}
// NewBufferedLogger creates a new buffered logger that will buffer up to maxEntries.
@@ -42,20 +45,38 @@ func (b *BufferedLogger) addToBuffer(level Level, format string, args []any, fie
message = fmt.Sprintf(format, args...)
}
- entry := logEntry{
- level: level,
- message: message,
- fields: fields,
- time: time.Now(),
- }
-
b.mu.Lock()
- defer b.mu.Unlock()
+ if b.target != nil {
+ target := b.target
+ b.mu.Unlock()
+
+ logger := target
+ if len(fields) > 0 {
+ logger = target.WithFields(fields)
+ }
+ switch level {
+ case Debug:
+ logger.Debug("%s", message)
+ case Info:
+ logger.Info("%s", message)
+ case Warn:
+ logger.Warn("%s", message)
+ case Error:
+ logger.Error("%s", message)
+ }
+ return
+ }
if len(b.buffer) >= b.maxEntries {
b.buffer = b.buffer[1:]
}
- b.buffer = append(b.buffer, entry)
+ b.buffer = append(b.buffer, logEntry{
+ level: level,
+ message: message,
+ fields: fields,
+ time: time.Now(),
+ })
+ b.mu.Unlock()
}
func (*BufferedLogger) logToTarget(target Logger, entry logEntry) {
@@ -105,6 +126,9 @@ func (b *BufferedLogger) WithFields(fields map[string]any) Logger {
func (b *BufferedLogger) GetLevel() Level {
b.mu.Lock()
defer b.mu.Unlock()
+ if b.target != nil {
+ return b.target.GetLevel()
+ }
return b.currentLevel
}
@@ -113,6 +137,9 @@ func (b *BufferedLogger) SetLevel(level Level) {
b.mu.Lock()
defer b.mu.Unlock()
b.currentLevel = level
+ if b.target != nil {
+ b.target.SetLevel(level)
+ }
}
// Close discards all buffered entries without flushing them.
@@ -124,14 +151,15 @@ func (b *BufferedLogger) Close() {
}
// Flush replays all buffered entries to the target logger.
-// After calling Flush, the BufferedLogger should not be used anymore.
-// The caller should switch to using the target logger directly.
+// After Flush, subsequent log calls on this BufferedLogger (and any
+// previously obtained WithFields loggers) are forwarded to the target.
func (b *BufferedLogger) Flush(targetLogger Logger) {
if targetLogger == nil {
return
}
b.mu.Lock()
+ b.target = targetLogger
targetLogger.SetLevel(b.currentLevel)
entries := b.buffer
b.buffer = nil
diff --git a/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go b/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go
index a69dba1bb8..6f2a824cb5 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/rego/rego.go
@@ -100,6 +100,7 @@ type EvalContext struct {
parsedInput ast.Value
metrics metrics.Metrics
txn storage.Transaction
+ generateJSON func(*ast.Term, *EvalContext) (any, error)
instrument bool
instrumentation *topdown.Instrumentation
partialNamespace string
@@ -125,6 +126,9 @@ type EvalContext struct {
baseCache topdown.BaseCache
tracing tracing.Options
externalCancel topdown.Cancel // Note(philip): If non-nil, the cancellation is handled outside of this package.
+ requestMetadata map[string]any
+ responseMetadata map[string]any
+ evaluated *topdown.EvaluatedRuleTracker
}
func (e *EvalContext) RawInput() *any {
@@ -224,6 +228,15 @@ func EvalTransaction(txn storage.Transaction) EvalOption {
}
}
+// EvalGenerateJSON sets the AST to JSON converter for an evaluation. When set, this
+// option takes precedence over [GenerateJSON] set on the Rego object from e.g. a prepared
+// query, allowing individual evaluations to customize how the result is transformed.
+func EvalGenerateJSON(f func(*ast.Term, *EvalContext) (any, error)) EvalOption {
+ return func(e *EvalContext) {
+ e.generateJSON = f
+ }
+}
+
// EvalInstrument enables or disables instrumenting for a Prepared Query's evaluation
func EvalInstrument(instrument bool) EvalOption {
return func(e *EvalContext) {
@@ -408,6 +421,31 @@ func EvalExternalCancel(ec topdown.Cancel) EvalOption {
}
}
+// EvalRequestMetadata sets arbitrary metadata from the caller that can be
+// passed through to the evaluation. This allows wrapping projects to attach
+// custom metadata to queries.
+func EvalRequestMetadata(m map[string]any) EvalOption {
+ return func(e *EvalContext) {
+ e.requestMetadata = m
+ }
+}
+
+// EvalResponseMetadata sets a map that wrapping projects can populate during
+// evaluation to include additional fields in the API response.
+func EvalResponseMetadata(m map[string]any) EvalOption {
+ return func(e *EvalContext) {
+ e.responseMetadata = m
+ }
+}
+
+// EvalEvaluatedRuleTracker sets a tracker to record rule identifiers that
+// were successfully evaluated during query evaluation.
+func EvalEvaluatedRuleTracker(t *topdown.EvaluatedRuleTracker) EvalOption {
+ return func(e *EvalContext) {
+ e.evaluated = t
+ }
+}
+
func (pq preparedQuery) Modules() map[string]*ast.Module {
mods := make(map[string]*ast.Module)
@@ -654,6 +692,7 @@ type Rego struct {
strictBuiltinErrors bool
builtinErrorList *[]topdown.Error
resolvers []refResolver
+ externalSources []ast.ExternalRuleSource
schemaSet *ast.SchemaSet
target string // target type (wasm, rego, etc.)
opa opa.EvalEngine
@@ -667,6 +706,7 @@ type Rego struct {
compilerHook func(*ast.Compiler)
evalMode *ast.CompilerEvalMode
filter filter.LoaderFilter
+ evaluated *topdown.EvaluatedRuleTracker
}
func (r *Rego) RegoVersion() ast.RegoVersion {
@@ -1268,6 +1308,15 @@ func Resolver(ref ast.Ref, r resolver.Resolver) func(r *Rego) {
}
}
+// ExternalSource adds an external rule source that provides rules dynamically.
+// The source declares which package refs it handles via its Refs() method.
+// A single source can provide rules for multiple packages.
+func ExternalSource(source ast.ExternalRuleSource) func(r *Rego) {
+ return func(rego *Rego) {
+ rego.externalSources = append(rego.externalSources, source)
+ }
+}
+
// Schemas sets the schemaSet
func Schemas(x *ast.SchemaSet) func(r *Rego) {
return func(r *Rego) {
@@ -1291,7 +1340,10 @@ func Target(t string) func(r *Rego) {
}
}
-// GenerateJSON sets the AST to JSON converter for the results.
+// GenerateJSON sets the AST to JSON converter to use for results. This will have any evaluationn on a Rego
+// object use the provided function for conversion. Use [EvalGenerateJSON] if you want to set a converter
+// function for individual evaluations, which will take precedence over GenerateJSON set on the Rego object,
+// (i.e. by this function) for the scope of that evaluation.
func GenerateJSON(f func(*ast.Term, *EvalContext) (any, error)) func(r *Rego) {
return func(r *Rego) {
r.generateJSON = f
@@ -1350,6 +1402,14 @@ func EvalMode(mode ast.CompilerEvalMode) func(r *Rego) {
}
}
+// EvaluatedRuleTracker returns an option that sets a tracker to record rule
+// identifiers that were successfully evaluated during query evaluation.
+func EvaluatedRuleTracker(t *topdown.EvaluatedRuleTracker) func(r *Rego) {
+ return func(r *Rego) {
+ r.evaluated = t
+ }
+}
+
// New returns a new Rego object.
func New(options ...func(r *Rego)) *Rego {
r := &Rego{
@@ -2123,6 +2183,18 @@ func parserOptionsFromRegoVersionImport(imports []*ast.Import, popts ast.ParserO
}
func (r *Rego) compileModules(ctx context.Context, txn storage.Transaction, m metrics.Metrics) error {
+ if len(r.externalSources) > 0 && r.target != "" && r.target != targetRego {
+ return fmt.Errorf("external rule sources are not supported with target %q: only the default (rego) target is supported", r.target)
+ }
+
+ // Apply external sources to the compiler before compilation
+ for i := range r.externalSources {
+ source := r.externalSources[i]
+ for _, ref := range source.Refs() {
+ r.compiler.WithExternalSource(ref, source)
+ }
+ }
+
// Only compile again if there are new modules.
if len(r.bundles) > 0 || len(r.parsedModules) > 0 {
@@ -2271,7 +2343,15 @@ func (r *Rego) eval(ctx context.Context, ectx *EvalContext) (ResultSet, error) {
WithPrintHook(ectx.printHook).
WithDistributedTracingOpts(r.distributedTracingOpts).
WithVirtualCache(ectx.virtualCache).
- WithBaseCache(ectx.baseCache)
+ WithBaseCache(ectx.baseCache).
+ WithRequestMetadata(ectx.requestMetadata).
+ WithResponseMetadata(ectx.responseMetadata)
+
+ if ectx.evaluated != nil {
+ q = q.WithEvaluatedRuleTracker(ectx.evaluated)
+ } else {
+ q = q.WithEvaluatedRuleTracker(r.evaluated)
+ }
if !ectx.time.IsZero() {
q = q.WithTime(ectx.time)
@@ -2395,6 +2475,11 @@ func (r *Rego) valueToQueryResult(res ast.Value, ectx *EvalContext) (ResultSet,
func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result, error) {
rewritten := ectx.compiledQuery.compiler.RewrittenVars()
+ generateJSON := r.generateJSON
+ if ectx.generateJSON != nil {
+ generateJSON = ectx.generateJSON
+ }
+
result := newResult()
for k, term := range qr {
if rw, ok := rewritten[k]; ok {
@@ -2404,7 +2489,7 @@ func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result
continue
}
- v, err := r.generateJSON(term, ectx)
+ v, err := generateJSON(term, ectx)
if err != nil {
return result, err
}
@@ -2418,7 +2503,7 @@ func (r *Rego) generateResult(qr topdown.QueryResult, ectx *EvalContext) (Result
}
if k, ok := r.capture[expr]; ok {
- v, err := r.generateJSON(qr[k], ectx)
+ v, err := generateJSON(qr[k], ectx)
if err != nil {
return result, err
}
@@ -2565,7 +2650,9 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
WithInterQueryBuiltinValueCache(ectx.interQueryBuiltinValueCache).
WithStrictBuiltinErrors(ectx.strictBuiltinErrors).
WithSeed(ectx.seed).
- WithPrintHook(ectx.printHook)
+ WithPrintHook(ectx.printHook).
+ WithRequestMetadata(ectx.requestMetadata).
+ WithResponseMetadata(ectx.responseMetadata)
if !ectx.time.IsZero() {
q = q.WithTime(ectx.time)
@@ -2616,31 +2703,31 @@ func (r *Rego) partial(ctx context.Context, ectx *EvalContext) (*PartialQueries,
for i, mod := range support {
// We can't apply the RegoV0CompatV1 version to the support module if it contains rules or vars that
// conflict with future keywords.
- applyRegoVersion := true
+ applyCompatRegoVersion := true
ast.WalkRules(mod, func(r *ast.Rule) bool {
name := r.Head.Name
if name == "" && len(r.Head.Reference) > 0 {
name = r.Head.Reference[0].Value.(ast.Var)
}
- if ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV0) {
- applyRegoVersion = false
+ if ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV0) && !ast.IsFutureKeywordForRegoVersion(name.String(), ast.RegoV1) {
+ applyCompatRegoVersion = false
return true
}
return false
})
- if applyRegoVersion {
+ if applyCompatRegoVersion {
ast.WalkVars(mod, func(v ast.Var) bool {
- if ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV0) {
- applyRegoVersion = false
+ if ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV0) && !ast.IsFutureKeywordForRegoVersion(v.String(), ast.RegoV1) {
+ applyCompatRegoVersion = false
return true
}
return false
})
}
- if applyRegoVersion {
+ if applyCompatRegoVersion {
support[i].SetRegoVersion(ast.RegoV0CompatV1)
} else {
support[i].SetRegoVersion(r.regoVersion)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go b/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go
index 884e4ca7cc..25c01300a8 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/resolver/wasm/wasm.go
@@ -16,8 +16,16 @@ import (
)
// New creates a new Resolver instance which is using the Wasm module
-// policy for the given entrypoint ref.
+// policy for the given entrypoint ref. This method creates a new
+// background context. If you need to pass an existing context use
+// NewWithContext instead.
func New(entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) {
+ return NewWithContext(context.Background(), entrypoints, policy, data)
+}
+
+// NewWithContext creates a new Resolver instance which is using the Wasm module
+// policy for the given entrypoint ref. This method accepts a context.
+func NewWithContext(ctx context.Context, entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) {
e, err := opa.LookupEngine("wasm")
if err != nil {
return nil, err
@@ -37,7 +45,7 @@ func New(entrypoints []ast.Ref, policy []byte, data any) (*Resolver, error) {
// only the configured ones will be used when Eval() is
// called.
entrypointRefToID := ast.NewValueMap()
- epIDs, err := o.Entrypoints(context.Background())
+ epIDs, err := o.Entrypoints(ctx)
if err != nil {
return nil, err
}
@@ -137,7 +145,6 @@ func (r *Resolver) RemoveDataPath(ctx context.Context, path []string) error {
}
func getResult(evalResult *opa.Result) (ast.Value, error) {
-
parsed, err := ast.ParseTerm(string(evalResult.Result))
if err != nil {
return nil, fmt.Errorf("failed to parse wasm result: %s", err)
diff --git a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go
index 9fa145a051..8aa1fc9e42 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/inmem.go
@@ -98,6 +98,21 @@ func NewFromReaderWithOpts(r io.Reader, opts ...Opt) storage.Store {
return NewFromObjectWithOpts(data, opts...)
}
+// NewFromASTObject returns a new in-memory store from the supplied AST object, with
+// [OptReturnASTValuesOnRead] enabled. This allows avoiding the overhead of an extra AST
+// -> map[string]any -> AST round trip for callers whose data already exists in AST form.
+// Note that data passed is **not** copied and it is the responsibility of the caller to
+// ensure either that ownership of the data is transferred fully to the store, or when
+// that's not possible, that a deep copy of the original data is passed.
+func NewFromASTObject(data ast.Object) storage.Store {
+ return &store{
+ data: data,
+ triggers: map[*handle]storage.TriggerConfig{},
+ policies: map[string][]byte{},
+ returnASTValuesOnRead: true,
+ }
+}
+
type store struct {
rmu sync.RWMutex // reader-writer lock
wmu sync.Mutex // writer lock
@@ -304,6 +319,14 @@ func (db *store) Register(_ context.Context, txn storage.Transaction, config sto
return h, nil
}
+func (db *store) MakeDir(_ context.Context, txn storage.Transaction, path storage.Path) error {
+ underlying, err := db.underlying(txn)
+ if err != nil {
+ return err
+ }
+ return underlying.makeDir(path)
+}
+
func (db *store) Read(_ context.Context, txn storage.Transaction, path storage.Path) (any, error) {
underlying, err := db.underlying(txn)
if err != nil {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go
index e76bccd013..99f0bca31c 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/storage/inmem/txn.go
@@ -297,6 +297,75 @@ func (txn *transaction) Read(path storage.Path) (any, error) {
return cpy, nil
}
+func (txn *transaction) makeDir(path storage.Path) error {
+ if len(path) == 0 {
+ return nil
+ }
+
+ exists, isObj, err := txn.isObject(path)
+ if err != nil {
+ return err
+ }
+
+ if exists {
+ if isObj {
+ return nil
+ }
+ return &storage.Error{
+ Code: storage.WriteConflictErr,
+ Message: path.String(),
+ }
+ }
+
+ if err := txn.makeDir(path[:len(path)-1]); err != nil {
+ return err
+ }
+
+ return txn.Write(storage.AddOp, path, map[string]any{})
+}
+
+// isObject checks whether the given path exists and points to an object,
+// without deep-copying the subtree the way transaction.Read would.
+func (txn *transaction) isObject(path storage.Path) (exists bool, isObj bool, err error) {
+ if !txn.write || txn.updates == nil {
+ return isObjectNode(pointer(txn.db.data, path))
+ }
+
+ for curr := txn.updates.Front(); curr != nil; curr = curr.Next() {
+ upd := curr.Value.(dataUpdate)
+
+ if path.HasPrefix(upd.Path()) {
+ if upd.Remove() {
+ return false, false, nil
+ }
+ return isObjectNode(pointer(upd.Value(), path[len(upd.Path()):]))
+ }
+
+ // A child of this path has been written, so this path is an object.
+ if upd.Path().HasPrefix(path) {
+ return true, true, nil
+ }
+ }
+
+ return isObjectNode(pointer(txn.db.data, path))
+}
+
+func isObjectNode(node any, err error) (bool, bool, error) {
+ if err != nil {
+ if storage.IsNotFound(err) {
+ return false, false, nil
+ }
+ return false, false, err
+ }
+
+ switch node.(type) {
+ case map[string]any, ast.Object:
+ return true, true, nil
+ default:
+ return true, false, nil
+ }
+}
+
func (txn *transaction) ListPolicies() (ids []string) {
for id := range txn.db.policies {
if _, ok := txn.policies[id]; !ok {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go
index 05050dbf7d..97cc2fef3e 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/binary.go
@@ -10,7 +10,6 @@ import (
)
func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
-
s1, err := builtins.SetOperand(operands[0].Value, 1)
if err != nil {
return err
@@ -21,6 +20,10 @@ func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
return err
}
+ if s1.Len() == 0 || s2.Len() == 0 {
+ return iter(ast.InternedEmptySet)
+ }
+
i := s1.Intersect(s2)
if i.Len() == 0 {
return iter(ast.InternedEmptySet)
@@ -30,7 +33,6 @@ func builtinBinaryAnd(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
}
func builtinBinaryOr(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
-
s1, err := builtins.SetOperand(operands[0].Value, 1)
if err != nil {
return err
@@ -41,6 +43,14 @@ func builtinBinaryOr(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term
return err
}
+ if s1.Len() == 0 {
+ return iter(operands[1])
+ }
+
+ if s2.Len() == 0 {
+ return iter(operands[0])
+ }
+
return iter(ast.NewTerm(s1.Union(s2)))
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go
index e0b893d477..b5a8a6714d 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/builtins.go
@@ -54,6 +54,8 @@ type (
PrintHook print.Hook // provides callback function to use for printing
RoundTripper CustomizeRoundTripper // customize transport to use for HTTP requests
DistributedTracingOpts tracing.Options // options to be used by distributed tracing.
+ RequestMetadata map[string]any // metadata from the caller, for use by wrapping projects
+ ResponseMetadata map[string]any // metadata for the response, populated by wrapping projects
rand *rand.Rand // randomization source for non-security-sensitive operations
Capabilities *ast.Capabilities
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go
index 607855632d..ae30723dfb 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/copypropagation/copypropagation.go
@@ -96,16 +96,24 @@ func (p *CopyPropagator) Apply(query ast.Body) ast.Body {
removedEqs := ast.NewValueMap()
for _, expr := range query {
-
pctx := &plugContext{
removedEqs: removedEqs,
uf: uf,
- negated: expr.Negated,
+ negated: expr.IsNegated(),
headvars: headvars,
}
expr = p.plugBindings(pctx, expr)
+ // Recurse into not-bodies after plugging outer bindings.
+ if n, ok := expr.Terms.(*ast.Not); ok {
+ innerLive := p.computeNotBodyLivevars(uf, removedEqs, n.Body)
+ innerCp := New(innerLive).
+ WithEnsureNonEmptyBody(true).
+ WithCompiler(p.compiler)
+ n.Body = innerCp.Apply(n.Body)
+ }
+
if p.updateBindings(pctx, expr) {
result.Append(expr)
}
@@ -158,7 +166,7 @@ func (p *CopyPropagator) Apply(query ast.Body) ast.Body {
safe.Update(ast.ReservedVars)
safe.Update(p.livevars)
safe.Update(ast.OutputVarsFromBody(p.compiler, result, safe))
- unsafe := result.Vars(ast.SafetyCheckVisitorParams).Diff(safe)
+ unsafe := result.Vars(ast.SafetyCheckVisitorParamsWithArity(p.compiler.GetArity)).Diff(safe)
for _, b := range sortbindings(removedEqs) {
removedEq := ast.Equality.Expr(ast.NewTerm(b.k), ast.NewTerm(b.v))
@@ -365,6 +373,25 @@ func (p *CopyPropagator) updateBindingsEqAsymmetric(a, b *ast.Term) (ast.Var, as
return "", nil, true
}
+// computeNotBodyLivevars returns the set of variables that must be treated as
+// live when recursing into a Not body. A variable is live if it appears in the
+// Not body and is bound or visible in the outer scope.
+func (p *CopyPropagator) computeNotBodyLivevars(uf *unionFind, removedEqs *ast.ValueMap, body ast.Body) ast.VarSet {
+ bodyVars := body.Vars(ast.SafetyCheckVisitorParams)
+
+ innerLive := ast.NewVarSet()
+ for v := range bodyVars {
+ if p.livevars.Contains(v) {
+ innerLive.Add(v)
+ } else if _, ok := uf.Find(v); ok {
+ innerLive.Add(v)
+ } else if removedEqs.Get(v) != nil {
+ innerLive.Add(v)
+ }
+ }
+ return innerLive
+}
+
type plugContext struct {
removedEqs *ast.ValueMap
uf *unionFind
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg
new file mode 100644
index 0000000000..339fe3bbe7
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration.peg
@@ -0,0 +1,34 @@
+{
+package durationparser
+}
+
+Duration <- sign:Sign? segments:Segment+ EOF {
+ signStr := ""
+ if sign != nil {
+ signStr = sign.(string)
+ }
+ raw := segments.([]any)
+ segs := make([]Segment, len(raw))
+ for i, s := range raw {
+ segs[i] = s.(Segment)
+ }
+ return Result{Sign: signStr, Segments: segs}, nil
+}
+
+Sign <- [-+] {
+ return string(c.text), nil
+}
+
+Segment <- digits:Digits unit:Unit {
+ return Segment{Digits: digits.(string), Unit: unit.(string)}, nil
+}
+
+Digits <- [0-9.]+ {
+ return string(c.text), nil
+}
+
+Unit <- ("ms" / "us" / "µs" / "ns" / [a-z]) {
+ return string(c.text), nil
+}
+
+EOF <- !.
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go
new file mode 100644
index 0000000000..0bef955f2d
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/duration_parser.go
@@ -0,0 +1,1549 @@
+// Code generated by pigeon; DO NOT EDIT.
+
+package durationparser
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "math"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "unicode"
+ "unicode/utf8"
+)
+
+var g = &grammar{
+ rules: []*rule{
+ {
+ name: "Duration",
+ pos: position{line: 5, col: 1, offset: 28},
+ expr: &actionExpr{
+ pos: position{line: 5, col: 13, offset: 40},
+ run: (*parser).callonDuration1,
+ expr: &seqExpr{
+ pos: position{line: 5, col: 13, offset: 40},
+ exprs: []any{
+ &labeledExpr{
+ pos: position{line: 5, col: 13, offset: 40},
+ label: "sign",
+ expr: &zeroOrOneExpr{
+ pos: position{line: 5, col: 18, offset: 45},
+ expr: &ruleRefExpr{
+ pos: position{line: 5, col: 18, offset: 45},
+ name: "Sign",
+ },
+ },
+ },
+ &labeledExpr{
+ pos: position{line: 5, col: 24, offset: 51},
+ label: "segments",
+ expr: &oneOrMoreExpr{
+ pos: position{line: 5, col: 33, offset: 60},
+ expr: &ruleRefExpr{
+ pos: position{line: 5, col: 33, offset: 60},
+ name: "Segment",
+ },
+ },
+ },
+ &ruleRefExpr{
+ pos: position{line: 5, col: 42, offset: 69},
+ name: "EOF",
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Sign",
+ pos: position{line: 18, col: 1, offset: 303},
+ expr: &actionExpr{
+ pos: position{line: 18, col: 9, offset: 311},
+ run: (*parser).callonSign1,
+ expr: &charClassMatcher{
+ pos: position{line: 18, col: 9, offset: 311},
+ val: "[-+]",
+ chars: []rune{'-', '+'},
+ ignoreCase: false,
+ inverted: false,
+ },
+ },
+ },
+ {
+ name: "Segment",
+ pos: position{line: 22, col: 1, offset: 349},
+ expr: &actionExpr{
+ pos: position{line: 22, col: 12, offset: 360},
+ run: (*parser).callonSegment1,
+ expr: &seqExpr{
+ pos: position{line: 22, col: 12, offset: 360},
+ exprs: []any{
+ &labeledExpr{
+ pos: position{line: 22, col: 12, offset: 360},
+ label: "digits",
+ expr: &ruleRefExpr{
+ pos: position{line: 22, col: 19, offset: 367},
+ name: "Digits",
+ },
+ },
+ &labeledExpr{
+ pos: position{line: 22, col: 26, offset: 374},
+ label: "unit",
+ expr: &ruleRefExpr{
+ pos: position{line: 22, col: 31, offset: 379},
+ name: "Unit",
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "Digits",
+ pos: position{line: 26, col: 1, offset: 456},
+ expr: &actionExpr{
+ pos: position{line: 26, col: 11, offset: 466},
+ run: (*parser).callonDigits1,
+ expr: &oneOrMoreExpr{
+ pos: position{line: 26, col: 11, offset: 466},
+ expr: &charClassMatcher{
+ pos: position{line: 26, col: 11, offset: 466},
+ val: "[0-9.]",
+ chars: []rune{'.'},
+ ranges: []rune{'0', '9'},
+ ignoreCase: false,
+ inverted: false,
+ },
+ },
+ },
+ },
+ {
+ name: "Unit",
+ pos: position{line: 30, col: 1, offset: 507},
+ expr: &actionExpr{
+ pos: position{line: 30, col: 9, offset: 515},
+ run: (*parser).callonUnit1,
+ expr: &choiceExpr{
+ pos: position{line: 30, col: 10, offset: 516},
+ alternatives: []any{
+ &litMatcher{
+ pos: position{line: 30, col: 10, offset: 516},
+ val: "ms",
+ ignoreCase: false,
+ want: "\"ms\"",
+ },
+ &litMatcher{
+ pos: position{line: 30, col: 17, offset: 523},
+ val: "us",
+ ignoreCase: false,
+ want: "\"us\"",
+ },
+ &litMatcher{
+ pos: position{line: 30, col: 24, offset: 530},
+ val: "µs",
+ ignoreCase: false,
+ want: "\"µs\"",
+ },
+ &litMatcher{
+ pos: position{line: 30, col: 31, offset: 538},
+ val: "ns",
+ ignoreCase: false,
+ want: "\"ns\"",
+ },
+ &charClassMatcher{
+ pos: position{line: 30, col: 38, offset: 545},
+ val: "[a-z]",
+ ranges: []rune{'a', 'z'},
+ ignoreCase: false,
+ inverted: false,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: "EOF",
+ pos: position{line: 34, col: 1, offset: 585},
+ expr: ¬Expr{
+ pos: position{line: 34, col: 8, offset: 592},
+ expr: &anyMatcher{
+ line: 34, col: 9, offset: 593,
+ },
+ },
+ },
+ },
+}
+
+func (c *current) onDuration1(sign, segments any) (any, error) {
+ signStr := ""
+ if sign != nil {
+ signStr = sign.(string)
+ }
+ raw := segments.([]any)
+ segs := make([]Segment, len(raw))
+ for i, s := range raw {
+ segs[i] = s.(Segment)
+ }
+ return Result{Sign: signStr, Segments: segs}, nil
+}
+
+func (p *parser) callonDuration1() (any, error) {
+ stack := p.vstack[len(p.vstack)-1]
+ _ = stack
+ return p.cur.onDuration1(stack["sign"], stack["segments"])
+}
+
+func (c *current) onSign1() (any, error) {
+ return string(c.text), nil
+}
+
+func (p *parser) callonSign1() (any, error) {
+ stack := p.vstack[len(p.vstack)-1]
+ _ = stack
+ return p.cur.onSign1()
+}
+
+func (c *current) onSegment1(digits, unit any) (any, error) {
+ return Segment{Digits: digits.(string), Unit: unit.(string)}, nil
+}
+
+func (p *parser) callonSegment1() (any, error) {
+ stack := p.vstack[len(p.vstack)-1]
+ _ = stack
+ return p.cur.onSegment1(stack["digits"], stack["unit"])
+}
+
+func (c *current) onDigits1() (any, error) {
+ return string(c.text), nil
+}
+
+func (p *parser) callonDigits1() (any, error) {
+ stack := p.vstack[len(p.vstack)-1]
+ _ = stack
+ return p.cur.onDigits1()
+}
+
+func (c *current) onUnit1() (any, error) {
+ return string(c.text), nil
+}
+
+func (p *parser) callonUnit1() (any, error) {
+ stack := p.vstack[len(p.vstack)-1]
+ _ = stack
+ return p.cur.onUnit1()
+}
+
+var (
+ // errNoRule is returned when the grammar to parse has no rule.
+ errNoRule = errors.New("grammar has no rule")
+
+ // errInvalidEntrypoint is returned when the specified entrypoint rule
+ // does not exit.
+ errInvalidEntrypoint = errors.New("invalid entrypoint")
+
+ // errInvalidEncoding is returned when the source is not properly
+ // utf8-encoded.
+ errInvalidEncoding = errors.New("invalid encoding")
+
+ // errMaxExprCnt is used to signal that the maximum number of
+ // expressions have been parsed.
+ errMaxExprCnt = errors.New("max number of expressions parsed")
+)
+
+// Option is a function that can set an option on the parser. It returns
+// the previous setting as an Option.
+type Option func(*parser) Option
+
+// MaxExpressions creates an Option to stop parsing after the provided
+// number of expressions have been parsed, if the value is 0 then the parser will
+// parse for as many steps as needed (possibly an infinite number).
+//
+// The default for maxExprCnt is 0.
+func MaxExpressions(maxExprCnt uint64) Option {
+ return func(p *parser) Option {
+ oldMaxExprCnt := p.maxExprCnt
+ p.maxExprCnt = maxExprCnt
+ return MaxExpressions(oldMaxExprCnt)
+ }
+}
+
+// Entrypoint creates an Option to set the rule name to use as entrypoint.
+// The rule name must have been specified in the -alternate-entrypoints
+// if generating the parser with the -optimize-grammar flag, otherwise
+// it may have been optimized out. Passing an empty string sets the
+// entrypoint to the first rule in the grammar.
+//
+// The default is to start parsing at the first rule in the grammar.
+func Entrypoint(ruleName string) Option {
+ return func(p *parser) Option {
+ oldEntrypoint := p.entrypoint
+ p.entrypoint = ruleName
+ if ruleName == "" {
+ p.entrypoint = g.rules[0].name
+ }
+ return Entrypoint(oldEntrypoint)
+ }
+}
+
+// Statistics adds a user provided Stats struct to the parser to allow
+// the user to process the results after the parsing has finished.
+// Also the key for the "no match" counter is set.
+//
+// Example usage:
+//
+// input := "input"
+// stats := Stats{}
+// _, err := Parse("input-file", []byte(input), Statistics(&stats, "no match"))
+// if err != nil {
+// log.Panicln(err)
+// }
+// b, err := json.MarshalIndent(stats.ChoiceAltCnt, "", " ")
+// if err != nil {
+// log.Panicln(err)
+// }
+// fmt.Println(string(b))
+func Statistics(stats *Stats, choiceNoMatch string) Option {
+ return func(p *parser) Option {
+ oldStats := p.Stats
+ p.Stats = stats
+ oldChoiceNoMatch := p.choiceNoMatch
+ p.choiceNoMatch = choiceNoMatch
+ if p.Stats.ChoiceAltCnt == nil {
+ p.Stats.ChoiceAltCnt = make(map[string]map[string]int)
+ }
+ return Statistics(oldStats, oldChoiceNoMatch)
+ }
+}
+
+// Debug creates an Option to set the debug flag to b. When set to true,
+// debugging information is printed to stdout while parsing.
+//
+// The default is false.
+func Debug(b bool) Option {
+ return func(p *parser) Option {
+ old := p.debug
+ p.debug = b
+ return Debug(old)
+ }
+}
+
+// Memoize creates an Option to set the memoize flag to b. When set to true,
+// the parser will cache all results so each expression is evaluated only
+// once. This guarantees linear parsing time even for pathological cases,
+// at the expense of more memory and slower times for typical cases.
+//
+// The default is false.
+func Memoize(b bool) Option {
+ return func(p *parser) Option {
+ old := p.memoize
+ p.memoize = b
+ return Memoize(old)
+ }
+}
+
+// AllowInvalidUTF8 creates an Option to allow invalid UTF-8 bytes.
+// Every invalid UTF-8 byte is treated as a utf8.RuneError (U+FFFD)
+// by character class matchers and is matched by the any matcher.
+// The returned matched value, c.text and c.offset are NOT affected.
+//
+// The default is false.
+func AllowInvalidUTF8(b bool) Option {
+ return func(p *parser) Option {
+ old := p.allowInvalidUTF8
+ p.allowInvalidUTF8 = b
+ return AllowInvalidUTF8(old)
+ }
+}
+
+// Recover creates an Option to set the recover flag to b. When set to
+// true, this causes the parser to recover from panics and convert it
+// to an error. Setting it to false can be useful while debugging to
+// access the full stack trace.
+//
+// The default is true.
+func Recover(b bool) Option {
+ return func(p *parser) Option {
+ old := p.recover
+ p.recover = b
+ return Recover(old)
+ }
+}
+
+// GlobalStore creates an Option to set a key to a certain value in
+// the globalStore.
+func GlobalStore(key string, value any) Option {
+ return func(p *parser) Option {
+ old := p.cur.globalStore[key]
+ p.cur.globalStore[key] = value
+ return GlobalStore(key, old)
+ }
+}
+
+// InitState creates an Option to set a key to a certain value in
+// the global "state" store.
+func InitState(key string, value any) Option {
+ return func(p *parser) Option {
+ old := p.cur.state[key]
+ p.cur.state[key] = value
+ return InitState(key, old)
+ }
+}
+
+// ParseFile parses the file identified by filename.
+func ParseFile(filename string, opts ...Option) (i any, err error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return nil, err
+ }
+ defer func() {
+ if closeErr := f.Close(); closeErr != nil {
+ err = closeErr
+ }
+ }()
+ return ParseReader(filename, f, opts...)
+}
+
+// ParseReader parses the data from r using filename as information in the
+// error messages.
+func ParseReader(filename string, r io.Reader, opts ...Option) (any, error) {
+ b, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ return Parse(filename, b, opts...)
+}
+
+// Parse parses the data from b using filename as information in the
+// error messages.
+func Parse(filename string, b []byte, opts ...Option) (any, error) {
+ return newParser(filename, b, opts...).parse(g)
+}
+
+// position records a position in the text.
+type position struct {
+ line, col, offset int
+}
+
+func (p position) String() string {
+ return strconv.Itoa(p.line) + ":" + strconv.Itoa(p.col) + " [" + strconv.Itoa(p.offset) + "]"
+}
+
+// savepoint stores all state required to go back to this point in the
+// parser.
+type savepoint struct {
+ position
+ rn rune
+ w int
+}
+
+type current struct {
+ pos position // start position of the match
+ text []byte // raw text of the match
+
+ // state is a store for arbitrary key,value pairs that the user wants to be
+ // tied to the backtracking of the parser.
+ // This is always rolled back if a parsing rule fails.
+ state storeDict
+
+ // globalStore is a general store for the user to store arbitrary key-value
+ // pairs that they need to manage and that they do not want tied to the
+ // backtracking of the parser. This is only modified by the user and never
+ // rolled back by the parser. It is always up to the user to keep this in a
+ // consistent state.
+ globalStore storeDict
+}
+
+type storeDict map[string]any
+
+// the AST types...
+
+type grammar struct {
+ pos position
+ rules []*rule
+}
+
+type rule struct {
+ pos position
+ name string
+ displayName string
+ expr any
+}
+
+type choiceExpr struct {
+ pos position
+ alternatives []any
+}
+
+type actionExpr struct {
+ pos position
+ expr any
+ run func(*parser) (any, error)
+}
+
+type recoveryExpr struct {
+ pos position
+ expr any
+ recoverExpr any
+ failureLabel []string
+}
+
+type seqExpr struct {
+ pos position
+ exprs []any
+}
+
+type throwExpr struct {
+ pos position
+ label string
+}
+
+type labeledExpr struct {
+ pos position
+ label string
+ expr any
+}
+
+type expr struct {
+ pos position
+ expr any
+}
+
+type (
+ andExpr expr
+ notExpr expr
+ zeroOrOneExpr expr
+ zeroOrMoreExpr expr
+ oneOrMoreExpr expr
+)
+
+type ruleRefExpr struct {
+ pos position
+ name string
+}
+
+type stateCodeExpr struct {
+ pos position
+ run func(*parser) error
+}
+
+type andCodeExpr struct {
+ pos position
+ run func(*parser) (bool, error)
+}
+
+type notCodeExpr struct {
+ pos position
+ run func(*parser) (bool, error)
+}
+
+type litMatcher struct {
+ pos position
+ val string
+ ignoreCase bool
+ want string
+}
+
+type charClassMatcher struct {
+ pos position
+ val string
+ basicLatinChars [128]bool
+ chars []rune
+ ranges []rune
+ classes []*unicode.RangeTable
+ ignoreCase bool
+ inverted bool
+}
+
+type anyMatcher position
+
+// errList cumulates the errors found by the parser.
+type errList []error
+
+func (e *errList) add(err error) {
+ *e = append(*e, err)
+}
+
+func (e errList) err() error {
+ if len(e) == 0 {
+ return nil
+ }
+ e.dedupe()
+ return e
+}
+
+func (e *errList) dedupe() {
+ var cleaned []error
+ set := make(map[string]bool)
+ for _, err := range *e {
+ if msg := err.Error(); !set[msg] {
+ set[msg] = true
+ cleaned = append(cleaned, err)
+ }
+ }
+ *e = cleaned
+}
+
+func (e errList) Error() string {
+ switch len(e) {
+ case 0:
+ return ""
+ case 1:
+ return e[0].Error()
+ default:
+ var buf bytes.Buffer
+
+ for i, err := range e {
+ if i > 0 {
+ buf.WriteRune('\n')
+ }
+ buf.WriteString(err.Error())
+ }
+ return buf.String()
+ }
+}
+
+// parserError wraps an error with a prefix indicating the rule in which
+// the error occurred. The original error is stored in the Inner field.
+type parserError struct {
+ Inner error
+ pos position
+ prefix string
+ expected []string
+}
+
+// Error returns the error message.
+func (p *parserError) Error() string {
+ return p.prefix + ": " + p.Inner.Error()
+}
+
+// newParser creates a parser with the specified input source and options.
+func newParser(filename string, b []byte, opts ...Option) *parser {
+ stats := Stats{
+ ChoiceAltCnt: make(map[string]map[string]int),
+ }
+
+ p := &parser{
+ filename: filename,
+ errs: new(errList),
+ data: b,
+ pt: savepoint{position: position{line: 1}},
+ recover: true,
+ cur: current{
+ state: make(storeDict),
+ globalStore: make(storeDict),
+ },
+ maxFailPos: position{col: 1, line: 1},
+ maxFailExpected: make([]string, 0, 20),
+ Stats: &stats,
+ // start rule is rule [0] unless an alternate entrypoint is specified
+ entrypoint: g.rules[0].name,
+ }
+ p.setOptions(opts)
+
+ if p.maxExprCnt == 0 {
+ p.maxExprCnt = math.MaxUint64
+ }
+
+ return p
+}
+
+// setOptions applies the options to the parser.
+func (p *parser) setOptions(opts []Option) {
+ for _, opt := range opts {
+ opt(p)
+ }
+}
+
+type resultTuple struct {
+ v any
+ b bool
+ end savepoint
+}
+
+const choiceNoMatch = -1
+
+// Stats stores some statistics, gathered during parsing
+type Stats struct {
+ // ExprCnt counts the number of expressions processed during parsing
+ // This value is compared to the maximum number of expressions allowed
+ // (set by the MaxExpressions option).
+ ExprCnt uint64
+
+ // ChoiceAltCnt is used to count for each ordered choice expression,
+ // which alternative is used how may times.
+ // These numbers allow to optimize the order of the ordered choice expression
+ // to increase the performance of the parser
+ //
+ // The outer key of ChoiceAltCnt is composed of the name of the rule as well
+ // as the line and the column of the ordered choice.
+ // The inner key of ChoiceAltCnt is the number (one-based) of the matching alternative.
+ // For each alternative the number of matches are counted. If an ordered choice does not
+ // match, a special counter is incremented. The name of this counter is set with
+ // the parser option Statistics.
+ // For an alternative to be included in ChoiceAltCnt, it has to match at least once.
+ ChoiceAltCnt map[string]map[string]int
+}
+
+type parser struct {
+ filename string
+ pt savepoint
+ cur current
+
+ data []byte
+ errs *errList
+
+ depth int
+ recover bool
+ debug bool
+
+ memoize bool
+ // memoization table for the packrat algorithm:
+ // map[offset in source] map[expression or rule] {value, match}
+ memo map[int]map[any]resultTuple
+
+ // rules table, maps the rule identifier to the rule node
+ rules map[string]*rule
+ // variables stack, map of label to value
+ vstack []map[string]any
+ // rule stack, allows identification of the current rule in errors
+ rstack []*rule
+
+ // parse fail
+ maxFailPos position
+ maxFailExpected []string
+ maxFailInvertExpected bool
+
+ // max number of expressions to be parsed
+ maxExprCnt uint64
+ // entrypoint for the parser
+ entrypoint string
+
+ allowInvalidUTF8 bool
+
+ *Stats
+
+ choiceNoMatch string
+ // recovery expression stack, keeps track of the currently available recovery expression, these are traversed in reverse
+ recoveryStack []map[string]any
+}
+
+// push a variable set on the vstack.
+func (p *parser) pushV() {
+ if cap(p.vstack) == len(p.vstack) {
+ // create new empty slot in the stack
+ p.vstack = append(p.vstack, nil)
+ } else {
+ // slice to 1 more
+ p.vstack = p.vstack[:len(p.vstack)+1]
+ }
+
+ // get the last args set
+ m := p.vstack[len(p.vstack)-1]
+ if m != nil && len(m) == 0 {
+ // empty map, all good
+ return
+ }
+
+ m = make(map[string]any)
+ p.vstack[len(p.vstack)-1] = m
+}
+
+// pop a variable set from the vstack.
+func (p *parser) popV() {
+ // if the map is not empty, clear it
+ m := p.vstack[len(p.vstack)-1]
+ if len(m) > 0 {
+ // GC that map
+ p.vstack[len(p.vstack)-1] = nil
+ }
+ p.vstack = p.vstack[:len(p.vstack)-1]
+}
+
+// push a recovery expression with its labels to the recoveryStack
+func (p *parser) pushRecovery(labels []string, expr any) {
+ if cap(p.recoveryStack) == len(p.recoveryStack) {
+ // create new empty slot in the stack
+ p.recoveryStack = append(p.recoveryStack, nil)
+ } else {
+ // slice to 1 more
+ p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)+1]
+ }
+
+ m := make(map[string]any, len(labels))
+ for _, fl := range labels {
+ m[fl] = expr
+ }
+ p.recoveryStack[len(p.recoveryStack)-1] = m
+}
+
+// pop a recovery expression from the recoveryStack
+func (p *parser) popRecovery() {
+ // GC that map
+ p.recoveryStack[len(p.recoveryStack)-1] = nil
+
+ p.recoveryStack = p.recoveryStack[:len(p.recoveryStack)-1]
+}
+
+func (p *parser) print(prefix, s string) string {
+ if !p.debug {
+ return s
+ }
+
+ fmt.Printf("%s %d:%d:%d: %s [%#U]\n",
+ prefix, p.pt.line, p.pt.col, p.pt.offset, s, p.pt.rn)
+ return s
+}
+
+func (p *parser) printIndent(mark string, s string) string {
+ return p.print(strings.Repeat(" ", p.depth)+mark, s)
+}
+
+func (p *parser) in(s string) string {
+ res := p.printIndent(">", s)
+ p.depth++
+ return res
+}
+
+func (p *parser) out(s string) string {
+ p.depth--
+ return p.printIndent("<", s)
+}
+
+func (p *parser) addErr(err error) {
+ p.addErrAt(err, p.pt.position, []string{})
+}
+
+func (p *parser) addErrAt(err error, pos position, expected []string) {
+ var buf bytes.Buffer
+ if p.filename != "" {
+ buf.WriteString(p.filename)
+ }
+ if buf.Len() > 0 {
+ buf.WriteString(":")
+ }
+ buf.WriteString(fmt.Sprintf("%d:%d (%d)", pos.line, pos.col, pos.offset))
+ if len(p.rstack) > 0 {
+ if buf.Len() > 0 {
+ buf.WriteString(": ")
+ }
+ rule := p.rstack[len(p.rstack)-1]
+ if rule.displayName != "" {
+ buf.WriteString("rule " + rule.displayName)
+ } else {
+ buf.WriteString("rule " + rule.name)
+ }
+ }
+ pe := &parserError{Inner: err, pos: pos, prefix: buf.String(), expected: expected}
+ p.errs.add(pe)
+}
+
+func (p *parser) failAt(fail bool, pos position, want string) {
+ // process fail if parsing fails and not inverted or parsing succeeds and invert is set
+ if fail == p.maxFailInvertExpected {
+ if pos.offset < p.maxFailPos.offset {
+ return
+ }
+
+ if pos.offset > p.maxFailPos.offset {
+ p.maxFailPos = pos
+ p.maxFailExpected = p.maxFailExpected[:0]
+ }
+
+ if p.maxFailInvertExpected {
+ want = "!" + want
+ }
+ p.maxFailExpected = append(p.maxFailExpected, want)
+ }
+}
+
+// read advances the parser to the next rune.
+func (p *parser) read() {
+ p.pt.offset += p.pt.w
+ rn, n := utf8.DecodeRune(p.data[p.pt.offset:])
+ p.pt.rn = rn
+ p.pt.w = n
+ p.pt.col++
+ if rn == '\n' {
+ p.pt.line++
+ p.pt.col = 0
+ }
+
+ if rn == utf8.RuneError && n == 1 { // see utf8.DecodeRune
+ if !p.allowInvalidUTF8 {
+ p.addErr(errInvalidEncoding)
+ }
+ }
+}
+
+// restore parser position to the savepoint pt.
+func (p *parser) restore(pt savepoint) {
+ if p.debug {
+ defer p.out(p.in("restore"))
+ }
+ if pt.offset == p.pt.offset {
+ return
+ }
+ p.pt = pt
+}
+
+// Cloner is implemented by any value that has a Clone method, which returns a
+// copy of the value. This is mainly used for types which are not passed by
+// value (e.g map, slice, chan) or structs that contain such types.
+//
+// This is used in conjunction with the global state feature to create proper
+// copies of the state to allow the parser to properly restore the state in
+// the case of backtracking.
+type Cloner interface {
+ Clone() any
+}
+
+var statePool = &sync.Pool{
+ New: func() any { return make(storeDict) },
+}
+
+func (sd storeDict) Discard() {
+ for k := range sd {
+ delete(sd, k)
+ }
+ statePool.Put(sd)
+}
+
+// clone and return parser current state.
+func (p *parser) cloneState() storeDict {
+ if p.debug {
+ defer p.out(p.in("cloneState"))
+ }
+
+ state := statePool.Get().(storeDict)
+ for k, v := range p.cur.state {
+ if c, ok := v.(Cloner); ok {
+ state[k] = c.Clone()
+ } else {
+ state[k] = v
+ }
+ }
+ return state
+}
+
+// restore parser current state to the state storeDict.
+// every restoreState should applied only one time for every cloned state
+func (p *parser) restoreState(state storeDict) {
+ if p.debug {
+ defer p.out(p.in("restoreState"))
+ }
+ p.cur.state.Discard()
+ p.cur.state = state
+}
+
+// get the slice of bytes from the savepoint start to the current position.
+func (p *parser) sliceFrom(start savepoint) []byte {
+ return p.data[start.position.offset:p.pt.position.offset]
+}
+
+func (p *parser) getMemoized(node any) (resultTuple, bool) {
+ if len(p.memo) == 0 {
+ return resultTuple{}, false
+ }
+ m := p.memo[p.pt.offset]
+ if len(m) == 0 {
+ return resultTuple{}, false
+ }
+ res, ok := m[node]
+ return res, ok
+}
+
+func (p *parser) setMemoized(pt savepoint, node any, tuple resultTuple) {
+ if p.memo == nil {
+ p.memo = make(map[int]map[any]resultTuple)
+ }
+ m := p.memo[pt.offset]
+ if m == nil {
+ m = make(map[any]resultTuple)
+ p.memo[pt.offset] = m
+ }
+ m[node] = tuple
+}
+
+func (p *parser) buildRulesTable(g *grammar) {
+ p.rules = make(map[string]*rule, len(g.rules))
+ for _, r := range g.rules {
+ p.rules[r.name] = r
+ }
+}
+
+func (p *parser) parse(g *grammar) (val any, err error) {
+ if len(g.rules) == 0 {
+ p.addErr(errNoRule)
+ return nil, p.errs.err()
+ }
+
+ // TODO : not super critical but this could be generated
+ p.buildRulesTable(g)
+
+ if p.recover {
+ // panic can be used in action code to stop parsing immediately
+ // and return the panic as an error.
+ defer func() {
+ if e := recover(); e != nil {
+ if p.debug {
+ defer p.out(p.in("panic handler"))
+ }
+ val = nil
+ switch e := e.(type) {
+ case error:
+ p.addErr(e)
+ default:
+ p.addErr(fmt.Errorf("%v", e))
+ }
+ err = p.errs.err()
+ }
+ }()
+ }
+
+ startRule, ok := p.rules[p.entrypoint]
+ if !ok {
+ p.addErr(errInvalidEntrypoint)
+ return nil, p.errs.err()
+ }
+
+ p.read() // advance to first rune
+ val, ok = p.parseRuleWrap(startRule)
+ if !ok {
+ if len(*p.errs) == 0 {
+ // If parsing fails, but no errors have been recorded, the expected values
+ // for the farthest parser position are returned as error.
+ maxFailExpectedMap := make(map[string]struct{}, len(p.maxFailExpected))
+ for _, v := range p.maxFailExpected {
+ maxFailExpectedMap[v] = struct{}{}
+ }
+ expected := make([]string, 0, len(maxFailExpectedMap))
+ eof := false
+ if _, ok := maxFailExpectedMap["!."]; ok {
+ delete(maxFailExpectedMap, "!.")
+ eof = true
+ }
+ for k := range maxFailExpectedMap {
+ expected = append(expected, k)
+ }
+ sort.Strings(expected)
+ if eof {
+ expected = append(expected, "EOF")
+ }
+ p.addErrAt(errors.New("no match found, expected: "+listJoin(expected, ", ", "or")), p.maxFailPos, expected)
+ }
+
+ return nil, p.errs.err()
+ }
+ return val, p.errs.err()
+}
+
+func listJoin(list []string, sep string, lastSep string) string {
+ switch len(list) {
+ case 0:
+ return ""
+ case 1:
+ return list[0]
+ default:
+ return strings.Join(list[:len(list)-1], sep) + " " + lastSep + " " + list[len(list)-1]
+ }
+}
+
+func (p *parser) parseRuleMemoize(rule *rule) (any, bool) {
+ res, ok := p.getMemoized(rule)
+ if ok {
+ p.restore(res.end)
+ return res.v, res.b
+ }
+
+ startMark := p.pt
+ val, ok := p.parseRule(rule)
+ p.setMemoized(startMark, rule, resultTuple{val, ok, p.pt})
+
+ return val, ok
+}
+
+func (p *parser) parseRuleWrap(rule *rule) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseRule " + rule.name))
+ }
+ var (
+ val any
+ ok bool
+ startMark = p.pt
+ )
+
+ if p.memoize {
+ val, ok = p.parseRuleMemoize(rule)
+ } else {
+ val, ok = p.parseRule(rule)
+ }
+
+ if ok && p.debug {
+ p.printIndent("MATCH", string(p.sliceFrom(startMark)))
+ }
+ return val, ok
+}
+
+func (p *parser) parseRule(rule *rule) (any, bool) {
+ p.rstack = append(p.rstack, rule)
+ p.pushV()
+ val, ok := p.parseExprWrap(rule.expr)
+ p.popV()
+ p.rstack = p.rstack[:len(p.rstack)-1]
+ return val, ok
+}
+
+func (p *parser) parseExprWrap(expr any) (any, bool) {
+ var pt savepoint
+
+ if p.memoize {
+ res, ok := p.getMemoized(expr)
+ if ok {
+ p.restore(res.end)
+ return res.v, res.b
+ }
+ pt = p.pt
+ }
+
+ val, ok := p.parseExpr(expr)
+
+ if p.memoize {
+ p.setMemoized(pt, expr, resultTuple{val, ok, p.pt})
+ }
+ return val, ok
+}
+
+func (p *parser) parseExpr(expr any) (any, bool) {
+ p.ExprCnt++
+ if p.ExprCnt > p.maxExprCnt {
+ panic(errMaxExprCnt)
+ }
+
+ var val any
+ var ok bool
+ switch expr := expr.(type) {
+ case *actionExpr:
+ val, ok = p.parseActionExpr(expr)
+ case *andCodeExpr:
+ val, ok = p.parseAndCodeExpr(expr)
+ case *andExpr:
+ val, ok = p.parseAndExpr(expr)
+ case *anyMatcher:
+ val, ok = p.parseAnyMatcher(expr)
+ case *charClassMatcher:
+ val, ok = p.parseCharClassMatcher(expr)
+ case *choiceExpr:
+ val, ok = p.parseChoiceExpr(expr)
+ case *labeledExpr:
+ val, ok = p.parseLabeledExpr(expr)
+ case *litMatcher:
+ val, ok = p.parseLitMatcher(expr)
+ case *notCodeExpr:
+ val, ok = p.parseNotCodeExpr(expr)
+ case *notExpr:
+ val, ok = p.parseNotExpr(expr)
+ case *oneOrMoreExpr:
+ val, ok = p.parseOneOrMoreExpr(expr)
+ case *recoveryExpr:
+ val, ok = p.parseRecoveryExpr(expr)
+ case *ruleRefExpr:
+ val, ok = p.parseRuleRefExpr(expr)
+ case *seqExpr:
+ val, ok = p.parseSeqExpr(expr)
+ case *stateCodeExpr:
+ val, ok = p.parseStateCodeExpr(expr)
+ case *throwExpr:
+ val, ok = p.parseThrowExpr(expr)
+ case *zeroOrMoreExpr:
+ val, ok = p.parseZeroOrMoreExpr(expr)
+ case *zeroOrOneExpr:
+ val, ok = p.parseZeroOrOneExpr(expr)
+ default:
+ panic(fmt.Sprintf("unknown expression type %T", expr))
+ }
+ return val, ok
+}
+
+func (p *parser) parseActionExpr(act *actionExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseActionExpr"))
+ }
+
+ start := p.pt
+ val, ok := p.parseExprWrap(act.expr)
+ if ok {
+ p.cur.pos = start.position
+ p.cur.text = p.sliceFrom(start)
+ state := p.cloneState()
+ actVal, err := act.run(p)
+ if err != nil {
+ p.addErrAt(err, start.position, []string{})
+ }
+ p.restoreState(state)
+
+ val = actVal
+ }
+ if ok && p.debug {
+ p.printIndent("MATCH", string(p.sliceFrom(start)))
+ }
+ return val, ok
+}
+
+func (p *parser) parseAndCodeExpr(and *andCodeExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseAndCodeExpr"))
+ }
+
+ state := p.cloneState()
+
+ ok, err := and.run(p)
+ if err != nil {
+ p.addErr(err)
+ }
+ p.restoreState(state)
+
+ return nil, ok
+}
+
+func (p *parser) parseAndExpr(and *andExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseAndExpr"))
+ }
+
+ pt := p.pt
+ state := p.cloneState()
+ p.pushV()
+ _, ok := p.parseExprWrap(and.expr)
+ p.popV()
+ p.restoreState(state)
+ p.restore(pt)
+
+ return nil, ok
+}
+
+func (p *parser) parseAnyMatcher(any *anyMatcher) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseAnyMatcher"))
+ }
+
+ if p.pt.rn == utf8.RuneError && p.pt.w == 0 {
+ // EOF - see utf8.DecodeRune
+ p.failAt(false, p.pt.position, ".")
+ return nil, false
+ }
+ start := p.pt
+ p.read()
+ p.failAt(true, start.position, ".")
+ return p.sliceFrom(start), true
+}
+
+func (p *parser) parseCharClassMatcher(chr *charClassMatcher) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseCharClassMatcher"))
+ }
+
+ cur := p.pt.rn
+ start := p.pt
+
+ // can't match EOF
+ if cur == utf8.RuneError && p.pt.w == 0 { // see utf8.DecodeRune
+ p.failAt(false, start.position, chr.val)
+ return nil, false
+ }
+
+ if chr.ignoreCase {
+ cur = unicode.ToLower(cur)
+ }
+
+ // try to match in the list of available chars
+ for _, rn := range chr.chars {
+ if rn == cur {
+ if chr.inverted {
+ p.failAt(false, start.position, chr.val)
+ return nil, false
+ }
+ p.read()
+ p.failAt(true, start.position, chr.val)
+ return p.sliceFrom(start), true
+ }
+ }
+
+ // try to match in the list of ranges
+ for i := 0; i < len(chr.ranges); i += 2 {
+ if cur >= chr.ranges[i] && cur <= chr.ranges[i+1] {
+ if chr.inverted {
+ p.failAt(false, start.position, chr.val)
+ return nil, false
+ }
+ p.read()
+ p.failAt(true, start.position, chr.val)
+ return p.sliceFrom(start), true
+ }
+ }
+
+ // try to match in the list of Unicode classes
+ for _, cl := range chr.classes {
+ if unicode.Is(cl, cur) {
+ if chr.inverted {
+ p.failAt(false, start.position, chr.val)
+ return nil, false
+ }
+ p.read()
+ p.failAt(true, start.position, chr.val)
+ return p.sliceFrom(start), true
+ }
+ }
+
+ if chr.inverted {
+ p.read()
+ p.failAt(true, start.position, chr.val)
+ return p.sliceFrom(start), true
+ }
+ p.failAt(false, start.position, chr.val)
+ return nil, false
+}
+
+func (p *parser) incChoiceAltCnt(ch *choiceExpr, altI int) {
+ choiceIdent := fmt.Sprintf("%s %d:%d", p.rstack[len(p.rstack)-1].name, ch.pos.line, ch.pos.col)
+ m := p.ChoiceAltCnt[choiceIdent]
+ if m == nil {
+ m = make(map[string]int)
+ p.ChoiceAltCnt[choiceIdent] = m
+ }
+ // We increment altI by 1, so the keys do not start at 0
+ alt := strconv.Itoa(altI + 1)
+ if altI == choiceNoMatch {
+ alt = p.choiceNoMatch
+ }
+ m[alt]++
+}
+
+func (p *parser) parseChoiceExpr(ch *choiceExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseChoiceExpr"))
+ }
+
+ for altI, alt := range ch.alternatives {
+ // dummy assignment to prevent compile error if optimized
+ _ = altI
+
+ state := p.cloneState()
+
+ p.pushV()
+ val, ok := p.parseExprWrap(alt)
+ p.popV()
+ if ok {
+ p.incChoiceAltCnt(ch, altI)
+ return val, ok
+ }
+ p.restoreState(state)
+ }
+ p.incChoiceAltCnt(ch, choiceNoMatch)
+ return nil, false
+}
+
+func (p *parser) parseLabeledExpr(lab *labeledExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseLabeledExpr"))
+ }
+
+ p.pushV()
+ val, ok := p.parseExprWrap(lab.expr)
+ p.popV()
+ if ok && lab.label != "" {
+ m := p.vstack[len(p.vstack)-1]
+ m[lab.label] = val
+ }
+ return val, ok
+}
+
+func (p *parser) parseLitMatcher(lit *litMatcher) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseLitMatcher"))
+ }
+
+ start := p.pt
+ for _, want := range lit.val {
+ cur := p.pt.rn
+ if lit.ignoreCase {
+ cur = unicode.ToLower(cur)
+ }
+ if cur != want {
+ p.failAt(false, start.position, lit.want)
+ p.restore(start)
+ return nil, false
+ }
+ p.read()
+ }
+ p.failAt(true, start.position, lit.want)
+ return p.sliceFrom(start), true
+}
+
+func (p *parser) parseNotCodeExpr(not *notCodeExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseNotCodeExpr"))
+ }
+
+ state := p.cloneState()
+
+ ok, err := not.run(p)
+ if err != nil {
+ p.addErr(err)
+ }
+ p.restoreState(state)
+
+ return nil, !ok
+}
+
+func (p *parser) parseNotExpr(not *notExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseNotExpr"))
+ }
+
+ pt := p.pt
+ state := p.cloneState()
+ p.pushV()
+ p.maxFailInvertExpected = !p.maxFailInvertExpected
+ _, ok := p.parseExprWrap(not.expr)
+ p.maxFailInvertExpected = !p.maxFailInvertExpected
+ p.popV()
+ p.restoreState(state)
+ p.restore(pt)
+
+ return nil, !ok
+}
+
+func (p *parser) parseOneOrMoreExpr(expr *oneOrMoreExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseOneOrMoreExpr"))
+ }
+
+ var vals []any
+
+ for {
+ p.pushV()
+ val, ok := p.parseExprWrap(expr.expr)
+ p.popV()
+ if !ok {
+ if len(vals) == 0 {
+ // did not match once, no match
+ return nil, false
+ }
+ return vals, true
+ }
+ vals = append(vals, val)
+ }
+}
+
+func (p *parser) parseRecoveryExpr(recover *recoveryExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseRecoveryExpr (" + strings.Join(recover.failureLabel, ",") + ")"))
+ }
+
+ p.pushRecovery(recover.failureLabel, recover.recoverExpr)
+ val, ok := p.parseExprWrap(recover.expr)
+ p.popRecovery()
+
+ return val, ok
+}
+
+func (p *parser) parseRuleRefExpr(ref *ruleRefExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseRuleRefExpr " + ref.name))
+ }
+
+ if ref.name == "" {
+ panic(fmt.Sprintf("%s: invalid rule: missing name", ref.pos))
+ }
+
+ rule := p.rules[ref.name]
+ if rule == nil {
+ p.addErr(fmt.Errorf("undefined rule: %s", ref.name))
+ return nil, false
+ }
+ return p.parseRuleWrap(rule)
+}
+
+func (p *parser) parseSeqExpr(seq *seqExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseSeqExpr"))
+ }
+
+ vals := make([]any, 0, len(seq.exprs))
+
+ pt := p.pt
+ state := p.cloneState()
+ for _, expr := range seq.exprs {
+ val, ok := p.parseExprWrap(expr)
+ if !ok {
+ p.restoreState(state)
+ p.restore(pt)
+ return nil, false
+ }
+ vals = append(vals, val)
+ }
+ return vals, true
+}
+
+func (p *parser) parseStateCodeExpr(state *stateCodeExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseStateCodeExpr"))
+ }
+
+ err := state.run(p)
+ if err != nil {
+ p.addErr(err)
+ }
+ return nil, true
+}
+
+func (p *parser) parseThrowExpr(expr *throwExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseThrowExpr"))
+ }
+
+ for i := len(p.recoveryStack) - 1; i >= 0; i-- {
+ if recoverExpr, ok := p.recoveryStack[i][expr.label]; ok {
+ if val, ok := p.parseExprWrap(recoverExpr); ok {
+ return val, ok
+ }
+ }
+ }
+
+ return nil, false
+}
+
+func (p *parser) parseZeroOrMoreExpr(expr *zeroOrMoreExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseZeroOrMoreExpr"))
+ }
+
+ var vals []any
+
+ for {
+ p.pushV()
+ val, ok := p.parseExprWrap(expr.expr)
+ p.popV()
+ if !ok {
+ return vals, true
+ }
+ vals = append(vals, val)
+ }
+}
+
+func (p *parser) parseZeroOrOneExpr(expr *zeroOrOneExpr) (any, bool) {
+ if p.debug {
+ defer p.out(p.in("parseZeroOrOneExpr"))
+ }
+
+ p.pushV()
+ val, _ := p.parseExprWrap(expr.expr)
+ p.popV()
+ // whether it matched or not, consider it a match
+ return val, true
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go
new file mode 100644
index 0000000000..1d5b2b6b8c
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/durationparser/types.go
@@ -0,0 +1,13 @@
+package durationparser
+
+// Result holds the parsed components of a duration string.
+type Result struct {
+ Sign string // "" or "-" or "+"
+ Segments []Segment
+}
+
+// Segment holds a single parsed segment (e.g. Digits="1.5", Unit="d").
+type Segment struct {
+ Digits string
+ Unit string
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go
index 6f93ba530e..87b94d1646 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/eval.go
@@ -92,8 +92,9 @@ type eval struct {
input *ast.Term
data *ast.Term
external *resolverTrie
+ externalTreeStack *externalTreeStack
targetStack *refStack
- traceLastLocation *ast.Location // Last location of a trace event.
+ traceLastLocation *ast.Location
instr *Instrumentation
builtins map[string]*Builtin
builtinCache builtins.Cache
@@ -108,6 +109,7 @@ type eval struct {
runtime *ast.Term
builtinErrors *builtinErrors
roundTripper CustomizeRoundTripper
+ evaluated *EvaluatedRuleTracker
genvarprefix string
query ast.Body
tracers []QueryTracer
@@ -124,6 +126,8 @@ type eval struct {
findOne bool
strictObjects bool
defined bool
+ requestMetadata map[string]any
+ responseMetadata map[string]any
}
type (
@@ -231,7 +235,6 @@ func (e *eval) closure(query ast.Body, cpy *eval) {
}
// childWithBindingSizeHint creates a child evaluator with bindings pre-sized for the expected number of variables.
-// This reduces memory waste when evaluating functions or rules with known argument counts.
func (e *eval) childWithBindingSizeHint(query ast.Body, cpy *eval, sizeHint int) {
*cpy = *e
cpy.index = 0
@@ -265,12 +268,15 @@ func (e *eval) unknown(x any, b *bindings) bool {
x = ast.NewTerm(v)
}
- return saveRequired(e.compiler, e.inliningControl, true, e.saveSet, b, x, false)
+ return saveRequired(e.compiler.RuleTree, e.externalTreeStack, e.inliningControl, true, e.saveSet, b, x, false)
}
// exactly like `unknown` above` but without the cost of `any` boxing when arg is known to be a ref
func (e *eval) unknownRef(ref ast.Ref, b *bindings) bool {
- return e.partial() && saveRequired(e.compiler, e.inliningControl, true, e.saveSet, b, ast.NewTerm(ref), false)
+ if !e.partial() {
+ return false
+ }
+ return saveRequired(e.compiler.RuleTree, e.externalTreeStack, e.inliningControl, true, e.saveSet, b, ast.NewTerm(ref), false)
}
func (e *eval) traceEnter(x ast.Node) {
@@ -517,8 +523,8 @@ func (e *eval) evalStep(iter evalIterator) error {
}
case *ast.Every:
eval := evalEvery{
- Every: terms,
e: e,
+ every: terms,
expr: expr,
}
err = eval.eval(func() error {
@@ -528,6 +534,19 @@ func (e *eval) evalStep(iter evalIterator) error {
return err
})
+ case *ast.Not:
+ en := evalNot{
+ e: e,
+ not: terms,
+ expr: expr,
+ }
+ err = en.eval(func(e *eval) error {
+ defined = true
+ err := iter(e)
+ e.traceRedo(expr)
+ return err
+ })
+
default: // guard-rail for adding extra (Expr).Terms types
return fmt.Errorf("got %T terms: %[1]v", terms)
}
@@ -571,14 +590,24 @@ func (e *eval) evalStep(iter evalIterator) error {
})
case *ast.Every:
eval := evalEvery{
- Every: terms,
e: e,
+ every: terms,
expr: expr,
}
err = eval.eval(func() error {
return iter(e)
})
+ case *ast.Not:
+ en := evalNot{
+ e: e,
+ not: terms,
+ expr: expr,
+ }
+ err = en.eval(func(e *eval) error {
+ return iter(e)
+ })
+
default: // guard-rail for adding extra (Expr).Terms types
return fmt.Errorf("got %T terms: %[1]v", terms)
}
@@ -604,7 +633,7 @@ func (e *eval) evalNot(iter evalIterator) error {
expr := e.query[e.index]
if e.unknown(expr, e.bindings) {
- return e.evalNotPartial(iter)
+ return e.setupAndEvalNotPartial(iter)
}
negation := ast.NewBody(expr.ComplementNoWith())
@@ -730,26 +759,33 @@ func (e *eval) evalWith(iter evalIterator) error {
}
}
- oldInput, oldData := e.evalWithPush(input, data, functionMocks, targets, disable)
+ oldInput, oldData, pushedFrame := e.evalWithPush(input, data, functionMocks, targets, disable)
err = e.evalStep(func(e *eval) error {
- e.evalWithPop(oldInput, oldData)
+ e.evalWithPop(oldInput, oldData, pushedFrame)
err := e.next(iter)
- oldInput, oldData = e.evalWithPush(input, data, functionMocks, targets, disable)
+ oldInput, oldData, pushedFrame = e.evalWithPush(input, data, functionMocks, targets, disable)
return err
})
- e.evalWithPop(oldInput, oldData)
+ e.evalWithPop(oldInput, oldData, pushedFrame)
return err
}
-func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term, targets, disable []ast.Ref) (*ast.Term, *ast.Term) {
+func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term, targets, disable []ast.Ref) (*ast.Term, *ast.Term, bool) {
var oldInput *ast.Term
+ var pushedFrame bool
if input != nil {
oldInput = e.input
e.input = input
+
+ // When input changes, push a new frame for external tree caching
+ if e.externalTreeStack != nil {
+ e.externalTreeStack.PushFrame()
+ pushedFrame = true
+ }
}
var oldData *ast.Term
@@ -779,29 +815,62 @@ func (e *eval) evalWithPush(input, data *ast.Term, functionMocks [][2]*ast.Term,
e.functionMocks.PutPairs(functionMocks)
- return oldInput, oldData
+ return oldInput, oldData, pushedFrame
}
-func (e *eval) evalWithPop(input, data *ast.Term) {
+func (e *eval) evalWithPop(input, data *ast.Term, popFrame bool) {
// NOTE(ae) no nil checks here as we assume evalWithPush always called first
e.inliningControl.PopDisable()
e.targetStack.Pop()
e.virtualCache.Pop()
e.comprehensionCache.Pop()
e.functionMocks.PopPairs()
+
+ // When input is restored, pop the external tree frame
+ if popFrame {
+ e.externalTreeStack.PopFrame()
+ }
+
e.data = data
e.input = input
}
-func (e *eval) evalNotPartial(iter evalIterator) error {
+func (e *eval) setupAndEvalNotPartial(iter evalIterator) error {
// Prepare query normally.
expr := e.query[e.index]
- negation := expr.ComplementNoWith()
+ unNegate := func(expr *ast.Expr) ast.Body {
+ return ast.NewBody(expr.ComplementNoWith())
+ }
+
+ complement := func(expr *ast.Expr) []*ast.Expr {
+ if expr.IsNot() {
+ return ast.Complement(expr)
+ }
+ return []*ast.Expr{expr.Complement()}
+ }
+
+ supportTerms := func(terms any) any {
+ return terms
+ }
+
+ return e.evalNotPartial(expr, unNegate, complement, supportTerms, iter)
+}
+
+// unNegateFn returns the "unwrapped" non-negated expressions (1..*) of a negated expressions
+type unNegateFn func(expr *ast.Expr) ast.Body
+
+// complementFn returns the set of expressions (1..*) that is the complement of an expression
+type complementFn func(*ast.Expr) []*ast.Expr
+
+// supportTermsFn transforms the given terms to a support reference/call to the form required by the negated expression's ast.Expr.Terms field.
+type supportTermsFn func(terms any) any
+
+func (e *eval) evalNotPartial(expr *ast.Expr, unNegateFn unNegateFn, complementFn complementFn, supportTermsFn supportTermsFn, iter evalIterator) error {
child := evalPool.Get()
defer evalPool.Put(child)
- e.closure(ast.NewBody(negation), child)
+ e.closure(unNegateFn(expr), child)
// Unknowns is the set of variables that are marked as unknown. The variables
// are namespaced with the query ID that they originate in. This ensures that
@@ -853,7 +922,7 @@ func (e *eval) evalNotPartial(iter evalIterator) error {
// the unknowns as safe because vars in the save set will either be known to
// the caller or made safe by an expression on the save stack.
if !canInlineNegation(unknowns, savedQueries) {
- return e.evalNotPartialSupport(child.queryID, expr, unknowns, savedQueries, iter)
+ return e.evalNotPartialSupport(child.queryID, expr, supportTermsFn, unknowns, savedQueries, iter)
}
// If we can inline the result, we have to generate the cross product of the
@@ -864,14 +933,14 @@ func (e *eval) evalNotPartial(iter evalIterator) error {
// Becomes:
//
// (!A && !C) || (!A && !D) || (!B && !C) || (!B && !D)
- return complementedCartesianProduct(savedQueries, 0, nil, func(q ast.Body) error {
+ return complementedCartesianProduct(savedQueries, 0, nil, complementFn, func(q ast.Body) error {
return e.saveInlinedNegatedExprs(q, func() error {
return iter(e)
})
})
}
-func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, unknowns ast.VarSet, queries []ast.Body, iter evalIterator) error {
+func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, supportTermsFn supportTermsFn, unknowns ast.VarSet, queries []ast.Body, iter evalIterator) error {
// Prepare support rule head.
supportName := fmt.Sprintf("__not%d_%d_%d__", e.queryID, e.index, negationID)
@@ -915,9 +984,9 @@ func (e *eval) evalNotPartialSupport(negationID uint64, expr *ast.Expr, unknowns
terms := make([]*ast.Term, len(args)+1)
terms[0] = term
copy(terms[1:], args)
- cpy.Terms = terms
+ cpy.Terms = supportTermsFn(terms)
} else {
- cpy.Terms = term
+ cpy.Terms = supportTermsFn(term)
}
return e.saveInlinedNegatedExprs([]*ast.Expr{cpy}, func() error {
@@ -955,15 +1024,19 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
var ir *ast.IndexResult
var err error
+ index := e.ruleIndex(ref)
if e.partial() {
- ir, err = e.getRules(ref, nil)
+ ir, err = e.getRules(ref, nil, index)
} else {
- ir, err = e.getRules(ref, terms[1:])
+ ir, err = e.getRules(ref, terms[1:], index)
}
defer ast.IndexResultPool.Put(ir)
if err != nil {
return err
}
+ if ir == nil {
+ return nil
+ }
eval := evalFuncPool.Get()
defer evalFuncPool.Put(eval)
@@ -1027,6 +1100,8 @@ func (e *eval) evalCall(terms []*ast.Term, iter unifyIterator) error {
DistributedTracingOpts: e.tracingOpts,
Capabilities: capabilities,
RoundTripper: e.roundTripper,
+ RequestMetadata: e.requestMetadata,
+ ResponseMetadata: e.responseMetadata,
}
}
@@ -1697,11 +1772,10 @@ func (e *eval) saveInlinedNegatedExprs(exprs []*ast.Expr, iter unifyIterator) er
return err
}
-func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error) {
+func (e *eval) getRules(ref ast.Ref, args []*ast.Term, index ast.RuleIndex) (*ast.IndexResult, error) {
e.instr.startTimer(evalOpRuleIndex)
defer e.instr.stopTimer(evalOpRuleIndex)
- index := e.ruleIndex(ref)
if index == nil {
return nil, nil
}
@@ -1715,6 +1789,7 @@ func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error)
var result *ast.IndexResult
var err error
+
resolver.e = e
if e.indexing {
resolver.args = args
@@ -1745,18 +1820,13 @@ func (e *eval) getRules(ref ast.Ref, args []*ast.Term) (*ast.IndexResult, error)
// Copy ref here as ref otherwise always escapes to the heap,
// whether tracing is enabled or not.
- r := ref.Copy()
+ r := ref.CopyNonGround()
e.traceIndex(e.query[e.index], msg.String(), &r)
}
return result, err
}
-// ruleIndex performs a lookup for a RuleIndex in the compiler's RuleTree.
-func (e *eval) ruleIndex(ref ast.Ref) ast.RuleIndex {
- return e.compiler.RuleIndex(ref)
-}
-
func (e *eval) Resolve(ref ast.Ref) (ast.Value, error) {
return (&evalResolver{e: e}).Resolve(ref)
}
@@ -1969,7 +2039,7 @@ func (e *eval) getDeclArgsLen(x *ast.Expr) (int, error) {
return bi.Decl.Arity(), nil
}
- ir, err := e.getRules(operator, nil)
+ ir, err := e.getRules(operator, nil, e.ruleIndex(operator))
defer ast.IndexResultPool.Put(ir)
if err != nil {
return -1, err
@@ -2303,6 +2373,7 @@ func (e *evalFunc) evalOneRule(iter unifyIterator, rule *ast.Rule, args []*ast.T
err := child.biunifyTerms(e.terms[1:], args, e.e.bindings, child.bindings, func() error {
return child.eval(func(child *eval) error {
child.traceExit(rule)
+ e.e.evaluated.Record(rule)
// Partial evaluation must save an expression that tests the output value if the output value
// was not captured to handle the case where the output value may be `false`.
@@ -2509,10 +2580,48 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error {
cpy.plugged[e.pos] = plugged
cpy.pos++
+ // Track whether we pushed an external tree that needs cleanup
+ pushedExternalTree := false
if !e.e.targetStack.Prefixed(cpy.plugged[:cpy.pos]) {
if e.node != nil {
node = e.node.Child(plugged.Value)
- if node != nil && len(node.Values) > 0 {
+
+ // Handle external sources transparently
+ if node != nil && node.External != nil {
+ externalRef := node.External.Ref
+ externalIndex := node.External.Index
+
+ // Initialize externalTreeStack if needed
+ if e.e.externalTreeStack == nil {
+ e.e.externalTreeStack = newExternalTreeStack(e.e)
+ }
+
+ // Check cache first
+ cachedNode, _, found := e.e.externalTreeStack.findCached(externalRef)
+ if found {
+ node = cachedNode
+ } else {
+ // Call Tree() and cache the result
+ e.e.instr.startTimer(evalOpExternalRuleSource)
+ tree, updatedIndex, err := node.External.Tree(e.e.ctx, e.e.compiler.RuleTree, externalRef, e.e.input, e.e.metrics, e.e.requestMetadata, e.e.responseMetadata)
+ e.e.instr.stopTimer(evalOpExternalRuleSource)
+ if err != nil {
+ return err
+ }
+ if tree != nil {
+ if updatedIndex != nil {
+ externalIndex = updatedIndex
+ }
+ e.e.externalTreeStack.Push(externalRef, tree, externalIndex, e.e.input)
+ node = tree
+ pushedExternalTree = true
+ }
+ }
+ }
+
+ hasRules := node != nil && len(node.Values) > 0
+
+ if hasRules {
r := evalVirtual{
e: e.e,
ref: e.ref,
@@ -2523,13 +2632,21 @@ func (e evalTree) next(iter unifyIterator, plugged *ast.Term) error {
rbindings: e.rbindings,
}
r.plugged[e.pos] = plugged
- return r.eval(iter)
+ err := r.eval(iter)
+ if pushedExternalTree {
+ e.e.externalTreeStack.Pop()
+ }
+ return err
}
}
}
cpy.node = node
- return cpy.eval(iter)
+ err := cpy.eval(iter)
+ if pushedExternalTree {
+ e.e.externalTreeStack.Pop()
+ }
+ return err
}
// enumerateNext is a helper to avoid closure allocation in enumerate loops.
@@ -2705,12 +2822,16 @@ type evalVirtual struct {
func (e evalVirtual) eval(iter unifyIterator) error {
- ir, err := e.e.getRules(e.plugged[:e.pos+1], nil)
+ ir, err := e.e.getRules(e.plugged[:e.pos+1], nil, e.e.ruleIndex(e.plugged[:e.pos+1]))
defer ast.IndexResultPool.Put(ir)
if err != nil {
return err
}
+ if ir == nil {
+ return nil
+ }
+
// Partial evaluation of ordered rules is not supported currently. Save the
// expression and continue. This could be revisited in the future.
if len(ir.Else) > 0 && e.e.unknownRef(e.ref, e.bindings) {
@@ -2925,6 +3046,7 @@ func (e evalVirtualPartial) evalAllRulesNoCache(rules []*ast.Rule) (*ast.Term, e
child.traceEnter(rule)
err := child.eval(func(*eval) error {
child.traceExit(rule)
+ e.e.evaluated.Record(rule)
var err error
result, _, err = e.reduce(rule, child.bindings, result, &visitedRefs)
if err != nil {
@@ -3397,7 +3519,7 @@ func (q vcKeyScope) AppendText(buf []byte) ([]byte, error) {
// reduce removes vars from the tail of the ref.
func (q vcKeyScope) reduce() vcKeyScope {
- ref := q.Ref.Copy()
+ ref := q.Ref.CopyNonGround()
var i int
for i = len(q.Ref) - 1; i >= 0; i-- {
if _, ok := q.Ref[i].Value.(ast.Var); !ok {
@@ -3647,6 +3769,7 @@ func (e evalVirtualComplete) evalValueRule(iter unifyIterator, rule *ast.Rule, p
var result *ast.Term
err := child.eval(func(child *eval) error {
child.traceExit(rule)
+ e.e.evaluated.Record(rule)
result = child.bindings.Plug(rule.Head.Value)
@@ -3990,19 +4113,19 @@ func (e evalTerm) save(iter unifyIterator) error {
}
type evalEvery struct {
- *ast.Every
- e *eval
- expr *ast.Expr
+ e *eval
+ every *ast.Every
+ expr *ast.Expr
}
func (e evalEvery) eval(iter unifyIterator) error {
// unknowns in domain or body: save the expression, PE its body
// partial() check to avoid e.Body -> Node boxing allocation
- if e.e.partial() && (e.e.unknown(e.Domain, e.e.bindings) || e.e.unknown(e.Body, e.e.bindings)) {
+ if e.e.partial() && (e.e.unknown(e.every.Domain, e.e.bindings) || e.e.unknown(e.every.Body, e.e.bindings)) {
return e.save(iter)
}
- if pd := e.e.bindings.Plug(e.Domain); pd != nil {
+ if pd := e.e.bindings.Plug(e.every.Domain); pd != nil {
if !isIterableValue(pd.Value) {
e.e.traceFail(e.expr)
return nil
@@ -4011,9 +4134,9 @@ func (e evalEvery) eval(iter unifyIterator) error {
generator := ast.NewBody(
ast.Equality.Expr(
- ast.RefTerm(e.Domain, e.Key).SetLocation(e.Domain.Location),
- e.Value,
- ).SetLocation(e.Domain.Location),
+ ast.RefTerm(e.every.Domain, e.every.Key).SetLocation(e.every.Domain.Location),
+ e.every.Value,
+ ).SetLocation(e.every.Domain.Location),
)
domain := evalPool.Get()
@@ -4035,18 +4158,18 @@ func (e evalEvery) eval(iter unifyIterator) error {
body := evalPool.Get()
defer evalPool.Put(body)
- child.closure(e.Body, body)
+ child.closure(e.every.Body, body)
body.findOne = true
if e.e.traceEnabled {
- body.traceEnter(e.Body)
+ body.traceEnter(e.every.Body)
}
done := false
err := body.eval(func(*eval) error {
if e.e.traceEnabled {
- body.traceExit(e.Body)
- body.traceRedo(e.Body)
+ body.traceExit(e.every.Body)
+ body.traceRedo(e.every.Body)
}
done = true
@@ -4111,6 +4234,107 @@ func (e *evalEvery) plug(expr *ast.Expr) *ast.Expr {
return cpy
}
+type evalNot struct {
+ e *eval
+ not *ast.Not
+ expr *ast.Expr
+}
+
+func (e evalNot) eval(iter evalIterator) error {
+ if e.e.partial() && e.e.unknown(e.not.Body, e.e.bindings) {
+ return e.evalPartial(iter)
+ }
+
+ child := evalPool.Get()
+ defer evalPool.Put(child)
+
+ e.e.closure(e.not.Body, child)
+
+ if e.e.traceEnabled {
+ child.traceEnter(e.not.Body)
+ }
+
+ if err := child.eval(func(*eval) error {
+ if e.e.traceEnabled {
+ child.traceExit(e.not.Body)
+ child.traceRedo(e.not.Body)
+ }
+ child.defined = true
+
+ return nil
+ }); err != nil {
+ return err
+ }
+
+ if !child.defined {
+ return iter(e.e)
+ }
+
+ return nil
+}
+
+func (e evalNot) evalPartial(iter evalIterator) error {
+ if e.not.ExplicitBody && !e.e.inliningControl.shallow {
+ // we don't need to calculate a cartesian product for explicit not-bodies, as each 'not' acts as a closure for PE:d child expressions.
+
+ child := evalPool.Get()
+ defer evalPool.Put(child)
+
+ e.e.closure(e.not.Body, child)
+
+ var savedQueries []ast.Body
+ e.e.saveStack.PushQuery(nil)
+
+ _ = child.eval(func(*eval) error {
+ query := e.e.saveStack.Peek()
+ plugged := query.Plug(e.e.caller.bindings)
+
+ // Note: we could possibly apply copy propagation here, but that might fold calls into a nested structure that would require a type-checker update.
+
+ // Skip this rule body if it fails to type-check.
+ // Type-checking failure means the rule body will never succeed.
+ if !e.e.compiler.PassesTypeCheck(plugged) {
+ return nil
+ }
+
+ savedQueries = append(savedQueries, plugged)
+ return nil
+ }) // cannot return error
+
+ e.e.saveStack.PopQuery()
+
+ // If partial evaluation produced no results, the expression is always undefined
+ // so it does not have to be saved.
+ if len(savedQueries) == 0 {
+ return iter(e.e)
+ }
+
+ q := make([]*ast.Expr, 0, len(savedQueries))
+ for i := range savedQueries {
+ q = append(q, ast.NewExpr(&ast.Not{
+ Body: savedQueries[i].Copy(),
+ ExplicitBody: true,
+ }))
+ }
+
+ return e.e.saveInlinedNegatedExprs(q, func() error {
+ return iter(e.e)
+ })
+ }
+
+ expr := e.e.query[e.e.index]
+
+ unNegate := func(expr *ast.Expr) ast.Body {
+ return e.not.Body
+ }
+
+ supportTerms := func(terms any) any {
+ return ast.NewNot(ast.NewExpr(terms))
+ }
+
+ return e.e.evalNotPartial(expr, unNegate, ast.Complement, supportTerms, iter)
+}
+
func (e *eval) comprehensionIndex(term *ast.Term) *ast.ComprehensionIndex {
if e.queryCompiler != nil {
return e.queryCompiler.ComprehensionIndex(term)
@@ -4120,7 +4344,7 @@ func (e *eval) comprehensionIndex(term *ast.Term) *ast.ComprehensionIndex {
func (e *eval) namespaceRef(ref ast.Ref) ast.Ref {
if e.skipSaveNamespace {
- return ref.Copy()
+ return ref.CopyNonGround()
}
return ref.Insert(e.saveNamespace, 1)
}
@@ -4202,7 +4426,7 @@ func canInlineNegation(safe ast.VarSet, queries []ast.Body) bool {
// in the future, we can handle more cases.
return false
}
- if !expr.Negated {
+ if !expr.IsNegated() {
// Positive expressions containing variables cannot be trivially negated
// because they become unsafe (e.g., "x = 1" negated is "not x = 1" making x
// unsafe.) We check if the vars in the expr are already safe.
@@ -4264,6 +4488,15 @@ func containsNestedRefOrCall(vis *nestedCheckVisitor, expr *ast.Expr) bool {
return false
}
+ if n, ok := expr.Terms.(*ast.Not); ok {
+ for _, nExpr := range n.Body {
+ if containsNestedRefOrCall(vis, nExpr) {
+ return true
+ }
+ }
+ return false
+ }
+
return containsNestedRefOrCallInTerm(vis, expr.Terms.(*ast.Term))
}
@@ -4286,16 +4519,17 @@ func containsNestedRefOrCallInTerm(vis *nestedCheckVisitor, term *ast.Term) bool
}
}
-func complementedCartesianProduct(queries []ast.Body, idx int, curr ast.Body, iter func(ast.Body) error) error {
+func complementedCartesianProduct(queries []ast.Body, idx int, curr ast.Body, complement func(expr *ast.Expr) []*ast.Expr, iter func(ast.Body) error) error {
if idx == len(queries) {
return iter(curr)
}
for _, expr := range queries[idx] {
- curr = append(curr, expr.Complement())
- if err := complementedCartesianProduct(queries, idx+1, curr, iter); err != nil {
+ mark := len(curr)
+ curr = append(curr, complement(expr)...)
+ if err := complementedCartesianProduct(queries, idx+1, curr, complement, iter); err != nil {
return err
}
- curr = curr[:len(curr)-1]
+ curr = curr[:mark]
}
return nil
}
@@ -4426,3 +4660,180 @@ func (e *eval) updateSavedMocks(withs []*ast.With) []*ast.With {
}
return ret
}
+
+// simpleTreeNode provides minimal tree structure for navigation
+type simpleTreeNode struct {
+ tree *ast.TreeNode
+ children map[ast.Value]*simpleTreeNode
+}
+
+func newSimpleTreeNode() *simpleTreeNode {
+ return &simpleTreeNode{
+ children: make(map[ast.Value]*simpleTreeNode),
+ }
+}
+
+// externalTreeStack caches external rule trees and tracks frames for input changes.
+// It maintains both a flat cache for lookups and a tree structure for navigation.
+type externalTreeStack struct {
+ eval *eval
+ entries []externalTreeEntry // flat list of cached entries
+ frames []int // frame markers for input changes (indices into entries)
+ root *simpleTreeNode // tree structure for navigation
+}
+
+type externalTreeEntry struct {
+ ref ast.Ref
+ input *ast.Term
+ tree *ast.TreeNode
+ index ast.ExternalRuleIndex
+}
+
+func newExternalTreeStack(e *eval) *externalTreeStack {
+ return &externalTreeStack{
+ eval: e,
+ entries: make([]externalTreeEntry, 0, 4),
+ frames: make([]int, 0, 4),
+ }
+}
+
+// findCached checks if we already have a cached tree for this ref.
+// Frame tracking ensures any cached entry has the correct input.
+func (s *externalTreeStack) findCached(ref ast.Ref) (*ast.TreeNode, ast.ExternalRuleIndex, bool) {
+ // Determine search boundary: only search within current frame if one exists
+ startIdx := 0
+ if len(s.frames) > 0 {
+ startIdx = s.frames[len(s.frames)-1]
+ }
+
+ // Search from most recent to the frame boundary
+ for i := len(s.entries) - 1; i >= startIdx; i-- {
+ entry := &s.entries[i]
+ if entry.ref.Equal(ref) {
+ return entry.tree, entry.index, true
+ }
+ }
+ return nil, nil, false
+}
+
+func (s *externalTreeStack) Push(ref ast.Ref, tree *ast.TreeNode, index ast.ExternalRuleIndex, input *ast.Term) {
+ // Add entry to cache (we never have duplicates in the same frame)
+ s.entries = append(s.entries, externalTreeEntry{
+ ref: ref,
+ input: input,
+ tree: tree,
+ index: index,
+ })
+
+ // Update root tree structure
+ if s.root == nil {
+ s.root = newSimpleTreeNode()
+ }
+ node := s.root
+ for _, term := range ref {
+ key := term.Value
+ if node.children[key] == nil {
+ node.children[key] = newSimpleTreeNode()
+ }
+ node = node.children[key]
+ }
+ node.tree = tree
+}
+
+// Pop removes the most recent entry from the stack and closes its index.
+// If a frame is active and entries are at or below the frame boundary,
+// Pop is a no-op — PopFrame already cleaned up (or will clean up) those entries.
+func (s *externalTreeStack) Pop() {
+ if len(s.entries) == 0 {
+ return
+ }
+
+ // Don't pop at or below the current frame boundary.
+ if len(s.frames) > 0 && len(s.entries) <= s.frames[len(s.frames)-1] {
+ return
+ }
+
+ // Close the external index if it supports closing
+ lastEntry := &s.entries[len(s.entries)-1]
+ if closer, ok := lastEntry.index.(ast.ExternalRuleIndexCloser); ok {
+ _ = closer.Close()
+ }
+
+ // Remove the most recent entry
+ s.entries = s.entries[:len(s.entries)-1]
+
+ // Rebuild tree from remaining entries
+ s.root = newSimpleTreeNode()
+ for i := range s.entries {
+ entry := &s.entries[i]
+ node := s.root
+ for _, term := range entry.ref {
+ key := term.Value
+ if node.children[key] == nil {
+ node.children[key] = newSimpleTreeNode()
+ }
+ node = node.children[key]
+ }
+ node.tree = entry.tree
+ }
+}
+
+// PushFrame marks the current stack position for input changes
+func (s *externalTreeStack) PushFrame() {
+ s.frames = append(s.frames, len(s.entries))
+}
+
+// PopFrame restores the cache to the marked position
+func (s *externalTreeStack) PopFrame() {
+ if len(s.frames) == 0 {
+ return
+ }
+
+ // Get the frame marker and truncate entries
+ targetSize := s.frames[len(s.frames)-1]
+ s.frames = s.frames[:len(s.frames)-1]
+
+ // Close indices of entries being removed
+ for i := len(s.entries) - 1; i >= targetSize; i-- {
+ if closer, ok := s.entries[i].index.(ast.ExternalRuleIndexCloser); ok {
+ _ = closer.Close()
+ }
+ }
+
+ // Rebuild tree from remaining entries
+ s.root = newSimpleTreeNode()
+ for i := range targetSize {
+ entry := &s.entries[i]
+ node := s.root
+ for _, term := range entry.ref {
+ key := term.Value
+ if node.children[key] == nil {
+ node.children[key] = newSimpleTreeNode()
+ }
+ node = node.children[key]
+ }
+ node.tree = entry.tree
+ }
+
+ s.entries = s.entries[:targetSize]
+}
+
+// ruleIndex performs a shadowed lookup for a RuleIndex, checking external trees first.
+// It searches through the pushStack (most recent to oldest), navigating the tree structure
+// to find matching rules, then falls back to the compiler's static RuleTree.
+func (e *eval) ruleIndex(ref ast.Ref) ast.RuleIndex {
+ if e.externalTreeStack != nil && len(e.externalTreeStack.entries) > 0 {
+ // Search from most recent to oldest
+ for i := len(e.externalTreeStack.entries) - 1; i >= 0; i-- {
+ entry := &e.externalTreeStack.entries[i]
+ if ref.HasPrefix(entry.ref) {
+ // Look for the relative ref in the cached tree
+ relativeRef := ref[len(entry.ref):]
+ if found := entry.tree.Find(relativeRef); found != nil {
+ return found.Index
+ }
+ }
+ }
+ }
+ return e.compiler.RuleIndex(ref)
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go
new file mode 100644
index 0000000000..c92be6fa93
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/evaluated.go
@@ -0,0 +1,50 @@
+// Copyright 2026 The OPA Authors. All rights reserved.
+// Use of this source code is governed by an Apache2
+// license that can be found in the LICENSE file.
+
+package topdown
+
+import (
+ "github.com/open-policy-agent/opa/v1/ast"
+)
+
+// EvaluatedRuleTracker records labels from annotations during evaluation.
+// For each successfully evaluated rule, labels from the rule's annotation
+// chain (subpackages, package, document, rule) are merged into a single map
+// with inner-scope-wins precedence. The merged maps are deduplicated across
+// rules so that identical label sets collapse to a single entry.
+//
+// An AnnotationSet must be set via WithAnnotationSet for the tracker to
+// resolve chains; without it, Record is a no-op.
+type EvaluatedRuleTracker struct {
+ Labels []map[string]any
+ seen map[string]struct{}
+ as *ast.AnnotationSet
+}
+
+// WithAnnotationSet configures the AnnotationSet used to resolve each
+// evaluated rule's annotation chain. Typically wired from the compiler.
+func (t *EvaluatedRuleTracker) WithAnnotationSet(as *ast.AnnotationSet) *EvaluatedRuleTracker {
+ if t != nil {
+ t.as = as
+ }
+ return t
+}
+
+func (t *EvaluatedRuleTracker) Record(rule *ast.Rule) {
+ if t == nil || t.as == nil {
+ return
+ }
+ labels, key := t.as.MergedLabels(rule)
+ if len(labels) == 0 {
+ return
+ }
+ if t.seen == nil {
+ t.seen = make(map[string]struct{})
+ }
+ if _, dup := t.seen[key]; dup {
+ return
+ }
+ t.seen[key] = struct{}{}
+ t.Labels = append(t.Labels, labels)
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go
index 93da1d0022..3fc7e0138e 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/instrumentation.go
@@ -19,6 +19,7 @@ const (
evalOpComprehensionCacheBuild = "eval_op_comprehension_cache_build"
evalOpComprehensionCacheHit = "eval_op_comprehension_cache_hit"
evalOpComprehensionCacheMiss = "eval_op_comprehension_cache_miss"
+ evalOpExternalRuleSource = "eval_op_external_rule_source"
partialOpSaveUnify = "partial_op_save_unify"
partialOpSaveSetContains = "partial_op_save_set_contains"
partialOpSaveSetContainsRec = "partial_op_save_set_contains_rec"
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go
index e48719d145..0236e5be09 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/jsonschema.go
@@ -47,6 +47,19 @@ func newResultTerm(valid bool, data *ast.Term) *ast.Term {
return ast.ArrayTerm(ast.InternedTerm(valid), data)
}
+// newPatternValidatingSchemaLoader returns a SchemaLoader configured to
+// compile and enforce the "pattern" keyword. This is the variant used by the
+// json.verify_schema and json.match_schema built-ins, where runtime pattern
+// validation is expected. It is intentionally not shared with the compile-time
+// type-checking path, where pattern validation is disabled to tolerate
+// schemas containing ECMA-262 regex features that Go's RE2 dialect can't
+// compile.
+func newPatternValidatingSchemaLoader() *gojsonschema.SchemaLoader {
+ sl := gojsonschema.NewSchemaLoader()
+ sl.ValidatePatterns = true
+ return sl
+}
+
// builtinJSONSchemaVerify accepts 1 argument which can be string or object and checks if it is valid JSON schema.
// Returns array [false, ] with error string at index 1, or [true, ""] with empty string at index 1 otherwise.
func builtinJSONSchemaVerify(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
@@ -57,7 +70,7 @@ func builtinJSONSchemaVerify(_ BuiltinContext, operands []*ast.Term, iter func(*
}
// Check that schema is correct and parses without errors.
- if _, err = gojsonschema.NewSchema(loader); err != nil {
+ if _, err = newPatternValidatingSchemaLoader().Compile(loader); err != nil {
return iter(newResultTerm(false, ast.StringTerm("jsonschema: "+err.Error())))
}
@@ -93,7 +106,7 @@ func builtinJSONMatchSchema(bctx BuiltinContext, operands []*ast.Term, iter func
return err
}
- schema, err = gojsonschema.NewSchema(schemaLoader)
+ schema, err = newPatternValidatingSchemaLoader().Compile(schemaLoader)
if err != nil {
return err
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go
index cd36b87b17..f912d276c2 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_bytes.go
@@ -5,6 +5,7 @@
package topdown
import (
+ "errors"
"fmt"
"math/big"
"strings"
@@ -43,8 +44,13 @@ var (
errBytesValueNoAmount = parseNumBytesError("no byte amount provided")
errBytesValueNumConv = parseNumBytesError("could not parse byte amount to a number")
errBytesValueIncludesSpaces = parseNumBytesError("spaces not allowed in resource strings")
+ errBytesExponentTooLarge = parseNumBytesError("exponent too large")
)
+// maxExponentDigits limits the number of digits allowed in the exponent of
+// scientific notation input.
+const maxExponentDigits = 6
+
func builtinNumBytes(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
var m big.Float
@@ -59,7 +65,10 @@ func builtinNumBytes(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term
return errBytesValueIncludesSpaces
}
- num, unit := extractNumAndUnit(s)
+ num, unit, err := extractNumAndUnit(s)
+ if err != nil {
+ return errBytesExponentTooLarge
+ }
if num == "" {
return errBytesValueNoAmount
}
@@ -115,7 +124,9 @@ func formatString(s ast.String) string {
// Splits the string into a number string à la "10" or "10.2" and a unit
// string à la "gb" or "MiB" or "foo". Either can be an empty string
// (error handling is provided elsewhere).
-func extractNumAndUnit(s string) (string, string) {
+// Returns an error if the exponent in scientific notation exceeds
+// maxExponentDigits digits.
+func extractNumAndUnit(s string) (string, string, error) {
isNum := func(r rune) bool {
return unicode.IsDigit(r) || r == '.'
}
@@ -138,18 +149,28 @@ func extractNumAndUnit(s string) (string, string) {
if idx+1 < len(s) && (s[idx+1] == '+' || s[idx+1] == '-') {
idx++
}
+
+ // Count the digits in the exponent and reject if too large.
+ expStart := idx + 1
+ expEnd := expStart
+ for expEnd < len(s) && unicode.IsDigit(rune(s[expEnd])) {
+ expEnd++
+ }
+ if expEnd-expStart > maxExponentDigits {
+ return "", "", errors.New("exponent too large")
+ }
}
}
if firstNonNumIdx == -1 { // only digits, '.', or valid scientific notation
- return s, ""
+ return s, "", nil
}
if firstNonNumIdx == 0 { // only units (starts with non-digit)
- return "", s
+ return "", s, nil
}
// Return the number and the rest as the unit
- return s[:firstNonNumIdx], s[firstNonNumIdx:]
+ return s[:firstNonNumIdx], s[firstNonNumIdx:], nil
}
func init() {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go
index 44aec86299..770eea9878 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/parse_units.go
@@ -34,9 +34,10 @@ func errUnitNotRecognized(unit string) error {
}
var (
- errNoAmount = parseUnitsError("no amount provided")
- errNumConv = parseUnitsError("could not parse amount to a number")
- errIncludesSpaces = parseUnitsError("spaces not allowed in resource strings")
+ errNoAmount = parseUnitsError("no amount provided")
+ errNumConv = parseUnitsError("could not parse amount to a number")
+ errIncludesSpaces = parseUnitsError("spaces not allowed in resource strings")
+ errUnitsExponentTooLarge = parseUnitsError("exponent too large")
)
// Accepts both normal SI and binary SI units.
@@ -56,7 +57,10 @@ func builtinUnits(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) e
return errIncludesSpaces
}
- num, unit := extractNumAndUnit(s)
+ num, unit, err := extractNumAndUnit(s)
+ if err != nil {
+ return errUnitsExponentTooLarge
+ }
if num == "" {
return errNoAmount
}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go
index a65f8a312c..85b3ed9e93 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/query.go
@@ -63,6 +63,9 @@ type Query struct {
tracingOpts tracing.Options
virtualCache VirtualCache
baseCache BaseCache
+ requestMetadata map[string]any
+ responseMetadata map[string]any
+ evaluated *EvaluatedRuleTracker
}
// Builtin represents a built-in function that queries can call.
@@ -333,6 +336,28 @@ func (q *Query) WithNondeterministicBuiltins(yes bool) *Query {
return q
}
+// WithRequestMetadata sets arbitrary metadata from the caller that can be
+// used by wrapping projects. The data is stored but not directly used by
+// OPA's evaluation engine.
+func (q *Query) WithRequestMetadata(m map[string]any) *Query {
+ q.requestMetadata = m
+ return q
+}
+
+// WithResponseMetadata sets a map that wrapping projects can populate during
+// evaluation to include additional fields in the API response.
+func (q *Query) WithResponseMetadata(m map[string]any) *Query {
+ q.responseMetadata = m
+ return q
+}
+
+// WithEvaluatedRuleTracker sets a tracker to record rule identifiers that were
+// successfully evaluated.
+func (q *Query) WithEvaluatedRuleTracker(t *EvaluatedRuleTracker) *Query {
+ q.evaluated = t
+ return q
+}
+
// PartialRun executes partial evaluation on the query with respect to unknown
// values. Partial evaluation attempts to evaluate as much of the query as
// possible without requiring values for the unknowns set on the query. The
@@ -344,6 +369,9 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support []
if q.partialNamespace == "" {
q.partialNamespace = "partial" // lazily initialize partial namespace
}
+ if q.evaluated != nil && q.compiler != nil {
+ q.evaluated.WithAnnotationSet(q.compiler.GetAnnotationSet())
+ }
if q.seed == nil {
q.seed = rand.Reader
}
@@ -407,13 +435,16 @@ func (q *Query) PartialRun(ctx context.Context) (partials []ast.Body, support []
shallow: q.shallowInlining,
nondeterministicBuiltins: q.nondeterministicBuiltins,
},
- genvarprefix: q.genvarprefix,
- runtime: q.runtime,
- indexing: q.indexing,
- earlyExit: q.earlyExit,
- builtinErrors: &builtinErrors{},
- printHook: q.printHook,
- strictObjects: q.strictObjects,
+ genvarprefix: q.genvarprefix,
+ runtime: q.runtime,
+ indexing: q.indexing,
+ earlyExit: q.earlyExit,
+ builtinErrors: &builtinErrors{},
+ printHook: q.printHook,
+ strictObjects: q.strictObjects,
+ requestMetadata: q.requestMetadata,
+ responseMetadata: q.responseMetadata,
+ evaluated: q.evaluated,
}
if len(q.disableInlining) > 0 {
@@ -540,6 +571,10 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error {
}
}
+ if q.evaluated != nil && q.compiler != nil {
+ q.evaluated.WithAnnotationSet(q.compiler.GetAnnotationSet())
+ }
+
if q.seed == nil {
q.seed = rand.Reader
}
@@ -602,6 +637,12 @@ func (q *Query) Iter(ctx context.Context, iter func(QueryResult) error) error {
tracingOpts: q.tracingOpts,
strictObjects: q.strictObjects,
roundTripper: q.roundTripper,
+ requestMetadata: q.requestMetadata,
+ responseMetadata: q.responseMetadata,
+ evaluated: q.evaluated,
+ }
+ if e.requestMetadata == nil {
+ e.requestMetadata = map[string]any{}
}
e.caller = e
q.metrics.Timer(metrics.RegoQueryEval).Start()
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go
index 1c31019db9..683c31c6c6 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/reachable.go
@@ -74,39 +74,31 @@ func builtinReachable(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
// pathBuilder is called recursively to build a Set of paths that are reachable from the root
func pathBuilder(graph ast.Object, root *ast.Term, path []*ast.Term, edgeRslt ast.Set, reached ast.Set) {
- paths := []*ast.Term{}
-
- if edges := graph.Get(root); edges != nil {
- path = append(path, root)
-
- if numberOfEdges(edges) >= 1 {
-
- foreachVertex(edges, func(neighbor *ast.Term) {
-
- if reached.Contains(neighbor) {
- // If we've already reached this node, return current path (avoid infinite recursion)
- paths = append(paths, path...)
- edgeRslt.Add(ast.ArrayTerm(paths...))
- } else {
- reached.Add(root)
- pathBuilder(graph, neighbor, path, edgeRslt, reached)
-
- }
-
- })
-
- } else {
- paths = append(paths, path...)
- edgeRslt.Add(ast.ArrayTerm(paths...))
-
- }
- } else {
- // Node is nonexistent (not in graph). Commit the current path (without adding this root)
- paths = append(paths, path...)
- edgeRslt.Add(ast.ArrayTerm(paths...))
-
+ edges := graph.Get(root)
+ if edges == nil {
+ // Node not in graph — commit path without this node.
+ edgeRslt.Add(ast.ArrayTerm(path...))
+ return
}
+ path = append(path, root)
+
+ if numberOfEdges(edges) == 0 {
+ edgeRslt.Add(ast.ArrayTerm(path...))
+ return
+ }
+
+ reached = reached.Copy()
+ reached.Add(root)
+
+ foreachVertex(edges, func(neighbor *ast.Term) {
+ if reached.Contains(neighbor) {
+ // Cycle detected — commit current path.
+ edgeRslt.Add(ast.ArrayTerm(path...))
+ } else {
+ pathBuilder(graph, neighbor, append([]*ast.Term(nil), path...), edgeRslt, reached)
+ }
+ })
}
func builtinReachablePaths(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go
index 47bf7521b4..7a922b5f7b 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/save.go
@@ -357,7 +357,7 @@ func splitPackageAndRule(path ast.Ref) (ast.Ref, ast.Ref) {
// being saved. This check allows the evaluator to evaluate statements
// completely during partial evaluation as long as they do not depend on any
// kind of unknown value or statements that would generate saves.
-func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, ss *saveSet, b *bindings, x any, rec bool) bool {
+func saveRequired(compilerTree *ast.TreeNode, extStack *externalTreeStack, ic *inliningControl, icIgnoreInternal bool, ss *saveSet, b *bindings, x any, rec bool) bool {
var found bool
@@ -389,8 +389,9 @@ func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, s
} else if ic.Disabled(v.ConstantPrefix(), icIgnoreInternal) {
found = true
} else {
- for _, rule := range c.GetRulesDynamicWithOpts(v, ast.RulesOptions{IncludeHiddenModules: false}) {
- if saveRequired(c, ic, icIgnoreInternal, ss, b, rule, true) {
+ rules := getRulesDynamic(compilerTree, extStack, v, ast.RulesOptions{IncludeHiddenModules: false})
+ for _, rule := range rules {
+ if saveRequired(compilerTree, extStack, ic, icIgnoreInternal, ss, b, rule, true) {
found = true
break
}
@@ -406,6 +407,76 @@ func saveRequired(c *ast.Compiler, ic *inliningControl, icIgnoreInternal bool, s
return found
}
+// getRulesDynamic looks up rules in both the compiler tree and external sources.
+func getRulesDynamic(compilerTree *ast.TreeNode, extStack *externalTreeStack, ref ast.Ref, opts ast.RulesOptions) []*ast.Rule {
+ var rules []*ast.Rule
+
+ // Check external trees
+ if extStack != nil {
+ for i := range extStack.entries {
+ entry := &extStack.entries[i]
+ if entry.tree != nil && ref.HasPrefix(entry.ref) {
+ // Navigate into the external tree using the remaining path
+ remaining := ref[len(entry.ref):]
+ rules = append(rules, getRulesFromTree(entry.tree, remaining, opts)...)
+ }
+ }
+ }
+
+ // Then check compiler tree
+ rules = append(rules, getRulesFromTree(compilerTree, ref, opts)...)
+
+ return rules
+}
+
+// getRulesFromTree walks a tree to find all rules matching the given ref.
+func getRulesFromTree(node *ast.TreeNode, ref ast.Ref, opts ast.RulesOptions) []*ast.Rule {
+ set := map[*ast.Rule]struct{}{}
+ var walk func(*ast.TreeNode, int)
+ walk = func(nav *ast.TreeNode, i int) {
+ switch {
+ case i >= len(ref):
+ nav.DepthFirst(func(descendant *ast.TreeNode) bool {
+ for _, rule := range descendant.Values {
+ set[rule] = struct{}{}
+ }
+ if opts.IncludeHiddenModules {
+ return false
+ }
+ return descendant.Hide
+ })
+
+ case i == 0 || ast.IsConstant(ref[i].Value):
+ if child := nav.Child(ref[i].Value); child != nil {
+ for _, rule := range child.Values {
+ set[rule] = struct{}{}
+ }
+ walk(child, i+1)
+ } else {
+ return
+ }
+
+ default:
+ for _, child := range nav.Children {
+ if child.Hide && !opts.IncludeHiddenModules {
+ continue
+ }
+ for _, rule := range child.Values {
+ set[rule] = struct{}{}
+ }
+ walk(child, i+1)
+ }
+ }
+ }
+
+ walk(node, 0)
+ rules := make([]*ast.Rule, 0, len(set))
+ for rule := range set {
+ rules = append(rules, rule)
+ }
+ return rules
+}
+
func ignoreExprDuringPartial(expr *ast.Expr) bool {
if !expr.IsCall() {
return false
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go
index 16eae3e0bd..7171d5c0b2 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/time.go
@@ -7,15 +7,18 @@ package topdown
import (
"encoding/json"
"errors"
+ "fmt"
"math"
"math/big"
"strconv"
+ "strings"
"sync"
"time"
_ "time/tzdata" // this is needed to have LoadLocation when no filesystem tzdata is available
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/topdown/builtins"
+ "github.com/open-policy-agent/opa/v1/topdown/durationparser"
)
var tzCache map[string]*time.Location
@@ -27,6 +30,84 @@ var minDateAllowedForNsConversion = time.Unix(0, math.MinInt64)
// 2262-04-11T23:47:16.854775807-00:00
var maxDateAllowedForNsConversion = time.Unix(0, math.MaxInt64)
+var durationCoefficients = map[string]int{
+ "d": 24,
+ "w": 7 * 24,
+ "y": 365 * 24,
+}
+
+// parseExtendedDuration parses a duration string that may contain extended
+// units (d, w, y) mixed with standard Go duration units (h, m, s, ms, us, ns).
+// Extended unit segments are rewritten to equivalent hours (e.g. "1d2h30m" → "24h2h30m")
+func parseExtendedDuration(s string) (int64, error) {
+ if s == "" {
+ return 0, fmt.Errorf("time: invalid duration %q", s)
+ }
+
+ if !strings.ContainsAny(s, "dwy") {
+ v, err := time.ParseDuration(s)
+ if err != nil {
+ return 0, err
+ }
+ return int64(v), nil
+ }
+
+ result, err := durationparser.Parse("", []byte(s))
+ if err != nil {
+ return 0, fmt.Errorf("time: invalid duration %q", s)
+ }
+
+ rewritten, err := rewriteDuration(result.(durationparser.Result))
+ if err != nil {
+ return 0, fmt.Errorf("time: invalid duration %q", s)
+ }
+
+ v, err := time.ParseDuration(rewritten)
+ if err != nil {
+ // Replace the rewritten duration in the error with the original.
+ msg := err.Error()
+ if i := strings.LastIndex(msg, " "); i != -1 {
+ msg = msg[:i]
+ }
+ return 0, fmt.Errorf("%s %q", msg, s)
+ }
+
+ return int64(v), nil
+}
+
+// rewriteDuration converts parsed duration segments, replacing extended
+// units (d, w, y) with their equivalent in hours.
+func rewriteDuration(dr durationparser.Result) (string, error) {
+ var b strings.Builder
+ if dr.Sign != "" {
+ b.WriteString(dr.Sign)
+ }
+ for _, seg := range dr.Segments {
+ s, err := rewriteSegment(seg)
+ if err != nil {
+ return "", err
+ }
+ b.WriteString(s)
+ }
+ return b.String(), nil
+}
+
+// rewriteSegment rewrites a single segment like {Digits:"1", Unit:"d"} to "24h".
+// Segments with standard units are returned unchanged.
+func rewriteSegment(seg durationparser.Segment) (string, error) {
+ coeff, ok := durationCoefficients[seg.Unit]
+ if !ok {
+ return seg.Digits + seg.Unit, nil
+ }
+
+ val, err := strconv.ParseFloat(seg.Digits, 64)
+ if err != nil {
+ return "", fmt.Errorf("time: invalid duration: bad value %q for unit %q", seg.Digits, seg.Unit)
+ }
+ hours := val * float64(coeff)
+ return strconv.FormatFloat(hours, 'f', -1, 64) + "h", nil
+}
+
func toSafeUnixNano(t time.Time, iter func(*ast.Term) error) error {
if t.Before(minDateAllowedForNsConversion) || t.After(maxDateAllowedForNsConversion) {
return errors.New("time outside of valid range")
@@ -77,16 +158,19 @@ func builtinTimeParseRFC3339Nanos(_ BuiltinContext, operands []*ast.Term, iter f
return toSafeUnixNano(result, iter)
}
+
func builtinParseDurationNanos(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
duration, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return err
}
- value, err := time.ParseDuration(string(duration))
+
+ ns, err := parseExtendedDuration(string(duration))
if err != nil {
return err
}
- return iter(ast.NumberTerm(int64ToJSONNumber(int64(value))))
+
+ return iter(ast.NumberTerm(int64ToJSONNumber(ns)))
}
// Represent exposed constants for formatting from the stdlib time pkg
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go
index bb2c9e1c1c..622f376c77 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/tokens.go
@@ -1289,6 +1289,8 @@ func createTokenCacheKey(serializedJwt ast.Value, publicKey ast.Value) ast.Value
}
func init() {
+ jwk.Configure(jwk.WithMinRSAPublicExponent(0), jwk.WithMinRSAModulusBits(0))
+
// By default, the JWT cache is disabled.
disabled := true
var tokenCache = cache.NamedValueCacheConfig{
diff --git a/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go b/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go
new file mode 100644
index 0000000000..2b97c3e680
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/topdown/uri.go
@@ -0,0 +1,74 @@
+// Copyright 2026 The OPA Authors. All rights reserved.
+// Use of this source code is governed by an Apache2
+// license that can be found in the LICENSE file.
+
+package topdown
+
+import (
+ "cmp"
+ "net/url"
+
+ "github.com/open-policy-agent/opa/v1/ast"
+ "github.com/open-policy-agent/opa/v1/topdown/builtins"
+)
+
+func builtinURIParse(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
+ str, err := builtins.StringOperand(operands[0].Value, 1)
+ if err != nil {
+ return err
+ }
+
+ parsed, err := url.Parse(string(str))
+ if err != nil {
+ return err
+ }
+
+ obj := ast.NewObject()
+
+ if parsed.Scheme != "" {
+ obj.Insert(ast.InternedTerm("scheme"), ast.StringTerm(parsed.Scheme))
+ }
+ if hostname := parsed.Hostname(); hostname != "" {
+ obj.Insert(ast.InternedTerm("hostname"), ast.StringTerm(hostname))
+ }
+ if port := parsed.Port(); port != "" {
+ obj.Insert(ast.InternedTerm("port"), ast.StringTerm(port))
+ }
+ if parsed.Path != "" {
+ obj.Insert(ast.InternedTerm("path"), ast.StringTerm(parsed.Path))
+ // raw_path is always set when path is present, so that users can
+ // rely on it being available for custom path normalization.
+ obj.Insert(ast.InternedTerm("raw_path"), ast.StringTerm(cmp.Or(parsed.RawPath, parsed.Path)))
+ }
+ if parsed.RawQuery != "" {
+ // raw_query can be piped into urlquery.decode_object() for structured access
+ obj.Insert(ast.InternedTerm("raw_query"), ast.StringTerm(parsed.RawQuery))
+ }
+ if parsed.Fragment != "" {
+ obj.Insert(ast.InternedTerm("fragment"), ast.StringTerm(parsed.Fragment))
+ }
+
+ return iter(ast.NewTerm(obj))
+}
+
+func builtinURIIsValid(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
+ str, err := builtins.StringOperand(operands[0].Value, 1)
+ if err != nil {
+ return iter(ast.InternedTerm(false))
+ }
+
+ // Empty strings are technically valid relative references per RFC 3986,
+ // but are rejected here to avoid unexpected results for policy use cases.
+ if len(str) == 0 {
+ return iter(ast.InternedTerm(false))
+ }
+
+ _, err = url.Parse(string(str))
+
+ return iter(ast.InternedTerm(err == nil))
+}
+
+func init() {
+ RegisterBuiltinFunc(ast.URIParse.Name, builtinURIParse)
+ RegisterBuiltinFunc(ast.URIIsValid.Name, builtinURIIsValid)
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/types/decode.go b/vendor/github.com/open-policy-agent/opa/v1/types/decode.go
index 367b64bffb..f2515d5df1 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/types/decode.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/types/decode.go
@@ -12,15 +12,16 @@ import (
)
const (
- typeNull = "null"
- typeBoolean = "boolean"
- typeNumber = "number"
- typeString = "string"
- typeArray = "array"
- typeSet = "set"
- typeObject = "object"
- typeAny = "any"
- typeFunction = "function"
+ typeNull = "null"
+ typeBoolean = "boolean"
+ typeNumber = "number"
+ typeString = "string"
+ typeArray = "array"
+ typeSet = "set"
+ typeObject = "object"
+ typeAny = "any"
+ typeFunction = "function"
+ typeRecursive = "recursive"
)
// Unmarshal deserializes bs and returns the resulting type.
diff --git a/vendor/github.com/open-policy-agent/opa/v1/types/types.go b/vendor/github.com/open-policy-agent/opa/v1/types/types.go
index fc1db120a0..aff4fde39e 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/types/types.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/types/types.go
@@ -48,15 +48,16 @@ type Type interface {
json.Marshaler
}
-func (Null) typeMarker() string { return typeNull }
-func (Boolean) typeMarker() string { return typeBoolean }
-func (Number) typeMarker() string { return typeNumber }
-func (String) typeMarker() string { return typeString }
-func (*Array) typeMarker() string { return typeArray }
-func (*Object) typeMarker() string { return typeObject }
-func (*Set) typeMarker() string { return typeSet }
-func (Any) typeMarker() string { return typeAny }
-func (Function) typeMarker() string { return typeFunction }
+func (Null) typeMarker() string { return typeNull }
+func (Boolean) typeMarker() string { return typeBoolean }
+func (Number) typeMarker() string { return typeNumber }
+func (String) typeMarker() string { return typeString }
+func (*Array) typeMarker() string { return typeArray }
+func (*Object) typeMarker() string { return typeObject }
+func (*Set) typeMarker() string { return typeSet }
+func (Any) typeMarker() string { return typeAny }
+func (Function) typeMarker() string { return typeFunction }
+func (*Recursive) typeMarker() string { return typeRecursive }
// Null represents the null type.
type Null struct{}
@@ -123,6 +124,13 @@ func unwrap(t Type) Type {
}
}
+func unwrapRecursive(t Type) Type {
+ if r, ok := t.(*Recursive); ok {
+ return r.typ
+ }
+ return t
+}
+
func (Null) String() string {
return typeNull
}
@@ -508,6 +516,36 @@ func mergeObjects(a, b *Object) *Object {
return NewObject(staticProps, dynamicProps)
}
+// Recursive is a Type that contains a pointer back to itself.
+// This is for representing recursive JSON Schema definitions.
+type Recursive struct {
+ name string // the $ref key (e.g. "#/$defs/foo")
+ typ Type // the referenced type (can be *Object, *Array, or Any)
+}
+
+// NewRecursive returns a new Recursive type that wraps typ under the given name.
+func NewRecursive(name string, typ Type) *Recursive {
+ return &Recursive{name: name, typ: typ}
+}
+
+func (t *Recursive) String() string {
+ return "recursive(" + t.name + ")"
+}
+
+func (t *Recursive) Unwrap() Type {
+ return t.typ
+}
+
+func (t *Recursive) SetType(typ Type) {
+ t.typ = typ
+}
+
+func (t *Recursive) MarshalJSON() ([]byte, error) {
+ return json.Marshal(map[string]any{
+ "name": t.name,
+ })
+}
+
// Any represents a dynamic type.
type Any []Type
@@ -845,7 +883,7 @@ func (a FuncArgs) Arg(x int) Type {
// Compare returns -1, 0, 1 based on comparison between a and b.
func Compare(a, b Type) int {
- a, b = unwrap(a), unwrap(b)
+ a, b = unwrapRecursive(unwrap(a)), unwrapRecursive(unwrap(b))
x := typeOrder(a)
y := typeOrder(b)
if x > y {
@@ -859,6 +897,9 @@ func Compare(a, b Type) int {
case *Array:
arrA := a.(*Array)
arrB := b.(*Array)
+ if arrA == arrB {
+ return 0
+ }
if arrA.dynamic != nil && arrB.dynamic == nil {
return 1
} else if arrB.dynamic != nil && arrA.dynamic == nil {
@@ -873,6 +914,9 @@ func Compare(a, b Type) int {
case *Object:
objA := a.(*Object)
objB := b.(*Object)
+ if objA == objB {
+ return 0
+ }
if objA.dynamic != nil && objB.dynamic == nil {
return 1
} else if objB.dynamic != nil && objA.dynamic == nil {
@@ -985,7 +1029,7 @@ func Or(a, b Type) Type {
// Select returns a property or item of a.
func Select(a Type, x any) Type {
- switch a := unwrap(a).(type) {
+ switch a := unwrapRecursive(unwrap(a)).(type) {
case *Array:
n, ok := x.(json.Number)
if !ok {
@@ -1028,7 +1072,7 @@ func Select(a Type, x any) Type {
// keys are always number types, for objects the keys are always string types,
// and for sets the keys are always the type of the set element.
func Keys(a Type) Type {
- switch a := unwrap(a).(type) {
+ switch a := unwrapRecursive(unwrap(a)).(type) {
case *Array:
return N
case *Object:
@@ -1058,7 +1102,7 @@ func Keys(a Type) Type {
// Values returns the type of values that can be enumerated for a.
func Values(a Type) Type {
- switch a := unwrap(a).(type) {
+ switch a := unwrapRecursive(unwrap(a)).(type) {
case *Array:
var tpe Type
for i := range a.static {
@@ -1091,32 +1135,52 @@ func Values(a Type) Type {
// Nil returns true if a's type is unknown.
func Nil(a Type) bool {
- switch a := unwrap(a).(type) {
- case nil:
+ return nilRec(a, nil)
+}
+
+func nilRec(a Type, seen map[Type]struct{}) bool {
+ a = unwrapRecursive(unwrap(a))
+ if a == nil {
return true
+ }
+ switch a := a.(type) {
case *Function:
- if slices.ContainsFunc(a.args, Nil) {
+ if slices.ContainsFunc(a.args, func(t Type) bool { return nilRec(t, seen) }) {
return true
}
- return Nil(a.result)
+ return nilRec(a.result, seen)
case *Array:
- if slices.ContainsFunc(a.static, Nil) {
+ if _, ok := seen[a]; ok {
+ return false
+ }
+ if seen == nil {
+ seen = make(map[Type]struct{})
+ }
+ seen[a] = struct{}{}
+ if slices.ContainsFunc(a.static, func(t Type) bool { return nilRec(t, seen) }) {
return true
}
if a.dynamic != nil {
- return Nil(a.dynamic)
+ return nilRec(a.dynamic, seen)
}
case *Object:
+ if _, ok := seen[a]; ok {
+ return false
+ }
+ if seen == nil {
+ seen = make(map[Type]struct{})
+ }
+ seen[a] = struct{}{}
for i := range a.static {
- if Nil(a.static[i].Value) {
+ if nilRec(a.static[i].Value, seen) {
return true
}
}
if a.dynamic != nil {
- return Nil(a.dynamic.Key) || Nil(a.dynamic.Value)
+ return nilRec(a.dynamic.Key, seen) || nilRec(a.dynamic.Value, seen)
}
case *Set:
- return Nil(a.of)
+ return nilRec(a.of, seen)
}
return false
}
@@ -1179,7 +1243,7 @@ func typeSliceCompare(a, b []Type) int {
}
func typeOrder(x Type) int {
- switch unwrap(x).(type) {
+ switch unwrapRecursive(unwrap(x)).(type) {
case Null:
return 0
case Boolean:
diff --git a/vendor/github.com/open-policy-agent/opa/v1/util/compare.go b/vendor/github.com/open-policy-agent/opa/v1/util/compare.go
index df78f64755..a930db21c5 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/util/compare.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/util/compare.go
@@ -10,6 +10,28 @@ import (
"math/big"
)
+const (
+ nilSort = iota
+ boolSort
+ numberSort
+ stringSort
+ arraySort
+ objectSort
+)
+
+// SliceLenCompare is a convenience function for comparing / sorting
+// slices by their length using the various slices.SortX functions.
+func SliceLenCompare[T any, S ~[]T](a, b S) int {
+ aLen, bLen := len(a), len(b)
+ if aLen == bLen {
+ return 0
+ } else if aLen < bLen {
+ return -1
+ }
+
+ return 1
+}
+
// Compare returns 0 if a equals b, -1 if a is less than b, and 1 if b is than a.
//
// For comparison between values of different types, the following ordering is used:
@@ -125,15 +147,6 @@ func Compare(a, b any) int {
panic(fmt.Sprintf("illegal arguments of type %T and type %T", a, b))
}
-const (
- nilSort = iota
- boolSort = iota
- numberSort = iota
- stringSort = iota
- arraySort = iota
- objectSort = iota
-)
-
func compareJSONNumber(a, b json.Number) int {
bigA, ok := new(big.Float).SetString(string(a))
if !ok {
diff --git a/vendor/github.com/open-policy-agent/opa/v1/util/slices.go b/vendor/github.com/open-policy-agent/opa/v1/util/slices.go
new file mode 100644
index 0000000000..57163a5649
--- /dev/null
+++ b/vendor/github.com/open-policy-agent/opa/v1/util/slices.go
@@ -0,0 +1,16 @@
+// Copyright 2026 The OPA Authors. All rights reserved.
+// Use of this source code is governed by an Apache2
+// license that can be found in the LICENSE file.
+
+package util
+
+func Map[T any, U any](s []T, f func(T) U) []U {
+ if s == nil {
+ return nil
+ }
+ r := make([]U, len(s))
+ for i, v := range s {
+ r[i] = f(v)
+ }
+ return r
+}
diff --git a/vendor/github.com/open-policy-agent/opa/v1/version/version.go b/vendor/github.com/open-policy-agent/opa/v1/version/version.go
index 7e71b3b7a9..d734707191 100644
--- a/vendor/github.com/open-policy-agent/opa/v1/version/version.go
+++ b/vendor/github.com/open-policy-agent/opa/v1/version/version.go
@@ -10,7 +10,7 @@ import (
"runtime/debug"
)
-var Version = "1.15.2"
+var Version = "1.17.1"
// GoVersion is the version of Go this was built with
var GoVersion = runtime.Version()
diff --git a/vendor/github.com/prometheus/procfs/.golangci.yml b/vendor/github.com/prometheus/procfs/.golangci.yml
index 3c3bf910fd..eac920ba80 100644
--- a/vendor/github.com/prometheus/procfs/.golangci.yml
+++ b/vendor/github.com/prometheus/procfs/.golangci.yml
@@ -1,7 +1,9 @@
version: "2"
linters:
enable:
+ - errorlint
- forbidigo
+ - gocritic
- godot
- misspell
- revive
@@ -11,6 +13,20 @@ linters:
forbid:
- pattern: ^fmt\.Print.*$
msg: Do not commit print statements.
+ gocritic:
+ enable-all: true
+ disabled-checks:
+ - commentFormatting
+ - commentedOutCode
+ - deferInLoop
+ - filepathJoin
+ - hugeParam
+ - importShadow
+ - paramTypeCombine
+ - rangeValCopy
+ - tooManyResultsChecker
+ - unnamedResult
+ - whyNoLint
godot:
exclude:
# Ignore "See: URL".
@@ -18,17 +34,21 @@ linters:
capital: true
misspell:
locale: US
+ revive:
+ rules:
+ - name: var-naming
+ # TODO(SuperQ): See: https://github.com/prometheus/prometheus/issues/17766
+ arguments:
+ - []
+ - []
+ - - skip-package-name-checks: true
exclusions:
- generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
- paths:
- - third_party$
- - builtin$
- - examples$
+ warn-unused: true
formatters:
enable:
- gofmt
@@ -37,9 +57,3 @@ formatters:
goimports:
local-prefixes:
- github.com/prometheus/procfs
- exclusions:
- generated: lax
- paths:
- - third_party$
- - builtin$
- - examples$
diff --git a/vendor/github.com/prometheus/procfs/Makefile b/vendor/github.com/prometheus/procfs/Makefile
index 7edfe4d093..bce50a19c5 100644
--- a/vendor/github.com/prometheus/procfs/Makefile
+++ b/vendor/github.com/prometheus/procfs/Makefile
@@ -1,4 +1,4 @@
-# Copyright 2018 The Prometheus Authors
+# Copyright The Prometheus Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/Makefile.common b/vendor/github.com/prometheus/procfs/Makefile.common
index 4de21512ff..cce3ef1d16 100644
--- a/vendor/github.com/prometheus/procfs/Makefile.common
+++ b/vendor/github.com/prometheus/procfs/Makefile.common
@@ -1,4 +1,4 @@
-# Copyright 2018 The Prometheus Authors
+# Copyright The Prometheus Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
@@ -55,13 +55,13 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),)
endif
endif
-PROMU_VERSION ?= 0.17.0
+PROMU_VERSION ?= 0.18.0
PROMU_URL := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
SKIP_GOLANGCI_LINT :=
GOLANGCI_LINT :=
GOLANGCI_LINT_OPTS ?=
-GOLANGCI_LINT_VERSION ?= v2.1.5
+GOLANGCI_LINT_VERSION ?= v2.10.1
GOLANGCI_FMT_OPTS ?=
# golangci-lint only supports linux, darwin and windows platforms on i386/amd64/arm64.
# windows isn't included here because of the path separator being different.
@@ -82,11 +82,50 @@ endif
PREFIX ?= $(shell pwd)
BIN_DIR ?= $(shell pwd)
DOCKER_IMAGE_TAG ?= $(subst /,-,$(shell git rev-parse --abbrev-ref HEAD))
-DOCKERFILE_PATH ?= ./Dockerfile
DOCKERBUILD_CONTEXT ?= ./
DOCKER_REPO ?= prom
+# Check if deprecated DOCKERFILE_PATH is set
+ifdef DOCKERFILE_PATH
+$(error DOCKERFILE_PATH is deprecated. Use DOCKERFILE_VARIANTS ?= $(DOCKERFILE_PATH) in the Makefile)
+endif
+
DOCKER_ARCHS ?= amd64
+DOCKERFILE_VARIANTS ?= Dockerfile $(wildcard Dockerfile.*)
+
+# Function to extract variant from Dockerfile label.
+# Returns the variant name from io.prometheus.image.variant label, or "default" if not found.
+define dockerfile_variant
+$(strip $(or $(shell sed -n 's/.*io\.prometheus\.image\.variant="\([^"]*\)".*/\1/p' $(1)),default))
+endef
+
+# Check for duplicate variant names (including default for Dockerfiles without labels).
+DOCKERFILE_VARIANT_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)))
+DOCKERFILE_VARIANT_NAMES_SORTED := $(sort $(DOCKERFILE_VARIANT_NAMES))
+ifneq ($(words $(DOCKERFILE_VARIANT_NAMES)),$(words $(DOCKERFILE_VARIANT_NAMES_SORTED)))
+$(error Duplicate variant names found. Each Dockerfile must have a unique io.prometheus.image.variant label, and only one can be without a label (default))
+endif
+
+# Build variant:dockerfile pairs for shell iteration.
+DOCKERFILE_VARIANTS_WITH_NAMES := $(foreach df,$(DOCKERFILE_VARIANTS),$(call dockerfile_variant,$(df)):$(df))
+
+# Shell helper to check whether a dockerfile/arch pair is excluded.
+define dockerfile_arch_is_excluded
+case " $(DOCKERFILE_ARCH_EXCLUSIONS) " in \
+ *" $$dockerfile:$(1) "*) true ;; \
+ *) false ;; \
+esac
+endef
+
+# Shell helper to check whether a registry/arch pair is excluded.
+# Extracts registry from DOCKER_REPO (e.g., quay.io/prometheus -> quay.io)
+define registry_arch_is_excluded
+registry=$$(echo "$(DOCKER_REPO)" | cut -d'/' -f1); \
+case " $(DOCKER_REGISTRY_ARCH_EXCLUSIONS) " in \
+ *" $$registry:$(1) "*) true ;; \
+ *) false ;; \
+esac
+endef
BUILD_DOCKER_ARCHS = $(addprefix common-docker-,$(DOCKER_ARCHS))
PUBLISH_DOCKER_ARCHS = $(addprefix common-docker-publish-,$(DOCKER_ARCHS))
@@ -112,7 +151,7 @@ common-all: precheck style check_license lint yamllint unused build test
.PHONY: common-style
common-style:
@echo ">> checking code style"
- @fmtRes=$$($(GOFMT) -d $$(find . -path ./vendor -prune -o -name '*.go' -print)); \
+ @fmtRes=$$($(GOFMT) -d $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -name '*.go' -print)); \
if [ -n "$${fmtRes}" ]; then \
echo "gofmt checking failed!"; echo "$${fmtRes}"; echo; \
echo "Please ensure you are using $$($(GO) version) for formatting code."; \
@@ -122,13 +161,19 @@ common-style:
.PHONY: common-check_license
common-check_license:
@echo ">> checking license header"
- @licRes=$$(for file in $$(find . -type f -iname '*.go' ! -path './vendor/*') ; do \
+ @licRes=$$(for file in $$(git ls-files '*.go' ':!:vendor/*' || find . -path ./vendor -prune -o -type f -iname '*.go' -print) ; do \
awk 'NR<=3' $$file | grep -Eq "(Copyright|generated|GENERATED)" || echo $$file; \
done); \
if [ -n "$${licRes}" ]; then \
echo "license header checking failed:"; echo "$${licRes}"; \
exit 1; \
fi
+ @echo ">> checking for copyright years 2026 or later"
+ @futureYearRes=$$(git grep -E 'Copyright (202[6-9]|20[3-9][0-9])' -- '*.go' ':!:vendor/*' || true); \
+ if [ -n "$${futureYearRes}" ]; then \
+ echo "Files with copyright year 2026 or later found (should use 'Copyright The Prometheus Authors'):"; echo "$${futureYearRes}"; \
+ exit 1; \
+ fi
.PHONY: common-deps
common-deps:
@@ -139,7 +184,7 @@ common-deps:
update-go-deps:
@echo ">> updating Go dependencies"
@for m in $$($(GO) list -mod=readonly -m -f '{{ if and (not .Indirect) (not .Main)}}{{.Path}}{{end}}' all); do \
- $(GO) get -d $$m; \
+ $(GO) get $$m; \
done
$(GO) mod tidy
@@ -220,28 +265,194 @@ common-docker-repo-name:
.PHONY: common-docker $(BUILD_DOCKER_ARCHS)
common-docker: $(BUILD_DOCKER_ARCHS)
$(BUILD_DOCKER_ARCHS): common-docker-%:
- docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
- -f $(DOCKERFILE_PATH) \
- --build-arg ARCH="$*" \
- --build-arg OS="linux" \
- $(DOCKERBUILD_CONTEXT)
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if $(call dockerfile_arch_is_excluded,$*); then \
+ echo "Skipping $$variant_name variant for linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ distroless_arch="$*"; \
+ if [ "$*" = "armv7" ]; then \
+ distroless_arch="arm"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Building default variant ($$variant_name) for linux-$* using $$dockerfile"; \
+ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
+ -f $$dockerfile \
+ --build-arg ARCH="$*" \
+ --build-arg OS="linux" \
+ --build-arg DISTROLESS_ARCH="$$distroless_arch" \
+ $(DOCKERBUILD_CONTEXT); \
+ if [ "$$variant_name" != "default" ]; then \
+ echo "Tagging default variant with $$variant_name suffix"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" \
+ "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ else \
+ echo "Building $$variant_name variant for linux-$* using $$dockerfile"; \
+ docker build -t "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" \
+ -f $$dockerfile \
+ --build-arg ARCH="$*" \
+ --build-arg OS="linux" \
+ --build-arg DISTROLESS_ARCH="$$distroless_arch" \
+ $(DOCKERBUILD_CONTEXT); \
+ fi; \
+ done
.PHONY: common-docker-publish $(PUBLISH_DOCKER_ARCHS)
common-docker-publish: $(PUBLISH_DOCKER_ARCHS)
$(PUBLISH_DOCKER_ARCHS): common-docker-publish-%:
- docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if $(call dockerfile_arch_is_excluded,$*); then \
+ echo "Skipping push for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$*); then \
+ echo "Skipping push for $$variant_name variant on linux-$* to $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Pushing $$variant_name variant for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Pushing default variant ($$variant_name) for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ fi; \
+ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Pushing $$variant_name variant version tags for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Pushing default variant version tag for linux-$*"; \
+ docker push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ fi; \
+ done
DOCKER_MAJOR_VERSION_TAG = $(firstword $(subst ., ,$(shell cat VERSION)))
.PHONY: common-docker-tag-latest $(TAG_DOCKER_ARCHS)
common-docker-tag-latest: $(TAG_DOCKER_ARCHS)
$(TAG_DOCKER_ARCHS): common-docker-tag-latest-%:
- docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"
- docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if $(call dockerfile_arch_is_excluded,$*); then \
+ echo "Skipping tag for $$variant_name variant on linux-$* (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$*); then \
+ echo "Skipping tag for $$variant_name variant on linux-$* for $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Tagging $$variant_name variant for linux-$* as latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest-$$variant_name"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Tagging default variant ($$variant_name) for linux-$* as latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:latest"; \
+ docker tag "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:$(SANITIZED_DOCKER_IMAGE_TAG)" "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$*:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ done
.PHONY: common-docker-manifest
common-docker-manifest:
- DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $(foreach ARCH,$(DOCKER_ARCHS),$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$(ARCH):$(SANITIZED_DOCKER_IMAGE_TAG))
- DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"
+ @for variant in $(DOCKERFILE_VARIANTS_WITH_NAMES); do \
+ dockerfile=$${variant#*:}; \
+ variant_name=$${variant%%:*}; \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Creating manifest for $$variant_name variant"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ if $(call dockerfile_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for $$variant_name (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for $$variant_name on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping manifest for $$variant_name variant (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name" $$refs; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Creating default variant ($$variant_name) manifest"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ if $(call dockerfile_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for default variant (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for default variant on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping default variant manifest (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)" $$refs; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):$(SANITIZED_DOCKER_IMAGE_TAG)"; \
+ fi; \
+ if [ "$(DOCKER_IMAGE_TAG)" = "latest" ]; then \
+ if [ "$$dockerfile" != "Dockerfile" ] || [ "$$variant_name" != "default" ]; then \
+ echo "Creating manifest for $$variant_name variant version tag"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ if $(call dockerfile_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for $$variant_name version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for $$variant_name version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping version-tag manifest for $$variant_name variant (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name" $$refs; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)-$$variant_name"; \
+ fi; \
+ if [ "$$dockerfile" = "Dockerfile" ]; then \
+ echo "Creating default variant version tag manifest"; \
+ refs=""; \
+ for arch in $(DOCKER_ARCHS); do \
+ if $(call dockerfile_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for default variant version tag (excluded by DOCKERFILE_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ if $(call registry_arch_is_excluded,$$arch); then \
+ echo " Skipping $$arch for default variant version tag on $(DOCKER_REPO) (excluded by DOCKER_REGISTRY_ARCH_EXCLUSIONS)"; \
+ continue; \
+ fi; \
+ refs="$$refs $(DOCKER_REPO)/$(DOCKER_IMAGE_NAME)-linux-$$arch:v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ done; \
+ if [ -z "$$refs" ]; then \
+ echo "Skipping default variant version-tag manifest (no supported architectures)"; \
+ continue; \
+ fi; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest create -a "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)" $$refs; \
+ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest push "$(DOCKER_REPO)/$(DOCKER_IMAGE_NAME):v$(DOCKER_MAJOR_VERSION_TAG)"; \
+ fi; \
+ fi; \
+ done
.PHONY: promu
promu: $(PROMU)
@@ -266,6 +477,10 @@ $(GOLANGCI_LINT):
| sh -s -- -b $(FIRST_GOPATH)/bin $(GOLANGCI_LINT_VERSION)
endif
+.PHONY: common-print-golangci-lint-version
+common-print-golangci-lint-version:
+ @echo $(GOLANGCI_LINT_VERSION)
+
.PHONY: precheck
precheck::
diff --git a/vendor/github.com/prometheus/procfs/arp.go b/vendor/github.com/prometheus/procfs/arp.go
index 2e53344151..716bdef109 100644
--- a/vendor/github.com/prometheus/procfs/arp.go
+++ b/vendor/github.com/prometheus/procfs/arp.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -73,15 +73,16 @@ func parseARPEntries(data []byte) ([]ARPEntry, error) {
columns := strings.Fields(line)
width := len(columns)
- if width == expectedHeaderWidth || width == 0 {
+ switch width {
+ case expectedHeaderWidth, 0:
continue
- } else if width == expectedDataWidth {
+ case expectedDataWidth:
entry, err := parseARPEntry(columns)
if err != nil {
return []ARPEntry{}, fmt.Errorf("%w: Failed to parse ARP entry: %v: %w", ErrFileParse, entry, err)
}
entries = append(entries, entry)
- } else {
+ default:
return []ARPEntry{}, fmt.Errorf("%w: %d columns found, but expected %d: %w", ErrFileParse, width, expectedDataWidth, err)
}
diff --git a/vendor/github.com/prometheus/procfs/buddyinfo.go b/vendor/github.com/prometheus/procfs/buddyinfo.go
index 8380750090..53243e6875 100644
--- a/vendor/github.com/prometheus/procfs/buddyinfo.go
+++ b/vendor/github.com/prometheus/procfs/buddyinfo.go
@@ -1,4 +1,4 @@
-// Copyright 2017 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -64,14 +64,12 @@ func parseBuddyInfo(r io.Reader) ([]BuddyInfo, error) {
if bucketCount == -1 {
bucketCount = arraySize
- } else {
- if bucketCount != arraySize {
- return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize)
- }
+ } else if bucketCount != arraySize {
+ return nil, fmt.Errorf("%w: mismatch in number of buddyinfo buckets, previous count %d, new count %d", ErrFileParse, bucketCount, arraySize)
}
sizes := make([]float64, arraySize)
- for i := 0; i < arraySize; i++ {
+ for i := range arraySize {
sizes[i], err = strconv.ParseFloat(parts[i+4], 64)
if err != nil {
return nil, fmt.Errorf("%w: Invalid valid in buddyinfo: %f: %w", ErrFileParse, sizes[i], err)
diff --git a/vendor/github.com/prometheus/procfs/cmdline.go b/vendor/github.com/prometheus/procfs/cmdline.go
index bf4f3b48c0..4f1cac1f0a 100644
--- a/vendor/github.com/prometheus/procfs/cmdline.go
+++ b/vendor/github.com/prometheus/procfs/cmdline.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo.go b/vendor/github.com/prometheus/procfs/cpuinfo.go
index f0950bb495..4b23d8d6b5 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build linux
-// +build linux
package procfs
@@ -502,7 +501,7 @@ func parseCPUInfoRISCV(info []byte) ([]CPUInfo, error) {
return cpuinfo, nil
}
-func parseCPUInfoDummy(_ []byte) ([]CPUInfo, error) { // nolint:unused,deadcode
+func parseCPUInfoDummy(_ []byte) ([]CPUInfo, error) { //nolint:unused
return nil, errors.New("not implemented")
}
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_armx.go b/vendor/github.com/prometheus/procfs/cpuinfo_armx.go
index 64cfd534c1..b09035ff38 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_armx.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_armx.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build linux && (arm || arm64)
-// +build linux
-// +build arm arm64
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go b/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go
index d88442f0ed..7bb20211f9 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_loong64.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build linux
-// +build linux
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go b/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go
index c11207f3ab..fd75d0f79d 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_mipsx.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build linux && (mips || mipsle || mips64 || mips64le)
-// +build linux
-// +build mips mipsle mips64 mips64le
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_others.go b/vendor/github.com/prometheus/procfs/cpuinfo_others.go
index a6b2b3127c..3d36ba0e6b 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_others.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_others.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build linux && !386 && !amd64 && !arm && !arm64 && !loong64 && !mips && !mips64 && !mips64le && !mipsle && !ppc64 && !ppc64le && !riscv64 && !s390x
-// +build linux,!386,!amd64,!arm,!arm64,!loong64,!mips,!mips64,!mips64le,!mipsle,!ppc64,!ppc64le,!riscv64,!s390x
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go b/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go
index 003bc2ad4a..b3425051ef 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_ppcx.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build linux && (ppc64 || ppc64le)
-// +build linux
-// +build ppc64 ppc64le
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go b/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go
index 1c9b7313b6..72598230c3 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_riscvx.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build linux && (riscv || riscv64)
-// +build linux
-// +build riscv riscv64
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go b/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go
index fa3686bc00..50a8239cbc 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_s390x.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build linux
-// +build linux
package procfs
diff --git a/vendor/github.com/prometheus/procfs/cpuinfo_x86.go b/vendor/github.com/prometheus/procfs/cpuinfo_x86.go
index a0ef55562e..00edb30a5c 100644
--- a/vendor/github.com/prometheus/procfs/cpuinfo_x86.go
+++ b/vendor/github.com/prometheus/procfs/cpuinfo_x86.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build linux && (386 || amd64)
-// +build linux
-// +build 386 amd64
package procfs
diff --git a/vendor/github.com/prometheus/procfs/crypto.go b/vendor/github.com/prometheus/procfs/crypto.go
index 5f2a37a78b..e4a5876eaf 100644
--- a/vendor/github.com/prometheus/procfs/crypto.go
+++ b/vendor/github.com/prometheus/procfs/crypto.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/doc.go b/vendor/github.com/prometheus/procfs/doc.go
index f9d961e441..26bfea071b 100644
--- a/vendor/github.com/prometheus/procfs/doc.go
+++ b/vendor/github.com/prometheus/procfs/doc.go
@@ -1,4 +1,4 @@
-// Copyright 2014 Prometheus Team
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/fs.go b/vendor/github.com/prometheus/procfs/fs.go
index 9bdaccc7c8..8f27912a13 100644
--- a/vendor/github.com/prometheus/procfs/fs.go
+++ b/vendor/github.com/prometheus/procfs/fs.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go
index 1b5bdbdf84..0bef25bdd9 100644
--- a/vendor/github.com/prometheus/procfs/fs_statfs_notype.go
+++ b/vendor/github.com/prometheus/procfs/fs_statfs_notype.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !freebsd && !linux
-// +build !freebsd,!linux
package procfs
diff --git a/vendor/github.com/prometheus/procfs/fs_statfs_type.go b/vendor/github.com/prometheus/procfs/fs_statfs_type.go
index 80df79c319..d183330390 100644
--- a/vendor/github.com/prometheus/procfs/fs_statfs_type.go
+++ b/vendor/github.com/prometheus/procfs/fs_statfs_type.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build freebsd || linux
-// +build freebsd linux
package procfs
diff --git a/vendor/github.com/prometheus/procfs/fscache.go b/vendor/github.com/prometheus/procfs/fscache.go
index 7db8633077..9dde857073 100644
--- a/vendor/github.com/prometheus/procfs/fscache.go
+++ b/vendor/github.com/prometheus/procfs/fscache.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -388,20 +388,21 @@ func parseFscacheinfo(r io.Reader) (*Fscacheinfo, error) {
}
}
case "CacheOp:":
- if strings.Split(fields[1], "=")[0] == "alo" {
+ switch strings.Split(fields[1], "=")[0] {
+ case "alo":
err := setFSCacheFields(fields[1:], &m.CacheopAllocationsinProgress, &m.CacheopLookupObjectInProgress,
&m.CacheopLookupCompleteInPorgress, &m.CacheopGrabObjectInProgress)
if err != nil {
return &m, err
}
- } else if strings.Split(fields[1], "=")[0] == "inv" {
+ case "inv":
err := setFSCacheFields(fields[1:], &m.CacheopInvalidations, &m.CacheopUpdateObjectInProgress,
&m.CacheopDropObjectInProgress, &m.CacheopPutObjectInProgress, &m.CacheopAttributeChangeInProgress,
&m.CacheopSyncCacheInProgress)
if err != nil {
return &m, err
}
- } else {
+ default:
err := setFSCacheFields(fields[1:], &m.CacheopReadOrAllocPageInProgress, &m.CacheopReadOrAllocPagesInProgress,
&m.CacheopAllocatePageInProgress, &m.CacheopAllocatePagesInProgress, &m.CacheopWritePagesInProgress,
&m.CacheopUncachePagesInProgress, &m.CacheopDissociatePagesInProgress)
diff --git a/vendor/github.com/prometheus/procfs/internal/fs/fs.go b/vendor/github.com/prometheus/procfs/internal/fs/fs.go
index 3a43e83915..e7ccad66b2 100644
--- a/vendor/github.com/prometheus/procfs/internal/fs/fs.go
+++ b/vendor/github.com/prometheus/procfs/internal/fs/fs.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/internal/util/parse.go b/vendor/github.com/prometheus/procfs/internal/util/parse.go
index 5a7d2df06a..30c5872019 100644
--- a/vendor/github.com/prometheus/procfs/internal/util/parse.go
+++ b/vendor/github.com/prometheus/procfs/internal/util/parse.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/internal/util/readfile.go b/vendor/github.com/prometheus/procfs/internal/util/readfile.go
index 71b7a70ebd..0e41f71af1 100644
--- a/vendor/github.com/prometheus/procfs/internal/util/readfile.go
+++ b/vendor/github.com/prometheus/procfs/internal/util/readfile.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go
index d5404a6d72..f6a4a4de62 100644
--- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go
+++ b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build (linux || darwin) && !appengine
-// +build linux darwin
-// +build !appengine
package util
diff --git a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go
index 1d86f5e63f..c80e082cb9 100644
--- a/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go
+++ b/vendor/github.com/prometheus/procfs/internal/util/sysreadfile_compat.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build (linux && appengine) || (!linux && !darwin)
-// +build linux,appengine !linux,!darwin
package util
diff --git a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go b/vendor/github.com/prometheus/procfs/internal/util/valueparser.go
index fe2355d3c6..e0ed671ea0 100644
--- a/vendor/github.com/prometheus/procfs/internal/util/valueparser.go
+++ b/vendor/github.com/prometheus/procfs/internal/util/valueparser.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/ipvs.go b/vendor/github.com/prometheus/procfs/ipvs.go
index bc3a20c932..5374da9fa8 100644
--- a/vendor/github.com/prometheus/procfs/ipvs.go
+++ b/vendor/github.com/prometheus/procfs/ipvs.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/kernel_hung.go b/vendor/github.com/prometheus/procfs/kernel_hung.go
new file mode 100644
index 0000000000..0c7a69f99f
--- /dev/null
+++ b/vendor/github.com/prometheus/procfs/kernel_hung.go
@@ -0,0 +1,44 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//go:build !windows
+
+package procfs
+
+import (
+ "os"
+ "strconv"
+ "strings"
+)
+
+// KernelHung contains information about to the kernel's hung_task_detect_count number.
+type KernelHung struct {
+ // Indicates the total number of tasks that have been detected as hung since the system boot.
+ // This file shows up if `CONFIG_DETECT_HUNG_TASK` is enabled.
+ HungTaskDetectCount *uint64
+}
+
+// KernelHung returns values from /proc/sys/kernel/hung_task_detect_count.
+func (fs FS) KernelHung() (KernelHung, error) {
+ data, err := os.ReadFile(fs.proc.Path("sys", "kernel", "hung_task_detect_count"))
+ if err != nil {
+ return KernelHung{}, err
+ }
+ val, err := strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
+ if err != nil {
+ return KernelHung{}, err
+ }
+ return KernelHung{
+ HungTaskDetectCount: &val,
+ }, nil
+}
diff --git a/vendor/github.com/prometheus/procfs/kernel_random.go b/vendor/github.com/prometheus/procfs/kernel_random.go
index db88566bdf..e7c5b8cf2b 100644
--- a/vendor/github.com/prometheus/procfs/kernel_random.go
+++ b/vendor/github.com/prometheus/procfs/kernel_random.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
-// +build !windows
package procfs
diff --git a/vendor/github.com/prometheus/procfs/loadavg.go b/vendor/github.com/prometheus/procfs/loadavg.go
index 332e76c17f..c8c78a65ed 100644
--- a/vendor/github.com/prometheus/procfs/loadavg.go
+++ b/vendor/github.com/prometheus/procfs/loadavg.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/mdstat.go b/vendor/github.com/prometheus/procfs/mdstat.go
index 1fd4381b22..d66eeda82a 100644
--- a/vendor/github.com/prometheus/procfs/mdstat.go
+++ b/vendor/github.com/prometheus/procfs/mdstat.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -27,13 +27,34 @@ var (
recoveryLinePctRE = regexp.MustCompile(`= (.+)%`)
recoveryLineFinishRE = regexp.MustCompile(`finish=(.+)min`)
recoveryLineSpeedRE = regexp.MustCompile(`speed=(.+)[A-Z]`)
- componentDeviceRE = regexp.MustCompile(`(.*)\[\d+\]`)
+ componentDeviceRE = regexp.MustCompile(`(.*)\[(\d+)\](\([SF]+\))?`)
+ personalitiesPrefix = "Personalities : "
)
+type MDStatComponent struct {
+ // Name of the component device.
+ Name string
+ // DescriptorIndex number of component device, e.g. the order in the superblock.
+ DescriptorIndex int32
+ // Flags per Linux drivers/md/md.[ch] as of v6.12-rc1
+ // Subset that are exposed in mdstat
+ WriteMostly bool
+ Journal bool
+ Faulty bool // "Faulty" is what kernel source uses for "(F)"
+ Spare bool
+ Replacement bool
+ // Some additional flags that are NOT exposed in procfs today; they may
+ // be available via sysfs.
+ // In_sync, Bitmap_sync, Blocked, WriteErrorSeen, FaultRecorded,
+ // BlockedBadBlocks, WantReplacement, Candidate, ...
+}
+
// MDStat holds info parsed from /proc/mdstat.
type MDStat struct {
// Name of the device.
Name string
+ // raid type of the device.
+ Type string
// activity-state of the device.
ActivityState string
// Number of active disks.
@@ -58,8 +79,8 @@ type MDStat struct {
BlocksSyncedFinishTime float64
// current sync speed (in Kilobytes/sec)
BlocksSyncedSpeed float64
- // Name of md component devices
- Devices []string
+ // component devices
+ Devices []MDStatComponent
}
// MDStat parses an mdstat-file (/proc/mdstat) and returns a slice of
@@ -80,28 +101,52 @@ func (fs FS) MDStat() ([]MDStat, error) {
// parseMDStat parses data from mdstat file (/proc/mdstat) and returns a slice of
// structs containing the relevant info.
func parseMDStat(mdStatData []byte) ([]MDStat, error) {
+ // TODO:
+ // - parse global hotspares from the "unused devices" line.
mdStats := []MDStat{}
lines := strings.Split(string(mdStatData), "\n")
+ knownRaidTypes := make(map[string]bool)
for i, line := range lines {
if strings.TrimSpace(line) == "" || line[0] == ' ' ||
- strings.HasPrefix(line, "Personalities") ||
strings.HasPrefix(line, "unused") {
continue
}
+ // Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10]
+ if len(knownRaidTypes) == 0 && strings.HasPrefix(line, personalitiesPrefix) {
+ personalities := strings.Fields(line[len(personalitiesPrefix):])
+ for _, word := range personalities {
+ word := word[1 : len(word)-1]
+ knownRaidTypes[word] = true
+ }
+ continue
+ }
deviceFields := strings.Fields(line)
if len(deviceFields) < 3 {
return nil, fmt.Errorf("%w: Expected 3+ lines, got %q", ErrFileParse, line)
}
mdName := deviceFields[0] // mdx
- state := deviceFields[2] // active or inactive
+ state := deviceFields[2] // active, inactive, broken
+
+ mdType := "unknown" // raid1, raid5, etc.
+ var deviceStartIndex int
+ if len(deviceFields) > 3 { // mdType may be in the 3rd or 4th field
+ if isRaidType(deviceFields[3], knownRaidTypes) {
+ mdType = deviceFields[3]
+ deviceStartIndex = 4
+ } else if len(deviceFields) > 4 && isRaidType(deviceFields[4], knownRaidTypes) {
+ // if the 3rd field is (...), the 4th field is the mdType
+ mdType = deviceFields[4]
+ deviceStartIndex = 5
+ }
+ }
if len(lines) <= i+3 {
return nil, fmt.Errorf("%w: Too few lines for md device: %q", ErrFileParse, mdName)
}
- // Failed disks have the suffix (F) & Spare disks have the suffix (S).
+ // Failed (Faulty) disks have the suffix (F) & Spare disks have the suffix (S).
fail := int64(strings.Count(line, "(F)"))
spare := int64(strings.Count(line, "(S)"))
active, total, down, size, err := evalStatusLine(lines[i], lines[i+1])
@@ -129,13 +174,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
// Append recovery and resyncing state info.
if recovering || resyncing || checking || reshaping {
- if recovering {
+ switch {
+ case recovering:
state = "recovering"
- } else if reshaping {
+ case reshaping:
state = "reshaping"
- } else if checking {
+ case checking:
state = "checking"
- } else {
+ default:
state = "resyncing"
}
@@ -151,8 +197,14 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
}
}
+ devices, err := evalComponentDevices(deviceFields[deviceStartIndex:])
+ if err != nil {
+ return nil, fmt.Errorf("error parsing components in md device %q: %w", mdName, err)
+ }
+
mdStats = append(mdStats, MDStat{
Name: mdName,
+ Type: mdType,
ActivityState: state,
DisksActive: active,
DisksFailed: fail,
@@ -165,14 +217,24 @@ func parseMDStat(mdStatData []byte) ([]MDStat, error) {
BlocksSyncedPct: pct,
BlocksSyncedFinishTime: finish,
BlocksSyncedSpeed: speed,
- Devices: evalComponentDevices(deviceFields),
+ Devices: devices,
})
}
return mdStats, nil
}
+// check if a string's format is like the mdType
+// Rule 1: mdType should not be like (...)
+// Rule 2: mdType should not be like sda[0]
+// .
+func isRaidType(mdType string, knownRaidTypes map[string]bool) bool {
+ _, ok := knownRaidTypes[mdType]
+ return !strings.ContainsAny(mdType, "([") && ok
+}
+
func evalStatusLine(deviceLine, statusLine string) (active, total, down, size int64, err error) {
+ // e.g. 523968 blocks super 1.2 [4/4] [UUUU]
statusFields := strings.Fields(statusLine)
if len(statusFields) < 1 {
return 0, 0, 0, 0, fmt.Errorf("%w: Unexpected statusline %q: %w", ErrFileParse, statusLine, err)
@@ -263,17 +325,29 @@ func evalRecoveryLine(recoveryLine string) (blocksSynced int64, blocksToBeSynced
return blocksSynced, blocksToBeSynced, pct, finish, speed, nil
}
-func evalComponentDevices(deviceFields []string) []string {
- mdComponentDevices := make([]string, 0)
- if len(deviceFields) > 3 {
- for _, field := range deviceFields[4:] {
- match := componentDeviceRE.FindStringSubmatch(field)
- if match == nil {
- continue
- }
- mdComponentDevices = append(mdComponentDevices, match[1])
+func evalComponentDevices(deviceFields []string) ([]MDStatComponent, error) {
+ mdComponentDevices := make([]MDStatComponent, 0)
+ for _, field := range deviceFields {
+ match := componentDeviceRE.FindStringSubmatch(field)
+ if match == nil {
+ continue
}
+ descriptorIndex, err := strconv.ParseInt(match[2], 10, 32)
+ if err != nil {
+ return mdComponentDevices, fmt.Errorf("error parsing int from device %q: %w", match[2], err)
+ }
+ mdComponentDevices = append(mdComponentDevices, MDStatComponent{
+ Name: match[1],
+ DescriptorIndex: int32(descriptorIndex),
+ // match may contain one or more of these
+ // https://github.com/torvalds/linux/blob/7ec462100ef9142344ddbf86f2c3008b97acddbe/drivers/md/md.c#L8376-L8392
+ Faulty: strings.Contains(match[3], "(F)"),
+ Spare: strings.Contains(match[3], "(S)"),
+ Journal: strings.Contains(match[3], "(J)"),
+ Replacement: strings.Contains(match[3], "(R)"),
+ WriteMostly: strings.Contains(match[3], "(W)"),
+ })
}
- return mdComponentDevices
+ return mdComponentDevices, nil
}
diff --git a/vendor/github.com/prometheus/procfs/meminfo.go b/vendor/github.com/prometheus/procfs/meminfo.go
index 937e1f9606..3420383187 100644
--- a/vendor/github.com/prometheus/procfs/meminfo.go
+++ b/vendor/github.com/prometheus/procfs/meminfo.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -307,7 +307,7 @@ func parseMemInfo(r io.Reader) (*Meminfo, error) {
m.ZswapBytes = &valBytes
case "Zswapped:":
m.Zswapped = &val
- m.ZswapBytes = &valBytes
+ m.ZswappedBytes = &valBytes
case "Dirty:":
m.Dirty = &val
m.DirtyBytes = &valBytes
diff --git a/vendor/github.com/prometheus/procfs/mountinfo.go b/vendor/github.com/prometheus/procfs/mountinfo.go
index a704c5e735..9414a12f42 100644
--- a/vendor/github.com/prometheus/procfs/mountinfo.go
+++ b/vendor/github.com/prometheus/procfs/mountinfo.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -147,8 +147,7 @@ func mountOptionsParseOptionalFields(o []string) (map[string]string, error) {
// mountOptionsParser parses the mount options, superblock options.
func mountOptionsParser(mountOptions string) map[string]string {
opts := make(map[string]string)
- options := strings.Split(mountOptions, ",")
- for _, opt := range options {
+ for opt := range strings.SplitSeq(mountOptions, ",") {
splitOption := strings.Split(opt, "=")
if len(splitOption) < 2 {
key := splitOption[0]
@@ -178,3 +177,21 @@ func GetProcMounts(pid int) ([]*MountInfo, error) {
}
return parseMountInfo(data)
}
+
+// GetMounts retrieves mountinfo information from `/proc/self/mountinfo`.
+func (fs FS) GetMounts() ([]*MountInfo, error) {
+ data, err := util.ReadFileNoStat(fs.proc.Path("self/mountinfo"))
+ if err != nil {
+ return nil, err
+ }
+ return parseMountInfo(data)
+}
+
+// GetProcMounts retrieves mountinfo information from a processes' `/proc//mountinfo`.
+func (fs FS) GetProcMounts(pid int) ([]*MountInfo, error) {
+ data, err := util.ReadFileNoStat(fs.proc.Path(fmt.Sprintf("%d/mountinfo", pid)))
+ if err != nil {
+ return nil, err
+ }
+ return parseMountInfo(data)
+}
diff --git a/vendor/github.com/prometheus/procfs/mountstats.go b/vendor/github.com/prometheus/procfs/mountstats.go
index 50caa73274..e503cb3a6c 100644
--- a/vendor/github.com/prometheus/procfs/mountstats.go
+++ b/vendor/github.com/prometheus/procfs/mountstats.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -383,7 +383,7 @@ func parseMountStatsNFS(s *bufio.Scanner, statVersion string) (*MountStatsNFS, e
if stats.Opts == nil {
stats.Opts = map[string]string{}
}
- for _, opt := range strings.Split(ss[1], ",") {
+ for opt := range strings.SplitSeq(ss[1], ",") {
split := strings.Split(opt, "=")
if len(split) == 2 {
stats.Opts[split[0]] = split[1]
diff --git a/vendor/github.com/prometheus/procfs/net_conntrackstat.go b/vendor/github.com/prometheus/procfs/net_conntrackstat.go
index 316df5fbb7..e9ca357079 100644
--- a/vendor/github.com/prometheus/procfs/net_conntrackstat.go
+++ b/vendor/github.com/prometheus/procfs/net_conntrackstat.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_dev.go b/vendor/github.com/prometheus/procfs/net_dev.go
index e66208aa05..7b3e1d61c9 100644
--- a/vendor/github.com/prometheus/procfs/net_dev.go
+++ b/vendor/github.com/prometheus/procfs/net_dev.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_dev_snmp6.go b/vendor/github.com/prometheus/procfs/net_dev_snmp6.go
index f50b38e352..2a0f60f29f 100644
--- a/vendor/github.com/prometheus/procfs/net_dev_snmp6.go
+++ b/vendor/github.com/prometheus/procfs/net_dev_snmp6.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -18,6 +18,7 @@ import (
"errors"
"io"
"os"
+ "path/filepath"
"strconv"
"strings"
)
@@ -56,7 +57,9 @@ func newNetDevSNMP6(dir string) (NetDevSNMP6, error) {
}
for _, iFaceFile := range ifaceFiles {
- f, err := os.Open(dir + "/" + iFaceFile.Name())
+ filePath := filepath.Join(dir, iFaceFile.Name())
+
+ f, err := os.Open(filePath)
if err != nil {
return netDevSNMP6, err
}
diff --git a/vendor/github.com/prometheus/procfs/net_ip_socket.go b/vendor/github.com/prometheus/procfs/net_ip_socket.go
index 19e3378f72..9291f8cd4c 100644
--- a/vendor/github.com/prometheus/procfs/net_ip_socket.go
+++ b/vendor/github.com/prometheus/procfs/net_ip_socket.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_protocols.go b/vendor/github.com/prometheus/procfs/net_protocols.go
index 8d4b1ac05b..eaa996cbcf 100644
--- a/vendor/github.com/prometheus/procfs/net_protocols.go
+++ b/vendor/github.com/prometheus/procfs/net_protocols.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -169,7 +169,7 @@ func (pc *NetProtocolCapabilities) parseCapabilities(capabilities []string) erro
&pc.EnterMemoryPressure,
}
- for i := 0; i < len(capabilities); i++ {
+ for i := range capabilities {
switch capabilities[i] {
case "y":
*capabilityFields[i] = true
diff --git a/vendor/github.com/prometheus/procfs/net_route.go b/vendor/github.com/prometheus/procfs/net_route.go
index deb7029fe1..fa3812d9d0 100644
--- a/vendor/github.com/prometheus/procfs/net_route.go
+++ b/vendor/github.com/prometheus/procfs/net_route.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_sockstat.go b/vendor/github.com/prometheus/procfs/net_sockstat.go
index fae62b13d9..8b221ebfff 100644
--- a/vendor/github.com/prometheus/procfs/net_sockstat.go
+++ b/vendor/github.com/prometheus/procfs/net_sockstat.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -139,9 +139,6 @@ func parseSockstatKVs(kvs []string) (map[string]int, error) {
func parseSockstatProtocol(kvs map[string]int) NetSockstatProtocol {
var nsp NetSockstatProtocol
for k, v := range kvs {
- // Capture the range variable to ensure we get unique pointers for
- // each of the optional fields.
- v := v
switch k {
case "inuse":
nsp.InUse = v
diff --git a/vendor/github.com/prometheus/procfs/net_softnet.go b/vendor/github.com/prometheus/procfs/net_softnet.go
index 71c8059f4d..4a2dfa18fd 100644
--- a/vendor/github.com/prometheus/procfs/net_softnet.go
+++ b/vendor/github.com/prometheus/procfs/net_softnet.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_tcp.go b/vendor/github.com/prometheus/procfs/net_tcp.go
index 0396d72015..2c7f9bc7c3 100644
--- a/vendor/github.com/prometheus/procfs/net_tcp.go
+++ b/vendor/github.com/prometheus/procfs/net_tcp.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -25,6 +25,7 @@ type (
// NetTCP returns the IPv4 kernel/networking statistics for TCP datagrams
// read from /proc/net/tcp.
+//
// Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET) instead.
func (fs FS) NetTCP() (NetTCP, error) {
return newNetTCP(fs.proc.Path("net/tcp"))
@@ -32,6 +33,7 @@ func (fs FS) NetTCP() (NetTCP, error) {
// NetTCP6 returns the IPv6 kernel/networking statistics for TCP datagrams
// read from /proc/net/tcp6.
+//
// Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET6) instead.
func (fs FS) NetTCP6() (NetTCP, error) {
return newNetTCP(fs.proc.Path("net/tcp6"))
@@ -39,6 +41,7 @@ func (fs FS) NetTCP6() (NetTCP, error) {
// NetTCPSummary returns already computed statistics like the total queue lengths
// for TCP datagrams read from /proc/net/tcp.
+//
// Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET) instead.
func (fs FS) NetTCPSummary() (*NetTCPSummary, error) {
return newNetTCPSummary(fs.proc.Path("net/tcp"))
@@ -46,6 +49,7 @@ func (fs FS) NetTCPSummary() (*NetTCPSummary, error) {
// NetTCP6Summary returns already computed statistics like the total queue lengths
// for TCP datagrams read from /proc/net/tcp6.
+//
// Deprecated: Use github.com/mdlayher/netlink#Conn (with syscall.AF_INET6) instead.
func (fs FS) NetTCP6Summary() (*NetTCPSummary, error) {
return newNetTCPSummary(fs.proc.Path("net/tcp6"))
diff --git a/vendor/github.com/prometheus/procfs/net_tls_stat.go b/vendor/github.com/prometheus/procfs/net_tls_stat.go
index 13994c1782..b1b3f6a6a2 100644
--- a/vendor/github.com/prometheus/procfs/net_tls_stat.go
+++ b/vendor/github.com/prometheus/procfs/net_tls_stat.go
@@ -1,4 +1,4 @@
-// Copyright 2023 Prometheus Team
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_udp.go b/vendor/github.com/prometheus/procfs/net_udp.go
index 9ac3daf2d4..8a32779102 100644
--- a/vendor/github.com/prometheus/procfs/net_udp.go
+++ b/vendor/github.com/prometheus/procfs/net_udp.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_unix.go b/vendor/github.com/prometheus/procfs/net_unix.go
index d7e0cacb4c..e4d6359236 100644
--- a/vendor/github.com/prometheus/procfs/net_unix.go
+++ b/vendor/github.com/prometheus/procfs/net_unix.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_wireless.go b/vendor/github.com/prometheus/procfs/net_wireless.go
index 7c597bc870..69d0794451 100644
--- a/vendor/github.com/prometheus/procfs/net_wireless.go
+++ b/vendor/github.com/prometheus/procfs/net_wireless.go
@@ -1,4 +1,4 @@
-// Copyright 2023 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/net_xfrm.go b/vendor/github.com/prometheus/procfs/net_xfrm.go
index 932ef20468..5a9f497d19 100644
--- a/vendor/github.com/prometheus/procfs/net_xfrm.go
+++ b/vendor/github.com/prometheus/procfs/net_xfrm.go
@@ -1,4 +1,4 @@
-// Copyright 2017 Prometheus Team
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/netstat.go b/vendor/github.com/prometheus/procfs/netstat.go
index 742dff453b..dbdae47392 100644
--- a/vendor/github.com/prometheus/procfs/netstat.go
+++ b/vendor/github.com/prometheus/procfs/netstat.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/nfnetlink_queue.go b/vendor/github.com/prometheus/procfs/nfnetlink_queue.go
new file mode 100644
index 0000000000..b0a73b11e9
--- /dev/null
+++ b/vendor/github.com/prometheus/procfs/nfnetlink_queue.go
@@ -0,0 +1,85 @@
+// Copyright The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package procfs
+
+import (
+ "bufio"
+ "bytes"
+ "fmt"
+
+ "github.com/prometheus/procfs/internal/util"
+)
+
+const nfNetLinkQueueFormat = "%d %d %d %d %d %d %d %d %d"
+
+// NFNetLinkQueue contains general information about netfilter queues found in /proc/net/netfilter/nfnetlink_queue.
+type NFNetLinkQueue struct {
+ // id of the queue
+ QueueID uint
+ // pid of process handling the queue
+ PeerPID uint
+ // number of packets waiting for a decision
+ QueueTotal uint
+ // indicate how userspace receive packets
+ CopyMode uint
+ // size of copy
+ CopyRange uint
+ // number of items dropped by the kernel because too many packets were waiting a decision.
+ // It queue_total is superior to queue_max_len (1024 per default) the packets are dropped.
+ QueueDropped uint
+ // number of packets dropped by userspace (due to kernel send failure on the netlink socket)
+ QueueUserDropped uint
+ // sequence number of packets queued. It gives a correct approximation of the number of queued packets.
+ SequenceID uint
+ // internal value (number of entity using the queue)
+ Use uint
+}
+
+// NFNetLinkQueue returns information about current state of netfilter queues.
+func (fs FS) NFNetLinkQueue() ([]NFNetLinkQueue, error) {
+ data, err := util.ReadFileNoStat(fs.proc.Path("net/netfilter/nfnetlink_queue"))
+ if err != nil {
+ return nil, err
+ }
+
+ queue := []NFNetLinkQueue{}
+ if len(data) == 0 {
+ return queue, nil
+ }
+
+ scanner := bufio.NewScanner(bytes.NewReader(data))
+ for scanner.Scan() {
+ line := scanner.Text()
+ nFNetLinkQueue, err := parseNFNetLinkQueueLine(line)
+ if err != nil {
+ return nil, err
+ }
+ queue = append(queue, *nFNetLinkQueue)
+ }
+ return queue, nil
+}
+
+// parseNFNetLinkQueueLine parses each line of the /proc/net/netfilter/nfnetlink_queue file.
+func parseNFNetLinkQueueLine(line string) (*NFNetLinkQueue, error) {
+ nFNetLinkQueue := NFNetLinkQueue{}
+ _, err := fmt.Sscanf(
+ line, nfNetLinkQueueFormat,
+ &nFNetLinkQueue.QueueID, &nFNetLinkQueue.PeerPID, &nFNetLinkQueue.QueueTotal, &nFNetLinkQueue.CopyMode,
+ &nFNetLinkQueue.CopyRange, &nFNetLinkQueue.QueueDropped, &nFNetLinkQueue.QueueUserDropped, &nFNetLinkQueue.SequenceID, &nFNetLinkQueue.Use,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return &nFNetLinkQueue, nil
+}
diff --git a/vendor/github.com/prometheus/procfs/proc.go b/vendor/github.com/prometheus/procfs/proc.go
index 368187fa88..39c14aa55e 100644
--- a/vendor/github.com/prometheus/procfs/proc.go
+++ b/vendor/github.com/prometheus/procfs/proc.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -49,7 +49,7 @@ func (p Procs) Less(i, j int) bool { return p[i].PID < p[j].PID }
// Self returns a process for the current process read via /proc/self.
func Self() (Proc, error) {
fs, err := NewFS(DefaultMountPoint)
- if err != nil || errors.Unwrap(err) == ErrMountPoint {
+ if err != nil || errors.Is(err, ErrMountPoint) {
return Proc{}, err
}
return fs.Self()
diff --git a/vendor/github.com/prometheus/procfs/proc_cgroup.go b/vendor/github.com/prometheus/procfs/proc_cgroup.go
index 4a64347c03..535c08d6fc 100644
--- a/vendor/github.com/prometheus/procfs/proc_cgroup.go
+++ b/vendor/github.com/prometheus/procfs/proc_cgroup.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_cgroups.go b/vendor/github.com/prometheus/procfs/proc_cgroups.go
index 5dd4938999..0b275c3b1f 100644
--- a/vendor/github.com/prometheus/procfs/proc_cgroups.go
+++ b/vendor/github.com/prometheus/procfs/proc_cgroups.go
@@ -1,4 +1,4 @@
-// Copyright 2021 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -40,13 +40,13 @@ type CgroupSummary struct {
// parseCgroupSummary parses each line of the /proc/cgroup file
// Line format is `subsys_name hierarchy num_cgroups enabled`.
-func parseCgroupSummaryString(CgroupSummaryStr string) (*CgroupSummary, error) {
+func parseCgroupSummaryString(cgroupSummaryStr string) (*CgroupSummary, error) {
var err error
- fields := strings.Fields(CgroupSummaryStr)
+ fields := strings.Fields(cgroupSummaryStr)
// require at least 4 fields
if len(fields) < 4 {
- return nil, fmt.Errorf("%w: 4+ fields required, found %d fields in cgroup info string: %s", ErrFileParse, len(fields), CgroupSummaryStr)
+ return nil, fmt.Errorf("%w: 4+ fields required, found %d fields in cgroup info string: %s", ErrFileParse, len(fields), cgroupSummaryStr)
}
CgroupSummary := &CgroupSummary{
diff --git a/vendor/github.com/prometheus/procfs/proc_environ.go b/vendor/github.com/prometheus/procfs/proc_environ.go
index 57a89895d6..5b941de047 100644
--- a/vendor/github.com/prometheus/procfs/proc_environ.go
+++ b/vendor/github.com/prometheus/procfs/proc_environ.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_fdinfo.go b/vendor/github.com/prometheus/procfs/proc_fdinfo.go
index fa761b3529..fa57761dbe 100644
--- a/vendor/github.com/prometheus/procfs/proc_fdinfo.go
+++ b/vendor/github.com/prometheus/procfs/proc_fdinfo.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -60,15 +60,16 @@ func (p Proc) FDInfo(fd string) (*ProcFDInfo, error) {
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
text = scanner.Text()
- if rPos.MatchString(text) {
+ switch {
+ case rPos.MatchString(text):
pos = rPos.FindStringSubmatch(text)[1]
- } else if rFlags.MatchString(text) {
+ case rFlags.MatchString(text):
flags = rFlags.FindStringSubmatch(text)[1]
- } else if rMntID.MatchString(text) {
+ case rMntID.MatchString(text):
mntid = rMntID.FindStringSubmatch(text)[1]
- } else if rIno.MatchString(text) {
+ case rIno.MatchString(text):
ino = rIno.FindStringSubmatch(text)[1]
- } else if rInotify.MatchString(text) {
+ case rInotify.MatchString(text):
newInotify, err := parseInotifyInfo(text)
if err != nil {
return nil, err
diff --git a/vendor/github.com/prometheus/procfs/proc_interrupts.go b/vendor/github.com/prometheus/procfs/proc_interrupts.go
index 86b4b45246..643b500d5d 100644
--- a/vendor/github.com/prometheus/procfs/proc_interrupts.go
+++ b/vendor/github.com/prometheus/procfs/proc_interrupts.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -42,7 +42,7 @@ type Interrupts map[string]Interrupt
// Interrupts creates a new instance from a given Proc instance.
func (p Proc) Interrupts() (Interrupts, error) {
- data, err := util.ReadFileNoStat(p.path("interrupts"))
+ data, err := util.ReadFileNoStat(p.fs.proc.Path("interrupts"))
if err != nil {
return nil, err
}
diff --git a/vendor/github.com/prometheus/procfs/proc_io.go b/vendor/github.com/prometheus/procfs/proc_io.go
index d15b66ddb6..dd8086ba2e 100644
--- a/vendor/github.com/prometheus/procfs/proc_io.go
+++ b/vendor/github.com/prometheus/procfs/proc_io.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_limits.go b/vendor/github.com/prometheus/procfs/proc_limits.go
index 9530b14bc6..4b7d337847 100644
--- a/vendor/github.com/prometheus/procfs/proc_limits.go
+++ b/vendor/github.com/prometheus/procfs/proc_limits.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -19,6 +19,7 @@ import (
"os"
"regexp"
"strconv"
+ "strings"
)
// ProcLimits represents the soft limits for each of the process's resource
@@ -74,7 +75,7 @@ const (
)
var (
- limitsMatch = regexp.MustCompile(`(Max \w+\s{0,1}?\w*\s{0,1}\w*)\s{2,}(\w+)\s+(\w+)`)
+ limitsMatch = regexp.MustCompile(`(Max \w+\s??\w*\s?\w*)\s{2,}(\w+)\s+(\w+)`)
)
// NewLimits returns the current soft limits of the process.
@@ -106,7 +107,7 @@ func (p Proc) Limits() (ProcLimits, error) {
return ProcLimits{}, fmt.Errorf("%w: couldn't parse %q line %q", ErrFileParse, f.Name(), s.Text())
}
- switch fields[1] {
+ switch strings.TrimSpace(fields[1]) {
case "Max cpu time":
l.CPUTime, err = parseUint(fields[2])
case "Max file size":
diff --git a/vendor/github.com/prometheus/procfs/proc_maps.go b/vendor/github.com/prometheus/procfs/proc_maps.go
index 7e75c286b5..08b89a6eb9 100644
--- a/vendor/github.com/prometheus/procfs/proc_maps.go
+++ b/vendor/github.com/prometheus/procfs/proc_maps.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,8 +12,6 @@
// limitations under the License.
//go:build (aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris) && !js
-// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
-// +build !js
package procfs
diff --git a/vendor/github.com/prometheus/procfs/proc_netstat.go b/vendor/github.com/prometheus/procfs/proc_netstat.go
index 4248c1716e..7f94cc8914 100644
--- a/vendor/github.com/prometheus/procfs/proc_netstat.go
+++ b/vendor/github.com/prometheus/procfs/proc_netstat.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_ns.go b/vendor/github.com/prometheus/procfs/proc_ns.go
index 0f8f847f95..5fc0eb9e2f 100644
--- a/vendor/github.com/prometheus/procfs/proc_ns.go
+++ b/vendor/github.com/prometheus/procfs/proc_ns.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_psi.go b/vendor/github.com/prometheus/procfs/proc_psi.go
index ccd35f153a..cc2c5de873 100644
--- a/vendor/github.com/prometheus/procfs/proc_psi.go
+++ b/vendor/github.com/prometheus/procfs/proc_psi.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_smaps.go b/vendor/github.com/prometheus/procfs/proc_smaps.go
index 9a297afcf8..f637309b3d 100644
--- a/vendor/github.com/prometheus/procfs/proc_smaps.go
+++ b/vendor/github.com/prometheus/procfs/proc_smaps.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
-// +build !windows
package procfs
diff --git a/vendor/github.com/prometheus/procfs/proc_snmp.go b/vendor/github.com/prometheus/procfs/proc_snmp.go
index 4bdc90b07e..8d9a9bcd67 100644
--- a/vendor/github.com/prometheus/procfs/proc_snmp.go
+++ b/vendor/github.com/prometheus/procfs/proc_snmp.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_snmp6.go b/vendor/github.com/prometheus/procfs/proc_snmp6.go
index fb7fd3995b..841fef4649 100644
--- a/vendor/github.com/prometheus/procfs/proc_snmp6.go
+++ b/vendor/github.com/prometheus/procfs/proc_snmp6.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_stat.go b/vendor/github.com/prometheus/procfs/proc_stat.go
index 3328556bdc..02e3f9e316 100644
--- a/vendor/github.com/prometheus/procfs/proc_stat.go
+++ b/vendor/github.com/prometheus/procfs/proc_stat.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/proc_statm.go b/vendor/github.com/prometheus/procfs/proc_statm.go
index ed57984243..6bcc97ec9c 100644
--- a/vendor/github.com/prometheus/procfs/proc_statm.go
+++ b/vendor/github.com/prometheus/procfs/proc_statm.go
@@ -1,4 +1,4 @@
-// Copyright 2025 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -45,6 +45,7 @@ type ProcStatm struct {
}
// NewStatm returns the current status information of the process.
+//
// Deprecated: Use p.Statm() instead.
func (p Proc) NewStatm() (ProcStatm, error) {
return p.Statm()
@@ -80,7 +81,7 @@ func (p Proc) Statm() (ProcStatm, error) {
func parseStatm(data []byte) ([]uint64, error) {
var statmSlice []uint64
statmItems := strings.Fields(string(data))
- for i := 0; i < len(statmItems); i++ {
+ for i := range statmItems {
statmItem, err := strconv.ParseUint(statmItems[i], 10, 64)
if err != nil {
return nil, err
diff --git a/vendor/github.com/prometheus/procfs/proc_status.go b/vendor/github.com/prometheus/procfs/proc_status.go
index dd8aa56885..12d65581c8 100644
--- a/vendor/github.com/prometheus/procfs/proc_status.go
+++ b/vendor/github.com/prometheus/procfs/proc_status.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,7 +16,7 @@ package procfs
import (
"bytes"
"math/bits"
- "sort"
+ "slices"
"strconv"
"strings"
@@ -83,6 +83,19 @@ type ProcStatus struct {
// CpusAllowedList: List of cpu cores processes are allowed to run on.
CpusAllowedList []uint64
+
+ // CapInh is the bitmap of inheritable capabilities
+ //
+ // See: https://www.kernel.org/doc/man-pages/online/pages/man7/capabilities.7.html
+ CapInh uint64
+ // CapPrm is the bitmap of permitted capabilities
+ CapPrm uint64
+ // CapEff is the bitmap of effective capabilities
+ CapEff uint64
+ // CapBnd is the bitmap of bounding capabilities
+ CapBnd uint64
+ // CapAmb is the bitmap of ambient capabilities
+ CapAmb uint64
}
// NewStatus returns the current status information of the process.
@@ -94,8 +107,7 @@ func (p Proc) NewStatus() (ProcStatus, error) {
s := ProcStatus{PID: p.PID}
- lines := strings.Split(string(data), "\n")
- for _, line := range lines {
+ for line := range strings.SplitSeq(string(data), "\n") {
if !bytes.Contains([]byte(line), []byte(":")) {
continue
}
@@ -191,6 +203,36 @@ func (s *ProcStatus) fillStatus(k string, vString string, vUint uint64, vUintByt
s.NonVoluntaryCtxtSwitches = vUint
case "Cpus_allowed_list":
s.CpusAllowedList = calcCpusAllowedList(vString)
+ case "CapInh":
+ var err error
+ s.CapInh, err = strconv.ParseUint(vString, 16, 64)
+ if err != nil {
+ return err
+ }
+ case "CapPrm":
+ var err error
+ s.CapPrm, err = strconv.ParseUint(vString, 16, 64)
+ if err != nil {
+ return err
+ }
+ case "CapEff":
+ var err error
+ s.CapEff, err = strconv.ParseUint(vString, 16, 64)
+ if err != nil {
+ return err
+ }
+ case "CapBnd":
+ var err error
+ s.CapBnd, err = strconv.ParseUint(vString, 16, 64)
+ if err != nil {
+ return err
+ }
+ case "CapAmb":
+ var err error
+ s.CapAmb, err = strconv.ParseUint(vString, 16, 64)
+ if err != nil {
+ return err
+ }
}
return nil
@@ -222,7 +264,7 @@ func calcCpusAllowedList(cpuString string) []uint64 {
}
- sort.Slice(g, func(i, j int) bool { return g[i] < g[j] })
+ slices.Sort(g)
return g
}
diff --git a/vendor/github.com/prometheus/procfs/proc_sys.go b/vendor/github.com/prometheus/procfs/proc_sys.go
index 3810d1ac99..52658a4d52 100644
--- a/vendor/github.com/prometheus/procfs/proc_sys.go
+++ b/vendor/github.com/prometheus/procfs/proc_sys.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/schedstat.go b/vendor/github.com/prometheus/procfs/schedstat.go
index 5f7f32dc83..fafd8dff74 100644
--- a/vendor/github.com/prometheus/procfs/schedstat.go
+++ b/vendor/github.com/prometheus/procfs/schedstat.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/slab.go b/vendor/github.com/prometheus/procfs/slab.go
index 8611c90177..32a04678ad 100644
--- a/vendor/github.com/prometheus/procfs/slab.go
+++ b/vendor/github.com/prometheus/procfs/slab.go
@@ -1,4 +1,4 @@
-// Copyright 2020 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/softirqs.go b/vendor/github.com/prometheus/procfs/softirqs.go
index 403e6ae708..47b73a7297 100644
--- a/vendor/github.com/prometheus/procfs/softirqs.go
+++ b/vendor/github.com/prometheus/procfs/softirqs.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/stat.go b/vendor/github.com/prometheus/procfs/stat.go
index e36b41c18a..593ad0f62f 100644
--- a/vendor/github.com/prometheus/procfs/stat.go
+++ b/vendor/github.com/prometheus/procfs/stat.go
@@ -1,4 +1,4 @@
-// Copyright 2018 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -16,6 +16,7 @@ package procfs
import (
"bufio"
"bytes"
+ "errors"
"fmt"
"io"
"strconv"
@@ -92,7 +93,7 @@ func parseCPUStat(line string) (CPUStat, int64, error) {
&cpuStat.Iowait, &cpuStat.IRQ, &cpuStat.SoftIRQ, &cpuStat.Steal,
&cpuStat.Guest, &cpuStat.GuestNice)
- if err != nil && err != io.EOF {
+ if err != nil && !errors.Is(err, io.EOF) {
return CPUStat{}, -1, fmt.Errorf("%w: couldn't parse %q (cpu): %w", ErrFileParse, line, err)
}
if count == 0 {
diff --git a/vendor/github.com/prometheus/procfs/swaps.go b/vendor/github.com/prometheus/procfs/swaps.go
index 65fec834bf..ee17bf4888 100644
--- a/vendor/github.com/prometheus/procfs/swaps.go
+++ b/vendor/github.com/prometheus/procfs/swaps.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/thread.go b/vendor/github.com/prometheus/procfs/thread.go
index 80e0e947be..0cfbb54184 100644
--- a/vendor/github.com/prometheus/procfs/thread.go
+++ b/vendor/github.com/prometheus/procfs/thread.go
@@ -1,4 +1,4 @@
-// Copyright 2022 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
diff --git a/vendor/github.com/prometheus/procfs/vm.go b/vendor/github.com/prometheus/procfs/vm.go
index 51c49d89e8..52180c03e2 100644
--- a/vendor/github.com/prometheus/procfs/vm.go
+++ b/vendor/github.com/prometheus/procfs/vm.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
-// +build !windows
package procfs
diff --git a/vendor/github.com/prometheus/procfs/zoneinfo.go b/vendor/github.com/prometheus/procfs/zoneinfo.go
index e54d94b090..63d1898bc8 100644
--- a/vendor/github.com/prometheus/procfs/zoneinfo.go
+++ b/vendor/github.com/prometheus/procfs/zoneinfo.go
@@ -1,4 +1,4 @@
-// Copyright 2019 The Prometheus Authors
+// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
@@ -12,7 +12,6 @@
// limitations under the License.
//go:build !windows
-// +build !windows
package procfs
@@ -88,11 +87,9 @@ func parseZoneinfo(zoneinfoData []byte) ([]Zoneinfo, error) {
zoneinfo := []Zoneinfo{}
- zoneinfoBlocks := bytes.Split(zoneinfoData, []byte("\nNode"))
- for _, block := range zoneinfoBlocks {
+ for block := range bytes.SplitSeq(zoneinfoData, []byte("\nNode")) {
var zoneinfoElement Zoneinfo
- lines := strings.Split(string(block), "\n")
- for _, line := range lines {
+ for line := range strings.SplitSeq(string(block), "\n") {
if nodeZone := nodeZoneRE.FindStringSubmatch(line); nodeZone != nil {
zoneinfoElement.Node = nodeZone[1]
diff --git a/vendor/github.com/valyala/fastjson/parser.go b/vendor/github.com/valyala/fastjson/parser.go
index 885e1841ef..f9f5ce4989 100644
--- a/vendor/github.com/valyala/fastjson/parser.go
+++ b/vendor/github.com/valyala/fastjson/parser.go
@@ -32,7 +32,7 @@ func (p *Parser) Parse(s string) (*Value, error) {
p.b = append(p.b[:0], s...)
p.c.reset()
- v, tail, err := parseValue(b2s(p.b), &p.c, 0)
+ v, tail, err := p.c.parseValue(b2s(p.b), 0)
if err != nil {
return nil, fmt.Errorf("cannot parse JSON: %s; unparsed tail: %q", err, startEndString(tail))
}
@@ -57,7 +57,11 @@ type cache struct {
}
func (c *cache) reset() {
- c.vs = c.vs[:0]
+ vs := c.vs
+ for i := range vs {
+ vs[i].reset()
+ }
+ c.vs = vs[:0]
}
func (c *cache) getValue() *Value {
@@ -66,7 +70,6 @@ func (c *cache) getValue() *Value {
} else {
c.vs = append(c.vs, Value{})
}
- // Do not reset the value, since the caller must properly init it.
return &c.vs[len(c.vs)-1]
}
@@ -98,7 +101,7 @@ type kv struct {
// MaxDepth is the maximum depth for nested JSON.
const MaxDepth = 300
-func parseValue(s string, c *cache, depth int) (*Value, string, error) {
+func (c *cache) parseValue(s string, depth int) (*Value, string, error) {
if len(s) == 0 {
return nil, s, fmt.Errorf("cannot parse empty string")
}
@@ -108,14 +111,14 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) {
}
if s[0] == '{' {
- v, tail, err := parseObject(s[1:], c, depth)
+ v, tail, err := c.parseObject(s[1:], depth)
if err != nil {
return nil, tail, fmt.Errorf("cannot parse object: %s", err)
}
return v, tail, nil
}
if s[0] == '[' {
- v, tail, err := parseArray(s[1:], c, depth)
+ v, tail, err := c.parseArray(s[1:], depth)
if err != nil {
return nil, tail, fmt.Errorf("cannot parse array: %s", err)
}
@@ -167,7 +170,7 @@ func parseValue(s string, c *cache, depth int) (*Value, string, error) {
return v, tail, nil
}
-func parseArray(s string, c *cache, depth int) (*Value, string, error) {
+func (c *cache) parseArray(s string, depth int) (*Value, string, error) {
s = skipWS(s)
if len(s) == 0 {
return nil, s, fmt.Errorf("missing ']'")
@@ -188,7 +191,7 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) {
var err error
s = skipWS(s)
- v, s, err = parseValue(s, c, depth)
+ v, s, err = c.parseValue(s, depth)
if err != nil {
return nil, s, fmt.Errorf("cannot parse array value: %s", err)
}
@@ -210,7 +213,7 @@ func parseArray(s string, c *cache, depth int) (*Value, string, error) {
}
}
-func parseObject(s string, c *cache, depth int) (*Value, string, error) {
+func (c *cache) parseObject(s string, depth int) (*Value, string, error) {
s = skipWS(s)
if len(s) == 0 {
return nil, s, fmt.Errorf("missing '}'")
@@ -247,7 +250,7 @@ func parseObject(s string, c *cache, depth int) (*Value, string, error) {
// Parse value
s = skipWS(s)
- kv.v, s, err = parseValue(s, c, depth)
+ kv.v, s, err = c.parseValue(s, depth)
if err != nil {
return nil, s, fmt.Errorf("cannot parse object value: %s", err)
}
@@ -283,7 +286,7 @@ func hasSpecialChars(s string) bool {
if strings.IndexByte(s, '"') >= 0 || strings.IndexByte(s, '\\') >= 0 {
return true
}
- for i := 0; i < len(s); i++ {
+ for i := range len(s) {
if s[i] < 0x20 {
return true
}
@@ -375,7 +378,7 @@ func unescapeStringBestEffort(s string) string {
// parseRawKey is similar to parseRawString, but is optimized
// for small-sized keys without escape sequences.
func parseRawKey(s string) (string, string, error) {
- for i := 0; i < len(s); i++ {
+ for i := range len(s) {
if s[i] == '"' {
// Fast path.
return s[:i], s[i+1:], nil
@@ -424,7 +427,7 @@ func parseRawNumber(s string) (string, string, error) {
// The caller must ensure len(s) > 0
// Find the end of the number.
- for i := 0; i < len(s); i++ {
+ for i := range len(s) {
ch := s[i]
if (ch >= '0' && ch <= '9') || ch == '.' || ch == '-' || ch == 'e' || ch == 'E' || ch == '+' {
continue
@@ -455,14 +458,19 @@ type Object struct {
}
func (o *Object) reset() {
+ // o.kvs entries can point to external byte slices. Clear these references, so GC could free memory.
+ clear(o.kvs)
o.kvs = o.kvs[:0]
+
o.keysUnescaped = false
}
// MarshalTo appends marshaled o to dst and returns the result.
func (o *Object) MarshalTo(dst []byte) []byte {
dst = append(dst, '{')
- for i, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if o.keysUnescaped {
dst = escapeString(dst, kv.k)
} else {
@@ -525,7 +533,9 @@ func (o *Object) Len() int {
func (o *Object) Get(key string) *Value {
if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 {
// Fast path - try searching for the key without object keys unescaping.
- for _, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if kv.k == key {
return kv.v
}
@@ -535,7 +545,9 @@ func (o *Object) Get(key string) *Value {
// Slow path - unescape object keys.
o.unescapeKeys()
- for _, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if kv.k == key {
return kv.v
}
@@ -554,7 +566,9 @@ func (o *Object) Visit(f func(key []byte, v *Value)) {
o.unescapeKeys()
- for _, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
f(s2b(kv.k), kv.v)
}
}
@@ -572,6 +586,16 @@ type Value struct {
t Type
}
+func (v *Value) reset() {
+ v.o.reset()
+
+ clear(v.a)
+ v.a = v.a[:0]
+
+ v.s = ""
+ v.t = 0
+}
+
// MarshalTo appends marshaled v to dst and returns the result.
func (v *Value) MarshalTo(dst []byte) []byte {
switch v.t {
diff --git a/vendor/github.com/valyala/fastjson/scanner.go b/vendor/github.com/valyala/fastjson/scanner.go
index 89b38816f0..5db3f14edb 100644
--- a/vendor/github.com/valyala/fastjson/scanner.go
+++ b/vendor/github.com/valyala/fastjson/scanner.go
@@ -65,7 +65,7 @@ func (sc *Scanner) Next() bool {
}
sc.c.reset()
- v, tail, err := parseValue(sc.s, &sc.c, 0)
+ v, tail, err := sc.c.parseValue(sc.s, 0)
if err != nil {
sc.err = err
return false
diff --git a/vendor/github.com/valyala/fastjson/update.go b/vendor/github.com/valyala/fastjson/update.go
index 795b7fbd6d..0b64e384df 100644
--- a/vendor/github.com/valyala/fastjson/update.go
+++ b/vendor/github.com/valyala/fastjson/update.go
@@ -12,7 +12,9 @@ func (o *Object) Del(key string) {
}
if !o.keysUnescaped && strings.IndexByte(key, '\\') < 0 {
// Fast path - try searching for the key without object keys unescaping.
- for i, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if kv.k == key {
o.kvs = append(o.kvs[:i], o.kvs[i+1:]...)
return
@@ -23,7 +25,9 @@ func (o *Object) Del(key string) {
// Slow path - unescape object keys before item search.
o.unescapeKeys()
- for i, kv := range o.kvs {
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if kv.k == key {
o.kvs = append(o.kvs[:i], o.kvs[i+1:]...)
return
@@ -62,8 +66,9 @@ func (o *Object) Set(key string, value *Value) {
o.unescapeKeys()
// Try substituting already existing entry with the given key.
- for i := range o.kvs {
- kv := &o.kvs[i]
+ kvs := o.kvs
+ for i := range kvs {
+ kv := &kvs[i]
if kv.k == key {
kv.v = value
return
diff --git a/vendor/github.com/valyala/fastjson/validate.go b/vendor/github.com/valyala/fastjson/validate.go
index 196f1c3dc6..8f1173cf61 100644
--- a/vendor/github.com/valyala/fastjson/validate.go
+++ b/vendor/github.com/valyala/fastjson/validate.go
@@ -51,7 +51,7 @@ func validateValue(s string) (string, error) {
return tail, fmt.Errorf("cannot parse string: %s", err)
}
// Scan the string for control chars.
- for i := 0; i < len(sv); i++ {
+ for i := range len(sv) {
if sv[i] < 0x20 {
return tail, fmt.Errorf("string cannot contain control char 0x%02X", sv[i])
}
@@ -142,7 +142,7 @@ func validateObject(s string) (string, error) {
return s, fmt.Errorf("cannot parse object key: %s", err)
}
// Scan the key for control chars.
- for i := 0; i < len(key); i++ {
+ for i := range len(key) {
if key[i] < 0x20 {
return s, fmt.Errorf("object key cannot contain control char 0x%02X", key[i])
}
@@ -177,7 +177,7 @@ func validateObject(s string) (string, error) {
// validateKey is similar to validateString, but is optimized
// for typical object keys, which are quite small and have no escape sequences.
func validateKey(s string) (string, string, error) {
- for i := 0; i < len(s); i++ {
+ for i := range len(s) {
if s[i] == '"' {
// Fast path - the key doesn't contain escape sequences.
return s[:i], s[i+1:], nil
diff --git a/vendor/github.com/vektah/gqlparser/v2/parser/schema.go b/vendor/github.com/vektah/gqlparser/v2/parser/schema.go
index 804f02c9f8..a004a0f6c0 100644
--- a/vendor/github.com/vektah/gqlparser/v2/parser/schema.go
+++ b/vendor/github.com/vektah/gqlparser/v2/parser/schema.go
@@ -231,7 +231,6 @@ func (p *parser) parseFieldsDefinition() (FieldList, *CommentGroup) {
func (p *parser) parseFieldDefinition() *FieldDefinition {
var def FieldDefinition
- def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
@@ -241,6 +240,7 @@ func (p *parser) parseFieldDefinition() *FieldDefinition {
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
+ def.Position = p.peekPos()
def.Name = p.parseName()
def.Arguments = p.parseArgumentDefs()
p.expect(lexer.Colon)
@@ -260,7 +260,6 @@ func (p *parser) parseArgumentDefs() ArgumentDefinitionList {
func (p *parser) parseArgumentDef() *ArgumentDefinition {
var def ArgumentDefinition
- def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
@@ -270,6 +269,7 @@ func (p *parser) parseArgumentDef() *ArgumentDefinition {
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
+ def.Position = p.peekPos()
def.Name = p.parseName()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
@@ -282,7 +282,6 @@ func (p *parser) parseArgumentDef() *ArgumentDefinition {
func (p *parser) parseInputValueDef() *FieldDefinition {
var def FieldDefinition
- def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
@@ -292,6 +291,7 @@ func (p *parser) parseInputValueDef() *FieldDefinition {
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
+ def.Position = p.peekPos()
def.Name = p.parseName()
p.expect(lexer.Colon)
def.Type = p.parseTypeReference()
@@ -372,7 +372,6 @@ func (p *parser) parseEnumValuesDefinition() (EnumValueList, *CommentGroup) {
func (p *parser) parseEnumValueDefinition() *EnumValueDefinition {
var def EnumValueDefinition
- def.Position = p.peekPos()
desc := p.parseDescription()
if desc.text != "" {
def.BeforeDescriptionComment = desc.comment
@@ -381,7 +380,7 @@ func (p *parser) parseEnumValueDefinition() *EnumValueDefinition {
p.peek() // peek to set p.comment
def.AfterDescriptionComment = p.comment
-
+ def.Position = p.peekPos()
def.Name = p.parseName()
def.Directives = p.parseDirectives(true)
diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go
index 95fd2298f4..5979c29980 100644
--- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go
+++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/scalar_leafs.go
@@ -6,31 +6,39 @@ import (
. "github.com/vektah/gqlparser/v2/validator/core"
)
-var ScalarLeafsRule = Rule{
- Name: "ScalarLeafs",
- RuleFunc: func(observers *Events, addError AddErrFunc) {
- observers.OnField(func(walker *Walker, field *ast.Field) {
- if field.Definition == nil {
- return
- }
+func ruleFuncScalarLeafs(observers *Events, addError AddErrFunc, disableSuggestion bool) {
+ observers.OnField(func(walker *Walker, field *ast.Field) {
+ if field.Definition == nil {
+ return
+ }
- fieldType := walker.Schema.Types[field.Definition.Type.Name()]
- if fieldType == nil {
- return
- }
+ fieldType := walker.Schema.Types[field.Definition.Type.Name()]
+ if fieldType == nil {
+ return
+ }
- if fieldType.IsLeafType() && len(field.SelectionSet) > 0 {
+ if fieldType.IsLeafType() && len(field.SelectionSet) > 0 {
+ addError(
+ Message(
+ `Field "%s" must not have a selection since type "%s" has no subfields.`,
+ field.Name,
+ fieldType.Name,
+ ),
+ At(field.Position),
+ )
+ }
+
+ if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 {
+ if disableSuggestion {
addError(
Message(
- `Field "%s" must not have a selection since type "%s" has no subfields.`,
+ `Field "%s" of type "%s" must have a selection of subfields.`,
field.Name,
- fieldType.Name,
+ field.Definition.Type.String(),
),
At(field.Position),
)
- }
-
- if !fieldType.IsLeafType() && len(field.SelectionSet) == 0 {
+ } else {
addError(
Message(
`Field "%s" of type "%s" must have a selection of subfields.`,
@@ -41,6 +49,20 @@ var ScalarLeafsRule = Rule{
At(field.Position),
)
}
- })
+ }
+ })
+}
+
+var ScalarLeafsRule = Rule{
+ Name: "ScalarLeafs",
+ RuleFunc: func(observers *Events, addError AddErrFunc) {
+ ruleFuncScalarLeafs(observers, addError, false)
+ },
+}
+
+var ScalarLeafsRuleWithoutSuggestions = Rule{
+ Name: "ScalarLeafsWithoutSuggestions",
+ RuleFunc: func(observers *Events, addError AddErrFunc) {
+ ruleFuncScalarLeafs(observers, addError, true)
},
}
diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go
index a1a0101671..b801ede378 100644
--- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go
+++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/unique_directives_per_location.go
@@ -13,7 +13,7 @@ var UniqueDirectivesPerLocationRule = Rule{
seen := map[string]bool{}
for _, dir := range directives {
- if dir.Name != "repeatable" && seen[dir.Name] {
+ if (dir.Definition == nil || !dir.Definition.IsRepeatable) && seen[dir.Name] {
addError(
Message(
`The directive "@%s" can only be used once at this location.`,
diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go
index 5126245e54..95a06099ec 100644
--- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go
+++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/values_of_correct_type.go
@@ -188,20 +188,8 @@ func ruleFuncValuesOfCorrectType(observers *Events, addError AddErrFunc, disable
return
}
- isVariable := fieldValue.Kind == ast.Variable
- if isVariable {
- variableName := fieldValue.VariableDefinition.Variable
- isNullableVariable := !fieldValue.VariableDefinition.Type.NonNull
- if isNullableVariable {
- addError(
- Message(
- `Variable "%s" must be non-nullable to be used for OneOf Input Object "%s".`,
- variableName,
- value.Definition.Name,
- ),
- At(fieldValue.Position),
- )
- }
+ if fieldValue.Kind == ast.Variable {
+ return
}
}()
}
diff --git a/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go b/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go
index d3d36a293f..a10cb9edf2 100644
--- a/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go
+++ b/vendor/github.com/vektah/gqlparser/v2/validator/rules/variables_in_allowed_position.go
@@ -44,5 +44,34 @@ var VariablesInAllowedPositionRule = Rule{
)
}
})
+
+ observers.OnValue(func(walker *Walker, value *ast.Value) {
+ if value.Kind != ast.ObjectValue || value.Definition == nil {
+ return
+ }
+ if value.Definition.Directives.ForName("oneOf") == nil {
+ return
+ }
+
+ for _, child := range value.Children {
+ fieldValue := child.Value
+ if fieldValue == nil || fieldValue.Kind != ast.Variable ||
+ fieldValue.VariableDefinition == nil {
+ continue
+ }
+ if !fieldValue.VariableDefinition.Type.NonNull {
+ addError(
+ Message(
+ `Variable "%s" is of type "%s" but must be non-nullable to be used for OneOf Input Object "%s".`,
+ fieldValue,
+ fieldValue.VariableDefinition.Type.String(),
+ value.Definition.Name,
+ ),
+ At(fieldValue.VariableDefinition.Position),
+ At(fieldValue.Position),
+ )
+ }
+ }
+ })
},
}
diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go
index 6dcf1b5b52..83c6ae2465 100644
--- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go
+++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/semconv/server.go
@@ -364,7 +364,9 @@ func (n HTTPServer) MetricAttributes(server string, req *http.Request, statusCod
if statusCode > 0 {
num++
}
-
+ if route == "" && req.Pattern != "" {
+ route = httpRoute(req.Pattern)
+ }
if route != "" {
num++
}
diff --git a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go
index 1d90fc264d..835ec5aa7e 100644
--- a/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go
+++ b/vendor/go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/version.go
@@ -4,4 +4,4 @@
package otelhttp // import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
// Version is the current release version of the otelhttp instrumentation.
-const Version = "0.67.0"
+const Version = "0.68.0"
diff --git a/vendor/modules.txt b/vendor/modules.txt
index df6ad146c1..8d929f961d 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -76,8 +76,8 @@ github.com/ProtonMail/go-crypto/openpgp/packet
github.com/ProtonMail/go-crypto/openpgp/s2k
github.com/ProtonMail/go-crypto/openpgp/x25519
github.com/ProtonMail/go-crypto/openpgp/x448
-# github.com/RoaringBitmap/roaring/v2 v2.4.5
-## explicit; go 1.15
+# github.com/RoaringBitmap/roaring/v2 v2.14.5
+## explicit; go 1.24.0
github.com/RoaringBitmap/roaring/v2
github.com/RoaringBitmap/roaring/v2/internal
github.com/RoaringBitmap/roaring/v2/roaring64
@@ -115,11 +115,11 @@ github.com/beorn7/perks/quantile
# github.com/bitly/go-simplejson v0.5.0
## explicit
github.com/bitly/go-simplejson
-# github.com/bits-and-blooms/bitset v1.22.0
+# github.com/bits-and-blooms/bitset v1.24.2
## explicit; go 1.16
github.com/bits-and-blooms/bitset
-# github.com/blevesearch/bleve/v2 v2.5.7
-## explicit; go 1.23
+# github.com/blevesearch/bleve/v2 v2.6.0
+## explicit; go 1.25.0
github.com/blevesearch/bleve/v2
github.com/blevesearch/bleve/v2/analysis
github.com/blevesearch/bleve/v2/analysis/analyzer/custom
@@ -161,18 +161,18 @@ github.com/blevesearch/bleve/v2/search/scorer
github.com/blevesearch/bleve/v2/search/searcher
github.com/blevesearch/bleve/v2/size
github.com/blevesearch/bleve/v2/util
-# github.com/blevesearch/bleve_index_api v1.2.11
-## explicit; go 1.21
+# github.com/blevesearch/bleve_index_api v1.3.11
+## explicit; go 1.24
github.com/blevesearch/bleve_index_api
-# github.com/blevesearch/geo v0.2.4
-## explicit; go 1.21.0
+# github.com/blevesearch/geo v0.2.5
+## explicit; go 1.24
github.com/blevesearch/geo/geojson
github.com/blevesearch/geo/r1
github.com/blevesearch/geo/r2
github.com/blevesearch/geo/r3
github.com/blevesearch/geo/s1
github.com/blevesearch/geo/s2
-# github.com/blevesearch/go-faiss v1.0.26
+# github.com/blevesearch/go-faiss v1.1.0
## explicit; go 1.21
github.com/blevesearch/go-faiss
# github.com/blevesearch/go-porterstemmer v1.0.3
@@ -181,11 +181,11 @@ github.com/blevesearch/go-porterstemmer
# github.com/blevesearch/gtreap v0.1.1
## explicit; go 1.13
github.com/blevesearch/gtreap
-# github.com/blevesearch/mmap-go v1.0.4
-## explicit; go 1.13
+# github.com/blevesearch/mmap-go v1.2.0
+## explicit; go 1.24.0
github.com/blevesearch/mmap-go
-# github.com/blevesearch/scorch_segment_api/v2 v2.3.13
-## explicit; go 1.21
+# github.com/blevesearch/scorch_segment_api/v2 v2.4.7
+## explicit; go 1.24.0
github.com/blevesearch/scorch_segment_api/v2
# github.com/blevesearch/segment v0.9.1
## explicit; go 1.18
@@ -197,30 +197,33 @@ github.com/blevesearch/snowballstem/english
# github.com/blevesearch/upsidedown_store_api v1.0.2
## explicit; go 1.18
github.com/blevesearch/upsidedown_store_api
-# github.com/blevesearch/vellum v1.1.0
-## explicit; go 1.21
+# github.com/blevesearch/vellum v1.2.0
+## explicit; go 1.24.0
github.com/blevesearch/vellum
github.com/blevesearch/vellum/levenshtein
github.com/blevesearch/vellum/regexp
github.com/blevesearch/vellum/utf8
-# github.com/blevesearch/zapx/v11 v11.4.2
+# github.com/blevesearch/zapx/v11 v11.4.3
## explicit; go 1.21
github.com/blevesearch/zapx/v11
-# github.com/blevesearch/zapx/v12 v12.4.2
+# github.com/blevesearch/zapx/v12 v12.4.3
## explicit; go 1.21
github.com/blevesearch/zapx/v12
-# github.com/blevesearch/zapx/v13 v13.4.2
+# github.com/blevesearch/zapx/v13 v13.4.3
## explicit; go 1.21
github.com/blevesearch/zapx/v13
-# github.com/blevesearch/zapx/v14 v14.4.2
+# github.com/blevesearch/zapx/v14 v14.4.3
## explicit; go 1.21
github.com/blevesearch/zapx/v14
-# github.com/blevesearch/zapx/v15 v15.4.2
+# github.com/blevesearch/zapx/v15 v15.4.3
## explicit; go 1.21
github.com/blevesearch/zapx/v15
-# github.com/blevesearch/zapx/v16 v16.2.8
-## explicit; go 1.23
+# github.com/blevesearch/zapx/v16 v16.3.4
+## explicit; go 1.24
github.com/blevesearch/zapx/v16
+# github.com/blevesearch/zapx/v17 v17.1.2
+## explicit; go 1.25.0
+github.com/blevesearch/zapx/v17
# github.com/bluele/gcache v0.0.2
## explicit; go 1.15
github.com/bluele/gcache
@@ -354,7 +357,7 @@ github.com/davidbyttow/govips/v2/vips
# github.com/deckarep/golang-set v1.8.0
## explicit; go 1.17
github.com/deckarep/golang-set
-# github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
+# github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1
## explicit; go 1.17
github.com/decred/dcrd/dcrec/secp256k1/v4
# github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f
@@ -423,8 +426,8 @@ github.com/fatih/color
# github.com/felixge/httpsnoop v1.0.4
## explicit; go 1.13
github.com/felixge/httpsnoop
-# github.com/fsnotify/fsnotify v1.9.0
-## explicit; go 1.17
+# github.com/fsnotify/fsnotify v1.10.1
+## explicit; go 1.23
github.com/fsnotify/fsnotify
github.com/fsnotify/fsnotify/internal
# github.com/gabriel-vasile/mimetype v1.4.13
@@ -654,7 +657,7 @@ github.com/gobwas/pool/pbytes
## explicit; go 1.15
github.com/gobwas/ws
github.com/gobwas/ws/wsutil
-# github.com/goccy/go-json v0.10.5
+# github.com/goccy/go-json v0.10.6
## explicit; go 1.19
github.com/goccy/go-json
github.com/goccy/go-json/internal/decoder
@@ -701,7 +704,7 @@ github.com/golang/groupcache/lru
github.com/golang/protobuf/jsonpb
github.com/golang/protobuf/proto
github.com/golang/protobuf/ptypes/empty
-# github.com/golang/snappy v0.0.4
+# github.com/golang/snappy v1.0.0
## explicit
github.com/golang/snappy
# github.com/google/go-cmp v0.7.0
@@ -845,7 +848,7 @@ github.com/justinas/alice
# github.com/kevinburke/ssh_config v1.2.0
## explicit
github.com/kevinburke/ssh_config
-# github.com/klauspost/compress v1.18.5
+# github.com/klauspost/compress v1.18.6
## explicit; go 1.24
github.com/klauspost/compress
github.com/klauspost/compress/flate
@@ -905,7 +908,7 @@ github.com/leonelquinteros/gotext/plurals
# github.com/lestrrat-go/blackmagic v1.0.4
## explicit; go 1.23
github.com/lestrrat-go/blackmagic
-# github.com/lestrrat-go/dsig v1.0.0
+# github.com/lestrrat-go/dsig v1.2.1
## explicit; go 1.23.0
github.com/lestrrat-go/dsig
github.com/lestrrat-go/dsig/internal/ecutil
@@ -915,20 +918,19 @@ github.com/lestrrat-go/dsig-secp256k1
# github.com/lestrrat-go/httpcc v1.0.1
## explicit; go 1.16
github.com/lestrrat-go/httpcc
-# github.com/lestrrat-go/httprc/v3 v3.0.2
+# github.com/lestrrat-go/httprc/v3 v3.0.5
## explicit; go 1.23.0
github.com/lestrrat-go/httprc/v3
github.com/lestrrat-go/httprc/v3/errsink
github.com/lestrrat-go/httprc/v3/proxysink
github.com/lestrrat-go/httprc/v3/tracesink
-# github.com/lestrrat-go/jwx/v3 v3.0.13
-## explicit; go 1.24.0
+# github.com/lestrrat-go/jwx/v3 v3.1.1
+## explicit; go 1.25.0
github.com/lestrrat-go/jwx/v3
github.com/lestrrat-go/jwx/v3/cert
github.com/lestrrat-go/jwx/v3/internal/base64
github.com/lestrrat-go/jwx/v3/internal/ecutil
github.com/lestrrat-go/jwx/v3/internal/json
-github.com/lestrrat-go/jwx/v3/internal/jwxio
github.com/lestrrat-go/jwx/v3/internal/keyconv
github.com/lestrrat-go/jwx/v3/internal/pool
github.com/lestrrat-go/jwx/v3/internal/tokens
@@ -942,6 +944,7 @@ github.com/lestrrat-go/jwx/v3/jwe/internal/keygen
github.com/lestrrat-go/jwx/v3/jwe/jwebb
github.com/lestrrat-go/jwx/v3/jwk
github.com/lestrrat-go/jwx/v3/jwk/ecdsa
+github.com/lestrrat-go/jwx/v3/jwk/internal/registry
github.com/lestrrat-go/jwx/v3/jwk/jwkbb
github.com/lestrrat-go/jwx/v3/jws
github.com/lestrrat-go/jwx/v3/jws/internal/keytype
@@ -1144,10 +1147,10 @@ github.com/mschoch/smat
# github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822
## explicit
github.com/munnerz/goautoneg
-# github.com/nats-io/jwt/v2 v2.8.1
+# github.com/nats-io/jwt/v2 v2.8.2
## explicit; go 1.25.0
github.com/nats-io/jwt/v2
-# github.com/nats-io/nats-server/v2 v2.14.0
+# github.com/nats-io/nats-server/v2 v2.14.2
## explicit; go 1.25.0
github.com/nats-io/nats-server/v2/conf
github.com/nats-io/nats-server/v2/internal/fastrand
@@ -1173,8 +1176,8 @@ github.com/nats-io/nats.go/internal/parser
github.com/nats-io/nats.go/internal/syncx
github.com/nats-io/nats.go/jetstream
github.com/nats-io/nats.go/util
-# github.com/nats-io/nkeys v0.4.15
-## explicit; go 1.24.0
+# github.com/nats-io/nkeys v0.4.16
+## explicit; go 1.25.0
github.com/nats-io/nkeys
# github.com/nats-io/nuid v1.0.1
## explicit
@@ -1275,7 +1278,7 @@ github.com/onsi/gomega/matchers/support/goraph/edge
github.com/onsi/gomega/matchers/support/goraph/node
github.com/onsi/gomega/matchers/support/goraph/util
github.com/onsi/gomega/types
-# github.com/open-policy-agent/opa v1.15.2
+# github.com/open-policy-agent/opa v1.17.1
## explicit; go 1.25.0
github.com/open-policy-agent/opa/ast
github.com/open-policy-agent/opa/ast/json
@@ -1348,6 +1351,7 @@ github.com/open-policy-agent/opa/v1/topdown
github.com/open-policy-agent/opa/v1/topdown/builtins
github.com/open-policy-agent/opa/v1/topdown/cache
github.com/open-policy-agent/opa/v1/topdown/copypropagation
+github.com/open-policy-agent/opa/v1/topdown/durationparser
github.com/open-policy-agent/opa/v1/topdown/print
github.com/open-policy-agent/opa/v1/tracing
github.com/open-policy-agent/opa/v1/types
@@ -1837,8 +1841,8 @@ github.com/prometheus/common/expfmt
github.com/prometheus/common/helpers/templates
github.com/prometheus/common/model
github.com/prometheus/common/promslog
-# github.com/prometheus/procfs v0.17.0
-## explicit; go 1.23.0
+# github.com/prometheus/procfs v0.20.1
+## explicit; go 1.25.0
github.com/prometheus/procfs
github.com/prometheus/procfs/internal/fs
github.com/prometheus/procfs/internal/util
@@ -2129,11 +2133,11 @@ github.com/unrolled/secure/cspbuilder
# github.com/urfave/cli/v2 v2.27.7
## explicit; go 1.18
github.com/urfave/cli/v2
-# github.com/valyala/fastjson v1.6.7
-## explicit; go 1.12
+# github.com/valyala/fastjson v1.6.10
+## explicit; go 1.24
github.com/valyala/fastjson
github.com/valyala/fastjson/fastfloat
-# github.com/vektah/gqlparser/v2 v2.5.32
+# github.com/vektah/gqlparser/v2 v2.5.33
## explicit; go 1.22
github.com/vektah/gqlparser/v2/ast
github.com/vektah/gqlparser/v2/gqlerror
@@ -2309,7 +2313,7 @@ go.opentelemetry.io/auto/sdk/internal/telemetry
## explicit; go 1.25.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc/internal
-# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
+# go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0
## explicit; go 1.25.0
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp/internal/request
@@ -2404,7 +2408,7 @@ go.uber.org/zap/internal/pool
go.uber.org/zap/internal/stacktrace
go.uber.org/zap/zapcore
go.uber.org/zap/zapgrpc
-# go.yaml.in/yaml/v2 v2.4.3
+# go.yaml.in/yaml/v2 v2.4.4
## explicit; go 1.15
go.yaml.in/yaml/v2
# go.yaml.in/yaml/v3 v3.0.4