@@ -35,28 +35,24 @@ defmodule Decimal do
3535 10 ^ exponent` and will refer to the sign in documentation as either *positive*
3636 or *negative*.
3737
38- By default there is no maximum or minimum value for the exponent because
39- `Decimal.Context` defaults `emax` and `emin` to `:infinity`. Because of that
40- all numbers are "normal". This means that when an operation should, according
41- to the specification, return a number that "underflows" 0 is returned instead
42- of Etiny. This may happen when dividing a number with infinity. When `emax`
43- or `emin` are finite, overflow and underflow may be signalled. Clamped is
44- still not signalled.
38+ The default `Decimal.Context` follows IEEE 754 decimal128: `precision` is
39+ 34, `emax` is 6 144, and `emin` is -6 143. Operation results whose adjusted
40+ exponent leaves that band signal overflow or underflow. Clamped is still
41+ not signalled.
4542
4643 ## Large exponents and untrusted input
4744
4845 Decimal can represent compact values with very large exponents, such as
4946 `1e1000000`. These values are valid decimals, but some APIs may need memory
50- or CPU proportional to the expanded size of the number. This is especially
51- important for decimals parsed from user input, JSON payloads, form fields,
52- database fields, or other external data.
47+ or CPU proportional to the expanded size of the number.
5348
54- Use `parse/2` or `cast/2` with `:max_digits` and `:max_exponent` when parsing
55- untrusted input. Use `to_string/3` with `:max_digits` when rendering output
56- formats that may expand the exponent, such as `:normal` or `:xsd`.
57- Finite `Decimal.Context` `emax` and `emin` values can limit operation
58- results, but they do not validate already-created decimals and should not
59- replace parse/cast limits for untrusted input.
49+ `parse/1`, `parse/2`, `cast/1`, `cast/2`, `to_string/2`, and `to_string/3`
50+ apply IEEE 754 decimal128 limits by default: `:max_digits` of 34,
51+ `:max_exponent` of 6 144, and a `:max_digits` for output of 6 178
52+ (precision + emax — large enough to render any in-range decimal128 in any
53+ format). These defaults reject the pathological inputs described in
54+ CVE-2026-32686 without materializing them. Pass options on the explicit
55+ arities to override; pass `:infinity` to disable a limit entirely.
6056
6157 ## Protocol Implementations
6258
@@ -164,6 +160,14 @@ defmodule Decimal do
164160 @ type to_string_option ::
165161 { :max_digits , non_neg_integer | :infinity }
166162
163+ # IEEE 754 decimal128 defaults: precision = 34, emax = 6_144, emin = -6_143.
164+ # The to_string default is precision + emax (34 + 6_144), which is the
165+ # worst-case `:normal` digit-character count for any in-range decimal128
166+ # value.
167+ @ default_max_digits 34
168+ @ default_max_exponent 6_144
169+ @ default_to_string_max_digits 6_178
170+
167171 # Below 10^2000 the BIF `:erlang.integer_to_binary/1` is fast enough; for
168172 # larger integers `integer_to_decimal_iodata/3` recursively splits on a
169173 # power of 10 (down to chunks of `@decimal_conversion_leaf_digits` digits)
@@ -1538,18 +1542,7 @@ defmodule Decimal do
15381542
15391543 """
15401544 @ spec cast ( term ) :: { :ok , t } | :error
1541- def cast ( integer ) when is_integer ( integer ) , do: { :ok , Decimal . new ( integer ) }
1542- def cast ( % Decimal { } = decimal ) , do: { :ok , decimal }
1543- def cast ( float ) when is_float ( float ) , do: { :ok , from_float ( float ) }
1544-
1545- def cast ( binary ) when is_binary ( binary ) do
1546- case parse ( binary ) do
1547- { decimal , "" } -> { :ok , decimal }
1548- _ -> :error
1549- end
1550- end
1551-
1552- def cast ( _ ) , do: :error
1545+ def cast ( term ) , do: cast_with_limits ( term , default_parse_limits ( ) )
15531546
15541547 @ doc """
15551548 Creates a new decimal number from an integer, string, float, or existing decimal
@@ -1560,8 +1553,10 @@ defmodule Decimal do
15601553 doc_since ( "2.4.0" )
15611554 @ spec cast ( term , [ parse_option ] ) :: { :ok , t } | :error
15621555 def cast ( term , opts ) when is_list ( opts ) do
1563- limits = parse_limits! ( opts )
1556+ cast_with_limits ( term , parse_limits! ( opts ) )
1557+ end
15641558
1559+ defp cast_with_limits ( term , limits ) do
15651560 cond do
15661561 is_integer ( term ) ->
15671562 decimal = Decimal . new ( term )
@@ -1591,6 +1586,10 @@ defmodule Decimal do
15911586 If successful, returns a tuple in the form of `{decimal, remainder_of_binary}`,
15921587 otherwise `:error`.
15931588
1589+ Inputs whose digit count or exponent magnitude exceed the default limits
1590+ (`#{ @ default_max_digits } ` digits, `#{ @ default_max_exponent } ` absolute
1591+ exponent) return `:error`. Use `parse/2` to override the limits.
1592+
15941593 ## Examples
15951594
15961595 iex> Decimal.parse("3.14")
@@ -1607,34 +1606,21 @@ defmodule Decimal do
16071606
16081607 """
16091608 @ spec parse ( binary ( ) ) :: { t ( ) , binary ( ) } | :error
1610- def parse ( "+" <> rest ) do
1611- parse_unsign ( rest )
1612- end
1613-
1614- def parse ( "-" <> rest ) do
1615- case parse_unsign ( rest ) do
1616- { % Decimal { } = num , rest } -> { % { num | sign: - 1 } , rest }
1617- :error -> :error
1618- end
1619- end
1620-
16211609 def parse ( binary ) when is_binary ( binary ) do
1622- parse_unsign ( binary )
1610+ parse_with_limits ( binary , default_parse_limits ( ) )
16231611 end
16241612
16251613 @ doc """
1626- Parses a binary into a decimal with optional limits.
1627-
1628- Use this function instead of `parse/1` for untrusted input. Without explicit
1629- limits, decimal parsing accepts any exponent and digit count for backwards
1630- compatibility.
1614+ Parses a binary into a decimal with explicit limits.
16311615
16321616 The following options are supported:
16331617
16341618 * `:max_digits` - maximum number of decimal digits consumed from the input,
1635- including leading and trailing zeros.
1619+ including leading and trailing zeros. Defaults to `#{ @ default_max_digits } `.
1620+ Pass `:infinity` to disable.
16361621 * `:max_exponent` - maximum absolute value of the parsed decimal exponent,
1637- after fractional digits are accounted for.
1622+ after fractional digits are accounted for. Defaults to
1623+ `#{ @ default_max_exponent } `. Pass `:infinity` to disable.
16381624
16391625 Returns `:error` when a parsed number exceeds the configured limits.
16401626 """
@@ -1663,10 +1649,11 @@ defmodule Decimal do
16631649 @ doc """
16641650 Converts given number to its string representation.
16651651
1666- The default `:scientific` format is compact for large positive exponents.
1667- The `:normal` and `:xsd` formats may allocate output proportional to the
1668- expanded size of the decimal. Use `to_string/3` with `:max_digits` when
1669- rendering decimals from untrusted input.
1652+ Output is bounded to `#{ @ default_to_string_max_digits } ` digit characters by
1653+ default; pass options via `to_string/3` to override. `:scientific` is compact
1654+ for large positive exponents and rarely hits the limit; `:normal` and `:xsd`
1655+ expand proportional to the exponent and will raise `ArgumentError` when the
1656+ limit would be exceeded.
16701657
16711658 ## Options
16721659
@@ -1696,15 +1683,21 @@ defmodule Decimal do
16961683 @ spec to_string ( t , :scientific | :normal | :xsd | :raw ) :: String . t ( )
16971684 def to_string ( num , type \\ :scientific )
16981685
1699- def to_string ( % Decimal { sign: sign , coef: :NaN } , _type ) do
1686+ def to_string ( % Decimal { } = num , type )
1687+ when type in [ :scientific , :normal , :xsd , :raw ] do
1688+ check_to_string_max_digits! ( num , type , @ default_to_string_max_digits )
1689+ do_to_string ( num , type )
1690+ end
1691+
1692+ defp do_to_string ( % Decimal { sign: sign , coef: :NaN } , _type ) do
17001693 if sign == 1 , do: "NaN" , else: "-NaN"
17011694 end
17021695
1703- def to_string ( % Decimal { sign: sign , coef: :inf } , _type ) do
1696+ defp do_to_string ( % Decimal { sign: sign , coef: :inf } , _type ) do
17041697 if sign == 1 , do: "Infinity" , else: "-Infinity"
17051698 end
17061699
1707- def to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :normal ) do
1700+ defp do_to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :normal ) do
17081701 digits = integer_to_decimal_binary ( coef )
17091702 length = byte_size ( digits )
17101703
@@ -1725,7 +1718,7 @@ defmodule Decimal do
17251718 IO . iodata_to_binary ( iodata )
17261719 end
17271720
1728- def to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :scientific ) do
1721+ defp do_to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :scientific ) do
17291722 digits = integer_to_decimal_binary ( coef )
17301723 length = byte_size ( digits )
17311724 adjusted = exp + length - 1
@@ -1762,16 +1755,16 @@ defmodule Decimal do
17621755 IO . iodata_to_binary ( iodata )
17631756 end
17641757
1765- def to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :raw ) do
1758+ defp do_to_string ( % Decimal { sign: sign , coef: coef , exp: exp } , :raw ) do
17661759 str = integer_to_decimal_binary ( coef )
17671760 str = if sign == - 1 , do: [ ?- | str ] , else: str
17681761 str = if exp != 0 , do: [ str , "E" , :erlang . integer_to_binary ( exp ) ] , else: str
17691762
17701763 IO . iodata_to_binary ( str )
17711764 end
17721765
1773- def to_string ( % Decimal { } = decimal , :xsd ) do
1774- decimal |> canonical_xsd ( ) |> to_string ( :normal )
1766+ defp do_to_string ( % Decimal { } = decimal , :xsd ) do
1767+ decimal |> canonical_xsd ( ) |> do_to_string ( :normal )
17751768 end
17761769
17771770 defp zeroes ( 0 ) , do: ""
@@ -1845,25 +1838,25 @@ defmodule Decimal do
18451838 defp byte_bit_length ( _byte ) , do: 1
18461839
18471840 @ doc """
1848- Converts given number to its string representation with optional limits.
1849-
1850- Use this function when rendering decimals from untrusted input, especially
1851- with `:normal` or `:xsd`, because those formats may otherwise allocate output
1852- proportional to the expanded size of the decimal.
1841+ Converts given number to its string representation with explicit limits.
18531842
18541843 The following options are supported:
18551844
18561845 * `:max_digits` - maximum number of digit characters in the output. Sign,
1857- decimal point, and exponent markers are not counted.
1846+ decimal point, and exponent markers are not counted. Defaults to
1847+ `#{ @ default_to_string_max_digits } `. Pass `:infinity` to disable.
18581848
18591849 Raises `ArgumentError` when the configured limit would be exceeded.
18601850 """
18611851 doc_since ( "2.4.0" )
18621852 @ spec to_string ( t , :scientific | :normal | :xsd | :raw , [ to_string_option ] ) :: String . t ( )
1863- def to_string ( % Decimal { } = num , type , opts ) when is_list ( opts ) do
1864- max_digits = limit! ( :max_digits , Keyword . get ( opts , :max_digits , :infinity ) )
1853+ def to_string ( % Decimal { } = num , type , opts )
1854+ when is_list ( opts ) and type in [ :scientific , :normal , :xsd , :raw ] do
1855+ max_digits =
1856+ limit! ( :max_digits , Keyword . get ( opts , :max_digits , @ default_to_string_max_digits ) )
1857+
18651858 check_to_string_max_digits! ( num , type , max_digits )
1866- to_string ( num , type )
1859+ do_to_string ( num , type )
18671860 end
18681861
18691862 defp canonical_xsd ( % Decimal { coef: 0 } = decimal ) , do: % { decimal | exp: - 1 }
@@ -2566,16 +2559,24 @@ defmodule Decimal do
25662559 ## PARSING ##
25672560
25682561 defp parse_limits! ( opts ) do
2569- Enum . reduce ( opts , % { max_digits: :infinity , max_exponent: :infinity } , fn
2570- { :max_digits , value } , acc ->
2571- % { acc | max_digits: limit! ( :max_digits , value ) }
2572-
2573- { :max_exponent , value } , acc ->
2574- % { acc | max_exponent: limit! ( :max_exponent , value ) }
2562+ Enum . reduce (
2563+ opts ,
2564+ % { max_digits: @ default_max_digits , max_exponent: @ default_max_exponent } ,
2565+ fn
2566+ { :max_digits , value } , acc ->
2567+ % { acc | max_digits: limit! ( :max_digits , value ) }
2568+
2569+ { :max_exponent , value } , acc ->
2570+ % { acc | max_exponent: limit! ( :max_exponent , value ) }
2571+
2572+ { key , _value } , _acc ->
2573+ raise ArgumentError , "unknown option #{ inspect ( key ) } "
2574+ end
2575+ )
2576+ end
25752577
2576- { key , _value } , _acc ->
2577- raise ArgumentError , "unknown option #{ inspect ( key ) } "
2578- end )
2578+ defp default_parse_limits do
2579+ % { max_digits: @ default_max_digits , max_exponent: @ default_max_exponent }
25792580 end
25802581
25812582 defp limit! ( _key , :infinity ) , do: :infinity
@@ -2587,66 +2588,12 @@ defmodule Decimal do
25872588 "#{ inspect ( key ) } must be a non-negative integer or :infinity, got: #{ inspect ( value ) } "
25882589 end
25892590
2590- defp parse_unsign ( << first , remainder :: size ( 7 ) - binary , rest :: binary >> ) when first in [ ?i , ?I ] do
2591- if String . downcase ( remainder ) == "nfinity" do
2592- { % Decimal { coef: :inf } , rest }
2593- else
2594- :error
2595- end
2596- end
2597-
2598- defp parse_unsign ( << first , remainder :: size ( 2 ) - binary , rest :: binary >> ) when first in [ ?i , ?I ] do
2599- if String . downcase ( remainder ) == "nf" do
2600- { % Decimal { coef: :inf } , rest }
2601- else
2602- :error
2603- end
2604- end
2605-
2606- defp parse_unsign ( << first , remainder :: size ( 2 ) - binary , rest :: binary >> ) when first in [ ?n , ?N ] do
2607- if String . downcase ( remainder ) == "an" do
2608- { % Decimal { coef: :NaN } , rest }
2609- else
2610- :error
2611- end
2612- end
2613-
2614- defp parse_unsign ( bin ) do
2615- { int_rev , int_size , after_int } = parse_digits_count ( bin , [ ] , 0 )
2616-
2617- case after_int do
2618- << ?. , after_dot :: binary >> ->
2619- { coef_rev , total_size , after_float } = parse_digits_count ( after_dot , int_rev , int_size )
2620-
2621- if total_size == 0 do
2622- :error
2623- else
2624- { exp , rest } = parse_exp ( after_float )
2625- coef = digits_acc_to_integer ( coef_rev , total_size )
2626- float_size = total_size - int_size
2627- { % Decimal { coef: coef , exp: parse_exp_int ( exp ) - float_size } , rest }
2628- end
2629-
2630- _ ->
2631- if int_size == 0 do
2632- :error
2633- else
2634- { exp , rest } = parse_exp ( after_int )
2635- coef = digits_acc_to_integer ( int_rev , int_size )
2636- { % Decimal { coef: coef , exp: parse_exp_int ( exp ) } , rest }
2637- end
2638- end
2639- end
2640-
26412591 defp parse_digits_count ( << digit , rest :: binary >> , acc , count ) when digit in ?0 .. ?9 do
26422592 parse_digits_count ( rest , [ digit | acc ] , count + 1 )
26432593 end
26442594
26452595 defp parse_digits_count ( rest , acc , count ) , do: { acc , count , rest }
26462596
2647- defp parse_exp_int ( [ ] ) , do: 0
2648- defp parse_exp_int ( chars ) , do: List . to_integer ( chars )
2649-
26502597 defp digits_acc_to_integer ( [ ] , _size ) , do: 0
26512598 defp digits_acc_to_integer ( acc , _size ) , do: :erlang . list_to_integer ( :lists . reverse ( acc ) )
26522599
@@ -2876,21 +2823,21 @@ end
28762823
28772824defimpl Inspect , for: Decimal do
28782825 def inspect ( dec , _opts ) do
2879- "Decimal.new(\" " <> Decimal . to_string ( dec ) <> "\" )"
2826+ "Decimal.new(\" " <> Decimal . to_string ( dec , :scientific , max_digits: :infinity ) <> "\" )"
28802827 end
28812828end
28822829
28832830defimpl String.Chars , for: Decimal do
28842831 def to_string ( dec ) do
2885- Decimal . to_string ( dec )
2832+ Decimal . to_string ( dec , :scientific , max_digits: :infinity )
28862833 end
28872834end
28882835
28892836# TODO: remove when we require Elixir 1.18
28902837if Code . ensure_loaded? ( JSON.Encoder ) and function_exported? ( JSON.Encoder , :encode , 2 ) do
28912838 defimpl JSON.Encoder , for: Decimal do
28922839 def encode ( decimal , _encoder ) do
2893- [ ?" , Decimal . to_string ( decimal ) , ?" ]
2840+ [ ?" , Decimal . to_string ( decimal , :scientific , max_digits: :infinity ) , ?" ]
28942841 end
28952842 end
28962843end
0 commit comments