// Copyright 2013 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 "components/nacl/browser/pnacl_translation_cache.h"
#include <string>
#include "base/callback.h"
#include "base/files/file_path.h"
#include "base/logging.h"
#include "base/strings/string_number_conversions.h"
#include "base/threading/thread_checker.h"
#include "components/nacl/common/pnacl_types.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"
using base::IntToString;
using content::BrowserThread;
namespace {
void CloseDiskCacheEntry(disk_cache::Entry* entry) { entry->Close(); }
} // namespace
namespace pnacl {
// This is in pnacl namespace instead of static so they can be used
// by the unit test.
const 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::RefCounted<PnaclTranslationCacheEntry> {
public:
static PnaclTranslationCacheEntry* GetReadEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
const GetNexeCallback& callback);
static PnaclTranslationCacheEntry* GetWriteEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
net::DrainableIOBuffer* write_nexe,
const CompletionCallback& callback);
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::RefCounted<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. Handle state
// transitions, track bytes transferred, and call the other helper methods.
void DispatchNext(int rv);
base::WeakPtr<PnaclTranslationCache> cache_;
std::string key_;
disk_cache::Entry* entry_;
CacheStep step_;
bool is_read_;
GetNexeCallback read_callback_;
CompletionCallback write_callback_;
scoped_refptr<net::DrainableIOBuffer> io_buf_;
base::ThreadChecker thread_checker_;
DISALLOW_COPY_AND_ASSIGN(PnaclTranslationCacheEntry);
};
// static
PnaclTranslationCacheEntry* PnaclTranslationCacheEntry::GetReadEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
const GetNexeCallback& callback) {
PnaclTranslationCacheEntry* entry(
new PnaclTranslationCacheEntry(cache, key, true));
entry->read_callback_ = callback;
return entry;
}
// static
PnaclTranslationCacheEntry* PnaclTranslationCacheEntry::GetWriteEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
net::DrainableIOBuffer* write_nexe,
const CompletionCallback& callback) {
PnaclTranslationCacheEntry* entry(
new PnaclTranslationCacheEntry(cache, key, false));
entry->io_buf_ = write_nexe;
entry->write_callback_ = callback;
return entry;
}
PnaclTranslationCacheEntry::PnaclTranslationCacheEntry(
base::WeakPtr<PnaclTranslationCache> cache,
const std::string& key,
bool is_read)
: cache_(cache),
key_(key),
entry_(NULL),
step_(UNINITIALIZED),
is_read_(is_read) {}
PnaclTranslationCacheEntry::~PnaclTranslationCacheEntry() {
// Ensure we have called the user's callback
if (step_ != FINISHED) {
if (!read_callback_.is_null()) {
BrowserThread::PostTask(
BrowserThread::IO,
FROM_HERE,
base::Bind(read_callback_,
net::ERR_ABORTED,
scoped_refptr<net::DrainableIOBuffer>()));
}
if (!write_callback_.is_null()) {
BrowserThread::PostTask(BrowserThread::IO,
FROM_HERE,
base::Bind(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() {
int rv = cache_->backend()->OpenEntry(
key_,
&entry_,
base::Bind(&PnaclTranslationCacheEntry::DispatchNext, this));
if (rv != net::ERR_IO_PENDING)
DispatchNext(rv);
}
void PnaclTranslationCacheEntry::CreateEntry() {
int rv = cache_->backend()->CreateEntry(
key_,
&entry_,
base::Bind(&PnaclTranslationCacheEntry::DispatchNext, this));
if (rv != net::ERR_IO_PENDING)
DispatchNext(rv);
}
void PnaclTranslationCacheEntry::WriteEntry(int offset, int len) {
DCHECK(io_buf_->BytesRemaining() == len);
int rv = entry_->WriteData(
1,
offset,
io_buf_.get(),
len,
base::Bind(&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::Bind(&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();
}
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE, base::Bind(&CloseDiskCacheEntry, entry_));
Finish(rv);
}
void PnaclTranslationCacheEntry::Finish(int rv) {
step_ = FINISHED;
if (is_read_) {
if (!read_callback_.is_null()) {
BrowserThread::PostTask(BrowserThread::IO,
FROM_HERE,
base::Bind(read_callback_, rv, io_buf_));
}
} else {
if (!write_callback_.is_null()) {
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE, base::Bind(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_ = new net::DrainableIOBuffer(
new net::IOBuffer(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_ = NULL;
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) {
is_read_
? ReadEntry(io_buf_->BytesConsumed(), io_buf_->BytesRemaining())
: 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 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,
const CompletionCallback& callback) {
int rv = disk_cache::CreateCacheBackend(
cache_type,
net::CACHE_BACKEND_DEFAULT,
cache_dir,
cache_size,
true /* force_initialize */,
BrowserThread::GetMessageLoopProxyForThread(BrowserThread::CACHE).get(),
NULL, /* dummy net log */
&disk_cache_,
base::Bind(&PnaclTranslationCache::OnCreateBackendComplete, AsWeakPtr()));
if (rv == net::ERR_IO_PENDING) {
init_callback_ = callback;
}
return rv;
}
void PnaclTranslationCache::OnCreateBackendComplete(int rv) {
if (rv < 0) {
LOG(ERROR) << "Backend init failed:" << net::ErrorToString(rv);
}
// Invoke our client's callback function.
if (!init_callback_.is_null()) {
BrowserThread::PostTask(
BrowserThread::IO, FROM_HERE, base::Bind(init_callback_, rv));
}
}
//////////////////////////////////////////////////////////////////////
// High-level API
void PnaclTranslationCache::StoreNexe(const std::string& key,
net::DrainableIOBuffer* nexe_data,
const CompletionCallback& callback) {
PnaclTranslationCacheEntry* entry = PnaclTranslationCacheEntry::GetWriteEntry(
AsWeakPtr(), key, nexe_data, callback);
open_entries_[entry] = entry;
entry->Start();
}
void PnaclTranslationCache::GetNexe(const std::string& key,
const GetNexeCallback& callback) {
PnaclTranslationCacheEntry* entry =
PnaclTranslationCacheEntry::GetReadEntry(AsWeakPtr(), key, callback);
open_entries_[entry] = entry;
entry->Start();
}
int PnaclTranslationCache::InitOnDisk(const base::FilePath& cache_directory,
const CompletionCallback& callback) {
in_memory_ = false;
return Init(net::PNACL_CACHE, cache_directory, 0 /* auto size */, callback);
}
int PnaclTranslationCache::InitInMemory(const CompletionCallback& callback) {
in_memory_ = true;
return Init(net::MEMORY_CACHE, base::FilePath(), kMaxMemCacheSize, callback);
}
int PnaclTranslationCache::Size() {
if (!disk_cache_)
return -1;
return disk_cache_->GetEntryCount();
}
// static
std::string PnaclTranslationCache::GetKey(const nacl::PnaclCacheInfo& info) {
if (!info.pexe_url.is_valid() || info.abi_version < 0 || info.opt_level < 0)
return std::string();
std::string retval("ABI:");
retval += IntToString(info.abi_version) + ";" + "opt:" +
IntToString(info.opt_level) + ";" + "URL:";
// Filter the username, password, and ref components from the URL
GURL::Replacements replacements;
replacements.ClearUsername();
replacements.ClearPassword();
replacements.ClearRef();
GURL key_url(info.pexe_url.ReplaceComponents(replacements));
retval += key_url.spec() + ";";
// You would think that there is already code to format base::Time values
// somewhere, but I haven't found it yet. In any case, doing it ourselves
// here means we can keep the format stable.
base::Time::Exploded exploded;
info.last_modified.UTCExplode(&exploded);
if (info.last_modified.is_null() || !exploded.HasValidValues()) {
memset(&exploded, 0, sizeof(exploded));
}
retval += "modified:" + IntToString(exploded.year) + ":" +
IntToString(exploded.month) + ":" +
IntToString(exploded.day_of_month) + ":" +
IntToString(exploded.hour) + ":" + IntToString(exploded.minute) +
":" + IntToString(exploded.second) + ":" +
IntToString(exploded.millisecond) + ":UTC;";
retval += "etag:" + info.etag;
return retval;
}
int PnaclTranslationCache::DoomEntriesBetween(
base::Time initial,
base::Time end,
const CompletionCallback& callback) {
return disk_cache_->DoomEntriesBetween(initial, end, callback);
}
} // namespace pnacl