910e62b5创建于 1月15日历史提交
// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "content/browser/code_cache/generated_code_cache.h"

#include <iostream>
#include <string_view>

#include "base/compiler_specific.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/numerics/byte_conversions.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/time/time.h"
#include "components/services/storage/public/cpp/big_io_buffer.h"
#include "content/common/features.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/common/content_client.h"
#include "content/public/common/url_constants.h"
#include "crypto/hash.h"
#include "net/base/completion_once_callback.h"
#include "net/base/features.h"
#include "net/base/network_isolation_key.h"
#include "net/base/url_util.h"
#include "net/http/http_cache.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/common/loader/code_cache_util.h"
#include "third_party/blink/public/common/scheme_registry.h"
#include "url/gurl.h"

using storage::BigIOBuffer;

namespace content {

namespace {

constexpr char kSeparator[] = " \n";

// In this and `CheckValidContext` it's expected to receive valid URLs that can
// be used as keys to the code cache. The relevant checks (for ex: resource_url
// is valid, origin_lock is not opaque etc.,) must be done prior to requesting
// the code cache.
//
// These functions don't enforce anything in the production code. They are here
// to make the assumptions explicit and to catch any errors when DCHECKs are
// enabled.
void CheckValidResource(const GURL& resource_url,
                        GeneratedCodeCache::CodeCacheType cache_type) {
  // If the resource url is invalid don't cache the code.
  DCHECK(resource_url.is_valid());
  bool resource_url_is_chrome_or_chrome_untrusted =
      resource_url.SchemeIs(content::kChromeUIScheme) ||
      resource_url.SchemeIs(content::kChromeUIUntrustedScheme);
  DCHECK(
      resource_url.SchemeIsHTTPOrHTTPS() ||
      resource_url_is_chrome_or_chrome_untrusted ||
      blink::CommonSchemeRegistry::IsExtensionScheme(resource_url.GetScheme()));

  if (!blink::features::IsPersistentCacheForCodeCacheEnabled()) {
    // The chrome and chrome-untrusted schemes are only used with the WebUI code
    // cache type when PersistentCache is not used. Otherwise, PersistentCache
    // segments WebUI from non-WebUI in multiple ways to prevent privilege
    // escalation, using both `GetCacheId` and
    // `CheckSecurityForAccessingCodeCacheData`.
    DCHECK_EQ(resource_url_is_chrome_or_chrome_untrusted,
              cache_type == GeneratedCodeCache::kWebUIJavaScript);
  }
}

void CheckValidContext(const GURL& origin_lock,
                       GeneratedCodeCache::CodeCacheType cache_type) {
  // |origin_lock| should be either empty or should have
  // Http/Https/chrome/chrome-untrusted schemes and it should not be a URL with
  // opaque origin. Empty origin_locks are allowed when the renderer is not
  // locked to an origin.
  bool origin_lock_is_chrome_or_chrome_untrusted =
      origin_lock.SchemeIs(content::kChromeUIScheme) ||
      origin_lock.SchemeIs(content::kChromeUIUntrustedScheme);
  DCHECK(origin_lock.is_empty() ||
         ((origin_lock.SchemeIsHTTPOrHTTPS() ||
           origin_lock_is_chrome_or_chrome_untrusted ||
           blink::CommonSchemeRegistry::IsExtensionScheme(
               origin_lock.GetScheme())) &&
          !url::Origin::Create(origin_lock).opaque()));

  if (!blink::features::IsPersistentCacheForCodeCacheEnabled()) {
    // The chrome and chrome-untrusted schemes are only used with the WebUI code
    // cache type when PersistentCache is not used. Otherwise, PersistentCache
    // segments WebUI from non-WebUI in multiple ways to prevent privilege
    // escalation, using both `GetCacheId` and
    // `CheckSecurityForAccessingCodeCacheData`.
    DCHECK_EQ(origin_lock_is_chrome_or_chrome_untrusted,
              cache_type == GeneratedCodeCache::kWebUIJavaScript);
  }
}

// Generates the cache key for the given |resource_url|, |origin_lock| and
// |nik|.
//   |resource_url| is the url corresponding to the requested resource.
//   |origin_lock| is the origin that the renderer which requested this
//   resource is locked to.
//   |nik| is the network isolation key that consists of top-level-site that
//   initiated the request.
// For example, if SitePerProcess is enabled and http://script.com/script1.js is
// requested by http://example.com, then http://script.com/script.js is the
// resource_url and http://example.com is the origin_lock.
//
// This returns the key by concatenating the serialized url, origin lock and nik
// with a separator in between. |origin_lock| could be empty when renderer is
// not locked to an origin (ex: SitePerProcess is disabled) and it is safe to
// use only |resource_url| as the key in such cases.
// TODO(wjmaclean): Either convert this to use a SiteInfo object, or convert it
// to something not based on URLs.
std::string GetCacheKey(const GURL& resource_url,
                        const GURL& origin_lock,
                        const net::NetworkIsolationKey& nik,
                        GeneratedCodeCache::CodeCacheType cache_type) {
  return base::StrCat(
      {GeneratedCodeCache::GetResourceKey(resource_url, cache_type),

       // Add a separator between URL and origin to avoid any possibility of
       // attacks by crafting the URL. URLs do not contain any control ASCII
       // characters, and also space is encoded. So use ' \n' as a
       // separator.
       kSeparator,

       GeneratedCodeCache::GetContextKey(origin_lock, nik, cache_type)});
}

constexpr size_t kResponseTimeSizeInBytes = sizeof(int64_t);
constexpr size_t kDataSizeInBytes = sizeof(uint32_t);
constexpr size_t kHeaderSizeInBytes =
    kResponseTimeSizeInBytes + kDataSizeInBytes;
// The SHA-256 checksum is used as the key for the de-duplicated code data. We
// must convert the checksum to a string key in a way that is guaranteed not to
// match a key generated by |GetCacheKey|. A simple way to do this is to convert
// it to a hex number string, which is twice as long as the checksum.
constexpr size_t kSHAKeySizeInBytes = 2 * crypto::hash::kSha256Size;

// This is the threshold for storing the header and cached code in stream 0,
// which is read into memory on opening an entry. JavaScript code caching stores
// time stamps with no data, or timestamps with just a tag, and we observe many
// 8 and 16 byte reads and writes. Make the threshold larger to speed up small
// code entries too.
constexpr size_t kInlineDataLimit = 4096;
// This is the maximum size for code that will be stored under the key generated
// by |GetCacheKey|. Each origin will get its own copy of the generated code for
// a given resource. Code that is larger than this limit will be stored under a
// key derived from the code checksum, and each origin using a given resource
// gets its own small entry under the key generated by |GetCacheKey| that holds
// the hash, enabling a two stage lookup. This limit was determined empirically
// by a Finch experiment.
constexpr size_t kDedicatedDataLimit = 16384;

void WriteCommonDataHeader(net::IOBufferWithSize* buffer,
                           const base::Time& response_time,
                           uint32_t data_size) {
  auto header = buffer->span().first<kHeaderSizeInBytes>();
  auto [header_time, header_size] = header.split_at<kResponseTimeSizeInBytes>();
  header_time.copy_from(base::I64ToLittleEndian(
      response_time.ToDeltaSinceWindowsEpoch().InMicroseconds()));
  header_size.copy_from(base::U32ToLittleEndian(data_size));
}

void ReadCommonDataHeader(net::IOBufferWithSize* buffer,
                          base::Time* response_time,
                          uint32_t* data_size) {
  auto header = buffer->span().first<kHeaderSizeInBytes>();
  auto [header_time, header_size] = header.split_at<kResponseTimeSizeInBytes>();
  int64_t raw_response_time = base::I64FromLittleEndian(header_time);
  *response_time = base::Time::FromDeltaSinceWindowsEpoch(
      base::Microseconds(raw_response_time));
  *data_size = base::U32FromLittleEndian(header_size);
}

static_assert(mojo_base::BigBuffer::kMaxInlineBytes <=
                  std::numeric_limits<int>::max(),
              "Buffer size calculations may overflow int");

net::CacheType CodeCacheTypeToNetCacheType(
    GeneratedCodeCache::CodeCacheType type) {
  switch (type) {
    case GeneratedCodeCache::CodeCacheType::kJavaScript:
      return net::GENERATED_BYTE_CODE_CACHE;
    case GeneratedCodeCache::CodeCacheType::kWebAssembly:
      return net::GENERATED_NATIVE_CODE_CACHE;
    case GeneratedCodeCache::CodeCacheType::kWebUIJavaScript:
      return net::GENERATED_WEBUI_BYTE_CODE_CACHE;
  }
  NOTREACHED();
}

void CollectStatisticsForEmbedderWebUIPages(
    const GURL& resource_url,
    const GURL& origin_lock,
    GeneratedCodeCache::CacheEntryStatus entry_status) {
  const content::ContentBrowserClient* browser_client =
      GetContentClient()->browser();
  CHECK(browser_client);
  const std::string resource_hostname =
      browser_client->GetWebUIHostnameForCodeCacheMetrics(resource_url);
  const std::string origin_hostname =
      browser_client->GetWebUIHostnameForCodeCacheMetrics(origin_lock);

  if (!resource_hostname.empty()) {
    base::UmaHistogramEnumeration(
        base::StrCat({"SiteIsolatedCodeCache.JS.WebUI.",
                      std::move(resource_hostname), ".Resource.Behaviour"}),
        entry_status);
  }

  if (!origin_hostname.empty()) {
    base::UmaHistogramEnumeration(
        base::StrCat({"SiteIsolatedCodeCache.JS.WebUI.",
                      std::move(origin_hostname), ".Origin.Behaviour"}),
        entry_status);
  }
}

}  // namespace

// static
std::string GeneratedCodeCache::GetResourceKey(
    const GURL& resource_url,
    GeneratedCodeCache::CodeCacheType cache_type) {
  CheckValidResource(resource_url, cache_type);

  return blink::UrlToCodeCacheKey(resource_url);
}

// static
std::string GeneratedCodeCache::GetContextKey(
    const GURL& origin_lock,
    const net::NetworkIsolationKey& nik,
    GeneratedCodeCache::CodeCacheType cache_type) {
  CheckValidContext(origin_lock, cache_type);

  std::string key;

  if (origin_lock.is_valid()) {
    key.append(net::SimplifyUrlForRequest(origin_lock).spec());
  }

  if (net::HttpCache::IsSplitCacheEnabled() &&
      base::FeatureList::IsEnabled(
          net::features::kSplitCodeCacheByNetworkIsolationKey)) {
    // TODO(crbug.com/40232395):  Transient NIKs return nullopt when
    // their ToCacheKeyString() method is invoked, as they generally shouldn't
    // be written to disk. This code is currently reached for transient NIKs,
    // which needs to be fixed.
    if (!nik.IsTransient()) {
      key.append(kSeparator);
      key.append(*nik.ToCacheKeyString());
    }
  }

  return key;
}

bool GeneratedCodeCache::IsValidHeader(
    scoped_refptr<net::IOBufferWithSize> small_buffer) const {
  size_t buffer_size = small_buffer->size();
  if (buffer_size < kHeaderSizeInBytes) {
    return false;
  }
  base::Time response_time;
  uint32_t data_size = 0;
  ReadCommonDataHeader(small_buffer.get(), &response_time, &data_size);
  if (data_size <= kInlineDataLimit) {
    return buffer_size == kHeaderSizeInBytes + data_size;
  }
  if (!ShouldDeduplicateEntry(data_size)) {
    return buffer_size == kHeaderSizeInBytes;
  }
  return buffer_size == kHeaderSizeInBytes + kSHAKeySizeInBytes;
}

std::string GeneratedCodeCache::GetResourceURLFromKey(const std::string& key) {
  constexpr size_t kPrefixStringLen = std::size(blink::kCodeCacheKeyPrefix) - 1;
  // |key| may not have a prefix and separator (e.g. for deduplicated entries).
  // In that case, return an empty string.
  const size_t separator_index = key.find(kSeparator);
  if (key.length() < kPrefixStringLen || separator_index == std::string::npos) {
    return std::string();
  }

  std::string resource_url =
      key.substr(kPrefixStringLen, separator_index - kPrefixStringLen);
  return resource_url;
}

void GeneratedCodeCache::CollectStatistics(
    const GURL& resource_url,
    const GURL& origin_lock,
    GeneratedCodeCache::CacheEntryStatus status) {
  switch (cache_type_) {
    case GeneratedCodeCache::CodeCacheType::kJavaScript:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.JS.Behaviour", status);
      break;
    case GeneratedCodeCache::CodeCacheType::kWebUIJavaScript:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.JS.Behaviour", status);
      UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.JS.WebUI.Behaviour",
                                status);
      CollectStatisticsForEmbedderWebUIPages(resource_url, origin_lock, status);
      break;
    case GeneratedCodeCache::CodeCacheType::kWebAssembly:
      UMA_HISTOGRAM_ENUMERATION("SiteIsolatedCodeCache.WASM.Behaviour", status);
      break;
  }
}

