chromium/chromeos/printing/ppd_cache.cc

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/40285824): Remove this and convert code to safer constructs.
#pragma allow_unsafe_buffers
#endif

#include "chromeos/printing/ppd_cache.h"

#include <string_view>
#include <utility>
#include <vector>

#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/threading/scoped_blocking_call.h"
#include "base/time/time.h"
#include "base/values.h"
#include "chromeos/printing/printing_constants.h"
#include "crypto/sha2.h"
#include "net/base/io_buffer.h"
#include "net/filter/gzip_header.h"

namespace chromeos {
namespace {

// Return the (full) path to the file we expect to find the given key at.
base::FilePath FilePathForKey(const base::FilePath& base_dir,
                              const std::string& key) {
  return base_dir.Append(base::HexEncode(crypto::SHA256HashString(key)));
}

// If the cache doesn't already exist, create it.
void MaybeCreateCache(const base::FilePath& base_dir) {
  if (!base::PathExists(base_dir)) {
    base::CreateDirectory(base_dir);
  }
}

// Find implementation, blocks on file access.  Must be run on a thread that
// allows I/O.
PpdCache::FindResult FindImpl(const base::FilePath& cache_dir,
                              const std::string& key) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);

  PpdCache::FindResult result;
  result.success = false;
  if (!base::PathExists(cache_dir)) {
    // If the cache dir was missing, we'll miss anyway.
    return result;
  }

  base::File file(FilePathForKey(cache_dir, key),
                  base::File::FLAG_OPEN | base::File::FLAG_READ);

  base::File::Info info;
  if (!file.IsValid() || !file.GetInfo(&info))
    return result;

  if (info.size < static_cast<int64_t>(crypto::kSHA256Length) ||
      info.size > static_cast<int64_t>(kMaxPpdSizeBytes) +
                      static_cast<int64_t>(crypto::kSHA256Length)) {
    return result;
  }

  std::vector<char> buf(info.size);
  if (file.ReadAtCurrentPos(buf.data(), info.size) != info.size)
    return result;

  std::string_view contents(buf.data(), info.size - crypto::kSHA256Length);
  std::string_view checksum(buf.data() + info.size - crypto::kSHA256Length,
                            crypto::kSHA256Length);
  if (crypto::SHA256HashString(contents) != checksum) {
    LOG(ERROR) << "Bad checksum for cache key " << key;
    return result;
  }

  result.success = true;
  result.age = base::Time::Now() - info.last_modified;
  result.contents = std::string(contents);
  return result;
}

// Store implementation, blocks on file access.  Must be run on a thread that
// allows I/O.  If |age| is non-zero, explicitly set the age of the resulting
// file to be |age| before Now.
void StoreImpl(const base::FilePath& cache_dir,
               const std::string& key,
               const std::string& contents,
               base::TimeDelta age) {
  base::ScopedBlockingCall scoped_blocking_call(FROM_HERE,
                                                base::BlockingType::MAY_BLOCK);
  MaybeCreateCache(cache_dir);
  if (contents.size() > kMaxPpdSizeBytes) {
    LOG(ERROR) << "Ignoring attempt to cache large object";
  } else {
    auto path = FilePathForKey(cache_dir, key);
    base::File file(path,
                    base::File::FLAG_CREATE_ALWAYS | base::File::FLAG_WRITE);
    std::string checksum = crypto::SHA256HashString(contents);
    if (!file.IsValid() ||
        file.WriteAtCurrentPos(contents.data(), contents.size()) !=
            static_cast<int>(contents.size()) ||
        file.WriteAtCurrentPos(checksum.data(), checksum.size()) !=
            static_cast<int>(checksum.size())) {
      LOG(ERROR) << "Failed to create ppd cache file";
      file.Close();
      if (!base::DeleteFile(path)) {
        LOG(ERROR) << "Failed to cleanup failed creation.";
      }
    } else {
      // Successfully wrote the file, adjust the age if requested.
      if (!age.is_zero()) {
        base::Time mod_time = base::Time::Now() - age;
        file.SetTimes(mod_time, mod_time);
      }
    }
  }
}

// Implementation of the PpdCache that uses two separate task runners for Store
// and Fetch since the two operations have different priorities. Note that the
// two operations are not sequenced so there should be no expectation that a
// call to Find will return a file that was previously Stored until the Store
// callback is run.
class PpdCacheImpl : public PpdCache {
 public:
  explicit PpdCacheImpl(
      const base::FilePath& cache_base_dir,
      scoped_refptr<base::SequencedTaskRunner> fetch_task_runner,
      scoped_refptr<base::SequencedTaskRunner> store_task_runner)
      : cache_base_dir_(cache_base_dir),
        fetch_task_runner_(std::move(fetch_task_runner)),
        store_task_runner_(std::move(store_task_runner)) {}

  PpdCacheImpl(const PpdCacheImpl&) = delete;
  PpdCacheImpl& operator=(const PpdCacheImpl&) = delete;

  // Public API functions.
  void Find(const std::string& key, FindCallback cb) override {
    fetch_task_runner_->PostTaskAndReplyWithResult(
        FROM_HERE, base::BindOnce(&FindImpl, cache_base_dir_, key),
        std::move(cb));
  }

  // Store the given contents at the given key.  If cb is non-null, it will
  // be invoked on completion.
  void Store(const std::string& key, const std::string& contents) override {
    store_task_runner_->PostTask(
        FROM_HERE, base::BindOnce(&StoreImpl, cache_base_dir_, key, contents,
                                  base::TimeDelta()));
  }

  void StoreForTesting(const std::string& key,
                       const std::string& contents,
                       base::TimeDelta age) override {
    store_task_runner_->PostTask(
        FROM_HERE,
        base::BindOnce(&StoreImpl, cache_base_dir_, key, contents, age));
  }

 private:
  ~PpdCacheImpl() override = default;

  base::FilePath cache_base_dir_;
  scoped_refptr<base::SequencedTaskRunner> fetch_task_runner_;
  scoped_refptr<base::SequencedTaskRunner> store_task_runner_;
};

}  // namespace

// static
scoped_refptr<PpdCache> PpdCache::Create(const base::FilePath& cache_base_dir) {
  return scoped_refptr<PpdCache>(
      new PpdCacheImpl(cache_base_dir,
                       base::ThreadPool::CreateSequencedTaskRunner(
                           {base::TaskPriority::USER_VISIBLE, base::MayBlock(),
                            base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN}),
                       base::ThreadPool::CreateSequencedTaskRunner(
                           {base::TaskPriority::BEST_EFFORT, base::MayBlock(),
                            base::TaskShutdownBehavior::BLOCK_SHUTDOWN})));
}

scoped_refptr<PpdCache> PpdCache::CreateForTesting(
    const base::FilePath& cache_base_dir,
    scoped_refptr<base::SequencedTaskRunner> io_task_runner) {
  return scoped_refptr<PpdCache>(
      new PpdCacheImpl(cache_base_dir, io_task_runner, io_task_runner));
}

}  // namespace chromeos