Bound a decimal by the digits it denotes, not just its characters - #98
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
maxNumberOfCharsForNumbersis 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:13 characters, under any limit, producing a
BigDecimalthat is trivial to hold and catastrophic to expand. No option is needed to get there:Factory.parsedoesn't validate on set, sovalidateLexical— 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:decimallexical space at all; it belongs tofloat/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 whytestParseAsBigDecimalAcceptsPlainValuescan assert the plain forms are untouched.The change
Applied in
MathUtil.parseAsBigDecimal, the single point that bothJavaDecimalHolder.set_textandJavaDecimalHolderEx.set_textgo through, along withvalidateLexical's exponent branch andValidator. The bound is the length of the value written out without an exponent, which is the formxsd:decimalallows and therefore the form the limit is meant to bound:Both directions are covered —
1E-2000000000would otherwise makeprintDecimalallocate a two-billion-characterStringBuilder.A raised limit still raises the bound:
1E+2000is rejected at the default 1024 and accepted atmaxChars(4096).Still open
setBigDecimalValueremains unbounded, so a value like this can still be constructed programmatically. That's why #97's guards invalue_hash_codeandto_BigIntegerstay: 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 inMaxNumberOfCharsTest(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