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