diff options
-rw-r--r-- | net/data/ftp/dir-listing-vms-1 | 19 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-1.expected | 98 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-2 | 35 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-2.expected | 260 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-3 | 3 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-3.expected | 0 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-4 | 15 | ||||
-rw-r--r-- | net/data/ftp/dir-listing-vms-4.expected | 71 | ||||
-rw-r--r-- | net/ftp/ftp_directory_listing_buffer.cc | 34 | ||||
-rw-r--r-- | net/ftp/ftp_directory_listing_buffer_unittest.cc | 32 | ||||
-rw-r--r-- | net/ftp/ftp_directory_listing_parsers.cc | 335 | ||||
-rw-r--r-- | net/ftp/ftp_directory_listing_parsers.h | 67 | ||||
-rw-r--r-- | net/ftp/ftp_directory_listing_parsers_unittest.cc | 116 |
13 files changed, 1006 insertions, 79 deletions
diff --git a/net/data/ftp/dir-listing-vms-1 b/net/data/ftp/dir-listing-vms-1 new file mode 100644 index 0000000..ca7cdf1 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-1 @@ -0,0 +1,19 @@ +
+
+
+Directory ANONYMOUS_ROOT:[000000]
+
+.WELCOME;1 2 13-FEB-2002 23:32:40.47
+DECUS.DIR;1 1 9-MAY-2001 22:18:51.69
+INFORMATION.DIR;1 1 9-MAY-2001 22:23:42.78
+MADGOAT.DIR;1 2 9-MAY-2001 22:23:44.85
+MAIL_ARCHIVES.DIR;1
+ 1 13-DEC-2005 08:45:27.56
+MOZILLA.DIR;1 1 21-JUN-2001 14:57:51.38
+README.TXT;4 2 18-APR-2000 10:40:39.90
+SSH.DIR;1 1 22-JUN-2002 15:11:12.71
+SUPPORT.DIR;1 3 9-MAY-2001 22:29:45.02
+TCPWARE.DIR;1 1 9-MAY-2001 23:34:10.92
+VMS-FREEWARE.DIR;1 2 9-MAY-2001 23:58:31.39
+
+Total of 11 files, 17 blocks.
diff --git a/net/data/ftp/dir-listing-vms-1.expected b/net/data/ftp/dir-listing-vms-1.expected new file mode 100644 index 0000000..5bc2ab5 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-1.expected @@ -0,0 +1,98 @@ +- +.welcome +1024 +2002 +2 +13 +23 +32 + +d +decus +-1 +2001 +5 +9 +22 +18 + +d +information +-1 +2001 +5 +9 +22 +23 + +d +madgoat +-1 +2001 +5 +9 +22 +23 + +d +mail_archives +-1 +2005 +12 +13 +8 +45 + +d +mozilla +-1 +2001 +6 +21 +14 +57 + +- +readme.txt +1024 +2000 +4 +18 +10 +40 + +d +ssh +-1 +2002 +6 +22 +15 +11 + +d +support +-1 +2001 +5 +9 +22 +29 + +d +tcpware +-1 +2001 +5 +9 +23 +34 + +d +vms-freeware +-1 +2001 +5 +9 +23 +58 diff --git a/net/data/ftp/dir-listing-vms-2 b/net/data/ftp/dir-listing-vms-2 new file mode 100644 index 0000000..7fc20a9 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-2 @@ -0,0 +1,35 @@ + +Directory SYS$SYSDEVICE:[ANONYMOUS] + +ANNOUNCE.TXT;2 1/16 12-MAR-2005 08:44:57 [SYSTEM] (RWED,RWED,RE,RE) +BOINC.DIR;1 1/16 29-DEC-2005 21:33:21 [SYSTEM] (RWE,RWE,RE,RE) +BZIP2.DIR;1 1/16 27-SEP-2005 19:45:39 [SYSTEM] (RWE,RWE,RE,RE) +CDRTOOLS.DIR;1 3/16 10-MAR-2005 17:31:44 [SYSTEM] (RWE,RWE,RE,RE) +DIFFUTILS.DIR;1 1/16 23-JUN-2007 23:04:21 [SYSTEM] (RWE,RWE,RE,RE) +DTSS_NTP.DIR;1 2/16 25-SEP-2000 21:03:28 [SYSTEM] (RWE,RWE,RE,RE) +FIXREC.DIR;1 1/16 20-DEC-2003 10:57:22 [SYSTEM] (RWE,RWE,RE,RE) +GNUPG.DIR;1 1/16 9-AUG-2006 02:11:51 [SYSTEM] (RWE,RWE,RE,RE) +GZIP.DIR;1 1/16 5-JUL-2006 21:59:45 [SYSTEM] (RWE,RWE,RE,RE) +INFO-ZIP.DIR;1 15/16 20-SEP-2004 21:27:27 [SYSTEM] (RWE,RWE,RE,RE) +INPUT.DIR;1 1/16 4-MAR-1999 22:14:34 [UCX$NOBO,ANONYMOUS] (RWE,RWE,RWE,RWE) +KERMIT.DIR;1 1/16 25-FEB-2006 12:22:34 [SYSTEM] (RWE,RWE,RE,RE) +LOGIN.COM;2 1/16 28-SEP-2006 09:20:32 [SYSTEM] (RWED,RWED,RE,RE) +MISC.DIR;1 6/16 12-DEC-1999 17:31:56 [SYSTEM] (RWE,RWE,RE,RE) +MMK.DIR;1 1/16 30-SEP-2009 08:06:26 [SYSTEM] (RWE,RWE,RE,RE) +MOZ_TEST.DIR;1 1/16 8-APR-2008 17:12:53 [SYSTEM] (RWE,RWE,RE,RE) +MPACK.DIR;1 1/16 21-AUG-2009 10:28:57 [SYSTEM] (RWE,RWE,RE,RE) +MTOOLS.DIR;1 1/16 14-MAR-2006 15:05:01 [SYSTEM] (RWE,RWE,RE,RE) +OPENSSL.DIR;1 1/16 12-JAN-2009 08:42:56 [SYSTEM] (RWE,RWE,RE,RE) +PGP.DIR;1 1/16 19-SEP-1999 16:39:04 [SYSTEM] (RWE,RWE,RE,RE) +PICS.DIR;1 No privilege for attempted operation +QREADCD.DIR;1 1/16 29-SEP-2004 20:32:38 [SYSTEM] (RWE,RWE,RE,RE) +RZSPINUP.DIR;1 1/16 24-JUL-2004 21:34:12 [SYSTEM] (RWE,RWE,RE,RE) +TEST.DIR;1 1/16 5-NOV-2008 21:59:10 [SYSTEM] (RWE,RWE,RE,RE) +VIM.DIR;1 1/16 30-APR-2005 16:32:56 [SYSTEM] (RWE,RWE,RE,RE) +VMSTAR.DIR;1 1/16 7-JUN-2007 09:36:04 [SYSTEM] (RWE,RWE,RE,RE) +WELCOME.TXT;3 1/16 12-MAR-2005 08:45:28 [SYSTEM] (RWED,RWED,RE,RE) +WGET.DIR;1 3/16 17-AUG-1999 20:41:54 [SYSTEM] (RWE,RWE,RE,RE) +WGET_TEST.DIR;1 1/16 13-JUN-2006 21:29:27 [SYSTEM] (RWE,RWE,RE,RE) +WPUT.DIR;1 1/16 9-DEC-2004 20:16:46 [SYSTEM] (RWE,RWE,RE,RE) + +Total of 30 files, 53/464 blocks diff --git a/net/data/ftp/dir-listing-vms-2.expected b/net/data/ftp/dir-listing-vms-2.expected new file mode 100644 index 0000000..6b3ca66 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-2.expected @@ -0,0 +1,260 @@ +- +announce.txt +512 +2005 +3 +12 +8 +44 + +d +boinc +-1 +2005 +12 +29 +21 +33 + +d +bzip2 +-1 +2005 +9 +27 +19 +45 + +d +cdrtools +-1 +2005 +3 +10 +17 +31 + +d +diffutils +-1 +2007 +6 +23 +23 +4 + +d +dtss_ntp +-1 +2000 +9 +25 +21 +3 + +d +fixrec +-1 +2003 +12 +20 +10 +57 + +d +gnupg +-1 +2006 +8 +9 +2 +11 + +d +gzip +-1 +2006 +7 +5 +21 +59 + +d +info-zip +-1 +2004 +9 +20 +21 +27 + +d +input +-1 +1999 +3 +4 +22 +14 + +d +kermit +-1 +2006 +2 +25 +12 +22 + +- +login.com +512 +2006 +9 +28 +9 +20 + +d +misc +-1 +1999 +12 +12 +17 +31 + +d +mmk +-1 +2009 +9 +30 +8 +6 + +d +moz_test +-1 +2008 +4 +8 +17 +12 + +d +mpack +-1 +2009 +8 +21 +10 +28 + +d +mtools +-1 +2006 +3 +14 +15 +5 + +d +openssl +-1 +2009 +1 +12 +8 +42 + +d +pgp +-1 +1999 +9 +19 +16 +39 + +d +qreadcd +-1 +2004 +9 +29 +20 +32 + +d +rzspinup +-1 +2004 +7 +24 +21 +34 + +d +test +-1 +2008 +11 +5 +21 +59 + +d +vim +-1 +2005 +4 +30 +16 +32 + +d +vmstar +-1 +2007 +6 +7 +9 +36 + +- +welcome.txt +512 +2005 +3 +12 +8 +45 + +d +wget +-1 +1999 +8 +17 +20 +41 + +d +wget_test +-1 +2006 +6 +13 +21 +29 + +d +wput +-1 +2004 +12 +9 +20 +16 diff --git a/net/data/ftp/dir-listing-vms-3 b/net/data/ftp/dir-listing-vms-3 new file mode 100644 index 0000000..3bda7ff --- /dev/null +++ b/net/data/ftp/dir-listing-vms-3 @@ -0,0 +1,3 @@ +
+
+Total of 0 blocks in 0 files in 0 directories.
diff --git a/net/data/ftp/dir-listing-vms-3.expected b/net/data/ftp/dir-listing-vms-3.expected new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-3.expected diff --git a/net/data/ftp/dir-listing-vms-4 b/net/data/ftp/dir-listing-vms-4 new file mode 100644 index 0000000..0954441 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-4 @@ -0,0 +1,15 @@ + +EISNER$DRA3:[DECUSERVE_USER.JOHNDOE] + +$MAIN.TPU$JOURNAL;1 1 4-NOV-2009 05:59 [JOHNDOE] (RWED,RWED,,) +.WELCOME;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,) +EXAMPLE.TXT;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,) +FILE.;1 1 4-NOV-2009 08:59 [JOHNDOE] (RWED,RWED,,) +FTP_SERVER.LOG;12 0 4-NOV-2009 09:12 [JOHNDOE] (RWED,RWED,,) +LOGIN.COM;1 2 4-NOV-2009 05:58 [JOHNDOE] (RWED,RWED,,) +NOTES$NOTEBOOK.NOTE;1 + 36 4-NOV-2009 05:55 [DECUSERVE] (RWE,RWE,,) +TEST.DIR;1 1 4-NOV-2009 08:15 [JOHNDOE] (RWE,RWE,,) + + +Total of 43 blocks in 8 files. diff --git a/net/data/ftp/dir-listing-vms-4.expected b/net/data/ftp/dir-listing-vms-4.expected new file mode 100644 index 0000000..3c89235 --- /dev/null +++ b/net/data/ftp/dir-listing-vms-4.expected @@ -0,0 +1,71 @@ +- +$main.tpu$journal +512 +2009 +11 +4 +5 +59 + +- +.welcome +512 +2009 +11 +4 +6 +2 + +- +example.txt +512 +2009 +11 +4 +6 +2 + +- +file. +512 +2009 +11 +4 +8 +59 + +- +ftp_server.log +0 +2009 +11 +4 +9 +12 + +- +login.com +1024 +2009 +11 +4 +5 +58 + +- +notes$notebook.note +18432 +2009 +11 +4 +5 +55 + +d +test +-1 +2009 +11 +4 +8 +15 diff --git a/net/ftp/ftp_directory_listing_buffer.cc b/net/ftp/ftp_directory_listing_buffer.cc index 0099e91..030da83 100644 --- a/net/ftp/ftp_directory_listing_buffer.cc +++ b/net/ftp/ftp_directory_listing_buffer.cc @@ -37,10 +37,11 @@ std::string DetectEncoding(const std::string& text) { } // namespace namespace net { - + FtpDirectoryListingBuffer::FtpDirectoryListingBuffer() : current_parser_(NULL) { parsers_.insert(new FtpLsDirectoryListingParser()); + parsers_.insert(new FtpVmsDirectoryListingParser()); } FtpDirectoryListingBuffer::~FtpDirectoryListingBuffer() { @@ -66,11 +67,11 @@ int FtpDirectoryListingBuffer::ProcessRemainingData() { return ParseLines(); } - + bool FtpDirectoryListingBuffer::EntryAvailable() const { return (current_parser_ ? current_parser_->EntryAvailable() : false); } - + FtpDirectoryListingEntry FtpDirectoryListingBuffer::PopEntry() { DCHECK(EntryAvailable()); return current_parser_->PopEntry(); @@ -86,19 +87,24 @@ bool FtpDirectoryListingBuffer::ConvertToDetectedEncoding( int FtpDirectoryListingBuffer::ExtractFullLinesFromBuffer() { if (encoding_.empty()) encoding_ = DetectEncoding(buffer_); - + int cut_pos = 0; + // TODO(phajdan.jr): This code accepts all endlines matching \r*\n. Should it + // be more strict, or enforce consistent line endings? for (size_t i = 0; i < buffer_.length(); ++i) { - if (i >= 1 && buffer_[i - 1] == '\r' && buffer_[i] == '\n') { - std::string line(buffer_.substr(cut_pos, i - cut_pos - 1)); - cut_pos = i + 1; - string16 line_converted; - if (!ConvertToDetectedEncoding(line, &line_converted)) { - buffer_.erase(0, cut_pos); - return ERR_ENCODING_CONVERSION_FAILED; - } - lines_.push_back(line_converted); + if (buffer_[i] != '\n') + continue; + int line_length = i - cut_pos; + if (i >= 1 && buffer_[i - 1] == '\r') + line_length--; + std::string line(buffer_.substr(cut_pos, line_length)); + cut_pos = i + 1; + string16 line_converted; + if (!ConvertToDetectedEncoding(line, &line_converted)) { + buffer_.erase(0, cut_pos); + return ERR_ENCODING_CONVERSION_FAILED; } + lines_.push_back(line_converted); } buffer_.erase(0, cut_pos); return OK; @@ -127,7 +133,7 @@ int FtpDirectoryListingBuffer::ParseLines() { current_parser_ = *parsers_.begin(); } } - + return OK; } diff --git a/net/ftp/ftp_directory_listing_buffer_unittest.cc b/net/ftp/ftp_directory_listing_buffer_unittest.cc index f72a151..f8f1a51 100644 --- a/net/ftp/ftp_directory_listing_buffer_unittest.cc +++ b/net/ftp/ftp_directory_listing_buffer_unittest.cc @@ -18,45 +18,49 @@ TEST(FtpDirectoryListingBufferTest, Parse) { const char* test_files[] = { "dir-listing-ls-1", "dir-listing-ls-2", + "dir-listing-vms-1", + "dir-listing-vms-2", + "dir-listing-vms-3", + "dir-listing-vms-4", }; - + FilePath test_dir; PathService::Get(base::DIR_SOURCE_ROOT, &test_dir); test_dir = test_dir.AppendASCII("net"); test_dir = test_dir.AppendASCII("data"); test_dir = test_dir.AppendASCII("ftp"); - + for (size_t i = 0; i < arraysize(test_files); i++) { SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, test_files[i])); - + net::FtpDirectoryListingBuffer buffer; - + std::string test_listing; EXPECT_TRUE(file_util::ReadFileToString(test_dir.AppendASCII(test_files[i]), &test_listing)); - + EXPECT_EQ(net::OK, buffer.ConsumeData(test_listing.data(), test_listing.length())); EXPECT_EQ(net::OK, buffer.ProcessRemainingData()); - + std::string expected_listing; ASSERT_TRUE(file_util::ReadFileToString( test_dir.AppendASCII(std::string(test_files[i]) + ".expected"), &expected_listing)); - + std::vector<std::string> lines; StringTokenizer tokenizer(expected_listing, "\r\n"); while (tokenizer.GetNext()) lines.push_back(tokenizer.token()); ASSERT_EQ(0U, lines.size() % 8); - + for (size_t i = 0; i < lines.size() / 8; i++) { std::string type(lines[8 * i]); std::string name(lines[8 * i + 1]); int64 size = StringToInt64(lines[8 * i + 2]); - + SCOPED_TRACE(StringPrintf("Filename: %s", name.c_str())); - + int year; if (lines[8 * i + 3] == "current") { base::Time::Exploded now_exploded; @@ -69,10 +73,10 @@ TEST(FtpDirectoryListingBufferTest, Parse) { int day_of_month = StringToInt(lines[8 * i + 5]); int hour = StringToInt(lines[8 * i + 6]); int minute = StringToInt(lines[8 * i + 7]); - + ASSERT_TRUE(buffer.EntryAvailable()); net::FtpDirectoryListingEntry entry = buffer.PopEntry(); - + if (type == "d") { EXPECT_EQ(net::FtpDirectoryListingEntry::DIRECTORY, entry.type); } else if (type == "-") { @@ -82,10 +86,10 @@ TEST(FtpDirectoryListingBufferTest, Parse) { } else { ADD_FAILURE() << "invalid gold test data: " << type; } - + EXPECT_EQ(UTF8ToUTF16(name), entry.name); EXPECT_EQ(size, entry.size); - + base::Time::Exploded time_exploded; entry.last_modified.LocalExplode(&time_exploded); EXPECT_EQ(year, time_exploded.year); diff --git a/net/ftp/ftp_directory_listing_parsers.cc b/net/ftp/ftp_directory_listing_parsers.cc index 60cef80..5f160e3 100644 --- a/net/ftp/ftp_directory_listing_parsers.cc +++ b/net/ftp/ftp_directory_listing_parsers.cc @@ -7,11 +7,11 @@ #include "base/string_util.h" namespace { - + bool LooksLikeUnixPermission(const string16& text) { if (text.length() != 3) return false; - + return ((text[0] == 'r' || text[0] == '-') && (text[1] == 'w' || text[1] == '-') && (text[2] == 'x' || text[2] == 's' || text[2] == 'S' || @@ -21,80 +21,221 @@ bool LooksLikeUnixPermission(const string16& text) { bool LooksLikeUnixPermissionsListing(const string16& text) { if (text.length() != 10) return false; - + if (text[0] != 'b' && text[0] != 'c' && text[0] != 'd' && text[0] != 'l' && text[0] != 'p' && text[0] != 's' && text[0] != '-') return false; - + return (LooksLikeUnixPermission(text.substr(1, 3)) && LooksLikeUnixPermission(text.substr(4, 3)) && LooksLikeUnixPermission(text.substr(7, 3))); } - -bool IsStringNonNegativeNumber(const string16& text) { + +bool IsStringNonNegativeInteger(const string16& text) { int number; if (!StringToInt(text, &number)) return false; - + return number >= 0; } - + bool ThreeLetterMonthToNumber(const string16& text, int* number) { const static char* months[] = { "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec" }; - + for (size_t i = 0; i < arraysize(months); i++) { if (LowerCaseEqualsASCII(text, months[i])) { *number = i + 1; return true; } } - + return false; } - + bool UnixDateListingToTime(const std::vector<string16>& columns, base::Time* time) { DCHECK_EQ(9U, columns.size()); - + base::Time::Exploded time_exploded = { 0 }; - + if (!ThreeLetterMonthToNumber(columns[5], &time_exploded.month)) return false; - + if (!StringToInt(columns[6], &time_exploded.day_of_month)) return false; - + if (!StringToInt(columns[7], &time_exploded.year)) { // Maybe it's time. Does it look like time (MM:HH)? if (columns[7].length() != 5 || columns[7][2] != ':') return false; - + if (!StringToInt(columns[7].substr(0, 2), &time_exploded.hour)) return false; - + if (!StringToInt(columns[7].substr(3, 2), &time_exploded.minute)) return false; - + // Use current year. base::Time::Exploded now_exploded; base::Time::Now().LocalExplode(&now_exploded); time_exploded.year = now_exploded.year; } - + + // We don't know the time zone of the server, so just use local time. + *time = base::Time::FromLocalExploded(time_exploded); + return true; +} + +// Converts the filename component in listing to the filename we can display. +// Returns true on success. +bool ParseVmsFilename(const string16& raw_filename, string16* parsed_filename, + bool* is_directory) { + // On VMS, the files and directories are versioned. The version number is + // separated from the file name by a semicolon. Example: ANNOUNCE.TXT;2. + std::vector<string16> listing_parts; + SplitString(raw_filename, ';', &listing_parts); + if (listing_parts.size() != 2) + return false; + if (!IsStringNonNegativeInteger(listing_parts[1])) + return false; + + // Even directories have extensions in the listings. Don't display extensions + // for directories; it's awkward for non-VMS users. Also, VMS is + // case-insensitive, but generally uses uppercase characters. This may look + // awkward, so we convert them to lower case. + std::vector<string16> filename_parts; + SplitString(listing_parts[0], '.', &filename_parts); + if (filename_parts.size() != 2) + return false; + if (filename_parts[1] == ASCIIToUTF16("DIR")) { + *parsed_filename = StringToLowerASCII(filename_parts[0]); + *is_directory = true; + } else { + *parsed_filename = StringToLowerASCII(listing_parts[0]); + *is_directory = false; + } + return true; +} + +bool ParseVmsFilesize(const string16& input, int64* size) { + // VMS's directory listing gives us file size in blocks. We assume that + // the block size is 512 bytes. It doesn't give accurate file size, but is the + // best information we have. + const int kBlockSize = 512; + + if (StringToInt64(input, size)) { + *size *= kBlockSize; + return true; + } + + std::vector<string16> parts; + SplitString(input, '/', &parts); + if (parts.size() != 2) + return false; + + int64 blocks_used, blocks_allocated; + if (!StringToInt64(parts[0], &blocks_used)) + return false; + if (!StringToInt64(parts[1], &blocks_allocated)) + return false; + if (blocks_used > blocks_allocated) + return false; + + *size = blocks_used * kBlockSize; + return true; +} + +bool LooksLikeVmsFileProtectionListingPart(const string16& input) { + if (input.length() > 4) + return false; + + // On VMS there are four different permission bits: Read, Write, Execute, + // and Delete. They appear in that order in the permission listing. + std::string pattern("RWED"); + string16 match(input); + while (!match.empty() && !pattern.empty()) { + if (match[0] == pattern[0]) + match = match.substr(1); + pattern = pattern.substr(1); + } + return match.empty(); +} + +bool LooksLikeVmsFileProtectionListing(const string16& input) { + if (input.length() < 2) + return false; + if (input[0] != '(' || input[input.length() - 1] != ')') + return false; + + // We expect four parts of the file protection listing: for System, Owner, + // Group, and World. + std::vector<string16> parts; + SplitString(input.substr(1, input.length() - 2), ',', &parts); + if (parts.size() != 4) + return false; + + return LooksLikeVmsFileProtectionListingPart(parts[0]) && + LooksLikeVmsFileProtectionListingPart(parts[1]) && + LooksLikeVmsFileProtectionListingPart(parts[2]) && + LooksLikeVmsFileProtectionListingPart(parts[3]); +} + +bool LooksLikeVmsUserIdentificationCode(const string16& input) { + if (input.length() < 2) + return false; + return input[0] == '[' && input[input.length() - 1] == ']'; +} + +bool VmsDateListingToTime(const std::vector<string16>& columns, + base::Time* time) { + DCHECK_EQ(3U, columns.size()); + + base::Time::Exploded time_exploded = { 0 }; + + // Date should be in format DD-MMM-YYYY. + std::vector<string16> date_parts; + SplitString(columns[1], '-', &date_parts); + if (date_parts.size() != 3) + return false; + if (!StringToInt(date_parts[0], &time_exploded.day_of_month)) + return false; + if (!ThreeLetterMonthToNumber(date_parts[1], &time_exploded.month)) + return false; + if (!StringToInt(date_parts[2], &time_exploded.year)) + return false; + + // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the + // last type first. Do not parse the seconds, they will be ignored anyway. + string16 time_column(columns[2]); + if (time_column.length() == 11 && time_column[8] == '.') + time_column = time_column.substr(0, 8); + if (time_column.length() == 8 && time_column[5] == ':') + time_column = time_column.substr(0, 5); + if (time_column.length() != 5) + return false; + std::vector<string16> time_parts; + SplitString(time_column, ':', &time_parts); + if (time_parts.size() != 2) + return false; + if (!StringToInt(time_parts[0], &time_exploded.hour)) + return false; + if (!StringToInt(time_parts[1], &time_exploded.minute)) + return false; + // We don't know the time zone of the server, so just use local time. *time = base::Time::FromLocalExploded(time_exploded); return true; } - + } // namespace namespace net { FtpDirectoryListingParser::~FtpDirectoryListingParser() { } - + FtpLsDirectoryListingParser::FtpLsDirectoryListingParser() { } @@ -105,17 +246,17 @@ bool FtpLsDirectoryListingParser::ConsumeLine(const string16& line) { // Check if it is a symlink. if (columns[9] != ASCIIToUTF16("->")) return false; - + // Drop the symlink target from columns, we don't use it. columns.resize(9); } - + if (columns.size() != 9) return false; - + if (!LooksLikeUnixPermissionsListing(columns[0])) return false; - + FtpDirectoryListingEntry entry; if (columns[0][0] == 'l') { entry.type = FtpDirectoryListingEntry::SYMLINK; @@ -124,34 +265,162 @@ bool FtpLsDirectoryListingParser::ConsumeLine(const string16& line) { } else { entry.type = FtpDirectoryListingEntry::FILE; } - - if (!IsStringNonNegativeNumber(columns[1])) + + if (!IsStringNonNegativeInteger(columns[1])) return false; - + if (!StringToInt64(columns[4], &entry.size)) return false; if (entry.size < 0) return false; if (entry.type != FtpDirectoryListingEntry::FILE) entry.size = -1; - + if (!UnixDateListingToTime(columns, &entry.last_modified)) return false; - + entry.name = columns[8]; - + entries_.push(entry); return true; } - + bool FtpLsDirectoryListingParser::EntryAvailable() const { return !entries_.empty(); } - + FtpDirectoryListingEntry FtpLsDirectoryListingParser::PopEntry() { FtpDirectoryListingEntry entry = entries_.front(); entries_.pop(); return entry; } +FtpVmsDirectoryListingParser::FtpVmsDirectoryListingParser() + : state_(STATE_INITIAL), + last_is_directory_(false) { +} + +bool FtpVmsDirectoryListingParser::ConsumeLine(const string16& line) { + switch (state_) { + case STATE_INITIAL: + DCHECK(last_filename_.empty()); + if (line.empty()) + return true; + if (StartsWith(line, ASCIIToUTF16("Total of "), true)) { + state_ = STATE_END; + return true; + } + // We assume that the first non-empty line is the listing header. It often + // starts with "Directory ", but not always. + state_ = STATE_RECEIVED_HEADER; + return true; + case STATE_RECEIVED_HEADER: + DCHECK(last_filename_.empty()); + if (line.empty()) + return true; + state_ = STATE_ENTRIES; + return ConsumeEntryLine(line); + case STATE_ENTRIES: + if (line.empty()) { + if (!last_filename_.empty()) + return false; + state_ = STATE_RECEIVED_LAST_ENTRY; + return true; + } + return ConsumeEntryLine(line); + case STATE_RECEIVED_LAST_ENTRY: + DCHECK(last_filename_.empty()); + if (line.empty()) + return true; + if (!StartsWith(line, ASCIIToUTF16("Total of "), true)) + return false; + state_ = STATE_END; + return true; + case STATE_END: + DCHECK(last_filename_.empty()); + return false; + default: + NOTREACHED(); + return false; + } +} + +bool FtpVmsDirectoryListingParser::EntryAvailable() const { + return !entries_.empty(); +} + +FtpDirectoryListingEntry FtpVmsDirectoryListingParser::PopEntry() { + FtpDirectoryListingEntry entry = entries_.front(); + entries_.pop(); + return entry; +} + +bool FtpVmsDirectoryListingParser::ConsumeEntryLine(const string16& line) { + std::vector<string16> columns; + SplitString(CollapseWhitespace(line, false), ' ', &columns); + + if (columns.size() == 1) { + if (!last_filename_.empty()) + return false; + return ParseVmsFilename(columns[0], &last_filename_, &last_is_directory_); + } + + // Recognize listing entries which generate "access denied" message even when + // trying to list them. We don't display them in the final listing. + static const char* kAccessDeniedMessages[] = { + "%RMS-E-PRV", + "privilege", + }; + for (size_t i = 0; i < arraysize(kAccessDeniedMessages); i++) { + if (line.find(ASCIIToUTF16(kAccessDeniedMessages[i])) != string16::npos) { + last_filename_.clear(); + last_is_directory_ = false; + return true; + } + } + + string16 filename; + bool is_directory = false; + if (last_filename_.empty()) { + if (!ParseVmsFilename(columns[0], &filename, &is_directory)) + return false; + columns.erase(columns.begin()); + } else { + filename = last_filename_; + is_directory = last_is_directory_; + last_filename_.clear(); + last_is_directory_ = false; + } + + if (columns.size() > 5) + return false; + + if (columns.size() == 5) { + if (!LooksLikeVmsFileProtectionListing(columns[4])) + return false; + if (!LooksLikeVmsUserIdentificationCode(columns[3])) + return false; + columns.resize(3); + } + + if (columns.size() != 3) + return false; + + FtpDirectoryListingEntry entry; + entry.name = filename; + entry.type = is_directory ? FtpDirectoryListingEntry::DIRECTORY + : FtpDirectoryListingEntry::FILE; + if (!ParseVmsFilesize(columns[0], &entry.size)) + return false; + if (entry.size < 0) + return false; + if (entry.type != FtpDirectoryListingEntry::FILE) + entry.size = -1; + if (!VmsDateListingToTime(columns, &entry.last_modified)) + return false; + + entries_.push(entry); + return true; +} + } // namespace net diff --git a/net/ftp/ftp_directory_listing_parsers.h b/net/ftp/ftp_directory_listing_parsers.h index 2f402e7..230c641 100644 --- a/net/ftp/ftp_directory_listing_parsers.h +++ b/net/ftp/ftp_directory_listing_parsers.h @@ -13,18 +13,18 @@ #include "net/ftp/ftp_server_type_histograms.h" namespace net { - + struct FtpDirectoryListingEntry { enum Type { FILE, DIRECTORY, SYMLINK, }; - + Type type; string16 name; int64 size; // File size, in bytes. -1 if not applicable. - + // Last modified time, in local time zone. base::Time last_modified; }; @@ -32,33 +32,78 @@ struct FtpDirectoryListingEntry { class FtpDirectoryListingParser { public: virtual ~FtpDirectoryListingParser(); - + // Adds |line| to the internal parsing buffer. Returns true on success. virtual bool ConsumeLine(const string16& line) = 0; - + // Returns true if there is at least one FtpDirectoryListingEntry available. virtual bool EntryAvailable() const = 0; - + // Returns the next entry. It is an error to call this function unless // EntryAvailable returns true. virtual FtpDirectoryListingEntry PopEntry() = 0; }; - + +// Parser for "ls -l"-style directory listing. class FtpLsDirectoryListingParser : public FtpDirectoryListingParser { public: FtpLsDirectoryListingParser(); - + // FtpDirectoryListingParser methods: virtual bool ConsumeLine(const string16& line); virtual bool EntryAvailable() const; virtual FtpDirectoryListingEntry PopEntry(); - + private: std::queue<FtpDirectoryListingEntry> entries_; - + DISALLOW_COPY_AND_ASSIGN(FtpLsDirectoryListingParser); }; - + +// Parser for VMS-style directory listing (including variants). +class FtpVmsDirectoryListingParser : public FtpDirectoryListingParser { + public: + FtpVmsDirectoryListingParser(); + + // FtpDirectoryListingParser methods: + virtual bool ConsumeLine(const string16& line); + virtual bool EntryAvailable() const; + virtual FtpDirectoryListingEntry PopEntry(); + + private: + // Consumes listing line which is expected to be a directory listing entry + // (and not a comment etc). Returns true on success. + bool ConsumeEntryLine(const string16& line); + + enum State { + STATE_INITIAL, + + // Indicates that we have received the header, like this: + // Directory SYS$SYSDEVICE:[ANONYMOUS] + STATE_RECEIVED_HEADER, + + // Indicates that we have received the first listing entry, like this: + // MADGOAT.DIR;1 2 9-MAY-2001 22:23:44.85 + STATE_ENTRIES, + + // Indicates that we have received the last listing entry. + STATE_RECEIVED_LAST_ENTRY, + + // Indicates that we have successfully received all parts of the listing. + STATE_END, + } state_; + + // VMS can use two physical lines if the filename is long. The first line will + // contain the filename, and the second line everything else. Store the + // filename until we receive the next line. + string16 last_filename_; + bool last_is_directory_; + + std::queue<FtpDirectoryListingEntry> entries_; + + DISALLOW_COPY_AND_ASSIGN(FtpVmsDirectoryListingParser); +}; + } // namespace net #endif // NET_FTP_FTP_DIRECTORY_LISTING_PARSERS_H_ diff --git a/net/ftp/ftp_directory_listing_parsers_unittest.cc b/net/ftp/ftp_directory_listing_parsers_unittest.cc index b7d9499..cc05f36 100644 --- a/net/ftp/ftp_directory_listing_parsers_unittest.cc +++ b/net/ftp/ftp_directory_listing_parsers_unittest.cc @@ -8,7 +8,7 @@ #include "testing/gtest/include/gtest/gtest.h" namespace { - + struct SingleLineTestData { const char* input; net::FtpDirectoryListingEntry::Type type; @@ -25,7 +25,7 @@ class FtpDirectoryListingParsersTest : public testing::Test { protected: FtpDirectoryListingParsersTest() { } - + void RunSingleLineTestCase(net::FtpDirectoryListingParser* parser, const SingleLineTestData& test_case) { ASSERT_TRUE(parser->ConsumeLine(UTF8ToUTF16(test_case.input))); @@ -34,7 +34,7 @@ class FtpDirectoryListingParsersTest : public testing::Test { EXPECT_EQ(test_case.type, entry.type); EXPECT_EQ(UTF8ToUTF16(test_case.filename), entry.name); EXPECT_EQ(test_case.size, entry.size); - + base::Time::Exploded time_exploded; entry.last_modified.LocalExplode(&time_exploded); EXPECT_EQ(test_case.year, time_exploded.year); @@ -45,7 +45,7 @@ class FtpDirectoryListingParsersTest : public testing::Test { EXPECT_EQ(0, time_exploded.second); EXPECT_EQ(0, time_exploded.millisecond); } - + private: DISALLOW_COPY_AND_ASSIGN(FtpDirectoryListingParsersTest); }; @@ -53,7 +53,7 @@ class FtpDirectoryListingParsersTest : public testing::Test { TEST_F(FtpDirectoryListingParsersTest, Ls) { base::Time::Exploded now_exploded; base::Time::Now().LocalExplode(&now_exploded); - + const struct SingleLineTestData good_cases[] = { { "-rw-r--r-- 1 ftp ftp 528 Nov 01 2007 README", net::FtpDirectoryListingEntry::FILE, "README", 528, @@ -70,11 +70,11 @@ TEST_F(FtpDirectoryListingParsersTest, Ls) { }; for (size_t i = 0; i < arraysize(good_cases); i++) { SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, good_cases[i].input)); - + net::FtpLsDirectoryListingParser parser; RunSingleLineTestCase(&parser, good_cases[i]); } - + const char* bad_cases[] = { "", "garbage", @@ -92,4 +92,106 @@ TEST_F(FtpDirectoryListingParsersTest, Ls) { } } +TEST_F(FtpDirectoryListingParsersTest, Vms) { + const struct SingleLineTestData good_cases[] = { + { "README.TXT;4 2 18-APR-2000 10:40:39.90", + net::FtpDirectoryListingEntry::FILE, "readme.txt", 1024, + 2000, 4, 18, 10, 40 }, + { ".WELCOME;1 2 13-FEB-2002 23:32:40.47", + net::FtpDirectoryListingEntry::FILE, ".welcome", 1024, + 2002, 2, 13, 23, 32 }, + { "FILE.;1 2 13-FEB-2002 23:32:40.47", + net::FtpDirectoryListingEntry::FILE, "file.", 1024, + 2002, 2, 13, 23, 32 }, + { "EXAMPLE.TXT;1 1 4-NOV-2009 06:02 [JOHNDOE] (RWED,RWED,,)", + net::FtpDirectoryListingEntry::FILE, "example.txt", 512, + 2009, 11, 4, 6, 2 }, + { "ANNOUNCE.TXT;2 1/16 12-MAR-2005 08:44:57 [SYSTEM] (RWED,RWED,RE,RE)", + net::FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "TEST.DIR;1 1 4-MAR-1999 22:14:34 [UCX$NOBO,ANONYMOUS] (RWE,RWE,RWE,RWE)", + net::FtpDirectoryListingEntry::DIRECTORY, "test", -1, + 1999, 3, 4, 22, 14 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (,,,)", + net::FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (R,RW,RWD,RE)", + net::FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + { "ANNOUNCE.TXT;2 1 12-MAR-2005 08:44:57 [X] (ED,RED,WD,WED)", + net::FtpDirectoryListingEntry::FILE, "announce.txt", 512, + 2005, 3, 12, 8, 44 }, + }; + for (size_t i = 0; i < arraysize(good_cases); i++) { + SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, good_cases[i].input)); + + net::FtpVmsDirectoryListingParser parser; + ASSERT_TRUE( + parser.ConsumeLine(ASCIIToUTF16("Directory ANONYMOUS_ROOT:[000000]"))); + RunSingleLineTestCase(&parser, good_cases[i]); + } + + const char* bad_cases[] = { + "Directory ROOT|garbage", + + // Missing file version number. + "Directory ROOT|README.TXT 2 18-APR-2000 10:40:39", + + // Missing extension. + "Directory ROOT|README;1 2 18-APR-2000 10:40:39", + + // Malformed file size. + "Directory ROOT|README.TXT;1 garbage 18-APR-2000 10:40:39", + "Directory ROOT|README.TXT;1 -2 18-APR-2000 10:40:39", + + // Malformed date. + "Directory ROOT|README.TXT;1 2 APR-2000 10:40:39", + "Directory ROOT|README.TXT;1 2 -18-APR-2000 10:40:39", + "Directory ROOT|README.TXT;1 2 18-APR 10:40:39", + "Directory ROOT|README.TXT;1 2 18-APR-2000 10", + "Directory ROOT|README.TXT;1 2 18-APR-2000 10:40.25", + "Directory ROOT|README.TXT;1 2 18-APR-2000 10:40.25.25", + + // Empty line inside the listing. + "Directory ROOT|README.TXT;1 2 18-APR-2000 10:40:42" + "||README.TXT;1 2 18-APR-2000 10:40:42", + + // Data after footer. + "Directory ROOT|README.TXT;4 2 18-APR-2000 10:40:39" + "||Total of 1 file|", + "Directory ROOT|README.TXT;4 2 18-APR-2000 10:40:39" + "||Total of 1 file|garbage", + "Directory ROOT|README.TXT;4 2 18-APR-2000 10:40:39" + "||Total of 1 file|Total of 1 file", + + // Malformed security information. + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 (RWED,RWED,RE,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [SYSTEM]", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 (SYSTEM) (RWED,RWED,RE,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [SYSTEM] [RWED,RWED,RE,RE]", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWED,RE,RE,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWEDRWED,RE,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,DEWR,RE,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RWED,Q,RE)", + "Directory ROOT|X.TXT;2 1 12-MAR-2005 08:44:57 [X] (RWED,RRWWEEDD,RE,RE)", + }; + for (size_t i = 0; i < arraysize(bad_cases); i++) { + SCOPED_TRACE(StringPrintf("Test[%d]: %s", i, bad_cases[i])); + + std::vector<std::string> lines; + SplitString(bad_cases[i], '|', &lines); + net::FtpVmsDirectoryListingParser parser; + bool failed = false; + for (std::vector<std::string>::const_iterator i = lines.begin(); + i != lines.end(); ++i) { + if (!parser.ConsumeLine(UTF8ToUTF16(*i))) { + failed = true; + break; + } + } + EXPECT_TRUE(failed); + } +} + } // namespace |