// Stores the information about a pending request while disk backend is
// being initialized or another request for the same key is live.
class GeneratedCodeCache::PendingOperation {
 public:
  PendingOperation(Operation op,
                   const GURL& resource_url,
                   const GURL& origin_lock,
                   const std::string& key,
                   scoped_refptr<net::IOBufferWithSize> small_buffer,
                   scoped_refptr<BigIOBuffer> large_buffer)
      : op_(op),
        resource_url_(resource_url),
        origin_lock_(origin_lock),
        key_(key),
        small_buffer_(small_buffer),
        large_buffer_(large_buffer) {
    DCHECK(Operation::kWrite == op_ || Operation::kWriteWithSHAKey == op_);
  }

  PendingOperation(Operation op,
                   const GURL& resource_url,
                   const GURL& origin_lock,
                   const std::string& key,
                   ReadDataCallback read_callback)
      : op_(op),
        resource_url_(resource_url),
        origin_lock_(origin_lock),
        key_(key),
        read_callback_(std::move(read_callback)) {
    DCHECK_EQ(Operation::kFetch, op_);
  }

  PendingOperation(Operation op,
                   const GURL& resource_url,
                   const GURL& origin_lock,
                   const std::string& key,
                   const base::Time& response_time,
                   const base::TimeTicks start_time,
                   scoped_refptr<net::IOBufferWithSize> small_buffer,
                   scoped_refptr<BigIOBuffer> large_buffer,
                   ReadDataCallback read_callback)
      : op_(op),
        resource_url_(resource_url),
        origin_lock_(origin_lock),
        key_(key),
        response_time_(response_time),
        start_time_(start_time),
        small_buffer_(small_buffer),
        large_buffer_(large_buffer),
        read_callback_(std::move(read_callback)) {
    DCHECK_EQ(Operation::kFetchWithSHAKey, op_);
  }

