chromium/components/download/internal/common/base_file_win.cc

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "components/download/public/common/base_file.h"

#include <objbase.h>

#include <shobjidl.h>
#include <windows.h>

#include <shellapi.h>
#include <wrl/client.h>
#include <wrl/implements.h>

#include "base/threading/scoped_blocking_call.h"
#include "base/win/com_init_util.h"
#include "components/download/public/common/download_interrupt_reasons_utils.h"
#include "components/download/public/common/download_stats.h"

namespace download {
namespace {

// By and large the errors seen here are listed in sherrors.h, included from
// shobjidl.h.
DownloadInterruptReason HRESULTToDownloadInterruptReason(HRESULT hr) {
  // S_OK, other success values are aggregated here.
  if (SUCCEEDED(hr) && HRESULT_FACILITY(hr) != FACILITY_SHELL)
    return DOWNLOAD_INTERRUPT_REASON_NONE;

  DownloadInterruptReason reason = DOWNLOAD_INTERRUPT_REASON_NONE;
  // All of the remaining HRESULTs to be considered are either from the copy
  // engine, or are unknown; we've got handling for all the copy engine errors,
  // and otherwise we'll just return the generic error reason.
  switch (hr) {
    case COPYENGINE_S_YES:
    case COPYENGINE_S_NOT_HANDLED:
    case COPYENGINE_S_USER_RETRY:
    case COPYENGINE_S_MERGE:
    case COPYENGINE_S_DONT_PROCESS_CHILDREN:
    case COPYENGINE_S_ALREADY_DONE:
    case COPYENGINE_S_PENDING:
    case COPYENGINE_S_KEEP_BOTH:
    case COPYENGINE_S_COLLISIONRESOLVED:
    case COPYENGINE_S_PROGRESS_PAUSE:
      return DOWNLOAD_INTERRUPT_REASON_NONE;

    case COPYENGINE_S_CLOSE_PROGRAM:
      // Like sharing violations, another process is using the file we want to
      // touch, so wait for it to close.
    case COPYENGINE_E_SHARING_VIOLATION_SRC:
    case COPYENGINE_E_SHARING_VIOLATION_DEST:
      // Sharing violations are encountered when some other process has a file
      // open; often it's antivirus scanning, and this error can be treated as
      // transient, as we assume eventually the other process will close its
      // handle.
      reason = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
      break;

    case COPYENGINE_E_PATH_TOO_DEEP_DEST:
    case COPYENGINE_E_PATH_TOO_DEEP_SRC:
    case COPYENGINE_E_NEWFILE_NAME_TOO_LONG:
    case COPYENGINE_E_NEWFOLDER_NAME_TOO_LONG:
      // Any of these errors can be encountered if MAXPATH is hit while writing
      // out a filename. This can happen really just about anywhere.
      reason = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
      break;

    case COPYENGINE_S_USER_IGNORED:
      // On Windows 7, inability to access a file may return "user ignored"
      // instead of correctly reporting the failure.
    case COPYENGINE_E_ACCESS_DENIED_DEST:
    case COPYENGINE_E_ACCESS_DENIED_SRC:
      // There's a security problem, or the file is otherwise inaccessible.
    case COPYENGINE_E_DEST_IS_RO_CD:
    case COPYENGINE_E_DEST_IS_RW_CD:
    case COPYENGINE_E_DEST_IS_R_CD:
    case COPYENGINE_E_DEST_IS_RO_DVD:
    case COPYENGINE_E_DEST_IS_RW_DVD:
    case COPYENGINE_E_DEST_IS_R_DVD:
    case COPYENGINE_E_SRC_IS_RO_CD:
    case COPYENGINE_E_SRC_IS_RW_CD:
    case COPYENGINE_E_SRC_IS_R_CD:
    case COPYENGINE_E_SRC_IS_RO_DVD:
    case COPYENGINE_E_SRC_IS_RW_DVD:
    case COPYENGINE_E_SRC_IS_R_DVD:
      // When the source is actually a disk, and a Move is attempted, it can't
      // delete the source. This is unlikely to be encountered in our scenario.
      reason = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
      break;

    case COPYENGINE_E_FILE_TOO_LARGE:
    case COPYENGINE_E_DISK_FULL:
    case COPYENGINE_E_REMOVABLE_FULL:
    case COPYENGINE_E_DISK_FULL_CLEAN:
      // No room for the file in the destination location.
      reason = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE;
      break;

    case COPYENGINE_E_ALREADY_EXISTS_NORMAL:
    case COPYENGINE_E_ALREADY_EXISTS_READONLY:
    case COPYENGINE_E_ALREADY_EXISTS_SYSTEM:
    case COPYENGINE_E_ALREADY_EXISTS_FOLDER:
      // The destination already exists and can't be replaced.
    case COPYENGINE_E_INVALID_FILES_SRC:
    case COPYENGINE_E_INVALID_FILES_DEST:
      // Either the source or destination file was invalid.
    case COPYENGINE_E_STREAM_LOSS:
    case COPYENGINE_E_EA_LOSS:
    case COPYENGINE_E_PROPERTY_LOSS:
    case COPYENGINE_E_PROPERTIES_LOSS:
    case COPYENGINE_E_ENCRYPTION_LOSS:
      // The destination can't support some functionality that the file needs.
      // The interesting one here is E_STREAM_LOSS, especially with MOTW.
    case COPYENGINE_E_FLD_IS_FILE_DEST:
    case COPYENGINE_E_FILE_IS_FLD_DEST:
      // There is an existing file with the same name as a new folder, and
      // vice versa.
    case COPYENGINE_E_ROOT_DIR_DEST:
    case COPYENGINE_E_ROOT_DIR_SRC:
    case COPYENGINE_E_DIFF_DIR:
    case COPYENGINE_E_SAME_FILE:
    case COPYENGINE_E_MANY_SRC_1_DEST:
    case COPYENGINE_E_DEST_SUBTREE:
    case COPYENGINE_E_DEST_SAME_TREE:
    case COPYENGINE_E_USER_CANCELLED:
    case COPYENGINE_E_CANCELLED:
    case COPYENGINE_E_REQUIRES_ELEVATION:
      reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
      break;
  }

  if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) {
    return reason;
  }

