Skip to content

Commit

Permalink
Remove encoding/json dependency
Browse files Browse the repository at this point in the history
The only purpose of using the built-in Go was to encode json
strings that had unicode or needed to escaped.

This commit adds the new function `AppendJSONString` which allows
for appending strings as their json representation to a byte
slice.

It's about 2x faster than using json.Marshal.
  • Loading branch information
tidwall committed Apr 19, 2022
1 parent 56c0a0a commit c3bb2c3
Show file tree
Hide file tree
Showing 2 changed files with 99 additions and 14 deletions.
73 changes: 59 additions & 14 deletions gjson.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package gjson

import (
"encoding/json"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -1824,17 +1823,64 @@ func isSimpleName(component string) bool {
return true
}

func appendJSONString(dst []byte, s string) []byte {
var hexchars = [...]byte{
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f',
}

func appendHex16(dst []byte, x uint16) []byte {
return append(dst,
hexchars[x>>12&0xF], hexchars[x>>8&0xF],
hexchars[x>>4&0xF], hexchars[x>>0&0xF],
)
}

// AppendJSONString is a convenience function that converts the provided string
// to a valid JSON string and appends it to dst.
func AppendJSONString(dst []byte, s string) []byte {
dst = append(dst, make([]byte, len(s)+2)...)
dst = append(dst[:len(dst)-len(s)-2], '"')
for i := 0; i < len(s); i++ {
if s[i] < ' ' || s[i] == '\\' || s[i] == '"' || s[i] > 126 {
d, _ := json.Marshal(s)
return append(dst, string(d)...)
if s[i] < ' ' {
dst = append(dst, '\\')
switch s[i] {
case '\n':
dst = append(dst, 'n')
case '\r':
dst = append(dst, 'r')
case '\t':
dst = append(dst, 't')
default:
dst = append(dst, 'u')
dst = appendHex16(dst, uint16(s[i]))
}
} else if s[i] == '>' || s[i] == '<' || s[i] == '&' {
dst = append(dst, '\\', 'u')
dst = appendHex16(dst, uint16(s[i]))
} else if s[i] == '\\' {
dst = append(dst, '\\', '\\')
} else if s[i] == '"' {
dst = append(dst, '\\', '"')
} else if s[i] > 127 {
// read utf8 character
r, n := utf8.DecodeRuneInString(s[i:])
if n == 0 {
break
}
if r == utf8.RuneError && n == 1 {
dst = append(dst, `\ufffd`...)
} else if r == '\u2028' || r == '\u2029' {
dst = append(dst, `\u202`...)
dst = append(dst, hexchars[r&0xF])
} else {
dst = append(dst, s[i:i+n]...)
}
i = i + n - 1
} else {
dst = append(dst, s[i])
}
}
dst = append(dst, '"')
dst = append(dst, s...)
dst = append(dst, '"')
return dst
return append(dst, '"')
}

type parseContext struct {
Expand Down Expand Up @@ -1924,14 +1970,14 @@ func Get(json, path string) Result {
if sub.name[0] == '"' && Valid(sub.name) {
b = append(b, sub.name...)
} else {
b = appendJSONString(b, sub.name)
b = AppendJSONString(b, sub.name)
}
} else {
last := nameOfLast(sub.path)
if isSimpleName(last) {
b = appendJSONString(b, last)
b = AppendJSONString(b, last)
} else {
b = appendJSONString(b, "_")
b = AppendJSONString(b, "_")
}
}
b = append(b, ':')
Expand Down Expand Up @@ -2974,8 +3020,7 @@ func modFromStr(json, arg string) string {
// @tostr converts a string to json
// {"id":1023,"name":"alert"} -> "{\"id\":1023,\"name\":\"alert\"}"
func modToStr(str, arg string) string {
data, _ := json.Marshal(str)
return string(data)
return string(AppendJSONString(nil, str))
}

func modGroup(json, arg string) string {
Expand Down
40 changes: 40 additions & 0 deletions gjson_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2503,3 +2503,43 @@ func TestGroup(t *testing.T) {
res = Get(json, `{"id":issues.#.id,"plans":issues.#.fields.labels.#(%"plan:*")#|#.#}|@group|#(plans>=2)#.id`).Raw
assert(t, res == `["123"]`)
}

func testJSONString(t *testing.T, str string) {
gjsonString := string(AppendJSONString(nil, str))
data, err := json.Marshal(str)
if err != nil {
panic(123)
}
goString := string(data)
if gjsonString != goString {
t.Fatal(strconv.Quote(str) + "\n\t" +
gjsonString + "\n\t" +
goString + "\n\t<<< MISMATCH >>>")
}
}

func TestJSONString(t *testing.T) {
testJSONString(t, "hello")
testJSONString(t, "he\"llo")
testJSONString(t, "he\"l\\lo")
const input = `{"utf8":"Example emoji, KO: \ud83d\udd13, \ud83c\udfc3 ` +
`OK: \u2764\ufe0f "}`
value := Get(input, "utf8")
var s string
json.Unmarshal([]byte(value.Raw), &s)
if value.String() != s {
t.Fatalf("expected '%v', got '%v'", s, value.String())
}
testJSONString(t, s)
testJSONString(t, "R\xfd\xfc\a!\x82eO\x16?_\x0f\x9ab\x1dr")
testJSONString(t, "_\xb9\v\xad\xb3|X!\xb6\xd9U&\xa4\x1a\x95\x04")
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
start := time.Now()
var buf [16]byte
for time.Since(start) < time.Second*2 {
if _, err := rng.Read(buf[:]); err != nil {
t.Fatal(err)
}
testJSONString(t, string(buf[:]))
}
}

0 comments on commit c3bb2c3

Please # to comment.