/* -*- 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/. */
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 = 29; // --------- // 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. constchar 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. constchar 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. constchar kIndexSecurityInfoHash[] = "CREATE INDEX security_info_hash_index ON security_info (hash)";
constchar 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, " "request_body_disk_size INTEGER NULL, " "response_body_disk_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. constchar kIndexEntriesRequest[] = "CREATE INDEX entries_request_match_index " "ON entries (cache_id, request_url_no_query_hash, " "request_url_query_hash)";
constchar kTableRequestHeaders[] = "CREATE TABLE request_headers (" "name TEXT NOT NULL, " "value TEXT NOT NULL, " "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE" ")";
constchar 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. constchar kIndexResponseHeadersName[] = "CREATE INDEX response_headers_name_index " "ON response_headers (name)";
constchar 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. constchar kTableStorage[] = "CREATE TABLE storage (" "namespace INTEGER NOT NULL, " "key BLOB NULL, " "cache_id INTEGER NOT NULL REFERENCES caches(id), " "PRIMARY KEY(namespace, key) " ")";
constchar kTableUsageInfo[] = "CREATE TABLE usage_info (" "id INTEGER NOT NULL PRIMARY KEY, " "total_disk_usage INTEGER NOT NULL " ")";
// --------- // 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 &&
ContiguousEnumSize<HeadersGuardEnum>::value == 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 &&
ContiguousEnumSize<ReferrerPolicy>::value == 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 &&
ContiguousEnumSize<RequestMode>::value == 4, "RequestMode values are as expected");
static_assert(int(RequestCredentials::Omit) == 0 && int(RequestCredentials::Same_origin) == 1 && int(RequestCredentials::Include) == 2 &&
ContiguousEnumSize<RequestCredentials>::value == 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 &&
ContiguousEnumSize<RequestCache>::value == 6, "RequestCache values are as expected");
static_assert(int(RequestRedirect::Follow) == 0 && int(RequestRedirect::Error) == 1 && int(RequestRedirect::Manual) == 2 &&
ContiguousEnumSize<RequestRedirect>::value == 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 &&
ContiguousEnumSize<ResponseType>::value == 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 (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()));
constbool 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(aDBDir, 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.ExecuteSimpleSQL(nsLiteralCString(kTableUsageInfo))));
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(
nsLiteralCString("INSERT INTO usage_info VALUES(1, 0);"))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTriggerEntriesInsert))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTriggerEntriesUpdate))));
QM_TRY(MOZ_TO_RESULT(
aConn.ExecuteSimpleSQL(nsLiteralCString(kTriggerEntriesDelete))));
QM_TRY(MOZ_TO_RESULT(aConn.SetSchemaVersion(kLatestSchemaVersion)));
QM_TRY_UNWRAP(schemaVersion, GetEffectiveSchemaVersion(aConn));
}
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.
// 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. // Note there is a default journal_size_limit set by mozStorage.
QM_TRY(MOZ_TO_RESULT(aConn.ExecuteSimpleSQL(nsPrintfCString( // WAL journal can grow to given number of *pages* "PRAGMA wal_autocheckpoint = %u; " // WAL must be enabled at the end to allow page size to be changed, etc. "PRAGMA journal_mode = WAL; ",
kWalAutoCheckpointPages))));
// 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(constauto& state,
quota::CreateAndExecuteSingleStepStatement(
aConn, "PRAGMA auto_vacuum;"_ns));
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteAllCacheEntries(aConn, aCacheId));
// Delete the remainder of the cache using cascade semantics.
QM_TRY_INSPECT(constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "DELETE FROM caches WHERE id=:id;"_ns));
Result<AutoTArray<CacheId, 8>, nsresult> FindOrphanedCacheIds(
mozIStorageConnection& aConn) {
QM_TRY_INSPECT(constauto& 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));
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteEntries(aConn, matches));
// 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)));
if (matches.IsEmpty()) { return Maybe<DeletionInfo>();
}
// XXX only deletedBodyIdList needs to be non-const
QM_TRY_UNWRAP(
(auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
DeleteEntries(aConn, matches));
// If we are given a cache to check, then simply find its cache ID // and perform the match. if (aParams.cacheNameSet()) {
QM_TRY_INSPECT(constauto& maybeCacheId,
StorageGetCacheId(aConn, aNamespace, aParams.cacheName())); if (maybeCacheId.isNothing()) { return Maybe<SavedResponse>();
}
// Otherwise we need to get a list of all the cache IDs in this namespace.
QM_TRY_INSPECT(constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "SELECT cache_id FROM storage WHERE " "namespace=:namespace ORDER BY rowid;"_ns));
// Now try to find a match in each cache in order for (constauto cacheId : cacheIdList) {
QM_TRY_UNWRAP(auto matchedResponse,
CacheMatch(aConn, cacheId, aRequest, aParams));
if (matchedResponse.isSome()) { return matchedResponse;
}
}
// 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. constchar* const query = "SELECT cache_id FROM storage " "WHERE namespace=:namespace AND %s " "ORDER BY rowid;";
// 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. constchar* const query = "DELETE FROM storage WHERE namespace=:namespace AND %s;";
QM_TRY_INSPECT( constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "SELECT key FROM storage WHERE namespace=:namespace ORDER BY rowid;"_ns));
QM_TRY_INSPECT( constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "SELECT id FROM entries WHERE cache_id=:cache_id ORDER BY id;"_ns));
// Assume the vary headers match until we find a conflict bool varyHeadersMatch = true;
for (constauto& 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()");
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(constbool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, i));
if (!isNull) {
QM_TRY_INSPECT(constauto& id, ExtractId(stmt, i));
aDeletedBodyIdListOut.AppendElement(id);
}
}
{ // and then a possible third entry for the security id
QM_TRY_INSPECT(constbool& 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](constauto& 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(constbool& 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));
do { // Sqlite limits the number of entries allowed for an IN clause, // so split up larger operations. auto currLen = std::min(kMaxEntriesPerStatement, remaining);
// XXX: We could create a query string with aggregation that would generate // a single summary result such that we don't have to go through each row // just to aggregate the result. This method could become much more // performant. // // The columns could look like: // // GROUP_CONCAT(request_body_id || ',' || response_body_id), // GROUP_CONCAT(response_security_info_id), // SUM(response_padding_size) // // strtok the result row to generate the desired output to fill-in // deletedBodyIdList, deletedSecurityIdList and deletedPaddingSize // // I am not sure about the memory requirements for such operation; // it will all depend upon the result filtered by the `cache_id`.
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(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(constbool& isNull,
MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetIsNull, i));
if (!isNull) {
QM_TRY_INSPECT(constauto& id, ExtractId(stmt, i));
deletedBodyIdList.AppendElement(id);
}
}
{ // and then a possible third entry for the security id
QM_TRY_INSPECT(constbool& 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](constauto& 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(constbool& 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));
// 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(constauto& 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( constauto& 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( constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
// This is a new security info blob. Create a new row in the security table // with an initial refcount of 1.
QM_TRY_INSPECT(constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "INSERT INTO security_info (hash, data, refcount) " "VALUES (:hash, :data, 1);"_ns));
// 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(constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "DELETE FROM security_info WHERE id=:id;"_ns));
// Otherwise update the refcount in the table to reflect the reduced // number of references to the security blob.
QM_TRY_INSPECT( constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
/** * 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;
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
nsCOMPtr<nsIPrincipal> principal =
BasePrincipal::CreateContentPrincipal(url, attrs); if (!principal) { return Err(NS_ERROR_NULL_POINTER);
}
// 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)));
}
nsresult IncrementalVacuum(mozIStorageConnection& aConn) { // Determine how much free space is in the database.
QM_TRY_INSPECT(constauto& state, quota::CreateAndExecuteSingleStepStatement(
aConn, "PRAGMA freelist_count;"_ns));
// 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;
// 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(constbool& hasColumn,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "SELECT name FROM pragma_table_info('entries') WHERE " "name = 'response_padding_size'"_ns));
if (hasColumn) { return kHackyPaddingSizePresentVersion;
}
}
#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
Expect("usage_info", "table", kTableUsageInfo),
Expect("entries_insert_trigger", "trigger", kTriggerEntriesInsert),
Expect("entries_update_trigger", "trigger", kTriggerEntriesUpdate),
Expect("entries_delete_trigger", "trigger", kTriggerEntriesDelete),
};
// Read the schema from the sqlite_master table and compare.
QM_TRY_INSPECT(constauto& state,
MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement, "SELECT name, type, sql FROM sqlite_master;"_ns));
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 (constauto& migration : sMigrationList) { if (migration.mFromVersion == currentVersion) { bool shouldRewrite = false;
QM_TRY(MOZ_TO_RESULT(migration.mFunc(aDBDir, aConn, shouldRewrite))); if (shouldRewrite) {
rewriteSchema = true;
} break;
}
}
// 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);
}
// 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)));
// 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)));
// Revalidate the foreign key constraints, and ensure that there are no // violations.
QM_TRY_INSPECT(constbool& hasResult,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "PRAGMA foreign_key_check;"_ns));
// 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)));
// 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)));
// 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)));
// 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)));
// Revalidate the foreign key constraints, and ensure that there are no // violations.
QM_TRY_INSPECT(constbool& hasResult,
quota::CreateAndExecuteSingleStepStatement<
quota::SingleStepResult::ReturnNullIfNoResult>(
aConn, "PRAGMA foreign_key_check;"_ns));
// 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)));
// 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)));
class BodyDiskSizeGetterFunction final : public mozIStorageFunction { public: explicit BodyDiskSizeGetterFunction(nsCOMPtr<nsIFile> aDBDir)
: mDBDir(std::move(aDBDir)), mTotalDiskUsage(0) {}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.