This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 6dc68cc4b2058b422327ab2c0e744da56211edbe Author: opencode <[email protected]> AuthorDate: Sun Oct 11 22:49:27 2026 +0200 Escape literal single quotes in Strftime SimpleDateFormat patterns convertDateFormat() tracked whether it was inside a generated SimpleDateFormat quoted literal, but a single quote in the source strftime format was appended as an ordinary character. Within an open quoted literal that character closed the literal in the parser's eyes while the inside flag stayed true, so the literal text that followed was emitted unquoted. Constructing the SimpleDateFormat then threw, as in the common SSI config timefmt pattern "%d %b %Y %H:%M:%S %Z 'GMT'" which failed with "Illegal pattern character 'T'", or the following characters were silently interpreted as pattern letters. The trailing-quote heuristic skipped the closing quote when the buffer happened to end in one, which corrupted patterns such as "a'" by dropping the quote. Emit a source quote as two quotes, the SimpleDateFormat escape for a literal quote, which is valid both inside and outside a quoted literal and leaves the quote state unchanged in either case. The inside flag then tracks the parser state exactly, so the closing quote of a literal open at the end of the conversion is appended unconditionally, replacing the heuristic. Emitted patterns for quote free formats are unchanged. Add TestStrftime covering embedded, lone and trailing quotes, including the corrupted trailing case, and note in the constructor javadoc that a quote in the format is an ordinary literal character. --- java/org/apache/catalina/util/Strftime.java | 32 ++++++----- test/org/apache/catalina/util/TestStrftime.java | 72 +++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 13 deletions(-) diff --git a/java/org/apache/catalina/util/Strftime.java b/java/org/apache/catalina/util/Strftime.java index a6620a576e..afc5a36b51 100644 --- a/java/org/apache/catalina/util/Strftime.java +++ b/java/org/apache/catalina/util/Strftime.java @@ -110,7 +110,8 @@ public class Strftime { /** * Create an instance of this date formatting class * - * @param origFormat the strftime-style formatting string + * @param origFormat the strftime-style formatting string. A single quote character within it is an ordinary + * literal character. * @param locale the locale to use for locale-specific conversions */ public Strftime(String origFormat, Locale locale) { @@ -185,23 +186,28 @@ public class Strftime { } } } else { - if (!inside && c != ' ') { - // We start a literal, which we need to quote - buf.append('\''); - inside = true; - } + if (c == '\'') { + // A single quote in the source format is an ordinary literal character. In a + // SimpleDateFormat pattern it is written as two quotes, both inside and outside a quoted + // literal, and in either case the quote state is unchanged. + buf.append("''"); + } else { + if (!inside && c != ' ') { + // We start a literal, which we need to quote + buf.append('\''); + inside = true; + } - buf.append(c); + buf.append(c); + } } } } - if (buf.length() > 0) { - char lastChar = buf.charAt(buf.length() - 1); - - if (lastChar != '\'' && inside) { - buf.append('\''); - } + if (inside && buf.length() > 0) { + // Close the quoted literal. The quote state is tracked exactly, so a trailing quote character in + // buf never means an already closed literal. + buf.append('\''); } return buf.toString(); } diff --git a/test/org/apache/catalina/util/TestStrftime.java b/test/org/apache/catalina/util/TestStrftime.java new file mode 100644 index 0000000000..4b9214653a --- /dev/null +++ b/test/org/apache/catalina/util/TestStrftime.java @@ -0,0 +1,72 @@ +/* + * 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.catalina.util; + +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; + +import org.junit.Assert; +import org.junit.Test; + +public class TestStrftime { + + private static String format(String pattern) { + Strftime strftime = new Strftime(pattern, Locale.ENGLISH); + strftime.setTimeZone(TimeZone.getTimeZone("UTC")); + return strftime.format(new Date(0)); + } + + @Test + public void testPlainPattern() { + Assert.assertEquals("00:00", format("%H:%M")); + } + + /* + * A single quote in the strftime format is an ordinary literal character. It used to close the generated + * SimpleDateFormat quoted literal without the conversion tracking that, leaving the literal text that follows + * unquoted. Parsing the pattern then threw IllegalArgumentException (an "Illegal pattern character" or + * "Unterminated quote"), or the following characters were silently interpreted as pattern letters. + */ + @Test + public void testQuoteInLiteral() { + Assert.assertEquals("1970'it's", format("%Y'it's")); + Assert.assertEquals("yyyy'ab'cd", format("yyyy'ab'cd")); + // Trailing quote: in a pattern not starting with a command, this used to be dropped from the output + Assert.assertEquals("a'", format("a'")); + Assert.assertEquals("1970'", format("%Y'")); + } + + @Test + public void testLoneQuote() { + Assert.assertEquals("'", format("'")); + } + + @Test + public void testQuoteBetweenWords() { + Assert.assertEquals("a' b", format("a' b")); + } + + /* + * A pattern of the kind SSI config timefmt values commonly use, with the literal word 'GMT' quoted. + */ + @Test + public void testQuotedWordLiteral() { + Assert.assertEquals("01 Jan 1970 00:00:00 UTC 'GMT'", format("%d %b %Y %H:%M:%S %Z 'GMT'")); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
