Skip to content

Commit

Permalink
Extend PpdProvider to list available ppds. Add caching for same to ke…
Browse files Browse the repository at this point in the history
…ep load on QuirksServer manageable. Add unittests for same.

BUG=617254

Review-Url: https://codereview.chromium.org/2449183004
Cr-Commit-Position: refs/heads/master@{#429040}
  • Loading branch information
justincarlson authored and Commit bot committed Nov 1, 2016
1 parent 916a3bc commit 9c860c8
Show file tree
Hide file tree
Showing 6 changed files with 555 additions and 123 deletions.
160 changes: 137 additions & 23 deletions chromeos/printing/ppd_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,32 @@

#include "chromeos/printing/ppd_cache.h"

#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/json/json_parser.h"
#include "base/json/json_writer.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/time/time.h"
#include "base/values.h"
#include "crypto/sha2.h"
#include "net/base/io_buffer.h"
#include "net/filter/filter.h"
#include "net/filter/gzip_header.h"

using base::FilePath;
using std::string;

namespace chromeos {
namespace printing {
namespace {

// Name of the file we use to cache the list of available printer drivers from
// QuirksServer. This file resides in the cache directory.
const char kAvailablePrintersFilename[] = "all_printers.json";

// Return true if it looks like contents is already gzipped, false otherwise.
bool IsGZipped(const string& contents) {
bool IsGZipped(const std::string& contents) {
const char* ignored;
net::GZipHeader header;
return header.ReadMore(contents.data(), contents.size(), &ignored) ==
Expand All @@ -32,20 +38,24 @@ bool IsGZipped(const string& contents) {

class PpdCacheImpl : public PpdCache {
public:
explicit PpdCacheImpl(const FilePath& cache_base_dir)
: cache_base_dir_(cache_base_dir) {}
PpdCacheImpl(const base::FilePath& cache_base_dir,
const PpdCache::Options& options)
: cache_base_dir_(cache_base_dir),
available_printers_file_(
cache_base_dir.Append(kAvailablePrintersFilename)),
options_(options) {}
~PpdCacheImpl() override {}

// Public API functions.
base::Optional<FilePath> Find(
base::Optional<base::FilePath> Find(
const Printer::PpdReference& reference) const override {
base::Optional<FilePath> ret;
base::Optional<base::FilePath> ret;

// We can't know here if we have a gzipped or un-gzipped version, so just
// look for both.
FilePath contents_path_base = GetCachePathBase(reference);
for (const string& extension : {".ppd", ".ppd.gz"}) {
FilePath contents_path = contents_path_base.AddExtension(extension);
base::FilePath contents_path_base = GetCachePathBase(reference);
for (const std::string& extension : {".ppd", ".ppd.gz"}) {
base::FilePath contents_path = contents_path_base.AddExtension(extension);
if (base::PathExists(contents_path)) {
ret = contents_path;
break;
Expand All @@ -54,10 +64,12 @@ class PpdCacheImpl : public PpdCache {
return ret;
}

base::Optional<FilePath> Store(const Printer::PpdReference& reference,
const string& ppd_contents) override {
base::Optional<FilePath> ret;
FilePath contents_path = GetCachePathBase(reference).AddExtension(".ppd");
base::Optional<base::FilePath> Store(
const Printer::PpdReference& reference,
const std::string& ppd_contents) override {
base::Optional<base::FilePath> ret;
base::FilePath contents_path =
GetCachePathBase(reference).AddExtension(".ppd");
if (IsGZipped(ppd_contents)) {
contents_path = contents_path.AddExtension(".gz");
}
Expand All @@ -79,6 +91,69 @@ class PpdCacheImpl : public PpdCache {
return ret;
}

base::Optional<PpdProvider::AvailablePrintersMap> FindAvailablePrinters()
override {
std::string buf;
if (!MaybeReadAvailablePrintersCache(&buf)) {
return base::nullopt;
}
auto dict = base::DictionaryValue::From(base::JSONReader::Read(buf));
if (dict == nullptr) {
LOG(ERROR) << "Failed to deserialize available printers cache";
return base::nullopt;
}
PpdProvider::AvailablePrintersMap ret;
const base::ListValue* models;
std::string model;
for (base::DictionaryValue::Iterator it(*dict); !it.IsAtEnd();
it.Advance()) {
auto& out = ret[it.key()];
if (!it.value().GetAsList(&models)) {
LOG(ERROR) << "Skipping malformed printer make: " << it.key();
continue;
}
for (const auto& model_value : *models) {
if (model_value->GetAsString(&model)) {
out.push_back(model);
} else {
LOG(ERROR) << "Skipping malformed printer model in: " << it.key()
<< ". Expected a string, found a "
<< base::Value::GetTypeName(model_value->GetType());
}
}
}
return ret;
}

// Note we throw up our hands and fail (gracefully) to store if we encounter
// non-unicode things in the strings of |available_printers|. Since these
// strings come from a source we control, being less paranoid about these
// values seems reasonable.
void StoreAvailablePrinters(
const PpdProvider::AvailablePrintersMap& available_printers) override {
// Convert the map to Values, in preparation for jsonification.
base::DictionaryValue top_level;
for (const auto& entry : available_printers) {
auto printers = base::MakeUnique<base::ListValue>();
printers->AppendStrings(entry.second);
top_level.Set(entry.first, std::move(printers));
}
std::string contents;
if (!base::JSONWriter::Write(top_level, &contents)) {
LOG(ERROR) << "Failed to generate JSON";
return;
}
if (contents.size() > options_.max_available_list_cached_size) {
LOG(ERROR) << "Serialized available printers list too large (size is "
<< contents.size() << " bytes)";
return;
}
if (base::WriteFile(available_printers_file_, contents.data(),
contents.size()) != static_cast<int>(contents.size())) {
LOG(ERROR) << "Failed to write available printers cache";
}
}

private:
// Get the file path at which we expect to find a PPD if it's cached.
//
Expand All @@ -104,8 +179,8 @@ class PpdCacheImpl : public PpdCache {
//
// Note this function expects that the caller will append ".ppd", or ".ppd.gz"
// to the output as needed.
FilePath GetCachePathBase(const Printer::PpdReference& ref) const {
std::vector<string> pieces;
base::FilePath GetCachePathBase(const Printer::PpdReference& ref) const {
std::vector<std::string> pieces;
if (!ref.user_supplied_ppd_url.empty()) {
pieces.push_back("user_supplied_ppd_url:");
pieces.push_back(ref.user_supplied_ppd_url);
Expand All @@ -119,25 +194,64 @@ class PpdCacheImpl : public PpdCache {
NOTREACHED() << "PpdCache hashing empty PpdReference";
}
// The separator here is not needed, but makes debug output more readable.
string full_key = base::JoinString(pieces, "|");
string hashed_key = crypto::SHA256HashString(full_key);
string ascii_hash = base::HexEncode(hashed_key.data(), hashed_key.size());
std::string full_key = base::JoinString(pieces, "|");
std::string hashed_key = crypto::SHA256HashString(full_key);
std::string ascii_hash =
base::HexEncode(hashed_key.data(), hashed_key.size());
VLOG(3) << "PPD Cache key is " << full_key << " which hashes to "
<< ascii_hash;

return cache_base_dir_.Append(ascii_hash);
}

const FilePath cache_base_dir_;
// Try to read the available printers cache. Returns true on success. On
// success, |buf| will contain the contents of the file, otherwise it will be
// cleared.
bool MaybeReadAvailablePrintersCache(std::string* buf) {
buf->clear();

base::File cache_file(available_printers_file_,
base::File::FLAG_OPEN | base::File::FLAG_READ);
base::File::Info info;
if (cache_file.IsValid() && cache_file.GetInfo(&info) &&
(base::Time::Now() - info.last_modified <=
options_.max_available_list_staleness)) {
// We have a file that's recent enough to use.
if (!base::ReadFileToStringWithMaxSize(
available_printers_file_, buf,
options_.max_available_list_cached_size)) {
LOG(ERROR) << "Failed to read printer cache";
buf->clear();
return false;
}
return true;
}
// Either we don't have an openable file, or it's too old.
//
// If we have an invalid file and it's not valid for reasons other than
// NOT_FOUND, that's unexpected and worth logging. Otherwise this is
// a normal cache miss.
if (!cache_file.IsValid() &&
cache_file.error_details() != base::File::FILE_ERROR_NOT_FOUND) {
LOG(ERROR) << "Unexpected result when attempting to open printer cache: "
<< base::File::ErrorToString(cache_file.error_details());
}
return false;
}

const base::FilePath cache_base_dir_;
const base::FilePath available_printers_file_;
const PpdCache::Options options_;

DISALLOW_COPY_AND_ASSIGN(PpdCacheImpl);
};

} // namespace

// static
std::unique_ptr<PpdCache> PpdCache::Create(const FilePath& cache_base_dir) {
return base::MakeUnique<PpdCacheImpl>(cache_base_dir);
std::unique_ptr<PpdCache> PpdCache::Create(const base::FilePath& cache_base_dir,
const PpdCache::Options& options) {
return base::MakeUnique<PpdCacheImpl>(cache_base_dir, options);
}

} // namespace printing
Expand Down
33 changes: 32 additions & 1 deletion chromeos/printing/ppd_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@

#include "base/files/file_path.h"
#include "base/optional.h"
#include "base/time/time.h"
#include "chromeos/chromeos_export.h"
#include "chromeos/printing/ppd_provider.h"
#include "chromeos/printing/printer_configuration.h"

namespace chromeos {
Expand All @@ -25,8 +27,28 @@ namespace printing {
// re-run the resolution logic if we have new meta-information about a printer.
class CHROMEOS_EXPORT PpdCache {
public:
// Options that can be tweaked. These should all have sane defaults. This
// structure is copyable.
struct Options {
Options() {}

// If the cached available printer data is older than this, we will consider
// it stale and won't return it. A non-positive value here means we will
// always consider data stale (which is useful for tests).
// Default is 14 days.
base::TimeDelta max_available_list_staleness =
base::TimeDelta::FromDays(14);

// Size limit on the serialized cached available printer list, in bytes.
// This is just a check to make sure we don't consume ridiculous amounts of
// disk if we get bad data.
// Default is 10 MB
size_t max_available_list_cached_size = 10 * 1024 * 1024;
};

// Create and return a Ppdcache that uses cache_dir to store state.
static std::unique_ptr<PpdCache> Create(const base::FilePath& cache_base_dir);
static std::unique_ptr<PpdCache> Create(const base::FilePath& cache_base_dir,
const Options& options = Options());
virtual ~PpdCache() {}

// Find a PPD that was previously cached with the given reference. Note that
Expand All @@ -49,6 +71,15 @@ class CHROMEOS_EXPORT PpdCache {
virtual base::Optional<base::FilePath> Store(
const Printer::PpdReference& reference,
const std::string& ppd_contents) = 0;

// Return a map of available printers, if we have one available and it's
// not too stale.
virtual base::Optional<PpdProvider::AvailablePrintersMap>
FindAvailablePrinters() = 0;

// Store |available_printers|, replacing any existing entry.
virtual void StoreAvailablePrinters(
const PpdProvider::AvailablePrintersMap& available_printers) = 0;
};

} // namespace printing
Expand Down
43 changes: 41 additions & 2 deletions chromeos/printing/ppd_cache_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ class PpdCacheTest : public ::testing::Test {

// Make and return a cache for the test that uses a temporary directory
// which is cleaned up at the end of the test.
std::unique_ptr<PpdCache> CreateTestCache() {
return PpdCache::Create(ppd_cache_temp_dir_.GetPath());
std::unique_ptr<PpdCache> CreateTestCache(
const PpdCache::Options& options = PpdCache::Options()) {
return PpdCache::Create(ppd_cache_temp_dir_.GetPath(), options);
}

protected:
Expand Down Expand Up @@ -145,6 +146,44 @@ TEST_F(PpdCacheTest, StoredExtensions) {
EXPECT_EQ(".ppd.gz", cache->Store(ref, gzipped_contents).value().Extension());
}

// Test that we get back what we stored when we store an available printers
// list.
TEST_F(PpdCacheTest, StoreAndRetrieveAvailablePrinters) {
auto cache = CreateTestCache();

// Nothing stored, so should miss in the cache.
base::Optional<PpdProvider::AvailablePrintersMap> result =
cache->FindAvailablePrinters();
EXPECT_FALSE(result);

// Create something to store.
PpdProvider::AvailablePrintersMap a;
a["foo"] = {"bar", "baz", "sna"};
a["bar"] = {"c", "d", "e"};
a["baz"] = {"f", "g", "h"};

// Store it, get it back.
cache->StoreAvailablePrinters(a);
result = cache->FindAvailablePrinters();
ASSERT_TRUE(result);

EXPECT_EQ(a, result.value());
}

// When an entry is too old, we shouldn't return it.
TEST_F(PpdCacheTest, ExpireStaleAvailablePrinters) {
PpdCache::Options options;
// Expire stuff immediately by setting the staleness limit to 0.
options.max_available_list_staleness = base::TimeDelta();
auto cache = CreateTestCache(options);

// Store an empty map. (Contents don't really matter for this test).
cache->StoreAvailablePrinters(PpdProvider::AvailablePrintersMap());

// Should *miss* in the cache because the entry is already expired.
EXPECT_FALSE(cache->FindAvailablePrinters());
}

} // namespace
} // namespace printing
} // namespace chromeos
Loading

0 comments on commit 9c860c8

Please sign in to comment.