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

Reply via email to