From 0687bd2b28b2dc9e431fa550425ff8b5852379ba Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 1 Apr 2026 11:44:05 +0200 Subject: [PATCH] refactor(proxy): Use jmespath to extract claim values Replace the custom dot-path walking implementation (SplitWithEscaping + WalkSegments) in the proxy service's JWT middleware with github.com/jmespath-community/go-jmespath. extractRoles in oidcroles.go and readUserIDClaim in account_resolver.go now uses jmespath.Search() NOTE: This change is backwards-incompatible for some corner cases. While simple dot-separated paths like 'realm_access.roles' are unchanged, claim paths containing literal dots (i.e. where the dot does not indicate a hierarchy) must now be quoted ('"sub.roles"' instead of 'sub\.roles'). --- go.mod | 1 + go.sum | 2 + pkg/oidc/claims.go | 62 -- pkg/oidc/claims_test.go | 182 ---- services/proxy/README.md | 22 +- services/proxy/pkg/config/config.go | 4 +- .../proxy/pkg/middleware/account_resolver.go | 36 +- .../pkg/middleware/account_resolver_test.go | 49 +- services/proxy/pkg/userroles/oidcroles.go | 23 +- .../proxy/pkg/userroles/oidcroles_test.go | 3 +- .../jmespath-community/go-jmespath/.gitignore | 5 + .../go-jmespath/.gitmodules | 3 + .../go-jmespath/.golangci.yml | 70 ++ .../jmespath-community/go-jmespath/LICENSE | 202 +++++ .../jmespath-community/go-jmespath/Makefile | 47 + .../jmespath-community/go-jmespath/NOTICE | 2 + .../jmespath-community/go-jmespath/README.md | 88 ++ .../jmespath-community/go-jmespath/jp.go | 45 + .../go-jmespath/pkg/api/api.go | 67 ++ .../go-jmespath/pkg/binding/binding.go | 37 + .../go-jmespath/pkg/error/errors.go | 66 ++ .../go-jmespath/pkg/functions/default.go | 279 ++++++ .../go-jmespath/pkg/functions/functions.go | 820 ++++++++++++++++++ .../go-jmespath/pkg/interpreter/functions.go | 159 ++++ .../pkg/interpreter/interpreter.go | 551 ++++++++++++ .../pkg/parsing/astnodetype_string.go | 52 ++ .../go-jmespath/pkg/parsing/lexer.go | 464 ++++++++++ .../go-jmespath/pkg/parsing/parser.go | 727 ++++++++++++++++ .../go-jmespath/pkg/parsing/toktype_string.go | 62 ++ .../go-jmespath/pkg/util/util.go | 251 ++++++ .../x/exp/constraints/constraints.go | 52 ++ vendor/modules.txt | 11 + 32 files changed, 4112 insertions(+), 332 deletions(-) delete mode 100644 pkg/oidc/claims_test.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/.gitignore create mode 100644 vendor/github.com/jmespath-community/go-jmespath/.gitmodules create mode 100644 vendor/github.com/jmespath-community/go-jmespath/.golangci.yml create mode 100644 vendor/github.com/jmespath-community/go-jmespath/LICENSE create mode 100644 vendor/github.com/jmespath-community/go-jmespath/Makefile create mode 100644 vendor/github.com/jmespath-community/go-jmespath/NOTICE create mode 100644 vendor/github.com/jmespath-community/go-jmespath/README.md create mode 100644 vendor/github.com/jmespath-community/go-jmespath/jp.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/api/api.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/binding/binding.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/error/errors.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/functions/default.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/functions/functions.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/functions.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/interpreter.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/astnodetype_string.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/lexer.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/parser.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/toktype_string.go create mode 100644 vendor/github.com/jmespath-community/go-jmespath/pkg/util/util.go create mode 100644 vendor/golang.org/x/exp/constraints/constraints.go diff --git a/go.mod b/go.mod index 2ec6eca747..2dcac87ef0 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/jellydator/ttlcache/v2 v2.11.1 github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jinzhu/now v1.1.5 + github.com/jmespath-community/go-jmespath v1.1.1 github.com/justinas/alice v1.2.0 github.com/kovidgoyal/imaging v1.8.20 github.com/leonelquinteros/gotext v1.7.2 diff --git a/go.sum b/go.sum index 6fcc4aa94b..2a0828f64f 100644 --- a/go.sum +++ b/go.sum @@ -688,6 +688,8 @@ github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5Xum github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath-community/go-jmespath v1.1.1 h1:bFikPhsi/FdmlZhVgSCd2jj1e7G/rw+zyQfyg5UF+L4= +github.com/jmespath-community/go-jmespath v1.1.1/go.mod h1:4gOyFJsR/Gk+05RgTKYrifT7tBPWD8Lubtb5jRrfy9I= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= diff --git a/pkg/oidc/claims.go b/pkg/oidc/claims.go index 38fbde106a..f20892231f 100644 --- a/pkg/oidc/claims.go +++ b/pkg/oidc/claims.go @@ -1,10 +1,5 @@ package oidc -import ( - "fmt" - "strings" -) - const ( Iss = "iss" Sub = "sub" @@ -17,60 +12,3 @@ const ( OpenCloudUUID = "openclouduuid" OpenCloudRoutingPolicy = "opencloud.routing.policy" ) - -// SplitWithEscaping splits s into segments using separator which can be escaped using the escape string -// See https://codereview.stackexchange.com/a/280193 -func SplitWithEscaping(s string, separator string, escapeString string) []string { - a := strings.Split(s, separator) - - for i := len(a) - 2; i >= 0; i-- { - if strings.HasSuffix(a[i], escapeString) { - a[i] = a[i][:len(a[i])-len(escapeString)] + separator + a[i+1] - a = append(a[:i+1], a[i+2:]...) - } - } - return a -} - -// WalkSegments uses the given array of segments to walk the claims and return whatever interface was found -func WalkSegments(segments []string, claims map[string]any) (any, error) { - i := 0 - for ; i < len(segments)-1; i++ { - switch castedClaims := claims[segments[i]].(type) { - case map[string]any: - claims = castedClaims - case map[any]any: - claims = make(map[string]any, len(castedClaims)) - for k, v := range castedClaims { - if s, ok := k.(string); ok { - claims[s] = v - } else { - return nil, fmt.Errorf("could not walk claims path, key '%v' is not a string", k) - } - } - default: - return nil, fmt.Errorf("unsupported type '%v'", castedClaims) - } - } - return claims[segments[i]], nil -} - -// ReadStringClaim returns the string obtained by following the . seperated path in the claims -func ReadStringClaim(path string, claims map[string]any) (string, error) { - // check the simple case first - value, _ := claims[path].(string) - if value != "" { - return value, nil - } - - claim, err := WalkSegments(SplitWithEscaping(path, ".", "\\"), claims) - if err != nil { - return "", err - } - - if value, _ = claim.(string); value != "" { - return value, nil - } - - return value, fmt.Errorf("claim path '%s' not set or empty", path) -} diff --git a/pkg/oidc/claims_test.go b/pkg/oidc/claims_test.go deleted file mode 100644 index e8b5d179f5..0000000000 --- a/pkg/oidc/claims_test.go +++ /dev/null @@ -1,182 +0,0 @@ -package oidc_test - -import ( - "encoding/json" - "reflect" - "testing" - - "github.com/opencloud-eu/opencloud/pkg/oidc" -) - -type splitWithEscapingTest struct { - // Name of the subtest. - name string - - // string to split - s string - - // seperator to use - seperator string - - // escape character to use for escaping - escape string - - expectedParts []string -} - -func (swet splitWithEscapingTest) run(t *testing.T) { - parts := oidc.SplitWithEscaping(swet.s, swet.seperator, swet.escape) - if len(swet.expectedParts) != len(parts) { - t.Errorf("mismatching length") - } - for i, v := range swet.expectedParts { - if parts[i] != v { - t.Errorf("expected part %d to be '%s', got '%s'", i, v, parts[i]) - } - } -} - -func TestSplitWithEscaping(t *testing.T) { - tests := []splitWithEscapingTest{ - { - name: "plain claim name", - s: "roles", - seperator: ".", - escape: "\\", - expectedParts: []string{"roles"}, - }, - { - name: "claim with .", - s: "my.roles", - seperator: ".", - escape: "\\", - expectedParts: []string{"my", "roles"}, - }, - { - name: "claim with escaped .", - s: "my\\.roles", - seperator: ".", - escape: "\\", - expectedParts: []string{"my.roles"}, - }, - { - name: "claim with escaped . left", - s: "my\\.other.roles", - seperator: ".", - escape: "\\", - expectedParts: []string{"my.other", "roles"}, - }, - { - name: "claim with escaped . right", - s: "my.other\\.roles", - seperator: ".", - escape: "\\", - expectedParts: []string{"my", "other.roles"}, - }, - } - for _, test := range tests { - t.Run(test.name, test.run) - } -} - -type walkSegmentsTest struct { - // Name of the subtest. - name string - - // path segments to walk - segments []string - - // seperator to use - claims map[string]any - - expected any - - wantErr bool -} - -func (wst walkSegmentsTest) run(t *testing.T) { - v, err := oidc.WalkSegments(wst.segments, wst.claims) - if err != nil && !wst.wantErr { - t.Errorf("%v", err) - } - if err == nil && wst.wantErr { - t.Errorf("expected error") - } - if !reflect.DeepEqual(v, wst.expected) { - t.Errorf("expected %v got %v", wst.expected, v) - } -} - -func TestWalkSegments(t *testing.T) { - byt := []byte(`{"first":{"second":{"third":["value1","value2"]},"foo":"bar"},"fizz":"buzz"}`) - var dat map[string]any - if err := json.Unmarshal(byt, &dat); err != nil { - t.Errorf("%v", err) - } - - tests := []walkSegmentsTest{ - { - name: "one segment, single value", - segments: []string{"first"}, - claims: map[string]any{ - "first": "value", - }, - expected: "value", - wantErr: false, - }, - { - name: "one segment, array value", - segments: []string{"first"}, - claims: map[string]any{ - "first": []string{"value1", "value2"}, - }, - expected: []string{"value1", "value2"}, - wantErr: false, - }, - { - name: "two segments, single value", - segments: []string{"first", "second"}, - claims: map[string]any{ - "first": map[string]any{ - "second": "value", - }, - }, - expected: "value", - wantErr: false, - }, - { - name: "two segments, array value", - segments: []string{"first", "second"}, - claims: map[string]any{ - "first": map[string]any{ - "second": []string{"value1", "value2"}, - }, - }, - expected: []string{"value1", "value2"}, - wantErr: false, - }, - { - name: "three segments, array value from json", - segments: []string{"first", "second", "third"}, - claims: dat, - expected: []any{"value1", "value2"}, - wantErr: false, - }, - { - name: "three segments, array value with interface key", - segments: []string{"first", "second", "third"}, - claims: map[string]any{ - "first": map[any]any{ - "second": map[any]any{ - "third": []string{"value1", "value2"}, - }, - }, - }, - expected: []string{"value1", "value2"}, - wantErr: false, - }, - } - for _, test := range tests { - t.Run(test.name, test.run) - } -} diff --git a/services/proxy/README.md b/services/proxy/README.md index 7e86297940..210d8c73a4 100644 --- a/services/proxy/README.md +++ b/services/proxy/README.md @@ -92,9 +92,13 @@ The name of an OIDC claim whose value should be used to maintain a user's group membership. The claim value should contain a list of group names the user should be a member of. Defaults to `groups`. * `PROXY_USER_OIDC_CLAIM`\ -When resolving and authenticated OIDC user, the value of this claims is used to -lookup the user in the users service. For auto provisioning setups this usually is the -same claims as set via `PROXY_AUTOPROVISION_CLAIM_USERNAME`. +When resolving an authenticated OIDC user, this JMESPath expression is evaluated +against the token claims to extract the user identifier used to look up the user +in the users service. Simple claim names such as `sub`, `email` or +`preferred_username` work as-is. Nested claims can be accessed with dot notation, +e.g. `identity.username`. Claim keys that contain a literal dot must use +JMESPath quoted identifier syntax, e.g. `"claim.name"`. For auto provisioning +setups this is usually the same claim as `PROXY_AUTOPROVISION_CLAIM_USERNAME`. * `PROXY_USER_CS3_CLAIM`\ This is the name of the user attribute in OpenCloud that is used to lookup the user by the value of the `PROXY_USER_OIDC_CLAIM`. For auto provisioning setups this usually @@ -177,10 +181,14 @@ get the role 'user' assigned. (This is also the default behavior if `PROXY_ROLE_ is unset. When `PROXY_ROLE_ASSIGNMENT_DRIVER` is set to `oidc` the role assignment for a user will happen -based on the values of an OpenID Connect Claim of that user. The name of the OpenID Connect Claim to -be used for the role assignment can be configured via the `PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM` -environment variable. It is also possible to define a mapping of claim values to role names defined -in OpenCloud via a `yaml` configuration. See the following `proxy.yaml` snippet for an example. +based on the values of an OIDC claim of that user. The claim to use is configured via +`PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM`, which accepts a JMESPath expression evaluated against +the token claims. Simple claim names such as `roles` work as-is. Nested claims can be +accessed with dot notation, e.g. `realm_access.roles`. Claim keys that contain a literal +dot must use JMESPath quoted identifier syntax, e.g. `"claim.name"`. The claim value may +be a single string or an array of strings. It is also possible to define a mapping of +claim values to role names defined in OpenCloud via a `yaml` configuration. See the +following `proxy.yaml` snippet for an example. ```yaml role_assignment: diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index cdaa3e81e7..f454a1ca0e 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -32,7 +32,7 @@ type Config struct { PolicySelector *PolicySelector `yaml:"policy_selector"` PreSignedURL PreSignedURL `yaml:"pre_signed_url"` AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here." introductionVersion:"1.0.0"` - UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_username' but you can also add your own claim." introductionVersion:"1.0.0"` + UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"A JMESPath expression to extract the user identifier from the OIDC token claims. Simple claim names such as 'email' or 'preferred_username' work as-is. Nested claims can be accessed with dot notation, e.g. 'identity.username'. Claim keys that contain a literal dot must use JMESPath quoted identifier syntax, e.g. '\"claim.name\"'. The extracted value must be unique, stable and non re-assignable for the lifetime of the user." introductionVersion:"1.0.0"` UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'." introductionVersion:"1.0.0"` TenantOIDCClaim string `yaml:"tenant_oidc_claim" env:"PROXY_TENANT_OIDC_CLAIM" desc:"JMESPath expression to extract the tenant ID from the OIDC token claims. When set, the extracted value is verified against the tenant ID returned by the user backend, rejecting requests where they do not match. Only relevant when multi-tenancy is enabled." introductionVersion:"6.1.0"` TenantIDMappingEnabled bool `yaml:"tenant_id_mapping_enabled" env:"PROXY_TENANT_ID_MAPPING_ENABLED" desc:"When set to 'true', the proxy will resolve the internal tenant ID from the external tenant ID provided in the OIDC claims by calling the TenantAPI before verifying the tenant. Use this when the external tenant ID in the OIDC token differs from the internal tenant ID stored on the user. Requires 'tenant_oidc_claim' to be set. Only relevant when multi-tenancy is enabled." introductionVersion:"6.1.0"` @@ -150,7 +150,7 @@ type RoleAssignment struct { // OIDCRoleMapper contains the configuration for the "oidc" role assignment driver type OIDCRoleMapper struct { - RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment." introductionVersion:"1.0.0"` + RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"A JMESPath expression to extract the role value(s) from the OIDC token claims. Simple claim names such as 'roles' work as-is. Nested claims can be accessed with dot notation, e.g. 'realm_access.roles'. Claim keys that contain a literal dot must use JMESPath quoted identifier syntax, e.g. '\"claim.name\"'. The extracted value may be a string or an array of strings." introductionVersion:"1.0.0"` RolesMap []RoleMapping `yaml:"role_mapping" desc:"A list of mappings of OpenCloud role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."` } diff --git a/services/proxy/pkg/middleware/account_resolver.go b/services/proxy/pkg/middleware/account_resolver.go index 04b57b6105..4b8bd89738 100644 --- a/services/proxy/pkg/middleware/account_resolver.go +++ b/services/proxy/pkg/middleware/account_resolver.go @@ -11,6 +11,7 @@ import ( tenantpb "github.com/cs3org/go-cs3apis/cs3/identity/tenant/v1beta1" rpcpb "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/jellydator/ttlcache/v3" + jmespath "github.com/jmespath-community/go-jmespath" "github.com/opencloud-eu/opencloud/services/proxy/pkg/router" "github.com/opencloud-eu/opencloud/services/proxy/pkg/user/backend" "github.com/opencloud-eu/opencloud/services/proxy/pkg/userroles" @@ -92,38 +93,17 @@ type accountResolver struct { } func readStringClaim(path string, claims map[string]any) (string, error) { - // happy path - value, _ := claims[path].(string) - if value != "" { - return value, nil + result, err := jmespath.Search(path, claims) + if err != nil { + return "", err } - // try splitting path at . - segments := oidc.SplitWithEscaping(path, ".", "\\") - subclaims := claims - lastSegment := len(segments) - 1 - for i := range segments { - if i < lastSegment { - if castedClaims, ok := subclaims[segments[i]].(map[string]any); ok { - subclaims = castedClaims - } else if castedClaims, ok := subclaims[segments[i]].(map[any]any); ok { - subclaims = make(map[string]any, len(castedClaims)) - for k, v := range castedClaims { - if s, ok := k.(string); ok { - subclaims[s] = v - } else { - return "", fmt.Errorf("could not walk claims path, key '%v' is not a string", k) - } - } - } - } else { - if value, _ = subclaims[segments[i]].(string); value != "" { - return value, nil - } - } + value, ok := result.(string) + if !ok || value == "" { + return "", fmt.Errorf("claim path '%s' not set or empty", path) } - return value, fmt.Errorf("claim path '%s' not set or empty", path) + return value, nil } // TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39 diff --git a/services/proxy/pkg/middleware/account_resolver_test.go b/services/proxy/pkg/middleware/account_resolver_test.go index f81e639ccb..d958176418 100644 --- a/services/proxy/pkg/middleware/account_resolver_test.go +++ b/services/proxy/pkg/middleware/account_resolver_test.go @@ -96,44 +96,23 @@ func TestTokenIsAddedWithDotUsernamePathClaim(t *testing.T) { assert.Contains(t, token, "eyJ") } -func TestTokenIsAddedWithDottedUsernameClaim(t *testing.T) { - tests := []struct { - name string - oidcClaim string - // comment describing what the claim exercises - desc string - }{ - { - name: "escaped dot treated as literal key", - oidcClaim: "li\\.un", - desc: "li\\.un escapes the dot so the claim is looked up as the literal key \"li.un\"", - }, - { - name: "dotted path falls back to literal key", - oidcClaim: "li.un", - desc: "li.un is first tried as a nested path; when \"un\" is absent under \"li\", it falls back to the literal key \"li.un\"", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - sut := newMockAccountResolver(&userv1beta1.User{ - Id: &userv1beta1.UserId{Idp: testIdP, OpaqueId: "123"}, - Mail: "foo@example.com", - }, nil, tc.oidcClaim, "username", false) +func TestTokenIsAddedWithDotUsernameClaim(t *testing.T) { + sut := newMockAccountResolver(&userv1beta1.User{ + Id: &userv1beta1.UserId{Idp: "https://idx.example.com", OpaqueId: "123"}, + Mail: "foo@example.com", + }, nil, `"li.un"`, "username", false) - req, rw := mockRequest(map[string]any{ - oidc.Iss: testIdP, - "li.un": "foo", - }) + // JMESPath quoted identifier syntax accesses keys containing dots + req, rw := mockRequest(map[string]any{ + oidc.Iss: testIdP, + "li.un": "foo", + }) - sut.ServeHTTP(rw, req) + sut.ServeHTTP(rw, req) - token := req.Header.Get(revactx.TokenHeader) - assert.NotEmpty(t, token) - assert.Contains(t, token, "eyJ") - }) - } + token := req.Header.Get(revactx.TokenHeader) + assert.NotEmpty(t, token) + assert.Contains(t, token, "eyJ") } func TestNSkipOnNoClaims(t *testing.T) { diff --git a/services/proxy/pkg/userroles/oidcroles.go b/services/proxy/pkg/userroles/oidcroles.go index 409f67f422..8b1c761884 100644 --- a/services/proxy/pkg/userroles/oidcroles.go +++ b/services/proxy/pkg/userroles/oidcroles.go @@ -7,8 +7,8 @@ import ( "time" cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + jmespath "github.com/jmespath-community/go-jmespath" "github.com/opencloud-eu/opencloud/pkg/middleware" - "github.com/opencloud-eu/opencloud/pkg/oidc" settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/settings/v0" "github.com/opencloud-eu/reva/v2/pkg/utils" "go-micro.dev/v4/metadata" @@ -31,21 +31,13 @@ func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner { } func extractRoles(rolesClaim string, claims map[string]any) (map[string]struct{}, error) { - - claimRoles := map[string]struct{}{} - // happy path - value, _ := claims[rolesClaim].(string) - if value != "" { - claimRoles[value] = struct{}{} - return claimRoles, nil - } - - claim, err := oidc.WalkSegments(oidc.SplitWithEscaping(rolesClaim, ".", "\\"), claims) + result, err := jmespath.Search(rolesClaim, claims) if err != nil { return nil, err } - switch v := claim.(type) { + claimRoles := map[string]struct{}{} + switch v := result.(type) { case []string: for _, cr := range v { claimRoles[cr] = struct{}{} @@ -54,13 +46,14 @@ func extractRoles(rolesClaim string, claims map[string]any) (map[string]struct{} for _, cri := range v { cr, ok := cri.(string) if !ok { - err := errors.New("invalid role in claims") - return nil, err + return nil, errors.New("invalid role in claims") } - claimRoles[cr] = struct{}{} } case string: + if v == "" { + return nil, errors.New("no roles in user claims") + } claimRoles[v] = struct{}{} default: return nil, errors.New("no roles in user claims") diff --git a/services/proxy/pkg/userroles/oidcroles_test.go b/services/proxy/pkg/userroles/oidcroles_test.go index 81548e5ae7..c4726196dd 100644 --- a/services/proxy/pkg/userroles/oidcroles_test.go +++ b/services/proxy/pkg/userroles/oidcroles_test.go @@ -92,7 +92,8 @@ func TestExtractEscapedRolesPathString(t *testing.T) { t.Fatal(err) } - roles, err := extractRoles("sub\\.roles", claims) + // JMESPath uses quoted identifiers to access keys containing dots + roles, err := extractRoles(`"sub.roles"`, claims) if err != nil { t.Fatal(err) } diff --git a/vendor/github.com/jmespath-community/go-jmespath/.gitignore b/vendor/github.com/jmespath-community/go-jmespath/.gitignore new file mode 100644 index 0000000000..fd731f8b4a --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/.gitignore @@ -0,0 +1,5 @@ +/jpgo +jmespath-fuzz.zip +cpu.out +go-jmespath.test +coverage.out diff --git a/vendor/github.com/jmespath-community/go-jmespath/.gitmodules b/vendor/github.com/jmespath-community/go-jmespath/.gitmodules new file mode 100644 index 0000000000..8162f8e472 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/.gitmodules @@ -0,0 +1,3 @@ +[submodule "compliance"] + path = compliance + url = https://github.com/jmespath-community/jmespath.test.git diff --git a/vendor/github.com/jmespath-community/go-jmespath/.golangci.yml b/vendor/github.com/jmespath-community/go-jmespath/.golangci.yml new file mode 100644 index 0000000000..a2ba7937f1 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/.golangci.yml @@ -0,0 +1,70 @@ +linters: + enable: + - asasalint + - asciicheck + - bidichk + - bodyclose + - containedctx + - contextcheck + # - cyclop + # - deadcode + - decorder + - depguard + - dogsled + - dupl + - dupword + - durationcheck + - errcheck + - errchkjson + - errname + # - errorlint + - execinquery + # - exhaustive + # - exhaustivestruct + # - exhaustruct + - exportloopref + # - forbidigo + # - forcetypeassert + # - funlen + - gci + # - gochecknoglobals + - gochecknoinits + # - gocognit + - goconst + # - gocritic + # - gocyclo + - godot + # - godox + # - goerr113 + - gofmt + - gofumpt + + + - prealloc + # - predeclared + - promlinter + - reassign + # - revive + - rowserrcheck + # - scopelint + - sqlclosecheck + - staticcheck + # - structcheck + - stylecheck + - tagliatelle + - tenv + - testableexamples + # - testpackage + - thelper + - tparallel + - typecheck + - unconvert + # - unparam + - unused + - usestdlibvars + # - varcheck + # - varnamelen + - wastedassign + - whitespace + # - wrapcheck + # - wsl diff --git a/vendor/github.com/jmespath-community/go-jmespath/LICENSE b/vendor/github.com/jmespath-community/go-jmespath/LICENSE new file mode 100644 index 0000000000..d645695673 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/jmespath-community/go-jmespath/Makefile b/vendor/github.com/jmespath-community/go-jmespath/Makefile new file mode 100644 index 0000000000..5984c9d26a --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/Makefile @@ -0,0 +1,47 @@ + +CMD = jpgo + + +help: + @echo "Please use \`make ' where is one of" + @echo " test to run all the tests" + @echo " build to build the library and jp executable" + @echo " generate to run codegen" + +generate: + go generate ./... + +build: + rm -f $(CMD) + git submodule update --init --checkout --recursive --force + go build ./... + rm -f cmd/$(CMD)/$(CMD) && cd cmd/$(CMD)/ && go build ./... + mv cmd/$(CMD)/$(CMD) . + +test: build + go test -v ./... + +check: + go vet ./... + golint ./... + golangci-lint run + +htmlc: + go test -cover -coverpkg ./... -coverprofile="/tmp/jpcov" ./... && go tool cover -html="/tmp/jpcov" && unlink /tmp/jpcov + +buildfuzz: + go-fuzz-build github.com/jmespath-community/go-jmespath/fuzz + +fuzz: buildfuzz + go-fuzz -bin=./jmespath-fuzz.zip -workdir=fuzz/testdata + +bench: + go test -bench . -cpuprofile cpu.out + +pprof-cpu: + go tool pprof ./go-jmespath.test ./cpu.out + +install-dev-cmds: + go install golang.org/x/lint/golint@latest + go install golang.org/x/tools/cmd/stringer@latest + command -v golangci-lint || { curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $$(go env GOPATH)/bin v1.46.2; } diff --git a/vendor/github.com/jmespath-community/go-jmespath/NOTICE b/vendor/github.com/jmespath-community/go-jmespath/NOTICE new file mode 100644 index 0000000000..c00cc539b0 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/NOTICE @@ -0,0 +1,2 @@ +go-jmespath +Copyright 2015 James Saryerwinnie diff --git a/vendor/github.com/jmespath-community/go-jmespath/README.md b/vendor/github.com/jmespath-community/go-jmespath/README.md new file mode 100644 index 0000000000..532c96dfa4 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/README.md @@ -0,0 +1,88 @@ +# go-jmespath - A JMESPath implementation in Go + +[![GoDoc](https://godoc.org/github.com/jmespath-community/go-jmespath?status.svg)](https://godoc.org/github.com/jmespath-community/go-jmespath) +[![codecov](https://codecov.io/gh/jmespath-community/go-jmespath/branch/main/graph/badge.svg)](https://app.codecov.io/gh/jmespath-community/go-jmespath/branch/main) +[![Go Report Card](https://goreportcard.com/badge/github.com/jmespath-community/go-jmespath)](https://goreportcard.com/report/github.com/jmespath-community/go-jmespath) +![License: Apache-2.0](https://img.shields.io/github/license/jmespath-community/go-jmespath?color=blue) + +go-jmespath is a GO implementation of JMESPath, +which is a query language for JSON. It will take a JSON +document and transform it into another JSON document +through a JMESPath expression. + +Using go-jmespath is really easy. There's a single function +you use, `jmespath.Search`: + + +```go +> import "github.com/jmespath-community/go-jmespath" +> +> var jsondata = []byte(`{"foo": {"bar": {"baz": [0, 1, 2, 3, 4]}}}`) // your data +> var data interface{} +> err := json.Unmarshal(jsondata, &data) +> result, err := jmespath.Search("foo.bar.baz[2]", data) +result = 2 +``` + +In the example we gave the ``Search`` function input data of +`{"foo": {"bar": {"baz": [0, 1, 2, 3, 4]}}}` as well as the JMESPath +expression `foo.bar.baz[2]`, and the `Search` function evaluated +the expression against the input data to produce the result ``2``. + +The JMESPath language can do a lot more than select an element +from a list. Here are a few more examples: + +```go +> var jsondata = []byte(`{"foo": {"bar": {"baz": [0, 1, 2, 3, 4]}}}`) // your data +> var data interface{} +> err := json.Unmarshal(jsondata, &data) +> result, err := jmespath.Search("foo.bar", data) +result = { "baz": [ 0, 1, 2, 3, 4 ] } + + +> var jsondata = []byte(`{"foo": [{"first": "a", "last": "b"}, + {"first": "c", "last": "d"}]}`) // your data +> var data interface{} +> err := json.Unmarshal(jsondata, &data) +> result, err := jmespath.Search({"foo[*].first", data) +result [ 'a', 'c' ] + + +> var jsondata = []byte(`{"foo": [{"age": 20}, {"age": 25}, + {"age": 30}, {"age": 35}, + {"age": 40}]}`) // your data +> var data interface{} +> err := json.Unmarshal(jsondata, &data) +> result, err := jmespath.Search("foo[?age > `30`]") +result = [ { age: 35 }, { age: 40 } ] +``` + +You can also pre-compile your query. This is usefull if +you are going to run multiple searches with it: + +```go +> var jsondata = []byte(`{"foo": "bar"}`) +> var data interface{} +> err := json.Unmarshal(jsondata, &data) +> precompiled, err := Compile("foo") +> if err != nil{ +> // ... handle the error +> } +> result, err := precompiled.Search(data) +result = "bar" +``` + +## More Resources + +The example above only show a small amount of what +a JMESPath expression can do. If you want to take a +tour of the language, the *best* place to go is the +[JMESPath Tutorial](https://jmespath.site/#tutorial). + +One of the best things about JMESPath is that it is +implemented in many different programming languages including +python, ruby, php, lua, etc. To see a complete list of libraries, +check out the [JMESPath libraries page](https://jmespath.site/#libraries). + +And finally, the full JMESPath specification can be found +on the [JMESPath site](https://jmespath.site/#specification). diff --git a/vendor/github.com/jmespath-community/go-jmespath/jp.go b/vendor/github.com/jmespath-community/go-jmespath/jp.go new file mode 100644 index 0000000000..2d6e4d3c04 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/jp.go @@ -0,0 +1,45 @@ +package jmespath + +import ( + "github.com/jmespath-community/go-jmespath/pkg/api" + "github.com/jmespath-community/go-jmespath/pkg/functions" + "github.com/jmespath-community/go-jmespath/pkg/parsing" +) + +// api types + +type JMESPath = api.JMESPath + +var ( + Compile = api.Compile + MustCompile = api.MustCompile + Search = api.Search +) + +// parsing types + +type SyntaxError = parsing.SyntaxError + +var NewParser = parsing.NewParser + +// function types + +type ( + JpFunction = functions.JpFunction + JpType = functions.JpType + FunctionEntry = functions.FunctionEntry + ArgSpec = functions.ArgSpec + ExpRef = functions.ExpRef +) + +const ( + JpNumber = functions.JpNumber + JpString = functions.JpString + JpArray = functions.JpArray + JpObject = functions.JpObject + JpArrayArray = functions.JpArrayArray + JpArrayNumber = functions.JpArrayNumber + JpArrayString = functions.JpArrayString + JpExpref = functions.JpExpref + JpAny = functions.JpAny +) diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/api/api.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/api/api.go new file mode 100644 index 0000000000..a58c4ebfbe --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/api/api.go @@ -0,0 +1,67 @@ +package api + +import ( + "strconv" + + "github.com/jmespath-community/go-jmespath/pkg/functions" + "github.com/jmespath-community/go-jmespath/pkg/interpreter" + "github.com/jmespath-community/go-jmespath/pkg/parsing" +) + +// JMESPath is the representation of a compiled JMES path query. A JMESPath is +// safe for concurrent use by multiple goroutines. +type JMESPath interface { + Search(interface{}) (interface{}, error) +} + +type jmesPath struct { + node parsing.ASTNode + caller interpreter.FunctionCaller +} + +func newJMESPath(node parsing.ASTNode, funcs ...functions.FunctionEntry) JMESPath { + return jmesPath{ + node: node, + caller: interpreter.NewFunctionCaller(funcs...), + } +} + +// Compile parses a JMESPath expression and returns, if successful, a JMESPath +// object that can be used to match against data. +func Compile(expression string, funcs ...functions.FunctionEntry) (JMESPath, error) { + parser := parsing.NewParser() + ast, err := parser.Parse(expression) + if err != nil { + return nil, err + } + var f []functions.FunctionEntry + f = append(f, functions.GetDefaultFunctions()...) + f = append(f, funcs...) + return newJMESPath(ast, f...), nil +} + +// MustCompile is like Compile but panics if the expression cannot be parsed. +// It simplifies safe initialization of global variables holding compiled +// JMESPaths. +func MustCompile(expression string, funcs ...functions.FunctionEntry) JMESPath { + jmespath, err := Compile(expression, funcs...) + if err != nil { + panic(`jmespath: Compile(` + strconv.Quote(expression) + `): ` + err.Error()) + } + return jmespath +} + +// Search evaluates a JMESPath expression against input data and returns the result. +func (jp jmesPath) Search(data interface{}) (interface{}, error) { + intr := interpreter.NewInterpreter(data, jp.caller, nil) + return intr.Execute(jp.node, data) +} + +// Search evaluates a JMESPath expression against input data and returns the result. +func Search(expression string, data interface{}, funcs ...functions.FunctionEntry) (interface{}, error) { + compiled, err := Compile(expression, funcs...) + if err != nil { + return nil, err + } + return compiled.Search(data) +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/binding/binding.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/binding/binding.go new file mode 100644 index 0000000000..d87a156e2f --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/binding/binding.go @@ -0,0 +1,37 @@ +package binding + +import "fmt" + +// Bindings stores let expression bindings by name. +type Bindings interface { + // Get returns the value bound for a given name. + Get(string) (interface{}, error) + // Register registers a value associated with a given name, it returns a new binding + Register(string, interface{}) Bindings +} + +type bindings struct { + values map[string]interface{} +} + +func NewBindings() Bindings { + return bindings{} +} + +func (b bindings) Get(name string) (interface{}, error) { + if value, ok := b.values[name]; ok { + return value, nil + } + return nil, fmt.Errorf("variable not defined: %s", name) +} + +func (b bindings) Register(name string, value interface{}) Bindings { + values := map[string]interface{}{} + for k, v := range b.values { + values[k] = v + } + values[name] = value + return bindings{ + values: values, + } +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/error/errors.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/error/errors.go new file mode 100644 index 0000000000..acc3b0e147 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/error/errors.go @@ -0,0 +1,66 @@ +package error + +import ( + "errors" + "fmt" +) + +func NotAnInteger(name string, arg string) error { + return errors.New(formatNotAnInteger(name, arg)) +} + +func NotAPositiveInteger(name string, arg string) error { + return errors.New(formatNotAPositiveInteger(name, arg)) +} + +func NotEnoughArgumentsSupplied(name string, count int, minExpected int, variadic bool) error { + return errors.New(formatNotEnoughArguments(name, count, minExpected, variadic)) +} + +func TooManyArgumentsSupplied(name string, count int, maxExpected int) error { + return errors.New(formatTooManyArguments(name, count, maxExpected)) +} + +func formatNotAnInteger(name string, arg string) string { + return fmt.Sprintf("invalid value, the function '%s' expects its '%s' argument to be an integer.", name, arg) +} + +func formatNotAPositiveInteger(name string, arg string) string { + return fmt.Sprintf("invalid value, the function '%s' expects its '%s' argument to be a an integer value greater than or equal to zero.", name, arg) +} + +func formatNotEnoughArguments(name string, count int, minExpected int, variadic bool) string { + more := "" + only := "" + + if variadic { + more = "or more " + only = "only " + } + + report := fmt.Sprintf("%s%d ", only, count) + if count == 0 { + report = "none " + } + + plural := "" + if minExpected > 1 { + plural = "s" + } + + return fmt.Sprintf( + "invalid arity, the function '%s' expects %d argument%s %sbut %swere supplied", + name, + minExpected, + plural, + more, + report) +} + +func formatTooManyArguments(name string, count int, maxExpected int) string { + return fmt.Sprintf( + "invalid arity, the function '%s' expects at most %d arguments but %d were supplied", + name, + maxExpected, + count) +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/default.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/default.go new file mode 100644 index 0000000000..8b942db816 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/default.go @@ -0,0 +1,279 @@ +package functions + +func GetDefaultFunctions() []FunctionEntry { + return []FunctionEntry{{ + Name: "abs", + Arguments: []ArgSpec{ + {Types: []JpType{JpNumber}}, + }, + Handler: jpfAbs, + }, { + Name: "avg", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayNumber}}, + }, + Handler: jpfAvg, + }, { + Name: "ceil", + Arguments: []ArgSpec{ + {Types: []JpType{JpNumber}}, + }, + Handler: jpfCeil, + }, { + Name: "contains", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray, JpString}}, + {Types: []JpType{JpAny}}, + }, + Handler: jpfContains, + }, { + Name: "ends_with", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfEndsWith, + }, { + Name: "find_first", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}, Optional: true}, + {Types: []JpType{JpNumber}, Optional: true}, + }, + Handler: jpfFindFirst, + }, { + Name: "find_last", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}, Optional: true}, + {Types: []JpType{JpNumber}, Optional: true}, + }, + Handler: jpfFindLast, + }, { + Name: "floor", + Arguments: []ArgSpec{ + {Types: []JpType{JpNumber}}, + }, + Handler: jpfFloor, + }, { + Name: "from_items", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayArray}}, + }, + Handler: jpfFromItems, + }, { + Name: "group_by", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray}}, + {Types: []JpType{JpExpref}}, + }, + Handler: jpfGroupBy, + }, { + Name: "items", + Arguments: []ArgSpec{ + {Types: []JpType{JpObject}}, + }, + Handler: jpfItems, + }, { + Name: "join", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpArrayString}}, + }, + Handler: jpfJoin, + }, { + Name: "keys", + Arguments: []ArgSpec{ + {Types: []JpType{JpObject}}, + }, + Handler: jpfKeys, + }, { + Name: "length", + Arguments: []ArgSpec{ + {Types: []JpType{JpString, JpArray, JpObject}}, + }, + Handler: jpfLength, + }, { + Name: "lower", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + }, + Handler: jpfLower, + }, { + Name: "map", + Arguments: []ArgSpec{ + {Types: []JpType{JpExpref}}, + {Types: []JpType{JpArray}}, + }, + Handler: jpfMap, + }, { + Name: "max", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayNumber, JpArrayString}}, + }, + Handler: jpfMax, + }, { + Name: "max_by", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray}}, + {Types: []JpType{JpExpref}}, + }, + Handler: jpfMaxBy, + }, { + Name: "merge", + Arguments: []ArgSpec{ + {Types: []JpType{JpObject}, Variadic: true}, + }, + Handler: jpfMerge, + }, { + Name: "min", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayNumber, JpArrayString}}, + }, + Handler: jpfMin, + }, { + Name: "min_by", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray}}, + {Types: []JpType{JpExpref}}, + }, + Handler: jpfMinBy, + }, { + Name: "not_null", + Arguments: []ArgSpec{ + {Types: []JpType{JpAny}, Variadic: true}, + }, + Handler: jpfNotNull, + }, { + Name: "pad_left", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}}, + {Types: []JpType{JpString}, Optional: true}, + }, + Handler: jpfPadLeft, + }, { + Name: "pad_right", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}}, + {Types: []JpType{JpString}, Optional: true}, + }, + Handler: jpfPadRight, + }, { + Name: "replace", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}, Optional: true}, + }, + Handler: jpfReplace, + }, { + Name: "reverse", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray, JpString}}, + }, + Handler: jpfReverse, + }, { + Name: "sort", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayString, JpArrayNumber}}, + }, + Handler: jpfSort, + }, { + Name: "sort_by", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray}}, + {Types: []JpType{JpExpref}}, + }, + Handler: jpfSortBy, + }, { + Name: "split", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + {Types: []JpType{JpNumber}, Optional: true}, + }, + Handler: jpfSplit, + }, { + Name: "starts_with", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}}, + }, + Handler: jpfStartsWith, + }, { + Name: "sum", + Arguments: []ArgSpec{ + {Types: []JpType{JpArrayNumber}}, + }, + Handler: jpfSum, + }, { + Name: "to_array", + Arguments: []ArgSpec{ + {Types: []JpType{JpAny}}, + }, + Handler: jpfToArray, + }, { + Name: "to_number", + Arguments: []ArgSpec{ + {Types: []JpType{JpAny}}, + }, + Handler: jpfToNumber, + }, { + Name: "to_string", + Arguments: []ArgSpec{ + {Types: []JpType{JpAny}}, + }, + Handler: jpfToString, + }, { + Name: "trim", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}, Optional: true}, + }, + Handler: jpfTrim, + }, { + Name: "trim_left", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}, Optional: true}, + }, + Handler: jpfTrimLeft, + }, { + Name: "trim_right", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + {Types: []JpType{JpString}, Optional: true}, + }, + Handler: jpfTrimRight, + }, { + Name: "type", + Arguments: []ArgSpec{ + {Types: []JpType{JpAny}}, + }, + Handler: jpfType, + }, { + Name: "upper", + Arguments: []ArgSpec{ + {Types: []JpType{JpString}}, + }, + Handler: jpfUpper, + }, { + Name: "values", + Arguments: []ArgSpec{ + {Types: []JpType{JpObject}}, + }, + Handler: jpfValues, + }, { + Name: "zip", + Arguments: []ArgSpec{ + {Types: []JpType{JpArray}}, + {Types: []JpType{JpArray}, Variadic: true}, + }, + Handler: jpfZip, + }} +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/functions.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/functions.go new file mode 100644 index 0000000000..53b63120f4 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/functions/functions.go @@ -0,0 +1,820 @@ +package functions + +import ( + "encoding/json" + "errors" + "fmt" + "math" + "reflect" + "sort" + "strconv" + "strings" + "unicode" + "unicode/utf8" + + jperror "github.com/jmespath-community/go-jmespath/pkg/error" + "github.com/jmespath-community/go-jmespath/pkg/util" +) + +type ( + JpFunction = func([]interface{}) (interface{}, error) + ExpRef = func(interface{}) (interface{}, error) + JpType string +) + +const ( + JpNumber JpType = "number" + JpString JpType = "string" + JpArray JpType = "array" + JpObject JpType = "object" + JpArrayArray JpType = "array[array]" + JpArrayNumber JpType = "array[number]" + JpArrayString JpType = "array[string]" + JpExpref JpType = "expref" + JpAny JpType = "any" +) + +type FunctionEntry struct { + Name string + Arguments []ArgSpec + Handler JpFunction +} + +type ArgSpec struct { + Types []JpType + Variadic bool + Optional bool +} + +type byExprString struct { + items []interface{} + keys []interface{} + hasError bool +} + +func (a *byExprString) Len() int { + return len(a.items) +} + +func (a *byExprString) Swap(i, j int) { + a.items[i], a.items[j] = a.items[j], a.items[i] + a.keys[i], a.keys[j] = a.keys[j], a.keys[i] +} + +func (a *byExprString) Less(i, j int) bool { + ith, ok := a.keys[i].(string) + if !ok { + a.hasError = true + return true + } + jth, ok := a.keys[j].(string) + if !ok { + a.hasError = true + return true + } + return ith < jth +} + +type byExprFloat struct { + items []interface{} + keys []interface{} + hasError bool +} + +func (a *byExprFloat) Len() int { + return len(a.items) +} + +func (a *byExprFloat) Swap(i, j int) { + a.items[i], a.items[j] = a.items[j], a.items[i] + a.keys[i], a.keys[j] = a.keys[j], a.keys[i] +} + +func (a *byExprFloat) Less(i, j int) bool { + ith, ok := a.keys[i].(float64) + if !ok { + a.hasError = true + return true + } + jth, ok := a.keys[j].(float64) + if !ok { + a.hasError = true + return true + } + return ith < jth +} + +func jpfAbs(arguments []interface{}) (interface{}, error) { + num := arguments[0].(float64) + return math.Abs(num), nil +} + +func jpfAvg(arguments []interface{}) (interface{}, error) { + // We've already type checked the value so we can safely use + // type assertions. + args := arguments[0].([]interface{}) + length := float64(len(args)) + if len(args) == 0 { + return nil, nil + } + numerator := 0.0 + for _, n := range args { + numerator += n.(float64) + } + return numerator / length, nil +} + +func jpfCeil(arguments []interface{}) (interface{}, error) { + val := arguments[0].(float64) + return math.Ceil(val), nil +} + +func jpfContains(arguments []interface{}) (interface{}, error) { + search := arguments[0] + el := arguments[1] + if searchStr, ok := search.(string); ok { + if elStr, ok := el.(string); ok { + return strings.Contains(searchStr, elStr), nil + } + return false, nil + } + // Otherwise this is a generic contains for []interface{} + general := search.([]interface{}) + for _, item := range general { + if item == el { + return true, nil + } + } + return false, nil +} + +func jpfEndsWith(arguments []interface{}) (interface{}, error) { + search := arguments[0].(string) + suffix := arguments[1].(string) + return strings.HasSuffix(search, suffix), nil +} + +func jpfFindImpl(name string, arguments []interface{}, find func(s string, substr string) int) (interface{}, error) { + subject := arguments[0].(string) + substr := arguments[1].(string) + + if len(subject) == 0 || len(substr) == 0 { + return nil, nil + } + + start := 0 + startSpecified := len(arguments) > 2 + if startSpecified { + num, ok := util.ToInteger(arguments[2]) + if !ok { + return nil, jperror.NotAnInteger(name, "start") + } + start = util.Max(0, num) + } + end := len(subject) + endSpecified := len(arguments) > 3 + if endSpecified { + num, ok := util.ToInteger(arguments[3]) + if !ok { + return nil, jperror.NotAnInteger(name, "end") + } + end = util.Min(num, len(subject)) + } + + offset := find(subject[start:end], substr) + + if offset == -1 { + return nil, nil + } + + return float64(start + offset), nil +} + +func jpfFindFirst(arguments []interface{}) (interface{}, error) { + return jpfFindImpl("find_first", arguments, strings.Index) +} + +func jpfFindLast(arguments []interface{}) (interface{}, error) { + return jpfFindImpl("find_last", arguments, strings.LastIndex) +} + +func jpfFloor(arguments []interface{}) (interface{}, error) { + val := arguments[0].(float64) + return math.Floor(val), nil +} + +func jpfFromItems(arguments []interface{}) (interface{}, error) { + if arr, ok := util.ToArrayArray(arguments[0]); ok { + result := make(map[string]interface{}) + for _, item := range arr { + if len(item) != 2 { + return nil, errors.New("invalid value, each array must contain two elements, a pair of string and value") + } + first, ok := item[0].(string) + if !ok { + return nil, errors.New("invalid value, each array must contain two elements, a pair of string and value") + } + second := item[1] + result[first] = second + } + return result, nil + } + return nil, errors.New("invalid type, first argument must be an array of arrays") +} + +func jpfGroupBy(arguments []interface{}) (interface{}, error) { + arr := arguments[0].([]interface{}) + exp := arguments[1].(ExpRef) + if len(arr) == 0 { + return nil, nil + } + groups := map[string]interface{}{} + for _, element := range arr { + spec, err := exp(element) + if err != nil { + return nil, err + } + key, ok := spec.(string) + if !ok { + return nil, errors.New("invalid type, the expression must evaluate to a string") + } + if _, ok := groups[key]; !ok { + groups[key] = []interface{}{} + } + groups[key] = append(groups[key].([]interface{}), element) + } + return groups, nil +} + +func jpfItems(arguments []interface{}) (interface{}, error) { + value := arguments[0].(map[string]interface{}) + arrays := []interface{}{} + for key, item := range value { + var element interface{} = []interface{}{key, item} + arrays = append(arrays, element) + } + + return arrays, nil +} + +func jpfJoin(arguments []interface{}) (interface{}, error) { + sep := arguments[0].(string) + // We can't just do arguments[1].([]string), we have to + // manually convert each item to a string. + arrayStr := []string{} + for _, item := range arguments[1].([]interface{}) { + arrayStr = append(arrayStr, item.(string)) + } + return strings.Join(arrayStr, sep), nil +} + +func jpfKeys(arguments []interface{}) (interface{}, error) { + arg := arguments[0].(map[string]interface{}) + collected := make([]interface{}, 0, len(arg)) + for key := range arg { + collected = append(collected, key) + } + return collected, nil +} + +func jpfLength(arguments []interface{}) (interface{}, error) { + arg := arguments[0] + if c, ok := arg.(string); ok { + return float64(utf8.RuneCountInString(c)), nil + } else if util.IsSliceType(arg) { + v := reflect.ValueOf(arg) + return float64(v.Len()), nil + } else if c, ok := arg.(map[string]interface{}); ok { + return float64(len(c)), nil + } + return nil, errors.New("could not compute length()") +} + +func jpfLower(arguments []interface{}) (interface{}, error) { + return strings.ToLower(arguments[0].(string)), nil +} + +func jpfMap(arguments []interface{}) (interface{}, error) { + exp := arguments[0].(ExpRef) + arr := arguments[1].([]interface{}) + mapped := make([]interface{}, 0, len(arr)) + for _, value := range arr { + current, err := exp(value) + if err != nil { + return nil, err + } + mapped = append(mapped, current) + } + return mapped, nil +} + +func jpfMax(arguments []interface{}) (interface{}, error) { + if items, ok := util.ToArrayNum(arguments[0]); ok { + if len(items) == 0 { + return nil, nil + } + if len(items) == 1 { + return items[0], nil + } + best := items[0] + for _, item := range items[1:] { + if item > best { + best = item + } + } + return best, nil + } + // Otherwise we're dealing with a max() of strings. + items, _ := util.ToArrayStr(arguments[0]) + if len(items) == 0 { + return nil, nil + } + if len(items) == 1 { + return items[0], nil + } + best := items[0] + for _, item := range items[1:] { + if item > best { + best = item + } + } + return best, nil +} + +func jpfMaxBy(arguments []interface{}) (interface{}, error) { + arr := arguments[0].([]interface{}) + exp := arguments[1].(ExpRef) + if len(arr) == 0 { + return nil, nil + } else if len(arr) == 1 { + return arr[0], nil + } + start, err := exp(arr[0]) + if err != nil { + return nil, err + } + switch t := start.(type) { + case float64: + bestVal := t + bestItem := arr[0] + for _, item := range arr[1:] { + result, err := exp(item) + if err != nil { + return nil, err + } + current, ok := result.(float64) + if !ok { + return nil, errors.New("invalid type, must be number") + } + if current > bestVal { + bestVal = current + bestItem = item + } + } + return bestItem, nil + case string: + bestVal := t + bestItem := arr[0] + for _, item := range arr[1:] { + result, err := exp(item) + if err != nil { + return nil, err + } + current, ok := result.(string) + if !ok { + return nil, errors.New("invalid type, must be string") + } + if current > bestVal { + bestVal = current + bestItem = item + } + } + return bestItem, nil + default: + return nil, errors.New("invalid type, must be number of string") + } +} + +func jpfMerge(arguments []interface{}) (interface{}, error) { + final := make(map[string]interface{}) + for _, m := range arguments { + mapped := m.(map[string]interface{}) + for key, value := range mapped { + final[key] = value + } + } + return final, nil +} + +func jpfMin(arguments []interface{}) (interface{}, error) { + if items, ok := util.ToArrayNum(arguments[0]); ok { + if len(items) == 0 { + return nil, nil + } + if len(items) == 1 { + return items[0], nil + } + best := items[0] + for _, item := range items[1:] { + if item < best { + best = item + } + } + return best, nil + } + items, _ := util.ToArrayStr(arguments[0]) + if len(items) == 0 { + return nil, nil + } + if len(items) == 1 { + return items[0], nil + } + best := items[0] + for _, item := range items[1:] { + if item < best { + best = item + } + } + return best, nil +} + +func jpfMinBy(arguments []interface{}) (interface{}, error) { + arr := arguments[0].([]interface{}) + exp := arguments[1].(ExpRef) + if len(arr) == 0 { + return nil, nil + } else if len(arr) == 1 { + return arr[0], nil + } + start, err := exp(arr[0]) + if err != nil { + return nil, err + } + if t, ok := start.(float64); ok { + bestVal := t + bestItem := arr[0] + for _, item := range arr[1:] { + result, err := exp(item) + if err != nil { + return nil, err + } + current, ok := result.(float64) + if !ok { + return nil, errors.New("invalid type, must be number") + } + if current < bestVal { + bestVal = current + bestItem = item + } + } + return bestItem, nil + } else if t, ok := start.(string); ok { + bestVal := t + bestItem := arr[0] + for _, item := range arr[1:] { + result, err := exp(item) + if err != nil { + return nil, err + } + current, ok := result.(string) + if !ok { + return nil, errors.New("invalid type, must be string") + } + if current < bestVal { + bestVal = current + bestItem = item + } + } + return bestItem, nil + } else { + return nil, errors.New("invalid type, must be number of string") + } +} + +func jpfNotNull(arguments []interface{}) (interface{}, error) { + for _, arg := range arguments { + if arg != nil { + return arg, nil + } + } + return nil, nil +} + +func jpfPadImpl( + name string, + arguments []interface{}, + pad func(s string, width int, pad string) string, +) (interface{}, error) { + s := arguments[0].(string) + width, ok := util.ToPositiveInteger(arguments[1]) + if !ok { + return nil, jperror.NotAPositiveInteger(name, "width") + } + chars := " " + if len(arguments) > 2 { + chars = arguments[2].(string) + if len(chars) > 1 { + return nil, fmt.Errorf("invalid value, the function '%s' expects its 'pad' argument to be a string of length 1", name) + } + } + + return pad(s, width, chars), nil +} + +func jpfPadLeft(arguments []interface{}) (interface{}, error) { + return jpfPadImpl("pad_left", arguments, padLeft) +} + +func jpfPadRight(arguments []interface{}) (interface{}, error) { + return jpfPadImpl("pad_right", arguments, padRight) +} + +func padLeft(s string, width int, pad string) string { + length := util.Max(0, width-len(s)) + padding := strings.Repeat(pad, length) + result := fmt.Sprintf("%s%s", padding, s) + return result +} + +func padRight(s string, width int, pad string) string { + length := util.Max(0, width-len(s)) + padding := strings.Repeat(pad, length) + result := fmt.Sprintf("%s%s", s, padding) + return result +} + +func jpfReplace(arguments []interface{}) (interface{}, error) { + subject := arguments[0].(string) + old := arguments[1].(string) + new := arguments[2].(string) + count := -1 + if len(arguments) > 3 { + num, ok := util.ToPositiveInteger(arguments[3]) + if !ok { + return nil, jperror.NotAPositiveInteger("replace", "count") + } + count = num + } + + return strings.Replace(subject, old, new, count), nil +} + +func jpfReverse(arguments []interface{}) (interface{}, error) { + if s, ok := arguments[0].(string); ok { + r := []rune(s) + for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 { + r[i], r[j] = r[j], r[i] + } + return string(r), nil + } + items := arguments[0].([]interface{}) + length := len(items) + reversed := make([]interface{}, length) + for i, item := range items { + reversed[length-(i+1)] = item + } + return reversed, nil +} + +func jpfSort(arguments []interface{}) (interface{}, error) { + if items, ok := util.ToArrayNum(arguments[0]); ok { + d := sort.Float64Slice(items) + sort.Stable(d) + final := make([]interface{}, len(d)) + for i, val := range d { + final[i] = val + } + return final, nil + } + // Otherwise we're dealing with sort()'ing strings. + items, _ := util.ToArrayStr(arguments[0]) + d := sort.StringSlice(items) + sort.Stable(d) + final := make([]interface{}, len(d)) + for i, val := range d { + final[i] = val + } + return final, nil +} + +func jpfSortBy(arguments []interface{}) (interface{}, error) { + arr := arguments[0].([]interface{}) + exp := arguments[1].(ExpRef) + if len(arr) == 0 { + return arr, nil + } else if len(arr) == 1 { + return arr, nil + } + var sortKeys []interface{} + for _, item := range arr { + if value, err := exp(item); err != nil { + return nil, err + } else { + sortKeys = append(sortKeys, value) + } + } + if _, ok := sortKeys[0].(float64); ok { + sortable := &byExprFloat{arr, sortKeys, false} + sort.Stable(sortable) + if sortable.hasError { + return nil, errors.New("error in sort_by comparison") + } + return arr, nil + } else if _, ok := sortKeys[0].(string); ok { + sortable := &byExprString{arr, sortKeys, false} + sort.Stable(sortable) + if sortable.hasError { + return nil, errors.New("error in sort_by comparison") + } + return arr, nil + } else { + return nil, errors.New("invalid type, must be number of string") + } +} + +func jpfSplit(arguments []interface{}) (interface{}, error) { + s := arguments[0].(string) + if len(s) == 0 { + return []interface{}{}, nil + } + + sep := arguments[1].(string) + n := 0 + nSpecified := len(arguments) > 2 + if nSpecified { + num, ok := util.ToPositiveInteger(arguments[2]) + if !ok { + return nil, jperror.NotAPositiveInteger("split", "count") + } + n = num + } + + if nSpecified && n == 0 { + result := []interface{}{s} + return result, nil + } + + count := -1 + if nSpecified { + count = n + 1 + } + splits := strings.SplitN(s, sep, count) + + // convert []string to []interface{} ☹️ + + result := []interface{}{} + for _, split := range splits { + result = append(result, split) + } + return result, nil +} + +func jpfStartsWith(arguments []interface{}) (interface{}, error) { + search := arguments[0].(string) + prefix := arguments[1].(string) + return strings.HasPrefix(search, prefix), nil +} + +func jpfSum(arguments []interface{}) (interface{}, error) { + items, _ := util.ToArrayNum(arguments[0]) + sum := 0.0 + for _, item := range items { + sum += item + } + return sum, nil +} + +func jpfToArray(arguments []interface{}) (interface{}, error) { + if _, ok := arguments[0].([]interface{}); ok { + return arguments[0], nil + } + return arguments[:1:1], nil +} + +func jpfToString(arguments []interface{}) (interface{}, error) { + if v, ok := arguments[0].(string); ok { + return v, nil + } + result, err := json.Marshal(arguments[0]) + if err != nil { + return nil, err + } + return string(result), nil +} + +func jpfToNumber(arguments []interface{}) (interface{}, error) { + arg := arguments[0] + if v, ok := arg.(float64); ok { + return v, nil + } + if v, ok := arg.(string); ok { + conv, err := strconv.ParseFloat(v, 64) + if err != nil { + return nil, nil + } + return conv, nil + } + if _, ok := arg.([]interface{}); ok { + return nil, nil + } + if _, ok := arg.(map[string]interface{}); ok { + return nil, nil + } + if arg == nil { + return nil, nil + } + if arg == true || arg == false { + return nil, nil + } + return nil, errors.New("unknown type") +} + +func jpfTrimImpl( + arguments []interface{}, + trimSpace func(s string, predicate func(r rune) bool) string, + trim func(s string, cutset string) string, +) (interface{}, error) { + s := arguments[0].(string) + cutset := "" + if len(arguments) > 1 { + cutset = arguments[1].(string) + } + + if len(cutset) == 0 { + return trimSpace(s, unicode.IsSpace), nil + } + return trim(s, cutset), nil +} + +func jpfTrim(arguments []interface{}) (interface{}, error) { + return jpfTrimImpl(arguments, strings.TrimFunc, strings.Trim) +} + +func jpfTrimLeft(arguments []interface{}) (interface{}, error) { + return jpfTrimImpl(arguments, strings.TrimLeftFunc, strings.TrimLeft) +} + +func jpfTrimRight(arguments []interface{}) (interface{}, error) { + return jpfTrimImpl(arguments, strings.TrimRightFunc, strings.TrimRight) +} + +func jpfType(arguments []interface{}) (interface{}, error) { + arg := arguments[0] + if _, ok := arg.(float64); ok { + return "number", nil + } + if _, ok := arg.(string); ok { + return "string", nil + } + if _, ok := arg.([]interface{}); ok { + return "array", nil + } + if _, ok := arg.(map[string]interface{}); ok { + return "object", nil + } + if arg == nil { + return "null", nil + } + if arg == true || arg == false { + return "boolean", nil + } + return nil, errors.New("unknown type") +} + +func jpfUpper(arguments []interface{}) (interface{}, error) { + return strings.ToUpper(arguments[0].(string)), nil +} + +func jpfValues(arguments []interface{}) (interface{}, error) { + arg := arguments[0].(map[string]interface{}) + collected := make([]interface{}, 0, len(arg)) + for _, value := range arg { + collected = append(collected, value) + } + return collected, nil +} + +func jpfZip(arguments []interface{}) (interface{}, error) { + // determine how many items are present + // for each array in the result + + count := math.MaxInt + for _, item := range arguments { + arr := item.([]interface{}) + // TODO: use go1.18 min[T constraints.Ordered] generic function + count = int(math.Min(float64(count), float64(len(arr)))) + } + + result := []interface{}{} + + for i := 0; i < count; i++ { + nth := []interface{}{} + for _, item := range arguments { + arr := item.([]interface{}) + nth = append(nth, arr[i]) + } + result = append(result, interface{}(nth)) + } + + return result, nil +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/functions.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/functions.go new file mode 100644 index 0000000000..6e32ff63ee --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/functions.go @@ -0,0 +1,159 @@ +package interpreter + +import ( + "errors" + "fmt" + + jperror "github.com/jmespath-community/go-jmespath/pkg/error" + "github.com/jmespath-community/go-jmespath/pkg/functions" + "github.com/jmespath-community/go-jmespath/pkg/util" +) + +type FunctionCaller interface { + CallFunction(string, []interface{}) (interface{}, error) +} + +type functionEntry struct { + arguments []functions.ArgSpec + handler functions.JpFunction +} + +type functionCaller struct { + functionTable map[string]functionEntry +} + +func NewFunctionCaller(funcs ...functions.FunctionEntry) *functionCaller { + fTable := map[string]functionEntry{} + for _, f := range funcs { + fTable[f.Name] = functionEntry{ + arguments: f.Arguments, + handler: f.Handler, + } + } + return &functionCaller{ + functionTable: fTable, + } +} + +func resolveArgs(name string, function functionEntry, arguments []interface{}) ([]interface{}, error) { + if len(function.arguments) == 0 { + return arguments, nil + } + + variadic := isVariadic(function.arguments) + minExpected := getMinExpected(function.arguments) + maxExpected, hasMax := getMaxExpected(function.arguments) + count := len(arguments) + + if count < minExpected { + return nil, jperror.NotEnoughArgumentsSupplied(name, count, minExpected, variadic) + } + + if hasMax && count > maxExpected { + return nil, jperror.TooManyArgumentsSupplied(name, count, maxExpected) + } + + for i, spec := range function.arguments { + if !spec.Optional || i <= len(arguments)-1 { + userArg := arguments[i] + err := typeCheck(spec, userArg) + if err != nil { + return nil, err + } + } + } + lastIndex := len(function.arguments) - 1 + lastArg := function.arguments[lastIndex] + if lastArg.Variadic { + for i := len(function.arguments) - 1; i < len(arguments); i++ { + userArg := arguments[i] + err := typeCheck(lastArg, userArg) + if err != nil { + return nil, err + } + } + } + return arguments, nil +} + +func isVariadic(arguments []functions.ArgSpec) bool { + for _, spec := range arguments { + if spec.Variadic { + return true + } + } + return false +} + +func getMinExpected(arguments []functions.ArgSpec) int { + expected := 0 + for _, spec := range arguments { + if !spec.Optional { + expected++ + } + } + return expected +} + +func getMaxExpected(arguments []functions.ArgSpec) (int, bool) { + if isVariadic(arguments) { + return 0, false + } + return len(arguments), true +} + +func typeCheck(a functions.ArgSpec, arg interface{}) error { + for _, t := range a.Types { + switch t { + case functions.JpNumber: + if _, ok := arg.(float64); ok { + return nil + } + case functions.JpString: + if _, ok := arg.(string); ok { + return nil + } + case functions.JpArray: + if util.IsSliceType(arg) { + return nil + } + case functions.JpObject: + if _, ok := arg.(map[string]interface{}); ok { + return nil + } + case functions.JpArrayArray: + if util.IsSliceType(arg) { + if _, ok := arg.([]interface{}); ok { + return nil + } + } + case functions.JpArrayNumber: + if _, ok := util.ToArrayNum(arg); ok { + return nil + } + case functions.JpArrayString: + if _, ok := util.ToArrayStr(arg); ok { + return nil + } + case functions.JpAny: + return nil + case functions.JpExpref: + if _, ok := arg.(functions.ExpRef); ok { + return nil + } + } + } + return fmt.Errorf("invalid type for: %v, expected: %#v", arg, a.Types) +} + +func (f *functionCaller) CallFunction(name string, arguments []interface{}) (interface{}, error) { + entry, ok := f.functionTable[name] + if !ok { + return nil, errors.New("unknown function: " + name) + } + resolvedArgs, err := resolveArgs(name, entry, arguments) + if err != nil { + return nil, err + } + return entry.handler(resolvedArgs) +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/interpreter.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/interpreter.go new file mode 100644 index 0000000000..7d5dc95e70 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/interpreter/interpreter.go @@ -0,0 +1,551 @@ +package interpreter + +import ( + "errors" + "math" + "reflect" + "unicode" + "unicode/utf8" + + "github.com/jmespath-community/go-jmespath/pkg/binding" + "github.com/jmespath-community/go-jmespath/pkg/parsing" + "github.com/jmespath-community/go-jmespath/pkg/util" +) + +/* +This is a tree based interpreter. It walks the AST and directly + + interprets the AST to search through a JSON document. +*/ +type Interpreter interface { + Execute(parsing.ASTNode, interface{}) (interface{}, error) +} + +type treeInterpreter struct { + caller FunctionCaller + root interface{} + bindings binding.Bindings +} + +func NewInterpreter(data interface{}, caller FunctionCaller, bindings binding.Bindings) Interpreter { + if bindings == nil { + bindings = binding.NewBindings() + } + return &treeInterpreter{ + caller: caller, + root: data, + bindings: bindings, + } +} + +// Execute takes an ASTNode and input data and interprets the AST directly. +// It will produce the result of applying the JMESPath expression associated +// with the ASTNode to the input data "value". +func (intr *treeInterpreter) Execute(node parsing.ASTNode, value interface{}) (interface{}, error) { + switch node.NodeType { + case parsing.ASTArithmeticUnaryExpression: + expr, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + num, ok := expr.(float64) + if !ok { + return nil, nil + } + switch node.Value { + case parsing.TOKPlus: + return num, nil + case parsing.TOKMinus: + return -num, nil + } + case parsing.ASTArithmeticExpression: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + right, err := intr.Execute(node.Children[1], value) + if err != nil { + return nil, err + } + leftNum, ok := left.(float64) + if !ok { + return nil, nil + } + rightNum, ok := right.(float64) + if !ok { + return nil, nil + } + switch node.Value { + case parsing.TOKPlus: + return leftNum + rightNum, nil + case parsing.TOKMinus: + return leftNum - rightNum, nil + case parsing.TOKStar: + return leftNum * rightNum, nil + case parsing.TOKMultiply: + return leftNum * rightNum, nil + case parsing.TOKDivide: + return leftNum / rightNum, nil + case parsing.TOKModulo: + return math.Mod(leftNum, rightNum), nil + case parsing.TOKDiv: + return math.Floor(leftNum / rightNum), nil + } + case parsing.ASTComparator: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + right, err := intr.Execute(node.Children[1], value) + if err != nil { + return nil, err + } + switch node.Value { + case parsing.TOKEQ: + return util.ObjsEqual(left, right), nil + case parsing.TOKNE: + return !util.ObjsEqual(left, right), nil + } + leftNum, ok := left.(float64) + if !ok { + return nil, nil + } + rightNum, ok := right.(float64) + if !ok { + return nil, nil + } + switch node.Value { + case parsing.TOKGT: + return leftNum > rightNum, nil + case parsing.TOKGTE: + return leftNum >= rightNum, nil + case parsing.TOKLT: + return leftNum < rightNum, nil + case parsing.TOKLTE: + return leftNum <= rightNum, nil + } + case parsing.ASTExpRef: + return func(data interface{}) (interface{}, error) { + return intr.Execute(node.Children[0], data) + }, nil + case parsing.ASTFunctionExpression: + resolvedArgs := []interface{}{} + for _, arg := range node.Children { + current, err := intr.Execute(arg, value) + if err != nil { + return nil, err + } + resolvedArgs = append(resolvedArgs, current) + } + return intr.caller.CallFunction(node.Value.(string), resolvedArgs) + case parsing.ASTField: + key := node.Value.(string) + var result interface{} + if m, ok := value.(map[string]interface{}); ok { + result = m[key] + if result != nil { + return result, nil + } + } + result = intr.fieldFromStruct(node.Value.(string), value) + if result != nil { + return result, nil + } + return nil, nil + case parsing.ASTFilterProjection: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, nil + } + sliceType, ok := left.([]interface{}) + if !ok { + if util.IsSliceType(left) { + return intr.filterProjectionWithReflection(node, left) + } + return nil, nil + } + compareNode := node.Children[2] + collected := []interface{}{} + for _, element := range sliceType { + result, err := intr.Execute(compareNode, element) + if err != nil { + return nil, err + } + if !util.IsFalse(result) { + current, err := intr.Execute(node.Children[1], element) + if err != nil { + return nil, err + } + if current != nil { + collected = append(collected, current) + } + } + } + return collected, nil + case parsing.ASTFlatten: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, nil + } + sliceType, ok := left.([]interface{}) + if !ok { + // If we can't type convert to []interface{}, there's + // a chance this could still work via reflection if we're + // dealing with user provided types. + if util.IsSliceType(left) { + return intr.flattenWithReflection(left) + } + return nil, nil + } + flattened := []interface{}{} + for _, element := range sliceType { + if elementSlice, ok := element.([]interface{}); ok { + flattened = append(flattened, elementSlice...) + } else if util.IsSliceType(element) { + reflectFlat := []interface{}{} + v := reflect.ValueOf(element) + for i := 0; i < v.Len(); i++ { + reflectFlat = append(reflectFlat, v.Index(i).Interface()) + } + flattened = append(flattened, reflectFlat...) + } else { + flattened = append(flattened, element) + } + } + return flattened, nil + case parsing.ASTIdentity, parsing.ASTCurrentNode: + return value, nil + case parsing.ASTRootNode: + return intr.root, nil + case parsing.ASTBindings: + bindings := intr.bindings + for _, child := range node.Children { + if value, err := intr.Execute(child.Children[1], value); err != nil { + return nil, err + } else { + bindings = bindings.Register(child.Children[0].Value.(string), value) + } + } + intr.bindings = bindings + // doesn't mutate value + return value, nil + case parsing.ASTLetExpression: + // save bindings state + bindings := intr.bindings + // retore bindings state + defer func() { + intr.bindings = bindings + }() + // evalute bindings first, then evaluate expression + if _, err := intr.Execute(node.Children[0], value); err != nil { + return nil, err + } else if value, err := intr.Execute(node.Children[1], value); err != nil { + return nil, err + } else { + return value, nil + } + case parsing.ASTVariable: + if value, err := intr.bindings.Get(node.Value.(string)); err != nil { + return nil, err + } else { + return value, nil + } + case parsing.ASTIndex: + if sliceType, ok := value.([]interface{}); ok { + index := node.Value.(int) + if index < 0 { + index += len(sliceType) + } + if index < len(sliceType) && index >= 0 { + return sliceType[index], nil + } + return nil, nil + } + // Otherwise try via reflection. + rv := reflect.ValueOf(value) + if rv.Kind() == reflect.Slice { + index := node.Value.(int) + if index < 0 { + index += rv.Len() + } + if index < rv.Len() && index >= 0 { + v := rv.Index(index) + return v.Interface(), nil + } + } + return nil, nil + case parsing.ASTKeyValPair: + return intr.Execute(node.Children[0], value) + case parsing.ASTLiteral: + return node.Value, nil + case parsing.ASTMultiSelectHash: + collected := make(map[string]interface{}) + for _, child := range node.Children { + current, err := intr.Execute(child, value) + if err != nil { + return nil, err + } + key := child.Value.(string) + collected[key] = current + } + return collected, nil + case parsing.ASTMultiSelectList: + collected := []interface{}{} + for _, child := range node.Children { + current, err := intr.Execute(child, value) + if err != nil { + return nil, err + } + collected = append(collected, current) + } + return collected, nil + case parsing.ASTOrExpression: + matched, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + if util.IsFalse(matched) { + matched, err = intr.Execute(node.Children[1], value) + if err != nil { + return nil, err + } + } + return matched, nil + case parsing.ASTAndExpression: + matched, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + if util.IsFalse(matched) { + return matched, nil + } + return intr.Execute(node.Children[1], value) + case parsing.ASTNotExpression: + matched, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + if util.IsFalse(matched) { + return true, nil + } + return false, nil + case parsing.ASTPipe: + result := value + var err error + for _, child := range node.Children { + result, err = intr.Execute(child, result) + if err != nil { + return nil, err + } + } + return result, nil + case parsing.ASTProjection: + + // projections typically operate on array | slices + // string slicing produces an ASTProjection whose + // first child is an ASTIndexExpression whose + // second child is an ASTSlice + + // we allow execution of the left index-expression + // to return a string only if the AST has this + // specific shape + + allowString := false + firstChild := node.Children[0] + if firstChild.NodeType == parsing.ASTIndexExpression { + nestedChildren := firstChild.Children + if len(nestedChildren) > 1 && nestedChildren[1].NodeType == parsing.ASTSlice { + allowString = true + } + } + + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + + sliceType, ok := left.([]interface{}) + if !ok { + if util.IsSliceType(left) { + return intr.projectWithReflection(node, left) + } + stringType, ok := left.(string) + if allowString && ok { + return stringType, nil + } + return nil, nil + } + collected := []interface{}{} + var current interface{} + for _, element := range sliceType { + current, err = intr.Execute(node.Children[1], element) + if err != nil { + return nil, err + } + if current != nil { + collected = append(collected, current) + } + } + return collected, nil + case parsing.ASTSubexpression, parsing.ASTIndexExpression: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, err + } + if left == nil { + return nil, nil + } + return intr.Execute(node.Children[1], left) + case parsing.ASTSlice: + parts := node.Value.([]*int) + sliceType, ok := value.([]interface{}) + if !ok { + if util.IsSliceType(value) { + return intr.sliceWithReflection(node, value) + } + // string slices is implemented by slicing + // the corresponding array of runes and + // converting the result back to a string + if stringType, ok := value.(string); ok { + runeType := []rune(stringType) + sliceParams := util.MakeSliceParams(parts) + runes, err := util.Slice(runeType, sliceParams) + if err != nil { + return nil, nil + } + return string(runes), nil + } + return nil, nil + } + sliceParams := util.MakeSliceParams(parts) + return util.Slice(sliceType, sliceParams) + case parsing.ASTValueProjection: + left, err := intr.Execute(node.Children[0], value) + if err != nil { + return nil, nil + } + mapType, ok := left.(map[string]interface{}) + if !ok { + return nil, nil + } + values := []interface{}{} + for _, value := range mapType { + values = append(values, value) + } + collected := []interface{}{} + for _, element := range values { + current, err := intr.Execute(node.Children[1], element) + if err != nil { + return nil, err + } + if current != nil { + collected = append(collected, current) + } + } + return collected, nil + } + return nil, errors.New("Unknown AST node: " + node.NodeType.String()) +} + +func (intr *treeInterpreter) fieldFromStruct(key string, value interface{}) interface{} { + rv := reflect.ValueOf(value) + first, n := utf8.DecodeRuneInString(key) + fieldName := string(unicode.ToUpper(first)) + key[n:] + if rv.Kind() == reflect.Struct { + v := rv.FieldByName(fieldName) + if !v.IsValid() { + return nil + } + return v.Interface() + } else if rv.Kind() == reflect.Ptr { + // Handle multiple levels of indirection? + if rv.IsNil() { + return nil + } + rv = rv.Elem() + v := rv.FieldByName(fieldName) + if !v.IsValid() { + return nil + } + return v.Interface() + } + return nil +} + +func (intr *treeInterpreter) flattenWithReflection(value interface{}) (interface{}, error) { + v := reflect.ValueOf(value) + flattened := []interface{}{} + for i := 0; i < v.Len(); i++ { + element := v.Index(i).Interface() + if reflect.TypeOf(element).Kind() == reflect.Slice { + // Then insert the contents of the element + // slice into the flattened slice, + // i.e flattened = append(flattened, mySlice...) + elementV := reflect.ValueOf(element) + for j := 0; j < elementV.Len(); j++ { + flattened = append( + flattened, elementV.Index(j).Interface()) + } + } else { + flattened = append(flattened, element) + } + } + return flattened, nil +} + +func (intr *treeInterpreter) sliceWithReflection(node parsing.ASTNode, value interface{}) (interface{}, error) { + v := reflect.ValueOf(value) + parts := node.Value.([]*int) + sliceParams := make([]util.SliceParam, 3) + for i, part := range parts { + if part != nil { + sliceParams[i].Specified = true + sliceParams[i].N = *part + } + } + final := []interface{}{} + for i := 0; i < v.Len(); i++ { + element := v.Index(i).Interface() + final = append(final, element) + } + return util.Slice(final, sliceParams) +} + +func (intr *treeInterpreter) filterProjectionWithReflection(node parsing.ASTNode, value interface{}) (interface{}, error) { + compareNode := node.Children[2] + collected := []interface{}{} + v := reflect.ValueOf(value) + for i := 0; i < v.Len(); i++ { + element := v.Index(i).Interface() + result, err := intr.Execute(compareNode, element) + if err != nil { + return nil, err + } + if !util.IsFalse(result) { + current, err := intr.Execute(node.Children[1], element) + if err != nil { + return nil, err + } + if current != nil { + collected = append(collected, current) + } + } + } + return collected, nil +} + +func (intr *treeInterpreter) projectWithReflection(node parsing.ASTNode, value interface{}) (interface{}, error) { + collected := []interface{}{} + v := reflect.ValueOf(value) + for i := 0; i < v.Len(); i++ { + element := v.Index(i).Interface() + result, err := intr.Execute(node.Children[1], element) + if err != nil { + return nil, err + } + if result != nil { + collected = append(collected, result) + } + } + return collected, nil +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/astnodetype_string.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/astnodetype_string.go new file mode 100644 index 0000000000..ff9ed360f3 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/astnodetype_string.go @@ -0,0 +1,52 @@ +// Code generated by "stringer -type astNodeType"; DO NOT EDIT. + +package parsing + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ASTEmpty-0] + _ = x[ASTArithmeticExpression-1] + _ = x[ASTArithmeticUnaryExpression-2] + _ = x[ASTComparator-3] + _ = x[ASTCurrentNode-4] + _ = x[ASTRootNode-5] + _ = x[ASTExpRef-6] + _ = x[ASTFunctionExpression-7] + _ = x[ASTField-8] + _ = x[ASTFilterProjection-9] + _ = x[ASTFlatten-10] + _ = x[ASTIdentity-11] + _ = x[ASTIndex-12] + _ = x[ASTIndexExpression-13] + _ = x[ASTKeyValPair-14] + _ = x[ASTLiteral-15] + _ = x[ASTMultiSelectHash-16] + _ = x[ASTMultiSelectList-17] + _ = x[ASTOrExpression-18] + _ = x[ASTAndExpression-19] + _ = x[ASTNotExpression-20] + _ = x[ASTPipe-21] + _ = x[ASTProjection-22] + _ = x[ASTSubexpression-23] + _ = x[ASTSlice-24] + _ = x[ASTValueProjection-25] + _ = x[ASTLetExpression-26] + _ = x[ASTVariable-27] + _ = x[ASTBindings-28] + _ = x[ASTBinding-29] +} + +const _astNodeType_name = "ASTEmptyASTArithmeticExpressionASTArithmeticUnaryExpressionASTComparatorASTCurrentNodeASTRootNodeASTExpRefASTFunctionExpressionASTFieldASTFilterProjectionASTFlattenASTIdentityASTIndexASTIndexExpressionASTKeyValPairASTLiteralASTMultiSelectHashASTMultiSelectListASTOrExpressionASTAndExpressionASTNotExpressionASTPipeASTProjectionASTSubexpressionASTSliceASTValueProjectionASTLetExpressionASTVariableASTBindingsASTBinding" + +var _astNodeType_index = [...]uint16{0, 8, 31, 59, 72, 86, 97, 106, 127, 135, 154, 164, 175, 183, 201, 214, 224, 242, 260, 275, 291, 307, 314, 327, 343, 351, 369, 385, 396, 407, 417} + +func (i astNodeType) String() string { + if i < 0 || i >= astNodeType(len(_astNodeType_index)-1) { + return "astNodeType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _astNodeType_name[_astNodeType_index[i]:_astNodeType_index[i+1]] +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/lexer.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/lexer.go new file mode 100644 index 0000000000..b5be6dacee --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/lexer.go @@ -0,0 +1,464 @@ +package parsing + +import ( + "bytes" + "encoding/json" + "fmt" + "strconv" + "strings" + "unicode/utf8" +) + +type token struct { + tokenType TokType + value string + position int + length int +} + +type TokType int + +const eof = -1 + +// Lexer contains information about the expression being tokenized. +type Lexer struct { + expression string // The expression provided by the user. + currentPos int // The current position in the string. + lastWidth int // The width of the current rune. This + buf bytes.Buffer // Internal buffer used for building up values. +} + +// SyntaxError is the main error used whenever a lexing or parsing error occurs. +type SyntaxError struct { + msg string // Error message displayed to user + Expression string // Expression that generated a SyntaxError + Offset int // The location in the string where the error occurred +} + +func (e SyntaxError) Error() string { + // In the future, it would be good to underline the specific + // location where the error occurred. + return "SyntaxError: " + e.msg +} + +// HighlightLocation will show where the syntax error occurred. +// It will place a "^" character on a line below the expression +// at the point where the syntax error occurred. +func (e SyntaxError) HighlightLocation() string { + return e.Expression + "\n" + strings.Repeat(" ", e.Offset) + "^" +} + +//go:generate stringer -type=TokType +const ( + TOKUnknown TokType = iota + TOKStar + TOKDot + TOKFilter + TOKFlatten + TOKLparen + TOKRparen + TOKLbracket + TOKRbracket + TOKLbrace + TOKRbrace + TOKOr + TOKPipe + TOKNumber + TOKUnquotedIdentifier + TOKQuotedIdentifier + TOKComma + TOKColon + TOKPlus + TOKMinus + TOKMultiply + TOKDivide + TOKModulo + TOKDiv + TOKLT + TOKLTE + TOKGT + TOKGTE + TOKEQ + TOKNE + TOKJSONLiteral + TOKStringLiteral + TOKCurrent + TOKRoot + TOKExpref + TOKAnd + TOKNot + TOKVarref + TOKAssign + TOKEOF +) + +var basicTokens = map[rune]TokType{ + '.': TOKDot, + '*': TOKStar, + ',': TOKComma, + ':': TOKColon, + '{': TOKLbrace, + '}': TOKRbrace, + ']': TOKRbracket, // tLbracket not included because it could be "[]" + '(': TOKLparen, + ')': TOKRparen, + '@': TOKCurrent, + '+': TOKPlus, + '%': TOKModulo, + '\u2212': TOKMinus, + '\u00d7': TOKMultiply, + '\u00f7': TOKDivide, +} + +// Bit mask for [a-zA-Z_] shifted down 64 bits to fit in a single uint64. +// When using this bitmask just be sure to shift the rune down 64 bits +// before checking against identifierStartBits. +const identifierStartBits uint64 = 576460745995190270 + +// Bit mask for [a-zA-Z0-9], 128 bits -> 2 uint64s. +var identifierTrailingBits = [2]uint64{287948901175001088, 576460745995190270} + +var whiteSpace = map[rune]bool{ + ' ': true, '\t': true, '\n': true, '\r': true, +} + +func (t token) String() string { + return fmt.Sprintf("Token{%+v, %s, %d, %d}", + t.tokenType, t.value, t.position, t.length) +} + +// NewLexer creates a new JMESPath lexer. +func NewLexer() *Lexer { + lexer := Lexer{} + return &lexer +} + +func (lexer *Lexer) next() rune { + if lexer.currentPos >= len(lexer.expression) { + lexer.lastWidth = 0 + return eof + } + r, w := utf8.DecodeRuneInString(lexer.expression[lexer.currentPos:]) + lexer.lastWidth = w + lexer.currentPos += w + return r +} + +func (lexer *Lexer) back() { + lexer.currentPos -= lexer.lastWidth +} + +func (lexer *Lexer) peek() rune { + t := lexer.next() + lexer.back() + return t +} + +// tokenize takes an expression and returns corresponding tokens. +func (lexer *Lexer) Tokenize(expression string) ([]token, error) { + var tokens []token + lexer.expression = expression + lexer.currentPos = 0 + lexer.lastWidth = 0 +loop: + for { + r := lexer.next() + if identifierStartBits&(1<<(uint64(r)-64)) > 0 { + t := lexer.consumeUnquotedIdentifier(TOKUnquotedIdentifier) + tokens = append(tokens, t) + } else if val, ok := basicTokens[r]; ok { + // Basic single char token. + t := token{ + tokenType: val, + value: string(r), + position: lexer.currentPos - lexer.lastWidth, + length: 1, + } + tokens = append(tokens, t) + } else if r == '-' { + p := lexer.peek() + if p >= '0' && p <= '9' { + t := lexer.consumeNumber() + tokens = append(tokens, t) + } else { + t := token{ + tokenType: TOKMinus, + value: string(r), + position: lexer.currentPos - lexer.lastWidth, + length: 1, + } + tokens = append(tokens, t) + } + } else if r >= '0' && r <= '9' { + t := lexer.consumeNumber() + tokens = append(tokens, t) + } else if r == '/' { + t := lexer.matchOrElse(r, '/', TOKDiv, TOKDivide) + tokens = append(tokens, t) + } else if r == '[' { + t := lexer.consumeLBracket() + tokens = append(tokens, t) + } else if r == '"' { + t, err := lexer.consumeQuotedIdentifier() + if err != nil { + return tokens, err + } + tokens = append(tokens, t) + } else if r == '\'' { + t, err := lexer.consumeRawStringLiteral() + if err != nil { + return tokens, err + } + tokens = append(tokens, t) + } else if r == '`' { + t, err := lexer.consumeLiteral() + if err != nil { + return tokens, err + } + tokens = append(tokens, t) + } else if r == '|' { + t := lexer.matchOrElse(r, '|', TOKOr, TOKPipe) + tokens = append(tokens, t) + } else if r == '<' { + t := lexer.matchOrElse(r, '=', TOKLTE, TOKLT) + tokens = append(tokens, t) + } else if r == '>' { + t := lexer.matchOrElse(r, '=', TOKGTE, TOKGT) + tokens = append(tokens, t) + } else if r == '!' { + t := lexer.matchOrElse(r, '=', TOKNE, TOKNot) + tokens = append(tokens, t) + } else if r == '$' { + t := lexer.consumeUnquotedIdentifier(TOKVarref) + if t.value == "$" { + t.tokenType = TOKRoot + } + tokens = append(tokens, t) + } else if r == '=' { + t := lexer.matchOrElse(r, '=', TOKEQ, TOKAssign) + tokens = append(tokens, t) + } else if r == '&' { + t := lexer.matchOrElse(r, '&', TOKAnd, TOKExpref) + tokens = append(tokens, t) + } else if r == eof { + break loop + } else if _, ok := whiteSpace[r]; ok { + // Ignore whitespace + } else { + return tokens, lexer.syntaxError(fmt.Sprintf("Unknown char: %s", strconv.QuoteRuneToASCII(r))) + } + } + tokens = append(tokens, token{TOKEOF, "", len(lexer.expression), 0}) + return tokens, nil +} + +// Consume characters until the ending rune "r" is reached. +// If the end of the expression is reached before seeing the +// terminating rune "r", then an error is returned. +// If no error occurs then the matching substring is returned. +// The returned string will not include the ending rune. +func (lexer *Lexer) consumeUntil(end rune) (string, error) { + start := lexer.currentPos + current := lexer.next() + for current != end && current != eof { + if current == '\\' && lexer.peek() != eof { + lexer.next() + } + current = lexer.next() + } + if lexer.lastWidth == 0 { + // Then we hit an EOF so we never reached the closing + // delimiter. + return "", SyntaxError{ + msg: "Unclosed delimiter: " + string(end), + Expression: lexer.expression, + Offset: len(lexer.expression), + } + } + return lexer.expression[start : lexer.currentPos-lexer.lastWidth], nil +} + +func (lexer *Lexer) consumeLiteral() (token, error) { + start := lexer.currentPos + value, err := lexer.consumeUntil('`') + if err != nil { + return token{}, err + } + value = strings.ReplaceAll(value, "\\`", "`") + return token{ + tokenType: TOKJSONLiteral, + value: value, + position: start, + length: len(value), + }, nil +} + +func (lexer *Lexer) consumeRawStringLiteral() (token, error) { + start := lexer.currentPos + currentIndex := start + current := lexer.next() + escapes := map[rune]struct{}{ + '\'': {}, + '\\': {}, + } + for current != '\'' && lexer.peek() != eof { + if current == '\\' { + escape := lexer.peek() + if _, ok := escapes[escape]; ok { + chunk := lexer.expression[currentIndex : lexer.currentPos-1] + lexer.buf.WriteString(chunk) + lexer.buf.WriteString(string(escape)) + lexer.next() + currentIndex = lexer.currentPos + } + } + current = lexer.next() + } + if lexer.lastWidth == 0 { + // Then we hit an EOF so we never reached the closing + // delimiter. + return token{}, SyntaxError{ + msg: "Unclosed delimiter: '", + Expression: lexer.expression, + Offset: len(lexer.expression), + } + } + if currentIndex < lexer.currentPos { + lexer.buf.WriteString(lexer.expression[currentIndex : lexer.currentPos-1]) + } + value := lexer.buf.String() + // Reset the buffer so it can reused again. + lexer.buf.Reset() + return token{ + tokenType: TOKStringLiteral, + value: value, + position: start, + length: len(value), + }, nil +} + +func (lexer *Lexer) syntaxError(msg string) SyntaxError { + return SyntaxError{ + msg: msg, + Expression: lexer.expression, + Offset: lexer.currentPos - 1, + } +} + +// Checks for a two char token, otherwise matches a single character +// token. This is used whenever a two char token overlaps a single +// char token, e.g. "||" -> tPipe, "|" -> tOr. +func (lexer *Lexer) matchOrElse(first rune, second rune, matchedType TokType, singleCharType TokType) token { + start := lexer.currentPos - lexer.lastWidth + nextRune := lexer.next() + var t token + if nextRune == second { + t = token{ + tokenType: matchedType, + value: string(first) + string(second), + position: start, + length: 2, + } + } else { + lexer.back() + t = token{ + tokenType: singleCharType, + value: string(first), + position: start, + length: 1, + } + } + return t +} + +func (lexer *Lexer) consumeLBracket() token { + // There's three options here: + // 1. A filter expression "[?" + // 2. A flatten operator "[]" + // 3. A bare rbracket "[" + start := lexer.currentPos - lexer.lastWidth + nextRune := lexer.next() + var t token + if nextRune == '?' { + t = token{ + tokenType: TOKFilter, + value: "[?", + position: start, + length: 2, + } + } else if nextRune == ']' { + t = token{ + tokenType: TOKFlatten, + value: "[]", + position: start, + length: 2, + } + } else { + t = token{ + tokenType: TOKLbracket, + value: "[", + position: start, + length: 1, + } + lexer.back() + } + return t +} + +func (lexer *Lexer) consumeQuotedIdentifier() (token, error) { + start := lexer.currentPos + value, err := lexer.consumeUntil('"') + if err != nil { + return token{}, err + } + var decoded string + asJSON := []byte("\"" + value + "\"") + if err := json.Unmarshal(asJSON, &decoded); err != nil { + return token{}, err + } + return token{ + tokenType: TOKQuotedIdentifier, + value: decoded, + position: start - 1, + length: len(decoded), + }, nil +} + +func (lexer *Lexer) consumeUnquotedIdentifier(matchedType TokType) token { + // Consume runes until we reach the end of an unquoted + // identifier. + start := lexer.currentPos - lexer.lastWidth + for { + r := lexer.next() + if r < 0 || r > 128 || identifierTrailingBits[uint64(r)/64]&(1<<(uint64(r)%64)) == 0 { + lexer.back() + break + } + } + value := lexer.expression[start:lexer.currentPos] + return token{ + tokenType: matchedType, + value: value, + position: start, + length: lexer.currentPos - start, + } +} + +func (lexer *Lexer) consumeNumber() token { + // Consume runes until we reach something that's not a number. + start := lexer.currentPos - lexer.lastWidth + for { + r := lexer.next() + if r < '0' || r > '9' { + lexer.back() + break + } + } + value := lexer.expression[start:lexer.currentPos] + return token{ + tokenType: TOKNumber, + value: value, + position: start, + length: lexer.currentPos - start, + } +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/parser.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/parser.go new file mode 100644 index 0000000000..a97d559559 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/parser.go @@ -0,0 +1,727 @@ +package parsing + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" +) + +type astNodeType int + +//go:generate stringer -type astNodeType +const ( + ASTEmpty astNodeType = iota + ASTArithmeticExpression + ASTArithmeticUnaryExpression + ASTComparator + ASTCurrentNode + ASTRootNode + ASTExpRef + ASTFunctionExpression + ASTField + ASTFilterProjection + ASTFlatten + ASTIdentity + ASTIndex + ASTIndexExpression + ASTKeyValPair + ASTLiteral + ASTMultiSelectHash + ASTMultiSelectList + ASTOrExpression + ASTAndExpression + ASTNotExpression + ASTPipe + ASTProjection + ASTSubexpression + ASTSlice + ASTValueProjection + ASTLetExpression + ASTVariable + ASTBindings + ASTBinding +) + +// ASTNode represents the abstract syntax tree of a JMESPath expression. +type ASTNode struct { + NodeType astNodeType + Value interface{} + Children []ASTNode +} + +func (node ASTNode) String() string { + return node.PrettyPrint(0) +} + +// PrettyPrint will pretty print the parsed AST. +// The AST is an implementation detail and this pretty print +// function is provided as a convenience method to help with +// debugging. You should not rely on its output as the internal +// structure of the AST may change at any time. +func (node ASTNode) PrettyPrint(indent int) string { + spaces := strings.Repeat(" ", indent) + output := fmt.Sprintf("%s%s {\n", spaces, node.NodeType) + nextIndent := indent + 2 + if node.Value != nil { + if converted, ok := node.Value.(fmt.Stringer); ok { + // Account for things like comparator nodes + // that are enums with a String() method. + output += fmt.Sprintf("%svalue: %s\n", strings.Repeat(" ", nextIndent), converted.String()) + } else { + output += fmt.Sprintf("%svalue: %#v\n", strings.Repeat(" ", nextIndent), node.Value) + } + } + lastIndex := len(node.Children) + if lastIndex > 0 { + output += fmt.Sprintf("%schildren: {\n", strings.Repeat(" ", nextIndent)) + childIndent := nextIndent + 2 + for _, elem := range node.Children { + output += elem.PrettyPrint(childIndent) + } + output += fmt.Sprintf("%s}\n", strings.Repeat(" ", nextIndent)) + } + output += fmt.Sprintf("%s}\n", spaces) + return output +} + +var bindingPowers = map[TokType]int{ + TOKEOF: 0, + TOKVarref: 0, + TOKUnquotedIdentifier: 0, + TOKQuotedIdentifier: 0, + TOKRbracket: 0, + TOKRparen: 0, + TOKComma: 0, + TOKRbrace: 0, + TOKNumber: 0, + TOKCurrent: 0, + TOKExpref: 0, + TOKColon: 0, + TOKAssign: 1, + TOKPipe: 1, + TOKOr: 2, + TOKAnd: 3, + TOKEQ: 5, + TOKLT: 5, + TOKLTE: 5, + TOKGT: 5, + TOKGTE: 5, + TOKNE: 5, + TOKMinus: 6, + TOKPlus: 6, + TOKDiv: 7, + TOKDivide: 7, + TOKModulo: 7, + TOKMultiply: 7, + TOKFlatten: 9, + TOKStar: 20, + TOKFilter: 21, + TOKDot: 40, + TOKNot: 45, + TOKLbrace: 50, + TOKLbracket: 55, + TOKLparen: 60, +} + +// Parser holds state about the current expression being parsed. +type Parser struct { + expression string + tokens []token + index int +} + +// NewParser creates a new JMESPath parser. +func NewParser() *Parser { + p := Parser{} + return &p +} + +// Parse will compile a JMESPath expression. +func (p *Parser) Parse(expression string) (ASTNode, error) { + lexer := NewLexer() + p.expression = expression + tokens, err := lexer.Tokenize(expression) + if err != nil { + return ASTNode{}, err + } + return p.parseTokens(tokens) +} + +func (p *Parser) parseTokens(tokens []token) (ASTNode, error) { + p.tokens = tokens + p.index = 0 + parsed, err := p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + if p.current() != TOKEOF { + return ASTNode{}, p.syntaxError(fmt.Sprintf( + "Unexpected token at the end of the expression: %s", p.current())) + } + return parsed, nil +} + +func (p *Parser) parseExpression(bindingPower int) (ASTNode, error) { + var err error + leftToken := p.lookaheadToken(0) + p.advance() + leftNode, err := p.nud(leftToken) + if err != nil { + return ASTNode{}, err + } + currentToken := p.current() + for bindingPower < bindingPowers[currentToken] { + p.advance() + leftNode, err = p.led(currentToken, leftNode) + if err != nil { + return ASTNode{}, err + } + currentToken = p.current() + } + return leftNode, nil +} + +func (p *Parser) parseIndexExpression() (ASTNode, error) { + if p.lookahead(0) == TOKColon || p.lookahead(1) == TOKColon { + return p.parseSliceExpression() + } + indexStr := p.lookaheadToken(0).value + parsedInt, err := strconv.Atoi(indexStr) + if err != nil { + return ASTNode{}, err + } + indexNode := ASTNode{NodeType: ASTIndex, Value: parsedInt} + p.advance() + if err := p.match(TOKRbracket); err != nil { + return ASTNode{}, err + } + return indexNode, nil +} + +func (p *Parser) parseSliceExpression() (ASTNode, error) { + parts := []*int{nil, nil, nil} + index := 0 + current := p.current() + for current != TOKRbracket && index < 3 { + if current == TOKColon { + index++ + if index == 3 { + return ASTNode{}, p.syntaxErrorToken("Too many colons in slice expression", p.lookaheadToken(0)) + } + p.advance() + } else if current == TOKNumber { + parsedInt, err := strconv.Atoi(p.lookaheadToken(0).value) + if err != nil { + return ASTNode{}, err + } + parts[index] = &parsedInt + p.advance() + } else { + return ASTNode{}, p.syntaxError( + "Expected tColon or tNumber" + ", received: " + p.current().String()) + } + current = p.current() + } + if err := p.match(TOKRbracket); err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTSlice, + Value: parts, + }, nil +} + +func isKeyword(token token, keyword string) bool { + return token.tokenType == TOKUnquotedIdentifier && token.value == keyword +} + +func (p *Parser) matchKeyword(keyword string) error { + if isKeyword(p.lookaheadToken(0), keyword) { + p.advance() + return nil + } + return p.syntaxError("Expected keyword " + keyword + ", received: " + p.current().String()) +} + +func (p *Parser) match(tokenType TokType) error { + if p.current() == tokenType { + p.advance() + return nil + } + return p.syntaxError("Expected " + tokenType.String() + ", received: " + p.current().String()) +} + +func (p *Parser) led(tokenType TokType, node ASTNode) (ASTNode, error) { + switch tokenType { + case TOKDot: + if p.current() != TOKStar { + right, err := p.parseDotRHS(bindingPowers[TOKDot]) + return ASTNode{ + NodeType: ASTSubexpression, + Children: []ASTNode{node, right}, + }, err + } + p.advance() + right, err := p.parseProjectionRHS(bindingPowers[TOKDot]) + return ASTNode{ + NodeType: ASTValueProjection, + Children: []ASTNode{node, right}, + }, err + case TOKPipe: + right, err := p.parseExpression(bindingPowers[TOKPipe]) + return ASTNode{NodeType: ASTPipe, Children: []ASTNode{node, right}}, err + case TOKOr: + right, err := p.parseExpression(bindingPowers[TOKOr]) + return ASTNode{NodeType: ASTOrExpression, Children: []ASTNode{node, right}}, err + case TOKAnd: + right, err := p.parseExpression(bindingPowers[TOKAnd]) + return ASTNode{NodeType: ASTAndExpression, Children: []ASTNode{node, right}}, err + case TOKLparen: + if node.NodeType != ASTField { + // 0 - first func arg or closing paren. + // -1 - '(' token + // -2 - invalid function "name" token. + return ASTNode{}, p.syntaxErrorToken("Invalid node as function name.", p.lookaheadToken(-2)) + } + name := node.Value + args, err := p.parseCommaSeparatedExpressionsUntilToken(TOKRparen) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTFunctionExpression, + Value: name, + Children: args, + }, nil + case TOKFilter: + return p.parseFilter(node) + case TOKFlatten: + left := ASTNode{NodeType: ASTFlatten, Children: []ASTNode{node}} + right, err := p.parseProjectionRHS(bindingPowers[TOKFlatten]) + return ASTNode{ + NodeType: ASTProjection, + Children: []ASTNode{left, right}, + }, err + case TOKPlus, TOKMinus, TOKStar, TOKMultiply, TOKDivide, TOKModulo, TOKDiv: + right, err := p.parseExpression(bindingPowers[tokenType]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTArithmeticExpression, + Value: tokenType, + Children: []ASTNode{node, right}, + }, nil + case TOKAssign: + { + right, err := p.parseExpression(bindingPowers[0]) + return ASTNode{ + NodeType: ASTBinding, + Children: []ASTNode{node, right}, + }, err + } + case TOKEQ, TOKNE, TOKGT, TOKGTE, TOKLT, TOKLTE: + return p.parseComparatorExpression(node, tokenType) + case TOKLbracket: + tokenType := p.current() + var right ASTNode + var err error + if tokenType == TOKNumber || tokenType == TOKColon { + right, err = p.parseIndexExpression() + if err != nil { + return ASTNode{}, err + } + return p.projectIfSlice(node, right) + } + // Otherwise this is a projection. + if err := p.match(TOKStar); err != nil { + return ASTNode{}, err + } + if err := p.match(TOKRbracket); err != nil { + return ASTNode{}, err + } + right, err = p.parseProjectionRHS(bindingPowers[TOKStar]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTProjection, + Children: []ASTNode{node, right}, + }, nil + } + return ASTNode{}, p.syntaxError("Unexpected token: " + tokenType.String()) +} + +func (p *Parser) nud(token token) (ASTNode, error) { + switch token.tokenType { + case TOKVarref: + return ASTNode{ + NodeType: ASTVariable, + Value: token.value, + }, nil + case TOKJSONLiteral: + var parsed interface{} + err := json.Unmarshal([]byte(token.value), &parsed) + if err != nil { + return ASTNode{}, err + } + return ASTNode{NodeType: ASTLiteral, Value: parsed}, nil + case TOKStringLiteral: + return ASTNode{NodeType: ASTLiteral, Value: token.value}, nil + case TOKUnquotedIdentifier: + if token.value == "let" && p.current() == TOKVarref { + return p.parseLetExpression() + } else { + return ASTNode{ + NodeType: ASTField, + Value: token.value, + }, nil + } + case TOKQuotedIdentifier: + node := ASTNode{NodeType: ASTField, Value: token.value} + if p.current() == TOKLparen { + return ASTNode{}, p.syntaxErrorToken("Can't have quoted identifier as function name.", token) + } + return node, nil + case TOKPlus: + expr, err := p.parseExpression(bindingPowers[TOKPlus]) + return ASTNode{NodeType: ASTArithmeticUnaryExpression, Value: TOKPlus, Children: []ASTNode{expr}}, err + case TOKMinus: + expr, err := p.parseExpression(bindingPowers[TOKMinus]) + return ASTNode{NodeType: ASTArithmeticUnaryExpression, Value: TOKMinus, Children: []ASTNode{expr}}, err + case TOKStar: + left := ASTNode{NodeType: ASTIdentity} + var right ASTNode + var err error + if p.current() == TOKRbracket { + right = ASTNode{NodeType: ASTIdentity} + } else { + right, err = p.parseProjectionRHS(bindingPowers[TOKStar]) + } + return ASTNode{NodeType: ASTValueProjection, Children: []ASTNode{left, right}}, err + case TOKFilter: + return p.parseFilter(ASTNode{NodeType: ASTIdentity}) + case TOKLbrace: + return p.parseMultiSelectHash() + case TOKFlatten: + left := ASTNode{ + NodeType: ASTFlatten, + Children: []ASTNode{{NodeType: ASTIdentity}}, + } + right, err := p.parseProjectionRHS(bindingPowers[TOKFlatten]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{NodeType: ASTProjection, Children: []ASTNode{left, right}}, nil + case TOKLbracket: + tokenType := p.current() + + if tokenType == TOKNumber || tokenType == TOKColon { + right, err := p.parseIndexExpression() + if err != nil { + return ASTNode{}, err + } + return p.projectIfSlice(ASTNode{NodeType: ASTIdentity}, right) + } else if tokenType == TOKStar && p.lookahead(1) == TOKRbracket { + p.advance() + p.advance() + right, err := p.parseProjectionRHS(bindingPowers[TOKStar]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTProjection, + Children: []ASTNode{{NodeType: ASTIdentity}, right}, + }, nil + } else { + return p.parseMultiSelectList() + } + case TOKCurrent: + return ASTNode{NodeType: ASTCurrentNode}, nil + case TOKRoot: + return ASTNode{NodeType: ASTRootNode}, nil + case TOKExpref: + expression, err := p.parseExpression(bindingPowers[TOKExpref]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{NodeType: ASTExpRef, Children: []ASTNode{expression}}, nil + case TOKNot: + expression, err := p.parseExpression(bindingPowers[TOKNot]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{NodeType: ASTNotExpression, Children: []ASTNode{expression}}, nil + case TOKLparen: + expression, err := p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + if err := p.match(TOKRparen); err != nil { + return ASTNode{}, err + } + return expression, nil + case TOKEOF: + return ASTNode{}, p.syntaxErrorToken("Incomplete expression", token) + } + + return ASTNode{}, p.syntaxErrorToken("Invalid token: "+token.tokenType.String(), token) +} + +func (p *Parser) parseMultiSelectList() (ASTNode, error) { + var expressions []ASTNode + for { + expression, err := p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + expressions = append(expressions, expression) + if p.current() == TOKRbracket { + break + } + err = p.match(TOKComma) + if err != nil { + return ASTNode{}, err + } + } + err := p.match(TOKRbracket) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTMultiSelectList, + Children: expressions, + }, nil +} + +func (p *Parser) parseMultiSelectHash() (ASTNode, error) { + var children []ASTNode + for { + keyToken := p.lookaheadToken(0) + if err := p.match(TOKUnquotedIdentifier); err != nil { + if err := p.match(TOKQuotedIdentifier); err != nil { + return ASTNode{}, p.syntaxError("Expected tQuotedIdentifier or tUnquotedIdentifier") + } + } + keyName := keyToken.value + err := p.match(TOKColon) + if err != nil { + return ASTNode{}, err + } + value, err := p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + node := ASTNode{ + NodeType: ASTKeyValPair, + Value: keyName, + Children: []ASTNode{value}, + } + children = append(children, node) + if p.current() == TOKComma { + err := p.match(TOKComma) + if err != nil { + return ASTNode{}, nil + } + } else if p.current() == TOKRbrace { + err := p.match(TOKRbrace) + if err != nil { + return ASTNode{}, nil + } + break + } + } + return ASTNode{ + NodeType: ASTMultiSelectHash, + Children: children, + }, nil +} + +func (p *Parser) projectIfSlice(left ASTNode, right ASTNode) (ASTNode, error) { + indexExpr := ASTNode{ + NodeType: ASTIndexExpression, + Children: []ASTNode{left, right}, + } + if right.NodeType == ASTSlice { + right, err := p.parseProjectionRHS(bindingPowers[TOKStar]) + return ASTNode{ + NodeType: ASTProjection, + Children: []ASTNode{indexExpr, right}, + }, err + } + return indexExpr, nil +} + +func (p *Parser) parseFilter(node ASTNode) (ASTNode, error) { + var right, condition ASTNode + var err error + condition, err = p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + if err := p.match(TOKRbracket); err != nil { + return ASTNode{}, err + } + if p.current() == TOKFlatten { + right = ASTNode{NodeType: ASTIdentity} + } else { + right, err = p.parseProjectionRHS(bindingPowers[TOKFilter]) + if err != nil { + return ASTNode{}, err + } + } + + return ASTNode{ + NodeType: ASTFilterProjection, + Children: []ASTNode{node, right, condition}, + }, nil +} + +func (p *Parser) parseDotRHS(bindingPower int) (ASTNode, error) { + lookahead := p.current() + if tokensOneOf([]TokType{TOKQuotedIdentifier, TOKUnquotedIdentifier, TOKStar}, lookahead) { + return p.parseExpression(bindingPower) + } else if lookahead == TOKLbracket { + if err := p.match(TOKLbracket); err != nil { + return ASTNode{}, err + } + return p.parseMultiSelectList() + } else if lookahead == TOKLbrace { + if err := p.match(TOKLbrace); err != nil { + return ASTNode{}, err + } + return p.parseMultiSelectHash() + } + return ASTNode{}, p.syntaxError("Expected identifier, lbracket, or lbrace") +} + +func (p *Parser) parseProjectionRHS(bindingPower int) (ASTNode, error) { + current := p.current() + if bindingPowers[current] < 10 { + return ASTNode{NodeType: ASTIdentity}, nil + } else if current == TOKLbracket { + return p.parseExpression(bindingPower) + } else if current == TOKFilter { + return p.parseExpression(bindingPower) + } else if current == TOKDot { + err := p.match(TOKDot) + if err != nil { + return ASTNode{}, err + } + return p.parseDotRHS(bindingPower) + } else { + return ASTNode{}, p.syntaxError("Error") + } +} + +func (p *Parser) parseLetExpression() (ASTNode, error) { + bindings, err := p.parseCommaSeparatedExpressionsUntilKeyword("in") + if err != nil { + return ASTNode{}, err + } + expression, err := p.parseExpression(0) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTLetExpression, + Children: []ASTNode{ + { + NodeType: ASTBindings, + Children: bindings, + }, + expression, + }, + }, nil +} + +func (p *Parser) parseCommaSeparatedExpressionsUntilKeyword(keyword string) ([]ASTNode, error) { + return p.parseCommaSeparatedExpressionsUntil( + func() bool { + return isKeyword(p.lookaheadToken(0), keyword) + }, + func() error { return p.matchKeyword(keyword) }) +} + +func (p *Parser) parseCommaSeparatedExpressionsUntilToken(endToken TokType) ([]ASTNode, error) { + return p.parseCommaSeparatedExpressionsUntil( + func() bool { return p.current() == endToken }, + func() error { return p.match(endToken) }) +} + +func (p *Parser) parseCommaSeparatedExpressionsUntil(isEndToken func() bool, matchEndToken func() error) ([]ASTNode, error) { + var nodes []ASTNode + for !isEndToken() { + expression, err := p.parseExpression(0) + if err != nil { + return []ASTNode{}, err + } + if p.current() == TOKComma { + if err := p.match(TOKComma); err != nil { + return []ASTNode{}, err + } + } + nodes = append(nodes, expression) + } + if err := matchEndToken(); err != nil { + return []ASTNode{}, err + } + return nodes, nil +} + +func (p *Parser) parseComparatorExpression(left ASTNode, tokenType TokType) (ASTNode, error) { + right, err := p.parseExpression(bindingPowers[tokenType]) + if err != nil { + return ASTNode{}, err + } + return ASTNode{ + NodeType: ASTComparator, + Value: tokenType, + Children: []ASTNode{left, right}, + }, nil +} + +func (p *Parser) lookahead(number int) TokType { + return p.lookaheadToken(number).tokenType +} + +func (p *Parser) current() TokType { + return p.lookahead(0) +} + +func (p *Parser) lookaheadToken(number int) token { + return p.tokens[p.index+number] +} + +func (p *Parser) advance() { + p.index++ +} + +func tokensOneOf(elements []TokType, token TokType) bool { + for _, elem := range elements { + if elem == token { + return true + } + } + return false +} + +func (p *Parser) syntaxError(msg string) SyntaxError { + return SyntaxError{ + msg: msg, + Expression: p.expression, + Offset: p.lookaheadToken(0).position, + } +} + +// Create a SyntaxError based on the provided token. +// This differs from syntaxError() which creates a SyntaxError +// based on the current lookahead token. +func (p *Parser) syntaxErrorToken(msg string, t token) SyntaxError { + return SyntaxError{ + msg: msg, + Expression: p.expression, + Offset: t.position, + } +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/toktype_string.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/toktype_string.go new file mode 100644 index 0000000000..469d2b7490 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/parsing/toktype_string.go @@ -0,0 +1,62 @@ +// Code generated by "stringer -type=TokType"; DO NOT EDIT. + +package parsing + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TOKUnknown-0] + _ = x[TOKStar-1] + _ = x[TOKDot-2] + _ = x[TOKFilter-3] + _ = x[TOKFlatten-4] + _ = x[TOKLparen-5] + _ = x[TOKRparen-6] + _ = x[TOKLbracket-7] + _ = x[TOKRbracket-8] + _ = x[TOKLbrace-9] + _ = x[TOKRbrace-10] + _ = x[TOKOr-11] + _ = x[TOKPipe-12] + _ = x[TOKNumber-13] + _ = x[TOKUnquotedIdentifier-14] + _ = x[TOKQuotedIdentifier-15] + _ = x[TOKComma-16] + _ = x[TOKColon-17] + _ = x[TOKPlus-18] + _ = x[TOKMinus-19] + _ = x[TOKMultiply-20] + _ = x[TOKDivide-21] + _ = x[TOKModulo-22] + _ = x[TOKDiv-23] + _ = x[TOKLT-24] + _ = x[TOKLTE-25] + _ = x[TOKGT-26] + _ = x[TOKGTE-27] + _ = x[TOKEQ-28] + _ = x[TOKNE-29] + _ = x[TOKJSONLiteral-30] + _ = x[TOKStringLiteral-31] + _ = x[TOKCurrent-32] + _ = x[TOKRoot-33] + _ = x[TOKExpref-34] + _ = x[TOKAnd-35] + _ = x[TOKNot-36] + _ = x[TOKVarref-37] + _ = x[TOKAssign-38] + _ = x[TOKEOF-39] +} + +const _TokType_name = "TOKUnknownTOKStarTOKDotTOKFilterTOKFlattenTOKLparenTOKRparenTOKLbracketTOKRbracketTOKLbraceTOKRbraceTOKOrTOKPipeTOKNumberTOKUnquotedIdentifierTOKQuotedIdentifierTOKCommaTOKColonTOKPlusTOKMinusTOKMultiplyTOKDivideTOKModuloTOKDivTOKLTTOKLTETOKGTTOKGTETOKEQTOKNETOKJSONLiteralTOKStringLiteralTOKCurrentTOKRootTOKExprefTOKAndTOKNotTOKVarrefTOKAssignTOKEOF" + +var _TokType_index = [...]uint16{0, 10, 17, 23, 32, 42, 51, 60, 71, 82, 91, 100, 105, 112, 121, 142, 161, 169, 177, 184, 192, 203, 212, 221, 227, 232, 238, 243, 249, 254, 259, 273, 289, 299, 306, 315, 321, 327, 336, 345, 351} + +func (i TokType) String() string { + if i < 0 || i >= TokType(len(_TokType_index)-1) { + return "TokType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _TokType_name[_TokType_index[i]:_TokType_index[i+1]] +} diff --git a/vendor/github.com/jmespath-community/go-jmespath/pkg/util/util.go b/vendor/github.com/jmespath-community/go-jmespath/pkg/util/util.go new file mode 100644 index 0000000000..90df205436 --- /dev/null +++ b/vendor/github.com/jmespath-community/go-jmespath/pkg/util/util.go @@ -0,0 +1,251 @@ +package util + +import ( + "errors" + "math" + "reflect" + + "golang.org/x/exp/constraints" +) + +// IsFalse determines if an object is false based on the JMESPath spec. +// JMESPath defines false values to be any of: +// - An empty string array, or hash. +// - The boolean value false. +// - nil. +func IsFalse(value interface{}) bool { + switch v := value.(type) { + case bool: + return !v + case []interface{}: + return len(v) == 0 + case map[string]interface{}: + return len(v) == 0 + case string: + return len(v) == 0 + case nil: + return true + } + // Try the reflection cases before returning false. + rv := reflect.ValueOf(value) + switch rv.Kind() { + case reflect.Struct: + // A struct type will never be false, even if + // all of its values are the zero type. + return false + case reflect.Slice, reflect.Map: + return rv.Len() == 0 + case reflect.Ptr: + if rv.IsNil() { + return true + } + // If it's a pointer type, we'll try to deref the pointer + // and evaluate the pointer value for isFalse. + element := rv.Elem() + return IsFalse(element.Interface()) + } + return false +} + +// ObjsEqual is a generic object equality check. +// It will take two arbitrary objects and recursively determine +// if they are equal. +func ObjsEqual(left interface{}, right interface{}) bool { + return reflect.DeepEqual(left, right) +} + +// SliceParam refers to a single part of a slice. +// A slice consists of a start, a stop, and a step, similar to +// python slices. +type SliceParam struct { + N int + Specified bool +} + +// Slice supports [start:stop:step] style slicing that's supported in JMESPath. +func Slice[T interface{} | rune](slice []T, parts []SliceParam) ([]T, error) { + computed, err := computeSliceParams(len(slice), parts) + if err != nil { + return nil, err + } + start, stop, step := computed[0], computed[1], computed[2] + result := []T{} + if step > 0 { + for i := start; i < stop; i += step { + result = append(result, slice[i]) + } + } else { + for i := start; i > stop; i += step { + result = append(result, slice[i]) + } + } + return result, nil +} + +func MakeSliceParams(parts []*int) []SliceParam { + sliceParams := make([]SliceParam, 3) + for i, part := range parts { + if part != nil { + sliceParams[i].Specified = true + sliceParams[i].N = *part + } + } + return sliceParams +} + +func computeSliceParams(length int, parts []SliceParam) ([]int, error) { + var start, stop, step int + if !parts[2].Specified { + step = 1 + } else if parts[2].N == 0 { + return nil, errors.New("invalid slice, step cannot be 0") + } else { + step = parts[2].N + } + var stepValueNegative bool + if step < 0 { + stepValueNegative = true + } else { + stepValueNegative = false + } + + if !parts[0].Specified { + if stepValueNegative { + start = length - 1 + } else { + start = 0 + } + } else { + start = capSlice(length, parts[0].N, step) + } + + if !parts[1].Specified { + if stepValueNegative { + stop = -1 + } else { + stop = length + } + } else { + stop = capSlice(length, parts[1].N, step) + } + return []int{start, stop, step}, nil +} + +func capSlice(length int, actual int, step int) int { + if actual < 0 { + actual += length + if actual < 0 { + if step < 0 { + actual = -1 + } else { + actual = 0 + } + } + } else if actual >= length { + if step < 0 { + actual = length - 1 + } else { + actual = length + } + } + return actual +} + +// ToArrayArray converts an empty interface type to a slice of slices. +// If any element in the array cannot be converted, then nil is returned +// along with a second value of false. +func ToArrayArray(data interface{}) ([][]interface{}, bool) { + result := [][]interface{}{} + arr, ok := data.([]interface{}) + if !ok { + return nil, false + } + for _, item := range arr { + nested, ok := item.([]interface{}) + if !ok { + return nil, false + } + result = append(result, nested) + } + return result, true +} + +// ToArrayNum converts an empty interface type to a slice of float64. +// If any element in the array cannot be converted, then nil is returned +// along with a second value of false. +func ToArrayNum(data interface{}) ([]float64, bool) { + // Is there a better way to do this with reflect? + if d, ok := data.([]interface{}); ok { + result := make([]float64, len(d)) + for i, el := range d { + item, ok := el.(float64) + if !ok { + return nil, false + } + result[i] = item + } + return result, true + } + return nil, false +} + +// ToArrayStr converts an empty interface type to a slice of strings. +// If any element in the array cannot be converted, then nil is returned +// along with a second value of false. If the input data could be entirely +// converted, then the converted data, along with a second value of true, +// will be returned. +func ToArrayStr(data interface{}) ([]string, bool) { + // Is there a better way to do this with reflect? + if d, ok := data.([]interface{}); ok { + result := make([]string, len(d)) + for i, el := range d { + item, ok := el.(string) + if !ok { + return nil, false + } + result[i] = item + } + return result, true + } + return nil, false +} + +// ToInteger converts an empty interface to a integer. +// It expects the empty interface to represent a float64 JSON number. +// If the empty interface cannot be converted or if the number +// is not an integer, the function returns a second boolean value false. +func ToInteger(v interface{}) (int, bool) { + if num, ok := v.(float64); ok { + if math.Floor(num) != num { + return 0, false + } + return int(math.Floor(num)), true + } + return 0, false +} + +func ToPositiveInteger(v interface{}) (int, bool) { + num, ok := ToInteger(v) + return num, ok && num >= 0 +} + +func IsSliceType(v interface{}) bool { + if v == nil { + return false + } + return reflect.TypeOf(v).Kind() == reflect.Slice +} + +func Min[T constraints.Ordered](a T, b T) T { + if a < b { + return a + } + return b +} + +func Max[T constraints.Ordered](a T, b T) T { + if a > b { + return a + } + return b +} diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go new file mode 100644 index 0000000000..a9392af7c1 --- /dev/null +++ b/vendor/golang.org/x/exp/constraints/constraints.go @@ -0,0 +1,52 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +// +// This type is redundant since Go 1.21 introduced [cmp.Ordered]. +type Ordered interface { + Integer | Float | ~string +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b00172aa40..53f76ea66d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -838,6 +838,16 @@ github.com/jellydator/ttlcache/v3 # github.com/jinzhu/now v1.1.5 ## explicit; go 1.12 github.com/jinzhu/now +# github.com/jmespath-community/go-jmespath v1.1.1 +## explicit; go 1.18 +github.com/jmespath-community/go-jmespath +github.com/jmespath-community/go-jmespath/pkg/api +github.com/jmespath-community/go-jmespath/pkg/binding +github.com/jmespath-community/go-jmespath/pkg/error +github.com/jmespath-community/go-jmespath/pkg/functions +github.com/jmespath-community/go-jmespath/pkg/interpreter +github.com/jmespath-community/go-jmespath/pkg/parsing +github.com/jmespath-community/go-jmespath/pkg/util # github.com/jonboulle/clockwork v0.5.0 ## explicit; go 1.21 github.com/jonboulle/clockwork @@ -2444,6 +2454,7 @@ golang.org/x/crypto/ssh/internal/bcrypt_pbkdf golang.org/x/crypto/ssh/knownhosts # golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac ## explicit; go 1.22.0 +golang.org/x/exp/constraints golang.org/x/exp/maps golang.org/x/exp/slices golang.org/x/exp/slog