You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tomcat.apache.org by re...@apache.org on 2026/10/14 20:26:52 UTC

(tomcat) branch main updated (fe6ce0b16c -> decfd3e92d)

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

rmaucher pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


    from fe6ce0b16c Validate the cookie max-age value at deployment time
     new a6f4819e7d Reset Digester state when a parse fails before end of document
     new ced49783cb Keep the Digester loggers usable after clear()
     new 6eb71c44c0 Signal EmptyStackException for a negative ArrayStack peek index
     new 581ea1b182 Emit a null literal for missing CallMethodRule code-gen parameters
     new c8924aa6bb Decode service binding entry files as UTF-8
     new 6ded02c632 Return a URI from ConfigurationSource.DEFAULT.getURI for invalid URI names
     new d5f1d40f02 Reject If headers containing an empty list in the WebDAV servlet
     new f301588994 Replace obs-fold with a single SP when parsing headers
     new 06d7c514ba Omit invalid parameter values when serializing a MediaType without charset
     new 8a80cb4d52 Ignore parameters with no name when parsing a MediaType
     new decfd3e92d Treat an empty port when parsing a host value as an absent port

The 11 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 java/org/apache/coyote/AbstractProcessor.java      |  36 ++--
 .../apache/tomcat/util/digester/ArrayStack.java    |  15 +-
 .../tomcat/util/digester/CallMethodRule.java       |   7 +
 java/org/apache/tomcat/util/digester/Digester.java |  25 ++-
 .../digester/ServiceBindingPropertySource.java     |  27 ++-
 .../tomcat/util/file/ConfigurationSource.java      |  10 +
 .../apache/tomcat/util/http/WebdavIfHeader.java    |  43 ++++-
 .../tomcat/util/http/parser/HttpHeaderParser.java  |   7 +-
 .../apache/tomcat/util/http/parser/HttpParser.java |   4 +
 .../apache/tomcat/util/http/parser/MediaType.java  |   8 +-
 .../catalina/servlets/TestWebdavServlet.java       |  62 +++++++
 .../coyote/http11/TestHttp11InputBuffer.java       |  69 ++++++-
 .../tomcat/util/digester/TestArrayStack.java       |  88 +++++++++
 .../tomcat/util/digester/TestCallMethodRule.java   |  98 ++++++++++
 .../apache/tomcat/util/digester/TestDigester.java  | 204 +++++++++++++++++++++
 .../digester/TestServiceBindingPropertySource.java |  91 +++++++++
 .../tomcat/util/file/TestConfigurationSource.java  |  90 +++++++++
 .../tomcat/util/http/TestWebdavIfHeader.java       | 134 ++++++++++++++
 .../util/http/parser/TestHttpParserHost.java       |   9 +
 .../tomcat/util/http/parser/TestMediaType.java     |  31 ++++
 20 files changed, 1012 insertions(+), 46 deletions(-)
 create mode 100644 test/org/apache/tomcat/util/digester/TestArrayStack.java
 create mode 100644 test/org/apache/tomcat/util/digester/TestCallMethodRule.java
 create mode 100644 test/org/apache/tomcat/util/digester/TestDigester.java
 create mode 100644 test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java
 create mode 100644 test/org/apache/tomcat/util/file/TestConfigurationSource.java
 create mode 100644 test/org/apache/tomcat/util/http/TestWebdavIfHeader.java


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 01/11: Reset Digester state when a parse fails before end of document

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a6f4819e7deab1956b6611edb2c23e82132eb73d
Author: opencode <re...@apache.org>
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 f79f72fbd9..9d9b1693bb 100644
--- a/java/org/apache/tomcat/util/digester/Digester.java
+++ b/java/org/apache/tomcat/util/digester/Digester.java
@@ -1579,6 +1579,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;
     }
@@ -1597,7 +1603,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;
     }
 
@@ -1614,10 +1628,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));
     }
 
 
@@ -1834,6 +1845,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: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 02/11: Keep the Digester loggers usable after clear()

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit ced49783cb538461f75e66f3bb0eb8d758e5338d
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 16:52:23 2026 +0200

    Keep the Digester loggers usable after clear()
    
    clear() nulled log and saxLog. It runs at the end of every successful
    parse via endDocument(), on every failed parse via the reset added in
    the previous commit, and via the public reset() method, so afterwards
    the loggers stayed null until the next parse re-ran configure().
    
    That left several public methods broken in exactly the empty state
    their Javadoc promises a null return or a plain call: peek(), peek(int)
    and pop() threw a NullPointerException inside their
    catch(EmptyStackException) handler instead of returning null, peekParams
    and popParams behaved the same way, popParams(), pushParams() and
    register() hit a NullPointerException unconditionally on the trace
    enablement check, and createSAXException(String, Exception) did so on
    its log.error path when no document locator has been set, for example
    after a parse failure on a file that could not be opened.
    
    The nulling also served no purpose. JULI loggers are cached by name by
    the logging implementation, so retaining the two handles between parses
    costs nothing, and configure() re-assigned equivalent handles on every
    subsequent parse anyway. Nothing reads the fields to test for null and
    no Digester subclass in the tree overrides configure().
    
    Adds regression tests to TestDigester for the stack and parameter
    methods after a successful parse and for createSAXException() in the
    no-locator state left by a failed parse; both fail with a
    NullPointerException without this change.
---
 java/org/apache/tomcat/util/digester/Digester.java |  2 -
 .../apache/tomcat/util/digester/TestDigester.java  | 44 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 2 deletions(-)

diff --git a/java/org/apache/tomcat/util/digester/Digester.java b/java/org/apache/tomcat/util/digester/Digester.java
index 9d9b1693bb..4690c3991e 100644
--- a/java/org/apache/tomcat/util/digester/Digester.java
+++ b/java/org/apache/tomcat/util/digester/Digester.java
@@ -1851,8 +1851,6 @@ public class Digester extends DefaultHandler2 {
         params.clear();
         publicId = null;
         stack.clear();
-        log = null;
-        saxLog = null;
         configured = false;
 
     }
diff --git a/test/org/apache/tomcat/util/digester/TestDigester.java b/test/org/apache/tomcat/util/digester/TestDigester.java
index f0930c64bd..25eaf04f66 100644
--- a/test/org/apache/tomcat/util/digester/TestDigester.java
+++ b/test/org/apache/tomcat/util/digester/TestDigester.java
@@ -16,6 +16,8 @@
  */
 package org.apache.tomcat.util.digester;
 
+import java.io.File;
+import java.io.IOException;
 import java.io.StringReader;
 
 import org.junit.Assert;
@@ -141,4 +143,46 @@ public class TestDigester {
         Assert.assertEquals(1, rule.getBegins());
     }
 
