ftp_directory_listing_parser_vms.cc revision 6e8cce623b6e4fe0c9e4af605d675dd9d0338c38
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
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 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
56bool 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
94bool 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
110bool 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
129bool LooksLikeVmsUserIdentificationCode(const base::string16& input) {
130  if (input.length() < 2)
131    return false;
132  return input[0] == '[' && input[input.length() - 1] == ']';
133}
134
135bool 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
152bool 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
196bool 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