From df000db5aa3eb1a1483d6ec9a5b2dd17c604d8cb Mon Sep 17 00:00:00 2001
From: Alhuda Khan
Date: Sat, 19 Sep 2026 14:10:40 +0530
Subject: [PATCH] copy the TimeZone in FastDatePrinter and FastDateParser
TimeZone is mutable and FastDateFormat shares cached instances process-wide, so mutating the zone passed to the factory, or the one returned by getTimeZone(), changed the formatter for every other holder. Clone it in both constructors and both getters.
---
.../commons/lang3/time/FastDateFormat.java | 2 +-
.../commons/lang3/time/FastDateParser.java | 5 ++--
.../commons/lang3/time/FastDatePrinter.java | 5 ++--
.../lang3/time/FastDateFormatTest.java | 28 +++++++++++++++++++
.../lang3/time/FastDateParserTest.java | 14 ++++++++++
5 files changed, 49 insertions(+), 5 deletions(-)
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java b/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
index e7c01293804..b1bf487489f 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
@@ -591,7 +591,7 @@ public String getPattern() {
* This zone is always used for {@link Date} formatting.
*
*
- * @return The time zone.
+ * @return A copy of the time zone, changing it has no effect on this formatter.
*/
@Override
public TimeZone getTimeZone() {
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
index 3f47d4cf9f4..7b632083e8c 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
@@ -940,7 +940,8 @@ protected FastDateParser(final String pattern, final TimeZone timeZone, final Lo
*/
protected FastDateParser(final String pattern, final TimeZone timeZone, final Locale locale, final Date centuryStart) {
this.pattern = Objects.requireNonNull(pattern, "pattern");
- this.timeZone = Objects.requireNonNull(timeZone, "timeZone");
+ // TimeZone is mutable and instances are shared through the FastDateFormat cache.
+ this.timeZone = (TimeZone) Objects.requireNonNull(timeZone, "timeZone").clone();
this.locale = LocaleUtils.toLocale(locale);
final Calendar definingCalendar = Calendar.getInstance(timeZone, this.locale);
final int centuryStartYear;
@@ -1108,7 +1109,7 @@ private Strategy getStrategy(final char f, final int width, final Calendar defin
*/
@Override
public TimeZone getTimeZone() {
- return timeZone;
+ return (TimeZone) timeZone.clone();
}
/**
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
index 0beed585eee..e67971fa2a1 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
@@ -1067,7 +1067,8 @@ static String getTimeZoneDisplay(final TimeZone tz, final boolean daylight, fina
*/
protected FastDatePrinter(final String pattern, final TimeZone timeZone, final Locale locale) {
this.pattern = pattern;
- this.timeZone = timeZone;
+ // TimeZone is mutable and instances are shared through the FastDateFormat cache.
+ this.timeZone = (TimeZone) timeZone.clone();
this.locale = LocaleUtils.toLocale(locale);
init();
}
@@ -1304,7 +1305,7 @@ public String getPattern() {
*/
@Override
public TimeZone getTimeZone() {
- return timeZone;
+ return (TimeZone) timeZone.clone();
}
/**
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 30586ac0a84..d86d7ec4be5 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDateFormatTest.java
@@ -478,4 +478,32 @@ void testTimeDefaults() {
assertEquals(FastDateFormat.getTimeInstance(FastDateFormat.LONG),
FastDateFormat.getTimeInstance(FastDateFormat.LONG, TimeZone.getDefault(), Locale.getDefault()));
}
+
+ /**
+ * Mutating the TimeZone passed to the factory must not change the cached, shared instance.
+ */
+ @Test
+ void testTimeZoneArgumentIsCopied() throws ParseException {
+ final TimeZone timeZone = TimeZones.getTimeZone("UTC");
+ final FastDateFormat printer = FastDateFormat.getInstance("yyyy-MM-dd HH:mm Z", timeZone, Locale.US);
+ final FastDateFormat parser = FastDateFormat.getInstance("yyyy-MM-dd HH:mm", timeZone, Locale.US);
+ timeZone.setRawOffset(5 * 3_600_000);
+ assertEquals(TimeZones.getTimeZone("UTC"), printer.getTimeZone());
+ assertEquals("1970-01-01 00:00 +0000", printer.format(new Date(0)));
+ assertEquals(new Date(0), parser.parse("1970-01-01 00:00"));
+ }
+
+ /**
+ * Mutating the TimeZone returned by the getter must not change the cached, shared instance.
+ */
+ @Test
+ void testTimeZoneGetterReturnsCopy() throws ParseException {
+ final FastDateFormat printer = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss Z", TimeZones.getTimeZone("UTC"), Locale.US);
+ final FastDateFormat parser = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss", TimeZones.getTimeZone("UTC"), Locale.US);
+ printer.getTimeZone().setRawOffset(5 * 3_600_000);
+ parser.getTimeZone().setRawOffset(5 * 3_600_000);
+ assertEquals(TimeZones.getTimeZone("UTC"), printer.getTimeZone());
+ assertEquals("1970-01-01 00:00:00 +0000", printer.format(new Date(0)));
+ assertEquals(new Date(0), parser.parse("1970-01-01 00:00:00"));
+ }
}
diff --git a/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java b/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java
index 22b6357ee48..0fddcbafce4 100644
--- a/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/FastDateParserTest.java
@@ -863,6 +863,20 @@ void testSpecialCharacters(final TriFunction