summary refs log tree commit diff
path: root/vendor/github.com/tidwall/sjson/sjson.go
diff options
context:
space:
mode:
authorEmile <git@emile.space>2024-10-25 15:55:50 +0200
committerEmile <git@emile.space>2024-10-25 15:55:50 +0200
commitc90f36e3dd179d2de96f4f5fe38d8dc9a9de6dfe (patch)
tree89e9afb41c5bf76f48cfb09305a2d3db8d302b06 /vendor/github.com/tidwall/sjson/sjson.go
parent98bbb0f559a8883bc47bae80607dbe326a448e61 (diff)
vendor HEAD main
Diffstat (limited to 'vendor/github.com/tidwall/sjson/sjson.go')
-rw-r--r--vendor/github.com/tidwall/sjson/sjson.go737
1 files changed, 737 insertions, 0 deletions
diff --git a/vendor/github.com/tidwall/sjson/sjson.go b/vendor/github.com/tidwall/sjson/sjson.go
new file mode 100644
index 0000000..a55eef3
--- /dev/null
+++ b/vendor/github.com/tidwall/sjson/sjson.go
@@ -0,0 +1,737 @@
+// Package sjson provides setting json values.
+package sjson
+
+import (
+	jsongo "encoding/json"
+	"sort"
+	"strconv"
+	"unsafe"
+
+	"github.com/tidwall/gjson"
+)
+
+type errorType struct {
+	msg string
+}
+
+func (err *errorType) Error() string {
+	return err.msg
+}
+
+// Options represents additional options for the Set and Delete functions.
+type Options struct {
+	// Optimistic is a hint that the value likely exists which
+	// allows for the sjson to perform a fast-track search and replace.
+	Optimistic bool
+	// ReplaceInPlace is a hint to replace the input json rather than
+	// allocate a new json byte slice. When this field is specified
+	// the input json will not longer be valid and it should not be used
+	// In the case when the destination slice doesn't have enough free
+	// bytes to replace the data in place, a new bytes slice will be
+	// created under the hood.
+	// The Optimistic flag must be set to true and the input must be a
+	// byte slice in order to use this field.
+	ReplaceInPlace bool
+}
+
+type pathResult struct {
+	part  string // current key part
+	gpart string // gjson get part
+	path  string // remaining path
+	force bool   // force a string key
+	more  bool   // there is more path to parse
+}
+
+func isSimpleChar(ch byte) bool {
+	switch ch {
+	case '|', '#', '@', '*', '?':
+		return false
+	default:
+		return true
+	}
+}
+
+func parsePath(path string) (res pathResult, simple bool) {
+	var r pathResult
+	if len(path) > 0 && path[0] == ':' {
+		r.force = true
+		path = path[1:]
+	}
+	for i := 0; i < len(path); i++ {
+		if path[i] == '.' {
+			r.part = path[:i]
+			r.gpart = path[:i]
+			r.path = path[i+1:]
+			r.more = true
+			return r, true
+		}
+		if !isSimpleChar(path[i]) {
+			return r, false
+		}
+		if path[i] == '\\' {
+			// go into escape mode. this is a slower path that
+			// strips off the escape character from the part.
+			epart := []byte(path[:i])
+			gpart := []byte(path[:i+1])
+			i++
+			if i < len(path) {
+				epart = append(epart, path[i])
+				gpart = append(gpart, path[i])
+				i++
+				for ; i < len(path); i++ {
+					if path[i] == '\\' {
+						gpart = append(gpart, '\\')
+						i++
+						if i < len(path) {
+							epart = append(epart, path[i])
+							gpart = append(gpart, path[i])
+						}
+						continue
+					} else if path[i] == '.' {
+						r.part = string(epart)
+						r.gpart = string(gpart)
+						r.path = path[i+1:]
+						r.more = true
+						return r, true
+					} else if !isSimpleChar(path[i]) {
+						return r, false
+					}
+					epart = append(epart, path[i])
+					gpart = append(gpart, path[i])
+				}
+			}
+			// append the last part
+			r.part = string(epart)
+			r.gpart = string(gpart)
+			return r, true
+		}
+	}
+	r.part = path
+	r.gpart = path
+	return r, true
+}
+
+func mustMarshalString(s string) bool {
+	for i := 0; i < len(s); i++ {
+		if s[i] < ' ' || s[i] > 0x7f || s[i] == '"' || s[i] == '\\' {
+			return true
+		}
+	}
+	return false
+}
+
+// appendStringify makes a json string and appends to buf.
+func appendStringify(buf []byte, s string) []byte {
+	if mustMarshalString(s) {
+		b, _ := jsongo.Marshal(s)
+		return append(buf, b...)
+	}
+	buf = append(buf, '"')
+	buf = append(buf, s...)
+	buf = append(buf, '"')
+	return buf
+}
+
+// appendBuild builds a json block from a json path.
+func appendBuild(buf []byte, array bool, paths []pathResult, raw string,
+	stringify bool) []byte {
+	if !array {
+		buf = appendStringify(buf, paths[0].part)
+		buf = append(buf, ':')
+	}
+	if len(paths) > 1 {
+		n, numeric := atoui(paths[1])
+		if numeric || (!paths[1].force && paths[1].part == "-1") {
+			buf = append(buf, '[')
+			buf = appendRepeat(buf, "null,", n)
+			buf = appendBuild(buf, true, paths[1:], raw, stringify)
+			buf = append(buf, ']')
+		} else {
+			buf = append(buf, '{')
+			buf = appendBuild(buf, false, paths[1:], raw, stringify)
+			buf = append(buf, '}')
+		}
+	} else {
+		if stringify {
+			buf = appendStringify(buf, raw)
+		} else {
+			buf = append(buf, raw...)
+		}
+	}
+	return buf
+}
+
+// atoui does a rip conversion of string -> unigned int.
+func atoui(r pathResult) (n int, ok bool) {
+	if r.force {
+		return 0, false
+	}
+	for i := 0; i < len(r.part); i++ {
+		if r.part[i] < '0' || r.part[i] > '9' {
+			return 0, false
+		}
+		n = n*10 + int(r.part[i]-'0')
+	}
+	return n, true
+}
+
+// appendRepeat repeats string "n" times and appends to buf.
+func appendRepeat(buf []byte, s string, n int) []byte {
+	for i := 0; i < n; i++ {
+		buf = append(buf, s...)
+	}
+	return buf
+}
+
+// trim does a rip trim
+func trim(s string) string {
+	for len(s) > 0 {
+		if s[0] <= ' ' {
+			s = s[1:]
+			continue
+		}
+		break
+	}
+	for len(s) > 0 {
+		if s[len(s)-1] <= ' ' {
+			s = s[:len(s)-1]
+			continue
+		}
+		break
+	}
+	return s
+}
+
+// deleteTailItem deletes the previous key or comma.
+func deleteTailItem(buf []byte) ([]byte, bool) {
+loop:
+	for i := len(buf) - 1; i >= 0; i-- {
+		// look for either a ',',':','['
+		switch buf[i] {
+		case '[':
+			return buf, true
+		case ',':
+			return buf[:i], false
+		case ':':
+			// delete tail string
+			i--
+			for ; i >= 0; i-- {
+				if buf[i] == '"' {
+					i--
+					for ; i >= 0; i-- {
+						if buf[i] == '"' {
+							i--
+							if i >= 0 && buf[i] == '\\' {
+								i--
+								continue
+							}
+							for ; i >= 0; i-- {
+								// look for either a ',','{'
+								switch buf[i] {
+								case '{':
+									return buf[:i+1], true
+								case ',':
+									return buf[:i], false
+								}
+							}
+						}
+					}
+					break
+				}
+			}
+			break loop
+		}
+	}
+	return buf, false
+}
+
+var errNoChange = &errorType{"no change"}
+
+func appendRawPaths(buf []byte, jstr string, paths []pathResult, raw string,
+	stringify, del bool) ([]byte, error) {
+	var err error
+	var res gjson.Result
+	var found bool
+	if del {
+		if paths[0].part == "-1" && !paths[0].force {
+			res = gjson.Get(jstr, "#")
+			if res.Int() > 0 {
+				res = gjson.Get(jstr, strconv.FormatInt(int64(res.Int()-1), 10))
+				found = true
+			}
+		}
+	}
+	if !found {
+		res = gjson.Get(jstr, paths[0].gpart)
+	}
+	if res.Index > 0 {
+		if len(paths) > 1 {
+			buf = append(buf, jstr[:res.Index]...)
+			buf, err = appendRawPaths(buf, res.Raw, paths[1:], raw,
+				stringify, del)
+			if err != nil {
+				return nil, err
+			}
+			buf = append(buf, jstr[res.Index+len(res.Raw):]...)
+			return buf, nil
+		}
+		buf = append(buf, jstr[:res.Index]...)
+		var exidx int // additional forward stripping
+		if del {
+			var delNextComma bool
+			buf, delNextComma = deleteTailItem(buf)
+			if delNextComma {
+				i, j := res.Index+len(res.Raw), 0
+				for ; i < len(jstr); i, j = i+1, j+1 {
+					if jstr[i] <= ' ' {
+						continue
+					}
+					if jstr[i] == ',' {
+						exidx = j + 1
+					}
+					break
+				}
+			}
+		} else {
+			if stringify {
+				buf = appendStringify(buf, raw)
+			} else {
+				buf = append(buf, raw...)
+			}
+		}
+		buf = append(buf, jstr[res.Index+len(res.Raw)+exidx:]...)
+		return buf, nil
+	}
+	if del {
+		return nil, errNoChange
+	}
+	n, numeric := atoui(paths[0])
+	isempty := true
+	for i := 0; i < len(jstr); i++ {
+		if jstr[i] > ' ' {
+			isempty = false
+			break
+		}
+	}
+	if isempty {
+		if numeric {
+			jstr = "[]"
+		} else {
+			jstr = "{}"
+		}
+	}
+	jsres := gjson.Parse(jstr)
+	if jsres.Type != gjson.JSON {
+		if numeric {
+			jstr = "[]"
+		} else {
+			jstr = "{}"
+		}
+		jsres = gjson.Parse(jstr)
+	}
+	var comma bool
+	for i := 1; i < len(jsres.Raw); i++ {
+		if jsres.Raw[i] <= ' ' {
+			continue
+		}
+		if jsres.Raw[i] == '}' || jsres.Raw[i] == ']' {
+			break
+		}
+		comma = true
+		break
+	}
+	switch jsres.Raw[0] {
+	default:
+		return nil, &errorType{"json must be an object or array"}
+	case '{':
+		end := len(jsres.Raw) - 1
+		for ; end > 0; end-- {
+			if jsres.Raw[end] == '}' {
+				break
+			}
+		}
+		buf = append(buf, jsres.Raw[:end]...)
+		if comma {
+			buf = append(buf, ',')
+		}
+		buf = appendBuild(buf, false, paths, raw, stringify)
+		buf = append(buf, '}')
+		return buf, nil
+	case '[':
+		var appendit bool
+		if !numeric {
+			if paths[0].part == "-1" && !paths[0].force {
+				appendit = true
+			} else {
+				return nil, &errorType{
+					"cannot set array element for non-numeric key '" +
+						paths[0].part + "'"}
+			}
+		}
+		if appendit {
+			njson := trim(jsres.Raw)
+			if njson[len(njson)-1] == ']' {
+				njson = njson[:len(njson)-1]
+			}
+			buf = append(buf, njson...)
+			if comma {
+				buf = append(buf, ',')
+			}
+
+			buf = appendBuild(buf, true, paths, raw, stringify)
+			buf = append(buf, ']')
+			return buf, nil
+		}
+		buf = append(buf, '[')
+		ress := jsres.Array()
+		for i := 0; i < len(ress); i++ {
+			if i > 0 {
+				buf = append(buf, ',')
+			}
+			buf = append(buf, ress[i].Raw...)
+		}
+		if len(ress) == 0 {
+			buf = appendRepeat(buf, "null,", n-len(ress))
+		} else {
+			buf = appendRepeat(buf, ",null", n-len(ress))
+			if comma {
+				buf = append(buf, ',')
+			}
+		}
+		buf = appendBuild(buf, true, paths, raw, stringify)
+		buf = append(buf, ']')
+		return buf, nil
+	}
+}
+
+func isOptimisticPath(path string) bool {
+	for i := 0; i < len(path); i++ {
+		if path[i] < '.' || path[i] > 'z' {
+			return false
+		}
+		if path[i] > '9' && path[i] < 'A' {
+			return false
+		}
+		if path[i] > 'z' {
+			return false
+		}
+	}
+	return true
+}
+
+// Set sets a json value for the specified path.
+// A path is in dot syntax, such as "name.last" or "age".
+// This function expects that the json is well-formed, and does not validate.
+// Invalid json will not panic, but it may return back unexpected results.
+// An error is returned if the path is not valid.
+//
+// A path is a series of keys separated by a dot.
+//
+//  {
+//    "name": {"first": "Tom", "last": "Anderson"},
+//    "age":37,
+//    "children": ["Sara","Alex","Jack"],
+//    "friends": [
+//      {"first": "James", "last": "Murphy"},
+//      {"first": "Roger", "last": "Craig"}
+//    ]
+//  }
+//  "name.last"          >> "Anderson"
+//  "age"                >> 37
+//  "children.1"         >> "Alex"
+//
+func Set(json, path string, value interface{}) (string, error) {
+	return SetOptions(json, path, value, nil)
+}
+
+// SetBytes sets a json value for the specified path.
+// If working with bytes, this method preferred over
+// Set(string(data), path, value)
+func SetBytes(json []byte, path string, value interface{}) ([]byte, error) {
+	return SetBytesOptions(json, path, value, nil)
+}
+
+// SetRaw sets a raw json value for the specified path.
+// This function works the same as Set except that the value is set as a
+// raw block of json. This allows for setting premarshalled json objects.
+func SetRaw(json, path, value string) (string, error) {
+	return SetRawOptions(json, path, value, nil)
+}
+
+// SetRawOptions sets a raw json value for the specified path with options.
+// This furnction works the same as SetOptions except that the value is set
+// as a raw block of json. This allows for setting premarshalled json objects.
+func SetRawOptions(json, path, value string, opts *Options) (string, error) {
+	var optimistic bool
+	if opts != nil {
+		optimistic = opts.Optimistic
+	}
+	res, err := set(json, path, value, false, false, optimistic, false)
+	if err == errNoChange {
+		return json, nil
+	}
+	return string(res), err
+}
+
+// SetRawBytes sets a raw json value for the specified path.
+// If working with bytes, this method preferred over
+// SetRaw(string(data), path, value)
+func SetRawBytes(json []byte, path string, value []byte) ([]byte, error) {
+	return SetRawBytesOptions(json, path, value, nil)
+}
+
+type dtype struct{}
+
+// Delete deletes a value from json for the specified path.
+func Delete(json, path string) (string, error) {
+	return Set(json, path, dtype{})
+}
+
+// DeleteBytes deletes a value from json for the specified path.
+func DeleteBytes(json []byte, path string) ([]byte, error) {
+	return SetBytes(json, path, dtype{})
+}
+
+type stringHeader struct {
+	data unsafe.Pointer
+	len  int
+}
+
+type sliceHeader struct {
+	data unsafe.Pointer
+	len  int
+	cap  int
+}
+
+func set(jstr, path, raw string,
+	stringify, del, optimistic, inplace bool) ([]byte, error) {
+	if path == "" {
+		return []byte(jstr), &errorType{"path cannot be empty"}
+	}
+	if !del && optimistic && isOptimisticPath(path) {
+		res := gjson.Get(jstr, path)
+		if res.Exists() && res.Index > 0 {
+			sz := len(jstr) - len(res.Raw) + len(raw)
+			if stringify {
+				sz += 2
+			}
+			if inplace && sz <= len(jstr) {
+				if !stringify || !mustMarshalString(raw) {
+					jsonh := *(*stringHeader)(unsafe.Pointer(&jstr))
+					jsonbh := sliceHeader{
+						data: jsonh.data, len: jsonh.len, cap: jsonh.len}
+					jbytes := *(*[]byte)(unsafe.Pointer(&jsonbh))
+					if stringify {
+						jbytes[res.Index] = '"'
+						copy(jbytes[res.Index+1:], []byte(raw))
+						jbytes[res.Index+1+len(raw)] = '"'
+						copy(jbytes[res.Index+1+len(raw)+1:],
+							jbytes[res.Index+len(res.Raw):])
+					} else {
+						copy(jbytes[res.Index:], []byte(raw))
+						copy(jbytes[res.Index+len(raw):],
+							jbytes[res.Index+len(res.Raw):])
+					}
+					return jbytes[:sz], nil
+				}
+				return []byte(jstr), nil
+			}
+			buf := make([]byte, 0, sz)
+			buf = append(buf, jstr[:res.Index]...)
+			if stringify {
+				buf = appendStringify(buf, raw)
+			} else {
+				buf = append(buf, raw...)
+			}
+			buf = append(buf, jstr[res.Index+len(res.Raw):]...)
+			return buf, nil
+		}
+	}
+	var paths []pathResult
+	r, simple := parsePath(path)
+	if simple {
+		paths = append(paths, r)
+		for r.more {
+			r, simple = parsePath(r.path)
+			if !simple {
+				break
+			}
+			paths = append(paths, r)
+		}
+	}
+	if !simple {
+		if del {
+			return []byte(jstr),
+				&errorType{"cannot delete value from a complex path"}
+		}
+		return setComplexPath(jstr, path, raw, stringify)
+	}
+	njson, err := appendRawPaths(nil, jstr, paths, raw, stringify, del)
+	if err != nil {
+		return []byte(jstr), err
+	}
+	return njson, nil
+}
+
+func setComplexPath(jstr, path, raw string, stringify bool) ([]byte, error) {
+	res := gjson.Get(jstr, path)
+	if !res.Exists() || !(res.Index != 0 || len(res.Indexes) != 0) {
+		return []byte(jstr), errNoChange
+	}
+	if res.Index != 0 {
+		njson := []byte(jstr[:res.Index])
+		if stringify {
+			njson = appendStringify(njson, raw)
+		} else {
+			njson = append(njson, raw...)
+		}
+		njson = append(njson, jstr[res.Index+len(res.Raw):]...)
+		jstr = string(njson)
+	}
+	if len(res.Indexes) > 0 {
+		type val struct {
+			index int
+			res   gjson.Result
+		}
+		vals := make([]val, 0, len(res.Indexes))
+		res.ForEach(func(_, vres gjson.Result) bool {
+			vals = append(vals, val{res: vres})
+			return true
+		})
+		if len(res.Indexes) != len(vals) {
+			return []byte(jstr), errNoChange
+		}
+		for i := 0; i < len(res.Indexes); i++ {
+			vals[i].index = res.Indexes[i]
+		}
+		sort.SliceStable(vals, func(i, j int) bool {
+			return vals[i].index > vals[j].index
+		})
+		for _, val := range vals {
+			vres := val.res
+			index := val.index
+			njson := []byte(jstr[:index])
+			if stringify {
+				njson = appendStringify(njson, raw)
+			} else {
+				njson = append(njson, raw...)
+			}
+			njson = append(njson, jstr[index+len(vres.Raw):]...)
+			jstr = string(njson)
+		}
+	}
+	return []byte(jstr), nil
+}
+
+// SetOptions sets a json value for the specified path with options.
+// A path is in dot syntax, such as "name.last" or "age".
+// This function expects that the json is well-formed, and does not validate.
+// Invalid json will not panic, but it may return back unexpected results.
+// An error is returned if the path is not valid.
+func SetOptions(json, path string, value interface{},
+	opts *Options) (string, error) {
+	if opts != nil {
+		if opts.ReplaceInPlace {
+			// it's not safe to replace bytes in-place for strings
+			// copy the Options and set options.ReplaceInPlace to false.
+			nopts := *opts
+			opts = &nopts
+			opts.ReplaceInPlace = false
+		}
+	}
+	jsonh := *(*stringHeader)(unsafe.Pointer(&json))
+	jsonbh := sliceHeader{data: jsonh.data, len: jsonh.len, cap: jsonh.len}
+	jsonb := *(*[]byte)(unsafe.Pointer(&jsonbh))
+	res, err := SetBytesOptions(jsonb, path, value, opts)
+	return string(res), err
+}
+
+// SetBytesOptions sets a json value for the specified path with options.
+// If working with bytes, this method preferred over
+// SetOptions(string(data), path, value)
+func SetBytesOptions(json []byte, path string, value interface{},
+	opts *Options) ([]byte, error) {
+	var optimistic, inplace bool
+	if opts != nil {
+		optimistic = opts.Optimistic
+		inplace = opts.ReplaceInPlace
+	}
+	jstr := *(*string)(unsafe.Pointer(&json))
+	var res []byte
+	var err error
+	switch v := value.(type) {
+	default:
+		b, merr := jsongo.Marshal(value)
+		if merr != nil {
+			return nil, merr
+		}
+		raw := *(*string)(unsafe.Pointer(&b))
+		res, err = set(jstr, path, raw, false, false, optimistic, inplace)
+	case dtype:
+		res, err = set(jstr, path, "", false, true, optimistic, inplace)
+	case string:
+		res, err = set(jstr, path, v, true, false, optimistic, inplace)
+	case []byte:
+		raw := *(*string)(unsafe.Pointer(&v))
+		res, err = set(jstr, path, raw, true, false, optimistic, inplace)
+	case bool:
+		if v {
+			res, err = set(jstr, path, "true", false, false, optimistic, inplace)
+		} else {
+			res, err = set(jstr, path, "false", false, false, optimistic, inplace)
+		}
+	case int8:
+		res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
+			false, false, optimistic, inplace)
+	case int16:
+		res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
+			false, false, optimistic, inplace)
+	case int32:
+		res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
+			false, false, optimistic, inplace)
+	case int64:
+		res, err = set(jstr, path, strconv.FormatInt(int64(v), 10),
+			false, false, optimistic, inplace)
+	case uint8:
+		res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
+			false, false, optimistic, inplace)
+	case uint16:
+		res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
+			false, false, optimistic, inplace)
+	case uint32:
+		res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
+			false, false, optimistic, inplace)
+	case uint64:
+		res, err = set(jstr, path, strconv.FormatUint(uint64(v), 10),
+			false, false, optimistic, inplace)
+	case float32:
+		res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
+			false, false, optimistic, inplace)
+	case float64:
+		res, err = set(jstr, path, strconv.FormatFloat(float64(v), 'f', -1, 64),
+			false, false, optimistic, inplace)
+	}
+	if err == errNoChange {
+		return json, nil
+	}
+	return res, err
+}
+
+// SetRawBytesOptions sets a raw json value for the specified path with options.
+// If working with bytes, this method preferred over
+// SetRawOptions(string(data), path, value, opts)
+func SetRawBytesOptions(json []byte, path string, value []byte,
+	opts *Options) ([]byte, error) {
+	jstr := *(*string)(unsafe.Pointer(&json))
+	vstr := *(*string)(unsafe.Pointer(&value))
+	var optimistic, inplace bool
+	if opts != nil {
+		optimistic = opts.Optimistic
+		inplace = opts.ReplaceInPlace
+	}
+	res, err := set(jstr, path, vstr, false, false, optimistic, inplace)
+	if err == errNoChange {
+		return json, nil
+	}
+	return res, err
+}