mirror of
https://codeberg.org/superseriousbusiness/gotosocial.git
synced 2024-12-23 17:40:20 +03:00
1284 lines
29 KiB
Go
1284 lines
29 KiB
Go
|
package jsonparser
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"strconv"
|
||
|
)
|
||
|
|
||
|
// Errors
|
||
|
var (
|
||
|
KeyPathNotFoundError = errors.New("Key path not found")
|
||
|
UnknownValueTypeError = errors.New("Unknown value type")
|
||
|
MalformedJsonError = errors.New("Malformed JSON error")
|
||
|
MalformedStringError = errors.New("Value is string, but can't find closing '\"' symbol")
|
||
|
MalformedArrayError = errors.New("Value is array, but can't find closing ']' symbol")
|
||
|
MalformedObjectError = errors.New("Value looks like object, but can't find closing '}' symbol")
|
||
|
MalformedValueError = errors.New("Value looks like Number/Boolean/None, but can't find its end: ',' or '}' symbol")
|
||
|
OverflowIntegerError = errors.New("Value is number, but overflowed while parsing")
|
||
|
MalformedStringEscapeError = errors.New("Encountered an invalid escape sequence in a string")
|
||
|
)
|
||
|
|
||
|
// How much stack space to allocate for unescaping JSON strings; if a string longer
|
||
|
// than this needs to be escaped, it will result in a heap allocation
|
||
|
const unescapeStackBufSize = 64
|
||
|
|
||
|
func tokenEnd(data []byte) int {
|
||
|
for i, c := range data {
|
||
|
switch c {
|
||
|
case ' ', '\n', '\r', '\t', ',', '}', ']':
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return len(data)
|
||
|
}
|
||
|
|
||
|
func findTokenStart(data []byte, token byte) int {
|
||
|
for i := len(data) - 1; i >= 0; i-- {
|
||
|
switch data[i] {
|
||
|
case token:
|
||
|
return i
|
||
|
case '[', '{':
|
||
|
return 0
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
func findKeyStart(data []byte, key string) (int, error) {
|
||
|
i := 0
|
||
|
ln := len(data)
|
||
|
if ln > 0 && (data[0] == '{' || data[0] == '[') {
|
||
|
i = 1
|
||
|
}
|
||
|
var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
|
||
|
|
||
|
if ku, err := Unescape(StringToBytes(key), stackbuf[:]); err == nil {
|
||
|
key = bytesToString(&ku)
|
||
|
}
|
||
|
|
||
|
for i < ln {
|
||
|
switch data[i] {
|
||
|
case '"':
|
||
|
i++
|
||
|
keyBegin := i
|
||
|
|
||
|
strEnd, keyEscaped := stringEnd(data[i:])
|
||
|
if strEnd == -1 {
|
||
|
break
|
||
|
}
|
||
|
i += strEnd
|
||
|
keyEnd := i - 1
|
||
|
|
||
|
valueOffset := nextToken(data[i:])
|
||
|
if valueOffset == -1 {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
i += valueOffset
|
||
|
|
||
|
// if string is a key, and key level match
|
||
|
k := data[keyBegin:keyEnd]
|
||
|
// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
|
||
|
// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
|
||
|
if keyEscaped {
|
||
|
if ku, err := Unescape(k, stackbuf[:]); err != nil {
|
||
|
break
|
||
|
} else {
|
||
|
k = ku
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if data[i] == ':' && len(key) == len(k) && bytesToString(&k) == key {
|
||
|
return keyBegin - 1, nil
|
||
|
}
|
||
|
|
||
|
case '[':
|
||
|
end := blockEnd(data[i:], data[i], ']')
|
||
|
if end != -1 {
|
||
|
i = i + end
|
||
|
}
|
||
|
case '{':
|
||
|
end := blockEnd(data[i:], data[i], '}')
|
||
|
if end != -1 {
|
||
|
i = i + end
|
||
|
}
|
||
|
}
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return -1, KeyPathNotFoundError
|
||
|
}
|
||
|
|
||
|
func tokenStart(data []byte) int {
|
||
|
for i := len(data) - 1; i >= 0; i-- {
|
||
|
switch data[i] {
|
||
|
case '\n', '\r', '\t', ',', '{', '[':
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
// Find position of next character which is not whitespace
|
||
|
func nextToken(data []byte) int {
|
||
|
for i, c := range data {
|
||
|
switch c {
|
||
|
case ' ', '\n', '\r', '\t':
|
||
|
continue
|
||
|
default:
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
// Find position of last character which is not whitespace
|
||
|
func lastToken(data []byte) int {
|
||
|
for i := len(data) - 1; i >= 0; i-- {
|
||
|
switch data[i] {
|
||
|
case ' ', '\n', '\r', '\t':
|
||
|
continue
|
||
|
default:
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
// Tries to find the end of string
|
||
|
// Support if string contains escaped quote symbols.
|
||
|
func stringEnd(data []byte) (int, bool) {
|
||
|
escaped := false
|
||
|
for i, c := range data {
|
||
|
if c == '"' {
|
||
|
if !escaped {
|
||
|
return i + 1, false
|
||
|
} else {
|
||
|
j := i - 1
|
||
|
for {
|
||
|
if j < 0 || data[j] != '\\' {
|
||
|
return i + 1, true // even number of backslashes
|
||
|
}
|
||
|
j--
|
||
|
if j < 0 || data[j] != '\\' {
|
||
|
break // odd number of backslashes
|
||
|
}
|
||
|
j--
|
||
|
|
||
|
}
|
||
|
}
|
||
|
} else if c == '\\' {
|
||
|
escaped = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return -1, escaped
|
||
|
}
|
||
|
|
||
|
// Find end of the data structure, array or object.
|
||
|
// For array openSym and closeSym will be '[' and ']', for object '{' and '}'
|
||
|
func blockEnd(data []byte, openSym byte, closeSym byte) int {
|
||
|
level := 0
|
||
|
i := 0
|
||
|
ln := len(data)
|
||
|
|
||
|
for i < ln {
|
||
|
switch data[i] {
|
||
|
case '"': // If inside string, skip it
|
||
|
se, _ := stringEnd(data[i+1:])
|
||
|
if se == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
i += se
|
||
|
case openSym: // If open symbol, increase level
|
||
|
level++
|
||
|
case closeSym: // If close symbol, increase level
|
||
|
level--
|
||
|
|
||
|
// If we have returned to the original level, we're done
|
||
|
if level == 0 {
|
||
|
return i + 1
|
||
|
}
|
||
|
}
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
func searchKeys(data []byte, keys ...string) int {
|
||
|
keyLevel := 0
|
||
|
level := 0
|
||
|
i := 0
|
||
|
ln := len(data)
|
||
|
lk := len(keys)
|
||
|
lastMatched := true
|
||
|
|
||
|
if lk == 0 {
|
||
|
return 0
|
||
|
}
|
||
|
|
||
|
var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
|
||
|
|
||
|
for i < ln {
|
||
|
switch data[i] {
|
||
|
case '"':
|
||
|
i++
|
||
|
keyBegin := i
|
||
|
|
||
|
strEnd, keyEscaped := stringEnd(data[i:])
|
||
|
if strEnd == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
i += strEnd
|
||
|
keyEnd := i - 1
|
||
|
|
||
|
valueOffset := nextToken(data[i:])
|
||
|
if valueOffset == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
i += valueOffset
|
||
|
|
||
|
// if string is a key
|
||
|
if data[i] == ':' {
|
||
|
if level < 1 {
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
key := data[keyBegin:keyEnd]
|
||
|
|
||
|
// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
|
||
|
// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
|
||
|
var keyUnesc []byte
|
||
|
if !keyEscaped {
|
||
|
keyUnesc = key
|
||
|
} else if ku, err := Unescape(key, stackbuf[:]); err != nil {
|
||
|
return -1
|
||
|
} else {
|
||
|
keyUnesc = ku
|
||
|
}
|
||
|
|
||
|
if level <= len(keys) {
|
||
|
if equalStr(&keyUnesc, keys[level-1]) {
|
||
|
lastMatched = true
|
||
|
|
||
|
// if key level match
|
||
|
if keyLevel == level-1 {
|
||
|
keyLevel++
|
||
|
// If we found all keys in path
|
||
|
if keyLevel == lk {
|
||
|
return i + 1
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
lastMatched = false
|
||
|
}
|
||
|
} else {
|
||
|
return -1
|
||
|
}
|
||
|
} else {
|
||
|
i--
|
||
|
}
|
||
|
case '{':
|
||
|
|
||
|
// in case parent key is matched then only we will increase the level otherwise can directly
|
||
|
// can move to the end of this block
|
||
|
if !lastMatched {
|
||
|
end := blockEnd(data[i:], '{', '}')
|
||
|
if end == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
i += end - 1
|
||
|
} else {
|
||
|
level++
|
||
|
}
|
||
|
case '}':
|
||
|
level--
|
||
|
if level == keyLevel {
|
||
|
keyLevel--
|
||
|
}
|
||
|
case '[':
|
||
|
// If we want to get array element by index
|
||
|
if keyLevel == level && keys[level][0] == '[' {
|
||
|
var keyLen = len(keys[level])
|
||
|
if keyLen < 3 || keys[level][0] != '[' || keys[level][keyLen-1] != ']' {
|
||
|
return -1
|
||
|
}
|
||
|
aIdx, err := strconv.Atoi(keys[level][1 : keyLen-1])
|
||
|
if err != nil {
|
||
|
return -1
|
||
|
}
|
||
|
var curIdx int
|
||
|
var valueFound []byte
|
||
|
var valueOffset int
|
||
|
var curI = i
|
||
|
ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
|
||
|
if curIdx == aIdx {
|
||
|
valueFound = value
|
||
|
valueOffset = offset
|
||
|
if dataType == String {
|
||
|
valueOffset = valueOffset - 2
|
||
|
valueFound = data[curI+valueOffset : curI+valueOffset+len(value)+2]
|
||
|
}
|
||
|
}
|
||
|
curIdx += 1
|
||
|
})
|
||
|
|
||
|
if valueFound == nil {
|
||
|
return -1
|
||
|
} else {
|
||
|
subIndex := searchKeys(valueFound, keys[level+1:]...)
|
||
|
if subIndex < 0 {
|
||
|
return -1
|
||
|
}
|
||
|
return i + valueOffset + subIndex
|
||
|
}
|
||
|
} else {
|
||
|
// Do not search for keys inside arrays
|
||
|
if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 {
|
||
|
return -1
|
||
|
} else {
|
||
|
i += arraySkip - 1
|
||
|
}
|
||
|
}
|
||
|
case ':': // If encountered, JSON data is malformed
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
func sameTree(p1, p2 []string) bool {
|
||
|
minLen := len(p1)
|
||
|
if len(p2) < minLen {
|
||
|
minLen = len(p2)
|
||
|
}
|
||
|
|
||
|
for pi_1, p_1 := range p1[:minLen] {
|
||
|
if p2[pi_1] != p_1 {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func EachKey(data []byte, cb func(int, []byte, ValueType, error), paths ...[]string) int {
|
||
|
var x struct{}
|
||
|
pathFlags := make([]bool, len(paths))
|
||
|
var level, pathsMatched, i int
|
||
|
ln := len(data)
|
||
|
|
||
|
var maxPath int
|
||
|
for _, p := range paths {
|
||
|
if len(p) > maxPath {
|
||
|
maxPath = len(p)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pathsBuf := make([]string, maxPath)
|
||
|
|
||
|
for i < ln {
|
||
|
switch data[i] {
|
||
|
case '"':
|
||
|
i++
|
||
|
keyBegin := i
|
||
|
|
||
|
strEnd, keyEscaped := stringEnd(data[i:])
|
||
|
if strEnd == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
i += strEnd
|
||
|
|
||
|
keyEnd := i - 1
|
||
|
|
||
|
valueOffset := nextToken(data[i:])
|
||
|
if valueOffset == -1 {
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
i += valueOffset
|
||
|
|
||
|
// if string is a key, and key level match
|
||
|
if data[i] == ':' {
|
||
|
match := -1
|
||
|
key := data[keyBegin:keyEnd]
|
||
|
|
||
|
// for unescape: if there are no escape sequences, this is cheap; if there are, it is a
|
||
|
// bit more expensive, but causes no allocations unless len(key) > unescapeStackBufSize
|
||
|
var keyUnesc []byte
|
||
|
if !keyEscaped {
|
||
|
keyUnesc = key
|
||
|
} else {
|
||
|
var stackbuf [unescapeStackBufSize]byte
|
||
|
if ku, err := Unescape(key, stackbuf[:]); err != nil {
|
||
|
return -1
|
||
|
} else {
|
||
|
keyUnesc = ku
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if maxPath >= level {
|
||
|
if level < 1 {
|
||
|
cb(-1, nil, Unknown, MalformedJsonError)
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
pathsBuf[level-1] = bytesToString(&keyUnesc)
|
||
|
for pi, p := range paths {
|
||
|
if len(p) != level || pathFlags[pi] || !equalStr(&keyUnesc, p[level-1]) || !sameTree(p, pathsBuf[:level]) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
match = pi
|
||
|
|
||
|
pathsMatched++
|
||
|
pathFlags[pi] = true
|
||
|
|
||
|
v, dt, _, e := Get(data[i+1:])
|
||
|
cb(pi, v, dt, e)
|
||
|
|
||
|
if pathsMatched == len(paths) {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
if pathsMatched == len(paths) {
|
||
|
return i
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if match == -1 {
|
||
|
tokenOffset := nextToken(data[i+1:])
|
||
|
i += tokenOffset
|
||
|
|
||
|
if data[i] == '{' {
|
||
|
blockSkip := blockEnd(data[i:], '{', '}')
|
||
|
i += blockSkip + 1
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if i < ln {
|
||
|
switch data[i] {
|
||
|
case '{', '}', '[', '"':
|
||
|
i--
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
i--
|
||
|
}
|
||
|
case '{':
|
||
|
level++
|
||
|
case '}':
|
||
|
level--
|
||
|
case '[':
|
||
|
var ok bool
|
||
|
arrIdxFlags := make(map[int]struct{})
|
||
|
pIdxFlags := make([]bool, len(paths))
|
||
|
|
||
|
if level < 0 {
|
||
|
cb(-1, nil, Unknown, MalformedJsonError)
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
for pi, p := range paths {
|
||
|
if len(p) < level+1 || pathFlags[pi] || p[level][0] != '[' || !sameTree(p, pathsBuf[:level]) {
|
||
|
continue
|
||
|
}
|
||
|
if len(p[level]) >= 2 {
|
||
|
aIdx, _ := strconv.Atoi(p[level][1 : len(p[level])-1])
|
||
|
arrIdxFlags[aIdx] = x
|
||
|
pIdxFlags[pi] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if len(arrIdxFlags) > 0 {
|
||
|
level++
|
||
|
|
||
|
var curIdx int
|
||
|
arrOff, _ := ArrayEach(data[i:], func(value []byte, dataType ValueType, offset int, err error) {
|
||
|
if _, ok = arrIdxFlags[curIdx]; ok {
|
||
|
for pi, p := range paths {
|
||
|
if pIdxFlags[pi] {
|
||
|
aIdx, _ := strconv.Atoi(p[level-1][1 : len(p[level-1])-1])
|
||
|
|
||
|
if curIdx == aIdx {
|
||
|
of := searchKeys(value, p[level:]...)
|
||
|
|
||
|
pathsMatched++
|
||
|
pathFlags[pi] = true
|
||
|
|
||
|
if of != -1 {
|
||
|
v, dt, _, e := Get(value[of:])
|
||
|
cb(pi, v, dt, e)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
curIdx += 1
|
||
|
})
|
||
|
|
||
|
if pathsMatched == len(paths) {
|
||
|
return i
|
||
|
}
|
||
|
|
||
|
i += arrOff - 1
|
||
|
} else {
|
||
|
// Do not search for keys inside arrays
|
||
|
if arraySkip := blockEnd(data[i:], '[', ']'); arraySkip == -1 {
|
||
|
return -1
|
||
|
} else {
|
||
|
i += arraySkip - 1
|
||
|
}
|
||
|
}
|
||
|
case ']':
|
||
|
level--
|
||
|
}
|
||
|
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
return -1
|
||
|
}
|
||
|
|
||
|
// Data types available in valid JSON data.
|
||
|
type ValueType int
|
||
|
|
||
|
const (
|
||
|
NotExist = ValueType(iota)
|
||
|
String
|
||
|
Number
|
||
|
Object
|
||
|
Array
|
||
|
Boolean
|
||
|
Null
|
||
|
Unknown
|
||
|
)
|
||
|
|
||
|
func (vt ValueType) String() string {
|
||
|
switch vt {
|
||
|
case NotExist:
|
||
|
return "non-existent"
|
||
|
case String:
|
||
|
return "string"
|
||
|
case Number:
|
||
|
return "number"
|
||
|
case Object:
|
||
|
return "object"
|
||
|
case Array:
|
||
|
return "array"
|
||
|
case Boolean:
|
||
|
return "boolean"
|
||
|
case Null:
|
||
|
return "null"
|
||
|
default:
|
||
|
return "unknown"
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
trueLiteral = []byte("true")
|
||
|
falseLiteral = []byte("false")
|
||
|
nullLiteral = []byte("null")
|
||
|
)
|
||
|
|
||
|
func createInsertComponent(keys []string, setValue []byte, comma, object bool) []byte {
|
||
|
isIndex := string(keys[0][0]) == "["
|
||
|
offset := 0
|
||
|
lk := calcAllocateSpace(keys, setValue, comma, object)
|
||
|
buffer := make([]byte, lk, lk)
|
||
|
if comma {
|
||
|
offset += WriteToBuffer(buffer[offset:], ",")
|
||
|
}
|
||
|
if isIndex && !comma {
|
||
|
offset += WriteToBuffer(buffer[offset:], "[")
|
||
|
} else {
|
||
|
if object {
|
||
|
offset += WriteToBuffer(buffer[offset:], "{")
|
||
|
}
|
||
|
if !isIndex {
|
||
|
offset += WriteToBuffer(buffer[offset:], "\"")
|
||
|
offset += WriteToBuffer(buffer[offset:], keys[0])
|
||
|
offset += WriteToBuffer(buffer[offset:], "\":")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i := 1; i < len(keys); i++ {
|
||
|
if string(keys[i][0]) == "[" {
|
||
|
offset += WriteToBuffer(buffer[offset:], "[")
|
||
|
} else {
|
||
|
offset += WriteToBuffer(buffer[offset:], "{\"")
|
||
|
offset += WriteToBuffer(buffer[offset:], keys[i])
|
||
|
offset += WriteToBuffer(buffer[offset:], "\":")
|
||
|
}
|
||
|
}
|
||
|
offset += WriteToBuffer(buffer[offset:], string(setValue))
|
||
|
for i := len(keys) - 1; i > 0; i-- {
|
||
|
if string(keys[i][0]) == "[" {
|
||
|
offset += WriteToBuffer(buffer[offset:], "]")
|
||
|
} else {
|
||
|
offset += WriteToBuffer(buffer[offset:], "}")
|
||
|
}
|
||
|
}
|
||
|
if isIndex && !comma {
|
||
|
offset += WriteToBuffer(buffer[offset:], "]")
|
||
|
}
|
||
|
if object && !isIndex {
|
||
|
offset += WriteToBuffer(buffer[offset:], "}")
|
||
|
}
|
||
|
return buffer
|
||
|
}
|
||
|
|
||
|
func calcAllocateSpace(keys []string, setValue []byte, comma, object bool) int {
|
||
|
isIndex := string(keys[0][0]) == "["
|
||
|
lk := 0
|
||
|
if comma {
|
||
|
// ,
|
||
|
lk += 1
|
||
|
}
|
||
|
if isIndex && !comma {
|
||
|
// []
|
||
|
lk += 2
|
||
|
} else {
|
||
|
if object {
|
||
|
// {
|
||
|
lk += 1
|
||
|
}
|
||
|
if !isIndex {
|
||
|
// "keys[0]"
|
||
|
lk += len(keys[0]) + 3
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
lk += len(setValue)
|
||
|
for i := 1; i < len(keys); i++ {
|
||
|
if string(keys[i][0]) == "[" {
|
||
|
// []
|
||
|
lk += 2
|
||
|
} else {
|
||
|
// {"keys[i]":setValue}
|
||
|
lk += len(keys[i]) + 5
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if object && !isIndex {
|
||
|
// }
|
||
|
lk += 1
|
||
|
}
|
||
|
|
||
|
return lk
|
||
|
}
|
||
|
|
||
|
func WriteToBuffer(buffer []byte, str string) int {
|
||
|
copy(buffer, str)
|
||
|
return len(str)
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
|
||
|
Del - Receives existing data structure, path to delete.
|
||
|
|
||
|
Returns:
|
||
|
`data` - return modified data
|
||
|
|
||
|
*/
|
||
|
func Delete(data []byte, keys ...string) []byte {
|
||
|
lk := len(keys)
|
||
|
if lk == 0 {
|
||
|
return data[:0]
|
||
|
}
|
||
|
|
||
|
array := false
|
||
|
if len(keys[lk-1]) > 0 && string(keys[lk-1][0]) == "[" {
|
||
|
array = true
|
||
|
}
|
||
|
|
||
|
var startOffset, keyOffset int
|
||
|
endOffset := len(data)
|
||
|
var err error
|
||
|
if !array {
|
||
|
if len(keys) > 1 {
|
||
|
_, _, startOffset, endOffset, err = internalGet(data, keys[:lk-1]...)
|
||
|
if err == KeyPathNotFoundError {
|
||
|
// problem parsing the data
|
||
|
return data
|
||
|
}
|
||
|
}
|
||
|
|
||
|
keyOffset, err = findKeyStart(data[startOffset:endOffset], keys[lk-1])
|
||
|
if err == KeyPathNotFoundError {
|
||
|
// problem parsing the data
|
||
|
return data
|
||
|
}
|
||
|
keyOffset += startOffset
|
||
|
_, _, _, subEndOffset, _ := internalGet(data[startOffset:endOffset], keys[lk-1])
|
||
|
endOffset = startOffset + subEndOffset
|
||
|
tokEnd := tokenEnd(data[endOffset:])
|
||
|
tokStart := findTokenStart(data[:keyOffset], ","[0])
|
||
|
|
||
|
if data[endOffset+tokEnd] == ","[0] {
|
||
|
endOffset += tokEnd + 1
|
||
|
} else if data[endOffset+tokEnd] == " "[0] && len(data) > endOffset+tokEnd+1 && data[endOffset+tokEnd+1] == ","[0] {
|
||
|
endOffset += tokEnd + 2
|
||
|
} else if data[endOffset+tokEnd] == "}"[0] && data[tokStart] == ","[0] {
|
||
|
keyOffset = tokStart
|
||
|
}
|
||
|
} else {
|
||
|
_, _, keyOffset, endOffset, err = internalGet(data, keys...)
|
||
|
if err == KeyPathNotFoundError {
|
||
|
// problem parsing the data
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
tokEnd := tokenEnd(data[endOffset:])
|
||
|
tokStart := findTokenStart(data[:keyOffset], ","[0])
|
||
|
|
||
|
if data[endOffset+tokEnd] == ","[0] {
|
||
|
endOffset += tokEnd + 1
|
||
|
} else if data[endOffset+tokEnd] == "]"[0] && data[tokStart] == ","[0] {
|
||
|
keyOffset = tokStart
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// We need to remove remaining trailing comma if we delete las element in the object
|
||
|
prevTok := lastToken(data[:keyOffset])
|
||
|
remainedValue := data[endOffset:]
|
||
|
|
||
|
var newOffset int
|
||
|
if nextToken(remainedValue) > -1 && remainedValue[nextToken(remainedValue)] == '}' && data[prevTok] == ',' {
|
||
|
newOffset = prevTok
|
||
|
} else {
|
||
|
newOffset = prevTok + 1
|
||
|
}
|
||
|
|
||
|
// We have to make a copy here if we don't want to mangle the original data, because byte slices are
|
||
|
// accessed by reference and not by value
|
||
|
dataCopy := make([]byte, len(data))
|
||
|
copy(dataCopy, data)
|
||
|
data = append(dataCopy[:newOffset], dataCopy[endOffset:]...)
|
||
|
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
|
||
|
Set - Receives existing data structure, path to set, and data to set at that key.
|
||
|
|
||
|
Returns:
|
||
|
`value` - modified byte array
|
||
|
`err` - On any parsing error
|
||
|
|
||
|
*/
|
||
|
func Set(data []byte, setValue []byte, keys ...string) (value []byte, err error) {
|
||
|
// ensure keys are set
|
||
|
if len(keys) == 0 {
|
||
|
return nil, KeyPathNotFoundError
|
||
|
}
|
||
|
|
||
|
_, _, startOffset, endOffset, err := internalGet(data, keys...)
|
||
|
if err != nil {
|
||
|
if err != KeyPathNotFoundError {
|
||
|
// problem parsing the data
|
||
|
return nil, err
|
||
|
}
|
||
|
// full path doesnt exist
|
||
|
// does any subpath exist?
|
||
|
var depth int
|
||
|
for i := range keys {
|
||
|
_, _, start, end, sErr := internalGet(data, keys[:i+1]...)
|
||
|
if sErr != nil {
|
||
|
break
|
||
|
} else {
|
||
|
endOffset = end
|
||
|
startOffset = start
|
||
|
depth++
|
||
|
}
|
||
|
}
|
||
|
comma := true
|
||
|
object := false
|
||
|
if endOffset == -1 {
|
||
|
firstToken := nextToken(data)
|
||
|
// We can't set a top-level key if data isn't an object
|
||
|
if firstToken < 0 || data[firstToken] != '{' {
|
||
|
return nil, KeyPathNotFoundError
|
||
|
}
|
||
|
// Don't need a comma if the input is an empty object
|
||
|
secondToken := firstToken + 1 + nextToken(data[firstToken+1:])
|
||
|
if data[secondToken] == '}' {
|
||
|
comma = false
|
||
|
}
|
||
|
// Set the top level key at the end (accounting for any trailing whitespace)
|
||
|
// This assumes last token is valid like '}', could check and return error
|
||
|
endOffset = lastToken(data)
|
||
|
}
|
||
|
depthOffset := endOffset
|
||
|
if depth != 0 {
|
||
|
// if subpath is a non-empty object, add to it
|
||
|
// or if subpath is a non-empty array, add to it
|
||
|
if (data[startOffset] == '{' && data[startOffset+1+nextToken(data[startOffset+1:])] != '}') ||
|
||
|
(data[startOffset] == '[' && data[startOffset+1+nextToken(data[startOffset+1:])] == '{') && keys[depth:][0][0] == 91 {
|
||
|
depthOffset--
|
||
|
startOffset = depthOffset
|
||
|
// otherwise, over-write it with a new object
|
||
|
} else {
|
||
|
comma = false
|
||
|
object = true
|
||
|
}
|
||
|
} else {
|
||
|
startOffset = depthOffset
|
||
|
}
|
||
|
value = append(data[:startOffset], append(createInsertComponent(keys[depth:], setValue, comma, object), data[depthOffset:]...)...)
|
||
|
} else {
|
||
|
// path currently exists
|
||
|
startComponent := data[:startOffset]
|
||
|
endComponent := data[endOffset:]
|
||
|
|
||
|
value = make([]byte, len(startComponent)+len(endComponent)+len(setValue))
|
||
|
newEndOffset := startOffset + len(setValue)
|
||
|
copy(value[0:startOffset], startComponent)
|
||
|
copy(value[startOffset:newEndOffset], setValue)
|
||
|
copy(value[newEndOffset:], endComponent)
|
||
|
}
|
||
|
return value, nil
|
||
|
}
|
||
|
|
||
|
func getType(data []byte, offset int) ([]byte, ValueType, int, error) {
|
||
|
var dataType ValueType
|
||
|
endOffset := offset
|
||
|
|
||
|
// if string value
|
||
|
if data[offset] == '"' {
|
||
|
dataType = String
|
||
|
if idx, _ := stringEnd(data[offset+1:]); idx != -1 {
|
||
|
endOffset += idx + 1
|
||
|
} else {
|
||
|
return nil, dataType, offset, MalformedStringError
|
||
|
}
|
||
|
} else if data[offset] == '[' { // if array value
|
||
|
dataType = Array
|
||
|
// break label, for stopping nested loops
|
||
|
endOffset = blockEnd(data[offset:], '[', ']')
|
||
|
|
||
|
if endOffset == -1 {
|
||
|
return nil, dataType, offset, MalformedArrayError
|
||
|
}
|
||
|
|
||
|
endOffset += offset
|
||
|
} else if data[offset] == '{' { // if object value
|
||
|
dataType = Object
|
||
|
// break label, for stopping nested loops
|
||
|
endOffset = blockEnd(data[offset:], '{', '}')
|
||
|
|
||
|
if endOffset == -1 {
|
||
|
return nil, dataType, offset, MalformedObjectError
|
||
|
}
|
||
|
|
||
|
endOffset += offset
|
||
|
} else {
|
||
|
// Number, Boolean or None
|
||
|
end := tokenEnd(data[endOffset:])
|
||
|
|
||
|
if end == -1 {
|
||
|
return nil, dataType, offset, MalformedValueError
|
||
|
}
|
||
|
|
||
|
value := data[offset : endOffset+end]
|
||
|
|
||
|
switch data[offset] {
|
||
|
case 't', 'f': // true or false
|
||
|
if bytes.Equal(value, trueLiteral) || bytes.Equal(value, falseLiteral) {
|
||
|
dataType = Boolean
|
||
|
} else {
|
||
|
return nil, Unknown, offset, UnknownValueTypeError
|
||
|
}
|
||
|
case 'u', 'n': // undefined or null
|
||
|
if bytes.Equal(value, nullLiteral) {
|
||
|
dataType = Null
|
||
|
} else {
|
||
|
return nil, Unknown, offset, UnknownValueTypeError
|
||
|
}
|
||
|
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-':
|
||
|
dataType = Number
|
||
|
default:
|
||
|
return nil, Unknown, offset, UnknownValueTypeError
|
||
|
}
|
||
|
|
||
|
endOffset += end
|
||
|
}
|
||
|
return data[offset:endOffset], dataType, endOffset, nil
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
Get - Receives data structure, and key path to extract value from.
|
||
|
|
||
|
Returns:
|
||
|
`value` - Pointer to original data structure containing key value, or just empty slice if nothing found or error
|
||
|
`dataType` - Can be: `NotExist`, `String`, `Number`, `Object`, `Array`, `Boolean` or `Null`
|
||
|
`offset` - Offset from provided data structure where key value ends. Used mostly internally, for example for `ArrayEach` helper.
|
||
|
`err` - If key not found or any other parsing issue it should return error. If key not found it also sets `dataType` to `NotExist`
|
||
|
|
||
|
Accept multiple keys to specify path to JSON value (in case of quering nested structures).
|
||
|
If no keys provided it will try to extract closest JSON value (simple ones or object/array), useful for reading streams or arrays, see `ArrayEach` implementation.
|
||
|
*/
|
||
|
func Get(data []byte, keys ...string) (value []byte, dataType ValueType, offset int, err error) {
|
||
|
a, b, _, d, e := internalGet(data, keys...)
|
||
|
return a, b, d, e
|
||
|
}
|
||
|
|
||
|
func internalGet(data []byte, keys ...string) (value []byte, dataType ValueType, offset, endOffset int, err error) {
|
||
|
if len(keys) > 0 {
|
||
|
if offset = searchKeys(data, keys...); offset == -1 {
|
||
|
return nil, NotExist, -1, -1, KeyPathNotFoundError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Go to closest value
|
||
|
nO := nextToken(data[offset:])
|
||
|
if nO == -1 {
|
||
|
return nil, NotExist, offset, -1, MalformedJsonError
|
||
|
}
|
||
|
|
||
|
offset += nO
|
||
|
value, dataType, endOffset, err = getType(data, offset)
|
||
|
if err != nil {
|
||
|
return value, dataType, offset, endOffset, err
|
||
|
}
|
||
|
|
||
|
// Strip quotes from string values
|
||
|
if dataType == String {
|
||
|
value = value[1 : len(value)-1]
|
||
|
}
|
||
|
|
||
|
return value[:len(value):len(value)], dataType, offset, endOffset, nil
|
||
|
}
|
||
|
|
||
|
// ArrayEach is used when iterating arrays, accepts a callback function with the same return arguments as `Get`.
|
||
|
func ArrayEach(data []byte, cb func(value []byte, dataType ValueType, offset int, err error), keys ...string) (offset int, err error) {
|
||
|
if len(data) == 0 {
|
||
|
return -1, MalformedObjectError
|
||
|
}
|
||
|
|
||
|
nT := nextToken(data)
|
||
|
if nT == -1 {
|
||
|
return -1, MalformedJsonError
|
||
|
}
|
||
|
|
||
|
offset = nT + 1
|
||
|
|
||
|
if len(keys) > 0 {
|
||
|
if offset = searchKeys(data, keys...); offset == -1 {
|
||
|
return offset, KeyPathNotFoundError
|
||
|
}
|
||
|
|
||
|
// Go to closest value
|
||
|
nO := nextToken(data[offset:])
|
||
|
if nO == -1 {
|
||
|
return offset, MalformedJsonError
|
||
|
}
|
||
|
|
||
|
offset += nO
|
||
|
|
||
|
if data[offset] != '[' {
|
||
|
return offset, MalformedArrayError
|
||
|
}
|
||
|
|
||
|
offset++
|
||
|
}
|
||
|
|
||
|
nO := nextToken(data[offset:])
|
||
|
if nO == -1 {
|
||
|
return offset, MalformedJsonError
|
||
|
}
|
||
|
|
||
|
offset += nO
|
||
|
|
||
|
if data[offset] == ']' {
|
||
|
return offset, nil
|
||
|
}
|
||
|
|
||
|
for true {
|
||
|
v, t, o, e := Get(data[offset:])
|
||
|
|
||
|
if e != nil {
|
||
|
return offset, e
|
||
|
}
|
||
|
|
||
|
if o == 0 {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if t != NotExist {
|
||
|
cb(v, t, offset+o-len(v), e)
|
||
|
}
|
||
|
|
||
|
if e != nil {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
offset += o
|
||
|
|
||
|
skipToToken := nextToken(data[offset:])
|
||
|
if skipToToken == -1 {
|
||
|
return offset, MalformedArrayError
|
||
|
}
|
||
|
offset += skipToToken
|
||
|
|
||
|
if data[offset] == ']' {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
if data[offset] != ',' {
|
||
|
return offset, MalformedArrayError
|
||
|
}
|
||
|
|
||
|
offset++
|
||
|
}
|
||
|
|
||
|
return offset, nil
|
||
|
}
|
||
|
|
||
|
// ObjectEach iterates over the key-value pairs of a JSON object, invoking a given callback for each such entry
|
||
|
func ObjectEach(data []byte, callback func(key []byte, value []byte, dataType ValueType, offset int) error, keys ...string) (err error) {
|
||
|
offset := 0
|
||
|
|
||
|
// Descend to the desired key, if requested
|
||
|
if len(keys) > 0 {
|
||
|
if off := searchKeys(data, keys...); off == -1 {
|
||
|
return KeyPathNotFoundError
|
||
|
} else {
|
||
|
offset = off
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Validate and skip past opening brace
|
||
|
if off := nextToken(data[offset:]); off == -1 {
|
||
|
return MalformedObjectError
|
||
|
} else if offset += off; data[offset] != '{' {
|
||
|
return MalformedObjectError
|
||
|
} else {
|
||
|
offset++
|
||
|
}
|
||
|
|
||
|
// Skip to the first token inside the object, or stop if we find the ending brace
|
||
|
if off := nextToken(data[offset:]); off == -1 {
|
||
|
return MalformedJsonError
|
||
|
} else if offset += off; data[offset] == '}' {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Loop pre-condition: data[offset] points to what should be either the next entry's key, or the closing brace (if it's anything else, the JSON is malformed)
|
||
|
for offset < len(data) {
|
||
|
// Step 1: find the next key
|
||
|
var key []byte
|
||
|
|
||
|
// Check what the the next token is: start of string, end of object, or something else (error)
|
||
|
switch data[offset] {
|
||
|
case '"':
|
||
|
offset++ // accept as string and skip opening quote
|
||
|
case '}':
|
||
|
return nil // we found the end of the object; stop and return success
|
||
|
default:
|
||
|
return MalformedObjectError
|
||
|
}
|
||
|
|
||
|
// Find the end of the key string
|
||
|
var keyEscaped bool
|
||
|
if off, esc := stringEnd(data[offset:]); off == -1 {
|
||
|
return MalformedJsonError
|
||
|
} else {
|
||
|
key, keyEscaped = data[offset:offset+off-1], esc
|
||
|
offset += off
|
||
|
}
|
||
|
|
||
|
// Unescape the string if needed
|
||
|
if keyEscaped {
|
||
|
var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
|
||
|
if keyUnescaped, err := Unescape(key, stackbuf[:]); err != nil {
|
||
|
return MalformedStringEscapeError
|
||
|
} else {
|
||
|
key = keyUnescaped
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Step 2: skip the colon
|
||
|
if off := nextToken(data[offset:]); off == -1 {
|
||
|
return MalformedJsonError
|
||
|
} else if offset += off; data[offset] != ':' {
|
||
|
return MalformedJsonError
|
||
|
} else {
|
||
|
offset++
|
||
|
}
|
||
|
|
||
|
// Step 3: find the associated value, then invoke the callback
|
||
|
if value, valueType, off, err := Get(data[offset:]); err != nil {
|
||
|
return err
|
||
|
} else if err := callback(key, value, valueType, offset+off); err != nil { // Invoke the callback here!
|
||
|
return err
|
||
|
} else {
|
||
|
offset += off
|
||
|
}
|
||
|
|
||
|
// Step 4: skip over the next comma to the following token, or stop if we hit the ending brace
|
||
|
if off := nextToken(data[offset:]); off == -1 {
|
||
|
return MalformedArrayError
|
||
|
} else {
|
||
|
offset += off
|
||
|
switch data[offset] {
|
||
|
case '}':
|
||
|
return nil // Stop if we hit the close brace
|
||
|
case ',':
|
||
|
offset++ // Ignore the comma
|
||
|
default:
|
||
|
return MalformedObjectError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Skip to the next token after the comma
|
||
|
if off := nextToken(data[offset:]); off == -1 {
|
||
|
return MalformedArrayError
|
||
|
} else {
|
||
|
offset += off
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return MalformedObjectError // we shouldn't get here; it's expected that we will return via finding the ending brace
|
||
|
}
|
||
|
|
||
|
// GetUnsafeString returns the value retrieved by `Get`, use creates string without memory allocation by mapping string to slice memory. It does not handle escape symbols.
|
||
|
func GetUnsafeString(data []byte, keys ...string) (val string, err error) {
|
||
|
v, _, _, e := Get(data, keys...)
|
||
|
|
||
|
if e != nil {
|
||
|
return "", e
|
||
|
}
|
||
|
|
||
|
return bytesToString(&v), nil
|
||
|
}
|
||
|
|
||
|
// GetString returns the value retrieved by `Get`, cast to a string if possible, trying to properly handle escape and utf8 symbols
|
||
|
// If key data type do not match, it will return an error.
|
||
|
func GetString(data []byte, keys ...string) (val string, err error) {
|
||
|
v, t, _, e := Get(data, keys...)
|
||
|
|
||
|
if e != nil {
|
||
|
return "", e
|
||
|
}
|
||
|
|
||
|
if t != String {
|
||
|
return "", fmt.Errorf("Value is not a string: %s", string(v))
|
||
|
}
|
||
|
|
||
|
// If no escapes return raw content
|
||
|
if bytes.IndexByte(v, '\\') == -1 {
|
||
|
return string(v), nil
|
||
|
}
|
||
|
|
||
|
return ParseString(v)
|
||
|
}
|
||
|
|
||
|
// GetFloat returns the value retrieved by `Get`, cast to a float64 if possible.
|
||
|
// The offset is the same as in `Get`.
|
||
|
// If key data type do not match, it will return an error.
|
||
|
func GetFloat(data []byte, keys ...string) (val float64, err error) {
|
||
|
v, t, _, e := Get(data, keys...)
|
||
|
|
||
|
if e != nil {
|
||
|
return 0, e
|
||
|
}
|
||
|
|
||
|
if t != Number {
|
||
|
return 0, fmt.Errorf("Value is not a number: %s", string(v))
|
||
|
}
|
||
|
|
||
|
return ParseFloat(v)
|
||
|
}
|
||
|
|
||
|
// GetInt returns the value retrieved by `Get`, cast to a int64 if possible.
|
||
|
// If key data type do not match, it will return an error.
|
||
|
func GetInt(data []byte, keys ...string) (val int64, err error) {
|
||
|
v, t, _, e := Get(data, keys...)
|
||
|
|
||
|
if e != nil {
|
||
|
return 0, e
|
||
|
}
|
||
|
|
||
|
if t != Number {
|
||
|
return 0, fmt.Errorf("Value is not a number: %s", string(v))
|
||
|
}
|
||
|
|
||
|
return ParseInt(v)
|
||
|
}
|
||
|
|
||
|
// GetBoolean returns the value retrieved by `Get`, cast to a bool if possible.
|
||
|
// The offset is the same as in `Get`.
|
||
|
// If key data type do not match, it will return error.
|
||
|
func GetBoolean(data []byte, keys ...string) (val bool, err error) {
|
||
|
v, t, _, e := Get(data, keys...)
|
||
|
|
||
|
if e != nil {
|
||
|
return false, e
|
||
|
}
|
||
|
|
||
|
if t != Boolean {
|
||
|
return false, fmt.Errorf("Value is not a boolean: %s", string(v))
|
||
|
}
|
||
|
|
||
|
return ParseBoolean(v)
|
||
|
}
|
||
|
|
||
|
// ParseBoolean parses a Boolean ValueType into a Go bool (not particularly useful, but here for completeness)
|
||
|
func ParseBoolean(b []byte) (bool, error) {
|
||
|
switch {
|
||
|
case bytes.Equal(b, trueLiteral):
|
||
|
return true, nil
|
||
|
case bytes.Equal(b, falseLiteral):
|
||
|
return false, nil
|
||
|
default:
|
||
|
return false, MalformedValueError
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ParseString parses a String ValueType into a Go string (the main parsing work is unescaping the JSON string)
|
||
|
func ParseString(b []byte) (string, error) {
|
||
|
var stackbuf [unescapeStackBufSize]byte // stack-allocated array for allocation-free unescaping of small strings
|
||
|
if bU, err := Unescape(b, stackbuf[:]); err != nil {
|
||
|
return "", MalformedValueError
|
||
|
} else {
|
||
|
return string(bU), nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ParseNumber parses a Number ValueType into a Go float64
|
||
|
func ParseFloat(b []byte) (float64, error) {
|
||
|
if v, err := parseFloat(&b); err != nil {
|
||
|
return 0, MalformedValueError
|
||
|
} else {
|
||
|
return v, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ParseInt parses a Number ValueType into a Go int64
|
||
|
func ParseInt(b []byte) (int64, error) {
|
||
|
if v, ok, overflow := parseInt(b); !ok {
|
||
|
if overflow {
|
||
|
return 0, OverflowIntegerError
|
||
|
}
|
||
|
return 0, MalformedValueError
|
||
|
} else {
|
||
|
return v, nil
|
||
|
}
|
||
|
}
|