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
28 changes: 26 additions & 2 deletions src/main/java/org/apache/xmlbeans/impl/util/MathUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@ public static BigDecimal parseAsBigDecimal(String s) {
* @param maxNumberOfChars maximum number of characters allowed in the string
* @return valid BigDecimal
* @throws NumberFormatException if parse fails
* @throws IllegalArgumentException if string is too long
* @throws IllegalArgumentException if the string is too long, or denotes more digits
* than it has characters to spare
* @throws NullPointerException if string is null
*/
public static BigDecimal parseAsBigDecimal(String s, int maxNumberOfChars) {
Expand All @@ -80,7 +81,30 @@ public static BigDecimal parseAsBigDecimal(String s, int maxNumberOfChars) {
if (s.length() > maxNumberOfChars) {
throw new IllegalArgumentException("Number has more than " + maxNumberOfChars + " characters");
}
return new BigDecimal(s);
final BigDecimal value = new BigDecimal(s);
final long digits = plainLength(value);
if (digits > maxNumberOfChars) {
throw new IllegalArgumentException("Number denotes " + digits + " digits, more than the "
+ maxNumberOfChars + " characters allowed");
}
return value;
}

/**
* The number of characters it takes to write the value out without an exponent, which
* is the form xsd:decimal allows and so the form the maximum number of characters is
* meant to bound. Only an exponent can make this exceed the length of the lexical
* value it was parsed from, and an exponent is a handful of characters: the 13
* characters of "1E+2000000000" denote two billion digits.
*/
private static long plainLength(BigDecimal value) {
final int scale = value.scale();
final int precision = value.precision();
return scale <= 0
// integer digits, ie the significant digits plus the trailing zeros
? (long) precision - scale
// the digits either side of the decimal point, plus the point itself
: Math.max(precision, (long) scale + 1) + 1;
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/misc/checkin/MaxNumberOfCharsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,37 @@ public void testIntegralSettersStillRejectOutOfRange() {
assertEquals(0L, truncated.getLongValue());
}

@Test
public void testDecimalRejectsExponentDenotingTooManyDigits() throws XmlException {
// "1E+2000000000" is 13 characters, well under any limit, but denotes two billion
// digits. It is not a valid xsd:decimal lexical value in the first place, and the
// limit has to catch it whether or not the value is validated on set.
XmlDecimal value = XmlDecimal.Factory.parse(frag("1E+2000000000"));
assertThrows(XmlValueOutOfRangeException.class, value::getBigDecimalValue);

XmlOptions exponent = maxChars(XmlOptions.DEFAULT_MAX_NUMBER_CHARS);
exponent.setLoadAllowDecimalExponent(true);
XmlDecimal allowed = XmlDecimal.Factory.parse(frag("1E+2000000000"), exponent);
assertThrows(XmlValueOutOfRangeException.class, allowed::getBigDecimalValue);

XmlDecimal negative = XmlDecimal.Factory.parse(frag("1E-2000000000"), exponent);
assertThrows(XmlValueOutOfRangeException.class, negative::getBigDecimalValue);
}

@Test
public void testDecimalStillAcceptsExponentWithinLimit() throws XmlException {
XmlOptions exponent = maxChars(XmlOptions.DEFAULT_MAX_NUMBER_CHARS);
exponent.setLoadAllowDecimalExponent(true);

XmlDecimal value = XmlDecimal.Factory.parse(frag("1E+20"), exponent);
assertEquals(new BigDecimal("1E+20"), value.getBigDecimalValue());

XmlOptions raised = maxChars(4096);
raised.setLoadAllowDecimalExponent(true);
XmlDecimal wide = XmlDecimal.Factory.parse(frag("1E+2000"), raised);
assertEquals(2001, wide.getBigDecimalValue().precision() - wide.getBigDecimalValue().scale());
}

@Test
public void testDecimalHashCodeIgnoresLimit() {
// hashCode() must not throw, whatever the limit is, and must stay aligned with
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/org/apache/xmlbeans/impl/util/TestMathUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,37 @@ public void testSafeFloatToIntWithValueOutOfRange() {
assertThrows(IllegalArgumentException.class, () -> MathUtil.safeFloatToInt(Float.POSITIVE_INFINITY));
}

@Test
public void testParseAsBigDecimalBoundsDigitsNotJustCharacters() {
// an exponent is a handful of characters denoting arbitrarily many digits, so the
// limit has to apply to what the value denotes, not only to what was written
assertThrows(IllegalArgumentException.class, () -> MathUtil.parseAsBigDecimal("1E+2000000000"));
assertThrows(IllegalArgumentException.class, () -> MathUtil.parseAsBigDecimal("1E-2000000000"));
assertThrows(IllegalArgumentException.class, () -> MathUtil.parseAsBigDecimal("1E+2000"));
assertThrows(IllegalArgumentException.class, () -> MathUtil.parseAsBigDecimal("1E+9", 4));

// within the limit an exponent is still accepted, and a raised limit still raises it
assertEquals(new BigDecimal("1E+20"), MathUtil.parseAsBigDecimal("1E+20"));
assertEquals(new BigDecimal("1E+2000"), MathUtil.parseAsBigDecimal("1E+2000", 4096));
}

@Test
public void testParseAsBigDecimalAcceptsPlainValues() {
// without an exponent the digit count cannot exceed the length of the lexical
// value, so the new bound never rejects what the length check accepts
assertEquals(new BigDecimal("123.45"), MathUtil.parseAsBigDecimal("123.45"));
assertEquals(new BigDecimal("-0.001"), MathUtil.parseAsBigDecimal("-0.001"));
assertEquals(new BigDecimal("0.10"), MathUtil.parseAsBigDecimal("0.10"));
assertEquals(BigDecimal.ZERO, MathUtil.parseAsBigDecimal("0"));

StringBuilder sb = new StringBuilder("1");
for (int i = 1; i < XmlOptions.DEFAULT_MAX_NUMBER_CHARS; i++) {
sb.append('0');
}
assertEquals(XmlOptions.DEFAULT_MAX_NUMBER_CHARS,
MathUtil.parseAsBigDecimal(sb.toString()).precision());
}

@Test
public void testSafeDoubleToInt() {
assertEquals(1, MathUtil.safeDoubleToInt(1.75));
Expand Down
Loading