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