Skip to content

Commit

Permalink
Merge pull request #25 from tdakkota/test/improve-coverage
Browse files Browse the repository at this point in the history
fix: float decoding, improve coverage
  • Loading branch information
ernado committed Jan 17, 2022
2 parents 451ba41 + 97d4e11 commit 0deeee4
Show file tree
Hide file tree
Showing 19 changed files with 510 additions and 442 deletions.
16 changes: 10 additions & 6 deletions bench_test.go
Expand Up @@ -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) {
Expand Down
143 changes: 0 additions & 143 deletions dec.go
Expand Up @@ -2,8 +2,6 @@ package jx

import (
"io"

"github.com/go-faster/errors"
)

// Type of json value.
Expand Down Expand Up @@ -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
}
34 changes: 34 additions & 0 deletions 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)
}
}
}
10 changes: 10 additions & 0 deletions 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
})
}
26 changes: 26 additions & 0 deletions 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
}
19 changes: 14 additions & 5 deletions dec_float.go
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 {
Expand Down
60 changes: 35 additions & 25 deletions dec_float_test.go
Expand Up @@ -2,6 +2,7 @@ package jx

import (
"bytes"
"io"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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)
}
}
})
})
}
1 change: 0 additions & 1 deletion dec_int.go
Expand Up @@ -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)
Expand Down
16 changes: 16 additions & 0 deletions 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
}

0 comments on commit 0deeee4

Please sign in to comment.