| // 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/string_number_conversions.h" |
| #include "base/string_split.h" |
| #include "base/string_util.h" |
| #include "base/time.h" |
| #include "base/utf_string_conversions.h" |
| #include "net/ftp/ftp_directory_listing_parser.h" |
| #include "net/ftp/ftp_util.h" |
| |
| namespace { |
| |
| bool LooksLikeUnixPermission(const string16& text) { |
| if (text.length() != 3) |
| return false; |
| |
| // Meaning of the flags: |
| // r - file is readable |
| // w - file is writable |
| // x - file is executable |
| // s or S - setuid/setgid bit set |
| // t or T - "sticky" bit set |
| return ((text[0] == 'r' || text[0] == '-') && |
| (text[1] == 'w' || text[1] == '-') && |
| (text[2] == 'x' || text[2] == 's' || text[2] == 'S' || |
| text[2] == 't' || text[2] == 'T' || text[2] == '-')); |
| } |
| |
| bool LooksLikeUnixPermissionsListing(const string16& text) { |
| if (text.length() < 7) |
| return false; |
| |
| // Do not check the first character (entry type). There are many weird |
| // servers that use special file types (for example Plan9 and append-only |
| // files). Fortunately, the rest of the permission listing is more consistent. |
| |
| // Do not check the rest of the string. Some servers fail to properly |
| // separate this column from the next column (number of links), resulting |
| // in additional characters at the end. Also, sometimes there is a "+" |
| // sign at the end indicating the file has ACLs set. |
| |
| // In fact, we don't even expect three "rwx" triplets of permission |
| // listing, as some FTP servers like Hylafax only send two. |
| return (LooksLikeUnixPermission(text.substr(1, 3)) && |
| LooksLikeUnixPermission(text.substr(4, 3))); |
| } |
| |
| // Returns the column index of the end of the date listing and detected |
| // last modification time. |
| bool DetectColumnOffsetAndModificationTime(const std::vector<string16>& columns, |
| const base::Time& current_time, |
| size_t* offset, |
| 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 (net::FtpUtil::LsDateListingToTime(columns[i - 2], |
| columns[i - 1], |
| columns[i], |
| current_time, |
| modification_time)) { |
| *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 (net::FtpUtil::LsDateListingToTime(columns[i - 1], |
| columns[i - 2], |
| columns[i], |
| current_time, |
| modification_time)) { |
| *offset = i; |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| } // namespace |
| |
| namespace net { |
| |
| bool ParseFtpDirectoryListingLs( |
| const std::vector<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<string16> columns; |
| base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns); |
| |
| // 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; |
| |
| int total_number; |
| if (!base::StringToInt(columns[1], &total_number)) |
| return false; |
| if (total_number < 0) |
| return false; |
| |
| continue; |
| } |
| |
| FtpDirectoryListingEntry entry; |
| |
| size_t column_offset; |
| if (!DetectColumnOffsetAndModificationTime(columns, |
| current_time, |
| &column_offset, |
| &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(ASCIIToUTF16(".:")) != string16::npos) |
| continue; |
| |
| return false; |
| } |
| |
| if (!LooksLikeUnixPermissionsListing(columns[0])) |
| return false; |
| if (columns[0][0] == 'l') { |
| entry.type = FtpDirectoryListingEntry::SYMLINK; |
| } else if (columns[0][0] == 'd') { |
| entry.type = FtpDirectoryListingEntry::DIRECTORY; |
| } else { |
| entry.type = FtpDirectoryListingEntry::FILE; |
| } |
| |
| if (!base::StringToInt64(columns[column_offset - 3], &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. |
| // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes. |
| entry.size = 0; |
| } |
| 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) { |
| string16::size_type pos = entry.name.rfind(ASCIIToUTF16(" -> ")); |
| |
| // We don't require the " -> " to be present. Some FTP servers don't send |
| // the symlink target, possibly for security reasons. |
| if (pos != string16::npos) |
| entry.name = entry.name.substr(0, pos); |
| } |
| |
| entries->push_back(entry); |
| } |
| |
| return true; |
| } |
| |
| } // namespace net |