diff --git a/lib/money/helpers.rb b/lib/money/helpers.rb index 4b9c1a47..a752fec4 100644 --- a/lib/money/helpers.rb +++ b/lib/money/helpers.rb @@ -8,6 +8,10 @@ module Helpers DECIMAL_ZERO = BigDecimal(0).freeze MAX_DECIMAL = 21 + # Largest supported BigDecimal#exponent (number of integer digits). Rejects + # values like "1e1000000000" that are cheap to store but expand into huge + # Integers or Strings in #subunits or #to_s. + MAX_INTEGER_DIGITS = 1000 def value_to_decimal(num) value = diff --git a/lib/money/money.rb b/lib/money/money.rb index 6b5d2c2e..f382b747 100644 --- a/lib/money/money.rb +++ b/lib/money/money.rb @@ -124,6 +124,9 @@ def new_from_money(amount, currency) def initialize(value, currency) raise ArgumentError if value.nan? raise ArgumentError if value.infinite? + if value.exponent > Helpers::MAX_INTEGER_DIGITS + raise ArgumentError, "value exceeds the supported range of #{Helpers::MAX_INTEGER_DIGITS} integer digits" + end @currency = currency @value = BigDecimal(value.round(@currency.minor_units)) diff --git a/sig/money/helpers.rbs b/sig/money/helpers.rbs index c377a1b9..b2c249e8 100644 --- a/sig/money/helpers.rbs +++ b/sig/money/helpers.rbs @@ -4,6 +4,7 @@ class Money module Helpers DECIMAL_ZERO: BigDecimal MAX_DECIMAL: Integer + MAX_INTEGER_DIGITS: Integer def self.value_to_decimal: (Numeric | String | Money | nil num) -> BigDecimal def self.value_to_currency: (String | Currency | NullCurrency | nil currency) -> (Currency | NullCurrency) diff --git a/spec/money_spec.rb b/spec/money_spec.rb index 50da21ef..87aa59ac 100644 --- a/spec/money_spec.rb +++ b/spec/money_spec.rb @@ -465,6 +465,47 @@ expect { Money.new(-Float::INFINITY) }.to raise_error(ArgumentError) end + describe "magnitude bound" do + it "raises when constructed with an exponent-form string beyond the supported range" do + expect { Money.new("1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/) + expect { Money.new("-1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/) + end + + it "raises when constructed with an out-of-range BigDecimal" do + expect { Money.new(BigDecimal("1e1000"), "USD") }.to raise_error(ArgumentError, /supported range/) + end + + it "raises when converting out-of-range subunits back to money" do + expect { Money.from_subunits("1e1000000000", "USD") }.to raise_error(ArgumentError, /supported range/) + end + + it "raises when arithmetic leaves the supported range" do + expect { Money.new("1e999", "USD") * 10 }.to raise_error(ArgumentError, /supported range/) + end + + it "raises when loaded from YAML with an out-of-range value" do + yaml = "--- !ruby/object:Money\nvalue: '1e1000000000'\ncurrency: USD\n" + expect { yaml_load(yaml) }.to raise_error(ArgumentError, /supported range/) + end + + it "accepts the largest in-range value and converts it to subunits unchanged" do + max = "9" * Money::Helpers::MAX_INTEGER_DIGITS + expect(Money.new(max, "USD").value).to eq(BigDecimal(max)) + expect(Money.new(max, "USD").subunits).to eq(Integer(max) * 100) + expect(Money.new("-#{max}", "USD").subunits).to eq(-Integer(max) * 100) + end + + it "accepts values larger than a DECIMAL(21,3) column" do + expect(Money.new("1e25", "USD") * 10**25).to eq(Money.new("1e50", "USD")) + expect(Money.new("1e25", "USD").subunits).to eq(10**27) + end + + it "still accepts ordinary exponent-form strings" do + expect(Money.new("1.5e3", "USD")).to eq(Money.new(1500, "USD")) + expect(Money.new("1e-10", "USD")).to eq(Money.new(0, "USD")) + end + end + it "is comparable with non-money objects" do expect(money).not_to eq(nil) end