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-net.git
The following commit(s) were added to refs/heads/master by this push:
new 049602ad Force Gregorian calendar in FTP listing timestamp parsers
(#406)
049602ad is described below
commit 049602ad0c5b852c6ababd0cb3c20e5e4e39e7ff
Author: Javid Khan <[email protected]>
AuthorDate: Wed Jul 29 20:32:44 2026 +0530
Force Gregorian calendar in FTP listing timestamp parsers (#406)
* force gregorian calendar in ftp listing timestamp parsers
* use junit pioneer for locale tests and derive appended year from
gregorian calendar
* No latin needed.
Updated comment for clarity regarding RFC 3659 timestamp parsing with
non-Gregorian locales.
* Clarify RFC 3659 timestamp parsing in test
Update test comment to clarify RFC 3659 timestamp parsing.
---------
Co-authored-by: Gary Gregory <[email protected]>
---
pom.xml | 5 +++
.../ftp/parser/EnterpriseUnixFTPEntryParser.java | 3 +-
.../net/ftp/parser/FTPTimestampParserImpl.java | 10 ++++-
.../commons/net/ftp/parser/MLSxEntryParser.java | 4 +-
.../parser/EnterpriseUnixFTPEntryParserTest.java | 16 +++++++
.../net/ftp/parser/FTPTimestampParserImplTest.java | 50 ++++++++++++++++++++++
.../net/ftp/parser/MLSxEntryParserTest.java | 22 ++++++++++
7 files changed, 106 insertions(+), 4 deletions(-)
diff --git a/pom.xml b/pom.xml
index b30b1cff..531aacab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,11 @@ Supported protocols include Echo, Finger, FTP, NNTP, NTP,
POP3(S), SMTP(S), Teln
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.junit-pioneer</groupId>
+ <artifactId>junit-pioneer</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.apache.ftpserver</groupId>
<artifactId>ftpserver-core</artifactId>
diff --git
a/src/main/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParser.java
b/src/main/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParser.java
index c18fc602..def415a3 100644
---
a/src/main/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParser.java
+++
b/src/main/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParser.java
@@ -18,6 +18,7 @@
package org.apache.commons.net.ftp.parser;
import java.util.Calendar;
+import java.util.GregorianCalendar;
import org.apache.commons.net.ftp.FTPFile;
@@ -105,7 +106,7 @@ public class EnterpriseUnixFTPEntryParser extends
RegexFTPFileEntryParserImpl {
// intentionally do nothing
}
- final Calendar cal = Calendar.getInstance();
+ final Calendar cal = new GregorianCalendar();
cal.set(Calendar.MILLISECOND, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MINUTE, 0);
diff --git
a/src/main/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImpl.java
b/src/main/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImpl.java
index 30a765bb..9976ff33 100644
---
a/src/main/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImpl.java
+++
b/src/main/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImpl.java
@@ -23,6 +23,7 @@ import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
+import java.util.GregorianCalendar;
import java.util.TimeZone;
import org.apache.commons.net.ftp.Configurable;
@@ -291,9 +292,14 @@ public class FTPTimestampParserImpl implements
FTPTimestampParser, Configurable
// all instances of short dates which are +- 6 months from current
date.
// TODO this won't always work for systems that use short dates
+0/-12months
// e.g. if today is Jan 1 2001 and the short date is Feb 29
- final String year = Integer.toString(now.get(Calendar.YEAR));
+ // now is a clone of the caller's serverTime, which under some
default locales (e.g. a Thai Buddhist calendar) is not Gregorian, so read the
year
+ // through a Gregorian calendar at the same instant to keep the
appended year Gregorian rather than 543 years out.
+ final GregorianCalendar gregorianNow = new
GregorianCalendar(now.getTimeZone());
+ gregorianNow.setTimeInMillis(now.getTimeInMillis());
+ final String year =
Integer.toString(gregorianNow.get(Calendar.YEAR));
final String timeStampStrPlusYear = timestampStr + " " + year;
final SimpleDateFormat hackFormatter = new
SimpleDateFormat(recentDateFormat.toPattern() + " yyyy",
recentDateFormat.getDateFormatSymbols());
+ hackFormatter.setCalendar(new GregorianCalendar());
hackFormatter.setLenient(false);
hackFormatter.setTimeZone(recentDateFormat.getTimeZone());
final ParsePosition pp = new ParsePosition(0);
@@ -338,6 +344,7 @@ public class FTPTimestampParserImpl implements
FTPTimestampParser, Configurable
} else {
defaultDateFormat = new SimpleDateFormat(format);
}
+ defaultDateFormat.setCalendar(new GregorianCalendar());
defaultDateFormat.setLenient(false);
} else {
defaultDateFormat = null;
@@ -363,6 +370,7 @@ public class FTPTimestampParserImpl implements
FTPTimestampParser, Configurable
} else {
recentDateFormat = new SimpleDateFormat(format);
}
+ recentDateFormat.setCalendar(new GregorianCalendar());
recentDateFormat.setLenient(false);
} else {
recentDateFormat = null;
diff --git
a/src/main/java/org/apache/commons/net/ftp/parser/MLSxEntryParser.java
b/src/main/java/org/apache/commons/net/ftp/parser/MLSxEntryParser.java
index 7c2bc8b0..5ffab350 100644
--- a/src/main/java/org/apache/commons/net/ftp/parser/MLSxEntryParser.java
+++ b/src/main/java/org/apache/commons/net/ftp/parser/MLSxEntryParser.java
@@ -111,10 +111,10 @@ public class MLSxEntryParser extends
FTPFileEntryParserImpl {
final SimpleDateFormat dateFormat;
final boolean hasMillis;
if (timestamp.contains(".")) {
- dateFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS");
+ dateFormat = new SimpleDateFormat("yyyyMMddHHmmss.SSS",
Locale.ROOT);
hasMillis = true;
} else {
- dateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
+ dateFormat = new SimpleDateFormat("yyyyMMddHHmmss", Locale.ROOT);
hasMillis = false;
}
final TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
diff --git
a/src/test/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParserTest.java
b/src/test/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParserTest.java
index 3ead102c..d1674dda 100644
---
a/src/test/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParserTest.java
+++
b/src/test/java/org/apache/commons/net/ftp/parser/EnterpriseUnixFTPEntryParserTest.java
@@ -30,6 +30,7 @@ import java.util.TimeZone;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileEntryParser;
import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.DefaultLocale;
/**
* Tests the EnterpriseUnixFTPEntryParser
@@ -156,6 +157,21 @@ class EnterpriseUnixFTPEntryParserTest extends
AbstractFTPParseTest {
assertEquals(0, zDateTime.getSecond());
}
+ /**
+ * A numeric year in a listing must be read as a Gregorian year regardless
of the JVM default locale. Under a Thai locale the base Calendar is a Buddhist
+ * calendar, which would otherwise store the timestamp 543 years out.
+ */
+ @Test
+ @DefaultLocale(language = "th", country = "TH")
+ void testAbsoluteYearWithNonGregorianDefaultLocale() {
+ final FTPFile ftpFile = getParser().parseFTPEntry("-C--E-----FTP A
QUA1I1 18128 41 Apr 1 2014 QUADTEST3");
+ final TimeZone timeZone = TimeZone.getDefault();
+ final ZonedDateTime zDateTime =
ZonedDateTime.ofInstant(ftpFile.getTimestampInstant(),
ZoneId.of(timeZone.getID()));
+ assertEquals(2014, zDateTime.getYear());
+ assertEquals(Month.APRIL, zDateTime.getMonth());
+ assertEquals(1, zDateTime.getDayOfMonth());
+ }
+
@Override
@Test
void testRecentPrecision() {
diff --git
a/src/test/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImplTest.java
b/src/test/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImplTest.java
index e7662ed4..55a5618a 100644
---
a/src/test/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImplTest.java
+++
b/src/test/java/org/apache/commons/net/ftp/parser/FTPTimestampParserImplTest.java
@@ -32,6 +32,7 @@ import java.util.TimeZone;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.DefaultLocale;
/**
* Test the FTPTimestampParser class.
@@ -271,6 +272,55 @@ java.text.ParseException: Timestamp 'Mar 13 02:33' could
not be parsed using a s
}
}
+ /**
+ * A numeric year from a server listing must be read as a Gregorian year
regardless of the JVM default locale. Under a Thai locale the default
+ * SimpleDateFormat calendar is a Buddhist calendar, which would otherwise
shift the parsed instant by 543 years.
+ */
+ @Test
+ @DefaultLocale(language = "th", country = "TH")
+ void testParseTimestampWithNonGregorianDefaultLocale() throws
ParseException {
+ final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
+ final FTPClientConfig config = new
FTPClientConfig(FTPClientConfig.SYST_UNIX);
+ config.setDefaultDateFormatStr("yyyy-MM-dd HH:mm");
+ config.setRecentDateFormatStr("MMM d HH:mm");
+ config.setServerLanguageCode("en");
+ config.setServerTimeZoneId("GMT");
+ parser.configure(config);
+ final Calendar parsed = parser.parseTimestamp("2010-03-13 22:45", new
GregorianCalendar());
+ final GregorianCalendar expected = new
GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
+ expected.clear();
+ expected.set(2010, Calendar.MARCH, 13, 22, 45, 0);
+ assertEquals(expected.getTimeInMillis(), parsed.getTimeInMillis());
+ }
+
+ /**
+ * A recent (short) date carries no year, so the parser appends the year
taken from the supplied server time. When a caller builds that server time via
+ * {@link Calendar#getInstance()} under a Thai default locale it is a
Buddhist calendar, so the appended year must be read through a Gregorian
calendar or
+ * the parsed instant lands 543 years out.
+ */
+ @Test
+ @DefaultLocale(language = "th", country = "TH")
+ void testParseRecentTimestampWithNonGregorianDefaultLocale() throws
ParseException {
+ final FTPTimestampParserImpl parser = new FTPTimestampParserImpl();
+ final FTPClientConfig config = new
FTPClientConfig(FTPClientConfig.SYST_UNIX);
+ config.setDefaultDateFormatStr("yyyy-MM-dd HH:mm");
+ config.setRecentDateFormatStr("MMM d HH:mm");
+ config.setServerLanguageCode("en");
+ config.setServerTimeZoneId("GMT");
+ parser.configure(config);
+ // Calendar.getInstance() under a Thai default locale is a Buddhist
calendar, mirroring how a caller builds the server time.
+ final Calendar serverTime =
Calendar.getInstance(TimeZone.getTimeZone("GMT"));
+ final GregorianCalendar serverInstant = new
GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
+ serverInstant.clear();
+ serverInstant.set(2010, Calendar.JUNE, 1, 0, 0, 0);
+ serverTime.setTimeInMillis(serverInstant.getTimeInMillis());
+ final Calendar parsed = parser.parseTimestamp("Mar 13 22:45",
serverTime);
+ final GregorianCalendar expected = new
GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
+ expected.clear();
+ expected.set(2010, Calendar.MARCH, 13, 22, 45, 0);
+ assertEquals(expected.getTimeInMillis(), parsed.getTimeInMillis());
+ }
+
@Test
void testParseShortFutureDates1() throws Exception {
final GregorianCalendar now = new GregorianCalendar(2001,
Calendar.MAY, 30, 12, 0);
diff --git
a/src/test/java/org/apache/commons/net/ftp/parser/MLSxEntryParserTest.java
b/src/test/java/org/apache/commons/net/ftp/parser/MLSxEntryParserTest.java
index 325d108a..b99b619b 100644
--- a/src/test/java/org/apache/commons/net/ftp/parser/MLSxEntryParserTest.java
+++ b/src/test/java/org/apache/commons/net/ftp/parser/MLSxEntryParserTest.java
@@ -16,9 +16,17 @@
*/
package org.apache.commons.net.ftp.parser;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Calendar;
+import java.util.GregorianCalendar;
+import java.util.Locale;
+import java.util.TimeZone;
+
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileEntryParser;
import org.junit.jupiter.api.Test;
+import org.junitpioneer.jupiter.DefaultLocale;
/**
*/
@@ -68,6 +76,20 @@ class MLSxEntryParserTest extends AbstractFTPParseTest {
return f;
}
+ /**
+ * The RFC 3659 time stamp is a numeric Gregorian date. Parsing it must
not depend on the JVM default locale's calendar, which, for example,
+ * a Thai locale is a Buddhist calendar that would read the year 543 years
out.
+ */
+ @Test
+ @DefaultLocale(language = "th", country = "TH")
+ void testParseGMTdateTimeWithNonGregorianDefaultLocale() {
+ final Calendar parsed =
MLSxEntryParser.parseGMTdateTime("20100313224553");
+ final GregorianCalendar expected = new
GregorianCalendar(TimeZone.getTimeZone("GMT"), Locale.ROOT);
+ expected.clear();
+ expected.set(2010, Calendar.MARCH, 13, 22, 45, 53);
+ assertEquals(expected.getTimeInMillis(), parsed.getTimeInMillis());
+ }
+
@Override
@Test
void testDefaultPrecision() {