Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 5 additions & 13 deletions json/string.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package json

import (
"encoding/binary"
"math/bits"
"unsafe"
)

const (
Expand All @@ -16,8 +16,8 @@ const (
// the chars <, > and & also require escaping. If no chars in `s` require
// escaping, the return value is -1.
func escapeIndex(s string, escapeHTML bool) int {
chunks := stringToUint64(s)
for _, n := range chunks {
for i := 0; i+8 <= len(s); i += 8 {
n := binary.LittleEndian.Uint64([]byte(s[i : i+8]))
// combine masks before checking for the MSB of each byte. We include
// `n` in the mask to check whether any of the *input* byte MSBs were
// set (i.e. the byte was outside the ASCII range).
Expand All @@ -26,11 +26,11 @@ func escapeIndex(s string, escapeHTML bool) int {
mask |= contains(n, '<') | contains(n, '>') | contains(n, '&')
}
if (mask & msb) != 0 {
return bits.TrailingZeros64(mask&msb) / 8
return bits.TrailingZeros64(mask&msb)/8 + i
}
}

for i := len(chunks) * 8; i < len(s); i++ {
for i := (len(s) / 8) * 8; i < len(s); i++ {
c := s[i]
if c < 0x20 || c > 0x7f || c == '"' || c == '\\' || (escapeHTML && (c == '<' || c == '>' || c == '&')) {
return i
Expand Down Expand Up @@ -79,11 +79,3 @@ func contains(n uint64, b byte) uint64 {
func expand(b byte) uint64 {
return lsb * uint64(b)
}

func stringToUint64(s string) []uint64 {
return *(*[]uint64)(unsafe.Pointer(&sliceHeader{
Data: *(*unsafe.Pointer)(unsafe.Pointer(&s)),
Len: len(s) / 8,
Cap: len(s) / 8,
}))
}
56 changes: 56 additions & 0 deletions json/string_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package json

import (
stdjson "encoding/json"
"strings"
"testing"
)
Expand Down Expand Up @@ -36,3 +37,58 @@ func benchmarkEscapeIndex(b *testing.B, s string, escapeHTML bool) {
}
b.SetBytes(int64(len(s)))
}

// reference: index of the first byte that needs escaping, or -1
func refEscapeIndex(s string, escapeHTML bool) int {
for i := 0; i < len(s); i++ {
c := s[i]
if c < 0x20 || c > 0x7f || c == '"' || c == '\\' ||
(escapeHTML && (c == '<' || c == '>' || c == '&')) {
return i
}
}
return -1
}

func TestEscapeIndexBigEndian(t *testing.T) {
inputs := []string{
`C:\Users\admin`, // backslash at byte 2
"key\tvalue", // tab at byte 3
"\nfoo_bar_", // newline at byte 0
`ab"cdefg`, // quote at byte 2
"line1\nline2", // newline at byte 5
`say "hello"`, // quote at byte 4
"1234567\nabc", // last byte of first chunk
"12345678\nabc", // first byte of second chunk
"-----BEGIN CERTIFICATE-----\nABCD\n-----END CERTIFICATE-----\n",
"<tag>&more", // HTML characters
"plain ascii with no escapes here", // no escaping needed
strings.Repeat("a", 33) + `"`, // quote far into the string
}

for _, s := range inputs {
for _, html := range []bool{false, true} {
if got, want := escapeIndex(s, html), refEscapeIndex(s, html); got != want {
t.Errorf("escapeIndex(%q, %v) = %d, want %d", s, html, got, want)
}
}

b, err := Marshal(struct {
V string `json:"v"`
}{s})
if err != nil {
t.Errorf("Marshal(%q): %v", s, err)
continue
}
var out struct {
V string `json:"v"`
}
if err := stdjson.Unmarshal(b, &out); err != nil {
t.Errorf("%q produced invalid JSON %s: %v", s, b, err)
continue
}
if out.V != s {
t.Errorf("%q round-trips to %q", s, out.V)
}
}
}