  // Copy operations may still return Win32 error codes, so handle those here.
  if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
    return ConvertFileErrorToInterruptReason(
        base::File::OSErrorToFileError(HRESULT_CODE(hr)));
  }

  return DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
}

class FileOperationProgressSink
    : public Microsoft::WRL::RuntimeClass<
          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
          IFileOperationProgressSink> {
 public:
  FileOperationProgressSink() = default;

  FileOperationProgressSink(const FileOperationProgressSink&) = delete;
  FileOperationProgressSink& operator=(const FileOperationProgressSink&) =
      delete;

  HRESULT GetOperationResult() { return result_; }

  // IFileOperationProgressSink:
  IFACEMETHODIMP FinishOperations(HRESULT hr) override {
    // If a failure has already been captured, don't bother overriding it. That
    // way, the original failure can be propagated; in the event that the new
    // HRESULT is also a success, overwriting will not harm anything and
    // captures the final state of the whole operation.
    if (SUCCEEDED(result_))
      result_ = hr;
    return S_OK;
  }

  IFACEMETHODIMP PauseTimer() override { return S_OK; }
  IFACEMETHODIMP PostCopyItem(DWORD,
                              IShellItem*,
                              IShellItem*,
                              PCWSTR,
                              HRESULT,
                              IShellItem*) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PostDeleteItem(DWORD,
                                IShellItem*,
                                HRESULT,
                                IShellItem*) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PostMoveItem(DWORD,
                              IShellItem*,
                              IShellItem*,
                              PCWSTR,
                              HRESULT hr,
                              IShellItem*) override {
    // Like in FinishOperations, overwriting with a different success value
    // does not have a negative impact, but replacing an existing failure will
    // cause issues.
    if (SUCCEEDED(result_))
      result_ = hr;
    return S_OK;
  }
  IFACEMETHODIMP PostNewItem(DWORD,
                             IShellItem*,
                             PCWSTR,
                             PCWSTR,
                             DWORD,
                             HRESULT,
                             IShellItem*) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP
  PostRenameItem(DWORD, IShellItem*, PCWSTR, HRESULT, IShellItem*) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PreCopyItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PreDeleteItem(DWORD, IShellItem*) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PreMoveItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override {
    return S_OK;
  }
  IFACEMETHODIMP PreNewItem(DWORD, IShellItem*, PCWSTR) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP PreRenameItem(DWORD, IShellItem*, PCWSTR) override {
    return E_NOTIMPL;
  }
  IFACEMETHODIMP ResetTimer() override { return S_OK; }
  IFACEMETHODIMP ResumeTimer() override { return S_OK; }
  IFACEMETHODIMP StartOperations() override { return S_OK; }
  IFACEMETHODIMP UpdateProgress(UINT, UINT) override { return S_OK; }

 protected:
  ~FileOperationProgressSink() override = default;

 private:
  HRESULT result_ = S_OK;
};

}  // namespace

