From 7774dd825c759ef2c0e78c0ed26b5ccf0cfa8ee1 Mon Sep 17 00:00:00 2001 From: aizu-m Date: Sat, 13 Jun 2026 13:07:42 +0530 Subject: [PATCH 1/4] reject non-xsd lexical forms in lexFloat and lexDouble --- .../xmlbeans/impl/util/XsTypeConverter.java | 94 +++++++++++++------ .../misc/checkin/XsTypeConverterTest.java | 31 ++++++ 2 files changed, 95 insertions(+), 30 deletions(-) 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 4fb738c5a..f361693fd 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -34,33 +34,66 @@ public final class XsTypeConverter { private static final char NAMESPACE_SEP = ':'; private static final String EMPTY_PREFIX = ""; + private static final BigDecimal DECIMAL__ZERO = new BigDecimal(0.0); // See Section 2.4.3 of FRC2396 http://www.ietf.org/rfc/rfc2396.txt private static final String[] URI_CHARS_TO_BE_REPLACED = {" ", "{", "}", "|", "\\", "^", "[", "]", "`"}; private static final String[] URI_CHARS_REPLACED_WITH = {"%20", "%7b", "%7d", "%7c", "%5c", "%5e", "%5b", "%5d", "%60"}; + // Float.parseFloat / Double.parseDouble accept lexical forms that are not + // in the XSD float/double value space: hexadecimal floats (0x1p4), the Java + // "Infinity" token, and a trailing type suffix (f/F/d/D). XSD only allows a + // decimal number with an optional exponent, or the special values INF, -INF + // and NaN. Reject the Java-only forms so they surface as invalid rather than + // being silently parsed. + private static void checkFloatingPointLexical(CharSequence cs) { + final int len = cs.length(); + for (int i = 0; i < len; i++) { + switch (cs.charAt(i)) { + case 'x': + case 'X': + case 'p': + case 'P': + case 'i': + case 't': + case 'y': + throw new NumberFormatException("invalid char '" + cs.charAt(i) + "' in floating point value"); + default: + break; + } + } + if (len > 0) { + final char last = cs.charAt(len - 1); + // a trailing 'F' is only valid as the last char of "INF" + if (last == 'd' || last == 'D' || + ((last == 'f' || last == 'F') && (len < 2 || cs.charAt(len - 2) != 'N'))) { + throw new NumberFormatException("invalid trailing char '" + last + "' in floating point value"); + } + } + } + // ======================== float ======================== public static float lexFloat(CharSequence cs) throws NumberFormatException { final String v = cs.toString(); - switch (v) { - case POS_INF_LEX: + try { + //current jdk impl of parseFloat calls trim() on the string. + //Any other space is illegal anyway, whether there are one or more spaces. + //so no need to do a collapse pass through the string. + checkFloatingPointLexical(cs); + return Float.parseFloat(v); + } catch (NumberFormatException e) { + if (v.equals(POS_INF_LEX)) { return Float.POSITIVE_INFINITY; - case NEG_INF_LEX: + } + if (v.equals(NEG_INF_LEX)) { return Float.NEGATIVE_INFINITY; - case NAN_LEX: + } + if (v.equals(NAN_LEX)) { return Float.NaN; - default: - //current jdk impl of parseFloat calls trim() on the string. - //Any other space is illegal anyway, whether there are one or more spaces. - //so no need to do a collapse pass through the string. - if (cs.length() > 1) { - char ch = cs.charAt(cs.length() - 1); - if ((ch == 'f' || ch == 'F') && cs.charAt(cs.length() - 2) != 'N') { - throw new NumberFormatException("Invalid char '" + ch + "' in float."); - } - } - return Float.parseFloat(v); + } + + throw e; } } @@ -92,24 +125,25 @@ public static String printFloat(float value) { public static double lexDouble(CharSequence cs) throws NumberFormatException { final String v = cs.toString(); - switch (v) { - case POS_INF_LEX: + + try { + //current jdk impl of parseDouble calls trim() on the string. + //Any other space is illegal anyway, whether there are one or more spaces. + //so no need to do a collapse pass through the string. + checkFloatingPointLexical(cs); + return Double.parseDouble(v); + } catch (NumberFormatException e) { + if (v.equals(POS_INF_LEX)) { return Double.POSITIVE_INFINITY; - case NEG_INF_LEX: + } + if (v.equals(NEG_INF_LEX)) { return Double.NEGATIVE_INFINITY; - case NAN_LEX: + } + if (v.equals(NAN_LEX)) { return Double.NaN; - default: - //current jdk impl of parseDouble calls trim() on the string. - //Any other space is illegal anyway, whether there are one or more spaces. - //so no need to do a collapse pass through the string. - if (cs.length() > 0) { - char ch = cs.charAt(cs.length() - 1); - if (ch == 'd' || ch == 'D') { - throw new NumberFormatException("Invalid char '" + ch + "' in double."); - } - } - return Double.parseDouble(v); + } + + throw e; } } diff --git a/src/test/java/misc/checkin/XsTypeConverterTest.java b/src/test/java/misc/checkin/XsTypeConverterTest.java index cf22bcfeb..d5567a531 100644 --- a/src/test/java/misc/checkin/XsTypeConverterTest.java +++ b/src/test/java/misc/checkin/XsTypeConverterTest.java @@ -75,6 +75,37 @@ void lexFloatAcceptsValidValues() { assertEquals(1.0f, XsTypeConverter.lexFloat("1.0")); assertEquals(Float.POSITIVE_INFINITY, XsTypeConverter.lexFloat("INF")); assertEquals(Float.NEGATIVE_INFINITY, XsTypeConverter.lexFloat("-INF")); + assertEquals(1500.0f, XsTypeConverter.lexFloat("1.5e3")); + } + + @Test + void lexFloatRejectsNonXsdLexicalForms() { + // hex floats, the java "Infinity" spelling and the double suffix are + // accepted by Float.parseFloat but are outside the xsd:float lexical space + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("0x1p4")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("Infinity")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("-Infinity")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1.0d")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1D")); + } + + @Test + void lexDoubleAcceptsValidValues() { + assertEquals(1.0, XsTypeConverter.lexDouble("1.0")); + assertEquals(Double.POSITIVE_INFINITY, XsTypeConverter.lexDouble("INF")); + assertEquals(Double.NEGATIVE_INFINITY, XsTypeConverter.lexDouble("-INF")); + assertEquals(1500.0, XsTypeConverter.lexDouble("1.5e3")); + } + + @Test + void lexDoubleRejectsNonXsdLexicalForms() { + // hex floats, the java "Infinity" spelling and the float suffix are + // accepted by Double.parseDouble but are outside the xsd:double lexical space + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("0x1p4")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("Infinity")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("-Infinity")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1.0f")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1F")); } @Test From 7f4580d94bc1bb0cc7549d74f104357829ffa1c3 Mon Sep 17 00:00:00 2001 From: aizu-m Date: Tue, 16 Jun 2026 00:24:10 +0530 Subject: [PATCH 2/4] gate strict float/double lexical checks behind XmlOptions Default parsing stays lenient (unchanged), and the strict XSD lexical check is only applied when setLoadStrictFloatingPoint is set. The flag is read off the parse Locale in JavaFloatHolder/JavaDoubleHolder.set_text, so it follows the XmlOptions already threaded through parse(), no ThreadLocals. lexFloat/lexDouble gain a strict overload; the existing signatures keep their long-standing lenient behaviour. INF/-INF/NaN and decimal/exponent forms are accepted in both modes. --- .gitignore | 1 + .../java/org/apache/xmlbeans/XmlOptions.java | 44 +++++++++- .../xmlbeans/impl/common/XmlLocale.java | 9 +- .../apache/xmlbeans/impl/store/Locale.java | 8 ++ .../xmlbeans/impl/util/XsTypeConverter.java | 60 ++++++++++++- .../impl/values/JavaDoubleHolder.java | 9 +- .../xmlbeans/impl/values/JavaFloatHolder.java | 9 +- .../java/misc/checkin/XmlOptionsTest.java | 10 +++ .../misc/checkin/XsTypeConverterTest.java | 86 ++++++++++++++++--- 9 files changed, 211 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 9644027b7..98ca6bf64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +.gradle/ lib/**/*.jar /bin gradle/wrapper/gradle-wrapper.jar diff --git a/src/main/java/org/apache/xmlbeans/XmlOptions.java b/src/main/java/org/apache/xmlbeans/XmlOptions.java index 3d055b867..6cec532fa 100644 --- a/src/main/java/org/apache/xmlbeans/XmlOptions.java +++ b/src/main/java/org/apache/xmlbeans/XmlOptions.java @@ -156,7 +156,8 @@ public enum XmlOptionsKeys { LOAD_USE_LOCALE_CHAR_UTIL, XPATH_USE_SAXON, XPATH_USE_XMLBEANS, - ATTRIBUTE_VALIDATION_COMPAT_MODE + ATTRIBUTE_VALIDATION_COMPAT_MODE, + LOAD_STRICT_FLOATING_POINT } @@ -1123,6 +1124,47 @@ public boolean isValidateOnSet() { return hasOption(XmlOptionsKeys.VALIDATE_ON_SET); } + /** + * If this option is set, xsd:float and xsd:double values are held to the XSD + * lexical space when parsing. {@link Float#parseFloat}/{@link Double#parseDouble} + * also accept lexical forms that XSD does not allow: hexadecimal floats + * ({@code 0x1p4}), the Java {@code Infinity} token, and a trailing type suffix + * ({@code f}/{@code F}/{@code d}/{@code D}). With this option set those forms are + * rejected as invalid; XSD only permits a decimal number with an optional + * exponent, or the special values {@code INF}, {@code -INF} and {@code NaN}. + * The default value is false, so the long-standing lenient behaviour is + * unchanged unless this is set. + * + * @return this + * @since 5.4.0 + */ + public XmlOptions setLoadStrictFloatingPoint() { + return setLoadStrictFloatingPoint(true); + } + + /** + * Sets whether xsd:float and xsd:double values are held to the XSD lexical + * space when parsing. See {@link #setLoadStrictFloatingPoint()}. + * + * @param b {@code true} to reject lexical forms outside the XSD float/double space + * @return this + * @since 5.4.0 + */ + public XmlOptions setLoadStrictFloatingPoint(boolean b) { + return set(XmlOptionsKeys.LOAD_STRICT_FLOATING_POINT, b); + } + + /** + * Returns whether xsd:float and xsd:double values are held to the XSD lexical + * space when parsing. See {@link #setLoadStrictFloatingPoint()}. + * + * @return {@code true} if strict XSD float/double parsing is enabled + * @since 5.4.0 + */ + public boolean isLoadStrictFloatingPoint() { + return hasOption(XmlOptionsKeys.LOAD_STRICT_FLOATING_POINT); + } + /** * 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 5f47a9f4b..8ced9416a 100755 --- a/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java +++ b/src/main/java/org/apache/xmlbeans/impl/common/XmlLocale.java @@ -21,7 +21,12 @@ public interface XmlLocale { boolean sync ( ); boolean noSync ( ); - + void enter ( ); void exit ( ); -} \ No newline at end of file + + // whether lexFloat/lexDouble should reject lexical forms that are outside + // 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; } +} 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 702f9ba3f..02728b7fa 100755 --- a/src/main/java/org/apache/xmlbeans/impl/store/Locale.java +++ b/src/main/java/org/apache/xmlbeans/impl/store/Locale.java @@ -103,6 +103,8 @@ private Locale(SchemaTypeLoader stl, XmlOptions options) { _validateOnSet = options.isValidateOnSet(); + _loadStrictFloatingPoint = options.isLoadStrictFloatingPoint(); + // // Check for Saaj implementation request // @@ -2071,6 +2073,10 @@ public boolean sync() { return !_noSync; } + public boolean isLoadStrictFloatingPoint() { + return _loadStrictFloatingPoint; + } + static boolean isWhiteSpace(String s) { int l = s.length(); @@ -2789,6 +2795,8 @@ public QName getQName(char[] uriSrc, int uriPos, int uriCch, boolean _validateOnSet; + boolean _loadStrictFloatingPoint; + 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 f361693fd..6f6cc834a 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -44,8 +44,8 @@ public final class XsTypeConverter { // in the XSD float/double value space: hexadecimal floats (0x1p4), the Java // "Infinity" token, and a trailing type suffix (f/F/d/D). XSD only allows a // decimal number with an optional exponent, or the special values INF, -INF - // and NaN. Reject the Java-only forms so they surface as invalid rather than - // being silently parsed. + // and NaN. This is only applied when strict floating point parsing is + // requested (XmlOptions.setLoadStrictFloatingPoint); the default stays lenient. private static void checkFloatingPointLexical(CharSequence cs) { final int len = cs.length(); for (int i = 0; i < len; i++) { @@ -74,13 +74,39 @@ private static void checkFloatingPointLexical(CharSequence cs) { // ======================== float ======================== public static float lexFloat(CharSequence cs) + throws NumberFormatException { + return lexFloat(cs, false); + } + + /** + * Parses an xsd:float lexical value. + * + * @param cs the lexical value + * @param strict when {@code true}, lexical forms that {@link Float#parseFloat} accepts + * but XSD does not are rejected: hexadecimal floats ({@code 0x1p4}), the + * Java {@code Infinity} token, and a trailing type suffix + * ({@code f}/{@code F}/{@code d}/{@code D}). When {@code false} the + * long-standing lenient behaviour applies. Driven by + * {@link org.apache.xmlbeans.XmlOptions#setLoadStrictFloatingPoint()}. + * @return the parsed float + * @throws NumberFormatException if the value is not a valid xsd:float + * @since 5.4.0 + */ + public static float lexFloat(CharSequence cs, boolean strict) throws NumberFormatException { final String v = cs.toString(); try { //current jdk impl of parseFloat calls trim() on the string. //Any other space is illegal anyway, whether there are one or more spaces. //so no need to do a collapse pass through the string. - checkFloatingPointLexical(cs); + if (strict) { + checkFloatingPointLexical(cs); + } else if (cs.length() > 1) { + char ch = cs.charAt(cs.length() - 1); + if ((ch == 'f' || ch == 'F') && cs.charAt(cs.length() - 2) != 'N') { + throw new NumberFormatException("Invalid char '" + ch + "' in float."); + } + } return Float.parseFloat(v); } catch (NumberFormatException e) { if (v.equals(POS_INF_LEX)) { @@ -123,6 +149,25 @@ public static String printFloat(float value) { // ======================== double ======================== public static double lexDouble(CharSequence cs) + throws NumberFormatException { + return lexDouble(cs, false); + } + + /** + * Parses an xsd:double lexical value. + * + * @param cs the lexical value + * @param strict when {@code true}, lexical forms that {@link Double#parseDouble} accepts + * but XSD does not are rejected: hexadecimal floats ({@code 0x1p4}), the + * Java {@code Infinity} token, and a trailing type suffix + * ({@code f}/{@code F}/{@code d}/{@code D}). When {@code false} the + * long-standing lenient behaviour applies. Driven by + * {@link org.apache.xmlbeans.XmlOptions#setLoadStrictFloatingPoint()}. + * @return the parsed double + * @throws NumberFormatException if the value is not a valid xsd:double + * @since 5.4.0 + */ + public static double lexDouble(CharSequence cs, boolean strict) throws NumberFormatException { final String v = cs.toString(); @@ -130,7 +175,14 @@ public static double lexDouble(CharSequence cs) //current jdk impl of parseDouble calls trim() on the string. //Any other space is illegal anyway, whether there are one or more spaces. //so no need to do a collapse pass through the string. - checkFloatingPointLexical(cs); + if (strict) { + checkFloatingPointLexical(cs); + } else if (cs.length() > 0) { + char ch = cs.charAt(cs.length() - 1); + if (ch == 'd' || ch == 'D') { + throw new NumberFormatException("Invalid char '" + ch + "' in double."); + } + } return Double.parseDouble(v); } catch (NumberFormatException e) { if (v.equals(POS_INF_LEX)) { diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaDoubleHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaDoubleHolder.java index 2ee47c13d..72759833c 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaDoubleHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaDoubleHolder.java @@ -52,12 +52,17 @@ public static String serialize(double d) { } protected void set_text(String s) { - set_double(validateLexical(s, _voorVc)); + boolean strict = has_store() && get_store().get_locale().isLoadStrictFloatingPoint(); + set_double(validateLexical(s, _voorVc, strict)); } public static double validateLexical(String v, ValidationContext context) { + return validateLexical(v, context, false); + } + + public static double validateLexical(String v, ValidationContext context, boolean strict) { try { - return XsTypeConverter.lexDouble(v); + return XsTypeConverter.lexDouble(v, strict); } catch (NumberFormatException e) { context.invalid(XmlErrorCodes.DOUBLE, new Object[]{v}); diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaFloatHolder.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaFloatHolder.java index e2365c7cb..a24c18f7d 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaFloatHolder.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaFloatHolder.java @@ -52,12 +52,17 @@ public static String serialize(float f) { } protected void set_text(String s) { - set_float(validateLexical(s, _voorVc)); + boolean strict = has_store() && get_store().get_locale().isLoadStrictFloatingPoint(); + set_float(validateLexical(s, _voorVc, strict)); } public static float validateLexical(String v, ValidationContext context) { + return validateLexical(v, context, false); + } + + public static float validateLexical(String v, ValidationContext context, boolean strict) { try { - return XsTypeConverter.lexFloat(v); + return XsTypeConverter.lexFloat(v, strict); } catch (NumberFormatException e) { context.invalid(XmlErrorCodes.FLOAT, new Object[]{v}); diff --git a/src/test/java/misc/checkin/XmlOptionsTest.java b/src/test/java/misc/checkin/XmlOptionsTest.java index 622b7ac8e..e7ee411df 100644 --- a/src/test/java/misc/checkin/XmlOptionsTest.java +++ b/src/test/java/misc/checkin/XmlOptionsTest.java @@ -30,4 +30,14 @@ void testUnsynchronizedFlag() { xmlOptions.setUnsynchronized(false); assertFalse(xmlOptions.isUnsynchronized()); } + + @Test + void testLoadStrictFloatingPointFlag() { + XmlOptions xmlOptions = new XmlOptions(); + assertFalse(xmlOptions.isLoadStrictFloatingPoint()); + xmlOptions.setLoadStrictFloatingPoint(); + assertTrue(xmlOptions.isLoadStrictFloatingPoint()); + xmlOptions.setLoadStrictFloatingPoint(false); + assertFalse(xmlOptions.isLoadStrictFloatingPoint()); + } } diff --git a/src/test/java/misc/checkin/XsTypeConverterTest.java b/src/test/java/misc/checkin/XsTypeConverterTest.java index d5567a531..702d2233c 100644 --- a/src/test/java/misc/checkin/XsTypeConverterTest.java +++ b/src/test/java/misc/checkin/XsTypeConverterTest.java @@ -14,7 +14,11 @@ */ package misc.checkin; +import org.apache.xmlbeans.XmlDouble; +import org.apache.xmlbeans.XmlFloat; +import org.apache.xmlbeans.XmlOptions; import org.apache.xmlbeans.impl.util.XsTypeConverter; +import org.apache.xmlbeans.impl.values.XmlValueOutOfRangeException; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -79,14 +83,32 @@ void lexFloatAcceptsValidValues() { } @Test - void lexFloatRejectsNonXsdLexicalForms() { - // hex floats, the java "Infinity" spelling and the double suffix are + void lexFloatLenientAcceptsJavaForms() { + // the default stays lenient: hex floats and the java "Infinity" spelling + // are accepted by Float.parseFloat, so lexFloat keeps accepting them + assertEquals(16.0f, XsTypeConverter.lexFloat("0x1p4")); + assertEquals(Float.POSITIVE_INFINITY, XsTypeConverter.lexFloat("Infinity")); + } + + @Test + void lexFloatStrictRejectsNonXsdLexicalForms() { + // hex floats, the java "Infinity" spelling and the f/F/d/D suffix are // accepted by Float.parseFloat but are outside the xsd:float lexical space - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("0x1p4")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("Infinity")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("-Infinity")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1.0d")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1D")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("0x1p4", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("Infinity", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("-Infinity", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1.0d", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1D", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexFloat("1.0f", true)); + } + + @Test + void lexFloatStrictAcceptsValidValues() { + assertEquals(1.0f, XsTypeConverter.lexFloat("1.0", true)); + assertEquals(1500.0f, XsTypeConverter.lexFloat("1.5e3", true)); + assertEquals(Float.POSITIVE_INFINITY, XsTypeConverter.lexFloat("INF", true)); + assertEquals(Float.NEGATIVE_INFINITY, XsTypeConverter.lexFloat("-INF", true)); + assertEquals(Float.NaN, XsTypeConverter.lexFloat("NaN", true)); } @Test @@ -98,14 +120,50 @@ void lexDoubleAcceptsValidValues() { } @Test - void lexDoubleRejectsNonXsdLexicalForms() { - // hex floats, the java "Infinity" spelling and the float suffix are + void lexDoubleLenientAcceptsJavaForms() { + assertEquals(16.0, XsTypeConverter.lexDouble("0x1p4")); + assertEquals(Double.POSITIVE_INFINITY, XsTypeConverter.lexDouble("Infinity")); + } + + @Test + void lexDoubleStrictRejectsNonXsdLexicalForms() { + // hex floats, the java "Infinity" spelling and the f/F/d/D suffix are // accepted by Double.parseDouble but are outside the xsd:double lexical space - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("0x1p4")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("Infinity")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("-Infinity")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1.0f")); - assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1F")); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("0x1p4", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("Infinity", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("-Infinity", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1.0f", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1F", true)); + assertThrows(NumberFormatException.class, () -> XsTypeConverter.lexDouble("1.0d", true)); + } + + @Test + void lexDoubleStrictAcceptsValidValues() { + assertEquals(1.0, XsTypeConverter.lexDouble("1.0", true)); + assertEquals(1500.0, XsTypeConverter.lexDouble("1.5e3", true)); + assertEquals(Double.POSITIVE_INFINITY, XsTypeConverter.lexDouble("INF", true)); + assertEquals(Double.NEGATIVE_INFINITY, XsTypeConverter.lexDouble("-INF", true)); + assertEquals(Double.NaN, XsTypeConverter.lexDouble("NaN", true)); + } + + @Test + void loadStrictFloatingPointOptionGatesFloatParsing() throws Exception { + // default load is lenient: the hex float parses as it always has + assertEquals(16.0f, XmlFloat.Factory.parse("0x1p4").getFloatValue()); + + // with the option set, the value is out of the xsd:float lexical space + XmlOptions strict = new XmlOptions().setLoadStrictFloatingPoint(); + assertThrows(XmlValueOutOfRangeException.class, () -> + XmlFloat.Factory.parse("0x1p4", strict).getFloatValue()); + } + + @Test + void loadStrictFloatingPointOptionGatesDoubleParsing() throws Exception { + assertEquals(16.0, XmlDouble.Factory.parse("0x1p4").getDoubleValue()); + + XmlOptions strict = new XmlOptions().setLoadStrictFloatingPoint(); + assertThrows(XmlValueOutOfRangeException.class, () -> + XmlDouble.Factory.parse("0x1p4", strict).getDoubleValue()); } @Test From d2ddce4af05ca56bb314e60d355e2c0f9b72da11 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 15 Jun 2026 20:24:16 +0100 Subject: [PATCH 3/4] Update XsTypeConverter.java --- .../xmlbeans/impl/util/XsTypeConverter.java | 73 ++++++++----------- 1 file changed, 30 insertions(+), 43 deletions(-) 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 6f6cc834a..d24a42807 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -95,32 +95,26 @@ public static float lexFloat(CharSequence cs) public static float lexFloat(CharSequence cs, boolean strict) throws NumberFormatException { final String v = cs.toString(); - try { - //current jdk impl of parseFloat calls trim() on the string. - //Any other space is illegal anyway, whether there are one or more spaces. - //so no need to do a collapse pass through the string. - if (strict) { - checkFloatingPointLexical(cs); - } else if (cs.length() > 1) { - char ch = cs.charAt(cs.length() - 1); - if ((ch == 'f' || ch == 'F') && cs.charAt(cs.length() - 2) != 'N') { - throw new NumberFormatException("Invalid char '" + ch + "' in float."); - } - } - return Float.parseFloat(v); - } catch (NumberFormatException e) { - if (v.equals(POS_INF_LEX)) { + switch (v) { + case POS_INF_LEX: return Float.POSITIVE_INFINITY; - } - if (v.equals(NEG_INF_LEX)) { + case NEG_INF_LEX: return Float.NEGATIVE_INFINITY; - } - if (v.equals(NAN_LEX)) { + case NAN_LEX: return Float.NaN; + } + //current jdk impl of parseFloat calls trim() on the string. + //Any other space is illegal anyway, whether there are one or more spaces. + //so no need to do a collapse pass through the string. + if (strict) { + checkFloatingPointLexical(cs); + } else if (cs.length() > 1) { + char ch = cs.charAt(cs.length() - 1); + if ((ch == 'f' || ch == 'F') && cs.charAt(cs.length() - 2) != 'N') { + throw new NumberFormatException("Invalid char '" + ch + "' in float."); } - - throw e; } + return Float.parseFloat(v); } public static float lexFloat(CharSequence cs, Collection errors) { @@ -170,33 +164,26 @@ public static double lexDouble(CharSequence cs) public static double lexDouble(CharSequence cs, boolean strict) throws NumberFormatException { final String v = cs.toString(); - - try { - //current jdk impl of parseDouble calls trim() on the string. - //Any other space is illegal anyway, whether there are one or more spaces. - //so no need to do a collapse pass through the string. - if (strict) { - checkFloatingPointLexical(cs); - } else if (cs.length() > 0) { - char ch = cs.charAt(cs.length() - 1); - if (ch == 'd' || ch == 'D') { - throw new NumberFormatException("Invalid char '" + ch + "' in double."); - } - } - return Double.parseDouble(v); - } catch (NumberFormatException e) { - if (v.equals(POS_INF_LEX)) { + switch (v) { + case POS_INF_LEX: return Double.POSITIVE_INFINITY; - } - if (v.equals(NEG_INF_LEX)) { + case NEG_INF_LEX: return Double.NEGATIVE_INFINITY; - } - if (v.equals(NAN_LEX)) { + case NAN_LEX: return Double.NaN; + } + //current jdk impl of parseDouble calls trim() on the string. + //Any other space is illegal anyway, whether there are one or more spaces. + //so no need to do a collapse pass through the string. + if (strict) { + checkFloatingPointLexical(cs); + } else if (cs.length() > 0) { + char ch = cs.charAt(cs.length() - 1); + if (ch == 'd' || ch == 'D') { + throw new NumberFormatException("Invalid char '" + ch + "' in double."); } - - throw e; } + return Double.parseDouble(v); } public static double lexDouble(CharSequence cs, Collection errors) { From 2683e5528f1a57715c0cffab3605a5e75f85ae60 Mon Sep 17 00:00:00 2001 From: PJ Fanning Date: Mon, 15 Jun 2026 20:28:33 +0100 Subject: [PATCH 4/4] Update XsTypeConverter.java --- src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java | 1 - 1 file changed, 1 deletion(-) 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 d24a42807..5462b6c5f 100644 --- a/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java +++ b/src/main/java/org/apache/xmlbeans/impl/util/XsTypeConverter.java @@ -34,7 +34,6 @@ public final class XsTypeConverter { private static final char NAMESPACE_SEP = ':'; private static final String EMPTY_PREFIX = ""; - private static final BigDecimal DECIMAL__ZERO = new BigDecimal(0.0); // See Section 2.4.3 of FRC2396 http://www.ietf.org/rfc/rfc2396.txt private static final String[] URI_CHARS_TO_BE_REPLACED = {" ", "{", "}", "|", "\\", "^", "[", "]", "`"};