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.7.0
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.8.0
Choose a head ref

Commits on Dec 11, 2023

  1. Bump actions/setup-go from 2 to 5

    Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2 to 5.
    - [Release notes](https://github.com/actions/setup-go/releases)
    - [Commits](actions/setup-go@v2...v5)
    
    ---
    updated-dependencies:
    - dependency-name: actions/setup-go
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Dec 11, 2023
    Copy the full SHA
    e1550af View commit details

Commits on Dec 18, 2023

  1. Bump actions/upload-artifact from 3 to 4

    Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
    - [Release notes](https://github.com/actions/upload-artifact/releases)
    - [Commits](actions/upload-artifact@v3...v4)
    
    ---
    updated-dependencies:
    - dependency-name: actions/upload-artifact
      dependency-type: direct:production
      update-type: version-update:semver-major
    ...
    
    Signed-off-by: dependabot[bot] <support@github.com>
    dependabot[bot] authored Dec 18, 2023
    Copy the full SHA
    3ee20a4 View commit details

Commits on Jan 11, 2024

  1. Merge pull request #190 from evanphx/dependabot/github_actions/action…

    …s/setup-go-5
    
    Bump actions/setup-go from 2 to 5
    evanphx authored Jan 11, 2024
    Copy the full SHA
    738124d View commit details
  2. Merge pull request #191 from evanphx/dependabot/github_actions/action…

    …s/upload-artifact-4
    
    Bump actions/upload-artifact from 3 to 4
    evanphx authored Jan 11, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    f72a464 View commit details
  3. Copy the full SHA
    67afbf6 View commit details
  4. Copy the full SHA
    a002ca6 View commit details

Commits on Jan 12, 2024

  1. Copy the full SHA
    4a9dce7 View commit details
  2. Additional null tests

    evanphx committed Jan 12, 2024
    Copy the full SHA
    742691b View commit details
  3. Copy the full SHA
    a82b43d View commit details
  4. Merge pull request #195 from evanphx/b-string-compare-unicode

    Compare strings after decoding them to handle unicode correctly. Fixes #172
    evanphx authored Jan 12, 2024
    Copy the full SHA
    c32ee8f View commit details
  5. Merge pull request #194 from evanphx/b-num-prec

    Always use UseNumber() to avoid float64 lossyness
    evanphx authored Jan 12, 2024
    Copy the full SHA
    850009d View commit details
  6. Merge pull request #196 from evanphx/b-null-mistype-panic

    Handle null correctly when introduced by replace. Fixes #171
    evanphx authored Jan 12, 2024
    Copy the full SHA
    3da7b27 View commit details
  7. Copy the full SHA
    b5e20d4 View commit details
  8. Handle add with path="". Fixes #188

    evanphx committed Jan 12, 2024
    Copy the full SHA
    c645ff4 View commit details
  9. Copy the full SHA
    e8ab61f View commit details
  10. Merge pull request #193 from evanphx/b-empty-from

    Handle from="" more properly. Fixes #192
    evanphx authored Jan 12, 2024
    Copy the full SHA
    a9c085a View commit details
  11. Copy the full SHA
    f18a498 View commit details
  12. Use internal/json

    evanphx committed Jan 12, 2024
    Copy the full SHA
    7b8895c View commit details
  13. Add MergePatch benchmark

    evanphx committed Jan 12, 2024
    Copy the full SHA
    174e1d7 View commit details
  14. More optimizations

    * Avoid checkValid and compact as much as possible
    * Keep decodeState's in a pool
    evanphx committed Jan 12, 2024
    Copy the full SHA
    2a122d1 View commit details
  15. Minor simplifications

    evanphx committed Jan 12, 2024
    Copy the full SHA
    7a438a6 View commit details
  16. Merge pull request #197 from evanphx/f-perf

    Improve performance
    evanphx authored Jan 12, 2024
    Copy the full SHA
    05c9526 View commit details
2 changes: 1 addition & 1 deletion .github/workflows/cifuzz.yml
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ jobs:
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.18

18 changes: 18 additions & 0 deletions v5/bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package jsonpatch

import "testing"

func BenchmarkMergePatch(b *testing.B) {
original := []byte(`{"name": "John", "age": 24, "height": 3.21}`)
target := []byte(`{"name": "Jane", "age": 24}`)
alternative := []byte(`{"name": "Tina", "age": 28, "height": 3.75}`)

patch, err := CreateMergePatch(original, target)
if err != nil {
panic(err)
}

for n := 0; n < b.N; n++ {
MergePatch(alternative, patch)
}
}
1,385 changes: 1,385 additions & 0 deletions v5/internal/json/decode.go

Large diffs are not rendered by default.

2,574 changes: 2,574 additions & 0 deletions v5/internal/json/decode_test.go

Large diffs are not rendered by default.

1,473 changes: 1,473 additions & 0 deletions v5/internal/json/encode.go

Large diffs are not rendered by default.

1,239 changes: 1,239 additions & 0 deletions v5/internal/json/encode_test.go

Large diffs are not rendered by default.

73 changes: 73 additions & 0 deletions v5/internal/json/example_marshaling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json_test

import (
"encoding/json"
"fmt"
"log"
"strings"
)

type Animal int

const (
Unknown Animal = iota
Gopher
Zebra
)

func (a *Animal) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
switch strings.ToLower(s) {
default:
*a = Unknown
case "gopher":
*a = Gopher
case "zebra":
*a = Zebra
}

return nil
}

func (a Animal) MarshalJSON() ([]byte, error) {
var s string
switch a {
default:
s = "unknown"
case Gopher:
s = "gopher"
case Zebra:
s = "zebra"
}

return json.Marshal(s)
}

func Example_customMarshalJSON() {
blob := `["gopher","armadillo","zebra","unknown","gopher","bee","gopher","zebra"]`
var zoo []Animal
if err := json.Unmarshal([]byte(blob), &zoo); err != nil {
log.Fatal(err)
}

census := make(map[Animal]int)
for _, animal := range zoo {
census[animal] += 1
}

fmt.Printf("Zoo Census:\n* Gophers: %d\n* Zebras: %d\n* Unknown: %d\n",
census[Gopher], census[Zebra], census[Unknown])

// Output:
// Zoo Census:
// * Gophers: 3
// * Zebras: 2
// * Unknown: 3
}
310 changes: 310 additions & 0 deletions v5/internal/json/example_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json_test

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strings"
)

