From a1e068ff4953b35d730b230508a253833390e59f Mon Sep 17 00:00:00 2001 From: Gyanu Mayank Date: Thu, 8 Oct 2026 08:13:52 +0530 Subject: [PATCH] Treat "" as a regular object key in v5 pointers partialDoc.get and partialArray.get returned the whole document for an empty key so that copy with from="" worked. RFC 6901 says "/" points at the member named "", so this made test "/" fail, replace "/" add a key instead of erroring, copy and move from "/" take the whole document, and paths like "//a" resolve against the root. Drop the shortcut and handle from="" in copy directly by building the node from the current container. That also makes copy from="" see earlier operations in the same patch instead of the original input. --- v5/patch.go | 60 +++++++++++++++++++++--------------------------- v5/patch_test.go | 51 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 34 deletions(-) diff --git a/v5/patch.go b/v5/patch.go index 83102e5..5eee455 100644 --- a/v5/patch.go +++ b/v5/patch.go @@ -59,7 +59,6 @@ type Operation map[string]*json.RawMessage type Patch []Operation type partialDoc struct { - self *lazyNode keys []string obj map[string]*lazyNode @@ -67,7 +66,6 @@ type partialDoc struct { } type partialArray struct { - self *lazyNode nodes []*lazyNode } @@ -600,10 +598,6 @@ func (d *partialDoc) add(key string, val *lazyNode, options *ApplyOptions) error } func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) { - if key == "" { - return d.self, nil - } - if d.obj == nil { return nil, ErrExpectedObject } @@ -701,10 +695,6 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err } func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error) { - if key == "" { - return d.self, nil - } - idx, err := strconv.Atoi(key) if err != nil { @@ -777,12 +767,9 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error { var pd container if (*val.raw)[0] == '[' { - pd = &partialArray{ - self: val, - } + pd = &partialArray{} } else { pd = &partialDoc{ - self: val, opts: options, } } @@ -1069,6 +1056,21 @@ func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error { return nil } +func containerNode(doc container) *lazyNode { + var self lazyNode + + switch sv := doc.(type) { + case *partialDoc: + self.doc = sv + self.which = eDoc + case *partialArray: + self.ary = sv + self.which = eAry + } + + return &self +} + func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error { path, err := op.Path() if err != nil { @@ -1076,16 +1078,7 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error { } if path == "" { - var self lazyNode - - switch sv := (*doc).(type) { - case *partialDoc: - self.doc = sv - self.which = eDoc - case *partialArray: - self.ary = sv - self.which = eAry - } + self := containerNode(*doc) if self.equal(op.value()) { return nil @@ -1135,9 +1128,14 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op return fmt.Errorf("copy operation does not apply: doc is missing from path: \"%s\": %w", from, ErrMissing) } - val, err := con.get(key, options) - if err != nil { - return fmt.Errorf("error in copy for from: '%s': %w", from, err) + var val *lazyNode + if from == "" { + val = containerNode(con) + } else { + val, err = con.get(key, options) + if err != nil { + return fmt.Errorf("error in copy for from: '%s': %w", from, err) + } } path, err := op.Path() @@ -1227,17 +1225,11 @@ func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyO return nil, ErrInvalid } - raw := json.RawMessage(doc) - self := newLazyNode(&raw) - var pd container if doc[0] == '[' { - pd = &partialArray{ - self: self, - } + pd = &partialArray{} } else { pd = &partialDoc{ - self: self, opts: options, } } diff --git a/v5/patch_test.go b/v5/patch_test.go index 1f807f5..eb699a7 100644 --- a/v5/patch_test.go +++ b/v5/patch_test.go @@ -613,6 +613,34 @@ var Cases = []Case{ false, false, }, + { + `{"foo": 1}`, + `[{"op": "add", "path": "/qux", "value": 2}, {"op": "copy", "from": "", "path": "/bar"}]`, + `{"foo": 1, "qux": 2, "bar": {"foo": 1, "qux": 2}}`, + false, + false, + }, + { + `{"": 1, "foo": 2}`, + `[{"op": "copy", "from": "/", "path": "/bar"}]`, + `{"": 1, "foo": 2, "bar": 1}`, + false, + false, + }, + { + `{"": 1, "foo": 2}`, + `[{"op": "move", "from": "/", "path": "/bar"}]`, + `{"foo": 2, "bar": 1}`, + false, + false, + }, + { + `{"": {"a": 1}, "a": 2}`, + `[{"op": "replace", "path": "//a", "value": 3}]`, + `{"": {"a": 3}, "a": 2}`, + false, + false, + }, } type BadCase struct { @@ -772,6 +800,11 @@ var BadCases = []BadCase{ `[{"op": "move", "path": "/qux", "from": ""}]`, false, }, + { + `{ "foo": "bar"}`, + `[{"op": "replace", "path": "/", "value": "qux"}]`, + false, + }, } // This is not thread safe, so we cannot run patch tests in parallel. @@ -949,6 +982,24 @@ var TestCases = []TestCase{ true, "/baz", }, + { + `{ "": 1 }`, + `[ { "op": "test", "path": "/", "value": 1 } ]`, + true, + "", + }, + { + `{ "": { "a": 1 }, "a": 2 }`, + `[ { "op": "test", "path": "//a", "value": 1 } ]`, + true, + "", + }, + { + `{ "foo": "bar" }`, + `[ { "op": "test", "path": "/", "value": { "foo": "bar" } } ]`, + false, + "/", + }, } func TestAllTest(t *testing.T) {