From 39a96aac674b555c84ed3e9dd1f3a7c1b7ff2f05 Mon Sep 17 00:00:00 2001 From: sayaoailun Date: Fri, 30 Jan 2026 15:49:48 +0800 Subject: [PATCH 1/3] [NET-740] FTP fails to parse listings for Linux vsftpd in Chinese or Japanese --- .../net/ftp/parser/UnixFTPEntryParser.java | 15 ++++-- .../ftp/parser/UnixFTPEntryParserTest.java | 54 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java index b1ff4a623..0cef42df2 100644 --- a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java +++ b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java @@ -47,8 +47,10 @@ public class UnixFTPEntryParser extends ConfigurableFTPFileEntryParserImpl { private static final String JA_YEAR = "\u5e74"; private static final String DEFAULT_DATE_FORMAT_JA = "M'" + JA_MONTH + "' d'" + JA_DAY + "' yyyy'" + JA_YEAR + "'"; // 6月 3日 2003年 + private static final String DEFAULT_DATE_FORMAT_CN = "M'" + JA_MONTH + "' d yyyy"; // 6月 3 2003 private static final String DEFAULT_RECENT_DATE_FORMAT_JA = "M'" + JA_MONTH + "' d'" + JA_DAY + "' HH:mm"; // 8月 17日 20:10 + private static final String DEFAULT_RECENT_DATE_FORMAT_CN = "M'" + JA_MONTH + "' d HH:mm"; // 8月 17 20:10 private static final Pattern TOTAL_PATTERN = Pattern.compile("^total \\d+$"); @@ -100,7 +102,9 @@ public class UnixFTPEntryParser extends ConfigurableFTPFileEntryParserImpl { + "(" + "(?:\\d+[-/]\\d+[-/]\\d+)" + // yyyy-mm-dd "|(?:\\S{3}\\s+\\d{1,2})" + // MMM [d]d "|(?:\\d{1,2}\\s+\\S{3})" + // [d]d MMM - "|(?:\\d{1,2}" + JA_MONTH + "\\s+\\d{1,2}" + JA_DAY + ")" + ")" + "|(?:\\d{1,2}" + JA_MONTH + "\\s+\\d{1,2}" + JA_DAY + ")" + + "|(?:\\d{1,2}" + JA_MONTH + "\\s+\\d{1,2})" + + ")" + "\\s+" // separator /* * year (for non-recent standard format) - yyyy or time (for numeric or recent standard format) [h]h:mm or Japanese year - yyyyXX @@ -187,11 +191,16 @@ public FTPFile parseFTPEntry(final String entry) { name = name.replaceFirst("^\\s+", ""); } try { - if (group(19).contains(JA_MONTH)) { // special processing for Japanese format + if (group(19).contains(JA_MONTH) && group(19).contains(JA_DAY)) { // special processing for Japanese format final FTPTimestampParserImpl jaParser = new FTPTimestampParserImpl(); jaParser.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX, DEFAULT_DATE_FORMAT_JA, DEFAULT_RECENT_DATE_FORMAT_JA)); file.setTimestamp(jaParser.parseTimestamp(datestr)); - } else { + } else if (group(19).contains(JA_MONTH) && !group(19).contains(JA_DAY)) { + final FTPTimestampParserImpl jaParser = new FTPTimestampParserImpl(); + jaParser.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX, DEFAULT_DATE_FORMAT_CN, DEFAULT_RECENT_DATE_FORMAT_CN)); + file.setTimestamp(jaParser.parseTimestamp(datestr)); + } + else { file.setTimestamp(super.parseTimestamp(datestr)); } } catch (final ParseException e) { diff --git a/src/test/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParserTest.java b/src/test/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParserTest.java index a6addc59d..366d2ddc9 100644 --- a/src/test/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParserTest.java +++ b/src/test/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParserTest.java @@ -71,6 +71,9 @@ public class UnixFTPEntryParserTest extends AbstractFTPParseTest { "-rw-r--r-- 1 1 3518644 May 25 12:12 std", "-rw-rw---- 1 ep1adm sapsys 0 6\u6708 3\u65e5 2003\u5e74 \u8a66\u9a13\u30d5\u30a1\u30a4\u30eb.csv", "-rw-rw---- 1 ep1adm sapsys 0 8\u6708 17\u65e5 20:10 caerrinf", + "-rw-r--r-- 1 1 3518644 May 25 12:12 std", "-rw-rw---- 1 ep1adm sapsys 0 6\u6708 3 2003 \u8a66\u9a13\u30d5\u30a1\u30a4\u30eb.csv", + "-rw-rw---- 1 ep1adm sapsys 0 8\u6708 17 20:10 caerrinf", + }; private void checkPermissions(final FTPFile f) { @@ -365,6 +368,34 @@ public void testParseFieldsOnFileJapaneseTime() { assertEquals(df.format(cal.getTime()), df.format(f.getTimestamp().getTime())); } + @Test + public void testParseFieldsOnFileChineseTime() { + final FTPFile f = getParser().parseFTPEntry("-rwxr-xr-x 2 user group 4096 3\u6708 2 15:13 zxbox"); + assertNotNull(f, "Could not parse entry."); + assertTrue(f.isFile(), "Should have been a file."); + checkPermissions(f); + assertEquals(2, f.getHardLinkCount()); + assertEquals("user", f.getUser()); + assertEquals("group", f.getGroup()); + assertEquals("zxbox", f.getName()); + assertEquals(4096, f.getSize()); + + assertNotNull(f.getTimestamp(), "Timestamp not null"); + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.MONTH, Calendar.MARCH); + cal.set(Calendar.DATE, 1); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + if (f.getTimestamp().getTime().before(cal.getTime())) { + cal.add(Calendar.YEAR, -1); + } + cal.set(Calendar.DATE, 2); + cal.set(Calendar.HOUR_OF_DAY, 15); + cal.set(Calendar.MINUTE, 13); + assertEquals(df.format(cal.getTime()), df.format(f.getTimestamp().getTime())); + } + // https://mail-archives.apache.org/mod_mbox/commons-dev/200408.mbox/%3c4122F3C1.9090402@tanukisoftware.com%3e @Test public void testParseFieldsOnFileJapaneseYear() { @@ -389,6 +420,29 @@ public void testParseFieldsOnFileJapaneseYear() { assertEquals(df.format(cal.getTime()), df.format(f.getTimestamp().getTime())); } + @Test + public void testParseFieldsOnFileChineseYear() { + final FTPFile f = getParser().parseFTPEntry("-rwxr-xr-x 2 user group 4096 3\u6708 2 2003 \u8a66\u9a13\u30d5\u30a1\u30a4\u30eb.csv"); + assertNotNull(f, "Could not parse entry."); + assertTrue(f.isFile(), "Should have been a file."); + checkPermissions(f); + assertEquals(2, f.getHardLinkCount()); + assertEquals("user", f.getUser()); + assertEquals("group", f.getGroup()); + assertEquals("\u8a66\u9a13\u30d5\u30a1\u30a4\u30eb.csv", f.getName()); + assertEquals(4096, f.getSize()); + + assertNotNull(f.getTimestamp(), "Timestamp not null"); + final Calendar cal = Calendar.getInstance(); + cal.set(Calendar.YEAR, 2003); + cal.set(Calendar.MONTH, Calendar.MARCH); + cal.set(Calendar.DATE, 2); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + assertEquals(df.format(cal.getTime()), df.format(f.getTimestamp().getTime())); + } + @Override @Test public void testRecentPrecision() { From 7706414b59bf335546e241d5c72015fd41e657d6 Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 30 Jan 2026 09:13:22 -0500 Subject: [PATCH 2/3] Fix formatting --- .../org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java index 0cef42df2..1b46e54ad 100644 --- a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java +++ b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java @@ -199,8 +199,7 @@ public FTPFile parseFTPEntry(final String entry) { final FTPTimestampParserImpl jaParser = new FTPTimestampParserImpl(); jaParser.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX, DEFAULT_DATE_FORMAT_CN, DEFAULT_RECENT_DATE_FORMAT_CN)); file.setTimestamp(jaParser.parseTimestamp(datestr)); - } - else { + } else { file.setTimestamp(super.parseTimestamp(datestr)); } } catch (final ParseException e) { From 09c92ea78e21b0ed6603a3903b85085a8daf70ba Mon Sep 17 00:00:00 2001 From: Gary Gregory Date: Fri, 30 Jan 2026 09:16:18 -0500 Subject: [PATCH 3/3] Fix trailing whitespace --- .../org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java index 1b46e54ad..70aef853c 100644 --- a/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java +++ b/src/main/java/org/apache/commons/net/ftp/parser/UnixFTPEntryParser.java @@ -195,7 +195,7 @@ public FTPFile parseFTPEntry(final String entry) { final FTPTimestampParserImpl jaParser = new FTPTimestampParserImpl(); jaParser.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX, DEFAULT_DATE_FORMAT_JA, DEFAULT_RECENT_DATE_FORMAT_JA)); file.setTimestamp(jaParser.parseTimestamp(datestr)); - } else if (group(19).contains(JA_MONTH) && !group(19).contains(JA_DAY)) { + } else if (group(19).contains(JA_MONTH) && !group(19).contains(JA_DAY)) { final FTPTimestampParserImpl jaParser = new FTPTimestampParserImpl(); jaParser.configure(new FTPClientConfig(FTPClientConfig.SYST_UNIX, DEFAULT_DATE_FORMAT_CN, DEFAULT_RECENT_DATE_FORMAT_CN)); file.setTimestamp(jaParser.parseTimestamp(datestr));