func ExampleMarshal() {
type ColorGroup struct {
ID int
Name string
Colors []string
}
group := ColorGroup{
ID: 1,
Name: "Reds",
Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b, err := json.Marshal(group)
if err != nil {
fmt.Println("error:", err)
}
os.Stdout.Write(b)
// Output:
// {"ID":1,"Name":"Reds","Colors":["Crimson","Red","Ruby","Maroon"]}
}

func ExampleUnmarshal() {
var jsonBlob = []byte(`[
{"Name": "Platypus", "Order": "Monotremata"},
{"Name": "Quoll", "Order": "Dasyuromorphia"}
]`)
type Animal struct {
Name string
Order string
}
var animals []Animal
err := json.Unmarshal(jsonBlob, &animals)
if err != nil {
fmt.Println("error:", err)
}
fmt.Printf("%+v", animals)
// Output:
// [{Name:Platypus Order:Monotremata} {Name:Quoll Order:Dasyuromorphia}]
}

// This example uses a Decoder to decode a stream of distinct JSON values.
func ExampleDecoder() {
const jsonStream = `
{"Name": "Ed", "Text": "Knock knock."}
{"Name": "Sam", "Text": "Who's there?"}
{"Name": "Ed", "Text": "Go fmt."}
{"Name": "Sam", "Text": "Go fmt who?"}
{"Name": "Ed", "Text": "Go fmt yourself!"}
`
type Message struct {
Name, Text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))
for {
var m Message
if err := dec.Decode(&m); err == io.EOF {
break
} else if err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", m.Name, m.Text)
}
// Output:
// Ed: Knock knock.
// Sam: Who's there?
// Ed: Go fmt.
// Sam: Go fmt who?
// Ed: Go fmt yourself!
}

// This example uses a Decoder to decode a stream of distinct JSON values.
func ExampleDecoder_Token() {
const jsonStream = `
{"Message": "Hello", "Array": [1, 2, 3], "Null": null, "Number": 1.234}
`
dec := json.NewDecoder(strings.NewReader(jsonStream))
for {
t, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Printf("%T: %v", t, t)
if dec.More() {
fmt.Printf(" (more)")
}
fmt.Printf("\n")
}
// Output:
// json.Delim: { (more)
// string: Message (more)
// string: Hello (more)
// string: Array (more)
// json.Delim: [ (more)
// float64: 1 (more)
// float64: 2 (more)
// float64: 3
// json.Delim: ] (more)
// string: Null (more)
// <nil>: <nil> (more)
// string: Number (more)
// float64: 1.234
// json.Delim: }
}

// This example uses a Decoder to decode a streaming array of JSON objects.
func ExampleDecoder_Decode_stream() {
const jsonStream = `
[
{"Name": "Ed", "Text": "Knock knock."},
{"Name": "Sam", "Text": "Who's there?"},
{"Name": "Ed", "Text": "Go fmt."},
{"Name": "Sam", "Text": "Go fmt who?"},
{"Name": "Ed", "Text": "Go fmt yourself!"}
]
`
type Message struct {
Name, Text string
}
dec := json.NewDecoder(strings.NewReader(jsonStream))

// read open bracket
t, err := dec.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%T: %v\n", t, t)

// while the array contains values
for dec.More() {
var m Message
// decode an array value (Message)
err := dec.Decode(&m)
if err != nil {
log.Fatal(err)
}

fmt.Printf("%v: %v\n", m.Name, m.Text)
}

// read closing bracket
t, err = dec.Token()
if err != nil {
log.Fatal(err)
}
fmt.Printf("%T: %v\n", t, t)

// Output:
// json.Delim: [
// Ed: Knock knock.
// Sam: Who's there?
// Ed: Go fmt.
// Sam: Go fmt who?
// Ed: Go fmt yourself!
// json.Delim: ]
}

// This example uses RawMessage to delay parsing part of a JSON message.
func ExampleRawMessage_unmarshal() {
type Color struct {
Space string
Point json.RawMessage // delay parsing until we know the color space
}
type RGB struct {
R uint8
G uint8
B uint8
}
type YCbCr struct {
Y uint8
Cb int8
Cr int8
}

var j = []byte(`[
{"Space": "YCbCr", "Point": {"Y": 255, "Cb": 0, "Cr": -10}},
{"Space": "RGB", "Point": {"R": 98, "G": 218, "B": 255}}
]`)
var colors []Color
err := json.Unmarshal(j, &colors)
if err != nil {
log.Fatalln("error:", err)
}

for _, c := range colors {
var dst any
switch c.Space {
case "RGB":
dst = new(RGB)
case "YCbCr":
dst = new(YCbCr)
}
err := json.Unmarshal(c.Point, dst)
if err != nil {
log.Fatalln("error:", err)
}
fmt.Println(c.Space, dst)
}
// Output:
// YCbCr &{255 0 -10}
// RGB &{98 218 255}
}

// This example uses RawMessage to use a precomputed JSON during marshal.
func ExampleRawMessage_marshal() {
h := json.RawMessage(`{"precomputed": true}`)

c := struct {
Header *json.RawMessage `json:"header"`
Body string `json:"body"`
}{Header: &h, Body: "Hello Gophers!"}

b, err := json.MarshalIndent(&c, "", "\t")
if err != nil {
fmt.Println("error:", err)
}
os.Stdout.Write(b)

// Output:
// {
// "header": {
// "precomputed": true
// },
// "body": "Hello Gophers!"
// }
}

func ExampleIndent() {
type Road struct {
Name string
Number int
}
roads := []Road{
{"Diamond Fork", 29},
{"Sheep Creek", 51},
}

b, err := json.Marshal(roads)
if err != nil {
log.Fatal(err)
}

var out bytes.Buffer
json.Indent(&out, b, "=", "\t")
out.WriteTo(os.Stdout)
// Output:
// [
// = {
// = "Name": "Diamond Fork",
// = "Number": 29
// = },
// = {
// = "Name": "Sheep Creek",
// = "Number": 51
// = }
// =]
}

func ExampleMarshalIndent() {
data := map[string]int{
"a": 1,
"b": 2,
}

b, err := json.MarshalIndent(data, "<prefix>", "<indent>")
if err != nil {
log.Fatal(err)
}

fmt.Println(string(b))
// Output:
// {
// <prefix><indent>"a": 1,
// <prefix><indent>"b": 2
// <prefix>}
}

func ExampleValid() {
goodJSON := `{"example": 1}`
badJSON := `{"example":2:]}}`

fmt.Println(json.Valid([]byte(goodJSON)), json.Valid([]byte(badJSON)))
// Output:
// true false
}

func ExampleHTMLEscape() {
var out bytes.Buffer
json.HTMLEscape(&out, []byte(`{"Name":"<b>HTML content</b>"}`))
out.WriteTo(os.Stdout)
// Output:
//{"Name":"\u003cb\u003eHTML content\u003c/b\u003e"}
}
67 changes: 67 additions & 0 deletions v5/internal/json/example_text_marshaling_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json_test

import (
"encoding/json"
"fmt"
"log"
"strings"
)

type Size int

const (
Unrecognized Size = iota
Small
Large
)

func (s *Size) UnmarshalText(text []byte) error {
switch strings.ToLower(string(text)) {
default:
*s = Unrecognized
case "small":
*s = Small
case "large":
*s = Large
}
return nil
}

func (s Size) MarshalText() ([]byte, error) {
var name string
switch s {
default:
name = "unrecognized"
case Small:
name = "small"
case Large:
name = "large"
}
return []byte(name), nil
}

