blob: 13fec0c454bd0f9acbcd2456c73db517d4b58c9a [file] [log] [blame]
// Copyright 2022 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/update_client/crx_cache.h"
#include <map>
#include <memory>
#include <optional>
#include <string>
#include "base/check.h"
#include "base/files/file_enumerator.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/path_service.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequence_bound.h"
#include "base/types/expected.h"
#include "components/prefs/json_pref_store.h"
#include "components/update_client/update_client_errors.h"
#include "third_party/abseil-cpp/absl/container/flat_hash_set.h"
namespace update_client {
// Intended to be used in a base::SequenceBound: functions may block.
class CrxCacheSynchronous {
public:
virtual ~CrxCacheSynchronous() = default;
virtual std::multimap<std::string, std::string> ListHashesByAppId() const = 0;
virtual base::expected<base::FilePath, UnpackerError> GetByHash(
const std::string& hash) const = 0;
virtual base::expected<base::FilePath, UnpackerError> GetByFp(
const std::string& fp) const = 0;
virtual base::expected<base::FilePath, UnpackerError> Put(
const base::FilePath& file,
const std::string& app_id,
const std::string& hash,
const std::string& fp) = 0;
virtual void RemoveAll(const std::string& app_id) = 0;
};
// CrxCacheImpl uses a metadata.json file of the following format:
// {
// "hashes": {
// "hash1": {"appid": "appid1", "fp": "fingerprint1"},
// "hash2": {"appid": "appid1", "fp": "fingerprint1"},
// ...
// }
// }
// and stores files at:
// cache_root/hash1
// cache_root/hash2
// ...
class CrxCacheImpl : public CrxCacheSynchronous {
public:
CrxCacheImpl(const CrxCacheImpl&) = delete;
CrxCacheImpl& operator=(const CrxCacheImpl&) = delete;
explicit CrxCacheImpl(const base::FilePath& cache_root);
~CrxCacheImpl() override;
// Overrides for CrxCacheSynchronous:
std::multimap<std::string, std::string> ListHashesByAppId() const override;
base::expected<base::FilePath, UnpackerError> GetByHash(
const std::string& hash) const override;
base::expected<base::FilePath, UnpackerError> GetByFp(
const std::string& fp) const override;
base::expected<base::FilePath, UnpackerError> Put(
const base::FilePath& file,
const std::string& app_id,
const std::string& hash,
const std::string& fp) override;
void RemoveAll(const std::string& app_id) override;
private:
void Remove(const std::string& hash);
SEQUENCE_CHECKER(sequence_checker_);
const base::FilePath cache_root_;
scoped_refptr<JsonPrefStore> metadata_;
};
CrxCacheImpl::CrxCacheImpl(const base::FilePath& cache_root)
: cache_root_(cache_root),
metadata_(base::MakeRefCounted<JsonPrefStore>(
cache_root_.Append(FILE_PATH_LITERAL("metadata.json")))) {
base::CreateDirectory(cache_root_);
metadata_->ReadPrefs();
absl::flat_hash_set<std::string> expected_basenames({"metadata.json"});
absl::flat_hash_set<std::string> found_basenames;
const base::Value* hashes_key = nullptr;
if (!metadata_->GetValue("hashes", &hashes_key) || !hashes_key->is_dict()) {
base::Value::Dict empty_dict;
metadata_->SetValue("hashes", base::Value(std::move(empty_dict)), 0);
CHECK(metadata_->GetValue("hashes", &hashes_key) && hashes_key->is_dict());
}
for (const auto [hash, value] : hashes_key->GetDict()) {
expected_basenames.insert(hash);
}
// Remove files that are missing metadata entries.
base::FileEnumerator(cache_root_, false, base::FileEnumerator::FILES)
.ForEach([&expected_basenames,
&found_basenames](const base::FilePath& file_path) {
if (!base::Contains(expected_basenames,
file_path.BaseName().AsUTF8Unsafe())) {
base::DeleteFile(file_path);
} else {
found_basenames.insert(file_path.BaseName().AsUTF8Unsafe());
}
});
// Remove metadata entries that are missing files.
for (const auto& hash : expected_basenames) {
if (!base::Contains(found_basenames, hash)) {
Remove(hash);
}
}
}
// Note: `~JsonPrefStore` calls `JsonPrefStore::CommitPendingWrite()`.
CrxCacheImpl::~CrxCacheImpl() = default;
std::multimap<std::string, std::string> CrxCacheImpl::ListHashesByAppId()
const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
std::multimap<std::string, std::string> hashes;
const base::Value* hashes_key = nullptr;
if (!metadata_->GetValue("hashes", &hashes_key) || !hashes_key->is_dict()) {
return hashes;
}
for (const auto [hash, value] : hashes_key->GetDict()) {
if (value.is_dict()) {
const std::string* item_appid = value.GetDict().FindString("appid");
if (item_appid) {
hashes.insert({*item_appid, hash});
}
}
}
return hashes;
}
base::expected<base::FilePath, UnpackerError> CrxCacheImpl::GetByHash(
const std::string& hash) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const base::Value* hashes_key = nullptr;
if (!metadata_->GetValue("hashes", &hashes_key) || !hashes_key->is_dict()) {
return base::unexpected(UnpackerError::kCrxCacheMetadataCorrupted);
}
if (!hashes_key->GetDict().contains(hash)) {
return base::unexpected(UnpackerError::kCrxCacheFileNotCached);
}
return cache_root_.AppendUTF8(hash);
}
base::expected<base::FilePath, UnpackerError> CrxCacheImpl::GetByFp(
const std::string& fp) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const base::Value* hashes_key = nullptr;
if (!metadata_->GetValue("hashes", &hashes_key) || !hashes_key->is_dict()) {
return base::unexpected(UnpackerError::kCrxCacheMetadataCorrupted);
}
for (const auto [hash, value] : hashes_key->GetDict()) {
if (value.is_dict()) {
const std::string* item_fp = value.GetDict().FindString("fp");
if (item_fp && fp == *item_fp) {
return cache_root_.AppendUTF8(hash);
}
}
}
return base::unexpected(UnpackerError::kCrxCacheFileNotCached);
}
base::expected<base::FilePath, UnpackerError> CrxCacheImpl::Put(
const base::FilePath& file,
const std::string& app_id,
const std::string& hash,
const std::string& fp) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::FilePath dest = cache_root_.AppendUTF8(hash);
if (file == dest) {
return dest; // Already cached.
}
RemoveAll(app_id);
if (!base::CreateDirectory(cache_root_)) {
return base::unexpected(UnpackerError::kFailedToCreateCacheDir);
}
if (!base::Move(file, dest)) {
return base::unexpected(UnpackerError::kFailedToAddToCache);
}
// Update metadata.
base::Value::Dict data;
data.Set("appid", app_id);
data.Set("fp", fp);
metadata_->SetValue(base::StrCat({"hashes.", hash}),
base::Value(std::move(data)), 0);
return dest;
}
void CrxCacheImpl::RemoveAll(const std::string& app_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
absl::flat_hash_set<std::string> eviction_hashes;
const base::Value* hashes_key = nullptr;
if (!metadata_->GetValue("hashes", &hashes_key) || !hashes_key->is_dict()) {
return;
}
for (const auto [hash, value] : hashes_key->GetDict()) {
if (value.is_dict()) {
const std::string* item_app_id = value.GetDict().FindString("appid");
if (item_app_id && app_id == *item_app_id) {
eviction_hashes.insert(hash);
}
}
}
for (const auto& hash : eviction_hashes) {
Remove(hash);
}
}
void CrxCacheImpl::Remove(const std::string& hash) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
base::DeleteFile(cache_root_.AppendUTF8(hash));
metadata_->RemoveValue(base::StrCat({"hashes.", hash}), 0);
}
// CrxCacheError implements CrxCache but always returns an error.
class CrxCacheError : public CrxCacheSynchronous {
public:
CrxCacheError(const CrxCacheError&) = delete;
CrxCacheError& operator=(const CrxCacheError&) = delete;
CrxCacheError() = default;
~CrxCacheError() override = default;
// Overrides for CrxCache:
std::multimap<std::string, std::string> ListHashesByAppId() const override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return {};
}
base::expected<base::FilePath, UnpackerError> GetByHash(
const std::string& hash) const override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::unexpected(UnpackerError::kCrxCacheNotProvided);
}
base::expected<base::FilePath, UnpackerError> GetByFp(
const std::string& fp) const override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::unexpected(UnpackerError::kCrxCacheNotProvided);
}
base::expected<base::FilePath, UnpackerError> Put(
const base::FilePath& file,
const std::string& app_id,
const std::string& hash,
const std::string& fp) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
return base::unexpected(UnpackerError::kCrxCacheNotProvided);
}
void RemoveAll(const std::string& app_id) override {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
};
CrxCache::CrxCache(std::optional<base::FilePath> path) {
if (path) {
delegate_ = base::SequenceBound<CrxCacheImpl>(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}), *path);
} else {
delegate_ = base::SequenceBound<CrxCacheError>(
base::ThreadPool::CreateSequencedTaskRunner({base::MayBlock()}));
}
}
CrxCache::~CrxCache() = default;
void CrxCache::ListHashesByAppId(
base::OnceCallback<void(const std::multimap<std::string, std::string>&)>
callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_.AsyncCall(&CrxCacheSynchronous::ListHashesByAppId)
.Then(std::move(callback));
}
void CrxCache::GetByHash(
const std::string& hash,
base::OnceCallback<void(base::expected<base::FilePath, UnpackerError>)>
callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_.AsyncCall(&CrxCacheSynchronous::GetByHash)
.WithArgs(hash)
.Then(std::move(callback));
}
void CrxCache::GetByFp(
const std::string& fp,
base::OnceCallback<void(base::expected<base::FilePath, UnpackerError>)>
callback) const {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_.AsyncCall(&CrxCacheSynchronous::GetByFp)
.WithArgs(fp)
.Then(std::move(callback));
}
void CrxCache::Put(
const base::FilePath& file,
const std::string& app_id,
const std::string& hash,
const std::string& fp,
base::OnceCallback<void(base::expected<base::FilePath, UnpackerError>)>
callback) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_.AsyncCall(&CrxCacheSynchronous::Put)
.WithArgs(file, app_id, hash, fp)
.Then(std::move(callback));
}
void CrxCache::RemoveAll(const std::string& app_id) {
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
delegate_.AsyncCall(&CrxCacheSynchronous::RemoveAll).WithArgs(app_id);
}
} // namespace update_client