1// Copyright (c) 2011 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/string_number_conversions.h"
10#include "base/string_split.h"
11#include "base/string_util.h"
12#include "base/time.h"
13#include "base/utf_string_conversions.h"
14#include "net/ftp/ftp_directory_listing_parser.h"
15#include "net/ftp/ftp_util.h"
16
17namespace {
18
19bool LooksLikeUnixPermission(const string16& text) {
20  if (text.length() != 3)
21    return false;
22
23  // Meaning of the flags:
24  // r - file is readable
25  // w - file is writable
26  // x - file is executable
27  // s or S - setuid/setgid bit set
28  // t or T - "sticky" bit set
29  return ((text[0] == 'r' || text[0] == '-') &&
30          (text[1] == 'w' || text[1] == '-') &&
31          (text[2] == 'x' || text[2] == 's' || text[2] == 'S' ||
32           text[2] == 't' || text[2] == 'T' || text[2] == '-'));
33}
34
35bool LooksLikeUnixPermissionsListing(const string16& text) {
36  if (text.length() < 10)
37    return false;
38
39  if (text[0] != 'b' && text[0] != 'c' && text[0] != 'd' &&
40      text[0] != 'l' && text[0] != 'p' && text[0] != 's' &&
41      text[0] != '-')
42    return false;
43
44  // Do not check the rest of the string. Some servers fail to properly
45  // separate this column from the next column (number of links), resulting
46  // in additional characters at the end. Also, sometimes there is a "+"
47  // sign at the end indicating the file has ACLs set.
48  return (LooksLikeUnixPermission(text.substr(1, 3)) &&
49          LooksLikeUnixPermission(text.substr(4, 3)) &&
50          LooksLikeUnixPermission(text.substr(7, 3)));
51}
52
53bool LooksLikePermissionDeniedError(const string16& text) {
54  // Try to recognize a three-part colon-separated error message:
55  //
56  //   1. ftpd server name
57  //   2. directory name (often just ".")
58  //   3. message text (usually "Permission denied")
59  std::vector<string16> parts;
60  base::SplitString(CollapseWhitespace(text, false), ':', &parts);
61
62  if (parts.size() != 3)
63    return false;
64
65  return parts[2] == ASCIIToUTF16("Permission denied");
66}
67
68// Returns the column index of the end of the date listing and detected
69// last modification time.
70bool DetectColumnOffsetAndModificationTime(const std::vector<string16>& columns,
71                                           const base::Time& current_time,
72                                           size_t* offset,
73                                           base::Time* modification_time) {
74  // The column offset can be arbitrarily large if some fields
75  // like owner or group name contain spaces. Try offsets from left to right
76  // and use the first one that matches a date listing.
77  //
78  // Here is how a listing line should look like. A star ("*") indicates
79  // a required field:
80  //
81  //  * 1. permission listing
82  //    2. number of links (optional)
83  //  * 3. owner name (may contain spaces)
84  //    4. group name (optional, may contain spaces)
85  //  * 5. size in bytes
86  //  * 6. month
87  //  * 7. day of month
88  //  * 8. year or time <-- column_offset will be the index of this column
89  //    9. file name (optional, may contain spaces)
90  for (size_t i = 5U; i < columns.size(); i++) {
91    if (net::FtpUtil::LsDateListingToTime(columns[i - 2],
92                                          columns[i - 1],
93                                          columns[i],
94                                          current_time,
95                                          modification_time)) {
96      *offset = i;
97      return true;
98    }
99  }
100
101  // Some FTP listings have swapped the "month" and "day of month" columns
102  // (for example Russian listings). We try to recognize them only after making
103  // sure no column offset works above (this is a more strict way).
104  for (size_t i = 5U; i < columns.size(); i++) {
105    if (net::FtpUtil::LsDateListingToTime(columns[i - 1],
106                                          columns[i - 2],
107                                          columns[i],
108                                          current_time,
109                                          modification_time)) {
110      *offset = i;
111      return true;
112    }
113  }
114
115  return false;
116}
117
118}  // namespace
119
120namespace net {
121
122bool ParseFtpDirectoryListingLs(
123    const std::vector<string16>& lines,
124    const base::Time& current_time,
125    std::vector<FtpDirectoryListingEntry>* entries) {
126  // True after we have received a "total n" listing header, where n is an
127  // integer. Only one such header is allowed per listing.
128  bool received_total_line = false;
129
130  for (size_t i = 0; i < lines.size(); i++) {
131    if (lines[i].empty())
132      continue;
133
134    std::vector<string16> columns;
135    base::SplitString(CollapseWhitespace(lines[i], false), ' ', &columns);
136
137    // Some FTP servers put a "total n" line at the beginning of the listing
138    // (n is an integer). Allow such a line, but only once, and only if it's
139    // the first non-empty line. Do not match the word exactly, because it may
140    // be in different languages (at least English and German have been seen
141    // in the field).
142    if (columns.size() == 2 && !received_total_line) {
143      received_total_line = true;
144
145      int total_number;
146      if (!base::StringToInt(columns[1], &total_number))
147        return false;
148      if (total_number < 0)
149        return false;
150
151      continue;
152    }
153
154    FtpDirectoryListingEntry entry;
155
156    size_t column_offset;
157    if (!DetectColumnOffsetAndModificationTime(columns,
158                                               current_time,
159                                               &column_offset,
160                                               &entry.last_modified)) {
161      // If we can't recognize a normal listing line, maybe it's an error?
162      // In that case, just ignore the error, but still recognize the data
163      // as valid listing.
164      if (LooksLikePermissionDeniedError(lines[i]))
165        continue;
166
167      return false;
168    }
169
170    if (!LooksLikeUnixPermissionsListing(columns[0]))
171      return false;
172    if (columns[0][0] == 'l') {
173      entry.type = FtpDirectoryListingEntry::SYMLINK;
174    } else if (columns[0][0] == 'd') {
175      entry.type = FtpDirectoryListingEntry::DIRECTORY;
176    } else {
177      entry.type = FtpDirectoryListingEntry::FILE;
178    }
179
180    if (!base::StringToInt64(columns[column_offset - 3], &entry.size)) {
181      // Some FTP servers do not separate owning group name from file size,
182      // like "group1234". We still want to display the file name for that
183      // entry, but can't really get the size (What if the group is named
184      // "group1", and the size is in fact 234? We can't distinguish between
185      // that and "group" with size 1234). Use a dummy value for the size.
186      // TODO(phajdan.jr): Use a value that means "unknown" instead of 0 bytes.
187      entry.size = 0;
188    }
189    if (entry.size < 0)
190      return false;
191    if (entry.type != FtpDirectoryListingEntry::FILE)
192      entry.size = -1;
193
194    if (column_offset == columns.size() - 1) {
195      // If the end of the date listing is the last column, there is no file
196      // name. Some FTP servers send listing entries with empty names.
197      // It's not obvious how to display such an entry, so we ignore them.
198      // We don't want to make the parsing fail at this point though.
199      // Other entries can still be useful.
200      continue;
201    }
202
203    entry.name = FtpUtil::GetStringPartAfterColumns(lines[i],
204                                                    column_offset + 1);
205
206    if (entry.type == FtpDirectoryListingEntry::SYMLINK) {
207      string16::size_type pos = entry.name.rfind(ASCIIToUTF16(" -> "));
208
209      // We don't require the " -> " to be present. Some FTP servers don't send
210      // the symlink target, possibly for security reasons.
211      if (pos != string16::npos)
212        entry.name = entry.name.substr(0, pos);
213    }
214
215    entries->push_back(entry);
216  }
217
218  return true;
219}
220
221}  // namespace net
222