func Example_textMarshalJSON() {
blob := `["small","regular","large","unrecognized","small","normal","small","large"]`
var inventory []Size
if err := json.Unmarshal([]byte(blob), &inventory); err != nil {
log.Fatal(err)
}

counts := make(map[Size]int)
for _, size := range inventory {
counts[size] += 1
}

fmt.Printf("Inventory Counts:\n* Small: %d\n* Large: %d\n* Unrecognized: %d\n",
counts[Small], counts[Large], counts[Unrecognized])

// Output:
// Inventory Counts:
// * Small: 3
// * Large: 2
// * Unrecognized: 3
}
141 changes: 141 additions & 0 deletions v5/internal/json/fold.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"bytes"
"unicode/utf8"
)

const (
caseMask = ^byte(0x20) // Mask to ignore case in ASCII.
kelvin = '\u212a'
smallLongEss = '\u017f'
)

// foldFunc returns one of four different case folding equivalence
// functions, from most general (and slow) to fastest:
//
// 1) bytes.EqualFold, if the key s contains any non-ASCII UTF-8
// 2) equalFoldRight, if s contains special folding ASCII ('k', 'K', 's', 'S')
// 3) asciiEqualFold, no special, but includes non-letters (including _)
// 4) simpleLetterEqualFold, no specials, no non-letters.
//
// The letters S and K are special because they map to 3 runes, not just 2:
// - S maps to s and to U+017F 'ſ' Latin small letter long s
// - k maps to K and to U+212A 'K' Kelvin sign
//
// See https://play.golang.org/p/tTxjOc0OGo
//
// The returned function is specialized for matching against s and
// should only be given s. It's not curried for performance reasons.
func foldFunc(s []byte) func(s, t []byte) bool {
nonLetter := false
special := false // special letter
for _, b := range s {
if b >= utf8.RuneSelf {
return bytes.EqualFold
}
upper := b & caseMask
if upper < 'A' || upper > 'Z' {
nonLetter = true
} else if upper == 'K' || upper == 'S' {
// See above for why these letters are special.
special = true
}
}
if special {
return equalFoldRight
}
if nonLetter {
return asciiEqualFold
}
return simpleLetterEqualFold
}

// equalFoldRight is a specialization of bytes.EqualFold when s is
// known to be all ASCII (including punctuation), but contains an 's',
// 'S', 'k', or 'K', requiring a Unicode fold on the bytes in t.
// See comments on foldFunc.
func equalFoldRight(s, t []byte) bool {
for _, sb := range s {
if len(t) == 0 {
return false
}
tb := t[0]
if tb < utf8.RuneSelf {
if sb != tb {
sbUpper := sb & caseMask
if 'A' <= sbUpper && sbUpper <= 'Z' {
if sbUpper != tb&caseMask {
return false
}
} else {
return false
}
}
t = t[1:]
continue
}
// sb is ASCII and t is not. t must be either kelvin
// sign or long s; sb must be s, S, k, or K.
tr, size := utf8.DecodeRune(t)
switch sb {
case 's', 'S':
if tr != smallLongEss {
return false
}
case 'k', 'K':
if tr != kelvin {
return false
}
default:
return false
}
t = t[size:]

}
return len(t) == 0
}

// asciiEqualFold is a specialization of bytes.EqualFold for use when
// s is all ASCII (but may contain non-letters) and contains no
// special-folding letters.
// See comments on foldFunc.
func asciiEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, sb := range s {
tb := t[i]
if sb == tb {
continue
}
if ('a' <= sb && sb <= 'z') || ('A' <= sb && sb <= 'Z') {
if sb&caseMask != tb&caseMask {
return false
}
} else {
return false
}
}
return true
}

// simpleLetterEqualFold is a specialization of bytes.EqualFold for
// use when s is all ASCII letters (no underscores, etc) and also
// doesn't contain 'k', 'K', 's', or 'S'.
// See comments on foldFunc.
func simpleLetterEqualFold(s, t []byte) bool {
if len(s) != len(t) {
return false
}
for i, b := range s {
if b&caseMask != t[i]&caseMask {
return false
}
}
return true
}
110 changes: 110 additions & 0 deletions v5/internal/json/fold_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"bytes"
"strings"
"testing"
"unicode/utf8"
)

var foldTests = []struct {
fn func(s, t []byte) bool
s, t string
want bool
}{
{equalFoldRight, "", "", true},
{equalFoldRight, "a", "a", true},
{equalFoldRight, "", "a", false},
{equalFoldRight, "a", "", false},
{equalFoldRight, "a", "A", true},
{equalFoldRight, "AB", "ab", true},
{equalFoldRight, "AB", "ac", false},
{equalFoldRight, "sbkKc", "ſbKKc", true},
{equalFoldRight, "SbKkc", "ſbKKc", true},
{equalFoldRight, "SbKkc", "ſbKK", false},
{equalFoldRight, "e", "é", false},
{equalFoldRight, "s", "S", true},

{simpleLetterEqualFold, "", "", true},
{simpleLetterEqualFold, "abc", "abc", true},
{simpleLetterEqualFold, "abc", "ABC", true},
{simpleLetterEqualFold, "abc", "ABCD", false},
{simpleLetterEqualFold, "abc", "xxx", false},

{asciiEqualFold, "a_B", "A_b", true},
{asciiEqualFold, "aa@", "aa`", false}, // verify 0x40 and 0x60 aren't case-equivalent
}

func TestFold(t *testing.T) {
for i, tt := range foldTests {
if got := tt.fn([]byte(tt.s), []byte(tt.t)); got != tt.want {
t.Errorf("%d. %q, %q = %v; want %v", i, tt.s, tt.t, got, tt.want)
}
truth := strings.EqualFold(tt.s, tt.t)
if truth != tt.want {
t.Errorf("strings.EqualFold doesn't agree with case %d", i)
}
}
}

func TestFoldAgainstUnicode(t *testing.T) {
var buf1, buf2 []byte
var runes []rune
for i := 0x20; i <= 0x7f; i++ {
runes = append(runes, rune(i))
}
runes = append(runes, kelvin, smallLongEss)

funcs := []struct {
name string
fold func(s, t []byte) bool
letter bool // must be ASCII letter
simple bool // must be simple ASCII letter (not 'S' or 'K')
}{
{
name: "equalFoldRight",
fold: equalFoldRight,
},
{
name: "asciiEqualFold",
fold: asciiEqualFold,
simple: true,
},
{
name: "simpleLetterEqualFold",
fold: simpleLetterEqualFold,
simple: true,
letter: true,
},
}

for _, ff := range funcs {
for _, r := range runes {
if r >= utf8.RuneSelf {
continue
}
if ff.letter && !isASCIILetter(byte(r)) {
continue
}
if ff.simple && (r == 's' || r == 'S' || r == 'k' || r == 'K') {
continue
}
for _, r2 := range runes {
buf1 = append(utf8.AppendRune(append(buf1[:0], 'x'), r), 'x')
buf2 = append(utf8.AppendRune(append(buf2[:0], 'x'), r2), 'x')
want := bytes.EqualFold(buf1, buf2)
if got := ff.fold(buf1, buf2); got != want {
t.Errorf("%s(%q, %q) = %v; want %v", ff.name, buf1, buf2, got, want)
}
}
}
}
}

