From cd30fdf648bfbbfbdfa31281d336a0e72d989135 Mon Sep 17 00:00:00 2001 From: Ivan Boyarkin Date: Sat, 18 Jul 2020 21:50:33 +0200 Subject: [PATCH] jsoniter: Fix errors during reading integers from chunked io.Reader 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 --- iter_int.go | 2 +- misc_tests/jsoniter_int_test.go | 91 +++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/iter_int.go b/iter_int.go index 21423203..cb8d1e54 100644 --- a/iter_int.go +++ b/iter_int.go @@ -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") } } diff --git a/misc_tests/jsoniter_int_test.go b/misc_tests/jsoniter_int_test.go index a730f698..e541ec8d 100644 --- a/misc_tests/jsoniter_int_test.go +++ b/misc_tests/jsoniter_int_test.go @@ -5,7 +5,9 @@ package misc_tests import ( "bytes" "encoding/json" + "io" "io/ioutil" + "math/rand" "strconv" "testing" @@ -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++ {