+
+    /*
+     * clear() runs at the end of every successful parse. The methods that are
+     * documented to return null on an empty stack must keep working after the
+     * digester has been cleared.
+     */
+    @Test
+    public void testStackMethodsAfterSuccessfulParse() throws Exception {
+        Digester digester = new Digester();
+        digester.addObjectCreate("root", "java.lang.Object");
+
+        digester.parse(new InputSource(new StringReader("<root/>")));
+
+        Assert.assertNull(digester.peek());
+        Assert.assertNull(digester.peek(0));
+        Assert.assertNull(digester.pop());
+        Assert.assertNull(digester.peekParams());
+        Assert.assertNull(digester.popParams());
+        digester.pushParams(new Object());
+        digester.register("public-id", "system-id");
+    }
+
+
+    /*
+     * A parse failure before setDocumentLocator() leaves the locator null, so
+     * createSAXException() takes the log.error path. That path must work even
+     * though the failed parse has run clear() via the parse() failure handling.
+     */
+    @Test
+    public void testCreateSaxExceptionAfterFailedParseWithoutLocator() throws Exception {
+        Digester digester = new Digester();
+
+        try {
+            digester.parse(new File("does-not-exist.xml"));
+            Assert.fail("Expected an IOException for the missing file");
+        } catch (IOException e) {
+            // Expected
+        }
+
+        Assert.assertNotNull(digester.createSAXException("message", new RuntimeException("cause")));
+    }
+
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 09/11: Omit invalid parameter values when serializing a MediaType without charset

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 06d7c514ba9ffa417efbe163ca6d57e7b3956f6c
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 20:01:42 2026 +0200

    Omit invalid parameter values when serializing a MediaType without charset
    
    toString() skipped parameters with a null or empty value but
    toStringNoCharset() did not, so a media type such as
    "text/html;charset=utf-8;foo=" re-serialised by Response with the
    literal string "foo=null" (null values occur for unterminated quoted
    strings and mid-string empty values) or with an empty "foo=". The
    corrupted value was then written to the Content-Type response header.
    
    toStringNoCharset() now applies the same null/empty value guard as
    toString(). Callers (coyote Response.setContentType and
    MediaTypeCache used by catalina connector Response) only use the
    no-charset serialisation when a charset is present, so this simply
    removes the corruption; programmatic access to the parameters via
    getParameterValue() is unchanged.
---
 java/org/apache/tomcat/util/http/parser/MediaType.java    |  6 +++++-
 .../org/apache/tomcat/util/http/parser/TestMediaType.java | 15 +++++++++++++++
 2 files changed, 20 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/tomcat/util/http/parser/MediaType.java b/java/org/apache/tomcat/util/http/parser/MediaType.java
index d0a54682af..473d0402d9 100644
--- a/java/org/apache/tomcat/util/http/parser/MediaType.java
+++ b/java/org/apache/tomcat/util/http/parser/MediaType.java
@@ -155,10 +155,14 @@ public class MediaType {
                         if (entry.getKey().equalsIgnoreCase("charset")) {
                             continue;
                         }
+                        String value = entry.getValue();
+                        if (value == null || value.isEmpty()) {
+                            continue;
+                        }
                         result.append(';');
                         result.append(entry.getKey());
                         result.append('=');
-                        result.append(entry.getValue());
+                        result.append(value);
                     }
 
                     noCharset = result.toString();
diff --git a/test/org/apache/tomcat/util/http/parser/TestMediaType.java b/test/org/apache/tomcat/util/http/parser/TestMediaType.java
index 85904cda58..10e896bd26 100644
--- a/test/org/apache/tomcat/util/http/parser/TestMediaType.java
+++ b/test/org/apache/tomcat/util/http/parser/TestMediaType.java
@@ -210,10 +210,25 @@ public class TestMediaType {
 
         // Note: Invalid input is filtered out
         Assert.assertEquals("text/html;charset=UTF-8", m.toString());
+        Assert.assertEquals("text/html", m.toStringNoCharset());
         Assert.assertEquals("UTF-8", m.getCharset());
     }
 
 
+    @Test
+    public void testToStringWithInvalidParameterValues() throws IOException {
+        // Parameters with an empty value or a null value (unterminated quoted
+        // string) must be omitted from the string representations.
+        String input = "type/sub-type;charset=UTF-8;a=;b=\"unterminated;c=";
+
+        StringReader sr = new StringReader(input);
+        MediaType m = MediaType.parseMediaType(sr);
+
+        Assert.assertEquals("type/sub-type;charset=UTF-8", m.toString());
+        Assert.assertEquals("type/sub-type", m.toStringNoCharset());
+    }
+
+
     @Test
     public void testBug55454() throws IOException {
         String input = "text/html;;charset=UTF-8";


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 06/11: Return a URI from ConfigurationSource.DEFAULT.getURI for invalid URI names

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 6ded02c632106daf971f2159c493dd38fffe545d
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 17:41:22 2026 +0200

    Return a URI from ConfigurationSource.DEFAULT.getURI for invalid URI names
    
    getURI() is documented to return URIs even for locations where no
    resource exists. For a relative name of a file that does not exist, the
    DEFAULT implementation ended by resolving the name against the user
    directory URI. URI.resolve() parses the reference strictly, so a name
    containing characters that are illegal in a URI but valid in a file
    name, such as a space, threw an unchecked IllegalArgumentException
    instead, contradicting the contract and inconsistent with getResource(),
    which wraps the same resolve() call and reports FileNotFoundException.
    
    Catch the IllegalArgumentException in the missing file case and fall
    back to the file based URI, which is valid whether or not the file
    exists and percent-encodes the name correctly. Resolvable names keep
    their existing resolve() result unchanged, including scheme based
    names such as classpath: and jar: that UriUtil does not classify as
    absolute URIs and which resolve() returns intact.
    
    Adds TestConfigurationSource covering the missing file with a space,
    the unchanged results for a missing URI-safe name, an existing file, a
    scheme based name and an absolute URI name.
---
 .../tomcat/util/file/ConfigurationSource.java      | 10 +++
 .../tomcat/util/file/TestConfigurationSource.java  | 90 ++++++++++++++++++++++
 2 files changed, 100 insertions(+)

diff --git a/java/org/apache/tomcat/util/file/ConfigurationSource.java b/java/org/apache/tomcat/util/file/ConfigurationSource.java
index c7ca969058..a713c983f6 100644
--- a/java/org/apache/tomcat/util/file/ConfigurationSource.java
+++ b/java/org/apache/tomcat/util/file/ConfigurationSource.java
@@ -79,6 +79,16 @@ public interface ConfigurationSource {
                 if (f.isFile()) {
                     return f.toURI();
                 }
+                try {
+                    return userDirUri.resolve(name);
+                } catch (IllegalArgumentException e) {
+                    // URI.resolve() rejects names that contain characters
+                    // which are not valid in a URI, such as a space. The
+                    // contract requires a URI to be returned even when no
+                    // resource exists, so fall back to the file based form
+                    // which encodes the name correctly.
+                    return f.toURI();
+                }
             }
             return userDirUri.resolve(name);
         }
