mirror of
https://github.com/ergochat/ergo.git
synced 2024-11-25 05:19:25 +01:00
c972a92e51
Resolves CVE-2021-42836, which probably didn't affect us, but we might as well upgrade.
675 lines
15 KiB
Go
675 lines
15 KiB
Go
package pretty
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"sort"
|
|
"strconv"
|
|
)
|
|
|
|
// Options is Pretty options
|
|
type Options struct {
|
|
// Width is an max column width for single line arrays
|
|
// Default is 80
|
|
Width int
|
|
// Prefix is a prefix for all lines
|
|
// Default is an empty string
|
|
Prefix string
|
|
// Indent is the nested indentation
|
|
// Default is two spaces
|
|
Indent string
|
|
// SortKeys will sort the keys alphabetically
|
|
// Default is false
|
|
SortKeys bool
|
|
}
|
|
|
|
// DefaultOptions is the default options for pretty formats.
|
|
var DefaultOptions = &Options{Width: 80, Prefix: "", Indent: " ", SortKeys: false}
|
|
|
|
// Pretty converts the input json into a more human readable format where each
|
|
// element is on it's own line with clear indentation.
|
|
func Pretty(json []byte) []byte { return PrettyOptions(json, nil) }
|
|
|
|
// PrettyOptions is like Pretty but with customized options.
|
|
func PrettyOptions(json []byte, opts *Options) []byte {
|
|
if opts == nil {
|
|
opts = DefaultOptions
|
|
}
|
|
buf := make([]byte, 0, len(json))
|
|
if len(opts.Prefix) != 0 {
|
|
buf = append(buf, opts.Prefix...)
|
|
}
|
|
buf, _, _, _ = appendPrettyAny(buf, json, 0, true,
|
|
opts.Width, opts.Prefix, opts.Indent, opts.SortKeys,
|
|
0, 0, -1)
|
|
if len(buf) > 0 {
|
|
buf = append(buf, '\n')
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// Ugly removes insignificant space characters from the input json byte slice
|
|
// and returns the compacted result.
|
|
func Ugly(json []byte) []byte {
|
|
buf := make([]byte, 0, len(json))
|
|
return ugly(buf, json)
|
|
}
|
|
|
|
// UglyInPlace removes insignificant space characters from the input json
|
|
// byte slice and returns the compacted result. This method reuses the
|
|
// input json buffer to avoid allocations. Do not use the original bytes
|
|
// slice upon return.
|
|
func UglyInPlace(json []byte) []byte { return ugly(json, json) }
|
|
|
|
func ugly(dst, src []byte) []byte {
|
|
dst = dst[:0]
|
|
for i := 0; i < len(src); i++ {
|
|
if src[i] > ' ' {
|
|
dst = append(dst, src[i])
|
|
if src[i] == '"' {
|
|
for i = i + 1; i < len(src); i++ {
|
|
dst = append(dst, src[i])
|
|
if src[i] == '"' {
|
|
j := i - 1
|
|
for ; ; j-- {
|
|
if src[j] != '\\' {
|
|
break
|
|
}
|
|
}
|
|
if (j-i)%2 != 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
func isNaNOrInf(src []byte) bool {
|
|
return src[0] == 'i' || //Inf
|
|
src[0] == 'I' || // inf
|
|
src[0] == '+' || // +Inf
|
|
src[0] == 'N' || // Nan
|
|
(src[0] == 'n' && len(src) > 1 && src[1] != 'u') // nan
|
|
}
|
|
|
|
func appendPrettyAny(buf, json []byte, i int, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) {
|
|
for ; i < len(json); i++ {
|
|
if json[i] <= ' ' {
|
|
continue
|
|
}
|
|
if json[i] == '"' {
|
|
return appendPrettyString(buf, json, i, nl)
|
|
}
|
|
|
|
if (json[i] >= '0' && json[i] <= '9') || json[i] == '-' || isNaNOrInf(json[i:]) {
|
|
return appendPrettyNumber(buf, json, i, nl)
|
|
}
|
|
if json[i] == '{' {
|
|
return appendPrettyObject(buf, json, i, '{', '}', pretty, width, prefix, indent, sortkeys, tabs, nl, max)
|
|
}
|
|
if json[i] == '[' {
|
|
return appendPrettyObject(buf, json, i, '[', ']', pretty, width, prefix, indent, sortkeys, tabs, nl, max)
|
|
}
|
|
switch json[i] {
|
|
case 't':
|
|
return append(buf, 't', 'r', 'u', 'e'), i + 4, nl, true
|
|
case 'f':
|
|
return append(buf, 'f', 'a', 'l', 's', 'e'), i + 5, nl, true
|
|
case 'n':
|
|
return append(buf, 'n', 'u', 'l', 'l'), i + 4, nl, true
|
|
}
|
|
}
|
|
return buf, i, nl, true
|
|
}
|
|
|
|
type pair struct {
|
|
kstart, kend int
|
|
vstart, vend int
|
|
}
|
|
|
|
type byKeyVal struct {
|
|
sorted bool
|
|
json []byte
|
|
buf []byte
|
|
pairs []pair
|
|
}
|
|
|
|
func (arr *byKeyVal) Len() int {
|
|
return len(arr.pairs)
|
|
}
|
|
func (arr *byKeyVal) Less(i, j int) bool {
|
|
if arr.isLess(i, j, byKey) {
|
|
return true
|
|
}
|
|
if arr.isLess(j, i, byKey) {
|
|
return false
|
|
}
|
|
return arr.isLess(i, j, byVal)
|
|
}
|
|
func (arr *byKeyVal) Swap(i, j int) {
|
|
arr.pairs[i], arr.pairs[j] = arr.pairs[j], arr.pairs[i]
|
|
arr.sorted = true
|
|
}
|
|
|
|
type byKind int
|
|
|
|
const (
|
|
byKey byKind = 0
|
|
byVal byKind = 1
|
|
)
|
|
|
|
type jtype int
|
|
|
|
const (
|
|
jnull jtype = iota
|
|
jfalse
|
|
jnumber
|
|
jstring
|
|
jtrue
|
|
jjson
|
|
)
|
|
|
|
func getjtype(v []byte) jtype {
|
|
if len(v) == 0 {
|
|
return jnull
|
|
}
|
|
switch v[0] {
|
|
case '"':
|
|
return jstring
|
|
case 'f':
|
|
return jfalse
|
|
case 't':
|
|
return jtrue
|
|
case 'n':
|
|
return jnull
|
|
case '[', '{':
|
|
return jjson
|
|
default:
|
|
return jnumber
|
|
}
|
|
}
|
|
|
|
func (arr *byKeyVal) isLess(i, j int, kind byKind) bool {
|
|
k1 := arr.json[arr.pairs[i].kstart:arr.pairs[i].kend]
|
|
k2 := arr.json[arr.pairs[j].kstart:arr.pairs[j].kend]
|
|
var v1, v2 []byte
|
|
if kind == byKey {
|
|
v1 = k1
|
|
v2 = k2
|
|
} else {
|
|
v1 = bytes.TrimSpace(arr.buf[arr.pairs[i].vstart:arr.pairs[i].vend])
|
|
v2 = bytes.TrimSpace(arr.buf[arr.pairs[j].vstart:arr.pairs[j].vend])
|
|
if len(v1) >= len(k1)+1 {
|
|
v1 = bytes.TrimSpace(v1[len(k1)+1:])
|
|
}
|
|
if len(v2) >= len(k2)+1 {
|
|
v2 = bytes.TrimSpace(v2[len(k2)+1:])
|
|
}
|
|
}
|
|
t1 := getjtype(v1)
|
|
t2 := getjtype(v2)
|
|
if t1 < t2 {
|
|
return true
|
|
}
|
|
if t1 > t2 {
|
|
return false
|
|
}
|
|
if t1 == jstring {
|
|
s1 := parsestr(v1)
|
|
s2 := parsestr(v2)
|
|
return string(s1) < string(s2)
|
|
}
|
|
if t1 == jnumber {
|
|
n1, _ := strconv.ParseFloat(string(v1), 64)
|
|
n2, _ := strconv.ParseFloat(string(v2), 64)
|
|
return n1 < n2
|
|
}
|
|
return string(v1) < string(v2)
|
|
|
|
}
|
|
|
|
func parsestr(s []byte) []byte {
|
|
for i := 1; i < len(s); i++ {
|
|
if s[i] == '\\' {
|
|
var str string
|
|
json.Unmarshal(s, &str)
|
|
return []byte(str)
|
|
}
|
|
if s[i] == '"' {
|
|
return s[1:i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func appendPrettyObject(buf, json []byte, i int, open, close byte, pretty bool, width int, prefix, indent string, sortkeys bool, tabs, nl, max int) ([]byte, int, int, bool) {
|
|
var ok bool
|
|
if width > 0 {
|
|
if pretty && open == '[' && max == -1 {
|
|
// here we try to create a single line array
|
|
max := width - (len(buf) - nl)
|
|
if max > 3 {
|
|
s1, s2 := len(buf), i
|
|
buf, i, _, ok = appendPrettyObject(buf, json, i, '[', ']', false, width, prefix, "", sortkeys, 0, 0, max)
|
|
if ok && len(buf)-s1 <= max {
|
|
return buf, i, nl, true
|
|
}
|
|
buf = buf[:s1]
|
|
i = s2
|
|
}
|
|
} else if max != -1 && open == '{' {
|
|
return buf, i, nl, false
|
|
}
|
|
}
|
|
buf = append(buf, open)
|
|
i++
|
|
var pairs []pair
|
|
if open == '{' && sortkeys {
|
|
pairs = make([]pair, 0, 8)
|
|
}
|
|
var n int
|
|
for ; i < len(json); i++ {
|
|
if json[i] <= ' ' {
|
|
continue
|
|
}
|
|
if json[i] == close {
|
|
if pretty {
|
|
if open == '{' && sortkeys {
|
|
buf = sortPairs(json, buf, pairs)
|
|
}
|
|
if n > 0 {
|
|
nl = len(buf)
|
|
if buf[nl-1] == ' ' {
|
|
buf[nl-1] = '\n'
|
|
} else {
|
|
buf = append(buf, '\n')
|
|
}
|
|
}
|
|
if buf[len(buf)-1] != open {
|
|
buf = appendTabs(buf, prefix, indent, tabs)
|
|
}
|
|
}
|
|
buf = append(buf, close)
|
|
return buf, i + 1, nl, open != '{'
|
|
}
|
|
if open == '[' || json[i] == '"' {
|
|
if n > 0 {
|
|
buf = append(buf, ',')
|
|
if width != -1 && open == '[' {
|
|
buf = append(buf, ' ')
|
|
}
|
|
}
|
|
var p pair
|
|
if pretty {
|
|
nl = len(buf)
|
|
if buf[nl-1] == ' ' {
|
|
buf[nl-1] = '\n'
|
|
} else {
|
|
buf = append(buf, '\n')
|
|
}
|
|
if open == '{' && sortkeys {
|
|
p.kstart = i
|
|
p.vstart = len(buf)
|
|
}
|
|
buf = appendTabs(buf, prefix, indent, tabs+1)
|
|
}
|
|
if open == '{' {
|
|
buf, i, nl, _ = appendPrettyString(buf, json, i, nl)
|
|
if sortkeys {
|
|
p.kend = i
|
|
}
|
|
buf = append(buf, ':')
|
|
if pretty {
|
|
buf = append(buf, ' ')
|
|
}
|
|
}
|
|
buf, i, nl, ok = appendPrettyAny(buf, json, i, pretty, width, prefix, indent, sortkeys, tabs+1, nl, max)
|
|
if max != -1 && !ok {
|
|
return buf, i, nl, false
|
|
}
|
|
if pretty && open == '{' && sortkeys {
|
|
p.vend = len(buf)
|
|
if p.kstart > p.kend || p.vstart > p.vend {
|
|
// bad data. disable sorting
|
|
sortkeys = false
|
|
} else {
|
|
pairs = append(pairs, p)
|
|
}
|
|
}
|
|
i--
|
|
n++
|
|
}
|
|
}
|
|
return buf, i, nl, open != '{'
|
|
}
|
|
func sortPairs(json, buf []byte, pairs []pair) []byte {
|
|
if len(pairs) == 0 {
|
|
return buf
|
|
}
|
|
vstart := pairs[0].vstart
|
|
vend := pairs[len(pairs)-1].vend
|
|
arr := byKeyVal{false, json, buf, pairs}
|
|
sort.Stable(&arr)
|
|
if !arr.sorted {
|
|
return buf
|
|
}
|
|
nbuf := make([]byte, 0, vend-vstart)
|
|
for i, p := range pairs {
|
|
nbuf = append(nbuf, buf[p.vstart:p.vend]...)
|
|
if i < len(pairs)-1 {
|
|
nbuf = append(nbuf, ',')
|
|
nbuf = append(nbuf, '\n')
|
|
}
|
|
}
|
|
return append(buf[:vstart], nbuf...)
|
|
}
|
|
|
|
func appendPrettyString(buf, json []byte, i, nl int) ([]byte, int, int, bool) {
|
|
s := i
|
|
i++
|
|
for ; i < len(json); i++ {
|
|
if json[i] == '"' {
|
|
var sc int
|
|
for j := i - 1; j > s; j-- {
|
|
if json[j] == '\\' {
|
|
sc++
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
if sc%2 == 1 {
|
|
continue
|
|
}
|
|
i++
|
|
break
|
|
}
|
|
}
|
|
return append(buf, json[s:i]...), i, nl, true
|
|
}
|
|
|
|
func appendPrettyNumber(buf, json []byte, i, nl int) ([]byte, int, int, bool) {
|
|
s := i
|
|
i++
|
|
for ; i < len(json); i++ {
|
|
if json[i] <= ' ' || json[i] == ',' || json[i] == ':' || json[i] == ']' || json[i] == '}' {
|
|
break
|
|
}
|
|
}
|
|
return append(buf, json[s:i]...), i, nl, true
|
|
}
|
|
|
|
func appendTabs(buf []byte, prefix, indent string, tabs int) []byte {
|
|
if len(prefix) != 0 {
|
|
buf = append(buf, prefix...)
|
|
}
|
|
if len(indent) == 2 && indent[0] == ' ' && indent[1] == ' ' {
|
|
for i := 0; i < tabs; i++ {
|
|
buf = append(buf, ' ', ' ')
|
|
}
|
|
} else {
|
|
for i := 0; i < tabs; i++ {
|
|
buf = append(buf, indent...)
|
|
}
|
|
}
|
|
return buf
|
|
}
|
|
|
|
// Style is the color style
|
|
type Style struct {
|
|
Key, String, Number [2]string
|
|
True, False, Null [2]string
|
|
Escape [2]string
|
|
Append func(dst []byte, c byte) []byte
|
|
}
|
|
|
|
func hexp(p byte) byte {
|
|
switch {
|
|
case p < 10:
|
|
return p + '0'
|
|
default:
|
|
return (p - 10) + 'a'
|
|
}
|
|
}
|
|
|
|
// TerminalStyle is for terminals
|
|
var TerminalStyle *Style
|
|
|
|
func init() {
|
|
TerminalStyle = &Style{
|
|
Key: [2]string{"\x1B[94m", "\x1B[0m"},
|
|
String: [2]string{"\x1B[92m", "\x1B[0m"},
|
|
Number: [2]string{"\x1B[93m", "\x1B[0m"},
|
|
True: [2]string{"\x1B[96m", "\x1B[0m"},
|
|
False: [2]string{"\x1B[96m", "\x1B[0m"},
|
|
Null: [2]string{"\x1B[91m", "\x1B[0m"},
|
|
Escape: [2]string{"\x1B[35m", "\x1B[0m"},
|
|
Append: func(dst []byte, c byte) []byte {
|
|
if c < ' ' && (c != '\r' && c != '\n' && c != '\t' && c != '\v') {
|
|
dst = append(dst, "\\u00"...)
|
|
dst = append(dst, hexp((c>>4)&0xF))
|
|
return append(dst, hexp((c)&0xF))
|
|
}
|
|
return append(dst, c)
|
|
},
|
|
}
|
|
}
|
|
|
|
// Color will colorize the json. The style parma is used for customizing
|
|
// the colors. Passing nil to the style param will use the default
|
|
// TerminalStyle.
|
|
func Color(src []byte, style *Style) []byte {
|
|
if style == nil {
|
|
style = TerminalStyle
|
|
}
|
|
apnd := style.Append
|
|
if apnd == nil {
|
|
apnd = func(dst []byte, c byte) []byte {
|
|
return append(dst, c)
|
|
}
|
|
}
|
|
type stackt struct {
|
|
kind byte
|
|
key bool
|
|
}
|
|
var dst []byte
|
|
var stack []stackt
|
|
for i := 0; i < len(src); i++ {
|
|
if src[i] == '"' {
|
|
key := len(stack) > 0 && stack[len(stack)-1].key
|
|
if key {
|
|
dst = append(dst, style.Key[0]...)
|
|
} else {
|
|
dst = append(dst, style.String[0]...)
|
|
}
|
|
dst = apnd(dst, '"')
|
|
esc := false
|
|
uesc := 0
|
|
for i = i + 1; i < len(src); i++ {
|
|
if src[i] == '\\' {
|
|
if key {
|
|
dst = append(dst, style.Key[1]...)
|
|
} else {
|
|
dst = append(dst, style.String[1]...)
|
|
}
|
|
dst = append(dst, style.Escape[0]...)
|
|
dst = apnd(dst, src[i])
|
|
esc = true
|
|
if i+1 < len(src) && src[i+1] == 'u' {
|
|
uesc = 5
|
|
} else {
|
|
uesc = 1
|
|
}
|
|
} else if esc {
|
|
dst = apnd(dst, src[i])
|
|
if uesc == 1 {
|
|
esc = false
|
|
dst = append(dst, style.Escape[1]...)
|
|
if key {
|
|
dst = append(dst, style.Key[0]...)
|
|
} else {
|
|
dst = append(dst, style.String[0]...)
|
|
}
|
|
} else {
|
|
uesc--
|
|
}
|
|
} else {
|
|
dst = apnd(dst, src[i])
|
|
}
|
|
if src[i] == '"' {
|
|
j := i - 1
|
|
for ; ; j-- {
|
|
if src[j] != '\\' {
|
|
break
|
|
}
|
|
}
|
|
if (j-i)%2 != 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if esc {
|
|
dst = append(dst, style.Escape[1]...)
|
|
} else if key {
|
|
dst = append(dst, style.Key[1]...)
|
|
} else {
|
|
dst = append(dst, style.String[1]...)
|
|
}
|
|
} else if src[i] == '{' || src[i] == '[' {
|
|
stack = append(stack, stackt{src[i], src[i] == '{'})
|
|
dst = apnd(dst, src[i])
|
|
} else if (src[i] == '}' || src[i] == ']') && len(stack) > 0 {
|
|
stack = stack[:len(stack)-1]
|
|
dst = apnd(dst, src[i])
|
|
} else if (src[i] == ':' || src[i] == ',') && len(stack) > 0 && stack[len(stack)-1].kind == '{' {
|
|
stack[len(stack)-1].key = !stack[len(stack)-1].key
|
|
dst = apnd(dst, src[i])
|
|
} else {
|
|
var kind byte
|
|
if (src[i] >= '0' && src[i] <= '9') || src[i] == '-' || isNaNOrInf(src[i:]) {
|
|
kind = '0'
|
|
dst = append(dst, style.Number[0]...)
|
|
} else if src[i] == 't' {
|
|
kind = 't'
|
|
dst = append(dst, style.True[0]...)
|
|
} else if src[i] == 'f' {
|
|
kind = 'f'
|
|
dst = append(dst, style.False[0]...)
|
|
} else if src[i] == 'n' {
|
|
kind = 'n'
|
|
dst = append(dst, style.Null[0]...)
|
|
} else {
|
|
dst = apnd(dst, src[i])
|
|
}
|
|
if kind != 0 {
|
|
for ; i < len(src); i++ {
|
|
if src[i] <= ' ' || src[i] == ',' || src[i] == ':' || src[i] == ']' || src[i] == '}' {
|
|
i--
|
|
break
|
|
}
|
|
dst = apnd(dst, src[i])
|
|
}
|
|
if kind == '0' {
|
|
dst = append(dst, style.Number[1]...)
|
|
} else if kind == 't' {
|
|
dst = append(dst, style.True[1]...)
|
|
} else if kind == 'f' {
|
|
dst = append(dst, style.False[1]...)
|
|
} else if kind == 'n' {
|
|
dst = append(dst, style.Null[1]...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return dst
|
|
}
|
|
|
|
// Spec strips out comments and trailing commas and convert the input to a
|
|
// valid JSON per the official spec: https://tools.ietf.org/html/rfc8259
|
|
//
|
|
// The resulting JSON will always be the same length as the input and it will
|
|
// include all of the same line breaks at matching offsets. This is to ensure
|
|
// the result can be later processed by a external parser and that that
|
|
// parser will report messages or errors with the correct offsets.
|
|
func Spec(src []byte) []byte {
|
|
return spec(src, nil)
|
|
}
|
|
|
|
// SpecInPlace is the same as Spec, but this method reuses the input json
|
|
// buffer to avoid allocations. Do not use the original bytes slice upon return.
|
|
func SpecInPlace(src []byte) []byte {
|
|
return spec(src, src)
|
|
}
|
|
|
|
func spec(src, dst []byte) []byte {
|
|
dst = dst[:0]
|
|
for i := 0; i < len(src); i++ {
|
|
if src[i] == '/' {
|
|
if i < len(src)-1 {
|
|
if src[i+1] == '/' {
|
|
dst = append(dst, ' ', ' ')
|
|
i += 2
|
|
for ; i < len(src); i++ {
|
|
if src[i] == '\n' {
|
|
dst = append(dst, '\n')
|
|
break
|
|
} else if src[i] == '\t' || src[i] == '\r' {
|
|
dst = append(dst, src[i])
|
|
} else {
|
|
dst = append(dst, ' ')
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
if src[i+1] == '*' {
|
|
dst = append(dst, ' ', ' ')
|
|
i += 2
|
|
for ; i < len(src)-1; i++ {
|
|
if src[i] == '*' && src[i+1] == '/' {
|
|
dst = append(dst, ' ', ' ')
|
|
i++
|
|
break
|
|
} else if src[i] == '\n' || src[i] == '\t' ||
|
|
src[i] == '\r' {
|
|
dst = append(dst, src[i])
|
|
} else {
|
|
dst = append(dst, ' ')
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
dst = append(dst, src[i])
|
|
if src[i] == '"' {
|
|
for i = i + 1; i < len(src); i++ {
|
|
dst = append(dst, src[i])
|
|
if src[i] == '"' {
|
|
j := i - 1
|
|
for ; ; j-- {
|
|
if src[j] != '\\' {
|
|
break
|
|
}
|
|
}
|
|
if (j-i)%2 != 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
} else if src[i] == '}' || src[i] == ']' {
|
|
for j := len(dst) - 2; j >= 0; j-- {
|
|
if dst[j] <= ' ' {
|
|
continue
|
|
}
|
|
if dst[j] == ',' {
|
|
dst[j] = ' '
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return dst
|
|
}
|