  PendingOperation(Operation op,
                   const GURL& resource_url,
                   const GURL& origin_lock,
                   const std::string& key)
      : op_(op),
        resource_url_(resource_url),
        origin_lock_(origin_lock),
        key_(key) {
    DCHECK_EQ(Operation::kDelete, op_);
  }

  PendingOperation(Operation op, GetBackendCallback backend_callback)
      : op_(op), backend_callback_(std::move(backend_callback)) {
    DCHECK_EQ(Operation::kGetBackend, op_);
  }

  ~PendingOperation();

  Operation operation() const { return op_; }
  const std::string& key() const { return key_; }
  const GURL& resource_url() const { return resource_url_; }
  const GURL& origin_lock() const { return origin_lock_; }
  scoped_refptr<net::IOBufferWithSize> small_buffer() { return small_buffer_; }
  scoped_refptr<BigIOBuffer> large_buffer() { return large_buffer_; }
  ReadDataCallback TakeReadCallback() { return std::move(read_callback_); }
  void RunReadCallback(GeneratedCodeCache* code_cache,
                       base::Time response_time,
                       mojo_base::BigBuffer data) {
    if (code_cache->cache_type_ == CodeCacheType::kJavaScript) {
      const bool code_cache_hit = data.size() > 0;
      const bool in_memory_code_cache_hit = code_cache->lru_cache_.Has(key_);
      if (code_cache_hit && !in_memory_code_cache_hit) {
        code_cache->lru_cache_.Put(key_, response_time, base::span(data));
      }
      if (!base::FeatureList::IsEnabled(features::kInMemoryCodeCache)) {
        if (code_cache_hit && in_memory_code_cache_hit) {
          base::UmaHistogramTimes(
              "SiteIsolatedCodeCache.JS.MemoryBackedCodeCachePotentialImpact",
              base::TimeTicks::Now() - start_time_);
        }
        base::UmaHistogramBoolean("SiteIsolatedCodeCache.JS.Hit",
                                  code_cache_hit);
        base::UmaHistogramBoolean(
            "SiteIsolatedCodeCache.JS.PotentialMemoryBackedCodeCacheHit",
            in_memory_code_cache_hit);
      }
    }
    std::move(read_callback_).Run(response_time, std::move(data));
  }
  GetBackendCallback TakeBackendCallback() {
    return std::move(backend_callback_);
  }

