From 49dfe1ae0f0fe13a43e8a5e1ae3889fca61bd753 Mon Sep 17 00:00:00 2001 From: waw666waw666 <184799977+waw666waw666@users.noreply.github.com> Date: Fri, 13 Nov 2026 01:27:32 +0000 Subject: [PATCH] Fix Delete matching keys that contain a backslash findKeyStart, which Delete uses to find the key of the last path element, JSON-unescaped the path key before comparing it with the (unescaped) keys in the document. Get, Set and EachKey compare the path as is, so for a path with a backslash Delete looked for a different key than Get: Delete(`{"a\\b":1}`, `a\b`) left the key in place, because `\b` became a backspace. Worse, the unescaped path key lived in the same stack buffer that is then used to unescape each escaped key in the document, so the first escaped key of the same length overwrote it and compared equal to itself. Delete then cut from that earlier key to the end of the real value: Delete(`{"\"q":1,"keep":0,"a\\b":2}`, `a\b`) returned {}. Compare the path key as is, like searchKeys does. --- parser.go | 7 +++---- parser_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/parser.go b/parser.go index cdbe09e..9d66c80 100644 --- a/parser.go +++ b/parser.go @@ -109,12 +109,11 @@ func findKeyStartConfig(config Config, data []byte, key string) (int, error) { if data[i] == '{' || data[i] == '[' { i += 1 } + // key is compared as is against the unescaped JSON keys, the same way + // searchKeys matches the path for Get, Set and EachKey. It must not be + // unescaped itself, and must not share stackbuf with the JSON keys. var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings - if ku, err := unescapeConfig(config, StringToBytes(key), stackbuf[:]); err == nil { - key = bytesToString(&ku) - } - for i < ln { switch data[i] { case '"', '\'': diff --git a/parser_test.go b/parser_test.go index 7ba4456..d2657c6 100644 --- a/parser_test.go +++ b/parser_test.go @@ -264,6 +264,40 @@ var deleteTests = []DeleteTest{ path: []string{"test"}, data: `}`, }, + { + // The path is matched against the unescaped JSON key, as in Get. + desc: "Delete key with simple escape", + json: `{"a\\b":1,"c":2}`, + path: []string{"a\\b"}, + data: `{"c":2}`, + }, + { + desc: "Delete key with Unicode escape", + json: `{"c":2,"a\u00B0b":1}`, + path: []string{"a\u00B0b"}, + data: `{"c":2}`, + }, + { + // The backslash in the path must not make an earlier escaped key + // of the same length match: this used to delete everything from + // "\"q" to the end, "keep" included. + desc: "Delete escaped key must not match an earlier escaped key", + json: `{"\"q":1,"keep":0,"a\\b":2}`, + path: []string{"a\\b"}, + data: `{"\"q":1,"keep":0}`, + }, + { + desc: "Delete nested escaped key must not match an earlier escaped key", + json: `{"x":{"D:\\xyzw":1,"C:\\temp":2}}`, + path: []string{"x", "C:\\temp"}, + data: `{"x":{"D:\\xyzw":1}}`, + }, + { + desc: "Delete with a path that is only equal to the escaped form of a key", + json: `{"a\u00B0b":1}`, + path: []string{"a\\u00B0b"}, + data: `{"a\u00B0b":1}`, + }, } var setTests = []SetTest{