diff --git a/bench_test.go b/bench_test.go index 4c2e21b..537eceb 100644 --- a/bench_test.go +++ b/bench_test.go @@ -27,13 +27,17 @@ func runTestdata(fatal func(...interface{}), cb func(name string, data []byte)) if e.IsDir() { continue } - name := path.Join("testdata", e.Name()) - data, err := testdata.ReadFile(name) - if err != nil { - fatal(err) - } - cb(e.Name(), data) + runTestdataFile(e.Name(), fatal, cb) + } +} + +func runTestdataFile(file string, fatal func(...interface{}), cb func(name string, data []byte)) { + name := path.Join("testdata", file) + data, err := testdata.ReadFile(name) + if err != nil { + fatal(err) } + cb(file, data) } func BenchmarkFile_Decode(b *testing.B) { diff --git a/dec.go b/dec.go index 6ac1b7f..5b0deb7 100644 --- a/dec.go +++ b/dec.go @@ -2,8 +2,6 @@ package jx import ( "io" - - "github.com/go-faster/errors" ) // Type of json value. @@ -157,144 +155,3 @@ func (d *Decoder) ResetBytes(input []byte) { d.buf = input } - -// Next gets Type of relatively next json element -func (d *Decoder) Next() Type { - v, _ := d.next() - d.unread() - return types[v] -} - -var spaceSet = [256]byte{ - ' ': 1, '\n': 1, '\t': 1, '\r': 1, -} - -func (d *Decoder) consume(c byte) (err error) { - for { - buf := d.buf[d.head:d.tail] - for i, got := range buf { - switch spaceSet[got] { - default: - d.head += i + 1 - if c != got { - return badToken(got) - } - return nil - case 1: - continue - } - } - if err = d.read(); err != nil { - if err == io.EOF { - return io.ErrUnexpectedEOF - } - return err - } - } -} - -// more is next but io.EOF is unexpected. -func (d *Decoder) more() (byte, error) { - c, err := d.next() - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - return c, err -} - -// next reads next non-whitespace token or error. -func (d *Decoder) next() (byte, error) { - for { - buf := d.buf[d.head:d.tail] - for i, c := range buf { - switch spaceSet[c] { - default: - d.head += i + 1 - return c, nil - case 1: - continue - } - } - if err := d.read(); err != nil { - return 0, err - } - } -} - -func (d *Decoder) byte() (byte, error) { - if d.head == d.tail { - err := d.read() - if err == io.EOF { - err = io.ErrUnexpectedEOF - } - if err != nil { - return 0, err - } - } - c := d.buf[d.head] - d.head++ - return c, nil -} - -func (d *Decoder) read() error { - if d.reader == nil { - d.head = d.tail - return io.EOF - } - - n, err := d.reader.Read(d.buf) - if err != nil { - return err - } - - d.head = 0 - d.tail = n - return nil -} - -func (d *Decoder) readAtLeast(min int) error { - if d.reader == nil { - d.head = d.tail - return io.ErrUnexpectedEOF - } - - if need := min - len(d.buf); need > 0 { - d.buf = append(d.buf, make([]byte, need)...) - } - n, err := io.ReadAtLeast(d.reader, d.buf, min) - if err != nil { - if err == io.EOF && n == 0 { - return io.ErrUnexpectedEOF - } - return err - } - - d.head = 0 - d.tail = n - return nil -} - -func (d *Decoder) unread() { d.head-- } - -// limit maximum depth of nesting, as allowed by https://tools.ietf.org/html/rfc7159#section-9 -const maxDepth = 10000 - -var errMaxDepth = errors.New("depth: maximum") - -func (d *Decoder) incDepth() error { - d.depth++ - if d.depth > maxDepth { - return errMaxDepth - } - return nil -} - -var errNegativeDepth = errors.New("depth: negative") - -func (d *Decoder) decDepth() error { - d.depth-- - if d.depth < 0 { - return errNegativeDepth - } - return nil -} diff --git a/dec_bool.go b/dec_bool.go new file mode 100644 index 0000000..5795306 --- /dev/null +++ b/dec_bool.go @@ -0,0 +1,34 @@ +package jx + +// Bool reads a json object as Bool +func (d *Decoder) Bool() (bool, error) { + var buf [4]byte + if err := d.readExact4(&buf); err != nil { + return false, err + } + + switch string(buf[:]) { + case "true": + return true, nil + case "fals": + c, err := d.byte() + if err != nil { + return false, err + } + if c != 'e' { + return false, badToken(c) + } + return false, nil + default: + switch c := buf[0]; c { + case 't': + const encodedTrue = 't' | 'r'<<8 | 'u'<<16 | 'e'<<24 + return false, findInvalidToken4(buf, encodedTrue) + case 'f': + const encodedAlse = 'a' | 'l'<<8 | 's'<<16 | 'e'<<24 + return false, findInvalidToken4(buf, encodedAlse) + default: + return false, badToken(c) + } + } +} diff --git a/dec_bool_test.go b/dec_bool_test.go new file mode 100644 index 0000000..740ccfe --- /dev/null +++ b/dec_bool_test.go @@ -0,0 +1,10 @@ +package jx + +import "testing" + +func TestDecoder_Bool(t *testing.T) { + runTestCases(t, testBools, func(t *testing.T, d *Decoder) error { + _, err := d.Bool() + return err + }) +} diff --git a/dec_depth.go b/dec_depth.go new file mode 100644 index 0000000..82b1335 --- /dev/null +++ b/dec_depth.go @@ -0,0 +1,26 @@ +package jx + +import "github.com/go-faster/errors" + +// limit maximum depth of nesting, as allowed by https://tools.ietf.org/html/rfc7159#section-9 +const maxDepth = 10000 + +var errMaxDepth = errors.New("depth: maximum") + +func (d *Decoder) incDepth() error { + d.depth++ + if d.depth > maxDepth { + return errMaxDepth + } + return nil +} + +var errNegativeDepth = errors.New("depth: negative") + +func (d *Decoder) decDepth() error { + d.depth-- + if d.depth < 0 { + return errNegativeDepth + } + return nil +} diff --git a/dec_float.go b/dec_float.go index 0836317..29e9bb4 100644 --- a/dec_float.go +++ b/dec_float.go @@ -16,6 +16,7 @@ var floatDigits []int8 const invalidCharForNumber = int8(-1) const endOfNumber = int8(-2) const dotInNumber = int8(-3) +const maxFloat64 = 1<<63 - 1 func init() { floatDigits = make([]int8, 256) @@ -213,15 +214,19 @@ func (d *Decoder) Float64() (float64, error) { if err != nil { return 0, errors.Wrap(err, "byte") } - if c == '-' { + switch c { + case '-': v, err := d.positiveFloat64() if err != nil { return 0, err } return -v, err + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + d.unread() + return d.positiveFloat64() + default: + return 0, badToken(c) } - d.unread() - return d.positiveFloat64() } func (d *Decoder) positiveFloat64() (float64, error) { @@ -333,8 +338,12 @@ func validateFloat(str []byte) error { if str[0] == '-' { return errors.New("double minus") } - if len(str) >= 2 && str[0] == '0' && str[1] == '0' { - return errors.New("leading zero") + if len(str) >= 2 && str[0] == '0' { + switch str[1] { + case 'e', 'E', '.': + default: + return errors.New("leading zero") + } } dotPos := bytes.IndexByte(str, '.') if dotPos != -1 { diff --git a/dec_float_test.go b/dec_float_test.go index b3f2259..692697f 100644 --- a/dec_float_test.go +++ b/dec_float_test.go @@ -2,6 +2,7 @@ package jx import ( "bytes" + "io" "testing" "github.com/stretchr/testify/require" @@ -40,31 +41,18 @@ func decodeStr(t *testing.T, s string, f func(d *Decoder)) { func TestDecoder_Float(t *testing.T) { t.Run("Invalid", func(t *testing.T) { - for _, s := range []string{ - ``, - `-`, - `-.`, - `.`, - `.-`, - `00`, - `.00`, - `00.1`, - } { - t.Run(s, func(t *testing.T) { - t.Run("64", func(t *testing.T) { - decodeStr(t, s, func(d *Decoder) { - _, err := d.Float64() - require.Error(t, err, s) - }) - }) - t.Run("32", func(t *testing.T) { - decodeStr(t, s, func(d *Decoder) { - _, err := d.Float32() - require.Error(t, err, s) - }) - }) - }) - } + runTestCases(t, testNumbers, func(t *testing.T, d *Decoder) error { + _, err := d.Float64() + if err != nil { + return err + } + if err := d.Skip(); err != nil { + if err != io.EOF && err != io.ErrUnexpectedEOF { + return err + } + } + return nil + }) }) t.Run("Slow", func(t *testing.T) { s := `,0.1` @@ -143,3 +131,25 @@ func TestDecoder_Float64(t *testing.T) { }) } } + +func BenchmarkDecoder_Float64(b *testing.B) { + runTestdataFile("floats.json", b.Fatal, func(name string, data []byte) { + b.Run(name, func(b *testing.B) { + d := GetDecoder() + cb := func(d *Decoder) error { + _, err := d.Float64() + return err + } + b.ReportAllocs() + b.ResetTimer() + + for i := 0; i < b.N; i++ { + d.ResetBytes(data) + + if err := d.Arr(cb); err != nil { + b.Fatal(err) + } + } + }) + }) +} diff --git a/dec_int.go b/dec_int.go index e57cd1c..64eab15 100644 --- a/dec_int.go +++ b/dec_int.go @@ -12,7 +12,6 @@ var intDigits []int8 const uint32SafeToMultiply10 = uint32(0xffffffff)/10 - 1 const uint64SafeToMultiple10 = uint64(0xffffffffffffffff)/10 - 1 -const maxFloat64 = 1<<53 - 1 func init() { intDigits = make([]int8, 256) diff --git a/dec_null.go b/dec_null.go new file mode 100644 index 0000000..6d26d6b --- /dev/null +++ b/dec_null.go @@ -0,0 +1,16 @@ +package jx + +// Null reads a json object as null and +// returns whether it's a null or not. +func (d *Decoder) Null() error { + var buf [4]byte + if err := d.readExact4(&buf); err != nil { + return err + } + + if string(buf[:]) != "null" { + const encodedNull = 'n' | 'u'<<8 | 'l'<<16 | 'l'<<24 + return findInvalidToken4(buf, encodedNull) + } + return nil +} diff --git a/dec_null_test.go b/dec_null_test.go new file mode 100644 index 0000000..74da390 --- /dev/null +++ b/dec_null_test.go @@ -0,0 +1,16 @@ +package jx + +import "testing" + +func TestDecoder_Null(t *testing.T) { + runTestCases(t, []string{ + "", + "nope", + "nul", + "nil", + "nul\x00", + "null", + }, func(t *testing.T, d *Decoder) error { + return d.Null() + }) +} diff --git a/dec_obj_test.go b/dec_obj_test.go index 3bbdd71..8a0216e 100644 --- a/dec_obj_test.go +++ b/dec_obj_test.go @@ -39,7 +39,9 @@ func TestDecoder_ObjectBytes(t *testing.T) { input = append(input, `{"1":`...) } d := DecodeBytes(input) - require.ErrorIs(t, d.ObjBytes(nil), errMaxDepth) + require.ErrorIs(t, d.ObjBytes(func(d *Decoder, key []byte) error { + return crawlValue(d) + }), errMaxDepth) }) t.Run("Invalid", func(t *testing.T) { for _, s := range testObjs { diff --git a/dec_read.go b/dec_read.go new file mode 100644 index 0000000..5d7488e --- /dev/null +++ b/dec_read.go @@ -0,0 +1,144 @@ +package jx + +import ( + "io" + "math/bits" +) + +// Next gets Type of relatively next json element +func (d *Decoder) Next() Type { + v, _ := d.next() + d.unread() + return types[v] +} + +var spaceSet = [256]byte{ + ' ': 1, '\n': 1, '\t': 1, '\r': 1, +} + +func (d *Decoder) consume(c byte) (err error) { + for { + buf := d.buf[d.head:d.tail] + for i, got := range buf { + switch spaceSet[got] { + default: + d.head += i + 1 + if c != got { + return badToken(got) + } + return nil + case 1: + continue + } + } + if err = d.read(); err != nil { + if err == io.EOF { + return io.ErrUnexpectedEOF + } + return err + } + } +} + +// more is next but io.EOF is unexpected. +func (d *Decoder) more() (byte, error) { + c, err := d.next() + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return c, err +} + +// next reads next non-whitespace token or error. +func (d *Decoder) next() (byte, error) { + for { + buf := d.buf[d.head:d.tail] + for i, c := range buf { + switch spaceSet[c] { + default: + d.head += i + 1 + return c, nil + case 1: + continue + } + } + if err := d.read(); err != nil { + return 0, err + } + } +} + +func (d *Decoder) byte() (byte, error) { + if d.head == d.tail { + err := d.read() + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if err != nil { + return 0, err + } + } + c := d.buf[d.head] + d.head++ + return c, nil +} + +func (d *Decoder) read() error { + if d.reader == nil { + d.head = d.tail + return io.EOF + } + + n, err := d.reader.Read(d.buf) + if err != nil { + return err + } + + d.head = 0 + d.tail = n + return nil +} + +func (d *Decoder) readAtLeast(min int) error { + if d.reader == nil { + d.head = d.tail + return io.ErrUnexpectedEOF + } + + if need := min - len(d.buf); need > 0 { + d.buf = append(d.buf, make([]byte, need)...) + } + n, err := io.ReadAtLeast(d.reader, d.buf, min) + if err != nil { + if err == io.EOF && n == 0 { + return io.ErrUnexpectedEOF + } + return err + } + + d.head = 0 + d.tail = n + return nil +} + +func (d *Decoder) unread() { d.head-- } + +func (d *Decoder) readExact4(b *[4]byte) error { + if buf := d.buf[d.head:d.tail]; len(buf) >= len(b) { + d.head += copy(b[:], buf[:4]) + return nil + } + + n := copy(b[:], d.buf[d.head:d.tail]) + if err := d.readAtLeast(len(b) - n); err != nil { + return err + } + d.head += copy(b[n:], d.buf[d.head:d.tail]) + return nil +} + +func findInvalidToken4(buf [4]byte, mask uint32) error { + c := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24 + idx := bits.TrailingZeros32(c^mask) / 8 + return badToken(buf[idx]) +} diff --git a/dec_read_test.go b/dec_read_test.go new file mode 100644 index 0000000..6db03e4 --- /dev/null +++ b/dec_read_test.go @@ -0,0 +1,21 @@ +package jx + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDecoder_readAtLeast(t *testing.T) { + a := require.New(t) + d := Decode(strings.NewReader("aboba"), 1) + a.NoError(d.readAtLeast(4)) + a.Equal(d.buf[d.head:d.tail], []byte("abob")) +} + +func TestDecoder_consume(t *testing.T) { + r := errReader{} + d := Decode(r, 1) + require.ErrorIs(t, d.consume('"'), r.Err()) +} diff --git a/dec_skip.go b/dec_skip.go index f954d0c..11aa5ac 100644 --- a/dec_skip.go +++ b/dec_skip.go @@ -2,79 +2,10 @@ package jx import ( "io" - "math/bits" "github.com/go-faster/errors" ) -func (d *Decoder) readExact4(b *[4]byte) error { - if buf := d.buf[d.head:d.tail]; len(buf) >= len(b) { - d.head += copy(b[:], buf[:4]) - return nil - } - - n := copy(b[:], d.buf[d.head:d.tail]) - if err := d.readAtLeast(len(b) - n); err != nil { - return err - } - d.head += copy(b[n:], d.buf[d.head:d.tail]) - return nil -} - -func findInvalidToken4(buf [4]byte, mask uint32) error { - c := uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24 - idx := bits.TrailingZeros32(c^mask) / 8 - return badToken(buf[idx]) -} - -// Null reads a json object as null and -// returns whether it's a null or not. -func (d *Decoder) Null() error { - var buf [4]byte - if err := d.readExact4(&buf); err != nil { - return err - } - - if string(buf[:]) != "null" { - const encodedNull = 'n' | 'u'<<8 | 'l'<<16 | 'l'<<24 - return findInvalidToken4(buf, encodedNull) - } - return nil -} - -// Bool reads a json object as Bool -func (d *Decoder) Bool() (bool, error) { - var buf [4]byte - if err := d.readExact4(&buf); err != nil { - return false, err - } - - switch string(buf[:]) { - case "true": - return true, nil - case "fals": - c, err := d.byte() - if err != nil { - return false, err - } - if c != 'e' { - return false, badToken(c) - } - return false, nil - default: - switch c := buf[0]; c { - case 't': - const encodedTrue = 't' | 'r'<<8 | 'u'<<16 | 'e'<<24 - return false, findInvalidToken4(buf, encodedTrue) - case 'f': - const encodedAlse = 'a' | 'l'<<8 | 's'<<16 | 'e'<<24 - return false, findInvalidToken4(buf, encodedAlse) - default: - return false, badToken(c) - } - } -} - // Skip skips a json object and positions to relatively the next json object. func (d *Decoder) Skip() error { c, err := d.next() @@ -405,6 +336,7 @@ readStr: } readTok: + ; // Bug in cover tool, see https://github.com/golang/go/issues/28319. switch { case c == '"': d.head += i + 1 diff --git a/dec_skip_cases_test.go b/dec_skip_cases_test.go index be041a8..15c036c 100644 --- a/dec_skip_cases_test.go +++ b/dec_skip_cases_test.go @@ -12,6 +12,88 @@ import ( "github.com/stretchr/testify/require" ) +var testBools = []string{ + "", + "tru", + "fals", + "fal\x00e", + "fals\x00", + "f\x00\x00\x00\x00", + "nope", + "true", + "false", +} + +var testNumbers = []string{ + "", // invalid + "0", // valid + "-", // invalid + "--", // invalid + "+", // invalid + ".", // invalid + "e", // invalid + "E", // invalid + "-.", // invalid + "-1", // valid + "--1", // invalid + "+1", // invalid + "++1", // invalid + "-a", // invalid + "-0", // valid + "00", // invalid + "01", // invalid + ".00", // invalid + "00.1", // invalid + "-00", // invalid + "-01", // invalid + "-\x00", // invalid, zero byte + "0.1", // valid + "0e1", // valid + "0e+1", // valid + "0e-1", // valid + "0e-11", // valid + "0e-1a", // invalid + "1.e1", // invalid + "0e-1+", // invalid + "0e", // invalid + "e", // invalid + "-e", // invalid + "+e", // invalid + ".e", // invalid + "e.", // invalid + "0.e", // invalid + "0-e", // invalid + "0e-", // invalid + "0e+", // invalid + "0.0e", // invalid + "0.0e1", // valid + "0.0e+", // invalid + "0.0e-", // invalid + "0e0+0", // invalid + "0.e0+0", // invalid + "0.0e+0", // valid + "0.0e+1", // valid + "0.0e0+0", // invalid + "0.", // invalid + "1.", // invalid + "0..1", // invalid, more dot + "1e+1", // valid + "1+1", // invalid + "1E1", // valid, e or E + "1ee1", // invalid + "100a", // invalid + "10.", // invalid + "-0.12", // valid + "0]", // invalid + "0e]", // invalid + "0e+]", // invalid + "1.2.3", // invalid + "0.0.0", // invalid + "9223372036854775807", // valid + "9223372036854775808", // valid + "9223372036854775807.1", // valid +} + var testStrings = append([]string{ `""`, // valid `"hello"`, // valid @@ -132,75 +214,16 @@ func TestDecoder_Skip(t *testing.T) { var testCases []testCase testCases = append(testCases, testCase{ - ptr: (*bool)(nil), - inputs: []string{ - "tru", - "fals", - "", - "nope", - "true", - "false", - }, + ptr: (*bool)(nil), + inputs: testBools, }) testCases = append(testCases, testCase{ ptr: (*string)(nil), inputs: testStrings, }) numberCase := testCase{ - ptr: (*float64)(nil), - inputs: []string{ - "0", // valid - "-", // invalid - "+", // invalid - "-1", // valid - "+1", // invalid - "-a", // invalid - "-0", // valid - "-00", // invalid - "-01", // invalid - "-\x00", // invalid, zero byte - "0.1", // valid - "0e1", // valid - "0e+1", // valid - "0e-1", // valid - "0e-11", // valid - "0e-1a", // invalid - "1.e1", // invalid - "0e-1+", // invalid - "0e", // invalid - "e", // invalid - "-e", // invalid - "+e", // invalid - ".e", // invalid - "e.", // invalid - "0.e", // invalid - "0-e", // invalid - "0e-", // invalid - "0e+", // invalid - "0.0e", // invalid - "0.0e1", // valid - "0.0e+", // invalid - "0.0e-", // invalid - "0e0+0", // invalid - "0.e0+0", // invalid - "0.0e+0", // valid - "0.0e+1", // valid - "0.0e0+0", // invalid - "0.", // invalid - "0..1", // invalid, more dot - "1e+1", // valid - "1+1", // invalid - "1E1", // valid, e or E - "1ee1", // invalid - "100a", // invalid - "10.", // invalid - "-0.12", // valid - "0]", // invalid - "0e]", // invalid - "0e+]", // invalid - "1.2.3", // invalid - "0.0.0", // invalid - }, + ptr: (*float64)(nil), + inputs: testNumbers, } testCases = append(testCases, numberCase) arrayCase := testCase{ diff --git a/dec_skip_test.go b/dec_skip_test.go index 573cf2c..b1693c3 100644 --- a/dec_skip_test.go +++ b/dec_skip_test.go @@ -1,87 +1,40 @@ package jx import ( + "fmt" + "io" + "strings" "testing" "github.com/stretchr/testify/require" ) -func TestSkip_number_in_array(t *testing.T) { - var err error - a := require.New(t) - d := DecodeStr(`[-0.12, "stream"]`) - _, err = d.Elem() - a.NoError(err) - err = d.Skip() - a.NoError(err) - _, err = d.Elem() - a.NoError(err) - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_string_in_array(t *testing.T) { - d := DecodeStr(`["hello", "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_null(t *testing.T) { - d := DecodeStr(`[null , "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_true(t *testing.T) { - d := DecodeStr(`[true , "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } +func TestDecoder_SkipArrayNested(t *testing.T) { + runTestCases(t, []string{ + `[-0.12, "stream"]`, + `["hello", "stream"]`, + `[null , "stream"]`, + `[true , "stream"]`, + `[false , "stream"]`, + `[[1, [2, [3], 4]], "stream"]`, + `[ [ ], "stream"]`, + }, func(t *testing.T, d *Decoder) error { + var err error + a := require.New(t) + _, err = d.Elem() + a.NoError(err) + err = d.Skip() + a.NoError(err) + _, err = d.Elem() + a.NoError(err) + if s, _ := d.Str(); s != "stream" { + t.FailNow() + } + return nil + }) } -func TestSkip_false(t *testing.T) { - d := DecodeStr(`[false , "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_array(t *testing.T) { - d := DecodeStr(`[[1, [2, [3], 4]], "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_empty_array(t *testing.T) { - d := DecodeStr(`[ [ ], "stream"]`) - d.Elem() - d.Skip() - d.Elem() - if s, _ := d.Str(); s != "stream" { - t.FailNow() - } -} - -func TestSkip_nested(t *testing.T) { +func TestDecoderSkip_Nested(t *testing.T) { d := DecodeStr(`[ {"a" : [{"stream": "c"}], "d": 102 }, "stream"]`) if _, err := d.Elem(); err != nil { t.Fatal(err) @@ -95,33 +48,35 @@ func TestSkip_nested(t *testing.T) { require.Equal(t, "stream", s) } -func TestSkip_simple_nested(t *testing.T) { +func TestDecoderSkip_SimpleNested(t *testing.T) { d := DecodeStr(`["foo", "bar", "baz"]`) require.NoError(t, d.Skip()) } -func TestDecoder_Bool(t *testing.T) { - for _, s := range []string{ - "tru", - "fals", - "", - "nope", - } { - d := DecodeStr(s) - v, err := d.Bool() - require.False(t, v) - require.Error(t, err) +func TestDecoder_skipNumber(t *testing.T) { + inputs := []string{ + `0`, + `120`, + `0.`, + `0.0e`, + `0.0e+1`, + } + sr := strings.NewReader("") + er := &errReader{} + for i, tt := range inputs { + t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) { + sr.Reset(tt) + d := Decode(io.MultiReader(sr, er), len(tt)) + require.NoError(t, d.read()) + require.Error(t, d.skipNumber()) + }) } } -func TestDecoder_Null(t *testing.T) { - for _, s := range []string{ - "", - "nope", - "nul", - "nil", - } { - d := DecodeStr(s) - require.Error(t, d.Null()) +func TestDecoder_SkipObjDepth(t *testing.T) { + var input []byte + for i := 0; i <= maxDepth; i++ { + input = append(input, `{"1":`...) } + require.Error(t, DecodeBytes(input).Skip()) } diff --git a/dec_str.go b/dec_str.go index 95c2f75..84ffd30 100644 --- a/dec_str.go +++ b/dec_str.go @@ -124,6 +124,7 @@ func (d *Decoder) str(v value) (value, error) { return d.strSlow(v) } readTok: + ; // Bug in cover tool, see https://github.com/golang/go/issues/28319. switch { case c == '"': buf := d.buf[d.head:d.tail] diff --git a/dec_str_test.go b/dec_str_test.go index a0a2aff..a49cbf4 100644 --- a/dec_str_test.go +++ b/dec_str_test.go @@ -1,12 +1,10 @@ package jx import ( - "encoding/json" "fmt" "io" "strings" "testing" - "testing/iotest" "unicode/utf8" "github.com/stretchr/testify/require" @@ -36,38 +34,10 @@ func TestUnexpectedTokenErr_Error(t *testing.T) { } func TestDecoder_Str(t *testing.T) { - testStr := func(d *Decoder, input string, valid bool) func(t *testing.T) { - return func(t *testing.T) { - t.Cleanup(func() { - if t.Failed() { - t.Logf("Input: %q", input) - } - }) - - _, err := d.Str() - if valid { - require.NoError(t, err) - } else { - require.Error(t, err) - } - } - } - for i, input := range testStrings { - valid := json.Valid([]byte(input)) - - t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) { - t.Run("Buffer", testStr(DecodeStr(input), input, valid)) - - r := strings.NewReader(input) - d := Decode(r, 512) - t.Run("Reader", testStr(d, input, valid)) - - r.Reset(input) - obr := iotest.OneByteReader(r) - d.Reset(obr) - t.Run("OneByteReader", testStr(d, input, valid)) - }) - } + runTestCases(t, testStrings, func(t *testing.T, d *Decoder) error { + _, err := d.Str() + return err + }) } func Benchmark_appendRune(b *testing.B) { diff --git a/dec_test.go b/dec_test.go index dd8470d..38e2cb7 100644 --- a/dec_test.go +++ b/dec_test.go @@ -2,12 +2,51 @@ package jx import ( "bytes" + "encoding/json" + "fmt" "io" + "strings" "testing" + "testing/iotest" "github.com/stretchr/testify/require" ) +func runTestCases(t *testing.T, cases []string, cb func(t *testing.T, d *Decoder) error) { + testCase := func(d *Decoder, input string, valid bool) func(t *testing.T) { + return func(t *testing.T) { + t.Cleanup(func() { + if t.Failed() { + t.Logf("Input: %q", input) + } + }) + + err := cb(t, d) + if valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } + } + } + for i, input := range cases { + valid := json.Valid([]byte(input)) + + t.Run(fmt.Sprintf("Test%d", i), func(t *testing.T) { + t.Run("Buffer", testCase(DecodeStr(input), input, valid)) + + r := strings.NewReader(input) + d := Decode(r, 512) + t.Run("Reader", testCase(d, input, valid)) + + r.Reset(input) + obr := iotest.OneByteReader(r) + d.Reset(obr) + t.Run("OneByteReader", testCase(d, input, valid)) + }) + } +} + func TestType_String(t *testing.T) { met := map[string]bool{} for i := Invalid; i <= Object+1; i++ {