func isASCIILetter(b byte) bool {
return ('A' <= b && b <= 'Z') || ('a' <= b && b <= 'z')
}
42 changes: 42 additions & 0 deletions v5/internal/json/fuzz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build gofuzz

package json

import (
"fmt"
)

func Fuzz(data []byte) (score int) {
for _, ctor := range []func() any{
func() any { return new(any) },
func() any { return new(map[string]any) },
func() any { return new([]any) },
} {
v := ctor()
err := Unmarshal(data, v)
if err != nil {
continue
}
score = 1

m, err := Marshal(v)
if err != nil {
fmt.Printf("v=%#v\n", v)
panic(err)
}

u := ctor()
err = Unmarshal(m, u)
if err != nil {
fmt.Printf("v=%#v\n", v)
fmt.Printf("m=%s\n", m)
panic(err)
}
}

return
}
83 changes: 83 additions & 0 deletions v5/internal/json/fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"bytes"
"io"
"testing"
)

func FuzzUnmarshalJSON(f *testing.F) {
f.Add([]byte(`{
"object": {
"slice": [
1,
2.0,
"3",
[4],
{5: {}}
]
},
"slice": [[]],
"string": ":)",
"int": 1e5,
"float": 3e-9"
}`))

f.Fuzz(func(t *testing.T, b []byte) {
for _, typ := range []func() interface{}{
func() interface{} { return new(interface{}) },
func() interface{} { return new(map[string]interface{}) },
func() interface{} { return new([]interface{}) },
} {
i := typ()
if err := Unmarshal(b, i); err != nil {
return
}

encoded, err := Marshal(i)
if err != nil {
t.Fatalf("failed to marshal: %s", err)
}

if err := Unmarshal(encoded, i); err != nil {
t.Fatalf("failed to roundtrip: %s", err)
}
}
})
}

func FuzzDecoderToken(f *testing.F) {
f.Add([]byte(`{
"object": {
"slice": [
1,
2.0,
"3",
[4],
{5: {}}
]
},
"slice": [[]],
"string": ":)",
"int": 1e5,
"float": 3e-9"
}`))

f.Fuzz(func(t *testing.T, b []byte) {
r := bytes.NewReader(b)
d := NewDecoder(r)
for {
_, err := d.Token()
if err != nil {
if err == io.EOF {
break
}
return
}
}
})
}
143 changes: 143 additions & 0 deletions v5/internal/json/indent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"bytes"
)

// Compact appends to dst the JSON-encoded src with
// insignificant space characters elided.
func Compact(dst *bytes.Buffer, src []byte) error {
return compact(dst, src, false)
}

func compact(dst *bytes.Buffer, src []byte, escape bool) error {
origLen := dst.Len()
scan := newScanner()
defer freeScanner(scan)
start := 0
for i, c := range src {
if escape && (c == '<' || c == '>' || c == '&') {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`\u00`)
dst.WriteByte(hex[c>>4])
dst.WriteByte(hex[c&0xF])
start = i + 1
}
// Convert U+2028 and U+2029 (E2 80 A8 and E2 80 A9).
if escape && c == 0xE2 && i+2 < len(src) && src[i+1] == 0x80 && src[i+2]&^1 == 0xA8 {
if start < i {
dst.Write(src[start:i])
}
dst.WriteString(`\u202`)
dst.WriteByte(hex[src[i+2]&0xF])
start = i + 3
}
v := scan.step(scan, c)
if v >= scanSkipSpace {
if v == scanError {
break
}
if start < i {
dst.Write(src[start:i])
}
start = i + 1
}
}
if scan.eof() == scanError {
dst.Truncate(origLen)
return scan.err
}
if start < len(src) {
dst.Write(src[start:])
}
return nil
}

func newline(dst *bytes.Buffer, prefix, indent string, depth int) {
dst.WriteByte('\n')
dst.WriteString(prefix)
for i := 0; i < depth; i++ {
dst.WriteString(indent)
}
}

// Indent appends to dst an indented form of the JSON-encoded src.
// Each element in a JSON object or array begins on a new,
// indented line beginning with prefix followed by one or more
// copies of indent according to the indentation nesting.
// The data appended to dst does not begin with the prefix nor
// any indentation, to make it easier to embed inside other formatted JSON data.
// Although leading space characters (space, tab, carriage return, newline)
// at the beginning of src are dropped, trailing space characters
// at the end of src are preserved and copied to dst.
// For example, if src has no trailing spaces, neither will dst;
// if src ends in a trailing newline, so will dst.
func Indent(dst *bytes.Buffer, src []byte, prefix, indent string) error {
origLen := dst.Len()
scan := newScanner()
defer freeScanner(scan)
needIndent := false
depth := 0
for _, c := range src {
scan.bytes++
v := scan.step(scan, c)
if v == scanSkipSpace {
continue
}
if v == scanError {
break
}
if needIndent && v != scanEndObject && v != scanEndArray {
needIndent = false
depth++
newline(dst, prefix, indent, depth)
}

// Emit semantically uninteresting bytes
// (in particular, punctuation in strings) unmodified.
if v == scanContinue {
dst.WriteByte(c)
continue
}

// Add spacing around real punctuation.
switch c {
case '{', '[':
// delay indent so that empty object and array are formatted as {} and [].
needIndent = true
dst.WriteByte(c)

case ',':
dst.WriteByte(c)
newline(dst, prefix, indent, depth)

case ':':
dst.WriteByte(c)
dst.WriteByte(' ')

case '}', ']':
if needIndent {
// suppress indent in empty object/array
needIndent = false
} else {
depth--
newline(dst, prefix, indent, depth)
}
dst.WriteByte(c)

default:
dst.WriteByte(c)
}
}
if scan.eof() == scanError {
dst.Truncate(origLen)
return scan.err
}
return nil
}
118 changes: 118 additions & 0 deletions v5/internal/json/number_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"regexp"
"testing"
)

