OLD | NEW |
| (Empty) |
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_ls.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 { | |
18 | |
19 bool TwoColumnDateListingToTime(const base::string16& date, | |
20 const base::string16& time, | |
21 base::Time* result) { | |
22 base::Time::Exploded time_exploded = { 0 }; | |
23 | |
24 // Date should be in format YYYY-MM-DD. | |
25 std::vector<base::string16> date_parts; | |
26 base::SplitString(date, '-', &date_parts); | |
27 if (date_parts.size() != 3) | |
28 return false; | |
29 if (!base::StringToInt(date_parts[0], &time_exploded.year)) | |
30 return false; | |
31 if (!base::StringToInt(date_parts[1], &time_exploded.month)) | |
32 return false; | |
33 if (!base::StringToInt(date_parts[2], &time_exploded.day_of_month)) | |
34 return false; | |
35 | |
36 // Time should be in format HH:MM | |
37 if (time.length() != 5) | |
38 return false; | |
39 | |
40 std::vector<base::string16> time_parts; | |
41 base::SplitString(time, ':', &time_parts); | |
42 if (time_parts.size() != 2) | |
43 return false; | |
44 if (!base::StringToInt(time_parts[0], &time_exploded.hour)) | |
45 return false; | |
46 if (!base::StringToInt(time_parts[1], &time_exploded.minute)) | |
47 return false; | |
48 if (!time_exploded.HasValidValues()) | |
49 return false; | |
50 | |
51 // We don't know the time zone of the server, so just use local time. | |
52 *result = base::Time::FromLocalExploded(time_exploded); | |
53 return true; | |
54 } | |
55 | |
56 // Returns the column index of the end of the date listing and detected | |
57 // last modification time. | |
58 bool DetectColumnOffsetSizeAndModificationTime( | |
59 const std::vector<base::string16>& columns, | |
60 const base::Time& current_time, | |
61 size_t* offset, | |
62 base::string16* size, | |
63 base::Time* modification_time) { | |
64 // The column offset can be arbitrarily large if some fields | |
65 // like owner or group name contain spaces. Try offsets from left to right | |
66 // and use the first one that matches a date listing. | |
67 // | |
68 // Here is how a listing line should look like. A star ("*") indicates | |
69 // a required field: | |
70 // | |
71 // * 1. permission listing | |
72 // 2. number of links (optional) | |
73 // * 3. owner name (may contain spaces) | |
74 // 4. group name (optional, may contain spaces) | |
75 // * 5. size in bytes | |
76 // * 6. month | |
77 // * 7. day of month | |
78 // * 8. year or time <-- column_offset will be the index of this column | |
79 // 9. file name (optional, may contain spaces) | |
80 for (size_t i = 5U; i < columns.size(); i++) { | |
81 if (net::FtpUtil::LsDateListingToTime(columns[i - 2], | |
82 columns[i - 1], | |
83 columns[i], | |
84 current_time, | |
85 modification_time)) { | |
86 *size = columns[i - 3]; | |
87 *offset = i; | |
88 return true; | |
89 } | |
90 } | |
91 | |
92 // Some FTP listings have swapped the "month" and "day of month" columns | |
93 // (for example Russian listings). We try to recognize them only after making | |
94 // sure no column offset works above (this is a more strict way). | |
95 for (size_t i = 5U; i < columns.size(); i++) { | |
96 if (net::FtpUtil::LsDateListingToTime(columns[i - 1], | |
97 columns[i - 2], | |
98 columns[i], | |
99 current_time, | |
100 modification_time)) { | |
101 *size = columns[i - 3]; | |
102 *offset = i; | |
103 return true; | |
104 } | |
105 } | |
106 | |
107 // Some FTP listings use a different date format. | |
108 for (size_t i = 5U; i < columns.size(); i++) { | |
109 if (TwoColumnDateListingToTime(columns[i - 1], | |
110 columns[i], | |
111 modification_time)) { | |
112 *size = columns[i - 2]; | |
113 *offset = i; | |
114 return true; | |
115 } | |
116 } | |
117 | |
118 return false; | |
119 } | |
120 | |
121 } // namespace | |
122 | |
123 namespace net { | |
124 | |
125 bool ParseFtpDirectoryListingLs( | |
126 const std::vector<base::string16>& lines, | |
127 const base::Time& current_time, | |
128 std::vector<FtpDirectoryListingEntry>* entries) { | |
129 // True after we have received a "total n" listing header, where n is an | |
130 // integer. Only one such header is allowed per listing. | |
131 bool received_total_line = false; | |
132 | |
133 for (size_t i = 0; i < lines.size(); i++) { | |
134 if (lines[i].empty()) | |
135 continue; | |
136 | |
137 std::vector<base::string16> columns; | |
138 base::SplitString(base::CollapseWhitespace(lines[i], false), ' ', &columns); | |
139 | |
140 // Some FTP servers put a "total n" line at the beginning of the listing | |
141 // (n is an integer). Allow such a line, but only once, and only if it's | |
142 // the first non-empty line. Do not match the word exactly, because it may | |
143 // be in different languages (at least English and German have been seen | |
144 // in the field). | |
145 if (columns.size() == 2 && !received_total_line) { | |
146 received_total_line = true; | |
147 | |
148 int64 total_number; | |
149 if (!base::StringToInt64(columns[1], &total_number)) | |
150 return false; | |
151 if (total_number < 0) | |
152 return false; | |
153 | |
154 continue; | |
155 } | |
156 | |
157 FtpDirectoryListingEntry entry; | |
158 | |
159 size_t column_offset; | |
160 base::string16 size; | |
161 if (!DetectColumnOffsetSizeAndModificationTime(columns, | |
162 current_time, | |
163 &column_offset, | |
164 &size, | |
165 &entry.last_modified)) { | |
166 // Some servers send a message in one of the first few lines. | |
167 // All those messages have in common is the string ".:", | |
168 // where "." means the current directory, and ":" separates it | |
169 // from the rest of the message, which may be empty. | |
170 if (lines[i].find(base::ASCIIToUTF16(".:")) != base::string16::npos) | |
171 continue; | |
172 | |
173 return false; | |
174 } | |
175 | |
176 // Do not check "validity" of the permission listing. It's quirky, | |
177 // and some servers send garbage here while other parts of the line are OK. | |
178 | |
179 if (!columns[0].empty() && columns[0][0] == 'l') { | |
180 entry.type = FtpDirectoryListingEntry::SYMLINK; | |
181 } else if (!columns[0].empty() && columns[0][0] == 'd') { | |
182 entry.type = FtpDirectoryListingEntry::DIRECTORY; | |
183 } else { | |
184 entry.type = FtpDirectoryListingEntry::FILE; | |
185 } | |
186 | |
187 if (!base::StringToInt64(size, &entry.size)) { | |
188 // Some FTP servers do not separate owning group name from file size, | |
189 // like "group1234". We still want to display the file name for that | |
190 // entry, but can't really get the size (What if the group is named | |
191 // "group1", and the size is in fact 234? We can't distinguish between | |
192 // that and "group" with size 1234). Use a dummy value for the size. | |
193 // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes. | |
194 entry.size = 0; | |
195 } | |
196 if (entry.size < 0) { | |
197 // Some FTP servers have bugs that cause them to display the file size | |
198 // as negative. They're most likely big files like DVD ISO images. | |
199 // We still want to display them, so just say the real file size | |
200 // is unknown. | |
201 entry.size = -1; | |
202 } | |
203 if (entry.type != FtpDirectoryListingEntry::FILE) | |
204 entry.size = -1; | |
205 | |
206 if (column_offset == columns.size() - 1) { | |
207 // If the end of the date listing is the last column, there is no file | |
208 // name. Some FTP servers send listing entries with empty names. | |
209 // It's not obvious how to display such an entry, so we ignore them. | |
210 // We don't want to make the parsing fail at this point though. | |
211 // Other entries can still be useful. | |
212 continue; | |
213 } | |
214 | |
215 entry.name = FtpUtil::GetStringPartAfterColumns(lines[i], | |
216 column_offset + 1); | |
217 | |
218 if (entry.type == FtpDirectoryListingEntry::SYMLINK) { | |
219 base::string16::size_type pos = | |
220 entry.name.rfind(base::ASCIIToUTF16(" -> ")); | |
221 | |
222 // We don't require the " -> " to be present. Some FTP servers don't send | |
223 // the symlink target, possibly for security reasons. | |
224 if (pos != base::string16::npos) | |
225 entry.name = entry.name.substr(0, pos); | |
226 } | |
227 | |
228 entries->push_back(entry); | |
229 } | |
230 | |
231 return true; | |
232 } | |
233 | |
234 } // namespace net | |
OLD | NEW |