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

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


The following commit(s) were added to refs/heads/master by this push:
     new 55dcbc4bb Fix mutable time zone keys in the FastDateFormat cache
55dcbc4bb is described below

commit 55dcbc4bb1449aa9f7c6ee422cc22b465a35fa2f
Author: Gary Gregory <[email protected]>
AuthorDate: Sat Sep 19 07:36:51 2026 -0700

    Fix mutable time zone keys in the FastDateFormat cache
    
    Clone the time zone before constructing the cache key so caller
    mutations
    cannot cause lookups to return formatters with stale time zone rules.
    
    Add a regression test covering cache reuse, formatting, and parsing
    after
    a DST rule change, and update the release notes.
---
 src/changes/changes.xml                            |  1 +
 .../commons/lang3/time/AbstractFormatCache.java    |  3 ++-
 .../commons/lang3/time/FastDateFormatTest.java     | 22 ++++++++++++++++++++++
 3 files changed, 25 insertions(+), 1 deletion(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 779773e32..244821452 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -287,6 +287,7 @@ java.lang.NullPointerException: Cannot invoke
     <action                   type="fix" dev="ggregory" due-to="gaurav kumar 
pandey, Gary Gregory">Fix TypeUtils.toString() recursion and bound handling on 
recursive generic types (#1789).</action>
     <action                   type="fix" dev="ggregory" due-to="尹茂椿萱, 
makarandhinge, Gary Gregory" issue="LANG-1834">Fix Fraction reduction for 
Integer.MIN_VALUE (#1794).</action>
     <action                   type="fix" dev="ggregory" due-to="alhuda, Gary 
Gregory">Defensively clone the TimeZone in FastDatePrinter and FastDateParser 
(#1795).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary 
Gregory">Defensively clone time zones used as FastDateFormat cache keys to 
prevent caller mutations from returning formatters with stale time zone 
rules.</action>
     <!-- ADD -->
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add JavaVersion.JAVA_27.</action>
     <action                   type="add" dev="ggregory" due-to="Gary 
Gregory">Add SystemUtils.IS_JAVA_27.</action>
diff --git 
a/src/main/java/org/apache/commons/lang3/time/AbstractFormatCache.java 
b/src/main/java/org/apache/commons/lang3/time/AbstractFormatCache.java
index a812f5705..fe9fc5767 100644
--- a/src/main/java/org/apache/commons/lang3/time/AbstractFormatCache.java
+++ b/src/main/java/org/apache/commons/lang3/time/AbstractFormatCache.java
@@ -219,7 +219,8 @@ public F getInstance() {
      */
     public F getInstance(final String pattern, final TimeZone timeZone, final 
Locale locale) {
         Objects.requireNonNull(pattern, "pattern");
-        final TimeZone actualTimeZone = TimeZones.toTimeZone(timeZone);
+        // Snapshot the mutable zone so the cache key and formatter retain the 
same rules.
+        final TimeZone actualTimeZone = (TimeZone) 
TimeZones.toTimeZone(timeZone).clone();
         final Locale actualLocale = LocaleUtils.toLocale(locale);
         final ArrayKey key = new ArrayKey(pattern, actualTimeZone, 
actualLocale);
         // Bound the cache: it is static and process-lifetime, so 
unbounded-cardinality keys
diff --git 
a/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java 
b/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
index d86d7ec4b..209a47d6b 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
@@ -35,6 +35,7 @@
 import java.util.Calendar;
 import java.util.Date;
 import java.util.Locale;
+import java.util.SimpleTimeZone;
 import java.util.TimeZone;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
@@ -493,6 +494,27 @@ void testTimeZoneArgumentIsCopied() throws ParseException {
         assertEquals(new Date(0), parser.parse("1970-01-01 00:00"));
     }
 
+    @Test
+    void testTimeZoneCacheKeyIsCopied() throws ParseException {
+        final SimpleTimeZone timeZone = new SimpleTimeZone(0, "CacheKeyCopy", 
Calendar.MARCH, 1, 0, 0, Calendar.OCTOBER, 1, 0, 0);
+        timeZone.setStartYear(2000);
+        final TimeZone originalTimeZone = (TimeZone) timeZone.clone();
+        final String pattern = "yyyy-MM-dd HH:mm";
+        final FastDateFormat original = FastDateFormat.getInstance(pattern, 
timeZone, Locale.US);
+        final Date date = Date.from(Instant.parse("2026-06-01T00:00:00Z"));
+        assertEquals("2026-06-01 01:00", original.format(date));
+
+        // Changing the DST start year preserves the hash code but changes the 
zone's rules and equality.
+        timeZone.setStartYear(2100);
+        final FastDateFormat changed = FastDateFormat.getInstance(pattern, 
(TimeZone) timeZone.clone(), Locale.US);
+        assertNotSame(original, changed);
+        assertSame(original, FastDateFormat.getInstance(pattern, 
originalTimeZone, Locale.US));
+        assertEquals("2026-06-01 01:00", original.format(date));
+        assertEquals("2026-06-01 00:00", changed.format(date));
+        assertEquals(date, original.parse("2026-06-01 01:00"));
+        assertEquals(date, changed.parse("2026-06-01 00:00"));
+    }
+
     /**
      * Mutating the TimeZone returned by the getter must not change the 
cached, shared instance.
      */

Reply via email to