From c98930ee1e7e771fa51445dd8c773dfe1dad431e Mon Sep 17 00:00:00 2001 From: aizu-m Date: Sat, 20 Jun 2026 15:24:11 +0530 Subject: [PATCH 1/2] reject exponent notation in lexDecimal --- .../xmlbeans/impl/util/XsTypeConverter.java | 15 +++++++++++++++ .../java/misc/checkin/XsTypeConverterTest.java | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java index 907fc199f..3ba9e8165 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -216,6 +216,7 @@ public static String printDouble(double value) { public static BigDecimal lexDecimal(CharSequence cs) throws NumberFormatException { rejectInvalidNumber(cs); + rejectExponent(cs); final String v = cs.toString(); //TODO: review this @@ -754,4 +755,18 @@ private static void rejectInvalidNumber(CharSequence cs) { throw new NumberFormatException("For input string: \"" + cs + "\""); } } + + // BigDecimal accepts scientific notation such as "1E5", but the xsd:decimal + // lexical space does not allow an exponent - that form belongs to xsd:double + // and xsd:float. Without this check an exponent value reaching lexDecimal via + // the rich parser parses to a wrong value (e.g. "1E5" -> 100000) instead of + // being reported as invalid. + private static void rejectExponent(CharSequence cs) { + for (int i = 0, len = cs.length(); i < len; i++) { + final char c = cs.charAt(i); + if (c == 'e' || c == 'E') { + throw new NumberFormatException("invalid char '" + c + "' in decimal value"); + } + } + } } diff --git a/src/test/java/misc/checkin/XsTypeConverterTest.java b/src/test/java/misc/checkin/XsTypeConverterTest.java index 615bfbfb9..b02ed2469 100644 --- a/src/test/java/misc/checkin/XsTypeConverterTest.java +++ b/src/test/java/misc/checkin/XsTypeConverterTest.java @@ -235,4 +235,16 @@ void lexDecimalTrimsTrailingZeros() { assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.500"))); assertEquals(0, new java.math.BigDecimal("12").compareTo(XsTypeConverter.lexDecimal("12.000"))); } + + @Test + void lexDecimalRejectsExponent() { + // BigDecimal accepts scientific notation, but the xsd:decimal lexical + // space has no exponent - "1E5" used to parse to 100000 instead of + // being rejected as invalid. + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1E5")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1.5e3")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("-2E-3")); + // plain decimals stay valid + assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.5"))); + } } From 3c59560025b634d88c34de51a30666d7791a29bd Mon Sep 17 00:00:00 2001 From: aizu-m Date: Sat, 20 Jun 2026 19:23:37 +0530 Subject: [PATCH 2/2] gate decimal exponent rejection behind XmlOptions.setLoadAllowDecimalExponent --- .../java/org/apache/xmlbeans/XmlOptions.java | 42 ++++++++++++++++++- .../xmlbeans/impl/common/XmlLocale.java | 5 +++ .../apache/xmlbeans/impl/store/Locale.java | 8 ++++ .../xmlbeans/impl/util/XsTypeConverter.java | 24 ++++++++++- .../impl/values/JavaDecimalHolder.java | 18 +++++++- .../impl/values/JavaDecimalHolderEx.java | 12 ++++-- .../java/misc/checkin/XmlOptionsTest.java | 10 +++++ .../misc/checkin/XsTypeConverterTest.java | 36 +++++++++++++++- 8 files changed, 148 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/apache/xmlbeans/XmlOptions.java b/src/main/java/org/apache/xmlbeans/XmlOptions.java index a79102b48..cd557439d 100644 --- a/src/main/java/org/apache/xmlbeans/XmlOptions.java +++ b/src/main/java/org/apache/xmlbeans/XmlOptions.java @@ -158,7 +158,8 @@ public enum XmlOptionsKeys { XPATH_USE_SAXON, XPATH_USE_XMLBEANS, ATTRIBUTE_VALIDATION_COMPAT_MODE, - LOAD_STRICT_FLOATING_POINT + LOAD_STRICT_FLOATING_POINT, + LOAD_ALLOW_DECIMAL_EXPONENT } @@ -1207,6 +1208,45 @@ public boolean isLoadStrictFloatingPoint() { return hasOption(XmlOptionsKeys.LOAD_STRICT_FLOATING_POINT); } + /** + * If this option is set, xsd:decimal values are allowed to use scientific/exponent + * notation (e.g. {@code 1E5}) when parsing. That form is outside the xsd:decimal + * lexical space - it belongs to xsd:double/xsd:float - and {@link java.math.BigDecimal} + * would otherwise parse it to a wrong value ({@code 1E5 -> 100000}). The default is to + * disallow it: an exponent in a decimal is reported as invalid. Such values can also be + * expensive to parse when the exponent is very large. Set this only to restore the + * long-standing lenient behaviour. + * + * @return this + * @since 5.4.0 + */ + public XmlOptions setLoadAllowDecimalExponent() { + return setLoadAllowDecimalExponent(true); + } + + /** + * Sets whether xsd:decimal values may use scientific/exponent notation when parsing. + * See {@link #setLoadAllowDecimalExponent()}. + * + * @param b {@code true} to accept an exponent in a decimal lexical value + * @return this + * @since 5.4.0 + */ + public XmlOptions setLoadAllowDecimalExponent(boolean b) { + return set(XmlOptionsKeys.LOAD_ALLOW_DECIMAL_EXPONENT, b); + } + + /** + * Returns whether xsd:decimal values may use scientific/exponent notation when parsing. + * See {@link #setLoadAllowDecimalExponent()}. + * + * @return {@code true} if an exponent is accepted in a decimal lexical value + * @since 5.4.0 + */ + public boolean isLoadAllowDecimalExponent() { + return hasOption(XmlOptionsKeys.LOAD_ALLOW_DECIMAL_EXPONENT); + } + /** * Instructs the validator to skip elements matching an {@code } * particle with contentModel="lax". This is useful because, diff --git a/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java b/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java index 8ced9416a..93bd75cea 100755 --- a/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java +++ b/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java @@ -29,4 +29,9 @@ public interface XmlLocale // the xsd:float/xsd:double space (hex floats, the java "Infinity" token and // the f/F/d/D suffix). Driven by XmlOptions.setLoadStrictFloatingPoint. default boolean isLoadStrictFloatingPoint ( ) { return false; } + + // whether xsd:decimal lexing should accept scientific/exponent notation + // (e.g. "1E5"), which is outside the xsd:decimal lexical space. Defaults to + // false (reject). Driven by XmlOptions.setLoadAllowDecimalExponent. + default boolean isLoadAllowDecimalExponent ( ) { return false; } } diff --git a/src/main/java/org/apache/xmlbeans/impl/store/Locale.java b/src/main/java/org/apache/xmlbeans/impl/store/Locale.java index 9148c611c..f27ea53ef 100755 --- a/src/main/java/org/apache/xmlbeans/impl/store/Locale.java +++ b/src/main/java/org/apache/xmlbeans/impl/store/Locale.java @@ -105,6 +105,8 @@ private Locale(SchemaTypeLoader stl, XmlOptions options) { _loadStrictFloatingPoint = options.isLoadStrictFloatingPoint(); + _loadAllowDecimalExponent = options.isLoadAllowDecimalExponent(); + // // Check for Saaj implementation request // @@ -2077,6 +2079,10 @@ public boolean isLoadStrictFloatingPoint() { return _loadStrictFloatingPoint; } + public boolean isLoadAllowDecimalExponent() { + return _loadAllowDecimalExponent; + } + static boolean isWhiteSpace(String s) { int l = s.length(); @@ -2797,6 +2803,8 @@ public QName getQName(char[] uriSrc, int uriPos, int uriCch, boolean _loadStrictFloatingPoint; + boolean _loadAllowDecimalExponent; + int _posTemp; nthCache _nthCache_A = new nthCache(); diff --git a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java index 3ba9e8165..fbcc97d66 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -214,9 +214,31 @@ public static String printDouble(double value) { // ======================== decimal ======================== public static BigDecimal lexDecimal(CharSequence cs) + throws NumberFormatException { + return lexDecimal(cs, false); + } + + /** + * Parses an xsd:decimal lexical value. + * + * @param cs the lexical value + * @param allowExponent when {@code false} (the default) scientific/exponent notation + * such as {@code 1E5} is rejected: it is outside the xsd:decimal + * lexical space (that form belongs to xsd:double/xsd:float) and + * {@link BigDecimal} would otherwise parse it to a wrong value + * ({@code 1E5 -> 100000}). When {@code true} the long-standing + * lenient behaviour applies and an exponent is accepted. Driven by + * {@link org.apache.xmlbeans.XmlOptions#setLoadAllowDecimalExponent()}. + * @return the parsed decimal + * @throws NumberFormatException if the value is not a valid xsd:decimal + * @since 5.4.0 + */ + public static BigDecimal lexDecimal(CharSequence cs, boolean allowExponent) throws NumberFormatException { rejectInvalidNumber(cs); - rejectExponent(cs); + if (!allowExponent) { + rejectExponent(cs); + } final String v = cs.toString(); //TODO: review this 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 4ad9a9875..03b4f9f9d 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolder.java @@ -41,8 +41,9 @@ protected String compute_text(NamespaceManager nsm) { } protected void set_text(String s) { + boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent(); if (_validateOnSet()) { - validateLexical(s, _voorVc); + validateLexical(s, _voorVc, allowExponent); } try { @@ -61,6 +62,21 @@ protected void set_nil() { */ public static void validateLexical(String v, ValidationContext context) { + validateLexical(v, context, false); + } + + public static void validateLexical(String v, ValidationContext context, boolean allowExponent) { + if (allowExponent) { + // long-standing lenient behaviour: accept whatever BigDecimal accepts, + // which includes scientific/exponent notation such as "1E5". + try { + new BigDecimal(v); + } catch (NumberFormatException e) { + context.invalid(XmlErrorCodes.DECIMAL, new Object[]{v}); + } + return; + } + // TODO - will want to validate Chars with built in white space handling // However, this fcn sometimes takes a value with wsr applied // already diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolderEx.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolderEx.java index 7cde65eab..6cc6dd2e0 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolderEx.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaDecimalHolderEx.java @@ -38,7 +38,8 @@ public JavaDecimalHolderEx(SchemaType type, boolean complex) { protected void set_text(String s) { if (_validateOnSet()) { - validateLexical(s, _schemaType, _voorVc); + boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent(); + validateLexical(s, _schemaType, _voorVc, allowExponent); } BigDecimal v = null; @@ -63,7 +64,11 @@ protected void set_BigDecimal(BigDecimal v) { } public static void validateLexical(String v, SchemaType sType, ValidationContext context) { - JavaDecimalHolder.validateLexical(v, context); + validateLexical(v, sType, context, false); + } + + public static void validateLexical(String v, SchemaType sType, ValidationContext context, boolean allowExponent) { + JavaDecimalHolder.validateLexical(v, context, allowExponent); // check pattern if (sType.hasPatternFacet()) { @@ -189,7 +194,8 @@ public static void validateValue(BigDecimal v, SchemaType sType, ValidationConte } protected void validate_simpleval(String lexical, ValidationContext ctx) { - validateLexical(lexical, schemaType(), ctx); + boolean allowExponent = has_store() && get_store().get_locale().isLoadAllowDecimalExponent(); + validateLexical(lexical, schemaType(), ctx, allowExponent); validateValue(getBigDecimalValue(), schemaType(), ctx); } diff --git a/src/test/java/misc/checkin/XmlOptionsTest.java b/src/test/java/misc/checkin/XmlOptionsTest.java index d791987ee..be7ec26a3 100644 --- a/src/test/java/misc/checkin/XmlOptionsTest.java +++ b/src/test/java/misc/checkin/XmlOptionsTest.java @@ -41,6 +41,16 @@ void testLoadStrictFloatingPointFlag() { assertFalse(xmlOptions.isLoadStrictFloatingPoint()); } + @Test + void testLoadAllowDecimalExponentFlag() { + XmlOptions xmlOptions = new XmlOptions(); + assertFalse(xmlOptions.isLoadAllowDecimalExponent()); + xmlOptions.setLoadAllowDecimalExponent(); + assertTrue(xmlOptions.isLoadAllowDecimalExponent()); + xmlOptions.setLoadAllowDecimalExponent(false); + assertFalse(xmlOptions.isLoadAllowDecimalExponent()); + } + @Test void testSaveNoAttributeWhitespaceEscapeFlag() { XmlOptions xmlOptions = new XmlOptions(); diff --git a/src/test/java/misc/checkin/XsTypeConverterTest.java b/src/test/java/misc/checkin/XsTypeConverterTest.java index b02ed2469..889d54cb0 100644 --- a/src/test/java/misc/checkin/XsTypeConverterTest.java +++ b/src/test/java/misc/checkin/XsTypeConverterTest.java @@ -14,6 +14,7 @@ */ package misc.checkin; +import org.apache.xmlbeans.XmlDecimal; import org.apache.xmlbeans.XmlDouble; import org.apache.xmlbeans.XmlFloat; import org.apache.xmlbeans.XmlOptions; @@ -197,6 +198,20 @@ void loadStrictFloatingPointOptionGatesDoubleParsing() throws Exception { XmlDouble.Factory.parse("0x1p4", strict).getDoubleValue()); } + @Test + void loadAllowDecimalExponentOptionGatesDecimalParsing() throws Exception { + // an exponent is outside the xsd:decimal lexical space, so validation + // rejects "1E5" by default + XmlOptions validate = new XmlOptions().setValidateOnSet(); + assertThrows(XmlValueOutOfRangeException.class, () -> + XmlDecimal.Factory.parse("1E5", validate).getBigDecimalValue()); + + // setLoadAllowDecimalExponent restores the lenient behaviour + XmlOptions lenient = new XmlOptions().setValidateOnSet().setLoadAllowDecimalExponent(); + assertEquals(0, new java.math.BigDecimal("100000").compareTo( + XmlDecimal.Factory.parse("1E5", lenient).getBigDecimalValue())); + } + @Test void lexLongRejectsDoubleSign() { // trimInitialPlus drops the leading '+', then Long.parseLong accepts its @@ -240,11 +255,30 @@ void lexDecimalTrimsTrailingZeros() { void lexDecimalRejectsExponent() { // BigDecimal accepts scientific notation, but the xsd:decimal lexical // space has no exponent - "1E5" used to parse to 100000 instead of - // being rejected as invalid. + // being rejected as invalid. This is the default (allowExponent = false). assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1E5")); assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1.5e3")); assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("-2E-3")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDecimal("1E5", false)); // plain decimals stay valid assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.5"))); } + + @Test + void lexDecimalAllowsExponentWhenRequested() { + // XmlOptions.setLoadAllowDecimalExponent restores the lenient behaviour: + // the exponent form is accepted again (e.g. "1E5" -> 100000). + assertEquals(0, new java.math.BigDecimal("100000").compareTo(XsTypeConverter.lexDecimal("1E5", true))); + assertEquals(0, new java.math.BigDecimal("1500").compareTo(XsTypeConverter.lexDecimal("1.5e3", true))); + // plain decimals are unaffected by the flag + assertEquals(0, new java.math.BigDecimal("1.5").compareTo(XsTypeConverter.lexDecimal("1.5", true))); + } + + @Test + void lexIntegerRejectsExponent() { + // xs:integer is parsed with BigInteger, which never accepts an exponent, + // so the exponent form is already rejected for integer types. + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexInteger("1E5")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexInteger("1e5")); + } }