Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion decimal.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ var UseScientificNotation = false
// precise natural exponent value using ExpHullAbrham method.
var ExpMaxIterations = 1000

// MaxDecodeExponent is the largest exponent magnitude that NewFromString and UnmarshalBinary accept,
// and so UnmarshalJSON, UnmarshalText, Scan, GobDecode, DecodeSpanner and their NullDecimal variants too.
// Operations like String, Add and Float64 take time and memory that grow with the exponent, so without
// this limit a short input like "1e-2000000000" can stall the process or exhaust its memory.
// The default covers the float64 and IEEE 754 decimal128 exponent ranges.
//
// Values created with New, NewFromBigInt or arithmetic are not checked, so they may not decode back from
// their own String or MarshalBinary output. The limit does not bound the cost of Pow or ExpTaylor
// arguments or of very long inputs; validate those separately.
//
// Set it once at program start, before any decoding. Raise it only for trusted input.
var MaxDecodeExponent = 10000

// Zero constant, to make computations faster.
// Zero should never be compared with == or != directly, please use decimal.Equal or decimal.Cmp instead.
var Zero = Decimal{}
Expand Down Expand Up @@ -272,6 +285,9 @@ func NewFromString(value string) (Decimal, error) {
// NOTE(vadim): I doubt a string could realistically be this long
return Decimal{}, fmt.Errorf("can't convert %s to decimal: fractional part too long", originalInput)
}
if exp > int64(MaxDecodeExponent) || exp < -int64(MaxDecodeExponent) {
return Decimal{}, fmt.Errorf("can't convert %s to decimal: exponent %d exceeds MaxDecodeExponent (%d)", originalInput, exp, MaxDecodeExponent)
}

return Decimal{
value: dValue,
Expand Down Expand Up @@ -2157,7 +2173,11 @@ func (d *Decimal) UnmarshalBinary(data []byte) error {
}

// Extract the exponent
d.exp = int32(binary.BigEndian.Uint32(data[:4]))
exp := int32(binary.BigEndian.Uint32(data[:4]))
if int64(exp) > int64(MaxDecodeExponent) || int64(exp) < -int64(MaxDecodeExponent) {
return fmt.Errorf("error decoding binary: exponent %d exceeds MaxDecodeExponent (%d)", exp, MaxDecodeExponent)
}
d.exp = exp

// Extract the value
d.value = new(big.Int)
Expand Down
65 changes: 57 additions & 8 deletions decimal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,36 @@ func TestNewFromStringErrs(t *testing.T) {
}
}

func TestMaxDecodeExponent(t *testing.T) {
limit := int32(MaxDecodeExponent)
for _, c := range []struct {
exp int32
ok bool
}{
{limit, true},
{-limit, true},
{limit + 1, false},
{-limit - 1, false},
{math.MaxInt32, false},
{math.MinInt32, false},
} {
if _, err := NewFromString(fmt.Sprintf("1e%d", c.exp)); (err == nil) != c.ok {
t.Errorf("NewFromString(1e%d): got error %v, want ok=%v", c.exp, err, c.ok)
}

b, _ := New(1, c.exp).MarshalBinary()
d := New(7, 0)
err := d.UnmarshalBinary(b)
if (err == nil) != c.ok || (!c.ok && d.Exponent() != 0) {
t.Errorf("UnmarshalBinary(exp %d): got error %v and exp %d, want ok=%v", c.exp, err, d.Exponent(), c.ok)
}
}

if _, err := NewFromString(fmt.Sprintf("0.1e%d", -limit)); err == nil {
t.Errorf("NewFromString(0.1e%d) accepted exponent %d", -limit, -limit-1)
}
}

func TestNewFromStringDeepEquals(t *testing.T) {
type StrCmp struct {
str1 string
Expand Down Expand Up @@ -2721,7 +2751,6 @@ func TestDecimal_Pow(t *testing.T) {
{"1.0041666666666667", "-360", "0.2238265956413493"},
{"1.0041666666666667", "360.5", "4.4770424391814548"},
{"0.01", "-7.585", "1479108388168207.4221993463468552"},
{"1.5", "-100000", "7.483321982518254e-17610"},
{"-1.5", "-1001", "-5.403183104351711e-177"},
{"-1.5", "-1000", "8.104774656527567e-177"},
{"1.0000001", "-1000000000", "0.00000000000000000000000000000000000000000003720094576445977"},
Expand All @@ -2734,16 +2763,21 @@ func TestDecimal_Pow(t *testing.T) {
{"0.0208333333333333", "0.33", "0.2787342852450195"},
{"1", "1" + strings.Repeat("0", 400) + ".5", "1"},
} {
base, _ := NewFromString(testCase.Base)
exp, _ := NewFromString(testCase.Exponent)
expected, _ := NewFromString(testCase.Expected)
base := RequireFromString(testCase.Base)
exp := RequireFromString(testCase.Exponent)
expected := RequireFromString(testCase.Expected)

result := base.Pow(exp)

if result.Cmp(expected) != 0 {
t.Errorf("expected %s, got %s, for %s^%s", testCase.Expected, result.String(), testCase.Base, testCase.Exponent)
}
}

// The expected exponent is beyond MaxDecodeExponent, so it can't be parsed from a string.
if got := New(15, -1).Pow(New(-100000, 0)); got.Cmp(New(7483321982518254, -17625)) != 0 {
t.Errorf("expected 7.483321982518254e-17610, got %s, for 1.5^-100000", got)
}
}

func TestDecimal_PowWithPrecision(t *testing.T) {
Expand Down Expand Up @@ -2896,18 +2930,33 @@ func TestDecimal_PowInt32(t *testing.T) {
{"10", -18, "0.000000000000000001"},
{"12345", -5, "0.000000000000000000003487743424943423"},
{"-1.5", -1001, "-5.403183104351711e-177"},
{"1.5", math.MinInt32, "4.189793427303781e-378153100"},
{"3e50000000", -1, "3.333333333333333e-50000001"},
} {
base, _ := NewFromString(testCase.Decimal)
expected, _ := NewFromString(testCase.Expected)
base := RequireFromString(testCase.Decimal)
expected := RequireFromString(testCase.Expected)

result, _ := base.PowInt32(testCase.Exponent)

if result.Cmp(expected) != 0 {
t.Errorf("expected %s, got %s, for %s**%d", testCase.Expected, result.String(), testCase.Decimal, testCase.Exponent)
}
}

// Exponents beyond MaxDecodeExponent can't be parsed, and String() on them is too slow for error messages.
for _, testCase := range []struct {
Decimal Decimal
Exponent int32
Expected Decimal
}{
{New(15, -1), math.MinInt32, New(4189793427303781, -378153115)},
{New(3, 50000000), -1, New(3333333333333333, -50000016)},
} {
result, err := testCase.Decimal.PowInt32(testCase.Exponent)
if err != nil || result.Cmp(testCase.Expected) != 0 {
t.Errorf("expected %de%d, got %de%d (err %v), for %de%d**%d",
testCase.Expected.Coefficient(), testCase.Expected.Exponent(), result.Coefficient(), result.Exponent(), err,
testCase.Decimal.Coefficient(), testCase.Decimal.Exponent(), testCase.Exponent)
}
}
}

func TestDecimal_Pow_InexactResultExponent(t *testing.T) {
Expand Down
Loading