|  | // Copyright 2014 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. | 
|  |  | 
|  | #include "chrome/browser/extensions/updater/local_extension_cache.h" | 
|  |  | 
|  | #include "base/bind.h" | 
|  | #include "base/callback_helpers.h" | 
|  | #include "base/files/file.h" | 
|  | #include "base/files/file_enumerator.h" | 
|  | #include "base/files/file_util.h" | 
|  | #include "base/sequenced_task_runner.h" | 
|  | #include "base/strings/string_util.h" | 
|  | #include "base/system/sys_info.h" | 
|  | #include "base/version.h" | 
|  | #include "components/crx_file/id_util.h" | 
|  | #include "content/public/browser/browser_task_traits.h" | 
|  | #include "content/public/browser/browser_thread.h" | 
|  |  | 
|  | namespace extensions { | 
|  | namespace { | 
|  |  | 
|  | // File name extension for CRX files (not case sensitive). | 
|  | const char kCRXFileExtension[] = ".crx"; | 
|  |  | 
|  | // Delay between checks for flag file presence when waiting for the cache to | 
|  | // become ready. | 
|  | constexpr base::TimeDelta kCacheStatusPollingDelay = | 
|  | base::TimeDelta::FromSeconds(1); | 
|  |  | 
|  | }  // namespace | 
|  |  | 
|  | const char LocalExtensionCache::kCacheReadyFlagFileName[] = ".initialized"; | 
|  |  | 
|  | LocalExtensionCache::LocalExtensionCache( | 
|  | const base::FilePath& cache_dir, | 
|  | uint64_t max_cache_size, | 
|  | const base::TimeDelta& max_cache_age, | 
|  | const scoped_refptr<base::SequencedTaskRunner>& backend_task_runner) | 
|  | : cache_dir_(cache_dir), | 
|  | max_cache_size_(max_cache_size), | 
|  | min_cache_age_(base::Time::Now() - max_cache_age), | 
|  | backend_task_runner_(backend_task_runner), | 
|  | state_(kUninitialized), | 
|  | cache_status_polling_delay_(kCacheStatusPollingDelay) {} | 
|  |  | 
|  | LocalExtensionCache::~LocalExtensionCache() { | 
|  | if (state_ == kReady) | 
|  | CleanUp(); | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::Init(bool wait_for_cache_initialization, | 
|  | base::OnceClosure callback) { | 
|  | DCHECK_EQ(state_, kUninitialized); | 
|  |  | 
|  | state_ = kWaitInitialization; | 
|  | if (wait_for_cache_initialization) | 
|  | CheckCacheStatus(std::move(callback)); | 
|  | else | 
|  | CheckCacheContents(std::move(callback)); | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::Shutdown(base::OnceClosure callback) { | 
|  | DCHECK_NE(state_, kShutdown); | 
|  | if (state_ == kReady) | 
|  | CleanUp(); | 
|  | state_ = kShutdown; | 
|  | backend_task_runner_->PostTaskAndReply(FROM_HERE, base::DoNothing(), | 
|  | std::move(callback)); | 
|  | } | 
|  |  | 
|  | // static | 
|  | LocalExtensionCache::CacheMap::iterator LocalExtensionCache::FindExtension( | 
|  | CacheMap& cache, | 
|  | const std::string& id, | 
|  | const std::string& expected_hash) { | 
|  | CacheHit hit = cache.equal_range(id); | 
|  | CacheMap::iterator empty_hash = cache.end(); | 
|  | std::string hash = base::ToLowerASCII(expected_hash); | 
|  | for (CacheMap::iterator it = hit.first; it != hit.second; ++it) { | 
|  | if (expected_hash.empty() || it->second.expected_hash == hash) { | 
|  | return it; | 
|  | } | 
|  | if (it->second.expected_hash.empty()) { | 
|  | empty_hash = it; | 
|  | } | 
|  | } | 
|  | return empty_hash; | 
|  | } | 
|  |  | 
|  | bool LocalExtensionCache::GetExtension(const std::string& id, | 
|  | const std::string& expected_hash, | 
|  | base::FilePath* file_path, | 
|  | std::string* version) { | 
|  | if (state_ != kReady) | 
|  | return false; | 
|  |  | 
|  | CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); | 
|  | if (it == cached_extensions_.end()) | 
|  | return false; | 
|  |  | 
|  | if (file_path) { | 
|  | *file_path = it->second.file_path; | 
|  |  | 
|  | // If caller is not interested in file_path, extension is not used. | 
|  | base::Time now = base::Time::Now(); | 
|  | backend_task_runner_->PostTask( | 
|  | FROM_HERE, base::BindOnce(&LocalExtensionCache::BackendMarkFileUsed, | 
|  | it->second.file_path, now)); | 
|  | it->second.last_used = now; | 
|  | } | 
|  |  | 
|  | if (version) | 
|  | *version = it->second.version; | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool LocalExtensionCache::ShouldRetryDownload( | 
|  | const std::string& id, | 
|  | const std::string& expected_hash) { | 
|  | if (state_ != kReady) | 
|  | return false; | 
|  |  | 
|  | // Should retry download only if in the previous attempt the extension was | 
|  | // present in the cache and the installer process failed. After the removal, | 
|  | // the extension is freshly downloaded. | 
|  | CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); | 
|  | if (it == cached_extensions_.end()) | 
|  | return false; | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | // static | 
|  | bool LocalExtensionCache::NewerOrSame(const CacheMap::iterator& entry, | 
|  | const std::string& version, | 
|  | const std::string& expected_hash, | 
|  | int* compare) { | 
|  | base::Version new_version(version); | 
|  | base::Version prev_version(entry->second.version); | 
|  | int cmp = new_version.CompareTo(prev_version); | 
|  |  | 
|  | if (compare) | 
|  | *compare = cmp; | 
|  |  | 
|  | // Cache entry is newer if its version is greater or same, and in the latter | 
|  | // case we will prefer the existing one if we are trying to add an | 
|  | // unhashed file, or we already have a hashed file in cache. | 
|  | return (cmp < 0 || (cmp == 0 && (expected_hash.empty() || | 
|  | !entry->second.expected_hash.empty()))); | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::PutExtension(const std::string& id, | 
|  | const std::string& expected_hash, | 
|  | const base::FilePath& file_path, | 
|  | const std::string& version, | 
|  | PutExtensionCallback callback) { | 
|  | if (state_ != kReady) { | 
|  | std::move(callback).Run(file_path, true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | base::Version version_validator(version); | 
|  | if (!version_validator.IsValid()) { | 
|  | LOG(ERROR) << "Extension " << id << " has bad version " << version; | 
|  | std::move(callback).Run(file_path, true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); | 
|  | if (it != cached_extensions_.end() && | 
|  | NewerOrSame(it, version, expected_hash, NULL)) { | 
|  | LOG(WARNING) << "Cache contains newer or the same version " | 
|  | << it->second.version << " for extension " << id << " version " | 
|  | << version; | 
|  | std::move(callback).Run(file_path, true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | backend_task_runner_->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&LocalExtensionCache::BackendInstallCacheEntry, | 
|  | weak_ptr_factory_.GetWeakPtr(), cache_dir_, id, | 
|  | expected_hash, file_path, version, std::move(callback))); | 
|  | } | 
|  |  | 
|  | bool LocalExtensionCache::RemoveExtensionAt(const CacheMap::iterator& it, | 
|  | bool match_hash) { | 
|  | if (state_ != kReady || it == cached_extensions_.end()) | 
|  | return false; | 
|  | std::string hash = match_hash ? it->second.expected_hash : std::string(); | 
|  | backend_task_runner_->PostTask( | 
|  | FROM_HERE, base::BindOnce(&LocalExtensionCache::BackendRemoveCacheEntry, | 
|  | cache_dir_, it->first, hash)); | 
|  | cached_extensions_.erase(it); | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool LocalExtensionCache::RemoveExtension(const std::string& id, | 
|  | const std::string& expected_hash) { | 
|  | if (state_ != kReady) | 
|  | return false; | 
|  |  | 
|  | CacheMap::iterator it = FindExtension(cached_extensions_, id, expected_hash); | 
|  | if (it == cached_extensions_.end()) | 
|  | return false; | 
|  |  | 
|  | while (it != cached_extensions_.end()) { | 
|  | RemoveExtensionAt(it, !expected_hash.empty()); | 
|  |  | 
|  | // For empty |expected_hash| this will iteratively return any cached file. | 
|  | // For any specific |expected_hash| this will only be able to find the | 
|  | // matching entry once. | 
|  | it = FindExtension(cached_extensions_, id, expected_hash); | 
|  | } | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | bool LocalExtensionCache::GetStatistics(uint64_t* cache_size, | 
|  | size_t* extensions_count) { | 
|  | if (state_ != kReady) | 
|  | return false; | 
|  |  | 
|  | *cache_size = 0; | 
|  | for (CacheMap::iterator it = cached_extensions_.begin(); | 
|  | it != cached_extensions_.end(); ++it) { | 
|  | *cache_size += it->second.size; | 
|  | } | 
|  | *extensions_count = cached_extensions_.size(); | 
|  |  | 
|  | return true; | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::SetCacheStatusPollingDelayForTests( | 
|  | const base::TimeDelta& delay) { | 
|  | cache_status_polling_delay_ = delay; | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::CheckCacheStatus(base::OnceClosure callback) { | 
|  | if (state_ == kShutdown) { | 
|  | std::move(callback).Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | backend_task_runner_->PostTask( | 
|  | FROM_HERE, base::BindOnce(&LocalExtensionCache::BackendCheckCacheStatus, | 
|  | weak_ptr_factory_.GetWeakPtr(), cache_dir_, | 
|  | std::move(callback))); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendCheckCacheStatus( | 
|  | base::WeakPtr<LocalExtensionCache> local_cache, | 
|  | const base::FilePath& cache_dir, | 
|  | base::OnceClosure callback) { | 
|  | base::FilePath ready_flag_file = | 
|  | cache_dir.AppendASCII(kCacheReadyFlagFileName); | 
|  | bool exists = base::PathExists(ready_flag_file); | 
|  |  | 
|  | static bool already_warned = false; | 
|  | if (!exists && !base::SysInfo::IsRunningOnChromeOS()) { | 
|  | // This is a developer build. Automatically create the directory. | 
|  | if (base::CreateDirectory(cache_dir)) { | 
|  | base::File file(ready_flag_file, base::File::FLAG_OPEN_ALWAYS); | 
|  | if (file.IsValid()) { | 
|  | exists = true; | 
|  | } else if (!already_warned) { | 
|  | LOG(WARNING) << "Could not create cache file " | 
|  | << ready_flag_file.value() | 
|  | << "; extensions cannot be installed from update URLs."; | 
|  | already_warned = true; | 
|  | } | 
|  | } else if (!already_warned) { | 
|  | LOG(WARNING) << "Could not create cache directory " << cache_dir.value() | 
|  | << "; extensions cannot be installed from update URLs."; | 
|  | already_warned = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | content::GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, base::BindOnce(&LocalExtensionCache::OnCacheStatusChecked, | 
|  | local_cache, exists, std::move(callback))); | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::OnCacheStatusChecked(bool ready, | 
|  | base::OnceClosure callback) { | 
|  | if (state_ == kShutdown) { | 
|  | std::move(callback).Run(); | 
|  | return; | 
|  | } | 
|  |  | 
|  | if (ready) { | 
|  | CheckCacheContents(std::move(callback)); | 
|  | } else { | 
|  | content::GetUIThreadTaskRunner({})->PostDelayedTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&LocalExtensionCache::CheckCacheStatus, | 
|  | weak_ptr_factory_.GetWeakPtr(), std::move(callback)), | 
|  | cache_status_polling_delay_); | 
|  | } | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::CheckCacheContents(base::OnceClosure callback) { | 
|  | DCHECK_EQ(state_, kWaitInitialization); | 
|  | backend_task_runner_->PostTask( | 
|  | FROM_HERE, base::BindOnce(&LocalExtensionCache::BackendCheckCacheContents, | 
|  | weak_ptr_factory_.GetWeakPtr(), cache_dir_, | 
|  | std::move(callback))); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendCheckCacheContents( | 
|  | base::WeakPtr<LocalExtensionCache> local_cache, | 
|  | const base::FilePath& cache_dir, | 
|  | base::OnceClosure callback) { | 
|  | std::unique_ptr<CacheMap> cache_content(new CacheMap); | 
|  | BackendCheckCacheContentsInternal(cache_dir, cache_content.get()); | 
|  | content::GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&LocalExtensionCache::OnCacheContentsChecked, local_cache, | 
|  | std::move(cache_content), std::move(callback))); | 
|  | } | 
|  |  | 
|  | // static | 
|  | LocalExtensionCache::CacheMap::iterator LocalExtensionCache::InsertCacheEntry( | 
|  | CacheMap& cache, | 
|  | const std::string& id, | 
|  | const CacheItemInfo& info, | 
|  | const bool delete_files) { | 
|  | bool keep = true; | 
|  | std::string any_hash; | 
|  | // FindExtension with empty hash will always return the first one | 
|  | CacheMap::iterator it = FindExtension(cache, id, any_hash); | 
|  | if (it != cache.end()) { | 
|  | // |cache_content| already has some version for this ID. Remove older ones. | 
|  | // If we loook at the first cache entry, it may be: | 
|  | // 1. an older version (in which case we should remove all its instances) | 
|  | // 2. a newer version (in which case we should skip current file) | 
|  | // 3. the same version without hash (skip if our hash is empty, | 
|  | // 4. remove if our hash in not empty), | 
|  | // 5. the same version with hash (skip if our hash is empty, | 
|  | // 6. skip if there is already an entry with the same hash, | 
|  | // otherwise add a new entry). | 
|  |  | 
|  | int cmp = 0; | 
|  | if (!NewerOrSame(it, info.version, info.expected_hash, &cmp)) { | 
|  | // Case #1 or #4, remove all instances from cache. | 
|  | while ((it != cache.end()) && (it->first == id)) { | 
|  | if (delete_files) { | 
|  | base::DeletePathRecursively(base::FilePath(it->second.file_path)); | 
|  | VLOG(1) << "Remove older version " << it->second.version | 
|  | << " for extension id " << id; | 
|  | } | 
|  | it = cache.erase(it); | 
|  | } | 
|  | } else if ((cmp < 0) || (cmp == 0 && info.expected_hash.empty())) { | 
|  | // Case #2, #3 or #5 | 
|  | keep = false; | 
|  | } else if (cmp == 0) { | 
|  | // Same version, both hashes are not empty, try to find the same hash. | 
|  | while (keep && (it != cache.end()) && (it->first == id)) { | 
|  | if (it->second.expected_hash == info.expected_hash) { | 
|  | // Case #6 | 
|  | keep = false; | 
|  | } | 
|  | ++it; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | if (keep) { | 
|  | it = cache.insert(std::make_pair(id, info)); | 
|  | } else { | 
|  | if (delete_files) { | 
|  | base::DeletePathRecursively(info.file_path); | 
|  | VLOG(1) << "Remove older version " << info.version << " for extension id " | 
|  | << id; | 
|  | } | 
|  | it = cache.end(); | 
|  | } | 
|  |  | 
|  | return it; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendCheckCacheContentsInternal( | 
|  | const base::FilePath& cache_dir, | 
|  | CacheMap* cache_content) { | 
|  | // Start by verifying that the cache_dir exists. | 
|  | if (!base::DirectoryExists(cache_dir)) { | 
|  | // Create it now. | 
|  | if (!base::CreateDirectory(cache_dir)) { | 
|  | LOG(ERROR) << "Failed to create cache directory at " | 
|  | << cache_dir.value(); | 
|  | } | 
|  |  | 
|  | // Nothing else to do. Cache is empty. | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Enumerate all the files in the cache |cache_dir|, including directories | 
|  | // and symlinks. Each unrecognized file will be erased. | 
|  | int types = base::FileEnumerator::FILES | base::FileEnumerator::DIRECTORIES; | 
|  | base::FileEnumerator enumerator(cache_dir, false /* recursive */, types); | 
|  | for (base::FilePath path = enumerator.Next(); | 
|  | !path.empty(); path = enumerator.Next()) { | 
|  | base::FileEnumerator::FileInfo info = enumerator.GetInfo(); | 
|  | std::string basename = path.BaseName().value(); | 
|  |  | 
|  | if (info.IsDirectory() || base::IsLink(info.GetName())) { | 
|  | LOG(ERROR) << "Erasing bad file in cache directory: " << basename; | 
|  | base::DeletePathRecursively(path); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | // Skip flag file that indicates that cache is ready. | 
|  | if (basename == kCacheReadyFlagFileName) | 
|  | continue; | 
|  |  | 
|  | // crx files in the cache are named | 
|  | // <extension-id>-<version>[-<expected_hash>].crx. | 
|  | std::string id; | 
|  | std::string version; | 
|  | std::string expected_hash; | 
|  | if (base::EndsWith(basename, kCRXFileExtension, | 
|  | base::CompareCase::INSENSITIVE_ASCII)) { | 
|  | size_t n = basename.find('-'); | 
|  | if (n != std::string::npos && n + 1 < basename.size() - 4) { | 
|  | id = basename.substr(0, n); | 
|  | // Size of |version| = total size - "<id>" - "-" - ".crx" | 
|  | version = basename.substr(n + 1, basename.size() - 5 - id.size()); | 
|  |  | 
|  | n = version.find('-'); | 
|  | if (n != std::string::npos && n + 1 < version.size()) { | 
|  | expected_hash = version.substr(n + 1, version.size() - n - 1); | 
|  | version.resize(n); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | // Enforce a lower-case id. | 
|  | id = base::ToLowerASCII(id); | 
|  | if (!crx_file::id_util::IdIsValid(id)) { | 
|  | LOG(ERROR) << "Bad extension id in cache: " << id; | 
|  | id.clear(); | 
|  | } | 
|  |  | 
|  | if (!base::Version(version).IsValid()) { | 
|  | LOG(ERROR) << "Bad extension version in cache: " << version; | 
|  | version.clear(); | 
|  | } | 
|  |  | 
|  | if (id.empty() || version.empty()) { | 
|  | LOG(ERROR) << "Invalid file in cache, erasing: " << basename; | 
|  | base::DeletePathRecursively(path); | 
|  | continue; | 
|  | } | 
|  |  | 
|  | VLOG(1) << "Found cached version " << version | 
|  | << " for extension id " << id; | 
|  |  | 
|  | InsertCacheEntry( | 
|  | *cache_content, id, | 
|  | CacheItemInfo(version, expected_hash, info.GetLastModifiedTime(), | 
|  | info.GetSize(), path), | 
|  | true); | 
|  | } | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::OnCacheContentsChecked( | 
|  | std::unique_ptr<CacheMap> cache_content, | 
|  | base::OnceClosure callback) { | 
|  | cache_content->swap(cached_extensions_); | 
|  | state_ = kReady; | 
|  | std::move(callback).Run(); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendMarkFileUsed(const base::FilePath& file_path, | 
|  | const base::Time& time) { | 
|  | base::TouchFile(file_path, time, time); | 
|  | } | 
|  |  | 
|  | // static | 
|  | std::string LocalExtensionCache::ExtensionFileName( | 
|  | const std::string& id, | 
|  | const std::string& version, | 
|  | const std::string& expected_hash) { | 
|  | std::string filename = id + "-" + version; | 
|  | if (!expected_hash.empty()) | 
|  | filename += "-" + base::ToLowerASCII(expected_hash); | 
|  | filename += kCRXFileExtension; | 
|  | return filename; | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendInstallCacheEntry( | 
|  | base::WeakPtr<LocalExtensionCache> local_cache, | 
|  | const base::FilePath& cache_dir, | 
|  | const std::string& id, | 
|  | const std::string& expected_hash, | 
|  | const base::FilePath& file_path, | 
|  | const std::string& version, | 
|  | PutExtensionCallback callback) { | 
|  | std::string basename = ExtensionFileName(id, version, expected_hash); | 
|  | base::FilePath cached_crx_path = cache_dir.AppendASCII(basename); | 
|  |  | 
|  | bool was_error = false; | 
|  | if (base::PathExists(cached_crx_path)) { | 
|  | LOG(ERROR) << "File already exists " << file_path.value(); | 
|  | cached_crx_path = file_path; | 
|  | was_error = true; | 
|  | } | 
|  |  | 
|  | base::File::Info info; | 
|  | if (!was_error) { | 
|  | if (!base::Move(file_path, cached_crx_path)) { | 
|  | LOG(ERROR) << "Failed to copy from " << file_path.value() | 
|  | << " to " << cached_crx_path.value(); | 
|  | cached_crx_path = file_path; | 
|  | was_error = true; | 
|  | } else { | 
|  | was_error = !base::GetFileInfo(cached_crx_path, &info); | 
|  | VLOG(1) << "Cache entry installed for extension id " << id | 
|  | << " version " << version; | 
|  | } | 
|  | } | 
|  |  | 
|  | content::GetUIThreadTaskRunner({})->PostTask( | 
|  | FROM_HERE, | 
|  | base::BindOnce(&LocalExtensionCache::OnCacheEntryInstalled, local_cache, | 
|  | id, | 
|  | CacheItemInfo(version, expected_hash, info.last_modified, | 
|  | info.size, cached_crx_path), | 
|  | was_error, std::move(callback))); | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::OnCacheEntryInstalled(const std::string& id, | 
|  | const CacheItemInfo& info, | 
|  | bool was_error, | 
|  | PutExtensionCallback callback) { | 
|  | if (state_ == kShutdown || was_error) { | 
|  | // If |was_error| is true, it means that the |info.file_path| refers to the | 
|  | // original downloaded file, otherwise it refers to a file in cache, which | 
|  | // should not be deleted by CrxInstaller. | 
|  | std::move(callback).Run(info.file_path, was_error); | 
|  | return; | 
|  | } | 
|  |  | 
|  | CacheMap::iterator it = InsertCacheEntry(cached_extensions_, id, info, false); | 
|  | if (it == cached_extensions_.end()) { | 
|  | LOG(WARNING) << "Cache contains newer or the same version for extension " | 
|  | << id << " version " << info.version; | 
|  | std::move(callback).Run(info.file_path, true); | 
|  | return; | 
|  | } | 
|  |  | 
|  | // Time from file system can have lower precision so use precise "now". | 
|  | it->second.last_used = base::Time::Now(); | 
|  |  | 
|  | std::move(callback).Run(info.file_path, false); | 
|  | } | 
|  |  | 
|  | // static | 
|  | void LocalExtensionCache::BackendRemoveCacheEntry( | 
|  | const base::FilePath& cache_dir, | 
|  | const std::string& id, | 
|  | const std::string& expected_hash) { | 
|  | std::string file_pattern = ExtensionFileName(id, "*", expected_hash); | 
|  | base::FileEnumerator enumerator(cache_dir, | 
|  | false /* not recursive */, | 
|  | base::FileEnumerator::FILES, | 
|  | file_pattern); | 
|  | for (base::FilePath path = enumerator.Next(); !path.empty(); | 
|  | path = enumerator.Next()) { | 
|  | base::DeleteFile(path); | 
|  | VLOG(1) << "Removed cached file " << path.value(); | 
|  | } | 
|  | } | 
|  |  | 
|  | // static | 
|  | bool LocalExtensionCache::CompareCacheItemsAge(const CacheMap::iterator& lhs, | 
|  | const CacheMap::iterator& rhs) { | 
|  | return lhs->second.last_used < rhs->second.last_used; | 
|  | } | 
|  |  | 
|  | void LocalExtensionCache::CleanUp() { | 
|  | DCHECK_EQ(state_, kReady); | 
|  |  | 
|  | std::vector<CacheMap::iterator> items; | 
|  | items.reserve(cached_extensions_.size()); | 
|  | uint64_t total_size = 0; | 
|  | for (CacheMap::iterator it = cached_extensions_.begin(); | 
|  | it != cached_extensions_.end(); ++it) { | 
|  | items.push_back(it); | 
|  | total_size += it->second.size; | 
|  | } | 
|  | std::sort(items.begin(), items.end(), CompareCacheItemsAge); | 
|  |  | 
|  | for (std::vector<CacheMap::iterator>::iterator it = items.begin(); | 
|  | it != items.end(); ++it) { | 
|  | if ((*it)->second.last_used < min_cache_age_ || | 
|  | (max_cache_size_ && total_size > max_cache_size_)) { | 
|  | total_size -= (*it)->second.size; | 
|  | VLOG(1) << "Clean up cached extension id " << (*it)->first; | 
|  | RemoveExtensionAt(*it, true); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | LocalExtensionCache::CacheItemInfo::CacheItemInfo( | 
|  | const std::string& version, | 
|  | const std::string& expected_hash, | 
|  | const base::Time& last_used, | 
|  | uint64_t size, | 
|  | const base::FilePath& file_path) | 
|  | : version(version), | 
|  | expected_hash(base::ToLowerASCII(expected_hash)), | 
|  | last_used(last_used), | 
|  | size(size), | 
|  | file_path(file_path) {} | 
|  |  | 
|  | LocalExtensionCache::CacheItemInfo::CacheItemInfo(const CacheItemInfo& other) = | 
|  | default; | 
|  |  | 
|  | LocalExtensionCache::CacheItemInfo::~CacheItemInfo() { | 
|  | } | 
|  |  | 
|  | }  // namespace extensions |