Merge pull request #3073 from nspcc-dev/fix-sci-jnum

JSON scientific numbers kludge
This commit is contained in:
Roman Khimov 2023-08-01 15:06:32 +03:00 committed by GitHub
commit 03b9b4a4a1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 60 additions and 12 deletions

View file

@ -26,6 +26,11 @@ const MaxAllowedInteger = 2<<53 - 1
// MaxJSONDepth is the maximum allowed nesting level of an encoded/decoded JSON.
const MaxJSONDepth = 10
// MaxIntegerPrec is the maximum precision allowed for big.Integer parsing.
// It equals to the reference value and doesn't allow to precisely parse big
// numbers, see the https://github.com/neo-project/neo/issues/2879.
const MaxIntegerPrec = 53
// ErrInvalidValue is returned when an item value doesn't fit some constraints
// during serialization or deserialization.
var ErrInvalidValue = errors.New("invalid value")
@ -213,18 +218,35 @@ func (d *decoder) decode() (Item, error) {
return NewByteArray([]byte(t)), nil
case json.Number:
ts := t.String()
dot := strings.IndexByte(ts, '.')
if dot != -1 {
// As a special case numbers like 123.000 are allowed (SetString rejects them).
// And yes, that's the way C# code works also.
for _, r := range ts[dot+1:] {
if r != '0' {
return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue)
}
var (
num *big.Int
ok bool
)
isScientific := strings.Contains(ts, "e+") || strings.Contains(ts, "E+")
if isScientific {
// As a special case numbers like 2.8e+22 are allowed (SetString rejects them).
// That's the way how C# code works.
f, _, err := big.ParseFloat(ts, 10, MaxIntegerPrec, big.ToNearestEven)
if err != nil {
return nil, fmt.Errorf("%w (malformed exp value for int)", ErrInvalidValue)
}
ts = ts[:dot]
num = new(big.Int)
_, acc := f.Int(num)
ok = acc == big.Exact
} else {
dot := strings.IndexByte(ts, '.')
if dot != -1 {
// As a special case numbers like 123.000 are allowed (SetString rejects them).
// And yes, that's the way C# code works also.
for _, r := range ts[dot+1:] {
if r != '0' {
return nil, fmt.Errorf("%w (real value for int)", ErrInvalidValue)
}
}
ts = ts[:dot]
}
num, ok = new(big.Int).SetString(ts, 10)
}
num, ok := new(big.Int).SetString(ts, 10)
if !ok {
return nil, fmt.Errorf("%w (integer)", ErrInvalidValue)
}

View file

@ -8,7 +8,11 @@ import (
"github.com/stretchr/testify/require"
)
func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) {
func getTestDecodeFunc(js string, expected ...interface{}) func(t *testing.T) {
return getTestDecodeEncodeFunc(js, true, expected...)
}
func getTestDecodeEncodeFunc(js string, needEncode bool, expected ...interface{}) func(t *testing.T) {
return func(t *testing.T) {
actual, err := FromJSON([]byte(js), 20)
if expected[0] == nil {
@ -18,7 +22,7 @@ func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) {
require.NoError(t, err)
require.Equal(t, Make(expected[0]), actual)
if len(expected) == 1 {
if needEncode && len(expected) == 1 {
encoded, err := ToJSON(actual)
require.NoError(t, err)
require.Equal(t, js, string(encoded))
@ -27,6 +31,8 @@ func getTestDecodeFunc(js string, expected ...any) func(t *testing.T) {
}
func TestFromToJSON(t *testing.T) {
bigInt, ok := new(big.Int).SetString("28000000000000000000000", 10)
require.True(t, ok)
t.Run("ByteString", func(t *testing.T) {
t.Run("Empty", getTestDecodeFunc(`""`, []byte{}))
t.Run("Base64", getTestDecodeFunc(`"test"`, "test"))
@ -35,6 +41,8 @@ func TestFromToJSON(t *testing.T) {
t.Run("BigInteger", func(t *testing.T) {
t.Run("ZeroFloat", getTestDecodeFunc(`12.000`, 12, nil))
t.Run("NonZeroFloat", getTestDecodeFunc(`12.01`, nil))
t.Run("ExpInteger", getTestDecodeEncodeFunc(`2.8e+22`, false, bigInt))
t.Run("ExpFloat", getTestDecodeEncodeFunc(`1.2345e+3`, false, nil)) // float value, parsing should fail for it.
t.Run("Negative", getTestDecodeFunc(`-4`, -4))
t.Run("Positive", getTestDecodeFunc(`123`, 123))
})
@ -122,6 +130,24 @@ func TestFromToJSON(t *testing.T) {
})
}
// TestFromJSON_CompatBigInt ensures that maximum BigInt parsing precision matches
// the C# one, ref. https://github.com/neo-project/neo/issues/2879.
func TestFromJSON_CompatBigInt(t *testing.T) {
tcs := map[string]string{
`9.05e+28`: "90499999999999993918259200000",
`1.871e+21`: "1871000000000000000000",
`3.0366e+32`: "303660000000000004445016810323968",
`1e+30`: "1000000000000000019884624838656",
}
for in, expected := range tcs {
t.Run(in, func(t *testing.T) {
actual, err := FromJSON([]byte(in), 5)
require.NoError(t, err)
require.Equal(t, expected, actual.Value().(*big.Int).String())
})
}
}
func testToJSON(t *testing.T, expectedErr error, item Item) {
data, err := ToJSON(item)
if expectedErr != nil {