diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaStringHolderEx.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaStringHolderEx.java index 1a7fd8df8..4e581c94e 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaStringHolderEx.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaStringHolderEx.java @@ -62,13 +62,17 @@ public static void validateLexical(String v, SchemaType sType, ValidationContext return; } + // xsd length facets count characters (code points), not Java UTF-16 + // code units, so a supplementary character counts as one + int cch = v.codePointCount(0, v.length()); + // check against length XmlObject len = sType.getFacet(SchemaType.FACET_LENGTH); if (len != null) { int m = ((XmlObjectBase) len).getBigIntegerValue().intValue(); - if (v.length() != m) { + if (cch != m) { context.invalid(XmlErrorCodes.DATATYPE_LENGTH_VALID$STRING, - new Object[]{"string", v.length(), m, QNameHelper.readable(sType)}); + new Object[]{"string", cch, m, QNameHelper.readable(sType)}); return; } } @@ -77,9 +81,9 @@ public static void validateLexical(String v, SchemaType sType, ValidationContext XmlObject min = sType.getFacet(SchemaType.FACET_MIN_LENGTH); if (min != null) { int m = ((XmlObjectBase) min).getBigIntegerValue().intValue(); - if (v.length() < m) { + if (cch < m) { context.invalid(XmlErrorCodes.DATATYPE_MIN_LENGTH_VALID$STRING, - new Object[]{"string", v.length(), m, QNameHelper.readable(sType)}); + new Object[]{"string", cch, m, QNameHelper.readable(sType)}); return; } } @@ -88,9 +92,9 @@ public static void validateLexical(String v, SchemaType sType, ValidationContext XmlObject max = sType.getFacet(SchemaType.FACET_MAX_LENGTH); if (max != null) { int m = ((XmlObjectBase) max).getBigIntegerValue().intValue(); - if (v.length() > m) { + if (cch > m) { context.invalid(XmlErrorCodes.DATATYPE_MAX_LENGTH_VALID$STRING, - new Object[]{"string", v.length(), m, QNameHelper.readable(sType)}); + new Object[]{"string", cch, m, QNameHelper.readable(sType)}); return; } } diff --git a/src/main/java/org/apache/xmlbeans/impl/values/JavaUriHolderEx.java b/src/main/java/org/apache/xmlbeans/impl/values/JavaUriHolderEx.java index 41817bc3d..1d8f5a4f4 100644 --- a/src/main/java/org/apache/xmlbeans/impl/values/JavaUriHolderEx.java +++ b/src/main/java/org/apache/xmlbeans/impl/values/JavaUriHolderEx.java @@ -95,22 +95,26 @@ public static void validateLexical(String v, SchemaType sType, ValidationContext XmlObject x; int i; + // xsd length facets count characters (code points), not Java UTF-16 + // code units, so a supplementary character counts as one + int cch = v.codePointCount(0, v.length()); + if ((x = sType.getFacet(SchemaType.FACET_LENGTH)) != null) { - if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) != v.length()) { + if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) != cch) { context.invalid(XmlErrorCodes.DATATYPE_LENGTH_VALID$STRING, new Object[]{"anyURI", v, i, QNameHelper.readable(sType)}); } } if ((x = sType.getFacet(SchemaType.FACET_MIN_LENGTH)) != null) { - if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) > v.length()) { + if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) > cch) { context.invalid(XmlErrorCodes.DATATYPE_MIN_LENGTH_VALID$STRING, new Object[]{"anyURI", v, i, QNameHelper.readable(sType)}); } } if ((x = sType.getFacet(SchemaType.FACET_MAX_LENGTH)) != null) { - if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) < v.length()) { + if ((i = ((SimpleValue) x).getBigIntegerValue().intValue()) < cch) { context.invalid(XmlErrorCodes.DATATYPE_MAX_LENGTH_VALID$STRING, new Object[]{"anyURI", v, i, QNameHelper.readable(sType)}); } diff --git a/src/test/java/misc/checkin/StringLengthFacetCodePointTest.java b/src/test/java/misc/checkin/StringLengthFacetCodePointTest.java new file mode 100644 index 000000000..ebbe4deba --- /dev/null +++ b/src/test/java/misc/checkin/StringLengthFacetCodePointTest.java @@ -0,0 +1,62 @@ +/* Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package misc.checkin; + +import org.apache.xmlbeans.SchemaTypeLoader; +import org.apache.xmlbeans.XmlBeans; +import org.apache.xmlbeans.XmlObject; +import org.apache.xmlbeans.impl.xb.xsdschema.SchemaDocument; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class StringLengthFacetCodePointTest { + + // U+1D54F MATHEMATICAL DOUBLE-STRUCK CAPITAL X: one character, encoded as a + // surrogate pair so String.length() is 2 but codePointCount is 1 + private static final String SUPPLEMENTARY = "𝕏"; + + private static boolean validates(String base, String facet, String value) throws Exception { + String xsd = + "" + + " " + + " " + facet + + "" + + " " + + ""; + SchemaTypeLoader loader = XmlBeans.loadXsd(new XmlObject[]{SchemaDocument.Factory.parse(xsd)}); + XmlObject doc = loader.parse("" + value + "", null, null); + return doc.validate(); + } + + @Test + void stringLengthFacetsCountCharactersNotCodeUnits() throws Exception { + // one supplementary character is one xsd character, so it meets length/maxLength of 1 + assertTrue(validates("xs:string", "", SUPPLEMENTARY)); + assertTrue(validates("xs:string", "", SUPPLEMENTARY)); + // it is one character, so it does not meet a length of 2 + assertFalse(validates("xs:string", "", SUPPLEMENTARY)); + } + + @Test + void anyUriLengthFacetsCountCharactersNotCodeUnits() throws Exception { + assertTrue(validates("xs:anyURI", "", SUPPLEMENTARY)); + assertTrue(validates("xs:anyURI", "", SUPPLEMENTARY)); + } +}