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 "components/download/public/common/base_file.h"
6
7 #include <windows.h>
8
9 #include <objbase.h>
10 #include <shellapi.h>
11 #include <shobjidl.h>
12 #include <wrl/client.h>
13 #include <wrl/implements.h>
14
15 #include "base/threading/scoped_blocking_call.h"
16 #include "base/win/com_init_util.h"
17 #include "components/download/public/common/download_interrupt_reasons_utils.h"
18 #include "components/download/public/common/download_stats.h"
19
20 namespace download {
21 namespace {
22
23 // By and large the errors seen here are listed in sherrors.h, included from
24 // shobjidl.h.
HRESULTToDownloadInterruptReason(HRESULT hr)25 DownloadInterruptReason HRESULTToDownloadInterruptReason(HRESULT hr) {
26 // S_OK, other success values are aggregated here.
27 if (SUCCEEDED(hr) && HRESULT_FACILITY(hr) != FACILITY_SHELL)
28 return DOWNLOAD_INTERRUPT_REASON_NONE;
29
30 DownloadInterruptReason reason = DOWNLOAD_INTERRUPT_REASON_NONE;
31 // All of the remaining HRESULTs to be considered are either from the copy
32 // engine, or are unknown; we've got handling for all the copy engine errors,
33 // and otherwise we'll just return the generic error reason.
34 switch (hr) {
35 case COPYENGINE_S_YES:
36 case COPYENGINE_S_NOT_HANDLED:
37 case COPYENGINE_S_USER_RETRY:
38 case COPYENGINE_S_MERGE:
39 case COPYENGINE_S_DONT_PROCESS_CHILDREN:
40 case COPYENGINE_S_ALREADY_DONE:
41 case COPYENGINE_S_PENDING:
42 case COPYENGINE_S_KEEP_BOTH:
43 case COPYENGINE_S_COLLISIONRESOLVED:
44 case COPYENGINE_S_PROGRESS_PAUSE:
45 return DOWNLOAD_INTERRUPT_REASON_NONE;
46
47 case COPYENGINE_S_CLOSE_PROGRAM:
48 // Like sharing violations, another process is using the file we want to
49 // touch, so wait for it to close.
50 case COPYENGINE_E_SHARING_VIOLATION_SRC:
51 case COPYENGINE_E_SHARING_VIOLATION_DEST:
52 // Sharing violations are encountered when some other process has a file
53 // open; often it's antivirus scanning, and this error can be treated as
54 // transient, as we assume eventually the other process will close its
55 // handle.
56 reason = DOWNLOAD_INTERRUPT_REASON_FILE_TRANSIENT_ERROR;
57 break;
58
59 case COPYENGINE_E_PATH_TOO_DEEP_DEST:
60 case COPYENGINE_E_PATH_TOO_DEEP_SRC:
61 case COPYENGINE_E_NEWFILE_NAME_TOO_LONG:
62 case COPYENGINE_E_NEWFOLDER_NAME_TOO_LONG:
63 // Any of these errors can be encountered if MAXPATH is hit while writing
64 // out a filename. This can happen really just about anywhere.
65 reason = DOWNLOAD_INTERRUPT_REASON_FILE_NAME_TOO_LONG;
66 break;
67
68 case COPYENGINE_S_USER_IGNORED:
69 // On Windows 7, inability to access a file may return "user ignored"
70 // instead of correctly reporting the failure.
71 case COPYENGINE_E_ACCESS_DENIED_DEST:
72 case COPYENGINE_E_ACCESS_DENIED_SRC:
73 // There's a security problem, or the file is otherwise inaccessible.
74 case COPYENGINE_E_DEST_IS_RO_CD:
75 case COPYENGINE_E_DEST_IS_RW_CD:
76 case COPYENGINE_E_DEST_IS_R_CD:
77 case COPYENGINE_E_DEST_IS_RO_DVD:
78 case COPYENGINE_E_DEST_IS_RW_DVD:
79 case COPYENGINE_E_DEST_IS_R_DVD:
80 case COPYENGINE_E_SRC_IS_RO_CD:
81 case COPYENGINE_E_SRC_IS_RW_CD:
82 case COPYENGINE_E_SRC_IS_R_CD:
83 case COPYENGINE_E_SRC_IS_RO_DVD:
84 case COPYENGINE_E_SRC_IS_RW_DVD:
85 case COPYENGINE_E_SRC_IS_R_DVD:
86 // When the source is actually a disk, and a Move is attempted, it can't
87 // delete the source. This is unlikely to be encountered in our scenario.
88 reason = DOWNLOAD_INTERRUPT_REASON_FILE_ACCESS_DENIED;
89 break;
90
91 case COPYENGINE_E_FILE_TOO_LARGE:
92 case COPYENGINE_E_DISK_FULL:
93 case COPYENGINE_E_REMOVABLE_FULL:
94 case COPYENGINE_E_DISK_FULL_CLEAN:
95 // No room for the file in the destination location.
96 reason = DOWNLOAD_INTERRUPT_REASON_FILE_TOO_LARGE;
97 break;
98
99 case COPYENGINE_E_ALREADY_EXISTS_NORMAL:
100 case COPYENGINE_E_ALREADY_EXISTS_READONLY:
101 case COPYENGINE_E_ALREADY_EXISTS_SYSTEM:
102 case COPYENGINE_E_ALREADY_EXISTS_FOLDER:
103 // The destination already exists and can't be replaced.
104 case COPYENGINE_E_INVALID_FILES_SRC:
105 case COPYENGINE_E_INVALID_FILES_DEST:
106 // Either the source or destination file was invalid.
107 case COPYENGINE_E_STREAM_LOSS:
108 case COPYENGINE_E_EA_LOSS:
109 case COPYENGINE_E_PROPERTY_LOSS:
110 case COPYENGINE_E_PROPERTIES_LOSS:
111 case COPYENGINE_E_ENCRYPTION_LOSS:
112 // The destination can't support some functionality that the file needs.
113 // The interesting one here is E_STREAM_LOSS, especially with MOTW.
114 case COPYENGINE_E_FLD_IS_FILE_DEST:
115 case COPYENGINE_E_FILE_IS_FLD_DEST:
116 // There is an existing file with the same name as a new folder, and
117 // vice versa.
118 case COPYENGINE_E_ROOT_DIR_DEST:
119 case COPYENGINE_E_ROOT_DIR_SRC:
120 case COPYENGINE_E_DIFF_DIR:
121 case COPYENGINE_E_SAME_FILE:
122 case COPYENGINE_E_MANY_SRC_1_DEST:
123 case COPYENGINE_E_DEST_SUBTREE:
124 case COPYENGINE_E_DEST_SAME_TREE:
125 case COPYENGINE_E_USER_CANCELLED:
126 case COPYENGINE_E_CANCELLED:
127 case COPYENGINE_E_REQUIRES_ELEVATION:
128 reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
129 break;
130 }
131
132 if (reason != DOWNLOAD_INTERRUPT_REASON_NONE) {
133 RecordWinFileMoveError(HRESULT_CODE(hr));
134 return reason;
135 }
136
137 // Copy operations may still return Win32 error codes, so handle those here.
138 if (HRESULT_FACILITY(hr) == FACILITY_WIN32) {
139 return ConvertFileErrorToInterruptReason(
140 base::File::OSErrorToFileError(HRESULT_CODE(hr)));
141 }
142
143 return DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
144 }
145
146 class FileOperationProgressSink
147 : public Microsoft::WRL::RuntimeClass<
148 Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
149 IFileOperationProgressSink> {
150 public:
151 FileOperationProgressSink() = default;
152
GetOperationResult()153 HRESULT GetOperationResult() { return result_; }
154
155 // IFileOperationProgressSink:
FinishOperations(HRESULT hr)156 IFACEMETHODIMP FinishOperations(HRESULT hr) override {
157 // If a failure has already been captured, don't bother overriding it. That
158 // way, the original failure can be propagated; in the event that the new
159 // HRESULT is also a success, overwriting will not harm anything and
160 // captures the final state of the whole operation.
161 if (SUCCEEDED(result_))
162 result_ = hr;
163 return S_OK;
164 }
165
PauseTimer()166 IFACEMETHODIMP PauseTimer() override { return S_OK; }
PostCopyItem(DWORD,IShellItem *,IShellItem *,PCWSTR,HRESULT,IShellItem *)167 IFACEMETHODIMP PostCopyItem(DWORD,
168 IShellItem*,
169 IShellItem*,
170 PCWSTR,
171 HRESULT,
172 IShellItem*) override {
173 return E_NOTIMPL;
174 }
PostDeleteItem(DWORD,IShellItem *,HRESULT,IShellItem *)175 IFACEMETHODIMP PostDeleteItem(DWORD,
176 IShellItem*,
177 HRESULT,
178 IShellItem*) override {
179 return E_NOTIMPL;
180 }
PostMoveItem(DWORD,IShellItem *,IShellItem *,PCWSTR,HRESULT hr,IShellItem *)181 IFACEMETHODIMP PostMoveItem(DWORD,
182 IShellItem*,
183 IShellItem*,
184 PCWSTR,
185 HRESULT hr,
186 IShellItem*) override {
187 // Like in FinishOperations, overwriting with a different success value
188 // does not have a negative impact, but replacing an existing failure will
189 // cause issues.
190 if (SUCCEEDED(result_))
191 result_ = hr;
192 return S_OK;
193 }
PostNewItem(DWORD,IShellItem *,PCWSTR,PCWSTR,DWORD,HRESULT,IShellItem *)194 IFACEMETHODIMP PostNewItem(DWORD,
195 IShellItem*,
196 PCWSTR,
197 PCWSTR,
198 DWORD,
199 HRESULT,
200 IShellItem*) override {
201 return E_NOTIMPL;
202 }
203 IFACEMETHODIMP
PostRenameItem(DWORD,IShellItem *,PCWSTR,HRESULT,IShellItem *)204 PostRenameItem(DWORD, IShellItem*, PCWSTR, HRESULT, IShellItem*) override {
205 return E_NOTIMPL;
206 }
PreCopyItem(DWORD,IShellItem *,IShellItem *,PCWSTR)207 IFACEMETHODIMP PreCopyItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override {
208 return E_NOTIMPL;
209 }
PreDeleteItem(DWORD,IShellItem *)210 IFACEMETHODIMP PreDeleteItem(DWORD, IShellItem*) override {
211 return E_NOTIMPL;
212 }
PreMoveItem(DWORD,IShellItem *,IShellItem *,PCWSTR)213 IFACEMETHODIMP PreMoveItem(DWORD, IShellItem*, IShellItem*, PCWSTR) override {
214 return S_OK;
215 }
PreNewItem(DWORD,IShellItem *,PCWSTR)216 IFACEMETHODIMP PreNewItem(DWORD, IShellItem*, PCWSTR) override {
217 return E_NOTIMPL;
218 }
PreRenameItem(DWORD,IShellItem *,PCWSTR)219 IFACEMETHODIMP PreRenameItem(DWORD, IShellItem*, PCWSTR) override {
220 return E_NOTIMPL;
221 }
ResetTimer()222 IFACEMETHODIMP ResetTimer() override { return S_OK; }
ResumeTimer()223 IFACEMETHODIMP ResumeTimer() override { return S_OK; }
StartOperations()224 IFACEMETHODIMP StartOperations() override { return S_OK; }
UpdateProgress(UINT,UINT)225 IFACEMETHODIMP UpdateProgress(UINT, UINT) override { return S_OK; }
226
227 protected:
228 ~FileOperationProgressSink() override = default;
229
230 private:
231 HRESULT result_ = S_OK;
232
233 DISALLOW_COPY_AND_ASSIGN(FileOperationProgressSink);
234 };
235
236 } // namespace
237
238 // Renames a file using IFileOperation::MoveItem() to ensure that the target
239 // file gets the correct default security descriptor in the new path.
240 // Returns a network error, or net::OK for success.
MoveFileAndAdjustPermissions(const base::FilePath & new_path)241 DownloadInterruptReason BaseFile::MoveFileAndAdjustPermissions(
242 const base::FilePath& new_path) {
243 base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
244 base::BlockingType::MAY_BLOCK);
245
246 base::win::AssertComInitialized();
247 Microsoft::WRL::ComPtr<IShellItem> original_path;
248 HRESULT hr = SHCreateItemFromParsingName(full_path_.value().c_str(), nullptr,
249 IID_PPV_ARGS(&original_path));
250
251 // |new_path| can be broken down to provide the new folder, as well as the
252 // new filename. We'll start with the folder, which the caller should ensure
253 // exists.
254 Microsoft::WRL::ComPtr<IShellItem> destination_folder;
255 if (SUCCEEDED(hr)) {
256 hr =
257 SHCreateItemFromParsingName(new_path.DirName().value().c_str(), nullptr,
258 IID_PPV_ARGS(&destination_folder));
259 }
260
261 Microsoft::WRL::ComPtr<IFileOperation> file_operation;
262 if (SUCCEEDED(hr)) {
263 hr = CoCreateInstance(CLSID_FileOperation, nullptr, CLSCTX_INPROC_SERVER,
264 IID_PPV_ARGS(&file_operation));
265 }
266
267 if (SUCCEEDED(hr)) {
268 // Don't show any UI, don't migrate security attributes (use the
269 // destination's attributes), and stop on first error-retaining the original
270 // failure reason.
271 hr = file_operation->SetOperationFlags(
272 FOF_NO_UI | FOF_NOCOPYSECURITYATTRIBS | FOFX_EARLYFAILURE);
273 }
274
275 Microsoft::WRL::ComPtr<FileOperationProgressSink> sink =
276 Microsoft::WRL::Make<FileOperationProgressSink>();
277 if (SUCCEEDED(hr)) {
278 hr = file_operation->MoveItem(original_path.Get(), destination_folder.Get(),
279 new_path.BaseName().value().c_str(),
280 sink.Get());
281 }
282
283 if (SUCCEEDED(hr))
284 hr = file_operation->PerformOperations();
285
286 if (SUCCEEDED(hr))
287 hr = sink->GetOperationResult();
288
289 // Convert HRESULT to DownloadInterruptReason.
290 DownloadInterruptReason interrupt_reason =
291 HRESULTToDownloadInterruptReason(hr);
292
293 if (interrupt_reason == DOWNLOAD_INTERRUPT_REASON_NONE) {
294 // The operation could still have been aborted; we can't get a better reason
295 // at this point, but we've got more information to go by.
296 BOOL any_operations_aborted = TRUE;
297 file_operation->GetAnyOperationsAborted(&any_operations_aborted);
298 if (any_operations_aborted)
299 interrupt_reason = DOWNLOAD_INTERRUPT_REASON_FILE_FAILED;
300 } else {
301 return LogInterruptReason("IFileOperation::MoveItem", hr, interrupt_reason);
302 }
303
304 return interrupt_reason;
305 }
306
307 } // namespace download
308