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.
5 #include "net/ftp/ftp_directory_listing_parser_vms.h"
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"
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
, base::ASCIIToUTF16(";"),
30 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
31 if (listing_parts
.size() != 2)
34 if (!base::StringToInt(listing_parts
[1], &version_number
))
36 if (version_number
< 0)
39 // Even directories have extensions in the listings. Don't display extensions
40 // for directories; it's awkward for non-VMS users. Also, VMS is
41 // case-insensitive, but generally uses uppercase characters. This may look
42 // awkward, so we convert them to lower case.
43 std::vector
<base::string16
> filename_parts
=
44 base::SplitString(listing_parts
[0], base::ASCIIToUTF16("."),
45 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
46 if (filename_parts
.size() != 2)
48 if (base::EqualsASCII(filename_parts
[1], "DIR")) {
49 *parsed_filename
= base::StringToLowerASCII(filename_parts
[0]);
50 *type
= FtpDirectoryListingEntry::DIRECTORY
;
52 *parsed_filename
= base::StringToLowerASCII(listing_parts
[0]);
53 *type
= FtpDirectoryListingEntry::FILE;
58 bool ParseVmsFilesize(const base::string16
& input
, int64
* size
) {
59 if (base::ContainsOnlyChars(input
, base::ASCIIToUTF16("*"))) {
60 // Response consisting of asterisks means unknown size.
65 // VMS's directory listing gives us file size in blocks. We assume that
66 // the block size is 512 bytes. It doesn't give accurate file size, but is the
67 // best information we have.
68 const int kBlockSize
= 512;
70 if (base::StringToInt64(input
, size
)) {
77 std::vector
<base::StringPiece16
> parts
=
78 base::SplitStringPiece(input
, base::ASCIIToUTF16("/"),
79 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
80 if (parts
.size() != 2)
83 int64 blocks_used
, blocks_allocated
;
84 if (!base::StringToInt64(parts
[0], &blocks_used
))
86 if (!base::StringToInt64(parts
[1], &blocks_allocated
))
88 if (blocks_used
> blocks_allocated
)
90 if (blocks_used
< 0 || blocks_allocated
< 0)
93 *size
= blocks_used
* kBlockSize
;
97 bool LooksLikeVmsFileProtectionListingPart(const base::string16
& input
) {
98 if (input
.length() > 4)
101 // On VMS there are four different permission bits: Read, Write, Execute,
102 // and Delete. They appear in that order in the permission listing.
103 std::string
pattern("RWED");
104 base::string16
match(input
);
105 while (!match
.empty() && !pattern
.empty()) {
106 if (match
[0] == pattern
[0])
107 match
= match
.substr(1);
108 pattern
= pattern
.substr(1);
110 return match
.empty();
113 bool LooksLikeVmsFileProtectionListing(const base::string16
& input
) {
114 if (input
.length() < 2)
116 if (input
[0] != '(' || input
[input
.length() - 1] != ')')
119 // We expect four parts of the file protection listing: for System, Owner,
121 std::vector
<base::string16
> parts
= base::SplitString(
122 base::StringPiece16(input
).substr(1, input
.length() - 2),
123 base::ASCIIToUTF16(","), base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
124 if (parts
.size() != 4)
127 return LooksLikeVmsFileProtectionListingPart(parts
[0]) &&
128 LooksLikeVmsFileProtectionListingPart(parts
[1]) &&
129 LooksLikeVmsFileProtectionListingPart(parts
[2]) &&
130 LooksLikeVmsFileProtectionListingPart(parts
[3]);
133 bool LooksLikeVmsUserIdentificationCode(const base::string16
& input
) {
134 if (input
.length() < 2)
136 return input
[0] == '[' && input
[input
.length() - 1] == ']';
139 bool LooksLikeVMSError(const base::string16
& text
) {
140 static const char* const kPermissionDeniedMessages
[] = {
141 "%RMS-E-FNF", // File not found.
142 "%RMS-E-PRV", // Access denied.
147 for (size_t i
= 0; i
< arraysize(kPermissionDeniedMessages
); i
++) {
148 if (text
.find(base::ASCIIToUTF16(kPermissionDeniedMessages
[i
])) !=
149 base::string16::npos
)
156 bool VmsDateListingToTime(const std::vector
<base::string16
>& columns
,
158 DCHECK_EQ(4U, columns
.size());
160 base::Time::Exploded time_exploded
= { 0 };
162 // Date should be in format DD-MMM-YYYY.
163 std::vector
<base::StringPiece16
> date_parts
=
164 base::SplitStringPiece(columns
[2], base::ASCIIToUTF16("-"),
165 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
166 if (date_parts
.size() != 3)
168 if (!base::StringToInt(date_parts
[0], &time_exploded
.day_of_month
))
170 if (!FtpUtil::AbbreviatedMonthToNumber(date_parts
[1].as_string(),
171 &time_exploded
.month
))
173 if (!base::StringToInt(date_parts
[2], &time_exploded
.year
))
176 // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the
177 // last type first. Do not parse the seconds, they will be ignored anyway.
178 base::string16
time_column(columns
[3]);
179 if (time_column
.length() == 11 && time_column
[8] == '.')
180 time_column
= time_column
.substr(0, 8);
181 if (time_column
.length() == 8 && time_column
[5] == ':')
182 time_column
= time_column
.substr(0, 5);
183 if (time_column
.length() != 5)
185 std::vector
<base::StringPiece16
> time_parts
=
186 base::SplitStringPiece(time_column
, base::ASCIIToUTF16(":"),
187 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
188 if (time_parts
.size() != 2)
190 if (!base::StringToInt(time_parts
[0], &time_exploded
.hour
))
192 if (!base::StringToInt(time_parts
[1], &time_exploded
.minute
))
195 // We don't know the time zone of the server, so just use local time.
196 *time
= base::Time::FromLocalExploded(time_exploded
);
202 bool ParseFtpDirectoryListingVms(
203 const std::vector
<base::string16
>& lines
,
204 std::vector
<FtpDirectoryListingEntry
>* entries
) {
205 // The first non-empty line is the listing header. It often
206 // starts with "Directory ", but not always. We set a flag after
208 bool seen_header
= false;
210 // Sometimes the listing doesn't end with a "Total" line, but
211 // it's only okay when it contains some errors (it's needed
212 // to distinguish it from "ls -l" format).
213 bool seen_error
= false;
215 base::string16 total_of
= base::ASCIIToUTF16("Total of ");
216 base::char16 space
[2] = { ' ', 0 };
217 for (size_t i
= 0; i
< lines
.size(); i
++) {
218 if (lines
[i
].empty())
221 if (base::StartsWith(lines
[i
], total_of
, base::CompareCase::SENSITIVE
)) {
222 // After the "total" line, all following lines must be empty.
223 for (size_t j
= i
+ 1; j
< lines
.size(); j
++)
224 if (!lines
[j
].empty())
235 if (LooksLikeVMSError(lines
[i
])) {
240 std::vector
<base::string16
> columns
= base::SplitString(
241 base::CollapseWhitespace(lines
[i
], false), space
,
242 base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
244 if (columns
.size() == 1) {
245 // There can be no continuation if the current line is the last one.
246 if (i
== lines
.size() - 1)
249 // Skip the next line.
252 // This refers to the continuation line.
253 if (LooksLikeVMSError(lines
[i
])) {
258 // Join the current and next line and split them into columns.
259 columns
= base::SplitString(
260 base::CollapseWhitespace(
261 lines
[i
- 1] + space
+ lines
[i
], false),
262 space
, base::TRIM_WHITESPACE
, base::SPLIT_WANT_ALL
);
265 FtpDirectoryListingEntry entry
;
266 if (!ParseVmsFilename(columns
[0], &entry
.name
, &entry
.type
))
269 // There are different variants of a VMS listing. Some display
270 // the protection listing and user identification code, some do not.
271 if (columns
.size() == 6) {
272 if (!LooksLikeVmsFileProtectionListing(columns
[5]))
274 if (!LooksLikeVmsUserIdentificationCode(columns
[4]))
277 // Drop the unneeded data, so that the following code can always expect
278 // just four columns.
282 if (columns
.size() != 4)
285 if (!ParseVmsFilesize(columns
[1], &entry
.size
))
287 if (entry
.type
!= FtpDirectoryListingEntry::FILE)
289 if (!VmsDateListingToTime(columns
, &entry
.last_modified
))
292 entries
->push_back(entry
);
295 // The only place where we return true is after receiving the "Total" line,
296 // that should be present in every VMS listing. Alternatively, if the listing
297 // contains error messages, it's OK not to have the "Total" line.