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.
*/