mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-12-24 09:25:21 +00:00
vm: allow parsing scientific JSON numbers
52-bit precision is not enough for our 256-bit VM, but this value matches the reference implementation, see the https://github.com/neo-project/neo/issues/2879. MaxIntegerPrec will be increased (or even removed) as soon as the ref. issue is resolved. Signed-off-by: Anna Shaleva <shaleva.ann@nspcc.ru>
This commit is contained in:
parent
6615cce81d
commit
8d5a41de6e
3 changed files with 60 additions and 12 deletions
|
@ -26,6 +26,11 @@ const MaxAllowedInteger = 2<<53 - 1
|
||||||
// MaxJSONDepth is the maximum allowed nesting level of an encoded/decoded JSON.
|
// MaxJSONDepth is the maximum allowed nesting level of an encoded/decoded JSON.
|
||||||
const MaxJSONDepth = 10
|
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
|
// ErrInvalidValue is returned when an item value doesn't fit some constraints
|
||||||
// during serialization or deserialization.
|
// during serialization or deserialization.
|
||||||
var ErrInvalidValue = errors.New("invalid value")
|
var ErrInvalidValue = errors.New("invalid value")
|
||||||
|
@ -213,6 +218,22 @@ func (d *decoder) decode() (Item, error) {
|
||||||
return NewByteArray([]byte(t)), nil
|
return NewByteArray([]byte(t)), nil
|
||||||
case json.Number:
|
case json.Number:
|
||||||
ts := t.String()
|
ts := t.String()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
num = new(big.Int)
|
||||||
|
_, acc := f.Int(num)
|
||||||
|
ok = acc == big.Exact
|
||||||
|
} else {
|
||||||
dot := strings.IndexByte(ts, '.')
|
dot := strings.IndexByte(ts, '.')
|
||||||
if dot != -1 {
|
if dot != -1 {
|
||||||
// As a special case numbers like 123.000 are allowed (SetString rejects them).
|
// As a special case numbers like 123.000 are allowed (SetString rejects them).
|
||||||
|
@ -224,7 +245,8 @@ func (d *decoder) decode() (Item, error) {
|
||||||
}
|
}
|
||||||
ts = ts[:dot]
|
ts = ts[:dot]
|
||||||
}
|
}
|
||||||
num, ok := new(big.Int).SetString(ts, 10)
|
num, ok = new(big.Int).SetString(ts, 10)
|
||||||
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("%w (integer)", ErrInvalidValue)
|
return nil, fmt.Errorf("%w (integer)", ErrInvalidValue)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func getTestDecodeFunc(js string, expected ...interface{}) 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) {
|
return func(t *testing.T) {
|
||||||
actual, err := FromJSON([]byte(js), 20)
|
actual, err := FromJSON([]byte(js), 20)
|
||||||
if expected[0] == nil {
|
if expected[0] == nil {
|
||||||
|
@ -19,7 +23,7 @@ func getTestDecodeFunc(js string, expected ...interface{}) func(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, Make(expected[0]), actual)
|
require.Equal(t, Make(expected[0]), actual)
|
||||||
|
|
||||||
if len(expected) == 1 {
|
if needEncode && len(expected) == 1 {
|
||||||
encoded, err := ToJSON(actual)
|
encoded, err := ToJSON(actual)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, js, string(encoded))
|
require.Equal(t, js, string(encoded))
|
||||||
|
@ -28,6 +32,8 @@ func getTestDecodeFunc(js string, expected ...interface{}) func(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFromToJSON(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("ByteString", func(t *testing.T) {
|
||||||
t.Run("Empty", getTestDecodeFunc(`""`, []byte{}))
|
t.Run("Empty", getTestDecodeFunc(`""`, []byte{}))
|
||||||
t.Run("Base64", getTestDecodeFunc(`"test"`, "test"))
|
t.Run("Base64", getTestDecodeFunc(`"test"`, "test"))
|
||||||
|
@ -36,6 +42,8 @@ func TestFromToJSON(t *testing.T) {
|
||||||
t.Run("BigInteger", func(t *testing.T) {
|
t.Run("BigInteger", func(t *testing.T) {
|
||||||
t.Run("ZeroFloat", getTestDecodeFunc(`12.000`, 12, nil))
|
t.Run("ZeroFloat", getTestDecodeFunc(`12.000`, 12, nil))
|
||||||
t.Run("NonZeroFloat", getTestDecodeFunc(`12.01`, 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("Negative", getTestDecodeFunc(`-4`, -4))
|
||||||
t.Run("Positive", getTestDecodeFunc(`123`, 123))
|
t.Run("Positive", getTestDecodeFunc(`123`, 123))
|
||||||
})
|
})
|
||||||
|
@ -123,6 +131,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) {
|
func testToJSON(t *testing.T, expectedErr error, item Item) {
|
||||||
data, err := ToJSON(item)
|
data, err := ToJSON(item)
|
||||||
if expectedErr != nil {
|
if expectedErr != nil {
|
||||||
|
|
2
pkg/vm/testdata/neo-vm
vendored
2
pkg/vm/testdata/neo-vm
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 02f2c68e7ba2694aff88c143631e7acf158d378a
|
Subproject commit 7e5996844a90b514739f879bc9f873f9a34c9a67
|
Loading…
Reference in a new issue