func TestNumberIsValid(t *testing.T) {
// From: https://stackoverflow.com/a/13340826
var jsonNumberRegexp = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$`)

validTests := []string{
"0",
"-0",
"1",
"-1",
"0.1",
"-0.1",
"1234",
"-1234",
"12.34",
"-12.34",
"12E0",
"12E1",
"12e34",
"12E-0",
"12e+1",
"12e-34",
"-12E0",
"-12E1",
"-12e34",
"-12E-0",
"-12e+1",
"-12e-34",
"1.2E0",
"1.2E1",
"1.2e34",
"1.2E-0",
"1.2e+1",
"1.2e-34",
"-1.2E0",
"-1.2E1",
"-1.2e34",
"-1.2E-0",
"-1.2e+1",
"-1.2e-34",
"0E0",
"0E1",
"0e34",
"0E-0",
"0e+1",
"0e-34",
"-0E0",
"-0E1",
"-0e34",
"-0E-0",
"-0e+1",
"-0e-34",
}

for _, test := range validTests {
if !isValidNumber(test) {
t.Errorf("%s should be valid", test)
}

var f float64
if err := Unmarshal([]byte(test), &f); err != nil {
t.Errorf("%s should be valid but Unmarshal failed: %v", test, err)
}

if !jsonNumberRegexp.MatchString(test) {
t.Errorf("%s should be valid but regexp does not match", test)
}
}

invalidTests := []string{
"",
"invalid",
"1.0.1",
"1..1",
"-1-2",
"012a42",
"01.2",
"012",
"12E12.12",
"1e2e3",
"1e+-2",
"1e--23",
"1e",
"e1",
"1e+",
"1ea",
"1a",
"1.a",
"1.",
"01",
"1.e1",
}

for _, test := range invalidTests {
if isValidNumber(test) {
t.Errorf("%s should be invalid", test)
}

var f float64
if err := Unmarshal([]byte(test), &f); err == nil {
t.Errorf("%s should be invalid but unmarshal wrote %v", test, f)
}

if jsonNumberRegexp.MatchString(test) {
t.Errorf("%s should be invalid but matches regexp", test)
}
}
}
610 changes: 610 additions & 0 deletions v5/internal/json/scanner.go

Large diffs are not rendered by default.

301 changes: 301 additions & 0 deletions v5/internal/json/scanner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,301 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"bytes"
"math"
"math/rand"
"reflect"
"testing"
)

var validTests = []struct {
data string
ok bool
}{
{`foo`, false},
{`}{`, false},
{`{]`, false},
{`{}`, true},
{`{"foo":"bar"}`, true},
{`{"foo":"bar","bar":{"baz":["qux"]}}`, true},
}

func TestValid(t *testing.T) {
for _, tt := range validTests {
if ok := Valid([]byte(tt.data)); ok != tt.ok {
t.Errorf("Valid(%#q) = %v, want %v", tt.data, ok, tt.ok)
}
}
}

// Tests of simple examples.

type example struct {
compact string
indent string
}

var examples = []example{
{`1`, `1`},
{`{}`, `{}`},
{`[]`, `[]`},
{`{"":2}`, "{\n\t\"\": 2\n}"},
{`[3]`, "[\n\t3\n]"},
{`[1,2,3]`, "[\n\t1,\n\t2,\n\t3\n]"},
{`{"x":1}`, "{\n\t\"x\": 1\n}"},
{ex1, ex1i},
{"{\"\":\"<>&\u2028\u2029\"}", "{\n\t\"\": \"<>&\u2028\u2029\"\n}"}, // See golang.org/issue/34070
}

var ex1 = `[true,false,null,"x",1,1.5,0,-5e+2]`

var ex1i = `[
true,
false,
null,
"x",
1,
1.5,
0,
-5e+2
]`

func TestCompact(t *testing.T) {
var buf bytes.Buffer
for _, tt := range examples {
buf.Reset()
if err := Compact(&buf, []byte(tt.compact)); err != nil {
t.Errorf("Compact(%#q): %v", tt.compact, err)
} else if s := buf.String(); s != tt.compact {
t.Errorf("Compact(%#q) = %#q, want original", tt.compact, s)
}

buf.Reset()
if err := Compact(&buf, []byte(tt.indent)); err != nil {
t.Errorf("Compact(%#q): %v", tt.indent, err)
continue
} else if s := buf.String(); s != tt.compact {
t.Errorf("Compact(%#q) = %#q, want %#q", tt.indent, s, tt.compact)
}
}
}

func TestCompactSeparators(t *testing.T) {
// U+2028 and U+2029 should be escaped inside strings.
// They should not appear outside strings.
tests := []struct {
in, compact string
}{
{"{\"\u2028\": 1}", "{\"\u2028\":1}"},
{"{\"\u2029\" :2}", "{\"\u2029\":2}"},
}
for _, tt := range tests {
var buf bytes.Buffer
if err := Compact(&buf, []byte(tt.in)); err != nil {
t.Errorf("Compact(%q): %v", tt.in, err)
} else if s := buf.String(); s != tt.compact {
t.Errorf("Compact(%q) = %q, want %q", tt.in, s, tt.compact)
}
}
}

func TestIndent(t *testing.T) {
var buf bytes.Buffer
for _, tt := range examples {
buf.Reset()
if err := Indent(&buf, []byte(tt.indent), "", "\t"); err != nil {
t.Errorf("Indent(%#q): %v", tt.indent, err)
} else if s := buf.String(); s != tt.indent {
t.Errorf("Indent(%#q) = %#q, want original", tt.indent, s)
}

buf.Reset()
if err := Indent(&buf, []byte(tt.compact), "", "\t"); err != nil {
t.Errorf("Indent(%#q): %v", tt.compact, err)
continue
} else if s := buf.String(); s != tt.indent {
t.Errorf("Indent(%#q) = %#q, want %#q", tt.compact, s, tt.indent)
}
}
}

// Tests of a large random structure.

func TestCompactBig(t *testing.T) {
initBig()
var buf bytes.Buffer
if err := Compact(&buf, jsonBig); err != nil {
t.Fatalf("Compact: %v", err)
}
b := buf.Bytes()
if !bytes.Equal(b, jsonBig) {
t.Error("Compact(jsonBig) != jsonBig")
diff(t, b, jsonBig)
return
}
}

func TestIndentBig(t *testing.T) {
t.Parallel()
initBig()
var buf bytes.Buffer
if err := Indent(&buf, jsonBig, "", "\t"); err != nil {
t.Fatalf("Indent1: %v", err)
}
b := buf.Bytes()
if len(b) == len(jsonBig) {
// jsonBig is compact (no unnecessary spaces);
// indenting should make it bigger
t.Fatalf("Indent(jsonBig) did not get bigger")
}

// should be idempotent
var buf1 bytes.Buffer
if err := Indent(&buf1, b, "", "\t"); err != nil {
t.Fatalf("Indent2: %v", err)
}
b1 := buf1.Bytes()
if !bytes.Equal(b1, b) {
t.Error("Indent(Indent(jsonBig)) != Indent(jsonBig)")
diff(t, b1, b)
return
}

// should get back to original
buf1.Reset()
if err := Compact(&buf1, b); err != nil {
t.Fatalf("Compact: %v", err)
}
b1 = buf1.Bytes()
if !bytes.Equal(b1, jsonBig) {
t.Error("Compact(Indent(jsonBig)) != jsonBig")
diff(t, b1, jsonBig)
return
}
}

type indentErrorTest struct {
in string
err error
}

var indentErrorTests = []indentErrorTest{
{`{"X": "foo", "Y"}`, &SyntaxError{"invalid character '}' after object key", 17}},
{`{"X": "foo" "Y": "bar"}`, &SyntaxError{"invalid character '\"' after object key:value pair", 13}},
}

func TestIndentErrors(t *testing.T) {
for i, tt := range indentErrorTests {
slice := make([]uint8, 0)
buf := bytes.NewBuffer(slice)
if err := Indent(buf, []uint8(tt.in), "", ""); err != nil {
if !reflect.DeepEqual(err, tt.err) {
t.Errorf("#%d: Indent: %#v", i, err)
continue
}
}
}
}

func diff(t *testing.T, a, b []byte) {
for i := 0; ; i++ {
if i >= len(a) || i >= len(b) || a[i] != b[i] {
j := i - 10
if j < 0 {
j = 0
}
t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:]))
return
}
}
}

func trim(b []byte) []byte {
if len(b) > 20 {
return b[0:20]
}
return b
}

// Generate a random JSON object.

var jsonBig []byte

func initBig() {
n := 10000
if testing.Short() {
n = 100
}
b, err := Marshal(genValue(n))
if err != nil {
panic(err)
}
jsonBig = b
}

func genValue(n int) any {
if n > 1 {
switch rand.Intn(2) {
case 0:
return genArray(n)
case 1:
return genMap(n)
}
}
switch rand.Intn(3) {
case 0:
return rand.Intn(2) == 0
case 1:
return rand.NormFloat64()
case 2:
return genString(30)
}
panic("unreachable")
}

func genString(stddev float64) string {
n := int(math.Abs(rand.NormFloat64()*stddev + stddev/2))
c := make([]rune, n)
for i := range c {
f := math.Abs(rand.NormFloat64()*64 + 32)
if f > 0x10ffff {
f = 0x10ffff
}
c[i] = rune(f)
}
return string(c)
}

func genArray(n int) []any {
f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2)))
if f > n {
f = n
}
if f < 1 {
f = 1
}
x := make([]any, f)
for i := range x {
x[i] = genValue(((i+1)*n)/f - (i*n)/f)
}
return x
}

func genMap(n int) map[string]any {
f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2)))
if f > n {
f = n
}
if n > 0 && f == 0 {
f = 1
}
x := make(map[string]any)
for i := 0; i < f; i++ {
x[genString(10)] = genValue(((i+1)*n)/f - (i*n)/f)
}
return x
}
515 changes: 515 additions & 0 deletions v5/internal/json/stream.go

Large diffs are not rendered by default.

497 changes: 497 additions & 0 deletions v5/internal/json/stream_test.go

Large diffs are not rendered by default.

218 changes: 218 additions & 0 deletions v5/internal/json/tables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import "unicode/utf8"

// safeSet holds the value true if the ASCII character with the given array
// position can be represented inside a JSON string without any further
// escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), and the backslash character ("\").
var safeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': true,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': true,
'=': true,
'>': true,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}

// htmlSafeSet holds the value true if the ASCII character with the given
// array position can be safely represented inside a JSON string, embedded
// inside of HTML <script> tags, without any additional escaping.
//
// All values are true except for the ASCII control characters (0-31), the
// double quote ("), the backslash character ("\"), HTML opening and closing
// tags ("<" and ">"), and the ampersand ("&").
var htmlSafeSet = [utf8.RuneSelf]bool{
' ': true,
'!': true,
'"': false,
'#': true,
'$': true,
'%': true,
'&': false,
'\'': true,
'(': true,
')': true,
'*': true,
'+': true,
',': true,
'-': true,
'.': true,
'/': true,
'0': true,
'1': true,
'2': true,
'3': true,
'4': true,
'5': true,
'6': true,
'7': true,
'8': true,
'9': true,
':': true,
';': true,
'<': false,
'=': true,
'>': false,
'?': true,
'@': true,
'A': true,
'B': true,
'C': true,
'D': true,
'E': true,
'F': true,
'G': true,
'H': true,
'I': true,
'J': true,
'K': true,
'L': true,
'M': true,
'N': true,
'O': true,
'P': true,
'Q': true,
'R': true,
'S': true,
'T': true,
'U': true,
'V': true,
'W': true,
'X': true,
'Y': true,
'Z': true,
'[': true,
'\\': false,
']': true,
'^': true,
'_': true,
'`': true,
'a': true,
'b': true,
'c': true,
'd': true,
'e': true,
'f': true,
'g': true,
'h': true,
'i': true,
'j': true,
'k': true,
'l': true,
'm': true,
'n': true,
'o': true,
'p': true,
'q': true,
'r': true,
's': true,
't': true,
'u': true,
'v': true,
'w': true,
'x': true,
'y': true,
'z': true,
'{': true,
'|': true,
'}': true,
'~': true,
'\u007f': true,
}
120 changes: 120 additions & 0 deletions v5/internal/json/tagkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"testing"
)

