1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

fix: add parsing of json pointers to support special chars (#3578 #3616) (#4767)

* Added jsonpointer package that supports parsing of paths and JSON pointers that can yield either a JSON pointer string or JMESPath string.
* Replaced the use of `strings.Split` and `strings.Join` in places where paths are converted to JMESPaths.

Signed-off-by: Tobias Dahlberg <tobias.dahlberg@sinch.com>

Signed-off-by: Tobias Dahlberg <tobias.dahlberg@sinch.com>
Co-authored-by: shuting <shuting@nirmata.com>
Co-authored-by: Prateek Pandey <prateek.pandey@nirmata.com>
Co-authored-by: Vyankatesh Kudtarkar <vyankateshkd@gmail.com>
This commit is contained in:
Tobias Dahlberg 2022-11-10 17:03:45 +01:00 committed by GitHub
parent 977dcc38a2
commit 19f0e7ebfe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 848 additions and 74 deletions

View file

@ -14,11 +14,11 @@ import (
"github.com/kyverno/kyverno/pkg/cosign"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
engineUtils "github.com/kyverno/kyverno/pkg/engine/utils"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/logging"
"github.com/kyverno/kyverno/pkg/registryclient"
apiutils "github.com/kyverno/kyverno/pkg/utils/api"
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
"github.com/kyverno/kyverno/pkg/utils/wildcard"
"github.com/pkg/errors"
"go.uber.org/multierr"
@ -170,8 +170,8 @@ func (iv *imageVerifier) verify(imageVerify kyvernov1.ImageVerification, images
continue
}
jmespath := engineUtils.JsonPointerToJMESPath(imageInfo.Pointer)
changed, err := iv.policyContext.JSONContext.HasChanged(jmespath)
pointer := jsonpointer.ParsePath(imageInfo.Pointer).JMESPath()
changed, err := iv.policyContext.JSONContext.HasChanged(pointer)
if err == nil && !changed {
iv.logger.V(4).Info("no change in image, skipping check", "image", image)
continue

View file

@ -3,6 +3,7 @@ package jsonutils
import (
"fmt"
"strconv"
"strings"
"github.com/kyverno/kyverno/pkg/utils"
)
@ -101,7 +102,7 @@ func (t *Traversal) traverseObject(object map[string]interface{}, path string) (
}
}
value, err := t.traverseJSON(element, path+"/"+key)
value, err := t.traverseJSON(element, path+"/"+strings.ReplaceAll(key, "/", `\/`))
if err != nil {
return nil, err
}

View file

@ -1587,3 +1587,369 @@ func Test_RuleSelectorMutate(t *testing.T) {
t.Error("rule 1 patches dont match")
}
}
func Test_SpecialCharacters(t *testing.T) {
t.Parallel()
tests := []struct {
name string
policyRaw []byte
documentRaw []byte
want [][]byte
}{
{
name: "regex_replace",
policyRaw: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "regex-replace-all-demo"
},
"spec": {
"background": false,
"rules": [
{
"name": "retention-adjust",
"match": {
"any": [
{
"resources": {
"kinds": [
"Deployment"
]
}
}
]
},
"mutate": {
"patchStrategicMerge": {
"metadata": {
"labels": {
"retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
}
}
}
}
}
]
}
}`),
documentRaw: []byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "busybox",
"labels": {
"app": "busybox",
"retention": "days_37"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": {
"app": "busybox"
}
},
"template": {
"metadata": {
"labels": {
"app": "busybox"
}
},
"spec": {
"containers": [
{
"image": "busybox:1.28",
"name": "busybox",
"command": [
"sleep",
"9999"
]
}
]
}
}
}
}`),
want: [][]byte{
[]byte(`{"op":"replace","path":"/metadata/labels/retention","value":"days_30"}`),
},
},
{
name: "regex_replace_with_slash",
policyRaw: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "regex-replace-all-demo"
},
"spec": {
"background": false,
"rules": [
{
"name": "retention-adjust",
"match": {
"any": [
{
"resources": {
"kinds": [
"Deployment"
]
}
}
]
},
"mutate": {
"patchStrategicMerge": {
"metadata": {
"labels": {
"corp.com/retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
}
}
}
}
}
]
}
}`),
documentRaw: []byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "busybox",
"labels": {
"app": "busybox",
"corp.com/retention": "days_37"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": {
"app": "busybox"
}
},
"template": {
"metadata": {
"labels": {
"app": "busybox"
}
},
"spec": {
"containers": [
{
"image": "busybox:1.28",
"name": "busybox",
"command": [
"sleep",
"9999"
]
}
]
}
}
}
}`),
want: [][]byte{
[]byte(`{"op":"replace","path":"/metadata/labels/corp.com~1retention","value":"days_30"}`),
},
},
{
name: "regex_replace_with_hyphen",
policyRaw: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "regex-replace-all-demo"
},
"spec": {
"background": false,
"rules": [
{
"name": "retention-adjust",
"match": {
"any": [
{
"resources": {
"kinds": [
"Deployment"
]
}
}
]
},
"mutate": {
"patchStrategicMerge": {
"metadata": {
"labels": {
"corp-retention": "{{ regex_replace_all('([0-9])([0-9])', '{{ @ }}', '${1}0') }}"
}
}
}
}
}
]
}
}`),
documentRaw: []byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "busybox",
"labels": {
"app": "busybox",
"corp-retention": "days_37"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": {
"app": "busybox"
}
},
"template": {
"metadata": {
"labels": {
"app": "busybox"
}
},
"spec": {
"containers": [
{
"image": "busybox:1.28",
"name": "busybox",
"command": [
"sleep",
"9999"
]
}
]
}
}
}
}`),
want: [][]byte{
[]byte(`{"op":"replace","path":"/metadata/labels/corp-retention","value":"days_30"}`),
},
},
{
name: "to_upper_with_hyphen",
policyRaw: []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {
"name": "to-upper-demo"
},
"spec": {
"rules": [
{
"name": "format-deploy-zone",
"match": {
"any": [
{
"resources": {
"kinds": [
"Deployment"
]
}
}
]
},
"mutate": {
"patchStrategicMerge": {
"metadata": {
"labels": {
"deploy-zone": "{{ to_upper('{{@}}') }}"
}
}
}
}
}
]
}
}`),
documentRaw: []byte(`{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {
"name": "busybox",
"labels": {
"app": "busybox",
"deploy-zone": "eu-central-1"
}
},
"spec": {
"replicas": 3,
"selector": {
"matchLabels": {
"app": "busybox"
}
},
"template": {
"metadata": {
"labels": {
"app": "busybox"
}
},
"spec": {
"containers": [
{
"image": "busybox:1.28",
"name": "busybox",
"command": [
"sleep",
"9999"
]
}
]
}
}
}
}`),
want: [][]byte{
[]byte(`{"op":"replace","path":"/metadata/labels/deploy-zone","value":"EU-CENTRAL-1"}`),
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Parse policy document.
var policy kyverno.ClusterPolicy
if err := json.Unmarshal(tt.policyRaw, &policy); err != nil {
t.Error(err)
}
// Parse resource document.
resource, err := utils.ConvertToUnstructured(tt.documentRaw)
if err != nil {
t.Fatalf("ConvertToUnstructured() error = %v", err)
}
// Create JSON context and add the resource.
ctx := context.NewContext()
err = ctx.AddResource(resource.Object)
if err != nil {
t.Fatalf("ctx.AddResource() error = %v", err)
}
// Create policy context.
policyContext := &PolicyContext{
Policy: &policy,
JSONContext: ctx,
NewResource: *resource,
}
// Mutate and make sure that we got the expected amount of rules.
patches := Mutate(policyContext).GetPatches()
if !reflect.DeepEqual(patches, tt.want) {
t.Errorf("Mutate() got patches %s, expected %s", patches, tt.want)
}
})
}
}

View file

@ -1,10 +1,6 @@
package utils
import (
"fmt"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch/v5"
commonAnchor "github.com/kyverno/kyverno/pkg/engine/anchor"
"github.com/kyverno/kyverno/pkg/logging"
@ -72,28 +68,3 @@ func GetAnchorsFromMap(anchorsMap map[string]interface{}) map[string]interface{}
return result
}
func JsonPointerToJMESPath(jsonPointer string) string {
var sb strings.Builder
tokens := strings.Split(jsonPointer, "/")
i := 0
for _, t := range tokens {
if t == "" {
continue
}
if _, err := strconv.Atoi(t); err == nil {
sb.WriteString(fmt.Sprintf("[%s]", t))
continue
}
if i > 0 {
sb.WriteString(".")
}
sb.WriteString(t)
i++
}
return sb.String()
}

View file

@ -27,11 +27,3 @@ func TestGetAnchorsFromMap_ThereAreNoAnchors(t *testing.T) {
actualMap := GetAnchorsFromMap(unmarshalled)
assert.Equal(t, len(actualMap), 0)
}
func Test_JsonPointerToJMESPath(t *testing.T) {
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("a/b/c/1//d"))
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("/a/b/c/1/d"))
assert.Equal(t, "a.b.c[1].d", JsonPointerToJMESPath("/a/b/c/1/d/"))
assert.Equal(t, "a[1].b.c[1].d", JsonPointerToJMESPath("a/1/b/c/1/d"))
assert.Equal(t, "a[1].b.c[1].d[2]", JsonPointerToJMESPath("/a/1/b/c/1/d/2/"))
}

View file

@ -15,6 +15,7 @@ import (
"github.com/kyverno/kyverno/pkg/engine/context"
jsonUtils "github.com/kyverno/kyverno/pkg/engine/jsonutils"
"github.com/kyverno/kyverno/pkg/engine/operator"
"github.com/kyverno/kyverno/pkg/utils/jsonpointer"
)
var RegexVariables = regexp.MustCompile(`(?:^|[^\\])(\{\{(?:\{[^{}]*\}|[^{}])*\}\})`)
@ -352,13 +353,11 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
if _, err := ctx.Query("target"); err != nil {
pathPrefix = "request.object"
}
path := getJMESPath(data.Path)
var val string
if strings.HasPrefix(path, "[") {
val = fmt.Sprintf("%s%s", pathPrefix, path)
} else {
val = fmt.Sprintf("%s.%s", pathPrefix, path)
}
// Convert path to JMESPath for current identifier.
// Skip 2 elements (e.g. mutate.overlay | validate.pattern) plus "foreach" if it is part of the pointer.
// Prefix the pointer with pathPrefix.
val := jsonpointer.ParsePath(data.Path).SkipPast("foreach").SkipN(2).Prepend(strings.Split(pathPrefix, ".")...).JMESPath()
variable = strings.Replace(variable, "@", val, -1)
}
@ -421,20 +420,6 @@ func IsDeleteRequest(ctx context.EvalInterface) bool {
return false
}
var regexPathDigit = regexp.MustCompile(`\.?([\d])\.?`)
// getJMESPath converts path to JMESPath format
func getJMESPath(rawPath string) string {
tokens := strings.Split(rawPath, "/")[3:] // skip "/" + 2 elements (e.g. mutate.overlay | validate.pattern)
if strings.Contains(rawPath, "foreach") {
tokens = strings.Split(rawPath, "/")[5:] // skip "/" + 4 elements (e.g. mutate.foreach/list/overlay | validate.mutate.foreach/list/pattern)
}
path := strings.Join(tokens, ".")
b := regexPathDigit.ReplaceAll([]byte(path), []byte("[$1]."))
result := strings.Trim(string(b), ".")
return result
}
func substituteVarInPattern(prefix, pattern, variable string, value interface{}) (string, error) {
var stringToSubstitute string

View file

@ -1177,15 +1177,3 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%v", pattern), "{{request.object.metadata.annotations.target}}")
}
func Test_getJMESPath(t *testing.T) {
assert.Equal(t, "spec.containers[0]", getJMESPath("/validate/pattern/spec/containers/0"))
assert.Equal(t, "spec.containers[0].volumes[1]", getJMESPath("/validate/pattern/spec/containers/0/volumes/1"))
assert.Equal(t, "[0]", getJMESPath("/mutate/overlay/0"))
}
func Test_getJMESPathForForeach(t *testing.T) {
assert.Equal(t, "spec.containers[0]", getJMESPath("/validate/foreach/0/pattern/spec/containers/0"))
assert.Equal(t, "spec.containers[0].volumes[1]", getJMESPath("/validate/foreach/0/pattern/spec/containers/0/volumes/1"))
assert.Equal(t, "[0]", getJMESPath("/mutate/foreach/0/overlay/0"))
}

View file

@ -0,0 +1,244 @@
package jsonpointer
import (
"fmt"
"strconv"
"strings"
"unicode"
"unicode/utf8"
"k8s.io/utils/strings/slices"
)
// Pointer is a JSON pointer that can be retrieved as either as a RFC6901 string or as a JMESPath formatted string.
type Pointer []string
// unquoted identifiers must only contain these characters.
var unquotedFirstCharRangeTable = &unicode.RangeTable{
R16: []unicode.Range16{
{Lo: '(', Hi: '(', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
{Lo: 'A', Hi: 'Z', Stride: 1},
{Lo: '_', Hi: '_', Stride: 1},
{Lo: 'a', Hi: 'z', Stride: 1},
},
}
// unquoted identifiers can contain any combination of these runes.
var unquotedStringRangeTable = &unicode.RangeTable{
R16: []unicode.Range16{
{Lo: ')', Hi: ')', Stride: 1}, // Special non-standard. Used by policy documents for matching attributes.
{Lo: '0', Hi: '9', Stride: 1},
{Lo: 'A', Hi: 'Z', Stride: 1},
{Lo: '_', Hi: '_', Stride: 1},
{Lo: 'a', Hi: 'z', Stride: 1},
},
}
// a quoted identifier can contain any of these characters as is.
var unescapedCharRangeTable = &unicode.RangeTable{
R16: []unicode.Range16{
{Lo: 0x20, Hi: 0x21, Stride: 1},
{Lo: 0x23, Hi: 0x5B, Stride: 1},
{Lo: 0x5D, Hi: 0x1EFF, Stride: 1},
},
R32: []unicode.Range32{
{Lo: 0x1000, Hi: 0x10FFFF, Stride: 1},
},
LatinOffset: 0x1EFF - unicode.MaxLatin1,
}
// some special characters must be escaped to be possible to use inside a quoted identifier.
var escapeCharMap = map[rune]string{
'"': `\"`, // quotation mark
'\\': `\\`, // reverse solidus
'/': `\/`, // solidus
'\b': `\b`, // backspace
'\f': `\f`, // form feed
'\n': `\n`, // line feed
'\r': `\r`, // carriage return
'\t': `\t`, // tab
}
const initialCapacity = 10 // pointers should start with a non-zero capacity to lower the amount of re-allocations done by append.
// New will return an empty Pointer.
func New() Pointer {
return make([]string, 0, initialCapacity)
}
// Parse will parse the string as a JSON pointer according to RFC 6901.
func Parse(s string) Pointer {
pointer := New()
replacer := strings.NewReplacer("~1", "/", "~0", "~", `\\`, `\`, `\"`, `"`)
for _, component := range strings.FieldsFunc(s, func(r rune) bool {
return r == '/'
}) {
pointer = append(pointer, replacer.Replace(component))
}
return pointer
}
// ParsePath will parse the raw path and return it in the form of a Pointer.
func ParsePath(rawPath string) Pointer {
// Start with a slice with a non-zero capacity to avoid reallocation for most paths.
pointer := New()
// Use a string builder and a flush function to append path components to the slice.
sb := strings.Builder{}
flush := func() {
s := sb.String()
if s != "" {
pointer = append(pointer, s)
}
sb.Reset()
}
var pos int
var escaped, quoted bool
for i, width := 0, 0; i <= len(rawPath); i += width {
var r rune
r, width = utf8.DecodeRuneInString(rawPath[i:])
if r == utf8.RuneError && width == 1 {
break
}
switch {
case escaped: // previous character was a backslash.
sb.WriteRune(r)
escaped = !escaped
case r == '\\': // escape character
escaped = !escaped
case r == '"': // quoted strings
if quoted {
s, _ := strconv.Unquote(rawPath[pos : i+width])
sb.WriteString(s)
}
quoted = !quoted
case r == '/' && !quoted:
flush()
case r == utf8.RuneError: // end of string
flush()
return pointer
default:
sb.WriteRune(r)
}
pos = i + width
}
// This is unreachable but we must return something.
return pointer
}
// JMESPath will return the Pointer in the form of a JMESPath string.
func (p Pointer) JMESPath() string {
sb := strings.Builder{}
for _, component := range p {
// Components that are valid unsigned integers are treated as indices.
if _, err := strconv.ParseUint(component, 10, 64); err == nil {
sb.WriteRune('[')
sb.WriteString(component)
sb.WriteRune(']')
continue
}
// Write a dot before we write anything, as long as buffer is not empty.
if sb.Len() > 0 {
sb.WriteRune('.')
}
// If the component starts with a character that is valid as an initial character for an identifier
// and the remaining characters are also valid for an unquoted identifier then we can append it to
// the JMESPath as is.
if ch, _ := utf8.DecodeRuneInString(component); unicode.Is(unquotedFirstCharRangeTable, ch) &&
strings.IndexFunc(component, func(r rune) bool {
return !unicode.Is(unquotedStringRangeTable, r)
}) == -1 {
sb.WriteString(component)
continue
}
// The component contains characters that are not allowed for unquoted identifiers, so we need to take some extra
// steps to ensure that it's a valid, quoted identifier.
sb.WriteRune('"')
for _, r := range component {
// Any character in the range table of allowed runes can be written as is.
if unicode.Is(unescapedCharRangeTable, r) {
sb.WriteRune(r)
continue
}
// Convert special characters to their escaped sequence.
if escaped, ok := escapeCharMap[r]; ok {
sb.WriteString(escaped)
continue
}
// All other characters must be written as unicode escape sequences ay 16 bits a piece.
if i := utf8.RuneLen(r); i <= 2 {
// Rune is 1 or 2 bytes.
_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
} else {
_, _ = fmt.Fprintf(&sb, "\\u%04x", r&0xffff)
_, _ = fmt.Fprintf(&sb, "\\u%04x", r>>16)
}
}
sb.WriteRune('"')
}
// Return the JMESPath.
return sb.String()
}
// String will return the pointer as a string (RFC6901).
func (p Pointer) String() string {
sb := strings.Builder{}
replacer := strings.NewReplacer("~", "~0", "/", "~1", `\`, `\\`, `"`, `\"`)
for _, component := range p {
if sb.Len() > 0 {
sb.WriteRune('/')
}
_, _ = replacer.WriteString(&sb, component)
}
// Return the pointer.
return sb.String()
}
// Append will return a Pointer with the strings appended.
func (p Pointer) Append(s ...string) Pointer {
return append(p, s...)
}
// Prepend will return a Pointer prefixed with the specified strings.
func (p Pointer) Prepend(s ...string) Pointer {
return append(s, p...)
}
// AppendPath will parse the string as a JSON pointer and return a new pointer.
func (p Pointer) AppendPath(s string) Pointer {
return append(p, ParsePath(s)...)
}
// SkipN will return a new Pointer where the first N element are stripped.
func (p Pointer) SkipN(n int) Pointer {
if n > len(p)-1 {
return []string{}
}
return p[n:]
}
// SkipPast will return a new Pointer where every element upto and including the specified string has been stripped off.
func (p Pointer) SkipPast(s string) Pointer {
return p[slices.Index(p, s)+1:]
}

View file

@ -0,0 +1,227 @@
package jsonpointer
import (
"reflect"
"testing"
)
func TestParsePath(t *testing.T) {
type args struct {
rawPath string
}
tests := []struct {
name string
args args
want Pointer
}{
{
name: "plain",
args: args{
rawPath: "a/b/c",
},
want: []string{"a", "b", "c"},
},
{
name: "hyphen",
args: args{
rawPath: "a/b-b/c",
},
want: []string{"a", "b-b", "c"},
},
{
name: "quotes",
args: args{
rawPath: `a/"b/b"/c`,
},
want: []string{"a", "b/b", "c"},
},
{
name: "escaped_slash",
args: args{
rawPath: `a/b\/b/c`,
},
want: []string{"a", "b/b", "c"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ParsePath(tt.args.rawPath); !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParsePath() = %v, want %v", got, tt.want)
}
})
}
}
func TestPointer_Append(t *testing.T) {
type args struct {
s []string
}
tests := []struct {
name string
p Pointer
args args
want Pointer
}{
{
p: []string{"a", "b"},
args: args{
s: []string{"c", "d"},
},
want: []string{"a", "b", "c", "d"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.Append(tt.args.s...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Append() = %v, want %v", got, tt.want)
}
})
}
}
func TestPointer_AppendPath(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
p Pointer
args args
want Pointer
}{
{
name: "",
p: []string{"a", "b", "c"},
args: args{
s: `d/e\/e/f`,
},
want: []string{"a", "b", "c", "d", "e/e", "f"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.AppendPath(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("AppendPath() = %v, want %v", got, tt.want)
}
})
}
}
func TestPointer_JMESPath(t *testing.T) {
tests := []struct {
name string
p Pointer
want string
}{
{
p: []string{"a", "b", "c", "3", "e/e", "f"},
want: `a.b.c[3]."e/e".f`,
},
{
p: []string{"a", "b", "c", "3", "e/e", "f"},
want: `a.b.c[3]."e/e".f`,
},
{
name: "hangul",
p: []string{"a", "바나나", "c", "3", "e/e", "f"},
want: `a."바나나".c[3]."e/e".f`,
},
{
name: "tab",
p: []string{"a", "a\tb", "c"},
want: `a."a\tb".c`,
},
{
name: "bell",
p: []string{"a", "a\aa", "c", "3", "e/e", "f"},
want: `a."a\u0007a".c[3]."e/e".f`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.JMESPath(); got != tt.want {
t.Errorf("JMESPath() = %v, want %v", got, tt.want)
}
})
}
}
func TestPointer_String(t *testing.T) {
tests := []struct {
name string
p Pointer
want string
}{
{
p: []string{"a", "b", "c"},
want: "a/b/c",
},
{
p: []string{"a", "b/b", "c~c"},
want: `a/b~1b/c~0c`,
},
{
p: []string{"a", `b\b`, `c"c`},
want: `a/b\\b/c\"c`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.String(); got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}
func TestPointer_Prepend(t *testing.T) {
type args struct {
s []string
}
tests := []struct {
name string
p Pointer
args args
want Pointer
}{
{
p: []string{"c", "d", "e"},
args: args{
s: []string{"a", "b"},
},
want: []string{"a", "b", "c", "d", "e"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.p.Prepend(tt.args.s...); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Prepend() = %v, want %v", got, tt.want)
}
})
}
}
func TestParse(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want Pointer
}{
{
args: args{
s: "a/b~1c/~0d",
},
want: []string{"a", "b/c", "~d"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Parse(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("Parse() = %v, want %v", got, tt.want)
}
})
}
}