| // Copyright (c) 2010 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. |
| // |
| // This file defines helper methods used to schedule files for deletion |
| // on next reboot. The code here is heavily borrowed and simplified from |
| // http://code.google.com/p/omaha/source/browse/trunk/common/file.cc and |
| // http://code.google.com/p/omaha/source/browse/trunk/common/utils.cc |
| // |
| // This implementation really is not fast, so do not use it where that will |
| // matter. |
| |
| #include "chrome/installer/util/delete_after_reboot_helper.h" |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/file_util.h" |
| #include "base/files/file_enumerator.h" |
| #include "base/strings/string_util.h" |
| #include "base/win/registry.h" |
| |
| // The moves-pending-reboot is a MULTISZ registry key in the HKLM part of the |
| // registry. |
| const wchar_t kSessionManagerKey[] = |
| L"SYSTEM\\CurrentControlSet\\Control\\Session Manager"; |
| const wchar_t kPendingFileRenameOps[] = L"PendingFileRenameOperations"; |
| |
| namespace { |
| |
| // Returns true if this directory name is 'safe' for deletion (doesn't contain |
| // "..", doesn't specify a drive root) |
| bool IsSafeDirectoryNameForDeletion(const base::FilePath& dir_name) { |
| // empty name isn't allowed |
| if (dir_name.empty()) |
| return false; |
| |
| // require a character other than \/:. after the last : |
| // disallow anything with ".." |
| bool ok = false; |
| const wchar_t* dir_name_str = dir_name.value().c_str(); |
| for (const wchar_t* s = dir_name_str; *s; ++s) { |
| if (*s != L'\\' && *s != L'/' && *s != L':' && *s != L'.') |
| ok = true; |
| if (*s == L'.' && s > dir_name_str && *(s - 1) == L'.') |
| return false; |
| if (*s == L':') |
| ok = false; |
| } |
| return ok; |
| } |
| |
| } // end namespace |
| |
| // Must only be called for regular files or directories that will be empty. |
| bool ScheduleFileSystemEntityForDeletion(const base::FilePath& path) { |
| // Check if the file exists, return false if not. |
| WIN32_FILE_ATTRIBUTE_DATA attrs = {0}; |
| if (!::GetFileAttributesEx(path.value().c_str(), |
| ::GetFileExInfoStandard, &attrs)) { |
| PLOG(WARNING) << path.value() << " does not exist."; |
| return false; |
| } |
| |
| DWORD flags = MOVEFILE_DELAY_UNTIL_REBOOT; |
| if (!base::DirectoryExists(path)) { |
| // This flag valid only for files |
| flags |= MOVEFILE_REPLACE_EXISTING; |
| } |
| |
| if (!::MoveFileEx(path.value().c_str(), NULL, flags)) { |
| PLOG(ERROR) << "Could not schedule " << path.value() << " for deletion."; |
| return false; |
| } |
| |
| #ifndef NDEBUG |
| // Useful debugging code to track down what files are in use. |
| if (flags & MOVEFILE_REPLACE_EXISTING) { |
| // Attempt to open the file exclusively. |
| HANDLE file = ::CreateFileW(path.value().c_str(), |
| GENERIC_READ | GENERIC_WRITE, 0, NULL, |
| OPEN_EXISTING, 0, NULL); |
| if (file != INVALID_HANDLE_VALUE) { |
| LOG(INFO) << " file not in use: " << path.value(); |
| ::CloseHandle(file); |
| } else { |
| PLOG(INFO) << " file in use (or not found?): " << path.value(); |
| } |
| } |
| #endif |
| |
| VLOG(1) << "Scheduled for deletion: " << path.value(); |
| return true; |
| } |
| |
| bool ScheduleDirectoryForDeletion(const base::FilePath& dir_name) { |
| if (!IsSafeDirectoryNameForDeletion(dir_name)) { |
| LOG(ERROR) << "Unsafe directory name for deletion: " << dir_name.value(); |
| return false; |
| } |
| |
| // Make sure the directory exists (it is ok if it doesn't) |
| DWORD dir_attributes = ::GetFileAttributes(dir_name.value().c_str()); |
| if (dir_attributes == INVALID_FILE_ATTRIBUTES) { |
| if (::GetLastError() == ERROR_FILE_NOT_FOUND) { |
| return true; // Ok if directory is missing |
| } else { |
| PLOG(ERROR) << "Could not GetFileAttributes for " << dir_name.value(); |
| return false; |
| } |
| } |
| // Confirm it is a directory |
| if (!(dir_attributes & FILE_ATTRIBUTE_DIRECTORY)) { |
| LOG(ERROR) << "Scheduled directory is not a directory: " |
| << dir_name.value(); |
| return false; |
| } |
| |
| // First schedule all the normal files for deletion. |
| { |
| bool success = true; |
| base::FileEnumerator file_enum(dir_name, false, |
| base::FileEnumerator::FILES); |
| for (base::FilePath file = file_enum.Next(); !file.empty(); |
| file = file_enum.Next()) { |
| success = ScheduleFileSystemEntityForDeletion(file); |
| if (!success) { |
| LOG(ERROR) << "Failed to schedule file for deletion: " << file.value(); |
| return false; |
| } |
| } |
| } |
| |
| // Then recurse to all the subdirectories. |
| { |
| bool success = true; |
| base::FileEnumerator dir_enum(dir_name, false, |
| base::FileEnumerator::DIRECTORIES); |
| for (base::FilePath sub_dir = dir_enum.Next(); !sub_dir.empty(); |
| sub_dir = dir_enum.Next()) { |
| success = ScheduleDirectoryForDeletion(sub_dir); |
| if (!success) { |
| LOG(ERROR) << "Failed to schedule subdirectory for deletion: " |
| << sub_dir.value(); |
| return false; |
| } |
| } |
| } |
| |
| // Now schedule the empty directory itself |
| if (!ScheduleFileSystemEntityForDeletion(dir_name)) { |
| LOG(ERROR) << "Failed to schedule directory for deletion: " |
| << dir_name.value(); |
| } |
| |
| return true; |
| } |
| |
| // Converts the strings found in |buffer| to a list of wstrings that is returned |
| // in |value|. |
| // |buffer| points to a series of pairs of null-terminated wchar_t strings |
| // followed by a terminating null character. |
| // |byte_count| is the length of |buffer| in bytes. |
| // |value| is a pointer to an empty vector of wstrings. On success, this vector |
| // contains all of the strings extracted from |buffer|. |
| // Returns S_OK on success, E_INVALIDARG if buffer does not meet tha above |
| // specification. |
| HRESULT MultiSZBytesToStringArray(const char* buffer, size_t byte_count, |
| std::vector<PendingMove>* value) { |
| DCHECK(buffer); |
| DCHECK(value); |
| DCHECK(value->empty()); |
| |
| DWORD data_len = byte_count / sizeof(wchar_t); |
| const wchar_t* data = reinterpret_cast<const wchar_t*>(buffer); |
| const wchar_t* data_end = data + data_len; |
| if (data_len > 1) { |
| // must be terminated by two null characters |
| if (data[data_len - 1] != 0 || data[data_len - 2] != 0) { |
| DLOG(ERROR) << "Invalid MULTI_SZ found."; |
| return E_INVALIDARG; |
| } |
| |
| // put null-terminated strings into arrays |
| while (data < data_end) { |
| std::wstring str_from(data); |
| data += str_from.length() + 1; |
| if (data < data_end) { |
| std::wstring str_to(data); |
| data += str_to.length() + 1; |
| value->push_back(std::make_pair(str_from, str_to)); |
| } |
| } |
| } |
| return S_OK; |
| } |
| |
| void StringArrayToMultiSZBytes(const std::vector<PendingMove>& strings, |
| std::vector<char>* buffer) { |
| DCHECK(buffer); |
| buffer->clear(); |
| |
| if (strings.empty()) { |
| // Leave buffer empty if we have no strings. |
| return; |
| } |
| |
| size_t total_wchars = 0; |
| { |
| std::vector<PendingMove>::const_iterator iter(strings.begin()); |
| for (; iter != strings.end(); ++iter) { |
| total_wchars += iter->first.length(); |
| total_wchars++; // Space for the null char. |
| total_wchars += iter->second.length(); |
| total_wchars++; // Space for the null char. |
| } |
| total_wchars++; // Space for the extra terminating null char. |
| } |
| |
| size_t total_length = total_wchars * sizeof(wchar_t); |
| buffer->resize(total_length); |
| wchar_t* write_pointer = reinterpret_cast<wchar_t*>(&((*buffer)[0])); |
| // Keep an end pointer around for sanity checking. |
| wchar_t* end_pointer = write_pointer + total_wchars; |
| |
| std::vector<PendingMove>::const_iterator copy_iter(strings.begin()); |
| for (; copy_iter != strings.end() && write_pointer < end_pointer; |
| copy_iter++) { |
| // First copy the source string. |
| size_t string_length = copy_iter->first.length() + 1; |
| memcpy(write_pointer, copy_iter->first.c_str(), |
| string_length * sizeof(wchar_t)); |
| write_pointer += string_length; |
| // Now copy the destination string. |
| string_length = copy_iter->second.length() + 1; |
| memcpy(write_pointer, copy_iter->second.c_str(), |
| string_length * sizeof(wchar_t)); |
| write_pointer += string_length; |
| |
| // We should never run off the end while in this loop. |
| DCHECK(write_pointer < end_pointer); |
| } |
| *write_pointer = L'\0'; // Explicitly set the final null char. |
| DCHECK(++write_pointer == end_pointer); |
| } |
| |
| base::FilePath GetShortPathName(const base::FilePath& path) { |
| std::wstring short_path; |
| DWORD length = GetShortPathName(path.value().c_str(), |
| WriteInto(&short_path, MAX_PATH), |
| MAX_PATH); |
| DWORD last_error = ::GetLastError(); |
| DLOG_IF(WARNING, length == 0 && last_error != ERROR_PATH_NOT_FOUND) |
| << __FUNCTION__ << " gle=" << last_error; |
| if (length == 0) { |
| // GetShortPathName fails if the path is no longer present. Instead of |
| // returning an empty string, just return the original string. This will |
| // serve our purposes. |
| return path; |
| } |
| |
| short_path.resize(length); |
| return base::FilePath(short_path); |
| } |
| |
| HRESULT GetPendingMovesValue(std::vector<PendingMove>* pending_moves) { |
| DCHECK(pending_moves); |
| pending_moves->clear(); |
| |
| // Get the current value of the key |
| // If the Key is missing, that's totally acceptable. |
| base::win::RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, |
| KEY_QUERY_VALUE); |
| HKEY session_manager_handle = session_manager_key.Handle(); |
| if (!session_manager_handle) |
| return HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); |
| |
| // The base::RegKey Read code squashes the return code from |
| // ReqQueryValueEx, we have to do things ourselves: |
| DWORD buffer_size = 0; |
| std::vector<char> buffer; |
| buffer.resize(1); |
| DWORD type; |
| DWORD result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, |
| 0, &type, reinterpret_cast<BYTE*>(&buffer[0]), |
| &buffer_size); |
| |
| if (result == ERROR_FILE_NOT_FOUND) { |
| // No pending moves were found. |
| return HRESULT_FROM_WIN32(result); |
| } |
| if (result != ERROR_MORE_DATA) { |
| // That was unexpected. |
| DLOG(ERROR) << "Unexpected result from RegQueryValueEx: " << result; |
| return HRESULT_FROM_WIN32(result); |
| } |
| if (type != REG_MULTI_SZ) { |
| DLOG(ERROR) << "Found PendingRename value of unexpected type."; |
| return E_UNEXPECTED; |
| } |
| if (buffer_size % 2) { |
| // The buffer size should be an even number (since we expect wchar_ts). |
| // If this is not the case, fail here. |
| DLOG(ERROR) << "Corrupt PendingRename value."; |
| return E_UNEXPECTED; |
| } |
| |
| // There are pending file renames. Read them in. |
| buffer.resize(buffer_size); |
| result = RegQueryValueEx(session_manager_handle, kPendingFileRenameOps, |
| 0, &type, reinterpret_cast<LPBYTE>(&buffer[0]), |
| &buffer_size); |
| if (result != ERROR_SUCCESS) { |
| DLOG(ERROR) << "Failed to read from " << kPendingFileRenameOps; |
| return HRESULT_FROM_WIN32(result); |
| } |
| |
| // We now have a buffer of bytes that is actually a sequence of |
| // null-terminated wchar_t strings terminated by an additional null character. |
| // Stick this into a vector of strings for clarity. |
| HRESULT hr = MultiSZBytesToStringArray(&buffer[0], buffer.size(), |
| pending_moves); |
| return hr; |
| } |
| |
| bool MatchPendingDeletePath(const base::FilePath& short_form_needle, |
| const base::FilePath& reg_path) { |
| // Stores the path stored in each entry. |
| std::wstring match_path(reg_path.value()); |
| |
| // First chomp the prefix since that will mess up GetShortPathName. |
| std::wstring prefix(L"\\??\\"); |
| if (StartsWith(match_path, prefix, false)) |
| match_path = match_path.substr(4); |
| |
| // Get the short path name of the entry. |
| base::FilePath short_match_path(GetShortPathName(base::FilePath(match_path))); |
| |
| // Now compare the paths. If it isn't one we're looking for, add it |
| // to the list to keep. |
| return StartsWith(short_match_path.value(), short_form_needle.value(), false); |
| } |
| |
| // Removes all pending moves for the given |directory| and any contained |
| // files or subdirectories. Returns true on success |
| bool RemoveFromMovesPendingReboot(const base::FilePath& directory) { |
| std::vector<PendingMove> pending_moves; |
| HRESULT hr = GetPendingMovesValue(&pending_moves); |
| if (hr == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)) { |
| // No pending moves, nothing to do. |
| return true; |
| } |
| if (FAILED(hr)) { |
| // Couldn't read the key or the key was corrupt. |
| return false; |
| } |
| |
| // Get the short form of |directory| and use that to match. |
| base::FilePath short_directory(GetShortPathName(directory)); |
| |
| std::vector<PendingMove> strings_to_keep; |
| for (std::vector<PendingMove>::const_iterator iter(pending_moves.begin()); |
| iter != pending_moves.end(); ++iter) { |
| base::FilePath move_path(iter->first); |
| if (!MatchPendingDeletePath(short_directory, move_path)) { |
| // This doesn't match the deletions we are looking for. Preserve |
| // this string pair, making sure that it is in fact a pair. |
| strings_to_keep.push_back(*iter); |
| } |
| } |
| |
| if (strings_to_keep.size() == pending_moves.size()) { |
| // Nothing to remove, return true. |
| return true; |
| } |
| |
| // Write the key back into a buffer. |
| base::win::RegKey session_manager_key(HKEY_LOCAL_MACHINE, kSessionManagerKey, |
| KEY_CREATE_SUB_KEY | KEY_SET_VALUE); |
| if (!session_manager_key.Handle()) { |
| // Couldn't open / create the key. |
| LOG(ERROR) << "Failed to open session manager key for writing."; |
| return false; |
| } |
| |
| if (strings_to_keep.size() <= 1) { |
| // We have only the trailing NULL string. Don't bother writing that. |
| return (session_manager_key.DeleteValue(kPendingFileRenameOps) == |
| ERROR_SUCCESS); |
| } |
| std::vector<char> buffer; |
| StringArrayToMultiSZBytes(strings_to_keep, &buffer); |
| DCHECK_GT(buffer.size(), 0U); |
| if (buffer.empty()) |
| return false; |
| return (session_manager_key.WriteValue(kPendingFileRenameOps, &buffer[0], |
| buffer.size(), REG_MULTI_SZ) == ERROR_SUCCESS); |
| } |