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_vms.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 net { 18 19 namespace { 20 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, ';', &listing_parts); 30 if (listing_parts.size() != 2) 31 return false; 32 int version_number; 33 if (!base::StringToInt(listing_parts[1], &version_number)) 34 return false; 35 if (version_number < 0) 36 return false; 37 38 // Even directories have extensions in the listings. Don't display extensions 39 // for directories; it's awkward for non-VMS users. Also, VMS is 40 // case-insensitive, but generally uses uppercase characters. This may look 41 // awkward, so we convert them to lower case. 42 std::vector<base::string16> filename_parts; 43 base::SplitString(listing_parts[0], '.', &filename_parts); 44 if (filename_parts.size() != 2) 45 return false; 46 if (EqualsASCII(filename_parts[1], "DIR")) { 47 *parsed_filename = base::StringToLowerASCII(filename_parts[0]); 48 *type = FtpDirectoryListingEntry::DIRECTORY; 49 } else { 50 *parsed_filename = base::StringToLowerASCII(listing_parts[0]); 51 *type = FtpDirectoryListingEntry::FILE; 52 } 53 return true; 54 } 55 56 bool ParseVmsFilesize(const base::string16& input, int64* size) { 57 if (base::ContainsOnlyChars(input, base::ASCIIToUTF16("*"))) { 58 // Response consisting of asterisks means unknown size. 59 *size = -1; 60 return true; 61 } 62 63 // VMS's directory listing gives us file size in blocks. We assume that 64 // the block size is 512 bytes. It doesn't give accurate file size, but is the 65 // best information we have. 66 const int kBlockSize = 512; 67 68 if (base::StringToInt64(input, size)) { 69 if (*size < 0) 70 return false; 71 *size *= kBlockSize; 72 return true; 73 } 74 75 std::vector<base::string16> parts; 76 base::SplitString(input, '/', &parts); 77 if (parts.size() != 2) 78 return false; 79 80 int64 blocks_used, blocks_allocated; 81 if (!base::StringToInt64(parts[0], &blocks_used)) 82 return false; 83 if (!base::StringToInt64(parts[1], &blocks_allocated)) 84 return false; 85 if (blocks_used > blocks_allocated) 86 return false; 87 if (blocks_used < 0 || blocks_allocated < 0) 88 return false; 89 90 *size = blocks_used * kBlockSize; 91 return true; 92 } 93 94 bool LooksLikeVmsFileProtectionListingPart(const base::string16& input) { 95 if (input.length() > 4) 96 return false; 97 98 // On VMS there are four different permission bits: Read, Write, Execute, 99 // and Delete. They appear in that order in the permission listing. 100 std::string pattern("RWED"); 101 base::string16 match(input); 102 while (!match.empty() && !pattern.empty()) { 103 if (match[0] == pattern[0]) 104 match = match.substr(1); 105 pattern = pattern.substr(1); 106 } 107 return match.empty(); 108 } 109 110 bool LooksLikeVmsFileProtectionListing(const base::string16& input) { 111 if (input.length() < 2) 112 return false; 113 if (input[0] != '(' || input[input.length() - 1] != ')') 114 return false; 115 116 // We expect four parts of the file protection listing: for System, Owner, 117 // Group, and World. 118 std::vector<base::string16> parts; 119 base::SplitString(input.substr(1, input.length() - 2), ',', &parts); 120 if (parts.size() != 4) 121 return false; 122 123 return LooksLikeVmsFileProtectionListingPart(parts[0]) && 124 LooksLikeVmsFileProtectionListingPart(parts[1]) && 125 LooksLikeVmsFileProtectionListingPart(parts[2]) && 126 LooksLikeVmsFileProtectionListingPart(parts[3]); 127 } 128 129 bool LooksLikeVmsUserIdentificationCode(const base::string16& input) { 130 if (input.length() < 2) 131 return false; 132 return input[0] == '[' && input[input.length() - 1] == ']'; 133 } 134 135 bool LooksLikeVMSError(const base::string16& text) { 136 static const char* kPermissionDeniedMessages[] = { 137 "%RMS-E-FNF", // File not found. 138 "%RMS-E-PRV", // Access denied. 139 "%SYSTEM-F-NOPRIV", 140 "privilege", 141 }; 142 143 for (size_t i = 0; i < arraysize(kPermissionDeniedMessages); i++) { 144 if (text.find(base::ASCIIToUTF16(kPermissionDeniedMessages[i])) != 145 base::string16::npos) 146 return true; 147 } 148 149 return false; 150 } 151 152 bool VmsDateListingToTime(const std::vector<base::string16>& columns, 153 base::Time* time) { 154 DCHECK_EQ(4U, columns.size()); 155 156 base::Time::Exploded time_exploded = { 0 }; 157 158 // Date should be in format DD-MMM-YYYY. 159 std::vector<base::string16> date_parts; 160 base::SplitString(columns[2], '-', &date_parts); 161 if (date_parts.size() != 3) 162 return false; 163 if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month)) 164 return false; 165 if (!FtpUtil::AbbreviatedMonthToNumber(date_parts[1], 166 &time_exploded.month)) 167 return false; 168 if (!base::StringToInt(date_parts[2], &time_exploded.year)) 169 return false; 170 171 // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the 172 // last type first. Do not parse the seconds, they will be ignored anyway. 173 base::string16 time_column(columns[3]); 174 if (time_column.length() == 11 && time_column[8] == '.') 175 time_column = time_column.substr(0, 8); 176 if (time_column.length() == 8 && time_column[5] == ':') 177 time_column = time_column.substr(0, 5); 178 if (time_column.length() != 5) 179 return false; 180 std::vector<base::string16> time_parts; 181 base::SplitString(time_column, ':', &time_parts); 182 if (time_parts.size() != 2) 183 return false; 184 if (!base::StringToInt(time_parts[0], &time_exploded.hour)) 185 return false; 186 if (!base::StringToInt(time_parts[1], &time_exploded.minute)) 187 return false; 188 189 // We don't know the time zone of the server, so just use local time. 190 *time = base::Time::FromLocalExploded(time_exploded); 191 return true; 192 } 193 194 } // namespace 195 196 bool ParseFtpDirectoryListingVms( 197 const std::vector<base::string16>& lines, 198 std::vector<FtpDirectoryListingEntry>* entries) { 199 // The first non-empty line is the listing header. It often 200 // starts with "Directory ", but not always. We set a flag after 201 // seing the header. 202 bool seen_header = false; 203 204 // Sometimes the listing doesn't end with a "Total" line, but 205 // it's only okay when it contains some errors (it's needed 206 // to distinguish it from "ls -l" format). 207 bool seen_error = false; 208 209 for (size_t i = 0; i < lines.size(); i++) { 210 if (lines[i].empty()) 211 continue; 212 213 if (StartsWith(lines[i], base::ASCIIToUTF16("Total of "), true)) { 214 // After the "total" line, all following lines must be empty. 215 for (size_t j = i + 1; j < lines.size(); j++) 216 if (!lines[j].empty()) 217 return false; 218 219 return true; 220 } 221 222 if (!seen_header) { 223 seen_header = true; 224 continue; 225 } 226 227 if (LooksLikeVMSError(lines[i])) { 228 seen_error = true; 229 continue; 230 } 231 232 std::vector<base::string16> columns; 233 base::SplitString(base::CollapseWhitespace(lines[i], false), ' ', &columns); 234 235 if (columns.size() == 1) { 236 // There can be no continuation if the current line is the last one. 237 if (i == lines.size() - 1) 238 return false; 239 240 // Skip the next line. 241 i++; 242 243 // This refers to the continuation line. 244 if (LooksLikeVMSError(lines[i])) { 245 seen_error = true; 246 continue; 247 } 248 249 // Join the current and next line and split them into columns. 250 base::SplitString( 251 base::CollapseWhitespace( 252 lines[i - 1] + base::ASCIIToUTF16(" ") + lines[i], false), 253 ' ', 254 &columns); 255 } 256 257 FtpDirectoryListingEntry entry; 258 if (!ParseVmsFilename(columns[0], &entry.name, &entry.type)) 259 return false; 260 261 // There are different variants of a VMS listing. Some display 262 // the protection listing and user identification code, some do not. 263 if (columns.size() == 6) { 264 if (!LooksLikeVmsFileProtectionListing(columns[5])) 265 return false; 266 if (!LooksLikeVmsUserIdentificationCode(columns[4])) 267 return false; 268 269 // Drop the unneeded data, so that the following code can always expect 270 // just four columns. 271 columns.resize(4); 272 } 273 274 if (columns.size() != 4) 275 return false; 276 277 if (!ParseVmsFilesize(columns[1], &entry.size)) 278 return false; 279 if (entry.type != FtpDirectoryListingEntry::FILE) 280 entry.size = -1; 281 if (!VmsDateListingToTime(columns, &entry.last_modified)) 282 return false; 283 284 entries->push_back(entry); 285 } 286 287 // The only place where we return true is after receiving the "Total" line, 288 // that should be present in every VMS listing. Alternatively, if the listing 289 // contains error messages, it's OK not to have the "Total" line. 290 return seen_error; 291 } 292 293 } // namespace net 294