Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: evanphx/json-patch
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v5.8.1
Choose a base ref
...
head repository: evanphx/json-patch
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.9.0
Choose a head ref
  • 5 commits
  • 3 files changed
  • 1 contributor

Commits on Jan 27, 2024

  1. Copy the full SHA
    009bc56 View commit details
  2. Copy the full SHA
    9d7ba23 View commit details
  3. Merge pull request #201 from evanphx/b-null

    Validate that the partialDoc is decoded correctly
    evanphx authored Jan 27, 2024
    Copy the full SHA
    1bcbd0f View commit details
  4. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7eef36c View commit details

Commits on Jan 28, 2024

  1. Merge pull request #202 from evanphx/f-html-escape

    Add option to control if the output is HTMLEscaped
    evanphx authored Jan 28, 2024
    Copy the full SHA
    b7a4e4a View commit details
Showing with 98 additions and 35 deletions.
  1. +13 −0 v5/internal/json/encode.go
  2. +29 −23 v5/merge.go
  3. +56 −12 v5/patch.go
13 changes: 13 additions & 0 deletions v5/internal/json/encode.go
Original file line number Diff line number Diff line change
@@ -167,6 +167,19 @@ func Marshal(v any) ([]byte, error) {
return buf, nil
}

func MarshalEscaped(v any, escape bool) ([]byte, error) {
e := newEncodeState()
defer encodeStatePool.Put(e)

err := e.marshal(v, encOpts{escapeHTML: escape})
if err != nil {
return nil, err
}
buf := append([]byte(nil), e.Bytes()...)

return buf, nil
}

// MarshalIndent is like Marshal but applies Indent to format the output.
// Each JSON element in the output will begin on a new line beginning with prefix
// followed by one or more copies of indent according to the indentation nesting.
52 changes: 29 additions & 23 deletions v5/merge.go
Original file line number Diff line number Diff line change
@@ -10,26 +10,26 @@ import (
"github.com/evanphx/json-patch/v5/internal/json"
)

