// Copyright 2013 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/nacl/browser/pnacl_translation_cache.h"
#include <string.h>
#include <string>
#include <utility>
#include "base/files/file_path.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/i18n/time_formatting.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
#include "base/threading/thread_checker.h"
#include "components/nacl/common/pnacl_types.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/io_buffer.h"
#include "net/base/net_errors.h"
#include "net/disk_cache/disk_cache.h"
#include "third_party/icu/source/i18n/unicode/timezone.h"
namespace pnacl {
// This is in pnacl namespace instead of static so it can be used by the unit
// test.
constexpr int kMaxMemCacheSize = 100 * 1024 * 1024;
//////////////////////////////////////////////////////////////////////
// Handle Reading/Writing to Cache.
// PnaclTranslationCacheEntry is a shim that provides storage for the
// 'key' and 'data' strings as the disk_cache is performing various async
// operations. It also tracks the open disk_cache::Entry
// and ensures that the entry is closed.
class PnaclTranslationCacheEntry
: public base::RefCountedThreadSafe<PnaclTranslationCacheEntry> {
public:
static PnaclTranslationCacheEntry* GetReadEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
GetNexeCallback callback);
static PnaclTranslationCacheEntry* GetWriteEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
net::DrainableIOBuffer* write_nexe,
CompletionOnceCallback callback);
PnaclTranslationCacheEntry(const PnaclTranslationCacheEntry&) = delete;
PnaclTranslationCacheEntry& operator=(const PnaclTranslationCacheEntry&) =
delete;
void Start();
// Writes: ---
// v |
// Start -> Open Existing --------------> Write ---> Close
// \ ^
// \ /
// --> Create --
// Reads:
// Start -> Open --------Read ----> Close
// | ^
// |__|
enum CacheStep {
UNINITIALIZED,
OPEN_ENTRY,
CREATE_ENTRY,
TRANSFER_ENTRY,
CLOSE_ENTRY,
FINISHED
};
private:
friend class base::RefCountedThreadSafe<PnaclTranslationCacheEntry>;
PnaclTranslationCacheEntry(base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
bool is_read);
~PnaclTranslationCacheEntry();
// Try to open an existing entry in the backend
void OpenEntry();
// Create a new entry in the backend (for writes)
void CreateEntry();
// Write |len| bytes to the backend, starting at |offset|
void WriteEntry(int offset, int len);
// Read |len| bytes from the backend, starting at |offset|
void ReadEntry(int offset, int len);
// If there was an error, doom the entry. Then post a task to the IO
// thread to close (and delete) it.
void CloseEntry(int rv);
// Call the user callback, and signal to the cache to delete this.
void Finish(int rv);
// Used as the callback for all operations to the backend except those that
// first open/create entries. Handle state transitions, track bytes
// transferred, and call the other helper methods.
void DispatchNext(int rv);
// Like above but for first opening or creating of |entry_|.
void SaveEntryAndDispatchNext(disk_cache::EntryResult result);
base::WeakPtr<PnaclTranslationCache> cache_;
std::string key_;
raw_ptr<disk_cache::Entry> entry_;
CacheStep step_;
bool is_read_;
GetNexeCallback read_callback_;
CompletionOnceCallback write_callback_;
scoped_refptr<net::DrainableIOBuffer> io_buf_;
base::ThreadChecker thread_checker_;
};
// static
PnaclTranslationCacheEntry* PnaclTranslationCacheEntry::GetReadEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
GetNexeCallback callback) {
PnaclTranslationCacheEntry* entry(
new PnaclTranslationCacheEntry(cache, key, true));
entry->read_callback_ = std::move(callback);
return entry;
}
// static
PnaclTranslationCacheEntry* PnaclTranslationCacheEntry::GetWriteEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
net::DrainableIOBuffer* write_nexe,
CompletionOnceCallback callback) {
PnaclTranslationCacheEntry* entry(
new PnaclTranslationCacheEntry(cache, key, false));
entry->io_buf_ = write_nexe;
entry->write_callback_ = std::move(callback);
return entry;
}
PnaclTranslationCacheEntry::PnaclTranslationCacheEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
bool is_read)
: cache_(cache),
key_(key),
entry_(nullptr),
step_(UNINITIALIZED),
is_read_(is_read) {}
PnaclTranslationCacheEntry::~PnaclTranslationCacheEntry() {
// Ensure we have called the user's callback
if (step_ != FINISHED) {
if (!read_callback_.is_null()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(read_callback_), net::ERR_ABORTED,
scoped_refptr<net::DrainableIOBuffer>()));
}
if (!write_callback_.is_null()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(std::move(write_callback_), net::ERR_ABORTED));
}
}
}
void PnaclTranslationCacheEntry::Start() {
DCHECK(thread_checker_.CalledOnValidThread());
step_ = OPEN_ENTRY;
OpenEntry();
}
// OpenEntry, CreateEntry, WriteEntry, ReadEntry and CloseEntry are only called
// from DispatchNext, so they know that cache_ is still valid.
void PnaclTranslationCacheEntry::OpenEntry() {
disk_cache::EntryResult result = cache_->backend()->OpenEntry(
key_, net::HIGHEST,
base::BindOnce(&PnaclTranslationCacheEntry::SaveEntryAndDispatchNext,
this));
if (result.net_error() != net::ERR_IO_PENDING)
SaveEntryAndDispatchNext(std::move(result));
}
void PnaclTranslationCacheEntry::CreateEntry() {
disk_cache::EntryResult result = cache_->backend()->CreateEntry(
key_, net::HIGHEST,
base::BindOnce(&PnaclTranslationCacheEntry::SaveEntryAndDispatchNext,
this));
if (result.net_error() != net::ERR_IO_PENDING)
SaveEntryAndDispatchNext(std::move(result));
}
void PnaclTranslationCacheEntry::WriteEntry(int offset, int len) {
DCHECK(io_buf_->BytesRemaining() == len);
int rv = entry_->WriteData(
1, offset, io_buf_.get(), len,
base::BindOnce(&PnaclTranslationCacheEntry::DispatchNext, this), false);
if (rv != net::ERR_IO_PENDING)
DispatchNext(rv);
}
void PnaclTranslationCacheEntry::ReadEntry(int offset, int len) {
int rv = entry_->ReadData(
1, offset, io_buf_.get(), len,
base::BindOnce(&PnaclTranslationCacheEntry::DispatchNext, this));
if (rv != net::ERR_IO_PENDING)
DispatchNext(rv);
}
void PnaclTranslationCacheEntry::CloseEntry(int rv) {
DCHECK(entry_);
if (rv < 0) {
LOG(ERROR) << "Failed to close entry: " << net::ErrorToString(rv);
entry_->Doom();
}
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&disk_cache::Entry::Close, base::Unretained(entry_)));
Finish(rv);
}
void PnaclTranslationCacheEntry::Finish(int rv) {
step_ = FINISHED;
if (is_read_) {
if (!read_callback_.is_null()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(read_callback_), rv, io_buf_));
}
} else {
if (!write_callback_.is_null()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(write_callback_), rv));
}
}
cache_->OpComplete(this);
}
void PnaclTranslationCacheEntry::DispatchNext(int rv) {
DCHECK(thread_checker_.CalledOnValidThread());
if (!cache_)
return;
switch (step_) {
case UNINITIALIZED:
case FINISHED:
LOG(ERROR) << "DispatchNext called uninitialized";
break;
case OPEN_ENTRY:
if (rv == net::OK) {
step_ = TRANSFER_ENTRY;
if (is_read_) {
int bytes_to_transfer = entry_->GetDataSize(1);
io_buf_ = base::MakeRefCounted<net::DrainableIOBuffer>(
base::MakeRefCounted<net::IOBufferWithSize>(bytes_to_transfer),
bytes_to_transfer);
ReadEntry(0, bytes_to_transfer);
} else {
WriteEntry(0, io_buf_->size());
}
} else {
if (rv != net::ERR_FAILED) {
// ERROR_FAILED is what we expect if the entry doesn't exist.
LOG(ERROR) << "OpenEntry failed: " << net::ErrorToString(rv);
}
if (is_read_) {
// Just a cache miss, not necessarily an error.
entry_ = nullptr;
Finish(rv);
} else {
step_ = CREATE_ENTRY;
CreateEntry();
}
}
break;
case CREATE_ENTRY:
if (rv == net::OK) {
step_ = TRANSFER_ENTRY;
WriteEntry(io_buf_->BytesConsumed(), io_buf_->BytesRemaining());
} else {
LOG(ERROR) << "Failed to Create Entry: " << net::ErrorToString(rv);
Finish(rv);
}
break;
case TRANSFER_ENTRY:
if (rv < 0) {
// We do not call DispatchNext directly if WriteEntry/ReadEntry returns
// ERR_IO_PENDING, and the callback should not return that value either.
LOG(ERROR) << "Failed to complete write to entry: "
<< net::ErrorToString(rv);
step_ = CLOSE_ENTRY;
CloseEntry(rv);
break;
} else if (rv > 0) {
io_buf_->DidConsume(rv);
if (io_buf_->BytesRemaining() > 0) {
if (is_read_) {
ReadEntry(io_buf_->BytesConsumed(), io_buf_->BytesRemaining());
} else {
WriteEntry(io_buf_->BytesConsumed(), io_buf_->BytesRemaining());
}
break;
}
}
// rv == 0 or we fell through (i.e. we have transferred all the bytes)
step_ = CLOSE_ENTRY;
DCHECK(io_buf_->BytesConsumed() == io_buf_->size());
if (is_read_)
io_buf_->SetOffset(0);
CloseEntry(0);
break;
case CLOSE_ENTRY:
step_ = UNINITIALIZED;
break;
}
}
void PnaclTranslationCacheEntry::SaveEntryAndDispatchNext(
disk_cache::EntryResult result) {
int rv = result.net_error();
entry_ = result.ReleaseEntry();
DispatchNext(rv);
}
//////////////////////////////////////////////////////////////////////
void PnaclTranslationCache::OpComplete(PnaclTranslationCacheEntry* entry) {
open_entries_.erase(entry);
}
//////////////////////////////////////////////////////////////////////
// Construction and cache backend initialization
PnaclTranslationCache::PnaclTranslationCache() : in_memory_(false) {}
PnaclTranslationCache::~PnaclTranslationCache() {}
int PnaclTranslationCache::Init(net::CacheType cache_type,
const base::FilePath& cache_dir,
int cache_size,
CompletionOnceCallback callback) {
disk_cache::BackendResult result = disk_cache::CreateCacheBackend(
cache_type, net::CACHE_BACKEND_DEFAULT, /*file_operations=*/nullptr,
cache_dir, cache_size, disk_cache::ResetHandling::kResetOnError,
nullptr, /* dummy net log */
base::BindOnce(&PnaclTranslationCache::OnCreateBackendComplete,
weak_ptr_factory_.GetWeakPtr()));
if (result.net_error == net::OK) {
disk_cache_ = std::move(result.backend);
} else if (result.net_error == net::ERR_IO_PENDING) {
init_callback_ = std::move(callback);
}
return result.net_error;
}
void PnaclTranslationCache::OnCreateBackendComplete(
disk_cache::BackendResult result) {
if (result.net_error < 0) {
LOG(ERROR) << "Backend init failed:"
<< net::ErrorToString(result.net_error);
}
disk_cache_ = std::move(result.backend);
// Invoke our client's callback function.
if (!init_callback_.is_null()) {
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE, base::BindOnce(std::move(init_callback_), result.net_error));
}
}
//////////////////////////////////////////////////////////////////////
// High-level API
void PnaclTranslationCache::StoreNexe(const std::string& key,
net::DrainableIOBuffer* nexe_data,
CompletionOnceCallback callback) {
PnaclTranslationCacheEntry* entry = PnaclTranslationCacheEntry::GetWriteEntry(
weak_ptr_factory_.GetWeakPtr(), key, nexe_data, std::move(callback));
open_entries_[entry] = entry;
entry->Start();
}
void PnaclTranslationCache::GetNexe(const std::string& key,
GetNexeCallback callback) {
PnaclTranslationCacheEntry* entry = PnaclTranslationCacheEntry::GetReadEntry(
weak_ptr_factory_.GetWeakPtr(), key, std::move(callback));
open_entries_[entry] = entry;
entry->Start();
}
int PnaclTranslationCache::InitOnDisk(const base::FilePath& cache_directory,
CompletionOnceCallback callback) {
in_memory_ = false;
return Init(net::PNACL_CACHE, cache_directory, 0 /* auto size */,
std::move(callback));
}
int PnaclTranslationCache::InitInMemory(CompletionOnceCallback callback) {
in_memory_ = true;
return Init(net::MEMORY_CACHE, base::FilePath(), kMaxMemCacheSize,
std::move(callback));
}
int PnaclTranslationCache::Size() {
return disk_cache_ ? disk_cache_->GetEntryCount() : -1;
}
// Beware that any changes to this function or to PnaclCacheInfo will
// effectively invalidate existing translation cache entries.
// static
std::string PnaclTranslationCache::GetKey(const nacl::PnaclCacheInfo& info) {
if (!info.pexe_url.is_valid() || info.abi_version < 0 || info.opt_level < 0 ||
info.extra_flags.size() > 512) {
return std::string();
}
// Filter the username, password, and ref components from the URL.
GURL::Replacements replacements;
replacements.ClearUsername();
replacements.ClearPassword();
replacements.ClearRef();
const GURL key_url = info.pexe_url.ReplaceComponents(replacements);
const std::string timestamp = base::UnlocalizedTimeFormatWithPattern(
info.last_modified, "y:M:d:H:m:s:S:'UTC'", icu::TimeZone::getGMT());
return base::StringPrintf(
"ABI:%d;opt:%d%s;URL:%s;modified:%s;etag:%s;sandbox:%s;extra_flags:%s;",
info.abi_version, info.opt_level, info.use_subzero ? "subzero" : "",
key_url.spec().c_str(), timestamp.c_str(), info.etag.c_str(),
info.sandbox_isa.c_str(), info.extra_flags.c_str());
}
int PnaclTranslationCache::DoomEntriesBetween(base::Time initial,
base::Time end,
CompletionOnceCallback callback) {
return disk_cache_->DoomEntriesBetween(initial, end, std::move(callback));
}
} // namespace pnacl