diff --git a/pom.xml b/pom.xml
index b30b1cff0..531aacab0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -119,6 +119,11 @@ Supported protocols include Echo, Finger, FTP, NNTP, NTP, POP3(S), SMTP(S), Teln
junit-jupiter-params
test
+
+ org.junit-pioneer
+ junit-pioneer
+ test
+
org.apache.ftpserver
ftpserver-core
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 c18fc6020..def415a36 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 FTPFile parseFTPEntry(final String entry) {
// 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 30a765bb5..9976ff338 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.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 Calendar parseTimestamp(final String timestampStr, final Calendar serverT
// 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 @@ private void setDefaultDateFormat(final String format, final DateFormatSymbols d
} else {
defaultDateFormat = new SimpleDateFormat(format);
}
+ defaultDateFormat.setCalendar(new GregorianCalendar());
defaultDateFormat.setLenient(false);
} else {
defaultDateFormat = null;
@@ -363,6 +370,7 @@ private void setRecentDateFormat(final String format, final DateFormatSymbols df
} 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 7c2bc8b0f..5ffab350e 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 static Calendar parseGMTdateTime(final String timestamp) {
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 3ead102c9..d1674ddad 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 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 @@ void testParseFieldsOnFile() throws Exception {
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 e7662ed4b..55a5618a6 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 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 @@ void testParser() throws ParseException {
}
}
+ /**
+ * 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 325d108a1..b99b619b3 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 @@ protected FTPFile nullFileOrNullDate(final FTPFile f) {
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() {