gojay

high performance JSON encoder/decoder with stream API for Golang
git clone git://git.lair.cx/gojay
Log | Files | Refs | README | LICENSE

commit 002972f789c10d5804f875d7a803c245542ebc9d
parent c135fe6615f52b48d922c0a49c88a78bca7253a3
Author: francoispqt <francois@parquet.ninja>
Date:   Sun, 13 May 2018 23:16:42 +0800

update escaping sequences for both decoding and encoding

Diffstat:
MREADME.md | 36++++++++++++++++++++----------------
Mdecode_object_test.go | 8++++----
Mdecode_string.go | 56++++++++++++++++++++++++++++++++++++++++++++++++++------
Mdecode_string_test.go | 59++++++++++++++++++++++++++++++++++++++++++++++-------------
Mencode.go | 7++++---
Mencode_builder.go | 20++++++++++++--------
Mencode_string.go | 20++++++++++++--------
Mencode_string_test.go | 4++--
8 files changed, 150 insertions(+), 60 deletions(-)

diff --git a/README.md b/README.md @@ -681,11 +681,12 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | | ns/op | bytes/op | allocs/op | |-------------|-------|--------------|-----------| -| Std Library | 4661 | 496 | 12 | -| JsonParser | 1313 | 0 | 0 | -| JsonIter | 899 | 192 | 5 | +| Std Library | 2547 | 496 | 4 | +| JsonIter | 2046 | 312 | 12 | +| JsonParser | 1408 | 0 | 0 | | EasyJson | 929 | 240 | 2 | -| GoJay | 662 | 112 | 1 | +| GoJay | 807 | 256 | 2 | +| GoJay-unsafe| 712 | 112 | 1 | ### Medium Payload [benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/decoder/decoder_bench_medium_test.go) @@ -695,10 +696,11 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | | ns/op | bytes/op | allocs/op | |-------------|-------|--------------|-----------| | Std Library | 30148 | 2152 | 496 | +| JsonIter | 16309 | 2976 | 80 | | JsonParser | 7793 | 0 | 0 | | EasyJson | 7957 | 232 | 6 | -| JsonIter | 5967 | 496 | 44 | -| GoJay | 3914 | 128 | 7 | +| GoJay | 4984 | 2448 | 8 | +| GoJay-unsafe| 4809 | 144 | 7 | ### Large Payload [benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/decoder/decoder_bench_large_test.go) @@ -707,10 +709,11 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | | ns/op | bytes/op | allocs/op | |-------------|-------|--------------|-----------| +| JsonIter | 210078| 41712 | 1136 | | EasyJson | 106626| 160 | 2 | | JsonParser | 66813 | 0 | 0 | -| JsonIter | 87994 | 6738 | 329 | -| GoJay | 43402 | 1408 | 76 | +| GoJay | 52153 | 31241 | 77 | +| GoJay-unsafe| 48277 | 2561 | 76 | ## Encode @@ -726,7 +729,8 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | Std Library | 1280 | 464 | 3 | | EasyJson | 871 | 944 | 6 | | JsonIter | 866 | 272 | 3 | -| GoJay | 484 | 320 | 2 | +| GoJay | 543 | 112 | 1 | +| GoJay-func | 347 | 0 | 0 | ### Medium Struct [benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/encoder/encoder_bench_medium_test.go) @@ -735,10 +739,10 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | | ns/op | bytes/op | allocs/op | |-------------|-------|--------------|-----------| -| Std Library | 3325 | 1496 | 18 | -| EasyJson | 1997 | 1320 | 19 | -| JsonIter | 1939 | 648 | 16 | -| GoJay | 1196 | 936 | 16 | +| Std Library | 5006 | 1496 | 25 | +| JsonIter | 2232 | 1544 | 20 | +| EasyJson | 1997 | 1544 | 19 | +| GoJay | 1522 | 312 | 14 | ### Large Struct [benchmark code is here](https://github.com/francoispqt/gojay/blob/master/benchmarks/encoder/encoder_bench_large_test.go) @@ -747,10 +751,10 @@ cd $GOPATH/src/github.com/francoispqt/gojay/benchmarks/encoder && make bench | | ns/op | bytes/op | allocs/op | |-------------|-------|--------------|-----------| -| Std Library | 51317 | 28704 | 326 | -| JsonIter | 35247 | 14608 | 320 | +| Std Library | 66441 | 20576 | 332 | +| JsonIter | 35247 | 20255 | 328 | | EasyJson | 32053 | 15474 | 327 | -| GoJay | 27847 | 27888 | 326 | +| GoJay | 27847 | 9802 | 318 | # Contributing diff --git a/decode_object_test.go b/decode_object_test.go @@ -158,7 +158,7 @@ func TestDecodeObjectNull(t *testing.T) { var jsonComplex = []byte(`{ "test": "{\"test\":\"1\",\"test1\":2}", - "test2\\n": "\\\\\\\\\\\n", + "test2\\n": "\\\\\\\\\\n", "testArrSkip": ["testString with escaped \\\" quotes"], "testSkipString": "skip \\ string with \\n escaped char \" ", "testSkipObject": { @@ -200,7 +200,7 @@ func (j *jsonObjectComplex) UnmarshalObject(dec *Decoder, key string) error { switch key { case "test": return dec.AddString(&j.Test) - case `test2\n`: + case "test2\n": return dec.AddString(&j.Test2) case "test3": return dec.AddInt(&j.Test3) @@ -224,9 +224,9 @@ func TestDecodeObjComplex(t *testing.T) { result := jsonObjectComplex{} err := UnmarshalObject(jsonComplex, &result) assert.NotNil(t, err, "err should not be as invalid type as been encountered nil") - assert.Equal(t, `Cannot unmarshal to struct, wrong char '"' found at pos 643`, err.Error(), "err should not be as invalid type as been encountered nil") + assert.Equal(t, `Cannot unmarshal to struct, wrong char '"' found at pos 639`, err.Error(), "err should not be as invalid type as been encountered nil") assert.Equal(t, `{"test":"1","test1":2}`, result.Test, "result.Test is not expected value") - assert.Equal(t, `\\\\\\n`, result.Test2, "result.Test2 is not expected value") + assert.Equal(t, "\\\\\\\\\n", result.Test2, "result.Test2 is not expected value") assert.Equal(t, 1, result.Test3, "result.test3 is not expected value") assert.Equal(t, `{"test":"1","test1":2}`, result.testSub.Test, "result.testSub.test is not expected value") assert.Equal(t, `[1,2,3]`, result.testSub.Test2, "result.testSub.test2 is not expected value") diff --git a/decode_string.go b/decode_string.go @@ -77,21 +77,65 @@ func (dec *Decoder) parseEscapedString() error { dec.length = len(dec.data) dec.cursor -= nSlash - diff return nil - case 'n', 'r', 't': + case 'b': // number of slash must be even // if is odd number of slashes // divide nSlash - 1 by 2 and leave last one // else divide nSlash by 2 and leave the letter var diff int - if nSlash&1 == 1 { - diff = (nSlash - 1) >> 1 - dec.data = append(dec.data[:start+diff], dec.data[dec.cursor-1:]...) + if nSlash&1 != 0 { + return InvalidJSONError("Invalid JSON unescaped character") } else { diff = nSlash >> 1 - dec.data = append(dec.data[:start+diff-1], dec.data[dec.cursor-1:]...) + dec.data = append(append(dec.data[:start+diff-2], '\b'), dec.data[dec.cursor:]...) } dec.length = len(dec.data) - dec.cursor -= nSlash - diff + dec.cursor -= nSlash - diff + 1 + return nil + case 'n': + // number of slash must be even + // if is odd number of slashes + // divide nSlash - 1 by 2 and leave last one + // else divide nSlash by 2 and leave the letter + var diff int + if nSlash&1 != 0 { + return InvalidJSONError("Invalid JSON unescaped character") + } else { + diff = nSlash >> 1 + dec.data = append(append(dec.data[:start+diff-2], '\n'), dec.data[dec.cursor:]...) + } + dec.length = len(dec.data) + dec.cursor -= nSlash - diff + 1 + return nil + case 'r': + // number of slash must be even + // if is odd number of slashes + // divide nSlash - 1 by 2 and leave last one + // else divide nSlash by 2 and leave the letter + var diff int + if nSlash&1 != 0 { + return InvalidJSONError("Invalid JSON unescaped character") + } else { + diff = nSlash >> 1 + dec.data = append(append(dec.data[:start+diff-2], '\r'), dec.data[dec.cursor:]...) + } + dec.length = len(dec.data) + dec.cursor -= nSlash - diff + 1 + return nil + case 't': + // number of slash must be even + // if is odd number of slashes + // divide nSlash - 1 by 2 and leave last one + // else divide nSlash by 2 and leave the letter + var diff int + if nSlash&1 != 0 { + return InvalidJSONError("Invalid JSON unescaped character") + } else { + diff = nSlash >> 1 + dec.data = append(append(dec.data[:start+diff-2], '\t'), dec.data[dec.cursor:]...) + } + dec.length = len(dec.data) + dec.cursor -= nSlash - diff + 1 return nil default: // nSlash must be even diff --git a/decode_string_test.go b/decode_string_test.go @@ -40,7 +40,7 @@ func TestDecoderStringComplex(t *testing.T) { var v string err := Unmarshal(json, &v) assert.Nil(t, err, "Err must be nil") - assert.Equal(t, "string with spaces and \"escape\"d \"quotes\" and escaped line returns \\n and escaped \\\\ escaped char", v, "v is not equal to the value expected") + assert.Equal(t, "string with spaces and \"escape\"d \"quotes\" and escaped line returns \n and escaped \\\\ escaped char", v, "v is not equal to the value expected") } func TestDecoderStringNull(t *testing.T) { @@ -137,6 +137,36 @@ func TestParseEscapedString(t *testing.T) { }{ { name: "escape quote err", + json: `"test string \" escaped"`, + expectedResult: `test string " escaped`, + err: false, + }, + { + name: "escape quote err2", + json: `"test string \\t escaped"`, + expectedResult: "test string \t escaped", + err: false, + }, + { + name: "escape quote err2", + json: `"test string \\r escaped"`, + expectedResult: "test string \r escaped", + err: false, + }, + { + name: "escape quote err2", + json: `"test string \\b escaped"`, + expectedResult: "test string \b escaped", + err: false, + }, + { + name: "escape quote err", + json: `"test string \\n escaped"`, + expectedResult: "test string \n escaped", + err: false, + }, + { + name: "escape quote err", json: `"test string \\" escaped"`, expectedResult: ``, err: true, @@ -152,20 +182,23 @@ func TestParseEscapedString(t *testing.T) { } for _, testCase := range testCases { - str := "" - dec := NewDecoder(strings.NewReader(testCase.json)) - err := dec.Decode(&str) - if testCase.err { - assert.NotNil(t, err, "err should not be nil") - if testCase.errType != nil { - assert.IsType(t, testCase.errType, err, "err should be of expected type") + t.Run(testCase.name, func(t *testing.T) { + str := "" + dec := NewDecoder(strings.NewReader(testCase.json)) + err := dec.Decode(&str) + if testCase.err { + assert.NotNil(t, err, "err should not be nil") + if testCase.errType != nil { + assert.IsType(t, testCase.errType, err, "err should be of expected type") + } + log.Print(err) + } else { + assert.Nil(t, err, "err should be nil") } - log.Print(err) - } else { - assert.Nil(t, err, "err should be nil") - } - assert.Equal(t, testCase.expectedResult, str, fmt.Sprintf("str should be equal to '%s'", testCase.expectedResult)) + assert.Equal(t, testCase.expectedResult, str, fmt.Sprintf("str should be equal to '%s'", testCase.expectedResult)) + }) } + } func TestSkipString(t *testing.T) { diff --git a/encode.go b/encode.go @@ -29,7 +29,8 @@ import ( // fmt.Println(b) // {"id":123456} // } func MarshalObject(v MarshalerObject) ([]byte, error) { - enc := newEncoder() + enc := BorrowEncoder(nil) + enc.grow(512) defer enc.Release() return enc.encodeObject(v) } @@ -56,8 +57,8 @@ func MarshalObject(v MarshalerObject) ([]byte, error) { // fmt.Println(b) // [{"id":123456},{"id":7890}] // } func MarshalArray(v MarshalerArray) ([]byte, error) { - enc := newEncoder() - enc.grow(200) + enc := BorrowEncoder(nil) + enc.grow(512) enc.writeByte('[') v.(MarshalerArray).MarshalArray(enc) enc.writeByte(']') diff --git a/encode_builder.go b/encode_builder.go @@ -17,6 +17,10 @@ func (enc *Encoder) writeBytes(p []byte) { enc.buf = append(enc.buf, p...) } +func (enc *Encoder) writeTwoBytes(b1 byte, b2 byte) { + enc.buf = append(enc.buf, b1, b2) +} + // WriteByte appends the byte c to b's Buffer. // The returned error is always nil. func (enc *Encoder) writeByte(c byte) { @@ -34,17 +38,17 @@ func (enc *Encoder) writeStringEscape(s string) { for i := 0; i < l; i++ { switch s[i] { case '\\', '"': - enc.writeByte('\\') - enc.writeByte(s[i]) + enc.writeTwoBytes('\\', s[i]) case '\n': - enc.writeByte('\\') - enc.writeByte('n') + enc.writeTwoBytes('\\', 'n') + case '\f': + enc.writeTwoBytes('\\', 'f') + case '\b': + enc.writeTwoBytes('\\', 'b') case '\r': - enc.writeByte('\\') - enc.writeByte('r') + enc.writeTwoBytes('\\', 'r') case '\t': - enc.writeByte('\\') - enc.writeByte('t') + enc.writeTwoBytes('\\', 't') default: enc.writeByte(s[i]) } diff --git a/encode_string.go b/encode_string.go @@ -35,9 +35,10 @@ func (enc *Encoder) AddString(v string) { enc.grow(len(v) + 4) r := enc.getPreviousRune() if r != '[' { - enc.writeByte(',') + enc.writeTwoBytes(',', '"') + } else { + enc.writeByte('"') } - enc.writeByte('"') enc.writeStringEscape(v) enc.writeByte('"') } @@ -50,9 +51,10 @@ func (enc *Encoder) AddStringOmitEmpty(v string) { } r := enc.getPreviousRune() if r != '[' { - enc.writeByte(',') + enc.writeTwoBytes(',', '"') + } else { + enc.writeByte('"') } - enc.writeByte('"') enc.writeStringEscape(v) enc.writeByte('"') } @@ -62,9 +64,10 @@ func (enc *Encoder) AddStringKey(key, v string) { enc.grow(len(key) + len(v) + 5) r := enc.getPreviousRune() if r != '{' { - enc.writeByte(',') + enc.writeTwoBytes(',', '"') + } else { + enc.writeByte('"') } - enc.writeByte('"') enc.writeStringEscape(key) enc.writeBytes(objKeyStr) enc.writeStringEscape(v) @@ -80,9 +83,10 @@ func (enc *Encoder) AddStringKeyOmitEmpty(key, v string) { enc.grow(len(key) + len(v) + 5) r := enc.getPreviousRune() if r != '{' { - enc.writeByte(',') + enc.writeTwoBytes(',', '"') + } else { + enc.writeByte('"') } - enc.writeByte('"') enc.writeStringEscape(key) enc.writeBytes(objKeyStr) enc.writeStringEscape(v) diff --git a/encode_string_test.go b/encode_string_test.go @@ -22,11 +22,11 @@ func TestEncoderStringEncodeAPI(t *testing.T) { t.Run("utf8", func(t *testing.T) { builder := &strings.Builder{} enc := NewEncoder(builder) - err := enc.EncodeString("漢字") + err := enc.EncodeString("漢字𩸽") assert.Nil(t, err, "Error should be nil") assert.Equal( t, - `"漢字"`, + `"漢字𩸽"`, builder.String(), "Result of marshalling is different as the one expected") })