type basicLatin2xTag struct {
V string `json:"$%-/"`
}

type basicLatin3xTag struct {
V string `json:"0123456789"`
}

type basicLatin4xTag struct {
V string `json:"ABCDEFGHIJKLMO"`
}

type basicLatin5xTag struct {
V string `json:"PQRSTUVWXYZ_"`
}

type basicLatin6xTag struct {
V string `json:"abcdefghijklmno"`
}

type basicLatin7xTag struct {
V string `json:"pqrstuvwxyz"`
}

type miscPlaneTag struct {
V string `json:"色は匂へど"`
}

type percentSlashTag struct {
V string `json:"text/html%"` // https://golang.org/issue/2718
}

type punctuationTag struct {
V string `json:"!#$%&()*+-./:;<=>?@[]^_{|}~ "` // https://golang.org/issue/3546
}

type dashTag struct {
V string `json:"-,"`
}

type emptyTag struct {
W string
}

type misnamedTag struct {
X string `jsom:"Misnamed"`
}

type badFormatTag struct {
Y string `:"BadFormat"`
}

type badCodeTag struct {
Z string `json:" !\"#&'()*+,."`
}

type spaceTag struct {
Q string `json:"With space"`
}

type unicodeTag struct {
W string `json:"Ελλάδα"`
}

var structTagObjectKeyTests = []struct {
raw any
value string
key string
}{
{basicLatin2xTag{"2x"}, "2x", "$%-/"},
{basicLatin3xTag{"3x"}, "3x", "0123456789"},
{basicLatin4xTag{"4x"}, "4x", "ABCDEFGHIJKLMO"},
{basicLatin5xTag{"5x"}, "5x", "PQRSTUVWXYZ_"},
{basicLatin6xTag{"6x"}, "6x", "abcdefghijklmno"},
{basicLatin7xTag{"7x"}, "7x", "pqrstuvwxyz"},
{miscPlaneTag{"いろはにほへと"}, "いろはにほへと", "色は匂へど"},
{dashTag{"foo"}, "foo", "-"},
{emptyTag{"Pour Moi"}, "Pour Moi", "W"},
{misnamedTag{"Animal Kingdom"}, "Animal Kingdom", "X"},
{badFormatTag{"Orfevre"}, "Orfevre", "Y"},
{badCodeTag{"Reliable Man"}, "Reliable Man", "Z"},
{percentSlashTag{"brut"}, "brut", "text/html%"},
{punctuationTag{"Union Rags"}, "Union Rags", "!#$%&()*+-./:;<=>?@[]^_{|}~ "},
{spaceTag{"Perreddu"}, "Perreddu", "With space"},
{unicodeTag{"Loukanikos"}, "Loukanikos", "Ελλάδα"},
}

