Files
dualfroz 5b70965fcb fix: allow setting and deleting an empty object key
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
2026-09-03 14:27:28 +02:00

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")
}
}