| // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "net/ftp/ftp_directory_listing_parser_ls.h" | |
| #include <vector> | |
| #include "base/strings/string_number_conversions.h" | |
| #include "base/strings/string_split.h" | |
| #include "base/strings/string_util.h" | |
| #include "base/strings/utf_string_conversions.h" | |
| #include "base/time/time.h" | |
| #include "net/ftp/ftp_directory_listing_parser.h" | |
| #include "net/ftp/ftp_util.h" | |
| namespace net { | |
| namespace { | |
| bool TwoColumnDateListingToTime(const base::string16& date, | |
| const base::string16& time, | |
| base::Time* result) { | |
| base::Time::Exploded time_exploded = { 0 }; | |
| // Date should be in format YYYY-MM-DD. | |
| std::vector<base::string16> date_parts = | |
| base::SplitString(date, base::ASCIIToUTF16("-"), base::TRIM_WHITESPACE, | |
| base::SPLIT_WANT_ALL); | |
| if (date_parts.size() != 3) | |
| return false; | |
| if (!base::StringToInt(date_parts[0], &time_exploded.year)) | |
| return false; | |
| if (!base::StringToInt(date_parts[1], &time_exploded.month)) | |
| return false; | |
| if (!base::StringToInt(date_parts[2], &time_exploded.day_of_month)) | |
| return false; | |
| // Time should be in format HH:MM | |
| if (time.length() != 5) | |
| return false; | |
| std::vector<base::string16> time_parts = | |
| base::SplitString(time, base::ASCIIToUTF16(":"), base::TRIM_WHITESPACE, | |
| base::SPLIT_WANT_ALL); | |
| if (time_parts.size() != 2) | |
| return false; | |
| if (!base::StringToInt(time_parts[0], &time_exploded.hour)) | |
| return false; | |
| if (!base::StringToInt(time_parts[1], &time_exploded.minute)) | |
| return false; | |
| if (!time_exploded.HasValidValues()) | |
| return false; | |
| // We don't know the time zone of the server, so just use UTC. | |
| return base::Time::FromUTCExploded(time_exploded, result); | |
| } | |
| // Returns the column index of the end of the date listing and detected | |
| // last modification time. | |
| bool DetectColumnOffsetSizeAndModificationTime( | |
| const std::vector<base::string16>& columns, | |
| const base::Time& current_time, | |
| size_t* offset, | |
| base::string16* size, | |
| base::Time* modification_time) { | |
| // The column offset can be arbitrarily large if some fields | |
| // like owner or group name contain spaces. Try offsets from left to right | |
| // and use the first one that matches a date listing. | |
| // | |
| // Here is how a listing line should look like. A star ("*") indicates | |
| // a required field: | |
| // | |
| // * 1. permission listing | |
| // 2. number of links (optional) | |
| // * 3. owner name (may contain spaces) | |
| // 4. group name (optional, may contain spaces) | |
| // * 5. size in bytes | |
| // * 6. month | |
| // * 7. day of month | |
| // * 8. year or time <-- column_offset will be the index of this column | |
| // 9. file name (optional, may contain spaces) | |
| for (size_t i = 5U; i < columns.size(); i++) { | |
| if (FtpUtil::LsDateListingToTime(columns[i - 2], columns[i - 1], columns[i], | |
| current_time, modification_time)) { | |
| *size = columns[i - 3]; | |
| *offset = i; | |
| return true; | |
| } | |
| } | |
| // Some FTP listings have swapped the "month" and "day of month" columns | |
| // (for example Russian listings). We try to recognize them only after making | |
| // sure no column offset works above (this is a more strict way). | |
| for (size_t i = 5U; i < columns.size(); i++) { | |
| if (FtpUtil::LsDateListingToTime(columns[i - 1], columns[i - 2], columns[i], | |
| current_time, modification_time)) { | |
| *size = columns[i - 3]; | |
| *offset = i; | |
| return true; | |
| } | |
| } | |
| // Some FTP listings use a different date format. | |
| for (size_t i = 5U; i < columns.size(); i++) { | |
| if (TwoColumnDateListingToTime(columns[i - 1], | |
| columns[i], | |
| modification_time)) { | |
| *size = columns[i - 2]; | |
| *offset = i; | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| } // namespace | |
| bool ParseFtpDirectoryListingLs( | |
| const std::vector<base::string16>& lines, | |
| const base::Time& current_time, | |
| std::vector<FtpDirectoryListingEntry>* entries) { | |
| // True after we have received a "total n" listing header, where n is an | |
| // integer. Only one such header is allowed per listing. | |
| bool received_total_line = false; | |
| for (size_t i = 0; i < lines.size(); i++) { | |
| if (lines[i].empty()) | |
| continue; | |
| std::vector<base::string16> columns = base::SplitString( | |
| base::CollapseWhitespace(lines[i], false), base::ASCIIToUTF16(" "), | |
| base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); | |
| // Some FTP servers put a "total n" line at the beginning of the listing | |
| // (n is an integer). Allow such a line, but only once, and only if it's | |
| // the first non-empty line. Do not match the word exactly, because it may | |
| // be in different languages (at least English and German have been seen | |
| // in the field). | |
| if (columns.size() == 2 && !received_total_line) { | |
| received_total_line = true; | |
| // Some FTP servers incorrectly return a negative integer for "n". Since | |
| // this value is ignored anyway, just check any valid integer was | |
| // provided. | |
| int64_t total_number; | |
| if (!base::StringToInt64(columns[1], &total_number)) | |
| return false; | |
| continue; | |
| } | |
| FtpDirectoryListingEntry entry; | |
| size_t column_offset; | |
| base::string16 size; | |
| if (!DetectColumnOffsetSizeAndModificationTime(columns, | |
| current_time, | |
| &column_offset, | |
| &size, | |
| &entry.last_modified)) { | |
| // Some servers send a message in one of the first few lines. | |
| // All those messages have in common is the string ".:", | |
| // where "." means the current directory, and ":" separates it | |
| // from the rest of the message, which may be empty. | |
| if (lines[i].find(base::ASCIIToUTF16(".:")) != base::string16::npos) | |
| continue; | |
| return false; | |
| } | |
| // Do not check "validity" of the permission listing. It's quirky, | |
| // and some servers send garbage here while other parts of the line are OK. | |
| if (!columns[0].empty() && columns[0][0] == 'l') { | |
| entry.type = FtpDirectoryListingEntry::SYMLINK; | |
| } else if (!columns[0].empty() && columns[0][0] == 'd') { | |
| entry.type = FtpDirectoryListingEntry::DIRECTORY; | |
| } else { | |
| entry.type = FtpDirectoryListingEntry::FILE; | |
| } | |
| if (!base::StringToInt64(size, &entry.size)) { | |
| // Some FTP servers do not separate owning group name from file size, | |
| // like "group1234". We still want to display the file name for that | |
| // entry, but can't really get the size (What if the group is named | |
| // "group1", and the size is in fact 234? We can't distinguish between | |
| // that and "group" with size 1234). Use a dummy value for the size. | |
| entry.size = -1; | |
| } | |
| if (entry.size < 0) { | |
| // Some FTP servers have bugs that cause them to display the file size | |
| // as negative. They're most likely big files like DVD ISO images. | |
| // We still want to display them, so just say the real file size | |
| // is unknown. | |
| entry.size = -1; | |
| } | |
| if (entry.type != FtpDirectoryListingEntry::FILE) | |
| entry.size = -1; | |
| if (column_offset == columns.size() - 1) { | |
| // If the end of the date listing is the last column, there is no file | |
| // name. Some FTP servers send listing entries with empty names. | |
| // It's not obvious how to display such an entry, so we ignore them. | |
| // We don't want to make the parsing fail at this point though. | |
| // Other entries can still be useful. | |
| continue; | |
| } | |
| entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], | |
| column_offset + 1); | |
| if (entry.type == FtpDirectoryListingEntry::SYMLINK) { | |
| base::string16::size_type pos = | |
| entry.name.rfind(base::ASCIIToUTF16(" -> ")); | |
| // We don't require the " -> " to be present. Some FTP servers don't send | |
| // the symlink target, possibly for security reasons. | |
| if (pos != base::string16::npos) | |
| entry.name = entry.name.substr(0, pos); | |
| } | |
| entries->push_back(entry); | |
| } | |
| return true; | |
| } | |
| } // namespace net |