func TestStructTagObjectKey(t *testing.T) {
for _, tt := range structTagObjectKeyTests {
b, err := Marshal(tt.raw)
if err != nil {
t.Fatalf("Marshal(%#q) failed: %v", tt.raw, err)
}
var f any
err = Unmarshal(b, &f)
if err != nil {
t.Fatalf("Unmarshal(%#q) failed: %v", b, err)
}
for i, v := range f.(map[string]any) {
switch i {
case tt.key:
if s, ok := v.(string); !ok || s != tt.value {
t.Fatalf("Unexpected value: %#q, want %v", s, tt.value)
}
default:
t.Fatalf("Unexpected key: %#q, from %#q", i, b)
}
}
}
}
38 changes: 38 additions & 0 deletions v5/internal/json/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"strings"
)

// tagOptions is the string following a comma in a struct field's "json"
// tag, or the empty string. It does not include the leading comma.
type tagOptions string

// parseTag splits a struct field's json tag into its name and
// comma-separated options.
func parseTag(tag string) (string, tagOptions) {
tag, opt, _ := strings.Cut(tag, ",")
return tag, tagOptions(opt)
}

// Contains reports whether a comma-separated list of options
// contains a particular substr flag. substr must be surrounded by a
// string boundary or commas.
func (o tagOptions) Contains(optionName string) bool {
if len(o) == 0 {
return false
}
s := string(o)
for s != "" {
var name string
name, s, _ = strings.Cut(s, ",")
if name == optionName {
return true
}
}
return false
}
28 changes: 28 additions & 0 deletions v5/internal/json/tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2011 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package json

import (
"testing"
)

func TestTagParsing(t *testing.T) {
name, opts := parseTag("field,foobar,foo")
if name != "field" {
t.Fatalf("name = %q, want field", name)
}
for _, tt := range []struct {
opt string
want bool
}{
{"foobar", true},
{"foo", true},
{"bar", false},
} {
if opts.Contains(tt.opt) != tt.want {
t.Errorf("Contains(%q) = %v", tt.opt, !tt.want)
}
}
}
Binary file added v5/internal/json/testdata/code.json.gz
Binary file not shown.
58 changes: 44 additions & 14 deletions v5/merge.go
Original file line number Diff line number Diff line change
@@ -2,9 +2,12 @@ package jsonpatch

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"reflect"

"github.com/evanphx/json-patch/v5/internal/json"
)

