This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 1c157be13f549bfdf8e0c2c12a9b9a769475e34c
Author: opencode <[email protected]>
AuthorDate: Wed Oct 14 16:44:23 2026 +0200

    Reset Digester state when a parse fails before end of document
    
    When getXMLReader().parse() aborted with a fatal SAX error, IOException
    or an exception from a rule, endDocument() and with it clear() never ran.
    The Digester was left with a partially built match path, entries on the
    matches stack, the parameter array pushed by CallMethodRule, objects on
    the object stack and a stale root object. A subsequent parse on the same
    instance then built its match patterns on top of the stale path, so
    rules stopped matching at the path they were registered for or matched
    at the wrong depth, and the leftover objects shifted the stack offsets
    the rules rely on.
    
    This is reachable in first-party code: ContextConfig.contextConfig()
    parses up to three context.xml files with one shared Digester and keeps
    using it after processContextConfig() swallows a parse exception, and
    StoreLoader.load() re-parses with the same instance after an
    IOException.
    
    All parse() overloads now restore a consistent state (clear() plus a
    null root) before rethrowing. clear() also clears the matches stack and
    the namespace prefix mappings, which leaked on every failed parse even
    via reset(); they are empty by the time endDocument() calls clear() on
    the success path, so this only removes residue. The code generation
    buffer is deliberately left alone because ContextConfig writes the
    generated file after processContextConfig() even when the parse failed.
    
    Adds TestDigester covering the residual state after a failed parse and
    rule matching on the parse following a failure; both tests fail against
    the previous code.
---
 java/org/apache/tomcat/util/digester/Digester.java |  23 +++-
 .../apache/tomcat/util/digester/TestDigester.java  | 144 +++++++++++++++++++++
 2 files changed, 162 insertions(+), 5 deletions(-)

diff --git a/java/org/apache/tomcat/util/digester/Digester.java 
b/java/org/apache/tomcat/util/digester/Digester.java
index 4fd75a2d93..44f4385d41 100644
--- a/java/org/apache/tomcat/util/digester/Digester.java
+++ b/java/org/apache/tomcat/util/digester/Digester.java
@@ -1595,6 +1595,12 @@ public class Digester extends DefaultHandler2 {
             InputSource input = new InputSource(fis);
             input.setSystemId("file://" + file.getAbsolutePath());
             getXMLReader().parse(input);
+        } catch (Exception e) {
+            // endDocument() is not reached when parsing fails. Restore a
+            // consistent state so the Digester can safely be reused.
+            clear();
+            root = null;
+            throw e;
         }
         return root;
     }
@@ -1613,7 +1619,15 @@ public class Digester extends DefaultHandler2 {
      */
     public Object parse(InputSource input) throws IOException, SAXException {
         configure();
-        getXMLReader().parse(input);
+        try {
+            getXMLReader().parse(input);
+        } catch (Exception e) {
+            // endDocument() is not reached when parsing fails. Restore a
+            // consistent state so the Digester can safely be reused.
+            clear();
+            root = null;
+            throw e;
+        }
         return root;
     }
 
@@ -1630,10 +1644,7 @@ public class Digester extends DefaultHandler2 {
      * @exception SAXException if a parsing exception occurs
      */
     public Object parse(InputStream input) throws IOException, SAXException {
-        configure();
-        InputSource is = new InputSource(input);
-        getXMLReader().parse(is);
-        return root;
+        return parse(new InputSource(input));
     }
 
 
@@ -1850,6 +1861,8 @@ public class Digester extends DefaultHandler2 {
     public void clear() {
 
         match = "";
+        matches.clear();
+        namespaces.clear();
         bodyTexts.clear();
         params.clear();
         publicId = null;
diff --git a/test/org/apache/tomcat/util/digester/TestDigester.java 
b/test/org/apache/tomcat/util/digester/TestDigester.java
new file mode 100644
index 0000000000..f0930c64bd
--- /dev/null
+++ b/test/org/apache/tomcat/util/digester/TestDigester.java
@@ -0,0 +1,144 @@
+/*
+ * 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.tomcat.util.digester;
+
+import java.io.StringReader;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+public class TestDigester {
+
+    /*
+     * A package level view of the internal stacks so that the state left after
+     * a failed parse can be inspected without using reflection.
+     */
+    private static final class InspectableDigester extends Digester {
+
+        int getMatchLength() {
+            return match.length();
+        }
+
+        int getMatchesSize() {
+            return matches.size();
+        }
+
+        int getNamespacesSize() {
+            return namespaces.size();
+        }
+
+        int getParamsSize() {
+            return params.size();
+        }
+    }
+
+
+    private static final class CountingRule extends Rule {
+
+        private int begins;
+
+        @Override
+        public void begin(String namespace, String name, Attributes 
attributes) throws SAXException {
+            begins++;
+        }
+
+        int getBegins() {
+            return begins;
+        }
+    }
+
+
+    /*
+     * Ensures the failed parse throws regardless of whether the parser itself
+     * re-throws fatal errors that the error handler accepts.
+     */
+    private static void rethrowFatalErrors(Digester digester) {
+        digester.setErrorHandler(new DefaultHandler() {
+            @Override
+            public void fatalError(SAXParseException exception) throws 
SAXParseException {
+                throw exception;
+            }
+        });
+    }
+
+
+    /*
+     * A fatal parse error aborts the parse before endDocument() (and with it
+     * clear()) runs, so the state must be reset explicitly to leave the
+     * Digester reusable. ContextConfig parses up to three context.xml files
+     * with a single shared Digester instance.
+     */
+    @Test
+    public void testParseFailureClearsState() throws Exception {
+        InspectableDigester digester = new InspectableDigester();
+        digester.setNamespaceAware(true);
+        rethrowFatalErrors(digester);
+        digester.addObjectCreate("root", "java.lang.Object");
+        digester.addCallMethod("root/method", "setString", 1);
+
+        try {
+            // The end tag of root is encountered while method is still open.
+            // The namespace prefix mapping is not deregistered either.
+            digester.parse(new InputSource(
+                    new StringReader("<root 
xmlns:p='urn:test:p'><method></root>")));
+            Assert.fail("Expected a SAXException for the mismatched end tag");
+        } catch (SAXException e) {
+            // Expected
+        }
+
+        Assert.assertNull(digester.getRoot());
+        Assert.assertEquals(0, digester.getCount());
+        Assert.assertEquals(0, digester.getMatchLength());
+        Assert.assertEquals(0, digester.getMatchesSize());
+        Assert.assertEquals(0, digester.getNamespacesSize());
+        Assert.assertEquals(0, digester.getParamsSize());
+    }
+
+
+    /*
+     * The match path left by a failed parse must not be carried over to the
+     * next parse on the same instance, otherwise rules no longer match at the
+     * path they were registered for.
+     */
+    @Test
+    public void testRulesMatchAfterFailedParse() throws Exception {
+        Digester digester = new Digester();
+        rethrowFatalErrors(digester);
+        CountingRule rule = new CountingRule();
+        digester.addRule("root/child", rule);
+
+        try {
+            // The end tag of root is encountered while bad is still open. The
+            // rule above does not fire for the failed parse.
+            digester.parse(new InputSource(new 
StringReader("<root><bad></root>")));
+            Assert.fail("Expected a SAXException for the mismatched end tag");
+        } catch (SAXException e) {
+            // Expected
+        }
+
+        digester.parse(new InputSource(new 
StringReader("<root><child/></root>")));
+
+        Assert.assertEquals(1, rule.getBegins());
+    }
+
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to