// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/ftp/ftp_directory_listing_parser_vms.h" #include #include "base/strings/string_number_conversions.h" #include "base/strings/string_split.h" #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "base/time/time.h" #include "net/ftp/ftp_directory_listing_parser.h" #include "net/ftp/ftp_util.h" namespace net { namespace { // Converts the filename component in listing to the filename we can display. // Returns true on success. bool ParseVmsFilename(const base::string16& raw_filename, base::string16* parsed_filename, FtpDirectoryListingEntry::Type* type) { // On VMS, the files and directories are versioned. The version number is // separated from the file name by a semicolon. Example: ANNOUNCE.TXT;2. std::vector listing_parts = base::SplitString(raw_filename, base::ASCIIToUTF16(";"), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (listing_parts.size() != 2) return false; int version_number; if (!base::StringToInt(listing_parts[1], &version_number)) return false; if (version_number < 0) return false; // Even directories have extensions in the listings. Don't display extensions // for directories; it's awkward for non-VMS users. Also, VMS is // case-insensitive, but generally uses uppercase characters. This may look // awkward, so we convert them to lower case. std::vector filename_parts = base::SplitString(listing_parts[0], base::ASCIIToUTF16("."), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (filename_parts.size() != 2) return false; if (base::EqualsASCII(filename_parts[1], "DIR")) { *parsed_filename = base::ToLowerASCII(filename_parts[0]); *type = FtpDirectoryListingEntry::DIRECTORY; } else { *parsed_filename = base::ToLowerASCII(listing_parts[0]); *type = FtpDirectoryListingEntry::FILE; } return true; } bool ParseVmsFilesize(const base::string16& input, int64_t* size) { if (base::ContainsOnlyChars(input, base::ASCIIToUTF16("*"))) { // Response consisting of asterisks means unknown size. *size = -1; return true; } // VMS's directory listing gives us file size in blocks. We assume that // the block size is 512 bytes. It doesn't give accurate file size, but is the // best information we have. const int kBlockSize = 512; if (base::StringToInt64(input, size)) { if (*size < 0) return false; *size *= kBlockSize; return true; } std::vector parts = base::SplitStringPiece(input, base::ASCIIToUTF16("/"), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (parts.size() != 2) return false; int64_t blocks_used, blocks_allocated; if (!base::StringToInt64(parts[0], &blocks_used)) return false; if (!base::StringToInt64(parts[1], &blocks_allocated)) return false; if (blocks_used > blocks_allocated) return false; if (blocks_used < 0 || blocks_allocated < 0) return false; *size = blocks_used * kBlockSize; return true; } bool LooksLikeVmsFileProtectionListingPart(const base::string16& input) { if (input.length() > 4) return false; // On VMS there are four different permission bits: Read, Write, Execute, // and Delete. They appear in that order in the permission listing. std::string pattern("RWED"); base::string16 match(input); while (!match.empty() && !pattern.empty()) { if (match[0] == pattern[0]) match = match.substr(1); pattern = pattern.substr(1); } return match.empty(); } bool LooksLikeVmsFileProtectionListing(const base::string16& input) { if (input.length() < 2) return false; if (input.front() != '(' || input.back() != ')') return false; // We expect four parts of the file protection listing: for System, Owner, // Group, and World. std::vector parts = base::SplitString( base::StringPiece16(input).substr(1, input.length() - 2), base::ASCIIToUTF16(","), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (parts.size() != 4) return false; return LooksLikeVmsFileProtectionListingPart(parts[0]) && LooksLikeVmsFileProtectionListingPart(parts[1]) && LooksLikeVmsFileProtectionListingPart(parts[2]) && LooksLikeVmsFileProtectionListingPart(parts[3]); } bool LooksLikeVmsUserIdentificationCode(const base::string16& input) { if (input.length() < 2) return false; return input.front() == '[' && input.back() == ']'; } bool LooksLikeVMSError(const base::string16& text) { static const char* const kPermissionDeniedMessages[] = { "%RMS-E-FNF", // File not found. "%RMS-E-PRV", // Access denied. "%SYSTEM-F-NOPRIV", "privilege", }; for (size_t i = 0; i < arraysize(kPermissionDeniedMessages); i++) { if (text.find(base::ASCIIToUTF16(kPermissionDeniedMessages[i])) != base::string16::npos) return true; } return false; } bool VmsDateListingToTime(const std::vector& columns, base::Time* time) { DCHECK_EQ(4U, columns.size()); base::Time::Exploded time_exploded = { 0 }; // Date should be in format DD-MMM-YYYY. std::vector date_parts = base::SplitStringPiece(columns[2], base::ASCIIToUTF16("-"), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (date_parts.size() != 3) return false; if (!base::StringToInt(date_parts[0], &time_exploded.day_of_month)) return false; if (!FtpUtil::AbbreviatedMonthToNumber(date_parts[1].as_string(), &time_exploded.month)) return false; if (!base::StringToInt(date_parts[2], &time_exploded.year)) return false; // Time can be in format HH:MM, HH:MM:SS, or HH:MM:SS.mm. Try to recognize the // last type first. Do not parse the seconds, they will be ignored anyway. base::string16 time_column(columns[3]); if (time_column.length() == 11 && time_column[8] == '.') time_column = time_column.substr(0, 8); if (time_column.length() == 8 && time_column[5] == ':') time_column = time_column.substr(0, 5); if (time_column.length() != 5) return false; std::vector time_parts = base::SplitStringPiece(time_column, base::ASCIIToUTF16(":"), base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (time_parts.size() != 2) return false; if (!base::StringToInt(time_parts[0], &time_exploded.hour)) return false; if (!base::StringToInt(time_parts[1], &time_exploded.minute)) return false; // We don't know the time zone of the server, so just use local time. *time = base::Time::FromLocalExploded(time_exploded); return true; } } // namespace bool ParseFtpDirectoryListingVms( const std::vector& lines, std::vector* entries) { // The first non-empty line is the listing header. It often // starts with "Directory ", but not always. We set a flag after // seing the header. bool seen_header = false; // Sometimes the listing doesn't end with a "Total" line, but // it's only okay when it contains some errors (it's needed // to distinguish it from "ls -l" format). bool seen_error = false; base::string16 total_of = base::ASCIIToUTF16("Total of "); base::char16 space[2] = { ' ', 0 }; for (size_t i = 0; i < lines.size(); i++) { if (lines[i].empty()) continue; if (base::StartsWith(lines[i], total_of, base::CompareCase::SENSITIVE)) { // After the "total" line, all following lines must be empty. for (size_t j = i + 1; j < lines.size(); j++) if (!lines[j].empty()) return false; return true; } if (!seen_header) { seen_header = true; continue; } if (LooksLikeVMSError(lines[i])) { seen_error = true; continue; } std::vector columns = base::SplitString( base::CollapseWhitespace(lines[i], false), space, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (columns.size() == 1) { // There can be no continuation if the current line is the last one. if (i == lines.size() - 1) return false; // Skip the next line. i++; // This refers to the continuation line. if (LooksLikeVMSError(lines[i])) { seen_error = true; continue; } // Join the current and next line and split them into columns. columns = base::SplitString( base::CollapseWhitespace( lines[i - 1] + space + lines[i], false), space, base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); } if (columns.empty()) return false; FtpDirectoryListingEntry entry; if (!ParseVmsFilename(columns[0], &entry.name, &entry.type)) return false; // There are different variants of a VMS listing. Some display // the protection listing and user identification code, some do not. if (columns.size() == 6) { if (!LooksLikeVmsFileProtectionListing(columns[5])) return false; if (!LooksLikeVmsUserIdentificationCode(columns[4])) return false; // Drop the unneeded data, so that the following code can always expect // just four columns. columns.resize(4); } if (columns.size() != 4) return false; if (!ParseVmsFilesize(columns[1], &entry.size)) return false; if (entry.type != FtpDirectoryListingEntry::FILE) entry.size = -1; if (!VmsDateListingToTime(columns, &entry.last_modified)) return false; entries->push_back(entry); } // The only place where we return true is after receiving the "Total" line, // that should be present in every VMS listing. Alternatively, if the listing // contains error messages, it's OK not to have the "Total" line. return seen_error; } } // namespace net