diff --git a/decode_bool.go b/decode_bool.go index de1edc5..84d7f4e 100644 --- a/decode_bool.go +++ b/decode_bool.go @@ -34,7 +34,7 @@ func trueBytes(s *stream) error { func falseBytes(s *stream) error { if s.cursor+4 >= s.length { - if s.read() { + if !s.read() { return errInvalidCharacter(s.char(), "bool(false)", s.totalOffset()) } } diff --git a/decode_stream.go b/decode_stream.go index ed63bbc..b714f9f 100644 --- a/decode_stream.go +++ b/decode_stream.go @@ -67,21 +67,25 @@ func (s *stream) read() bool { if n < readChunkSize || err == io.EOF { s.allRead = true } - totalSize := s.length + int64(n) + 1 + // extend buffer (2) is protect ( s.cursor++ x2 ) + // e.g.) line 85 in decode_interface.go + const extendBufLength = int64(2) + + totalSize := s.length + int64(n) + extendBufLength if totalSize > readChunkSize { newBuf := make([]byte, totalSize) copy(newBuf, s.buf) copy(newBuf[s.length:], buf) s.buf = newBuf - s.length = totalSize - 1 + s.length = totalSize - extendBufLength } else if s.length > 0 { copy(buf[s.length:], buf) copy(buf, s.buf[:s.length]) s.buf = buf - s.length = totalSize - 1 + s.length = totalSize - extendBufLength } else { s.buf = buf - s.length = totalSize - 1 + s.length = totalSize - extendBufLength } s.offset += s.cursor if n == 0 { diff --git a/encode_vm.go b/encode_vm.go index a591a8d..2dee165 100644 --- a/encode_vm.go +++ b/encode_vm.go @@ -84,9 +84,19 @@ func (e *Encoder) run(code *opcode) error { typ = typ.Elem() } e.indent = ifaceCode.indent - c, err := e.compile(typ, ifaceCode.root, e.enabledIndent) - if err != nil { - return err + var c *opcode + if typ.Kind() == reflect.Map { + code, err := e.compileMap(typ, false, ifaceCode.root, e.enabledIndent) + if err != nil { + return err + } + c = code + } else { + code, err := e.compile(typ, ifaceCode.root, e.enabledIndent) + if err != nil { + return err + } + c = code } c.ptr = uintptr(header.ptr) c.beforeLastCode().next = code.next diff --git a/export_test.go b/export_test.go new file mode 100644 index 0000000..bcb307d --- /dev/null +++ b/export_test.go @@ -0,0 +1,8 @@ +package json + +func NewSyntaxError(msg string, offset int64) *SyntaxError { + return &SyntaxError{ + msg: msg, + Offset: offset, + } +} diff --git a/json_test.go b/json_test.go index 1458f03..a61c4e0 100644 --- a/json_test.go +++ b/json_test.go @@ -2,6 +2,9 @@ package json_test import ( "bytes" + "math" + "math/rand" + "reflect" "testing" "github.com/goccy/go-json" @@ -116,3 +119,181 @@ func TestIndent(t *testing.T) { } } } + +// Tests of a large random structure. + +func TestCompactBig(t *testing.T) { + initBig() + var buf bytes.Buffer + if err := json.Compact(&buf, jsonBig); err != nil { + t.Fatalf("Compact: %v", err) + } + b := buf.Bytes() + if !bytes.Equal(b, jsonBig) { + t.Error("Compact(jsonBig) != jsonBig") + diff(t, b, jsonBig) + return + } +} + +func TestIndentBig(t *testing.T) { + t.Parallel() + initBig() + var buf bytes.Buffer + if err := json.Indent(&buf, jsonBig, "", "\t"); err != nil { + t.Fatalf("Indent1: %v", err) + } + b := buf.Bytes() + if len(b) == len(jsonBig) { + // jsonBig is compact (no unnecessary spaces); + // indenting should make it bigger + t.Fatalf("Indent(jsonBig) did not get bigger") + } + + // should be idempotent + var buf1 bytes.Buffer + if err := json.Indent(&buf1, b, "", "\t"); err != nil { + t.Fatalf("Indent2: %v", err) + } + b1 := buf1.Bytes() + if !bytes.Equal(b1, b) { + t.Error("Indent(Indent(jsonBig)) != Indent(jsonBig)") + diff(t, b1, b) + return + } + + // should get back to original + buf1.Reset() + if err := json.Compact(&buf1, b); err != nil { + t.Fatalf("Compact: %v", err) + } + b1 = buf1.Bytes() + if !bytes.Equal(b1, jsonBig) { + t.Error("Compact(Indent(jsonBig)) != jsonBig") + diff(t, b1, jsonBig) + return + } +} + +type indentErrorTest struct { + in string + err error +} + +var indentErrorTests = []indentErrorTest{ + {`{"X": "foo", "Y"}`, json.NewSyntaxError("invalid character '}' after object key", 17)}, + {`{"X": "foo" "Y": "bar"}`, json.NewSyntaxError("invalid character '\"' after object key:value pair", 13)}, +} + +func TestIndentErrors(t *testing.T) { + for i, tt := range indentErrorTests { + slice := make([]uint8, 0) + buf := bytes.NewBuffer(slice) + if err := json.Indent(buf, []uint8(tt.in), "", ""); err != nil { + if !reflect.DeepEqual(err, tt.err) { + t.Errorf("#%d: Indent: %#v", i, err) + continue + } + } + } +} + +func diff(t *testing.T, a, b []byte) { + for i := 0; ; i++ { + if i >= len(a) || i >= len(b) || a[i] != b[i] { + j := i - 10 + if j < 0 { + j = 0 + } + t.Errorf("diverge at %d: «%s» vs «%s»", i, trim(a[j:]), trim(b[j:])) + return + } + } +} + +func trim(b []byte) []byte { + if len(b) > 20 { + return b[0:20] + } + return b +} + +// Generate a random JSON object. + +var jsonBig []byte + +func initBig() { + n := 10000 + if testing.Short() { + n = 100 + } + v := genValue(n) + b, err := json.Marshal(v) + if err != nil { + panic(err) + } + jsonBig = b +} + +func genValue(n int) interface{} { + if n > 1 { + switch rand.Intn(2) { + case 0: + return genArray(n) + case 1: + return genMap(n) + } + } + switch rand.Intn(3) { + case 0: + return rand.Intn(2) == 0 + case 1: + return rand.NormFloat64() + case 2: + return genString(30) + } + panic("unreachable") +} + +func genString(stddev float64) string { + n := int(math.Abs(rand.NormFloat64()*stddev + stddev/2)) + c := make([]rune, n) + for i := range c { + f := math.Abs(rand.NormFloat64()*64 + 32) + if f > 0x10ffff { + f = 0x10ffff + } + c[i] = rune(f) + } + return string(c) +} + +func genArray(n int) []interface{} { + f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) + if f > n { + f = n + } + if f < 1 { + f = 1 + } + x := make([]interface{}, f) + for i := range x { + x[i] = genValue(((i+1)*n)/f - (i*n)/f) + } + return x +} + +func genMap(n int) map[string]interface{} { + f := int(math.Abs(rand.NormFloat64()) * math.Min(10, float64(n/2))) + if f > n { + f = n + } + if n > 0 && f == 0 { + f = 1 + } + x := make(map[string]interface{}) + for i := 0; i < f; i++ { + x[genString(10)] = genValue(((i+1)*n)/f - (i*n)/f) + } + return x +}