  // These are called by Fetch operations to hold the buffers we create once the
  // entry is opened.
  void set_small_buffer(scoped_refptr<net::IOBufferWithSize> small_buffer) {
    DCHECK_EQ(Operation::kFetch, op_);
    small_buffer_ = small_buffer;
  }
  void set_large_buffer(scoped_refptr<BigIOBuffer> large_buffer) {
    DCHECK_EQ(Operation::kFetch, op_);
    large_buffer_ = large_buffer;
  }

  // This returns the site-specific response time for merged code entries.
  const base::Time& response_time() const {
    DCHECK_EQ(Operation::kFetchWithSHAKey, op_);
    return response_time_;
  }

  base::TimeTicks start_time() const { return start_time_; }

  // These are called by write and fetch operations to track buffer completions
  // and signal when the operation has finished, and whether it was successful.
  bool succeeded() const { return succeeded_; }

  bool AddBufferCompletion(bool succeeded) {
    DCHECK(op_ == Operation::kWrite || op_ == Operation::kWriteWithSHAKey ||
           op_ == Operation::kFetch || op_ == Operation::kFetchWithSHAKey);
    if (!succeeded)
      succeeded_ = false;
    DCHECK_GT(2, completions_);
    completions_++;
    return completions_ == 2;
  }

 private:
  const Operation op_;
  const GURL resource_url_;
  const GURL origin_lock_;
  const std::string key_;
  const base::Time response_time_;
  const base::TimeTicks start_time_ = base::TimeTicks::Now();
  scoped_refptr<net::IOBufferWithSize> small_buffer_;
  scoped_refptr<BigIOBuffer> large_buffer_;
  ReadDataCallback read_callback_;
  GetBackendCallback backend_callback_;
  int completions_ = 0;
  bool succeeded_ = true;
};

GeneratedCodeCache::PendingOperation::~PendingOperation() = default;

GeneratedCodeCache::GeneratedCodeCache(const base::FilePath& path,
                                       int max_size_bytes,
                                       CodeCacheType cache_type)
    : backend_state_(kInitializing),
      path_(path),
      max_size_bytes_(max_size_bytes),
      cache_type_(cache_type),
      lru_cache_(max_size_bytes == 0
                     ? kLruCacheCapacity
                     : std::min<int64_t>(kLruCacheCapacity, max_size_bytes)) {
  CreateBackend();
}

GeneratedCodeCache::~GeneratedCodeCache() = default;

void GeneratedCodeCache::GetBackend(GetBackendCallback callback) {
  switch (backend_state_) {
    case kFailed:
      std::move(callback).Run(nullptr);
      return;
    case kInitialized:
      std::move(callback).Run(backend_.get());
      return;
    case kInitializing:
      pending_ops_.emplace(std::make_unique<PendingOperation>(
          Operation::kGetBackend, std::move(callback)));
      return;
  }
}

void GeneratedCodeCache::WriteEntry(const GURL& url,
                                    const GURL& origin_lock,
                                    const net::NetworkIsolationKey& nik,
                                    const base::Time& response_time,
                                    mojo_base::BigBuffer data) {
  if (backend_state_ == kFailed) {
    // Silently fail the request.
    CollectStatistics(url, origin_lock, CacheEntryStatus::kError);
    return;
  }

  // Reject buffers that are large enough to cause overflow problems.
  if (data.size() >= std::numeric_limits<int32_t>::max())
    return;

  const std::string key = GetCacheKey(url, origin_lock, nik, cache_type_);
  if (cache_type_ == CodeCacheType::kJavaScript) {
    lru_cache_.Put(key, response_time, base::span(data));
  }

  scoped_refptr<net::IOBufferWithSize> small_buffer;
  scoped_refptr<BigIOBuffer> large_buffer;
  const uint32_t data_size = static_cast<uint32_t>(data.size());
  // We have three different cache entry layouts, depending on data size.
  if (data_size <= kInlineDataLimit) {
    // 1. Inline
    // [stream0] response time, size, data
    // [stream1] <empty>
    small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(
        kHeaderSizeInBytes + data.size());
    // Copy |data| into the small buffer.
    small_buffer->span().subspan(kHeaderSizeInBytes).copy_from(data);
    // Write 0 bytes and truncate stream 1 to clear any stale data.
    large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer());
  } else if (!ShouldDeduplicateEntry(data_size)) {
    // 2. Dedicated
    // [stream0] response time, size
    // [stream1] data
    small_buffer =
        base::MakeRefCounted<net::IOBufferWithSize>(kHeaderSizeInBytes);
    large_buffer = base::MakeRefCounted<BigIOBuffer>(std::move(data));
  } else {
    // 3. Indirect
    // [stream0] response time, size, checksum
    // [stream1] <empty>
    // [stream0 (checksum key entry)] <empty>
    // [stream1 (checksum key entry)] data

    // Make a copy of the data before hashing. A compromised renderer could
    // change shared memory before we can compute the hash and write the data.
    // TODO(crbug.com/40151989) Eliminate this copy when the shared memory can't
    // be written by the sender.
    mojo_base::BigBuffer copy(base::span{data});
    if (copy.size() != data.size())
      return;
    data = mojo_base::BigBuffer();  // Release the old buffer.
    std::string checksum_key = base::HexEncode(crypto::hash::Sha256(copy));
    DCHECK_EQ(kSHAKeySizeInBytes, checksum_key.length());
    small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(
        kHeaderSizeInBytes + checksum_key.length());
    // Copy |checksum_key| into the small buffer.
    small_buffer->span()
        .subspan(kHeaderSizeInBytes)
        .copy_from(base::as_byte_span(checksum_key));
    // Write 0 bytes and truncate stream 1 to clear any stale data.
    large_buffer = base::MakeRefCounted<BigIOBuffer>(mojo_base::BigBuffer());

    // Issue another write operation for the code, with the checksum as the key
    // and nothing in the header.
    auto small_buffer2 = base::MakeRefCounted<net::IOBufferWithSize>(0);
    auto large_buffer2 = base::MakeRefCounted<BigIOBuffer>(std::move(copy));
    auto op2 = std::make_unique<PendingOperation>(
        Operation::kWriteWithSHAKey, url, origin_lock, checksum_key,
        small_buffer2, large_buffer2);
    EnqueueOperation(std::move(op2));
  }
  WriteCommonDataHeader(small_buffer.get(), response_time, data_size);

  // Create the write operation.
  auto op = std::make_unique<PendingOperation>(
      Operation::kWrite, url, origin_lock, key, small_buffer, large_buffer);
  EnqueueOperation(std::move(op));
}

