1 // Copyright 2014 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.
5 #include "chrome/browser/extensions/updater/local_extension_cache.h"
8 #include "base/files/file_enumerator.h"
9 #include "base/files/file_util.h"
10 #include "base/sequenced_task_runner.h"
11 #include "base/strings/string_util.h"
12 #include "base/sys_info.h"
13 #include "base/version.h"
14 #include "components/crx_file/id_util.h"
15 #include "content/public/browser/browser_thread.h"
17 namespace extensions
{
20 // File name extension for CRX files (not case sensitive).
21 const char kCRXFileExtension
[] = ".crx";
23 // Delay between checks for flag file presence when waiting for the cache to
25 const int64_t kCacheStatusPollingDelayMs
= 1000;
29 const char LocalExtensionCache::kCacheReadyFlagFileName
[] = ".initialized";
31 LocalExtensionCache::LocalExtensionCache(
32 const base::FilePath
& cache_dir
,
33 uint64 max_cache_size
,
34 const base::TimeDelta
& max_cache_age
,
35 const scoped_refptr
<base::SequencedTaskRunner
>& backend_task_runner
)
36 : cache_dir_(cache_dir
),
37 max_cache_size_(max_cache_size
),
38 min_cache_age_(base::Time::Now() - max_cache_age
),
39 backend_task_runner_(backend_task_runner
),
40 state_(kUninitialized
),
41 cache_status_polling_delay_(
42 base::TimeDelta::FromMilliseconds(kCacheStatusPollingDelayMs
)),
43 weak_ptr_factory_(this) {
46 LocalExtensionCache::~LocalExtensionCache() {
51 void LocalExtensionCache::Init(bool wait_for_cache_initialization
,
52 const base::Closure
& callback
) {
53 DCHECK_EQ(state_
, kUninitialized
);
55 state_
= kWaitInitialization
;
56 if (wait_for_cache_initialization
)
57 CheckCacheStatus(callback
);
59 CheckCacheContents(callback
);
62 void LocalExtensionCache::Shutdown(const base::Closure
& callback
) {
63 DCHECK_NE(state_
, kShutdown
);
67 backend_task_runner_
->PostTaskAndReply(FROM_HERE
,
68 base::Bind(&base::DoNothing
), callback
);
72 LocalExtensionCache::CacheMap::iterator
LocalExtensionCache::FindExtension(
74 const std::string
& id
,
75 const std::string
& expected_hash
) {
76 CacheHit hit
= cache
.equal_range(id
);
77 CacheMap::iterator empty_hash
= cache
.end();
78 std::string hash
= base::ToLowerASCII(expected_hash
);
79 for (CacheMap::iterator it
= hit
.first
; it
!= hit
.second
; ++it
) {
80 if (expected_hash
.empty() || it
->second
.expected_hash
== hash
) {
83 if (it
->second
.expected_hash
.empty()) {
90 bool LocalExtensionCache::GetExtension(const std::string
& id
,
91 const std::string
& expected_hash
,
92 base::FilePath
* file_path
,
93 std::string
* version
) {
97 CacheMap::iterator it
= FindExtension(cached_extensions_
, id
, expected_hash
);
98 if (it
== cached_extensions_
.end())
102 *file_path
= it
->second
.file_path
;
104 // If caller is not interested in file_path, extension is not used.
105 base::Time now
= base::Time::Now();
106 backend_task_runner_
->PostTask(FROM_HERE
,
107 base::Bind(&LocalExtensionCache::BackendMarkFileUsed
,
108 it
->second
.file_path
, now
));
109 it
->second
.last_used
= now
;
113 *version
= it
->second
.version
;
118 bool LocalExtensionCache::ShouldRetryDownload(
119 const std::string
& id
,
120 const std::string
& expected_hash
) {
121 if (state_
!= kReady
)
124 CacheMap::iterator it
= FindExtension(cached_extensions_
, id
, expected_hash
);
125 if (it
== cached_extensions_
.end())
128 return (!expected_hash
.empty() && it
->second
.expected_hash
.empty());
132 bool LocalExtensionCache::NewerOrSame(const CacheMap::iterator
& entry
,
133 const std::string
& version
,
134 const std::string
& expected_hash
,
136 Version
new_version(version
);
137 Version
prev_version(entry
->second
.version
);
138 int cmp
= new_version
.CompareTo(prev_version
);
143 // Cache entry is newer if its version is greater or same, and in the latter
144 // case we will prefer the existing one if we are trying to add an
145 // unhashed file, or we already have a hashed file in cache.
146 return (cmp
< 0 || (cmp
== 0 && (expected_hash
.empty() ||
147 !entry
->second
.expected_hash
.empty())));
150 void LocalExtensionCache::PutExtension(const std::string
& id
,
151 const std::string
& expected_hash
,
152 const base::FilePath
& file_path
,
153 const std::string
& version
,
154 const PutExtensionCallback
& callback
) {
155 if (state_
!= kReady
) {
156 callback
.Run(file_path
, true);
160 Version
version_validator(version
);
161 if (!version_validator
.IsValid()) {
162 LOG(ERROR
) << "Extension " << id
<< " has bad version " << version
;
163 callback
.Run(file_path
, true);
167 CacheMap::iterator it
= FindExtension(cached_extensions_
, id
, expected_hash
);
168 if (it
!= cached_extensions_
.end() &&
169 NewerOrSame(it
, version
, expected_hash
, NULL
)) {
170 LOG(WARNING
) << "Cache contains newer or the same version "
171 << it
->second
.version
<< " for extension " << id
<< " version "
173 callback
.Run(file_path
, true);
177 backend_task_runner_
->PostTask(
178 FROM_HERE
, base::Bind(&LocalExtensionCache::BackendInstallCacheEntry
,
179 weak_ptr_factory_
.GetWeakPtr(), cache_dir_
, id
,
180 expected_hash
, file_path
, version
, callback
));
183 bool LocalExtensionCache::RemoveExtensionAt(const CacheMap::iterator
& it
,
185 if (state_
!= kReady
|| it
== cached_extensions_
.end())
187 std::string hash
= match_hash
? it
->second
.expected_hash
: std::string();
188 backend_task_runner_
->PostTask(
189 FROM_HERE
, base::Bind(&LocalExtensionCache::BackendRemoveCacheEntry
,
190 cache_dir_
, it
->first
, hash
));
191 cached_extensions_
.erase(it
);
195 bool LocalExtensionCache::RemoveExtension(const std::string
& id
,
196 const std::string
& expected_hash
) {
197 if (state_
!= kReady
)
200 CacheMap::iterator it
= FindExtension(cached_extensions_
, id
, expected_hash
);
201 if (it
== cached_extensions_
.end())
204 while (it
!= cached_extensions_
.end()) {
205 RemoveExtensionAt(it
, !expected_hash
.empty());
207 // For empty |expected_hash| this will iteratively return any cached file.
208 // For any specific |expected_hash| this will only be able to find the
209 // matching entry once.
210 it
= FindExtension(cached_extensions_
, id
, expected_hash
);
216 bool LocalExtensionCache::GetStatistics(uint64
* cache_size
,
217 size_t* extensions_count
) {
218 if (state_
!= kReady
)
222 for (CacheMap::iterator it
= cached_extensions_
.begin();
223 it
!= cached_extensions_
.end(); ++it
) {
224 *cache_size
+= it
->second
.size
;
226 *extensions_count
= cached_extensions_
.size();
231 void LocalExtensionCache::SetCacheStatusPollingDelayForTests(
232 const base::TimeDelta
& delay
) {
233 cache_status_polling_delay_
= delay
;
236 void LocalExtensionCache::CheckCacheStatus(const base::Closure
& callback
) {
237 if (state_
== kShutdown
) {
242 backend_task_runner_
->PostTask(
244 base::Bind(&LocalExtensionCache::BackendCheckCacheStatus
,
245 weak_ptr_factory_
.GetWeakPtr(),
251 void LocalExtensionCache::BackendCheckCacheStatus(
252 base::WeakPtr
<LocalExtensionCache
> local_cache
,
253 const base::FilePath
& cache_dir
,
254 const base::Closure
& callback
) {
256 base::PathExists(cache_dir
.AppendASCII(kCacheReadyFlagFileName
));
258 static bool first_check
= true;
259 if (first_check
&& !exists
&& !base::SysInfo::IsRunningOnChromeOS()) {
260 LOG(WARNING
) << "Extensions will not be installed from update URLs until "
261 << cache_dir
.AppendASCII(kCacheReadyFlagFileName
).value()
266 content::BrowserThread::PostTask(
267 content::BrowserThread::UI
,
269 base::Bind(&LocalExtensionCache::OnCacheStatusChecked
,
275 void LocalExtensionCache::OnCacheStatusChecked(bool ready
,
276 const base::Closure
& callback
) {
277 if (state_
== kShutdown
) {
283 CheckCacheContents(callback
);
285 content::BrowserThread::PostDelayedTask(
286 content::BrowserThread::UI
,
288 base::Bind(&LocalExtensionCache::CheckCacheStatus
,
289 weak_ptr_factory_
.GetWeakPtr(),
291 cache_status_polling_delay_
);
295 void LocalExtensionCache::CheckCacheContents(const base::Closure
& callback
) {
296 DCHECK_EQ(state_
, kWaitInitialization
);
297 backend_task_runner_
->PostTask(
299 base::Bind(&LocalExtensionCache::BackendCheckCacheContents
,
300 weak_ptr_factory_
.GetWeakPtr(),
306 void LocalExtensionCache::BackendCheckCacheContents(
307 base::WeakPtr
<LocalExtensionCache
> local_cache
,
308 const base::FilePath
& cache_dir
,
309 const base::Closure
& callback
) {
310 scoped_ptr
<CacheMap
> cache_content(new CacheMap
);
311 BackendCheckCacheContentsInternal(cache_dir
, cache_content
.get());
312 content::BrowserThread::PostTask(
313 content::BrowserThread::UI
,
315 base::Bind(&LocalExtensionCache::OnCacheContentsChecked
,
317 base::Passed(&cache_content
),
322 LocalExtensionCache::CacheMap::iterator
LocalExtensionCache::InsertCacheEntry(
324 const std::string
& id
,
325 const CacheItemInfo
& info
,
326 const bool delete_files
) {
328 std::string any_hash
;
329 // FindExtension with empty hash will always return the first one
330 CacheMap::iterator it
= FindExtension(cache
, id
, any_hash
);
331 if (it
!= cache
.end()) {
332 // |cache_content| already has some version for this ID. Remove older ones.
333 // If we loook at the first cache entry, it may be:
334 // 1. an older version (in which case we should remove all its instances)
335 // 2. a newer version (in which case we should skip current file)
336 // 3. the same version without hash (skip if our hash is empty,
337 // 4. remove if our hash in not empty),
338 // 5. the same version with hash (skip if our hash is empty,
339 // 6. skip if there is already an entry with the same hash,
340 // otherwise add a new entry).
343 if (!NewerOrSame(it
, info
.version
, info
.expected_hash
, &cmp
)) {
344 // Case #1 or #4, remove all instances from cache.
345 while ((it
!= cache
.end()) && (it
->first
== id
)) {
347 base::DeleteFile(base::FilePath(it
->second
.file_path
),
348 true /* recursive */);
349 VLOG(1) << "Remove older version " << it
->second
.version
350 << " for extension id " << id
;
352 it
= cache
.erase(it
);
354 } else if ((cmp
< 0) || (cmp
== 0 && info
.expected_hash
.empty())) {
357 } else if (cmp
== 0) {
358 // Same version, both hashes are not empty, try to find the same hash.
359 while (keep
&& (it
!= cache
.end()) && (it
->first
== id
)) {
360 if (it
->second
.expected_hash
== info
.expected_hash
) {
370 it
= cache
.insert(std::make_pair(id
, info
));
373 base::DeleteFile(info
.file_path
, true /* recursive */);
374 VLOG(1) << "Remove older version " << info
.version
<< " for extension id "
384 void LocalExtensionCache::BackendCheckCacheContentsInternal(
385 const base::FilePath
& cache_dir
,
386 CacheMap
* cache_content
) {
387 // Start by verifying that the cache_dir exists.
388 if (!base::DirectoryExists(cache_dir
)) {
390 if (!base::CreateDirectory(cache_dir
)) {
391 LOG(ERROR
) << "Failed to create cache directory at "
392 << cache_dir
.value();
395 // Nothing else to do. Cache is empty.
399 // Enumerate all the files in the cache |cache_dir|, including directories
400 // and symlinks. Each unrecognized file will be erased.
401 int types
= base::FileEnumerator::FILES
| base::FileEnumerator::DIRECTORIES
;
402 base::FileEnumerator
enumerator(cache_dir
, false /* recursive */, types
);
403 for (base::FilePath path
= enumerator
.Next();
404 !path
.empty(); path
= enumerator
.Next()) {
405 base::FileEnumerator::FileInfo info
= enumerator
.GetInfo();
406 std::string basename
= path
.BaseName().value();
408 if (info
.IsDirectory() || base::IsLink(info
.GetName())) {
409 LOG(ERROR
) << "Erasing bad file in cache directory: " << basename
;
410 base::DeleteFile(path
, true /* recursive */);
414 // Skip flag file that indicates that cache is ready.
415 if (basename
== kCacheReadyFlagFileName
)
418 // crx files in the cache are named
419 // <extension-id>-<version>[-<expected_hash>].crx.
422 std::string expected_hash
;
423 if (base::EndsWith(basename
, kCRXFileExtension
,
424 base::CompareCase::INSENSITIVE_ASCII
)) {
425 size_t n
= basename
.find('-');
426 if (n
!= std::string::npos
&& n
+ 1 < basename
.size() - 4) {
427 id
= basename
.substr(0, n
);
428 // Size of |version| = total size - "<id>" - "-" - ".crx"
429 version
= basename
.substr(n
+ 1, basename
.size() - 5 - id
.size());
431 n
= version
.find('-');
432 if (n
!= std::string::npos
&& n
+ 1 < version
.size()) {
433 expected_hash
= version
.substr(n
+ 1, version
.size() - n
- 1);
439 // Enforce a lower-case id.
440 id
= base::ToLowerASCII(id
);
441 if (!crx_file::id_util::IdIsValid(id
)) {
442 LOG(ERROR
) << "Bad extension id in cache: " << id
;
446 if (!Version(version
).IsValid()) {
447 LOG(ERROR
) << "Bad extension version in cache: " << version
;
451 if (id
.empty() || version
.empty()) {
452 LOG(ERROR
) << "Invalid file in cache, erasing: " << basename
;
453 base::DeleteFile(path
, true /* recursive */);
457 VLOG(1) << "Found cached version " << version
458 << " for extension id " << id
;
462 CacheItemInfo(version
, expected_hash
, info
.GetLastModifiedTime(),
463 info
.GetSize(), path
),
468 void LocalExtensionCache::OnCacheContentsChecked(
469 scoped_ptr
<CacheMap
> cache_content
,
470 const base::Closure
& callback
) {
471 cache_content
->swap(cached_extensions_
);
477 void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath
& file_path
,
478 const base::Time
& time
) {
479 base::TouchFile(file_path
, time
, time
);
483 std::string
LocalExtensionCache::ExtensionFileName(
484 const std::string
& id
,
485 const std::string
& version
,
486 const std::string
& expected_hash
) {
487 std::string filename
= id
+ "-" + version
;
488 if (!expected_hash
.empty())
489 filename
+= "-" + base::ToLowerASCII(expected_hash
);
490 filename
+= kCRXFileExtension
;
495 void LocalExtensionCache::BackendInstallCacheEntry(
496 base::WeakPtr
<LocalExtensionCache
> local_cache
,
497 const base::FilePath
& cache_dir
,
498 const std::string
& id
,
499 const std::string
& expected_hash
,
500 const base::FilePath
& file_path
,
501 const std::string
& version
,
502 const PutExtensionCallback
& callback
) {
503 std::string basename
= ExtensionFileName(id
, version
, expected_hash
);
504 base::FilePath cached_crx_path
= cache_dir
.AppendASCII(basename
);
506 bool was_error
= false;
507 if (base::PathExists(cached_crx_path
)) {
508 LOG(ERROR
) << "File already exists " << file_path
.value();
509 cached_crx_path
= file_path
;
513 base::File::Info info
;
515 if (!base::Move(file_path
, cached_crx_path
)) {
516 LOG(ERROR
) << "Failed to copy from " << file_path
.value()
517 << " to " << cached_crx_path
.value();
518 cached_crx_path
= file_path
;
521 was_error
= !base::GetFileInfo(cached_crx_path
, &info
);
522 VLOG(1) << "Cache entry installed for extension id " << id
523 << " version " << version
;
527 content::BrowserThread::PostTask(
528 content::BrowserThread::UI
, FROM_HERE
,
529 base::Bind(&LocalExtensionCache::OnCacheEntryInstalled
, local_cache
, id
,
530 CacheItemInfo(version
, expected_hash
, info
.last_modified
,
531 info
.size
, cached_crx_path
),
532 was_error
, callback
));
535 void LocalExtensionCache::OnCacheEntryInstalled(
536 const std::string
& id
,
537 const CacheItemInfo
& info
,
539 const PutExtensionCallback
& callback
) {
540 if (state_
== kShutdown
|| was_error
) {
541 // If |was_error| is true, it means that the |info.file_path| refers to the
542 // original downloaded file, otherwise it refers to a file in cache, which
543 // should not be deleted by CrxInstaller.
544 callback
.Run(info
.file_path
, was_error
);
548 CacheMap::iterator it
= InsertCacheEntry(cached_extensions_
, id
, info
, false);
549 if (it
== cached_extensions_
.end()) {
550 DCHECK(0) << "Cache contains newer or the same version";
551 callback
.Run(info
.file_path
, true);
555 // Time from file system can have lower precision so use precise "now".
556 it
->second
.last_used
= base::Time::Now();
558 callback
.Run(info
.file_path
, false);
562 void LocalExtensionCache::BackendRemoveCacheEntry(
563 const base::FilePath
& cache_dir
,
564 const std::string
& id
,
565 const std::string
& expected_hash
) {
566 std::string file_pattern
= ExtensionFileName(id
, "*", expected_hash
);
567 base::FileEnumerator
enumerator(cache_dir
,
568 false /* not recursive */,
569 base::FileEnumerator::FILES
,
571 for (base::FilePath path
= enumerator
.Next(); !path
.empty();
572 path
= enumerator
.Next()) {
573 base::DeleteFile(path
, false);
574 VLOG(1) << "Removed cached file " << path
.value();
579 bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator
& lhs
,
580 const CacheMap::iterator
& rhs
) {
581 return lhs
->second
.last_used
< rhs
->second
.last_used
;
584 void LocalExtensionCache::CleanUp() {
585 DCHECK_EQ(state_
, kReady
);
587 std::vector
<CacheMap::iterator
> items
;
588 items
.reserve(cached_extensions_
.size());
589 uint64_t total_size
= 0;
590 for (CacheMap::iterator it
= cached_extensions_
.begin();
591 it
!= cached_extensions_
.end(); ++it
) {
593 total_size
+= it
->second
.size
;
595 std::sort(items
.begin(), items
.end(), CompareCacheItemsAge
);
597 for (std::vector
<CacheMap::iterator
>::iterator it
= items
.begin();
598 it
!= items
.end(); ++it
) {
599 if ((*it
)->second
.last_used
< min_cache_age_
||
600 (max_cache_size_
&& total_size
> max_cache_size_
)) {
601 total_size
-= (*it
)->second
.size
;
602 VLOG(1) << "Clean up cached extension id " << (*it
)->first
;
603 RemoveExtensionAt(*it
, true);
608 LocalExtensionCache::CacheItemInfo::CacheItemInfo(
609 const std::string
& version
,
610 const std::string
& expected_hash
,
611 const base::Time
& last_used
,
613 const base::FilePath
& file_path
)
615 expected_hash(base::ToLowerASCII(expected_hash
)),
616 last_used(last_used
),
618 file_path(file_path
) {
621 LocalExtensionCache::CacheItemInfo::~CacheItemInfo() {
624 } // namespace extensions