func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode {
curDoc, err := cur.intoDoc()
func merge(cur, patch *lazyNode, mergeMerge bool, options *ApplyOptions) *lazyNode {
curDoc, err := cur.intoDoc(options)

if err != nil {
pruneNulls(patch)
pruneNulls(patch, options)
return patch
}

patchDoc, err := patch.intoDoc()
patchDoc, err := patch.intoDoc(options)

if err != nil {
return patch
}

mergeDocs(curDoc, patchDoc, mergeMerge)
mergeDocs(curDoc, patchDoc, mergeMerge, options)

return cur
}

func mergeDocs(doc, patch *partialDoc, mergeMerge bool) {
func mergeDocs(doc, patch *partialDoc, mergeMerge bool, options *ApplyOptions) {
for k, v := range patch.obj {
if v == nil {
if mergeMerge {
@@ -45,55 +45,55 @@ func mergeDocs(doc, patch *partialDoc, mergeMerge bool) {
}
doc.obj[k] = nil
} else {
_ = doc.remove(k, &ApplyOptions{})
_ = doc.remove(k, options)
}
} else {
cur, ok := doc.obj[k]

if !ok || cur == nil {
if !mergeMerge {
pruneNulls(v)
pruneNulls(v, options)
}
_ = doc.set(k, v, &ApplyOptions{})
_ = doc.set(k, v, options)
} else {
_ = doc.set(k, merge(cur, v, mergeMerge), &ApplyOptions{})
_ = doc.set(k, merge(cur, v, mergeMerge, options), options)
}
}
}
}

func pruneNulls(n *lazyNode) {
sub, err := n.intoDoc()
func pruneNulls(n *lazyNode, options *ApplyOptions) {
sub, err := n.intoDoc(options)

if err == nil {
pruneDocNulls(sub)
pruneDocNulls(sub, options)
} else {
ary, err := n.intoAry()

if err == nil {
pruneAryNulls(ary)
pruneAryNulls(ary, options)
}
}
}

func pruneDocNulls(doc *partialDoc) *partialDoc {
func pruneDocNulls(doc *partialDoc, options *ApplyOptions) *partialDoc {
for k, v := range doc.obj {
if v == nil {
_ = doc.remove(k, &ApplyOptions{})
} else {
pruneNulls(v)
pruneNulls(v, options)
}
}

return doc
}

func pruneAryNulls(ary *partialArray) *partialArray {
func pruneAryNulls(ary *partialArray, options *ApplyOptions) *partialArray {
newAry := []*lazyNode{}

for _, v := range ary.nodes {
if v != nil {
pruneNulls(v)
pruneNulls(v, options)
}
newAry = append(newAry, v)
}
@@ -128,11 +128,17 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
return nil, errBadJSONPatch
}

doc := &partialDoc{}
options := NewApplyOptions()

doc := &partialDoc{
opts: options,
}

docErr := doc.UnmarshalJSON(docData)

patch := &partialDoc{}
patch := &partialDoc{
opts: options,
}

patchErr := patch.UnmarshalJSON(patchData)

@@ -158,7 +164,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
if mergeMerge {
doc = patch
} else {
doc = pruneDocNulls(patch)
doc = pruneDocNulls(patch, options)
}
} else {
patchAry := &partialArray{}
@@ -172,7 +178,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
return nil, errBadJSONPatch
}

pruneAryNulls(patchAry)
pruneAryNulls(patchAry, options)

out, patchErr := json.Marshal(patchAry.nodes)

@@ -183,7 +189,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
return out, nil
}
} else {
mergeDocs(doc, patch, mergeMerge)
mergeDocs(doc, patch, mergeMerge, options)
}

return json.Marshal(doc)
68 changes: 56 additions & 12 deletions v5/patch.go
Original file line number Diff line number Diff line change
@@ -38,6 +38,8 @@ var (
ErrInvalid = errors.New("invalid state detected")
ErrInvalidIndex = errors.New("invalid index referenced")

ErrExpectedObject = errors.New("invalid value, expected object")

rawJSONArray = []byte("[]")
rawJSONObject = []byte("{}")
rawJSONNull = []byte("null")
@@ -60,6 +62,8 @@ type partialDoc struct {
self *lazyNode
keys []string
obj map[string]*lazyNode

opts *ApplyOptions
}

type partialArray struct {
@@ -90,6 +94,8 @@ type ApplyOptions struct {
// EnsurePathExistsOnAdd instructs json-patch to recursively create the missing parts of path on "add" operation.
// Default to false.
EnsurePathExistsOnAdd bool

EscapeHTML bool
}

// NewApplyOptions creates a default set of options for calls to ApplyWithOptions.
@@ -99,6 +105,7 @@ func NewApplyOptions() *ApplyOptions {
AccumulatedCopySizeLimit: AccumulatedCopySizeLimit,
AllowMissingPathOnRemove: false,
EnsurePathExistsOnAdd: false,
EscapeHTML: true,
}
}

@@ -134,16 +141,28 @@ func (n *lazyNode) UnmarshalJSON(data []byte) error {
}

func (n *partialDoc) TrustMarshalJSON(buf *bytes.Buffer) error {
if n.obj == nil {
return ErrExpectedObject
}

if err := buf.WriteByte('{'); err != nil {
return err
}
escaped := true

// n.opts should always be set, but in case we missed a case,
// guard.
if n.opts != nil {
escaped = n.opts.EscapeHTML
}

for i, k := range n.keys {
if i > 0 {
if err := buf.WriteByte(','); err != nil {
return err
}
}
key, err := json.Marshal(k)
key, err := json.MarshalEscaped(k, escaped)
if err != nil {
return err
}
@@ -153,7 +172,7 @@ func (n *partialDoc) TrustMarshalJSON(buf *bytes.Buffer) error {
if err := buf.WriteByte(':'); err != nil {
return err
}
value, err := json.Marshal(n.obj[k])
value, err := json.MarshalEscaped(n.obj[k], escaped)
if err != nil {
return err
}
@@ -194,11 +213,11 @@ func (n *partialArray) RedirectMarshalJSON() (interface{}, error) {
return n.nodes, nil
}

func deepCopy(src *lazyNode) (*lazyNode, int, error) {
func deepCopy(src *lazyNode, options *ApplyOptions) (*lazyNode, int, error) {
if src == nil {
return nil, 0, nil
}
a, err := json.Marshal(src)
a, err := json.MarshalEscaped(src, options.EscapeHTML)
if err != nil {
return nil, 0, err
}
@@ -216,7 +235,7 @@ func (n *lazyNode) nextByte() byte {
return s[0]
}

func (n *lazyNode) intoDoc() (*partialDoc, error) {
func (n *lazyNode) intoDoc(options *ApplyOptions) (*partialDoc, error) {
if n.which == eDoc {
return n.doc, nil
}
@@ -235,6 +254,7 @@ func (n *lazyNode) intoDoc() (*partialDoc, error) {
return nil, ErrInvalid
}

n.doc.opts = options
if err != nil {
return nil, err
}
@@ -545,7 +565,7 @@ func findObject(pd *container, path string, options *ApplyOptions) (container, s
return nil, ""
}
} else {
doc, err = next.intoDoc()
doc, err = next.intoDoc(options)

if err != nil {
return nil, ""
@@ -557,6 +577,10 @@ func findObject(pd *container, path string, options *ApplyOptions) (container, s
}

func (d *partialDoc) set(key string, val *lazyNode, options *ApplyOptions) error {
if d.obj == nil {
return ErrExpectedObject
}

found := false
for _, k := range d.keys {
if k == key {
@@ -579,6 +603,11 @@ func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
if key == "" {
return d.self, nil
}

if d.obj == nil {
return nil, ErrExpectedObject
}

v, ok := d.obj[key]
if !ok {
return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key)
@@ -587,6 +616,10 @@ func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
}

func (d *partialDoc) remove(key string, options *ApplyOptions) error {
if d.obj == nil {
return ErrExpectedObject
}

_, ok := d.obj[key]
if !ok {
if options.AllowMissingPathOnRemove {
@@ -750,6 +783,7 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
} else {
pd = &partialDoc{
self: val,
opts: options,
}
}

@@ -855,7 +889,7 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
newNode := newLazyNode(newRawMessage(rawJSONObject))

doc.add(part, newNode, options)
doc, err = newNode.intoDoc()
doc, err = newNode.intoDoc(options)
if err != nil {
return err
}
@@ -868,7 +902,7 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
return err
}
} else {
doc, err = target.intoDoc()
doc, err = target.intoDoc(options)

if err != nil {
return err
@@ -954,6 +988,8 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
if !val.tryAry() {
return errors.Wrapf(err, "replace operation value must be object or array")
}
} else {
val.doc.opts = options
}
}

@@ -1115,7 +1151,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)
}

valCopy, sz, err := deepCopy(val)
valCopy, sz, err := deepCopy(val, options)
if err != nil {
return errors.Wrapf(err, "error while performing deep copy")
}
@@ -1202,6 +1238,7 @@ func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyO
} else {
pd = &partialDoc{
self: self,
opts: options,
}
}

@@ -1238,11 +1275,18 @@ func (p Patch) ApplyIndentWithOptions(doc []byte, indent string, options *ApplyO
}
}

if indent != "" {
return json.MarshalIndent(pd, "", indent)
data, err := json.MarshalEscaped(pd, options.EscapeHTML)
if err != nil {
return nil, err
}

if indent == "" {
return data, nil
}

return json.Marshal(pd)
var buf bytes.Buffer
json.Indent(&buf, data, "", indent)
return buf.Bytes(), nil
}

// From http://tools.ietf.org/html/rfc6901#section-4 :