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