This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/occurs-parsing in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 9cd70b96784576116d88c6cb26214b51fdf5cd65 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Aug 27 14:53:21 2026 +0100 Tighten Max/Min Occurs parsing --- .../apache/ws/commons/schema/SchemaBuilder.java | 44 ++++---- .../ws/commons/schema/XmlSchemaSerializer.java | 23 ++-- .../src/test/java/tests/OccursParsingTest.java | 119 +++++++++++++++++++++ 3 files changed, 157 insertions(+), 29 deletions(-) diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java index bbd215fa..5c57d6af 100644 --- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java +++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/SchemaBuilder.java @@ -228,35 +228,39 @@ public class SchemaBuilder { } long getMaxOccurs(Element el) { - try { - if (el.getAttributeNode("maxOccurs") != null) { - String value = el.getAttribute("maxOccurs"); - if ("unbounded".equals(value)) { - return Long.MAX_VALUE; - } else { - return Long.parseLong(value); + if (el.getAttributeNode("maxOccurs") != null) { + String value = el.getAttribute("maxOccurs").trim(); + if ("unbounded".equals(value)) { + return Long.MAX_VALUE; + } + try { + long parsed = Long.parseLong(value); + if (parsed < 0 || parsed == Long.MAX_VALUE) { + throw new XmlSchemaException("Invalid maxOccurs value \"" + value + "\"."); } + return parsed; + } catch (java.lang.NumberFormatException e) { + throw new XmlSchemaException("Invalid maxOccurs value \"" + value + "\".", e); } - return 1; - } catch (java.lang.NumberFormatException e) { - return 1; } + return 1; } long getMinOccurs(Element el) { - try { - if (el.getAttributeNode("minOccurs") != null) { - String value = el.getAttribute("minOccurs"); - if ("unbounded".equals(value)) { - return Long.MAX_VALUE; - } else { - return Long.parseLong(value); + if (el.getAttributeNode("minOccurs") != null) { + String value = el.getAttribute("minOccurs").trim(); + try { + long parsed = Long.parseLong(value); + if (parsed < 0 || parsed == Long.MAX_VALUE) { + throw new XmlSchemaException("Invalid minOccurs value \"" + value + "\"."); } + return parsed; + } catch (java.lang.NumberFormatException e) { + throw new XmlSchemaException("Invalid minOccurs value \"" + value + + "\" (\"unbounded\" is only permitted on maxOccurs).", e); } - return 1; - } catch (java.lang.NumberFormatException e) { - return 1; } + return 1; } /** diff --git a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaSerializer.java b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaSerializer.java index 9f5ec5d2..26be3e2c 100644 --- a/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaSerializer.java +++ b/xmlschema-core/src/main/java/org/apache/ws/commons/schema/XmlSchemaSerializer.java @@ -2379,18 +2379,23 @@ public class XmlSchemaSerializer { * @param element */ private void serializeMaxMinOccurs(XmlSchemaParticle particle, Element element) { - if (particle.getMaxOccurs() < Long.MAX_VALUE - && (particle.getMaxOccurs() > 1 || particle.getMaxOccurs() == 0)) { - element.setAttributeNS(null, "maxOccurs", particle.getMaxOccurs() + ""); - } else if (particle.getMaxOccurs() == Long.MAX_VALUE) { + long maxOccurs = particle.getMaxOccurs(); + long minOccurs = particle.getMinOccurs(); + if (minOccurs < 0 || maxOccurs < 0) { + throw new XmlSchemaException("Negative occurrence bounds cannot be serialized."); + } + if (maxOccurs == Long.MAX_VALUE) { element.setAttributeNS(null, "maxOccurs", "unbounded"); - // else not serialized + } else if (maxOccurs > 1 || maxOccurs == 0) { + element.setAttributeNS(null, "maxOccurs", maxOccurs + ""); } - // 1 is the default and hence not serialized - // there is no valid case where min occurs can be unbounded! - if (particle.getMinOccurs() > 1 || particle.getMinOccurs() == 0) { - element.setAttributeNS(null, "minOccurs", particle.getMinOccurs() + ""); + if (minOccurs == Long.MAX_VALUE) { + throw new XmlSchemaException("minOccurs == Long.MAX_VALUE cannot be serialized: " + + "unbounded is only permitted on maxOccurs."); + } + if (minOccurs > 1 || minOccurs == 0) { + element.setAttributeNS(null, "minOccurs", minOccurs + ""); } } diff --git a/xmlschema-core/src/test/java/tests/OccursParsingTest.java b/xmlschema-core/src/test/java/tests/OccursParsingTest.java new file mode 100644 index 00000000..46990a77 --- /dev/null +++ b/xmlschema-core/src/test/java/tests/OccursParsingTest.java @@ -0,0 +1,119 @@ +/** + * 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 tests; + +import java.io.StringReader; + +import javax.xml.namespace.QName; + +import org.apache.ws.commons.schema.XmlSchema; +import org.apache.ws.commons.schema.XmlSchemaCollection; +import org.apache.ws.commons.schema.XmlSchemaComplexType; +import org.apache.ws.commons.schema.XmlSchemaElement; +import org.apache.ws.commons.schema.XmlSchemaException; +import org.apache.ws.commons.schema.XmlSchemaSequence; + +import org.junit.Assert; +import org.junit.Test; + +public class OccursParsingTest extends Assert { + + private XmlSchema read(String particleAttributes) { + String schema = + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"" + + " targetNamespace=\"urn:occurs\">" + + "<xs:element name=\"root\"><xs:complexType><xs:sequence>" + + "<xs:element name=\"e\" type=\"xs:string\" " + particleAttributes + "/>" + + "</xs:sequence></xs:complexType></xs:element>" + + "</xs:schema>"; + return new XmlSchemaCollection().read(new StringReader(schema)); + } + + private XmlSchemaElement childElement(XmlSchema schema) { + XmlSchemaElement root = schema.getElementByName(new QName("urn:occurs", "root")); + XmlSchemaComplexType type = (XmlSchemaComplexType)root.getSchemaType(); + XmlSchemaSequence sequence = (XmlSchemaSequence)type.getParticle(); + return (XmlSchemaElement)sequence.getItems().get(0); + } + + private void assertRejected(String particleAttributes, String message) { + try { + read(particleAttributes); + fail(message); + } catch (XmlSchemaException expected) { + // Expected documented failure surface. + } + } + + @Test + public void testValidOccursValuesStillParse() { + XmlSchemaElement element = childElement(read("minOccurs=\"0\" maxOccurs=\"unbounded\"")); + assertEquals(0, element.getMinOccurs()); + assertEquals(Long.MAX_VALUE, element.getMaxOccurs()); + } + + @Test + public void testNonNumericMaxOccursIsRejected() { + assertRejected("maxOccurs=\"18446744073709551616\"", + "An overflowing maxOccurs value should be rejected, not coerced to 1."); + } + + @Test + public void testUnboundedMinOccursIsRejected() { + assertRejected("minOccurs=\"unbounded\"", + "minOccurs=unbounded should be rejected."); + } + + @Test + public void testNegativeMinOccursIsRejected() { + assertRejected("minOccurs=\"-1\"", + "A negative minOccurs should be rejected."); + } + + @Test + public void testNegativeMaxOccursIsRejected() { + assertRejected("maxOccurs=\"-1\"", + "A negative maxOccurs should be rejected."); + } + + @Test + public void testMaxValueLiteralMaxOccursIsRejected() { + assertRejected("maxOccurs=\"9223372036854775807\"", + "The maxOccurs sentinel literal should be rejected."); + } + + @Test + public void testMaxValueLiteralMinOccursIsRejected() { + assertRejected("minOccurs=\"9223372036854775807\"", + "The minOccurs sentinel literal should be rejected."); + } + + @Test + public void testSentinelMinOccursDoesNotSerializeSilently() throws Exception { + XmlSchema schema = read("minOccurs=\"0\""); + childElement(schema).setMinOccurs(Long.MAX_VALUE); + try { + schema.getSchemaDocument(); + fail("A sentinel minOccurs must be rejected during serialization."); + } catch (XmlSchemaException expected) { + // Expected documented failure surface. + } + } +}