func merge(cur, patch *lazyNode, mergeMerge bool) *lazyNode {
@@ -88,14 +91,14 @@ func pruneDocNulls(doc *partialDoc) *partialDoc {
func pruneAryNulls(ary *partialArray) *partialArray {
newAry := []*lazyNode{}

for _, v := range *ary {
for _, v := range ary.nodes {
if v != nil {
pruneNulls(v)
}
newAry = append(newAry, v)
}

*ary = newAry
ary.nodes = newAry

return ary
}
@@ -117,28 +120,36 @@ func MergePatch(docData, patchData []byte) ([]byte, error) {
}

func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
if !json.Valid(docData) {
return nil, errBadJSONDoc
}

if !json.Valid(patchData) {
return nil, errBadJSONPatch
}

doc := &partialDoc{}

docErr := json.Unmarshal(docData, doc)
docErr := doc.UnmarshalJSON(docData)

patch := &partialDoc{}

patchErr := json.Unmarshal(patchData, patch)
patchErr := patch.UnmarshalJSON(patchData)

if isSyntaxError(docErr) {
return nil, errBadJSONDoc
}

if isSyntaxError(patchErr) {
return nil, errBadJSONPatch
return patchData, nil
}

if docErr == nil && doc.obj == nil {
return nil, errBadJSONDoc
}

if patchErr == nil && patch.obj == nil {
return nil, errBadJSONPatch
return patchData, nil
}

if docErr != nil || patchErr != nil {
@@ -151,15 +162,19 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
}
} else {
patchAry := &partialArray{}
patchErr = json.Unmarshal(patchData, patchAry)
patchErr = unmarshal(patchData, &patchAry.nodes)

if patchErr != nil {
// Not an array either, a literal is the result directly.
if json.Valid(patchData) {
return patchData, nil
}
return nil, errBadJSONPatch
}

pruneAryNulls(patchAry)

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

if patchErr != nil {
return nil, errBadJSONPatch
@@ -175,6 +190,12 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
}

func isSyntaxError(err error) bool {
if errors.Is(err, io.EOF) {
return true
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
if _, ok := err.(*json.SyntaxError); ok {
return true
}
@@ -227,12 +248,12 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
originalDoc := map[string]interface{}{}
modifiedDoc := map[string]interface{}{}

err := json.Unmarshal(originalJSON, &originalDoc)
err := unmarshal(originalJSON, &originalDoc)
if err != nil {
return nil, errBadJSONDoc
}

err = json.Unmarshal(modifiedJSON, &modifiedDoc)
err = unmarshal(modifiedJSON, &modifiedDoc)
if err != nil {
return nil, errBadJSONDoc
}
@@ -245,6 +266,10 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
return json.Marshal(dest)
}

func unmarshal(data []byte, into interface{}) error {
return json.UnmarshalValid(data, into)
}

// createArrayMergePatch will return an array of merge-patch documents capable
// of converting the original document to the modified document for each
// pair of JSON documents provided in the arrays.
@@ -253,12 +278,12 @@ func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
originalDocs := []json.RawMessage{}
modifiedDocs := []json.RawMessage{}

err := json.Unmarshal(originalJSON, &originalDocs)
err := unmarshal(originalJSON, &originalDocs)
if err != nil {
return nil, errBadJSONDoc
}

err = json.Unmarshal(modifiedJSON, &modifiedDocs)
err = unmarshal(modifiedJSON, &modifiedDocs)
if err != nil {
return nil, errBadJSONDoc
}
@@ -314,6 +339,11 @@ func matchesValue(av, bv interface{}) bool {
if bt == at {
return true
}
case json.Number:
bt := bv.(json.Number)
if bt == at {
return true
}
case float64:
bt := bv.(float64)
if bt == at {
@@ -377,7 +407,7 @@ func getDiff(a, b map[string]interface{}) (map[string]interface{}, error) {
if len(dst) > 0 {
into[key] = dst
}
case string, float64, bool:
case string, float64, bool, json.Number:
if !matchesValue(av, bv) {
into[key] = bv
}
54 changes: 14 additions & 40 deletions v5/merge_test.go
Original file line number Diff line number Diff line change
@@ -2,15 +2,15 @@ package jsonpatch

import (
"fmt"
"strings"
"testing"
)

func mergePatch(doc, patch string) string {
func mergePatch(t *testing.T, doc, patch string) string {
t.Helper()
out, err := MergePatch([]byte(doc), []byte(patch))

if err != nil {
panic(err)
t.Errorf(fmt.Sprintf("%s: %s", err, patch))
}

return string(out)
@@ -20,7 +20,7 @@ func TestMergePatchReplaceKey(t *testing.T) {
doc := `{ "title": "hello" }`
pat := `{ "title": "goodbye" }`

res := mergePatch(doc, pat)
res := mergePatch(t, doc, pat)

if !compareJSON(pat, res) {
t.Fatalf("Key was not replaced")
@@ -31,7 +31,7 @@ func TestMergePatchIgnoresOtherValues(t *testing.T) {
doc := `{ "title": "hello", "age": 18 }`
pat := `{ "title": "goodbye" }`

res := mergePatch(doc, pat)
res := mergePatch(t, doc, pat)

exp := `{ "title": "goodbye", "age": 18 }`

@@ -44,7 +44,7 @@ func TestMergePatchNilDoc(t *testing.T) {
doc := `{ "title": null }`
pat := `{ "title": {"foo": "bar"} }`

res := mergePatch(doc, pat)
res := mergePatch(t, doc, pat)

exp := `{ "title": {"foo": "bar"} }`

@@ -70,7 +70,8 @@ func TestMergePatchNilArray(t *testing.T) {
}

for _, c := range cases {
act := mergePatch(c.original, c.patch)
t.Log(c.original)
act := mergePatch(t, c.original, c.patch)

if !compareJSON(c.res, act) {
t.Errorf("null values not preserved in array")
@@ -82,7 +83,7 @@ func TestMergePatchRecursesIntoObjects(t *testing.T) {
doc := `{ "person": { "title": "hello", "age": 18 } }`
pat := `{ "person": { "title": "goodbye" } }`

res := mergePatch(doc, pat)
res := mergePatch(t, doc, pat)

exp := `{ "person": { "title": "goodbye", "age": 18 } }`

@@ -111,7 +112,7 @@ func TestMergePatchReplacesNonObjectsWholesale(t *testing.T) {
}

for _, c := range cases {
act := mergePatch(c.doc, c.pat)
act := mergePatch(t, c.doc, c.pat)

if !compareJSON(c.res, act) {
t.Errorf("whole object replacement failed")
@@ -166,50 +167,23 @@ var rfcTests = []struct {
{target: `{"a":[{"b":"c"}]}`, patch: `{"a":[1]}`, expected: `{"a":[1]}`},
{target: `["a","b"]`, patch: `["c","d"]`, expected: `["c","d"]`},
{target: `{"a":"b"}`, patch: `["c"]`, expected: `["c"]`},
// {target: `{"a":"foo"}`, patch: `null`, expected: `null`},
// {target: `{"a":"foo"}`, patch: `"bar"`, expected: `"bar"`},
{target: `{"a":"foo"}`, patch: `null`, expected: `null`},
{target: `{"a":"foo"}`, patch: `"bar"`, expected: `"bar"`},
{target: `{"e":null}`, patch: `{"a":1}`, expected: `{"a":1,"e":null}`},
{target: `[1,2]`, patch: `{"a":"b","c":null}`, expected: `{"a":"b"}`},
{target: `{}`, patch: `{"a":{"bb":{"ccc":null}}}`, expected: `{"a":{"bb":{}}}`},
}

func TestMergePatchRFCCases(t *testing.T) {
for i, c := range rfcTests {
out := mergePatch(c.target, c.patch)
out := mergePatch(t, c.target, c.patch)

if !compareJSON(out, c.expected) {
t.Errorf("case[%d], patch '%s' did not apply properly to '%s'. expected:\n'%s'\ngot:\n'%s'", i, c.patch, c.target, c.expected, out)
}
}
}

var rfcFailTests = `
{"a":"foo"} | null
{"a":"foo"} | "bar"
`

func TestMergePatchFailRFCCases(t *testing.T) {
tests := strings.Split(rfcFailTests, "\n")

for _, c := range tests {
if strings.TrimSpace(c) == "" {
continue
}

parts := strings.SplitN(c, "|", 2)

doc := strings.TrimSpace(parts[0])
pat := strings.TrimSpace(parts[1])

out, err := MergePatch([]byte(doc), []byte(pat))

if err != errBadJSONPatch {
t.Errorf("error not returned properly: %s, %s", err, string(out))
}
}

}

func TestResembleJSONArray(t *testing.T) {
testCases := []struct {
input []byte
@@ -648,7 +622,7 @@ func TestMergePatchReplaceKeyNotEscaping(t *testing.T) {
pat := `{ "obj": { "title/escaped": "goodbye" } }`
exp := `{ "obj": { "title/escaped": "goodbye" } }`

res := mergePatch(doc, pat)
res := mergePatch(t, doc, pat)

if !compareJSON(exp, res) {
t.Fatalf("Key was not replaced")
325 changes: 203 additions & 122 deletions v5/patch.go

Large diffs are not rendered by default.

64 changes: 58 additions & 6 deletions v5/patch_test.go
Original file line number Diff line number Diff line change
@@ -578,6 +578,41 @@ var Cases = []Case{
false,
false,
},
{
`{}`,
`[{"op": "replace", "path": "", "value": null}]`,
`null`,
false,
false,
},
{
`{"foo": 1}`,
`[ { "op": "copy", "from": "", "path": "/bar"}]`,
`{"foo": 1, "bar": {"foo": 1}}`,
false,
false,
},
{
`[{"foo": 1}]`,
`[ { "op": "copy", "from": "", "path": "/1"}]`,
`[{"foo": 1}, [{"foo": 1}]]`,
false,
false,
},
{
`{}`,
`[{"op":"add","path":"","value":{"foo":"bar"}}]`,
`{"foo": "bar"}`,
false,
false,
},
{
`[]`,
`[{"op":"add","path":"","value":{"foo":"bar"}}, {"op": "add", "path": "/qux", "value": 1}]`,
`{"foo": "bar", "qux": 1}`,
false,
false,
},
}

type BadCase struct {
@@ -635,11 +670,6 @@ var BadCases = []BadCase{
`[ { "op": "add", "pathz": "/baz", "value": "qux" } ]`,
true,
},
{
`{ "foo": "bar" }`,
`[ { "op": "add", "path": "", "value": "qux" } ]`,
false,
},
{
`{ "foo": ["bar","baz"]}`,
`[ { "op": "replace", "path": "/foo/2", "value": "bum"}]`,
@@ -727,6 +757,21 @@ var BadCases = []BadCase{
`[{"op": "copy", "path": "/qux", "from": "/baz"}]`,
false,
},
{
`{ "foo": {"bar": []}}`,
`[{"op": "replace", "path": "/foo/bar", "value": null}, {"op": "add", "path": "/foo/bar/0", "value": "blah"}]`,
false,
},
{
`{}`,
`[{"op": "replace", "path": ""}]`,
true,
},
{
`{ "foo": "bar"}`,
`[{"op": "move", "path": "/qux", "from": ""}]`,
false,
},
}

// This is not thread safe, so we cannot run patch tests in parallel.
@@ -802,9 +847,10 @@ func TestAllCases(t *testing.T) {
}

if err == nil && !c.failOnDecode {
_, err = p.Apply([]byte(c.doc))
out, err := p.Apply([]byte(c.doc))

if err == nil {
t.Log(string(out))
t.Errorf("Patch %q should have failed to apply but it did not", c.patch)
}

@@ -1078,6 +1124,12 @@ var EqualityCases = []EqualityCase{
`null`,
false,
},
{
"Unicode",
`{"name": "λJohn"}`,
`{"name": "\u03BBJohn"}`,
true,
},
}

func TestEquality(t *testing.T) {