diff --git a/src/main/java/org/apache/xmlbeans/GDurationBuilder.java b/src/main/java/org/apache/xmlbeans/GDurationBuilder.java index 6bf32d476..fcc78e93c 100644 --- a/src/main/java/org/apache/xmlbeans/GDurationBuilder.java +++ b/src/main/java/org/apache/xmlbeans/GDurationBuilder.java @@ -350,7 +350,7 @@ private void _normalizeImpl(boolean adjustSign) { if (_fs != null && (_fs.signum() < 0 || _fs.compareTo(GDate._one) >= 0)) { BigDecimal bdcarry = _fs.setScale(0, RoundingMode.FLOOR); _fs = _fs.subtract(bdcarry); - carry = MathUtil.toInt(bdcarry); + carry = MathUtil.toLong(bdcarry); } if (carry != 0 || _s < 0 || _s > 59 || _m < 0 || _m > 50 || _h < 0 || _h > 23) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java index c2bdfbc25..fad36d97e 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java @@ -152,6 +152,15 @@ protected boolean equal_to(XmlObject decimal) { private static final BigInteger _maxlong = BigInteger.valueOf(Long.MAX_VALUE); private static final BigInteger _minlong = BigInteger.valueOf(Long.MIN_VALUE); + /** + * The number of integer digits we are prepared to materialise when hashing. + * A value with a large negative scale (eg 1E+2000000000) expands to billions of + * digits, so it is hashed from its canonical form instead. hashCode() must not + * throw, so this is a fallback rather than the max-number-chars limit applied + * elsewhere. + */ + private static final long MAX_HASH_INTEGER_DIGITS = 100000; + /** * Note, this is carefully aligned with hash codes for all xsd:decimal * primitives. @@ -163,7 +172,20 @@ protected int value_hash_code() { } } - BigInteger intval = MathUtil.toBigInteger(_value, get_max_number_chars()); + // precision() - scale() is the number of integer digits, and is the same for + // every representation of a given value, so this branches consistently for + // values that compare equal + long integerDigits = (long) _value.precision() - _value.scale(); + if (integerDigits > MAX_HASH_INTEGER_DIGITS) { + // hash on the digit count and sign: both are cheap and, like the branch + // above, the same for every representation of the value. Values this wide + // collide with each other, which is allowed - expanding them is not. + return (int) integerDigits * 31 + _value.signum(); + } + + // deliberately BigDecimal.toBigInteger() and not MathUtil.toBigInteger(): + // hashCode() must not throw, and the expansion is bounded by the check above + BigInteger intval = _value.toBigInteger(); if (intval.compareTo(_maxlong) > 0 || intval.compareTo(_minlong) < 0) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaIntHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaIntHolder.java index 72d765ef2..53fb40c53 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaIntHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaIntHolder.java @@ -20,7 +20,6 @@ import org.apache.xmlbeans.XmlErrorCodes; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem; -import org.apache.xmlbeans.impl.util.MathUtil; import org.apache.xmlbeans.impl.util.XsTypeConverter; import java.math.BigDecimal; @@ -76,9 +75,12 @@ public int getIntValue() { static final BigInteger _max = BigInteger.valueOf(Integer.MAX_VALUE); static final BigInteger _min = BigInteger.valueOf(Integer.MIN_VALUE); + /** both ends of the int range are 10 digits, so anything wider is out of range */ + private static final int MAX_INT_DIGITS = 10; + // setters protected void set_BigDecimal(BigDecimal v) { - set_BigInteger(MathUtil.toBigInteger(v, get_max_number_chars())); + set_BigInteger(to_BigInteger(v, MAX_INT_DIGITS)); } protected void set_BigInteger(BigInteger v) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaIntegerHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaIntegerHolder.java index 3c4c8f8ca..9e6143e59 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaIntegerHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaIntegerHolder.java @@ -79,7 +79,7 @@ public BigInteger getBigIntegerValue() { // setters protected void set_BigDecimal(BigDecimal v) { - _value = MathUtil.toBigInteger(v, get_max_number_chars()); + _value = to_BigInteger(v); } protected void set_BigInteger(BigInteger v) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaLongHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaLongHolder.java index bc21efe96..83783a277 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaLongHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaLongHolder.java @@ -20,7 +20,6 @@ import org.apache.xmlbeans.XmlErrorCodes; import org.apache.xmlbeans.XmlObject; import org.apache.xmlbeans.impl.schema.BuiltinSchemaTypeSystem; -import org.apache.xmlbeans.impl.util.MathUtil; import org.apache.xmlbeans.impl.util.XsTypeConverter; import java.math.BigDecimal; @@ -71,9 +70,12 @@ public long getLongValue() { private static final BigInteger _max = BigInteger.valueOf(Long.MAX_VALUE); private static final BigInteger _min = BigInteger.valueOf(Long.MIN_VALUE); + /** both ends of the long range are 19 digits, so anything wider is out of range */ + private static final int MAX_LONG_DIGITS = 19; + // setters protected void set_BigDecimal(BigDecimal v) { - set_BigInteger(MathUtil.toBigInteger(v, get_max_number_chars())); + set_BigInteger(to_BigInteger(v, MAX_LONG_DIGITS)); } protected void set_BigInteger(BigInteger v) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/XmlObjectBase.java b/src/main/java/org/apache/xmlbeans/impl/values/XmlObjectBase.java index daf812a49..27bb65099 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/XmlObjectBase.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/XmlObjectBase.java @@ -1357,7 +1357,33 @@ public BigDecimal getBigDecimalValue() { // numerics: integral public BigInteger getBigIntegerValue() { BigDecimal bd = getBigDecimalValue(); - return bd == null ? null : MathUtil.toBigInteger(bd, get_max_number_chars()); + return bd == null ? null : to_BigInteger(bd); + } + + /** + * Converts to BigInteger, applying the maximum number of characters configured by + * XmlOptions.setMaxNumberOfCharsForNumbers. + */ + protected final BigInteger to_BigInteger(BigDecimal v) { + return to_BigInteger(v, get_max_number_chars()); + } + + /** + * Converts to BigInteger, rejecting anything wider than maxIntegerDigits. Types with + * a bound of their own pass that bound rather than the configured maximum number of + * characters, which limits the size of numbers read out of a document and so has no + * business rejecting a value handed to a setter directly. + *

+ * MathUtil reports an over-large magnitude as a plain IllegalArgumentException, but + * the XmlObject API reports out-of-range values as XmlValueOutOfRangeException, so + * translate it here rather than let it escape to callers. + */ + protected final BigInteger to_BigInteger(BigDecimal v, int maxIntegerDigits) { + try { + return MathUtil.toBigInteger(v, maxIntegerDigits); + } catch (IllegalArgumentException e) { + throw new XmlValueOutOfRangeException(e.getMessage()); + } } public byte getByteValue() { diff --git a/src/test/java/misc/checkin/MaxNumberOfCharsTest.java b/src/test/java/misc/checkin/MaxNumberOfCharsTest.java index ad7f32311..6e771436a 100644 --- a/src/test/java/misc/checkin/MaxNumberOfCharsTest.java +++ b/src/test/java/misc/checkin/MaxNumberOfCharsTest.java @@ -17,13 +17,19 @@ import org.apache.xmlbeans.SimpleValue; import org.apache.xmlbeans.XmlDecimal; import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlInt; import org.apache.xmlbeans.XmlInteger; +import org.apache.xmlbeans.XmlLong; import org.apache.xmlbeans.XmlOptions; import org.apache.xmlbeans.impl.values.XmlValueOutOfRangeException; import org.junit.jupiter.api.Test; +import java.math.BigDecimal; +import java.math.BigInteger; + import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; /** * XmlOptions.setMaxNumberOfCharsForNumbers has to apply to values materialised from the @@ -95,4 +101,119 @@ public void testDecimalToBigIntegerUsesDefaultLimitWhenUnset() throws XmlExcepti SimpleValue value = (SimpleValue) XmlDecimal.Factory.parse(frag(digits(2000))); assertThrows(XmlValueOutOfRangeException.class, value::getBigIntegerValue); } + + @Test + public void testDecimalToBigIntegerReportsOutOfRangeWhenSetProgrammatically() { + // setBigDecimalValue() bypasses the lexical path, so the limit is only applied + // on the way out - and has to be reported the same way it is on the way in + XmlDecimal value = XmlDecimal.Factory.newInstance(); + value.setBigDecimalValue(new BigDecimal(digits(2000))); + assertThrows(XmlValueOutOfRangeException.class, + () -> ((SimpleValue) value).getBigIntegerValue()); + } + + @Test + public void testIntegralSettersReportOutOfRange() { + BigDecimal oversized = new BigDecimal(digits(2000)); + + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlInt.Factory.newInstance().setBigDecimalValue(oversized)); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlLong.Factory.newInstance().setBigDecimalValue(oversized)); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlInteger.Factory.newInstance().setBigDecimalValue(oversized)); + + // a value that merely overflows the java type is unaffected + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlInt.Factory.newInstance().setBigDecimalValue(new BigDecimal("1E+20"))); + } + + @Test + public void testIntegralSettersUseTheirOwnBoundNotTheCharLimit() throws XmlException { + // maxNumberOfCharsForNumbers bounds numbers read out of a document; it must not + // reject a valid value handed to a setter directly, or setBigDecimalValue would + // disagree with the other setters for the same value + XmlLong value = (XmlLong) XmlLong.Factory.parse(frag("1"), maxChars(8)); + + value.setBigDecimalValue(new BigDecimal("123456789012")); + assertEquals(123456789012L, value.getLongValue()); + value.setBigIntegerValue(new BigInteger("123456789012")); + assertEquals(123456789012L, value.getLongValue()); + value.setLongValue(123456789012L); + assertEquals(123456789012L, value.getLongValue()); + + XmlInt intValue = (XmlInt) XmlInt.Factory.parse(frag("1"), maxChars(4)); + intValue.setBigDecimalValue(new BigDecimal("123456789")); + assertEquals(123456789, intValue.getIntValue()); + } + + @Test + public void testIntegralSettersStillRejectOutOfRange() { + // the type's own bound still applies, and reaching it does not need the value + // to be expanded first + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlLong.Factory.newInstance().setBigDecimalValue(new BigDecimal("1E+2000000000"))); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlInt.Factory.newInstance().setBigDecimalValue(new BigDecimal("1E+2000000000"))); + + // 19 digits is the widest a long can be at either end - precision() counts the + // digits of the unscaled value, so the sign does not consume one - and the bound + // is on width, so a 19-digit value out of range is still caught by set_BigInteger + XmlLong value = XmlLong.Factory.newInstance(); + value.setBigDecimalValue(new BigDecimal("9223372036854775807")); + assertEquals(Long.MAX_VALUE, value.getLongValue()); + value.setBigDecimalValue(new BigDecimal("-9223372036854775808")); + assertEquals(Long.MIN_VALUE, value.getLongValue()); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlLong.Factory.newInstance().setBigDecimalValue(new BigDecimal("9999999999999999999"))); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlLong.Factory.newInstance().setBigDecimalValue(new BigDecimal("-9999999999999999999"))); + + XmlInt intValue = XmlInt.Factory.newInstance(); + intValue.setBigDecimalValue(new BigDecimal("2147483647")); + assertEquals(Integer.MAX_VALUE, intValue.getIntValue()); + intValue.setBigDecimalValue(new BigDecimal("-2147483648")); + assertEquals(Integer.MIN_VALUE, intValue.getIntValue()); + assertThrows(XmlValueOutOfRangeException.class, + () -> XmlInt.Factory.newInstance().setBigDecimalValue(new BigDecimal("-9999999999"))); + + // a value below 1 truncates to zero, as it always has + XmlLong truncated = XmlLong.Factory.newInstance(); + truncated.setBigDecimalValue(new BigDecimal("1E-10000000")); + assertEquals(0L, truncated.getLongValue()); + } + + @Test + public void testDecimalHashCodeIgnoresLimit() { + // hashCode() must not throw, whatever the limit is, and must stay aligned with + // the hash of the same value held as an xsd:integer + XmlDecimal decimal = XmlDecimal.Factory.newInstance(); + decimal.setBigDecimalValue(new BigDecimal(digits(2000))); + XmlInteger integer = XmlInteger.Factory.newInstance(); + integer.setBigIntegerValue(new BigInteger(digits(2000))); + + assertEquals(integer.valueHashCode(), decimal.valueHashCode()); + } + + @Test + public void testDecimalHashCodeIsIndependentOfScale() { + // 1E+200000 and the same value written out in full are equal, and are both past + // the threshold at which hashing stops expanding the value - so the two have to + // reach the same hash without either being expanded + XmlDecimal exponent = XmlDecimal.Factory.newInstance(); + exponent.setBigDecimalValue(new BigDecimal("1E+200000")); + XmlDecimal expanded = XmlDecimal.Factory.newInstance(); + expanded.setBigDecimalValue(new BigDecimal(digits(200001))); + + assertEquals(true, exponent.valueEquals(expanded)); + assertEquals(expanded.valueHashCode(), exponent.valueHashCode()); + } + + @Test + public void testDecimalHashCodeDoesNotExpandHugeExponent() { + // expanding 1E+2000000000 would need gigabytes; hashing must not attempt it + XmlDecimal value = XmlDecimal.Factory.newInstance(); + value.setBigDecimalValue(new BigDecimal("1E+2000000000")); + assertTimeoutPreemptively(java.time.Duration.ofSeconds(10), value::valueHashCode); + } } diff --git a/src/test/java/xmlobject/schematypes/checkin/GDateTests.java b/src/test/java/xmlobject/schematypes/checkin/GDateTests.java index 06409bc51..5e5fdbdb7 100755 --- a/src/test/java/xmlobject/schematypes/checkin/GDateTests.java +++ b/src/test/java/xmlobject/schematypes/checkin/GDateTests.java @@ -690,6 +690,21 @@ void testInvalidGDurations() { assertThrows(IllegalArgumentException.class, () -> new GDuration("PT3000000000S")); } + @Test + void testDurationFractionCarryBeyondIntRange() { + // setFraction() is unvalidated, so the whole-second carry can exceed int range. + // GDateBuilder handles the same quantity as a long, so GDurationBuilder must too. + GDurationBuilder gdb = new GDurationBuilder(); + gdb.setFraction(new BigDecimal("1E+10")); + gdb.normalize(); + assertEquals("P115740DT17H46M40S", gdb.toString()); + + GDurationBuilder small = new GDurationBuilder(); + small.setFraction(new BigDecimal("1.5")); + small.normalize(); + assertEquals("PT1.5S", small.toString()); + } + // Assert-style check that prints PASS/FAIL against the expected validity. static void check(SchemaTypeLoader loader, String durationLiteral, boolean expectedValid, String note) throws Exception {