diff --git a/test/org/apache/tomcat/util/file/TestConfigurationSource.java b/test/org/apache/tomcat/util/file/TestConfigurationSource.java
new file mode 100644
index 0000000000..9990977e93
--- /dev/null
+++ b/test/org/apache/tomcat/util/file/TestConfigurationSource.java
@@ -0,0 +1,90 @@
+/*
+ * 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.file;
+
+import java.io.File;
+import java.net.URI;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestConfigurationSource {
+
+    private static final ConfigurationSource DEFAULT = ConfigurationSource.DEFAULT;
+
+
+    /*
+     * getURI() is documented to return a URI even when no resource exists. A relative name that contains characters
+     * illegal in a URI (a space here) but valid in a file name must not trigger an IllegalArgumentException from
+     * URI.resolve().
+     */
+    @Test
+    public void testGetUriMissingFileWithIllegalUriCharacter() {
+        String name = "conf/does not exist.xml";
+
+        URI uri = DEFAULT.getURI(name);
+
+        Assert.assertNotNull(uri);
+        // The file name is recovered unchanged by decoding the path.
+        Assert.assertEquals(new File(System.getProperty("user.dir"), name), new File(uri));
+    }
+
+
+    @Test
+    public void testGetUriMissingFile() {
+        String name = "conf/does-not-exist.xml";
+
+        URI uri = DEFAULT.getURI(name);
+
+        Assert.assertEquals(new File(System.getProperty("user.dir"), name).toURI(), uri);
+    }
+
+
+    @Test
+    public void testGetUriExistingFile() {
+        File file = new File("test/webresources/dir1/f1.txt");
+        Assert.assertTrue(file.isFile());
+
+        URI uri = DEFAULT.getURI(file.getPath());
+
+        Assert.assertEquals(file.toURI(), uri);
+    }
+
+
+    /*
+     * Scheme based names that are not absolute URIs per UriUtil (classpath: and jar: have no "://") keep the
+     * URI.resolve() behaviour, which returns the name as a URI unchanged, as before the fix.
+     */
+    @Test
+    public void testGetUriSchemeNameIsPreserved() {
+        String name = "classpath:org/apache/catalina/mbeans-descriptors.xml";
+
+        URI uri = DEFAULT.getURI(name);
+
+        Assert.assertEquals(URI.create(name), uri);
+    }
+
+
+    @Test
+    public void testGetUriAbsoluteUriIsReturnedAsIs() {
+        String name = "file:/tmp/no-such-config.xml";
+
+        URI uri = DEFAULT.getURI(name);
+
+        Assert.assertEquals(URI.create(name), uri);
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 08/11: Replace obs-fold with a single SP when parsing headers

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit f301588994cf48bc1dcc32e137ca7ca21ca68ed4
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 19:15:09 2026 +0200

    Replace obs-fold with a single SP when parsing headers
    
    The header parser's line folding (obs-fold) handling peeked the first
    byte of the continuation line and copied that raw byte into the
    compacted header value. Since an obs-fold may start with HTAB, a TAB
    could end up in the value, e.g. a header sent as "X: abcd CRLF TAB
    efgh" was stored as "abcd\tefgh".
    
    RFC 9112 section 5.2 requires a server that receives an obs-fold to
    either reject the message or replace each obs-fold with one or more SP
    octets. The parser now always inserts a single space at the fold point
    instead of copying the peeked byte. Whitespace beyond the first byte of
    the continuation line was and is skipped by the value-start state, so
    the stored value for the HTAB case changes from a TAB to a space while
    the SP case is unchanged. This also removes a divergence between what
    Tomcat sees and what a spec-compliant intermediary would forward for
    the same message.
    
    The existing test for folded headers (bz 48839) asserted the legacy
    behaviour with a tab in the value and was updated to expect the single
    space mandated by the specification. A new test pins the SP
    continuation case.
---
 .../tomcat/util/http/parser/HttpHeaderParser.java  |  7 ++-
 .../coyote/http11/TestHttp11InputBuffer.java       | 69 +++++++++++++++++++++-
 2 files changed, 72 insertions(+), 4 deletions(-)

diff --git a/java/org/apache/tomcat/util/http/parser/HttpHeaderParser.java b/java/org/apache/tomcat/util/http/parser/HttpHeaderParser.java
index 2e7be33c3e..0eb653909b 100644
--- a/java/org/apache/tomcat/util/http/parser/HttpHeaderParser.java
+++ b/java/org/apache/tomcat/util/http/parser/HttpHeaderParser.java
@@ -255,9 +255,10 @@ public class HttpHeaderParser {
                     headerParsePos = HeaderParsePosition.HEADER_START;
                     break;
                 } else {
-                    // Copying one extra space in the buffer (since there must
-                    // be at least one space inserted between the lines)
-                    source.getHeaderByteBuffer().put(headerData.realPos, peek);
+                    // Replace the line folding with a single space as required
+                    // by RFC 9112 section 5.2. Any further whitespace at the
+                    // start of the continuation line is skipped below.
+                    source.getHeaderByteBuffer().put(headerData.realPos, SP);
                     headerData.realPos++;
                     headerParsePos = HeaderParsePosition.HEADER_VALUE_START;
                 }
diff --git a/test/org/apache/coyote/http11/TestHttp11InputBuffer.java b/test/org/apache/coyote/http11/TestHttp11InputBuffer.java
index 6533ba6c1c..0a376424cb 100644
--- a/test/org/apache/coyote/http11/TestHttp11InputBuffer.java
+++ b/test/org/apache/coyote/http11/TestHttp11InputBuffer.java
@@ -103,7 +103,9 @@ public class TestHttp11InputBuffer extends TomcatBaseTest {
             if (getResponseBody() == null) {
                 return false;
             }
-            if (!getResponseBody().contains("abcd\tefgh")) {
+            // The line folding (CRLF + HTAB) is replaced with a single SP as
+            // required by RFC 9110 section 5.2
+            if (!getResponseBody().contains("abcd efgh")) {
                 return false;
             }
             return true;
@@ -133,6 +135,71 @@ public class TestHttp11InputBuffer extends TomcatBaseTest {
     }
 
 
+    /**
+     * Test case for https://bz.apache.org/bugzilla/show_bug.cgi?id=48839 with a space rather than a tab starting the
+     * continuation line. Both fold forms must result in a single space in the parsed value (RFC 9112 section 5.2).
+     */
+    @Test
+    public void testObsFoldWithSpaceContinuation() {
+
+        ObsFoldClient client = new ObsFoldClient();
+
+        client.doRequest();
+        Assert.assertTrue(client.isResponse200());
+        Assert.assertTrue(client.getResponseBody(), client.getResponseBody().contains("abcd efgh"));
+    }
+
+
+    /**
+     * Obs-fold test client. Uses a SP continuation line.
+     */
+    private class ObsFoldClient extends SimpleHttpClient {
+
+        private Exception doRequest() {
+
+            Tomcat tomcat = getTomcatInstance();
+
+            Context root = tomcat.addContext("", TEMP_DIR);
+            Tomcat.addServlet(root, "ObsFold", new Bug48839Servlet());
+            root.addServletMapping("/test", "ObsFold");
+
+            try {
+                tomcat.start();
+                setPort(tomcat.getConnector().getLocalPort());
+
+                // Open connection
+                connect();
+
+                String[] request = new String[1];
+                // @formatter:off
+                request[0] =
+                        "GET http://localhost:8080/test HTTP/1.1" + CRLF +
+                        "Host: localhost:8080" + CRLF +
+                        "X-Bug48839: abcd" + CRLF +
+                        " efgh" + CRLF +
+                        "Connection: close" + CRLF +
+                        CRLF;
+                // @formatter:on
+
+                setRequest(request);
+                processRequest(); // blocks until response has been read
+
+                // Close the connection
+                disconnect();
+            } catch (Exception e) {
+                return e;
+            }
+            return null;
+        }
+
+        @Override
+        public boolean isResponseBodyOK() {
+            return getResponseBody() != null && getResponseBody().contains("abcd efgh");
+        }
+
+    }
+
+
     @Test
     public void testBug51557Valid() {
 


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 11/11: Treat an empty port when parsing a host value as an absent port

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit decfd3e92d34dcc497a43e2ae85e763d4268f8e8
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 21:28:28 2026 +0200

    Treat an empty port when parsing a host value as an absent port
    
    RFC 3986 (section 3.2.3) allows a ':' separator that is not followed
    by any digits. HttpParser.validatePort() already accepted such a value
    but AbstractProcessor parsed zero digits for the port, associating a
    port of zero with the request.
    
    AbstractProcessor now treats an empty port as equivalent to an absent
    port, keeping the port already associated with the request rather than
    overwriting it with zero.
    
    Document the empty port behaviour of validatePort() for its callers and
    add tests for host values with an empty port.
---
 java/org/apache/coyote/AbstractProcessor.java      | 36 +++++++++++++---------
 .../apache/tomcat/util/http/parser/HttpParser.java |  4 +++
 .../util/http/parser/TestHttpParserHost.java       |  9 ++++++
 3 files changed, 34 insertions(+), 15 deletions(-)

diff --git a/java/org/apache/coyote/AbstractProcessor.java b/java/org/apache/coyote/AbstractProcessor.java
index 2fdfee6b2e..4e49da8ea9 100644
--- a/java/org/apache/coyote/AbstractProcessor.java
+++ b/java/org/apache/coyote/AbstractProcessor.java
@@ -353,23 +353,29 @@ public abstract class AbstractProcessor extends AbstractProcessorLight implement
 
             // Extract the port information first, if any
             if (colonPos != -1) {
-                int port = 0;
-                for (int i = colonPos + 1; i < valueL; i++) {
-                    char c = (char) valueB[i + valueS];
-                    if (c < '0' || c > '9') {
-                        response.setStatus(400);
-                        setErrorState(ErrorState.CLOSE_CLEAN, null);
-                        return;
+                // A ':' not followed by digits is an empty port, which the
+                // grammar allows (RFC 3986, section 3.2.3). Treat it like an
+                // absent port: keep the port already associated with the
+                // request rather than storing zero for the absent digits.
+                if (colonPos + 1 < valueL) {
+                    int port = 0;
+                    for (int i = colonPos + 1; i < valueL; i++) {
+                        char c = (char) valueB[i + valueS];
+                        if (c < '0' || c > '9') {
+                            response.setStatus(400);
+                            setErrorState(ErrorState.CLOSE_CLEAN, null);
+                            return;
+                        }
+                        int digit = c - '0';
+                        if (port > (Integer.MAX_VALUE - digit) / 10) {
+                            response.setStatus(400);
+                            setErrorState(ErrorState.CLOSE_CLEAN, null);
+                            return;
+                        }
+                        port = port * 10 + digit;
                     }
-                    int digit = c - '0';
-                    if (port > (Integer.MAX_VALUE - digit) / 10) {
-                        response.setStatus(400);
-                        setErrorState(ErrorState.CLOSE_CLEAN, null);
-                        return;
-                    }
-                    port = port * 10 + digit;
+                    request.setServerPort(port);
                 }
-                request.setServerPort(port);
 
                 // Only need to copy the host name up to the :
                 valueL = colonPos;
diff --git a/java/org/apache/tomcat/util/http/parser/HttpParser.java b/java/org/apache/tomcat/util/http/parser/HttpParser.java
index 617da26043..45f63f7b0f 100644
--- a/java/org/apache/tomcat/util/http/parser/HttpParser.java
+++ b/java/org/apache/tomcat/util/http/parser/HttpParser.java
@@ -1107,6 +1107,10 @@ public class HttpParser {
 
     static int validatePort(Reader reader, int colonPosition) throws IOException {
         // Remaining characters should be numeric ...
+        // An empty port (a ':' not followed by any digits) is permitted by the
+        // grammar (RFC 3986, section 3.2.3). It is treated as equivalent to no
+        // port being present: the colon position is returned and the caller
+        // must not overwrite the port already associated with the request.
         readInteger(reader);
         // ... followed by EOS
         if (reader.read() == -1) {
diff --git a/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java b/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java
index 4c0452f846..b09cb7da1c 100644
--- a/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java
+++ b/test/org/apache/tomcat/util/http/parser/TestHttpParserHost.java
@@ -228,6 +228,15 @@ public class TestHttpParserHost {
         // IPv6 - invalid port
         result.add(new Object[] { TestType.IPv4, "[::1]:x", Integer.valueOf(-1), IAE} );
         result.add(new Object[] { TestType.IPv4, "[::1]:-1", Integer.valueOf(-1), IAE} );
+        // Domain name - empty port (permitted, treated as no port)
+        result.add(new Object[] { TestType.IPv4, "localhost:", Integer.valueOf(9), null} );
+        result.add(new Object[] { TestType.IPv4, "foo.bar.:", Integer.valueOf(8), null} );
+        // IPv4 - empty port (permitted, treated as no port)
+        result.add(new Object[] { TestType.IPv4, "127.0.0.1:", Integer.valueOf(9), null} );
+        // IPv6 - empty port (permitted, treated as no port). Only the IPv6
+        // test type is valid here: readHostIPv4() rejects the brackets before
+        // the empty port is reached.
+        result.add(new Object[] { TestType.IPv6, "[::1]:", Integer.valueOf(5), null} );
         return result;
     }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 05/11: Decode service binding entry files as UTF-8

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit c8924aa6bbbb25db4e14ca12b0b5eeda26c91f2f
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 17:31:19 2026 +0200

    Decode service binding entry files as UTF-8
    
    getProperty() decoded the binding file bytes with new String(byte[],int,
    int), which uses the JVM default character set. Service binding entries
    under SERVICE_BINDING_ROOT are text files that follow the UTF-8
    convention (Kubernetes secret projections encode stringData as UTF-8)
    and the resolved values flow into configuration such as SSLHostConfig
    certificate key passwords, so on a JVM whose default character set is
    not UTF-8 every non-ASCII character was replaced with a question mark,
    producing a silently wrong credential.
    
    Decode the bytes as UTF-8 explicitly. Since the service binding
    specification fixes no encoding, the class Javadoc now states that
    entry files are decoded as UTF-8 and that invalid bytes become U+FFFD.
    
    Adds TestServiceBindingPropertySource covering the UTF-8 decoding of
    multi-byte characters (with and without the chomp: prefix), the full
    chomp matrix and the null results for unknown bindings and malformed
    keys. The environment variable cannot be set from the tests, so a
    package-private constructor pins the binding root for the test class;
    the public no-argument path is unchanged and still reads the
    environment on every resolution. Note that the tests pass on the
    default-UTF-8 test JVM regardless of the fix; they fail against the
    previous code when the default character set is not UTF-8, and they
    pin the contract either way.
---
 .../digester/ServiceBindingPropertySource.java     | 27 +++++--
 .../digester/TestServiceBindingPropertySource.java | 91 ++++++++++++++++++++++
 2 files changed, 113 insertions(+), 5 deletions(-)

diff --git a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
index be53e1a100..a3ba4ae9ed 100644
--- a/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
+++ b/java/org/apache/tomcat/util/digester/ServiceBindingPropertySource.java
@@ -17,6 +17,7 @@
 package org.apache.tomcat.util.digester;
 
 import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -62,6 +63,10 @@ import org.apache.tomcat.util.IntrospectionUtils;
  * convenience for hand-edited files/values where removing a trailing newline is difficult, and trailing whitespace
  * changes the meaning of the value.
  * </p>
+ * <p>
+ * Binding entry files are decoded as UTF-8. Bytes that are not valid UTF-8 are replaced with the Unicode replacement
+ * character (U+FFFD).
+ * </p>
  * How to configure:
  *
  * <pre>
@@ -85,11 +90,23 @@ import org.apache.tomcat.util.IntrospectionUtils;
 public class ServiceBindingPropertySource implements IntrospectionUtils.PropertySource {
 
     /**
-     * Default constructor.
+     * Default constructor. The binding root is taken from the {@code SERVICE_BINDING_ROOT} environment variable when
+     * an expression is resolved.
      */
     public ServiceBindingPropertySource() {
+        this(null);
+    }
+
+    /*
+     * Test seam: pins the binding root so the tests do not need to set the environment variable. A null value selects
+     * the environment variable at resolution time.
+     */
+    ServiceBindingPropertySource(String serviceBindingRoot) {
+        this.serviceBindingRoot = serviceBindingRoot;
     }
 
+    private final String serviceBindingRoot;
+
     private static final String SERVICE_BINDING_ROOT_ENV_VAR = "SERVICE_BINDING_ROOT";
 
     /**
@@ -106,8 +123,8 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property
     @Override
     public String getProperty(String key) {
         // get the root to search from
-        String serviceBindingRoot = System.getenv(SERVICE_BINDING_ROOT_ENV_VAR);
-        if (serviceBindingRoot == null) {
+        String root = serviceBindingRoot != null ? serviceBindingRoot : System.getenv(SERVICE_BINDING_ROOT_ENV_VAR);
+        if (root == null) {
             return null;
         }
 
@@ -123,7 +140,7 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property
             return null;
         }
 
-        Path path = Paths.get(serviceBindingRoot, parts[0], parts[1]);
+        Path path = Paths.get(root, parts[0], parts[1]);
 
         if (!path.toFile().exists()) {
             return null;
@@ -145,7 +162,7 @@ public class ServiceBindingPropertySource implements IntrospectionUtils.Property
                 }
             }
 
-            return new String(bytes, 0, length);
+            return new String(bytes, 0, length, StandardCharsets.UTF_8);
         } catch (IOException ioe) {
             // Treat as not found
             return null;
diff --git a/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java
new file mode 100644
index 0000000000..3a7e8d43fa
--- /dev/null
+++ b/test/org/apache/tomcat/util/digester/TestServiceBindingPropertySource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class TestServiceBindingPropertySource {
+
+    @Rule
+    public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+
+    private File writeBinding(String name, String key, String content) throws Exception {
+        File bindingFolder = new File(temporaryFolder.getRoot(), name);
+        Assert.assertTrue(bindingFolder.mkdirs() || bindingFolder.isDirectory());
+        File file = new File(bindingFolder, key);
+        Files.write(file.toPath(), content.getBytes(StandardCharsets.UTF_8));
+        return file;
+    }
+
+
+    @Test
+    public void testPropertyValueIsDecodedAsUtf8() throws Exception {
+        String value = "p\u00e4ssw\u00f6rd-\u00fcnicode-\u2603";
+        writeBinding("my-binding", "keyPassword", value);
+        writeBinding("my-binding", "withNewline", value + "\n");
+
+        ServiceBindingPropertySource source =
+                new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertEquals(value, source.getProperty("my-binding.keyPassword"));
+        Assert.assertEquals(value, source.getProperty("chomp:my-binding.withNewline"));
+    }
+
+
+    @Test
+    public void testChompTrimsSingleTrailingLineBreak() throws Exception {
+        writeBinding("my-binding", "lf", "value\n");
+        writeBinding("my-binding", "crlf", "value\r\n");
+        writeBinding("my-binding", "cr", "value\r");
+        writeBinding("my-binding", "none", "value");
+        writeBinding("my-binding", "blankline", "value\n\n");
+        writeBinding("my-binding", "trailingspace", "value \n");
+
+        ServiceBindingPropertySource source =
+                new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertEquals("value", source.getProperty("chomp:my-binding.lf"));
+        Assert.assertEquals("value", source.getProperty("chomp:my-binding.crlf"));
+        Assert.assertEquals("value", source.getProperty("chomp:my-binding.cr"));
+        Assert.assertEquals("value", source.getProperty("chomp:my-binding.none"));
+        Assert.assertEquals("value\n", source.getProperty("chomp:my-binding.blankline"));
+        Assert.assertEquals("value ", source.getProperty("chomp:my-binding.trailingspace"));
+    }
+
+
+    @Test
+    public void testUnknownBindingsResolveToNull() throws Exception {
+        writeBinding("my-binding", "known", "value");
+
+        ServiceBindingPropertySource source =
+                new ServiceBindingPropertySource(temporaryFolder.getRoot().getAbsolutePath());
+
+        Assert.assertNull(source.getProperty("my-binding.unknown"));
+        Assert.assertNull(source.getProperty("unknown-binding.known"));
+        // A key without exactly one dot is outside the <binding-name>.<key> contract
+        Assert.assertNull(source.getProperty("no-dot"));
+        Assert.assertNull(source.getProperty("too.many.dots"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 07/11: Reject If headers containing an empty list in the WebDAV servlet

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit d5f1d40f02bb9a310849116eca4cccc2ee51cf00
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 18:59:19 2026 +0200

    Reject If headers containing an empty list in the WebDAV servlet
    
    RFC 4918 section 10.4.2 requires a WebDAV If header list to contain at
    least one condition (List = "(" 1*Condition ")"). The parser in
    WebdavIfHeader previously accepted an empty list, such as "If: ()",
    "( )" or a list without any valid entry, and returned a non-null
    structure for it. An empty IfList.match() vacuously returned true, so
    the header passed the If precondition checks for any token and ETag,
    and hasValue() returned true so the strict mode rejection of invalid
    If syntax in WebdavServlet.checkIfHeaders() was bypassed.
    
    parseIfList() now returns null when it did not read at least one valid
    entry, and parseUntagged() and parseTagged() propagate this so the
    whole header value is treated as invalid. This routes processing into
    the existing invalid syntax handling: with strictIfProcessing enabled,
    the default, requests using such headers now fail the precondition
    check; with strictIfProcessing disabled the header is ignored, as for
    any other invalid If header syntax, and lock tokens in the raw header
    are still evaluated separately by isLocked(). Lenient error recovery
    for entries that fail to parse is retained where other valid entries
    were read.
    
    Add TestWebdavIfHeader covering the parser behavior and new
    TestWebdavServlet cases covering the strict and non-strict servlet
    behavior for "If: ()".
---
 .../apache/tomcat/util/http/WebdavIfHeader.java    |  43 +++++--
 .../catalina/servlets/TestWebdavServlet.java       |  62 ++++++++++
 .../tomcat/util/http/TestWebdavIfHeader.java       | 134 +++++++++++++++++++++
 3 files changed, 231 insertions(+), 8 deletions(-)

diff --git a/java/org/apache/tomcat/util/http/WebdavIfHeader.java b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
index 2c918511e7..21de88dfed 100644
--- a/java/org/apache/tomcat/util/http/WebdavIfHeader.java
+++ b/java/org/apache/tomcat/util/http/WebdavIfHeader.java
@@ -155,9 +155,10 @@ public class WebdavIfHeader {
     }
 
     /**
-     * Returns true if an If header is present. False otherwise.
+     * Returns true if an If header is present and was parsed successfully. False if no If header is present or the
+     * header value contains invalid syntax.
      *
-     * @return true if an If header is present.
+     * @return true if an If header is present and was parsed successfully. False otherwise.
      */
     public boolean hasValue() {
         return ifHeader != null;
@@ -269,7 +270,7 @@ public class WebdavIfHeader {
      *
      * @param reader the reader
      *
-     * @return the parsed map
+     * @return the parsed map, or <code>null</code> if an invalid (empty) <em>IfList</em> was encountered
      */
     private IfHeaderMap parseTagged(StringReader reader) throws IOException {
         IfHeaderMap map = new IfHeaderMap();
@@ -284,7 +285,13 @@ public class WebdavIfHeader {
                 String resource = readWord(reader, '>');
                 if (resource != null) {
                     // go to untagged after reading the resource
-                    map.put(resource, parseUntagged(reader));
+                    IfHeaderList untagged = parseUntagged(reader);
+                    if (untagged == null) {
+                        // An invalid (empty) IfList makes the whole header
+                        // invalid, stop parsing
+                        return null;
+                    }
+                    map.put(resource, untagged);
                     resources.add(resource);
                 } else {
                     break;
@@ -309,7 +316,8 @@ public class WebdavIfHeader {
      *
      * @param reader The <code>StringReader</code> to read from for parsing
      *
-     * @return An <code>ArrayList</code> of {@link IfList} entries.
+     * @return An <code>ArrayList</code> of {@link IfList} entries, or <code>null</code> if an invalid (empty)
+     *             <em>IfList</em> was encountered.
      */
     private IfHeaderList parseUntagged(StringReader reader) throws IOException {
         IfHeaderList list = new IfHeaderList();
@@ -323,7 +331,13 @@ public class WebdavIfHeader {
 
             } else if (c == '(') {
                 // start of an IfList, parse
-                list.add(parseIfList(reader));
+                IfList ifList = parseIfList(reader);
+                if (ifList == null) {
+                    // An invalid (empty) IfList makes the whole header
+                    // invalid, stop parsing
+                    return null;
+                }
+                list.add(ifList);
 
             } else if (c == '<') {
                 // start of a tag, return current list
@@ -349,7 +363,8 @@ public class WebdavIfHeader {
      *
      * @param reader The <code>StringReader</code> to read from for parsing
      *
-     * @return The {@link IfList} for the input <em>IfList</em>.
+     * @return The {@link IfList} for the input <em>IfList</em>, or <code>null</code> if the list did not contain at
+     *             least one valid entry.
      *
      * @throws IOException if a problem occurs during reading.
      */
@@ -427,7 +442,19 @@ public class WebdavIfHeader {
             }
         }
 
-        // return the current list anyway
+        // RFC 4918 section 10.4.2 requires an IfList to contain at least one
+        // condition:
+        //    List = "(" 1*Condition ")"
+        // An empty list (e.g. "()") is invalid syntax. Return null so that
+        // the caller treats the entire header value as invalid which allows
+        // strict mode to reject it.
+        if (res.isEmpty()) {
+            if (log.isTraceEnabled()) {
+                log.trace("parseIfList: Empty If list, treating header as invalid");
+            }
+            return null;
+        }
+
         return res;
     }
 
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index 0f5500af2c..e54e655c77 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -1538,6 +1538,68 @@ public class TestWebdavServlet extends TomcatBaseTest {
         Assert.assertEquals(WebdavStatus.SC_CREATED, client.getStatusCode());
     }
 
+    /*
+     * An If header value that contains an empty list is invalid syntax (RFC 4918 requires at least one entry in a
+     * list). With strict If processing (the default) such a header must fail the precondition check. Without strict
+     * If processing the header is ignored, as for any other invalid If header syntax.
+     */
+    @Test
+    public void testIfHeaderEmptyListStrict() throws Exception {
+        Client client = doTestIfHeaderEmptyList("webdav-emptyif-strict", true);
+        client.setRequest(new String[] {
+                "PUT /file1.txt HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "If: ()" + CRLF +
+                    "Content-Length: 6" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF +
+                    CONTENT
+                });
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(WebdavStatus.SC_PRECONDITION_FAILED, client.getStatusCode());
+    }
+
+
+    @Test
+    public void testIfHeaderEmptyListNonStrict() throws Exception {
+        Client client = doTestIfHeaderEmptyList("webdav-emptyif-nonstrict", false);
+        client.setRequest(new String[] {
+                "PUT /file1.txt HTTP/1.1" + CRLF +
+                    "Host: localhost:" + getPort() + CRLF +
+                    "If: ()" + CRLF +
+                    "Content-Length: 6" + CRLF +
+                    "Connection: Close" + CRLF +
+                    CRLF +
+                    CONTENT
+                });
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(HttpServletResponse.SC_CREATED, client.getStatusCode());
+    }
+
+
+    private Client doTestIfHeaderEmptyList(String webappName, boolean strictIfProcessing) throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // Create a temp webapp that can be safely written to
+        File tempWebapp = new File(getTemporaryDirectory(), webappName);
+        tempWebapp.deleteOnExit();
+        Assert.assertTrue(tempWebapp.mkdirs());
+        Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
+        Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new WebdavServlet());
+        webdavServlet.addInitParameter("readonly", "false");
+        webdavServlet.addInitParameter("strictIfProcessing", Boolean.toString(strictIfProcessing));
+        ctxt.addServletMapping("/*", "webdav");
+        ctxt.addMimeMapping("txt", "text/plain");
+        tomcat.start();
+
+        Client client = new Client();
+        client.setPort(getPort());
+        return client;
+    }
+
+
     @Test
     public void testPropertyStore() throws Exception {
         Tomcat tomcat = getTomcatInstance();
diff --git a/test/org/apache/tomcat/util/http/TestWebdavIfHeader.java b/test/org/apache/tomcat/util/http/TestWebdavIfHeader.java
new file mode 100644
index 0000000000..d9b556919b
--- /dev/null
+++ b/test/org/apache/tomcat/util/http/TestWebdavIfHeader.java
@@ -0,0 +1,134 @@
+/*
+ * 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.http;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestWebdavIfHeader {
+
+    private static final String TOKEN = "urn:uuid:0123456789abcdef";
+    private static final List<String> TOKENS = Collections.singletonList(TOKEN);
+
+    private static final String URI_PREFIX = "http://localhost/webdav/";
+
+    /*
+     * RFC 4918 section 10.4.2 requires an IfList to contain at least one condition:
+     *
+     * List = "(" 1*Condition ")"
+     *
+     * An empty list must make the header value invalid (hasValue() returns false) so that strict mode processing
+     * rejects it rather than matching every token and etag vacuously.
+     */
+    @Test
+    public void testEmptyIfListIsInvalid() throws Exception {
+        assertInvalid("()");
+        assertInvalid("( )");
+        assertInvalid("(Not)");
+        assertInvalid("(Not )");
+        assertInvalid("(junk)");
+        assertInvalid("(<urn");
+        // A single empty list invalidates the entire header value
+        assertInvalid("(<" + TOKEN + ">) ()");
+        assertInvalid("() (<" + TOKEN + ">)");
+        assertInvalid("</myfolder/> ()");
+    }
+
+    @Test
+    public void testAbsentHeaderHasNoValue() throws Exception {
+        assertInvalid(null);
+        assertInvalid("");
+    }
+
+    @Test
+    public void testUntaggedMatches() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + ">)");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+        Assert.assertFalse(header.matches("/myfolder/", Collections.singletonList("other"), "etag"));
+        Assert.assertTrue(header.matches("/other/", TOKENS, "etag"));
+    }
+
+    @Test
+    public void testEtagMatches() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "([etag])");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+        Assert.assertFalse(header.matches("/myfolder/", TOKENS, "other"));
+    }
+
+    @Test
+    public void testMultipleLists() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + ">) ([etag])");
+        Assert.assertTrue(header.hasValue());
+        // Untagged lists are alternatives: the first list matches the token
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "other"));
+        // The second list matches the etag
+        Assert.assertTrue(header.matches("/myfolder/", Collections.singletonList("other"), "etag"));
+    }
+
+    @Test
+    public void testTaggedMatches() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "</myfolder/> (<" + TOKEN + ">)");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+        Assert.assertFalse(header.matches("/myfolder/", Collections.singletonList("other"), "etag"));
+        // A resource not listed in a tagged header does not match
+        Assert.assertFalse(header.matches("/other/", TOKENS, "etag"));
+    }
+
+    @Test
+    public void testNotMatches() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "(Not <" + TOKEN + ">)");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertFalse(header.matches("/myfolder/", TOKENS, "etag"));
+        Assert.assertTrue(header.matches("/myfolder/", Collections.singletonList("other"), "etag"));
+    }
+
+    /*
+     * The parser is deliberately lenient with malformed entries that do not prevent the rest of the header from
+     * parsing (error recovery inherited from the Jackrabbit code). Only a list without any valid entry invalidates
+     * the header value.
+     */
+    @Test
+    public void testErrorRecoveryKeepsValidEntries() throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + "> junk)");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+
+        header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + ">) junk ([etag])");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "other"));
+        Assert.assertTrue(header.matches("/myfolder/", Collections.singletonList("other"), "etag"));
+
+        header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + "> <incomplete");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+
+        header = new WebdavIfHeader(URI_PREFIX, "(<" + TOKEN + ">)]");
+        Assert.assertTrue(header.hasValue());
+        Assert.assertTrue(header.matches("/myfolder/", TOKENS, "etag"));
+    }
+
+    private void assertInvalid(String headerValue) throws Exception {
+        WebdavIfHeader header = new WebdavIfHeader(URI_PREFIX, headerValue);
+        Assert.assertFalse("Expected invalid header for value: " + headerValue, header.hasValue());
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 03/11: Signal EmptyStackException for a negative ArrayStack peek index

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 6eb71c44c0bc24d33c135d67233a181b6822b186
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 16:58:30 2026 +0200

    Signal EmptyStackException for a negative ArrayStack peek index
    
    ArrayStack.peek(int) computed [size() - n - 1] and threw
    EmptyStackException only when that index fell below zero, which covers
    indexes beyond the bottom of the stack. A negative index moved the
    computed position beyond the end of the backing list instead, so get()
    threw an IndexOutOfBoundsException.
    
    That exception escaped Digester.peek(int), which only catches
    EmptyStackException, even though the method is documented to return
    null for any out of range index. One path where a negative index can be
    passed in is CallMethodRule with a negative targetOffset deeper than
    the current stack depth, where digester.peek(getCount() + targetOffset)
    then reported a wrapped IndexOutOfBoundsException rather than the
    intended 'Call target is null' parse error that names the offset and
    the stack depth.
    
    Guard both ends of the range instead. For non-negative indexes the
    condition is equivalent to the previous one, so no legitimate use
    changes, and the outcome for extreme values no longer depends on
    integer wrap-around.
    
    Adds TestArrayStack covering the top, the bottom, the empty stack and
    both out of range directions including Integer.MIN_VALUE and
    Integer.MAX_VALUE, plus a TestDigester test for the peek(int) null
    contract for negative indexes.
---
 .../apache/tomcat/util/digester/ArrayStack.java    | 15 ++--
 .../tomcat/util/digester/TestArrayStack.java       | 88 ++++++++++++++++++++++
 .../apache/tomcat/util/digester/TestDigester.java  | 16 ++++
 3 files changed, 114 insertions(+), 5 deletions(-)

diff --git a/java/org/apache/tomcat/util/digester/ArrayStack.java b/java/org/apache/tomcat/util/digester/ArrayStack.java
index fb6a533814..5ed71ba933 100644
--- a/java/org/apache/tomcat/util/digester/ArrayStack.java
+++ b/java/org/apache/tomcat/util/digester/ArrayStack.java
@@ -100,15 +100,20 @@ public class ArrayStack<E> extends ArrayList<E> {
      *
      * @return the n'th item on the stack, zero relative
      *
-     * @throws EmptyStackException if there are not enough items on the stack to satisfy this request
+     * @throws EmptyStackException if there are not enough items on the stack to satisfy this request, including when
+     *                             {@code n} is negative. Note that unlike the original Commons Collections code, a
+     *                             negative index signals EmptyStackException rather than surfacing as an
+     *                             IndexOutOfBoundsException.
      */
     public E peek(int n) throws EmptyStackException {
-        int m = (size() - n) - 1;
-        if (m < 0) {
+        // A negative index is out of range just like an index beyond the bottom
+        // of the stack. Guarding only the upper bound would let get() throw an
+        // IndexOutOfBoundsException, which callers such as Digester.peek(int)
+        // do not expect.
+        if (n < 0 || n >= size()) {
             throw new EmptyStackException();
-        } else {
-            return get(m);
         }
+        return get(size() - 1 - n);
     }
 
     /**
diff --git a/test/org/apache/tomcat/util/digester/TestArrayStack.java b/test/org/apache/tomcat/util/digester/TestArrayStack.java
new file mode 100644
index 0000000000..9146b40588
--- /dev/null
+++ b/test/org/apache/tomcat/util/digester/TestArrayStack.java
@@ -0,0 +1,88 @@
+/*
+ * 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.util.EmptyStackException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestArrayStack {
+
+    @Test
+    public void testPeekTopAndBottom() {
+        ArrayStack<String> stack = new ArrayStack<>();
+        stack.push("bottom");
+        stack.push("top");
+
+        Assert.assertEquals("top", stack.peek(0));
+        Assert.assertEquals("bottom", stack.peek(1));
+    }
+
+
+    @Test(expected = EmptyStackException.class)
+    public void testPeekEmptyStack() {
+        new ArrayStack<String>().peek(0);
+    }
+
+
+    @Test(expected = EmptyStackException.class)
+    public void testPeekBeyondBottom() {
+        ArrayStack<String> stack = new ArrayStack<>();
+        stack.push("only");
+
+        stack.peek(1);
+    }
+
+
+    /*
+     * A negative index is out of range rather than beyond the end of the
+     * backing list, so it must signal EmptyStackException like the other
+     * out of range indexes.
+     */
+    @Test(expected = EmptyStackException.class)
+    public void testPeekNegativeIndex() {
+        ArrayStack<String> stack = new ArrayStack<>();
+        stack.push("only");
+
+        stack.peek(-1);
+    }
+
+
+    @Test(expected = EmptyStackException.class)
+    public void testPeekNegativeIndexEmptyStack() {
+        new ArrayStack<String>().peek(-1);
+    }
+
+
+    @Test(expected = EmptyStackException.class)
+    public void testPeekIntegerMinValue() {
+        ArrayStack<String> stack = new ArrayStack<>();
+        stack.push("only");
+
+        stack.peek(Integer.MIN_VALUE);
+    }
+
+
+    @Test(expected = EmptyStackException.class)
+    public void testPeekIntegerMaxValue() {
+        ArrayStack<String> stack = new ArrayStack<>();
+        stack.push("only");
+
+        stack.peek(Integer.MAX_VALUE);
+    }
+}
diff --git a/test/org/apache/tomcat/util/digester/TestDigester.java b/test/org/apache/tomcat/util/digester/TestDigester.java
index 25eaf04f66..3f98771dd5 100644
--- a/test/org/apache/tomcat/util/digester/TestDigester.java
+++ b/test/org/apache/tomcat/util/digester/TestDigester.java
@@ -185,4 +185,20 @@ public class TestDigester {
         Assert.assertNotNull(digester.createSAXException("message", new RuntimeException("cause")));
     }
 
+
+    /*
+     * peek(int) is documented to return null for any out of range index,
+     * including negative ones such as the values a negative CallMethodRule
+     * target offset can produce.
+     */
+    @Test
+    public void testPeekOutOfRangeIndexReturnsNull() {
+        Digester digester = new Digester();
+        digester.push(new Object());
+
+        Assert.assertNotNull(digester.peek(0));
+        Assert.assertNull(digester.peek(1));
+        Assert.assertNull(digester.peek(-1));
+    }
+
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 04/11: Emit a null literal for missing CallMethodRule code-gen parameters

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 581ea1b182ed0207e7283502712dd6db62dc491c
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 17:14:02 2026 +0200

    Emit a null literal for missing CallMethodRule code-gen parameters
    
    A CallMethodRule collecting two or more parameters passes a parameter
    whose nested element is absent to the target method as a null value.
    That is documented behaviour, and the method call itself handles it.
    
    The code generation block at the end of end() did not. A null parameter
    is not a String, so it was handed to Digester.toVariableName() to name a
    variable for it. That method registers the object and then dereferences
    it, so the generated code path aborted the parse with a
    NullPointerException, wrapped in a SAXException, as soon as a
    multi-parameter element omitted one of its parameter children, for
    example an init-param without an init-value. This affects the opt-in
    -generateCode path used to pre-compile descriptor processing.
    
    Emit the null literal for a null parameter instead. The generated
    statement then reflects the call that is actually made. All first-party
    targets of these rules declare a single method of that name, so the
    generated source resolves to the same method the reflective invocation
    selected; a cast is not needed and would require canonicalising type
    names for no first-party benefit.
---
 .../tomcat/util/digester/CallMethodRule.java       |  7 ++
 .../tomcat/util/digester/TestCallMethodRule.java   | 98 ++++++++++++++++++++++
 2 files changed, 105 insertions(+)

diff --git a/java/org/apache/tomcat/util/digester/CallMethodRule.java b/java/org/apache/tomcat/util/digester/CallMethodRule.java
index c475df0ebe..1f2cb4f37f 100644
--- a/java/org/apache/tomcat/util/digester/CallMethodRule.java
+++ b/java/org/apache/tomcat/util/digester/CallMethodRule.java
@@ -352,6 +352,13 @@ public class CallMethodRule extends Rule {
                 }
                 if (bodyText != null) {
                     code.append("\"").append(IntrospectionUtils.escape(bodyText)).append("\"");
+                } else if (paramValues[i] == null) {
+                    // Missing parameters are passed to the method as null
+                    // values. Emit the literal rather than trying to name a
+                    // variable for an object that does not exist. The
+                    // generated call assumes the target method resolves
+                    // unambiguously for a null argument.
+                    code.append("null");
                 } else if (paramValues[i] instanceof String) {
                     code.append("\"").append(IntrospectionUtils.escape(paramValues[i].toString())).append("\"");
                 } else {
diff --git a/test/org/apache/tomcat/util/digester/TestCallMethodRule.java b/test/org/apache/tomcat/util/digester/TestCallMethodRule.java
new file mode 100644
index 0000000000..43510aff83
--- /dev/null
+++ b/test/org/apache/tomcat/util/digester/TestCallMethodRule.java
@@ -0,0 +1,98 @@
+/*
+ * 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.InputSource;
+
+public class TestCallMethodRule {
+
+    public static class TwoArgBean {
+
+        private String first;
+        private String second;
+        private int calls;
+
+        public void setTwo(String first, String second) {
+            this.first = first;
+            this.second = second;
+            calls++;
+        }
+
+        public String getFirst() {
+            return first;
+        }
+
+        public String getSecond() {
+            return second;
+        }
+
+        public int getCalls() {
+            return calls;
+        }
+    }
+
+
+    private static Digester createDigester() {
+        Digester digester = new Digester();
+        digester.addObjectCreate("root", TwoArgBean.class.getName());
+        digester.addCallMethod("root/call", "setTwo", 2);
+        // Only the first parameter has a nested element. The second one is
+        // legitimately missing and is passed to the method as null.
+        digester.addCallParam("root/call/first", 0);
+        return digester;
+    }
+
+
+    @Test
+    public void testMissingParameterIsPassedAsNull() throws Exception {
+        Digester digester = createDigester();
+
+        TwoArgBean bean = (TwoArgBean) digester
+                .parse(new InputSource(new StringReader("<root><call><first>x</first></call></root>")));
+
+        Assert.assertEquals(1, bean.getCalls());
+        Assert.assertEquals("x", bean.getFirst());
+        Assert.assertNull(bean.getSecond());
+    }
+
+
+    /*
+     * Code generation must render a missing (null) parameter as the null
+     * literal rather than generating a variable name for it, which previously
+     * failed with a NullPointerException.
+     */
+    @Test
+    public void testGenerateCodeWithMissingParameter() throws Exception {
+        Digester digester = createDigester();
+
+        digester.startGeneratingCode();
+        TwoArgBean bean = (TwoArgBean) digester
+                .parse(new InputSource(new StringReader("<root><call><first>x</first></call></root>")));
+        String code = digester.getGeneratedCode().toString();
+        digester.endGeneratingCode();
+
+        Assert.assertEquals(1, bean.getCalls());
+        Assert.assertNull(bean.getSecond());
+        Assert.assertTrue("Generated code was: " + System.lineSeparator() + code,
+                code.contains(".setTwo(\"x\", null);"));
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org


(tomcat) 10/11: Ignore parameters with no name when parsing a MediaType

Posted by re...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 8a80cb4d5203f6ab9a72626f116453d662438e6a
Author: opencode <re...@apache.org>
AuthorDate: Wed Oct 14 20:34:50 2026 +0200

    Ignore parameters with no name when parsing a MediaType
    
    A dangling ';' at the end of a media type is invalid but tolerated by
    the parser. Previously it resulted in a phantom parameter with an
    empty name and an empty value being added to the parsed MediaType,
    inflating getParameterCount() (e.g. "text/html;" reported one
    parameter). Trailing ";;" had the same effect, inconsistent with the
    handling of a ";;" sequence in the middle of the value where the null
    attribute is skipped and no parameter is stored.
    
    readToken() only returns the empty string at end of input, so an empty
    attribute can never carry a real name; attributes with an empty name
    are now ignored in the same way as null attributes.
---
 java/org/apache/tomcat/util/http/parser/MediaType.java   |  2 +-
 .../apache/tomcat/util/http/parser/TestMediaType.java    | 16 ++++++++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/java/org/apache/tomcat/util/http/parser/MediaType.java b/java/org/apache/tomcat/util/http/parser/MediaType.java
index 473d0402d9..2f2ccdcd5b 100644
--- a/java/org/apache/tomcat/util/http/parser/MediaType.java
+++ b/java/org/apache/tomcat/util/http/parser/MediaType.java
@@ -217,7 +217,7 @@ public class MediaType {
                 value = "";
             }
 
-            if (attribute != null) {
+            if (attribute != null && !attribute.isEmpty()) {
                 parameters.put(attribute.toLowerCase(Locale.ENGLISH), value);
             }
 
diff --git a/test/org/apache/tomcat/util/http/parser/TestMediaType.java b/test/org/apache/tomcat/util/http/parser/TestMediaType.java
index 10e896bd26..13b8dffadd 100644
--- a/test/org/apache/tomcat/util/http/parser/TestMediaType.java
+++ b/test/org/apache/tomcat/util/http/parser/TestMediaType.java
@@ -334,7 +334,23 @@ public class TestMediaType {
         StringReader sr = new StringReader("type/sub-type;;a=1;;b=2;;");
         MediaType m = MediaType.parseMediaType(sr);
 
+        Assert.assertEquals(2, m.getParameterCount());
         Assert.assertEquals("1", m.getParameterValue("a"));
         Assert.assertEquals("2", m.getParameterValue("b"));
     }
+
+    @Test
+    public void testTrailingSemicolon() throws Exception {
+        // A dangling ';' is invalid but tolerated. It must not result in a
+        // phantom parameter with an empty name.
+        StringReader sr = new StringReader("type/sub-type;charset=UTF-8;");
+        MediaType m = MediaType.parseMediaType(sr);
+
+        Assert.assertNotNull(m);
+        Assert.assertEquals(1, m.getParameterCount());
+        Assert.assertNull(m.getParameterValue(""));
+        Assert.assertEquals("UTF-8", m.getParameterValue("charset"));
+        Assert.assertEquals("type/sub-type;charset=UTF-8", m.toString());
+        Assert.assertEquals("type/sub-type", m.toStringNoCharset());
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@tomcat.apache.org
For additional commands, e-mail: dev-help@tomcat.apache.org