1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "mozilla/dom/cache/DBSchema.h"
8
9 #include "ipc/IPCMessageUtils.h"
10 #include "mozilla/BasePrincipal.h"
11 #include "mozilla/dom/HeadersBinding.h"
12 #include "mozilla/dom/InternalHeaders.h"
13 #include "mozilla/dom/InternalResponse.h"
14 #include "mozilla/dom/RequestBinding.h"
15 #include "mozilla/dom/ResponseBinding.h"
16 #include "mozilla/dom/cache/CacheCommon.h"
17 #include "mozilla/dom/cache/CacheTypes.h"
18 #include "mozilla/dom/cache/SavedTypes.h"
19 #include "mozilla/dom/cache/Types.h"
20 #include "mozilla/dom/cache/TypeUtils.h"
21 #include "mozilla/net/MozURL.h"
22 #include "mozilla/ResultExtensions.h"
23 #include "mozilla/StaticPrefs_extensions.h"
24 #include "mozilla/Telemetry.h"
25 #include "mozIStorageConnection.h"
26 #include "mozIStorageStatement.h"
27 #include "mozStorageHelper.h"
28 #include "nsCharSeparatedTokenizer.h"
29 #include "nsCOMPtr.h"
30 #include "nsComponentManagerUtils.h"
31 #include "nsHttp.h"
32 #include "nsIContentPolicy.h"
33 #include "nsICryptoHash.h"
34 #include "nsNetCID.h"
35 #include "nsPrintfCString.h"
36 #include "nsTArray.h"
37
38 namespace mozilla::dom::cache::db {
39 const int32_t kFirstShippedSchemaVersion = 15;
40 namespace {
41 // ## Firefox 57 Cache API v25/v26/v27 Schema Hack Info
42 // ### Overview
43 // In Firefox 57 we introduced Cache API schema version 26 and Quota Manager
44 // schema v3 to support tracking padding for opaque responses. Unfortunately,
45 // Firefox 57 is a big release that may potentially result in users downgrading
46 // to Firefox 56 due to 57 retiring add-ons. These schema changes have the
47 // unfortunate side-effect of causing QuotaManager and all its clients to break
48 // if the user downgrades to 56. In order to avoid making a bad situation
49 // worse, we're now retrofitting 57 so that Firefox 56 won't freak out.
50 //
51 // ### Implementation
52 // We're introducing a new schema version 27 that uses an on-disk schema version
53 // of v25. We differentiate v25 from v27 by the presence of the column added
54 // by v26. This translates to:
55 // - v25: on-disk schema=25, no "response_padding_size" column in table
56 // "entries".
57 // - v26: on-disk schema=26, yes "response_padding_size" column in table
58 // "entries".
59 // - v27: on-disk schema=25, yes "response_padding_size" column in table
60 // "entries".
61 //
62 // ### Fallout
63 // Firefox 57 is happy because it sees schema 27 and everything is as it
64 // expects.
65 //
66 // Firefox 56 non-DEBUG build is fine/happy, but DEBUG builds will not be.
67 // - Our QuotaClient will invoke `NS_WARNING("Unknown Cache file found!");`
68 // at QuotaManager init time. This is harmless but annoying and potentially
69 // misleading.
70 // - The DEBUG-only Validate() call will error out whenever an attempt is made
71 // to open a DOM Cache database because it will notice the schema is broken
72 // and there is no attempt at recovery.
73 //
74 const int32_t kHackyDowngradeSchemaVersion = 25;
75 const int32_t kHackyPaddingSizePresentVersion = 27;
76 //
77 // Update this whenever the DB schema is changed.
78 const int32_t kLatestSchemaVersion = 28;
79 // ---------
80 // The following constants define the SQL schema. These are defined in the
81 // same order the SQL should be executed in CreateOrMigrateSchema(). They are
82 // broken out as constants for convenient use in validation and migration.
83 // ---------
84 // The caches table is the single source of truth about what Cache
85 // objects exist for the origin. The contents of the Cache are stored
86 // in the entries table that references back to caches.
87 //
88 // The caches table is also referenced from storage. Rows in storage
89 // represent named Cache objects. There are cases, however, where
90 // a Cache can still exist, but not be in a named Storage. For example,
91 // when content is still using the Cache after CacheStorage::Delete()
92 // has been run.
93 //
94 // For now, the caches table mainly exists for data integrity with
95 // foreign keys, but could be expanded to contain additional cache object
96 // information.
97 //
98 // AUTOINCREMENT is necessary to prevent CacheId values from being reused.
99 const char kTableCaches[] =
100 "CREATE TABLE caches ("
101 "id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT "
102 ")";
103
104 // Security blobs are quite large and duplicated for every Response from
105 // the same https origin. This table is used to de-duplicate this data.
106 const char kTableSecurityInfo[] =
107 "CREATE TABLE security_info ("
108 "id INTEGER NOT NULL PRIMARY KEY, "
109 "hash BLOB NOT NULL, " // first 8-bytes of the sha1 hash of data column
110 "data BLOB NOT NULL, " // full security info data, usually a few KB
111 "refcount INTEGER NOT NULL"
112 ")";
113
114 // Index the smaller hash value instead of the large security data blob.
115 const char kIndexSecurityInfoHash[] =
116 "CREATE INDEX security_info_hash_index ON security_info (hash)";
117
118 const char kTableEntries[] =
119 "CREATE TABLE entries ("
120 "id INTEGER NOT NULL PRIMARY KEY, "
121 "request_method TEXT NOT NULL, "
122 "request_url_no_query TEXT NOT NULL, "
123 "request_url_no_query_hash BLOB NOT NULL, " // first 8-bytes of sha1 hash
124 "request_url_query TEXT NOT NULL, "
125 "request_url_query_hash BLOB NOT NULL, " // first 8-bytes of sha1 hash
126 "request_referrer TEXT NOT NULL, "
127 "request_headers_guard INTEGER NOT NULL, "
128 "request_mode INTEGER NOT NULL, "
129 "request_credentials INTEGER NOT NULL, "
130 "request_contentpolicytype INTEGER NOT NULL, "
131 "request_cache INTEGER NOT NULL, "
132 "request_body_id TEXT NULL, "
133 "response_type INTEGER NOT NULL, "
134 "response_status INTEGER NOT NULL, "
135 "response_status_text TEXT NOT NULL, "
136 "response_headers_guard INTEGER NOT NULL, "
137 "response_body_id TEXT NULL, "
138 "response_security_info_id INTEGER NULL REFERENCES security_info(id), "
139 "response_principal_info TEXT NOT NULL, "
140 "cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
141 "request_redirect INTEGER NOT NULL, "
142 "request_referrer_policy INTEGER NOT NULL, "
143 "request_integrity TEXT NOT NULL, "
144 "request_url_fragment TEXT NOT NULL, "
145 "response_padding_size INTEGER NULL "
146 // New columns must be added at the end of table to migrate and
147 // validate properly.
148 ")";
149 // Create an index to support the QueryCache() matching algorithm. This
150 // needs to quickly find entries in a given Cache that match the request
151 // URL. The url query is separated in order to support the ignoreSearch
152 // option. Finally, we index hashes of the URL values instead of the
153 // actual strings to avoid excessive disk bloat. The index will duplicate
154 // the contents of the columsn in the index. The hash index will prune
155 // the vast majority of values from the query result so that normal
156 // scanning only has to be done on a few values to find an exact URL match.
157 const char kIndexEntriesRequest[] =
158 "CREATE INDEX entries_request_match_index "
159 "ON entries (cache_id, request_url_no_query_hash, "
160 "request_url_query_hash)";
161
162 const char kTableRequestHeaders[] =
163 "CREATE TABLE request_headers ("
164 "name TEXT NOT NULL, "
165 "value TEXT NOT NULL, "
166 "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
167 ")";
168
169 const char kTableResponseHeaders[] =
170 "CREATE TABLE response_headers ("
171 "name TEXT NOT NULL, "
172 "value TEXT NOT NULL, "
173 "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
174 ")";
175
176 // We need an index on response_headers, but not on request_headers,
177 // because we quickly need to determine if a VARY header is present.
178 const char kIndexResponseHeadersName[] =
179 "CREATE INDEX response_headers_name_index "
180 "ON response_headers (name)";
181
182 const char kTableResponseUrlList[] =
183 "CREATE TABLE response_url_list ("
184 "url TEXT NOT NULL, "
185 "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
186 ")";
187
188 // NOTE: key allows NULL below since that is how "" is represented
189 // in a BLOB column. We use BLOB to avoid encoding issues
190 // with storing DOMStrings.
191 const char kTableStorage[] =
192 "CREATE TABLE storage ("
193 "namespace INTEGER NOT NULL, "
194 "key BLOB NULL, "
195 "cache_id INTEGER NOT NULL REFERENCES caches(id), "
196 "PRIMARY KEY(namespace, key) "
197 ")";
198
199 // ---------
200 // End schema definition
201 // ---------
202
203 const int32_t kMaxEntriesPerStatement = 255;
204
205 const uint32_t kPageSize = 4 * 1024;
206
207 // Grow the database in chunks to reduce fragmentation
208 const uint32_t kGrowthSize = 32 * 1024;
209 const uint32_t kGrowthPages = kGrowthSize / kPageSize;
210 static_assert(kGrowthSize % kPageSize == 0,
211 "Growth size must be multiple of page size");
212
213 // Only release free pages when we have more than this limit
214 const int32_t kMaxFreePages = kGrowthPages;
215
216 // Limit WAL journal to a reasonable size
217 const uint32_t kWalAutoCheckpointSize = 512 * 1024;
218 const uint32_t kWalAutoCheckpointPages = kWalAutoCheckpointSize / kPageSize;
219 static_assert(kWalAutoCheckpointSize % kPageSize == 0,
220 "WAL checkpoint size must be multiple of page size");
221
222 } // namespace
223
224 // If any of the static_asserts below fail, it means that you have changed
225 // the corresponding WebIDL enum in a way that may be incompatible with the
226 // existing data stored in the DOM Cache. You would need to update the Cache
227 // database schema accordingly and adjust the failing static_assert.
228 static_assert(int(HeadersGuardEnum::None) == 0 &&
229 int(HeadersGuardEnum::Request) == 1 &&
230 int(HeadersGuardEnum::Request_no_cors) == 2 &&
231 int(HeadersGuardEnum::Response) == 3 &&
232 int(HeadersGuardEnum::Immutable) == 4 &&
233 HeadersGuardEnumValues::Count == 5,
234 "HeadersGuardEnum values are as expected");
235 static_assert(int(ReferrerPolicy::_empty) == 0 &&
236 int(ReferrerPolicy::No_referrer) == 1 &&
237 int(ReferrerPolicy::No_referrer_when_downgrade) == 2 &&
238 int(ReferrerPolicy::Origin) == 3 &&
239 int(ReferrerPolicy::Origin_when_cross_origin) == 4 &&
240 int(ReferrerPolicy::Unsafe_url) == 5 &&
241 int(ReferrerPolicy::Same_origin) == 6 &&
242 int(ReferrerPolicy::Strict_origin) == 7 &&
243 int(ReferrerPolicy::Strict_origin_when_cross_origin) == 8 &&
244 ReferrerPolicyValues::Count == 9,
245 "ReferrerPolicy values are as expected");
246 static_assert(int(RequestMode::Same_origin) == 0 &&
247 int(RequestMode::No_cors) == 1 &&
248 int(RequestMode::Cors) == 2 &&
249 int(RequestMode::Navigate) == 3 &&
250 RequestModeValues::Count == 4,
251 "RequestMode values are as expected");
252 static_assert(int(RequestCredentials::Omit) == 0 &&
253 int(RequestCredentials::Same_origin) == 1 &&
254 int(RequestCredentials::Include) == 2 &&
255 RequestCredentialsValues::Count == 3,
256 "RequestCredentials values are as expected");
257 static_assert(int(RequestCache::Default) == 0 &&
258 int(RequestCache::No_store) == 1 &&
259 int(RequestCache::Reload) == 2 &&
260 int(RequestCache::No_cache) == 3 &&
261 int(RequestCache::Force_cache) == 4 &&
262 int(RequestCache::Only_if_cached) == 5 &&
263 RequestCacheValues::Count == 6,
264 "RequestCache values are as expected");
265 static_assert(int(RequestRedirect::Follow) == 0 &&
266 int(RequestRedirect::Error) == 1 &&
267 int(RequestRedirect::Manual) == 2 &&
268 RequestRedirectValues::Count == 3,
269 "RequestRedirect values are as expected");
270 static_assert(int(ResponseType::Basic) == 0 && int(ResponseType::Cors) == 1 &&
271 int(ResponseType::Default) == 2 &&
272 int(ResponseType::Error) == 3 &&
273 int(ResponseType::Opaque) == 4 &&
274 int(ResponseType::Opaqueredirect) == 5 &&
275 ResponseTypeValues::Count == 6,
276 "ResponseType values are as expected");
277
278 // If the static_asserts below fails, it means that you have changed the
279 // Namespace enum in a way that may be incompatible with the existing data
280 // stored in the DOM Cache. You would need to update the Cache database schema
281 // accordingly and adjust the failing static_assert.
282 static_assert(DEFAULT_NAMESPACE == 0 && CHROME_ONLY_NAMESPACE == 1 &&
283 NUMBER_OF_NAMESPACES == 2,
284 "Namespace values are as expected");
285
286 // If the static_asserts below fails, it means that you have changed the
287 // nsContentPolicy enum in a way that may be incompatible with the existing data
288 // stored in the DOM Cache. You would need to update the Cache database schema
289 // accordingly and adjust the failing static_assert.
290 static_assert(
291 nsIContentPolicy::TYPE_INVALID == 0 && nsIContentPolicy::TYPE_OTHER == 1 &&
292 nsIContentPolicy::TYPE_SCRIPT == 2 &&
293 nsIContentPolicy::TYPE_IMAGE == 3 &&
294 nsIContentPolicy::TYPE_STYLESHEET == 4 &&
295 nsIContentPolicy::TYPE_OBJECT == 5 &&
296 nsIContentPolicy::TYPE_DOCUMENT == 6 &&
297 nsIContentPolicy::TYPE_SUBDOCUMENT == 7 &&
298 nsIContentPolicy::TYPE_PING == 10 &&
299 nsIContentPolicy::TYPE_XMLHTTPREQUEST == 11 &&
300 nsIContentPolicy::TYPE_OBJECT_SUBREQUEST == 12 &&
301 nsIContentPolicy::TYPE_DTD == 13 && nsIContentPolicy::TYPE_FONT == 14 &&
302 nsIContentPolicy::TYPE_MEDIA == 15 &&
303 nsIContentPolicy::TYPE_WEBSOCKET == 16 &&
304 nsIContentPolicy::TYPE_CSP_REPORT == 17 &&
305 nsIContentPolicy::TYPE_XSLT == 18 &&
306 nsIContentPolicy::TYPE_BEACON == 19 &&
307 nsIContentPolicy::TYPE_FETCH == 20 &&
308 nsIContentPolicy::TYPE_IMAGESET == 21 &&
309 nsIContentPolicy::TYPE_WEB_MANIFEST == 22 &&
310 nsIContentPolicy::TYPE_INTERNAL_SCRIPT == 23 &&
311 nsIContentPolicy::TYPE_INTERNAL_WORKER == 24 &&
312 nsIContentPolicy::TYPE_INTERNAL_SHARED_WORKER == 25 &&
313 nsIContentPolicy::TYPE_INTERNAL_EMBED == 26 &&
314 nsIContentPolicy::TYPE_INTERNAL_OBJECT == 27 &&
315 nsIContentPolicy::TYPE_INTERNAL_FRAME == 28 &&
316 nsIContentPolicy::TYPE_INTERNAL_IFRAME == 29 &&
317 nsIContentPolicy::TYPE_INTERNAL_AUDIO == 30 &&
318 nsIContentPolicy::TYPE_INTERNAL_VIDEO == 31 &&
319 nsIContentPolicy::TYPE_INTERNAL_TRACK == 32 &&
320 nsIContentPolicy::TYPE_INTERNAL_XMLHTTPREQUEST == 33 &&
321 nsIContentPolicy::TYPE_INTERNAL_EVENTSOURCE == 34 &&
322 nsIContentPolicy::TYPE_INTERNAL_SERVICE_WORKER == 35 &&
323 nsIContentPolicy::TYPE_INTERNAL_SCRIPT_PRELOAD == 36 &&
324 nsIContentPolicy::TYPE_INTERNAL_IMAGE == 37 &&
325 nsIContentPolicy::TYPE_INTERNAL_IMAGE_PRELOAD == 38 &&
326 nsIContentPolicy::TYPE_INTERNAL_STYLESHEET == 39 &&
327 nsIContentPolicy::TYPE_INTERNAL_STYLESHEET_PRELOAD == 40 &&
328 nsIContentPolicy::TYPE_INTERNAL_IMAGE_FAVICON == 41 &&
329 nsIContentPolicy::TYPE_INTERNAL_WORKER_IMPORT_SCRIPTS == 42 &&
330 nsIContentPolicy::TYPE_SAVEAS_DOWNLOAD == 43 &&
331 nsIContentPolicy::TYPE_SPECULATIVE == 44 &&
332 nsIContentPolicy::TYPE_INTERNAL_MODULE == 45 &&
333 nsIContentPolicy::TYPE_INTERNAL_MODULE_PRELOAD == 46 &&
334 nsIContentPolicy::TYPE_INTERNAL_DTD == 47 &&
335 nsIContentPolicy::TYPE_INTERNAL_FORCE_ALLOWED_DTD == 48 &&
336 nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET == 49 &&
337 nsIContentPolicy::TYPE_INTERNAL_PAINTWORKLET == 50 &&
338 nsIContentPolicy::TYPE_INTERNAL_FONT_PRELOAD == 51 &&
339 nsIContentPolicy::TYPE_INTERNAL_CHROMEUTILS_COMPILED_SCRIPT == 52 &&
340 nsIContentPolicy::TYPE_INTERNAL_FRAME_MESSAGEMANAGER_SCRIPT == 53 &&
341 nsIContentPolicy::TYPE_INTERNAL_FETCH_PRELOAD == 54 &&
342 nsIContentPolicy::TYPE_UA_FONT == 55,
343 "nsContentPolicyType values are as expected");
344
345 namespace {
346
347 typedef int32_t EntryId;
348
349 struct IdCount {
IdCountmozilla::dom::cache::db::__anon339f3aee0211::IdCount350 explicit IdCount(int32_t aId) : mId(aId), mCount(1) {}
351 int32_t mId;
352 int32_t mCount;
353 };
354
355 using EntryIds = AutoTArray<EntryId, 256>;
356
357 static Result<EntryIds, nsresult> QueryAll(mozIStorageConnection& aConn,
358 CacheId aCacheId);
359 static Result<EntryIds, nsresult> QueryCache(mozIStorageConnection& aConn,
360 CacheId aCacheId,
361 const CacheRequest& aRequest,
362 const CacheQueryParams& aParams,
363 uint32_t aMaxResults = UINT32_MAX);
364 static Result<bool, nsresult> MatchByVaryHeader(mozIStorageConnection& aConn,
365 const CacheRequest& aRequest,
366 EntryId entryId);
367 // Returns a success tuple containing the deleted body ids, deleted security ids
368 // and deleted padding size.
369 static Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>,
370 nsresult>
371 DeleteEntries(mozIStorageConnection& aConn,
372 const nsTArray<EntryId>& aEntryIdList);
373 static Result<int32_t, nsresult> InsertSecurityInfo(
374 mozIStorageConnection& aConn, nsICryptoHash& aCrypto,
375 const nsACString& aData);
376 static nsresult DeleteSecurityInfo(mozIStorageConnection& aConn, int32_t aId,
377 int32_t aCount);
378 static nsresult DeleteSecurityInfoList(
379 mozIStorageConnection& aConn,
380 const nsTArray<IdCount>& aDeletedStorageIdList);
381 static nsresult InsertEntry(mozIStorageConnection& aConn, CacheId aCacheId,
382 const CacheRequest& aRequest,
383 const nsID* aRequestBodyId,
384 const CacheResponse& aResponse,
385 const nsID* aResponseBodyId);
386 static Result<SavedResponse, nsresult> ReadResponse(
387 mozIStorageConnection& aConn, EntryId aEntryId);
388 static Result<SavedRequest, nsresult> ReadRequest(mozIStorageConnection& aConn,
389 EntryId aEntryId);
390
391 static void AppendListParamsToQuery(nsACString& aQuery,
392 const nsTArray<EntryId>& aEntryIdList,
393 uint32_t aPos, int32_t aLen);
394 static nsresult BindListParamsToQuery(mozIStorageStatement& aState,
395 const nsTArray<EntryId>& aEntryIdList,
396 uint32_t aPos, int32_t aLen);
397 static nsresult BindId(mozIStorageStatement& aState, const nsACString& aName,
398 const nsID* aId);
399 static Result<nsID, nsresult> ExtractId(mozIStorageStatement& aState,
400 uint32_t aPos);
401 static Result<NotNull<nsCOMPtr<mozIStorageStatement>>, nsresult>
402 CreateAndBindKeyStatement(mozIStorageConnection& aConn,
403 const char* aQueryFormat, const nsAString& aKey);
404 static Result<nsAutoCString, nsresult> HashCString(nsICryptoHash& aCrypto,
405 const nsACString& aIn);
406 Result<int32_t, nsresult> GetEffectiveSchemaVersion(
407 mozIStorageConnection& aConn);
408 nsresult Validate(mozIStorageConnection& aConn);
409 nsresult Migrate(mozIStorageConnection& aConn);
410 } // namespace
411
412 class MOZ_RAII AutoDisableForeignKeyChecking {
413 public:
AutoDisableForeignKeyChecking(mozIStorageConnection * aConn)414 explicit AutoDisableForeignKeyChecking(mozIStorageConnection* aConn)
415 : mConn(aConn), mForeignKeyCheckingDisabled(false) {
416 QM_TRY_INSPECT(const auto& state,
417 quota::CreateAndExecuteSingleStepStatement(
418 *mConn, "PRAGMA foreign_keys;"_ns),
419 QM_VOID);
420
421 QM_TRY_INSPECT(const int32_t& mode,
422 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0), QM_VOID);
423
424 if (mode) {
425 QM_WARNONLY_TRY(
426 ToResult(mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = OFF;"_ns))
427 .andThen([this](const auto) -> Result<Ok, nsresult> {
428 mForeignKeyCheckingDisabled = true;
429 return Ok{};
430 }));
431 }
432 }
433
~AutoDisableForeignKeyChecking()434 ~AutoDisableForeignKeyChecking() {
435 if (mForeignKeyCheckingDisabled) {
436 QM_WARNONLY_TRY(mConn->ExecuteSimpleSQL("PRAGMA foreign_keys = ON;"_ns));
437 }
438 }
439
440 private:
441 nsCOMPtr<mozIStorageConnection> mConn;
442 bool mForeignKeyCheckingDisabled;
443 };
444
IntegrityCheck(mozIStorageConnection & aConn)445 nsresult IntegrityCheck(mozIStorageConnection& aConn) {
446 // CACHE_INTEGRITY_CHECK_COUNT is designed to report at most once.
447 static bool reported = false;
448 if (reported) {
449 return NS_OK;
450 }
451
452 QM_TRY_INSPECT(const auto& stmt,
453 quota::CreateAndExecuteSingleStepStatement(
454 aConn,
455 "SELECT COUNT(*) FROM pragma_integrity_check() "
456 "WHERE integrity_check != 'ok';"_ns));
457
458 QM_TRY_INSPECT(const auto& result,
459 MOZ_TO_RESULT_INVOKE_TYPED(nsString, *stmt, GetString, 0));
460
461 nsresult rv;
462 const uint32_t count = result.ToInteger(&rv);
463 QM_TRY(OkIf(NS_SUCCEEDED(rv)), rv);
464
465 Telemetry::ScalarSet(Telemetry::ScalarID::CACHE_INTEGRITY_CHECK_COUNT, count);
466
467 reported = true;
468
469 return NS_OK;
470 }
471
CreateOrMigrateSchema(mozIStorageConnection & aConn)472 nsresult CreateOrMigrateSchema(mozIStorageConnection& aConn) {
473 MOZ_ASSERT(!NS_IsMainThread());
474
475 QM_TRY_UNWRAP(int32_t schemaVersion, GetEffectiveSchemaVersion(aConn));
476
477 if (schemaVersion == kLatestSchemaVersion) {
478 // We already have the correct schema version. Validate it matches
479 // our expected schema and then proceed.
480 QM_TRY(Validate(aConn));
481
482 return NS_OK;
483 }
484
485 // Turn off checking foreign keys before starting a transaction, and restore
486 // it once we're done.
487 AutoDisableForeignKeyChecking restoreForeignKeyChecking(&aConn);
488 mozStorageTransaction trans(&aConn, false,
489 mozIStorageConnection::TRANSACTION_IMMEDIATE);
490
491 QM_TRY(trans.Start());
492
493 const bool migrating = schemaVersion != 0;
494
495 if (migrating) {
496 // A schema exists, but its not the current version. Attempt to
497 // migrate it to our new schema.
498 QM_TRY(Migrate(aConn));
499 } else {
500 // There is no schema installed. Create the database from scratch.
501 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableCaches)));
502 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableSecurityInfo)));
503 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexSecurityInfoHash)));
504 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableEntries)));
505 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexEntriesRequest)));
506 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableRequestHeaders)));
507 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableResponseHeaders)));
508 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexResponseHeadersName)));
509 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableResponseUrlList)));
510 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kTableStorage)));
511 QM_TRY(aConn.SetSchemaVersion(kLatestSchemaVersion));
512 QM_TRY_UNWRAP(schemaVersion, GetEffectiveSchemaVersion(aConn));
513 }
514
515 QM_TRY(Validate(aConn));
516 QM_TRY(trans.Commit());
517
518 if (migrating) {
519 // Migrations happen infrequently and reflect a chance in DB structure.
520 // This is a good time to rebuild the database. It also helps catch
521 // if a new migration is incorrect by fast failing on the corruption.
522 // Unfortunately, this must be performed outside of the transaction.
523
524 QM_TRY(aConn.ExecuteSimpleSQL("VACUUM"_ns), QM_PROPAGATE,
525 ([&aConn](const nsresult rv) {
526 if (rv == NS_ERROR_STORAGE_CONSTRAINT) {
527 QM_WARNONLY_TRY(IntegrityCheck(aConn));
528 }
529 }));
530 }
531
532 return NS_OK;
533 }
534
InitializeConnection(mozIStorageConnection & aConn)535 nsresult InitializeConnection(mozIStorageConnection& aConn) {
536 MOZ_ASSERT(!NS_IsMainThread());
537
538 // This function needs to perform per-connection initialization tasks that
539 // need to happen regardless of the schema.
540
541 // Note, the default encoding of UTF-8 is preferred. mozStorage does all
542 // the work necessary to convert UTF-16 nsString values for us. We don't
543 // need ordering and the binary equality operations are correct. So, do
544 // NOT set PRAGMA encoding to UTF-16.
545
546 QM_TRY(aConn.ExecuteSimpleSQL(nsPrintfCString(
547 // Use a smaller page size to improve perf/footprint; default is too large
548 "PRAGMA page_size = %u; "
549 // Enable auto_vacuum; this must happen after page_size and before WAL
550 "PRAGMA auto_vacuum = INCREMENTAL; "
551 "PRAGMA foreign_keys = ON; ",
552 kPageSize)));
553
554 // Limit fragmentation by growing the database by many pages at once.
555 QM_TRY(QM_OR_ELSE_WARN_IF(
556 // Expression.
557 ToResult(aConn.SetGrowthIncrement(kGrowthSize, ""_ns)),
558 // Predicate.
559 IsSpecificError<NS_ERROR_FILE_TOO_BIG>,
560 // Fallback.
561 ErrToDefaultOk<>));
562
563 // Enable WAL journaling. This must be performed in a separate transaction
564 // after changing the page_size and enabling auto_vacuum.
565 QM_TRY(aConn.ExecuteSimpleSQL(nsPrintfCString(
566 // WAL journal can grow to given number of *pages*
567 "PRAGMA wal_autocheckpoint = %u; "
568 // Always truncate the journal back to given number of *bytes*
569 "PRAGMA journal_size_limit = %u; "
570 // WAL must be enabled at the end to allow page size to be changed, etc.
571 "PRAGMA journal_mode = WAL; ",
572 kWalAutoCheckpointPages, kWalAutoCheckpointSize)));
573
574 // Verify that we successfully set the vacuum mode to incremental. It
575 // is very easy to put the database in a state where the auto_vacuum
576 // pragma above fails silently.
577 #ifdef DEBUG
578 {
579 QM_TRY_INSPECT(const auto& state,
580 quota::CreateAndExecuteSingleStepStatement(
581 aConn, "PRAGMA auto_vacuum;"_ns));
582
583 QM_TRY_INSPECT(const int32_t& mode,
584 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
585
586 // integer value 2 is incremental mode
587 QM_TRY(OkIf(mode == 2), NS_ERROR_UNEXPECTED);
588 }
589 #endif
590
591 return NS_OK;
592 }
593
CreateCacheId(mozIStorageConnection & aConn)594 Result<CacheId, nsresult> CreateCacheId(mozIStorageConnection& aConn) {
595 MOZ_ASSERT(!NS_IsMainThread());
596
597 QM_TRY(aConn.ExecuteSimpleSQL("INSERT INTO caches DEFAULT VALUES;"_ns));
598
599 QM_TRY_INSPECT(const auto& state,
600 quota::CreateAndExecuteSingleStepStatement<
601 quota::SingleStepResult::ReturnNullIfNoResult>(
602 aConn, "SELECT last_insert_rowid()"_ns));
603
604 QM_TRY(OkIf(state), Err(NS_ERROR_UNEXPECTED));
605
606 QM_TRY_INSPECT(const CacheId& id, MOZ_TO_RESULT_INVOKE(state, GetInt64, 0));
607
608 return id;
609 }
610
DeleteCacheId(mozIStorageConnection & aConn,CacheId aCacheId)611 Result<DeletionInfo, nsresult> DeleteCacheId(mozIStorageConnection& aConn,
612 CacheId aCacheId) {
613 MOZ_ASSERT(!NS_IsMainThread());
614
615 // Delete the bodies explicitly as we need to read out the body IDs
616 // anyway. These body IDs must be deleted one-by-one as content may
617 // still be referencing them invidivually.
618 QM_TRY_INSPECT(const auto& matches, QueryAll(aConn, aCacheId));
619
620 // XXX only deletedBodyIdList needs to be non-const
621 QM_TRY_UNWRAP(
622 (auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
623 DeleteEntries(aConn, matches));
624
625 QM_TRY(DeleteSecurityInfoList(aConn, deletedSecurityIdList));
626
627 // Delete the remainder of the cache using cascade semantics.
628 QM_TRY_INSPECT(const auto& state,
629 MOZ_TO_RESULT_INVOKE_TYPED(
630 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
631 "DELETE FROM caches WHERE id=:id;"_ns));
632
633 QM_TRY(state->BindInt64ByName("id"_ns, aCacheId));
634
635 QM_TRY(state->Execute());
636
637 return DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize};
638 }
639
FindOrphanedCacheIds(mozIStorageConnection & aConn)640 Result<AutoTArray<CacheId, 8>, nsresult> FindOrphanedCacheIds(
641 mozIStorageConnection& aConn) {
642 QM_TRY_INSPECT(const auto& state,
643 MOZ_TO_RESULT_INVOKE_TYPED(
644 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
645 "SELECT id FROM caches "
646 "WHERE id NOT IN (SELECT cache_id from storage);"_ns));
647
648 QM_TRY_RETURN(
649 (quota::CollectElementsWhileHasResultTyped<AutoTArray<CacheId, 8>>(
650 *state, [](auto& stmt) {
651 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 0));
652 })));
653 }
654
FindOverallPaddingSize(mozIStorageConnection & aConn)655 Result<int64_t, nsresult> FindOverallPaddingSize(mozIStorageConnection& aConn) {
656 QM_TRY_INSPECT(const auto& state,
657 MOZ_TO_RESULT_INVOKE_TYPED(
658 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
659 "SELECT response_padding_size FROM entries "
660 "WHERE response_padding_size IS NOT NULL;"_ns));
661
662 int64_t overallPaddingSize = 0;
663
664 QM_TRY(quota::CollectWhileHasResult(
665 *state, [&overallPaddingSize](auto& stmt) -> Result<Ok, nsresult> {
666 QM_TRY_INSPECT(const int64_t& padding_size,
667 MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 0));
668
669 MOZ_DIAGNOSTIC_ASSERT(padding_size >= 0);
670 MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - padding_size >= overallPaddingSize);
671 overallPaddingSize += padding_size;
672
673 return Ok{};
674 }));
675
676 return overallPaddingSize;
677 }
678
GetKnownBodyIds(mozIStorageConnection & aConn)679 Result<nsTArray<nsID>, nsresult> GetKnownBodyIds(mozIStorageConnection& aConn) {
680 MOZ_ASSERT(!NS_IsMainThread());
681
682 QM_TRY_INSPECT(
683 const auto& state,
684 MOZ_TO_RESULT_INVOKE_TYPED(
685 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
686 "SELECT request_body_id, response_body_id FROM entries;"_ns));
687
688 AutoTArray<nsID, 64> idList;
689
690 QM_TRY(quota::CollectWhileHasResult(
691 *state, [&idList](auto& stmt) -> Result<Ok, nsresult> {
692 // extract 0 to 2 nsID structs per row
693 for (uint32_t i = 0; i < 2; ++i) {
694 QM_TRY_INSPECT(const bool& isNull,
695 MOZ_TO_RESULT_INVOKE(stmt, GetIsNull, i));
696
697 if (!isNull) {
698 QM_TRY_INSPECT(const auto& id, ExtractId(stmt, i));
699
700 idList.AppendElement(id);
701 }
702 }
703
704 return Ok{};
705 }));
706
707 return std::move(idList);
708 }
709
CacheMatch(mozIStorageConnection & aConn,CacheId aCacheId,const CacheRequest & aRequest,const CacheQueryParams & aParams)710 Result<Maybe<SavedResponse>, nsresult> CacheMatch(
711 mozIStorageConnection& aConn, CacheId aCacheId,
712 const CacheRequest& aRequest, const CacheQueryParams& aParams) {
713 MOZ_ASSERT(!NS_IsMainThread());
714
715 QM_TRY_INSPECT(const auto& matches,
716 QueryCache(aConn, aCacheId, aRequest, aParams, 1));
717
718 if (matches.IsEmpty()) {
719 return Maybe<SavedResponse>();
720 }
721
722 QM_TRY_UNWRAP(auto response, ReadResponse(aConn, matches[0]));
723
724 response.mCacheId = aCacheId;
725
726 return Some(std::move(response));
727 }
728
CacheMatchAll(mozIStorageConnection & aConn,CacheId aCacheId,const Maybe<CacheRequest> & aMaybeRequest,const CacheQueryParams & aParams)729 Result<nsTArray<SavedResponse>, nsresult> CacheMatchAll(
730 mozIStorageConnection& aConn, CacheId aCacheId,
731 const Maybe<CacheRequest>& aMaybeRequest, const CacheQueryParams& aParams) {
732 MOZ_ASSERT(!NS_IsMainThread());
733
734 QM_TRY_INSPECT(
735 const auto& matches, ([&aConn, aCacheId, &aMaybeRequest, &aParams] {
736 if (aMaybeRequest.isNothing()) {
737 QM_TRY_RETURN(QueryAll(aConn, aCacheId));
738 }
739
740 QM_TRY_RETURN(
741 QueryCache(aConn, aCacheId, aMaybeRequest.ref(), aParams));
742 }()));
743
744 // TODO: replace this with a bulk load using SQL IN clause (bug 1110458)
745 QM_TRY_RETURN(TransformIntoNewArrayAbortOnErr(
746 matches,
747 [&aConn, aCacheId](const auto match) -> Result<SavedResponse, nsresult> {
748 QM_TRY_UNWRAP(auto savedResponse, ReadResponse(aConn, match));
749
750 savedResponse.mCacheId = aCacheId;
751 return savedResponse;
752 },
753 fallible));
754 }
755
CachePut(mozIStorageConnection & aConn,CacheId aCacheId,const CacheRequest & aRequest,const nsID * aRequestBodyId,const CacheResponse & aResponse,const nsID * aResponseBodyId)756 Result<DeletionInfo, nsresult> CachePut(mozIStorageConnection& aConn,
757 CacheId aCacheId,
758 const CacheRequest& aRequest,
759 const nsID* aRequestBodyId,
760 const CacheResponse& aResponse,
761 const nsID* aResponseBodyId) {
762 MOZ_ASSERT(!NS_IsMainThread());
763
764 QM_TRY_INSPECT(
765 const auto& matches,
766 QueryCache(aConn, aCacheId, aRequest,
767 CacheQueryParams(false, false, false, false, u""_ns)));
768
769 // XXX only deletedBodyIdList needs to be non-const
770 QM_TRY_UNWRAP(
771 (auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
772 DeleteEntries(aConn, matches));
773
774 QM_TRY(InsertEntry(aConn, aCacheId, aRequest, aRequestBodyId, aResponse,
775 aResponseBodyId));
776
777 // Delete the security values after doing the insert to avoid churning
778 // the security table when its not necessary.
779 QM_TRY(DeleteSecurityInfoList(aConn, deletedSecurityIdList));
780
781 return DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize};
782 }
783
CacheDelete(mozIStorageConnection & aConn,CacheId aCacheId,const CacheRequest & aRequest,const CacheQueryParams & aParams)784 Result<Maybe<DeletionInfo>, nsresult> CacheDelete(
785 mozIStorageConnection& aConn, CacheId aCacheId,
786 const CacheRequest& aRequest, const CacheQueryParams& aParams) {
787 MOZ_ASSERT(!NS_IsMainThread());
788
789 QM_TRY_INSPECT(const auto& matches,
790 QueryCache(aConn, aCacheId, aRequest, aParams));
791
792 if (matches.IsEmpty()) {
793 return Maybe<DeletionInfo>();
794 }
795
796 // XXX only deletedBodyIdList needs to be non-const
797 QM_TRY_UNWRAP(
798 (auto [deletedBodyIdList, deletedSecurityIdList, deletedPaddingSize]),
799 DeleteEntries(aConn, matches));
800
801 QM_TRY(DeleteSecurityInfoList(aConn, deletedSecurityIdList));
802
803 return Some(DeletionInfo{std::move(deletedBodyIdList), deletedPaddingSize});
804 }
805
CacheKeys(mozIStorageConnection & aConn,CacheId aCacheId,const Maybe<CacheRequest> & aMaybeRequest,const CacheQueryParams & aParams)806 Result<nsTArray<SavedRequest>, nsresult> CacheKeys(
807 mozIStorageConnection& aConn, CacheId aCacheId,
808 const Maybe<CacheRequest>& aMaybeRequest, const CacheQueryParams& aParams) {
809 MOZ_ASSERT(!NS_IsMainThread());
810
811 QM_TRY_INSPECT(
812 const auto& matches, ([&aConn, aCacheId, &aMaybeRequest, &aParams] {
813 if (aMaybeRequest.isNothing()) {
814 QM_TRY_RETURN(QueryAll(aConn, aCacheId));
815 }
816
817 QM_TRY_RETURN(
818 QueryCache(aConn, aCacheId, aMaybeRequest.ref(), aParams));
819 }()));
820
821 // TODO: replace this with a bulk load using SQL IN clause (bug 1110458)
822 QM_TRY_RETURN(TransformIntoNewArrayAbortOnErr(
823 matches,
824 [&aConn, aCacheId](const auto match) -> Result<SavedRequest, nsresult> {
825 QM_TRY_UNWRAP(auto savedRequest, ReadRequest(aConn, match));
826
827 savedRequest.mCacheId = aCacheId;
828 return savedRequest;
829 },
830 fallible));
831 }
832
StorageMatch(mozIStorageConnection & aConn,Namespace aNamespace,const CacheRequest & aRequest,const CacheQueryParams & aParams)833 Result<Maybe<SavedResponse>, nsresult> StorageMatch(
834 mozIStorageConnection& aConn, Namespace aNamespace,
835 const CacheRequest& aRequest, const CacheQueryParams& aParams) {
836 MOZ_ASSERT(!NS_IsMainThread());
837
838 // If we are given a cache to check, then simply find its cache ID
839 // and perform the match.
840 if (aParams.cacheNameSet()) {
841 QM_TRY_INSPECT(const auto& maybeCacheId,
842 StorageGetCacheId(aConn, aNamespace, aParams.cacheName()));
843 if (maybeCacheId.isNothing()) {
844 return Maybe<SavedResponse>();
845 }
846
847 return CacheMatch(aConn, maybeCacheId.ref(), aRequest, aParams);
848 }
849
850 // Otherwise we need to get a list of all the cache IDs in this namespace.
851
852 QM_TRY_INSPECT(const auto& state,
853 MOZ_TO_RESULT_INVOKE_TYPED(
854 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
855 "SELECT cache_id FROM storage WHERE "
856 "namespace=:namespace ORDER BY rowid;"_ns));
857
858 QM_TRY(state->BindInt32ByName("namespace"_ns, aNamespace));
859
860 QM_TRY_INSPECT(
861 const auto& cacheIdList,
862 (quota::CollectElementsWhileHasResultTyped<AutoTArray<CacheId, 32>>(
863 *state, [](auto& stmt) {
864 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 0));
865 })));
866
867 // Now try to find a match in each cache in order
868 for (const auto cacheId : cacheIdList) {
869 QM_TRY_UNWRAP(auto matchedResponse,
870 CacheMatch(aConn, cacheId, aRequest, aParams));
871
872 if (matchedResponse.isSome()) {
873 return matchedResponse;
874 }
875 }
876
877 return Maybe<SavedResponse>();
878 }
879
StorageGetCacheId(mozIStorageConnection & aConn,Namespace aNamespace,const nsAString & aKey)880 Result<Maybe<CacheId>, nsresult> StorageGetCacheId(mozIStorageConnection& aConn,
881 Namespace aNamespace,
882 const nsAString& aKey) {
883 MOZ_ASSERT(!NS_IsMainThread());
884
885 // How we constrain the key column depends on the value of our key. Use
886 // a format string for the query and let CreateAndBindKeyStatement() fill
887 // it in for us.
888 const char* const query =
889 "SELECT cache_id FROM storage "
890 "WHERE namespace=:namespace AND %s "
891 "ORDER BY rowid;";
892
893 QM_TRY_INSPECT(const auto& state,
894 CreateAndBindKeyStatement(aConn, query, aKey));
895
896 QM_TRY(state->BindInt32ByName("namespace"_ns, aNamespace));
897
898 QM_TRY_INSPECT(const bool& hasMoreData,
899 MOZ_TO_RESULT_INVOKE(*state, ExecuteStep));
900
901 if (!hasMoreData) {
902 return Maybe<CacheId>();
903 }
904
905 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*state, GetInt64, 0).map(Some<CacheId>));
906 }
907
StoragePutCache(mozIStorageConnection & aConn,Namespace aNamespace,const nsAString & aKey,CacheId aCacheId)908 nsresult StoragePutCache(mozIStorageConnection& aConn, Namespace aNamespace,
909 const nsAString& aKey, CacheId aCacheId) {
910 MOZ_ASSERT(!NS_IsMainThread());
911
912 QM_TRY_INSPECT(const auto& state,
913 MOZ_TO_RESULT_INVOKE_TYPED(
914 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
915 "INSERT INTO storage (namespace, key, cache_id) "
916 "VALUES (:namespace, :key, :cache_id);"_ns));
917
918 QM_TRY(state->BindInt32ByName("namespace"_ns, aNamespace));
919 QM_TRY(state->BindStringAsBlobByName("key"_ns, aKey));
920 QM_TRY(state->BindInt64ByName("cache_id"_ns, aCacheId));
921 QM_TRY(state->Execute());
922
923 return NS_OK;
924 }
925
StorageForgetCache(mozIStorageConnection & aConn,Namespace aNamespace,const nsAString & aKey)926 nsresult StorageForgetCache(mozIStorageConnection& aConn, Namespace aNamespace,
927 const nsAString& aKey) {
928 MOZ_ASSERT(!NS_IsMainThread());
929
930 // How we constrain the key column depends on the value of our key. Use
931 // a format string for the query and let CreateAndBindKeyStatement() fill
932 // it in for us.
933 const char* const query =
934 "DELETE FROM storage WHERE namespace=:namespace AND %s;";
935
936 QM_TRY_INSPECT(const auto& state,
937 CreateAndBindKeyStatement(aConn, query, aKey));
938
939 QM_TRY(state->BindInt32ByName("namespace"_ns, aNamespace));
940
941 QM_TRY(state->Execute());
942
943 return NS_OK;
944 }
945
StorageGetKeys(mozIStorageConnection & aConn,Namespace aNamespace)946 Result<nsTArray<nsString>, nsresult> StorageGetKeys(
947 mozIStorageConnection& aConn, Namespace aNamespace) {
948 MOZ_ASSERT(!NS_IsMainThread());
949
950 QM_TRY_INSPECT(
951 const auto& state,
952 MOZ_TO_RESULT_INVOKE_TYPED(
953 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
954 "SELECT key FROM storage WHERE namespace=:namespace ORDER BY rowid;"_ns));
955
956 QM_TRY(state->BindInt32ByName("namespace"_ns, aNamespace));
957
958 QM_TRY_RETURN(quota::CollectElementsWhileHasResult(*state, [](auto& stmt) {
959 QM_TRY_RETURN(
960 MOZ_TO_RESULT_INVOKE_TYPED(nsString, stmt, GetBlobAsString, 0));
961 }));
962 }
963
964 namespace {
965
QueryAll(mozIStorageConnection & aConn,CacheId aCacheId)966 Result<EntryIds, nsresult> QueryAll(mozIStorageConnection& aConn,
967 CacheId aCacheId) {
968 MOZ_ASSERT(!NS_IsMainThread());
969
970 QM_TRY_INSPECT(
971 const auto& state,
972 MOZ_TO_RESULT_INVOKE_TYPED(
973 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
974 "SELECT id FROM entries WHERE cache_id=:cache_id ORDER BY id;"_ns));
975
976 QM_TRY(state->BindInt64ByName("cache_id"_ns, aCacheId));
977
978 QM_TRY_RETURN((quota::CollectElementsWhileHasResultTyped<EntryIds>(
979 *state, [](auto& stmt) {
980 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 0));
981 })));
982 }
983
QueryCache(mozIStorageConnection & aConn,CacheId aCacheId,const CacheRequest & aRequest,const CacheQueryParams & aParams,uint32_t aMaxResults)984 Result<EntryIds, nsresult> QueryCache(mozIStorageConnection& aConn,
985 CacheId aCacheId,
986 const CacheRequest& aRequest,
987 const CacheQueryParams& aParams,
988 uint32_t aMaxResults) {
989 MOZ_ASSERT(!NS_IsMainThread());
990 MOZ_DIAGNOSTIC_ASSERT(aMaxResults > 0);
991
992 if (!aParams.ignoreMethod() &&
993 !aRequest.method().LowerCaseEqualsLiteral("get")) {
994 return Result<EntryIds, nsresult>{std::in_place};
995 }
996
997 nsAutoCString query(
998 "SELECT id, COUNT(response_headers.name) AS vary_count "
999 "FROM entries "
1000 "LEFT OUTER JOIN response_headers ON "
1001 "entries.id=response_headers.entry_id "
1002 "AND response_headers.name='vary' COLLATE NOCASE "
1003 "WHERE entries.cache_id=:cache_id "
1004 "AND entries.request_url_no_query_hash=:url_no_query_hash ");
1005
1006 if (!aParams.ignoreSearch()) {
1007 query.AppendLiteral("AND entries.request_url_query_hash=:url_query_hash ");
1008 }
1009
1010 query.AppendLiteral("AND entries.request_url_no_query=:url_no_query ");
1011
1012 if (!aParams.ignoreSearch()) {
1013 query.AppendLiteral("AND entries.request_url_query=:url_query ");
1014 }
1015
1016 query.AppendLiteral("GROUP BY entries.id ORDER BY entries.id;");
1017
1018 QM_TRY_INSPECT(const auto& state,
1019 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1020 aConn, CreateStatement, query));
1021
1022 QM_TRY(state->BindInt64ByName("cache_id"_ns, aCacheId));
1023
1024 QM_TRY_INSPECT(const auto& crypto, ToResultGet<nsCOMPtr<nsICryptoHash>>(
1025 MOZ_SELECT_OVERLOAD(do_CreateInstance),
1026 NS_CRYPTO_HASH_CONTRACTID));
1027
1028 QM_TRY_INSPECT(const auto& urlWithoutQueryHash,
1029 HashCString(*crypto, aRequest.urlWithoutQuery()));
1030
1031 QM_TRY(state->BindUTF8StringAsBlobByName("url_no_query_hash"_ns,
1032 urlWithoutQueryHash));
1033
1034 if (!aParams.ignoreSearch()) {
1035 QM_TRY_INSPECT(const auto& urlQueryHash,
1036 HashCString(*crypto, aRequest.urlQuery()));
1037
1038 QM_TRY(
1039 state->BindUTF8StringAsBlobByName("url_query_hash"_ns, urlQueryHash));
1040 }
1041
1042 QM_TRY(state->BindUTF8StringByName("url_no_query"_ns,
1043 aRequest.urlWithoutQuery()));
1044
1045 if (!aParams.ignoreSearch()) {
1046 QM_TRY(state->BindUTF8StringByName("url_query"_ns, aRequest.urlQuery()));
1047 }
1048
1049 EntryIds entryIdList;
1050
1051 QM_TRY(CollectWhile(
1052 [&state, &entryIdList, aMaxResults]() -> Result<bool, nsresult> {
1053 if (entryIdList.Length() == aMaxResults) {
1054 return false;
1055 }
1056 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(state, ExecuteStep));
1057 },
1058 [&state, &entryIdList, ignoreVary = aParams.ignoreVary(), &aConn,
1059 &aRequest]() -> Result<Ok, nsresult> {
1060 QM_TRY_INSPECT(const EntryId& entryId,
1061 MOZ_TO_RESULT_INVOKE(state, GetInt32, 0));
1062
1063 QM_TRY_INSPECT(const int32_t& varyCount,
1064 MOZ_TO_RESULT_INVOKE(state, GetInt32, 1));
1065
1066 if (!ignoreVary && varyCount > 0) {
1067 QM_TRY_INSPECT(const bool& matchedByVary,
1068 MatchByVaryHeader(aConn, aRequest, entryId));
1069 if (!matchedByVary) {
1070 return Ok{};
1071 }
1072 }
1073
1074 entryIdList.AppendElement(entryId);
1075
1076 return Ok{};
1077 }));
1078
1079 return entryIdList;
1080 }
1081
MatchByVaryHeader(mozIStorageConnection & aConn,const CacheRequest & aRequest,EntryId entryId)1082 Result<bool, nsresult> MatchByVaryHeader(mozIStorageConnection& aConn,
1083 const CacheRequest& aRequest,
1084 EntryId entryId) {
1085 MOZ_ASSERT(!NS_IsMainThread());
1086
1087 QM_TRY_INSPECT(
1088 const auto& varyValues,
1089 ([&aConn, entryId]() -> Result<AutoTArray<nsCString, 8>, nsresult> {
1090 QM_TRY_INSPECT(
1091 const auto& state,
1092 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>, aConn,
1093 CreateStatement,
1094 "SELECT value FROM response_headers "
1095 "WHERE name='vary' COLLATE NOCASE "
1096 "AND entry_id=:entry_id;"_ns));
1097
1098 QM_TRY(state->BindInt32ByName("entry_id"_ns, entryId));
1099
1100 QM_TRY_RETURN((
1101 quota::CollectElementsWhileHasResultTyped<AutoTArray<nsCString, 8>>(
1102 *state, [](auto& stmt) {
1103 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt,
1104 GetUTF8String, 0));
1105 })));
1106 }()));
1107
1108 // Should not have called this function if this was not the case
1109 MOZ_DIAGNOSTIC_ASSERT(!varyValues.IsEmpty());
1110
1111 QM_TRY_INSPECT(const auto& state,
1112 MOZ_TO_RESULT_INVOKE_TYPED(
1113 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1114 "SELECT name, value FROM request_headers "
1115 "WHERE entry_id=:entry_id;"_ns));
1116
1117 QM_TRY(state->BindInt32ByName("entry_id"_ns, entryId));
1118
1119 RefPtr<InternalHeaders> cachedHeaders =
1120 new InternalHeaders(HeadersGuardEnum::None);
1121
1122 QM_TRY(quota::CollectWhileHasResult(
1123 *state, [&cachedHeaders](auto& stmt) -> Result<Ok, nsresult> {
1124 QM_TRY_INSPECT(
1125 const auto& name,
1126 MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 0));
1127 QM_TRY_INSPECT(
1128 const auto& value,
1129 MOZ_TO_RESULT_INVOKE_TYPED(nsCString, stmt, GetUTF8String, 1));
1130
1131 ErrorResult errorResult;
1132
1133 cachedHeaders->Append(name, value, errorResult);
1134 if (errorResult.Failed()) {
1135 return Err(errorResult.StealNSResult());
1136 }
1137
1138 return Ok{};
1139 }));
1140
1141 RefPtr<InternalHeaders> queryHeaders =
1142 TypeUtils::ToInternalHeaders(aRequest.headers());
1143
1144 // Assume the vary headers match until we find a conflict
1145 bool varyHeadersMatch = true;
1146
1147 for (const auto& varyValue : varyValues) {
1148 // Extract the header names inside the Vary header value.
1149 bool bailOut = false;
1150 for (const nsACString& header :
1151 nsCCharSeparatedTokenizer(varyValue, NS_HTTP_HEADER_SEP).ToRange()) {
1152 MOZ_DIAGNOSTIC_ASSERT(!header.EqualsLiteral("*"),
1153 "We should have already caught this in "
1154 "TypeUtils::ToPCacheResponseWithoutBody()");
1155
1156 ErrorResult errorResult;
1157 nsAutoCString queryValue;
1158 queryHeaders->Get(header, queryValue, errorResult);
1159 if (errorResult.Failed()) {
1160 errorResult.SuppressException();
1161 MOZ_DIAGNOSTIC_ASSERT(queryValue.IsEmpty());
1162 }
1163
1164 nsAutoCString cachedValue;
1165 cachedHeaders->Get(header, cachedValue, errorResult);
1166 if (errorResult.Failed()) {
1167 errorResult.SuppressException();
1168 MOZ_DIAGNOSTIC_ASSERT(cachedValue.IsEmpty());
1169 }
1170
1171 if (queryValue != cachedValue) {
1172 varyHeadersMatch = false;
1173 bailOut = true;
1174 break;
1175 }
1176 }
1177
1178 if (bailOut) {
1179 break;
1180 }
1181 }
1182
1183 return varyHeadersMatch;
1184 }
1185
DeleteEntriesInternal(mozIStorageConnection & aConn,const nsTArray<EntryId> & aEntryIdList,nsTArray<nsID> & aDeletedBodyIdListOut,nsTArray<IdCount> & aDeletedSecurityIdListOut,int64_t * aDeletedPaddingSizeOut,uint32_t aPos,uint32_t aLen)1186 static nsresult DeleteEntriesInternal(
1187 mozIStorageConnection& aConn, const nsTArray<EntryId>& aEntryIdList,
1188 nsTArray<nsID>& aDeletedBodyIdListOut,
1189 nsTArray<IdCount>& aDeletedSecurityIdListOut,
1190 int64_t* aDeletedPaddingSizeOut, uint32_t aPos, uint32_t aLen) {
1191 MOZ_ASSERT(!NS_IsMainThread());
1192 MOZ_DIAGNOSTIC_ASSERT(aDeletedPaddingSizeOut);
1193
1194 if (aEntryIdList.IsEmpty()) {
1195 return NS_OK;
1196 }
1197
1198 MOZ_DIAGNOSTIC_ASSERT(aPos < aEntryIdList.Length());
1199
1200 // Sqlite limits the number of entries allowed for an IN clause,
1201 // so split up larger operations.
1202 if (aLen > kMaxEntriesPerStatement) {
1203 int64_t overallDeletedPaddingSize = 0;
1204 uint32_t curPos = aPos;
1205 int32_t remaining = aLen;
1206 while (remaining > 0) {
1207 int64_t deletedPaddingSize = 0;
1208 int32_t max = kMaxEntriesPerStatement;
1209 int32_t curLen = std::min(max, remaining);
1210 nsresult rv = DeleteEntriesInternal(
1211 aConn, aEntryIdList, aDeletedBodyIdListOut, aDeletedSecurityIdListOut,
1212 &deletedPaddingSize, curPos, curLen);
1213 if (NS_FAILED(rv)) {
1214 return rv;
1215 }
1216
1217 MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - deletedPaddingSize >=
1218 overallDeletedPaddingSize);
1219 overallDeletedPaddingSize += deletedPaddingSize;
1220 curPos += curLen;
1221 remaining -= curLen;
1222 }
1223
1224 *aDeletedPaddingSizeOut += overallDeletedPaddingSize;
1225 return NS_OK;
1226 }
1227
1228 nsAutoCString query(
1229 "SELECT "
1230 "request_body_id, "
1231 "response_body_id, "
1232 "response_security_info_id, "
1233 "response_padding_size "
1234 "FROM entries WHERE id IN (");
1235 AppendListParamsToQuery(query, aEntryIdList, aPos, aLen);
1236 query.AppendLiteral(")");
1237
1238 QM_TRY_INSPECT(const auto& state,
1239 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1240 aConn, CreateStatement, query));
1241
1242 QM_TRY(BindListParamsToQuery(*state, aEntryIdList, aPos, aLen));
1243
1244 int64_t overallPaddingSize = 0;
1245
1246 QM_TRY(quota::CollectWhileHasResult(
1247 *state,
1248 [&overallPaddingSize, &aDeletedBodyIdListOut,
1249 &aDeletedSecurityIdListOut](auto& stmt) -> Result<Ok, nsresult> {
1250 // extract 0 to 2 nsID structs per row
1251 for (uint32_t i = 0; i < 2; ++i) {
1252 QM_TRY_INSPECT(const bool& isNull,
1253 MOZ_TO_RESULT_INVOKE(stmt, GetIsNull, i));
1254
1255 if (!isNull) {
1256 QM_TRY_INSPECT(const auto& id, ExtractId(stmt, i));
1257
1258 aDeletedBodyIdListOut.AppendElement(id);
1259 }
1260 }
1261
1262 { // and then a possible third entry for the security id
1263 QM_TRY_INSPECT(const bool& isNull,
1264 MOZ_TO_RESULT_INVOKE(stmt, GetIsNull, 2));
1265
1266 if (!isNull) {
1267 QM_TRY_INSPECT(const int32_t& securityId,
1268 MOZ_TO_RESULT_INVOKE(stmt, GetInt32, 2));
1269
1270 // XXXtt: Consider using map for aDeletedSecuityIdListOut.
1271 auto foundIt =
1272 std::find_if(aDeletedSecurityIdListOut.begin(),
1273 aDeletedSecurityIdListOut.end(),
1274 [securityId](const auto& deletedSecurityId) {
1275 return deletedSecurityId.mId == securityId;
1276 });
1277
1278 if (foundIt == aDeletedSecurityIdListOut.end()) {
1279 // Add a new entry for this ID with a count of 1, if it's not in
1280 // the list
1281 aDeletedSecurityIdListOut.AppendElement(IdCount(securityId));
1282 } else {
1283 // Otherwise, increment the count for this ID
1284 foundIt->mCount += 1;
1285 }
1286 }
1287 }
1288
1289 {
1290 // It's possible to have null padding size for non-opaque response
1291 QM_TRY_INSPECT(const bool& isNull,
1292 MOZ_TO_RESULT_INVOKE(stmt, GetIsNull, 3));
1293
1294 if (!isNull) {
1295 QM_TRY_INSPECT(const int64_t& paddingSize,
1296 MOZ_TO_RESULT_INVOKE(stmt, GetInt64, 3));
1297
1298 MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
1299 MOZ_DIAGNOSTIC_ASSERT(INT64_MAX - overallPaddingSize >=
1300 paddingSize);
1301 overallPaddingSize += paddingSize;
1302 }
1303 }
1304
1305 return Ok{};
1306 }));
1307
1308 *aDeletedPaddingSizeOut = overallPaddingSize;
1309
1310 // Dependent records removed via ON DELETE CASCADE
1311
1312 query = "DELETE FROM entries WHERE id IN ("_ns;
1313 AppendListParamsToQuery(query, aEntryIdList, aPos, aLen);
1314 query.AppendLiteral(")");
1315
1316 {
1317 QM_TRY_INSPECT(const auto& state,
1318 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1319 aConn, CreateStatement, query));
1320
1321 QM_TRY(BindListParamsToQuery(*state, aEntryIdList, aPos, aLen));
1322
1323 QM_TRY(state->Execute());
1324 }
1325
1326 return NS_OK;
1327 }
1328
1329 Result<std::tuple<nsTArray<nsID>, AutoTArray<IdCount, 16>, int64_t>, nsresult>
DeleteEntries(mozIStorageConnection & aConn,const nsTArray<EntryId> & aEntryIdList)1330 DeleteEntries(mozIStorageConnection& aConn,
1331 const nsTArray<EntryId>& aEntryIdList) {
1332 auto result =
1333 std::make_tuple(nsTArray<nsID>{}, AutoTArray<IdCount, 16>{}, int64_t{0});
1334 QM_TRY(DeleteEntriesInternal(aConn, aEntryIdList, std::get<0>(result),
1335 std::get<1>(result), &std::get<2>(result), 0,
1336 aEntryIdList.Length()));
1337
1338 return result;
1339 }
1340
InsertSecurityInfo(mozIStorageConnection & aConn,nsICryptoHash & aCrypto,const nsACString & aData)1341 Result<int32_t, nsresult> InsertSecurityInfo(mozIStorageConnection& aConn,
1342 nsICryptoHash& aCrypto,
1343 const nsACString& aData) {
1344 MOZ_DIAGNOSTIC_ASSERT(!aData.IsEmpty());
1345
1346 // We want to use an index to find existing security blobs, but indexing
1347 // the full blob would be quite expensive. Instead, we index a small
1348 // hash value. Calculate this hash as the first 8 bytes of the SHA1 of
1349 // the full data.
1350 QM_TRY_INSPECT(const auto& hash, HashCString(aCrypto, aData));
1351
1352 // Next, search for an existing entry for this blob by comparing the hash
1353 // value first and then the full data. SQLite is smart enough to use
1354 // the index on the hash to search the table before doing the expensive
1355 // comparison of the large data column. (This was verified with EXPLAIN.)
1356 QM_TRY_INSPECT(
1357 const auto& selectStmt,
1358 quota::CreateAndExecuteSingleStepStatement<
1359 quota::SingleStepResult::ReturnNullIfNoResult>(
1360 aConn,
1361 // Note that hash and data are blobs, but we can use = here since the
1362 // columns are NOT NULL.
1363 "SELECT id, refcount FROM security_info WHERE hash=:hash AND "
1364 "data=:data;"_ns,
1365 [&hash, &aData](auto& state) -> Result<Ok, nsresult> {
1366 QM_TRY(state.BindUTF8StringAsBlobByName("hash"_ns, hash));
1367 QM_TRY(state.BindUTF8StringAsBlobByName("data"_ns, aData));
1368
1369 return Ok{};
1370 }));
1371
1372 // This security info blob is already in the database
1373 if (selectStmt) {
1374 // get the existing security blob id to return
1375 QM_TRY_INSPECT(const int32_t& id,
1376 MOZ_TO_RESULT_INVOKE(selectStmt, GetInt32, 0));
1377 QM_TRY_INSPECT(const int32_t& refcount,
1378 MOZ_TO_RESULT_INVOKE(selectStmt, GetInt32, 1));
1379
1380 // But first, update the refcount in the database.
1381 QM_TRY_INSPECT(
1382 const auto& state,
1383 MOZ_TO_RESULT_INVOKE_TYPED(
1384 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1385 "UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
1386
1387 QM_TRY(state->BindInt32ByName("refcount"_ns, refcount + 1));
1388 QM_TRY(state->BindInt32ByName("id"_ns, id));
1389 QM_TRY(state->Execute());
1390
1391 return id;
1392 }
1393
1394 // This is a new security info blob. Create a new row in the security table
1395 // with an initial refcount of 1.
1396 QM_TRY_INSPECT(const auto& state,
1397 MOZ_TO_RESULT_INVOKE_TYPED(
1398 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1399 "INSERT INTO security_info (hash, data, refcount) "
1400 "VALUES (:hash, :data, 1);"_ns));
1401
1402 QM_TRY(state->BindUTF8StringAsBlobByName("hash"_ns, hash));
1403 QM_TRY(state->BindUTF8StringAsBlobByName("data"_ns, aData));
1404 QM_TRY(state->Execute());
1405
1406 {
1407 QM_TRY_INSPECT(const auto& state,
1408 quota::CreateAndExecuteSingleStepStatement(
1409 aConn, "SELECT last_insert_rowid()"_ns));
1410
1411 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
1412 }
1413 }
1414
DeleteSecurityInfo(mozIStorageConnection & aConn,int32_t aId,int32_t aCount)1415 nsresult DeleteSecurityInfo(mozIStorageConnection& aConn, int32_t aId,
1416 int32_t aCount) {
1417 // First, we need to determine the current refcount for this security blob.
1418 QM_TRY_INSPECT(
1419 const int32_t& refcount, ([&aConn, aId]() -> Result<int32_t, nsresult> {
1420 QM_TRY_INSPECT(
1421 const auto& state,
1422 quota::CreateAndExecuteSingleStepStatement(
1423 aConn, "SELECT refcount FROM security_info WHERE id=:id;"_ns,
1424 [aId](auto& state) -> Result<Ok, nsresult> {
1425 QM_TRY(state.BindInt32ByName("id"_ns, aId));
1426 return Ok{};
1427 }));
1428
1429 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
1430 }()));
1431
1432 MOZ_DIAGNOSTIC_ASSERT(refcount >= aCount);
1433
1434 // Next, calculate the new refcount
1435 int32_t newCount = refcount - aCount;
1436
1437 // If the last reference to this security blob was removed we can
1438 // just remove the entire row.
1439 if (newCount == 0) {
1440 QM_TRY_INSPECT(const auto& state,
1441 MOZ_TO_RESULT_INVOKE_TYPED(
1442 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1443 "DELETE FROM security_info WHERE id=:id;"_ns));
1444
1445 QM_TRY(state->BindInt32ByName("id"_ns, aId));
1446 QM_TRY(state->Execute());
1447
1448 return NS_OK;
1449 }
1450
1451 // Otherwise update the refcount in the table to reflect the reduced
1452 // number of references to the security blob.
1453 QM_TRY_INSPECT(
1454 const auto& state,
1455 MOZ_TO_RESULT_INVOKE_TYPED(
1456 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1457 "UPDATE security_info SET refcount=:refcount WHERE id=:id;"_ns));
1458
1459 QM_TRY(state->BindInt32ByName("refcount"_ns, newCount));
1460 QM_TRY(state->BindInt32ByName("id"_ns, aId));
1461 QM_TRY(state->Execute());
1462
1463 return NS_OK;
1464 }
1465
DeleteSecurityInfoList(mozIStorageConnection & aConn,const nsTArray<IdCount> & aDeletedStorageIdList)1466 nsresult DeleteSecurityInfoList(
1467 mozIStorageConnection& aConn,
1468 const nsTArray<IdCount>& aDeletedStorageIdList) {
1469 for (const auto& deletedStorageId : aDeletedStorageIdList) {
1470 QM_TRY(DeleteSecurityInfo(aConn, deletedStorageId.mId,
1471 deletedStorageId.mCount));
1472 }
1473
1474 return NS_OK;
1475 }
1476
InsertEntry(mozIStorageConnection & aConn,CacheId aCacheId,const CacheRequest & aRequest,const nsID * aRequestBodyId,const CacheResponse & aResponse,const nsID * aResponseBodyId)1477 nsresult InsertEntry(mozIStorageConnection& aConn, CacheId aCacheId,
1478 const CacheRequest& aRequest, const nsID* aRequestBodyId,
1479 const CacheResponse& aResponse,
1480 const nsID* aResponseBodyId) {
1481 MOZ_ASSERT(!NS_IsMainThread());
1482
1483 QM_TRY_INSPECT(const auto& crypto, ToResultGet<nsCOMPtr<nsICryptoHash>>(
1484 MOZ_SELECT_OVERLOAD(do_CreateInstance),
1485 NS_CRYPTO_HASH_CONTRACTID));
1486
1487 int32_t securityId = -1;
1488 if (!aResponse.channelInfo().securityInfo().IsEmpty()) {
1489 QM_TRY_UNWRAP(securityId,
1490 InsertSecurityInfo(aConn, *crypto,
1491 aResponse.channelInfo().securityInfo()));
1492 }
1493
1494 {
1495 QM_TRY_INSPECT(const auto& state,
1496 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1497 aConn, CreateStatement,
1498 "INSERT INTO entries ("
1499 "request_method, "
1500 "request_url_no_query, "
1501 "request_url_no_query_hash, "
1502 "request_url_query, "
1503 "request_url_query_hash, "
1504 "request_url_fragment, "
1505 "request_referrer, "
1506 "request_referrer_policy, "
1507 "request_headers_guard, "
1508 "request_mode, "
1509 "request_credentials, "
1510 "request_contentpolicytype, "
1511 "request_cache, "
1512 "request_redirect, "
1513 "request_integrity, "
1514 "request_body_id, "
1515 "response_type, "
1516 "response_status, "
1517 "response_status_text, "
1518 "response_headers_guard, "
1519 "response_body_id, "
1520 "response_security_info_id, "
1521 "response_principal_info, "
1522 "response_padding_size, "
1523 "cache_id "
1524 ") VALUES ("
1525 ":request_method, "
1526 ":request_url_no_query, "
1527 ":request_url_no_query_hash, "
1528 ":request_url_query, "
1529 ":request_url_query_hash, "
1530 ":request_url_fragment, "
1531 ":request_referrer, "
1532 ":request_referrer_policy, "
1533 ":request_headers_guard, "
1534 ":request_mode, "
1535 ":request_credentials, "
1536 ":request_contentpolicytype, "
1537 ":request_cache, "
1538 ":request_redirect, "
1539 ":request_integrity, "
1540 ":request_body_id, "
1541 ":response_type, "
1542 ":response_status, "
1543 ":response_status_text, "
1544 ":response_headers_guard, "
1545 ":response_body_id, "
1546 ":response_security_info_id, "
1547 ":response_principal_info, "
1548 ":response_padding_size, "
1549 ":cache_id "
1550 ");"_ns));
1551
1552 QM_TRY(state->BindUTF8StringByName("request_method"_ns, aRequest.method()));
1553
1554 QM_TRY(state->BindUTF8StringByName("request_url_no_query"_ns,
1555 aRequest.urlWithoutQuery()));
1556
1557 QM_TRY_INSPECT(const auto& urlWithoutQueryHash,
1558 HashCString(*crypto, aRequest.urlWithoutQuery()));
1559
1560 QM_TRY(state->BindUTF8StringAsBlobByName("request_url_no_query_hash"_ns,
1561 urlWithoutQueryHash));
1562
1563 QM_TRY(state->BindUTF8StringByName("request_url_query"_ns,
1564 aRequest.urlQuery()));
1565
1566 QM_TRY_INSPECT(const auto& urlQueryHash,
1567 HashCString(*crypto, aRequest.urlQuery()));
1568
1569 QM_TRY(state->BindUTF8StringAsBlobByName("request_url_query_hash"_ns,
1570 urlQueryHash));
1571
1572 QM_TRY(state->BindUTF8StringByName("request_url_fragment"_ns,
1573 aRequest.urlFragment()));
1574
1575 QM_TRY(state->BindStringByName("request_referrer"_ns, aRequest.referrer()));
1576
1577 QM_TRY(state->BindInt32ByName(
1578 "request_referrer_policy"_ns,
1579 static_cast<int32_t>(aRequest.referrerPolicy())));
1580
1581 QM_TRY(
1582 state->BindInt32ByName("request_headers_guard"_ns,
1583 static_cast<int32_t>(aRequest.headersGuard())));
1584
1585 QM_TRY(state->BindInt32ByName("request_mode"_ns,
1586 static_cast<int32_t>(aRequest.mode())));
1587
1588 QM_TRY(
1589 state->BindInt32ByName("request_credentials"_ns,
1590 static_cast<int32_t>(aRequest.credentials())));
1591
1592 QM_TRY(state->BindInt32ByName(
1593 "request_contentpolicytype"_ns,
1594 static_cast<int32_t>(aRequest.contentPolicyType())));
1595
1596 QM_TRY(state->BindInt32ByName(
1597 "request_cache"_ns, static_cast<int32_t>(aRequest.requestCache())));
1598
1599 QM_TRY(state->BindInt32ByName(
1600 "request_redirect"_ns,
1601 static_cast<int32_t>(aRequest.requestRedirect())));
1602
1603 QM_TRY(
1604 state->BindStringByName("request_integrity"_ns, aRequest.integrity()));
1605
1606 QM_TRY(BindId(*state, "request_body_id"_ns, aRequestBodyId));
1607
1608 QM_TRY(state->BindInt32ByName("response_type"_ns,
1609 static_cast<int32_t>(aResponse.type())));
1610
1611 QM_TRY(state->BindInt32ByName("response_status"_ns, aResponse.status()));
1612
1613 QM_TRY(state->BindUTF8StringByName("response_status_text"_ns,
1614 aResponse.statusText()));
1615
1616 QM_TRY(
1617 state->BindInt32ByName("response_headers_guard"_ns,
1618 static_cast<int32_t>(aResponse.headersGuard())));
1619
1620 QM_TRY(BindId(*state, "response_body_id"_ns, aResponseBodyId));
1621
1622 if (aResponse.channelInfo().securityInfo().IsEmpty()) {
1623 QM_TRY(state->BindNullByName("response_security_info_id"_ns));
1624 } else {
1625 QM_TRY(
1626 state->BindInt32ByName("response_security_info_id"_ns, securityId));
1627 }
1628
1629 nsAutoCString serializedInfo;
1630 // We only allow content serviceworkers right now.
1631 if (aResponse.principalInfo().isSome()) {
1632 const mozilla::ipc::PrincipalInfo& principalInfo =
1633 aResponse.principalInfo().ref();
1634 MOZ_DIAGNOSTIC_ASSERT(principalInfo.type() ==
1635 mozilla::ipc::PrincipalInfo::TContentPrincipalInfo);
1636 const mozilla::ipc::ContentPrincipalInfo& cInfo =
1637 principalInfo.get_ContentPrincipalInfo();
1638
1639 serializedInfo.Append(cInfo.spec());
1640
1641 nsAutoCString suffix;
1642 cInfo.attrs().CreateSuffix(suffix);
1643 serializedInfo.Append(suffix);
1644 }
1645
1646 QM_TRY(state->BindUTF8StringByName("response_principal_info"_ns,
1647 serializedInfo));
1648
1649 if (aResponse.paddingSize() == InternalResponse::UNKNOWN_PADDING_SIZE) {
1650 MOZ_DIAGNOSTIC_ASSERT(aResponse.type() != ResponseType::Opaque);
1651 QM_TRY(state->BindNullByName("response_padding_size"_ns));
1652 } else {
1653 MOZ_DIAGNOSTIC_ASSERT(aResponse.paddingSize() >= 0);
1654 MOZ_DIAGNOSTIC_ASSERT(aResponse.type() == ResponseType::Opaque);
1655
1656 QM_TRY(state->BindInt64ByName("response_padding_size"_ns,
1657 aResponse.paddingSize()));
1658 }
1659
1660 QM_TRY(state->BindInt64ByName("cache_id"_ns, aCacheId));
1661
1662 QM_TRY(state->Execute());
1663 }
1664
1665 QM_TRY_INSPECT(const int32_t& entryId,
1666 ([&aConn]() -> Result<int32_t, nsresult> {
1667 QM_TRY_INSPECT(const auto& state,
1668 quota::CreateAndExecuteSingleStepStatement(
1669 aConn, "SELECT last_insert_rowid()"_ns));
1670
1671 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
1672 }()));
1673
1674 {
1675 QM_TRY_INSPECT(const auto& state,
1676 MOZ_TO_RESULT_INVOKE_TYPED(
1677 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1678 "INSERT INTO request_headers ("
1679 "name, "
1680 "value, "
1681 "entry_id "
1682 ") VALUES (:name, :value, :entry_id)"_ns));
1683
1684 for (const auto& requestHeader : aRequest.headers()) {
1685 QM_TRY(state->BindUTF8StringByName("name"_ns, requestHeader.name()));
1686
1687 QM_TRY(state->BindUTF8StringByName("value"_ns, requestHeader.value()));
1688
1689 QM_TRY(state->BindInt32ByName("entry_id"_ns, entryId));
1690
1691 QM_TRY(state->Execute());
1692 }
1693 }
1694
1695 {
1696 QM_TRY_INSPECT(const auto& state,
1697 MOZ_TO_RESULT_INVOKE_TYPED(
1698 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
1699 "INSERT INTO response_headers ("
1700 "name, "
1701 "value, "
1702 "entry_id "
1703 ") VALUES (:name, :value, :entry_id)"_ns));
1704
1705 for (const auto& responseHeader : aResponse.headers()) {
1706 QM_TRY(state->BindUTF8StringByName("name"_ns, responseHeader.name()));
1707 QM_TRY(state->BindUTF8StringByName("value"_ns, responseHeader.value()));
1708 QM_TRY(state->BindInt32ByName("entry_id"_ns, entryId));
1709 QM_TRY(state->Execute());
1710 }
1711 }
1712
1713 {
1714 QM_TRY_INSPECT(const auto& state,
1715 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1716 aConn, CreateStatement,
1717 "INSERT INTO response_url_list ("
1718 "url, "
1719 "entry_id "
1720 ") VALUES (:url, :entry_id)"_ns));
1721
1722 for (const auto& responseUrl : aResponse.urlList()) {
1723 QM_TRY(state->BindUTF8StringByName("url"_ns, responseUrl));
1724 QM_TRY(state->BindInt32ByName("entry_id"_ns, entryId));
1725 QM_TRY(state->Execute());
1726 }
1727 }
1728
1729 return NS_OK;
1730 }
1731
1732 /**
1733 * Gets a HeadersEntry from a storage statement by retrieving the first column
1734 * as the name and the second column as the value.
1735 */
GetHeadersEntryFromStatement(mozIStorageStatement & aStmt)1736 Result<HeadersEntry, nsresult> GetHeadersEntryFromStatement(
1737 mozIStorageStatement& aStmt) {
1738 HeadersEntry header;
1739
1740 QM_TRY_UNWRAP(header.name(),
1741 MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aStmt, GetUTF8String, 0));
1742 QM_TRY_UNWRAP(header.value(),
1743 MOZ_TO_RESULT_INVOKE_TYPED(nsCString, aStmt, GetUTF8String, 1));
1744
1745 return header;
1746 }
1747
ReadResponse(mozIStorageConnection & aConn,EntryId aEntryId)1748 Result<SavedResponse, nsresult> ReadResponse(mozIStorageConnection& aConn,
1749 EntryId aEntryId) {
1750 MOZ_ASSERT(!NS_IsMainThread());
1751
1752 SavedResponse savedResponse;
1753
1754 QM_TRY_INSPECT(const auto& state,
1755 quota::CreateAndExecuteSingleStepStatement(
1756 aConn,
1757 "SELECT "
1758 "entries.response_type, "
1759 "entries.response_status, "
1760 "entries.response_status_text, "
1761 "entries.response_headers_guard, "
1762 "entries.response_body_id, "
1763 "entries.response_principal_info, "
1764 "entries.response_padding_size, "
1765 "security_info.data "
1766 "FROM entries "
1767 "LEFT OUTER JOIN security_info "
1768 "ON entries.response_security_info_id=security_info.id "
1769 "WHERE entries.id=:id;"_ns,
1770 [aEntryId](auto& state) -> Result<Ok, nsresult> {
1771 QM_TRY(state.BindInt32ByName("id"_ns, aEntryId));
1772
1773 return Ok{};
1774 }));
1775
1776 QM_TRY_INSPECT(const int32_t& type,
1777 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
1778 savedResponse.mValue.type() = static_cast<ResponseType>(type);
1779
1780 QM_TRY_INSPECT(const int32_t& status,
1781 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 1));
1782 savedResponse.mValue.status() = static_cast<uint32_t>(status);
1783
1784 QM_TRY(state->GetUTF8String(2, savedResponse.mValue.statusText()));
1785
1786 QM_TRY_INSPECT(const int32_t& guard,
1787 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 3));
1788 savedResponse.mValue.headersGuard() = static_cast<HeadersGuardEnum>(guard);
1789
1790 QM_TRY_INSPECT(const bool& nullBody,
1791 MOZ_TO_RESULT_INVOKE(*state, GetIsNull, 4));
1792 savedResponse.mHasBodyId = !nullBody;
1793
1794 if (savedResponse.mHasBodyId) {
1795 QM_TRY_UNWRAP(savedResponse.mBodyId, ExtractId(*state, 4));
1796 }
1797
1798 QM_TRY_INSPECT(
1799 const auto& serializedInfo,
1800 MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, *state, GetUTF8String, 5));
1801
1802 savedResponse.mValue.principalInfo() = Nothing();
1803 if (!serializedInfo.IsEmpty()) {
1804 nsAutoCString specNoSuffix;
1805 OriginAttributes attrs;
1806 if (!attrs.PopulateFromOrigin(serializedInfo, specNoSuffix)) {
1807 NS_WARNING("Something went wrong parsing a serialized principal!");
1808 return Err(NS_ERROR_FAILURE);
1809 }
1810
1811 RefPtr<net::MozURL> url;
1812 QM_TRY(net::MozURL::Init(getter_AddRefs(url), specNoSuffix));
1813
1814 #ifdef DEBUG
1815 nsDependentCSubstring scheme = url->Scheme();
1816
1817 MOZ_ASSERT(
1818 scheme == "http" || scheme == "https" || scheme == "file" ||
1819 // A cached response entry may have a moz-extension principal if:
1820 //
1821 // - This is an extension background service worker. The response for
1822 // the main script is expected tobe a moz-extension content principal
1823 // (the pref "extensions.backgroundServiceWorker.enabled" must be
1824 // enabled, if the pref is toggled to false at runtime then any
1825 // service worker registered for a moz-extension principal will be
1826 // unregistered on the next startup).
1827 //
1828 // - An extension is redirecting a script being imported info a worker
1829 // created from a regular webpage to a web-accessible extension
1830 // script. The reponse for these redirects will have a moz-extension
1831 // principal. Although extensions can attempt to redirect the main
1832 // script of service workers, this will always cause the install
1833 // process to fail.
1834 scheme == "moz-extension");
1835 #endif
1836
1837 nsCString origin;
1838 url->Origin(origin);
1839
1840 nsCString baseDomain;
1841 QM_TRY(url->BaseDomain(baseDomain));
1842
1843 savedResponse.mValue.principalInfo() =
1844 Some(mozilla::ipc::ContentPrincipalInfo(attrs, origin, specNoSuffix,
1845 Nothing(), baseDomain));
1846 }
1847
1848 QM_TRY_INSPECT(const bool& nullPadding,
1849 MOZ_TO_RESULT_INVOKE(*state, GetIsNull, 6));
1850
1851 if (nullPadding) {
1852 MOZ_DIAGNOSTIC_ASSERT(savedResponse.mValue.type() != ResponseType::Opaque);
1853 savedResponse.mValue.paddingSize() = InternalResponse::UNKNOWN_PADDING_SIZE;
1854 } else {
1855 MOZ_DIAGNOSTIC_ASSERT(savedResponse.mValue.type() == ResponseType::Opaque);
1856 QM_TRY_INSPECT(const int64_t& paddingSize,
1857 MOZ_TO_RESULT_INVOKE(*state, GetInt64, 6));
1858
1859 MOZ_DIAGNOSTIC_ASSERT(paddingSize >= 0);
1860 savedResponse.mValue.paddingSize() = paddingSize;
1861 }
1862
1863 QM_TRY(state->GetBlobAsUTF8String(
1864 7, savedResponse.mValue.channelInfo().securityInfo()));
1865
1866 {
1867 QM_TRY_INSPECT(const auto& state,
1868 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1869 aConn, CreateStatement,
1870 "SELECT "
1871 "name, "
1872 "value "
1873 "FROM response_headers "
1874 "WHERE entry_id=:entry_id;"_ns));
1875
1876 QM_TRY(state->BindInt32ByName("entry_id"_ns, aEntryId));
1877
1878 QM_TRY_UNWRAP(savedResponse.mValue.headers(),
1879 quota::CollectElementsWhileHasResult(
1880 *state, GetHeadersEntryFromStatement));
1881 }
1882
1883 {
1884 QM_TRY_INSPECT(const auto& state,
1885 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1886 aConn, CreateStatement,
1887 "SELECT "
1888 "url "
1889 "FROM response_url_list "
1890 "WHERE entry_id=:entry_id;"_ns));
1891
1892 QM_TRY(state->BindInt32ByName("entry_id"_ns, aEntryId));
1893
1894 QM_TRY_UNWRAP(savedResponse.mValue.urlList(),
1895 quota::CollectElementsWhileHasResult(
1896 *state, [](auto& stmt) -> Result<nsCString, nsresult> {
1897 QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_TYPED(
1898 nsCString, stmt, GetUTF8String, 0));
1899 }));
1900 }
1901
1902 return savedResponse;
1903 }
1904
ReadRequest(mozIStorageConnection & aConn,EntryId aEntryId)1905 Result<SavedRequest, nsresult> ReadRequest(mozIStorageConnection& aConn,
1906 EntryId aEntryId) {
1907 MOZ_ASSERT(!NS_IsMainThread());
1908
1909 SavedRequest savedRequest;
1910
1911 QM_TRY_INSPECT(const auto& state,
1912 quota::CreateAndExecuteSingleStepStatement<
1913 quota::SingleStepResult::ReturnNullIfNoResult>(
1914 aConn,
1915 "SELECT "
1916 "request_method, "
1917 "request_url_no_query, "
1918 "request_url_query, "
1919 "request_url_fragment, "
1920 "request_referrer, "
1921 "request_referrer_policy, "
1922 "request_headers_guard, "
1923 "request_mode, "
1924 "request_credentials, "
1925 "request_contentpolicytype, "
1926 "request_cache, "
1927 "request_redirect, "
1928 "request_integrity, "
1929 "request_body_id "
1930 "FROM entries "
1931 "WHERE id=:id;"_ns,
1932 [aEntryId](auto& state) -> Result<Ok, nsresult> {
1933 QM_TRY(state.BindInt32ByName("id"_ns, aEntryId));
1934
1935 return Ok{};
1936 }));
1937
1938 QM_TRY(OkIf(state), Err(NS_ERROR_UNEXPECTED));
1939
1940 QM_TRY(state->GetUTF8String(0, savedRequest.mValue.method()));
1941 QM_TRY(state->GetUTF8String(1, savedRequest.mValue.urlWithoutQuery()));
1942 QM_TRY(state->GetUTF8String(2, savedRequest.mValue.urlQuery()));
1943 QM_TRY(state->GetUTF8String(3, savedRequest.mValue.urlFragment()));
1944 QM_TRY(state->GetString(4, savedRequest.mValue.referrer()));
1945
1946 QM_TRY_INSPECT(const int32_t& referrerPolicy,
1947 MOZ_TO_RESULT_INVOKE(state, GetInt32, 5));
1948 savedRequest.mValue.referrerPolicy() =
1949 static_cast<ReferrerPolicy>(referrerPolicy);
1950
1951 QM_TRY_INSPECT(const int32_t& guard,
1952 MOZ_TO_RESULT_INVOKE(state, GetInt32, 6));
1953 savedRequest.mValue.headersGuard() = static_cast<HeadersGuardEnum>(guard);
1954
1955 QM_TRY_INSPECT(const int32_t& mode, MOZ_TO_RESULT_INVOKE(state, GetInt32, 7));
1956 savedRequest.mValue.mode() = static_cast<RequestMode>(mode);
1957
1958 QM_TRY_INSPECT(const int32_t& credentials,
1959 MOZ_TO_RESULT_INVOKE(state, GetInt32, 8));
1960 savedRequest.mValue.credentials() =
1961 static_cast<RequestCredentials>(credentials);
1962
1963 QM_TRY_INSPECT(const int32_t& requestContentPolicyType,
1964 MOZ_TO_RESULT_INVOKE(state, GetInt32, 9));
1965 savedRequest.mValue.contentPolicyType() =
1966 static_cast<nsContentPolicyType>(requestContentPolicyType);
1967
1968 QM_TRY_INSPECT(const int32_t& requestCache,
1969 MOZ_TO_RESULT_INVOKE(state, GetInt32, 10));
1970 savedRequest.mValue.requestCache() = static_cast<RequestCache>(requestCache);
1971
1972 QM_TRY_INSPECT(const int32_t& requestRedirect,
1973 MOZ_TO_RESULT_INVOKE(state, GetInt32, 11));
1974 savedRequest.mValue.requestRedirect() =
1975 static_cast<RequestRedirect>(requestRedirect);
1976
1977 QM_TRY(state->GetString(12, savedRequest.mValue.integrity()));
1978
1979 QM_TRY_INSPECT(const bool& nullBody,
1980 MOZ_TO_RESULT_INVOKE(state, GetIsNull, 13));
1981 savedRequest.mHasBodyId = !nullBody;
1982 if (savedRequest.mHasBodyId) {
1983 QM_TRY_UNWRAP(savedRequest.mBodyId, ExtractId(*state, 13));
1984 }
1985
1986 {
1987 QM_TRY_INSPECT(const auto& state,
1988 MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<mozIStorageStatement>,
1989 aConn, CreateStatement,
1990 "SELECT "
1991 "name, "
1992 "value "
1993 "FROM request_headers "
1994 "WHERE entry_id=:entry_id;"_ns));
1995
1996 QM_TRY(state->BindInt32ByName("entry_id"_ns, aEntryId));
1997
1998 QM_TRY_UNWRAP(savedRequest.mValue.headers(),
1999 quota::CollectElementsWhileHasResult(
2000 *state, GetHeadersEntryFromStatement));
2001 }
2002
2003 return savedRequest;
2004 }
2005
AppendListParamsToQuery(nsACString & aQuery,const nsTArray<EntryId> & aEntryIdList,uint32_t aPos,int32_t aLen)2006 void AppendListParamsToQuery(nsACString& aQuery,
2007 const nsTArray<EntryId>& aEntryIdList,
2008 uint32_t aPos, int32_t aLen) {
2009 MOZ_ASSERT(!NS_IsMainThread());
2010 MOZ_DIAGNOSTIC_ASSERT((aPos + aLen) <= aEntryIdList.Length());
2011
2012 // XXX This seems to be quite inefficient. Can't we do a BulkWrite?
2013 for (int32_t i = aPos; i < aLen; ++i) {
2014 if (i == 0) {
2015 aQuery.AppendLiteral("?");
2016 } else {
2017 aQuery.AppendLiteral(",?");
2018 }
2019 }
2020 }
2021
BindListParamsToQuery(mozIStorageStatement & aState,const nsTArray<EntryId> & aEntryIdList,uint32_t aPos,int32_t aLen)2022 nsresult BindListParamsToQuery(mozIStorageStatement& aState,
2023 const nsTArray<EntryId>& aEntryIdList,
2024 uint32_t aPos, int32_t aLen) {
2025 MOZ_ASSERT(!NS_IsMainThread());
2026 MOZ_DIAGNOSTIC_ASSERT((aPos + aLen) <= aEntryIdList.Length());
2027 for (int32_t i = aPos; i < aLen; ++i) {
2028 QM_TRY(aState.BindInt32ByIndex(i, aEntryIdList[i]));
2029 }
2030 return NS_OK;
2031 }
2032
BindId(mozIStorageStatement & aState,const nsACString & aName,const nsID * aId)2033 nsresult BindId(mozIStorageStatement& aState, const nsACString& aName,
2034 const nsID* aId) {
2035 MOZ_ASSERT(!NS_IsMainThread());
2036
2037 if (!aId) {
2038 QM_TRY(aState.BindNullByName(aName));
2039 return NS_OK;
2040 }
2041
2042 char idBuf[NSID_LENGTH];
2043 aId->ToProvidedString(idBuf);
2044 QM_TRY(aState.BindUTF8StringByName(aName, nsDependentCString(idBuf)));
2045
2046 return NS_OK;
2047 }
2048
ExtractId(mozIStorageStatement & aState,uint32_t aPos)2049 Result<nsID, nsresult> ExtractId(mozIStorageStatement& aState, uint32_t aPos) {
2050 MOZ_ASSERT(!NS_IsMainThread());
2051
2052 QM_TRY_INSPECT(
2053 const auto& idString,
2054 MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, aState, GetUTF8String, aPos));
2055
2056 nsID id;
2057 QM_TRY(OkIf(id.Parse(idString.get())), Err(NS_ERROR_UNEXPECTED));
2058
2059 return id;
2060 }
2061
2062 Result<NotNull<nsCOMPtr<mozIStorageStatement>>, nsresult>
CreateAndBindKeyStatement(mozIStorageConnection & aConn,const char * const aQueryFormat,const nsAString & aKey)2063 CreateAndBindKeyStatement(mozIStorageConnection& aConn,
2064 const char* const aQueryFormat,
2065 const nsAString& aKey) {
2066 MOZ_DIAGNOSTIC_ASSERT(aQueryFormat);
2067
2068 // The key is stored as a blob to avoid encoding issues. An empty string
2069 // is mapped to NULL for blobs. Normally we would just write the query
2070 // as "key IS :key" to do the proper NULL checking, but that prevents
2071 // sqlite from using the key index. Therefore use "IS NULL" explicitly
2072 // if the key is empty, otherwise use "=:key" so that sqlite uses the
2073 // index.
2074
2075 QM_TRY_UNWRAP(
2076 auto state,
2077 MOZ_TO_RESULT_INVOKE_TYPED(
2078 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
2079 nsPrintfCString(aQueryFormat,
2080 aKey.IsEmpty() ? "key IS NULL" : "key=:key")));
2081
2082 if (!aKey.IsEmpty()) {
2083 QM_TRY(state->BindStringAsBlobByName("key"_ns, aKey));
2084 }
2085
2086 return WrapNotNull(std::move(state));
2087 }
2088
HashCString(nsICryptoHash & aCrypto,const nsACString & aIn)2089 Result<nsAutoCString, nsresult> HashCString(nsICryptoHash& aCrypto,
2090 const nsACString& aIn) {
2091 QM_TRY(aCrypto.Init(nsICryptoHash::SHA1));
2092
2093 QM_TRY(aCrypto.Update(reinterpret_cast<const uint8_t*>(aIn.BeginReading()),
2094 aIn.Length()));
2095
2096 nsAutoCString fullHash;
2097 QM_TRY(aCrypto.Finish(false /* based64 result */, fullHash));
2098
2099 return Result<nsAutoCString, nsresult>{std::in_place,
2100 Substring(fullHash, 0, 8)};
2101 }
2102
2103 } // namespace
2104
IncrementalVacuum(mozIStorageConnection & aConn)2105 nsresult IncrementalVacuum(mozIStorageConnection& aConn) {
2106 // Determine how much free space is in the database.
2107 QM_TRY_INSPECT(const auto& state, quota::CreateAndExecuteSingleStepStatement(
2108 aConn, "PRAGMA freelist_count;"_ns));
2109
2110 QM_TRY_INSPECT(const int32_t& freePages,
2111 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
2112
2113 // We have a relatively small page size, so we want to be careful to avoid
2114 // fragmentation. We already use a growth incremental which will cause
2115 // sqlite to allocate and release multiple pages at the same time. We can
2116 // further reduce fragmentation by making our allocated chunks a bit
2117 // "sticky". This is done by creating some hysteresis where we allocate
2118 // pages/chunks as soon as we need them, but we only release pages/chunks
2119 // when we have a large amount of free space. This helps with the case
2120 // where a page is adding and remove resources causing it to dip back and
2121 // forth across a chunk boundary.
2122 //
2123 // So only proceed with releasing pages if we have more than our constant
2124 // threshold.
2125 if (freePages <= kMaxFreePages) {
2126 return NS_OK;
2127 }
2128
2129 // Release the excess pages back to the sqlite VFS. This may also release
2130 // chunks of multiple pages back to the OS.
2131 const int32_t pagesToRelease = freePages - kMaxFreePages;
2132
2133 QM_TRY(aConn.ExecuteSimpleSQL(
2134 nsPrintfCString("PRAGMA incremental_vacuum(%d);", pagesToRelease)));
2135
2136 // Verify that our incremental vacuum actually did something
2137 #ifdef DEBUG
2138 {
2139 QM_TRY_INSPECT(const auto& state,
2140 quota::CreateAndExecuteSingleStepStatement(
2141 aConn, "PRAGMA freelist_count;"_ns));
2142
2143 QM_TRY_INSPECT(const int32_t& freePages,
2144 MOZ_TO_RESULT_INVOKE(*state, GetInt32, 0));
2145
2146 MOZ_ASSERT(freePages <= kMaxFreePages);
2147 }
2148 #endif
2149
2150 return NS_OK;
2151 }
2152
2153 namespace {
2154
2155 // Wrapper around mozIStorageConnection::GetSchemaVersion() that compensates
2156 // for hacky downgrade schema version tricks. See the block comments for
2157 // kHackyDowngradeSchemaVersion and kHackyPaddingSizePresentVersion.
GetEffectiveSchemaVersion(mozIStorageConnection & aConn)2158 Result<int32_t, nsresult> GetEffectiveSchemaVersion(
2159 mozIStorageConnection& aConn) {
2160 QM_TRY_INSPECT(const int32_t& schemaVersion,
2161 MOZ_TO_RESULT_INVOKE(aConn, GetSchemaVersion));
2162
2163 if (schemaVersion == kHackyDowngradeSchemaVersion) {
2164 // This is the special case. Check for the existence of the
2165 // "response_padding_size" colum in table "entries".
2166 //
2167 // (pragma_table_info is a table-valued function format variant of
2168 // "PRAGMA table_info" supported since SQLite 3.16.0. Firefox 53 shipped
2169 // was the first release with this functionality, shipping 3.16.2.)
2170 //
2171 // If there are any result rows, then the column is present.
2172 QM_TRY_INSPECT(const bool& hasColumn,
2173 quota::CreateAndExecuteSingleStepStatement<
2174 quota::SingleStepResult::ReturnNullIfNoResult>(
2175 aConn,
2176 "SELECT name FROM pragma_table_info('entries') WHERE "
2177 "name = 'response_padding_size'"_ns));
2178
2179 if (hasColumn) {
2180 return kHackyPaddingSizePresentVersion;
2181 }
2182 }
2183
2184 return schemaVersion;
2185 }
2186
2187 #ifdef DEBUG
2188 struct Expect {
2189 // Expect exact SQL
Expectmozilla::dom::cache::db::__anon339f3aee1e11::Expect2190 Expect(const char* aName, const char* aType, const char* aSql)
2191 : mName(aName), mType(aType), mSql(aSql), mIgnoreSql(false) {}
2192
2193 // Ignore SQL
Expectmozilla::dom::cache::db::__anon339f3aee1e11::Expect2194 Expect(const char* aName, const char* aType)
2195 : mName(aName), mType(aType), mIgnoreSql(true) {}
2196
2197 const nsCString mName;
2198 const nsCString mType;
2199 const nsCString mSql;
2200 const bool mIgnoreSql;
2201 };
2202 #endif
2203
Validate(mozIStorageConnection & aConn)2204 nsresult Validate(mozIStorageConnection& aConn) {
2205 QM_TRY_INSPECT(const int32_t& schemaVersion,
2206 GetEffectiveSchemaVersion(aConn));
2207 QM_TRY(OkIf(schemaVersion == kLatestSchemaVersion), NS_ERROR_FAILURE);
2208
2209 #ifdef DEBUG
2210 // This is the schema we expect the database at the latest version to
2211 // contain. Update this list if you add a new table or index.
2212 const Expect expects[] = {
2213 Expect("caches", "table", kTableCaches),
2214 Expect("sqlite_sequence", "table"), // auto-gen by sqlite
2215 Expect("security_info", "table", kTableSecurityInfo),
2216 Expect("security_info_hash_index", "index", kIndexSecurityInfoHash),
2217 Expect("entries", "table", kTableEntries),
2218 Expect("entries_request_match_index", "index", kIndexEntriesRequest),
2219 Expect("request_headers", "table", kTableRequestHeaders),
2220 Expect("response_headers", "table", kTableResponseHeaders),
2221 Expect("response_headers_name_index", "index", kIndexResponseHeadersName),
2222 Expect("response_url_list", "table", kTableResponseUrlList),
2223 Expect("storage", "table", kTableStorage),
2224 Expect("sqlite_autoindex_storage_1", "index"), // auto-gen by sqlite
2225 };
2226
2227 // Read the schema from the sqlite_master table and compare.
2228 QM_TRY_INSPECT(const auto& state,
2229 MOZ_TO_RESULT_INVOKE_TYPED(
2230 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
2231 "SELECT name, type, sql FROM sqlite_master;"_ns));
2232
2233 QM_TRY(quota::CollectWhileHasResult(
2234 *state, [&expects](auto& stmt) -> Result<Ok, nsresult> {
2235 QM_TRY_INSPECT(
2236 const auto& name,
2237 MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, stmt, GetUTF8String, 0));
2238 QM_TRY_INSPECT(
2239 const auto& type,
2240 MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, stmt, GetUTF8String, 1));
2241 QM_TRY_INSPECT(
2242 const auto& sql,
2243 MOZ_TO_RESULT_INVOKE_TYPED(nsAutoCString, stmt, GetUTF8String, 2));
2244
2245 bool foundMatch = false;
2246 for (const auto& expect : expects) {
2247 if (name == expect.mName) {
2248 if (type != expect.mType) {
2249 NS_WARNING(
2250 nsPrintfCString("Unexpected type for Cache schema entry %s",
2251 name.get())
2252 .get());
2253 return Err(NS_ERROR_FAILURE);
2254 }
2255
2256 if (!expect.mIgnoreSql && sql != expect.mSql) {
2257 NS_WARNING(
2258 nsPrintfCString("Unexpected SQL for Cache schema entry %s",
2259 name.get())
2260 .get());
2261 return Err(NS_ERROR_FAILURE);
2262 }
2263
2264 foundMatch = true;
2265 break;
2266 }
2267 }
2268
2269 if (NS_WARN_IF(!foundMatch)) {
2270 NS_WARNING(
2271 nsPrintfCString("Unexpected schema entry %s in Cache database",
2272 name.get())
2273 .get());
2274 return Err(NS_ERROR_FAILURE);
2275 }
2276
2277 return Ok{};
2278 }));
2279 #endif
2280
2281 return NS_OK;
2282 }
2283
2284 // -----
2285 // Schema migration code
2286 // -----
2287
2288 typedef nsresult (*MigrationFunc)(mozIStorageConnection&, bool&);
2289 struct Migration {
2290 int32_t mFromVersion;
2291 MigrationFunc mFunc;
2292 };
2293
2294 // Declare migration functions here. Each function should upgrade
2295 // the version by a single increment. Don't skip versions.
2296 nsresult MigrateFrom15To16(mozIStorageConnection& aConn, bool& aRewriteSchema);
2297 nsresult MigrateFrom16To17(mozIStorageConnection& aConn, bool& aRewriteSchema);
2298 nsresult MigrateFrom17To18(mozIStorageConnection& aConn, bool& aRewriteSchema);
2299 nsresult MigrateFrom18To19(mozIStorageConnection& aConn, bool& aRewriteSchema);
2300 nsresult MigrateFrom19To20(mozIStorageConnection& aConn, bool& aRewriteSchema);
2301 nsresult MigrateFrom20To21(mozIStorageConnection& aConn, bool& aRewriteSchema);
2302 nsresult MigrateFrom21To22(mozIStorageConnection& aConn, bool& aRewriteSchema);
2303 nsresult MigrateFrom22To23(mozIStorageConnection& aConn, bool& aRewriteSchema);
2304 nsresult MigrateFrom23To24(mozIStorageConnection& aConn, bool& aRewriteSchema);
2305 nsresult MigrateFrom24To25(mozIStorageConnection& aConn, bool& aRewriteSchema);
2306 nsresult MigrateFrom25To26(mozIStorageConnection& aConn, bool& aRewriteSchema);
2307 nsresult MigrateFrom26To27(mozIStorageConnection& aConn, bool& aRewriteSchema);
2308 nsresult MigrateFrom27To28(mozIStorageConnection& aConn, bool& aRewriteSchema);
2309 // Configure migration functions to run for the given starting version.
2310 constexpr Migration sMigrationList[] = {
2311 Migration{15, MigrateFrom15To16}, Migration{16, MigrateFrom16To17},
2312 Migration{17, MigrateFrom17To18}, Migration{18, MigrateFrom18To19},
2313 Migration{19, MigrateFrom19To20}, Migration{20, MigrateFrom20To21},
2314 Migration{21, MigrateFrom21To22}, Migration{22, MigrateFrom22To23},
2315 Migration{23, MigrateFrom23To24}, Migration{24, MigrateFrom24To25},
2316 Migration{25, MigrateFrom25To26}, Migration{26, MigrateFrom26To27},
2317 Migration{27, MigrateFrom27To28},
2318 };
2319
RewriteEntriesSchema(mozIStorageConnection & aConn)2320 nsresult RewriteEntriesSchema(mozIStorageConnection& aConn) {
2321 QM_TRY(aConn.ExecuteSimpleSQL("PRAGMA writable_schema = ON"_ns));
2322
2323 QM_TRY_INSPECT(
2324 const auto& state,
2325 MOZ_TO_RESULT_INVOKE_TYPED(
2326 nsCOMPtr<mozIStorageStatement>, aConn, CreateStatement,
2327 "UPDATE sqlite_master SET sql=:sql WHERE name='entries'"_ns));
2328
2329 QM_TRY(
2330 state->BindUTF8StringByName("sql"_ns, nsDependentCString(kTableEntries)));
2331 QM_TRY(state->Execute());
2332
2333 QM_TRY(aConn.ExecuteSimpleSQL("PRAGMA writable_schema = OFF"_ns));
2334
2335 return NS_OK;
2336 }
2337
Migrate(mozIStorageConnection & aConn)2338 nsresult Migrate(mozIStorageConnection& aConn) {
2339 MOZ_ASSERT(!NS_IsMainThread());
2340
2341 QM_TRY_UNWRAP(int32_t currentVersion, GetEffectiveSchemaVersion(aConn));
2342
2343 bool rewriteSchema = false;
2344
2345 while (currentVersion < kLatestSchemaVersion) {
2346 // Wiping old databases is handled in DBAction because it requires
2347 // making a whole new mozIStorageConnection. Make sure we don't
2348 // accidentally get here for one of those old databases.
2349 MOZ_DIAGNOSTIC_ASSERT(currentVersion >= kFirstShippedSchemaVersion);
2350
2351 for (const auto& migration : sMigrationList) {
2352 if (migration.mFromVersion == currentVersion) {
2353 bool shouldRewrite = false;
2354 QM_TRY(migration.mFunc(aConn, shouldRewrite));
2355 if (shouldRewrite) {
2356 rewriteSchema = true;
2357 }
2358 break;
2359 }
2360 }
2361
2362 #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
2363 int32_t lastVersion = currentVersion;
2364 #endif
2365 QM_TRY_UNWRAP(currentVersion, GetEffectiveSchemaVersion(aConn));
2366
2367 MOZ_DIAGNOSTIC_ASSERT(currentVersion > lastVersion);
2368 }
2369
2370 // Don't release assert this since people do sometimes share profiles
2371 // across schema versions. Our check in Validate() will catch it.
2372 MOZ_ASSERT(currentVersion == kLatestSchemaVersion);
2373
2374 nsresult rv = NS_OK;
2375 if (rewriteSchema) {
2376 // Now overwrite the master SQL for the entries table to remove the column
2377 // default value. This is also necessary for our Validate() method to
2378 // pass on this database.
2379 rv = RewriteEntriesSchema(aConn);
2380 }
2381
2382 return rv;
2383 }
2384
MigrateFrom15To16(mozIStorageConnection & aConn,bool & aRewriteSchema)2385 nsresult MigrateFrom15To16(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2386 MOZ_ASSERT(!NS_IsMainThread());
2387
2388 // Add the request_redirect column with a default value of "follow". Note,
2389 // we only use a default value here because its required by ALTER TABLE and
2390 // we need to apply the default "follow" to existing records in the table.
2391 // We don't actually want to keep the default in the schema for future
2392 // INSERTs.
2393 QM_TRY(aConn.ExecuteSimpleSQL(
2394 "ALTER TABLE entries "
2395 "ADD COLUMN request_redirect INTEGER NOT NULL DEFAULT 0"_ns));
2396
2397 QM_TRY(aConn.SetSchemaVersion(16));
2398
2399 aRewriteSchema = true;
2400
2401 return NS_OK;
2402 }
2403
MigrateFrom16To17(mozIStorageConnection & aConn,bool & aRewriteSchema)2404 nsresult MigrateFrom16To17(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2405 MOZ_ASSERT(!NS_IsMainThread());
2406
2407 // This migration path removes the response_redirected and
2408 // response_redirected_url columns from the entries table. sqlite doesn't
2409 // support removing a column from a table using ALTER TABLE, so we need to
2410 // create a new table without those columns, fill it up with the existing
2411 // data, and then drop the original table and rename the new one to the old
2412 // one.
2413
2414 // Create a new_entries table with the new fields as of version 17.
2415 QM_TRY(aConn.ExecuteSimpleSQL(
2416 "CREATE TABLE new_entries ("
2417 "id INTEGER NOT NULL PRIMARY KEY, "
2418 "request_method TEXT NOT NULL, "
2419 "request_url_no_query TEXT NOT NULL, "
2420 "request_url_no_query_hash BLOB NOT NULL, "
2421 "request_url_query TEXT NOT NULL, "
2422 "request_url_query_hash BLOB NOT NULL, "
2423 "request_referrer TEXT NOT NULL, "
2424 "request_headers_guard INTEGER NOT NULL, "
2425 "request_mode INTEGER NOT NULL, "
2426 "request_credentials INTEGER NOT NULL, "
2427 "request_contentpolicytype INTEGER NOT NULL, "
2428 "request_cache INTEGER NOT NULL, "
2429 "request_body_id TEXT NULL, "
2430 "response_type INTEGER NOT NULL, "
2431 "response_url TEXT NOT NULL, "
2432 "response_status INTEGER NOT NULL, "
2433 "response_status_text TEXT NOT NULL, "
2434 "response_headers_guard INTEGER NOT NULL, "
2435 "response_body_id TEXT NULL, "
2436 "response_security_info_id INTEGER NULL REFERENCES security_info(id), "
2437 "response_principal_info TEXT NOT NULL, "
2438 "cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
2439 "request_redirect INTEGER NOT NULL"
2440 ")"_ns));
2441
2442 // Copy all of the data to the newly created table.
2443 QM_TRY(
2444 aConn.ExecuteSimpleSQL("INSERT INTO new_entries ("
2445 "id, "
2446 "request_method, "
2447 "request_url_no_query, "
2448 "request_url_no_query_hash, "
2449 "request_url_query, "
2450 "request_url_query_hash, "
2451 "request_referrer, "
2452 "request_headers_guard, "
2453 "request_mode, "
2454 "request_credentials, "
2455 "request_contentpolicytype, "
2456 "request_cache, "
2457 "request_redirect, "
2458 "request_body_id, "
2459 "response_type, "
2460 "response_url, "
2461 "response_status, "
2462 "response_status_text, "
2463 "response_headers_guard, "
2464 "response_body_id, "
2465 "response_security_info_id, "
2466 "response_principal_info, "
2467 "cache_id "
2468 ") SELECT "
2469 "id, "
2470 "request_method, "
2471 "request_url_no_query, "
2472 "request_url_no_query_hash, "
2473 "request_url_query, "
2474 "request_url_query_hash, "
2475 "request_referrer, "
2476 "request_headers_guard, "
2477 "request_mode, "
2478 "request_credentials, "
2479 "request_contentpolicytype, "
2480 "request_cache, "
2481 "request_redirect, "
2482 "request_body_id, "
2483 "response_type, "
2484 "response_url, "
2485 "response_status, "
2486 "response_status_text, "
2487 "response_headers_guard, "
2488 "response_body_id, "
2489 "response_security_info_id, "
2490 "response_principal_info, "
2491 "cache_id "
2492 "FROM entries;"_ns));
2493
2494 // Remove the old table.
2495 QM_TRY(aConn.ExecuteSimpleSQL("DROP TABLE entries;"_ns));
2496
2497 // Rename new_entries to entries.
2498 QM_TRY(
2499 aConn.ExecuteSimpleSQL("ALTER TABLE new_entries RENAME to entries;"_ns));
2500
2501 // Now, recreate our indices.
2502 QM_TRY(aConn.ExecuteSimpleSQL(nsDependentCString(kIndexEntriesRequest)));
2503
2504 // Revalidate the foreign key constraints, and ensure that there are no
2505 // violations.
2506 QM_TRY_INSPECT(const bool& hasResult,
2507 quota::CreateAndExecuteSingleStepStatement<
2508 quota::SingleStepResult::ReturnNullIfNoResult>(
2509 aConn, "PRAGMA foreign_key_check;"_ns));
2510
2511 QM_TRY(OkIf(!hasResult), NS_ERROR_FAILURE);
2512
2513 QM_TRY(aConn.SetSchemaVersion(17));
2514
2515 return NS_OK;
2516 }
2517
MigrateFrom17To18(mozIStorageConnection & aConn,bool & aRewriteSchema)2518 nsresult MigrateFrom17To18(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2519 MOZ_ASSERT(!NS_IsMainThread());
2520
2521 // This migration is needed in order to remove "only-if-cached" RequestCache
2522 // values from the database. This enum value was removed from the spec in
2523 // https://github.com/whatwg/fetch/issues/39 but we unfortunately happily
2524 // accepted this value in the Request constructor.
2525 //
2526 // There is no good value to upgrade this to, so we just stick to "default".
2527
2528 static_assert(int(RequestCache::Default) == 0,
2529 "This is where the 0 below comes from!");
2530 QM_TRY(
2531 aConn.ExecuteSimpleSQL("UPDATE entries SET request_cache = 0 "
2532 "WHERE request_cache = 5;"_ns));
2533
2534 QM_TRY(aConn.SetSchemaVersion(18));
2535
2536 return NS_OK;
2537 }
2538
MigrateFrom18To19(mozIStorageConnection & aConn,bool & aRewriteSchema)2539 nsresult MigrateFrom18To19(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2540 MOZ_ASSERT(!NS_IsMainThread());
2541
2542 // This migration is needed in order to update the RequestMode values for
2543 // Request objects corresponding to a navigation content policy type to
2544 // "navigate".
2545
2546 static_assert(int(nsIContentPolicy::TYPE_DOCUMENT) == 6 &&
2547 int(nsIContentPolicy::TYPE_SUBDOCUMENT) == 7 &&
2548 int(nsIContentPolicy::TYPE_INTERNAL_FRAME) == 28 &&
2549 int(nsIContentPolicy::TYPE_INTERNAL_IFRAME) == 29 &&
2550 int(RequestMode::Navigate) == 3,
2551 "This is where the numbers below come from!");
2552 // 8 is former TYPE_REFRESH.
2553
2554 QM_TRY(aConn.ExecuteSimpleSQL(
2555 "UPDATE entries SET request_mode = 3 "
2556 "WHERE request_contentpolicytype IN (6, 7, 28, 29, 8);"_ns));
2557
2558 QM_TRY(aConn.SetSchemaVersion(19));
2559
2560 return NS_OK;
2561 }
2562
MigrateFrom19To20(mozIStorageConnection & aConn,bool & aRewriteSchema)2563 nsresult MigrateFrom19To20(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2564 MOZ_ASSERT(!NS_IsMainThread());
2565
2566 // Add the request_referrer_policy column with a default value of
2567 // "no-referrer-when-downgrade". Note, we only use a default value here
2568 // because its required by ALTER TABLE and we need to apply the default
2569 // "no-referrer-when-downgrade" to existing records in the table. We don't
2570 // actually want to keep the default in the schema for future INSERTs.
2571 QM_TRY(aConn.ExecuteSimpleSQL(
2572 "ALTER TABLE entries "
2573 "ADD COLUMN request_referrer_policy INTEGER NOT NULL DEFAULT 2"_ns));
2574
2575 QM_TRY(aConn.SetSchemaVersion(20));
2576
2577 aRewriteSchema = true;
2578
2579 return NS_OK;
2580 }
2581
MigrateFrom20To21(mozIStorageConnection & aConn,bool & aRewriteSchema)2582 nsresult MigrateFrom20To21(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2583 MOZ_ASSERT(!NS_IsMainThread());
2584
2585 // This migration creates response_url_list table to store response_url and
2586 // removes the response_url column from the entries table.
2587 // sqlite doesn't support removing a column from a table using ALTER TABLE,
2588 // so we need to create a new table without those columns, fill it up with the
2589 // existing data, and then drop the original table and rename the new one to
2590 // the old one.
2591
2592 // Create a new_entries table with the new fields as of version 21.
2593 QM_TRY(aConn.ExecuteSimpleSQL(
2594 "CREATE TABLE new_entries ("
2595 "id INTEGER NOT NULL PRIMARY KEY, "
2596 "request_method TEXT NOT NULL, "
2597 "request_url_no_query TEXT NOT NULL, "
2598 "request_url_no_query_hash BLOB NOT NULL, "
2599 "request_url_query TEXT NOT NULL, "
2600 "request_url_query_hash BLOB NOT NULL, "
2601 "request_referrer TEXT NOT NULL, "
2602 "request_headers_guard INTEGER NOT NULL, "
2603 "request_mode INTEGER NOT NULL, "
2604 "request_credentials INTEGER NOT NULL, "
2605 "request_contentpolicytype INTEGER NOT NULL, "
2606 "request_cache INTEGER NOT NULL, "
2607 "request_body_id TEXT NULL, "
2608 "response_type INTEGER NOT NULL, "
2609 "response_status INTEGER NOT NULL, "
2610 "response_status_text TEXT NOT NULL, "
2611 "response_headers_guard INTEGER NOT NULL, "
2612 "response_body_id TEXT NULL, "
2613 "response_security_info_id INTEGER NULL REFERENCES security_info(id), "
2614 "response_principal_info TEXT NOT NULL, "
2615 "cache_id INTEGER NOT NULL REFERENCES caches(id) ON DELETE CASCADE, "
2616 "request_redirect INTEGER NOT NULL, "
2617 "request_referrer_policy INTEGER NOT NULL"
2618 ")"_ns));
2619
2620 // Create a response_url_list table with the new fields as of version 21.
2621 QM_TRY(aConn.ExecuteSimpleSQL(
2622 "CREATE TABLE response_url_list ("
2623 "url TEXT NOT NULL, "
2624 "entry_id INTEGER NOT NULL REFERENCES entries(id) ON DELETE CASCADE"
2625 ")"_ns));
2626
2627 // Copy all of the data to the newly created entries table.
2628 QM_TRY(
2629 aConn.ExecuteSimpleSQL("INSERT INTO new_entries ("
2630 "id, "
2631 "request_method, "
2632 "request_url_no_query, "
2633 "request_url_no_query_hash, "
2634 "request_url_query, "
2635 "request_url_query_hash, "
2636 "request_referrer, "
2637 "request_headers_guard, "
2638 "request_mode, "
2639 "request_credentials, "
2640 "request_contentpolicytype, "
2641 "request_cache, "
2642 "request_redirect, "
2643 "request_referrer_policy, "
2644 "request_body_id, "
2645 "response_type, "
2646 "response_status, "
2647 "response_status_text, "
2648 "response_headers_guard, "
2649 "response_body_id, "
2650 "response_security_info_id, "
2651 "response_principal_info, "
2652 "cache_id "
2653 ") SELECT "
2654 "id, "
2655 "request_method, "
2656 "request_url_no_query, "
2657 "request_url_no_query_hash, "
2658 "request_url_query, "
2659 "request_url_query_hash, "
2660 "request_referrer, "
2661 "request_headers_guard, "
2662 "request_mode, "
2663 "request_credentials, "
2664 "request_contentpolicytype, "
2665 "request_cache, "
2666 "request_redirect, "
2667 "request_referrer_policy, "
2668 "request_body_id, "
2669 "response_type, "
2670 "response_status, "
2671 "response_status_text, "
2672 "response_headers_guard, "
2673 "response_body_id, "
2674 "response_security_info_id, "
2675 "response_principal_info, "
2676 "cache_id "
2677 "FROM entries;"_ns));
2678
2679 // Copy reponse_url to the newly created response_url_list table.
2680 QM_TRY(
2681 aConn.ExecuteSimpleSQL("INSERT INTO response_url_list ("
2682 "url, "
2683 "entry_id "
2684 ") SELECT "
2685 "response_url, "
2686 "id "
2687 "FROM entries;"_ns));
2688
2689 // Remove the old table.
2690 QM_TRY(aConn.ExecuteSimpleSQL("DROP TABLE entries;"_ns));
2691
2692 // Rename new_entries to entries.
2693 QM_TRY(
2694 aConn.ExecuteSimpleSQL("ALTER TABLE new_entries RENAME to entries;"_ns));
2695
2696 // Now, recreate our indices.
2697 QM_TRY(aConn.ExecuteSimpleSQL(nsLiteralCString(kIndexEntriesRequest)));
2698
2699 // Revalidate the foreign key constraints, and ensure that there are no
2700 // violations.
2701 QM_TRY_INSPECT(const bool& hasResult,
2702 quota::CreateAndExecuteSingleStepStatement<
2703 quota::SingleStepResult::ReturnNullIfNoResult>(
2704 aConn, "PRAGMA foreign_key_check;"_ns));
2705
2706 QM_TRY(OkIf(!hasResult), NS_ERROR_FAILURE);
2707
2708 QM_TRY(aConn.SetSchemaVersion(21));
2709
2710 aRewriteSchema = true;
2711
2712 return NS_OK;
2713 }
2714
MigrateFrom21To22(mozIStorageConnection & aConn,bool & aRewriteSchema)2715 nsresult MigrateFrom21To22(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2716 MOZ_ASSERT(!NS_IsMainThread());
2717
2718 // Add the request_integrity column.
2719 QM_TRY(aConn.ExecuteSimpleSQL(
2720 "ALTER TABLE entries "
2721 "ADD COLUMN request_integrity TEXT NOT NULL DEFAULT '';"_ns));
2722
2723 QM_TRY(
2724 aConn.ExecuteSimpleSQL("UPDATE entries SET request_integrity = '';"_ns));
2725
2726 QM_TRY(aConn.SetSchemaVersion(22));
2727
2728 aRewriteSchema = true;
2729
2730 return NS_OK;
2731 }
2732
MigrateFrom22To23(mozIStorageConnection & aConn,bool & aRewriteSchema)2733 nsresult MigrateFrom22To23(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2734 MOZ_ASSERT(!NS_IsMainThread());
2735
2736 // The only change between 22 and 23 was a different snappy compression
2737 // format, but it's backwards-compatible.
2738 QM_TRY(aConn.SetSchemaVersion(23));
2739
2740 return NS_OK;
2741 }
2742
MigrateFrom23To24(mozIStorageConnection & aConn,bool & aRewriteSchema)2743 nsresult MigrateFrom23To24(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2744 MOZ_ASSERT(!NS_IsMainThread());
2745
2746 // Add the request_url_fragment column.
2747 QM_TRY(aConn.ExecuteSimpleSQL(
2748 "ALTER TABLE entries "
2749 "ADD COLUMN request_url_fragment TEXT NOT NULL DEFAULT ''"_ns));
2750
2751 QM_TRY(aConn.SetSchemaVersion(24));
2752
2753 aRewriteSchema = true;
2754
2755 return NS_OK;
2756 }
2757
MigrateFrom24To25(mozIStorageConnection & aConn,bool & aRewriteSchema)2758 nsresult MigrateFrom24To25(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2759 MOZ_ASSERT(!NS_IsMainThread());
2760
2761 // The only change between 24 and 25 was a new nsIContentPolicy type.
2762 QM_TRY(aConn.SetSchemaVersion(25));
2763
2764 return NS_OK;
2765 }
2766
MigrateFrom25To26(mozIStorageConnection & aConn,bool & aRewriteSchema)2767 nsresult MigrateFrom25To26(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2768 MOZ_ASSERT(!NS_IsMainThread());
2769
2770 // Add the response_padding_size column.
2771 // Note: only opaque repsonse should be non-null interger.
2772 QM_TRY(aConn.ExecuteSimpleSQL(
2773 "ALTER TABLE entries "
2774 "ADD COLUMN response_padding_size INTEGER NULL "_ns));
2775
2776 QM_TRY(
2777 aConn.ExecuteSimpleSQL("UPDATE entries SET response_padding_size = 0 "
2778 "WHERE response_type = 4"_ns // opaque response
2779 ));
2780
2781 QM_TRY(aConn.SetSchemaVersion(26));
2782
2783 aRewriteSchema = true;
2784
2785 return NS_OK;
2786 }
2787
MigrateFrom26To27(mozIStorageConnection & aConn,bool & aRewriteSchema)2788 nsresult MigrateFrom26To27(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2789 MOZ_ASSERT(!NS_IsMainThread());
2790
2791 QM_TRY(aConn.SetSchemaVersion(kHackyDowngradeSchemaVersion));
2792
2793 return NS_OK;
2794 }
2795
MigrateFrom27To28(mozIStorageConnection & aConn,bool & aRewriteSchema)2796 nsresult MigrateFrom27To28(mozIStorageConnection& aConn, bool& aRewriteSchema) {
2797 MOZ_ASSERT(!NS_IsMainThread());
2798
2799 // In Bug 1264178, we added a column request_integrity into table entries.
2800 // However, at that time, the default value for the existing rows is NULL
2801 // which against the statement in kTableEntries. Thus, we need to have another
2802 // upgrade to update these values to an empty string.
2803 QM_TRY(
2804 aConn.ExecuteSimpleSQL("UPDATE entries SET request_integrity = '' "
2805 "WHERE request_integrity is NULL;"_ns));
2806
2807 QM_TRY(aConn.SetSchemaVersion(28));
2808
2809 return NS_OK;
2810 }
2811
2812 } // anonymous namespace
2813 } // namespace mozilla::dom::cache::db
2814