From 19f0e7ebfe66c9e211097fb406d8949a7aecec1f Mon Sep 17 00:00:00 2001 From: Tobias Dahlberg Date: Thu, 10 Nov 2022 17:03:45 +0100 Subject: [PATCH] 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 Signed-off-by: Tobias Dahlberg Co-authored-by: shuting Co-authored-by: Prateek Pandey Co-authored-by: Vyankatesh Kudtarkar --- pkg/engine/imageVerify.go | 6 +- pkg/engine/jsonutils/traverse.go | 3 +- pkg/engine/mutation_test.go | 366 ++++++++++++++++++++++++++ pkg/engine/utils/utils.go | 29 -- pkg/engine/utils/utils_test.go | 8 - pkg/engine/variables/vars.go | 27 +- pkg/engine/variables/vars_test.go | 12 - pkg/utils/jsonpointer/pointer.go | 244 +++++++++++++++++ pkg/utils/jsonpointer/pointer_test.go | 227 ++++++++++++++++ 9 files changed, 848 insertions(+), 74 deletions(-) create mode 100644 pkg/utils/jsonpointer/pointer.go create mode 100644 pkg/utils/jsonpointer/pointer_test.go diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index aa747b77f966..7b42ad293357 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -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 diff --git a/pkg/engine/jsonutils/traverse.go b/pkg/engine/jsonutils/traverse.go index 738e48454806..136fe403b90a 100644 --- a/pkg/engine/jsonutils/traverse.go +++ b/pkg/engine/jsonutils/traverse.go @@ -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 } diff --git a/pkg/engine/mutation_test.go b/pkg/engine/mutation_test.go index 6097af4f2bb4..7125f4e71654 100644 --- a/pkg/engine/mutation_test.go +++ b/pkg/engine/mutation_test.go @@ -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) + } + }) + } +} diff --git a/pkg/engine/utils/utils.go b/pkg/engine/utils/utils.go index 6df018b33f71..7ec66a1a2d2e 100644 --- a/pkg/engine/utils/utils.go +++ b/pkg/engine/utils/utils.go @@ -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() -} diff --git a/pkg/engine/utils/utils_test.go b/pkg/engine/utils/utils_test.go index 536780fb2958..173228350390 100644 --- a/pkg/engine/utils/utils_test.go +++ b/pkg/engine/utils/utils_test.go @@ -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/")) -} diff --git a/pkg/engine/variables/vars.go b/pkg/engine/variables/vars.go index ce9e41bed614..ecc35d454161 100644 --- a/pkg/engine/variables/vars.go +++ b/pkg/engine/variables/vars.go @@ -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 diff --git a/pkg/engine/variables/vars_test.go b/pkg/engine/variables/vars_test.go index 445bf2cf8643..7fd5b8da5094 100644 --- a/pkg/engine/variables/vars_test.go +++ b/pkg/engine/variables/vars_test.go @@ -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")) -} diff --git a/pkg/utils/jsonpointer/pointer.go b/pkg/utils/jsonpointer/pointer.go new file mode 100644 index 000000000000..99e53e18ac24 --- /dev/null +++ b/pkg/utils/jsonpointer/pointer.go @@ -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:] +} diff --git a/pkg/utils/jsonpointer/pointer_test.go b/pkg/utils/jsonpointer/pointer_test.go new file mode 100644 index 000000000000..08024e2ea8b6 --- /dev/null +++ b/pkg/utils/jsonpointer/pointer_test.go @@ -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) + } + }) + } +}