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]

Reply via email to