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