fune/dom/cache/DBSchema.cpp
Csoregi Natalia c382c6a7ca Backed out 16 changesets (bug 1247687) for frequent string bundle related crashes with PDF viewer (bug 1806064). a=backout
Backed out changeset 721f612fd09f (bug 1247687)
Backed out changeset c6c5750cf713 (bug 1247687)
Backed out changeset 5d05ab0c7cde (bug 1247687)
Backed out changeset 2429599729cb (bug 1247687)
Backed out changeset 55f13fb4ee3f (bug 1247687)
Backed out changeset 354711cf113a (bug 1247687)
Backed out changeset 40b8abaf1c0b (bug 1247687)
Backed out changeset 0c9650a1ac48 (bug 1247687)
Backed out changeset e7b103c79b1a (bug 1247687)
Backed out changeset 4dbd510fb042 (bug 1247687)
Backed out changeset 9276c7e1ddd9 (bug 1247687)
Backed out changeset 6ee318df6641 (bug 1247687)
Backed out changeset 6c129bd72b61 (bug 1247687)
Backed out changeset 4b0a4fcc6894 (bug 1247687)
Backed out changeset 34680059b9f0 (bug 1247687)
Backed out changeset 85b827971a48 (bug 1247687)
2022-12-17 11:27:32 +02:00

