Skip to content

Commit bdecf22

Browse files
authored
Merge pull request #1 from panther-labs/pap-append-printable
Cherry pick valyala/pull/87
2 parents 93f67d9 + b4ef4ca commit bdecf22

File tree

2 files changed

+92
-21
lines changed

2 files changed

+92
-21
lines changed

parser.go

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -267,28 +267,41 @@ func parseObject(s string, c *cache, depth int) (*Value, string, error) {
267267
}
268268

269269
func escapeString(dst []byte, s string) []byte {
270-
if !hasSpecialChars(s) {
271-
// Fast path - nothing to escape.
272-
dst = append(dst, '"')
273-
dst = append(dst, s...)
274-
dst = append(dst, '"')
275-
return dst
276-
}
277-
278-
// Slow path.
279-
return strconv.AppendQuote(dst, s)
280-
}
281-
282-
func hasSpecialChars(s string) bool {
283-
if strings.IndexByte(s, '"') >= 0 || strings.IndexByte(s, '\\') >= 0 {
284-
return true
285-
}
270+
dst = append(dst, '"')
286271
for i := 0; i < len(s); i++ {
287-
if s[i] < 0x20 {
288-
return true
289-
}
290-
}
291-
return false
272+
c := s[i]
273+
switch {
274+
case c == '"':
275+
// quotation mark
276+
dst = append(dst, []byte{'\\', '"'}...)
277+
case c == '\\':
278+
// reverse solidus
279+
dst = append(dst, []byte{'\\', '\\'}...)
280+
case c >= 0x20:
281+
// default, rest below are control chars
282+
dst = append(dst, c)
283+
case c == 0x08:
284+
dst = append(dst, []byte{'\\', 'b'}...)
285+
case c < 0x09:
286+
dst = append(dst, []byte{'\\', 'u', '0', '0', '0', '0' + c}...)
287+
case c == 0x09:
288+
dst = append(dst, []byte{'\\', 't'}...)
289+
case c == 0x0a:
290+
dst = append(dst, []byte{'\\', 'n'}...)
291+
case c == 0x0c:
292+
dst = append(dst, []byte{'\\', 'f'}...)
293+
case c == 0x0d:
294+
dst = append(dst, []byte{'\\', 'r'}...)
295+
case c < 0x10:
296+
dst = append(dst, []byte{'\\', 'u', '0', '0', '0', 0x57 + c}...)
297+
case c < 0x1a:
298+
dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x20 + c}...)
299+
case c < 0x20:
300+
dst = append(dst, []byte{'\\', 'u', '0', '0', '1', 0x47 + c}...)
301+
}
302+
}
303+
dst = append(dst, '"')
304+
return dst
292305
}
293306

294307
func unescapeStringBestEffort(s string) string {

parser_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package fastjson
22

33
import (
4+
"bytes"
5+
"encoding/json"
46
"fmt"
57
"math"
68
"strings"
@@ -1275,3 +1277,59 @@ func testParseGetSerial(s string) error {
12751277
}
12761278
return nil
12771279
}
1280+
1281+
// Tests for https://github.com/valyala/fastjson/issues/90
1282+
// This was manifesting due to the use of strconv.AppendQuote
1283+
func TestUTF8NonPrintableArtifacts(t *testing.T) {
1284+
testCases := []struct {
1285+
name string
1286+
s string
1287+
}{
1288+
{
1289+
name: "problematic bytes",
1290+
s: "data:\"\\xd6`\\xb76d\\xf6E\U000E8737(\\x91\\xb294\"",
1291+
},
1292+
}
1293+
1294+
for _, tc := range testCases {
1295+
t.Run(tc.name, func(t *testing.T) {
1296+
jsonEnvelope := struct {
1297+
Inner string
1298+
}{
1299+
Inner: tc.s,
1300+
}
1301+
1302+
m, err := json.Marshal(jsonEnvelope) // `m` is a full valid json
1303+
if err != nil {
1304+
t.Fatalf("unexpected error: %s", err)
1305+
}
1306+
1307+
// we will pass that `m` json through a fastjson marshal/unmarshal cycle
1308+
1309+
fastjsonValue, err := ParseBytes(m)
1310+
if err != nil {
1311+
t.Fatalf("unexpected error: %s", err)
1312+
}
1313+
1314+
o, err := fastjsonValue.Object()
1315+
if err != nil {
1316+
t.Fatalf("unexpected error: %s", err)
1317+
}
1318+
1319+
// In order to trigger the bug we need to visit all the keys and call Type() on them
1320+
o.Visit(func(k []byte, v *Value) {
1321+
v.Type()
1322+
})
1323+
1324+
res := fastjsonValue.MarshalTo(nil)
1325+
if !bytes.Equal(res, m) {
1326+
t.Fatalf("unexpected result; got %q; want %q", res, m)
1327+
}
1328+
1329+
err = ValidateBytes(res)
1330+
if err != nil {
1331+
t.Fatalf("unexpected error: %s", err)
1332+
}
1333+
})
1334+
}
1335+
}

0 commit comments

Comments
 (0)