diff options
Diffstat (limited to 'dom/cache/DBSchema.cpp')
-rw-r--r-- | dom/cache/DBSchema.cpp | 3018 |
1 files changed, 3018 insertions, 0 deletions
diff --git a/dom/cache/DBSchema.cpp b/dom/cache/DBSchema.cpp new file mode 100644 index 000000000..d16ba2d6a --- /dev/null +++ b/dom/cache/DBSchema.cpp @@ -0,0 +1,3018 @@ +/* -*- 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 "mozilla/BasePrincipal.h" +#include "mozilla/dom/HeadersBinding.h" +#include "mozilla/dom/InternalHeaders.h" +#include "mozilla/dom/RequestBinding.h" +#include "mozilla/dom/ResponseBinding.h" +#include "mozilla/dom/cache/CacheTypes.h" +#include "mozilla/dom/cache/SavedTypes.h" +#include "mozilla/dom/cache/Types.h" +#include "mozilla/dom/cache/TypeUtils.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozStorageHelper.h" +#include "nsCOMPtr.h" +#include "nsCRT.h" +#include "nsHttp.h" +#include "nsIContentPolicy.h" +#include "nsICryptoHash.h" +#include "nsNetCID.h" +#include "nsPrintfCString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace dom { +namespace cache { +namespace db { +const int32_t kFirstShippedSchemaVersion = 15; +namespace { +// Update this whenever the DB schema is changed. +const int32_t kLatestSchemaVersion = 24; +// --------- +// 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* const 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* const 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* const kIndexSecurityInfoHash = + "CREATE INDEX security_info_hash_index ON security_info (hash)"; + +const char* const 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" + // 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* const kIndexEntriesRequest = + "CREATE INDEX entries_request_match_index " + "ON entries (cache_id, request_url_no_query_hash, " + "request_url_query_hash)"; + +const char* const 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* const 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* const kIndexResponseHeadersName = + "CREATE INDEX response_headers_name_index " + "ON response_headers (name)"; + +const char* const 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* const 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 int32_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 && + int(HeadersGuardEnum::EndGuard_) == 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::EndGuard_) == 6, + "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 && + int(RequestMode::EndGuard_) == 4, + "RequestMode values are as expected"); +static_assert(int(RequestCredentials::Omit) == 0 && + int(RequestCredentials::Same_origin) == 1 && + int(RequestCredentials::Include) == 2 && + int(RequestCredentials::EndGuard_) == 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 && + int(RequestCache::EndGuard_) == 6, + "RequestCache values are as expected"); +static_assert(int(RequestRedirect::Follow) == 0 && + int(RequestRedirect::Error) == 1 && + int(RequestRedirect::Manual) == 2 && + int(RequestRedirect::EndGuard_) == 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 && + int(ResponseType::EndGuard_) == 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_REFRESH == 8 && + nsIContentPolicy::TYPE_XBL == 9 && + nsIContentPolicy::TYPE_PING == 10 && + nsIContentPolicy::TYPE_XMLHTTPREQUEST == 11 && + nsIContentPolicy::TYPE_DATAREQUEST == 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, + "nsContentPolicyType values are as expected"); + +namespace { + +typedef int32_t EntryId; + +struct IdCount +{ + IdCount() : mId(-1), mCount(0) { } + explicit IdCount(int32_t aId) : mId(aId), mCount(1) { } + int32_t mId; + int32_t mCount; +}; + +static nsresult QueryAll(mozIStorageConnection* aConn, CacheId aCacheId, + nsTArray<EntryId>& aEntryIdListOut); +static nsresult QueryCache(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequest& aRequest, + const CacheQueryParams& aParams, + nsTArray<EntryId>& aEntryIdListOut, + uint32_t aMaxResults = UINT32_MAX); +static nsresult MatchByVaryHeader(mozIStorageConnection* aConn, + const CacheRequest& aRequest, + EntryId entryId, bool* aSuccessOut); +static nsresult DeleteEntries(mozIStorageConnection* aConn, + const nsTArray<EntryId>& aEntryIdList, + nsTArray<nsID>& aDeletedBodyIdListOut, + nsTArray<IdCount>& aDeletedSecurityIdListOut, + uint32_t aPos=0, int32_t aLen=-1); +static nsresult InsertSecurityInfo(mozIStorageConnection* aConn, + nsICryptoHash* aCrypto, + const nsACString& aData, int32_t *aIdOut); +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 nsresult ReadResponse(mozIStorageConnection* aConn, EntryId aEntryId, + SavedResponse* aSavedResponseOut); +static nsresult ReadRequest(mozIStorageConnection* aConn, EntryId aEntryId, + SavedRequest* aSavedRequestOut); + +static void AppendListParamsToQuery(nsACString& aQuery, + const nsTArray<EntryId>& aEntryIdList, + uint32_t aPos, int32_t aLen); +static nsresult BindListParamsToQuery(mozIStorageStatement* aState, + const nsTArray<EntryId>& aEntryIdList, + uint32_t aPos, int32_t aLen); +static nsresult BindId(mozIStorageStatement* aState, const nsACString& aName, + const nsID* aId); +static nsresult ExtractId(mozIStorageStatement* aState, uint32_t aPos, + nsID* aIdOut); +static nsresult CreateAndBindKeyStatement(mozIStorageConnection* aConn, + const char* aQueryFormat, + const nsAString& aKey, + mozIStorageStatement** aStateOut); +static nsresult HashCString(nsICryptoHash* aCrypto, const nsACString& aIn, + nsACString& aOut); +nsresult Validate(mozIStorageConnection* aConn); +nsresult Migrate(mozIStorageConnection* aConn); +} // namespace + +class MOZ_RAII AutoDisableForeignKeyChecking +{ +public: + explicit AutoDisableForeignKeyChecking(mozIStorageConnection* aConn) + : mConn(aConn) + , mForeignKeyCheckingDisabled(false) + { + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = mConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA foreign_keys;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return; } + + int32_t mode; + rv = state->GetInt32(0, &mode); + if (NS_WARN_IF(NS_FAILED(rv))) { return; } + + if (mode) { + nsresult rv = mConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA foreign_keys = OFF;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return; } + mForeignKeyCheckingDisabled = true; + } + } + + ~AutoDisableForeignKeyChecking() + { + if (mForeignKeyCheckingDisabled) { + nsresult rv = mConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA foreign_keys = ON;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return; } + } + } + +private: + nsCOMPtr<mozIStorageConnection> mConn; + bool mForeignKeyCheckingDisabled; +}; + +nsresult +CreateOrMigrateSchema(mozIStorageConnection* aConn) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + int32_t schemaVersion; + nsresult rv = aConn->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (schemaVersion == kLatestSchemaVersion) { + // We already have the correct schema version. Validate it matches + // our expected schema and then proceed. + rv = Validate(aConn); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; + } + + // 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); + bool needVacuum = false; + + if (schemaVersion) { + // A schema exists, but its not the current version. Attempt to + // migrate it to our new schema. + rv = Migrate(aConn); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // 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. + needVacuum = true; + } else { + // There is no schema installed. Create the database from scratch. + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableCaches)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableSecurityInfo)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kIndexSecurityInfoHash)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableEntries)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kIndexEntriesRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableRequestHeaders)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableResponseHeaders)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kIndexResponseHeadersName)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableResponseUrlList)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kTableStorage)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(kLatestSchemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + rv = Validate(aConn); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = trans.Commit(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (needVacuum) { + // Unfortunately, this must be performed outside of the transaction. + aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING("VACUUM")); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + return rv; +} + +nsresult +InitializeConnection(mozIStorageConnection* aConn) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // This function needs to perform per-connection initialization tasks that + // need to happen regardless of the schema. + + nsPrintfCString pragmas( + // 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 + ); + + // 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. + + nsresult rv = aConn->ExecuteSimpleSQL(pragmas); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Limit fragmentation by growing the database by many pages at once. + rv = aConn->SetGrowthIncrement(kGrowthSize, EmptyCString()); + if (rv == NS_ERROR_FILE_TOO_BIG) { + NS_WARNING("Not enough disk space to set sqlite growth increment."); + rv = NS_OK; + } + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Enable WAL journaling. This must be performed in a separate transaction + // after changing the page_size and enabling auto_vacuum. + nsPrintfCString wal( + // 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 + ); + + rv = aConn->ExecuteSimpleSQL(wal); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // 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 + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA auto_vacuum;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t mode; + rv = state->GetInt32(0, &mode); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // integer value 2 is incremental mode + if (NS_WARN_IF(mode != 2)) { return NS_ERROR_UNEXPECTED; } +#endif + + return NS_OK; +} + +nsresult +CreateCacheId(mozIStorageConnection* aConn, CacheId* aCacheIdOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aCacheIdOut); + + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO caches DEFAULT VALUES;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT last_insert_rowid()" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (NS_WARN_IF(!hasMoreData)) { return NS_ERROR_UNEXPECTED; } + + rv = state->GetInt64(0, aCacheIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +DeleteCacheId(mozIStorageConnection* aConn, CacheId aCacheId, + nsTArray<nsID>& aDeletedBodyIdListOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // Delete the bodies explicitly as we need to read out the body IDs + // anyway. These body IDs must be deleted one-by-one as content may + // still be referencing them invidivually. + AutoTArray<EntryId, 256> matches; + nsresult rv = QueryAll(aConn, aCacheId, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + AutoTArray<IdCount, 16> deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Delete the remainder of the cache using cascade semantics. + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM caches WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +IsCacheOrphaned(mozIStorageConnection* aConn, CacheId aCacheId, + bool* aOrphanedOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aOrphanedOut); + + // err on the side of not deleting user data + *aOrphanedOut = false; + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT COUNT(*) FROM storage WHERE cache_id=:cache_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + MOZ_DIAGNOSTIC_ASSERT(hasMoreData); + + int32_t refCount; + rv = state->GetInt32(0, &refCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + *aOrphanedOut = refCount == 0; + + return rv; +} + +nsresult +FindOrphanedCacheIds(mozIStorageConnection* aConn, + nsTArray<CacheId>& aOrphanedListOut) +{ + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id FROM caches " + "WHERE id NOT IN (SELECT cache_id from storage);" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + CacheId cacheId = INVALID_CACHE_ID; + rv = state->GetInt64(0, &cacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aOrphanedListOut.AppendElement(cacheId); + } + + return rv; +} + +nsresult +GetKnownBodyIds(mozIStorageConnection* aConn, nsTArray<nsID>& aBodyIdListOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT request_body_id, response_body_id FROM entries;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + // extract 0 to 2 nsID structs per row + for (uint32_t i = 0; i < 2; ++i) { + bool isNull = false; + + rv = state->GetIsNull(i, &isNull); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!isNull) { + nsID id; + rv = ExtractId(state, i, &id); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aBodyIdListOut.AppendElement(id); + } + } + } + + return rv; +} + +nsresult +CacheMatch(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequest& aRequest, + const CacheQueryParams& aParams, + bool* aFoundResponseOut, + SavedResponse* aSavedResponseOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aFoundResponseOut); + MOZ_DIAGNOSTIC_ASSERT(aSavedResponseOut); + + *aFoundResponseOut = false; + + AutoTArray<EntryId, 1> matches; + nsresult rv = QueryCache(aConn, aCacheId, aRequest, aParams, matches, 1); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (matches.IsEmpty()) { + return rv; + } + + rv = ReadResponse(aConn, matches[0], aSavedResponseOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aSavedResponseOut->mCacheId = aCacheId; + *aFoundResponseOut = true; + + return rv; +} + +nsresult +CacheMatchAll(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequestOrVoid& aRequestOrVoid, + const CacheQueryParams& aParams, + nsTArray<SavedResponse>& aSavedResponsesOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + nsresult rv; + + AutoTArray<EntryId, 256> matches; + if (aRequestOrVoid.type() == CacheRequestOrVoid::Tvoid_t) { + rv = QueryAll(aConn, aCacheId, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } else { + rv = QueryCache(aConn, aCacheId, aRequestOrVoid, aParams, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + // TODO: replace this with a bulk load using SQL IN clause (bug 1110458) + for (uint32_t i = 0; i < matches.Length(); ++i) { + SavedResponse savedResponse; + rv = ReadResponse(aConn, matches[i], &savedResponse); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + savedResponse.mCacheId = aCacheId; + aSavedResponsesOut.AppendElement(savedResponse); + } + + return rv; +} + +nsresult +CachePut(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequest& aRequest, + const nsID* aRequestBodyId, + const CacheResponse& aResponse, + const nsID* aResponseBodyId, + nsTArray<nsID>& aDeletedBodyIdListOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + CacheQueryParams params(false, false, false, false, + NS_LITERAL_STRING("")); + AutoTArray<EntryId, 256> matches; + nsresult rv = QueryCache(aConn, aCacheId, aRequest, params, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + AutoTArray<IdCount, 16> deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = InsertEntry(aConn, aCacheId, aRequest, aRequestBodyId, aResponse, + aResponseBodyId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Delete the security values after doing the insert to avoid churning + // the security table when its not necessary. + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +CacheDelete(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequest& aRequest, + const CacheQueryParams& aParams, + nsTArray<nsID>& aDeletedBodyIdListOut, bool* aSuccessOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aSuccessOut); + + *aSuccessOut = false; + + AutoTArray<EntryId, 256> matches; + nsresult rv = QueryCache(aConn, aCacheId, aRequest, aParams, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (matches.IsEmpty()) { + return rv; + } + + AutoTArray<IdCount, 16> deletedSecurityIdList; + rv = DeleteEntries(aConn, matches, aDeletedBodyIdListOut, + deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = DeleteSecurityInfoList(aConn, deletedSecurityIdList); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + *aSuccessOut = true; + + return rv; +} + +nsresult +CacheKeys(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequestOrVoid& aRequestOrVoid, + const CacheQueryParams& aParams, + nsTArray<SavedRequest>& aSavedRequestsOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + nsresult rv; + + AutoTArray<EntryId, 256> matches; + if (aRequestOrVoid.type() == CacheRequestOrVoid::Tvoid_t) { + rv = QueryAll(aConn, aCacheId, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } else { + rv = QueryCache(aConn, aCacheId, aRequestOrVoid, aParams, matches); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + // TODO: replace this with a bulk load using SQL IN clause (bug 1110458) + for (uint32_t i = 0; i < matches.Length(); ++i) { + SavedRequest savedRequest; + rv = ReadRequest(aConn, matches[i], &savedRequest); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + savedRequest.mCacheId = aCacheId; + aSavedRequestsOut.AppendElement(savedRequest); + } + + return rv; +} + +nsresult +StorageMatch(mozIStorageConnection* aConn, + Namespace aNamespace, + const CacheRequest& aRequest, + const CacheQueryParams& aParams, + bool* aFoundResponseOut, + SavedResponse* aSavedResponseOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aFoundResponseOut); + MOZ_DIAGNOSTIC_ASSERT(aSavedResponseOut); + + *aFoundResponseOut = false; + + nsresult rv; + + // If we are given a cache to check, then simply find its cache ID + // and perform the match. + if (!aParams.cacheName().EqualsLiteral("")) { + bool foundCache = false; + // no invalid CacheId, init to least likely real value + CacheId cacheId = INVALID_CACHE_ID; + rv = StorageGetCacheId(aConn, aNamespace, aParams.cacheName(), &foundCache, + &cacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (!foundCache) { return NS_OK; } + + rv = CacheMatch(aConn, cacheId, aRequest, aParams, aFoundResponseOut, + aSavedResponseOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; + } + + // Otherwise we need to get a list of all the cache IDs in this namespace. + + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT cache_id FROM storage WHERE namespace=:namespace ORDER BY rowid;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + AutoTArray<CacheId, 32> cacheIdList; + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + CacheId cacheId = INVALID_CACHE_ID; + rv = state->GetInt64(0, &cacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + cacheIdList.AppendElement(cacheId); + } + + // Now try to find a match in each cache in order + for (uint32_t i = 0; i < cacheIdList.Length(); ++i) { + rv = CacheMatch(aConn, cacheIdList[i], aRequest, aParams, aFoundResponseOut, + aSavedResponseOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (*aFoundResponseOut) { + aSavedResponseOut->mCacheId = cacheIdList[i]; + return rv; + } + } + + return NS_OK; +} + +nsresult +StorageGetCacheId(mozIStorageConnection* aConn, Namespace aNamespace, + const nsAString& aKey, bool* aFoundCacheOut, + CacheId* aCacheIdOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aFoundCacheOut); + MOZ_DIAGNOSTIC_ASSERT(aCacheIdOut); + + *aFoundCacheOut = false; + + // 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* query = "SELECT cache_id FROM storage " + "WHERE namespace=:namespace AND %s " + "ORDER BY rowid;"; + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = CreateAndBindKeyStatement(aConn, query, aKey, + getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!hasMoreData) { + return rv; + } + + rv = state->GetInt64(0, aCacheIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + *aFoundCacheOut = true; + return rv; +} + +nsresult +StoragePutCache(mozIStorageConnection* aConn, Namespace aNamespace, + const nsAString& aKey, CacheId aCacheId) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO storage (namespace, key, cache_id) " + "VALUES (:namespace, :key, :cache_id);" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindStringAsBlobByName(NS_LITERAL_CSTRING("key"), aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +StorageForgetCache(mozIStorageConnection* aConn, Namespace aNamespace, + const nsAString& aKey) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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 *query = "DELETE FROM storage WHERE namespace=:namespace AND %s;"; + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = CreateAndBindKeyStatement(aConn, query, aKey, + getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +StorageGetKeys(mozIStorageConnection* aConn, Namespace aNamespace, + nsTArray<nsString>& aKeysOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT key FROM storage WHERE namespace=:namespace ORDER BY rowid;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("namespace"), aNamespace); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + nsAutoString key; + rv = state->GetBlobAsString(0, key); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aKeysOut.AppendElement(key); + } + + return rv; +} + +namespace { + +nsresult +QueryAll(mozIStorageConnection* aConn, CacheId aCacheId, + nsTArray<EntryId>& aEntryIdListOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT id FROM entries WHERE cache_id=:cache_id ORDER BY id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + EntryId entryId = INT32_MAX; + rv = state->GetInt32(0, &entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aEntryIdListOut.AppendElement(entryId); + } + + return rv; +} + +nsresult +QueryCache(mozIStorageConnection* aConn, CacheId aCacheId, + const CacheRequest& aRequest, + const CacheQueryParams& aParams, + nsTArray<EntryId>& aEntryIdListOut, + uint32_t aMaxResults) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aMaxResults > 0); + + if (!aParams.ignoreMethod() && !aRequest.method().LowerCaseEqualsLiteral("get") + && !aRequest.method().LowerCaseEqualsLiteral("head")) + { + return NS_OK; + } + + 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' " + "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;"); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(query, getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsCOMPtr<nsICryptoHash> crypto = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString urlWithoutQueryHash; + rv = HashCString(crypto, aRequest.urlWithoutQuery(), urlWithoutQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("url_no_query_hash"), + urlWithoutQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!aParams.ignoreSearch()) { + nsAutoCString urlQueryHash; + rv = HashCString(crypto, aRequest.urlQuery(), urlQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("url_query_hash"), + urlQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("url_no_query"), + aRequest.urlWithoutQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!aParams.ignoreSearch()) { + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("url_query"), + aRequest.urlQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + // no invalid EntryId, init to least likely real value + EntryId entryId = INT32_MAX; + rv = state->GetInt32(0, &entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t varyCount; + rv = state->GetInt32(1, &varyCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!aParams.ignoreVary() && varyCount > 0) { + bool matchedByVary = false; + rv = MatchByVaryHeader(aConn, aRequest, entryId, &matchedByVary); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (!matchedByVary) { + continue; + } + } + + aEntryIdListOut.AppendElement(entryId); + + if (aEntryIdListOut.Length() == aMaxResults) { + return NS_OK; + } + } + + return rv; +} + +nsresult +MatchByVaryHeader(mozIStorageConnection* aConn, + const CacheRequest& aRequest, + EntryId entryId, bool* aSuccessOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + *aSuccessOut = false; + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT value FROM response_headers " + "WHERE name='vary' AND entry_id=:entry_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + AutoTArray<nsCString, 8> varyValues; + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + nsAutoCString value; + rv = state->GetUTF8String(0, value); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + varyValues.AppendElement(value); + } + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Should not have called this function if this was not the case + MOZ_DIAGNOSTIC_ASSERT(!varyValues.IsEmpty()); + + state->Reset(); + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name, value FROM request_headers " + "WHERE entry_id=:entry_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + RefPtr<InternalHeaders> cachedHeaders = + new InternalHeaders(HeadersGuardEnum::None); + + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + nsAutoCString name; + nsAutoCString value; + rv = state->GetUTF8String(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->GetUTF8String(1, value); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + ErrorResult errorResult; + + cachedHeaders->Append(name, value, errorResult); + if (errorResult.Failed()) { return errorResult.StealNSResult(); } + } + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + RefPtr<InternalHeaders> queryHeaders = + TypeUtils::ToInternalHeaders(aRequest.headers()); + + // Assume the vary headers match until we find a conflict + bool varyHeadersMatch = true; + + for (uint32_t i = 0; i < varyValues.Length(); ++i) { + // Extract the header names inside the Vary header value. + nsAutoCString varyValue(varyValues[i]); + char* rawBuffer = varyValue.BeginWriting(); + char* token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer); + bool bailOut = false; + for (; token; + token = nsCRT::strtok(rawBuffer, NS_HTTP_HEADER_SEPS, &rawBuffer)) { + nsDependentCString header(token); + 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; + } + } + + *aSuccessOut = varyHeadersMatch; + return rv; +} + +nsresult +DeleteEntries(mozIStorageConnection* aConn, + const nsTArray<EntryId>& aEntryIdList, + nsTArray<nsID>& aDeletedBodyIdListOut, + nsTArray<IdCount>& aDeletedSecurityIdListOut, + uint32_t aPos, int32_t aLen) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + if (aEntryIdList.IsEmpty()) { + return NS_OK; + } + + MOZ_DIAGNOSTIC_ASSERT(aPos < aEntryIdList.Length()); + + if (aLen < 0) { + aLen = aEntryIdList.Length() - aPos; + } + + // Sqlite limits the number of entries allowed for an IN clause, + // so split up larger operations. + if (aLen > kMaxEntriesPerStatement) { + uint32_t curPos = aPos; + int32_t remaining = aLen; + while (remaining > 0) { + int32_t max = kMaxEntriesPerStatement; + int32_t curLen = std::min(max, remaining); + nsresult rv = DeleteEntries(aConn, aEntryIdList, aDeletedBodyIdListOut, + aDeletedSecurityIdListOut, curPos, curLen); + if (NS_FAILED(rv)) { return rv; } + + curPos += curLen; + remaining -= curLen; + } + return NS_OK; + } + + nsCOMPtr<mozIStorageStatement> state; + nsAutoCString query( + "SELECT request_body_id, response_body_id, response_security_info_id " + "FROM entries WHERE id IN (" + ); + AppendListParamsToQuery(query, aEntryIdList, aPos, aLen); + query.AppendLiteral(")"); + + nsresult rv = aConn->CreateStatement(query, getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = BindListParamsToQuery(state, aEntryIdList, aPos, aLen); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + // extract 0 to 2 nsID structs per row + for (uint32_t i = 0; i < 2; ++i) { + bool isNull = false; + + rv = state->GetIsNull(i, &isNull); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!isNull) { + nsID id; + rv = ExtractId(state, i, &id); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aDeletedBodyIdListOut.AppendElement(id); + } + } + + // and then a possible third entry for the security id + bool isNull = false; + rv = state->GetIsNull(2, &isNull); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!isNull) { + int32_t securityId = -1; + rv = state->GetInt32(2, &securityId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // First try to increment the count for this ID if we're already + // seen it + bool found = false; + for (uint32_t i = 0; i < aDeletedSecurityIdListOut.Length(); ++i) { + if (aDeletedSecurityIdListOut[i].mId == securityId) { + found = true; + aDeletedSecurityIdListOut[i].mCount += 1; + break; + } + } + + // Otherwise add a new entry for this ID with a count of 1 + if (!found) { + aDeletedSecurityIdListOut.AppendElement(IdCount(securityId)); + } + } + } + + // Dependent records removed via ON DELETE CASCADE + + query = NS_LITERAL_CSTRING( + "DELETE FROM entries WHERE id IN (" + ); + AppendListParamsToQuery(query, aEntryIdList, aPos, aLen); + query.AppendLiteral(")"); + + rv = aConn->CreateStatement(query, getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = BindListParamsToQuery(state, aEntryIdList, aPos, aLen); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +InsertSecurityInfo(mozIStorageConnection* aConn, nsICryptoHash* aCrypto, + const nsACString& aData, int32_t *aIdOut) +{ + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aCrypto); + MOZ_DIAGNOSTIC_ASSERT(aIdOut); + MOZ_DIAGNOSTIC_ASSERT(!aData.IsEmpty()); + + // 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. + nsAutoCString hash; + nsresult rv = HashCString(aCrypto, aData, hash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // 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.) + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + // 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;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("hash"), hash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("data"), aData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // This security info blob is already in the database + if (hasMoreData) { + // get the existing security blob id to return + rv = state->GetInt32(0, aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t refcount = -1; + rv = state->GetInt32(1, &refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // But first, update the refcount in the database. + refcount += 1; + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE security_info SET refcount=:refcount WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("refcount"), refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), *aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; + } + + // This is a new security info blob. Create a new row in the security table + // with an initial refcount of 1. + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO security_info (hash, data, refcount) VALUES (:hash, :data, 1);" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("hash"), hash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName(NS_LITERAL_CSTRING("data"), aData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT last_insert_rowid()" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->GetInt32(0, aIdOut); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; +} + +nsresult +DeleteSecurityInfo(mozIStorageConnection* aConn, int32_t aId, int32_t aCount) +{ + // First, we need to determine the current refcount for this security blob. + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT refcount FROM security_info WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t refcount = -1; + rv = state->GetInt32(0, &refcount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + 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) { + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "DELETE FROM security_info WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; + } + + // Otherwise update the refcount in the table to reflect the reduced + // number of references to the security blob. + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE security_info SET refcount=:refcount WHERE id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("refcount"), newCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return NS_OK; +} + +nsresult +DeleteSecurityInfoList(mozIStorageConnection* aConn, + const nsTArray<IdCount>& aDeletedStorageIdList) +{ + for (uint32_t i = 0; i < aDeletedStorageIdList.Length(); ++i) { + nsresult rv = DeleteSecurityInfo(aConn, aDeletedStorageIdList[i].mId, + aDeletedStorageIdList[i].mCount); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + 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()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + nsresult rv = NS_OK; + + nsCOMPtr<nsICryptoHash> crypto = + do_CreateInstance(NS_CRYPTO_HASH_CONTRACTID, &rv); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t securityId = -1; + if (!aResponse.channelInfo().securityInfo().IsEmpty()) { + rv = InsertSecurityInfo(aConn, crypto, + aResponse.channelInfo().securityInfo(), + &securityId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "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, " + "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, " + ":cache_id " + ");" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("request_method"), + aRequest.method()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("request_url_no_query"), + aRequest.urlWithoutQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString urlWithoutQueryHash; + rv = HashCString(crypto, aRequest.urlWithoutQuery(), urlWithoutQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringAsBlobByName( + NS_LITERAL_CSTRING("request_url_no_query_hash"), urlWithoutQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("request_url_query"), + aRequest.urlQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString urlQueryHash; + rv = HashCString(crypto, aRequest.urlQuery(), urlQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->BindUTF8StringAsBlobByName( + NS_LITERAL_CSTRING("request_url_query_hash"), urlQueryHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("request_url_fragment"), + aRequest.urlFragment()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindStringByName(NS_LITERAL_CSTRING("request_referrer"), + aRequest.referrer()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_referrer_policy"), + static_cast<int32_t>(aRequest.referrerPolicy())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_headers_guard"), + static_cast<int32_t>(aRequest.headersGuard())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_mode"), + static_cast<int32_t>(aRequest.mode())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_credentials"), + static_cast<int32_t>(aRequest.credentials())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_contentpolicytype"), + static_cast<int32_t>(aRequest.contentPolicyType())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_cache"), + static_cast<int32_t>(aRequest.requestCache())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("request_redirect"), + static_cast<int32_t>(aRequest.requestRedirect())); + + rv = state->BindStringByName(NS_LITERAL_CSTRING("request_integrity"), + aRequest.integrity()); + + rv = BindId(state, NS_LITERAL_CSTRING("request_body_id"), aRequestBodyId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_type"), + static_cast<int32_t>(aResponse.type())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_status"), + aResponse.status()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("response_status_text"), + aResponse.statusText()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_headers_guard"), + static_cast<int32_t>(aResponse.headersGuard())); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = BindId(state, NS_LITERAL_CSTRING("response_body_id"), aResponseBodyId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (aResponse.channelInfo().securityInfo().IsEmpty()) { + rv = state->BindNullByName(NS_LITERAL_CSTRING("response_security_info_id")); + } else { + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("response_security_info_id"), + securityId); + } + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString serializedInfo; + // We only allow content serviceworkers right now. + if (aResponse.principalInfo().type() == mozilla::ipc::OptionalPrincipalInfo::TPrincipalInfo) { + const mozilla::ipc::PrincipalInfo& principalInfo = + aResponse.principalInfo().get_PrincipalInfo(); + 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); + } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("response_principal_info"), + serializedInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("cache_id"), aCacheId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT last_insert_rowid()" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t entryId; + rv = state->GetInt32(0, &entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO request_headers (" + "name, " + "value, " + "entry_id " + ") VALUES (:name, :value, :entry_id)" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + const nsTArray<HeadersEntry>& requestHeaders = aRequest.headers(); + for (uint32_t i = 0; i < requestHeaders.Length(); ++i) { + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + requestHeaders[i].name()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), + requestHeaders[i].value()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO response_headers (" + "name, " + "value, " + "entry_id " + ") VALUES (:name, :value, :entry_id)" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + const nsTArray<HeadersEntry>& responseHeaders = aResponse.headers(); + for (uint32_t i = 0; i < responseHeaders.Length(); ++i) { + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("name"), + responseHeaders[i].name()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("value"), + responseHeaders[i].value()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "INSERT INTO response_url_list (" + "url, " + "entry_id " + ") VALUES (:url, :entry_id)" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + const nsTArray<nsCString>& responseUrlList = aResponse.urlList(); + for (uint32_t i = 0; i < responseUrlList.Length(); ++i) { + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("url"), + responseUrlList[i]); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt64ByName(NS_LITERAL_CSTRING("entry_id"), entryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + return rv; +} + +nsresult +ReadResponse(mozIStorageConnection* aConn, EntryId aEntryId, + SavedResponse* aSavedResponseOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aSavedResponseOut); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "entries.response_type, " + "entries.response_status, " + "entries.response_status_text, " + "entries.response_headers_guard, " + "entries.response_body_id, " + "entries.response_principal_info, " + "security_info.data " + "FROM entries " + "LEFT OUTER JOIN security_info " + "ON entries.response_security_info_id=security_info.id " + "WHERE entries.id=:id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aEntryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t type; + rv = state->GetInt32(0, &type); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedResponseOut->mValue.type() = static_cast<ResponseType>(type); + + int32_t status; + rv = state->GetInt32(1, &status); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedResponseOut->mValue.status() = status; + + rv = state->GetUTF8String(2, aSavedResponseOut->mValue.statusText()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t guard; + rv = state->GetInt32(3, &guard); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedResponseOut->mValue.headersGuard() = + static_cast<HeadersGuardEnum>(guard); + + bool nullBody = false; + rv = state->GetIsNull(4, &nullBody); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedResponseOut->mHasBodyId = !nullBody; + + if (aSavedResponseOut->mHasBodyId) { + rv = ExtractId(state, 4, &aSavedResponseOut->mBodyId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + nsAutoCString serializedInfo; + rv = state->GetUTF8String(5, serializedInfo); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aSavedResponseOut->mValue.principalInfo() = void_t(); + if (!serializedInfo.IsEmpty()) { + nsAutoCString specNoSuffix; + PrincipalOriginAttributes attrs; + if (!attrs.PopulateFromOrigin(serializedInfo, specNoSuffix)) { + NS_WARNING("Something went wrong parsing a serialized principal!"); + return NS_ERROR_FAILURE; + } + + aSavedResponseOut->mValue.principalInfo() = + mozilla::ipc::ContentPrincipalInfo(attrs, void_t(), specNoSuffix); + } + + rv = state->GetBlobAsUTF8String(6, aSavedResponseOut->mValue.channelInfo().securityInfo()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "name, " + "value " + "FROM response_headers " + "WHERE entry_id=:entry_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), aEntryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + HeadersEntry header; + + rv = state->GetUTF8String(0, header.name()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->GetUTF8String(1, header.value()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aSavedResponseOut->mValue.headers().AppendElement(header); + } + + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "url " + "FROM response_url_list " + "WHERE entry_id=:entry_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), aEntryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + nsCString url; + + rv = state->GetUTF8String(0, url); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aSavedResponseOut->mValue.urlList().AppendElement(url); + } + + return rv; +} + +nsresult +ReadRequest(mozIStorageConnection* aConn, EntryId aEntryId, + SavedRequest* aSavedRequestOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aSavedRequestOut); + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "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;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("id"), aEntryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->GetUTF8String(0, aSavedRequestOut->mValue.method()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->GetUTF8String(1, aSavedRequestOut->mValue.urlWithoutQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->GetUTF8String(2, aSavedRequestOut->mValue.urlQuery()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->GetUTF8String(3, aSavedRequestOut->mValue.urlFragment()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + rv = state->GetString(4, aSavedRequestOut->mValue.referrer()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t referrerPolicy; + rv = state->GetInt32(5, &referrerPolicy); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.referrerPolicy() = + static_cast<ReferrerPolicy>(referrerPolicy); + int32_t guard; + rv = state->GetInt32(6, &guard); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.headersGuard() = + static_cast<HeadersGuardEnum>(guard); + int32_t mode; + rv = state->GetInt32(7, &mode); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.mode() = static_cast<RequestMode>(mode); + int32_t credentials; + rv = state->GetInt32(8, &credentials); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.credentials() = + static_cast<RequestCredentials>(credentials); + int32_t requestContentPolicyType; + rv = state->GetInt32(9, &requestContentPolicyType); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.contentPolicyType() = + static_cast<nsContentPolicyType>(requestContentPolicyType); + int32_t requestCache; + rv = state->GetInt32(10, &requestCache); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.requestCache() = + static_cast<RequestCache>(requestCache); + int32_t requestRedirect; + rv = state->GetInt32(11, &requestRedirect); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mValue.requestRedirect() = + static_cast<RequestRedirect>(requestRedirect); + rv = state->GetString(12, aSavedRequestOut->mValue.integrity()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + bool nullBody = false; + rv = state->GetIsNull(13, &nullBody); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + aSavedRequestOut->mHasBodyId = !nullBody; + if (aSavedRequestOut->mHasBodyId) { + rv = ExtractId(state, 13, &aSavedRequestOut->mBodyId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT " + "name, " + "value " + "FROM request_headers " + "WHERE entry_id=:entry_id;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindInt32ByName(NS_LITERAL_CSTRING("entry_id"), aEntryId); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + HeadersEntry header; + + rv = state->GetUTF8String(0, header.name()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->GetUTF8String(1, header.value()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aSavedRequestOut->mValue.headers().AppendElement(header); + } + + return rv; +} + +void +AppendListParamsToQuery(nsACString& aQuery, + const nsTArray<EntryId>& aEntryIdList, + uint32_t aPos, int32_t aLen) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT((aPos + aLen) <= aEntryIdList.Length()); + for (int32_t i = aPos; i < aLen; ++i) { + if (i == 0) { + aQuery.AppendLiteral("?"); + } else { + aQuery.AppendLiteral(",?"); + } + } +} + +nsresult +BindListParamsToQuery(mozIStorageStatement* aState, + const nsTArray<EntryId>& aEntryIdList, + uint32_t aPos, int32_t aLen) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT((aPos + aLen) <= aEntryIdList.Length()); + for (int32_t i = aPos; i < aLen; ++i) { + nsresult rv = aState->BindInt32ByIndex(i, aEntryIdList[i]); + NS_ENSURE_SUCCESS(rv, rv); + } + return NS_OK; +} + +nsresult +BindId(mozIStorageStatement* aState, const nsACString& aName, const nsID* aId) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aState); + nsresult rv; + + if (!aId) { + rv = aState->BindNullByName(aName); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + return rv; + } + + char idBuf[NSID_LENGTH]; + aId->ToProvidedString(idBuf); + rv = aState->BindUTF8StringByName(aName, nsDependentCString(idBuf)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +ExtractId(mozIStorageStatement* aState, uint32_t aPos, nsID* aIdOut) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aState); + MOZ_DIAGNOSTIC_ASSERT(aIdOut); + + nsAutoCString idString; + nsresult rv = aState->GetUTF8String(aPos, idString); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool success = aIdOut->Parse(idString.get()); + if (NS_WARN_IF(!success)) { return NS_ERROR_UNEXPECTED; } + + return rv; +} + +nsresult +CreateAndBindKeyStatement(mozIStorageConnection* aConn, + const char* aQueryFormat, + const nsAString& aKey, + mozIStorageStatement** aStateOut) +{ + MOZ_DIAGNOSTIC_ASSERT(aConn); + MOZ_DIAGNOSTIC_ASSERT(aQueryFormat); + MOZ_DIAGNOSTIC_ASSERT(aStateOut); + + // 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. + const char* constraint = nullptr; + if (aKey.IsEmpty()) { + constraint = "key IS NULL"; + } else { + constraint = "key=:key"; + } + + nsPrintfCString query(aQueryFormat, constraint); + + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(query, getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (!aKey.IsEmpty()) { + rv = state->BindStringAsBlobByName(NS_LITERAL_CSTRING("key"), aKey); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + } + + state.forget(aStateOut); + + return rv; +} + +nsresult +HashCString(nsICryptoHash* aCrypto, const nsACString& aIn, nsACString& aOut) +{ + MOZ_DIAGNOSTIC_ASSERT(aCrypto); + + nsresult rv = aCrypto->Init(nsICryptoHash::SHA1); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aCrypto->Update(reinterpret_cast<const uint8_t*>(aIn.BeginReading()), + aIn.Length()); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString fullHash; + rv = aCrypto->Finish(false /* based64 result */, fullHash); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aOut = Substring(fullHash, 0, 8); + return rv; +} + +} // namespace + +nsresult +IncrementalVacuum(mozIStorageConnection* aConn) +{ + // Determine how much free space is in the database. + nsCOMPtr<mozIStorageStatement> state; + nsresult rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA freelist_count;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + int32_t freePages = 0; + rv = state->GetInt32(0, &freePages); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // 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. + int32_t pagesToRelease = freePages - kMaxFreePages; + + rv = aConn->ExecuteSimpleSQL(nsPrintfCString( + "PRAGMA incremental_vacuum(%d);", pagesToRelease + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Verify that our incremental vacuum actually did something +#ifdef DEBUG + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA freelist_count;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + freePages = 0; + rv = state->GetInt32(0, &freePages); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + MOZ_ASSERT(freePages <= kMaxFreePages); +#endif + + return NS_OK; +} + +namespace { + +#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) +{ + int32_t schemaVersion; + nsresult rv = aConn->GetSchemaVersion(&schemaVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + if (NS_WARN_IF(schemaVersion != kLatestSchemaVersion)) { + return 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. + Expect expect[] = { + 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 + }; + const uint32_t expectLength = sizeof(expect) / sizeof(Expect); + + // Read the schema from the sqlite_master table and compare. + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "SELECT name, type, sql FROM sqlite_master;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + while (NS_SUCCEEDED(state->ExecuteStep(&hasMoreData)) && hasMoreData) { + nsAutoCString name; + rv = state->GetUTF8String(0, name); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString type; + rv = state->GetUTF8String(1, type); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsAutoCString sql; + rv = state->GetUTF8String(2, sql); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool foundMatch = false; + for (uint32_t i = 0; i < expectLength; ++i) { + if (name == expect[i].mName) { + if (type != expect[i].mType) { + NS_WARNING(nsPrintfCString("Unexpected type for Cache schema entry %s", + name.get()).get()); + return NS_ERROR_FAILURE; + } + + if (!expect[i].mIgnoreSql && sql != expect[i].mSql) { + NS_WARNING(nsPrintfCString("Unexpected SQL for Cache schema entry %s", + name.get()).get()); + return 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 NS_ERROR_FAILURE; + } + } +#endif + + return rv; +} + +// ----- +// Schema migration code +// ----- + +typedef nsresult (*MigrationFunc)(mozIStorageConnection*, bool&); +struct Migration +{ + constexpr Migration(int32_t aFromVersion, MigrationFunc aFunc) + : mFromVersion(aFromVersion) + , mFunc(aFunc) + { } + 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); +// Configure migration functions to run for the given starting version. +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), +}; +uint32_t sMigrationListLength = sizeof(sMigrationList) / sizeof(Migration); +nsresult +RewriteEntriesSchema(mozIStorageConnection* aConn) +{ + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA writable_schema = ON" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "UPDATE sqlite_master SET sql=:sql WHERE name='entries'" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->BindUTF8StringByName(NS_LITERAL_CSTRING("sql"), + nsDependentCString(kTableEntries)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = state->Execute(); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "PRAGMA writable_schema = OFF" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +Migrate(mozIStorageConnection* aConn) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + int32_t currentVersion = 0; + nsresult rv = aConn->GetSchemaVersion(¤tVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + 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 (uint32_t i = 0; i < sMigrationListLength; ++i) { + if (sMigrationList[i].mFromVersion == currentVersion) { + bool shouldRewrite = false; + rv = sMigrationList[i].mFunc(aConn, shouldRewrite); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (shouldRewrite) { + rewriteSchema = true; + } + break; + } + } + +#if defined(DEBUG) || !defined(RELEASE_OR_BETA) + int32_t lastVersion = currentVersion; +#endif + rv = aConn->GetSchemaVersion(¤tVersion); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + MOZ_DIAGNOSTIC_ASSERT(currentVersion > lastVersion); + } + + MOZ_DIAGNOSTIC_ASSERT(currentVersion == kLatestSchemaVersion); + + 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()); + MOZ_DIAGNOSTIC_ASSERT(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. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE entries " + "ADD COLUMN request_redirect INTEGER NOT NULL DEFAULT 0" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(16); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aRewriteSchema = true; + + return rv; +} + +nsresult +MigrateFrom16To17(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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" + ")" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Copy all of the data to the newly created table. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Remove the old table. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE entries;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Rename new_entries to entries. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE new_entries RENAME to entries;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Now, recreate our indices. + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kIndexEntriesRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Revalidate the foreign key constraints, and ensure that there are no + // violations. + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA foreign_key_check;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (NS_WARN_IF(hasMoreData)) { return NS_ERROR_FAILURE; } + + rv = aConn->SetSchemaVersion(17); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +MigrateFrom17To18(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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!"); + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE entries SET request_cache = 0 " + "WHERE request_cache = 5;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(18); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult +MigrateFrom18To19(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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(nsIContentPolicy::TYPE_REFRESH) == 8 && + int(RequestMode::Navigate) == 3, + "This is where the numbers below come from!"); + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "UPDATE entries SET request_mode = 3 " + "WHERE request_contentpolicytype IN (6, 7, 28, 29, 8);" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(19); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + return rv; +} + +nsresult MigrateFrom19To20(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE entries " + "ADD COLUMN request_referrer_policy INTEGER NOT NULL DEFAULT 2" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(20); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aRewriteSchema = true; + + return rv; +} + +nsresult MigrateFrom20To21(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // 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. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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" + ")" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Create a response_url_list table with the new fields as of version 21. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE response_url_list (" + "url TEXT NOT NULL, " + "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE" + ")" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Copy all of the data to the newly created entries table. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "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;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Copy reponse_url to the newly created response_url_list table. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "INSERT INTO response_url_list (" + "url, " + "entry_id " + ") SELECT " + "response_url, " + "id " + "FROM entries;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Remove the old table. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "DROP TABLE entries;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Rename new_entries to entries. + rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE new_entries RENAME to entries;" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Now, recreate our indices. + rv = aConn->ExecuteSimpleSQL(nsDependentCString(kIndexEntriesRequest)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + // Revalidate the foreign key constraints, and ensure that there are no + // violations. + nsCOMPtr<mozIStorageStatement> state; + rv = aConn->CreateStatement(NS_LITERAL_CSTRING( + "PRAGMA foreign_key_check;" + ), getter_AddRefs(state)); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + bool hasMoreData = false; + rv = state->ExecuteStep(&hasMoreData); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + if (NS_WARN_IF(hasMoreData)) { return NS_ERROR_FAILURE; } + + rv = aConn->SetSchemaVersion(21); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aRewriteSchema = true; + + return rv; +} + +nsresult MigrateFrom21To22(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // Add the request_integrity column. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE entries " + "ADD COLUMN request_integrity TEXT NULL" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(22); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aRewriteSchema = true; + + return rv; +} + +nsresult MigrateFrom22To23(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // The only change between 22 and 23 was a different snappy compression + // format, but it's backwards-compatible. + nsresult rv = aConn->SetSchemaVersion(23); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + return rv; +} +nsresult MigrateFrom23To24(mozIStorageConnection* aConn, bool& aRewriteSchema) +{ + MOZ_ASSERT(!NS_IsMainThread()); + MOZ_DIAGNOSTIC_ASSERT(aConn); + + // Add the request_url_fragment column. + nsresult rv = aConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "ALTER TABLE entries " + "ADD COLUMN request_url_fragment TEXT NOT NULL DEFAULT ''" + )); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + rv = aConn->SetSchemaVersion(24); + if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } + + aRewriteSchema = true; + + return rv; +} + +} // anonymous namespace +} // namespace db +} // namespace cache +} // namespace dom +} // namespace mozilla |