This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
The following commit(s) were added to refs/heads/master by this push:
new 53219e54 Walker: bound the depth of an acyclic schema walk (#163)
53219e54 is described below
commit 53219e54ff2b6e031cb7e710ae7b7edd8c6ab141
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 24 06:55:59 2026 +0100
Walker: bound the depth of an acyclic schema walk (#163)
---
README.txt | 13 +
THREAT-MODEL.md | 26 +-
.../ws/commons/schema/walker/XmlSchemaScope.java | 24 +-
.../ws/commons/schema/walker/XmlSchemaWalker.java | 58 ++++-
.../schema/walker/SubstitutionGroupTypeTest.java | 3 +-
.../schema/walker/WalkerDepthLimitTest.java | 276 +++++++++++++++++++++
6 files changed, 388 insertions(+), 12 deletions(-)
diff --git a/README.txt b/README.txt
index 1e5f2019..c5153d9b 100644
--- a/README.txt
+++ b/README.txt
@@ -39,6 +39,17 @@ adjust the per-document limits:
Maximum previously traversed events replayed while backtracking. The
default is 1000000.
+The XML Schema walker limits how deeply it recurses while walking a schema,
+so that a deeply nested schema is rejected instead of exhausting the thread
+stack. The following JVM system property adjusts the limit:
+
+ org.apache.ws.commons.schema.walker.maxDepth
+ Maximum depth of nested elements, model groups and substitution group
+ members while walking a schema; the same limit applies separately to
+ levels of type derivation and of attribute group references. The
+ default is 256, which fits a thread stack of 512 KB. Lower it if the
+ walker runs on threads with smaller stacks.
+
The schema collection limits the work performed while resolving imported and
included schemas. The following JVM system properties adjust the limits:
@@ -127,6 +138,8 @@ For example, set a limit with:
-Dorg.apache.ws.commons.schema.walker.maxDecisionPoints=20000
+ -Dorg.apache.ws.commons.schema.walker.maxDepth=128
+
-Dorg.apache.ws.commons.schema.maxImportDepth=128
-Dorg.apache.ws.commons.schema.maxNestingDepth=256
diff --git a/THREAT-MODEL.md b/THREAT-MODEL.md
index 639d0fca..9bda3e77 100644
--- a/THREAT-MODEL.md
+++ b/THREAT-MODEL.md
@@ -192,7 +192,10 @@ A finding is in-model only if it reaches a row marked
**yes**.
derivation, substitution groups, model groups, or attribute groups and
recurse until `StackOverflowError`. The walker now tracks these expansion
paths and rejects cyclic re-entry with `XmlSchemaException`; recursion that
- crosses an element declaration remains supported.
+ crosses an element declaration remains supported. Acyclic chains of the
+ same expansions could also be nested deeply enough to exhaust the stack;
+ the walker bounds their depth and rejects a deeper chain with
+ `XmlSchemaException`.
- **`XmlSchemaPathFinder`**: in-model when caller-supplied SAX events
are matched against an attacker-controlled schema. Its backtracking
work is bounded per document by configurable decision-point and
@@ -239,7 +242,8 @@ A finding is in-model only if it reaches a row marked
**yes**.
construction is bounded by a default structural nesting depth of 512;
these limits are configurable with JVM system properties. The walker has
active-path cycle detection for the schema expansion graphs it traverses,
- but large acyclic schemas may still consume substantial memory and CPU
+ and a configurable default maximum walk depth of 256, but large acyclic
+ schemas may still consume substantial memory and CPU
*(documented: `README.txt`)*.
- **System properties**: `org.apache.ws.commons.schema.extension_registry`
is consulted at `XmlSchemaCollection` construction time, and the
@@ -282,6 +286,7 @@ points*:
| `XmlSchemaCollection.setBaseUri(String)` | unset *(documented)* |
caller-supplied | base URI against which relative `schemaLocation` values
resolve |
| `org.apache.ws.commons.schema.walker.maxDecisionPoints` system property |
`10000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable
per-process limit | maximum decision points created while matching one document
|
| `org.apache.ws.commons.schema.walker.maxReplayedEvents` system property |
`1000000` *(documented: `XmlSchemaPathFinder.java`)* | operator-tunable
per-process limit | maximum previously traversed events replayed while
backtracking through one document |
+| `org.apache.ws.commons.schema.walker.maxDepth` system property | `256`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
depth of nested elements, model groups and substitution group members while
walking a schema, and separately of type derivation and of attribute group
references; a deployment running the walker on threads with small stacks (under
about 512 KB) should lower it |
| `org.apache.ws.commons.schema.maxImportDepth` system property | `64`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
import/include resolution depth for one schema read |
| `org.apache.ws.commons.schema.maxSchemaResolutions` system property | `1000`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
schema documents resolved during one top-level read |
| `org.apache.ws.commons.schema.maxNestingDepth` system property | `512`
*(documented: `README.txt`)* | operator-tunable per-process limit | maximum
structural nesting depth while building the schema model, including nested
include/import/redefine document resolutions |
@@ -350,9 +355,13 @@ leave open.
the limit is configurable with a JVM system property *(documented:
`README.txt`)*.
- Walker expansion cycles are rejected for type derivation, substitution
- groups, model groups, and attribute groups. This prevents recursive
- stack exhaustion for malformed but parseable schemas; it is not a general
- limit on the size or cost of an acyclic schema.
+ groups, model groups, and attribute groups. The depth of an acyclic walk
+ is bounded by default to 256 nested elements, model groups and
+ substitution group members, and separately to 256 levels of type
+ derivation and of attribute group references; the limit is configurable
+ with a JVM system property *(documented: `README.txt`)*. Together these
+ prevent recursive stack exhaustion for malformed but parseable schemas;
+ they are not a general limit on the size or cost of an acyclic schema.
- No rate limit on URL fetches when following `xs:import`; bounding
fetch rate is a §10 caller responsibility *(maintainer — §14 Q12)*.
- `XmlSchemaPathFinder` bounds decision points and replayed events per
@@ -440,7 +449,8 @@ leave open.
`XmlSchemaWalker.walk(XmlSchemaElement)`.
- **Property**: cyclic type derivation, substitution-group, model-group,
and attribute-group expansions terminate with `XmlSchemaException` rather
- than recursing indefinitely. Legal recursive content that passes through
+ than recursing indefinitely, as do acyclic chains of them nested beyond
+ the configurable walk depth. Legal recursive content that passes through
an element declaration remains walkable.
- **Violation symptom**: a parseable schema causes the walker to recurse
until `StackOverflowError` or another resource-exhaustion failure.
@@ -550,8 +560,8 @@ matching disclaimer.
`FEATURE_SECURE_PROCESSING=true`, but not universally.
- **Schema-amplification DoS** — large or heavily-recursive acyclic schemas
can exhaust memory or CPU within the documented resource limits;
- structural nesting and cyclic walker expansion are rejected as described
- in §8 P5 and §8 P6.
+ structural nesting and cyclic or over-deep walker expansion are rejected
+ as described in §8 P5 and §8 P6.
- **Confused-deputy fetch via untrusted `baseUri` + relative
`schemaLocation`** — the operator-supplied base URI is trusted.
diff --git
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java
index 24cd4dd8..1fcb5592 100644
---
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java
+++
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java
@@ -75,6 +75,7 @@ final class XmlSchemaScope {
private XmlSchemaAnyAttribute anyAttr;
private Set<QName> userRecognizedTypes;
private Set<XmlSchemaType> typesInProgress;
+ private int maxDepth;
private final Set<QName> attributeGroupsInProgress = new HashSet<QName>();
/**
@@ -93,6 +94,7 @@ final class XmlSchemaScope {
this.scopeCache = child.scopeCache;
this.userRecognizedTypes = child.userRecognizedTypes;
this.typesInProgress = child.typesInProgress;
+ this.maxDepth = child.maxDepth;
walkWithCycleCheck(type);
}
@@ -101,9 +103,12 @@ final class XmlSchemaScope {
* Initializes a new {@link XmlSchemaScope} with a base
* {@link XmlSchemaElement}. The element type and attributes will be
* traversed, and attribute lists and element children will be retrieved.
+ * Chains of type derivation and of attribute group references deeper
+ * than <code>maxDepth</code> are rejected.
*/
XmlSchemaScope(XmlSchemaType type, SchemasByNamespace
xmlSchemasByNamespace,
- Map<QName, XmlSchemaScope> scopeCache, Set<QName>
userRecognizedTypes) {
+ Map<QName, XmlSchemaScope> scopeCache, Set<QName>
userRecognizedTypes,
+ int maxDepth) {
this();
@@ -112,11 +117,21 @@ final class XmlSchemaScope {
this.userRecognizedTypes = userRecognizedTypes;
this.typesInProgress =
Collections.newSetFromMap(new IdentityHashMap<XmlSchemaType,
Boolean>());
+ this.maxDepth = maxDepth;
walkWithCycleCheck(type);
}
private void walkWithCycleCheck(XmlSchemaType type) {
+ // Each level of derivation recurses; an acyclic chain can still
+ // exhaust the thread stack.
+ if (typesInProgress.size() >= maxDepth) {
+ throw new XmlSchemaException("The type " + getName(type,
"{Anonymous}")
+ + " is derived through more than " +
maxDepth
+ + " levels of base types; refusing to
walk it. The limit"
+ + " may be changed with the "
+ + XmlSchemaWalker.MAX_DEPTH_PROPERTY
+ " system property.");
+ }
if (!typesInProgress.add(type)) {
throw new XmlSchemaException("Cyclic type derivation detected
involving type "
+ getName(type, "{Anonymous}") + '.');
@@ -539,6 +554,13 @@ final class XmlSchemaScope {
throw new XmlSchemaException("The attribute group reference " +
groupName
+ " does not resolve to an attribute
group in this collection.");
}
+ if (attributeGroupsInProgress.size() >= maxDepth) {
+ throw new XmlSchemaException("The attribute group reference " +
groupName
+ + " is nested more than " + maxDepth
+ + " levels deep; refusing to walk it.
The limit may be"
+ + " changed with the " +
XmlSchemaWalker.MAX_DEPTH_PROPERTY
+ + " system property.");
+ }
if ((groupName != null) && !attributeGroupsInProgress.add(groupName)) {
throw new XmlSchemaException("Cyclic attribute group reference
detected involving "
+ groupName + '.');
diff --git
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
index 34601c98..59f575c1 100644
---
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
+++
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
@@ -65,6 +65,18 @@ public final class XmlSchemaWalker {
private final IdentityHashMap<XmlSchemaType, XmlSchemaScope>
anonymousScopeCache;
private final Map<QName, XmlSchemaType> typesBySubstGroupHead;
private Set<QName> substGroupsInProgress = new HashSet<QName>();
+
+ /*
+ * The walk recurses once per nested element, model group and
+ * substitution group member. An acyclic schema can nest these deeply
+ * enough to exhaust the thread stack, so the depth is bounded. The bound
+ * is read per instance, so that setting the system property takes effect
+ * without a class reload.
+ */
+ static final String MAX_DEPTH_PROPERTY =
"org.apache.ws.commons.schema.walker.maxDepth";
+ static final int DEFAULT_MAX_DEPTH = 256;
+ private final int maxDepth = getMaxDepth();
+ private int depth;
private Set<QName> groupsInProgress = new HashSet<QName>();
/**
@@ -199,6 +211,15 @@ public final class XmlSchemaWalker {
* @param element The root element to start the walk from.
*/
public void walk(XmlSchemaElement element) {
+ enterNested();
+ try {
+ walkElement(element);
+ } finally {
+ depth--;
+ }
+ }
+
+ private void walkElement(XmlSchemaElement element) {
element = getElement(element, false);
final XmlSchemaElement substGroupElem = element;
@@ -235,7 +256,8 @@ public final class XmlSchemaWalker {
*/
scope = anonymousScopeCache.get(schemaType);
} else {
- scope = new XmlSchemaScope(schemaType, schemasByNamespace,
scopeCache, userRecognizedTypes);
+ scope = new XmlSchemaScope(schemaType, schemasByNamespace,
scopeCache, userRecognizedTypes,
+ maxDepth);
if (schemaType.getQName() != null) {
scopeCache.put(schemaType.getQName(), scope);
} else {
@@ -280,7 +302,7 @@ public final class XmlSchemaWalker {
} else {
attrScope = new XmlSchemaScope(attrType,
schemasByNamespace, scopeCache,
-
userRecognizedTypes);
+
userRecognizedTypes, maxDepth);
if (attrType.getName() != null) {
scopeCache.put(attrType.getQName(),
attrScope);
@@ -408,6 +430,15 @@ public final class XmlSchemaWalker {
}
private void walk(XmlSchemaGroupParticle group, long minOccurs, long
maxOccurs) {
+ enterNested();
+ try {
+ walkGroup(group, minOccurs, maxOccurs);
+ } finally {
+ depth--;
+ }
+ }
+
+ private void walkGroup(XmlSchemaGroupParticle group, long minOccurs, long
maxOccurs) {
// Only make a copy of the particle if the minOccurs or maxOccurs was
// set.
@@ -676,6 +707,29 @@ public final class XmlSchemaWalker {
return schemaType;
}
+ private void enterNested() {
+ if (depth >= maxDepth) {
+ throw new XmlSchemaException("The schema is nested more than " +
maxDepth
+ + " levels deep (nested elements,
model groups and"
+ + " substitution group members each
count one level);"
+ + " refusing to walk it. The limit
may be changed with the "
+ + MAX_DEPTH_PROPERTY + " system
property.");
+ }
+ depth++;
+ }
+
+ static int getMaxDepth() {
+ try {
+ Integer value = Integer.getInteger(MAX_DEPTH_PROPERTY);
+ if (value != null) {
+ return value.intValue();
+ }
+ } catch (SecurityException e) {
+ // fall through to the default
+ }
+ return DEFAULT_MAX_DEPTH;
+ }
+
private static QName getElementQName(XmlSchemaElement element) {
if (element.isRef()) {
return element.getRefBase().getTargetQName();
diff --git
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
index 26c29247..9e122685 100644
---
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
+++
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
@@ -127,7 +127,8 @@ public class SubstitutionGroupTypeTest extends Assert {
public void testLongUntypedChainResolvesEachHeadOnce() {
StringBuilder body = new StringBuilder("<xs:element name=\"root\"
type=\"xs:string\"/>");
String previous = "root";
- for (int i = 0; i < 2000; i++) {
+ // The longest chain the walker's default depth limit allows.
+ for (int i = 0; i < XmlSchemaWalker.DEFAULT_MAX_DEPTH - 1; i++) {
body.append("<xs:element name=\"e").append(i).append("\"
substitutionGroup=\"tns:")
.append(previous).append("\"/>");
previous = "e" + i;
diff --git
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java
new file mode 100644
index 00000000..f4cd8570
--- /dev/null
+++
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/WalkerDepthLimitTest.java
@@ -0,0 +1,276 @@
+/**
+ * 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 org.apache.ws.commons.schema.walker;
+
+import java.io.StringReader;
+
+import javax.xml.namespace.QName;
+
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.XmlSchemaElement;
+import org.apache.ws.commons.schema.XmlSchemaException;
+import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineGenerator;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * An acyclic schema can nest elements, model groups, substitution group
+ * members, type derivations or attribute group references deeply enough to
+ * exhaust the thread stack while it is walked. Each of these is bounded by
+ * the walker's maximum depth, and exceeding it is reported as an
+ * XmlSchemaException.
+ */
+public class WalkerDepthLimitTest extends Assert {
+
+ private static final String NS = "urn:depth";
+
+ /** Far beyond the default limit, and deep enough to overflow a default
stack without it. */
+ private static final int DEEP = 5000;
+
+ /** Comfortably within the default limit. */
+ private static final int SHALLOW = 100;
+
+ private static void walkRoot(String body) {
+ XmlSchemaCollection collection = new XmlSchemaCollection();
+ collection.read(new StringReader(
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"
xmlns:tns=\"" + NS + "\""
+ + " targetNamespace=\"" + NS + "\"
elementFormDefault=\"qualified\">"
+ + body
+ + "</xs:schema>"));
+ XmlSchemaElement root = collection.getElementByQName(new QName(NS,
"root"));
+ assertNotNull("the schema under test must declare a 'root' element",
root);
+ new XmlSchemaWalker(collection, new
XmlSchemaStateMachineGenerator()).walk(root);
+ }
+
+ private static void assertRejected(String body) {
+ try {
+ walkRoot(body);
+ fail("expected the deeply nested schema to be rejected");
+ } catch (XmlSchemaException expected) {
+ assertTrue(expected.getMessage(),
+
expected.getMessage().contains(XmlSchemaWalker.MAX_DEPTH_PROPERTY));
+ }
+ }
+
+ /** root, with e0 substituting for root, e1 for e0, and so on. */
+ private static String substitutionGroupChain(int length) {
+ StringBuilder body = new StringBuilder("<xs:element name=\"root\"
type=\"xs:string\"/>");
+ String head = "root";
+ for (int i = 0; i < length; i++) {
+ body.append("<xs:element name=\"e").append(i).append("\"
type=\"xs:string\"")
+ .append("
substitutionGroup=\"tns:").append(head).append("\"/>");
+ head = "e" + i;
+ }
+ return body.toString();
+ }
+
+ /** root contains a reference to e1, which contains a reference to e2, and
so on. */
+ private static String elementReferenceChain(int length) {
+ return elementReferenceChain(length, null);
+ }
+
+ private static String elementReferenceChain(int length, String lastType) {
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ body.append("<xs:element name=\"").append((i == 0) ? "root" : "e"
+ i).append('"');
+ if (i + 1 < length) {
+ body.append("><xs:complexType><xs:sequence><xs:element
ref=\"tns:e").append(i + 1)
+
.append("\"/></xs:sequence></xs:complexType></xs:element>");
+ } else {
+ body.append(" type=\"").append((lastType == null) ?
"xs:string" : lastType).append("\"/>");
+ }
+ }
+ return body.toString();
+ }
+
+ /** root is of type T0, which contains an element of type T1, and so on. */
+ private static String namedTypeChain(int length) {
+ StringBuilder body = new StringBuilder("<xs:element name=\"root\"
type=\"tns:T0\"/>");
+ for (int i = 0; i < length; i++) {
+ body.append("<xs:complexType
name=\"T").append(i).append("\"><xs:sequence>");
+ if (i + 1 < length) {
+ body.append("<xs:element name=\"c\" type=\"tns:T").append(i +
1).append("\"/>");
+ }
+ body.append("</xs:sequence></xs:complexType>");
+ }
+ return body.toString();
+ }
+
+ /** T1 extends T0, T2 extends T1, and so on; T0 references the given
attribute group, if any. */
+ private static String complexExtensionChain(int length, String
attributeGroup) {
+ StringBuilder body = new StringBuilder("<xs:complexType
name=\"T0\"><xs:sequence>"
+ + "<xs:element name=\"a0\"
type=\"xs:string\"/>"
+ + "</xs:sequence>");
+ if (attributeGroup != null) {
+ body.append("<xs:attributeGroup
ref=\"tns:").append(attributeGroup).append("\"/>");
+ }
+ body.append("</xs:complexType>");
+ for (int i = 1; i < length; i++) {
+ body.append("<xs:complexType
name=\"T").append(i).append("\"><xs:complexContent>")
+ .append("<xs:extension base=\"tns:T").append(i -
1).append("\"><xs:sequence>")
+ .append("<xs:element name=\"a").append(i).append("\"
type=\"xs:string\"/>")
+
.append("</xs:sequence></xs:extension></xs:complexContent></xs:complexType>");
+ }
+ return body.toString();
+ }
+
+ /** S1 restricts S0, S2 restricts S1, and so on. */
+ private static String simpleRestrictionChain(int length) {
+ StringBuilder body = new StringBuilder("<xs:element name=\"root\"
type=\"tns:S")
+ .append(length - 1).append("\"/>")
+ .append("<xs:simpleType name=\"S0\"><xs:restriction
base=\"xs:string\"/></xs:simpleType>");
+ for (int i = 1; i < length; i++) {
+ body.append("<xs:simpleType
name=\"S").append(i).append("\"><xs:restriction base=\"tns:S")
+ .append(i - 1).append("\"/></xs:simpleType>");
+ }
+ return body.toString();
+ }
+
+ /** g0 references g1, which references g2, and so on. */
+ private static String groupReferenceChain(int length) {
+ StringBuilder body = new StringBuilder("<xs:element
name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:group ref=\"tns:g0\"/>"
+ +
"</xs:sequence></xs:complexType></xs:element>");
+ for (int i = 0; i < length; i++) {
+ body.append("<xs:group
name=\"g").append(i).append("\"><xs:sequence>");
+ if (i + 1 < length) {
+ body.append("<xs:group ref=\"tns:g").append(i +
1).append("\"/>");
+ } else {
+ body.append("<xs:element name=\"leaf\" type=\"xs:string\"/>");
+ }
+ body.append("</xs:sequence></xs:group>");
+ }
+ return body.toString();
+ }
+
+ /** ag0 references ag1, which references ag2, and so on. */
+ private static String attributeGroupChain(int length) {
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ body.append("<xs:attributeGroup
name=\"ag").append(i).append("\">");
+ if (i + 1 < length) {
+ body.append("<xs:attributeGroup ref=\"tns:ag").append(i +
1).append("\"/>");
+ } else {
+ body.append("<xs:attribute name=\"a\" type=\"xs:string\"/>");
+ }
+ body.append("</xs:attributeGroup>");
+ }
+ return body.toString();
+ }
+
+ private static String rootWithAttributeGroupChain(int length) {
+ return "<xs:element name=\"root\"><xs:complexType><xs:attributeGroup
ref=\"tns:ag0\"/>"
+ + "</xs:complexType></xs:element>" +
attributeGroupChain(length);
+ }
+
+ private static String rootWithComplexExtensionChain(int length) {
+ return "<xs:element name=\"root\" type=\"tns:T" + (length - 1) + "\"/>"
+ + complexExtensionChain(length, null);
+ }
+
+ @Test
+ public void testDeepSubstitutionGroupChainIsRejected() {
+ assertRejected(substitutionGroupChain(DEEP));
+ }
+
+ @Test
+ public void testDeepElementReferenceChainIsRejected() {
+ assertRejected(elementReferenceChain(DEEP));
+ }
+
+ @Test
+ public void testDeepNamedTypeChainIsRejected() {
+ assertRejected(namedTypeChain(DEEP));
+ }
+
+ @Test
+ public void testDeepGroupReferenceChainIsRejected() {
+ assertRejected(groupReferenceChain(DEEP));
+ }
+
+ @Test
+ public void testDeepComplexExtensionChainIsRejected() {
+ assertRejected(rootWithComplexExtensionChain(DEEP));
+ }
+
+ @Test
+ public void testDeepSimpleRestrictionChainIsRejected() {
+ assertRejected(simpleRestrictionChain(DEEP));
+ }
+
+ @Test
+ public void testDeepAttributeGroupChainIsRejected() {
+ assertRejected(rootWithAttributeGroupChain(DEEP));
+ }
+
+ @Test
+ public void testShallowChainsStillWalk() {
+ walkRoot(substitutionGroupChain(SHALLOW));
+ walkRoot(elementReferenceChain(SHALLOW));
+ walkRoot(namedTypeChain(SHALLOW));
+ walkRoot(groupReferenceChain(SHALLOW));
+ walkRoot(rootWithComplexExtensionChain(SHALLOW));
+ walkRoot(simpleRestrictionChain(SHALLOW));
+ walkRoot(rootWithAttributeGroupChain(SHALLOW));
+ }
+
+ /*
+ * Type derivation and attribute group references are resolved while the
+ * element walk is already at depth, so the stack must hold the element
+ * walk and either chain at their limits at once. A complex type's
+ * content is walked as one nested model group per level of extension,
+ * so the deepest derivation that does not also nest content is a simple
+ * type's.
+ */
+ @Test
+ public void testAllChainsJustWithinTheLimitsTogetherStillWalk() {
+ final int limit = XmlSchemaWalker.DEFAULT_MAX_DEPTH;
+ // Each element level counts itself and its sequence.
+ final int elementLevels = limit / 2;
+ // The leaf type and the built-in xs:string at the base of the
+ // chain each count one level of derivation too.
+ final int simpleDerivations = limit - 2;
+ walkRoot(elementReferenceChain(elementLevels, "tns:Leaf")
+ + "<xs:complexType name=\"Leaf\"><xs:simpleContent>"
+ + "<xs:extension base=\"tns:S" + (simpleDerivations - 1) +
"\">"
+ + "<xs:attributeGroup ref=\"tns:ag0\"/>"
+ + "</xs:extension></xs:simpleContent></xs:complexType>"
+ +
simpleRestrictionChain(simpleDerivations).replaceFirst("<xs:element [^>]*/>",
"")
+ + attributeGroupChain(limit));
+ }
+
+ @Test
+ public void testLimitIsConfigurable() {
+ final String previous =
System.getProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY);
+ System.setProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY, "10");
+ try {
+ assertRejected(substitutionGroupChain(20));
+ assertRejected(rootWithComplexExtensionChain(20));
+ assertRejected(rootWithAttributeGroupChain(20));
+ walkRoot(substitutionGroupChain(5));
+ } finally {
+ if (previous == null) {
+ System.clearProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY);
+ } else {
+ System.setProperty(XmlSchemaWalker.MAX_DEPTH_PROPERTY,
previous);
+ }
+ }
+ }
+}