vendor: github.com/open-policy-agent/opa v1.14.1

updating to the lowest minor release that contains [opa@e9ca3ed], which removed
some redundant imports that resulted in indirect dependencies.

full diff: https://github.com/open-policy-agent/opa/compare/v1.10.1...v1.14.1

[opa@e9ca3ed]: https://github.com/open-policy-agent/opa/commit/e9ca3ed4151e5f1850b379aa62cad77669f453a5

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn
2026-07-01 15:31:34 +02:00
parent 3bcb873b04
commit c74f522b8e
307 changed files with 55780 additions and 47201 deletions
+1 -7
View File
@@ -38,7 +38,7 @@ require (
github.com/moby/sys/atomicwriter v0.1.0
github.com/moby/sys/mountinfo v0.7.2
github.com/morikuni/aec v1.1.0
github.com/open-policy-agent/opa v1.10.1
github.com/open-policy-agent/opa v1.14.1
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.1.1
github.com/pelletier/go-toml/v2 v2.3.1
@@ -97,7 +97,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
github.com/aws/smithy-go v1.27.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver v3.5.1+incompatible // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -118,7 +117,6 @@ require (
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.25.2 // indirect
@@ -184,10 +182,6 @@ require (
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/package-url/packageurl-go v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
+6 -8
View File
@@ -98,8 +98,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
github.com/bytecodealliance/wasmtime-go/v37 v37.0.0 h1:DPjdn2V3JhXHMoZ2ymRqGK+y1bDyr9wgpyYCvhjMky8=
github.com/bytecodealliance/wasmtime-go/v37 v37.0.0/go.mod h1:Pf1l2JCTUFMnOqDIwkjzx1qfVJ09xbaXETKgRVE4jZ0=
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/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
@@ -203,16 +203,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI=
github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=
github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM=
github.com/go-chi/chi/v5 v5.3.0/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -449,8 +447,8 @@ github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/open-policy-agent/opa v1.10.1 h1:haIvxZSPky8HLjRrvQwWAjCPLg8JDFSZMbbG4yyUHgY=
github.com/open-policy-agent/opa v1.10.1/go.mod h1:7uPI3iRpOalJ0BhK6s1JALWPU9HvaV1XeBSSMZnr/PM=
github.com/open-policy-agent/opa v1.14.1 h1:MhurLB9mSbXmojYFCmGbiC1Uagu1+aFAV4XVotDA86M=
github.com/open-policy-agent/opa v1.14.1/go.mod h1:B5gykwJ2l0g0wZS4ClCcpfSSEx51n4NHpTsWfuPwqnQ=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
-20
View File
@@ -1,20 +0,0 @@
Copyright (C) 2013 Blake Mizerany
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
File diff suppressed because it is too large Load Diff
-316
View File
@@ -1,316 +0,0 @@
// Package quantile computes approximate quantiles over an unbounded data
// stream within low memory and CPU bounds.
//
// A small amount of accuracy is traded to achieve the above properties.
//
// Multiple streams can be merged before calling Query to generate a single set
// of results. This is meaningful when the streams represent the same type of
// data. See Merge and Samples.
//
// For more detailed information about the algorithm used, see:
//
// Effective Computation of Biased Quantiles over Data Streams
//
// http://www.cs.rutgers.edu/~muthu/bquant.pdf
package quantile
import (
"math"
"sort"
)
// Sample holds an observed value and meta information for compression. JSON
// tags have been added for convenience.
type Sample struct {
Value float64 `json:",string"`
Width float64 `json:",string"`
Delta float64 `json:",string"`
}
// Samples represents a slice of samples. It implements sort.Interface.
type Samples []Sample
func (a Samples) Len() int { return len(a) }
func (a Samples) Less(i, j int) bool { return a[i].Value < a[j].Value }
func (a Samples) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
type invariant func(s *stream, r float64) float64
// NewLowBiased returns an initialized Stream for low-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the lower ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within (1±Epsilon)*Quantile.
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewLowBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * r
}
return newStream(ƒ)
}
// NewHighBiased returns an initialized Stream for high-biased quantiles
// (e.g. 0.01, 0.1, 0.5) where the needed quantiles are not known a priori, but
// error guarantees can still be given even for the higher ranks of the data
// distribution.
//
// The provided epsilon is a relative error, i.e. the true quantile of a value
// returned by a query is guaranteed to be within 1-(1±Epsilon)*(1-Quantile).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error
// properties.
func NewHighBiased(epsilon float64) *Stream {
ƒ := func(s *stream, r float64) float64 {
return 2 * epsilon * (s.n - r)
}
return newStream(ƒ)
}
// NewTargeted returns an initialized Stream concerned with a particular set of
// quantile values that are supplied a priori. Knowing these a priori reduces
// space and computation time. The targets map maps the desired quantiles to
// their absolute errors, i.e. the true quantile of a value returned by a query
// is guaranteed to be within (Quantile±Epsilon).
//
// See http://www.cs.rutgers.edu/~muthu/bquant.pdf for time, space, and error properties.
func NewTargeted(targetMap map[float64]float64) *Stream {
// Convert map to slice to avoid slow iterations on a map.
// ƒ is called on the hot path, so converting the map to a slice
// beforehand results in significant CPU savings.
targets := targetMapToSlice(targetMap)
ƒ := func(s *stream, r float64) float64 {
var m = math.MaxFloat64
var f float64
for _, t := range targets {
if t.quantile*s.n <= r {
f = (2 * t.epsilon * r) / t.quantile
} else {
f = (2 * t.epsilon * (s.n - r)) / (1 - t.quantile)
}
if f < m {
m = f
}
}
return m
}
return newStream(ƒ)
}
type target struct {
quantile float64
epsilon float64
}
func targetMapToSlice(targetMap map[float64]float64) []target {
targets := make([]target, 0, len(targetMap))
for quantile, epsilon := range targetMap {
t := target{
quantile: quantile,
epsilon: epsilon,
}
targets = append(targets, t)
}
return targets
}
// Stream computes quantiles for a stream of float64s. It is not thread-safe by
// design. Take care when using across multiple goroutines.
type Stream struct {
*stream
b Samples
sorted bool
}
func newStream(ƒ invariant) *Stream {
x := &stream{ƒ: ƒ}
return &Stream{x, make(Samples, 0, 500), true}
}
// Insert inserts v into the stream.
func (s *Stream) Insert(v float64) {
s.insert(Sample{Value: v, Width: 1})
}
func (s *Stream) insert(sample Sample) {
s.b = append(s.b, sample)
s.sorted = false
if len(s.b) == cap(s.b) {
s.flush()
}
}
// Query returns the computed qth percentiles value. If s was created with
// NewTargeted, and q is not in the set of quantiles provided a priori, Query
// will return an unspecified result.
func (s *Stream) Query(q float64) float64 {
if !s.flushed() {
// Fast path when there hasn't been enough data for a flush;
// this also yields better accuracy for small sets of data.
l := len(s.b)
if l == 0 {
return 0
}
i := int(math.Ceil(float64(l) * q))
if i > 0 {
i -= 1
}
s.maybeSort()
return s.b[i].Value
}
s.flush()
return s.stream.query(q)
}
// Merge merges samples into the underlying streams samples. This is handy when
// merging multiple streams from separate threads, database shards, etc.
//
// ATTENTION: This method is broken and does not yield correct results. The
// underlying algorithm is not capable of merging streams correctly.
func (s *Stream) Merge(samples Samples) {
sort.Sort(samples)
s.stream.merge(samples)
}
// Reset reinitializes and clears the list reusing the samples buffer memory.
func (s *Stream) Reset() {
s.stream.reset()
s.b = s.b[:0]
}
// Samples returns stream samples held by s.
func (s *Stream) Samples() Samples {
if !s.flushed() {
return s.b
}
s.flush()
return s.stream.samples()
}
// Count returns the total number of samples observed in the stream
// since initialization.
func (s *Stream) Count() int {
return len(s.b) + s.stream.count()
}
func (s *Stream) flush() {
s.maybeSort()
s.stream.merge(s.b)
s.b = s.b[:0]
}
func (s *Stream) maybeSort() {
if !s.sorted {
s.sorted = true
sort.Sort(s.b)
}
}
func (s *Stream) flushed() bool {
return len(s.stream.l) > 0
}
type stream struct {
n float64
l []Sample
ƒ invariant
}
func (s *stream) reset() {
s.l = s.l[:0]
s.n = 0
}
func (s *stream) insert(v float64) {
s.merge(Samples{{v, 1, 0}})
}
func (s *stream) merge(samples Samples) {
// TODO(beorn7): This tries to merge not only individual samples, but
// whole summaries. The paper doesn't mention merging summaries at
// all. Unittests show that the merging is inaccurate. Find out how to
// do merges properly.
var r float64
i := 0
for _, sample := range samples {
for ; i < len(s.l); i++ {
c := s.l[i]
if c.Value > sample.Value {
// Insert at position i.
s.l = append(s.l, Sample{})
copy(s.l[i+1:], s.l[i:])
s.l[i] = Sample{
sample.Value,
sample.Width,
math.Max(sample.Delta, math.Floor(s.ƒ(s, r))-1),
// TODO(beorn7): How to calculate delta correctly?
}
i++
goto inserted
}
r += c.Width
}
s.l = append(s.l, Sample{sample.Value, sample.Width, 0})
i++
inserted:
s.n += sample.Width
r += sample.Width
}
s.compress()
}
func (s *stream) count() int {
return int(s.n)
}
func (s *stream) query(q float64) float64 {
t := math.Ceil(q * s.n)
t += math.Ceil(s.ƒ(s, t) / 2)
p := s.l[0]
var r float64
for _, c := range s.l[1:] {
r += p.Width
if r+c.Width+c.Delta > t {
return p.Value
}
p = c
}
return p.Value
}
func (s *stream) compress() {
if len(s.l) < 2 {
return
}
x := s.l[len(s.l)-1]
xi := len(s.l) - 1
r := s.n - 1 - x.Width
for i := len(s.l) - 2; i >= 0; i-- {
c := s.l[i]
if c.Width+x.Width+x.Delta <= s.ƒ(s, r) {
x.Width += c.Width
s.l[xi] = x
// Remove element at i.
copy(s.l[i:], s.l[i+1:])
s.l = s.l[:len(s.l)-1]
xi -= 1
} else {
x = c
xi = i
}
r -= c.Width
}
}
func (s *stream) samples() Samples {
samples := make(Samples, len(s.l))
copy(samples, s.l)
return samples
}
-12
View File
@@ -1,12 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*_test.go]
trim_trailing_whitespace = false
-7
View File
@@ -1,7 +0,0 @@
testdata/conf_out.ini
ini.sublime-project
ini.sublime-workspace
testdata/conf_reflect.ini
.idea
/.vscode
.DS_Store
-27
View File
@@ -1,27 +0,0 @@
linters-settings:
staticcheck:
checks: [
"all",
"-SA1019" # There are valid use cases of strings.Title
]
nakedret:
max-func-lines: 0 # Disallow any unnamed return statement
linters:
enable:
- deadcode
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- structcheck
- typecheck
- unused
- varcheck
- nakedret
- gofmt
- rowserrcheck
- unconvert
- goimports
- unparam
-191
View File
@@ -1,191 +0,0 @@
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:
You must give any other recipients of the Work or Derivative Works a copy of
this License; and
You must cause any modified files to carry prominent notices stating that You
changed the files; and
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
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 2014 Unknwon
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.
-15
View File
@@ -1,15 +0,0 @@
.PHONY: build test bench vet coverage
build: vet bench
test:
go test -v -cover -race
bench:
go test -v -cover -test.bench=. -test.benchmem
vet:
go vet
coverage:
go test -coverprofile=c.out && go tool cover -html=c.out && rm c.out
-43
View File
@@ -1,43 +0,0 @@
# INI
[![GitHub Workflow Status](https://img.shields.io/github/checks-status/go-ini/ini/main?logo=github&style=for-the-badge)](https://github.com/go-ini/ini/actions?query=branch%3Amain)
[![codecov](https://img.shields.io/codecov/c/github/go-ini/ini/master?logo=codecov&style=for-the-badge)](https://codecov.io/gh/go-ini/ini)
[![GoDoc](https://img.shields.io/badge/GoDoc-Reference-blue?style=for-the-badge&logo=go)](https://pkg.go.dev/github.com/go-ini/ini?tab=doc)
[![Sourcegraph](https://img.shields.io/badge/view%20on-Sourcegraph-brightgreen.svg?style=for-the-badge&logo=sourcegraph)](https://sourcegraph.com/github.com/go-ini/ini)
![](https://avatars0.githubusercontent.com/u/10216035?v=3&s=200)
Package ini provides INI file read and write functionality in Go.
## Features
- Load from multiple data sources(file, `[]byte`, `io.Reader` and `io.ReadCloser`) with overwrites.
- Read with recursion values.
- Read with parent-child sections.
- Read with auto-increment key names.
- Read with multiple-line values.
- Read with tons of helper methods.
- Read and convert values to Go types.
- Read and **WRITE** comments of sections and keys.
- Manipulate sections, keys and comments with ease.
- Keep sections and keys in order as you parse and save.
## Installation
The minimum requirement of Go is **1.13**.
```sh
$ go get gopkg.in/ini.v1
```
Please add `-u` flag to update in the future.
## Getting Help
- [Getting Started](https://ini.unknwon.io/docs/intro/getting_started)
- [API Documentation](https://gowalker.org/gopkg.in/ini.v1)
- 中国大陆镜像:https://ini.unknwon.cn
## License
This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text.
-16
View File
@@ -1,16 +0,0 @@
coverage:
range: "60...95"
status:
project:
default:
threshold: 1%
informational: true
patch:
defualt:
only_pulls: true
informational: true
comment:
layout: 'diff'
github_checks: false
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2019 Unknwon
//
// 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 ini
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
)
var (
_ dataSource = (*sourceFile)(nil)
_ dataSource = (*sourceData)(nil)
_ dataSource = (*sourceReadCloser)(nil)
)
// dataSource is an interface that returns object which can be read and closed.
type dataSource interface {
ReadCloser() (io.ReadCloser, error)
}
// sourceFile represents an object that contains content on the local file system.
type sourceFile struct {
name string
}
func (s sourceFile) ReadCloser() (_ io.ReadCloser, err error) {
return os.Open(s.name)
}
// sourceData represents an object that contains content in memory.
type sourceData struct {
data []byte
}
func (s *sourceData) ReadCloser() (io.ReadCloser, error) {
return ioutil.NopCloser(bytes.NewReader(s.data)), nil
}
// sourceReadCloser represents an input stream with Close method.
type sourceReadCloser struct {
reader io.ReadCloser
}
func (s *sourceReadCloser) ReadCloser() (io.ReadCloser, error) {
return s.reader, nil
}
func parseDataSource(source interface{}) (dataSource, error) {
switch s := source.(type) {
case string:
return sourceFile{s}, nil
case []byte:
return &sourceData{s}, nil
case io.ReadCloser:
return &sourceReadCloser{s}, nil
case io.Reader:
return &sourceReadCloser{ioutil.NopCloser(s)}, nil
default:
return nil, fmt.Errorf("error parsing data source: unknown type %q", s)
}
}
-22
View File
@@ -1,22 +0,0 @@
// Copyright 2019 Unknwon
//
// 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 ini
var (
// Deprecated: Use "DefaultSection" instead.
DEFAULT_SECTION = DefaultSection
// Deprecated: AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE.
AllCapsUnderscore = SnackCase
)
-49
View File
@@ -1,49 +0,0 @@
// Copyright 2016 Unknwon
//
// 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 ini
import (
"fmt"
)
// ErrDelimiterNotFound indicates the error type of no delimiter is found which there should be one.
type ErrDelimiterNotFound struct {
Line string
}
// IsErrDelimiterNotFound returns true if the given error is an instance of ErrDelimiterNotFound.
func IsErrDelimiterNotFound(err error) bool {
_, ok := err.(ErrDelimiterNotFound)
return ok
}
func (err ErrDelimiterNotFound) Error() string {
return fmt.Sprintf("key-value delimiter not found: %s", err.Line)
}
// ErrEmptyKeyName indicates the error type of no key name is found which there should be one.
type ErrEmptyKeyName struct {
Line string
}
// IsErrEmptyKeyName returns true if the given error is an instance of ErrEmptyKeyName.
func IsErrEmptyKeyName(err error) bool {
_, ok := err.(ErrEmptyKeyName)
return ok
}
func (err ErrEmptyKeyName) Error() string {
return fmt.Sprintf("empty key name: %s", err.Line)
}
-541
View File
@@ -1,541 +0,0 @@
// Copyright 2017 Unknwon
//
// 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 ini
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"sync"
)
// File represents a combination of one or more INI files in memory.
type File struct {
options LoadOptions
dataSources []dataSource
// Should make things safe, but sometimes doesn't matter.
BlockMode bool
lock sync.RWMutex
// To keep data in order.
sectionList []string
// To keep track of the index of a section with same name.
// This meta list is only used with non-unique section names are allowed.
sectionIndexes []int
// Actual data is stored here.
sections map[string][]*Section
NameMapper
ValueMapper
}
// newFile initializes File object with given data sources.
func newFile(dataSources []dataSource, opts LoadOptions) *File {
if len(opts.KeyValueDelimiters) == 0 {
opts.KeyValueDelimiters = "=:"
}
if len(opts.KeyValueDelimiterOnWrite) == 0 {
opts.KeyValueDelimiterOnWrite = "="
}
if len(opts.ChildSectionDelimiter) == 0 {
opts.ChildSectionDelimiter = "."
}
return &File{
BlockMode: true,
dataSources: dataSources,
sections: make(map[string][]*Section),
options: opts,
}
}
// Empty returns an empty file object.
func Empty(opts ...LoadOptions) *File {
var opt LoadOptions
if len(opts) > 0 {
opt = opts[0]
}
// Ignore error here, we are sure our data is good.
f, _ := LoadSources(opt, []byte(""))
return f
}
// NewSection creates a new section.
func (f *File) NewSection(name string) (*Section, error) {
if len(name) == 0 {
return nil, errors.New("empty section name")
}
if (f.options.Insensitive || f.options.InsensitiveSections) && name != DefaultSection {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
if !f.options.AllowNonUniqueSections && inSlice(name, f.sectionList) {
return f.sections[name][0], nil
}
f.sectionList = append(f.sectionList, name)
// NOTE: Append to indexes must happen before appending to sections,
// otherwise index will have off-by-one problem.
f.sectionIndexes = append(f.sectionIndexes, len(f.sections[name]))
sec := newSection(f, name)
f.sections[name] = append(f.sections[name], sec)
return sec, nil
}
// NewRawSection creates a new section with an unparseable body.
func (f *File) NewRawSection(name, body string) (*Section, error) {
section, err := f.NewSection(name)
if err != nil {
return nil, err
}
section.isRawSection = true
section.rawBody = body
return section, nil
}
// NewSections creates a list of sections.
func (f *File) NewSections(names ...string) (err error) {
for _, name := range names {
if _, err = f.NewSection(name); err != nil {
return err
}
}
return nil
}
// GetSection returns section by given name.
func (f *File) GetSection(name string) (*Section, error) {
secs, err := f.SectionsByName(name)
if err != nil {
return nil, err
}
return secs[0], err
}
// HasSection returns true if the file contains a section with given name.
func (f *File) HasSection(name string) bool {
section, _ := f.GetSection(name)
return section != nil
}
// SectionsByName returns all sections with given name.
func (f *File) SectionsByName(name string) ([]*Section, error) {
if len(name) == 0 {
name = DefaultSection
}
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
secs := f.sections[name]
if len(secs) == 0 {
return nil, fmt.Errorf("section %q does not exist", name)
}
return secs, nil
}
// Section assumes named section exists and returns a zero-value when not.
func (f *File) Section(name string) *Section {
sec, err := f.GetSection(name)
if err != nil {
if name == "" {
name = DefaultSection
}
sec, _ = f.NewSection(name)
return sec
}
return sec
}
// SectionWithIndex assumes named section exists and returns a new section when not.
func (f *File) SectionWithIndex(name string, index int) *Section {
secs, err := f.SectionsByName(name)
if err != nil || len(secs) <= index {
// NOTE: It's OK here because the only possible error is empty section name,
// but if it's empty, this piece of code won't be executed.
newSec, _ := f.NewSection(name)
return newSec
}
return secs[index]
}
// Sections returns a list of Section stored in the current instance.
func (f *File) Sections() []*Section {
if f.BlockMode {
f.lock.RLock()
defer f.lock.RUnlock()
}
sections := make([]*Section, len(f.sectionList))
for i, name := range f.sectionList {
sections[i] = f.sections[name][f.sectionIndexes[i]]
}
return sections
}
// ChildSections returns a list of child sections of given section name.
func (f *File) ChildSections(name string) []*Section {
return f.Section(name).ChildSections()
}
// SectionStrings returns list of section names.
func (f *File) SectionStrings() []string {
list := make([]string, len(f.sectionList))
copy(list, f.sectionList)
return list
}
// DeleteSection deletes a section or all sections with given name.
func (f *File) DeleteSection(name string) {
secs, err := f.SectionsByName(name)
if err != nil {
return
}
for i := 0; i < len(secs); i++ {
// For non-unique sections, it is always needed to remove the first one so
// in the next iteration, the subsequent section continue having index 0.
// Ignoring the error as index 0 never returns an error.
_ = f.DeleteSectionWithIndex(name, 0)
}
}
// DeleteSectionWithIndex deletes a section with given name and index.
func (f *File) DeleteSectionWithIndex(name string, index int) error {
if !f.options.AllowNonUniqueSections && index != 0 {
return fmt.Errorf("delete section with non-zero index is only allowed when non-unique sections is enabled")
}
if len(name) == 0 {
name = DefaultSection
}
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(name)
}
if f.BlockMode {
f.lock.Lock()
defer f.lock.Unlock()
}
// Count occurrences of the sections
occurrences := 0
sectionListCopy := make([]string, len(f.sectionList))
copy(sectionListCopy, f.sectionList)
for i, s := range sectionListCopy {
if s != name {
continue
}
if occurrences == index {
if len(f.sections[name]) <= 1 {
delete(f.sections, name) // The last one in the map
} else {
f.sections[name] = append(f.sections[name][:index], f.sections[name][index+1:]...)
}
// Fix section lists
f.sectionList = append(f.sectionList[:i], f.sectionList[i+1:]...)
f.sectionIndexes = append(f.sectionIndexes[:i], f.sectionIndexes[i+1:]...)
} else if occurrences > index {
// Fix the indices of all following sections with this name.
f.sectionIndexes[i-1]--
}
occurrences++
}
return nil
}
func (f *File) reload(s dataSource) error {
r, err := s.ReadCloser()
if err != nil {
return err
}
defer r.Close()
return f.parse(r)
}
// Reload reloads and parses all data sources.
func (f *File) Reload() (err error) {
for _, s := range f.dataSources {
if err = f.reload(s); err != nil {
// In loose mode, we create an empty default section for nonexistent files.
if os.IsNotExist(err) && f.options.Loose {
_ = f.parse(bytes.NewBuffer(nil))
continue
}
return err
}
if f.options.ShortCircuit {
return nil
}
}
return nil
}
// Append appends one or more data sources and reloads automatically.
func (f *File) Append(source interface{}, others ...interface{}) error {
ds, err := parseDataSource(source)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
for _, s := range others {
ds, err = parseDataSource(s)
if err != nil {
return err
}
f.dataSources = append(f.dataSources, ds)
}
return f.Reload()
}
func (f *File) writeToBuffer(indent string) (*bytes.Buffer, error) {
equalSign := DefaultFormatLeft + f.options.KeyValueDelimiterOnWrite + DefaultFormatRight
if PrettyFormat || PrettyEqual {
equalSign = fmt.Sprintf(" %s ", f.options.KeyValueDelimiterOnWrite)
}
// Use buffer to make sure target is safe until finish encoding.
buf := bytes.NewBuffer(nil)
lastSectionIdx := len(f.sectionList) - 1
for i, sname := range f.sectionList {
sec := f.SectionWithIndex(sname, f.sectionIndexes[i])
if len(sec.Comment) > 0 {
// Support multiline comments
lines := strings.Split(sec.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + lines[i]
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
if i > 0 || DefaultHeader || (i == 0 && strings.ToUpper(sec.name) != DefaultSection) {
if _, err := buf.WriteString("[" + sname + "]" + LineBreak); err != nil {
return nil, err
}
} else {
// Write nothing if default section is empty
if len(sec.keyList) == 0 {
continue
}
}
isLastSection := i == lastSectionIdx
if sec.isRawSection {
if _, err := buf.WriteString(sec.rawBody); err != nil {
return nil, err
}
if PrettySection && !isLastSection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
continue
}
// Count and generate alignment length and buffer spaces using the
// longest key. Keys may be modified if they contain certain characters so
// we need to take that into account in our calculation.
alignLength := 0
if PrettyFormat {
for _, kname := range sec.keyList {
keyLength := len(kname)
// First case will surround key by ` and second by """
if strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters) {
keyLength += 2
} else if strings.Contains(kname, "`") {
keyLength += 6
}
if keyLength > alignLength {
alignLength = keyLength
}
}
}
alignSpaces := bytes.Repeat([]byte(" "), alignLength)
KeyList:
for _, kname := range sec.keyList {
key := sec.Key(kname)
if len(key.Comment) > 0 {
if len(indent) > 0 && sname != DefaultSection {
buf.WriteString(indent)
}
// Support multiline comments
lines := strings.Split(key.Comment, LineBreak)
for i := range lines {
if lines[i][0] != '#' && lines[i][0] != ';' {
lines[i] = "; " + strings.TrimSpace(lines[i])
} else {
lines[i] = lines[i][:1] + " " + strings.TrimSpace(lines[i][1:])
}
if _, err := buf.WriteString(lines[i] + LineBreak); err != nil {
return nil, err
}
}
}
if len(indent) > 0 && sname != DefaultSection {
buf.WriteString(indent)
}
switch {
case key.isAutoIncrement:
kname = "-"
case strings.Contains(kname, "\"") || strings.ContainsAny(kname, f.options.KeyValueDelimiters):
kname = "`" + kname + "`"
case strings.Contains(kname, "`"):
kname = `"""` + kname + `"""`
}
writeKeyValue := func(val string) (bool, error) {
if _, err := buf.WriteString(kname); err != nil {
return false, err
}
if key.isBooleanType {
buf.WriteString(LineBreak)
return true, nil
}
// Write out alignment spaces before "=" sign
if PrettyFormat {
buf.Write(alignSpaces[:alignLength-len(kname)])
}
// In case key value contains "\n", "`", "\"", "#" or ";"
if strings.ContainsAny(val, "\n`") {
val = `"""` + val + `"""`
} else if !f.options.IgnoreInlineComment && strings.ContainsAny(val, "#;") {
val = "`" + val + "`"
} else if len(strings.TrimSpace(val)) != len(val) {
val = `"` + val + `"`
}
if _, err := buf.WriteString(equalSign + val + LineBreak); err != nil {
return false, err
}
return false, nil
}
shadows := key.ValueWithShadows()
if len(shadows) == 0 {
if _, err := writeKeyValue(""); err != nil {
return nil, err
}
}
for _, val := range shadows {
exitLoop, err := writeKeyValue(val)
if err != nil {
return nil, err
} else if exitLoop {
continue KeyList
}
}
for _, val := range key.nestedValues {
if _, err := buf.WriteString(indent + " " + val + LineBreak); err != nil {
return nil, err
}
}
}
if PrettySection && !isLastSection {
// Put a line between sections
if _, err := buf.WriteString(LineBreak); err != nil {
return nil, err
}
}
}
return buf, nil
}
// WriteToIndent writes content into io.Writer with given indention.
// If PrettyFormat has been set to be true,
// it will align "=" sign with spaces under each section.
func (f *File) WriteToIndent(w io.Writer, indent string) (int64, error) {
buf, err := f.writeToBuffer(indent)
if err != nil {
return 0, err
}
return buf.WriteTo(w)
}
// WriteTo writes file content into io.Writer.
func (f *File) WriteTo(w io.Writer) (int64, error) {
return f.WriteToIndent(w, "")
}
// SaveToIndent writes content to file system with given value indention.
func (f *File) SaveToIndent(filename, indent string) error {
// Note: Because we are truncating with os.Create,
// so it's safer to save to a temporary file location and rename after done.
buf, err := f.writeToBuffer(indent)
if err != nil {
return err
}
return ioutil.WriteFile(filename, buf.Bytes(), 0666)
}
// SaveTo writes content to file system.
func (f *File) SaveTo(filename string) error {
return f.SaveToIndent(filename, "")
}
-24
View File
@@ -1,24 +0,0 @@
// Copyright 2019 Unknwon
//
// 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 ini
func inSlice(str string, s []string) bool {
for _, v := range s {
if str == v {
return true
}
}
return false
}
-176
View File
@@ -1,176 +0,0 @@
// Copyright 2014 Unknwon
//
// 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 ini provides INI file read and write functionality in Go.
package ini
import (
"os"
"regexp"
"runtime"
"strings"
)
const (
// Maximum allowed depth when recursively substituing variable names.
depthValues = 99
)
var (
// DefaultSection is the name of default section. You can use this var or the string literal.
// In most of cases, an empty string is all you need to access the section.
DefaultSection = "DEFAULT"
// LineBreak is the delimiter to determine or compose a new line.
// This variable will be changed to "\r\n" automatically on Windows at package init time.
LineBreak = "\n"
// Variable regexp pattern: %(variable)s
varPattern = regexp.MustCompile(`%\(([^)]+)\)s`)
// DefaultHeader explicitly writes default section header.
DefaultHeader = false
// PrettySection indicates whether to put a line between sections.
PrettySection = true
// PrettyFormat indicates whether to align "=" sign with spaces to produce pretty output
// or reduce all possible spaces for compact format.
PrettyFormat = true
// PrettyEqual places spaces around "=" sign even when PrettyFormat is false.
PrettyEqual = false
// DefaultFormatLeft places custom spaces on the left when PrettyFormat and PrettyEqual are both disabled.
DefaultFormatLeft = ""
// DefaultFormatRight places custom spaces on the right when PrettyFormat and PrettyEqual are both disabled.
DefaultFormatRight = ""
)
var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
func init() {
if runtime.GOOS == "windows" && !inTest {
LineBreak = "\r\n"
}
}
// LoadOptions contains all customized options used for load data source(s).
type LoadOptions struct {
// Loose indicates whether the parser should ignore nonexistent files or return error.
Loose bool
// Insensitive indicates whether the parser forces all section and key names to lowercase.
Insensitive bool
// InsensitiveSections indicates whether the parser forces all section to lowercase.
InsensitiveSections bool
// InsensitiveKeys indicates whether the parser forces all key names to lowercase.
InsensitiveKeys bool
// IgnoreContinuation indicates whether to ignore continuation lines while parsing.
IgnoreContinuation bool
// IgnoreInlineComment indicates whether to ignore comments at the end of value and treat it as part of value.
IgnoreInlineComment bool
// SkipUnrecognizableLines indicates whether to skip unrecognizable lines that do not conform to key/value pairs.
SkipUnrecognizableLines bool
// ShortCircuit indicates whether to ignore other configuration sources after loaded the first available configuration source.
ShortCircuit bool
// AllowBooleanKeys indicates whether to allow boolean type keys or treat as value is missing.
// This type of keys are mostly used in my.cnf.
AllowBooleanKeys bool
// AllowShadows indicates whether to keep track of keys with same name under same section.
AllowShadows bool
// AllowNestedValues indicates whether to allow AWS-like nested values.
// Docs: http://docs.aws.amazon.com/cli/latest/topic/config-vars.html#nested-values
AllowNestedValues bool
// AllowPythonMultilineValues indicates whether to allow Python-like multi-line values.
// Docs: https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
// Relevant quote: Values can also span multiple lines, as long as they are indented deeper
// than the first line of the value.
AllowPythonMultilineValues bool
// SpaceBeforeInlineComment indicates whether to allow comment symbols (\# and \;) inside value.
// Docs: https://docs.python.org/2/library/configparser.html
// Quote: Comments may appear on their own in an otherwise empty line, or may be entered in lines holding values or section names.
// In the latter case, they need to be preceded by a whitespace character to be recognized as a comment.
SpaceBeforeInlineComment bool
// UnescapeValueDoubleQuotes indicates whether to unescape double quotes inside value to regular format
// when value is surrounded by double quotes, e.g. key="a \"value\"" => key=a "value"
UnescapeValueDoubleQuotes bool
// UnescapeValueCommentSymbols indicates to unescape comment symbols (\# and \;) inside value to regular format
// when value is NOT surrounded by any quotes.
// Note: UNSTABLE, behavior might change to only unescape inside double quotes but may noy necessary at all.
UnescapeValueCommentSymbols bool
// UnparseableSections stores a list of blocks that are allowed with raw content which do not otherwise
// conform to key/value pairs. Specify the names of those blocks here.
UnparseableSections []string
// KeyValueDelimiters is the sequence of delimiters that are used to separate key and value. By default, it is "=:".
KeyValueDelimiters string
// KeyValueDelimiterOnWrite is the delimiter that are used to separate key and value output. By default, it is "=".
KeyValueDelimiterOnWrite string
// ChildSectionDelimiter is the delimiter that is used to separate child sections. By default, it is ".".
ChildSectionDelimiter string
// PreserveSurroundedQuote indicates whether to preserve surrounded quote (single and double quotes).
PreserveSurroundedQuote bool
// DebugFunc is called to collect debug information (currently only useful to debug parsing Python-style multiline values).
DebugFunc DebugFunc
// ReaderBufferSize is the buffer size of the reader in bytes.
ReaderBufferSize int
// AllowNonUniqueSections indicates whether to allow sections with the same name multiple times.
AllowNonUniqueSections bool
// AllowDuplicateShadowValues indicates whether values for shadowed keys should be deduplicated.
AllowDuplicateShadowValues bool
}
// DebugFunc is the type of function called to log parse events.
type DebugFunc func(message string)
// LoadSources allows caller to apply customized options for loading from data source(s).
func LoadSources(opts LoadOptions, source interface{}, others ...interface{}) (_ *File, err error) {
sources := make([]dataSource, len(others)+1)
sources[0], err = parseDataSource(source)
if err != nil {
return nil, err
}
for i := range others {
sources[i+1], err = parseDataSource(others[i])
if err != nil {
return nil, err
}
}
f := newFile(sources, opts)
if err = f.Reload(); err != nil {
return nil, err
}
return f, nil
}
// Load loads and parses from INI data sources.
// Arguments can be mixed of file name with string type, or raw data in []byte.
// It will return error if list contains nonexistent files.
func Load(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{}, source, others...)
}
// LooseLoad has exactly same functionality as Load function
// except it ignores nonexistent files instead of returning error.
func LooseLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Loose: true}, source, others...)
}
// InsensitiveLoad has exactly same functionality as Load function
// except it forces all section and key names to be lowercased.
func InsensitiveLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{Insensitive: true}, source, others...)
}
// ShadowLoad has exactly same functionality as Load function
// except it allows have shadow keys.
func ShadowLoad(source interface{}, others ...interface{}) (*File, error) {
return LoadSources(LoadOptions{AllowShadows: true}, source, others...)
}
-837
View File
@@ -1,837 +0,0 @@
// Copyright 2014 Unknwon
//
// 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 ini
import (
"bytes"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
// Key represents a key under a section.
type Key struct {
s *Section
Comment string
name string
value string
isAutoIncrement bool
isBooleanType bool
isShadow bool
shadows []*Key
nestedValues []string
}
// newKey simply return a key object with given values.
func newKey(s *Section, name, val string) *Key {
return &Key{
s: s,
name: name,
value: val,
}
}
func (k *Key) addShadow(val string) error {
if k.isShadow {
return errors.New("cannot add shadow to another shadow key")
} else if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add shadow to auto-increment or boolean key")
}
if !k.s.f.options.AllowDuplicateShadowValues {
// Deduplicate shadows based on their values.
if k.value == val {
return nil
}
for i := range k.shadows {
if k.shadows[i].value == val {
return nil
}
}
}
shadow := newKey(k.s, k.name, val)
shadow.isShadow = true
k.shadows = append(k.shadows, shadow)
return nil
}
// AddShadow adds a new shadow key to itself.
func (k *Key) AddShadow(val string) error {
if !k.s.f.options.AllowShadows {
return errors.New("shadow key is not allowed")
}
return k.addShadow(val)
}
func (k *Key) addNestedValue(val string) error {
if k.isAutoIncrement || k.isBooleanType {
return errors.New("cannot add nested value to auto-increment or boolean key")
}
k.nestedValues = append(k.nestedValues, val)
return nil
}
// AddNestedValue adds a nested value to the key.
func (k *Key) AddNestedValue(val string) error {
if !k.s.f.options.AllowNestedValues {
return errors.New("nested value is not allowed")
}
return k.addNestedValue(val)
}
// ValueMapper represents a mapping function for values, e.g. os.ExpandEnv
type ValueMapper func(string) string
// Name returns name of key.
func (k *Key) Name() string {
return k.name
}
// Value returns raw value of key for performance purpose.
func (k *Key) Value() string {
return k.value
}
// ValueWithShadows returns raw values of key and its shadows if any. Shadow
// keys with empty values are ignored from the returned list.
func (k *Key) ValueWithShadows() []string {
if len(k.shadows) == 0 {
if k.value == "" {
return []string{}
}
return []string{k.value}
}
vals := make([]string, 0, len(k.shadows)+1)
if k.value != "" {
vals = append(vals, k.value)
}
for _, s := range k.shadows {
if s.value != "" {
vals = append(vals, s.value)
}
}
return vals
}
// NestedValues returns nested values stored in the key.
// It is possible returned value is nil if no nested values stored in the key.
func (k *Key) NestedValues() []string {
return k.nestedValues
}
// transformValue takes a raw value and transforms to its final string.
func (k *Key) transformValue(val string) string {
if k.s.f.ValueMapper != nil {
val = k.s.f.ValueMapper(val)
}
// Fail-fast if no indicate char found for recursive value
if !strings.Contains(val, "%") {
return val
}
for i := 0; i < depthValues; i++ {
vr := varPattern.FindString(val)
if len(vr) == 0 {
break
}
// Take off leading '%(' and trailing ')s'.
noption := vr[2 : len(vr)-2]
// Search in the same section.
// If not found or found the key itself, then search again in default section.
nk, err := k.s.GetKey(noption)
if err != nil || k == nk {
nk, _ = k.s.f.Section("").GetKey(noption)
if nk == nil {
// Stop when no results found in the default section,
// and returns the value as-is.
break
}
}
// Substitute by new value and take off leading '%(' and trailing ')s'.
val = strings.Replace(val, vr, nk.value, -1)
}
return val
}
// String returns string representation of value.
func (k *Key) String() string {
return k.transformValue(k.value)
}
// Validate accepts a validate function which can
// return modifed result as key value.
func (k *Key) Validate(fn func(string) string) string {
return fn(k.String())
}
// parseBool returns the boolean value represented by the string.
//
// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, y, ON, on, On,
// 0, f, F, FALSE, false, False, NO, no, No, n, OFF, off, Off.
// Any other value returns an error.
func parseBool(str string) (value bool, err error) {
switch str {
case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "y", "ON", "on", "On":
return true, nil
case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "n", "OFF", "off", "Off":
return false, nil
}
return false, fmt.Errorf("parsing \"%s\": invalid syntax", str)
}
// Bool returns bool type value.
func (k *Key) Bool() (bool, error) {
return parseBool(k.String())
}
// Float64 returns float64 type value.
func (k *Key) Float64() (float64, error) {
return strconv.ParseFloat(k.String(), 64)
}
// Int returns int type value.
func (k *Key) Int() (int, error) {
v, err := strconv.ParseInt(k.String(), 0, 64)
return int(v), err
}
// Int64 returns int64 type value.
func (k *Key) Int64() (int64, error) {
return strconv.ParseInt(k.String(), 0, 64)
}
// Uint returns uint type valued.
func (k *Key) Uint() (uint, error) {
u, e := strconv.ParseUint(k.String(), 0, 64)
return uint(u), e
}
// Uint64 returns uint64 type value.
func (k *Key) Uint64() (uint64, error) {
return strconv.ParseUint(k.String(), 0, 64)
}
// Duration returns time.Duration type value.
func (k *Key) Duration() (time.Duration, error) {
return time.ParseDuration(k.String())
}
// TimeFormat parses with given format and returns time.Time type value.
func (k *Key) TimeFormat(format string) (time.Time, error) {
return time.Parse(format, k.String())
}
// Time parses with RFC3339 format and returns time.Time type value.
func (k *Key) Time() (time.Time, error) {
return k.TimeFormat(time.RFC3339)
}
// MustString returns default value if key value is empty.
func (k *Key) MustString(defaultVal string) string {
val := k.String()
if len(val) == 0 {
k.value = defaultVal
return defaultVal
}
return val
}
// MustBool always returns value without error,
// it returns false if error occurs.
func (k *Key) MustBool(defaultVal ...bool) bool {
val, err := k.Bool()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatBool(defaultVal[0])
return defaultVal[0]
}
return val
}
// MustFloat64 always returns value without error,
// it returns 0.0 if error occurs.
func (k *Key) MustFloat64(defaultVal ...float64) float64 {
val, err := k.Float64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatFloat(defaultVal[0], 'f', -1, 64)
return defaultVal[0]
}
return val
}
// MustInt always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt(defaultVal ...int) int {
val, err := k.Int()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(int64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustInt64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustInt64(defaultVal ...int64) int64 {
val, err := k.Int64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatInt(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustUint always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint(defaultVal ...uint) uint {
val, err := k.Uint()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(uint64(defaultVal[0]), 10)
return defaultVal[0]
}
return val
}
// MustUint64 always returns value without error,
// it returns 0 if error occurs.
func (k *Key) MustUint64(defaultVal ...uint64) uint64 {
val, err := k.Uint64()
if len(defaultVal) > 0 && err != nil {
k.value = strconv.FormatUint(defaultVal[0], 10)
return defaultVal[0]
}
return val
}
// MustDuration always returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustDuration(defaultVal ...time.Duration) time.Duration {
val, err := k.Duration()
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].String()
return defaultVal[0]
}
return val
}
// MustTimeFormat always parses with given format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTimeFormat(format string, defaultVal ...time.Time) time.Time {
val, err := k.TimeFormat(format)
if len(defaultVal) > 0 && err != nil {
k.value = defaultVal[0].Format(format)
return defaultVal[0]
}
return val
}
// MustTime always parses with RFC3339 format and returns value without error,
// it returns zero value if error occurs.
func (k *Key) MustTime(defaultVal ...time.Time) time.Time {
return k.MustTimeFormat(time.RFC3339, defaultVal...)
}
// In always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) In(defaultVal string, candidates []string) string {
val := k.String()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InFloat64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InFloat64(defaultVal float64, candidates []float64) float64 {
val := k.MustFloat64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt(defaultVal int, candidates []int) int {
val := k.MustInt()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InInt64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InInt64(defaultVal int64, candidates []int64) int64 {
val := k.MustInt64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint(defaultVal uint, candidates []uint) uint {
val := k.MustUint()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InUint64 always returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InUint64(defaultVal uint64, candidates []uint64) uint64 {
val := k.MustUint64()
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTimeFormat always parses with given format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTimeFormat(format string, defaultVal time.Time, candidates []time.Time) time.Time {
val := k.MustTimeFormat(format)
for _, cand := range candidates {
if val == cand {
return val
}
}
return defaultVal
}
// InTime always parses with RFC3339 format and returns value without error,
// it returns default value if error occurs or doesn't fit into candidates.
func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time {
return k.InTimeFormat(time.RFC3339, defaultVal, candidates)
}
// RangeFloat64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 {
val := k.MustFloat64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt(defaultVal, min, max int) int {
val := k.MustInt()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeInt64 checks if value is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeInt64(defaultVal, min, max int64) int64 {
val := k.MustInt64()
if val < min || val > max {
return defaultVal
}
return val
}
// RangeTimeFormat checks if value with given format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time {
val := k.MustTimeFormat(format)
if val.Unix() < min.Unix() || val.Unix() > max.Unix() {
return defaultVal
}
return val
}
// RangeTime checks if value with RFC3339 format is in given range inclusively,
// and returns default value if it's not.
func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time {
return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max)
}
// Strings returns list of string divided by given delimiter.
func (k *Key) Strings(delim string) []string {
str := k.String()
if len(str) == 0 {
return []string{}
}
runes := []rune(str)
vals := make([]string, 0, 2)
var buf bytes.Buffer
escape := false
idx := 0
for {
if escape {
escape = false
if runes[idx] != '\\' && !strings.HasPrefix(string(runes[idx:]), delim) {
buf.WriteRune('\\')
}
buf.WriteRune(runes[idx])
} else {
if runes[idx] == '\\' {
escape = true
} else if strings.HasPrefix(string(runes[idx:]), delim) {
idx += len(delim) - 1
vals = append(vals, strings.TrimSpace(buf.String()))
buf.Reset()
} else {
buf.WriteRune(runes[idx])
}
}
idx++
if idx == len(runes) {
break
}
}
if buf.Len() > 0 {
vals = append(vals, strings.TrimSpace(buf.String()))
}
return vals
}
// StringsWithShadows returns list of string divided by given delimiter.
// Shadows will also be appended if any.
func (k *Key) StringsWithShadows(delim string) []string {
vals := k.ValueWithShadows()
results := make([]string, 0, len(vals)*2)
for i := range vals {
if len(vals) == 0 {
continue
}
results = append(results, strings.Split(vals[i], delim)...)
}
for i := range results {
results[i] = k.transformValue(strings.TrimSpace(results[i]))
}
return results
}
// Float64s returns list of float64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Float64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), true, false)
return vals
}
// Ints returns list of int divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Ints(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), true, false)
return vals
}
// Int64s returns list of int64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Int64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), true, false)
return vals
}
// Uints returns list of uint divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), true, false)
return vals
}
// Uint64s returns list of uint64 divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Uint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), true, false)
return vals
}
// Bools returns list of bool divided by given delimiter. Any invalid input will be treated as zero value.
func (k *Key) Bools(delim string) []bool {
vals, _ := k.parseBools(k.Strings(delim), true, false)
return vals
}
// TimesFormat parses with given format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) TimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), true, false)
return vals
}
// Times parses with RFC3339 format and returns list of time.Time divided by given delimiter.
// Any invalid input will be treated as zero value (0001-01-01 00:00:00 +0000 UTC).
func (k *Key) Times(delim string) []time.Time {
return k.TimesFormat(time.RFC3339, delim)
}
// ValidFloat64s returns list of float64 divided by given delimiter. If some value is not float, then
// it will not be included to result list.
func (k *Key) ValidFloat64s(delim string) []float64 {
vals, _ := k.parseFloat64s(k.Strings(delim), false, false)
return vals
}
// ValidInts returns list of int divided by given delimiter. If some value is not integer, then it will
// not be included to result list.
func (k *Key) ValidInts(delim string) []int {
vals, _ := k.parseInts(k.Strings(delim), false, false)
return vals
}
// ValidInt64s returns list of int64 divided by given delimiter. If some value is not 64-bit integer,
// then it will not be included to result list.
func (k *Key) ValidInt64s(delim string) []int64 {
vals, _ := k.parseInt64s(k.Strings(delim), false, false)
return vals
}
// ValidUints returns list of uint divided by given delimiter. If some value is not unsigned integer,
// then it will not be included to result list.
func (k *Key) ValidUints(delim string) []uint {
vals, _ := k.parseUints(k.Strings(delim), false, false)
return vals
}
// ValidUint64s returns list of uint64 divided by given delimiter. If some value is not 64-bit unsigned
// integer, then it will not be included to result list.
func (k *Key) ValidUint64s(delim string) []uint64 {
vals, _ := k.parseUint64s(k.Strings(delim), false, false)
return vals
}
// ValidBools returns list of bool divided by given delimiter. If some value is not 64-bit unsigned
// integer, then it will not be included to result list.
func (k *Key) ValidBools(delim string) []bool {
vals, _ := k.parseBools(k.Strings(delim), false, false)
return vals
}
// ValidTimesFormat parses with given format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimesFormat(format, delim string) []time.Time {
vals, _ := k.parseTimesFormat(format, k.Strings(delim), false, false)
return vals
}
// ValidTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter.
func (k *Key) ValidTimes(delim string) []time.Time {
return k.ValidTimesFormat(time.RFC3339, delim)
}
// StrictFloat64s returns list of float64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictFloat64s(delim string) ([]float64, error) {
return k.parseFloat64s(k.Strings(delim), false, true)
}
// StrictInts returns list of int divided by given delimiter or error on first invalid input.
func (k *Key) StrictInts(delim string) ([]int, error) {
return k.parseInts(k.Strings(delim), false, true)
}
// StrictInt64s returns list of int64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictInt64s(delim string) ([]int64, error) {
return k.parseInt64s(k.Strings(delim), false, true)
}
// StrictUints returns list of uint divided by given delimiter or error on first invalid input.
func (k *Key) StrictUints(delim string) ([]uint, error) {
return k.parseUints(k.Strings(delim), false, true)
}
// StrictUint64s returns list of uint64 divided by given delimiter or error on first invalid input.
func (k *Key) StrictUint64s(delim string) ([]uint64, error) {
return k.parseUint64s(k.Strings(delim), false, true)
}
// StrictBools returns list of bool divided by given delimiter or error on first invalid input.
func (k *Key) StrictBools(delim string) ([]bool, error) {
return k.parseBools(k.Strings(delim), false, true)
}
// StrictTimesFormat parses with given format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimesFormat(format, delim string) ([]time.Time, error) {
return k.parseTimesFormat(format, k.Strings(delim), false, true)
}
// StrictTimes parses with RFC3339 format and returns list of time.Time divided by given delimiter
// or error on first invalid input.
func (k *Key) StrictTimes(delim string) ([]time.Time, error) {
return k.StrictTimesFormat(time.RFC3339, delim)
}
// parseBools transforms strings to bools.
func (k *Key) parseBools(strs []string, addInvalid, returnOnInvalid bool) ([]bool, error) {
vals := make([]bool, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := parseBool(str)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(bool))
}
}
return vals, err
}
// parseFloat64s transforms strings to float64s.
func (k *Key) parseFloat64s(strs []string, addInvalid, returnOnInvalid bool) ([]float64, error) {
vals := make([]float64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseFloat(str, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(float64))
}
}
return vals, err
}
// parseInts transforms strings to ints.
func (k *Key) parseInts(strs []string, addInvalid, returnOnInvalid bool) ([]int, error) {
vals := make([]int, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseInt(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, int(val.(int64)))
}
}
return vals, err
}
// parseInt64s transforms strings to int64s.
func (k *Key) parseInt64s(strs []string, addInvalid, returnOnInvalid bool) ([]int64, error) {
vals := make([]int64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseInt(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(int64))
}
}
return vals, err
}
// parseUints transforms strings to uints.
func (k *Key) parseUints(strs []string, addInvalid, returnOnInvalid bool) ([]uint, error) {
vals := make([]uint, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseUint(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, uint(val.(uint64)))
}
}
return vals, err
}
// parseUint64s transforms strings to uint64s.
func (k *Key) parseUint64s(strs []string, addInvalid, returnOnInvalid bool) ([]uint64, error) {
vals := make([]uint64, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := strconv.ParseUint(str, 0, 64)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(uint64))
}
}
return vals, err
}
type Parser func(str string) (interface{}, error)
// parseTimesFormat transforms strings to times in given format.
func (k *Key) parseTimesFormat(format string, strs []string, addInvalid, returnOnInvalid bool) ([]time.Time, error) {
vals := make([]time.Time, 0, len(strs))
parser := func(str string) (interface{}, error) {
val, err := time.Parse(format, str)
return val, err
}
rawVals, err := k.doParse(strs, addInvalid, returnOnInvalid, parser)
if err == nil {
for _, val := range rawVals {
vals = append(vals, val.(time.Time))
}
}
return vals, err
}
// doParse transforms strings to different types
func (k *Key) doParse(strs []string, addInvalid, returnOnInvalid bool, parser Parser) ([]interface{}, error) {
vals := make([]interface{}, 0, len(strs))
for _, str := range strs {
val, err := parser(str)
if err != nil && returnOnInvalid {
return nil, err
}
if err == nil || addInvalid {
vals = append(vals, val)
}
}
return vals, nil
}
// SetValue changes key value.
func (k *Key) SetValue(v string) {
if k.s.f.BlockMode {
k.s.f.lock.Lock()
defer k.s.f.lock.Unlock()
}
k.value = v
k.s.keysHash[k.name] = v
}
-520
View File
@@ -1,520 +0,0 @@
// Copyright 2015 Unknwon
//
// 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 ini
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"strconv"
"strings"
"unicode"
)
const minReaderBufferSize = 4096
var pythonMultiline = regexp.MustCompile(`^([\t\f ]+)(.*)`)
type parserOptions struct {
IgnoreContinuation bool
IgnoreInlineComment bool
AllowPythonMultilineValues bool
SpaceBeforeInlineComment bool
UnescapeValueDoubleQuotes bool
UnescapeValueCommentSymbols bool
PreserveSurroundedQuote bool
DebugFunc DebugFunc
ReaderBufferSize int
}
type parser struct {
buf *bufio.Reader
options parserOptions
isEOF bool
count int
comment *bytes.Buffer
}
func (p *parser) debug(format string, args ...interface{}) {
if p.options.DebugFunc != nil {
p.options.DebugFunc(fmt.Sprintf(format, args...))
}
}
func newParser(r io.Reader, opts parserOptions) *parser {
size := opts.ReaderBufferSize
if size < minReaderBufferSize {
size = minReaderBufferSize
}
return &parser{
buf: bufio.NewReaderSize(r, size),
options: opts,
count: 1,
comment: &bytes.Buffer{},
}
}
// BOM handles header of UTF-8, UTF-16 LE and UTF-16 BE's BOM format.
// http://en.wikipedia.org/wiki/Byte_order_mark#Representations_of_byte_order_marks_by_encoding
func (p *parser) BOM() error {
mask, err := p.buf.Peek(2)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 2 {
return nil
}
switch {
case mask[0] == 254 && mask[1] == 255:
fallthrough
case mask[0] == 255 && mask[1] == 254:
_, err = p.buf.Read(mask)
if err != nil {
return err
}
case mask[0] == 239 && mask[1] == 187:
mask, err := p.buf.Peek(3)
if err != nil && err != io.EOF {
return err
} else if len(mask) < 3 {
return nil
}
if mask[2] == 191 {
_, err = p.buf.Read(mask)
if err != nil {
return err
}
}
}
return nil
}
func (p *parser) readUntil(delim byte) ([]byte, error) {
data, err := p.buf.ReadBytes(delim)
if err != nil {
if err == io.EOF {
p.isEOF = true
} else {
return nil, err
}
}
return data, nil
}
func cleanComment(in []byte) ([]byte, bool) {
i := bytes.IndexAny(in, "#;")
if i == -1 {
return nil, false
}
return in[i:], true
}
func readKeyName(delimiters string, in []byte) (string, int, error) {
line := string(in)
// Check if key name surrounded by quotes.
var keyQuote string
if line[0] == '"' {
if len(line) > 6 && line[0:3] == `"""` {
keyQuote = `"""`
} else {
keyQuote = `"`
}
} else if line[0] == '`' {
keyQuote = "`"
}
// Get out key name
var endIdx int
if len(keyQuote) > 0 {
startIdx := len(keyQuote)
// FIXME: fail case -> """"""name"""=value
pos := strings.Index(line[startIdx:], keyQuote)
if pos == -1 {
return "", -1, fmt.Errorf("missing closing key quote: %s", line)
}
pos += startIdx
// Find key-value delimiter
i := strings.IndexAny(line[pos+startIdx:], delimiters)
if i < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
endIdx = pos + i
return strings.TrimSpace(line[startIdx:pos]), endIdx + startIdx + 1, nil
}
endIdx = strings.IndexAny(line, delimiters)
if endIdx < 0 {
return "", -1, ErrDelimiterNotFound{line}
}
if endIdx == 0 {
return "", -1, ErrEmptyKeyName{line}
}
return strings.TrimSpace(line[0:endIdx]), endIdx + 1, nil
}
func (p *parser) readMultilines(line, val, valQuote string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := string(data)
pos := strings.LastIndex(next, valQuote)
if pos > -1 {
val += next[:pos]
comment, has := cleanComment([]byte(next[pos:]))
if has {
p.comment.Write(bytes.TrimSpace(comment))
}
break
}
val += next
if p.isEOF {
return "", fmt.Errorf("missing closing key quote from %q to %q", line, next)
}
}
return val, nil
}
func (p *parser) readContinuationLines(val string) (string, error) {
for {
data, err := p.readUntil('\n')
if err != nil {
return "", err
}
next := strings.TrimSpace(string(data))
if len(next) == 0 {
break
}
val += next
if val[len(val)-1] != '\\' {
break
}
val = val[:len(val)-1]
}
return val, nil
}
// hasSurroundedQuote check if and only if the first and last characters
// are quotes \" or \'.
// It returns false if any other parts also contain same kind of quotes.
func hasSurroundedQuote(in string, quote byte) bool {
return len(in) >= 2 && in[0] == quote && in[len(in)-1] == quote &&
strings.IndexByte(in[1:], quote) == len(in)-2
}
func (p *parser) readValue(in []byte, bufferSize int) (string, error) {
line := strings.TrimLeftFunc(string(in), unicode.IsSpace)
if len(line) == 0 {
if p.options.AllowPythonMultilineValues && len(in) > 0 && in[len(in)-1] == '\n' {
return p.readPythonMultilines(line, bufferSize)
}
return "", nil
}
var valQuote string
if len(line) > 3 && line[0:3] == `"""` {
valQuote = `"""`
} else if line[0] == '`' {
valQuote = "`"
} else if p.options.UnescapeValueDoubleQuotes && line[0] == '"' {
valQuote = `"`
}
if len(valQuote) > 0 {
startIdx := len(valQuote)
pos := strings.LastIndex(line[startIdx:], valQuote)
// Check for multi-line value
if pos == -1 {
return p.readMultilines(line, line[startIdx:], valQuote)
}
if p.options.UnescapeValueDoubleQuotes && valQuote == `"` {
return strings.Replace(line[startIdx:pos+startIdx], `\"`, `"`, -1), nil
}
return line[startIdx : pos+startIdx], nil
}
lastChar := line[len(line)-1]
// Won't be able to reach here if value only contains whitespace
line = strings.TrimSpace(line)
trimmedLastChar := line[len(line)-1]
// Check continuation lines when desired
if !p.options.IgnoreContinuation && trimmedLastChar == '\\' {
return p.readContinuationLines(line[:len(line)-1])
}
// Check if ignore inline comment
if !p.options.IgnoreInlineComment {
var i int
if p.options.SpaceBeforeInlineComment {
i = strings.Index(line, " #")
if i == -1 {
i = strings.Index(line, " ;")
}
} else {
i = strings.IndexAny(line, "#;")
}
if i > -1 {
p.comment.WriteString(line[i:])
line = strings.TrimSpace(line[:i])
}
}
// Trim single and double quotes
if (hasSurroundedQuote(line, '\'') ||
hasSurroundedQuote(line, '"')) && !p.options.PreserveSurroundedQuote {
line = line[1 : len(line)-1]
} else if len(valQuote) == 0 && p.options.UnescapeValueCommentSymbols {
line = strings.ReplaceAll(line, `\;`, ";")
line = strings.ReplaceAll(line, `\#`, "#")
} else if p.options.AllowPythonMultilineValues && lastChar == '\n' {
return p.readPythonMultilines(line, bufferSize)
}
return line, nil
}
func (p *parser) readPythonMultilines(line string, bufferSize int) (string, error) {
parserBufferPeekResult, _ := p.buf.Peek(bufferSize)
peekBuffer := bytes.NewBuffer(parserBufferPeekResult)
for {
peekData, peekErr := peekBuffer.ReadBytes('\n')
if peekErr != nil && peekErr != io.EOF {
p.debug("readPythonMultilines: failed to peek with error: %v", peekErr)
return "", peekErr
}
p.debug("readPythonMultilines: parsing %q", string(peekData))
peekMatches := pythonMultiline.FindStringSubmatch(string(peekData))
p.debug("readPythonMultilines: matched %d parts", len(peekMatches))
for n, v := range peekMatches {
p.debug(" %d: %q", n, v)
}
// Return if not a Python multiline value.
if len(peekMatches) != 3 {
p.debug("readPythonMultilines: end of value, got: %q", line)
return line, nil
}
// Advance the parser reader (buffer) in-sync with the peek buffer.
_, err := p.buf.Discard(len(peekData))
if err != nil {
p.debug("readPythonMultilines: failed to skip to the end, returning error")
return "", err
}
line += "\n" + peekMatches[0]
}
}
// parse parses data through an io.Reader.
func (f *File) parse(reader io.Reader) (err error) {
p := newParser(reader, parserOptions{
IgnoreContinuation: f.options.IgnoreContinuation,
IgnoreInlineComment: f.options.IgnoreInlineComment,
AllowPythonMultilineValues: f.options.AllowPythonMultilineValues,
SpaceBeforeInlineComment: f.options.SpaceBeforeInlineComment,
UnescapeValueDoubleQuotes: f.options.UnescapeValueDoubleQuotes,
UnescapeValueCommentSymbols: f.options.UnescapeValueCommentSymbols,
PreserveSurroundedQuote: f.options.PreserveSurroundedQuote,
DebugFunc: f.options.DebugFunc,
ReaderBufferSize: f.options.ReaderBufferSize,
})
if err = p.BOM(); err != nil {
return fmt.Errorf("BOM: %v", err)
}
// Ignore error because default section name is never empty string.
name := DefaultSection
if f.options.Insensitive || f.options.InsensitiveSections {
name = strings.ToLower(DefaultSection)
}
section, _ := f.NewSection(name)
// This "last" is not strictly equivalent to "previous one" if current key is not the first nested key
var isLastValueEmpty bool
var lastRegularKey *Key
var line []byte
var inUnparseableSection bool
// NOTE: Iterate and increase `currentPeekSize` until
// the size of the parser buffer is found.
// TODO(unknwon): When Golang 1.10 is the lowest version supported, replace with `parserBufferSize := p.buf.Size()`.
parserBufferSize := 0
// NOTE: Peek 4kb at a time.
currentPeekSize := minReaderBufferSize
if f.options.AllowPythonMultilineValues {
for {
peekBytes, _ := p.buf.Peek(currentPeekSize)
peekBytesLength := len(peekBytes)
if parserBufferSize >= peekBytesLength {
break
}
currentPeekSize *= 2
parserBufferSize = peekBytesLength
}
}
for !p.isEOF {
line, err = p.readUntil('\n')
if err != nil {
return err
}
if f.options.AllowNestedValues &&
isLastValueEmpty && len(line) > 0 {
if line[0] == ' ' || line[0] == '\t' {
err = lastRegularKey.addNestedValue(string(bytes.TrimSpace(line)))
if err != nil {
return err
}
continue
}
}
line = bytes.TrimLeftFunc(line, unicode.IsSpace)
if len(line) == 0 {
continue
}
// Comments
if line[0] == '#' || line[0] == ';' {
// Note: we do not care ending line break,
// it is needed for adding second line,
// so just clean it once at the end when set to value.
p.comment.Write(line)
continue
}
// Section
if line[0] == '[' {
// Read to the next ']' (TODO: support quoted strings)
closeIdx := bytes.LastIndexByte(line, ']')
if closeIdx == -1 {
return fmt.Errorf("unclosed section: %s", line)
}
name := string(line[1:closeIdx])
section, err = f.NewSection(name)
if err != nil {
return err
}
comment, has := cleanComment(line[closeIdx+1:])
if has {
p.comment.Write(comment)
}
section.Comment = strings.TrimSpace(p.comment.String())
// Reset auto-counter and comments
p.comment.Reset()
p.count = 1
// Nested values can't span sections
isLastValueEmpty = false
inUnparseableSection = false
for i := range f.options.UnparseableSections {
if f.options.UnparseableSections[i] == name ||
((f.options.Insensitive || f.options.InsensitiveSections) && strings.EqualFold(f.options.UnparseableSections[i], name)) {
inUnparseableSection = true
continue
}
}
continue
}
if inUnparseableSection {
section.isRawSection = true
section.rawBody += string(line)
continue
}
kname, offset, err := readKeyName(f.options.KeyValueDelimiters, line)
if err != nil {
switch {
// Treat as boolean key when desired, and whole line is key name.
case IsErrDelimiterNotFound(err):
switch {
case f.options.AllowBooleanKeys:
kname, err := p.readValue(line, parserBufferSize)
if err != nil {
return err
}
key, err := section.NewBooleanKey(kname)
if err != nil {
return err
}
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
continue
case f.options.SkipUnrecognizableLines:
continue
}
case IsErrEmptyKeyName(err) && f.options.SkipUnrecognizableLines:
continue
}
return err
}
// Auto increment.
isAutoIncr := false
if kname == "-" {
isAutoIncr = true
kname = "#" + strconv.Itoa(p.count)
p.count++
}
value, err := p.readValue(line[offset:], parserBufferSize)
if err != nil {
return err
}
isLastValueEmpty = len(value) == 0
key, err := section.NewKey(kname, value)
if err != nil {
return err
}
key.isAutoIncrement = isAutoIncr
key.Comment = strings.TrimSpace(p.comment.String())
p.comment.Reset()
lastRegularKey = key
}
return nil
}
-256
View File
@@ -1,256 +0,0 @@
// Copyright 2014 Unknwon
//
// 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 ini
import (
"errors"
"fmt"
"strings"
)
// Section represents a config section.
type Section struct {
f *File
Comment string
name string
keys map[string]*Key
keyList []string
keysHash map[string]string
isRawSection bool
rawBody string
}
func newSection(f *File, name string) *Section {
return &Section{
f: f,
name: name,
keys: make(map[string]*Key),
keyList: make([]string, 0, 10),
keysHash: make(map[string]string),
}
}
// Name returns name of Section.
func (s *Section) Name() string {
return s.name
}
// Body returns rawBody of Section if the section was marked as unparseable.
// It still follows the other rules of the INI format surrounding leading/trailing whitespace.
func (s *Section) Body() string {
return strings.TrimSpace(s.rawBody)
}
// SetBody updates body content only if section is raw.
func (s *Section) SetBody(body string) {
if !s.isRawSection {
return
}
s.rawBody = body
}
// NewKey creates a new key to given section.
func (s *Section) NewKey(name, val string) (*Key, error) {
if len(name) == 0 {
return nil, errors.New("error creating new key: empty key name")
} else if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
name = strings.ToLower(name)
}
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
if inSlice(name, s.keyList) {
if s.f.options.AllowShadows {
if err := s.keys[name].addShadow(val); err != nil {
return nil, err
}
} else {
s.keys[name].value = val
s.keysHash[name] = val
}
return s.keys[name], nil
}
s.keyList = append(s.keyList, name)
s.keys[name] = newKey(s, name, val)
s.keysHash[name] = val
return s.keys[name], nil
}
// NewBooleanKey creates a new boolean type key to given section.
func (s *Section) NewBooleanKey(name string) (*Key, error) {
key, err := s.NewKey(name, "true")
if err != nil {
return nil, err
}
key.isBooleanType = true
return key, nil
}
// GetKey returns key in section by given name.
func (s *Section) GetKey(name string) (*Key, error) {
if s.f.BlockMode {
s.f.lock.RLock()
}
if s.f.options.Insensitive || s.f.options.InsensitiveKeys {
name = strings.ToLower(name)
}
key := s.keys[name]
if s.f.BlockMode {
s.f.lock.RUnlock()
}
if key == nil {
// Check if it is a child-section.
sname := s.name
for {
if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
return sec.GetKey(name)
}
break
}
return nil, fmt.Errorf("error when getting key of section %q: key %q not exists", s.name, name)
}
return key, nil
}
// HasKey returns true if section contains a key with given name.
func (s *Section) HasKey(name string) bool {
key, _ := s.GetKey(name)
return key != nil
}
// Deprecated: Use "HasKey" instead.
func (s *Section) Haskey(name string) bool {
return s.HasKey(name)
}
// HasValue returns true if section contains given raw value.
func (s *Section) HasValue(value string) bool {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
for _, k := range s.keys {
if value == k.value {
return true
}
}
return false
}
// Key assumes named Key exists in section and returns a zero-value when not.
func (s *Section) Key(name string) *Key {
key, err := s.GetKey(name)
if err != nil {
// It's OK here because the only possible error is empty key name,
// but if it's empty, this piece of code won't be executed.
key, _ = s.NewKey(name, "")
return key
}
return key
}
// Keys returns list of keys of section.
func (s *Section) Keys() []*Key {
keys := make([]*Key, len(s.keyList))
for i := range s.keyList {
keys[i] = s.Key(s.keyList[i])
}
return keys
}
// ParentKeys returns list of keys of parent section.
func (s *Section) ParentKeys() []*Key {
var parentKeys []*Key
sname := s.name
for {
if i := strings.LastIndex(sname, s.f.options.ChildSectionDelimiter); i > -1 {
sname = sname[:i]
sec, err := s.f.GetSection(sname)
if err != nil {
continue
}
parentKeys = append(parentKeys, sec.Keys()...)
} else {
break
}
}
return parentKeys
}
// KeyStrings returns list of key names of section.
func (s *Section) KeyStrings() []string {
list := make([]string, len(s.keyList))
copy(list, s.keyList)
return list
}
// KeysHash returns keys hash consisting of names and values.
func (s *Section) KeysHash() map[string]string {
if s.f.BlockMode {
s.f.lock.RLock()
defer s.f.lock.RUnlock()
}
hash := make(map[string]string, len(s.keysHash))
for key, value := range s.keysHash {
hash[key] = value
}
return hash
}
// DeleteKey deletes a key from section.
func (s *Section) DeleteKey(name string) {
if s.f.BlockMode {
s.f.lock.Lock()
defer s.f.lock.Unlock()
}
for i, k := range s.keyList {
if k == name {
s.keyList = append(s.keyList[:i], s.keyList[i+1:]...)
delete(s.keys, name)
delete(s.keysHash, name)
return
}
}
}
// ChildSections returns a list of child sections of current section.
// For example, "[parent.child1]" and "[parent.child12]" are child sections
// of section "[parent]".
func (s *Section) ChildSections() []*Section {
prefix := s.name + s.f.options.ChildSectionDelimiter
children := make([]*Section, 0, 3)
for _, name := range s.f.sectionList {
if strings.HasPrefix(name, prefix) {
children = append(children, s.f.sections[name]...)
}
}
return children
}
-747
View File
@@ -1,747 +0,0 @@
// Copyright 2014 Unknwon
//
// 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 ini
import (
"bytes"
"errors"
"fmt"
"reflect"
"strings"
"time"
"unicode"
)
// NameMapper represents a ini tag name mapper.
type NameMapper func(string) string
// Built-in name getters.
var (
// SnackCase converts to format SNACK_CASE.
SnackCase NameMapper = func(raw string) string {
newstr := make([]rune, 0, len(raw))
for i, chr := range raw {
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
if i > 0 {
newstr = append(newstr, '_')
}
}
newstr = append(newstr, unicode.ToUpper(chr))
}
return string(newstr)
}
// TitleUnderscore converts to format title_underscore.
TitleUnderscore NameMapper = func(raw string) string {
newstr := make([]rune, 0, len(raw))
for i, chr := range raw {
if isUpper := 'A' <= chr && chr <= 'Z'; isUpper {
if i > 0 {
newstr = append(newstr, '_')
}
chr -= 'A' - 'a'
}
newstr = append(newstr, chr)
}
return string(newstr)
}
)
func (s *Section) parseFieldName(raw, actual string) string {
if len(actual) > 0 {
return actual
}
if s.f.NameMapper != nil {
return s.f.NameMapper(raw)
}
return raw
}
func parseDelim(actual string) string {
if len(actual) > 0 {
return actual
}
return ","
}
var reflectTime = reflect.TypeOf(time.Now()).Kind()
// setSliceWithProperType sets proper values to slice based on its type.
func setSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
var strs []string
if allowShadow {
strs = key.StringsWithShadows(delim)
} else {
strs = key.Strings(delim)
}
numVals := len(strs)
if numVals == 0 {
return nil
}
var vals interface{}
var err error
sliceOf := field.Type().Elem().Kind()
switch sliceOf {
case reflect.String:
vals = strs
case reflect.Int:
vals, err = key.parseInts(strs, true, false)
case reflect.Int64:
vals, err = key.parseInt64s(strs, true, false)
case reflect.Uint:
vals, err = key.parseUints(strs, true, false)
case reflect.Uint64:
vals, err = key.parseUint64s(strs, true, false)
case reflect.Float64:
vals, err = key.parseFloat64s(strs, true, false)
case reflect.Bool:
vals, err = key.parseBools(strs, true, false)
case reflectTime:
vals, err = key.parseTimesFormat(time.RFC3339, strs, true, false)
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
if err != nil && isStrict {
return err
}
slice := reflect.MakeSlice(field.Type(), numVals, numVals)
for i := 0; i < numVals; i++ {
switch sliceOf {
case reflect.String:
slice.Index(i).Set(reflect.ValueOf(vals.([]string)[i]))
case reflect.Int:
slice.Index(i).Set(reflect.ValueOf(vals.([]int)[i]))
case reflect.Int64:
slice.Index(i).Set(reflect.ValueOf(vals.([]int64)[i]))
case reflect.Uint:
slice.Index(i).Set(reflect.ValueOf(vals.([]uint)[i]))
case reflect.Uint64:
slice.Index(i).Set(reflect.ValueOf(vals.([]uint64)[i]))
case reflect.Float64:
slice.Index(i).Set(reflect.ValueOf(vals.([]float64)[i]))
case reflect.Bool:
slice.Index(i).Set(reflect.ValueOf(vals.([]bool)[i]))
case reflectTime:
slice.Index(i).Set(reflect.ValueOf(vals.([]time.Time)[i]))
}
}
field.Set(slice)
return nil
}
func wrapStrictError(err error, isStrict bool) error {
if isStrict {
return err
}
return nil
}
// setWithProperType sets proper value to field based on its type,
// but it does not return error for failing parsing,
// because we want to use default value that is already assigned to struct.
func setWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow, isStrict bool) error {
vt := t
isPtr := t.Kind() == reflect.Ptr
if isPtr {
vt = t.Elem()
}
switch vt.Kind() {
case reflect.String:
stringVal := key.String()
if isPtr {
field.Set(reflect.ValueOf(&stringVal))
} else if len(stringVal) > 0 {
field.SetString(key.String())
}
case reflect.Bool:
boolVal, err := key.Bool()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&boolVal))
} else {
field.SetBool(boolVal)
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
// ParseDuration will not return err for `0`, so check the type name
if vt.Name() == "Duration" {
durationVal, err := key.Duration()
if err != nil {
if intVal, err := key.Int64(); err == nil {
field.SetInt(intVal)
return nil
}
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&durationVal))
} else if int64(durationVal) > 0 {
field.Set(reflect.ValueOf(durationVal))
}
return nil
}
intVal, err := key.Int64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetInt(intVal)
field.Set(pv)
} else {
field.SetInt(intVal)
}
// byte is an alias for uint8, so supporting uint8 breaks support for byte
case reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64:
durationVal, err := key.Duration()
// Skip zero value
if err == nil && uint64(durationVal) > 0 {
if isPtr {
field.Set(reflect.ValueOf(&durationVal))
} else {
field.Set(reflect.ValueOf(durationVal))
}
return nil
}
uintVal, err := key.Uint64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetUint(uintVal)
field.Set(pv)
} else {
field.SetUint(uintVal)
}
case reflect.Float32, reflect.Float64:
floatVal, err := key.Float64()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
pv := reflect.New(t.Elem())
pv.Elem().SetFloat(floatVal)
field.Set(pv)
} else {
field.SetFloat(floatVal)
}
case reflectTime:
timeVal, err := key.Time()
if err != nil {
return wrapStrictError(err, isStrict)
}
if isPtr {
field.Set(reflect.ValueOf(&timeVal))
} else {
field.Set(reflect.ValueOf(timeVal))
}
case reflect.Slice:
return setSliceWithProperType(key, field, delim, allowShadow, isStrict)
default:
return fmt.Errorf("unsupported type %q", t)
}
return nil
}
func parseTagOptions(tag string) (rawName string, omitEmpty bool, allowShadow bool, allowNonUnique bool, extends bool) {
opts := strings.SplitN(tag, ",", 5)
rawName = opts[0]
for _, opt := range opts[1:] {
omitEmpty = omitEmpty || (opt == "omitempty")
allowShadow = allowShadow || (opt == "allowshadow")
allowNonUnique = allowNonUnique || (opt == "nonunique")
extends = extends || (opt == "extends")
}
return rawName, omitEmpty, allowShadow, allowNonUnique, extends
}
// mapToField maps the given value to the matching field of the given section.
// The sectionIndex is the index (if non unique sections are enabled) to which the value should be added.
func (s *Section) mapToField(val reflect.Value, isStrict bool, sectionIndex int, sectionName string) error {
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
field := val.Field(i)
tpField := typ.Field(i)
tag := tpField.Tag.Get("ini")
if tag == "-" {
continue
}
rawName, _, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
fieldName := s.parseFieldName(tpField.Name, rawName)
if len(fieldName) == 0 || !field.CanSet() {
continue
}
isStruct := tpField.Type.Kind() == reflect.Struct
isStructPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct
isAnonymousPtr := tpField.Type.Kind() == reflect.Ptr && tpField.Anonymous
if isAnonymousPtr {
field.Set(reflect.New(tpField.Type.Elem()))
}
if extends && (isAnonymousPtr || (isStruct && tpField.Anonymous)) {
if isStructPtr && field.IsNil() {
field.Set(reflect.New(tpField.Type.Elem()))
}
fieldSection := s
if rawName != "" {
sectionName = s.name + s.f.options.ChildSectionDelimiter + rawName
if secs, err := s.f.SectionsByName(sectionName); err == nil && sectionIndex < len(secs) {
fieldSection = secs[sectionIndex]
}
}
if err := fieldSection.mapToField(field, isStrict, sectionIndex, sectionName); err != nil {
return fmt.Errorf("map to field %q: %v", fieldName, err)
}
} else if isAnonymousPtr || isStruct || isStructPtr {
if secs, err := s.f.SectionsByName(fieldName); err == nil {
if len(secs) <= sectionIndex {
return fmt.Errorf("there are not enough sections (%d <= %d) for the field %q", len(secs), sectionIndex, fieldName)
}
// Only set the field to non-nil struct value if we have a section for it.
// Otherwise, we end up with a non-nil struct ptr even though there is no data.
if isStructPtr && field.IsNil() {
field.Set(reflect.New(tpField.Type.Elem()))
}
if err = secs[sectionIndex].mapToField(field, isStrict, sectionIndex, fieldName); err != nil {
return fmt.Errorf("map to field %q: %v", fieldName, err)
}
continue
}
}
// Map non-unique sections
if allowNonUnique && tpField.Type.Kind() == reflect.Slice {
newField, err := s.mapToSlice(fieldName, field, isStrict)
if err != nil {
return fmt.Errorf("map to slice %q: %v", fieldName, err)
}
field.Set(newField)
continue
}
if key, err := s.GetKey(fieldName); err == nil {
delim := parseDelim(tpField.Tag.Get("delim"))
if err = setWithProperType(tpField.Type, key, field, delim, allowShadow, isStrict); err != nil {
return fmt.Errorf("set field %q: %v", fieldName, err)
}
}
}
return nil
}
// mapToSlice maps all sections with the same name and returns the new value.
// The type of the Value must be a slice.
func (s *Section) mapToSlice(secName string, val reflect.Value, isStrict bool) (reflect.Value, error) {
secs, err := s.f.SectionsByName(secName)
if err != nil {
return reflect.Value{}, err
}
typ := val.Type().Elem()
for i, sec := range secs {
elem := reflect.New(typ)
if err = sec.mapToField(elem, isStrict, i, sec.name); err != nil {
return reflect.Value{}, fmt.Errorf("map to field from section %q: %v", secName, err)
}
val = reflect.Append(val, elem.Elem())
}
return val, nil
}
// mapTo maps a section to object v.
func (s *Section) mapTo(v interface{}, isStrict bool) error {
typ := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
} else {
return errors.New("not a pointer to a struct")
}
if typ.Kind() == reflect.Slice {
newField, err := s.mapToSlice(s.name, val, isStrict)
if err != nil {
return err
}
val.Set(newField)
return nil
}
return s.mapToField(val, isStrict, 0, s.name)
}
// MapTo maps section to given struct.
func (s *Section) MapTo(v interface{}) error {
return s.mapTo(v, false)
}
// StrictMapTo maps section to given struct in strict mode,
// which returns all possible error including value parsing error.
func (s *Section) StrictMapTo(v interface{}) error {
return s.mapTo(v, true)
}
// MapTo maps file to given struct.
func (f *File) MapTo(v interface{}) error {
return f.Section("").MapTo(v)
}
// StrictMapTo maps file to given struct in strict mode,
// which returns all possible error including value parsing error.
func (f *File) StrictMapTo(v interface{}) error {
return f.Section("").StrictMapTo(v)
}
// MapToWithMapper maps data sources to given struct with name mapper.
func MapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error {
cfg, err := Load(source, others...)
if err != nil {
return err
}
cfg.NameMapper = mapper
return cfg.MapTo(v)
}
// StrictMapToWithMapper maps data sources to given struct with name mapper in strict mode,
// which returns all possible error including value parsing error.
func StrictMapToWithMapper(v interface{}, mapper NameMapper, source interface{}, others ...interface{}) error {
cfg, err := Load(source, others...)
if err != nil {
return err
}
cfg.NameMapper = mapper
return cfg.StrictMapTo(v)
}
// MapTo maps data sources to given struct.
func MapTo(v, source interface{}, others ...interface{}) error {
return MapToWithMapper(v, nil, source, others...)
}
// StrictMapTo maps data sources to given struct in strict mode,
// which returns all possible error including value parsing error.
func StrictMapTo(v, source interface{}, others ...interface{}) error {
return StrictMapToWithMapper(v, nil, source, others...)
}
// reflectSliceWithProperType does the opposite thing as setSliceWithProperType.
func reflectSliceWithProperType(key *Key, field reflect.Value, delim string, allowShadow bool) error {
slice := field.Slice(0, field.Len())
if field.Len() == 0 {
return nil
}
sliceOf := field.Type().Elem().Kind()
if allowShadow {
var keyWithShadows *Key
for i := 0; i < field.Len(); i++ {
var val string
switch sliceOf {
case reflect.String:
val = slice.Index(i).String()
case reflect.Int, reflect.Int64:
val = fmt.Sprint(slice.Index(i).Int())
case reflect.Uint, reflect.Uint64:
val = fmt.Sprint(slice.Index(i).Uint())
case reflect.Float64:
val = fmt.Sprint(slice.Index(i).Float())
case reflect.Bool:
val = fmt.Sprint(slice.Index(i).Bool())
case reflectTime:
val = slice.Index(i).Interface().(time.Time).Format(time.RFC3339)
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
if i == 0 {
keyWithShadows = newKey(key.s, key.name, val)
} else {
_ = keyWithShadows.AddShadow(val)
}
}
*key = *keyWithShadows
return nil
}
var buf bytes.Buffer
for i := 0; i < field.Len(); i++ {
switch sliceOf {
case reflect.String:
buf.WriteString(slice.Index(i).String())
case reflect.Int, reflect.Int64:
buf.WriteString(fmt.Sprint(slice.Index(i).Int()))
case reflect.Uint, reflect.Uint64:
buf.WriteString(fmt.Sprint(slice.Index(i).Uint()))
case reflect.Float64:
buf.WriteString(fmt.Sprint(slice.Index(i).Float()))
case reflect.Bool:
buf.WriteString(fmt.Sprint(slice.Index(i).Bool()))
case reflectTime:
buf.WriteString(slice.Index(i).Interface().(time.Time).Format(time.RFC3339))
default:
return fmt.Errorf("unsupported type '[]%s'", sliceOf)
}
buf.WriteString(delim)
}
key.SetValue(buf.String()[:buf.Len()-len(delim)])
return nil
}
// reflectWithProperType does the opposite thing as setWithProperType.
func reflectWithProperType(t reflect.Type, key *Key, field reflect.Value, delim string, allowShadow bool) error {
switch t.Kind() {
case reflect.String:
key.SetValue(field.String())
case reflect.Bool:
key.SetValue(fmt.Sprint(field.Bool()))
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
key.SetValue(fmt.Sprint(field.Int()))
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
key.SetValue(fmt.Sprint(field.Uint()))
case reflect.Float32, reflect.Float64:
key.SetValue(fmt.Sprint(field.Float()))
case reflectTime:
key.SetValue(fmt.Sprint(field.Interface().(time.Time).Format(time.RFC3339)))
case reflect.Slice:
return reflectSliceWithProperType(key, field, delim, allowShadow)
case reflect.Ptr:
if !field.IsNil() {
return reflectWithProperType(t.Elem(), key, field.Elem(), delim, allowShadow)
}
default:
return fmt.Errorf("unsupported type %q", t)
}
return nil
}
// CR: copied from encoding/json/encode.go with modifications of time.Time support.
// TODO: add more test coverage.
func isEmptyValue(v reflect.Value) bool {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
return v.Len() == 0
case reflect.Bool:
return !v.Bool()
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return v.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return v.Uint() == 0
case reflect.Float32, reflect.Float64:
return v.Float() == 0
case reflect.Interface, reflect.Ptr:
return v.IsNil()
case reflectTime:
t, ok := v.Interface().(time.Time)
return ok && t.IsZero()
}
return false
}
// StructReflector is the interface implemented by struct types that can extract themselves into INI objects.
type StructReflector interface {
ReflectINIStruct(*File) error
}
func (s *Section) reflectFrom(val reflect.Value) error {
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
if !val.Field(i).CanInterface() {
continue
}
field := val.Field(i)
tpField := typ.Field(i)
tag := tpField.Tag.Get("ini")
if tag == "-" {
continue
}
rawName, omitEmpty, allowShadow, allowNonUnique, extends := parseTagOptions(tag)
if omitEmpty && isEmptyValue(field) {
continue
}
if r, ok := field.Interface().(StructReflector); ok {
return r.ReflectINIStruct(s.f)
}
fieldName := s.parseFieldName(tpField.Name, rawName)
if len(fieldName) == 0 || !field.CanSet() {
continue
}
if extends && tpField.Anonymous && (tpField.Type.Kind() == reflect.Ptr || tpField.Type.Kind() == reflect.Struct) {
if err := s.reflectFrom(field); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
continue
}
if (tpField.Type.Kind() == reflect.Ptr && tpField.Type.Elem().Kind() == reflect.Struct) ||
(tpField.Type.Kind() == reflect.Struct && tpField.Type.Name() != "Time") {
// Note: The only error here is section doesn't exist.
sec, err := s.f.GetSection(fieldName)
if err != nil {
// Note: fieldName can never be empty here, ignore error.
sec, _ = s.f.NewSection(fieldName)
}
// Add comment from comment tag
if len(sec.Comment) == 0 {
sec.Comment = tpField.Tag.Get("comment")
}
if err = sec.reflectFrom(field); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
continue
}
if allowNonUnique && tpField.Type.Kind() == reflect.Slice {
slice := field.Slice(0, field.Len())
if field.Len() == 0 {
return nil
}
sliceOf := field.Type().Elem().Kind()
for i := 0; i < field.Len(); i++ {
if sliceOf != reflect.Struct && sliceOf != reflect.Ptr {
return fmt.Errorf("field %q is not a slice of pointer or struct", fieldName)
}
sec, err := s.f.NewSection(fieldName)
if err != nil {
return err
}
// Add comment from comment tag
if len(sec.Comment) == 0 {
sec.Comment = tpField.Tag.Get("comment")
}
if err := sec.reflectFrom(slice.Index(i)); err != nil {
return fmt.Errorf("reflect from field %q: %v", fieldName, err)
}
}
continue
}
// Note: Same reason as section.
key, err := s.GetKey(fieldName)
if err != nil {
key, _ = s.NewKey(fieldName, "")
}
// Add comment from comment tag
if len(key.Comment) == 0 {
key.Comment = tpField.Tag.Get("comment")
}
delim := parseDelim(tpField.Tag.Get("delim"))
if err = reflectWithProperType(tpField.Type, key, field, delim, allowShadow); err != nil {
return fmt.Errorf("reflect field %q: %v", fieldName, err)
}
}
return nil
}
// ReflectFrom reflects section from given struct. It overwrites existing ones.
func (s *Section) ReflectFrom(v interface{}) error {
typ := reflect.TypeOf(v)
val := reflect.ValueOf(v)
if s.name != DefaultSection && s.f.options.AllowNonUniqueSections &&
(typ.Kind() == reflect.Slice || typ.Kind() == reflect.Ptr) {
// Clear sections to make sure none exists before adding the new ones
s.f.DeleteSection(s.name)
if typ.Kind() == reflect.Ptr {
sec, err := s.f.NewSection(s.name)
if err != nil {
return err
}
return sec.reflectFrom(val.Elem())
}
slice := val.Slice(0, val.Len())
sliceOf := val.Type().Elem().Kind()
if sliceOf != reflect.Ptr {
return fmt.Errorf("not a slice of pointers")
}
for i := 0; i < slice.Len(); i++ {
sec, err := s.f.NewSection(s.name)
if err != nil {
return err
}
err = sec.reflectFrom(slice.Index(i))
if err != nil {
return fmt.Errorf("reflect from %dth field: %v", i, err)
}
}
return nil
}
if typ.Kind() == reflect.Ptr {
val = val.Elem()
} else {
return errors.New("not a pointer to a struct")
}
return s.reflectFrom(val)
}
// ReflectFrom reflects file from given struct.
func (f *File) ReflectFrom(v interface{}) error {
return f.Section("").ReflectFrom(v)
}
// ReflectFromWithMapper reflects data sources from given struct with name mapper.
func ReflectFromWithMapper(cfg *File, v interface{}, mapper NameMapper) error {
cfg.NameMapper = mapper
return cfg.ReflectFrom(v)
}
// ReflectFrom reflects data sources from given struct.
func ReflectFrom(cfg *File, v interface{}) error {
return ReflectFromWithMapper(cfg, v, nil)
}
-3
View File
@@ -2,9 +2,6 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
//go:build go1.16
// +build go1.16
package capabilities
import (
@@ -40,7 +40,8 @@
"type": "boolean"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "and",
@@ -95,7 +96,8 @@
"type": "boolean"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "array.concat",
@@ -385,7 +387,8 @@
"type": "array"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "cast_boolean",
@@ -399,7 +402,8 @@
"type": "boolean"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "cast_null",
@@ -413,7 +417,8 @@
"type": "null"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "cast_object",
@@ -435,7 +440,8 @@
"type": "object"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "cast_set",
@@ -452,7 +458,8 @@
"type": "set"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "cast_string",
@@ -466,7 +473,8 @@
"type": "string"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "ceil",
@@ -2975,7 +2983,8 @@
"type": "boolean"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "net.lookup_ip_addr",
@@ -3493,7 +3502,8 @@
"type": "boolean"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "regex.find_all_string_submatch_n",
@@ -3808,7 +3818,8 @@
"type": "set"
},
"type": "function"
}
},
"deprecated": true
},
{
"name": "sort",
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -133,7 +133,7 @@ func (c *Compiler) removeUnusedCode() error {
}
caller, ok := c.funcs[callerName]
if !ok {
return fmt.Errorf("caller not found: %s (%s)", cg[i][0], callerName)
continue // without a caller, it should get removed anyways (right?)
}
callee, ok := c.funcs[calleeName]
if !ok {
+237 -192
View File
@@ -32,7 +32,7 @@ const (
opaWasmABIMinorVersionVar = "opa_wasm_abi_minor_version"
)
// nolint: deadcode,varcheck
// nolint: varcheck
const (
opaTypeNull int32 = iota + 1
opaTypeBoolean
@@ -90,6 +90,7 @@ var builtinsFunctions = map[string]string{
ast.Floor.Name: "opa_arith_floor",
ast.Rem.Name: "opa_arith_rem",
ast.ArrayConcat.Name: "opa_array_concat",
ast.ArrayFlatten.Name: "opa_array_flatten",
ast.ArrayReverse.Name: "opa_array_reverse",
ast.ArraySlice.Name: "opa_array_slice",
ast.SetDiff.Name: "opa_set_diff",
@@ -162,6 +163,7 @@ var builtinsFunctions = map[string]string{
ast.TrimRight.Name: "opa_strings_trim_right",
ast.TrimSuffix.Name: "opa_strings_trim_suffix",
ast.TrimSpace.Name: "opa_strings_trim_space",
ast.InternalTemplateString.Name: "opa_template_string",
ast.NumbersRange.Name: "opa_numbers_range",
ast.ToNumber.Name: "opa_to_number",
ast.WalkBuiltin.Name: "opa_value_transitive_closure",
@@ -414,7 +416,7 @@ func (c *Compiler) initModule() error {
},
},
},
Init: bytes.Repeat([]byte{0}, int(heapBase-offset)),
Init: make([]byte, int(heapBase-offset)),
})
return nil
@@ -1058,9 +1060,11 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
},
})
case *ir.AssignIntStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs, instruction.I64Const{Value: stmt.Value})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueNumberSetInt)})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Target)},
instruction.I64Const{Value: stmt.Value},
instruction.Call{Index: c.function(opaValueNumberSetInt)},
)
case *ir.ScanStmt:
if err := c.compileScan(stmt, &instrs); err != nil {
return nil, err
@@ -1073,12 +1077,14 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
}
case *ir.DotStmt:
if loc, ok := stmt.Source.Value.(ir.Local); ok {
instrs = append(instrs, instruction.GetLocal{Index: c.local(loc)})
instrs = append(instrs, c.instrRead(stmt.Key))
instrs = append(instrs, instruction.Call{Index: c.function(opaValueGet)})
instrs = append(instrs, instruction.TeeLocal{Index: c.local(stmt.Target)})
instrs = append(instrs, instruction.I32Eqz{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(loc)},
c.instrRead(stmt.Key),
instruction.Call{Index: c.function(opaValueGet)},
instruction.TeeLocal{Index: c.local(stmt.Target)},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
)
} else {
// Booleans and string sources would lead to the BrIf (since opa_value_get
// on them returns 0), so let's skip trying that.
@@ -1086,97 +1092,131 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
break
}
case *ir.LenStmt:
instrs = append(instrs, c.instrRead(stmt.Source))
instrs = append(instrs, instruction.Call{Index: c.function(opaValueLength)})
instrs = append(instrs, instruction.Call{Index: c.function(opaNumberSize)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
c.instrRead(stmt.Source),
instruction.Call{Index: c.function(opaValueLength)},
instruction.Call{Index: c.function(opaNumberSize)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.EqualStmt:
instrs = append(instrs, c.instrRead(stmt.A))
instrs = append(instrs, c.instrRead(stmt.B))
instrs = append(instrs, instruction.Call{Index: c.function(opaValueCompare)})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
c.instrRead(stmt.A),
c.instrRead(stmt.B),
instruction.Call{Index: c.function(opaValueCompare)},
instruction.BrIf{Index: 0},
)
case *ir.NotEqualStmt:
instrs = append(instrs, c.instrRead(stmt.A))
instrs = append(instrs, c.instrRead(stmt.B))
instrs = append(instrs, instruction.Call{Index: c.function(opaValueCompare)})
instrs = append(instrs, instruction.I32Eqz{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
c.instrRead(stmt.A),
c.instrRead(stmt.B),
instruction.Call{Index: c.function(opaValueCompare)},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
)
case *ir.MakeNullStmt:
instrs = append(instrs, instruction.Call{Index: c.function(opaNull)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.Call{Index: c.function(opaNull)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.MakeNumberIntStmt:
instrs = append(instrs, instruction.I64Const{Value: stmt.Value})
instrs = append(instrs, instruction.Call{Index: c.function(opaNumberInt)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.I64Const{Value: stmt.Value},
instruction.Call{Index: c.function(opaNumberInt)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.MakeNumberRefStmt:
instrs = append(instrs, instruction.I32Const{Value: c.stringAddr(stmt.Index)})
instrs = append(instrs, instruction.I32Const{Value: int32(len(c.policy.Static.Strings[stmt.Index].Value))})
instrs = append(instrs, instruction.Call{Index: c.function(opaNumberRef)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.I32Const{Value: c.stringAddr(stmt.Index)},
instruction.I32Const{Value: int32(len(c.policy.Static.Strings[stmt.Index].Value))},
instruction.Call{Index: c.function(opaNumberRef)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.MakeArrayStmt:
instrs = append(instrs, instruction.I32Const{Value: stmt.Capacity})
instrs = append(instrs, instruction.Call{Index: c.function(opaArrayWithCap)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.I32Const{Value: stmt.Capacity},
instruction.Call{Index: c.function(opaArrayWithCap)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.MakeObjectStmt:
instrs = append(instrs, instruction.Call{Index: c.function(opaObject)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.Call{Index: c.function(opaObject)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.MakeSetStmt:
instrs = append(instrs, instruction.Call{Index: c.function(opaSet)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.Call{Index: c.function(opaSet)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.IsArrayStmt:
if loc, ok := stmt.Source.Value.(ir.Local); ok {
instrs = append(instrs, instruction.GetLocal{Index: c.local(loc)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueType)})
instrs = append(instrs, instruction.I32Const{Value: opaTypeArray})
instrs = append(instrs, instruction.I32Ne{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(loc)},
instruction.Call{Index: c.function(opaValueType)},
instruction.I32Const{Value: opaTypeArray},
instruction.I32Ne{},
instruction.BrIf{Index: 0},
)
} else {
instrs = append(instrs, instruction.Br{Index: 0})
break
}
case *ir.IsObjectStmt:
if loc, ok := stmt.Source.Value.(ir.Local); ok {
instrs = append(instrs, instruction.GetLocal{Index: c.local(loc)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueType)})
instrs = append(instrs, instruction.I32Const{Value: opaTypeObject})
instrs = append(instrs, instruction.I32Ne{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(loc)},
instruction.Call{Index: c.function(opaValueType)},
instruction.I32Const{Value: opaTypeObject},
instruction.I32Ne{},
instruction.BrIf{Index: 0},
)
} else {
instrs = append(instrs, instruction.Br{Index: 0})
break
}
case *ir.IsSetStmt:
if loc, ok := stmt.Source.Value.(ir.Local); ok {
instrs = append(instrs, instruction.GetLocal{Index: c.local(loc)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueType)})
instrs = append(instrs, instruction.I32Const{Value: opaTypeSet})
instrs = append(instrs, instruction.I32Ne{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(loc)},
instruction.Call{Index: c.function(opaValueType)},
instruction.I32Const{Value: opaTypeSet},
instruction.I32Ne{},
instruction.BrIf{Index: 0},
)
} else {
instrs = append(instrs, instruction.Br{Index: 0})
break
}
case *ir.IsUndefinedStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Source)})
instrs = append(instrs, instruction.I32Const{Value: 0})
instrs = append(instrs, instruction.I32Ne{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Source)},
instruction.I32Const{Value: 0},
instruction.I32Ne{},
instruction.BrIf{Index: 0},
)
case *ir.ResetLocalStmt:
instrs = append(instrs, instruction.I32Const{Value: 0})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.I32Const{Value: 0},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.IsDefinedStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Source)})
instrs = append(instrs, instruction.I32Eqz{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Source)},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
)
case *ir.ArrayAppendStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Array)})
instrs = append(instrs, c.instrRead(stmt.Value))
instrs = append(instrs, instruction.Call{Index: c.function(opaArrayAppend)})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Array)},
c.instrRead(stmt.Value),
instruction.Call{Index: c.function(opaArrayAppend)},
)
case *ir.ObjectInsertStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Object)})
instrs = append(instrs, c.instrRead(stmt.Key))
instrs = append(instrs, c.instrRead(stmt.Value))
instrs = append(instrs, instruction.Call{Index: c.function(opaObjectInsert)})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Object)},
c.instrRead(stmt.Key),
c.instrRead(stmt.Value),
instruction.Call{Index: c.function(opaObjectInsert)},
)
case *ir.ObjectInsertOnceStmt:
tmp := c.genLocal()
instrs = append(instrs, instruction.Block{
@@ -1203,14 +1243,18 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
},
})
case *ir.ObjectMergeStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.A)})
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.B)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueMerge)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(stmt.Target)})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.A)},
instruction.GetLocal{Index: c.local(stmt.B)},
instruction.Call{Index: c.function(opaValueMerge)},
instruction.SetLocal{Index: c.local(stmt.Target)},
)
case *ir.SetAddStmt:
instrs = append(instrs, instruction.GetLocal{Index: c.local(stmt.Set)})
instrs = append(instrs, c.instrRead(stmt.Value))
instrs = append(instrs, instruction.Call{Index: c.function(opaSetAdd)})
instrs = append(instrs,
instruction.GetLocal{Index: c.local(stmt.Set)},
c.instrRead(stmt.Value),
instruction.Call{Index: c.function(opaSetAdd)},
)
default:
var buf bytes.Buffer
err := ir.Pretty(&buf, stmt)
@@ -1226,8 +1270,7 @@ func (c *Compiler) compileBlock(block *ir.Block) ([]instruction.Instruction, err
func (c *Compiler) compileScan(scan *ir.ScanStmt, result *[]instruction.Instruction) error {
var instrs = *result
instrs = append(instrs, instruction.I32Const{Value: 0})
instrs = append(instrs, instruction.SetLocal{Index: c.local(scan.Key)})
instrs = append(instrs, instruction.I32Const{Value: 0}, instruction.SetLocal{Index: c.local(scan.Key)})
body, err := c.compileScanBlock(scan)
if err != nil {
return err
@@ -1242,23 +1285,22 @@ func (c *Compiler) compileScan(scan *ir.ScanStmt, result *[]instruction.Instruct
}
func (c *Compiler) compileScanBlock(scan *ir.ScanStmt) ([]instruction.Instruction, error) {
var instrs []instruction.Instruction
// Execute iterator.
instrs = append(instrs, instruction.GetLocal{Index: c.local(scan.Source)})
instrs = append(instrs, instruction.GetLocal{Index: c.local(scan.Key)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueIter)})
// Check for emptiness.
instrs = append(instrs, instruction.TeeLocal{Index: c.local(scan.Key)})
instrs = append(instrs, instruction.I32Eqz{})
instrs = append(instrs, instruction.BrIf{Index: 1})
// Load value.
instrs = append(instrs, instruction.GetLocal{Index: c.local(scan.Source)})
instrs = append(instrs, instruction.GetLocal{Index: c.local(scan.Key)})
instrs = append(instrs, instruction.Call{Index: c.function(opaValueGet)})
instrs = append(instrs, instruction.SetLocal{Index: c.local(scan.Value)})
//nolint:prealloc // instruction list is known and fixed, clearer as slice literal
instrs := []instruction.Instruction{
// Execute iterator.
instruction.GetLocal{Index: c.local(scan.Source)},
instruction.GetLocal{Index: c.local(scan.Key)},
instruction.Call{Index: c.function(opaValueIter)},
// Check for emptiness.
instruction.TeeLocal{Index: c.local(scan.Key)},
instruction.I32Eqz{},
instruction.BrIf{Index: 1},
// Load value.
instruction.GetLocal{Index: c.local(scan.Source)},
instruction.GetLocal{Index: c.local(scan.Key)},
instruction.Call{Index: c.function(opaValueGet)},
instruction.SetLocal{Index: c.local(scan.Value)},
}
// Loop body.
nested, err := c.compileBlock(scan.Block)
@@ -1278,8 +1320,7 @@ func (c *Compiler) compileNot(not *ir.NotStmt, result *[]instruction.Instruction
// generate and initialize condition variable
cond := c.genLocal()
instrs = append(instrs, instruction.I32Const{Value: 1})
instrs = append(instrs, instruction.SetLocal{Index: cond})
instrs = append(instrs, instruction.I32Const{Value: 1}, instruction.SetLocal{Index: cond})
nested, err := c.compileBlock(not.Block)
if err != nil {
@@ -1287,14 +1328,15 @@ func (c *Compiler) compileNot(not *ir.NotStmt, result *[]instruction.Instruction
}
// unset condition variable if end of block is reached
nested = append(nested, instruction.I32Const{Value: 0})
nested = append(nested, instruction.SetLocal{Index: cond})
instrs = append(instrs, instruction.Block{Instrs: nested})
// break out of block if condition variable was unset
instrs = append(instrs, instruction.GetLocal{Index: cond})
instrs = append(instrs, instruction.I32Eqz{})
instrs = append(instrs, instruction.BrIf{Index: 0})
instrs = append(instrs, instruction.Block{Instrs: append(nested,
instruction.I32Const{Value: 0},
instruction.SetLocal{Index: cond},
)},
// break out of block if condition variable was unset
instruction.GetLocal{Index: cond},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
)
*result = instrs
return nil
@@ -1304,34 +1346,36 @@ func (c *Compiler) compileWithStmt(with *ir.WithStmt, result *[]instruction.Inst
var instrs = *result
save := c.genLocal()
instrs = append(instrs, instruction.Call{Index: c.function(opaMemoizePush)})
instrs = append(instrs, instruction.GetLocal{Index: c.local(with.Local)})
instrs = append(instrs, instruction.SetLocal{Index: save})
instrs = append(instrs,
instruction.Call{Index: c.function(opaMemoizePush)},
instruction.GetLocal{Index: c.local(with.Local)},
instruction.SetLocal{Index: save},
)
if len(with.Path) == 0 {
instrs = append(instrs, c.instrRead(with.Value))
instrs = append(instrs, instruction.SetLocal{Index: c.local(with.Local)})
instrs = append(instrs, c.instrRead(with.Value), instruction.SetLocal{Index: c.local(with.Local)})
} else {
instrs = c.compileUpsert(with.Local, with.Path, with.Value, with.Location, instrs)
}
undefined := c.genLocal()
instrs = append(instrs, instruction.I32Const{Value: 1})
instrs = append(instrs, instruction.SetLocal{Index: undefined})
instrs = append(instrs, instruction.I32Const{Value: 1}, instruction.SetLocal{Index: undefined})
nested, err := c.compileBlock(with.Block)
if err != nil {
return err
}
nested = append(nested, instruction.I32Const{Value: 0})
nested = append(nested, instruction.SetLocal{Index: undefined})
instrs = append(instrs, instruction.Block{Instrs: nested})
instrs = append(instrs, instruction.GetLocal{Index: save})
instrs = append(instrs, instruction.SetLocal{Index: c.local(with.Local)})
instrs = append(instrs, instruction.Call{Index: c.function(opaMemoizePop)})
instrs = append(instrs, instruction.GetLocal{Index: undefined})
instrs = append(instrs, instruction.BrIf{Index: 0})
nested = append(nested, instruction.I32Const{Value: 0}, instruction.SetLocal{Index: undefined})
instrs = append(instrs,
instruction.Block{Instrs: nested},
instruction.GetLocal{Index: save},
instruction.SetLocal{Index: c.local(with.Local)},
instruction.Call{Index: c.function(opaMemoizePop)},
instruction.GetLocal{Index: undefined},
instruction.BrIf{Index: 0},
)
*result = instrs
@@ -1339,37 +1383,38 @@ func (c *Compiler) compileWithStmt(with *ir.WithStmt, result *[]instruction.Inst
}
func (c *Compiler) compileUpsert(local ir.Local, path []int, value ir.Operand, _ ir.Location, instrs []instruction.Instruction) []instruction.Instruction {
lcopy := c.genLocal() // holds copy of local
instrs = append(instrs, instruction.GetLocal{Index: c.local(local)})
instrs = append(instrs, instruction.SetLocal{Index: lcopy})
// Shallow copy the local if defined otherwise initialize to an empty object.
instrs = append(instrs, instruction.Block{
Instrs: []instruction.Instruction{
instruction.Block{Instrs: []instruction.Instruction{
instruction.GetLocal{Index: lcopy},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
instruction.GetLocal{Index: lcopy},
instruction.Call{Index: c.function(opaValueShallowCopy)},
instrs = append(instrs,
instruction.GetLocal{Index: c.local(local)},
instruction.SetLocal{Index: lcopy},
// Shallow copy the local if defined otherwise initialize to an empty object.
instruction.Block{
Instrs: []instruction.Instruction{
instruction.Block{Instrs: []instruction.Instruction{
instruction.GetLocal{Index: lcopy},
instruction.I32Eqz{},
instruction.BrIf{Index: 0},
instruction.GetLocal{Index: lcopy},
instruction.Call{Index: c.function(opaValueShallowCopy)},
instruction.TeeLocal{Index: lcopy},
instruction.SetLocal{Index: c.local(local)},
instruction.Br{Index: 1},
}},
instruction.Call{Index: c.function(opaObject)},
instruction.TeeLocal{Index: lcopy},
instruction.SetLocal{Index: c.local(local)},
instruction.Br{Index: 1},
}},
instruction.Call{Index: c.function(opaObject)},
instruction.TeeLocal{Index: lcopy},
instruction.SetLocal{Index: c.local(local)},
},
})
},
})
// Initialize the locals that specify the path of the upsert operation.
lpath := make(map[int]uint32, len(path))
for i := range path {
lpath[i] = c.genLocal()
instrs = append(instrs, instruction.I32Const{Value: c.opaStringAddr(path[i])})
instrs = append(instrs, instruction.SetLocal{Index: lpath[i]})
instrs = append(instrs,
instruction.I32Const{Value: c.opaStringAddr(path[i])},
instruction.SetLocal{Index: lpath[i]},
)
}
// Generate a block that traverses the path of the upsert operation,
@@ -1379,36 +1424,34 @@ func (c *Compiler) compileUpsert(local ir.Local, path []int, value ir.Operand, _
ltemp := c.genLocal()
for i := range len(path) - 1 {
// Lookup the next part of the path.
inner = append(inner, instruction.GetLocal{Index: lcopy})
inner = append(inner, instruction.GetLocal{Index: lpath[i]})
inner = append(inner, instruction.Call{Index: c.function(opaValueGet)})
inner = append(inner, instruction.SetLocal{Index: ltemp})
// If the next node is missing, break.
inner = append(inner, instruction.GetLocal{Index: ltemp})
inner = append(inner, instruction.I32Eqz{})
inner = append(inner, instruction.BrIf{Index: uint32(i)})
// If the next node is not an object, break.
inner = append(inner, instruction.GetLocal{Index: ltemp})
inner = append(inner, instruction.Call{Index: c.function(opaValueType)})
inner = append(inner, instruction.I32Const{Value: opaTypeObject})
inner = append(inner, instruction.I32Ne{})
inner = append(inner, instruction.BrIf{Index: uint32(i)})
// Otherwise, shallow copy the next node node and insert into the copy
// before continuing.
inner = append(inner, instruction.GetLocal{Index: ltemp})
inner = append(inner, instruction.Call{Index: c.function(opaValueShallowCopy)})
inner = append(inner, instruction.SetLocal{Index: ltemp})
inner = append(inner, instruction.GetLocal{Index: lcopy})
inner = append(inner, instruction.GetLocal{Index: lpath[i]})
inner = append(inner, instruction.GetLocal{Index: ltemp})
inner = append(inner, instruction.Call{Index: c.function(opaObjectInsert)})
inner = append(inner, instruction.GetLocal{Index: ltemp})
inner = append(inner, instruction.SetLocal{Index: lcopy})
inner = append(inner,
// Lookup the next part of the path.
instruction.GetLocal{Index: lcopy},
instruction.GetLocal{Index: lpath[i]},
instruction.Call{Index: c.function(opaValueGet)},
instruction.SetLocal{Index: ltemp},
// If the next node is missing, break.
instruction.GetLocal{Index: ltemp},
instruction.I32Eqz{},
instruction.BrIf{Index: uint32(i)},
// If the next node is not an object, break.
instruction.GetLocal{Index: ltemp},
instruction.Call{Index: c.function(opaValueType)},
instruction.I32Const{Value: opaTypeObject},
instruction.I32Ne{},
instruction.BrIf{Index: uint32(i)},
// Otherwise, shallow copy the next node node and insert into the copy
// before continuing.
instruction.GetLocal{Index: ltemp},
instruction.Call{Index: c.function(opaValueShallowCopy)},
instruction.SetLocal{Index: ltemp},
instruction.GetLocal{Index: lcopy},
instruction.GetLocal{Index: lpath[i]},
instruction.GetLocal{Index: ltemp},
instruction.Call{Index: c.function(opaObjectInsert)},
instruction.GetLocal{Index: ltemp},
instruction.SetLocal{Index: lcopy},
)
}
inner = append(inner, instruction.Br{Index: uint32(len(path) - 1)})
@@ -1418,31 +1461,33 @@ func (c *Compiler) compileUpsert(local ir.Local, path []int, value ir.Operand, _
lval := c.genLocal()
for i := range len(path) - 1 {
block = append(block, instruction.Block{Instrs: inner})
block = append(block, instruction.Call{Index: c.function(opaObject)})
block = append(block, instruction.SetLocal{Index: lval})
block = append(block, instruction.GetLocal{Index: lcopy})
block = append(block, instruction.GetLocal{Index: lpath[i]})
block = append(block, instruction.GetLocal{Index: lval})
block = append(block, instruction.Call{Index: c.function(opaObjectInsert)})
block = append(block, instruction.GetLocal{Index: lval})
block = append(block, instruction.SetLocal{Index: lcopy})
block = append(block,
instruction.Block{Instrs: inner},
instruction.Call{Index: c.function(opaObject)},
instruction.SetLocal{Index: lval},
instruction.GetLocal{Index: lcopy},
instruction.GetLocal{Index: lpath[i]},
instruction.GetLocal{Index: lval},
instruction.Call{Index: c.function(opaObjectInsert)},
instruction.GetLocal{Index: lval},
instruction.SetLocal{Index: lcopy},
)
inner = block
block = nil
}
// Finish by inserting the statement's value into the shallow copied node.
instrs = append(instrs, instruction.Block{Instrs: inner})
instrs = append(instrs, instruction.GetLocal{Index: lcopy})
instrs = append(instrs, instruction.GetLocal{Index: lpath[len(path)-1]})
instrs = append(instrs, c.instrRead(value))
instrs = append(instrs, instruction.Call{Index: c.function(opaObjectInsert)})
return instrs
return append(instrs,
instruction.Block{Instrs: inner},
instruction.GetLocal{Index: lcopy},
instruction.GetLocal{Index: lpath[len(path)-1]},
c.instrRead(value),
instruction.Call{Index: c.function(opaObjectInsert)},
)
}
func (c *Compiler) compileCallDynamicStmt(stmt *ir.CallDynamicStmt, result *[]instruction.Instruction) error {
instrs := []instruction.Instruction{}
instrs := make([]instruction.Instruction, 0, 3+3*len(stmt.Path)+len(stmt.Args)+10)
larray := c.genLocal()
lidx := c.genLocal()
@@ -1515,7 +1560,7 @@ func (c *Compiler) compileCallStmt(stmt *ir.CallStmt, result *[]instruction.Inst
func (c *Compiler) compileInternalCall(stmt *ir.CallStmt, index uint32, result *[]instruction.Instruction) error {
instrs := []instruction.Instruction{}
instrs := make([]instruction.Instruction, 0, len(stmt.Args)+4)
// Prepare function args and call.
for _, arg := range stmt.Args {
-176
View File
@@ -1,176 +0,0 @@
// Copyright 2020 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 config implements helper functions to parse OPA's configuration.
package config
import (
"encoding/json"
"fmt"
"os"
"regexp"
"strings"
"sigs.k8s.io/yaml"
"github.com/open-policy-agent/opa/internal/strvals"
"github.com/open-policy-agent/opa/v1/keys"
"github.com/open-policy-agent/opa/v1/logging"
"github.com/open-policy-agent/opa/v1/plugins/rest"
"github.com/open-policy-agent/opa/v1/tracing"
"github.com/open-policy-agent/opa/v1/util"
)
// ServiceOptions stores the options passed to ParseServicesConfig
type ServiceOptions struct {
Raw json.RawMessage
AuthPlugin rest.AuthPluginLookupFunc
Keys map[string]*keys.Config
Logger logging.Logger
DistributedTacingOpts tracing.Options
}
// ParseServicesConfig returns a set of named service clients. The service
// clients can be specified either as an array or as a map. Some systems (e.g.,
// Helm) do not have proper support for configuration values nested under
// arrays, so just support both here.
func ParseServicesConfig(opts ServiceOptions) (map[string]rest.Client, error) {
services := map[string]rest.Client{}
var arr []json.RawMessage
var obj map[string]json.RawMessage
if err := util.Unmarshal(opts.Raw, &arr); err == nil {
for _, s := range arr {
client, err := rest.New(s, opts.Keys, rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
if err != nil {
return nil, err
}
services[client.Service()] = client
}
} else if util.Unmarshal(opts.Raw, &obj) == nil {
for k := range obj {
client, err := rest.New(obj[k], opts.Keys, rest.Name(k), rest.AuthPluginLookup(opts.AuthPlugin), rest.Logger(opts.Logger), rest.DistributedTracingOpts(opts.DistributedTacingOpts))
if err != nil {
return nil, err
}
services[client.Service()] = client
}
} else {
// Return error from array decode as that is the default format.
return nil, err
}
return services, nil
}
// Load implements configuration file loading. The supplied config file will be
// read from disk (if specified) and overrides will be applied. If no config file is
// specified, the overrides can still be applied to an empty config.
func Load(configFile string, overrides []string, overrideFiles []string) ([]byte, error) {
baseConf := map[string]any{}
// User specified config file
if configFile != "" {
var bytes []byte
var err error
bytes, err = os.ReadFile(configFile)
if err != nil {
return nil, err
}
processedConf := subEnvVars(string(bytes))
if err := yaml.Unmarshal([]byte(processedConf), &baseConf); err != nil {
return nil, fmt.Errorf("failed to parse %s: %s", configFile, err)
}
}
overrideConf := map[string]any{}
// User specified a config override via --set
for _, override := range overrides {
processedOverride := subEnvVars(override)
if err := strvals.ParseInto(processedOverride, overrideConf); err != nil {
return nil, fmt.Errorf("failed parsing --set data: %s", err)
}
}
// User specified a config override value via --set-file
for _, override := range overrideFiles {
reader := func(rs []rune) (any, error) {
bytes, err := os.ReadFile(string(rs))
value := strings.TrimSpace(string(bytes))
return value, err
}
if err := strvals.ParseIntoFile(override, overrideConf, reader); err != nil {
return nil, fmt.Errorf("failed parsing --set-file data: %s", err)
}
}
// Merge together base config file and overrides, prefer the overrides
conf := mergeValues(baseConf, overrideConf)
// Take the patched config and marshal back to YAML
return yaml.Marshal(conf)
}
// regex looking for ${...} notation strings
var envRegex = regexp.MustCompile(`(?U:\${.*})`)
// SubEnvVars will look for any environment variables in the passed in string
// with the syntax of ${VAR_NAME} and replace that string with ENV[VAR_NAME]
func SubEnvVars(s string) string {
return subEnvVars(s)
}
func subEnvVars(s string) string {
updatedConfig := envRegex.ReplaceAllStringFunc(s, func(s string) string {
// Trim off the '${' and '}'
if len(s) <= 3 {
// This should never happen..
return ""
}
varName := s[2 : len(s)-1]
// Lookup the variable in the environment. We do not
// play by bash rules: if its undefined we'll keep it
// as-is, it could be replaced somewhere down the line.
// If it's set to "", we'll return that.
if lu, ok := os.LookupEnv(varName); ok {
return lu
}
return s
})
return updatedConfig
}
// mergeValues will merge source and destination map, preferring values from the source map
func mergeValues(dest map[string]any, src map[string]any) map[string]any {
for k, v := range src {
// If the key doesn't exist already, then just set the key to that value
if _, exists := dest[k]; !exists {
dest[k] = v
continue
}
nextMap, ok := v.(map[string]any)
// If it isn't another map, overwrite the value
if !ok {
dest[k] = v
continue
}
// Edge case: If the key exists in the destination, but isn't a map
destMap, isMap := dest[k].(map[string]any)
// If the source map has a map for this key, prefer it
if !isMap {
dest[k] = v
continue
}
// If we got to this point, it is a map in both, so merge them
dest[k] = mergeValues(destMap, nextMap)
}
return dest
}
@@ -2,10 +2,12 @@
// which supports lookups, sets, appends, insertions, and deletions.
package bitvector
import "slices"
// A BitVector is a variable sized vector of bits. It supports
// lookups, sets, appends, insertions, and deletions.
//
// This class is not thread safe.
// Operations are not thread safe.
type BitVector struct {
data []byte
length int
@@ -14,10 +16,25 @@ type BitVector struct {
// NewBitVector creates and initializes a new bit vector with length
// elements, using data as its initial contents.
func NewBitVector(data []byte, length int) *BitVector {
return &BitVector{
data: data,
length: length,
return &BitVector{data: data, length: length}
}
func (vector *BitVector) Clear() *BitVector {
if vector == nil {
return nil
}
clear(vector.data)
vector.length = 0
return vector
}
func (vector *BitVector) Reset(size, length int) *BitVector {
clear(vector.data)
vector.data = slices.Grow(vector.data, size)[:size]
vector.length = length
return vector
}
// Bytes returns a slice of the contents of the bit vector. If the caller changes the returned slice,
+163 -103
View File
@@ -148,13 +148,17 @@ package edittree
import (
"errors"
"fmt"
"sort"
"strings"
"github.com/open-policy-agent/opa/internal/edittree/bitvector"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
var refPool = util.NewSlicePool[*ast.Term](1)
var editTreePool = util.NewSyncPool[EditTree]()
// Deletions are encoded with a nil value pointer.
type EditTree struct {
value *ast.Term
@@ -171,31 +175,66 @@ func NewEditTree(term *ast.Term) *EditTree {
return nil
}
var tree EditTree
return initForTerm(&EditTree{}, term)
}
func EditTreeFromPool(term *ast.Term) *EditTree {
return initForTerm(editTreePool.Get(), term)
}
func Dispose(e *EditTree) {
if e != nil {
editTreePool.Put(e.Reset())
}
}
func (e *EditTree) Reset() *EditTree {
e.value = nil
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
e.eliminated = e.eliminated.Clear()
e.insertions = e.insertions.Clear()
return e
}
func initForTerm(tree *EditTree, term *ast.Term) *EditTree {
tree.value = term
switch x := term.Value.(type) {
case ast.Object, ast.Set:
tree = EditTree{
value: term,
childKeys: map[int]*ast.Term{},
childScalarValues: map[int]*ast.Term{},
childCompositeValues: map[int]*EditTree{},
if tree.childKeys == nil {
tree.childKeys = map[int]*ast.Term{}
}
if tree.childScalarValues == nil {
tree.childScalarValues = map[int]*ast.Term{}
}
if tree.childCompositeValues == nil {
tree.childCompositeValues = map[int]*EditTree{}
}
case *ast.Array:
tree = EditTree{
value: term,
childScalarValues: map[int]*ast.Term{},
childCompositeValues: map[int]*EditTree{},
if tree.childScalarValues == nil {
tree.childScalarValues = map[int]*ast.Term{}
}
if tree.childCompositeValues == nil {
tree.childCompositeValues = map[int]*EditTree{}
}
bytesLength := ((x.Len() - 1) / 8) + 1 // How many bytes to use for the bit-vectors.
tree.eliminated = bitvector.NewBitVector(make([]byte, bytesLength), x.Len())
tree.insertions = bitvector.NewBitVector(make([]byte, bytesLength), x.Len())
default:
tree = EditTree{
value: term,
if tree.eliminated == nil {
tree.eliminated = bitvector.NewBitVector(make([]byte, bytesLength), x.Len())
} else {
tree.eliminated = tree.eliminated.Reset(bytesLength, x.Len())
}
if tree.insertions == nil {
tree.insertions = bitvector.NewBitVector(make([]byte, bytesLength), x.Len())
} else {
tree.insertions = tree.insertions.Reset(bytesLength, x.Len())
}
}
return &tree
return tree
}
// Returns correct (collision-resolved) hash for this term + whether or not
@@ -231,17 +270,14 @@ func isComposite(t *ast.Term) bool {
}
}
//gcassert:inline
func (e *EditTree) setChildKey(hash int, key *ast.Term) {
e.childKeys[hash] = key
}
//gcassert:inline
func (e *EditTree) setChildScalarValue(hash int, value *ast.Term) {
e.childScalarValues[hash] = value
}
//gcassert:inline
func (e *EditTree) setChildCompositeValue(hash int, child *EditTree) {
e.childCompositeValues[hash] = child
}
@@ -278,11 +314,10 @@ func (e *EditTree) Insert(key, value *ast.Term) (*EditTree, error) {
// We only collapse this Set-typed node if a composite type is involved.
if isComposite(key) {
// TODO: Investigate re-rendering *only* the immediate composite children.
collapsed := e.Render()
e.value = collapsed
e.childKeys = map[int]*ast.Term{}
e.childScalarValues = map[int]*ast.Term{}
e.childCompositeValues = map[int]*EditTree{}
e.value = e.Render()
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
}
return e.unsafeInsertSet(key, value), nil
case *ast.Array:
@@ -301,12 +336,13 @@ func (e *EditTree) Insert(key, value *ast.Term) (*EditTree, error) {
}
func (e *EditTree) unsafeInsertObject(key, value *ast.Term) *EditTree {
child := NewEditTree(value)
keyHash, found := e.getKeyHash(key)
if found {
e.deleteChildValue(keyHash)
}
e.setChildKey(keyHash, key)
child := NewEditTree(value)
if isComposite(value) {
e.setChildCompositeValue(keyHash, child)
} else {
@@ -331,10 +367,9 @@ func (e *EditTree) unsafeInsertSet(key, value *ast.Term) *EditTree {
}
func (e *EditTree) unsafeInsertArray(idx int, value *ast.Term) *EditTree {
child := NewEditTree(value)
// Collect insertion indexes above the insertion site for rewriting.
rewritesScalars := []int{}
rewritesComposites := []int{}
var rewritesScalars, rewritesComposites []int
for i := idx; i < e.insertions.Length(); i++ {
if e.insertions.Element(i) == 1 {
if _, ok := e.childScalarValues[i]; ok {
@@ -369,6 +404,8 @@ func (e *EditTree) unsafeInsertArray(idx int, value *ast.Term) *EditTree {
} else {
e.insertions.Insert(1, idx)
}
child := NewEditTree(value)
if isComposite(value) {
e.setChildCompositeValue(idx, child)
} else {
@@ -427,9 +464,9 @@ func (e *EditTree) Delete(key *ast.Term) (*EditTree, error) {
// TODO: Investigate re-rendering *only* the immediate composite children.
collapsed := e.Render()
e.value = collapsed
e.childKeys = map[int]*ast.Term{}
e.childScalarValues = map[int]*ast.Term{}
e.childCompositeValues = map[int]*EditTree{}
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
} else {
keyHash, found := e.getKeyHash(key)
// If child found, replace with delete node. If delete node already existed, error.
@@ -539,7 +576,14 @@ func findIndexOfNthZero(n int, bv *bitvector.BitVector) (int, bool) {
// Helper function for sets/objects when the key isn't present in either
// child map.
func (e *EditTree) fallbackDelete(key *ast.Term) (*EditTree, error) {
value, err := e.value.Value.Find(ast.Ref{key})
// get ref from pool
rptr := refPool.Get(1)
defer refPool.Put(rptr)
ref := *rptr
ref[0] = key
value, err := e.value.Value.Find(ref)
if err != nil {
return nil, fmt.Errorf("cannot delete child key %v that does not exist", key)
}
@@ -595,14 +639,14 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
// Fall back to looking up the key in e.value.
// Extend the tree if key is present. Error otherwise.
if v, err := x.Find(ast.Ref{path[0]}); err == nil {
if v, err := x.Find(path[:1]); err == nil {
child, err := e.Insert(path[0], ast.NewTerm(v))
if err != nil {
return nil, err
}
return child.Unfold(path[1:])
}
return nil, fmt.Errorf("path %v does not exist in object term %v", ast.Ref{path[0]}, e.value.Value)
return nil, fmt.Errorf("path %v does not exist in object term %v", path[0], e.value.Value)
case ast.Set:
// Sets' keys *are* their values, so in order to allow accurate
// traversal, we have to collapse the tree beneath this node,
@@ -611,12 +655,11 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
if isComposite(key) {
collapsed := e.Render()
e.value = collapsed
e.childKeys = map[int]*ast.Term{}
e.childScalarValues = map[int]*ast.Term{}
e.childCompositeValues = map[int]*EditTree{}
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
} else {
keyHash, found := e.getKeyHash(key)
if found {
if keyHash, found := e.getKeyHash(key); found {
if term, ok := e.childScalarValues[keyHash]; ok {
child := NewEditTree(term)
return child.Unfold(path[1:])
@@ -625,14 +668,14 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
// Fall back to looking up the key in e.value.
// Extend the tree if key is present. Error otherwise.
if v, err := e.value.Value.Find(ast.Ref{path[0]}); err == nil {
if v, err := e.value.Value.Find(path[:1]); err == nil {
child, err := e.Insert(path[0], ast.NewTerm(v))
if err != nil {
return nil, err
}
return child.Unfold(path[1:])
}
return nil, fmt.Errorf("path %v does not exist in set term %v", ast.Ref{path[0]}, e.value.Value)
return nil, fmt.Errorf("path %v does not exist in set term %v", path[:1], e.value.Value)
case *ast.Array:
idx, err := toIndex(e.insertions.Length(), path[0])
if err != nil {
@@ -647,13 +690,17 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
idxt := ast.InternedTerm(idx)
rptr := refPool.Get(1)
defer refPool.Put(rptr)
ref := *rptr
ref[0] = idxt
// Fall back to looking up the key in e.value.
// Extend the tree if key is present. Error otherwise.
if v, err := x.Find(ast.Ref{idxt}); err == nil {
if v, err := x.Find(ref); err == nil {
// TODO: Consider a more efficient "Replace" function that special-cases this for arrays instead?
_, err := e.Delete(idxt)
if err != nil {
if _, err := e.Delete(idxt); err != nil {
return nil, err
}
child, err := e.Insert(idxt, ast.NewTerm(v))
@@ -662,10 +709,10 @@ func (e *EditTree) Unfold(path ast.Ref) (*EditTree, error) {
}
return child.Unfold(path[1:])
}
return nil, fmt.Errorf("path %v does not exist in array term %v", ast.Ref{ast.IntNumberTerm(idx)}, e.value.Value)
return nil, fmt.Errorf("path %v does not exist in array term %v", ast.InternedTerm(idx), e.value.Value)
default:
// Catch all primitive types.
return nil, fmt.Errorf("expected composite type for path %v, found value: %v (type: %T)", ast.Ref{path[0]}, x, x)
return nil, fmt.Errorf("expected composite type for path %v, found value: %v (type: %T)", path[0], x, x)
}
}
@@ -787,8 +834,7 @@ func (e *EditTree) Render() *ast.Term {
if t, ok := e.childScalarValues[i]; ok {
out = append(out, t)
} else if child, ok := e.childCompositeValues[i]; ok {
t := child.Render()
out = append(out, t)
out = append(out, child.Render())
} else {
panic(fmt.Errorf("invalid index %d does not exist in array", i))
}
@@ -810,9 +856,10 @@ func (e *EditTree) InsertAtPath(path ast.Ref, value *ast.Term) (*EditTree, error
if len(path) == 0 {
e.value = value
e.childKeys = map[int]*ast.Term{}
e.childScalarValues = map[int]*ast.Term{}
e.childCompositeValues = map[int]*EditTree{}
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
if v, ok := value.Value.(*ast.Array); ok {
bytesLength := ((v.Len() - 1) / 8) + 1 // How many bytes to use for the bit-vectors.
e.eliminated = bitvector.NewBitVector(make([]byte, bytesLength), v.Len())
@@ -921,24 +968,19 @@ func (e *EditTree) Exists(path ast.Ref) bool {
// so that we can accurately unfold it again for an update,
// once we know that the key we care about is present.
if isComposite(key) {
collapsed := e.Render()
e.value = collapsed
e.childKeys = map[int]*ast.Term{}
e.childScalarValues = map[int]*ast.Term{}
e.childCompositeValues = map[int]*EditTree{}
} else {
keyHash, found := e.getKeyHash(key)
if found {
if _, ok := e.childScalarValues[keyHash]; ok {
return len(path) == 1
}
e.value = e.Render()
clear(e.childKeys)
clear(e.childScalarValues)
clear(e.childCompositeValues)
} else if keyHash, found := e.getKeyHash(key); found {
if _, ok := e.childScalarValues[keyHash]; ok {
return len(path) == 1
}
}
// Fallback if child lookup failed.
_, err := e.value.Value.Find(path)
return err == nil
case *ast.Array:
var idx int
idx, err := toIndex(e.insertions.Length(), path[0])
if err != nil {
return false
@@ -951,7 +993,16 @@ func (e *EditTree) Exists(path ast.Ref) bool {
}
// Fallback if child lookup failed.
// We have to ensure that the lookup term is a number here, or Find will fail.
_, err = x.Find(ast.Ref{ast.InternedTerm(idx)}.Concat(path[1:]))
rptr := refPool.Get(len(path))
ref := *rptr
ref[0] = ast.InternedTerm(idx)
copy(ref[1:], path[1:])
_, err = x.Find(ref)
refPool.Put(rptr)
return err == nil
default:
// Catch all primitive types.
@@ -978,8 +1029,7 @@ func toIndex(arrayLength int, term *ast.Term) (int, error) {
if v == "-" {
return arrayLength, nil
}
num := ast.Number(v)
if i, ok = num.Int(); !ok {
if i, ok = ast.Number(v).Int(); !ok {
return 0, errors.New("invalid string for indexing")
}
if v != "0" && strings.HasPrefix(string(v), "0") {
@@ -1004,6 +1054,14 @@ func (e *EditTree) Filter(paths []ast.Ref) *ast.Term {
return nil
}
// term pointer and ref pointer for reuse in lookups and iteration below.
tptr, rptr := ast.TermPtrPool.Get(), refPool.Get(1)
defer func() {
tptr.Value = nil
ast.TermPtrPool.Put(tptr)
refPool.Put(rptr)
}()
// Separate out keys for this level.
// In the event of paths like "a", "a/b", "a/b/c", the "a" path will win out.
// Nil keys, such as "" or [], are not permitted. (legacy behavior)
@@ -1026,7 +1084,8 @@ func (e *EditTree) Filter(paths []ast.Ref) *ast.Term {
renderNow := ast.NewSet(renderNowList...)
// Clear everything out of the pathMap that has a renderNow candidate.
for k := range pathMap {
if renderNow.Contains(ast.NewTerm(k)) {
tptr.Value = k
if renderNow.Contains(tptr) {
delete(pathMap, k)
}
}
@@ -1035,37 +1094,46 @@ func (e *EditTree) Filter(paths []ast.Ref) *ast.Term {
switch e.value.Value.(type) {
case ast.Object:
out := make([][2]*ast.Term, 0, renderNow.Len()+len(pathMap))
ref := *rptr
// Render any finished paths.
renderNow.Foreach(func(k *ast.Term) {
if e.Exists(ast.Ref{k}) {
subtreeResult, _ := e.RenderAtPath(ast.Ref{k})
for _, k := range renderNow.Slice() {
ref[0] = k
if e.Exists(ref) {
subtreeResult, _ := e.RenderAtPath(ref)
out = append(out, [2]*ast.Term{k, subtreeResult})
}
})
}
// Recursively descend remaining paths.
for k, p := range pathMap {
if e.Exists(ast.Ref{ast.NewTerm(k)}) {
child, _ := e.Unfold(ast.Ref{ast.NewTerm(k)})
tptr.Value = k
ref[0] = tptr
if e.Exists(ref) {
child, _ := e.Unfold(ref)
subtreeResult := child.Filter(p)
out = append(out, [2]*ast.Term{ast.NewTerm(k), subtreeResult})
}
}
return ast.ObjectTerm(out...)
case ast.Set:
out := make([]*ast.Term, 0, renderNow.Len()+len(pathMap))
ref := *rptr
// Render any finished paths.
renderNow.Foreach(func(k *ast.Term) {
if e.Exists(ast.Ref{k}) {
subtreeResult, _ := e.RenderAtPath(ast.Ref{k})
for _, k := range renderNow.Slice() {
ref[0] = k
if e.Exists(ref) {
subtreeResult, _ := e.RenderAtPath(ref)
out = append(out, subtreeResult)
}
})
}
// Recursively descend remaining paths.
for k, p := range pathMap {
if e.Exists(ast.Ref{ast.NewTerm(k)}) {
child, _ := e.Unfold(ast.Ref{ast.NewTerm(k)})
subtreeResult := child.Filter(p)
out = append(out, subtreeResult)
tptr.Value = k
ref[0] = tptr
if e.Exists(ref) {
child, _ := e.Unfold(ref)
out = append(out, child.Filter(p))
}
}
return ast.SetTerm(out...)
@@ -1073,27 +1141,25 @@ func (e *EditTree) Filter(paths []ast.Ref) *ast.Term {
// No early exit here, because we might have just deletes on the
// original array. We build a new Array with modified/deleted keys.
out := make([]*ast.Term, 0, renderNow.Len()+len(pathMap))
// Sort array indexes before descending.
idxList := make([]*ast.Term, 0, len(pathMap))
renderNow.Foreach(func(k *ast.Term) {
idxList = append(idxList, k)
})
idxList := append(make([]*ast.Term, 0, renderNow.Len()+len(pathMap)), renderNow.Slice()...)
for k := range pathMap {
idxList = append(idxList, ast.NewTerm(k))
}
sort.Sort(termSlice(idxList))
// Render child or recursively descend as needed.
for i := range idxList {
ref := *rptr
// Render child or recursively descend sorted indexes as needed.
for i := range util.SortedFunc(idxList, ast.TermValueCompare) {
k := idxList[i]
if renderNow.Contains(k) {
if e.Exists(ast.Ref{k}) {
subtreeResult, _ := e.RenderAtPath(ast.Ref{k})
ref[0] = k
if e.Exists(ref) {
if renderNow.Contains(k) {
subtreeResult, _ := e.RenderAtPath(ref)
out = append(out, subtreeResult)
} else {
child, _ := e.Unfold(ref)
out = append(out, child.Filter(pathMap[k.Value]))
}
} else if e.Exists(ast.Ref{k}) {
child, _ := e.Unfold(ast.Ref{k})
subtreeResult := child.Filter(pathMap[k.Value])
out = append(out, subtreeResult)
}
}
return ast.ArrayTerm(out...)
@@ -1101,9 +1167,3 @@ func (e *EditTree) Filter(paths []ast.Ref) *ast.Term {
return e.value
}
}
type termSlice []*ast.Term
func (s termSlice) Less(i, j int) bool { return ast.Compare(s[i].Value, s[j].Value) < 0 }
func (s termSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s termSlice) Len() int { return len(s) }
+53 -20
View File
@@ -4,39 +4,72 @@ import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"io"
"strings"
)
// MustWriteTarGz write the list of file names and content
// into a tarball.
func MustWriteTarGz(files [][2]string) *bytes.Buffer {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
for _, file := range files {
if err := WriteFile(tw, file[0], []byte(file[1])); err != nil {
panic(err)
}
}
return &buf
type TarGzWriter struct {
*tar.Writer
gw *gzip.Writer
}
// WriteFile adds a file header with content to the given tar writer
func WriteFile(tw *tar.Writer, path string, bs []byte) error {
func NewTarGzWriter(w io.Writer) *TarGzWriter {
gw := gzip.NewWriter(w)
tw := tar.NewWriter(gw)
return &TarGzWriter{
Writer: tw,
gw: gw,
}
}
func (tgw *TarGzWriter) WriteFile(path string, bs []byte) (err error) {
hdr := &tar.Header{
Name: "/" + strings.TrimLeft(path, "/"),
Name: path,
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len(bs)),
}
if err := tw.WriteHeader(hdr); err != nil {
if err = tgw.WriteHeader(hdr); err == nil {
_, err = tgw.Write(bs)
}
return err
}
func (tgw *TarGzWriter) WriteJSONFile(path string, v any) error {
buf := &bytes.Buffer{}
if err := json.NewEncoder(buf).Encode(v); err != nil {
return err
}
_, err := tw.Write(bs)
return err
return tgw.WriteFile(path, buf.Bytes())
}
func (tgw *TarGzWriter) Close() error {
return errors.Join(tgw.Writer.Close(), tgw.gw.Close())
}
// MustWriteTarGz writes the list of file names and content into a tarball.
// Paths are prefixed with "/".
func MustWriteTarGz(files [][2]string) *bytes.Buffer {
buf := &bytes.Buffer{}
tgw := NewTarGzWriter(buf)
defer tgw.Close()
for _, file := range files {
if !strings.HasPrefix(file[0], "/") {
file[0] = "/" + file[0]
}
if err := tgw.WriteFile(file[0], []byte(file[1])); err != nil {
panic(err)
}
}
return buf
}
+1 -1
View File
@@ -23,7 +23,7 @@
//
// created 26-02-2013
// nolint: deadcode,unused,varcheck // Package in development (2021).
// nolint:unused,varcheck // Package in development (2021).
package gojsonschema
import (
+2 -2
View File
@@ -1768,7 +1768,7 @@ func (p *Planner) planRef(ref ast.Ref, iter planiter) error {
return errors.New("illegal ref: non-var head")
}
if head.Compare(ast.DefaultRootDocument.Value) == 0 {
if head.Equal(ast.DefaultRootDocument.Value) {
virtual := p.rules.Get(ref[0].Value)
base := &baseptr{local: p.vars.GetOrEmpty(ast.DefaultRootDocument.Value.(ast.Var))}
return p.planRefData(virtual, base, ref, 1, iter)
@@ -2070,7 +2070,7 @@ func (p *Planner) planRefDataExtent(virtual *ruletrie, base *baseptr, iter plani
}
}
if anyKeyNonGround {
var rules []*ast.Rule
rules := make([]*ast.Rule, 0, len(virtual.Children()))
for _, key := range virtual.Children() {
// TODO(sr): skip functions
rules = append(rules, virtual.Get(key).Rules()...)
@@ -158,6 +158,8 @@ func SignV4(headers map[string][]string, method string, theURL *url.URL, body []
// include the values for the signed headers
orderedKeys := util.KeysSorted(headersToSign)
for _, k := range orderedKeys {
// TODO: fix later
//nolint:perfsprint
canonicalReq += k + ":" + strings.Join(headersToSign[k], ",") + "\n"
}
canonicalReq += "\n" // linefeed to terminate headers
+2 -2
View File
@@ -7,16 +7,16 @@ package ref
import (
"errors"
"strings"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/storage"
"github.com/open-policy-agent/opa/v1/util"
)
// ParseDataPath returns a ref from the slash separated path s rooted at data.
// All path segments are treated as identifier strings.
func ParseDataPath(s string) (ast.Ref, error) {
path, ok := storage.ParsePath("/" + strings.TrimPrefix(s, "/"))
path, ok := storage.ParsePath(util.WithPrefix(s, "/"))
if !ok {
return nil, errors.New("invalid path")
}
-218
View File
@@ -1,218 +0,0 @@
// Copyright 2020 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 report provides functions to report OPA's version information to an external service and process the response.
package report
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"runtime"
"strconv"
"strings"
"time"
"github.com/open-policy-agent/opa/internal/semver"
"github.com/open-policy-agent/opa/v1/keys"
"github.com/open-policy-agent/opa/v1/logging"
"github.com/open-policy-agent/opa/v1/version"
"github.com/open-policy-agent/opa/v1/plugins/rest"
"github.com/open-policy-agent/opa/v1/util"
)
// ExternalServiceURL is the base HTTP URL for a github instance used
// to query for more recent version.
// If not otherwise specified, it will use the hard-coded default, api.github.com.
// GHRepo is the repository to use, and defaults to "open-policy-agent/opa"
//
// Override at build time via:
//
// -ldflags "-X github.com/open-policy-agent/opa/internal/report.ExternalServiceURL=<url>"
// -ldflags "-X github.com/open-policy-agent/opa/internal/report.GHRepo=<url>"
//
// ExternalServiceURL will be overridden if the OPA_TELEMETRY_SERVICE_URL environment variable
// is provided.
var ExternalServiceURL = "https://api.github.com"
var GHRepo = "open-policy-agent/opa"
// Reporter reports information such as the version, heap usage about the running OPA instance to an external service
type Reporter interface {
SendReport(ctx context.Context) (*DataResponse, error)
RegisterGatherer(key string, f Gatherer)
}
// Gatherer represents a mechanism to inject additional data in the telemetry report
type Gatherer func(ctx context.Context) (any, error)
// DataResponse represents the data returned by the external service
type DataResponse struct {
Latest ReleaseDetails `json:"latest"`
}
// ReleaseDetails holds information about the latest OPA release
type ReleaseDetails struct {
Download string `json:"download,omitempty"` // link to download the OPA release
ReleaseNotes string `json:"release_notes,omitempty"` // link to the OPA release notes
LatestRelease string `json:"latest_release,omitempty"` // latest OPA released version
OPAUpToDate bool `json:"opa_up_to_date,omitempty"` // is running OPA version greater than or equal to the latest released
}
// Options supplies parameters to the reporter.
type Options struct {
Logger logging.Logger
}
type GHVersionCollector struct {
client rest.Client
}
type GHResponse struct {
TagName string `json:"tag_name,omitempty"` // latest OPA release tag
ReleaseNotes string `json:"html_url,omitempty"` // link to the OPA release notes
Download string `json:"assets_url,omitempty"` // link to download the OPA release
}
// New returns an instance of the Reporter
func New(opts Options) (Reporter, error) {
r := GHVersionCollector{}
url := cmp.Or(os.Getenv("OPA_TELEMETRY_SERVICE_URL"), ExternalServiceURL)
restConfig := fmt.Appendf(nil, `{
"url": %q,
}`, url)
client, err := rest.New(restConfig, map[string]*keys.Config{}, rest.Logger(opts.Logger))
if err != nil {
return nil, err
}
r.client = client
// heap_usage_bytes is always present, so register it unconditionally
r.RegisterGatherer("heap_usage_bytes", readRuntimeMemStats)
return &r, nil
}
// SendReport sends the telemetry report which includes information such as the OPA version, current memory usage to
// the external service
func (r *GHVersionCollector) SendReport(ctx context.Context) (*DataResponse, error) {
rCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
resp, err := r.client.Do(rCtx, "GET", fmt.Sprintf("/repos/%s/releases/latest", GHRepo))
if err != nil {
return nil, err
}
defer util.Close(resp)
switch resp.StatusCode {
case http.StatusOK:
if resp.Body != nil {
var result GHResponse
err := json.NewDecoder(resp.Body).Decode(&result)
if err != nil {
return nil, err
}
return createDataResponse(result)
}
return nil, nil
default:
return nil, fmt.Errorf("server replied with HTTP %v", resp.StatusCode)
}
}
func createDataResponse(ghResp GHResponse) (*DataResponse, error) {
if ghResp.TagName == "" {
return nil, errors.New("server response does not contain tag_name")
}
v := strings.TrimPrefix(version.Version, "v")
sv, err := semver.NewVersion(v)
if err != nil {
return nil, fmt.Errorf("failed to parse current version %q: %w", v, err)
}
latestV := strings.TrimPrefix(ghResp.TagName, "v")
latestSV, err := semver.NewVersion(latestV)
if err != nil {
return nil, fmt.Errorf("failed to parse latest version %q: %w", latestV, err)
}
isLatest := sv.Compare(*latestSV) >= 0
// Note: alternatively, we could look through the assets in the GH API response to find a matching asset,
// and use its URL. However, this is not guaranteed to be more robust, and wouldn't use the 'openpolicyagent.org' domain.
downloadLink := fmt.Sprintf("https://openpolicyagent.org/downloads/%v/opa_%v_%v",
ghResp.TagName, runtime.GOOS, runtime.GOARCH)
if runtime.GOARCH == "arm64" {
downloadLink = fmt.Sprintf("%v_static", downloadLink)
}
if strings.HasPrefix(runtime.GOOS, "win") {
downloadLink = fmt.Sprintf("%v.exe", downloadLink)
}
return &DataResponse{
Latest: ReleaseDetails{
Download: downloadLink,
ReleaseNotes: ghResp.ReleaseNotes,
LatestRelease: ghResp.TagName,
OPAUpToDate: isLatest,
},
}, nil
}
func (*GHVersionCollector) RegisterGatherer(_ string, _ Gatherer) {
// no-op for this implementation
}
// IsSet returns true if dr is populated.
func (dr *DataResponse) IsSet() bool {
return dr != nil && dr.Latest.LatestRelease != "" && dr.Latest.Download != "" && dr.Latest.ReleaseNotes != ""
}
// Slice returns the dr as a slice of key-value string pairs. If dr is nil, this function returns an empty slice.
func (dr *DataResponse) Slice() [][2]string {
if !dr.IsSet() {
return nil
}
return [][2]string{
{"Latest Upstream Version", strings.TrimPrefix(dr.Latest.LatestRelease, "v")},
{"Download", dr.Latest.Download},
{"Release Notes", dr.Latest.ReleaseNotes},
}
}
// Pretty returns OPA release information in a human-readable format.
func (dr *DataResponse) Pretty() string {
if !dr.IsSet() {
return ""
}
pairs := dr.Slice()
lines := make([]string, 0, len(pairs))
for _, pair := range pairs {
lines = append(lines, fmt.Sprintf("%v: %v", pair[0], pair[1]))
}
return strings.Join(lines, "\n")
}
func readRuntimeMemStats(_ context.Context) (any, error) {
var m runtime.MemStats
runtime.ReadMemStats(&m)
return strconv.FormatUint(m.Alloc, 10), nil
}
-261
View File
@@ -1,261 +0,0 @@
// Copyright 2020 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 init is an internal package with helpers for data and policy loading during initialization.
package init
import (
"context"
"fmt"
"io/fs"
"path/filepath"
"strings"
storedversion "github.com/open-policy-agent/opa/internal/version"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/bundle"
"github.com/open-policy-agent/opa/v1/loader"
"github.com/open-policy-agent/opa/v1/metrics"
"github.com/open-policy-agent/opa/v1/storage"
)
// InsertAndCompileOptions contains the input for the operation.
type InsertAndCompileOptions struct {
Store storage.Store
Txn storage.Transaction
Files loader.Result
Bundles map[string]*bundle.Bundle
MaxErrors int
EnablePrintStatements bool
ParserOptions ast.ParserOptions
BundleActivatorPlugin string
}
// InsertAndCompileResult contains the output of the operation.
type InsertAndCompileResult struct {
Compiler *ast.Compiler
Metrics metrics.Metrics
}
// InsertAndCompile writes data and policy into the store and returns a compiler for the
// store contents.
func InsertAndCompile(ctx context.Context, opts InsertAndCompileOptions) (*InsertAndCompileResult, error) {
if len(opts.Files.Documents) > 0 {
if err := opts.Store.Write(ctx, opts.Txn, storage.AddOp, storage.RootPath, opts.Files.Documents); err != nil {
return nil, fmt.Errorf("storage error: %w", err)
}
}
policies := make(map[string]*ast.Module, len(opts.Files.Modules))
for id, parsed := range opts.Files.Modules {
policies[id] = parsed.Parsed
}
compiler := ast.NewCompiler().
WithDefaultRegoVersion(opts.ParserOptions.RegoVersion).
SetErrorLimit(opts.MaxErrors).
WithPathConflictsCheck(storage.NonEmpty(ctx, opts.Store, opts.Txn)).
WithEnablePrintStatements(opts.EnablePrintStatements)
m := metrics.New()
activation := &bundle.ActivateOpts{
Ctx: ctx,
Store: opts.Store,
Txn: opts.Txn,
Compiler: compiler,
Metrics: m,
Bundles: opts.Bundles,
ExtraModules: policies,
ParserOptions: opts.ParserOptions,
Plugin: opts.BundleActivatorPlugin,
}
err := bundle.Activate(activation)
if err != nil {
return nil, err
}
// Policies in bundles will have already been added to the store, but
// modules loaded outside of bundles will need to be added manually.
for id, parsed := range opts.Files.Modules {
if err := opts.Store.UpsertPolicy(ctx, opts.Txn, id, parsed.Raw); err != nil {
return nil, fmt.Errorf("storage error: %w", err)
}
}
// Set the version in the store last to prevent data files from overwriting.
if err := storedversion.Write(ctx, opts.Store, opts.Txn); err != nil {
return nil, fmt.Errorf("storage error: %w", err)
}
return &InsertAndCompileResult{Compiler: compiler, Metrics: m}, nil
}
// LoadPathsResult contains the output loading a set of paths.
type LoadPathsResult struct {
Bundles map[string]*bundle.Bundle
Files loader.Result
}
// WalkPathsResult contains the output loading a set of paths.
type WalkPathsResult struct {
BundlesLoader []BundleLoader
FileDescriptors []*Descriptor
}
// BundleLoader contains information about files in a bundle
type BundleLoader struct {
DirectoryLoader bundle.DirectoryLoader
IsDir bool
}
// Descriptor contains information about a file
type Descriptor struct {
Root string
Path string
}
// LoadPaths reads data and policy from the given paths and returns a set of bundles or
// raw loader file results.
func LoadPaths(paths []string,
filter loader.Filter,
asBundle bool,
bvc *bundle.VerificationConfig,
skipVerify bool,
bundleLazyLoading bool,
processAnnotations bool,
caps *ast.Capabilities,
fsys fs.FS) (*LoadPathsResult, error) {
return LoadPathsForRegoVersion(ast.RegoV0, paths, filter, asBundle, bvc, skipVerify, bundleLazyLoading, processAnnotations, false, caps, fsys)
}
func LoadPathsForRegoVersion(regoVersion ast.RegoVersion,
paths []string,
filter loader.Filter,
asBundle bool,
bvc *bundle.VerificationConfig,
skipVerify bool,
bundleLazyLoading bool,
processAnnotations bool,
followSymlinks bool,
caps *ast.Capabilities,
fsys fs.FS) (*LoadPathsResult, error) {
if caps == nil {
caps = ast.CapabilitiesForThisVersion()
}
// tar.gz files are automatically loaded as bundles
var likelyBundles, nonBundlePaths []string
if !asBundle {
likelyBundles, nonBundlePaths = splitByTarGzExt(paths)
paths = likelyBundles
}
var result LoadPathsResult
var err error
if asBundle || len(likelyBundles) > 0 {
result.Bundles = make(map[string]*bundle.Bundle, len(paths))
for _, path := range paths {
result.Bundles[path], err = loader.NewFileLoader().
WithFS(fsys).
WithBundleVerificationConfig(bvc).
WithSkipBundleVerification(skipVerify).
WithBundleLazyLoadingMode(bundleLazyLoading).
WithFilter(filter).
WithProcessAnnotation(processAnnotations).
WithCapabilities(caps).
WithRegoVersion(regoVersion).
WithFollowSymlinks(followSymlinks).
AsBundle(path)
if err != nil {
return nil, err
}
}
}
if asBundle {
return &result, nil
}
files, err := loader.NewFileLoader().
WithFS(fsys).
WithBundleLazyLoadingMode(bundleLazyLoading).
WithProcessAnnotation(processAnnotations).
WithCapabilities(caps).
WithRegoVersion(regoVersion).
Filtered(nonBundlePaths, filter)
if err != nil {
return nil, err
}
result.Files = *files
return &result, nil
}
// splitByTarGzExt splits the paths in 2 groups. Ones with .tar.gz and another with
// non .tar.gz extensions.
func splitByTarGzExt(paths []string) (targzs []string, nonTargzs []string) {
for _, path := range paths {
if strings.HasSuffix(path, ".tar.gz") {
targzs = append(targzs, path)
} else {
nonTargzs = append(nonTargzs, path)
}
}
return
}
// WalkPaths reads data and policy from the given paths and returns a set of bundle directory loaders
// or descriptors that contain information about files.
func WalkPaths(paths []string, filter loader.Filter, asBundle bool) (*WalkPathsResult, error) {
var result WalkPathsResult
if asBundle {
result.BundlesLoader = make([]BundleLoader, len(paths))
for i, path := range paths {
bundleLoader, isDir, err := loader.GetBundleDirectoryLoader(path)
if err != nil {
return nil, err
}
result.BundlesLoader[i] = BundleLoader{
DirectoryLoader: bundleLoader,
IsDir: isDir,
}
}
return &result, nil
}
result.FileDescriptors = []*Descriptor{}
for _, path := range paths {
filePaths, err := loader.FilteredPaths([]string{path}, filter)
if err != nil {
return nil, err
}
for _, fp := range filePaths {
// Trim off the root directory and return path as if chrooted
cleanedPath := strings.TrimPrefix(fp, path)
if path == "." && filepath.Base(fp) == bundle.ManifestExt {
cleanedPath = fp
}
if !strings.HasPrefix(cleanedPath, "/") {
cleanedPath = "/" + cleanedPath
}
result.FileDescriptors = append(result.FileDescriptors, &Descriptor{
Root: path,
Path: cleanedPath,
})
}
}
return &result, nil
}
+194 -197
View File
@@ -14,237 +14,234 @@
// Semantic Versions http://semver.org
// Package semver has been vendored from:
// This file was originally vendored from:
// https://github.com/coreos/go-semver/tree/e214231b295a8ea9479f11b70b35d5acf3556d9b/semver
// A number of the original functions of the package have been removed since
// they are not required for our built-ins.
// There isn't a single line left from the original source today, but being generous about
// attribution won't hurt.
package semver
import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
// reMetaIdentifier matches pre-release and metadata identifiers against the spec requirements
var reMetaIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)
// Version represents a parsed SemVer
type Version struct {
Major int64
Minor int64
Patch int64
PreRelease PreRelease
Metadata string
PreRelease string `json:"PreRelease,omitempty"`
Metadata string `json:"Metadata,omitempty"`
}
// PreRelease represents a pre-release suffix string
type PreRelease string
// Parse constructs new semver Version from version string.
func Parse(version string) (v Version, err error) {
version = strings.TrimPrefix(version, "v")
func splitOff(input *string, delim string) (val string) {
parts := strings.SplitN(*input, delim, 2)
if len(parts) == 2 {
*input = parts[0]
val = parts[1]
version, v.Metadata = cut(version, '+')
if v.Metadata != "" && !reMetaIdentifier.MatchString(v.Metadata) {
return v, fmt.Errorf("invalid metadata identifier: %s", v.Metadata)
}
return val
version, v.PreRelease = cut(version, '-')
if v.PreRelease != "" && !reMetaIdentifier.MatchString(v.PreRelease) {
return v, fmt.Errorf("invalid pre-release identifier: %s", v.PreRelease)
}
if strings.Count(version, ".") != 2 {
return v, fmt.Errorf("%s should contain major, minor, and patch versions", version)
}
major, after := cut(version, '.')
if v.Major, err = strconv.ParseInt(major, 10, 64); err != nil {
return v, err
}
minor, after := cut(after, '.')
if v.Minor, err = strconv.ParseInt(minor, 10, 64); err != nil {
return v, err
}
if v.Patch, err = strconv.ParseInt(after, 10, 64); err != nil {
return v, err
}
return v, nil
}
// NewVersion constructs new SemVers from strings
func NewVersion(version string) (*Version, error) {
v := Version{}
if err := v.Set(version); err != nil {
return nil, err
// MustParse is like Parse but panics if the version string is invalid instead of returning an error.
func MustParse(version string) Version {
v, err := Parse(version)
if err != nil {
panic(err)
}
return &v, nil
return v
}
// Set parses and updates v from the given version string. Implements flag.Value
func (v *Version) Set(version string) error {
metadata := splitOff(&version, "+")
preRelease := PreRelease(splitOff(&version, "-"))
dotParts := strings.SplitN(version, ".", 3)
if len(dotParts) != 3 {
return fmt.Errorf("%s is not in dotted-tri format", version)
}
if err := validateIdentifier(string(preRelease)); err != nil {
return fmt.Errorf("failed to validate pre-release: %v", err)
}
if err := validateIdentifier(metadata); err != nil {
return fmt.Errorf("failed to validate metadata: %v", err)
}
parsed := make([]int64, 3)
for i, v := range dotParts[:3] {
val, err := strconv.ParseInt(v, 10, 64)
parsed[i] = val
if err != nil {
return err
}
}
v.Metadata = metadata
v.PreRelease = preRelease
v.Major = parsed[0]
v.Minor = parsed[1]
v.Patch = parsed[2]
return nil
}
func (v Version) String() string {
var buffer bytes.Buffer
fmt.Fprintf(&buffer, "%d.%d.%d", v.Major, v.Minor, v.Patch)
if v.PreRelease != "" {
fmt.Fprintf(&buffer, "-%s", v.PreRelease)
}
if v.Metadata != "" {
fmt.Fprintf(&buffer, "+%s", v.Metadata)
}
return buffer.String()
}
// Compare tests if v is less than, equal to, or greater than versionB,
// returning -1, 0, or +1 respectively.
func (v Version) Compare(versionB Version) int {
if cmp := recursiveCompare(v.Slice(), versionB.Slice()); cmp != 0 {
return cmp
}
return preReleaseCompare(v, versionB)
}
// Slice converts the comparable parts of the semver into a slice of integers.
func (v Version) Slice() []int64 {
return []int64{v.Major, v.Minor, v.Patch}
}
// Slice splits the pre-release suffix string
func (p PreRelease) Slice() []string {
preRelease := string(p)
return strings.Split(preRelease, ".")
}
func preReleaseCompare(versionA Version, versionB Version) int {
a := versionA.PreRelease
b := versionB.PreRelease
/* Handle the case where if two versions are otherwise equal it is the
* one without a PreRelease that is greater */
if len(a) == 0 && (len(b) > 0) {
return 1
} else if len(b) == 0 && (len(a) > 0) {
return -1
}
// If there is a prerelease, check and compare each part.
return recursivePreReleaseCompare(a.Slice(), b.Slice())
}
func recursiveCompare(versionA []int64, versionB []int64) int {
if len(versionA) == 0 {
return 0
}
a := versionA[0]
b := versionB[0]
if a > b {
return 1
} else if a < b {
return -1
}
return recursiveCompare(versionA[1:], versionB[1:])
}
func recursivePreReleaseCompare(versionA []string, versionB []string) int {
// A larger set of pre-release fields has a higher precedence than a smaller set,
// if all of the preceding identifiers are equal.
if len(versionA) == 0 {
if len(versionB) > 0 {
return -1
}
return 0
} else if len(versionB) == 0 {
// We're longer than versionB so return 1.
return 1
}
a := versionA[0]
b := versionB[0]
aInt := false
bInt := false
aI, err := strconv.Atoi(versionA[0])
if err == nil {
aInt = true
}
bI, err := strconv.Atoi(versionB[0])
if err == nil {
bInt = true
}
// Numeric identifiers always have lower precedence than non-numeric identifiers.
if aInt && !bInt {
return -1
} else if !aInt && bInt {
return 1
}
// Handle Integer Comparison
if aInt && bInt {
if aI > bI {
return 1
} else if aI < bI {
return -1
}
}
// Handle String Comparison
if a > b {
return 1
} else if a < b {
return -1
}
return recursivePreReleaseCompare(versionA[1:], versionB[1:])
}
// validateIdentifier makes sure the provided identifier satisfies semver spec
func validateIdentifier(id string) error {
if id != "" && !reIdentifier.MatchString(id) {
return fmt.Errorf("%s is not a valid semver identifier", id)
}
return nil
}
// reIdentifier is a regular expression used to check that pre-release and metadata
// identifiers satisfy the spec requirements
var reIdentifier = regexp.MustCompile(`^[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*$`)
// Compare compares two semver strings.
func Compare(a, b string) int {
aV, err := NewVersion(strings.TrimPrefix(a, "v"))
aV, err := Parse(a)
if err != nil {
return -1
}
bV, err := NewVersion(strings.TrimPrefix(b, "v"))
bV, err := Parse(b)
if err != nil {
return 1
}
return aV.Compare(*bV)
return aV.Compare(bV)
}
// AppendText appends the textual representation of the version to b and returns the extended buffer.
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
// without allocating, provided the caller has pre-allocated sufficient space in b.
func (v Version) AppendText(b []byte) ([]byte, error) {
if b == nil {
b = make([]byte, 0, length(v))
}
b = append(strconv.AppendInt(b, v.Major, 10), '.')
b = append(strconv.AppendInt(b, v.Minor, 10), '.')
b = strconv.AppendInt(b, v.Patch, 10)
if v.PreRelease != "" {
b = append(append(b, '-'), v.PreRelease...)
}
if v.Metadata != "" {
b = append(append(b, '+'), v.Metadata...)
}
return b, nil
}
// String returns the string representation of the version.
func (v Version) String() string {
bs := make([]byte, 0, length(v))
bs, _ = v.AppendText(bs)
return string(bs)
}
// Compare tests if v is less than, equal to, or greater than other, returning -1, 0, or +1 respectively.
// Comparison is based on the SemVer specification (https://semver.org/#spec-item-11).
func (v Version) Compare(other Version) int {
if v.Major > other.Major {
return 1
} else if v.Major < other.Major {
return -1
}
if v.Minor > other.Minor {
return 1
} else if v.Minor < other.Minor {
return -1
}
if v.Patch > other.Patch {
return 1
} else if v.Patch < other.Patch {
return -1
}
if v.PreRelease == other.PreRelease {
return 0
}
// if two versions are otherwise equal it is the one without a pre-release that is greater
if v.PreRelease == "" && other.PreRelease != "" {
return 1
}
if other.PreRelease == "" && v.PreRelease != "" {
return -1
}
a, afterA := cut(v.PreRelease, '.')
b, afterB := cut(other.PreRelease, '.')
for {
if a == "" && b != "" {
return -1
}
if a != "" && b == "" {
return 1
}
aIsInt := isAllDecimals(a)
bIsInt := isAllDecimals(b)
// numeric identifiers have lower precedence than non-numeric
if aIsInt && !bIsInt {
return -1
} else if !aIsInt && bIsInt {
return 1
}
if aIsInt && bIsInt {
aInt, _ := strconv.Atoi(a)
bInt, _ := strconv.Atoi(b)
if aInt > bInt {
return 1
} else if aInt < bInt {
return -1
}
} else {
// string comparison
if a > b {
return 1
} else if a < b {
return -1
}
}
// a larger set of pre-release fields has a higher precedence than a
// smaller set, if all of the preceding identifiers are equal.
if afterA != "" && afterB == "" {
return 1
} else if afterA == "" && afterB != "" {
return -1
}
a, afterA = cut(afterA, '.')
b, afterB = cut(afterB, '.')
}
}
func isAllDecimals(s string) bool {
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return s != ""
}
// length allows calculating the length of the version for pre-allocation.
func length(v Version) int {
n := util.NumDigitsInt64(v.Major) + util.NumDigitsInt64(v.Minor) + util.NumDigitsInt64(v.Patch) + 2
if v.PreRelease != "" {
n += len(v.PreRelease) + 1
}
if v.Metadata != "" {
n += len(v.Metadata) + 1
}
return n
}
// cut is a *slightly* faster version of strings.Cut only accepting
// single byte separators, and skipping the boolean return value.
func cut(s string, sep byte) (before, after string) {
if i := strings.IndexByte(s, sep); i >= 0 {
return s[:i], s[i+1:]
}
return s, ""
}
-33
View File
@@ -1,33 +0,0 @@
/*
Copyright The Helm 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 strvals provides tools for working with strval lines.
OPA runtime config supports a compressed format for YAML settings which we call strvals.
The format is roughly like this:
name=value,topname.subname=value
The above is equivalent to the YAML document
name: value
topname:
subname: value
This package provides a parser and utilities for converting the strvals format
to other formats.
*/
package strvals
-429
View File
@@ -1,429 +0,0 @@
/*
Copyright The Helm 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 strvals
import (
"bytes"
"errors"
"fmt"
"io"
"strconv"
"strings"
"sigs.k8s.io/yaml"
)
// ErrNotList indicates that a non-list was treated as a list.
var ErrNotList = errors.New("not a list")
// MaxIndex is the maximum index that will be allowed by setIndex.
// The default value 65536 = 1024 * 64
const MaxIndex = 65536
// ToYAML takes a string of arguments and converts to a YAML document.
func ToYAML(s string) (string, error) {
m, err := Parse(s)
if err != nil {
return "", err
}
d, err := yaml.Marshal(m)
return string(d), err
}
// Parse parses a set line.
//
// A set line is of the form name1=value1,name2=value2
func Parse(s string) (map[string]any, error) {
vals := map[string]any{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, false)
err := t.parse()
return vals, err
}
// ParseString parses a set line and forces a string value.
//
// A set line is of the form name1=value1,name2=value2
func ParseString(s string) (map[string]any, error) {
vals := map[string]any{}
scanner := bytes.NewBufferString(s)
t := newParser(scanner, vals, true)
err := t.parse()
return vals, err
}
// ParseInto parses a strvals line and merges the result into dest.
//
// If the strval string has a key that exists in dest, it overwrites the
// dest version.
func ParseInto(s string, dest map[string]any) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, false)
return t.parse()
}
// ParseIntoFile parses a filevals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoFile(s string, dest map[string]any, runesToVal runesToVal) error {
scanner := bytes.NewBufferString(s)
t := newFileParser(scanner, dest, runesToVal)
return t.parse()
}
// ParseIntoString parses a strvals line and merges the result into dest.
//
// This method always returns a string as the value.
func ParseIntoString(s string, dest map[string]any) error {
scanner := bytes.NewBufferString(s)
t := newParser(scanner, dest, true)
return t.parse()
}
// parser is a simple parser that takes a strvals line and parses it into a
// map representation.
//
// where sc is the source of the original data being parsed
// where data is the final parsed data from the parses with correct types
// where st is a boolean to figure out if we're forcing it to parse values as string
type parser struct {
sc *bytes.Buffer
data map[string]any
runesToVal runesToVal
}
type runesToVal func([]rune) (any, error)
func newParser(sc *bytes.Buffer, data map[string]any, stringBool bool) *parser {
rs2v := func(rs []rune) (any, error) {
return typedVal(rs, stringBool), nil
}
return &parser{sc: sc, data: data, runesToVal: rs2v}
}
func newFileParser(sc *bytes.Buffer, data map[string]any, runesToVal runesToVal) *parser {
return &parser{sc: sc, data: data, runesToVal: runesToVal}
}
func (t *parser) parse() error {
for {
err := t.key(t.data)
if err == nil {
continue
}
if err == io.EOF {
return nil
}
return err
}
}
func runeSet(r []rune) map[rune]bool {
s := make(map[rune]bool, len(r))
for _, rr := range r {
s[rr] = true
}
return s
}
func (t *parser) key(data map[string]any) error {
stop := runeSet([]rune{'=', '[', ',', '.'})
for {
switch k, last, err := runesUntil(t.sc, stop); {
case err != nil:
if len(k) == 0 {
return err
}
return fmt.Errorf("key %q has no value", string(k))
case last == '[':
// We are in a list index context, so we need to set an index.
i, err := t.keyIndex()
if err != nil {
return fmt.Errorf("error parsing index: %s", err)
}
kk := string(k)
// Find or create target list
list := []any{}
if _, ok := data[kk]; ok {
list = data[kk].([]any)
}
// Now we need to get the value after the ].
list, err = t.listItem(list, i)
set(data, kk, list)
return err
case last == '=':
// End of key. Consume =, Get value.
// FIXME: Get value list first
vl, e := t.valList()
switch e {
case nil:
set(data, string(k), vl)
return nil
case io.EOF:
set(data, string(k), "")
return e
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return e
}
v, e := t.runesToVal(rs)
set(data, string(k), v)
return e
default:
return e
}
case last == ',':
// No value given. Set the value to empty string. Return error.
set(data, string(k), "")
return fmt.Errorf("key %q has no value (cannot end with ,)", string(k))
case last == '.':
// First, create or find the target map.
inner := map[string]any{}
if _, ok := data[string(k)]; ok {
inner = data[string(k)].(map[string]any)
}
// Recurse
e := t.key(inner)
if len(inner) == 0 {
return fmt.Errorf("key map %q has no value", string(k))
}
set(data, string(k), inner)
return e
}
}
}
func set(data map[string]any, key string, val any) {
// If key is empty, don't set it.
if len(key) == 0 {
return
}
data[key] = val
}
func setIndex(list []any, index int, val any) (l2 []any, err error) {
// There are possible index values that are out of range on a target system
// causing a panic. This will catch the panic and return an error instead.
// The value of the index that causes a panic varies from system to system.
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("error processing index %d: %s", index, r)
}
}()
if index < 0 {
return list, fmt.Errorf("negative %d index not allowed", index)
}
if index > MaxIndex {
return list, fmt.Errorf("index of %d is greater than maximum supported index of %d", index, MaxIndex)
}
if len(list) <= index {
newlist := make([]any, index+1)
copy(newlist, list)
list = newlist
}
list[index] = val
return list, nil
}
func (t *parser) keyIndex() (int, error) {
// First, get the key.
stop := runeSet([]rune{']'})
v, _, err := runesUntil(t.sc, stop)
if err != nil {
return 0, err
}
// v should be the index
return strconv.Atoi(string(v))
}
func (t *parser) listItem(list []any, i int) ([]any, error) {
if i < 0 {
return list, fmt.Errorf("negative %d index not allowed", i)
}
stop := runeSet([]rune{'[', '.', '='})
switch k, last, err := runesUntil(t.sc, stop); {
case len(k) > 0:
return list, fmt.Errorf("unexpected data at end of array index: %q", k)
case err != nil:
return list, err
case last == '=':
vl, e := t.valList()
switch e {
case nil:
return setIndex(list, i, vl)
case io.EOF:
return setIndex(list, i, "")
case ErrNotList:
rs, e := t.val()
if e != nil && e != io.EOF {
return list, e
}
v, e := t.runesToVal(rs)
if e != nil {
return nil, e
}
return setIndex(list, i, v)
default:
return list, e
}
case last == '[':
// now we have a nested list. Read the index and handle.
i, err := t.keyIndex()
if err != nil {
return list, fmt.Errorf("error parsing index: %s", err)
}
// Now we need to get the value after the ].
list2, err := t.listItem(list, i)
if err != nil {
return nil, err
}
return setIndex(list, i, list2)
case last == '.':
// We have a nested object. Send to t.key
inner := map[string]any{}
if len(list) > i {
var ok bool
inner, ok = list[i].(map[string]any)
if !ok {
// We have indices out of order. Initialize empty value.
list[i] = map[string]any{}
inner = list[i].(map[string]any)
}
}
// Recurse
e := t.key(inner)
if e != nil {
return list, e
}
return setIndex(list, i, inner)
default:
return nil, fmt.Errorf("parse error: unexpected token %v", last)
}
}
func (t *parser) val() ([]rune, error) {
stop := runeSet([]rune{','})
v, _, err := runesUntil(t.sc, stop)
return v, err
}
func (t *parser) valList() ([]any, error) {
r, _, e := t.sc.ReadRune()
if e != nil {
return []any{}, e
}
if r != '{' {
e = t.sc.UnreadRune()
if e != nil {
return []any{}, e
}
return []any{}, ErrNotList
}
list := []any{}
stop := runeSet([]rune{',', '}'})
for {
switch rs, last, err := runesUntil(t.sc, stop); {
case err != nil:
if err == io.EOF {
err = errors.New("list must terminate with '}'")
}
return list, err
case last == '}':
// If this is followed by ',', consume it.
if r, _, e := t.sc.ReadRune(); e == nil && r != ',' {
e = t.sc.UnreadRune()
if e != nil {
return []any{}, e
}
}
v, e := t.runesToVal(rs)
list = append(list, v)
return list, e
case last == ',':
v, e := t.runesToVal(rs)
if e != nil {
return list, e
}
list = append(list, v)
}
}
}
func runesUntil(in io.RuneReader, stop map[rune]bool) ([]rune, rune, error) {
var v []rune
for {
switch r, _, e := in.ReadRune(); {
case e != nil:
return v, r, e
case inMap(r, stop):
return v, r, nil
case r == '\\':
next, _, e := in.ReadRune()
if e != nil {
return v, next, e
}
v = append(v, next)
default:
v = append(v, r)
}
}
}
func inMap(k rune, m map[rune]bool) bool {
_, ok := m[k]
return ok
}
func typedVal(v []rune, st bool) any {
val := string(v)
if st {
return val
}
if strings.EqualFold(val, "true") {
return true
}
if strings.EqualFold(val, "false") {
return false
}
if strings.EqualFold(val, "null") {
return struct{}{}
}
if strings.EqualFold(val, "0") {
return int64(0)
}
// If this value does not start with zero, try parsing it to an int
if len(val) != 0 && val[0] != '0' {
if iv, err := strconv.ParseInt(val, 10, 64); err == nil {
return iv
}
}
return val
}
+1 -1
View File
@@ -83,7 +83,7 @@ func readModule(r io.Reader) (*module.Module, error) {
var m module.Module
if err := readSections(r, &m); err != nil && err != io.EOF {
if err := readSections(r, &m); err != io.EOF {
return nil, err
}
+2 -16
View File
@@ -433,18 +433,7 @@ func (a *Annotations) toObject() (*Object, *Error) {
}
if len(a.Scope) > 0 {
switch a.Scope {
case annotationScopeDocument:
obj.Insert(InternedTerm("scope"), InternedTerm("document"))
case annotationScopePackage:
obj.Insert(InternedTerm("scope"), InternedTerm("package"))
case annotationScopeRule:
obj.Insert(InternedTerm("scope"), InternedTerm("rule"))
case annotationScopeSubpackages:
obj.Insert(InternedTerm("scope"), InternedTerm("subpackages"))
default:
obj.Insert(InternedTerm("scope"), StringTerm(a.Scope))
}
obj.Insert(InternedTerm("scope"), InternedTerm(a.Scope))
}
if len(a.Title) > 0 {
@@ -752,10 +741,7 @@ func (c *CompileAnnotation) Compare(other *CompileAnnotation) int {
return -1
}
if cmp := slices.CompareFunc(c.Unknowns, other.Unknowns,
func(x, y Ref) int {
return x.Compare(y)
}); cmp != 0 {
if cmp := slices.CompareFunc(c.Unknowns, other.Unknowns, RefCompare); cmp != 0 {
return cmp
}
return c.MaskRule.Compare(other.MaskRule)
+68 -42
View File
@@ -26,11 +26,16 @@ func RegisterBuiltin(b *Builtin) {
BuiltinMap[b.Infix] = b
InternStringTerm(b.Infix)
InternVarValue(b.Infix)
}
InternStringTerm(b.Name)
if strings.Contains(b.Name, ".") {
InternStringTerm(strings.Split(b.Name, ".")...)
parts := strings.Split(b.Name, ".")
InternStringTerm(parts...)
InternVarValue(parts[0])
} else {
InternStringTerm(b.Name)
InternVarValue(b.Name)
}
}
@@ -90,6 +95,7 @@ var DefaultBuiltins = [...]*Builtin{
// Arrays
ArrayConcat,
ArrayFlatten,
ArraySlice,
ArrayReverse,
@@ -146,6 +152,7 @@ var DefaultBuiltins = [...]*Builtin{
Sprintf,
StringReverse,
RenderTemplate,
InternalTemplateString,
// Numbers
NumbersRange,
@@ -887,6 +894,18 @@ var ArrayConcat = &Builtin{
CanSkipBctx: true,
}
var ArrayFlatten = &Builtin{
Name: "array.flatten",
Description: "Non-recursively unpacks array items in arr into the flattened array. Other types are appended as-is.",
Decl: types.NewFunction(
types.Args(
types.Named("arr", types.NewArray(nil, types.A)).Description("the array to be flattened"),
),
types.Named("flattened", types.NewArray(nil, types.A)).Description("array flattened one level"),
),
CanSkipBctx: true,
}
var ArraySlice = &Builtin{
Name: "array.slice",
Description: "Returns a slice of a given array. If `start` is greater or equal than `stop`, `slice` is `[]`.",
@@ -1104,7 +1123,7 @@ var Concat = &Builtin{
types.Named("output", types.S).Description("the joined string"),
),
Categories: stringsCat,
CanSkipBctx: true,
CanSkipBctx: false,
}
var FormatInt = &Builtin{
@@ -1272,7 +1291,7 @@ var Replace = &Builtin{
types.Named("y", types.S).Description("string with replaced substrings"),
),
Categories: stringsCat,
CanSkipBctx: true,
CanSkipBctx: false,
}
var ReplaceN = &Builtin{
@@ -1292,7 +1311,7 @@ The old string comparisons are done in argument order.`,
),
types.Named("output", types.S).Description("string with replaced substrings"),
),
CanSkipBctx: true,
CanSkipBctx: false,
}
var RegexReplace = &Builtin{
@@ -1656,7 +1675,7 @@ var JSONPatch = &Builtin{
"Additionally works on sets, where a value contained in the set is considered to be its path.",
Decl: types.NewFunction(
types.Args(
types.Named("object", types.A).Description("the object to patch"), // TODO(sr): types.A?
types.Named("target", types.A).Description("the object, array or set to patch"),
types.Named("patches", types.NewArray(
nil,
types.NewObject(
@@ -1817,7 +1836,8 @@ var ObjectKeys = &Builtin{
/*
* Encoding
*/
var encoding = category("encoding")
// Not using 'encoding' to avoid having to alias stdlib "encoding" imports
var catEncoding = category("encoding")
var JSONMarshal = &Builtin{
Name: "json.marshal",
@@ -1828,7 +1848,7 @@ var JSONMarshal = &Builtin{
),
types.Named("y", types.S).Description("the JSON string representation of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1850,7 +1870,7 @@ var JSONMarshalWithOptions = &Builtin{
),
types.Named("y", types.S).Description("the JSON string representation of `x`, with configured prefix/indent string(s) as appropriate"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1863,7 +1883,7 @@ var JSONUnmarshal = &Builtin{
),
types.Named("y", types.A).Description("the term deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1876,7 +1896,7 @@ var JSONIsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid JSON, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1889,7 +1909,7 @@ var Base64Encode = &Builtin{
),
types.Named("y", types.S).Description("base64 serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1902,7 +1922,7 @@ var Base64Decode = &Builtin{
),
types.Named("y", types.S).Description("base64 deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1915,7 +1935,7 @@ var Base64IsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid base64 encoded value, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1928,7 +1948,7 @@ var Base64UrlEncode = &Builtin{
),
types.Named("y", types.S).Description("base64url serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1941,7 +1961,7 @@ var Base64UrlEncodeNoPad = &Builtin{
),
types.Named("y", types.S).Description("base64url serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1954,7 +1974,7 @@ var Base64UrlDecode = &Builtin{
),
types.Named("y", types.S).Description("base64url deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1967,7 +1987,7 @@ var URLQueryDecode = &Builtin{
),
types.Named("y", types.S).Description("URL-encoding deserialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -1980,7 +2000,7 @@ var URLQueryEncode = &Builtin{
),
types.Named("y", types.S).Description("URL-encoding serialization of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2004,7 +2024,7 @@ var URLQueryEncodeObject = &Builtin{
),
types.Named("y", types.S).Description("the URL-encoded serialization of `object`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2019,7 +2039,7 @@ var URLQueryDecodeObject = &Builtin{
types.S,
types.NewArray(nil, types.S)))).Description("the resulting object"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2032,7 +2052,7 @@ var YAMLMarshal = &Builtin{
),
types.Named("y", types.S).Description("the YAML string representation of `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2045,7 +2065,7 @@ var YAMLUnmarshal = &Builtin{
),
types.Named("y", types.A).Description("the term deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2059,7 +2079,7 @@ var YAMLIsValid = &Builtin{
),
types.Named("result", types.B).Description("`true` if `x` is valid YAML, `false` otherwise"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2072,7 +2092,7 @@ var HexEncode = &Builtin{
),
types.Named("y", types.S).Description("serialization of `x` using hex-encoding"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -2085,7 +2105,7 @@ var HexDecode = &Builtin{
),
types.Named("y", types.S).Description("deserialized from `x`"),
),
Categories: encoding,
Categories: catEncoding,
CanSkipBctx: true,
}
@@ -3383,6 +3403,12 @@ var InternalTestCase = &Builtin{
Decl: types.NewFunction([]types.Type{types.NewArray(nil, types.A)}, nil),
}
var InternalTemplateString = &Builtin{
Name: "internal.template_string",
Decl: types.NewFunction([]types.Type{types.NewArray(nil, types.A)}, types.S),
CanSkipBctx: true, // Uses bctx.Location for error reporting, but that is always provided in eval
}
/**
* Deprecated built-ins.
*/
@@ -3397,7 +3423,7 @@ var SetDiff = &Builtin{
),
types.SetOfAny,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3411,7 +3437,7 @@ var NetCIDROverlap = &Builtin{
),
types.B,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3423,7 +3449,7 @@ var CastArray = &Builtin{
types.Args(types.A),
types.NewArray(nil, types.A),
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3437,7 +3463,7 @@ var CastSet = &Builtin{
types.Args(types.A),
types.SetOfAny,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3449,7 +3475,7 @@ var CastString = &Builtin{
types.Args(types.A),
types.S,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3460,7 +3486,7 @@ var CastBoolean = &Builtin{
types.Args(types.A),
types.B,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3471,7 +3497,7 @@ var CastNull = &Builtin{
types.Args(types.A),
types.Nl,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3482,11 +3508,11 @@ var CastObject = &Builtin{
types.Args(types.A),
types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
// RegexMatchDeprecated declares `re_match` which has been deprecated. Use `regex.match` instead.
// RegexMatchDeprecated declares `re_match` which has been Deprecated. Use `regex.match` instead.
var RegexMatchDeprecated = &Builtin{
Name: "re_match",
Decl: types.NewFunction(
@@ -3496,7 +3522,7 @@ var RegexMatchDeprecated = &Builtin{
),
types.B,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: false,
}
@@ -3513,7 +3539,7 @@ var All = &Builtin{
),
types.B,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3530,7 +3556,7 @@ var Any = &Builtin{
),
types.B,
),
deprecated: true,
Deprecated: true,
CanSkipBctx: true,
}
@@ -3548,7 +3574,7 @@ type Builtin struct {
Decl *types.Function `json:"decl"` // Built-in function type declaration.
Infix string `json:"infix,omitempty"` // Unique name of infix operator. Default should be unset.
Relation bool `json:"relation,omitempty"` // Indicates if the built-in acts as a relation.
deprecated bool `json:"-"` // Indicates if the built-in has been deprecated.
Deprecated bool `json:"deprecated,omitempty"` // Indicates if the built-in has been deprecated.
CanSkipBctx bool `json:"-"` // Built-in needs no data from the built-in context.
Nondeterministic bool `json:"nondeterministic,omitempty"` // Indicates if the built-in returns non-deterministic results.
}
@@ -3573,12 +3599,12 @@ func (b *Builtin) Minimal() *Builtin {
return &cpy
}
// IsDeprecated returns true if the Builtin function is deprecated and will be removed in a future release.
// IsDeprecated returns true if the Builtin function is Deprecated and will be removed in a future release.
func (b *Builtin) IsDeprecated() bool {
return b.deprecated
return b.Deprecated
}
// IsDeterministic returns true if the Builtin function returns non-deterministic results.
// IsNondeterministic returns true if the Builtin function returns non-deterministic results.
func (b *Builtin) IsNondeterministic() bool {
return b.Nondeterministic
}
+9 -6
View File
@@ -58,12 +58,14 @@ const FeatureRefHeads = "rule_head_refs"
const FeatureRegoV1 = "rego_v1"
const FeatureRegoV1Import = "rego_v1_import"
const FeatureKeywordsInRefs = "keywords_in_refs"
const FeatureTemplateStrings = "template_strings"
// Features carries the default features supported by this version of OPA.
// Use RegisterFeatures to add to them.
var Features = []string{
FeatureRegoV1,
FeatureKeywordsInRefs,
FeatureTemplateStrings,
}
// RegisterFeatures lets applications wrapping OPA register features, to be
@@ -228,13 +230,8 @@ func LoadCapabilitiesVersions() ([]string, error) {
// MinimumCompatibleVersion returns the minimum compatible OPA version based on
// the built-ins, features, and keywords in c.
func (c *Capabilities) MinimumCompatibleVersion() (string, bool) {
var maxVersion semver.Version
// this is the oldest OPA release that includes capabilities
if err := maxVersion.Set("0.17.0"); err != nil {
panic("unreachable")
}
maxVersion := semver.MustParse("0.17.0")
minVersionIndex := minVersionIndexOnce()
for _, bi := range c.Builtins {
@@ -274,6 +271,12 @@ func (c *Capabilities) ContainsFeature(feature string) bool {
return slices.Contains(c.Features, feature)
}
func (c *Capabilities) ContainsBuiltin(name string) bool {
return slices.ContainsFunc(c.Builtins, func(builtin *Builtin) bool {
return builtin.Name == name
})
}
// 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) {
+85 -89
View File
@@ -6,8 +6,8 @@ package ast
import (
"fmt"
"regexp"
"slices"
"sort"
"strings"
"github.com/open-policy-agent/opa/v1/types"
@@ -16,11 +16,6 @@ import (
type varRewriter func(Ref) Ref
// exprChecker defines the interface for executing type checking on a single
// expression. The exprChecker must update the provided TypeEnv with inferred
// types of vars.
type exprChecker func(*TypeEnv, *Expr) *Error
// typeChecker implements type checking on queries and rules. Errors are
// accumulated on the typeChecker so that a single run can report multiple
// issues.
@@ -28,7 +23,6 @@ type typeChecker struct {
builtins map[string]*Builtin
required *Capabilities
errs Errors
exprCheckers map[string]exprChecker
varRewriter varRewriter
ss *SchemaSet
allowNet []string
@@ -39,11 +33,7 @@ type typeChecker struct {
// newTypeChecker returns a new typeChecker object that has no errors.
func newTypeChecker() *typeChecker {
return &typeChecker{
exprCheckers: map[string]exprChecker{
"eq": checkExprEq,
},
}
return &typeChecker{}
}
func (tc *typeChecker) newEnv(exist *TypeEnv) *TypeEnv {
@@ -126,43 +116,39 @@ func (tc *typeChecker) Env(builtins map[string]*Builtin) *TypeEnv {
// are found. The resulting TypeEnv wraps the provided one. The resulting
// TypeEnv will be able to resolve types of vars contained in the body.
func (tc *typeChecker) CheckBody(env *TypeEnv, body Body) (*TypeEnv, Errors) {
var errors []*Error
errors := []*Error{}
env = tc.newEnv(env)
vis := newRefChecker(env, tc.varRewriter)
gv := NewGenericVisitor(vis.Visit)
WalkExprs(body, func(expr *Expr) bool {
for _, bexpr := range body {
WalkExprs(bexpr, func(expr *Expr) bool {
closureErrs := tc.checkClosures(env, expr)
errors = append(errors, closureErrs...)
closureErrs := tc.checkClosures(env, expr)
for _, err := range closureErrs {
errors = append(errors, err)
}
// reset errors from previous iteration
vis.errs = nil
gv.Walk(expr)
errors = append(errors, vis.errs...)
hasClosureErrors := len(closureErrs) > 0
// reset errors from previous iteration
vis.errs = nil
NewGenericVisitor(vis.Visit).Walk(expr)
for _, err := range vis.errs {
errors = append(errors, err)
}
hasRefErrors := len(vis.errs) > 0
if err := tc.checkExpr(env, expr); err != nil {
// Suppress this error if a more actionable one has occurred. In
// this case, if an error occurred in a ref or closure contained in
// this expression, and the error is due to a nil type, then it's
// likely to be the result of the more specific error.
skip := (hasClosureErrors || hasRefErrors) && causedByNilType(err)
if !skip {
errors = append(errors, err)
if err := tc.checkExpr(env, expr); err != nil {
hasClosureErrors := len(closureErrs) > 0
hasRefErrors := len(vis.errs) > 0
// Suppress this error if a more actionable one has occurred. In
// this case, if an error occurred in a ref or closure contained in
// this expression, and the error is due to a nil type, then it's
// likely to be the result of the more specific error.
skip := (hasClosureErrors || hasRefErrors) && causedByNilType(err)
if !skip {
errors = append(errors, err)
}
}
}
return true
})
return true
})
}
tc.err(errors)
tc.err(errors...)
return env, errors
}
@@ -243,7 +229,7 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
for _, schemaAnnot := range schemaAnnots {
refType, err := tc.getSchemaType(schemaAnnot, rule)
if err != nil {
tc.err([]*Error{err})
tc.err(err)
continue
}
@@ -259,7 +245,7 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
} else {
newType, err := override(ref[len(prefixRef):], t, refType, rule)
if err != nil {
tc.err([]*Error{err})
tc.err(err)
continue
}
env.tree.Put(prefixRef, newType)
@@ -281,23 +267,25 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
var tpe types.Type
if len(rule.Head.Args) > 0 {
// If args are not referred to in body, infer as any.
WalkVars(rule.Head.Args, func(v Var) bool {
if cpy.GetByValue(v) == nil {
cpy.tree.PutOne(v, types.A)
}
return false
})
for _, arg := range rule.Head.Args {
// If args are not referred to in body, infer as any.
WalkTerms(arg, func(t *Term) bool {
if _, ok := t.Value.(Var); ok {
if cpy.GetByValue(t.Value) == nil {
cpy.tree.PutOne(t.Value, types.A)
}
}
return false
})
}
// Construct function type.
args := make([]types.Type, len(rule.Head.Args))
for i := range len(rule.Head.Args) {
for i := range rule.Head.Args {
args[i] = cpy.GetByValue(rule.Head.Args[i].Value)
}
f := types.NewFunction(args, cpy.Get(rule.Head.Value))
tpe = f
tpe = types.NewFunction(args, cpy.GetByValue(rule.Head.Value.Value))
} else {
switch rule.Head.RuleKind() {
case SingleValue:
@@ -310,7 +298,7 @@ func (tc *typeChecker) checkRule(env *TypeEnv, as *AnnotationSet, rule *Rule) {
var err error
tpe, err = nestedObject(cpy, objPath, typeV)
if err != nil {
tc.err([]*Error{NewError(TypeErr, rule.Head.Location, "%s", err.Error())})
tc.err(NewError(TypeErr, rule.Head.Location, "%s", err.Error()))
tpe = nil
}
} else if typeV != nil {
@@ -374,19 +362,14 @@ func (tc *typeChecker) checkExpr(env *TypeEnv, expr *Expr) *Error {
}
}
checker := tc.exprCheckers[operator]
if checker != nil {
return checker(env, expr)
if operator == "eq" {
return checkExprEq(env, expr)
}
return tc.checkExprBuiltin(env, expr)
}
func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error {
args := expr.Operands()
pre := getArgTypes(env, args)
// NOTE(tsandall): undefined functions will have been caught earlier in the
// compiler. We check for undefined functions before the safety check so
// that references to non-existent functions result in undefined function
@@ -405,10 +388,12 @@ func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error {
return NewError(TypeErr, expr.Location, "undefined function %v", name)
}
// check if the expression refers to a function that contains an error
_, ok := tpe.(types.Any)
if ok {
return nil
if t, ok := tpe.(types.Any); ok {
// A type.Any with a len(0) is created by using types.A , this represents a potential non-local reference
// This is the exception when checking if the type represents a function
if len(t) == 0 {
return nil
}
}
ftpe, ok := tpe.(*types.Function)
@@ -424,12 +409,14 @@ func (tc *typeChecker) checkExprBuiltin(env *TypeEnv, expr *Expr) *Error {
namedFargs.Args = append(namedFargs.Args, ftpe.NamedResult())
}
args := expr.Operands()
if len(args) > len(fargs.Args) && fargs.Variadic == nil {
return newArgError(expr.Location, name, "too many arguments", pre, namedFargs)
return newArgError(expr.Location, name, "too many arguments", getArgTypes(env, args), namedFargs)
}
if len(args) < len(ftpe.FuncArgs().Args) {
return newArgError(expr.Location, name, "too few arguments", pre, namedFargs)
return newArgError(expr.Location, name, "too few arguments", getArgTypes(env, args), namedFargs)
}
for i := range args {
@@ -601,7 +588,7 @@ func unify1(env *TypeEnv, term *Term, tpe types.Type, union bool) bool {
return unifies
}
return false
case Set:
case *set:
switch tpe := tpe.(type) {
case *types.Set:
return unify1Set(env, v, tpe, union)
@@ -676,14 +663,14 @@ func unify1Object(env *TypeEnv, val Object, tpe *types.Object, union bool) bool
return !stop
}
func unify1Set(env *TypeEnv, val Set, tpe *types.Set, union bool) bool {
func unify1Set(env *TypeEnv, val *set, tpe *types.Set, union bool) bool {
of := types.Values(tpe)
return !val.Until(func(elem *Term) bool {
return !unify1(env, elem, of, union)
})
}
func (tc *typeChecker) err(errors []*Error) {
func (tc *typeChecker) err(errors ...*Error) {
tc.errs = append(tc.errs, errors...)
}
@@ -704,7 +691,6 @@ func newRefChecker(env *TypeEnv, f varRewriter) *refChecker {
return &refChecker{
env: env,
errs: nil,
varRewriter: f,
}
}
@@ -716,8 +702,9 @@ func (rc *refChecker) Visit(x any) bool {
case *Expr:
switch terms := x.Terms.(type) {
case []*Term:
vis := NewGenericVisitor(rc.Visit)
for i := 1; i < len(terms); i++ {
NewGenericVisitor(rc.Visit).Walk(terms[i])
vis.Walk(terms[i])
}
return true
case *Term:
@@ -807,7 +794,6 @@ func (rc *refChecker) checkRef(curr *TypeEnv, node *typeTreeNode, ref Ref, idx i
}
func (rc *refChecker) checkRefLeaf(tpe types.Type, ref Ref, idx int) *Error {
if idx == len(ref) {
return nil
}
@@ -822,16 +808,16 @@ func (rc *refChecker) checkRefLeaf(tpe types.Type, ref Ref, idx int) *Error {
switch value := head.Value.(type) {
case Var:
if exist := rc.env.GetByValue(value); exist != nil {
if exist := rc.env.GetByValue(head.Value); exist != nil {
if !unifies(exist, keys) {
return newRefErrInvalid(ref[0].Location, rc.varRewriter(ref), idx, exist, keys, getOneOfForType(tpe))
}
} else {
rc.env.tree.PutOne(value, types.Keys(tpe))
rc.env.tree.PutOne(head.Value, types.Keys(tpe))
}
case Ref:
if exist := rc.env.Get(value); exist != nil {
if exist := rc.env.GetByRef(value); exist != nil {
if !unifies(exist, keys) {
return newRefErrInvalid(ref[0].Location, rc.varRewriter(ref), idx, exist, keys, getOneOfForType(tpe))
}
@@ -1104,7 +1090,21 @@ func newRefErrInvalid(loc *Location, ref Ref, idx int, have, want types.Type, on
}
func newRefErrUnsupported(loc *Location, ref Ref, idx int, have types.Type) *Error {
err := newRefError(loc, ref)
var err *Error
switch have.(type) {
case *types.Function:
var function string
// drop any trailing references to unidentified parameters (e.g. __local1__)
if match, err := regexp.MatchString(`__local[0-9]+__`, ref[len(ref)-1].Value.String()); err == nil && match {
function = ref[:len(ref)-1].String()
} else {
function = ref.String()
}
err = NewError(TypeErr, loc, "function %s used as reference, not called", function)
default:
err = newRefError(loc, ref)
}
err.Details = &RefErrUnsupportedDetail{
Ref: ref,
Pos: idx,
@@ -1132,7 +1132,7 @@ func getOneOfForNode(node *typeTreeNode) (result []Value) {
return false
})
sortValueSlice(result)
slices.SortFunc(result, Value.Compare)
return result
}
@@ -1155,16 +1155,10 @@ func getOneOfForType(tpe types.Type) (result []Value) {
}
result = removeDuplicate(result)
sortValueSlice(result)
slices.SortFunc(result, Value.Compare)
return result
}
func sortValueSlice(sl []Value) {
sort.Slice(sl, func(i, j int) bool {
return sl[i].Compare(sl[j]) < 0
})
}
func removeDuplicate(list []Value) []Value {
seen := make(map[Value]bool)
var newResult []Value
@@ -1188,13 +1182,13 @@ func getArgTypes(env *TypeEnv, args []*Term) []types.Type {
// getPrefix returns the shortest prefix of ref that exists in env
func getPrefix(env *TypeEnv, ref Ref) (Ref, types.Type) {
if len(ref) == 1 {
t := env.Get(ref)
t := env.GetByRef(ref)
if t != nil {
return ref, t
}
}
for i := 1; i < len(ref); i++ {
t := env.Get(ref[:i])
t := env.GetByRef(ref[:i])
if t != nil {
return ref[:i], t
}
@@ -1202,12 +1196,14 @@ func getPrefix(env *TypeEnv, ref Ref) (Ref, types.Type) {
return nil, nil
}
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
obj, ok := t.(*types.Object)
if !ok {
newType, err := getObjectType(ref, o, rule, types.NewDynamicProperty(types.A, types.A))
newType, err := getObjectType(ref, o, rule, dynamicAnyAny)
if err != nil {
return nil, err
}
+17 -18
View File
@@ -96,6 +96,9 @@ func Compare(a, b any) int {
return -1
}
return 1
case *TemplateString:
b := b.(*TemplateString)
return a.Compare(b)
case Var:
return VarCompare(a, b.(Var))
case Ref:
@@ -179,26 +182,28 @@ func sortOrder(x any) int {
return 2
case String:
return 3
case Var:
case *TemplateString:
return 4
case Ref:
case Var:
return 5
case *Array:
case Ref:
return 6
case Object:
case *Array:
return 7
case Set:
case Object:
return 8
case *ArrayComprehension:
case Set:
return 9
case *ObjectComprehension:
case *ArrayComprehension:
return 10
case *SetComprehension:
case *ObjectComprehension:
return 11
case Call:
case *SetComprehension:
return 12
case Args:
case Call:
return 13
case Args:
return 14
case *Expr:
return 100
case *SomeDecl:
@@ -322,14 +327,6 @@ func TermValueEqual(a, b *Term) bool {
}
func ValueEqual(a, b Value) bool {
// TODO(ae): why doesn't this work the same?
//
// case interface{ Equal(Value) bool }:
// return v.Equal(b)
//
// When put on top, golangci-lint even flags the other cases as unreachable..
// but TestTopdownVirtualCache will have failing test cases when we replace
// the other cases with the above one.. 🤔
switch v := a.(type) {
case Null:
return v.Equal(b)
@@ -345,6 +342,8 @@ func ValueEqual(a, b Value) bool {
return v.Equal(b)
case *Array:
return v.Equal(b)
case *TemplateString:
return v.Equal(b)
}
return a.Compare(b) == 0
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -33,7 +33,8 @@ func CompileModulesWithOpt(modules map[string]string, opts CompileOpts) (*Compil
compiler := NewCompiler().
WithDefaultRegoVersion(opts.ParserOptions.RegoVersion).
WithEnablePrintStatements(opts.EnablePrintStatements)
WithEnablePrintStatements(opts.EnablePrintStatements).
WithCapabilities(opts.ParserOptions.Capabilities)
compiler.Compile(parsed)
if compiler.Failed() {
+2 -2
View File
@@ -63,9 +63,9 @@ func checkDocumentConflicts(node *TreeNode, exists func([]string) (bool, error),
if len(node.Values) > 0 {
s := strings.Join(path, "/")
if ok, err := exists(path); err != nil {
return Errors{NewError(CompileErr, node.Values[0].(*Rule).Loc(), "conflict check for data path %v: %v", s, err.Error())}
return Errors{NewError(CompileErr, node.Values[0].Loc(), "conflict check for data path %v: %v", s, err.Error())}
} else if ok {
return Errors{NewError(CompileErr, node.Values[0].(*Rule).Loc(), "conflicting rule for data path %v found", s)}
return Errors{NewError(CompileErr, node.Values[0].Loc(), "conflicting rule for data path %v found", s)}
}
}
+28 -42
View File
@@ -29,6 +29,7 @@ func newTypeEnv(f func() *typeChecker) *TypeEnv {
}
// Get returns the type of x.
//
// Deprecated: Use GetByValue or GetByRef instead, as they are more efficient.
func (env *TypeEnv) Get(x any) types.Type {
if term, ok := x.(*Term); ok {
@@ -53,15 +54,14 @@ func (env *TypeEnv) GetByValue(v Value) types.Type {
return types.B
case Number:
return types.N
case String:
case String, *TemplateString:
return types.S
// Composites.
case *Array:
static := make([]types.Type, x.Len())
for i := range static {
tpe := env.GetByValue(x.Elem(i).Value)
static[i] = tpe
static[i] = env.GetByValue(x.Elem(i).Value)
}
var dynamic types.Type
@@ -79,17 +79,13 @@ func (env *TypeEnv) GetByValue(v Value) types.Type {
x.Foreach(func(k, v *Term) {
if IsConstant(k.Value) {
kjson, err := JSON(k.Value)
if err == nil {
tpe := env.GetByValue(v.Value)
static = append(static, types.NewStaticProperty(kjson, tpe))
if kjson, err := JSON(k.Value); err == nil {
static = append(static, types.NewStaticProperty(kjson, env.GetByValue(v.Value)))
return
}
}
// Can't handle it as a static property, fallback to dynamic
typeK := env.GetByValue(k.Value)
typeV := env.GetByValue(v.Value)
dynamic = types.NewDynamicProperty(typeK, typeV)
dynamic = types.NewDynamicProperty(env.GetByValue(k.Value), env.GetByValue(v.Value))
})
if len(static) == 0 && dynamic == nil {
@@ -98,7 +94,7 @@ func (env *TypeEnv) GetByValue(v Value) types.Type {
return types.NewObject(static, dynamic)
case Set:
case *set:
var tpe types.Type
x.Foreach(func(elem *Term) {
tpe = types.Or(tpe, env.GetByValue(elem.Value))
@@ -161,12 +157,13 @@ func (env *TypeEnv) GetByRef(ref Ref) types.Type {
}
func (env *TypeEnv) getRefFallback(ref Ref) types.Type {
if env.next != nil {
return env.next.GetByRef(ref)
}
if RootDocumentNames.Contains(ref[0]) {
// types.A is an empty types.Any
// this is used to represent a potential non-local reference
return types.A
}
@@ -298,15 +295,11 @@ func (n *typeTreeNode) PutOne(key Value, tpe types.Type) {
func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
curr := n
for _, term := range path {
c, ok := curr.children.Get(term.Value)
var child *typeTreeNode
child, ok := curr.children.Get(term.Value)
if !ok {
child = newTypeTree()
child.key = term.Value
curr.children.Put(child.key, child)
} else {
child = c
}
curr = child
@@ -320,23 +313,18 @@ func (n *typeTreeNode) Put(path Ref, tpe types.Type) {
func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
curr := n
for i, term := range path {
c, ok := curr.children.Get(term.Value)
var child *typeTreeNode
child, ok := curr.children.Get(term.Value)
if !ok {
child = newTypeTree()
child.key = term.Value
curr.children.Put(child.key, child)
} else {
child = c
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 {
var err error
child.value, err = insertIntoObject(o, path[i+1:], tpe, env)
if err != nil {
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
}
} 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 {
var err error
child.value, err = insertIntoObject(o, path[i+1:], tpe, env)
if err != nil {
panic(fmt.Errorf("unreachable, insertIntoObject: %w", err))
}
}
}
@@ -348,8 +336,7 @@ func (n *typeTreeNode) Insert(path Ref, tpe types.Type, env *TypeEnv) {
if _, ok := tpe.(*types.Object); ok && curr.children.Len() > 0 {
// merge all leafs into the inserted object
leafs := curr.Leafs()
for p, t := range leafs {
for p, t := range curr.Leafs() {
var err error
curr.value, err = insertIntoObject(curr.value.(*types.Object), *p, t, env)
if err != nil {
@@ -387,7 +374,8 @@ func mergeTypes(a, b types.Type) types.Type {
bDynProps := bObj.DynamicProperties()
dynProps := types.NewDynamicProperty(
types.Or(aDynProps.Key, bDynProps.Key),
mergeTypes(aDynProps.Value, bDynProps.Value))
mergeTypes(aDynProps.Value, bDynProps.Value),
)
return types.NewObject(nil, dynProps)
} else if bAny, ok := b.(types.Any); ok && len(a.StaticProperties()) == 0 {
// If a is an object type with no static components ...
@@ -416,14 +404,14 @@ func mergeTypes(a, b types.Type) types.Type {
}
func (n *typeTreeNode) String() string {
b := strings.Builder{}
b := &strings.Builder{}
key := "-"
if k := n.key; k != nil {
b.WriteString(k.String())
} else {
b.WriteString("-")
key = k.String()
}
b.WriteString(key)
if v := n.value; v != nil {
b.WriteString(": ")
b.WriteString(v.String())
@@ -431,9 +419,7 @@ func (n *typeTreeNode) String() string {
n.children.Iter(func(_ Value, child *typeTreeNode) bool {
b.WriteString("\n\t+ ")
s := child.String()
s = strings.ReplaceAll(s, "\n", "\n\t")
b.WriteString(s)
b.WriteString(strings.ReplaceAll(child.String(), "\n", "\n\t"))
return false
})
@@ -484,7 +470,8 @@ func (n *typeTreeNode) Leafs() map[*Ref]types.Type {
func collectLeafs(n *typeTreeNode, path Ref, leafs map[*Ref]types.Type) {
nPath := append(path, NewTerm(n.key))
if n.Leaf() {
leafs[&nPath] = n.Value()
npc := nPath // copy of else nPath escapes to heap even if !n.Leaf()
leafs[&npc] = n.Value()
return
}
n.children.Iter(func(_ Value, v *typeTreeNode) bool {
@@ -512,7 +499,6 @@ func selectConstant(tpe types.Type, term *Term) types.Type {
// contains vars or refs, then the returned type will be a union of the
// possible types.
func selectRef(tpe types.Type, ref Ref) types.Type {
if tpe == nil || len(ref) == 0 {
return tpe
}
+15 -6
View File
@@ -99,26 +99,35 @@ func (e *Error) Error() string {
}
}
msg := fmt.Sprintf("%v: %v", e.Code, e.Message)
sb := strings.Builder{}
if len(prefix) > 0 {
msg = prefix + ": " + msg
sb.WriteString(prefix)
sb.WriteString(": ")
}
sb.WriteString(e.Code)
sb.WriteString(": ")
sb.WriteString(e.Message)
if e.Details != nil {
for _, line := range e.Details.Lines() {
msg += "\n\t" + line
sb.WriteString("\n\t")
sb.WriteString(line)
}
}
return msg
return sb.String()
}
// NewError returns a new Error object.
func NewError(code string, loc *Location, f string, a ...any) *Error {
return newErrorString(code, loc, fmt.Sprintf(f, a...))
}
func newErrorString(code string, loc *Location, m string) *Error {
return &Error{
Code: code,
Location: loc,
Message: fmt.Sprintf(f, a...),
Message: m,
}
}
+32 -42
View File
@@ -412,7 +412,7 @@ func (i *refindices) updateGlobMatch(rule *Rule, expr *Expr) {
if _, ok := match.Value.(Var); ok {
var ref Ref
for _, other := range i.rules[rule] {
if _, ok := other.Value.(Var); ok && other.Value.Compare(match.Value) == 0 {
if ov, ok := other.Value.(Var); ok && ov.Equal(match.Value) {
ref = other.Ref
}
}
@@ -586,28 +586,24 @@ func newTrieNodeImpl() *trieNode {
}
func (node *trieNode) Do(walker trieWalker) {
if node == nil {
return
}
next := walker.Do(node)
if next == nil {
return
}
if node.any != nil {
node.any.Do(next)
}
if node.undefined != nil {
node.undefined.Do(next)
}
node.any.Do(next)
node.undefined.Do(next)
node.scalars.Iter(func(_ Value, child *trieNode) bool {
child.Do(next)
return false
})
if node.array != nil {
node.array.Do(next)
}
if node.next != nil {
node.next.Do(next)
}
node.array.Do(next)
node.next.Do(next)
}
func (node *trieNode) Insert(ref Ref, value Value, mapper *valueMapper) *trieNode {
@@ -699,7 +695,6 @@ func (node *trieNode) insertArray(arr *Array) *trieNode {
}
func (node *trieNode) traverse(resolver ValueResolver, tr *trieTraversalResult) error {
if node == nil {
return nil
}
@@ -712,31 +707,31 @@ func (node *trieNode) traverse(resolver ValueResolver, tr *trieTraversalResult)
return err
}
if node.undefined != nil {
err = node.undefined.Traverse(resolver, tr)
if err != nil {
return err
}
err = node.undefined.Traverse(resolver, tr)
if err != nil {
return err
}
if v == nil {
return nil
}
if node.any != nil {
err = node.any.Traverse(resolver, tr)
if err != nil {
return err
}
err = node.any.Traverse(resolver, tr)
if err != nil {
return err
}
if err := node.traverseValue(resolver, tr, v); err != nil {
err = node.traverseValue(resolver, tr, v)
if err != nil {
return err
}
for i := range node.mappers {
if err := node.traverseValue(resolver, tr, node.mappers[i].MapValue(v)); err != nil {
return err
mapped := node.mappers[i].MapValue(v)
if !ValueEqual(mapped, v) {
if err := node.traverseValue(resolver, tr, mapped); err != nil {
return err
}
}
}
@@ -747,9 +742,6 @@ func (node *trieNode) traverseValue(resolver ValueResolver, tr *trieTraversalRes
switch value := value.(type) {
case *Array:
if node.array == nil {
return nil
}
return node.array.traverseArray(resolver, tr, value)
case Null, Boolean, Number, String:
@@ -764,16 +756,17 @@ func (node *trieNode) traverseValue(resolver ValueResolver, tr *trieTraversalRes
}
func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalResult, arr *Array) error {
if node == nil {
return nil
}
if arr.Len() == 0 {
return node.Traverse(resolver, tr)
}
if node.any != nil {
err := node.any.traverseArray(resolver, tr, arr.Slice(1, -1))
if err != nil {
return err
}
err := node.any.traverseArray(resolver, tr, arr.Slice(1, -1))
if err != nil {
return err
}
head := arr.Elem(0).Value
@@ -784,10 +777,7 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
switch head := head.(type) {
case Null, Boolean, Number, String:
child, ok := node.scalars.Get(head)
if !ok {
return nil
}
child, _ := node.scalars.Get(head)
return child.traverseArray(resolver, tr, arr.Slice(1, -1))
}
@@ -795,7 +785,6 @@ func (node *trieNode) traverseArray(resolver ValueResolver, tr *trieTraversalRes
}
func (node *trieNode) traverseUnknown(resolver ValueResolver, tr *trieTraversalResult) error {
if node == nil {
return nil
}
@@ -884,7 +873,6 @@ func indexValue(b Value) (Value, bool) {
}
func globDelimiterToString(delim *Term) (string, bool) {
arr, ok := delim.Value.(*Array)
if !ok {
return "", false
@@ -895,14 +883,16 @@ func globDelimiterToString(delim *Term) (string, bool) {
if arr.Len() == 0 {
result = "."
} else {
sb := strings.Builder{}
for i := range arr.Len() {
term := arr.Elem(i)
s, ok := term.Value.(String)
if !ok {
return "", false
}
result += string(s)
sb.WriteString(string(s))
}
result = sb.String()
}
return result, true
+148 -3
View File
@@ -158,18 +158,42 @@ func (s *Scanner) WithoutKeywords(kws map[string]tokens.Token) (*Scanner, map[st
return &cpy, kw
}
type ScanOptions struct {
continueTemplateString bool
rawTemplateString bool
}
type ScanOption func(*ScanOptions)
// ContinueTemplateString will continue scanning a template string
func ContinueTemplateString(raw bool) ScanOption {
return func(opts *ScanOptions) {
opts.continueTemplateString = true
opts.rawTemplateString = raw
}
}
// Scan will increment the scanners position in the source
// code until the next token is found. The token, starting position
// of the token, string literal, and any errors encountered are
// returned. A token will always be returned, the caller must check
// for any errors before using the other values.
func (s *Scanner) Scan() (tokens.Token, Position, string, []Error) {
func (s *Scanner) Scan(opts ...ScanOption) (tokens.Token, Position, string, []Error) {
scanOpts := &ScanOptions{}
for _, opt := range opts {
opt(scanOpts)
}
pos := Position{Offset: s.offset - s.width, Row: s.row, Col: s.col, Tabs: s.tabs}
var tok tokens.Token
var lit string
if s.isWhitespace() {
if scanOpts.continueTemplateString {
if scanOpts.rawTemplateString {
lit, tok = s.scanRawTemplateString()
} else {
lit, tok = s.scanTemplateString()
}
} else if s.isWhitespace() {
// string(rune) is an unnecessary heap allocation in this case as we know all
// the possible whitespace values, and can simply translate to string ourselves
switch s.curr {
@@ -275,6 +299,17 @@ func (s *Scanner) Scan() (tokens.Token, Position, string, []Error) {
tok = tokens.Semicolon
case '.':
tok = tokens.Dot
case '$':
switch s.curr {
case '`':
s.next()
lit, tok = s.scanRawTemplateString()
case '"':
s.next()
lit, tok = s.scanTemplateString()
default:
s.error("illegal $ character")
}
}
}
@@ -395,6 +430,116 @@ func (s *Scanner) scanRawString() string {
return util.ByteSliceToString(s.bs[start : s.offset-1])
}
func (s *Scanner) scanTemplateString() (string, tokens.Token) {
tok := tokens.TemplateStringPart
start := s.literalStart()
var escapes []int
for {
ch := s.curr
if ch == '\n' || ch < 0 {
s.error("non-terminated string")
break
}
s.next()
if ch == '"' {
tok = tokens.TemplateStringEnd
break
}
if ch == '{' {
break
}
if ch == '\\' {
switch s.curr {
case '\\', '"', '/', 'b', 'f', 'n', 'r', 't':
s.next()
case '{':
escapes = append(escapes, s.offset-1)
s.next()
case 'u':
s.next()
s.next()
s.next()
s.next()
default:
s.error("illegal escape sequence")
}
}
}
// Lazily remove escapes to not unnecessarily allocate a new byte slice
if len(escapes) > 0 {
return util.ByteSliceToString(removeEscapes(s, escapes, start)), tok
}
return util.ByteSliceToString(s.bs[start : s.offset-1]), tok
}
func (s *Scanner) scanRawTemplateString() (string, tokens.Token) {
tok := tokens.RawTemplateStringPart
start := s.literalStart()
var escapes []int
for {
ch := s.curr
if ch < 0 {
s.error("non-terminated string")
break
}
s.next()
if ch == '`' {
tok = tokens.RawTemplateStringEnd
break
}
if ch == '{' {
break
}
if ch == '\\' {
switch s.curr {
case '{':
escapes = append(escapes, s.offset-1)
s.next()
}
}
}
// Lazily remove escapes to not unnecessarily allocate a new byte slice
if len(escapes) > 0 {
return util.ByteSliceToString(removeEscapes(s, escapes, start)), tok
}
return util.ByteSliceToString(s.bs[start : s.offset-1]), tok
}
func removeEscapes(s *Scanner, escapes []int, start int) []byte {
from := start
bs := make([]byte, 0, s.offset-start-len(escapes))
for _, escape := range escapes {
// Append the bytes before the escape sequence.
if escape > from {
bs = append(bs, s.bs[from:escape-1]...)
}
// Skip the escape character.
from = escape
}
// Append the remaining bytes after the last escape sequence.
if from < s.offset-1 {
bs = append(bs, s.bs[from:s.offset-1]...)
}
return bs
}
func (s *Scanner) scanComment() string {
start := s.literalStart()
for s.curr != '\n' && s.curr != -1 {
+61 -47
View File
@@ -39,6 +39,10 @@ const (
Number
String
TemplateStringPart
TemplateStringEnd
RawTemplateStringPart
RawTemplateStringEnd
LBrack
RBrack
@@ -67,6 +71,7 @@ const (
Lte
Dot
Semicolon
Dollar
Every
Contains
@@ -74,53 +79,58 @@ const (
)
var strings = [...]string{
Illegal: "illegal",
EOF: "eof",
Whitespace: "whitespace",
Comment: "comment",
Ident: "identifier",
Package: "package",
Import: "import",
As: "as",
Default: "default",
Else: "else",
Not: "not",
Some: "some",
With: "with",
Null: "null",
True: "true",
False: "false",
Number: "number",
String: "string",
LBrack: "[",
RBrack: "]",
LBrace: "{",
RBrace: "}",
LParen: "(",
RParen: ")",
Comma: ",",
Colon: ":",
Add: "plus",
Sub: "minus",
Mul: "mul",
Quo: "div",
Rem: "rem",
And: "and",
Or: "or",
Unify: "eq",
Equal: "equal",
Assign: "assign",
In: "in",
Neq: "neq",
Gt: "gt",
Lt: "lt",
Gte: "gte",
Lte: "lte",
Dot: ".",
Semicolon: ";",
Every: "every",
Contains: "contains",
If: "if",
Illegal: "illegal",
EOF: "eof",
Whitespace: "whitespace",
Comment: "comment",
Ident: "identifier",
Package: "package",
Import: "import",
As: "as",
Default: "default",
Else: "else",
Not: "not",
Some: "some",
With: "with",
Null: "null",
True: "true",
False: "false",
Number: "number",
String: "string",
TemplateStringPart: "template-string-part",
TemplateStringEnd: "template-string-end",
RawTemplateStringPart: "raw-template-string-part",
RawTemplateStringEnd: "raw-template-string-end",
LBrack: "[",
RBrack: "]",
LBrace: "{",
RBrace: "}",
LParen: "(",
RParen: ")",
Comma: ",",
Colon: ":",
Add: "plus",
Sub: "minus",
Mul: "mul",
Quo: "div",
Rem: "rem",
And: "and",
Or: "or",
Unify: "eq",
Equal: "equal",
Assign: "assign",
In: "in",
Neq: "neq",
Gt: "gt",
Lt: "lt",
Gte: "gte",
Lte: "lte",
Dot: ".",
Semicolon: ";",
Dollar: "dollar",
Every: "every",
Contains: "contains",
If: "if",
}
var keywords = map[string]Token{
@@ -147,3 +157,7 @@ func IsKeyword(tok Token) bool {
_, ok := keywords[strings[tok]]
return ok
}
func KeywordFor(tok Token) string {
return strings[tok]
}
+78 -14
View File
@@ -5,6 +5,7 @@
package ast
import (
"iter"
"strconv"
)
@@ -19,27 +20,46 @@ type internable interface {
var (
InternedNullValue Value = Null{}
InternedNullTerm = &Term{Value: InternedNullValue}
InternedNullTerm = NewTerm(InternedNullValue)
InternedBooleanTrueValue Value = Boolean(true)
InternedBooleanFalseValue Value = Boolean(false)
InternedBooleanTrueTerm = &Term{Value: InternedBooleanTrueValue}
InternedBooleanFalseTerm = &Term{Value: InternedBooleanFalseValue}
InternedEmptyStringValue Value = String("")
InternedEmptyArrayValue Value = NewArray()
InternedEmptyRefValue Value = Ref{}
InternedEmptyObjectValue Value = NewObject()
InternedEmptySetValue Value = NewSet()
InternedEmptyString = StringTerm("")
InternedEmptyObject = ObjectTerm()
InternedEmptyArray = ArrayTerm()
InternedEmptySet = SetTerm()
InternedEmptyArrayValue = NewArray()
InternedBooleanTrue = NewTerm(InternedBooleanTrueValue)
InternedBooleanFalse = NewTerm(InternedBooleanFalseValue)
InternedEmptyString = NewTerm(InternedEmptyStringValue)
InternedEmptyObject = NewTerm(InternedEmptyObjectValue)
InternedEmptyArray = NewTerm(InternedEmptyArrayValue)
InternedEmptySet = NewTerm(InternedEmptySetValue)
// since this is by far the most common negative number
minusOneValue Value = Number("-1")
minusOneTerm = &Term{Value: minusOneValue}
minusOneTerm = NewTerm(minusOneValue)
internedStringTerms = map[string]*Term{
"": InternedEmptyString,
}
internedVarValues = map[string]Value{
"input": Var("input"),
"data": Var("data"),
"args": Var("args"),
"schema": Var("schema"),
"key": Var("key"),
"value": Var("value"),
"future": Var("future"),
"rego": Var("rego"),
"set": Var("set"),
"internal": Var("internal"),
"else": Var("else"),
"i": Var("i"), "j": Var("j"), "k": Var("k"), "v": Var("v"), "x": Var("x"), "y": Var("y"), "z": Var("z"),
}
)
// InternStringTerm interns the given strings as terms. Note that Interning is
@@ -52,7 +72,21 @@ func InternStringTerm(str ...string) {
continue
}
internedStringTerms[s] = StringTerm(s)
internedStringTerms[s] = &Term{Value: String(s)}
}
}
// InternVarValue interns the given variable names as Var Values. Note that Interning is
// considered experimental and should not be relied upon by external code.
// WARNING: This must **only** be called at initialization time, as the
// interned terms are shared globally, and the underlying map is not thread-safe.
func InternVarValue(names ...string) {
for _, name := range names {
if _, ok := internedVarValues[name]; ok {
continue
}
internedVarValues[name] = Var(name)
}
}
@@ -94,6 +128,16 @@ func InternedValue[T internable](v T) Value {
return InternedValueOr(v, internedTermValue)
}
// InternedVarValue returns an interned Var Value for the given name. If the
// name is not interned, a new Var Value is returned.
func InternedVarValue(name string) Value {
if v, ok := internedVarValues[name]; ok {
return v
}
return Var(name)
}
// InternedValueOr returns an interned Value for scalar v. Calls supplier
// to produce a Value if the value is not interned.
func InternedValueOr[T internable](v T, supplier func(T) Value) Value {
@@ -157,6 +201,13 @@ func InternedTerm[T internable](v T) *Term {
}
}
// InternedItem works just like [Item] but returns interned terms for both
// key and value where possible. This is mostly useful for making tests less
// verbose.
func InternedItem[K, V internable](key K, value V) [2]*Term {
return [2]*Term{InternedTerm(key), InternedTerm(value)}
}
// InternedIntFromString returns a term with the given integer value if the string
// maps to an interned term. If the string does not map to an interned term, nil is
// returned.
@@ -168,6 +219,19 @@ func InternedIntNumberTermFromString(s string) *Term {
return nil
}
// InternedIntRange returns a sequence of interned integer number terms
// from start (inclusive) to end (exclusive). For values outside of the
// interned range, non-interned IntNumberTerms are returned.
func InternedIntRange(start, end int) iter.Seq[*Term] {
return func(yield func(*Term) bool) {
for i := start; i < end; i++ {
if !yield(internedIntNumberTerm(i)) {
return
}
}
}
}
// HasInternedIntNumberTerm returns true if the given integer value maps to an interned
// term, otherwise false.
func HasInternedIntNumberTerm(i int) bool {
@@ -206,10 +270,10 @@ func internedBooleanValue(b bool) Value {
// InternedBooleanTerm returns an interned term with the given boolean value.
func internedBooleanTerm(b bool) *Term {
if b {
return InternedBooleanTrueTerm
return InternedBooleanTrue
}
return InternedBooleanFalseTerm
return InternedBooleanFalse
}
func internedIntNumberValue(i int) Value {
@@ -276,7 +340,7 @@ func init() {
// Various
"data", "input", "result", "keywords", "path", "v1", "error", "partial",
// HTTP
"code", "message", "status_code", "method", "url", "uri",
"code", "message", "status_code", "method", "url", "uri", "body", "raw_body", "headers", "query_params",
// JWT
"enc", "cty", "iss", "exp", "nbf", "aud", "secret", "cert",
// Decisions
+32 -9
View File
@@ -8,6 +8,7 @@ import (
"fmt"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/util"
)
// Location records a position in source code
@@ -28,10 +29,10 @@ func NewLocation(text []byte, file string, row int, col int) *Location {
// Equal checks if two locations are equal to each other.
func (loc *Location) Equal(other *Location) bool {
return bytes.Equal(loc.Text, other.Text) &&
loc.File == other.File &&
return loc.File == other.File &&
loc.Row == other.Row &&
loc.Col == other.Col
loc.Col == other.Col &&
bytes.Equal(loc.Text, other.Text)
}
// Errorf returns a new error value with a message formatted to include the location
@@ -57,13 +58,35 @@ func (loc *Location) Format(f string, a ...any) string {
}
func (loc *Location) String() string {
if len(loc.File) > 0 {
return fmt.Sprintf("%v:%v", loc.File, loc.Row)
buf, _ := loc.AppendText(make([]byte, 0, loc.StringLength()))
return util.ByteSliceToString(buf)
}
func (loc *Location) AppendText(buf []byte) ([]byte, error) {
if loc != nil {
switch {
case len(loc.File) > 0:
buf = util.AppendInt(append(append(buf, loc.File...), ':'), loc.Row)
case len(loc.Text) > 0:
buf = append(buf, loc.Text...)
default:
buf = util.AppendInt(append(util.AppendInt(buf, loc.Row), ':'), loc.Col)
}
}
if len(loc.Text) > 0 {
return string(loc.Text)
return buf, nil
}
func (loc *Location) StringLength() (n int) {
if loc != nil {
if l := len(loc.File); l > 0 {
n = l + 1 + util.NumDigitsInt(loc.Row)
} else if l := len(loc.Text); l > 0 {
n = l
} else {
n = util.NumDigitsInt(loc.Row) + 1 + util.NumDigitsInt(loc.Col)
}
}
return fmt.Sprintf("%v:%v", loc.Row, loc.Col)
return n
}
// Compare returns -1, 0, or 1 to indicate if this loc is less than, equal to,
@@ -71,7 +94,7 @@ func (loc *Location) String() string {
// column of the Location (but not on the text.) Nil locations are greater than
// non-nil locations.
func (loc *Location) Compare(other *Location) int {
if loc == nil && other == nil {
if loc == other {
return 0
} else if loc == nil {
return 1
+318 -174
View File
@@ -20,12 +20,13 @@ import (
"strings"
"unicode/utf8"
"gopkg.in/yaml.v3"
"go.yaml.in/yaml/v3"
"github.com/open-policy-agent/opa/v1/ast/internal/scanner"
"github.com/open-policy-agent/opa/v1/ast/internal/tokens"
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
"github.com/open-policy-agent/opa/v1/ast/location"
"github.com/open-policy-agent/opa/v1/util"
)
// DefaultMaxParsingRecursionDepth is the default maximum recursion
@@ -57,6 +58,25 @@ const (
RegoV1
)
var (
// this is the name to use for instantiating an empty set, e.g., `set()`.
setConstructor = RefTerm(VarTerm("set"))
preAllocWildcards = [...]Value{
Var("$0"), Var("$1"), Var("$2"), Var("$3"), Var("$4"), Var("$5"),
Var("$6"), Var("$7"), Var("$8"), Var("$9"), Var("$10"),
}
// use static references to avoid allocations, and
// copy them to the call term only when needed
memberWithKeyRef = MemberWithKey.Ref()
memberRef = Member.Ref()
newlineBytes = []byte{'\n'}
metadataBytes = []byte("METADATA")
metadataParserPool = util.NewSyncPool[metadataParser]()
)
func (v RegoVersion) Int() int {
if v == RegoV1 {
return 1
@@ -88,17 +108,17 @@ func RegoVersionFromInt(i int) RegoVersion {
// can do efficient shallow copies of these values when doing a
// save() and restore().
type state struct {
s *scanner.Scanner
lastEnd int
skippedNL bool
tok tokens.Token
tokEnd int
lit string
loc Location
errors Errors
hints []string
comments []*Comment
hints []string
s *scanner.Scanner
loc Location
lit string
lastEnd int
tokEnd int
wildcard int
tok tokens.Token
skippedNL bool
}
func (s *state) String() string {
@@ -451,7 +471,6 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
// next type of statement. If a statement can be parsed, continue from that
// point trying to parse packages, imports, etc. in the same order.
for p.s.tok != tokens.EOF {
s := p.save()
if pkg := p.parsePackage(); pkg != nil {
@@ -512,12 +531,12 @@ func (p *Parser) Parse() ([]Statement, []*Comment, Errors) {
}
func (p *Parser) parseAnnotations(stmts []Statement) []Statement {
annotStmts, errs := parseAnnotations(p.s.comments)
for _, err := range errs {
p.error(err.Location, err.Message)
}
stmts = slices.Grow(stmts, len(annotStmts))
for _, annotStmt := range annotStmts {
stmts = append(stmts, annotStmt)
}
@@ -525,53 +544,54 @@ func (p *Parser) parseAnnotations(stmts []Statement) []Statement {
return stmts
}
func parseAnnotations(comments []*Comment) ([]*Annotations, Errors) {
func parseAnnotations(comments []*Comment) (stmts []*Annotations, errs Errors) {
numBlocks := CountFunc(comments, isMetadataComment)
if numBlocks == 0 {
return nil, nil
}
var hint = []byte("METADATA")
var curr *metadataParser
var blocks []*metadataParser
stmts = make([]*Annotations, 0, numBlocks)
mdp := metadataParserPool.Get()
if mdp.buf == nil {
mdp.buf = &bytes.Buffer{}
}
for i := range comments {
if curr != nil {
if comments[i].Location.Row == comments[i-1].Location.Row+1 && comments[i].Location.Col == 1 {
curr.Append(comments[i])
continue
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])
}
if a, err := mdp.Parse(); err != nil {
errs = append(errs, &Error{Code: ParseErr, Message: err.Error(), Location: mdp.loc})
} else {
stmts = append(stmts, a)
}
curr = nil
}
if bytes.HasPrefix(bytes.TrimSpace(comments[i].Text), hint) {
curr = newMetadataParser(comments[i].Location)
blocks = append(blocks, curr)
}
}
var stmts []*Annotations
var errs Errors
for _, b := range blocks {
a, err := b.Parse()
if err != nil {
errs = append(errs, &Error{
Code: ParseErr,
Message: err.Error(),
Location: b.loc,
})
} else {
stmts = append(stmts, a)
}
}
metadataParserPool.Put(mdp)
return stmts, errs
}
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
}
func (p *Parser) parsePackage() *Package {
var pkg Package
pkg.SetLoc(p.s.Loc())
if p.s.tok != tokens.Package {
return nil
}
var pkg Package
pkg.SetLoc(p.s.Loc())
p.scanWS()
// Make sure we allow the first term of refs to be the 'package' keyword.
@@ -633,14 +653,13 @@ func (p *Parser) parsePackage() *Package {
}
func (p *Parser) parseImport() *Import {
var imp Import
imp.SetLoc(p.s.Loc())
if p.s.tok != tokens.Import {
return nil
}
var imp Import
imp.SetLoc(p.s.Loc())
p.scanWS()
// Make sure we allow the first term of refs to be the 'import' keyword.
@@ -952,7 +971,7 @@ func (p *Parser) parseRules() []*Rule {
next.Head.keywords = rule.Head.keywords
for i := range next.Head.Args {
if v, ok := next.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
next.Head.Args[i].Value = Var(p.genwildcard())
next.Head.Args[i].Value = p.genwildcard()
}
}
setLocRecursive(next.Head, loc)
@@ -972,7 +991,7 @@ func (p *Parser) parseElse(head *Head) *Rule {
rule.Head.generatedValue = false
for i := range rule.Head.Args {
if v, ok := rule.Head.Args[i].Value.(Var); ok && v.IsWildcard() {
rule.Head.Args[i].Value = Var(p.genwildcard())
rule.Head.Args[i].Value = p.genwildcard()
}
}
rule.Head.SetLoc(p.s.Loc())
@@ -1281,14 +1300,11 @@ func (p *Parser) parseLiteralExpr(negated bool) *Expr {
}
func (p *Parser) parseWith() []*With {
withs := []*With{}
for {
with := With{Location: p.s.Loc()}
with := With{
Location: p.s.Loc(),
}
p.scan()
if p.s.tok != tokens.Ident {
@@ -1525,11 +1541,6 @@ func (p *Parser) parseTermInfixCallInList() *Term {
return p.parseTermIn(nil, false, p.s.loc.Offset)
}
// use static references to avoid allocations, and
// copy them to the call term only when needed
var memberWithKeyRef = MemberWithKey.Ref()
var memberRef = Member.Ref()
func (p *Parser) parseTermIn(lhs *Term, keyVal bool, offset int) *Term {
if !p.enter() {
return nil
@@ -1731,6 +1742,10 @@ func (p *Parser) parseTerm() *Term {
term = p.parseNumber()
case tokens.String:
term = p.parseString()
case tokens.TemplateStringPart, tokens.TemplateStringEnd:
term = p.parseTemplateString(false)
case tokens.RawTemplateStringPart, tokens.RawTemplateStringEnd:
term = p.parseTemplateString(true)
case tokens.Ident, tokens.Contains: // NOTE(sr): contains anywhere BUT in rule heads gets no special treatment
term = p.parseVar()
case tokens.LBrack:
@@ -1762,7 +1777,7 @@ func (p *Parser) parseTermFinish(head *Term, skipws bool) *Term {
return nil
}
offset := p.s.loc.Offset
p.doScan(skipws)
p.doScan(skipws, noScanOptions...)
switch p.s.tok {
case tokens.LParen, tokens.Dot, tokens.LBrack:
@@ -1783,7 +1798,7 @@ func (p *Parser) parseHeadFinish(head *Term, skipws bool) *Term {
return nil
}
offset := p.s.loc.Offset
p.doScan(false)
p.scanWS()
switch p.s.tok {
case tokens.Add, tokens.Sub, tokens.Mul, tokens.Quo, tokens.Rem,
@@ -1791,7 +1806,7 @@ func (p *Parser) parseHeadFinish(head *Term, skipws bool) *Term {
tokens.Equal, tokens.Neq, tokens.Gt, tokens.Gte, tokens.Lt, tokens.Lte:
p.illegalToken()
case tokens.Whitespace:
p.doScan(skipws)
p.doScan(skipws, noScanOptions...)
}
switch p.s.tok {
@@ -1878,7 +1893,12 @@ func (p *Parser) parseNumber() *Term {
func (p *Parser) parseString() *Term {
if p.s.lit[0] == '"' {
if p.s.lit == "\"\"" {
return NewTerm(InternedEmptyString.Value).SetLocation(p.s.Loc())
return NewTerm(InternedEmptyStringValue).SetLocation(p.s.Loc())
}
inner := p.s.lit[1 : len(p.s.lit)-1]
if !strings.ContainsRune(inner, '\\') { // nothing to un-escape
return StringTerm(inner).SetLocation(p.s.Loc())
}
var s string
@@ -1898,8 +1918,119 @@ func (p *Parser) parseRawString() *Term {
return StringTerm(p.s.lit[1 : len(p.s.lit)-1]).SetLocation(p.s.Loc())
}
// this is the name to use for instantiating an empty set, e.g., `set()`.
var setConstructor = RefTerm(VarTerm("set"))
func templateStringPartToStringLiteral(tok tokens.Token, lit string) (string, error) {
switch tok {
case tokens.TemplateStringPart, tokens.TemplateStringEnd:
inner := lit[1 : len(lit)-1]
if !strings.ContainsRune(inner, '\\') { // nothing to un-escape
return inner, nil
}
buf := make([]byte, 0, len(inner)+2)
buf = append(buf, '"')
buf = append(buf, inner...)
buf = append(buf, '"')
var s string
if err := json.Unmarshal(buf, &s); err != nil {
return "", fmt.Errorf("illegal template-string part: %s", lit)
}
return s, nil
case tokens.RawTemplateStringPart, tokens.RawTemplateStringEnd:
return lit[1 : len(lit)-1], nil
default:
return "", errors.New("expected template-string part")
}
}
func (p *Parser) parseTemplateString(multiLine bool) *Term {
loc := p.s.Loc()
if !p.po.Capabilities.ContainsFeature(FeatureTemplateStrings) {
p.errorf(loc, "template strings are not supported by current capabilities")
return nil
}
var parts []Node
for {
s, err := templateStringPartToStringLiteral(p.s.tok, p.s.lit)
if err != nil {
p.error(p.s.Loc(), err.Error())
return nil
}
// Don't add empty strings
if len(s) > 0 {
parts = append(parts, StringTerm(s).SetLocation(p.s.Loc()))
}
if p.s.tok == tokens.TemplateStringEnd || p.s.tok == tokens.RawTemplateStringEnd {
break
}
numCommentsBefore := len(p.s.comments)
p.scan()
numCommentsAfter := len(p.s.comments)
expr := p.parseLiteral()
if expr == nil {
p.error(p.s.Loc(), "invalid template-string expression")
return nil
}
if expr.Negated {
p.errorf(expr.Loc(), "unexpected negation ('%s') in template-string expression", tokens.KeywordFor(tokens.Not))
return nil
}
// Note: Actually unification
if expr.IsEquality() {
p.errorf(expr.Loc(), "unexpected unification ('=') in template-string expression")
return nil
}
if expr.IsAssignment() {
p.errorf(expr.Loc(), "unexpected assignment (':=') in template-string expression")
return nil
}
if expr.IsEvery() {
p.errorf(expr.Loc(), "unexpected '%s' in template-string expression", tokens.KeywordFor(tokens.Every))
return nil
}
if expr.IsSome() {
p.errorf(expr.Loc(), "unexpected '%s' in template-string expression", tokens.KeywordFor(tokens.Some))
return nil
}
// FIXME: Can we optimize for collections and comprehensions too? To qualify, they must not contain refs or calls.
var nonOptional bool
if term, ok := expr.Terms.(*Term); ok && numCommentsAfter == numCommentsBefore {
switch term.Value.(type) {
case String, Number, Boolean, Null:
nonOptional = true
parts = append(parts, term)
}
}
if !nonOptional {
parts = append(parts, expr)
}
if p.s.tok != tokens.RBrace {
p.errorf(p.s.Loc(), "expected %s to end template string expression", tokens.RBrace)
return nil
}
p.doScan(false, scanner.ContinueTemplateString(multiLine))
}
// When there are template-expressions, the initial location will only contain the text up to the first expression
loc.Text = p.s.Text(loc.Offset, p.s.tokEnd)
return TemplateStringTerm(multiLine, parts...).SetLocation(loc)
}
func (p *Parser) parseCall(operator *Term, offset int) (term *Term) {
if !p.enter() {
@@ -1978,7 +2109,7 @@ func (p *Parser) parseRef(head *Term, offset int) (term *Term) {
term = p.parseRef(term, offset)
}
}
end = p.s.tokEnd
end = p.s.lastEnd
return term
case tokens.LBrack:
p.scan()
@@ -2042,7 +2173,6 @@ func (p *Parser) parseArray() (term *Term) {
// Does this represent a set comprehension or a set containing binary OR
// call? We resolve the ambiguity by prioritizing comprehensions.
head := p.parseTerm()
if head == nil {
return nil
}
@@ -2286,7 +2416,7 @@ func (p *Parser) parseTermList(end tokens.Token, r []*Term) []*Term {
}
continue
default:
p.illegal(fmt.Sprintf("expected %q or %q", tokens.Comma, end))
p.illegal("expected %q or %q", tokens.Comma, end)
return nil
}
}
@@ -2316,12 +2446,12 @@ func (p *Parser) parseTermPairList(end tokens.Token, r [][2]*Term) [][2]*Term {
}
continue
default:
p.illegal(fmt.Sprintf("expected %q or %q", tokens.Comma, end))
p.illegal("expected %q or %q", tokens.Comma, end)
return nil
}
}
default:
p.illegal(fmt.Sprintf("expected %q", tokens.Colon))
p.illegal("expected %q", tokens.Colon)
return nil
}
}
@@ -2331,7 +2461,8 @@ func (p *Parser) parseTermPairList(end tokens.Token, r [][2]*Term) [][2]*Term {
func (p *Parser) parseTermOp(values ...tokens.Token) *Term {
if slices.Contains(values, p.s.tok) {
r := RefTerm(VarTerm(p.s.tok.String()).SetLocation(p.s.Loc())).SetLocation(p.s.Loc())
loc := p.s.Loc()
r := RefTerm(VarTerm(p.s.tok.String()).SetLocation(loc)).SetLocation(loc)
p.scan()
return r
}
@@ -2341,11 +2472,12 @@ func (p *Parser) parseTermOp(values ...tokens.Token) *Term {
func (p *Parser) parseTermOpName(ref Ref, values ...tokens.Token) *Term {
if slices.Contains(values, p.s.tok) {
cp := ref.Copy()
loc := p.s.Loc()
for _, r := range cp {
r.SetLocation(p.s.Loc())
r.SetLocation(loc)
}
t := RefTerm(cp...)
t.SetLocation(p.s.Loc())
t.SetLocation(loc)
p.scan()
return t
}
@@ -2353,48 +2485,69 @@ func (p *Parser) parseTermOpName(ref Ref, values ...tokens.Token) *Term {
}
func (p *Parser) parseVar() *Term {
s := p.s.lit
term := VarTerm(s).SetLocation(p.s.Loc())
// Update wildcard values with unique identifiers
if term.Equal(Wildcard) {
term.Value = Var(p.genwildcard())
if p.s.lit == WildcardString {
// Update wildcard values with unique identifiers
return NewTerm(p.genwildcard()).SetLocation(p.s.Loc())
}
return term
return VarTerm(p.s.lit).SetLocation(p.s.Loc())
}
func (p *Parser) genwildcard() string {
c := p.s.wildcard
func (p *Parser) genwildcard() Value {
var v Value
if p.s.wildcard < len(preAllocWildcards) {
v = preAllocWildcards[p.s.wildcard]
} else {
v = Var(WildcardPrefix + strconv.Itoa(p.s.wildcard))
}
p.s.wildcard++
return fmt.Sprintf("%v%d", WildcardPrefix, c)
return v
}
func (p *Parser) error(loc *location.Location, reason string) {
p.errorf(loc, "%s", reason)
}
func (p *Parser) errorf(loc *location.Location, f string, a ...any) {
msg := strings.Builder{}
msg.WriteString(fmt.Sprintf(f, a...))
switch len(p.s.hints) {
func writeHints(msg *strings.Builder, hints []string) {
switch len(hints) {
case 0: // nothing to do
case 1:
msg.WriteString(" (hint: ")
msg.WriteString(p.s.hints[0])
msg.WriteRune(')')
msg.WriteString(hints[0])
msg.WriteByte(')')
default:
msg.WriteString(" (hints: ")
for i, h := range p.s.hints {
for i, h := range hints {
if i > 0 {
msg.WriteString(", ")
}
msg.WriteString(h)
}
msg.WriteRune(')')
msg.WriteByte(')')
}
}
func (p *Parser) error(loc *location.Location, reason string) {
msg := reason
if len(p.s.hints) > 0 {
sb := &strings.Builder{}
sb.WriteString(reason)
writeHints(sb, p.s.hints)
msg = sb.String()
}
p.s.errors = append(p.s.errors, &Error{
Code: ParseErr,
Message: msg,
Location: loc,
Details: newParserErrorDetail(p.s.s.Bytes(), loc.Offset),
})
p.s.hints = nil
}
func (p *Parser) errorf(loc *location.Location, f string, a ...any) {
msg := &strings.Builder{}
fmt.Fprintf(msg, f, a...)
if len(p.s.hints) > 0 {
writeHints(msg, p.s.hints)
}
p.s.errors = append(p.s.errors, &Error{
@@ -2406,28 +2559,25 @@ func (p *Parser) errorf(loc *location.Location, f string, a ...any) {
p.s.hints = nil
}
func (p *Parser) hint(f string, a ...any) {
p.s.hints = append(p.s.hints, fmt.Sprintf(f, a...))
func (p *Parser) hint(s string) {
p.s.hints = append(p.s.hints, s)
}
func (p *Parser) illegal(note string, a ...any) {
tok := p.s.tok.String()
if p.s.tok == tokens.Illegal {
p.errorf(p.s.Loc(), "illegal token")
return
}
tok := p.s.tok.String()
tokType := "token"
if tokens.IsKeyword(p.s.tok) {
tokType = "keyword"
} else if _, ok := allFutureKeywords[p.s.tok.String()]; ok {
if _, ok := allFutureKeywords[tok]; ok || tokens.IsKeyword(p.s.tok) {
tokType = "keyword"
}
note = fmt.Sprintf(note, a...)
if len(note) > 0 {
p.errorf(p.s.Loc(), "unexpected %s %s: %s", tok, tokType, note)
p.errorf(p.s.Loc(), "unexpected %s %s: %s", tok, tokType, fmt.Sprintf(note, a...))
} else {
p.errorf(p.s.Loc(), "unexpected %s %s", tok, tokType)
}
@@ -2437,15 +2587,17 @@ func (p *Parser) illegalToken() {
p.illegal("")
}
var noScanOptions []scanner.ScanOption
func (p *Parser) scan() {
p.doScan(true)
p.doScan(true, noScanOptions...)
}
func (p *Parser) scanWS() {
p.doScan(false)
p.doScan(false, noScanOptions...)
}
func (p *Parser) doScan(skipws bool) {
func (p *Parser) doScan(skipws bool, scanOpts ...scanner.ScanOption) {
// NOTE(tsandall): the last position is used to compute the "text" field for
// complex AST nodes. Whitespace never affects the last position of an AST
@@ -2458,7 +2610,7 @@ func (p *Parser) doScan(skipws bool) {
var errs []scanner.Error
for {
var pos scanner.Position
p.s.tok, pos, p.s.lit, errs = p.s.s.Scan()
p.s.tok, pos, p.s.lit, errs = p.s.s.Scan(scanOpts...)
p.s.tokEnd = pos.End
p.s.loc.Row = pos.Row
@@ -2513,12 +2665,10 @@ func (p *Parser) restore(s *state) {
}
func setLocRecursive(x any, loc *location.Location) {
NewGenericVisitor(func(x any) bool {
if node, ok := x.(Node); ok {
node.SetLoc(loc)
}
WalkNodes(x, func(n Node) bool {
n.SetLoc(loc)
return false
}).Walk(x)
})
}
func (p *Parser) setLoc(term *Term, loc *location.Location, offset, end int) *Term {
@@ -2601,13 +2751,17 @@ type rawAnnotation struct {
}
type metadataParser struct {
buf *bytes.Buffer
comments []*Comment
buf *bytes.Buffer
loc *location.Location
}
func newMetadataParser(loc *Location) *metadataParser {
return &metadataParser{loc: loc, buf: bytes.NewBuffer(nil)}
func (b *metadataParser) Reset(loc *location.Location) {
b.comments = b.comments[:0]
b.loc = loc
if b.buf != nil {
b.buf.Reset()
}
}
func (b *metadataParser) Append(c *Comment) {
@@ -2618,14 +2772,12 @@ func (b *metadataParser) Append(c *Comment) {
var yamlLineErrRegex = regexp.MustCompile(`^yaml:(?: unmarshal errors:[\n\s]*)? line ([[:digit:]]+):`)
func (b *metadataParser) Parse() (*Annotations, error) {
var raw rawAnnotation
func (b *metadataParser) Parse() (result *Annotations, err error) {
if len(bytes.TrimSpace(b.buf.Bytes())) == 0 {
return nil, errors.New("expected METADATA block, found whitespace")
}
var raw rawAnnotation
if err := yaml.Unmarshal(b.buf.Bytes(), &raw); err != nil {
var comment *Comment
match := yamlLineErrRegex.FindStringSubmatch(err.Error())
@@ -2648,13 +2800,14 @@ func (b *metadataParser) Parse() (*Annotations, error) {
return nil, augmentYamlError(err, b.comments)
}
var result Annotations
result.comments = b.comments
result.Scope = raw.Scope
result.Entrypoint = raw.Entrypoint
result.Title = raw.Title
result.Description = raw.Description
result.Organizations = raw.Organizations
result = &Annotations{
comments: b.comments,
Scope: raw.Scope,
Entrypoint: raw.Entrypoint,
Title: raw.Title,
Description: raw.Description,
Organizations: raw.Organizations,
}
for _, v := range raw.RelatedResources {
rr, err := parseRelatedResource(v)
@@ -2736,32 +2889,30 @@ func (b *metadataParser) Parse() (*Annotations, error) {
result.Authors = append(result.Authors, author)
}
result.Custom = make(map[string]any)
for k, v := range raw.Custom {
val, err := convertYAMLMapKeyTypes(v, nil)
if err != nil {
return nil, err
if raw.Custom != nil {
result.Custom = make(map[string]any, len(raw.Custom))
for k, v := range raw.Custom {
if result.Custom[k], err = convertYAMLMapKeyTypes(v, nil); err != nil {
return nil, err
}
}
result.Custom[k] = val
}
result.Location = b.loc
// recreate original text of entire metadata block for location text attribute
sb := strings.Builder{}
sb.WriteString("# METADATA\n")
original := bytes.TrimSuffix(b.buf.Bytes(), newlineBytes)
numLines := bytes.Count(original, newlineBytes) + 1
preAlloc := len("# METADATA\n") + len(original) + numLines*2 // '# ' prefix added per line
lines := bytes.Split(b.buf.Bytes(), []byte{'\n'})
result.Location.Text = append(make([]byte, 0, preAlloc), "# METADATA\n"...)
for _, line := range lines[:len(lines)-1] {
sb.WriteString("# ")
sb.Write(line)
sb.WriteByte('\n')
for line := range bytes.SplitAfterSeq(original, newlineBytes) {
result.Location.Text = append(result.Location.Text, "# "...)
result.Location.Text = append(result.Location.Text, line...)
}
result.Location.Text = []byte(strings.TrimSuffix(sb.String(), "\n"))
return &result, nil
return result, err
}
// augmentYamlError augments a YAML error with hints intended to help the user figure out the cause of an otherwise
@@ -2770,30 +2921,29 @@ func (b *metadataParser) Parse() (*Annotations, error) {
func augmentYamlError(err error, comments []*Comment) error {
// Adding hints for when key/value ':' separator isn't suffixed with a legal YAML space symbol
for _, comment := range comments {
txt := string(comment.Text)
parts := strings.Split(txt, ":")
if len(parts) > 1 {
parts = parts[1:]
var invalidSpaces []string
for partIndex, part := range parts {
if len(part) == 0 && partIndex == len(parts)-1 {
invalidSpaces = []string{}
break
}
if bytes.IndexByte(comment.Text, ':') == -1 {
continue
}
parts := bytes.Split(comment.Text, []byte{':'})[1:]
r, _ := utf8.DecodeRuneInString(part)
if r == ' ' || r == '\t' {
invalidSpaces = []string{}
break
}
var invalidSpaces []string
for partIndex, part := range parts {
if len(part) == 0 && partIndex == len(parts)-1 {
break
}
invalidSpaces = append(invalidSpaces, fmt.Sprintf("%+q", r))
}
if len(invalidSpaces) > 0 {
err = fmt.Errorf(
"%s\n Hint: on line %d, symbol(s) %v immediately following a key/value separator ':' is not a legal yaml space character",
err.Error(), comment.Location.Row, invalidSpaces)
r, _ := utf8.DecodeRune(part)
if r == ' ' || r == '\t' {
break
}
invalidSpaces = append(invalidSpaces, fmt.Sprintf("%+q", r))
}
if len(invalidSpaces) > 0 {
err = fmt.Errorf(
"%s\n Hint: on line %d, symbol(s) %v immediately following a"+
" key/value separator ':' is not a legal yaml space character",
err.Error(), comment.Location.Row, invalidSpaces)
}
}
return err
@@ -2911,7 +3061,7 @@ func parseAuthorString(s string) (*AuthorAnnotation, error) {
if len(trailing) >= len(emailPrefix)+len(emailSuffix) && strings.HasPrefix(trailing, emailPrefix) &&
strings.HasSuffix(trailing, emailSuffix) {
email = trailing[len(emailPrefix):]
email = email[0 : len(email)-len(emailSuffix)]
email = email[:len(email)-len(emailSuffix)]
namePartCount -= 1
}
@@ -2999,10 +3149,7 @@ func (p *Parser) futureImport(imp *Import, allowedFutureKeywords map[string]toke
return
}
kwds := make([]string, 0, len(allowedFutureKeywords))
for k := range allowedFutureKeywords {
kwds = append(kwds, k)
}
kwds := util.Keys(allowedFutureKeywords)
switch len(path) {
case 2: // all keywords imported, nothing to do
@@ -3052,10 +3199,7 @@ func (p *Parser) regoV1Import(imp *Import) {
}
// import all future keywords with the rego.v1 import
kwds := make([]string, 0, len(futureKeywordsV0))
for k := range futureKeywordsV0 {
kwds = append(kwds, k)
}
kwds := util.Keys(futureKeywordsV0)
p.s.s.SetRegoV1Compatible()
for _, kw := range kwds {
+6 -12
View File
@@ -11,7 +11,6 @@
package ast
import (
"bytes"
"errors"
"fmt"
"slices"
@@ -625,10 +624,9 @@ func ParseStatements(filename, input string) ([]Statement, []*Comment, error) {
// ParseStatementsWithOpts returns a slice of parsed statements. This is the
// default return value from the parser.
func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Statement, []*Comment, error) {
parser := NewParser().
WithFilename(filename).
WithReader(bytes.NewBufferString(input)).
WithReader(strings.NewReader(input)).
WithProcessAnnotation(popts.ProcessAnnotation).
WithFutureKeywords(popts.FutureKeywords...).
WithAllFutureKeywords(popts.AllFutureKeywords).
@@ -638,7 +636,6 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
withUnreleasedKeywords(popts.unreleasedKeywords)
stmts, comments, errs := parser.Parse()
if len(errs) > 0 {
return nil, nil, errs
}
@@ -647,7 +644,6 @@ func ParseStatementsWithOpts(filename, input string, popts ParserOptions) ([]Sta
}
func parseModule(filename string, stmts []Statement, comments []*Comment, regoCompatibilityMode RegoVersion) (*Module, error) {
if len(stmts) == 0 {
return nil, NewError(ParseErr, &Location{File: filename}, "empty module")
}
@@ -662,23 +658,21 @@ func parseModule(filename string, stmts []Statement, comments []*Comment, regoCo
mod := &Module{
Package: pkg,
stmts: stmts,
// The comments slice only holds comments that were not their own statements.
Comments: comments,
stmts: stmts,
}
// The comments slice only holds comments that were not their own statements.
mod.Comments = append(mod.Comments, comments...)
mod.regoVersion = regoCompatibilityMode
if regoCompatibilityMode == RegoUndefined {
mod.regoVersion = DefaultRegoVersion
} else {
mod.regoVersion = regoCompatibilityMode
}
for i, stmt := range stmts[1:] {
switch stmt := stmt.(type) {
case *Import:
mod.Imports = append(mod.Imports, stmt)
if mod.regoVersion == RegoV0 && Compare(stmt.Path.Value, RegoV1CompatibleRef) == 0 {
if mod.regoVersion == RegoV0 && RegoV1CompatibleRef.Equal(stmt.Path.Value) {
mod.regoVersion = RegoV0CompatV1
}
case *Rule:
+14
View File
@@ -4,6 +4,7 @@
package ast
import (
"encoding"
"strings"
"sync"
)
@@ -83,3 +84,16 @@ func BuiltinNameFromRef(ref Ref) (string, bool) {
return "", false
}
func AppendDelimeted[T encoding.TextAppender](buf []byte, appenders []T, delim string) ([]byte, error) {
for i, item := range appenders {
if i > 0 {
buf = append(buf, delim...)
}
var err error
if buf, err = item.AppendText(buf); err != nil {
return nil, err
}
}
return buf, nil
}
+84 -195
View File
@@ -86,7 +86,11 @@ var ReservedVars = NewVarSet(
)
// Wildcard represents the wildcard variable as defined in the language.
var Wildcard = &Term{Value: Var("_")}
var (
WildcardString = "_"
WildcardValue Value = Var(WildcardString)
Wildcard = &Term{Value: WildcardValue}
)
// WildcardPrefix is the special character that all wildcard variables are
// prefixed with when the statement they are contained in is parsed.
@@ -367,40 +371,8 @@ func (mod *Module) Equal(other *Module) bool {
}
func (mod *Module) String() string {
byNode := map[Node][]*Annotations{}
for _, a := range mod.Annotations {
byNode[a.node] = append(byNode[a.node], a)
}
appendAnnotationStrings := func(buf []string, node Node) []string {
if as, ok := byNode[node]; ok {
for i := range as {
buf = append(buf, "# METADATA")
buf = append(buf, "# "+as[i].String())
}
}
return buf
}
buf := []string{}
buf = appendAnnotationStrings(buf, mod.Package)
buf = append(buf, mod.Package.String())
if len(mod.Imports) > 0 {
buf = append(buf, "")
for _, imp := range mod.Imports {
buf = appendAnnotationStrings(buf, imp)
buf = append(buf, imp.String())
}
}
if len(mod.Rules) > 0 {
buf = append(buf, "")
for _, rule := range mod.Rules {
buf = appendAnnotationStrings(buf, rule)
buf = append(buf, rule.stringWithOpts(toStringOpts{regoVersion: mod.regoVersion}))
}
}
return strings.Join(buf, "\n")
buf, _ := mod.AppendText(make([]byte, 0, mod.StringLength()))
return util.ByteSliceToString(buf)
}
// RuleSet returns a RuleSet containing named rules in the mod.
@@ -469,7 +441,8 @@ func (c *Comment) SetLoc(loc *Location) {
}
func (c *Comment) String() string {
return "#" + string(c.Text)
buf, _ := c.AppendText(make([]byte, 0, c.StringLength()))
return util.ByteSliceToString(buf)
}
// Copy returns a deep copy of c.
@@ -519,16 +492,8 @@ func (pkg *Package) SetLoc(loc *Location) {
}
func (pkg *Package) String() string {
if pkg == nil {
return "<illegal nil package>"
} else if len(pkg.Path) <= 1 {
return fmt.Sprintf("package <illegal path %q>", pkg.Path)
}
// Omit head as all packages have the DefaultRootDocument prepended at parse time.
path := make(Ref, len(pkg.Path)-1)
path[0] = VarTerm(string(pkg.Path[1].Value.(String)))
copy(path[1:], pkg.Path[2:])
return fmt.Sprintf("package %v", path)
buf, _ := pkg.AppendText(make([]byte, 0, pkg.StringLength()))
return util.ByteSliceToString(buf)
}
func (pkg *Package) MarshalJSON() ([]byte, error) {
@@ -615,7 +580,7 @@ func (imp *Import) SetLoc(loc *Location) {
// document. This is the alias if defined otherwise the last element in the
// path.
func (imp *Import) Name() Var {
if len(imp.Alias) != 0 {
if imp.Alias != "" {
return imp.Alias
}
switch v := imp.Path.Value.(type) {
@@ -631,11 +596,8 @@ func (imp *Import) Name() Var {
}
func (imp *Import) String() string {
buf := []string{"import", imp.Path.String()}
if len(imp.Alias) > 0 {
buf = append(buf, "as", imp.Alias.String())
}
return strings.Join(buf, " ")
buf, _ := imp.AppendText(make([]byte, 0, imp.StringLength()))
return util.ByteSliceToString(buf)
}
func (imp *Import) MarshalJSON() ([]byte, error) {
@@ -726,6 +688,7 @@ func (rule *Rule) SetLoc(loc *Location) {
// Path returns a ref referring to the document produced by this rule. If rule
// is not contained in a module, this function panics.
//
// Deprecated: Poor handling of ref rules. Use `(*Rule).Ref()` instead.
func (rule *Rule) Path() Ref {
if rule.Module == nil {
@@ -745,11 +708,12 @@ func (rule *Rule) Ref() Ref {
}
func (rule *Rule) String() string {
regoVersion := DefaultRegoVersion
opts := toStringOpts{}
if rule.Module != nil {
regoVersion = rule.Module.RegoVersion()
opts.regoVersion = rule.Module.RegoVersion()
}
return rule.stringWithOpts(toStringOpts{regoVersion: regoVersion})
buf, _ := rule.appendWithOpts(opts, make([]byte, 0, rule.stringLengthWithOpts(opts)))
return util.ByteSliceToString(buf)
}
type toStringOpts struct {
@@ -763,80 +727,46 @@ func (o toStringOpts) RegoVersion() RegoVersion {
return o.regoVersion
}
func (rule *Rule) stringWithOpts(opts toStringOpts) string {
buf := []string{}
if rule.Default {
buf = append(buf, "default")
}
buf = append(buf, rule.Head.stringWithOpts(opts))
if !rule.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, "if")
}
buf = append(buf, "{", rule.Body.String(), "}")
}
if rule.Else != nil {
buf = append(buf, rule.Else.elseString(opts))
}
return strings.Join(buf, " ")
}
func (rule *Rule) isFunction() bool {
return len(rule.Head.Args) > 0
}
// ruleJSON is used for JSON serialization of Rule to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type ruleJSON struct {
Annotations []*Annotations `json:"annotations,omitempty"`
Body Body `json:"body"`
Default bool `json:"default,omitempty"`
Else *Rule `json:"else,omitempty"`
Head *Head `json:"head"`
Location *Location `json:"location,omitempty"`
}
func (rule *Rule) MarshalJSON() ([]byte, error) {
data := map[string]any{
"head": rule.Head,
"body": rule.Body,
data := ruleJSON{
Head: rule.Head,
Body: rule.Body,
}
if rule.Default {
data["default"] = true
data.Default = true
}
if rule.Else != nil {
data["else"] = rule.Else
data.Else = rule.Else
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
if rule.Location != nil {
data["location"] = rule.Location
}
data.Location = rule.Location
}
if len(rule.Annotations) != 0 {
data["annotations"] = rule.Annotations
data.Annotations = rule.Annotations
}
return json.Marshal(data)
}
func (rule *Rule) elseString(opts toStringOpts) string {
var buf []string
buf = append(buf, "else")
value := rule.Head.Value
if value != nil {
buf = append(buf, "=", value.String())
}
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, "if")
}
buf = append(buf, "{", rule.Body.String(), "}")
if rule.Else != nil {
buf = append(buf, rule.Else.elseString(opts))
}
return strings.Join(buf, " ")
}
// NewHead returns a new Head object. If args are provided, the first will be
// used for the key and the second will be used for the value.
func NewHead(name Var, args ...*Term) *Head {
@@ -981,6 +911,7 @@ func (head *Head) Copy() *Head {
cpy.Key = head.Key.Copy()
cpy.Value = head.Value.Copy()
cpy.keywords = nil
cpy.Assign = head.Assign
return &cpy
}
@@ -994,37 +925,8 @@ func (head *Head) String() string {
}
func (head *Head) stringWithOpts(opts toStringOpts) string {
buf := strings.Builder{}
buf.WriteString(head.Ref().String())
containsAdded := false
switch {
case len(head.Args) != 0:
buf.WriteString(head.Args.String())
case len(head.Reference) == 1 && head.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
buf.WriteRune('[')
buf.WriteString(head.Key.String())
buf.WriteRune(']')
default:
containsAdded = true
buf.WriteString(" contains ")
buf.WriteString(head.Key.String())
}
}
if head.Value != nil {
if head.Assign {
buf.WriteString(" := ")
} else {
buf.WriteString(" = ")
}
buf.WriteString(head.Value.String())
} else if !containsAdded && head.Name == "" && head.Key != nil {
buf.WriteString(" contains ")
buf.WriteString(head.Key.String())
}
return buf.String()
buf, _ := head.appendWithOpts(opts, make([]byte, 0, head.stringLengthWithOpts(opts)))
return util.ByteSliceToString(buf)
}
func (head *Head) MarshalJSON() ([]byte, error) {
@@ -1087,7 +989,7 @@ func (head *Head) HasDynamicRef() bool {
// Copy returns a deep copy of a.
func (a Args) Copy() Args {
cpy := Args{}
cpy := make(Args, 0, len(a))
for _, t := range a {
cpy = append(cpy, t.Copy())
}
@@ -1095,11 +997,8 @@ func (a Args) Copy() Args {
}
func (a Args) String() string {
buf := make([]string, 0, len(a))
for _, t := range a {
buf = append(buf, t.String())
}
return "(" + strings.Join(buf, ", ") + ")"
buf, _ := a.AppendText(make([]byte, 0, a.StringLength()))
return util.ByteSliceToString(buf)
}
// Loc returns the Location of a.
@@ -1232,11 +1131,12 @@ func (body Body) SetLoc(loc *Location) {
}
func (body Body) String() string {
buf := make([]string, 0, len(body))
for _, v := range body {
buf = append(buf, v.String())
}
return strings.Join(buf, "; ")
buf, _ := body.AppendText(make([]byte, 0, body.StringLength()))
return util.ByteSliceToString(buf)
}
func (body Body) AppendText(buf []byte) ([]byte, error) {
return AppendDelimeted(buf, body, "; ")
}
// Vars returns a VarSet containing variables in body. The params can be set to
@@ -1547,50 +1447,41 @@ func (expr *Expr) SetLoc(loc *Location) {
}
func (expr *Expr) String() string {
buf := make([]string, 0, 2+len(expr.With))
if expr.Negated {
buf = append(buf, "not")
}
switch t := expr.Terms.(type) {
case []*Term:
if expr.IsEquality() && validEqAssignArgCount(expr) {
buf = append(buf, fmt.Sprintf("%v %v %v", t[1], Equality.Infix, t[2]))
} else {
buf = append(buf, Call(t).String())
}
case fmt.Stringer:
buf = append(buf, t.String())
}
buf, _ := expr.AppendText(make([]byte, 0, expr.StringLength()))
return util.ByteSliceToString(buf)
}
for i := range expr.With {
buf = append(buf, expr.With[i].String())
}
return strings.Join(buf, " ")
// exprJSON is used for JSON serialization of Expr to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type exprJSON struct {
Generated bool `json:"generated,omitempty"`
Index int `json:"index"`
Location *Location `json:"location,omitempty"`
Negated bool `json:"negated,omitempty"`
Terms any `json:"terms"`
With []*With `json:"with,omitempty"`
}
func (expr *Expr) MarshalJSON() ([]byte, error) {
data := map[string]any{
"terms": expr.Terms,
"index": expr.Index,
data := exprJSON{
Index: expr.Index,
Terms: expr.Terms,
}
if len(expr.With) > 0 {
data["with"] = expr.With
data.With = expr.With
}
if expr.Generated {
data["generated"] = true
data.Generated = true
}
if expr.Negated {
data["negated"] = true
data.Negated = true
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
if expr.Location != nil {
data["location"] = expr.Location
}
data.Location = expr.Location
}
return json.Marshal(data)
@@ -1660,17 +1551,8 @@ func visitCogeneratedExprs(expr *Expr, f func(*Expr) bool) {
}
func (d *SomeDecl) String() string {
if call, ok := d.Symbols[0].Value.(Call); ok {
if len(call) == 4 {
return "some " + call[1].String() + ", " + call[2].String() + " in " + call[3].String()
}
return "some " + call[1].String() + " in " + call[2].String()
}
buf := make([]string, len(d.Symbols))
for i := range buf {
buf[i] = d.Symbols[i].String()
}
return "some " + strings.Join(buf, ", ")
buf, _ := d.AppendText(make([]byte, 0, d.StringLength()))
return util.ByteSliceToString(buf)
}
// SetLoc sets the Location on d.
@@ -1789,7 +1671,8 @@ func (q *Every) MarshalJSON() ([]byte, error) {
}
func (w *With) String() string {
return "with " + w.Target.String() + " as " + w.Value.String()
buf, _ := w.AppendText(make([]byte, 0, w.StringLength()))
return util.ByteSliceToString(buf)
}
// Equal returns true if this With is equals the other With.
@@ -1846,16 +1729,22 @@ func (w *With) SetLoc(loc *Location) {
w.Location = loc
}
// withJSON is used for JSON serialization of With to avoid map allocation overhead.
// Field order is alphabetical to match previous map-based output.
type withJSON struct {
Location *Location `json:"location,omitempty"`
Target *Term `json:"target"`
Value *Term `json:"value"`
}
func (w *With) MarshalJSON() ([]byte, error) {
data := map[string]any{
"target": w.Target,
"value": w.Value,
data := withJSON{
Target: w.Target,
Value: w.Value,
}
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
if w.Location != nil {
data["location"] = w.Location
}
data.Location = w.Location
}
return json.Marshal(data)
+324
View File
@@ -0,0 +1,324 @@
package ast
import (
"encoding"
"fmt"
)
func (m *Module) AppendText(buf []byte) ([]byte, error) {
if m == nil {
return append(buf, "<nil module>"...), nil
}
var err error
// NOTE(anderseknert): this DOES allocate still, and while that's unfortunate,
// we'll be better off dealing with that when we have v2 JSON in the stdlib than
// doing manual JSON marshalling (and string length calculations) here.
for _, annotations := range m.Annotations {
// rule annotations are attached to rules, so only check for package scoped ones here
if annotations.Scope == "package" || annotations.Scope == "subpackages" {
buf = append(buf, "# METADATA\n# "...)
buf = append(buf, annotations.String()...)
buf = append(buf, '\n')
}
}
if buf, err = m.Package.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, '\n')
if len(m.Imports) > 0 {
for _, imp := range m.Imports {
buf = append(buf, '\n')
if buf, err = imp.AppendText(buf); err != nil {
return nil, err
}
}
buf = append(buf, '\n')
}
if len(m.Rules) > 0 {
for _, rule := range m.Rules {
buf = append(buf, '\n')
if buf, err = rule.appendWithOpts(toStringOpts{regoVersion: m.regoVersion}, buf); err != nil {
return nil, err
}
}
}
return buf, nil
}
func (pkg *Package) AppendText(buf []byte) ([]byte, error) {
var err error
if pkg == nil {
return append(buf, "<illegal nil package>"...), nil
}
if len(pkg.Path) <= 1 {
buf = append(buf, "package <illegal path \""...)
if buf, err = pkg.Path.AppendText(buf); err != nil {
return nil, err
}
return append(buf, "\">"...), nil
}
buf = append(buf, "package "...)
path := pkg.Path[1:] // omit "data"
return path.AppendText(buf)
}
func (imp *Import) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "import "...)
var err error
if buf, err = imp.Path.AppendText(buf); err != nil {
return nil, err
}
if imp.Alias != "" {
buf = append(buf, ' ', 'a', 's', ' ')
buf = append(buf, imp.Alias...)
}
return buf, nil
}
func (r *Rule) AppendText(buf []byte) ([]byte, error) {
regoVersion := DefaultRegoVersion
if r.Module != nil {
regoVersion = r.Module.RegoVersion()
}
return r.appendWithOpts(toStringOpts{regoVersion: regoVersion}, buf)
}
func (r *Rule) appendWithOpts(opts toStringOpts, buf []byte) ([]byte, error) {
// See note in [Module.AppendText] regarding annotations.
for _, annotations := range r.Annotations {
buf = append(buf, "# METADATA\n# "...)
buf = append(buf, annotations.String()...)
buf = append(buf, '\n')
}
if r.Default {
buf = append(buf, "default "...)
}
var err error
if buf, err = r.Head.appendWithOpts(opts, buf); err != nil {
return nil, err
}
if !r.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
buf = append(buf, " if { "...)
default:
buf = append(buf, " { "...)
}
if buf, err = r.Body.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " }"...)
}
if r.Else != nil {
if buf, err = r.Else.appendElse(opts, buf); err != nil {
return nil, err
}
}
return buf, nil
}
func (r *Rule) appendElse(opts toStringOpts, buf []byte) ([]byte, error) {
buf = append(buf, " else "...)
var err error
if r.Head.Value != nil {
buf = append(buf, "= "...)
if buf, err = r.Head.Value.AppendText(buf); err != nil {
return nil, err
}
}
if v := opts.RegoVersion(); v == RegoV1 || v == RegoV0CompatV1 {
buf = append(buf, " if { "...)
} else {
buf = append(buf, " { "...)
}
if buf, err = r.Body.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " }"...)
if r.Else != nil {
if buf, err = r.Else.appendElse(opts, buf); err != nil {
return nil, err
}
}
return buf, nil
}
func (h *Head) AppendText(buf []byte) ([]byte, error) {
return h.appendWithOpts(toStringOpts{}, buf)
}
func (h *Head) appendWithOpts(opts toStringOpts, buf []byte) ([]byte, error) {
var err error
if h.Reference == nil {
buf = append(buf, h.Name...)
} else {
if buf, err = h.Reference.AppendText(buf); err != nil {
return nil, err
}
}
containsAdded := false
switch {
case len(h.Args) != 0:
if buf, err = h.Args.AppendText(buf); err != nil {
return nil, err
}
case len(h.Reference) == 1 && h.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
buf = append(buf, '[')
if buf, err = h.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ']')
default:
if buf, err = h.Key.AppendText(append(buf, " contains "...)); err != nil {
return nil, err
}
containsAdded = true
}
}
if h.Value != nil {
if h.Assign {
buf = append(buf, " := "...)
} else {
buf = append(buf, " = "...)
}
if buf, err = h.Value.AppendText(buf); err != nil {
return nil, err
}
} else if !containsAdded && h.Name == "" && h.Key != nil {
if buf, err = h.Key.AppendText(append(buf, " contains "...)); err != nil {
return nil, err
}
}
return buf, nil
}
func (a Args) AppendText(buf []byte) ([]byte, error) {
var err error
buf = append(buf, '(')
if buf, err = AppendDelimeted(buf, a, ", "); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
func (expr *Expr) AppendText(buf []byte) ([]byte, error) {
if expr.Negated {
buf = append(buf, "not "...)
}
var err error
switch t := expr.Terms.(type) {
case []*Term:
if expr.IsEquality() && validEqAssignArgCount(expr) {
if buf, err = t[1].AppendText(buf); err != nil {
return nil, err
}
buf = append(append(append(buf, ' '), Equality.Infix...), ' ')
if buf, err = t[2].AppendText(buf); err != nil {
return nil, err
}
} else if buf, err = Call(t).AppendText(buf); err != nil {
return nil, err
}
case encoding.TextAppender:
if buf, err = t.AppendText(buf); err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unsupported expr terms type: %T", expr.Terms)
}
if len(expr.With) > 0 {
buf = append(buf, ' ')
}
return AppendDelimeted(buf, expr.With, " ")
}
func (w *With) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "with "...)
var err error
if buf, err = w.Target.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, " as "...)
if buf, err = w.Value.AppendText(buf); err != nil {
return nil, err
}
return buf, nil
}
func (w *Every) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "every "...)
var err error
if w.Key != nil {
if buf, err = w.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ", "...)
}
if buf, err = w.Value.AppendText(buf); err == nil {
buf = append(buf, " in "...)
if buf, err = w.Domain.AppendText(buf); err == nil {
buf = append(buf, " { "...)
if buf, err = w.Body.AppendText(buf); err == nil {
buf = append(buf, " }"...)
}
}
}
return buf, err
}
func (d *SomeDecl) AppendText(buf []byte) ([]byte, error) {
var err error
buf = append(buf, "some "...)
if call, ok := d.Symbols[0].Value.(Call); ok {
if buf, err = call[1].AppendText(buf); err != nil {
return nil, err
}
if len(call) == 3 {
buf = append(buf, " in "...)
} else {
buf = append(buf, ", "...)
}
if buf, err = call[2].AppendText(buf); err != nil {
return nil, err
}
if len(call) == 4 {
buf = append(buf, " in "...)
if buf, err = call[3].AppendText(buf); err != nil {
return nil, err
}
}
return buf, nil
}
buf, err = AppendDelimeted(buf, d.Symbols, ", ")
return buf, err
}
func (c *Comment) AppendText(buf []byte) ([]byte, error) {
return append(append(buf, '#'), c.Text...), nil
}
+12 -15
View File
@@ -27,13 +27,12 @@ func checkRootDocumentOverrides(node any) Errors {
errors := Errors{}
WalkRules(node, func(rule *Rule) bool {
var name string
name := rule.Head.Name
if len(rule.Head.Reference) > 0 {
name = rule.Head.Reference[0].Value.(Var).String()
} else {
name = rule.Head.Name.String()
name = rule.Head.Reference[0].Value.(Var)
}
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
if ReservedVars.Contains(name) {
errors = append(errors, NewError(CompileErr, rule.Location, "rules must not shadow %v (use a different rule name)", name))
}
@@ -52,8 +51,8 @@ func checkRootDocumentOverrides(node any) Errors {
if expr.IsAssignment() {
// assign() can be called directly, so we need to assert its given first operand exists before checking its name.
if nameOp := expr.Operand(0); nameOp != nil {
name := nameOp.String()
if RootDocumentRefs.Contains(RefTerm(VarTerm(name))) {
name := Var(nameOp.String())
if ReservedVars.Contains(name) {
errors = append(errors, NewError(CompileErr, expr.Location, "variables must not shadow %v (use a different variable name)", name))
}
}
@@ -65,26 +64,24 @@ func checkRootDocumentOverrides(node any) Errors {
}
func walkCalls(node any, f func(any) bool) {
vis := &GenericVisitor{func(x any) bool {
switch x := x.(type) {
vis := NewGenericVisitor(func(x any) bool {
switch y := x.(type) {
case Call:
return f(x)
case *Expr:
if x.IsCall() {
if y.IsCall() {
return f(x)
}
case *Head:
// GenericVisitor doesn't walk the rule head ref
walkCalls(x.Reference, f)
walkCalls(y.Reference, f)
}
return false
}}
})
vis.Walk(node)
}
func checkDeprecatedBuiltins(deprecatedBuiltinsMap map[string]struct{}, node any) Errors {
errs := make(Errors, 0)
func checkDeprecatedBuiltins(deprecatedBuiltinsMap map[string]struct{}, node any) (errs Errors) {
walkCalls(node, func(x any) bool {
var operator string
var loc *Location
+15
View File
@@ -0,0 +1,15 @@
// 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
// CountFunc counts the number of items in a slice S that satisfy predicate function f.
func CountFunc[T any, S ~[]T](items S, f func(T) bool) (n int) {
for i := range items {
if f(items[i]) {
n++
}
}
return n
}
+351
View File
@@ -0,0 +1,351 @@
package ast
import (
"fmt"
"unicode/utf8"
"github.com/open-policy-agent/opa/v1/util"
)
// StringLengther is an interface for types that can report their string length without
// actually constructing the string. This is useful for pre-allocating buffers, like those
// used in AppendText, strings.Builder, bytes.Buffer, etc.
type StringLengther interface {
StringLength() int
}
// TermSliceStringLength returns the total string length of the given terms, as reported
// by the [StringLengther.StringLength] method implementation of each term's [Value]. The
// delimLen value will be added between each term's length to account for a delimiter, or
// no delimiter if delimLen is 0.
// Implementation note: this function is optimized for inlining, and just meets the threshold
// for that. Don't change without making sure that's still the case.
func TermSliceStringLength(terms []*Term, delimLen int) (n int) {
for i := range terms {
n += terms[i].StringLength() + delimLen
}
return max(n-delimLen, 0)
}
func (t *Term) StringLength() int {
if sl, ok := t.Value.(StringLengther); ok {
return sl.StringLength()
}
panic("expected all ast.Value types to implement StringLenghter interface, got: " + ValueName(t.Value))
}
func (s String) StringLength() int {
n := 2 // surrounding quotes
bs := util.StringToByteSlice(s)
for i := 0; i < len(bs); {
r, size := utf8.DecodeRune(bs[i:])
switch r {
case '\\', '"':
n += 2 // escaped backslash or quote
case '\b', '\f', '\n', '\r', '\t':
n += 2 // escaped control characters
default:
if r < 0x20 {
n += 6 // unicode escape for other control characters
} else {
n += size // normal rune
}
}
i += size
}
return n
}
func (n Number) StringLength() int {
return len(n)
}
func (b Boolean) StringLength() int {
if b {
return 4
}
return 5
}
func (Null) StringLength() int {
return 4
}
func (s *set) StringLength() int {
if s.Len() == 0 {
return 5 // set()
}
// surrounding {} + ", " for every element - 1
return TermSliceStringLength(s.Slice(), 2) + 2
}
func (a *Array) StringLength() int {
if a.Len() == 0 {
return 2 // []
}
// surrounding brackets + ", " for every element - 1
return TermSliceStringLength(a.elems, 2) + 2
}
func (o *object) StringLength() (n int) {
if o.Len() == 0 {
return 2 // {}
}
// ": " for every item + ", " for every item - 1
o.Foreach(func(key, value *Term) {
n += key.StringLength() + 4 + value.StringLength() // ": " and ", "
})
return n // surrounding {} but also minus last ", "
}
func (l *lazyObj) StringLength() int {
return l.force().(*object).StringLength()
}
func (ts *TemplateString) StringLength() (n int) {
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
n += 2 + x.StringLength() // for {}
case *Term:
if s, ok := x.Value.(String); ok {
n += len(s) + countUnescapedLeftCurly(string(s))
} else {
n += x.StringLength()
}
default:
n += 9 // <invalid>
}
}
return n + 3 // $"" or $``
}
func (c Call) StringLength() int {
return c[0].StringLength() + 2 + TermSliceStringLength(c[1:], 2)
}
func (r Ref) StringLength() (n int) {
rlen := len(r)
if rlen == 0 {
return 0
}
if s, ok := r[0].Value.(String); ok {
n = len(s) // first term should never be quoted
} else {
n = r[0].StringLength()
}
if rlen == 1 {
return n
}
for _, p := range r[1:] {
switch v := p.Value.(type) {
case String:
str := string(v)
if IsVarCompatibleString(str) && !IsKeyword(str) {
n += 1 + len(str) // dot + name
} else {
n += 2 + p.StringLength() // brackets
}
default:
n += 2 + p.StringLength() // brackets
}
}
return n
}
func (v Var) StringLength() int {
if v.IsWildcard() {
return 1
}
return len(v)
}
func (s *SetComprehension) StringLength() int {
return s.Term.StringLength() + s.Body.StringLength() + 5 // {} and " | "
}
func (a *ArrayComprehension) StringLength() int {
return a.Term.StringLength() + a.Body.StringLength() + 5 // [] and " | "
}
func (o *ObjectComprehension) StringLength() (n int) {
n += o.Key.StringLength()
n += o.Value.StringLength()
n += o.Body.StringLength()
return n + 7 // "{}"", " | ", and ": "
}
func (m *Module) StringLength() (n int) {
if m.Package != nil {
n += m.Package.StringLength() + 2 // newlines
}
if len(m.Imports) > 0 {
for _, imp := range m.Imports {
n += imp.StringLength() + 1 // newline
}
}
if len(m.Rules) > 0 {
for _, rule := range m.Rules {
n += rule.stringLengthWithOpts(toStringOpts{regoVersion: m.regoVersion}) + 1 // newline
}
}
return n
}
func (p *Package) StringLength() int {
if p == nil {
return 21 // <illegal nil package>
}
if len(p.Path) <= 1 {
return 25 + p.Path.StringLength() // // package <illegal path " ... ">
}
return 8 + p.Path[1:].StringLength() // "package ..."
}
func (i *Import) StringLength() (n int) {
n = 7 + i.Path.StringLength() // "import " and path
if i.Alias != "" {
n += 4 + i.Alias.StringLength() // " as " and alias
}
return n
}
func (r *Rule) StringLength() int {
return r.stringLengthWithOpts(toStringOpts{})
}
func (r *Rule) stringLengthWithOpts(opts toStringOpts) int {
n := 0
if r.Default {
n += 8 // "default "
}
n += r.Head.stringLengthWithOpts(opts)
if !r.Default {
switch opts.RegoVersion() {
case RegoV1, RegoV0CompatV1:
n += 6 // " if { "
default:
n += 3 // " { "
}
n += r.Body.StringLength() + 2 // body and closing " }"
}
if r.Else != nil {
n += r.Else.stringLengthWithOpts(opts)
}
return n
}
func (h *Head) StringLength() int {
return h.stringLengthWithOpts(toStringOpts{})
}
func (h *Head) stringLengthWithOpts(opts toStringOpts) int {
n := h.Reference.StringLength()
containsAdded := false
switch {
case len(h.Args) != 0:
n += h.Args.StringLength()
case len(h.Reference) == 1 && h.Key != nil:
switch opts.RegoVersion() {
case RegoV0:
n += 2 + h.Key.StringLength() // for []
default:
n += 10 + h.Key.StringLength() // " contains "
containsAdded = true
}
}
if h.Value != nil {
if h.Assign {
n += 4 // " := "
} else {
n += 3 // " = "
}
n += h.Value.StringLength()
} else if !containsAdded && h.Name == "" && h.Key != nil {
n += 10 + h.Key.StringLength() // " contains "
}
return n
}
func (a Args) StringLength() (n int) {
n = 2 // ()
for _, t := range a {
n += t.StringLength() + 2 // ", "
}
return n - 2 // minus last ", "
}
func (b Body) StringLength() (n int) {
for _, expr := range b {
n += expr.StringLength() + 2 // "; "
}
return max(n-2, 0) // minus last "; " (if `n` isn't 0)
}
func (e *Expr) StringLength() (n int) {
if e.Negated {
n += 4 // "not "
}
switch terms := e.Terms.(type) {
case []*Term:
if e.IsEquality() && validEqAssignArgCount(e) {
n += terms[1].StringLength() + len(Equality.Infix) + terms[2].StringLength() + 2 // spaces around =
} else {
n += Call(terms).StringLength()
}
case StringLengther:
n += terms.StringLength()
default:
panic(fmt.Sprintf("string length estimation not implemented for type: %T", e.Terms))
}
for _, w := range e.With {
n += w.StringLength() + 1 // space before with
}
return n
}
func (w *With) StringLength() int {
return w.Target.StringLength() + w.Value.StringLength() + 9 // "with " and " as "
}
func (e *Every) StringLength() int {
n := 6 // "every "
if e.Key != nil {
n += e.Key.StringLength() + 2 // ", "
}
n += e.Value.StringLength() + 4 // " in "
n += e.Domain.StringLength() + 3 // " { "
n += e.Body.StringLength() + 2 // " }"
return n
}
func (s *SomeDecl) StringLength() int {
n := 5 // "some "
if call, ok := s.Symbols[0].Value.(Call); ok {
n += 4 // " in "
n += call[1].StringLength()
if len(call) == 4 {
n += 2 // ", "
}
n += call[2].StringLength()
if len(call) == 4 {
n += call[3].StringLength()
}
return n
}
return n + TermSliceStringLength(s.Symbols, 2)
}
func (c *Comment) StringLength() int {
return 1 + len(c.Text) // '#' + text
}
+2
View File
@@ -48,6 +48,8 @@ func ValueName(x Value) string {
return "objectcomprehension"
case *SetComprehension:
return "setcomprehension"
case *TemplateString:
return "templatestring"
}
return TypeName(x)
+16 -70
View File
@@ -1,53 +1,31 @@
package ast
import (
"strings"
"bytes"
"sync"
"github.com/open-policy-agent/opa/v1/util"
)
type termPtrPool struct {
pool sync.Pool
}
var (
TermPtrPool = util.NewSyncPool[Term]()
BytesReaderPool = util.NewSyncPool[bytes.Reader]()
IndexResultPool = util.NewSyncPool[IndexResult]()
type stringBuilderPool struct {
pool sync.Pool
}
type indexResultPool struct {
pool sync.Pool
}
// Needs custom pool because of custom Put logic.
varVisitorPool = &vvPool{
pool: sync.Pool{
New: func() any {
return NewVarVisitor()
},
},
}
)
type vvPool struct {
pool sync.Pool
}
func (p *termPtrPool) Get() *Term {
return p.pool.Get().(*Term)
}
func (p *termPtrPool) Put(t *Term) {
p.pool.Put(t)
}
func (p *stringBuilderPool) Get() *strings.Builder {
return p.pool.Get().(*strings.Builder)
}
func (p *stringBuilderPool) Put(sb *strings.Builder) {
sb.Reset()
p.pool.Put(sb)
}
func (p *indexResultPool) Get() *IndexResult {
return p.pool.Get().(*IndexResult)
}
func (p *indexResultPool) Put(x *IndexResult) {
if x != nil {
p.pool.Put(x)
}
}
func (p *vvPool) Get() *VarVisitor {
return p.pool.Get().(*VarVisitor)
}
@@ -58,35 +36,3 @@ func (p *vvPool) Put(vv *VarVisitor) {
p.pool.Put(vv)
}
}
var TermPtrPool = &termPtrPool{
pool: sync.Pool{
New: func() any {
return &Term{}
},
},
}
var sbPool = &stringBuilderPool{
pool: sync.Pool{
New: func() any {
return &strings.Builder{}
},
},
}
var varVisitorPool = &vvPool{
pool: sync.Pool{
New: func() any {
return NewVarVisitor()
},
},
}
var IndexResultPool = &indexResultPool{
pool: sync.Pool{
New: func() any {
return &IndexResult{}
},
},
}
+360 -183
View File
@@ -2,7 +2,6 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// nolint: deadcode // Public API.
package ast
import (
@@ -13,7 +12,6 @@ import (
"io"
"math"
"net/url"
"regexp"
"slices"
"strconv"
"strings"
@@ -26,7 +24,22 @@ import (
"github.com/open-policy-agent/opa/v1/util"
)
var errFindNotFound = errors.New("find: not found")
// maxBindingsEstimate is the cap for binding count estimates in comprehensions.
// This value aligns with maxLinearScan in topdown/bindings.go.
const maxBindingsEstimate = 16
// EstimateBodyBindingCount returns an estimate of the number of bindings needed
// for evaluating a comprehension body. It uses the body length as a heuristic,
// capped at maxBindingsEstimate.
func EstimateBodyBindingCount(body Body) (estimate int) {
return min(len(body), maxBindingsEstimate)
}
var (
NullValue Value = Null{}
errFindNotFound = errors.New("find: not found")
)
// Location records a position in source code.
type Location = location.Location
@@ -44,12 +57,15 @@ func NewLocation(text []byte, file string, row int, col int) *Location {
// - Variables, References
// - Array, Set, and Object Comprehensions
// - Calls
// - Template Strings
type Value interface {
Compare(other Value) int // Compare returns <0, 0, or >0 if this Value is less than, equal to, or greater than other, respectively.
Find(path Ref) (Value, error) // Find returns value referred to by path or an error if path is not found.
Hash() int // Returns hash code of the value.
IsGround() bool // IsGround returns true if this value is not a variable or contains no variables.
String() string // String returns a human readable string representation of the value.
StringLengther // All Values must be able to report their string length during optimization.
}
// InterfaceToValue converts a native Go value x to a Value.
@@ -352,6 +368,8 @@ func (term *Term) Copy() *Term {
cpy.Value = v.Copy()
case *SetComprehension:
cpy.Value = v.Copy()
case *TemplateString:
cpy.Value = v.Copy()
case Call:
cpy.Value = v.Copy()
}
@@ -405,19 +423,24 @@ func (term *Term) IsGround() bool {
return term.Value.IsGround()
}
// termJSON is used to serialize Term to JSON without map allocation.
type termJSON struct {
Location *Location `json:"location,omitempty"`
Type string `json:"type"`
Value Value `json:"value"`
}
// MarshalJSON returns the JSON encoding of the term.
//
// Specialized marshalling logic is required to include a type hint for Value.
func (term *Term) MarshalJSON() ([]byte, error) {
d := map[string]any{
"type": ValueName(term.Value),
"value": term.Value,
d := termJSON{
Type: ValueName(term.Value),
Value: term.Value,
}
jsonOptions := astJSON.GetOptions().MarshalOptions
if jsonOptions.IncludeLocation.Term {
if term.Location != nil {
d["location"] = term.Location
}
d.Location = term.Location
}
return json.Marshal(d)
}
@@ -457,7 +480,17 @@ func (term *Term) Vars() VarSet {
}
// IsConstant returns true if the AST value is constant.
// Note that this is only a shallow check as we currently don't have a real
// notion of constant "vars" in the AST implementation. Meaning that while we could
// derive that a reference to a constant value is also constant, we currently don't.
func IsConstant(v Value) bool {
switch v.(type) {
case Null, Boolean, Number, String:
return true
case Var, Ref, *ArrayComprehension, *ObjectComprehension, *SetComprehension, Call:
return false
}
found := false
vis := GenericVisitor{
func(x any) bool {
@@ -532,8 +565,6 @@ func IsScalar(v Value) bool {
// Null represents the null value defined by JSON.
type Null struct{}
var NullValue Value = Null{}
// NullTerm creates a new Term with a Null value.
func NullTerm() *Term {
return &Term{Value: NullValue}
@@ -651,8 +682,9 @@ func NumberTerm(n json.Number) *Term {
}
// IntNumberTerm creates a new Term with an integer Number value.
// For values between -1 and 512, returns a cached Term to reduce allocations.
func IntNumberTerm(i int) *Term {
return &Term{Value: newIntNumberValue(i)}
return internedIntNumberTerm(i)
}
// UIntNumberTerm creates a new Term with an unsigned integer Number value.
@@ -819,12 +851,159 @@ func (str String) Hash() int {
return int(xxhash.Sum64String(string(str)))
}
type TemplateString struct {
Parts []Node `json:"parts"`
MultiLine bool `json:"multi_line"`
}
func (ts *TemplateString) Copy() *TemplateString {
cpy := &TemplateString{MultiLine: ts.MultiLine, Parts: make([]Node, len(ts.Parts))}
for i, p := range ts.Parts {
switch v := p.(type) {
case *Expr:
cpy.Parts[i] = v.Copy()
case *Term:
cpy.Parts[i] = v.Copy()
}
}
return cpy
}
func (ts *TemplateString) Equal(other Value) bool {
if o, ok := other.(*TemplateString); ok && ts.MultiLine == o.MultiLine && len(ts.Parts) == len(o.Parts) {
for i, p := range ts.Parts {
switch v := p.(type) {
case *Expr:
if ope, ok := o.Parts[i].(*Expr); !ok || !v.Equal(ope) {
return false
}
case *Term:
if opt, ok := o.Parts[i].(*Term); !ok || !v.Equal(opt) {
return false
}
default:
return false
}
}
return true
}
return false
}
func (ts *TemplateString) Compare(other Value) int {
if ots, ok := other.(*TemplateString); ok {
if ts.MultiLine != ots.MultiLine {
if !ts.MultiLine {
return -1
}
return 1
}
if len(ts.Parts) != len(ots.Parts) {
return len(ts.Parts) - len(ots.Parts)
}
for i := range ts.Parts {
if cmp := Compare(ts.Parts[i], ots.Parts[i]); cmp != 0 {
return cmp
}
}
return 0
}
return Compare(ts, other)
}
func (ts *TemplateString) Find(path Ref) (Value, error) {
if len(path) == 0 {
return ts, nil
}
return nil, errFindNotFound
}
func (ts *TemplateString) Hash() int {
hash := 0
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
hash += x.Hash()
case *Term:
hash += x.Value.Hash()
default:
panic(fmt.Sprintf("invalid template part type %T", p))
}
}
return hash
}
func (*TemplateString) IsGround() bool {
return false
}
func (ts *TemplateString) String() string {
buf, _ := ts.AppendText(make([]byte, 0, ts.StringLength()))
return util.ByteSliceToString(buf)
}
func TemplateStringTerm(multiLine bool, parts ...Node) *Term {
return &Term{Value: &TemplateString{MultiLine: multiLine, Parts: parts}}
}
// EscapeTemplateStringStringPart escapes unescaped left curly braces in s - i.e "{" becomes "\{".
// The internal representation of string terms within a template string does **NOT**
// treat '{' as special, but expects code dealing with template strings to escape them when
// required, such as when serializing the complete template string. Code that programmatically
// constructs template strings should not pre-escape left curly braces in string term parts.
//
// // TODO(anders): a future optimization would be to combine this with the other escaping done
// // for strings (e.g. escaping quotes, backslashes, and JSON control characters) in a single operation
// // to avoid multiple passes and allocations over the same string. That's currently done by
// // strconv.Quote, so we would need to re-implement that logic in code of our own.
// // NOTE(anders): I would love to come up with a better name for this component than
// // "TemplateStringStringPart"..
func EscapeTemplateStringStringPart(s string) string {
numUnescaped := countUnescapedLeftCurly(s)
if numUnescaped == 0 {
return s
}
return util.ByteSliceToString(AppendEscapedTemplateStringStringPart(make([]byte, 0, len(s)+numUnescaped), s))
}
func AppendEscapedTemplateStringStringPart(buf []byte, s string) []byte {
if s[0] == '{' {
buf = append(buf, '\\', s[0])
} else {
buf = append(buf, s[0])
}
for i := 1; i < len(s); i++ {
if s[i] == '{' && s[i-1] != '\\' {
buf = append(buf, '\\', s[i])
} else {
buf = append(buf, s[i])
}
}
return buf
}
func countUnescapedLeftCurly(s string) (n int) {
// Note(anders): while not the functions I'd intuitively reach for to solve this,
// they are hands down the fastest option here, as they're done in assembly, which
// performs about an order of magnitude better than a manual loop in Go.
if n = strings.Count(s, "{"); n > 0 {
n -= strings.Count(s, `\{`)
}
return n
}
// Var represents a variable as defined by the language.
type Var string
// VarTerm creates a new Term with a Variable value.
func VarTerm(v string) *Term {
return &Term{Value: Var(v)}
return &Term{Value: InternedVarValue(v)}
}
// Equal returns true if the other Value is a Variable and has the same value
@@ -881,7 +1060,7 @@ func (v Var) String() string {
// illegal variable name character (WildcardPrefix) to avoid conflicts. When
// we serialize the variable here, we need to make sure it's parseable.
if v.IsWildcard() {
return Wildcard.String()
return WildcardString
}
return string(v)
}
@@ -952,14 +1131,14 @@ func (ref Ref) Insert(x *Term, pos int) Ref {
// Extend returns a copy of ref with the terms from other appended. The head of
// other will be converted to a string.
func (ref Ref) Extend(other Ref) Ref {
dst := make(Ref, len(ref)+len(other))
offset := len(ref)
dst := make(Ref, offset+len(other))
copy(dst, ref)
head := other[0].Copy()
head.Value = String(head.Value.(Var))
offset := len(ref)
dst[offset] = head
dst[offset] = head
copy(dst[offset+1:], other[1:])
return dst
}
@@ -1071,42 +1250,38 @@ func (ref Ref) HasPrefix(other Ref) bool {
func (ref Ref) ConstantPrefix() Ref {
i := ref.Dynamic()
if i < 0 {
return ref.Copy()
return ref
}
return ref[:i].Copy()
return ref[:i]
}
// StringPrefix returns the string portion of the ref starting from the head.
func (ref Ref) StringPrefix() Ref {
for i := 1; i < len(ref); i++ {
switch ref[i].Value.(type) {
case String: // pass
default: // cut off
return ref[:i].Copy()
return ref[:i]
}
}
return ref.Copy()
return ref
}
// GroundPrefix returns the ground portion of the ref starting from the head. By
// definition, the head of the reference is always ground.
func (ref Ref) GroundPrefix() Ref {
if ref.IsGround() {
return ref
}
prefix := make(Ref, 0, len(ref))
for i, x := range ref {
if i > 0 && !x.IsGround() {
break
for i := range ref {
if i > 0 && !ref[i].IsGround() {
return ref[:i]
}
prefix = append(prefix, x)
}
return prefix
return ref
}
// DynamicSuffix returns the dynamic portion of the ref.
// If the ref is not dynamic, nil is returned.
func (ref Ref) DynamicSuffix() Ref {
i := ref.Dynamic()
if i < 0 {
@@ -1117,7 +1292,7 @@ func (ref Ref) DynamicSuffix() Ref {
// IsGround returns true if all of the parts of the Ref are ground.
func (ref Ref) IsGround() bool {
if len(ref) == 0 {
if len(ref) < 2 {
return true
}
return termSliceIsGround(ref[1:])
@@ -1137,85 +1312,84 @@ func (ref Ref) IsNested() bool {
// contains non-string terms this function returns an error. Path
// components are escaped.
func (ref Ref) Ptr() (string, error) {
parts := make([]string, 0, len(ref)-1)
for _, term := range ref[1:] {
if str, ok := term.Value.(String); ok {
parts = append(parts, url.PathEscape(string(str)))
} else {
buf := &strings.Builder{}
tail := ref[1:]
l := max(len(tail)-1, 0) // number of '/' to add
for i := range tail {
str, ok := tail[i].Value.(String)
if !ok {
return "", errors.New("invalid path value type")
}
l += len(str)
}
return strings.Join(parts, "/"), nil
buf.Grow(l)
for i := range tail {
if i > 0 {
buf.WriteByte('/')
}
str := string(tail[i].Value.(String))
// Sadly, the url package does not expose an appender for this.
buf.WriteString(url.PathEscape(str))
}
return buf.String(), nil
}
var varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
// IsVarCompatibleString returns true if s is a valid variable name. String s is a valid variable
// name if it starts with a letter (a-z or A-Z) or underscore (_) and is followed by
// letters (a-z or A-Z), digits (0-9), and underscores.
func IsVarCompatibleString(s string) bool {
return varRegexp.MatchString(s)
}
l := len(s)
if l == 0 {
return false
}
// not exactly easy on the eyes, but often orders of magnitude faster
// than using a compiled regex (see benchmarks in term_bench_test.go)
is_letter := func(c byte) bool {
return (c > 96 && c < 123) || (c > 64 && c < 91)
}
is_digit := func(c byte) bool {
return c > 47 && c < 58
}
var bbPool = &sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
// first character must be a letter or underscore
c := s[0]
if !(is_letter(c) || c == 95) {
return false
}
// remaining characters must be letters, digits, or underscores
for i := 1; i < l; i++ {
if c = s[i]; !(is_letter(c) || is_digit(c) || c == 95) {
return false
}
}
return true
}
func (ref Ref) String() string {
// Note(anderseknert):
// Options tried in the order of cheapness, where after some effort,
// only the last option now requires a (single) allocation:
// 1. empty ref
// 2. single var ref
// 3. built-in function ref
// 4. concatenated parts
reflen := len(ref)
if reflen == 0 {
l := len(ref)
// First check for zero-alloc options, as making the buffer for AppendText
// always costs an allocation.
if l == 0 {
return ""
}
if reflen == 1 {
if l == 1 {
if s, ok := ref[0].Value.(String); ok {
// Ref head should normally be a Var, but if for some reason
// it's a string, don't quote it.
return string(s)
}
return ref[0].Value.String()
}
if name, ok := BuiltinNameFromRef(ref); ok {
return name
}
_var := ref[0].Value.String()
bb := bbPool.Get().(*bytes.Buffer)
bb.Reset()
defer bbPool.Put(bb)
bb.Grow(len(_var) + len(ref[1:])*7) // rough estimate
bb.WriteString(_var)
for _, p := range ref[1:] {
switch p := p.Value.(type) {
case String:
str := string(p)
if IsVarCompatibleString(str) && !IsKeyword(str) {
bb.WriteByte('.')
bb.WriteString(str)
} else {
bb.WriteByte('[')
// Determine whether we need the full JSON-escaped form
if strings.ContainsFunc(str, isControlOrBackslash) {
bb.Write(strconv.AppendQuote(bb.AvailableBuffer(), str))
} else {
bb.WriteByte('"')
bb.WriteString(str)
bb.WriteByte('"')
}
bb.WriteByte(']')
}
default:
bb.WriteByte('[')
bb.WriteString(p.String())
bb.WriteByte(']')
}
}
return bb.String()
buf, _ := ref.AppendText(make([]byte, 0, ref.StringLength()))
return util.ByteSliceToString(buf)
}
// OutputVars returns a VarSet containing variables that would be bound by evaluating
@@ -1258,6 +1432,15 @@ func NewArray(a ...*Term) *Array {
return arr
}
// NewArrayWithCapacity returns a new empty Array with the given capacity pre-allocated.
func NewArrayWithCapacity(capacity int) *Array {
return &Array{
elems: make([]*Term, 0, capacity),
hashs: make([]int, 0, capacity),
ground: true,
}
}
// Array represents an array as defined by the language. Arrays are similar to the
// same types as defined by JSON with the exception that they can contain Vars
// and References.
@@ -1270,13 +1453,12 @@ type Array struct {
// Copy returns a deep copy of arr.
func (arr *Array) Copy() *Array {
cpy := make([]int, len(arr.elems))
copy(cpy, arr.hashs)
return &Array{
elems: termSliceCopy(arr.elems),
hashs: cpy,
hashs: slices.Clone(arr.hashs),
hash: arr.hash,
ground: arr.IsGround()}
ground: arr.ground,
}
}
// Equal returns true if arr is equal to other.
@@ -1387,21 +1569,8 @@ func (arr *Array) MarshalJSON() ([]byte, error) {
}
func (arr *Array) String() string {
sb := sbPool.Get()
sb.Grow(len(arr.elems) * 16)
defer sbPool.Put(sb)
sb.WriteByte('[')
for i, e := range arr.elems {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(e.String())
}
sb.WriteByte(']')
return sb.String()
buf, _ := arr.AppendText(make([]byte, 0, arr.StringLength()))
return util.ByteSliceToString(buf)
}
// Len returns the number of elements in the array.
@@ -1519,6 +1688,11 @@ func NewSet(t ...*Term) Set {
return s
}
// NewSetWithCapacity returns a new empty Set with the given capacity pre-allocated.
func NewSetWithCapacity(capacity int) Set {
return newset(capacity)
}
func newset(n int) *set {
var keys []*Term
if n > 0 {
@@ -1555,13 +1729,19 @@ type set struct {
// Copy returns a deep copy of s.
func (s *set) Copy() Set {
terms := make([]*Term, len(s.keys))
for i := range s.keys {
terms[i] = s.keys[i].Copy()
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)),
}
cpy := NewSet(terms...).(*set)
cpy.hash = s.hash
cpy.ground = s.ground
for hash := range s.elems {
cpy.elems[hash] = s.elems[hash].Copy()
cpy.keys = append(cpy.keys, cpy.elems[hash])
}
return cpy
}
@@ -1576,25 +1756,8 @@ func (s *set) Hash() int {
}
func (s *set) String() string {
if s.Len() == 0 {
return "set()"
}
sb := sbPool.Get()
sb.Grow(s.Len() * 16)
defer sbPool.Put(sb)
sb.WriteByte('{')
for i := range s.sortedKeys() {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(s.keys[i].Value.String())
}
sb.WriteByte('}')
return sb.String()
buf, _ := s.AppendText(make([]byte, 0, s.StringLength()))
return util.ByteSliceToString(buf)
}
func (s *set) sortedKeys() []*Term {
@@ -1635,14 +1798,14 @@ func (s *set) Diff(other Set) Set {
return NewSet()
}
terms := make([]*Term, 0, len(s.keys))
for _, term := range s.sortedKeys() {
result := newset(len(s.keys))
for _, term := range s.keys {
if !other.Contains(term) {
terms = append(terms, term)
result.insert(term, false)
}
}
return NewSet(terms...)
return result
}
// Intersect returns the set containing elements in both s and other.
@@ -1657,21 +1820,28 @@ func (s *set) Intersect(other Set) Set {
n = m
}
terms := make([]*Term, 0, n)
for _, term := range ss.sortedKeys() {
result := newset(n)
for _, term := range ss.keys {
if so.Contains(term) {
terms = append(terms, term)
result.insert(term, false)
}
}
return NewSet(terms...)
return result
}
// Union returns the set containing all elements of s and other.
func (s *set) Union(other Set) Set {
r := NewSet()
s.Foreach(r.Add)
other.Foreach(r.Add)
o := other.(*set)
// Pre-allocate with max size - avoids over-allocation for overlapping sets
// while only requiring one potential grow for disjoint sets.
r := newset(max(len(s.keys), len(o.keys)))
for _, term := range s.keys {
r.insert(term, false)
}
for _, term := range o.keys {
r.insert(term, false)
}
return r
}
@@ -1845,6 +2015,11 @@ func NewObject(t ...[2]*Term) Object {
return obj
}
// NewObjectWithCapacity returns a new empty Object with the given capacity pre-allocated.
func NewObjectWithCapacity(capacity int) Object {
return newobject(capacity)
}
// ObjectTerm creates a new Term with an Object value.
func ObjectTerm(o ...[2]*Term) *Term {
return &Term{Value: NewObject(o...)}
@@ -2316,19 +2491,21 @@ func (obj *object) Merge(other Object) (Object, bool) {
// is called. The conflictResolver can return a merged value and a boolean
// indicating if the merge has failed and should stop.
func (obj *object) MergeWith(other Object, conflictResolver func(v1, v2 *Term) (*Term, bool)) (Object, bool) {
result := NewObject()
// Might overallocate assuming no conflicts is the common case,
// but that's typically faster than iterating over each object twice.
result := newobject(obj.Len() + other.Len())
stop := obj.Until(func(k, v *Term) bool {
v2 := other.Get(k)
// The key didn't exist in other, keep the original value
if v2 == nil {
result.Insert(k, v)
result.insert(k, v, false)
return false
}
// The key exists in both, resolve the conflict if possible
merged, stop := conflictResolver(v, v2)
if !stop {
result.Insert(k, merged)
result.insert(k, merged, false)
}
return stop
})
@@ -2340,7 +2517,7 @@ func (obj *object) MergeWith(other Object, conflictResolver func(v1, v2 *Term) (
// Copy in any values from other for keys that don't exist in obj
other.Foreach(func(k, v *Term) {
if v2 := obj.Get(k); v2 == nil {
result.Insert(k, v)
result.insert(k, v, false)
}
})
return result, true
@@ -2363,24 +2540,8 @@ func (obj *object) Len() int {
}
func (obj *object) String() string {
sb := sbPool.Get()
sb.Grow(obj.Len() * 32)
defer sbPool.Put(sb)
sb.WriteByte('{')
for i, elem := range obj.sortedKeys() {
if i > 0 {
sb.WriteString(", ")
}
sb.WriteString(elem.key.String())
sb.WriteString(": ")
sb.WriteString(elem.value.String())
}
sb.WriteByte('}')
return sb.String()
buf, _ := obj.AppendText(make([]byte, 0, obj.StringLength()))
return util.ByteSliceToString(buf)
}
func (*object) get(*Term) *objectElem {
@@ -2451,7 +2612,7 @@ func filterObject(o Value, filter Value) (Value, error) {
case String, Number, Boolean, Null:
return o, nil
case *Array:
values := NewArray()
values := make([]*Term, 0, v.Len())
for i := range v.Len() {
subFilter := filteredObj.Get(InternedIntegerString(i))
if subFilter != nil {
@@ -2459,10 +2620,10 @@ func filterObject(o Value, filter Value) (Value, error) {
if err != nil {
return nil, err
}
values = values.Append(NewTerm(filteredValue))
values = append(values, NewTerm(filteredValue))
}
}
return values, nil
return NewArray(values...), nil
case Set:
terms := make([]*Term, 0, v.Len())
for _, t := range v.Slice() {
@@ -2585,7 +2746,8 @@ func (ac *ArrayComprehension) IsGround() bool {
}
func (ac *ArrayComprehension) String() string {
return "[" + ac.Term.String() + " | " + ac.Body.String() + "]"
buf, _ := ac.AppendText(make([]byte, 0, ac.StringLength()))
return util.ByteSliceToString(buf)
}
// ObjectComprehension represents an object comprehension as defined in the language.
@@ -2645,7 +2807,8 @@ func (oc *ObjectComprehension) IsGround() bool {
}
func (oc *ObjectComprehension) String() string {
return "{" + oc.Key.String() + ": " + oc.Value.String() + " | " + oc.Body.String() + "}"
buf, _ := oc.AppendText(make([]byte, 0, oc.StringLength()))
return util.ByteSliceToString(buf)
}
// SetComprehension represents a set comprehension as defined in the language.
@@ -2702,7 +2865,8 @@ func (sc *SetComprehension) IsGround() bool {
}
func (sc *SetComprehension) String() string {
return "{" + sc.Term.String() + " | " + sc.Body.String() + "}"
buf, _ := sc.AppendText(make([]byte, 0, sc.StringLength()))
return util.ByteSliceToString(buf)
}
// Call represents as function call in the language.
@@ -2740,18 +2904,31 @@ func (c Call) IsGround() bool {
return termSliceIsGround(c)
}
// MakeExpr returns an ew Expr from this call.
// MakeExpr returns a new Expr from this call.
func (c Call) MakeExpr(output *Term) *Expr {
terms := []*Term(c)
return NewExpr(append(terms, output))
}
func (c Call) String() string {
args := make([]string, len(c)-1)
for i := 1; i < len(c); i++ {
args[i-1] = c[i].String()
func (c Call) Operator() Ref {
if len(c) == 0 {
return nil
}
return fmt.Sprintf("%v(%v)", c[0], strings.Join(args, ", "))
return c[0].Value.(Ref)
}
func (c Call) Operands() []*Term {
if len(c) < 1 {
return nil
}
return c[1:]
}
func (c Call) String() string {
buf, _ := c.AppendText(make([]byte, 0, c.StringLength()))
return util.ByteSliceToString(buf)
}
func termSliceCopy(a []*Term) []*Term {
+266
View File
@@ -0,0 +1,266 @@
package ast
import (
"encoding"
"strconv"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
// AppendText appends the text representation of term (i.e. as printed in policy) to
// buf and returns the extended buffer.
func (term *Term) AppendText(buf []byte) ([]byte, error) {
if app, ok := term.Value.(encoding.TextAppender); ok {
return app.AppendText(buf)
}
return append(buf, term.Value.String()...), nil
}
func (v Var) AppendText(buf []byte) ([]byte, error) {
if v.IsWildcard() {
return append(buf, WildcardString...), nil
}
return append(buf, v...), nil
}
func (b Boolean) AppendText(buf []byte) ([]byte, error) {
if b {
return append(buf, "true"...), nil
}
return append(buf, "false"...), nil
}
func (Null) AppendText(buf []byte) ([]byte, error) {
return append(buf, "null"...), nil
}
func (str String) AppendText(buf []byte) ([]byte, error) {
return strconv.AppendQuote(buf, string(str)), nil
}
func (str String) appendNoQuote(buf []byte) []byte {
// Append using strconv.AppendQuote for proper escaping, but trim off
// the leading and trailing quotes afterwards.
oldLen := len(buf)
buf = strconv.AppendQuote(buf, string(str))
newLen := len(buf)
quoted := buf[oldLen:newLen]
return append(buf[:oldLen], quoted[1:len(quoted)-1]...)
}
func (num Number) AppendText(buf []byte) ([]byte, error) {
return append(buf, num...), nil
}
func (arr *Array) AppendText(buf []byte) ([]byte, error) {
buf, err := AppendDelimeted(append(buf, '['), arr.elems, ", ")
if err != nil {
return nil, err
}
return append(buf, ']'), nil
}
func (obj *object) AppendText(buf []byte) ([]byte, error) {
olen := obj.Len()
if olen == 0 {
return append(buf, "{}"...), nil
}
buf = append(buf, '{')
var err error
// first key-value pair
keys := obj.sortedKeys()
for i := range keys {
if buf, err = keys[i].key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ": "...)
if buf, err = keys[i].value.AppendText(buf); err != nil {
return nil, err
}
if i < olen-1 {
buf = append(buf, ", "...)
}
}
return append(buf, '}'), nil
}
func (obj *lazyObj) AppendText(buf []byte) ([]byte, error) {
return append(buf, obj.force().String()...), nil
}
func (s *set) AppendText(buf []byte) ([]byte, error) {
slen := s.Len()
if slen == 0 {
return append(buf, "set()"...), nil
}
var err error
buf = append(buf, '{')
if buf, err = AppendDelimeted(buf, s.sortedKeys(), ", "); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
func (c Call) AppendText(buf []byte) ([]byte, error) {
if len(c) == 0 {
return buf, nil
}
var err error
if buf, err = c[0].AppendText(buf); err != nil {
return nil, err
}
if buf, err = AppendDelimeted(append(buf, '('), c[1:], ", "); err != nil {
return nil, err
}
return append(buf, ')'), nil
}
func (ts *TemplateString) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, "$\""...)
for _, p := range ts.Parts {
switch x := p.(type) {
case *Expr:
buf = append(buf, '{')
var err error
if buf, err = x.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, '}')
case *Term:
if str, ok := x.Value.(String); ok {
// TODO(anders): this is a bit of a mess, but as explained by the comment on
// [EscapeTemplateStringStringPart], required as long as we rely on strconv for escaping, which adds
// quotes around the string that we don't want here, and trying to "unappend" them is not nice at all..
s := string(str)
ulc := countUnescapedLeftCurly(s)
sl := str.StringLength() + ulc - 2 // no surrounding quotes
if sl == len(s) { // no escaping needed
buf = append(buf, s...)
} else { // some escaping needed
if sl == len(s)+ulc { // only unescaped {
buf = AppendEscapedTemplateStringStringPart(buf, string(str))
} else { // full escaping needed. this is expensive but luckily rare
tmp := str.appendNoQuote(make([]byte, 0, sl))
ets := EscapeTemplateStringStringPart(util.ByteSliceToString(tmp))
buf = append(buf, ets...)
}
}
} else {
var err error
if buf, err = x.AppendText(buf); err != nil {
return nil, err
}
}
default:
buf = append(buf, "<invalid>"...)
}
}
return append(buf, '"'), nil
}
func (r Ref) AppendText(buf []byte) ([]byte, error) {
reflen := len(r)
if reflen == 0 {
return buf, nil
}
if reflen == 1 {
if s, ok := r[0].Value.(String); ok {
// While a ref head is typically a Var, a lone String term should not be quoted
return append(buf, s...), nil
}
return r[0].AppendText(buf)
}
if name, ok := BuiltinNameFromRef(r); ok {
return append(buf, name...), nil
}
var err error
if s, ok := r[0].Value.(String); ok {
buf = append(buf, s...)
} else if buf, err = r[0].AppendText(buf); err != nil {
return nil, err
}
for _, p := range r[1:] {
switch v := p.Value.(type) {
case String:
str := string(v)
if IsVarCompatibleString(str) && !IsKeyword(str) {
buf = append(append(buf, '.'), str...)
} else {
buf = append(buf, '[')
// Determine whether we need the full JSON-escaped form
if strings.ContainsFunc(str, isControlOrBackslash) {
if buf, err = v.AppendText(buf); err != nil {
return nil, err
}
} else {
buf = append(append(append(buf, '"'), str...), '"')
}
buf = append(buf, ']')
}
default:
buf = append(buf, '[')
if buf, err = p.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ']')
}
}
return buf, nil
}
func (sc *SetComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
if buf, err = sc.Term.AppendText(buf); err != nil {
return nil, err
}
if buf, err = sc.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
func (ac *ArrayComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '[')
var err error
if buf, err = ac.Term.AppendText(buf); err != nil {
return nil, err
}
if buf, err = ac.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, ']'), nil
}
func (oc *ObjectComprehension) AppendText(buf []byte) ([]byte, error) {
buf = append(buf, '{')
var err error
if buf, err = oc.Key.AppendText(buf); err != nil {
return nil, err
}
buf = append(buf, ": "...)
if buf, err = oc.Value.AppendText(buf); err != nil {
return nil, err
}
if buf, err = oc.Body.AppendText(append(buf, " | "...)); err != nil {
return nil, err
}
return append(buf, '}'), nil
}
+29 -16
View File
@@ -19,7 +19,6 @@ type Transformer interface {
// Transform iterates the AST and calls the Transform function on the
// Transformer t for x before recursing.
func Transform(t Transformer, x any) (any, error) {
if term, ok := x.(*Term); ok {
return Transform(t, term.Value)
}
@@ -284,6 +283,19 @@ func Transform(t Transformer, x any) (any, error) {
}
}
return y, nil
case *TemplateString:
for i := range y.Parts {
if expr, ok := y.Parts[i].(*Expr); ok {
transformed, err := Transform(t, expr)
if err != nil {
return nil, err
}
if y.Parts[i], ok = transformed.(*Expr); !ok {
return nil, fmt.Errorf("illegal transform: %T != %T", expr, transformed)
}
}
}
return y, nil
default:
return y, nil
}
@@ -291,29 +303,29 @@ func Transform(t Transformer, x any) (any, error) {
// TransformRefs calls the function f on all references under x.
func TransformRefs(x any, f func(Ref) (Value, error)) (any, error) {
t := &GenericTransformer{func(x any) (any, error) {
t := NewGenericTransformer(func(x any) (any, error) {
if r, ok := x.(Ref); ok {
return f(r)
}
return x, nil
}}
})
return Transform(t, x)
}
// TransformVars calls the function f on all vars under x.
func TransformVars(x any, f func(Var) (Value, error)) (any, error) {
t := &GenericTransformer{func(x any) (any, error) {
t := NewGenericTransformer(func(x any) (any, error) {
if v, ok := x.(Var); ok {
return f(v)
}
return x, nil
}}
})
return Transform(t, x)
}
// TransformComprehensions calls the functio nf on all comprehensions under x.
// TransformComprehensions calls the function f on all comprehensions under x.
func TransformComprehensions(x any, f func(any) (Value, error)) (any, error) {
t := &GenericTransformer{func(x any) (any, error) {
t := NewGenericTransformer(func(x any) (any, error) {
switch x := x.(type) {
case *ArrayComprehension:
return f(x)
@@ -323,7 +335,7 @@ func TransformComprehensions(x any, f func(any) (Value, error)) (any, error) {
return f(x)
}
return x, nil
}}
})
return Transform(t, x)
}
@@ -387,11 +399,7 @@ func transformTerm(t Transformer, term *Term) (*Term, error) {
if err != nil {
return nil, err
}
r := &Term{
Value: v,
Location: term.Location,
}
return r, nil
return &Term{Value: v, Location: term.Location}, nil
}
func transformValue(t Transformer, v Value) (Value, error) {
@@ -407,13 +415,18 @@ func transformValue(t Transformer, v Value) (Value, error) {
}
func transformVar(t Transformer, v Var) (Var, error) {
v1, err := Transform(t, v)
tv, err := t.Transform(v)
if err != nil {
return "", err
}
r, ok := v1.(Var)
if tv == nil {
return "", nil
}
r, ok := tv.(Var)
if !ok {
return "", fmt.Errorf("illegal transform: %T != %T", v, v1)
return "", fmt.Errorf("illegal transform: %T != %T", v, tv)
}
return r, nil
}
+5 -6
View File
@@ -11,12 +11,11 @@ func isRefSafe(ref Ref, safe VarSet) bool {
case Call:
return isCallSafe(head, safe)
default:
for v := range ref[0].Vars() {
if !safe.Contains(v) {
return false
}
}
return true
vis := varVisitorPool.Get().WithParams(SafetyCheckVisitorParams)
vis.Walk(ref[0])
isSafe := vis.Vars().DiffCount(safe) == 0
varVisitorPool.Put(vis)
return isSafe
}
}
+12 -2
View File
@@ -12,7 +12,7 @@ import (
)
// VarSet represents a set of variables.
type VarSet map[Var]struct{}
type VarSet map[Var]struct{ *Location }
// NewVarSet returns a new VarSet containing the specified variables.
func NewVarSet(vs ...Var) VarSet {
@@ -30,7 +30,16 @@ func NewVarSetOfSize(size int) VarSet {
// Add updates the set to include the variable "v".
func (s VarSet) Add(v Var) {
s[v] = struct{}{}
if _, ok := s[v]; !ok {
s[v] = struct{ *Location }{}
}
}
func (s VarSet) AddLocation(v Var, l *Location) {
if entry, ok := s[v]; ok {
entry.Location = l
s[v] = entry
}
}
// Contains returns true if the set contains the variable "v".
@@ -54,6 +63,7 @@ func (s VarSet) Diff(vs VarSet) VarSet {
for v := range s {
if !vs.Contains(v) {
r.Add(v)
r.AddLocation(v, s[v].Location)
}
}
return r
File diff suppressed because it is too large Load Diff
+329 -135
View File
@@ -4,40 +4,109 @@
package ast
// Visitor defines the interface for iterating AST elements. The Visit function
// can return a Visitor w which will be used to visit the children of the AST
// element v. If the Visit function returns nil, the children will not be
// visited.
// Deprecated: use GenericVisitor or another visitor implementation
type Visitor interface {
Visit(v any) (w Visitor)
}
var (
termTypeVisitor = newTypeVisitor[*Term]()
varTypeVisitor = newTypeVisitor[Var]()
exprTypeVisitor = newTypeVisitor[*Expr]()
ruleTypeVisitor = newTypeVisitor[*Rule]()
refTypeVisitor = newTypeVisitor[Ref]()
bodyTypeVisitor = newTypeVisitor[Body]()
withTypeVisitor = newTypeVisitor[*With]()
)
// BeforeAndAfterVisitor wraps Visitor to provide hooks for being called before
// and after the AST has been visited.
// Deprecated: use GenericVisitor or another visitor implementation
type BeforeAndAfterVisitor interface {
Visitor
Before(x any)
After(x any)
}
type (
// GenericVisitor provides a utility to walk over AST nodes using a
// closure. If the closure returns true, the visitor will not walk
// over AST nodes under x.
GenericVisitor struct {
f func(x any) bool
}
// Walk iterates the AST by calling the Visit function on the Visitor
// BeforeAfterVisitor provides a utility to walk over AST nodes using
// closures. If the before closure returns true, the visitor will not
// walk over AST nodes under x. The after closure is invoked always
// after visiting a node.
BeforeAfterVisitor struct {
before func(x any) bool
after func(x any)
}
// VarVisitor walks AST nodes under a given node and collects all encountered
// variables. The collected variables can be controlled by specifying
// VarVisitorParams when creating the visitor.
VarVisitor struct {
params VarVisitorParams
vars VarSet
}
// VarVisitorParams contains settings for a VarVisitor.
VarVisitorParams struct {
SkipRefHead bool
SkipRefCallHead bool
SkipObjectKeys bool
SkipClosures bool
SkipWithTarget bool
SkipSets bool
SkipTemplateStrings bool
}
// Visitor defines the interface for iterating AST elements. The Visit function
// can return a Visitor w which will be used to visit the children of the AST
// element v. If the Visit function returns nil, the children will not be
// visited.
//
// Deprecated: use [GenericVisitor] or another visitor implementation
Visitor interface {
Visit(v any) (w Visitor)
}
// BeforeAndAfterVisitor wraps Visitor to provide hooks for being called before
// and after the AST has been visited.
//
// Deprecated: use [GenericVisitor] or another visitor implementation
BeforeAndAfterVisitor interface {
Visitor
Before(x any)
After(x any)
}
// typeVisitor is a generic visitor for a specific type T (the "generic" name was
// however taken). Contrary to the [GenericVisitor], the typeVisitor only invokes
// the visit function for nodes of type T, saving both CPU cycles and type assertions.
// typeVisitor implementations carry no state, and can be shared freely across
// goroutines. Access is private for the time being, as there is already inflation
// in visitor types exposed in the AST package. The various WalkXXX functions however
// now leverage typeVisitor under the hood.
//
// While a typeVisitor is generally a more performant option over a GenericVisitor,
// it is not as flexible: a type visitor can only visit nodes of a single type T,
// whereas a GenericVisitor visits all nodes. Adding to that, a typeVisitor can only
// be instantiated for **concrete types** — not interfaces (e.g., [*Expr], not [Node]),
// as reflection would be required to determine the concrete type at runtime, thus
// nullifying the performance benefits of the typeVisitor in the first place.
typeVisitor[T any] struct {
typ any
}
)
// Walk iterates the AST by calling the Visit function on the [Visitor]
// v for x before recursing.
// Deprecated: use GenericVisitor.Walk
//
// Deprecated: use [GenericVisitor.Walk]
func Walk(v Visitor, x any) {
if bav, ok := v.(BeforeAndAfterVisitor); !ok {
walk(v, x)
} else {
bav.Before(x)
defer bav.After(x)
walk(bav, x)
bav.After(x)
}
}
// WalkBeforeAndAfter iterates the AST by calling the Visit function on the
// Visitor v for x before recursing.
// Deprecated: use GenericVisitor.Walk
//
// Deprecated: use [GenericVisitor.Walk]
func WalkBeforeAndAfter(v BeforeAndAfterVisitor, x any) {
Walk(v, x)
}
@@ -149,132 +218,258 @@ func walk(v Visitor, x any) {
for i := range x.Symbols {
Walk(w, x.Symbols[i])
}
case *TemplateString:
for i := range x.Parts {
Walk(w, x.Parts[i])
}
}
}
// WalkVars calls the function f on all vars under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkVars(x any, f func(Var) bool) {
vis := &GenericVisitor{func(x any) bool {
if v, ok := x.(Var); ok {
return f(v)
}
return false
}}
vis.Walk(x)
varTypeVisitor.walk(x, f)
}
// WalkClosures calls the function f on all closures under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkClosures(x any, f func(any) bool) {
vis := &GenericVisitor{func(x any) bool {
vis := NewGenericVisitor(func(x any) bool {
switch x := x.(type) {
case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *Every:
return f(x)
}
return false
}}
})
vis.Walk(x)
}
// WalkRefs calls the function f on all references under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkRefs(x any, f func(Ref) bool) {
vis := &GenericVisitor{func(x any) bool {
if r, ok := x.(Ref); ok {
return f(r)
}
return false
}}
vis.Walk(x)
refTypeVisitor.walk(x, f)
}
// WalkTerms calls the function f on all terms under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkTerms(x any, f func(*Term) bool) {
vis := &GenericVisitor{func(x any) bool {
if term, ok := x.(*Term); ok {
return f(term)
}
return false
}}
vis.Walk(x)
termTypeVisitor.walk(x, f)
}
// WalkWiths calls the function f on all with modifiers under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkWiths(x any, f func(*With) bool) {
vis := &GenericVisitor{func(x any) bool {
if w, ok := x.(*With); ok {
return f(w)
}
return false
}}
vis.Walk(x)
withTypeVisitor.walk(x, f)
}
// WalkExprs calls the function f on all expressions under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkExprs(x any, f func(*Expr) bool) {
vis := &GenericVisitor{func(x any) bool {
if r, ok := x.(*Expr); ok {
return f(r)
}
return false
}}
vis.Walk(x)
exprTypeVisitor.walk(x, f)
}
// WalkBodies calls the function f on all bodies under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkBodies(x any, f func(Body) bool) {
vis := &GenericVisitor{func(x any) bool {
if b, ok := x.(Body); ok {
return f(b)
}
return false
}}
vis.Walk(x)
bodyTypeVisitor.walk(x, f)
}
// WalkRules calls the function f on all rules under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkRules(x any, f func(*Rule) bool) {
vis := &GenericVisitor{func(x any) bool {
if r, ok := x.(*Rule); ok {
stop := f(r)
// NOTE(tsandall): since rules cannot be embedded inside of queries
// we can stop early if there is no else block.
if stop || r.Else == nil {
return true
switch x := x.(type) {
case *Module:
for i := range x.Rules {
if !f(x.Rules[i]) && x.Rules[i].Else != nil {
WalkRules(x.Rules[i].Else, f)
}
}
return false
}}
vis.Walk(x)
case *Rule:
if !f(x) && x.Else != nil {
WalkRules(x.Else, f)
}
default:
ruleTypeVisitor.walk(x, f)
}
}
// WalkNodes calls the function f on all nodes under x. If the function f
// returns true, AST nodes under the last node will not be visited.
func WalkNodes(x any, f func(Node) bool) {
vis := &GenericVisitor{func(x any) bool {
vis := NewGenericVisitor(func(x any) bool {
if n, ok := x.(Node); ok {
return f(n)
}
return false
}}
})
vis.Walk(x)
}
// GenericVisitor provides a utility to walk over AST nodes using a
// closure. If the closure returns true, the visitor will not walk
// over AST nodes under x.
type GenericVisitor struct {
f func(x any) bool
func newTypeVisitor[T any]() *typeVisitor[T] {
var t T
return &typeVisitor[T]{typ: any(t)}
}
func (tv *typeVisitor[T]) walkArgs(args Args, visit func(x T) bool) {
// If T is not Args, avoid allocation by inlining the walk.
if _, ok := tv.typ.(Args); !ok {
for i := range args {
tv.walk(args[i], visit)
}
} else {
tv.walk(args, visit) // allocates
}
}
func (tv *typeVisitor[T]) walkBody(body Body, visit func(x T) bool) {
if _, ok := tv.typ.(Body); !ok {
for i := range body {
tv.walk(body[i], visit)
}
} else {
tv.walk(body, visit) // allocates
}
}
func (tv *typeVisitor[T]) walkRef(ref Ref, visit func(x T) bool) {
if _, ok := tv.typ.(Ref); !ok {
for i := range ref {
tv.walk(ref[i], visit)
}
} else {
tv.walk(ref, visit) // allocates
}
}
func (tv *typeVisitor[T]) walk(x any, visit func(x T) bool) {
if v, ok := x.(T); ok && visit(v) {
return
}
switch x := x.(type) {
case *Module:
tv.walk(x.Package, visit)
for i := range x.Imports {
tv.walk(x.Imports[i], visit)
}
for i := range x.Rules {
tv.walk(x.Rules[i], visit)
}
for i := range x.Annotations {
tv.walk(x.Annotations[i], visit)
}
for i := range x.Comments {
tv.walk(x.Comments[i], visit)
}
case *Package:
tv.walkRef(x.Path, visit)
case *Import:
tv.walk(x.Path, visit)
if _, ok := tv.typ.(Var); ok {
tv.walk(x.Alias, visit)
}
case *Rule:
tv.walk(x.Head, visit)
tv.walkBody(x.Body, visit)
if x.Else != nil {
tv.walk(x.Else, visit)
}
case *Head:
if _, ok := tv.typ.(Var); ok {
tv.walk(x.Name, visit)
}
tv.walkArgs(x.Args, visit)
if x.Key != nil {
tv.walk(x.Key, visit)
}
if x.Value != nil {
tv.walk(x.Value, visit)
}
case Body:
for i := range x {
tv.walk(x[i], visit)
}
case Args:
for i := range x {
tv.walk(x[i], visit)
}
case *Expr:
switch ts := x.Terms.(type) {
case *Term, *SomeDecl, *Every:
tv.walk(ts, visit)
case []*Term:
for i := range ts {
tv.walk(ts[i], visit)
}
}
for i := range x.With {
tv.walk(x.With[i], visit)
}
case *With:
tv.walk(x.Target, visit)
tv.walk(x.Value, visit)
case *Term:
tv.walk(x.Value, visit)
case Ref:
for i := range x {
tv.walk(x[i], visit)
}
case *object:
x.Foreach(func(k, v *Term) {
tv.walk(k, visit)
tv.walk(v, visit)
})
case Object:
for _, k := range x.Keys() {
tv.walk(k, visit)
tv.walk(x.Get(k), visit)
}
case *Array:
for i := range x.Len() {
tv.walk(x.Elem(i), visit)
}
case Set:
xSlice := x.Slice()
for i := range xSlice {
tv.walk(xSlice[i], visit)
}
case *ArrayComprehension:
tv.walk(x.Term, visit)
tv.walkBody(x.Body, visit)
case *ObjectComprehension:
tv.walk(x.Key, visit)
tv.walk(x.Value, visit)
tv.walkBody(x.Body, visit)
case *SetComprehension:
tv.walk(x.Term, visit)
tv.walkBody(x.Body, visit)
case Call:
for i := range x {
tv.walk(x[i], visit)
}
case *Every:
if x.Key != nil {
tv.walk(x.Key, visit)
}
tv.walk(x.Value, visit)
tv.walk(x.Domain, visit)
tv.walkBody(x.Body, visit)
case *SomeDecl:
for i := range x.Symbols {
tv.walk(x.Symbols[i], visit)
}
case *TemplateString:
for i := range x.Parts {
tv.walk(x.Parts[i], visit)
}
}
}
// NewGenericVisitor returns a new GenericVisitor that will invoke the function
// f on AST nodes.
// f on AST nodes. Note that while it returns a pointer, the creating a GenericVisitor
// doesn't commonly allocate it on the heap, as long as it doesn't escape the function
// in which it is created and used (as it's trivially inlined).
func NewGenericVisitor(f func(x any) bool) *GenericVisitor {
return &GenericVisitor{f}
}
@@ -306,7 +501,9 @@ func (vis *GenericVisitor) Walk(x any) {
vis.Walk(x.Path)
case *Import:
vis.Walk(x.Path)
vis.Walk(x.Alias)
if x.Alias != "" {
vis.f(x.Alias)
}
case *Rule:
vis.Walk(x.Head)
vis.Walk(x.Body)
@@ -314,8 +511,12 @@ func (vis *GenericVisitor) Walk(x any) {
vis.Walk(x.Else)
}
case *Head:
vis.Walk(x.Name)
vis.Walk(x.Args)
if x.Name != "" {
vis.f(x.Name)
}
if x.Args != nil {
vis.Walk(x.Args)
}
if x.Key != nil {
vis.Walk(x.Key)
}
@@ -395,18 +596,13 @@ func (vis *GenericVisitor) Walk(x any) {
for i := range x.Symbols {
vis.Walk(x.Symbols[i])
}
case *TemplateString:
for i := range x.Parts {
vis.Walk(x.Parts[i])
}
}
}
// BeforeAfterVisitor provides a utility to walk over AST nodes using
// closures. If the before closure returns true, the visitor will not
// walk over AST nodes under x. The after closure is invoked always
// after visiting a node.
type BeforeAfterVisitor struct {
before func(x any) bool
after func(x any)
}
// NewBeforeAfterVisitor returns a new BeforeAndAfterVisitor that
// will invoke the functions before and after AST nodes.
func NewBeforeAfterVisitor(before func(x any) bool, after func(x any)) *BeforeAfterVisitor {
@@ -538,31 +734,29 @@ func (vis *BeforeAfterVisitor) Walk(x any) {
}
}
// VarVisitor walks AST nodes under a given node and collects all encountered
// variables. The collected variables can be controlled by specifying
// VarVisitorParams when creating the visitor.
type VarVisitor struct {
params VarVisitorParams
vars VarSet
}
// VarVisitorParams contains settings for a VarVisitor.
type VarVisitorParams struct {
SkipRefHead bool
SkipRefCallHead bool
SkipObjectKeys bool
SkipClosures bool
SkipWithTarget bool
SkipSets bool
}
// NewVarVisitor returns a new VarVisitor object.
// NewVarVisitor returns a new [VarVisitor] object.
func NewVarVisitor() *VarVisitor {
return &VarVisitor{
vars: NewVarSet(),
}
}
// ClearOrNewVarVisitor clears a non-nil [VarVisitor] or returns a new one.
func ClearOrNewVarVisitor(vis *VarVisitor) *VarVisitor {
if vis == nil {
return NewVarVisitor()
}
return vis.Clear()
}
// ClearOrNew resets the visitor to its initial state, or returns a new one if nil.
//
// Deprecated: use [ClearOrNewVarVisitor] instead.
func (vis *VarVisitor) ClearOrNew() *VarVisitor {
return ClearOrNewVarVisitor(vis)
}
// Clear resets the visitor to its initial state, and returns it for chaining.
func (vis *VarVisitor) Clear() *VarVisitor {
vis.params = VarVisitorParams{}
@@ -571,14 +765,6 @@ func (vis *VarVisitor) Clear() *VarVisitor {
return vis
}
// ClearOrNew returns a new VarVisitor if vis is nil, or else a cleared VarVisitor.
func (vis *VarVisitor) ClearOrNew() *VarVisitor {
if vis == nil {
return NewVarVisitor()
}
return vis.Clear()
}
// WithParams sets the parameters in params on vis.
func (vis *VarVisitor) WithParams(params VarVisitorParams) *VarVisitor {
vis.params = params
@@ -594,7 +780,7 @@ func (vis *VarVisitor) Add(v Var) {
}
}
// Vars returns a VarSet that contains collected vars.
// Vars returns a [VarSet] that contains collected vars.
func (vis *VarVisitor) Vars() VarSet {
return vis.vars
}
@@ -621,7 +807,7 @@ func (vis *VarVisitor) visit(v any) bool {
}
if vis.params.SkipClosures {
switch v := v.(type) {
case *ArrayComprehension, *ObjectComprehension, *SetComprehension:
case *ArrayComprehension, *ObjectComprehension, *SetComprehension, *TemplateString:
return true
case *Expr:
if ev, ok := v.Terms.(*Every); ok {
@@ -685,15 +871,20 @@ func (vis *VarVisitor) visit(v any) bool {
return true
}
}
if vis.params.SkipTemplateStrings {
if _, ok := v.(*TemplateString); ok {
return true
}
}
if v, ok := v.(Var); ok {
vis.Add(v)
return true
}
return false
}
// Walk iterates the AST by calling the function f on the
// GenericVisitor before recursing. Contrary to the generic Walk, this
// does not require allocating the visitor from heap.
// 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.visit(x) {
return
@@ -701,16 +892,9 @@ func (vis *VarVisitor) Walk(x any) {
switch x := x.(type) {
case *Module:
vis.Walk(x.Package)
for i := range x.Imports {
vis.Walk(x.Imports[i])
}
for i := range x.Rules {
vis.Walk(x.Rules[i])
}
for i := range x.Comments {
vis.Walk(x.Comments[i])
}
case *Package:
vis.WalkRef(x.Path)
case *Import:
@@ -758,14 +942,17 @@ func (vis *VarVisitor) Walk(x any) {
vis.Walk(x.Value.Value)
case *Term:
vis.Walk(x.Value)
if vVar, ok := x.Value.(Var); ok {
vis.vars.AddLocation(vVar, x.Location)
}
case Ref:
for i := range x {
vis.Walk(x[i].Value)
}
case *object:
x.Foreach(func(k, _ *Term) {
x.Foreach(func(k, v *Term) {
vis.Walk(k)
vis.Walk(x.Get(k))
vis.Walk(v)
})
case *Array:
x.Foreach(func(t *Term) {
@@ -801,6 +988,10 @@ func (vis *VarVisitor) Walk(x any) {
for i := range x.Symbols {
vis.Walk(x.Symbols[i])
}
case *TemplateString:
for i := range x.Parts {
vis.Walk(x.Parts[i])
}
}
}
@@ -820,6 +1011,9 @@ func (vis *VarVisitor) WalkRef(ref Ref) {
}
for _, term := range ref {
vis.Walk(term.Value)
if vVar, ok := term.Value.(Var); ok {
vis.vars.AddLocation(vVar, term.Location)
}
}
}
+127 -189
View File
@@ -6,9 +6,7 @@
package bundle
import (
"archive/tar"
"bytes"
"compress/gzip"
"encoding/hex"
"encoding/json"
"errors"
@@ -24,6 +22,8 @@ import (
"sync"
"github.com/gobwas/glob"
"golang.org/x/sync/errgroup"
"github.com/open-policy-agent/opa/internal/file/archive"
"github.com/open-policy-agent/opa/internal/merge"
"github.com/open-policy-agent/opa/v1/ast"
@@ -51,6 +51,10 @@ const (
SnapshotBundleType = "snapshot"
)
var (
empty Bundle
)
// Bundle represents a loaded bundle. The bundle can contain data and policies.
type Bundle struct {
Signatures SignaturesConfig
@@ -96,7 +100,7 @@ type SignaturesConfig struct {
// isEmpty returns if the SignaturesConfig is empty.
func (s SignaturesConfig) isEmpty() bool {
return reflect.DeepEqual(s, SignaturesConfig{})
return s.Signatures == nil && s.Plugin == ""
}
// DecodedSignature represents the decoded JWT payload.
@@ -186,7 +190,6 @@ func (m *Manifest) SetRegoVersion(v ast.RegoVersion) {
// Equal returns true if m is semantically equivalent to other.
func (m Manifest) Equal(other Manifest) bool {
// This is safe since both are passed by value.
m.Init()
other.Init()
@@ -323,7 +326,6 @@ func (ss stringSet) Equal(other stringSet) bool {
}
func (m *Manifest) validateAndInjectDefaults(b Bundle) error {
m.Init()
// Validate roots in bundle.
@@ -337,7 +339,7 @@ func (m *Manifest) validateAndInjectDefaults(b Bundle) error {
for i := range len(roots) - 1 {
for j := i + 1; j < len(roots); j++ {
if RootPathsOverlap(roots[i], roots[j]) {
return fmt.Errorf("manifest has overlapped roots: '%v' and '%v'", roots[i], roots[j])
return fmt.Errorf("manifest has overlapped roots: '%s' and '%s'", roots[i], roots[j])
}
}
}
@@ -349,7 +351,7 @@ func (m *Manifest) validateAndInjectDefaults(b Bundle) error {
found = RootPathsContain(roots, path)
}
if !found {
return fmt.Errorf("manifest roots %v do not permit '%v' in module '%v'", roots, module.Parsed.Package, module.Path)
return fmt.Errorf("manifest roots %v do not permit '%v' in module '%s'", roots, module.Parsed.Package, module.Path)
}
}
@@ -368,7 +370,7 @@ func (m *Manifest) validateAndInjectDefaults(b Bundle) error {
// Ensure wasm module entrypoint in within bundle roots
if !RootPathsContain(roots, wmConfig.Entrypoint) {
return fmt.Errorf("manifest roots %v do not permit '%v' entrypoint for wasm module '%v'", roots, wmConfig.Entrypoint, wmConfig.Module)
return fmt.Errorf("manifest roots %v do not permit '%s' entrypoint for wasm module '%s'", roots, wmConfig.Entrypoint, wmConfig.Module)
}
if _, ok := seenEps[wmConfig.Entrypoint]; ok {
@@ -504,14 +506,13 @@ func NewReader(r io.Reader) *Reader {
// NewCustomReader returns a new Reader configured to use the
// specified DirectoryLoader.
func NewCustomReader(loader DirectoryLoader) *Reader {
nr := Reader{
return &Reader{
loader: loader,
metrics: metrics.New(),
metrics: metrics.NoOp(),
files: make(map[string]FileInfo),
sizeLimitBytes: DefaultSizeLimitBytes + 1,
lazyLoadingMode: HasExtension(),
}
return &nr
}
// IncludeManifestInData sets whether the manifest metadata should be
@@ -620,24 +621,17 @@ func (r *Reader) ParserOptions() ast.ParserOptions {
// Read returns a new Bundle loaded from the reader.
func (r *Reader) Read() (Bundle, error) {
var bundle Bundle
var descriptors []*Descriptor
var err error
var raw []Raw
bundle.Signatures, bundle.Patch, descriptors, err = preProcessBundle(r.loader, r.skipVerify, r.sizeLimitBytes)
bundle, descriptors, err := preProcessBundle(r.loader, r.skipVerify, r.sizeLimitBytes)
if err != nil {
return bundle, err
return empty, err
}
bundle.lazyLoadingMode = r.lazyLoadingMode
bundle.sizeLimitBytes = r.sizeLimitBytes
if bundle.Type() == SnapshotBundleType {
err = r.checkSignaturesAndDescriptors(bundle.Signatures)
if err != nil {
return bundle, err
if err := r.checkSignaturesAndDescriptors(bundle.Signatures); err != nil {
return empty, err
}
bundle.Data = map[string]any{}
@@ -647,7 +641,7 @@ func (r *Reader) Read() (Bundle, error) {
for _, f := range descriptors {
buf, err := readFile(f, r.sizeLimitBytes)
if err != nil {
return bundle, err
return empty, err
}
// verify the file content
@@ -663,7 +657,7 @@ func (r *Reader) Read() (Bundle, error) {
delete(r.files, path)
} else {
if err = r.verifyBundleFile(path, buf); err != nil {
return bundle, err
return empty, err
}
}
}
@@ -690,7 +684,7 @@ func (r *Reader) Read() (Bundle, error) {
p = modulePathWithPrefix(r.name, fullPath)
}
raw = append(raw, Raw{Path: p, Value: bs, module: &mf})
bundle.Raw = append(bundle.Raw, Raw{Path: p, Value: bs, module: &mf})
}
} else if filepath.Base(path) == WasmFile {
bundle.WasmModules = append(bundle.WasmModules, WasmModuleFile{
@@ -706,7 +700,7 @@ func (r *Reader) Read() (Bundle, error) {
})
} else if filepath.Base(path) == dataFile {
if r.lazyLoadingMode {
raw = append(raw, Raw{Path: path, Value: buf.Bytes()})
bundle.Raw = append(bundle.Raw, Raw{Path: path, Value: buf.Bytes()})
continue
}
@@ -717,16 +711,16 @@ func (r *Reader) Read() (Bundle, error) {
r.metrics.Timer(metrics.RegoDataParse).Stop()
if err != nil {
return bundle, fmt.Errorf("bundle load failed on %v: %w", r.fullPath(path), err)
return empty, fmt.Errorf("bundle load failed on %v: %w", r.fullPath(path), err)
}
if err := insertValue(&bundle, path, value); err != nil {
return bundle, err
if err := insertValue(bundle, path, value); err != nil {
return empty, err
}
} else if filepath.Base(path) == yamlDataFile || filepath.Base(path) == ymlDataFile {
if r.lazyLoadingMode {
raw = append(raw, Raw{Path: path, Value: buf.Bytes()})
bundle.Raw = append(bundle.Raw, Raw{Path: path, Value: buf.Bytes()})
continue
}
@@ -737,16 +731,16 @@ func (r *Reader) Read() (Bundle, error) {
r.metrics.Timer(metrics.RegoDataParse).Stop()
if err != nil {
return bundle, fmt.Errorf("bundle load failed on %v: %w", r.fullPath(path), err)
return empty, fmt.Errorf("bundle load failed on %v: %w", r.fullPath(path), err)
}
if err := insertValue(&bundle, path, value); err != nil {
return bundle, err
if err := insertValue(bundle, path, value); err != nil {
return empty, err
}
} else if strings.HasSuffix(path, ManifestExt) {
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Manifest); err != nil {
return bundle, fmt.Errorf("bundle load failed on manifest decode: %w", err)
return empty, fmt.Errorf("bundle load failed on manifest decode: %w", err)
}
}
}
@@ -754,52 +748,63 @@ func (r *Reader) Read() (Bundle, error) {
// Parse modules
popts := r.ParserOptions()
popts.RegoVersion = bundle.RegoVersion(popts.EffectiveRegoVersion())
for _, mf := range modules {
modulePopts := popts
g := &errgroup.Group{}
r.metrics.Timer(metrics.RegoModuleParse).Start()
for i, mf := range modules {
mpopts := popts
if regoVersion, err := bundle.RegoVersionForFile(mf.RelativePath, popts.EffectiveRegoVersion()); err != nil {
return bundle, err
return *bundle, err
} else if regoVersion != ast.RegoUndefined {
// We don't expect ast.RegoUndefined here, but don't override configured rego-version if we do just to be extra protective
modulePopts.RegoVersion = regoVersion
// We don't expect ast.RegoUndefined here, but don't override
// configured rego-version if we do just to be extra protective
mpopts.RegoVersion = regoVersion
}
r.metrics.Timer(metrics.RegoModuleParse).Start()
mf.Parsed, err = ast.ParseModuleWithOpts(mf.Path, util.ByteSliceToString(mf.Raw), modulePopts)
r.metrics.Timer(metrics.RegoModuleParse).Stop()
if err != nil {
return bundle, err
}
bundle.Modules = append(bundle.Modules, mf)
g.Go(func() (err error) {
if mf.Parsed, err = ast.ParseModuleWithOpts(mf.Path, util.ByteSliceToString(mf.Raw), mpopts); err == nil {
modules[i] = mf
}
return err
})
}
err = g.Wait()
r.metrics.Timer(metrics.RegoModuleParse).Stop()
if err != nil {
return empty, err
}
bundle.Modules = modules
if bundle.Type() == DeltaBundleType {
if len(bundle.Data) != 0 {
return bundle, errors.New("delta bundle expected to contain only patch file but data files found")
return empty, errors.New("delta bundle expected to contain only patch file but data files found")
}
if len(bundle.Modules) != 0 {
return bundle, errors.New("delta bundle expected to contain only patch file but policy files found")
return empty, errors.New("delta bundle expected to contain only patch file but policy files found")
}
if len(bundle.WasmModules) != 0 {
return bundle, errors.New("delta bundle expected to contain only patch file but wasm files found")
return empty, errors.New("delta bundle expected to contain only patch file but wasm files found")
}
if r.persist {
return bundle, errors.New("'persist' property is true in config. persisting delta bundle to disk is not supported")
return empty, errors.New(
"'persist' property is true in config. persisting delta bundle to disk is not supported")
}
}
// check if the bundle signatures specify any files that weren't found in the bundle
if bundle.Type() == SnapshotBundleType && len(r.files) != 0 {
extra := []string{}
for k := range r.files {
extra = append(extra, k)
}
return bundle, fmt.Errorf("file(s) %v specified in bundle signatures but not found in the target bundle", extra)
return empty, fmt.Errorf(
"file(s) %v specified in bundle signatures but not found in the target bundle", util.Keys(r.files))
}
if err := bundle.Manifest.validateAndInjectDefaults(bundle); err != nil {
return bundle, err
if err := bundle.Manifest.validateAndInjectDefaults(*bundle); err != nil {
return empty, err
}
// Inject the wasm module entrypoint refs into the WasmModuleFile structs
@@ -812,36 +817,33 @@ func (r *Reader) Read() (Bundle, error) {
for _, entrypoint := range entrypoints {
ref, err := ast.PtrRef(ast.DefaultRootDocument, entrypoint)
if err != nil {
return bundle, fmt.Errorf("failed to parse wasm module entrypoint '%s': %s", entrypoint, err)
return empty, fmt.Errorf("failed to parse wasm module entrypoint '%s': %s", entrypoint, err)
}
bundle.WasmModules[i].Entrypoints = append(bundle.WasmModules[i].Entrypoints, ref)
}
}
if r.includeManifestInData {
var metadata map[string]any
b, err := json.Marshal(&bundle.Manifest)
if err != nil {
return bundle, fmt.Errorf("bundle load failed on manifest marshal: %w", err)
return empty, fmt.Errorf("bundle load failed on manifest marshal: %w", err)
}
err = util.UnmarshalJSON(b, &metadata)
if err != nil {
return bundle, fmt.Errorf("bundle load failed on manifest unmarshal: %w", err)
var metadata map[string]any
if err := util.UnmarshalJSON(b, &metadata); err != nil {
return empty, fmt.Errorf("bundle load failed on manifest unmarshal: %w", err)
}
// For backwards compatibility always write to the old unnamed manifest path
// This will *not* be correct if >1 bundle is in use...
if err := bundle.insertData(legacyManifestStoragePath, metadata); err != nil {
return bundle, fmt.Errorf("bundle load failed on %v: %w", legacyRevisionStoragePath, err)
return empty, fmt.Errorf("bundle load failed on %v: %w", legacyRevisionStoragePath, err)
}
}
bundle.Etag = r.etag
bundle.Raw = raw
return bundle, nil
return *bundle, nil
}
func (r *Reader) isFileExcluded(path string) bool {
@@ -869,10 +871,9 @@ func (r *Reader) checkSignaturesAndDescriptors(signatures SignaturesConfig) erro
}
// verify the JWT signatures included in the `.signatures.json` file
if err := r.verifyBundleSignature(signatures); err != nil {
return err
}
return r.verifyBundleSignature(signatures)
}
return nil
}
@@ -931,19 +932,10 @@ func (w *Writer) DisableFormat(yes bool) *Writer {
// Write writes the bundle to the writer's output stream.
func (w *Writer) Write(bundle Bundle) error {
gw := gzip.NewWriter(w.w)
tw := tar.NewWriter(gw)
tw := archive.NewTarGzWriter(w.w)
bundleType := bundle.Type()
if bundleType == SnapshotBundleType {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bundle.Data); err != nil {
return err
}
if err := archive.WriteFile(tw, "data.json", buf.Bytes()); err != nil {
if bundle.Type() == SnapshotBundleType {
if err := tw.WriteJSONFile("/data.json", bundle.Data); err != nil {
return err
}
@@ -953,7 +945,7 @@ func (w *Writer) Write(bundle Bundle) error {
path = module.Path
}
if err := archive.WriteFile(tw, path, module.Raw); err != nil {
if err := tw.WriteFile(util.WithPrefix(path, "/"), module.Raw); err != nil {
return err
}
}
@@ -969,55 +961,48 @@ func (w *Writer) Write(bundle Bundle) error {
if err := w.writePlan(tw, bundle); err != nil {
return err
}
} else if bundleType == DeltaBundleType {
if err := writePatch(tw, bundle); err != nil {
} else if bundle.Type() == DeltaBundleType {
if err := tw.WriteJSONFile("/patch.json", bundle.Patch); err != nil {
return err
}
}
if err := writeManifest(tw, bundle); err != nil {
return err
if !bundle.Manifest.Empty() {
if err := tw.WriteJSONFile("/.manifest", bundle.Manifest); err != nil {
return err
}
}
if err := tw.Close(); err != nil {
return err
}
return gw.Close()
return tw.Close()
}
func (w *Writer) writeWasm(tw *tar.Writer, bundle Bundle) error {
func (w *Writer) writeWasm(tw *archive.TarGzWriter, bundle Bundle) error {
for _, wm := range bundle.WasmModules {
path := wm.URL
if w.usePath {
path = wm.Path
}
err := archive.WriteFile(tw, path, wm.Raw)
if err != nil {
if err := tw.WriteFile(util.WithPrefix(path, "/"), wm.Raw); err != nil {
return err
}
}
if len(bundle.Wasm) > 0 {
err := archive.WriteFile(tw, "/"+WasmFile, bundle.Wasm)
if err != nil {
return err
}
if len(bundle.Wasm) == 0 {
return nil
}
return nil
return tw.WriteFile(util.WithPrefix(WasmFile, "/"), bundle.Wasm)
}
func (w *Writer) writePlan(tw *tar.Writer, bundle Bundle) error {
func (w *Writer) writePlan(tw *archive.TarGzWriter, bundle Bundle) error {
for _, wm := range bundle.PlanModules {
path := wm.URL
if w.usePath {
path = wm.Path
}
err := archive.WriteFile(tw, path, wm.Raw)
if err != nil {
if err := tw.WriteFile(util.WithPrefix(path, "/"), wm.Raw); err != nil {
return err
}
}
@@ -1025,34 +1010,7 @@ func (w *Writer) writePlan(tw *tar.Writer, bundle Bundle) error {
return nil
}
func writeManifest(tw *tar.Writer, bundle Bundle) error {
if bundle.Manifest.Empty() {
return nil
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bundle.Manifest); err != nil {
return err
}
return archive.WriteFile(tw, ManifestExt, buf.Bytes())
}
func writePatch(tw *tar.Writer, bundle Bundle) error {
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(bundle.Patch); err != nil {
return err
}
return archive.WriteFile(tw, patchFile, buf.Bytes())
}
func writeSignatures(tw *tar.Writer, bundle Bundle) error {
func writeSignatures(tw *archive.TarGzWriter, bundle Bundle) error {
if bundle.Signatures.isEmpty() {
return nil
}
@@ -1062,7 +1020,7 @@ func writeSignatures(tw *tar.Writer, bundle Bundle) error {
return err
}
return archive.WriteFile(tw, fmt.Sprintf(".%v", SignaturesFile), bs)
return tw.WriteFile(util.WithPrefix(SignaturesFile, "/."), bs)
}
func hashBundleFiles(hash SignatureHasher, b *Bundle) ([]FileInfo, error) {
@@ -1115,8 +1073,7 @@ func hashBundleFiles(hash SignatureHasher, b *Bundle) ([]FileInfo, error) {
return files, err
}
bs, err = hash.HashFile(result)
if err != nil {
if bs, err = hash.HashFile(result); err != nil {
return files, err
}
@@ -1227,10 +1184,6 @@ func (b *Bundle) GenerateSignature(signingConfig *SigningConfig, keyID string, u
return err
}
if b.Signatures.isEmpty() {
b.Signatures = SignaturesConfig{}
}
if signingConfig.Plugin != "" {
b.Signatures.Plugin = signingConfig.Plugin
}
@@ -1243,7 +1196,6 @@ func (b *Bundle) GenerateSignature(signingConfig *SigningConfig, keyID string, u
// ParsedModules returns a map of parsed modules with names that are
// unique and human readable for the given a bundle name.
func (b *Bundle) ParsedModules(bundleName string) map[string]*ast.Module {
mods := make(map[string]*ast.Module, len(b.Modules))
for _, mf := range b.Modules {
@@ -1255,9 +1207,10 @@ func (b *Bundle) ParsedModules(bundleName string) map[string]*ast.Module {
func (b *Bundle) RegoVersion(def ast.RegoVersion) ast.RegoVersion {
if v := b.Manifest.RegoVersion; v != nil {
if *v == 0 {
switch *v {
case 0:
return ast.RegoV0
} else if *v == 1 {
case 1:
return ast.RegoV1
}
}
@@ -1328,10 +1281,6 @@ func (m *Manifest) numericRegoVersionForFile(path string) (*int, error) {
// Equal returns true if this bundle's contents equal the other bundle's
// contents.
func (b Bundle) Equal(other Bundle) bool {
if !reflect.DeepEqual(b.Data, other.Data) {
return false
}
if len(b.Modules) != len(other.Modules) {
return false
}
@@ -1357,6 +1306,10 @@ func (b Bundle) Equal(other Bundle) bool {
return false
}
if !reflect.DeepEqual(b.Data, other.Data) {
return false
}
return bytes.Equal(b.Wasm, other.Wasm)
}
@@ -1487,7 +1440,6 @@ func Merge(bundles []*Bundle) (*Bundle, error) {
// If usePath is true, per-file rego-versions will be calculated using the file's ModuleFile.Path; otherwise, the file's
// ModuleFile.URL will be used.
func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePath bool) (*Bundle, error) {
if len(bundles) == 0 {
return nil, errors.New("expected at least one bundle")
}
@@ -1512,7 +1464,6 @@ func MergeWithRegoVersion(bundles []*Bundle, regoVersion ast.RegoVersion, usePat
var result Bundle
for _, b := range bundles {
if b.Manifest.Roots == nil {
return nil, errors.New("bundle manifest not initialized")
}
@@ -1607,16 +1558,11 @@ func bundleRelativePath(m ModuleFile, usePath bool) string {
}
func bundleAbsolutePath(m ModuleFile, usePath bool) string {
var p string
p := m.URL
if usePath {
p = m.Path
} else {
p = m.URL
}
if !path.IsAbs(p) {
p = "/" + p
}
return path.Clean(p)
return path.Clean(util.WithPrefix(p, "/"))
}
// RootPathsOverlap takes in two bundle root paths and returns true if they overlap.
@@ -1642,7 +1588,6 @@ func rootPathSegments(path string) []string {
}
func rootContains(root []string, other []string) bool {
// A single segment, empty string root always contains the other.
if len(root) == 1 && root[0] == "" {
return true
@@ -1674,7 +1619,7 @@ func getNormalizedPath(path string) []string {
// other hand, if the path is empty, filepath.Dir will return '.'.
// Note: filepath.Dir can return paths with '\' separators, always use
// filepath.ToSlash to keep them normalized.
dirpath := strings.TrimLeft(normalizePath(filepath.Dir(path)), "/.")
dirpath := strings.TrimLeft(filepath.ToSlash(filepath.Dir(path)), "/.")
var key []string
if dirpath != "" {
key = strings.Split(dirpath, "/")
@@ -1701,56 +1646,52 @@ func dfs(value any, path string, fn func(string, any) (bool, error)) error {
}
func modulePathWithPrefix(bundleName string, modulePath string) string {
// Default prefix is just the bundle name
prefix := bundleName
// Bundle names are sometimes just file paths, some of which
// are full urls (file:///foo/). Parse these and only use the path.
parsed, err := url.Parse(bundleName)
if err == nil {
prefix = filepath.Join(parsed.Host, parsed.Path)
return path.Join(parsed.Host, parsed.Path, modulePath)
}
// Note: filepath.Join can return paths with '\' separators, always use
// filepath.ToSlash to keep them normalized.
return normalizePath(filepath.Join(prefix, modulePath))
return path.Join(bundleName, modulePath)
}
// IsStructuredDoc checks if the file name equals a structured file extension ex. ".json"
func IsStructuredDoc(name string) bool {
return filepath.Base(name) == dataFile || filepath.Base(name) == yamlDataFile ||
filepath.Base(name) == SignaturesFile || filepath.Base(name) == ManifestExt
base := filepath.Base(name)
return base == dataFile || base == yamlDataFile || base == SignaturesFile || base == ManifestExt
}
func preProcessBundle(loader DirectoryLoader, skipVerify bool, sizeLimitBytes int64) (SignaturesConfig, Patch, []*Descriptor, error) {
func preProcessBundle(loader DirectoryLoader, skipVerify bool, sizeLimitBytes int64) (*Bundle, []*Descriptor, error) {
bundle := &Bundle{}
descriptors := []*Descriptor{}
var signatures SignaturesConfig
var patch Patch
for {
f, err := loader.NextFile()
if err == io.EOF {
break
}
if err != nil {
return signatures, patch, nil, fmt.Errorf("bundle read failed: %w", err)
if err == io.EOF {
break
}
return bundle, nil, fmt.Errorf("bundle read failed: %w", err)
}
// check for the signatures file
if !skipVerify && strings.HasSuffix(f.Path(), SignaturesFile) {
isSignaturesFile := strings.HasSuffix(f.Path(), SignaturesFile)
if !skipVerify && isSignaturesFile {
buf, err := readFile(f, sizeLimitBytes)
if err != nil {
return signatures, patch, nil, err
return bundle, nil, err
}
if err := util.NewJSONDecoder(&buf).Decode(&signatures); err != nil {
return signatures, patch, nil, fmt.Errorf("bundle load failed on signatures decode: %w", err)
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Signatures); err != nil {
return bundle, nil, fmt.Errorf("bundle load failed on signatures decode: %w", err)
}
} else if !strings.HasSuffix(f.Path(), SignaturesFile) {
} else if !isSignaturesFile {
descriptors = append(descriptors, f)
if filepath.Base(f.Path()) == patchFile {
base := filepath.Base(f.Path())
if base == patchFile {
var b bytes.Buffer
tee := io.TeeReader(f.reader, &b)
@@ -1758,18 +1699,19 @@ func preProcessBundle(loader DirectoryLoader, skipVerify bool, sizeLimitBytes in
buf, err := readFile(f, sizeLimitBytes)
if err != nil {
return signatures, patch, nil, err
return bundle, nil, err
}
if err := util.NewJSONDecoder(&buf).Decode(&patch); err != nil {
return signatures, patch, nil, fmt.Errorf("bundle load failed on patch decode: %w", err)
if err := util.NewJSONDecoder(&buf).Decode(&bundle.Patch); err != nil {
return bundle, nil, fmt.Errorf("bundle load failed on patch decode: %w", err)
}
f.reader = &b
}
}
}
return signatures, patch, descriptors, nil
return bundle, descriptors, nil
}
func readFile(f *Descriptor, sizeLimitBytes int64) (bytes.Buffer, error) {
@@ -1839,7 +1781,3 @@ func fstatFileSize(f *os.File) (int64, error) {
}
return fileInfo.Size(), nil
}
func normalizePath(p string) string {
return filepath.ToSlash(p)
}
+5 -8
View File
@@ -352,12 +352,10 @@ func (t *tarballLoader) NextFile() (*Descriptor, error) {
for {
header, err := t.tr.Next()
if err == io.EOF {
break
}
if err != nil {
if err == io.EOF {
break
}
return nil, err
}
@@ -365,7 +363,6 @@ func (t *tarballLoader) NextFile() (*Descriptor, error) {
if header.Typeflag == tar.TypeReg {
if t.filter != nil {
if t.filter(filepath.ToSlash(header.Name), header.FileInfo(), getdepth(header.Name, false)) {
continue
}
@@ -504,9 +501,9 @@ func getdepth(path string, isDir bool) int {
}
func getFileStoragePath(path string) (storage.Path, error) {
fpath := strings.TrimLeft(normalizePath(filepath.Dir(path)), "/.")
fpath := strings.TrimLeft(filepath.ToSlash(filepath.Dir(path)), "/.")
if strings.HasSuffix(path, RegoExt) {
fpath = strings.Trim(normalizePath(path), "/")
fpath = strings.Trim(filepath.ToSlash(path), "/")
}
p, ok := storage.ParsePathEscaped("/" + fpath)
-3
View File
@@ -1,6 +1,3 @@
//go:build go1.16
// +build go1.16
package bundle
import (
+1 -2
View File
@@ -14,7 +14,6 @@ import (
"fmt"
"hash"
"io"
"strings"
"github.com/open-policy-agent/opa/v1/util"
)
@@ -132,5 +131,5 @@ func encodePrimitive(v any) []byte {
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
_ = encoder.Encode(v)
return []byte(strings.Trim(buf.String(), "\n"))
return bytes.Trim(buf.Bytes(), "\n")
}
+7 -3
View File
@@ -571,12 +571,11 @@ func doDFS(obj map[string]json.RawMessage, path string, roots []string) error {
}
for key := range obj {
newPath := filepath.Join(strings.Trim(path, "/"), key)
// Note: filepath.Join can return paths with '\' separators, always use
// filepath.ToSlash to keep them normalized.
newPath = strings.TrimLeft(normalizePath(newPath), "/.")
newPath = strings.TrimLeft(filepath.ToSlash(newPath), "/.")
contains := false
prefix := false
@@ -971,7 +970,7 @@ func compileModules(compiler *ast.Compiler, m metrics.Metrics, bundles map[strin
m.Timer(metrics.RegoModuleCompile).Start()
defer m.Timer(metrics.RegoModuleCompile).Stop()
modules := map[string]*ast.Module{}
modules := make(map[string]*ast.Module, len(compiler.Modules)+len(extraModules)+len(bundles))
// preserve any modules already on the compiler
maps.Copy(modules, compiler.Modules)
@@ -1191,17 +1190,20 @@ func applyPatches(ctx context.Context, store storage.Store, txn storage.Transact
// Helpers for the older single (unnamed) bundle style manifest storage.
// 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")
// LegacyWriteManifestToStore will write the bundle manifest to the older single (unnamed) bundle manifest location.
//
// Deprecated: Use WriteManifestToStore and named bundles instead.
func LegacyWriteManifestToStore(ctx context.Context, store storage.Store, txn storage.Transaction, manifest Manifest) error {
return write(ctx, store, txn, legacyManifestStoragePath, manifest)
}
// LegacyEraseManifestFromStore will erase the bundle manifest from the older single (unnamed) bundle manifest location.
//
// Deprecated: Use WriteManifestToStore and named bundles instead.
func LegacyEraseManifestFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) error {
err := store.Write(ctx, txn, storage.RemoveOp, legacyManifestStoragePath, nil)
@@ -1212,12 +1214,14 @@ func LegacyEraseManifestFromStore(ctx context.Context, store storage.Store, txn
}
// LegacyReadRevisionFromStore will read the bundle manifest revision from the older single (unnamed) bundle manifest location.
//
// Deprecated: Use ReadBundleRevisionFromStore and named bundles instead.
func LegacyReadRevisionFromStore(ctx context.Context, store storage.Store, txn storage.Transaction) (string, error) {
return readRevisionFromStore(ctx, store, txn, legacyRevisionStoragePath)
}
// ActivateLegacy calls Activate for the bundles but will also write their manifest to the older unnamed store location.
//
// Deprecated: Use Activate with named bundles instead.
func ActivateLegacy(opts *ActivateOpts) error {
opts.legacy = true
-4
View File
@@ -144,10 +144,6 @@ func verifyJWTSignature(token string, bvc *VerificationConfig) (*DecodedSignatur
// Because we want to fallback to ds.KeyID when we can't find the
// keyID, we need to parse the payload here already.
//
// (lestrrat) Whoa, you're going to trust the payload before you
// verify the signature? Even if it's for backwrds compatibility,
// Is this OK?
decoder := base64.RawURLEncoding
payload := make([]byte, decoder.DecodedLen(len(payloadb64)))
if _, err := decoder.Decode(payload, payloadb64); err != nil {
@@ -2,9 +2,6 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
//go:build go1.16
// +build go1.16
package capabilities
import (
-393
View File
@@ -1,393 +0,0 @@
// Copyright 2018 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 config implements OPA configuration file parsing and validation.
package config
import (
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"github.com/open-policy-agent/opa/internal/ref"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
"github.com/open-policy-agent/opa/v1/version"
)
// ServerConfig represents the different server configuration options.
type ServerConfig struct {
Metrics json.RawMessage `json:"metrics,omitempty"`
Encoding json.RawMessage `json:"encoding,omitempty"`
Decoding json.RawMessage `json:"decoding,omitempty"`
}
// Clone creates a deep copy of ServerConfig.
func (s *ServerConfig) Clone() *ServerConfig {
if s == nil {
return nil
}
clone := &ServerConfig{}
if s.Encoding != nil {
clone.Encoding = make(json.RawMessage, len(s.Encoding))
copy(clone.Encoding, s.Encoding)
}
if s.Decoding != nil {
clone.Decoding = make(json.RawMessage, len(s.Decoding))
copy(clone.Decoding, s.Decoding)
}
if s.Metrics != nil {
clone.Metrics = make(json.RawMessage, len(s.Metrics))
copy(clone.Metrics, s.Metrics)
}
return clone
}
// StorageConfig represents Config's storage options.
type StorageConfig struct {
Disk json.RawMessage `json:"disk,omitempty"`
}
// Clone creates a deep copy of StorageConfig.
func (s *StorageConfig) Clone() *StorageConfig {
if s == nil {
return nil
}
clone := &StorageConfig{}
if s.Disk != nil {
clone.Disk = make(json.RawMessage, len(s.Disk))
copy(clone.Disk, s.Disk)
}
return clone
}
// Config represents the configuration file that OPA can be started with.
type Config struct {
Services json.RawMessage `json:"services,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Discovery json.RawMessage `json:"discovery,omitempty"`
Bundle json.RawMessage `json:"bundle,omitempty"` // Deprecated: Use `bundles` instead
Bundles json.RawMessage `json:"bundles,omitempty"`
DecisionLogs json.RawMessage `json:"decision_logs,omitempty"`
Status json.RawMessage `json:"status,omitempty"`
Plugins map[string]json.RawMessage `json:"plugins,omitempty"`
Keys json.RawMessage `json:"keys,omitempty"`
DefaultDecision *string `json:"default_decision,omitempty"`
DefaultAuthorizationDecision *string `json:"default_authorization_decision,omitempty"`
Caching json.RawMessage `json:"caching,omitempty"`
NDBuiltinCache bool `json:"nd_builtin_cache,omitempty"`
PersistenceDirectory *string `json:"persistence_directory,omitempty"`
DistributedTracing json.RawMessage `json:"distributed_tracing,omitempty"`
Server *ServerConfig `json:"server,omitempty"`
Storage *StorageConfig `json:"storage,omitempty"`
Extra map[string]json.RawMessage `json:"-"`
}
// ParseConfig returns a valid Config object with defaults injected. The id
// and version parameters will be set in the labels map.
func ParseConfig(raw []byte, id string) (*Config, error) {
// NOTE(sr): based on https://stackoverflow.com/a/33499066/993018
var result Config
objValue := reflect.ValueOf(&result).Elem()
knownFields := map[string]reflect.Value{}
for i := 0; i != objValue.NumField(); i++ {
jsonName := strings.Split(objValue.Type().Field(i).Tag.Get("json"), ",")[0]
knownFields[jsonName] = objValue.Field(i)
}
if err := util.Unmarshal(raw, &result.Extra); err != nil {
return nil, err
}
for key, chunk := range result.Extra {
if field, found := knownFields[key]; found {
if err := util.Unmarshal(chunk, field.Addr().Interface()); err != nil {
return nil, err
}
delete(result.Extra, key)
}
}
if len(result.Extra) == 0 {
result.Extra = nil
}
return &result, result.validateAndInjectDefaults(id)
}
// PluginNames returns a sorted list of names of enabled plugins.
func (c Config) PluginNames() (result []string) {
if c.Bundle != nil || c.Bundles != nil {
result = append(result, "bundles")
}
if c.Status != nil {
result = append(result, "status")
}
if c.DecisionLogs != nil {
result = append(result, "decision_logs")
}
for name := range c.Plugins {
result = append(result, name)
}
sort.Strings(result)
return result
}
// PluginsEnabled returns true if one or more plugin features are enabled.
//
// Deprecated: Use PluginNames instead.
func (c Config) PluginsEnabled() bool {
return c.Bundle != nil || c.Bundles != nil || c.DecisionLogs != nil || c.Status != nil || len(c.Plugins) > 0
}
// DefaultDecisionRef returns the default decision as a reference.
func (c Config) DefaultDecisionRef() ast.Ref {
r, _ := ref.ParseDataPath(*c.DefaultDecision)
return r
}
// DefaultAuthorizationDecisionRef returns the default authorization decision
// as a reference.
func (c Config) DefaultAuthorizationDecisionRef() ast.Ref {
r, _ := ref.ParseDataPath(*c.DefaultAuthorizationDecision)
return r
}
// NDBuiltinCacheEnabled returns if the ND builtins cache should be used.
func (c Config) NDBuiltinCacheEnabled() bool {
return c.NDBuiltinCache
}
// GetPersistenceDirectory returns the configured persistence directory, or $PWD/.opa if none is configured
func (c Config) GetPersistenceDirectory() (string, error) {
if c.PersistenceDirectory == nil {
pwd, err := os.Getwd()
if err != nil {
return "", err
}
return filepath.Join(pwd, ".opa"), nil
}
return *c.PersistenceDirectory, nil
}
// ActiveConfig returns OPA's active configuration
// with the credentials and crypto keys removed
func (c *Config) ActiveConfig() (any, error) {
bs, err := json.Marshal(c)
if err != nil {
return nil, err
}
var result map[string]any
if err := util.UnmarshalJSON(bs, &result); err != nil {
return nil, err
}
for k, e := range c.Extra {
var v any
if err := util.UnmarshalJSON(e, &v); err != nil {
return nil, err
}
result[k] = v
}
if err := removeServiceCredentials(result["services"]); err != nil {
return nil, err
}
if err := removeCryptoKeys(result["keys"]); err != nil {
return nil, err
}
return result, nil
}
// Clone creates a deep copy of the Config struct
func (c *Config) Clone() *Config {
if c == nil {
return nil
}
clone := &Config{
NDBuiltinCache: c.NDBuiltinCache,
Server: c.Server.Clone(),
Storage: c.Storage.Clone(),
Labels: maps.Clone(c.Labels),
}
if c.Services != nil {
clone.Services = make(json.RawMessage, len(c.Services))
copy(clone.Services, c.Services)
}
if c.Discovery != nil {
clone.Discovery = make(json.RawMessage, len(c.Discovery))
copy(clone.Discovery, c.Discovery)
}
if c.Bundle != nil {
clone.Bundle = make(json.RawMessage, len(c.Bundle))
copy(clone.Bundle, c.Bundle)
}
if c.Bundles != nil {
clone.Bundles = make(json.RawMessage, len(c.Bundles))
copy(clone.Bundles, c.Bundles)
}
if c.DecisionLogs != nil {
clone.DecisionLogs = make(json.RawMessage, len(c.DecisionLogs))
copy(clone.DecisionLogs, c.DecisionLogs)
}
if c.Status != nil {
clone.Status = make(json.RawMessage, len(c.Status))
copy(clone.Status, c.Status)
}
if c.Keys != nil {
clone.Keys = make(json.RawMessage, len(c.Keys))
copy(clone.Keys, c.Keys)
}
if c.Caching != nil {
clone.Caching = make(json.RawMessage, len(c.Caching))
copy(clone.Caching, c.Caching)
}
if c.DistributedTracing != nil {
clone.DistributedTracing = make(json.RawMessage, len(c.DistributedTracing))
copy(clone.DistributedTracing, c.DistributedTracing)
}
if c.DefaultDecision != nil {
s := *c.DefaultDecision
clone.DefaultDecision = &s
}
if c.DefaultAuthorizationDecision != nil {
s := *c.DefaultAuthorizationDecision
clone.DefaultAuthorizationDecision = &s
}
if c.PersistenceDirectory != nil {
s := *c.PersistenceDirectory
clone.PersistenceDirectory = &s
}
if c.Plugins != nil {
clone.Plugins = make(map[string]json.RawMessage, len(c.Plugins))
for k, v := range c.Plugins {
if v != nil {
clone.Plugins[k] = make(json.RawMessage, len(v))
copy(clone.Plugins[k], v)
}
}
}
if c.Extra != nil {
clone.Extra = make(map[string]json.RawMessage, len(c.Extra))
for k, v := range c.Extra {
if v != nil {
clone.Extra[k] = make(json.RawMessage, len(v))
copy(clone.Extra[k], v)
}
}
}
return clone
}
func (c *Config) validateAndInjectDefaults(id string) error {
if c.DefaultDecision == nil {
s := defaultDecisionPath
c.DefaultDecision = &s
}
_, err := ref.ParseDataPath(*c.DefaultDecision)
if err != nil {
return err
}
if c.DefaultAuthorizationDecision == nil {
s := defaultAuthorizationDecisionPath
c.DefaultAuthorizationDecision = &s
}
_, err = ref.ParseDataPath(*c.DefaultAuthorizationDecision)
if err != nil {
return err
}
if c.Labels == nil {
c.Labels = map[string]string{}
}
c.Labels["id"] = id
c.Labels["version"] = version.Version
return nil
}
func removeServiceCredentials(x any) error {
switch x := x.(type) {
case nil:
return nil
case []any:
for _, v := range x {
err := removeKey(v, "credentials")
if err != nil {
return err
}
}
case map[string]any:
for _, v := range x {
err := removeKey(v, "credentials")
if err != nil {
return err
}
}
default:
return fmt.Errorf("illegal service config type: %T", x)
}
return nil
}
func removeCryptoKeys(x any) error {
switch x := x.(type) {
case nil:
return nil
case map[string]any:
for _, v := range x {
err := removeKey(v, "key", "private_key")
if err != nil {
return err
}
}
default:
return fmt.Errorf("illegal keys config type: %T", x)
}
return nil
}
func removeKey(x any, keys ...string) error {
val, ok := x.(map[string]any)
if !ok {
return errors.New("type assertion error")
}
for _, key := range keys {
delete(val, key)
}
return nil
}
const (
defaultDecisionPath = "/system/main"
defaultAuthorizationDecisionPath = "/system/authz/allow"
)
+93 -7
View File
@@ -9,7 +9,6 @@ import (
"bytes"
"errors"
"fmt"
"regexp"
"slices"
"sort"
"strings"
@@ -27,11 +26,8 @@ import (
const defaultLocationFile = "__format_default__"
var (
elseVar ast.Value = ast.Var("else")
expandedConst = ast.NewBody(ast.NewExpr(ast.InternedTerm(true)))
commentsSlicePool = util.NewSlicePool[*ast.Comment](50)
varRegexp = regexp.MustCompile("^[[:alpha:]_][[:alpha:][:digit:]_]*$")
)
// Opts lets you control the code formatting via `AstWithOpts()`.
@@ -732,7 +728,7 @@ func (w *writer) writeElse(rule *ast.Rule, comments []*ast.Comment) ([]*ast.Comm
rule.Else.Head.Name = "else" // NOTE(sr): whaaat
elseHeadReference := ast.NewTerm(elseVar) // construct a reference for the term
elseHeadReference := ast.VarTerm("else") // construct a reference for the term
elseHeadReference.Location = rule.Else.Head.Location // and set the location to match the rule location
rule.Else.Head.Reference = ast.Ref{elseHeadReference}
@@ -1284,6 +1280,11 @@ func (w *writer) writeTermParens(parens bool, term *ast.Term, comments []*ast.Co
}
}
case *ast.TemplateString:
comments, err = w.writeTemplateString(x, comments)
if err != nil {
return nil, err
}
case ast.Var:
w.write(w.formatVar(x))
case ast.Call:
@@ -1301,6 +1302,91 @@ func (w *writer) writeTermParens(parens bool, term *ast.Term, comments []*ast.Co
return comments, nil
}
func (w *writer) writeTemplateString(ts *ast.TemplateString, comments []*ast.Comment) ([]*ast.Comment, error) {
w.write("$")
if ts.MultiLine {
w.write("`")
} else {
w.write(`"`)
}
for i, p := range ts.Parts {
switch x := p.(type) {
case *ast.Expr:
w.write("{")
w.up()
if w.beforeEnd != nil {
// We have a comment on the same line as the opening template-expression brace '{'
w.endLine()
w.startLine()
} else {
// We might have comments to write; the first of which should be on the same line as the opening template-expression brace '{'
before, _, _ := partitionComments(comments, x.Location)
if len(before) > 0 {
w.write(" ")
w.inline = true
if err := w.writeComments(before); err != nil {
return nil, err
}
comments = comments[len(before):]
}
}
var err error
comments, err = w.writeExpr(x, comments)
if err != nil {
return comments, err
}
// write trailing comments
if i+1 < len(ts.Parts) {
before, _, _ := partitionComments(comments, ts.Parts[i+1].Loc())
if len(before) > 0 {
w.endLine()
if err := w.writeComments(before); err != nil {
return nil, err
}
comments = comments[len(before):]
w.startLine()
}
}
w.write("}")
if err := w.down(); err != nil {
return nil, err
}
case *ast.Term:
if s, ok := x.Value.(ast.String); ok {
if ts.MultiLine {
w.write(ast.EscapeTemplateStringStringPart(string(s)))
} else {
str := ast.EscapeTemplateStringStringPart(s.String())
w.write(str[1 : len(str)-1])
}
} else {
s := x.String()
s = strings.TrimPrefix(s, "\"")
s = strings.TrimSuffix(s, "\"")
w.write(s)
}
default:
w.write("<invalid>")
}
}
if ts.MultiLine {
w.write("`")
} else {
w.write(`"`)
}
return comments, nil
}
func (w *writer) writeRef(x ast.Ref, comments []*ast.Comment) ([]*ast.Comment, error) {
if len(x) > 0 {
parens := false
@@ -1353,7 +1439,7 @@ func (w *writer) writeRefStringPath(s ast.String, l *ast.Location) {
}
func (w *writer) shouldBracketRefTerm(s string, l *ast.Location) bool {
if !varRegexp.MatchString(s) {
if !ast.IsVarCompatibleString(s) {
return true
}
@@ -1931,7 +2017,7 @@ func partitionComments(comments []*ast.Comment, l *ast.Location) ([]*ast.Comment
var at *ast.Comment
before := make([]*ast.Comment, 0, numBefore)
after := comments[0 : 0 : len(comments)-numBefore]
after := make([]*ast.Comment, 0, numAfter)
for _, c := range comments {
switch cmp := c.Location.Row - l.Row; {
-97
View File
@@ -1,97 +0,0 @@
// Copyright 2023 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 hooks
import (
"context"
"fmt"
"github.com/open-policy-agent/opa/v1/config"
topdown_cache "github.com/open-policy-agent/opa/v1/topdown/cache"
)
// Hook is a hook to be called in some select places in OPA's operation.
//
// The base Hook interface is any, and wherever a hook can occur, the calling code
// will check if your hook implements an appropriate interface. If so, your hook
// is called.
//
// This allows you to only hook in to behavior you care about, and it allows the
// OPA to add more hooks in the future.
//
// All hook interfaces in this package have Hook in the name. Hooks must be safe
// for concurrent use. It is expected that hooks are fast; if a hook needs to take
// time, then copy what you need and ensure the hook is async.
//
// When multiple instances of a hook are provided, they are all going to be executed
// in an unspecified order (it's a map-range call underneath). If you need hooks to
// be run in order, you can wrap them into another hook, and configure that one.
type Hook any
// Hooks is the type used for every struct in OPA that can work with hooks.
type Hooks struct {
m map[Hook]struct{} // we are NOT providing a stable invocation ordering
}
// New creates a new instance of Hooks.
func New(hs ...Hook) Hooks {
h := Hooks{m: make(map[Hook]struct{}, len(hs))}
for i := range hs {
h.m[hs[i]] = struct{}{}
}
return h
}
func (hs Hooks) Each(fn func(Hook)) {
for h := range hs.m {
fn(h)
}
}
func (hs Hooks) Len() int {
return len(hs.m)
}
// ConfigHook allows inspecting or rewriting the configuration when the plugin
// manager is processing it.
// Note that this hook is not run when the plugin manager is reconfigured. This
// usually only happens when there's a new config from a discovery bundle, and
// for processing _that_, there's `ConfigDiscoveryHook`.
type ConfigHook interface {
OnConfig(context.Context, *config.Config) (*config.Config, error)
}
// ConfigHook allows inspecting or rewriting the discovered configuration when
// the discovery plugin is processing it.
type ConfigDiscoveryHook interface {
OnConfigDiscovery(context.Context, *config.Config) (*config.Config, error)
}
// InterQueryCacheHook allows access to the server's inter-query cache instance.
// It's useful for out-of-tree handlers that also need to evaluate something.
// Using this hook, they can share the caches with the rest of OPA.
type InterQueryCacheHook interface {
OnInterQueryCache(context.Context, topdown_cache.InterQueryCache) error
}
// InterQueryValueCacheHook allows access to the server's inter-query value cache
// instance.
type InterQueryValueCacheHook interface {
OnInterQueryValueCache(context.Context, topdown_cache.InterQueryValueCache) error
}
func (hs Hooks) Validate() error {
for h := range hs.m {
switch h.(type) {
case InterQueryCacheHook,
InterQueryValueCacheHook,
ConfigHook,
ConfigDiscoveryHook: // OK
default:
return fmt.Errorf("unknown hook type %T", h)
}
}
return nil
}
+4 -2
View File
@@ -495,6 +495,7 @@ func loadOneSchema(path string) (any, error) {
}
// All returns a Result object loaded (recursively) from the specified paths.
//
// Deprecated: Use FileLoader.Filtered() instead.
func All(paths []string) (*Result, error) {
return NewFileLoader().Filtered(paths, nil)
@@ -503,6 +504,7 @@ func All(paths []string) (*Result, error) {
// Filtered returns a Result object loaded (recursively) from the specified
// paths while applying the given filters. If any filter returns true, the
// file/directory is excluded.
//
// Deprecated: Use FileLoader.Filtered() instead.
func Filtered(paths []string, filter Filter) (*Result, error) {
return NewFileLoader().Filtered(paths, filter)
@@ -511,6 +513,7 @@ func Filtered(paths []string, filter Filter) (*Result, error) {
// AsBundle loads a path as a bundle. If it is a single file
// it will be treated as a normal tarball bundle. If a directory
// is supplied it will be loaded as an unzipped bundle tree.
//
// Deprecated: Use FileLoader.AsBundle() instead.
func AsBundle(path string) (*bundle.Bundle, error) {
return NewFileLoader().AsBundle(path)
@@ -631,11 +634,10 @@ func (l *Result) mergeDocument(path string, doc any) error {
}
func (l *Result) withParent(p string) *Result {
path := append(l.path, p)
return &Result{
Documents: l.Documents,
Modules: l.Modules,
path: path,
path: append(l.path, p),
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-287
View File
@@ -1,287 +0,0 @@
package rest
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"time"
)
var (
azureIMDSEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
defaultAPIVersion = "2018-02-01"
defaultResource = "https://storage.azure.com/"
timeout = 5 * time.Second
defaultAPIVersionForAppServiceMsi = "2019-08-01"
defaultKeyVaultAPIVersion = "7.4"
)
// azureManagedIdentitiesToken holds a token for managed identities for Azure resources
type azureManagedIdentitiesToken struct {
AccessToken string `json:"access_token"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
NotBefore string `json:"not_before"`
Resource string `json:"resource"`
TokenType string `json:"token_type"`
}
// azureManagedIdentitiesError represents an error fetching an azureManagedIdentitiesToken
type azureManagedIdentitiesError struct {
Err string `json:"error"`
Description string `json:"error_description"`
Endpoint string
StatusCode int
}
func (e *azureManagedIdentitiesError) Error() string {
return fmt.Sprintf("%v %s retrieving azure token from %s: %s", e.StatusCode, e.Err, e.Endpoint, e.Description)
}
// azureManagedIdentitiesAuthPlugin uses an azureManagedIdentitiesToken.AccessToken for bearer authorization
type azureManagedIdentitiesAuthPlugin struct {
Endpoint string `json:"endpoint"`
APIVersion string `json:"api_version"`
Resource string `json:"resource"`
ObjectID string `json:"object_id"`
ClientID string `json:"client_id"`
MiResID string `json:"mi_res_id"`
UseAppServiceMsi bool `json:"use_app_service_msi,omitempty"`
}
func (ap *azureManagedIdentitiesAuthPlugin) setDefaults() {
if ap.Endpoint == "" {
identityEndpoint := os.Getenv("IDENTITY_ENDPOINT")
if identityEndpoint != "" {
ap.UseAppServiceMsi = true
ap.Endpoint = identityEndpoint
} else {
ap.Endpoint = azureIMDSEndpoint
}
}
if ap.Resource == "" {
ap.Resource = defaultResource
}
if ap.APIVersion == "" {
if ap.UseAppServiceMsi {
ap.APIVersion = defaultAPIVersionForAppServiceMsi
} else {
ap.APIVersion = defaultAPIVersion
}
}
}
func (ap *azureManagedIdentitiesAuthPlugin) NewClient(c Config) (*http.Client, error) {
if c.Type == "oci" {
return nil, errors.New("azure managed identities auth: OCI service not supported")
}
ap.setDefaults()
t, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
func (ap *azureManagedIdentitiesAuthPlugin) Prepare(req *http.Request) error {
token, err := azureManagedIdentitiesTokenRequest(
ap.Endpoint, ap.APIVersion, ap.Resource,
ap.ObjectID, ap.ClientID, ap.MiResID,
ap.UseAppServiceMsi,
)
if err != nil {
return err
}
req.Header.Add("Authorization", "Bearer "+token.AccessToken)
return nil
}
// azureManagedIdentitiesTokenRequest fetches an azureManagedIdentitiesToken
func azureManagedIdentitiesTokenRequest(
endpoint, apiVersion, resource, objectID, clientID, miResID string,
useAppServiceMsi bool,
) (azureManagedIdentitiesToken, error) {
var token azureManagedIdentitiesToken
e := buildAzureManagedIdentitiesRequestPath(endpoint, apiVersion, resource, objectID, clientID, miResID)
request, err := http.NewRequest("GET", e, nil)
if err != nil {
return token, err
}
if useAppServiceMsi {
identityHeader := os.Getenv("IDENTITY_HEADER")
if identityHeader == "" {
return token, errors.New("azure managed identities auth: IDENTITY_HEADER env var not found")
}
request.Header.Add("x-identity-header", identityHeader)
} else {
request.Header.Add("Metadata", "true")
}
httpClient := http.Client{Timeout: timeout}
response, err := httpClient.Do(request)
if err != nil {
return token, err
}
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return token, err
}
if s := response.StatusCode; s != http.StatusOK {
var azureError azureManagedIdentitiesError
err = json.Unmarshal(data, &azureError)
if err != nil {
return token, err
}
azureError.Endpoint = e
azureError.StatusCode = s
return token, &azureError
}
err = json.Unmarshal(data, &token)
if err != nil {
return token, err
}
return token, nil
}
// buildAzureManagedIdentitiesRequestPath constructs the request URL for an Azure managed identities token request
func buildAzureManagedIdentitiesRequestPath(
endpoint, apiVersion, resource, objectID, clientID, miResID string,
) string {
params := url.Values{
"api-version": []string{apiVersion},
"resource": []string{resource},
}
if objectID != "" {
params.Add("object_id", objectID)
}
if clientID != "" {
params.Add("client_id", clientID)
}
if miResID != "" {
params.Add("mi_res_id", miResID)
}
return endpoint + "?" + params.Encode()
}
type azureKeyVaultSignPlugin struct {
config azureKeyVaultConfig
tokener func() (string, error)
}
func newKeyVaultSignPlugin(ap *azureManagedIdentitiesAuthPlugin, cfg *azureKeyVaultConfig) *azureKeyVaultSignPlugin {
resp := &azureKeyVaultSignPlugin{
tokener: func() (string, error) {
resp, err := azureManagedIdentitiesTokenRequest(
ap.Endpoint,
ap.APIVersion,
cfg.URL.String(),
ap.ObjectID,
ap.ClientID,
ap.MiResID,
ap.UseAppServiceMsi)
if err != nil {
return "", err
}
return resp.AccessToken, nil
},
config: *cfg,
}
return resp
}
func (akv *azureKeyVaultSignPlugin) setDefaults() {
if akv.config.APIVersion == "" {
akv.config.APIVersion = defaultKeyVaultAPIVersion
}
}
type kvRequest struct {
Alg string `json:"alg"`
Value string `json:"value"`
}
type kvResponse struct {
KID string `json:"kid"`
Value string `json:"value"`
}
// SignDigest() uses the Microsoft keyvault rest api to sign a byte digest
// https://learn.microsoft.com/en-us/rest/api/keyvault/keys/sign/sign
func (ap *azureKeyVaultSignPlugin) SignDigest(ctx context.Context, digest []byte) (string, error) {
tkn, err := ap.tokener()
if err != nil {
return "", err
}
if ap.config.URL.Host == "" {
return "", errors.New("keyvault host not set")
}
signingURL := ap.config.URL.JoinPath("keys", ap.config.Key, ap.config.KeyVersion, "sign")
q := signingURL.Query()
q.Set("api-version", ap.config.APIVersion)
signingURL.RawQuery = q.Encode()
reqBody, err := json.Marshal(kvRequest{
Alg: ap.config.Alg,
Value: base64.StdEncoding.EncodeToString(digest)})
if err != nil {
return "", err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, signingURL.String(), bytes.NewBuffer(reqBody))
if err != nil {
return "", err
}
req.Header.Add("Authorization", "Bearer "+tkn)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
if resp.Body != nil {
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return "", fmt.Errorf("non 200 status code, got: %d. Body: %v", resp.StatusCode, string(b))
}
return "", fmt.Errorf("non 200 status code from keyvault sign, got: %d", resp.StatusCode)
}
defer resp.Body.Close()
respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return "", errors.New("failed to read keyvault response body")
}
var res kvResponse
err = json.Unmarshal(respBytes, &res)
if err != nil {
return "", fmt.Errorf("no valid keyvault response, got: %v", string(respBytes))
}
return res.Value, nil
}
-173
View File
@@ -1,173 +0,0 @@
// Copyright 2020 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 rest
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
)
var (
defaultGCPMetadataEndpoint = "http://metadata.google.internal"
defaultAccessTokenPath = "/computeMetadata/v1/instance/service-accounts/default/token"
defaultIdentityTokenPath = "/computeMetadata/v1/instance/service-accounts/default/identity"
)
// AccessToken holds a GCP access token.
type AccessToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
TokenType string `json:"token_type"`
}
type gcpMetadataError struct {
err error
endpoint string
statusCode int
}
func (e *gcpMetadataError) Error() string {
return fmt.Sprintf("error retrieving gcp ID token from %s %d: %v", e.endpoint, e.statusCode, e.err)
}
func (e *gcpMetadataError) Unwrap() error { return e.err }
var (
errGCPMetadataNotFound = errors.New("not found")
errGCPMetadataInvalidRequest = errors.New("invalid request")
errGCPMetadataUnexpected = errors.New("unexpected error")
)
// gcpMetadataAuthPlugin represents authentication via GCP metadata service.
type gcpMetadataAuthPlugin struct {
AccessTokenPath string `json:"access_token_path"`
Audience string `json:"audience"`
Endpoint string `json:"endpoint"`
IdentityTokenPath string `json:"identity_token_path"`
Scopes []string `json:"scopes"`
}
func (ap *gcpMetadataAuthPlugin) NewClient(c Config) (*http.Client, error) {
if ap.Audience == "" && len(ap.Scopes) == 0 {
return nil, errors.New("audience or scopes is required when gcp metadata is enabled")
}
if ap.Audience != "" && len(ap.Scopes) > 0 {
return nil, errors.New("either audience or scopes can be set, not both, when gcp metadata is enabled")
}
if ap.Endpoint == "" {
ap.Endpoint = defaultGCPMetadataEndpoint
}
if ap.AccessTokenPath == "" {
ap.AccessTokenPath = defaultAccessTokenPath
}
if ap.IdentityTokenPath == "" {
ap.IdentityTokenPath = defaultIdentityTokenPath
}
t, err := DefaultTLSConfig(c)
if err != nil {
return nil, err
}
return DefaultRoundTripperClient(t, *c.ResponseHeaderTimeoutSeconds), nil
}
func (ap *gcpMetadataAuthPlugin) Prepare(req *http.Request) error {
var err error
var token string
if ap.Audience != "" {
token, err = identityTokenFromMetadataService(ap.Endpoint, ap.IdentityTokenPath, ap.Audience)
if err != nil {
return fmt.Errorf("error retrieving identity token from gcp metadata service: %w", err)
}
}
if len(ap.Scopes) != 0 {
token, err = accessTokenFromMetadataService(ap.Endpoint, ap.AccessTokenPath, ap.Scopes)
if err != nil {
return fmt.Errorf("error retrieving access token from gcp metadata service: %w", err)
}
}
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", token))
return nil
}
// accessTokenFromMetadataService returns an access token based on the scopes.
func accessTokenFromMetadataService(endpoint, path string, scopes []string) (string, error) {
s := strings.Join(scopes, ",")
e := fmt.Sprintf("%s%s?scopes=%s", endpoint, path, s)
data, err := gcpMetadataServiceRequest(e)
if err != nil {
return "", err
}
var accessToken AccessToken
err = json.Unmarshal(data, &accessToken)
if err != nil {
return "", err
}
return accessToken.AccessToken, nil
}
// identityTokenFromMetadataService returns an identity token based on the audience.
func identityTokenFromMetadataService(endpoint, path, audience string) (string, error) {
e := fmt.Sprintf("%s%s?audience=%s", endpoint, path, audience)
data, err := gcpMetadataServiceRequest(e)
if err != nil {
return "", err
}
return string(data), nil
}
func gcpMetadataServiceRequest(endpoint string) ([]byte, error) {
request, err := http.NewRequest("GET", endpoint, nil)
if err != nil {
return nil, err
}
request.Header.Add("Metadata-Flavor", "Google")
timeout := time.Duration(5) * time.Second
httpClient := http.Client{Timeout: timeout}
response, err := httpClient.Do(request)
if err != nil {
return nil, err
}
defer response.Body.Close()
switch s := response.StatusCode; s {
case 200:
break
case 400:
return nil, &gcpMetadataError{errGCPMetadataInvalidRequest, endpoint, s}
case 404:
return nil, &gcpMetadataError{errGCPMetadataNotFound, endpoint, s}
default:
return nil, &gcpMetadataError{errGCPMetadataUnexpected, endpoint, s}
}
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
return data, nil
}

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