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_ls.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 bool TwoColumnDateListingToTime(const base::string16
& date
,
22 const base::string16
& time
,
24 base::Time::Exploded time_exploded
= { 0 };
26 // Date should be in format YYYY-MM-DD.
27 std::vector
<base::string16
> date_parts
;
28 base::SplitString(date
, '-', &date_parts
);
29 if (date_parts
.size() != 3)
31 if (!base::StringToInt(date_parts
[0], &time_exploded
.year
))
33 if (!base::StringToInt(date_parts
[1], &time_exploded
.month
))
35 if (!base::StringToInt(date_parts
[2], &time_exploded
.day_of_month
))
38 // Time should be in format HH:MM
39 if (time
.length() != 5)
42 std::vector
<base::string16
> time_parts
;
43 base::SplitString(time
, ':', &time_parts
);
44 if (time_parts
.size() != 2)
46 if (!base::StringToInt(time_parts
[0], &time_exploded
.hour
))
48 if (!base::StringToInt(time_parts
[1], &time_exploded
.minute
))
50 if (!time_exploded
.HasValidValues())
53 // We don't know the time zone of the server, so just use local time.
54 *result
= base::Time::FromLocalExploded(time_exploded
);
58 // Returns the column index of the end of the date listing and detected
59 // last modification time.
60 bool DetectColumnOffsetSizeAndModificationTime(
61 const std::vector
<base::string16
>& columns
,
62 const base::Time
& current_time
,
65 base::Time
* modification_time
) {
66 // The column offset can be arbitrarily large if some fields
67 // like owner or group name contain spaces. Try offsets from left to right
68 // and use the first one that matches a date listing.
70 // Here is how a listing line should look like. A star ("*") indicates
73 // * 1. permission listing
74 // 2. number of links (optional)
75 // * 3. owner name (may contain spaces)
76 // 4. group name (optional, may contain spaces)
80 // * 8. year or time <-- column_offset will be the index of this column
81 // 9. file name (optional, may contain spaces)
82 for (size_t i
= 5U; i
< columns
.size(); i
++) {
83 if (FtpUtil::LsDateListingToTime(columns
[i
- 2], columns
[i
- 1], columns
[i
],
84 current_time
, modification_time
)) {
85 *size
= columns
[i
- 3];
91 // Some FTP listings have swapped the "month" and "day of month" columns
92 // (for example Russian listings). We try to recognize them only after making
93 // sure no column offset works above (this is a more strict way).
94 for (size_t i
= 5U; i
< columns
.size(); i
++) {
95 if (FtpUtil::LsDateListingToTime(columns
[i
- 1], columns
[i
- 2], columns
[i
],
96 current_time
, modification_time
)) {
97 *size
= columns
[i
- 3];
103 // Some FTP listings use a different date format.
104 for (size_t i
= 5U; i
< columns
.size(); i
++) {
105 if (TwoColumnDateListingToTime(columns
[i
- 1],
107 modification_time
)) {
108 *size
= columns
[i
- 2];
119 bool ParseFtpDirectoryListingLs(
120 const std::vector
<base::string16
>& lines
,
121 const base::Time
& current_time
,
122 std::vector
<FtpDirectoryListingEntry
>* entries
) {
123 // True after we have received a "total n" listing header, where n is an
124 // integer. Only one such header is allowed per listing.
125 bool received_total_line
= false;
127 for (size_t i
= 0; i
< lines
.size(); i
++) {
128 if (lines
[i
].empty())
131 std::vector
<base::string16
> columns
;
132 base::SplitString(base::CollapseWhitespace(lines
[i
], false), ' ', &columns
);
134 // Some FTP servers put a "total n" line at the beginning of the listing
135 // (n is an integer). Allow such a line, but only once, and only if it's
136 // the first non-empty line. Do not match the word exactly, because it may
137 // be in different languages (at least English and German have been seen
139 if (columns
.size() == 2 && !received_total_line
) {
140 received_total_line
= true;
143 if (!base::StringToInt64(columns
[1], &total_number
))
145 if (total_number
< 0)
151 FtpDirectoryListingEntry entry
;
153 size_t column_offset
;
155 if (!DetectColumnOffsetSizeAndModificationTime(columns
,
159 &entry
.last_modified
)) {
160 // Some servers send a message in one of the first few lines.
161 // All those messages have in common is the string ".:",
162 // where "." means the current directory, and ":" separates it
163 // from the rest of the message, which may be empty.
164 if (lines
[i
].find(base::ASCIIToUTF16(".:")) != base::string16::npos
)
170 // Do not check "validity" of the permission listing. It's quirky,
171 // and some servers send garbage here while other parts of the line are OK.
173 if (!columns
[0].empty() && columns
[0][0] == 'l') {
174 entry
.type
= FtpDirectoryListingEntry::SYMLINK
;
175 } else if (!columns
[0].empty() && columns
[0][0] == 'd') {
176 entry
.type
= FtpDirectoryListingEntry::DIRECTORY
;
178 entry
.type
= FtpDirectoryListingEntry::FILE;
181 if (!base::StringToInt64(size
, &entry
.size
)) {
182 // Some FTP servers do not separate owning group name from file size,
183 // like "group1234". We still want to display the file name for that
184 // entry, but can't really get the size (What if the group is named
185 // "group1", and the size is in fact 234? We can't distinguish between
186 // that and "group" with size 1234). Use a dummy value for the size.
187 // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes.
190 if (entry
.size
< 0) {
191 // Some FTP servers have bugs that cause them to display the file size
192 // as negative. They're most likely big files like DVD ISO images.
193 // We still want to display them, so just say the real file size
197 if (entry
.type
!= FtpDirectoryListingEntry::FILE)
200 if (column_offset
== columns
.size() - 1) {
201 // If the end of the date listing is the last column, there is no file
202 // name. Some FTP servers send listing entries with empty names.
203 // It's not obvious how to display such an entry, so we ignore them.
204 // We don't want to make the parsing fail at this point though.
205 // Other entries can still be useful.
209 entry
.name
= FtpUtil::GetStringPartAfterColumns(lines
[i
],
212 if (entry
.type
== FtpDirectoryListingEntry::SYMLINK
) {
213 base::string16::size_type pos
=
214 entry
.name
.rfind(base::ASCIIToUTF16(" -> "));
216 // We don't require the " -> " to be present. Some FTP servers don't send
217 // the symlink target, possibly for security reasons.
218 if (pos
!= base::string16::npos
)
219 entry
.name
= entry
.name
.substr(0, pos
);
222 entries
->push_back(entry
);