Home | History | Annotate | Download | only in ftp
      1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #include "net/ftp/ftp_directory_listing_parser_ls.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/string_number_conversions.h"
     10 #include "base/string_split.h"
     11 #include "base/string_util.h"
     12 #include "base/time.h"
     13 #include "base/utf_string_conversions.h"
     14 #include "net/ftp/ftp_directory_listing_parser.h"
     15 #include "net/ftp/ftp_util.h"
     16 
     17 namespace {
     18 
     19 bool LooksLikeUnixPermission(const string16& text) {
     20   if (text.length() != 3)
     21     return false;
     22 
     23   // Meaning of the flags:
     24   // r - file is readable
     25   // w - file is writable
     26   // x - file is executable
     27   // s or S - setuid/setgid bit set
     28   // t or T - "sticky" bit set
     29   return ((text[0] == 'r' || text[0] == '-') &&
     30           (text[1] == 'w' || text[1] == '-') &&
     31           (text[2] == 'x' || text[2] == 's' || text[2] == 'S' ||
     32            text[2] == 't' || text[2] == 'T' || text[2] == '-'));
     33 }
     34 
     35 bool LooksLikeUnixPermissionsListing(const string16& text) {
     36   if (text.length() < 10)
     37     return false;
     38 
     39   if (text[0] != 'b' && text[0] != 'c' && text[0] != 'd' &&
     40       text[0] != 'l' && text[0] != 'p' && text[0] != 's' &&
     41       text[0] != '-')
     42     return false;
     43 
     44   // Do not check the rest of the string. Some servers fail to properly
     45   // separate this column from the next column (number of links), resulting
     46   // in additional characters at the end. Also, sometimes there is a "+"
     47   // sign at the end indicating the file has ACLs set.
     48   return (LooksLikeUnixPermission(text.substr(1, 3)) &&
     49           LooksLikeUnixPermission(text.substr(4, 3)) &&
     50           LooksLikeUnixPermission(text.substr(7, 3)));
     51 }
     52 
     53 bool LooksLikePermissionDeniedError(const string16& text) {
     54   // Try to recognize a three-part colon-separated error message:
     55   //
     56   //   1. ftpd server name
     57   //   2. directory name (often just ".")
     58   //   3. message text (usually "Permission denied")
     59   std::vector<string16> parts;
     60   base::SplitString(CollapseWhitespace(text, false), ':', &parts);
     61 
     62   if (parts.size() != 3)
     63     return false;
     64 
     65   return parts[2] == ASCIIToUTF16("Permission denied");
     66 }
     67 
     68 // Returns the column index of the end of the date listing and detected
     69 // last modification time.
     70 bool DetectColumnOffsetAndModificationTime(const std::vector<string16>& columns,
     71                                            const base::Time& current_time,
     72                                            size_t* offset,
     73                                            base::Time* modification_time) {
     74   // The column offset can be arbitrarily large if some fields
     75   // like owner or group name contain spaces. Try offsets from left to right
     76   // and use the first one that matches a date listing.
     77   //
     78   // Here is how a listing line should look like. A star ("*") indicates
     79   // a required field:
     80   //
     81   //  * 1. permission listing
     82   //    2. number of links (optional)
     83   //  * 3. owner name (may contain spaces)
     84   //    4. group name (optional, may contain spaces)
     85   //  * 5. size in bytes
     86   //  * 6. month
     87   //  * 7. day of month
     88   //  * 8. year or time <-- column_offset will be the index of this column
     89   //    9. file name (optional, may contain spaces)
     90   for (size_t i = 5U; i < columns.size(); i++) {
     91     if (net::FtpUtil::LsDateListingToTime(columns[i - 2],
     92                                           columns[i - 1],
     93                                           columns[i],
     94                                           current_time,
     95                                           modification_time)) {
     96       *offset = i;
     97       return true;
     98     }
     99   }
    100 
    101   // Some FTP listings have swapped the "month" and "day of month" columns
    102   // (for example Russian listings). We try to recognize them only after making
    103   // sure no column offset works above (this is a more strict way).
    104   for (size_t i = 5U; i < columns.size(); i++) {
    105     if (net::FtpUtil::LsDateListingToTime(columns[i - 1],
    106                                           columns[i - 2],
    107                                           columns[i],
    108                                           current_time,
    109                                           modification_time)) {
    110       *offset = i;
    111       return true;
    112     }
    113   }
    114 
    115   return false;
    116 }
    117 
    118 }  // namespace
    119 
    120 namespace net {
    121 
    122 bool ParseFtpDirectoryListingLs(
    123     const std::vector<string16>& lines,
    124     const base::Time& current_time,
    125     std::vector<FtpDirectoryListingEntry>* entries) {
    126   // True after we have received a "total n" listing header, where n is an
    127   // integer. Only one such header is allowed per listing.
    128   bool received_total_line = false;
    129 
    130   for (size_t i = 0; i < lines.size(); i++) {
    131     if (lines[i].empty())
    132       continue;
    133 
    134     std::vector<string16> columns;
    135     base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns);
    136 
    137     // Some FTP servers put a "total n" line at the beginning of the listing
    138     // (n is an integer). Allow such a line, but only once, and only if it's
    139     // the first non-empty line. Do not match the word exactly, because it may
    140     // be in different languages (at least English and German have been seen
    141     // in the field).
    142     if (columns.size() == 2 && !received_total_line) {
    143       received_total_line = true;
    144 
    145       int total_number;
    146       if (!base::StringToInt(columns[1], &total_number))
    147         return false;
    148       if (total_number < 0)
    149         return false;
    150 
    151       continue;
    152     }
    153 
    154     FtpDirectoryListingEntry entry;
    155 
    156     size_t column_offset;
    157     if (!DetectColumnOffsetAndModificationTime(columns,
    158                                                current_time,
    159                                                &column_offset,
    160                                                &entry.last_modified)) {
    161       // If we can't recognize a normal listing line, maybe it's an error?
    162       // In that case, just ignore the error, but still recognize the data
    163       // as valid listing.
    164       if (LooksLikePermissionDeniedError(lines[i]))
    165         continue;
    166 
    167       return false;
    168     }
    169 
    170     if (!LooksLikeUnixPermissionsListing(columns[0]))
    171       return false;
    172     if (columns[0][0] == 'l') {
    173       entry.type = FtpDirectoryListingEntry::SYMLINK;
    174     } else if (columns[0][0] == 'd') {
    175       entry.type = FtpDirectoryListingEntry::DIRECTORY;
    176     } else {
    177       entry.type = FtpDirectoryListingEntry::FILE;
    178     }
    179 
    180     if (!base::StringToInt64(columns[column_offset - 3], &entry.size)) {
    181       // Some FTP servers do not separate owning group name from file size,
    182       // like "group1234". We still want to display the file name for that
    183       // entry, but can't really get the size (What if the group is named
    184       // "group1", and the size is in fact 234? We can't distinguish between
    185       // that and "group" with size 1234). Use a dummy value for the size.
    186       // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes.
    187       entry.size = 0;
    188     }
    189     if (entry.size < 0)
    190       return false;
    191     if (entry.type != FtpDirectoryListingEntry::FILE)
    192       entry.size = -1;
    193 
    194     if (column_offset == columns.size() - 1) {
    195       // If the end of the date listing is the last column, there is no file
    196       // name. Some FTP servers send listing entries with empty names.
    197       // It's not obvious how to display such an entry, so we ignore them.
    198       // We don't want to make the parsing fail at this point though.
    199       // Other entries can still be useful.
    200       continue;
    201     }
    202 
    203     entry.name = FtpUtil::GetStringPartAfterColumns(lines[i],
    204                                                     column_offset + 1);
    205 
    206     if (entry.type == FtpDirectoryListingEntry::SYMLINK) {
    207       string16::size_type pos = entry.name.rfind(ASCIIToUTF16(" -> "));
    208 
    209       // We don't require the " -> " to be present. Some FTP servers don't send
    210       // the symlink target, possibly for security reasons.
    211       if (pos != string16::npos)
    212         entry.name = entry.name.substr(0, pos);
    213     }
    214 
    215     entries->push_back(entry);
    216   }
    217 
    218   return true;
    219 }
    220 
    221 }  // namespace net
    222