// Renames a file using IFileOperation::MoveItem() to ensure that the target
// file gets the correct default security descriptor in the new path.
// Returns a network error, or net::OK for success.
DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
    const base::FilePath& new_path) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  base::win::AssertComInitialized();
  Microsoft::WRL::ComPtr<IShellItem> original_path;
  HRESULT hr = SHCreateItemFromParsingName(full_path_.value().c_str(), nullptr,
                                           IID_PPV_ARGS(&original_path));

  // |new_path| can be broken down to provide the new folder, as well as the
  // new filename. We'll start with the folder, which the caller should ensure
  // exists.
  Microsoft::WRL::ComPtr<IShellItem> destination_folder;
  if (SUCCEEDED(hr)) {
    hr =
        SHCreateItemFromParsingName(new_path.DirName().value().c_str(), nullptr,
                                    IID_PPV_ARGS(&destination_folder));
  }

  Microsoft::WRL::ComPtr<IFileOperation> file_operation;
  if (SUCCEEDED(hr)) {
    hr = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_INPROC_SERVER,
                          IID_PPV_ARGS(&file_operation));
  }

  if (SUCCEEDED(hr)) {
    // Don't show any UI, don't migrate security attributes (use the
    // destination's attributes), and stop on first error-retaining the original
    // failure reason.
    hr = file_operation->SetOperationFlags(
        FOF_NO_UI | FOF_NOCOPYSECURITYATTRIBS | FOFX_EARLYFAILURE);
  }

  Microsoft::WRL::ComPtr<FileOperationProgressSink> sink =
      Microsoft::WRL::Make<FileOperationProgressSink>();
  if (SUCCEEDED(hr)) {
    hr = file_operation->MoveItem(original_path.Get(), destination_folder.Get(),
                                  new_path.BaseName().value().c_str(),
                                  sink.Get());
  }

  if (SUCCEEDED(hr))
    hr = file_operation->PerformOperations();

  if (SUCCEEDED(hr))
    hr = sink->GetOperationResult();

  // Convert HRESULT to DownloadInterruptReason.
  DownloadInterruptReason interrupt_reason =
      HRESULTToDownloadInterruptReason(hr);

  if (interrupt_reason == DOWNLOAD_INTERRUPT_REASON_NONE) {
    // The operation could still have been aborted; we can't get a better reason
    // at this point, but we've got more information to go by.
    BOOL any_operations_aborted = TRUE;
    file_operation->GetAnyOperationsAborted(&any_operations_aborted);
    if (any_operations_aborted)
      interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
  } else {
    return LogInterruptReason("IFileOperation::MoveItem", hr, interrupt_reason);
  }

  return interrupt_reason;
}

}  // namespace download