void GeneratedCodeCache::FetchEntry(const GURL& url,
                                    const GURL& origin_lock,
                                    const net::NetworkIsolationKey& nik,
                                    ReadDataCallback read_data_callback) {
  if (backend_state_ == kFailed) {
    CollectStatistics(url, origin_lock, CacheEntryStatus::kError);
    // Fail the request.
    std::move(read_data_callback).Run(base::Time(), mojo_base::BigBuffer());
    return;
  }

  std::string key = GetCacheKey(url, origin_lock, nik, cache_type_);
  auto op = std::make_unique<PendingOperation>(
      Operation::kFetch, url, origin_lock, key, std::move(read_data_callback));
  EnqueueOperation(std::move(op));
}

void GeneratedCodeCache::DeleteEntry(const GURL& url,
                                     const GURL& origin_lock,
                                     const net::NetworkIsolationKey& nik) {
  if (backend_state_ == kFailed) {
    // Silently fail.
    CollectStatistics(url, origin_lock, CacheEntryStatus::kError);
    return;
  }

  std::string key = GetCacheKey(url, origin_lock, nik, cache_type_);
  auto op = std::make_unique<PendingOperation>(Operation::kDelete, url,
                                               origin_lock, key);
  EnqueueOperation(std::move(op));

  lru_cache_.Delete(key);
}

void GeneratedCodeCache::CreateBackend() {
  // If the initialization of the existing cache fails, this call would delete
  // all the contents and recreates a new one.
  disk_cache::BackendResult result = disk_cache::CreateCacheBackend(
      CodeCacheTypeToNetCacheType(cache_type_), net::CACHE_BACKEND_SIMPLE,
      /*file_operations=*/nullptr, path_, max_size_bytes_,
      disk_cache::ResetHandling::kResetOnError, /*net_log=*/nullptr,
      base::BindOnce(&GeneratedCodeCache::DidCreateBackend,
                     weak_ptr_factory_.GetWeakPtr()));
  if (result.net_error != net::ERR_IO_PENDING) {
    DidCreateBackend(std::move(result));
  }
}

void GeneratedCodeCache::DidCreateBackend(disk_cache::BackendResult result) {
  if (result.net_error != net::OK) {
    backend_state_ = kFailed;
  } else {
    backend_ = std::move(result.backend);
    backend_state_ = kInitialized;
  }
  IssuePendingOperations();
}

void GeneratedCodeCache::EnqueueOperation(
    std::unique_ptr<PendingOperation> op) {
  if (backend_state_ != kInitialized) {
    // Insert it into the list of pending operations while the backend is
    // still being opened.
    pending_ops_.emplace(std::move(op));
    return;
  }

  EnqueueOperationAndIssueIfNext(std::move(op));
}

void GeneratedCodeCache::IssuePendingOperations() {
  // Issue any operations that were received while creating the backend.
  while (!pending_ops_.empty()) {
    // Take ownership of the next PendingOperation here. |op| will either be
    // moved onto a queue in active_entries_map_ or issued and completed in
    // |DoPendingGetBackend|.
    std::unique_ptr<PendingOperation> op = std::move(pending_ops_.front());
    pending_ops_.pop();
    // Properly enqueue/dequeue ops for Write, Fetch, and Delete.
    if (op->operation() != Operation::kGetBackend) {
      EnqueueOperationAndIssueIfNext(std::move(op));
    } else {
      // There is no queue for get backend operations. Issue them immediately.
      IssueOperation(op.get());
    }
  }
}

void GeneratedCodeCache::IssueOperation(PendingOperation* op) {
  switch (op->operation()) {
    case kFetch:
    case kFetchWithSHAKey:
      FetchEntryImpl(op);
      break;
    case kWrite:
    case kWriteWithSHAKey:
      WriteEntryImpl(op);
      break;
    case kDelete:
      DeleteEntryImpl(op);
      break;
    case kGetBackend:
      DoPendingGetBackend(op);
      break;
  }
}

