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
2 changes: 1 addition & 1 deletion src/main/java/org/apache/xmlbeans/GDurationBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
28 changes: 27 additions & 1 deletion src/main/java/org/apache/xmlbeans/impl/values/XmlObjectBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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() {
Expand Down
121 changes: 121 additions & 0 deletions src/test/java/misc/checkin/MaxNumberOfCharsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
}
15 changes: 15 additions & 0 deletions src/test/java/xmlobject/schematypes/checkin/GDateTests.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading