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() {