void GeneratedCodeCache::WriteEntryImpl(PendingOperation* op) {
  DCHECK(Operation::kWrite == op->operation() ||
         Operation::kWriteWithSHAKey == op->operation());
  if (backend_state_ != kInitialized) {
    // Silently fail the request.
    CloseOperationAndIssueNext(op);
    return;
  }

  disk_cache::EntryResult result = backend_->OpenOrCreateEntry(
      op->key(), net::LOW,
      base::BindOnce(&GeneratedCodeCache::OpenCompleteForWrite,
                     weak_ptr_factory_.GetWeakPtr(), op));

  if (result.net_error() != net::ERR_IO_PENDING) {
    OpenCompleteForWrite(op, std::move(result));
  }
}

void GeneratedCodeCache::OpenCompleteForWrite(
    PendingOperation* op,
    disk_cache::EntryResult entry_result) {
  DCHECK(Operation::kWrite == op->operation() ||
         Operation::kWriteWithSHAKey == op->operation());
  if (entry_result.net_error() != net::OK) {
    CollectStatistics(op->resource_url(), op->origin_lock(),
                      CacheEntryStatus::kError);
    CloseOperationAndIssueNext(op);
    return;
  }

  if (entry_result.opened()) {
    CollectStatistics(op->resource_url(), op->origin_lock(),
                      CacheEntryStatus::kUpdate);
  } else {
    CollectStatistics(op->resource_url(), op->origin_lock(),
                      CacheEntryStatus::kCreate);
  }

  disk_cache::ScopedEntryPtr entry(entry_result.ReleaseEntry());
  // There should be a valid entry if the open was successful.
  DCHECK(entry);

  // For merged entries, don't write if the entry already exists.
  if (op->operation() == Operation::kWriteWithSHAKey) {
    int small_size = entry->GetDataSize(kSmallDataStream);
    int large_size = entry->GetDataSize(kLargeDataStream);
    if (small_size == 0 && large_size == op->large_buffer()->size()) {
      // Skip overwriting with identical data.
      CloseOperationAndIssueNext(op);
      return;
    }
    // Otherwise, there shouldn't be any data for this entry yet.
    DCHECK_EQ(0, small_size);
    DCHECK_EQ(0, large_size);
  }

  // Write the small data first, truncating.
  auto small_buffer = op->small_buffer();
  int result = entry->WriteData(
      kSmallDataStream, 0, small_buffer.get(), small_buffer->size(),
      base::BindOnce(&GeneratedCodeCache::WriteSmallBufferComplete,
                     weak_ptr_factory_.GetWeakPtr(), op),
      true);

  if (result != net::ERR_IO_PENDING) {
    WriteSmallBufferComplete(op, result);
  }

  // Write the large data, truncating.
  auto large_buffer = op->large_buffer();
  result = entry->WriteData(
      kLargeDataStream, 0, large_buffer.get(), large_buffer->size(),
      base::BindOnce(&GeneratedCodeCache::WriteLargeBufferComplete,
                     weak_ptr_factory_.GetWeakPtr(), op),
      true);

  if (result != net::ERR_IO_PENDING) {
    WriteLargeBufferComplete(op, result);
  }
}

void GeneratedCodeCache::WriteSmallBufferComplete(PendingOperation* op,
                                                  int rv) {
  DCHECK(Operation::kWrite == op->operation() ||
         Operation::kWriteWithSHAKey == op->operation());
  if (op->AddBufferCompletion(rv == op->small_buffer()->size())) {
    WriteComplete(op);
  }
}

void GeneratedCodeCache::WriteLargeBufferComplete(PendingOperation* op,
                                                  int rv) {
  DCHECK(Operation::kWrite == op->operation() ||
         Operation::kWriteWithSHAKey == op->operation());
  if (op->AddBufferCompletion(rv == op->large_buffer()->size())) {
    WriteComplete(op);
  }
}

void GeneratedCodeCache::WriteComplete(PendingOperation* op) {
  DCHECK(Operation::kWrite == op->operation() ||
         Operation::kWriteWithSHAKey == op->operation());
  if (!op->succeeded()) {
    // The write failed; record the failure and doom the entry here.
    CollectStatistics(op->resource_url(), op->origin_lock(),
                      CacheEntryStatus::kWriteFailed);
    DoomEntry(op);
  }
  CloseOperationAndIssueNext(op);
}

void GeneratedCodeCache::FetchEntryImpl(PendingOperation* op) {
  DCHECK(Operation::kFetch == op->operation() ||
         Operation::kFetchWithSHAKey == op->operation());
  if (base::FeatureList::IsEnabled(features::kInMemoryCodeCache)) {
    if (auto result = lru_cache_.Get(op->key())) {
      op->RunReadCallback(this, result->response_time, std::move(result->data));
      CloseOperationAndIssueNext(op);
      return;
    }
  }

  if (backend_state_ != kInitialized) {
    op->RunReadCallback(this, base::Time(), mojo_base::BigBuffer());
    CloseOperationAndIssueNext(op);
    return;
  }

  // This is a part of loading cycle and hence should run with a high priority.
  disk_cache::EntryResult result = backend_->OpenEntry(
      op->key(), net::HIGHEST,
      base::BindOnce(&GeneratedCodeCache::OpenCompleteForRead,
                     weak_ptr_factory_.GetWeakPtr(), op));
  if (result.net_error() != net::ERR_IO_PENDING) {
    OpenCompleteForRead(op, std::move(result));
  }
}

