2026-10-19 06:47:34 +01:00
|
|
|
|
package jsonstream
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bytes"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
2026-11-08 19:03:21 +01:00
|
|
|
|
"slices"
|
2026-10-19 06:47:34 +01:00
|
|
|
|
"strings"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
func copyString(t *testing.T, in string, fn func(*Member) error) (string, error) {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
var out bytes.Buffer
|
2026-11-08 19:03:21 +01:00
|
|
|
|
err := CopyObject(strings.NewReader(in), &out, fn)
|
2026-10-19 06:47:34 +01:00
|
|
|
|
return out.String(), err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestCopyObjectKeepsValueBytes(t *testing.T) {
|
|
|
|
|
|
in := `{"a": 1.50, "b" : {"x": "<&>", "y":[1, 2]}, "c":null, "d":"é"}`
|
|
|
|
|
|
got, err := copyString(t, in, func(m *Member) error { return nil })
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := `{"a":1.50,"b":{"x": "<&>", "y":[1, 2]},"c":null,"d":"é"}`; got != want {
|
|
|
|
|
|
t.Errorf("got %s\nwant %s", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestCopyObjectFiltersNested(t *testing.T) {
|
|
|
|
|
|
in := `{"info":{"subdir":"noarch"},"packages":{"keep":{"n":1},"drop":{"n":2},"also":{"n":3}},"tail":[]}`
|
|
|
|
|
|
got, err := copyString(t, in, func(m *Member) error {
|
|
|
|
|
|
if m.Key != "packages" {
|
|
|
|
|
|
return m.Copy()
|
|
|
|
|
|
}
|
|
|
|
|
|
return m.Object(func(entry *Member) error {
|
|
|
|
|
|
raw, err := entry.Raw()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if entry.Key == "drop" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return entry.Write(raw)
|
|
|
|
|
|
})
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := `{"info":{"subdir":"noarch"},"packages":{"keep":{"n":1},"also":{"n":3}},"tail":[]}`; got != want {
|
|
|
|
|
|
t.Errorf("got %s\nwant %s", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
if !json.Valid([]byte(got)) {
|
|
|
|
|
|
t.Error("output is not valid JSON")
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestCopyObjectSkipReplaceAndEmpty(t *testing.T) {
|
|
|
|
|
|
got, err := copyString(t, `{"first":1,"secret":"x","n":2,"empty":{}}`, func(m *Member) error {
|
|
|
|
|
|
switch m.Key {
|
|
|
|
|
|
case "first", "secret":
|
2026-10-19 08:35:46 +01:00
|
|
|
|
_, err := m.Raw()
|
|
|
|
|
|
return err
|
2026-10-19 06:47:34 +01:00
|
|
|
|
case "n":
|
|
|
|
|
|
if _, err := m.Raw(); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return m.Write([]byte(`3`))
|
|
|
|
|
|
case "empty":
|
|
|
|
|
|
return m.Object(func(*Member) error { return nil })
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := `{"n":3,"empty":{}}`; got != want {
|
|
|
|
|
|
t.Errorf("got %s, want %s", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestCopyObjectKeys(t *testing.T) {
|
|
|
|
|
|
got, err := copyString(t, `{"a<b":1,"quote\"d":2,"tab\t":3}`, func(m *Member) error { return nil })
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := `{"a<b":1,"quote\"d":2,"tab\t":3}`; got != want {
|
|
|
|
|
|
t.Errorf("got %s, want %s", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestMemberMisuse(t *testing.T) {
|
|
|
|
|
|
_, err := copyString(t, `{"a":1}`, func(m *Member) error { return m.Write([]byte("1")) })
|
|
|
|
|
|
if !errors.Is(err, errUnconsumed) {
|
|
|
|
|
|
t.Errorf("Write before Raw: err = %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
_, err = copyString(t, `{"a":1}`, func(m *Member) error {
|
2026-10-19 08:35:46 +01:00
|
|
|
|
if _, err := m.Raw(); err != nil {
|
2026-10-19 06:47:34 +01:00
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-10-19 08:35:46 +01:00
|
|
|
|
_, err := m.Raw()
|
|
|
|
|
|
return err
|
2026-10-19 06:47:34 +01:00
|
|
|
|
})
|
|
|
|
|
|
if !errors.Is(err, errConsumed) {
|
2026-10-19 08:35:46 +01:00
|
|
|
|
t.Errorf("second Raw: err = %v", err)
|
2026-10-19 06:47:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestObjectNull(t *testing.T) {
|
|
|
|
|
|
got, err := copyString(t, `{"packages":null}`, func(m *Member) error {
|
|
|
|
|
|
return m.Object(func(*Member) error { return nil })
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil || got != `{"packages":null}` {
|
|
|
|
|
|
t.Errorf("got %s, %v", got, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestCopyObjectErrors(t *testing.T) {
|
|
|
|
|
|
tests := map[string]string{
|
|
|
|
|
|
"not an object": `[1,2]`,
|
|
|
|
|
|
"truncated": `{"a":{"b":1}`,
|
|
|
|
|
|
"truncated value": `{"a":"abc`,
|
|
|
|
|
|
"trailing data": `{"a":1} {"b":2}`,
|
|
|
|
|
|
"nested not an object": `{"packages":[1]}`,
|
|
|
|
|
|
"empty": ``,
|
|
|
|
|
|
}
|
|
|
|
|
|
for name, in := range tests {
|
|
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
|
|
_, err := copyString(t, in, func(m *Member) error {
|
|
|
|
|
|
if m.Key == "packages" {
|
|
|
|
|
|
return m.Object(func(*Member) error { return nil })
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
t.Error("expected an error")
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := copyString(t, `[1]`, func(*Member) error { return nil }); !errors.Is(err, ErrNotObject) {
|
|
|
|
|
|
t.Errorf("err = %v, want ErrNotObject", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-08 19:03:21 +01:00
|
|
|
|
|
|
|
|
|
|
// TestCopyObjectReadsJSONAsEncodingJSON checks CopyObject accepts what
|
|
|
|
|
|
// encoding/json does: repeated names, each passed to fn, and invalid UTF-8.
|
|
|
|
|
|
// Keys keep their bytes, escapes included, while Key is unquoted.
|
|
|
|
|
|
func TestCopyObjectReadsJSONAsEncodingJSON(t *testing.T) {
|
|
|
|
|
|
var keys []string
|
|
|
|
|
|
in := "{\"a\":1,\"a\":2,\"caf\\u00e9\":\"bad \xff byte\",\"skip\":{\"x\":[1,2]}}"
|
|
|
|
|
|
got, err := copyString(t, in, func(m *Member) error {
|
|
|
|
|
|
keys = append(keys, m.Key)
|
|
|
|
|
|
if m.Key == "skip" {
|
|
|
|
|
|
return m.Skip()
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := "{\"a\":1,\"a\":2,\"caf\\u00e9\":\"bad \xff byte\"}"; got != want {
|
|
|
|
|
|
t.Errorf("got %q, want %q", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := []string{"a", "a", "café", "skip"}; !slices.Equal(keys, want) {
|
|
|
|
|
|
t.Errorf("keys %q, want %q", keys, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func TestSkipThenWriteReplaces(t *testing.T) {
|
|
|
|
|
|
got, err := copyString(t, `{"a":{"big":[1,2,3]},"b":2}`, func(m *Member) error {
|
|
|
|
|
|
if m.Key != "a" {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := m.Skip(); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return m.Write([]byte(`"small"`))
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil || got != `{"a":"small","b":2}` {
|
|
|
|
|
|
t.Errorf("got %s, %v", got, err)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-08 19:12:30 +01:00
|
|
|
|
|
|
|
|
|
|
// TestCopyObjectInvalidUTF8Key checks a key with invalid UTF-8 is accepted:
|
|
|
|
|
|
// Key has U+FFFD in its place, as with encoding/json, and the key is written
|
|
|
|
|
|
// with its original bytes.
|
|
|
|
|
|
func TestCopyObjectInvalidUTF8Key(t *testing.T) {
|
|
|
|
|
|
var keys []string
|
|
|
|
|
|
got, err := copyString(t, "{\"k\xff\":1}", func(m *Member) error {
|
|
|
|
|
|
keys = append(keys, m.Key)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
})
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := "{\"k\xff\":1}"; got != want {
|
|
|
|
|
|
t.Errorf("got %q, want %q", got, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
if want := []string{"k<>"}; !slices.Equal(keys, want) {
|
|
|
|
|
|
t.Errorf("keys %q, want %q", keys, want)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|