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

garydgregory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-secure-xml.git


The following commit(s) were added to refs/heads/main by this push:
     new da38c00  Create the Transformer of an XMLFilter eagerly and reuse it 
(#95)
da38c00 is described below

commit da38c00b2499d0e6b61985ba5e8f7b8aea315ce8
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Mon Sep 14 00:42:42 2026 +0200

    Create the Transformer of an XMLFilter eagerly and reuse it (#95)
    
    * Create the Transformer of an XMLFilter eagerly.
    
    SAXTransformerFactory.newXMLFilter now builds the Transformer the filter
    drives, so a failure to create it is reported from newXMLFilter rather than
    from the first parse, and every parse of a filter runs on that one
    Transformer, the way the stock TrAX filters do.
    
    The work a parse used to repeat moves to the setters: setParent wires the
    resolver, DTD and error callbacks onto the parent, and setContentHandler
    builds the SAXResult the transformation writes to. It is the filter that is
    installed on the parent, not the caller's callbacks, so a resolver or
    handler the caller sets afterwards is still reached.
    
    The ContentHandler role inherited from XMLFilterImpl is not usable: events
    pushed into it would reach the caller's handler untransformed, so
    startDocument now fails and names the TrAX shape for that job.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
    
    * Report a missing XMLFilter in the TrAX shape.
    
    newXMLFilter(Source) handed back the null its implementation returned for a
    stylesheet that failed to compile, while newXMLFilter(Templates) threw when
    the Templates produced no Transformer. Both now report the failure as a
    TransformerConfigurationException: a filter has no null to hand back, and
    null in that contract means the factory supports no filters at all.
    
    The products the factory wraps rather than synthesizes keep preserving the
    implementation's null.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
    
    * Document the type parameter of the null check.
    
    Checkstyle 14.1.0, which the build resolves on recent JDKs, requires a
    @param tag for a method's type parameter.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
    
    * fix: remove implementation details from doc
---
 src/changes/changes.xml                            |   2 +
 .../commons/xml/secure/SecureTransformer.java      |   2 +-
 .../xml/secure/SecureTransformerFactory.java       |  26 +++++-
 .../apache/commons/xml/secure/SecureXMLFilter.java | 100 ++++++++++++++++-----
 .../xml/secure/SecureTransformerFactoryTest.java   |  23 ++++-
 .../commons/xml/secure/SecureXMLFilterTest.java    |  34 ++++++-
 .../apache/commons/xml/secure/XMLFilterTest.java   |  22 ++++-
 7 files changed, 173 insertions(+), 36 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 3c8af33..837d9cd 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -37,6 +37,8 @@ The <action> type attribute can be add, update, fix, or 
remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix the 
OpenRewrite migration recipe to add a dependency on 
org.apache.commons:commons-secure-xml:1.0.0.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Fix rejection behavior of foreign Templates in 
SAXTransformerFactory.newTransformerHandler.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Fix a NullPointerException when an XMLFilter with a self-driven parent 
reader is parsed with a null InputSource. #86</action>
+      <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Create the Transformer of an XMLFilter eagerly and reuse it for every 
parse.</action>
+      <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Report a stylesheet that produces no XMLFilter as a 
TransformerConfigurationException instead of returning null.</action>
       <!-- ADD -->
       <!-- UPDATE -->
       <action dev="ggregory" type="update" due-to="Gary Gregory">Bump 
org.apache.commons:commons-parent from 104 to 105.</action>
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
index 1ec40d5..d84127c 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
@@ -54,7 +54,7 @@ final class SecureTransformer extends Transformer {
      * Snapshot of the factory's {@value 
SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome at creation, like the 
JDK copies the feature onto the
      * transformers it creates.
      */
-    private final boolean overrideDefaultParser;
+    final boolean overrideDefaultParser;
 
     /**
      * Constructs a new instance.
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
index a609e1e..8b6bd48 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
@@ -148,6 +148,23 @@ private static boolean probeOverrideDefaultParser(final 
SAXTransformerFactory fa
             }
         }
 
+        /**
+         * Returns the value, or reports its absence in the TrAX shape.
+         *
+         * @param <T>   The type of the product.
+         * @param value The value an implementation produced.
+         * @param what  Name of the missing product, for the message.
+         * @return The value, never {@code null}.
+         * @throws TransformerConfigurationException Thrown if {@code value} 
is {@code null}.
+         */
+        private static <T> T required(final T value, final String what) throws 
TransformerConfigurationException {
+            // Xalan hands back null instead of throwing when the stylesheet 
failed to compile: XALANJ-2410.
+            if (value == null) {
+                throw new TransformerConfigurationException("Underlying 
implementation returned a null " + what + ".");
+            }
+            return value;
+        }
+
         private static Templates unwrap(final Templates templates) {
             return templates instanceof SecureTemplates ? ((SecureTemplates) 
templates).getDelegate() : templates;
         }
@@ -336,14 +353,15 @@ public TransformerHandler newTransformerHandler(final 
Templates templates) throw
          */
         @Override
         public XMLFilter newXMLFilter(final Source source) throws 
TransformerConfigurationException {
-            final Templates templates = newTemplates(source);
-            return templates == null ? null : new 
SecureXMLFilter((SecureTemplates) templates);
+            return newXMLFilter(required(newTemplates(source), "Templates"));
         }
 
         @Override
         public XMLFilter newXMLFilter(final Templates templates) throws 
TransformerConfigurationException {
-            return new SecureXMLFilter(templates instanceof SecureTemplates ? 
(SecureTemplates) templates
-                    : new SecureTemplates(templates, getURIResolver(), 
emptySource, overrideDefaultParser()));
+            final Transformer transformer = 
required(templates.newTransformer(), "Transformer");
+            return new SecureXMLFilter(transformer instanceof SecureTransformer
+                    ? (SecureTransformer) transformer
+                    : new SecureTransformer(transformer, getURIResolver(), 
emptySource, overrideDefaultParser()));
         }
 
         /**
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java 
b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
index 1287c65..b887c30 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
@@ -38,7 +38,7 @@
 import org.xml.sax.helpers.XMLFilterImpl;
 
 /**
- * {@link XMLFilter} that transforms the parsed input through a {@link 
SecureTemplates} and emits the result as SAX events.
+ * {@link XMLFilter} that transforms the parsed input through a {@link 
SecureTransformer} and emits the result as SAX events.
  *
  * <p>Composed from the library's own wrappers instead of delegating to the 
implementation's filter, because the implementation filters self-provision an
  * unsecured reader for the input (the stock JDK's does so as early as {@code 
setContentHandler}) and cast a supplied {@link javax.xml.transform.Templates} to
@@ -46,6 +46,10 @@
  * caller has not set a parent (a caller-set parent is trusted configuration, 
used as-is), and the transformation runs on a {@link SecureTransformer}, so
  * runtime {@code document()} sits on the resolver floor. The filter is also 
the transformer's {@link ErrorListener}, forwarding TrAX error reports to the
  * caller-set {@link org.xml.sax.ErrorHandler} the way the parent reader's SAX 
reports are.</p>
+ *
+ * <p>Every parse runs on the one {@link SecureTransformer} the filter is 
constructed with, the way every stock TrAX filter is built from a single
+ * Transformer. The filter is therefore reusable for successive parses and 
inherits that transformer's reuse contract: one parse at a time, not two 
threads at
+ * once.</p>
  */
 final class SecureXMLFilter extends XMLFilterImpl implements ErrorListener {
 
@@ -71,16 +75,25 @@ private static SAXParseException toSAXParseException(final 
TransformerException
                 : new SAXParseException(e.getMessage(), locator.getPublicId(), 
locator.getSystemId(), locator.getLineNumber(), locator.getColumnNumber(), 
embedded);
     }
 
-    private final SecureTemplates templates;
+    /** Snapshot of the transformer's {@value 
SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome, carried onto the 
self-provisioned parent reader. */
+    private final boolean overrideDefaultParser;
+
+    /** Where the transformation writes, rebuilt whenever the caller sets a 
ContentHandler; {@code null} until one is set. */
+    private SAXResult result;
+
+    private final Transformer transformer;
 
     /**
      * Constructs a new instance.
      *
-     * @param templates The templates to wrap; must not be {@code null}.
-     * @throws NullPointerException Thrown if {@code templates} is {@code 
null}.
+     * @param transformer The transformer every parse runs on; must not be 
{@code null}.
+     * @throws NullPointerException Thrown if {@code transformer} is {@code 
null}.
      */
-    SecureXMLFilter(final SecureTemplates templates) {
-        this.templates = Objects.requireNonNull(templates, "templates");
+    SecureXMLFilter(final SecureTransformer transformer) {
+        this.transformer = Objects.requireNonNull(transformer, "transformer");
+        this.overrideDefaultParser = transformer.overrideDefaultParser;
+        // The filter is the listener, so TrAX error reports reach the 
caller-set ErrorHandler like the parent reader's SAX reports do.
+        transformer.setErrorListener(this);
     }
 
     /**
@@ -110,6 +123,15 @@ public void fatalError(final TransformerException e) 
throws TransformerException
         throw e;
     }
 
+    /**
+     * Gets the Transformer this filter drives.
+     *
+     * @return the filter's {@link Transformer}, never {@code null}.
+     */
+    Transformer getTransformer() {
+        return transformer;
+    }
+
     /**
      * {@inheritDoc}
      *
@@ -118,31 +140,16 @@ public void fatalError(final TransformerException e) 
throws TransformerException
      */
     @Override
     public void parse(final InputSource input) throws SAXException, 
IOException {
-        final ContentHandler handler = getContentHandler();
-        if (handler == null) {
+        if (result == null) {
             throw new SAXException("No ContentHandler set on the XMLFilter to 
receive the transformation result");
         }
         if (getParent() == null) {
-            
setParent(SecureSAXParserFactory.newXMLReader(templates.overrideDefaultParser));
-        }
-        final XMLReader parent = getParent();
-        // Like XMLFilterImpl.setupParse, minus the ContentHandler: the 
transformer owns the parent's content events and delivers the transformed 
stream to
-        // the caller's handler through the SAXResult instead.
-        parent.setEntityResolver(this);
-        parent.setDTDHandler(this);
-        parent.setErrorHandler(this);
-        final SAXResult result = new SAXResult(handler);
-        if (handler instanceof LexicalHandler) {
-            result.setLexicalHandler((LexicalHandler) handler);
+            
setParent(SecureSAXParserFactory.newXMLReader(overrideDefaultParser));
         }
         try {
-            // A new SecureTransformer per parse: the floor is installed on 
it, and transformers are not reusable across concurrent parses.
-            final Transformer transformer = templates.newTransformer();
-            // The filter is the listener, so TrAX error reports reach the 
caller-set ErrorHandler like the parent reader's SAX reports do.
-            transformer.setErrorListener(this);
             // A self-driven parent needs no InputSource, so a caller may pass 
null here; most TrAX implementations dereference the one they get unchecked.
             // See: https://issues.apache.org/jira/browse/XALANJ-2851
-            transformer.transform(new SAXSource(parent, input != null ? input 
: new InputSource(NO_INPUT_SYSTEM_ID)), result);
+            transformer.transform(new SAXSource(getParent(), input != null ? 
input : new InputSource(NO_INPUT_SYSTEM_ID)), result);
         } catch (final TransformerException e) {
             // The parent reader's parse errors and the handler's own 
exceptions arrive wrapped; rethrow the original rather than nesting the 
hierarchies.
             final Throwable cause = e.getCause();
@@ -156,6 +163,51 @@ public void parse(final InputSource input) throws 
SAXException, IOException {
         }
     }
 
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Builds the destination the transformation writes to, so a parse only 
has to run it. A handler that is also a {@link LexicalHandler} receives the
+     * result's comments and CDATA boundaries too, the way {@link 
javax.xml.transform.sax.SAXResult} expects them to be supplied.</p>
+     */
+    @Override
+    public void setContentHandler(final ContentHandler handler) {
+        super.setContentHandler(handler);
+        result = handler == null ? null : new SAXResult(handler);
+        if (handler instanceof LexicalHandler) {
+            result.setLexicalHandler((LexicalHandler) handler);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>Wires the filter onto the new parent the way {@link 
XMLFilterImpl#setupParse()} would, minus the ContentHandler: the transformer 
owns the parent's
+     * content events and delivers the transformed stream to the caller's 
handler through a {@link SAXResult} instead. Wiring the parent here rather than 
per
+     * parse is enough because it is the filter that is installed, not the 
caller's callbacks, so a callback the caller sets afterwards is still 
reached.</p>
+     */
+    @Override
+    public void setParent(final XMLReader parent) {
+        super.setParent(parent);
+        // XMLFilterImpl tolerates a null parent, so do not wire one.
+        if (parent != null) {
+            parent.setEntityResolver(this);
+            parent.setDTDHandler(this);
+            parent.setErrorHandler(this);
+        }
+    }
+
+    /**
+     * Fails: events pushed into the {@link ContentHandler} role inherited 
from {@link XMLFilterImpl} would reach the caller's handler untransformed.
+     *
+     * <p>The stock filters make that role inert too, by dropping the events 
(Apache Xalan, the JDK) or by not implementing it at all (Saxon).</p>
+     *
+     * @throws SAXException Always.
+     */
+    @Override
+    public void startDocument() throws SAXException {
+        throw new SAXException("This XMLFilter only implements ContentHandler 
for technical reasons. To push SAX events, use newTransformerHandler instead.");
+    }
+
     /** Forwards a transformation warning to the caller-set {@link 
org.xml.sax.ErrorHandler}; the transformation continues unless that handler 
throws. */
     @Override
     public void warning(final TransformerException e) throws 
TransformerException {
diff --git 
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java 
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
index 8f83029..6ca4491 100644
--- 
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
+++ 
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerFactoryTest.java
@@ -186,7 +186,8 @@ void preservesNullResultsFromEveryWrappableProduct() throws 
Exception {
         assertNull(factory.newTransformerHandler());
         assertNull(factory.newTransformerHandler(stylesheet()));
         assertNull(factory.newTransformerHandler(templates));
-        assertNull(factory.newXMLFilter(stylesheet()));
+        // A filter has no null to hand back: a null in this contract would 
mean the factory has no filters at all.
+        assertThrows(TransformerConfigurationException.class, () -> 
factory.newXMLFilter(stylesheet()));
         factory.setAttribute("test", "value");
         assertEquals("value", factory.getAttribute("test"));
         factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
@@ -234,6 +235,26 @@ public Transformer newTransformer() throws 
TransformerConfigurationException {
         }
     }
 
+    @Test
+    void rejectsTemplatesThatProduceNoTransformer() {
+        final Templates templates = new Templates() {
+
+            @Override
+            public Properties getOutputProperties() {
+                return new Properties();
+            }
+
+            @Override
+            public Transformer newTransformer() {
+                // Xalan hands back null instead of throwing when the 
stylesheet failed to compile: XALANJ-2410.
+                return null;
+            }
+        };
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
SecureTransformerFactory.newInstance();
+        // A filter has no null to hand back, so the null the wrappers 
preserve from Templates is reported in the TrAX shape instead.
+        assertThrows(TransformerConfigurationException.class, () -> 
factory.newXMLFilter(templates));
+    }
+
     @Test
     void securesAssociatedStylesheetSourcesOfEverySupportedShape() throws 
Exception {
         final SAXTransformerFactory factory = (SAXTransformerFactory) 
SecureTransformerFactory.newInstance();
diff --git 
a/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java 
b/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
index 5f68d0b..a17ff84 100644
--- a/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/SecureXMLFilterTest.java
@@ -18,6 +18,7 @@
 package org.apache.commons.xml.secure;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -53,7 +54,7 @@ class SecureXMLFilterTest {
     private static SecureXMLFilter filter() throws Exception {
         final Templates templates = 
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
                 "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template 
match='@*|node()'><xsl:copy><xsl:apply-templates 
select='@*|node()'/></xsl:copy></xsl:template></xsl:stylesheet>")));
-        return new SecureXMLFilter(new SecureTemplates(templates, null, null, 
false));
+        return new SecureXMLFilter(new 
SecureTransformer(templates.newTransformer(), null, null, false));
     }
 
     @Test
@@ -71,6 +72,14 @@ public void startElement(final String uri, final String 
localName, final String
         assertEquals("handler", exception.getMessage());
     }
 
+    @Test
+    void rejectsSaxEventsPushedIntoTheFilter() throws Exception {
+        final SecureXMLFilter filter = filter();
+        filter.setContentHandler(new DefaultHandler());
+        // The ContentHandler role is inherited from XMLFilterImpl; events 
pushed in that way would reach the handler untransformed.
+        assertThrows(SAXException.class, filter::startDocument);
+    }
+
     @Test
     void reportsWarningErrorAndFatalErrorUsingSaxShape() throws Exception {
         final SecureXMLFilter filter = filter();
@@ -188,12 +197,33 @@ public void transform(final Source source, final Result 
result) throws Transform
                 };
             }
         };
-        final SecureXMLFilter filter = new SecureXMLFilter(new 
SecureTemplates(templates, null, null, false));
+        final SecureXMLFilter filter = new SecureXMLFilter(new 
SecureTransformer(templates.newTransformer(), null, null, false));
         filter.setContentHandler(new DefaultHandler());
         final IOException exception = assertThrows(IOException.class, () -> 
filter.parse(new InputSource(new StringReader("<root/>"))));
         assertEquals("transform", exception.getMessage());
     }
 
+    @Test
+    void reusesOneTransformerAcrossParses() throws Exception {
+        final Templates templates = 
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
+                "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:param name='p'/>"
+                        + "<xsl:template match='/'><out><xsl:value-of 
select='$p'/></out></xsl:template></xsl:stylesheet>")));
+        final SecureXMLFilter filter = new SecureXMLFilter(new 
SecureTransformer(templates.newTransformer(), null, null, false));
+        final Transformer transformer = filter.getTransformer();
+        assertInstanceOf(SecureTransformer.class, transformer, "the reused 
Transformer must carry the resolver floor");
+        transformer.setParameter("p", "carried");
+        final StringBuilder first = new StringBuilder();
+        filter.setContentHandler(AttackTestSupport.capturingHandler(first));
+        filter.parse(new InputSource(new StringReader("<root/>")));
+        assertEquals("carried", first.toString());
+        final StringBuilder second = new StringBuilder();
+        filter.setContentHandler(AttackTestSupport.capturingHandler(second));
+        filter.parse(new InputSource(new StringReader("<root/>")));
+        // The parameter survives the first parse only because the second runs 
on the same Transformer.
+        assertEquals("carried", second.toString());
+        assertSame(transformer, filter.getTransformer());
+    }
+
     @Test
     void sendsLexicalEventsToALexicalContentHandler() throws Exception {
         final SecureXMLFilter filter = filter();
diff --git a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java 
b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
index d57e355..e1cedd3 100644
--- a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
@@ -168,6 +168,14 @@ void secureFilterDoesNotLeakDocument() throws Exception {
         assertFalse(filterAndCapture(filter, 
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() through 
XMLFilter leaked");
     }
 
+    @Test
+    void secureFilterDoesNotLeakDocumentOnRepeatedParse() throws Exception {
+        // One Transformer drives every parse of a filter, so the floor has to 
hold on the second parse as it did on the first.
+        final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.resourceSource("with-document.xsl"));
+        assertFalse(filterAndCapture(filter, 
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() leaked on the 
first parse");
+        assertFalse(filterAndCapture(filter, 
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() leaked on the 
second parse");
+    }
+
     @Test
     void secureFilterDoesNotLeakExternalEntity() throws Exception {
         // The f003 vector: with no caller-set parent, the input must be 
parsed by a secure reader, not a self-provisioned permissive one.
@@ -182,6 +190,8 @@ void secureFilterDoesNotReWrapParseError() throws Exception 
{
         filter.setErrorHandler(AttackTestSupport.STRICT_REPORTER);
         final SAXException e = assertThrows(SAXException.class, () -> 
filter.parse(new InputSource(new StringReader("<root>"))));
         assertNotReWrapped(e);
+        // The parent is wired once, when it is set, so a repeated parse must 
still report to the caller's ErrorHandler.
+        assertNotReWrapped(assertThrows(SAXException.class, () -> 
filter.parse(new InputSource(new StringReader("<root>")))));
     }
 
     @Test
@@ -230,25 +240,29 @@ public void startDocument() throws SAXException {
 
     @Test
     void secureFilterRoutesEntityResolverToParent() throws Exception {
-        // parse must wire the caller-set EntityResolver to the parent reader, 
chaining it onto the floor so a caller can opt a specific entity in.
+        // setParent must wire the caller-set EntityResolver to the parent 
reader, chaining it onto the floor so a caller can opt a specific entity in.
         Assumptions.assumeFalse(AttackTestSupport.IS_ANDROID, "Android's Expat 
does not resolve the external general entity here");
         final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
         filter.setEntityResolver((publicId, systemId) -> new InputSource(new 
StringReader("resolved-by-caller")));
         final String output = filterAndCapture(filter, entityPayload());
         assertTrue(output.contains("resolved-by-caller"), "caller-set 
EntityResolver should opt the external entity in through the parent");
         assertFalse(output.contains(AttackTestSupport.LEAKED_MARKER), "the 
real external resource must not be fetched");
+        // The parent is wired once, when it is set, so a repeated parse must 
still reach the caller's resolver.
+        final String repeated = filterAndCapture(filter, entityPayload());
+        assertTrue(repeated.contains("resolved-by-caller"), "caller-set 
EntityResolver should still be reached on a repeated parse");
+        assertFalse(repeated.contains(AttackTestSupport.LEAKED_MARKER), "the 
real external resource must not be fetched on a repeated parse");
     }
 
     @Test
     void secureFilterWiresCallbacksToParent() throws Exception {
-        // parse must perform the XMLFilterImpl.setupParse wiring on the 
parent for the resolver, DTD and error callbacks (the ContentHandler is owned 
by the
-        // transformer). The wiring calls are asserted directly on a recording 
parent: which of them the implementation later consults or overwrites varies.
+        // setParent must perform the XMLFilterImpl.setupParse wiring on the 
parent for the resolver, DTD and error callbacks (the ContentHandler is owned by
+        // the transformer). The wiring calls are asserted directly on a 
recording parent: which of them the implementation later consults or overwrites 
varies.
         final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
         final SelfDrivenParent parent = new SelfDrivenParent();
         filter.setParent(parent);
         assertEquals("", filterAndCapture(filter, "<ignored/>"));
         assertEquals(3, parent.wired.stream().filter(callback -> callback == 
filter).count(),
-                "parse should wire the filter as the parent's EntityResolver, 
DTDHandler and ErrorHandler: " + parent.wired);
+                "setParent should wire the filter as the parent's 
EntityResolver, DTDHandler and ErrorHandler: " + parent.wired);
     }
 
     @Test

Reply via email to