void GeneratedCodeCache::OpenCompleteForRead(
    PendingOperation* op,
    disk_cache::EntryResult entry_result) {
  DCHECK(Operation::kFetch == op->operation() ||
         Operation::kFetchWithSHAKey == op->operation());
  if (entry_result.net_error() != net::OK) {
    CollectStatistics(op->resource_url(), op->origin_lock(),
                      CacheEntryStatus::kMiss);
    op->RunReadCallback(this, base::Time(), mojo_base::BigBuffer());
    CloseOperationAndIssueNext(op);
    return;
  }

  disk_cache::ScopedEntryPtr entry(entry_result.ReleaseEntry());
  // There should be a valid entry if the open was successful.
  DCHECK(entry);

  int small_size = entry->GetDataSize(kSmallDataStream);
  int large_size = entry->GetDataSize(kLargeDataStream);
  scoped_refptr<net::IOBufferWithSize> small_buffer;
  scoped_refptr<BigIOBuffer> large_buffer;
  if (op->operation() == Operation::kFetch) {
    small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(small_size);
    op->set_small_buffer(small_buffer);
    large_buffer = base::MakeRefCounted<BigIOBuffer>(large_size);
    op->set_large_buffer(large_buffer);
  } else {
    small_buffer = op->small_buffer();
    large_buffer = op->large_buffer();
    DCHECK_EQ(small_size, small_buffer->size());
    DCHECK_EQ(large_size, large_buffer->size());
  }

  // Read the small data first.
  int result = entry->ReadData(
      kSmallDataStream, 0, small_buffer.get(), small_buffer->size(),
      base::BindOnce(&GeneratedCodeCache::ReadSmallBufferComplete,
                     weak_ptr_factory_.GetWeakPtr(), op));

  if (result != net::ERR_IO_PENDING) {
    ReadSmallBufferComplete(op, result);
  }

  // Skip the large read if data is in the small read.
  if (large_size == 0)
    return;

  // Read the large data.
  result = entry->ReadData(
      kLargeDataStream, 0, large_buffer.get(), large_buffer->size(),
      base::BindOnce(&GeneratedCodeCache::ReadLargeBufferComplete,
                     weak_ptr_factory_.GetWeakPtr(), op));
  if (result != net::ERR_IO_PENDING) {
    ReadLargeBufferComplete(op, result);
  }
}

void GeneratedCodeCache::ReadSmallBufferComplete(PendingOperation* op, int rv) {
  DCHECK(Operation::kFetch == op->operation() ||
         Operation::kFetchWithSHAKey == op->operation());
  bool no_header = op->operation() == Operation::kFetchWithSHAKey;
  bool succeeded = (rv == op->small_buffer()->size() &&
                    (no_header || IsValidHeader(op->small_buffer())));
  CollectStatistics(
      op->resource_url(), op->origin_lock(),
      succeeded ? CacheEntryStatus::kHit : CacheEntryStatus::kMiss);

  if (op->AddBufferCompletion(succeeded))
    ReadComplete(op);

  // Small reads must finish now since no large read is pending.
  if (op->large_buffer()->size() == 0)
    ReadLargeBufferComplete(op, 0);
}

void GeneratedCodeCache::ReadLargeBufferComplete(PendingOperation* op, int rv) {
  DCHECK(Operation::kFetch == op->operation() ||
         Operation::kFetchWithSHAKey == op->operation());
  if (op->AddBufferCompletion(rv == op->large_buffer()->size()))
    ReadComplete(op);
}

void GeneratedCodeCache::ReadComplete(PendingOperation* op) {
  DCHECK(Operation::kFetch == op->operation() ||
         Operation::kFetchWithSHAKey == op->operation());
  if (!op->succeeded()) {
    op->RunReadCallback(this, base::Time(), mojo_base::BigBuffer());
    // Doom this entry since it is inaccessible.
    DoomEntry(op);
  } else {
    if (op->operation() != Operation::kFetchWithSHAKey) {
      base::Time response_time;
      uint32_t data_size = 0;
      ReadCommonDataHeader(op->small_buffer().get(), &response_time,
                           &data_size);
      if (data_size <= kInlineDataLimit) {
        // Small data. Copy the data from the small buffer.
        DCHECK_EQ(0, op->large_buffer()->size());
        mojo_base::BigBuffer data(
            op->small_buffer()->span().subspan(kHeaderSizeInBytes, data_size));
        op->RunReadCallback(this, response_time, std::move(data));
      } else if (!ShouldDeduplicateEntry(data_size)) {
        // Large data below the merging threshold, or deduplication is disabled.
        // Return the large buffer.
        op->RunReadCallback(this, response_time,
                            op->large_buffer()->TakeBuffer());
      } else {
        // Very large data. Create the second fetch using the checksum as key.
        DCHECK_EQ(static_cast<int>(kHeaderSizeInBytes + kSHAKeySizeInBytes),
                  op->small_buffer()->size());
        std::string checksum_key(
            UNSAFE_TODO(op->small_buffer()->data() + kHeaderSizeInBytes),
            kSHAKeySizeInBytes);
        auto small_buffer = base::MakeRefCounted<net::IOBufferWithSize>(0);
        auto large_buffer = base::MakeRefCounted<BigIOBuffer>(data_size);
        auto op2 = std::make_unique<PendingOperation>(
            Operation::kFetchWithSHAKey, op->resource_url(), op->origin_lock(),
            checksum_key, response_time, op->start_time(), small_buffer,
            large_buffer, op->TakeReadCallback());
        EnqueueOperation(std::move(op2));
      }
    } else {
      // Large merged code data with no header. |op| holds the response time.
      op->RunReadCallback(this, op->response_time(),
                          op->large_buffer()->TakeBuffer());
    }
  }
  CloseOperationAndIssueNext(op);
}

