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.
ParseVmsFilename(const base::string16 & raw_filename,base::string16 * parsed_filename,FtpDirectoryListingEntry::Type * type)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
ParseVmsFilesize(const base::string16 & input,int64 * size)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
LooksLikeVmsFileProtectionListingPart(const base::string16 & input)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
LooksLikeVmsFileProtectionListing(const base::string16 & input)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
LooksLikeVmsUserIdentificationCode(const base::string16 & input)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
LooksLikeVMSError(const base::string16 & text)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
VmsDateListingToTime(const std::vector<base::string16> & columns,base::Time * time)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
ParseFtpDirectoryListingVms(const std::vector<base::string16> & lines,std::vector<FtpDirectoryListingEntry> * entries)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