107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
// Copyright (c) 2021 Tailscale Inc & 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 hujson
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
var errNotFound = fmt.Errorf("value not found")
|
|
|
|
// Find locates the value specified by the JSON pointer (see RFC 6901).
|
|
// It returns nil if the value does not exist or the pointer is invalid.
|
|
// If a JSON object has multiple members matching a given name,
|
|
// the first is returned. Object names are matched exactly,
|
|
// rather than with a case-insensitive match.
|
|
func (v *Value) Find(ptr string) *Value {
|
|
if s, err := v.find(findState{pointer: ptr}); err == nil {
|
|
return s.value
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type findState struct {
|
|
pointer string // pointer[:offset] is the current value, pointer[offset:] is the remainder
|
|
offset int
|
|
|
|
parent composite // nil for root pointer
|
|
name string // name into parent to obtain current value
|
|
idx int // idx into parent to obtain current value
|
|
value *Value // the current value
|
|
}
|
|
|
|
func (v *Value) find(s findState) (findState, error) {
|
|
// An empty pointer denotes the value itself.
|
|
s.value = v
|
|
if s.pointer[s.offset:] == "" {
|
|
return s, nil
|
|
}
|
|
comp, ok := v.Value.(composite)
|
|
if !ok {
|
|
return s, fmt.Errorf("invalid pointer: cannot index into literal at %v", s.pointer[:s.offset])
|
|
}
|
|
|
|
// There must be one or more fragments.
|
|
s.parent, s.idx, s.name = nil, 0, ""
|
|
if !strings.HasPrefix(s.pointer[s.offset:], "/") {
|
|
return s, fmt.Errorf("invalid pointer: lacks a forward slash prefix")
|
|
}
|
|
n := len("/")
|
|
if i := strings.IndexByte(s.pointer[s.offset+n:], '/'); i >= 0 {
|
|
n += i
|
|
} else {
|
|
n = len(s.pointer) - s.offset
|
|
}
|
|
s.offset += n
|
|
|
|
// Unescape the name if necessary (section 4).
|
|
name := s.pointer[s.offset-n : s.offset]
|
|
if strings.IndexByte(name, '~') >= 0 {
|
|
name = strings.ReplaceAll(name, "~1", "/")
|
|
name = strings.ReplaceAll(name, "~0", "~")
|
|
}
|
|
name = name[len("/"):]
|
|
|
|
// Index into the object or array.
|
|
s.parent, s.name, s.idx = comp, name, comp.length()
|
|
switch comp := v.Value.(type) {
|
|
case *Object:
|
|
for i, m := range comp.Members {
|
|
if m.Name.Value.(Literal).equalString(name) {
|
|
s.idx = i
|
|
return comp.Members[i].Value.find(s)
|
|
}
|
|
}
|
|
case *Array:
|
|
if name == "-" {
|
|
return s, errNotFound
|
|
}
|
|
i, err := strconv.ParseUint(name, 10, 0)
|
|
if err != nil || (i == 0 && name != "0") {
|
|
return s, fmt.Errorf("invalid array index: %s", name)
|
|
}
|
|
if i < uint64(len(comp.Elements)) {
|
|
s.idx = int(i)
|
|
return comp.Elements[i].find(s)
|
|
}
|
|
}
|
|
return s, errNotFound
|
|
}
|
|
|
|
func (b Literal) equalString(s string) bool {
|
|
// Fast-path: Assume there are no escape characters.
|
|
if len(b) >= 2 && b[0] == '"' && b[len(b)-1] == '"' && bytes.IndexByte(b, '\\') < 0 {
|
|
return string(b[len(`"`):len(b)-len(`"`)]) == s
|
|
}
|
|
// Slow-path: Unescape the string and then compare it.
|
|
// TODO(dsnet): Implement allocation-free comparison.
|
|
var s2 string
|
|
return json.Unmarshal(b, &s2) == nil && s == s2
|
|
}
|