void GeneratedCodeCache::DeleteEntryImpl(PendingOperation* op) {
  DCHECK_EQ(Operation::kDelete, op->operation());
  DoomEntry(op);
  CloseOperationAndIssueNext(op);
}

void GeneratedCodeCache::DoomEntry(PendingOperation* op) {
  // Write, Fetch, and Delete may all doom an entry.
  DCHECK_NE(Operation::kGetBackend, op->operation());
  // Entries shouldn't be doomed if the backend hasn't been initialized.
  DCHECK_EQ(kInitialized, backend_state_);
  CollectStatistics(op->resource_url(), op->origin_lock(),
                    CacheEntryStatus::kClear);
  backend_->DoomEntry(op->key(), net::LOWEST, net::CompletionOnceCallback());
}

void GeneratedCodeCache::IssueNextOperation(const std::string& key) {
  auto it = active_entries_map_.find(key);
  if (it == active_entries_map_.end())
    return;

  DCHECK(!it->second.empty());
  IssueOperation(it->second.front().get());
}

void GeneratedCodeCache::CloseOperationAndIssueNext(PendingOperation* op) {
  // Dequeue op, keeping it alive long enough to issue another op.
  std::unique_ptr<PendingOperation> keep_alive = DequeueOperation(op);
  IssueNextOperation(op->key());
}

void GeneratedCodeCache::EnqueueOperationAndIssueIfNext(
    std::unique_ptr<PendingOperation> op) {
  // GetBackend ops have no key and shouldn't be enqueued here.
  DCHECK_NE(Operation::kGetBackend, op->operation());
  auto it = active_entries_map_.find(op->key());
  bool can_issue = false;
  if (it == active_entries_map_.end()) {
    it = active_entries_map_.emplace(op->key(), PendingOperationQueue()).first;
    can_issue = true;
  }
  const std::string& key = op->key();
  it->second.emplace(std::move(op));
  if (can_issue)
    IssueNextOperation(key);
}

std::unique_ptr<GeneratedCodeCache::PendingOperation>
GeneratedCodeCache::DequeueOperation(PendingOperation* op) {
  auto it = active_entries_map_.find(op->key());
  CHECK(it != active_entries_map_.end());
  DCHECK(!it->second.empty());
  std::unique_ptr<PendingOperation> result = std::move(it->second.front());
  // |op| should be at the front.
  DCHECK_EQ(op, result.get());
  it->second.pop();
  // Delete the queue if it becomes empty.
  if (it->second.empty()) {
    active_entries_map_.erase(it);
  }
  return result;
}

void GeneratedCodeCache::DoPendingGetBackend(PendingOperation* op) {
  // |op| is kept alive in |IssuePendingOperations| for the duration of this
  // call. We shouldn't access |op| after returning from this function.
  DCHECK_EQ(kGetBackend, op->operation());
  if (backend_state_ == kInitialized) {
    op->TakeBackendCallback().Run(backend_.get());
  } else {
    DCHECK_EQ(backend_state_, kFailed);
    op->TakeBackendCallback().Run(nullptr);
  }
}

bool GeneratedCodeCache::IsDeduplicationEnabled() const {
  // Deduplication is disabled in the WebUI code cache, as an additional defense
  // against privilege escalation in case there is a bug in the deduplication
  // logic.
  return cache_type_ != kWebUIJavaScript;
}

bool GeneratedCodeCache::ShouldDeduplicateEntry(uint32_t data_size) const {
  return data_size > kDedicatedDataLimit && IsDeduplicationEnabled();
}

void GeneratedCodeCache::SetLastUsedTimeForTest(
    const GURL& resource_url,
    const GURL& origin_lock,
    const net::NetworkIsolationKey& nik,
    base::Time time,
    base::OnceClosure user_callback) {
  // This is used only for tests. So reasonable to assume that backend is
  // initialized here. All other operations handle the case when backend was not
  // yet opened.
  DCHECK_EQ(backend_state_, kInitialized);
  auto split = base::SplitOnceCallback(std::move(user_callback));

  disk_cache::EntryResultCallback callback = base::BindOnce(
      &GeneratedCodeCache::OpenCompleteForSetLastUsedForTest,
      weak_ptr_factory_.GetWeakPtr(), time, std::move(split.first));

  std::string key = GetCacheKey(resource_url, origin_lock, nik, cache_type_);
  disk_cache::EntryResult result =
      backend_->OpenEntry(key, net::LOWEST, std::move(callback));
  if (result.net_error() != net::ERR_IO_PENDING) {
    OpenCompleteForSetLastUsedForTest(time, std::move(split.second),
                                      std::move(result));
  }
}

void GeneratedCodeCache::ClearInMemoryCache() {
  lru_cache_.Clear();
}

void GeneratedCodeCache::OpenCompleteForSetLastUsedForTest(
    base::Time time,
    base::OnceClosure callback,
    disk_cache::EntryResult result) {
  DCHECK_EQ(result.net_error(), net::OK);
  {
    disk_cache::ScopedEntryPtr disk_entry(result.ReleaseEntry());
    DCHECK(disk_entry);
    disk_entry->SetLastUsedTimeForTest(time);
  }
  std::move(callback).Run();
}

void GeneratedCodeCache::CollectStatisticsForTest(
    const GURL& resource_url,
    const GURL& origin_lock,
    GeneratedCodeCache::CacheEntryStatus status) {
  CollectStatistics(resource_url, origin_lock, status);
}

}  // namespace content