Skip to content

Commit

Permalink
jsoniter: Fix errors during reading integers from chunked io.Reader
Browse files Browse the repository at this point in the history
This commit fixes bug in Iterator.assertInteger method if the next
conditions are met:
- Iterator reads data from `io.Reader`,
- expected value is `0` (zero)
- `Iterator.tail == Iterator.head + 1`
- `Iterator.tail < len(Iterator.buf)`
- value in the buffer after `Iterator.tail` is presented from the previous read and has '.' character.

Typical error which user cal see is:
- assertInteger: can not decode float as int, error found in #X byte of ...

Regression test added for checking the correct behaviour.

Fixes #476
  • Loading branch information
mrVanboy committed Aug 5, 2020
1 parent 9461257 commit cd30fdf
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 1 deletion.
2 changes: 1 addition & 1 deletion iter_int.go
Expand Up @@ -339,7 +339,7 @@ func (iter *Iterator) readUint64(c byte) (ret uint64) {
}

func (iter *Iterator) assertInteger() {
if iter.head < len(iter.buf) && iter.buf[iter.head] == '.' {
if iter.head < iter.tail && iter.buf[iter.head] == '.' {
iter.ReportError("assertInteger", "can not decode float as int")
}
}
91 changes: 91 additions & 0 deletions misc_tests/jsoniter_int_test.go
Expand Up @@ -5,7 +5,9 @@ package misc_tests
import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"math/rand"
"strconv"
"testing"

Expand Down Expand Up @@ -70,6 +72,95 @@ func Test_float_as_int(t *testing.T) {
should.NotNil(jsoniter.Unmarshal([]byte(`1.1`), &i))
}

// chunkedData is io.Reader which returns random amount of data in range [1, chunkedData.chunkSize].
// It simulates chunked data on from HTTP server, which is commonly used by net/http package.
type chunkedData struct {
chunkSize int
data []byte
head int
}

// Read is implementation of the io.Reader which returns random amount of data in range [1, chunkedData.chunkSize].
func (c *chunkedData) Read(p []byte) (n int, err error) {
to := c.head + int(rand.Int31n(int32(c.chunkSize))+1)

// copy does not copy more data then p can consume
n = copy(p, c.data[c.head:to])
c.head = c.head + n
if c.head >= len(c.data) {
err = io.EOF
}
return n, err
}

// TestIterator_ReadInt_chunkedInput validates the behaviour of Iterator.ReadInt() method in where:
// - it reads data from io.Reader,
// - expected value is 0 (zero)
// - Iterator.tail == Iterator.head
// - Iterator.tail < len(Iterator.buf)
// - value in buffer after Iterator.tail is presented from previous read and has '.' character.
func TestIterator_ReadInt_chunkedInput(t *testing.T) {
should := require.New(t)

data := &chunkedData{
data: jsonFloatIntArray(t, 10),
}

// because this test is rely on randomness of chunkedData, we are doing multiple iterations to
// be sure, that we can hit a required case.
for data.chunkSize = 3; data.chunkSize <= len(data.data); data.chunkSize++ {
data.head = 0

iter := jsoniter.Parse(jsoniter.ConfigDefault, data, data.chunkSize)
i := 0
for iter.ReadArray() {
// every even item is float, let's just skip it.
if i%2 == 0 {
iter.Skip()
i++
continue
}

should.Zero(iter.ReadInt())
should.NoError(iter.Error)

i++
}
}
}

// jsonFloatIntArray generates JSON array where every
// - even item is float 0.1
// - odd item is integer 0
//
// [0.1, 0, 0.1, 0]
func jsonFloatIntArray(t *testing.T, numberOfItems int) []byte {
t.Helper()
numbers := make([]jsoniter.Any, numberOfItems)
for i := range numbers {
switch i % 2 {
case 0:
numbers[i] = jsoniter.WrapFloat64(0.1)
default:
numbers[i] = jsoniter.WrapInt64(0)
}
}

fixture, err := jsoniter.ConfigFastest.Marshal(numbers)
if err != nil {
panic(err)
}

b := &bytes.Buffer{}

require.NoError(
t,
json.Compact(b, fixture),
"json should be compactable",
)
return b.Bytes()
}

func Benchmark_jsoniter_encode_int(b *testing.B) {
stream := jsoniter.NewStream(jsoniter.ConfigDefault, ioutil.Discard, 64)
for n := 0; n < b.N; n++ {
Expand Down

0 comments on commit cd30fdf

Please sign in to comment.