Home | History | Annotate | Download | only in ftp
      1 // Copyright (c) 2012 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_vms.h"
      6 
      7 #include <vector>
      8 
      9 #include "base/strings/string_number_conversions.h"
     10 #include "base/strings/string_split.h"
     11 #include "base/strings/string_util.h"
     12 #include "base/strings/utf_string_conversions.h"
     13 #include "base/time/time.h"
     14 #include "net/ftp/ftp_directory_listing_parser.h"
     15 #include "net/ftp/ftp_util.h"
     16 
     17 namespace net {
     18 
     19 namespace {
     20 
     21 // Converts the filename component in listing to the filename we can display.
     22 // Returns true on success.
     23 bool ParseVmsFilename(const base::string16& raw_filename,
     24                       base::string16* parsed_filename,
     25                       FtpDirectoryListingEntry::Type* type) {
     26   // On VMS, the files and directories are versioned. The version number is
     27   // separated from the file name by a semicolon. Example: ANNOUNCE.TXT;2.
     28   std::vector<base::string16> listing_parts;
     29   base::SplitString(raw_filename, ';', &listing_parts);
     30   if (listing_parts.size() != 2)
     31     return false;
     32   int version_number;
     33   if (!base::StringToInt(listing_parts[1], &version_number))
     34     return false;
     35   if (version_number < 0)
     36     return false;
     37 
     38   // Even directories have extensions in the listings. Don't display extensions
     39   // for directories; it's awkward for non-VMS users. Also, VMS is
     40   // case-insensitive, but generally uses uppercase characters. This may look
     41   // awkward, so we convert them to lower case.
     42   std::vector<base::string16> filename_parts;
     43   base::SplitString(listing_parts[0], '.', &filename_parts);
     44   if (filename_parts.size() != 2)
     45     return false;
     46   if (EqualsASCII(filename_parts[1], "DIR")) {
     47     *parsed_filename = base::StringToLowerASCII(filename_parts[0]);
     48     *type = FtpDirectoryListingEntry::DIRECTORY;
     49   } else {
     50     *parsed_filename = base::StringToLowerASCII(listing_parts[0]);
     51     *type = FtpDirectoryListingEntry::FILE;
     52   }
     53   return true;
     54 }
     55 
     56 bool ParseVmsFilesize(const base::string16& input, int64* size) {
     57   if (base::ContainsOnlyChars(input, base::ASCIIToUTF16("*"))) {
     58     // Response consisting of asterisks means unknown size.
     59     *size = -1;
     60     return true;
     61   }
     62 
     63   // VMS's directory listing gives us file size in blocks. We assume that
     64   // the block size is 512 bytes. It doesn't give accurate file size, but is the
     65   // best information we have.
     66   const int kBlockSize = 512;
     67 
     68   if (base::StringToInt64(input, size)) {
     69     if (*size < 0)
     70       return false;
     71     *size *= kBlockSize;
     72     return true;
     73   }
     74 
     75   std::vector<base::string16> parts;
     76   base::SplitString(input, '/', &parts);
     77   if (parts.size() != 2)
     78     return false;
     79 
     80   int64 blocks_used, blocks_allocated;
     81   if (!base::StringToInt64(parts[0], &blocks_used))
     82     return false;
     83   if (!base::StringToInt64(parts[1], &blocks_allocated))
     84     return false;
     85   if (blocks_used > blocks_allocated)
     86     return false;
     87   if (blocks_used < 0 || blocks_allocated < 0)
     88     return false;
     89 
     90   *size = blocks_used * kBlockSize;
     91   return true;
     92 }
     93 
     94 bool LooksLikeVmsFileProtectionListingPart(const base::string16& input) {
     95   if (input.length() > 4)
     96     return false;
     97 
     98   // On VMS there are four different permission bits: Read, Write, Execute,
     99   // and Delete. They appear in that order in the permission listing.
    100   std::string pattern("RWED");
    101   base::string16 match(input);
    102   while (!match.empty() && !pattern.empty()) {
    103     if (match[0] == pattern[0])
    104       match = match.substr(1);
    105     pattern = pattern.substr(1);
    106   }
    107   return match.empty();
    108 }
    109 
    110 bool LooksLikeVmsFileProtectionListing(const base::string16& input) {
    111   if (input.length() < 2)
    112     return false;
    113   if (input[0] != '(' || input[input.length() - 1] != ')')
    114     return false;
    115 
    116   // We expect four parts of the file protection listing: for System, Owner,
    117   // Group, and World.
    118   std::vector<base::string16> parts;
    119   base::SplitString(input.substr(1, input.length() - 2), ',', &parts);
    120   if (parts.size() != 4)
    121     return false;
    122 
    123   return LooksLikeVmsFileProtectionListingPart(parts[0]) &&
    124       LooksLikeVmsFileProtectionListingPart(parts[1]) &&
    125       LooksLikeVmsFileProtectionListingPart(parts[2]) &&
    126       LooksLikeVmsFileProtectionListingPart(parts[3]);
    127 }
    128 
    129 bool LooksLikeVmsUserIdentificationCode(const base::string16& input) {
    130   if (input.length() < 2)
    131     return false;
    132   return input[0] == '[' && input[input.length() - 1] == ']';
    133 }
    134 
    135 bool LooksLikeVMSError(const base::string16& text) {
    136   static const char* kPermissionDeniedMessages[] = {
    137     "%RMS-E-FNF",  // File not found.
    138     "%RMS-E-PRV",  // Access denied.
    139     "%SYSTEM-F-NOPRIV",
    140     "privilege",
    141   };
    142 
    143   for (size_t i = 0; i < arraysize(kPermissionDeniedMessages); i++) {
    144     if (text.find(base::ASCIIToUTF16(kPermissionDeniedMessages[i])) !=
    145         base::string16::npos)
    146       return true;
    147   }
    148 
    149   return false;
    150 }
    151 
    152 bool VmsDateListingToTime(const std::vector<base::string16>& columns,
    153                           base::Time* time) {
    154   DCHECK_EQ(4U, columns.size());
    155 
    156   base::Time::Exploded time_exploded = { 0 };
    157 
    158   // Date should be in format DD-MMM-YYYY.
    159   std::vector<base::string16> date_parts;
    160   base::SplitString(columns[2], '-', &date_parts);
    161   if (date_parts.size() != 3)
    162     return false;
    163   if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month))
    164     return false;
    165   if (!FtpUtil::AbbreviatedMonthToNumber(date_parts[1],
    166                                          &time_exploded.month))
    167     return false;
    168   if (!base::StringToInt(date_parts[2], &time_exploded.year))
    169     return false;
    170 
    171   // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the
    172   // last type first. Do not parse the seconds, they will be ignored anyway.
    173   base::string16 time_column(columns[3]);
    174   if (time_column.length() == 11 && time_column[8] == '.')
    175     time_column = time_column.substr(0, 8);
    176   if (time_column.length() == 8 && time_column[5] == ':')
    177     time_column = time_column.substr(0, 5);
    178   if (time_column.length() != 5)
    179     return false;
    180   std::vector<base::string16> time_parts;
    181   base::SplitString(time_column, ':', &time_parts);
    182   if (time_parts.size() != 2)
    183     return false;
    184   if (!base::StringToInt(time_parts[0], &time_exploded.hour))
    185     return false;
    186   if (!base::StringToInt(time_parts[1], &time_exploded.minute))
    187     return false;
    188 
    189   // We don't know the time zone of the server, so just use local time.
    190   *time = base::Time::FromLocalExploded(time_exploded);
    191   return true;
    192 }
    193 
    194 }  // namespace
    195 
    196 bool ParseFtpDirectoryListingVms(
    197     const std::vector<base::string16>& lines,
    198     std::vector<FtpDirectoryListingEntry>* entries) {
    199   // The first non-empty line is the listing header. It often
    200   // starts with "Directory ", but not always. We set a flag after
    201   // seing the header.
    202   bool seen_header = false;
    203 
    204   // Sometimes the listing doesn't end with a "Total" line, but
    205   // it's only okay when it contains some errors (it's needed
    206   // to distinguish it from "ls -l" format).
    207   bool seen_error = false;
    208 
    209   for (size_t i = 0; i < lines.size(); i++) {
    210     if (lines[i].empty())
    211       continue;
    212 
    213     if (StartsWith(lines[i], base::ASCIIToUTF16("Total of "), true)) {
    214       // After the "total" line, all following lines must be empty.
    215       for (size_t j = i + 1; j < lines.size(); j++)
    216         if (!lines[j].empty())
    217           return false;
    218 
    219       return true;
    220     }
    221 
    222     if (!seen_header) {
    223       seen_header = true;
    224       continue;
    225     }
    226 
    227     if (LooksLikeVMSError(lines[i])) {
    228       seen_error = true;
    229       continue;
    230     }
    231 
    232     std::vector<base::string16> columns;
    233     base::SplitString(base::CollapseWhitespace(lines[i], false), ' ', &columns);
    234 
    235     if (columns.size() == 1) {
    236       // There can be no continuation if the current line is the last one.
    237       if (i == lines.size() - 1)
    238         return false;
    239 
    240       // Skip the next line.
    241       i++;
    242 
    243       // This refers to the continuation line.
    244       if (LooksLikeVMSError(lines[i])) {
    245         seen_error = true;
    246         continue;
    247       }
    248 
    249       // Join the current and next line and split them into columns.
    250       base::SplitString(
    251           base::CollapseWhitespace(
    252               lines[i - 1] + base::ASCIIToUTF16(" ") + lines[i], false),
    253           ' ',
    254           &columns);
    255     }
    256 
    257     FtpDirectoryListingEntry entry;
    258     if (!ParseVmsFilename(columns[0], &entry.name, &entry.type))
    259       return false;
    260 
    261     // There are different variants of a VMS listing. Some display
    262     // the protection listing and user identification code, some do not.
    263     if (columns.size() == 6) {
    264       if (!LooksLikeVmsFileProtectionListing(columns[5]))
    265         return false;
    266       if (!LooksLikeVmsUserIdentificationCode(columns[4]))
    267         return false;
    268 
    269       // Drop the unneeded data, so that the following code can always expect
    270       // just four columns.
    271       columns.resize(4);
    272     }
    273 
    274     if (columns.size() != 4)
    275       return false;
    276 
    277     if (!ParseVmsFilesize(columns[1], &entry.size))
    278       return false;
    279     if (entry.type != FtpDirectoryListingEntry::FILE)
    280       entry.size = -1;
    281     if (!VmsDateListingToTime(columns, &entry.last_modified))
    282       return false;
    283 
    284     entries->push_back(entry);
    285   }
    286 
    287   // The only place where we return true is after receiving the "Total" line,
    288   // that should be present in every VMS listing. Alternatively, if the listing
    289   // contains error messages, it's OK not to have the "Total" line.
    290   return seen_error;
    291 }
    292 
    293 }  // namespace net
    294