2967 lines
114 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "mozilla/dom/cache/DBSchema.h"
#include "ipc/IPCMessageUtils.h"
#include "mozIStorageConnection.h"
#include "mozIStorageStatement.h"
#include "mozStorageHelper.h"
#include "mozilla/BasePrincipal.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/StaticPrefs_extensions.h"
#include "mozilla/Telemetry.h"
#include "mozilla/dom/HeadersBinding.h"
#include "mozilla/dom/InternalHeaders.h"
#include "mozilla/dom/InternalResponse.h"
#include "mozilla/dom/RequestBinding.h"
#include "mozilla/dom/ResponseBinding.h"
#include "mozilla/dom/cache/CacheCommon.h"
#include "mozilla/dom/cache/CacheTypes.h"
#include "mozilla/dom/cache/SavedTypes.h"
#include "mozilla/dom/cache/TypeUtils.h"
#include "mozilla/dom/cache/Types.h"
#include "mozilla/dom/quota/ResultExtensions.h"
#include "mozilla/net/MozURL.h"
#include "mozilla/psm/TransportSecurityInfo.h"
#include "nsCOMPtr.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsComponentManagerUtils.h"
#include "nsHttp.h"
#include "nsIContentPolicy.h"
#include "nsICryptoHash.h"
#include "nsNetCID.h"
#include "nsPrintfCString.h"
#include "nsTArray.h"
namespace mozilla::dom::cache::db {
const int32_t kFirstShippedSchemaVersion = 15;
namespace {
// ## Firefox 57 Cache API v25/v26/v27 Schema Hack Info
// ### Overview
// In Firefox 57 we introduced Cache API schema version 26 and Quota Manager
// schema v3 to support tracking padding for opaque responses. Unfortunately,
// Firefox 57 is a big release that may potentially result in users downgrading
// to Firefox 56 due to 57 retiring add-ons. These schema changes have the
// unfortunate side-effect of causing QuotaManager and all its clients to break
// if the user downgrades to 56. In order to avoid making a bad situation
// worse, we're now retrofitting 57 so that Firefox 56 won't freak out.
//
// ### Implementation
// We're introducing a new schema version 27 that uses an on-disk schema version
// of v25. We differentiate v25 from v27 by the presence of the column added
// by v26. This translates to:
// - v25: on-disk schema=25, no "response_padding_size" column in table
// "entries".
// - v26: on-disk schema=26, yes "response_padding_size" column in table
// "entries".
// - v27: on-disk schema=25, yes "response_padding_size" column in table
// "entries".
//
// ### Fallout
// Firefox 57 is happy because it sees schema 27 and everything is as it
// expects.
//
// Firefox 56 non-DEBUG build is fine/happy, but DEBUG builds will not be.
// - Our QuotaClient will invoke `NS_WARNING("Unknown Cache file found!");`
// at QuotaManager init time. This is harmless but annoying and potentially
// misleading.
// - The DEBUG-only Validate() call will error out whenever an attempt is made
// to open a DOM Cache database because it will notice the schema is broken
// and there is no attempt at recovery.
//
const int32_t kHackyDowngradeSchemaVersion = 25;
const int32_t kHackyPaddingSizePresentVersion = 27;
//
// Update this whenever the DB schema is changed.
const int32_t kLatestSchemaVersion = 28;
// ---------
// The following constants define the SQL schema. These are defined in the
// same order the SQL should be executed in CreateOrMigrateSchema(). They are
// broken out as constants for convenient use in validation and migration.
// ---------
// The caches table is the single source of truth about what Cache
// objects exist for the origin. The contents of the Cache are stored
// in the entries table that references back to caches.
//
// The caches table is also referenced from storage. Rows in storage
// represent named Cache objects. There are cases, however, where
// a Cache can still exist, but not be in a named Storage. For example,
// when content is still using the Cache after CacheStorage::Delete()
// has been run.
//
// For now, the caches table mainly exists for data integrity with
// foreign keys, but could be expanded to contain additional cache object
// information.
//
// AUTOINCREMENT is necessary to prevent CacheId values from being reused.
const char kTableCaches[] =
"CREATE TABLE caches ("
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT "
")";
// Security blobs are quite large and duplicated for every Response from
// the same https origin. This table is used to de-duplicate this data.
const char kTableSecurityInfo[] =
"CREATE TABLE security_info ("
"id INTEGER NOT NULL PRIMARY KEY, "
"hash BLOB NOT NULL, " // first 8-bytes of the sha1 hash of data column
"data BLOB NOT NULL, " // full security info data, usually a few KB
"refcount INTEGER NOT NULL"
")";
// Index the smaller hash value instead of the large security data blob.
const char kIndexSecurityInfoHash[] =
"CREATE INDEX security_info_hash_index ON security_info (hash)";
const char kTableEntries[] =
"CREATE TABLE entries ("
"id INTEGER NOT NULL PRIMARY KEY, "
"request_method TEXT NOT NULL, "
"request_url_no_query TEXT NOT NULL, "
"request_url_no_query_hash BLOB NOT NULL, " // first 8-bytes of sha1 hash
"request_url_query TEXT NOT NULL, "
"request_url_query_hash BLOB NOT NULL, " // first 8-bytes of sha1 hash
"request_referrer TEXT NOT NULL, "
"request_headers_guard INTEGER NOT NULL, "
"request_mode INTEGER NOT NULL, "
"request_credentials INTEGER NOT NULL, "
"request_contentpolicytype INTEGER NOT NULL, "
"request_cache INTEGER NOT NULL, "
"request_body_id TEXT NULL, "
"response_type INTEGER NOT NULL, "
"response_status INTEGER NOT NULL, "
"response_status_text TEXT NOT NULL, "
"response_headers_guard INTEGER NOT NULL, "
"response_body_id TEXT NULL, "
"response_security_info_id INTEGER NULL REFERENCES security_info(id), "
"response_principal_info TEXT NOT NULL, "
"cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
"request_redirect INTEGER NOT NULL, "
"request_referrer_policy INTEGER NOT NULL, "
"request_integrity TEXT NOT NULL, "
"request_url_fragment TEXT NOT NULL, "
"response_padding_size INTEGER NULL "
// New columns must be added at the end of table to migrate and
// validate properly.
")";
// Create an index to support the QueryCache() matching algorithm. This
// needs to quickly find entries in a given Cache that match the request
// URL. The url query is separated in order to support the ignoreSearch
// option. Finally, we index hashes of the URL values instead of the
// actual strings to avoid excessive disk bloat. The index will duplicate
// the contents of the columsn in the index. The hash index will prune
// the vast majority of values from the query result so that normal
// scanning only has to be done on a few values to find an exact URL match.
const char kIndexEntriesRequest[] =
"CREATE INDEX entries_request_match_index "
"ON entries (cache_id, request_url_no_query_hash, "
"request_url_query_hash)";
const char kTableRequestHeaders[] =
"CREATE TABLE request_headers ("
"name TEXT NOT NULL, "
"value TEXT NOT NULL, "
"entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
")";
const char kTableResponseHeaders[] =
"CREATE TABLE response_headers ("
"name TEXT NOT NULL, "
"value TEXT NOT NULL, "
"entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
")";
// We need an index on response_headers, but not on request_headers,
// because we quickly need to determine if a VARY header is present.
const char kIndexResponseHeadersName[] =
"CREATE INDEX response_headers_name_index "
"ON response_headers (name)";
const char kTableResponseUrlList[] =
"CREATE TABLE response_url_list ("
"url TEXT NOT NULL, "
"entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
")";
// NOTE: key allows NULL below since that is how "" is represented
// in a BLOB column. We use BLOB to avoid encoding issues
// with storing DOMStrings.
const char kTableStorage[] =
"CREATE TABLE storage ("
"namespace INTEGER NOT NULL, "
"key BLOB NULL, "
"cache_id INTEGER NOT NULL REFERENCES caches(id), "
"PRIMARY KEY(namespace, key) "
")";
// ---------
// End schema definition
// ---------
const uint32_t kMaxEntriesPerStatement = 255;
const uint32_t kPageSize = 4 * 1024;
// Grow the database in chunks to reduce fragmentation
const uint32_t kGrowthSize = 32 * 1024;
const uint32_t kGrowthPages = kGrowthSize / kPageSize;
static_assert(kGrowthSize % kPageSize == 0,
"Growth size must be multiple of page size");
// Only release free pages when we have more than this limit
const int32_t kMaxFreePages = kGrowthPages;
// Limit WAL journal to a reasonable size
const uint32_t kWalAutoCheckpointSize = 512 * 1024;
const uint32_t kWalAutoCheckpointPages = kWalAutoCheckpointSize / kPageSize;
static_assert(kWalAutoCheckpointSize % kPageSize == 0,
"WAL checkpoint size must be multiple of page size");
} // namespace
// If any of the static_asserts below fail, it means that you have changed
// the corresponding WebIDL enum in a way that may be incompatible with the
// existing data stored in the DOM Cache. You would need to update the Cache
// database schema accordingly and adjust the failing static_assert.
static_assert(int(HeadersGuardEnum::None) == 0 &&
int(HeadersGuardEnum::Request) == 1 &&
int(HeadersGuardEnum::Request_no_cors) == 2 &&
int(HeadersGuardEnum::Response) == 3 &&
int(HeadersGuardEnum::Immutable) == 4 &&
HeadersGuardEnumValues::Count == 5,
"HeadersGuardEnum values are as expected");
static_assert(int(ReferrerPolicy::_empty) == 0 &&
int(ReferrerPolicy::No_referrer) == 1 &&
int(ReferrerPolicy::No_referrer_when_downgrade) == 2 &&
int(ReferrerPolicy::Origin) == 3 &&
int(ReferrerPolicy::Origin_when_cross_origin) == 4 &&
int(ReferrerPolicy::Unsafe_url) == 5 &&
int(ReferrerPolicy::Same_origin) == 6 &&
int(ReferrerPolicy::Strict_origin) == 7 &&
int(ReferrerPolicy::Strict_origin_when_cross_origin) == 8 &&
ReferrerPolicyValues::Count == 9,
"ReferrerPolicy values are as expected");
static_assert(int(RequestMode::Same_origin) == 0 &&
int(RequestMode::No_cors) == 1 &&
int(RequestMode::Cors) == 2 &&
int(RequestMode::Navigate) == 3 &&
RequestModeValues::Count == 4,
"RequestMode values are as expected");
static_assert(int(RequestCredentials::Omit) == 0 &&
int(RequestCredentials::Same_origin) == 1 &&
int(RequestCredentials::Include) == 2 &&
RequestCredentialsValues::Count == 3,
"RequestCredentials values are as expected");
static_assert(int(RequestCache::Default) == 0 &&
int(RequestCache::No_store) == 1 &&
int(RequestCache::Reload) == 2 &&
int(RequestCache::No_cache) == 3 &&
int(RequestCache::Force_cache) == 4 &&
int(RequestCache::Only_if_cached) == 5 &&
RequestCacheValues::Count == 6,
"RequestCache values are as expected");
static_assert(int(RequestRedirect::Follow) == 0 &&
int(RequestRedirect::Error) == 1 &&
int(RequestRedirect::Manual) == 2 &&
RequestRedirectValues::Count == 3,
"RequestRedirect values are as expected");
static_assert(int(ResponseType::Basic) == 0 && int(ResponseType::Cors) == 1 &&
int(ResponseType::Default) == 2 &&
int(ResponseType::Error) == 3 &&
int(ResponseType::Opaque) == 4 &&
int(ResponseType::Opaqueredirect) == 5 &&
ResponseTypeValues::Count == 6,
"ResponseType values are as expected");
// If the static_asserts below fails, it means that you have changed the
// Namespace enum in a way that may be incompatible with the existing data
// stored in the DOM Cache. You would need to update the Cache database schema
// accordingly and adjust the failing static_assert.
static_assert(DEFAULT_NAMESPACE == 0 && CHROME_ONLY_NAMESPACE == 1 &&
NUMBER_OF_NAMESPACES == 2,
"Namespace values are as expected");
// If the static_asserts below fails, it means that you have changed the
// nsContentPolicy enum in a way that may be incompatible with the existing data
// stored in the DOM Cache. You would need to update the Cache database schema
// accordingly and adjust the failing static_assert.
static_assert(
nsIContentPolicy::TYPE_INVALID == 0 && nsIContentPolicy::TYPE_OTHER == 1 &&
nsIContentPolicy::TYPE_SCRIPT == 2 &&
nsIContentPolicy::TYPE_IMAGE == 3 &&
nsIContentPolicy::TYPE_STYLESHEET == 4 &&
nsIContentPolicy::TYPE_OBJECT == 5 &&
nsIContentPolicy::TYPE_DOCUMENT == 6 &&
nsIContentPolicy::TYPE_SUBDOCUMENT == 7 &&
nsIContentPolicy::TYPE_PING == 10 &&
nsIContentPolicy::TYPE_XMLHTTPREQUEST == 11 &&
nsIContentPolicy::TYPE_OBJECT_SUBREQUEST == 12 &&
nsIContentPolicy::TYPE_DTD == 13 && nsIContentPolicy::TYPE_FONT == 14 &&
nsIContentPolicy::TYPE_MEDIA == 15 &&
nsIContentPolicy::TYPE_WEBSOCKET == 16 &&
nsIContentPolicy::TYPE_CSP_REPORT == 17 &&
nsIContentPolicy::TYPE_XSLT == 18 &&
nsIContentPolicy::TYPE_BEACON == 19 &&
nsIContentPolicy::TYPE_FETCH == 20 &&
nsIContentPolicy::TYPE_IMAGESET == 21 &&
nsIContentPolicy::TYPE_WEB_MANIFEST == 22 &&
nsIContentPolicy::TYPE_INTERNAL_SCRIPT == 23 &&
nsIContentPolicy::TYPE_INTERNAL_WORKER == 24 &&
nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER == 25 &&
nsIContentPolicy::TYPE_INTERNAL_EMBED == 26 &&
nsIContentPolicy::TYPE_INTERNAL_OBJECT == 27 &&
nsIContentPolicy::TYPE_INTERNAL_FRAME == 28 &&
nsIContentPolicy::TYPE_INTERNAL_IFRAME == 29 &&
nsIContentPolicy::TYPE_INTERNAL_AUDIO == 30 &&
nsIContentPolicy::TYPE_INTERNAL_VIDEO == 31 &&
nsIContentPolicy::TYPE_INTERNAL_TRACK == 32 &&
nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST == 33 &&
nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE == 34 &&
nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER == 35 &&
nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD == 36 &&
nsIContentPolicy::TYPE_INTERNAL_IMAGE == 37 &&
nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD == 38 &&
nsIContentPolicy::TYPE_INTERNAL_STYLESHEET == 39 &&
nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD == 40 &&
nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON == 41 &&
nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS == 42 &&
nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD == 43 &&
nsIContentPolicy::TYPE_SPECULATIVE == 44 &&
nsIContentPolicy::TYPE_INTERNAL_MODULE == 45 &&
nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD == 46 &&
nsIContentPolicy::TYPE_INTERNAL_DTD == 47 &&
nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD == 48 &&
nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET == 49 &&
nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET == 50 &&
nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD == 51 &&
nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT == 52 &&
nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT == 53 &&
nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD == 54 &&
nsIContentPolicy::TYPE_UA_FONT == 55 &&
nsIContentPolicy::TYPE_WEB_IDENTITY == 57,
"nsContentPolicyType values are as expected");
namespace {
using EntryId = int32_t;
struct IdCount {
explicit IdCount(int32_t aId) : mId(aId), mCount(1) {}
int32_t mId;
int32_t mCount;
};
using EntryIds = AutoTArray<EntryId, 256>;
static Result<EntryIds, nsresult> QueryAll(mozIStorageConnection& aConn,
CacheId aCacheId);
static Result<EntryIds, nsresult> QueryCache(mozIStorageConnection& aConn,
CacheId aCacheId,
const CacheRequest& aRequest,
const CacheQueryParams& aParams,
uint32_t aMaxResults = UINT32_MAX);
static Result<bool, nsresult> MatchByVaryHeader(mozIStorageConnection& aConn,
const CacheRequest& aRequest,
EntryId entryId);
// Returns a success tuple containing the deleted body ids, deleted security ids
// and deleted padding size.
static Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>,
nsresult>
DeleteEntries(mozIStorageConnection& aConn,
const nsTArray<EntryId>& aEntryIdList);
static Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>,
nsresult>
DeleteAllCacheEntries(mozIStorageConnection& aConn, CacheId& aCacheId);
static Result<int32_t, nsresult> InsertSecurityInfo(
mozIStorageConnection& aConn, nsICryptoHash& aCrypto,
nsITransportSecurityInfo* aSecurityInfo);
static nsresult DeleteSecurityInfo(mozIStorageConnection& aConn, int32_t aId,
int32_t aCount);
static nsresult DeleteSecurityInfoList(
mozIStorageConnection& aConn,
const nsTArray<IdCount>& aDeletedStorageIdList);
static nsresult InsertEntry(mozIStorageConnection& aConn, CacheId aCacheId,
const CacheRequest& aRequest,
const nsID* aRequestBodyId,
const CacheResponse& aResponse,
const nsID* aResponseBodyId);
static Result<SavedResponse, nsresult> ReadResponse(
mozIStorageConnection& aConn, EntryId aEntryId);
static Result<SavedRequest, nsresult> ReadRequest(mozIStorageConnection& aConn,
EntryId aEntryId);
static void AppendListParamsToQuery(nsACString& aQuery, size_t aLen);
static nsresult BindListParamsToQuery(mozIStorageStatement& aState,
const Span<const EntryId>& aEntryIdList);
static nsresult BindId(mozIStorageStatement& aState, const nsACString& aName,
const nsID* aId);
static Result<nsID, nsresult> ExtractId(mozIStorageStatement& aState,
uint32_t aPos);
static Result<NotNull<nsCOMPtr<mozIStorageStatement>>, nsresult>
CreateAndBindKeyStatement(mozIStorageConnection& aConn,
const char* aQueryFormat, const nsAString& aKey);
static Result<nsAutoCString, nsresult> HashCString(nsICryptoHash& aCrypto,
const nsACString& aIn);
Result<int32_t, nsresult> GetEffectiveSchemaVersion(
mozIStorageConnection& aConn);
nsresult Validate(mozIStorageConnection& aConn);
nsresult Migrate(mozIStorageConnection& aConn);
} // namespace
class MOZ_RAII AutoDisableForeignKeyChecking {
public:
explicit AutoDisableForeignKeyChecking(mozIStorageConnection* aConn)
: mConn(aConn), mForeignKeyCheckingDisabled(false) {
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement(
*mConn, "PRAGMA foreign_keys;"_ns),
QM_VOID);
QM_TRY_INSPECT(const int32_t& mode,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0), QM_VOID);
if (mode) {
QM_WARNONLY_TRY(MOZ_TO_RESULT(mConn->ExecuteSimpleSQL(
"PRAGMA foreign_keys = OFF;"_ns))
.andThen([this](const auto) -> Result<Ok, nsresult> {
mForeignKeyCheckingDisabled = true;
return Ok{};
}));
}
}
~AutoDisableForeignKeyChecking() {
if (mForeignKeyCheckingDisabled) {
QM_WARNONLY_TRY(QM_TO_RESULT(
mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns)));
}
}
private:
nsCOMPtr<mozIStorageConnection> mConn;
bool mForeignKeyCheckingDisabled;
};
nsresult IntegrityCheck(mozIStorageConnection& aConn) {
// CACHE_INTEGRITY_CHECK_COUNT is designed to report at most once.
static bool reported = false;
if (reported) {
return NS_OK;
}
QM_TRY_INSPECT(const auto& stmt,
quota::CreateAndExecuteSingleStepStatement(
aConn,
"SELECT COUNT(*) FROM pragma_integrity_check() "
"WHERE integrity_check != 'ok';"_ns));
QM_TRY_INSPECT(const auto& result, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsString, *stmt, GetString, 0));
nsresult rv;
const uint32_t count = result.ToInteger(&rv);
QM_TRY(OkIf(NS_SUCCEEDED(rv)), rv);
Telemetry::ScalarSet(Telemetry::ScalarID::CACHE_INTEGRITY_CHECK_COUNT, count);
reported = true;
return NS_OK;
}
nsresult CreateOrMigrateSchema(mozIStorageConnection& aConn) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_UNWRAP(int32_t schemaVersion, GetEffectiveSchemaVersion(aConn));
if (schemaVersion == kLatestSchemaVersion) {
// We already have the correct schema version. Validate it matches
// our expected schema and then proceed.
QM_TRY(MOZ_TO_RESULT(Validate(aConn)));
return NS_OK;
}
// Turn off checking foreign keys before starting a transaction, and restore
// it once we're done.
AutoDisableForeignKeyChecking restoreForeignKeyChecking(&aConn);
mozStorageTransaction trans(&aConn, false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
QM_TRY(MOZ_TO_RESULT(trans.Start()));
const bool migrating = schemaVersion != 0;
if (migrating) {
// A schema exists, but its not the current version. Attempt to
// migrate it to our new schema.
QM_TRY(MOZ_TO_RESULT(Migrate(aConn)));
} else {
// There is no schema installed. Create the database from scratch.
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableCaches))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTableSecurityInfo))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexSecurityInfoHash))));
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableEntries))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexEntriesRequest))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTableRequestHeaders))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTableResponseHeaders))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexResponseHeadersName))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTableResponseUrlList))));
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableStorage))));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(kLatestSchemaVersion)));
QM_TRY_UNWRAP(schemaVersion, GetEffectiveSchemaVersion(aConn));
}
QM_TRY(MOZ_TO_RESULT(Validate(aConn)));
QM_TRY(MOZ_TO_RESULT(trans.Commit()));
if (migrating) {
// Migrations happen infrequently and reflect a chance in DB structure.
// This is a good time to rebuild the database. It also helps catch
// if a new migration is incorrect by fast failing on the corruption.
// Unfortunately, this must be performed outside of the transaction.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("VACUUM"_ns)), QM_PROPAGATE,
([&aConn](const nsresult rv) {
if (rv == NS_ERROR_STORAGE_CONSTRAINT) {
QM_WARNONLY_TRY(QM_TO_RESULT(IntegrityCheck(aConn)));
}
}));
}
return NS_OK;
}
nsresult InitializeConnection(mozIStorageConnection& aConn) {
MOZ_ASSERT(!NS_IsMainThread());
// This function needs to perform per-connection initialization tasks that
// need to happen regardless of the schema.
// Note, the default encoding of UTF-8 is preferred. mozStorage does all
// the work necessary to convert UTF-16 nsString values for us. We don't
// need ordering and the binary equality operations are correct. So, do
// NOT set PRAGMA encoding to UTF-16.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsPrintfCString(
// Use a smaller page size to improve perf/footprint; default is too large
"PRAGMA page_size = %u; "
// Enable auto_vacuum; this must happen after page_size and before WAL
"PRAGMA auto_vacuum = INCREMENTAL; "
"PRAGMA foreign_keys = ON; ",
kPageSize))));
// Limit fragmentation by growing the database by many pages at once.
QM_TRY(QM_OR_ELSE_WARN_IF(
// Expression.
MOZ_TO_RESULT(aConn.SetGrowthIncrement(kGrowthSize, ""_ns)),
// Predicate.
IsSpecificError<NS_ERROR_FILE_TOO_BIG>,
// Fallback.
ErrToDefaultOk<>));
// Enable WAL journaling. This must be performed in a separate transaction
// after changing the page_size and enabling auto_vacuum.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsPrintfCString(
// WAL journal can grow to given number of *pages*
"PRAGMA wal_autocheckpoint = %u; "
// Always truncate the journal back to given number of *bytes*
"PRAGMA journal_size_limit = %u; "
// WAL must be enabled at the end to allow page size to be changed, etc.
"PRAGMA journal_mode = WAL; ",
kWalAutoCheckpointPages, kWalAutoCheckpointSize))));
// Verify that we successfully set the vacuum mode to incremental. It
// is very easy to put the database in a state where the auto_vacuum
// pragma above fails silently.
#ifdef DEBUG
{
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "PRAGMA auto_vacuum;"_ns));
QM_TRY_INSPECT(const int32_t& mode,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
// integer value 2 is incremental mode
QM_TRY(OkIf(mode == 2), NS_ERROR_UNEXPECTED);
}
#endif
return NS_OK;
}
Result<CacheId, nsresult> CreateCacheId(mozIStorageConnection& aConn) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("INSERT INTO caches DEFAULT VALUES;"_ns)));
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "SELECT last_insert_rowid()"_ns));
QM_TRY(OkIf(state), Err(NS_ERROR_UNEXPECTED));
QM_TRY_INSPECT(const CacheId& id,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt64, 0));
return id;
}
Result<DeletionInfo, nsresult> DeleteCacheId(mozIStorageConnection& aConn,
CacheId aCacheId) {
MOZ_ASSERT(!NS_IsMainThread());
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteAllCacheEntries(aConn, aCacheId));
QM_TRY(MOZ_TO_RESULT(DeleteSecurityInfoList(aConn, deletedSecurityIdList)));
// Delete the remainder of the cache using cascade semantics.
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"DELETE FROM caches WHERE id=:id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("id"_ns, aCacheId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize};
}
Result<AutoTArray<CacheId, 8>, nsresult> FindOrphanedCacheIds(
mozIStorageConnection& aConn) {
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT id FROM caches "
"WHERE id NOT IN (SELECT cache_id from storage);"_ns));
QM_TRY_RETURN(
(quota::CollectElementsWhileHasResultTyped<AutoTArray<CacheId, 8>>(
*state, [](auto& stmt) {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0));
})));
}
Result<int64_t, nsresult> FindOverallPaddingSize(mozIStorageConnection& aConn) {
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT response_padding_size FROM entries "
"WHERE response_padding_size IS NOT NULL;"_ns));
int64_t overallPaddingSize = 0;
QM_TRY(quota::CollectWhileHasResult(
*state, [&overallPaddingSize](auto& stmt) -> Result<Ok, nsresult> {
QM_TRY_INSPECT(const int64_t& padding_size,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0));
MOZ_DIAGNOSTIC_ASSERT(padding_size >= 0);
MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - padding_size >= overallPaddingSize);
overallPaddingSize += padding_size;
return Ok{};
}));
return overallPaddingSize;
}
Result<nsTArray<nsID>, nsresult> GetKnownBodyIds(mozIStorageConnection& aConn) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT request_body_id, response_body_id FROM entries;"_ns));
AutoTArray<nsID, 64> idList;
QM_TRY(quota::CollectWhileHasResult(
*state, [&idList](auto& stmt) -> Result<Ok, nsresult> {
// extract 0 to 2 nsID structs per row
for (uint32_t i = 0; i < 2; ++i) {
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, i));
if (!isNull) {
QM_TRY_INSPECT(const auto& id, ExtractId(stmt, i));
idList.AppendElement(id);
}
}
return Ok{};
}));
return std::move(idList);
}
Result<Maybe<SavedResponse>, nsresult> CacheMatch(
mozIStorageConnection& aConn, CacheId aCacheId,
const CacheRequest& aRequest, const CacheQueryParams& aParams) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(const auto& matches,
QueryCache(aConn, aCacheId, aRequest, aParams, 1));
if (matches.IsEmpty()) {
return Maybe<SavedResponse>();
}
QM_TRY_UNWRAP(auto response, ReadResponse(aConn, matches[0]));
response.mCacheId = aCacheId;
return Some(std::move(response));
}
Result<nsTArray<SavedResponse>, nsresult> CacheMatchAll(
mozIStorageConnection& aConn, CacheId aCacheId,
const Maybe<CacheRequest>& aMaybeRequest, const CacheQueryParams& aParams) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& matches, ([&aConn, aCacheId, &aMaybeRequest, &aParams] {
if (aMaybeRequest.isNothing()) {
QM_TRY_RETURN(QueryAll(aConn, aCacheId));
}
QM_TRY_RETURN(
QueryCache(aConn, aCacheId, aMaybeRequest.ref(), aParams));
}()));
// TODO: replace this with a bulk load using SQL IN clause (bug 1110458)
QM_TRY_RETURN(TransformIntoNewArrayAbortOnErr(
matches,
[&aConn, aCacheId](const auto match) -> Result<SavedResponse, nsresult> {
QM_TRY_UNWRAP(auto savedResponse, ReadResponse(aConn, match));
savedResponse.mCacheId = aCacheId;
return savedResponse;
},
fallible));
}
Result<DeletionInfo, nsresult> CachePut(mozIStorageConnection& aConn,
CacheId aCacheId,
const CacheRequest& aRequest,
const nsID* aRequestBodyId,
const CacheResponse& aResponse,
const nsID* aResponseBodyId) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& matches,
QueryCache(aConn, aCacheId, aRequest,
CacheQueryParams(false, false, false, false, u""_ns)));
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteEntries(aConn, matches));
QM_TRY(MOZ_TO_RESULT(InsertEntry(aConn, aCacheId, aRequest, aRequestBodyId,
aResponse, aResponseBodyId)));
// Delete the security values after doing the insert to avoid churning
// the security table when its not necessary.
QM_TRY(MOZ_TO_RESULT(DeleteSecurityInfoList(aConn, deletedSecurityIdList)));
return DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize};
}
Result<Maybe<DeletionInfo>, nsresult> CacheDelete(
mozIStorageConnection& aConn, CacheId aCacheId,
const CacheRequest& aRequest, const CacheQueryParams& aParams) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(const auto& matches,
QueryCache(aConn, aCacheId, aRequest, aParams));
if (matches.IsEmpty()) {
return Maybe<DeletionInfo>();
}
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteEntries(aConn, matches));
QM_TRY(MOZ_TO_RESULT(DeleteSecurityInfoList(aConn, deletedSecurityIdList)));
return Some(DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize});
}
Result<nsTArray<SavedRequest>, nsresult> CacheKeys(
mozIStorageConnection& aConn, CacheId aCacheId,
const Maybe<CacheRequest>& aMaybeRequest, const CacheQueryParams& aParams) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& matches, ([&aConn, aCacheId, &aMaybeRequest, &aParams] {
if (aMaybeRequest.isNothing()) {
QM_TRY_RETURN(QueryAll(aConn, aCacheId));
}
QM_TRY_RETURN(
QueryCache(aConn, aCacheId, aMaybeRequest.ref(), aParams));
}()));
// TODO: replace this with a bulk load using SQL IN clause (bug 1110458)
QM_TRY_RETURN(TransformIntoNewArrayAbortOnErr(
matches,
[&aConn, aCacheId](const auto match) -> Result<SavedRequest, nsresult> {
QM_TRY_UNWRAP(auto savedRequest, ReadRequest(aConn, match));
savedRequest.mCacheId = aCacheId;
return savedRequest;
},
fallible));
}
Result<Maybe<SavedResponse>, nsresult> StorageMatch(
mozIStorageConnection& aConn, Namespace aNamespace,
const CacheRequest& aRequest, const CacheQueryParams& aParams) {
MOZ_ASSERT(!NS_IsMainThread());
// If we are given a cache to check, then simply find its cache ID
// and perform the match.
if (aParams.cacheNameSet()) {
QM_TRY_INSPECT(const auto& maybeCacheId,
StorageGetCacheId(aConn, aNamespace, aParams.cacheName()));
if (maybeCacheId.isNothing()) {
return Maybe<SavedResponse>();
}
return CacheMatch(aConn, maybeCacheId.ref(), aRequest, aParams);
}
// Otherwise we need to get a list of all the cache IDs in this namespace.
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT cache_id FROM storage WHERE "
"namespace=:namespace ORDER BY rowid;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("namespace"_ns, aNamespace)));
QM_TRY_INSPECT(
const auto& cacheIdList,
(quota::CollectElementsWhileHasResultTyped<AutoTArray<CacheId, 32>>(
*state, [](auto& stmt) {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 0));
})));
// Now try to find a match in each cache in order
for (const auto cacheId : cacheIdList) {
QM_TRY_UNWRAP(auto matchedResponse,
CacheMatch(aConn, cacheId, aRequest, aParams));
if (matchedResponse.isSome()) {
return matchedResponse;
}
}
return Maybe<SavedResponse>();
}
Result<Maybe<CacheId>, nsresult> StorageGetCacheId(mozIStorageConnection& aConn,
Namespace aNamespace,
const nsAString& aKey) {
MOZ_ASSERT(!NS_IsMainThread());
// How we constrain the key column depends on the value of our key. Use
// a format string for the query and let CreateAndBindKeyStatement() fill
// it in for us.
const char* const query =
"SELECT cache_id FROM storage "
"WHERE namespace=:namespace AND %s "
"ORDER BY rowid;";
QM_TRY_INSPECT(const auto& state,
CreateAndBindKeyStatement(aConn, query, aKey));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("namespace"_ns, aNamespace)));
QM_TRY_INSPECT(const bool& hasMoreData,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, ExecuteStep));
if (!hasMoreData) {
return Maybe<CacheId>();
}
QM_TRY_RETURN(
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt64, 0).map(Some<CacheId>));
}
nsresult StoragePutCache(mozIStorageConnection& aConn, Namespace aNamespace,
const nsAString& aKey, CacheId aCacheId) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO storage (namespace, key, cache_id) "
"VALUES (:namespace, :key, :cache_id);"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("namespace"_ns, aNamespace)));
QM_TRY(MOZ_TO_RESULT(state->BindStringAsBlobByName("key"_ns, aKey)));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return NS_OK;
}
nsresult StorageForgetCache(mozIStorageConnection& aConn, Namespace aNamespace,
const nsAString& aKey) {
MOZ_ASSERT(!NS_IsMainThread());
// How we constrain the key column depends on the value of our key. Use
// a format string for the query and let CreateAndBindKeyStatement() fill
// it in for us.
const char* const query =
"DELETE FROM storage WHERE namespace=:namespace AND %s;";
QM_TRY_INSPECT(const auto& state,
CreateAndBindKeyStatement(aConn, query, aKey));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("namespace"_ns, aNamespace)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return NS_OK;
}
Result<nsTArray<nsString>, nsresult> StorageGetKeys(
mozIStorageConnection& aConn, Namespace aNamespace) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT key FROM storage WHERE namespace=:namespace ORDER BY rowid;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("namespace"_ns, aNamespace)));
QM_TRY_RETURN(quota::CollectElementsWhileHasResult(*state, [](auto& stmt) {
QM_TRY_RETURN(
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, stmt, GetBlobAsString, 0));
}));
}
namespace {
Result<EntryIds, nsresult> QueryAll(mozIStorageConnection& aConn,
CacheId aCacheId) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT id FROM entries WHERE cache_id=:cache_id ORDER BY id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY_RETURN((quota::CollectElementsWhileHasResultTyped<EntryIds>(
*state, [](auto& stmt) {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
})));
}
Result<EntryIds, nsresult> QueryCache(mozIStorageConnection& aConn,
CacheId aCacheId,
const CacheRequest& aRequest,
const CacheQueryParams& aParams,
uint32_t aMaxResults) {
MOZ_ASSERT(!NS_IsMainThread());
MOZ_DIAGNOSTIC_ASSERT(aMaxResults > 0);
if (!aParams.ignoreMethod() &&
!aRequest.method().LowerCaseEqualsLiteral("get")) {
return Result<EntryIds, nsresult>{std::in_place};
}
nsAutoCString query(
"SELECT id, COUNT(response_headers.name) AS vary_count "
"FROM entries "
"LEFT OUTER JOIN response_headers ON "
"entries.id=response_headers.entry_id "
"AND response_headers.name='vary' COLLATE NOCASE "
"WHERE entries.cache_id=:cache_id "
"AND entries.request_url_no_query_hash=:url_no_query_hash ");
if (!aParams.ignoreSearch()) {
query.AppendLiteral("AND entries.request_url_query_hash=:url_query_hash ");
}
query.AppendLiteral("AND entries.request_url_no_query=:url_no_query ");
if (!aParams.ignoreSearch()) {
query.AppendLiteral("AND entries.request_url_query=:url_query ");
}
query.AppendLiteral("GROUP BY entries.id ORDER BY entries.id;");
QM_TRY_INSPECT(const auto& state, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn,
CreateStatement, query));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY_INSPECT(const auto& crypto,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsICryptoHash>,
MOZ_SELECT_OVERLOAD(do_CreateInstance),
NS_CRYPTO_HASH_CONTRACTID));
QM_TRY_INSPECT(const auto& urlWithoutQueryHash,
HashCString(*crypto, aRequest.urlWithoutQuery()));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringAsBlobByName("url_no_query_hash"_ns,
urlWithoutQueryHash)));
if (!aParams.ignoreSearch()) {
QM_TRY_INSPECT(const auto& urlQueryHash,
HashCString(*crypto, aRequest.urlQuery()));
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringAsBlobByName("url_query_hash"_ns, urlQueryHash)));
}
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName(
"url_no_query"_ns, aRequest.urlWithoutQuery())));
if (!aParams.ignoreSearch()) {
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("url_query"_ns, aRequest.urlQuery())));
}
EntryIds entryIdList;
QM_TRY(CollectWhile(
[&state, &entryIdList, aMaxResults]() -> Result<bool, nsresult> {
if (entryIdList.Length() == aMaxResults) {
return false;
}
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(state, ExecuteStep));
},
[&state, &entryIdList, ignoreVary = aParams.ignoreVary(), &aConn,
&aRequest]() -> Result<Ok, nsresult> {
QM_TRY_INSPECT(const EntryId& entryId,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 0));
QM_TRY_INSPECT(const int32_t& varyCount,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 1));
if (!ignoreVary && varyCount > 0) {
QM_TRY_INSPECT(const bool& matchedByVary,
MatchByVaryHeader(aConn, aRequest, entryId));
if (!matchedByVary) {
return Ok{};
}
}
entryIdList.AppendElement(entryId);
return Ok{};
}));
return entryIdList;
}
Result<bool, nsresult> MatchByVaryHeader(mozIStorageConnection& aConn,
const CacheRequest& aRequest,
EntryId entryId) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(
const auto& varyValues,
([&aConn, entryId]() -> Result<AutoTArray<nsCString, 8>, nsresult> {
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT value FROM response_headers "
"WHERE name='vary' COLLATE NOCASE "
"AND entry_id=:entry_id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, entryId)));
QM_TRY_RETURN((
quota::CollectElementsWhileHasResultTyped<AutoTArray<nsCString, 8>>(
*state, [](auto& stmt) {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, stmt, GetUTF8String, 0));
})));
}()));
// Should not have called this function if this was not the case
MOZ_DIAGNOSTIC_ASSERT(!varyValues.IsEmpty());
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT name, value FROM request_headers "
"WHERE entry_id=:entry_id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, entryId)));
RefPtr<InternalHeaders> cachedHeaders =
new InternalHeaders(HeadersGuardEnum::None);
QM_TRY(quota::CollectWhileHasResult(
*state, [&cachedHeaders](auto& stmt) -> Result<Ok, nsresult> {
QM_TRY_INSPECT(const auto& name,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 0));
QM_TRY_INSPECT(const auto& value,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
GetUTF8String, 1));
ErrorResult errorResult;
cachedHeaders->Append(name, value, errorResult);
if (errorResult.Failed()) {
return Err(errorResult.StealNSResult());
}
return Ok{};
}));
RefPtr<InternalHeaders> queryHeaders =
TypeUtils::ToInternalHeaders(aRequest.headers());
// Assume the vary headers match until we find a conflict
bool varyHeadersMatch = true;
for (const auto& varyValue : varyValues) {
// Extract the header names inside the Vary header value.
bool bailOut = false;
for (const nsACString& header :
nsCCharSeparatedTokenizer(varyValue, NS_HTTP_HEADER_SEP).ToRange()) {
MOZ_DIAGNOSTIC_ASSERT(!header.EqualsLiteral("*"),
"We should have already caught this in "
"TypeUtils::ToPCacheResponseWithoutBody()");
ErrorResult errorResult;
nsAutoCString queryValue;
queryHeaders->Get(header, queryValue, errorResult);
if (errorResult.Failed()) {
errorResult.SuppressException();
MOZ_DIAGNOSTIC_ASSERT(queryValue.IsEmpty());
}
nsAutoCString cachedValue;
cachedHeaders->Get(header, cachedValue, errorResult);
if (errorResult.Failed()) {
errorResult.SuppressException();
MOZ_DIAGNOSTIC_ASSERT(cachedValue.IsEmpty());
}
if (queryValue != cachedValue) {
varyHeadersMatch = false;
bailOut = true;
break;
}
}
if (bailOut) {
break;
}
}
return varyHeadersMatch;
}
static nsresult SelectAndDeleteEntriesInternal(
mozIStorageConnection& aConn, const Span<const EntryId>& aEntryIdList,
nsTArray<nsID>& aDeletedBodyIdListOut,
nsTArray<IdCount>& aDeletedSecurityIdListOut,
int64_t& aDeletedPaddingSizeOut) {
nsAutoCString query(
"SELECT "
"request_body_id, "
"response_body_id, "
"response_security_info_id, "
"response_padding_size "
"FROM entries WHERE id IN (");
AppendListParamsToQuery(query, aEntryIdList.Length());
query.AppendLiteral(")");
QM_TRY_INSPECT(const auto& state, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn,
CreateStatement, query));
QM_TRY(MOZ_TO_RESULT(BindListParamsToQuery(*state, aEntryIdList)));
int64_t overallPaddingSize = 0;
QM_TRY(quota::CollectWhileHasResult(
*state,
[&overallPaddingSize, &aDeletedBodyIdListOut,
&aDeletedSecurityIdListOut](auto& stmt) -> Result<Ok, nsresult> {
// extract 0 to 2 nsID structs per row
for (uint32_t i = 0; i < 2; ++i) {
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, i));
if (!isNull) {
QM_TRY_INSPECT(const auto& id, ExtractId(stmt, i));
aDeletedBodyIdListOut.AppendElement(id);
}
}
{ // and then a possible third entry for the security id
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, 2));
if (!isNull) {
QM_TRY_INSPECT(const int32_t& securityId,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 2));
// XXXtt: Consider using map for aDeletedSecuityIdListOut.
auto foundIt =
std::find_if(aDeletedSecurityIdListOut.begin(),
aDeletedSecurityIdListOut.end(),
[securityId](const auto& deletedSecurityId) {
return deletedSecurityId.mId == securityId;
});
if (foundIt == aDeletedSecurityIdListOut.end()) {
// Add a new entry for this ID with a count of 1, if it's not in
// the list
aDeletedSecurityIdListOut.AppendElement(IdCount(securityId));
} else {
// Otherwise, increment the count for this ID
foundIt->mCount += 1;
}
}
}
{
// It's possible to have null padding size for non-opaque response
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, 3));
if (!isNull) {
QM_TRY_INSPECT(const int64_t& paddingSize,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 3));
MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - overallPaddingSize >=
paddingSize);
overallPaddingSize += paddingSize;
}
}
return Ok{};
}));
aDeletedPaddingSizeOut += overallPaddingSize;
// Dependent records removed via ON DELETE CASCADE
query = "DELETE FROM entries WHERE id IN ("_ns;
AppendListParamsToQuery(query, aEntryIdList.Length());
query.AppendLiteral(")");
{
QM_TRY_INSPECT(const auto& state, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn,
CreateStatement, query));
QM_TRY(MOZ_TO_RESULT(BindListParamsToQuery(*state, aEntryIdList)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
return NS_OK;
}
static nsresult DeleteEntriesInternal(
mozIStorageConnection& aConn, const nsTArray<EntryId>& aEntryIdList,
nsTArray<nsID>& aDeletedBodyIdListOut,
nsTArray<IdCount>& aDeletedSecurityIdListOut,
int64_t& aDeletedPaddingSizeOut, uint32_t aPos, uint32_t aLen) {
MOZ_ASSERT(!NS_IsMainThread());
if (aEntryIdList.IsEmpty()) {
return NS_OK;
}
MOZ_DIAGNOSTIC_ASSERT(aPos < aEntryIdList.Length());
auto remaining = aLen;
uint32_t currPos = 0;
do {
// Sqlite limits the number of entries allowed for an IN clause,
// so split up larger operations.
auto currLen = std::min(kMaxEntriesPerStatement, remaining);
SelectAndDeleteEntriesInternal(
aConn, Span<const EntryId>(aEntryIdList.Elements() + currPos, currLen),
aDeletedBodyIdListOut, aDeletedSecurityIdListOut,
aDeletedPaddingSizeOut);
remaining -= currLen;
currPos += currLen;
} while (remaining > 0);
return NS_OK;
}
Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>, nsresult>
DeleteEntries(mozIStorageConnection& aConn,
const nsTArray<EntryId>& aEntryIdList) {
auto result =
std::make_tuple(nsTArray<nsID>{}, AutoTArray<IdCount, 16>{}, int64_t{0});
QM_TRY(MOZ_TO_RESULT(DeleteEntriesInternal(
aConn, aEntryIdList, std::get<0>(result), std::get<1>(result),
std::get<2>(result), 0, aEntryIdList.Length())));
return result;
}
Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>, nsresult>
DeleteAllCacheEntries(mozIStorageConnection& aConn, CacheId& aCacheId) {
auto result =
std::make_tuple(nsTArray<nsID>{}, AutoTArray<IdCount, 16>{}, int64_t{0});
auto& deletedBodyIdList = std::get<0>(result);
auto& deletedSecurityIdList = std::get<1>(result);
auto& deletedPaddingSize = std::get<2>(result);
nsAutoCString query(
"SELECT "
"request_body_id, "
"response_body_id, "
"response_security_info_id, "
"response_padding_size "
"FROM entries WHERE cache_id=:cache_id ORDER BY id;"_ns);
QM_TRY_INSPECT(const auto& state, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn,
CreateStatement, query));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY(quota::CollectWhileHasResult(
*state,
[&deletedPaddingSize, &deletedBodyIdList,
&deletedSecurityIdList](auto& stmt) -> Result<Ok, nsresult> {
// extract 0 to 2 nsID structs per row
for (uint32_t i = 0; i < 2; ++i) {
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, i));
if (!isNull) {
QM_TRY_INSPECT(const auto& id, ExtractId(stmt, i));
deletedBodyIdList.AppendElement(id);
}
}
{ // and then a possible third entry for the security id
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, 2));
if (!isNull) {
QM_TRY_INSPECT(const int32_t& securityId,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 2));
// XXXtt: Consider using map for aDeletedSecuityIdListOut.
auto foundIt = std::find_if(
deletedSecurityIdList.begin(), deletedSecurityIdList.end(),
[securityId](const auto& deletedSecurityId) {
return deletedSecurityId.mId == securityId;
});
if (foundIt == deletedSecurityIdList.end()) {
// Add a new entry for this ID with a count of 1, if it's not in
// the list
deletedSecurityIdList.AppendElement(IdCount(securityId));
} else {
// Otherwise, increment the count for this ID
foundIt->mCount += 1;
}
}
}
{
// It's possible to have null padding size for non-opaque response
QM_TRY_INSPECT(const bool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, 3));
if (!isNull) {
QM_TRY_INSPECT(const int64_t& paddingSize,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 3));
MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
MOZ_DIAGNOSTIC_ASSERT(paddingSize + deletedPaddingSize <= INT_MAX);
deletedPaddingSize += paddingSize;
}
}
return Ok{};
}));
// Dependent records removed via ON DELETE CASCADE
query = "DELETE FROM entries WHERE cache_id=:cache_id"_ns;
{
QM_TRY_INSPECT(const auto& state, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn,
CreateStatement, query));
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
return result;
}
Result<int32_t, nsresult> InsertSecurityInfo(
mozIStorageConnection& aConn, nsICryptoHash& aCrypto,
nsITransportSecurityInfo* aSecurityInfo) {
MOZ_DIAGNOSTIC_ASSERT(aSecurityInfo);
if (!aSecurityInfo) {
return Err(NS_ERROR_FAILURE);
}
nsCString data;
nsresult rv = aSecurityInfo->ToString(data);
if (NS_FAILED(rv)) {
return Err(rv);
}
// We want to use an index to find existing security blobs, but indexing
// the full blob would be quite expensive. Instead, we index a small
// hash value. Calculate this hash as the first 8 bytes of the SHA1 of
// the full data.
QM_TRY_INSPECT(const auto& hash, HashCString(aCrypto, data));
// Next, search for an existing entry for this blob by comparing the hash
// value first and then the full data. SQLite is smart enough to use
// the index on the hash to search the table before doing the expensive
// comparison of the large data column. (This was verified with EXPLAIN.)
QM_TRY_INSPECT(
const auto& selectStmt,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn,
// Note that hash and data are blobs, but we can use = here since the
// columns are NOT NULL.
"SELECT id, refcount FROM security_info WHERE hash=:hash AND "
"data=:data;"_ns,
[&hash, &data](auto& state) -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(
state.BindUTF8StringAsBlobByName("hash"_ns, hash)));
QM_TRY(MOZ_TO_RESULT(
state.BindUTF8StringAsBlobByName("data"_ns, data)));
return Ok{};
}));
// This security info blob is already in the database
if (selectStmt) {
// get the existing security blob id to return
QM_TRY_INSPECT(const int32_t& id,
MOZ_TO_RESULT_INVOKE_MEMBER(selectStmt, GetInt32, 0));
QM_TRY_INSPECT(const int32_t& refcount,
MOZ_TO_RESULT_INVOKE_MEMBER(selectStmt, GetInt32, 1));
// But first, update the refcount in the database.
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("refcount"_ns, refcount + 1)));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("id"_ns, id)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return id;
}
// This is a new security info blob. Create a new row in the security table
// with an initial refcount of 1.
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO security_info (hash, data, refcount) "
"VALUES (:hash, :data, 1);"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringAsBlobByName("hash"_ns, hash)));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringAsBlobByName("data"_ns, data)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
{
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "SELECT last_insert_rowid()"_ns));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
}
}
nsresult DeleteSecurityInfo(mozIStorageConnection& aConn, int32_t aId,
int32_t aCount) {
// First, we need to determine the current refcount for this security blob.
QM_TRY_INSPECT(
const int32_t& refcount, ([&aConn, aId]() -> Result<int32_t, nsresult> {
QM_TRY_INSPECT(
const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "SELECT refcount FROM security_info WHERE id=:id;"_ns,
[aId](auto& state) -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(state.BindInt32ByName("id"_ns, aId)));
return Ok{};
}));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
}()));
MOZ_DIAGNOSTIC_ASSERT(refcount >= aCount);
// Next, calculate the new refcount
int32_t newCount = refcount - aCount;
// If the last reference to this security blob was removed we can
// just remove the entire row.
if (newCount == 0) {
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"DELETE FROM security_info WHERE id=:id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("id"_ns, aId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return NS_OK;
}
// Otherwise update the refcount in the table to reflect the reduced
// number of references to the security blob.
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("refcount"_ns, newCount)));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("id"_ns, aId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
return NS_OK;
}
nsresult DeleteSecurityInfoList(
mozIStorageConnection& aConn,
const nsTArray<IdCount>& aDeletedStorageIdList) {
for (const auto& deletedStorageId : aDeletedStorageIdList) {
QM_TRY(MOZ_TO_RESULT(DeleteSecurityInfo(aConn, deletedStorageId.mId,
deletedStorageId.mCount)));
}
return NS_OK;
}
nsresult InsertEntry(mozIStorageConnection& aConn, CacheId aCacheId,
const CacheRequest& aRequest, const nsID* aRequestBodyId,
const CacheResponse& aResponse,
const nsID* aResponseBodyId) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(const auto& crypto,
MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsICryptoHash>,
MOZ_SELECT_OVERLOAD(do_CreateInstance),
NS_CRYPTO_HASH_CONTRACTID));
int32_t securityId = -1;
if (aResponse.securityInfo()) {
QM_TRY_UNWRAP(securityId,
InsertSecurityInfo(aConn, *crypto, aResponse.securityInfo()));
}
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO entries ("
"request_method, "
"request_url_no_query, "
"request_url_no_query_hash, "
"request_url_query, "
"request_url_query_hash, "
"request_url_fragment, "
"request_referrer, "
"request_referrer_policy, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_integrity, "
"request_body_id, "
"response_type, "
"response_status, "
"response_status_text, "
"response_headers_guard, "
"response_body_id, "
"response_security_info_id, "
"response_principal_info, "
"response_padding_size, "
"cache_id "
") VALUES ("
":request_method, "
":request_url_no_query, "
":request_url_no_query_hash, "
":request_url_query, "
":request_url_query_hash, "
":request_url_fragment, "
":request_referrer, "
":request_referrer_policy, "
":request_headers_guard, "
":request_mode, "
":request_credentials, "
":request_contentpolicytype, "
":request_cache, "
":request_redirect, "
":request_integrity, "
":request_body_id, "
":response_type, "
":response_status, "
":response_status_text, "
":response_headers_guard, "
":response_body_id, "
":response_security_info_id, "
":response_principal_info, "
":response_padding_size, "
":cache_id "
");"_ns));
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("request_method"_ns, aRequest.method())));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName(
"request_url_no_query"_ns, aRequest.urlWithoutQuery())));
QM_TRY_INSPECT(const auto& urlWithoutQueryHash,
HashCString(*crypto, aRequest.urlWithoutQuery()));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringAsBlobByName(
"request_url_no_query_hash"_ns, urlWithoutQueryHash)));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("request_url_query"_ns,
aRequest.urlQuery())));
QM_TRY_INSPECT(const auto& urlQueryHash,
HashCString(*crypto, aRequest.urlQuery()));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringAsBlobByName(
"request_url_query_hash"_ns, urlQueryHash)));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("request_url_fragment"_ns,
aRequest.urlFragment())));
QM_TRY(MOZ_TO_RESULT(
state->BindStringByName("request_referrer"_ns, aRequest.referrer())));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"request_referrer_policy"_ns,
static_cast<int32_t>(aRequest.referrerPolicy()))));
QM_TRY(MOZ_TO_RESULT(
state->BindInt32ByName("request_headers_guard"_ns,
static_cast<int32_t>(aRequest.headersGuard()))));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"request_mode"_ns, static_cast<int32_t>(aRequest.mode()))));
QM_TRY(MOZ_TO_RESULT(
state->BindInt32ByName("request_credentials"_ns,
static_cast<int32_t>(aRequest.credentials()))));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"request_contentpolicytype"_ns,
static_cast<int32_t>(aRequest.contentPolicyType()))));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"request_cache"_ns, static_cast<int32_t>(aRequest.requestCache()))));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"request_redirect"_ns,
static_cast<int32_t>(aRequest.requestRedirect()))));
QM_TRY(MOZ_TO_RESULT(
state->BindStringByName("request_integrity"_ns, aRequest.integrity())));
QM_TRY(MOZ_TO_RESULT(BindId(*state, "request_body_id"_ns, aRequestBodyId)));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"response_type"_ns, static_cast<int32_t>(aResponse.type()))));
QM_TRY(MOZ_TO_RESULT(
state->BindInt32ByName("response_status"_ns, aResponse.status())));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("response_status_text"_ns,
aResponse.statusText())));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName(
"response_headers_guard"_ns,
static_cast<int32_t>(aResponse.headersGuard()))));
QM_TRY(
MOZ_TO_RESULT(BindId(*state, "response_body_id"_ns, aResponseBodyId)));
if (!aResponse.securityInfo()) {
QM_TRY(
MOZ_TO_RESULT(state->BindNullByName("response_security_info_id"_ns)));
} else {
QM_TRY(MOZ_TO_RESULT(
state->BindInt32ByName("response_security_info_id"_ns, securityId)));
}
nsAutoCString serializedInfo;
// We only allow content serviceworkers right now.
if (aResponse.principalInfo().isSome()) {
const mozilla::ipc::PrincipalInfo& principalInfo =
aResponse.principalInfo().ref();
MOZ_DIAGNOSTIC_ASSERT(principalInfo.type() ==
mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
const mozilla::ipc::ContentPrincipalInfo& cInfo =
principalInfo.get_ContentPrincipalInfo();
serializedInfo.Append(cInfo.spec());
nsAutoCString suffix;
cInfo.attrs().CreateSuffix(suffix);
serializedInfo.Append(suffix);
}
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName(
"response_principal_info"_ns, serializedInfo)));
if (aResponse.paddingSize() == InternalResponse::UNKNOWN_PADDING_SIZE) {
MOZ_DIAGNOSTIC_ASSERT(aResponse.type() != ResponseType::Opaque);
QM_TRY(MOZ_TO_RESULT(state->BindNullByName("response_padding_size"_ns)));
} else {
MOZ_DIAGNOSTIC_ASSERT(aResponse.paddingSize() >= 0);
MOZ_DIAGNOSTIC_ASSERT(aResponse.type() == ResponseType::Opaque);
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("response_padding_size"_ns,
aResponse.paddingSize())));
}
QM_TRY(MOZ_TO_RESULT(state->BindInt64ByName("cache_id"_ns, aCacheId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
QM_TRY_INSPECT(
const int32_t& entryId, ([&aConn]() -> Result<int32_t, nsresult> {
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "SELECT last_insert_rowid()"_ns));
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
}()));
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO request_headers ("
"name, "
"value, "
"entry_id "
") VALUES (:name, :value, :entry_id)"_ns));
for (const auto& requestHeader : aRequest.headers()) {
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("name"_ns, requestHeader.name())));
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("value"_ns, requestHeader.value())));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, entryId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
}
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO response_headers ("
"name, "
"value, "
"entry_id "
") VALUES (:name, :value, :entry_id)"_ns));
for (const auto& responseHeader : aResponse.headers()) {
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("name"_ns, responseHeader.name())));
QM_TRY(MOZ_TO_RESULT(
state->BindUTF8StringByName("value"_ns, responseHeader.value())));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, entryId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
}
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"INSERT INTO response_url_list ("
"url, "
"entry_id "
") VALUES (:url, :entry_id)"_ns));
for (const auto& responseUrl : aResponse.urlList()) {
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName("url"_ns, responseUrl)));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, entryId)));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
}
}
return NS_OK;
}
/**
* Gets a HeadersEntry from a storage statement by retrieving the first column
* as the name and the second column as the value.
*/
Result<HeadersEntry, nsresult> GetHeadersEntryFromStatement(
mozIStorageStatement& aStmt) {
HeadersEntry header;
QM_TRY_UNWRAP(header.name(), MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, aStmt, GetUTF8String, 0));
QM_TRY_UNWRAP(header.value(), MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, aStmt, GetUTF8String, 1));
return header;
}
Result<SavedResponse, nsresult> ReadResponse(mozIStorageConnection& aConn,
EntryId aEntryId) {
MOZ_ASSERT(!NS_IsMainThread());
SavedResponse savedResponse;
QM_TRY_INSPECT(
const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn,
"SELECT "
"entries.response_type, "
"entries.response_status, "
"entries.response_status_text, "
"entries.response_headers_guard, "
"entries.response_body_id, "
"entries.response_principal_info, "
"entries.response_padding_size, "
"security_info.data, "
"entries.request_credentials "
"FROM entries "
"LEFT OUTER JOIN security_info "
"ON entries.response_security_info_id=security_info.id "
"WHERE entries.id=:id;"_ns,
[aEntryId](auto& state) -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(state.BindInt32ByName("id"_ns, aEntryId)));
return Ok{};
}));
QM_TRY_INSPECT(const int32_t& type,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
savedResponse.mValue.type() = static_cast<ResponseType>(type);
QM_TRY_INSPECT(const int32_t& status,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 1));
savedResponse.mValue.status() = static_cast<uint32_t>(status);
QM_TRY(MOZ_TO_RESULT(
state->GetUTF8String(2, savedResponse.mValue.statusText())));
QM_TRY_INSPECT(const int32_t& guard,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 3));
savedResponse.mValue.headersGuard() = static_cast<HeadersGuardEnum>(guard);
QM_TRY_INSPECT(const bool& nullBody,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetIsNull, 4));
savedResponse.mHasBodyId = !nullBody;
if (savedResponse.mHasBodyId) {
QM_TRY_UNWRAP(savedResponse.mBodyId, ExtractId(*state, 4));
}
QM_TRY_INSPECT(const auto& serializedInfo,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *state,
GetUTF8String, 5));
savedResponse.mValue.principalInfo() = Nothing();
if (!serializedInfo.IsEmpty()) {
nsAutoCString specNoSuffix;
OriginAttributes attrs;
if (!attrs.PopulateFromOrigin(serializedInfo, specNoSuffix)) {
NS_WARNING("Something went wrong parsing a serialized principal!");
return Err(NS_ERROR_FAILURE);
}
RefPtr<net::MozURL> url;
QM_TRY(MOZ_TO_RESULT(net::MozURL::Init(getter_AddRefs(url), specNoSuffix)));
#ifdef DEBUG
nsDependentCSubstring scheme = url->Scheme();
MOZ_ASSERT(
scheme == "http" || scheme == "https" || scheme == "file" ||
// A cached response entry may have a moz-extension principal if:
//
// - This is an extension background service worker. The response for
// the main script is expected tobe a moz-extension content principal
// (the pref "extensions.backgroundServiceWorker.enabled" must be
// enabled, if the pref is toggled to false at runtime then any
// service worker registered for a moz-extension principal will be
// unregistered on the next startup).
//
// - An extension is redirecting a script being imported info a worker
// created from a regular webpage to a web-accessible extension
// script. The reponse for these redirects will have a moz-extension
// principal. Although extensions can attempt to redirect the main
// script of service workers, this will always cause the install
// process to fail.
scheme == "moz-extension");
#endif
nsCString origin;
url->Origin(origin);
nsCString baseDomain;
QM_TRY(MOZ_TO_RESULT(url->BaseDomain(baseDomain)));
savedResponse.mValue.principalInfo() =
Some(mozilla::ipc::ContentPrincipalInfo(attrs, origin, specNoSuffix,
Nothing(), baseDomain));
}
QM_TRY_INSPECT(const bool& nullPadding,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetIsNull, 6));
if (nullPadding) {
MOZ_DIAGNOSTIC_ASSERT(savedResponse.mValue.type() != ResponseType::Opaque);
savedResponse.mValue.paddingSize() = InternalResponse::UNKNOWN_PADDING_SIZE;
} else {
MOZ_DIAGNOSTIC_ASSERT(savedResponse.mValue.type() == ResponseType::Opaque);
QM_TRY_INSPECT(const int64_t& paddingSize,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt64, 6));
MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
savedResponse.mValue.paddingSize() = paddingSize;
}
nsCString data;
QM_TRY(MOZ_TO_RESULT(state->GetBlobAsUTF8String(7, data)));
if (!data.IsEmpty()) {
nsCOMPtr<nsITransportSecurityInfo> securityInfo;
nsresult rv = mozilla::psm::TransportSecurityInfo::Read(
data, getter_AddRefs(securityInfo));
if (NS_FAILED(rv)) {
return Err(rv);
}
if (!securityInfo) {
return Err(NS_ERROR_FAILURE);
}
savedResponse.mValue.securityInfo() = securityInfo.forget();
}
QM_TRY_INSPECT(const int32_t& credentials,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 8));
savedResponse.mValue.credentials() =
static_cast<RequestCredentials>(credentials);
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT "
"name, "
"value "
"FROM response_headers "
"WHERE entry_id=:entry_id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, aEntryId)));
QM_TRY_UNWRAP(savedResponse.mValue.headers(),
quota::CollectElementsWhileHasResult(
*state, GetHeadersEntryFromStatement));
}
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT "
"url "
"FROM response_url_list "
"WHERE entry_id=:entry_id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, aEntryId)));
QM_TRY_UNWRAP(savedResponse.mValue.urlList(),
quota::CollectElementsWhileHasResult(
*state, [](auto& stmt) -> Result<nsCString, nsresult> {
QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCString, stmt, GetUTF8String, 0));
}));
}
return savedResponse;
}
Result<SavedRequest, nsresult> ReadRequest(mozIStorageConnection& aConn,
EntryId aEntryId) {
MOZ_ASSERT(!NS_IsMainThread());
SavedRequest savedRequest;
QM_TRY_INSPECT(
const auto& state,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn,
"SELECT "
"request_method, "
"request_url_no_query, "
"request_url_query, "
"request_url_fragment, "
"request_referrer, "
"request_referrer_policy, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_integrity, "
"request_body_id "
"FROM entries "
"WHERE id=:id;"_ns,
[aEntryId](auto& state) -> Result<Ok, nsresult> {
QM_TRY(MOZ_TO_RESULT(state.BindInt32ByName("id"_ns, aEntryId)));
return Ok{};
}));
QM_TRY(OkIf(state), Err(NS_ERROR_UNEXPECTED));
QM_TRY(MOZ_TO_RESULT(state->GetUTF8String(0, savedRequest.mValue.method())));
QM_TRY(MOZ_TO_RESULT(
state->GetUTF8String(1, savedRequest.mValue.urlWithoutQuery())));
QM_TRY(
MOZ_TO_RESULT(state->GetUTF8String(2, savedRequest.mValue.urlQuery())));
QM_TRY(MOZ_TO_RESULT(
state->GetUTF8String(3, savedRequest.mValue.urlFragment())));
QM_TRY(MOZ_TO_RESULT(state->GetString(4, savedRequest.mValue.referrer())));
QM_TRY_INSPECT(const int32_t& referrerPolicy,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 5));
savedRequest.mValue.referrerPolicy() =
static_cast<ReferrerPolicy>(referrerPolicy);
QM_TRY_INSPECT(const int32_t& guard,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 6));
savedRequest.mValue.headersGuard() = static_cast<HeadersGuardEnum>(guard);
QM_TRY_INSPECT(const int32_t& mode,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 7));
savedRequest.mValue.mode() = static_cast<RequestMode>(mode);
QM_TRY_INSPECT(const int32_t& credentials,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 8));
savedRequest.mValue.credentials() =
static_cast<RequestCredentials>(credentials);
QM_TRY_INSPECT(const int32_t& requestContentPolicyType,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 9));
savedRequest.mValue.contentPolicyType() =
static_cast<nsContentPolicyType>(requestContentPolicyType);
QM_TRY_INSPECT(const int32_t& requestCache,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 10));
savedRequest.mValue.requestCache() = static_cast<RequestCache>(requestCache);
QM_TRY_INSPECT(const int32_t& requestRedirect,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetInt32, 11));
savedRequest.mValue.requestRedirect() =
static_cast<RequestRedirect>(requestRedirect);
QM_TRY(MOZ_TO_RESULT(state->GetString(12, savedRequest.mValue.integrity())));
QM_TRY_INSPECT(const bool& nullBody,
MOZ_TO_RESULT_INVOKE_MEMBER(state, GetIsNull, 13));
savedRequest.mHasBodyId = !nullBody;
if (savedRequest.mHasBodyId) {
QM_TRY_UNWRAP(savedRequest.mBodyId, ExtractId(*state, 13));
}
{
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT "
"name, "
"value "
"FROM request_headers "
"WHERE entry_id=:entry_id;"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindInt32ByName("entry_id"_ns, aEntryId)));
QM_TRY_UNWRAP(savedRequest.mValue.headers(),
quota::CollectElementsWhileHasResult(
*state, GetHeadersEntryFromStatement));
}
return savedRequest;
}
void AppendListParamsToQuery(nsACString& aQuery, size_t aLen) {
MOZ_ASSERT(!NS_IsMainThread());
aQuery.AppendLiteral("?");
for (size_t i = 1; i < aLen; ++i) {
aQuery.AppendLiteral(",?");
}
}
nsresult BindListParamsToQuery(mozIStorageStatement& aState,
const Span<const EntryId>& aEntryIdList) {
MOZ_ASSERT(!NS_IsMainThread());
for (size_t i = 0, n = aEntryIdList.Length(); i < n; ++i) {
QM_TRY(MOZ_TO_RESULT(aState.BindInt32ByIndex(i, aEntryIdList[i])));
}
return NS_OK;
}
nsresult BindId(mozIStorageStatement& aState, const nsACString& aName,
const nsID* aId) {
MOZ_ASSERT(!NS_IsMainThread());
if (!aId) {
QM_TRY(MOZ_TO_RESULT(aState.BindNullByName(aName)));
return NS_OK;
}
char idBuf[NSID_LENGTH];
aId->ToProvidedString(idBuf);
QM_TRY(MOZ_TO_RESULT(
aState.BindUTF8StringByName(aName, nsDependentCString(idBuf))));
return NS_OK;
}
Result<nsID, nsresult> ExtractId(mozIStorageStatement& aState, uint32_t aPos) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_INSPECT(const auto& idString,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, aState,
GetUTF8String, aPos));
nsID id;
QM_TRY(OkIf(id.Parse(idString.get())), Err(NS_ERROR_UNEXPECTED));
return id;
}
Result<NotNull<nsCOMPtr<mozIStorageStatement>>, nsresult>
CreateAndBindKeyStatement(mozIStorageConnection& aConn,
const char* const aQueryFormat,
const nsAString& aKey) {
MOZ_DIAGNOSTIC_ASSERT(aQueryFormat);
// The key is stored as a blob to avoid encoding issues. An empty string
// is mapped to NULL for blobs. Normally we would just write the query
// as "key IS :key" to do the proper NULL checking, but that prevents
// sqlite from using the key index. Therefore use "IS NULL" explicitly
// if the key is empty, otherwise use "=:key" so that sqlite uses the
// index.
QM_TRY_UNWRAP(
auto state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
nsPrintfCString(aQueryFormat,
aKey.IsEmpty() ? "key IS NULL" : "key=:key")));
if (!aKey.IsEmpty()) {
QM_TRY(MOZ_TO_RESULT(state->BindStringAsBlobByName("key"_ns, aKey)));
}
return WrapNotNull(std::move(state));
}
Result<nsAutoCString, nsresult> HashCString(nsICryptoHash& aCrypto,
const nsACString& aIn) {
QM_TRY(MOZ_TO_RESULT(aCrypto.Init(nsICryptoHash::SHA1)));
QM_TRY(MOZ_TO_RESULT(aCrypto.Update(
reinterpret_cast<const uint8_t*>(aIn.BeginReading()), aIn.Length())));
nsAutoCString fullHash;
QM_TRY(MOZ_TO_RESULT(aCrypto.Finish(false /* based64 result */, fullHash)));
return Result<nsAutoCString, nsresult>{std::in_place,
Substring(fullHash, 0, 8)};
}
} // namespace
nsresult IncrementalVacuum(mozIStorageConnection& aConn) {
// Determine how much free space is in the database.
QM_TRY_INSPECT(const auto& state, quota::CreateAndExecuteSingleStepStatement(
aConn, "PRAGMA freelist_count;"_ns));
QM_TRY_INSPECT(const int32_t& freePages,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
// We have a relatively small page size, so we want to be careful to avoid
// fragmentation. We already use a growth incremental which will cause
// sqlite to allocate and release multiple pages at the same time. We can
// further reduce fragmentation by making our allocated chunks a bit
// "sticky". This is done by creating some hysteresis where we allocate
// pages/chunks as soon as we need them, but we only release pages/chunks
// when we have a large amount of free space. This helps with the case
// where a page is adding and remove resources causing it to dip back and
// forth across a chunk boundary.
//
// So only proceed with releasing pages if we have more than our constant
// threshold.
if (freePages <= kMaxFreePages) {
return NS_OK;
}
// Release the excess pages back to the sqlite VFS. This may also release
// chunks of multiple pages back to the OS.
const int32_t pagesToRelease = freePages - kMaxFreePages;
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
nsPrintfCString("PRAGMA incremental_vacuum(%d);", pagesToRelease))));
// Verify that our incremental vacuum actually did something
#ifdef DEBUG
{
QM_TRY_INSPECT(const auto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "PRAGMA freelist_count;"_ns));
QM_TRY_INSPECT(const int32_t& freePages,
MOZ_TO_RESULT_INVOKE_MEMBER(*state, GetInt32, 0));
MOZ_ASSERT(freePages <= kMaxFreePages);
}
#endif
return NS_OK;
}
namespace {
// Wrapper around mozIStorageConnection::GetSchemaVersion() that compensates
// for hacky downgrade schema version tricks. See the block comments for
// kHackyDowngradeSchemaVersion and kHackyPaddingSizePresentVersion.
Result<int32_t, nsresult> GetEffectiveSchemaVersion(
mozIStorageConnection& aConn) {
QM_TRY_INSPECT(const int32_t& schemaVersion,
MOZ_TO_RESULT_INVOKE_MEMBER(aConn, GetSchemaVersion));
if (schemaVersion == kHackyDowngradeSchemaVersion) {
// This is the special case. Check for the existence of the
// "response_padding_size" colum in table "entries".
//
// (pragma_table_info is a table-valued function format variant of
// "PRAGMA table_info" supported since SQLite 3.16.0. Firefox 53 shipped
// was the first release with this functionality, shipping 3.16.2.)
//
// If there are any result rows, then the column is present.
QM_TRY_INSPECT(const bool& hasColumn,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn,
"SELECT name FROM pragma_table_info('entries') WHERE "
"name = 'response_padding_size'"_ns));
if (hasColumn) {
return kHackyPaddingSizePresentVersion;
}
}
return schemaVersion;
}
#ifdef DEBUG
struct Expect {
// Expect exact SQL
Expect(const char* aName, const char* aType, const char* aSql)
: mName(aName), mType(aType), mSql(aSql), mIgnoreSql(false) {}
// Ignore SQL
Expect(const char* aName, const char* aType)
: mName(aName), mType(aType), mIgnoreSql(true) {}
const nsCString mName;
const nsCString mType;
const nsCString mSql;
const bool mIgnoreSql;
};
#endif
nsresult Validate(mozIStorageConnection& aConn) {
QM_TRY_INSPECT(const int32_t& schemaVersion,
GetEffectiveSchemaVersion(aConn));
QM_TRY(OkIf(schemaVersion == kLatestSchemaVersion), NS_ERROR_FAILURE);
#ifdef DEBUG
// This is the schema we expect the database at the latest version to
// contain. Update this list if you add a new table or index.
const Expect expects[] = {
Expect("caches", "table", kTableCaches),
Expect("sqlite_sequence", "table"), // auto-gen by sqlite
Expect("security_info", "table", kTableSecurityInfo),
Expect("security_info_hash_index", "index", kIndexSecurityInfoHash),
Expect("entries", "table", kTableEntries),
Expect("entries_request_match_index", "index", kIndexEntriesRequest),
Expect("request_headers", "table", kTableRequestHeaders),
Expect("response_headers", "table", kTableResponseHeaders),
Expect("response_headers_name_index", "index", kIndexResponseHeadersName),
Expect("response_url_list", "table", kTableResponseUrlList),
Expect("storage", "table", kTableStorage),
Expect("sqlite_autoindex_storage_1", "index"), // auto-gen by sqlite
};
// Read the schema from the sqlite_master table and compare.
QM_TRY_INSPECT(const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"SELECT name, type, sql FROM sqlite_master;"_ns));
QM_TRY(quota::CollectWhileHasResult(
*state, [&expects](auto& stmt) -> Result<Ok, nsresult> {
QM_TRY_INSPECT(const auto& name,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, stmt,
GetUTF8String, 0));
QM_TRY_INSPECT(const auto& type,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, stmt,
GetUTF8String, 1));
QM_TRY_INSPECT(const auto& sql,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, stmt,
GetUTF8String, 2));
bool foundMatch = false;
for (const auto& expect : expects) {
if (name == expect.mName) {
if (type != expect.mType) {
NS_WARNING(
nsPrintfCString("Unexpected type for Cache schema entry %s",
name.get())
.get());
return Err(NS_ERROR_FAILURE);
}
if (!expect.mIgnoreSql && sql != expect.mSql) {
NS_WARNING(
nsPrintfCString("Unexpected SQL for Cache schema entry %s",
name.get())
.get());
return Err(NS_ERROR_FAILURE);
}
foundMatch = true;
break;
}
}
if (NS_WARN_IF(!foundMatch)) {
NS_WARNING(
nsPrintfCString("Unexpected schema entry %s in Cache database",
name.get())
.get());
return Err(NS_ERROR_FAILURE);
}
return Ok{};
}));
#endif
return NS_OK;
}
// -----
// Schema migration code
// -----
using MigrationFunc = nsresult (*)(mozIStorageConnection&, bool&);
struct Migration {
int32_t mFromVersion;
MigrationFunc mFunc;
};
// Declare migration functions here. Each function should upgrade
// the version by a single increment. Don't skip versions.
nsresult MigrateFrom15To16(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom16To17(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom17To18(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom18To19(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom19To20(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom20To21(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom21To22(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom22To23(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom23To24(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom24To25(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom25To26(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom26To27(mozIStorageConnection& aConn, bool& aRewriteSchema);
nsresult MigrateFrom27To28(mozIStorageConnection& aConn, bool& aRewriteSchema);
// Configure migration functions to run for the given starting version.
constexpr Migration sMigrationList[] = {
Migration{15, MigrateFrom15To16}, Migration{16, MigrateFrom16To17},
Migration{17, MigrateFrom17To18}, Migration{18, MigrateFrom18To19},
Migration{19, MigrateFrom19To20}, Migration{20, MigrateFrom20To21},
Migration{21, MigrateFrom21To22}, Migration{22, MigrateFrom22To23},
Migration{23, MigrateFrom23To24}, Migration{24, MigrateFrom24To25},
Migration{25, MigrateFrom25To26}, Migration{26, MigrateFrom26To27},
Migration{27, MigrateFrom27To28},
};
nsresult RewriteEntriesSchema(mozIStorageConnection& aConn) {
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("PRAGMA writable_schema = ON"_ns)));
QM_TRY_INSPECT(
const auto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
"UPDATE sqlite_master SET sql=:sql WHERE name='entries'"_ns));
QM_TRY(MOZ_TO_RESULT(state->BindUTF8StringByName(
"sql"_ns, nsDependentCString(kTableEntries))));
QM_TRY(MOZ_TO_RESULT(state->Execute()));
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("PRAGMA writable_schema = OFF"_ns)));
return NS_OK;
}
nsresult Migrate(mozIStorageConnection& aConn) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY_UNWRAP(int32_t currentVersion, GetEffectiveSchemaVersion(aConn));
bool rewriteSchema = false;
while (currentVersion < kLatestSchemaVersion) {
// Wiping old databases is handled in DBAction because it requires
// making a whole new mozIStorageConnection. Make sure we don't
// accidentally get here for one of those old databases.
MOZ_DIAGNOSTIC_ASSERT(currentVersion >= kFirstShippedSchemaVersion);
for (const auto& migration : sMigrationList) {
if (migration.mFromVersion == currentVersion) {
bool shouldRewrite = false;
QM_TRY(MOZ_TO_RESULT(migration.mFunc(aConn, shouldRewrite)));
if (shouldRewrite) {
rewriteSchema = true;
}
break;
}
}
#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
int32_t lastVersion = currentVersion;
#endif
QM_TRY_UNWRAP(currentVersion, GetEffectiveSchemaVersion(aConn));
MOZ_DIAGNOSTIC_ASSERT(currentVersion > lastVersion);
}
// Don't release assert this since people do sometimes share profiles
// across schema versions. Our check in Validate() will catch it.
MOZ_ASSERT(currentVersion == kLatestSchemaVersion);
nsresult rv = NS_OK;
if (rewriteSchema) {
// Now overwrite the master SQL for the entries table to remove the column
// default value. This is also necessary for our Validate() method to
// pass on this database.
rv = RewriteEntriesSchema(aConn);
}
return rv;
}
nsresult MigrateFrom15To16(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// Add the request_redirect column with a default value of "follow". Note,
// we only use a default value here because its required by ALTER TABLE and
// we need to apply the default "follow" to existing records in the table.
// We don't actually want to keep the default in the schema for future
// INSERTs.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"ALTER TABLE entries "
"ADD COLUMN request_redirect INTEGER NOT NULL DEFAULT 0"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(16)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom16To17(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// This migration path removes the response_redirected and
// response_redirected_url columns from the entries table. sqlite doesn't
// support removing a column from a table using ALTER TABLE, so we need to
// create a new table without those columns, fill it up with the existing
// data, and then drop the original table and rename the new one to the old
// one.
// Create a new_entries table with the new fields as of version 17.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"CREATE TABLE new_entries ("
"id INTEGER NOT NULL PRIMARY KEY, "
"request_method TEXT NOT NULL, "
"request_url_no_query TEXT NOT NULL, "
"request_url_no_query_hash BLOB NOT NULL, "
"request_url_query TEXT NOT NULL, "
"request_url_query_hash BLOB NOT NULL, "
"request_referrer TEXT NOT NULL, "
"request_headers_guard INTEGER NOT NULL, "
"request_mode INTEGER NOT NULL, "
"request_credentials INTEGER NOT NULL, "
"request_contentpolicytype INTEGER NOT NULL, "
"request_cache INTEGER NOT NULL, "
"request_body_id TEXT NULL, "
"response_type INTEGER NOT NULL, "
"response_url TEXT NOT NULL, "
"response_status INTEGER NOT NULL, "
"response_status_text TEXT NOT NULL, "
"response_headers_guard INTEGER NOT NULL, "
"response_body_id TEXT NULL, "
"response_security_info_id INTEGER NULL REFERENCES security_info(id), "
"response_principal_info TEXT NOT NULL, "
"cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
"request_redirect INTEGER NOT NULL"
")"_ns)));
// Copy all of the data to the newly created table.
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("INSERT INTO new_entries ("
"id, "
"request_method, "
"request_url_no_query, "
"request_url_no_query_hash, "
"request_url_query, "
"request_url_query_hash, "
"request_referrer, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_body_id, "
"response_type, "
"response_url, "
"response_status, "
"response_status_text, "
"response_headers_guard, "
"response_body_id, "
"response_security_info_id, "
"response_principal_info, "
"cache_id "
") SELECT "
"id, "
"request_method, "
"request_url_no_query, "
"request_url_no_query_hash, "
"request_url_query, "
"request_url_query_hash, "
"request_referrer, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_body_id, "
"response_type, "
"response_url, "
"response_status, "
"response_status_text, "
"response_headers_guard, "
"response_body_id, "
"response_security_info_id, "
"response_principal_info, "
"cache_id "
"FROM entries;"_ns)));
// Remove the old table.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("DROP TABLE entries;"_ns)));
// Rename new_entries to entries.
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("ALTER TABLE new_entries RENAME to entries;"_ns)));
// Now, recreate our indices.
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsDependentCString(kIndexEntriesRequest))));
// Revalidate the foreign key constraints, and ensure that there are no
// violations.
QM_TRY_INSPECT(const bool& hasResult,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "PRAGMA foreign_key_check;"_ns));
QM_TRY(OkIf(!hasResult), NS_ERROR_FAILURE);
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(17)));
return NS_OK;
}
nsresult MigrateFrom17To18(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// This migration is needed in order to remove "only-if-cached" RequestCache
// values from the database. This enum value was removed from the spec in
// https://github.com/whatwg/fetch/issues/39 but we unfortunately happily
// accepted this value in the Request constructor.
//
// There is no good value to upgrade this to, so we just stick to "default".
static_assert(int(RequestCache::Default) == 0,
"This is where the 0 below comes from!");
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("UPDATE entries SET request_cache = 0 "
"WHERE request_cache = 5;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(18)));
return NS_OK;
}
nsresult MigrateFrom18To19(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// This migration is needed in order to update the RequestMode values for
// Request objects corresponding to a navigation content policy type to
// "navigate".
static_assert(int(nsIContentPolicy::TYPE_DOCUMENT) == 6 &&
int(nsIContentPolicy::TYPE_SUBDOCUMENT) == 7 &&
int(nsIContentPolicy::TYPE_INTERNAL_FRAME) == 28 &&
int(nsIContentPolicy::TYPE_INTERNAL_IFRAME) == 29 &&
int(RequestMode::Navigate) == 3,
"This is where the numbers below come from!");
// 8 is former TYPE_REFRESH.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"UPDATE entries SET request_mode = 3 "
"WHERE request_contentpolicytype IN (6, 7, 28, 29, 8);"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(19)));
return NS_OK;
}
nsresult MigrateFrom19To20(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// Add the request_referrer_policy column with a default value of
// "no-referrer-when-downgrade". Note, we only use a default value here
// because its required by ALTER TABLE and we need to apply the default
// "no-referrer-when-downgrade" to existing records in the table. We don't
// actually want to keep the default in the schema for future INSERTs.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"ALTER TABLE entries "
"ADD COLUMN request_referrer_policy INTEGER NOT NULL DEFAULT 2"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(20)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom20To21(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// This migration creates response_url_list table to store response_url and
// removes the response_url column from the entries table.
// sqlite doesn't support removing a column from a table using ALTER TABLE,
// so we need to create a new table without those columns, fill it up with the
// existing data, and then drop the original table and rename the new one to
// the old one.
// Create a new_entries table with the new fields as of version 21.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"CREATE TABLE new_entries ("
"id INTEGER NOT NULL PRIMARY KEY, "
"request_method TEXT NOT NULL, "
"request_url_no_query TEXT NOT NULL, "
"request_url_no_query_hash BLOB NOT NULL, "
"request_url_query TEXT NOT NULL, "
"request_url_query_hash BLOB NOT NULL, "
"request_referrer TEXT NOT NULL, "
"request_headers_guard INTEGER NOT NULL, "
"request_mode INTEGER NOT NULL, "
"request_credentials INTEGER NOT NULL, "
"request_contentpolicytype INTEGER NOT NULL, "
"request_cache INTEGER NOT NULL, "
"request_body_id TEXT NULL, "
"response_type INTEGER NOT NULL, "
"response_status INTEGER NOT NULL, "
"response_status_text TEXT NOT NULL, "
"response_headers_guard INTEGER NOT NULL, "
"response_body_id TEXT NULL, "
"response_security_info_id INTEGER NULL REFERENCES security_info(id), "
"response_principal_info TEXT NOT NULL, "
"cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
"request_redirect INTEGER NOT NULL, "
"request_referrer_policy INTEGER NOT NULL"
")"_ns)));
// Create a response_url_list table with the new fields as of version 21.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"CREATE TABLE response_url_list ("
"url TEXT NOT NULL, "
"entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
")"_ns)));
// Copy all of the data to the newly created entries table.
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("INSERT INTO new_entries ("
"id, "
"request_method, "
"request_url_no_query, "
"request_url_no_query_hash, "
"request_url_query, "
"request_url_query_hash, "
"request_referrer, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_referrer_policy, "
"request_body_id, "
"response_type, "
"response_status, "
"response_status_text, "
"response_headers_guard, "
"response_body_id, "
"response_security_info_id, "
"response_principal_info, "
"cache_id "
") SELECT "
"id, "
"request_method, "
"request_url_no_query, "
"request_url_no_query_hash, "
"request_url_query, "
"request_url_query_hash, "
"request_referrer, "
"request_headers_guard, "
"request_mode, "
"request_credentials, "
"request_contentpolicytype, "
"request_cache, "
"request_redirect, "
"request_referrer_policy, "
"request_body_id, "
"response_type, "
"response_status, "
"response_status_text, "
"response_headers_guard, "
"response_body_id, "
"response_security_info_id, "
"response_principal_info, "
"cache_id "
"FROM entries;"_ns)));
// Copy reponse_url to the newly created response_url_list table.
QM_TRY(
MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("INSERT INTO response_url_list ("
"url, "
"entry_id "
") SELECT "
"response_url, "
"id "
"FROM entries;"_ns)));
// Remove the old table.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL("DROP TABLE entries;"_ns)));
// Rename new_entries to entries.
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("ALTER TABLE new_entries RENAME to entries;"_ns)));
// Now, recreate our indices.
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexEntriesRequest))));
// Revalidate the foreign key constraints, and ensure that there are no
// violations.
QM_TRY_INSPECT(const bool& hasResult,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "PRAGMA foreign_key_check;"_ns));
QM_TRY(OkIf(!hasResult), NS_ERROR_FAILURE);
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(21)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom21To22(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// Add the request_integrity column.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"ALTER TABLE entries "
"ADD COLUMN request_integrity TEXT NOT NULL DEFAULT '';"_ns)));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("UPDATE entries SET request_integrity = '';"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(22)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom22To23(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// The only change between 22 and 23 was a different snappy compression
// format, but it's backwards-compatible.
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(23)));
return NS_OK;
}
nsresult MigrateFrom23To24(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// Add the request_url_fragment column.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"ALTER TABLE entries "
"ADD COLUMN request_url_fragment TEXT NOT NULL DEFAULT ''"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(24)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom24To25(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// The only change between 24 and 25 was a new nsIContentPolicy type.
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(25)));
return NS_OK;
}
nsresult MigrateFrom25To26(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// Add the response_padding_size column.
// Note: only opaque repsonse should be non-null interger.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
"ALTER TABLE entries "
"ADD COLUMN response_padding_size INTEGER NULL "_ns)));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("UPDATE entries SET response_padding_size = 0 "
"WHERE response_type = 4"_ns // opaque response
)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(26)));
aRewriteSchema = true;
return NS_OK;
}
nsresult MigrateFrom26To27(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(kHackyDowngradeSchemaVersion)));
return NS_OK;
}
nsresult MigrateFrom27To28(mozIStorageConnection& aConn, bool& aRewriteSchema) {
MOZ_ASSERT(!NS_IsMainThread());
// In Bug 1264178, we added a column request_integrity into table entries.
// However, at that time, the default value for the existing rows is NULL
// which against the statement in kTableEntries. Thus, we need to have another
// upgrade to update these values to an empty string.
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL("UPDATE entries SET request_integrity = '' "
"WHERE request_integrity is NULL;"_ns)));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(28)));
return NS_OK;
}
} // anonymous namespace
} // namespace mozilla::dom::cache::db