2026-10-25 22:19:37 +01:00
|
|
|
// Package jsonscan walks a JSON document in place, reporting where
|
|
|
|
|
// object members sit as offsets into the original bytes. It lets a caller
|
|
|
|
|
// read or replace one value in a large metadata document without decoding the
|
|
|
|
|
// rest of it into Go values, which for an npm packument costs many times the
|
|
|
|
|
// document's size.
|
|
|
|
|
//
|
|
|
|
|
// It checks structure only as far as it needs to find its way through
|
|
|
|
|
// the document. Callers that copy unvisited bytes into a response validate the
|
|
|
|
|
// whole document first with json.Valid.
|
2026-10-25 22:19:24 +01:00
|
|
|
package jsonscan
|
2026-10-08 07:36:33 +02:00
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
)
|
|
|
|
|
|
2026-10-25 22:19:37 +01:00
|
|
|
// ErrMalformed is returned when the document is not well-formed JSON.
|
2026-10-25 22:19:24 +01:00
|
|
|
var ErrMalformed = errors.New("malformed JSON")
|
2026-10-08 07:36:33 +02:00
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// ErrNotObject is returned when a value expected to be an object is not.
|
|
|
|
|
var ErrNotObject = errors.New("JSON value is not an object")
|
2026-10-08 07:36:33 +02:00
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// ErrStop is returned by a callback to end a walk early once it has
|
2026-10-10 08:31:37 +02:00
|
|
|
// found what it was looking for.
|
2026-10-25 22:19:24 +01:00
|
|
|
var ErrStop = errors.New("stop scanning")
|
2026-10-10 08:31:37 +02:00
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// Member is one member of a JSON object as offsets into the bytes that
|
2026-10-08 07:36:33 +02:00
|
|
|
// were scanned. The key span includes its quotes.
|
2026-10-25 22:19:24 +01:00
|
|
|
type Member struct {
|
|
|
|
|
KeyStart, KeyEnd int
|
|
|
|
|
ValStart, ValEnd int
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
func (m Member) Key(data []byte) []byte { return data[m.KeyStart:m.KeyEnd] }
|
|
|
|
|
func (m Member) Value(data []byte) []byte { return data[m.ValStart:m.ValEnd] }
|
2026-10-08 07:36:33 +02:00
|
|
|
|
|
|
|
|
func skipJSONSpace(data []byte, i int) int {
|
|
|
|
|
for i < len(data) {
|
|
|
|
|
switch data[i] {
|
|
|
|
|
case ' ', '\t', '\n', '\r':
|
|
|
|
|
i++
|
|
|
|
|
default:
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return i
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// skipJSONString returns the index just past the string starting at data[i].
|
|
|
|
|
func skipJSONString(data []byte, i int) (int, error) {
|
|
|
|
|
for j := i + 1; j < len(data); j++ {
|
|
|
|
|
switch data[j] {
|
|
|
|
|
case '\\':
|
|
|
|
|
j++
|
|
|
|
|
case '"':
|
|
|
|
|
return j + 1, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
return 0, ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// skipJSONValue returns the index just past the value starting at data[i].
|
|
|
|
|
func skipJSONValue(data []byte, i int) (int, error) {
|
|
|
|
|
if i >= len(data) {
|
2026-10-25 22:19:24 +01:00
|
|
|
return 0, ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
switch data[i] {
|
|
|
|
|
case '"':
|
|
|
|
|
return skipJSONString(data, i)
|
|
|
|
|
case '{', '[':
|
|
|
|
|
depth := 0
|
|
|
|
|
for j := i; j < len(data); j++ {
|
|
|
|
|
switch data[j] {
|
|
|
|
|
case '"':
|
|
|
|
|
end, err := skipJSONString(data, j)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, err
|
|
|
|
|
}
|
|
|
|
|
j = end - 1
|
|
|
|
|
case '{', '[':
|
|
|
|
|
depth++
|
|
|
|
|
case '}', ']':
|
|
|
|
|
depth--
|
|
|
|
|
if depth == 0 {
|
|
|
|
|
return j + 1, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
return 0, ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
case '}', ']', ',', ':':
|
2026-10-25 22:19:24 +01:00
|
|
|
return 0, ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
default:
|
|
|
|
|
// A number, true, false or null runs to the next delimiter.
|
|
|
|
|
j := i
|
|
|
|
|
for j < len(data) {
|
|
|
|
|
switch data[j] {
|
|
|
|
|
case ',', '}', ']', ' ', '\t', '\n', '\r':
|
|
|
|
|
return j, nil
|
|
|
|
|
}
|
|
|
|
|
j++
|
|
|
|
|
}
|
|
|
|
|
return j, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// EachMember calls fn for each member of the object obj holds, in
|
|
|
|
|
// document order. It returns ErrNotObject if obj is not an object, and
|
2026-10-08 07:36:33 +02:00
|
|
|
// stops early with fn's error if fn returns one.
|
2026-10-25 22:19:24 +01:00
|
|
|
func EachMember(obj []byte, fn func(Member) error) error {
|
2026-10-08 07:36:33 +02:00
|
|
|
i := skipJSONSpace(obj, 0)
|
|
|
|
|
if i >= len(obj) || obj[i] != '{' {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrNotObject
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
i = skipJSONSpace(obj, i+1)
|
|
|
|
|
if i < len(obj) && obj[i] == '}' {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
if i >= len(obj) || obj[i] != '"' {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
keyEnd, err := skipJSONString(obj, i)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
colon := skipJSONSpace(obj, keyEnd)
|
|
|
|
|
if colon >= len(obj) || obj[colon] != ':' {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
valStart := skipJSONSpace(obj, colon+1)
|
|
|
|
|
valEnd, err := skipJSONValue(obj, valStart)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
if err := fn(Member{KeyStart: i, KeyEnd: keyEnd, ValStart: valStart, ValEnd: valEnd}); err != nil {
|
2026-10-08 07:36:33 +02:00
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
next := skipJSONSpace(obj, valEnd)
|
|
|
|
|
if next >= len(obj) {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
switch obj[next] {
|
|
|
|
|
case ',':
|
|
|
|
|
i = skipJSONSpace(obj, next+1)
|
|
|
|
|
case '}':
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// EachElement calls fn with each element of the array arr holds, in
|
2026-10-10 08:31:37 +02:00
|
|
|
// order, and stops early with fn's error if fn returns one.
|
2026-10-25 22:19:24 +01:00
|
|
|
func EachElement(arr []byte, fn func(element []byte) error) error {
|
2026-10-10 08:31:37 +02:00
|
|
|
i := skipJSONSpace(arr, 0)
|
|
|
|
|
if i >= len(arr) || arr[i] != '[' {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-10 08:31:37 +02:00
|
|
|
}
|
|
|
|
|
i = skipJSONSpace(arr, i+1)
|
|
|
|
|
if i < len(arr) && arr[i] == ']' {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
for {
|
|
|
|
|
end, err := skipJSONValue(arr, i)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := fn(arr[i:end]); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
next := skipJSONSpace(arr, end)
|
|
|
|
|
if next >= len(arr) {
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-10 08:31:37 +02:00
|
|
|
}
|
|
|
|
|
switch arr[next] {
|
|
|
|
|
case ',':
|
|
|
|
|
i = skipJSONSpace(arr, next+1)
|
|
|
|
|
case ']':
|
|
|
|
|
return nil
|
|
|
|
|
default:
|
2026-10-25 22:19:24 +01:00
|
|
|
return ErrMalformed
|
2026-10-10 08:31:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// RewriteMembers copies the object obj holds to out byte for byte,
|
2026-10-10 08:31:37 +02:00
|
|
|
// except that rewrite may write a member's value itself. It returns true
|
|
|
|
|
// when it did; otherwise the original value is copied.
|
2026-10-25 22:19:24 +01:00
|
|
|
func RewriteMembers(out *bytes.Buffer, obj []byte, rewrite func(out *bytes.Buffer, m Member) (bool, error)) error {
|
2026-10-10 08:31:37 +02:00
|
|
|
copied := 0
|
2026-10-25 22:19:24 +01:00
|
|
|
err := EachMember(obj, func(m Member) error {
|
|
|
|
|
out.Write(obj[copied:m.ValStart])
|
|
|
|
|
copied = m.ValStart
|
2026-10-10 08:31:37 +02:00
|
|
|
rewritten, err := rewrite(out, m)
|
|
|
|
|
if rewritten {
|
2026-10-25 22:19:24 +01:00
|
|
|
copied = m.ValEnd
|
2026-10-10 08:31:37 +02:00
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
})
|
|
|
|
|
out.Write(obj[copied:])
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// Key decodes a quoted key as EachMember reports it.
|
|
|
|
|
func Key(raw []byte) (string, error) {
|
2026-10-08 07:36:33 +02:00
|
|
|
if bytes.IndexByte(raw, '\\') < 0 {
|
|
|
|
|
return string(raw[1 : len(raw)-1]), nil
|
|
|
|
|
}
|
|
|
|
|
var key string
|
|
|
|
|
err := json.Unmarshal(raw, &key)
|
|
|
|
|
return key, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// KeyIs reports whether a quoted key decodes to name.
|
|
|
|
|
func KeyIs(raw []byte, name string) bool {
|
2026-10-08 07:36:33 +02:00
|
|
|
if bytes.IndexByte(raw, '\\') < 0 {
|
|
|
|
|
return string(raw[1:len(raw)-1]) == name
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
key, err := Key(raw)
|
2026-10-08 07:36:33 +02:00
|
|
|
return err == nil && key == name
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// StringIs reports whether raw is a JSON string that decodes to s.
|
|
|
|
|
func StringIs(raw []byte, s string) bool {
|
|
|
|
|
return len(raw) >= 2 && raw[0] == '"' && KeyIs(raw, s)
|
2026-10-10 08:31:37 +02:00
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// FindMember returns the member of obj named name. When the key repeats,
|
2026-10-08 07:36:33 +02:00
|
|
|
// the last one wins, as it does for JSON.parse and encoding/json.
|
2026-10-25 22:19:24 +01:00
|
|
|
func FindMember(obj []byte, name string) (Member, bool, error) {
|
|
|
|
|
var found Member
|
2026-10-08 07:36:33 +02:00
|
|
|
ok := false
|
2026-10-25 22:19:24 +01:00
|
|
|
err := EachMember(obj, func(m Member) error {
|
|
|
|
|
if KeyIs(m.Key(obj), name) {
|
2026-10-08 07:36:33 +02:00
|
|
|
found, ok = m, true
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
return found, ok, err
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// Lookup follows path through nested objects in doc and returns the
|
2026-10-08 07:36:33 +02:00
|
|
|
// value at its end. It returns nil and no error when a key along the path is
|
|
|
|
|
// missing or names something other than an object.
|
2026-10-25 22:19:24 +01:00
|
|
|
func Lookup(doc []byte, path ...string) ([]byte, error) {
|
2026-10-08 07:36:33 +02:00
|
|
|
value := doc
|
|
|
|
|
for _, name := range path {
|
2026-10-25 22:19:24 +01:00
|
|
|
m, ok, err := FindMember(value, name)
|
|
|
|
|
if errors.Is(err, ErrNotObject) {
|
2026-10-08 07:36:33 +02:00
|
|
|
return nil, nil
|
|
|
|
|
}
|
|
|
|
|
if err != nil || !ok {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
value = m.Value(value)
|
2026-10-08 07:36:33 +02:00
|
|
|
}
|
|
|
|
|
return value, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-25 22:19:24 +01:00
|
|
|
// LookupString is Lookup for a string value. ok is false when the
|
2026-10-08 07:36:33 +02:00
|
|
|
// value is missing or is not a string.
|
2026-10-25 22:19:24 +01:00
|
|
|
func LookupString(doc []byte, path ...string) (string, bool, error) {
|
|
|
|
|
raw, err := Lookup(doc, path...)
|
2026-10-08 07:36:33 +02:00
|
|
|
if err != nil || len(raw) == 0 || raw[0] != '"' {
|
|
|
|
|
return "", false, err
|
|
|
|
|
}
|
|
|
|
|
var s string
|
|
|
|
|
if err := json.Unmarshal(raw, &s); err != nil {
|
|
|
|
|
return "", false, err
|
|
|
|
|
}
|
|
|
|
|
return s, true, nil
|
|
|
|
|
}
|
2026-10-25 22:19:24 +01:00
|
|
|
|
|
|
|
|
// WriteFilteredObject writes the object obj holds, keeping the members
|
|
|
|
|
// whose decoded key keep accepts, followed by extra string members.
|
|
|
|
|
func WriteFilteredObject(out *bytes.Buffer, obj []byte, keep func(string) bool, extra [][2]string) error {
|
|
|
|
|
out.WriteByte('{')
|
|
|
|
|
first := true
|
|
|
|
|
err := EachMember(obj, func(m Member) error {
|
|
|
|
|
key, err := Key(m.Key(obj))
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if !keep(key) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if !first {
|
|
|
|
|
out.WriteByte(',')
|
|
|
|
|
}
|
|
|
|
|
first = false
|
|
|
|
|
out.Write(obj[m.KeyStart:m.ValEnd])
|
|
|
|
|
return nil
|
|
|
|
|
})
|
|
|
|
|
for _, member := range extra {
|
|
|
|
|
if !first {
|
|
|
|
|
out.WriteByte(',')
|
|
|
|
|
}
|
|
|
|
|
first = false
|
|
|
|
|
key, _ := json.Marshal(member[0])
|
|
|
|
|
value, _ := json.Marshal(member[1])
|
|
|
|
|
out.Write(key)
|
|
|
|
|
out.WriteByte(':')
|
|
|
|
|
out.Write(value)
|
|
|
|
|
}
|
|
|
|
|
out.WriteByte('}')
|
|
|
|
|
return err
|
|
|
|
|
}
|