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/download_path_reservation_tracker.h"
6 
7 #include <stddef.h>
8 
9 #include <map>
10 #include <string>
11 
12 #include "base/bind.h"
13 #include "base/callback.h"
14 #include "base/files/file_path.h"
15 #include "base/files/file_util.h"
16 #include "base/logging.h"
17 #include "base/macros.h"
18 #include "base/memory/ptr_util.h"
19 #include "base/sequenced_task_runner.h"
20 #include "base/stl_util.h"
21 #include "base/strings/string_util.h"
22 #include "base/strings/stringprintf.h"
23 #include "base/task/lazy_thread_pool_task_runner.h"
24 #include "base/task/post_task.h"
25 #include "base/task_runner_util.h"
26 #include "base/third_party/icu/icu_utf.h"
27 #include "base/time/time.h"
28 #include "build/build_config.h"
29 #include "components/download/public/common/download_features.h"
30 #include "components/download/public/common/download_item.h"
31 #include "components/filename_generation/filename_generation.h"
32 #include "net/base/filename_util.h"
33 #include "url/gurl.h"
34 
35 #if defined(OS_ANDROID)
36 #include "components/download/internal/common/android/download_collection_bridge.h"
37 #endif
38 
39 namespace download {
40 
41 namespace {
42 
43 typedef DownloadItem* ReservationKey;
44 typedef std::map<ReservationKey, base::FilePath> ReservationMap;
45 
46 // The length of the suffix string we append for an intermediate file name.
47 // In the file name truncation, we keep the margin to append the suffix.
48 // TODO(kinaba): remove the margin. The user should be able to set maximum
49 // possible filename.
50 const size_t kIntermediateNameSuffixLength = sizeof(".crdownload") - 1;
51 
52 #if defined(OS_WIN)
53 // On windows, zone identifier is appended to the downloaded file name during
54 // annotation. That increases the length of the final target path.
55 const size_t kZoneIdentifierLength = sizeof(":Zone.Identifier") - 1;
56 #endif  // defined(OS_WIN)
57 
58 // Map of download path reservations. Each reserved path is associated with a
59 // ReservationKey=DownloadItem*. This object is destroyed in |Revoke()| when
60 // there are no more reservations.
61 ReservationMap* g_reservation_map = NULL;
62 
63 base::LazyThreadPoolSequencedTaskRunner g_sequenced_task_runner =
64     LAZY_THREAD_POOL_SEQUENCED_TASK_RUNNER_INITIALIZER(
65         base::TaskTraits(base::MayBlock()));
66 
67 // Observes a DownloadItem for changes to its target path and state. Updates or
68 // revokes associated download path reservations as necessary. Created, invoked
69 // and destroyed on the UI thread.
70 class DownloadItemObserver : public DownloadItem::Observer,
71                              public base::SupportsUserData::Data {
72  public:
73   explicit DownloadItemObserver(DownloadItem* download_item);
74   ~DownloadItemObserver() override;
75 
76  private:
77   // DownloadItem::Observer
78   void OnDownloadUpdated(DownloadItem* download) override;
79   void OnDownloadDestroyed(DownloadItem* download) override;
80 
81   DownloadItem* download_item_;
82 
83   // Last known target path for the download.
84   base::FilePath last_target_path_;
85 
86   static const int kUserDataKey;
87 
88   DISALLOW_COPY_AND_ASSIGN(DownloadItemObserver);
89 };
90 
91 // Returns true if the given path is in use by a path reservation.
IsPathReserved(const base::FilePath & path)92 bool IsPathReserved(const base::FilePath& path) {
93   // No reservation map => no reservations.
94   if (g_reservation_map == NULL)
95     return false;
96 
97   // We only expect a small number of concurrent downloads at any given time, so
98   // going through all of them shouldn't be too slow.
99   for (ReservationMap::const_iterator iter = g_reservation_map->begin();
100        iter != g_reservation_map->end(); ++iter) {
101     if (base::FilePath::CompareEqualIgnoreCase(iter->second.value(),
102                                                path.value()))
103       return true;
104   }
105   return false;
106 }
107 
108 // Returns true if the given file name is in use by any path reservation or the
109 // file system. Called on the task runner returned by
110 // DownloadPathReservationTracker::GetTaskRunner().
IsFileNameInUse(const base::FilePath & path)111 bool IsFileNameInUse(const base::FilePath& path) {
112 #if defined(OS_ANDROID)
113   // If there is a reservation, then the path is in use.
114   if (IsPathReserved(path.BaseName()))
115     return true;
116 
117   if (DownloadCollectionBridge::FileNameExists(path.BaseName()))
118     return true;
119 #endif
120   return false;
121 }
122 
123 // Returns true if the given path is in use by a path reservation,
124 // and has a different key then the item arg. Called on the task runner returned
125 // by DownloadPathReservationTracker::GetTaskRunner().
IsAdditionalPathReserved(const base::FilePath & path,DownloadItem * item)126 bool IsAdditionalPathReserved(const base::FilePath& path, DownloadItem* item) {
127   if (!g_reservation_map)
128     return false;
129 
130   for (ReservationMap::const_iterator iter = g_reservation_map->begin();
131        iter != g_reservation_map->end(); ++iter) {
132     if (iter->first != item && base::FilePath::CompareEqualIgnoreCase(
133                                    iter->second.value(), path.value()))
134       return true;
135   }
136   return false;
137 }
138 
139 // Returns true if the given path is in use by any path reservation or the
140 // file system. Called on the task runner returned by
141 // DownloadPathReservationTracker::GetTaskRunner().
IsPathInUse(const base::FilePath & path)142 bool IsPathInUse(const base::FilePath& path) {
143   // If there is a reservation, then the path is in use.
144   if (IsPathReserved(path))
145     return true;
146 
147   // If the path exists in the file system, then the path is in use.
148   if (base::PathExists(path))
149     return true;
150 
151   return false;
152 }
153 
154 // Create a unique filename by appending a uniquifier. Modifies |path| in place
155 // if successful and returns true. Otherwise |path| is left unmodified and
156 // returns false. If |check_file_name_only| is true, this method will only check
157 // the name of the file to guarantee that it is unique.
CreateUniqueFilename(int max_path_component_length,const base::Time & download_start_time,base::FilePath * path,bool check_file_name_only)158 bool CreateUniqueFilename(int max_path_component_length,
159                           const base::Time& download_start_time,
160                           base::FilePath* path,
161                           bool check_file_name_only) {
162   // Try every numeric uniquifier. Then make one attempt with the timestamp.
163   for (int uniquifier = 1;
164        uniquifier <= DownloadPathReservationTracker::kMaxUniqueFiles + 1;
165        ++uniquifier) {
166     // Append uniquifier.
167     std::string suffix(base::StringPrintf(" (%d)", uniquifier));
168 
169     // After we've tried all the unique numeric indices, make one attempt using
170     // the timestamp.
171     if (uniquifier > DownloadPathReservationTracker::kMaxUniqueFiles) {
172       // Generate an ISO8601 compliant local timestamp suffix that avoids
173       // reserved characters that are forbidden on some OSes like Windows.
174       base::Time::Exploded exploded;
175       download_start_time.LocalExplode(&exploded);
176       suffix = base::StringPrintf(
177           " - %04d-%02d-%02dT%02d%02d%02d.%03d", exploded.year, exploded.month,
178           exploded.day_of_month, exploded.hour, exploded.minute,
179           exploded.second, exploded.millisecond);
180     }
181 
182     base::FilePath path_to_check(*path);
183     // If the name length limit is available (max_length != -1), and the
184     // the current name exceeds the limit, truncate.
185     if (max_path_component_length != -1) {
186 #if defined(OS_WIN)
187       int limit =
188           max_path_component_length -
189           std::max(kIntermediateNameSuffixLength, kZoneIdentifierLength) -
190           suffix.size();
191 #else
192       int limit = max_path_component_length - kIntermediateNameSuffixLength -
193                   suffix.size();
194 #endif  // defined(OS_WIN)
195       // If truncation failed, give up uniquification.
196       if (limit <= 0 ||
197           !filename_generation::TruncateFilename(&path_to_check, limit))
198         break;
199     }
200     path_to_check = path_to_check.InsertBeforeExtensionASCII(suffix);
201 
202     if ((check_file_name_only && !IsFileNameInUse(path_to_check)) ||
203         (!check_file_name_only && !IsPathInUse(path_to_check))) {
204       *path = path_to_check;
205       return true;
206     }
207   }
208 
209   return false;
210 }
211 
212 struct CreateReservationInfo {
213   ReservationKey key;
214   base::FilePath source_path;
215   base::FilePath suggested_path;
216   base::FilePath default_download_path;
217   base::FilePath temporary_path;
218   base::FilePath fallback_directory;  // directory to use when target path
219                                       // cannot be used.
220   bool create_target_directory;
221   base::Time start_time;
222   DownloadPathReservationTracker::FilenameConflictAction conflict_action;
223 };
224 
225 // Check if |target_path| is writable.
IsPathWritable(const CreateReservationInfo & info,const base::FilePath & target_path)226 bool IsPathWritable(const CreateReservationInfo& info,
227                     const base::FilePath& target_path) {
228   if (base::PathIsWritable(target_path.DirName()))
229     return true;
230   // If a temporary file is already created under the same dir as |target_path|,
231   // return true. This is to avoid the windows network share issue. See
232   // http://crbug.com/383765.
233   return !info.temporary_path.empty() &&
234          info.temporary_path.DirName() == target_path.DirName();
235 }
236 
237 // Called when reservation conflicts happen. Returns the result on whether the
238 // conflict can be resolved, and uniquifying the file name if necessary. If
239 // |check_file_name_only| is true, this method will only check the name of the
240 // file to guarantee that it is unique.
ResolveReservationConflicts(const CreateReservationInfo & info,int max_path_component_length,base::FilePath * target_path,bool check_file_name_only)241 PathValidationResult ResolveReservationConflicts(
242     const CreateReservationInfo& info,
243     int max_path_component_length,
244     base::FilePath* target_path,
245     bool check_file_name_only) {
246   switch (info.conflict_action) {
247     case DownloadPathReservationTracker::UNIQUIFY:
248       return CreateUniqueFilename(max_path_component_length, info.start_time,
249                                   target_path, check_file_name_only)
250                  ? PathValidationResult::SUCCESS
251                  : PathValidationResult::CONFLICT;
252 
253     case DownloadPathReservationTracker::OVERWRITE:
254       if (IsPathReserved(*target_path))
255         return PathValidationResult::CONFLICT;
256       return PathValidationResult::SUCCESS;
257 
258     case DownloadPathReservationTracker::PROMPT:
259       return PathValidationResult::CONFLICT;
260   }
261   NOTREACHED();
262   return PathValidationResult::SUCCESS;
263 }
264 
265 // Verify that |target_path| can be written to and also resolve any conflicts if
266 // necessary by uniquifying the filename.
ValidatePathAndResolveConflicts(const CreateReservationInfo & info,base::FilePath * target_path)267 PathValidationResult ValidatePathAndResolveConflicts(
268     const CreateReservationInfo& info,
269     base::FilePath* target_path) {
270   // Check writability of the suggested path. If we can't write to it, use
271   // the |default_download_path| if it is not empty or |fallback_directory|.
272   // We'll prompt them in this case. No further amendments are made to the
273   // filename since the user is going to be prompted.
274   if (!IsPathWritable(info, *target_path)) {
275     DVLOG(1) << "Unable to write to path \"" << target_path->value() << "\"";
276     if (!info.default_download_path.empty() &&
277         target_path->DirName() != info.default_download_path) {
278       *target_path = info.default_download_path.Append(target_path->BaseName());
279     } else {
280       *target_path = info.fallback_directory.Append(target_path->BaseName());
281     }
282     return PathValidationResult::PATH_NOT_WRITABLE;
283   }
284 
285   int max_path_component_length =
286       base::GetMaximumPathComponentLength(target_path->DirName());
287   // Check the limit of file name length if it could be obtained. When the
288   // suggested name exceeds the limit, truncate or prompt the user.
289   if (max_path_component_length != -1) {
290 #if defined(OS_WIN)
291     int limit = max_path_component_length -
292                 std::max(kIntermediateNameSuffixLength, kZoneIdentifierLength);
293 #else
294     int limit = max_path_component_length - kIntermediateNameSuffixLength;
295 #endif  // defined(OS_WIN)
296     if (limit <= 0 ||
297         !filename_generation::TruncateFilename(target_path, limit))
298       return PathValidationResult::NAME_TOO_LONG;
299   }
300 
301   // Disallow downloading a file onto itself. Assume that downloading a file
302   // onto another file that differs only by case is not enough of a legitimate
303   // edge case to justify determining the case sensitivity of the underlying
304   // filesystem.
305   if (*target_path == info.source_path)
306     return PathValidationResult::SAME_AS_SOURCE;
307 
308   if (!IsPathInUse(*target_path))
309     return PathValidationResult::SUCCESS;
310 
311   return ResolveReservationConflicts(info, max_path_component_length,
312                                      target_path, false);
313 }
314 
315 // Called on the task runner returned by
316 // DownloadPathReservationTracker::GetTaskRunner() to reserve a download path.
317 // This method:
318 // - Creates directory |default_download_path| if it doesn't exist.
319 // - Verifies that the parent directory of |suggested_path| exists and is
320 //   writeable.
321 // - Truncates the suggested name if it exceeds the filesystem's limit.
322 // - Uniquifies |suggested_path| if |should_uniquify_path| is true.
323 // - Schedules |callback| on the UI thread with the reserved path and a flag
324 //   indicating whether the returned path has been successfully verified.
325 // - Returns the result of creating the path reservation.
CreateReservation(const CreateReservationInfo & info,base::FilePath * reserved_path)326 PathValidationResult CreateReservation(const CreateReservationInfo& info,
327                                        base::FilePath* reserved_path) {
328   // Create a reservation map if one doesn't exist. It will be automatically
329   // deleted when all the reservations are revoked.
330   if (g_reservation_map == NULL)
331     g_reservation_map = new ReservationMap;
332 
333   // Erase the reservation if it already exists. This can happen during
334   // automatic resumption where a new target determination request may be issued
335   // for a DownloadItem without an intervening transition to INTERRUPTED.
336   //
337   // Revoking and re-acquiring the reservation forces us to re-verify the claims
338   // we are making about the path.
339   g_reservation_map->erase(info.key);
340 
341   base::FilePath target_path(info.suggested_path.NormalizePathSeparators());
342   base::FilePath target_dir = target_path.DirName();
343   base::FilePath filename = target_path.BaseName();
344 
345 #if defined(OS_ANDROID)
346   if (DownloadCollectionBridge::ShouldPublishDownload(target_path)) {
347     // If the download is written to a content URI, put file name in the
348     // reservation map as content URIs will always be different.
349     PathValidationResult result = PathValidationResult::SUCCESS;
350     if (IsFileNameInUse(filename)) {
351       int max_path_component_length =
352           base::GetMaximumPathComponentLength(target_path.DirName());
353       result = ResolveReservationConflicts(info, max_path_component_length,
354                                            &target_path, true);
355     }
356     (*g_reservation_map)[info.key] = target_path.BaseName();
357     *reserved_path = target_path;
358     return result;
359   }
360 #endif
361   // Create target_dir if necessary and appropriate. target_dir may be the last
362   // directory that the user selected in a FilePicker; if that directory has
363   // since been removed, do NOT automatically re-create it. Only automatically
364   // create the directory if it is the default Downloads directory or if the
365   // caller explicitly requested automatic directory creation.
366   if (!base::DirectoryExists(target_dir) &&
367       (info.create_target_directory ||
368        (!info.default_download_path.empty() &&
369         (info.default_download_path == target_dir)))) {
370     base::CreateDirectory(target_dir);
371   }
372 
373   PathValidationResult result =
374       ValidatePathAndResolveConflicts(info, &target_path);
375   (*g_reservation_map)[info.key] = target_path;
376   *reserved_path = target_path;
377   return result;
378 }
379 
380 // Called on a background thread to update the path of the reservation
381 // associated with |key| to |new_path|.
UpdateReservation(ReservationKey key,const base::FilePath & new_path)382 void UpdateReservation(ReservationKey key, const base::FilePath& new_path) {
383   DCHECK(g_reservation_map != NULL);
384   auto iter = g_reservation_map->find(key);
385   if (iter != g_reservation_map->end()) {
386     iter->second = new_path;
387   } else {
388     // This would happen if an UpdateReservation() notification was scheduled on
389     // the SequencedTaskRunner before ReserveInternal(), or after a Revoke()
390     // call. Neither should happen.
391     NOTREACHED();
392   }
393 }
394 
395 // Called on the FILE thread to remove the path reservation associated with
396 // |key|.
RevokeReservation(ReservationKey key)397 void RevokeReservation(ReservationKey key) {
398   DCHECK(g_reservation_map != NULL);
399   DCHECK(base::Contains(*g_reservation_map, key));
400   g_reservation_map->erase(key);
401   if (g_reservation_map->size() == 0) {
402     // No more reservations. Delete map.
403     delete g_reservation_map;
404     g_reservation_map = NULL;
405   }
406 }
407 
RunGetReservedPathCallback(DownloadPathReservationTracker::ReservedPathCallback callback,const base::FilePath * reserved_path,PathValidationResult result)408 void RunGetReservedPathCallback(
409     DownloadPathReservationTracker::ReservedPathCallback callback,
410     const base::FilePath* reserved_path,
411     PathValidationResult result) {
412   std::move(callback).Run(result, *reserved_path);
413 }
414 
415 // Gets the path reserved in the global |g_reservation_map|. For content Uri,
416 // file name instead of file path is used.
GetReservationPath(DownloadItem * download_item)417 base::FilePath GetReservationPath(DownloadItem* download_item) {
418 #if defined(OS_ANDROID)
419   if (download_item->GetTargetFilePath().IsContentUri())
420     return download_item->GetFileNameToReportUser();
421 #endif
422   return download_item->GetTargetFilePath();
423 }
424 
DownloadItemObserver(DownloadItem * download_item)425 DownloadItemObserver::DownloadItemObserver(DownloadItem* download_item)
426     : download_item_(download_item),
427       last_target_path_(GetReservationPath(download_item)) {
428   download_item_->AddObserver(this);
429   download_item_->SetUserData(&kUserDataKey, base::WrapUnique(this));
430 }
431 
~DownloadItemObserver()432 DownloadItemObserver::~DownloadItemObserver() {
433   download_item_->RemoveObserver(this);
434   // DownloadItemObserver is owned by DownloadItem. It should only be getting
435   // destroyed because it's being removed from the UserData pool. No need to
436   // call DownloadItem::RemoveUserData().
437 }
438 
OnDownloadUpdated(DownloadItem * download)439 void DownloadItemObserver::OnDownloadUpdated(DownloadItem* download) {
440   switch (download->GetState()) {
441     case DownloadItem::IN_PROGRESS: {
442       // Update the reservation.
443       base::FilePath new_target_path = GetReservationPath(download);
444       if (new_target_path != last_target_path_) {
445         DownloadPathReservationTracker::GetTaskRunner()->PostTask(
446             FROM_HERE,
447             base::BindOnce(&UpdateReservation, download, new_target_path));
448         last_target_path_ = new_target_path;
449       }
450       break;
451     }
452 
453     case DownloadItem::COMPLETE:
454       // If the download is complete, then it has already been renamed to the
455       // final name. The existence of the file on disk is sufficient to prevent
456       // conflicts from now on.
457 
458     case DownloadItem::CANCELLED:
459       // We no longer need the reservation if the download is being removed.
460 
461     case DownloadItem::INTERRUPTED:
462       // The download filename will need to be re-generated when the download is
463       // restarted. Holding on to the reservation now would prevent the name
464       // from being used for a subsequent retry attempt.
465       DownloadPathReservationTracker::GetTaskRunner()->PostTask(
466           FROM_HERE, base::BindOnce(&RevokeReservation, download));
467       download->RemoveUserData(&kUserDataKey);
468       break;
469 
470     case DownloadItem::MAX_DOWNLOAD_STATE:
471       // Compiler appeasement.
472       NOTREACHED();
473   }
474 }
475 
OnDownloadDestroyed(DownloadItem * download)476 void DownloadItemObserver::OnDownloadDestroyed(DownloadItem* download) {
477   // Items should be COMPLETE/INTERRUPTED/CANCELLED before being destroyed.
478   NOTREACHED();
479   DownloadPathReservationTracker::GetTaskRunner()->PostTask(
480       FROM_HERE, base::BindOnce(&RevokeReservation, download));
481 }
482 
483 // static
484 const int DownloadItemObserver::kUserDataKey = 0;
485 
486 }  // namespace
487 
488 // static
GetReservedPath(DownloadItem * download_item,const base::FilePath & target_path,const base::FilePath & default_path,const base::FilePath & fallback_directory,bool create_directory,FilenameConflictAction conflict_action,ReservedPathCallback callback)489 void DownloadPathReservationTracker::GetReservedPath(
490     DownloadItem* download_item,
491     const base::FilePath& target_path,
492     const base::FilePath& default_path,
493     const base::FilePath& fallback_directory,
494     bool create_directory,
495     FilenameConflictAction conflict_action,
496     ReservedPathCallback callback) {
497   // Attach an observer to the download item so that we know when the target
498   // path changes and/or the download is no longer active.
499   new DownloadItemObserver(download_item);
500   // DownloadItemObserver deletes itself.
501 
502   base::FilePath* reserved_path = new base::FilePath;
503   base::FilePath source_path;
504   if (download_item->GetURL().SchemeIsFile())
505     net::FileURLToFilePath(download_item->GetURL(), &source_path);
506   CreateReservationInfo info = {static_cast<ReservationKey>(download_item),
507                                 source_path,
508                                 target_path,
509                                 default_path,
510                                 download_item->GetTemporaryFilePath(),
511                                 fallback_directory,
512                                 create_directory,
513                                 download_item->GetStartTime(),
514                                 conflict_action};
515 
516   base::PostTaskAndReplyWithResult(
517       GetTaskRunner().get(), FROM_HERE,
518       base::BindOnce(&CreateReservation, info, reserved_path),
519       base::BindOnce(&RunGetReservedPathCallback, std::move(callback),
520                      base::Owned(reserved_path)));
521 }
522 
523 // static
IsPathInUseForTesting(const base::FilePath & path)524 bool DownloadPathReservationTracker::IsPathInUseForTesting(
525     const base::FilePath& path) {
526   return IsPathInUse(path);
527 }
528 
529 // static
530 scoped_refptr<base::SequencedTaskRunner>
GetTaskRunner()531 DownloadPathReservationTracker::GetTaskRunner() {
532   return g_sequenced_task_runner.Get();
533 }
534 
535 // static
CheckDownloadPathForExistingDownload(const base::FilePath & target_path,DownloadItem * download_item,CheckDownloadPathCallback callback)536 void DownloadPathReservationTracker::CheckDownloadPathForExistingDownload(
537     const base::FilePath& target_path,
538     DownloadItem* download_item,
539     CheckDownloadPathCallback callback) {
540   base::PostTaskAndReplyWithResult(
541       GetTaskRunner().get(), FROM_HERE,
542       base::BindOnce(&IsAdditionalPathReserved, target_path, download_item),
543       std::move(callback));
544 }
545 
546 }  // namespace download
547