Skip to content

Bound a decimal by the digits it denotes, not just its characters - #98

Merged
pjfanning merged 1 commit into
apache:trunkfrom
pjfanning:limit-decimal-exponent-digits
Aug 26, 2026
Merged

Bound a decimal by the digits it denotes, not just its characters#98
pjfanning merged 1 commit into
apache:trunkfrom
pjfanning:limit-decimal-exponent-digits

Conversation

@pjfanning

Copy link
Copy Markdown
Member

Follow-up to #97. That PR hardened the paths that consume an oversized decimal; this one stops one being created from a document in the first place.

The gap

maxNumberOfCharsForNumbers is documented as bounding the size of numbers read out of a document, but it only checked the length of the lexical value. An exponent is a handful of characters denoting arbitrarily many digits:

parse <xml-fragment>1E+2000000000</xml-fragment> as XmlDecimal, default options
  -> stored, scale=-2000000000 precision=1

13 characters, under any limit, producing a BigDecimal that is trivial to hold and catastrophic to expand. No option is needed to get there: Factory.parse doesn't validate on set, so validateLexical — the check that rejects an exponent — never runs. setLoadAllowDecimalExponent(true) reaches it by the documented route.

Why the check is safe

Exponent notation isn't part of the xsd:decimal lexical space at all; it belongs to float/double. XSD leaves the value space unbounded and only requires that a minimally conforming processor support at least 18 digits (Part 2 §3.2.3), so an implementation limit well above that is conformant either way.

The consequence that matters here: for conformant input the digit count is bounded by the length of the lexical value, so a bound on digits can never reject something the existing length check accepts. It fires only for exponent forms that were never valid xsd:decimal — which is why testParseAsBigDecimalAcceptsPlainValues can assert the plain forms are untouched.

The change

Applied in MathUtil.parseAsBigDecimal, the single point that both JavaDecimalHolder.set_text and JavaDecimalHolderEx.set_text go through, along with validateLexical's exponent branch and Validator. The bound is the length of the value written out without an exponent, which is the form xsd:decimal allows and therefore the form the limit is meant to bound:

return scale <= 0
    ? (long) precision - scale                        // integer digits
    : Math.max(precision, (long) scale + 1) + 1;      // digits either side of the point

Both directions are covered — 1E-2000000000 would otherwise make printDecimal allocate a two-billion-character StringBuilder.

A raised limit still raises the bound: 1E+2000 is rejected at the default 1024 and accepted at maxChars(4096).

Still open

setBigDecimalValue remains unbounded, so a value like this can still be constructed programmatically. That's why #97's guards in value_hash_code and to_BigInteger stay: they now defend a path that a document can no longer reach, rather than the one it could.

Tests

Two in TestMathUtil (the bound applies to denoted digits, in both exponent directions and under raised and lowered limits; plain lexical forms are unaffected) and two in MaxNumberOfCharsTest (a document is rejected with and without the exponent option, and an exponent within the limit still parses).

Full suite: 3086 tests, 0 failures.

🤖 Generated with Claude Code

maxNumberOfCharsForNumbers limits the size of numbers read out of a
document, but it only checked the length of the lexical value. An
exponent is a handful of characters denoting arbitrarily many digits, so
the 13 characters of "1E+2000000000" got through and produced a
BigDecimal of scale -2000000000: trivial to hold, catastrophic to expand.
No option is needed to reach this - Factory.parse does not validate on
set, so the lexical check that rejects an exponent never runs.

Exponent notation is not part of the xsd:decimal lexical space to begin
with, so for conformant input the digit count is bounded by the length of
the lexical value and this check never fires. Apply it in
parseAsBigDecimal, which is the single point both set_text paths and
validateLexical go through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@pjfanning
pjfanning merged commit c99ee98 into apache:trunk Aug 26, 2026
2 checks passed
@pjfanning
pjfanning deleted the limit-decimal-exponent-digits branch August 26, 2026 23:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant