Skip to content

Commit

Permalink
Properly escape JSON with char codes below 0x20
Browse files Browse the repository at this point in the history
Updates VictoriaMetrics/VictoriaMetrics#613

Previously such char codes couldn't be parsed by JSON parsers.
  • Loading branch information
valyala committed Jul 10, 2020
1 parent 1a0f4e9 commit 7dd50ac
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 100 deletions.
132 changes: 52 additions & 80 deletions jsonstring.go
Original file line number Diff line number Diff line change
@@ -1,93 +1,65 @@
package quicktemplate

import (
"io"
"fmt"
"strings"
)

func writeJSONString(w io.Writer, s string) {
if len(s) > 24 &&
strings.IndexByte(s, '"') < 0 &&
strings.IndexByte(s, '\\') < 0 &&
strings.IndexByte(s, '\n') < 0 &&
strings.IndexByte(s, '\r') < 0 &&
strings.IndexByte(s, '\t') < 0 &&
strings.IndexByte(s, '\f') < 0 &&
strings.IndexByte(s, '\b') < 0 &&
strings.IndexByte(s, '<') < 0 &&
strings.IndexByte(s, '\'') < 0 &&
strings.IndexByte(s, 0) < 0 {
func hasSpecialChars(s string) bool {
if strings.IndexByte(s, '"') >= 0 || strings.IndexByte(s, '\\') >= 0 || strings.IndexByte(s, '<') >= 0 || strings.IndexByte(s, '\'') >= 0 {
return true
}
for i := 0; i < len(s); i++ {
if s[i] < 0x20 {
return true
}
}
return false
}

// fast path - nothing to escape
w.Write(unsafeStrToBytes(s))
return
func appendJSONString(dst []byte, s string, addQuotes bool) []byte {
if !hasSpecialChars(s) {
// Fast path - nothing to escape.
if !addQuotes {
return append(dst, s...)
}
dst = append(dst, '"')
dst = append(dst, s...)
dst = append(dst, '"')
return dst
}

// slow path
write := w.Write
b := unsafeStrToBytes(s)
j := 0
n := len(b)
if n > 0 {
// Hint the compiler to remove bounds checks in the loop below.
_ = b[n-1]
// Slow path - there are chars to escape.
if addQuotes {
dst = append(dst, '"')
}
for i := 0; i < n; i++ {
switch b[i] {
case '"':
write(b[j:i])
write(strBackslashQuote)
j = i + 1
case '\\':
write(b[j:i])
write(strBackslashBackslash)
j = i + 1
case '\n':
write(b[j:i])
write(strBackslashN)
j = i + 1
case '\r':
write(b[j:i])
write(strBackslashR)
j = i + 1
case '\t':
write(b[j:i])
write(strBackslashT)
j = i + 1
case '\f':
write(b[j:i])
write(strBackslashF)
j = i + 1
case '\b':
write(b[j:i])
write(strBackslashB)
j = i + 1
case '<':
write(b[j:i])
write(strBackslashLT)
j = i + 1
case '\'':
write(b[j:i])
write(strBackslashQ)
j = i + 1
case 0:
write(b[j:i])
write(strBackslashZero)
j = i + 1
}
bb := AcquireByteBuffer()
var tmp []byte
tmp, bb.B = bb.B, dst
_, err := jsonReplacer.WriteString(bb, s)
if err != nil {
panic(fmt.Errorf("BUG: unexpected error returned from jsonReplacer.WriteString: %s", err))
}
dst, bb.B = bb.B, tmp
ReleaseByteBuffer(bb)
if addQuotes {
dst = append(dst, '"')
}
write(b[j:])
return dst
}

var (
strBackslashQuote = []byte(`\"`)
strBackslashBackslash = []byte(`\\`)
strBackslashN = []byte(`\n`)
strBackslashR = []byte(`\r`)
strBackslashT = []byte(`\t`)
strBackslashF = []byte(`\u000c`)
strBackslashB = []byte(`\u0008`)
strBackslashLT = []byte(`\u003c`)
strBackslashQ = []byte(`\u0027`)
strBackslashZero = []byte(`\u0000`)
)
var jsonReplacer = strings.NewReplacer(func() []string {
a := []string{
"\n", `\n`,
"\r", `\r`,
"\t", `\t`,
"\"", `\"`,
"\\", `\\`,
"<", `\u003c`,
"'", `\u0027`,
}
for i := 0; i < 0x20; i++ {
a = append(a, string([]byte{byte(i)}), fmt.Sprintf(`\u%04x`, i))
}
return a
}()...)
29 changes: 15 additions & 14 deletions jsonstring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,32 @@ import (
"testing"
)

func TestWriteJSONString(t *testing.T) {
testWriteJSONString(t, ``)
testWriteJSONString(t, `f`)
testWriteJSONString(t, `"`)
testWriteJSONString(t, `<`)
testWriteJSONString(t, "\x00\n\r\t\b\f"+`"\`)
testWriteJSONString(t, `"foobar`)
testWriteJSONString(t, `foobar"`)
testWriteJSONString(t, `foo "bar"
func TestAppendJSONString(t *testing.T) {
testAppendJSONString(t, ``)
testAppendJSONString(t, `f`)
testAppendJSONString(t, `"`)
testAppendJSONString(t, `<`)
testAppendJSONString(t, "\x00\n\r\t\b\f"+`"\`)
testAppendJSONString(t, `"foobar`)
testAppendJSONString(t, `foobar"`)
testAppendJSONString(t, `foo "bar"
baz`)
testWriteJSONString(t, `this is a "тест"`)
testWriteJSONString(t, `привет test ыва`)
testAppendJSONString(t, `this is a "тест"`)
testAppendJSONString(t, `привет test ыва`)

testWriteJSONString(t, `</script><script>alert('evil')</script>`)
testAppendJSONString(t, `</script><script>alert('evil')</script>`)
testAppendJSONString(t, "\u001b")
}

func testWriteJSONString(t *testing.T, s string) {
func testAppendJSONString(t *testing.T, s string) {
expectedResult, err := json.Marshal(s)
if err != nil {
t.Fatalf("unexpected error when encoding string %q: %s", s, err)
}
expectedResult = expectedResult[1 : len(expectedResult)-1]

bb := AcquireByteBuffer()
writeJSONString(bb, s)
bb.B = appendJSONString(bb.B[:0], s, false)
result := string(bb.B)
ReleaseByteBuffer(bb)

Expand Down
18 changes: 14 additions & 4 deletions writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,13 @@ func (w *QWriter) FPrec(f float64, prec int) {

// Q writes quoted json-safe s to w.
func (w *QWriter) Q(s string) {
w.Write(strQuote)
writeJSONString(w, s)
w.Write(strQuote)
bb, ok := w.w.(*ByteBuffer)
if ok {
bb.B = appendJSONString(bb.B, s, true)
} else {
w.b = appendJSONString(w.b[:0], s, true)
w.Write(w.b)
}
}

var strQuote = []byte(`"`)
Expand All @@ -167,7 +171,13 @@ func (w *QWriter) QZ(z []byte) {
//
// Unlike Q it doesn't qoute resulting s.
func (w *QWriter) J(s string) {
writeJSONString(w, s)
bb, ok := w.w.(*ByteBuffer)
if ok {
bb.B = appendJSONString(bb.B, s, false)
} else {
w.b = appendJSONString(w.b[:0], s, false)
w.Write(w.b)
}
}

// JZ writes json-safe z to w.
Expand Down
4 changes: 2 additions & 2 deletions writer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestWriter(t *testing.T) {
expectedS := "<a></a>123'\"foo\"ds1.23%D0%B0%D0%B1%D0%B2{}aaa\"asadf\"asdabc" +
"&lt;a&gt;&lt;/a&gt;321&#39;&quot;foo&quot;ds1.23%D0%B0%D0%B1%D0%B2{}aaa&quot;asadf&quot;asdabc"
if string(bb.B) != expectedS {
t.Fatalf("unexpected output: %q. Expecting %q", bb.B, expectedS)
t.Fatalf("unexpected output:\n%q\nExpecting\n%q", bb.B, expectedS)
}

ReleaseByteBuffer(bb)
Expand Down Expand Up @@ -198,7 +198,7 @@ func testQWriter(t *testing.T, f func(wn, we *QWriter) (expectedS string)) {
ReleaseWriter(qw)

if string(bb.B) != expectedS {
t.Fatalf("unexpected output: %q. Expecting %q", bb.B, expectedS)
t.Fatalf("unexpected output:\n%q\nExpecting\n%q", bb.B, expectedS)
}

ReleaseByteBuffer(bb)
Expand Down

0 comments on commit 7dd50ac

Please sign in to comment.