An empty string is a valid JSON object key and gjson reads it, but set()
rejected an empty path outright, so such a key could neither be changed
nor removed through sjson.
Nothing below that guard needed a non-empty path. parsePath("") already
returns a well-formed simple pathResult and gjson.Get resolves the empty
key. What did break was atoui: its digit loop never runs for an empty
part, so it fell through and reported the empty key as numeric index 0.
That also affected paths the guard never blocked. A nested empty key was
built as an array, so Set(`{"x":1}`, "a.", 2) produced
{"x":1,"a":[2]}, which gjson cannot read back.
Return false from atoui for an empty part and drop the guard. An empty
key on an array now reports the same error as any other non-numeric key
instead of a special-cased one.
Fixes #77
398 lines
11 KiB
Go
398 lines
11 KiB
Go
package sjson
|
|
|
|
import (
|
|
"encoding/hex"
|
|
"fmt"
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/tidwall/gjson"
|
|
"github.com/tidwall/pretty"
|
|
)
|
|
|
|
const (
|
|
setRaw = 1
|
|
setBool = 2
|
|
setInt = 3
|
|
setFloat = 4
|
|
setString = 5
|
|
setDelete = 6
|
|
)
|
|
|
|
func sortJSON(json string) string {
|
|
opts := pretty.Options{SortKeys: true}
|
|
return string(pretty.Ugly(pretty.PrettyOptions([]byte(json), &opts)))
|
|
}
|
|
|
|
func testRaw(t *testing.T, kind int, expect, json, path string, value interface{}) {
|
|
t.Helper()
|
|
expect = sortJSON(expect)
|
|
var json2 string
|
|
var err error
|
|
switch kind {
|
|
default:
|
|
json2, err = Set(json, path, value)
|
|
case setRaw:
|
|
json2, err = SetRaw(json, path, value.(string))
|
|
case setDelete:
|
|
json2, err = Delete(json, path)
|
|
}
|
|
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
json2 = sortJSON(json2)
|
|
if json2 != expect {
|
|
t.Fatalf("expected '%v', got '%v'", expect, json2)
|
|
}
|
|
var json3 []byte
|
|
switch kind {
|
|
default:
|
|
json3, err = SetBytes([]byte(json), path, value)
|
|
case setRaw:
|
|
json3, err = SetRawBytes([]byte(json), path, []byte(value.(string)))
|
|
case setDelete:
|
|
json3, err = DeleteBytes([]byte(json), path)
|
|
}
|
|
json3 = []byte(sortJSON(string(json3)))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
} else if string(json3) != expect {
|
|
t.Fatalf("expected '%v', got '%v'", expect, string(json3))
|
|
}
|
|
}
|
|
func TestBasic(t *testing.T) {
|
|
testRaw(t, setRaw, `[{"hiw":"planet","hi":"world"}]`, `[{"hi":"world"}]`, "0.hiw", `"planet"`)
|
|
testRaw(t, setRaw, `[true]`, ``, "0", `true`)
|
|
testRaw(t, setRaw, `[null,true]`, ``, "1", `true`)
|
|
testRaw(t, setRaw, `[1,null,true]`, `[1]`, "2", `true`)
|
|
testRaw(t, setRaw, `[1,true,false]`, `[1,null,false]`, "1", `true`)
|
|
testRaw(t, setRaw,
|
|
`[1,{"hello":"when","this":[0,null,2]},false]`,
|
|
`[1,{"hello":"when","this":[0,1,2]},false]`,
|
|
"1.this.1", `null`)
|
|
testRaw(t, setRaw,
|
|
`{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`,
|
|
`{"a":1,"b":{"hello":"when","this":[0,1,2]},"c":false}`,
|
|
"b.this.1", `null`)
|
|
testRaw(t, setRaw,
|
|
`{"a":1,"b":{"hello":"when","this":[0,null,2,null,4]},"c":false}`,
|
|
`{"a":1,"b":{"hello":"when","this":[0,null,2]},"c":false}`,
|
|
"b.this.4", `4`)
|
|
testRaw(t, setRaw,
|
|
`{"b":{"this":[null,null,null,null,4]}}`,
|
|
``,
|
|
"b.this.4", `4`)
|
|
testRaw(t, setRaw,
|
|
`[null,{"this":[null,null,null,null,4]}]`,
|
|
``,
|
|
"1.this.4", `4`)
|
|
testRaw(t, setRaw,
|
|
`{"1":{"this":[null,null,null,null,4]}}`,
|
|
``,
|
|
":1.this.4", `4`)
|
|
testRaw(t, setRaw,
|
|
`{":1":{"this":[null,null,null,null,4]}}`,
|
|
``,
|
|
"\\:1.this.4", `4`)
|
|
testRaw(t, setRaw,
|
|
`{":\\1":{"this":[null,null,null,null,{".HI":4}]}}`,
|
|
``,
|
|
"\\:\\\\1.this.4.\\.HI", `4`)
|
|
testRaw(t, setRaw,
|
|
`{"app.token":"cde"}`,
|
|
`{"app.token":"abc"}`,
|
|
"app\\.token", `"cde"`)
|
|
testRaw(t, setRaw,
|
|
`{"b":{"this":{"😇":""}}}`,
|
|
``,
|
|
"b.this.😇", `""`)
|
|
testRaw(t, setRaw,
|
|
`[ 1,2 ,3]`,
|
|
` [ 1,2 ] `,
|
|
"-1", `3`)
|
|
testRaw(t, setInt, `[1234]`, ``, `0`, int64(1234))
|
|
testRaw(t, setFloat, `[1234.5]`, ``, `0`, float64(1234.5))
|
|
testRaw(t, setString, `["1234.5"]`, ``, `0`, "1234.5")
|
|
testRaw(t, setBool, `[true]`, ``, `0`, true)
|
|
testRaw(t, setBool, `[null]`, ``, `0`, nil)
|
|
testRaw(t, setString, `{"arr":[1]}`, ``, `arr.-1`, 1)
|
|
testRaw(t, setString, `{"a":"\\"}`, ``, `a`, "\\")
|
|
testRaw(t, setString, `{"a":"C:\\Windows\\System32"}`, ``, `a`, `C:\Windows\System32`)
|
|
}
|
|
|
|
func TestDelete(t *testing.T) {
|
|
testRaw(t, setDelete, `[456]`, `[123,456]`, `0`, nil)
|
|
testRaw(t, setDelete, `[123,789]`, `[123,456,789]`, `1`, nil)
|
|
testRaw(t, setDelete, `[123,456]`, `[123,456,789]`, `-1`, nil)
|
|
testRaw(t, setDelete, `{"a":[123,456]}`, `{"a":[123,456,789]}`, `a.-1`, nil)
|
|
testRaw(t, setDelete, `{"and":"another"}`, `{"this":"that","and":"another"}`, `this`, nil)
|
|
testRaw(t, setDelete, `{"this":"that"}`, `{"this":"that","and":"another"}`, `and`, nil)
|
|
testRaw(t, setDelete, `{}`, `{"and":"another"}`, `and`, nil)
|
|
testRaw(t, setDelete, `{"1":"2"}`, `{"1":"2"}`, `3`, nil)
|
|
}
|
|
|
|
// TestRandomData is a fuzzing test that throws random data at SetRaw
|
|
// function looking for panics.
|
|
func TestRandomData(t *testing.T) {
|
|
var lstr string
|
|
defer func() {
|
|
if v := recover(); v != nil {
|
|
println("'" + hex.EncodeToString([]byte(lstr)) + "'")
|
|
println("'" + lstr + "'")
|
|
panic(v)
|
|
}
|
|
}()
|
|
rand.Seed(time.Now().UnixNano())
|
|
b := make([]byte, 200)
|
|
for i := 0; i < 2000000; i++ {
|
|
n, err := rand.Read(b[:rand.Int()%len(b)])
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lstr = string(b[:n])
|
|
SetRaw(lstr, "zzzz.zzzz.zzzz", "123")
|
|
}
|
|
}
|
|
|
|
func TestDeleteIssue21(t *testing.T) {
|
|
json := `{"country_code_from":"NZ","country_code_to":"SA","date_created":"2018-09-13T02:56:11.25783Z","date_updated":"2018-09-14T03:15:16.67356Z","disabled":false,"last_edited_by":"Developers","id":"a3e...bc454","merchant_id":"f2b...b91abf","signed_date":"2018-02-01T00:00:00Z","start_date":"2018-03-01T00:00:00Z","url":"https://www.google.com"}`
|
|
res1 := gjson.Get(json, "date_updated")
|
|
var err error
|
|
json, err = Delete(json, "date_updated")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
res2 := gjson.Get(json, "date_updated")
|
|
res3 := gjson.Get(json, "date_created")
|
|
if !res1.Exists() || res2.Exists() || !res3.Exists() {
|
|
t.Fatal("bad news")
|
|
}
|
|
|
|
// We change the number of characters in this to make the section of the string before the section that we want to delete a certain length
|
|
|
|
//---------------------------
|
|
lenBeforeToDeleteIs307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","to_delete":"0","2":""}`
|
|
|
|
expectedForLenBefore307AsBytes := `{"1":"","0":"012345678901234567890123456789012345678901234567890123456789012345678901234567","2":""}`
|
|
//---------------------------
|
|
|
|
//---------------------------
|
|
lenBeforeToDeleteIs308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","to_delete":"0","2":""}`
|
|
|
|
expectedForLenBefore308AsBytes := `{"1":"","0":"0123456789012345678901234567890123456789012345678901234567890123456789012345678","2":""}`
|
|
//---------------------------
|
|
|
|
//---------------------------
|
|
lenBeforeToDeleteIs309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","to_delete":"0","2":""}`
|
|
|
|
expectedForLenBefore309AsBytes := `{"1":"","0":"01234567890123456789012345678901234567890123456789012345678901234567890123456","2":""}`
|
|
//---------------------------
|
|
|
|
var data = []struct {
|
|
desc string
|
|
input string
|
|
expected string
|
|
}{
|
|
{
|
|
desc: "len before \"to_delete\"... = 307",
|
|
input: lenBeforeToDeleteIs307AsBytes,
|
|
expected: expectedForLenBefore307AsBytes,
|
|
},
|
|
{
|
|
desc: "len before \"to_delete\"... = 308",
|
|
input: lenBeforeToDeleteIs308AsBytes,
|
|
expected: expectedForLenBefore308AsBytes,
|
|
},
|
|
{
|
|
desc: "len before \"to_delete\"... = 309",
|
|
input: lenBeforeToDeleteIs309AsBytes,
|
|
expected: expectedForLenBefore309AsBytes,
|
|
},
|
|
}
|
|
|
|
for i, d := range data {
|
|
result, err := Delete(d.input, "to_delete")
|
|
|
|
if err != nil {
|
|
t.Error(fmtErrorf(testError{
|
|
unexpected: "error",
|
|
desc: d.desc,
|
|
i: i,
|
|
lenInput: len(d.input),
|
|
input: d.input,
|
|
expected: d.expected,
|
|
result: result,
|
|
}))
|
|
}
|
|
if result != d.expected {
|
|
t.Error(fmtErrorf(testError{
|
|
unexpected: "result",
|
|
desc: d.desc,
|
|
i: i,
|
|
lenInput: len(d.input),
|
|
input: d.input,
|
|
expected: d.expected,
|
|
result: result,
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
|
|
type testError struct {
|
|
unexpected string
|
|
desc string
|
|
i int
|
|
lenInput int
|
|
input interface{}
|
|
expected interface{}
|
|
result interface{}
|
|
}
|
|
|
|
func fmtErrorf(e testError) string {
|
|
return fmt.Sprintf(
|
|
"Unexpected %s:\n\t"+
|
|
"for=%q\n\t"+
|
|
"i=%d\n\t"+
|
|
"len(input)=%d\n\t"+
|
|
"input=%v\n\t"+
|
|
"expected=%v\n\t"+
|
|
"result=%v",
|
|
e.unexpected, e.desc, e.i, e.lenInput, e.input, e.expected, e.result,
|
|
)
|
|
}
|
|
|
|
func TestSetDotKeyIssue10(t *testing.T) {
|
|
json := `{"app.token":"abc"}`
|
|
json, _ = Set(json, `app\.token`, "cde")
|
|
if json != `{"app.token":"cde"}` {
|
|
t.Fatalf("expected '%v', got '%v'", `{"app.token":"cde"}`, json)
|
|
}
|
|
}
|
|
func TestDeleteDotKeyIssue19(t *testing.T) {
|
|
json := []byte(`{"data":{"key1":"value1","key2.something":"value2"}}`)
|
|
json, _ = DeleteBytes(json, `data.key2\.something`)
|
|
if string(json) != `{"data":{"key1":"value1"}}` {
|
|
t.Fatalf("expected '%v', got '%v'", `{"data":{"key1":"value1"}}`, json)
|
|
}
|
|
}
|
|
|
|
func TestIssue36(t *testing.T) {
|
|
var json = `
|
|
{
|
|
"size": 1000
|
|
}
|
|
`
|
|
var raw = `
|
|
{
|
|
"sample": "hello"
|
|
}
|
|
`
|
|
_ = raw
|
|
if true {
|
|
json, _ = SetRaw(json, "aggs", raw)
|
|
}
|
|
if !gjson.Valid(json) {
|
|
t.Fatal("invalid json")
|
|
}
|
|
res := gjson.Get(json, "aggs.sample").String()
|
|
if res != "hello" {
|
|
t.Fatal("unexpected result")
|
|
}
|
|
}
|
|
|
|
var example = `
|
|
{
|
|
"name": {"first": "Tom", "last": "Anderson"},
|
|
"age":37,
|
|
"children": ["Sara","Alex","Jack"],
|
|
"fav.movie": "Deer Hunter",
|
|
"friends": [
|
|
{"first": "Dale", "last": "Murphy", "age": 44, "nets": ["ig", "fb", "tw"]},
|
|
{"first": "Roger", "last": "Craig", "age": 68, "nets": ["fb", "tw"]},
|
|
{"first": "Jane", "last": "Murphy", "age": 47, "nets": ["ig", "tw"]}
|
|
]
|
|
}
|
|
`
|
|
|
|
func TestIndex(t *testing.T) {
|
|
path := `friends.#(last="Murphy").last`
|
|
json, err := Set(example, path, "Johnson")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Murphy"]` {
|
|
t.Fatal("mismatch")
|
|
}
|
|
}
|
|
|
|
func TestIndexes(t *testing.T) {
|
|
path := `friends.#(last="Murphy")#.last`
|
|
json, err := Set(example, path, "Johnson")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if gjson.Get(json, "friends.#.last").String() != `["Johnson","Craig","Johnson"]` {
|
|
t.Fatal("mismatch")
|
|
}
|
|
}
|
|
|
|
func TestIssue61(t *testing.T) {
|
|
json := `{
|
|
"@context": {
|
|
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
|
"@vocab": "http://schema.org/",
|
|
"sh": "http://www.w3.org/ns/shacl#"
|
|
}
|
|
}`
|
|
json1, _ := Set(json, "@context.@vocab", "newval")
|
|
if gjson.Get(json1, "@context.@vocab").String() != "newval" {
|
|
t.Fail()
|
|
}
|
|
}
|
|
|
|
func TestEmptyKeyIssue77(t *testing.T) {
|
|
const json = `{"name":{"first":"Janet"},"age":47,"":"foo"}`
|
|
|
|
// An empty key is a valid object key and gjson reads it, so sjson has to
|
|
// be able to write and delete it too.
|
|
if gjson.Get(json, "").String() != "foo" {
|
|
t.Fatalf("gjson cannot read the empty key")
|
|
}
|
|
|
|
testRaw(t, setString, `{"name":{"first":"Janet"},"age":47,"":"bar"}`,
|
|
json, "", "bar")
|
|
testRaw(t, setDelete, `{"name":{"first":"Janet"},"age":47}`,
|
|
json, "", nil)
|
|
|
|
// setting an empty key that is not there yet adds it
|
|
testRaw(t, setString, `{"a":1,"":"bar"}`, `{"a":1}`, "", "bar")
|
|
testRaw(t, setString, `{"":"bar"}`, `{}`, "", "bar")
|
|
|
|
// deleting an empty key that is not there is a no-op, like any other
|
|
// missing key
|
|
testRaw(t, setDelete, `{"a":1}`, `{"a":1}`, "", nil)
|
|
|
|
// an empty key is a key, never the array index zero
|
|
testRaw(t, setInt, `{"x":1,"a":{"":2}}`, `{"x":1}`, "a.", 2)
|
|
testRaw(t, setInt, `{"a":{"b":{"":2}}}`, `{}`, "a.b.", 2)
|
|
testRaw(t, setInt, `{"a":{"":{"b":2}}}`, `{}`, "a..b", 2)
|
|
|
|
// whatever sjson writes for an empty key, gjson has to read back
|
|
out, err := Set(`{"x":1}`, "a.", 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res := gjson.Get(out, "a."); !res.Exists() || res.Int() != 2 {
|
|
t.Fatalf("expected gjson to read back '2', got '%v' from '%v'",
|
|
res.Raw, out)
|
|
}
|
|
|
|
// an empty key still cannot address an array element, and reports the
|
|
// same error as any other non-numeric key
|
|
if _, err := Set(`[1,2,3]`, "", "x"); err == nil {
|
|
t.Fatalf("expected an error for an empty key on an array")
|
|
}
|
|
}
|