summaryrefslogtreecommitdiffstats
path: root/dom/quota
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /dom/quota
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/quota')
-rw-r--r--dom/quota/ActorsChild.cpp408
-rw-r--r--dom/quota/ActorsChild.h158
-rw-r--r--dom/quota/ActorsParent.cpp10856
-rw-r--r--dom/quota/ActorsParent.h26
-rw-r--r--dom/quota/Assertions.cpp36
-rw-r--r--dom/quota/Assertions.h30
-rw-r--r--dom/quota/AssertionsImpl.h52
-rw-r--r--dom/quota/CachingDatabaseConnection.cpp193
-rw-r--r--dom/quota/CachingDatabaseConnection.h238
-rw-r--r--dom/quota/CanonicalQuotaObject.cpp331
-rw-r--r--dom/quota/CanonicalQuotaObject.h90
-rw-r--r--dom/quota/CheckedUnsafePtr.h405
-rw-r--r--dom/quota/CipherStrategy.h42
-rw-r--r--dom/quota/Client.cpp299
-rw-r--r--dom/quota/Client.h191
-rw-r--r--dom/quota/ClientImpl.h36
-rw-r--r--dom/quota/ClientUsageArray.cpp55
-rw-r--r--dom/quota/ClientUsageArray.h35
-rw-r--r--dom/quota/CommonMetadata.h72
-rw-r--r--dom/quota/Config.h32
-rw-r--r--dom/quota/DecryptingInputStream.cpp98
-rw-r--r--dom/quota/DecryptingInputStream.h163
-rw-r--r--dom/quota/DecryptingInputStream_impl.h520
-rw-r--r--dom/quota/DirectoryLock.h100
-rw-r--r--dom/quota/DirectoryLockImpl.cpp372
-rw-r--r--dom/quota/DirectoryLockImpl.h273
-rw-r--r--dom/quota/DummyCipherStrategy.h56
-rw-r--r--dom/quota/EncryptedBlock.h93
-rw-r--r--dom/quota/EncryptingOutputStream.cpp64
-rw-r--r--dom/quota/EncryptingOutputStream.h103
-rw-r--r--dom/quota/EncryptingOutputStream_impl.h246
-rw-r--r--dom/quota/FileStreams.cpp200
-rw-r--r--dom/quota/FileStreams.h176
-rw-r--r--dom/quota/FirstInitializationAttempts.h69
-rw-r--r--dom/quota/FirstInitializationAttemptsImpl.h36
-rw-r--r--dom/quota/Flatten.h118
-rw-r--r--dom/quota/ForwardDecls.h37
-rw-r--r--dom/quota/GroupInfo.cpp89
-rw-r--r--dom/quota/GroupInfo.h71
-rw-r--r--dom/quota/GroupInfoPair.cpp26
-rw-r--r--dom/quota/GroupInfoPair.h79
-rw-r--r--dom/quota/IPCQuotaObject.ipdlh20
-rw-r--r--dom/quota/IPCStreamCipherStrategy.h16
-rw-r--r--dom/quota/InitializationTypes.cpp57
-rw-r--r--dom/quota/InitializationTypes.h79
-rw-r--r--dom/quota/NSSCipherStrategy.cpp152
-rw-r--r--dom/quota/NSSCipherStrategy.h56
-rw-r--r--dom/quota/OriginInfo.cpp167
-rw-r--r--dom/quota/OriginInfo.h138
-rw-r--r--dom/quota/OriginScope.h344
-rw-r--r--dom/quota/PQuota.ipdl180
-rw-r--r--dom/quota/PQuotaRequest.ipdl127
-rw-r--r--dom/quota/PQuotaUsageRequest.ipdl51
-rw-r--r--dom/quota/PRemoteQuotaObject.ipdl22
-rw-r--r--dom/quota/PersistenceType.cpp202
-rw-r--r--dom/quota/PersistenceType.h69
-rw-r--r--dom/quota/QMResult.cpp26
-rw-r--r--dom/quota/QMResult.h58
-rw-r--r--dom/quota/QuotaCommon.cpp639
-rw-r--r--dom/quota/QuotaCommon.h1675
-rw-r--r--dom/quota/QuotaManager.h676
-rw-r--r--dom/quota/QuotaManagerImpl.h25
-rw-r--r--dom/quota/QuotaManagerService.cpp1046
-rw-r--r--dom/quota/QuotaManagerService.h102
-rw-r--r--dom/quota/QuotaObject.cpp73
-rw-r--r--dom/quota/QuotaObject.h70
-rw-r--r--dom/quota/QuotaRequests.cpp288
-rw-r--r--dom/quota/QuotaRequests.h121
-rw-r--r--dom/quota/QuotaResults.cpp144
-rw-r--r--dom/quota/QuotaResults.h79
-rw-r--r--dom/quota/RemoteQuotaObject.cpp65
-rw-r--r--dom/quota/RemoteQuotaObject.h47
-rw-r--r--dom/quota/RemoteQuotaObjectChild.cpp33
-rw-r--r--dom/quota/RemoteQuotaObjectChild.h35
-rw-r--r--dom/quota/RemoteQuotaObjectParent.cpp45
-rw-r--r--dom/quota/RemoteQuotaObjectParent.h39
-rw-r--r--dom/quota/RemoteQuotaObjectParentTracker.h45
-rw-r--r--dom/quota/RemoveParen.h18
-rw-r--r--dom/quota/ResultExtensions.h160
-rw-r--r--dom/quota/ScopedLogExtraInfo.cpp80
-rw-r--r--dom/quota/ScopedLogExtraInfo.h75
-rw-r--r--dom/quota/SerializationHelpers.h79
-rw-r--r--dom/quota/StorageHelpers.cpp70
-rw-r--r--dom/quota/StorageHelpers.h54
-rw-r--r--dom/quota/StorageManager.cpp808
-rw-r--r--dom/quota/StorageManager.h68
-rw-r--r--dom/quota/UsageInfo.h103
-rw-r--r--dom/quota/components.conf23
-rw-r--r--dom/quota/moz.build120
-rw-r--r--dom/quota/nsIQuotaCallbacks.idl22
-rw-r--r--dom/quota/nsIQuotaManagerService.idl296
-rw-r--r--dom/quota/nsIQuotaRequests.idl49
-rw-r--r--dom/quota/nsIQuotaResults.idl51
-rw-r--r--dom/quota/nsIndexedDBProtocolHandler.cpp44
-rw-r--r--dom/quota/nsIndexedDBProtocolHandler.h26
-rwxr-xr-xdom/quota/scripts/analyze_qm_failures.py137
-rwxr-xr-xdom/quota/scripts/fetch_fn_names.sh17
-rwxr-xr-xdom/quota/scripts/fetch_qm_failures.py142
-rw-r--r--dom/quota/scripts/fn_anchors.py68
-rw-r--r--dom/quota/scripts/stackanalysis.py396
-rw-r--r--dom/quota/scripts/telemetry.py54
-rw-r--r--dom/quota/scripts/utils.py89
-rw-r--r--dom/quota/test/browser/browser.ini13
-rw-r--r--dom/quota/test/browser/browser_permissionsCrossOrigin.js56
-rw-r--r--dom/quota/test/browser/browser_permissionsPromptAllow.js66
-rw-r--r--dom/quota/test/browser/browser_permissionsPromptDeny.js150
-rw-r--r--dom/quota/test/browser/browser_permissionsPromptUnknown.js53
-rw-r--r--dom/quota/test/browser/browser_simpledb.js51
-rw-r--r--dom/quota/test/browser/empty.html8
-rw-r--r--dom/quota/test/browser/head.js149
-rw-r--r--dom/quota/test/browser/helpers.js46
-rw-r--r--dom/quota/test/browser/permissionsPrompt.html34
-rw-r--r--dom/quota/test/common/browser.js34
-rw-r--r--dom/quota/test/common/content.js62
-rw-r--r--dom/quota/test/common/file.js45
-rw-r--r--dom/quota/test/common/global.js47
-rw-r--r--dom/quota/test/common/mochitest.js19
-rw-r--r--dom/quota/test/common/nestedtest.js6
-rw-r--r--dom/quota/test/common/system.js74
-rw-r--r--dom/quota/test/common/test_simpledb.js50
-rw-r--r--dom/quota/test/common/test_storage_manager_persist_allow.js30
-rw-r--r--dom/quota/test/common/test_storage_manager_persist_deny.js34
-rw-r--r--dom/quota/test/common/test_storage_manager_persisted.js13
-rw-r--r--dom/quota/test/common/xpcshell.js91
-rw-r--r--dom/quota/test/gtest/Common.cpp28
-rw-r--r--dom/quota/test/gtest/Common.h32
-rw-r--r--dom/quota/test/gtest/PQuotaTest.ipdl26
-rw-r--r--dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp160
-rw-r--r--dom/quota/test/gtest/QuotaManagerDependencyFixture.h87
-rw-r--r--dom/quota/test/gtest/QuotaTestChild.h23
-rw-r--r--dom/quota/test/gtest/QuotaTestParent.h36
-rw-r--r--dom/quota/test/gtest/TestCheckedUnsafePtr.cpp145
-rw-r--r--dom/quota/test/gtest/TestClientUsageArray.cpp17
-rw-r--r--dom/quota/test/gtest/TestEncryptedStream.cpp786
-rw-r--r--dom/quota/test/gtest/TestFileOutputStream.cpp182
-rw-r--r--dom/quota/test/gtest/TestFlatten.cpp81
-rw-r--r--dom/quota/test/gtest/TestForwardDecls.cpp12
-rw-r--r--dom/quota/test/gtest/TestPersistenceType.cpp44
-rw-r--r--dom/quota/test/gtest/TestQMResult.cpp72
-rw-r--r--dom/quota/test/gtest/TestQuotaCommon.cpp2171
-rw-r--r--dom/quota/test/gtest/TestQuotaManager.cpp181
-rw-r--r--dom/quota/test/gtest/TestResultExtensions.cpp333
-rw-r--r--dom/quota/test/gtest/TestScopedLogExtraInfo.cpp65
-rw-r--r--dom/quota/test/gtest/TestUsageInfo.cpp136
-rw-r--r--dom/quota/test/gtest/moz.build44
-rw-r--r--dom/quota/test/mochitest/helpers.js312
-rw-r--r--dom/quota/test/mochitest/mochitest.ini16
-rw-r--r--dom/quota/test/mochitest/test_simpledb.html21
-rw-r--r--dom/quota/test/mochitest/test_storage_manager_persist_allow.html21
-rw-r--r--dom/quota/test/mochitest/test_storage_manager_persist_deny.html21
-rw-r--r--dom/quota/test/mochitest/test_storage_manager_persisted.html24
-rw-r--r--dom/quota/test/modules/content/.eslintrc.js22
-rw-r--r--dom/quota/test/modules/content/Assert.js10
-rw-r--r--dom/quota/test/modules/content/ModuleLoader.js61
-rw-r--r--dom/quota/test/modules/content/StorageUtils.js48
-rw-r--r--dom/quota/test/modules/content/WorkerDriver.js53
-rw-r--r--dom/quota/test/modules/content/worker/.eslintrc.js21
-rw-r--r--dom/quota/test/modules/content/worker/Assert.js22
-rw-r--r--dom/quota/test/modules/content/worker/ModuleLoader.js52
-rw-r--r--dom/quota/test/modules/content/worker/head.js55
-rw-r--r--dom/quota/test/modules/system/ModuleLoader.sys.mjs63
-rw-r--r--dom/quota/test/modules/system/StorageUtils.sys.mjs65
-rw-r--r--dom/quota/test/modules/system/WorkerDriver.sys.mjs52
-rw-r--r--dom/quota/test/modules/system/worker/.eslintrc.js21
-rw-r--r--dom/quota/test/modules/system/worker/Assert.js22
-rw-r--r--dom/quota/test/modules/system/worker/ModuleLoader.js52
-rw-r--r--dom/quota/test/modules/system/worker/head.js56
-rw-r--r--dom/quota/test/moz.build76
-rw-r--r--dom/quota/test/xpcshell/basics_profile.zipbin0 -> 3405 bytes
-rw-r--r--dom/quota/test/xpcshell/caching/groupMismatch_profile.zipbin0 -> 2576 bytes
-rw-r--r--dom/quota/test/xpcshell/caching/head.js14
-rw-r--r--dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js25
-rw-r--r--dom/quota/test/xpcshell/caching/removedOrigin_profile.zipbin0 -> 1551 bytes
-rw-r--r--dom/quota/test/xpcshell/caching/test_groupMismatch.js45
-rw-r--r--dom/quota/test/xpcshell/caching/test_removedOrigin.js61
-rw-r--r--dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js46
-rw-r--r--dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zipbin0 -> 1574 bytes
-rw-r--r--dom/quota/test/xpcshell/caching/xpcshell.ini17
-rw-r--r--dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zipbin0 -> 7380 bytes
-rw-r--r--dom/quota/test/xpcshell/common/head.js629
-rw-r--r--dom/quota/test/xpcshell/common/utils.js47
-rw-r--r--dom/quota/test/xpcshell/createLocalStorage_profile.zipbin0 -> 1006 bytes
-rw-r--r--dom/quota/test/xpcshell/defaultStorageDirectory_shared.json109
-rw-r--r--dom/quota/test/xpcshell/defaultStorageDirectory_shared.zipbin0 -> 1804 bytes
-rw-r--r--dom/quota/test/xpcshell/getUsage_profile.zipbin0 -> 24717 bytes
-rw-r--r--dom/quota/test/xpcshell/groupMismatch_profile.zipbin0 -> 1706 bytes
-rw-r--r--dom/quota/test/xpcshell/head.js14
-rw-r--r--dom/quota/test/xpcshell/indexedDBDirectory_shared.json35
-rw-r--r--dom/quota/test/xpcshell/indexedDBDirectory_shared.zipbin0 -> 524 bytes
-rw-r--r--dom/quota/test/xpcshell/make_unknownFiles.js172
-rw-r--r--dom/quota/test/xpcshell/make_unsetLastAccessTime.js25
-rw-r--r--dom/quota/test/xpcshell/originMismatch_profile.json74
-rw-r--r--dom/quota/test/xpcshell/originMismatch_profile.zipbin0 -> 3104 bytes
-rw-r--r--dom/quota/test/xpcshell/persistentStorageDirectory_shared.json57
-rw-r--r--dom/quota/test/xpcshell/persistentStorageDirectory_shared.zipbin0 -> 1272 bytes
-rw-r--r--dom/quota/test/xpcshell/removeLocalStorage1_profile.zipbin0 -> 1187 bytes
-rw-r--r--dom/quota/test/xpcshell/removeLocalStorage2_profile.zipbin0 -> 2827 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/head.js14
-rw-r--r--dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js867
-rw-r--r--dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zipbin0 -> 1978 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version0_0_profile.zipbin0 -> 1292 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zipbin0 -> 1978 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version1_0_profile.zipbin0 -> 1475 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zipbin0 -> 1978 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_0_profile.zipbin0 -> 1473 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zipbin0 -> 1978 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_1_profile.zipbin0 -> 1472 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zipbin0 -> 299 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/version2_2_profile.zipbin0 -> 1507 bytes
-rw-r--r--dom/quota/test/xpcshell/telemetry/xpcshell.ini20
-rw-r--r--dom/quota/test/xpcshell/tempMetadataCleanup_profile.zipbin0 -> 1569 bytes
-rw-r--r--dom/quota/test/xpcshell/test_allowListFiles.js61
-rw-r--r--dom/quota/test/xpcshell/test_bad_origin_directory.js36
-rw-r--r--dom/quota/test/xpcshell/test_basics.js143
-rw-r--r--dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js58
-rw-r--r--dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js56
-rw-r--r--dom/quota/test/xpcshell/test_createLocalStorage.js155
-rw-r--r--dom/quota/test/xpcshell/test_estimateOrigin.js80
-rw-r--r--dom/quota/test/xpcshell/test_getUsage.js129
-rw-r--r--dom/quota/test/xpcshell/test_groupMismatch.js74
-rw-r--r--dom/quota/test/xpcshell/test_initTemporaryStorage.js49
-rw-r--r--dom/quota/test/xpcshell/test_listOrigins.js80
-rw-r--r--dom/quota/test/xpcshell/test_originEndsWithDot.js70
-rw-r--r--dom/quota/test/xpcshell/test_originMismatch.js75
-rw-r--r--dom/quota/test/xpcshell/test_originWithCaret.js17
-rw-r--r--dom/quota/test/xpcshell/test_orpahnedQuotaObject.js44
-rw-r--r--dom/quota/test/xpcshell/test_persist.js121
-rw-r--r--dom/quota/test/xpcshell/test_persist_eviction.js82
-rw-r--r--dom/quota/test/xpcshell/test_persist_globalLimit.js82
-rw-r--r--dom/quota/test/xpcshell/test_persist_groupLimit.js104
-rw-r--r--dom/quota/test/xpcshell/test_removeLocalStorage.js89
-rw-r--r--dom/quota/test/xpcshell/test_simpledb.js6
-rw-r--r--dom/quota/test/xpcshell/test_specialOrigins.js55
-rw-r--r--dom/quota/test/xpcshell/test_storagePressure.js135
-rw-r--r--dom/quota/test/xpcshell/test_tempMetadataCleanup.js45
-rw-r--r--dom/quota/test/xpcshell/test_unaccessedOrigins.js168
-rw-r--r--dom/quota/test/xpcshell/test_unknownFiles.js106
-rw-r--r--dom/quota/test/xpcshell/test_unsetLastAccessTime.js68
-rw-r--r--dom/quota/test/xpcshell/test_validOrigins.js99
-rw-r--r--dom/quota/test/xpcshell/unknownFiles_profile.zipbin0 -> 13582 bytes
-rw-r--r--dom/quota/test/xpcshell/unsetLastAccessTime_profile.zipbin0 -> 1574 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json64
-rw-r--r--dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zipbin0 -> 2675 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/head.js14
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json63
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zipbin0 -> 1336 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json55
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zipbin0 -> 812 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json65
-rw-r--r--dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zipbin0 -> 1508 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zipbin0 -> 7675 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zipbin0 -> 7853 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zipbin0 -> 7799 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json63
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zipbin0 -> 1413 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json64
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zipbin0 -> 1028 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json92
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zipbin0 -> 2720 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json382
-rw-r--r--dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zipbin0 -> 12717 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js65
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js107
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js66
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js79
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js187
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js121
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js86
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js378
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js102
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js162
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js158
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js43
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js101
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js60
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js179
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js97
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js85
-rw-r--r--dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js64
-rw-r--r--dom/quota/test/xpcshell/upgrades/version0_0_profile.json88
-rw-r--r--dom/quota/test/xpcshell/upgrades/version0_0_profile.zipbin0 -> 2684 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json72
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zipbin0 -> 3145 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json73
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zipbin0 -> 3944 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json57
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zipbin0 -> 1438 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json112
-rw-r--r--dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zipbin0 -> 4843 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_0_profile.json105
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_0_profile.zipbin0 -> 5119 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_1_profile.json69
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_1_profile.zipbin0 -> 2400 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_2_profile.json18
-rw-r--r--dom/quota/test/xpcshell/upgrades/version2_2_profile.zipbin0 -> 237 bytes
-rw-r--r--dom/quota/test/xpcshell/upgrades/xpcshell.ini61
-rw-r--r--dom/quota/test/xpcshell/xpcshell.ini61
297 files changed, 41868 insertions, 0 deletions
diff --git a/dom/quota/ActorsChild.cpp b/dom/quota/ActorsChild.cpp
new file mode 100644
index 0000000000..071d13214c
--- /dev/null
+++ b/dom/quota/ActorsChild.cpp
@@ -0,0 +1,408 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ActorsChild.h"
+
+// Local includes
+#include "QuotaManagerService.h"
+#include "QuotaRequests.h"
+#include "QuotaResults.h"
+
+// Global includes
+#include <new>
+#include <utility>
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/quota/PQuotaRequest.h"
+#include "mozilla/dom/quota/PQuotaUsageRequest.h"
+#include "nsError.h"
+#include "nsID.h"
+#include "nsIEventTarget.h"
+#include "nsIQuotaResults.h"
+#include "nsISupports.h"
+#include "nsIVariant.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsVariant.h"
+
+namespace mozilla::dom::quota {
+
+/*******************************************************************************
+ * QuotaChild
+ ******************************************************************************/
+
+QuotaChild::QuotaChild(QuotaManagerService* aService)
+ : mService(aService)
+#ifdef DEBUG
+ ,
+ mOwningThread(GetCurrentEventTarget())
+#endif
+{
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aService);
+
+ MOZ_COUNT_CTOR(quota::QuotaChild);
+}
+
+QuotaChild::~QuotaChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_DTOR(quota::QuotaChild);
+}
+
+#ifdef DEBUG
+
+void QuotaChild::AssertIsOnOwningThread() const {
+ MOZ_ASSERT(mOwningThread);
+
+ bool current;
+ MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(&current)));
+ MOZ_ASSERT(current);
+}
+
+#endif // DEBUG
+
+void QuotaChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mService) {
+ mService->ClearBackgroundActor();
+#ifdef DEBUG
+ mService = nullptr;
+#endif
+ }
+}
+
+PQuotaUsageRequestChild* QuotaChild::AllocPQuotaUsageRequestChild(
+ const UsageRequestParams& aParams) {
+ AssertIsOnOwningThread();
+
+ MOZ_CRASH("PQuotaUsageRequestChild actors should be manually constructed!");
+}
+
+bool QuotaChild::DeallocPQuotaUsageRequestChild(
+ PQuotaUsageRequestChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+
+ delete static_cast<QuotaUsageRequestChild*>(aActor);
+ return true;
+}
+
+PQuotaRequestChild* QuotaChild::AllocPQuotaRequestChild(
+ const RequestParams& aParams) {
+ AssertIsOnOwningThread();
+
+ MOZ_CRASH("PQuotaRequestChild actors should be manually constructed!");
+}
+
+bool QuotaChild::DeallocPQuotaRequestChild(PQuotaRequestChild* aActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aActor);
+
+ delete static_cast<QuotaRequestChild*>(aActor);
+ return true;
+}
+
+/*******************************************************************************
+ * QuotaUsageRequestChild
+ ******************************************************************************/
+
+QuotaUsageRequestChild::QuotaUsageRequestChild(UsageRequest* aRequest)
+ : mRequest(aRequest) {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_CTOR(quota::QuotaUsageRequestChild);
+}
+
+QuotaUsageRequestChild::~QuotaUsageRequestChild() {
+ // Can't assert owning thread here because the request is cleared.
+
+ MOZ_COUNT_DTOR(quota::QuotaUsageRequestChild);
+}
+
+#ifdef DEBUG
+
+void QuotaUsageRequestChild::AssertIsOnOwningThread() const {
+ MOZ_ASSERT(mRequest);
+ mRequest->AssertIsOnOwningThread();
+}
+
+#endif // DEBUG
+
+void QuotaUsageRequestChild::HandleResponse(nsresult aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(aResponse));
+ MOZ_ASSERT(mRequest);
+
+ mRequest->SetError(aResponse);
+}
+
+void QuotaUsageRequestChild::HandleResponse(
+ const nsTArray<OriginUsage>& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+
+ if (aResponse.IsEmpty()) {
+ variant->SetAsEmptyArray();
+ } else {
+ nsTArray<RefPtr<UsageResult>> usageResults(aResponse.Length());
+
+ for (const auto& originUsage : aResponse) {
+ usageResults.AppendElement(MakeRefPtr<UsageResult>(
+ originUsage.origin(), originUsage.persisted(), originUsage.usage(),
+ originUsage.lastAccessed()));
+ }
+
+ variant->SetAsArray(nsIDataType::VTYPE_INTERFACE_IS,
+ &NS_GET_IID(nsIQuotaUsageResult), usageResults.Length(),
+ static_cast<void*>(usageResults.Elements()));
+ }
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaUsageRequestChild::HandleResponse(
+ const OriginUsageResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<OriginUsageResult> result =
+ new OriginUsageResult(aResponse.usage(), aResponse.fileUsage());
+
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsInterface(NS_GET_IID(nsIQuotaOriginUsageResult), result);
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaUsageRequestChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ if (mRequest) {
+ mRequest->ClearBackgroundActor();
+#ifdef DEBUG
+ mRequest = nullptr;
+#endif
+ }
+}
+
+mozilla::ipc::IPCResult QuotaUsageRequestChild::Recv__delete__(
+ const UsageRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ switch (aResponse.type()) {
+ case UsageRequestResponse::Tnsresult:
+ HandleResponse(aResponse.get_nsresult());
+ break;
+
+ case UsageRequestResponse::TAllUsageResponse:
+ HandleResponse(aResponse.get_AllUsageResponse().originUsages());
+ break;
+
+ case UsageRequestResponse::TOriginUsageResponse:
+ HandleResponse(aResponse.get_OriginUsageResponse());
+ break;
+
+ default:
+ MOZ_CRASH("Unknown response type!");
+ }
+
+ return IPC_OK();
+}
+
+/*******************************************************************************
+ * QuotaRequestChild
+ ******************************************************************************/
+
+QuotaRequestChild::QuotaRequestChild(Request* aRequest) : mRequest(aRequest) {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_CTOR(quota::QuotaRequestChild);
+}
+
+QuotaRequestChild::~QuotaRequestChild() {
+ AssertIsOnOwningThread();
+
+ MOZ_COUNT_DTOR(quota::QuotaRequestChild);
+}
+
+#ifdef DEBUG
+
+void QuotaRequestChild::AssertIsOnOwningThread() const {
+ MOZ_ASSERT(mRequest);
+ mRequest->AssertIsOnOwningThread();
+}
+
+#endif // DEBUG
+
+void QuotaRequestChild::HandleResponse(nsresult aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(NS_FAILED(aResponse));
+ MOZ_ASSERT(mRequest);
+
+ mRequest->SetError(aResponse);
+}
+
+void QuotaRequestChild::HandleResponse() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsVoid();
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::HandleResponse(bool aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsBool(aResponse);
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::HandleResponse(const nsAString& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsAString(aResponse);
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::HandleResponse(const EstimateResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<EstimateResult> result =
+ new EstimateResult(aResponse.usage(), aResponse.limit());
+
+ RefPtr<nsVariant> variant = new nsVariant();
+ variant->SetAsInterface(NS_GET_IID(nsIQuotaEstimateResult), result);
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::HandleResponse(const nsTArray<nsCString>& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+
+ if (aResponse.IsEmpty()) {
+ variant->SetAsEmptyArray();
+ } else {
+ nsTArray<const char*> stringPointers(aResponse.Length());
+ std::transform(aResponse.cbegin(), aResponse.cend(),
+ MakeBackInserter(stringPointers),
+ std::mem_fn(&nsCString::get));
+
+ variant->SetAsArray(nsIDataType::VTYPE_CHAR_STR, nullptr,
+ stringPointers.Length(), stringPointers.Elements());
+ }
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::HandleResponse(
+ const GetFullOriginMetadataResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ RefPtr<nsVariant> variant = new nsVariant();
+
+ if (aResponse.maybeFullOriginMetadata()) {
+ RefPtr<FullOriginMetadataResult> result =
+ new FullOriginMetadataResult(*aResponse.maybeFullOriginMetadata());
+
+ variant->SetAsInterface(NS_GET_IID(nsIQuotaFullOriginMetadataResult),
+ result);
+
+ } else {
+ variant->SetAsVoid();
+ }
+
+ mRequest->SetResult(variant);
+}
+
+void QuotaRequestChild::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+}
+
+mozilla::ipc::IPCResult QuotaRequestChild::Recv__delete__(
+ const RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mRequest);
+
+ switch (aResponse.type()) {
+ case RequestResponse::Tnsresult:
+ HandleResponse(aResponse.get_nsresult());
+ break;
+
+ case RequestResponse::TStorageNameResponse:
+ HandleResponse(aResponse.get_StorageNameResponse().name());
+ break;
+
+ case RequestResponse::TStorageInitializedResponse:
+ HandleResponse(aResponse.get_StorageInitializedResponse().initialized());
+ break;
+
+ case RequestResponse::TTemporaryStorageInitializedResponse:
+ HandleResponse(
+ aResponse.get_TemporaryStorageInitializedResponse().initialized());
+ break;
+
+ case RequestResponse::TInitResponse:
+ case RequestResponse::TInitTemporaryStorageResponse:
+ case RequestResponse::TClearOriginResponse:
+ case RequestResponse::TResetOriginResponse:
+ case RequestResponse::TClearDataResponse:
+ case RequestResponse::TClearAllResponse:
+ case RequestResponse::TResetAllResponse:
+ case RequestResponse::TPersistResponse:
+ HandleResponse();
+ break;
+
+ case RequestResponse::TInitializePersistentOriginResponse:
+ HandleResponse(
+ aResponse.get_InitializePersistentOriginResponse().created());
+ break;
+
+ case RequestResponse::TInitializeTemporaryOriginResponse:
+ HandleResponse(
+ aResponse.get_InitializeTemporaryOriginResponse().created());
+ break;
+
+ case RequestResponse::TGetFullOriginMetadataResponse:
+ HandleResponse(aResponse.get_GetFullOriginMetadataResponse());
+ break;
+
+ case RequestResponse::TPersistedResponse:
+ HandleResponse(aResponse.get_PersistedResponse().persisted());
+ break;
+
+ case RequestResponse::TEstimateResponse:
+ HandleResponse(aResponse.get_EstimateResponse());
+ break;
+
+ case RequestResponse::TListOriginsResponse:
+ HandleResponse(aResponse.get_ListOriginsResponse().origins());
+ break;
+
+ default:
+ MOZ_CRASH("Unknown response type!");
+ }
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/ActorsChild.h b/dom/quota/ActorsChild.h
new file mode 100644
index 0000000000..df1881b234
--- /dev/null
+++ b/dom/quota/ActorsChild.h
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_ActorsChild_h
+#define mozilla_dom_quota_ActorsChild_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/quota/PQuotaChild.h"
+#include "mozilla/dom/quota/PQuotaRequestChild.h"
+#include "mozilla/dom/quota/PQuotaUsageRequestChild.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "nsCOMPtr.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+class nsIEventTarget;
+
+namespace mozilla {
+namespace ipc {
+
+class BackgroundChildImpl;
+
+} // namespace ipc
+
+namespace dom::quota {
+
+class QuotaManagerService;
+class Request;
+class UsageRequest;
+
+class QuotaChild final : public PQuotaChild {
+ friend class mozilla::ipc::BackgroundChildImpl;
+ friend class QuotaManagerService;
+
+ QuotaManagerService* mService;
+
+#ifdef DEBUG
+ nsCOMPtr<nsIEventTarget> mOwningThread;
+#endif
+
+ public:
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ private:
+ // Only created by QuotaManagerService.
+ explicit QuotaChild(QuotaManagerService* aService);
+
+ // Only destroyed by mozilla::ipc::BackgroundChildImpl.
+ ~QuotaChild();
+
+ // IPDL methods are only called by IPDL.
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual PQuotaUsageRequestChild* AllocPQuotaUsageRequestChild(
+ const UsageRequestParams& aParams) override;
+
+ virtual bool DeallocPQuotaUsageRequestChild(
+ PQuotaUsageRequestChild* aActor) override;
+
+ virtual PQuotaRequestChild* AllocPQuotaRequestChild(
+ const RequestParams& aParams) override;
+
+ virtual bool DeallocPQuotaRequestChild(PQuotaRequestChild* aActor) override;
+};
+
+class QuotaUsageRequestChild final : public PQuotaUsageRequestChild {
+ friend class QuotaChild;
+ friend class QuotaManagerService;
+
+ RefPtr<UsageRequest> mRequest;
+
+ public:
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ private:
+ // Only created by QuotaManagerService.
+ explicit QuotaUsageRequestChild(UsageRequest* aRequest);
+
+ // Only destroyed by QuotaChild.
+ ~QuotaUsageRequestChild();
+
+ void HandleResponse(nsresult aResponse);
+
+ void HandleResponse(const nsTArray<OriginUsage>& aResponse);
+
+ void HandleResponse(const OriginUsageResponse& aResponse);
+
+ // IPDL methods are only called by IPDL.
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual mozilla::ipc::IPCResult Recv__delete__(
+ const UsageRequestResponse& aResponse) override;
+};
+
+class QuotaRequestChild final : public PQuotaRequestChild {
+ friend class QuotaChild;
+ friend class QuotaManagerService;
+
+ RefPtr<Request> mRequest;
+
+ public:
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ private:
+ // Only created by QuotaManagerService.
+ explicit QuotaRequestChild(Request* aRequest);
+
+ // Only destroyed by QuotaChild.
+ ~QuotaRequestChild();
+
+ void HandleResponse(nsresult aResponse);
+
+ void HandleResponse();
+
+ void HandleResponse(bool aResponse);
+
+ void HandleResponse(const nsAString& aResponse);
+
+ void HandleResponse(const EstimateResponse& aResponse);
+
+ void HandleResponse(const nsTArray<nsCString>& aResponse);
+
+ void HandleResponse(const GetFullOriginMetadataResponse& aResponse);
+
+ // IPDL methods are only called by IPDL.
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual mozilla::ipc::IPCResult Recv__delete__(
+ const RequestResponse& aResponse) override;
+};
+
+} // namespace dom::quota
+} // namespace mozilla
+
+#endif // mozilla_dom_quota_ActorsChild_h
diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp
new file mode 100644
index 0000000000..ec1507b05d
--- /dev/null
+++ b/dom/quota/ActorsParent.cpp
@@ -0,0 +1,10856 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ActorsParent.h"
+
+// Local includes
+#include "CanonicalQuotaObject.h"
+#include "ClientUsageArray.h"
+#include "Flatten.h"
+#include "FirstInitializationAttemptsImpl.h"
+#include "GroupInfo.h"
+#include "GroupInfoPair.h"
+#include "OriginScope.h"
+#include "OriginInfo.h"
+#include "QuotaCommon.h"
+#include "QuotaManager.h"
+#include "ScopedLogExtraInfo.h"
+#include "UsageInfo.h"
+
+// Global includes
+#include <cinttypes>
+#include <cstdlib>
+#include <cstring>
+#include <algorithm>
+#include <cstdint>
+#include <functional>
+#include <new>
+#include <numeric>
+#include <tuple>
+#include <type_traits>
+#include <utility>
+#include "DirectoryLockImpl.h"
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "mozIStorageAsyncConnection.h"
+#include "mozIStorageConnection.h"
+#include "mozIStorageService.h"
+#include "mozIStorageStatement.h"
+#include "mozStorageCID.h"
+#include "mozStorageHelper.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/AutoRestore.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/CondVar.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/Services.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/FileSystemQuotaClient.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/LocalStorageCommon.h"
+#include "mozilla/dom/StorageDBUpdater.h"
+#include "mozilla/dom/cache/QuotaClient.h"
+#include "mozilla/dom/indexedDB/ActorsParent.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/localstorage/ActorsParent.h"
+#include "mozilla/dom/quota/AssertionsImpl.h"
+#include "mozilla/dom/quota/CheckedUnsafePtr.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/DirectoryLock.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/PQuota.h"
+#include "mozilla/dom/quota/PQuotaParent.h"
+#include "mozilla/dom/quota/PQuotaRequest.h"
+#include "mozilla/dom/quota/PQuotaRequestParent.h"
+#include "mozilla/dom/quota/PQuotaUsageRequest.h"
+#include "mozilla/dom/quota/PQuotaUsageRequestParent.h"
+#include "mozilla/dom/quota/QuotaManagerImpl.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
+#include "mozilla/dom/simpledb/ActorsParent.h"
+#include "mozilla/fallible.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/net/MozURL.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsBaseHashtable.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsClassHashtable.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsError.h"
+#include "nsHashKeys.h"
+#include "nsIBinaryInputStream.h"
+#include "nsIBinaryOutputStream.h"
+#include "nsIConsoleService.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIEventTarget.h"
+#include "nsIFile.h"
+#include "nsIFileStreams.h"
+#include "nsIInputStream.h"
+#include "nsIObjectInputStream.h"
+#include "nsIObjectOutputStream.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsIOutputStream.h"
+#include "nsIPlatformInfo.h"
+#include "nsIPrincipal.h"
+#include "nsIRunnable.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsISupports.h"
+#include "nsISupportsPrimitives.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+#include "nsIURI.h"
+#include "nsIWidget.h"
+#include "nsLiteralString.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+#include "nsTLiteralString.h"
+#include "nsTPromiseFlatString.h"
+#include "nsTStringRepr.h"
+#include "nsThreadUtils.h"
+#include "nsURLHelper.h"
+#include "nsXPCOM.h"
+#include "nsXPCOMCID.h"
+#include "nsXULAppAPI.h"
+#include "prinrval.h"
+#include "prio.h"
+#include "prthread.h"
+#include "prtime.h"
+
+// As part of bug 1536596 in order to identify the remaining sources of
+// principal info inconsistencies, we have added anonymized crash logging and
+// are temporarily making these checks occur on both debug and optimized
+// nightly, dev-edition, and early beta builds through use of
+// EARLY_BETA_OR_EARLIER during Firefox 82. The plan is to return this
+// condition to MOZ_DIAGNOSTIC_ASSERT_ENABLED during Firefox 84 at the latest.
+// The analysis and disabling is tracked by bug 1536596.
+
+#ifdef EARLY_BETA_OR_EARLIER
+# define QM_PRINCIPALINFO_VERIFICATION_ENABLED
+#endif
+
+// The amount of time, in milliseconds, that our IO thread will stay alive
+// after the last event it processes.
+#define DEFAULT_THREAD_TIMEOUT_MS 30000
+
+/**
+ * If shutdown takes this long, kill actors of a quota client, to avoid reaching
+ * the crash timeout.
+ */
+#define SHUTDOWN_KILL_ACTORS_TIMEOUT_MS 5000
+
+/**
+ * Automatically crash the browser if shutdown of a quota client takes this
+ * long. We've chosen a value that is long enough that it is unlikely for the
+ * problem to be falsely triggered by slow system I/O. We've also chosen a
+ * value long enough so that automated tests should time out and fail if
+ * shutdown of a quota client takes too long. Also, this value is long enough
+ * so that testers can notice the timeout; we want to know about the timeouts,
+ * not hide them. On the other hand this value is less than 60 seconds which is
+ * used by nsTerminator to crash a hung main process.
+ */
+#define SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS 45000
+
+static_assert(
+ SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS > SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
+ "The kill actors timeout must be shorter than the crash browser one.");
+
+// profile-before-change, when we need to shut down quota manager
+#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm"
+
+#define KB *1024ULL
+#define MB *1024ULL KB
+#define GB *1024ULL MB
+
+namespace mozilla::dom::quota {
+
+using namespace mozilla::ipc;
+using mozilla::net::MozURL;
+
+// We want profiles to be platform-independent so we always need to replace
+// the same characters on every platform. Windows has the most extensive set
+// of illegal characters so we use its FILE_ILLEGAL_CHARACTERS and
+// FILE_PATH_SEPARATOR.
+const char QuotaManager::kReplaceChars[] = CONTROL_CHARACTERS "/:*?\"<>|\\";
+const char16_t QuotaManager::kReplaceChars16[] =
+ u"" CONTROL_CHARACTERS "/:*?\"<>|\\";
+
+namespace {
+
+/*******************************************************************************
+ * Constants
+ ******************************************************************************/
+
+const uint32_t kSQLitePageSizeOverride = 512;
+
+// Important version history:
+// - Bug 1290481 bumped our schema from major.minor 2.0 to 3.0 in Firefox 57
+// which caused Firefox 57 release concerns because the major schema upgrade
+// means anyone downgrading to Firefox 56 will experience a non-operational
+// QuotaManager and all of its clients.
+// - Bug 1404344 got very concerned about that and so we decided to effectively
+// rename 3.0 to 2.1, effective in Firefox 57. This works because post
+// storage.sqlite v1.0, QuotaManager doesn't care about minor storage version
+// increases. It also works because all the upgrade did was give the DOM
+// Cache API QuotaClient an opportunity to create its newly added .padding
+// files during initialization/upgrade, which isn't functionally necessary as
+// that can be done on demand.
+
+// Major storage version. Bump for backwards-incompatible changes.
+// (The next major version should be 4 to distinguish from the Bug 1290481
+// downgrade snafu.)
+const uint32_t kMajorStorageVersion = 2;
+
+// Minor storage version. Bump for backwards-compatible changes.
+const uint32_t kMinorStorageVersion = 3;
+
+// The storage version we store in the SQLite database is a (signed) 32-bit
+// integer. The major version is left-shifted 16 bits so the max value is
+// 0xFFFF. The minor version occupies the lower 16 bits and its max is 0xFFFF.
+static_assert(kMajorStorageVersion <= 0xFFFF,
+ "Major version needs to fit in 16 bits.");
+static_assert(kMinorStorageVersion <= 0xFFFF,
+ "Minor version needs to fit in 16 bits.");
+
+const int32_t kStorageVersion =
+ int32_t((kMajorStorageVersion << 16) + kMinorStorageVersion);
+
+// See comments above about why these are a thing.
+const int32_t kHackyPreDowngradeStorageVersion = int32_t((3 << 16) + 0);
+const int32_t kHackyPostDowngradeStorageVersion = int32_t((2 << 16) + 1);
+
+const char kChromeOrigin[] = "chrome";
+const char kAboutHomeOriginPrefix[] = "moz-safe-about:home";
+const char kIndexedDBOriginPrefix[] = "indexeddb://";
+const char kResourceOriginPrefix[] = "resource://";
+
+constexpr auto kStorageName = u"storage"_ns;
+constexpr auto kSQLiteSuffix = u".sqlite"_ns;
+
+#define INDEXEDDB_DIRECTORY_NAME u"indexedDB"
+#define ARCHIVES_DIRECTORY_NAME u"archives"
+#define PERSISTENT_DIRECTORY_NAME u"persistent"
+#define PERMANENT_DIRECTORY_NAME u"permanent"
+#define TEMPORARY_DIRECTORY_NAME u"temporary"
+#define DEFAULT_DIRECTORY_NAME u"default"
+
+// The name of the file that we use to load/save the last access time of an
+// origin.
+// XXX We should get rid of old metadata files at some point, bug 1343576.
+#define METADATA_FILE_NAME u".metadata"
+#define METADATA_TMP_FILE_NAME u".metadata-tmp"
+#define METADATA_V2_FILE_NAME u".metadata-v2"
+#define METADATA_V2_TMP_FILE_NAME u".metadata-v2-tmp"
+
+#define WEB_APPS_STORE_FILE_NAME u"webappsstore.sqlite"
+#define LS_ARCHIVE_FILE_NAME u"ls-archive.sqlite"
+#define LS_ARCHIVE_TMP_FILE_NAME u"ls-archive-tmp.sqlite"
+
+const int32_t kLocalStorageArchiveVersion = 4;
+
+const char kProfileDoChangeTopic[] = "profile-do-change";
+
+const int32_t kCacheVersion = 2;
+
+/******************************************************************************
+ * SQLite functions
+ ******************************************************************************/
+
+int32_t MakeStorageVersion(uint32_t aMajorStorageVersion,
+ uint32_t aMinorStorageVersion) {
+ return int32_t((aMajorStorageVersion << 16) + aMinorStorageVersion);
+}
+
+uint32_t GetMajorStorageVersion(int32_t aStorageVersion) {
+ return uint32_t(aStorageVersion >> 16);
+}
+
+nsresult CreateTables(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // Table `database`
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection->ExecuteSimpleSQL("CREATE TABLE database"
+ "( cache_version INTEGER NOT NULL DEFAULT 0"
+ ");"_ns)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const int32_t& storageVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));
+
+ MOZ_ASSERT(storageVersion == 0);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(kStorageVersion)));
+
+ return NS_OK;
+}
+
+Result<int32_t, nsresult> LoadCacheVersion(mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection, "SELECT cache_version FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
+}
+
+nsresult SaveCacheVersion(mozIStorageConnection& aConnection,
+ int32_t aVersion) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ "UPDATE database SET cache_version = :version;"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, aVersion)));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+nsresult CreateCacheTables(mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ // Table `cache`
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection.ExecuteSimpleSQL("CREATE TABLE cache"
+ "( valid INTEGER NOT NULL DEFAULT 0"
+ ", build_id TEXT NOT NULL DEFAULT ''"
+ ");"_ns)));
+
+ // Table `repository`
+ QM_TRY(
+ MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL("CREATE TABLE repository"
+ "( id INTEGER PRIMARY KEY"
+ ", name TEXT NOT NULL"
+ ");"_ns)));
+
+ // Table `origin`
+ QM_TRY(MOZ_TO_RESULT(
+ aConnection.ExecuteSimpleSQL("CREATE TABLE origin"
+ "( repository_id INTEGER NOT NULL"
+ ", suffix TEXT"
+ ", group_ TEXT NOT NULL"
+ ", origin TEXT NOT NULL"
+ ", client_usages TEXT NOT NULL"
+ ", usage INTEGER NOT NULL"
+ ", last_access_time INTEGER NOT NULL"
+ ", accessed INTEGER NOT NULL"
+ ", persisted INTEGER NOT NULL"
+ ", PRIMARY KEY (repository_id, origin)"
+ ", FOREIGN KEY (repository_id) "
+ "REFERENCES repository(id) "
+ ");"_ns)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));
+ MOZ_ASSERT(cacheVersion == 0);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, kCacheVersion)));
+
+ return NS_OK;
+}
+
+OkOrErr InvalidateCache(mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ static constexpr auto kDeleteCacheQuery = "DELETE FROM origin;"_ns;
+ static constexpr auto kSetInvalidFlagQuery = "UPDATE cache SET valid = 0"_ns;
+
+ QM_TRY(QM_OR_ELSE_WARN(
+ // Expression.
+ ([&]() -> OkOrErr {
+ mozStorageTransaction transaction(&aConnection,
+ /*aCommitOnComplete */ false);
+
+ QM_TRY(QM_TO_RESULT(transaction.Start()));
+ QM_TRY(QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kDeleteCacheQuery)));
+ QM_TRY(
+ QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));
+ QM_TRY(QM_TO_RESULT(transaction.Commit()));
+
+ return Ok{};
+ }()),
+ // Fallback.
+ ([&](const QMResult& rv) -> OkOrErr {
+ QM_TRY(
+ QM_TO_RESULT(aConnection.ExecuteSimpleSQL(kSetInvalidFlagQuery)));
+
+ return Ok{};
+ })));
+
+ return Ok{};
+}
+
+nsresult UpgradeCacheFrom1To2(mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
+ "ALTER TABLE origin ADD COLUMN suffix TEXT"_ns)));
+
+ QM_TRY(InvalidateCache(aConnection));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const int32_t& cacheVersion, LoadCacheVersion(aConnection));
+
+ MOZ_ASSERT(cacheVersion == 1);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(SaveCacheVersion(aConnection, 2)));
+
+ return NS_OK;
+}
+
+Result<bool, nsresult> MaybeCreateOrUpgradeCache(
+ mozIStorageConnection& aConnection) {
+ bool cacheUsable = true;
+
+ QM_TRY_UNWRAP(int32_t cacheVersion, LoadCacheVersion(aConnection));
+
+ if (cacheVersion > kCacheVersion) {
+ cacheUsable = false;
+ } else if (cacheVersion != kCacheVersion) {
+ const bool newCache = !cacheVersion;
+
+ mozStorageTransaction transaction(
+ &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ if (newCache) {
+ QM_TRY(MOZ_TO_RESULT(CreateCacheTables(aConnection)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const int32_t& cacheVersion,
+ LoadCacheVersion(aConnection));
+ MOZ_ASSERT(cacheVersion == kCacheVersion);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
+ nsLiteralCString("INSERT INTO cache (valid, build_id) "
+ "VALUES (0, '')"))));
+
+ nsCOMPtr<mozIStorageStatement> insertStmt;
+
+ for (const PersistenceType persistenceType : kAllPersistenceTypes) {
+ if (insertStmt) {
+ MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
+ } else {
+ QM_TRY_UNWRAP(insertStmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>,
+ aConnection, CreateStatement,
+ "INSERT INTO repository (id, name) "
+ "VALUES (:id, :name)"_ns));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ insertStmt->BindInt32ByName("id"_ns, persistenceType)));
+
+ QM_TRY(MOZ_TO_RESULT(insertStmt->BindUTF8StringByName(
+ "name"_ns, PersistenceTypeToString(persistenceType))));
+
+ QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()));
+ }
+ } else {
+ // This logic needs to change next time we change the cache!
+ static_assert(kCacheVersion == 2,
+ "Upgrade function needed due to cache version increase.");
+
+ while (cacheVersion != kCacheVersion) {
+ if (cacheVersion == 1) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeCacheFrom1To2(aConnection)));
+ } else {
+ QM_FAIL(Err(NS_ERROR_FAILURE), []() {
+ QM_WARNING(
+ "Unable to initialize cache, no upgrade path is "
+ "available!");
+ });
+ }
+
+ QM_TRY_UNWRAP(cacheVersion, LoadCacheVersion(aConnection));
+ }
+
+ MOZ_ASSERT(cacheVersion == kCacheVersion);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+ }
+
+ return cacheUsable;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult> CreateWebAppsStoreConnection(
+ nsIFile& aWebAppsStoreFile, mozIStorageService& aStorageService) {
+ AssertIsOnIOThread();
+
+ // Check if the old database exists at all.
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, Exists));
+
+ if (!exists) {
+ // webappsstore.sqlite doesn't exist, return a null connection.
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aWebAppsStoreFile, IsDirectory));
+
+ if (isDirectory) {
+ QM_WARNING("webappsstore.sqlite is not a file!");
+ return nsCOMPtr<mozIStorageConnection>{};
+ }
+
+ QM_TRY_INSPECT(const auto& connection,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, aStorageService,
+ OpenUnsharedDatabase, &aWebAppsStoreFile,
+ mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback. Don't throw an error, leave a corrupted
+ // webappsstore database as it is.
+ ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
+
+ if (connection) {
+ // Don't propagate an error, leave a non-updateable webappsstore database as
+ // it is.
+ QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::Update(connection)),
+ nsCOMPtr<mozIStorageConnection>{});
+ }
+
+ return connection;
+}
+
+Result<nsCOMPtr<nsIFile>, QMResult> GetLocalStorageArchiveFile(
+ const nsAString& aDirectoryPath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto lsArchiveFile,
+ QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aDirectoryPath)));
+
+ QM_TRY(QM_TO_RESULT(
+ lsArchiveFile->Append(nsLiteralString(LS_ARCHIVE_FILE_NAME))));
+
+ return lsArchiveFile;
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> GetLocalStorageArchiveTmpFile(
+ const nsAString& aDirectoryPath) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aDirectoryPath.IsEmpty());
+
+ QM_TRY_UNWRAP(auto lsArchiveTmpFile, QM_NewLocalFile(aDirectoryPath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ lsArchiveTmpFile->Append(nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))));
+
+ return lsArchiveTmpFile;
+}
+
+Result<bool, nsresult> IsLocalStorageArchiveInitialized(
+ mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ QM_TRY_RETURN(
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, TableExists, "database"_ns));
+}
+
+nsresult InitializeLocalStorageArchive(mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const auto& initialized,
+ IsLocalStorageArchiveInitialized(*aConnection));
+ MOZ_ASSERT(!initialized);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ "CREATE TABLE database(version INTEGER NOT NULL DEFAULT 0);"_ns)));
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ "INSERT INTO database (version) VALUES (:version)"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("version"_ns, 0)));
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+}
+
+Result<int32_t, nsresult> LoadLocalStorageArchiveVersion(
+ mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ aConnection, "SELECT version FROM database"_ns));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
+}
+
+nsresult SaveLocalStorageArchiveVersion(mozIStorageConnection* aConnection,
+ int32_t aVersion) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ nsCOMPtr<mozIStorageStatement> stmt;
+ nsresult rv = aConnection->CreateStatement(
+ "UPDATE database SET version = :version;"_ns, getter_AddRefs(stmt));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->BindInt32ByName("version"_ns, aVersion);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = stmt->Execute();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+template <typename FileFunc, typename DirectoryFunc>
+Result<mozilla::Ok, nsresult> CollectEachFileEntry(
+ nsIFile& aDirectory, const FileFunc& aFileFunc,
+ const DirectoryFunc& aDirectoryFunc) {
+ AssertIsOnIOThread();
+
+ return CollectEachFile(
+ aDirectory,
+ [&aFileFunc, &aDirectoryFunc](
+ const nsCOMPtr<nsIFile>& file) -> Result<mozilla::Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ return aDirectoryFunc(file);
+
+ case nsIFileKind::ExistsAsFile:
+ return aFileFunc(file);
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return Ok{};
+ });
+}
+
+/******************************************************************************
+ * Quota manager class declarations
+ ******************************************************************************/
+
+} // namespace
+
+class QuotaManager::Observer final : public nsIObserver {
+ static Observer* sInstance;
+
+ bool mPendingProfileChange;
+ bool mShutdownComplete;
+
+ public:
+ static nsresult Initialize();
+
+ static nsIObserver* GetInstance();
+
+ static void ShutdownCompleted();
+
+ private:
+ Observer() : mPendingProfileChange(false), mShutdownComplete(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ ~Observer() { MOZ_ASSERT(NS_IsMainThread()); }
+
+ nsresult Init();
+
+ nsresult Shutdown();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+};
+
+namespace {
+
+/*******************************************************************************
+ * Local class declarations
+ ******************************************************************************/
+
+} // namespace
+
+namespace {
+
+class CollectOriginsHelper final : public Runnable {
+ uint64_t mMinSizeToBeFreed;
+
+ Mutex& mMutex;
+ CondVar mCondVar;
+
+ // The members below are protected by mMutex.
+ nsTArray<RefPtr<OriginDirectoryLock>> mLocks;
+ uint64_t mSizeToBeFreed;
+ bool mWaiting;
+
+ public:
+ CollectOriginsHelper(mozilla::Mutex& aMutex, uint64_t aMinSizeToBeFreed);
+
+ // Blocks the current thread until origins are collected on the main thread.
+ // The returned value contains an aggregate size of those origins.
+ int64_t BlockAndReturnOriginsForEviction(
+ nsTArray<RefPtr<OriginDirectoryLock>>& aLocks);
+
+ private:
+ ~CollectOriginsHelper() = default;
+
+ NS_IMETHOD
+ Run() override;
+};
+
+class OriginOperationBase : public BackgroundThreadObject, public Runnable {
+ protected:
+ nsresult mResultCode;
+
+ enum State {
+ // Not yet run.
+ State_Initial,
+
+ // Running on the owning thread in the listener for OpenDirectory.
+ State_DirectoryOpenPending,
+
+ // Running on the IO thread.
+ State_DirectoryWorkOpen,
+
+ // Running on the owning thread after all work is done.
+ State_UnblockingOpen,
+
+ // All done.
+ State_Complete
+ };
+
+ private:
+ State mState;
+ bool mActorDestroyed;
+
+ protected:
+ bool mNeedsQuotaManagerInit;
+ bool mNeedsStorageInit;
+
+ public:
+ void NoteActorDestroyed() {
+ AssertIsOnOwningThread();
+
+ mActorDestroyed = true;
+ }
+
+ bool IsActorDestroyed() const {
+ AssertIsOnOwningThread();
+
+ return mActorDestroyed;
+ }
+
+ protected:
+ explicit OriginOperationBase(nsISerialEventTarget* aOwningThread,
+ const char* aRunnableName)
+ : BackgroundThreadObject(aOwningThread),
+ Runnable(aRunnableName),
+ mResultCode(NS_OK),
+ mState(State_Initial),
+ mActorDestroyed(false),
+ mNeedsQuotaManagerInit(false),
+ mNeedsStorageInit(false) {}
+
+ // Reference counted.
+ virtual ~OriginOperationBase() {
+ MOZ_ASSERT(mState == State_Complete);
+ MOZ_ASSERT(mActorDestroyed);
+ }
+
+#ifdef DEBUG
+ State GetState() const { return mState; }
+#endif
+
+ void SetState(State aState) {
+ MOZ_ASSERT(mState == State_Initial);
+ mState = aState;
+ }
+
+ void AdvanceState() {
+ switch (mState) {
+ case State_Initial:
+ mState = State_DirectoryOpenPending;
+ return;
+ case State_DirectoryOpenPending:
+ mState = State_DirectoryWorkOpen;
+ return;
+ case State_DirectoryWorkOpen:
+ mState = State_UnblockingOpen;
+ return;
+ case State_UnblockingOpen:
+ mState = State_Complete;
+ return;
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+ }
+
+ NS_IMETHOD
+ Run() override;
+
+ virtual void Open() = 0;
+
+#ifdef DEBUG
+ virtual nsresult DirectoryOpen();
+#else
+ nsresult DirectoryOpen();
+#endif
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) = 0;
+
+ void Finish(nsresult aResult);
+
+ virtual void UnblockOpen() = 0;
+
+ private:
+ nsresult Init();
+
+ nsresult FinishInit();
+
+ nsresult DirectoryWork();
+};
+
+class FinalizeOriginEvictionOp : public OriginOperationBase {
+ nsTArray<RefPtr<OriginDirectoryLock>> mLocks;
+
+ public:
+ FinalizeOriginEvictionOp(nsISerialEventTarget* aBackgroundThread,
+ nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks)
+ : OriginOperationBase(aBackgroundThread,
+ "dom::quota::FinalizeOriginEvictionOp"),
+ mLocks(std::move(aLocks)) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ }
+
+ void Dispatch();
+
+ void RunOnIOThreadImmediately();
+
+ private:
+ ~FinalizeOriginEvictionOp() = default;
+
+ virtual void Open() override;
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ virtual void UnblockOpen() override;
+};
+
+class NormalOriginOperationBase
+ : public OriginOperationBase,
+ public OpenDirectoryListener,
+ public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
+ protected:
+ OriginScope mOriginScope;
+ RefPtr<DirectoryLock> mDirectoryLock;
+ Nullable<PersistenceType> mPersistenceType;
+ Nullable<Client::Type> mClientType;
+ mozilla::Atomic<bool> mCanceled;
+ const bool mExclusive;
+
+ public:
+ void RunImmediately() {
+ MOZ_ASSERT(GetState() == State_Initial);
+
+ MOZ_ALWAYS_SUCCEEDS(this->Run());
+ }
+
+ protected:
+ NormalOriginOperationBase(const char* aRunnableName,
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope, bool aExclusive)
+ : OriginOperationBase(GetCurrentSerialEventTarget(), aRunnableName),
+ mOriginScope(aOriginScope),
+ mPersistenceType(aPersistenceType),
+ mExclusive(aExclusive) {
+ AssertIsOnOwningThread();
+ }
+
+ ~NormalOriginOperationBase() = default;
+
+ virtual RefPtr<DirectoryLock> CreateDirectoryLock();
+
+ private:
+ // Need to declare refcounting unconditionally, because
+ // OpenDirectoryListener has pure-virtual refcounting.
+ NS_DECL_ISUPPORTS_INHERITED
+
+ virtual void Open() override;
+
+ virtual void UnblockOpen() override;
+
+ // OpenDirectoryListener overrides.
+ virtual void DirectoryLockAcquired(DirectoryLock* aLock) override;
+
+ virtual void DirectoryLockFailed() override;
+
+ // Used to send results before unblocking open.
+ virtual void SendResults() = 0;
+};
+
+class SaveOriginAccessTimeOp : public NormalOriginOperationBase {
+ int64_t mTimestamp;
+
+ public:
+ SaveOriginAccessTimeOp(PersistenceType aPersistenceType,
+ const nsACString& aOrigin, int64_t aTimestamp)
+ : NormalOriginOperationBase("dom::quota::SaveOriginAccessTimeOp",
+ Nullable<PersistenceType>(aPersistenceType),
+ OriginScope::FromOrigin(aOrigin),
+ /* aExclusive */ false),
+ mTimestamp(aTimestamp) {
+ AssertIsOnOwningThread();
+ }
+
+ private:
+ ~SaveOriginAccessTimeOp() = default;
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ virtual void SendResults() override;
+};
+
+class ShutdownStorageOp : public NormalOriginOperationBase {
+ MozPromiseHolder<BoolPromise> mPromiseHolder;
+
+ public:
+ ShutdownStorageOp()
+ : NormalOriginOperationBase("dom::quota::ShutdownStorageOp",
+ Nullable<PersistenceType>(),
+ OriginScope::FromNull(),
+ /* aExclusive */ true) {
+ AssertIsOnOwningThread();
+ }
+
+ RefPtr<BoolPromise> OnResults() {
+ AssertIsOnOwningThread();
+
+ return mPromiseHolder.Ensure(__func__);
+ }
+
+ private:
+ ~ShutdownStorageOp() = default;
+
+#ifdef DEBUG
+ nsresult DirectoryOpen() override;
+#endif
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void SendResults() override;
+};
+
+/*******************************************************************************
+ * Actor class declarations
+ ******************************************************************************/
+
+class Quota final : public PQuotaParent {
+#ifdef DEBUG
+ bool mActorDestroyed;
+#endif
+
+ public:
+ Quota();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(mozilla::dom::quota::Quota)
+
+ private:
+ ~Quota();
+
+ bool VerifyRequestParams(const UsageRequestParams& aParams) const;
+
+ bool VerifyRequestParams(const RequestParams& aParams) const;
+
+ // IPDL methods.
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual PQuotaUsageRequestParent* AllocPQuotaUsageRequestParent(
+ const UsageRequestParams& aParams) override;
+
+ virtual mozilla::ipc::IPCResult RecvPQuotaUsageRequestConstructor(
+ PQuotaUsageRequestParent* aActor,
+ const UsageRequestParams& aParams) override;
+
+ virtual bool DeallocPQuotaUsageRequestParent(
+ PQuotaUsageRequestParent* aActor) override;
+
+ virtual PQuotaRequestParent* AllocPQuotaRequestParent(
+ const RequestParams& aParams) override;
+
+ virtual mozilla::ipc::IPCResult RecvPQuotaRequestConstructor(
+ PQuotaRequestParent* aActor, const RequestParams& aParams) override;
+
+ virtual bool DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) override;
+
+ virtual mozilla::ipc::IPCResult RecvStartIdleMaintenance() override;
+
+ virtual mozilla::ipc::IPCResult RecvStopIdleMaintenance() override;
+
+ virtual mozilla::ipc::IPCResult RecvAbortOperationsForProcess(
+ const ContentParentId& aContentParentId) override;
+};
+
+class QuotaUsageRequestBase : public NormalOriginOperationBase,
+ public PQuotaUsageRequestParent {
+ public:
+ // May be overridden by subclasses if they need to perform work on the
+ // background thread before being run.
+ virtual void Init(Quota& aQuota);
+
+ protected:
+ QuotaUsageRequestBase(const char* aRunnableName)
+ : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(),
+ OriginScope::FromNull(),
+ /* aExclusive */ false) {}
+
+ mozilla::Result<UsageInfo, nsresult> GetUsageForOrigin(
+ QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata);
+
+ // Subclasses use this override to set the IPDL response value.
+ virtual void GetResponse(UsageRequestResponse& aResponse) = 0;
+
+ private:
+ mozilla::Result<UsageInfo, nsresult> GetUsageForOriginEntries(
+ QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata, nsIFile& aDirectory,
+ bool aInitialized);
+
+ void SendResults() override;
+
+ // IPDL methods.
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvCancel() final;
+};
+
+// A mix-in class to simplify operations that need to process every origin in
+// one or more repositories. Sub-classes should call TraverseRepository in their
+// DoDirectoryWork and implement a ProcessOrigin method for their per-origin
+// logic.
+class TraverseRepositoryHelper {
+ public:
+ TraverseRepositoryHelper() = default;
+
+ protected:
+ virtual ~TraverseRepositoryHelper() = default;
+
+ // If ProcessOrigin returns an error, TraverseRepository will immediately
+ // terminate and return the received error code to its caller.
+ nsresult TraverseRepository(QuotaManager& aQuotaManager,
+ PersistenceType aPersistenceType);
+
+ private:
+ virtual const Atomic<bool>& GetIsCanceledFlag() = 0;
+
+ virtual nsresult ProcessOrigin(QuotaManager& aQuotaManager,
+ nsIFile& aOriginDir, const bool aPersistent,
+ const PersistenceType aPersistenceType) = 0;
+};
+
+class GetUsageOp final : public QuotaUsageRequestBase,
+ public TraverseRepositoryHelper {
+ nsTArray<OriginUsage> mOriginUsages;
+ nsTHashMap<nsCStringHashKey, uint32_t> mOriginUsagesIndex;
+
+ bool mGetAll;
+
+ public:
+ explicit GetUsageOp(const UsageRequestParams& aParams);
+
+ private:
+ ~GetUsageOp() = default;
+
+ void ProcessOriginInternal(QuotaManager* aQuotaManager,
+ const PersistenceType aPersistenceType,
+ const nsACString& aOrigin,
+ const int64_t aTimestamp, const bool aPersisted,
+ const uint64_t aUsage);
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ const Atomic<bool>& GetIsCanceledFlag() override;
+
+ nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir,
+ const bool aPersistent,
+ const PersistenceType aPersistenceType) override;
+
+ void GetResponse(UsageRequestResponse& aResponse) override;
+};
+
+class GetOriginUsageOp final : public QuotaUsageRequestBase {
+ nsCString mSuffix;
+ nsCString mGroup;
+ uint64_t mUsage;
+ uint64_t mFileUsage;
+ bool mFromMemory;
+
+ public:
+ explicit GetOriginUsageOp(const UsageRequestParams& aParams);
+
+ private:
+ ~GetOriginUsageOp() = default;
+
+ RefPtr<DirectoryLock> CreateDirectoryLock() override;
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(UsageRequestResponse& aResponse) override;
+};
+
+class QuotaRequestBase : public NormalOriginOperationBase,
+ public PQuotaRequestParent {
+ public:
+ // May be overridden by subclasses if they need to perform work on the
+ // background thread before being run.
+ virtual void Init(Quota& aQuota);
+
+ protected:
+ explicit QuotaRequestBase(const char* aRunnableName, bool aExclusive)
+ : NormalOriginOperationBase(aRunnableName, Nullable<PersistenceType>(),
+ OriginScope::FromNull(), aExclusive) {}
+
+ // Subclasses use this override to set the IPDL response value.
+ virtual void GetResponse(RequestResponse& aResponse) = 0;
+
+ private:
+ virtual void SendResults() override;
+
+ // IPDL methods.
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+};
+
+class StorageNameOp final : public QuotaRequestBase {
+ nsString mName;
+
+ public:
+ StorageNameOp();
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~StorageNameOp() = default;
+
+ RefPtr<DirectoryLock> CreateDirectoryLock() override;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class InitializedRequestBase : public QuotaRequestBase {
+ protected:
+ bool mInitialized;
+
+ public:
+ void Init(Quota& aQuota) override;
+
+ protected:
+ InitializedRequestBase(const char* aRunnableName);
+
+ private:
+ RefPtr<DirectoryLock> CreateDirectoryLock() override;
+};
+
+class StorageInitializedOp final : public InitializedRequestBase {
+ public:
+ StorageInitializedOp()
+ : InitializedRequestBase("dom::quota::StorageInitializedOp") {}
+
+ private:
+ ~StorageInitializedOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class TemporaryStorageInitializedOp final : public InitializedRequestBase {
+ public:
+ TemporaryStorageInitializedOp()
+ : InitializedRequestBase("dom::quota::StorageInitializedOp") {}
+
+ private:
+ ~TemporaryStorageInitializedOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class InitOp final : public QuotaRequestBase {
+ public:
+ InitOp();
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~InitOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class InitTemporaryStorageOp final : public QuotaRequestBase {
+ public:
+ InitTemporaryStorageOp();
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~InitTemporaryStorageOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class InitializeOriginRequestBase : public QuotaRequestBase {
+ protected:
+ nsCString mSuffix;
+ nsCString mGroup;
+ bool mCreated;
+
+ public:
+ void Init(Quota& aQuota) override;
+
+ protected:
+ InitializeOriginRequestBase(const char* aRunnableName,
+ PersistenceType aPersistenceType,
+ const PrincipalInfo& aPrincipalInfo);
+};
+
+class InitializePersistentOriginOp final : public InitializeOriginRequestBase {
+ public:
+ explicit InitializePersistentOriginOp(const RequestParams& aParams);
+
+ private:
+ ~InitializePersistentOriginOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class InitializeTemporaryOriginOp final : public InitializeOriginRequestBase {
+ public:
+ explicit InitializeTemporaryOriginOp(const RequestParams& aParams);
+
+ private:
+ ~InitializeTemporaryOriginOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class GetFullOriginMetadataOp : public QuotaRequestBase {
+ const OriginMetadata mOriginMetadata;
+ Maybe<FullOriginMetadata> mMaybeFullOriginMetadata;
+
+ public:
+ explicit GetFullOriginMetadataOp(const GetFullOriginMetadataParams& aParams);
+
+ private:
+ RefPtr<DirectoryLock> CreateDirectoryLock() override;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class ResetOrClearOp final : public QuotaRequestBase {
+ const bool mClear;
+
+ public:
+ explicit ResetOrClearOp(bool aClear);
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~ResetOrClearOp() = default;
+
+ void DeleteFiles(QuotaManager& aQuotaManager);
+
+ void DeleteStorageFile(QuotaManager& aQuotaManager);
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ virtual void GetResponse(RequestResponse& aResponse) override;
+};
+
+class ClearRequestBase : public QuotaRequestBase {
+ protected:
+ explicit ClearRequestBase(const char* aRunnableName, bool aExclusive)
+ : QuotaRequestBase(aRunnableName, aExclusive) {
+ AssertIsOnOwningThread();
+ }
+
+ void DeleteFiles(QuotaManager& aQuotaManager,
+ PersistenceType aPersistenceType);
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+};
+
+class ClearOriginOp final : public ClearRequestBase {
+ const ClearResetOriginParams mParams;
+ const bool mMatchAll;
+
+ public:
+ explicit ClearOriginOp(const RequestParams& aParams);
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~ClearOriginOp() = default;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class ClearDataOp final : public ClearRequestBase {
+ const ClearDataParams mParams;
+
+ public:
+ explicit ClearDataOp(const RequestParams& aParams);
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~ClearDataOp() = default;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class ResetOriginOp final : public QuotaRequestBase {
+ public:
+ explicit ResetOriginOp(const RequestParams& aParams);
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~ResetOriginOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class PersistRequestBase : public QuotaRequestBase {
+ const PrincipalInfo mPrincipalInfo;
+
+ protected:
+ nsCString mSuffix;
+ nsCString mGroup;
+
+ public:
+ void Init(Quota& aQuota) override;
+
+ protected:
+ explicit PersistRequestBase(const PrincipalInfo& aPrincipalInfo);
+};
+
+class PersistedOp final : public PersistRequestBase {
+ bool mPersisted;
+
+ public:
+ explicit PersistedOp(const RequestParams& aParams);
+
+ private:
+ ~PersistedOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class PersistOp final : public PersistRequestBase {
+ public:
+ explicit PersistOp(const RequestParams& aParams);
+
+ private:
+ ~PersistOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class EstimateOp final : public QuotaRequestBase {
+ const OriginMetadata mOriginMetadata;
+ std::pair<uint64_t, uint64_t> mUsageAndLimit;
+
+ public:
+ explicit EstimateOp(const EstimateParams& aParams);
+
+ private:
+ ~EstimateOp() = default;
+
+ RefPtr<DirectoryLock> CreateDirectoryLock() override;
+
+ virtual nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+class ListOriginsOp final : public QuotaRequestBase,
+ public TraverseRepositoryHelper {
+ // XXX Bug 1521541 will make each origin has it's own state.
+ nsTArray<nsCString> mOrigins;
+
+ public:
+ ListOriginsOp();
+
+ void Init(Quota& aQuota) override;
+
+ private:
+ ~ListOriginsOp() = default;
+
+ nsresult DoDirectoryWork(QuotaManager& aQuotaManager) override;
+
+ const Atomic<bool>& GetIsCanceledFlag() override;
+
+ nsresult ProcessOrigin(QuotaManager& aQuotaManager, nsIFile& aOriginDir,
+ const bool aPersistent,
+ const PersistenceType aPersistenceType) override;
+
+ void GetResponse(RequestResponse& aResponse) override;
+};
+
+/*******************************************************************************
+ * Other class declarations
+ ******************************************************************************/
+
+class StoragePressureRunnable final : public Runnable {
+ const uint64_t mUsage;
+
+ public:
+ explicit StoragePressureRunnable(uint64_t aUsage)
+ : Runnable("dom::quota::QuotaObject::StoragePressureRunnable"),
+ mUsage(aUsage) {}
+
+ private:
+ ~StoragePressureRunnable() = default;
+
+ NS_DECL_NSIRUNNABLE
+};
+
+class RecordQuotaInfoLoadTimeHelper final : public Runnable {
+ // TimeStamps that are set on the IO thread.
+ LazyInitializedOnceNotNull<const TimeStamp> mStartTime;
+ LazyInitializedOnceNotNull<const TimeStamp> mEndTime;
+
+ // A TimeStamp that is set on the main thread.
+ LazyInitializedOnceNotNull<const TimeStamp> mInitializedTime;
+
+ public:
+ RecordQuotaInfoLoadTimeHelper()
+ : Runnable("dom::quota::RecordQuotaInfoLoadTimeHelper") {}
+
+ TimeStamp Start();
+
+ TimeStamp End();
+
+ private:
+ ~RecordQuotaInfoLoadTimeHelper() = default;
+
+ NS_DECL_NSIRUNNABLE
+};
+
+/*******************************************************************************
+ * Helper classes
+ ******************************************************************************/
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+
+class PrincipalVerifier final : public Runnable {
+ nsTArray<PrincipalInfo> mPrincipalInfos;
+
+ public:
+ static already_AddRefed<PrincipalVerifier> CreateAndDispatch(
+ nsTArray<PrincipalInfo>&& aPrincipalInfos);
+
+ private:
+ explicit PrincipalVerifier(nsTArray<PrincipalInfo>&& aPrincipalInfos)
+ : Runnable("dom::quota::PrincipalVerifier"),
+ mPrincipalInfos(std::move(aPrincipalInfos)) {
+ AssertIsOnIOThread();
+ }
+
+ virtual ~PrincipalVerifier() = default;
+
+ Result<Ok, nsCString> CheckPrincipalInfoValidity(
+ const PrincipalInfo& aPrincipalInfo);
+
+ NS_DECL_NSIRUNNABLE
+};
+
+#endif
+
+/*******************************************************************************
+ * Helper Functions
+ ******************************************************************************/
+
+inline bool IsDotFile(const nsAString& aFileName) {
+ return QuotaManager::IsDotFile(aFileName);
+}
+
+inline bool IsOSMetadata(const nsAString& aFileName) {
+ return QuotaManager::IsOSMetadata(aFileName);
+}
+
+bool IsOriginMetadata(const nsAString& aFileName) {
+ return aFileName.EqualsLiteral(METADATA_FILE_NAME) ||
+ aFileName.EqualsLiteral(METADATA_V2_FILE_NAME) ||
+ IsOSMetadata(aFileName);
+}
+
+bool IsTempMetadata(const nsAString& aFileName) {
+ return aFileName.EqualsLiteral(METADATA_TMP_FILE_NAME) ||
+ aFileName.EqualsLiteral(METADATA_V2_TMP_FILE_NAME);
+}
+
+// Return whether the group was actually updated.
+Result<bool, nsresult> MaybeUpdateGroupForOrigin(
+ OriginMetadata& aOriginMetadata) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ bool updated = false;
+
+ if (aOriginMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) {
+ if (!aOriginMetadata.mGroup.EqualsLiteral(kChromeOrigin)) {
+ aOriginMetadata.mGroup.AssignLiteral(kChromeOrigin);
+ updated = true;
+ }
+ } else {
+ OriginAttributes originAttributes;
+ nsCString originNoSuffix;
+ QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin,
+ originNoSuffix)),
+ Err(NS_ERROR_FAILURE));
+
+ RefPtr<MozURL> url;
+ QM_TRY(MOZ_TO_RESULT(MozURL::Init(getter_AddRefs(url), originNoSuffix)),
+ QM_PROPAGATE, [&originNoSuffix](const nsresult) {
+ QM_WARNING("A URL %s is not recognized by MozURL",
+ originNoSuffix.get());
+ });
+
+ QM_TRY_INSPECT(
+ const auto& baseDomain,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *url, BaseDomain));
+
+ const nsCString upToDateGroup = baseDomain + aOriginMetadata.mSuffix;
+
+ if (aOriginMetadata.mGroup != upToDateGroup) {
+ aOriginMetadata.mGroup = upToDateGroup;
+ updated = true;
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+ ContentPrincipalInfo contentPrincipalInfo;
+ contentPrincipalInfo.attrs() = originAttributes;
+ contentPrincipalInfo.originNoSuffix() = originNoSuffix;
+ contentPrincipalInfo.spec() = originNoSuffix;
+ contentPrincipalInfo.baseDomain() = baseDomain;
+
+ PrincipalInfo principalInfo(contentPrincipalInfo);
+
+ nsTArray<PrincipalInfo> principalInfos;
+ principalInfos.AppendElement(principalInfo);
+
+ RefPtr<PrincipalVerifier> principalVerifier =
+ PrincipalVerifier::CreateAndDispatch(std::move(principalInfos));
+#endif
+ }
+ }
+
+ return updated;
+}
+
+Result<bool, nsresult> MaybeUpdateLastAccessTimeForOrigin(
+ FullOriginMetadata& aFullOriginMetadata) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (aFullOriginMetadata.mLastAccessTime == INT64_MIN) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY_INSPECT(
+ const auto& metadataFile,
+ quotaManager->GetDirectoryForOrigin(
+ aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mOrigin));
+
+ QM_TRY(MOZ_TO_RESULT(
+ metadataFile->Append(nsLiteralString(METADATA_V2_FILE_NAME))));
+
+ QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
+ metadataFile, GetLastModifiedTime));
+
+ // Need to convert from milliseconds to microseconds.
+ MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
+ timestamp *= int64_t(PR_USEC_PER_MSEC);
+
+ aFullOriginMetadata.mLastAccessTime = timestamp;
+
+ return true;
+ }
+
+ return false;
+}
+
+} // namespace
+
+BackgroundThreadObject::BackgroundThreadObject()
+ : mOwningThread(GetCurrentSerialEventTarget()) {
+ AssertIsOnOwningThread();
+}
+
+BackgroundThreadObject::BackgroundThreadObject(
+ nsISerialEventTarget* aOwningThread)
+ : mOwningThread(aOwningThread) {}
+
+#ifdef DEBUG
+
+void BackgroundThreadObject::AssertIsOnOwningThread() const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mOwningThread);
+ bool current;
+ MOZ_ASSERT(NS_SUCCEEDED(mOwningThread->IsOnCurrentThread(&current)));
+ MOZ_ASSERT(current);
+}
+
+#endif // DEBUG
+
+nsISerialEventTarget* BackgroundThreadObject::OwningThread() const {
+ MOZ_ASSERT(mOwningThread);
+ return mOwningThread;
+}
+
+bool IsOnIOThread() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ NS_ASSERTION(quotaManager, "Must have a manager here!");
+
+ bool currentThread;
+ return NS_SUCCEEDED(
+ quotaManager->IOThread()->IsOnCurrentThread(&currentThread)) &&
+ currentThread;
+}
+
+void AssertIsOnIOThread() {
+ NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!");
+}
+
+void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); }
+
+void AssertCurrentThreadOwnsQuotaMutex() {
+#ifdef DEBUG
+ QuotaManager* quotaManager = QuotaManager::Get();
+ NS_ASSERTION(quotaManager, "Must have a manager here!");
+
+ quotaManager->AssertCurrentThreadOwnsQuotaMutex();
+#endif
+}
+
+void ReportInternalError(const char* aFile, uint32_t aLine, const char* aStr) {
+ // Get leaf of file path
+ for (const char* p = aFile; *p; ++p) {
+ if (*p == '/' && *(p + 1)) {
+ aFile = p + 1;
+ }
+ }
+
+ nsContentUtils::LogSimpleConsoleError(
+ NS_ConvertUTF8toUTF16(
+ nsPrintfCString("Quota %s: %s:%" PRIu32, aStr, aFile, aLine)),
+ "quota"_ns,
+ false /* Quota Manager is not active in private browsing mode */,
+ true /* Quota Manager runs always in a chrome context */);
+}
+
+namespace {
+
+bool gInvalidateQuotaCache = false;
+StaticAutoPtr<nsString> gBasePath;
+StaticAutoPtr<nsString> gStorageName;
+StaticAutoPtr<nsCString> gBuildId;
+
+#ifdef DEBUG
+bool gQuotaManagerInitialized = false;
+#endif
+
+StaticRefPtr<QuotaManager> gInstance;
+mozilla::Atomic<bool> gShutdown(false);
+
+// A time stamp that can only be accessed on the main thread.
+TimeStamp gLastOSWake;
+
+using NormalOriginOpArray =
+ nsTArray<CheckedUnsafePtr<NormalOriginOperationBase>>;
+StaticAutoPtr<NormalOriginOpArray> gNormalOriginOps;
+
+void RegisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) {
+ AssertIsOnBackgroundThread();
+
+ if (!gNormalOriginOps) {
+ gNormalOriginOps = new NormalOriginOpArray();
+ }
+
+ gNormalOriginOps->AppendElement(&aNormalOriginOp);
+}
+
+void UnregisterNormalOriginOp(NormalOriginOperationBase& aNormalOriginOp) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(gNormalOriginOps);
+
+ gNormalOriginOps->RemoveElement(&aNormalOriginOp);
+
+ if (gNormalOriginOps->IsEmpty()) {
+ gNormalOriginOps = nullptr;
+ }
+}
+
+class StorageOperationBase {
+ protected:
+ struct OriginProps {
+ enum Type { eChrome, eContent, eObsolete, eInvalid };
+
+ NotNull<nsCOMPtr<nsIFile>> mDirectory;
+ nsString mLeafName;
+ nsCString mSpec;
+ OriginAttributes mAttrs;
+ int64_t mTimestamp;
+ OriginMetadata mOriginMetadata;
+ nsCString mOriginalSuffix;
+
+ LazyInitializedOnceEarlyDestructible<const PersistenceType>
+ mPersistenceType;
+ Type mType;
+ bool mNeedsRestore;
+ bool mNeedsRestore2;
+ bool mIgnore;
+
+ public:
+ explicit OriginProps(MovingNotNull<nsCOMPtr<nsIFile>> aDirectory)
+ : mDirectory(std::move(aDirectory)),
+ mTimestamp(0),
+ mType(eContent),
+ mNeedsRestore(false),
+ mNeedsRestore2(false),
+ mIgnore(false) {}
+
+ template <typename PersistenceTypeFunc>
+ nsresult Init(PersistenceTypeFunc&& aPersistenceTypeFunc);
+ };
+
+ nsTArray<OriginProps> mOriginProps;
+
+ nsCOMPtr<nsIFile> mDirectory;
+
+ public:
+ explicit StorageOperationBase(nsIFile* aDirectory) : mDirectory(aDirectory) {
+ AssertIsOnIOThread();
+ }
+
+ NS_INLINE_DECL_REFCOUNTING(StorageOperationBase)
+
+ protected:
+ virtual ~StorageOperationBase() = default;
+
+ nsresult GetDirectoryMetadata(nsIFile* aDirectory, int64_t& aTimestamp,
+ nsACString& aGroup, nsACString& aOrigin,
+ Nullable<bool>& aIsApp);
+
+ // Upgrade helper to load the contents of ".metadata-v2" files from previous
+ // schema versions. Although QuotaManager has a similar GetDirectoryMetadata2
+ // method, it is only intended to read current version ".metadata-v2" files.
+ // And unlike the old ".metadata" files, the ".metadata-v2" format can evolve
+ // because our "storage.sqlite" lets us track the overall version of the
+ // storage directory.
+ nsresult GetDirectoryMetadata2(nsIFile* aDirectory, int64_t& aTimestamp,
+ nsACString& aSuffix, nsACString& aGroup,
+ nsACString& aOrigin, bool& aIsApp);
+
+ int64_t GetOriginLastModifiedTime(const OriginProps& aOriginProps);
+
+ nsresult RemoveObsoleteOrigin(const OriginProps& aOriginProps);
+
+ /**
+ * Rename the origin if the origin string generation from nsIPrincipal
+ * changed. This consists of renaming the origin in the metadata files and
+ * renaming the origin directory itself. For simplicity, the origin in
+ * metadata files is not actually updated, but the metadata files are
+ * recreated instead.
+ *
+ * @param aOriginProps the properties of the origin to check.
+ *
+ * @return whether origin was renamed.
+ */
+ Result<bool, nsresult> MaybeRenameOrigin(const OriginProps& aOriginProps);
+
+ nsresult ProcessOriginDirectories();
+
+ virtual nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) = 0;
+};
+
+class MOZ_STACK_CLASS OriginParser final {
+ public:
+ enum ResultType { InvalidOrigin, ObsoleteOrigin, ValidOrigin };
+
+ private:
+ using Tokenizer =
+ nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>;
+
+ enum SchemeType { eNone, eFile, eAbout, eChrome };
+
+ enum State {
+ eExpectingAppIdOrScheme,
+ eExpectingInMozBrowser,
+ eExpectingScheme,
+ eExpectingEmptyToken1,
+ eExpectingEmptyToken2,
+ eExpectingEmptyTokenOrUniversalFileOrigin,
+ eExpectingHost,
+ eExpectingPort,
+ eExpectingEmptyTokenOrDriveLetterOrPathnameComponent,
+ eExpectingEmptyTokenOrPathnameComponent,
+ eExpectingEmptyToken1OrHost,
+
+ // We transit from eExpectingHost to this state when we encounter a host
+ // beginning with "[" which indicates an IPv6 literal. Because we mangle the
+ // IPv6 ":" delimiter to be a "+", we will receive separate tokens for each
+ // portion of the IPv6 address, including a final token that ends with "]".
+ // (Note that we do not mangle "[" or "]".) Note that the URL spec
+ // explicitly disclaims support for "<zone_id>" and so we don't have to deal
+ // with that.
+ eExpectingIPV6Token,
+ eComplete,
+ eHandledTrailingSeparator
+ };
+
+ const nsCString mOrigin;
+ Tokenizer mTokenizer;
+
+ nsCString mScheme;
+ nsCString mHost;
+ Nullable<uint32_t> mPort;
+ nsTArray<nsCString> mPathnameComponents;
+ nsCString mHandledTokens;
+
+ SchemeType mSchemeType;
+ State mState;
+ bool mInIsolatedMozBrowser;
+ bool mUniversalFileOrigin;
+ bool mMaybeDriveLetter;
+ bool mError;
+ bool mMaybeObsolete;
+
+ // Number of group which a IPv6 address has. Should be less than 9.
+ uint8_t mIPGroup;
+
+ public:
+ explicit OriginParser(const nsACString& aOrigin)
+ : mOrigin(aOrigin),
+ mTokenizer(aOrigin, '+'),
+ mPort(),
+ mSchemeType(eNone),
+ mState(eExpectingAppIdOrScheme),
+ mInIsolatedMozBrowser(false),
+ mUniversalFileOrigin(false),
+ mMaybeDriveLetter(false),
+ mError(false),
+ mMaybeObsolete(false),
+ mIPGroup(0) {}
+
+ static ResultType ParseOrigin(const nsACString& aOrigin, nsCString& aSpec,
+ OriginAttributes* aAttrs,
+ nsCString& aOriginalSuffix);
+
+ ResultType Parse(nsACString& aSpec);
+
+ private:
+ void HandleScheme(const nsDependentCSubstring& aToken);
+
+ void HandlePathnameComponent(const nsDependentCSubstring& aToken);
+
+ void HandleToken(const nsDependentCSubstring& aToken);
+
+ void HandleTrailingSeparator();
+};
+
+class RepositoryOperationBase : public StorageOperationBase {
+ public:
+ explicit RepositoryOperationBase(nsIFile* aDirectory)
+ : StorageOperationBase(aDirectory) {}
+
+ nsresult ProcessRepository();
+
+ protected:
+ virtual ~RepositoryOperationBase() = default;
+
+ template <typename UpgradeMethod>
+ nsresult MaybeUpgradeClients(const OriginProps& aOriginsProps,
+ UpgradeMethod aMethod);
+
+ private:
+ virtual PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) = 0;
+
+ virtual nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) = 0;
+
+ virtual nsresult PrepareClientDirectory(nsIFile* aFile,
+ const nsAString& aLeafName,
+ bool& aRemoved);
+};
+
+class CreateOrUpgradeDirectoryMetadataHelper final
+ : public RepositoryOperationBase {
+ nsCOMPtr<nsIFile> mPermanentStorageDir;
+
+ // The legacy PersistenceType, before the default repository introduction.
+ enum class LegacyPersistenceType {
+ Persistent = 0,
+ Temporary
+ // The PersistenceType had also PERSISTENCE_TYPE_INVALID, but we don't need
+ // it here.
+ };
+
+ LazyInitializedOnce<const LegacyPersistenceType> mLegacyPersistenceType;
+
+ public:
+ explicit CreateOrUpgradeDirectoryMetadataHelper(nsIFile* aDirectory)
+ : RepositoryOperationBase(aDirectory) {}
+
+ nsresult Init();
+
+ private:
+ Maybe<LegacyPersistenceType> LegacyPersistenceTypeFromFile(nsIFile& aFile,
+ const fallible_t&);
+
+ PersistenceType PersistenceTypeFromLegacyPersistentSpec(
+ const nsCString& aSpec);
+
+ PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;
+
+ nsresult MaybeUpgradeOriginDirectory(nsIFile* aDirectory);
+
+ nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) override;
+
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+};
+
+class UpgradeStorageHelperBase : public RepositoryOperationBase {
+ LazyInitializedOnce<const PersistenceType> mPersistenceType;
+
+ public:
+ explicit UpgradeStorageHelperBase(nsIFile* aDirectory)
+ : RepositoryOperationBase(aDirectory) {}
+
+ nsresult Init();
+
+ private:
+ PersistenceType PersistenceTypeFromSpec(const nsCString& aSpec) override;
+};
+
+class UpgradeStorageFrom0_0To1_0Helper final : public UpgradeStorageHelperBase {
+ public:
+ explicit UpgradeStorageFrom0_0To1_0Helper(nsIFile* aDirectory)
+ : UpgradeStorageHelperBase(aDirectory) {}
+
+ private:
+ nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) override;
+
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+};
+
+class UpgradeStorageFrom1_0To2_0Helper final : public UpgradeStorageHelperBase {
+ public:
+ explicit UpgradeStorageFrom1_0To2_0Helper(nsIFile* aDirectory)
+ : UpgradeStorageHelperBase(aDirectory) {}
+
+ private:
+ nsresult MaybeRemoveMorgueDirectory(const OriginProps& aOriginProps);
+
+ /**
+ * Remove the origin directory if appId is present in origin attributes.
+ *
+ * @param aOriginProps the properties of the origin to check.
+ *
+ * @return whether the origin directory was removed.
+ */
+ Result<bool, nsresult> MaybeRemoveAppsData(const OriginProps& aOriginProps);
+
+ nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) override;
+
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+};
+
+class UpgradeStorageFrom2_0To2_1Helper final : public UpgradeStorageHelperBase {
+ public:
+ explicit UpgradeStorageFrom2_0To2_1Helper(nsIFile* aDirectory)
+ : UpgradeStorageHelperBase(aDirectory) {}
+
+ private:
+ nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) override;
+
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+};
+
+class UpgradeStorageFrom2_1To2_2Helper final : public UpgradeStorageHelperBase {
+ public:
+ explicit UpgradeStorageFrom2_1To2_2Helper(nsIFile* aDirectory)
+ : UpgradeStorageHelperBase(aDirectory) {}
+
+ private:
+ nsresult PrepareOriginDirectory(OriginProps& aOriginProps,
+ bool* aRemoved) override;
+
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+
+ nsresult PrepareClientDirectory(nsIFile* aFile, const nsAString& aLeafName,
+ bool& aRemoved) override;
+};
+
+class RestoreDirectoryMetadata2Helper final : public StorageOperationBase {
+ LazyInitializedOnce<const PersistenceType> mPersistenceType;
+
+ public:
+ explicit RestoreDirectoryMetadata2Helper(nsIFile* aDirectory)
+ : StorageOperationBase(aDirectory) {}
+
+ nsresult Init();
+
+ nsresult RestoreMetadata2File();
+
+ private:
+ nsresult ProcessOriginDirectory(const OriginProps& aOriginProps) override;
+};
+
+auto MakeSanitizedOriginCString(const nsACString& aOrigin) {
+#ifdef XP_WIN
+ NS_ASSERTION(!strcmp(QuotaManager::kReplaceChars,
+ FILE_ILLEGAL_CHARACTERS FILE_PATH_SEPARATOR),
+ "Illegal file characters have changed!");
+#endif
+
+ nsAutoCString res{aOrigin};
+
+ res.ReplaceChar(QuotaManager::kReplaceChars, '+');
+
+ return res;
+}
+
+auto MakeSanitizedOriginString(const nsACString& aOrigin) {
+ // An origin string is ASCII-only, since it is obtained via
+ // nsIPrincipal::GetOrigin, which returns an ACString.
+ return NS_ConvertASCIItoUTF16(MakeSanitizedOriginCString(aOrigin));
+}
+
+Result<nsAutoString, nsresult> GetPathForStorage(
+ nsIFile& aBaseDir, const nsAString& aStorageName) {
+ QM_TRY_INSPECT(const auto& storageDir,
+ CloneFileAndAppend(aBaseDir, aStorageName));
+
+ QM_TRY_RETURN(
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, storageDir, GetPath));
+}
+
+int64_t GetLastModifiedTime(PersistenceType aPersistenceType, nsIFile& aFile) {
+ AssertIsOnIOThread();
+
+ class MOZ_STACK_CLASS Helper final {
+ public:
+ static nsresult GetLastModifiedTime(nsIFile* aFile, int64_t* aTimestamp) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aFile);
+ MOZ_ASSERT(aTimestamp);
+
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*aFile));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ QM_TRY(CollectEachFile(
+ *aFile,
+ [&aTimestamp](const nsCOMPtr<nsIFile>& file)
+ -> Result<mozilla::Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(GetLastModifiedTime(file, aTimestamp)));
+
+ return Ok{};
+ }));
+ break;
+
+ case nsIFileKind::ExistsAsFile: {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, aFile,
+ GetLeafName));
+
+ // Bug 1595445 will handle unknown files here.
+
+ if (IsOriginMetadata(leafName) || IsTempMetadata(leafName) ||
+ IsDotFile(leafName)) {
+ return NS_OK;
+ }
+
+ QM_TRY_UNWRAP(int64_t timestamp, MOZ_TO_RESULT_INVOKE_MEMBER(
+ aFile, GetLastModifiedTime));
+
+ // Need to convert from milliseconds to microseconds.
+ MOZ_ASSERT((INT64_MAX / PR_USEC_PER_MSEC) > timestamp);
+ timestamp *= int64_t(PR_USEC_PER_MSEC);
+
+ if (timestamp > *aTimestamp) {
+ *aTimestamp = timestamp;
+ }
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return NS_OK;
+ }
+ };
+
+ if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
+ return PR_Now();
+ }
+
+ int64_t timestamp = INT64_MIN;
+ nsresult rv = Helper::GetLastModifiedTime(&aFile, &timestamp);
+ if (NS_FAILED(rv)) {
+ timestamp = PR_Now();
+ }
+
+ // XXX if there were no suitable files for getting last modified time
+ // (timestamp is still set to INT64_MIN), we should return the current time
+ // instead of returning INT64_MIN.
+
+ return timestamp;
+}
+
+// Returns a bool indicating whether the directory was newly created.
+Result<bool, nsresult> EnsureDirectory(nsIFile& aDirectory) {
+ AssertIsOnIOThread();
+
+ // Callers call this function without checking if the directory already
+ // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
+ // just want to log NS_ERROR_FILE_ALREADY_EXISTS result and not spam the
+ // reports.
+ QM_TRY_INSPECT(const auto& exists,
+ QM_OR_ELSE_LOG_VERBOSE_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Create,
+ nsIFile::DIRECTORY_TYPE, 0755,
+ /* aSkipAncestors = */ false)
+ .map([](Ok) { return false; }),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
+ // Fallback.
+ ErrToOk<true>));
+
+ if (exists) {
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, IsDirectory));
+ QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));
+ }
+
+ return !exists;
+}
+
+enum FileFlag { Truncate, Update, Append };
+
+Result<nsCOMPtr<nsIOutputStream>, nsresult> GetOutputStream(
+ nsIFile& aFile, FileFlag aFileFlag) {
+ AssertIsOnIOThread();
+
+ switch (aFileFlag) {
+ case FileFlag::Truncate:
+ QM_TRY_RETURN(NS_NewLocalFileOutputStream(&aFile));
+
+ case FileFlag::Update: {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(&aFile, Exists));
+
+ if (!exists) {
+ return nsCOMPtr<nsIOutputStream>();
+ }
+
+ QM_TRY_INSPECT(const auto& stream,
+ NS_NewLocalFileRandomAccessStream(&aFile));
+
+ nsCOMPtr<nsIOutputStream> outputStream = do_QueryInterface(stream);
+ QM_TRY(OkIf(outputStream), Err(NS_ERROR_FAILURE));
+
+ return outputStream;
+ }
+
+ case FileFlag::Append:
+ QM_TRY_RETURN(NS_NewLocalFileOutputStream(
+ &aFile, PR_WRONLY | PR_CREATE_FILE | PR_APPEND));
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+}
+
+Result<nsCOMPtr<nsIBinaryOutputStream>, nsresult> GetBinaryOutputStream(
+ nsIFile& aFile, FileFlag aFileFlag) {
+ QM_TRY_UNWRAP(auto outputStream, GetOutputStream(aFile, aFileFlag));
+
+ QM_TRY(OkIf(outputStream), Err(NS_ERROR_UNEXPECTED));
+
+ return nsCOMPtr<nsIBinaryOutputStream>(
+ NS_NewObjectOutputStream(outputStream));
+}
+
+void GetJarPrefix(bool aInIsolatedMozBrowser, nsACString& aJarPrefix) {
+ aJarPrefix.Truncate();
+
+ // Fallback.
+ if (!aInIsolatedMozBrowser) {
+ return;
+ }
+
+ // AppId is an unused b2g identifier. Let's set it to 0 all the time (see bug
+ // 1320404).
+ // aJarPrefix = appId + "+" + { 't', 'f' } + "+";
+ aJarPrefix.AppendInt(0); // TODO: this is the appId, to be removed.
+ aJarPrefix.Append('+');
+ aJarPrefix.Append(aInIsolatedMozBrowser ? 't' : 'f');
+ aJarPrefix.Append('+');
+}
+
+nsresult CreateDirectoryMetadata(nsIFile& aDirectory, int64_t aTimestamp,
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ OriginAttributes groupAttributes;
+
+ nsCString groupNoSuffix;
+ QM_TRY(OkIf(groupAttributes.PopulateFromOrigin(aOriginMetadata.mGroup,
+ groupNoSuffix)),
+ NS_ERROR_FAILURE);
+
+ nsCString groupPrefix;
+ GetJarPrefix(groupAttributes.mInIsolatedMozBrowser, groupPrefix);
+
+ nsCString group = groupPrefix + groupNoSuffix;
+
+ OriginAttributes originAttributes;
+
+ nsCString originNoSuffix;
+ QM_TRY(OkIf(originAttributes.PopulateFromOrigin(aOriginMetadata.mOrigin,
+ originNoSuffix)),
+ NS_ERROR_FAILURE);
+
+ nsCString originPrefix;
+ GetJarPrefix(originAttributes.mInIsolatedMozBrowser, originPrefix);
+
+ nsCString origin = originPrefix + originNoSuffix;
+
+ MOZ_ASSERT(groupPrefix == originPrefix);
+
+ QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, aDirectory, Clone));
+
+ QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_TMP_FILE_NAME))));
+
+ QM_TRY_INSPECT(const auto& stream,
+ GetBinaryOutputStream(*file, FileFlag::Truncate));
+ MOZ_ASSERT(stream);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));
+
+ QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(group.get())));
+
+ QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(origin.get())));
+
+ // Currently unused (used to be isApp).
+ QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));
+
+ QM_TRY(MOZ_TO_RESULT(stream->Flush()));
+
+ QM_TRY(MOZ_TO_RESULT(stream->Close()));
+
+ QM_TRY(MOZ_TO_RESULT(
+ file->RenameTo(nullptr, nsLiteralString(METADATA_FILE_NAME))));
+
+ return NS_OK;
+}
+
+nsresult CreateDirectoryMetadata2(nsIFile& aDirectory, int64_t aTimestamp,
+ bool aPersisted,
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, aDirectory, Clone));
+
+ QM_TRY(
+ MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_TMP_FILE_NAME))));
+
+ QM_TRY_INSPECT(const auto& stream,
+ GetBinaryOutputStream(*file, FileFlag::Truncate));
+ MOZ_ASSERT(stream);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Write64(aTimestamp)));
+
+ QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(aPersisted)));
+
+ // Reserved data 1
+ QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));
+
+ // Reserved data 2
+ QM_TRY(MOZ_TO_RESULT(stream->Write32(0)));
+
+ // The suffix isn't used right now, but we might need it in future. It's
+ // a bit of redundancy we can live with given how painful is to upgrade
+ // metadata files.
+ QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mSuffix.get())));
+
+ QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mGroup.get())));
+
+ QM_TRY(MOZ_TO_RESULT(stream->WriteStringZ(aOriginMetadata.mOrigin.get())));
+
+ // Currently unused (used to be isApp).
+ QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));
+
+ QM_TRY(MOZ_TO_RESULT(stream->Flush()));
+
+ QM_TRY(MOZ_TO_RESULT(stream->Close()));
+
+ QM_TRY(MOZ_TO_RESULT(
+ file->RenameTo(nullptr, nsLiteralString(METADATA_V2_FILE_NAME))));
+
+ return NS_OK;
+}
+
+Result<nsCOMPtr<nsIBinaryInputStream>, nsresult> GetBinaryInputStream(
+ nsIFile& aDirectory, const nsAString& aFilename) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ QM_TRY_INSPECT(const auto& file, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, aDirectory, Clone));
+
+ QM_TRY(MOZ_TO_RESULT(file->Append(aFilename)));
+
+ QM_TRY_UNWRAP(auto stream, NS_NewLocalFileInputStream(file));
+
+ QM_TRY_INSPECT(const auto& bufferedStream,
+ NS_NewBufferedInputStream(stream.forget(), 512));
+
+ QM_TRY(OkIf(bufferedStream), Err(NS_ERROR_FAILURE));
+
+ return nsCOMPtr<nsIBinaryInputStream>(
+ NS_NewObjectInputStream(bufferedStream));
+}
+
+// This method computes and returns our best guess for the temporary storage
+// limit (in bytes), based on disk capacity.
+Result<uint64_t, nsresult> GetTemporaryStorageLimit(nsIFile& aStorageDir) {
+ // The fixed limit pref can be used to override temporary storage limit
+ // calculation.
+ if (StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit() >= 0) {
+ return static_cast<uint64_t>(
+ StaticPrefs::dom_quotaManager_temporaryStorage_fixedLimit()) *
+ 1024;
+ }
+
+ constexpr int64_t teraByte = (1024LL * 1024LL * 1024LL * 1024LL);
+ constexpr int64_t maxAllowedCapacity = 8LL * teraByte;
+
+ // Check for disk capacity of user's device on which storage directory lives.
+ int64_t diskCapacity = maxAllowedCapacity;
+
+ // Log error when default disk capacity is returned due to the error
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(aStorageDir.GetDiskCapacity(&diskCapacity)));
+
+ MOZ_ASSERT(diskCapacity >= 0LL);
+
+ // Allow temporary storage to consume up to 50% of disk capacity.
+ int64_t capacityLimit = diskCapacity / 2LL;
+
+ // If the disk capacity reported by the operating system is very
+ // large and potentially incorrect due to hardware issues,
+ // a hardcoded limit is supplied instead.
+ QM_WARNONLY_TRY(
+ OkIf(capacityLimit < maxAllowedCapacity),
+ ([&capacityLimit](const auto&) { capacityLimit = maxAllowedCapacity; }));
+
+ return capacityLimit;
+}
+
+bool IsOriginUnaccessed(const FullOriginMetadata& aFullOriginMetadata,
+ const int64_t aRecentTime) {
+ if (aFullOriginMetadata.mLastAccessTime > aRecentTime) {
+ return false;
+ }
+
+ return (aRecentTime - aFullOriginMetadata.mLastAccessTime) / PR_USEC_PER_SEC >
+ StaticPrefs::dom_quotaManager_unaccessedForLongTimeThresholdSec();
+}
+
+} // namespace
+
+/*******************************************************************************
+ * Exported functions
+ ******************************************************************************/
+
+void InitializeQuotaManager() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gQuotaManagerInitialized);
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ ScopedLogExtraInfo::Initialize();
+#endif
+
+ if (!QuotaManager::IsRunningGTests()) {
+ // This service has to be started on the main thread currently.
+ const nsCOMPtr<mozIStorageService> ss =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+
+ QM_WARNONLY_TRY(OkIf(ss));
+ }
+
+ QM_WARNONLY_TRY(QM_TO_RESULT(QuotaManager::Initialize()));
+
+#ifdef DEBUG
+ gQuotaManagerInitialized = true;
+#endif
+}
+
+PQuotaParent* AllocPQuotaParent() {
+ AssertIsOnBackgroundThread();
+
+ if (NS_WARN_IF(QuotaManager::IsShuttingDown())) {
+ return nullptr;
+ }
+
+ auto actor = MakeRefPtr<Quota>();
+
+ return actor.forget().take();
+}
+
+bool DeallocPQuotaParent(PQuotaParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ RefPtr<Quota> actor = dont_AddRef(static_cast<Quota*>(aActor));
+ return true;
+}
+
+bool RecvShutdownQuotaManager() {
+ AssertIsOnBackgroundThread();
+
+ // If we are already in shutdown, don't call ShutdownInstance()
+ // again and return true immediately. We shall see this incident
+ // in Telemetry.
+ // XXX todo: Make QM_TRY stacks thread-aware (Bug 1735124)
+ // XXX todo: Active QM_TRY context for shutdown (Bug 1735170)
+ QM_TRY(OkIf(!gShutdown), true);
+
+ QuotaManager::ShutdownInstance();
+
+ return true;
+}
+
+QuotaManager::Observer* QuotaManager::Observer::sInstance = nullptr;
+
+// static
+nsresult QuotaManager::Observer::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Observer> observer = new Observer();
+
+ nsresult rv = observer->Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ sInstance = observer;
+
+ return NS_OK;
+}
+
+// static
+nsIObserver* QuotaManager::Observer::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return sInstance;
+}
+
+// static
+void QuotaManager::Observer::ShutdownCompleted() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(sInstance);
+
+ sInstance->mShutdownComplete = true;
+}
+
+nsresult QuotaManager::Observer::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // XXX: Improve the way that we remove observer in failure cases.
+ nsresult rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = obs->AddObserver(this, kProfileDoChangeTopic, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ return rv;
+ }
+
+ rv = obs->AddObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ obs->RemoveObserver(this, kProfileDoChangeTopic);
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ return rv;
+ }
+
+ rv = obs->AddObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ obs->RemoveObserver(this, kProfileDoChangeTopic);
+ obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
+ obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID);
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult QuotaManager::Observer::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (NS_WARN_IF(!obs)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_WIDGET_WAKE_OBSERVER_TOPIC));
+ MOZ_ALWAYS_SUCCEEDS(
+ obs->RemoveObserver(this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID));
+ MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, kProfileDoChangeTopic));
+ MOZ_ALWAYS_SUCCEEDS(obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
+
+ sInstance = nullptr;
+
+ // In general, the instance will have died after the latter removal call, so
+ // it's not safe to do anything after that point.
+ // However, Shutdown is currently called from Observe which is called by the
+ // Observer Service which holds a strong reference to the observer while the
+ // Observe method is being called.
+
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(QuotaManager::Observer, nsIObserver)
+
+NS_IMETHODIMP
+QuotaManager::Observer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv;
+
+ if (!strcmp(aTopic, kProfileDoChangeTopic)) {
+ if (NS_WARN_IF(gBasePath)) {
+ NS_WARNING(
+ "profile-before-change-qm must precede repeated "
+ "profile-do-change!");
+ return NS_OK;
+ }
+
+ Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, true);
+
+ gBasePath = new nsString();
+
+ nsCOMPtr<nsIFile> baseDir;
+ rv = NS_GetSpecialDirectory(NS_APP_INDEXEDDB_PARENT_DIR,
+ getter_AddRefs(baseDir));
+ if (NS_FAILED(rv)) {
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(baseDir));
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = baseDir->GetPath(*gBasePath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ gStorageName = new nsString();
+
+ rv = Preferences::GetString("dom.quotaManager.storageName", *gStorageName);
+ if (NS_FAILED(rv)) {
+ *gStorageName = kStorageName;
+ }
+
+ gBuildId = new nsCString();
+
+ nsCOMPtr<nsIPlatformInfo> platformInfo =
+ do_GetService("@mozilla.org/xre/app-info;1");
+ if (NS_WARN_IF(!platformInfo)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = platformInfo->GetPlatformBuildID(*gBuildId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) {
+ if (NS_WARN_IF(!gBasePath)) {
+ NS_WARNING("profile-do-change must precede profile-before-change-qm!");
+ return NS_OK;
+ }
+
+ // mPendingProfileChange is our re-entrancy guard (the nested event loop
+ // below may cause re-entrancy).
+ if (mPendingProfileChange) {
+ return NS_OK;
+ }
+
+ AutoRestore<bool> pending(mPendingProfileChange);
+ mPendingProfileChange = true;
+
+ mShutdownComplete = false;
+
+ PBackgroundChild* backgroundActor =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_WARN_IF(!backgroundActor->SendShutdownQuotaManager())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
+ "QuotaManager::Observer::Observe profile-before-change-qm"_ns,
+ [&]() { return mShutdownComplete; }));
+
+ gBasePath = nullptr;
+
+ gStorageName = nullptr;
+
+ gBuildId = nullptr;
+
+ Telemetry::SetEventRecordingEnabled("dom.quota.try"_ns, false);
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ rv = Shutdown();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, NS_WIDGET_WAKE_OBSERVER_TOPIC)) {
+ gLastOSWake = TimeStamp::Now();
+
+ return NS_OK;
+ }
+
+ NS_WARNING("Unknown observer topic!");
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * Quota manager
+ ******************************************************************************/
+
+QuotaManager::QuotaManager(const nsAString& aBasePath,
+ const nsAString& aStorageName)
+ : mQuotaMutex("QuotaManager.mQuotaMutex"),
+ mBasePath(aBasePath),
+ mStorageName(aStorageName),
+ mTemporaryStorageUsage(0),
+ mNextDirectoryLockId(0),
+ mTemporaryStorageInitialized(false),
+ mCacheUsable(false),
+ mShuttingDownStorage(false) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!gInstance);
+}
+
+QuotaManager::~QuotaManager() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(!gInstance || gInstance == this);
+}
+
+// static
+nsresult QuotaManager::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = Observer::Initialize();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// static
+Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult>
+QuotaManager::GetOrCreate() {
+ AssertIsOnBackgroundThread();
+
+ if (gInstance) {
+ return WrapMovingNotNullUnchecked(RefPtr<QuotaManager>{gInstance});
+ }
+
+ QM_TRY(OkIf(gBasePath), Err(NS_ERROR_FAILURE), [](const auto&) {
+ NS_WARNING(
+ "Trying to create QuotaManager before profile-do-change! "
+ "Forgot to call do_get_profile()?");
+ });
+
+ QM_TRY(OkIf(!IsShuttingDown()), Err(NS_ERROR_FAILURE), [](const auto&) {
+ MOZ_ASSERT(false,
+ "Trying to create QuotaManager after profile-before-change-qm!");
+ });
+
+ auto instance = MakeRefPtr<QuotaManager>(*gBasePath, *gStorageName);
+
+ QM_TRY(MOZ_TO_RESULT(instance->Init()));
+
+ gInstance = instance;
+
+ return WrapMovingNotNullUnchecked(std::move(instance));
+}
+
+Result<Ok, nsresult> QuotaManager::EnsureCreated() {
+ AssertIsOnBackgroundThread();
+
+ QM_TRY_RETURN(GetOrCreate().map([](const auto& res) { return Ok{}; }))
+}
+
+// static
+QuotaManager* QuotaManager::Get() {
+ // Does not return an owning reference.
+ return gInstance;
+}
+
+// static
+nsIObserver* QuotaManager::GetObserver() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return Observer::GetInstance();
+}
+
+// static
+bool QuotaManager::IsShuttingDown() { return gShutdown; }
+
+// static
+void QuotaManager::ShutdownInstance() {
+ AssertIsOnBackgroundThread();
+
+ if (gInstance) {
+ gInstance->Shutdown();
+
+ gInstance = nullptr;
+ }
+
+ RefPtr<Runnable> runnable =
+ NS_NewRunnableFunction("dom::quota::QuotaManager::ShutdownCompleted",
+ []() { Observer::ShutdownCompleted(); });
+ MOZ_ASSERT(runnable);
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(runnable.forget()));
+}
+
+// static
+void QuotaManager::Reset() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(!gInstance);
+ MOZ_ASSERT(gShutdown);
+
+ gShutdown = false;
+}
+
+// static
+bool QuotaManager::IsOSMetadata(const nsAString& aFileName) {
+ return aFileName.EqualsLiteral(DSSTORE_FILE_NAME) ||
+ aFileName.EqualsLiteral(DESKTOP_FILE_NAME) ||
+ aFileName.LowerCaseEqualsLiteral(DESKTOP_INI_FILE_NAME) ||
+ aFileName.LowerCaseEqualsLiteral(THUMBS_DB_FILE_NAME);
+}
+
+// static
+bool QuotaManager::IsDotFile(const nsAString& aFileName) {
+ return aFileName.First() == char16_t('.');
+}
+
+void QuotaManager::RegisterDirectoryLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ mDirectoryLocks.AppendElement(WrapNotNullUnchecked(&aLock));
+
+ if (aLock.ShouldUpdateLockIdTable()) {
+ MutexAutoLock lock(mQuotaMutex);
+
+ MOZ_DIAGNOSTIC_ASSERT(!mDirectoryLockIdTable.Contains(aLock.Id()));
+ mDirectoryLockIdTable.InsertOrUpdate(aLock.Id(),
+ WrapNotNullUnchecked(&aLock));
+ }
+
+ if (aLock.ShouldUpdateLockTable()) {
+ DirectoryLockTable& directoryLockTable =
+ GetDirectoryLockTable(aLock.GetPersistenceType());
+
+ // XXX It seems that the contents of the array are never actually used, we
+ // just use that like an inefficient use counter. Can't we just change
+ // DirectoryLockTable to a nsTHashMap<nsCStringHashKey, uint32_t>?
+ directoryLockTable
+ .LookupOrInsertWith(
+ aLock.Origin(),
+ [this, &aLock] {
+ if (!IsShuttingDown()) {
+ UpdateOriginAccessTime(aLock.GetPersistenceType(),
+ aLock.OriginMetadata());
+ }
+ return MakeUnique<nsTArray<NotNull<DirectoryLockImpl*>>>();
+ })
+ ->AppendElement(WrapNotNullUnchecked(&aLock));
+ }
+
+ aLock.SetRegistered(true);
+}
+
+void QuotaManager::UnregisterDirectoryLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ MOZ_ALWAYS_TRUE(mDirectoryLocks.RemoveElement(&aLock));
+
+ if (aLock.ShouldUpdateLockIdTable()) {
+ MutexAutoLock lock(mQuotaMutex);
+
+ MOZ_DIAGNOSTIC_ASSERT(mDirectoryLockIdTable.Contains(aLock.Id()));
+ mDirectoryLockIdTable.Remove(aLock.Id());
+ }
+
+ if (aLock.ShouldUpdateLockTable()) {
+ DirectoryLockTable& directoryLockTable =
+ GetDirectoryLockTable(aLock.GetPersistenceType());
+
+ nsTArray<NotNull<DirectoryLockImpl*>>* array;
+ MOZ_ALWAYS_TRUE(directoryLockTable.Get(aLock.Origin(), &array));
+
+ MOZ_ALWAYS_TRUE(array->RemoveElement(&aLock));
+ if (array->IsEmpty()) {
+ directoryLockTable.Remove(aLock.Origin());
+
+ if (!IsShuttingDown()) {
+ UpdateOriginAccessTime(aLock.GetPersistenceType(),
+ aLock.OriginMetadata());
+ }
+ }
+ }
+
+ aLock.SetRegistered(false);
+}
+
+void QuotaManager::AddPendingDirectoryLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ mPendingDirectoryLocks.AppendElement(&aLock);
+}
+
+void QuotaManager::RemovePendingDirectoryLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ MOZ_ALWAYS_TRUE(mPendingDirectoryLocks.RemoveElement(&aLock));
+}
+
+uint64_t QuotaManager::CollectOriginsForEviction(
+ uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aLocks.IsEmpty());
+
+ // XXX This looks as if this could/should also use CollectLRUOriginInfosUntil,
+ // or maybe a generalization if that.
+
+ struct MOZ_STACK_CLASS Helper final {
+ static void GetInactiveOriginInfos(
+ const nsTArray<NotNull<RefPtr<OriginInfo>>>& aOriginInfos,
+ const nsTArray<NotNull<const DirectoryLockImpl*>>& aLocks,
+ OriginInfosFlatTraversable& aInactiveOriginInfos) {
+ for (const auto& originInfo : aOriginInfos) {
+ MOZ_ASSERT(originInfo->mGroupInfo->mPersistenceType !=
+ PERSISTENCE_TYPE_PERSISTENT);
+
+ if (originInfo->LockedPersisted()) {
+ continue;
+ }
+
+ // Never evict PERSISTENCE_TYPE_DEFAULT data associated to a
+ // moz-extension origin, unlike websites (which may more likely using
+ // the local data as a cache but still able to retrieve the same data
+ // from the server side) extensions do not have the same data stored
+ // anywhere else and evicting the data would result into potential data
+ // loss for the users.
+ //
+ // Also, unlike a website the extensions are explicitly installed and
+ // uninstalled by the user and all data associated to the extension
+ // principal will be completely removed once the addon is uninstalled.
+ if (originInfo->mGroupInfo->mPersistenceType !=
+ PERSISTENCE_TYPE_TEMPORARY &&
+ originInfo->IsExtensionOrigin()) {
+ continue;
+ }
+
+ const auto originScope = OriginScope::FromOrigin(originInfo->mOrigin);
+
+ const bool match =
+ std::any_of(aLocks.begin(), aLocks.end(),
+ [&originScope](const DirectoryLockImpl* const lock) {
+ return originScope.Matches(lock->GetOriginScope());
+ });
+
+ if (!match) {
+ MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count(),
+ "Inactive origin shouldn't have open files!");
+ aInactiveOriginInfos.InsertElementSorted(
+ originInfo, OriginInfoAccessTimeComparator());
+ }
+ }
+ }
+ };
+
+ // Split locks into separate arrays and filter out locks for persistent
+ // storage, they can't block us.
+ const auto [temporaryStorageLocks, defaultStorageLocks] = [this] {
+ nsTArray<NotNull<const DirectoryLockImpl*>> temporaryStorageLocks;
+ nsTArray<NotNull<const DirectoryLockImpl*>> defaultStorageLocks;
+ for (NotNull<const DirectoryLockImpl*> const lock : mDirectoryLocks) {
+ const Nullable<PersistenceType>& persistenceType =
+ lock->NullablePersistenceType();
+
+ if (persistenceType.IsNull()) {
+ temporaryStorageLocks.AppendElement(lock);
+ defaultStorageLocks.AppendElement(lock);
+ } else if (persistenceType.Value() == PERSISTENCE_TYPE_TEMPORARY) {
+ temporaryStorageLocks.AppendElement(lock);
+ } else if (persistenceType.Value() == PERSISTENCE_TYPE_DEFAULT) {
+ defaultStorageLocks.AppendElement(lock);
+ } else {
+ MOZ_ASSERT(persistenceType.Value() == PERSISTENCE_TYPE_PERSISTENT);
+
+ // Do nothing here, persistent origins don't need to be collected ever.
+ }
+ }
+
+ return std::pair(std::move(temporaryStorageLocks),
+ std::move(defaultStorageLocks));
+ }();
+
+ // Enumerate and process inactive origins. This must be protected by the
+ // mutex.
+ MutexAutoLock lock(mQuotaMutex);
+
+ const auto [inactiveOrigins, sizeToBeFreed] =
+ [this, &temporaryStorageLocks = temporaryStorageLocks,
+ &defaultStorageLocks = defaultStorageLocks, aMinSizeToBeFreed] {
+ nsTArray<NotNull<RefPtr<const OriginInfo>>> inactiveOrigins;
+ for (const auto& entry : mGroupInfoPairs) {
+ const auto& pair = entry.GetData();
+
+ MOZ_ASSERT(!entry.GetKey().IsEmpty());
+ MOZ_ASSERT(pair);
+
+ RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
+ if (groupInfo) {
+ Helper::GetInactiveOriginInfos(groupInfo->mOriginInfos,
+ temporaryStorageLocks,
+ inactiveOrigins);
+ }
+
+ groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
+ if (groupInfo) {
+ Helper::GetInactiveOriginInfos(
+ groupInfo->mOriginInfos, defaultStorageLocks, inactiveOrigins);
+ }
+ }
+
+#ifdef DEBUG
+ // Make sure the array is sorted correctly.
+ const bool inactiveOriginsSorted =
+ std::is_sorted(inactiveOrigins.cbegin(), inactiveOrigins.cend(),
+ [](const auto& lhs, const auto& rhs) {
+ return lhs->mAccessTime < rhs->mAccessTime;
+ });
+ MOZ_ASSERT(inactiveOriginsSorted);
+#endif
+
+ // Create a list of inactive and the least recently used origins
+ // whose aggregate size is greater or equals the minimal size to be
+ // freed.
+ uint64_t sizeToBeFreed = 0;
+ for (uint32_t count = inactiveOrigins.Length(), index = 0;
+ index < count; index++) {
+ if (sizeToBeFreed >= aMinSizeToBeFreed) {
+ inactiveOrigins.TruncateLength(index);
+ break;
+ }
+
+ sizeToBeFreed += inactiveOrigins[index]->LockedUsage();
+ }
+
+ return std::pair(std::move(inactiveOrigins), sizeToBeFreed);
+ }();
+
+ if (sizeToBeFreed >= aMinSizeToBeFreed) {
+ // Success, add directory locks for these origins, so any other
+ // operations for them will be delayed (until origin eviction is finalized).
+
+ for (const auto& originInfo : inactiveOrigins) {
+ auto lock = DirectoryLockImpl::CreateForEviction(
+ WrapNotNullUnchecked(this), originInfo->mGroupInfo->mPersistenceType,
+ originInfo->FlattenToOriginMetadata());
+
+ lock->AcquireImmediately();
+
+ aLocks.AppendElement(lock.forget());
+ }
+
+ return sizeToBeFreed;
+ }
+
+ return 0;
+}
+
+template <typename P>
+void QuotaManager::CollectPendingOriginsForListing(P aPredicate) {
+ MutexAutoLock lock(mQuotaMutex);
+
+ for (const auto& entry : mGroupInfoPairs) {
+ const auto& pair = entry.GetData();
+
+ MOZ_ASSERT(!entry.GetKey().IsEmpty());
+ MOZ_ASSERT(pair);
+
+ RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
+ if (groupInfo) {
+ for (const auto& originInfo : groupInfo->mOriginInfos) {
+ if (!originInfo->mDirectoryExists) {
+ aPredicate(originInfo);
+ }
+ }
+ }
+ }
+}
+
+nsresult QuotaManager::Init() {
+ AssertIsOnOwningThread();
+
+#ifdef XP_WIN
+ CacheUseDOSDevicePathSyntaxPrefValue();
+#endif
+
+ QM_TRY_INSPECT(const auto& baseDir, QM_NewLocalFile(mBasePath));
+
+ QM_TRY_UNWRAP(
+ do_Init(mIndexedDBPath),
+ GetPathForStorage(*baseDir, nsLiteralString(INDEXEDDB_DIRECTORY_NAME)));
+
+ QM_TRY(MOZ_TO_RESULT(baseDir->Append(mStorageName)));
+
+ QM_TRY_UNWRAP(do_Init(mStoragePath),
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, baseDir, GetPath));
+
+ QM_TRY_UNWRAP(
+ do_Init(mStorageArchivesPath),
+ GetPathForStorage(*baseDir, nsLiteralString(ARCHIVES_DIRECTORY_NAME)));
+
+ QM_TRY_UNWRAP(
+ do_Init(mPermanentStoragePath),
+ GetPathForStorage(*baseDir, nsLiteralString(PERMANENT_DIRECTORY_NAME)));
+
+ QM_TRY_UNWRAP(
+ do_Init(mTemporaryStoragePath),
+ GetPathForStorage(*baseDir, nsLiteralString(TEMPORARY_DIRECTORY_NAME)));
+
+ QM_TRY_UNWRAP(
+ do_Init(mDefaultStoragePath),
+ GetPathForStorage(*baseDir, nsLiteralString(DEFAULT_DIRECTORY_NAME)));
+
+ QM_TRY_UNWRAP(do_Init(mIOThread),
+ MOZ_TO_RESULT_INVOKE_TYPED(
+ nsCOMPtr<nsIThread>, MOZ_SELECT_OVERLOAD(NS_NewNamedThread),
+ "QuotaManager IO"));
+
+ static_assert(Client::IDB == 0 && Client::DOMCACHE == 1 && Client::SDB == 2 &&
+ Client::FILESYSTEM == 3 && Client::LS == 4 &&
+ Client::TYPE_MAX == 5,
+ "Fix the registration!");
+
+ // Register clients.
+ auto clients = decltype(mClients)::ValueType{};
+ clients.AppendElement(indexedDB::CreateQuotaClient());
+ clients.AppendElement(cache::CreateQuotaClient());
+ clients.AppendElement(simpledb::CreateQuotaClient());
+ clients.AppendElement(fs::CreateQuotaClient());
+ if (NextGenLocalStorageEnabled()) {
+ clients.AppendElement(localstorage::CreateQuotaClient());
+ } else {
+ clients.SetLength(Client::TypeMax());
+ }
+
+ mClients.init(std::move(clients));
+
+ MOZ_ASSERT(mClients->Capacity() == Client::TYPE_MAX,
+ "Should be using an auto array with correct capacity!");
+
+ mAllClientTypes.init(ClientTypesArray{
+ Client::Type::IDB, Client::Type::DOMCACHE, Client::Type::SDB,
+ Client::Type::FILESYSTEM, Client::Type::LS});
+ mAllClientTypesExceptLS.init(
+ ClientTypesArray{Client::Type::IDB, Client::Type::DOMCACHE,
+ Client::Type::SDB, Client::Type::FILESYSTEM});
+
+ return NS_OK;
+}
+
+// static
+void QuotaManager::MaybeRecordQuotaClientShutdownStep(
+ const Client::Type aClientType, const nsACString& aStepDescription) {
+ // Callable on any thread.
+
+ auto* const quotaManager = QuotaManager::Get();
+ MOZ_DIAGNOSTIC_ASSERT(quotaManager);
+
+ if (quotaManager->IsShuttingDown()) {
+ quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
+ }
+}
+
+// static
+void QuotaManager::SafeMaybeRecordQuotaClientShutdownStep(
+ const Client::Type aClientType, const nsACString& aStepDescription) {
+ // Callable on any thread.
+
+ auto* const quotaManager = QuotaManager::Get();
+
+ if (quotaManager && quotaManager->IsShuttingDown()) {
+ quotaManager->RecordShutdownStep(Some(aClientType), aStepDescription);
+ }
+}
+
+void QuotaManager::RecordQuotaManagerShutdownStep(
+ const nsACString& aStepDescription) {
+ // Callable on any thread.
+ MOZ_ASSERT(mShutdownStarted);
+
+ RecordShutdownStep(Nothing{}, aStepDescription);
+}
+
+void QuotaManager::MaybeRecordQuotaManagerShutdownStep(
+ const nsACString& aStepDescription) {
+ // Callable on any thread.
+
+ if (IsShuttingDown()) {
+ RecordQuotaManagerShutdownStep(aStepDescription);
+ }
+}
+
+void QuotaManager::RecordShutdownStep(const Maybe<Client::Type> aClientType,
+ const nsACString& aStepDescription) {
+ MOZ_ASSERT(IsShuttingDown());
+
+ const TimeDuration elapsedSinceShutdownStart =
+ TimeStamp::NowLoRes() - *mShutdownStartedAt;
+
+ const auto stepString =
+ nsPrintfCString("%fs: %s", elapsedSinceShutdownStart.ToSeconds(),
+ nsPromiseFlatCString(aStepDescription).get());
+
+ if (aClientType) {
+ AssertIsOnBackgroundThread();
+
+ mShutdownSteps[*aClientType].Append(stepString + "\n"_ns);
+ } else {
+ // Callable on any thread.
+ MutexAutoLock lock(mQuotaMutex);
+
+ mQuotaManagerShutdownSteps.Append(stepString + "\n"_ns);
+ }
+
+#ifdef DEBUG
+ // XXX Probably this isn't the mechanism that should be used here.
+
+ NS_DebugBreak(
+ NS_DEBUG_WARNING,
+ nsAutoCString(aClientType ? Client::TypeToText(*aClientType)
+ : "quota manager"_ns + " shutdown step"_ns)
+ .get(),
+ stepString.get(), __FILE__, __LINE__);
+#endif
+}
+
+void QuotaManager::Shutdown() {
+ AssertIsOnOwningThread();
+ MOZ_DIAGNOSTIC_ASSERT(!gShutdown);
+
+ // Define some local helper functions
+
+ auto flagShutdownStarted = [this]() {
+ // Setting this flag prevents the service from being recreated and prevents
+ // further storages from being created.
+ // XXX: Harmonize QM shutdown flags, see bug 1726714
+ gShutdown = true;
+
+ // StopIdleMaintenance used to happen before mShutdownStarted is set true
+ // but it is just an internal flag for the recording of shutdown steps
+ // and not evaluated elsewhere.
+
+ mShutdownStartedAt.init(TimeStamp::NowLoRes());
+ mShutdownStarted = true;
+ };
+
+ nsCOMPtr<nsITimer> crashBrowserTimer;
+
+ auto crashBrowserTimerCallback = [](nsITimer* aTimer, void* aClosure) {
+ auto* const quotaManager = static_cast<QuotaManager*>(aClosure);
+
+ nsCString annotation;
+
+ for (Client::Type type : quotaManager->AllClientTypes()) {
+ auto& quotaClient = *(*quotaManager->mClients)[type];
+
+ if (!quotaClient.IsShutdownCompleted()) {
+ annotation.AppendPrintf("%s: %s\nIntermediate steps:\n%s\n\n",
+ Client::TypeToText(type).get(),
+ quotaClient.GetShutdownStatus().get(),
+ quotaManager->mShutdownSteps[type].get());
+ }
+ }
+
+ if (gNormalOriginOps) {
+ annotation.AppendPrintf("QM: %zu normal origin ops pending\n",
+ gNormalOriginOps->Length());
+#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY
+ for (const auto& op : *gNormalOriginOps) {
+ nsCString name;
+ op->GetName(name);
+ annotation.AppendPrintf("Op: %s pending\n", name.get());
+ }
+#endif
+ }
+ {
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ annotation.AppendPrintf("Intermediate steps:\n%s\n",
+ quotaManager->mQuotaManagerShutdownSteps.get());
+ }
+
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::QuotaManagerShutdownTimeout, annotation);
+
+ MOZ_CRASH("Quota manager shutdown timed out");
+ };
+
+ auto startCrashBrowserTimer = [&]() {
+ crashBrowserTimer = NS_NewTimer();
+ MOZ_ASSERT(crashBrowserTimer);
+ if (crashBrowserTimer) {
+ RecordQuotaManagerShutdownStep("startCrashBrowserTimer"_ns);
+ MOZ_ALWAYS_SUCCEEDS(crashBrowserTimer->InitWithNamedFuncCallback(
+ crashBrowserTimerCallback, this, SHUTDOWN_CRASH_BROWSER_TIMEOUT_MS,
+ nsITimer::TYPE_ONE_SHOT,
+ "quota::QuotaManager::Shutdown::crashBrowserTimer"));
+ }
+ };
+
+ auto stopCrashBrowserTimer = [&]() {
+ if (crashBrowserTimer) {
+ RecordQuotaManagerShutdownStep("stopCrashBrowserTimer"_ns);
+ QM_WARNONLY_TRY(QM_TO_RESULT(crashBrowserTimer->Cancel()));
+ }
+ };
+
+ auto initiateShutdownWorkThreads = [this]() {
+ RecordQuotaManagerShutdownStep("initiateShutdownWorkThreads"_ns);
+ bool needsToWait = false;
+ for (Client::Type type : AllClientTypes()) {
+ // Clients are supposed to also AbortAllOperations from this point on
+ // to speed up shutdown, if possible. Thus pending operations
+ // might not be executed anymore.
+ needsToWait |= (*mClients)[type]->InitiateShutdownWorkThreads();
+ }
+
+ return needsToWait;
+ };
+
+ nsCOMPtr<nsITimer> killActorsTimer;
+
+ auto killActorsTimerCallback = [](nsITimer* aTimer, void* aClosure) {
+ auto* const quotaManager = static_cast<QuotaManager*>(aClosure);
+
+ quotaManager->RecordQuotaManagerShutdownStep("killActorsTimerCallback"_ns);
+
+ // XXX: This abort is a workaround to unblock shutdown, which
+ // ought to be removed by bug 1682326. We probably need more
+ // checks to immediately abort new operations during
+ // shutdown.
+ quotaManager->GetClient(Client::IDB)->AbortAllOperations();
+
+ for (Client::Type type : quotaManager->AllClientTypes()) {
+ quotaManager->GetClient(type)->ForceKillActors();
+ }
+ };
+
+ auto startKillActorsTimer = [&]() {
+ killActorsTimer = NS_NewTimer();
+ MOZ_ASSERT(killActorsTimer);
+ if (killActorsTimer) {
+ RecordQuotaManagerShutdownStep("startKillActorsTimer"_ns);
+ MOZ_ALWAYS_SUCCEEDS(killActorsTimer->InitWithNamedFuncCallback(
+ killActorsTimerCallback, this, SHUTDOWN_KILL_ACTORS_TIMEOUT_MS,
+ nsITimer::TYPE_ONE_SHOT,
+ "quota::QuotaManager::Shutdown::killActorsTimer"));
+ }
+ };
+
+ auto stopKillActorsTimer = [&]() {
+ if (killActorsTimer) {
+ RecordQuotaManagerShutdownStep("stopKillActorsTimer"_ns);
+ QM_WARNONLY_TRY(QM_TO_RESULT(killActorsTimer->Cancel()));
+ }
+ };
+
+ auto isAllClientsShutdownComplete = [this] {
+ return std::all_of(AllClientTypes().cbegin(), AllClientTypes().cend(),
+ [&self = *this](const auto type) {
+ return (*self.mClients)[type]->IsShutdownCompleted();
+ });
+ };
+
+ auto shutdownAndJoinWorkThreads = [this]() {
+ RecordQuotaManagerShutdownStep("shutdownAndJoinWorkThreads"_ns);
+ for (Client::Type type : AllClientTypes()) {
+ (*mClients)[type]->FinalizeShutdownWorkThreads();
+ }
+ };
+
+ auto shutdownAndJoinIOThread = [this]() {
+ RecordQuotaManagerShutdownStep("shutdownAndJoinIOThread"_ns);
+
+ // Make sure to join with our IO thread.
+ QM_WARNONLY_TRY(QM_TO_RESULT((*mIOThread)->Shutdown()));
+ };
+
+ auto invalidatePendingDirectoryLocks = [this]() {
+ RecordQuotaManagerShutdownStep("invalidatePendingDirectoryLocks"_ns);
+ for (RefPtr<DirectoryLockImpl>& lock : mPendingDirectoryLocks) {
+ lock->Invalidate();
+ }
+ };
+
+ // Body of the function
+ ScopedLogExtraInfo scope{ScopedLogExtraInfo::kTagContext,
+ "dom::quota::QuotaManager::Shutdown"_ns};
+
+ // This must be called before `flagShutdownStarted`, it would fail otherwise.
+ // `ShutdownStorageOp` needs to acquire an exclusive directory lock over
+ // entire <profile>/storage which will abort any existing operations and wait
+ // for all existing directory locks to be released. So the shutdown operation
+ // will effectively run after all existing operations.
+ // We don't need to use the returned promise here because `ShutdownStorage`
+ // registers `ShudownStorageOp` in `gNormalOriginOps`.
+ ShutdownStorage();
+
+ flagShutdownStarted();
+
+ startCrashBrowserTimer();
+
+ // XXX: StopIdleMaintenance now just notifies all clients to abort any
+ // maintenance work.
+ // This could be done as part of QuotaClient::AbortAllOperations.
+ StopIdleMaintenance();
+
+ // XXX In theory, we could simplify the code below (and also the `Client`
+ // interface) by removing the `initiateShutdownWorkThreads` and
+ // `isAllClientsShutdownComplete` calls because it should be sufficient
+ // to rely on `ShutdownStorage` to abort all existing operations and to
+ // wait for all existing directory locks to be released as well.
+
+ const bool needsToWait =
+ initiateShutdownWorkThreads() || static_cast<bool>(gNormalOriginOps);
+
+ // If any clients cannot shutdown immediately, spin the event loop while we
+ // wait on all the threads to close.
+ if (needsToWait) {
+ startKillActorsTimer();
+
+ MOZ_ALWAYS_TRUE(SpinEventLoopUntil(
+ "QuotaManager::Shutdown"_ns, [isAllClientsShutdownComplete]() {
+ return !gNormalOriginOps && isAllClientsShutdownComplete();
+ }));
+
+ stopKillActorsTimer();
+ }
+
+ shutdownAndJoinWorkThreads();
+
+ shutdownAndJoinIOThread();
+
+ invalidatePendingDirectoryLocks();
+
+ stopCrashBrowserTimer();
+}
+
+void QuotaManager::InitQuotaForOrigin(
+ const FullOriginMetadata& aFullOriginMetadata,
+ const ClientUsageArray& aClientUsages, uint64_t aUsageBytes) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(IsBestEffortPersistenceType(aFullOriginMetadata.mPersistenceType));
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
+ aFullOriginMetadata.mPersistenceType, aFullOriginMetadata.mSuffix,
+ aFullOriginMetadata.mGroup);
+
+ groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
+ groupInfo, aFullOriginMetadata.mOrigin, aClientUsages, aUsageBytes,
+ aFullOriginMetadata.mLastAccessTime, aFullOriginMetadata.mPersisted,
+ /* aDirectoryExists */ true));
+}
+
+void QuotaManager::EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType));
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
+ aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix,
+ aOriginMetadata.mGroup);
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+ if (!originInfo) {
+ groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
+ groupInfo, aOriginMetadata.mOrigin, ClientUsageArray(),
+ /* aUsageBytes */ 0,
+ /* aAccessTime */ PR_Now(), /* aPersisted */ false,
+ /* aDirectoryExists */ false));
+ }
+}
+
+int64_t QuotaManager::NoteOriginDirectoryCreated(
+ const OriginMetadata& aOriginMetadata, bool aPersisted) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(IsBestEffortPersistenceType(aOriginMetadata.mPersistenceType));
+
+ int64_t timestamp;
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<GroupInfo> groupInfo = LockedGetOrCreateGroupInfo(
+ aOriginMetadata.mPersistenceType, aOriginMetadata.mSuffix,
+ aOriginMetadata.mGroup);
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+ if (originInfo) {
+ timestamp = originInfo->LockedAccessTime();
+ originInfo->mPersisted = aPersisted;
+ originInfo->mDirectoryExists = true;
+ } else {
+ timestamp = PR_Now();
+ groupInfo->LockedAddOriginInfo(MakeNotNull<RefPtr<OriginInfo>>(
+ groupInfo, aOriginMetadata.mOrigin, ClientUsageArray(),
+ /* aUsageBytes */ 0,
+ /* aAccessTime */ timestamp, aPersisted, /* aDirectoryExists */ true));
+ }
+
+ return timestamp;
+}
+
+void QuotaManager::DecreaseUsageForClient(const ClientMetadata& aClientMetadata,
+ int64_t aSize) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
+ return;
+ }
+
+ RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
+ if (!groupInfo) {
+ return;
+ }
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
+ if (originInfo) {
+ originInfo->LockedDecreaseUsage(aClientMetadata.mClientType, aSize);
+ }
+}
+
+void QuotaManager::ResetUsageForClient(const ClientMetadata& aClientMetadata) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(IsBestEffortPersistenceType(aClientMetadata.mPersistenceType));
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aClientMetadata.mGroup, &pair)) {
+ return;
+ }
+
+ RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(aClientMetadata.mPersistenceType);
+ if (!groupInfo) {
+ return;
+ }
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aClientMetadata.mOrigin);
+ if (originInfo) {
+ originInfo->LockedResetUsageForClient(aClientMetadata.mClientType);
+ }
+}
+
+UsageInfo QuotaManager::GetUsageForClient(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ return UsageInfo{};
+ }
+
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
+ if (!groupInfo) {
+ return UsageInfo{};
+ }
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+ if (!originInfo) {
+ return UsageInfo{};
+ }
+
+ return originInfo->LockedGetUsageForClient(aClientType);
+}
+
+void QuotaManager::UpdateOriginAccessTime(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+ MOZ_ASSERT(!IsShuttingDown());
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ return;
+ }
+
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
+ if (!groupInfo) {
+ return;
+ }
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+ if (originInfo) {
+ int64_t timestamp = PR_Now();
+ originInfo->LockedUpdateAccessTime(timestamp);
+
+ MutexAutoUnlock autoUnlock(mQuotaMutex);
+
+ auto op = MakeRefPtr<SaveOriginAccessTimeOp>(
+ aPersistenceType, aOriginMetadata.mOrigin, timestamp);
+
+ RegisterNormalOriginOp(*op);
+
+ op->RunImmediately();
+ }
+}
+
+void QuotaManager::RemoveQuota() {
+ AssertIsOnIOThread();
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ for (const auto& entry : mGroupInfoPairs) {
+ const auto& pair = entry.GetData();
+
+ MOZ_ASSERT(!entry.GetKey().IsEmpty());
+ MOZ_ASSERT(pair);
+
+ RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
+ if (groupInfo) {
+ groupInfo->LockedRemoveOriginInfos();
+ }
+
+ groupInfo = pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
+ if (groupInfo) {
+ groupInfo->LockedRemoveOriginInfos();
+ }
+ }
+
+ mGroupInfoPairs.Clear();
+
+ MOZ_ASSERT(mTemporaryStorageUsage == 0, "Should be zero!");
+}
+
+nsresult QuotaManager::LoadQuota() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mStorageConnection);
+ MOZ_ASSERT(!mTemporaryStorageInitialized);
+
+ // A list of all unaccessed default or temporary origins.
+ nsTArray<FullOriginMetadata> unaccessedOrigins;
+
+ auto MaybeCollectUnaccessedOrigin =
+ [loadQuotaInfoStartTime = PR_Now(),
+ &unaccessedOrigins](auto& fullOriginMetadata) {
+ if (IsOriginUnaccessed(fullOriginMetadata, loadQuotaInfoStartTime)) {
+ unaccessedOrigins.AppendElement(std::move(fullOriginMetadata));
+ }
+ };
+
+ auto recordQuotaInfoLoadTimeHelper =
+ MakeRefPtr<RecordQuotaInfoLoadTimeHelper>();
+
+ const auto startTime = recordQuotaInfoLoadTimeHelper->Start();
+
+ auto LoadQuotaFromCache = [&]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
+ "SELECT repository_id, suffix, group_, "
+ "origin, client_usages, usage, "
+ "last_access_time, accessed, persisted "
+ "FROM origin"_ns));
+
+ auto autoRemoveQuota = MakeScopeExit([&] {
+ RemoveQuota();
+ unaccessedOrigins.Clear();
+ });
+
+ QM_TRY(quota::CollectWhileHasResult(
+ *stmt,
+ [this,
+ &MaybeCollectUnaccessedOrigin](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const int32_t& repositoryId,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromInt32(repositoryId, fallible);
+ QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
+
+ FullOriginMetadata fullOriginMetadata;
+
+ fullOriginMetadata.mPersistenceType = maybePersistenceType.value();
+
+ QM_TRY_UNWRAP(fullOriginMetadata.mSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 1));
+
+ QM_TRY_UNWRAP(fullOriginMetadata.mGroup,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 2));
+
+ QM_TRY_UNWRAP(fullOriginMetadata.mOrigin,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 3));
+
+ QM_TRY_INSPECT(const auto& clientUsagesText,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, stmt,
+ GetUTF8String, 4));
+
+ ClientUsageArray clientUsages;
+ QM_TRY(MOZ_TO_RESULT(clientUsages.Deserialize(clientUsagesText)));
+
+ QM_TRY_INSPECT(const int64_t& usage,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 5));
+ QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt64, 6));
+ QM_TRY_INSPECT(const int64_t& accessed,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 7));
+ QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 8));
+
+ QM_TRY_INSPECT(const bool& groupUpdated,
+ MaybeUpdateGroupForOrigin(fullOriginMetadata));
+
+ Unused << groupUpdated;
+
+ QM_TRY_INSPECT(
+ const bool& lastAccessTimeUpdated,
+ MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));
+
+ Unused << lastAccessTimeUpdated;
+
+ // We don't need to update the .metadata-v2 file on disk here,
+ // EnsureTemporaryOriginIsInitialized is responsible for doing that.
+ // We just need to use correct group and last access time before
+ // initializing quota for the given origin. (Note that calling
+ // LoadFullOriginMetadataWithRestore below might update the group in
+ // the metadata file, but only as a side-effect. The actual place we
+ // ensure consistency is in EnsureTemporaryOriginIsInitialized.)
+
+ if (accessed) {
+ QM_TRY_INSPECT(
+ const auto& directory,
+ GetDirectoryForOrigin(fullOriginMetadata.mPersistenceType,
+ fullOriginMetadata.mOrigin));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+
+ QM_TRY(OkIf(exists), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, IsDirectory));
+
+ QM_TRY(OkIf(isDirectory), Err(NS_ERROR_FAILURE));
+
+ // Calling LoadFullOriginMetadataWithRestore might update the group
+ // in the metadata file, but only as a side-effect. The actual place
+ // we ensure consistency is in EnsureTemporaryOriginIsInitialized.
+
+ QM_TRY_INSPECT(const auto& metadata,
+ LoadFullOriginMetadataWithRestore(directory));
+
+ QM_TRY(OkIf(fullOriginMetadata.mLastAccessTime ==
+ metadata.mLastAccessTime),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(OkIf(fullOriginMetadata.mPersisted == metadata.mPersisted),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(OkIf(fullOriginMetadata.mPersistenceType ==
+ metadata.mPersistenceType),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(OkIf(fullOriginMetadata.mSuffix == metadata.mSuffix),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(OkIf(fullOriginMetadata.mGroup == metadata.mGroup),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(OkIf(fullOriginMetadata.mOrigin == metadata.mOrigin),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(MOZ_TO_RESULT(InitializeOrigin(
+ fullOriginMetadata.mPersistenceType, fullOriginMetadata,
+ fullOriginMetadata.mLastAccessTime,
+ fullOriginMetadata.mPersisted, directory)));
+ } else {
+ InitQuotaForOrigin(fullOriginMetadata, clientUsages, usage);
+ }
+
+ MaybeCollectUnaccessedOrigin(fullOriginMetadata);
+
+ return Ok{};
+ }));
+
+ autoRemoveQuota.release();
+
+ return NS_OK;
+ };
+
+ QM_TRY_INSPECT(
+ const bool& loadQuotaFromCache, ([this]() -> Result<bool, nsresult> {
+ if (mCacheUsable) {
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ CreateAndExecuteSingleStepStatement<
+ SingleStepResult::ReturnNullIfNoResult>(
+ *mStorageConnection, "SELECT valid, build_id FROM cache"_ns));
+
+ QM_TRY(OkIf(stmt), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_INSPECT(const int32_t& valid,
+ MOZ_TO_RESULT_INVOKE_MEMBER(stmt, GetInt32, 0));
+
+ if (valid) {
+ if (!StaticPrefs::dom_quotaManager_caching_checkBuildId()) {
+ return true;
+ }
+
+ QM_TRY_INSPECT(const auto& buildId,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoCString, stmt, GetUTF8String, 1));
+
+ return buildId == *gBuildId;
+ }
+ }
+
+ return false;
+ }()));
+
+ auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); });
+
+ if (!loadQuotaFromCache ||
+ !StaticPrefs::dom_quotaManager_loadQuotaFromCache() ||
+ ![&LoadQuotaFromCache] {
+ QM_WARNONLY_TRY_UNWRAP(auto res, MOZ_TO_RESULT(LoadQuotaFromCache()));
+ return static_cast<bool>(res);
+ }()) {
+ // A keeper to defer the return only in Nightly, so that the telemetry data
+ // for whole profile can be collected.
+#ifdef NIGHTLY_BUILD
+ nsresult statusKeeper = NS_OK;
+#endif
+
+ const auto statusKeeperFunc = [&](const nsresult rv) {
+ RECORD_IN_NIGHTLY(statusKeeper, rv);
+ };
+
+ for (const PersistenceType type : kBestEffortPersistenceTypes) {
+ if (NS_WARN_IF(IsShuttingDown())) {
+ RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
+ }
+
+ QM_TRY(([&]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(([this, type, &MaybeCollectUnaccessedOrigin] {
+ const auto innerFunc = [&](const auto&) -> nsresult {
+ return InitializeRepository(type,
+ MaybeCollectUnaccessedOrigin);
+ };
+
+ return ExecuteInitialization(
+ type == PERSISTENCE_TYPE_DEFAULT
+ ? Initialization::DefaultRepository
+ : Initialization::TemporaryRepository,
+ innerFunc);
+ }())),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }()));
+ }
+
+#ifdef NIGHTLY_BUILD
+ if (NS_FAILED(statusKeeper)) {
+ return statusKeeper;
+ }
+#endif
+ }
+
+ autoRemoveQuota.release();
+
+ const auto endTime = recordQuotaInfoLoadTimeHelper->End();
+
+ if (StaticPrefs::dom_quotaManager_checkQuotaInfoLoadTime() &&
+ static_cast<uint32_t>((endTime - startTime).ToMilliseconds()) >=
+ StaticPrefs::dom_quotaManager_longQuotaInfoLoadTimeThresholdMs() &&
+ !unaccessedOrigins.IsEmpty()) {
+ QM_WARNONLY_TRY(ArchiveOrigins(unaccessedOrigins));
+ }
+
+ return NS_OK;
+}
+
+void QuotaManager::UnloadQuota() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mStorageConnection);
+ MOZ_ASSERT(mTemporaryStorageInitialized);
+ MOZ_ASSERT(mCacheUsable);
+
+ auto autoRemoveQuota = MakeScopeExit([&] { RemoveQuota(); });
+
+ mozStorageTransaction transaction(
+ mStorageConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()), QM_VOID);
+
+ QM_TRY(MOZ_TO_RESULT(
+ mStorageConnection->ExecuteSimpleSQL("DELETE FROM origin;"_ns)),
+ QM_VOID);
+
+ nsCOMPtr<mozIStorageStatement> insertStmt;
+
+ {
+ MutexAutoLock lock(mQuotaMutex);
+
+ for (auto iter = mGroupInfoPairs.Iter(); !iter.Done(); iter.Next()) {
+ MOZ_ASSERT(!iter.Key().IsEmpty());
+
+ GroupInfoPair* const pair = iter.UserData();
+ MOZ_ASSERT(pair);
+
+ for (const PersistenceType type : kBestEffortPersistenceTypes) {
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
+ if (!groupInfo) {
+ continue;
+ }
+
+ for (const auto& originInfo : groupInfo->mOriginInfos) {
+ MOZ_ASSERT(!originInfo->mCanonicalQuotaObjects.Count());
+
+ if (!originInfo->mDirectoryExists) {
+ continue;
+ }
+
+ if (insertStmt) {
+ MOZ_ALWAYS_SUCCEEDS(insertStmt->Reset());
+ } else {
+ QM_TRY_UNWRAP(
+ insertStmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, mStorageConnection,
+ CreateStatement,
+ "INSERT INTO origin (repository_id, suffix, group_, "
+ "origin, client_usages, usage, last_access_time, "
+ "accessed, persisted) "
+ "VALUES (:repository_id, :suffix, :group_, :origin, "
+ ":client_usages, :usage, :last_access_time, :accessed, "
+ ":persisted)"_ns),
+ QM_VOID);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(originInfo->LockedBindToStatement(insertStmt)),
+ QM_VOID);
+
+ QM_TRY(MOZ_TO_RESULT(insertStmt->Execute()), QM_VOID);
+ }
+
+ groupInfo->LockedRemoveOriginInfos();
+ }
+
+ iter.Remove();
+ }
+ }
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, mStorageConnection, CreateStatement,
+ "UPDATE cache SET valid = :valid, build_id = :buildId;"_ns),
+ QM_VOID);
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindInt32ByName("valid"_ns, 1)), QM_VOID);
+ QM_TRY(MOZ_TO_RESULT(stmt->BindUTF8StringByName("buildId"_ns, *gBuildId)),
+ QM_VOID);
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()), QM_VOID);
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()), QM_VOID);
+}
+
+already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile, int64_t aFileSize,
+ int64_t* aFileSizeOut /* = nullptr */) {
+ NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
+
+ if (aFileSizeOut) {
+ *aFileSizeOut = 0;
+ }
+
+ if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
+ return nullptr;
+ }
+
+ QM_TRY_INSPECT(const auto& path,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aFile, GetPath),
+ nullptr);
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(
+ const auto& directory,
+ GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin),
+ nullptr);
+
+ nsAutoString clientType;
+ QM_TRY(OkIf(Client::TypeToText(aClientType, clientType, fallible)),
+ nullptr);
+
+ QM_TRY(MOZ_TO_RESULT(directory->Append(clientType)), nullptr);
+
+ QM_TRY_INSPECT(
+ const auto& directoryPath,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, directory, GetPath),
+ nullptr);
+
+ MOZ_ASSERT(StringBeginsWith(path, directoryPath));
+ }
+#endif
+
+ QM_TRY_INSPECT(
+ const int64_t fileSize,
+ ([&aFile, aFileSize]() -> Result<int64_t, nsresult> {
+ if (aFileSize == -1) {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aFile, Exists));
+
+ if (exists) {
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aFile, GetFileSize));
+ }
+
+ return 0;
+ }
+
+ return aFileSize;
+ }()),
+ nullptr);
+
+ RefPtr<QuotaObject> result;
+ {
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ return nullptr;
+ }
+
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
+
+ if (!groupInfo) {
+ return nullptr;
+ }
+
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+
+ if (!originInfo) {
+ return nullptr;
+ }
+
+ // We need this extra raw pointer because we can't assign to the smart
+ // pointer directly since QuotaObject::AddRef would try to acquire the same
+ // mutex.
+ const NotNull<CanonicalQuotaObject*> canonicalQuotaObject =
+ originInfo->mCanonicalQuotaObjects.LookupOrInsertWith(path, [&] {
+ // Create a new QuotaObject. The hashtable is not responsible to
+ // delete the QuotaObject.
+ return WrapNotNullUnchecked(new CanonicalQuotaObject(
+ originInfo, aClientType, path, fileSize));
+ });
+
+ // Addref the QuotaObject and move the ownership to the result. This must
+ // happen before we unlock!
+ result = canonicalQuotaObject->LockedAddRef();
+ }
+
+ if (aFileSizeOut) {
+ *aFileSizeOut = fileSize;
+ }
+
+ // The caller becomes the owner of the QuotaObject, that is, the caller is
+ // is responsible to delete it when the last reference is removed.
+ return result.forget();
+}
+
+already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, const nsAString& aPath, int64_t aFileSize,
+ int64_t* aFileSizeOut /* = nullptr */) {
+ NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
+
+ if (aFileSizeOut) {
+ *aFileSizeOut = 0;
+ }
+
+ QM_TRY_INSPECT(const auto& file, QM_NewLocalFile(aPath), nullptr);
+
+ return GetQuotaObject(aPersistenceType, aOriginMetadata, aClientType, file,
+ aFileSize, aFileSizeOut);
+}
+
+already_AddRefed<QuotaObject> QuotaManager::GetQuotaObject(
+ const int64_t aDirectoryLockId, const nsAString& aPath) {
+ NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
+
+ Maybe<MutexAutoLock> lock;
+
+ // See the comment for mDirectoryLockIdTable in QuotaManager.h
+ if (!IsOnBackgroundThread()) {
+ lock.emplace(mQuotaMutex);
+ }
+
+ if (auto maybeDirectoryLock =
+ mDirectoryLockIdTable.MaybeGet(aDirectoryLockId)) {
+ const auto& directoryLock = *maybeDirectoryLock;
+ MOZ_DIAGNOSTIC_ASSERT(directoryLock->ShouldUpdateLockIdTable());
+
+ const PersistenceType persistenceType = directoryLock->GetPersistenceType();
+ const OriginMetadata& originMetadata = directoryLock->OriginMetadata();
+ const Client::Type clientType = directoryLock->ClientType();
+
+ lock.reset();
+
+ return GetQuotaObject(persistenceType, originMetadata, clientType, aPath);
+ }
+
+ MOZ_CRASH("Getting quota object for an unregistered directory lock?");
+}
+
+Nullable<bool> QuotaManager::OriginPersisted(
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<OriginInfo> originInfo =
+ LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
+ if (originInfo) {
+ return Nullable<bool>(originInfo->LockedPersisted());
+ }
+
+ return Nullable<bool>();
+}
+
+void QuotaManager::PersistOrigin(const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<OriginInfo> originInfo =
+ LockedGetOriginInfo(PERSISTENCE_TYPE_DEFAULT, aOriginMetadata);
+ if (originInfo && !originInfo->LockedPersisted()) {
+ originInfo->LockedPersist();
+ }
+}
+
+void QuotaManager::AbortOperationsForLocks(
+ const DirectoryLockIdTableArray& aLockIds) {
+ for (Client::Type type : AllClientTypes()) {
+ if (aLockIds[type].Filled()) {
+ (*mClients)[type]->AbortOperationsForLocks(aLockIds[type]);
+ }
+ }
+}
+
+void QuotaManager::AbortOperationsForProcess(ContentParentId aContentParentId) {
+ AssertIsOnOwningThread();
+
+ for (const RefPtr<Client>& client : *mClients) {
+ client->AbortOperationsForProcess(aContentParentId);
+ }
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> QuotaManager::GetDirectoryForOrigin(
+ PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const {
+ QM_TRY_UNWRAP(auto directory,
+ QM_NewLocalFile(GetStoragePath(aPersistenceType)));
+
+ QM_TRY(MOZ_TO_RESULT(
+ directory->Append(MakeSanitizedOriginString(aASCIIOrigin))));
+
+ return directory;
+}
+
+nsresult QuotaManager::RestoreDirectoryMetadata2(nsIFile* aDirectory) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aDirectory);
+ MOZ_ASSERT(mStorageConnection);
+
+ RefPtr<RestoreDirectoryMetadata2Helper> helper =
+ new RestoreDirectoryMetadata2Helper(aDirectory);
+
+ QM_TRY(MOZ_TO_RESULT(helper->Init()));
+
+ QM_TRY(MOZ_TO_RESULT(helper->RestoreMetadata2File()));
+
+ return NS_OK;
+}
+
+Result<FullOriginMetadata, nsresult> QuotaManager::LoadFullOriginMetadata(
+ nsIFile* aDirectory, PersistenceType aPersistenceType) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aDirectory);
+ MOZ_ASSERT(mStorageConnection);
+
+ QM_TRY_INSPECT(const auto& binaryStream,
+ GetBinaryInputStream(*aDirectory,
+ nsLiteralString(METADATA_V2_FILE_NAME)));
+
+ FullOriginMetadata fullOriginMetadata;
+
+ QM_TRY_UNWRAP(fullOriginMetadata.mLastAccessTime,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
+
+ QM_TRY_UNWRAP(fullOriginMetadata.mPersisted,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
+
+ QM_TRY_INSPECT(const bool& reservedData1,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+ Unused << reservedData1;
+
+ // XXX Use for the persistence type.
+ QM_TRY_INSPECT(const bool& reservedData2,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+ Unused << reservedData2;
+
+ fullOriginMetadata.mPersistenceType = aPersistenceType;
+
+ QM_TRY_UNWRAP(
+ fullOriginMetadata.mSuffix,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString));
+
+ QM_TRY_UNWRAP(
+ fullOriginMetadata.mGroup,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString));
+
+ QM_TRY_UNWRAP(
+ fullOriginMetadata.mOrigin,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, binaryStream, ReadCString));
+
+ // Currently unused (used to be isApp).
+ QM_TRY_INSPECT(const bool& dummy,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
+ Unused << dummy;
+
+ QM_TRY(MOZ_TO_RESULT(binaryStream->Close()));
+
+ QM_TRY_INSPECT(const bool& groupUpdated,
+ MaybeUpdateGroupForOrigin(fullOriginMetadata));
+
+ // A workaround for a bug in GetLastModifiedTime implementation which should
+ // have returned the current time instead of INT64_MIN when there were no
+ // suitable files for getting last modified time.
+ QM_TRY_INSPECT(const bool& lastAccessTimeUpdated,
+ MaybeUpdateLastAccessTimeForOrigin(fullOriginMetadata));
+
+ if (groupUpdated || lastAccessTimeUpdated) {
+ // Only overwriting .metadata-v2 (used to overwrite .metadata too) to reduce
+ // I/O.
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aDirectory, fullOriginMetadata.mLastAccessTime,
+ fullOriginMetadata.mPersisted, fullOriginMetadata)));
+ }
+
+ return fullOriginMetadata;
+}
+
+Result<FullOriginMetadata, nsresult>
+QuotaManager::LoadFullOriginMetadataWithRestore(nsIFile* aDirectory) {
+ // XXX Once the persistence type is stored in the metadata file, this block
+ // for getting the persistence type from the parent directory name can be
+ // removed.
+ nsCOMPtr<nsIFile> parentDir;
+ QM_TRY(MOZ_TO_RESULT(aDirectory->GetParent(getter_AddRefs(parentDir))));
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromFile(*parentDir, fallible);
+ QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
+
+ const auto& persistenceType = maybePersistenceType.value();
+
+ QM_TRY_RETURN(QM_OR_ELSE_WARN(
+ // Expression.
+ LoadFullOriginMetadata(aDirectory, persistenceType),
+ // Fallback.
+ ([&aDirectory, &persistenceType,
+ this](const nsresult rv) -> Result<FullOriginMetadata, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(RestoreDirectoryMetadata2(aDirectory)));
+
+ QM_TRY_RETURN(LoadFullOriginMetadata(aDirectory, persistenceType));
+ })));
+}
+
+template <typename OriginFunc>
+nsresult QuotaManager::InitializeRepository(PersistenceType aPersistenceType,
+ OriginFunc&& aOriginFunc) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_TEMPORARY ||
+ aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ QM_TRY_INSPECT(const auto& directory,
+ QM_NewLocalFile(GetStoragePath(aPersistenceType)));
+
+ QM_TRY_INSPECT(const bool& created, EnsureDirectory(*directory));
+
+ Unused << created;
+
+ // A keeper to defer the return only in Nightly, so that the telemetry data
+ // for whole profile can be collected
+#ifdef NIGHTLY_BUILD
+ nsresult statusKeeper = NS_OK;
+#endif
+
+ const auto statusKeeperFunc = [&](const nsresult rv) {
+ RECORD_IN_NIGHTLY(statusKeeper, rv);
+ };
+
+ struct RenameAndInitInfo {
+ nsCOMPtr<nsIFile> mOriginDirectory;
+ FullOriginMetadata mFullOriginMetadata;
+ int64_t mTimestamp;
+ bool mPersisted;
+ };
+ nsTArray<RenameAndInitInfo> renameAndInitInfos;
+
+ QM_TRY(([&]() -> Result<Ok, nsresult> {
+ QM_TRY(
+ CollectEachFile(
+ *directory,
+ [&](nsCOMPtr<nsIFile>&& childDirectory) -> Result<Ok, nsresult> {
+ if (NS_WARN_IF(IsShuttingDown())) {
+ RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
+ }
+
+ QM_TRY(
+ ([this, &childDirectory, &renameAndInitInfos,
+ aPersistenceType, &aOriginFunc]() -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoString, childDirectory, GetLeafName));
+
+ QM_TRY_INSPECT(const auto& dirEntryKind,
+ GetDirEntryKind(*childDirectory));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ QM_TRY_UNWRAP(
+ auto metadata,
+ LoadFullOriginMetadataWithRestore(childDirectory));
+
+ MOZ_ASSERT(metadata.mPersistenceType ==
+ aPersistenceType);
+
+ // FIXME(tt): The check for origin name consistency can
+ // be removed once we have an upgrade to traverse origin
+ // directories and check through the directory metadata
+ // files.
+ const auto originSanitized =
+ MakeSanitizedOriginCString(metadata.mOrigin);
+
+ NS_ConvertUTF16toUTF8 utf8LeafName(leafName);
+ if (!originSanitized.Equals(utf8LeafName)) {
+ QM_WARNING(
+ "The name of the origin directory (%s) doesn't "
+ "match the sanitized origin string (%s) in the "
+ "metadata file!",
+ utf8LeafName.get(), originSanitized.get());
+
+ // If it's the known case, we try to restore the
+ // origin directory name if it's possible.
+ if (originSanitized.Equals(utf8LeafName + "."_ns)) {
+ const int64_t lastAccessTime =
+ metadata.mLastAccessTime;
+ const bool persisted = metadata.mPersisted;
+ renameAndInitInfos.AppendElement(RenameAndInitInfo{
+ std::move(childDirectory), std::move(metadata),
+ lastAccessTime, persisted});
+ break;
+ }
+
+ // XXXtt: Try to restore the unknown cases base on the
+ // content for their metadata files. Note that if the
+ // restore fails, QM should maintain a list and ensure
+ // they won't be accessed after initialization.
+ }
+
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(InitializeOrigin(
+ aPersistenceType, metadata,
+ metadata.mLastAccessTime, metadata.mPersisted,
+ childDirectory)),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&childDirectory](
+ const nsresult rv) -> Result<Ok, nsresult> {
+ // If the origin can't be initialized due to
+ // corruption, this is a permanent
+ // condition, and we need to remove all data
+ // for the origin on disk.
+
+ QM_TRY(
+ MOZ_TO_RESULT(childDirectory->Remove(true)));
+
+ return Ok{};
+ })));
+
+ std::forward<OriginFunc>(aOriginFunc)(metadata);
+
+ break;
+ }
+
+ case nsIFileKind::ExistsAsFile:
+ if (IsOSMetadata(leafName) || IsDotFile(leafName)) {
+ break;
+ }
+
+ // Unknown files during initialization are now allowed.
+ // Just warn if we find them.
+ UNKNOWN_FILE_WARNING(leafName);
+ break;
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while
+ // iterating.
+ break;
+ }
+
+ return Ok{};
+ }()),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }()));
+
+ for (auto& info : renameAndInitInfos) {
+ QM_TRY(([&]() -> Result<Ok, nsresult> {
+ QM_TRY(([&directory, &info, this, aPersistenceType,
+ &aOriginFunc]() -> Result<Ok, nsresult> {
+ const auto originDirName =
+ MakeSanitizedOriginString(info.mFullOriginMetadata.mOrigin);
+
+ // Check if targetDirectory exist.
+ QM_TRY_INSPECT(const auto& targetDirectory,
+ CloneFileAndAppend(*directory, originDirName));
+
+ QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(
+ targetDirectory, Exists));
+
+ if (exists) {
+ QM_TRY(MOZ_TO_RESULT(info.mOriginDirectory->Remove(true)));
+
+ return Ok{};
+ }
+
+ QM_TRY(MOZ_TO_RESULT(
+ info.mOriginDirectory->RenameTo(nullptr, originDirName)));
+
+ // XXX We don't check corruption here ?
+ QM_TRY(MOZ_TO_RESULT(InitializeOrigin(
+ aPersistenceType, info.mFullOriginMetadata, info.mTimestamp,
+ info.mPersisted, targetDirectory)));
+
+ std::forward<OriginFunc>(aOriginFunc)(info.mFullOriginMetadata);
+
+ return Ok{};
+ }()),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }()));
+ }
+
+#ifdef NIGHTLY_BUILD
+ if (NS_FAILED(statusKeeper)) {
+ return statusKeeper;
+ }
+#endif
+
+ return NS_OK;
+}
+
+nsresult QuotaManager::InitializeOrigin(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ int64_t aAccessTime, bool aPersisted,
+ nsIFile* aDirectory) {
+ AssertIsOnIOThread();
+
+ const bool trackQuota = aPersistenceType != PERSISTENCE_TYPE_PERSISTENT;
+
+ // We need to initialize directories of all clients if they exists and also
+ // get the total usage to initialize the quota.
+
+ ClientUsageArray clientUsages;
+
+ // A keeper to defer the return only in Nightly, so that the telemetry data
+ // for whole profile can be collected
+#ifdef NIGHTLY_BUILD
+ nsresult statusKeeper = NS_OK;
+#endif
+
+ QM_TRY(([&, statusKeeperFunc = [&](const nsresult rv) {
+ RECORD_IN_NIGHTLY(statusKeeper, rv);
+ }]() -> Result<Ok, nsresult> {
+ QM_TRY(
+ CollectEachFile(
+ *aDirectory,
+ [&](const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ if (NS_WARN_IF(IsShuttingDown())) {
+ RETURN_STATUS_OR_RESULT(statusKeeper, NS_ERROR_ABORT);
+ }
+
+ QM_TRY(
+ ([this, &file, trackQuota, aPersistenceType, &aOriginMetadata,
+ &clientUsages]() -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoString, file, GetLeafName));
+
+ QM_TRY_INSPECT(const auto& dirEntryKind,
+ GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ Client::Type clientType;
+ const bool ok = Client::TypeFromText(
+ leafName, clientType, fallible);
+ if (!ok) {
+ // Unknown directories during initialization are now
+ // allowed. Just warn if we find them.
+ UNKNOWN_FILE_WARNING(leafName);
+ break;
+ }
+
+ if (trackQuota) {
+ QM_TRY_INSPECT(
+ const auto& usageInfo,
+ (*mClients)[clientType]->InitOrigin(
+ aPersistenceType, aOriginMetadata,
+ /* aCanceled */ Atomic<bool>(false)));
+
+ MOZ_ASSERT(!clientUsages[clientType]);
+
+ if (usageInfo.TotalUsage()) {
+ // XXX(Bug 1683863) Until we identify the root cause
+ // of seemingly converted-from-negative usage
+ // values, we will just treat them as unset here,
+ // but log a warning to the browser console.
+ if (static_cast<int64_t>(*usageInfo.TotalUsage()) >=
+ 0) {
+ clientUsages[clientType] = usageInfo.TotalUsage();
+ } else {
+#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
+ const nsCOMPtr<nsIConsoleService> console =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (console) {
+ console->LogStringMessage(
+ nsString(
+ u"QuotaManager warning: client "_ns +
+ leafName +
+ u" reported negative usage for group "_ns +
+ NS_ConvertUTF8toUTF16(
+ aOriginMetadata.mGroup) +
+ u", origin "_ns +
+ NS_ConvertUTF8toUTF16(
+ aOriginMetadata.mOrigin))
+ .get());
+ }
+#endif
+ }
+ }
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ (*mClients)[clientType]
+ ->InitOriginWithoutTracking(
+ aPersistenceType, aOriginMetadata,
+ /* aCanceled */ Atomic<bool>(false))));
+ }
+
+ break;
+ }
+
+ case nsIFileKind::ExistsAsFile:
+ if (IsOriginMetadata(leafName)) {
+ break;
+ }
+
+ if (IsTempMetadata(leafName)) {
+ QM_TRY(MOZ_TO_RESULT(
+ file->Remove(/* recursive */ false)));
+
+ break;
+ }
+
+ if (IsOSMetadata(leafName) || IsDotFile(leafName)) {
+ break;
+ }
+
+ // Unknown files during initialization are now allowed.
+ // Just warn if we find them.
+ UNKNOWN_FILE_WARNING(leafName);
+ // Bug 1595448 will handle the case for unknown files
+ // like idb, cache, or ls.
+ break;
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while
+ // iterating.
+ break;
+ }
+
+ return Ok{};
+ }()),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }),
+ OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS, statusKeeperFunc);
+
+ return Ok{};
+ }()));
+
+#ifdef NIGHTLY_BUILD
+ if (NS_FAILED(statusKeeper)) {
+ return statusKeeper;
+ }
+#endif
+
+ if (trackQuota) {
+ const auto usage = std::accumulate(
+ clientUsages.cbegin(), clientUsages.cend(), CheckedUint64(0),
+ [](CheckedUint64 value, const Maybe<uint64_t>& clientUsage) {
+ return value + clientUsage.valueOr(0);
+ });
+
+ // XXX Should we log more information, i.e. the whole clientUsages array, in
+ // case usage is not valid?
+
+ QM_TRY(OkIf(usage.isValid()), NS_ERROR_FAILURE);
+
+ InitQuotaForOrigin(
+ FullOriginMetadata{aOriginMetadata, aPersisted, aAccessTime},
+ clientUsages, usage.value());
+ }
+
+ return NS_OK;
+}
+
+nsresult
+QuotaManager::UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory(
+ nsIFile* aIndexedDBDir) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aIndexedDBDir);
+
+ const auto innerFunc = [this, &aIndexedDBDir](const auto&) -> nsresult {
+ bool isDirectory;
+ QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->IsDirectory(&isDirectory)));
+
+ if (!isDirectory) {
+ NS_WARNING("indexedDB entry is not a directory!");
+ return NS_OK;
+ }
+
+ auto persistentStorageDirOrErr = QM_NewLocalFile(*mStoragePath);
+ if (NS_WARN_IF(persistentStorageDirOrErr.isErr())) {
+ return persistentStorageDirOrErr.unwrapErr();
+ }
+
+ nsCOMPtr<nsIFile> persistentStorageDir = persistentStorageDirOrErr.unwrap();
+
+ QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append(
+ nsLiteralString(PERSISTENT_DIRECTORY_NAME))));
+
+ bool exists;
+ QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Exists(&exists)));
+
+ if (exists) {
+ QM_WARNING("Deleting old <profile>/indexedDB directory!");
+
+ QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->Remove(/* aRecursive */ true)));
+
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> storageDir;
+ QM_TRY(MOZ_TO_RESULT(
+ persistentStorageDir->GetParent(getter_AddRefs(storageDir))));
+
+ // MoveTo() is atomic if the move happens on the same volume which should
+ // be our case, so even if we crash in the middle of the operation nothing
+ // breaks next time we try to initialize.
+ // However there's a theoretical possibility that the indexedDB directory
+ // is on different volume, but it should be rare enough that we don't have
+ // to worry about it.
+ QM_TRY(MOZ_TO_RESULT(aIndexedDBDir->MoveTo(
+ storageDir, nsLiteralString(PERSISTENT_DIRECTORY_NAME))));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeFromIndexedDBDirectory,
+ innerFunc);
+}
+
+nsresult
+QuotaManager::UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory(
+ nsIFile* aPersistentStorageDir) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistentStorageDir);
+
+ const auto innerFunc = [this,
+ &aPersistentStorageDir](const auto&) -> nsresult {
+ QM_TRY_INSPECT(
+ const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aPersistentStorageDir, IsDirectory));
+
+ if (!isDirectory) {
+ NS_WARNING("persistent entry is not a directory!");
+ return NS_OK;
+ }
+
+ {
+ QM_TRY_INSPECT(const auto& defaultStorageDir,
+ QM_NewLocalFile(*mDefaultStoragePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists));
+
+ if (exists) {
+ QM_WARNING("Deleting old <profile>/storage/persistent directory!");
+
+ QM_TRY(MOZ_TO_RESULT(
+ aPersistentStorageDir->Remove(/* aRecursive */ true)));
+
+ return NS_OK;
+ }
+ }
+
+ {
+ // Create real metadata files for origin directories in persistent
+ // storage.
+ auto helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>(
+ aPersistentStorageDir);
+
+ QM_TRY(MOZ_TO_RESULT(helper->Init()));
+
+ QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));
+
+ // Upgrade metadata files for origin directories in temporary storage.
+ QM_TRY_INSPECT(const auto& temporaryStorageDir,
+ QM_NewLocalFile(*mTemporaryStoragePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, Exists));
+
+ if (exists) {
+ QM_TRY_INSPECT(
+ const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(temporaryStorageDir, IsDirectory));
+
+ if (!isDirectory) {
+ NS_WARNING("temporary entry is not a directory!");
+ return NS_OK;
+ }
+
+ helper = MakeRefPtr<CreateOrUpgradeDirectoryMetadataHelper>(
+ temporaryStorageDir);
+
+ QM_TRY(MOZ_TO_RESULT(helper->Init()));
+
+ QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));
+ }
+ }
+
+ // And finally rename persistent to default.
+ QM_TRY(MOZ_TO_RESULT(aPersistentStorageDir->RenameTo(
+ nullptr, nsLiteralString(DEFAULT_DIRECTORY_NAME))));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(
+ Initialization::UpgradeFromPersistentStorageDirectory, innerFunc);
+}
+
+template <typename Helper>
+nsresult QuotaManager::UpgradeStorage(const int32_t aOldVersion,
+ const int32_t aNewVersion,
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aNewVersion > aOldVersion);
+ MOZ_ASSERT(aNewVersion <= kStorageVersion);
+ MOZ_ASSERT(aConnection);
+
+ for (const PersistenceType persistenceType : kAllPersistenceTypes) {
+ QM_TRY_UNWRAP(auto directory,
+ QM_NewLocalFile(GetStoragePath(persistenceType)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+
+ if (!exists) {
+ continue;
+ }
+
+ RefPtr<UpgradeStorageHelperBase> helper = new Helper(directory);
+
+ QM_TRY(MOZ_TO_RESULT(helper->Init()));
+
+ QM_TRY(MOZ_TO_RESULT(helper->ProcessRepository()));
+ }
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const int32_t& storageVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));
+
+ MOZ_ASSERT(storageVersion == aOldVersion);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->SetSchemaVersion(aNewVersion)));
+
+ return NS_OK;
+}
+
+nsresult QuotaManager::UpgradeStorageFrom0_0To1_0(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom0_0To1_0Helper>(
+ 0, MakeStorageVersion(1, 0), aConnection)));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeStorageFrom0_0To1_0,
+ innerFunc);
+}
+
+nsresult QuotaManager::UpgradeStorageFrom1_0To2_0(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // The upgrade consists of a number of logically distinct bugs that
+ // intentionally got fixed at the same time to trigger just one major
+ // version bump.
+ //
+ //
+ // Morgue directory cleanup
+ // [Feature/Bug]:
+ // The original bug that added "on demand" morgue cleanup is 1165119.
+ //
+ // [Mutations]:
+ // Morgue directories are removed from all origin directories during the
+ // upgrade process. Origin initialization and usage calculation doesn't try
+ // to remove morgue directories anymore.
+ //
+ // [Downgrade-incompatible changes]:
+ // Morgue directories can reappear if user runs an already upgraded profile
+ // in an older version of Firefox. Morgue directories then prevent current
+ // Firefox from initializing and using the storage.
+ //
+ //
+ // App data removal
+ // [Feature/Bug]:
+ // The bug that removes isApp flags is 1311057.
+ //
+ // [Mutations]:
+ // Origin directories with appIds are removed during the upgrade process.
+ //
+ // [Downgrade-incompatible changes]:
+ // Origin directories with appIds can reappear if user runs an already
+ // upgraded profile in an older version of Firefox. Origin directories with
+ // appIds don't prevent current Firefox from initializing and using the
+ // storage, but they wouldn't ever be removed again, potentially causing
+ // problems once appId is removed from origin attributes.
+ //
+ //
+ // Strip obsolete origin attributes
+ // [Feature/Bug]:
+ // The bug that strips obsolete origin attributes is 1314361.
+ //
+ // [Mutations]:
+ // Origin directories with obsolete origin attributes are renamed and their
+ // metadata files are updated during the upgrade process.
+ //
+ // [Downgrade-incompatible changes]:
+ // Origin directories with obsolete origin attributes can reappear if user
+ // runs an already upgraded profile in an older version of Firefox. Origin
+ // directories with obsolete origin attributes don't prevent current Firefox
+ // from initializing and using the storage, but they wouldn't ever be upgraded
+ // again, potentially causing problems in future.
+ //
+ //
+ // File manager directory renaming (client specific)
+ // [Feature/Bug]:
+ // The original bug that added "on demand" file manager directory renaming is
+ // 1056939.
+ //
+ // [Mutations]:
+ // All file manager directories are renamed to contain the ".files" suffix.
+ //
+ // [Downgrade-incompatible changes]:
+ // File manager directories with the ".files" suffix prevent older versions of
+ // Firefox from initializing and using the storage.
+ // File manager directories without the ".files" suffix can appear if user
+ // runs an already upgraded profile in an older version of Firefox. File
+ // manager directories without the ".files" suffix then prevent current
+ // Firefox from initializing and using the storage.
+
+ const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom1_0To2_0Helper>(
+ MakeStorageVersion(1, 0), MakeStorageVersion(2, 0), aConnection)));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeStorageFrom1_0To2_0,
+ innerFunc);
+}
+
+nsresult QuotaManager::UpgradeStorageFrom2_0To2_1(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // The upgrade is mainly to create a directory padding file in DOM Cache
+ // directory to record the overall padding size of an origin.
+
+ const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_0To2_1Helper>(
+ MakeStorageVersion(2, 0), MakeStorageVersion(2, 1), aConnection)));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeStorageFrom2_0To2_1,
+ innerFunc);
+}
+
+nsresult QuotaManager::UpgradeStorageFrom2_1To2_2(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ // The upgrade is mainly to clean obsolete origins in the repositoies, remove
+ // asmjs client, and ".tmp" file in the idb folers.
+
+ const auto innerFunc = [this, &aConnection](const auto&) -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorage<UpgradeStorageFrom2_1To2_2Helper>(
+ MakeStorageVersion(2, 1), MakeStorageVersion(2, 2), aConnection)));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeStorageFrom2_1To2_2,
+ innerFunc);
+}
+
+nsresult QuotaManager::UpgradeStorageFrom2_2To2_3(
+ mozIStorageConnection* aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aConnection);
+
+ const auto innerFunc = [&aConnection](const auto&) -> nsresult {
+ // Table `database`
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ nsLiteralCString("CREATE TABLE database"
+ "( cache_version INTEGER NOT NULL DEFAULT 0"
+ ");"))));
+
+ QM_TRY(MOZ_TO_RESULT(aConnection->ExecuteSimpleSQL(
+ nsLiteralCString("INSERT INTO database (cache_version) "
+ "VALUES (0)"))));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(
+ const int32_t& storageVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));
+
+ MOZ_ASSERT(storageVersion == MakeStorageVersion(2, 2));
+ }
+#endif
+
+ QM_TRY(
+ MOZ_TO_RESULT(aConnection->SetSchemaVersion(MakeStorageVersion(2, 3))));
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(Initialization::UpgradeStorageFrom2_2To2_3,
+ innerFunc);
+}
+
+nsresult QuotaManager::MaybeRemoveLocalStorageDataAndArchive(
+ nsIFile& aLsArchiveFile) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!CachedNextGenLocalStorageEnabled());
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+
+ if (!exists) {
+ // If the ls archive doesn't exist then ls directories can't exist either.
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories()));
+
+ InvalidateQuotaCache();
+
+ // Finally remove the ls archive, so we don't have to check all origin
+ // directories next time this method is called.
+ QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false)));
+
+ return NS_OK;
+}
+
+nsresult QuotaManager::MaybeRemoveLocalStorageDirectories() {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& defaultStorageDir,
+ QM_NewLocalFile(*mDefaultStoragePath));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(defaultStorageDir, Exists));
+
+ if (!exists) {
+ return NS_OK;
+ }
+
+ QM_TRY(CollectEachFile(
+ *defaultStorageDir,
+ [](const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> {
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(originDir, Exists));
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ QM_TRY_INSPECT(
+ const auto& lsDir,
+ CloneFileAndAppend(*originDir, NS_LITERAL_STRING_FROM_CSTRING(
+ LS_DIRECTORY_NAME)));
+
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, Exists));
+
+ if (!exists) {
+ return Ok{};
+ }
+ }
+
+ {
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(lsDir, IsDirectory));
+
+ if (!isDirectory) {
+ QM_WARNING("ls entry is not a directory!");
+
+ return Ok{};
+ }
+ }
+
+ nsString path;
+ QM_TRY(MOZ_TO_RESULT(lsDir->GetPath(path)));
+
+ QM_WARNING("Deleting %s directory!",
+ NS_ConvertUTF16toUTF8(path).get());
+
+ QM_TRY(MOZ_TO_RESULT(lsDir->Remove(/* aRecursive */ true)));
+
+ break;
+ }
+
+ case nsIFileKind::ExistsAsFile: {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoString, originDir, GetLeafName));
+
+ // Unknown files during upgrade are allowed. Just warn if we find
+ // them.
+ if (!IsOSMetadata(leafName)) {
+ UNKNOWN_FILE_WARNING(leafName);
+ }
+
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+ return Ok{};
+ }));
+
+ return NS_OK;
+}
+
+Result<Ok, nsresult> QuotaManager::CopyLocalStorageArchiveFromWebAppsStore(
+ nsIFile& aLsArchiveFile) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+ MOZ_ASSERT(!exists);
+ }
+#endif
+
+ // Get the storage service first, we will need it at multiple places.
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ // Get the web apps store file.
+ QM_TRY_INSPECT(const auto& webAppsStoreFile, QM_NewLocalFile(mBasePath));
+
+ QM_TRY(MOZ_TO_RESULT(
+ webAppsStoreFile->Append(nsLiteralString(WEB_APPS_STORE_FILE_NAME))));
+
+ // Now check if the web apps store is useable.
+ QM_TRY_INSPECT(const auto& connection,
+ CreateWebAppsStoreConnection(*webAppsStoreFile, *ss));
+
+ if (connection) {
+ // Find out the journal mode.
+ QM_TRY_INSPECT(const auto& stmt,
+ CreateAndExecuteSingleStepStatement(
+ *connection, "PRAGMA journal_mode;"_ns));
+
+ QM_TRY_INSPECT(const auto& journalMode,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoCString, *stmt,
+ GetUTF8String, 0));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->Finalize()));
+
+ if (journalMode.EqualsLiteral("wal")) {
+ // We don't copy the WAL file, so make sure the old database is fully
+ // checkpointed.
+ QM_TRY(MOZ_TO_RESULT(
+ connection->ExecuteSimpleSQL("PRAGMA wal_checkpoint(TRUNCATE);"_ns)));
+ }
+
+ // Explicitely close the connection before the old database is copied.
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+
+ // Copy the old database. The database is copied from
+ // <profile>/webappsstore.sqlite to
+ // <profile>/storage/ls-archive-tmp.sqlite
+ // We use a "-tmp" postfix since we are not done yet.
+ QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath));
+
+ QM_TRY(MOZ_TO_RESULT(webAppsStoreFile->CopyTo(
+ storageDir, nsLiteralString(LS_ARCHIVE_TMP_FILE_NAME))));
+
+ QM_TRY_INSPECT(const auto& lsArchiveTmpFile,
+ GetLocalStorageArchiveTmpFile(*mStoragePath));
+
+ if (journalMode.EqualsLiteral("wal")) {
+ QM_TRY_INSPECT(
+ const auto& lsArchiveTmpConnection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ lsArchiveTmpFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ // The archive will only be used for lazy data migration. There won't be
+ // any concurrent readers and writers that could benefit from Write-Ahead
+ // Logging. So switch to a standard rollback journal. The standard
+ // rollback journal also provides atomicity across multiple attached
+ // databases which is import for the lazy data migration to work safely.
+ QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->ExecuteSimpleSQL(
+ "PRAGMA journal_mode = DELETE;"_ns)));
+
+ // Close the connection explicitly. We are going to rename the file below.
+ QM_TRY(MOZ_TO_RESULT(lsArchiveTmpConnection->Close()));
+ }
+
+ // Finally, rename ls-archive-tmp.sqlite to ls-archive.sqlite
+ QM_TRY(MOZ_TO_RESULT(lsArchiveTmpFile->MoveTo(
+ nullptr, nsLiteralString(LS_ARCHIVE_FILE_NAME))));
+
+ return Ok{};
+ }
+
+ // If webappsstore database is not useable, just create an empty archive.
+ // XXX The code below should be removed and the caller should call us only
+ // when webappstore.sqlite exists. CreateWebAppsStoreConnection should be
+ // reworked to propagate database corruption instead of returning null
+ // connection.
+ // So, if there's no webappsstore.sqlite
+ // MaybeCreateOrUpgradeLocalStorageArchive will call
+ // CreateEmptyLocalStorageArchive instead of
+ // CopyLocalStorageArchiveFromWebAppsStore.
+ // If there's any corruption detected during
+ // MaybeCreateOrUpgradeLocalStorageArchive (including nested calls like
+ // CopyLocalStorageArchiveFromWebAppsStore and CreateWebAppsStoreConnection)
+ // EnsureStorageIsInitialized will fallback to
+ // CreateEmptyLocalStorageArchive.
+
+ // Ensure the storage directory actually exists.
+ QM_TRY_INSPECT(const auto& storageDirectory, QM_NewLocalFile(*mStoragePath));
+
+ QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDirectory));
+
+ Unused << created;
+
+ QM_TRY_UNWRAP(auto lsArchiveConnection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(
+ StorageDBUpdater::CreateCurrentSchema(lsArchiveConnection)));
+
+ return Ok{};
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+QuotaManager::CreateLocalStorageArchiveConnection(
+ nsIFile& aLsArchiveFile) const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, IsDirectory));
+
+ // A directory with the name of the archive file is treated as corruption
+ // (similarly as wrong content of the file).
+ QM_TRY(OkIf(!isDirectory), Err(NS_ERROR_FILE_CORRUPTED));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ // This may return NS_ERROR_FILE_CORRUPTED too.
+ QM_TRY_UNWRAP(auto connection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ // The legacy LS implementation removes the database and creates an empty one
+ // when the schema can't be updated. The same effect can be achieved here by
+ // mapping all errors to NS_ERROR_FILE_CORRUPTED. One such case is tested by
+ // sub test case 3 of dom/localstorage/test/unit/test_archive.js
+ QM_TRY(
+ MOZ_TO_RESULT(StorageDBUpdater::Update(connection))
+ .mapErr([](const nsresult rv) { return NS_ERROR_FILE_CORRUPTED; }));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+QuotaManager::RecopyLocalStorageArchiveFromWebAppsStore(
+ nsIFile& aLsArchiveFile) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ QM_TRY(MOZ_TO_RESULT(MaybeRemoveLocalStorageDirectories()));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(false)));
+
+ QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));
+
+ QM_TRY_UNWRAP(auto connection,
+ CreateLocalStorageArchiveConnection(aLsArchiveFile));
+
+ QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+QuotaManager::DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ QM_TRY_UNWRAP(auto connection,
+ RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));
+
+ QM_TRY(MOZ_TO_RESULT(
+ SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion)));
+
+ return connection;
+}
+
+Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+QuotaManager::UpgradeLocalStorageArchiveFromLessThan4To4(
+ nsIFile& aLsArchiveFile) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ QM_TRY_UNWRAP(auto connection,
+ RecopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));
+
+ QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(connection, 4)));
+
+ return connection;
+}
+
+/*
+nsresult QuotaManager::UpgradeLocalStorageArchiveFrom4To5(
+ nsCOMPtr<mozIStorageConnection>& aConnection) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(CachedNextGenLocalStorageEnabled());
+
+ nsresult rv = SaveLocalStorageArchiveVersion(aConnection, 5);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+*/
+
+#ifdef DEBUG
+
+void QuotaManager::AssertStorageIsInitialized() const {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(IsStorageInitialized());
+}
+
+#endif // DEBUG
+
+nsresult QuotaManager::MaybeUpgradeToDefaultStorageDirectory(
+ nsIFile& aStorageFile) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& storageFileExists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aStorageFile, Exists));
+
+ if (!storageFileExists) {
+ QM_TRY_INSPECT(const auto& indexedDBDir, QM_NewLocalFile(*mIndexedDBPath));
+
+ QM_TRY_INSPECT(const auto& indexedDBDirExists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(indexedDBDir, Exists));
+
+ if (indexedDBDirExists) {
+ QM_TRY(MOZ_TO_RESULT(
+ UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory(
+ indexedDBDir)));
+ }
+
+ QM_TRY_INSPECT(const auto& persistentStorageDir,
+ QM_NewLocalFile(*mStoragePath));
+
+ QM_TRY(MOZ_TO_RESULT(persistentStorageDir->Append(
+ nsLiteralString(PERSISTENT_DIRECTORY_NAME))));
+
+ QM_TRY_INSPECT(const auto& persistentStorageDirExists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(persistentStorageDir, Exists));
+
+ if (persistentStorageDirExists) {
+ QM_TRY(MOZ_TO_RESULT(
+ UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory(
+ persistentStorageDir)));
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult QuotaManager::MaybeCreateOrUpgradeStorage(
+ mozIStorageConnection& aConnection) {
+ AssertIsOnIOThread();
+
+ QM_TRY_UNWRAP(auto storageVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion));
+
+ // Hacky downgrade logic!
+ // If we see major.minor of 3.0, downgrade it to be 2.1.
+ if (storageVersion == kHackyPreDowngradeStorageVersion) {
+ storageVersion = kHackyPostDowngradeStorageVersion;
+ QM_TRY(MOZ_TO_RESULT(aConnection.SetSchemaVersion(storageVersion)),
+ QM_PROPAGATE,
+ [](const auto&) { MOZ_ASSERT(false, "Downgrade didn't take."); });
+ }
+
+ QM_TRY(OkIf(GetMajorStorageVersion(storageVersion) <= kMajorStorageVersion),
+ NS_ERROR_FAILURE, [](const auto&) {
+ NS_WARNING("Unable to initialize storage, version is too high!");
+ });
+
+ if (storageVersion < kStorageVersion) {
+ const bool newDatabase = !storageVersion;
+
+ QM_TRY_INSPECT(const auto& storageDir, QM_NewLocalFile(*mStoragePath));
+
+ QM_TRY_INSPECT(const auto& storageDirExists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(storageDir, Exists));
+
+ const bool newDirectory = !storageDirExists;
+
+ if (newDatabase) {
+ // Set the page size first.
+ if (kSQLitePageSizeOverride) {
+ QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(nsPrintfCString(
+ "PRAGMA page_size = %" PRIu32 ";", kSQLitePageSizeOverride))));
+ }
+ }
+
+ mozStorageTransaction transaction(
+ &aConnection, false, mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Start()));
+
+ // An upgrade method can upgrade the database, the storage or both.
+ // The upgrade loop below can only be avoided when there's no database and
+ // no storage yet (e.g. new profile).
+ if (newDatabase && newDirectory) {
+ QM_TRY(MOZ_TO_RESULT(CreateTables(&aConnection)));
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(
+ const int32_t& storageVersion,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aConnection, GetSchemaVersion),
+ QM_ASSERT_UNREACHABLE);
+ MOZ_ASSERT(storageVersion == kStorageVersion);
+ }
+#endif
+
+ QM_TRY(MOZ_TO_RESULT(aConnection.ExecuteSimpleSQL(
+ nsLiteralCString("INSERT INTO database (cache_version) "
+ "VALUES (0)"))));
+ } else {
+ // This logic needs to change next time we change the storage!
+ static_assert(kStorageVersion == int32_t((2 << 16) + 3),
+ "Upgrade function needed due to storage version increase.");
+
+ while (storageVersion != kStorageVersion) {
+ if (storageVersion == 0) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom0_0To1_0(&aConnection)));
+ } else if (storageVersion == MakeStorageVersion(1, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom1_0To2_0(&aConnection)));
+ } else if (storageVersion == MakeStorageVersion(2, 0)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_0To2_1(&aConnection)));
+ } else if (storageVersion == MakeStorageVersion(2, 1)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_1To2_2(&aConnection)));
+ } else if (storageVersion == MakeStorageVersion(2, 2)) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeStorageFrom2_2To2_3(&aConnection)));
+ } else {
+ QM_FAIL(NS_ERROR_FAILURE, []() {
+ NS_WARNING(
+ "Unable to initialize storage, no upgrade path is "
+ "available!");
+ });
+ }
+
+ QM_TRY_UNWRAP(storageVersion, MOZ_TO_RESULT_INVOKE_MEMBER(
+ aConnection, GetSchemaVersion));
+ }
+
+ MOZ_ASSERT(storageVersion == kStorageVersion);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(transaction.Commit()));
+ }
+
+ return NS_OK;
+}
+
+OkOrErr QuotaManager::MaybeRemoveLocalStorageArchiveTmpFile() {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const auto& lsArchiveTmpFile,
+ QM_TO_RESULT_TRANSFORM(GetLocalStorageArchiveTmpFile(*mStoragePath)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ QM_TO_RESULT_INVOKE_MEMBER(lsArchiveTmpFile, Exists));
+
+ if (exists) {
+ QM_TRY(QM_TO_RESULT(lsArchiveTmpFile->Remove(false)));
+ }
+
+ return Ok{};
+}
+
+Result<Ok, nsresult> QuotaManager::MaybeCreateOrUpgradeLocalStorageArchive(
+ nsIFile& aLsArchiveFile) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const bool& lsArchiveFileExisted,
+ ([this, &aLsArchiveFile]() -> Result<bool, nsresult> {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+
+ if (!exists) {
+ QM_TRY(CopyLocalStorageArchiveFromWebAppsStore(aLsArchiveFile));
+ }
+
+ return exists;
+ }()));
+
+ QM_TRY_UNWRAP(auto connection,
+ CreateLocalStorageArchiveConnection(aLsArchiveFile));
+
+ QM_TRY_INSPECT(const auto& initialized,
+ IsLocalStorageArchiveInitialized(*connection));
+
+ if (!initialized) {
+ QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));
+ }
+
+ QM_TRY_UNWRAP(int32_t version, LoadLocalStorageArchiveVersion(*connection));
+
+ if (version > kLocalStorageArchiveVersion) {
+ // Close local storage archive connection. We are going to remove underlying
+ // file.
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+
+ // This will wipe the archive and any migrated data and recopy the archive
+ // from webappsstore.sqlite.
+ QM_TRY_UNWRAP(connection, DowngradeLocalStorageArchive(aLsArchiveFile));
+
+ QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection));
+
+ MOZ_ASSERT(version == kLocalStorageArchiveVersion);
+ } else if (version != kLocalStorageArchiveVersion) {
+ // The version can be zero either when the archive didn't exist or it did
+ // exist, but the archive was created without any version information.
+ // We don't need to do any upgrades only if it didn't exist because existing
+ // archives without version information must be recopied to really fix bug
+ // 1542104. See also bug 1546305 which introduced archive versions.
+ if (!lsArchiveFileExisted) {
+ MOZ_ASSERT(version == 0);
+
+ QM_TRY(MOZ_TO_RESULT(SaveLocalStorageArchiveVersion(
+ connection, kLocalStorageArchiveVersion)));
+ } else {
+ static_assert(kLocalStorageArchiveVersion == 4,
+ "Upgrade function needed due to LocalStorage archive "
+ "version increase.");
+
+ while (version != kLocalStorageArchiveVersion) {
+ if (version < 4) {
+ // Close local storage archive connection. We are going to remove
+ // underlying file.
+ QM_TRY(MOZ_TO_RESULT(connection->Close()));
+
+ // This won't do an "upgrade" in a normal sense. It will wipe the
+ // archive and any migrated data and recopy the archive from
+ // webappsstore.sqlite
+ QM_TRY_UNWRAP(connection, UpgradeLocalStorageArchiveFromLessThan4To4(
+ aLsArchiveFile));
+ } /* else if (version == 4) {
+ QM_TRY(MOZ_TO_RESULT(UpgradeLocalStorageArchiveFrom4To5(connection)));
+ } */
+ else {
+ QM_FAIL(Err(NS_ERROR_FAILURE), []() {
+ QM_WARNING(
+ "Unable to initialize LocalStorage archive, no upgrade path "
+ "is available!");
+ });
+ }
+
+ QM_TRY_UNWRAP(version, LoadLocalStorageArchiveVersion(*connection));
+ }
+
+ MOZ_ASSERT(version == kLocalStorageArchiveVersion);
+ }
+ }
+
+ // At this point, we have finished initializing the local storage archive, and
+ // can continue storage initialization. We don't know though if the actual
+ // data in the archive file is readable. We can't do a PRAGMA integrity_check
+ // here though, because that would be too heavyweight.
+
+ return Ok{};
+}
+
+Result<Ok, nsresult> QuotaManager::CreateEmptyLocalStorageArchive(
+ nsIFile& aLsArchiveFile) const {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aLsArchiveFile, Exists));
+
+ // If it exists, remove it. It might be a directory, so remove it recursively.
+ if (exists) {
+ QM_TRY(MOZ_TO_RESULT(aLsArchiveFile.Remove(true)));
+
+ // XXX If we crash right here, the next session will copy the archive from
+ // webappsstore.sqlite again!
+ // XXX Create a marker file before removing the archive which can be
+ // used in MaybeCreateOrUpgradeLocalStorageArchive to create an empty
+ // archive instead of recopying it from webapppstore.sqlite (in other
+ // words, finishing what was started here).
+ }
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(const auto connection,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ &aLsArchiveFile, mozIStorageService::CONNECTION_DEFAULT));
+
+ QM_TRY(MOZ_TO_RESULT(StorageDBUpdater::CreateCurrentSchema(connection)));
+
+ QM_TRY(MOZ_TO_RESULT(InitializeLocalStorageArchive(connection)));
+
+ QM_TRY(MOZ_TO_RESULT(
+ SaveLocalStorageArchiveVersion(connection, kLocalStorageArchiveVersion)));
+
+ return Ok{};
+}
+
+nsresult QuotaManager::EnsureStorageIsInitialized() {
+ DiagnosticAssertIsOnIOThread();
+
+ const auto innerFunc =
+ [&](const auto& firstInitializationAttempt) -> nsresult {
+ if (mStorageConnection) {
+ MOZ_ASSERT(firstInitializationAttempt.Recorded());
+ return NS_OK;
+ }
+
+ QM_TRY_INSPECT(const auto& storageFile, QM_NewLocalFile(mBasePath));
+ QM_TRY(MOZ_TO_RESULT(storageFile->Append(mStorageName + kSQLiteSuffix)));
+
+ QM_TRY(MOZ_TO_RESULT(MaybeUpgradeToDefaultStorageDirectory(*storageFile)));
+
+ QM_TRY_INSPECT(const auto& ss,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<mozIStorageService>,
+ MOZ_SELECT_OVERLOAD(do_GetService),
+ MOZ_STORAGE_SERVICE_CONTRACTID));
+
+ QM_TRY_UNWRAP(
+ auto connection,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss, OpenUnsharedDatabase,
+ storageFile, mozIStorageService::CONNECTION_DEFAULT),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ErrToDefaultOk<nsCOMPtr<mozIStorageConnection>>));
+
+ if (!connection) {
+ // Nuke the database file.
+ QM_TRY(MOZ_TO_RESULT(storageFile->Remove(false)));
+
+ QM_TRY_UNWRAP(connection, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageConnection>, ss,
+ OpenUnsharedDatabase, storageFile,
+ mozIStorageService::CONNECTION_DEFAULT));
+ }
+
+ // We want extra durability for this important file.
+ QM_TRY(MOZ_TO_RESULT(
+ connection->ExecuteSimpleSQL("PRAGMA synchronous = EXTRA;"_ns)));
+
+ // Check to make sure that the storage version is correct.
+ QM_TRY(MOZ_TO_RESULT(MaybeCreateOrUpgradeStorage(*connection)));
+
+ QM_TRY(MaybeRemoveLocalStorageArchiveTmpFile());
+
+ QM_TRY_INSPECT(const auto& lsArchiveFile,
+ GetLocalStorageArchiveFile(*mStoragePath));
+
+ if (CachedNextGenLocalStorageEnabled()) {
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MaybeCreateOrUpgradeLocalStorageArchive(*lsArchiveFile),
+ // Predicate.
+ IsDatabaseCorruptionError,
+ // Fallback.
+ ([&](const nsresult rv) -> Result<Ok, nsresult> {
+ QM_TRY_RETURN(CreateEmptyLocalStorageArchive(*lsArchiveFile));
+ })));
+ } else {
+ QM_TRY(
+ MOZ_TO_RESULT(MaybeRemoveLocalStorageDataAndArchive(*lsArchiveFile)));
+ }
+
+ QM_TRY_UNWRAP(mCacheUsable, MaybeCreateOrUpgradeCache(*connection));
+
+ if (mCacheUsable && gInvalidateQuotaCache) {
+ QM_TRY(InvalidateCache(*connection));
+
+ gInvalidateQuotaCache = false;
+ }
+
+ mStorageConnection = std::move(connection);
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(
+ Initialization::Storage,
+ "dom::quota::FirstInitializationAttempt::Storage"_ns, innerFunc);
+}
+
+RefPtr<ClientDirectoryLock> QuotaManager::CreateDirectoryLock(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, bool aExclusive) {
+ AssertIsOnOwningThread();
+
+ return DirectoryLockImpl::Create(WrapNotNullUnchecked(this), aPersistenceType,
+ aOriginMetadata, aClientType, aExclusive);
+}
+
+RefPtr<UniversalDirectoryLock> QuotaManager::CreateDirectoryLockInternal(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType,
+ bool aExclusive) {
+ AssertIsOnOwningThread();
+
+ return DirectoryLockImpl::CreateInternal(WrapNotNullUnchecked(this),
+ aPersistenceType, aOriginScope,
+ aClientType, aExclusive);
+}
+
+Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
+QuotaManager::EnsurePersistentOriginIsInitialized(
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aOriginMetadata.mPersistenceType == PERSISTENCE_TYPE_PERSISTENT);
+ MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
+
+ const auto innerFunc = [&aOriginMetadata,
+ this](const auto& firstInitializationAttempt)
+ -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
+ QM_TRY_UNWRAP(auto directory,
+ GetDirectoryForOrigin(PERSISTENCE_TYPE_PERSISTENT,
+ aOriginMetadata.mOrigin));
+
+ if (mInitializedOrigins.Contains(aOriginMetadata.mOrigin)) {
+ MOZ_ASSERT(firstInitializationAttempt.Recorded());
+ return std::pair(std::move(directory), false);
+ }
+
+ QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory));
+
+ QM_TRY_INSPECT(
+ const int64_t& timestamp,
+ ([this, created, &directory,
+ &aOriginMetadata]() -> Result<int64_t, nsresult> {
+ if (created) {
+ const int64_t timestamp = PR_Now();
+
+ // Only creating .metadata-v2 to reduce IO.
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
+ /* aPersisted */ true,
+ aOriginMetadata)));
+
+ return timestamp;
+ }
+
+ // Get the metadata. We only use the timestamp.
+ QM_TRY_INSPECT(const auto& metadata,
+ LoadFullOriginMetadataWithRestore(directory));
+
+ MOZ_ASSERT(metadata.mLastAccessTime <= PR_Now());
+
+ return metadata.mLastAccessTime;
+ }()));
+
+ QM_TRY(MOZ_TO_RESULT(InitializeOrigin(PERSISTENCE_TYPE_PERSISTENT,
+ aOriginMetadata, timestamp,
+ /* aPersisted */ true, directory)));
+
+ mInitializedOrigins.AppendElement(aOriginMetadata.mOrigin);
+
+ return std::pair(std::move(directory), created);
+ };
+
+ return ExecuteOriginInitialization(
+ aOriginMetadata.mOrigin, OriginInitialization::PersistentOrigin,
+ "dom::quota::FirstOriginInitializationAttempt::PersistentOrigin"_ns,
+ innerFunc);
+}
+
+Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
+QuotaManager::EnsureTemporaryOriginIsInitialized(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+ MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
+ MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized);
+
+ const auto innerFunc = [&aPersistenceType, &aOriginMetadata,
+ this](const auto&)
+ -> mozilla::Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult> {
+ // Get directory for this origin and persistence type.
+ QM_TRY_UNWRAP(
+ auto directory,
+ GetDirectoryForOrigin(aPersistenceType, aOriginMetadata.mOrigin));
+
+ QM_TRY_INSPECT(const bool& created, EnsureOriginDirectory(*directory));
+
+ if (created) {
+ const int64_t timestamp =
+ NoteOriginDirectoryCreated(aOriginMetadata, /* aPersisted */ false);
+
+ // Only creating .metadata-v2 to reduce IO.
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
+ /* aPersisted */ false,
+ aOriginMetadata)));
+ }
+
+ // TODO: If the metadata file exists and we didn't call
+ // LoadFullOriginMetadataWithRestore for it (because the quota info
+ // was loaded from the cache), then the group in the metadata file
+ // may be wrong, so it should be checked and eventually updated.
+ // It's not a big deal that we are not doing it here, because the
+ // origin will be marked as "accessed", so
+ // LoadFullOriginMetadataWithRestore will be called for the metadata
+ // file in next session in LoadQuotaFromCache.
+
+ return std::pair(std::move(directory), created);
+ };
+
+ return ExecuteOriginInitialization(
+ aOriginMetadata.mOrigin, OriginInitialization::TemporaryOrigin,
+ "dom::quota::FirstOriginInitializationAttempt::TemporaryOrigin"_ns,
+ innerFunc);
+}
+
+nsresult QuotaManager::EnsureTemporaryStorageIsInitialized() {
+ AssertIsOnIOThread();
+ MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
+
+ const auto innerFunc =
+ [&](const auto& firstInitializationAttempt) -> nsresult {
+ if (mTemporaryStorageInitialized) {
+ MOZ_ASSERT(firstInitializationAttempt.Recorded());
+ return NS_OK;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& storageDir,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsIFile>,
+ MOZ_SELECT_OVERLOAD(do_CreateInstance),
+ NS_LOCAL_FILE_CONTRACTID));
+
+ QM_TRY(MOZ_TO_RESULT(storageDir->InitWithPath(GetStoragePath())));
+
+ // The storage directory must exist before calling GetTemporaryStorageLimit.
+ QM_TRY_INSPECT(const bool& created, EnsureDirectory(*storageDir));
+
+ Unused << created;
+
+ QM_TRY_UNWRAP(mTemporaryStorageLimit,
+ GetTemporaryStorageLimit(*storageDir));
+
+ QM_TRY(MOZ_TO_RESULT(LoadQuota()));
+
+ mTemporaryStorageInitialized = true;
+
+ CleanupTemporaryStorage();
+
+ if (mCacheUsable) {
+ QM_TRY(InvalidateCache(*mStorageConnection));
+ }
+
+ return NS_OK;
+ };
+
+ return ExecuteInitialization(
+ Initialization::TemporaryStorage,
+ "dom::quota::FirstInitializationAttempt::TemporaryStorage"_ns, innerFunc);
+}
+
+RefPtr<BoolPromise> QuotaManager::ShutdownStorage() {
+ if (!mShuttingDownStorage) {
+ mShuttingDownStorage = true;
+
+ auto shutdownStorageOp = MakeRefPtr<ShutdownStorageOp>();
+
+ RegisterNormalOriginOp(*shutdownStorageOp);
+
+ shutdownStorageOp->RunImmediately();
+
+ shutdownStorageOp->OnResults()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<QuotaManager>(this)](bool aResolveValue) {
+ self->mShuttingDownStorage = false;
+
+ self->mShutdownStoragePromiseHolder.ResolveIfExists(aResolveValue,
+ __func__);
+ },
+ [self = RefPtr<QuotaManager>(this)](nsresult aRejectValue) {
+ self->mShuttingDownStorage = false;
+
+ self->mShutdownStoragePromiseHolder.RejectIfExists(aRejectValue,
+ __func__);
+ });
+ }
+
+ return mShutdownStoragePromiseHolder.Ensure(__func__);
+}
+
+void QuotaManager::ShutdownStorageInternal() {
+ AssertIsOnIOThread();
+
+ if (mStorageConnection) {
+ mInitializationInfo.ResetOriginInitializationInfos();
+ mInitializedOrigins.Clear();
+
+ if (mTemporaryStorageInitialized) {
+ if (mCacheUsable) {
+ UnloadQuota();
+ } else {
+ RemoveQuota();
+ }
+
+ mTemporaryStorageInitialized = false;
+ }
+
+ ReleaseIOThreadObjects();
+
+ mStorageConnection = nullptr;
+ mCacheUsable = false;
+ }
+
+ mInitializationInfo.ResetFirstInitializationAttempts();
+}
+
+Result<bool, nsresult> QuotaManager::EnsureOriginDirectory(
+ nsIFile& aDirectory) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aDirectory, Exists));
+
+ if (!exists) {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, aDirectory, GetLeafName)
+ .map([](const auto& leafName) {
+ return NS_ConvertUTF16toUTF8(leafName);
+ }));
+
+ QM_TRY(OkIf(IsSanitizedOriginValid(leafName)), Err(NS_ERROR_FAILURE),
+ [](const auto&) {
+ QM_WARNING(
+ "Preventing creation of a new origin directory which is not "
+ "supported by our origin parser or is obsolete!");
+ });
+ }
+
+ QM_TRY_RETURN(EnsureDirectory(aDirectory));
+}
+
+nsresult QuotaManager::AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType) {
+ AssertIsOnIOThread();
+
+ if (aClientType.IsNull()) {
+ for (Client::Type type : AllClientTypes()) {
+ QM_TRY(MOZ_TO_RESULT((*mClients)[type]->AboutToClearOrigins(
+ aPersistenceType, aOriginScope)));
+ }
+ } else {
+ QM_TRY(MOZ_TO_RESULT((*mClients)[aClientType.Value()]->AboutToClearOrigins(
+ aPersistenceType, aOriginScope)));
+ }
+
+ return NS_OK;
+}
+
+void QuotaManager::OriginClearCompleted(
+ PersistenceType aPersistenceType, const nsACString& aOrigin,
+ const Nullable<Client::Type>& aClientType) {
+ AssertIsOnIOThread();
+
+ if (aClientType.IsNull()) {
+ if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
+ mInitializedOrigins.RemoveElement(aOrigin);
+ }
+
+ for (Client::Type type : AllClientTypes()) {
+ (*mClients)[type]->OnOriginClearCompleted(aPersistenceType, aOrigin);
+ }
+ } else {
+ (*mClients)[aClientType.Value()]->OnOriginClearCompleted(aPersistenceType,
+ aOrigin);
+ }
+}
+
+Client* QuotaManager::GetClient(Client::Type aClientType) {
+ MOZ_ASSERT(aClientType >= Client::IDB);
+ MOZ_ASSERT(aClientType < Client::TypeMax());
+
+ return (*mClients)[aClientType];
+}
+
+const AutoTArray<Client::Type, Client::TYPE_MAX>&
+QuotaManager::AllClientTypes() {
+ if (CachedNextGenLocalStorageEnabled()) {
+ return *mAllClientTypes;
+ }
+ return *mAllClientTypesExceptLS;
+}
+
+uint64_t QuotaManager::GetGroupLimit() const {
+ // To avoid one group evicting all the rest, limit the amount any one group
+ // can use to 20% resp. a fifth. To prevent individual sites from using
+ // exorbitant amounts of storage where there is a lot of free space, cap the
+ // group limit to 10GB.
+ const auto x = std::min<uint64_t>(mTemporaryStorageLimit / 5, 10 GB);
+
+ // In low-storage situations, make an exception (while not exceeding the total
+ // storage limit).
+ return std::min<uint64_t>(mTemporaryStorageLimit,
+ std::max<uint64_t>(x, 10 MB));
+}
+
+std::pair<uint64_t, uint64_t> QuotaManager::GetUsageAndLimitForEstimate(
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ uint64_t totalGroupUsage = 0;
+
+ {
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ for (const PersistenceType type : kBestEffortPersistenceTypes) {
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
+ if (groupInfo) {
+ if (type == PERSISTENCE_TYPE_DEFAULT) {
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+
+ if (originInfo && originInfo->LockedPersisted()) {
+ return std::pair(mTemporaryStorageUsage, mTemporaryStorageLimit);
+ }
+ }
+
+ AssertNoOverflow(totalGroupUsage, groupInfo->mUsage);
+ totalGroupUsage += groupInfo->mUsage;
+ }
+ }
+ }
+ }
+
+ return std::pair(totalGroupUsage, GetGroupLimit());
+}
+
+uint64_t QuotaManager::GetOriginUsage(
+ const PrincipalMetadata& aPrincipalMetadata) {
+ AssertIsOnIOThread();
+
+ uint64_t usage = 0;
+
+ {
+ MutexAutoLock lock(mQuotaMutex);
+
+ GroupInfoPair* pair;
+ if (mGroupInfoPairs.Get(aPrincipalMetadata.mGroup, &pair)) {
+ for (const PersistenceType type : kBestEffortPersistenceTypes) {
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(type);
+ if (groupInfo) {
+ RefPtr<OriginInfo> originInfo =
+ groupInfo->LockedGetOriginInfo(aPrincipalMetadata.mOrigin);
+ if (originInfo) {
+ AssertNoOverflow(usage, originInfo->LockedUsage());
+ usage += originInfo->LockedUsage();
+ }
+ }
+ }
+ }
+ }
+
+ return usage;
+}
+
+Maybe<FullOriginMetadata> QuotaManager::GetFullOriginMetadata(
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+ MOZ_DIAGNOSTIC_ASSERT(mStorageConnection);
+ MOZ_DIAGNOSTIC_ASSERT(mTemporaryStorageInitialized);
+
+ MutexAutoLock lock(mQuotaMutex);
+
+ RefPtr<OriginInfo> originInfo =
+ LockedGetOriginInfo(aOriginMetadata.mPersistenceType, aOriginMetadata);
+ if (originInfo) {
+ return Some(originInfo->LockedFlattenToFullOriginMetadata());
+ }
+
+ return Nothing();
+}
+
+void QuotaManager::NotifyStoragePressure(uint64_t aUsage) {
+ mQuotaMutex.AssertNotCurrentThreadOwns();
+
+ RefPtr<StoragePressureRunnable> storagePressureRunnable =
+ new StoragePressureRunnable(aUsage);
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(storagePressureRunnable));
+}
+
+// static
+void QuotaManager::GetStorageId(PersistenceType aPersistenceType,
+ const nsACString& aOrigin,
+ Client::Type aClientType,
+ nsACString& aDatabaseId) {
+ nsAutoCString str;
+ str.AppendInt(aPersistenceType);
+ str.Append('*');
+ str.Append(aOrigin);
+ str.Append('*');
+ str.AppendInt(aClientType);
+
+ aDatabaseId = str;
+}
+
+// static
+bool QuotaManager::IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo) {
+ switch (aPrincipalInfo.type()) {
+ // A system principal is acceptable.
+ case PrincipalInfo::TSystemPrincipalInfo: {
+ return true;
+ }
+
+ // Validate content principals to ensure that the spec, originNoSuffix and
+ // baseDomain are sane.
+ case PrincipalInfo::TContentPrincipalInfo: {
+ const ContentPrincipalInfo& info =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+
+ // Verify the principal spec parses.
+ RefPtr<MozURL> specURL;
+ nsresult rv = MozURL::Init(getter_AddRefs(specURL), info.spec());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ QM_WARNING("A URL %s is not recognized by MozURL", info.spec().get());
+ return false;
+ }
+
+ // Verify the principal originNoSuffix matches spec.
+ nsCString originNoSuffix;
+ specURL->Origin(originNoSuffix);
+
+ if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) {
+ QM_WARNING("originNoSuffix (%s) doesn't match passed one (%s)!",
+ originNoSuffix.get(), info.originNoSuffix().get());
+ return false;
+ }
+
+ if (NS_WARN_IF(info.originNoSuffix().EqualsLiteral(kChromeOrigin))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(info.originNoSuffix().FindChar('^', 0) != -1)) {
+ QM_WARNING("originNoSuffix (%s) contains the '^' character!",
+ info.originNoSuffix().get());
+ return false;
+ }
+
+ // Verify the principal baseDomain exists.
+ if (NS_WARN_IF(info.baseDomain().IsVoid())) {
+ return false;
+ }
+
+ // Verify the principal baseDomain matches spec.
+ nsCString baseDomain;
+ rv = specURL->BaseDomain(baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(baseDomain != info.baseDomain())) {
+ QM_WARNING("baseDomain (%s) doesn't match passed one (%s)!",
+ baseDomain.get(), info.baseDomain().get());
+ return false;
+ }
+
+ return true;
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ // Null and expanded principals are not acceptable.
+ return false;
+}
+
+// static
+PrincipalMetadata QuotaManager::GetInfoFromValidatedPrincipalInfo(
+ const PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo));
+
+ switch (aPrincipalInfo.type()) {
+ case PrincipalInfo::TSystemPrincipalInfo: {
+ return GetInfoForChrome();
+ }
+
+ case PrincipalInfo::TContentPrincipalInfo: {
+ const ContentPrincipalInfo& info =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+
+ PrincipalMetadata principalMetadata;
+
+ info.attrs().CreateSuffix(principalMetadata.mSuffix);
+
+ principalMetadata.mGroup = info.baseDomain() + principalMetadata.mSuffix;
+
+ principalMetadata.mOrigin =
+ info.originNoSuffix() + principalMetadata.mSuffix;
+
+ return principalMetadata;
+ }
+
+ default: {
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+}
+
+// static
+nsAutoCString QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ const PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(IsPrincipalInfoValid(aPrincipalInfo));
+
+ switch (aPrincipalInfo.type()) {
+ case PrincipalInfo::TSystemPrincipalInfo: {
+ return nsAutoCString{GetOriginForChrome()};
+ }
+
+ case PrincipalInfo::TContentPrincipalInfo: {
+ const ContentPrincipalInfo& info =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+
+ nsAutoCString suffix;
+
+ info.attrs().CreateSuffix(suffix);
+
+ return info.originNoSuffix() + suffix;
+ }
+
+ default: {
+ MOZ_CRASH("Should never get here!");
+ }
+ }
+}
+
+// static
+Result<PrincipalMetadata, nsresult> QuotaManager::GetInfoFromPrincipal(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ if (aPrincipal->IsSystemPrincipal()) {
+ return GetInfoForChrome();
+ }
+
+ if (aPrincipal->GetIsNullPrincipal()) {
+ NS_WARNING("IndexedDB not supported from this principal!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ PrincipalMetadata principalMetadata;
+
+ QM_TRY(MOZ_TO_RESULT(aPrincipal->GetOrigin(principalMetadata.mOrigin)));
+
+ if (principalMetadata.mOrigin.EqualsLiteral(kChromeOrigin)) {
+ NS_WARNING("Non-chrome principal can't use chrome origin!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ aPrincipal->OriginAttributesRef().CreateSuffix(principalMetadata.mSuffix);
+
+ nsAutoCString baseDomain;
+ QM_TRY(MOZ_TO_RESULT(aPrincipal->GetBaseDomain(baseDomain)));
+
+ MOZ_ASSERT(!baseDomain.IsEmpty());
+
+ principalMetadata.mGroup = baseDomain + principalMetadata.mSuffix;
+
+ return principalMetadata;
+}
+
+// static
+Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromPrincipal(
+ nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ if (aPrincipal->IsSystemPrincipal()) {
+ return nsAutoCString{GetOriginForChrome()};
+ }
+
+ if (aPrincipal->GetIsNullPrincipal()) {
+ NS_WARNING("IndexedDB not supported from this principal!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ QM_TRY_UNWRAP(const auto origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoCString, aPrincipal, GetOrigin));
+
+ if (origin.EqualsLiteral(kChromeOrigin)) {
+ NS_WARNING("Non-chrome principal can't use chrome origin!");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ return origin;
+}
+
+// static
+Result<nsAutoCString, nsresult> QuotaManager::GetOriginFromWindow(
+ nsPIDOMWindowOuter* aWindow) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+ QM_TRY(OkIf(sop), Err(NS_ERROR_FAILURE));
+
+ nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal();
+ QM_TRY(OkIf(principal), Err(NS_ERROR_FAILURE));
+
+ QM_TRY_RETURN(GetOriginFromPrincipal(principal));
+}
+
+// static
+PrincipalMetadata QuotaManager::GetInfoForChrome() {
+ return {{}, GetOriginForChrome(), GetOriginForChrome()};
+}
+
+// static
+nsLiteralCString QuotaManager::GetOriginForChrome() {
+ return nsLiteralCString{kChromeOrigin};
+}
+
+// static
+bool QuotaManager::IsOriginInternal(const nsACString& aOrigin) {
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ // The first prompt is not required for these origins.
+ if (aOrigin.EqualsLiteral(kChromeOrigin) ||
+ StringBeginsWith(aOrigin, nsDependentCString(kAboutHomeOriginPrefix)) ||
+ StringBeginsWith(aOrigin, nsDependentCString(kIndexedDBOriginPrefix)) ||
+ StringBeginsWith(aOrigin, nsDependentCString(kResourceOriginPrefix))) {
+ return true;
+ }
+
+ return false;
+}
+
+// static
+bool QuotaManager::AreOriginsEqualOnDisk(const nsACString& aOrigin1,
+ const nsACString& aOrigin2) {
+ return MakeSanitizedOriginCString(aOrigin1) ==
+ MakeSanitizedOriginCString(aOrigin2);
+}
+
+// static
+Result<PrincipalInfo, nsresult> QuotaManager::ParseOrigin(
+ const nsACString& aOrigin) {
+ // An origin string either corresponds to a SystemPrincipalInfo or a
+ // ContentPrincipalInfo, see
+ // QuotaManager::GetOriginFromValidatedPrincipalInfo.
+
+ if (aOrigin.Equals(kChromeOrigin)) {
+ return PrincipalInfo{SystemPrincipalInfo{}};
+ }
+
+ ContentPrincipalInfo contentPrincipalInfo;
+
+ nsCString originalSuffix;
+ const OriginParser::ResultType result = OriginParser::ParseOrigin(
+ MakeSanitizedOriginCString(aOrigin), contentPrincipalInfo.spec(),
+ &contentPrincipalInfo.attrs(), originalSuffix);
+ QM_TRY(OkIf(result == OriginParser::ValidOrigin), Err(NS_ERROR_FAILURE));
+
+ return PrincipalInfo{std::move(contentPrincipalInfo)};
+}
+
+// static
+void QuotaManager::InvalidateQuotaCache() { gInvalidateQuotaCache = true; }
+
+uint64_t QuotaManager::LockedCollectOriginsForEviction(
+ uint64_t aMinSizeToBeFreed, nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
+ mQuotaMutex.AssertCurrentThreadOwns();
+
+ RefPtr<CollectOriginsHelper> helper =
+ new CollectOriginsHelper(mQuotaMutex, aMinSizeToBeFreed);
+
+ // Unlock while calling out to XPCOM (code behind the dispatch method needs
+ // to acquire its own lock which can potentially lead to a deadlock and it
+ // also calls an observer that can do various stuff like IO, so it's better
+ // to not hold our mutex while that happens).
+ {
+ MutexAutoUnlock autoUnlock(mQuotaMutex);
+
+ MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(helper, NS_DISPATCH_NORMAL));
+ }
+
+ return helper->BlockAndReturnOriginsForEviction(aLocks);
+}
+
+void QuotaManager::LockedRemoveQuotaForOrigin(
+ const OriginMetadata& aOriginMetadata) {
+ mQuotaMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(aOriginMetadata.mPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ GroupInfoPair* pair;
+ if (!mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ return;
+ }
+
+ MOZ_ASSERT(pair);
+
+ if (RefPtr<GroupInfo> groupInfo =
+ pair->LockedGetGroupInfo(aOriginMetadata.mPersistenceType)) {
+ groupInfo->LockedRemoveOriginInfo(aOriginMetadata.mOrigin);
+
+ if (!groupInfo->LockedHasOriginInfos()) {
+ pair->LockedClearGroupInfo(aOriginMetadata.mPersistenceType);
+
+ if (!pair->LockedHasGroupInfos()) {
+ mGroupInfoPairs.Remove(aOriginMetadata.mGroup);
+ }
+ }
+ }
+}
+
+already_AddRefed<GroupInfo> QuotaManager::LockedGetOrCreateGroupInfo(
+ PersistenceType aPersistenceType, const nsACString& aSuffix,
+ const nsACString& aGroup) {
+ mQuotaMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ GroupInfoPair* const pair =
+ mGroupInfoPairs.GetOrInsertNew(aGroup, aSuffix, aGroup);
+
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
+ if (!groupInfo) {
+ groupInfo = new GroupInfo(pair, aPersistenceType);
+ pair->LockedSetGroupInfo(aPersistenceType, groupInfo);
+ }
+
+ return groupInfo.forget();
+}
+
+already_AddRefed<OriginInfo> QuotaManager::LockedGetOriginInfo(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata) {
+ mQuotaMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ GroupInfoPair* pair;
+ if (mGroupInfoPairs.Get(aOriginMetadata.mGroup, &pair)) {
+ RefPtr<GroupInfo> groupInfo = pair->LockedGetGroupInfo(aPersistenceType);
+ if (groupInfo) {
+ return groupInfo->LockedGetOriginInfo(aOriginMetadata.mOrigin);
+ }
+ }
+
+ return nullptr;
+}
+
+template <typename Iterator>
+void QuotaManager::MaybeInsertNonPersistedOriginInfos(
+ Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo,
+ const RefPtr<GroupInfo>& aDefaultGroupInfo) {
+ const auto copy = [&aDest](const GroupInfo& groupInfo) {
+ std::copy_if(
+ groupInfo.mOriginInfos.cbegin(), groupInfo.mOriginInfos.cend(), aDest,
+ [](const auto& originInfo) { return !originInfo->LockedPersisted(); });
+ };
+
+ if (aTemporaryGroupInfo) {
+ MOZ_ASSERT(PERSISTENCE_TYPE_TEMPORARY ==
+ aTemporaryGroupInfo->GetPersistenceType());
+
+ copy(*aTemporaryGroupInfo);
+ }
+ if (aDefaultGroupInfo) {
+ MOZ_ASSERT(PERSISTENCE_TYPE_DEFAULT ==
+ aDefaultGroupInfo->GetPersistenceType());
+
+ copy(*aDefaultGroupInfo);
+ }
+}
+
+template <typename Collect, typename Pred>
+QuotaManager::OriginInfosFlatTraversable
+QuotaManager::CollectLRUOriginInfosUntil(Collect&& aCollect, Pred&& aPred) {
+ OriginInfosFlatTraversable originInfos;
+
+ std::forward<Collect>(aCollect)(MakeBackInserter(originInfos));
+
+ originInfos.Sort(OriginInfoAccessTimeComparator());
+
+ const auto foundIt = std::find_if(originInfos.cbegin(), originInfos.cend(),
+ std::forward<Pred>(aPred));
+
+ originInfos.TruncateLength(foundIt - originInfos.cbegin());
+
+ return originInfos;
+}
+
+QuotaManager::OriginInfosNestedTraversable
+QuotaManager::GetOriginInfosExceedingGroupLimit() const {
+ MutexAutoLock lock(mQuotaMutex);
+
+ OriginInfosNestedTraversable originInfos;
+
+ for (const auto& entry : mGroupInfoPairs) {
+ const auto& pair = entry.GetData();
+
+ MOZ_ASSERT(!entry.GetKey().IsEmpty());
+ MOZ_ASSERT(pair);
+
+ uint64_t groupUsage = 0;
+
+ const RefPtr<GroupInfo> temporaryGroupInfo =
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY);
+ if (temporaryGroupInfo) {
+ groupUsage += temporaryGroupInfo->mUsage;
+ }
+
+ const RefPtr<GroupInfo> defaultGroupInfo =
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT);
+ if (defaultGroupInfo) {
+ groupUsage += defaultGroupInfo->mUsage;
+ }
+
+ if (groupUsage > 0) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager, "Shouldn't be null!");
+
+ if (groupUsage > quotaManager->GetGroupLimit()) {
+ originInfos.AppendElement(CollectLRUOriginInfosUntil(
+ [&temporaryGroupInfo, &defaultGroupInfo](auto inserter) {
+ MaybeInsertNonPersistedOriginInfos(
+ std::move(inserter), temporaryGroupInfo, defaultGroupInfo);
+ },
+ [&groupUsage, quotaManager](const auto& originInfo) {
+ groupUsage -= originInfo->LockedUsage();
+
+ return groupUsage <= quotaManager->GetGroupLimit();
+ }));
+ }
+ }
+ }
+
+ return originInfos;
+}
+
+QuotaManager::OriginInfosNestedTraversable
+QuotaManager::GetOriginInfosExceedingGlobalLimit() const {
+ MutexAutoLock lock(mQuotaMutex);
+
+ QuotaManager::OriginInfosNestedTraversable res;
+ res.AppendElement(CollectLRUOriginInfosUntil(
+ // XXX The lambda only needs to capture this, but due to Bug 1421435 it
+ // can't.
+ [&](auto inserter) {
+ for (const auto& entry : mGroupInfoPairs) {
+ const auto& pair = entry.GetData();
+
+ MOZ_ASSERT(!entry.GetKey().IsEmpty());
+ MOZ_ASSERT(pair);
+
+ MaybeInsertNonPersistedOriginInfos(
+ inserter, pair->LockedGetGroupInfo(PERSISTENCE_TYPE_TEMPORARY),
+ pair->LockedGetGroupInfo(PERSISTENCE_TYPE_DEFAULT));
+ }
+ },
+ [temporaryStorageUsage = mTemporaryStorageUsage,
+ temporaryStorageLimit = mTemporaryStorageLimit,
+ doomedUsage = uint64_t{0}](const auto& originInfo) mutable {
+ if (temporaryStorageUsage - doomedUsage <= temporaryStorageLimit) {
+ return true;
+ }
+
+ doomedUsage += originInfo->LockedUsage();
+ return false;
+ }));
+
+ return res;
+}
+
+void QuotaManager::ClearOrigins(
+ const OriginInfosNestedTraversable& aDoomedOriginInfos) {
+ AssertIsOnIOThread();
+
+ // If we are in shutdown, we could break off early from clearing origins.
+ // In such cases, we would like to track the ones that were already cleared
+ // up, such that other essential cleanup could be performed on clearedOrigins.
+ // clearedOrigins is used in calls to LockedRemoveQuotaForOrigin and
+ // OriginClearCompleted below. We could have used a collection of OriginInfos
+ // rather than flattening them to OriginMetadata but groupInfo in OriginInfo
+ // is just a raw ptr and LockedRemoveQuotaForOrigin might delete groupInfo and
+ // as a result, we would not be able to get origin persistence type required
+ // in OriginClearCompleted call after lockedRemoveQuotaForOrigin call.
+ nsTArray<OriginMetadata> clearedOrigins;
+
+ // XXX Does this need to be done a) in order and/or b) sequentially?
+ for (const auto& doomedOriginInfo :
+ Flatten<OriginInfosFlatTraversable::value_type>(aDoomedOriginInfos)) {
+#ifdef DEBUG
+ {
+ MutexAutoLock lock(mQuotaMutex);
+ MOZ_ASSERT(!doomedOriginInfo->LockedPersisted());
+ }
+#endif
+
+ // TODO: We are currently only checking for this flag here which
+ // means that we cannot break off once we start cleaning an origin. It
+ // could be better if we could check for shutdown flag while cleaning an
+ // origin such that we could break off early from the cleaning process if
+ // we are stuck cleaning on one huge origin. Bug1797098 has been filed to
+ // track this.
+ if (QuotaManager::IsShuttingDown()) {
+ break;
+ }
+
+ DeleteFilesForOrigin(doomedOriginInfo->mGroupInfo->mPersistenceType,
+ doomedOriginInfo->mOrigin);
+
+ clearedOrigins.AppendElement(doomedOriginInfo->FlattenToOriginMetadata());
+ }
+
+ {
+ MutexAutoLock lock(mQuotaMutex);
+
+ for (const auto& clearedOrigin : clearedOrigins) {
+ LockedRemoveQuotaForOrigin(clearedOrigin);
+ }
+ }
+
+ for (const auto& clearedOrigin : clearedOrigins) {
+ OriginClearCompleted(clearedOrigin.mPersistenceType, clearedOrigin.mOrigin,
+ Nullable<Client::Type>());
+ }
+}
+
+void QuotaManager::CleanupTemporaryStorage() {
+ AssertIsOnIOThread();
+
+ // Evicting origins that exceed their group limit also affects the global
+ // temporary storage usage, so these steps have to be taken sequentially.
+ // Combining them doesn't seem worth the added complexity.
+ ClearOrigins(GetOriginInfosExceedingGroupLimit());
+ ClearOrigins(GetOriginInfosExceedingGlobalLimit());
+
+ if (mTemporaryStorageUsage > mTemporaryStorageLimit) {
+ // If disk space is still low after origin clear, notify storage pressure.
+ NotifyStoragePressure(mTemporaryStorageUsage);
+ }
+}
+
+void QuotaManager::DeleteFilesForOrigin(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) {
+ QM_TRY_INSPECT(const auto& directory,
+ GetDirectoryForOrigin(aPersistenceType, aOrigin), QM_VOID);
+
+ nsresult rv = directory->Remove(true);
+ if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ // This should never fail if we've closed all storage connections
+ // correctly...
+ NS_ERROR("Failed to remove directory!");
+ }
+}
+
+void QuotaManager::FinalizeOriginEviction(
+ nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks) {
+ NS_ASSERTION(!NS_IsMainThread(), "Wrong thread!");
+
+ RefPtr<FinalizeOriginEvictionOp> op =
+ new FinalizeOriginEvictionOp(mOwningThread, std::move(aLocks));
+
+ if (IsOnIOThread()) {
+ op->RunOnIOThreadImmediately();
+ } else {
+ op->Dispatch();
+ }
+}
+
+Result<Ok, nsresult> QuotaManager::ArchiveOrigins(
+ const nsTArray<FullOriginMetadata>& aFullOriginMetadatas) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!aFullOriginMetadatas.IsEmpty());
+
+ QM_TRY_INSPECT(const auto& storageArchivesDir,
+ QM_NewLocalFile(*mStorageArchivesPath));
+
+ // Create another subdir, so once we decide to remove all temporary archives,
+ // we can remove only the subdir and the parent directory can still be used
+ // for something else or similar in future. Otherwise, we would have to
+ // figure out a new name for it.
+ QM_TRY(MOZ_TO_RESULT(storageArchivesDir->Append(u"0"_ns)));
+
+ PRExplodedTime now;
+ PR_ExplodeTime(PR_Now(), PR_LocalTimeParameters, &now);
+
+ const auto dateStr =
+ nsPrintfCString("%04hd-%02" PRId32 "-%02" PRId32, now.tm_year,
+ now.tm_month + 1, now.tm_mday);
+
+ QM_TRY_INSPECT(
+ const auto& storageArchiveDir,
+ CloneFileAndAppend(*storageArchivesDir, NS_ConvertASCIItoUTF16(dateStr)));
+
+ QM_TRY(MOZ_TO_RESULT(
+ storageArchiveDir->CreateUnique(nsIFile::DIRECTORY_TYPE, 0700)));
+
+ QM_TRY_INSPECT(const auto& defaultStorageArchiveDir,
+ CloneFileAndAppend(*storageArchiveDir,
+ nsLiteralString(DEFAULT_DIRECTORY_NAME)));
+
+ QM_TRY_INSPECT(const auto& temporaryStorageArchiveDir,
+ CloneFileAndAppend(*storageArchiveDir,
+ nsLiteralString(TEMPORARY_DIRECTORY_NAME)));
+
+ for (const auto& fullOriginMetadata : aFullOriginMetadatas) {
+ MOZ_ASSERT(
+ IsBestEffortPersistenceType(fullOriginMetadata.mPersistenceType));
+
+ QM_TRY_INSPECT(const auto& directory,
+ GetDirectoryForOrigin(fullOriginMetadata.mPersistenceType,
+ fullOriginMetadata.mOrigin));
+
+ // The origin could have been removed, for example due to corruption.
+ QM_TRY_INSPECT(
+ const auto& moved,
+ QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(
+ directory->MoveTo(fullOriginMetadata.mPersistenceType ==
+ PERSISTENCE_TYPE_DEFAULT
+ ? defaultStorageArchiveDir
+ : temporaryStorageArchiveDir,
+ u""_ns))
+ .map([](Ok) { return true; }),
+ // Predicate.
+ ([](const nsresult rv) { return rv == NS_ERROR_FILE_NOT_FOUND; }),
+ // Fallback.
+ ErrToOk<false>));
+
+ if (moved) {
+ RemoveQuotaForOrigin(fullOriginMetadata.mPersistenceType,
+ fullOriginMetadata);
+ }
+ }
+
+ return Ok{};
+}
+
+auto QuotaManager::GetDirectoryLockTable(PersistenceType aPersistenceType)
+ -> DirectoryLockTable& {
+ switch (aPersistenceType) {
+ case PERSISTENCE_TYPE_TEMPORARY:
+ return mTemporaryDirectoryLockTable;
+ case PERSISTENCE_TYPE_DEFAULT:
+ return mDefaultDirectoryLockTable;
+
+ case PERSISTENCE_TYPE_PERSISTENT:
+ case PERSISTENCE_TYPE_INVALID:
+ default:
+ MOZ_CRASH("Bad persistence type value!");
+ }
+}
+
+bool QuotaManager::IsSanitizedOriginValid(const nsACString& aSanitizedOrigin) {
+ AssertIsOnIOThread();
+
+ // Do not parse this sanitized origin string, if we already parsed it.
+ return mValidOrigins.LookupOrInsertWith(
+ aSanitizedOrigin, [&aSanitizedOrigin] {
+ nsCString spec;
+ OriginAttributes attrs;
+ nsCString originalSuffix;
+ const auto result = OriginParser::ParseOrigin(aSanitizedOrigin, spec,
+ &attrs, originalSuffix);
+
+ return result == OriginParser::ValidOrigin;
+ });
+}
+
+int64_t QuotaManager::GenerateDirectoryLockId() {
+ const int64_t directorylockId = mNextDirectoryLockId;
+
+ if (CheckedInt64 result = CheckedInt64(mNextDirectoryLockId) + 1;
+ result.isValid()) {
+ mNextDirectoryLockId = result.value();
+ } else {
+ NS_WARNING("Quota manager has run out of ids for directory locks!");
+
+ // There's very little chance for this to happen given the max size of
+ // 64 bit integer but if it happens we can just reset mNextDirectoryLockId
+ // to zero since such old directory locks shouldn't exist anymore.
+ mNextDirectoryLockId = 0;
+ }
+
+ // TODO: Maybe add an assertion here to check that there is no existing
+ // directory lock with given id.
+
+ return directorylockId;
+}
+
+template <typename Func>
+auto QuotaManager::ExecuteInitialization(const Initialization aInitialization,
+ Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&> {
+ return quota::ExecuteInitialization(mInitializationInfo, aInitialization,
+ std::forward<Func>(aFunc));
+}
+
+template <typename Func>
+auto QuotaManager::ExecuteInitialization(const Initialization aInitialization,
+ const nsACString& aContext,
+ Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&> {
+ return quota::ExecuteInitialization(mInitializationInfo, aInitialization,
+ aContext, std::forward<Func>(aFunc));
+}
+
+template <typename Func>
+auto QuotaManager::ExecuteOriginInitialization(
+ const nsACString& aOrigin, const OriginInitialization aInitialization,
+ const nsACString& aContext, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&> {
+ return quota::ExecuteInitialization(
+ mInitializationInfo.MutableOriginInitializationInfoRef(
+ aOrigin, CreateIfNonExistent{}),
+ aInitialization, aContext, std::forward<Func>(aFunc));
+}
+
+/*******************************************************************************
+ * Local class implementations
+ ******************************************************************************/
+
+CollectOriginsHelper::CollectOriginsHelper(mozilla::Mutex& aMutex,
+ uint64_t aMinSizeToBeFreed)
+ : Runnable("dom::quota::CollectOriginsHelper"),
+ mMinSizeToBeFreed(aMinSizeToBeFreed),
+ mMutex(aMutex),
+ mCondVar(aMutex, "CollectOriginsHelper::mCondVar"),
+ mSizeToBeFreed(0),
+ mWaiting(true) {
+ MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!");
+ mMutex.AssertCurrentThreadOwns();
+}
+
+int64_t CollectOriginsHelper::BlockAndReturnOriginsForEviction(
+ nsTArray<RefPtr<OriginDirectoryLock>>& aLocks) {
+ MOZ_ASSERT(!NS_IsMainThread(), "Wrong thread!");
+ mMutex.AssertCurrentThreadOwns();
+
+ while (mWaiting) {
+ mCondVar.Wait();
+ }
+
+ mLocks.SwapElements(aLocks);
+ return mSizeToBeFreed;
+}
+
+NS_IMETHODIMP
+CollectOriginsHelper::Run() {
+ AssertIsOnBackgroundThread();
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ NS_ASSERTION(quotaManager, "Shouldn't be null!");
+
+ // We use extra stack vars here to avoid race detector warnings (the same
+ // memory accessed with and without the lock held).
+ nsTArray<RefPtr<OriginDirectoryLock>> locks;
+ uint64_t sizeToBeFreed =
+ quotaManager->CollectOriginsForEviction(mMinSizeToBeFreed, locks);
+
+ MutexAutoLock lock(mMutex);
+
+ NS_ASSERTION(mWaiting, "Huh?!");
+
+ mLocks.SwapElements(locks);
+ mSizeToBeFreed = sizeToBeFreed;
+ mWaiting = false;
+ mCondVar.Notify();
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * OriginOperationBase
+ ******************************************************************************/
+
+NS_IMETHODIMP
+OriginOperationBase::Run() {
+ nsresult rv;
+
+ switch (mState) {
+ case State_Initial: {
+ rv = Init();
+ break;
+ }
+
+ case State_DirectoryOpenPending: {
+ rv = DirectoryOpen();
+ break;
+ }
+
+ case State_DirectoryWorkOpen: {
+ rv = DirectoryWork();
+ break;
+ }
+
+ case State_UnblockingOpen: {
+ UnblockOpen();
+ return NS_OK;
+ }
+
+ default:
+ MOZ_CRASH("Bad state!");
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv)) && mState != State_UnblockingOpen) {
+ Finish(rv);
+ }
+
+ return NS_OK;
+}
+
+nsresult OriginOperationBase::DirectoryOpen() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State_DirectoryOpenPending);
+
+ QuotaManager* const quotaManager = QuotaManager::Get();
+ QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE);
+
+ // Must set this before dispatching otherwise we will race with the IO thread.
+ AdvanceState();
+
+ QM_TRY(MOZ_TO_RESULT(
+ quotaManager->IOThread()->Dispatch(this, NS_DISPATCH_NORMAL)),
+ NS_ERROR_FAILURE);
+
+ return NS_OK;
+}
+
+void OriginOperationBase::Finish(nsresult aResult) {
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = aResult;
+ }
+
+ // Must set mState before dispatching otherwise we will race with the main
+ // thread.
+ mState = State_UnblockingOpen;
+
+ MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+nsresult OriginOperationBase::Init() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState == State_Initial);
+
+ if (QuotaManager::IsShuttingDown()) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (mNeedsQuotaManagerInit) {
+ QM_TRY(QuotaManager::EnsureCreated());
+ }
+
+ Open();
+
+ return NS_OK;
+}
+
+nsresult OriginOperationBase::DirectoryWork() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mState == State_DirectoryWorkOpen);
+
+ QuotaManager* const quotaManager = QuotaManager::Get();
+ QM_TRY(OkIf(quotaManager), NS_ERROR_FAILURE);
+
+ if (mNeedsStorageInit) {
+ QM_TRY(MOZ_TO_RESULT(quotaManager->EnsureStorageIsInitialized()));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(DoDirectoryWork(*quotaManager)));
+
+ // Must set mState before dispatching otherwise we will race with the owning
+ // thread.
+ AdvanceState();
+
+ MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL));
+
+ return NS_OK;
+}
+
+void FinalizeOriginEvictionOp::Dispatch() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(GetState() == State_Initial);
+
+ SetState(State_DirectoryOpenPending);
+
+ MOZ_ALWAYS_SUCCEEDS(mOwningThread->Dispatch(this, NS_DISPATCH_NORMAL));
+}
+
+void FinalizeOriginEvictionOp::RunOnIOThreadImmediately() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(GetState() == State_Initial);
+
+ SetState(State_DirectoryWorkOpen);
+
+ MOZ_ALWAYS_SUCCEEDS(this->Run());
+}
+
+void FinalizeOriginEvictionOp::Open() { MOZ_CRASH("Shouldn't get here!"); }
+
+nsresult FinalizeOriginEvictionOp::DoDirectoryWork(
+ QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("FinalizeOriginEvictionOp::DoDirectoryWork", OTHER);
+
+ for (const auto& lock : mLocks) {
+ aQuotaManager.OriginClearCompleted(
+ lock->GetPersistenceType(), lock->Origin(), Nullable<Client::Type>());
+ }
+
+ return NS_OK;
+}
+
+void FinalizeOriginEvictionOp::UnblockOpen() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(GetState() == State_UnblockingOpen);
+
+#ifdef DEBUG
+ NoteActorDestroyed();
+#endif
+
+ mLocks.Clear();
+
+ AdvanceState();
+}
+
+NS_IMPL_ISUPPORTS_INHERITED0(NormalOriginOperationBase, Runnable)
+
+RefPtr<DirectoryLock> NormalOriginOperationBase::CreateDirectoryLock() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(GetState() == State_DirectoryOpenPending);
+ MOZ_ASSERT(QuotaManager::Get());
+
+ return QuotaManager::Get()->CreateDirectoryLockInternal(
+ mPersistenceType, mOriginScope, mClientType, mExclusive);
+}
+
+void NormalOriginOperationBase::Open() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(GetState() == State_Initial);
+ MOZ_ASSERT(QuotaManager::Get());
+
+ AdvanceState();
+
+ RefPtr<DirectoryLock> directoryLock = CreateDirectoryLock();
+ if (directoryLock) {
+ directoryLock->Acquire(this);
+ } else {
+ QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID,
+ [this](const nsresult rv) { Finish(rv); });
+ }
+}
+
+void NormalOriginOperationBase::UnblockOpen() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(GetState() == State_UnblockingOpen);
+
+ SendResults();
+
+ if (mDirectoryLock) {
+ mDirectoryLock = nullptr;
+ }
+
+ UnregisterNormalOriginOp(*this);
+
+ AdvanceState();
+}
+
+void NormalOriginOperationBase::DirectoryLockAcquired(DirectoryLock* aLock) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aLock);
+ MOZ_ASSERT(GetState() == State_DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ mDirectoryLock = aLock;
+
+ QM_TRY(MOZ_TO_RESULT(DirectoryOpen()), QM_VOID,
+ [this](const nsresult rv) { Finish(rv); });
+}
+
+void NormalOriginOperationBase::DirectoryLockFailed() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(GetState() == State_DirectoryOpenPending);
+ MOZ_ASSERT(!mDirectoryLock);
+
+ Finish(NS_ERROR_FAILURE);
+}
+
+nsresult SaveOriginAccessTimeOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!mPersistenceType.IsNull());
+ MOZ_ASSERT(mOriginScope.IsOrigin());
+
+ AUTO_PROFILER_LABEL("SaveOriginAccessTimeOp::DoDirectoryWork", OTHER);
+
+ QM_TRY_INSPECT(const auto& file,
+ aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(),
+ mOriginScope.GetOrigin()));
+
+ // The origin directory might not exist
+ // anymore, because it was deleted by a clear operation.
+ QM_TRY_INSPECT(const bool& exists, MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists));
+
+ if (exists) {
+ QM_TRY(MOZ_TO_RESULT(file->Append(nsLiteralString(METADATA_V2_FILE_NAME))));
+
+ QM_TRY_INSPECT(const auto& stream,
+ GetBinaryOutputStream(*file, FileFlag::Update));
+ MOZ_ASSERT(stream);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Write64(mTimestamp)));
+ }
+
+ return NS_OK;
+}
+
+void SaveOriginAccessTimeOp::SendResults() {
+#ifdef DEBUG
+ NoteActorDestroyed();
+#endif
+}
+
+#ifdef DEBUG
+nsresult ShutdownStorageOp::DirectoryOpen() {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(mDirectoryLock);
+ mDirectoryLock->AssertIsAcquiredExclusively();
+
+ return NormalOriginOperationBase::DirectoryOpen();
+}
+#endif
+
+nsresult ShutdownStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("ShutdownStorageOp::DoDirectoryWork", OTHER);
+
+ aQuotaManager.ShutdownStorageInternal();
+
+ return NS_OK;
+}
+
+void ShutdownStorageOp::SendResults() {
+#ifdef DEBUG
+ NoteActorDestroyed();
+#endif
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ mPromiseHolder.ResolveIfExists(true, __func__);
+ } else {
+ mPromiseHolder.RejectIfExists(mResultCode, __func__);
+ }
+}
+
+NS_IMETHODIMP
+StoragePressureRunnable::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obsSvc = mozilla::services::GetObserverService();
+ if (NS_WARN_IF(!obsSvc)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsISupportsPRUint64> wrapper =
+ do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID);
+ if (NS_WARN_IF(!wrapper)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ wrapper->SetData(mUsage);
+
+ obsSvc->NotifyObservers(wrapper, "QuotaManager::StoragePressure", u"");
+
+ return NS_OK;
+}
+
+TimeStamp RecordQuotaInfoLoadTimeHelper::Start() {
+ AssertIsOnIOThread();
+
+ // XXX: If a OS sleep/wake occur after mStartTime is initialized but before
+ // gLastOSWake is set, then this time duration would still be recorded with
+ // key "Normal". We are assumming this is rather rare to happen.
+ mStartTime.init(TimeStamp::Now());
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this));
+
+ return *mStartTime;
+}
+
+TimeStamp RecordQuotaInfoLoadTimeHelper::End() {
+ AssertIsOnIOThread();
+
+ mEndTime.init(TimeStamp::Now());
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(this));
+
+ return *mEndTime;
+}
+
+NS_IMETHODIMP
+RecordQuotaInfoLoadTimeHelper::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mInitializedTime.isSome()) {
+ // Keys for QM_QUOTA_INFO_LOAD_TIME_V0:
+ // Normal: Normal conditions.
+ // WasSuspended: There was a OS sleep so that it was suspended.
+ // TimeStampErr1: The recorded start time is unexpectedly greater than the
+ // end time.
+ // TimeStampErr2: The initialized time for the recording class is unexpectly
+ // greater than the last OS wake time.
+ const auto key = [this, wasSuspended = gLastOSWake > *mInitializedTime]() {
+ if (wasSuspended) {
+ return "WasSuspended"_ns;
+ }
+
+ // XXX File a bug if we have data for this key.
+ // We found negative values in our query in STMO for
+ // ScalarID::QM_REPOSITORIES_INITIALIZATION_TIME. This shouldn't happen
+ // because the documentation for TimeStamp::Now() says it returns a
+ // monotonically increasing number.
+ if (*mStartTime > *mEndTime) {
+ return "TimeStampErr1"_ns;
+ }
+
+ if (*mInitializedTime > gLastOSWake) {
+ return "TimeStampErr2"_ns;
+ }
+
+ return "Normal"_ns;
+ }();
+
+ Telemetry::AccumulateTimeDelta(Telemetry::QM_QUOTA_INFO_LOAD_TIME_V0, key,
+ *mStartTime, *mEndTime);
+
+ return NS_OK;
+ }
+
+ gLastOSWake = TimeStamp::Now();
+ mInitializedTime.init(gLastOSWake);
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * Quota
+ ******************************************************************************/
+
+Quota::Quota()
+#ifdef DEBUG
+ : mActorDestroyed(false)
+#endif
+{
+}
+
+Quota::~Quota() { MOZ_ASSERT(mActorDestroyed); }
+
+bool Quota::VerifyRequestParams(const UsageRequestParams& aParams) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None);
+
+ switch (aParams.type()) {
+ case UsageRequestParams::TAllUsageParams:
+ break;
+
+ case UsageRequestParams::TOriginUsageParams: {
+ const OriginUsageParams& params = aParams.get_OriginUsageParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+bool Quota::VerifyRequestParams(const RequestParams& aParams) const {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != RequestParams::T__None);
+
+ switch (aParams.type()) {
+ case RequestParams::TStorageNameParams:
+ case RequestParams::TStorageInitializedParams:
+ case RequestParams::TTemporaryStorageInitializedParams:
+ case RequestParams::TInitParams:
+ case RequestParams::TInitTemporaryStorageParams:
+ break;
+
+ case RequestParams::TInitializePersistentOriginParams: {
+ const InitializePersistentOriginParams& params =
+ aParams.get_InitializePersistentOriginParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TInitializeTemporaryOriginParams: {
+ const InitializeTemporaryOriginParams& params =
+ aParams.get_InitializeTemporaryOriginParams();
+
+ if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TGetFullOriginMetadataParams: {
+ const GetFullOriginMetadataParams& params =
+ aParams.get_GetFullOriginMetadataParams();
+ if (NS_WARN_IF(!IsBestEffortPersistenceType(params.persistenceType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TClearOriginParams: {
+ const ClearResetOriginParams& params =
+ aParams.get_ClearOriginParams().commonParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ if (params.persistenceTypeIsExplicit()) {
+ if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+ }
+
+ if (params.clientTypeIsExplicit()) {
+ if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+ }
+
+ break;
+ }
+
+ case RequestParams::TResetOriginParams: {
+ const ClearResetOriginParams& params =
+ aParams.get_ResetOriginParams().commonParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ if (params.persistenceTypeIsExplicit()) {
+ if (NS_WARN_IF(!IsValidPersistenceType(params.persistenceType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+ }
+
+ if (params.clientTypeIsExplicit()) {
+ if (NS_WARN_IF(!Client::IsValidType(params.clientType()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+ }
+
+ break;
+ }
+
+ case RequestParams::TClearDataParams: {
+ if (BackgroundParent::IsOtherProcessActor(Manager())) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TClearAllParams:
+ case RequestParams::TResetAllParams:
+ case RequestParams::TListOriginsParams:
+ break;
+
+ case RequestParams::TPersistedParams: {
+ const PersistedParams& params = aParams.get_PersistedParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TPersistParams: {
+ const PersistParams& params = aParams.get_PersistParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ case RequestParams::TEstimateParams: {
+ const EstimateParams& params = aParams.get_EstimateParams();
+
+ if (NS_WARN_IF(
+ !QuotaManager::IsPrincipalInfoValid(params.principalInfo()))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return false;
+ }
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+
+ return true;
+}
+
+void Quota::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnBackgroundThread();
+#ifdef DEBUG
+ MOZ_ASSERT(!mActorDestroyed);
+ mActorDestroyed = true;
+#endif
+}
+
+PQuotaUsageRequestParent* Quota::AllocPQuotaUsageRequestParent(
+ const UsageRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None);
+
+ if (NS_WARN_IF(QuotaManager::IsShuttingDown())) {
+ return nullptr;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ auto actor = [&]() -> RefPtr<QuotaUsageRequestBase> {
+ switch (aParams.type()) {
+ case UsageRequestParams::TAllUsageParams:
+ return MakeRefPtr<GetUsageOp>(aParams);
+
+ case UsageRequestParams::TOriginUsageParams:
+ return MakeRefPtr<GetOriginUsageOp>(aParams);
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }();
+
+ MOZ_ASSERT(actor);
+
+ RegisterNormalOriginOp(*actor);
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+mozilla::ipc::IPCResult Quota::RecvPQuotaUsageRequestConstructor(
+ PQuotaUsageRequestParent* aActor, const UsageRequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None);
+ MOZ_ASSERT(!QuotaManager::IsShuttingDown());
+
+ auto* op = static_cast<QuotaUsageRequestBase*>(aActor);
+
+ op->Init(*this);
+
+ op->RunImmediately();
+ return IPC_OK();
+}
+
+bool Quota::DeallocPQuotaUsageRequestParent(PQuotaUsageRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<QuotaUsageRequestBase> actor =
+ dont_AddRef(static_cast<QuotaUsageRequestBase*>(aActor));
+ return true;
+}
+
+PQuotaRequestParent* Quota::AllocPQuotaRequestParent(
+ const RequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aParams.type() != RequestParams::T__None);
+
+ if (NS_WARN_IF(QuotaManager::IsShuttingDown())) {
+ return nullptr;
+ }
+
+#ifdef DEBUG
+ // Always verify parameters in DEBUG builds!
+ bool trustParams = false;
+#else
+ bool trustParams = !BackgroundParent::IsOtherProcessActor(Manager());
+#endif
+
+ if (!trustParams && NS_WARN_IF(!VerifyRequestParams(aParams))) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return nullptr;
+ }
+
+ auto actor = [&]() -> RefPtr<QuotaRequestBase> {
+ switch (aParams.type()) {
+ case RequestParams::TStorageNameParams:
+ return MakeRefPtr<StorageNameOp>();
+
+ case RequestParams::TStorageInitializedParams:
+ return MakeRefPtr<StorageInitializedOp>();
+
+ case RequestParams::TTemporaryStorageInitializedParams:
+ return MakeRefPtr<TemporaryStorageInitializedOp>();
+
+ case RequestParams::TInitParams:
+ return MakeRefPtr<InitOp>();
+
+ case RequestParams::TInitTemporaryStorageParams:
+ return MakeRefPtr<InitTemporaryStorageOp>();
+
+ case RequestParams::TInitializePersistentOriginParams:
+ return MakeRefPtr<InitializePersistentOriginOp>(aParams);
+
+ case RequestParams::TInitializeTemporaryOriginParams:
+ return MakeRefPtr<InitializeTemporaryOriginOp>(aParams);
+
+ case RequestParams::TGetFullOriginMetadataParams:
+ return MakeRefPtr<GetFullOriginMetadataOp>(
+ aParams.get_GetFullOriginMetadataParams());
+
+ case RequestParams::TClearOriginParams:
+ return MakeRefPtr<ClearOriginOp>(aParams);
+
+ case RequestParams::TResetOriginParams:
+ return MakeRefPtr<ResetOriginOp>(aParams);
+
+ case RequestParams::TClearDataParams:
+ return MakeRefPtr<ClearDataOp>(aParams);
+
+ case RequestParams::TClearAllParams:
+ return MakeRefPtr<ResetOrClearOp>(/* aClear */ true);
+
+ case RequestParams::TResetAllParams:
+ return MakeRefPtr<ResetOrClearOp>(/* aClear */ false);
+
+ case RequestParams::TPersistedParams:
+ return MakeRefPtr<PersistedOp>(aParams);
+
+ case RequestParams::TPersistParams:
+ return MakeRefPtr<PersistOp>(aParams);
+
+ case RequestParams::TEstimateParams:
+ return MakeRefPtr<EstimateOp>(aParams.get_EstimateParams());
+
+ case RequestParams::TListOriginsParams:
+ return MakeRefPtr<ListOriginsOp>();
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+ }();
+
+ MOZ_ASSERT(actor);
+
+ RegisterNormalOriginOp(*actor);
+
+ // Transfer ownership to IPDL.
+ return actor.forget().take();
+}
+
+mozilla::ipc::IPCResult Quota::RecvPQuotaRequestConstructor(
+ PQuotaRequestParent* aActor, const RequestParams& aParams) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+ MOZ_ASSERT(aParams.type() != RequestParams::T__None);
+ MOZ_ASSERT(!QuotaManager::IsShuttingDown());
+
+ auto* op = static_cast<QuotaRequestBase*>(aActor);
+
+ op->Init(*this);
+
+ op->RunImmediately();
+ return IPC_OK();
+}
+
+bool Quota::DeallocPQuotaRequestParent(PQuotaRequestParent* aActor) {
+ AssertIsOnBackgroundThread();
+ MOZ_ASSERT(aActor);
+
+ // Transfer ownership back from IPDL.
+ RefPtr<QuotaRequestBase> actor =
+ dont_AddRef(static_cast<QuotaRequestBase*>(aActor));
+ return true;
+}
+
+mozilla::ipc::IPCResult Quota::RecvStartIdleMaintenance() {
+ AssertIsOnBackgroundThread();
+
+ PBackgroundParent* actor = Manager();
+ MOZ_ASSERT(actor);
+
+ if (BackgroundParent::IsOtherProcessActor(actor)) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return IPC_FAIL(this, "Wrong actor");
+ }
+
+ if (QuotaManager::IsShuttingDown()) {
+ return IPC_OK();
+ }
+
+ QM_TRY(QuotaManager::EnsureCreated(), IPC_OK());
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->StartIdleMaintenance();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Quota::RecvStopIdleMaintenance() {
+ AssertIsOnBackgroundThread();
+
+ PBackgroundParent* actor = Manager();
+ MOZ_ASSERT(actor);
+
+ if (BackgroundParent::IsOtherProcessActor(actor)) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return IPC_FAIL(this, "Wrong actor");
+ }
+
+ if (QuotaManager::IsShuttingDown()) {
+ return IPC_OK();
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ if (!quotaManager) {
+ return IPC_OK();
+ }
+
+ quotaManager->StopIdleMaintenance();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult Quota::RecvAbortOperationsForProcess(
+ const ContentParentId& aContentParentId) {
+ AssertIsOnBackgroundThread();
+
+ PBackgroundParent* actor = Manager();
+ MOZ_ASSERT(actor);
+
+ if (BackgroundParent::IsOtherProcessActor(actor)) {
+ MOZ_CRASH_UNLESS_FUZZING();
+ return IPC_FAIL(this, "Wrong actor");
+ }
+
+ if (QuotaManager::IsShuttingDown()) {
+ return IPC_OK();
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ if (!quotaManager) {
+ return IPC_OK();
+ }
+
+ quotaManager->AbortOperationsForProcess(aContentParentId);
+
+ return IPC_OK();
+}
+
+void QuotaUsageRequestBase::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = true;
+}
+
+Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOrigin(
+ QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& directory,
+ aQuotaManager.GetDirectoryForOrigin(aPersistenceType,
+ aOriginMetadata.mOrigin));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+
+ if (!exists || mCanceled) {
+ return UsageInfo();
+ }
+
+ // If the directory exists then enumerate all the files inside, adding up
+ // the sizes to get the final usage statistic.
+ bool initialized;
+
+ if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
+ initialized = aQuotaManager.IsOriginInitialized(aOriginMetadata.mOrigin);
+ } else {
+ initialized = aQuotaManager.IsTemporaryStorageInitialized();
+ }
+
+ return GetUsageForOriginEntries(aQuotaManager, aPersistenceType,
+ aOriginMetadata, *directory, initialized);
+}
+
+Result<UsageInfo, nsresult> QuotaUsageRequestBase::GetUsageForOriginEntries(
+ QuotaManager& aQuotaManager, PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata, nsIFile& aDirectory,
+ const bool aInitialized) {
+ AssertIsOnIOThread();
+
+ QM_TRY_RETURN((ReduceEachFileAtomicCancelable(
+ aDirectory, mCanceled, UsageInfo{},
+ [&](UsageInfo oldUsageInfo, const nsCOMPtr<nsIFile>& file)
+ -> mozilla::Result<UsageInfo, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName));
+
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ Client::Type clientType;
+ const bool ok =
+ Client::TypeFromText(leafName, clientType, fallible);
+ if (!ok) {
+ // Unknown directories during getting usage for an origin (even
+ // for an uninitialized origin) are now allowed. Just warn if we
+ // find them.
+ UNKNOWN_FILE_WARNING(leafName);
+ break;
+ }
+
+ Client* const client = aQuotaManager.GetClient(clientType);
+ MOZ_ASSERT(client);
+
+ QM_TRY_INSPECT(
+ const auto& usageInfo,
+ aInitialized ? client->GetUsageForOrigin(
+ aPersistenceType, aOriginMetadata, mCanceled)
+ : client->InitOrigin(aPersistenceType,
+ aOriginMetadata, mCanceled));
+ return oldUsageInfo + usageInfo;
+ }
+
+ case nsIFileKind::ExistsAsFile:
+ // We are maintaining existing behavior for unknown files here (just
+ // continuing).
+ // This can possibly be used by developers to add temporary backups
+ // into origin directories without losing get usage functionality.
+ if (IsTempMetadata(leafName)) {
+ if (!aInitialized) {
+ QM_TRY(MOZ_TO_RESULT(file->Remove(/* recursive */ false)));
+ }
+
+ break;
+ }
+
+ if (IsOriginMetadata(leafName) || IsOSMetadata(leafName) ||
+ IsDotFile(leafName)) {
+ break;
+ }
+
+ // Unknown files during getting usage for an origin (even for an
+ // uninitialized origin) are now allowed. Just warn if we find them.
+ UNKNOWN_FILE_WARNING(leafName);
+ break;
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return oldUsageInfo;
+ })));
+}
+
+void QuotaUsageRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+
+ if (IsActorDestroyed()) {
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = NS_ERROR_FAILURE;
+ }
+ } else {
+ if (mCanceled) {
+ mResultCode = NS_ERROR_FAILURE;
+ }
+
+ UsageRequestResponse response;
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ GetResponse(response);
+ } else {
+ response = mResultCode;
+ }
+
+ Unused << PQuotaUsageRequestParent::Send__delete__(this, response);
+ }
+}
+
+void QuotaUsageRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteActorDestroyed();
+}
+
+mozilla::ipc::IPCResult QuotaUsageRequestBase::RecvCancel() {
+ AssertIsOnOwningThread();
+
+ if (mCanceled.exchange(true)) {
+ NS_WARNING("Canceled more than once?!");
+ return IPC_FAIL(this, "Request canceled more than once");
+ }
+
+ return IPC_OK();
+}
+
+nsresult TraverseRepositoryHelper::TraverseRepository(
+ QuotaManager& aQuotaManager, PersistenceType aPersistenceType) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(
+ const auto& directory,
+ QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+
+ if (!exists) {
+ return NS_OK;
+ }
+
+ QM_TRY(CollectEachFileAtomicCancelable(
+ *directory, GetIsCanceledFlag(),
+ [this, aPersistenceType, &aQuotaManager,
+ persistent = aPersistenceType == PERSISTENCE_TYPE_PERSISTENT](
+ const nsCOMPtr<nsIFile>& originDir) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*originDir));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory:
+ QM_TRY(MOZ_TO_RESULT(ProcessOrigin(aQuotaManager, *originDir,
+ persistent, aPersistenceType)));
+ break;
+
+ case nsIFileKind::ExistsAsFile: {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoString, originDir, GetLeafName));
+
+ // Unknown files during getting usages are allowed. Just warn if we
+ // find them.
+ if (!IsOSMetadata(leafName)) {
+ UNKNOWN_FILE_WARNING(leafName);
+ }
+
+ break;
+ }
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return Ok{};
+ }));
+
+ return NS_OK;
+}
+
+GetUsageOp::GetUsageOp(const UsageRequestParams& aParams)
+ : QuotaUsageRequestBase("dom::quota::GetUsageOp"),
+ mGetAll(aParams.get_AllUsageParams().getAll()) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aParams.type() == UsageRequestParams::TAllUsageParams);
+}
+
+void GetUsageOp::ProcessOriginInternal(QuotaManager* aQuotaManager,
+ const PersistenceType aPersistenceType,
+ const nsACString& aOrigin,
+ const int64_t aTimestamp,
+ const bool aPersisted,
+ const uint64_t aUsage) {
+ if (!mGetAll && aQuotaManager->IsOriginInternal(aOrigin)) {
+ return;
+ }
+
+ // We can't store pointers to OriginUsage objects in the hashtable
+ // since AppendElement() reallocates its internal array buffer as number
+ // of elements grows.
+ const auto& originUsage =
+ mOriginUsagesIndex.WithEntryHandle(aOrigin, [&](auto&& entry) {
+ if (entry) {
+ return WrapNotNullUnchecked(&mOriginUsages[entry.Data()]);
+ }
+
+ entry.Insert(mOriginUsages.Length());
+
+ return mOriginUsages.EmplaceBack(nsCString{aOrigin}, false, 0, 0);
+ });
+
+ if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) {
+ originUsage->persisted() = aPersisted;
+ }
+
+ originUsage->usage() = originUsage->usage() + aUsage;
+
+ originUsage->lastAccessed() =
+ std::max<int64_t>(originUsage->lastAccessed(), aTimestamp);
+}
+
+const Atomic<bool>& GetUsageOp::GetIsCanceledFlag() {
+ AssertIsOnIOThread();
+
+ return mCanceled;
+}
+
+// XXX Remove aPersistent
+// XXX Remove aPersistenceType once GetUsageForOrigin uses the persistence
+// type from OriginMetadata
+nsresult GetUsageOp::ProcessOrigin(QuotaManager& aQuotaManager,
+ nsIFile& aOriginDir, const bool aPersistent,
+ const PersistenceType aPersistenceType) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& metadata,
+ aQuotaManager.LoadFullOriginMetadataWithRestore(&aOriginDir));
+
+ QM_TRY_INSPECT(const auto& usageInfo,
+ GetUsageForOrigin(aQuotaManager, aPersistenceType, metadata));
+
+ ProcessOriginInternal(&aQuotaManager, aPersistenceType, metadata.mOrigin,
+ metadata.mLastAccessTime, metadata.mPersisted,
+ usageInfo.TotalUsage().valueOr(0));
+
+ return NS_OK;
+}
+
+nsresult GetUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+
+ AUTO_PROFILER_LABEL("GetUsageOp::DoDirectoryWork", OTHER);
+
+ nsresult rv;
+
+ for (const PersistenceType type : kAllPersistenceTypes) {
+ rv = TraverseRepository(aQuotaManager, type);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ // TraverseRepository above only consulted the filesystem. We also need to
+ // consider origins which may have pending quota usage, such as buffered
+ // LocalStorage writes for an origin which didn't previously have any
+ // LocalStorage data.
+
+ aQuotaManager.CollectPendingOriginsForListing(
+ [this, &aQuotaManager](const auto& originInfo) {
+ ProcessOriginInternal(
+ &aQuotaManager, originInfo->GetGroupInfo()->GetPersistenceType(),
+ originInfo->Origin(), originInfo->LockedAccessTime(),
+ originInfo->LockedPersisted(), originInfo->LockedUsage());
+ });
+
+ return NS_OK;
+}
+
+void GetUsageOp::GetResponse(UsageRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = AllUsageResponse();
+
+ aResponse.get_AllUsageResponse().originUsages() = std::move(mOriginUsages);
+}
+
+GetOriginUsageOp::GetOriginUsageOp(const UsageRequestParams& aParams)
+ : QuotaUsageRequestBase("dom::quota::GetOriginUsageOp"),
+ mUsage(0),
+ mFileUsage(0) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aParams.type() == UsageRequestParams::TOriginUsageParams);
+
+ const OriginUsageParams& params = aParams.get_OriginUsageParams();
+
+ PrincipalMetadata principalMetadata =
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(params.principalInfo());
+
+ mSuffix = std::move(principalMetadata.mSuffix);
+ mGroup = std::move(principalMetadata.mGroup);
+ mOriginScope.SetFromOrigin(principalMetadata.mOrigin);
+
+ mFromMemory = params.fromMemory();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = true;
+}
+
+RefPtr<DirectoryLock> GetOriginUsageOp::CreateDirectoryLock() {
+ if (mFromMemory) {
+ return nullptr;
+ }
+
+ return QuotaUsageRequestBase::CreateDirectoryLock();
+}
+
+nsresult GetOriginUsageOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+ MOZ_ASSERT(mUsage == 0);
+ MOZ_ASSERT(mFileUsage == 0);
+
+ AUTO_PROFILER_LABEL("GetOriginUsageOp::DoDirectoryWork", OTHER);
+
+ if (mFromMemory) {
+ const PrincipalMetadata principalMetadata = {
+ mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}};
+
+ // Ensure temporary storage is initialized. If temporary storage hasn't been
+ // initialized yet, the method will initialize it by traversing the
+ // repositories for temporary and default storage (including our origin).
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized()));
+
+ // Get cached usage (the method doesn't have to stat any files). File usage
+ // is not tracked in memory separately, so just add to the total usage.
+ mUsage = aQuotaManager.GetOriginUsage(principalMetadata);
+
+ return NS_OK;
+ }
+
+ UsageInfo usageInfo;
+
+ // Add all the persistent/temporary/default storage files we care about.
+ for (const PersistenceType type : kAllPersistenceTypes) {
+ const OriginMetadata originMetadata = {
+ mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()}, type};
+
+ auto usageInfoOrErr =
+ GetUsageForOrigin(aQuotaManager, type, originMetadata);
+ if (NS_WARN_IF(usageInfoOrErr.isErr())) {
+ return usageInfoOrErr.unwrapErr();
+ }
+
+ usageInfo += usageInfoOrErr.unwrap();
+ }
+
+ mUsage = usageInfo.TotalUsage().valueOr(0);
+ mFileUsage = usageInfo.FileUsage().valueOr(0);
+
+ return NS_OK;
+}
+
+void GetOriginUsageOp::GetResponse(UsageRequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ OriginUsageResponse usageResponse;
+
+ usageResponse.usage() = mUsage;
+ usageResponse.fileUsage() = mFileUsage;
+
+ aResponse = usageResponse;
+}
+
+void QuotaRequestBase::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = true;
+}
+
+void QuotaRequestBase::SendResults() {
+ AssertIsOnOwningThread();
+
+ if (IsActorDestroyed()) {
+ if (NS_SUCCEEDED(mResultCode)) {
+ mResultCode = NS_ERROR_FAILURE;
+ }
+ } else {
+ RequestResponse response;
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ GetResponse(response);
+ } else {
+ response = mResultCode;
+ }
+
+ Unused << PQuotaRequestParent::Send__delete__(this, response);
+ }
+}
+
+void QuotaRequestBase::ActorDestroy(ActorDestroyReason aWhy) {
+ AssertIsOnOwningThread();
+
+ NoteActorDestroyed();
+}
+
+StorageNameOp::StorageNameOp()
+ : QuotaRequestBase("dom::quota::StorageNameOp", /* aExclusive */ false) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+}
+
+void StorageNameOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+RefPtr<DirectoryLock> StorageNameOp::CreateDirectoryLock() { return nullptr; }
+
+nsresult StorageNameOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("StorageNameOp::DoDirectoryWork", OTHER);
+
+ mName = aQuotaManager.GetStorageName();
+
+ return NS_OK;
+}
+
+void StorageNameOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ StorageNameResponse storageNameResponse;
+
+ storageNameResponse.name() = mName;
+
+ aResponse = storageNameResponse;
+}
+
+InitializedRequestBase::InitializedRequestBase(const char* aRunnableName)
+ : QuotaRequestBase(aRunnableName, /* aExclusive */ false),
+ mInitialized(false) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+}
+
+void InitializedRequestBase::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+RefPtr<DirectoryLock> InitializedRequestBase::CreateDirectoryLock() {
+ return nullptr;
+}
+
+nsresult StorageInitializedOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("StorageInitializedOp::DoDirectoryWork", OTHER);
+
+ mInitialized = aQuotaManager.IsStorageInitialized();
+
+ return NS_OK;
+}
+
+void StorageInitializedOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ StorageInitializedResponse storageInitializedResponse;
+
+ storageInitializedResponse.initialized() = mInitialized;
+
+ aResponse = storageInitializedResponse;
+}
+
+nsresult TemporaryStorageInitializedOp::DoDirectoryWork(
+ QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("TemporaryStorageInitializedOp::DoDirectoryWork", OTHER);
+
+ mInitialized = aQuotaManager.IsTemporaryStorageInitialized();
+
+ return NS_OK;
+}
+
+void TemporaryStorageInitializedOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ TemporaryStorageInitializedResponse temporaryStorageInitializedResponse;
+
+ temporaryStorageInitializedResponse.initialized() = mInitialized;
+
+ aResponse = temporaryStorageInitializedResponse;
+}
+
+InitOp::InitOp()
+ : QuotaRequestBase("dom::quota::InitOp", /* aExclusive */ false) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+}
+
+void InitOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+nsresult InitOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("InitOp::DoDirectoryWork", OTHER);
+
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureStorageIsInitialized()));
+
+ return NS_OK;
+}
+
+void InitOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = InitResponse();
+}
+
+InitTemporaryStorageOp::InitTemporaryStorageOp()
+ : QuotaRequestBase("dom::quota::InitTemporaryStorageOp",
+ /* aExclusive */ false) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+}
+
+void InitTemporaryStorageOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+nsresult InitTemporaryStorageOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("InitTemporaryStorageOp::DoDirectoryWork", OTHER);
+
+ QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE);
+
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized()));
+
+ return NS_OK;
+}
+
+void InitTemporaryStorageOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = InitTemporaryStorageResponse();
+}
+
+InitializeOriginRequestBase::InitializeOriginRequestBase(
+ const char* aRunnableName, const PersistenceType aPersistenceType,
+ const PrincipalInfo& aPrincipalInfo)
+ : QuotaRequestBase(aRunnableName,
+ /* aExclusive */ false),
+ mCreated(false) {
+ AssertIsOnOwningThread();
+
+ auto principalMetadata =
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(aPrincipalInfo);
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+
+ // Overwrite NormalOriginOperationBase default values.
+ mPersistenceType.SetValue(aPersistenceType);
+ mOriginScope.SetFromOrigin(principalMetadata.mOrigin);
+
+ // Overwrite InitializeOriginRequestBase default values.
+ mSuffix = std::move(principalMetadata.mSuffix);
+ mGroup = std::move(principalMetadata.mGroup);
+}
+
+void InitializeOriginRequestBase::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+}
+
+InitializePersistentOriginOp::InitializePersistentOriginOp(
+ const RequestParams& aParams)
+ : InitializeOriginRequestBase(
+ "dom::quota::InitializePersistentOriginOp",
+ PERSISTENCE_TYPE_PERSISTENT,
+ aParams.get_InitializePersistentOriginParams().principalInfo()) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aParams.type() ==
+ RequestParams::TInitializePersistentOriginParams);
+}
+
+nsresult InitializePersistentOriginOp::DoDirectoryWork(
+ QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!mPersistenceType.IsNull());
+
+ AUTO_PROFILER_LABEL("InitializePersistentOriginOp::DoDirectoryWork", OTHER);
+
+ QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE);
+
+ QM_TRY_UNWRAP(mCreated,
+ (aQuotaManager
+ .EnsurePersistentOriginIsInitialized(OriginMetadata{
+ mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()},
+ PERSISTENCE_TYPE_PERSISTENT})
+ .map([](const auto& res) { return res.second; })));
+
+ return NS_OK;
+}
+
+void InitializePersistentOriginOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = InitializePersistentOriginResponse(mCreated);
+}
+
+InitializeTemporaryOriginOp::InitializeTemporaryOriginOp(
+ const RequestParams& aParams)
+ : InitializeOriginRequestBase(
+ "dom::quota::InitializeTemporaryOriginOp",
+ aParams.get_InitializeTemporaryOriginParams().persistenceType(),
+ aParams.get_InitializeTemporaryOriginParams().principalInfo()) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aParams.type() == RequestParams::TInitializeTemporaryOriginParams);
+}
+
+nsresult InitializeTemporaryOriginOp::DoDirectoryWork(
+ QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!mPersistenceType.IsNull());
+
+ AUTO_PROFILER_LABEL("InitializeTemporaryOriginOp::DoDirectoryWork", OTHER);
+
+ QM_TRY(OkIf(aQuotaManager.IsStorageInitialized()), NS_ERROR_FAILURE);
+
+ QM_TRY(OkIf(aQuotaManager.IsTemporaryStorageInitialized()), NS_ERROR_FAILURE);
+
+ QM_TRY_UNWRAP(mCreated,
+ (aQuotaManager
+ .EnsureTemporaryOriginIsInitialized(
+ mPersistenceType.Value(),
+ OriginMetadata{mSuffix, mGroup,
+ nsCString{mOriginScope.GetOrigin()},
+ mPersistenceType.Value()})
+ .map([](const auto& res) { return res.second; })));
+
+ return NS_OK;
+}
+
+void InitializeTemporaryOriginOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = InitializeTemporaryOriginResponse(mCreated);
+}
+
+GetFullOriginMetadataOp::GetFullOriginMetadataOp(
+ const GetFullOriginMetadataParams& aParams)
+ : QuotaRequestBase("dom::quota::GetFullOriginMetadataOp",
+ /* aExclusive */ false),
+ mOriginMetadata(QuotaManager::GetInfoFromValidatedPrincipalInfo(
+ aParams.principalInfo()),
+ aParams.persistenceType()) {
+ AssertIsOnOwningThread();
+}
+
+RefPtr<DirectoryLock> GetFullOriginMetadataOp::CreateDirectoryLock() {
+ return nullptr;
+}
+
+nsresult GetFullOriginMetadataOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("GetFullOriginMetadataOp::DoDirectoryWork", OTHER);
+
+ // Ensure temporary storage is initialized. If temporary storage hasn't
+ // been initialized yet, the method will initialize it by traversing the
+ // repositories for temporary and default storage (including our origin).
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized()));
+
+ // Get metadata cached in memory (the method doesn't have to stat any
+ // files).
+ mMaybeFullOriginMetadata =
+ aQuotaManager.GetFullOriginMetadata(mOriginMetadata);
+
+ return NS_OK;
+}
+
+void GetFullOriginMetadataOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = GetFullOriginMetadataResponse();
+ aResponse.get_GetFullOriginMetadataResponse().maybeFullOriginMetadata() =
+ std::move(mMaybeFullOriginMetadata);
+}
+
+ResetOrClearOp::ResetOrClearOp(bool aClear)
+ : QuotaRequestBase("dom::quota::ResetOrClearOp", /* aExclusive */ true),
+ mClear(aClear) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+}
+
+void ResetOrClearOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+void ResetOrClearOp::DeleteFiles(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ nsresult rv = aQuotaManager.AboutToClearOrigins(Nullable<PersistenceType>(),
+ OriginScope::FromNull(),
+ Nullable<Client::Type>());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ auto directoryOrErr = QM_NewLocalFile(aQuotaManager.GetStoragePath());
+ if (NS_WARN_IF(directoryOrErr.isErr())) {
+ return;
+ }
+
+ nsCOMPtr<nsIFile> directory = directoryOrErr.unwrap();
+
+ rv = directory->Remove(true);
+ if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ // This should never fail if we've closed all storage connections
+ // correctly...
+ MOZ_ASSERT(false, "Failed to remove storage directory!");
+ }
+}
+
+void ResetOrClearOp::DeleteStorageFile(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& storageFile,
+ QM_NewLocalFile(aQuotaManager.GetBasePath()), QM_VOID);
+
+ QM_TRY(MOZ_TO_RESULT(storageFile->Append(aQuotaManager.GetStorageName() +
+ kSQLiteSuffix)),
+ QM_VOID);
+
+ const nsresult rv = storageFile->Remove(true);
+ if (rv != NS_ERROR_FILE_NOT_FOUND && NS_FAILED(rv)) {
+ // This should never fail if we've closed the storage connection
+ // correctly...
+ MOZ_ASSERT(false, "Failed to remove storage file!");
+ }
+}
+
+nsresult ResetOrClearOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("ResetOrClearOp::DoDirectoryWork", OTHER);
+
+ if (mClear) {
+ DeleteFiles(aQuotaManager);
+
+ aQuotaManager.RemoveQuota();
+ }
+
+ aQuotaManager.ShutdownStorageInternal();
+
+ if (mClear) {
+ DeleteStorageFile(aQuotaManager);
+ }
+
+ return NS_OK;
+}
+
+void ResetOrClearOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+ if (mClear) {
+ aResponse = ClearAllResponse();
+ } else {
+ aResponse = ResetAllResponse();
+ }
+}
+
+static Result<nsCOMPtr<nsIFile>, QMResult> OpenToBeRemovedDirectory(
+ const nsAString& aStoragePath) {
+ QM_TRY_INSPECT(const auto& dir,
+ QM_TO_RESULT_TRANSFORM(QM_NewLocalFile(aStoragePath)));
+ QM_TRY(QM_TO_RESULT(dir->Append(u"to-be-removed"_ns)));
+
+ nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (NS_SUCCEEDED(rv) || rv == NS_ERROR_FILE_ALREADY_EXISTS) {
+ return dir;
+ }
+ return Err(QMResult(rv));
+}
+
+static Result<Ok, QMResult> RemoveOrMoveToDir(nsIFile& aFile,
+ nsIFile* aMoveTargetDir) {
+ if (!aMoveTargetDir) {
+ QM_TRY(QM_TO_RESULT(aFile.Remove(true)));
+ return Ok();
+ }
+
+ nsIDToCString uuid(nsID::GenerateUUID());
+ NS_ConvertUTF8toUTF16 subDirName(uuid.get(), NSID_LENGTH - 1);
+ QM_TRY(QM_TO_RESULT(aFile.MoveTo(aMoveTargetDir, subDirName)));
+ return Ok();
+}
+
+void ClearRequestBase::DeleteFiles(QuotaManager& aQuotaManager,
+ PersistenceType aPersistenceType) {
+ AssertIsOnIOThread();
+
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.AboutToClearOrigins(
+ Nullable<PersistenceType>(aPersistenceType), mOriginScope,
+ mClientType)),
+ QM_VOID);
+
+ QM_TRY_INSPECT(
+ const auto& directory,
+ QM_NewLocalFile(aQuotaManager.GetStoragePath(aPersistenceType)), QM_VOID);
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists), QM_VOID);
+
+ if (!exists) {
+ return;
+ }
+
+ nsTArray<nsCOMPtr<nsIFile>> directoriesForRemovalRetry;
+
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStep(
+ "ClearRequestBase: Starting deleting files"_ns);
+ nsCOMPtr<nsIFile> toBeRemovedDir;
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownTeardown)) {
+ QM_WARNONLY_TRY_UNWRAP(
+ auto result, OpenToBeRemovedDirectory(aQuotaManager.GetStoragePath()));
+ toBeRemovedDir = result.valueOr(nullptr);
+ }
+ QM_TRY(
+ CollectEachFile(
+ *directory,
+ [originScope =
+ [this] {
+ OriginScope originScope = mOriginScope.Clone();
+ if (originScope.IsOrigin()) {
+ originScope.SetOrigin(
+ MakeSanitizedOriginCString(originScope.GetOrigin()));
+ } else if (originScope.IsPrefix()) {
+ originScope.SetOriginNoSuffix(MakeSanitizedOriginCString(
+ originScope.GetOriginNoSuffix()));
+ }
+ return originScope;
+ }(),
+ aPersistenceType, &aQuotaManager, &directoriesForRemovalRetry,
+ &toBeRemovedDir,
+ this](nsCOMPtr<nsIFile>&& file) -> mozilla::Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file,
+ GetLeafName));
+
+ QM_TRY_INSPECT(const auto& dirEntryKind, GetDirEntryKind(*file));
+
+ switch (dirEntryKind) {
+ case nsIFileKind::ExistsAsDirectory: {
+ // Skip the origin directory if it doesn't match the pattern.
+ if (!originScope.Matches(OriginScope::FromOrigin(
+ NS_ConvertUTF16toUTF8(leafName)))) {
+ break;
+ }
+
+ QM_TRY_INSPECT(
+ const auto& metadata,
+ aQuotaManager.LoadFullOriginMetadataWithRestore(file));
+
+ MOZ_ASSERT(metadata.mPersistenceType == aPersistenceType);
+
+ if (!mClientType.IsNull()) {
+ nsAutoString clientDirectoryName;
+ QM_TRY(
+ OkIf(Client::TypeToText(mClientType.Value(),
+ clientDirectoryName, fallible)),
+ Err(NS_ERROR_FAILURE));
+
+ QM_TRY(MOZ_TO_RESULT(file->Append(clientDirectoryName)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(file, Exists));
+
+ if (!exists) {
+ break;
+ }
+ }
+
+ // We can't guarantee that this will always succeed on
+ // Windows...
+ QM_WARNONLY_TRY(
+ RemoveOrMoveToDir(*file, toBeRemovedDir), [&](const auto&) {
+ directoriesForRemovalRetry.AppendElement(std::move(file));
+ });
+
+ const bool initialized =
+ aPersistenceType == PERSISTENCE_TYPE_PERSISTENT
+ ? aQuotaManager.IsOriginInitialized(metadata.mOrigin)
+ : aQuotaManager.IsTemporaryStorageInitialized();
+
+ // If it hasn't been initialized, we don't need to update the
+ // quota and notify the removing client.
+ if (!initialized) {
+ break;
+ }
+
+ if (aPersistenceType != PERSISTENCE_TYPE_PERSISTENT) {
+ if (mClientType.IsNull()) {
+ aQuotaManager.RemoveQuotaForOrigin(aPersistenceType,
+ metadata);
+ } else {
+ aQuotaManager.ResetUsageForClient(
+ ClientMetadata{metadata, mClientType.Value()});
+ }
+ }
+
+ aQuotaManager.OriginClearCompleted(
+ aPersistenceType, metadata.mOrigin, mClientType);
+
+ break;
+ }
+
+ case nsIFileKind::ExistsAsFile:
+ // Unknown files during clearing are allowed. Just warn if we
+ // find them.
+ if (!IsOSMetadata(leafName)) {
+ UNKNOWN_FILE_WARNING(leafName);
+ }
+
+ break;
+
+ case nsIFileKind::DoesNotExist:
+ // Ignore files that got removed externally while iterating.
+ break;
+ }
+
+ return Ok{};
+ }),
+ QM_VOID);
+
+ // Retry removing any directories that failed to be removed earlier now.
+ //
+ // XXX This will still block this operation. We might instead dispatch a
+ // runnable to our own thread for each retry round with a timer. We must
+ // ensure that the directory lock is upheld until we complete or give up
+ // though.
+ for (uint32_t index = 0; index < 10; index++) {
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() {
+ return nsPrintfCString(
+ "ClearRequestBase: Starting repeated directory removal #%d", index);
+ });
+
+ for (auto&& file : std::exchange(directoriesForRemovalRetry,
+ nsTArray<nsCOMPtr<nsIFile>>{})) {
+ QM_WARNONLY_TRY(
+ QM_TO_RESULT(file->Remove(true)),
+ ([&directoriesForRemovalRetry, &file](const auto&) {
+ directoriesForRemovalRetry.AppendElement(std::move(file));
+ }));
+ }
+
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() {
+ return nsPrintfCString(
+ "ClearRequestBase: Completed repeated directory removal #%d", index);
+ });
+
+ if (directoriesForRemovalRetry.IsEmpty()) {
+ break;
+ }
+
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() {
+ return nsPrintfCString("ClearRequestBase: Before sleep #%d", index);
+ });
+
+ PR_Sleep(PR_MillisecondsToInterval(200));
+
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStepWith([index]() {
+ return nsPrintfCString("ClearRequestBase: After sleep #%d", index);
+ });
+ }
+
+ QM_WARNONLY_TRY(OkIf(directoriesForRemovalRetry.IsEmpty()));
+
+ aQuotaManager.MaybeRecordQuotaManagerShutdownStep(
+ "ClearRequestBase: Completed deleting files"_ns);
+}
+
+nsresult ClearRequestBase::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+
+ AUTO_PROFILER_LABEL("ClearRequestBase::DoDirectoryWork", OTHER);
+
+ if (mPersistenceType.IsNull()) {
+ for (const PersistenceType type : kAllPersistenceTypes) {
+ DeleteFiles(aQuotaManager, type);
+ }
+ } else {
+ DeleteFiles(aQuotaManager, mPersistenceType.Value());
+ }
+
+ return NS_OK;
+}
+
+ClearOriginOp::ClearOriginOp(const RequestParams& aParams)
+ : ClearRequestBase("dom::quota::ClearOriginOp", /* aExclusive */ true),
+ mParams(aParams.get_ClearOriginParams().commonParams()),
+ mMatchAll(aParams.get_ClearOriginParams().matchAll()) {
+ MOZ_ASSERT(aParams.type() == RequestParams::TClearOriginParams);
+}
+
+void ClearOriginOp::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ QuotaRequestBase::Init(aQuota);
+
+ if (mParams.persistenceTypeIsExplicit()) {
+ mPersistenceType.SetValue(mParams.persistenceType());
+ }
+
+ // Figure out which origin we're dealing with.
+ const auto origin = QuotaManager::GetOriginFromValidatedPrincipalInfo(
+ mParams.principalInfo());
+
+ if (mMatchAll) {
+ mOriginScope.SetFromPrefix(origin);
+ } else {
+ mOriginScope.SetFromOrigin(origin);
+ }
+
+ if (mParams.clientTypeIsExplicit()) {
+ mClientType.SetValue(mParams.clientType());
+ }
+}
+
+void ClearOriginOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = ClearOriginResponse();
+}
+
+ClearDataOp::ClearDataOp(const RequestParams& aParams)
+ : ClearRequestBase("dom::quota::ClearDataOp", /* aExclusive */ true),
+ mParams(aParams) {
+ MOZ_ASSERT(aParams.type() == RequestParams::TClearDataParams);
+}
+
+void ClearDataOp::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ QuotaRequestBase::Init(aQuota);
+
+ mOriginScope.SetFromPattern(mParams.pattern());
+}
+
+void ClearDataOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = ClearDataResponse();
+}
+
+ResetOriginOp::ResetOriginOp(const RequestParams& aParams)
+ : QuotaRequestBase("dom::quota::ResetOriginOp", /* aExclusive */ true) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aParams.type() == RequestParams::TResetOriginParams);
+
+ const ClearResetOriginParams& params =
+ aParams.get_ResetOriginParams().commonParams();
+
+ const auto origin =
+ QuotaManager::GetOriginFromValidatedPrincipalInfo(params.principalInfo());
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = false;
+
+ // Overwrite NormalOriginOperationBase default values.
+ if (params.persistenceTypeIsExplicit()) {
+ mPersistenceType.SetValue(params.persistenceType());
+ }
+
+ mOriginScope.SetFromOrigin(origin);
+
+ if (params.clientTypeIsExplicit()) {
+ mClientType.SetValue(params.clientType());
+ }
+}
+
+void ResetOriginOp::Init(Quota& aQuota) { AssertIsOnOwningThread(); }
+
+nsresult ResetOriginOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+
+ AUTO_PROFILER_LABEL("ResetOriginOp::DoDirectoryWork", OTHER);
+
+ // All the work is handled by NormalOriginOperationBase parent class. In this
+ // particular case, we just needed to acquire an exclusive directory lock and
+ // that's it.
+
+ return NS_OK;
+}
+
+void ResetOriginOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = ResetOriginResponse();
+}
+
+PersistRequestBase::PersistRequestBase(const PrincipalInfo& aPrincipalInfo)
+ : QuotaRequestBase("dom::quota::PersistRequestBase",
+ /* aExclusive */ false),
+ mPrincipalInfo(aPrincipalInfo) {
+ AssertIsOnOwningThread();
+}
+
+void PersistRequestBase::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ QuotaRequestBase::Init(aQuota);
+
+ mPersistenceType.SetValue(PERSISTENCE_TYPE_DEFAULT);
+
+ // Figure out which origin we're dealing with.
+ PrincipalMetadata principalMetadata =
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(mPrincipalInfo);
+
+ mSuffix = std::move(principalMetadata.mSuffix);
+ mGroup = std::move(principalMetadata.mGroup);
+ mOriginScope.SetFromOrigin(principalMetadata.mOrigin);
+}
+
+PersistedOp::PersistedOp(const RequestParams& aParams)
+ : PersistRequestBase(aParams.get_PersistedParams().principalInfo()),
+ mPersisted(false) {
+ MOZ_ASSERT(aParams.type() == RequestParams::TPersistedParams);
+}
+
+nsresult PersistedOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+ MOZ_ASSERT(!mPersistenceType.IsNull());
+ MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT);
+ MOZ_ASSERT(mOriginScope.IsOrigin());
+
+ AUTO_PROFILER_LABEL("PersistedOp::DoDirectoryWork", OTHER);
+
+ Nullable<bool> persisted = aQuotaManager.OriginPersisted(
+ OriginMetadata{mSuffix, mGroup, nsCString{mOriginScope.GetOrigin()},
+ mPersistenceType.Value()});
+
+ if (!persisted.IsNull()) {
+ mPersisted = persisted.Value();
+ return NS_OK;
+ }
+
+ // If we get here, it means the origin hasn't been initialized yet.
+ // Try to get the persisted flag from directory metadata on disk.
+
+ QM_TRY_INSPECT(const auto& directory,
+ aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(),
+ mOriginScope.GetOrigin()));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(directory, Exists));
+
+ if (exists) {
+ // Get the metadata. We only use the persisted flag.
+ QM_TRY_INSPECT(const auto& metadata,
+ aQuotaManager.LoadFullOriginMetadataWithRestore(directory));
+
+ mPersisted = metadata.mPersisted;
+ } else {
+ // The directory has not been created yet.
+ mPersisted = false;
+ }
+
+ return NS_OK;
+}
+
+void PersistedOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ PersistedResponse persistedResponse;
+ persistedResponse.persisted() = mPersisted;
+
+ aResponse = persistedResponse;
+}
+
+PersistOp::PersistOp(const RequestParams& aParams)
+ : PersistRequestBase(aParams.get_PersistParams().principalInfo()) {
+ MOZ_ASSERT(aParams.type() == RequestParams::TPersistParams);
+}
+
+nsresult PersistOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+ MOZ_ASSERT(!mPersistenceType.IsNull());
+ MOZ_ASSERT(mPersistenceType.Value() == PERSISTENCE_TYPE_DEFAULT);
+ MOZ_ASSERT(mOriginScope.IsOrigin());
+
+ const OriginMetadata originMetadata = {mSuffix, mGroup,
+ nsCString{mOriginScope.GetOrigin()},
+ mPersistenceType.Value()};
+
+ AUTO_PROFILER_LABEL("PersistOp::DoDirectoryWork", OTHER);
+
+ // Update directory metadata on disk first. Then, create/update the originInfo
+ // if needed.
+ QM_TRY_INSPECT(const auto& directory,
+ aQuotaManager.GetDirectoryForOrigin(mPersistenceType.Value(),
+ originMetadata.mOrigin));
+
+ QM_TRY_INSPECT(const bool& created,
+ aQuotaManager.EnsureOriginDirectory(*directory));
+
+ if (created) {
+ int64_t timestamp;
+
+ // Origin directory has been successfully created.
+ // Create OriginInfo too if temporary storage was already initialized.
+ if (aQuotaManager.IsTemporaryStorageInitialized()) {
+ timestamp = aQuotaManager.NoteOriginDirectoryCreated(
+ originMetadata, /* aPersisted */ true);
+ } else {
+ timestamp = PR_Now();
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(*directory, timestamp,
+ /* aPersisted */ true,
+ originMetadata)));
+ } else {
+ // Get the metadata (restore the metadata file if necessary). We only use
+ // the persisted flag.
+ QM_TRY_INSPECT(const auto& metadata,
+ aQuotaManager.LoadFullOriginMetadataWithRestore(directory));
+
+ if (!metadata.mPersisted) {
+ QM_TRY_INSPECT(const auto& file,
+ CloneFileAndAppend(
+ *directory, nsLiteralString(METADATA_V2_FILE_NAME)));
+
+ QM_TRY_INSPECT(const auto& stream,
+ GetBinaryOutputStream(*file, FileFlag::Update));
+
+ MOZ_ASSERT(stream);
+
+ // Update origin access time while we are here.
+ QM_TRY(MOZ_TO_RESULT(stream->Write64(PR_Now())));
+
+ // Set the persisted flag to true.
+ QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(true)));
+ }
+
+ // Directory metadata has been successfully updated.
+ // Update OriginInfo too if temporary storage was already initialized.
+ if (aQuotaManager.IsTemporaryStorageInitialized()) {
+ aQuotaManager.PersistOrigin(originMetadata);
+ }
+ }
+
+ return NS_OK;
+}
+
+void PersistOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = PersistResponse();
+}
+
+EstimateOp::EstimateOp(const EstimateParams& aParams)
+ : QuotaRequestBase("dom::quota::EstimateOp", /* aExclusive */ false),
+ mOriginMetadata(QuotaManager::GetInfoFromValidatedPrincipalInfo(
+ aParams.principalInfo()),
+ PERSISTENCE_TYPE_DEFAULT) {
+ AssertIsOnOwningThread();
+
+ // Overwrite OriginOperationBase default values.
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = true;
+}
+
+RefPtr<DirectoryLock> EstimateOp::CreateDirectoryLock() { return nullptr; }
+
+nsresult EstimateOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+
+ AUTO_PROFILER_LABEL("EstimateOp::DoDirectoryWork", OTHER);
+
+ // Ensure temporary storage is initialized. If temporary storage hasn't been
+ // initialized yet, the method will initialize it by traversing the
+ // repositories for temporary and default storage (including origins belonging
+ // to our group).
+ QM_TRY(MOZ_TO_RESULT(aQuotaManager.EnsureTemporaryStorageIsInitialized()));
+
+ // Get cached usage (the method doesn't have to stat any files).
+ mUsageAndLimit = aQuotaManager.GetUsageAndLimitForEstimate(mOriginMetadata);
+
+ return NS_OK;
+}
+
+void EstimateOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ EstimateResponse estimateResponse;
+
+ estimateResponse.usage() = mUsageAndLimit.first;
+ estimateResponse.limit() = mUsageAndLimit.second;
+
+ aResponse = estimateResponse;
+}
+
+ListOriginsOp::ListOriginsOp()
+ : QuotaRequestBase("dom::quota::ListOriginsOp", /* aExclusive */ false),
+ TraverseRepositoryHelper() {
+ AssertIsOnOwningThread();
+}
+
+void ListOriginsOp::Init(Quota& aQuota) {
+ AssertIsOnOwningThread();
+
+ mNeedsQuotaManagerInit = true;
+ mNeedsStorageInit = true;
+}
+
+nsresult ListOriginsOp::DoDirectoryWork(QuotaManager& aQuotaManager) {
+ AssertIsOnIOThread();
+ aQuotaManager.AssertStorageIsInitialized();
+
+ AUTO_PROFILER_LABEL("ListOriginsOp::DoDirectoryWork", OTHER);
+
+ for (const PersistenceType type : kAllPersistenceTypes) {
+ QM_TRY(MOZ_TO_RESULT(TraverseRepository(aQuotaManager, type)));
+ }
+
+ // TraverseRepository above only consulted the file-system to get a list of
+ // known origins, but we also need to include origins that have pending quota
+ // usage.
+
+ aQuotaManager.CollectPendingOriginsForListing([this](const auto& originInfo) {
+ mOrigins.AppendElement(originInfo->Origin());
+ });
+
+ return NS_OK;
+}
+
+const Atomic<bool>& ListOriginsOp::GetIsCanceledFlag() {
+ AssertIsOnIOThread();
+
+ return mCanceled;
+}
+
+nsresult ListOriginsOp::ProcessOrigin(QuotaManager& aQuotaManager,
+ nsIFile& aOriginDir,
+ const bool aPersistent,
+ const PersistenceType aPersistenceType) {
+ AssertIsOnIOThread();
+
+ // XXX We only use metadata.mOriginMetadata.mOrigin...
+ QM_TRY_UNWRAP(auto metadata,
+ aQuotaManager.LoadFullOriginMetadataWithRestore(&aOriginDir));
+
+ if (aQuotaManager.IsOriginInternal(metadata.mOrigin)) {
+ return NS_OK;
+ }
+
+ mOrigins.AppendElement(std::move(metadata.mOrigin));
+
+ return NS_OK;
+}
+
+void ListOriginsOp::GetResponse(RequestResponse& aResponse) {
+ AssertIsOnOwningThread();
+
+ aResponse = ListOriginsResponse();
+ if (mOrigins.IsEmpty()) {
+ return;
+ }
+
+ nsTArray<nsCString>& origins = aResponse.get_ListOriginsResponse().origins();
+ mOrigins.SwapElements(origins);
+}
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+
+// static
+already_AddRefed<PrincipalVerifier> PrincipalVerifier::CreateAndDispatch(
+ nsTArray<PrincipalInfo>&& aPrincipalInfos) {
+ AssertIsOnIOThread();
+
+ RefPtr<PrincipalVerifier> verifier =
+ new PrincipalVerifier(std::move(aPrincipalInfos));
+
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(verifier));
+
+ return verifier.forget();
+}
+
+Result<Ok, nsCString> PrincipalVerifier::CheckPrincipalInfoValidity(
+ const PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ switch (aPrincipalInfo.type()) {
+ // A system principal is acceptable.
+ case PrincipalInfo::TSystemPrincipalInfo: {
+ return Ok{};
+ }
+
+ case PrincipalInfo::TContentPrincipalInfo: {
+ const ContentPrincipalInfo& info =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), info.spec());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err("NS_NewURI failed"_ns);
+ }
+
+ nsCOMPtr<nsIPrincipal> principal =
+ BasePrincipal::CreateContentPrincipal(uri, info.attrs());
+ if (NS_WARN_IF(!principal)) {
+ return Err("CreateContentPrincipal failed"_ns);
+ }
+
+ nsCString originNoSuffix;
+ rv = principal->GetOriginNoSuffix(originNoSuffix);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err("GetOriginNoSuffix failed"_ns);
+ }
+
+ if (NS_WARN_IF(originNoSuffix != info.originNoSuffix())) {
+ static const char messageTemplate[] =
+ "originNoSuffix (%s) doesn't match passed one (%s)!";
+
+ QM_WARNING(messageTemplate, originNoSuffix.get(),
+ info.originNoSuffix().get());
+
+ return Err(nsPrintfCString(
+ messageTemplate, AnonymizedOriginString(originNoSuffix).get(),
+ AnonymizedOriginString(info.originNoSuffix()).get()));
+ }
+
+ nsCString baseDomain;
+ rv = principal->GetBaseDomain(baseDomain);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err("GetBaseDomain failed"_ns);
+ }
+
+ if (NS_WARN_IF(baseDomain != info.baseDomain())) {
+ static const char messageTemplate[] =
+ "baseDomain (%s) doesn't match passed one (%s)!";
+
+ QM_WARNING(messageTemplate, baseDomain.get(), info.baseDomain().get());
+
+ return Err(nsPrintfCString(messageTemplate,
+ AnonymizedCString(baseDomain).get(),
+ AnonymizedCString(info.baseDomain()).get()));
+ }
+
+ return Ok{};
+ }
+
+ default: {
+ break;
+ }
+ }
+
+ return Err("Null and expanded principals are not acceptable"_ns);
+}
+
+NS_IMETHODIMP
+PrincipalVerifier::Run() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString allDetails;
+ for (auto& principalInfo : mPrincipalInfos) {
+ const auto res = CheckPrincipalInfoValidity(principalInfo);
+ if (res.isErr()) {
+ if (!allDetails.IsEmpty()) {
+ allDetails.AppendLiteral(", ");
+ }
+
+ allDetails.Append(res.inspectErr());
+ }
+ }
+
+ if (!allDetails.IsEmpty()) {
+ allDetails.Insert("Invalid principal infos found: ", 0);
+
+ // In case of invalid principal infos, this will produce a crash reason such
+ // as:
+ // Invalid principal infos found: originNoSuffix (https://aaa.aaaaaaa.aaa)
+ // doesn't match passed one (about:aaaa)!
+ //
+ // In case of errors while validating a principal, it will contain a
+ // different message describing that error, which does not contain any
+ // details of the actual principal info at the moment.
+ //
+ // This string will be leaked.
+ MOZ_CRASH_UNSAFE(strdup(allDetails.BeginReading()));
+ }
+
+ return NS_OK;
+}
+
+#endif
+
+nsresult StorageOperationBase::GetDirectoryMetadata(nsIFile* aDirectory,
+ int64_t& aTimestamp,
+ nsACString& aGroup,
+ nsACString& aOrigin,
+ Nullable<bool>& aIsApp) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aDirectory);
+
+ QM_TRY_INSPECT(
+ const auto& binaryStream,
+ GetBinaryInputStream(*aDirectory, nsLiteralString(METADATA_FILE_NAME)));
+
+ QM_TRY_INSPECT(const uint64_t& timestamp,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
+
+ QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, binaryStream, ReadCString));
+
+ QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, binaryStream, ReadCString));
+
+ Nullable<bool> isApp;
+ bool value;
+ if (NS_SUCCEEDED(binaryStream->ReadBoolean(&value))) {
+ isApp.SetValue(value);
+ }
+
+ aTimestamp = timestamp;
+ aGroup = group;
+ aOrigin = origin;
+ aIsApp = std::move(isApp);
+ return NS_OK;
+}
+
+nsresult StorageOperationBase::GetDirectoryMetadata2(
+ nsIFile* aDirectory, int64_t& aTimestamp, nsACString& aSuffix,
+ nsACString& aGroup, nsACString& aOrigin, bool& aIsApp) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aDirectory);
+
+ QM_TRY_INSPECT(const auto& binaryStream,
+ GetBinaryInputStream(*aDirectory,
+ nsLiteralString(METADATA_V2_FILE_NAME)));
+
+ QM_TRY_INSPECT(const uint64_t& timestamp,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read64));
+
+ QM_TRY_INSPECT(const bool& persisted,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
+ Unused << persisted;
+
+ QM_TRY_INSPECT(const bool& reservedData1,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+ Unused << reservedData1;
+
+ QM_TRY_INSPECT(const bool& reservedData2,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, Read32));
+ Unused << reservedData2;
+
+ QM_TRY_INSPECT(const auto& suffix, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, binaryStream, ReadCString));
+
+ QM_TRY_INSPECT(const auto& group, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, binaryStream, ReadCString));
+
+ QM_TRY_INSPECT(const auto& origin, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCString, binaryStream, ReadCString));
+
+ QM_TRY_INSPECT(const bool& isApp,
+ MOZ_TO_RESULT_INVOKE_MEMBER(binaryStream, ReadBoolean));
+
+ aTimestamp = timestamp;
+ aSuffix = suffix;
+ aGroup = group;
+ aOrigin = origin;
+ aIsApp = isApp;
+ return NS_OK;
+}
+
+int64_t StorageOperationBase::GetOriginLastModifiedTime(
+ const OriginProps& aOriginProps) {
+ return GetLastModifiedTime(*aOriginProps.mPersistenceType,
+ *aOriginProps.mDirectory);
+}
+
+nsresult StorageOperationBase::RemoveObsoleteOrigin(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ QM_WARNING(
+ "Deleting obsolete %s directory that is no longer a legal "
+ "origin!",
+ NS_ConvertUTF16toUTF8(aOriginProps.mLeafName).get());
+
+ QM_TRY(MOZ_TO_RESULT(aOriginProps.mDirectory->Remove(/* recursive */ true)));
+
+ return NS_OK;
+}
+
+Result<bool, nsresult> StorageOperationBase::MaybeRenameOrigin(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ const nsAString& oldLeafName = aOriginProps.mLeafName;
+
+ const auto newLeafName =
+ MakeSanitizedOriginString(aOriginProps.mOriginMetadata.mOrigin);
+
+ if (oldLeafName == newLeafName) {
+ return false;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(*aOriginProps.mDirectory,
+ aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+
+ QM_TRY_INSPECT(const auto& newFile,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, GetParent));
+
+ QM_TRY(MOZ_TO_RESULT(newFile->Append(newLeafName)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(newFile, Exists));
+
+ if (exists) {
+ QM_WARNING(
+ "Can't rename %s directory to %s, the target already exists, removing "
+ "instead of renaming!",
+ NS_ConvertUTF16toUTF8(oldLeafName).get(),
+ NS_ConvertUTF16toUTF8(newLeafName).get());
+ }
+
+ QM_TRY(CallWithDelayedRetriesIfAccessDenied(
+ [&exists, &aOriginProps, &newLeafName] {
+ if (exists) {
+ QM_TRY_RETURN(MOZ_TO_RESULT(
+ aOriginProps.mDirectory->Remove(/* recursive */ true)));
+ }
+ QM_TRY_RETURN(MOZ_TO_RESULT(
+ aOriginProps.mDirectory->RenameTo(nullptr, newLeafName)));
+ },
+ StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_maxRetries(),
+ StaticPrefs::dom_quotaManager_directoryRemovalOrRenaming_delayMs()));
+
+ return true;
+}
+
+nsresult StorageOperationBase::ProcessOriginDirectories() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(!mOriginProps.IsEmpty());
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+ nsTArray<PrincipalInfo> principalInfos;
+#endif
+
+ for (auto& originProps : mOriginProps) {
+ switch (originProps.mType) {
+ case OriginProps::eChrome: {
+ originProps.mOriginMetadata = {QuotaManager::GetInfoForChrome(),
+ *originProps.mPersistenceType};
+ break;
+ }
+
+ case OriginProps::eContent: {
+ RefPtr<MozURL> specURL;
+ nsresult rv = MozURL::Init(getter_AddRefs(specURL), originProps.mSpec);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // If a URL cannot be understood by MozURL during restoring or
+ // upgrading, either marking the directory as broken or removing that
+ // corresponding directory should be considered. While the cost of
+ // marking the directory as broken during a upgrade is too high,
+ // removing the directory is a better choice rather than blocking the
+ // initialization or the upgrade.
+ QM_WARNING(
+ "A URL (%s) for the origin directory is not recognized by "
+ "MozURL. The directory will be deleted for now to pass the "
+ "initialization or the upgrade.",
+ originProps.mSpec.get());
+
+ originProps.mType = OriginProps::eObsolete;
+ break;
+ }
+
+ nsCString originNoSuffix;
+ specURL->Origin(originNoSuffix);
+
+ QM_TRY_INSPECT(
+ const auto& baseDomain,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCString, specURL, BaseDomain));
+
+ ContentPrincipalInfo contentPrincipalInfo;
+ contentPrincipalInfo.attrs() = originProps.mAttrs;
+ contentPrincipalInfo.originNoSuffix() = originNoSuffix;
+ contentPrincipalInfo.spec() = originProps.mSpec;
+ contentPrincipalInfo.baseDomain() = baseDomain;
+
+ PrincipalInfo principalInfo(contentPrincipalInfo);
+
+ originProps.mOriginMetadata = {
+ QuotaManager::GetInfoFromValidatedPrincipalInfo(principalInfo),
+ *originProps.mPersistenceType};
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+ principalInfos.AppendElement(principalInfo);
+#endif
+
+ break;
+ }
+
+ case OriginProps::eObsolete: {
+ // There's no way to get info for obsolete origins.
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Bad type!");
+ }
+ }
+
+#ifdef QM_PRINCIPALINFO_VERIFICATION_ENABLED
+ if (!principalInfos.IsEmpty()) {
+ RefPtr<PrincipalVerifier> principalVerifier =
+ PrincipalVerifier::CreateAndDispatch(std::move(principalInfos));
+ }
+#endif
+
+ // Don't try to upgrade obsolete origins, remove them right after we detect
+ // them.
+ for (const auto& originProps : mOriginProps) {
+ if (originProps.mType == OriginProps::eObsolete) {
+ MOZ_ASSERT(originProps.mOriginMetadata.mSuffix.IsEmpty());
+ MOZ_ASSERT(originProps.mOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(originProps.mOriginMetadata.mOrigin.IsEmpty());
+
+ QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(originProps)));
+ } else {
+ MOZ_ASSERT(!originProps.mOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(!originProps.mOriginMetadata.mOrigin.IsEmpty());
+
+ QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectory(originProps)));
+ }
+ }
+
+ return NS_OK;
+}
+
+// XXX Do the fallible initialization in a separate non-static member function
+// of StorageOperationBase and eventually get rid of this method and use a
+// normal constructor instead.
+template <typename PersistenceTypeFunc>
+nsresult StorageOperationBase::OriginProps::Init(
+ PersistenceTypeFunc&& aPersistenceTypeFunc) {
+ AssertIsOnIOThread();
+
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, *mDirectory,
+ GetLeafName));
+
+ nsCString spec;
+ OriginAttributes attrs;
+ nsCString originalSuffix;
+ OriginParser::ResultType result = OriginParser::ParseOrigin(
+ NS_ConvertUTF16toUTF8(leafName), spec, &attrs, originalSuffix);
+ if (NS_WARN_IF(result == OriginParser::InvalidOrigin)) {
+ mType = OriginProps::eInvalid;
+ return NS_OK;
+ }
+
+ const auto persistenceType = [&]() -> PersistenceType {
+ // XXX We shouldn't continue with initialization if OriginParser returned
+ // anything else but ValidOrigin. Otherwise, we have to deal with empty
+ // spec when the origin is obsolete, like here. The caller should handle
+ // the errors. Until it's fixed, we have to treat obsolete origins as
+ // origins with unknown/invalid persistence type.
+ if (result != OriginParser::ValidOrigin) {
+ return PERSISTENCE_TYPE_INVALID;
+ }
+ return std::forward<PersistenceTypeFunc>(aPersistenceTypeFunc)(spec);
+ }();
+
+ mLeafName = leafName;
+ mSpec = spec;
+ mAttrs = attrs;
+ mOriginalSuffix = originalSuffix;
+ mPersistenceType.init(persistenceType);
+ if (result == OriginParser::ObsoleteOrigin) {
+ mType = eObsolete;
+ } else if (mSpec.EqualsLiteral(kChromeOrigin)) {
+ mType = eChrome;
+ } else {
+ mType = eContent;
+ }
+
+ return NS_OK;
+}
+
+// static
+auto OriginParser::ParseOrigin(const nsACString& aOrigin, nsCString& aSpec,
+ OriginAttributes* aAttrs,
+ nsCString& aOriginalSuffix) -> ResultType {
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+ MOZ_ASSERT(aAttrs);
+
+ nsCString origin(aOrigin);
+ int32_t pos = origin.RFindChar('^');
+
+ if (pos == kNotFound) {
+ aOriginalSuffix.Truncate();
+ } else {
+ aOriginalSuffix = Substring(origin, pos);
+ }
+
+ OriginAttributes originAttributes;
+
+ nsCString originNoSuffix;
+ bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix);
+ if (!ok) {
+ return InvalidOrigin;
+ }
+
+ OriginParser parser(originNoSuffix);
+
+ *aAttrs = originAttributes;
+ return parser.Parse(aSpec);
+}
+
+auto OriginParser::Parse(nsACString& aSpec) -> ResultType {
+ while (mTokenizer.hasMoreTokens()) {
+ const nsDependentCSubstring& token = mTokenizer.nextToken();
+
+ HandleToken(token);
+
+ if (mError) {
+ break;
+ }
+
+ if (!mHandledTokens.IsEmpty()) {
+ mHandledTokens.AppendLiteral(", ");
+ }
+ mHandledTokens.Append('\'');
+ mHandledTokens.Append(token);
+ mHandledTokens.Append('\'');
+ }
+
+ if (!mError && mTokenizer.separatorAfterCurrentToken()) {
+ HandleTrailingSeparator();
+ }
+
+ if (mError) {
+ QM_WARNING("Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(),
+ mHandledTokens.get());
+
+ return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin
+ : InvalidOrigin;
+ }
+
+ MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator);
+
+ // For IPv6 URL, it should at least have three groups.
+ MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3);
+
+ nsAutoCString spec(mScheme);
+
+ if (mSchemeType == eFile) {
+ spec.AppendLiteral("://");
+
+ if (mUniversalFileOrigin) {
+ MOZ_ASSERT(mPathnameComponents.Length() == 1);
+
+ spec.Append(mPathnameComponents[0]);
+ } else {
+ for (uint32_t count = mPathnameComponents.Length(), index = 0;
+ index < count; index++) {
+ spec.Append('/');
+ spec.Append(mPathnameComponents[index]);
+ }
+ }
+
+ aSpec = spec;
+
+ return ValidOrigin;
+ }
+
+ if (mSchemeType == eAbout) {
+ if (mMaybeObsolete) {
+ // The "moz-safe-about+++home" was acciedntally created by a buggy nightly
+ // and can be safely removed.
+ return mHost.EqualsLiteral("home") ? ObsoleteOrigin : InvalidOrigin;
+ }
+ spec.Append(':');
+ } else if (mSchemeType != eChrome) {
+ spec.AppendLiteral("://");
+ }
+
+ spec.Append(mHost);
+
+ if (!mPort.IsNull()) {
+ spec.Append(':');
+ spec.AppendInt(mPort.Value());
+ }
+
+ aSpec = spec;
+
+ return mScheme.EqualsLiteral("app") ? ObsoleteOrigin : ValidOrigin;
+}
+
+void OriginParser::HandleScheme(const nsDependentCSubstring& aToken) {
+ MOZ_ASSERT(!aToken.IsEmpty());
+ MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme);
+
+ bool isAbout = false;
+ bool isMozSafeAbout = false;
+ bool isFile = false;
+ bool isChrome = false;
+ if (aToken.EqualsLiteral("http") || aToken.EqualsLiteral("https") ||
+ (isAbout = aToken.EqualsLiteral("about") ||
+ (isMozSafeAbout = aToken.EqualsLiteral("moz-safe-about"))) ||
+ aToken.EqualsLiteral("indexeddb") ||
+ (isFile = aToken.EqualsLiteral("file")) || aToken.EqualsLiteral("app") ||
+ aToken.EqualsLiteral("resource") ||
+ aToken.EqualsLiteral("moz-extension") ||
+ (isChrome = aToken.EqualsLiteral(kChromeOrigin))) {
+ mScheme = aToken;
+
+ if (isAbout) {
+ mSchemeType = eAbout;
+ mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost;
+ } else if (isChrome) {
+ mSchemeType = eChrome;
+ if (mTokenizer.hasMoreTokens()) {
+ mError = true;
+ }
+ mState = eComplete;
+ } else {
+ if (isFile) {
+ mSchemeType = eFile;
+ }
+ mState = eExpectingEmptyToken1;
+ }
+
+ return;
+ }
+
+ QM_WARNING("'%s' is not a valid scheme!", nsCString(aToken).get());
+
+ mError = true;
+}
+
+void OriginParser::HandlePathnameComponent(
+ const nsDependentCSubstring& aToken) {
+ MOZ_ASSERT(!aToken.IsEmpty());
+ MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent ||
+ mState == eExpectingEmptyTokenOrPathnameComponent);
+ MOZ_ASSERT(mSchemeType == eFile);
+
+ mPathnameComponents.AppendElement(aToken);
+
+ mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent
+ : eComplete;
+}
+
+void OriginParser::HandleToken(const nsDependentCSubstring& aToken) {
+ switch (mState) {
+ case eExpectingAppIdOrScheme: {
+ if (aToken.IsEmpty()) {
+ QM_WARNING("Expected an app id or scheme (not an empty string)!");
+
+ mError = true;
+ return;
+ }
+
+ if (IsAsciiDigit(aToken.First())) {
+ // nsDependentCSubstring doesn't provice ToInteger()
+ nsCString token(aToken);
+
+ nsresult rv;
+ Unused << token.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) {
+ mState = eExpectingInMozBrowser;
+ return;
+ }
+ }
+
+ HandleScheme(aToken);
+
+ return;
+ }
+
+ case eExpectingInMozBrowser: {
+ if (aToken.Length() != 1) {
+ QM_WARNING("'%zu' is not a valid length for the inMozBrowser flag!",
+ aToken.Length());
+
+ mError = true;
+ return;
+ }
+
+ if (aToken.First() == 't') {
+ mInIsolatedMozBrowser = true;
+ } else if (aToken.First() == 'f') {
+ mInIsolatedMozBrowser = false;
+ } else {
+ QM_WARNING("'%s' is not a valid value for the inMozBrowser flag!",
+ nsCString(aToken).get());
+
+ mError = true;
+ return;
+ }
+
+ mState = eExpectingScheme;
+
+ return;
+ }
+
+ case eExpectingScheme: {
+ if (aToken.IsEmpty()) {
+ QM_WARNING("Expected a scheme (not an empty string)!");
+
+ mError = true;
+ return;
+ }
+
+ HandleScheme(aToken);
+
+ return;
+ }
+
+ case eExpectingEmptyToken1: {
+ if (!aToken.IsEmpty()) {
+ QM_WARNING("Expected the first empty token!");
+
+ mError = true;
+ return;
+ }
+
+ mState = eExpectingEmptyToken2;
+
+ return;
+ }
+
+ case eExpectingEmptyToken2: {
+ if (!aToken.IsEmpty()) {
+ QM_WARNING("Expected the second empty token!");
+
+ mError = true;
+ return;
+ }
+
+ if (mSchemeType == eFile) {
+ mState = eExpectingEmptyTokenOrUniversalFileOrigin;
+ } else {
+ if (mSchemeType == eAbout) {
+ mMaybeObsolete = true;
+ }
+ mState = eExpectingHost;
+ }
+
+ return;
+ }
+
+ case eExpectingEmptyTokenOrUniversalFileOrigin: {
+ MOZ_ASSERT(mSchemeType == eFile);
+
+ if (aToken.IsEmpty()) {
+ mState = mTokenizer.hasMoreTokens()
+ ? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent
+ : eComplete;
+
+ return;
+ }
+
+ if (aToken.EqualsLiteral("UNIVERSAL_FILE_URI_ORIGIN")) {
+ mUniversalFileOrigin = true;
+
+ mPathnameComponents.AppendElement(aToken);
+
+ mState = eComplete;
+
+ return;
+ }
+
+ QM_WARNING(
+ "Expected the third empty token or "
+ "UNIVERSAL_FILE_URI_ORIGIN!");
+
+ mError = true;
+ return;
+ }
+
+ case eExpectingHost: {
+ if (aToken.IsEmpty()) {
+ QM_WARNING("Expected a host (not an empty string)!");
+
+ mError = true;
+ return;
+ }
+
+ mHost = aToken;
+
+ if (aToken.First() == '[') {
+ MOZ_ASSERT(mIPGroup == 0);
+
+ ++mIPGroup;
+ mState = eExpectingIPV6Token;
+
+ MOZ_ASSERT(mTokenizer.hasMoreTokens());
+ return;
+ }
+
+ if (mTokenizer.hasMoreTokens()) {
+ if (mSchemeType == eAbout) {
+ QM_WARNING("Expected an empty string after host!");
+
+ mError = true;
+ return;
+ }
+
+ mState = eExpectingPort;
+
+ return;
+ }
+
+ mState = eComplete;
+
+ return;
+ }
+
+ case eExpectingPort: {
+ MOZ_ASSERT(mSchemeType == eNone);
+
+ if (aToken.IsEmpty()) {
+ QM_WARNING("Expected a port (not an empty string)!");
+
+ mError = true;
+ return;
+ }
+
+ // nsDependentCSubstring doesn't provice ToInteger()
+ nsCString token(aToken);
+
+ nsresult rv;
+ uint32_t port = token.ToInteger(&rv);
+ if (NS_SUCCEEDED(rv)) {
+ mPort.SetValue() = port;
+ } else {
+ QM_WARNING("'%s' is not a valid port number!", token.get());
+
+ mError = true;
+ return;
+ }
+
+ mState = eComplete;
+
+ return;
+ }
+
+ case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: {
+ MOZ_ASSERT(mSchemeType == eFile);
+
+ if (aToken.IsEmpty()) {
+ mPathnameComponents.AppendElement(""_ns);
+
+ mState = mTokenizer.hasMoreTokens()
+ ? eExpectingEmptyTokenOrPathnameComponent
+ : eComplete;
+
+ return;
+ }
+
+ if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) {
+ mMaybeDriveLetter = true;
+
+ mPathnameComponents.AppendElement(aToken);
+
+ mState = mTokenizer.hasMoreTokens()
+ ? eExpectingEmptyTokenOrPathnameComponent
+ : eComplete;
+
+ return;
+ }
+
+ HandlePathnameComponent(aToken);
+
+ return;
+ }
+
+ case eExpectingEmptyTokenOrPathnameComponent: {
+ MOZ_ASSERT(mSchemeType == eFile);
+
+ if (aToken.IsEmpty()) {
+ if (mMaybeDriveLetter) {
+ MOZ_ASSERT(mPathnameComponents.Length() == 1);
+
+ nsCString& pathnameComponent = mPathnameComponents[0];
+ pathnameComponent.Append(':');
+
+ mMaybeDriveLetter = false;
+ } else {
+ mPathnameComponents.AppendElement(""_ns);
+ }
+
+ mState = mTokenizer.hasMoreTokens()
+ ? eExpectingEmptyTokenOrPathnameComponent
+ : eComplete;
+
+ return;
+ }
+
+ HandlePathnameComponent(aToken);
+
+ return;
+ }
+
+ case eExpectingEmptyToken1OrHost: {
+ MOZ_ASSERT(mSchemeType == eAbout &&
+ mScheme.EqualsLiteral("moz-safe-about"));
+
+ if (aToken.IsEmpty()) {
+ mState = eExpectingEmptyToken2;
+ } else {
+ mHost = aToken;
+ mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
+ }
+
+ return;
+ }
+
+ case eExpectingIPV6Token: {
+ // A safe check for preventing infinity recursion.
+ if (++mIPGroup > 8) {
+ mError = true;
+ return;
+ }
+
+ mHost.AppendLiteral(":");
+ mHost.Append(aToken);
+ if (!aToken.IsEmpty() && aToken.Last() == ']') {
+ mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
+ }
+
+ return;
+ }
+
+ default:
+ MOZ_CRASH("Should never get here!");
+ }
+}
+
+void OriginParser::HandleTrailingSeparator() {
+ MOZ_ASSERT(mState == eComplete);
+ MOZ_ASSERT(mSchemeType == eFile);
+
+ mPathnameComponents.AppendElement(""_ns);
+
+ mState = eHandledTrailingSeparator;
+}
+
+nsresult RepositoryOperationBase::ProcessRepository() {
+ AssertIsOnIOThread();
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(mDirectory, Exists),
+ QM_ASSERT_UNREACHABLE);
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY(CollectEachFileEntry(
+ *mDirectory,
+ [](const auto& originFile) -> Result<mozilla::Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsAutoString, originFile, GetLeafName));
+
+ // Unknown files during upgrade are allowed. Just warn if we find
+ // them.
+ if (!IsOSMetadata(leafName)) {
+ UNKNOWN_FILE_WARNING(leafName);
+ }
+
+ return mozilla::Ok{};
+ },
+ [&self = *this](const auto& originDir) -> Result<mozilla::Ok, nsresult> {
+ OriginProps originProps(WrapMovingNotNullUnchecked(originDir));
+ QM_TRY(MOZ_TO_RESULT(originProps.Init([&self](const auto& aSpec) {
+ return self.PersistenceTypeFromSpec(aSpec);
+ })));
+ // Bypass invalid origins while upgrading
+ QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), mozilla::Ok{});
+
+ if (originProps.mType != OriginProps::eObsolete) {
+ QM_TRY_INSPECT(const bool& removed,
+ MOZ_TO_RESULT_INVOKE_MEMBER(
+ self, PrepareOriginDirectory, originProps));
+ if (removed) {
+ return mozilla::Ok{};
+ }
+ }
+
+ self.mOriginProps.AppendElement(std::move(originProps));
+
+ return mozilla::Ok{};
+ }));
+
+ if (mOriginProps.IsEmpty()) {
+ return NS_OK;
+ }
+
+ QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories()));
+
+ return NS_OK;
+}
+
+template <typename UpgradeMethod>
+nsresult RepositoryOperationBase::MaybeUpgradeClients(
+ const OriginProps& aOriginProps, UpgradeMethod aMethod) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aMethod);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ QM_TRY(CollectEachFileEntry(
+ *aOriginProps.mDirectory,
+ [](const auto& file) -> Result<mozilla::Ok, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file, GetLeafName));
+
+ if (!IsOriginMetadata(leafName) && !IsTempMetadata(leafName)) {
+ UNKNOWN_FILE_WARNING(leafName);
+ }
+
+ return mozilla::Ok{};
+ },
+ [quotaManager, &aMethod,
+ &self = *this](const auto& dir) -> Result<mozilla::Ok, nsresult> {
+ QM_TRY_INSPECT(
+ const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, dir, GetLeafName));
+
+ QM_TRY_INSPECT(const bool& removed,
+ MOZ_TO_RESULT_INVOKE_MEMBER(self, PrepareClientDirectory,
+ dir, leafName));
+ if (removed) {
+ return mozilla::Ok{};
+ }
+
+ Client::Type clientType;
+ bool ok = Client::TypeFromText(leafName, clientType, fallible);
+ if (!ok) {
+ UNKNOWN_FILE_WARNING(leafName);
+ return mozilla::Ok{};
+ }
+
+ Client* client = quotaManager->GetClient(clientType);
+ MOZ_ASSERT(client);
+
+ QM_TRY(MOZ_TO_RESULT((client->*aMethod)(dir)));
+
+ return mozilla::Ok{};
+ }));
+
+ return NS_OK;
+}
+
+nsresult RepositoryOperationBase::PrepareClientDirectory(
+ nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) {
+ AssertIsOnIOThread();
+
+ aRemoved = false;
+ return NS_OK;
+}
+
+nsresult CreateOrUpgradeDirectoryMetadataHelper::Init() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mDirectory);
+
+ const auto maybeLegacyPersistenceType =
+ LegacyPersistenceTypeFromFile(*mDirectory, fallible);
+ QM_TRY(OkIf(maybeLegacyPersistenceType.isSome()), Err(NS_ERROR_FAILURE));
+
+ mLegacyPersistenceType.init(maybeLegacyPersistenceType.value());
+
+ return NS_OK;
+}
+
+Maybe<CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceType>
+CreateOrUpgradeDirectoryMetadataHelper::LegacyPersistenceTypeFromFile(
+ nsIFile& aFile, const fallible_t&) {
+ nsAutoString leafName;
+ MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName));
+
+ if (leafName.Equals(u"persistent"_ns)) {
+ return Some(LegacyPersistenceType::Persistent);
+ }
+
+ if (leafName.Equals(u"temporary"_ns)) {
+ return Some(LegacyPersistenceType::Temporary);
+ }
+
+ return Nothing();
+}
+
+PersistenceType
+CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromLegacyPersistentSpec(
+ const nsCString& aSpec) {
+ if (QuotaManager::IsOriginInternal(aSpec)) {
+ return PERSISTENCE_TYPE_PERSISTENT;
+ }
+
+ return PERSISTENCE_TYPE_DEFAULT;
+}
+
+PersistenceType CreateOrUpgradeDirectoryMetadataHelper::PersistenceTypeFromSpec(
+ const nsCString& aSpec) {
+ switch (*mLegacyPersistenceType) {
+ case LegacyPersistenceType::Persistent:
+ return PersistenceTypeFromLegacyPersistentSpec(aSpec);
+ case LegacyPersistenceType::Temporary:
+ return PERSISTENCE_TYPE_TEMPORARY;
+ }
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad legacy persistence type value!");
+}
+
+nsresult CreateOrUpgradeDirectoryMetadataHelper::MaybeUpgradeOriginDirectory(
+ nsIFile* aDirectory) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aDirectory);
+
+ QM_TRY_INSPECT(
+ const auto& metadataFile,
+ CloneFileAndAppend(*aDirectory, nsLiteralString(METADATA_FILE_NAME)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(metadataFile, Exists));
+
+ if (!exists) {
+ // Directory structure upgrade needed.
+ // Move all files to IDB specific directory.
+
+ nsString idbDirectoryName;
+ QM_TRY(OkIf(Client::TypeToText(Client::IDB, idbDirectoryName, fallible)),
+ NS_ERROR_FAILURE);
+
+ QM_TRY_INSPECT(const auto& idbDirectory,
+ CloneFileAndAppend(*aDirectory, idbDirectoryName));
+
+ // Usually we only use QM_OR_ELSE_LOG_VERBOSE/QM_OR_ELSE_LOG_VERBOSE_IF
+ // with Create and NS_ERROR_FILE_ALREADY_EXISTS check, but typically the
+ // idb directory shouldn't exist during the upgrade and the upgrade runs
+ // only once in most of the cases, so the use of QM_OR_ELSE_WARN_IF is ok
+ // here.
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ // Expression.
+ MOZ_TO_RESULT(idbDirectory->Create(nsIFile::DIRECTORY_TYPE, 0755)),
+ // Predicate.
+ IsSpecificError<NS_ERROR_FILE_ALREADY_EXISTS>,
+ // Fallback.
+ ([&idbDirectory](const nsresult rv) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(
+ const bool& isDirectory,
+ MOZ_TO_RESULT_INVOKE_MEMBER(idbDirectory, IsDirectory));
+
+ QM_TRY(OkIf(isDirectory), Err(NS_ERROR_UNEXPECTED));
+
+ return Ok{};
+ })));
+
+ QM_TRY(CollectEachFile(
+ *aDirectory,
+ [&idbDirectory, &idbDirectoryName](
+ const nsCOMPtr<nsIFile>& file) -> Result<Ok, nsresult> {
+ QM_TRY_INSPECT(const auto& leafName,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsAutoString, file,
+ GetLeafName));
+
+ if (!leafName.Equals(idbDirectoryName)) {
+ QM_TRY(MOZ_TO_RESULT(file->MoveTo(idbDirectory, u""_ns)));
+ }
+
+ return Ok{};
+ }));
+
+ QM_TRY(
+ MOZ_TO_RESULT(metadataFile->Create(nsIFile::NORMAL_FILE_TYPE, 0644)));
+ }
+
+ return NS_OK;
+}
+
+nsresult CreateOrUpgradeDirectoryMetadataHelper::PrepareOriginDirectory(
+ OriginProps& aOriginProps, bool* aRemoved) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aRemoved);
+
+ if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) {
+ QM_TRY(MOZ_TO_RESULT(
+ MaybeUpgradeOriginDirectory(aOriginProps.mDirectory.get())));
+
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ } else {
+ int64_t timestamp;
+ nsCString group;
+ nsCString origin;
+ Nullable<bool> isApp;
+
+ QM_WARNONLY_TRY_UNWRAP(
+ const auto maybeDirectoryMetadata,
+ MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
+ timestamp, group, origin, isApp)));
+ if (!maybeDirectoryMetadata) {
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ aOriginProps.mNeedsRestore = true;
+ } else if (!isApp.IsNull()) {
+ aOriginProps.mIgnore = true;
+ }
+ }
+
+ *aRemoved = false;
+ return NS_OK;
+}
+
+nsresult CreateOrUpgradeDirectoryMetadataHelper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ if (*mLegacyPersistenceType == LegacyPersistenceType::Persistent) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+
+ // Move internal origins to new persistent storage.
+ if (PersistenceTypeFromLegacyPersistentSpec(aOriginProps.mSpec) ==
+ PERSISTENCE_TYPE_PERSISTENT) {
+ if (!mPermanentStorageDir) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ const nsString& permanentStoragePath =
+ quotaManager->GetStoragePath(PERSISTENCE_TYPE_PERSISTENT);
+
+ QM_TRY_UNWRAP(mPermanentStorageDir,
+ QM_NewLocalFile(permanentStoragePath));
+ }
+
+ const nsAString& leafName = aOriginProps.mLeafName;
+
+ QM_TRY_INSPECT(const auto& newDirectory,
+ CloneFileAndAppend(*mPermanentStorageDir, leafName));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(newDirectory, Exists));
+
+ if (exists) {
+ QM_WARNING("Found %s in storage/persistent and storage/permanent !",
+ NS_ConvertUTF16toUTF8(leafName).get());
+
+ QM_TRY(MOZ_TO_RESULT(
+ aOriginProps.mDirectory->Remove(/* recursive */ true)));
+ } else {
+ QM_TRY(MOZ_TO_RESULT(
+ aOriginProps.mDirectory->MoveTo(mPermanentStorageDir, u""_ns)));
+ }
+ }
+ } else if (aOriginProps.mNeedsRestore) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+ } else if (!aOriginProps.mIgnore) {
+ QM_TRY_INSPECT(const auto& file,
+ CloneFileAndAppend(*aOriginProps.mDirectory,
+ nsLiteralString(METADATA_FILE_NAME)));
+
+ QM_TRY_INSPECT(const auto& stream,
+ GetBinaryOutputStream(*file, FileFlag::Append));
+
+ MOZ_ASSERT(stream);
+
+ // Currently unused (used to be isApp).
+ QM_TRY(MOZ_TO_RESULT(stream->WriteBoolean(false)));
+ }
+
+ return NS_OK;
+}
+
+nsresult UpgradeStorageHelperBase::Init() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mDirectory);
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromFile(*mDirectory, fallible);
+ QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
+
+ mPersistenceType.init(maybePersistenceType.value());
+
+ return NS_OK;
+}
+
+PersistenceType UpgradeStorageHelperBase::PersistenceTypeFromSpec(
+ const nsCString& aSpec) {
+ // There's no moving of origin directories between repositories like in the
+ // CreateOrUpgradeDirectoryMetadataHelper
+ return *mPersistenceType;
+}
+
+nsresult UpgradeStorageFrom0_0To1_0Helper::PrepareOriginDirectory(
+ OriginProps& aOriginProps, bool* aRemoved) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aRemoved);
+
+ int64_t timestamp;
+ nsCString group;
+ nsCString origin;
+ Nullable<bool> isApp;
+
+ QM_WARNONLY_TRY_UNWRAP(
+ const auto maybeDirectoryMetadata,
+ MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
+ timestamp, group, origin, isApp)));
+ if (!maybeDirectoryMetadata || isApp.IsNull()) {
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ aOriginProps.mNeedsRestore = true;
+ } else {
+ aOriginProps.mTimestamp = timestamp;
+ }
+
+ *aRemoved = false;
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom0_0To1_0Helper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ // This handles changes in origin string generation from nsIPrincipal,
+ // especially the change from: appId+inMozBrowser+originNoSuffix
+ // to: origin (with origin suffix).
+ QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps));
+ if (renamed) {
+ return NS_OK;
+ }
+
+ if (aOriginProps.mNeedsRestore) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+ }
+
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveMorgueDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ // The Cache API was creating top level morgue directories by accident for
+ // a short time in nightly. This unfortunately prevents all storage from
+ // working. So recover these profiles permanently by removing these corrupt
+ // directories as part of this upgrade.
+
+ QM_TRY_INSPECT(const auto& morgueDir,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, *aOriginProps.mDirectory, Clone));
+
+ QM_TRY(MOZ_TO_RESULT(morgueDir->Append(u"morgue"_ns)));
+
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(morgueDir, Exists));
+
+ if (exists) {
+ QM_WARNING("Deleting accidental morgue directory!");
+
+ QM_TRY(MOZ_TO_RESULT(morgueDir->Remove(/* recursive */ true)));
+ }
+
+ return NS_OK;
+}
+
+Result<bool, nsresult> UpgradeStorageFrom1_0To2_0Helper::MaybeRemoveAppsData(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ // TODO: This method was empty for some time due to accidental changes done
+ // in bug 1320404. This led to renaming of origin directories like:
+ // https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1
+ // to:
+ // https+++developer.cdn.mozilla.net^inBrowser=1
+ // instead of just removing them.
+
+ const nsCString& originalSuffix = aOriginProps.mOriginalSuffix;
+ if (!originalSuffix.IsEmpty()) {
+ MOZ_ASSERT(originalSuffix[0] == '^');
+
+ if (!URLParams::Parse(
+ Substring(originalSuffix, 1, originalSuffix.Length() - 1),
+ [](const nsAString& aName, const nsAString& aValue) {
+ if (aName.EqualsLiteral("appId")) {
+ return false;
+ }
+
+ return true;
+ })) {
+ QM_TRY(MOZ_TO_RESULT(RemoveObsoleteOrigin(aOriginProps)));
+
+ return true;
+ }
+ }
+
+ return false;
+}
+
+nsresult UpgradeStorageFrom1_0To2_0Helper::PrepareOriginDirectory(
+ OriginProps& aOriginProps, bool* aRemoved) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aRemoved);
+
+ QM_TRY(MOZ_TO_RESULT(MaybeRemoveMorgueDirectory(aOriginProps)));
+
+ QM_TRY(MOZ_TO_RESULT(
+ MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom1_0To2_0)));
+
+ QM_TRY_INSPECT(const bool& removed, MaybeRemoveAppsData(aOriginProps));
+ if (removed) {
+ *aRemoved = true;
+ return NS_OK;
+ }
+
+ int64_t timestamp;
+ nsCString group;
+ nsCString origin;
+ Nullable<bool> isApp;
+ QM_WARNONLY_TRY_UNWRAP(
+ const auto maybeDirectoryMetadata,
+ MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
+ timestamp, group, origin, isApp)));
+ if (!maybeDirectoryMetadata || isApp.IsNull()) {
+ aOriginProps.mNeedsRestore = true;
+ }
+
+ nsCString suffix;
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
+ MOZ_TO_RESULT(GetDirectoryMetadata2(
+ aOriginProps.mDirectory.get(), timestamp, suffix,
+ group, origin, isApp.SetValue())));
+ if (!maybeDirectoryMetadata2) {
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ aOriginProps.mNeedsRestore2 = true;
+ } else {
+ aOriginProps.mTimestamp = timestamp;
+ }
+
+ *aRemoved = false;
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom1_0To2_0Helper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ // This handles changes in origin string generation from nsIPrincipal,
+ // especially the stripping of obsolete origin attributes like addonId.
+ QM_TRY_INSPECT(const bool& renamed, MaybeRenameOrigin(aOriginProps));
+ if (renamed) {
+ return NS_OK;
+ }
+
+ if (aOriginProps.mNeedsRestore) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+ }
+
+ if (aOriginProps.mNeedsRestore2) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+ }
+
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom2_0To2_1Helper::PrepareOriginDirectory(
+ OriginProps& aOriginProps, bool* aRemoved) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aRemoved);
+
+ QM_TRY(MOZ_TO_RESULT(
+ MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_0To2_1)));
+
+ int64_t timestamp;
+ nsCString group;
+ nsCString origin;
+ Nullable<bool> isApp;
+ QM_WARNONLY_TRY_UNWRAP(
+ const auto maybeDirectoryMetadata,
+ MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
+ timestamp, group, origin, isApp)));
+ if (!maybeDirectoryMetadata || isApp.IsNull()) {
+ aOriginProps.mNeedsRestore = true;
+ }
+
+ nsCString suffix;
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
+ MOZ_TO_RESULT(GetDirectoryMetadata2(
+ aOriginProps.mDirectory.get(), timestamp, suffix,
+ group, origin, isApp.SetValue())));
+ if (!maybeDirectoryMetadata2) {
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ aOriginProps.mNeedsRestore2 = true;
+ } else {
+ aOriginProps.mTimestamp = timestamp;
+ }
+
+ *aRemoved = false;
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom2_0To2_1Helper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ if (aOriginProps.mNeedsRestore) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+ }
+
+ if (aOriginProps.mNeedsRestore2) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+ }
+
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareOriginDirectory(
+ OriginProps& aOriginProps, bool* aRemoved) {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(aRemoved);
+
+ QM_TRY(MOZ_TO_RESULT(
+ MaybeUpgradeClients(aOriginProps, &Client::UpgradeStorageFrom2_1To2_2)));
+
+ int64_t timestamp;
+ nsCString group;
+ nsCString origin;
+ Nullable<bool> isApp;
+ QM_WARNONLY_TRY_UNWRAP(
+ const auto maybeDirectoryMetadata,
+ MOZ_TO_RESULT(GetDirectoryMetadata(aOriginProps.mDirectory.get(),
+ timestamp, group, origin, isApp)));
+ if (!maybeDirectoryMetadata || isApp.IsNull()) {
+ aOriginProps.mNeedsRestore = true;
+ }
+
+ nsCString suffix;
+ QM_WARNONLY_TRY_UNWRAP(const auto maybeDirectoryMetadata2,
+ MOZ_TO_RESULT(GetDirectoryMetadata2(
+ aOriginProps.mDirectory.get(), timestamp, suffix,
+ group, origin, isApp.SetValue())));
+ if (!maybeDirectoryMetadata2) {
+ aOriginProps.mTimestamp = GetOriginLastModifiedTime(aOriginProps);
+ aOriginProps.mNeedsRestore2 = true;
+ } else {
+ aOriginProps.mTimestamp = timestamp;
+ }
+
+ *aRemoved = false;
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom2_1To2_2Helper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ if (aOriginProps.mNeedsRestore) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ aOriginProps.mOriginMetadata)));
+ }
+
+ if (aOriginProps.mNeedsRestore2) {
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+ }
+
+ return NS_OK;
+}
+
+nsresult UpgradeStorageFrom2_1To2_2Helper::PrepareClientDirectory(
+ nsIFile* aFile, const nsAString& aLeafName, bool& aRemoved) {
+ AssertIsOnIOThread();
+
+ if (Client::IsDeprecatedClient(aLeafName)) {
+ QM_WARNING("Deleting deprecated %s client!",
+ NS_ConvertUTF16toUTF8(aLeafName).get());
+
+ QM_TRY(MOZ_TO_RESULT(aFile->Remove(true)));
+
+ aRemoved = true;
+ } else {
+ aRemoved = false;
+ }
+
+ return NS_OK;
+}
+
+nsresult RestoreDirectoryMetadata2Helper::Init() {
+ AssertIsOnIOThread();
+ MOZ_ASSERT(mDirectory);
+
+ nsCOMPtr<nsIFile> parentDir;
+ QM_TRY(MOZ_TO_RESULT(mDirectory->GetParent(getter_AddRefs(parentDir))));
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromFile(*parentDir, fallible);
+ QM_TRY(OkIf(maybePersistenceType.isSome()), Err(NS_ERROR_FAILURE));
+
+ mPersistenceType.init(maybePersistenceType.value());
+
+ return NS_OK;
+}
+
+nsresult RestoreDirectoryMetadata2Helper::RestoreMetadata2File() {
+ OriginProps originProps(WrapMovingNotNull(mDirectory));
+ QM_TRY(MOZ_TO_RESULT(originProps.Init(
+ [&self = *this](const auto& aSpec) { return *self.mPersistenceType; })));
+
+ QM_TRY(OkIf(originProps.mType != OriginProps::eInvalid), NS_ERROR_FAILURE);
+
+ originProps.mTimestamp = GetOriginLastModifiedTime(originProps);
+
+ mOriginProps.AppendElement(std::move(originProps));
+
+ QM_TRY(MOZ_TO_RESULT(ProcessOriginDirectories()));
+
+ return NS_OK;
+}
+
+nsresult RestoreDirectoryMetadata2Helper::ProcessOriginDirectory(
+ const OriginProps& aOriginProps) {
+ AssertIsOnIOThread();
+
+ // We don't have any approach to restore aPersisted, so reset it to false.
+ QM_TRY(MOZ_TO_RESULT(CreateDirectoryMetadata2(
+ *aOriginProps.mDirectory, aOriginProps.mTimestamp,
+ /* aPersisted */ false, aOriginProps.mOriginMetadata)));
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/ActorsParent.h b/dom/quota/ActorsParent.h
new file mode 100644
index 0000000000..4f8fcebd3e
--- /dev/null
+++ b/dom/quota/ActorsParent.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_ActorsParent_h
+#define mozilla_dom_quota_ActorsParent_h
+
+#include "mozilla/dom/quota/Config.h"
+
+namespace mozilla::dom::quota {
+
+class PQuotaParent;
+
+void InitializeQuotaManager();
+
+PQuotaParent* AllocPQuotaParent();
+
+bool DeallocPQuotaParent(PQuotaParent* aActor);
+
+bool RecvShutdownQuotaManager();
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_ActorsParent_h
diff --git a/dom/quota/Assertions.cpp b/dom/quota/Assertions.cpp
new file mode 100644
index 0000000000..a2dd155743
--- /dev/null
+++ b/dom/quota/Assertions.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Assertions.h"
+
+namespace mozilla::dom::quota {
+
+bool IsOnIOThread() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ NS_ASSERTION(quotaManager, "Must have a manager here!");
+
+ bool currentThread;
+ return NS_SUCCEEDED(
+ quotaManager->IOThread()->IsOnCurrentThread(&currentThread)) &&
+ currentThread;
+}
+
+void AssertIsOnIOThread() {
+ NS_ASSERTION(IsOnIOThread(), "Running on the wrong thread!");
+}
+
+void DiagnosticAssertIsOnIOThread() { MOZ_DIAGNOSTIC_ASSERT(IsOnIOThread()); }
+
+void AssertCurrentThreadOwnsQuotaMutex() {
+#ifdef DEBUG
+ QuotaManager* quotaManager = QuotaManager::Get();
+ NS_ASSERTION(quotaManager, "Must have a manager here!");
+
+ quotaManager->AssertCurrentThreadOwnsQuotaMutex();
+#endif
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/Assertions.h b/dom/quota/Assertions.h
new file mode 100644
index 0000000000..d2eacb0c1d
--- /dev/null
+++ b/dom/quota/Assertions.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_ASSERTIONS_H_
+#define DOM_QUOTA_ASSERTIONS_H_
+
+#include <cstdint>
+
+namespace mozilla::dom::quota {
+
+template <typename T>
+void AssertNoOverflow(uint64_t aDest, T aArg);
+
+template <typename T, typename U>
+void AssertNoUnderflow(T aDest, U aArg);
+
+bool IsOnIOThread();
+
+void AssertIsOnIOThread();
+
+void DiagnosticAssertIsOnIOThread();
+
+void AssertCurrentThreadOwnsQuotaMutex();
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_ASSERTIONS_H_
diff --git a/dom/quota/AssertionsImpl.h b/dom/quota/AssertionsImpl.h
new file mode 100644
index 0000000000..77df103311
--- /dev/null
+++ b/dom/quota/AssertionsImpl.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_ASSERTIONSIMPL_H_
+#define DOM_QUOTA_ASSERTIONSIMPL_H_
+
+#include "mozilla/dom/quota/Assertions.h"
+
+#include <type_traits>
+#include "mozilla/Assertions.h"
+
+namespace mozilla::dom::quota {
+
+namespace detail {
+
+template <typename T, bool = std::is_unsigned_v<T>>
+struct IntChecker {
+ static void Assert(T aInt) {
+ static_assert(std::is_integral_v<T>, "Not an integer!");
+ MOZ_ASSERT(aInt >= 0);
+ }
+};
+
+template <typename T>
+struct IntChecker<T, true> {
+ static void Assert(T aInt) {
+ static_assert(std::is_integral_v<T>, "Not an integer!");
+ }
+};
+
+} // namespace detail
+
+template <typename T>
+void AssertNoOverflow(uint64_t aDest, T aArg) {
+ detail::IntChecker<T>::Assert(aDest);
+ detail::IntChecker<T>::Assert(aArg);
+ MOZ_ASSERT(UINT64_MAX - aDest >= uint64_t(aArg));
+}
+
+template <typename T, typename U>
+void AssertNoUnderflow(T aDest, U aArg) {
+ detail::IntChecker<T>::Assert(aDest);
+ detail::IntChecker<T>::Assert(aArg);
+ MOZ_ASSERT(uint64_t(aDest) >= uint64_t(aArg));
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_ASSERTIONSIMPL_H_
diff --git a/dom/quota/CachingDatabaseConnection.cpp b/dom/quota/CachingDatabaseConnection.cpp
new file mode 100644
index 0000000000..cd07dba4e7
--- /dev/null
+++ b/dom/quota/CachingDatabaseConnection.cpp
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/CachingDatabaseConnection.h"
+
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla::dom::quota {
+
+CachingDatabaseConnection::CachingDatabaseConnection(
+ MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection)
+ :
+#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
+ mOwningThread{nsAutoOwningThread{}},
+#endif
+ mStorageConnection(std::move(aStorageConnection)) {
+}
+
+void CachingDatabaseConnection::LazyInit(
+ MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection) {
+#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
+ mOwningThread.init();
+#endif
+ mStorageConnection.init(std::move(aStorageConnection));
+}
+
+Result<CachingDatabaseConnection::CachedStatement, nsresult>
+CachingDatabaseConnection::GetCachedStatement(const nsACString& aQuery) {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(!aQuery.IsEmpty());
+ MOZ_ASSERT(mStorageConnection);
+
+ AUTO_PROFILER_LABEL("CachingDatabaseConnection::GetCachedStatement", DOM);
+
+ QM_TRY_UNWRAP(
+ auto stmt,
+ mCachedStatements.TryLookupOrInsertWith(
+ aQuery, [&]() -> Result<nsCOMPtr<mozIStorageStatement>, nsresult> {
+ const auto extraInfo =
+ ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, aQuery};
+
+ QM_TRY_RETURN(
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, **mStorageConnection,
+ CreateStatement, aQuery),
+ QM_PROPAGATE,
+ ([&aQuery,
+ &storageConnection = **mStorageConnection](const auto&) {
+#ifdef DEBUG
+ nsCString msg;
+ MOZ_ALWAYS_SUCCEEDS(
+ storageConnection.GetLastErrorString(msg));
+
+ nsAutoCString error =
+ "The statement '"_ns + aQuery +
+ "' failed to compile with the error message '"_ns + msg +
+ "'."_ns;
+
+ NS_WARNING(error.get());
+#else
+ (void)aQuery;
+#endif
+ }));
+ }));
+
+ return CachedStatement{this, std::move(stmt), aQuery};
+}
+
+Result<CachingDatabaseConnection::BorrowedStatement, nsresult>
+CachingDatabaseConnection::BorrowCachedStatement(const nsACString& aQuery) {
+ QM_TRY_UNWRAP(auto cachedStatement, GetCachedStatement(aQuery));
+
+ return cachedStatement.Borrow();
+}
+
+nsresult CachingDatabaseConnection::ExecuteCachedStatement(
+ const nsACString& aQuery) {
+ return ExecuteCachedStatement(
+ aQuery, [](auto&) -> Result<Ok, nsresult> { return Ok{}; });
+}
+
+void CachingDatabaseConnection::Close() {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(HasStorageConnection());
+
+ AUTO_PROFILER_LABEL("CachingDatabaseConnection::Close", DOM);
+
+ mCachedStatements.Clear();
+
+ MOZ_ALWAYS_SUCCEEDS((*mStorageConnection)->Close());
+ mStorageConnection.destroy();
+}
+
+#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
+CachingDatabaseConnection::CachedStatement::CachedStatement()
+# ifdef DEBUG
+ : mDEBUGConnection(nullptr)
+# endif
+{
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement);
+}
+
+CachingDatabaseConnection::CachedStatement::~CachedStatement() {
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_DTOR(CachingDatabaseConnection::CachedStatement);
+}
+#endif
+
+CachingDatabaseConnection::CachedStatement::operator bool() const {
+ AssertIsOnConnectionThread();
+
+ return mStatement;
+}
+
+mozIStorageStatement& CachingDatabaseConnection::BorrowedStatement::operator*()
+ const {
+ return *operator->();
+}
+
+mozIStorageStatement* CachingDatabaseConnection::BorrowedStatement::operator->()
+ const {
+ MOZ_ASSERT(mStatement);
+
+ return mStatement;
+}
+
+CachingDatabaseConnection::BorrowedStatement
+CachingDatabaseConnection::CachedStatement::Borrow() const {
+ AssertIsOnConnectionThread();
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ return BorrowedStatement{WrapNotNull(mStatement), mQuery};
+#else
+ return BorrowedStatement{WrapNotNull(mStatement)};
+#endif
+}
+
+CachingDatabaseConnection::CachedStatement::CachedStatement(
+ CachingDatabaseConnection* aConnection,
+ nsCOMPtr<mozIStorageStatement> aStatement, const nsACString& aQuery)
+ : mStatement(std::move(aStatement))
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ ,
+ mQuery(aQuery)
+#endif
+#if defined(DEBUG)
+ ,
+ mDEBUGConnection(aConnection)
+#endif
+{
+#ifdef DEBUG
+ MOZ_ASSERT(aConnection);
+ aConnection->AssertIsOnConnectionThread();
+#endif
+ MOZ_ASSERT(mStatement);
+ AssertIsOnConnectionThread();
+
+ MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement);
+}
+
+void CachingDatabaseConnection::CachedStatement::AssertIsOnConnectionThread()
+ const {
+#ifdef DEBUG
+ if (mDEBUGConnection) {
+ mDEBUGConnection->AssertIsOnConnectionThread();
+ }
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread());
+#endif
+}
+
+Result<CachingDatabaseConnection::BorrowedStatement, nsresult>
+CachingDatabaseConnection::LazyStatement::Borrow() {
+ if (!mCachedStatement) {
+ QM_TRY(Initialize());
+ }
+
+ return mCachedStatement.Borrow();
+}
+
+Result<Ok, nsresult> CachingDatabaseConnection::LazyStatement::Initialize() {
+ QM_TRY_UNWRAP(mCachedStatement, mConnection.GetCachedStatement(mQueryString));
+ return Ok{};
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/CachingDatabaseConnection.h b/dom/quota/CachingDatabaseConnection.h
new file mode 100644
index 0000000000..355baf5293
--- /dev/null
+++ b/dom/quota/CachingDatabaseConnection.h
@@ -0,0 +1,238 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_CACHINGDATABASECONNECTION_H_
+#define DOM_QUOTA_CACHINGDATABASECONNECTION_H_
+
+#include "mozilla/dom/quota/Config.h"
+
+#include "mozStorageHelper.h"
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsHashKeys.h"
+#include "nsInterfaceHashtable.h"
+#include "nsString.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
+
+namespace mozilla::dom::quota {
+
+class CachingDatabaseConnection {
+ public:
+ class CachedStatement;
+
+ // A stack-only RAII wrapper that resets its borrowed statement when the
+ // wrapper goes out of scope. Note it's intentionally not declared MOZ_RAII,
+ // because it actually is used as a temporary in simple cases like
+ // `stmt.Borrow()->Execute()`. It also automatically exposes the current query
+ // to ScopedLogExtraInfo as "query" in builds where this mechanism is active.
+ class MOZ_STACK_CLASS BorrowedStatement : mozStorageStatementScoper {
+ public:
+ mozIStorageStatement& operator*() const;
+
+ MOZ_NONNULL_RETURN mozIStorageStatement* operator->() const
+ MOZ_NO_ADDREF_RELEASE_ON_RETURN;
+
+ BorrowedStatement(BorrowedStatement&& aOther) = default;
+
+ // No funny business allowed.
+ BorrowedStatement& operator=(BorrowedStatement&&) = delete;
+ BorrowedStatement(const BorrowedStatement&) = delete;
+ BorrowedStatement& operator=(const BorrowedStatement&) = delete;
+
+ private:
+ friend class CachedStatement;
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ BorrowedStatement(NotNull<mozIStorageStatement*> aStatement,
+ const nsACString& aQuery)
+ : mozStorageStatementScoper(aStatement),
+ mExtraInfo{ScopedLogExtraInfo::kTagQuery, aQuery} {}
+
+ ScopedLogExtraInfo mExtraInfo;
+#else
+ MOZ_IMPLICIT BorrowedStatement(NotNull<mozIStorageStatement*> aStatement)
+ : mozStorageStatementScoper(aStatement) {}
+#endif
+ };
+
+ class LazyStatement;
+
+ void AssertIsOnConnectionThread() const {
+#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
+ mOwningThread->AssertOwnership("CachingDatabaseConnection not thread-safe");
+#endif
+ }
+
+ bool HasStorageConnection() const {
+ return static_cast<bool>(mStorageConnection);
+ }
+
+ mozIStorageConnection& MutableStorageConnection() const {
+ AssertIsOnConnectionThread();
+ MOZ_ASSERT(mStorageConnection);
+
+ return **mStorageConnection;
+ }
+
+ Result<CachedStatement, nsresult> GetCachedStatement(
+ const nsACString& aQuery);
+
+ Result<BorrowedStatement, nsresult> BorrowCachedStatement(
+ const nsACString& aQuery);
+
+ template <typename BindFunctor>
+ nsresult ExecuteCachedStatement(const nsACString& aQuery,
+ BindFunctor&& aBindFunctor) {
+ QM_TRY_INSPECT(const auto& stmt, BorrowCachedStatement(aQuery));
+ QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*stmt));
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ return NS_OK;
+ }
+
+ nsresult ExecuteCachedStatement(const nsACString& aQuery);
+
+ template <typename BindFunctor>
+ Result<Maybe<BorrowedStatement>, nsresult>
+ BorrowAndExecuteSingleStepStatement(const nsACString& aQuery,
+ BindFunctor&& aBindFunctor);
+
+#ifdef DEBUG
+ ~CachingDatabaseConnection() {
+ MOZ_ASSERT(!mStorageConnection);
+ MOZ_ASSERT(!mCachedStatements.Count());
+ }
+#endif
+
+ protected:
+ explicit CachingDatabaseConnection(
+ MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection);
+
+ CachingDatabaseConnection() = default;
+
+ void LazyInit(
+ MovingNotNull<nsCOMPtr<mozIStorageConnection>> aStorageConnection);
+
+ void Close();
+
+ private:
+#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED
+ LazyInitializedOnce<const nsAutoOwningThread> mOwningThread;
+#endif
+
+ LazyInitializedOnceEarlyDestructible<
+ const NotNull<nsCOMPtr<mozIStorageConnection>>>
+ mStorageConnection;
+ nsInterfaceHashtable<nsCStringHashKey, mozIStorageStatement>
+ mCachedStatements;
+};
+
+class CachingDatabaseConnection::CachedStatement final {
+ friend class CachingDatabaseConnection;
+
+ nsCOMPtr<mozIStorageStatement> mStatement;
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ nsCString mQuery;
+#endif
+
+#ifdef DEBUG
+ CachingDatabaseConnection* mDEBUGConnection;
+#endif
+
+ public:
+#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
+ CachedStatement();
+ ~CachedStatement();
+#else
+ CachedStatement() = default;
+#endif
+
+ void AssertIsOnConnectionThread() const;
+
+ explicit operator bool() const;
+
+ BorrowedStatement Borrow() const;
+
+ private:
+ // Only called by CachingDatabaseConnection.
+ CachedStatement(CachingDatabaseConnection* aConnection,
+ nsCOMPtr<mozIStorageStatement> aStatement,
+ const nsACString& aQuery);
+
+ public:
+#if defined(NS_BUILD_REFCNT_LOGGING)
+ CachedStatement(CachedStatement&& aOther)
+ : mStatement(std::move(aOther.mStatement))
+# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ ,
+ mQuery(std::move(aOther.mQuery))
+# endif
+# ifdef DEBUG
+ ,
+ mDEBUGConnection(aOther.mDEBUGConnection)
+# endif
+ {
+ MOZ_COUNT_CTOR(CachingDatabaseConnection::CachedStatement);
+ }
+#else
+ CachedStatement(CachedStatement&&) = default;
+#endif
+
+ CachedStatement& operator=(CachedStatement&&) = default;
+
+ // No funny business allowed.
+ CachedStatement(const CachedStatement&) = delete;
+ CachedStatement& operator=(const CachedStatement&) = delete;
+};
+
+class CachingDatabaseConnection::LazyStatement final {
+ public:
+ LazyStatement(CachingDatabaseConnection& aConnection,
+ const nsACString& aQueryString)
+ : mConnection{aConnection}, mQueryString{aQueryString} {}
+
+ Result<CachingDatabaseConnection::BorrowedStatement, nsresult> Borrow();
+
+ template <typename BindFunctor>
+ Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult>
+ BorrowAndExecuteSingleStep(BindFunctor&& aBindFunctor) {
+ QM_TRY_UNWRAP(auto borrowedStatement, Borrow());
+
+ QM_TRY(std::forward<BindFunctor>(aBindFunctor)(*borrowedStatement));
+
+ QM_TRY_INSPECT(
+ const bool& hasResult,
+ MOZ_TO_RESULT_INVOKE_MEMBER(&*borrowedStatement, ExecuteStep));
+
+ return hasResult ? Some(std::move(borrowedStatement)) : Nothing{};
+ }
+
+ private:
+ Result<Ok, nsresult> Initialize();
+
+ CachingDatabaseConnection& mConnection;
+ const nsCString mQueryString;
+ CachingDatabaseConnection::CachedStatement mCachedStatement;
+};
+
+template <typename BindFunctor>
+Result<Maybe<CachingDatabaseConnection::BorrowedStatement>, nsresult>
+CachingDatabaseConnection::BorrowAndExecuteSingleStepStatement(
+ const nsACString& aQuery, BindFunctor&& aBindFunctor) {
+ return LazyStatement{*this, aQuery}.BorrowAndExecuteSingleStep(
+ std::forward<BindFunctor>(aBindFunctor));
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_CACHINGDATABASECONNECTION_H_
diff --git a/dom/quota/CanonicalQuotaObject.cpp b/dom/quota/CanonicalQuotaObject.cpp
new file mode 100644
index 0000000000..388f20e541
--- /dev/null
+++ b/dom/quota/CanonicalQuotaObject.cpp
@@ -0,0 +1,331 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanonicalQuotaObject.h"
+
+#include "GroupInfo.h"
+#include "GroupInfoPair.h"
+#include "mozilla/dom/StorageActivityService.h"
+#include "mozilla/dom/quota/AssertionsImpl.h"
+#include "mozilla/dom/quota/DirectoryLock.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "OriginInfo.h"
+
+namespace mozilla::dom::quota {
+
+NS_IMETHODIMP_(MozExternalRefCountType) CanonicalQuotaObject::AddRef() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ if (!quotaManager) {
+ NS_ERROR("Null quota manager, this shouldn't happen, possible leak!");
+
+ return ++mRefCnt;
+ }
+
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ return ++mRefCnt;
+}
+
+NS_IMETHODIMP_(MozExternalRefCountType) CanonicalQuotaObject::Release() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ if (!quotaManager) {
+ NS_ERROR("Null quota manager, this shouldn't happen, possible leak!");
+
+ nsrefcnt count = --mRefCnt;
+ if (count == 0) {
+ mRefCnt = 1;
+ delete this;
+ return 0;
+ }
+
+ return mRefCnt;
+ }
+
+ {
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ --mRefCnt;
+
+ if (mRefCnt > 0) {
+ return mRefCnt;
+ }
+
+ if (mOriginInfo) {
+ mOriginInfo->mCanonicalQuotaObjects.Remove(mPath);
+ }
+ }
+
+ delete this;
+ return 0;
+}
+
+bool CanonicalQuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ return LockedMaybeUpdateSize(aSize, aTruncate);
+}
+
+bool CanonicalQuotaObject::IncreaseSize(int64_t aDelta) {
+ MOZ_ASSERT(aDelta >= 0);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ AssertNoOverflow(mSize, aDelta);
+ int64_t size = mSize + aDelta;
+
+ return LockedMaybeUpdateSize(size, /* aTruncate */ false);
+}
+
+void CanonicalQuotaObject::DisableQuotaCheck() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ mQuotaCheckDisabled = true;
+}
+
+void CanonicalQuotaObject::EnableQuotaCheck() {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ MutexAutoLock lock(quotaManager->mQuotaMutex);
+
+ mQuotaCheckDisabled = false;
+}
+
+bool CanonicalQuotaObject::LockedMaybeUpdateSize(int64_t aSize,
+ bool aTruncate) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ quotaManager->mQuotaMutex.AssertCurrentThreadOwns();
+
+ if (mWritingDone == false && mOriginInfo) {
+ mWritingDone = true;
+ StorageActivityService::SendActivity(mOriginInfo->mOrigin);
+ }
+
+ if (mQuotaCheckDisabled) {
+ return true;
+ }
+
+ if (mSize == aSize) {
+ return true;
+ }
+
+ if (!mOriginInfo) {
+ mSize = aSize;
+ return true;
+ }
+
+ GroupInfo* groupInfo = mOriginInfo->mGroupInfo;
+ MOZ_ASSERT(groupInfo);
+
+ if (mSize > aSize) {
+ if (aTruncate) {
+ const int64_t delta = mSize - aSize;
+
+ AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, delta);
+ quotaManager->mTemporaryStorageUsage -= delta;
+
+ if (!mOriginInfo->LockedPersisted()) {
+ AssertNoUnderflow(groupInfo->mUsage, delta);
+ groupInfo->mUsage -= delta;
+ }
+
+ AssertNoUnderflow(mOriginInfo->mUsage, delta);
+ mOriginInfo->mUsage -= delta;
+
+ MOZ_ASSERT(mOriginInfo->mClientUsages[mClientType].isSome());
+ AssertNoUnderflow(mOriginInfo->mClientUsages[mClientType].value(), delta);
+ mOriginInfo->mClientUsages[mClientType] =
+ Some(mOriginInfo->mClientUsages[mClientType].value() - delta);
+
+ mSize = aSize;
+ }
+ return true;
+ }
+
+ MOZ_ASSERT(mSize < aSize);
+
+ RefPtr<GroupInfo> complementaryGroupInfo =
+ groupInfo->mGroupInfoPair->LockedGetGroupInfo(
+ ComplementaryPersistenceType(groupInfo->mPersistenceType));
+
+ uint64_t delta = aSize - mSize;
+
+ AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta);
+ uint64_t newClientUsage =
+ mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta;
+
+ AssertNoOverflow(mOriginInfo->mUsage, delta);
+ uint64_t newUsage = mOriginInfo->mUsage + delta;
+
+ // Temporary storage has no limit for origin usage (there's a group and the
+ // global limit though).
+
+ uint64_t newGroupUsage = groupInfo->mUsage;
+ if (!mOriginInfo->LockedPersisted()) {
+ AssertNoOverflow(groupInfo->mUsage, delta);
+ newGroupUsage += delta;
+
+ uint64_t groupUsage = groupInfo->mUsage;
+ if (complementaryGroupInfo) {
+ AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
+ groupUsage += complementaryGroupInfo->mUsage;
+ }
+
+ // Temporary storage has a hard limit for group usage (20 % of the global
+ // limit).
+ AssertNoOverflow(groupUsage, delta);
+ if (groupUsage + delta > quotaManager->GetGroupLimit()) {
+ return false;
+ }
+ }
+
+ AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta);
+ uint64_t newTemporaryStorageUsage =
+ quotaManager->mTemporaryStorageUsage + delta;
+
+ if (newTemporaryStorageUsage > quotaManager->mTemporaryStorageLimit) {
+ // This will block the thread without holding the lock while waitting.
+
+ AutoTArray<RefPtr<OriginDirectoryLock>, 10> locks;
+ uint64_t sizeToBeFreed;
+
+ if (::mozilla::ipc::IsOnBackgroundThread()) {
+ MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);
+
+ sizeToBeFreed = quotaManager->CollectOriginsForEviction(delta, locks);
+ } else {
+ sizeToBeFreed =
+ quotaManager->LockedCollectOriginsForEviction(delta, locks);
+ }
+
+ if (!sizeToBeFreed) {
+ uint64_t usage = quotaManager->mTemporaryStorageUsage;
+
+ MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);
+
+ quotaManager->NotifyStoragePressure(usage);
+
+ return false;
+ }
+
+ NS_ASSERTION(sizeToBeFreed >= delta, "Huh?");
+
+ {
+ MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);
+
+ for (const auto& lock : locks) {
+ quotaManager->DeleteFilesForOrigin(lock->GetPersistenceType(),
+ lock->Origin());
+ }
+ }
+
+ // Relocked.
+
+ NS_ASSERTION(mOriginInfo, "How come?!");
+
+ for (const auto& lock : locks) {
+ MOZ_ASSERT(!(lock->GetPersistenceType() == groupInfo->mPersistenceType &&
+ lock->Origin() == mOriginInfo->mOrigin),
+ "Deleted itself!");
+
+ quotaManager->LockedRemoveQuotaForOrigin(lock->OriginMetadata());
+ }
+
+ // We unlocked and relocked several times so we need to recompute all the
+ // essential variables and recheck the group limit.
+
+ AssertNoUnderflow(aSize, mSize);
+ delta = aSize - mSize;
+
+ AssertNoOverflow(mOriginInfo->mClientUsages[mClientType].valueOr(0), delta);
+ newClientUsage = mOriginInfo->mClientUsages[mClientType].valueOr(0) + delta;
+
+ AssertNoOverflow(mOriginInfo->mUsage, delta);
+ newUsage = mOriginInfo->mUsage + delta;
+
+ newGroupUsage = groupInfo->mUsage;
+ if (!mOriginInfo->LockedPersisted()) {
+ AssertNoOverflow(groupInfo->mUsage, delta);
+ newGroupUsage += delta;
+
+ uint64_t groupUsage = groupInfo->mUsage;
+ if (complementaryGroupInfo) {
+ AssertNoOverflow(groupUsage, complementaryGroupInfo->mUsage);
+ groupUsage += complementaryGroupInfo->mUsage;
+ }
+
+ AssertNoOverflow(groupUsage, delta);
+ if (groupUsage + delta > quotaManager->GetGroupLimit()) {
+ // Unfortunately some other thread increased the group usage in the
+ // meantime and we are not below the group limit anymore.
+
+ // However, the origin eviction must be finalized in this case too.
+ MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);
+
+ quotaManager->FinalizeOriginEviction(std::move(locks));
+
+ return false;
+ }
+ }
+
+ AssertNoOverflow(quotaManager->mTemporaryStorageUsage, delta);
+ newTemporaryStorageUsage = quotaManager->mTemporaryStorageUsage + delta;
+
+ NS_ASSERTION(
+ newTemporaryStorageUsage <= quotaManager->mTemporaryStorageLimit,
+ "How come?!");
+
+ // Ok, we successfully freed enough space and the operation can continue
+ // without throwing the quota error.
+ mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage);
+
+ mOriginInfo->mUsage = newUsage;
+ if (!mOriginInfo->LockedPersisted()) {
+ groupInfo->mUsage = newGroupUsage;
+ }
+ quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;
+ ;
+
+ // Some other thread could increase the size in the meantime, but no more
+ // than this one.
+ MOZ_ASSERT(mSize < aSize);
+ mSize = aSize;
+
+ // Finally, release IO thread only objects and allow next synchronized
+ // ops for the evicted origins.
+ MutexAutoUnlock autoUnlock(quotaManager->mQuotaMutex);
+
+ quotaManager->FinalizeOriginEviction(std::move(locks));
+
+ return true;
+ }
+
+ mOriginInfo->mClientUsages[mClientType] = Some(newClientUsage);
+
+ mOriginInfo->mUsage = newUsage;
+ if (!mOriginInfo->LockedPersisted()) {
+ groupInfo->mUsage = newGroupUsage;
+ }
+ quotaManager->mTemporaryStorageUsage = newTemporaryStorageUsage;
+
+ mSize = aSize;
+
+ return true;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/CanonicalQuotaObject.h b/dom/quota/CanonicalQuotaObject.h
new file mode 100644
index 0000000000..c47862f887
--- /dev/null
+++ b/dom/quota/CanonicalQuotaObject.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_CANONICALQUOTAOBJECT_H_
+#define DOM_QUOTA_CANONICALQUOTAOBJECT_H_
+
+// Local includes
+#include "Client.h"
+
+// Global includes
+#include <cstdint>
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "nsCOMPtr.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file.
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+namespace mozilla::dom::quota {
+
+class OriginInfo;
+class QuotaManager;
+
+class CanonicalQuotaObject final : public QuotaObject {
+ friend class OriginInfo;
+ friend class QuotaManager;
+
+ class StoragePressureRunnable;
+
+ public:
+ NS_IMETHOD_(MozExternalRefCountType) AddRef() override;
+
+ NS_IMETHOD_(MozExternalRefCountType) Release() override;
+
+ const nsAString& Path() const override { return mPath; }
+
+ [[nodiscard]] bool MaybeUpdateSize(int64_t aSize, bool aTruncate) override;
+
+ bool IncreaseSize(int64_t aDelta) override;
+
+ void DisableQuotaCheck() override;
+
+ void EnableQuotaCheck() override;
+
+ private:
+ CanonicalQuotaObject(OriginInfo* aOriginInfo, Client::Type aClientType,
+ const nsAString& aPath, int64_t aSize)
+ : QuotaObject(/* aIsRemote */ false),
+ mOriginInfo(aOriginInfo),
+ mPath(aPath),
+ mSize(aSize),
+ mClientType(aClientType),
+ mQuotaCheckDisabled(false),
+ mWritingDone(false) {
+ MOZ_COUNT_CTOR(CanonicalQuotaObject);
+ }
+
+ MOZ_COUNTED_DTOR(CanonicalQuotaObject)
+
+ already_AddRefed<QuotaObject> LockedAddRef() {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ ++mRefCnt;
+
+ RefPtr<QuotaObject> result = dont_AddRef(this);
+ return result.forget();
+ }
+
+ bool LockedMaybeUpdateSize(int64_t aSize, bool aTruncate);
+
+ mozilla::ThreadSafeAutoRefCnt mRefCnt;
+
+ OriginInfo* mOriginInfo;
+ nsString mPath;
+ int64_t mSize;
+ Client::Type mClientType;
+ bool mQuotaCheckDisabled;
+ bool mWritingDone;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_CANONICALQUOTAOBJECT_H_
diff --git a/dom/quota/CheckedUnsafePtr.h b/dom/quota/CheckedUnsafePtr.h
new file mode 100644
index 0000000000..70d18b4c7c
--- /dev/null
+++ b/dom/quota/CheckedUnsafePtr.h
@@ -0,0 +1,405 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Diagnostic class template that helps finding dangling pointers.
+
+#ifndef mozilla_CheckedUnsafePtr_h
+#define mozilla_CheckedUnsafePtr_h
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DataMutex.h"
+#include "nsTArray.h"
+
+#include <cstddef>
+#include <type_traits>
+#include <utility>
+
+namespace mozilla {
+enum class CheckingSupport {
+ Disabled,
+ Enabled,
+};
+
+template <typename T>
+class CheckedUnsafePtr;
+
+namespace detail {
+class CheckedUnsafePtrBaseCheckingEnabled;
+
+struct CheckedUnsafePtrCheckData {
+ using Data = nsTArray<CheckedUnsafePtrBaseCheckingEnabled*>;
+
+ DataMutex<Data> mPtrs{"mozilla::SupportsCheckedUnsafePtr"};
+};
+
+class CheckedUnsafePtrBaseCheckingEnabled {
+ friend class CheckedUnsafePtrBaseAccess;
+
+ protected:
+ constexpr CheckedUnsafePtrBaseCheckingEnabled() = default;
+ CheckedUnsafePtrBaseCheckingEnabled(
+ const CheckedUnsafePtrBaseCheckingEnabled& aOther) = default;
+
+ // When copying an CheckedUnsafePtr, its mIsDangling member must be copied as
+ // well; otherwise the new copy might try to dereference a dangling pointer
+ // when destructed.
+ void CopyDanglingFlagIfAvailableFrom(
+ const CheckedUnsafePtrBaseCheckingEnabled& aOther) {
+ mIsDangling = aOther.mIsDangling;
+ }
+
+ template <typename Ptr>
+ using DisableForCheckedUnsafePtr = std::enable_if_t<
+ !std::is_base_of<CheckedUnsafePtrBaseCheckingEnabled, Ptr>::value>;
+
+ // When constructing an CheckedUnsafePtr from a different kind of pointer it's
+ // not possible to determine whether it's dangling; therefore it's undefined
+ // behavior to construct one from a dangling pointer, and we assume that any
+ // CheckedUnsafePtr thus constructed is not dangling.
+ template <typename Ptr>
+ DisableForCheckedUnsafePtr<Ptr> CopyDanglingFlagIfAvailableFrom(const Ptr&) {}
+
+ template <typename F>
+ void WithCheckedUnsafePtrsImpl(CheckedUnsafePtrCheckData* const aRawPtr,
+ F&& aClosure) {
+ if (!mIsDangling && aRawPtr) {
+ const auto CheckedUnsafePtrs = aRawPtr->mPtrs.Lock();
+ aClosure(this, *CheckedUnsafePtrs);
+ }
+ }
+
+ private:
+ bool mIsDangling = false;
+};
+
+class CheckedUnsafePtrBaseAccess {
+ protected:
+ static void SetDanglingFlag(CheckedUnsafePtrBaseCheckingEnabled& aBase) {
+ aBase.mIsDangling = true;
+ }
+};
+
+template <typename T, CheckingSupport = T::SupportsChecking::value>
+class CheckedUnsafePtrBase;
+
+template <typename T, typename U, typename S = std::nullptr_t>
+using EnableIfCompatible = std::enable_if_t<
+ std::is_base_of<
+ T, std::remove_reference_t<decltype(*std::declval<U>())>>::value,
+ S>;
+
+template <typename T>
+class CheckedUnsafePtrBase<T, CheckingSupport::Enabled>
+ : detail::CheckedUnsafePtrBaseCheckingEnabled {
+ public:
+ MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const std::nullptr_t = nullptr)
+ : mRawPtr(nullptr) {}
+
+ template <typename U, typename = EnableIfCompatible<T, U>>
+ MOZ_IMPLICIT CheckedUnsafePtrBase(const U& aPtr) {
+ Set(aPtr);
+ }
+
+ CheckedUnsafePtrBase(const CheckedUnsafePtrBase& aOther) {
+ Set(aOther.Downcast());
+ }
+
+ ~CheckedUnsafePtrBase() { Reset(); }
+
+ CheckedUnsafePtr<T>& operator=(const std::nullptr_t) {
+ Reset();
+ return Downcast();
+ }
+
+ template <typename U>
+ EnableIfCompatible<T, U, CheckedUnsafePtr<T>&> operator=(const U& aPtr) {
+ Replace(aPtr);
+ return Downcast();
+ }
+
+ CheckedUnsafePtrBase& operator=(const CheckedUnsafePtrBase& aOther) {
+ if (&aOther != this) {
+ Replace(aOther.Downcast());
+ }
+ return Downcast();
+ }
+
+ constexpr T* get() const { return mRawPtr; }
+
+ private:
+ template <typename U, CheckingSupport>
+ friend class CheckedUnsafePtrBase;
+
+ CheckedUnsafePtr<T>& Downcast() {
+ return static_cast<CheckedUnsafePtr<T>&>(*this);
+ }
+ const CheckedUnsafePtr<T>& Downcast() const {
+ return static_cast<const CheckedUnsafePtr<T>&>(*this);
+ }
+
+ using Base = detail::CheckedUnsafePtrBaseCheckingEnabled;
+
+ template <typename U>
+ void Replace(const U& aPtr) {
+ Reset();
+ Set(aPtr);
+ }
+
+ void Reset() {
+ WithCheckedUnsafePtrs(
+ [](Base* const aSelf,
+ detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) {
+ const auto index = aCheckedUnsafePtrs.IndexOf(aSelf);
+ aCheckedUnsafePtrs.UnorderedRemoveElementAt(index);
+ });
+ mRawPtr = nullptr;
+ }
+
+ template <typename U>
+ void Set(const U& aPtr) {
+ this->CopyDanglingFlagIfAvailableFrom(aPtr);
+ mRawPtr = &*aPtr;
+ WithCheckedUnsafePtrs(
+ [](Base* const aSelf,
+ detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) {
+ aCheckedUnsafePtrs.AppendElement(aSelf);
+ });
+ }
+
+ template <typename F>
+ void WithCheckedUnsafePtrs(F&& aClosure) {
+ this->WithCheckedUnsafePtrsImpl(mRawPtr, std::forward<F>(aClosure));
+ }
+
+ T* mRawPtr;
+};
+
+template <typename T>
+class CheckedUnsafePtrBase<T, CheckingSupport::Disabled> {
+ public:
+ MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const std::nullptr_t = nullptr)
+ : mRawPtr(nullptr) {}
+
+ template <typename U, typename = EnableIfCompatible<T, U>>
+ MOZ_IMPLICIT constexpr CheckedUnsafePtrBase(const U& aPtr) : mRawPtr(aPtr) {}
+
+ constexpr CheckedUnsafePtr<T>& operator=(const std::nullptr_t) {
+ mRawPtr = nullptr;
+ return Downcast();
+ }
+
+ template <typename U>
+ constexpr EnableIfCompatible<T, U, CheckedUnsafePtr<T>&> operator=(
+ const U& aPtr) {
+ mRawPtr = aPtr;
+ return Downcast();
+ }
+
+ constexpr T* get() const { return mRawPtr; }
+
+ private:
+ constexpr CheckedUnsafePtr<T>& Downcast() {
+ return static_cast<CheckedUnsafePtr<T>&>(*this);
+ }
+
+ T* mRawPtr;
+};
+} // namespace detail
+
+class CheckingPolicyAccess {
+ protected:
+ template <typename CheckingPolicy>
+ static void NotifyCheckFailure(CheckingPolicy& aPolicy) {
+ aPolicy.NotifyCheckFailure();
+ }
+};
+
+template <typename Derived>
+class CheckCheckedUnsafePtrs : private CheckingPolicyAccess,
+ private detail::CheckedUnsafePtrBaseAccess {
+ public:
+ using SupportsChecking =
+ std::integral_constant<CheckingSupport, CheckingSupport::Enabled>;
+
+ protected:
+ static constexpr bool ShouldCheck() {
+ static_assert(
+ std::is_base_of<CheckCheckedUnsafePtrs, Derived>::value,
+ "cannot instantiate with a type that's not a subclass of this class");
+ return true;
+ }
+
+ void Check(detail::CheckedUnsafePtrCheckData::Data& aCheckedUnsafePtrs) {
+ if (!aCheckedUnsafePtrs.IsEmpty()) {
+ for (auto* const aCheckedUnsafePtrBase : aCheckedUnsafePtrs) {
+ SetDanglingFlag(*aCheckedUnsafePtrBase);
+ }
+ NotifyCheckFailure(*static_cast<Derived*>(this));
+ }
+ }
+};
+
+class CrashOnDanglingCheckedUnsafePtr
+ : public CheckCheckedUnsafePtrs<CrashOnDanglingCheckedUnsafePtr> {
+ friend class mozilla::CheckingPolicyAccess;
+ void NotifyCheckFailure() { MOZ_CRASH("Found dangling CheckedUnsafePtr"); }
+};
+
+struct DoNotCheckCheckedUnsafePtrs {
+ using SupportsChecking =
+ std::integral_constant<CheckingSupport, CheckingSupport::Disabled>;
+};
+
+namespace detail {
+// Template parameter CheckingSupport controls the inclusion of
+// CheckedUnsafePtrCheckData as a subobject of instantiations of
+// SupportsCheckedUnsafePtr, ensuring that choosing a policy without checking
+// support incurs no size overhead.
+template <typename CheckingPolicy,
+ CheckingSupport = CheckingPolicy::SupportsChecking::value>
+class SupportCheckedUnsafePtrImpl;
+
+template <typename CheckingPolicy>
+class SupportCheckedUnsafePtrImpl<CheckingPolicy, CheckingSupport::Disabled>
+ : public CheckingPolicy {
+ protected:
+ template <typename... Args>
+ explicit SupportCheckedUnsafePtrImpl(Args&&... aArgs)
+ : CheckingPolicy(std::forward<Args>(aArgs)...) {}
+};
+
+template <typename CheckingPolicy>
+class SupportCheckedUnsafePtrImpl<CheckingPolicy, CheckingSupport::Enabled>
+ : public CheckedUnsafePtrCheckData, public CheckingPolicy {
+ template <typename T>
+ friend class CheckedUnsafePtr;
+
+ protected:
+ template <typename... Args>
+ explicit SupportCheckedUnsafePtrImpl(Args&&... aArgs)
+ : CheckingPolicy(std::forward<Args>(aArgs)...) {}
+
+ ~SupportCheckedUnsafePtrImpl() {
+ if (this->ShouldCheck()) {
+ const auto ptrs = mPtrs.Lock();
+ this->Check(*ptrs);
+ }
+ }
+};
+
+struct SupportsCheckedUnsafePtrTag {};
+} // namespace detail
+
+template <typename Condition,
+ typename CheckingPolicy = CrashOnDanglingCheckedUnsafePtr>
+using CheckIf = std::conditional_t<Condition::value, CheckingPolicy,
+ DoNotCheckCheckedUnsafePtrs>;
+
+using DiagnosticAssertEnabled = std::integral_constant<bool,
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ true
+#else
+ false
+#endif
+ >;
+
+// A T class that publicly inherits from an instantiation of
+// SupportsCheckedUnsafePtr and its subclasses can be pointed to by smart
+// pointers of type CheckedUnsafePtr<T>. Whenever such a smart pointer is
+// created, its existence is tracked by the pointee according to its
+// CheckingPolicy. When the pointee goes out of scope it then uses the its
+// CheckingPolicy to verify that no CheckedUnsafePtr pointers are left pointing
+// to it.
+//
+// The CheckingPolicy type is used to control the kind of verification that
+// happen at the end of the object's lifetime. By default, debug builds always
+// check for dangling CheckedUnsafePtr pointers and assert that none are found,
+// while release builds forgo all checks. (Release builds incur no size or
+// runtime penalties compared to bare pointers.)
+template <typename CheckingPolicy>
+class SupportsCheckedUnsafePtr
+ : public detail::SupportCheckedUnsafePtrImpl<CheckingPolicy>,
+ public detail::SupportsCheckedUnsafePtrTag {
+ public:
+ template <typename... Args>
+ explicit SupportsCheckedUnsafePtr(Args&&... aArgs)
+ : detail::SupportCheckedUnsafePtrImpl<CheckingPolicy>(
+ std::forward<Args>(aArgs)...) {}
+};
+
+// CheckedUnsafePtr<T> is a smart pointer class that helps detect dangling
+// pointers in cases where such pointers are not allowed. In order to use it,
+// the pointee T must publicly inherit from an instantiation of
+// SupportsCheckedUnsafePtr. An CheckedUnsafePtr<T> can be used anywhere a T*
+// can be used, has the same size, and imposes no additional thread-safety
+// restrictions.
+template <typename T>
+class CheckedUnsafePtr : public detail::CheckedUnsafePtrBase<T> {
+ static_assert(
+ std::is_base_of<detail::SupportsCheckedUnsafePtrTag, T>::value,
+ "type T must be derived from instantiation of SupportsCheckedUnsafePtr");
+
+ public:
+ using detail::CheckedUnsafePtrBase<T>::CheckedUnsafePtrBase;
+ using detail::CheckedUnsafePtrBase<T>::get;
+
+ constexpr T* operator->() const { return get(); }
+
+ constexpr T& operator*() const { return *get(); }
+
+ MOZ_IMPLICIT constexpr operator T*() const { return get(); }
+
+ template <typename U>
+ constexpr bool operator==(
+ detail::EnableIfCompatible<T, U, const U&> aRhs) const {
+ return get() == aRhs.get();
+ }
+
+ template <typename U>
+ friend constexpr bool operator==(
+ detail::EnableIfCompatible<T, U, const U&> aLhs,
+ const CheckedUnsafePtr& aRhs) {
+ return aRhs == aLhs;
+ }
+
+ template <typename U>
+ constexpr bool operator!=(
+ detail::EnableIfCompatible<T, U, const U&> aRhs) const {
+ return !(*this == aRhs);
+ }
+
+ template <typename U>
+ friend constexpr bool operator!=(
+ detail::EnableIfCompatible<T, U, const U&> aLhs,
+ const CheckedUnsafePtr& aRhs) {
+ return aRhs != aLhs;
+ }
+};
+
+} // namespace mozilla
+
+// nsTArray<T> requires by default that T can be safely moved with std::memmove.
+// Since CheckedUnsafePtr<T> has a non-trivial copy constructor, it has to opt
+// into nsTArray<T> using them.
+template <typename T>
+struct nsTArray_RelocationStrategy<mozilla::CheckedUnsafePtr<T>> {
+ using Type = std::conditional_t<
+ T::SupportsChecking::value == mozilla::CheckingSupport::Enabled,
+ nsTArray_RelocateUsingMoveConstructor<mozilla::CheckedUnsafePtr<T>>,
+ nsTArray_RelocateUsingMemutils>;
+};
+
+template <typename T>
+struct nsTArray_RelocationStrategy<
+ mozilla::NotNull<mozilla::CheckedUnsafePtr<T>>> {
+ using Type =
+ std::conditional_t<T::SupportsChecking::value ==
+ mozilla::CheckingSupport::Enabled,
+ nsTArray_RelocateUsingMoveConstructor<
+ mozilla::NotNull<mozilla::CheckedUnsafePtr<T>>>,
+ nsTArray_RelocateUsingMemutils>;
+};
+
+#endif // mozilla_CheckedUnsafePtr_h
diff --git a/dom/quota/CipherStrategy.h b/dom/quota/CipherStrategy.h
new file mode 100644
index 0000000000..495bdb7528
--- /dev/null
+++ b/dom/quota/CipherStrategy.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_CipherStrategy_h
+#define mozilla_dom_quota_CipherStrategy_h
+
+namespace mozilla::dom::quota {
+
+enum class CipherMode { Encrypt, Decrypt };
+
+// An implementation of the CipherStrategy concept must provide the following
+// data members:
+//
+// static constexpr size_t BlockPrefixLength;
+// static constexpr size_t BasicBlockSize;
+//
+// It must provide the following member types:
+//
+// KeyType
+// BlockPrefixType, which must be an InputRange of type uint8_t and a
+// SizedRange of size BlockPrefixLength
+//
+// It must provide the following member functions with compatible signatures:
+//
+// static Result<KeyType, nsresult> GenerateKey();
+//
+// nsresult Cipher(const CipherMode aMode, const KeyType& aKey,
+// Span<uint8_t> aIv, Span<const uint8_t> aIn,
+// Span<uint8_t> aOut);
+//
+// BlockPrefixType MakeBlockPrefix();
+//
+// Span<const uint8_t> SerializeKey(const KeyType& aKey);
+//
+// KeyType DeserializeKey(const Span<const uint8_t>& aSerializedKey);
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/Client.cpp b/dom/quota/Client.cpp
new file mode 100644
index 0000000000..87da8815cc
--- /dev/null
+++ b/dom/quota/Client.cpp
@@ -0,0 +1,299 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Client.h"
+
+// Global includes
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+
+namespace mozilla::dom::quota {
+
+using mozilla::ipc::AssertIsOnBackgroundThread;
+using mozilla::ipc::IsOnBackgroundThread;
+
+namespace {
+
+const char kIDBPrefix = 'I';
+const char kDOMCachePrefix = 'C';
+const char kSDBPrefix = 'S';
+const char kFILESYSTEMPrefix = 'F';
+const char kLSPrefix = 'L';
+
+template <Client::Type type>
+struct ClientTypeTraits;
+
+template <>
+struct ClientTypeTraits<Client::Type::IDB> {
+ template <typename T>
+ static void To(T& aData) {
+ aData.AssignLiteral(IDB_DIRECTORY_NAME);
+ }
+
+ static void To(char& aData) { aData = kIDBPrefix; }
+
+ template <typename T>
+ static bool From(const T& aData) {
+ return aData.EqualsLiteral(IDB_DIRECTORY_NAME);
+ }
+
+ static bool From(char aData) { return aData == kIDBPrefix; }
+};
+
+template <>
+struct ClientTypeTraits<Client::Type::DOMCACHE> {
+ template <typename T>
+ static void To(T& aData) {
+ aData.AssignLiteral(DOMCACHE_DIRECTORY_NAME);
+ }
+
+ static void To(char& aData) { aData = kDOMCachePrefix; }
+
+ template <typename T>
+ static bool From(const T& aData) {
+ return aData.EqualsLiteral(DOMCACHE_DIRECTORY_NAME);
+ }
+
+ static bool From(char aData) { return aData == kDOMCachePrefix; }
+};
+
+template <>
+struct ClientTypeTraits<Client::Type::SDB> {
+ template <typename T>
+ static void To(T& aData) {
+ aData.AssignLiteral(SDB_DIRECTORY_NAME);
+ }
+
+ static void To(char& aData) { aData = kSDBPrefix; }
+
+ template <typename T>
+ static bool From(const T& aData) {
+ return aData.EqualsLiteral(SDB_DIRECTORY_NAME);
+ }
+
+ static bool From(char aData) { return aData == kSDBPrefix; }
+};
+
+template <>
+struct ClientTypeTraits<Client::Type::FILESYSTEM> {
+ template <typename T>
+ static void To(T& aData) {
+ aData.AssignLiteral(FILESYSTEM_DIRECTORY_NAME);
+ }
+
+ static void To(char& aData) { aData = kFILESYSTEMPrefix; }
+
+ template <typename T>
+ static bool From(const T& aData) {
+ return aData.EqualsLiteral(FILESYSTEM_DIRECTORY_NAME);
+ }
+
+ static bool From(char aData) { return aData == kFILESYSTEMPrefix; }
+};
+
+template <>
+struct ClientTypeTraits<Client::Type::LS> {
+ template <typename T>
+ static void To(T& aData) {
+ aData.AssignLiteral(LS_DIRECTORY_NAME);
+ }
+
+ static void To(char& aData) { aData = kLSPrefix; }
+
+ template <typename T>
+ static bool From(const T& aData) {
+ return aData.EqualsLiteral(LS_DIRECTORY_NAME);
+ }
+
+ static bool From(char aData) { return aData == kLSPrefix; }
+};
+
+template <typename T>
+bool TypeTo_impl(Client::Type aType, T& aData) {
+ switch (aType) {
+ case Client::IDB:
+ ClientTypeTraits<Client::Type::IDB>::To(aData);
+ return true;
+
+ case Client::DOMCACHE:
+ ClientTypeTraits<Client::Type::DOMCACHE>::To(aData);
+ return true;
+
+ case Client::SDB:
+ ClientTypeTraits<Client::Type::SDB>::To(aData);
+ return true;
+
+ case Client::FILESYSTEM:
+ ClientTypeTraits<Client::Type::FILESYSTEM>::To(aData);
+ return true;
+
+ case Client::LS:
+ if (CachedNextGenLocalStorageEnabled()) {
+ ClientTypeTraits<Client::Type::LS>::To(aData);
+ return true;
+ }
+ [[fallthrough]];
+
+ case Client::TYPE_MAX:
+ default:
+ return false;
+ }
+
+ MOZ_CRASH("Should never get here!");
+}
+
+template <typename T>
+bool TypeFrom_impl(const T& aData, Client::Type& aType) {
+ if (ClientTypeTraits<Client::Type::IDB>::From(aData)) {
+ aType = Client::IDB;
+ return true;
+ }
+
+ if (ClientTypeTraits<Client::Type::DOMCACHE>::From(aData)) {
+ aType = Client::DOMCACHE;
+ return true;
+ }
+
+ if (ClientTypeTraits<Client::Type::SDB>::From(aData)) {
+ aType = Client::SDB;
+ return true;
+ }
+
+ if (ClientTypeTraits<Client::Type::FILESYSTEM>::From(aData)) {
+ aType = Client::FILESYSTEM;
+ return true;
+ }
+
+ if (CachedNextGenLocalStorageEnabled() &&
+ ClientTypeTraits<Client::Type::LS>::From(aData)) {
+ aType = Client::LS;
+ return true;
+ }
+
+ return false;
+}
+
+void BadType() { MOZ_CRASH("Bad client type value!"); }
+
+} // namespace
+
+// static
+bool Client::IsValidType(Type aType) {
+ switch (aType) {
+ case Client::IDB:
+ case Client::DOMCACHE:
+ case Client::SDB:
+ case Client::FILESYSTEM:
+ return true;
+
+ case Client::LS:
+ if (CachedNextGenLocalStorageEnabled()) {
+ return true;
+ }
+ [[fallthrough]];
+
+ default:
+ return false;
+ }
+}
+
+// static
+bool Client::TypeToText(Type aType, nsAString& aText, const fallible_t&) {
+ nsString text;
+ if (!TypeTo_impl(aType, text)) {
+ return false;
+ }
+ aText = text;
+ return true;
+}
+
+// static
+nsAutoString Client::TypeToString(Type aType) {
+ nsAutoString res;
+ if (!TypeTo_impl(aType, res)) {
+ BadType();
+ }
+ return res;
+}
+
+// static
+nsAutoCString Client::TypeToText(Type aType) {
+ nsAutoCString res;
+ if (!TypeTo_impl(aType, res)) {
+ BadType();
+ }
+ return res;
+}
+
+// static
+bool Client::TypeFromText(const nsAString& aText, Type& aType,
+ const fallible_t&) {
+ Type type;
+ if (!TypeFrom_impl(aText, type)) {
+ return false;
+ }
+ aType = type;
+ return true;
+}
+
+// static
+Client::Type Client::TypeFromText(const nsACString& aText) {
+ Type type;
+ if (!TypeFrom_impl(aText, type)) {
+ BadType();
+ }
+ return type;
+}
+
+// static
+char Client::TypeToPrefix(Type aType) {
+ char prefix;
+ if (!TypeTo_impl(aType, prefix)) {
+ BadType();
+ }
+ return prefix;
+}
+
+// static
+bool Client::TypeFromPrefix(char aPrefix, Type& aType, const fallible_t&) {
+ Type type;
+ if (!TypeFrom_impl(aPrefix, type)) {
+ return false;
+ }
+ aType = type;
+ return true;
+}
+
+bool Client::InitiateShutdownWorkThreads() {
+ AssertIsOnBackgroundThread();
+
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(GetType(), "starting"_ns);
+
+ InitiateShutdown();
+
+ return IsShutdownCompleted();
+}
+
+void Client::FinalizeShutdownWorkThreads() {
+ QuotaManager::MaybeRecordQuotaClientShutdownStep(GetType(), "completed"_ns);
+
+ FinalizeShutdown();
+}
+
+// static
+bool Client::IsShuttingDownOnBackgroundThread() {
+ MOZ_ASSERT(IsOnBackgroundThread());
+ return QuotaManager::IsShuttingDown();
+}
+
+// static
+bool Client::IsShuttingDownOnNonBackgroundThread() {
+ MOZ_ASSERT(!IsOnBackgroundThread());
+ return QuotaManager::IsShuttingDown();
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/Client.h b/dom/quota/Client.h
new file mode 100644
index 0000000000..8a6f0a9548
--- /dev/null
+++ b/dom/quota/Client.h
@@ -0,0 +1,191 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_client_h__
+#define mozilla_dom_quota_client_h__
+
+#include "ErrorList.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "nsHashKeys.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTHashSet.h"
+
+// XXX Remove this dependency.
+#include "mozilla/dom/LocalStorageCommon.h"
+
+class nsIFile;
+
+#define IDB_DIRECTORY_NAME "idb"
+#define DOMCACHE_DIRECTORY_NAME "cache"
+#define SDB_DIRECTORY_NAME "sdb"
+#define FILESYSTEM_DIRECTORY_NAME "fs"
+#define LS_DIRECTORY_NAME "ls"
+
+// Deprecated
+#define ASMJSCACHE_DIRECTORY_NAME "asmjs"
+
+namespace mozilla::dom {
+template <typename T>
+struct Nullable;
+}
+
+namespace mozilla::dom::quota {
+
+struct OriginMetadata;
+class OriginScope;
+class QuotaManager;
+class UsageInfo;
+
+// An abstract interface for quota manager clients.
+// Each storage API must provide an implementation of this interface in order
+// to participate in centralized quota and storage handling.
+class Client {
+ public:
+ using AtomicBool = Atomic<bool>;
+
+ enum Type {
+ IDB = 0,
+ // APPCACHE,
+ DOMCACHE,
+ SDB,
+ FILESYSTEM,
+ LS,
+ TYPE_MAX
+ };
+
+ class DirectoryLockIdTable final {
+ nsTHashSet<uint64_t> mIds;
+
+ public:
+ void Put(const int64_t aId) { mIds.Insert(aId); }
+
+ bool Has(const int64_t aId) const { return mIds.Contains(aId); }
+
+ bool Filled() const { return mIds.Count(); }
+ };
+
+ static Type TypeMax() {
+ if (NextGenLocalStorageEnabled()) {
+ return TYPE_MAX;
+ }
+ return LS;
+ }
+
+ static bool IsValidType(Type aType);
+
+ static bool TypeToText(Type aType, nsAString& aText, const fallible_t&);
+
+ // TODO: Rename other similar methods to use String/CString instead of Text.
+ static nsAutoString TypeToString(Type aType);
+
+ static nsAutoCString TypeToText(Type aType);
+
+ static bool TypeFromText(const nsAString& aText, Type& aType,
+ const fallible_t&);
+
+ static Type TypeFromText(const nsACString& aText);
+
+ static char TypeToPrefix(Type aType);
+
+ static bool TypeFromPrefix(char aPrefix, Type& aType, const fallible_t&);
+
+ static bool IsDeprecatedClient(const nsAString& aText) {
+ return aText.EqualsLiteral(ASMJSCACHE_DIRECTORY_NAME);
+ }
+
+ template <typename T>
+ static bool IsLockForObjectContainedInLockTable(
+ const T& aObject, const DirectoryLockIdTable& aIds);
+
+ template <typename T>
+ static bool IsLockForObjectAcquiredAndContainedInLockTable(
+ const T& aObject, const DirectoryLockIdTable& aIds);
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual Type GetType() = 0;
+
+ // Methods which are called on the IO thread.
+ virtual nsresult UpgradeStorageFrom1_0To2_0(nsIFile* aDirectory) {
+ return NS_OK;
+ }
+
+ virtual nsresult UpgradeStorageFrom2_0To2_1(nsIFile* aDirectory) {
+ return NS_OK;
+ }
+
+ virtual nsresult UpgradeStorageFrom2_1To2_2(nsIFile* aDirectory) {
+ return NS_OK;
+ }
+
+ virtual Result<UsageInfo, nsresult> InitOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) = 0;
+
+ virtual nsresult InitOriginWithoutTracking(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) = 0;
+
+ virtual Result<UsageInfo, nsresult> GetUsageForOrigin(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ const AtomicBool& aCanceled) = 0;
+
+ // This method is called when origins are about to be cleared
+ // (except the case when clearing is triggered by the origin eviction).
+ virtual nsresult AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope) {
+ return NS_OK;
+ }
+
+ virtual void OnOriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin) = 0;
+
+ virtual void ReleaseIOThreadObjects() = 0;
+
+ // Methods which are called on the background thread.
+ virtual void AbortOperationsForLocks(
+ const DirectoryLockIdTable& aDirectoryLockIds) = 0;
+
+ virtual void AbortOperationsForProcess(ContentParentId aContentParentId) = 0;
+
+ virtual void AbortAllOperations() = 0;
+
+ virtual void StartIdleMaintenance() = 0;
+
+ virtual void StopIdleMaintenance() = 0;
+
+ // Both variants just check for QuotaManager::IsShuttingDown()
+ // but assert to be on the right thread.
+ // They must not be used for re-entrance checks.
+ // Deprecated: This distinction is not needed anymore.
+ // QuotaClients should call QuotaManager::IsShuttingDown instead.
+ static bool IsShuttingDownOnBackgroundThread();
+ static bool IsShuttingDownOnNonBackgroundThread();
+
+ // Returns true if there is work that needs to be waited for.
+ bool InitiateShutdownWorkThreads();
+ void FinalizeShutdownWorkThreads();
+
+ virtual nsCString GetShutdownStatus() const = 0;
+ virtual bool IsShutdownCompleted() const = 0;
+ virtual void ForceKillActors() = 0;
+
+ private:
+ virtual void InitiateShutdown() = 0;
+ virtual void FinalizeShutdown() = 0;
+
+ protected:
+ virtual ~Client() = default;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_client_h__
diff --git a/dom/quota/ClientImpl.h b/dom/quota/ClientImpl.h
new file mode 100644
index 0000000000..9602345e5d
--- /dev/null
+++ b/dom/quota/ClientImpl.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_CLIENTIMPL_H_
+#define DOM_QUOTA_CLIENTIMPL_H_
+
+#include "mozilla/dom/quota/DirectoryLock.h"
+
+namespace mozilla::dom::quota {
+
+// static
+template <typename T>
+bool Client::IsLockForObjectContainedInLockTable(
+ const T& aObject, const DirectoryLockIdTable& aIds) {
+ const auto& maybeDirectoryLock = aObject.MaybeDirectoryLockRef();
+
+ MOZ_ASSERT(maybeDirectoryLock.isSome());
+
+ return aIds.Has(maybeDirectoryLock->Id());
+}
+
+// static
+template <typename T>
+bool Client::IsLockForObjectAcquiredAndContainedInLockTable(
+ const T& aObject, const DirectoryLockIdTable& aIds) {
+ const auto& maybeDirectoryLock = aObject.MaybeDirectoryLockRef();
+
+ return maybeDirectoryLock && aIds.Has(maybeDirectoryLock->Id());
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_CLIENTIMPL_H_
diff --git a/dom/quota/ClientUsageArray.cpp b/dom/quota/ClientUsageArray.cpp
new file mode 100644
index 0000000000..f0c8095b1c
--- /dev/null
+++ b/dom/quota/ClientUsageArray.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ClientUsageArray.h"
+
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::quota {
+
+void ClientUsageArray::Serialize(nsACString& aText) const {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ bool first = true;
+
+ for (Client::Type type : quotaManager->AllClientTypes()) {
+ const Maybe<uint64_t>& clientUsage = ElementAt(type);
+ if (clientUsage.isSome()) {
+ if (first) {
+ first = false;
+ } else {
+ aText.Append(" ");
+ }
+
+ aText.Append(Client::TypeToPrefix(type));
+ aText.AppendInt(clientUsage.value());
+ }
+ }
+}
+
+nsresult ClientUsageArray::Deserialize(const nsACString& aText) {
+ for (const auto& token :
+ nsCCharSeparatedTokenizerTemplate<NS_TokenizerIgnoreNothing>(aText, ' ')
+ .ToRange()) {
+ QM_TRY(OkIf(token.Length() >= 2), NS_ERROR_FAILURE);
+
+ Client::Type clientType;
+ QM_TRY(OkIf(Client::TypeFromPrefix(token.First(), clientType, fallible)),
+ NS_ERROR_FAILURE);
+
+ nsresult rv;
+ const uint64_t usage = Substring(token, 1).ToInteger64(&rv);
+ QM_TRY(MOZ_TO_RESULT(rv));
+
+ ElementAt(clientType) = Some(usage);
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/ClientUsageArray.h b/dom/quota/ClientUsageArray.h
new file mode 100644
index 0000000000..3b57656143
--- /dev/null
+++ b/dom/quota/ClientUsageArray.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_CLIENTUSAGEARAY_H_
+#define DOM_QUOTA_CLIENTUSAGEARAY_H_
+
+#include <cstdint>
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/quota/Client.h"
+
+namespace mozilla::dom::quota {
+
+// XXX Change this not to derive from AutoTArray.
+class ClientUsageArray final
+ : public AutoTArray<Maybe<uint64_t>, Client::TYPE_MAX> {
+ public:
+ ClientUsageArray() { SetLength(Client::TypeMax()); }
+
+ void Serialize(nsACString& aText) const;
+
+ nsresult Deserialize(const nsACString& aText);
+
+ ClientUsageArray Clone() const {
+ ClientUsageArray res;
+ res.Assign(*this);
+ return res;
+ }
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_CLIENTUSAGEARAY_H_
diff --git a/dom/quota/CommonMetadata.h b/dom/quota/CommonMetadata.h
new file mode 100644
index 0000000000..094c965195
--- /dev/null
+++ b/dom/quota/CommonMetadata.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_COMMONMETADATA_H_
+#define DOM_QUOTA_COMMONMETADATA_H_
+
+#include <utility>
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "nsString.h"
+
+namespace mozilla::dom::quota {
+
+struct PrincipalMetadata {
+ nsCString mSuffix;
+ nsCString mGroup;
+ nsCString mOrigin;
+
+ // These explicit constructors exist to prevent accidental aggregate
+ // initialization which could for example initialize mSuffix as group and
+ // mGroup as origin (if only two string arguments are used).
+ PrincipalMetadata() = default;
+
+ PrincipalMetadata(nsCString aSuffix, nsCString aGroup, nsCString aOrigin)
+ : mSuffix{std::move(aSuffix)},
+ mGroup{std::move(aGroup)},
+ mOrigin{std::move(aOrigin)} {}
+};
+
+struct OriginMetadata : public PrincipalMetadata {
+ PersistenceType mPersistenceType;
+
+ OriginMetadata() = default;
+
+ OriginMetadata(nsCString aSuffix, nsCString aGroup, nsCString aOrigin,
+ PersistenceType aPersistenceType)
+ : PrincipalMetadata(std::move(aSuffix), std::move(aGroup),
+ std::move(aOrigin)),
+ mPersistenceType(aPersistenceType) {}
+
+ OriginMetadata(PrincipalMetadata&& aPrincipalMetadata,
+ PersistenceType aPersistenceType)
+ : PrincipalMetadata(std::move(aPrincipalMetadata)),
+ mPersistenceType(aPersistenceType) {}
+};
+
+struct FullOriginMetadata : OriginMetadata {
+ bool mPersisted;
+ int64_t mLastAccessTime;
+
+ FullOriginMetadata() = default;
+
+ FullOriginMetadata(OriginMetadata aOriginMetadata, bool aPersisted,
+ int64_t aLastAccessTime)
+ : OriginMetadata(std::move(aOriginMetadata)),
+ mPersisted(aPersisted),
+ mLastAccessTime(aLastAccessTime) {}
+};
+
+struct ClientMetadata : OriginMetadata {
+ const Client::Type mClientType;
+
+ ClientMetadata(OriginMetadata aOriginMetadata, Client::Type aClientType)
+ : OriginMetadata(std::move(aOriginMetadata)), mClientType(aClientType) {}
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_COMMONMETADATA_H_
diff --git a/dom/quota/Config.h b/dom/quota/Config.h
new file mode 100644
index 0000000000..33056265a3
--- /dev/null
+++ b/dom/quota/Config.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_CONFIG_H_
+#define DOM_QUOTA_CONFIG_H_
+
+#ifdef DEBUG
+# define QM_LOG_ERROR_TO_CONSOLE_ENABLED
+#endif
+
+#define QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED
+
+#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
+# define QM_LOG_ERROR_TO_TELEMETRY_ENABLED
+#endif
+
+#if defined(QM_LOG_ERROR_TO_CONSOLE_ENABLED) || \
+ defined(QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED) || \
+ defined(QM_LOG_ERROR_TO_TELEMETRY_ENABLED)
+# define QM_LOG_ERROR_ENABLED
+#endif
+
+#if defined(EARLY_BETA_OR_EARLIER) || defined(DEBUG)
+# define QM_ERROR_STACKS_ENABLED
+#endif
+
+#define QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+
+#endif // DOM_QUOTA_CONFIG_H_
diff --git a/dom/quota/DecryptingInputStream.cpp b/dom/quota/DecryptingInputStream.cpp
new file mode 100644
index 0000000000..939b1e3ad7
--- /dev/null
+++ b/dom/quota/DecryptingInputStream.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecryptingInputStream.h"
+#include "DecryptingInputStream_impl.h"
+
+#include "nsStreamUtils.h"
+
+namespace mozilla::dom::quota {
+
+NS_IMPL_ADDREF(DecryptingInputStreamBase);
+NS_IMPL_RELEASE(DecryptingInputStreamBase);
+
+NS_INTERFACE_MAP_BEGIN(DecryptingInputStreamBase)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsISeekableStream)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsICloneableInputStream,
+ mBaseCloneableInputStream || !mBaseStream)
+ NS_INTERFACE_MAP_ENTRY_CONDITIONAL(
+ nsIIPCSerializableInputStream,
+ mBaseIPCSerializableInputStream || !mBaseStream)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
+NS_INTERFACE_MAP_END
+
+DecryptingInputStreamBase::DecryptingInputStreamBase(
+ MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize) {
+ Init(std::move(aBaseStream), aBlockSize);
+}
+
+void DecryptingInputStreamBase::Init(
+ MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize) {
+ mBlockSize.init(aBlockSize);
+ mBaseStream.init(std::move(aBaseStream));
+
+ const nsCOMPtr<nsISeekableStream> seekableStream =
+ do_QueryInterface(mBaseStream->get());
+ MOZ_ASSERT(seekableStream &&
+ SameCOMIdentity(mBaseStream->get(), seekableStream));
+ mBaseSeekableStream.init(WrapNotNullUnchecked(seekableStream));
+
+ const nsCOMPtr<nsICloneableInputStream> cloneableInputStream =
+ do_QueryInterface(mBaseStream->get());
+ if (cloneableInputStream &&
+ SameCOMIdentity(mBaseStream->get(), cloneableInputStream)) {
+ mBaseCloneableInputStream.init(WrapNotNullUnchecked(cloneableInputStream));
+ }
+
+ const nsCOMPtr<nsIIPCSerializableInputStream> ipcSerializeInputStream =
+ do_QueryInterface(mBaseStream->get());
+ if (ipcSerializeInputStream &&
+ SameCOMIdentity(mBaseStream->get(), ipcSerializeInputStream)) {
+ mBaseIPCSerializableInputStream.init(
+ WrapNotNullUnchecked(ipcSerializeInputStream));
+ }
+}
+
+NS_IMETHODIMP DecryptingInputStreamBase::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aBytesReadOut) {
+ return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aBytesReadOut);
+}
+
+NS_IMETHODIMP DecryptingInputStreamBase::IsNonBlocking(bool* aNonBlockingOut) {
+ *aNonBlockingOut = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP DecryptingInputStreamBase::SetEOF() {
+ // Cannot truncate a read-only stream.
+
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP DecryptingInputStreamBase::GetCloneable(bool* aCloneable) {
+ *aCloneable = true;
+ return NS_OK;
+}
+
+void DecryptingInputStreamBase::SerializedComplexity(uint32_t aMaxSize,
+ uint32_t* aSizeUsed,
+ uint32_t* aPipes,
+ uint32_t* aTransferables) {
+ (*mBaseIPCSerializableInputStream)
+ ->SerializedComplexity(aMaxSize, aSizeUsed, aPipes, aTransferables);
+}
+
+size_t DecryptingInputStreamBase::PlainLength() const {
+ MOZ_ASSERT(mNextByte <= mPlainBytes);
+ return mPlainBytes - mNextByte;
+}
+
+size_t DecryptingInputStreamBase::EncryptedBufferLength() const {
+ return *mBlockSize;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/DecryptingInputStream.h b/dom/quota/DecryptingInputStream.h
new file mode 100644
index 0000000000..7559ca860e
--- /dev/null
+++ b/dom/quota/DecryptingInputStream.h
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_DecryptingInputStream_h
+#define mozilla_dom_quota_DecryptingInputStream_h
+
+// Local includes
+#include "EncryptedBlock.h"
+
+// Global includes
+#include <cstddef>
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/ipc/InputStreamParams.h"
+#include "nsCOMPtr.h"
+#include "nsICloneableInputStream.h"
+#include "nsIIPCSerializableInputStream.h"
+#include "nsIInputStream.h"
+#include "nsISeekableStream.h"
+#include "nsISupports.h"
+#include "nsITellableStream.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+template <class T>
+class nsCOMPtr;
+
+namespace mozilla::dom::quota {
+
+class DecryptingInputStreamBase : public nsIInputStream,
+ public nsISeekableStream,
+ public nsICloneableInputStream,
+ public nsIIPCSerializableInputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD Read(char* aBuf, uint32_t aCount, uint32_t* _retval) final;
+ NS_IMETHOD IsNonBlocking(bool* _retval) final;
+
+ NS_IMETHOD SetEOF() final;
+
+ using nsICloneableInputStream::GetCloneable;
+ NS_IMETHOD GetCloneable(bool* aCloneable) final;
+
+ void SerializedComplexity(uint32_t aMaxSize, uint32_t* aSizeUsed,
+ uint32_t* aPipes,
+ uint32_t* aTransferables) override;
+
+ protected:
+ DecryptingInputStreamBase(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream,
+ size_t aBlockSize);
+
+ // For deserialization only.
+ DecryptingInputStreamBase() = default;
+
+ virtual ~DecryptingInputStreamBase() = default;
+
+ void Init(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream,
+ size_t aBlockSize);
+
+ // Convenience routine to determine how many bytes of plain data
+ // we currently have in our buffer.
+ size_t PlainLength() const;
+
+ size_t EncryptedBufferLength() const;
+
+ LazyInitializedOnceEarlyDestructible<const NotNull<nsCOMPtr<nsIInputStream>>>
+ mBaseStream;
+ LazyInitializedOnce<const NotNull<nsISeekableStream*>> mBaseSeekableStream;
+ LazyInitializedOnce<const NotNull<nsICloneableInputStream*>>
+ mBaseCloneableInputStream;
+ LazyInitializedOnce<const NotNull<nsIIPCSerializableInputStream*>>
+ mBaseIPCSerializableInputStream;
+
+ // Number of bytes of plain data in mBuffer.
+ size_t mPlainBytes = 0;
+
+ // Next byte of mBuffer to return in ReadSegments().
+ size_t mNextByte = 0;
+
+ LazyInitializedOnceNotNull<const size_t> mBlockSize;
+
+ size_t mLastBlockLength = 0;
+};
+
+// Wraps another nsIInputStream which contains data written using
+// EncryptingInputStream with a compatible CipherStategy and key. See the
+// remarks on EncryptingOutputStream.
+template <typename CipherStrategy>
+class DecryptingInputStream final : public DecryptingInputStreamBase {
+ public:
+ // Construct a new blocking stream to decrypt the given base stream. The
+ // base stream must also be blocking. The base stream does not have to be
+ // buffered.
+ DecryptingInputStream(MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream,
+ size_t aBlockSize,
+ typename CipherStrategy::KeyType aKey);
+
+ // For deserialization only.
+ explicit DecryptingInputStream();
+
+ NS_IMETHOD Close() override;
+ NS_IMETHOD Available(uint64_t* _retval) override;
+ NS_IMETHOD ReadSegments(nsWriteSegmentFun aWriter, void* aClosure,
+ uint32_t aCount, uint32_t* _retval) override;
+
+ NS_DECL_NSITELLABLESTREAM
+
+ NS_IMETHOD Seek(int32_t aWhence, int64_t aOffset) override;
+
+ NS_IMETHOD Clone(nsIInputStream** _retval) override;
+
+ void Serialize(mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize,
+ uint32_t* aSizeUsed) override;
+
+ bool Deserialize(const mozilla::ipc::InputStreamParams& aParams) override;
+
+ private:
+ ~DecryptingInputStream();
+
+ // Parse the next chunk of data. This may populate mBuffer and set
+ // mBufferFillSize. This should not be called when mBuffer already
+ // contains data.
+ nsresult ParseNextChunk(uint32_t* aBytesReadOut);
+
+ // Convenience routine to Read() from the base stream until we get
+ // the given number of bytes or reach EOF.
+ //
+ // aBuf - The buffer to write the bytes into.
+ // aCount - Max number of bytes to read. If the stream closes
+ // fewer bytes my be read.
+ // aMinValidCount - A minimum expected number of bytes. If we find
+ // fewer than this many bytes, then return
+ // NS_ERROR_CORRUPTED_CONTENT. If nothing was read due
+ // due to EOF (aBytesReadOut == 0), then NS_OK is returned.
+ // aBytesReadOut - An out parameter indicating how many bytes were read.
+ nsresult ReadAll(char* aBuf, uint32_t aCount, uint32_t aMinValidCount,
+ uint32_t* aBytesReadOut);
+
+ bool EnsureBuffers();
+
+ CipherStrategy mCipherStrategy;
+ LazyInitializedOnce<const typename CipherStrategy::KeyType> mKey;
+
+ // Buffer to hold encrypted data. Must copy here since we need a
+ // flat buffer to run the decryption process on.
+ using EncryptedBlockType = EncryptedBlock<CipherStrategy::BlockPrefixLength,
+ CipherStrategy::BasicBlockSize>;
+ Maybe<EncryptedBlockType> mEncryptedBlock;
+
+ // Buffer storing the resulting plain data.
+ nsTArray<uint8_t> mPlainBuffer;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/DecryptingInputStream_impl.h b/dom/quota/DecryptingInputStream_impl.h
new file mode 100644
index 0000000000..ed5353d6ae
--- /dev/null
+++ b/dom/quota/DecryptingInputStream_impl.h
@@ -0,0 +1,520 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_DecryptingInputStream_impl_h
+#define mozilla_dom_quota_DecryptingInputStream_impl_h
+
+#include "DecryptingInputStream.h"
+
+#include <algorithm>
+#include <cstdio>
+#include <type_traits>
+#include <utility>
+#include "CipherStrategy.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Span.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsFileStreams.h"
+#include "nsID.h"
+#include "nsIFileStreams.h"
+
+namespace mozilla::dom::quota {
+
+template <typename CipherStrategy>
+DecryptingInputStream<CipherStrategy>::DecryptingInputStream(
+ MovingNotNull<nsCOMPtr<nsIInputStream>> aBaseStream, size_t aBlockSize,
+ typename CipherStrategy::KeyType aKey)
+ : DecryptingInputStreamBase(std::move(aBaseStream), aBlockSize),
+ mKey(aKey) {
+ // XXX Move this to a fallible init function.
+ MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Decrypt,
+ CipherStrategy::SerializeKey(aKey)));
+
+ // This implementation only supports sync base streams. Verify this in debug
+ // builds.
+#ifdef DEBUG
+ bool baseNonBlocking;
+ nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(!baseNonBlocking);
+#endif
+}
+
+template <typename CipherStrategy>
+DecryptingInputStream<CipherStrategy>::~DecryptingInputStream() {
+ Close();
+}
+
+template <typename CipherStrategy>
+DecryptingInputStream<CipherStrategy>::DecryptingInputStream()
+ : DecryptingInputStreamBase{} {}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Close() {
+ if (!mBaseStream) {
+ return NS_OK;
+ }
+
+ (*mBaseStream)->Close();
+ mBaseStream.destroy();
+
+ mPlainBuffer.Clear();
+ mEncryptedBlock.reset();
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Available(
+ uint64_t* aLengthOut) {
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ int64_t oldPos, endPos;
+ nsresult rv = Tell(&oldPos);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = Seek(SEEK_END, 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = Tell(&endPos);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = Seek(SEEK_SET, oldPos);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *aLengthOut = endPos - oldPos;
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::ReadSegments(
+ nsWriteSegmentFun aWriter, void* aClosure, uint32_t aCount,
+ uint32_t* aBytesReadOut) {
+ *aBytesReadOut = 0;
+
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ nsresult rv;
+
+ // Do not try to use the base stream's ReadSegments here. Its very
+ // unlikely we will get a single buffer that contains all of the encrypted
+ // data and therefore would have to copy into our own buffer anyways.
+ // Instead, focus on making efficient use of the Read() interface.
+
+ while (aCount > 0) {
+ // We have some decrypted data in our buffer. Provide it to the callers
+ // writer function.
+ if (mPlainBytes > 0) {
+ MOZ_ASSERT(!mPlainBuffer.IsEmpty());
+ uint32_t remaining = PlainLength();
+ uint32_t numToWrite = std::min(aCount, remaining);
+ uint32_t numWritten;
+ rv = aWriter(this, aClosure,
+ reinterpret_cast<const char*>(&mPlainBuffer[mNextByte]),
+ *aBytesReadOut, numToWrite, &numWritten);
+
+ // As defined in nsIInputputStream.idl, do not pass writer func errors.
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ // End-of-file
+ if (numWritten == 0) {
+ return NS_OK;
+ }
+
+ *aBytesReadOut += numWritten;
+ mNextByte += numWritten;
+ MOZ_ASSERT(mNextByte <= mPlainBytes);
+
+ if (mNextByte == mPlainBytes) {
+ mNextByte = 0;
+ mLastBlockLength = mPlainBytes;
+ mPlainBytes = 0;
+ }
+
+ aCount -= numWritten;
+
+ continue;
+ }
+
+ // Otherwise decrypt the next chunk and loop. Any resulting data
+ // will set mPlainBytes which we check at the top of the loop.
+ uint32_t bytesRead;
+ rv = ParseNextChunk(&bytesRead);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // If we couldn't read anything and there is no more data to provide
+ // to the caller, then this is eof.
+ if (bytesRead == 0 && mPlainBytes == 0) {
+ return NS_OK;
+ }
+
+ mPlainBytes += bytesRead;
+ }
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+nsresult DecryptingInputStream<CipherStrategy>::ParseNextChunk(
+ uint32_t* const aBytesReadOut) {
+ // There must not be any plain data already in mPlainBuffer.
+ MOZ_ASSERT(mPlainBytes == 0);
+ MOZ_ASSERT(mNextByte == 0);
+
+ *aBytesReadOut = 0;
+
+ if (!EnsureBuffers()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // Read the data to our internal encrypted buffer.
+ auto wholeBlock = mEncryptedBlock->MutableWholeBlock();
+ nsresult rv =
+ ReadAll(AsWritableChars(wholeBlock).Elements(), wholeBlock.Length(),
+ wholeBlock.Length(), aBytesReadOut);
+ if (NS_WARN_IF(NS_FAILED(rv)) || *aBytesReadOut == 0) {
+ return rv;
+ }
+
+ // XXX Do we need to know the actual decrypted size?
+ rv = mCipherStrategy.Cipher(mEncryptedBlock->MutableCipherPrefix(),
+ mEncryptedBlock->Payload(),
+ AsWritableBytes(Span{mPlainBuffer}));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *aBytesReadOut = mEncryptedBlock->ActualPayloadLength();
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+nsresult DecryptingInputStream<CipherStrategy>::ReadAll(
+ char* aBuf, uint32_t aCount, uint32_t aMinValidCount,
+ uint32_t* aBytesReadOut) {
+ MOZ_ASSERT(aCount >= aMinValidCount);
+ MOZ_ASSERT(mBaseStream);
+
+ *aBytesReadOut = 0;
+
+ uint32_t offset = 0;
+ while (aCount > 0) {
+ uint32_t bytesRead = 0;
+ nsresult rv = (*mBaseStream)->Read(aBuf + offset, aCount, &bytesRead);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // EOF, but don't immediately return. We need to validate min read bytes
+ // below.
+ if (bytesRead == 0) {
+ break;
+ }
+
+ *aBytesReadOut += bytesRead;
+ offset += bytesRead;
+ aCount -= bytesRead;
+ }
+
+ // Reading zero bytes is not an error. Its the expected EOF condition.
+ // Only compare to the minimum valid count if we read at least one byte.
+ if (*aBytesReadOut != 0 && *aBytesReadOut < aMinValidCount) {
+ return NS_ERROR_CORRUPTED_CONTENT;
+ }
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+bool DecryptingInputStream<CipherStrategy>::EnsureBuffers() {
+ // Lazily create our two buffers so we can report OOM during stream
+ // operation. These allocations only happens once. The buffers are reused
+ // until the stream is closed.
+ if (!mEncryptedBlock) {
+ // XXX Do we need to do this fallible (as the comment above suggests)?
+ mEncryptedBlock.emplace(*mBlockSize);
+
+ MOZ_ASSERT(mPlainBuffer.IsEmpty());
+ if (NS_WARN_IF(!mPlainBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(),
+ fallible))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Tell(
+ int64_t* const aRetval) {
+ MOZ_ASSERT(aRetval);
+
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!EnsureBuffers()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int64_t basePosition;
+ nsresult rv = (*mBaseSeekableStream)->Tell(&basePosition);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ const auto fullBlocks = basePosition / *mBlockSize;
+ MOZ_ASSERT(0 == basePosition % *mBlockSize);
+
+ *aRetval = (fullBlocks - ((mPlainBytes || mLastBlockLength) ? 1 : 0)) *
+ mEncryptedBlock->MaxPayloadLength() +
+ mNextByte + (mNextByte ? 0 : mLastBlockLength);
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Seek(const int32_t aWhence,
+ int64_t aOffset) {
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!EnsureBuffers()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ int64_t baseBlocksOffset;
+ int64_t nextByteOffset;
+ switch (aWhence) {
+ case NS_SEEK_CUR:
+ // XXX Simplify this without using Tell.
+ {
+ int64_t current;
+ nsresult rv = Tell(&current);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ aOffset += current;
+ }
+ break;
+ case NS_SEEK_SET:
+ break;
+
+ case NS_SEEK_END:
+ // XXX Simplify this without using Seek/Tell.
+ {
+ // XXX The size of the stream could also be queried and stored once
+ // only.
+ nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_SET, 0);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint64_t baseStreamSize;
+ rv = (*mBaseStream)->Available(&baseStreamSize);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ auto decryptedStreamSizeOrErr = [baseStreamSize,
+ this]() -> Result<int64_t, nsresult> {
+ if (!baseStreamSize) {
+ return 0;
+ }
+
+ nsresult rv =
+ (*mBaseSeekableStream)
+ ->Seek(NS_SEEK_END, -static_cast<int64_t>(*mBlockSize));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ mNextByte = 0;
+ mPlainBytes = 0;
+
+ uint32_t bytesRead;
+ rv = ParseNextChunk(&bytesRead);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+ MOZ_ASSERT(bytesRead);
+
+ // XXX Shouldn't ParseNextChunk better update mPlainBytes?
+ mPlainBytes = bytesRead;
+
+ mNextByte = bytesRead;
+
+ int64_t current;
+ rv = Tell(&current);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ return current;
+ }();
+
+ if (decryptedStreamSizeOrErr.isErr()) {
+ return decryptedStreamSizeOrErr.unwrapErr();
+ }
+
+ aOffset += decryptedStreamSizeOrErr.unwrap();
+ }
+ break;
+
+ default:
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ baseBlocksOffset = aOffset / mEncryptedBlock->MaxPayloadLength();
+ nextByteOffset = aOffset % mEncryptedBlock->MaxPayloadLength();
+
+ // XXX If we remain in the same block as before, we can skip this.
+ nsresult rv =
+ (*mBaseSeekableStream)->Seek(NS_SEEK_SET, baseBlocksOffset * *mBlockSize);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mNextByte = 0;
+ mPlainBytes = 0;
+
+ uint32_t readBytes;
+ rv = ParseNextChunk(&readBytes);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // XXX Do we need to do more here? Restore any previous state?
+ return rv;
+ }
+
+ // We positioned after the last block, we must read that to know its size.
+ // XXX We could know earlier if we positioned us after the last block.
+ if (!readBytes) {
+ if (baseBlocksOffset == 0) {
+ // The stream is empty.
+ return aOffset == 0 ? NS_OK : NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ nsresult rv = (*mBaseSeekableStream)->Seek(NS_SEEK_CUR, -*mBlockSize);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = ParseNextChunk(&readBytes);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // XXX Do we need to do more here? Restore any previous state?
+ return rv;
+ }
+ }
+
+ mPlainBytes = readBytes;
+ mNextByte = nextByteOffset;
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP DecryptingInputStream<CipherStrategy>::Clone(
+ nsIInputStream** _retval) {
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!(*mBaseCloneableInputStream)->GetCloneable()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIInputStream> clonedStream;
+ nsresult rv =
+ (*mBaseCloneableInputStream)->Clone(getter_AddRefs(clonedStream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ *_retval = MakeAndAddRef<DecryptingInputStream>(
+ WrapNotNull(std::move(clonedStream)), *mBlockSize, *mKey)
+ .take();
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+void DecryptingInputStream<CipherStrategy>::Serialize(
+ mozilla::ipc::InputStreamParams& aParams, uint32_t aMaxSize,
+ uint32_t* aSizeUsed) {
+ MOZ_ASSERT(mBaseStream);
+ MOZ_ASSERT(mBaseIPCSerializableInputStream);
+
+ mozilla::ipc::InputStreamParams baseStreamParams;
+ (*mBaseIPCSerializableInputStream)
+ ->Serialize(baseStreamParams, aMaxSize, aSizeUsed);
+
+ MOZ_ASSERT(baseStreamParams.type() ==
+ mozilla::ipc::InputStreamParams::TFileInputStreamParams);
+
+ mozilla::ipc::EncryptedFileInputStreamParams encryptedFileInputStreamParams;
+ encryptedFileInputStreamParams.fileInputStreamParams() =
+ std::move(baseStreamParams);
+ encryptedFileInputStreamParams.key().AppendElements(
+ mCipherStrategy.SerializeKey(*mKey));
+ encryptedFileInputStreamParams.blockSize() = *mBlockSize;
+
+ aParams = std::move(encryptedFileInputStreamParams);
+}
+
+template <typename CipherStrategy>
+bool DecryptingInputStream<CipherStrategy>::Deserialize(
+ const mozilla::ipc::InputStreamParams& aParams) {
+ MOZ_ASSERT(aParams.type() ==
+ mozilla::ipc::InputStreamParams::TEncryptedFileInputStreamParams);
+ const auto& params = aParams.get_EncryptedFileInputStreamParams();
+
+ nsCOMPtr<nsIFileInputStream> stream;
+ nsFileInputStream::Create(NS_GET_IID(nsIFileInputStream),
+ getter_AddRefs(stream));
+ nsCOMPtr<nsIIPCSerializableInputStream> baseSerializable =
+ do_QueryInterface(stream);
+
+ if (NS_WARN_IF(
+ !baseSerializable->Deserialize(params.fileInputStreamParams()))) {
+ return false;
+ }
+
+ Init(WrapNotNull<nsCOMPtr<nsIInputStream>>(std::move(stream)),
+ params.blockSize());
+ mKey.init(mCipherStrategy.DeserializeKey(params.key()));
+ if (NS_WARN_IF(
+ NS_FAILED(mCipherStrategy.Init(CipherMode::Decrypt, params.key())))) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/DirectoryLock.h b/dom/quota/DirectoryLock.h
new file mode 100644
index 0000000000..b35f74b47e
--- /dev/null
+++ b/dom/quota/DirectoryLock.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_DIRECTORYLOCK_H_
+#define DOM_QUOTA_DIRECTORYLOCK_H_
+
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+
+namespace mozilla::dom::quota {
+
+class ClientDirectoryLock;
+class OpenDirectoryListener;
+struct OriginMetadata;
+
+// Basic directory lock interface shared by all other directory lock classes.
+// The class must contain pure virtual functions only to avoid problems with
+// multiple inheritance.
+class NS_NO_VTABLE DirectoryLock {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual int64_t Id() const = 0;
+
+ // XXX This method is now deprecated, use the one which returns the
+ // `BoolPromise`
+ virtual void Acquire(RefPtr<OpenDirectoryListener> aOpenListener) = 0;
+
+ virtual RefPtr<BoolPromise> Acquire() = 0;
+
+ virtual void AcquireImmediately() = 0;
+
+ virtual void AssertIsAcquiredExclusively() = 0;
+
+ virtual void Log() const = 0;
+};
+
+// A directory lock specialized for a given origin directory.
+class NS_NO_VTABLE OriginDirectoryLock : public DirectoryLock {
+ public:
+ // 'Get' prefix is to avoid name collisions with the enum
+ virtual PersistenceType GetPersistenceType() const = 0;
+
+ virtual quota::OriginMetadata OriginMetadata() const = 0;
+
+ virtual const nsACString& Origin() const = 0;
+};
+
+// A directory lock specialized for a given client directory (inside an origin
+// directory).
+class NS_NO_VTABLE ClientDirectoryLock : public OriginDirectoryLock {
+ public:
+ virtual Client::Type ClientType() const = 0;
+};
+
+// A directory lock for universal use. A universal lock can handle any possible
+// combination of nullable persistence type, origin scope and nullable client
+// type.
+//
+// For example, if the persistence type is set to null, origin scope is null
+// and the client type is set to Client::IDB, then the lock will cover
+// <profile>/storage/*/*/idb
+//
+// If no property is set, then the lock will cover the entire storage directory
+// and its subdirectories.
+class UniversalDirectoryLock : public DirectoryLock {
+ public:
+ // XXX Rename to NullablePersistenceTypeRef.
+ virtual const Nullable<PersistenceType>& NullablePersistenceType() const = 0;
+
+ // XXX Rename to OriginScopeRef.
+ virtual const OriginScope& GetOriginScope() const = 0;
+
+ // XXX Rename to NullableClientTypeRef.
+ virtual const Nullable<Client::Type>& NullableClientType() const = 0;
+
+ virtual RefPtr<ClientDirectoryLock> SpecializeForClient(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType) const = 0;
+};
+
+class NS_NO_VTABLE OpenDirectoryListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void DirectoryLockAcquired(DirectoryLock* aLock) = 0;
+
+ virtual void DirectoryLockFailed() = 0;
+
+ protected:
+ virtual ~OpenDirectoryListener() = default;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_DIRECTORYLOCK_H_
diff --git a/dom/quota/DirectoryLockImpl.cpp b/dom/quota/DirectoryLockImpl.cpp
new file mode 100644
index 0000000000..05c210d195
--- /dev/null
+++ b/dom/quota/DirectoryLockImpl.cpp
@@ -0,0 +1,372 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DirectoryLockImpl.h"
+
+#include "mozilla/ReverseIterator.h"
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+
+namespace mozilla::dom::quota {
+
+DirectoryLockImpl::DirectoryLockImpl(
+ MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ const Nullable<PersistenceType>& aPersistenceType,
+ const nsACString& aSuffix, const nsACString& aGroup,
+ const OriginScope& aOriginScope, const Nullable<Client::Type>& aClientType,
+ const bool aExclusive, const bool aInternal,
+ const ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag)
+ : mQuotaManager(std::move(aQuotaManager)),
+ mPersistenceType(aPersistenceType),
+ mSuffix(aSuffix),
+ mGroup(aGroup),
+ mOriginScope(aOriginScope),
+ mClientType(aClientType),
+ mId(mQuotaManager->GenerateDirectoryLockId()),
+ mExclusive(aExclusive),
+ mInternal(aInternal),
+ mShouldUpdateLockIdTable(aShouldUpdateLockIdTableFlag ==
+ ShouldUpdateLockIdTableFlag::Yes),
+ mRegistered(false) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty());
+ MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull());
+ MOZ_ASSERT_IF(!aInternal,
+ aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID);
+ MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty());
+ MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin());
+ MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull());
+ MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax());
+}
+
+DirectoryLockImpl::~DirectoryLockImpl() {
+ AssertIsOnOwningThread();
+
+ // We must call UnregisterDirectoryLock before unblocking other locks because
+ // UnregisterDirectoryLock also updates the origin last access time and the
+ // access flag (if the last lock for given origin is unregistered). One of the
+ // blocked locks could be requested by the clear/reset operation which stores
+ // cached information about origins in storage.sqlite. So if the access flag
+ // is not updated before unblocking the lock for reset/clear, we might store
+ // invalid information which can lead to omitting origin initialization during
+ // next temporary storage initialization.
+ if (mRegistered) {
+ mQuotaManager->UnregisterDirectoryLock(*this);
+ }
+
+ MOZ_ASSERT(!mRegistered);
+
+ for (NotNull<RefPtr<DirectoryLockImpl>> blockingLock : mBlocking) {
+ blockingLock->MaybeUnblock(*this);
+ }
+
+ mBlocking.Clear();
+}
+
+#ifdef DEBUG
+
+void DirectoryLockImpl::AssertIsOnOwningThread() const {
+ mQuotaManager->AssertIsOnOwningThread();
+}
+
+#endif // DEBUG
+
+bool DirectoryLockImpl::Overlaps(const DirectoryLockImpl& aLock) const {
+ AssertIsOnOwningThread();
+
+ // If the persistence types don't overlap, the op can proceed.
+ if (!aLock.mPersistenceType.IsNull() && !mPersistenceType.IsNull() &&
+ aLock.mPersistenceType.Value() != mPersistenceType.Value()) {
+ return false;
+ }
+
+ // If the origin scopes don't overlap, the op can proceed.
+ bool match = aLock.mOriginScope.Matches(mOriginScope);
+ if (!match) {
+ return false;
+ }
+
+ // If the client types don't overlap, the op can proceed.
+ if (!aLock.mClientType.IsNull() && !mClientType.IsNull() &&
+ aLock.mClientType.Value() != mClientType.Value()) {
+ return false;
+ }
+
+ // Otherwise, when all attributes overlap (persistence type, origin scope and
+ // client type) the op must wait.
+ return true;
+}
+
+bool DirectoryLockImpl::MustWaitFor(const DirectoryLockImpl& aLock) const {
+ AssertIsOnOwningThread();
+
+ // Waiting is never required if the ops in comparison represent shared locks.
+ if (!aLock.mExclusive && !mExclusive) {
+ return false;
+ }
+
+ // Wait if the ops overlap.
+ return Overlaps(aLock);
+}
+
+void DirectoryLockImpl::NotifyOpenListener() {
+ AssertIsOnOwningThread();
+
+ if (mInvalidated) {
+ if (mOpenListener) {
+ (*mOpenListener)->DirectoryLockFailed();
+ } else {
+ mAcquirePromiseHolder.Reject(NS_ERROR_FAILURE, __func__);
+ }
+ } else {
+#ifdef DEBUG
+ mAcquired.Flip();
+#endif
+
+ if (mOpenListener) {
+ (*mOpenListener)
+ ->DirectoryLockAcquired(static_cast<UniversalDirectoryLock*>(this));
+ } else {
+ mAcquirePromiseHolder.Resolve(true, __func__);
+ }
+ }
+
+ if (mOpenListener) {
+ mOpenListener.destroy();
+ } else {
+ MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty());
+ }
+
+ mQuotaManager->RemovePendingDirectoryLock(*this);
+
+ mPending.Flip();
+}
+
+void DirectoryLockImpl::Acquire(RefPtr<OpenDirectoryListener> aOpenListener) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aOpenListener);
+
+ mOpenListener.init(WrapNotNullUnchecked(std::move(aOpenListener)));
+
+ AcquireInternal();
+}
+
+RefPtr<BoolPromise> DirectoryLockImpl::Acquire() {
+ AssertIsOnOwningThread();
+
+ RefPtr<BoolPromise> result = mAcquirePromiseHolder.Ensure(__func__);
+
+ AcquireInternal();
+
+ return result;
+}
+
+void DirectoryLockImpl::AcquireInternal() {
+ AssertIsOnOwningThread();
+
+ mQuotaManager->AddPendingDirectoryLock(*this);
+
+ // See if this lock needs to wait.
+ bool blocked = false;
+
+ // XXX It is probably unnecessary to iterate this in reverse order.
+ for (DirectoryLockImpl* const existingLock :
+ Reversed(mQuotaManager->mDirectoryLocks)) {
+ if (MustWaitFor(*existingLock)) {
+ existingLock->AddBlockingLock(*this);
+ AddBlockedOnLock(*existingLock);
+ blocked = true;
+ }
+ }
+
+ mQuotaManager->RegisterDirectoryLock(*this);
+
+ // Otherwise, notify the open listener immediately.
+ if (!blocked) {
+ NotifyOpenListener();
+ return;
+ }
+
+ if (!mExclusive || !mInternal) {
+ return;
+ }
+
+ // All the locks that block this new exclusive internal lock need to be
+ // invalidated. We also need to notify clients to abort operations for them.
+ QuotaManager::DirectoryLockIdTableArray lockIds;
+ lockIds.SetLength(Client::TypeMax());
+
+ const auto& blockedOnLocks = GetBlockedOnLocks();
+ MOZ_ASSERT(!blockedOnLocks.IsEmpty());
+
+ for (DirectoryLockImpl* blockedOnLock : blockedOnLocks) {
+ if (!blockedOnLock->IsInternal()) {
+ blockedOnLock->Invalidate();
+
+ // Clients don't have to handle pending locks. Invalidation is sufficient
+ // in that case (once a lock is ready and the listener needs to be
+ // notified, we will call DirectoryLockFailed instead of
+ // DirectoryLockAcquired which should release any remaining references to
+ // the lock).
+ if (!blockedOnLock->IsPending()) {
+ lockIds[blockedOnLock->ClientType()].Put(blockedOnLock->Id());
+ }
+ }
+ }
+
+ mQuotaManager->AbortOperationsForLocks(lockIds);
+}
+
+void DirectoryLockImpl::AcquireImmediately() {
+ AssertIsOnOwningThread();
+
+#ifdef DEBUG
+ for (const DirectoryLockImpl* const existingLock :
+ mQuotaManager->mDirectoryLocks) {
+ MOZ_ASSERT(!MustWaitFor(*existingLock));
+ }
+#endif
+
+ mQuotaManager->RegisterDirectoryLock(*this);
+}
+
+#ifdef DEBUG
+void DirectoryLockImpl::AssertIsAcquiredExclusively() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mBlockedOn.IsEmpty());
+ MOZ_ASSERT(mExclusive);
+ MOZ_ASSERT(mInternal);
+ MOZ_ASSERT(mRegistered);
+ MOZ_ASSERT(!mInvalidated);
+ MOZ_ASSERT(mAcquired);
+
+ bool found = false;
+
+ for (const DirectoryLockImpl* const existingLock :
+ mQuotaManager->mDirectoryLocks) {
+ if (existingLock == this) {
+ MOZ_ASSERT(!found);
+ found = true;
+ } else if (existingLock->mAcquired) {
+ MOZ_ASSERT(false);
+ }
+ }
+
+ MOZ_ASSERT(found);
+}
+#endif
+
+RefPtr<ClientDirectoryLock> DirectoryLockImpl::SpecializeForClient(
+ PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ Client::Type aClientType) const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID);
+ MOZ_ASSERT(!aOriginMetadata.mGroup.IsEmpty());
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+ MOZ_ASSERT(aClientType < Client::TypeMax());
+ MOZ_ASSERT(!mOpenListener);
+ MOZ_ASSERT(mAcquirePromiseHolder.IsEmpty());
+ MOZ_ASSERT(mBlockedOn.IsEmpty());
+
+ if (NS_WARN_IF(mExclusive)) {
+ return nullptr;
+ }
+
+ RefPtr<DirectoryLockImpl> lock = Create(
+ mQuotaManager, Nullable<PersistenceType>(aPersistenceType),
+ aOriginMetadata.mSuffix, aOriginMetadata.mGroup,
+ OriginScope::FromOrigin(aOriginMetadata.mOrigin),
+ Nullable<Client::Type>(aClientType),
+ /* aExclusive */ false, mInternal, ShouldUpdateLockIdTableFlag::Yes);
+ if (NS_WARN_IF(!Overlaps(*lock))) {
+ return nullptr;
+ }
+
+#ifdef DEBUG
+ for (DirectoryLockImpl* const existingLock :
+ Reversed(mQuotaManager->mDirectoryLocks)) {
+ if (existingLock != this && !existingLock->MustWaitFor(*this)) {
+ MOZ_ASSERT(!existingLock->MustWaitFor(*lock));
+ }
+ }
+#endif
+
+ for (const auto& blockedLock : mBlocking) {
+ if (blockedLock->MustWaitFor(*lock)) {
+ lock->AddBlockingLock(*blockedLock);
+ blockedLock->AddBlockedOnLock(*lock);
+ }
+ }
+
+ mQuotaManager->RegisterDirectoryLock(*lock);
+
+ if (mInvalidated) {
+ lock->Invalidate();
+ }
+
+ return lock;
+}
+
+void DirectoryLockImpl::Log() const {
+ AssertIsOnOwningThread();
+
+ if (!QM_LOG_TEST()) {
+ return;
+ }
+
+ QM_LOG(("DirectoryLockImpl [%p]", this));
+
+ nsCString persistenceType;
+ if (mPersistenceType.IsNull()) {
+ persistenceType.AssignLiteral("null");
+ } else {
+ persistenceType.Assign(PersistenceTypeToString(mPersistenceType.Value()));
+ }
+ QM_LOG((" mPersistenceType: %s", persistenceType.get()));
+
+ QM_LOG((" mGroup: %s", mGroup.get()));
+
+ nsCString originScope;
+ if (mOriginScope.IsOrigin()) {
+ originScope.AssignLiteral("origin:");
+ originScope.Append(mOriginScope.GetOrigin());
+ } else if (mOriginScope.IsPrefix()) {
+ originScope.AssignLiteral("prefix:");
+ originScope.Append(mOriginScope.GetOriginNoSuffix());
+ } else if (mOriginScope.IsPattern()) {
+ originScope.AssignLiteral("pattern:");
+ // Can't call GetJSONPattern since it only works on the main thread.
+ } else {
+ MOZ_ASSERT(mOriginScope.IsNull());
+ originScope.AssignLiteral("null");
+ }
+ QM_LOG((" mOriginScope: %s", originScope.get()));
+
+ const auto clientType = mClientType.IsNull()
+ ? nsAutoCString{"null"_ns}
+ : Client::TypeToText(mClientType.Value());
+ QM_LOG((" mClientType: %s", clientType.get()));
+
+ nsCString blockedOnString;
+ for (auto blockedOn : mBlockedOn) {
+ blockedOnString.Append(
+ nsPrintfCString(" [%p]", static_cast<void*>(blockedOn)));
+ }
+ QM_LOG((" mBlockedOn:%s", blockedOnString.get()));
+
+ QM_LOG((" mExclusive: %d", mExclusive));
+
+ QM_LOG((" mInternal: %d", mInternal));
+
+ QM_LOG((" mInvalidated: %d", static_cast<bool>(mInvalidated)));
+
+ for (auto blockedOn : mBlockedOn) {
+ blockedOn->Log();
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/DirectoryLockImpl.h b/dom/quota/DirectoryLockImpl.h
new file mode 100644
index 0000000000..818d1c81bd
--- /dev/null
+++ b/dom/quota/DirectoryLockImpl.h
@@ -0,0 +1,273 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_DIRECTORYLOCKIMPL_H_
+#define DOM_QUOTA_DIRECTORYLOCKIMPL_H_
+
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/FlippedOnce.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/DirectoryLock.h"
+#include "mozilla/dom/quota/OriginScope.h"
+
+namespace mozilla::dom::quota {
+
+enum class ShouldUpdateLockIdTableFlag { No, Yes };
+
+class DirectoryLockImpl final : public ClientDirectoryLock,
+ public UniversalDirectoryLock {
+ const NotNull<RefPtr<QuotaManager>> mQuotaManager;
+
+ const Nullable<PersistenceType> mPersistenceType;
+ const nsCString mSuffix;
+ const nsCString mGroup;
+ const OriginScope mOriginScope;
+ const Nullable<Client::Type> mClientType;
+ LazyInitializedOnceEarlyDestructible<
+ const NotNull<RefPtr<OpenDirectoryListener>>>
+ mOpenListener;
+ MozPromiseHolder<BoolPromise> mAcquirePromiseHolder;
+
+ nsTArray<NotNull<DirectoryLockImpl*>> mBlocking;
+ nsTArray<NotNull<DirectoryLockImpl*>> mBlockedOn;
+
+ const int64_t mId;
+
+ const bool mExclusive;
+
+ // Internal quota manager operations use this flag to prevent directory lock
+ // registraction/unregistration from updating origin access time, etc.
+ const bool mInternal;
+
+ const bool mShouldUpdateLockIdTable;
+
+ bool mRegistered;
+ FlippedOnce<true> mPending;
+ FlippedOnce<false> mInvalidated;
+
+#ifdef DEBUG
+ FlippedOnce<false> mAcquired;
+#endif
+
+ public:
+ DirectoryLockImpl(MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ const Nullable<PersistenceType>& aPersistenceType,
+ const nsACString& aSuffix, const nsACString& aGroup,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType, bool aExclusive,
+ bool aInternal,
+ ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag);
+
+ static RefPtr<ClientDirectoryLock> Create(
+ MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata, Client::Type aClientType,
+ bool aExclusive) {
+ return Create(std::move(aQuotaManager),
+ Nullable<PersistenceType>(aPersistenceType),
+ aOriginMetadata.mSuffix, aOriginMetadata.mGroup,
+ OriginScope::FromOrigin(aOriginMetadata.mOrigin),
+ Nullable<Client::Type>(aClientType), aExclusive, false,
+ ShouldUpdateLockIdTableFlag::Yes);
+ }
+
+ static RefPtr<OriginDirectoryLock> CreateForEviction(
+ MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata) {
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_INVALID);
+ MOZ_ASSERT(!aOriginMetadata.mOrigin.IsEmpty());
+
+ return Create(std::move(aQuotaManager),
+ Nullable<PersistenceType>(aPersistenceType),
+ aOriginMetadata.mSuffix, aOriginMetadata.mGroup,
+ OriginScope::FromOrigin(aOriginMetadata.mOrigin),
+ Nullable<Client::Type>(),
+ /* aExclusive */ true, /* aInternal */ true,
+ ShouldUpdateLockIdTableFlag::No);
+ }
+
+ static RefPtr<UniversalDirectoryLock> CreateInternal(
+ MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType, bool aExclusive) {
+ return Create(std::move(aQuotaManager), aPersistenceType, ""_ns, ""_ns,
+ aOriginScope, aClientType, aExclusive, true,
+ ShouldUpdateLockIdTableFlag::Yes);
+ }
+
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ bool IsInternal() const { return mInternal; }
+
+ void SetRegistered(bool aRegistered) { mRegistered = aRegistered; }
+
+ bool IsPending() const { return mPending; }
+
+ // Ideally, we would have just one table (instead of these two:
+ // QuotaManager::mDirectoryLocks and QuotaManager::mDirectoryLockIdTable) for
+ // all registered locks. However, some directory locks need to be accessed off
+ // the PBackground thread, so the access must be protected by the quota mutex.
+ // The problem is that directory locks for eviction must be currently created
+ // while the mutex lock is already acquired. So we decided to have two tables
+ // for now and to not register directory locks for eviction in
+ // QuotaManager::mDirectoryLockIdTable. This can be improved in future after
+ // some refactoring of the mutex locking.
+ bool ShouldUpdateLockIdTable() const { return mShouldUpdateLockIdTable; }
+
+ bool ShouldUpdateLockTable() {
+ return !mInternal &&
+ mPersistenceType.Value() != PERSISTENCE_TYPE_PERSISTENT;
+ }
+
+ bool Overlaps(const DirectoryLockImpl& aLock) const;
+
+ // Test whether this DirectoryLock needs to wait for the given lock.
+ bool MustWaitFor(const DirectoryLockImpl& aLock) const;
+
+ void AddBlockingLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ mBlocking.AppendElement(WrapNotNull(&aLock));
+ }
+
+ const nsTArray<NotNull<DirectoryLockImpl*>>& GetBlockedOnLocks() {
+ return mBlockedOn;
+ }
+
+ void AddBlockedOnLock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ mBlockedOn.AppendElement(WrapNotNull(&aLock));
+ }
+
+ void MaybeUnblock(DirectoryLockImpl& aLock) {
+ AssertIsOnOwningThread();
+
+ mBlockedOn.RemoveElement(&aLock);
+ if (mBlockedOn.IsEmpty()) {
+ NotifyOpenListener();
+ }
+ }
+
+ void NotifyOpenListener();
+
+ void Invalidate() {
+ AssertIsOnOwningThread();
+
+ mInvalidated.EnsureFlipped();
+ }
+
+ // DirectoryLock interface
+
+ NS_INLINE_DECL_REFCOUNTING(DirectoryLockImpl, override)
+
+ int64_t Id() const override { return mId; }
+
+ void Acquire(RefPtr<OpenDirectoryListener> aOpenListener) override;
+
+ RefPtr<BoolPromise> Acquire() override;
+
+ void AcquireImmediately() override;
+
+ void AssertIsAcquiredExclusively() override
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ void Log() const override;
+
+ // OriginDirectoryLock interface
+
+ PersistenceType GetPersistenceType() const override {
+ MOZ_DIAGNOSTIC_ASSERT(!mPersistenceType.IsNull());
+
+ return mPersistenceType.Value();
+ }
+
+ quota::OriginMetadata OriginMetadata() const override {
+ MOZ_DIAGNOSTIC_ASSERT(!mGroup.IsEmpty());
+
+ return quota::OriginMetadata{mSuffix, mGroup, nsCString(Origin()),
+ GetPersistenceType()};
+ }
+
+ const nsACString& Origin() const override {
+ MOZ_DIAGNOSTIC_ASSERT(mOriginScope.IsOrigin());
+ MOZ_DIAGNOSTIC_ASSERT(!mOriginScope.GetOrigin().IsEmpty());
+
+ return mOriginScope.GetOrigin();
+ }
+
+ // ClientDirectoryLock interface
+
+ Client::Type ClientType() const override {
+ MOZ_DIAGNOSTIC_ASSERT(!mClientType.IsNull());
+ MOZ_DIAGNOSTIC_ASSERT(mClientType.Value() < Client::TypeMax());
+
+ return mClientType.Value();
+ }
+
+ // UniversalDirectoryLock interface
+
+ const Nullable<PersistenceType>& NullablePersistenceType() const override {
+ return mPersistenceType;
+ }
+
+ const OriginScope& GetOriginScope() const override { return mOriginScope; }
+
+ const Nullable<Client::Type>& NullableClientType() const override {
+ return mClientType;
+ }
+
+ RefPtr<ClientDirectoryLock> SpecializeForClient(
+ PersistenceType aPersistenceType,
+ const quota::OriginMetadata& aOriginMetadata,
+ Client::Type aClientType) const override;
+
+ private:
+ ~DirectoryLockImpl();
+
+ static RefPtr<DirectoryLockImpl> Create(
+ MovingNotNull<RefPtr<QuotaManager>> aQuotaManager,
+ const Nullable<PersistenceType>& aPersistenceType,
+ const nsACString& aSuffix, const nsACString& aGroup,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType, bool aExclusive,
+ bool aInternal,
+ ShouldUpdateLockIdTableFlag aShouldUpdateLockIdTableFlag) {
+ MOZ_ASSERT_IF(aOriginScope.IsOrigin(), !aOriginScope.GetOrigin().IsEmpty());
+ MOZ_ASSERT_IF(!aInternal, !aPersistenceType.IsNull());
+ MOZ_ASSERT_IF(!aInternal,
+ aPersistenceType.Value() != PERSISTENCE_TYPE_INVALID);
+ MOZ_ASSERT_IF(!aInternal, !aGroup.IsEmpty());
+ MOZ_ASSERT_IF(!aInternal, aOriginScope.IsOrigin());
+ MOZ_ASSERT_IF(!aInternal, !aClientType.IsNull());
+ MOZ_ASSERT_IF(!aInternal, aClientType.Value() < Client::TypeMax());
+
+ return MakeRefPtr<DirectoryLockImpl>(
+ std::move(aQuotaManager), aPersistenceType, aSuffix, aGroup,
+ aOriginScope, aClientType, aExclusive, aInternal,
+ aShouldUpdateLockIdTableFlag);
+ }
+
+ void AcquireInternal();
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_DIRECTORYLOCKIMPL_H_
diff --git a/dom/quota/DummyCipherStrategy.h b/dom/quota/DummyCipherStrategy.h
new file mode 100644
index 0000000000..6c6b7f7b0d
--- /dev/null
+++ b/dom/quota/DummyCipherStrategy.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_DummyCipherStrategy_h
+#define mozilla_dom_quota_DummyCipherStrategy_h
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <utility>
+#include "ErrorList.h"
+#include "mozilla/Result.h"
+#include "mozilla/Span.h"
+#include "mozilla/dom/quota/CipherStrategy.h"
+
+namespace mozilla::dom::quota {
+
+struct DummyCipherStrategy {
+ struct KeyType {};
+
+ static constexpr size_t BlockPrefixLength = 8;
+ static constexpr size_t BasicBlockSize = 4;
+
+ static void DummyTransform(Span<const uint8_t> aIn, Span<uint8_t> aOut) {
+ std::transform(aIn.cbegin(), aIn.cend(), aOut.begin(),
+ [](const uint8_t byte) { return byte ^ 42; });
+ }
+
+ static Result<KeyType, nsresult> GenerateKey() { return KeyType{}; }
+
+ nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey,
+ Span<const uint8_t> aInitialIv = Span<const uint8_t>{}) {
+ return NS_OK;
+ }
+
+ nsresult Cipher(Span<uint8_t> aIv, Span<const uint8_t> aIn,
+ Span<uint8_t> aOut) {
+ DummyTransform(aIn, aOut);
+ return NS_OK;
+ }
+
+ static std::array<uint8_t, BlockPrefixLength> MakeBlockPrefix() {
+ return {{42, 43, 44, 45}};
+ }
+
+ static Span<const uint8_t> SerializeKey(const KeyType&) { return {}; }
+
+ static KeyType DeserializeKey(const Span<const uint8_t>&) { return {}; }
+};
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/EncryptedBlock.h b/dom/quota/EncryptedBlock.h
new file mode 100644
index 0000000000..703fede8fd
--- /dev/null
+++ b/dom/quota/EncryptedBlock.h
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_EncryptedBlock_h
+#define mozilla_dom_quota_EncryptedBlock_h
+
+#include <cstdint>
+#include <cstring>
+#include <limits>
+#include "mozilla/Assertions.h"
+#include "mozilla/Span.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom::quota {
+
+// An encrypted block has the following format:
+// - one basic block containing a uint16_t stating the actual payload length
+// - one basic block containing the cipher prefix (tyipically a nonce)
+// - encrypted payload up to the remainder of the specified overall size
+// We currently assume the basic block size is the same as the cipher prefix
+// length.
+//
+// XXX Actually, we don't need the actual payload length in every block. Only
+// the last block may be incomplete. The tricky thing is just that it might be
+// incomplete by just one or two bytes.
+template <size_t CipherPrefixLength, size_t BasicBlockSize>
+class EncryptedBlock {
+ public:
+ explicit EncryptedBlock(const size_t aOverallSize) {
+ MOZ_RELEASE_ASSERT(aOverallSize >
+ CipherPrefixOffset() + CipherPrefixLength);
+ MOZ_RELEASE_ASSERT(aOverallSize <= std::numeric_limits<uint16_t>::max());
+ // XXX Do we need this to be fallible? Then we need a factory/init function.
+ // But maybe that's not necessary as the block size is not user-provided and
+ // small.
+ mData.SetLength(aOverallSize);
+ SetActualPayloadLength(MaxPayloadLength());
+ }
+
+ size_t MaxPayloadLength() const {
+ return mData.Length() - CipherPrefixLength - CipherPrefixOffset();
+ }
+
+ void SetActualPayloadLength(uint16_t aActualPayloadLength) {
+ memcpy(mData.Elements(), &aActualPayloadLength, sizeof(uint16_t));
+ }
+ size_t ActualPayloadLength() const {
+ return *reinterpret_cast<const uint16_t*>(mData.Elements());
+ }
+
+ using ConstSpan = Span<const uint8_t>;
+ using MutableSpan = Span<uint8_t>;
+
+ ConstSpan CipherPrefix() const {
+ return WholeBlock().Subspan(CipherPrefixOffset(), CipherPrefixLength);
+ }
+ MutableSpan MutableCipherPrefix() {
+ return MutableWholeBlock().Subspan(CipherPrefixOffset(),
+ CipherPrefixLength);
+ }
+
+ ConstSpan Payload() const {
+ return WholeBlock()
+ .SplitAt(CipherPrefixOffset() + CipherPrefixLength)
+ .second.First(RoundedUpToBasicBlockSize(ActualPayloadLength()));
+ }
+ MutableSpan MutablePayload() {
+ return MutableWholeBlock()
+ .SplitAt(CipherPrefixOffset() + CipherPrefixLength)
+ .second.First(RoundedUpToBasicBlockSize(ActualPayloadLength()));
+ }
+
+ ConstSpan WholeBlock() const { return mData; }
+ MutableSpan MutableWholeBlock() { return mData; }
+
+ private:
+ static constexpr size_t CipherPrefixOffset() {
+ return RoundedUpToBasicBlockSize(sizeof(uint16_t));
+ }
+
+ static constexpr size_t RoundedUpToBasicBlockSize(const size_t aValue) {
+ return (aValue + BasicBlockSize - 1) / BasicBlockSize * BasicBlockSize;
+ }
+
+ nsTArray<uint8_t> mData; ///< XXX use some "safe memory" here?
+};
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/EncryptingOutputStream.cpp b/dom/quota/EncryptingOutputStream.cpp
new file mode 100644
index 0000000000..80279eae27
--- /dev/null
+++ b/dom/quota/EncryptingOutputStream.cpp
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EncryptingOutputStream.h"
+#include "EncryptingOutputStream_impl.h"
+
+#include <type_traits>
+#include "mozilla/MacroForEach.h"
+#include "nsStreamUtils.h"
+
+namespace mozilla::dom::quota {
+
+NS_IMPL_ISUPPORTS(EncryptingOutputStreamBase, nsIOutputStream);
+
+EncryptingOutputStreamBase::EncryptingOutputStreamBase(
+ nsCOMPtr<nsIOutputStream> aBaseStream, size_t aBlockSize)
+ : mBaseStream(WrapNotNull(std::move(aBaseStream))),
+ mBlockSize(aBlockSize) {}
+
+NS_IMETHODIMP EncryptingOutputStreamBase::Write(const char* aBuf,
+ uint32_t aCount,
+ uint32_t* aResultOut) {
+ return WriteSegments(NS_CopyBufferToSegment, const_cast<char*>(aBuf), aCount,
+ aResultOut);
+}
+
+NS_IMETHODIMP EncryptingOutputStreamBase::WriteFrom(nsIInputStream*, uint32_t,
+ uint32_t*) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP EncryptingOutputStreamBase::IsNonBlocking(bool* aNonBlockingOut) {
+ *aNonBlockingOut = false;
+ return NS_OK;
+}
+
+nsresult EncryptingOutputStreamBase::WriteAll(const char* aBuf, uint32_t aCount,
+ uint32_t* aBytesWrittenOut) {
+ *aBytesWrittenOut = 0;
+
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ uint32_t offset = 0;
+ while (aCount > 0) {
+ uint32_t numWritten = 0;
+ nsresult rv = (*mBaseStream)->Write(aBuf + offset, aCount, &numWritten);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ offset += numWritten;
+ aCount -= numWritten;
+ *aBytesWrittenOut += numWritten;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/EncryptingOutputStream.h b/dom/quota/EncryptingOutputStream.h
new file mode 100644
index 0000000000..adcfe73155
--- /dev/null
+++ b/dom/quota/EncryptingOutputStream.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_EncryptingOutputStream_h
+#define mozilla_dom_quota_EncryptingOutputStream_h
+
+// Local includes
+#include "EncryptedBlock.h" // for EncryptedBlock
+
+// Global includes
+#include <cstddef>
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/NotNull.h"
+#include "nsCOMPtr.h"
+#include "nsIOutputStream.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+#include "nscore.h"
+
+class nsIInputStream;
+
+namespace mozilla::dom::quota {
+class EncryptingOutputStreamBase : public nsIOutputStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) final;
+ NS_IMETHOD WriteFrom(nsIInputStream* aFromStream, uint32_t aCount,
+ uint32_t* _retval) final;
+ NS_IMETHOD IsNonBlocking(bool* _retval) final;
+
+ protected:
+ EncryptingOutputStreamBase(nsCOMPtr<nsIOutputStream> aBaseStream,
+ size_t aBlockSize);
+
+ virtual ~EncryptingOutputStreamBase() = default;
+
+ nsresult WriteAll(const char* aBuf, uint32_t aCount,
+ uint32_t* aBytesWrittenOut);
+
+ InitializedOnce<const NotNull<nsCOMPtr<nsIOutputStream>>> mBaseStream;
+ const size_t mBlockSize;
+};
+
+// Wraps another nsIOutputStream using the CipherStrategy to encrypt it a
+// page-based manner. Essentially, the CipherStrategy is not actually
+// necessarily doing encryption, but any transformation to a page requiring some
+// fixed-size reserved size per page.
+//
+// Paired with DecryptingInputStream which can be used to read the data written
+// to the underlying stream, using the same (or more generally, a compatible)
+// CipherStrategy, when created with the same key (assuming a symmetric cipher
+// is being used; in principle, an asymmetric cipher would probably also work).
+template <typename CipherStrategy>
+class EncryptingOutputStream final : public EncryptingOutputStreamBase {
+ public:
+ // Construct a new blocking output stream to encrypt data to
+ // the given base stream. The base stream must also be blocking.
+ // The encryption block size may optionally be set to a value
+ // up to kMaxBlockSize.
+ explicit EncryptingOutputStream(nsCOMPtr<nsIOutputStream> aBaseStream,
+ size_t aBlockSize,
+ typename CipherStrategy::KeyType aKey);
+
+ private:
+ ~EncryptingOutputStream();
+
+ nsresult FlushToBaseStream();
+
+ bool EnsureBuffers();
+
+ CipherStrategy mCipherStrategy;
+
+ // Buffer holding copied plain data. This must be copied here
+ // so that the encryption can be performed on a single flat buffer.
+ // XXX This is only necessary if the data written doesn't contain a portion of
+ // effective block size at a block boundary.
+ nsTArray<uint8_t> mBuffer;
+
+ // The next byte in the plain data to copy incoming data to.
+ size_t mNextByte = 0;
+
+ // Buffer holding the resulting encrypted data.
+ using EncryptedBlockType = EncryptedBlock<CipherStrategy::BlockPrefixLength,
+ CipherStrategy::BasicBlockSize>;
+ Maybe<EncryptedBlockType> mEncryptedBlock;
+
+ public:
+ NS_IMETHOD Close() override;
+ NS_IMETHOD Flush() override;
+ NS_IMETHOD WriteSegments(nsReadSegmentFun aReader, void* aClosure,
+ uint32_t aCount, uint32_t* _retval) override;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/EncryptingOutputStream_impl.h b/dom/quota/EncryptingOutputStream_impl.h
new file mode 100644
index 0000000000..25ba7a26a3
--- /dev/null
+++ b/dom/quota/EncryptingOutputStream_impl.h
@@ -0,0 +1,246 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_EncryptingOutputStream_impl_h
+#define mozilla_dom_quota_EncryptingOutputStream_impl_h
+
+#include "EncryptingOutputStream.h"
+
+#include <algorithm>
+#include <utility>
+#include "CipherStrategy.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Span.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIAsyncOutputStream.h"
+
+namespace mozilla::dom::quota {
+template <typename CipherStrategy>
+EncryptingOutputStream<CipherStrategy>::EncryptingOutputStream(
+ nsCOMPtr<nsIOutputStream> aBaseStream, size_t aBlockSize,
+ typename CipherStrategy::KeyType aKey)
+ : EncryptingOutputStreamBase(std::move(aBaseStream), aBlockSize) {
+ // XXX Move this to a fallible init function.
+ MOZ_ALWAYS_SUCCEEDS(mCipherStrategy.Init(CipherMode::Encrypt,
+ CipherStrategy::SerializeKey(aKey),
+ CipherStrategy::MakeBlockPrefix()));
+
+ MOZ_ASSERT(mBlockSize > 0);
+ MOZ_ASSERT(mBlockSize % CipherStrategy::BasicBlockSize == 0);
+ static_assert(
+ CipherStrategy::BlockPrefixLength % CipherStrategy::BasicBlockSize == 0);
+
+ // This implementation only supports sync base streams. Verify this in debug
+ // builds. Note, this is a bit complicated because the streams we support
+ // advertise different capabilities:
+ // - nsFileOutputStream - blocking and sync
+ // - FixedBufferOutputStream - non-blocking and sync
+ // - nsPipeOutputStream - can be blocking, but provides async interface
+#ifdef DEBUG
+ bool baseNonBlocking;
+ nsresult rv = (*mBaseStream)->IsNonBlocking(&baseNonBlocking);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ if (baseNonBlocking) {
+ nsCOMPtr<nsIAsyncOutputStream> async =
+ do_QueryInterface((*mBaseStream).get());
+ MOZ_ASSERT(!async);
+ }
+#endif
+}
+
+template <typename CipherStrategy>
+EncryptingOutputStream<CipherStrategy>::~EncryptingOutputStream() {
+ Close();
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Close() {
+ if (!mBaseStream) {
+ return NS_OK;
+ }
+
+ // When closing, flush to the base stream unconditionally, i.e. even if the
+ // buffer is not completely full.
+ nsresult rv = FlushToBaseStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // XXX Maybe this Flush call can be removed, since the base stream is closed
+ // afterwards anyway.
+ rv = (*mBaseStream)->Flush();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // XXX What if closing the base stream failed? Fail this method, or at least
+ // log a warning?
+ (*mBaseStream)->Close();
+ mBaseStream.destroy();
+
+ mBuffer.Clear();
+ mEncryptedBlock.reset();
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::Flush() {
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!EnsureBuffers()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // We cannot call FlushBaseStream() here if the buffer is not completely
+ // full, we would write an incomplete page, which might be read sequentially,
+ // but we want to support random accesses in DecryptingInputStream, which
+ // would no longer be feasible.
+ if (mNextByte && mNextByte == mEncryptedBlock->MaxPayloadLength()) {
+ nsresult rv = FlushToBaseStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return (*mBaseStream)->Flush();
+}
+
+template <typename CipherStrategy>
+NS_IMETHODIMP EncryptingOutputStream<CipherStrategy>::WriteSegments(
+ nsReadSegmentFun aReader, void* aClosure, uint32_t aCount,
+ uint32_t* aBytesWrittenOut) {
+ *aBytesWrittenOut = 0;
+
+ if (!mBaseStream) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (!EnsureBuffers()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ const size_t plainBufferSize = mEncryptedBlock->MaxPayloadLength();
+
+ while (aCount > 0) {
+ // Determine how much space is left in our flat, plain buffer.
+ MOZ_ASSERT(mNextByte <= plainBufferSize);
+ uint32_t remaining = plainBufferSize - mNextByte;
+
+ // If it is full, then encrypt and flush the data to the base stream.
+ if (remaining == 0) {
+ nsresult rv = FlushToBaseStream();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Now the entire buffer should be available for copying.
+ MOZ_ASSERT(!mNextByte);
+ remaining = plainBufferSize;
+ }
+
+ uint32_t numToRead = std::min(remaining, aCount);
+ uint32_t numRead = 0;
+
+ nsresult rv =
+ aReader(this, aClosure, reinterpret_cast<char*>(&mBuffer[mNextByte]),
+ *aBytesWrittenOut, numToRead, &numRead);
+
+ // As defined in nsIOutputStream.idl, do not pass reader func errors.
+ if (NS_FAILED(rv)) {
+ return NS_OK;
+ }
+
+ // End-of-file
+ if (numRead == 0) {
+ return NS_OK;
+ }
+
+ mNextByte += numRead;
+ *aBytesWrittenOut += numRead;
+ aCount -= numRead;
+ }
+
+ return NS_OK;
+}
+
+template <typename CipherStrategy>
+bool EncryptingOutputStream<CipherStrategy>::EnsureBuffers() {
+ // Lazily create the encrypted buffer on our first flush. This
+ // allows us to report OOM during stream operation. This buffer
+ // will then get re-used until the stream is closed.
+ if (!mEncryptedBlock) {
+ // XXX Do we need to do this fallible (as the comment above suggests)?
+ mEncryptedBlock.emplace(mBlockSize);
+ MOZ_ASSERT(mBuffer.IsEmpty());
+
+ if (NS_WARN_IF(!mBuffer.SetLength(mEncryptedBlock->MaxPayloadLength(),
+ fallible))) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+template <typename CipherStrategy>
+nsresult EncryptingOutputStream<CipherStrategy>::FlushToBaseStream() {
+ MOZ_ASSERT(mBaseStream);
+
+ if (!mNextByte) {
+ // Nothing to do.
+ return NS_OK;
+ }
+
+ // XXX The compressing stream implementation this was based on wrote a stream
+ // identifier, containing e.g. the block size. Should we do something like
+ // that as well? At the moment, we don't need it, but maybe this were
+ // convenient if we use this for persistent files in the future across version
+ // updates, which might change such parameters.
+
+ const auto iv = mCipherStrategy.MakeBlockPrefix();
+ static_assert(iv.size() * sizeof(decltype(*iv.begin())) ==
+ CipherStrategy::BlockPrefixLength);
+ std::copy(iv.cbegin(), iv.cend(),
+ mEncryptedBlock->MutableCipherPrefix().begin());
+
+ // Encrypt the data to our internal encrypted buffer.
+ // XXX Do we need to know the actual encrypted size?
+ nsresult rv = mCipherStrategy.Cipher(
+ mEncryptedBlock->MutableCipherPrefix(),
+ mozilla::Span(reinterpret_cast<uint8_t*>(mBuffer.Elements()),
+ ((mNextByte + (CipherStrategy::BasicBlockSize - 1)) /
+ CipherStrategy::BasicBlockSize) *
+ CipherStrategy::BasicBlockSize),
+ mEncryptedBlock->MutablePayload());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mEncryptedBlock->SetActualPayloadLength(mNextByte);
+
+ mNextByte = 0;
+
+ // Write the encrypted buffer out to the base stream.
+ uint32_t numWritten = 0;
+ const auto& wholeBlock = mEncryptedBlock->WholeBlock();
+ rv = WriteAll(AsChars(wholeBlock).Elements(), wholeBlock.Length(),
+ &numWritten);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ MOZ_ASSERT(wholeBlock.Length() == numWritten);
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/FileStreams.cpp b/dom/quota/FileStreams.cpp
new file mode 100644
index 0000000000..22419fe70c
--- /dev/null
+++ b/dom/quota/FileStreams.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileStreams.h"
+
+// Local includes
+#include "QuotaCommon.h"
+#include "QuotaManager.h"
+#include "QuotaObject.h"
+#include "RemoteQuotaObject.h"
+
+// Global includes
+#include <utility>
+#include "mozilla/Assertions.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/ipc/RandomAccessStreamParams.h"
+#include "nsDebug.h"
+#include "prio.h"
+
+namespace mozilla::dom::quota {
+
+template <class FileStreamBase>
+NS_IMETHODIMP FileQuotaStream<FileStreamBase>::SetEOF() {
+ // If the stream is not quota tracked, or on an early or late stage in the
+ // lifecycle, mQuotaObject is null. Under these circumstances,
+ // we don't check the quota limit in order to avoid breakage.
+ if (mQuotaObject) {
+ int64_t offset = 0;
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::Tell(&offset)));
+
+ QM_TRY(OkIf(mQuotaObject->MaybeUpdateSize(offset, /* aTruncate */ true)),
+ NS_ERROR_FILE_NO_DEVICE_SPACE);
+ }
+
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::SetEOF()));
+
+ return NS_OK;
+}
+
+template <class FileStreamBase>
+NS_IMETHODIMP FileQuotaStream<FileStreamBase>::Close() {
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::Close()));
+
+ if (mQuotaObject) {
+ if (auto* remoteQuotaObject = mQuotaObject->AsRemoteQuotaObject()) {
+ remoteQuotaObject->Close();
+ }
+
+ mQuotaObject = nullptr;
+ }
+
+ return NS_OK;
+}
+
+template <class FileStreamBase>
+nsresult FileQuotaStream<FileStreamBase>::DoOpen() {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(!mDeserialized);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager, "Shouldn't be null!");
+
+ MOZ_ASSERT(!mQuotaObject, "Creating quota object more than once?");
+ mQuotaObject = quotaManager->GetQuotaObject(
+ mPersistenceType, mOriginMetadata, mClientType,
+ FileStreamBase::mOpenParams.localFile);
+
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::DoOpen()));
+
+ if (mQuotaObject && (FileStreamBase::mOpenParams.ioFlags & PR_TRUNCATE)) {
+ DebugOnly<bool> res =
+ mQuotaObject->MaybeUpdateSize(0, /* aTruncate */ true);
+ MOZ_ASSERT(res);
+ }
+
+ return NS_OK;
+}
+
+template <class FileStreamBase>
+NS_IMETHODIMP FileQuotaStreamWithWrite<FileStreamBase>::Write(
+ const char* aBuf, uint32_t aCount, uint32_t* _retval) {
+ if (FileQuotaStreamWithWrite::mQuotaObject) {
+ int64_t offset;
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::Tell(&offset)));
+
+ MOZ_ASSERT(INT64_MAX - offset >= int64_t(aCount));
+
+ if (!FileQuotaStreamWithWrite::mQuotaObject->MaybeUpdateSize(
+ offset + int64_t(aCount),
+ /* aTruncate */ false)) {
+ *_retval = 0;
+ return NS_ERROR_FILE_NO_DEVICE_SPACE;
+ }
+ }
+
+ QM_TRY(MOZ_TO_RESULT(FileStreamBase::Write(aBuf, aCount, _retval)));
+
+ return NS_OK;
+}
+
+mozilla::ipc::RandomAccessStreamParams FileRandomAccessStream::Serialize(
+ nsIInterfaceRequestor* aCallbacks) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(!mDeserialized);
+ MOZ_ASSERT(mOpenParams.localFile);
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ RefPtr<QuotaObject> quotaObject = quotaManager->GetQuotaObject(
+ mPersistenceType, mOriginMetadata, mClientType, mOpenParams.localFile);
+ MOZ_ASSERT(quotaObject);
+
+ IPCQuotaObject ipcQuotaObject = quotaObject->Serialize(aCallbacks);
+
+ mozilla::ipc::RandomAccessStreamParams randomAccessStreamParams =
+ nsFileRandomAccessStream::Serialize(aCallbacks);
+
+ MOZ_ASSERT(
+ randomAccessStreamParams.type() ==
+ mozilla::ipc::RandomAccessStreamParams::TFileRandomAccessStreamParams);
+
+ mozilla::ipc::LimitingFileRandomAccessStreamParams
+ limitingFileRandomAccessStreamParams;
+ limitingFileRandomAccessStreamParams.fileRandomAccessStreamParams() =
+ std::move(randomAccessStreamParams);
+ limitingFileRandomAccessStreamParams.quotaObject() =
+ std::move(ipcQuotaObject);
+
+ return limitingFileRandomAccessStreamParams;
+}
+
+bool FileRandomAccessStream::Deserialize(
+ mozilla::ipc::RandomAccessStreamParams& aParams) {
+ MOZ_ASSERT(aParams.type() == mozilla::ipc::RandomAccessStreamParams::
+ TLimitingFileRandomAccessStreamParams);
+
+ auto& params = aParams.get_LimitingFileRandomAccessStreamParams();
+
+ mozilla::ipc::RandomAccessStreamParams randomAccessStreamParams(
+ std::move(params.fileRandomAccessStreamParams()));
+
+ QM_TRY(MOZ_TO_RESULT(
+ nsFileRandomAccessStream::Deserialize(randomAccessStreamParams)),
+ false);
+
+ mQuotaObject = QuotaObject::Deserialize(params.quotaObject());
+
+ return true;
+}
+
+Result<MovingNotNull<nsCOMPtr<nsIInputStream>>, nsresult> CreateFileInputStream(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile, int32_t aIOFlags, int32_t aPerm,
+ int32_t aBehaviorFlags) {
+ auto stream = MakeRefPtr<FileInputStream>(aPersistenceType, aOriginMetadata,
+ aClientType);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags)));
+
+ return WrapMovingNotNullUnchecked(
+ nsCOMPtr<nsIInputStream>(std::move(stream)));
+}
+
+Result<MovingNotNull<nsCOMPtr<nsIOutputStream>>, nsresult>
+CreateFileOutputStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile,
+ int32_t aIOFlags, int32_t aPerm,
+ int32_t aBehaviorFlags) {
+ auto stream = MakeRefPtr<FileOutputStream>(aPersistenceType, aOriginMetadata,
+ aClientType);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags)));
+
+ return WrapMovingNotNullUnchecked(
+ nsCOMPtr<nsIOutputStream>(std::move(stream)));
+}
+
+Result<MovingNotNull<nsCOMPtr<nsIRandomAccessStream>>, nsresult>
+CreateFileRandomAccessStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile,
+ int32_t aIOFlags, int32_t aPerm,
+ int32_t aBehaviorFlags) {
+ auto stream = MakeRefPtr<FileRandomAccessStream>(
+ aPersistenceType, aOriginMetadata, aClientType);
+
+ QM_TRY(MOZ_TO_RESULT(stream->Init(aFile, aIOFlags, aPerm, aBehaviorFlags)));
+
+ return WrapMovingNotNullUnchecked(
+ nsCOMPtr<nsIRandomAccessStream>(std::move(stream)));
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/FileStreams.h b/dom/quota/FileStreams.h
new file mode 100644
index 0000000000..9a91b474c5
--- /dev/null
+++ b/dom/quota/FileStreams.h
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_filestreams_h__
+#define mozilla_dom_quota_filestreams_h__
+
+// Local includes
+#include "Client.h"
+
+// Global includes
+#include <cstdint>
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaObject.h"
+#include "nsFileStreams.h"
+#include "nsISupports.h"
+#include "nscore.h"
+
+class nsIFile;
+
+namespace mozilla {
+class Runnable;
+}
+
+namespace mozilla::dom::quota {
+
+class QuotaObject;
+
+template <class FileStreamBase>
+class FileQuotaStream : public FileStreamBase {
+ public:
+ // nsFileStreamBase override
+ NS_IMETHOD
+ SetEOF() override;
+
+ NS_IMETHOD
+ Close() override;
+
+ protected:
+ FileQuotaStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType)
+ : mPersistenceType(aPersistenceType),
+ mOriginMetadata(aOriginMetadata),
+ mClientType(aClientType),
+ mDeserialized(false) {}
+
+ FileQuotaStream()
+ : mPersistenceType(PERSISTENCE_TYPE_INVALID),
+ mClientType(Client::TYPE_MAX),
+ mDeserialized(true) {}
+
+ ~FileQuotaStream() { Close(); }
+
+ // nsFileStreamBase override
+ virtual nsresult DoOpen() override;
+
+ PersistenceType mPersistenceType;
+ OriginMetadata mOriginMetadata;
+ Client::Type mClientType;
+ RefPtr<QuotaObject> mQuotaObject;
+ const bool mDeserialized;
+};
+
+template <class FileStreamBase>
+class FileQuotaStreamWithWrite : public FileQuotaStream<FileStreamBase> {
+ public:
+ // nsFileStreamBase override
+ NS_IMETHOD
+ Write(const char* aBuf, uint32_t aCount, uint32_t* _retval) override;
+
+ protected:
+ FileQuotaStreamWithWrite(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType)
+ : FileQuotaStream<FileStreamBase>(aPersistenceType, aOriginMetadata,
+ aClientType) {}
+
+ FileQuotaStreamWithWrite() = default;
+};
+
+class FileInputStream : public FileQuotaStream<nsFileInputStream> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(FileInputStream,
+ FileQuotaStream<nsFileInputStream>)
+
+ FileInputStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType)
+ : FileQuotaStream<nsFileInputStream>(aPersistenceType, aOriginMetadata,
+ aClientType) {}
+
+ private:
+ virtual ~FileInputStream() { Close(); }
+};
+
+class FileOutputStream : public FileQuotaStreamWithWrite<nsFileOutputStream> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(
+ FileOutputStream, FileQuotaStreamWithWrite<nsFileOutputStream>);
+
+ FileOutputStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType)
+ : FileQuotaStreamWithWrite<nsFileOutputStream>(
+ aPersistenceType, aOriginMetadata, aClientType) {}
+
+ private:
+ virtual ~FileOutputStream() { Close(); }
+};
+
+// FileRandomAccessStream type is serializable, but only in a restricted
+// manner. The type is only safe to serialize in the parent process and only
+// when the type hasn't been previously deserialized. So the type can be
+// serialized in the parent process and desrialized in a child process or it
+// can be serialized in the parent process and deserialized in the parent
+// process as well (non-e10s mode). The same type can never be
+// serialized/deserialized more than once.
+class FileRandomAccessStream
+ : public FileQuotaStreamWithWrite<nsFileRandomAccessStream> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(
+ FileRandomAccessStream,
+ FileQuotaStreamWithWrite<nsFileRandomAccessStream>)
+
+ FileRandomAccessStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType)
+ : FileQuotaStreamWithWrite<nsFileRandomAccessStream>(
+ aPersistenceType, aOriginMetadata, aClientType) {}
+
+ FileRandomAccessStream() = default;
+
+ // nsFileRandomAccessStream override
+
+ // Serialize this FileRandomAccessStream. This method works only in the
+ // parent process and only with streams which haven't been previously
+ // deserialized.
+ mozilla::ipc::RandomAccessStreamParams Serialize(
+ nsIInterfaceRequestor* aCallbacks) override;
+
+ // Deserialize this FileRandomAccessStream. This method works in both the
+ // child and parent.
+ bool Deserialize(mozilla::ipc::RandomAccessStreamParams& aParams) override;
+
+ private:
+ virtual ~FileRandomAccessStream() { Close(); }
+};
+
+Result<MovingNotNull<nsCOMPtr<nsIInputStream>>, nsresult> CreateFileInputStream(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile, int32_t aIOFlags = -1,
+ int32_t aPerm = -1, int32_t aBehaviorFlags = 0);
+
+Result<MovingNotNull<nsCOMPtr<nsIOutputStream>>, nsresult>
+CreateFileOutputStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile,
+ int32_t aIOFlags = -1, int32_t aPerm = -1,
+ int32_t aBehaviorFlags = 0);
+
+Result<MovingNotNull<nsCOMPtr<nsIRandomAccessStream>>, nsresult>
+CreateFileRandomAccessStream(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile,
+ int32_t aIOFlags = -1, int32_t aPerm = -1,
+ int32_t aBehaviorFlags = 0);
+
+} // namespace mozilla::dom::quota
+
+#endif /* mozilla_dom_quota_filestreams_h__ */
diff --git a/dom/quota/FirstInitializationAttempts.h b/dom/quota/FirstInitializationAttempts.h
new file mode 100644
index 0000000000..e997985371
--- /dev/null
+++ b/dom/quota/FirstInitializationAttempts.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_
+#define DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_
+
+#include <cstdint>
+#include <utility>
+#include "ErrorList.h"
+
+namespace mozilla::dom::quota {
+
+template <typename Initialization, typename StringGenerator>
+class FirstInitializationAttempts {
+ Initialization mFirstInitializationAttempts = Initialization::None;
+
+ public:
+ class FirstInitializationAttemptImpl {
+ using FirstInitializationAttemptsType =
+ FirstInitializationAttempts<Initialization, StringGenerator>;
+
+ FirstInitializationAttemptsType& mOwner;
+ const Initialization mInitialization;
+
+ public:
+ FirstInitializationAttemptImpl(FirstInitializationAttemptsType& aOwner,
+ const Initialization aInitialization)
+ : mOwner(aOwner), mInitialization(aInitialization) {}
+
+ bool Recorded() const {
+ return mOwner.FirstInitializationAttemptRecorded(mInitialization);
+ }
+
+ void Record(const nsresult aRv) const {
+ mOwner.RecordFirstInitializationAttempt(mInitialization, aRv);
+ }
+ };
+
+ template <typename Func>
+ auto WithFirstInitializationAttempt(const Initialization aInitialization,
+ Func&& aFunc)
+ -> std::invoke_result_t<Func, FirstInitializationAttemptImpl&&> {
+ return std::forward<Func>(aFunc)(
+ FirstInitializationAttemptImpl(*this, aInitialization));
+ }
+
+ bool FirstInitializationAttemptRecorded(
+ const Initialization aInitialization) const {
+ return static_cast<bool>(mFirstInitializationAttempts & aInitialization);
+ }
+
+ void RecordFirstInitializationAttempt(const Initialization aInitialization,
+ nsresult aRv);
+
+ void ResetFirstInitializationAttempts() {
+ mFirstInitializationAttempts = Initialization::None;
+ }
+};
+
+template <typename Initialization, typename StringGenerator>
+using FirstInitializationAttempt = typename FirstInitializationAttempts<
+ Initialization, StringGenerator>::FirstInitializationAttemptImpl;
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTS_H_
diff --git a/dom/quota/FirstInitializationAttemptsImpl.h b/dom/quota/FirstInitializationAttemptsImpl.h
new file mode 100644
index 0000000000..79ae3daae0
--- /dev/null
+++ b/dom/quota/FirstInitializationAttemptsImpl.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_
+#define DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_
+
+#include "FirstInitializationAttempts.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "nsError.h"
+
+namespace mozilla::dom::quota {
+
+template <typename Initialization, typename StringGenerator>
+void FirstInitializationAttempts<Initialization, StringGenerator>::
+ RecordFirstInitializationAttempt(const Initialization aInitialization,
+ const nsresult aRv) {
+ MOZ_ASSERT(!FirstInitializationAttemptRecorded(aInitialization));
+
+ mFirstInitializationAttempts |= aInitialization;
+
+ if constexpr (!std::is_same_v<StringGenerator, Nothing>) {
+ Telemetry::Accumulate(Telemetry::QM_FIRST_INITIALIZATION_ATTEMPT,
+ StringGenerator::GetString(aInitialization),
+ static_cast<uint32_t>(NS_SUCCEEDED(aRv)));
+ }
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_FIRSTINITIALIZATIONATTEMPTSIMPL_H_
diff --git a/dom/quota/Flatten.h b/dom/quota/Flatten.h
new file mode 100644
index 0000000000..2eb29c6010
--- /dev/null
+++ b/dom/quota/Flatten.h
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_FLATTEN_H_
+#define DOM_QUOTA_FLATTEN_H_
+
+#include <iterator>
+#include <type_traits>
+#include <utility>
+
+// XXX This should be moved to MFBT.
+
+namespace mozilla::dom::quota {
+
+namespace detail {
+
+using std::begin;
+using std::end;
+
+template <typename T, typename NestedRange>
+auto Flatten(NestedRange&& aRange) -> std::enable_if_t<
+ std::is_same_v<T, std::decay_t<typename decltype(begin(
+ std::declval<const NestedRange&>()))::value_type>>,
+ std::conditional_t<std::is_rvalue_reference_v<NestedRange>,
+ std::decay_t<NestedRange>, NestedRange>> {
+ return std::forward<NestedRange>(aRange);
+}
+
+template <typename T, typename NestedRange>
+struct FlatIter {
+ using OuterIterator =
+ decltype(begin(std::declval<const std::decay_t<NestedRange>&>()));
+ using InnerIterator =
+ decltype(begin(*begin(std::declval<const std::decay_t<NestedRange>&>())));
+
+ explicit FlatIter(const NestedRange& aRange, OuterIterator aIter)
+ : mOuterIter{std::move(aIter)}, mOuterEnd{end(aRange)} {
+ InitInner();
+ }
+
+ const T& operator*() const { return *mInnerIter; }
+
+ FlatIter& operator++() {
+ ++mInnerIter;
+ if (mInnerIter == mInnerEnd) {
+ ++mOuterIter;
+ InitInner();
+ }
+ return *this;
+ }
+
+ bool operator!=(const FlatIter& aOther) const {
+ return mOuterIter != aOther.mOuterIter ||
+ (mOuterIter != mOuterEnd && mInnerIter != aOther.mInnerIter);
+ }
+
+ private:
+ void InitInner() {
+ while (mOuterIter != mOuterEnd) {
+ const typename OuterIterator::value_type& innerRange = *mOuterIter;
+
+ mInnerIter = begin(innerRange);
+ mInnerEnd = end(innerRange);
+
+ if (mInnerIter != mInnerEnd) {
+ break;
+ }
+
+ ++mOuterIter;
+ }
+ }
+
+ OuterIterator mOuterIter;
+ const OuterIterator mOuterEnd;
+
+ InnerIterator mInnerIter;
+ InnerIterator mInnerEnd;
+};
+
+template <typename T, typename NestedRange>
+struct FlatRange {
+ explicit FlatRange(NestedRange aRange) : mRange{std::move(aRange)} {}
+
+ auto begin() const {
+ using std::begin;
+ return FlatIter<T, NestedRange>{mRange, begin(mRange)};
+ }
+ auto end() const {
+ using std::end;
+ return FlatIter<T, NestedRange>{mRange, end(mRange)};
+ }
+
+ private:
+ NestedRange mRange;
+};
+
+template <typename T, typename NestedRange>
+auto Flatten(NestedRange&& aRange) -> std::enable_if_t<
+ !std::is_same_v<
+ T, std::decay_t<typename decltype(begin(
+ std::declval<const std::decay_t<NestedRange>&>()))::value_type>>,
+ FlatRange<T, NestedRange>> {
+ return FlatRange<T, NestedRange>{std::forward<NestedRange>(aRange)};
+}
+
+} // namespace detail
+
+template <typename T, typename NestedRange>
+auto Flatten(NestedRange&& aRange) -> decltype(auto) {
+ return detail::Flatten<T>(std::forward<NestedRange>(aRange));
+}
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/ForwardDecls.h b/dom/quota/ForwardDecls.h
new file mode 100644
index 0000000000..6989e91d69
--- /dev/null
+++ b/dom/quota/ForwardDecls.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_FORWARD_DECLS_H_
+#define DOM_QUOTA_FORWARD_DECLS_H_
+
+#include <cstdint>
+#include "mozilla/dom/quota/Config.h"
+
+enum class nsresult : uint32_t;
+
+namespace mozilla {
+
+#ifdef QM_ERROR_STACKS_ENABLED
+class QMResult;
+#else
+using QMResult = nsresult;
+#endif
+
+struct Ok;
+template <typename V, typename E>
+class Result;
+
+using OkOrErr = Result<Ok, QMResult>;
+
+template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
+class MozPromise;
+
+using BoolPromise = MozPromise<bool, nsresult, false>;
+using Int64Promise = MozPromise<int64_t, nsresult, false>;
+
+} // namespace mozilla
+
+#endif // DOM_QUOTA_FORWARD_DECLS_H_
diff --git a/dom/quota/GroupInfo.cpp b/dom/quota/GroupInfo.cpp
new file mode 100644
index 0000000000..96f4f806f6
--- /dev/null
+++ b/dom/quota/GroupInfo.cpp
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GroupInfo.h"
+
+#include "mozilla/dom/quota/AssertionsImpl.h"
+#include "OriginInfo.h"
+
+namespace mozilla::dom::quota {
+
+already_AddRefed<OriginInfo> GroupInfo::LockedGetOriginInfo(
+ const nsACString& aOrigin) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ for (const auto& originInfo : mOriginInfos) {
+ if (originInfo->mOrigin == aOrigin) {
+ RefPtr<OriginInfo> result = originInfo;
+ return result.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+void GroupInfo::LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ NS_ASSERTION(!mOriginInfos.Contains(aOriginInfo),
+ "Replacing an existing entry!");
+ mOriginInfos.AppendElement(std::move(aOriginInfo));
+
+ uint64_t usage = aOriginInfo->LockedUsage();
+
+ if (!aOriginInfo->LockedPersisted()) {
+ AssertNoOverflow(mUsage, usage);
+ mUsage += usage;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ AssertNoOverflow(quotaManager->mTemporaryStorageUsage, usage);
+ quotaManager->mTemporaryStorageUsage += usage;
+}
+
+void GroupInfo::LockedAdjustUsageForRemovedOriginInfo(
+ const OriginInfo& aOriginInfo) {
+ const uint64_t usage = aOriginInfo.LockedUsage();
+
+ if (!aOriginInfo.LockedPersisted()) {
+ AssertNoUnderflow(mUsage, usage);
+ mUsage -= usage;
+ }
+
+ QuotaManager* const quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, usage);
+ quotaManager->mTemporaryStorageUsage -= usage;
+}
+
+void GroupInfo::LockedRemoveOriginInfo(const nsACString& aOrigin) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ const auto foundIt = std::find_if(mOriginInfos.cbegin(), mOriginInfos.cend(),
+ [&aOrigin](const auto& originInfo) {
+ return originInfo->mOrigin == aOrigin;
+ });
+
+ // XXX Or can we MOZ_ASSERT(foundIt != mOriginInfos.cend()) ?
+ if (foundIt != mOriginInfos.cend()) {
+ LockedAdjustUsageForRemovedOriginInfo(**foundIt);
+
+ mOriginInfos.RemoveElementAt(foundIt);
+ }
+}
+
+void GroupInfo::LockedRemoveOriginInfos() {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ for (const auto& originInfo : std::exchange(mOriginInfos, {})) {
+ LockedAdjustUsageForRemovedOriginInfo(*originInfo);
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/GroupInfo.h b/dom/quota/GroupInfo.h
new file mode 100644
index 0000000000..a88914743b
--- /dev/null
+++ b/dom/quota/GroupInfo.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_GROUPINFO_H_
+#define DOM_QUOTA_GROUPINFO_H_
+
+#include "OriginInfo.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom::quota {
+
+class GroupInfoPair;
+class OriginInfo;
+
+class GroupInfo final {
+ friend class CanonicalQuotaObject;
+ friend class GroupInfoPair;
+ friend class OriginInfo;
+ friend class QuotaManager;
+
+ public:
+ GroupInfo(GroupInfoPair* aGroupInfoPair, PersistenceType aPersistenceType)
+ : mGroupInfoPair(aGroupInfoPair),
+ mPersistenceType(aPersistenceType),
+ mUsage(0) {
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ MOZ_COUNT_CTOR(GroupInfo);
+ }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GroupInfo)
+
+ PersistenceType GetPersistenceType() const { return mPersistenceType; }
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ MOZ_COUNTED_DTOR(GroupInfo)
+
+ already_AddRefed<OriginInfo> LockedGetOriginInfo(const nsACString& aOrigin);
+
+ void LockedAddOriginInfo(NotNull<RefPtr<OriginInfo>>&& aOriginInfo);
+
+ void LockedAdjustUsageForRemovedOriginInfo(const OriginInfo& aOriginInfo);
+
+ void LockedRemoveOriginInfo(const nsACString& aOrigin);
+
+ void LockedRemoveOriginInfos();
+
+ bool LockedHasOriginInfos() {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ return !mOriginInfos.IsEmpty();
+ }
+
+ nsTArray<NotNull<RefPtr<OriginInfo>>> mOriginInfos;
+
+ GroupInfoPair* mGroupInfoPair;
+ PersistenceType mPersistenceType;
+ uint64_t mUsage;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_GROUPINFO_H_
diff --git a/dom/quota/GroupInfoPair.cpp b/dom/quota/GroupInfoPair.cpp
new file mode 100644
index 0000000000..bd912f004b
--- /dev/null
+++ b/dom/quota/GroupInfoPair.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GroupInfoPair.h"
+
+namespace mozilla::dom::quota {
+
+RefPtr<GroupInfo>& GroupInfoPair::GetGroupInfoForPersistenceType(
+ PersistenceType aPersistenceType) {
+ switch (aPersistenceType) {
+ case PERSISTENCE_TYPE_TEMPORARY:
+ return mTemporaryStorageGroupInfo;
+ case PERSISTENCE_TYPE_DEFAULT:
+ return mDefaultStorageGroupInfo;
+
+ case PERSISTENCE_TYPE_PERSISTENT:
+ case PERSISTENCE_TYPE_INVALID:
+ default:
+ MOZ_CRASH("Bad persistence type value!");
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/GroupInfoPair.h b/dom/quota/GroupInfoPair.h
new file mode 100644
index 0000000000..20b8ab8ee9
--- /dev/null
+++ b/dom/quota/GroupInfoPair.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_GROUPINFOPAIR_H_
+#define DOM_QUOTA_GROUPINFOPAIR_H_
+
+#include "GroupInfo.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+
+namespace mozilla::dom::quota {
+
+class GroupInfo;
+
+// XXX Consider a new name for this class, it has other data members now
+// (besides two GroupInfo objects).
+class GroupInfoPair {
+ public:
+ GroupInfoPair(const nsACString& aSuffix, const nsACString& aGroup)
+ : mSuffix(aSuffix), mGroup(aGroup) {
+ MOZ_COUNT_CTOR(GroupInfoPair);
+ }
+
+ MOZ_COUNTED_DTOR(GroupInfoPair)
+
+ const nsCString& Suffix() const { return mSuffix; }
+
+ const nsCString& Group() const { return mGroup; }
+
+ RefPtr<GroupInfo> LockedGetGroupInfo(PersistenceType aPersistenceType) {
+ AssertCurrentThreadOwnsQuotaMutex();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ return GetGroupInfoForPersistenceType(aPersistenceType);
+ }
+
+ void LockedSetGroupInfo(PersistenceType aPersistenceType,
+ GroupInfo* aGroupInfo) {
+ AssertCurrentThreadOwnsQuotaMutex();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ RefPtr<GroupInfo>& groupInfo =
+ GetGroupInfoForPersistenceType(aPersistenceType);
+ groupInfo = aGroupInfo;
+ }
+
+ void LockedClearGroupInfo(PersistenceType aPersistenceType) {
+ AssertCurrentThreadOwnsQuotaMutex();
+ MOZ_ASSERT(aPersistenceType != PERSISTENCE_TYPE_PERSISTENT);
+
+ RefPtr<GroupInfo>& groupInfo =
+ GetGroupInfoForPersistenceType(aPersistenceType);
+ groupInfo = nullptr;
+ }
+
+ bool LockedHasGroupInfos() {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ return mTemporaryStorageGroupInfo || mDefaultStorageGroupInfo;
+ }
+
+ private:
+ RefPtr<GroupInfo>& GetGroupInfoForPersistenceType(
+ PersistenceType aPersistenceType);
+
+ const nsCString mSuffix;
+ const nsCString mGroup;
+ RefPtr<GroupInfo> mTemporaryStorageGroupInfo;
+ RefPtr<GroupInfo> mDefaultStorageGroupInfo;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_GROUPINFOPAIR_H_
diff --git a/dom/quota/IPCQuotaObject.ipdlh b/dom/quota/IPCQuotaObject.ipdlh
new file mode 100644
index 0000000000..04c86ff585
--- /dev/null
+++ b/dom/quota/IPCQuotaObject.ipdlh
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PRemoteQuotaObject;
+
+using mozilla::ipc::Endpoint from "mozilla/ipc/Endpoint.h";
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+struct IPCQuotaObject
+{
+ Endpoint<PRemoteQuotaObjectChild> childEndpoint;
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/IPCStreamCipherStrategy.h b/dom/quota/IPCStreamCipherStrategy.h
new file mode 100644
index 0000000000..c66c644827
--- /dev/null
+++ b/dom/quota/IPCStreamCipherStrategy.h
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_IPCStreamCipherStrategy_h
+#define mozilla_dom_quota_IPCStreamCipherStrategy_h
+
+#include "mozilla/dom/quota/NSSCipherStrategy.h"
+
+namespace mozilla::dom::quota {
+using IPCStreamCipherStrategy = NSSCipherStrategy;
+}
+
+#endif
diff --git a/dom/quota/InitializationTypes.cpp b/dom/quota/InitializationTypes.cpp
new file mode 100644
index 0000000000..fffb4693aa
--- /dev/null
+++ b/dom/quota/InitializationTypes.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "InitializationTypes.h"
+
+namespace mozilla::dom::quota {
+
+// static
+nsLiteralCString StringGenerator::GetString(
+ const Initialization aInitialization) {
+ switch (aInitialization) {
+ case Initialization::Storage:
+ return "Storage"_ns;
+ case Initialization::TemporaryStorage:
+ return "TemporaryStorage"_ns;
+ case Initialization::DefaultRepository:
+ return "DefaultRepository"_ns;
+ case Initialization::TemporaryRepository:
+ return "TemporaryRepository"_ns;
+ case Initialization::UpgradeStorageFrom0_0To1_0:
+ return "UpgradeStorageFrom0_0To1_0"_ns;
+ case Initialization::UpgradeStorageFrom1_0To2_0:
+ return "UpgradeStorageFrom1_0To2_0"_ns;
+ case Initialization::UpgradeStorageFrom2_0To2_1:
+ return "UpgradeStorageFrom2_0To2_1"_ns;
+ case Initialization::UpgradeStorageFrom2_1To2_2:
+ return "UpgradeStorageFrom2_1To2_2"_ns;
+ case Initialization::UpgradeStorageFrom2_2To2_3:
+ return "UpgradeStorageFrom2_2To2_3"_ns;
+ case Initialization::UpgradeFromIndexedDBDirectory:
+ return "UpgradeFromIndexedDBDirectory"_ns;
+ case Initialization::UpgradeFromPersistentStorageDirectory:
+ return "UpgradeFromPersistentStorageDirectory"_ns;
+
+ default:
+ MOZ_CRASH("Bad initialization value!");
+ }
+}
+
+// static
+nsLiteralCString StringGenerator::GetString(
+ const OriginInitialization aOriginInitialization) {
+ switch (aOriginInitialization) {
+ case OriginInitialization::PersistentOrigin:
+ return "PersistentOrigin"_ns;
+ case OriginInitialization::TemporaryOrigin:
+ return "TemporaryOrigin"_ns;
+
+ default:
+ MOZ_CRASH("Bad origin initialization value!");
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/InitializationTypes.h b/dom/quota/InitializationTypes.h
new file mode 100644
index 0000000000..06d46abbd1
--- /dev/null
+++ b/dom/quota/InitializationTypes.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_INITIALIZATIONTYPES_H_
+#define DOM_QUOTA_INITIALIZATIONTYPES_H_
+
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/dom/quota/FirstInitializationAttempts.h"
+#include "nsLiteralString.h"
+#include "nsStringFwd.h"
+#include "nsTHashMap.h"
+
+namespace mozilla {
+struct CreateIfNonExistent;
+}
+
+namespace mozilla::dom::quota {
+
+enum class Initialization {
+ None = 0,
+ Storage = 1 << 0,
+ TemporaryStorage = 1 << 1,
+ DefaultRepository = 1 << 2,
+ TemporaryRepository = 1 << 3,
+ UpgradeStorageFrom0_0To1_0 = 1 << 4,
+ UpgradeStorageFrom1_0To2_0 = 1 << 5,
+ UpgradeStorageFrom2_0To2_1 = 1 << 6,
+ UpgradeStorageFrom2_1To2_2 = 1 << 7,
+ UpgradeStorageFrom2_2To2_3 = 1 << 8,
+ UpgradeFromIndexedDBDirectory = 1 << 9,
+ UpgradeFromPersistentStorageDirectory = 1 << 10,
+};
+
+enum class OriginInitialization {
+ None = 0,
+ PersistentOrigin = 1 << 0,
+ TemporaryOrigin = 1 << 1,
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(Initialization)
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(OriginInitialization)
+
+class StringGenerator final {
+ public:
+ // TODO: Use constexpr here once bug 1594094 is addressed.
+ static nsLiteralCString GetString(Initialization aInitialization);
+
+ // TODO: Use constexpr here once bug 1594094 is addressed.
+ static nsLiteralCString GetString(OriginInitialization aOriginInitialization);
+};
+
+using OriginInitializationInfo =
+ FirstInitializationAttempts<OriginInitialization, StringGenerator>;
+
+class InitializationInfo
+ : public FirstInitializationAttempts<Initialization, StringGenerator> {
+ nsTHashMap<nsCStringHashKey, OriginInitializationInfo>
+ mOriginInitializationInfos;
+
+ public:
+ OriginInitializationInfo& MutableOriginInitializationInfoRef(
+ const nsACString& aOrigin) {
+ return *mOriginInitializationInfos.Lookup(aOrigin);
+ }
+
+ OriginInitializationInfo& MutableOriginInitializationInfoRef(
+ const nsACString& aOrigin, const CreateIfNonExistent&) {
+ return mOriginInitializationInfos.LookupOrInsert(aOrigin);
+ }
+
+ void ResetOriginInitializationInfos() { mOriginInitializationInfos.Clear(); }
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_INITIALIZATIONTYPES_H_
diff --git a/dom/quota/NSSCipherStrategy.cpp b/dom/quota/NSSCipherStrategy.cpp
new file mode 100644
index 0000000000..d3278b54b7
--- /dev/null
+++ b/dom/quota/NSSCipherStrategy.cpp
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "NSSCipherStrategy.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <cstring>
+#include <memory>
+#include <type_traits>
+#include <utility>
+#include "mozilla/Assertions.h"
+#include "mozilla/ResultExtensions.h"
+
+// NSS includes
+#include "blapit.h"
+#include "pk11pub.h"
+#include "pkcs11t.h"
+#include "seccomon.h"
+#include "secmodt.h"
+
+namespace mozilla::dom::quota {
+
+static_assert(sizeof(NSSCipherStrategy::KeyType) == 32);
+static_assert(NSSCipherStrategy::BlockPrefixLength == 32);
+static_assert(NSSCipherStrategy::BasicBlockSize == 16);
+
+Result<NSSCipherStrategy::KeyType, nsresult> NSSCipherStrategy::GenerateKey() {
+ const auto slot = UniquePK11SlotInfo{PK11_GetInternalSlot()};
+ if (slot == nullptr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ const auto symKey = UniquePK11SymKey{PK11_KeyGen(
+ slot.get(), CKM_CHACHA20_KEY_GEN, nullptr, sizeof(KeyType), nullptr)};
+ if (symKey == nullptr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (PK11_ExtractKeyValue(symKey.get()) != SECSuccess) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ // No need to free keyData as it is a buffer managed by symKey.
+ SECItem* keyData = PK11_GetKeyData(symKey.get());
+ if (keyData == nullptr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ KeyType key;
+ MOZ_RELEASE_ASSERT(keyData->len == key.size());
+ std::copy(keyData->data, keyData->data + key.size(), key.data());
+ return key;
+}
+
+nsresult NSSCipherStrategy::Init(const CipherMode aMode,
+ const Span<const uint8_t> aKey,
+ const Span<const uint8_t> aInitialIv) {
+ MOZ_ASSERT_IF(CipherMode::Encrypt == aMode, aInitialIv.Length() == 32);
+
+ mMode.init(aMode);
+
+ mIv.AppendElements(aInitialIv);
+
+ const auto slot = UniquePK11SlotInfo{PK11_GetInternalSlot()};
+ if (slot == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SECItem keyItem;
+ keyItem.data = const_cast<uint8_t*>(aKey.Elements());
+ keyItem.len = aKey.Length();
+ const auto symKey = UniquePK11SymKey{
+ PK11_ImportSymKey(slot.get(), CKM_CHACHA20_POLY1305, PK11_OriginUnwrap,
+ CKA_ENCRYPT, &keyItem, nullptr)};
+ if (symKey == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SECItem empty = {siBuffer, nullptr, 0};
+ auto pk11Context = UniquePK11Context{PK11_CreateContextBySymKey(
+ CKM_CHACHA20_POLY1305,
+ CKA_NSS_MESSAGE |
+ (CipherMode::Encrypt == aMode ? CKA_ENCRYPT : CKA_DECRYPT),
+ symKey.get(), &empty)};
+ if (pk11Context == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mPK11Context.init(std::move(pk11Context));
+ return NS_OK;
+}
+
+nsresult NSSCipherStrategy::Cipher(const Span<uint8_t> aIv,
+ const Span<const uint8_t> aIn,
+ const Span<uint8_t> aOut) {
+ if (CipherMode::Encrypt == *mMode) {
+ MOZ_RELEASE_ASSERT(aIv.Length() == mIv.Length());
+ memcpy(aIv.Elements(), mIv.Elements(), aIv.Length());
+ }
+
+ // XXX make tag a separate parameter
+ constexpr size_t tagLen = 16;
+ const auto tag = Span{aIv}.Last(tagLen);
+ // tag is const on decrypt, but returned on encrypt
+
+ const auto iv = Span{aIv}.First(12);
+ MOZ_ASSERT(tag.Length() + iv.Length() <= aIv.Length());
+
+ int outLen;
+ // aIn and aOut may not overlap resp. be the same, so we can't do this
+ // in-place.
+ const SECStatus rv = PK11_AEADOp(
+ mPK11Context->get(), CKG_GENERATE_COUNTER, 0, iv.Elements(), iv.Length(),
+ nullptr, 0, aOut.Elements(), &outLen, aOut.Length(), tag.Elements(),
+ tag.Length(), aIn.Elements(), aIn.Length());
+
+ if (CipherMode::Encrypt == *mMode) {
+ memcpy(mIv.Elements(), aIv.Elements(), aIv.Length());
+ }
+
+ return MapSECStatus(rv);
+}
+
+template <size_t N>
+static std::array<uint8_t, N> MakeRandomData() {
+ std::array<uint8_t, N> res;
+
+ const auto rv = PK11_GenerateRandom(res.data(), res.size());
+ /// XXX Allow return of error code to handle this gracefully.
+ MOZ_RELEASE_ASSERT(rv == SECSuccess);
+
+ return res;
+}
+
+std::array<uint8_t, NSSCipherStrategy::BlockPrefixLength>
+NSSCipherStrategy::MakeBlockPrefix() {
+ return MakeRandomData<BlockPrefixLength>();
+}
+
+Span<const uint8_t> NSSCipherStrategy::SerializeKey(const KeyType& aKey) {
+ return Span(aKey);
+}
+
+NSSCipherStrategy::KeyType NSSCipherStrategy::DeserializeKey(
+ const Span<const uint8_t>& aSerializedKey) {
+ KeyType res;
+ MOZ_ASSERT(res.size() == aSerializedKey.size());
+ std::copy(aSerializedKey.cbegin(), aSerializedKey.cend(), res.begin());
+ return res;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/NSSCipherStrategy.h b/dom/quota/NSSCipherStrategy.h
new file mode 100644
index 0000000000..31b3eb03e2
--- /dev/null
+++ b/dom/quota/NSSCipherStrategy.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_NSSCipherStrategy_h
+#define mozilla_dom_quota_NSSCipherStrategy_h
+
+#include "CipherStrategy.h"
+
+#include <cstddef>
+#include <cstdint>
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/Result.h"
+#include "mozilla/Span.h"
+#include "ErrorList.h"
+#include "nsTArray.h"
+
+#include "ScopedNSSTypes.h"
+
+#include <array>
+
+namespace mozilla::dom::quota {
+
+struct NSSCipherStrategy {
+ // Use numeric literals here to avoid having to include NSS headers here.
+ // static_assert's in the cpp file check their consistency.
+ using KeyType = std::array<uint8_t, 32>;
+ static constexpr size_t BlockPrefixLength = 32;
+ static constexpr size_t BasicBlockSize = 16;
+
+ static Result<KeyType, nsresult> GenerateKey();
+
+ nsresult Init(CipherMode aCipherMode, Span<const uint8_t> aKey,
+ Span<const uint8_t> aInitialIv = Span<const uint8_t>{});
+
+ nsresult Cipher(Span<uint8_t> aIv, Span<const uint8_t> aIn,
+ Span<uint8_t> aOut);
+
+ static std::array<uint8_t, BlockPrefixLength> MakeBlockPrefix();
+
+ static Span<const uint8_t> SerializeKey(const KeyType& aKey);
+
+ static KeyType DeserializeKey(const Span<const uint8_t>& aSerializedKey);
+
+ private:
+ // XXX Remove EarlyDestructible, remove moving of the CipherStrategy.
+ LazyInitializedOnceEarlyDestructible<const CipherMode> mMode;
+ LazyInitializedOnceEarlyDestructible<const UniquePK11Context> mPK11Context;
+ nsTArray<uint8_t> mIv;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif
diff --git a/dom/quota/OriginInfo.cpp b/dom/quota/OriginInfo.cpp
new file mode 100644
index 0000000000..4985bb627c
--- /dev/null
+++ b/dom/quota/OriginInfo.cpp
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OriginInfo.h"
+
+#include "GroupInfo.h"
+#include "GroupInfoPair.h"
+#include "mozilla/dom/quota/AssertionsImpl.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/UsageInfo.h"
+
+namespace mozilla::dom::quota {
+
+OriginInfo::OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin,
+ const ClientUsageArray& aClientUsages, uint64_t aUsage,
+ int64_t aAccessTime, bool aPersisted,
+ bool aDirectoryExists)
+ : mClientUsages(aClientUsages.Clone()),
+ mGroupInfo(aGroupInfo),
+ mOrigin(aOrigin),
+ mUsage(aUsage),
+ mAccessTime(aAccessTime),
+ mAccessed(false),
+ mPersisted(aPersisted),
+ mDirectoryExists(aDirectoryExists) {
+ MOZ_ASSERT(aGroupInfo);
+ MOZ_ASSERT(aClientUsages.Length() == Client::TypeMax());
+ MOZ_ASSERT_IF(aPersisted,
+ aGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ // This constructor is called from the "QuotaManager IO" thread and so
+ // we can't check if the principal has a WebExtensionPolicy instance
+ // associated to it, and even besides that if the extension is currently
+ // disabled (and so no WebExtensionPolicy instance would actually exist)
+ // its stored data shouldn't be cleared until the extension is uninstalled
+ // and so here we resort to check the origin scheme instead.
+ mIsExtension = StringBeginsWith(mOrigin, "moz-extension://"_ns);
+
+#ifdef DEBUG
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ uint64_t usage = 0;
+ for (Client::Type type : quotaManager->AllClientTypes()) {
+ AssertNoOverflow(usage, aClientUsages[type].valueOr(0));
+ usage += aClientUsages[type].valueOr(0);
+ }
+ MOZ_ASSERT(aUsage == usage);
+#endif
+
+ MOZ_COUNT_CTOR(OriginInfo);
+}
+
+OriginMetadata OriginInfo::FlattenToOriginMetadata() const {
+ return {mGroupInfo->mGroupInfoPair->Suffix(),
+ mGroupInfo->mGroupInfoPair->Group(), mOrigin,
+ mGroupInfo->mPersistenceType};
+}
+
+FullOriginMetadata OriginInfo::LockedFlattenToFullOriginMetadata() const {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ return {FlattenToOriginMetadata(), mPersisted, mAccessTime};
+}
+
+nsresult OriginInfo::LockedBindToStatement(
+ mozIStorageStatement* aStatement) const {
+ AssertCurrentThreadOwnsQuotaMutex();
+ MOZ_ASSERT(mGroupInfo);
+
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName(
+ "repository_id"_ns, mGroupInfo->mPersistenceType)));
+
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName(
+ "suffix"_ns, mGroupInfo->mGroupInfoPair->Suffix())));
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName(
+ "group_"_ns, mGroupInfo->mGroupInfoPair->Group())));
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindUTF8StringByName("origin"_ns, mOrigin)));
+
+ nsCString clientUsagesText;
+ mClientUsages.Serialize(clientUsagesText);
+
+ QM_TRY(MOZ_TO_RESULT(
+ aStatement->BindUTF8StringByName("client_usages"_ns, clientUsagesText)));
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindInt64ByName("usage"_ns, mUsage)));
+ QM_TRY(MOZ_TO_RESULT(
+ aStatement->BindInt64ByName("last_access_time"_ns, mAccessTime)));
+ QM_TRY(MOZ_TO_RESULT(aStatement->BindInt32ByName("accessed"_ns, mAccessed)));
+ QM_TRY(
+ MOZ_TO_RESULT(aStatement->BindInt32ByName("persisted"_ns, mPersisted)));
+
+ return NS_OK;
+}
+
+void OriginInfo::LockedDecreaseUsage(Client::Type aClientType, int64_t aSize) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ MOZ_ASSERT(mClientUsages[aClientType].isSome());
+ AssertNoUnderflow(mClientUsages[aClientType].value(), aSize);
+ mClientUsages[aClientType] = Some(mClientUsages[aClientType].value() - aSize);
+
+ AssertNoUnderflow(mUsage, aSize);
+ mUsage -= aSize;
+
+ if (!LockedPersisted()) {
+ AssertNoUnderflow(mGroupInfo->mUsage, aSize);
+ mGroupInfo->mUsage -= aSize;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, aSize);
+ quotaManager->mTemporaryStorageUsage -= aSize;
+}
+
+void OriginInfo::LockedResetUsageForClient(Client::Type aClientType) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ uint64_t size = mClientUsages[aClientType].valueOr(0);
+
+ mClientUsages[aClientType].reset();
+
+ AssertNoUnderflow(mUsage, size);
+ mUsage -= size;
+
+ if (!LockedPersisted()) {
+ AssertNoUnderflow(mGroupInfo->mUsage, size);
+ mGroupInfo->mUsage -= size;
+ }
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ AssertNoUnderflow(quotaManager->mTemporaryStorageUsage, size);
+ quotaManager->mTemporaryStorageUsage -= size;
+}
+
+UsageInfo OriginInfo::LockedGetUsageForClient(Client::Type aClientType) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ // The current implementation of this method only supports DOMCACHE and LS,
+ // which only use DatabaseUsage. If this assertion is lifted, the logic below
+ // must be adapted.
+ MOZ_ASSERT(aClientType == Client::Type::DOMCACHE ||
+ aClientType == Client::Type::LS ||
+ aClientType == Client::Type::FILESYSTEM);
+
+ return UsageInfo{DatabaseUsageType{mClientUsages[aClientType]}};
+}
+
+void OriginInfo::LockedPersist() {
+ AssertCurrentThreadOwnsQuotaMutex();
+ MOZ_ASSERT(mGroupInfo->mPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+ MOZ_ASSERT(!mPersisted);
+
+ mPersisted = true;
+
+ // Remove Usage from GroupInfo
+ AssertNoUnderflow(mGroupInfo->mUsage, mUsage);
+ mGroupInfo->mUsage -= mUsage;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/OriginInfo.h b/dom/quota/OriginInfo.h
new file mode 100644
index 0000000000..33a3a30355
--- /dev/null
+++ b/dom/quota/OriginInfo.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_ORIGININFO_H_
+#define DOM_QUOTA_ORIGININFO_H_
+
+#include "Assertions.h"
+#include "ClientUsageArray.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+
+namespace mozilla::dom::quota {
+
+class CanonicalQuotaObject;
+class GroupInfo;
+
+class OriginInfo final {
+ friend class CanonicalQuotaObject;
+ friend class GroupInfo;
+ friend class QuotaManager;
+
+ public:
+ OriginInfo(GroupInfo* aGroupInfo, const nsACString& aOrigin,
+ const ClientUsageArray& aClientUsages, uint64_t aUsage,
+ int64_t aAccessTime, bool aPersisted, bool aDirectoryExists);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OriginInfo)
+
+ GroupInfo* GetGroupInfo() const { return mGroupInfo; }
+
+ const nsCString& Origin() const { return mOrigin; }
+
+ int64_t LockedUsage() const {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+#ifdef DEBUG
+ QuotaManager* quotaManager = QuotaManager::Get();
+ MOZ_ASSERT(quotaManager);
+
+ uint64_t usage = 0;
+ for (Client::Type type : quotaManager->AllClientTypes()) {
+ AssertNoOverflow(usage, mClientUsages[type].valueOr(0));
+ usage += mClientUsages[type].valueOr(0);
+ }
+ MOZ_ASSERT(mUsage == usage);
+#endif
+
+ return mUsage;
+ }
+
+ int64_t LockedAccessTime() const {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ return mAccessTime;
+ }
+
+ bool LockedPersisted() const {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ return mPersisted;
+ }
+
+ OriginMetadata FlattenToOriginMetadata() const;
+
+ FullOriginMetadata LockedFlattenToFullOriginMetadata() const;
+
+ nsresult LockedBindToStatement(mozIStorageStatement* aStatement) const;
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~OriginInfo() {
+ MOZ_COUNT_DTOR(OriginInfo);
+
+ MOZ_ASSERT(!mCanonicalQuotaObjects.Count());
+ }
+
+ void LockedDecreaseUsage(Client::Type aClientType, int64_t aSize);
+
+ void LockedResetUsageForClient(Client::Type aClientType);
+
+ UsageInfo LockedGetUsageForClient(Client::Type aClientType);
+
+ void LockedUpdateAccessTime(int64_t aAccessTime) {
+ AssertCurrentThreadOwnsQuotaMutex();
+
+ mAccessTime = aAccessTime;
+ if (!mAccessed) {
+ mAccessed = true;
+ }
+ }
+
+ void LockedPersist();
+
+ bool IsExtensionOrigin() { return mIsExtension; }
+
+ nsTHashMap<nsStringHashKey, NotNull<CanonicalQuotaObject*>>
+ mCanonicalQuotaObjects;
+ ClientUsageArray mClientUsages;
+ GroupInfo* mGroupInfo;
+ const nsCString mOrigin;
+ bool mIsExtension;
+ uint64_t mUsage;
+ int64_t mAccessTime;
+ bool mAccessed;
+ bool mPersisted;
+ /**
+ * In some special cases like the LocalStorage client where it's possible to
+ * create a Quota-using representation but not actually write any data, we
+ * want to be able to track quota for an origin without creating its origin
+ * directory or the per-client files until they are actually needed to store
+ * data. In those cases, the OriginInfo will be created by
+ * EnsureQuotaForOrigin and the resulting mDirectoryExists will be false until
+ * the origin actually needs to be created. It is possible for mUsage to be
+ * greater than zero while mDirectoryExists is false, representing a state
+ * where a client like LocalStorage has reserved quota for disk writes, but
+ * has not yet flushed the data to disk.
+ */
+ bool mDirectoryExists;
+};
+
+class OriginInfoAccessTimeComparator {
+ public:
+ bool Equals(const NotNull<RefPtr<const OriginInfo>>& a,
+ const NotNull<RefPtr<const OriginInfo>>& b) const {
+ return a->LockedAccessTime() == b->LockedAccessTime();
+ }
+
+ bool LessThan(const NotNull<RefPtr<const OriginInfo>>& a,
+ const NotNull<RefPtr<const OriginInfo>>& b) const {
+ return a->LockedAccessTime() < b->LockedAccessTime();
+ }
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_ORIGININFO_H_
diff --git a/dom/quota/OriginScope.h b/dom/quota/OriginScope.h
new file mode 100644
index 0000000000..a7047e5c82
--- /dev/null
+++ b/dom/quota/OriginScope.h
@@ -0,0 +1,344 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_originorpatternstring_h__
+#define mozilla_dom_quota_originorpatternstring_h__
+
+#include <utility>
+#include "mozilla/Assertions.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Variant.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::dom::quota {
+
+class OriginScope {
+ class Origin {
+ nsCString mOrigin;
+ nsCString mOriginNoSuffix;
+ UniquePtr<OriginAttributes> mAttributes;
+
+ public:
+ explicit Origin(const nsACString& aOrigin) : mOrigin(aOrigin) {
+ InitMembers();
+ }
+
+ Origin(const Origin& aOther)
+ : mOrigin(aOther.mOrigin),
+ mOriginNoSuffix(aOther.mOriginNoSuffix),
+ mAttributes(MakeUnique<OriginAttributes>(*aOther.mAttributes)) {}
+
+ Origin(Origin&& aOther) = default;
+
+ const nsACString& GetOrigin() const { return mOrigin; }
+
+ void SetOrigin(const nsACString& aOrigin) {
+ mOrigin = aOrigin;
+
+ InitMembers();
+ }
+
+ const nsACString& GetOriginNoSuffix() const { return mOriginNoSuffix; }
+
+ const OriginAttributes& GetAttributes() const {
+ MOZ_ASSERT(mAttributes);
+
+ return *mAttributes;
+ }
+
+ private:
+ void InitMembers() {
+ mAttributes = MakeUnique<OriginAttributes>();
+
+ MOZ_ALWAYS_TRUE(
+ mAttributes->PopulateFromOrigin(mOrigin, mOriginNoSuffix));
+ }
+ };
+
+ class Prefix {
+ nsCString mOriginNoSuffix;
+
+ public:
+ explicit Prefix(const nsACString& aOriginNoSuffix)
+ : mOriginNoSuffix(aOriginNoSuffix) {}
+
+ const nsCString& GetOriginNoSuffix() const { return mOriginNoSuffix; }
+
+ void SetOriginNoSuffix(const nsACString& aOriginNoSuffix) {
+ mOriginNoSuffix = aOriginNoSuffix;
+ }
+ };
+
+ class Pattern {
+ UniquePtr<OriginAttributesPattern> mPattern;
+
+ public:
+ explicit Pattern(const OriginAttributesPattern& aPattern)
+ : mPattern(MakeUnique<OriginAttributesPattern>(aPattern)) {}
+
+ explicit Pattern(const nsAString& aJSONPattern)
+ : mPattern(MakeUnique<OriginAttributesPattern>()) {
+ MOZ_ALWAYS_TRUE(mPattern->Init(aJSONPattern));
+ }
+
+ Pattern(const Pattern& aOther)
+ : mPattern(MakeUnique<OriginAttributesPattern>(*aOther.mPattern)) {}
+
+ Pattern(Pattern&& aOther) = default;
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(mPattern);
+
+ return *mPattern;
+ }
+
+ void SetPattern(const OriginAttributesPattern& aPattern) {
+ mPattern = MakeUnique<OriginAttributesPattern>(aPattern);
+ }
+
+ nsString GetJSONPattern() const {
+ MOZ_ASSERT(mPattern);
+
+ nsString result;
+ MOZ_ALWAYS_TRUE(mPattern->ToJSON(result));
+
+ return result;
+ }
+ };
+
+ struct Null {};
+
+ using DataType = Variant<Origin, Prefix, Pattern, Null>;
+
+ DataType mData;
+
+ public:
+ OriginScope() : mData(Null()) {}
+
+ static OriginScope FromOrigin(const nsACString& aOrigin) {
+ return OriginScope(std::move(Origin(aOrigin)));
+ }
+
+ static OriginScope FromPrefix(const nsACString& aPrefix) {
+ return OriginScope(std::move(Prefix(aPrefix)));
+ }
+
+ static OriginScope FromPattern(const OriginAttributesPattern& aPattern) {
+ return OriginScope(std::move(Pattern(aPattern)));
+ }
+
+ static OriginScope FromJSONPattern(const nsAString& aJSONPattern) {
+ return OriginScope(std::move(Pattern(aJSONPattern)));
+ }
+
+ static OriginScope FromNull() { return OriginScope(std::move(Null())); }
+
+ bool IsOrigin() const { return mData.is<Origin>(); }
+
+ bool IsPrefix() const { return mData.is<Prefix>(); }
+
+ bool IsPattern() const { return mData.is<Pattern>(); }
+
+ bool IsNull() const { return mData.is<Null>(); }
+
+ void SetFromOrigin(const nsACString& aOrigin) {
+ mData = AsVariant(Origin(aOrigin));
+ }
+
+ void SetFromPrefix(const nsACString& aPrefix) {
+ mData = AsVariant(Prefix(aPrefix));
+ }
+
+ void SetFromPattern(const OriginAttributesPattern& aPattern) {
+ mData = AsVariant(Pattern(aPattern));
+ }
+
+ void SetFromJSONPattern(const nsAString& aJSONPattern) {
+ mData = AsVariant(Pattern(aJSONPattern));
+ }
+
+ void SetFromNull() { mData = AsVariant(Null()); }
+
+ const nsACString& GetOrigin() const {
+ MOZ_ASSERT(IsOrigin());
+
+ return mData.as<Origin>().GetOrigin();
+ }
+
+ void SetOrigin(const nsACString& aOrigin) {
+ MOZ_ASSERT(IsOrigin());
+
+ mData.as<Origin>().SetOrigin(aOrigin);
+ }
+
+ const nsACString& GetOriginNoSuffix() const {
+ MOZ_ASSERT(IsOrigin() || IsPrefix());
+
+ if (IsOrigin()) {
+ return mData.as<Origin>().GetOriginNoSuffix();
+ }
+ return mData.as<Prefix>().GetOriginNoSuffix();
+ }
+
+ void SetOriginNoSuffix(const nsACString& aOriginNoSuffix) {
+ MOZ_ASSERT(IsPrefix());
+
+ mData.as<Prefix>().SetOriginNoSuffix(aOriginNoSuffix);
+ }
+
+ const OriginAttributesPattern& GetPattern() const {
+ MOZ_ASSERT(IsPattern());
+
+ return mData.as<Pattern>().GetPattern();
+ }
+
+ nsString GetJSONPattern() const {
+ MOZ_ASSERT(IsPattern());
+
+ return mData.as<Pattern>().GetJSONPattern();
+ }
+
+ void SetPattern(const OriginAttributesPattern& aPattern) {
+ MOZ_ASSERT(IsPattern());
+
+ mData.as<Pattern>().SetPattern(aPattern);
+ }
+
+ bool Matches(const OriginScope& aOther) const {
+ struct Matcher {
+ const OriginScope& mThis;
+
+ explicit Matcher(const OriginScope& aThis) : mThis(aThis) {}
+
+ bool operator()(const Origin& aOther) {
+ return mThis.MatchesOrigin(aOther);
+ }
+
+ bool operator()(const Prefix& aOther) {
+ return mThis.MatchesPrefix(aOther);
+ }
+
+ bool operator()(const Pattern& aOther) {
+ return mThis.MatchesPattern(aOther);
+ }
+
+ bool operator()(const Null& aOther) { return true; }
+ };
+
+ return aOther.mData.match(Matcher(*this));
+ }
+
+ OriginScope Clone() { return OriginScope(mData); }
+
+ private:
+ // Move constructors
+ explicit OriginScope(const Origin&& aOrigin) : mData(aOrigin) {}
+
+ explicit OriginScope(const Prefix&& aPrefix) : mData(aPrefix) {}
+
+ explicit OriginScope(const Pattern&& aPattern) : mData(aPattern) {}
+
+ explicit OriginScope(const Null&& aNull) : mData(aNull) {}
+
+ // Copy constructor
+ explicit OriginScope(const DataType& aOther) : mData(aOther) {}
+
+ bool MatchesOrigin(const Origin& aOther) const {
+ struct OriginMatcher {
+ const Origin& mOther;
+
+ explicit OriginMatcher(const Origin& aOther) : mOther(aOther) {}
+
+ bool operator()(const Origin& aThis) {
+ return aThis.GetOrigin().Equals(mOther.GetOrigin());
+ }
+
+ bool operator()(const Prefix& aThis) {
+ return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
+ }
+
+ bool operator()(const Pattern& aThis) {
+ return aThis.GetPattern().Matches(mOther.GetAttributes());
+ }
+
+ bool operator()(const Null& aThis) {
+ // Null covers everything.
+ return true;
+ }
+ };
+
+ return mData.match(OriginMatcher(aOther));
+ }
+
+ bool MatchesPrefix(const Prefix& aOther) const {
+ struct PrefixMatcher {
+ const Prefix& mOther;
+
+ explicit PrefixMatcher(const Prefix& aOther) : mOther(aOther) {}
+
+ bool operator()(const Origin& aThis) {
+ return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
+ }
+
+ bool operator()(const Prefix& aThis) {
+ return aThis.GetOriginNoSuffix().Equals(mOther.GetOriginNoSuffix());
+ }
+
+ bool operator()(const Pattern& aThis) {
+ // The match will be always true here because any origin attributes
+ // pattern overlaps any origin prefix (an origin prefix targets all
+ // origin attributes).
+ return true;
+ }
+
+ bool operator()(const Null& aThis) {
+ // Null covers everything.
+ return true;
+ }
+ };
+
+ return mData.match(PrefixMatcher(aOther));
+ }
+
+ bool MatchesPattern(const Pattern& aOther) const {
+ struct PatternMatcher {
+ const Pattern& mOther;
+
+ explicit PatternMatcher(const Pattern& aOther) : mOther(aOther) {}
+
+ bool operator()(const Origin& aThis) {
+ return mOther.GetPattern().Matches(aThis.GetAttributes());
+ }
+
+ bool operator()(const Prefix& aThis) {
+ // The match will be always true here because any origin attributes
+ // pattern overlaps any origin prefix (an origin prefix targets all
+ // origin attributes).
+ return true;
+ }
+
+ bool operator()(const Pattern& aThis) {
+ return aThis.GetPattern().Overlaps(mOther.GetPattern());
+ }
+
+ bool operator()(const Null& aThis) {
+ // Null covers everything.
+ return true;
+ }
+ };
+
+ PatternMatcher patternMatcher(aOther);
+ return mData.match(PatternMatcher(aOther));
+ }
+
+ bool operator==(const OriginScope& aOther) = delete;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_originorpatternstring_h__
diff --git a/dom/quota/PQuota.ipdl b/dom/quota/PQuota.ipdl
new file mode 100644
index 0000000000..f34109b239
--- /dev/null
+++ b/dom/quota/PQuota.ipdl
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+include protocol PQuotaRequest;
+include protocol PQuotaUsageRequest;
+
+include PBackgroundSharedTypes;
+
+include "mozilla/dom/quota/SerializationHelpers.h";
+
+using mozilla::OriginAttributesPattern
+ from "mozilla/OriginAttributes.h";
+
+using mozilla::dom::quota::PersistenceType
+ from "mozilla/dom/quota/PersistenceType.h";
+
+using mozilla::dom::quota::Client::Type
+ from "mozilla/dom/quota/Client.h";
+
+using mozilla::dom::ContentParentId
+ from "mozilla/dom/ipc/IdType.h";
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+struct StorageNameParams
+{
+};
+
+struct StorageInitializedParams
+{
+};
+
+struct TemporaryStorageInitializedParams
+{
+};
+
+struct InitParams
+{
+};
+
+struct InitTemporaryStorageParams
+{
+};
+
+struct InitializePersistentOriginParams
+{
+ PrincipalInfo principalInfo;
+};
+
+struct InitializeTemporaryOriginParams
+{
+ PersistenceType persistenceType;
+ PrincipalInfo principalInfo;
+};
+
+struct GetFullOriginMetadataParams
+{
+ PersistenceType persistenceType;
+ PrincipalInfo principalInfo;
+};
+
+struct AllUsageParams
+{
+ bool getAll;
+};
+
+struct OriginUsageParams
+{
+ PrincipalInfo principalInfo;
+ bool fromMemory;
+};
+
+union UsageRequestParams
+{
+ AllUsageParams;
+ OriginUsageParams;
+};
+
+struct ClearResetOriginParams
+{
+ PrincipalInfo principalInfo;
+ PersistenceType persistenceType;
+ bool persistenceTypeIsExplicit;
+ Type clientType;
+ bool clientTypeIsExplicit;
+};
+
+struct ClearOriginParams
+{
+ ClearResetOriginParams commonParams;
+ bool matchAll;
+};
+
+struct ResetOriginParams
+{
+ ClearResetOriginParams commonParams;
+};
+
+struct ClearDataParams
+{
+ OriginAttributesPattern pattern;
+};
+
+struct ClearAllParams
+{
+};
+
+struct ResetAllParams
+{
+};
+
+struct PersistedParams
+{
+ PrincipalInfo principalInfo;
+};
+
+struct PersistParams
+{
+ PrincipalInfo principalInfo;
+};
+
+struct EstimateParams
+{
+ PrincipalInfo principalInfo;
+};
+
+struct ListOriginsParams
+{
+};
+
+union RequestParams
+{
+ StorageNameParams;
+ StorageInitializedParams;
+ TemporaryStorageInitializedParams;
+ InitParams;
+ InitTemporaryStorageParams;
+ InitializePersistentOriginParams;
+ InitializeTemporaryOriginParams;
+ GetFullOriginMetadataParams;
+ ClearOriginParams;
+ ResetOriginParams;
+ ClearDataParams;
+ ClearAllParams;
+ ResetAllParams;
+ PersistedParams;
+ PersistParams;
+ EstimateParams;
+ ListOriginsParams;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PQuota
+{
+ manager PBackground;
+
+ manages PQuotaRequest;
+ manages PQuotaUsageRequest;
+
+parent:
+ async __delete__();
+
+ async PQuotaUsageRequest(UsageRequestParams params);
+
+ async PQuotaRequest(RequestParams params);
+
+ async StartIdleMaintenance();
+
+ async StopIdleMaintenance();
+
+ async AbortOperationsForProcess(ContentParentId contentParentId);
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/PQuotaRequest.ipdl b/dom/quota/PQuotaRequest.ipdl
new file mode 100644
index 0000000000..b155bda557
--- /dev/null
+++ b/dom/quota/PQuotaRequest.ipdl
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PQuota;
+
+include "mozilla/dom/quota/SerializationHelpers.h";
+
+using mozilla::dom::quota::FullOriginMetadata
+ from "mozilla/dom/quota/CommonMetadata.h";
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+struct StorageNameResponse
+{
+ nsString name;
+};
+
+struct StorageInitializedResponse
+{
+ bool initialized;
+};
+
+struct TemporaryStorageInitializedResponse
+{
+ bool initialized;
+};
+
+struct InitResponse
+{
+};
+
+struct InitTemporaryStorageResponse
+{
+};
+
+struct InitializePersistentOriginResponse
+{
+ bool created;
+};
+
+struct InitializeTemporaryOriginResponse
+{
+ bool created;
+};
+
+struct GetFullOriginMetadataResponse
+{
+ FullOriginMetadata? maybeFullOriginMetadata;
+};
+
+struct ClearOriginResponse
+{
+};
+
+struct ResetOriginResponse
+{
+};
+
+struct ClearDataResponse
+{
+};
+
+struct ClearAllResponse
+{
+};
+
+struct ResetAllResponse
+{
+};
+
+struct PersistedResponse
+{
+ bool persisted;
+};
+
+struct PersistResponse
+{
+};
+
+struct EstimateResponse
+{
+ uint64_t usage;
+ uint64_t limit;
+};
+
+struct ListOriginsResponse
+{
+ nsCString[] origins;
+};
+
+union RequestResponse
+{
+ nsresult;
+ StorageNameResponse;
+ StorageInitializedResponse;
+ TemporaryStorageInitializedResponse;
+ InitResponse;
+ InitTemporaryStorageResponse;
+ InitializePersistentOriginResponse;
+ InitializeTemporaryOriginResponse;
+ GetFullOriginMetadataResponse;
+ ClearOriginResponse;
+ ResetOriginResponse;
+ ClearDataResponse;
+ ClearAllResponse;
+ ResetAllResponse;
+ PersistedResponse;
+ PersistResponse;
+ EstimateResponse;
+ ListOriginsResponse;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PQuotaRequest
+{
+ manager PQuota;
+
+child:
+ async __delete__(RequestResponse response);
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/PQuotaUsageRequest.ipdl b/dom/quota/PQuotaUsageRequest.ipdl
new file mode 100644
index 0000000000..26401c84e6
--- /dev/null
+++ b/dom/quota/PQuotaUsageRequest.ipdl
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PQuota;
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+struct OriginUsage
+{
+ nsCString origin;
+ bool persisted;
+ uint64_t usage;
+ uint64_t lastAccessed;
+};
+
+struct AllUsageResponse
+{
+ OriginUsage[] originUsages;
+};
+
+struct OriginUsageResponse
+{
+ uint64_t usage;
+ uint64_t fileUsage;
+};
+
+union UsageRequestResponse
+{
+ nsresult;
+ AllUsageResponse;
+ OriginUsageResponse;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+protocol PQuotaUsageRequest
+{
+ manager PQuota;
+
+parent:
+ async Cancel();
+
+child:
+ async __delete__(UsageRequestResponse response);
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/PRemoteQuotaObject.ipdl b/dom/quota/PRemoteQuotaObject.ipdl
new file mode 100644
index 0000000000..f0221d7b9c
--- /dev/null
+++ b/dom/quota/PRemoteQuotaObject.ipdl
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+sync protocol PRemoteQuotaObject
+{
+ parent:
+ // This must be synchronous because we don't have quota file stream wrappers
+ // which would be fully asynchronous (non-blocking). Given that, this message
+ // should never be sent on the main thread or the PBackground thread or a DOM
+ // worker thread.
+ sync MaybeUpdateSize(int64_t size, bool truncate)
+ returns(bool result);
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/PersistenceType.cpp b/dom/quota/PersistenceType.cpp
new file mode 100644
index 0000000000..637175c886
--- /dev/null
+++ b/dom/quota/PersistenceType.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PersistenceType.h"
+
+#include <utility>
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+
+namespace mozilla::dom::quota {
+
+namespace {
+
+constexpr auto kPersistentCString = "persistent"_ns;
+constexpr auto kTemporaryCString = "temporary"_ns;
+constexpr auto kDefaultCString = "default"_ns;
+
+constexpr auto kPermanentString = u"permanent"_ns;
+constexpr auto kTemporaryString = u"temporary"_ns;
+constexpr auto kDefaultString = u"default"_ns;
+
+static_assert(PERSISTENCE_TYPE_PERSISTENT == 0 &&
+ PERSISTENCE_TYPE_TEMPORARY == 1 &&
+ PERSISTENCE_TYPE_DEFAULT == 2 &&
+ PERSISTENCE_TYPE_INVALID == 3,
+ "Incorrect enum values!");
+
+template <PersistenceType type>
+struct PersistenceTypeTraits;
+
+template <>
+struct PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT> {
+ template <typename T>
+ static T To();
+
+ static bool From(const nsACString& aString) {
+ return aString == kPersistentCString;
+ }
+
+ static bool From(const int32_t aInt32) { return aInt32 == 0; }
+
+ static bool From(nsIFile& aFile) {
+ nsAutoString leafName;
+ MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName));
+ return leafName == kPermanentString;
+ }
+};
+
+template <>
+nsLiteralCString
+PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::To<nsLiteralCString>() {
+ return kPersistentCString;
+}
+
+template <>
+struct PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY> {
+ template <typename T>
+ static T To();
+
+ static bool From(const nsACString& aString) {
+ return aString == kTemporaryCString;
+ }
+
+ static bool From(const int32_t aInt32) { return aInt32 == 1; }
+
+ static bool From(nsIFile& aFile) {
+ nsAutoString leafName;
+ MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName));
+ return leafName == kTemporaryString;
+ }
+};
+
+template <>
+nsLiteralCString
+PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::To<nsLiteralCString>() {
+ return kTemporaryCString;
+}
+
+template <>
+struct PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT> {
+ template <typename T>
+ static T To();
+
+ static bool From(const nsACString& aString) {
+ return aString == kDefaultCString;
+ }
+
+ static bool From(const int32_t aInt32) { return aInt32 == 2; }
+
+ static bool From(nsIFile& aFile) {
+ nsAutoString leafName;
+ MOZ_ALWAYS_SUCCEEDS(aFile.GetLeafName(leafName));
+ return leafName == kDefaultString;
+ }
+};
+
+template <>
+nsLiteralCString
+PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::To<nsLiteralCString>() {
+ return kDefaultCString;
+}
+
+template <typename T>
+Maybe<T> TypeTo_impl(const PersistenceType aPersistenceType) {
+ switch (aPersistenceType) {
+ case PERSISTENCE_TYPE_PERSISTENT:
+ return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::To<T>());
+
+ case PERSISTENCE_TYPE_TEMPORARY:
+ return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::To<T>());
+
+ case PERSISTENCE_TYPE_DEFAULT:
+ return Some(PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::To<T>());
+
+ default:
+ return Nothing();
+ }
+}
+
+template <typename T>
+Maybe<PersistenceType> TypeFrom_impl(T& aData) {
+ if (PersistenceTypeTraits<PERSISTENCE_TYPE_PERSISTENT>::From(aData)) {
+ return Some(PERSISTENCE_TYPE_PERSISTENT);
+ }
+
+ if (PersistenceTypeTraits<PERSISTENCE_TYPE_TEMPORARY>::From(aData)) {
+ return Some(PERSISTENCE_TYPE_TEMPORARY);
+ }
+
+ if (PersistenceTypeTraits<PERSISTENCE_TYPE_DEFAULT>::From(aData)) {
+ return Some(PERSISTENCE_TYPE_DEFAULT);
+ }
+
+ return Nothing();
+}
+
+void BadPersistenceType() { MOZ_CRASH("Bad persistence type value!"); }
+
+} // namespace
+
+bool IsValidPersistenceType(const PersistenceType aPersistenceType) {
+ switch (aPersistenceType) {
+ case PERSISTENCE_TYPE_PERSISTENT:
+ case PERSISTENCE_TYPE_TEMPORARY:
+ case PERSISTENCE_TYPE_DEFAULT:
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+bool IsBestEffortPersistenceType(const PersistenceType aPersistenceType) {
+ switch (aPersistenceType) {
+ case PERSISTENCE_TYPE_TEMPORARY:
+ case PERSISTENCE_TYPE_DEFAULT:
+ return true;
+
+ case PERSISTENCE_TYPE_PERSISTENT:
+ case PERSISTENCE_TYPE_INVALID:
+ default:
+ return false;
+ }
+}
+
+nsLiteralCString PersistenceTypeToString(
+ const PersistenceType aPersistenceType) {
+ const auto maybeString = TypeTo_impl<nsLiteralCString>(aPersistenceType);
+ if (maybeString.isNothing()) {
+ BadPersistenceType();
+ }
+ return maybeString.value();
+}
+
+Maybe<PersistenceType> PersistenceTypeFromString(const nsACString& aString,
+ const fallible_t&) {
+ return TypeFrom_impl(aString);
+}
+
+PersistenceType PersistenceTypeFromString(const nsACString& aString) {
+ const auto maybePersistenceType = TypeFrom_impl(aString);
+ if (maybePersistenceType.isNothing()) {
+ BadPersistenceType();
+ }
+ return maybePersistenceType.value();
+}
+
+Maybe<PersistenceType> PersistenceTypeFromInt32(const int32_t aInt32,
+ const fallible_t&) {
+ return TypeFrom_impl(aInt32);
+}
+
+Maybe<PersistenceType> PersistenceTypeFromFile(nsIFile& aFile,
+ const fallible_t&) {
+ return TypeFrom_impl(aFile);
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/PersistenceType.h b/dom/quota/PersistenceType.h
new file mode 100644
index 0000000000..0fcab4ff63
--- /dev/null
+++ b/dom/quota/PersistenceType.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_persistencetype_h__
+#define mozilla_dom_quota_persistencetype_h__
+
+#include <cstdint>
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/fallible.h"
+#include "nsStringFwd.h"
+
+class nsIFile;
+
+namespace mozilla::dom::quota {
+
+enum PersistenceType {
+ PERSISTENCE_TYPE_PERSISTENT = 0,
+ PERSISTENCE_TYPE_TEMPORARY,
+ PERSISTENCE_TYPE_DEFAULT,
+
+ // Only needed for IPC serialization helper, should never be used in code.
+ PERSISTENCE_TYPE_INVALID
+};
+
+static const PersistenceType kAllPersistenceTypes[] = {
+ PERSISTENCE_TYPE_PERSISTENT, PERSISTENCE_TYPE_TEMPORARY,
+ PERSISTENCE_TYPE_DEFAULT};
+
+static const PersistenceType kBestEffortPersistenceTypes[] = {
+ PERSISTENCE_TYPE_TEMPORARY, PERSISTENCE_TYPE_DEFAULT};
+
+bool IsValidPersistenceType(PersistenceType aPersistenceType);
+
+bool IsBestEffortPersistenceType(const PersistenceType aPersistenceType);
+
+nsLiteralCString PersistenceTypeToString(PersistenceType aPersistenceType);
+
+Maybe<PersistenceType> PersistenceTypeFromString(const nsACString& aString,
+ const fallible_t&);
+
+PersistenceType PersistenceTypeFromString(const nsACString& aString);
+
+Maybe<PersistenceType> PersistenceTypeFromInt32(int32_t aInt32,
+ const fallible_t&);
+
+// aFile is expected to be a repository directory (not some file or directory
+// within that).
+Maybe<PersistenceType> PersistenceTypeFromFile(nsIFile& aFile,
+ const fallible_t&);
+
+inline PersistenceType ComplementaryPersistenceType(
+ const PersistenceType aPersistenceType) {
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT ||
+ aPersistenceType == PERSISTENCE_TYPE_TEMPORARY);
+
+ if (aPersistenceType == PERSISTENCE_TYPE_DEFAULT) {
+ return PERSISTENCE_TYPE_TEMPORARY;
+ }
+
+ return PERSISTENCE_TYPE_DEFAULT;
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_persistencetype_h__
diff --git a/dom/quota/QMResult.cpp b/dom/quota/QMResult.cpp
new file mode 100644
index 0000000000..1b743679ad
--- /dev/null
+++ b/dom/quota/QMResult.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QMResult.h"
+
+#ifdef QM_ERROR_STACKS_ENABLED
+# include "mozilla/Atomics.h"
+#endif
+
+namespace mozilla {
+
+#ifdef QM_ERROR_STACKS_ENABLED
+namespace {
+
+static Atomic<uint64_t> gLastStackId{0};
+
+}
+
+QMResult::QMResult(nsresult aNSResult)
+ : mStackId(++gLastStackId), mFrameId(1), mNSResult(aNSResult) {}
+#endif
+
+} // namespace mozilla
diff --git a/dom/quota/QMResult.h b/dom/quota/QMResult.h
new file mode 100644
index 0000000000..f47c82584e
--- /dev/null
+++ b/dom/quota/QMResult.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_QMRESULT_H_
+#define DOM_QUOTA_QMRESULT_H_
+
+#include "ErrorList.h"
+#include "mozilla/dom/quota/Config.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+namespace mozilla {
+
+#ifdef QM_ERROR_STACKS_ENABLED
+// A wrapped nsresult, primarily intended for use along with mozilla::Result
+// and QM_TRY macros. The wrapper contains stack id and frame id which are
+// reported in LogError besides the error result itself.
+//
+// XXX Document the general situation more, bug 1709777.
+class QMResult {
+ uint64_t mStackId;
+ uint32_t mFrameId;
+ nsresult mNSResult;
+
+ public:
+ QMResult() : QMResult(NS_OK) {}
+
+ explicit QMResult(nsresult aNSResult);
+
+ uint64_t StackId() const { return mStackId; }
+
+ uint32_t FrameId() const { return mFrameId; }
+
+ nsresult NSResult() const { return mNSResult; }
+
+ /**
+ * Propagate the result.
+ *
+ * This is used by GenericErrorResult<QMResult> to create a propagated
+ * result.
+ */
+ QMResult Propagate() const {
+ return QMResult{mStackId, mFrameId + 1, mNSResult};
+ }
+
+ private:
+ QMResult(uint64_t aStackId, uint32_t aFrameId, nsresult aNSResult)
+ : mStackId(aStackId), mFrameId(aFrameId), mNSResult(aNSResult) {}
+};
+#endif
+
+inline QMResult ToQMResult(nsresult aValue) { return QMResult(aValue); }
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/quota/QuotaCommon.cpp b/dom/quota/QuotaCommon.cpp
new file mode 100644
index 0000000000..e2df8a1082
--- /dev/null
+++ b/dom/quota/QuotaCommon.cpp
@@ -0,0 +1,639 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+#ifdef QM_ERROR_STACKS_ENABLED
+# include "base/process_util.h"
+#endif
+#include "mozIStorageConnection.h"
+#include "mozIStorageStatement.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
+#include "nsIConsoleService.h"
+#include "nsIFile.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringFlags.h"
+#include "nsTStringRepr.h"
+#include "nsUnicharUtils.h"
+#include "nsXPCOM.h"
+#include "nsXULAppAPI.h"
+
+#ifdef XP_WIN
+# include "mozilla/Atomics.h"
+# include "mozilla/ipc/BackgroundParent.h"
+# include "mozilla/StaticPrefs_dom.h"
+# include "nsILocalFileWin.h"
+#endif
+
+namespace mozilla {
+
+RefPtr<BoolPromise> CreateAndRejectBoolPromise(const char* aFunc,
+ nsresult aRv) {
+ return CreateAndRejectMozPromise<BoolPromise>(aFunc, aRv);
+}
+
+RefPtr<Int64Promise> CreateAndRejectInt64Promise(const char* aFunc,
+ nsresult aRv) {
+ return CreateAndRejectMozPromise<Int64Promise>(aFunc, aRv);
+}
+
+RefPtr<BoolPromise> CreateAndRejectBoolPromiseFromQMResult(
+ const char* aFunc, const QMResult& aRv) {
+ return CreateAndRejectMozPromise<BoolPromise>(aFunc, aRv);
+}
+
+namespace dom::quota {
+
+using namespace mozilla::Telemetry;
+
+namespace {
+
+#ifdef DEBUG
+constexpr auto kDSStoreFileName = u".DS_Store"_ns;
+constexpr auto kDesktopFileName = u".desktop"_ns;
+constexpr auto kDesktopIniFileName = u"desktop.ini"_ns;
+constexpr auto kThumbsDbFileName = u"thumbs.db"_ns;
+#endif
+
+#ifdef XP_WIN
+Atomic<int32_t> gUseDOSDevicePathSyntax(-1);
+#endif
+
+LazyLogModule gLogger("QuotaManager");
+
+void AnonymizeCString(nsACString& aCString, uint32_t aStart) {
+ MOZ_ASSERT(!aCString.IsEmpty());
+ MOZ_ASSERT(aStart < aCString.Length());
+
+ char* iter = aCString.BeginWriting() + aStart;
+ char* end = aCString.EndWriting();
+
+ while (iter != end) {
+ char c = *iter;
+
+ if (IsAsciiAlpha(c)) {
+ *iter = 'a';
+ } else if (IsAsciiDigit(c)) {
+ *iter = 'D';
+ }
+
+ ++iter;
+ }
+}
+
+} // namespace
+
+const char kQuotaGenericDelimiter = '|';
+
+#ifdef NIGHTLY_BUILD
+const nsLiteralCString kQuotaInternalError = "internal"_ns;
+const nsLiteralCString kQuotaExternalError = "external"_ns;
+#endif
+
+LogModule* GetQuotaManagerLogger() { return gLogger; }
+
+void AnonymizeCString(nsACString& aCString) {
+ if (aCString.IsEmpty()) {
+ return;
+ }
+ AnonymizeCString(aCString, /* aStart */ 0);
+}
+
+void AnonymizeOriginString(nsACString& aOriginString) {
+ if (aOriginString.IsEmpty()) {
+ return;
+ }
+
+ int32_t start = aOriginString.FindChar(':');
+ if (start < 0) {
+ start = 0;
+ }
+
+ AnonymizeCString(aOriginString, start);
+}
+
+#ifdef XP_WIN
+void CacheUseDOSDevicePathSyntaxPrefValue() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ ::mozilla::ipc::AssertIsOnBackgroundThread();
+
+ if (gUseDOSDevicePathSyntax == -1) {
+ bool useDOSDevicePathSyntax =
+ StaticPrefs::dom_quotaManager_useDOSDevicePathSyntax_DoNotUseDirectly();
+ gUseDOSDevicePathSyntax = useDOSDevicePathSyntax ? 1 : 0;
+ }
+}
+#endif
+
+Result<nsCOMPtr<nsIFile>, nsresult> QM_NewLocalFile(const nsAString& aPath) {
+ QM_TRY_UNWRAP(
+ auto file,
+ MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr<nsIFile>, NS_NewLocalFile, aPath,
+ /* aFollowLinks */ false),
+ QM_PROPAGATE, [&aPath](const nsresult rv) {
+ QM_WARNING("Failed to construct a file for path (%s)",
+ NS_ConvertUTF16toUTF8(aPath).get());
+ });
+
+#ifdef XP_WIN
+ MOZ_ASSERT(gUseDOSDevicePathSyntax != -1);
+
+ if (gUseDOSDevicePathSyntax) {
+ QM_TRY_INSPECT(
+ const auto& winFile,
+ MOZ_TO_RESULT_GET_TYPED(nsCOMPtr<nsILocalFileWin>,
+ MOZ_SELECT_OVERLOAD(do_QueryInterface), file));
+
+ MOZ_ASSERT(winFile);
+ winFile->SetUseDOSDevicePathSyntax(true);
+ }
+#endif
+
+ return file;
+}
+
+nsDependentCSubstring GetLeafName(const nsACString& aPath) {
+ nsACString::const_iterator start, end;
+ aPath.BeginReading(start);
+ aPath.EndReading(end);
+
+ bool found = RFindInReadable("/"_ns, start, end);
+ if (found) {
+ start = end;
+ }
+
+ aPath.EndReading(end);
+
+ return nsDependentCSubstring(start.get(), end.get());
+}
+
+Result<nsCOMPtr<nsIFile>, nsresult> CloneFileAndAppend(
+ nsIFile& aDirectory, const nsAString& aPathElement) {
+ QM_TRY_UNWRAP(auto resultFile, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIFile>, aDirectory, Clone));
+
+ QM_TRY(MOZ_TO_RESULT(resultFile->Append(aPathElement)));
+
+ return resultFile;
+}
+
+Result<nsIFileKind, nsresult> GetDirEntryKind(nsIFile& aFile) {
+ // Callers call this function without checking if the directory already
+ // exists (idempotent usage). QM_OR_ELSE_WARN_IF is not used here since we
+ // just want to log NS_ERROR_FILE_NOT_FOUND and NS_ERROR_FILE_FS_CORRUPTED
+ // results and not spam the reports.
+ QM_TRY_RETURN(QM_OR_ELSE_LOG_VERBOSE_IF(
+ MOZ_TO_RESULT_INVOKE_MEMBER(aFile, IsDirectory)
+ .map([](const bool isDirectory) {
+ return isDirectory ? nsIFileKind::ExistsAsDirectory
+ : nsIFileKind::ExistsAsFile;
+ }),
+ ([](const nsresult rv) {
+ return rv == NS_ERROR_FILE_NOT_FOUND ||
+ // We treat NS_ERROR_FILE_FS_CORRUPTED as if the file did not
+ // exist at all.
+ rv == NS_ERROR_FILE_FS_CORRUPTED;
+ }),
+ ErrToOk<nsIFileKind::DoesNotExist>));
+}
+
+Result<nsCOMPtr<mozIStorageStatement>, nsresult> CreateStatement(
+ mozIStorageConnection& aConnection, const nsACString& aStatementString) {
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection, CreateStatement,
+ aStatementString));
+}
+
+template <SingleStepResult ResultHandling>
+Result<SingleStepSuccessType<ResultHandling>, nsresult> ExecuteSingleStep(
+ nsCOMPtr<mozIStorageStatement>&& aStatement) {
+ QM_TRY_INSPECT(const bool& hasResult,
+ MOZ_TO_RESULT_INVOKE_MEMBER(aStatement, ExecuteStep));
+
+ if constexpr (ResultHandling == SingleStepResult::AssertHasResult) {
+ MOZ_ASSERT(hasResult);
+ (void)hasResult;
+
+ return WrapNotNullUnchecked(std::move(aStatement));
+ } else {
+ return hasResult ? std::move(aStatement) : nullptr;
+ }
+}
+
+template Result<SingleStepSuccessType<SingleStepResult::AssertHasResult>,
+ nsresult>
+ExecuteSingleStep<SingleStepResult::AssertHasResult>(
+ nsCOMPtr<mozIStorageStatement>&&);
+
+template Result<SingleStepSuccessType<SingleStepResult::ReturnNullIfNoResult>,
+ nsresult>
+ExecuteSingleStep<SingleStepResult::ReturnNullIfNoResult>(
+ nsCOMPtr<mozIStorageStatement>&&);
+
+template <SingleStepResult ResultHandling>
+Result<SingleStepSuccessType<ResultHandling>, nsresult>
+CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection,
+ const nsACString& aStatementString) {
+ QM_TRY_UNWRAP(auto stmt, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, aConnection,
+ CreateStatement, aStatementString));
+
+ return ExecuteSingleStep<ResultHandling>(std::move(stmt));
+}
+
+template Result<SingleStepSuccessType<SingleStepResult::AssertHasResult>,
+ nsresult>
+CreateAndExecuteSingleStepStatement<SingleStepResult::AssertHasResult>(
+ mozIStorageConnection& aConnection, const nsACString& aStatementString);
+
+template Result<SingleStepSuccessType<SingleStepResult::ReturnNullIfNoResult>,
+ nsresult>
+CreateAndExecuteSingleStepStatement<SingleStepResult::ReturnNullIfNoResult>(
+ mozIStorageConnection& aConnection, const nsACString& aStatementString);
+
+namespace detail {
+
+// Given aPath of /foo/bar/baz and aRelativePath of /bar/baz, returns the
+// absolute portion of aPath /foo by removing the common suffix from aPath.
+nsDependentCSubstring GetTreeBase(const nsLiteralCString& aPath,
+ const nsLiteralCString& aRelativePath) {
+ MOZ_ASSERT(StringEndsWith(aPath, aRelativePath));
+ return Substring(aPath, 0, aPath.Length() - aRelativePath.Length());
+}
+
+nsDependentCSubstring GetSourceTreeBase() {
+ static constexpr auto thisSourceFileRelativePath =
+ "/dom/quota/QuotaCommon.cpp"_ns;
+
+ return GetTreeBase(nsLiteralCString(__FILE__), thisSourceFileRelativePath);
+}
+
+nsDependentCSubstring GetObjdirDistIncludeTreeBase(
+ const nsLiteralCString& aQuotaCommonHPath) {
+ static constexpr auto quotaCommonHSourceFileRelativePath =
+ "/mozilla/dom/quota/QuotaCommon.h"_ns;
+
+ return GetTreeBase(aQuotaCommonHPath, quotaCommonHSourceFileRelativePath);
+}
+
+static constexpr auto kSourceFileRelativePathMap =
+ std::array<std::pair<nsLiteralCString, nsLiteralCString>, 1>{
+ {{"mozilla/dom/LocalStorageCommon.h"_ns,
+ "dom/localstorage/LocalStorageCommon.h"_ns}}};
+
+nsDependentCSubstring MakeSourceFileRelativePath(
+ const nsACString& aSourceFilePath) {
+ static constexpr auto error = "ERROR"_ns;
+ static constexpr auto mozillaRelativeBase = "mozilla/"_ns;
+
+ static const auto sourceTreeBase = GetSourceTreeBase();
+
+ if (MOZ_LIKELY(StringBeginsWith(aSourceFilePath, sourceTreeBase))) {
+ return Substring(aSourceFilePath, sourceTreeBase.Length() + 1);
+ }
+
+ // The source file could have been exported to the OBJDIR/dist/include
+ // directory, so we need to check that case as well.
+ static const auto objdirDistIncludeTreeBase = GetObjdirDistIncludeTreeBase();
+
+ if (MOZ_LIKELY(
+ StringBeginsWith(aSourceFilePath, objdirDistIncludeTreeBase))) {
+ const auto sourceFileRelativePath =
+ Substring(aSourceFilePath, objdirDistIncludeTreeBase.Length() + 1);
+
+ // Exported source files don't have to use the same directory structure as
+ // original source files. Check if we have a mapping for the exported
+ // source file.
+ const auto foundIt = std::find_if(
+ kSourceFileRelativePathMap.cbegin(), kSourceFileRelativePathMap.cend(),
+ [&sourceFileRelativePath](const auto& entry) {
+ return entry.first == sourceFileRelativePath;
+ });
+
+ if (MOZ_UNLIKELY(foundIt != kSourceFileRelativePathMap.cend())) {
+ return Substring(foundIt->second, 0);
+ }
+
+ // If we don't have a mapping for it, just remove the mozilla/ prefix
+ // (if there's any).
+ if (MOZ_LIKELY(
+ StringBeginsWith(sourceFileRelativePath, mozillaRelativeBase))) {
+ return Substring(sourceFileRelativePath, mozillaRelativeBase.Length());
+ }
+
+ // At this point, we don't know how to transform the relative path of the
+ // exported source file back to the relative path of the original source
+ // file. This can happen when QM_TRY is used in an exported nsIFoo.h file.
+ // If you really need to use QM_TRY there, consider adding a new mapping
+ // for the exported source file.
+ return sourceFileRelativePath;
+ }
+
+ nsCString::const_iterator begin, end;
+ if (RFindInReadable("/"_ns, aSourceFilePath.BeginReading(begin),
+ aSourceFilePath.EndReading(end))) {
+ // Use the basename as a fallback, to avoid exposing any user parts of the
+ // path.
+ ++begin;
+ return Substring(begin, aSourceFilePath.EndReading(end));
+ }
+
+ return nsDependentCSubstring{static_cast<mozilla::Span<const char>>(
+ static_cast<const nsCString&>(error))};
+}
+
+} // namespace detail
+
+#ifdef QM_LOG_ERROR_ENABLED
+# ifdef QM_ERROR_STACKS_ENABLED
+void LogError(const nsACString& aExpr, const ResultType& aResult,
+ const nsACString& aSourceFilePath, const int32_t aSourceFileLine,
+ const Severity aSeverity)
+# else
+void LogError(const nsACString& aExpr, const Maybe<nsresult> aMaybeRv,
+ const nsACString& aSourceFilePath, const int32_t aSourceFileLine,
+ const Severity aSeverity)
+# endif
+{
+ // TODO: Add MOZ_LOG support, bug 1711661.
+
+ // We have to ignore failures with the Verbose severity until we have support
+ // for MOZ_LOG.
+ if (aSeverity == Severity::Verbose) {
+ return;
+ }
+
+ nsAutoCString context;
+
+# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+
+ if (const auto contextIt = extraInfoMap.find(ScopedLogExtraInfo::kTagContext);
+ contextIt != extraInfoMap.cend()) {
+ context = *contextIt->second;
+ }
+# endif
+
+ const auto severityString = [&aSeverity]() -> nsLiteralCString {
+ switch (aSeverity) {
+ case Severity::Error:
+ return "ERROR"_ns;
+ case Severity::Warning:
+ return "WARNING"_ns;
+ case Severity::Info:
+ return "INFO"_ns;
+ case Severity::Verbose:
+ return "VERBOSE"_ns;
+ }
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad severity value!");
+ }();
+
+ Maybe<nsresult> maybeRv;
+
+# ifdef QM_ERROR_STACKS_ENABLED
+ if (aResult.is<QMResult>()) {
+ maybeRv = Some(aResult.as<QMResult>().NSResult());
+ } else if (aResult.is<nsresult>()) {
+ maybeRv = Some(aResult.as<nsresult>());
+ }
+# else
+ maybeRv = aMaybeRv;
+# endif
+
+ nsAutoCString rvCode;
+ nsAutoCString rvName;
+
+ if (maybeRv) {
+ nsresult rv = *maybeRv;
+
+ rvCode = nsPrintfCString("0x%" PRIX32, static_cast<uint32_t>(rv));
+
+ // XXX NS_ERROR_MODULE_WIN32 should be handled in GetErrorName directly.
+ if (NS_ERROR_GET_MODULE(rv) == NS_ERROR_MODULE_WIN32) {
+ // XXX We could also try to get the Win32 error name here.
+ rvName = nsPrintfCString(
+ "NS_ERROR_GENERATE_FAILURE(NS_ERROR_MODULE_WIN32, 0x%" PRIX16 ")",
+ NS_ERROR_GET_CODE(rv));
+ } else {
+ mozilla::GetErrorName(rv, rvName);
+ }
+ }
+
+# ifdef QM_ERROR_STACKS_ENABLED
+ nsAutoCString frameIdString;
+ nsAutoCString stackIdString;
+ nsAutoCString processIdString;
+
+ if (aResult.is<QMResult>()) {
+ const QMResult& result = aResult.as<QMResult>();
+ frameIdString = IntToCString(result.FrameId());
+ stackIdString = IntToCString(result.StackId());
+ processIdString =
+ IntToCString(static_cast<uint32_t>(base::GetCurrentProcId()));
+ }
+# endif
+
+ nsAutoCString extraInfosString;
+
+ if (!rvCode.IsEmpty()) {
+ extraInfosString.Append(" failed with resultCode "_ns + rvCode);
+ }
+
+ if (!rvName.IsEmpty()) {
+ extraInfosString.Append(", resultName "_ns + rvName);
+ }
+
+# ifdef QM_ERROR_STACKS_ENABLED
+ if (!frameIdString.IsEmpty()) {
+ extraInfosString.Append(", frameId "_ns + frameIdString);
+ }
+
+ if (!stackIdString.IsEmpty()) {
+ extraInfosString.Append(", stackId "_ns + stackIdString);
+ }
+
+ if (!processIdString.IsEmpty()) {
+ extraInfosString.Append(", processId "_ns + processIdString);
+ }
+# endif
+
+# ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ for (const auto& item : extraInfoMap) {
+ extraInfosString.Append(", "_ns + nsDependentCString(item.first) + " "_ns +
+ *item.second);
+ }
+# endif
+
+ const auto sourceFileRelativePath =
+ detail::MakeSourceFileRelativePath(aSourceFilePath);
+
+# ifdef QM_LOG_ERROR_TO_CONSOLE_ENABLED
+ NS_DebugBreak(
+ NS_DEBUG_WARNING,
+ nsAutoCString("QM_TRY failure ("_ns + severityString + ")"_ns).get(),
+ (extraInfosString.IsEmpty() ? nsPromiseFlatCString(aExpr)
+ : static_cast<const nsCString&>(nsAutoCString(
+ aExpr + extraInfosString)))
+ .get(),
+ nsPromiseFlatCString(sourceFileRelativePath).get(), aSourceFileLine);
+# endif
+
+# ifdef QM_LOG_ERROR_TO_BROWSER_CONSOLE_ENABLED
+ // XXX We might want to allow reporting to the browsing console even when
+ // there's no context in future once we are sure that it can't spam the
+ // browser console or when we have special about:quotamanager for the
+ // reporting (instead of the browsing console).
+ // Another option is to keep the current check and rely on MOZ_LOG reporting
+ // in future once that's available.
+ if (!context.IsEmpty()) {
+ nsCOMPtr<nsIConsoleService> console =
+ do_GetService(NS_CONSOLESERVICE_CONTRACTID);
+ if (console) {
+ NS_ConvertUTF8toUTF16 message(
+ "QM_TRY failure ("_ns + severityString + ")"_ns + ": '"_ns + aExpr +
+ extraInfosString + "', file "_ns + sourceFileRelativePath + ":"_ns +
+ IntToCString(aSourceFileLine));
+
+ // The concatenation above results in a message like:
+ // QM_TRY failure (ERROR): 'MaybeRemoveLocalStorageArchiveTmpFile() failed
+ // with resultCode 0x80004005, resultName NS_ERROR_FAILURE, frameId 1,
+ // stackId 1, processId 53978, context Initialization::Storage', file
+ // dom/quota/ActorsParent.cpp:6029
+
+ console->LogStringMessage(message.get());
+ }
+ }
+# endif
+
+# ifdef QM_LOG_ERROR_TO_TELEMETRY_ENABLED
+ if (!context.IsEmpty()) {
+ // For now, we don't include aExpr in the telemetry event. It might help to
+ // match locations across versions, but they might be large.
+ auto extra = Some([&] {
+ auto res = CopyableTArray<EventExtraEntry>{};
+ res.SetCapacity(9);
+
+ res.AppendElement(EventExtraEntry{"context"_ns, nsCString{context}});
+
+# ifdef QM_ERROR_STACKS_ENABLED
+ if (!frameIdString.IsEmpty()) {
+ res.AppendElement(
+ EventExtraEntry{"frame_id"_ns, nsCString{frameIdString}});
+ }
+
+ if (!processIdString.IsEmpty()) {
+ res.AppendElement(
+ EventExtraEntry{"process_id"_ns, nsCString{processIdString}});
+ }
+# endif
+
+ if (!rvName.IsEmpty()) {
+ res.AppendElement(EventExtraEntry{"result"_ns, nsCString{rvName}});
+ }
+
+ // Here, we are generating thread local sequence number and thread Id
+ // information which could be useful for summarizing and categorizing
+ // log statistics in QM_TRY stack propagation scripts. Since, this is
+ // a thread local object, we do not need to worry about data races.
+ static MOZ_THREAD_LOCAL(uint32_t) sSequenceNumber;
+
+ // This would be initialized once, all subsequent calls would be a no-op.
+ MOZ_ALWAYS_TRUE(sSequenceNumber.init());
+
+ // sequence number should always starts at number 1.
+ // `sSequenceNumber` gets initialized to 0; so we have to increment here.
+ const auto newSeqNum = sSequenceNumber.get() + 1;
+ const auto threadId =
+ mozilla::baseprofiler::profiler_current_thread_id().ToNumber();
+
+ const auto threadIdAndSequence =
+ (static_cast<uint64_t>(threadId) << 32) | (newSeqNum & 0xFFFFFFFF);
+
+ res.AppendElement(
+ EventExtraEntry{"seq"_ns, IntToCString(threadIdAndSequence)});
+
+ sSequenceNumber.set(newSeqNum);
+
+ res.AppendElement(EventExtraEntry{"severity"_ns, severityString});
+
+ res.AppendElement(
+ EventExtraEntry{"source_file"_ns, nsCString(sourceFileRelativePath)});
+
+ res.AppendElement(
+ EventExtraEntry{"source_line"_ns, IntToCString(aSourceFileLine)});
+
+# ifdef QM_ERROR_STACKS_ENABLED
+ if (!stackIdString.IsEmpty()) {
+ res.AppendElement(
+ EventExtraEntry{"stack_id"_ns, nsCString{stackIdString}});
+ }
+# endif
+
+ return res;
+ }());
+
+ Telemetry::RecordEvent(Telemetry::EventID::DomQuotaTry_Error_Step,
+ Nothing(), extra);
+ }
+# endif
+}
+#endif
+
+#ifdef DEBUG
+Result<bool, nsresult> WarnIfFileIsUnknown(nsIFile& aFile,
+ const char* aSourceFilePath,
+ const int32_t aSourceFileLine) {
+ nsString leafName;
+ nsresult rv = aFile.GetLeafName(leafName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ bool isDirectory;
+ rv = aFile.IsDirectory(&isDirectory);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return Err(rv);
+ }
+
+ if (!isDirectory) {
+ // Don't warn about OS metadata files. These files are only used in
+ // different platforms, but the profile can be shared across different
+ // operating systems, so we check it on all platforms.
+ if (leafName.Equals(kDSStoreFileName) ||
+ leafName.Equals(kDesktopFileName) ||
+ leafName.Equals(kDesktopIniFileName,
+ nsCaseInsensitiveStringComparator) ||
+ leafName.Equals(kThumbsDbFileName, nsCaseInsensitiveStringComparator)) {
+ return false;
+ }
+
+ // Don't warn about files starting with ".".
+ if (leafName.First() == char16_t('.')) {
+ return false;
+ }
+ }
+
+ NS_DebugBreak(
+ NS_DEBUG_WARNING,
+ nsPrintfCString("Something (%s) in the directory that doesn't belong!",
+ NS_ConvertUTF16toUTF8(leafName).get())
+ .get(),
+ nullptr, aSourceFilePath, aSourceFileLine);
+
+ return true;
+}
+#endif
+
+} // namespace dom::quota
+} // namespace mozilla
diff --git a/dom/quota/QuotaCommon.h b/dom/quota/QuotaCommon.h
new file mode 100644
index 0000000000..848118797e
--- /dev/null
+++ b/dom/quota/QuotaCommon.h
@@ -0,0 +1,1675 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_quotacommon_h__
+#define mozilla_dom_quota_quotacommon_h__
+
+#include "mozilla/dom/quota/Config.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+#include <utility>
+#include "mozIStorageStatement.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MacroArgs.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#if defined(QM_LOG_ERROR_ENABLED) && defined(QM_ERROR_STACKS_ENABLED)
+# include "mozilla/Variant.h"
+#endif
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/FirstInitializationAttemptsImpl.h"
+#include "mozilla/dom/quota/RemoveParen.h"
+#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "nsCOMPtr.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsIEventTarget.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsTLiteralString.h"
+
+namespace mozilla {
+template <typename T>
+class NotNull;
+}
+
+#define MOZ_ARGS_AFTER_3(a1, a2, a3, ...) __VA_ARGS__
+
+#define MOZ_ADD_ARGS2(...) , ##__VA_ARGS__
+#define MOZ_ADD_ARGS(...) MOZ_ADD_ARGS2(__VA_ARGS__)
+
+// Proper use of unique variable names can be tricky (especially if nesting of
+// the final macro is required).
+// See https://lifecs.likai.org/2016/07/c-preprocessor-hygienic-macros.html
+#define MOZ_UNIQUE_VAR(base) MOZ_CONCAT(base, __COUNTER__)
+
+// See https://florianjw.de/en/passing_overloaded_functions.html
+// TODO: Add a test for this macro.
+#define MOZ_SELECT_OVERLOAD(func) \
+ [](auto&&... aArgs) -> decltype(auto) { \
+ return func(std::forward<decltype(aArgs)>(aArgs)...); \
+ }
+
+#define DSSTORE_FILE_NAME ".DS_Store"
+#define DESKTOP_FILE_NAME ".desktop"
+#define DESKTOP_INI_FILE_NAME "desktop.ini"
+#define THUMBS_DB_FILE_NAME "thumbs.db"
+
+#define QM_WARNING(...) \
+ do { \
+ nsPrintfCString str(__VA_ARGS__); \
+ mozilla::dom::quota::ReportInternalError(__FILE__, __LINE__, str.get()); \
+ NS_WARNING(str.get()); \
+ } while (0)
+
+#define QM_LOG_TEST() MOZ_LOG_TEST(GetQuotaManagerLogger(), LogLevel::Info)
+#define QM_LOG(_args) MOZ_LOG(GetQuotaManagerLogger(), LogLevel::Info, _args)
+
+#define UNKNOWN_FILE_WARNING(_leafName) \
+ NS_WARNING( \
+ nsPrintfCString("Something (%s) in the directory that doesn't belong!", \
+ NS_ConvertUTF16toUTF8(_leafName).get()) \
+ .get())
+
+// This macro should be used in directory traversals for files or directories
+// that are unknown for given directory traversal. It should only be called
+// after all known (directory traversal specific) files or directories have
+// been checked and handled.
+// XXX Consider renaming the macro to QM_LOG_UNKNOWN_DIR_ENTRY.
+#ifdef DEBUG
+# define WARN_IF_FILE_IS_UNKNOWN(_file) \
+ mozilla::dom::quota::WarnIfFileIsUnknown(_file, __FILE__, __LINE__)
+#else
+# define WARN_IF_FILE_IS_UNKNOWN(_file) Result<bool, nsresult>(false)
+#endif
+
+/**
+ * There are multiple ways to handle unrecoverable conditions (note that the
+ * patterns are put in reverse chronological order and only the first pattern
+ * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL should be used in
+ * new code):
+ *
+ * 1. Using QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL macros
+ * (Quota manager specific, defined below)
+ *
+ * Typical use cases:
+ *
+ * nsresult MyFunc1(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(aFile.Exists(&exists));
+ * QM_TRY(OkIf(exists), NS_ERROR_FAILURE);
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc2(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(aFile.Exists(&exists), NS_ERROR_UNEXPECTED);
+ * QM_TRY(OkIf(exists), NS_ERROR_UNEXPECTED);
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * void MyFunc3(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(aFile.Exists(&exists), QM_VOID);
+ * QM_TRY(OkIf(exists), QM_VOID);
+ *
+ * // The file exists, and data could be read from it here.
+ * }
+ *
+ * nsresult MyFunc4(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(storageFile->Exists(&exists), QM_PROPAGATE,
+ * []() { NS_WARNING("The Exists call failed!"); });
+ * QM_TRY(OkIf(exists), NS_ERROR_FAILURE,
+ * []() { NS_WARNING("The file doesn't exist!"); });
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc5(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(aFile.Exists(&exists));
+ * if (exists) {
+ * // The file exists, and data could be read from it here.
+ * } else {
+ * QM_FAIL(NS_ERROR_FAILURE);
+ * }
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc6(nsIFile& aFile) {
+ * bool exists;
+ * QM_TRY(aFile.Exists(&exists));
+ * if (exists) {
+ * // The file exists, and data could be read from it here.
+ * } else {
+ * QM_FAIL(NS_ERROR_FAILURE,
+ * []() { NS_WARNING("The file doesn't exist!"); });
+ * }
+ *
+ * return NS_OK;
+ * }
+ *
+ * 2. Using MOZ_TRY/MOZ_TRY_VAR macros
+ *
+ * Typical use cases:
+ *
+ * nsresult MyFunc1(nsIFile& aFile) {
+ * // MOZ_TRY can't return a custom return value
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc2(nsIFile& aFile) {
+ * // MOZ_TRY can't return a custom return value
+ *
+ * return NS_OK;
+ * }
+ *
+ * void MyFunc3(nsIFile& aFile) {
+ * // MOZ_TRY can't return a custom return value, "void" in this case
+ * }
+ *
+ * nsresult MyFunc4(nsIFile& aFile) {
+ * // MOZ_TRY can't return a custom return value and run an additional
+ * // cleanup function
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc5(nsIFile& aFile) {
+ * // There's no MOZ_FAIL, MOZ_TRY can't return a custom return value
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc6(nsIFile& aFile) {
+ * // There's no MOZ_FAIL, MOZ_TRY can't return a custom return value and run
+ * // an additional cleanup function
+ *
+ * return NS_OK;
+ * }
+ *
+ * 3. Using NS_WARN_IF and NS_WARNING macro with own control flow handling
+ *
+ * Typical use cases:
+ *
+ * nsresult MyFunc1(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * return rv;
+ * }
+ * if (NS_WARN_IF(!exists) {
+ * return NS_ERROR_FAILURE;
+ * }
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc2(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * return NS_ERROR_UNEXPECTED;
+ * }
+ * if (NS_WARN_IF(!exists) {
+ * return NS_ERROR_UNEXPECTED;
+ * }
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * void MyFunc3(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * return;
+ * }
+ * if (NS_WARN_IF(!exists) {
+ * return;
+ * }
+ *
+ * // The file exists, and data could be read from it here.
+ * }
+ *
+ * nsresult MyFunc4(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * NS_WARNING("The Exists call failed!");
+ * return rv;
+ * }
+ * if (NS_WARN_IF(!exists) {
+ * NS_WARNING("The file doesn't exist!");
+ * return NS_ERROR_FAILURE;
+ * }
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc5(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * return rv;
+ * }
+ * if (exists) {
+ * // The file exists, and data could be read from it here.
+ * } else {
+ * return NS_ERROR_FAILURE;
+ * }
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc6(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * if (NS_WARN_IF(NS_FAILED(rv)) {
+ * return rv;
+ * }
+ * if (exists) {
+ * // The file exists, and data could be read from it here.
+ * } else {
+ * NS_WARNING("The file doesn't exist!");
+ * return NS_ERROR_FAILURE;
+ * }
+ *
+ * return NS_OK;
+ * }
+ *
+ * 4. Using NS_ENSURE_* macros
+ *
+ * Mainly:
+ * - NS_ENSURE_SUCCESS
+ * - NS_ENSURE_SUCCESS_VOID
+ * - NS_ENSURE_TRUE
+ * - NS_ENSURE_TRUE_VOID
+ *
+ * Typical use cases:
+ *
+ * nsresult MyFunc1(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * NS_ENSURE_SUCCESS(rv, rv);
+ * NS_ENSURE_TRUE(exists, NS_ERROR_FAILURE);
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc2(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * NS_ENSURE_SUCCESS(rv, NS_ERROR_UNEXPECTED);
+ * NS_ENSURE_TRUE(exists, NS_ERROR_UNEXPECTED);
+ *
+ * // The file exists, and data could be read from it here.
+ *
+ * return NS_OK;
+ * }
+ *
+ * void MyFunc3(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * NS_ENSURE_SUCCESS_VOID(rv);
+ * NS_ENSURE_TRUE_VOID(exists);
+ *
+ * // The file exists, and data could be read from it here.
+ * }
+ *
+ * nsresult MyFunc4(nsIFile& aFile) {
+ * // NS_ENSURE_SUCCESS/NS_ENSURE_TRUE can't run an additional cleanup
+ * // function
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc5(nsIFile& aFile) {
+ * bool exists;
+ * nsresult rv = aFile.Exists(&exists);
+ * NS_ENSURE_SUCCESS(rv, rv);
+ * if (exists) {
+ * // The file exists, and data could be read from it here.
+ * } else {
+ * NS_ENSURE_TRUE(false, NS_ERROR_FAILURE);
+ * }
+ *
+ * return NS_OK;
+ * }
+ *
+ * nsresult MyFunc6(nsIFile& aFile) {
+ * // NS_ENSURE_TRUE can't run an additional cleanup function
+ *
+ * return NS_OK;
+ * }
+ *
+ * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT is like MOZ_TRY/MOZ_TRY_VAR but if an
+ * error occurs it additionally calls a generic function HandleError to handle
+ * the error and it can be used to return custom return values as well and even
+ * call an additional cleanup function.
+ * HandleError currently only warns in debug builds, it will report to the
+ * browser console and telemetry in the future.
+ * The other advantage of QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT is that a local
+ * nsresult is not needed at all in all cases, all calls can be wrapped
+ * directly. If an error occurs, the warning contains a concrete call instead
+ * of the rv local variable. For example:
+ *
+ * 1. WARNING: NS_ENSURE_SUCCESS(rv, rv) failed with result 0x80004005
+ * (NS_ERROR_FAILURE): file XYZ, line N
+ *
+ * 2. WARNING: 'NS_FAILED(rv)', file XYZ, line N
+ *
+ * 3. Nothing (MOZ_TRY doesn't warn)
+ *
+ * 4. WARNING: Error: 'aFile.Exists(&exists)', file XYZ, line N
+ *
+ * QM_TRY_RETURN is a supplementary macro for cases when the result's success
+ * value can be directly returned (instead of assigning to a variable as in the
+ * QM_TRY_UNWRAP/QM_TRY_INSPECT case).
+ *
+ * QM_FAIL is a supplementary macro for cases when an error needs to be
+ * returned without evaluating an expression. It's possible to write
+ * QM_TRY(OkIf(false), NS_ERROR_FAILURE), but QM_FAIL(NS_ERROR_FAILURE) looks
+ * more straightforward.
+ *
+ * It's highly recommended to use
+ * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_TRY_RETURN/QM_FAIL in new code for
+ * quota manager and quota clients. Existing code should be incrementally
+ * converted as needed.
+ *
+ * QM_TRY_VOID/QM_TRY_UNWRAP_VOID/QM_TRY_INSPECT_VOID/QM_FAIL_VOID is not
+ * defined on purpose since it's possible to use
+ * QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_FAIL even in void functions.
+ * However, QM_TRY(Task(), ) would look odd so it's recommended to use a dummy
+ * define QM_VOID that evaluates to nothing instead: QM_TRY(Task(), QM_VOID)
+ *
+ * Custom return values can be static or dynamically generated using functions
+ * with one of these signatures:
+ * auto(const char* aFunc, const char* aExpr);
+ * auto(const char* aFunc, const T& aRv);
+ * auto(const T& aRc);
+ */
+
+#define QM_VOID
+
+#define QM_PROPAGATE Err(tryTempError)
+
+#define QM_IPC_FAIL(actor) \
+ [&_actor = *actor](const char* aFunc, const char* aExpr) { \
+ return Err( \
+ mozilla::ipc::IPCResult::Fail(WrapNotNull(&_actor), aFunc, aExpr)); \
+ }
+
+#ifdef DEBUG
+# define QM_ASSERT_UNREACHABLE \
+ [](const char*, const char*) -> ::mozilla::GenericErrorResult<nsresult> { \
+ MOZ_CRASH("Should never be reached."); \
+ }
+
+# define QM_ASSERT_UNREACHABLE_VOID \
+ [](const char*, const char*) { MOZ_CRASH("Should never be reached."); }
+#endif
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+# define QM_DIAGNOSTIC_ASSERT_UNREACHABLE \
+ [](const char*, const char*) -> ::mozilla::GenericErrorResult<nsresult> { \
+ MOZ_CRASH("Should never be reached."); \
+ }
+
+# define QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID \
+ [](const char*, const char*) { MOZ_CRASH("Should never be reached."); }
+#endif
+
+// QM_MISSING_ARGS and QM_HANDLE_ERROR macros are implementation details of
+// QM_TRY/QM_TRY_UNWRAP/QM_TRY_INSPECT/QM_FAIL and shouldn't be used directly.
+
+#define QM_MISSING_ARGS(...) \
+ do { \
+ static_assert(false, "Did you forget arguments?"); \
+ } while (0)
+
+#ifdef DEBUG
+# define QM_HANDLE_ERROR(expr, error, severity) \
+ HandleError(#expr, error, __FILE__, __LINE__, severity)
+#else
+# define QM_HANDLE_ERROR(expr, error, severity) \
+ HandleError("Unavailable", error, __FILE__, __LINE__, severity)
+#endif
+
+#ifdef DEBUG
+# define QM_HANDLE_ERROR_RETURN_NOTHING(expr, error, severity) \
+ HandleErrorReturnNothing(#expr, error, __FILE__, __LINE__, severity)
+#else
+# define QM_HANDLE_ERROR_RETURN_NOTHING(expr, error, severity) \
+ HandleErrorReturnNothing("Unavailable", error, __FILE__, __LINE__, severity)
+#endif
+
+#ifdef DEBUG
+# define QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING(expr, error, severity, \
+ cleanup) \
+ HandleErrorWithCleanupReturnNothing(#expr, error, __FILE__, __LINE__, \
+ severity, cleanup)
+#else
+# define QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING(expr, error, severity, \
+ cleanup) \
+ HandleErrorWithCleanupReturnNothing("Unavailable", error, __FILE__, \
+ __LINE__, severity, cleanup)
+#endif
+
+// Handles the case when QM_VOID is passed as a custom return value.
+#define QM_HANDLE_CUSTOM_RET_VAL_HELPER0(func, expr, error)
+
+#define QM_HANDLE_CUSTOM_RET_VAL_HELPER1(func, expr, error, customRetVal) \
+ mozilla::dom::quota::HandleCustomRetVal(func, #expr, error, customRetVal)
+
+#define QM_HANDLE_CUSTOM_RET_VAL_GLUE(a, b) a b
+
+#define QM_HANDLE_CUSTOM_RET_VAL(...) \
+ QM_HANDLE_CUSTOM_RET_VAL_GLUE( \
+ MOZ_PASTE_PREFIX_AND_ARG_COUNT(QM_HANDLE_CUSTOM_RET_VAL_HELPER, \
+ MOZ_ARGS_AFTER_3(__VA_ARGS__)), \
+ (MOZ_ARG_1(__VA_ARGS__), MOZ_ARG_2(__VA_ARGS__), \
+ MOZ_ARG_3(__VA_ARGS__) MOZ_ADD_ARGS(MOZ_ARGS_AFTER_3(__VA_ARGS__))))
+
+// QM_TRY_PROPAGATE_ERR, QM_TRY_CUSTOM_RET_VAL,
+// QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_GLUE macros are implementation
+// details of QM_TRY and shouldn't be used directly.
+
+// Handles the two arguments case when the error is propagated.
+#define QM_TRY_PROPAGATE_ERR(tryResult, expr) \
+ auto tryResult = (expr); \
+ static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \
+ return tryResult.propagateErr(); \
+ }
+
+// Handles the three arguments case when a custom return value needs to be
+// returned
+#define QM_TRY_CUSTOM_RET_VAL(tryResult, expr, customRetVal) \
+ auto tryResult = (expr); \
+ static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::Error); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ }
+
+// Handles the four arguments case when a cleanup function needs to be called
+// before a custom return value is returned
+#define QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP(tryResult, expr, customRetVal, \
+ cleanup) \
+ auto tryResult = (expr); \
+ static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::Error); \
+ cleanup(tryTempError); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ }
+
+// Chooses the final implementation macro for given argument count.
+// This could use MOZ_PASTE_PREFIX_AND_ARG_COUNT, but explicit named suffxes
+// read slightly better than plain numbers.
+// See also
+// https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros
+#define QM_TRY_META(...) \
+ { \
+ MOZ_ARG_6( \
+ , ##__VA_ARGS__, QM_TRY_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \
+ QM_TRY_CUSTOM_RET_VAL(__VA_ARGS__), QM_TRY_PROPAGATE_ERR(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \
+ }
+
+// Generates unique variable name. This extra internal macro (along with
+// __COUNTER__) allows nesting of the final macro.
+#define QM_TRY_GLUE(...) QM_TRY_META(MOZ_UNIQUE_VAR(tryResult), ##__VA_ARGS__)
+
+/**
+ * QM_TRY(expr[, customRetVal, cleanup]) is the C++ equivalent of Rust's
+ * `try!(expr);`. First, it evaluates expr, which must produce a Result value
+ * with empty ok_type. On Success, it does nothing else. On error, it calls
+ * HandleError and an additional cleanup function (if the third argument was
+ * passed) and finally returns an error Result from the enclosing function or a
+ * custom return value (if the second argument was passed).
+ */
+#define QM_TRY(...) QM_TRY_GLUE(__VA_ARGS__)
+
+// QM_TRY_ASSIGN_PROPAGATE_ERR, QM_TRY_ASSIGN_CUSTOM_RET_VAL,
+// QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_ASSIGN_GLUE macros are
+// implementation details of QM_TRY_UNWRAP/QM_TRY_INSPECT and shouldn't be used
+// directly.
+
+// Handles the four arguments case when the error is propagated.
+#define QM_TRY_ASSIGN_PROPAGATE_ERR(tryResult, accessFunction, target, expr) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \
+ return tryResult.propagateErr(); \
+ } \
+ MOZ_REMOVE_PAREN(target) = tryResult.accessFunction();
+
+// Handles the five arguments case when a custom return value needs to be
+// returned
+#define QM_TRY_ASSIGN_CUSTOM_RET_VAL(tryResult, accessFunction, target, expr, \
+ customRetVal) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::Error); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ } \
+ MOZ_REMOVE_PAREN(target) = tryResult.accessFunction();
+
+// Handles the six arguments case when a cleanup function needs to be called
+// before a custom return value is returned
+#define QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP( \
+ tryResult, accessFunction, target, expr, customRetVal, cleanup) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::Error); \
+ cleanup(tryTempError); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ } \
+ MOZ_REMOVE_PAREN(target) = tryResult.accessFunction();
+
+// Chooses the final implementation macro for given argument count.
+// See also the comment for QM_TRY_META.
+#define QM_TRY_ASSIGN_META(...) \
+ MOZ_ARG_8(, ##__VA_ARGS__, \
+ QM_TRY_ASSIGN_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \
+ QM_TRY_ASSIGN_CUSTOM_RET_VAL(__VA_ARGS__), \
+ QM_TRY_ASSIGN_PROPAGATE_ERR(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__))
+
+// Generates unique variable name. This extra internal macro (along with
+// __COUNTER__) allows nesting of the final macro.
+#define QM_TRY_ASSIGN_GLUE(accessFunction, ...) \
+ QM_TRY_ASSIGN_META(MOZ_UNIQUE_VAR(tryResult), accessFunction, ##__VA_ARGS__)
+
+/**
+ * QM_TRY_UNWRAP(target, expr[, customRetVal, cleanup]) is the C++ equivalent of
+ * Rust's `target = try!(expr);`. First, it evaluates expr, which must produce
+ * a Result value. On success, the result's success value is unwrapped and
+ * assigned to target. On error, it calls HandleError and an additional cleanup
+ * function (if the fourth argument was passed) and finally returns the error
+ * result or a custom return value (if the third argument was passed). |target|
+ * must be an lvalue.
+ */
+#define QM_TRY_UNWRAP(...) QM_TRY_ASSIGN_GLUE(unwrap, __VA_ARGS__)
+
+/**
+ * QM_TRY_INSPECT is similar to QM_TRY_UNWRAP, but it does not unwrap a success
+ * value, but inspects it and binds it to the target. It can therefore only be
+ * used when the target declares a const&. In general,
+ *
+ * QM_TRY_INSPECT(const auto &target, DoSomething())
+ *
+ * should be preferred over
+ *
+ * QM_TRY_UNWRAP(const auto target, DoSomething())
+ *
+ * as it avoids unnecessary moves/copies.
+ */
+#define QM_TRY_INSPECT(...) QM_TRY_ASSIGN_GLUE(inspect, __VA_ARGS__)
+
+// QM_TRY_RETURN_PROPAGATE_ERR, QM_TRY_RETURN_CUSTOM_RET_VAL,
+// QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP and QM_TRY_RETURN_GLUE macros are
+// implementation details of QM_TRY_RETURN and shouldn't be used directly.
+
+// Handles the two arguments case when the error is (also) propagated.
+// Note that this deliberately uses a single return statement without going
+// through unwrap/unwrapErr/propagateErr, so that this does not prevent NRVO or
+// tail call optimizations when possible.
+#define QM_TRY_RETURN_PROPAGATE_ERR(tryResult, expr) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \
+ } \
+ return tryResult;
+
+// Handles the three arguments case when a custom return value needs to be
+// returned
+#define QM_TRY_RETURN_CUSTOM_RET_VAL(tryResult, expr, customRetVal) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError MOZ_MAYBE_UNUSED = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryResult.inspectErr(), mozilla::dom::quota::Severity::Error); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ } \
+ return tryResult.unwrap();
+
+// Handles the four arguments case when a cleanup function needs to be called
+// before a custom return value is returned
+#define QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP(tryResult, expr, \
+ customRetVal, cleanup) \
+ auto tryResult = (expr); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::Error); \
+ cleanup(tryTempError); \
+ constexpr const auto& func MOZ_MAYBE_UNUSED = __func__; \
+ return QM_HANDLE_CUSTOM_RET_VAL(func, expr, tryTempError, customRetVal); \
+ } \
+ return tryResult.unwrap();
+
+// Chooses the final implementation macro for given argument count.
+// See also the comment for QM_TRY_META.
+#define QM_TRY_RETURN_META(...) \
+ { \
+ MOZ_ARG_6(, ##__VA_ARGS__, \
+ QM_TRY_RETURN_CUSTOM_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \
+ QM_TRY_RETURN_CUSTOM_RET_VAL(__VA_ARGS__), \
+ QM_TRY_RETURN_PROPAGATE_ERR(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \
+ }
+
+// Generates unique variable name. This extra internal macro (along with
+// __COUNTER__) allows nesting of the final macro.
+#define QM_TRY_RETURN_GLUE(...) \
+ QM_TRY_RETURN_META(MOZ_UNIQUE_VAR(tryResult), ##__VA_ARGS__)
+
+/**
+ * QM_TRY_RETURN(expr[, customRetVal, cleanup]) evaluates expr, which must
+ * produce a Result value. On success, the result's success value is returned.
+ * On error, it calls HandleError and an additional cleanup function (if the
+ * third argument was passed) and finally returns the error result or a custom
+ * return value (if the second argument was passed).
+ */
+#define QM_TRY_RETURN(...) QM_TRY_RETURN_GLUE(__VA_ARGS__)
+
+// QM_FAIL_RET_VAL and QM_FAIL_RET_VAL_WITH_CLEANUP macros are implementation
+// details of QM_FAIL and shouldn't be used directly.
+
+// Handles the one argument case when just an error is returned
+#define QM_FAIL_RET_VAL(retVal) \
+ mozilla::dom::quota::QM_HANDLE_ERROR(Failure, 0, \
+ mozilla::dom::quota::Severity::Error); \
+ return retVal;
+
+// Handles the two arguments case when a cleanup function needs to be called
+// before a return value is returned
+#define QM_FAIL_RET_VAL_WITH_CLEANUP(retVal, cleanup) \
+ mozilla::dom::quota::QM_HANDLE_ERROR(Failure, 0, \
+ mozilla::dom::quota::Severity::Error); \
+ cleanup(); \
+ return retVal;
+
+// Chooses the final implementation macro for given argument count.
+// See also the comment for QM_TRY_META.
+#define QM_FAIL_META(...) \
+ MOZ_ARG_4(, ##__VA_ARGS__, QM_FAIL_RET_VAL_WITH_CLEANUP(__VA_ARGS__), \
+ QM_FAIL_RET_VAL(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__))
+
+// This extra internal macro allows nesting of the final macro.
+#define QM_FAIL_GLUE(...) QM_FAIL_META(__VA_ARGS__)
+
+/**
+ * QM_FAIL(retVal[, cleanup]) calls HandleError and an additional cleanup
+ * function (if the second argument was passed) and returns a return value.
+ */
+#define QM_FAIL(...) QM_FAIL_GLUE(__VA_ARGS__)
+
+// QM_REPORTONLY_TRY, QM_REPORTONLY_TRY_WITH_CLEANUP, QM_REPORTONLY_TRY_GLUE
+// macros are implementation details of QM_WARNONLY_TRY/QM_INFOONLY_TRY and
+// shouldn't be used directly.
+
+// Handles the three arguments case when only a warning/info is reported.
+#define QM_REPORTONLY_TRY(tryResult, severity, expr) \
+ auto tryResult = (expr); \
+ static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryResult.unwrapErr(), mozilla::dom::quota::Severity::severity); \
+ }
+
+// Handles the four arguments case when a cleanup function needs to be called
+#define QM_REPORTONLY_TRY_WITH_CLEANUP(tryResult, severity, expr, cleanup) \
+ auto tryResult = (expr); \
+ static_assert(std::is_empty_v<typename decltype(tryResult)::ok_type>); \
+ if (MOZ_UNLIKELY(tryResult.isErr())) { \
+ auto tryTempError = tryResult.unwrapErr(); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ expr, tryTempError, mozilla::dom::quota::Severity::severity); \
+ cleanup(tryTempError); \
+ }
+
+// Chooses the final implementation macro for given argument count.
+// See also the comment for QM_TRY_META.
+#define QM_REPORTONLY_TRY_META(...) \
+ { \
+ MOZ_ARG_6(, ##__VA_ARGS__, QM_REPORTONLY_TRY_WITH_CLEANUP(__VA_ARGS__), \
+ QM_REPORTONLY_TRY(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__)) \
+ }
+
+// Generates unique variable name. This extra internal macro (along with
+// __COUNTER__) allows nesting of the final macro.
+#define QM_REPORTONLY_TRY_GLUE(severity, ...) \
+ QM_REPORTONLY_TRY_META(MOZ_UNIQUE_VAR(tryResult), severity, ##__VA_ARGS__)
+
+/**
+ * QM_WARNONLY_TRY(expr[, cleanup]) evaluates expr, which must produce a
+ * Result value with empty ok_type. On Success, it does nothing else. On error,
+ * it calls HandleError and an additional cleanup function (if the second
+ * argument was passed). This macro never returns and failures are always
+ * reported using a lower level of severity relative to failures reported by
+ * QM_TRY. The macro is intended for failures that should not be propagated.
+ */
+#define QM_WARNONLY_TRY(...) QM_REPORTONLY_TRY_GLUE(Warning, __VA_ARGS__)
+
+/**
+ * QM_INFOONLY_TRY is like QM_WARNONLY_TRY. The only difference is that
+ * failures are reported using a lower level of severity relative to failures
+ * reported by QM_WARNONLY_TRY.
+ */
+#define QM_INFOONLY_TRY(...) QM_REPORTONLY_TRY_GLUE(Info, __VA_ARGS__)
+
+// QM_REPORTONLY_TRY_ASSIGN, QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP,
+// QM_REPORTONLY_TRY_ASSIGN_GLUE macros are implementation details of
+// QM_WARNONLY_TRY_UNWRAP/QM_INFOONLY_TRY_UNWRAP and shouldn't be used
+// directly.
+
+// Handles the four arguments case when only a warning/info is reported.
+#define QM_REPORTONLY_TRY_ASSIGN(tryResult, severity, target, expr) \
+ auto tryResult = (expr); \
+ MOZ_REMOVE_PAREN(target) = \
+ MOZ_LIKELY(tryResult.isOk()) \
+ ? Some(tryResult.unwrap()) \
+ : mozilla::dom::quota::QM_HANDLE_ERROR_RETURN_NOTHING( \
+ expr, tryResult.unwrapErr(), \
+ mozilla::dom::quota::Severity::severity);
+
+// Handles the five arguments case when a cleanup function needs to be called
+#define QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP(tryResult, severity, target, \
+ expr, cleanup) \
+ auto tryResult = (expr); \
+ MOZ_REMOVE_PAREN(target) = \
+ MOZ_LIKELY(tryResult.isOk()) \
+ ? Some(tryResult.unwrap()) \
+ : mozilla::dom::quota::QM_HANDLE_ERROR_WITH_CLEANUP_RETURN_NOTHING( \
+ expr, tryResult.unwrapErr(), \
+ mozilla::dom::quota::Severity::severity, cleanup);
+
+// Chooses the final implementation macro for given argument count.
+// See also the comment for QM_TRY_META.
+#define QM_REPORTONLY_TRY_ASSIGN_META(...) \
+ MOZ_ARG_7(, ##__VA_ARGS__, \
+ QM_REPORTONLY_TRY_ASSIGN_WITH_CLEANUP(__VA_ARGS__), \
+ QM_REPORTONLY_TRY_ASSIGN(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__), \
+ QM_MISSING_ARGS(__VA_ARGS__), QM_MISSING_ARGS(__VA_ARGS__))
+
+// Generates unique variable name. This extra internal macro (along with
+// __COUNTER__) allows nesting of the final macro.
+#define QM_REPORTONLY_TRY_ASSIGN_GLUE(severity, ...) \
+ QM_REPORTONLY_TRY_ASSIGN_META(MOZ_UNIQUE_VAR(tryResult), severity, \
+ ##__VA_ARGS__)
+
+/**
+ * QM_WARNONLY_TRY_UNWRAP(target, expr[, cleanup]) evaluates expr, which must
+ * produce a Result value. On success, the result's success value is first
+ * unwrapped from the Result, then wrapped in a Maybe and finally assigned to
+ * target. On error, it calls HandleError and an additional cleanup
+ * function (if the third argument was passed) and finally assigns Nothing to
+ * target. |target| must be an lvalue. This macro never returns and failures
+ * are always reported using a lower level of severity relative to failures
+ * reported by QM_TRY_UNWRAP/QM_TRY_INSPECT. The macro is intended for failures
+ * that should not be propagated.
+ */
+#define QM_WARNONLY_TRY_UNWRAP(...) \
+ QM_REPORTONLY_TRY_ASSIGN_GLUE(Warning, __VA_ARGS__)
+
+// QM_WARNONLY_TRY_INSPECT doesn't make sense.
+
+/**
+ * QM_INFOONLY_TRY_UNWRAP is like QM_WARNONLY_TRY_UNWRAP. The only difference is
+ * that failures are reported using a lower level of severity relative to
+ * failures reported by QM_WARNONLY_TRY_UNWRAP.
+ */
+#define QM_INFOONLY_TRY_UNWRAP(...) \
+ QM_REPORTONLY_TRY_ASSIGN_GLUE(Info, __VA_ARGS__)
+
+// QM_INFOONLY_TRY_INSPECT doesn't make sense.
+
+// QM_OR_ELSE_REPORT macro is an implementation detail of
+// QM_OR_ELSE_WARN/QM_OR_ELSE_INFO/QM_OR_ELSE_LOG_VERBOSE and shouldn't be used
+// directly.
+
+#define QM_OR_ELSE_REPORT(severity, expr, fallback) \
+ (expr).orElse([&](const auto& firstRes) { \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ #expr, firstRes, mozilla::dom::quota::Severity::severity); \
+ return fallback(firstRes); \
+ })
+
+/*
+ * QM_OR_ELSE_WARN(expr, fallback) evaluates expr, which must produce a Result
+ * value. On Success, it just moves the success over. On error, it calls
+ * HandleError (with the Warning severity) and a fallback function (passed as
+ * the second argument) which produces a new result. Failed expr is always
+ * reported as a warning (the macro essentially wraps the fallback function
+ * with a warning). QM_OR_ELSE_WARN is a sub macro and is intended to be used
+ * along with one of the main macros such as QM_TRY.
+ */
+#define QM_OR_ELSE_WARN(...) QM_OR_ELSE_REPORT(Warning, __VA_ARGS__)
+
+/**
+ * QM_OR_ELSE_INFO is like QM_OR_ELSE_WARN. The only difference is that
+ * failures are reported using a lower level of severity relative to failures
+ * reported by QM_OR_ELSE_WARN.
+ */
+#define QM_OR_ELSE_INFO(...) QM_OR_ELSE_REPORT(Info, __VA_ARGS__)
+
+/**
+ * QM_OR_ELSE_LOG_VERBOSE is like QM_OR_ELSE_WARN. The only difference is that
+ * failures are reported using the lowest severity which is currently ignored
+ * in LogError, so nothing goes to the console, browser console and telemetry.
+ * Since nothing goes to the telemetry, the macro can't signal the end of the
+ * underlying error stack or change the type of the error stack in the
+ * telemetry. For that reason, the expression shouldn't contain nested QM_TRY
+ * macro uses.
+ */
+#define QM_OR_ELSE_LOG_VERBOSE(...) QM_OR_ELSE_REPORT(Log, __VA_ARGS__)
+
+namespace mozilla::dom::quota {
+
+// XXX Support orElseIf directly in mozilla::Result
+template <typename V, typename E, typename P, typename F>
+auto OrElseIf(Result<V, E>&& aResult, P&& aPred, F&& aFunc) -> Result<V, E> {
+ return MOZ_UNLIKELY(aResult.isErr())
+ ? (std::forward<P>(aPred)(aResult.inspectErr()))
+ ? std::forward<F>(aFunc)(aResult.unwrapErr())
+ : aResult.propagateErr()
+ : aResult.unwrap();
+}
+
+} // namespace mozilla::dom::quota
+
+// QM_OR_ELSE_REPORT_IF macro is an implementation detail of
+// QM_OR_ELSE_WARN_IF/QM_OR_ELSE_INFO_IF/QM_OR_ELSE_LOG_VERBOSE_IF and
+// shouldn't be used directly.
+
+#define QM_OR_ELSE_REPORT_IF(severity, expr, predicate, fallback) \
+ mozilla::dom::quota::OrElseIf( \
+ (expr), \
+ [&](const auto& firstRes) { \
+ bool res = predicate(firstRes); \
+ mozilla::dom::quota::QM_HANDLE_ERROR( \
+ #expr, firstRes, \
+ res ? mozilla::dom::quota::Severity::severity \
+ : mozilla::dom::quota::Severity::Error); \
+ return res; \
+ }, \
+ fallback)
+
+/*
+ * QM_OR_ELSE_WARN_IF(expr, predicate, fallback) evaluates expr first, which
+ * must produce a Result value. On Success, it just moves the success over.
+ * On error, it calls a predicate function (passed as the second argument) and
+ * then it either calls HandleError (with the Warning severity) and a fallback
+ * function (passed as the third argument) which produces a new result if the
+ * predicate returned true. Or it calls HandleError (with the Error severity)
+ * and propagates the error result if the predicate returned false. So failed
+ * expr can be reported as a warning or as an error depending on the predicate.
+ * QM_OR_ELSE_WARN_IF is a sub macro and is intended to be used along with one
+ * of the main macros such as QM_TRY.
+ */
+#define QM_OR_ELSE_WARN_IF(...) QM_OR_ELSE_REPORT_IF(Warning, __VA_ARGS__)
+
+/**
+ * QM_OR_ELSE_INFO_IF is like QM_OR_ELSE_WARN_IF. The only difference is that
+ * failures are reported using a lower level of severity relative to failures
+ * reported by QM_OR_ELSE_WARN_IF.
+ */
+#define QM_OR_ELSE_INFO_IF(...) QM_OR_ELSE_REPORT_IF(Info, __VA_ARGS__)
+
+/**
+ * QM_OR_ELSE_LOG_VERBOSE_IF is like QM_OR_ELSE_WARN_IF. The only difference is
+ * that failures are reported using the lowest severity which is currently
+ * ignored in LogError, so nothing goes to the console, browser console and
+ * telemetry. Since nothing goes to the telemetry, the macro can't signal the
+ * end of the underlying error stack or change the type of the error stack in
+ * the telemetry. For that reason, the expression shouldn't contain nested
+ * QM_TRY macro uses.
+ */
+#define QM_OR_ELSE_LOG_VERBOSE_IF(...) \
+ QM_OR_ELSE_REPORT_IF(Verbose, __VA_ARGS__)
+
+// Telemetry probes to collect number of failure during the initialization.
+#ifdef NIGHTLY_BUILD
+# define RECORD_IN_NIGHTLY(_recorder, _status) \
+ do { \
+ if (NS_SUCCEEDED(_recorder)) { \
+ _recorder = _status; \
+ } \
+ } while (0)
+
+# define OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS \
+ Ok {}
+
+# define RETURN_STATUS_OR_RESULT(_status, _rv) \
+ return Err(NS_FAILED(_status) ? (_status) : (_rv))
+#else
+# define RECORD_IN_NIGHTLY(_dummy, _status) \
+ {}
+
+# define OK_IN_NIGHTLY_PROPAGATE_IN_OTHERS QM_PROPAGATE
+
+# define RETURN_STATUS_OR_RESULT(_status, _rv) return Err(_rv)
+#endif
+
+class mozIStorageConnection;
+class mozIStorageStatement;
+class nsIFile;
+
+namespace mozilla {
+
+class LogModule;
+
+struct CreateIfNonExistent {};
+
+struct NotOk {};
+
+// Allow MOZ_TRY/QM_TRY to handle `bool` values by wrapping them with OkIf.
+// TODO: Maybe move this to mfbt/ResultExtensions.h
+inline Result<Ok, NotOk> OkIf(bool aValue) {
+ if (aValue) {
+ return Ok();
+ }
+ return Err(NotOk());
+}
+
+// TODO: Maybe move this to mfbt/ResultExtensions.h
+template <auto SuccessValue>
+auto OkToOk(Ok) -> Result<decltype(SuccessValue), nsresult> {
+ return SuccessValue;
+}
+
+template <nsresult ErrorValue, auto SuccessValue,
+ typename V = decltype(SuccessValue)>
+auto ErrToOkOrErr(nsresult aValue) -> Result<V, nsresult> {
+ if (aValue == ErrorValue) {
+ return V{SuccessValue};
+ }
+ return Err(aValue);
+}
+
+template <nsresult ErrorValue, typename V = mozilla::Ok>
+auto ErrToDefaultOkOrErr(nsresult aValue) -> Result<V, nsresult> {
+ if (aValue == ErrorValue) {
+ return V{};
+ }
+ return Err(aValue);
+}
+
+// Helper template function so that QM_TRY predicates checking for a specific
+// error can be concisely written as IsSpecificError<NS_SOME_ERROR> instead of
+// as a more verbose lambda.
+template <nsresult ErrorValue>
+bool IsSpecificError(const nsresult aValue) {
+ return aValue == ErrorValue;
+}
+
+// Helper template function so that QM_TRY fallback functions that are
+// converting errors into specific in-band success values can be concisely
+// written as ErrToOk<SuccessValueToReturn> (with the return type inferred).
+// For example, many file-related APIs that access information about a file may
+// return an nsresult error code if the file does not exist. From an
+// application perspective, the file not existing is not actually exceptional
+// and can instead be handled by the success case.
+template <auto SuccessValue, typename V = decltype(SuccessValue)>
+auto ErrToOk(const nsresult aValue) -> Result<V, nsresult> {
+ return V{SuccessValue};
+}
+
+// Helper template function so that QM_TRY fallback functions that are
+// suppressing errors by converting them into (generic) success can be
+// concisely written as ErrToDefaultOk<>.
+template <typename V = mozilla::Ok>
+auto ErrToDefaultOk(const nsresult aValue) -> Result<V, nsresult> {
+ return V{};
+}
+
+template <typename MozPromiseType, typename RejectValueT = nsresult>
+auto CreateAndRejectMozPromise(const char* aFunc, const RejectValueT& aRv)
+ -> decltype(auto) {
+ if constexpr (std::is_same_v<RejectValueT, nsresult>) {
+ return MozPromiseType::CreateAndReject(aRv, aFunc);
+ } else if constexpr (std::is_same_v<RejectValueT, QMResult>) {
+ return MozPromiseType::CreateAndReject(aRv.NSResult(), aFunc);
+ }
+}
+
+RefPtr<BoolPromise> CreateAndRejectBoolPromise(const char* aFunc, nsresult aRv);
+
+RefPtr<Int64Promise> CreateAndRejectInt64Promise(const char* aFunc,
+ nsresult aRv);
+
+RefPtr<BoolPromise> CreateAndRejectBoolPromiseFromQMResult(const char* aFunc,
+ const QMResult& aRv);
+
+// Like Rust's collect with a step function, not a generic iterator/range.
+//
+// Cond must be a function type with a return type to Result<V, E>, where
+// V is convertible to bool
+// - success converts to true indicates that collection shall continue
+// - success converts to false indicates that collection is completed
+// - error indicates that collection shall stop, propagating the error
+//
+// Body must a function type accepting a V xvalue with a return type convertible
+// to Result<empty, E>.
+template <typename Step, typename Body>
+auto CollectEach(Step aStep, const Body& aBody)
+ -> Result<mozilla::Ok, typename std::invoke_result_t<Step>::err_type> {
+ using StepResultType = typename std::invoke_result_t<Step>::ok_type;
+
+ static_assert(
+ std::is_empty_v<
+ typename std::invoke_result_t<Body, StepResultType&&>::ok_type>);
+
+ while (true) {
+ StepResultType element;
+ MOZ_TRY_VAR(element, aStep());
+
+ if (!static_cast<bool>(element)) {
+ break;
+ }
+
+ MOZ_TRY(aBody(std::move(element)));
+ }
+
+ return mozilla::Ok{};
+}
+
+// This is like std::reduce with a to-be-defined execution policy (we don't want
+// to std::terminate on an error, but probably it's fine to just propagate any
+// error that occurred), operating not on a pair of iterators but rather a
+// generator function.
+template <typename InputGenerator, typename T, typename BinaryOp>
+auto ReduceEach(InputGenerator aInputGenerator, T aInit,
+ const BinaryOp& aBinaryOp)
+ -> Result<T, typename std::invoke_result_t<InputGenerator>::err_type> {
+ T res = std::move(aInit);
+
+ // XXX This can be done in parallel!
+ MOZ_TRY(CollectEach(
+ std::move(aInputGenerator),
+ [&res, &aBinaryOp](const auto& element)
+ -> Result<Ok,
+ typename std::invoke_result_t<InputGenerator>::err_type> {
+ MOZ_TRY_VAR(res, aBinaryOp(std::move(res), element));
+
+ return Ok{};
+ }));
+
+ return std::move(res);
+}
+
+// This is like std::reduce with a to-be-defined execution policy (we don't want
+// to std::terminate on an error, but probably it's fine to just propagate any
+// error that occurred).
+template <typename Range, typename T, typename BinaryOp>
+auto Reduce(Range&& aRange, T aInit, const BinaryOp& aBinaryOp) {
+ using std::begin;
+ using std::end;
+ return ReduceEach(
+ [it = begin(aRange), end = end(aRange)]() mutable {
+ auto res = ToMaybeRef(it != end ? &*it++ : nullptr);
+ return Result<decltype(res), typename std::invoke_result_t<
+ BinaryOp, T, decltype(res)>::err_type>(
+ res);
+ },
+ aInit, aBinaryOp);
+}
+
+template <typename Range, typename Body>
+auto CollectEachInRange(Range&& aRange, const Body& aBody)
+ -> Result<mozilla::Ok, nsresult> {
+ for (auto&& element : aRange) {
+ MOZ_TRY(aBody(element));
+ }
+
+ return mozilla::Ok{};
+}
+
+// Like Rust's collect with a while loop, not a generic iterator/range.
+//
+// Cond must be a function type accepting no parameters with a return type
+// convertible to Result<bool, E>, where
+// - success true indicates that collection shall continue
+// - success false indicates that collection is completed
+// - error indicates that collection shall stop, propagating the error
+//
+// Body must a function type accepting no parameters with a return type
+// convertible to Result<empty, E>.
+template <typename Cond, typename Body>
+auto CollectWhile(const Cond& aCond, const Body& aBody)
+ -> Result<mozilla::Ok, typename std::invoke_result_t<Cond>::err_type> {
+ return CollectEach(aCond, [&aBody](bool) { return aBody(); });
+}
+
+template <>
+class [[nodiscard]] GenericErrorResult<mozilla::ipc::IPCResult> {
+ mozilla::ipc::IPCResult mErrorValue;
+
+ template <typename V, typename E2>
+ friend class Result;
+
+ public:
+ explicit GenericErrorResult(mozilla::ipc::IPCResult aErrorValue)
+ : mErrorValue(aErrorValue) {
+ MOZ_ASSERT(!aErrorValue);
+ }
+
+ GenericErrorResult(mozilla::ipc::IPCResult aErrorValue,
+ const ErrorPropagationTag&)
+ : GenericErrorResult(aErrorValue) {}
+
+ operator mozilla::ipc::IPCResult() const { return mErrorValue; }
+};
+
+namespace dom::quota {
+
+extern const char kQuotaGenericDelimiter;
+
+// Telemetry keys to indicate types of errors.
+#ifdef NIGHTLY_BUILD
+extern const nsLiteralCString kQuotaInternalError;
+extern const nsLiteralCString kQuotaExternalError;
+#else
+// No need for these when we're not collecting telemetry.
+# define kQuotaInternalError
+# define kQuotaExternalError
+#endif
+
+class BackgroundThreadObject {
+ protected:
+ nsCOMPtr<nsISerialEventTarget> mOwningThread;
+
+ public:
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ nsISerialEventTarget* OwningThread() const;
+
+ protected:
+ BackgroundThreadObject();
+
+ explicit BackgroundThreadObject(nsISerialEventTarget* aOwningThread);
+};
+
+MOZ_COLD void ReportInternalError(const char* aFile, uint32_t aLine,
+ const char* aStr);
+
+LogModule* GetQuotaManagerLogger();
+
+void AnonymizeCString(nsACString& aCString);
+
+inline auto AnonymizedCString(const nsACString& aCString) {
+ nsAutoCString result{aCString};
+ AnonymizeCString(result);
+ return result;
+}
+
+void AnonymizeOriginString(nsACString& aOriginString);
+
+inline auto AnonymizedOriginString(const nsACString& aOriginString) {
+ nsAutoCString result{aOriginString};
+ AnonymizeOriginString(result);
+ return result;
+}
+
+#ifdef XP_WIN
+void CacheUseDOSDevicePathSyntaxPrefValue();
+#endif
+
+Result<nsCOMPtr<nsIFile>, nsresult> QM_NewLocalFile(const nsAString& aPath);
+
+nsDependentCSubstring GetLeafName(const nsACString& aPath);
+
+Result<nsCOMPtr<nsIFile>, nsresult> CloneFileAndAppend(
+ nsIFile& aDirectory, const nsAString& aPathElement);
+
+enum class nsIFileKind {
+ ExistsAsDirectory,
+ ExistsAsFile,
+ DoesNotExist,
+};
+
+// XXX We can use this outside of QM and its clients as well, probably. Maybe it
+// could be moved to xpcom/io?
+Result<nsIFileKind, nsresult> GetDirEntryKind(nsIFile& aFile);
+
+Result<nsCOMPtr<mozIStorageStatement>, nsresult> CreateStatement(
+ mozIStorageConnection& aConnection, const nsACString& aStatementString);
+
+enum class SingleStepResult { AssertHasResult, ReturnNullIfNoResult };
+
+template <SingleStepResult ResultHandling>
+using SingleStepSuccessType =
+ std::conditional_t<ResultHandling == SingleStepResult::AssertHasResult,
+ NotNull<nsCOMPtr<mozIStorageStatement>>,
+ nsCOMPtr<mozIStorageStatement>>;
+
+template <SingleStepResult ResultHandling>
+Result<SingleStepSuccessType<ResultHandling>, nsresult> ExecuteSingleStep(
+ nsCOMPtr<mozIStorageStatement>&& aStatement);
+
+// Creates a statement with the specified aStatementString, executes a single
+// step, and returns the statement.
+// Depending on the value ResultHandling,
+// - it is asserted that there is a result (default resp.
+// SingleStepResult::AssertHasResult), and the success type is
+// MovingNotNull<nsCOMPtr<mozIStorageStatement>>
+// - it is asserted that there is no result, and the success type is Ok
+// - in case there is no result, nullptr is returned, and the success type is
+// nsCOMPtr<mozIStorageStatement>
+// Any other errors are always propagated.
+template <SingleStepResult ResultHandling = SingleStepResult::AssertHasResult>
+Result<SingleStepSuccessType<ResultHandling>, nsresult>
+CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection,
+ const nsACString& aStatementString);
+
+namespace detail {
+
+// Determine the absolute path of the root of our built source tree so we can
+// derive source-relative paths for non-exported header files in
+// MakeSourceFileRelativePath. Exported header files end up in the objdir and
+// we have GetObjdirDistIncludeTreeBase for that.
+nsDependentCSubstring GetSourceTreeBase();
+
+// Determine the absolute path of the root of our built OBJDIR/dist/include
+// directory. The aQuotaCommonHPath argument cleverly defaults to __FILE__
+// initialized in our exported header; no argument should ever be provided to
+// this method. GetSourceTreeBase handles identifying the root of the source
+// tree.
+nsDependentCSubstring GetObjdirDistIncludeTreeBase(
+ const nsLiteralCString& aQuotaCommonHPath = nsLiteralCString(__FILE__));
+
+nsDependentCSubstring MakeSourceFileRelativePath(
+ const nsACString& aSourceFilePath);
+
+} // namespace detail
+
+enum class Severity {
+ Error,
+ Warning,
+ Info,
+ Verbose,
+};
+
+#ifdef QM_LOG_ERROR_ENABLED
+# ifdef QM_ERROR_STACKS_ENABLED
+using ResultType = Variant<QMResult, nsresult, Nothing>;
+
+void LogError(const nsACString& aExpr, const ResultType& aResult,
+ const nsACString& aSourceFilePath, int32_t aSourceFileLine,
+ Severity aSeverity)
+# else
+void LogError(const nsACString& aExpr, Maybe<nsresult> aMaybeRv,
+ const nsACString& aSourceFilePath, int32_t aSourceFileLine,
+ Severity aSeverity)
+# endif
+ ;
+#endif
+
+#ifdef DEBUG
+Result<bool, nsresult> WarnIfFileIsUnknown(nsIFile& aFile,
+ const char* aSourceFilePath,
+ int32_t aSourceFileLine);
+#endif
+
+// As HandleError is a function that will only be called in error cases, it is
+// marked with MOZ_COLD to avoid bloating the code of calling functions, if it's
+// not empty.
+//
+// For the same reason, the string-ish parameters are of type const char* rather
+// than any ns*String type, to minimize the code at each call site. This
+// deliberately de-optimizes runtime performance, which is uncritical during
+// error handling.
+//
+// This functions are not intended to be called
+// directly, they should only be called from the QM_* macros.
+#ifdef QM_LOG_ERROR_ENABLED
+template <typename T>
+MOZ_COLD MOZ_NEVER_INLINE void HandleError(const char* aExpr, const T& aRv,
+ const char* aSourceFilePath,
+ int32_t aSourceFileLine,
+ const Severity aSeverity) {
+# ifdef QM_ERROR_STACKS_ENABLED
+ if constexpr (std::is_same_v<T, QMResult> || std::is_same_v<T, nsresult>) {
+ mozilla::dom::quota::LogError(nsDependentCString(aExpr), ResultType(aRv),
+ nsDependentCString(aSourceFilePath),
+ aSourceFileLine, aSeverity);
+ } else {
+ mozilla::dom::quota::LogError(
+ nsDependentCString(aExpr), ResultType(Nothing{}),
+ nsDependentCString(aSourceFilePath), aSourceFileLine, aSeverity);
+ }
+# else
+ if constexpr (std::is_same_v<T, nsresult>) {
+ mozilla::dom::quota::LogError(nsDependentCString(aExpr), Some(aRv),
+ nsDependentCString(aSourceFilePath),
+ aSourceFileLine, aSeverity);
+ } else {
+ mozilla::dom::quota::LogError(nsDependentCString(aExpr), Nothing{},
+ nsDependentCString(aSourceFilePath),
+ aSourceFileLine, aSeverity);
+ }
+# endif
+}
+#else
+template <typename T>
+MOZ_ALWAYS_INLINE constexpr void HandleError(const char* aExpr, const T& aRv,
+ const char* aSourceFilePath,
+ int32_t aSourceFileLine,
+ const Severity aSeverity) {}
+#endif
+
+template <typename T>
+Nothing HandleErrorReturnNothing(const char* aExpr, const T& aRv,
+ const char* aSourceFilePath,
+ int32_t aSourceFileLine,
+ const Severity aSeverity) {
+ HandleError(aExpr, aRv, aSourceFilePath, aSourceFileLine, aSeverity);
+ return Nothing();
+}
+
+template <typename T, typename CleanupFunc>
+Nothing HandleErrorWithCleanupReturnNothing(const char* aExpr, const T& aRv,
+ const char* aSourceFilePath,
+ int32_t aSourceFileLine,
+ const Severity aSeverity,
+ CleanupFunc&& aCleanupFunc) {
+ HandleError(aExpr, aRv, aSourceFilePath, aSourceFileLine, aSeverity);
+ std::forward<CleanupFunc>(aCleanupFunc)(aRv);
+ return Nothing();
+}
+
+template <typename T, typename CustomRetVal>
+auto HandleCustomRetVal(const char* aFunc, const char* aExpr, const T& aRv,
+ CustomRetVal&& aCustomRetVal) {
+ if constexpr (std::is_invocable<CustomRetVal, const char*,
+ const char*>::value) {
+ return aCustomRetVal(aFunc, aExpr);
+ } else if constexpr (std::is_invocable<CustomRetVal, const char*,
+ const T&>::value) {
+ return aCustomRetVal(aFunc, aRv);
+ } else if constexpr (std::is_invocable<CustomRetVal, const T&>::value) {
+ return aCustomRetVal(aRv);
+ } else {
+ return std::forward<CustomRetVal>(aCustomRetVal);
+ }
+}
+
+template <SingleStepResult ResultHandling = SingleStepResult::AssertHasResult,
+ typename BindFunctor>
+Result<SingleStepSuccessType<ResultHandling>, nsresult>
+CreateAndExecuteSingleStepStatement(mozIStorageConnection& aConnection,
+ const nsACString& aStatementString,
+ BindFunctor aBindFunctor) {
+ QM_TRY_UNWRAP(auto stmt, CreateStatement(aConnection, aStatementString));
+
+ QM_TRY(aBindFunctor(*stmt));
+
+ return ExecuteSingleStep<ResultHandling>(std::move(stmt));
+}
+
+template <typename StepFunc>
+Result<Ok, nsresult> CollectWhileHasResult(mozIStorageStatement& aStmt,
+ StepFunc&& aStepFunc) {
+ return CollectWhile(
+ [&aStmt] {
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER(aStmt, ExecuteStep));
+ },
+ [&aStmt, &aStepFunc] { return aStepFunc(aStmt); });
+}
+
+template <typename StepFunc,
+ typename ArrayType = nsTArray<typename std::invoke_result_t<
+ StepFunc, mozIStorageStatement&>::ok_type>>
+auto CollectElementsWhileHasResult(mozIStorageStatement& aStmt,
+ StepFunc&& aStepFunc)
+ -> Result<ArrayType, nsresult> {
+ ArrayType res;
+
+ QM_TRY(CollectWhileHasResult(
+ aStmt, [&aStepFunc, &res](auto& stmt) -> Result<Ok, nsresult> {
+ QM_TRY_UNWRAP(auto element, aStepFunc(stmt));
+ res.AppendElement(std::move(element));
+ return Ok{};
+ }));
+
+ return std::move(res);
+}
+
+template <typename ArrayType, typename StepFunc>
+auto CollectElementsWhileHasResultTyped(mozIStorageStatement& aStmt,
+ StepFunc&& aStepFunc) {
+ return CollectElementsWhileHasResult<StepFunc, ArrayType>(
+ aStmt, std::forward<StepFunc>(aStepFunc));
+}
+
+namespace detail {
+template <typename Cancel, typename Body>
+Result<mozilla::Ok, nsresult> CollectEachFile(nsIFile& aDirectory,
+ const Cancel& aCancel,
+ const Body& aBody) {
+ QM_TRY_INSPECT(const auto& entries, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIDirectoryEnumerator>,
+ aDirectory, GetDirectoryEntries));
+
+ return CollectEach(
+ [&entries, &aCancel]() -> Result<nsCOMPtr<nsIFile>, nsresult> {
+ if (aCancel()) {
+ return nsCOMPtr<nsIFile>{};
+ }
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>,
+ entries, GetNextFile));
+ },
+ aBody);
+}
+} // namespace detail
+
+template <typename Body>
+Result<mozilla::Ok, nsresult> CollectEachFile(nsIFile& aDirectory,
+ const Body& aBody) {
+ return detail::CollectEachFile(
+ aDirectory, [] { return false; }, aBody);
+}
+
+template <typename Body>
+Result<mozilla::Ok, nsresult> CollectEachFileAtomicCancelable(
+ nsIFile& aDirectory, const Atomic<bool>& aCanceled, const Body& aBody) {
+ return detail::CollectEachFile(
+ aDirectory, [&aCanceled] { return static_cast<bool>(aCanceled); }, aBody);
+}
+
+template <typename T, typename Body>
+auto ReduceEachFileAtomicCancelable(nsIFile& aDirectory,
+ const Atomic<bool>& aCanceled, T aInit,
+ const Body& aBody) -> Result<T, nsresult> {
+ QM_TRY_INSPECT(const auto& entries, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<nsIDirectoryEnumerator>,
+ aDirectory, GetDirectoryEntries));
+
+ return ReduceEach(
+ [&entries, &aCanceled]() -> Result<nsCOMPtr<nsIFile>, nsresult> {
+ if (aCanceled) {
+ return nsCOMPtr<nsIFile>{};
+ }
+
+ QM_TRY_RETURN(MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsCOMPtr<nsIFile>,
+ entries, GetNextFile));
+ },
+ std::move(aInit), aBody);
+}
+
+constexpr bool IsDatabaseCorruptionError(const nsresult aRv) {
+ return aRv == NS_ERROR_FILE_CORRUPTED || aRv == NS_ERROR_STORAGE_IOERR;
+}
+
+template <typename Func>
+auto CallWithDelayedRetriesIfAccessDenied(Func&& aFunc, uint32_t aMaxRetries,
+ uint32_t aDelayMs)
+ -> Result<typename std::invoke_result_t<Func>::ok_type, nsresult> {
+ uint32_t retries = 0;
+
+ while (true) {
+ auto result = std::forward<Func>(aFunc)();
+
+ if (result.isOk()) {
+ return result;
+ }
+
+ if (result.inspectErr() != NS_ERROR_FILE_IS_LOCKED &&
+ result.inspectErr() != NS_ERROR_FILE_ACCESS_DENIED) {
+ return result;
+ }
+
+ if (retries++ >= aMaxRetries) {
+ return result;
+ }
+
+ PR_Sleep(PR_MillisecondsToInterval(aDelayMs));
+ }
+}
+
+namespace detail {
+
+template <bool flag = false>
+void UnsupportedReturnType() {
+ static_assert(flag, "Unsupported return type!");
+}
+
+} // namespace detail
+
+template <typename Initialization, typename StringGenerator, typename Func>
+auto ExecuteInitialization(
+ FirstInitializationAttempts<Initialization, StringGenerator>&
+ aFirstInitializationAttempts,
+ const Initialization aInitialization, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&> {
+ return aFirstInitializationAttempts.WithFirstInitializationAttempt(
+ aInitialization, [&aFunc](auto&& firstInitializationAttempt) {
+ auto res = std::forward<Func>(aFunc)(firstInitializationAttempt);
+
+ const auto rv = [&res]() -> nsresult {
+ using RetType =
+ std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&>;
+
+ if constexpr (std::is_same_v<RetType, nsresult>) {
+ return res;
+ } else if constexpr (mozilla::detail::IsResult<RetType>::value &&
+ std::is_same_v<typename RetType::err_type,
+ nsresult>) {
+ return res.isOk() ? NS_OK : res.inspectErr();
+ } else {
+ detail::UnsupportedReturnType();
+ }
+ }();
+
+ // NS_ERROR_ABORT signals a non-fatal, recoverable problem during
+ // initialization. We do not want these kind of failures to count
+ // against our overall first initialization attempt telemetry. Thus we
+ // just ignore this kind of failure and keep
+ // aFirstInitializationAttempts unflagged to stay ready to record a real
+ // success or failure on the next attempt.
+ if (rv == NS_ERROR_ABORT) {
+ return res;
+ }
+
+ if (!firstInitializationAttempt.Recorded()) {
+ firstInitializationAttempt.Record(rv);
+ }
+
+ return res;
+ });
+}
+
+template <typename Initialization, typename StringGenerator, typename Func>
+auto ExecuteInitialization(
+ FirstInitializationAttempts<Initialization, StringGenerator>&
+ aFirstInitializationAttempts,
+ const Initialization aInitialization, const nsACString& aContext,
+ Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&> {
+ return ExecuteInitialization(
+ aFirstInitializationAttempts, aInitialization,
+ [&](const auto& firstInitializationAttempt) -> decltype(auto) {
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto maybeScopedLogExtraInfo =
+ firstInitializationAttempt.Recorded()
+ ? Nothing{}
+ : Some(ScopedLogExtraInfo{ScopedLogExtraInfo::kTagContext,
+ aContext});
+#endif
+
+ return std::forward<Func>(aFunc)(firstInitializationAttempt);
+ });
+}
+
+} // namespace dom::quota
+} // namespace mozilla
+
+#endif // mozilla_dom_quota_quotacommon_h__
diff --git a/dom/quota/QuotaManager.h b/dom/quota/QuotaManager.h
new file mode 100644
index 0000000000..e18454909f
--- /dev/null
+++ b/dom/quota/QuotaManager.h
@@ -0,0 +1,676 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_quotamanager_h__
+#define mozilla_dom_quota_quotamanager_h__
+
+#include <cstdint>
+#include <utility>
+#include "Client.h"
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/InitializedOnce.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/dom/quota/Assertions.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/InitializationTypes.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "nsDebug.h"
+#include "nsHashKeys.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsTStringRepr.h"
+#include "nscore.h"
+#include "prenv.h"
+
+#define QUOTA_MANAGER_CONTRACTID "@mozilla.org/dom/quota/manager;1"
+
+class mozIStorageConnection;
+class nsIEventTarget;
+class nsIFile;
+class nsIPrincipal;
+class nsIRunnable;
+class nsIThread;
+class nsITimer;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+class OriginAttributes;
+
+namespace ipc {
+
+class PrincipalInfo;
+
+} // namespace ipc
+
+} // namespace mozilla
+
+namespace mozilla::dom::quota {
+
+class CanonicalQuotaObject;
+class ClientUsageArray;
+class ClientDirectoryLock;
+class DirectoryLockImpl;
+class GroupInfo;
+class GroupInfoPair;
+class OriginDirectoryLock;
+class OriginInfo;
+class OriginScope;
+class QuotaObject;
+class UniversalDirectoryLock;
+
+class QuotaManager final : public BackgroundThreadObject {
+ friend class CanonicalQuotaObject;
+ friend class DirectoryLockImpl;
+ friend class GroupInfo;
+ friend class OriginInfo;
+
+ using PrincipalInfo = mozilla::ipc::PrincipalInfo;
+ using DirectoryLockTable =
+ nsClassHashtable<nsCStringHashKey, nsTArray<NotNull<DirectoryLockImpl*>>>;
+
+ class Observer;
+
+ public:
+ QuotaManager(const nsAString& aBasePath, const nsAString& aStorageName);
+
+ NS_INLINE_DECL_REFCOUNTING(QuotaManager)
+
+ static nsresult Initialize();
+
+ static bool IsRunningXPCShellTests() {
+ static bool kRunningXPCShellTests =
+ !!PR_GetEnv("XPCSHELL_TEST_PROFILE_DIR");
+ return kRunningXPCShellTests;
+ }
+
+ static bool IsRunningGTests() {
+ static bool kRunningGTests = !!PR_GetEnv("MOZ_RUN_GTEST");
+ return kRunningGTests;
+ }
+
+ static const char kReplaceChars[];
+ static const char16_t kReplaceChars16[];
+
+ static Result<MovingNotNull<RefPtr<QuotaManager>>, nsresult> GetOrCreate();
+
+ static Result<Ok, nsresult> EnsureCreated();
+
+ // Returns a non-owning reference.
+ static QuotaManager* Get();
+
+ // Use only in gtests!
+ static nsIObserver* GetObserver();
+
+ // Returns true if we've begun the shutdown process.
+ static bool IsShuttingDown();
+
+ static void ShutdownInstance();
+
+ // Use only in gtests!
+ static void Reset();
+
+ static bool IsOSMetadata(const nsAString& aFileName);
+
+ static bool IsDotFile(const nsAString& aFileName);
+
+ bool IsOriginInitialized(const nsACString& aOrigin) const {
+ AssertIsOnIOThread();
+
+ return mInitializedOrigins.Contains(aOrigin);
+ }
+
+ bool IsTemporaryStorageInitialized() const {
+ AssertIsOnIOThread();
+
+ return mTemporaryStorageInitialized;
+ }
+
+ /**
+ * For initialization of an origin where the directory already exists. This is
+ * used by EnsureTemporaryStorageIsInitialized/InitializeRepository once it
+ * has tallied origin usage by calling each of the QuotaClient InitOrigin
+ * methods.
+ */
+ void InitQuotaForOrigin(const FullOriginMetadata& aFullOriginMetadata,
+ const ClientUsageArray& aClientUsages,
+ uint64_t aUsageBytes);
+
+ /**
+ * For use in special-cases like LSNG where we need to be able to know that
+ * there is no data stored for an origin. LSNG knows that there is 0 usage for
+ * its storage of an origin and wants to make sure there is a QuotaObject
+ * tracking this. This method will create a non-persisted, 0-usage,
+ * mDirectoryExists=false OriginInfo if there isn't already an OriginInfo. If
+ * an OriginInfo already exists, it will be left as-is, because that implies a
+ * different client has usages for the origin (and there's no need to add
+ * LSNG's 0 usage to the QuotaObject).
+ */
+ void EnsureQuotaForOrigin(const OriginMetadata& aOriginMetadata);
+
+ /**
+ * For use when creating an origin directory. It's possible that origin usage
+ * is already being tracked due to a call to EnsureQuotaForOrigin, and in that
+ * case we need to update the existing OriginInfo rather than create a new
+ * one.
+ *
+ * @return last access time of the origin.
+ */
+ int64_t NoteOriginDirectoryCreated(const OriginMetadata& aOriginMetadata,
+ bool aPersisted);
+
+ // XXX clients can use QuotaObject instead of calling this method directly.
+ void DecreaseUsageForClient(const ClientMetadata& aClientMetadata,
+ int64_t aSize);
+
+ void ResetUsageForClient(const ClientMetadata& aClientMetadata);
+
+ UsageInfo GetUsageForClient(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType);
+
+ void UpdateOriginAccessTime(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata);
+
+ void RemoveQuota();
+
+ void RemoveQuotaForOrigin(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata) {
+ MutexAutoLock lock(mQuotaMutex);
+ LockedRemoveQuotaForOrigin(aOriginMetadata);
+ }
+
+ nsresult LoadQuota();
+
+ void UnloadQuota();
+
+ already_AddRefed<QuotaObject> GetQuotaObject(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, nsIFile* aFile, int64_t aFileSize = -1,
+ int64_t* aFileSizeOut = nullptr);
+
+ already_AddRefed<QuotaObject> GetQuotaObject(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, const nsAString& aPath, int64_t aFileSize = -1,
+ int64_t* aFileSizeOut = nullptr);
+
+ already_AddRefed<QuotaObject> GetQuotaObject(const int64_t aDirectoryLockId,
+ const nsAString& aPath);
+
+ Nullable<bool> OriginPersisted(const OriginMetadata& aOriginMetadata);
+
+ void PersistOrigin(const OriginMetadata& aOriginMetadata);
+
+ using DirectoryLockIdTableArray =
+ AutoTArray<Client::DirectoryLockIdTable, Client::TYPE_MAX>;
+ void AbortOperationsForLocks(const DirectoryLockIdTableArray& aLockIds);
+
+ // Called when a process is being shot down. Aborts any running operations
+ // for the given process.
+ void AbortOperationsForProcess(ContentParentId aContentParentId);
+
+ Result<nsCOMPtr<nsIFile>, nsresult> GetDirectoryForOrigin(
+ PersistenceType aPersistenceType, const nsACString& aASCIIOrigin) const;
+
+ nsresult RestoreDirectoryMetadata2(nsIFile* aDirectory);
+
+ // XXX Remove aPersistenceType argument once the persistence type is stored
+ // in the metadata file.
+ Result<FullOriginMetadata, nsresult> LoadFullOriginMetadata(
+ nsIFile* aDirectory, PersistenceType aPersistenceType);
+
+ Result<FullOriginMetadata, nsresult> LoadFullOriginMetadataWithRestore(
+ nsIFile* aDirectory);
+
+ // This is the main entry point into the QuotaManager API.
+ // Any storage API implementation (quota client) that participates in
+ // centralized quota and storage handling should call this method to get
+ // a directory lock which will protect client's files from being deleted
+ // while they are still in use.
+ // After a lock is acquired, client is notified via the open listener's
+ // method DirectoryLockAcquired. If the lock couldn't be acquired, client
+ // gets DirectoryLockFailed notification.
+ // A lock is a reference counted object and at the time DirectoryLockAcquired
+ // is called, quota manager holds just one strong reference to it which is
+ // then immediatelly cleared by quota manager. So it's up to client to add
+ // a new reference in order to keep the lock alive.
+ // Unlocking is simply done by dropping all references to the lock object.
+ // In other words, protection which the lock represents dies with the lock
+ // object itself.
+ RefPtr<ClientDirectoryLock> CreateDirectoryLock(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata,
+ Client::Type aClientType, bool aExclusive);
+
+ // XXX RemoveMe once bug 1170279 gets fixed.
+ RefPtr<UniversalDirectoryLock> CreateDirectoryLockInternal(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType, bool aExclusive);
+
+ // Collect inactive and the least recently used origins.
+ uint64_t CollectOriginsForEviction(
+ uint64_t aMinSizeToBeFreed,
+ nsTArray<RefPtr<OriginDirectoryLock>>& aLocks);
+
+ /**
+ * Helper method to invoke the provided predicate on all "pending" OriginInfo
+ * instances. These are origins for which the origin directory has not yet
+ * been created but for which quota is already being tracked. This happens,
+ * for example, for the LocalStorage client where an origin that previously
+ * was not using LocalStorage can start issuing writes which it buffers until
+ * eventually flushing them. We defer creating the origin directory for as
+ * long as possible in that case, so the directory won't exist. Logic that
+ * would otherwise only consult the filesystem also needs to use this method.
+ */
+ template <typename P>
+ void CollectPendingOriginsForListing(P aPredicate);
+
+ bool IsStorageInitialized() const {
+ AssertIsOnIOThread();
+ return static_cast<bool>(mStorageConnection);
+ }
+
+ void AssertStorageIsInitialized() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ nsresult EnsureStorageIsInitialized();
+
+ // Returns a pair of an nsIFile object referring to the directory, and a bool
+ // indicating whether the directory was newly created.
+ Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
+ EnsurePersistentOriginIsInitialized(const OriginMetadata& aOriginMetadata);
+
+ // Returns a pair of an nsIFile object referring to the directory, and a bool
+ // indicating whether the directory was newly created.
+ Result<std::pair<nsCOMPtr<nsIFile>, bool>, nsresult>
+ EnsureTemporaryOriginIsInitialized(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata);
+
+ nsresult EnsureTemporaryStorageIsInitialized();
+
+ RefPtr<BoolPromise> ShutdownStorage();
+
+ void ShutdownStorageInternal();
+
+ // Returns a bool indicating whether the directory was newly created.
+ Result<bool, nsresult> EnsureOriginDirectory(nsIFile& aDirectory);
+
+ nsresult AboutToClearOrigins(
+ const Nullable<PersistenceType>& aPersistenceType,
+ const OriginScope& aOriginScope,
+ const Nullable<Client::Type>& aClientType);
+
+ void OriginClearCompleted(PersistenceType aPersistenceType,
+ const nsACString& aOrigin,
+ const Nullable<Client::Type>& aClientType);
+
+ void StartIdleMaintenance() {
+ AssertIsOnOwningThread();
+
+ for (const auto& client : *mClients) {
+ client->StartIdleMaintenance();
+ }
+ }
+
+ void StopIdleMaintenance() {
+ AssertIsOnOwningThread();
+
+ for (const auto& client : *mClients) {
+ client->StopIdleMaintenance();
+ }
+ }
+
+ void AssertCurrentThreadOwnsQuotaMutex() {
+ mQuotaMutex.AssertCurrentThreadOwns();
+ }
+
+ nsIThread* IOThread() { return mIOThread->get(); }
+
+ Client* GetClient(Client::Type aClientType);
+
+ const AutoTArray<Client::Type, Client::TYPE_MAX>& AllClientTypes();
+
+ const nsString& GetBasePath() const { return mBasePath; }
+
+ const nsString& GetStorageName() const { return mStorageName; }
+
+ const nsString& GetStoragePath() const { return *mStoragePath; }
+
+ const nsString& GetStoragePath(PersistenceType aPersistenceType) const {
+ if (aPersistenceType == PERSISTENCE_TYPE_PERSISTENT) {
+ return *mPermanentStoragePath;
+ }
+
+ if (aPersistenceType == PERSISTENCE_TYPE_TEMPORARY) {
+ return *mTemporaryStoragePath;
+ }
+
+ MOZ_ASSERT(aPersistenceType == PERSISTENCE_TYPE_DEFAULT);
+
+ return *mDefaultStoragePath;
+ }
+
+ uint64_t GetGroupLimit() const;
+
+ std::pair<uint64_t, uint64_t> GetUsageAndLimitForEstimate(
+ const OriginMetadata& aOriginMetadata);
+
+ uint64_t GetOriginUsage(const PrincipalMetadata& aPrincipalMetadata);
+
+ Maybe<FullOriginMetadata> GetFullOriginMetadata(
+ const OriginMetadata& aOriginMetadata);
+
+ void NotifyStoragePressure(uint64_t aUsage);
+
+ // Record a quota client shutdown step, if shutting down.
+ // Assumes that the QuotaManager singleton is alive.
+ static void MaybeRecordQuotaClientShutdownStep(
+ const Client::Type aClientType, const nsACString& aStepDescription);
+
+ // Record a quota client shutdown step, if shutting down.
+ // Checks if the QuotaManager singleton is alive.
+ static void SafeMaybeRecordQuotaClientShutdownStep(
+ Client::Type aClientType, const nsACString& aStepDescription);
+
+ // Record a quota manager shutdown step, use only if shutdown is active.
+ void RecordQuotaManagerShutdownStep(const nsACString& aStepDescription);
+
+ // Record a quota manager shutdown step, if shutting down.
+ void MaybeRecordQuotaManagerShutdownStep(const nsACString& aStepDescription);
+
+ template <typename F>
+ void MaybeRecordQuotaManagerShutdownStepWith(F&& aFunc);
+
+ static void GetStorageId(PersistenceType aPersistenceType,
+ const nsACString& aOrigin, Client::Type aClientType,
+ nsACString& aDatabaseId);
+
+ static bool IsPrincipalInfoValid(const PrincipalInfo& aPrincipalInfo);
+
+ static PrincipalMetadata GetInfoFromValidatedPrincipalInfo(
+ const PrincipalInfo& aPrincipalInfo);
+
+ static nsAutoCString GetOriginFromValidatedPrincipalInfo(
+ const PrincipalInfo& aPrincipalInfo);
+
+ static Result<PrincipalMetadata, nsresult> GetInfoFromPrincipal(
+ nsIPrincipal* aPrincipal);
+
+ static Result<nsAutoCString, nsresult> GetOriginFromPrincipal(
+ nsIPrincipal* aPrincipal);
+
+ static Result<nsAutoCString, nsresult> GetOriginFromWindow(
+ nsPIDOMWindowOuter* aWindow);
+
+ static nsLiteralCString GetOriginForChrome();
+
+ static PrincipalMetadata GetInfoForChrome();
+
+ static bool IsOriginInternal(const nsACString& aOrigin);
+
+ static bool AreOriginsEqualOnDisk(const nsACString& aOrigin1,
+ const nsACString& aOrigin2);
+
+ static Result<PrincipalInfo, nsresult> ParseOrigin(const nsACString& aOrigin);
+
+ static void InvalidateQuotaCache();
+
+ private:
+ virtual ~QuotaManager();
+
+ nsresult Init();
+
+ void Shutdown();
+
+ void RegisterDirectoryLock(DirectoryLockImpl& aLock);
+
+ void UnregisterDirectoryLock(DirectoryLockImpl& aLock);
+
+ void AddPendingDirectoryLock(DirectoryLockImpl& aLock);
+
+ void RemovePendingDirectoryLock(DirectoryLockImpl& aLock);
+
+ uint64_t LockedCollectOriginsForEviction(
+ uint64_t aMinSizeToBeFreed,
+ nsTArray<RefPtr<OriginDirectoryLock>>& aLocks);
+
+ void LockedRemoveQuotaForOrigin(const OriginMetadata& aOriginMetadata);
+
+ already_AddRefed<GroupInfo> LockedGetOrCreateGroupInfo(
+ PersistenceType aPersistenceType, const nsACString& aSuffix,
+ const nsACString& aGroup);
+
+ already_AddRefed<OriginInfo> LockedGetOriginInfo(
+ PersistenceType aPersistenceType, const OriginMetadata& aOriginMetadata);
+
+ nsresult UpgradeFromIndexedDBDirectoryToPersistentStorageDirectory(
+ nsIFile* aIndexedDBDir);
+
+ nsresult UpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory(
+ nsIFile* aPersistentStorageDir);
+
+ nsresult MaybeUpgradeToDefaultStorageDirectory(nsIFile& aStorageFile);
+
+ template <typename Helper>
+ nsresult UpgradeStorage(const int32_t aOldVersion, const int32_t aNewVersion,
+ mozIStorageConnection* aConnection);
+
+ nsresult UpgradeStorageFrom0_0To1_0(mozIStorageConnection* aConnection);
+
+ nsresult UpgradeStorageFrom1_0To2_0(mozIStorageConnection* aConnection);
+
+ nsresult UpgradeStorageFrom2_0To2_1(mozIStorageConnection* aConnection);
+
+ nsresult UpgradeStorageFrom2_1To2_2(mozIStorageConnection* aConnection);
+
+ nsresult UpgradeStorageFrom2_2To2_3(mozIStorageConnection* aConnection);
+
+ nsresult MaybeCreateOrUpgradeStorage(mozIStorageConnection& aConnection);
+
+ OkOrErr MaybeRemoveLocalStorageArchiveTmpFile();
+
+ nsresult MaybeRemoveLocalStorageDataAndArchive(nsIFile& aLsArchiveFile);
+
+ nsresult MaybeRemoveLocalStorageDirectories();
+
+ Result<Ok, nsresult> CopyLocalStorageArchiveFromWebAppsStore(
+ nsIFile& aLsArchiveFile) const;
+
+ Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+ CreateLocalStorageArchiveConnection(nsIFile& aLsArchiveFile) const;
+
+ Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+ RecopyLocalStorageArchiveFromWebAppsStore(nsIFile& aLsArchiveFile);
+
+ Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+ DowngradeLocalStorageArchive(nsIFile& aLsArchiveFile);
+
+ Result<nsCOMPtr<mozIStorageConnection>, nsresult>
+ UpgradeLocalStorageArchiveFromLessThan4To4(nsIFile& aLsArchiveFile);
+
+ /*
+ nsresult UpgradeLocalStorageArchiveFrom4To5();
+ */
+
+ Result<Ok, nsresult> MaybeCreateOrUpgradeLocalStorageArchive(
+ nsIFile& aLsArchiveFile);
+
+ Result<Ok, nsresult> CreateEmptyLocalStorageArchive(
+ nsIFile& aLsArchiveFile) const;
+
+ template <typename OriginFunc>
+ nsresult InitializeRepository(PersistenceType aPersistenceType,
+ OriginFunc&& aOriginFunc);
+
+ nsresult InitializeOrigin(PersistenceType aPersistenceType,
+ const OriginMetadata& aOriginMetadata,
+ int64_t aAccessTime, bool aPersisted,
+ nsIFile* aDirectory);
+
+ using OriginInfosFlatTraversable =
+ nsTArray<NotNull<RefPtr<const OriginInfo>>>;
+
+ using OriginInfosNestedTraversable =
+ nsTArray<nsTArray<NotNull<RefPtr<const OriginInfo>>>>;
+
+ OriginInfosNestedTraversable GetOriginInfosExceedingGroupLimit() const;
+
+ OriginInfosNestedTraversable GetOriginInfosExceedingGlobalLimit() const;
+
+ void ClearOrigins(const OriginInfosNestedTraversable& aDoomedOriginInfos);
+
+ void CleanupTemporaryStorage();
+
+ void DeleteFilesForOrigin(PersistenceType aPersistenceType,
+ const nsACString& aOrigin);
+
+ void FinalizeOriginEviction(nsTArray<RefPtr<OriginDirectoryLock>>&& aLocks);
+
+ Result<Ok, nsresult> ArchiveOrigins(
+ const nsTArray<FullOriginMetadata>& aFullOriginMetadatas);
+
+ void ReleaseIOThreadObjects() {
+ AssertIsOnIOThread();
+
+ for (Client::Type type : AllClientTypes()) {
+ (*mClients)[type]->ReleaseIOThreadObjects();
+ }
+ }
+
+ DirectoryLockTable& GetDirectoryLockTable(PersistenceType aPersistenceType);
+
+ bool IsSanitizedOriginValid(const nsACString& aSanitizedOrigin);
+
+ int64_t GenerateDirectoryLockId();
+
+ bool ShutdownStarted() const;
+
+ void RecordShutdownStep(Maybe<Client::Type> aClientType,
+ const nsACString& aStepDescription);
+
+ template <typename Func>
+ auto ExecuteInitialization(Initialization aInitialization, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&>;
+
+ template <typename Func>
+ auto ExecuteInitialization(Initialization aInitialization,
+ const nsACString& aContext, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&>;
+
+ template <typename Func>
+ auto ExecuteOriginInitialization(const nsACString& aOrigin,
+ const OriginInitialization aInitialization,
+ const nsACString& aContext, Func&& aFunc)
+ -> std::invoke_result_t<Func, const FirstInitializationAttempt<
+ Initialization, StringGenerator>&>;
+
+ template <typename Iterator>
+ static void MaybeInsertNonPersistedOriginInfos(
+ Iterator aDest, const RefPtr<GroupInfo>& aTemporaryGroupInfo,
+ const RefPtr<GroupInfo>& aDefaultGroupInfo);
+
+ template <typename Collect, typename Pred>
+ static OriginInfosFlatTraversable CollectLRUOriginInfosUntil(
+ Collect&& aCollect, Pred&& aPred);
+
+ // Thread on which IO is performed.
+ LazyInitializedOnceNotNull<const nsCOMPtr<nsIThread>> mIOThread;
+
+ nsCOMPtr<mozIStorageConnection> mStorageConnection;
+
+ EnumeratedArray<Client::Type, Client::TYPE_MAX, nsCString> mShutdownSteps;
+ LazyInitializedOnce<const TimeStamp> mShutdownStartedAt;
+ Atomic<bool> mShutdownStarted;
+
+ // Accesses to mQuotaManagerShutdownSteps must be protected by mQuotaMutex.
+ nsCString mQuotaManagerShutdownSteps;
+
+ mutable mozilla::Mutex mQuotaMutex MOZ_UNANNOTATED;
+
+ nsClassHashtable<nsCStringHashKey, GroupInfoPair> mGroupInfoPairs;
+
+ // Maintains a list of directory locks that are queued.
+ nsTArray<RefPtr<DirectoryLockImpl>> mPendingDirectoryLocks;
+
+ // Maintains a list of directory locks that are acquired or queued. It can be
+ // accessed on the owning (PBackground) thread only.
+ nsTArray<NotNull<DirectoryLockImpl*>> mDirectoryLocks;
+
+ // Only modifed on the owning thread, but read on multiple threads. Therefore
+ // all modifications (including those on the owning thread) and all reads off
+ // the owning thread must be protected by mQuotaMutex. In other words, only
+ // reads on the owning thread don't have to be protected by mQuotaMutex.
+ nsTHashMap<nsUint64HashKey, NotNull<DirectoryLockImpl*>>
+ mDirectoryLockIdTable;
+
+ // Directory lock tables that are used to update origin access time.
+ DirectoryLockTable mTemporaryDirectoryLockTable;
+ DirectoryLockTable mDefaultDirectoryLockTable;
+
+ // A list of all successfully initialized persistent origins. This list isn't
+ // protected by any mutex but it is only ever touched on the IO thread.
+ nsTArray<nsCString> mInitializedOrigins;
+
+ // A hash table that is used to cache origin parser results for given
+ // sanitized origin strings. This hash table isn't protected by any mutex but
+ // it is only ever touched on the IO thread.
+ nsTHashMap<nsCStringHashKey, bool> mValidOrigins;
+
+ // This array is populated at initialization time and then never modified, so
+ // it can be iterated on any thread.
+ LazyInitializedOnce<const AutoTArray<RefPtr<Client>, Client::TYPE_MAX>>
+ mClients;
+
+ using ClientTypesArray = AutoTArray<Client::Type, Client::TYPE_MAX>;
+ LazyInitializedOnce<const ClientTypesArray> mAllClientTypes;
+ LazyInitializedOnce<const ClientTypesArray> mAllClientTypesExceptLS;
+
+ // This object isn't protected by any mutex but it is only ever touched on
+ // the IO thread.
+ InitializationInfo mInitializationInfo;
+
+ const nsString mBasePath;
+ const nsString mStorageName;
+ LazyInitializedOnce<const nsString> mIndexedDBPath;
+ LazyInitializedOnce<const nsString> mStoragePath;
+ LazyInitializedOnce<const nsString> mStorageArchivesPath;
+ LazyInitializedOnce<const nsString> mPermanentStoragePath;
+ LazyInitializedOnce<const nsString> mTemporaryStoragePath;
+ LazyInitializedOnce<const nsString> mDefaultStoragePath;
+
+ MozPromiseHolder<BoolPromise> mShutdownStoragePromiseHolder;
+
+ uint64_t mTemporaryStorageLimit;
+ uint64_t mTemporaryStorageUsage;
+ int64_t mNextDirectoryLockId;
+ bool mTemporaryStorageInitialized;
+ bool mCacheUsable;
+ bool mShuttingDownStorage;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif /* mozilla_dom_quota_quotamanager_h__ */
diff --git a/dom/quota/QuotaManagerImpl.h b/dom/quota/QuotaManagerImpl.h
new file mode 100644
index 0000000000..1cdd5ed99e
--- /dev/null
+++ b/dom/quota/QuotaManagerImpl.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_QUOTAMANAGERIMPL_H_
+#define DOM_QUOTA_QUOTAMANAGERIMPL_H_
+
+#include "QuotaManager.h"
+
+namespace mozilla::dom::quota {
+
+template <typename F>
+void QuotaManager::MaybeRecordQuotaManagerShutdownStepWith(F&& aFunc) {
+ // Callable on any thread.
+
+ if (IsShuttingDown()) {
+ RecordShutdownStep(Nothing{}, std::forward<F>(aFunc)());
+ }
+}
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_QUOTAMANAGERIMPL_H_
diff --git a/dom/quota/QuotaManagerService.cpp b/dom/quota/QuotaManagerService.cpp
new file mode 100644
index 0000000000..48dcbfa659
--- /dev/null
+++ b/dom/quota/QuotaManagerService.cpp
@@ -0,0 +1,1046 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QuotaManagerService.h"
+
+// Local includes
+#include "ActorsChild.h"
+#include "Client.h"
+#include "QuotaManager.h"
+#include "QuotaRequests.h"
+
+// Global includes
+#include <cstdint>
+#include <cstring>
+#include <utility>
+#include "MainThreadUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Hal.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/OriginAttributes.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Variant.h"
+#include "mozilla/dom/quota/PQuota.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "mozilla/fallible.h"
+#include "mozilla/hal_sandbox/PHal.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/BackgroundUtils.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/PBackgroundSharedTypes.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIObserverService.h"
+#include "nsIPrincipal.h"
+#include "nsIUserIdleService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsStringFwd.h"
+#include "nsXULAppAPI.h"
+#include "nscore.h"
+
+#define PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID "profile-before-change-qm"
+
+namespace mozilla::dom::quota {
+
+using namespace mozilla::ipc;
+
+namespace {
+
+const char kIdleServiceContractId[] = "@mozilla.org/widget/useridleservice;1";
+
+// The number of seconds we will wait after receiving the idle-daily
+// notification before beginning maintenance.
+const uint32_t kIdleObserverTimeSec = 1;
+
+mozilla::StaticRefPtr<QuotaManagerService> gQuotaManagerService;
+
+mozilla::Atomic<bool> gInitialized(false);
+mozilla::Atomic<bool> gClosed(false);
+
+nsresult CheckedPrincipalToPrincipalInfo(nsIPrincipal* aPrincipal,
+ PrincipalInfo& aPrincipalInfo) {
+ MOZ_ASSERT(aPrincipal);
+
+ nsresult rv = PrincipalToPrincipalInfo(aPrincipal, &aPrincipalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_WARN_IF(!QuotaManager::IsPrincipalInfoValid(aPrincipalInfo))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aPrincipalInfo.type() != PrincipalInfo::TContentPrincipalInfo &&
+ aPrincipalInfo.type() != PrincipalInfo::TSystemPrincipalInfo) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ return NS_OK;
+}
+
+nsresult GetClearResetOriginParams(nsIPrincipal* aPrincipal,
+ const nsACString& aPersistenceType,
+ const nsAString& aClientType,
+ ClearResetOriginParams& aParams) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, aParams.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (aPersistenceType.IsVoid()) {
+ aParams.persistenceTypeIsExplicit() = false;
+ } else {
+ const auto maybePersistenceType =
+ PersistenceTypeFromString(aPersistenceType, fallible);
+ if (NS_WARN_IF(maybePersistenceType.isNothing())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aParams.persistenceType() = maybePersistenceType.value();
+ aParams.persistenceTypeIsExplicit() = true;
+ }
+
+ if (aClientType.IsVoid()) {
+ aParams.clientTypeIsExplicit() = false;
+ } else {
+ Client::Type clientType;
+ bool ok = Client::TypeFromText(aClientType, clientType, fallible);
+ if (NS_WARN_IF(!ok)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aParams.clientType() = clientType;
+ aParams.clientTypeIsExplicit() = true;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+class QuotaManagerService::PendingRequestInfo {
+ protected:
+ RefPtr<RequestBase> mRequest;
+
+ public:
+ explicit PendingRequestInfo(RequestBase* aRequest) : mRequest(aRequest) {}
+
+ virtual ~PendingRequestInfo() = default;
+
+ RequestBase* GetRequest() const { return mRequest; }
+
+ virtual nsresult InitiateRequest(QuotaChild* aActor) = 0;
+};
+
+class QuotaManagerService::UsageRequestInfo : public PendingRequestInfo {
+ UsageRequestParams mParams;
+
+ public:
+ UsageRequestInfo(UsageRequest* aRequest, const UsageRequestParams& aParams)
+ : PendingRequestInfo(aRequest), mParams(aParams) {
+ MOZ_ASSERT(aRequest);
+ MOZ_ASSERT(aParams.type() != UsageRequestParams::T__None);
+ }
+
+ virtual nsresult InitiateRequest(QuotaChild* aActor) override;
+};
+
+class QuotaManagerService::RequestInfo : public PendingRequestInfo {
+ RequestParams mParams;
+
+ public:
+ RequestInfo(Request* aRequest, const RequestParams& aParams)
+ : PendingRequestInfo(aRequest), mParams(aParams) {
+ MOZ_ASSERT(aRequest);
+ MOZ_ASSERT(aParams.type() != RequestParams::T__None);
+ }
+
+ virtual nsresult InitiateRequest(QuotaChild* aActor) override;
+};
+
+class QuotaManagerService::IdleMaintenanceInfo : public PendingRequestInfo {
+ const bool mStart;
+
+ public:
+ explicit IdleMaintenanceInfo(bool aStart)
+ : PendingRequestInfo(nullptr), mStart(aStart) {}
+
+ virtual nsresult InitiateRequest(QuotaChild* aActor) override;
+};
+
+QuotaManagerService::QuotaManagerService()
+ : mBackgroundActor(nullptr),
+ mBackgroundActorFailed(false),
+ mIdleObserverRegistered(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+QuotaManagerService::~QuotaManagerService() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mIdleObserverRegistered);
+}
+
+// static
+QuotaManagerService* QuotaManagerService::GetOrCreate() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (gClosed) {
+ MOZ_ASSERT(false, "Calling GetOrCreate() after shutdown!");
+ return nullptr;
+ }
+
+ if (!gQuotaManagerService) {
+ RefPtr<QuotaManagerService> instance(new QuotaManagerService());
+
+ nsresult rv = instance->Init();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ if (gInitialized.exchange(true)) {
+ MOZ_ASSERT(false, "Initialized more than once?!");
+ }
+
+ gQuotaManagerService = instance;
+
+ ClearOnShutdown(&gQuotaManagerService);
+ }
+
+ return gQuotaManagerService;
+}
+
+// static
+QuotaManagerService* QuotaManagerService::Get() {
+ // Does not return an owning reference.
+ return gQuotaManagerService;
+}
+
+// static
+already_AddRefed<QuotaManagerService> QuotaManagerService::FactoryCreate() {
+ RefPtr<QuotaManagerService> quotaManagerService = GetOrCreate();
+ return quotaManagerService.forget();
+}
+
+void QuotaManagerService::ClearBackgroundActor() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mBackgroundActor = nullptr;
+}
+
+void QuotaManagerService::AbortOperationsForProcess(
+ ContentParentId aContentParentId) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = EnsureBackgroundActor();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (NS_WARN_IF(
+ !mBackgroundActor->SendAbortOperationsForProcess(aContentParentId))) {
+ return;
+ }
+}
+
+nsresult QuotaManagerService::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (XRE_IsParentProcess()) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (NS_WARN_IF(!observerService)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = observerService->AddObserver(
+ this, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void QuotaManagerService::Destroy() {
+ // Setting the closed flag prevents the service from being recreated.
+ // Don't set it though if there's no real instance created.
+ if (gInitialized && gClosed.exchange(true)) {
+ MOZ_ASSERT(false, "Shutdown more than once?!");
+ }
+
+ delete this;
+}
+
+nsresult QuotaManagerService::EnsureBackgroundActor() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Nothing can be done here if we have previously failed to create a
+ // background actor.
+ if (mBackgroundActorFailed) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mBackgroundActor) {
+ PBackgroundChild* backgroundActor =
+ BackgroundChild::GetOrCreateForCurrentThread();
+ if (NS_WARN_IF(!backgroundActor)) {
+ mBackgroundActorFailed = true;
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ QuotaChild* actor = new QuotaChild(this);
+
+ mBackgroundActor = static_cast<QuotaChild*>(
+ backgroundActor->SendPQuotaConstructor(actor));
+ }
+ }
+
+ if (!mBackgroundActor) {
+ mBackgroundActorFailed = true;
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult QuotaManagerService::InitiateRequest(PendingRequestInfo& aInfo) {
+ nsresult rv = EnsureBackgroundActor();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = aInfo.InitiateRequest(mBackgroundActor);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void QuotaManagerService::PerformIdleMaintenance() {
+ using namespace mozilla::hal;
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // If we're running on battery power then skip all idle maintenance since we
+ // would otherwise be doing lots of disk I/O.
+ BatteryInformation batteryInfo;
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Android XPCShell doesn't load the AndroidBridge that is needed to make
+ // GetCurrentBatteryInformation work...
+ if (!QuotaManager::IsRunningXPCShellTests())
+#endif
+ {
+ // In order to give the correct battery level, hal must have registered
+ // battery observers.
+ RegisterBatteryObserver(this);
+ GetCurrentBatteryInformation(&batteryInfo);
+ UnregisterBatteryObserver(this);
+ }
+
+ // If we're running XPCShell because we always want to be able to test this
+ // code so pretend that we're always charging.
+ if (QuotaManager::IsRunningXPCShellTests()) {
+ batteryInfo.level() = 100;
+ batteryInfo.charging() = true;
+ }
+
+ if (NS_WARN_IF(!batteryInfo.charging())) {
+ return;
+ }
+
+ if (QuotaManager::IsRunningXPCShellTests()) {
+ // We don't want user activity to impact this code if we're running tests.
+ Unused << Observe(nullptr, OBSERVER_TOPIC_IDLE, nullptr);
+ } else if (!mIdleObserverRegistered) {
+ nsCOMPtr<nsIUserIdleService> idleService =
+ do_GetService(kIdleServiceContractId);
+ MOZ_ASSERT(idleService);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ idleService->AddIdleObserver(this, kIdleObserverTimeSec));
+
+ mIdleObserverRegistered = true;
+ }
+}
+
+void QuotaManagerService::RemoveIdleObserver() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mIdleObserverRegistered) {
+ nsCOMPtr<nsIUserIdleService> idleService =
+ do_GetService(kIdleServiceContractId);
+ MOZ_ASSERT(idleService);
+
+ // Ignore the return value of RemoveIdleObserver, it may fail if the
+ // observer has already been unregistered during shutdown.
+ Unused << idleService->RemoveIdleObserver(this, kIdleObserverTimeSec);
+
+ mIdleObserverRegistered = false;
+ }
+}
+
+NS_IMPL_ADDREF(QuotaManagerService)
+NS_IMPL_RELEASE_WITH_DESTROY(QuotaManagerService, Destroy())
+NS_IMPL_QUERY_INTERFACE(QuotaManagerService, nsIQuotaManagerService,
+ nsIObserver)
+
+NS_IMETHODIMP
+QuotaManagerService::StorageName(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ StorageNameParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::StorageInitialized(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ StorageInitializedParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::TemporaryStorageInitialized(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ TemporaryStorageInitializedParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Init(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ InitParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::InitTemporaryStorage(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ InitTemporaryStorageParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::InitializePersistentOrigin(nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ InitializePersistentOriginParams params;
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::InitializeTemporaryOrigin(
+ const nsACString& aPersistenceType, nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ InitializeTemporaryOriginParams params;
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromString(aPersistenceType, fallible);
+ if (NS_WARN_IF(maybePersistenceType.isNothing())) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (NS_WARN_IF(!IsBestEffortPersistenceType(maybePersistenceType.value()))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ params.persistenceType() = maybePersistenceType.value();
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::GetFullOriginMetadata(const nsACString& aPersistenceType,
+ nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(nsContentUtils::IsCallerChrome());
+
+ QM_TRY(OkIf(StaticPrefs::dom_quotaManager_testing()), NS_ERROR_UNEXPECTED);
+
+ const auto maybePersistenceType =
+ PersistenceTypeFromString(aPersistenceType, fallible);
+ QM_TRY(OkIf(maybePersistenceType.isSome()), NS_ERROR_INVALID_ARG);
+ QM_TRY(OkIf(IsBestEffortPersistenceType(*maybePersistenceType)),
+ NS_ERROR_INVALID_ARG);
+
+ PrincipalInfo principalInfo;
+ QM_TRY(MOZ_TO_RESULT(PrincipalToPrincipalInfo(aPrincipal, &principalInfo)));
+ QM_TRY(OkIf(QuotaManager::IsPrincipalInfoValid(principalInfo)),
+ NS_ERROR_INVALID_ARG);
+
+ RefPtr<Request> request = new Request();
+
+ GetFullOriginMetadataParams params;
+
+ params.persistenceType() = *maybePersistenceType;
+ params.principalInfo() = std::move(principalInfo);
+
+ RequestInfo info(request, params);
+
+ QM_TRY(MOZ_TO_RESULT(InitiateRequest(info)));
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::GetUsage(nsIQuotaUsageCallback* aCallback, bool aGetAll,
+ nsIQuotaUsageRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCallback);
+
+ RefPtr<UsageRequest> request = new UsageRequest(aCallback);
+
+ AllUsageParams params;
+
+ params.getAll() = aGetAll;
+
+ UsageRequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::GetUsageForPrincipal(nsIPrincipal* aPrincipal,
+ nsIQuotaUsageCallback* aCallback,
+ bool aFromMemory,
+ nsIQuotaUsageRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+
+ RefPtr<UsageRequest> request = new UsageRequest(aPrincipal, aCallback);
+
+ OriginUsageParams params;
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ params.fromMemory() = aFromMemory;
+
+ UsageRequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Clear(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ ClearAllParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::ClearStoragesForOriginAttributesPattern(
+ const nsAString& aPattern, nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ OriginAttributesPattern pattern;
+ MOZ_ALWAYS_TRUE(pattern.Init(aPattern));
+
+ RefPtr<Request> request = new Request();
+
+ ClearDataParams params;
+
+ params.pattern() = pattern;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::ClearStoragesForPrincipal(
+ nsIPrincipal* aPrincipal, const nsACString& aPersistenceType,
+ const nsAString& aClientType, bool aClearAll, nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ nsCString suffix;
+ aPrincipal->OriginAttributesRef().CreateSuffix(suffix);
+
+ if (NS_WARN_IF(aClearAll && !suffix.IsEmpty())) {
+ // The originAttributes should be default originAttributes when the
+ // aClearAll flag is set.
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<Request> request = new Request(aPrincipal);
+
+ ClearResetOriginParams commonParams;
+
+ nsresult rv = GetClearResetOriginParams(aPrincipal, aPersistenceType,
+ aClientType, commonParams);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ ClearOriginParams params;
+ params.commonParams() = std::move(commonParams);
+ params.matchAll() = aClearAll;
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Reset(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request();
+
+ ResetAllParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::ResetStoragesForPrincipal(
+ nsIPrincipal* aPrincipal, const nsACString& aPersistenceType,
+ const nsAString& aClientType, nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ if (NS_WARN_IF(!StaticPrefs::dom_quotaManager_testing())) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ RefPtr<Request> request = new Request(aPrincipal);
+
+ ClearResetOriginParams commonParams;
+
+ nsresult rv = GetClearResetOriginParams(aPrincipal, aPersistenceType,
+ aClientType, commonParams);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestParams params;
+ params = ResetOriginParams(commonParams);
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Persisted(nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(_retval);
+
+ RefPtr<Request> request = new Request(aPrincipal);
+
+ PersistedParams params;
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Persist(nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(_retval);
+
+ RefPtr<Request> request = new Request(aPrincipal);
+
+ PersistParams params;
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Estimate(nsIPrincipal* aPrincipal,
+ nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPrincipal);
+
+ RefPtr<Request> request = new Request(aPrincipal);
+
+ EstimateParams params;
+
+ nsresult rv =
+ CheckedPrincipalToPrincipalInfo(aPrincipal, params.principalInfo());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ RequestInfo info(request, params);
+
+ rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::ListOrigins(nsIQuotaRequest** _retval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<Request> request = new Request();
+
+ ListOriginsParams params;
+
+ RequestInfo info(request, params);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ request.forget(_retval);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+QuotaManagerService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!strcmp(aTopic, PROFILE_BEFORE_CHANGE_QM_OBSERVER_ID)) {
+ RemoveIdleObserver();
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY)) {
+ PerformIdleMaintenance();
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, OBSERVER_TOPIC_IDLE)) {
+ IdleMaintenanceInfo info(/* aStart */ true);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ if (!strcmp(aTopic, OBSERVER_TOPIC_ACTIVE)) {
+ RemoveIdleObserver();
+
+ IdleMaintenanceInfo info(/* aStart */ false);
+
+ nsresult rv = InitiateRequest(info);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Should never get here!");
+ return NS_OK;
+}
+
+void QuotaManagerService::Notify(const hal::BatteryInformation& aBatteryInfo) {
+ // This notification is received when battery data changes. We don't need to
+ // deal with this notification.
+}
+
+nsresult QuotaManagerService::UsageRequestInfo::InitiateRequest(
+ QuotaChild* aActor) {
+ MOZ_ASSERT(aActor);
+
+ auto request = static_cast<UsageRequest*>(mRequest.get());
+
+ auto actor = new QuotaUsageRequestChild(request);
+
+ if (!aActor->SendPQuotaUsageRequestConstructor(actor, mParams)) {
+ request->SetError(NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ request->SetBackgroundActor(actor);
+
+ return NS_OK;
+}
+
+nsresult QuotaManagerService::RequestInfo::InitiateRequest(QuotaChild* aActor) {
+ MOZ_ASSERT(aActor);
+
+ auto request = static_cast<Request*>(mRequest.get());
+
+ auto actor = new QuotaRequestChild(request);
+
+ if (!aActor->SendPQuotaRequestConstructor(actor, mParams)) {
+ request->SetError(NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult QuotaManagerService::IdleMaintenanceInfo::InitiateRequest(
+ QuotaChild* aActor) {
+ MOZ_ASSERT(aActor);
+
+ bool result;
+
+ if (mStart) {
+ result = aActor->SendStartIdleMaintenance();
+ } else {
+ result = aActor->SendStopIdleMaintenance();
+ }
+
+ if (!result) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/QuotaManagerService.h b/dom/quota/QuotaManagerService.h
new file mode 100644
index 0000000000..f8f8c01c31
--- /dev/null
+++ b/dom/quota/QuotaManagerService.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_QuotaManagerService_h
+#define mozilla_dom_quota_QuotaManagerService_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/HalBatteryInformation.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "nsIObserver.h"
+#include "nsIQuotaManagerService.h"
+#include "nsISupports.h"
+
+#define QUOTAMANAGER_SERVICE_CONTRACTID \
+ "@mozilla.org/dom/quota-manager-service;1"
+
+class nsIPrincipal;
+class nsIQuotaRequest;
+class nsIQuotaUsageCallback;
+class nsIQuotaUsageRequest;
+
+namespace mozilla {
+namespace ipc {
+
+class PBackgroundChild;
+
+} // namespace ipc
+
+namespace hal {
+class BatteryInformation;
+}
+
+namespace dom::quota {
+
+class QuotaChild;
+class QuotaManager;
+
+class QuotaManagerService final : public nsIQuotaManagerService,
+ public nsIObserver,
+ public hal::BatteryObserver {
+ using PBackgroundChild = mozilla::ipc::PBackgroundChild;
+
+ class BackgroundCreateCallback;
+ class PendingRequestInfo;
+ class UsageRequestInfo;
+ class RequestInfo;
+ class IdleMaintenanceInfo;
+
+ QuotaChild* mBackgroundActor;
+
+ bool mBackgroundActorFailed;
+ bool mIdleObserverRegistered;
+
+ public:
+ // Returns a non-owning reference.
+ static QuotaManagerService* GetOrCreate();
+
+ // Returns a non-owning reference.
+ static QuotaManagerService* Get();
+
+ // No one should call this but the factory.
+ static already_AddRefed<QuotaManagerService> FactoryCreate();
+
+ void ClearBackgroundActor();
+
+ // Called when a process is being shot down. Aborts any running operations
+ // for the given process.
+ void AbortOperationsForProcess(ContentParentId aContentParentId);
+
+ private:
+ QuotaManagerService();
+ ~QuotaManagerService();
+
+ nsresult Init();
+
+ void Destroy();
+
+ nsresult EnsureBackgroundActor();
+
+ nsresult InitiateRequest(PendingRequestInfo& aInfo);
+
+ void PerformIdleMaintenance();
+
+ void RemoveIdleObserver();
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIQUOTAMANAGERSERVICE
+ NS_DECL_NSIOBSERVER
+
+ // BatteryObserver override
+ void Notify(const hal::BatteryInformation& aBatteryInfo) override;
+};
+
+} // namespace dom::quota
+} // namespace mozilla
+
+#endif /* mozilla_dom_quota_QuotaManagerService_h */
diff --git a/dom/quota/QuotaObject.cpp b/dom/quota/QuotaObject.cpp
new file mode 100644
index 0000000000..94256ed1eb
--- /dev/null
+++ b/dom/quota/QuotaObject.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QuotaObject.h"
+
+#include "CanonicalQuotaObject.h"
+#include "mozilla/dom/quota/IPCQuotaObject.h"
+#include "mozilla/dom/quota/PRemoteQuotaObject.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectChild.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectParent.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "RemoteQuotaObject.h"
+
+namespace mozilla::dom::quota {
+
+CanonicalQuotaObject* QuotaObject::AsCanonicalQuotaObject() {
+ return mIsRemote ? nullptr : static_cast<CanonicalQuotaObject*>(this);
+}
+
+RemoteQuotaObject* QuotaObject::AsRemoteQuotaObject() {
+ return mIsRemote ? static_cast<RemoteQuotaObject*>(this) : nullptr;
+}
+
+IPCQuotaObject QuotaObject::Serialize(nsIInterfaceRequestor* aCallbacks) {
+ MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
+ MOZ_RELEASE_ASSERT(!NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(!mozilla::ipc::IsOnBackgroundThread());
+ MOZ_RELEASE_ASSERT(!GetCurrentThreadWorkerPrivate());
+ MOZ_RELEASE_ASSERT(!mIsRemote);
+
+ mozilla::ipc::Endpoint<PRemoteQuotaObjectParent> parentEndpoint;
+ mozilla::ipc::Endpoint<PRemoteQuotaObjectChild> childEndpoint;
+ MOZ_ALWAYS_SUCCEEDS(
+ PRemoteQuotaObject::CreateEndpoints(&parentEndpoint, &childEndpoint));
+
+ nsCOMPtr<RemoteQuotaObjectParentTracker> tracker =
+ do_GetInterface(aCallbacks);
+
+ auto actor =
+ MakeRefPtr<RemoteQuotaObjectParent>(AsCanonicalQuotaObject(), tracker);
+
+ if (tracker) {
+ tracker->RegisterRemoteQuotaObjectParent(WrapNotNull(actor));
+ }
+
+ parentEndpoint.Bind(actor);
+
+ IPCQuotaObject ipcQuotaObject;
+ ipcQuotaObject.childEndpoint() = std::move(childEndpoint);
+
+ return ipcQuotaObject;
+}
+
+// static
+RefPtr<QuotaObject> QuotaObject::Deserialize(IPCQuotaObject& aQuotaObject) {
+ MOZ_RELEASE_ASSERT(!NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(!mozilla::ipc::IsOnBackgroundThread());
+ MOZ_RELEASE_ASSERT(!GetCurrentThreadWorkerPrivate());
+
+ auto actor = MakeRefPtr<RemoteQuotaObjectChild>();
+
+ aQuotaObject.childEndpoint().Bind(actor);
+
+ return MakeRefPtr<RemoteQuotaObject>(actor);
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/QuotaObject.h b/dom/quota/QuotaObject.h
new file mode 100644
index 0000000000..96397faf66
--- /dev/null
+++ b/dom/quota/QuotaObject.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_QUOTAOBJECT_H_
+#define DOM_QUOTA_QUOTAOBJECT_H_
+
+#include "nsISupportsImpl.h"
+
+class nsIInterfaceRequestor;
+
+namespace mozilla::dom::quota {
+
+class CanonicalQuotaObject;
+class IPCQuotaObject;
+class RemoteQuotaObject;
+
+// QuotaObject type is serializable, but only in a restricted manner. The type
+// is only safe to serialize in the parent process and only when the type
+// hasn't been previously deserialized. So the type can be serialized in the
+// parent process and deserialized in a child process or it can be serialized
+// in the parent process and deserialized in the parent process as well
+// (non-e10s mode). The same type can never be serialized/deserialized more
+// than once.
+// The deserialized type (remote variant) can only be used on the thread it was
+// deserialized on and it will stop working if the thread it was sent from is
+// shutdown (consumers should make sure that the originating thread is kept
+// alive for the necessary time).
+class QuotaObject {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ CanonicalQuotaObject* AsCanonicalQuotaObject();
+
+ RemoteQuotaObject* AsRemoteQuotaObject();
+
+ // Serialize this QuotaObject. This method works only in the parent process
+ // and only with objects which haven't been previously deserialized.
+ // The serial event target where this method is called should be highly
+ // available, as it will be used to process requests from the remote variant.
+ IPCQuotaObject Serialize(nsIInterfaceRequestor* aCallbacks);
+
+ // Deserialize a QuotaObject. This method works in both the child and parent.
+ // The deserialized QuotaObject can only be used on the calling serial event
+ // target.
+ static RefPtr<QuotaObject> Deserialize(IPCQuotaObject& aQuotaObject);
+
+ virtual const nsAString& Path() const = 0;
+
+ [[nodiscard]] virtual bool MaybeUpdateSize(int64_t aSize, bool aTruncate) = 0;
+
+ virtual bool IncreaseSize(int64_t aDelta) = 0;
+
+ virtual void DisableQuotaCheck() = 0;
+
+ virtual void EnableQuotaCheck() = 0;
+
+ protected:
+ QuotaObject(bool aIsRemote) : mIsRemote(aIsRemote) {}
+
+ virtual ~QuotaObject() = default;
+
+ const bool mIsRemote;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_QUOTAOBJECT_H_
diff --git a/dom/quota/QuotaRequests.cpp b/dom/quota/QuotaRequests.cpp
new file mode 100644
index 0000000000..ee781831c8
--- /dev/null
+++ b/dom/quota/QuotaRequests.cpp
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QuotaRequests.h"
+
+// Local includes
+#include "ActorsChild.h"
+
+// Global includes
+#include "mozilla/ErrorNames.h"
+#include "mozilla/MacroForEach.h"
+#include "nsDebug.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsISupportsUtils.h"
+#include "nsIVariant.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+
+namespace mozilla {
+class Runnable;
+}
+
+namespace mozilla::dom::quota {
+
+RequestBase::RequestBase() : mResultCode(NS_OK), mHaveResultOrErrorCode(false) {
+ AssertIsOnOwningThread();
+}
+
+RequestBase::RequestBase(nsIPrincipal* aPrincipal)
+ : mPrincipal(aPrincipal),
+ mResultCode(NS_OK),
+ mHaveResultOrErrorCode(false) {
+ AssertIsOnOwningThread();
+}
+
+#ifdef DEBUG
+
+void RequestBase::AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(RequestBase);
+}
+
+#endif // DEBUG
+
+void RequestBase::SetError(nsresult aRv) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mResultCode == NS_OK);
+ MOZ_ASSERT(!mHaveResultOrErrorCode);
+
+ mResultCode = aRv;
+ mHaveResultOrErrorCode = true;
+
+ FireCallback();
+}
+
+NS_IMPL_CYCLE_COLLECTION_0(RequestBase)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RequestBase)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RequestBase)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RequestBase)
+
+NS_IMETHODIMP
+RequestBase::GetPrincipal(nsIPrincipal** aPrincipal) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aPrincipal);
+
+ NS_IF_ADDREF(*aPrincipal = mPrincipal);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestBase::GetResultCode(nsresult* aResultCode) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aResultCode);
+
+ if (!mHaveResultOrErrorCode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ *aResultCode = mResultCode;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestBase::GetResultName(nsACString& aResultName) {
+ AssertIsOnOwningThread();
+
+ if (!mHaveResultOrErrorCode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mozilla::GetErrorName(mResultCode, aResultName);
+ return NS_OK;
+}
+
+UsageRequest::UsageRequest(nsIQuotaUsageCallback* aCallback)
+ : mCallback(aCallback), mBackgroundActor(nullptr), mCanceled(false) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+}
+
+UsageRequest::UsageRequest(nsIPrincipal* aPrincipal,
+ nsIQuotaUsageCallback* aCallback)
+ : RequestBase(aPrincipal),
+ mCallback(aCallback),
+ mBackgroundActor(nullptr),
+ mCanceled(false) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+}
+
+UsageRequest::~UsageRequest() { AssertIsOnOwningThread(); }
+
+void UsageRequest::SetBackgroundActor(
+ QuotaUsageRequestChild* aBackgroundActor) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aBackgroundActor);
+ MOZ_ASSERT(!mBackgroundActor);
+
+ mBackgroundActor = aBackgroundActor;
+
+ if (mCanceled) {
+ mBackgroundActor->SendCancel();
+ }
+}
+
+void UsageRequest::SetResult(nsIVariant* aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aResult);
+ MOZ_ASSERT(!mHaveResultOrErrorCode);
+
+ mResult = aResult;
+
+ mHaveResultOrErrorCode = true;
+
+ FireCallback();
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(UsageRequest, RequestBase, mCallback)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(UsageRequest)
+ NS_INTERFACE_MAP_ENTRY(nsIQuotaUsageRequest)
+NS_INTERFACE_MAP_END_INHERITING(RequestBase)
+
+NS_IMPL_ADDREF_INHERITED(UsageRequest, RequestBase)
+NS_IMPL_RELEASE_INHERITED(UsageRequest, RequestBase)
+
+NS_IMETHODIMP
+UsageRequest::GetResult(nsIVariant** aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aResult);
+
+ if (!mHaveResultOrErrorCode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IF_ADDREF(*aResult = mResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageRequest::GetCallback(nsIQuotaUsageCallback** aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+
+ NS_IF_ADDREF(*aCallback = mCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageRequest::SetCallback(nsIQuotaUsageCallback* aCallback) {
+ AssertIsOnOwningThread();
+
+ mCallback = aCallback;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageRequest::Cancel() {
+ AssertIsOnOwningThread();
+
+ if (mCanceled) {
+ NS_WARNING("Canceled more than once?!");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (mBackgroundActor) {
+ mBackgroundActor->SendCancel();
+ }
+
+ mCanceled = true;
+
+ return NS_OK;
+}
+
+void UsageRequest::FireCallback() {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mCallback);
+
+ mCallback->OnUsageResult(this);
+
+ // Clean up.
+ mCallback = nullptr;
+}
+
+Request::Request() { AssertIsOnOwningThread(); }
+
+Request::Request(nsIPrincipal* aPrincipal) : RequestBase(aPrincipal) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aPrincipal);
+}
+
+Request::Request(nsIQuotaCallback* aCallback) : mCallback(aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+}
+
+Request::~Request() { AssertIsOnOwningThread(); }
+
+void Request::SetResult(nsIVariant* aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aResult);
+ MOZ_ASSERT(!mHaveResultOrErrorCode);
+
+ mResult = aResult;
+
+ mHaveResultOrErrorCode = true;
+
+ FireCallback();
+}
+
+NS_IMETHODIMP
+Request::GetResult(nsIVariant** aResult) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aResult);
+
+ if (!mHaveResultOrErrorCode) {
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_IF_ADDREF(*aResult = mResult);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(Request, RequestBase, mCallback, mResult)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Request)
+ NS_INTERFACE_MAP_ENTRY(nsIQuotaRequest)
+NS_INTERFACE_MAP_END_INHERITING(RequestBase)
+
+NS_IMPL_ADDREF_INHERITED(mozilla::dom::quota::Request, RequestBase)
+NS_IMPL_RELEASE_INHERITED(mozilla::dom::quota::Request, RequestBase)
+
+NS_IMETHODIMP
+Request::GetCallback(nsIQuotaCallback** aCallback) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aCallback);
+
+ NS_IF_ADDREF(*aCallback = mCallback);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+Request::SetCallback(nsIQuotaCallback* aCallback) {
+ AssertIsOnOwningThread();
+
+ mCallback = aCallback;
+ return NS_OK;
+}
+
+void Request::FireCallback() {
+ AssertIsOnOwningThread();
+
+ if (mCallback) {
+ mCallback->OnComplete(this);
+
+ // Clean up.
+ mCallback = nullptr;
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/QuotaRequests.h b/dom/quota/QuotaRequests.h
new file mode 100644
index 0000000000..7cfa69ae6d
--- /dev/null
+++ b/dom/quota/QuotaRequests.h
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_UsageRequest_h
+#define mozilla_dom_quota_UsageRequest_h
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIPrincipal.h"
+#include "nsIQuotaRequests.h"
+#include "nsISupports.h"
+
+class nsIQuotaCallback;
+class nsIQuotaUsageCallback;
+class nsIVariant;
+
+namespace mozilla::dom::quota {
+
+class QuotaUsageRequestChild;
+
+class RequestBase : public nsIQuotaRequestBase {
+ protected:
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+
+ nsresult mResultCode;
+ bool mHaveResultOrErrorCode;
+
+ public:
+ void AssertIsOnOwningThread() const
+#ifdef DEBUG
+ ;
+#else
+ {
+ }
+#endif
+
+ void SetError(nsresult aRv);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSIQUOTAREQUESTBASE
+ NS_DECL_CYCLE_COLLECTION_CLASS(RequestBase)
+
+ protected:
+ RequestBase();
+
+ RequestBase(nsIPrincipal* aPrincipal);
+
+ virtual ~RequestBase() { AssertIsOnOwningThread(); }
+
+ virtual void FireCallback() = 0;
+};
+
+class UsageRequest final : public RequestBase, public nsIQuotaUsageRequest {
+ nsCOMPtr<nsIQuotaUsageCallback> mCallback;
+
+ nsCOMPtr<nsIVariant> mResult;
+
+ QuotaUsageRequestChild* mBackgroundActor;
+
+ bool mCanceled;
+
+ public:
+ explicit UsageRequest(nsIQuotaUsageCallback* aCallback);
+
+ UsageRequest(nsIPrincipal* aPrincipal, nsIQuotaUsageCallback* aCallback);
+
+ void SetBackgroundActor(QuotaUsageRequestChild* aBackgroundActor);
+
+ void ClearBackgroundActor() {
+ AssertIsOnOwningThread();
+
+ mBackgroundActor = nullptr;
+ }
+
+ void SetResult(nsIVariant* aResult);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_FORWARD_NSIQUOTAREQUESTBASE(RequestBase::)
+ NS_DECL_NSIQUOTAUSAGEREQUEST
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(UsageRequest, RequestBase)
+
+ private:
+ ~UsageRequest();
+
+ virtual void FireCallback() override;
+};
+
+class Request final : public RequestBase, public nsIQuotaRequest {
+ nsCOMPtr<nsIQuotaCallback> mCallback;
+
+ nsCOMPtr<nsIVariant> mResult;
+
+ public:
+ Request();
+
+ explicit Request(nsIPrincipal* aPrincipal);
+
+ explicit Request(nsIQuotaCallback* aCallback);
+
+ void SetResult(nsIVariant* aResult);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_FORWARD_NSIQUOTAREQUESTBASE(RequestBase::)
+ NS_DECL_NSIQUOTAREQUEST
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Request, RequestBase)
+
+ private:
+ ~Request();
+
+ virtual void FireCallback() override;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_UsageRequest_h
diff --git a/dom/quota/QuotaResults.cpp b/dom/quota/QuotaResults.cpp
new file mode 100644
index 0000000000..2b715efa22
--- /dev/null
+++ b/dom/quota/QuotaResults.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QuotaResults.h"
+
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/MacroForEach.h"
+#include "nscore.h"
+
+namespace mozilla::dom::quota {
+
+FullOriginMetadataResult::FullOriginMetadataResult(
+ const FullOriginMetadata& aFullOriginMetadata)
+ : mFullOriginMetadata(aFullOriginMetadata) {}
+
+NS_IMPL_ISUPPORTS(FullOriginMetadataResult, nsIQuotaFullOriginMetadataResult)
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetSuffix(nsACString& aSuffix) {
+ aSuffix = mFullOriginMetadata.mSuffix;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetGroup(nsACString& aGroup) {
+ aGroup = mFullOriginMetadata.mGroup;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetOrigin(nsACString& aOrigin) {
+ aOrigin = mFullOriginMetadata.mOrigin;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetPersistenceType(nsACString& aPersistenceType) {
+ aPersistenceType =
+ PersistenceTypeToString(mFullOriginMetadata.mPersistenceType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetPersisted(bool* aPersisted) {
+ MOZ_ASSERT(aPersisted);
+
+ *aPersisted = mFullOriginMetadata.mPersisted;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FullOriginMetadataResult::GetLastAccessTime(int64_t* aLastAccessTime) {
+ MOZ_ASSERT(aLastAccessTime);
+
+ *aLastAccessTime = mFullOriginMetadata.mLastAccessTime;
+ return NS_OK;
+}
+
+UsageResult::UsageResult(const nsACString& aOrigin, bool aPersisted,
+ uint64_t aUsage, uint64_t aLastAccessed)
+ : mOrigin(aOrigin),
+ mUsage(aUsage),
+ mPersisted(aPersisted),
+ mLastAccessed(aLastAccessed) {}
+
+NS_IMPL_ISUPPORTS(UsageResult, nsIQuotaUsageResult)
+
+NS_IMETHODIMP
+UsageResult::GetOrigin(nsACString& aOrigin) {
+ aOrigin = mOrigin;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageResult::GetPersisted(bool* aPersisted) {
+ MOZ_ASSERT(aPersisted);
+
+ *aPersisted = mPersisted;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageResult::GetUsage(uint64_t* aUsage) {
+ MOZ_ASSERT(aUsage);
+
+ *aUsage = mUsage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+UsageResult::GetLastAccessed(uint64_t* aLastAccessed) {
+ MOZ_ASSERT(aLastAccessed);
+
+ *aLastAccessed = mLastAccessed;
+ return NS_OK;
+}
+
+OriginUsageResult::OriginUsageResult(uint64_t aUsage, uint64_t aFileUsage)
+ : mUsage(aUsage), mFileUsage(aFileUsage) {}
+
+NS_IMPL_ISUPPORTS(OriginUsageResult, nsIQuotaOriginUsageResult)
+
+NS_IMETHODIMP
+OriginUsageResult::GetUsage(uint64_t* aUsage) {
+ MOZ_ASSERT(aUsage);
+
+ *aUsage = mUsage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OriginUsageResult::GetFileUsage(uint64_t* aFileUsage) {
+ MOZ_ASSERT(aFileUsage);
+
+ *aFileUsage = mFileUsage;
+ return NS_OK;
+}
+
+EstimateResult::EstimateResult(uint64_t aUsage, uint64_t aLimit)
+ : mUsage(aUsage), mLimit(aLimit) {}
+
+NS_IMPL_ISUPPORTS(EstimateResult, nsIQuotaEstimateResult)
+
+NS_IMETHODIMP
+EstimateResult::GetUsage(uint64_t* aUsage) {
+ MOZ_ASSERT(aUsage);
+
+ *aUsage = mUsage;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+EstimateResult::GetLimit(uint64_t* aLimit) {
+ MOZ_ASSERT(aLimit);
+
+ *aLimit = mLimit;
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/QuotaResults.h b/dom/quota/QuotaResults.h
new file mode 100644
index 0000000000..e60eff4043
--- /dev/null
+++ b/dom/quota/QuotaResults.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_QuotaResults_h
+#define mozilla_dom_quota_QuotaResults_h
+
+#include <cstdint>
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "nsIQuotaResults.h"
+#include "nsISupports.h"
+#include "nsString.h"
+
+namespace mozilla::dom::quota {
+
+class FullOriginMetadataResult : public nsIQuotaFullOriginMetadataResult {
+ const FullOriginMetadata mFullOriginMetadata;
+
+ public:
+ explicit FullOriginMetadataResult(
+ const FullOriginMetadata& aFullOriginMetadata);
+
+ private:
+ virtual ~FullOriginMetadataResult() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIQUOTAFULLORIGINMETADATARESULT
+};
+
+class UsageResult : public nsIQuotaUsageResult {
+ nsCString mOrigin;
+ uint64_t mUsage;
+ bool mPersisted;
+ uint64_t mLastAccessed;
+
+ public:
+ UsageResult(const nsACString& aOrigin, bool aPersisted, uint64_t aUsage,
+ uint64_t aLastAccessed);
+
+ private:
+ virtual ~UsageResult() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIQUOTAUSAGERESULT
+};
+
+class OriginUsageResult : public nsIQuotaOriginUsageResult {
+ uint64_t mUsage;
+ uint64_t mFileUsage;
+
+ public:
+ OriginUsageResult(uint64_t aUsage, uint64_t aFileUsage);
+
+ private:
+ virtual ~OriginUsageResult() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIQUOTAORIGINUSAGERESULT
+};
+
+class EstimateResult : public nsIQuotaEstimateResult {
+ uint64_t mUsage;
+ uint64_t mLimit;
+
+ public:
+ EstimateResult(uint64_t aUsage, uint64_t aLimit);
+
+ private:
+ virtual ~EstimateResult() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIQUOTAESTIMATERESULT
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_QuotaResults_h
diff --git a/dom/quota/RemoteQuotaObject.cpp b/dom/quota/RemoteQuotaObject.cpp
new file mode 100644
index 0000000000..3fa3370fc1
--- /dev/null
+++ b/dom/quota/RemoteQuotaObject.cpp
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteQuotaObject.h"
+
+#include "mozilla/dom/quota/RemoteQuotaObjectChild.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla::dom::quota {
+
+RemoteQuotaObject::RemoteQuotaObject(RefPtr<RemoteQuotaObjectChild> aActor)
+ : QuotaObject(/* aIsRemote */ true), mActor(std::move(aActor)) {
+ MOZ_COUNT_CTOR(RemoteQuotaObject);
+
+ mActor->SetRemoteQuotaObject(this);
+}
+
+RemoteQuotaObject::~RemoteQuotaObject() {
+ MOZ_COUNT_DTOR(RemoteQuotaObject);
+
+ Close();
+}
+
+void RemoteQuotaObject::ClearActor() {
+ MOZ_ASSERT(mActor);
+
+ mActor = nullptr;
+}
+
+void RemoteQuotaObject::Close() {
+ if (!mActor) {
+ return;
+ }
+
+ MOZ_ASSERT(mActor->GetActorEventTarget()->IsOnCurrentThread());
+
+ mActor->Close();
+ MOZ_ASSERT(!mActor);
+}
+
+const nsAString& RemoteQuotaObject::Path() const { return EmptyString(); }
+
+bool RemoteQuotaObject::MaybeUpdateSize(int64_t aSize, bool aTruncate) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread());
+ MOZ_ASSERT(!GetCurrentThreadWorkerPrivate());
+
+ if (!mActor) {
+ return false;
+ }
+
+ MOZ_ASSERT(mActor->GetActorEventTarget()->IsOnCurrentThread());
+
+ bool result;
+ if (!mActor->SendMaybeUpdateSize(aSize, aTruncate, &result)) {
+ return false;
+ }
+
+ return result;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/RemoteQuotaObject.h b/dom/quota/RemoteQuotaObject.h
new file mode 100644
index 0000000000..48ae0c51f3
--- /dev/null
+++ b/dom/quota/RemoteQuotaObject.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_REMOTEQUOTAOBJECT_H_
+#define DOM_QUOTA_REMOTEQUOTAOBJECT_H_
+
+#include "mozilla/dom/quota/QuotaObject.h"
+
+namespace mozilla::dom::quota {
+
+class RemoteQuotaObjectChild;
+
+// This object can only be used on the thread which it was created on
+class RemoteQuotaObject final : public QuotaObject {
+ public:
+ explicit RemoteQuotaObject(RefPtr<RemoteQuotaObjectChild> aActor);
+
+ NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObject, override)
+
+ void ClearActor();
+
+ void Close();
+
+ const nsAString& Path() const override;
+
+ // This method should never be called on the main thread or the PBackground
+ // thread or a DOM worker thread (It does sync IPC).
+ [[nodiscard]] bool MaybeUpdateSize(int64_t aSize, bool aTruncate) override;
+
+ bool IncreaseSize(int64_t aDelta) override { return false; }
+
+ void DisableQuotaCheck() override {}
+
+ void EnableQuotaCheck() override {}
+
+ private:
+ ~RemoteQuotaObject();
+
+ RefPtr<RemoteQuotaObjectChild> mActor;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_REMOTEQUOTAOBJECT_H_
diff --git a/dom/quota/RemoteQuotaObjectChild.cpp b/dom/quota/RemoteQuotaObjectChild.cpp
new file mode 100644
index 0000000000..2ea0d64978
--- /dev/null
+++ b/dom/quota/RemoteQuotaObjectChild.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteQuotaObjectChild.h"
+
+#include "RemoteQuotaObject.h"
+
+namespace mozilla::dom::quota {
+
+RemoteQuotaObjectChild::RemoteQuotaObjectChild()
+ : mRemoteQuotaObject(nullptr) {}
+
+RemoteQuotaObjectChild::~RemoteQuotaObjectChild() = default;
+
+void RemoteQuotaObjectChild::SetRemoteQuotaObject(
+ RemoteQuotaObject* aRemoteQuotaObject) {
+ MOZ_ASSERT(aRemoteQuotaObject);
+ MOZ_ASSERT(!mRemoteQuotaObject);
+
+ mRemoteQuotaObject = aRemoteQuotaObject;
+}
+
+void RemoteQuotaObjectChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (mRemoteQuotaObject) {
+ mRemoteQuotaObject->ClearActor();
+ mRemoteQuotaObject = nullptr;
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/RemoteQuotaObjectChild.h b/dom/quota/RemoteQuotaObjectChild.h
new file mode 100644
index 0000000000..a052a3748a
--- /dev/null
+++ b/dom/quota/RemoteQuotaObjectChild.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_REMOTEQUOTAOBJECTCHILD_H_
+#define DOM_QUOTA_REMOTEQUOTAOBJECTCHILD_H_
+
+#include "mozilla/dom/quota/PRemoteQuotaObjectChild.h"
+
+namespace mozilla::dom::quota {
+
+class RemoteQuotaObject;
+
+class RemoteQuotaObjectChild : public PRemoteQuotaObjectChild {
+ public:
+ RemoteQuotaObjectChild();
+
+ NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObjectChild, override)
+
+ void SetRemoteQuotaObject(RemoteQuotaObject* aRemoteQuotaObject);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~RemoteQuotaObjectChild();
+
+ // The weak reference is cleared in ActorDestroy.
+ RemoteQuotaObject* MOZ_NON_OWNING_REF mRemoteQuotaObject;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_REMOTEQUOTAOBJECTCHILD_H_
diff --git a/dom/quota/RemoteQuotaObjectParent.cpp b/dom/quota/RemoteQuotaObjectParent.cpp
new file mode 100644
index 0000000000..2624aa8b58
--- /dev/null
+++ b/dom/quota/RemoteQuotaObjectParent.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteQuotaObjectParent.h"
+
+#include "CanonicalQuotaObject.h"
+#include "mozilla/dom/quota/RemoteQuotaObjectParentTracker.h"
+#include "mozilla/ipc/BackgroundParent.h"
+
+namespace mozilla::dom::quota {
+
+RemoteQuotaObjectParent::RemoteQuotaObjectParent(
+ RefPtr<CanonicalQuotaObject> aCanonicalQuotaObject,
+ nsCOMPtr<RemoteQuotaObjectParentTracker> aTracker)
+ : mCanonicalQuotaObject(std::move(aCanonicalQuotaObject)),
+ mTracker(std::move(aTracker)) {}
+
+RemoteQuotaObjectParent::~RemoteQuotaObjectParent() { MOZ_ASSERT(!CanSend()); }
+
+mozilla::ipc::IPCResult RemoteQuotaObjectParent::RecvMaybeUpdateSize(
+ int64_t aSize, bool aTruncate, bool* aResult) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!mozilla::ipc::IsOnBackgroundThread());
+ MOZ_ASSERT(!GetCurrentThreadWorkerPrivate());
+
+ *aResult = mCanonicalQuotaObject->MaybeUpdateSize(aSize, aTruncate);
+ return IPC_OK();
+}
+
+void RemoteQuotaObjectParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // XXX Check that the child properly used `MaybeUpdateSize` before each
+ // write, so the file size on disk matches mCanonicalQuotaObject::mSize.
+ // If the size doesn't match, do necessary adjustments.
+
+ mCanonicalQuotaObject = nullptr;
+
+ if (mTracker) {
+ mTracker->UnregisterRemoteQuotaObjectParent(WrapNotNullUnchecked(this));
+ }
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/RemoteQuotaObjectParent.h b/dom/quota/RemoteQuotaObjectParent.h
new file mode 100644
index 0000000000..4932e37e64
--- /dev/null
+++ b/dom/quota/RemoteQuotaObjectParent.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_REMOTEQUOTAOBJECTPARENT_H_
+#define DOM_QUOTA_REMOTEQUOTAOBJECTPARENT_H_
+
+#include "mozilla/dom/quota/PRemoteQuotaObjectParent.h"
+
+namespace mozilla::dom::quota {
+
+class CanonicalQuotaObject;
+class RemoteQuotaObjectParentTracker;
+
+class RemoteQuotaObjectParent : public PRemoteQuotaObjectParent {
+ public:
+ RemoteQuotaObjectParent(RefPtr<CanonicalQuotaObject> aCanonicalQuotaObject,
+ nsCOMPtr<RemoteQuotaObjectParentTracker> aTracker);
+
+ NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(RemoteQuotaObjectParent, override)
+
+ mozilla::ipc::IPCResult RecvMaybeUpdateSize(int64_t aSize, bool aTruncate,
+ bool* aResult);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ virtual ~RemoteQuotaObjectParent();
+
+ RefPtr<CanonicalQuotaObject> mCanonicalQuotaObject;
+
+ nsCOMPtr<RemoteQuotaObjectParentTracker> mTracker;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_REMOTEQUOTAOBJECTPARENT_H_
diff --git a/dom/quota/RemoteQuotaObjectParentTracker.h b/dom/quota/RemoteQuotaObjectParentTracker.h
new file mode 100644
index 0000000000..d712e08c0d
--- /dev/null
+++ b/dom/quota/RemoteQuotaObjectParentTracker.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_
+#define DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_
+
+#include "mozilla/NotNull.h"
+#include "nsISupports.h"
+
+#define MOZILLA_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID \
+ { \
+ 0x42f96136, 0x5b2b, 0x4487, { \
+ 0xa4, 0x4e, 0x45, 0x0a, 0x00, 0x8f, 0xc5, 0xd4 \
+ } \
+ }
+
+namespace mozilla::dom::quota {
+
+class RemoteQuotaObjectParent;
+
+class RemoteQuotaObjectParentTracker : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(
+ NS_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID)
+
+ virtual void RegisterRemoteQuotaObjectParent(
+ NotNull<RemoteQuotaObjectParent*> aActor) = 0;
+
+ virtual void UnregisterRemoteQuotaObjectParent(
+ NotNull<RemoteQuotaObjectParent*> aActor) = 0;
+
+ protected:
+ virtual ~RemoteQuotaObjectParentTracker() = default;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(
+ RemoteQuotaObjectParentTracker,
+ MOZILLA_DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_IID)
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_REMOTEQUOTAOBJECTPARENTTRACKER_H_
diff --git a/dom/quota/RemoveParen.h b/dom/quota/RemoveParen.h
new file mode 100644
index 0000000000..4dbc9f129b
--- /dev/null
+++ b/dom/quota/RemoveParen.h
@@ -0,0 +1,18 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_REMOVEPAREN_H_
+#define DOM_QUOTA_REMOVEPAREN_H_
+
+// See
+// https://stackoverflow.com/questions/24481810/how-to-remove-the-enclosing-parentheses-with-macro
+#define MOZ_REMOVE_PAREN(X) MOZ_REMOVE_PAREN_HELPER2(MOZ_REMOVE_PAREN_HELPER X)
+#define MOZ_REMOVE_PAREN_HELPER(...) MOZ_REMOVE_PAREN_HELPER __VA_ARGS__
+#define MOZ_REMOVE_PAREN_HELPER2(...) MOZ_REMOVE_PAREN_HELPER3(__VA_ARGS__)
+#define MOZ_REMOVE_PAREN_HELPER3(...) MOZ_REMOVE_PAREN_HELPER4_##__VA_ARGS__
+#define MOZ_REMOVE_PAREN_HELPER4_MOZ_REMOVE_PAREN_HELPER
+
+#endif // DOM_QUOTA_REMOVEPAREN_H_
diff --git a/dom/quota/ResultExtensions.h b/dom/quota/ResultExtensions.h
new file mode 100644
index 0000000000..c19bf92661
--- /dev/null
+++ b/dom/quota/ResultExtensions.h
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_QMRESULTINLINES_H_
+#define DOM_QUOTA_QMRESULTINLINES_H_
+
+#include "nsError.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/Config.h"
+#include "mozilla/dom/quota/RemoveParen.h"
+
+#ifdef QM_ERROR_STACKS_ENABLED
+# include "mozilla/ResultVariant.h"
+#endif
+
+namespace mozilla {
+
+// Allow bool errors to automatically convert to bool values, so MOZ_TRY/QM_TRY
+// can be used in bool returning methods with Result<T, bool> results.
+template <>
+class [[nodiscard]] GenericErrorResult<bool> {
+ bool mErrorValue;
+
+ template <typename V, typename E2>
+ friend class Result;
+
+ public:
+ explicit GenericErrorResult(bool aErrorValue) : mErrorValue(aErrorValue) {
+ MOZ_ASSERT(!aErrorValue);
+ }
+
+ GenericErrorResult(bool aErrorValue, const ErrorPropagationTag&)
+ : GenericErrorResult(aErrorValue) {}
+
+ MOZ_IMPLICIT operator bool() const { return mErrorValue; }
+};
+
+// Allow MOZ_TRY/QM_TRY to handle `bool` values.
+template <typename E = nsresult>
+inline Result<Ok, E> ToResult(bool aValue) {
+ if (aValue) {
+ return Ok();
+ }
+ return Err(ResultTypeTraits<E>::From(NS_ERROR_FAILURE));
+}
+
+constexpr nsresult ToNSResult(nsresult aError) { return aError; }
+
+#ifdef QM_ERROR_STACKS_ENABLED
+
+inline nsresult ToNSResult(const QMResult& aError) { return aError.NSResult(); }
+
+// Allow QMResult errors to use existing stack id and to increase the frame id
+// during error propagation.
+template <>
+class [[nodiscard]] GenericErrorResult<QMResult> {
+ QMResult mErrorValue;
+
+ template <typename V, typename E2>
+ friend class Result;
+
+ public:
+ explicit GenericErrorResult(const QMResult& aErrorValue)
+ : mErrorValue(aErrorValue) {
+ MOZ_ASSERT(NS_FAILED(aErrorValue.NSResult()));
+ }
+
+ explicit GenericErrorResult(QMResult&& aErrorValue)
+ : mErrorValue(std::move(aErrorValue)) {
+ MOZ_ASSERT(NS_FAILED(aErrorValue.NSResult()));
+ }
+
+ explicit GenericErrorResult(const QMResult& aErrorValue,
+ const ErrorPropagationTag&)
+ : GenericErrorResult(aErrorValue.Propagate()) {}
+
+ explicit GenericErrorResult(QMResult&& aErrorValue,
+ const ErrorPropagationTag&)
+ : GenericErrorResult(aErrorValue.Propagate()) {}
+
+ operator QMResult() const { return mErrorValue; }
+
+ operator nsresult() const { return mErrorValue.NSResult(); }
+};
+
+template <>
+struct ResultTypeTraits<QMResult> {
+ static QMResult From(nsresult aValue) { return ToQMResult(aValue); }
+
+ static QMResult From(const QMResult& aValue) { return aValue; }
+
+ static QMResult From(QMResult&& aValue) { return std::move(aValue); }
+};
+
+template <typename E>
+inline Result<Ok, E> ToResult(const QMResult& aValue) {
+ if (NS_FAILED(aValue.NSResult())) {
+ return Err(ResultTypeTraits<E>::From(aValue));
+ }
+ return Ok();
+}
+
+template <typename E>
+inline Result<Ok, E> ToResult(QMResult&& aValue) {
+ if (NS_FAILED(aValue.NSResult())) {
+ return Err(ResultTypeTraits<E>::From(aValue));
+ }
+ return Ok();
+}
+#endif
+
+template <typename E = nsresult, typename V, typename E2>
+inline Result<V, E> ToResultTransform(Result<V, E2>&& aValue) {
+ return std::forward<Result<V, E2>>(aValue).mapErr(
+ [](auto&& err) { return ResultTypeTraits<E>::From(err); });
+}
+
+// TODO: Maybe move this to mfbt/ResultExtensions.h
+template <typename R, typename Func, typename... Args>
+Result<R, nsresult> ToResultGet(const Func& aFunc, Args&&... aArgs) {
+ nsresult rv;
+ R res = aFunc(std::forward<Args>(aArgs)..., &rv);
+ if (NS_FAILED(rv)) {
+ return Err(rv);
+ }
+ return res;
+}
+
+} // namespace mozilla
+
+// TODO: Maybe move this to mfbt/ResultExtensions.h
+#define MOZ_TO_RESULT(expr) ToResult(expr)
+
+#define QM_TO_RESULT(expr) ToResult<QMResult>(expr)
+
+#define QM_TO_RESULT_TRANSFORM(value) ToResultTransform<QMResult>(value)
+
+#define MOZ_TO_RESULT_GET_TYPED(resultType, ...) \
+ ::mozilla::ToResultGet<MOZ_REMOVE_PAREN(resultType)>(__VA_ARGS__)
+
+#define MOZ_TO_RESULT_INVOKE_TYPED(resultType, ...) \
+ ::mozilla::ToResultInvoke<MOZ_REMOVE_PAREN(resultType)>(__VA_ARGS__)
+
+#define QM_TO_RESULT_INVOKE_MEMBER(obj, methodname, ...) \
+ ::mozilla::ToResultInvokeMember<QMResult>( \
+ (obj), &::mozilla::detail::DerefedType<decltype(obj)>::methodname, \
+ ##__VA_ARGS__)
+
+#define QM_TO_RESULT_INVOKE_MEMBER_TYPED(resultType, obj, methodname, ...) \
+ (::mozilla::ToResultInvoke<resultType, QMResult>( \
+ ::std::mem_fn( \
+ &::mozilla::detail::DerefedType<decltype(obj)>::methodname), \
+ (obj), ##__VA_ARGS__))
+
+#endif
diff --git a/dom/quota/ScopedLogExtraInfo.cpp b/dom/quota/ScopedLogExtraInfo.cpp
new file mode 100644
index 0000000000..c3ad5e04e1
--- /dev/null
+++ b/dom/quota/ScopedLogExtraInfo.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScopedLogExtraInfo.h"
+
+namespace mozilla::dom::quota {
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sQueryValue;
+MOZ_THREAD_LOCAL(const nsACString*) ScopedLogExtraInfo::sContextValue;
+
+/* static */
+auto ScopedLogExtraInfo::FindSlot(const char* aTag) {
+ // XXX For now, don't use a real map but just allow the known tag values.
+
+ if (aTag == kTagQuery) {
+ return &sQueryValue;
+ }
+
+ if (aTag == kTagContext) {
+ return &sContextValue;
+ }
+
+ MOZ_CRASH("Unknown tag!");
+}
+
+ScopedLogExtraInfo::~ScopedLogExtraInfo() {
+ if (mTag) {
+ MOZ_ASSERT(&mCurrentValue == FindSlot(mTag)->get(),
+ "Bad scoping of ScopedLogExtraInfo, must not be interleaved!");
+
+ FindSlot(mTag)->set(mPreviousValue);
+ }
+}
+
+ScopedLogExtraInfo::ScopedLogExtraInfo(ScopedLogExtraInfo&& aOther)
+ : mTag(aOther.mTag),
+ mPreviousValue(aOther.mPreviousValue),
+ mCurrentValue(std::move(aOther.mCurrentValue)) {
+ aOther.mTag = nullptr;
+ FindSlot(mTag)->set(&mCurrentValue);
+}
+
+/* static */ ScopedLogExtraInfo::ScopedLogExtraInfoMap
+ScopedLogExtraInfo::GetExtraInfoMap() {
+ // This could be done in a cheaper way, but this is never called on a hot
+ // path, so we anticipate using a real map inside here to make use simpler for
+ // the caller(s).
+
+ ScopedLogExtraInfoMap map;
+ if (XRE_IsParentProcess()) {
+ if (sQueryValue.get()) {
+ map.emplace(kTagQuery, sQueryValue.get());
+ }
+
+ if (sContextValue.get()) {
+ map.emplace(kTagContext, sContextValue.get());
+ }
+ }
+ return map;
+}
+
+/* static */ void ScopedLogExtraInfo::Initialize() {
+ MOZ_ALWAYS_TRUE(sQueryValue.init());
+ MOZ_ALWAYS_TRUE(sContextValue.init());
+}
+
+void ScopedLogExtraInfo::AddInfo() {
+ auto* slot = FindSlot(mTag);
+ MOZ_ASSERT(slot);
+ mPreviousValue = slot->get();
+
+ slot->set(&mCurrentValue);
+}
+#endif
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/ScopedLogExtraInfo.h b/dom/quota/ScopedLogExtraInfo.h
new file mode 100644
index 0000000000..d8412ae5b1
--- /dev/null
+++ b/dom/quota/ScopedLogExtraInfo.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_SCOPEDLOGEXTRAINFO_H_
+#define DOM_QUOTA_SCOPEDLOGEXTRAINFO_H_
+
+#include "mozilla/dom/quota/Config.h"
+
+#include <map>
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ThreadLocal.h"
+#include "nsString.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla::dom::quota {
+
+struct MOZ_STACK_CLASS ScopedLogExtraInfo {
+ static constexpr const char kTagQuery[] = "query";
+ static constexpr const char kTagContext[] = "context";
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ private:
+ static auto FindSlot(const char* aTag);
+
+ public:
+ template <size_t N>
+ ScopedLogExtraInfo(const char (&aTag)[N], const nsACString& aExtraInfo)
+ : mTag{aTag}, mCurrentValue{aExtraInfo} {
+ // Initialize is currently only called in the parent process, we could call
+ // it directly from nsLayoutStatics::Initialize in the content process to
+ // allow use of ScopedLogExtraInfo in that too. The check in GetExtraInfoMap
+ // must be removed then.
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ AddInfo();
+ }
+
+ ~ScopedLogExtraInfo();
+
+ ScopedLogExtraInfo(ScopedLogExtraInfo&& aOther);
+ ScopedLogExtraInfo& operator=(ScopedLogExtraInfo&& aOther) = delete;
+
+ ScopedLogExtraInfo(const ScopedLogExtraInfo&) = delete;
+ ScopedLogExtraInfo& operator=(const ScopedLogExtraInfo&) = delete;
+
+ using ScopedLogExtraInfoMap = std::map<const char*, const nsACString*>;
+ static ScopedLogExtraInfoMap GetExtraInfoMap();
+
+ static void Initialize();
+
+ private:
+ const char* mTag;
+ const nsACString* mPreviousValue;
+ nsCString mCurrentValue;
+
+ static MOZ_THREAD_LOCAL(const nsACString*) sQueryValue;
+ static MOZ_THREAD_LOCAL(const nsACString*) sContextValue;
+
+ void AddInfo();
+#else
+ template <size_t N>
+ ScopedLogExtraInfo(const char (&aTag)[N], const nsACString& aExtraInfo) {}
+
+ // user-defined to silence unused variable warnings
+ ~ScopedLogExtraInfo() {}
+#endif
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_SCOPEDLOGEXTRAINFO_H_
diff --git a/dom/quota/SerializationHelpers.h b/dom/quota/SerializationHelpers.h
new file mode 100644
index 0000000000..e63e3ef67d
--- /dev/null
+++ b/dom/quota/SerializationHelpers.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_SerializationHelpers_h
+#define mozilla_dom_quota_SerializationHelpers_h
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/PersistenceType.h"
+#include "mozilla/OriginAttributes.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::quota::PersistenceType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::quota::PersistenceType,
+ mozilla::dom::quota::PERSISTENCE_TYPE_PERSISTENT,
+ mozilla::dom::quota::PERSISTENCE_TYPE_INVALID> {};
+
+template <>
+struct ParamTraits<mozilla::dom::quota::Client::Type>
+ : public ContiguousEnumSerializer<mozilla::dom::quota::Client::Type,
+ mozilla::dom::quota::Client::IDB,
+ mozilla::dom::quota::Client::TYPE_MAX> {};
+
+template <>
+struct ParamTraits<mozilla::dom::quota::FullOriginMetadata> {
+ using ParamType = mozilla::dom::quota::FullOriginMetadata;
+
+ static void Write(MessageWriter* aWriter, const ParamType& aParam) {
+ WriteParam(aWriter, aParam.mSuffix);
+ WriteParam(aWriter, aParam.mGroup);
+ WriteParam(aWriter, aParam.mOrigin);
+ WriteParam(aWriter, aParam.mPersistenceType);
+ WriteParam(aWriter, aParam.mPersisted);
+ WriteParam(aWriter, aParam.mLastAccessTime);
+ }
+
+ static bool Read(MessageReader* aReader, ParamType* aResult) {
+ return ReadParam(aReader, &aResult->mSuffix) &&
+ ReadParam(aReader, &aResult->mGroup) &&
+ ReadParam(aReader, &aResult->mOrigin) &&
+ ReadParam(aReader, &aResult->mPersistenceType) &&
+ ReadParam(aReader, &aResult->mPersisted) &&
+ ReadParam(aReader, &aResult->mLastAccessTime);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::OriginAttributesPattern> {
+ typedef mozilla::OriginAttributesPattern paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mFirstPartyDomain);
+ WriteParam(aWriter, aParam.mInIsolatedMozBrowser);
+ WriteParam(aWriter, aParam.mPrivateBrowsingId);
+ WriteParam(aWriter, aParam.mUserContextId);
+ WriteParam(aWriter, aParam.mGeckoViewSessionContextId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mFirstPartyDomain) &&
+ ReadParam(aReader, &aResult->mInIsolatedMozBrowser) &&
+ ReadParam(aReader, &aResult->mPrivateBrowsingId) &&
+ ReadParam(aReader, &aResult->mUserContextId) &&
+ ReadParam(aReader, &aResult->mGeckoViewSessionContextId);
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_dom_quota_SerializationHelpers_h
diff --git a/dom/quota/StorageHelpers.cpp b/dom/quota/StorageHelpers.cpp
new file mode 100644
index 0000000000..84cd806297
--- /dev/null
+++ b/dom/quota/StorageHelpers.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/StorageHelpers.h"
+
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+namespace mozilla::dom::quota {
+
+AutoDatabaseAttacher::AutoDatabaseAttacher(
+ nsCOMPtr<mozIStorageConnection> aConnection,
+ nsCOMPtr<nsIFile> aDatabaseFile, const nsLiteralCString& aSchemaName)
+ : mConnection(std::move(aConnection)),
+ mDatabaseFile(std::move(aDatabaseFile)),
+ mSchemaName(aSchemaName),
+ mAttached(false) {}
+
+AutoDatabaseAttacher::~AutoDatabaseAttacher() {
+ if (mAttached) {
+ QM_WARNONLY_TRY(MOZ_TO_RESULT(Detach()));
+ }
+}
+
+nsresult AutoDatabaseAttacher::Attach() {
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mDatabaseFile);
+ MOZ_ASSERT(!mAttached);
+
+#ifdef DEBUG
+ {
+ QM_TRY_INSPECT(const bool& exists,
+ MOZ_TO_RESULT_INVOKE_MEMBER(mDatabaseFile, Exists));
+
+ MOZ_ASSERT(exists);
+ }
+#endif
+
+ QM_TRY_INSPECT(const auto& path, MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsString, mDatabaseFile, GetPath));
+
+ QM_TRY_INSPECT(
+ const auto& stmt,
+ MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(
+ nsCOMPtr<mozIStorageStatement>, mConnection, CreateStatement,
+ "ATTACH DATABASE :path AS "_ns + mSchemaName + ";"_ns));
+
+ QM_TRY(MOZ_TO_RESULT(stmt->BindStringByName("path"_ns, path)));
+ QM_TRY(MOZ_TO_RESULT(stmt->Execute()));
+
+ mAttached = true;
+
+ return NS_OK;
+}
+
+nsresult AutoDatabaseAttacher::Detach() {
+ MOZ_ASSERT(mConnection);
+ MOZ_ASSERT(mAttached);
+
+ QM_TRY(MOZ_TO_RESULT(
+ mConnection->ExecuteSimpleSQL("DETACH DATABASE "_ns + mSchemaName)));
+
+ mAttached = false;
+ return NS_OK;
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/StorageHelpers.h b/dom/quota/StorageHelpers.h
new file mode 100644
index 0000000000..f1830db360
--- /dev/null
+++ b/dom/quota/StorageHelpers.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_QUOTADATABASEHELPER_H
+#define DOM_QUOTA_QUOTADATABASEHELPER_H
+
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozIStorageConnection.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+
+namespace mozilla::dom::quota {
+
+/**
+ * This class provides a RAII wrap of attaching and detaching database
+ * in a given C++ scope. It is guaranteed that an attached database will
+ * be detached even if you have an exception or return early.
+ *
+ * @param aConnection
+ * The connection to attach a database to.
+ * @param aDatabaseFile
+ * The database file to attach.
+ * @param aSchemaName
+ * The schema-name. Can be any string literal which is supported by the
+ * underlying database. For more details about schema-name, see
+ * https://www.sqlite.org/lang_attach.html
+ */
+class MOZ_STACK_CLASS AutoDatabaseAttacher final {
+ public:
+ explicit AutoDatabaseAttacher(nsCOMPtr<mozIStorageConnection> aConnection,
+ nsCOMPtr<nsIFile> aDatabaseFile,
+ const nsLiteralCString& aSchemaName);
+
+ ~AutoDatabaseAttacher();
+
+ AutoDatabaseAttacher() = delete;
+
+ [[nodiscard]] nsresult Attach();
+
+ [[nodiscard]] nsresult Detach();
+
+ private:
+ nsCOMPtr<mozIStorageConnection> mConnection;
+ nsCOMPtr<nsIFile> mDatabaseFile;
+ const nsLiteralCString mSchemaName;
+ bool mAttached;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_QUOTADATABASEHELPER_H
diff --git a/dom/quota/StorageManager.cpp b/dom/quota/StorageManager.cpp
new file mode 100644
index 0000000000..bf05265bc0
--- /dev/null
+++ b/dom/quota/StorageManager.cpp
@@ -0,0 +1,808 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "StorageManager.h"
+#include "fs/FileSystemRequestHandler.h"
+
+#include <cstdint>
+#include <cstdlib>
+#include <utility>
+#include "ErrorList.h"
+#include "fs/FileSystemRequestHandler.h"
+#include "MainThreadUtils.h"
+#include "js/CallArgs.h"
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FileSystemManager.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseWorkerProxy.h"
+#include "mozilla/dom/StorageManagerBinding.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRunnable.h"
+#include "mozilla/dom/WorkerStatus.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "nsContentPermissionHelper.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIGlobalObject.h"
+#include "nsIPrincipal.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaManagerService.h"
+#include "nsIQuotaRequests.h"
+#include "nsIQuotaResults.h"
+#include "nsIVariant.h"
+#include "nsLiteralString.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "nsStringFlags.h"
+#include "nsTLiteralString.h"
+#include "nscore.h"
+
+class JSObject;
+struct JSContext;
+struct nsID;
+
+namespace mozilla {
+class Runnable;
+}
+
+using namespace mozilla::dom::quota;
+
+namespace mozilla::dom {
+
+namespace {
+
+// This class is used to get quota usage, request persist and check persisted
+// status callbacks.
+class RequestResolver final : public nsIQuotaCallback {
+ public:
+ enum Type { Estimate, Persist, Persisted };
+
+ private:
+ class FinishWorkerRunnable;
+
+ // If this resolver was created for a window then mPromise must be non-null.
+ // Otherwise mProxy must be non-null.
+ RefPtr<Promise> mPromise;
+ RefPtr<PromiseWorkerProxy> mProxy;
+
+ nsresult mResultCode;
+ StorageEstimate mStorageEstimate;
+ const Type mType;
+ bool mPersisted;
+
+ public:
+ RequestResolver(Type aType, Promise* aPromise)
+ : mPromise(aPromise),
+ mResultCode(NS_OK),
+ mType(aType),
+ mPersisted(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPromise);
+ }
+
+ RequestResolver(Type aType, PromiseWorkerProxy* aProxy)
+ : mProxy(aProxy), mResultCode(NS_OK), mType(aType), mPersisted(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aProxy);
+ }
+
+ void ResolveOrReject();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIQUOTACALLBACK
+
+ private:
+ ~RequestResolver() = default;
+
+ nsresult GetStorageEstimate(nsIVariant* aResult);
+
+ nsresult GetPersisted(nsIVariant* aResult);
+
+ nsresult OnCompleteInternal(nsIQuotaRequest* aRequest);
+
+ nsresult Finish();
+};
+
+// This class is used to return promise on worker thread.
+class RequestResolver::FinishWorkerRunnable final : public WorkerRunnable {
+ RefPtr<RequestResolver> mResolver;
+
+ public:
+ explicit FinishWorkerRunnable(RequestResolver* aResolver)
+ : WorkerRunnable(aResolver->mProxy->GetWorkerPrivate()),
+ mResolver(aResolver) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aResolver);
+ }
+
+ bool WorkerRun(JSContext* aCx, WorkerPrivate* aWorkerPrivate) override;
+};
+
+class EstimateWorkerMainThreadRunnable final : public WorkerMainThreadRunnable {
+ RefPtr<PromiseWorkerProxy> mProxy;
+
+ public:
+ EstimateWorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate,
+ PromiseWorkerProxy* aProxy)
+ : WorkerMainThreadRunnable(aWorkerPrivate,
+ "StorageManager :: Estimate"_ns),
+ mProxy(aProxy) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aProxy);
+ }
+
+ bool MainThreadRun() override;
+};
+
+class PersistedWorkerMainThreadRunnable final
+ : public WorkerMainThreadRunnable {
+ RefPtr<PromiseWorkerProxy> mProxy;
+
+ public:
+ PersistedWorkerMainThreadRunnable(WorkerPrivate* aWorkerPrivate,
+ PromiseWorkerProxy* aProxy)
+ : WorkerMainThreadRunnable(aWorkerPrivate,
+ "StorageManager :: Persisted"_ns),
+ mProxy(aProxy) {
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+ MOZ_ASSERT(aProxy);
+ }
+
+ bool MainThreadRun() override;
+};
+
+/*******************************************************************************
+ * PersistentStoragePermissionRequest
+ ******************************************************************************/
+
+class PersistentStoragePermissionRequest final
+ : public ContentPermissionRequestBase {
+ RefPtr<Promise> mPromise;
+
+ public:
+ PersistentStoragePermissionRequest(nsIPrincipal* aPrincipal,
+ nsPIDOMWindowInner* aWindow,
+ Promise* aPromise)
+ : ContentPermissionRequestBase(aPrincipal, aWindow,
+ "dom.storageManager"_ns,
+ "persistent-storage"_ns),
+ mPromise(aPromise) {
+ MOZ_ASSERT(aWindow);
+ MOZ_ASSERT(aPromise);
+ }
+
+ nsresult Start();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PersistentStoragePermissionRequest,
+ ContentPermissionRequestBase)
+
+ // nsIContentPermissionRequest
+ NS_IMETHOD Cancel(void) override;
+ NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override;
+
+ private:
+ ~PersistentStoragePermissionRequest() = default;
+};
+
+nsresult Estimate(nsIPrincipal* aPrincipal, nsIQuotaCallback* aCallback,
+ nsIQuotaRequest** aRequest) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aRequest);
+
+ // Firefox and Quota Manager have always used the schemeless origin group
+ // (https://storage.spec.whatwg.org/#schemeless-origin-group) for quota limit
+ // purposes. This has been to prevent a site/eTLD+1 from claiming more than
+ // its fair share of storage through the use of sub-domains. Because the limit
+ // is at the group level and the usage needs to make sense in the context of
+ // that limit, we also expose the group usage. Bug 1374970 reflects this
+ // reality and bug 1305665 tracks our plan to eliminate our use of groups for
+ // this.
+
+ nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ if (NS_WARN_IF(!qms)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIQuotaRequest> request;
+ nsresult rv = qms->Estimate(aPrincipal, getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(request->SetCallback(aCallback));
+
+ request.forget(aRequest);
+ return NS_OK;
+};
+
+nsresult Persisted(nsIPrincipal* aPrincipal, nsIQuotaCallback* aCallback,
+ nsIQuotaRequest** aRequest) {
+ MOZ_ASSERT(aPrincipal);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aRequest);
+
+ nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ if (NS_WARN_IF(!qms)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIQuotaRequest> request;
+ nsresult rv = qms->Persisted(aPrincipal, getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // All the methods in nsIQuotaManagerService shouldn't synchronously fire
+ // any callbacks when they are being executed. Even when a result is ready,
+ // a new runnable should be dispatched to current thread to fire the callback
+ // asynchronously. It's safe to set the callback after we call Persisted().
+ MOZ_ALWAYS_SUCCEEDS(request->SetCallback(aCallback));
+
+ request.forget(aRequest);
+
+ return NS_OK;
+};
+
+already_AddRefed<Promise> ExecuteOpOnMainOrWorkerThread(
+ nsIGlobalObject* aGlobal, RequestResolver::Type aType, ErrorResult& aRv) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT_IF(aType == RequestResolver::Type::Persist, NS_IsMainThread());
+
+ RefPtr<Promise> promise = Promise::Create(aGlobal, aRv);
+ if (NS_WARN_IF(!promise)) {
+ return nullptr;
+ }
+
+ if (NS_IsMainThread()) {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal);
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIPrincipal> principal = doc->NodePrincipal();
+ MOZ_ASSERT(principal);
+
+ // Storage Standard 7. API
+ // If origin is an opaque origin, then reject promise with a TypeError.
+ if (principal->GetIsNullPrincipal()) {
+ switch (aType) {
+ case RequestResolver::Type::Persisted:
+ promise->MaybeRejectWithTypeError(
+ "persisted() called for opaque origin");
+ break;
+ case RequestResolver::Type::Persist:
+ promise->MaybeRejectWithTypeError(
+ "persist() called for opaque origin");
+ break;
+ case RequestResolver::Type::Estimate:
+ promise->MaybeRejectWithTypeError(
+ "estimate() called for opaque origin");
+ break;
+ }
+
+ return promise.forget();
+ }
+
+ switch (aType) {
+ case RequestResolver::Type::Persisted: {
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Persisted, promise);
+
+ RefPtr<nsIQuotaRequest> request;
+ aRv = Persisted(principal, resolver, getter_AddRefs(request));
+
+ break;
+ }
+
+ case RequestResolver::Type::Persist: {
+ RefPtr<PersistentStoragePermissionRequest> request =
+ new PersistentStoragePermissionRequest(principal, window, promise);
+
+ // In private browsing mode, no permission prompt.
+ if (nsContentUtils::IsInPrivateBrowsing(doc)) {
+ aRv = request->Cancel();
+ } else if (!request->CheckPermissionDelegate()) {
+ aRv = request->Cancel();
+ } else {
+ aRv = request->Start();
+ }
+
+ break;
+ }
+
+ case RequestResolver::Type::Estimate: {
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Estimate, promise);
+
+ RefPtr<nsIQuotaRequest> request;
+ aRv = Estimate(principal, resolver, getter_AddRefs(request));
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Invalid aRequest type!");
+ }
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return promise.forget();
+ }
+
+ WorkerPrivate* workerPrivate = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(workerPrivate);
+
+ RefPtr<PromiseWorkerProxy> promiseProxy =
+ PromiseWorkerProxy::Create(workerPrivate, promise);
+ if (NS_WARN_IF(!promiseProxy)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ switch (aType) {
+ case RequestResolver::Type::Estimate: {
+ RefPtr<EstimateWorkerMainThreadRunnable> runnnable =
+ new EstimateWorkerMainThreadRunnable(promiseProxy->GetWorkerPrivate(),
+ promiseProxy);
+ runnnable->Dispatch(Canceling, aRv);
+
+ break;
+ }
+
+ case RequestResolver::Type::Persisted: {
+ RefPtr<PersistedWorkerMainThreadRunnable> runnnable =
+ new PersistedWorkerMainThreadRunnable(
+ promiseProxy->GetWorkerPrivate(), promiseProxy);
+ runnnable->Dispatch(Canceling, aRv);
+
+ break;
+ }
+
+ default:
+ MOZ_CRASH("Invalid aRequest type");
+ }
+
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return promise.forget();
+};
+
+} // namespace
+
+/*******************************************************************************
+ * Local class implementations
+ ******************************************************************************/
+
+void RequestResolver::ResolveOrReject() {
+ class MOZ_STACK_CLASS AutoCleanup final {
+ RefPtr<PromiseWorkerProxy> mProxy;
+
+ public:
+ explicit AutoCleanup(PromiseWorkerProxy* aProxy) : mProxy(aProxy) {
+ MOZ_ASSERT(aProxy);
+ }
+
+ ~AutoCleanup() {
+ MOZ_ASSERT(mProxy);
+
+ mProxy->CleanUp();
+ }
+ };
+
+ RefPtr<Promise> promise;
+ Maybe<AutoCleanup> autoCleanup;
+
+ if (mPromise) {
+ promise = mPromise;
+ } else {
+ MOZ_ASSERT(mProxy);
+
+ promise = mProxy->WorkerPromise();
+
+ // Only clean up for worker case.
+ autoCleanup.emplace(mProxy);
+ }
+
+ MOZ_ASSERT(promise);
+
+ if (mType == Type::Estimate) {
+ if (NS_SUCCEEDED(mResultCode)) {
+ promise->MaybeResolve(mStorageEstimate);
+ } else {
+ promise->MaybeRejectWithTypeError(
+ "Internal error while estimating storage usage");
+ }
+
+ return;
+ }
+
+ MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted);
+
+ if (NS_SUCCEEDED(mResultCode)) {
+ promise->MaybeResolve(mPersisted);
+ } else {
+ promise->MaybeResolve(false);
+ }
+}
+
+NS_IMPL_ISUPPORTS(RequestResolver, nsIQuotaCallback)
+
+nsresult RequestResolver::GetStorageEstimate(nsIVariant* aResult) {
+ MOZ_ASSERT(aResult);
+ MOZ_ASSERT(mType == Type::Estimate);
+
+ MOZ_ASSERT(aResult->GetDataType() == nsIDataType::VTYPE_INTERFACE_IS);
+
+ nsID* iid;
+ nsCOMPtr<nsISupports> supports;
+ nsresult rv = aResult->GetAsInterface(&iid, getter_AddRefs(supports));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ free(iid);
+
+ nsCOMPtr<nsIQuotaEstimateResult> estimateResult = do_QueryInterface(supports);
+ MOZ_ASSERT(estimateResult);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ estimateResult->GetUsage(&mStorageEstimate.mUsage.Construct()));
+
+ MOZ_ALWAYS_SUCCEEDS(
+ estimateResult->GetLimit(&mStorageEstimate.mQuota.Construct()));
+
+ return NS_OK;
+}
+
+nsresult RequestResolver::GetPersisted(nsIVariant* aResult) {
+ MOZ_ASSERT(aResult);
+ MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted);
+
+#ifdef DEBUG
+ uint16_t dataType = aResult->GetDataType();
+#endif
+
+ if (mType == Type::Persist) {
+ MOZ_ASSERT(dataType == nsIDataType::VTYPE_VOID);
+
+ mPersisted = true;
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(dataType == nsIDataType::VTYPE_BOOL);
+
+ bool persisted;
+ nsresult rv = aResult->GetAsBool(&persisted);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mPersisted = persisted;
+ return NS_OK;
+}
+
+nsresult RequestResolver::OnCompleteInternal(nsIQuotaRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+
+ nsresult resultCode;
+ nsresult rv = aRequest->GetResultCode(&resultCode);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (NS_FAILED(resultCode)) {
+ return resultCode;
+ }
+
+ nsCOMPtr<nsIVariant> result;
+ rv = aRequest->GetResult(getter_AddRefs(result));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mType == Type::Estimate) {
+ rv = GetStorageEstimate(result);
+ } else {
+ MOZ_ASSERT(mType == Type::Persist || mType == Type::Persisted);
+
+ rv = GetPersisted(result);
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+nsresult RequestResolver::Finish() {
+ // In a main thread request.
+ if (!mProxy) {
+ MOZ_ASSERT(mPromise);
+
+ ResolveOrReject();
+ return NS_OK;
+ }
+
+ {
+ // In a worker thread request.
+ MutexAutoLock lock(mProxy->Lock());
+
+ if (NS_WARN_IF(mProxy->CleanedUp())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FinishWorkerRunnable> runnable = new FinishWorkerRunnable(this);
+ if (NS_WARN_IF(!runnable->Dispatch())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RequestResolver::OnComplete(nsIQuotaRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+
+ mResultCode = OnCompleteInternal(aRequest);
+
+ nsresult rv = Finish();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+bool RequestResolver::FinishWorkerRunnable::WorkerRun(
+ JSContext* aCx, WorkerPrivate* aWorkerPrivate) {
+ MOZ_ASSERT(aCx);
+ MOZ_ASSERT(aWorkerPrivate);
+ aWorkerPrivate->AssertIsOnWorkerThread();
+
+ MOZ_ASSERT(mResolver);
+ mResolver->ResolveOrReject();
+
+ return true;
+}
+
+bool EstimateWorkerMainThreadRunnable::MainThreadRun() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPrincipal> principal;
+
+ {
+ MutexAutoLock lock(mProxy->Lock());
+ if (mProxy->CleanedUp()) {
+ return true;
+ }
+ principal = mProxy->GetWorkerPrivate()->GetPrincipal();
+ }
+
+ MOZ_ASSERT(principal);
+
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Estimate, mProxy);
+
+ RefPtr<nsIQuotaRequest> request;
+ nsresult rv = Estimate(principal, resolver, getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return true;
+}
+
+bool PersistedWorkerMainThreadRunnable::MainThreadRun() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPrincipal> principal;
+
+ {
+ MutexAutoLock lock(mProxy->Lock());
+ if (mProxy->CleanedUp()) {
+ return true;
+ }
+ principal = mProxy->GetWorkerPrivate()->GetPrincipal();
+ }
+
+ MOZ_ASSERT(principal);
+
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Persisted, mProxy);
+
+ RefPtr<nsIQuotaRequest> request;
+ nsresult rv = Persisted(principal, resolver, getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult PersistentStoragePermissionRequest::Start() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ PromptResult pr;
+#ifdef MOZ_WIDGET_ANDROID
+ // on Android calling `ShowPrompt` here calls
+ // `nsContentPermissionUtils::AskPermission` once, and a response of
+ // `PromptResult::Pending` calls it again. This results in multiple requests
+ // for storage access, so we check the prompt prefs only to ensure we only
+ // request it once.
+ pr = CheckPromptPrefs();
+#else
+ nsresult rv = ShowPrompt(pr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+#endif
+ if (pr == PromptResult::Granted) {
+ return Allow(JS::UndefinedHandleValue);
+ }
+ if (pr == PromptResult::Denied) {
+ return Cancel();
+ }
+
+ return nsContentPermissionUtils::AskPermission(this, mWindow);
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ PersistentStoragePermissionRequest, ContentPermissionRequestBase)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PersistentStoragePermissionRequest,
+ ContentPermissionRequestBase, mPromise)
+
+NS_IMETHODIMP
+PersistentStoragePermissionRequest::Cancel() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPromise);
+
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Persisted, mPromise);
+
+ RefPtr<nsIQuotaRequest> request;
+
+ return Persisted(mPrincipal, resolver, getter_AddRefs(request));
+}
+
+NS_IMETHODIMP
+PersistentStoragePermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<RequestResolver> resolver =
+ new RequestResolver(RequestResolver::Type::Persist, mPromise);
+
+ nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ if (NS_WARN_IF(!qms)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<nsIQuotaRequest> request;
+
+ nsresult rv = qms->Persist(mPrincipal, getter_AddRefs(request));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(request->SetCallback(resolver));
+
+ return NS_OK;
+}
+
+/*******************************************************************************
+ * StorageManager
+ ******************************************************************************/
+
+StorageManager::StorageManager(nsIGlobalObject* aGlobal) : mOwner(aGlobal) {
+ MOZ_ASSERT(aGlobal);
+}
+
+StorageManager::~StorageManager() { Shutdown(); }
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StorageManager)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(StorageManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(StorageManager)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StorageManager)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(StorageManager)
+ tmp->Shutdown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOwner)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(StorageManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOwner)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFileSystemManager)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+void StorageManager::Shutdown() {
+ if (mFileSystemManager) {
+ mFileSystemManager->Shutdown();
+ mFileSystemManager = nullptr;
+ }
+}
+
+already_AddRefed<FileSystemManager> StorageManager::GetFileSystemManager() {
+ if (!mFileSystemManager) {
+ MOZ_ASSERT(mOwner);
+
+ mFileSystemManager = MakeRefPtr<FileSystemManager>(mOwner, this);
+ }
+
+ return do_AddRef(mFileSystemManager);
+}
+
+// WebIDL Boilerplate
+
+JSObject* StorageManager::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return StorageManager_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// WebIDL Interface
+
+already_AddRefed<Promise> StorageManager::Persisted(ErrorResult& aRv) {
+ MOZ_ASSERT(mOwner);
+
+ return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Persisted,
+ aRv);
+}
+
+already_AddRefed<Promise> StorageManager::Persist(ErrorResult& aRv) {
+ MOZ_ASSERT(mOwner);
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_PERSIST_COUNT, 1);
+ return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Persist,
+ aRv);
+}
+
+already_AddRefed<Promise> StorageManager::Estimate(ErrorResult& aRv) {
+ MOZ_ASSERT(mOwner);
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::NAVIGATOR_STORAGE_ESTIMATE_COUNT,
+ 1);
+ return ExecuteOpOnMainOrWorkerThread(mOwner, RequestResolver::Type::Estimate,
+ aRv);
+}
+
+already_AddRefed<Promise> StorageManager::GetDirectory(ErrorResult& aRv) {
+ return RefPtr(GetFileSystemManager())->GetDirectory(aRv);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/quota/StorageManager.h b/dom/quota/StorageManager.h
new file mode 100644
index 0000000000..8622c1eb5d
--- /dev/null
+++ b/dom/quota/StorageManager.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_StorageManager_h
+#define mozilla_dom_StorageManager_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+
+class JSObject;
+class nsIGlobalObject;
+struct JSContext;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class FileSystemManager;
+class Promise;
+struct StorageEstimate;
+
+class StorageManager final : public nsISupports, public nsWrapperCache {
+ nsCOMPtr<nsIGlobalObject> mOwner;
+
+ public:
+ explicit StorageManager(nsIGlobalObject* aGlobal);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StorageManager)
+
+ void Shutdown();
+
+ already_AddRefed<FileSystemManager> GetFileSystemManager();
+
+ // WebIDL Boilerplate
+ nsIGlobalObject* GetParentObject() const { return mOwner; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL Interface
+ already_AddRefed<Promise> Persisted(ErrorResult& aRv);
+
+ already_AddRefed<Promise> Persist(ErrorResult& aRv);
+
+ already_AddRefed<Promise> Estimate(ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetDirectory(ErrorResult& aRv);
+
+ private:
+ ~StorageManager();
+
+ RefPtr<FileSystemManager> mFileSystemManager;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_StorageManager_h
diff --git a/dom/quota/UsageInfo.h b/dom/quota/UsageInfo.h
new file mode 100644
index 0000000000..7f7bf5f85a
--- /dev/null
+++ b/dom/quota/UsageInfo.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_quota_usageinfo_h__
+#define mozilla_dom_quota_usageinfo_h__
+
+#include <cstdint>
+#include <utility>
+#include "mozilla/CheckedInt.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla::dom::quota {
+
+enum struct UsageKind { Database, File };
+
+namespace detail {
+inline void AddCapped(Maybe<uint64_t>& aValue, const Maybe<uint64_t> aDelta) {
+ if (aDelta.isSome()) {
+ CheckedUint64 value = aValue.valueOr(0);
+
+ value += aDelta.value();
+
+ aValue = Some(value.isValid() ? value.value() : UINT64_MAX);
+ }
+}
+
+template <UsageKind Kind>
+struct Usage {
+ explicit Usage(Maybe<uint64_t> aValue = Nothing{}) : mValue(aValue) {}
+
+ Maybe<uint64_t> GetValue() const { return mValue; }
+
+ Usage& operator+=(const Usage aDelta) {
+ AddCapped(mValue, aDelta.mValue);
+
+ return *this;
+ }
+
+ Usage operator+(const Usage aDelta) const {
+ Usage res = *this;
+ res += aDelta;
+ return res;
+ }
+
+ private:
+ Maybe<uint64_t> mValue;
+};
+} // namespace detail
+
+using DatabaseUsageType = detail::Usage<UsageKind::Database>;
+using FileUsageType = detail::Usage<UsageKind::File>;
+
+class UsageInfo final {
+ public:
+ UsageInfo() = default;
+
+ explicit UsageInfo(const DatabaseUsageType aUsage) : mDatabaseUsage(aUsage) {}
+
+ explicit UsageInfo(const FileUsageType aUsage) : mFileUsage(aUsage) {}
+
+ UsageInfo operator+(const UsageInfo& aUsageInfo) {
+ UsageInfo res = *this;
+ res += aUsageInfo;
+ return res;
+ }
+
+ UsageInfo& operator+=(const UsageInfo& aUsageInfo) {
+ mDatabaseUsage += aUsageInfo.mDatabaseUsage;
+ mFileUsage += aUsageInfo.mFileUsage;
+ return *this;
+ }
+
+ UsageInfo& operator+=(const DatabaseUsageType aUsage) {
+ mDatabaseUsage += aUsage;
+ return *this;
+ }
+
+ UsageInfo& operator+=(const FileUsageType aUsage) {
+ mFileUsage += aUsage;
+ return *this;
+ }
+
+ Maybe<uint64_t> DatabaseUsage() const { return mDatabaseUsage.GetValue(); }
+
+ Maybe<uint64_t> FileUsage() const { return mFileUsage.GetValue(); }
+
+ Maybe<uint64_t> TotalUsage() const {
+ Maybe<uint64_t> res = mDatabaseUsage.GetValue();
+ detail::AddCapped(res, FileUsage());
+ return res;
+ }
+
+ private:
+ DatabaseUsageType mDatabaseUsage;
+ FileUsageType mFileUsage;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // mozilla_dom_quota_usageinfo_h__
diff --git a/dom/quota/components.conf b/dom/quota/components.conf
new file mode 100644
index 0000000000..b55e861e0f
--- /dev/null
+++ b/dom/quota/components.conf
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{b6f2f870-b0bc-4a1a-9c40-02cc171adb5b}',
+ 'contract_ids': ['@mozilla.org/network/protocol;1?name=indexeddb'],
+ 'type': 'nsIndexedDBProtocolHandler',
+ 'headers': ['/dom/quota/nsIndexedDBProtocolHandler.h'],
+ 'protocol_config': {
+ 'scheme': 'indexeddb',
+ 'flags': [
+ 'URI_STD',
+ 'URI_DANGEROUS_TO_LOAD',
+ 'URI_DOES_NOT_RETURN_DATA',
+ 'URI_NON_PERSISTABLE',
+ ],
+ },
+ },
+]
diff --git a/dom/quota/moz.build b/dom/quota/moz.build
new file mode 100644
index 0000000000..b4416a4bed
--- /dev/null
+++ b/dom/quota/moz.build
@@ -0,0 +1,120 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Storage: Quota Manager")
+
+DIRS += ["test"]
+
+XPIDL_SOURCES += [
+ "nsIQuotaCallbacks.idl",
+ "nsIQuotaManagerService.idl",
+ "nsIQuotaRequests.idl",
+ "nsIQuotaResults.idl",
+]
+
+XPIDL_MODULE = "dom_quota"
+
+EXPORTS.mozilla.dom += [
+ "QMResult.h",
+ "StorageManager.h",
+]
+
+EXPORTS.mozilla.dom.quota += [
+ "ActorsParent.h",
+ "Assertions.h",
+ "AssertionsImpl.h",
+ "CachingDatabaseConnection.h",
+ "CheckedUnsafePtr.h",
+ "CipherStrategy.h",
+ "Client.h",
+ "ClientImpl.h",
+ "CommonMetadata.h",
+ "Config.h",
+ "DecryptingInputStream.h",
+ "DecryptingInputStream_impl.h",
+ "DirectoryLock.h",
+ "DummyCipherStrategy.h",
+ "EncryptedBlock.h",
+ "EncryptingOutputStream.h",
+ "EncryptingOutputStream_impl.h",
+ "FileStreams.h",
+ "FirstInitializationAttempts.h",
+ "FirstInitializationAttemptsImpl.h",
+ "ForwardDecls.h",
+ "InitializationTypes.h",
+ "IPCStreamCipherStrategy.h",
+ "NSSCipherStrategy.h",
+ "OriginScope.h",
+ "PersistenceType.h",
+ "QuotaCommon.h",
+ "QuotaManager.h",
+ "QuotaManagerImpl.h",
+ "QuotaManagerService.h",
+ "QuotaObject.h",
+ "RemoteQuotaObjectChild.h",
+ "RemoteQuotaObjectParent.h",
+ "RemoteQuotaObjectParentTracker.h",
+ "RemoveParen.h",
+ "ResultExtensions.h",
+ "ScopedLogExtraInfo.h",
+ "SerializationHelpers.h",
+ "StorageHelpers.h",
+ "UsageInfo.h",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+UNIFIED_SOURCES += [
+ "ActorsChild.cpp",
+ "ActorsParent.cpp",
+ "CachingDatabaseConnection.cpp",
+ "CanonicalQuotaObject.cpp",
+ "Client.cpp",
+ "ClientUsageArray.cpp",
+ "DecryptingInputStream.cpp",
+ "DirectoryLockImpl.cpp",
+ "EncryptingOutputStream.cpp",
+ "FileStreams.cpp",
+ "GroupInfo.cpp",
+ "GroupInfoPair.cpp",
+ "InitializationTypes.cpp",
+ "nsIndexedDBProtocolHandler.cpp",
+ "NSSCipherStrategy.cpp",
+ "OriginInfo.cpp",
+ "PersistenceType.cpp",
+ "QMResult.cpp",
+ "QuotaCommon.cpp",
+ "QuotaManagerService.cpp",
+ "QuotaObject.cpp",
+ "QuotaRequests.cpp",
+ "QuotaResults.cpp",
+ "RemoteQuotaObject.cpp",
+ "RemoteQuotaObjectChild.cpp",
+ "RemoteQuotaObjectParent.cpp",
+ "ScopedLogExtraInfo.cpp",
+ "StorageHelpers.cpp",
+ "StorageManager.cpp",
+]
+
+IPDL_SOURCES += [
+ "IPCQuotaObject.ipdlh",
+ "PQuota.ipdl",
+ "PQuotaRequest.ipdl",
+ "PQuotaUsageRequest.ipdl",
+ "PRemoteQuotaObject.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/caps",
+ "/dom/fs/include",
+]
diff --git a/dom/quota/nsIQuotaCallbacks.idl b/dom/quota/nsIQuotaCallbacks.idl
new file mode 100644
index 0000000000..7c53db20c0
--- /dev/null
+++ b/dom/quota/nsIQuotaCallbacks.idl
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIQuotaRequest;
+interface nsIQuotaUsageRequest;
+
+[scriptable, function, uuid(c8a21a2a-17b9-4b63-ad95-e0fbcff5de18)]
+interface nsIQuotaUsageCallback : nsISupports
+{
+ void onUsageResult(in nsIQuotaUsageRequest aRequest);
+};
+
+[scriptable, function, uuid(a08a28e2-5a74-4c84-8070-ed45a07eb013)]
+interface nsIQuotaCallback : nsISupports
+{
+ void onComplete(in nsIQuotaRequest aRequest);
+};
diff --git a/dom/quota/nsIQuotaManagerService.idl b/dom/quota/nsIQuotaManagerService.idl
new file mode 100644
index 0000000000..9855283a5f
--- /dev/null
+++ b/dom/quota/nsIQuotaManagerService.idl
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIPrincipal;
+interface nsIQuotaRequest;
+interface nsIQuotaCallback;
+interface nsIQuotaUsageCallback;
+interface nsIQuotaUsageRequest;
+
+[scriptable, builtinclass, uuid(1b3d0a38-8151-4cf9-89fa-4f92c2ef0e7e)]
+interface nsIQuotaManagerService : nsISupports
+{
+ /**
+ * Asynchronously retrieves storage name and returns it as a plain string.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ storageName();
+
+ /**
+ * Check if storage is initialized.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ storageInitialized();
+
+ /**
+ * Check if temporary storage is initialized.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ temporaryStorageInitialized();
+
+ /**
+ * Initializes storage directory. This can be used in tests to verify
+ * upgrade methods.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ init();
+
+ /**
+ * Initializes temporary storage. This can be used in tests to verify
+ * temporary storage initialization.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ initTemporaryStorage();
+
+ /**
+ * Initializes persistent origin directory for the given origin. This can be
+ * used in tests to verify origin initialization.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ *
+ * @param aPrincipal
+ * A principal for the origin whose directory is to be initialized.
+ */
+ [must_use] nsIQuotaRequest
+ initializePersistentOrigin(in nsIPrincipal aPrincipal);
+
+ /**
+ * Initializes temporary origin directory for the given origin. This can be
+ * used in tests to verify origin initialization.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ *
+ * @param aPersistenceType
+ * A string that tells what persistence type of origin will be
+ * initialized (temporary or default).
+ *
+ * @param aPrincipal
+ * A principal for the origin whose directory is to be initialized.
+ */
+ [must_use] nsIQuotaRequest
+ initializeTemporaryOrigin(in ACString aPersistenceType,
+ in nsIPrincipal aPrincipal);
+
+ /**
+ * Gets full origin metadata cached in memory for the given persistence type
+ * and origin.
+ *
+ * NOTE: This operation may still be delayed by other operations on the QM
+ * I/O thread that are peforming I/O.
+ *
+ * @param aPersistenceType
+ * A string that tells what persistence type will be used for getting
+ * the metadata (either "temporary" or "default").
+ * @param aPrincipal
+ * A principal that tells which origin will be used for getting the
+ * metadata.
+ */
+ [must_use] nsIQuotaRequest
+ getFullOriginMetadata(in ACString aPersistenceType,
+ in nsIPrincipal aPrincipal);
+
+ /**
+ * Schedules an asynchronous callback that will inspect all origins and
+ * return the total amount of disk space being used by storages for each
+ * origin separately.
+ *
+ * @param aCallback
+ * The callback that will be called when the usage is available.
+ * @param aGetAll
+ * An optional boolean to indicate inspection of all origins,
+ * including internal ones.
+ */
+ [must_use] nsIQuotaUsageRequest
+ getUsage(in nsIQuotaUsageCallback aCallback,
+ [optional] in boolean aGetAll);
+
+ /**
+ * Schedules an asynchronous callback that will return the total amount of
+ * disk space being used by storages for the given origin.
+ *
+ * @param aPrincipal
+ * A principal for the origin whose usage is being queried.
+ * @param aCallback
+ * The callback that will be called when the usage is available.
+ * @param aFromMemory
+ * An optional flag to indicate whether the cached usage should be
+ * obtained. The default value is false. Note that this operation may
+ * still be delayed by other operations on the QM I/O thread that are
+ * peforming I/O.
+ * Note: Origin usage here represents total usage of an origin. However,
+ * cached usage here represents only non-persistent usage of an origin.
+ */
+ [must_use] nsIQuotaUsageRequest
+ getUsageForPrincipal(in nsIPrincipal aPrincipal,
+ in nsIQuotaUsageCallback aCallback,
+ [optional] in boolean aFromMemory);
+
+ /**
+ * Asynchronously lists all origins and returns them as plain strings.
+ */
+ [must_use] nsIQuotaRequest
+ listOrigins();
+
+ /**
+ * Removes all storages. The files may not be deleted immediately depending
+ * on prohibitive concurrent operations.
+ * Be careful, this removes *all* the data that has ever been stored!
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ clear();
+
+ /**
+ * Removes all storages stored for the given pattern. The files may not be
+ * deleted immediately depending on prohibitive concurrent operations. In
+ * terms of locks, it will get an exclusive multi directory lock for given
+ * pattern. For example, given pattern {"userContextId":1007} and set of 3
+ * origins ["http://www.mozilla.org^userContextId=1007",
+ * "http://www.example.org^userContextId=1007",
+ * "http://www.example.org^userContextId=1008"], the method will only lock 2
+ * origins ["http://www.mozilla.org^userContextId=1007",
+ * "http://www.example.org^userContextId=1007"].
+ *
+ * @param aPattern
+ * A pattern for the origins whose storages are to be cleared.
+ * Currently this is expected to be a JSON representation of the
+ * OriginAttributesPatternDictionary defined in ChromeUtils.webidl.
+ */
+ [must_use] nsIQuotaRequest
+ clearStoragesForOriginAttributesPattern(in AString aPattern);
+
+ /**
+ * Removes all storages stored for the given principal. The files may not be
+ * deleted immediately depending on prohibitive concurrent operations.
+ *
+ * @param aPrincipal
+ * A principal for the origin whose storages are to be cleared.
+ * @param aPersistenceType
+ * An optional string that tells what persistence type of storages
+ * will be cleared. If omitted (or void), all persistence types will
+ * be cleared for the principal. If a single persistence type
+ * ("persistent", "temporary", or "default") is provided, then only
+ * that persistence directory will be considered. Note that
+ * "persistent" is different than being "persisted" via persist() and
+ * is only for chrome principals. See bug 1354500 for more info.
+ * In general, null is the right thing to pass here.
+ * @param aClientType
+ * An optional string that tells what client type of storages
+ * will be cleared. If omitted (or void), all client types will be
+ * cleared for the principal. If a single client type is provided
+ * from Client.h, then only that client's storage will be cleared.
+ * If you want to clear multiple client types (but not all), then you
+ * must call this method multiple times.
+ * @param aClearAll
+ * An optional boolean to indicate clearing all storages under the
+ * given origin.
+ */
+ [must_use] nsIQuotaRequest
+ clearStoragesForPrincipal(in nsIPrincipal aPrincipal,
+ [optional] in ACString aPersistenceType,
+ [optional] in AString aClientType,
+ [optional] in boolean aClearAll);
+
+ /**
+ * Resets quota and storage management. This can be used to force
+ * reinitialization of the temp storage, for example when the pref for
+ * overriding the temp storage limit has changed.
+ * Be carefull, this invalidates all live storages!
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ */
+ [must_use] nsIQuotaRequest
+ reset();
+
+ /**
+ * Resets all storages stored for the given principal.
+ *
+ * If the dom.quotaManager.testing preference is not true the call will be
+ * a no-op.
+ *
+ * @param aPrincipal
+ * A principal for the origin whose storages are to be reset.
+ * @param aPersistenceType
+ * An optional string that tells what persistence type of storages
+ * will be reset. If omitted (or void), all persistence types will
+ * be cleared for the principal. If a single persistence type
+ * ("persistent", "temporary", or "default") is provided, then only
+ * that persistence directory will be considered. Note that
+ * "persistent" is different than being "persisted" via persist() and
+ * is only for chrome principals. See bug 1354500 for more info.
+ * In general, null is the right thing to pass here.
+ * @param aClientType
+ * An optional string that tells what client type of storages
+ * will be reset. If omitted (or void), all client types will be
+ * cleared for the principal. If a single client type is provided
+ * from Client.h, then only that client's storage will be cleared.
+ * If you want to clear multiple client types (but not all), then you
+ * must call this method multiple times.
+ */
+ [must_use] nsIQuotaRequest
+ resetStoragesForPrincipal(in nsIPrincipal aPrincipal,
+ [optional] in ACString aPersistenceType,
+ [optional] in AString aClientType);
+
+ /**
+ * Check if given origin is persisted.
+ *
+ * @param aPrincipal
+ * A principal for the origin which we want to check.
+ */
+ [must_use] nsIQuotaRequest
+ persisted(in nsIPrincipal aPrincipal);
+
+ /**
+ * Persist given origin.
+ *
+ * @param aPrincipal
+ * A principal for the origin which we want to persist.
+ */
+ [must_use] nsIQuotaRequest
+ persist(in nsIPrincipal aPrincipal);
+
+ /**
+ * Given an origin, asynchronously calculate its group quota usage and quota
+ * limit. An origin's group is the set of all origins that share the same
+ * eTLD+1. This method is intended to be used for our implementation of the
+ * StorageManager.estimate() method. When we fix bug 1305665 and stop tracking
+ * quota limits on a group basis, this method will switch to operating on
+ * origins. Callers should strongly consider whether they want to be using
+ * getUsageForPrincipal() instead.
+ *
+ * This mechanism uses cached quota values and does not perform any I/O on its
+ * own, but it may be delayed by QuotaManager operations that do need to
+ * perform I/O on the QuotaManager I/O thread.
+ *
+ * @param aPrincipal
+ * A principal for the origin (group) which we want to estimate.
+ */
+ [must_use] nsIQuotaRequest
+ estimate(in nsIPrincipal aPrincipal);
+};
diff --git a/dom/quota/nsIQuotaRequests.idl b/dom/quota/nsIQuotaRequests.idl
new file mode 100644
index 0000000000..8b05717e83
--- /dev/null
+++ b/dom/quota/nsIQuotaRequests.idl
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIPrincipal;
+interface nsIQuotaCallback;
+interface nsIQuotaUsageCallback;
+interface nsIVariant;
+
+[scriptable, uuid(9af54222-0407-48fd-a4ab-9457c986fc49)]
+interface nsIQuotaRequestBase : nsISupports
+{
+ readonly attribute nsIPrincipal principal;
+
+ [must_use] readonly attribute nsresult resultCode;
+
+ [must_use] readonly attribute ACString resultName;
+};
+
+[scriptable, uuid(166e28e6-cf6d-4927-a6d7-b51bca9d3469)]
+interface nsIQuotaUsageRequest : nsIQuotaRequestBase
+{
+ // The result can contain one of these types:
+ // array of nsIQuotaUsageResult
+ // nsIQuotaOriginUsageResult
+ [must_use] readonly attribute nsIVariant result;
+
+ attribute nsIQuotaUsageCallback callback;
+
+ [must_use] void
+ cancel();
+};
+
+[scriptable, uuid(22890e3e-ff25-4372-9684-d901060e2f6c)]
+interface nsIQuotaRequest : nsIQuotaRequestBase
+{
+ // The result can contain one of these types:
+ // void
+ // bool
+ // nsIQuotaEstimateResult
+ // array of strings
+ [must_use] readonly attribute nsIVariant result;
+
+ attribute nsIQuotaCallback callback;
+};
diff --git a/dom/quota/nsIQuotaResults.idl b/dom/quota/nsIQuotaResults.idl
new file mode 100644
index 0000000000..bf9ab322b8
--- /dev/null
+++ b/dom/quota/nsIQuotaResults.idl
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, function, uuid(4d8def75-014e-404d-bf30-e2f0Bfcf4d89)]
+interface nsIQuotaFullOriginMetadataResult : nsISupports
+{
+ readonly attribute ACString suffix;
+
+ readonly attribute ACString group;
+
+ readonly attribute ACString origin;
+
+ readonly attribute ACString persistenceType;
+
+ readonly attribute boolean persisted;
+
+ readonly attribute long long lastAccessTime;
+};
+
+[scriptable, function, uuid(d8c9328b-9aa8-4f5d-90e6-482de4a6d5b8)]
+interface nsIQuotaUsageResult : nsISupports
+{
+ readonly attribute ACString origin;
+
+ readonly attribute boolean persisted;
+
+ readonly attribute unsigned long long usage;
+
+ readonly attribute unsigned long long lastAccessed;
+};
+
+[scriptable, function, uuid(96df03d2-116a-493f-bb0b-118c212a6b32)]
+interface nsIQuotaOriginUsageResult : nsISupports
+{
+ readonly attribute unsigned long long usage;
+
+ readonly attribute unsigned long long fileUsage;
+};
+
+[scriptable, function, uuid(9827fc69-7ea9-48ef-b30d-2e2ae0451ec0)]
+interface nsIQuotaEstimateResult : nsISupports
+{
+ readonly attribute unsigned long long usage;
+
+ readonly attribute unsigned long long limit;
+};
diff --git a/dom/quota/nsIndexedDBProtocolHandler.cpp b/dom/quota/nsIndexedDBProtocolHandler.cpp
new file mode 100644
index 0000000000..10ca278098
--- /dev/null
+++ b/dom/quota/nsIndexedDBProtocolHandler.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim:set ts=2 sts=2 sw=2 et cin:
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIndexedDBProtocolHandler.h"
+
+#include <cstdint>
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/MacroForEach.h"
+#include "nsIWeakReference.h"
+#include "nsStandardURL.h"
+#include "nsStringFwd.h"
+#include "nscore.h"
+
+using namespace mozilla::net;
+
+nsIndexedDBProtocolHandler::nsIndexedDBProtocolHandler() = default;
+
+nsIndexedDBProtocolHandler::~nsIndexedDBProtocolHandler() = default;
+
+NS_IMPL_ISUPPORTS(nsIndexedDBProtocolHandler, nsIProtocolHandler,
+ nsISupportsWeakReference)
+
+NS_IMETHODIMP nsIndexedDBProtocolHandler::GetScheme(nsACString& aScheme) {
+ aScheme.AssignLiteral("indexeddb");
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsIndexedDBProtocolHandler::NewChannel(nsIURI* aURI, nsILoadInfo* aLoadInfo,
+ nsIChannel** _retval) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP
+nsIndexedDBProtocolHandler::AllowPort(int32_t aPort, const char* aScheme,
+ bool* _retval) {
+ *_retval = false;
+ return NS_OK;
+}
diff --git a/dom/quota/nsIndexedDBProtocolHandler.h b/dom/quota/nsIndexedDBProtocolHandler.h
new file mode 100644
index 0000000000..00cc554cb4
--- /dev/null
+++ b/dom/quota/nsIndexedDBProtocolHandler.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIndexedDBProtocolHandler_h
+#define nsIndexedDBProtocolHandler_h
+
+#include "nsIProtocolHandler.h"
+#include "nsISupports.h"
+#include "nsWeakReference.h"
+
+class nsIndexedDBProtocolHandler final : public nsIProtocolHandler,
+ public nsSupportsWeakReference {
+ public:
+ nsIndexedDBProtocolHandler();
+
+ private:
+ ~nsIndexedDBProtocolHandler();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIPROTOCOLHANDLER
+};
+
+#endif // nsIndexedDBProtocolHandler_h
diff --git a/dom/quota/scripts/analyze_qm_failures.py b/dom/quota/scripts/analyze_qm_failures.py
new file mode 100755
index 0000000000..f4afd64ab4
--- /dev/null
+++ b/dom/quota/scripts/analyze_qm_failures.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python3
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import getopt
+import sys
+
+import fn_anchors
+import stackanalysis
+import utils
+
+"""
+The analysis is based on stack frames of the following form:
+
+[
+ {
+ "event_timeabs": 1617121013137,
+ "session_startabs": 1617120840000,
+ "build_id": "20210329095128",
+ "client_id": "0013a68f-9893-461a-93d4-2d7a2f85583f",
+ "session_id": "8cd37159-bd5c-481c-99ad-9eace9ea726a",
+ "seq": 1,
+ "context": "Initialization::TemporaryStorage",
+ "source_file": "dom/localstorage/ActorsParent.cpp",
+ "source_line": "1018",
+ "severity": "ERROR",
+ "result": "NS_ERROR_FILE_NOT_FOUND"
+ },
+...
+]
+
+The location of the input file is expected to be found in the
+last item of the list inside qmexecutions.json.
+"""
+
+
+def usage():
+ print("analyze_qm_faiures.py -w <workdir=.>")
+ print("")
+ print("Analyzes the results from fetch_qm_failures.py's JSON file.")
+ print(
+ "Writes out several JSON results as files and a bugzilla markup table on stdout."
+ )
+ print("-w <workdir>: Working directory, default is '.'")
+ sys.exit(2)
+
+
+days = 1
+workdir = "."
+
+try:
+ opts, args = getopt.getopt(sys.argv[1:], "w:", ["workdir="])
+ for opt, arg in opts:
+ if opt == "-w":
+ workdir = arg
+except getopt.GetoptError:
+ usage()
+
+run = utils.getLastRunFromExecutionFile(workdir)
+if "numrows" not in run:
+ print("No previous execution from fetch_qm_failures.py found.")
+ usage()
+if run["numrows"] == 0:
+ print("The last execution yielded no result.")
+
+infile = run["rawfile"]
+
+
+def getFname(prefix):
+ return "{}/{}_until_{}.json".format(workdir, prefix, run["lasteventtime"])
+
+
+# read rows from JSON
+rows = utils.readJSONFile(getFname("qmrows"))
+print("Found {} rows of data.".format(len(rows)))
+rows = stackanalysis.sanitize(rows)
+
+# enrich rows with hg locations
+buildids = stackanalysis.extractBuildIDs(rows)
+utils.fetchBuildRevisions(buildids)
+stackanalysis.constructHGLinks(buildids, rows)
+
+# transform rows to unique stacks
+raw_stacks = stackanalysis.collectRawStacks(rows)
+all_stacks = stackanalysis.mergeEqualStacks(raw_stacks)
+
+# enrich with function anchors
+for stack in all_stacks:
+ for frame in stack["frames"]:
+ frame["anchor"] = "{}:{}".format(
+ frame["source_file"], fn_anchors.getFunctionName(frame["location"])
+ )
+
+# separate stacks for relevance
+error_stacks = []
+warn_stacks = []
+info_stacks = []
+abort_stacks = []
+stackanalysis.filterStacksForPropagation(
+ all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks
+)
+run["errorfile"] = getFname("qmerrors")
+utils.writeJSONFile(run["errorfile"], error_stacks)
+run["warnfile"] = getFname("qmwarnings")
+utils.writeJSONFile(run["warnfile"], warn_stacks)
+run["infofile"] = getFname("qminfo")
+utils.writeJSONFile(run["infofile"], info_stacks)
+run["abortfile"] = getFname("qmabort")
+utils.writeJSONFile(run["abortfile"], abort_stacks)
+utils.updateLastRunToExecutionFile(workdir, run)
+
+
+# print results to stdout
+print("Found {} error stacks.".format(len(error_stacks)))
+print("Found {} warning stacks.".format(len(warn_stacks)))
+print("Found {} info stacks.".format(len(info_stacks)))
+print("Found {} aborted stacks.".format(len(abort_stacks)))
+print("")
+print("Error stacks:")
+print(stackanalysis.printStacks(error_stacks))
+print("")
+print("Error stacks grouped by anchors:")
+anchors = stackanalysis.groupStacksForAnchors(error_stacks)
+anchornames = list(anchors.keys())
+for a in anchornames:
+ print(stackanalysis.printStacks(anchors[a]["stacks"]))
+ print("")
+print("")
+print("Warning stacks:")
+print(stackanalysis.printStacks(warn_stacks))
+print("")
+print("Info stacks:")
+print(stackanalysis.printStacks(info_stacks))
+print("")
+print("Aborted stacks:")
+print(stackanalysis.printStacks(abort_stacks))
diff --git a/dom/quota/scripts/fetch_fn_names.sh b/dom/quota/scripts/fetch_fn_names.sh
new file mode 100755
index 0000000000..6d3a3c4d23
--- /dev/null
+++ b/dom/quota/scripts/fetch_fn_names.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This script assumes to have rust-code-analysis-cli in the path.
+HG_URL=$1
+TEMPDIR=/tmp/fetch_fn_names_$BASHPID
+TEMPSRC=$TEMPDIR/src
+mkdir $TEMPDIR
+echo "" > $TEMPDIR/empty.json
+HG_URL=`echo $HG_URL | sed 's/annotate/raw-file/g'`
+wget -q -O "$TEMPSRC" $HG_URL
+rust-code-analysis-cli -m -O json -o "$TEMPDIR" -p "$TEMPSRC"
+CONTENT=`cat $TEMPDIR/*.json`
+rm -rf $TEMPDIR
+echo $CONTENT
diff --git a/dom/quota/scripts/fetch_qm_failures.py b/dom/quota/scripts/fetch_qm_failures.py
new file mode 100755
index 0000000000..546b213582
--- /dev/null
+++ b/dom/quota/scripts/fetch_qm_failures.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import getopt
+import sys
+
+import telemetry
+import utils
+
+"""
+The analysis is based on the following query:
+https://sql.telemetry.mozilla.org/queries/78691/source?p_day=28&p_month=03&p_year=2021
+
+SELECT UNIX_MILLIS(timestamp) AS submit_timeabs,
+ session_start_time,
+ submission_date,
+ build_id,
+ client_id,
+ session_id,
+ event_timestamp,
+ CAST(mozfun.map.get_key(event_map_values, "seq") AS INT64) AS seq,
+ mozfun.map.get_key(event_map_values, "context") AS context,
+ mozfun.map.get_key(event_map_values, "source_file") AS source_file,
+ mozfun.map.get_key(event_map_values, "source_line") AS source_line,
+ mozfun.map.get_key(event_map_values, "severity") AS severity,
+ mozfun.map.get_key(event_map_values, "result") AS result,
+FROM telemetry.events
+WHERE submission_date >= CAST('{{ year }}-{{ month }}-{{ day }}' AS DATE)
+ AND event_category='dom.quota.try'
+ AND build_id >= '{{ build }}'
+ AND UNIX_MILLIS(timestamp) > {{ last }}
+ORDER BY submit_timeabs
+LIMIT 600000
+
+We fetch events in chronological order, as we want to keep track of where we already
+arrived with our analysis. To accomplish this we write our runs into qmexecutions.json.
+
+[
+ {
+ "workdir": ".",
+ "daysback": 1,
+ "numrows": 17377,
+ "lasteventtime": 1617303855145,
+ "rawfile": "./qmrows_until_1617303855145.json"
+ }
+]
+
+lasteventtime is the highest value of event_timeabs we found in our data.
+
+analyze_qm_failures instead needs the rows to be ordered by
+client_id, session_id, thread_id, submit_timeabs, seq
+Thus we sort the rows accordingly before writing them.
+"""
+
+
+def usage():
+ print(
+ "fetch_qm_faiures.py -k <apikey> -b <minimum build=20210329000000>"
+ "-d <days back=1> -l <last event time> -w <workdir=.>"
+ )
+ print("")
+ print("Invokes the query 78691 and stores the result in a JSON file.")
+ print("-k <apikey>: Your personal telemetry API key (not the query key!).")
+ print("-d <daysback>: Number of days to go back. Default is 1.")
+ print("-b <minimum build>: The lowest build id we will fetch data for.")
+ print("-l <last event time>: Fetch only events after this. Default is 0.")
+ print("-w <workdir>: Working directory, default is '.'")
+ sys.exit(2)
+
+
+days = 1
+lasteventtime = 0
+key = "undefined"
+workdir = "."
+minbuild = "20210329000000"
+
+try:
+ opts, args = getopt.getopt(
+ sys.argv[1:],
+ "k:b:d:l:w:",
+ ["key=", "build=", "days=", "lasteventtime=", "workdir="],
+ )
+ for opt, arg in opts:
+ if opt == "-k":
+ key = arg
+ elif opt == "-d":
+ days = int(arg)
+ elif opt == "-l":
+ lasteventtime = int(arg)
+ elif opt == "-b":
+ minbuild = arg
+ elif opt == "-w":
+ workdir = arg
+except getopt.GetoptError:
+ usage()
+
+if key == "undefined":
+ usage()
+
+start = utils.dateback(days)
+year = start.year
+month = start.month
+day = start.day
+
+run = {}
+lastrun = utils.getLastRunFromExecutionFile(workdir)
+if "lasteventtime" in lastrun:
+ lasteventtime = lastrun["lasteventtime"]
+run["workdir"] = workdir
+run["daysback"] = days
+run["minbuild"] = minbuild
+
+p_params = "p_year={:04d}&p_month={:02d}&p_day={:02d}&p_build={}" "&p_last={}".format(
+ year, month, day, minbuild, lasteventtime
+)
+print(p_params)
+result = telemetry.query(key, 78691, p_params)
+rows = result["query_result"]["data"]["rows"]
+run["numrows"] = len(rows)
+if run["numrows"] > 0:
+ lasteventtime = telemetry.getLastEventTimeAbs(rows)
+ run["lasteventtime"] = lasteventtime
+ rows.sort(
+ key=lambda row: "{}.{}.{}.{}.{:06d}".format(
+ row["client_id"],
+ row["session_id"],
+ row["seq"] >> 32, # thread_id
+ row["submit_timeabs"],
+ row["seq"] & 0x00000000FFFFFFFF, # seq,
+ ),
+ reverse=False,
+ )
+ outfile = "{}/qmrows_until_{}.json".format(workdir, lasteventtime)
+ utils.writeJSONFile(outfile, rows)
+ run["rawfile"] = outfile
+else:
+ print("No results found, maybe next time.")
+ run["lasteventtime"] = lasteventtime
+
+utils.addNewRunToExecutionFile(workdir, run)
diff --git a/dom/quota/scripts/fn_anchors.py b/dom/quota/scripts/fn_anchors.py
new file mode 100644
index 0000000000..eeaf43764c
--- /dev/null
+++ b/dom/quota/scripts/fn_anchors.py
@@ -0,0 +1,68 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import subprocess
+
+cached_functions = {}
+
+
+def getMetricsJson(src_url):
+ if src_url.startswith("http"):
+ print("Fetching source for function extraction: {}".format(src_url))
+ metrics = subprocess.check_output(["./fetch_fn_names.sh", src_url])
+ else:
+ print("Skip fetching source: {}".format(src_url))
+ metrics = ""
+
+ try:
+ return json.loads(metrics)
+ except ValueError:
+ return {"kind": "empty", "name": "anonymous", "spaces": []}
+
+
+def getSpaceFunctionsRecursive(metrics_space):
+ functions = []
+ if (
+ metrics_space["kind"] == "function"
+ and metrics_space["name"]
+ and metrics_space["name"] != "<anonymous>"
+ ):
+ functions.append(
+ {
+ "name": metrics_space["name"],
+ "start_line": int(metrics_space["start_line"]),
+ "end_line": int(metrics_space["end_line"]),
+ }
+ )
+ for space in metrics_space["spaces"]:
+ functions += getSpaceFunctionsRecursive(space)
+ return functions
+
+
+def getSourceFunctions(src_url):
+ if src_url not in cached_functions:
+ metrics_space = getMetricsJson(src_url)
+ cached_functions[src_url] = getSpaceFunctionsRecursive(metrics_space)
+
+ return cached_functions[src_url]
+
+
+def getFunctionName(location):
+ location.replace("annotate", "raw-file")
+ pieces = location.split("#l")
+ src_url = pieces[0]
+ line = int(pieces[1])
+ closest_name = "<Unknown {}>".format(line)
+ closest_start = 0
+ functions = getSourceFunctions(src_url)
+ for fn in functions:
+ if (
+ fn["start_line"] > closest_start
+ and line >= fn["start_line"]
+ and line <= fn["end_line"]
+ ):
+ closest_start = fn["start_line"]
+ closest_name = fn["name"]
+ return closest_name
diff --git a/dom/quota/scripts/stackanalysis.py b/dom/quota/scripts/stackanalysis.py
new file mode 100644
index 0000000000..f0363c5e1f
--- /dev/null
+++ b/dom/quota/scripts/stackanalysis.py
@@ -0,0 +1,396 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+# There seem to be sometimes identical events recorded twice by telemetry
+def sanitize(rows):
+ newrows = []
+ pcid = "unset"
+ psid = "unset"
+ pseq = "unset"
+ for row in rows:
+ cid = row["client_id"]
+ sid = row["session_id"]
+ seq = row["seq"]
+ if cid != pcid or sid != psid or seq != pseq:
+ newrows.append(row)
+ pcid = cid
+ psid = sid
+ pseq = seq
+
+ return newrows
+
+
+# Given a set of rows, find all distinct build ids
+def extractBuildIDs(rows):
+ buildids = {}
+ for row in rows:
+ id = row["build_id"]
+ if id in buildids:
+ buildids[id] = buildids[id] + 1
+ else:
+ buildids[id] = 1
+ return buildids
+
+
+# Given a set of build ids and rows, enrich each row by an hg link.
+# Relys on the result of utils.fetchBuildRevisions in buildids.
+def constructHGLinks(buildids, rows):
+ for row in rows:
+ id = row["build_id"]
+ if id in buildids:
+ row["location"] = (
+ buildids[id] + "/" + row["source_file"] + "#l" + row["source_line"]
+ )
+ else:
+ row["location"] = id + "/" + row["source_file"] + "#l" + row["source_line"]
+
+
+topmost_stackframes = set()
+delta_frames = {}
+
+
+def isTopmostFrame(frame):
+ f = (frame["location"], frame["result"])
+ return f in topmost_stackframes
+
+
+def addTopmostFrame(frame):
+ f = (frame["location"], frame["result"])
+ if not isTopmostFrame(frame):
+ # print("Found new topmost frame {}.".format(frame))
+ topmost_stackframes.add(f)
+ frame["topmost"] = True
+
+
+def addFrameDelta(frame1, frame2):
+ if frame1["client_id"] != frame2["client_id"]:
+ return
+ if frame1["session_id"] != frame2["session_id"]:
+ return
+
+ fkey = "{}:{}-{}:{}".format(
+ frame2["location"], frame2["result"], frame1["location"], frame1["result"]
+ )
+ if fkey not in delta_frames:
+ fdelta = {"delta_sum": 0, "delta_cnt": 0}
+ fdelta["prev_row"] = frame1
+ fdelta["candidate"] = frame2
+ delta_frames[fkey] = fdelta
+
+ fdelta = delta_frames[fkey]
+ etv1 = frame1["event_timestamp"]
+ etv2 = frame2["event_timestamp"]
+ if isinstance(etv1, int) and isinstance(etv2, int) and etv2 > etv1:
+ delta = etv2 - etv1
+ fdelta["delta_sum"] = fdelta["delta_sum"] + delta
+ fdelta["delta_cnt"] = fdelta["delta_cnt"] + 1
+
+
+# There can be outliers in terms of time distance between two stack frames
+# that belong to the same propagation stack. In order to not increase the
+# risk that one outlier breaks thousands of stacks, we check for the average
+# time distance.
+def checkAverageFrameTimeDeltas(rows, max_delta):
+ # print("checkAverageFrameTimeDeltas")
+ prev_row = None
+ for row in rows:
+ if "topmost" in row or not row["session_complete"]:
+ prev_row = None
+ continue
+
+ if prev_row:
+ addFrameDelta(prev_row, row)
+ prev_row = row
+
+ for fd in delta_frames:
+ sum = delta_frames[fd]["delta_sum"]
+ cnt = delta_frames[fd]["delta_cnt"]
+ if cnt > 0 and (sum / cnt) > max_delta:
+ # print(delta_frames[fd])
+ addTopmostFrame(delta_frames[fd]["candidate"])
+
+
+# A topmost frame is considered to initiate a new raw stack. We collect all
+# candidates before we actually apply them. This implies, that we should run
+# this function on a "large enough" sample of rows to be more accurate.
+# As a side effect, we mark all rows that are part of a "complete" session
+# (a session, that started within our data scope).
+def collectTopmostFrames(rows):
+ prev_cid = "unset"
+ prev_sid = "unset"
+ prev_tid = "unset"
+ prev_ctx = "unset"
+ prev_sev = "ERROR"
+ session_complete = False
+ after_severity_downgrade = False
+ for row in rows:
+ cid = row["client_id"]
+ sid = row["session_id"]
+ tid = row["seq"] >> 32 # thread_id
+ ctx = row["context"]
+ seq = row["seq"] & 0x00000000FFFFFFFF # seq
+ sev = row["severity"]
+
+ # If we have a new session, ensure it is complete from start,
+ # otherwise we will ignore it entirely.
+ if cid != prev_cid or sid != prev_sid or tid != prev_tid:
+ if seq == 1:
+ session_complete = True
+ else:
+ session_complete = False
+ row["session_complete"] = session_complete
+ if session_complete:
+ # If we change client, session, thread or context, we can be sure to have
+ # a new topmost frame.
+ if (
+ seq == 1
+ or cid != prev_cid
+ or sid != prev_sid
+ or tid != prev_tid
+ or ctx != prev_ctx
+ ):
+ addTopmostFrame(row)
+ after_severity_downgrade = False
+ # We do not expect a non-error to be ever upgraded to an error
+ elif sev == "ERROR" and prev_sev != "ERROR":
+ addTopmostFrame(row)
+ after_severity_downgrade = False
+ # If we just had a severity downgrade, we assume that we wanted
+ # to break the error propagation after this point and split, too
+ elif after_severity_downgrade:
+ addTopmostFrame(row)
+ after_severity_downgrade = False
+ elif prev_sev == "ERROR" and sev != "ERROR":
+ after_severity_downgrade = True
+
+ prev_cid = cid
+ prev_sid = sid
+ prev_tid = tid
+ prev_ctx = ctx
+ prev_sev = sev
+
+ # Should be ms. We've seen quite some runtime between stackframes in the
+ # wild. We might want to consider to make this configurable. In general
+ # we prefer local context over letting slip through some topmost frame
+ # unrecognized, assuming that fixing the issues one by one they will
+ # uncover them succesively. This is achieved by a rather high delta value.
+ max_avg_delta = 200
+ checkAverageFrameTimeDeltas(rows, max_avg_delta)
+
+
+def getFrameKey(frame):
+ return "{}.{}|".format(frame["location"], frame["result"])
+
+
+def getStackKey(stack):
+ stack_key = ""
+ for frame in stack["frames"]:
+ stack_key += getFrameKey(frame)
+ return hash(stack_key)
+
+
+# A "raw stack" is a list of frames, that:
+# - share the same build_id (implicitely through location)
+# - share the same client_id
+# - share the same session_id
+# - has a growing sequence number
+# - stops at the first downgrade of severity from ERROR to else
+# - XXX: contains each location at most once (no recursion)
+# - appears to be in a reasonable short timeframe
+# Calculates also a hash key to identify identical stacks
+def collectRawStacks(rows):
+ collectTopmostFrames(rows)
+ raw_stacks = []
+ stack = {
+ "stack_id": "unset",
+ "client_id": "unset",
+ "session_id": "unset",
+ "submit_timeabs": "unset",
+ "frames": [{"location": "unset"}],
+ }
+ stack_id = 1
+ first = True
+ for row in rows:
+ if isTopmostFrame(row):
+ if not first:
+ stack["stack_key"] = getStackKey(stack)
+ raw_stacks.append(stack)
+ stack_id += 1
+ stack = {
+ "stack_id": stack_id,
+ "client_id": row["client_id"],
+ "session_id": row["session_id"],
+ "submit_timeabs": row["submit_timeabs"],
+ "context": row["context"],
+ "frames": [],
+ }
+
+ stack["frames"].append(
+ {
+ "location": row["location"],
+ "source_file": row["source_file"],
+ "source_line": row["source_line"],
+ "seq": row["seq"],
+ "severity": row["severity"],
+ "result": row["result"],
+ }
+ )
+ first = False
+
+ return raw_stacks
+
+
+# Merge all stacks that have the same hash key and count occurences.
+# Relys on the ordering per client_id/session_id for correct counting.
+def mergeEqualStacks(raw_stacks):
+ merged_stacks = {}
+ last_client_id = "none"
+ last_session_id = "none"
+ for stack in raw_stacks:
+ stack_key = stack["stack_key"]
+ merged_stack = stack
+ if stack_key in merged_stacks:
+ merged_stack = merged_stacks[stack_key]
+ if stack["client_id"] != last_client_id:
+ last_client_id = stack["client_id"]
+ merged_stack["client_count"] += 1
+ if stack["session_id"] != last_session_id:
+ last_session_id = stack["session_id"]
+ merged_stack["session_count"] += 1
+ merged_stack["hit_count"] += 1
+ else:
+ merged_stack["client_count"] = 1
+ last_client_id = merged_stack["client_id"]
+ merged_stack["session_count"] = 1
+ last_session_id = merged_stack["session_id"]
+ merged_stack["hit_count"] = 1
+ merged_stacks[stack_key] = merged_stack
+
+ merged_list = list(merged_stacks.values())
+ merged_list.sort(key=lambda x: x.get("hit_count"), reverse=True)
+ return merged_list
+
+
+# Split the list of stacks into:
+# - aborted (has at least one frame with NS_ERROR_ABORT)
+# - info/warning (has at least one frame with that severity)
+# - error (has only error frames)
+def filterStacksForPropagation(
+ all_stacks, error_stacks, warn_stacks, info_stacks, abort_stacks
+):
+ for stack in all_stacks:
+ warn = list(filter(lambda x: x["severity"] == "WARNING", stack["frames"]))
+ info = list(filter(lambda x: x["severity"] == "INFO", stack["frames"]))
+ abort = list(filter(lambda x: x["result"] == "NS_ERROR_ABORT", stack["frames"]))
+ if len(abort) > 0:
+ abort_stacks.append(stack)
+ elif len(info) > 0:
+ info_stacks.append(stack)
+ elif len(warn) > 0:
+ warn_stacks.append(stack)
+ else:
+ error_stacks.append(stack)
+
+
+# Bugzilla comment markup
+def printStacks(stacks):
+ row_format = "{} | {} | {} | {} | {}\n"
+ out = ""
+ out += row_format.format("Clients", "Sessions", "Hits", "Anchor (Context)", "Stack")
+ out += row_format.format("-------", "--------", "----", "----------------", "-----")
+ for stack in stacks:
+ framestr = ""
+ first = True
+ for frame in stack["frames"]:
+ if not first:
+ framestr += " <- "
+ framestr += "[{}#{}:{}]({})".format(
+ frame["source_file"],
+ frame["source_line"],
+ frame["result"],
+ frame["location"],
+ )
+ first = False
+ out += row_format.format(
+ stack["client_count"],
+ stack["session_count"],
+ stack["hit_count"],
+ "{} ({})".format(stack["frames"][0]["anchor"], stack["context"]),
+ framestr,
+ )
+
+ return out
+
+
+def groupStacksForAnchors(stacks):
+ anchors = {}
+ for stack in stacks:
+ anchor_name = stack["frames"][0]["anchor"]
+ if anchor_name in anchors:
+ anchors[anchor_name]["stacks"].append(stack)
+ else:
+ anchor = {"anchor": anchor_name, "stacks": [stack]}
+ anchors[anchor_name] = anchor
+ return anchors
+
+
+"""
+def getSummaryForAnchor(anchor):
+ return "[QM_TRY] Errors in function {}".format(anchor)
+
+
+def searchBugForAnchor(bugzilla_key, anchor):
+ summary = getSummaryForAnchor(anchor)
+ bug_url = "https://bugzilla.mozilla.org/rest/bug?" \
+ "summary={}&api_key={}".format(summary, bugzilla_key)
+ return requests.get(url=bug_url).json()["bugs"]
+
+
+def createBugForAnchor(bugzilla_key, anchor):
+ summary = getSummaryForAnchor(anchor)
+ bug_url = "https://bugzilla.mozilla.org/rest/bug?" \
+ "Bugzilla_api_key={}".format(bugzilla_key)
+ body = {
+ "product" : "Core",
+ "component" : "Storage: Quota Manager",
+ "version" : "unspecified",
+ "summary" : summary,
+ "description" : "This bug collects errors reported by QM_TRY"
+ "macros for function {}.".format(anchor),
+ }
+ resp = requests.post(url=bug_url, json=body)
+ if resp.status_code != 200:
+ print(resp)
+ return 0
+ id = resp.json()["id"]
+ print("Added new bug {}:".format(id))
+ return id
+
+
+def ensureBugForAnchor(bugzilla_key, anchor):
+ buglist = searchBugForAnchor(bugzilla_key, anchor)
+ if (len(buglist) > 0):
+ id = buglist[0]["id"]
+ print("Found existing bug {}:".format(id))
+ return id
+ return createBugForAnchor(bugzilla_key, anchor)
+
+
+def addCommentForAnchor(bugzilla_key, anchor, stacks):
+ id = ensureBugForAnchor(bugzilla_key, anchor)
+ if (id <= 0):
+ print("Unable to create a bug for {}.".format(anchor))
+ return
+ comment = printStacks(stacks)
+ print("")
+ print("Add comment to bug {}:".format(id))
+ print(comment)
+
+
+def addCommentsForStacks(bugzilla_key, stacks):
+ anchors = groupStacksForAnchors(stacks)
+ for anchor in anchors:
+ addCommentForAnchor(bugzilla_key, anchors[anchor]["anchor"], anchors[anchor]["stacks"])
+"""
diff --git a/dom/quota/scripts/telemetry.py b/dom/quota/scripts/telemetry.py
new file mode 100644
index 0000000000..a62abd62b1
--- /dev/null
+++ b/dom/quota/scripts/telemetry.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import time
+
+import requests
+
+
+def query(key, query, p_params):
+ headers = {"Authorization": "Key {}".format(key)}
+ start_url = "https://sql.telemetry.mozilla.org/api/" "queries/{}/refresh?{}".format(
+ query, p_params
+ )
+ print(start_url)
+ resp = requests.post(url=start_url, headers=headers)
+ job = resp.json()["job"]
+ jid = job["id"]
+ print("Started job {}".format(jid))
+
+ poll_url = "https://sql.telemetry.mozilla.org/api/" "jobs/{}".format(jid)
+ print(poll_url)
+ poll = True
+ status = 0
+ qresultid = 0
+ while poll:
+ print(".", end="", flush=True)
+ resp = requests.get(url=poll_url, headers=headers)
+ status = resp.json()["job"]["status"]
+ if status > 2:
+ # print(resp.json())
+ poll = False
+ qresultid = resp.json()["job"]["query_result_id"]
+ else:
+ time.sleep(0.2)
+ print(".")
+ print("Finished with status {}".format(status))
+
+ if status == 3:
+ fetch_url = (
+ "https://sql.telemetry.mozilla.org/api/"
+ "queries/78691/results/{}.json".format(qresultid)
+ )
+ print(fetch_url)
+ resp = requests.get(url=fetch_url, headers=headers)
+ return resp.json()
+
+ return {"query_result": {"data": {"rows": {}}}}
+
+
+def getLastEventTimeAbs(rows):
+ if len(rows) == 0:
+ return 0
+ return rows[len(rows) - 1]["submit_timeabs"]
diff --git a/dom/quota/scripts/utils.py b/dom/quota/scripts/utils.py
new file mode 100644
index 0000000000..d5322728a5
--- /dev/null
+++ b/dom/quota/scripts/utils.py
@@ -0,0 +1,89 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import datetime
+import json
+
+import requests
+
+
+def readJSONFile(FileName):
+ f = open(FileName, "r")
+ p = json.load(f)
+ f.close()
+ return p
+
+
+def writeJSONFile(FileName, Content):
+ with open(FileName, "w") as outfile:
+ json.dump(Content, outfile, indent=4)
+
+
+def dateback(days):
+ today = datetime.date.today()
+ delta = datetime.timedelta(days)
+ return today - delta
+
+
+def lastweek():
+ today = datetime.date.today()
+ delta = datetime.timedelta(days=7)
+ return today - delta
+
+
+# Given a set of build ids, fetch the repository base URL for each id.
+def fetchBuildRevisions(buildids):
+ buildhub_url = "https://buildhub.moz.tools/api/search"
+ delids = {}
+ for bid in buildids:
+ print("Fetching revision for build {}.".format(bid))
+ body = {"size": 1, "query": {"term": {"build.id": bid}}}
+ resp = requests.post(url=buildhub_url, json=body)
+ hits = resp.json()["hits"]["hits"]
+ if len(hits) > 0:
+ buildids[bid] = (
+ hits[0]["_source"]["source"]["repository"]
+ + "/annotate/"
+ + hits[0]["_source"]["source"]["revision"]
+ )
+ else:
+ print("No revision for build.id {}".format(bid))
+ delids[bid] = "x"
+ for bid in delids:
+ buildids.pop(bid)
+
+
+def readExecutionFile(workdir):
+ exefile = "{}/qmexecutions.json".format(workdir)
+ try:
+ return readJSONFile(exefile)
+ except OSError:
+ return []
+
+
+def writeExecutionFile(workdir, executions):
+ exefile = "{}/qmexecutions.json".format(workdir)
+ try:
+ writeJSONFile(exefile, executions)
+ except OSError:
+ print("Error writing execution record.")
+
+
+def getLastRunFromExecutionFile(workdir):
+ executions = readExecutionFile(workdir)
+ if len(executions) > 0:
+ return executions[len(executions) - 1]
+ return {}
+
+
+def updateLastRunToExecutionFile(workdir, run):
+ executions = readExecutionFile(workdir)
+ executions[len(executions) - 1] = run
+ writeExecutionFile(workdir, executions)
+
+
+def addNewRunToExecutionFile(workdir, run):
+ executions = readExecutionFile(workdir)
+ executions.append(run)
+ writeExecutionFile(workdir, executions)
diff --git a/dom/quota/test/browser/browser.ini b/dom/quota/test/browser/browser.ini
new file mode 100644
index 0000000000..d0c79f90a9
--- /dev/null
+++ b/dom/quota/test/browser/browser.ini
@@ -0,0 +1,13 @@
+[DEFAULT]
+skip-if = (buildapp != "browser")
+support-files =
+ head.js
+ helpers.js
+ empty.html
+ permissionsPrompt.html
+
+[browser_permissionsCrossOrigin.js]
+[browser_permissionsPromptAllow.js]
+[browser_permissionsPromptDeny.js]
+[browser_permissionsPromptUnknown.js]
+[browser_simpledb.js]
diff --git a/dom/quota/test/browser/browser_permissionsCrossOrigin.js b/dom/quota/test/browser/browser_permissionsCrossOrigin.js
new file mode 100644
index 0000000000..6dcf32ff67
--- /dev/null
+++ b/dom/quota/test/browser/browser_permissionsCrossOrigin.js
@@ -0,0 +1,56 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const emptyURL =
+ "https://example.com/browser/dom/quota/test/browser/empty.html";
+
+addTest(async function testNoPermissionPrompt() {
+ registerPopupEventHandler("popupshowing", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+
+ info("Creating tab");
+
+ await BrowserTestUtils.withNewTab(emptyURL, async function(browser) {
+ await new Promise(r => {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [
+ ["permissions.delegation.enabled", true],
+ ["dom.security.featurePolicy.header.enabled", true],
+ ["dom.security.featurePolicy.webidl.enabled", true],
+ ],
+ },
+ r
+ );
+ });
+
+ await SpecialPowers.spawn(browser, [], async function(host0) {
+ let frame = content.document.createElement("iframe");
+ // Cross origin src
+ frame.src = "https://example.org/browser/dom/quota/test/empty.html";
+ content.document.body.appendChild(frame);
+ await ContentTaskUtils.waitForEvent(frame, "load");
+
+ await content.SpecialPowers.spawn(frame, [], async function() {
+ // Request a permission.
+ const persistAllowed = await this.content.navigator.storage.persist();
+ Assert.ok(
+ !persistAllowed,
+ "navigator.storage.persist() has been denied"
+ );
+ });
+ content.document.body.removeChild(frame);
+ });
+ });
+
+ unregisterAllPopupEventHandlers();
+});
diff --git a/dom/quota/test/browser/browser_permissionsPromptAllow.js b/dom/quota/test/browser/browser_permissionsPromptAllow.js
new file mode 100644
index 0000000000..4ddcfada96
--- /dev/null
+++ b/dom/quota/test/browser/browser_permissionsPromptAllow.js
@@ -0,0 +1,66 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const testPageURL =
+ "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html";
+
+addTest(async function testPermissionAllow() {
+ removePermission(testPageURL, "persistent-storage");
+
+ registerPopupEventHandler("popupshowing", function() {
+ ok(true, "prompt showing");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(true, "prompt shown");
+ triggerMainCommand(this);
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(true, "prompt hidden");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(true, gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "Correct permission set"
+ );
+ gBrowser.removeCurrentTab();
+ unregisterAllPopupEventHandlers();
+ // Keep persistent-storage permission for the next test.
+});
+
+addTest(async function testNoPermissionPrompt() {
+ registerPopupEventHandler("popupshowing", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(true, gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.ALLOW_ACTION,
+ "Correct permission set"
+ );
+ gBrowser.removeCurrentTab();
+ unregisterAllPopupEventHandlers();
+ removePermission(testPageURL, "persistent-storage");
+});
diff --git a/dom/quota/test/browser/browser_permissionsPromptDeny.js b/dom/quota/test/browser/browser_permissionsPromptDeny.js
new file mode 100644
index 0000000000..19ee8ef499
--- /dev/null
+++ b/dom/quota/test/browser/browser_permissionsPromptDeny.js
@@ -0,0 +1,150 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const testPageURL =
+ "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html";
+
+addTest(async function testPermissionTemporaryDenied() {
+ registerPopupEventHandler("popupshowing", function() {
+ ok(true, "prompt showing");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(true, "prompt shown");
+ triggerSecondaryCommand(this);
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(true, "prompt hidden");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(false, gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "Correct permission set"
+ );
+
+ let tempBlock = SitePermissions.getAllForBrowser(
+ gBrowser.selectedBrowser
+ ).find(
+ p =>
+ p.id == "persistent-storage" &&
+ p.state == SitePermissions.BLOCK &&
+ p.scope == SitePermissions.SCOPE_TEMPORARY
+ );
+ ok(tempBlock, "Should have a temporary block permission on active browser");
+
+ unregisterAllPopupEventHandlers();
+ gBrowser.removeCurrentTab();
+ removePermission(testPageURL, "persistent-storage");
+});
+
+addTest(async function testPermissionDenied() {
+ removePermission(testPageURL, "persistent-storage");
+
+ registerPopupEventHandler("popupshowing", function() {
+ ok(true, "prompt showing");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(true, "prompt shown");
+ triggerSecondaryCommand(this, /* remember = */ true);
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(true, "prompt hidden");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(false, gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.DENY_ACTION,
+ "Correct permission set"
+ );
+ unregisterAllPopupEventHandlers();
+ gBrowser.removeCurrentTab();
+ // Keep persistent-storage permission for the next test.
+});
+
+addTest(async function testNoPermissionPrompt() {
+ registerPopupEventHandler("popupshowing", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(false, "Shouldn't show a popup this time");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(false, gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.DENY_ACTION,
+ "Correct permission set"
+ );
+ unregisterAllPopupEventHandlers();
+ gBrowser.removeCurrentTab();
+ removePermission(testPageURL, "persistent-storage");
+});
+
+addTest(async function testPermissionDeniedDismiss() {
+ registerPopupEventHandler("popupshowing", function() {
+ ok(true, "prompt showing");
+ });
+ registerPopupEventHandler("popupshown", function() {
+ ok(true, "prompt shown");
+ // Dismiss permission prompt.
+ dismissNotification(this);
+ });
+ registerPopupEventHandler("popuphidden", function() {
+ ok(true, "prompt hidden");
+ });
+
+ info("Creating tab");
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(false, gBrowser);
+
+ // Pressing ESC results in a temporary block permission on the browser object.
+ // So the global permission for the URL should still be unknown, but the browser
+ // should have a block permission with a temporary scope.
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "Correct permission set"
+ );
+
+ let tempBlock = SitePermissions.getAllForBrowser(
+ gBrowser.selectedBrowser
+ ).find(
+ p =>
+ p.id == "persistent-storage" &&
+ p.state == SitePermissions.BLOCK &&
+ p.scope == SitePermissions.SCOPE_TEMPORARY
+ );
+ ok(tempBlock, "Should have a temporary block permission on active browser");
+
+ unregisterAllPopupEventHandlers();
+ gBrowser.removeCurrentTab();
+ removePermission(testPageURL, "persistent-storage");
+});
diff --git a/dom/quota/test/browser/browser_permissionsPromptUnknown.js b/dom/quota/test/browser/browser_permissionsPromptUnknown.js
new file mode 100644
index 0000000000..31c7e588a5
--- /dev/null
+++ b/dom/quota/test/browser/browser_permissionsPromptUnknown.js
@@ -0,0 +1,53 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const testPageURL =
+ "https://example.com/browser/dom/quota/test/browser/permissionsPrompt.html";
+
+addTest(async function testPermissionUnknownInPrivateWindow() {
+ removePermission(testPageURL, "persistent-storage");
+ info("Creating private window");
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ registerPopupEventHandler(
+ "popupshowing",
+ function() {
+ ok(false, "Shouldn't show a popup this time");
+ },
+ win
+ );
+ registerPopupEventHandler(
+ "popupshown",
+ function() {
+ ok(false, "Shouldn't show a popup this time");
+ },
+ win
+ );
+ registerPopupEventHandler(
+ "popuphidden",
+ function() {
+ ok(false, "Shouldn't show a popup this time");
+ },
+ win
+ );
+
+ info("Creating private tab");
+ win.gBrowser.selectedTab = BrowserTestUtils.addTab(win.gBrowser);
+
+ info("Loading test page: " + testPageURL);
+ BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, testPageURL);
+ await waitForMessage(false, win.gBrowser);
+
+ is(
+ getPermission(testPageURL, "persistent-storage"),
+ Ci.nsIPermissionManager.UNKNOWN_ACTION,
+ "Correct permission set"
+ );
+ unregisterAllPopupEventHandlers(win);
+ win.gBrowser.removeCurrentTab();
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+ removePermission(testPageURL, "persistent-storage");
+});
diff --git a/dom/quota/test/browser/browser_simpledb.js b/dom/quota/test/browser/browser_simpledb.js
new file mode 100644
index 0000000000..b3be78fca6
--- /dev/null
+++ b/dom/quota/test/browser/browser_simpledb.js
@@ -0,0 +1,51 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// getRandomBuffer, compareBuffers
+loadScript("dom/quota/test/common/file.js");
+
+addTest(async function testSimpleDB() {
+ const name = "data";
+ const bufferSize = 100;
+
+ let database = getSimpleDatabase();
+
+ let request = database.open("data");
+ await requestFinished(request);
+
+ let buffer1 = getRandomBuffer(bufferSize);
+
+ request = database.write(buffer1);
+ await requestFinished(request);
+
+ request = database.seek(0);
+ await requestFinished(request);
+
+ request = database.read(bufferSize);
+ let result = await requestFinished(request);
+
+ let buffer2 = result.getAsArrayBuffer();
+
+ ok(compareBuffers(buffer1, buffer2), "Buffers equal.");
+
+ let database2 = getSimpleDatabase();
+
+ try {
+ request = database2.open(name);
+ await requestFinished(request);
+ ok(false, "Should have thrown!");
+ } catch (ex) {
+ ok(request.resultCode == NS_ERROR_STORAGE_BUSY, "Good result code.");
+ }
+
+ request = database.close();
+ await requestFinished(request);
+
+ request = database2.open(name);
+ await requestFinished(request);
+
+ request = database2.close();
+ await requestFinished(request);
+});
diff --git a/dom/quota/test/browser/empty.html b/dom/quota/test/browser/empty.html
new file mode 100644
index 0000000000..1ad28bb1f7
--- /dev/null
+++ b/dom/quota/test/browser/empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Empty file</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/quota/test/browser/head.js b/dom/quota/test/browser/head.js
new file mode 100644
index 0000000000..fd81168b02
--- /dev/null
+++ b/dom/quota/test/browser/head.js
@@ -0,0 +1,149 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../";
+
+var gActiveListeners = {};
+
+loadScript("dom/quota/test/common/browser.js");
+
+function loadScript(path) {
+ const url = new URL(depth + path, gTestPath);
+ Services.scriptloader.loadSubScript(url.href, this);
+}
+
+// These event (un)registration handlers only work for one window, DONOT use
+// them with multiple windows.
+
+function registerPopupEventHandler(eventName, callback, win) {
+ if (!win) {
+ win = window;
+ }
+ gActiveListeners[eventName] = function(event) {
+ if (event.target != win.PopupNotifications.panel) {
+ return;
+ }
+ win.PopupNotifications.panel.removeEventListener(
+ eventName,
+ gActiveListeners[eventName]
+ );
+ delete gActiveListeners[eventName];
+
+ callback.call(win.PopupNotifications.panel);
+ };
+ win.PopupNotifications.panel.addEventListener(
+ eventName,
+ gActiveListeners[eventName]
+ );
+}
+
+function unregisterAllPopupEventHandlers(win) {
+ if (!win) {
+ win = window;
+ }
+ for (let eventName in gActiveListeners) {
+ win.PopupNotifications.panel.removeEventListener(
+ eventName,
+ gActiveListeners[eventName]
+ );
+ }
+ gActiveListeners = {};
+}
+
+function triggerMainCommand(popup, win) {
+ if (!win) {
+ win = window;
+ }
+ info("triggering main command");
+ let notifications = popup.childNodes;
+ ok(notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+ info("triggering command: " + notification.getAttribute("buttonlabel"));
+
+ EventUtils.synthesizeMouseAtCenter(notification.button, {}, win);
+}
+
+async function triggerSecondaryCommand(popup, remember = false, win = window) {
+ info("triggering secondary command");
+ let notifications = popup.childNodes;
+ ok(notifications.length, "at least one notification displayed");
+ let notification = notifications[0];
+
+ if (remember) {
+ notification.checkbox.checked = true;
+ }
+
+ await EventUtils.synthesizeMouseAtCenter(
+ notification.secondaryButton,
+ {},
+ win
+ );
+}
+
+function dismissNotification(popup, win = window) {
+ info("dismissing notification");
+ executeSoon(function() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ });
+}
+
+function waitForMessage(aMessage, browser) {
+ // We cannot capture aMessage inside the checkFn, so we override the
+ // checkFn.toSource to tunnel aMessage instead.
+ let checkFn = function() {};
+ checkFn.toSource = function() {
+ return `function checkFn(event) {
+ let message = ${aMessage.toSource()};
+ if (event.data == message) {
+ return true;
+ }
+ throw new Error(
+ \`Unexpected result: \$\{event.data\}, expected \$\{message\}\`
+ );
+ }`;
+ };
+
+ return BrowserTestUtils.waitForContentEvent(
+ browser.selectedBrowser,
+ "message",
+ /* capture */ true,
+ checkFn,
+ /* wantsUntrusted */ true
+ ).then(() => {
+ // An assertion in checkFn wouldn't be recorded as part of the test, so we
+ // use this assertion to confirm that we've successfully received the
+ // message (we'll only reach this point if that's the case).
+ ok(true, "Received message: " + aMessage);
+ });
+}
+
+function removePermission(url, permission) {
+ let uri = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(url);
+ let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ let principal = ssm.createContentPrincipal(uri, {});
+
+ Cc["@mozilla.org/permissionmanager;1"]
+ .getService(Ci.nsIPermissionManager)
+ .removeFromPrincipal(principal, permission);
+}
+
+function getPermission(url, permission) {
+ let uri = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(url);
+ let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ let principal = ssm.createContentPrincipal(uri, {});
+
+ return Cc["@mozilla.org/permissionmanager;1"]
+ .getService(Ci.nsIPermissionManager)
+ .testPermissionFromPrincipal(principal, permission);
+}
diff --git a/dom/quota/test/browser/helpers.js b/dom/quota/test/browser/helpers.js
new file mode 100644
index 0000000000..f3bbb36b71
--- /dev/null
+++ b/dom/quota/test/browser/helpers.js
@@ -0,0 +1,46 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../";
+
+var testGenerator;
+var testResult;
+
+loadScript("dom/quota/test/common/nestedtest.js");
+
+function loadScript(path) {
+ const url = new URL(depth + path, window.location.href);
+ SpecialPowers.Services.scriptloader.loadSubScript(url.href, this);
+}
+
+function runTest() {
+ clearAllDatabases(() => {
+ testGenerator = testSteps();
+ testGenerator.next();
+ });
+}
+
+function finishTestNow() {
+ if (testGenerator) {
+ testGenerator.return();
+ testGenerator = undefined;
+ }
+}
+
+function finishTest() {
+ clearAllDatabases(() => {
+ setTimeout(finishTestNow, 0);
+ setTimeout(() => {
+ window.parent.postMessage(testResult, "*");
+ }, 0);
+ });
+}
+
+function continueToNextStep() {
+ setTimeout(() => {
+ testGenerator.next();
+ }, 0);
+}
diff --git a/dom/quota/test/browser/permissionsPrompt.html b/dom/quota/test/browser/permissionsPrompt.html
new file mode 100644
index 0000000000..5f11bf6c95
--- /dev/null
+++ b/dom/quota/test/browser/permissionsPrompt.html
@@ -0,0 +1,34 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+ <head>
+ <meta charset=UTF-8>
+ <title>Persistent-Storage Permission Prompt Test</title>
+
+ <script type="text/javascript" src="helpers.js"></script>
+
+ <script type="text/javascript">
+ function* testSteps()
+ {
+ SpecialPowers.pushPrefEnv({
+ "set": [["dom.storageManager.prompt.testing", false],
+ ["dom.storageManager.prompt.testing.allow", false]]
+ }, continueToNextStep);
+ yield undefined;
+
+ navigator.storage.persist().then(result => {
+ testGenerator.next(result);
+ });
+ testResult = yield undefined;
+
+ finishTest();
+ }
+ </script>
+
+ </head>
+
+ <body onload="runTest();" onunload="finishTestNow();"></body>
+
+</html>
diff --git a/dom/quota/test/common/browser.js b/dom/quota/test/common/browser.js
new file mode 100644
index 0000000000..aae80adcd1
--- /dev/null
+++ b/dom/quota/test/common/browser.js
@@ -0,0 +1,34 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/system.js");
+
+function addTest(testFunction) {
+ const taskFunction = async function() {
+ await enableStorageTesting();
+
+ await testFunction();
+ };
+
+ Object.defineProperty(taskFunction, "name", {
+ value: testFunction.name,
+ writable: false,
+ });
+
+ add_task(taskFunction);
+}
+
+async function enableStorageTesting() {
+ const prefsToSet = [
+ ["dom.quotaManager.testing", true],
+ ["dom.storageManager.enabled", true],
+ ["dom.simpleDB.enabled", true],
+ ];
+ if (Services.appinfo.OS === "WINNT") {
+ prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]);
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: prefsToSet });
+}
diff --git a/dom/quota/test/common/content.js b/dom/quota/test/common/content.js
new file mode 100644
index 0000000000..d3927d649a
--- /dev/null
+++ b/dom/quota/test/common/content.js
@@ -0,0 +1,62 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const NS_ERROR_STORAGE_BUSY = SpecialPowers.Cr.NS_ERROR_STORAGE_BUSY;
+
+loadScript("dom/quota/test/common/global.js");
+
+function clearAllDatabases(callback) {
+ let qms = SpecialPowers.Services.qms;
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+ let request = qms.clearStoragesForPrincipal(principal);
+ let cb = SpecialPowers.wrapCallback(callback);
+ request.callback = cb;
+ return request;
+}
+
+// SimpleDB connections and SpecialPowers wrapping:
+//
+// SpecialPowers provides a SpecialPowersHandler Proxy mechanism that lets our
+// content-privileged code borrow its chrome-privileged principal to access
+// things we shouldn't be able to access. The proxies wrap their returned
+// values, so once we have something wrapped we can rely on returned objects
+// being wrapped as well. The proxy will also automatically unwrap wrapped
+// arguments we pass in. However, we need to invoke wrapCallback on callback
+// functions so that the arguments they receive will be wrapped because the
+// proxy does not automatically wrap content-privileged functions.
+//
+// Our use of (wrapped) SpecialPowers.Cc results in getSimpleDatabase()
+// producing a wrapped nsISDBConnection instance. The nsISDBResult instances
+// exposed on the (wrapped) nsISDBRequest are also wrapped.
+// In particular, the wrapper takes responsibility for automatically cloning
+// the ArrayBuffer returned by nsISDBResult.getAsArrayBuffer into the content
+// compartment (rather than wrapping it) so that constructing a Uint8Array
+// from it will succeed.
+
+function getSimpleDatabase() {
+ let connection = SpecialPowers.Cc[
+ "@mozilla.org/dom/sdb-connection;1"
+ ].createInstance(SpecialPowers.Ci.nsISDBConnection);
+
+ let principal = SpecialPowers.wrap(document).nodePrincipal;
+
+ connection.init(principal);
+
+ return connection;
+}
+
+async function requestFinished(request) {
+ await new Promise(function(resolve) {
+ request.callback = SpecialPowers.wrapCallback(function() {
+ resolve();
+ });
+ });
+
+ if (request.resultCode != SpecialPowers.Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
diff --git a/dom/quota/test/common/file.js b/dom/quota/test/common/file.js
new file mode 100644
index 0000000000..55e2e189fb
--- /dev/null
+++ b/dom/quota/test/common/file.js
@@ -0,0 +1,45 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function getBuffer(size) {
+ let buffer = new ArrayBuffer(size);
+ is(buffer.byteLength, size, "Correct byte length");
+ return buffer;
+}
+
+// May be called for any size, but you should call getBuffer() if you know
+// that size is big and that randomness is not necessary because it is
+// noticeably faster.
+function getRandomBuffer(size) {
+ let buffer = getBuffer(size);
+ let view = new Uint8Array(buffer);
+ for (let i = 0; i < size; i++) {
+ view[i] = parseInt(Math.random() * 255);
+ }
+ return buffer;
+}
+
+function compareBuffers(buffer1, buffer2) {
+ if (buffer1.byteLength != buffer2.byteLength) {
+ return false;
+ }
+
+ let view1 = buffer1 instanceof Uint8Array ? buffer1 : new Uint8Array(buffer1);
+ let view2 = buffer2 instanceof Uint8Array ? buffer2 : new Uint8Array(buffer2);
+ for (let i = 0; i < buffer1.byteLength; i++) {
+ if (view1[i] != view2[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function getBlob(type, object) {
+ return new Blob([object], { type });
+}
+
+function getNullBlob(size) {
+ return getBlob("binary/null", getBuffer(size));
+}
diff --git a/dom/quota/test/common/global.js b/dom/quota/test/common/global.js
new file mode 100644
index 0000000000..bb0f3669be
--- /dev/null
+++ b/dom/quota/test/common/global.js
@@ -0,0 +1,47 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const INT64_MIN = -0x8000000000000000n;
+
+class RequestError extends Error {
+ constructor(resultCode, resultName) {
+ super(`Request failed (code: ${resultCode}, name: ${resultName})`);
+ this.name = "RequestError";
+ this.resultCode = resultCode;
+ this.resultName = resultName;
+ }
+}
+
+function openDBRequestUpgradeNeeded(request) {
+ return new Promise(function(resolve, reject) {
+ request.onerror = function(event) {
+ ok(false, "indexedDB error, '" + event.target.error.name + "'");
+ reject(event);
+ };
+ request.onupgradeneeded = function(event) {
+ resolve(event);
+ };
+ request.onsuccess = function(event) {
+ ok(false, "Got success, but did not expect it!");
+ reject(event);
+ };
+ });
+}
+
+function openDBRequestSucceeded(request) {
+ return new Promise(function(resolve, reject) {
+ request.onerror = function(event) {
+ ok(false, "indexedDB error, '" + event.target.error.name + "'");
+ reject(event);
+ };
+ request.onupgradeneeded = function(event) {
+ ok(false, "Got upgrade, but did not expect it!");
+ reject(event);
+ };
+ request.onsuccess = function(event) {
+ resolve(event);
+ };
+ });
+}
diff --git a/dom/quota/test/common/mochitest.js b/dom/quota/test/common/mochitest.js
new file mode 100644
index 0000000000..1b867f6e92
--- /dev/null
+++ b/dom/quota/test/common/mochitest.js
@@ -0,0 +1,19 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/content.js");
+
+async function enableStorageTesting() {
+ let prefsToSet = [
+ ["dom.quotaManager.testing", true],
+ ["dom.storageManager.enabled", true],
+ ["dom.simpleDB.enabled", true],
+ ];
+ if (SpecialPowers.Services.appinfo.OS === "WINNT") {
+ prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]);
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: prefsToSet });
+}
diff --git a/dom/quota/test/common/nestedtest.js b/dom/quota/test/common/nestedtest.js
new file mode 100644
index 0000000000..5c2011bfe9
--- /dev/null
+++ b/dom/quota/test/common/nestedtest.js
@@ -0,0 +1,6 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/content.js");
diff --git a/dom/quota/test/common/system.js b/dom/quota/test/common/system.js
new file mode 100644
index 0000000000..25acbe9384
--- /dev/null
+++ b/dom/quota/test/common/system.js
@@ -0,0 +1,74 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const PR_USEC_PER_SEC = 1000000;
+
+const NS_ERROR_STORAGE_BUSY = Cr.NS_ERROR_STORAGE_BUSY;
+
+loadScript("dom/quota/test/common/global.js");
+
+function getProfileDir() {
+ return Services.dirsvc.get("ProfD", Ci.nsIFile);
+}
+
+// Given a "/"-delimited path relative to a base file (or the profile
+// directory if a base file is not provided) return an nsIFile representing the
+// path. This does not test for the existence of the file or parent
+// directories. It is safe even on Windows where the directory separator is
+// not "/", but make sure you're not passing in a "\"-delimited path.
+function getRelativeFile(relativePath, baseFile) {
+ if (!baseFile) {
+ baseFile = getProfileDir();
+ }
+
+ let file = baseFile.clone();
+
+ if (Services.appinfo.OS === "WINNT") {
+ let winFile = file.QueryInterface(Ci.nsILocalFileWin);
+ winFile.useDOSDevicePathSyntax = true;
+ }
+
+ relativePath.split("/").forEach(function(component) {
+ if (component == "..") {
+ file = file.parent;
+ } else {
+ file.append(component);
+ }
+ });
+
+ return file;
+}
+
+function getCurrentPrincipal() {
+ return Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal);
+}
+
+function getSimpleDatabase(principal, persistence) {
+ let connection = Cc["@mozilla.org/dom/sdb-connection;1"].createInstance(
+ Ci.nsISDBConnection
+ );
+
+ if (!principal) {
+ principal = getCurrentPrincipal();
+ }
+
+ connection.init(principal, persistence);
+
+ return connection;
+}
+
+async function requestFinished(request) {
+ await new Promise(function(resolve) {
+ request.callback = function() {
+ resolve();
+ };
+ });
+
+ if (request.resultCode !== Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
diff --git a/dom/quota/test/common/test_simpledb.js b/dom/quota/test/common/test_simpledb.js
new file mode 100644
index 0000000000..dee7019097
--- /dev/null
+++ b/dom/quota/test/common/test_simpledb.js
@@ -0,0 +1,50 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+async function testSteps() {
+ const name = "data";
+ const bufferSize = 100;
+
+ let database = getSimpleDatabase();
+
+ let request = database.open(name);
+ await requestFinished(request);
+
+ let buffer1 = getRandomBuffer(bufferSize);
+
+ request = database.write(buffer1);
+ await requestFinished(request);
+
+ request = database.seek(0);
+ await requestFinished(request);
+
+ request = database.read(bufferSize);
+ let result = await requestFinished(request);
+
+ let buffer2 = result.getAsArrayBuffer();
+
+ ok(compareBuffers(buffer1, buffer2), "Buffers equal.");
+
+ let database2 = getSimpleDatabase();
+
+ try {
+ request = database2.open(name);
+ await requestFinished(request);
+ ok(false, "Should have thrown!");
+ } catch (ex) {
+ ok(request.resultCode == NS_ERROR_STORAGE_BUSY, "Good result code.");
+ }
+
+ request = database.close();
+ await requestFinished(request);
+
+ request = database2.open(name);
+ await requestFinished(request);
+
+ request = database2.close();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/common/test_storage_manager_persist_allow.js b/dom/quota/test/common/test_storage_manager_persist_allow.js
new file mode 100644
index 0000000000..0a6e59843d
--- /dev/null
+++ b/dom/quota/test/common/test_storage_manager_persist_allow.js
@@ -0,0 +1,30 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["dom.storageManager.prompt.testing.allow", true]],
+ },
+ continueToNextStep
+ );
+ yield undefined;
+
+ navigator.storage.persist().then(grabArgAndContinueHandler);
+ let persistResult = yield undefined;
+
+ is(persistResult, true, "Persist succeeded");
+
+ navigator.storage.persisted().then(grabArgAndContinueHandler);
+ let persistedResult = yield undefined;
+
+ is(
+ persistResult,
+ persistedResult,
+ "Persist/persisted results are consistent"
+ );
+
+ finishTest();
+}
diff --git a/dom/quota/test/common/test_storage_manager_persist_deny.js b/dom/quota/test/common/test_storage_manager_persist_deny.js
new file mode 100644
index 0000000000..855d739ca3
--- /dev/null
+++ b/dom/quota/test/common/test_storage_manager_persist_deny.js
@@ -0,0 +1,34 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["dom.storageManager.prompt.testing.allow", false]],
+ },
+ continueToNextStep
+ );
+ yield undefined;
+
+ navigator.storage.persist().then(grabArgAndContinueHandler);
+ let persistResult = yield undefined;
+
+ is(
+ persistResult,
+ false,
+ "Cancel the persist prompt and resolve a promise with false"
+ );
+
+ navigator.storage.persisted().then(grabArgAndContinueHandler);
+ let persistedResult = yield undefined;
+
+ is(
+ persistResult,
+ persistedResult,
+ "Persist/persisted results are consistent"
+ );
+
+ finishTest();
+}
diff --git a/dom/quota/test/common/test_storage_manager_persisted.js b/dom/quota/test/common/test_storage_manager_persisted.js
new file mode 100644
index 0000000000..ebda93649a
--- /dev/null
+++ b/dom/quota/test/common/test_storage_manager_persisted.js
@@ -0,0 +1,13 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ navigator.storage.persisted().then(grabArgAndContinueHandler);
+ let persistedResult = yield undefined;
+
+ is(persistedResult, false, "Persisted returns false");
+
+ finishTest();
+}
diff --git a/dom/quota/test/common/xpcshell.js b/dom/quota/test/common/xpcshell.js
new file mode 100644
index 0000000000..ed3afaa467
--- /dev/null
+++ b/dom/quota/test/common/xpcshell.js
@@ -0,0 +1,91 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/system.js");
+
+function enableStorageTesting() {
+ Services.prefs.setBoolPref("dom.quotaManager.testing", true);
+ Services.prefs.setBoolPref("dom.storageManager.enabled", true);
+ Services.prefs.setBoolPref("dom.simpleDB.enabled", true);
+ if (Services.appinfo.OS === "WINNT") {
+ Services.prefs.setBoolPref("dom.quotaManager.useDOSDevicePathSyntax", true);
+ }
+}
+
+function resetStorageTesting() {
+ Services.prefs.clearUserPref("dom.quotaManager.testing");
+ Services.prefs.clearUserPref("dom.storageManager.enabled");
+ Services.prefs.clearUserPref("dom.simpleDB.enabled");
+ if (Services.appinfo.OS === "WINNT") {
+ Services.prefs.clearUserPref("dom.quotaManager.useDOSDevicePathSyntax");
+ }
+}
+
+function clear(callback) {
+ let request = Services.qms.clear();
+ request.callback = callback;
+
+ return request;
+}
+
+function reset(callback) {
+ let request = Services.qms.reset();
+ request.callback = callback;
+
+ return request;
+}
+
+function installPackage(packageRelativePath, allowFileOverwrites) {
+ let currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+
+ let packageFile = getRelativeFile(packageRelativePath + ".zip", currentDir);
+
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(
+ Ci.nsIZipReader
+ );
+ zipReader.open(packageFile);
+
+ let entryNames = Array.from(zipReader.findEntries(null));
+ entryNames.sort();
+
+ for (let entryName of entryNames) {
+ if (entryName.match(/^create_db\.(html|js)/)) {
+ continue;
+ }
+
+ let zipentry = zipReader.getEntry(entryName);
+
+ let file = getRelativeFile(entryName);
+
+ if (zipentry.isDirectory) {
+ if (!file.exists()) {
+ file.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+ } else {
+ if (!allowFileOverwrites && file.exists()) {
+ throw new Error("File already exists!");
+ }
+
+ let istream = zipReader.getInputStream(entryName);
+
+ var ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(file, -1, parseInt("0644", 8), 0);
+
+ let bostream = Cc[
+ "@mozilla.org/network/buffered-output-stream;1"
+ ].createInstance(Ci.nsIBufferedOutputStream);
+ bostream.init(ostream, 32768);
+
+ bostream.writeFrom(istream, istream.available());
+
+ istream.close();
+ bostream.close();
+ }
+ }
+
+ zipReader.close();
+}
diff --git a/dom/quota/test/gtest/Common.cpp b/dom/quota/test/gtest/Common.cpp
new file mode 100644
index 0000000000..efbfd94775
--- /dev/null
+++ b/dom/quota/test/gtest/Common.cpp
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Common.h"
+
+#include "mozilla/dom/QMResult.h"
+
+namespace mozilla::dom::quota {
+
+#ifdef QM_ERROR_STACKS_ENABLED
+uint64_t DOM_Quota_Test::sExpectedStackId;
+
+// static
+void DOM_Quota_Test::SetUpTestCase() {
+ sExpectedStackId = QMResult().StackId();
+}
+
+// static
+void DOM_Quota_Test::IncreaseExpectedStackId() { sExpectedStackId++; }
+
+// static
+uint64_t DOM_Quota_Test::ExpectedStackId() { return sExpectedStackId; }
+#endif
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/test/gtest/Common.h b/dom/quota/test/gtest/Common.h
new file mode 100644
index 0000000000..eac58bd7fb
--- /dev/null
+++ b/dom/quota/test/gtest/Common.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_TEST_GTEST_COMMON_H_
+#define DOM_QUOTA_TEST_GTEST_COMMON_H_
+
+#include <cstdint>
+#include "gtest/gtest.h"
+#include "mozilla/dom/quota/Config.h"
+
+namespace mozilla::dom::quota {
+
+class DOM_Quota_Test : public testing::Test {
+#ifdef QM_ERROR_STACKS_ENABLED
+ public:
+ static void SetUpTestCase();
+
+ static void IncreaseExpectedStackId();
+
+ static uint64_t ExpectedStackId();
+
+ private:
+ static uint64_t sExpectedStackId;
+#endif
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_TEST_GTEST_COMMON_H_
diff --git a/dom/quota/test/gtest/PQuotaTest.ipdl b/dom/quota/test/gtest/PQuotaTest.ipdl
new file mode 100644
index 0000000000..e4ca37325a
--- /dev/null
+++ b/dom/quota/test/gtest/PQuotaTest.ipdl
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+namespace dom {
+namespace quota {
+
+sync protocol PQuotaTest {
+ parent:
+ sync Try_Success_CustomErr_QmIpcFail()
+ returns (bool tryDidNotReturn);
+
+ sync Try_Success_CustomErr_IpcFail()
+ returns (bool tryDidNotReturn);
+
+ sync TryInspect_Success_CustomErr_QmIpcFail()
+ returns (bool tryDidNotReturn);
+
+ sync TryInspect_Success_CustomErr_IpcFail()
+ returns (bool tryDidNotReturn);
+};
+
+} // namespace quota
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp b/dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp
new file mode 100644
index 0000000000..2e52b4b69a
--- /dev/null
+++ b/dom/quota/test/gtest/QuotaManagerDependencyFixture.cpp
@@ -0,0 +1,160 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QuotaManagerDependencyFixture.h"
+
+#include "mozIStorageService.h"
+#include "mozStorageCID.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/quota/QuotaManagerService.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsIQuotaCallbacks.h"
+#include "nsIQuotaRequests.h"
+#include "nsIVariant.h"
+#include "nsScriptSecurityManager.h"
+
+namespace mozilla::dom::quota::test {
+
+namespace {
+
+class RequestResolver final : public nsIQuotaCallback {
+ public:
+ RequestResolver() : mDone(false) {}
+
+ bool Done() const { return mDone; }
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD OnComplete(nsIQuotaRequest* aRequest) override {
+ mDone = true;
+
+ return NS_OK;
+ }
+
+ private:
+ ~RequestResolver() = default;
+
+ bool mDone;
+};
+
+} // namespace
+
+NS_IMPL_ISUPPORTS(RequestResolver, nsIQuotaCallback)
+
+// static
+void QuotaManagerDependencyFixture::InitializeFixture() {
+ // Some QuotaManagerService methods fail if the testing pref is not set.
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ prefs->SetBoolPref("dom.quotaManager.testing", true);
+
+ // The first initialization of storage service must be done on the main
+ // thread.
+ nsCOMPtr<mozIStorageService> storageService =
+ do_GetService(MOZ_STORAGE_SERVICE_CONTRACTID);
+ ASSERT_TRUE(storageService);
+
+ nsIObserver* observer = QuotaManager::GetObserver();
+ ASSERT_TRUE(observer);
+
+ nsresult rv = observer->Observe(nullptr, "profile-do-change", nullptr);
+ ASSERT_NS_SUCCEEDED(rv);
+
+ ASSERT_NO_FATAL_FAILURE(StorageInitialized());
+
+ QuotaManager* quotaManager = QuotaManager::Get();
+ ASSERT_TRUE(quotaManager);
+
+ ASSERT_TRUE(quotaManager->OwningThread());
+
+ sBackgroundTarget = quotaManager->OwningThread();
+}
+
+// static
+void QuotaManagerDependencyFixture::ShutdownFixture() {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ prefs->SetBoolPref("dom.quotaManager.testing", false);
+
+ nsIObserver* observer = QuotaManager::GetObserver();
+ ASSERT_TRUE(observer);
+
+ nsresult rv = observer->Observe(nullptr, "profile-before-change-qm", nullptr);
+ ASSERT_NS_SUCCEEDED(rv);
+
+ PerformOnBackgroundThread([]() { QuotaManager::Reset(); });
+
+ sBackgroundTarget = nullptr;
+}
+
+// static
+void QuotaManagerDependencyFixture::StorageInitialized(bool* aResult) {
+ AutoJSAPI jsapi;
+
+ bool ok = jsapi.Init(xpc::PrivilegedJunkScope());
+ ASSERT_TRUE(ok);
+
+ nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ ASSERT_TRUE(qms);
+
+ nsCOMPtr<nsIQuotaRequest> request;
+ nsresult rv = qms->StorageInitialized(getter_AddRefs(request));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ RefPtr<RequestResolver> resolver = new RequestResolver();
+
+ rv = request->SetCallback(resolver);
+ ASSERT_NS_SUCCEEDED(rv);
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&resolver]() { return resolver->Done(); });
+
+ if (aResult) {
+ nsCOMPtr<nsIVariant> result;
+ rv = request->GetResult(getter_AddRefs(result));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ rv = result->GetAsBool(aResult);
+ ASSERT_NS_SUCCEEDED(rv);
+ }
+}
+
+// static
+void QuotaManagerDependencyFixture::ClearStoragesForOrigin(
+ const OriginMetadata& aOriginMetadata) {
+ nsCOMPtr<nsIQuotaManagerService> qms = QuotaManagerService::GetOrCreate();
+ ASSERT_TRUE(qms);
+
+ nsCOMPtr<nsIScriptSecurityManager> ssm =
+ nsScriptSecurityManager::GetScriptSecurityManager();
+ ASSERT_TRUE(ssm);
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsresult rv = ssm->CreateContentPrincipalFromOrigin(
+ aOriginMetadata.mOrigin, getter_AddRefs(principal));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIQuotaRequest> request;
+ rv = qms->ClearStoragesForPrincipal(principal, VoidCString(), VoidString(),
+ /* aClearAll */ false,
+ getter_AddRefs(request));
+ ASSERT_NS_SUCCEEDED(rv);
+
+ RefPtr<RequestResolver> resolver = new RequestResolver();
+ ASSERT_TRUE(resolver);
+
+ rv = request->SetCallback(resolver);
+ ASSERT_NS_SUCCEEDED(rv);
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns,
+ [&resolver]() { return resolver->Done(); });
+}
+
+nsCOMPtr<nsISerialEventTarget> QuotaManagerDependencyFixture::sBackgroundTarget;
+
+} // namespace mozilla::dom::quota::test
diff --git a/dom/quota/test/gtest/QuotaManagerDependencyFixture.h b/dom/quota/test/gtest/QuotaManagerDependencyFixture.h
new file mode 100644
index 0000000000..4d57952242
--- /dev/null
+++ b/dom/quota/test/gtest/QuotaManagerDependencyFixture.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_
+#define DOM_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_
+
+#include "gtest/gtest.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/dom/quota/ForwardDecls.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+
+namespace mozilla::dom::quota::test {
+
+class QuotaManagerDependencyFixture : public testing::Test {
+ public:
+ protected:
+ static void InitializeFixture();
+
+ static void ShutdownFixture();
+
+ static void StorageInitialized(bool* aResult = nullptr);
+
+ static void ClearStoragesForOrigin(const OriginMetadata& aOriginMetadata);
+
+ /* Convenience method for tasks which must be called on PBackground thread */
+ template <class Invokable, class... Args>
+ static void PerformOnBackgroundThread(Invokable&& aInvokable,
+ Args&&... aArgs) {
+ bool done = false;
+ auto boundTask =
+ // For c++17, bind is cleaner than tuple for parameter pack forwarding
+ // NOLINTNEXTLINE(modernize-avoid-bind)
+ std::bind(std::forward<Invokable>(aInvokable),
+ std::forward<Args>(aArgs)...);
+ InvokeAsync(BackgroundTargetStrongRef(), __func__,
+ [boundTask = std::move(boundTask)] {
+ boundTask();
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue& /* aValue */) {
+ done = true;
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+
+ /* Convenience method for tasks which must be executed on IO thread */
+ template <class Invokable, class... Args>
+ static void PerformOnIOThread(Invokable&& aInvokable, Args&&... aArgs) {
+ QuotaManager* quotaManager = QuotaManager::Get();
+ ASSERT_TRUE(quotaManager);
+
+ bool done = false;
+ auto boundTask =
+ // For c++17, bind is cleaner than tuple for parameter pack forwarding
+ // NOLINTNEXTLINE(modernize-avoid-bind)
+ std::bind(std::forward<Invokable>(aInvokable),
+ std::forward<Args>(aArgs)...);
+ InvokeAsync(quotaManager->IOThread(), __func__,
+ [boundTask = std::move(boundTask)]() {
+ boundTask();
+ return BoolPromise::CreateAndResolve(true, __func__);
+ })
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [&done](const BoolPromise::ResolveOrRejectValue& value) {
+ done = true;
+ });
+
+ SpinEventLoopUntil("Promise is fulfilled"_ns, [&done]() { return done; });
+ }
+
+ static const nsCOMPtr<nsISerialEventTarget>& BackgroundTargetStrongRef() {
+ return sBackgroundTarget;
+ }
+
+ private:
+ static nsCOMPtr<nsISerialEventTarget> sBackgroundTarget;
+};
+
+} // namespace mozilla::dom::quota::test
+
+#endif // DOM_QUOTA_TEST_GTEST_QUOTAMANAGERDEPENDENCYFIXTURE_H_
diff --git a/dom/quota/test/gtest/QuotaTestChild.h b/dom/quota/test/gtest/QuotaTestChild.h
new file mode 100644
index 0000000000..d0d87d8267
--- /dev/null
+++ b/dom/quota/test/gtest/QuotaTestChild.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_
+#define DOM_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_
+
+#include "mozilla/dom/quota/PQuotaTestChild.h"
+
+namespace mozilla::dom::quota {
+
+class QuotaTestChild : public PQuotaTestChild {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaTestChild, override)
+
+ private:
+ ~QuotaTestChild() = default;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_TEST_GTEST_QUOTATESTCHILD_H_
diff --git a/dom/quota/test/gtest/QuotaTestParent.h b/dom/quota/test/gtest/QuotaTestParent.h
new file mode 100644
index 0000000000..5ec1e128b3
--- /dev/null
+++ b/dom/quota/test/gtest/QuotaTestParent.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_
+#define DOM_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_
+
+#include "mozilla/dom/quota/PQuotaTestParent.h"
+
+namespace mozilla::dom::quota {
+
+class QuotaTestParent : public PQuotaTestParent {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QuotaTestParent, override)
+
+ public:
+ mozilla::ipc::IPCResult RecvTry_Success_CustomErr_QmIpcFail(
+ bool* aTryDidNotReturn);
+
+ mozilla::ipc::IPCResult RecvTry_Success_CustomErr_IpcFail(
+ bool* aTryDidNotReturn);
+
+ mozilla::ipc::IPCResult RecvTryInspect_Success_CustomErr_QmIpcFail(
+ bool* aTryDidNotReturn);
+
+ mozilla::ipc::IPCResult RecvTryInspect_Success_CustomErr_IpcFail(
+ bool* aTryDidNotReturn);
+
+ private:
+ ~QuotaTestParent() = default;
+};
+
+} // namespace mozilla::dom::quota
+
+#endif // DOM_QUOTA_TEST_GTEST_QUOTATESTPARENT_H_
diff --git a/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp b/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp
new file mode 100644
index 0000000000..14abb6631d
--- /dev/null
+++ b/dom/quota/test/gtest/TestCheckedUnsafePtr.cpp
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/CheckedUnsafePtr.h"
+
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <type_traits>
+#include <utility>
+#include "mozilla/fallible.h"
+
+using namespace mozilla;
+
+class NoCheckTestType
+ : public SupportsCheckedUnsafePtr<DoNotCheckCheckedUnsafePtrs> {};
+
+#if __cplusplus < 202002L
+static_assert(std::is_literal_type_v<CheckedUnsafePtr<NoCheckTestType>>);
+#endif
+
+static_assert(
+ std::is_trivially_copy_constructible_v<CheckedUnsafePtr<NoCheckTestType>>);
+static_assert(
+ std::is_trivially_copy_assignable_v<CheckedUnsafePtr<NoCheckTestType>>);
+static_assert(
+ std::is_trivially_move_constructible_v<CheckedUnsafePtr<NoCheckTestType>>);
+static_assert(
+ std::is_trivially_move_assignable_v<CheckedUnsafePtr<NoCheckTestType>>);
+
+class TestCheckingPolicy : public CheckCheckedUnsafePtrs<TestCheckingPolicy> {
+ protected:
+ explicit TestCheckingPolicy(bool& aPassedCheck)
+ : mPassedCheck(aPassedCheck) {}
+
+ private:
+ friend class mozilla::CheckingPolicyAccess;
+ void NotifyCheckFailure() { mPassedCheck = false; }
+
+ bool& mPassedCheck;
+};
+
+struct BasePointee : public SupportsCheckedUnsafePtr<TestCheckingPolicy> {
+ explicit BasePointee(bool& aCheckPassed)
+ : SupportsCheckedUnsafePtr<TestCheckingPolicy>(aCheckPassed) {}
+};
+
+struct DerivedPointee : public BasePointee {
+ using BasePointee::BasePointee;
+};
+
+class CheckedUnsafePtrTest : public ::testing::Test {
+ protected:
+ bool mPassedCheck = true;
+};
+
+TEST_F(CheckedUnsafePtrTest, PointeeWithNoCheckedUnsafePtrs) {
+ { DerivedPointee pointee{mPassedCheck}; }
+ ASSERT_TRUE(mPassedCheck);
+}
+
+template <typename PointerType>
+class TypedCheckedUnsafePtrTest : public CheckedUnsafePtrTest {};
+
+TYPED_TEST_SUITE_P(TypedCheckedUnsafePtrTest);
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest, PointeeWithOneCheckedUnsafePtr) {
+ {
+ DerivedPointee pointee{this->mPassedCheck};
+ CheckedUnsafePtr<TypeParam> ptr = &pointee;
+ }
+ ASSERT_TRUE(this->mPassedCheck);
+}
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest, CheckedUnsafePtrCopyConstructed) {
+ {
+ DerivedPointee pointee{this->mPassedCheck};
+ CheckedUnsafePtr<TypeParam> ptr1 = &pointee;
+ CheckedUnsafePtr<TypeParam> ptr2 = ptr1;
+ }
+ ASSERT_TRUE(this->mPassedCheck);
+}
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest, CheckedUnsafePtrCopyAssigned) {
+ {
+ DerivedPointee pointee{this->mPassedCheck};
+ CheckedUnsafePtr<TypeParam> ptr1 = &pointee;
+ CheckedUnsafePtr<TypeParam> ptr2;
+ ptr2 = ptr1;
+ }
+ ASSERT_TRUE(this->mPassedCheck);
+}
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest,
+ PointeeWithOneDanglingCheckedUnsafePtr) {
+ [this]() -> CheckedUnsafePtr<TypeParam> {
+ DerivedPointee pointee{this->mPassedCheck};
+ return &pointee;
+ }();
+ ASSERT_FALSE(this->mPassedCheck);
+}
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest,
+ PointeeWithOneCopiedDanglingCheckedUnsafePtr) {
+ const auto dangling1 = [this]() -> CheckedUnsafePtr<DerivedPointee> {
+ DerivedPointee pointee{this->mPassedCheck};
+ return &pointee;
+ }();
+ EXPECT_FALSE(this->mPassedCheck);
+
+ // With AddressSanitizer we would hopefully detect if the copy constructor
+ // tries to add dangling2 to the now-gone pointee's unsafe pointer array. No
+ // promises though, since it might be optimized away.
+ CheckedUnsafePtr<TypeParam> dangling2{dangling1};
+ ASSERT_TRUE(dangling2);
+}
+
+TYPED_TEST_P(TypedCheckedUnsafePtrTest,
+ PointeeWithOneCopyAssignedDanglingCheckedUnsafePtr) {
+ const auto dangling1 = [this]() -> CheckedUnsafePtr<DerivedPointee> {
+ DerivedPointee pointee{this->mPassedCheck};
+ return &pointee;
+ }();
+ EXPECT_FALSE(this->mPassedCheck);
+
+ // With AddressSanitizer we would hopefully detect if the assignment tries to
+ // add dangling2 to the now-gone pointee's unsafe pointer array. No promises
+ // though, since it might be optimized away.
+ CheckedUnsafePtr<TypeParam> dangling2;
+ dangling2 = dangling1;
+ ASSERT_TRUE(dangling2);
+}
+
+REGISTER_TYPED_TEST_SUITE_P(TypedCheckedUnsafePtrTest,
+ PointeeWithOneCheckedUnsafePtr,
+ CheckedUnsafePtrCopyConstructed,
+ CheckedUnsafePtrCopyAssigned,
+ PointeeWithOneDanglingCheckedUnsafePtr,
+ PointeeWithOneCopiedDanglingCheckedUnsafePtr,
+ PointeeWithOneCopyAssignedDanglingCheckedUnsafePtr);
+
+using BothTypes = ::testing::Types<BasePointee, DerivedPointee>;
+INSTANTIATE_TYPED_TEST_SUITE_P(InstantiationOf, TypedCheckedUnsafePtrTest,
+ BothTypes);
diff --git a/dom/quota/test/gtest/TestClientUsageArray.cpp b/dom/quota/test/gtest/TestClientUsageArray.cpp
new file mode 100644
index 0000000000..f5db984ccd
--- /dev/null
+++ b/dom/quota/test/gtest/TestClientUsageArray.cpp
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ClientUsageArray.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla::dom::quota;
+
+TEST(DOM_Quota_ClientUsageArray, Deserialize)
+{
+ ClientUsageArray clientUsages;
+ nsresult rv = clientUsages.Deserialize("I872215 C8404073805 L161709"_ns);
+ ASSERT_EQ(rv, NS_OK);
+}
diff --git a/dom/quota/test/gtest/TestEncryptedStream.cpp b/dom/quota/test/gtest/TestEncryptedStream.cpp
new file mode 100644
index 0000000000..b5e64f2cf1
--- /dev/null
+++ b/dom/quota/test/gtest/TestEncryptedStream.cpp
@@ -0,0 +1,786 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstdlib>
+#include <new>
+#include <numeric>
+#include <ostream>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+#include "ErrorList.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/FixedBufferOutputStream.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Scoped.h"
+#include "mozilla/Span.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/SafeRefPtr.h"
+#include "mozilla/dom/quota/DecryptingInputStream_impl.h"
+#include "mozilla/dom/quota/DummyCipherStrategy.h"
+#include "mozilla/dom/quota/EncryptedBlock.h"
+#include "mozilla/dom/quota/EncryptingOutputStream_impl.h"
+#include "mozilla/dom/quota/NSSCipherStrategy.h"
+#include "mozilla/fallible.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsICloneableInputStream.h"
+#include "nsIInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISeekableStream.h"
+#include "nsISupports.h"
+#include "nsITellableStream.h"
+#include "nsStreamUtils.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nscore.h"
+#include "nss.h"
+
+namespace mozilla::dom::quota {
+
+// Similar to ArrayBufferInputStream from netwerk/base/ArrayBufferInputStream.h,
+// but this is initialized from a Span on construction, rather than lazily from
+// a JS ArrayBuffer.
+class ArrayBufferInputStream : public nsIInputStream,
+ public nsISeekableStream,
+ public nsICloneableInputStream {
+ public:
+ explicit ArrayBufferInputStream(mozilla::Span<const uint8_t> aData);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIINPUTSTREAM
+ NS_DECL_NSITELLABLESTREAM
+ NS_DECL_NSISEEKABLESTREAM
+ NS_DECL_NSICLONEABLEINPUTSTREAM
+
+ private:
+ virtual ~ArrayBufferInputStream() = default;
+
+ mozilla::UniquePtr<char[]> mArrayBuffer;
+ uint32_t mBufferLength;
+ uint32_t mPos;
+ bool mClosed;
+};
+
+NS_IMPL_ADDREF(ArrayBufferInputStream);
+NS_IMPL_RELEASE(ArrayBufferInputStream);
+
+NS_INTERFACE_MAP_BEGIN(ArrayBufferInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStream)
+ NS_INTERFACE_MAP_ENTRY(nsISeekableStream)
+ NS_INTERFACE_MAP_ENTRY(nsICloneableInputStream)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStream)
+NS_INTERFACE_MAP_END
+
+ArrayBufferInputStream::ArrayBufferInputStream(
+ mozilla::Span<const uint8_t> aData)
+ : mArrayBuffer(MakeUnique<char[]>(aData.Length())),
+ mBufferLength(aData.Length()),
+ mPos(0),
+ mClosed(false) {
+ std::copy(aData.cbegin(), aData.cend(), mArrayBuffer.get());
+}
+
+NS_IMETHODIMP
+ArrayBufferInputStream::Close() {
+ mClosed = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ArrayBufferInputStream::Available(uint64_t* aCount) {
+ if (mClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ if (mArrayBuffer) {
+ *aCount = mBufferLength ? mBufferLength - mPos : 0;
+ } else {
+ *aCount = 0;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ArrayBufferInputStream::Read(char* aBuf, uint32_t aCount,
+ uint32_t* aReadCount) {
+ return ReadSegments(NS_CopySegmentToBuffer, aBuf, aCount, aReadCount);
+}
+
+NS_IMETHODIMP
+ArrayBufferInputStream::ReadSegments(nsWriteSegmentFun writer, void* closure,
+ uint32_t aCount, uint32_t* result) {
+ MOZ_ASSERT(result, "null ptr");
+ MOZ_ASSERT(mBufferLength >= mPos, "bad stream state");
+
+ if (mClosed) {
+ return NS_BASE_STREAM_CLOSED;
+ }
+
+ MOZ_ASSERT(mArrayBuffer || (mPos == mBufferLength),
+ "stream inited incorrectly");
+
+ *result = 0;
+ while (mPos < mBufferLength) {
+ uint32_t remaining = mBufferLength - mPos;
+ MOZ_ASSERT(mArrayBuffer);
+
+ uint32_t count = std::min(aCount, remaining);
+ if (count == 0) {
+ break;
+ }
+
+ uint32_t written;
+ nsresult rv = writer(this, closure, &mArrayBuffer[0] + mPos, *result, count,
+ &written);
+ if (NS_FAILED(rv)) {
+ // InputStreams do not propagate errors to caller.
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(written <= count,
+ "writer should not write more than we asked it to write");
+ mPos += written;
+ *result += written;
+ aCount -= written;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ArrayBufferInputStream::IsNonBlocking(bool* aNonBlocking) {
+ // Actually, the stream never blocks, but we lie about it because of the
+ // assumptions in DecryptingInputStream.
+ *aNonBlocking = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ArrayBufferInputStream::Tell(int64_t* const aRetval) {
+ MOZ_ASSERT(aRetval);
+
+ *aRetval = mPos;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ArrayBufferInputStream::Seek(const int32_t aWhence,
+ const int64_t aOffset) {
+ // XXX This is not safe. it's hard to use CheckedInt here, though. As long as
+ // the class is only used for testing purposes, that's probably fine.
+
+ int32_t newPos = mPos;
+ switch (aWhence) {
+ case NS_SEEK_SET:
+ newPos = aOffset;
+ break;
+ case NS_SEEK_CUR:
+ newPos += aOffset;
+ break;
+ case NS_SEEK_END:
+ newPos = mBufferLength;
+ newPos += aOffset;
+ break;
+ default:
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (newPos < 0 || static_cast<uint32_t>(newPos) > mBufferLength) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ mPos = newPos;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP ArrayBufferInputStream::SetEOF() {
+ // Truncating is not supported on a read-only stream.
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+NS_IMETHODIMP ArrayBufferInputStream::GetCloneable(bool* aCloneable) {
+ *aCloneable = true;
+ return NS_OK;
+}
+
+NS_IMETHODIMP ArrayBufferInputStream::Clone(nsIInputStream** _retval) {
+ *_retval = MakeAndAddRef<ArrayBufferInputStream>(
+ AsBytes(Span{mArrayBuffer.get(), mBufferLength}))
+ .take();
+
+ return NS_OK;
+}
+} // namespace mozilla::dom::quota
+
+namespace mozilla {
+MOZ_TYPE_SPECIFIC_SCOPED_POINTER_TEMPLATE(ScopedNSSContext, NSSInitContext,
+ NSS_ShutdownContext);
+
+} // namespace mozilla
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+class DOM_Quota_EncryptedStream : public ::testing::Test {
+ public:
+ static void SetUpTestCase() {
+ // Do this only once, do not tear it down per test case.
+ if (!sNssContext) {
+ sNssContext =
+ NSS_InitContext("", "", "", "", nullptr,
+ NSS_INIT_READONLY | NSS_INIT_NOCERTDB |
+ NSS_INIT_NOMODDB | NSS_INIT_FORCEOPEN |
+ NSS_INIT_OPTIMIZESPACE | NSS_INIT_NOROOTINIT);
+ }
+ }
+
+ static void TearDownTestCase() { sNssContext = nullptr; }
+
+ private:
+ inline static ScopedNSSContext sNssContext = ScopedNSSContext{};
+};
+
+enum struct FlushMode { AfterEachChunk, Never };
+enum struct ChunkSize { SingleByte, Unaligned, DataSize };
+
+using PackedTestParams =
+ std::tuple<size_t, ChunkSize, ChunkSize, size_t, FlushMode>;
+
+static size_t EffectiveChunkSize(const ChunkSize aChunkSize,
+ const size_t aDataSize) {
+ switch (aChunkSize) {
+ case ChunkSize::SingleByte:
+ return 1;
+ case ChunkSize::Unaligned:
+ return 17;
+ case ChunkSize::DataSize:
+ return aDataSize;
+ }
+ MOZ_CRASH("Unknown ChunkSize");
+}
+
+struct TestParams {
+ MOZ_IMPLICIT constexpr TestParams(const PackedTestParams& aPackedParams)
+ : mDataSize(std::get<0>(aPackedParams)),
+ mWriteChunkSize(std::get<1>(aPackedParams)),
+ mReadChunkSize(std::get<2>(aPackedParams)),
+ mBlockSize(std::get<3>(aPackedParams)),
+ mFlushMode(std::get<4>(aPackedParams)) {}
+
+ constexpr size_t DataSize() const { return mDataSize; }
+
+ size_t EffectiveWriteChunkSize() const {
+ return EffectiveChunkSize(mWriteChunkSize, mDataSize);
+ }
+
+ size_t EffectiveReadChunkSize() const {
+ return EffectiveChunkSize(mReadChunkSize, mDataSize);
+ }
+
+ constexpr size_t BlockSize() const { return mBlockSize; }
+
+ constexpr enum FlushMode FlushMode() const { return mFlushMode; }
+
+ private:
+ size_t mDataSize;
+
+ ChunkSize mWriteChunkSize;
+ ChunkSize mReadChunkSize;
+
+ size_t mBlockSize;
+ enum FlushMode mFlushMode;
+};
+
+std::string TestParamToString(
+ const testing::TestParamInfo<PackedTestParams>& aTestParams) {
+ const TestParams& testParams = aTestParams.param;
+
+ static constexpr char kSeparator[] = "_";
+
+ std::stringstream ss;
+ ss << "data" << testParams.DataSize() << kSeparator << "writechunk"
+ << testParams.EffectiveWriteChunkSize() << kSeparator << "readchunk"
+ << testParams.EffectiveReadChunkSize() << kSeparator << "block"
+ << testParams.BlockSize() << kSeparator;
+ switch (testParams.FlushMode()) {
+ case FlushMode::Never:
+ ss << "FlushNever";
+ break;
+ case FlushMode::AfterEachChunk:
+ ss << "FlushAfterEachChunk";
+ break;
+ };
+ return ss.str();
+}
+
+class ParametrizedCryptTest
+ : public DOM_Quota_EncryptedStream,
+ public testing::WithParamInterface<PackedTestParams> {};
+
+static auto MakeTestData(const size_t aDataSize) {
+ auto data = nsTArray<uint8_t>();
+ data.SetLength(aDataSize);
+ std::iota(data.begin(), data.end(), 0);
+ return data;
+}
+
+template <typename CipherStrategy>
+static void WriteTestData(nsCOMPtr<nsIOutputStream>&& aBaseOutputStream,
+ const Span<const uint8_t> aData,
+ const size_t aWriteChunkSize, const size_t aBlockSize,
+ const typename CipherStrategy::KeyType& aKey,
+ const FlushMode aFlushMode) {
+ auto outStream = MakeSafeRefPtr<EncryptingOutputStream<CipherStrategy>>(
+ std::move(aBaseOutputStream), aBlockSize, aKey);
+
+ for (auto remaining = aData; !remaining.IsEmpty();) {
+ auto [currentChunk, newRemaining] =
+ remaining.SplitAt(std::min(aWriteChunkSize, remaining.Length()));
+ remaining = newRemaining;
+
+ uint32_t written;
+ EXPECT_EQ(NS_OK, outStream->Write(
+ reinterpret_cast<const char*>(currentChunk.Elements()),
+ currentChunk.Length(), &written));
+ EXPECT_EQ(currentChunk.Length(), written);
+
+ if (aFlushMode == FlushMode::AfterEachChunk) {
+ outStream->Flush();
+ }
+ }
+
+ // Close explicitly so we can check the result.
+ EXPECT_EQ(NS_OK, outStream->Close());
+}
+
+template <typename CipherStrategy>
+static void NoExtraChecks(DecryptingInputStream<CipherStrategy>& aInputStream,
+ Span<const uint8_t> aExpectedData,
+ Span<const uint8_t> aRemainder) {}
+
+template <typename CipherStrategy,
+ typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)>
+static void ReadTestData(
+ DecryptingInputStream<CipherStrategy>& aDecryptingInputStream,
+ const Span<const uint8_t> aExpectedData, const size_t aReadChunkSize,
+ const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) {
+ auto readData = nsTArray<uint8_t>();
+ readData.SetLength(aReadChunkSize);
+ for (auto remainder = aExpectedData; !remainder.IsEmpty();) {
+ auto [currentExpected, newExpectedRemainder] =
+ remainder.SplitAt(std::min(aReadChunkSize, remainder.Length()));
+ remainder = newExpectedRemainder;
+
+ uint32_t read;
+ EXPECT_EQ(NS_OK, aDecryptingInputStream.Read(
+ reinterpret_cast<char*>(readData.Elements()),
+ currentExpected.Length(), &read));
+ EXPECT_EQ(currentExpected.Length(), read);
+ EXPECT_EQ(currentExpected,
+ Span{readData}.First(currentExpected.Length()).AsConst());
+
+ aExtraChecks(aDecryptingInputStream, aExpectedData, remainder);
+ }
+
+ // Expect EOF.
+ uint32_t read;
+ EXPECT_EQ(NS_OK, aDecryptingInputStream.Read(
+ reinterpret_cast<char*>(readData.Elements()),
+ readData.Length(), &read));
+ EXPECT_EQ(0u, read);
+}
+
+template <typename CipherStrategy,
+ typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)>
+static auto ReadTestData(
+ MovingNotNull<nsCOMPtr<nsIInputStream>>&& aBaseInputStream,
+ const Span<const uint8_t> aExpectedData, const size_t aReadChunkSize,
+ const size_t aBlockSize, const typename CipherStrategy::KeyType& aKey,
+ const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) {
+ auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>(
+ std::move(aBaseInputStream), aBlockSize, aKey);
+
+ ReadTestData(*inStream, aExpectedData, aReadChunkSize, aExtraChecks);
+
+ return inStream;
+}
+
+// XXX Change to return the buffer instead.
+template <typename CipherStrategy,
+ typename ExtraChecks = decltype(NoExtraChecks<CipherStrategy>)>
+static RefPtr<FixedBufferOutputStream> DoRoundtripTest(
+ const size_t aDataSize, const size_t aWriteChunkSize,
+ const size_t aReadChunkSize, const size_t aBlockSize,
+ const typename CipherStrategy::KeyType& aKey, const FlushMode aFlushMode,
+ const ExtraChecks& aExtraChecks = NoExtraChecks<CipherStrategy>) {
+ // XXX Add deduction guide for RefPtr from already_AddRefed
+ const auto baseOutputStream = WrapNotNull(
+ RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)});
+
+ const auto data = MakeTestData(aDataSize);
+
+ WriteTestData<CipherStrategy>(
+ nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data},
+ aWriteChunkSize, aBlockSize, aKey, aFlushMode);
+
+ const auto baseInputStream =
+ MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData());
+
+ ReadTestData<CipherStrategy>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), Span{data},
+ aReadChunkSize, aBlockSize, aKey, aExtraChecks);
+
+ return baseOutputStream;
+}
+
+TEST_P(ParametrizedCryptTest, NSSCipherStrategy) {
+ using CipherStrategy = NSSCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ auto keyOrErr = CipherStrategy::GenerateKey();
+ ASSERT_FALSE(keyOrErr.isErr());
+
+ DoRoundtripTest<CipherStrategy>(
+ testParams.DataSize(), testParams.EffectiveWriteChunkSize(),
+ testParams.EffectiveReadChunkSize(), testParams.BlockSize(),
+ keyOrErr.unwrap(), testParams.FlushMode());
+}
+
+TEST_P(ParametrizedCryptTest, DummyCipherStrategy_CheckOutput) {
+ using CipherStrategy = DummyCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ const auto encryptedDataStream = DoRoundtripTest<CipherStrategy>(
+ testParams.DataSize(), testParams.EffectiveWriteChunkSize(),
+ testParams.EffectiveReadChunkSize(), testParams.BlockSize(),
+ CipherStrategy::KeyType{}, testParams.FlushMode());
+
+ if (HasFailure()) {
+ return;
+ }
+
+ const auto encryptedData = encryptedDataStream->WrittenData();
+ const auto encryptedDataSpan = AsBytes(Span(encryptedData));
+
+ const auto plainTestData = MakeTestData(testParams.DataSize());
+ auto encryptedBlock = EncryptedBlock<DummyCipherStrategy::BlockPrefixLength,
+ DummyCipherStrategy::BasicBlockSize>{
+ testParams.BlockSize(),
+ };
+ for (auto [encryptedRemainder, plainRemainder] =
+ std::pair(encryptedDataSpan, Span(plainTestData));
+ !encryptedRemainder.IsEmpty();) {
+ const auto [currentBlock, newEncryptedRemainder] =
+ encryptedRemainder.SplitAt(testParams.BlockSize());
+ encryptedRemainder = newEncryptedRemainder;
+
+ std::copy(currentBlock.cbegin(), currentBlock.cend(),
+ encryptedBlock.MutableWholeBlock().begin());
+
+ ASSERT_FALSE(plainRemainder.IsEmpty());
+ const auto [currentPlain, newPlainRemainder] =
+ plainRemainder.SplitAt(encryptedBlock.ActualPayloadLength());
+ plainRemainder = newPlainRemainder;
+
+ const auto pseudoIV = encryptedBlock.CipherPrefix();
+ const auto payload = encryptedBlock.Payload();
+
+ EXPECT_EQ(Span(DummyCipherStrategy::MakeBlockPrefix()), pseudoIV);
+
+ auto untransformedPayload = nsTArray<uint8_t>();
+ untransformedPayload.SetLength(testParams.BlockSize());
+ DummyCipherStrategy::DummyTransform(payload, untransformedPayload);
+
+ EXPECT_EQ(
+ currentPlain,
+ Span(untransformedPayload).AsConst().First(currentPlain.Length()));
+ }
+}
+
+TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Tell) {
+ using CipherStrategy = DummyCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ DoRoundtripTest<CipherStrategy>(
+ testParams.DataSize(), testParams.EffectiveWriteChunkSize(),
+ testParams.EffectiveReadChunkSize(), testParams.BlockSize(),
+ CipherStrategy::KeyType{}, testParams.FlushMode(),
+ [](auto& inStream, Span<const uint8_t> expectedData,
+ Span<const uint8_t> remainder) {
+ // Check that Tell tells the right position.
+ int64_t pos;
+ EXPECT_EQ(NS_OK, inStream.Tell(&pos));
+ EXPECT_EQ(expectedData.Length() - remainder.Length(),
+ static_cast<uint64_t>(pos));
+ });
+}
+
+TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Available) {
+ using CipherStrategy = DummyCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ DoRoundtripTest<CipherStrategy>(
+ testParams.DataSize(), testParams.EffectiveWriteChunkSize(),
+ testParams.EffectiveReadChunkSize(), testParams.BlockSize(),
+ CipherStrategy::KeyType{}, testParams.FlushMode(),
+ [](auto& inStream, Span<const uint8_t> expectedData,
+ Span<const uint8_t> remainder) {
+ // Check that Available tells the right remainder.
+ uint64_t available;
+ EXPECT_EQ(NS_OK, inStream.Available(&available));
+ EXPECT_EQ(remainder.Length(), available);
+ });
+}
+
+TEST_P(ParametrizedCryptTest, DummyCipherStrategy_Clone) {
+ using CipherStrategy = DummyCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ // XXX Add deduction guide for RefPtr from already_AddRefed
+ const auto baseOutputStream = WrapNotNull(
+ RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)});
+
+ const auto data = MakeTestData(testParams.DataSize());
+
+ WriteTestData<CipherStrategy>(
+ nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data},
+ testParams.EffectiveWriteChunkSize(), testParams.BlockSize(),
+ CipherStrategy::KeyType{}, testParams.FlushMode());
+
+ const auto baseInputStream =
+ MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData());
+
+ const auto inStream = ReadTestData<CipherStrategy>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}), Span{data},
+ testParams.EffectiveReadChunkSize(), testParams.BlockSize(),
+ CipherStrategy::KeyType{});
+
+ nsCOMPtr<nsIInputStream> clonedInputStream;
+ EXPECT_EQ(NS_OK, inStream->Clone(getter_AddRefs(clonedInputStream)));
+
+ ReadTestData(
+ static_cast<DecryptingInputStream<CipherStrategy>&>(*clonedInputStream),
+ Span{data}, testParams.EffectiveReadChunkSize());
+}
+
+// XXX This test is actually only parametrized on the block size.
+TEST_P(ParametrizedCryptTest, DummyCipherStrategy_IncompleteBlock) {
+ using CipherStrategy = DummyCipherStrategy;
+ const TestParams& testParams = GetParam();
+
+ // Provide half a block, content doesn't matter.
+ nsTArray<uint8_t> data;
+ data.SetLength(testParams.BlockSize() / 2);
+
+ const auto baseInputStream = MakeRefPtr<ArrayBufferInputStream>(data);
+
+ const auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}),
+ testParams.BlockSize(), CipherStrategy::KeyType{});
+
+ nsTArray<uint8_t> readData;
+ readData.SetLength(testParams.BlockSize());
+ uint32_t read;
+ EXPECT_EQ(NS_ERROR_CORRUPTED_CONTENT,
+ inStream->Read(reinterpret_cast<char*>(readData.Elements()),
+ readData.Length(), &read));
+}
+
+enum struct SeekOffset {
+ Zero,
+ MinusHalfDataSize,
+ PlusHalfDataSize,
+ PlusDataSize,
+ MinusDataSize
+};
+using SeekOp = std::pair<int32_t, SeekOffset>;
+
+using PackedSeekTestParams = std::tuple<size_t, size_t, std::vector<SeekOp>>;
+
+struct SeekTestParams {
+ size_t mDataSize;
+ size_t mBlockSize;
+ std::vector<SeekOp> mSeekOps;
+
+ MOZ_IMPLICIT SeekTestParams(const PackedSeekTestParams& aPackedParams)
+ : mDataSize(std::get<0>(aPackedParams)),
+ mBlockSize(std::get<1>(aPackedParams)),
+ mSeekOps(std::get<2>(aPackedParams)) {}
+};
+
+std::string SeekTestParamToString(
+ const testing::TestParamInfo<PackedSeekTestParams>& aTestParams) {
+ const SeekTestParams& testParams = aTestParams.param;
+
+ static constexpr char kSeparator[] = "_";
+
+ std::stringstream ss;
+ ss << "data" << testParams.mDataSize << kSeparator << "writechunk"
+ << testParams.mBlockSize << kSeparator;
+ for (const auto& seekOp : testParams.mSeekOps) {
+ switch (seekOp.first) {
+ case nsISeekableStream::NS_SEEK_SET:
+ ss << "Set";
+ break;
+ case nsISeekableStream::NS_SEEK_CUR:
+ ss << "Cur";
+ break;
+ case nsISeekableStream::NS_SEEK_END:
+ ss << "End";
+ break;
+ };
+ switch (seekOp.second) {
+ case SeekOffset::Zero:
+ ss << "Zero";
+ break;
+ case SeekOffset::MinusHalfDataSize:
+ ss << "MinusHalfDataSize";
+ break;
+ case SeekOffset::PlusHalfDataSize:
+ ss << "PlusHalfDataSize";
+ break;
+ case SeekOffset::MinusDataSize:
+ ss << "MinusDataSize";
+ break;
+ case SeekOffset::PlusDataSize:
+ ss << "PlusDataSize";
+ break;
+ };
+ }
+ return ss.str();
+}
+
+class ParametrizedSeekCryptTest
+ : public DOM_Quota_EncryptedStream,
+ public testing::WithParamInterface<PackedSeekTestParams> {};
+
+TEST_P(ParametrizedSeekCryptTest, DummyCipherStrategy_Seek) {
+ using CipherStrategy = DummyCipherStrategy;
+ const SeekTestParams& testParams = GetParam();
+
+ const auto baseOutputStream = WrapNotNull(
+ RefPtr<FixedBufferOutputStream>{FixedBufferOutputStream::Create(2048)});
+
+ const auto data = MakeTestData(testParams.mDataSize);
+
+ WriteTestData<CipherStrategy>(
+ nsCOMPtr<nsIOutputStream>{baseOutputStream.get()}, Span{data},
+ testParams.mDataSize, testParams.mBlockSize, CipherStrategy::KeyType{},
+ FlushMode::Never);
+
+ const auto baseInputStream =
+ MakeRefPtr<ArrayBufferInputStream>(baseOutputStream->WrittenData());
+
+ const auto inStream = MakeSafeRefPtr<DecryptingInputStream<CipherStrategy>>(
+ WrapNotNull(nsCOMPtr<nsIInputStream>{baseInputStream}),
+ testParams.mBlockSize, CipherStrategy::KeyType{});
+
+ uint32_t accumulatedOffset = 0;
+ for (const auto& seekOp : testParams.mSeekOps) {
+ const auto offset = [offsetKind = seekOp.second,
+ dataSize = testParams.mDataSize]() -> int64_t {
+ switch (offsetKind) {
+ case SeekOffset::Zero:
+ return 0;
+ case SeekOffset::MinusHalfDataSize:
+ return -static_cast<int64_t>(dataSize) / 2;
+ case SeekOffset::PlusHalfDataSize:
+ return dataSize / 2;
+ case SeekOffset::MinusDataSize:
+ return -static_cast<int64_t>(dataSize);
+ case SeekOffset::PlusDataSize:
+ return dataSize;
+ }
+ MOZ_CRASH("Unknown SeekOffset");
+ }();
+ switch (seekOp.first) {
+ case nsISeekableStream::NS_SEEK_SET:
+ accumulatedOffset = offset;
+ break;
+ case nsISeekableStream::NS_SEEK_CUR:
+ accumulatedOffset += offset;
+ break;
+ case nsISeekableStream::NS_SEEK_END:
+ accumulatedOffset = testParams.mDataSize + offset;
+ break;
+ }
+ EXPECT_EQ(NS_OK, inStream->Seek(seekOp.first, offset));
+ }
+
+ {
+ int64_t actualOffset;
+ EXPECT_EQ(NS_OK, inStream->Tell(&actualOffset));
+
+ EXPECT_EQ(actualOffset, accumulatedOffset);
+ }
+
+ auto readData = nsTArray<uint8_t>();
+ readData.SetLength(data.Length());
+ uint32_t read;
+ EXPECT_EQ(NS_OK, inStream->Read(reinterpret_cast<char*>(readData.Elements()),
+ readData.Length(), &read));
+ // XXX Or should 'read' indicate the actual number of bytes read,
+ // including the encryption overhead?
+ EXPECT_EQ(testParams.mDataSize - accumulatedOffset, read);
+ EXPECT_EQ(Span{data}.SplitAt(accumulatedOffset).second,
+ Span{readData}.First(read).AsConst());
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ DOM_Quota_EncryptedStream_Parametrized, ParametrizedCryptTest,
+ testing::Combine(
+ /* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u),
+ /* writeChunkSize */
+ testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned,
+ ChunkSize::DataSize),
+ /* readChunkSize */
+ testing::Values(ChunkSize::SingleByte, ChunkSize::Unaligned,
+ ChunkSize::DataSize),
+ /* blockSize */ testing::Values(256u, 1024u /*, 8192u*/),
+ /* flushMode */
+ testing::Values(FlushMode::Never, FlushMode::AfterEachChunk)),
+ TestParamToString);
+
+INSTANTIATE_TEST_SUITE_P(
+ DOM_IndexedDB_EncryptedStream_ParametrizedSeek, ParametrizedSeekCryptTest,
+ testing::Combine(
+ /* dataSize */ testing::Values(0u, 16u, 256u, 512u, 513u),
+ /* blockSize */ testing::Values(256u, 1024u /*, 8192u*/),
+ /* seekOperations */
+ testing::Values(/* NS_SEEK_SET only, single ops */
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET,
+ SeekOffset::PlusDataSize}},
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_SET,
+ SeekOffset::PlusHalfDataSize}},
+ /* NS_SEEK_SET only, multiple ops */
+ std::vector<SeekOp>{
+ {nsISeekableStream::NS_SEEK_SET,
+ SeekOffset::PlusHalfDataSize},
+ {nsISeekableStream::NS_SEEK_SET, SeekOffset::Zero}},
+ /* NS_SEEK_CUR only, single ops */
+ std::vector<SeekOp>{
+ {nsISeekableStream::NS_SEEK_CUR, SeekOffset::Zero}},
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR,
+ SeekOffset::PlusDataSize}},
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_CUR,
+ SeekOffset::PlusHalfDataSize}},
+ /* NS_SEEK_END only, single ops */
+ std::vector<SeekOp>{
+ {nsISeekableStream::NS_SEEK_END, SeekOffset::Zero}},
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END,
+ SeekOffset::MinusDataSize}},
+ std::vector<SeekOp>{{nsISeekableStream::NS_SEEK_END,
+ SeekOffset::MinusHalfDataSize}})),
+ SeekTestParamToString);
diff --git a/dom/quota/test/gtest/TestFileOutputStream.cpp b/dom/quota/test/gtest/TestFileOutputStream.cpp
new file mode 100644
index 0000000000..b62f15363e
--- /dev/null
+++ b/dom/quota/test/gtest/TestFileOutputStream.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/Client.h"
+#include "mozilla/dom/quota/CommonMetadata.h"
+#include "mozilla/dom/quota/FileStreams.h"
+#include "mozilla/dom/quota/QuotaManager.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "QuotaManagerDependencyFixture.h"
+
+namespace mozilla::dom::quota::test {
+
+class TestFileOutputStream : public QuotaManagerDependencyFixture {
+ public:
+ static void SetUpTestCase() {
+ ASSERT_NO_FATAL_FAILURE(InitializeFixture());
+
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ prefs->SetIntPref("dom.quotaManager.temporaryStorage.fixedLimit",
+ mQuotaLimit);
+ }
+
+ static void TearDownTestCase() {
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+
+ prefs->ClearUserPref("dom.quotaManager.temporaryStorage.fixedLimit");
+
+ ASSERT_NO_FATAL_FAILURE(ShutdownFixture());
+ }
+
+ static const int32_t mQuotaLimit = 8192;
+};
+
+TEST_F(TestFileOutputStream, extendFileStreamWithSetEOF) {
+ auto ioTask = []() {
+ quota::QuotaManager* quotaManager = quota::QuotaManager::Get();
+
+ auto originMetadata =
+ quota::OriginMetadata{""_ns, "example.com"_ns, "http://example.com"_ns,
+ quota::PERSISTENCE_TYPE_DEFAULT};
+
+ {
+ ASSERT_NS_SUCCEEDED(quotaManager->EnsureStorageIsInitialized());
+
+ ASSERT_NS_SUCCEEDED(quotaManager->EnsureTemporaryStorageIsInitialized());
+
+ auto res = quotaManager->EnsureTemporaryOriginIsInitialized(
+ quota::PERSISTENCE_TYPE_DEFAULT, originMetadata);
+ ASSERT_TRUE(res.isOk());
+ }
+
+ const int64_t groupLimit =
+ static_cast<int64_t>(quotaManager->GetGroupLimit());
+ ASSERT_TRUE(mQuotaLimit * 1024LL == groupLimit);
+
+ // We don't use the tested stream itself to check the file size as it
+ // may report values which have not been written to disk.
+ RefPtr<quota::FileOutputStream> check = MakeRefPtr<quota::FileOutputStream>(
+ quota::PERSISTENCE_TYPE_DEFAULT, originMetadata,
+ quota::Client::Type::SDB);
+
+ RefPtr<quota::FileOutputStream> stream =
+ MakeRefPtr<quota::FileOutputStream>(quota::PERSISTENCE_TYPE_DEFAULT,
+ originMetadata,
+ quota::Client::Type::SDB);
+
+ {
+ auto testPathRes = quotaManager->GetDirectoryForOrigin(
+ quota::PERSISTENCE_TYPE_DEFAULT, originMetadata.mOrigin);
+
+ ASSERT_TRUE(testPathRes.isOk());
+
+ nsCOMPtr<nsIFile> testPath = testPathRes.unwrap();
+
+ ASSERT_NS_SUCCEEDED(testPath->AppendRelativePath(u"sdb"_ns));
+
+ ASSERT_NS_SUCCEEDED(
+ testPath->AppendRelativePath(u"tTestFileOutputStream.txt"_ns));
+
+ bool exists = true;
+ ASSERT_NS_SUCCEEDED(testPath->Exists(&exists));
+
+ if (exists) {
+ ASSERT_NS_SUCCEEDED(testPath->Remove(/* recursive */ false));
+ }
+
+ ASSERT_NS_SUCCEEDED(testPath->Exists(&exists));
+ ASSERT_FALSE(exists);
+
+ ASSERT_NS_SUCCEEDED(testPath->Create(nsIFile::NORMAL_FILE_TYPE, 0666));
+
+ ASSERT_NS_SUCCEEDED(testPath->Exists(&exists));
+ ASSERT_TRUE(exists);
+
+ nsCOMPtr<nsIFile> checkPath;
+ ASSERT_NS_SUCCEEDED(testPath->Clone(getter_AddRefs(checkPath)));
+
+ const int32_t IOFlags = -1;
+ const int32_t perm = -1;
+ const int32_t behaviorFlags = 0;
+ ASSERT_NS_SUCCEEDED(stream->Init(testPath, IOFlags, perm, behaviorFlags));
+
+ ASSERT_NS_SUCCEEDED(check->Init(testPath, IOFlags, perm, behaviorFlags));
+ }
+
+ // Check that we start with an empty file
+ int64_t avail = 42;
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(0 == avail);
+
+ // Enlarge the file
+ const int64_t toSize = groupLimit;
+ ASSERT_NS_SUCCEEDED(stream->Seek(nsISeekableStream::NS_SEEK_SET, toSize));
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(0 == avail);
+
+ ASSERT_NS_SUCCEEDED(stream->SetEOF());
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toSize == avail);
+
+ // Try to enlarge the file past the limit
+ const int64_t overGroupLimit = groupLimit + 1;
+
+ // Seeking is allowed
+ ASSERT_NS_SUCCEEDED(
+ stream->Seek(nsISeekableStream::NS_SEEK_SET, overGroupLimit));
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toSize == avail);
+
+ // Setting file size to exceed quota should yield no device space error
+ ASSERT_TRUE(NS_ERROR_FILE_NO_DEVICE_SPACE == stream->SetEOF());
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toSize == avail);
+
+ // Shrink the file
+ const int64_t toHalfSize = toSize / 2;
+ ASSERT_NS_SUCCEEDED(
+ stream->Seek(nsISeekableStream::NS_SEEK_SET, toHalfSize));
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toSize == avail);
+
+ ASSERT_NS_SUCCEEDED(stream->SetEOF());
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toHalfSize == avail);
+
+ // Shrink the file back to nothing
+ ASSERT_NS_SUCCEEDED(stream->Seek(nsISeekableStream::NS_SEEK_SET, 0));
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(toHalfSize == avail);
+
+ ASSERT_NS_SUCCEEDED(stream->SetEOF());
+
+ ASSERT_NS_SUCCEEDED(check->GetSize(&avail));
+
+ ASSERT_TRUE(0 == avail);
+ };
+
+ PerformOnIOThread(std::move(ioTask));
+}
+
+} // namespace mozilla::dom::quota::test
diff --git a/dom/quota/test/gtest/TestFlatten.cpp b/dom/quota/test/gtest/TestFlatten.cpp
new file mode 100644
index 0000000000..5ca7675887
--- /dev/null
+++ b/dom/quota/test/gtest/TestFlatten.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Flatten.h"
+
+#include "gtest/gtest.h"
+
+#include "mozilla/Unused.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom::quota {
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunreachable-code-loop-increment"
+#endif
+TEST(Flatten, FlatEmpty)
+{
+ for (const auto& item : Flatten<int>(nsTArray<int>{})) {
+ Unused << item;
+ FAIL();
+ }
+}
+
+TEST(Flatten, NestedOuterEmpty)
+{
+ for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{})) {
+ Unused << item;
+ FAIL();
+ }
+}
+
+TEST(Flatten, NestedInnerEmpty)
+{
+ for (const auto& item :
+ Flatten<int>(nsTArray<CopyableTArray<int>>{CopyableTArray<int>{}})) {
+ Unused << item;
+ FAIL();
+ }
+}
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+TEST(Flatten, NestedInnerSingular)
+{
+ nsTArray<int> flattened;
+ for (const auto& item :
+ Flatten<int>(nsTArray<CopyableTArray<int>>{CopyableTArray<int>{1}})) {
+ flattened.AppendElement(item);
+ }
+
+ EXPECT_EQ(nsTArray{1}, flattened);
+}
+
+TEST(Flatten, NestedInnerSingulars)
+{
+ nsTArray<int> flattened;
+ for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{
+ CopyableTArray<int>{1}, CopyableTArray<int>{2}})) {
+ flattened.AppendElement(item);
+ }
+
+ EXPECT_EQ((nsTArray<int>{{1, 2}}), flattened);
+}
+
+TEST(Flatten, NestedInnerNonSingulars)
+{
+ nsTArray<int> flattened;
+ for (const auto& item : Flatten<int>(nsTArray<CopyableTArray<int>>{
+ CopyableTArray<int>{1, 2}, CopyableTArray<int>{3, 4}})) {
+ flattened.AppendElement(item);
+ }
+
+ EXPECT_EQ((nsTArray<int>{{1, 2, 3, 4}}), flattened);
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/test/gtest/TestForwardDecls.cpp b/dom/quota/test/gtest/TestForwardDecls.cpp
new file mode 100644
index 0000000000..223f83af03
--- /dev/null
+++ b/dom/quota/test/gtest/TestForwardDecls.cpp
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <type_traits>
+#include "mozilla/dom/quota/ForwardDecls.h"
+
+using namespace mozilla;
+
+static_assert(std::is_same_v<OkOrErr, Result<Ok, QMResult>>);
diff --git a/dom/quota/test/gtest/TestPersistenceType.cpp b/dom/quota/test/gtest/TestPersistenceType.cpp
new file mode 100644
index 0000000000..c4f1e5d581
--- /dev/null
+++ b/dom/quota/test/gtest/TestPersistenceType.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/PersistenceType.h"
+
+#include "gtest/gtest.h"
+
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+
+namespace mozilla::dom::quota {
+
+TEST(PersistenceType, FromFile)
+{
+ nsCOMPtr<nsIFile> base;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base));
+ EXPECT_EQ(rv, NS_OK);
+
+ const auto testPersistenceType = [&base](const nsLiteralString& aString,
+ const Maybe<PersistenceType> aType) {
+ nsCOMPtr<nsIFile> file;
+
+ nsresult rv = base->Clone(getter_AddRefs(file));
+ EXPECT_EQ(rv, NS_OK);
+
+ rv = file->Append(aString);
+ EXPECT_EQ(rv, NS_OK);
+
+ auto maybePersistenceType = PersistenceTypeFromFile(*file, fallible);
+ EXPECT_EQ(maybePersistenceType, aType);
+ };
+
+ testPersistenceType(u"permanent"_ns, Some(PERSISTENCE_TYPE_PERSISTENT));
+ testPersistenceType(u"temporary"_ns, Some(PERSISTENCE_TYPE_TEMPORARY));
+ testPersistenceType(u"default"_ns, Some(PERSISTENCE_TYPE_DEFAULT));
+ testPersistenceType(u"persistent"_ns, Nothing());
+ testPersistenceType(u"foobar"_ns, Nothing());
+}
+
+} // namespace mozilla::dom::quota
diff --git a/dom/quota/test/gtest/TestQMResult.cpp b/dom/quota/test/gtest/TestQMResult.cpp
new file mode 100644
index 0000000000..94cbec7364
--- /dev/null
+++ b/dom/quota/test/gtest/TestQMResult.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Common.h"
+#include "gtest/gtest.h"
+#include "mozilla/dom/QMResult.h"
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+class DOM_Quota_QMResult : public DOM_Quota_Test {};
+
+#ifdef QM_ERROR_STACKS_ENABLED
+TEST_F(DOM_Quota_QMResult, Construct_Default) {
+ QMResult res;
+
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(res.StackId(), ExpectedStackId());
+ ASSERT_EQ(res.FrameId(), 1u);
+ ASSERT_EQ(res.NSResult(), NS_OK);
+}
+#endif
+
+TEST_F(DOM_Quota_QMResult, Construct_FromNSResult) {
+ QMResult res(NS_ERROR_FAILURE);
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(res.StackId(), ExpectedStackId());
+ ASSERT_EQ(res.FrameId(), 1u);
+ ASSERT_EQ(res.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(res, NS_ERROR_FAILURE);
+#endif
+}
+
+#ifdef QM_ERROR_STACKS_ENABLED
+TEST_F(DOM_Quota_QMResult, Propagate) {
+ QMResult res1(NS_ERROR_FAILURE);
+
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(res1.StackId(), ExpectedStackId());
+ ASSERT_EQ(res1.FrameId(), 1u);
+ ASSERT_EQ(res1.NSResult(), NS_ERROR_FAILURE);
+
+ QMResult res2 = res1.Propagate();
+
+ ASSERT_EQ(res2.StackId(), ExpectedStackId());
+ ASSERT_EQ(res2.FrameId(), 2u);
+ ASSERT_EQ(res2.NSResult(), NS_ERROR_FAILURE);
+}
+#endif
+
+TEST_F(DOM_Quota_QMResult, ToQMResult) {
+ auto res = ToQMResult(NS_ERROR_FAILURE);
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(res.StackId(), ExpectedStackId());
+ ASSERT_EQ(res.FrameId(), 1u);
+ ASSERT_EQ(res.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(res, NS_ERROR_FAILURE);
+#endif
+}
diff --git a/dom/quota/test/gtest/TestQuotaCommon.cpp b/dom/quota/test/gtest/TestQuotaCommon.cpp
new file mode 100644
index 0000000000..c1a047a533
--- /dev/null
+++ b/dom/quota/test/gtest/TestQuotaCommon.cpp
@@ -0,0 +1,2171 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/QuotaCommon.h"
+
+#include "gtest/gtest.h"
+
+#include <algorithm>
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <map>
+#include <new>
+#include <ostream>
+#include <type_traits>
+#include <utility>
+#include <vector>
+#include "ErrorList.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/Unused.h"
+#include "mozilla/fallible.h"
+#include "mozilla/dom/quota/QuotaTestParent.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+#include "nsCOMPtr.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTLiteralString.h"
+
+class nsISupports;
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+mozilla::ipc::IPCResult QuotaTestParent::RecvTry_Success_CustomErr_QmIpcFail(
+ bool* aTryDidNotReturn) {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_IPC_FAIL(this));
+
+ *aTryDidNotReturn = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult QuotaTestParent::RecvTry_Success_CustomErr_IpcFail(
+ bool* aTryDidNotReturn) {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), IPC_FAIL(this, "Custom why"));
+
+ *aTryDidNotReturn = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+QuotaTestParent::RecvTryInspect_Success_CustomErr_QmIpcFail(
+ bool* aTryDidNotReturn) {
+ QM_TRY_INSPECT(const auto& x, (mozilla::Result<int32_t, nsresult>{42}),
+ QM_IPC_FAIL(this));
+ Unused << x;
+
+ *aTryDidNotReturn = true;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+QuotaTestParent::RecvTryInspect_Success_CustomErr_IpcFail(
+ bool* aTryDidNotReturn) {
+ QM_TRY_INSPECT(const auto& x, (mozilla::Result<int32_t, nsresult>{42}),
+ IPC_FAIL(this, "Custom why"));
+ Unused << x;
+
+ *aTryDidNotReturn = true;
+
+ return IPC_OK();
+}
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wunreachable-code"
+#endif
+
+TEST(QuotaCommon_Try, Success)
+{
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, Success_CustomErr_QmIpcFail)
+{
+ auto foo = MakeRefPtr<QuotaTestParent>();
+
+ bool tryDidNotReturn = false;
+
+ auto res = foo->RecvTry_Success_CustomErr_QmIpcFail(&tryDidNotReturn);
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_TRUE(res);
+}
+
+TEST(QuotaCommon_Try, Success_CustomErr_IpcFail)
+{
+ auto foo = MakeRefPtr<QuotaTestParent>();
+
+ bool tryDidNotReturn = false;
+
+ auto res = foo->RecvTry_Success_CustomErr_IpcFail(&tryDidNotReturn);
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_TRUE(res);
+}
+
+#ifdef DEBUG
+TEST(QuotaCommon_Try, Success_CustomErr_AssertUnreachable)
+{
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_ASSERT_UNREACHABLE);
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, Success_NoErr_AssertUnreachable)
+{
+ bool tryDidNotReturn = false;
+
+ [&tryDidNotReturn]() -> void {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_ASSERT_UNREACHABLE_VOID);
+
+ tryDidNotReturn = true;
+ }();
+
+ EXPECT_TRUE(tryDidNotReturn);
+}
+#else
+# if defined(QM_ASSERT_UNREACHABLE) || defined(QM_ASSERT_UNREACHABLE_VOID)
+#error QM_ASSERT_UNREACHABLE and QM_ASSERT_UNREACHABLE_VOID should not be defined.
+# endif
+#endif
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+TEST(QuotaCommon_Try, Success_CustomErr_DiagnosticAssertUnreachable)
+{
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_DIAGNOSTIC_ASSERT_UNREACHABLE);
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, Success_NoErr_DiagnosticAssertUnreachable)
+{
+ bool tryDidNotReturn = false;
+
+ [&tryDidNotReturn]() -> void {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID);
+
+ tryDidNotReturn = true;
+ }();
+
+ EXPECT_TRUE(tryDidNotReturn);
+}
+#else
+# if defined(QM_DIAGNOSTIC_ASSERT_UNREACHABLE) || \
+ defined(QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID)
+#error QM_DIAGNOSTIC_ASSERT_UNREACHABLE and QM_DIAGNOSTIC_ASSERT_UNREACHABLE_VOID should not be defined.
+# endif
+#endif
+
+TEST(QuotaCommon_Try, Success_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryDidNotReturn = false; \
+ \
+ nsresult rv = [&tryDidNotReturn]() -> nsresult { \
+ QM_TRY(MOZ_TO_RESULT(NS_OK), [](__VA_ARGS__) { return aRv; }); \
+ \
+ tryDidNotReturn = true; \
+ \
+ return NS_OK; \
+ }(); \
+ \
+ EXPECT_TRUE(tryDidNotReturn); \
+ EXPECT_EQ(rv, NS_OK); \
+ }
+
+ SUBTEST(const char*, nsresult aRv);
+ SUBTEST(nsresult aRv);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_Try, Success_WithCleanup)
+{
+ bool tryCleanupRan = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryCleanupRan, &tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_PROPAGATE,
+ [&tryCleanupRan](const auto&) { tryCleanupRan = true; });
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryCleanupRan);
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, Failure_PropagateErr)
+{
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Try, Failure_CustomErr)
+{
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), NS_ERROR_UNEXPECTED);
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_Try, Failure_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryDidNotReturn = false; \
+ \
+ nsresult rv = [&tryDidNotReturn]() -> nsresult { \
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), \
+ [](__VA_ARGS__) { return NS_ERROR_UNEXPECTED; }); \
+ \
+ tryDidNotReturn = true; \
+ \
+ return NS_OK; \
+ }(); \
+ \
+ EXPECT_FALSE(tryDidNotReturn); \
+ EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); \
+ }
+
+ SUBTEST(const char* aFunc, nsresult);
+ SUBTEST(nsresult rv);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_Try, Failure_NoErr)
+{
+ bool tryDidNotReturn = false;
+
+ [&tryDidNotReturn]() -> void {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_VOID);
+
+ tryDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryDidNotReturn);
+}
+
+TEST(QuotaCommon_Try, Failure_WithCleanup)
+{
+ bool tryCleanupRan = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&tryCleanupRan, &tryDidNotReturn]() -> nsresult {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_PROPAGATE,
+ [&tryCleanupRan](const auto& result) {
+ EXPECT_EQ(result, NS_ERROR_FAILURE);
+
+ tryCleanupRan = true;
+ });
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryCleanupRan);
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Try, Failure_WithCleanup_UnwrapErr)
+{
+ bool tryCleanupRan = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv;
+
+ [&tryCleanupRan, &tryDidNotReturn](nsresult& aRv) -> void {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE), QM_VOID,
+ ([&tryCleanupRan, &aRv](auto& result) {
+ EXPECT_EQ(result, NS_ERROR_FAILURE);
+
+ aRv = result;
+
+ tryCleanupRan = true;
+ }));
+
+ tryDidNotReturn = true;
+
+ aRv = NS_OK;
+ }(rv);
+
+ EXPECT_TRUE(tryCleanupRan);
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Try, SameLine)
+{
+ // clang-format off
+ QM_TRY(MOZ_TO_RESULT(NS_OK), QM_VOID); QM_TRY(MOZ_TO_RESULT(NS_OK), QM_VOID);
+ // clang-format on
+}
+
+TEST(QuotaCommon_Try, NestingMadness_Success)
+{
+ bool nestedTryDidNotReturn = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&nestedTryDidNotReturn, &tryDidNotReturn]() -> nsresult {
+ QM_TRY(([&nestedTryDidNotReturn]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ nestedTryDidNotReturn = true;
+
+ return Ok();
+ }()));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTryDidNotReturn);
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, NestingMadness_Failure)
+{
+ bool nestedTryDidNotReturn = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&nestedTryDidNotReturn, &tryDidNotReturn]() -> nsresult {
+ QM_TRY(([&nestedTryDidNotReturn]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE));
+
+ nestedTryDidNotReturn = true;
+
+ return Ok();
+ }()));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(nestedTryDidNotReturn);
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Try, NestingMadness_Multiple_Success)
+{
+ bool nestedTry1DidNotReturn = false;
+ bool nestedTry2DidNotReturn = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn,
+ &tryDidNotReturn]() -> nsresult {
+ QM_TRY(([&nestedTry1DidNotReturn,
+ &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ nestedTry1DidNotReturn = true;
+
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ nestedTry2DidNotReturn = true;
+
+ return Ok();
+ }()));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTry1DidNotReturn);
+ EXPECT_TRUE(nestedTry2DidNotReturn);
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_Try, NestingMadness_Multiple_Failure1)
+{
+ bool nestedTry1DidNotReturn = false;
+ bool nestedTry2DidNotReturn = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn,
+ &tryDidNotReturn]() -> nsresult {
+ QM_TRY(([&nestedTry1DidNotReturn,
+ &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE));
+
+ nestedTry1DidNotReturn = true;
+
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ nestedTry2DidNotReturn = true;
+
+ return Ok();
+ }()));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(nestedTry1DidNotReturn);
+ EXPECT_FALSE(nestedTry2DidNotReturn);
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Try, NestingMadness_Multiple_Failure2)
+{
+ bool nestedTry1DidNotReturn = false;
+ bool nestedTry2DidNotReturn = false;
+ bool tryDidNotReturn = false;
+
+ nsresult rv = [&nestedTry1DidNotReturn, &nestedTry2DidNotReturn,
+ &tryDidNotReturn]() -> nsresult {
+ QM_TRY(([&nestedTry1DidNotReturn,
+ &nestedTry2DidNotReturn]() -> Result<Ok, nsresult> {
+ QM_TRY(MOZ_TO_RESULT(NS_OK));
+
+ nestedTry1DidNotReturn = true;
+
+ QM_TRY(MOZ_TO_RESULT(NS_ERROR_FAILURE));
+
+ nestedTry2DidNotReturn = true;
+
+ return Ok();
+ }()));
+
+ tryDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTry1DidNotReturn);
+ EXPECT_FALSE(nestedTry2DidNotReturn);
+ EXPECT_FALSE(tryDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, Success)
+{
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}));
+ EXPECT_EQ(x, 42);
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_TryInspect, Success_CustomErr_QmIpcFail)
+{
+ auto foo = MakeRefPtr<QuotaTestParent>();
+
+ bool tryDidNotReturn = false;
+
+ auto res = foo->RecvTryInspect_Success_CustomErr_QmIpcFail(&tryDidNotReturn);
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_TRUE(res);
+}
+
+TEST(QuotaCommon_TryInspect, Success_CustomErr_IpcFail)
+{
+ auto foo = MakeRefPtr<QuotaTestParent>();
+
+ bool tryDidNotReturn = false;
+
+ auto res = foo->RecvTryInspect_Success_CustomErr_IpcFail(&tryDidNotReturn);
+
+ EXPECT_TRUE(tryDidNotReturn);
+ EXPECT_TRUE(res);
+}
+
+#ifdef DEBUG
+TEST(QuotaCommon_TryInspect, Success_CustomErr_AssertUnreachable)
+{
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}),
+ QM_ASSERT_UNREACHABLE);
+ EXPECT_EQ(x, 42);
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_TryInspect, Success_NoErr_AssertUnreachable)
+{
+ bool tryInspectDidNotReturn = false;
+
+ [&tryInspectDidNotReturn]() -> void {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}),
+ QM_ASSERT_UNREACHABLE_VOID);
+ EXPECT_EQ(x, 42);
+
+ tryInspectDidNotReturn = true;
+ }();
+
+ EXPECT_TRUE(tryInspectDidNotReturn);
+}
+#endif
+
+TEST(QuotaCommon_TryInspect, Success_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryInspectDidNotReturn = false; \
+ \
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { \
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}), \
+ [](__VA_ARGS__) { return aRv; }); \
+ EXPECT_EQ(x, 42); \
+ \
+ tryInspectDidNotReturn = true; \
+ \
+ return NS_OK; \
+ }(); \
+ \
+ EXPECT_TRUE(tryInspectDidNotReturn); \
+ EXPECT_EQ(rv, NS_OK); \
+ }
+
+ SUBTEST(const char*, nsresult aRv);
+ SUBTEST(nsresult aRv);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_TryInspect, Success_WithCleanup)
+{
+ bool tryInspectCleanupRan = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectCleanupRan, &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& x, (Result<int32_t, nsresult>{42}), QM_PROPAGATE,
+ [&tryInspectCleanupRan](const auto&) { tryInspectCleanupRan = true; });
+ EXPECT_EQ(x, 42);
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryInspectCleanupRan);
+ EXPECT_TRUE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_TryInspect, Failure_PropagateErr)
+{
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, Failure_CustomErr)
+{
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}),
+ NS_ERROR_UNEXPECTED);
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_TryInspect, Failure_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryInspectDidNotReturn = false; \
+ \
+ nsresult rv = [&tryInspectDidNotReturn]() -> nsresult { \
+ QM_TRY_INSPECT(const auto& x, \
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), \
+ [](__VA_ARGS__) { return NS_ERROR_UNEXPECTED; }); \
+ Unused << x; \
+ \
+ tryInspectDidNotReturn = true; \
+ \
+ return NS_OK; \
+ }(); \
+ \
+ EXPECT_FALSE(tryInspectDidNotReturn); \
+ EXPECT_EQ(rv, NS_ERROR_UNEXPECTED); \
+ }
+
+ SUBTEST(const char*, nsresult);
+ SUBTEST(nsresult);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_TryInspect, Failure_NoErr)
+{
+ bool tryInspectDidNotReturn = false;
+
+ [&tryInspectDidNotReturn]() -> void {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), QM_VOID);
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryInspectDidNotReturn);
+}
+
+TEST(QuotaCommon_TryInspect, Failure_WithCleanup)
+{
+ bool tryInspectCleanupRan = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&tryInspectCleanupRan, &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}),
+ QM_PROPAGATE, [&tryInspectCleanupRan](const auto& result) {
+ EXPECT_EQ(result, NS_ERROR_FAILURE);
+
+ tryInspectCleanupRan = true;
+ });
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(tryInspectCleanupRan);
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, Failure_WithCleanup_UnwrapErr)
+{
+ bool tryInspectCleanupRan = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv;
+
+ [&tryInspectCleanupRan, &tryInspectDidNotReturn](nsresult& aRv) -> void {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), QM_VOID,
+ ([&tryInspectCleanupRan, &aRv](auto& result) {
+ EXPECT_EQ(result, NS_ERROR_FAILURE);
+
+ aRv = result;
+
+ tryInspectCleanupRan = true;
+ }));
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+
+ aRv = NS_OK;
+ }(rv);
+
+ EXPECT_TRUE(tryInspectCleanupRan);
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, ConstDecl)
+{
+ QM_TRY_INSPECT(const int32_t& x, (Result<int32_t, nsresult>{42}), QM_VOID);
+
+ static_assert(std::is_same_v<decltype(x), const int32_t&>);
+
+ EXPECT_EQ(x, 42);
+}
+
+TEST(QuotaCommon_TryInspect, SameScopeDecl)
+{
+ QM_TRY_INSPECT(const int32_t& x, (Result<int32_t, nsresult>{42}), QM_VOID);
+ EXPECT_EQ(x, 42);
+
+ QM_TRY_INSPECT(const int32_t& y, (Result<int32_t, nsresult>{42}), QM_VOID);
+ EXPECT_EQ(y, 42);
+}
+
+TEST(QuotaCommon_TryInspect, SameLine)
+{
+ // clang-format off
+ QM_TRY_INSPECT(const auto &x, (Result<int32_t, nsresult>{42}), QM_VOID); QM_TRY_INSPECT(const auto &y, (Result<int32_t, nsresult>{42}), QM_VOID);
+ // clang-format on
+
+ EXPECT_EQ(x, 42);
+ EXPECT_EQ(y, 42);
+}
+
+TEST(QuotaCommon_TryInspect, NestingMadness_Success)
+{
+ bool nestedTryInspectDidNotReturn = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&nestedTryInspectDidNotReturn,
+ &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& x,
+ ([&nestedTryInspectDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}));
+
+ nestedTryInspectDidNotReturn = true;
+
+ return x;
+ }()));
+ EXPECT_EQ(x, 42);
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTryInspectDidNotReturn);
+ EXPECT_TRUE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_TryInspect, NestingMadness_Failure)
+{
+ bool nestedTryInspectDidNotReturn = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&nestedTryInspectDidNotReturn,
+ &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& x,
+ ([&nestedTryInspectDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+
+ nestedTryInspectDidNotReturn = true;
+
+ return x;
+ }()));
+ Unused << x;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(nestedTryInspectDidNotReturn);
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Success)
+{
+ bool nestedTryInspect1DidNotReturn = false;
+ bool nestedTryInspect2DidNotReturn = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn,
+ &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& z,
+ ([&nestedTryInspect1DidNotReturn,
+ &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}));
+
+ nestedTryInspect1DidNotReturn = true;
+
+ QM_TRY_INSPECT(const auto& y, (Result<int32_t, nsresult>{42}));
+
+ nestedTryInspect2DidNotReturn = true;
+
+ return x + y;
+ }()));
+ EXPECT_EQ(z, 84);
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTryInspect1DidNotReturn);
+ EXPECT_TRUE(nestedTryInspect2DidNotReturn);
+ EXPECT_TRUE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_OK);
+}
+
+TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Failure1)
+{
+ bool nestedTryInspect1DidNotReturn = false;
+ bool nestedTryInspect2DidNotReturn = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn,
+ &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& z,
+ ([&nestedTryInspect1DidNotReturn,
+ &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_INSPECT(const auto& x,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+
+ nestedTryInspect1DidNotReturn = true;
+
+ QM_TRY_INSPECT(const auto& y, (Result<int32_t, nsresult>{42}));
+
+ nestedTryInspect2DidNotReturn = true;
+
+ return x + y;
+ }()));
+ Unused << z;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(nestedTryInspect1DidNotReturn);
+ EXPECT_FALSE(nestedTryInspect2DidNotReturn);
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryInspect, NestingMadness_Multiple_Failure2)
+{
+ bool nestedTryInspect1DidNotReturn = false;
+ bool nestedTryInspect2DidNotReturn = false;
+ bool tryInspectDidNotReturn = false;
+
+ nsresult rv = [&nestedTryInspect1DidNotReturn, &nestedTryInspect2DidNotReturn,
+ &tryInspectDidNotReturn]() -> nsresult {
+ QM_TRY_INSPECT(
+ const auto& z,
+ ([&nestedTryInspect1DidNotReturn,
+ &nestedTryInspect2DidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_INSPECT(const auto& x, (Result<int32_t, nsresult>{42}));
+
+ nestedTryInspect1DidNotReturn = true;
+
+ QM_TRY_INSPECT(const auto& y,
+ (Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+
+ nestedTryInspect2DidNotReturn = true;
+
+ return x + y;
+ }()));
+ Unused << z;
+
+ tryInspectDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(nestedTryInspect1DidNotReturn);
+ EXPECT_FALSE(nestedTryInspect2DidNotReturn);
+ EXPECT_FALSE(tryInspectDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+// We are not repeating all QM_TRY_INSPECT test cases for QM_TRY_UNWRAP, since
+// they are largely based on the same implementation. We just add some where
+// inspecting and unwrapping differ.
+
+TEST(QuotaCommon_TryUnwrap, NonConstDecl)
+{
+ QM_TRY_UNWRAP(int32_t x, (Result<int32_t, nsresult>{42}), QM_VOID);
+
+ static_assert(std::is_same_v<decltype(x), int32_t>);
+
+ EXPECT_EQ(x, 42);
+}
+
+TEST(QuotaCommon_TryUnwrap, RvalueDecl)
+{
+ QM_TRY_UNWRAP(int32_t && x, (Result<int32_t, nsresult>{42}), QM_VOID);
+
+ static_assert(std::is_same_v<decltype(x), int32_t&&>);
+
+ EXPECT_EQ(x, 42);
+}
+
+TEST(QuotaCommon_TryUnwrap, ParenDecl)
+{
+ QM_TRY_UNWRAP(
+ (auto&& [x, y]),
+ (Result<std::pair<int32_t, bool>, nsresult>{std::pair{42, true}}),
+ QM_VOID);
+
+ static_assert(std::is_same_v<decltype(x), int32_t>);
+ static_assert(std::is_same_v<decltype(y), bool>);
+
+ EXPECT_EQ(x, 42);
+ EXPECT_EQ(y, true);
+}
+
+TEST(QuotaCommon_TryReturn, Success)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn] {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{42}));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_TryReturn, Success_nsresult)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn] {
+ QM_TRY_RETURN(MOZ_TO_RESULT(NS_OK));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isOk());
+}
+
+#ifdef DEBUG
+TEST(QuotaCommon_TryReturn, Success_CustomErr_AssertUnreachable)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{42}), QM_ASSERT_UNREACHABLE);
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+#endif
+
+TEST(QuotaCommon_TryReturn, Success_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryReturnDidNotReturn = false; \
+ \
+ auto res = [&tryReturnDidNotReturn]() -> Result<Ok, nsresult> { \
+ QM_TRY_RETURN(MOZ_TO_RESULT(NS_OK), \
+ [](__VA_ARGS__) { return Err(aRv); }); \
+ \
+ tryReturnDidNotReturn = true; \
+ }(); \
+ \
+ EXPECT_FALSE(tryReturnDidNotReturn); \
+ EXPECT_TRUE(res.isOk()); \
+ }
+
+ SUBTEST(const char*, nsresult aRv);
+ SUBTEST(nsresult aRv);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_TryReturn, Success_WithCleanup)
+{
+ bool tryReturnCleanupRan = false;
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnCleanupRan,
+ &tryReturnDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_RETURN(
+ (Result<int32_t, nsresult>{42}), QM_PROPAGATE,
+ [&tryReturnCleanupRan](const auto&) { tryReturnCleanupRan = true; });
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnCleanupRan);
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_TryReturn, Failure_PropagateErr)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn] {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryReturn, Failure_PropagateErr_nsresult)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn] {
+ QM_TRY_RETURN(MOZ_TO_RESULT(NS_ERROR_FAILURE));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryReturn, Failure_CustomErr)
+{
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}),
+ Err(NS_ERROR_UNEXPECTED));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_TryReturn, Failure_CustomErr_CustomLambda)
+{
+#define SUBTEST(...) \
+ { \
+ bool tryReturnDidNotReturn = false; \
+ \
+ auto res = [&tryReturnDidNotReturn]() -> Result<int32_t, nsresult> { \
+ QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}), \
+ [](__VA_ARGS__) { return Err(NS_ERROR_UNEXPECTED); }); \
+ \
+ tryReturnDidNotReturn = true; \
+ }(); \
+ \
+ EXPECT_FALSE(tryReturnDidNotReturn); \
+ EXPECT_TRUE(res.isErr()); \
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED); \
+ }
+
+ SUBTEST(const char*, nsresult);
+ SUBTEST(nsresult);
+
+#undef SUBTEST
+}
+
+TEST(QuotaCommon_TryReturn, Failure_WithCleanup)
+{
+ bool tryReturnCleanupRan = false;
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&tryReturnCleanupRan,
+ &tryReturnDidNotReturn]() -> Result<int32_t, nsresult> {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}),
+ QM_PROPAGATE, [&tryReturnCleanupRan](const auto& result) {
+ EXPECT_EQ(result, NS_ERROR_FAILURE);
+
+ tryReturnCleanupRan = true;
+ });
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_TRUE(tryReturnCleanupRan);
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_TryReturn, SameLine)
+{
+ // clang-format off
+ auto res1 = [] { QM_TRY_RETURN((Result<int32_t, nsresult>{42})); }(); auto res2 = []() -> Result<int32_t, nsresult> { QM_TRY_RETURN((Result<int32_t, nsresult>{42})); }();
+ // clang-format on
+
+ EXPECT_TRUE(res1.isOk());
+ EXPECT_EQ(res1.unwrap(), 42);
+ EXPECT_TRUE(res2.isOk());
+ EXPECT_EQ(res2.unwrap(), 42);
+}
+
+TEST(QuotaCommon_TryReturn, NestingMadness_Success)
+{
+ bool nestedTryReturnDidNotReturn = false;
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&nestedTryReturnDidNotReturn, &tryReturnDidNotReturn] {
+ QM_TRY_RETURN(([&nestedTryReturnDidNotReturn] {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{42}));
+
+ nestedTryReturnDidNotReturn = true;
+ }()));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(nestedTryReturnDidNotReturn);
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_TryReturn, NestingMadness_Failure)
+{
+ bool nestedTryReturnDidNotReturn = false;
+ bool tryReturnDidNotReturn = false;
+
+ auto res = [&nestedTryReturnDidNotReturn, &tryReturnDidNotReturn] {
+ QM_TRY_RETURN(([&nestedTryReturnDidNotReturn] {
+ QM_TRY_RETURN((Result<int32_t, nsresult>{Err(NS_ERROR_FAILURE)}));
+
+ nestedTryReturnDidNotReturn = true;
+ }()));
+
+ tryReturnDidNotReturn = true;
+ }();
+
+ EXPECT_FALSE(nestedTryReturnDidNotReturn);
+ EXPECT_FALSE(tryReturnDidNotReturn);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Fail, ReturnValue)
+{
+ bool failDidNotReturn = false;
+
+ nsresult rv = [&failDidNotReturn]() -> nsresult {
+ QM_FAIL(NS_ERROR_FAILURE);
+
+ failDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_FALSE(failDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_Fail, ReturnValue_WithCleanup)
+{
+ bool failCleanupRan = false;
+ bool failDidNotReturn = false;
+
+ nsresult rv = [&failCleanupRan, &failDidNotReturn]() -> nsresult {
+ QM_FAIL(NS_ERROR_FAILURE, [&failCleanupRan]() { failCleanupRan = true; });
+
+ failDidNotReturn = true;
+
+ return NS_OK;
+ }();
+
+ EXPECT_TRUE(failCleanupRan);
+ EXPECT_FALSE(failDidNotReturn);
+ EXPECT_EQ(rv, NS_ERROR_FAILURE);
+}
+
+TEST(QuotaCommon_WarnOnlyTry, Success)
+{
+ bool warnOnlyTryDidNotReturn = false;
+
+ const auto res =
+ [&warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY(OkIf(true));
+
+ warnOnlyTryDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTry, Success_WithCleanup)
+{
+ bool warnOnlyTryCleanupRan = false;
+ bool warnOnlyTryDidNotReturn = false;
+
+ const auto res =
+ [&warnOnlyTryCleanupRan,
+ &warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY(OkIf(true), [&warnOnlyTryCleanupRan](const auto&) {
+ warnOnlyTryCleanupRan = true;
+ });
+
+ warnOnlyTryDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_FALSE(warnOnlyTryCleanupRan);
+ EXPECT_TRUE(warnOnlyTryDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTry, Failure)
+{
+ bool warnOnlyTryDidNotReturn = false;
+
+ const auto res =
+ [&warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY(OkIf(false));
+
+ warnOnlyTryDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTry, Failure_WithCleanup)
+{
+ bool warnOnlyTryCleanupRan = false;
+ bool warnOnlyTryDidNotReturn = false;
+
+ const auto res =
+ [&warnOnlyTryCleanupRan,
+ &warnOnlyTryDidNotReturn]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY(OkIf(false), ([&warnOnlyTryCleanupRan](const auto&) {
+ warnOnlyTryCleanupRan = true;
+ }));
+
+ warnOnlyTryDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryCleanupRan);
+ EXPECT_TRUE(warnOnlyTryDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTryUnwrap, Success)
+{
+ bool warnOnlyTryUnwrapDidNotReturn = false;
+
+ const auto res = [&warnOnlyTryUnwrapDidNotReturn]()
+ -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{42}));
+ EXPECT_TRUE(x);
+ EXPECT_EQ(*x, 42);
+
+ warnOnlyTryUnwrapDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTryUnwrap, Success_WithCleanup)
+{
+ bool warnOnlyTryUnwrapCleanupRan = false;
+ bool warnOnlyTryUnwrapDidNotReturn = false;
+
+ const auto res = [&warnOnlyTryUnwrapCleanupRan,
+ &warnOnlyTryUnwrapDidNotReturn]()
+ -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{42}),
+ [&warnOnlyTryUnwrapCleanupRan](const auto&) {
+ warnOnlyTryUnwrapCleanupRan = true;
+ });
+ EXPECT_TRUE(x);
+ EXPECT_EQ(*x, 42);
+
+ warnOnlyTryUnwrapDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_FALSE(warnOnlyTryUnwrapCleanupRan);
+ EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTryUnwrap, Failure)
+{
+ bool warnOnlyTryUnwrapDidNotReturn = false;
+
+ const auto res = [&warnOnlyTryUnwrapDidNotReturn]()
+ -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY_UNWRAP(const auto x,
+ (Result<int32_t, NotOk>{Err(NotOk{})}));
+ EXPECT_FALSE(x);
+
+ warnOnlyTryUnwrapDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn);
+}
+
+TEST(QuotaCommon_WarnOnlyTryUnwrap, Failure_WithCleanup)
+{
+ bool warnOnlyTryUnwrapCleanupRan = false;
+ bool warnOnlyTryUnwrapDidNotReturn = false;
+
+ const auto res = [&warnOnlyTryUnwrapCleanupRan,
+ &warnOnlyTryUnwrapDidNotReturn]()
+ -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_WARNONLY_TRY_UNWRAP(const auto x, (Result<int32_t, NotOk>{Err(NotOk{})}),
+ [&warnOnlyTryUnwrapCleanupRan](const auto&) {
+ warnOnlyTryUnwrapCleanupRan = true;
+ });
+ EXPECT_FALSE(x);
+
+ warnOnlyTryUnwrapDidNotReturn = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(warnOnlyTryUnwrapCleanupRan);
+ EXPECT_TRUE(warnOnlyTryUnwrapDidNotReturn);
+}
+
+TEST(QuotaCommon_OrElseWarn, Success)
+{
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN(OkIf(true), ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{
+ mozilla::Ok{}};
+ })));
+
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_FALSE(fallbackRun);
+ EXPECT_TRUE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarn, Failure_MappedToSuccess)
+{
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ // XXX Consider allowing to set a custom error handler, so that we can
+ // actually assert that a warning was emitted.
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN(OkIf(false), ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{
+ mozilla::Ok{}};
+ })));
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(fallbackRun);
+ EXPECT_TRUE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarn, Failure_MappedToError)
+{
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ // XXX Consider allowing to set a custom error handler, so that we can
+ // actually assert that a warning was emitted.
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN(OkIf(false), ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{
+ NotOk{}};
+ })));
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_TRUE(fallbackRun);
+ EXPECT_FALSE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarnIf, Success)
+{
+ bool predicateRun = false;
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ OkIf(true),
+ [&predicateRun](const NotOk) {
+ predicateRun = true;
+ return false;
+ },
+ ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}};
+ })));
+
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_FALSE(predicateRun);
+ EXPECT_FALSE(fallbackRun);
+ EXPECT_TRUE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsFalse)
+{
+ bool predicateRun = false;
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ OkIf(false),
+ [&predicateRun](const NotOk) {
+ predicateRun = true;
+ return false;
+ },
+ ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}};
+ })));
+
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_TRUE(predicateRun);
+ EXPECT_FALSE(fallbackRun);
+ EXPECT_FALSE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsTrue_MappedToSuccess)
+{
+ bool predicateRun = false;
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ OkIf(false),
+ [&predicateRun](const NotOk) {
+ predicateRun = true;
+ return true;
+ },
+ ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{mozilla::Ok{}};
+ })));
+
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_TRUE(predicateRun);
+ EXPECT_TRUE(fallbackRun);
+ EXPECT_TRUE(tryContinued);
+}
+
+TEST(QuotaCommon_OrElseWarnIf, Failure_PredicateReturnsTrue_MappedToError)
+{
+ bool predicateRun = false;
+ bool fallbackRun = false;
+ bool tryContinued = false;
+
+ const auto res = [&]() -> mozilla::Result<mozilla::Ok, NotOk> {
+ QM_TRY(QM_OR_ELSE_WARN_IF(
+ OkIf(false),
+ [&predicateRun](const NotOk) {
+ predicateRun = true;
+ return true;
+ },
+ ([&fallbackRun](const NotOk) {
+ fallbackRun = true;
+ return mozilla::Result<mozilla::Ok, NotOk>{mozilla::NotOk{}};
+ })));
+
+ tryContinued = true;
+ return mozilla::Ok{};
+ }();
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_TRUE(predicateRun);
+ EXPECT_TRUE(fallbackRun);
+ EXPECT_FALSE(tryContinued);
+}
+
+TEST(QuotaCommon_OkIf, True)
+{
+ auto res = OkIf(true);
+
+ EXPECT_TRUE(res.isOk());
+}
+
+TEST(QuotaCommon_OkIf, False)
+{
+ auto res = OkIf(false);
+
+ EXPECT_TRUE(res.isErr());
+}
+
+TEST(QuotaCommon_OkToOk, Bool_True)
+{
+ auto res = OkToOk<true>(Ok());
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), true);
+}
+
+TEST(QuotaCommon_OkToOk, Bool_False)
+{
+ auto res = OkToOk<false>(Ok());
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), false);
+}
+
+TEST(QuotaCommon_OkToOk, Int_42)
+{
+ auto res = OkToOk<42>(Ok());
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Bool_True)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, true>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), true);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Bool_True_Err)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, true>(NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Bool_False)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, false>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), false);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Bool_False_Err)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, false>(NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Int_42)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, 42>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, Int_42_Err)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, 42>(NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, NsCOMPtr_nullptr)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, nullptr, nsCOMPtr<nsISupports>>(
+ NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), nullptr);
+}
+
+TEST(QuotaCommon_ErrToOkOrErr, NsCOMPtr_nullptr_Err)
+{
+ auto res = ErrToOkOrErr<NS_ERROR_FAILURE, nullptr, nsCOMPtr<nsISupports>>(
+ NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_ErrToDefaultOkOrErr, Ok)
+{
+ auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, Ok>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+}
+
+TEST(QuotaCommon_ErrToDefaultOkOrErr, Ok_Err)
+{
+ auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, Ok>(NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_ErrToDefaultOkOrErr, NsCOMPtr)
+{
+ auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, nsCOMPtr<nsISupports>>(
+ NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), nullptr);
+}
+
+TEST(QuotaCommon_ErrToDefaultOkOrErr, NsCOMPtr_Err)
+{
+ auto res = ErrToDefaultOkOrErr<NS_ERROR_FAILURE, nsCOMPtr<nsISupports>>(
+ NS_ERROR_UNEXPECTED);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_UNEXPECTED);
+}
+
+TEST(QuotaCommon_IsSpecificError, Match)
+{ EXPECT_TRUE(IsSpecificError<NS_ERROR_FAILURE>(NS_ERROR_FAILURE)); }
+
+TEST(QuotaCommon_IsSpecificError, Mismatch)
+{ EXPECT_FALSE(IsSpecificError<NS_ERROR_FAILURE>(NS_ERROR_UNEXPECTED)); }
+
+TEST(QuotaCommon_ErrToOk, Bool_True)
+{
+ auto res = ErrToOk<true>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), true);
+}
+
+TEST(QuotaCommon_ErrToOk, Bool_False)
+{
+ auto res = ErrToOk<false>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), false);
+}
+
+TEST(QuotaCommon_ErrToOk, Int_42)
+{
+ auto res = ErrToOk<42>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(QuotaCommon_ErrToOk, NsCOMPtr_nullptr)
+{
+ auto res = ErrToOk<nullptr, nsCOMPtr<nsISupports>>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), nullptr);
+}
+
+TEST(QuotaCommon_ErrToDefaultOk, Ok)
+{
+ auto res = ErrToDefaultOk<Ok>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+}
+
+TEST(QuotaCommon_ErrToDefaultOk, NsCOMPtr)
+{
+ auto res = ErrToDefaultOk<nsCOMPtr<nsISupports>>(NS_ERROR_FAILURE);
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), nullptr);
+}
+
+class StringPairParameterized
+ : public ::testing::TestWithParam<std::pair<const char*, const char*>> {};
+
+TEST_P(StringPairParameterized, AnonymizedOriginString) {
+ const auto [in, expectedAnonymized] = GetParam();
+ const auto anonymized = AnonymizedOriginString(nsDependentCString(in));
+ EXPECT_STREQ(anonymized.get(), expectedAnonymized);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+ QuotaCommon, StringPairParameterized,
+ ::testing::Values(
+ // XXX Do we really want to anonymize about: origins?
+ std::pair("about:home", "about:aaaa"),
+ std::pair("https://foo.bar.com", "https://aaa.aaa.aaa"),
+ std::pair("https://foo.bar.com:8000", "https://aaa.aaa.aaa:DDDD"),
+ std::pair("file://UNIVERSAL_FILE_ORIGIN",
+ "file://aaaaaaaaa_aaaa_aaaaaa")));
+
+// BEGIN COPY FROM mfbt/tests/TestResult.cpp
+struct Failed {};
+
+static GenericErrorResult<Failed> Fail() { return Err(Failed()); }
+
+static Result<Ok, Failed> Task1(bool pass) {
+ if (!pass) {
+ return Fail(); // implicit conversion from GenericErrorResult to Result
+ }
+ return Ok();
+}
+// END COPY FROM mfbt/tests/TestResult.cpp
+
+static Result<bool, Failed> Condition(bool aNoError, bool aResult) {
+ return Task1(aNoError).map([aResult](auto) { return aResult; });
+}
+
+TEST(QuotaCommon_CollectWhileTest, NoFailures)
+{
+ const size_t loopCount = 5;
+ size_t conditionExecutions = 0;
+ size_t bodyExecutions = 0;
+ auto result = CollectWhile(
+ [&conditionExecutions] {
+ ++conditionExecutions;
+ return Condition(true, conditionExecutions <= loopCount);
+ },
+ [&bodyExecutions] {
+ ++bodyExecutions;
+ return Task1(true);
+ });
+ static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>);
+ MOZ_RELEASE_ASSERT(result.isOk());
+ MOZ_RELEASE_ASSERT(loopCount == bodyExecutions);
+ MOZ_RELEASE_ASSERT(1 + loopCount == conditionExecutions);
+}
+
+TEST(QuotaCommon_CollectWhileTest, BodyFailsImmediately)
+{
+ size_t conditionExecutions = 0;
+ size_t bodyExecutions = 0;
+ auto result = CollectWhile(
+ [&conditionExecutions] {
+ ++conditionExecutions;
+ return Condition(true, true);
+ },
+ [&bodyExecutions] {
+ ++bodyExecutions;
+ return Task1(false);
+ });
+ static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>);
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(1 == bodyExecutions);
+ MOZ_RELEASE_ASSERT(1 == conditionExecutions);
+}
+
+TEST(QuotaCommon_CollectWhileTest, BodyFailsOnSecondExecution)
+{
+ size_t conditionExecutions = 0;
+ size_t bodyExecutions = 0;
+ auto result = CollectWhile(
+ [&conditionExecutions] {
+ ++conditionExecutions;
+ return Condition(true, true);
+ },
+ [&bodyExecutions] {
+ ++bodyExecutions;
+ return Task1(bodyExecutions < 2);
+ });
+ static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>);
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(2 == bodyExecutions);
+ MOZ_RELEASE_ASSERT(2 == conditionExecutions);
+}
+
+TEST(QuotaCommon_CollectWhileTest, ConditionFailsImmediately)
+{
+ size_t conditionExecutions = 0;
+ size_t bodyExecutions = 0;
+ auto result = CollectWhile(
+ [&conditionExecutions] {
+ ++conditionExecutions;
+ return Condition(false, true);
+ },
+ [&bodyExecutions] {
+ ++bodyExecutions;
+ return Task1(true);
+ });
+ static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>);
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(0 == bodyExecutions);
+ MOZ_RELEASE_ASSERT(1 == conditionExecutions);
+}
+
+TEST(QuotaCommon_CollectWhileTest, ConditionFailsOnSecondExecution)
+{
+ size_t conditionExecutions = 0;
+ size_t bodyExecutions = 0;
+ auto result = CollectWhile(
+ [&conditionExecutions] {
+ ++conditionExecutions;
+ return Condition(conditionExecutions < 2, true);
+ },
+ [&bodyExecutions] {
+ ++bodyExecutions;
+ return Task1(true);
+ });
+ static_assert(std::is_same_v<decltype(result), Result<Ok, Failed>>);
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(1 == bodyExecutions);
+ MOZ_RELEASE_ASSERT(2 == conditionExecutions);
+}
+
+TEST(QuotaCommon_CollectEachInRange, Success)
+{
+ size_t bodyExecutions = 0;
+ const auto result = CollectEachInRange(
+ std::array<int, 5>{{1, 2, 3, 4, 5}},
+ [&bodyExecutions](const int val) -> Result<Ok, nsresult> {
+ ++bodyExecutions;
+ return Ok{};
+ });
+
+ MOZ_RELEASE_ASSERT(result.isOk());
+ MOZ_RELEASE_ASSERT(5 == bodyExecutions);
+}
+
+TEST(QuotaCommon_CollectEachInRange, FailureShortCircuit)
+{
+ size_t bodyExecutions = 0;
+ const auto result = CollectEachInRange(
+ std::array<int, 5>{{1, 2, 3, 4, 5}},
+ [&bodyExecutions](const int val) -> Result<Ok, nsresult> {
+ ++bodyExecutions;
+ return val == 3 ? Err(NS_ERROR_FAILURE) : Result<Ok, nsresult>{Ok{}};
+ });
+
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(NS_ERROR_FAILURE == result.inspectErr());
+ MOZ_RELEASE_ASSERT(3 == bodyExecutions);
+}
+
+TEST(QuotaCommon_ReduceEach, Success)
+{
+ const auto result = ReduceEach(
+ [i = int{0}]() mutable -> Result<int, Failed> {
+ if (i < 5) {
+ return ++i;
+ }
+ return 0;
+ },
+ 0, [](int val, int add) -> Result<int, Failed> { return val + add; });
+ static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>);
+
+ MOZ_RELEASE_ASSERT(result.isOk());
+ MOZ_RELEASE_ASSERT(15 == result.inspect());
+}
+
+TEST(QuotaCommon_ReduceEach, StepError)
+{
+ const auto result = ReduceEach(
+ [i = int{0}]() mutable -> Result<int, Failed> {
+ if (i < 5) {
+ return ++i;
+ }
+ return 0;
+ },
+ 0,
+ [](int val, int add) -> Result<int, Failed> {
+ if (val > 2) {
+ return Err(Failed{});
+ }
+ return val + add;
+ });
+ static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>);
+
+ MOZ_RELEASE_ASSERT(result.isErr());
+}
+
+TEST(QuotaCommon_ReduceEach, GeneratorError)
+{
+ size_t generatorExecutions = 0;
+ const auto result = ReduceEach(
+ [i = int{0}, &generatorExecutions]() mutable -> Result<int, Failed> {
+ ++generatorExecutions;
+ if (i < 1) {
+ return ++i;
+ }
+ return Err(Failed{});
+ },
+ 0,
+ [](int val, int add) -> Result<int, Failed> {
+ if (val > 2) {
+ return Err(Failed{});
+ }
+ return val + add;
+ });
+ static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>);
+
+ MOZ_RELEASE_ASSERT(result.isErr());
+ MOZ_RELEASE_ASSERT(2 == generatorExecutions);
+}
+
+TEST(QuotaCommon_Reduce, Success)
+{
+ const auto range = std::vector{0, 1, 2, 3, 4, 5};
+ const auto result = Reduce(
+ range, 0, [](int val, Maybe<const int&> add) -> Result<int, Failed> {
+ return val + add.ref();
+ });
+ static_assert(std::is_same_v<decltype(result), const Result<int, Failed>>);
+
+ MOZ_RELEASE_ASSERT(result.isOk());
+ MOZ_RELEASE_ASSERT(15 == result.inspect());
+}
+
+TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, NoFailures)
+{
+ uint32_t tries = 0;
+
+ auto res = CallWithDelayedRetriesIfAccessDenied(
+ [&tries]() -> Result<Ok, nsresult> {
+ ++tries;
+ return Ok{};
+ },
+ 10, 2);
+
+ EXPECT_EQ(tries, 1u);
+ EXPECT_TRUE(res.isOk());
+}
+
+TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, PermanentFailures)
+{
+ uint32_t tries = 0;
+
+ auto res = CallWithDelayedRetriesIfAccessDenied(
+ [&tries]() -> Result<Ok, nsresult> {
+ ++tries;
+ return Err(NS_ERROR_FILE_IS_LOCKED);
+ },
+ 10, 2);
+
+ EXPECT_EQ(tries, 11u);
+ EXPECT_TRUE(res.isErr());
+}
+
+TEST(QuotaCommon_CallWithDelayedRetriesIfAccessDenied, FailuresAndSuccess)
+{
+ uint32_t tries = 0;
+
+ auto res = CallWithDelayedRetriesIfAccessDenied(
+ [&tries]() -> Result<Ok, nsresult> {
+ if (++tries == 5) {
+ return Ok{};
+ }
+ return Err(NS_ERROR_FILE_ACCESS_DENIED);
+ },
+ 10, 2);
+
+ EXPECT_EQ(tries, 5u);
+ EXPECT_TRUE(res.isOk());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, ThisSourceFile)
+{
+ static constexpr auto thisSourceFileRelativePath =
+ "dom/quota/test/gtest/TestQuotaCommon.cpp"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ nsLiteralCString(__FILE__))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(), thisSourceFileRelativePath.get());
+}
+
+static nsCString MakeTreePath(const nsACString& aBasePath,
+ const nsACString& aRelativePath) {
+ nsCString path{aBasePath};
+
+ path.Append("/");
+ path.Append(aRelativePath);
+
+ return path;
+}
+
+static nsCString MakeSourceTreePath(const nsACString& aRelativePath) {
+ return MakeTreePath(mozilla::dom::quota::detail::GetSourceTreeBase(),
+ aRelativePath);
+}
+
+static nsCString MakeObjdirDistIncludeTreePath(
+ const nsACString& aRelativePath) {
+ return MakeTreePath(
+ mozilla::dom::quota::detail::GetObjdirDistIncludeTreeBase(),
+ aRelativePath);
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, DomQuotaSourceFile)
+{
+ static constexpr auto domQuotaSourceFileRelativePath =
+ "dom/quota/ActorsParent.cpp"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ MakeSourceTreePath(domQuotaSourceFileRelativePath))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(),
+ domQuotaSourceFileRelativePath.get());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, DomQuotaSourceFile_Exported)
+{
+ static constexpr auto mozillaDomQuotaSourceFileRelativePath =
+ "mozilla/dom/quota/QuotaCommon.h"_ns;
+
+ static constexpr auto domQuotaSourceFileRelativePath =
+ "dom/quota/QuotaCommon.h"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ MakeObjdirDistIncludeTreePath(
+ mozillaDomQuotaSourceFileRelativePath))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(),
+ domQuotaSourceFileRelativePath.get());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, DomIndexedDBSourceFile)
+{
+ static constexpr auto domIndexedDBSourceFileRelativePath =
+ "dom/indexedDB/ActorsParent.cpp"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ MakeSourceTreePath(domIndexedDBSourceFileRelativePath))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(),
+ domIndexedDBSourceFileRelativePath.get());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath,
+ DomLocalstorageSourceFile_Exported_Mapped)
+{
+ static constexpr auto mozillaDomSourceFileRelativePath =
+ "mozilla/dom/LocalStorageCommon.h"_ns;
+
+ static constexpr auto domLocalstorageSourceFileRelativePath =
+ "dom/localstorage/LocalStorageCommon.h"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ MakeObjdirDistIncludeTreePath(mozillaDomSourceFileRelativePath))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(),
+ domLocalstorageSourceFileRelativePath.get());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, NonDomSourceFile)
+{
+ static constexpr auto nonDomSourceFileRelativePath =
+ "storage/mozStorageService.cpp"_ns;
+
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ MakeSourceTreePath(nonDomSourceFileRelativePath))};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(),
+ nonDomSourceFileRelativePath.get());
+}
+
+TEST(QuotaCommon_MakeSourceFileRelativePath, OtherSourceFile)
+{
+ constexpr auto otherSourceFilePath = "/foo/bar/Test.cpp"_ns;
+ const nsCString sourceFileRelativePath{
+ mozilla::dom::quota::detail::MakeSourceFileRelativePath(
+ otherSourceFilePath)};
+
+ EXPECT_STREQ(sourceFileRelativePath.get(), "Test.cpp");
+}
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
diff --git a/dom/quota/test/gtest/TestQuotaManager.cpp b/dom/quota/test/gtest/TestQuotaManager.cpp
new file mode 100644
index 0000000000..7d185df481
--- /dev/null
+++ b/dom/quota/test/gtest/TestQuotaManager.cpp
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/OriginScope.h"
+
+#include "gtest/gtest.h"
+
+#include <cstdint>
+#include <memory>
+#include "ErrorList.h"
+#include "mozilla/Result.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/fallible.h"
+#include "nsCOMPtr.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "nsString.h"
+#include "nsStringFwd.h"
+#include "nsTLiteralString.h"
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+namespace {
+
+struct OriginTest {
+ const char* mOrigin;
+ bool mMatch;
+};
+
+void CheckOriginScopeMatchesOrigin(const OriginScope& aOriginScope,
+ const char* aOrigin, bool aMatch) {
+ bool result = aOriginScope.Matches(
+ OriginScope::FromOrigin(nsDependentCString(aOrigin)));
+
+ EXPECT_TRUE(result == aMatch);
+}
+
+void CheckUnknownFileEntry(nsIFile& aBase, const nsAString& aName,
+ const bool aWarnIfFile, const bool aWarnIfDir) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = aBase.Clone(getter_AddRefs(file));
+ ASSERT_EQ(rv, NS_OK);
+
+ rv = file->Append(aName);
+ ASSERT_EQ(rv, NS_OK);
+
+ rv = file->Create(nsIFile::NORMAL_FILE_TYPE, 0600);
+ ASSERT_EQ(rv, NS_OK);
+
+ auto okOrErr = WARN_IF_FILE_IS_UNKNOWN(*file);
+ ASSERT_TRUE(okOrErr.isOk());
+
+#ifdef DEBUG
+ EXPECT_TRUE(okOrErr.inspect() == aWarnIfFile);
+#else
+ EXPECT_TRUE(okOrErr.inspect() == false);
+#endif
+
+ rv = file->Remove(false);
+ ASSERT_EQ(rv, NS_OK);
+
+ rv = file->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ ASSERT_EQ(rv, NS_OK);
+
+ okOrErr = WARN_IF_FILE_IS_UNKNOWN(*file);
+ ASSERT_TRUE(okOrErr.isOk());
+
+#ifdef DEBUG
+ EXPECT_TRUE(okOrErr.inspect() == aWarnIfDir);
+#else
+ EXPECT_TRUE(okOrErr.inspect() == false);
+#endif
+
+ rv = file->Remove(false);
+ ASSERT_EQ(rv, NS_OK);
+}
+
+} // namespace
+
+TEST(QuotaManager, OriginScope)
+{
+ OriginScope originScope;
+
+ // Sanity checks.
+
+ {
+ constexpr auto origin = "http://www.mozilla.org"_ns;
+ originScope.SetFromOrigin(origin);
+ EXPECT_TRUE(originScope.IsOrigin());
+ EXPECT_TRUE(originScope.GetOrigin().Equals(origin));
+ EXPECT_TRUE(originScope.GetOriginNoSuffix().Equals(origin));
+ }
+
+ {
+ constexpr auto prefix = "http://www.mozilla.org"_ns;
+ originScope.SetFromPrefix(prefix);
+ EXPECT_TRUE(originScope.IsPrefix());
+ EXPECT_TRUE(originScope.GetOriginNoSuffix().Equals(prefix));
+ }
+
+ {
+ originScope.SetFromNull();
+ EXPECT_TRUE(originScope.IsNull());
+ }
+
+ // Test each origin scope type against particular origins.
+
+ {
+ originScope.SetFromOrigin("http://www.mozilla.org"_ns);
+
+ static const OriginTest tests[] = {
+ {"http://www.mozilla.org", true},
+ {"http://www.example.org", false},
+ };
+
+ for (const auto& test : tests) {
+ CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch);
+ }
+ }
+
+ {
+ originScope.SetFromPrefix("http://www.mozilla.org"_ns);
+
+ static const OriginTest tests[] = {
+ {"http://www.mozilla.org", true},
+ {"http://www.mozilla.org^userContextId=1", true},
+ {"http://www.example.org^userContextId=1", false},
+ };
+
+ for (const auto& test : tests) {
+ CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch);
+ }
+ }
+
+ {
+ originScope.SetFromNull();
+
+ static const OriginTest tests[] = {
+ {"http://www.mozilla.org", true},
+ {"http://www.mozilla.org^userContextId=1", true},
+ {"http://www.example.org^userContextId=1", true},
+ };
+
+ for (const auto& test : tests) {
+ CheckOriginScopeMatchesOrigin(originScope, test.mOrigin, test.mMatch);
+ }
+ }
+}
+
+TEST(QuotaManager, WarnIfUnknownFile)
+{
+ nsCOMPtr<nsIFile> base;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(base));
+ ASSERT_EQ(rv, NS_OK);
+
+ rv = base->Append(u"mozquotatests"_ns);
+ ASSERT_EQ(rv, NS_OK);
+
+ base->Remove(true);
+
+ rv = base->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ ASSERT_EQ(rv, NS_OK);
+
+ CheckUnknownFileEntry(*base, u"foo.bar"_ns, true, true);
+ CheckUnknownFileEntry(*base, u".DS_Store"_ns, false, true);
+ CheckUnknownFileEntry(*base, u".desktop"_ns, false, true);
+ CheckUnknownFileEntry(*base, u"desktop.ini"_ns, false, true);
+ CheckUnknownFileEntry(*base, u"DESKTOP.INI"_ns, false, true);
+ CheckUnknownFileEntry(*base, u"thumbs.db"_ns, false, true);
+ CheckUnknownFileEntry(*base, u"THUMBS.DB"_ns, false, true);
+ CheckUnknownFileEntry(*base, u".xyz"_ns, false, true);
+
+ rv = base->Remove(true);
+ ASSERT_EQ(rv, NS_OK);
+}
diff --git a/dom/quota/test/gtest/TestResultExtensions.cpp b/dom/quota/test/gtest/TestResultExtensions.cpp
new file mode 100644
index 0000000000..137acc1423
--- /dev/null
+++ b/dom/quota/test/gtest/TestResultExtensions.cpp
@@ -0,0 +1,333 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Common.h"
+#include "gtest/gtest.h"
+#include "mozilla/dom/QMResult.h"
+#include "mozilla/dom/quota/ResultExtensions.h"
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+namespace {
+class TestClass {
+ public:
+ static constexpr int kTestValue = 42;
+
+ nsresult NonOverloadedNoInputComplex(std::pair<int, int>* aOut) {
+ *aOut = std::pair{kTestValue, kTestValue};
+ return NS_OK;
+ }
+ nsresult NonOverloadedNoInputFailsComplex(std::pair<int, int>* aOut) {
+ return NS_ERROR_FAILURE;
+ }
+};
+} // namespace
+
+class DOM_Quota_ResultExtensions_ToResult : public DOM_Quota_Test {};
+class DOM_Quota_ResultExtensions_GenericErrorResult : public DOM_Quota_Test {};
+
+TEST_F(DOM_Quota_ResultExtensions_ToResult, FromBool) {
+ // success
+ {
+ auto res = ToResult(true);
+ static_assert(std::is_same_v<decltype(res), Result<Ok, nsresult>>);
+ EXPECT_TRUE(res.isOk());
+ }
+
+ // failure
+ {
+ auto res = ToResult(false);
+ static_assert(std::is_same_v<decltype(res), Result<Ok, nsresult>>);
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+ }
+}
+
+TEST_F(DOM_Quota_ResultExtensions_ToResult, FromQMResult_Failure) {
+ // copy
+ {
+ const auto res = ToQMResult(NS_ERROR_FAILURE);
+ auto okOrErr = ToResult<QMResult>(res);
+ static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>);
+ ASSERT_TRUE(okOrErr.isErr());
+ auto err = okOrErr.unwrapErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(err.StackId(), ExpectedStackId());
+ ASSERT_EQ(err.FrameId(), 1u);
+ ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err, NS_ERROR_FAILURE);
+#endif
+ }
+
+ // move
+ {
+ auto res = ToQMResult(NS_ERROR_FAILURE);
+ auto okOrErr = ToResult<QMResult>(std::move(res));
+ static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>);
+ ASSERT_TRUE(okOrErr.isErr());
+ auto err = okOrErr.unwrapErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(err.StackId(), ExpectedStackId());
+ ASSERT_EQ(err.FrameId(), 1u);
+ ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err, NS_ERROR_FAILURE);
+#endif
+ }
+}
+
+TEST_F(DOM_Quota_ResultExtensions_ToResult, FromNSResult_Failure_Macro) {
+ auto okOrErr = QM_TO_RESULT(NS_ERROR_FAILURE);
+ static_assert(std::is_same_v<decltype(okOrErr), OkOrErr>);
+ ASSERT_TRUE(okOrErr.isErr());
+ auto err = okOrErr.unwrapErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(err.StackId(), ExpectedStackId());
+ ASSERT_EQ(err.FrameId(), 1u);
+ ASSERT_EQ(err.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err, NS_ERROR_FAILURE);
+#endif
+}
+
+TEST_F(DOM_Quota_ResultExtensions_GenericErrorResult, ErrorPropagation) {
+ OkOrErr okOrErr1 = ToResult<QMResult>(ToQMResult(NS_ERROR_FAILURE));
+ const auto& err1 = okOrErr1.inspectErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ IncreaseExpectedStackId();
+
+ ASSERT_EQ(err1.StackId(), ExpectedStackId());
+ ASSERT_EQ(err1.FrameId(), 1u);
+ ASSERT_EQ(err1.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err1, NS_ERROR_FAILURE);
+#endif
+
+ OkOrErr okOrErr2 = okOrErr1.propagateErr();
+ const auto& err2 = okOrErr2.inspectErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ ASSERT_EQ(err2.StackId(), ExpectedStackId());
+ ASSERT_EQ(err2.FrameId(), 2u);
+ ASSERT_EQ(err2.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err2, NS_ERROR_FAILURE);
+#endif
+
+ OkOrErr okOrErr3 = okOrErr2.propagateErr();
+ const auto& err3 = okOrErr3.inspectErr();
+
+#ifdef QM_ERROR_STACKS_ENABLED
+ ASSERT_EQ(err3.StackId(), ExpectedStackId());
+ ASSERT_EQ(err3.FrameId(), 3u);
+ ASSERT_EQ(err3.NSResult(), NS_ERROR_FAILURE);
+#else
+ ASSERT_EQ(err3, NS_ERROR_FAILURE);
+#endif
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput)
+{
+ auto res = ToResultGet<int32_t>([](nsresult* aRv) -> int32_t {
+ *aRv = NS_OK;
+ return 42;
+ });
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Err)
+{
+ auto res = ToResultGet<int32_t>([](nsresult* aRv) -> int32_t {
+ *aRv = NS_ERROR_FAILURE;
+ return -1;
+ });
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput)
+{
+ auto res = ToResultGet<int32_t>(
+ [](int32_t aValue, nsresult* aRv) -> int32_t {
+ *aRv = NS_OK;
+ return aValue * 2;
+ },
+ 42);
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 84);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Err)
+{
+ auto res = ToResultGet<int32_t>(
+ [](int32_t aValue, nsresult* aRv) -> int32_t {
+ *aRv = NS_ERROR_FAILURE;
+ return -1;
+ },
+ 42);
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Macro_Typed)
+{
+ auto res = MOZ_TO_RESULT_GET_TYPED(int32_t, [](nsresult* aRv) -> int32_t {
+ *aRv = NS_OK;
+ return 42;
+ });
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 42);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Macro_Typed_Parens)
+{
+ auto res =
+ MOZ_TO_RESULT_GET_TYPED((std::pair<int32_t, int32_t>),
+ [](nsresult* aRv) -> std::pair<int32_t, int32_t> {
+ *aRv = NS_OK;
+ return std::pair{42, 42};
+ });
+
+ static_assert(std::is_same_v<decltype(res),
+ Result<std::pair<int32_t, int32_t>, nsresult>>);
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), (std::pair{42, 42}));
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_NoInput_Err_Macro_Typed)
+{
+ auto res = MOZ_TO_RESULT_GET_TYPED(int32_t, [](nsresult* aRv) -> int32_t {
+ *aRv = NS_ERROR_FAILURE;
+ return -1;
+ });
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Macro_Typed)
+{
+ auto res = MOZ_TO_RESULT_GET_TYPED(
+ int32_t,
+ [](int32_t aValue, nsresult* aRv) -> int32_t {
+ *aRv = NS_OK;
+ return aValue * 2;
+ },
+ 42);
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isOk());
+ EXPECT_EQ(res.unwrap(), 84);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultGet, Lambda_WithInput_Err_Macro_Typed)
+{
+ auto res = MOZ_TO_RESULT_GET_TYPED(
+ int32_t,
+ [](int32_t aValue, nsresult* aRv) -> int32_t {
+ *aRv = NS_ERROR_FAILURE;
+ return -1;
+ },
+ 42);
+
+ static_assert(std::is_same_v<decltype(res), Result<int32_t, nsresult>>);
+
+ EXPECT_TRUE(res.isErr());
+ EXPECT_EQ(res.unwrapErr(), NS_ERROR_FAILURE);
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultInvoke, Lambda_NoInput_Complex)
+{
+ TestClass foo;
+
+ // success
+ {
+ auto valOrErr =
+ ToResultInvoke<std::pair<int, int>>([&foo](std::pair<int, int>* out) {
+ return foo.NonOverloadedNoInputComplex(out);
+ });
+ static_assert(std::is_same_v<decltype(valOrErr),
+ Result<std::pair<int, int>, nsresult>>);
+ ASSERT_TRUE(valOrErr.isOk());
+ ASSERT_EQ((std::pair{TestClass::kTestValue, TestClass::kTestValue}),
+ valOrErr.unwrap());
+ }
+
+ // failure
+ {
+ auto valOrErr =
+ ToResultInvoke<std::pair<int, int>>([&foo](std::pair<int, int>* out) {
+ return foo.NonOverloadedNoInputFailsComplex(out);
+ });
+ static_assert(std::is_same_v<decltype(valOrErr),
+ Result<std::pair<int, int>, nsresult>>);
+ ASSERT_TRUE(valOrErr.isErr());
+ ASSERT_EQ(NS_ERROR_FAILURE, valOrErr.unwrapErr());
+ }
+}
+
+TEST(DOM_Quota_ResultExtensions_ToResultInvoke,
+ Lambda_NoInput_Complex_Macro_Typed)
+{
+ TestClass foo;
+
+ // success
+ {
+ auto valOrErr = MOZ_TO_RESULT_INVOKE_TYPED(
+ (std::pair<int, int>), [&foo](std::pair<int, int>* out) {
+ return foo.NonOverloadedNoInputComplex(out);
+ });
+ static_assert(std::is_same_v<decltype(valOrErr),
+ Result<std::pair<int, int>, nsresult>>);
+ ASSERT_TRUE(valOrErr.isOk());
+ ASSERT_EQ((std::pair{TestClass::kTestValue, TestClass::kTestValue}),
+ valOrErr.unwrap());
+ }
+
+ // failure
+ {
+ auto valOrErr = MOZ_TO_RESULT_INVOKE_TYPED(
+ (std::pair<int, int>), [&foo](std::pair<int, int>* out) {
+ return foo.NonOverloadedNoInputFailsComplex(out);
+ });
+ static_assert(std::is_same_v<decltype(valOrErr),
+ Result<std::pair<int, int>, nsresult>>);
+ ASSERT_TRUE(valOrErr.isErr());
+ ASSERT_EQ(NS_ERROR_FAILURE, valOrErr.unwrapErr());
+ }
+}
diff --git a/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp b/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp
new file mode 100644
index 0000000000..00a3393844
--- /dev/null
+++ b/dom/quota/test/gtest/TestScopedLogExtraInfo.cpp
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/ScopedLogExtraInfo.h"
+
+#include "gtest/gtest.h"
+
+using namespace mozilla::dom::quota;
+
+TEST(DOM_Quota_ScopedLogExtraInfo, AddAndRemove)
+{
+ static constexpr auto text = "foo"_ns;
+
+ {
+ const auto extraInfo =
+ ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, text};
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+
+ EXPECT_EQ(text, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery));
+#endif
+ }
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+
+ EXPECT_EQ(0u, extraInfoMap.count(ScopedLogExtraInfo::kTagQuery));
+#endif
+}
+
+TEST(DOM_Quota_ScopedLogExtraInfo, Nested)
+{
+ static constexpr auto text = "foo"_ns;
+ static constexpr auto nestedText = "bar"_ns;
+
+ {
+ const auto extraInfo =
+ ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, text};
+
+ {
+ const auto extraInfo =
+ ScopedLogExtraInfo{ScopedLogExtraInfo::kTagQuery, nestedText};
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+ EXPECT_EQ(nestedText, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery));
+#endif
+ }
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+ EXPECT_EQ(text, *extraInfoMap.at(ScopedLogExtraInfo::kTagQuery));
+#endif
+ }
+
+#ifdef QM_SCOPED_LOG_EXTRA_INFO_ENABLED
+ const auto& extraInfoMap = ScopedLogExtraInfo::GetExtraInfoMap();
+
+ EXPECT_EQ(0u, extraInfoMap.count(ScopedLogExtraInfo::kTagQuery));
+#endif
+}
diff --git a/dom/quota/test/gtest/TestUsageInfo.cpp b/dom/quota/test/gtest/TestUsageInfo.cpp
new file mode 100644
index 0000000000..124783b715
--- /dev/null
+++ b/dom/quota/test/gtest/TestUsageInfo.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/quota/UsageInfo.h"
+
+#include "gtest/gtest.h"
+
+#include <cstdint>
+#include <memory>
+#include <ostream>
+#include <utility>
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/quota/QuotaCommon.h"
+#include "mozilla/fallible.h"
+
+using namespace mozilla;
+using namespace mozilla::dom::quota;
+
+namespace {
+constexpr uint64_t kTestValue = 42;
+constexpr uint64_t kTestValueZero = 0;
+} // namespace
+
+TEST(DOM_Quota_UsageInfo, DefaultConstructed)
+{
+ const UsageInfo usageInfo;
+ EXPECT_EQ(Nothing(), usageInfo.FileUsage());
+ EXPECT_EQ(Nothing(), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, FileOnly)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(kTestValue), usageInfo.FileUsage());
+ EXPECT_EQ(Some(kTestValue), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, DatabaseOnly)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += DatabaseUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Nothing(), usageInfo.FileUsage());
+ EXPECT_EQ(Some(kTestValue), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, FileOnly_Zero)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(kTestValueZero));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(kTestValueZero), usageInfo.FileUsage());
+ EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, DatabaseOnly_Zero)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += DatabaseUsageType(Some(kTestValueZero));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Nothing(), usageInfo.FileUsage());
+ EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, Both)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(kTestValue));
+ usageInfo += DatabaseUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(kTestValue), usageInfo.FileUsage());
+ EXPECT_EQ(Some(2 * kTestValue), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, Both_Zero)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(kTestValueZero));
+ usageInfo += DatabaseUsageType(Some(kTestValueZero));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(kTestValueZero), usageInfo.FileUsage());
+ EXPECT_EQ(Some(kTestValueZero), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, CapCombined)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(UINT64_MAX));
+ usageInfo += DatabaseUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(UINT64_MAX), usageInfo.FileUsage());
+ EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, CapFileUsage)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += FileUsageType(Some(UINT64_MAX));
+ usageInfo += FileUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Some(UINT64_MAX), usageInfo.FileUsage());
+ EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage());
+}
+
+TEST(DOM_Quota_UsageInfo, CapDatabaseUsage)
+{
+ const UsageInfo usageInfo = [] {
+ UsageInfo usageInfo;
+ usageInfo += DatabaseUsageType(Some(UINT64_MAX));
+ usageInfo += DatabaseUsageType(Some(kTestValue));
+ return usageInfo;
+ }();
+ EXPECT_EQ(Nothing(), usageInfo.FileUsage());
+ EXPECT_EQ(Some(UINT64_MAX), usageInfo.TotalUsage());
+}
diff --git a/dom/quota/test/gtest/moz.build b/dom/quota/test/gtest/moz.build
new file mode 100644
index 0000000000..30ca99b254
--- /dev/null
+++ b/dom/quota/test/gtest/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom.quota += [
+ "QuotaTestChild.h",
+ "QuotaTestParent.h",
+]
+
+EXPORTS.mozilla.dom.quota.test += [
+ "QuotaManagerDependencyFixture.h",
+]
+
+UNIFIED_SOURCES = [
+ "Common.cpp",
+ "QuotaManagerDependencyFixture.cpp",
+ "TestCheckedUnsafePtr.cpp",
+ "TestClientUsageArray.cpp",
+ "TestEncryptedStream.cpp",
+ "TestFileOutputStream.cpp",
+ "TestFlatten.cpp",
+ "TestForwardDecls.cpp",
+ "TestPersistenceType.cpp",
+ "TestQMResult.cpp",
+ "TestQuotaCommon.cpp",
+ "TestQuotaManager.cpp",
+ "TestResultExtensions.cpp",
+ "TestScopedLogExtraInfo.cpp",
+ "TestUsageInfo.cpp",
+]
+
+IPDL_SOURCES += [
+ "PQuotaTest.ipdl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
+
+LOCAL_INCLUDES += [
+ "/dom/quota",
+]
diff --git a/dom/quota/test/mochitest/helpers.js b/dom/quota/test/mochitest/helpers.js
new file mode 100644
index 0000000000..4be2e131c2
--- /dev/null
+++ b/dom/quota/test/mochitest/helpers.js
@@ -0,0 +1,312 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../";
+
+var testGenerator;
+var testHarnessGenerator;
+var workerScriptPaths = [];
+
+loadScript("dom/quota/test/common/mochitest.js");
+
+function loadScript(path) {
+ const url = new URL(depth + path, window.location.href);
+ SpecialPowers.Services.scriptloader.loadSubScript(url.href, this);
+}
+
+function loadWorkerScript(path) {
+ const url = new URL(depth + path, window.location.href);
+ workerScriptPaths.push(url.href);
+}
+
+function* testHarnessSteps() {
+ function nextTestHarnessStep(val) {
+ testHarnessGenerator.next(val);
+ }
+
+ info("Enabling storage testing");
+
+ enableStorageTesting().then(nextTestHarnessStep);
+ yield undefined;
+
+ info("Pushing preferences");
+
+ SpecialPowers.pushPrefEnv(
+ {
+ set: [["dom.storageManager.prompt.testing", true]],
+ },
+ nextTestHarnessStep
+ );
+ yield undefined;
+
+ info("Clearing old databases");
+
+ clearAllDatabases(nextTestHarnessStep);
+ yield undefined;
+
+ info("Running test in given scopes");
+
+ if (workerScriptPaths.length) {
+ info("Running test in a worker");
+
+ let workerScriptBlob = new Blob(["(" + workerScript.toString() + ")();"], {
+ type: "text/javascript",
+ });
+ let workerScriptURL = URL.createObjectURL(workerScriptBlob);
+
+ let worker = new Worker(workerScriptURL);
+
+ worker.onerror = function(event) {
+ ok(false, "Worker had an error: " + event.message);
+ worker.terminate();
+ nextTestHarnessStep();
+ };
+
+ worker.onmessage = function(event) {
+ let message = event.data;
+ switch (message.op) {
+ case "ok":
+ ok(message.condition, message.name + " - " + message.diag);
+ break;
+
+ case "todo":
+ todo(message.condition, message.name, message.diag);
+ break;
+
+ case "info":
+ info(message.msg);
+ break;
+
+ case "ready":
+ worker.postMessage({ op: "load", files: workerScriptPaths });
+ break;
+
+ case "loaded":
+ worker.postMessage({ op: "start" });
+ break;
+
+ case "done":
+ ok(true, "Worker finished");
+ nextTestHarnessStep();
+ break;
+
+ case "clearAllDatabases":
+ clearAllDatabases(function() {
+ worker.postMessage({ op: "clearAllDatabasesDone" });
+ });
+ break;
+
+ default:
+ ok(
+ false,
+ "Received a bad message from worker: " + JSON.stringify(message)
+ );
+ nextTestHarnessStep();
+ }
+ };
+
+ URL.revokeObjectURL(workerScriptURL);
+
+ yield undefined;
+
+ worker.terminate();
+ worker = null;
+
+ clearAllDatabases(nextTestHarnessStep);
+ yield undefined;
+ }
+
+ info("Running test in main thread");
+
+ // Now run the test script in the main thread.
+ if (testSteps.constructor.name === "AsyncFunction") {
+ SimpleTest.registerCleanupFunction(async function() {
+ await requestFinished(clearAllDatabases());
+ });
+
+ add_task(testSteps);
+ } else {
+ testGenerator = testSteps();
+ testGenerator.next();
+
+ yield undefined;
+ }
+}
+
+if (!window.runTest) {
+ window.runTest = function() {
+ SimpleTest.waitForExplicitFinish();
+ testHarnessGenerator = testHarnessSteps();
+ testHarnessGenerator.next();
+ };
+}
+
+function finishTest() {
+ SimpleTest.executeSoon(function() {
+ clearAllDatabases(function() {
+ SimpleTest.finish();
+ });
+ });
+}
+
+function grabArgAndContinueHandler(arg) {
+ testGenerator.next(arg);
+}
+
+function continueToNextStep() {
+ SimpleTest.executeSoon(function() {
+ testGenerator.next();
+ });
+}
+
+function continueToNextStepSync() {
+ testGenerator.next();
+}
+
+function workerScript() {
+ "use strict";
+
+ self.testGenerator = null;
+
+ self.repr = function(_thing_) {
+ if (typeof _thing_ == "undefined") {
+ return "undefined";
+ }
+
+ let str;
+
+ try {
+ str = _thing_ + "";
+ } catch (e) {
+ return "[" + typeof _thing_ + "]";
+ }
+
+ if (typeof _thing_ == "function") {
+ str = str.replace(/^\s+/, "");
+ let idx = str.indexOf("{");
+ if (idx != -1) {
+ str = str.substr(0, idx) + "{...}";
+ }
+ }
+
+ return str;
+ };
+
+ self.ok = function(_condition_, _name_, _diag_) {
+ self.postMessage({
+ op: "ok",
+ condition: !!_condition_,
+ name: _name_,
+ diag: _diag_,
+ });
+ };
+
+ self.is = function(_a_, _b_, _name_) {
+ let pass = _a_ == _b_;
+ let diag = pass ? "" : "got " + repr(_a_) + ", expected " + repr(_b_);
+ ok(pass, _name_, diag);
+ };
+
+ self.isnot = function(_a_, _b_, _name_) {
+ let pass = _a_ != _b_;
+ let diag = pass ? "" : "didn't expect " + repr(_a_) + ", but got it";
+ ok(pass, _name_, diag);
+ };
+
+ self.todo = function(_condition_, _name_, _diag_) {
+ self.postMessage({
+ op: "todo",
+ condition: !!_condition_,
+ name: _name_,
+ diag: _diag_,
+ });
+ };
+
+ self.info = function(_msg_) {
+ self.postMessage({ op: "info", msg: _msg_ });
+ };
+
+ self.executeSoon = function(_fun_) {
+ var channel = new MessageChannel();
+ channel.port1.postMessage("");
+ channel.port2.onmessage = function(event) {
+ _fun_();
+ };
+ };
+
+ self.finishTest = function() {
+ self.postMessage({ op: "done" });
+ };
+
+ self.grabArgAndContinueHandler = function(_arg_) {
+ testGenerator.next(_arg_);
+ };
+
+ self.continueToNextStep = function() {
+ executeSoon(function() {
+ testGenerator.next();
+ });
+ };
+
+ self.continueToNextStepSync = function() {
+ testGenerator.next();
+ };
+
+ self._clearAllDatabasesCallback = undefined;
+ self.clearAllDatabases = function(_callback_) {
+ self._clearAllDatabasesCallback = _callback_;
+ self.postMessage({ op: "clearAllDatabases" });
+ };
+
+ self.onerror = function(_message_, _file_, _line_) {
+ ok(
+ false,
+ "Worker: uncaught exception [" +
+ _file_ +
+ ":" +
+ _line_ +
+ "]: '" +
+ _message_ +
+ "'"
+ );
+ self.finishTest();
+ self.close();
+ return true;
+ };
+
+ self.onmessage = function(_event_) {
+ let message = _event_.data;
+ switch (message.op) {
+ case "load":
+ info("Worker: loading " + JSON.stringify(message.files));
+ self.importScripts(message.files);
+ self.postMessage({ op: "loaded" });
+ break;
+
+ case "start":
+ executeSoon(function() {
+ info("Worker: starting tests");
+ testGenerator = testSteps();
+ testGenerator.next();
+ });
+ break;
+
+ case "clearAllDatabasesDone":
+ info("Worker: all databases are cleared");
+ if (self._clearAllDatabasesCallback) {
+ self._clearAllDatabasesCallback();
+ }
+ break;
+
+ default:
+ throw new Error(
+ "Received a bad message from parent: " + JSON.stringify(message)
+ );
+ }
+ };
+
+ self.postMessage({ op: "ready" });
+}
diff --git a/dom/quota/test/mochitest/mochitest.ini b/dom/quota/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..09cd6e66c8
--- /dev/null
+++ b/dom/quota/test/mochitest/mochitest.ini
@@ -0,0 +1,16 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+support-files =
+ helpers.js
+
+[test_simpledb.html]
+[test_storage_manager_persist_allow.html]
+fail-if = xorigin
+scheme=https
+[test_storage_manager_persist_deny.html]
+scheme=https
+[test_storage_manager_persisted.html]
+scheme=https
diff --git a/dom/quota/test/mochitest/test_simpledb.html b/dom/quota/test/mochitest/test_simpledb.html
new file mode 100644
index 0000000000..29ca8be65e
--- /dev/null
+++ b/dom/quota/test/mochitest/test_simpledb.html
@@ -0,0 +1,21 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>SimpleDB Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="helpers.js"></script>
+ <script type="text/javascript">
+ loadScript("dom/quota/test/common/test_simpledb.js");
+ </script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
diff --git a/dom/quota/test/mochitest/test_storage_manager_persist_allow.html b/dom/quota/test/mochitest/test_storage_manager_persist_allow.html
new file mode 100644
index 0000000000..8477630f21
--- /dev/null
+++ b/dom/quota/test/mochitest/test_storage_manager_persist_allow.html
@@ -0,0 +1,21 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Allow Persist Prompt for StorageManager</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="helpers.js"></script>
+ <script type="text/javascript">
+ loadScript("dom/quota/test/common/test_storage_manager_persist_allow.js");
+ </script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
diff --git a/dom/quota/test/mochitest/test_storage_manager_persist_deny.html b/dom/quota/test/mochitest/test_storage_manager_persist_deny.html
new file mode 100644
index 0000000000..2b0fab4423
--- /dev/null
+++ b/dom/quota/test/mochitest/test_storage_manager_persist_deny.html
@@ -0,0 +1,21 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Deny Persist Prompt for StorageManager</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="helpers.js"></script>
+ <script type="text/javascript">
+ loadScript("dom/quota/test/common/test_storage_manager_persist_deny.js");
+ </script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
diff --git a/dom/quota/test/mochitest/test_storage_manager_persisted.html b/dom/quota/test/mochitest/test_storage_manager_persisted.html
new file mode 100644
index 0000000000..6e03f33e2d
--- /dev/null
+++ b/dom/quota/test/mochitest/test_storage_manager_persisted.html
@@ -0,0 +1,24 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<html>
+<head>
+ <title>Storage Manager Persisted Test</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+
+ <script type="text/javascript" src="helpers.js"></script>
+ <script type="text/javascript">
+ const path = "dom/quota/test/common/test_storage_manager_persisted.js";
+
+ loadScript(path);
+ loadWorkerScript(path);
+ </script>
+
+</head>
+
+<body onload="runTest();"></body>
+
+</html>
diff --git a/dom/quota/test/modules/content/.eslintrc.js b/dom/quota/test/modules/content/.eslintrc.js
new file mode 100644
index 0000000000..03499166b5
--- /dev/null
+++ b/dom/quota/test/modules/content/.eslintrc.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ overrides: [
+ {
+ files: [
+ "Assert.js",
+ "ModuleLoader.js",
+ "StorageUtils.js",
+ "WorkerDriver.js",
+ ],
+ parserOptions: {
+ sourceType: "module",
+ },
+ },
+ ],
+};
diff --git a/dom/quota/test/modules/content/Assert.js b/dom/quota/test/modules/content/Assert.js
new file mode 100644
index 0000000000..e2c8df19c8
--- /dev/null
+++ b/dom/quota/test/modules/content/Assert.js
@@ -0,0 +1,10 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Just a wrapper around SimpleTest related functions for now.
+export const Assert = {
+ ok,
+ equal: is,
+};
diff --git a/dom/quota/test/modules/content/ModuleLoader.js b/dom/quota/test/modules/content/ModuleLoader.js
new file mode 100644
index 0000000000..a47061f751
--- /dev/null
+++ b/dom/quota/test/modules/content/ModuleLoader.js
@@ -0,0 +1,61 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+export function ModuleLoader(base, depth, proto) {
+ const modules = {};
+
+ const principal = SpecialPowers.wrap(document).nodePrincipal;
+
+ const sharedGlobalSandbox = SpecialPowers.Cu.Sandbox(principal, {
+ invisibleToDebugger: true,
+ sandboxName: "FS Module Loader",
+ sandboxPrototype: proto,
+ wantComponents: false,
+ wantGlobalProperties: [],
+ wantXrays: false,
+ });
+
+ const require = async function(id) {
+ if (modules[id]) {
+ return modules[id].exported_symbols;
+ }
+
+ const url = new URL(depth + id, base);
+
+ const module = Object.create(null, {
+ exported_symbols: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+
+ modules[id] = module;
+
+ const properties = {
+ require_module: require,
+ exported_symbols: module.exported_symbols,
+ };
+
+ // Create a new object in this sandbox, that will be used as the scope
+ // object for this particular module.
+ const sandbox = sharedGlobalSandbox.Object();
+ Object.assign(sandbox, properties);
+
+ SpecialPowers.Services.scriptloader.loadSubScript(url.href, sandbox);
+
+ return module.exported_symbols;
+ };
+
+ const returnObj = {
+ require: {
+ enumerable: true,
+ value: require,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
diff --git a/dom/quota/test/modules/content/StorageUtils.js b/dom/quota/test/modules/content/StorageUtils.js
new file mode 100644
index 0000000000..b29f7d9c26
--- /dev/null
+++ b/dom/quota/test/modules/content/StorageUtils.js
@@ -0,0 +1,48 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+class RequestError extends Error {
+ constructor(resultCode, resultName) {
+ super(`Request failed (code: ${resultCode}, name: ${resultName})`);
+ this.name = "RequestError";
+ this.resultCode = resultCode;
+ this.resultName = resultName;
+ }
+}
+
+export async function setStoragePrefs(optionalPrefsToSet) {
+ const prefsToSet = [
+ // Not needed right now, but might be needed in future.
+ // ["dom.quotaManager.testing", true],
+ ];
+
+ if (SpecialPowers.Services.appinfo.OS === "WINNT") {
+ prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]);
+ }
+
+ if (optionalPrefsToSet) {
+ prefsToSet.push(...optionalPrefsToSet);
+ }
+
+ await SpecialPowers.pushPrefEnv({ set: prefsToSet });
+}
+
+export async function clearStoragesForOrigin(principal) {
+ const request = SpecialPowers.Services.qms.clearStoragesForPrincipal(
+ principal
+ );
+
+ await new Promise(function(resolve) {
+ request.callback = SpecialPowers.wrapCallback(function() {
+ resolve();
+ });
+ });
+
+ if (request.resultCode != SpecialPowers.Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
diff --git a/dom/quota/test/modules/content/WorkerDriver.js b/dom/quota/test/modules/content/WorkerDriver.js
new file mode 100644
index 0000000000..365d00f363
--- /dev/null
+++ b/dom/quota/test/modules/content/WorkerDriver.js
@@ -0,0 +1,53 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+export async function runTestInWorker(script, base, listener) {
+ return new Promise(function(resolve) {
+ const globalHeadUrl = new URL(
+ "/tests/dom/quota/test/modules/worker/head.js",
+ base
+ );
+
+ const worker = new Worker(globalHeadUrl.href);
+
+ worker.onmessage = function(event) {
+ const data = event.data;
+
+ switch (data.op) {
+ case "ok":
+ listener.onOk(data.value, data.message);
+ break;
+
+ case "is":
+ listener.onIs(data.a, data.b, data.message);
+ break;
+
+ case "info":
+ listener.onInfo(data.message);
+ break;
+
+ case "finish":
+ resolve();
+ break;
+
+ case "failure":
+ listener.onOk(false, "Worker had a failure: " + data.message);
+ resolve();
+ break;
+ }
+ };
+
+ worker.onerror = function(event) {
+ listener.onOk(false, "Worker had an error: " + event.data);
+ resolve();
+ };
+
+ const scriptUrl = new URL(script, base);
+
+ const localHeadUrl = new URL("head.js", scriptUrl);
+
+ worker.postMessage([localHeadUrl.href, scriptUrl.href]);
+ });
+}
diff --git a/dom/quota/test/modules/content/worker/.eslintrc.js b/dom/quota/test/modules/content/worker/.eslintrc.js
new file mode 100644
index 0000000000..470c23b2fa
--- /dev/null
+++ b/dom/quota/test/modules/content/worker/.eslintrc.js
@@ -0,0 +1,21 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+
+ overrides: [
+ {
+ files: ["Assert.js", "ModuleLoader.js"],
+ parserOptions: {
+ sourceType: "script",
+ },
+ },
+ ],
+};
diff --git a/dom/quota/test/modules/content/worker/Assert.js b/dom/quota/test/modules/content/worker/Assert.js
new file mode 100644
index 0000000000..7c7e2683ea
--- /dev/null
+++ b/dom/quota/test/modules/content/worker/Assert.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const Assert = {
+ ok(value, message) {
+ postMessage({
+ op: "ok",
+ value: !!value,
+ message,
+ });
+ },
+ equal(a, b, message) {
+ postMessage({
+ op: "is",
+ a,
+ b,
+ message,
+ });
+ },
+};
diff --git a/dom/quota/test/modules/content/worker/ModuleLoader.js b/dom/quota/test/modules/content/worker/ModuleLoader.js
new file mode 100644
index 0000000000..23ee7c06df
--- /dev/null
+++ b/dom/quota/test/modules/content/worker/ModuleLoader.js
@@ -0,0 +1,52 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function ModuleLoader(base, depth) {
+ const modules = {};
+
+ const require = async function(id) {
+ if (modules[id]) {
+ return modules[id].exported_symbols;
+ }
+
+ const url = new URL(depth + id, base);
+
+ const module = Object.create(null, {
+ exported_symbols: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+
+ modules[id] = module;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url.href, false);
+ xhr.responseType = "text";
+ xhr.send();
+
+ let source = xhr.responseText;
+
+ let code = new Function(
+ "require_module",
+ "exported_symbols",
+ `eval(arguments[2] + "\\n//# sourceURL=" + arguments[3] + "\\n")`
+ );
+ code(require, module.exported_symbols, source, url.href);
+
+ return module.exported_symbols;
+ };
+
+ const returnObj = {
+ require: {
+ enumerable: true,
+ value: require,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
diff --git a/dom/quota/test/modules/content/worker/head.js b/dom/quota/test/modules/content/worker/head.js
new file mode 100644
index 0000000000..58d4591e47
--- /dev/null
+++ b/dom/quota/test/modules/content/worker/head.js
@@ -0,0 +1,55 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const Cr = {
+ NS_ERROR_NOT_IMPLEMENTED: 2147500033,
+};
+
+function add_task(func) {
+ if (!add_task.tasks) {
+ add_task.tasks = [];
+ add_task.index = 0;
+ }
+
+ add_task.tasks.push(func);
+}
+
+addEventListener("message", async function onMessage(event) {
+ function info(message) {
+ postMessage({ op: "info", message });
+ }
+
+ function executeSoon(callback) {
+ const channel = new MessageChannel();
+ channel.port1.postMessage("");
+ channel.port2.onmessage = function() {
+ callback();
+ };
+ }
+
+ function runNextTest() {
+ if (add_task.index < add_task.tasks.length) {
+ const task = add_task.tasks[add_task.index++];
+ info("add_task | Entering test " + task.name);
+ task()
+ .then(function() {
+ executeSoon(runNextTest);
+ info("add_task | Leaving test " + task.name);
+ })
+ .catch(function(ex) {
+ postMessage({ op: "failure", message: "" + ex });
+ });
+ } else {
+ postMessage({ op: "finish" });
+ }
+ }
+
+ removeEventListener("message", onMessage);
+
+ const data = event.data;
+ importScripts(...data);
+
+ executeSoon(runNextTest);
+});
diff --git a/dom/quota/test/modules/system/ModuleLoader.sys.mjs b/dom/quota/test/modules/system/ModuleLoader.sys.mjs
new file mode 100644
index 0000000000..7bddfd2760
--- /dev/null
+++ b/dom/quota/test/modules/system/ModuleLoader.sys.mjs
@@ -0,0 +1,63 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+export function ModuleLoader(base, depth, proto) {
+ const modules = {};
+
+ const principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+
+ const sharedGlobalsandbox = Cu.Sandbox(principal, {
+ invisibleToDebugger: true,
+ sandboxName: "FS Module Loader",
+ sandboxPrototype: proto,
+ wantComponents: false,
+ wantGlobalProperties: [],
+ wantXrays: false,
+ });
+
+ const require = async function(id) {
+ if (modules[id]) {
+ return modules[id].exported_symbols;
+ }
+
+ const url = new URL(depth + id, base);
+
+ const module = Object.create(null, {
+ exported_symbols: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+
+ modules[id] = module;
+
+ const properties = {
+ require_module: require,
+ exported_symbols: module.exported_symbols,
+ };
+
+ // Create a new object in this sandbox, that will be used as the scope
+ // object for this particular module.
+ const sandbox = sharedGlobalsandbox.Object();
+ Object.assign(sandbox, properties);
+
+ Services.scriptloader.loadSubScript(url.href, sandbox);
+
+ return module.exported_symbols;
+ };
+
+ const returnObj = {
+ require: {
+ enumerable: true,
+ value: require,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
diff --git a/dom/quota/test/modules/system/StorageUtils.sys.mjs b/dom/quota/test/modules/system/StorageUtils.sys.mjs
new file mode 100644
index 0000000000..bda40a4747
--- /dev/null
+++ b/dom/quota/test/modules/system/StorageUtils.sys.mjs
@@ -0,0 +1,65 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+class RequestError extends Error {
+ constructor(resultCode, resultName) {
+ super(`Request failed (code: ${resultCode}, name: ${resultName})`);
+ this.name = "RequestError";
+ this.resultCode = resultCode;
+ this.resultName = resultName;
+ }
+}
+
+export function setStoragePrefs(optionalPrefsToSet) {
+ const prefsToSet = [["dom.quotaManager.testing", true]];
+
+ if (Services.appinfo.OS === "WINNT") {
+ prefsToSet.push(["dom.quotaManager.useDOSDevicePathSyntax", true]);
+ }
+
+ if (optionalPrefsToSet) {
+ prefsToSet.push(...optionalPrefsToSet);
+ }
+
+ for (const pref of prefsToSet) {
+ Services.prefs.setBoolPref(pref[0], pref[1]);
+ }
+}
+
+export function clearStoragePrefs(optionalPrefsToClear) {
+ const prefsToClear = [
+ "dom.quotaManager.testing",
+ "dom.simpleDB.enabled",
+ "dom.storageManager.enabled",
+ ];
+
+ if (Services.appinfo.OS === "WINNT") {
+ prefsToClear.push("dom.quotaManager.useDOSDevicePathSyntax");
+ }
+
+ if (optionalPrefsToClear) {
+ prefsToClear.push(...optionalPrefsToClear);
+ }
+
+ for (const pref of prefsToClear) {
+ Services.prefs.clearUserPref(pref);
+ }
+}
+
+export async function clearStoragesForOrigin(principal) {
+ const request = Services.qms.clearStoragesForPrincipal(principal);
+
+ await new Promise(function(resolve) {
+ request.callback = function() {
+ resolve();
+ };
+ });
+
+ if (request.resultCode != Cr.NS_OK) {
+ throw new RequestError(request.resultCode, request.resultName);
+ }
+
+ return request.result;
+}
diff --git a/dom/quota/test/modules/system/WorkerDriver.sys.mjs b/dom/quota/test/modules/system/WorkerDriver.sys.mjs
new file mode 100644
index 0000000000..c03a94cf56
--- /dev/null
+++ b/dom/quota/test/modules/system/WorkerDriver.sys.mjs
@@ -0,0 +1,52 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+export async function runTestInWorker(script, base, listener) {
+ return new Promise(function(resolve) {
+ const globalHeadUrl = new URL(
+ "resource://testing-common/dom/quota/test/modules/worker/head.js"
+ );
+
+ const worker = new Worker(globalHeadUrl.href);
+
+ worker.onmessage = function(event) {
+ const data = event.data;
+
+ switch (data.op) {
+ case "ok":
+ listener.onOk(data.value, data.message);
+ break;
+
+ case "is":
+ listener.onIs(data.a, data.b, data.message);
+ break;
+
+ case "info":
+ listener.onInfo(data.message);
+ break;
+
+ case "finish":
+ resolve();
+ break;
+
+ case "failure":
+ listener.onOk(false, "Worker had a failure: " + data.message);
+ resolve();
+ break;
+ }
+ };
+
+ worker.onerror = function(event) {
+ listener.onOk(false, "Worker had an error: " + event.data);
+ resolve();
+ };
+
+ const scriptUrl = new URL(script, base);
+
+ const localHeadUrl = new URL("head.js", scriptUrl);
+
+ worker.postMessage([localHeadUrl.href, scriptUrl.href]);
+ });
+}
diff --git a/dom/quota/test/modules/system/worker/.eslintrc.js b/dom/quota/test/modules/system/worker/.eslintrc.js
new file mode 100644
index 0000000000..505e079c57
--- /dev/null
+++ b/dom/quota/test/modules/system/worker/.eslintrc.js
@@ -0,0 +1,21 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+module.exports = {
+ env: {
+ worker: true,
+ },
+
+ overrides: [
+ {
+ files: ["head.js"],
+ env: {
+ worker: true,
+ },
+ },
+ ],
+};
diff --git a/dom/quota/test/modules/system/worker/Assert.js b/dom/quota/test/modules/system/worker/Assert.js
new file mode 100644
index 0000000000..7c7e2683ea
--- /dev/null
+++ b/dom/quota/test/modules/system/worker/Assert.js
@@ -0,0 +1,22 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const Assert = {
+ ok(value, message) {
+ postMessage({
+ op: "ok",
+ value: !!value,
+ message,
+ });
+ },
+ equal(a, b, message) {
+ postMessage({
+ op: "is",
+ a,
+ b,
+ message,
+ });
+ },
+};
diff --git a/dom/quota/test/modules/system/worker/ModuleLoader.js b/dom/quota/test/modules/system/worker/ModuleLoader.js
new file mode 100644
index 0000000000..6de1fbc299
--- /dev/null
+++ b/dom/quota/test/modules/system/worker/ModuleLoader.js
@@ -0,0 +1,52 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function ModuleLoader(base, depth, proto) {
+ const modules = {};
+
+ const require = async function(id) {
+ if (modules[id]) {
+ return modules[id].exported_symbols;
+ }
+
+ const url = new URL(depth + id, base);
+
+ const module = Object.create(null, {
+ exported_symbols: {
+ configurable: false,
+ enumerable: true,
+ value: Object.create(null),
+ writable: true,
+ },
+ });
+
+ modules[id] = module;
+
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", url.href, false);
+ xhr.responseType = "text";
+ xhr.send();
+
+ let source = xhr.responseText;
+
+ let code = new Function(
+ "require_module",
+ "exported_symbols",
+ `eval(arguments[2] + "\\n//# sourceURL=" + arguments[3] + "\\n")`
+ );
+ code(require, module.exported_symbols, source, url.href);
+
+ return module.exported_symbols;
+ };
+
+ const returnObj = {
+ require: {
+ enumerable: true,
+ value: require,
+ },
+ };
+
+ return Object.create(null, returnObj);
+}
diff --git a/dom/quota/test/modules/system/worker/head.js b/dom/quota/test/modules/system/worker/head.js
new file mode 100644
index 0000000000..1308508918
--- /dev/null
+++ b/dom/quota/test/modules/system/worker/head.js
@@ -0,0 +1,56 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// eslint-disable-next-line mozilla/no-define-cc-etc
+const Cr = {
+ NS_ERROR_NOT_IMPLEMENTED: 2147500033,
+};
+
+function add_task(func) {
+ if (!add_task.tasks) {
+ add_task.tasks = [];
+ add_task.index = 0;
+ }
+
+ add_task.tasks.push(func);
+}
+
+addEventListener("message", async function onMessage(event) {
+ function info(message) {
+ postMessage({ op: "info", message });
+ }
+
+ function executeSoon(callback) {
+ const channel = new MessageChannel();
+ channel.port1.postMessage("");
+ channel.port2.onmessage = function() {
+ callback();
+ };
+ }
+
+ function runNextTest() {
+ if (add_task.index < add_task.tasks.length) {
+ const task = add_task.tasks[add_task.index++];
+ info("add_task | Entering test " + task.name);
+ task()
+ .then(function() {
+ executeSoon(runNextTest);
+ info("add_task | Leaving test " + task.name);
+ })
+ .catch(function(ex) {
+ postMessage({ op: "failure", message: "" + ex });
+ });
+ } else {
+ postMessage({ op: "finish" });
+ }
+ }
+
+ removeEventListener("message", onMessage);
+
+ const data = event.data;
+ importScripts(...data);
+
+ executeSoon(runNextTest);
+});
diff --git a/dom/quota/test/moz.build b/dom/quota/test/moz.build
new file mode 100644
index 0000000000..bab65f643e
--- /dev/null
+++ b/dom/quota/test/moz.build
@@ -0,0 +1,76 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ["gtest"]
+
+BROWSER_CHROME_MANIFESTS += ["browser/browser.ini"]
+
+MOCHITEST_MANIFESTS += ["mochitest/mochitest.ini"]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "xpcshell/caching/xpcshell.ini",
+ "xpcshell/telemetry/xpcshell.ini",
+ "xpcshell/upgrades/xpcshell.ini",
+ "xpcshell/xpcshell.ini",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.browser.dom.quota.test.common += [
+ "common/browser.js",
+ "common/content.js",
+ "common/file.js",
+ "common/global.js",
+ "common/nestedtest.js",
+ "common/system.js",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.common += [
+ "common/content.js",
+ "common/file.js",
+ "common/global.js",
+ "common/mochitest.js",
+ "common/test_simpledb.js",
+ "common/test_storage_manager_persist_allow.js",
+ "common/test_storage_manager_persist_deny.js",
+ "common/test_storage_manager_persisted.js",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.modules += [
+ "modules/content/Assert.js",
+ "modules/content/ModuleLoader.js",
+ "modules/content/StorageUtils.js",
+ "modules/content/WorkerDriver.js",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.dom.quota.test.modules.worker += [
+ "modules/content/worker/Assert.js",
+ "modules/content/worker/head.js",
+ "modules/content/worker/ModuleLoader.js",
+]
+
+TEST_HARNESS_FILES.xpcshell.dom.quota.test.common += [
+ "common/file.js",
+ "common/global.js",
+ "common/system.js",
+ "common/test_simpledb.js",
+ "common/xpcshell.js",
+]
+
+TEST_HARNESS_FILES.xpcshell.dom.quota.test.xpcshell.common += [
+ "xpcshell/common/head.js",
+ "xpcshell/common/utils.js",
+]
+
+TESTING_JS_MODULES.dom.quota.test.modules += [
+ "modules/system/ModuleLoader.sys.mjs",
+ "modules/system/StorageUtils.sys.mjs",
+ "modules/system/WorkerDriver.sys.mjs",
+]
+
+TESTING_JS_MODULES.dom.quota.test.modules.worker += [
+ "modules/system/worker/Assert.js",
+ "modules/system/worker/head.js",
+ "modules/system/worker/ModuleLoader.js",
+]
diff --git a/dom/quota/test/xpcshell/basics_profile.zip b/dom/quota/test/xpcshell/basics_profile.zip
new file mode 100644
index 0000000000..bbdb0f50cf
--- /dev/null
+++ b/dom/quota/test/xpcshell/basics_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip b/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip
new file mode 100644
index 0000000000..8124e589de
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/groupMismatch_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/caching/head.js b/dom/quota/test/xpcshell/caching/head.js
new file mode 100644
index 0000000000..5c36d82ca6
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/head.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../../";
+
+loadScript("dom/quota/test/xpcshell/common/head.js");
+
+function loadScript(path) {
+ let uri = Services.io.newFileURI(do_get_file(depth + path));
+ Services.scriptloader.loadSubScript(uri.spec);
+}
diff --git a/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js b/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js
new file mode 100644
index 0000000000..9be377e4f3
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js
@@ -0,0 +1,25 @@
+/*
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+async function testSteps() {
+ const originDirPath = "storage/default/https+++foo.example.com";
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Creating an empty origin directory");
+
+ let originDir = getRelativeFile(originDirPath);
+ originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ // The metadata file should be now restored.
+}
diff --git a/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip b/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip
new file mode 100644
index 0000000000..a5ccc05aa9
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/removedOrigin_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/caching/test_groupMismatch.js b/dom/quota/test/xpcshell/caching/test_groupMismatch.js
new file mode 100644
index 0000000000..3f8e843798
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/test_groupMismatch.js
@@ -0,0 +1,45 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that the group loaded from the origin table
+ * gets updated (if necessary) before quota initialization for the given origin.
+ */
+
+async function testSteps() {
+ const principal = getPrincipal("https://foo.bar.mozilla-iot.org");
+ const originUsage = 100;
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory with simple database
+ // data, a script for origin initialization and the storage database:
+ // - storage/default/https+++foo.bar.mozilla-iot.org
+ // - create_db.js
+ // - storage.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Manually change the group and accessed values in the origin table in
+ // storage.sqlite by running this SQL statement:
+ // UPDATE origin SET group_ = 'mozilla-iot.org', accessed = 0
+ // 2. Manually change the group in .metadata-v2 from "bar.mozilla-iot.org" to
+ // "mozilla-iot.org".
+ // 3. Remove the folder "storage/temporary".
+ // 4. Remove the file "storage/ls-archive.sqlite".
+ installPackage("groupMismatch_profile");
+
+ request = getOriginUsage(principal, /* fromMemory */ true);
+ await requestFinished(request);
+
+ is(request.result.usage, originUsage, "Correct origin usage");
+}
diff --git a/dom/quota/test/xpcshell/caching/test_removedOrigin.js b/dom/quota/test/xpcshell/caching/test_removedOrigin.js
new file mode 100644
index 0000000000..8b5702ad9c
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/test_removedOrigin.js
@@ -0,0 +1,61 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Verify that temporary storage initialization will notice a removed origin
+ * that the cache has data for and which indicates the origin was accessed
+ * during the last run. Currently, we expect LoadQuotaFromCache to fail because
+ * of this inconsistency and to fall back to full initialization.
+ */
+
+async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+ const originUsage = 0;
+
+ info("Setting pref");
+
+ // The packaged profile will have a different build ID and we would treat the
+ // cache as invalid if we didn't bypass this check.
+ Services.prefs.setBoolPref("dom.quotaManager.caching.checkBuildId", false);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains empty default storage, a script for origin
+ // initialization and the storage database:
+ // - storage/default
+ // - create_db.js
+ // - storage.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/default/http+++example.com".
+ // 2. Remove the folder "storage/temporary".
+ // 3. Remove the file "storage/ls-archive.sqlite".
+ installPackage("removedOrigin_profile");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Getting origin usage");
+
+ request = getOriginUsage(principal, /* fromMemory */ true);
+ await requestFinished(request);
+
+ is(request.result.usage, originUsage, "Correct origin usage");
+}
diff --git a/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js b/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js
new file mode 100644
index 0000000000..5abe76eade
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/test_unsetLastAccessTime.js
@@ -0,0 +1,46 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const principal = getPrincipal("https://foo.example.com/");
+
+ info("Setting pref");
+
+ // The packaged profile will have a different build ID and we would treat the
+ // cache as invalid if we didn't bypass this check.
+ Services.prefs.setBoolPref("dom.quotaManager.caching.checkBuildId", false);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory and the storage
+ // database:
+ // - storage/default/https+++foo.example.com
+ // - storage.sqlite
+ // The file make_unsetLastAccessTime.js was run locally, specifically it was
+ // temporarily enabled in xpcshell.ini and then executed:
+ // mach test --interactive dom/quota/test/xpcshell/caching/make_unsetLastAccessTime.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ // 2. Remove the file "storage/ls-archive.sqlite".
+ installPackage("unsetLastAccessTime_profile");
+
+ info("Getting full origin metadata");
+
+ request = getFullOriginMetadata("default", principal);
+ await requestFinished(request);
+
+ info("Verifying last access time");
+
+ ok(
+ BigInt(request.result.lastAccessTime) != INT64_MIN,
+ "Correct last access time"
+ );
+}
diff --git a/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip b/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip
new file mode 100644
index 0000000000..2b14ca7276
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/unsetLastAccessTime_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/caching/xpcshell.ini b/dom/quota/test/xpcshell/caching/xpcshell.ini
new file mode 100644
index 0000000000..d08f15c0a8
--- /dev/null
+++ b/dom/quota/test/xpcshell/caching/xpcshell.ini
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+head = head.js
+support-files =
+ groupMismatch_profile.zip
+ removedOrigin_profile.zip
+
+[make_unsetLastAccessTime.js]
+skip-if = true # Only used for recreating unsetLastAccessTime_profile.zip
+[test_groupMismatch.js]
+[test_removedOrigin.js]
+[test_unsetLastAccessTime.js]
+support-files =
+ unsetLastAccessTime_profile.zip
diff --git a/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip b/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip
new file mode 100644
index 0000000000..7d7985ddd0
--- /dev/null
+++ b/dom/quota/test/xpcshell/clearStoragesForPrincipal_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/common/head.js b/dom/quota/test/xpcshell/common/head.js
new file mode 100644
index 0000000000..3471b56de7
--- /dev/null
+++ b/dom/quota/test/xpcshell/common/head.js
@@ -0,0 +1,629 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const NS_OK = Cr.NS_OK;
+const NS_ERROR_FAILURE = Cr.NS_ERROR_FAILURE;
+const NS_ERROR_UNEXPECTED = Cr.NS_ERROR_UNEXPECTED;
+const NS_ERROR_FILE_NO_DEVICE_SPACE = Cr.NS_ERROR_FILE_NO_DEVICE_SPACE;
+
+const loggingEnabled = false;
+
+var testGenerator;
+
+loadScript("dom/quota/test/common/xpcshell.js");
+
+function log(msg) {
+ if (loggingEnabled) {
+ info(msg);
+ }
+}
+
+function is(a, b, msg) {
+ Assert.equal(a, b, msg);
+}
+
+function ok(cond, msg) {
+ Assert.ok(!!cond, msg);
+}
+
+function todo(cond, msg) {
+ todo_check_true(cond);
+}
+
+function run_test() {
+ runTest();
+}
+
+if (!this.runTest) {
+ this.runTest = function() {
+ do_get_profile();
+
+ enableStorageTesting();
+ enableTesting();
+
+ Cu.importGlobalProperties(["indexedDB", "File", "Blob", "FileReader"]);
+
+ // In order to support converting tests to using async functions from using
+ // generator functions, we detect async functions by checking the name of
+ // function's constructor.
+ Assert.ok(
+ typeof testSteps === "function",
+ "There should be a testSteps function"
+ );
+ if (testSteps.constructor.name === "AsyncFunction") {
+ // Do run our existing cleanup function that would normally be called by
+ // the generator's call to finishTest().
+ registerCleanupFunction(function() {
+ resetStorageTesting();
+ resetTesting();
+ });
+
+ add_task(testSteps);
+
+ // Since we defined run_test, we must invoke run_next_test() to start the
+ // async test.
+ run_next_test();
+ } else {
+ Assert.ok(
+ testSteps.constructor.name === "GeneratorFunction",
+ "Unsupported function type"
+ );
+
+ do_test_pending();
+
+ testGenerator = testSteps();
+ testGenerator.next();
+ }
+ };
+}
+
+function finishTest() {
+ resetStorageTesting();
+ resetTesting();
+
+ executeSoon(function() {
+ do_test_finished();
+ });
+}
+
+function grabArgAndContinueHandler(arg) {
+ testGenerator.next(arg);
+}
+
+function continueToNextStep() {
+ executeSoon(function() {
+ testGenerator.next();
+ });
+}
+
+function continueToNextStepSync() {
+ testGenerator.next();
+}
+
+function enableTesting() {
+ SpecialPowers.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ false
+ );
+}
+
+function resetTesting() {
+ SpecialPowers.clearUserPref(
+ "dom.storage.enable_unsupported_legacy_implementation"
+ );
+}
+
+function setGlobalLimit(globalLimit) {
+ SpecialPowers.setIntPref(
+ "dom.quotaManager.temporaryStorage.fixedLimit",
+ globalLimit
+ );
+}
+
+function resetGlobalLimit() {
+ SpecialPowers.clearUserPref("dom.quotaManager.temporaryStorage.fixedLimit");
+}
+
+function storageInitialized(callback) {
+ let request = SpecialPowers._getQuotaManager().storageInitialized();
+ request.callback = callback;
+
+ return request;
+}
+
+function temporaryStorageInitialized(callback) {
+ let request = SpecialPowers._getQuotaManager().temporaryStorageInitialized();
+ request.callback = callback;
+
+ return request;
+}
+
+function init(callback) {
+ let request = SpecialPowers._getQuotaManager().init();
+ request.callback = callback;
+
+ return request;
+}
+
+function initTemporaryStorage(callback) {
+ let request = SpecialPowers._getQuotaManager().initTemporaryStorage();
+ request.callback = callback;
+
+ return request;
+}
+
+function initPersistentOrigin(principal, callback) {
+ let request = SpecialPowers._getQuotaManager().initializePersistentOrigin(
+ principal
+ );
+ request.callback = callback;
+
+ return request;
+}
+
+function initTemporaryOrigin(persistence, principal, callback) {
+ let request = SpecialPowers._getQuotaManager().initializeTemporaryOrigin(
+ persistence,
+ principal
+ );
+ request.callback = callback;
+
+ return request;
+}
+
+function getFullOriginMetadata(persistence, principal, callback) {
+ const request = SpecialPowers._getQuotaManager().getFullOriginMetadata(
+ persistence,
+ principal
+ );
+ request.callback = callback;
+
+ return request;
+}
+
+function clearClient(principal, persistence, client, callback) {
+ let request = SpecialPowers._getQuotaManager().clearStoragesForPrincipal(
+ principal,
+ persistence,
+ client
+ );
+ request.callback = callback;
+
+ return request;
+}
+
+function clearOrigin(principal, persistence, callback) {
+ let request = SpecialPowers._getQuotaManager().clearStoragesForPrincipal(
+ principal,
+ persistence
+ );
+ request.callback = callback;
+
+ return request;
+}
+
+function resetClient(principal, client) {
+ let request = Services.qms.resetStoragesForPrincipal(
+ principal,
+ "default",
+ client
+ );
+
+ return request;
+}
+
+function persist(principal, callback) {
+ let request = SpecialPowers._getQuotaManager().persist(principal);
+ request.callback = callback;
+
+ return request;
+}
+
+function persisted(principal, callback) {
+ let request = SpecialPowers._getQuotaManager().persisted(principal);
+ request.callback = callback;
+
+ return request;
+}
+
+function estimateOrigin(principal, callback) {
+ let request = SpecialPowers._getQuotaManager().estimate(principal);
+ request.callback = callback;
+
+ return request;
+}
+
+function listOrigins(callback) {
+ let request = SpecialPowers._getQuotaManager().listOrigins(callback);
+ request.callback = callback;
+
+ return request;
+}
+
+function getPersistedFromMetadata(readBuffer) {
+ const persistedPosition = 8; // Persisted state is stored in the 9th byte
+ let view =
+ readBuffer instanceof Uint8Array ? readBuffer : new Uint8Array(readBuffer);
+
+ return !!view[persistedPosition];
+}
+
+function grabResultAndContinueHandler(request) {
+ testGenerator.next(request.result);
+}
+
+function grabUsageAndContinueHandler(request) {
+ testGenerator.next(request.result.usage);
+}
+
+function getUsage(usageHandler, getAll) {
+ let request = SpecialPowers._getQuotaManager().getUsage(usageHandler, getAll);
+
+ return request;
+}
+
+function getOriginUsage(principal, fromMemory = false) {
+ let request = Services.qms.getUsageForPrincipal(
+ principal,
+ function() {},
+ fromMemory
+ );
+
+ return request;
+}
+
+function getCurrentUsage(usageHandler) {
+ let principal = Cc["@mozilla.org/systemprincipal;1"].createInstance(
+ Ci.nsIPrincipal
+ );
+ let request = SpecialPowers._getQuotaManager().getUsageForPrincipal(
+ principal,
+ usageHandler
+ );
+
+ return request;
+}
+
+function getPrincipal(url, attr = {}) {
+ let uri = Cc["@mozilla.org/network/io-service;1"]
+ .getService(Ci.nsIIOService)
+ .newURI(url);
+ let ssm = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(
+ Ci.nsIScriptSecurityManager
+ );
+ return ssm.createContentPrincipal(uri, attr);
+}
+
+var SpecialPowers = {
+ getBoolPref(prefName) {
+ return this._getPrefs().getBoolPref(prefName);
+ },
+
+ setBoolPref(prefName, value) {
+ this._getPrefs().setBoolPref(prefName, value);
+ },
+
+ setIntPref(prefName, value) {
+ this._getPrefs().setIntPref(prefName, value);
+ },
+
+ clearUserPref(prefName) {
+ this._getPrefs().clearUserPref(prefName);
+ },
+
+ _getPrefs() {
+ let prefService = Cc["@mozilla.org/preferences-service;1"].getService(
+ Ci.nsIPrefService
+ );
+ return prefService.getBranch(null);
+ },
+
+ _getQuotaManager() {
+ return Cc["@mozilla.org/dom/quota-manager-service;1"].getService(
+ Ci.nsIQuotaManagerService
+ );
+ },
+};
+
+function installPackages(packageRelativePaths) {
+ if (packageRelativePaths.length != 2) {
+ throw new Error("Unsupported number of package relative paths");
+ }
+
+ for (const packageRelativePath of packageRelativePaths) {
+ installPackage(packageRelativePath);
+ }
+}
+
+// Take current storage structure on disk and compare it with the expected
+// structure. The expected structure is defined in JSON and consists of a per
+// test package definition and a shared package definition. The shared package
+// definition should contain unknown stuff which needs to be properly handled
+// in all situations.
+function verifyStorage(packageDefinitionRelativePaths, key) {
+ if (packageDefinitionRelativePaths.length != 2) {
+ throw new Error("Unsupported number of package definition relative paths");
+ }
+
+ function verifyEntries(entries, name, indent = "") {
+ log(`${indent}Verifying ${name} entries`);
+
+ indent += " ";
+
+ for (const entry of entries) {
+ const maybeName = entry.name;
+
+ log(`${indent}Verifying entry ${maybeName}`);
+
+ let hasName = false;
+ let hasDir = false;
+ let hasEntries = false;
+
+ for (const property in entry) {
+ switch (property) {
+ case "note":
+ case "todo":
+ break;
+
+ case "name":
+ hasName = true;
+ break;
+
+ case "dir":
+ hasDir = true;
+ break;
+
+ case "entries":
+ hasEntries = true;
+ break;
+
+ default:
+ throw new Error(`Unknown property ${property}`);
+ }
+ }
+
+ if (!hasName) {
+ throw new Error("An entry must have the name property");
+ }
+
+ if (!hasDir) {
+ throw new Error("An entry must have the dir property");
+ }
+
+ if (hasEntries && !entry.dir) {
+ throw new Error("An entry can't have entries if it's not a directory");
+ }
+
+ if (hasEntries) {
+ verifyEntries(entry.entries, entry.name, indent);
+ }
+ }
+ }
+
+ function getCurrentEntries() {
+ log("Getting current entries");
+
+ function getEntryForFile(file) {
+ let entry = {
+ name: file.leafName,
+ dir: file.isDirectory(),
+ };
+
+ if (file.isDirectory()) {
+ const enumerator = file.directoryEntries;
+ let nextFile;
+ while ((nextFile = enumerator.nextFile)) {
+ if (!entry.entries) {
+ entry.entries = [];
+ }
+ entry.entries.push(getEntryForFile(nextFile));
+ }
+ }
+
+ return entry;
+ }
+
+ let entries = [];
+
+ let file = getRelativeFile("indexedDB");
+ if (file.exists()) {
+ entries.push(getEntryForFile(file));
+ }
+
+ file = getRelativeFile("storage");
+ if (file.exists()) {
+ entries.push(getEntryForFile(file));
+ }
+
+ file = getRelativeFile("storage.sqlite");
+ if (file.exists()) {
+ entries.push(getEntryForFile(file));
+ }
+
+ verifyEntries(entries, "current");
+
+ return entries;
+ }
+
+ function getEntriesFromPackageDefinition(
+ packageDefinitionRelativePath,
+ lookupKey
+ ) {
+ log(`Getting ${lookupKey} entries from ${packageDefinitionRelativePath}`);
+
+ const currentDir = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ const file = getRelativeFile(
+ packageDefinitionRelativePath + ".json",
+ currentDir
+ );
+
+ const fileInputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ fileInputStream.init(file, -1, -1, 0);
+
+ const scriptableInputStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].createInstance(Ci.nsIScriptableInputStream);
+ scriptableInputStream.init(fileInputStream);
+
+ const data = scriptableInputStream.readBytes(
+ scriptableInputStream.available()
+ );
+
+ const obj = JSON.parse(data);
+
+ const result = obj.find(({ key: elementKey }) => elementKey == lookupKey);
+
+ if (!result) {
+ throw new Error("The file doesn't contain an element for given key");
+ }
+
+ if (!result.entries) {
+ throw new Error("The element doesn't have the entries property");
+ }
+
+ verifyEntries(result.entries, lookupKey);
+
+ return result.entries;
+ }
+
+ function addSharedEntries(expectedEntries, sharedEntries, name, indent = "") {
+ log(`${indent}Checking common ${name} entries`);
+
+ indent += " ";
+
+ for (const sharedEntry of sharedEntries) {
+ const expectedEntry = expectedEntries.find(
+ ({ name: elementName }) => elementName == sharedEntry.name
+ );
+
+ if (expectedEntry) {
+ log(`${indent}Checking common entry ${sharedEntry.name}`);
+
+ if (!expectedEntry.dir || !sharedEntry.dir) {
+ throw new Error("A common entry must be a directory");
+ }
+
+ if (!expectedEntry.entries && !sharedEntry.entries) {
+ throw new Error("A common entry must not be a leaf");
+ }
+
+ if (sharedEntry.entries) {
+ if (!expectedEntry.entries) {
+ expectedEntry.entries = [];
+ }
+
+ addSharedEntries(
+ expectedEntry.entries,
+ sharedEntry.entries,
+ sharedEntry.name,
+ indent
+ );
+ }
+ } else {
+ log(`${indent}Adding entry ${sharedEntry.name}`);
+ expectedEntries.push(sharedEntry);
+ }
+ }
+ }
+
+ function compareEntries(currentEntries, expectedEntries, name, indent = "") {
+ log(`${indent}Comparing ${name} entries`);
+
+ indent += " ";
+
+ if (currentEntries.length != expectedEntries.length) {
+ throw new Error("Entries must have the same length");
+ }
+
+ for (const currentEntry of currentEntries) {
+ log(`${indent}Comparing entry ${currentEntry.name}`);
+
+ const expectedEntry = expectedEntries.find(
+ ({ name: elementName }) => elementName == currentEntry.name
+ );
+
+ if (!expectedEntry) {
+ throw new Error("Cannot find a matching entry");
+ }
+
+ if (expectedEntry.dir != currentEntry.dir) {
+ throw new Error("The dir property doesn't match");
+ }
+
+ if (
+ (expectedEntry.entries && !currentEntry.entries) ||
+ (!expectedEntry.entries && currentEntry.entries)
+ ) {
+ throw new Error("The entries property doesn't match");
+ }
+
+ if (expectedEntry.entries) {
+ compareEntries(
+ currentEntry.entries,
+ expectedEntry.entries,
+ currentEntry.name,
+ indent
+ );
+ }
+ }
+ }
+
+ const currentEntries = getCurrentEntries();
+
+ log("Stringified current entries: " + JSON.stringify(currentEntries));
+
+ const expectedEntries = getEntriesFromPackageDefinition(
+ packageDefinitionRelativePaths[0],
+ key
+ );
+ const sharedEntries = getEntriesFromPackageDefinition(
+ packageDefinitionRelativePaths[1],
+ key
+ );
+
+ addSharedEntries(expectedEntries, sharedEntries, key);
+
+ log("Stringified expected entries: " + JSON.stringify(expectedEntries));
+
+ compareEntries(currentEntries, expectedEntries, key);
+}
+
+async function verifyInitializationStatus(
+ expectStorageIsInitialized,
+ expectTemporaryStorageIsInitialized
+) {
+ if (!expectStorageIsInitialized && expectTemporaryStorageIsInitialized) {
+ throw new Error("Invalid expectation");
+ }
+
+ let request = storageInitialized();
+ await requestFinished(request);
+
+ const storageIsInitialized = request.result;
+
+ request = temporaryStorageInitialized();
+ await requestFinished(request);
+
+ const temporaryStorageIsInitialized = request.result;
+
+ ok(
+ !(!storageIsInitialized && temporaryStorageIsInitialized),
+ "Initialization status is consistent"
+ );
+
+ if (expectStorageIsInitialized) {
+ ok(storageIsInitialized, "Storage is initialized");
+ } else {
+ ok(!storageIsInitialized, "Storage is not initialized");
+ }
+
+ if (expectTemporaryStorageIsInitialized) {
+ ok(temporaryStorageIsInitialized, "Temporary storage is initialized");
+ } else {
+ ok(!temporaryStorageIsInitialized, "Temporary storage is not initialized");
+ }
+}
diff --git a/dom/quota/test/xpcshell/common/utils.js b/dom/quota/test/xpcshell/common/utils.js
new file mode 100644
index 0000000000..ee21c90cf2
--- /dev/null
+++ b/dom/quota/test/xpcshell/common/utils.js
@@ -0,0 +1,47 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+function getOriginDir(persistence, origin) {
+ return getRelativeFile(`storage/${persistence}/${origin}`);
+}
+
+function getMetadataFile(persistence, origin) {
+ const metadataFile = getOriginDir(persistence, origin);
+ metadataFile.append(".metadata-v2");
+ return metadataFile;
+}
+
+function populateRepository(persistence) {
+ const originDir = getOriginDir(persistence, "https+++good-example.com");
+ originDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+}
+
+function makeRepositoryUnusable(persistence) {
+ // For the purpose of testing, we make a repository unusable by creating an
+ // origin directory with the metadata file created as a directory (not a
+ // file).
+ const metadataFile = getMetadataFile(persistence, "https+++bad-example.com");
+ metadataFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+}
+
+async function fillOrigin(principal, size) {
+ let database = getSimpleDatabase(principal);
+
+ let request = database.open("data");
+ await requestFinished(request);
+
+ try {
+ request = database.write(getBuffer(size));
+ await requestFinished(request);
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ request = database.close();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/createLocalStorage_profile.zip b/dom/quota/test/xpcshell/createLocalStorage_profile.zip
new file mode 100644
index 0000000000..d5958dbd59
--- /dev/null
+++ b/dom/quota/test/xpcshell/createLocalStorage_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json
new file mode 100644
index 0000000000..a59cebdc87
--- /dev/null
+++ b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.json
@@ -0,0 +1,109 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInitTemporaryStorage",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {"name":"ls-archive.sqlite","dir":false},
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "default",
+ "dir": true,
+ "todo": "Add entry invalid+++example.com once bug 1594075 is fixed",
+ "entries": [
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "todo": "Add entry invalid+++example.com once bug 1594075 is fixed",
+ "entries": [
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip
new file mode 100644
index 0000000000..61e5b60a87
--- /dev/null
+++ b/dom/quota/test/xpcshell/defaultStorageDirectory_shared.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/getUsage_profile.zip b/dom/quota/test/xpcshell/getUsage_profile.zip
new file mode 100644
index 0000000000..5144112bde
--- /dev/null
+++ b/dom/quota/test/xpcshell/getUsage_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/groupMismatch_profile.zip b/dom/quota/test/xpcshell/groupMismatch_profile.zip
new file mode 100644
index 0000000000..182b013de0
--- /dev/null
+++ b/dom/quota/test/xpcshell/groupMismatch_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/head.js b/dom/quota/test/xpcshell/head.js
new file mode 100644
index 0000000000..bf9ba22ce3
--- /dev/null
+++ b/dom/quota/test/xpcshell/head.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../";
+
+loadScript("dom/quota/test/xpcshell/common/head.js");
+
+function loadScript(path) {
+ let uri = Services.io.newFileURI(do_get_file(depth + path));
+ Services.scriptloader.loadSubScript(uri.spec);
+}
diff --git a/dom/quota/test/xpcshell/indexedDBDirectory_shared.json b/dom/quota/test/xpcshell/indexedDBDirectory_shared.json
new file mode 100644
index 0000000000..6e3d63bafc
--- /dev/null
+++ b/dom/quota/test/xpcshell/indexedDBDirectory_shared.json
@@ -0,0 +1,35 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "indexedDB",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip b/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip
new file mode 100644
index 0000000000..6b959e7525
--- /dev/null
+++ b/dom/quota/test/xpcshell/indexedDBDirectory_shared.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/make_unknownFiles.js b/dom/quota/test/xpcshell/make_unknownFiles.js
new file mode 100644
index 0000000000..dd8213ade4
--- /dev/null
+++ b/dom/quota/test/xpcshell/make_unknownFiles.js
@@ -0,0 +1,172 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+
+ const repoRelativePath = "storage/default";
+ const originRelativePath = `${repoRelativePath}/http+++example.com`;
+
+ let unknownFileCounter = 1;
+ let unknownDirCounter = 1;
+
+ function createUnknownFileIn(dirRelativePath, recursive) {
+ const dir = getRelativeFile(dirRelativePath);
+
+ let file = dir.clone();
+ file.append("foo" + unknownFileCounter + ".bar");
+
+ const ostream = Cc[
+ "@mozilla.org/network/file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+
+ ostream.init(file, -1, parseInt("0644", 8), 0);
+
+ ostream.write("x".repeat(unknownFileCounter), unknownFileCounter);
+
+ ostream.close();
+
+ unknownFileCounter++;
+
+ if (recursive) {
+ const entries = dir.directoryEntries;
+ while ((file = entries.nextFile)) {
+ if (file.isDirectory()) {
+ createUnknownFileIn(dirRelativePath + "/" + file.leafName);
+ }
+ }
+ }
+ }
+
+ function createUnknownDirectoryIn(dirRelativePath) {
+ createUnknownFileIn(dirRelativePath + "/foo" + unknownDirCounter++);
+ }
+
+ // storage.sqlite and storage/ls-archive.sqlite
+ {
+ const request = init();
+ await requestFinished(request);
+ }
+
+ // Unknown file in the repository
+ {
+ createUnknownFileIn(repoRelativePath);
+ }
+
+ // Unknown file and unknown directory in the origin directory
+ {
+ let request = init();
+ await requestFinished(request);
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ request = initTemporaryOrigin("default", principal);
+ await requestFinished(request);
+
+ ok(request.result === true, "The origin directory was created");
+
+ createUnknownFileIn(originRelativePath);
+ createUnknownDirectoryIn(originRelativePath);
+ }
+
+ // Unknown files in idb client directory and its subdirectories and unknown
+ // directory in .files directory
+ {
+ const request = indexedDB.openForPrincipal(principal, "myIndexedDB");
+ await openDBRequestUpgradeNeeded(request);
+
+ const database = request.result;
+
+ const objectStore = database.createObjectStore("Blobs", {});
+
+ objectStore.add(getNullBlob(200), 42);
+
+ await openDBRequestSucceeded(request);
+
+ database.close();
+
+ createUnknownFileIn(`${originRelativePath}/idb`);
+ createUnknownFileIn(
+ `${originRelativePath}/idb/2320029346mByDIdnedxe.files`
+ );
+ createUnknownDirectoryIn(
+ `${originRelativePath}/idb/2320029346mByDIdnedxe.files`
+ );
+ createUnknownFileIn(
+ `${originRelativePath}/idb/2320029346mByDIdnedxe.files/journals`
+ );
+ }
+
+ // Unknown files in cache client directory and its subdirectories
+ {
+ async function sandboxScript() {
+ const cache = await caches.open("myCache");
+ const request = new Request("http://example.com/index.html");
+ const response = new Response("hello world");
+ await cache.put(request, response);
+ }
+
+ const sandbox = new Cu.Sandbox(principal, {
+ wantGlobalProperties: ["caches", "fetch"],
+ });
+
+ const promise = new Promise(function(resolve, reject) {
+ sandbox.resolve = resolve;
+ sandbox.reject = reject;
+ });
+
+ Cu.evalInSandbox(
+ sandboxScript.toSource() + " sandboxScript().then(resolve, reject);",
+ sandbox
+ );
+ await promise;
+
+ createUnknownFileIn(`${originRelativePath}/cache`);
+ createUnknownFileIn(
+ `${originRelativePath}/cache/morgue`,
+ /* recursive */ true
+ );
+ }
+
+ // Unknown file and unknown directory in sdb client directory
+ {
+ const database = getSimpleDatabase(principal);
+
+ let request = database.open("mySimpleDB");
+ await requestFinished(request);
+
+ request = database.write(getBuffer(100));
+ await requestFinished(request);
+
+ request = database.close();
+ await requestFinished(request);
+
+ createUnknownFileIn(`${originRelativePath}/sdb`);
+ createUnknownDirectoryIn(`${originRelativePath}/sdb`);
+ }
+
+ // Unknown file and unknown directory in ls client directory
+ {
+ Services.prefs.setBoolPref("dom.storage.testing", true);
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+
+ const storage = Services.domStorageManager.createStorage(
+ null,
+ principal,
+ principal,
+ ""
+ );
+
+ storage.setItem("foo", "bar");
+
+ storage.close();
+
+ createUnknownFileIn(`${originRelativePath}/ls`);
+ createUnknownDirectoryIn(`${originRelativePath}/ls`);
+ }
+}
diff --git a/dom/quota/test/xpcshell/make_unsetLastAccessTime.js b/dom/quota/test/xpcshell/make_unsetLastAccessTime.js
new file mode 100644
index 0000000000..9be377e4f3
--- /dev/null
+++ b/dom/quota/test/xpcshell/make_unsetLastAccessTime.js
@@ -0,0 +1,25 @@
+/*
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+async function testSteps() {
+ const originDirPath = "storage/default/https+++foo.example.com";
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Creating an empty origin directory");
+
+ let originDir = getRelativeFile(originDirPath);
+ originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ // The metadata file should be now restored.
+}
diff --git a/dom/quota/test/xpcshell/originMismatch_profile.json b/dom/quota/test/xpcshell/originMismatch_profile.json
new file mode 100644
index 0000000000..5e1cb50631
--- /dev/null
+++ b/dom/quota/test/xpcshell/originMismatch_profile.json
@@ -0,0 +1,74 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.example.com",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata-v2", "dir": false },
+ { "name": "cache",
+ "dir": true,
+ "entries": [{ "name": ".padding", "dir": false }]
+ }
+ ]
+ },
+ { "name": "http+++www.example.com.", "dir": true },
+ {
+ "name": "http+++www.example.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata-v2", "dir": false },
+ { "name": "cache",
+ "dir": true,
+ "entries": [{ "name": ".padding", "dir": false }]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInitTemporaryStorage",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "http+++www.example.com.",
+ "dir": true,
+ "entries": [{ "name": ".metadata-v2", "dir": false }]
+ },
+ {
+ "name": "http+++www.example.org.",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata-v2", "dir": false },
+ { "name": "cache", "dir": true }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/originMismatch_profile.zip b/dom/quota/test/xpcshell/originMismatch_profile.zip
new file mode 100644
index 0000000000..dd5795736f
--- /dev/null
+++ b/dom/quota/test/xpcshell/originMismatch_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json
new file mode 100644
index 0000000000..8d36293bbf
--- /dev/null
+++ b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.json
@@ -0,0 +1,57 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ { "name": "invalid+++example.com", "dir": true },
+ { "name": "foo.bar", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip
new file mode 100644
index 0000000000..b80bfe904b
--- /dev/null
+++ b/dom/quota/test/xpcshell/persistentStorageDirectory_shared.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip b/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip
new file mode 100644
index 0000000000..19e971433c
--- /dev/null
+++ b/dom/quota/test/xpcshell/removeLocalStorage1_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip b/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip
new file mode 100644
index 0000000000..04d1a3462b
--- /dev/null
+++ b/dom/quota/test/xpcshell/removeLocalStorage2_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/head.js b/dom/quota/test/xpcshell/telemetry/head.js
new file mode 100644
index 0000000000..5c36d82ca6
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/head.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../../";
+
+loadScript("dom/quota/test/xpcshell/common/head.js");
+
+function loadScript(path) {
+ let uri = Services.io.newFileURI(do_get_file(depth + path));
+ Services.scriptloader.loadSubScript(uri.spec);
+}
diff --git a/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js b/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js
new file mode 100644
index 0000000000..eafbc95754
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/test_qm_first_initialization_attempt.js
@@ -0,0 +1,867 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const storageDirName = "storage";
+const storageFileName = "storage.sqlite";
+const indexedDBDirName = "indexedDB";
+const persistentStorageDirName = "storage/persistent";
+const histogramName = "QM_FIRST_INITIALIZATION_ATTEMPT";
+
+const testcases = [
+ {
+ mainKey: "Storage",
+ async setup(expectedInitResult) {
+ if (!expectedInitResult) {
+ // Make the database unusable by creating it as a directory (not a
+ // file).
+ const storageFile = getRelativeFile(storageFileName);
+ storageFile.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "TemporaryStorage",
+ async setup(expectedInitResult) {
+ // We need to initialize storage before populating the repositories. If
+ // we don't do that, the storage directory created for the repositories
+ // would trigger storage upgrades (from version 0 to current version).
+ let request = init();
+ await requestFinished(request);
+
+ populateRepository("temporary");
+ populateRepository("default");
+
+ if (!expectedInitResult) {
+ makeRepositoryUnusable("temporary");
+ makeRepositoryUnusable("default");
+ }
+ },
+ initFunction: initTemporaryStorage,
+ getExpectedSnapshots() {
+ const expectedSnapshotsInNightly = {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ TemporaryRepository: {
+ values: [1, 0],
+ },
+ DefaultRepository: {
+ values: [1, 0],
+ },
+ // mainKey
+ TemporaryStorage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ TemporaryRepository: {
+ values: [1, 1, 0],
+ },
+ DefaultRepository: {
+ values: [1, 1, 0],
+ },
+ // mainKey
+ TemporaryStorage: {
+ values: [1, 1, 0],
+ },
+ },
+ };
+
+ const expectedSnapshotsInOthers = {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ TemporaryRepository: {
+ values: [1, 0],
+ },
+ // mainKey
+ TemporaryStorage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ TemporaryRepository: {
+ values: [1, 1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 1, 0],
+ },
+ // mainKey
+ TemporaryStorage: {
+ values: [1, 1, 0],
+ },
+ },
+ };
+
+ return AppConstants.NIGHTLY_BUILD
+ ? expectedSnapshotsInNightly
+ : expectedSnapshotsInOthers;
+ },
+ },
+ {
+ mainKey: "DefaultRepository",
+ async setup(expectedInitResult) {
+ // See the comment for "TemporaryStorage".
+ let request = init();
+ await requestFinished(request);
+
+ populateRepository("default");
+
+ if (!expectedInitResult) {
+ makeRepositoryUnusable("default");
+ }
+ },
+ initFunction: initTemporaryStorage,
+ expectedSnapshots: {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 1, 0],
+ },
+ // mainKey
+ DefaultRepository: {
+ values: [1, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 2, 0],
+ },
+ // mainKey
+ DefaultRepository: {
+ values: [1, 1, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "TemporaryRepository",
+ async setup(expectedInitResult) {
+ // See the comment for "TemporaryStorage".
+ let request = init();
+ await requestFinished(request);
+
+ populateRepository("temporary");
+
+ if (!expectedInitResult) {
+ makeRepositoryUnusable("temporary");
+ }
+ },
+ initFunction: initTemporaryStorage,
+ getExpectedSnapshots() {
+ const expectedSnapshotsInNightly = {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ // mainKey
+ TemporaryRepository: {
+ values: [1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 1, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ // mainKey
+ TemporaryRepository: {
+ values: [1, 1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 2, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 1, 0],
+ },
+ },
+ };
+
+ const expectedSnapshotsInOthers = {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ // mainKey
+ TemporaryRepository: {
+ values: [1, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ // mainKey
+ TemporaryRepository: {
+ values: [1, 1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 1, 0],
+ },
+ TemporaryStorage: {
+ values: [1, 1, 0],
+ },
+ },
+ };
+
+ return AppConstants.NIGHTLY_BUILD
+ ? expectedSnapshotsInNightly
+ : expectedSnapshotsInOthers;
+ },
+ },
+ {
+ mainKey: "UpgradeStorageFrom0_0To1_0",
+ async setup(expectedInitResult) {
+ // storage used prior FF 49 (storage version 0.0)
+ installPackage("version0_0_profile");
+
+ if (!expectedInitResult) {
+ installPackage("version0_0_make_it_unusable");
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeStorageFrom0_0To1_0: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeStorageFrom0_0To1_0: {
+ values: [1, 1, 0],
+ },
+ UpgradeStorageFrom1_0To2_0: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_0To2_1: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_1To2_2: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeStorageFrom1_0To2_0",
+ async setup(expectedInitResult) {
+ // storage used by FF 49-54 (storage version 1.0)
+ installPackage("version1_0_profile");
+
+ if (!expectedInitResult) {
+ installPackage("version1_0_make_it_unusable");
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeStorageFrom1_0To2_0: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeStorageFrom1_0To2_0: {
+ values: [1, 1, 0],
+ },
+ UpgradeStorageFrom2_0To2_1: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_1To2_2: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeStorageFrom2_0To2_1",
+ async setup(expectedInitResult) {
+ // storage used by FF 55-56 (storage version 2.0)
+ installPackage("version2_0_profile");
+
+ if (!expectedInitResult) {
+ installPackage("version2_0_make_it_unusable");
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeStorageFrom2_0To2_1: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeStorageFrom2_0To2_1: {
+ values: [1, 1, 0],
+ },
+ UpgradeStorageFrom2_1To2_2: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeStorageFrom2_1To2_2",
+ async setup(expectedInitResult) {
+ // storage used by FF 57-67 (storage version 2.1)
+ installPackage("version2_1_profile");
+
+ if (!expectedInitResult) {
+ installPackage("version2_1_make_it_unusable");
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeStorageFrom2_1To2_2: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeStorageFrom2_1To2_2: {
+ values: [1, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeStorageFrom2_2To2_3",
+ async setup(expectedInitResult) {
+ // storage used by FF 68-69 (storage version 2.2)
+ installPackage("version2_2_profile");
+
+ if (!expectedInitResult) {
+ installPackage(
+ "version2_2_make_it_unusable",
+ /* allowFileOverwrites */ true
+ );
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeStorageFrom2_2To2_3: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeStorageFrom2_2To2_3: {
+ values: [1, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeFromIndexedDBDirectory",
+ async setup(expectedInitResult) {
+ const indexedDBDir = getRelativeFile(indexedDBDirName);
+ indexedDBDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ if (!expectedInitResult) {
+ // "indexedDB" directory will be moved under "storage" directory and at
+ // the same time renamed to "persistent". Create a storage file to cause
+ // the moves to fail.
+ const storageFile = getRelativeFile(storageDirName);
+ storageFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeFromIndexedDBDirectory: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeFromIndexedDBDirectory: {
+ values: [1, 1, 0],
+ },
+ UpgradeFromPersistentStorageDirectory: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom0_0To1_0: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom1_0To2_0: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_0To2_1: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_1To2_2: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "UpgradeFromPersistentStorageDirectory",
+ async setup(expectedInitResult) {
+ const persistentStorageDir = getRelativeFile(persistentStorageDirName);
+ persistentStorageDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ if (!expectedInitResult) {
+ // Create a metadata directory to break creating or upgrading directory
+ // metadata files.
+ const metadataDir = getRelativeFile(
+ "storage/persistent/https+++bad.example.com/.metadata"
+ );
+ metadataDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ },
+ initFunction: init,
+ expectedSnapshots: {
+ initFailure: {
+ // mainKey
+ UpgradeFromPersistentStorageDirectory: {
+ values: [1, 0],
+ },
+ Storage: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ // mainKey
+ UpgradeFromPersistentStorageDirectory: {
+ values: [1, 1, 0],
+ },
+ UpgradeStorageFrom0_0To1_0: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom1_0To2_0: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_0To2_1: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_1To2_2: {
+ values: [0, 1, 0],
+ },
+ UpgradeStorageFrom2_2To2_3: {
+ values: [0, 1, 0],
+ },
+ Storage: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "PersistentOrigin",
+ async setup(expectedInitResult) {
+ // We need to initialize storage before creating the origin files. If we
+ // don't do that, the storage directory created for the origin files
+ // would trigger storage upgrades (from version 0 to current version).
+ let request = init();
+ await requestFinished(request);
+
+ if (!expectedInitResult) {
+ const originFiles = [
+ getRelativeFile("storage/permanent/https+++example.com"),
+ getRelativeFile("storage/permanent/https+++example1.com"),
+ getRelativeFile("storage/default/https+++example2.com"),
+ ];
+
+ for (const originFile of originFiles) {
+ originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ }
+ }
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+ },
+ initFunctions: [
+ {
+ name: initPersistentOrigin,
+ args: [getPrincipal("https://example.com")],
+ },
+ {
+ name: initPersistentOrigin,
+ args: [getPrincipal("https://example1.com")],
+ },
+ {
+ name: initTemporaryOrigin,
+ args: ["default", getPrincipal("https://example2.com")],
+ },
+ ],
+ expectedSnapshots: {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 1, 0],
+ },
+ TemporaryStorage: {
+ values: [0, 1, 0],
+ },
+ // mainKey
+ PersistentOrigin: {
+ values: [2, 0],
+ },
+ TemporaryOrigin: {
+ values: [1, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 2, 0],
+ },
+ DefaultRepository: {
+ values: [0, 2, 0],
+ },
+ TemporaryStorage: {
+ values: [0, 2, 0],
+ },
+ // mainKey
+ PersistentOrigin: {
+ values: [2, 2, 0],
+ },
+ TemporaryOrigin: {
+ values: [1, 1, 0],
+ },
+ },
+ },
+ },
+ {
+ mainKey: "TemporaryOrigin",
+ async setup(expectedInitResult) {
+ // See the comment for "PersistentOrigin".
+ let request = init();
+ await requestFinished(request);
+
+ if (!expectedInitResult) {
+ const originFiles = [
+ getRelativeFile("storage/temporary/https+++example.com"),
+ getRelativeFile("storage/default/https+++example.com"),
+ getRelativeFile("storage/default/https+++example1.com"),
+ getRelativeFile("storage/permanent/https+++example2.com"),
+ ];
+
+ for (const originFile of originFiles) {
+ originFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o666);
+ }
+ }
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+ },
+ initFunctions: [
+ {
+ name: initTemporaryOrigin,
+ args: ["temporary", getPrincipal("https://example.com")],
+ },
+ {
+ name: initTemporaryOrigin,
+ args: ["default", getPrincipal("https://example.com")],
+ },
+ {
+ name: initTemporaryOrigin,
+ args: ["default", getPrincipal("https://example1.com")],
+ },
+ {
+ name: initPersistentOrigin,
+ args: [getPrincipal("https://example2.com")],
+ },
+ ],
+ // Only the first result of EnsureTemporaryOriginIsInitialized per origin
+ // should be reported. Thus, only the results for (temporary, example.com),
+ // and (default, example1.com) should be reported.
+ expectedSnapshots: {
+ initFailure: {
+ Storage: {
+ values: [0, 1, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 1, 0],
+ },
+ DefaultRepository: {
+ values: [0, 1, 0],
+ },
+ TemporaryStorage: {
+ values: [0, 1, 0],
+ },
+ PersistentOrigin: {
+ values: [1, 0],
+ },
+ // mainKey
+ TemporaryOrigin: {
+ values: [2, 0],
+ },
+ },
+ initFailureThenSuccess: {
+ Storage: {
+ values: [0, 2, 0],
+ },
+ TemporaryRepository: {
+ values: [0, 2, 0],
+ },
+ DefaultRepository: {
+ values: [0, 2, 0],
+ },
+ TemporaryStorage: {
+ values: [0, 2, 0],
+ },
+ PersistentOrigin: {
+ values: [1, 1, 0],
+ },
+ // mainKey
+ TemporaryOrigin: {
+ values: [2, 2, 0],
+ },
+ },
+ },
+ },
+];
+
+loadScript("dom/quota/test/xpcshell/common/utils.js");
+
+function verifyHistogram(histogram, mainKey, expectedSnapshot) {
+ const snapshot = histogram.snapshot();
+
+ ok(
+ mainKey in snapshot,
+ `The histogram ${histogram.name()} must contain the main key ${mainKey}`
+ );
+
+ const keys = Object.keys(snapshot);
+
+ is(
+ keys.length,
+ Object.keys(expectedSnapshot).length,
+ `The number of keys must match the expected number of keys for ` +
+ `${histogram.name()}`
+ );
+
+ for (const key of keys) {
+ ok(
+ key in expectedSnapshot,
+ `The key ${key} must match the expected keys for ${histogram.name()}`
+ );
+
+ const values = Object.entries(snapshot[key].values);
+ const expectedValues = expectedSnapshot[key].values;
+
+ is(
+ values.length,
+ expectedValues.length,
+ `The number of values should match the expected number of values for ` +
+ `${histogram.name()}`
+ );
+
+ for (let [i, val] of values) {
+ is(
+ val,
+ expectedValues[i],
+ `Expected counts should match for ${histogram.name()} at index ${i}`
+ );
+ }
+ }
+}
+
+async function testSteps() {
+ let request;
+ for (const testcase of testcases) {
+ const mainKey = testcase.mainKey;
+
+ info(`Verifying ${histogramName} histogram for the main key ${mainKey}`);
+
+ const histogram = TelemetryTestUtils.getAndClearKeyedHistogram(
+ histogramName
+ );
+
+ for (const expectedInitResult of [false, true]) {
+ info(
+ `Verifying the histogram when the initialization ` +
+ `${expectedInitResult ? "failed and then succeeds" : "fails"}`
+ );
+
+ await testcase.setup(expectedInitResult);
+
+ const msg = `Should ${expectedInitResult ? "not " : ""} have thrown`;
+
+ // Call the initialization function twice, so we can verify below that
+ // only the first initialization attempt has been reported.
+ for (let i = 0; i < 2; ++i) {
+ let initFunctions;
+
+ if (testcase.initFunctions) {
+ initFunctions = testcase.initFunctions;
+ } else {
+ initFunctions = [
+ {
+ name: testcase.initFunction,
+ args: [],
+ },
+ ];
+ }
+
+ for (const initFunction of initFunctions) {
+ request = initFunction.name(...initFunction.args);
+ try {
+ await requestFinished(request);
+ ok(expectedInitResult, msg);
+ } catch (ex) {
+ ok(!expectedInitResult, msg);
+ }
+ }
+ }
+
+ const expectedSnapshots = testcase.getExpectedSnapshots
+ ? testcase.getExpectedSnapshots()
+ : testcase.expectedSnapshots;
+
+ const expectedSnapshot = expectedInitResult
+ ? expectedSnapshots.initFailureThenSuccess
+ : expectedSnapshots.initFailure;
+
+ verifyHistogram(histogram, mainKey, expectedSnapshot);
+
+ // The first initialization attempt has been reported in the histogram
+ // and any new attemps wouldn't be reported if we didn't reset or clear
+ // the storage here. We need a clean profile for the next iteration
+ // anyway.
+ // However, the clear storage operation needs initialized storage, so
+ // clearing can fail if the storage is unusable and it can also increase
+ // some of the telemetry counters. Instead of calling clear, we can just
+ // call reset and clear profile manually.
+ request = reset();
+ await requestFinished(request);
+
+ const indexedDBDir = getRelativeFile(indexedDBDirName);
+ if (indexedDBDir.exists()) {
+ indexedDBDir.remove(false);
+ }
+
+ const storageDir = getRelativeFile(storageDirName);
+ if (storageDir.exists()) {
+ storageDir.remove(true);
+ }
+
+ const storageFile = getRelativeFile(storageFileName);
+ if (storageFile.exists()) {
+ // It could be a non empty directory, so remove it recursively.
+ storageFile.remove(true);
+ }
+ }
+ }
+}
diff --git a/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip
new file mode 100644
index 0000000000..92dcfb777e
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version0_0_make_it_unusable.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip
new file mode 100644
index 0000000000..2fb0f525c2
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version0_0_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip
new file mode 100644
index 0000000000..92dcfb777e
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version1_0_make_it_unusable.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip
new file mode 100644
index 0000000000..6169f439c1
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version1_0_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip
new file mode 100644
index 0000000000..92dcfb777e
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_0_make_it_unusable.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip
new file mode 100644
index 0000000000..465f53cea9
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_0_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip
new file mode 100644
index 0000000000..92dcfb777e
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_1_make_it_unusable.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip
new file mode 100644
index 0000000000..81463235ab
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_1_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip b/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip
new file mode 100644
index 0000000000..b6b8eecabf
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_2_make_it_unusable.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip b/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip
new file mode 100644
index 0000000000..e572726cca
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/version2_2_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/telemetry/xpcshell.ini b/dom/quota/test/xpcshell/telemetry/xpcshell.ini
new file mode 100644
index 0000000000..bb5655f103
--- /dev/null
+++ b/dom/quota/test/xpcshell/telemetry/xpcshell.ini
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+head = head.js
+support-files =
+ version0_0_make_it_unusable.zip
+ version0_0_profile.zip
+ version1_0_make_it_unusable.zip
+ version1_0_profile.zip
+ version2_0_make_it_unusable.zip
+ version2_0_profile.zip
+ version2_1_make_it_unusable.zip
+ version2_1_profile.zip
+ version2_2_make_it_unusable.zip
+ version2_2_profile.zip
+
+[test_qm_first_initialization_attempt.js]
+skip-if = appname == "thunderbird"
diff --git a/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip b/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip
new file mode 100644
index 0000000000..da1de0979b
--- /dev/null
+++ b/dom/quota/test/xpcshell/tempMetadataCleanup_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/test_allowListFiles.js b/dom/quota/test/xpcshell/test_allowListFiles.js
new file mode 100644
index 0000000000..04c64c2ef5
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_allowListFiles.js
@@ -0,0 +1,61 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify thoes unexpected files are in the allow list of
+ * QuotaManager. They aren't expected in the repository but if there are,
+ * QuotaManager shouldn't fail to initialize an origin and getting usage, though
+ * those files aren't managed by QuotaManager.
+ */
+
+async function testSteps() {
+ const allowListFiles = [
+ ".dot-file",
+ "desktop.ini",
+ "Desktop.ini",
+ "Thumbs.db",
+ "thumbs.db",
+ ];
+
+ for (let allowListFile of allowListFiles) {
+ info("Testing " + allowListFile + " in the repository");
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Creating unknown files");
+
+ for (let dir of ["persistenceType dir", "origin dir"]) {
+ let dirPath =
+ dir == "persistenceType dir"
+ ? "storage/default/"
+ : "storage/default/http+++example.com/";
+ let file = getRelativeFile(dirPath + allowListFile);
+ file.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0644", 8));
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Resetting");
+
+ request = reset();
+ await requestFinished(request);
+
+ info("Getting usage");
+
+ request = getCurrentUsage(continueToNextStepSync);
+ await requestFinished(request);
+
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+ }
+}
diff --git a/dom/quota/test/xpcshell/test_bad_origin_directory.js b/dom/quota/test/xpcshell/test_bad_origin_directory.js
new file mode 100644
index 0000000000..3058e31473
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_bad_origin_directory.js
@@ -0,0 +1,36 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const invalidOrigin = {
+ url: "ftp://ftp.invalid.origin",
+ path: "storage/default/ftp+++ftp.invalid.origin",
+ };
+
+ info("Persisting an invalid origin");
+
+ let invalidPrincipal = getPrincipal(invalidOrigin.url);
+
+ let request = persist(invalidPrincipal, continueToNextStepSync);
+ yield undefined;
+
+ ok(
+ request.resultCode === NS_ERROR_FAILURE,
+ "Persist() failed because of the invalid origin"
+ );
+ ok(request.result === null, "The request result is null");
+
+ let originDir = getRelativeFile(invalidOrigin.path);
+ let exists = originDir.exists();
+ ok(!exists, "Directory for invalid origin doesn't exist");
+
+ request = persisted(invalidPrincipal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persisted() succeeded");
+ ok(!request.result, "The origin isn't persisted since the operation failed");
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_basics.js b/dom/quota/test/xpcshell/test_basics.js
new file mode 100644
index 0000000000..b72c2feb9f
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_basics.js
@@ -0,0 +1,143 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const storageFile = "storage.sqlite";
+
+ const metadataFiles = [
+ {
+ path: "storage/permanent/chrome/.metadata",
+ shouldExistAfterInit: false,
+ },
+
+ {
+ path: "storage/permanent/chrome/.metadata-tmp",
+ shouldExistAfterInit: false,
+ },
+
+ {
+ path: "storage/permanent/chrome/.metadata-v2",
+ shouldExistAfterInit: true,
+ },
+
+ {
+ path: "storage/permanent/chrome/.metadata-v2-tmp",
+ shouldExistAfterInit: false,
+ },
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(false, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Getting usage");
+
+ getCurrentUsage(grabUsageAndContinueHandler);
+ let usage = yield undefined;
+
+ ok(usage == 0, "Usage is zero");
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(true, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(false, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+
+ // The profile contains just one empty IndexedDB database. The file
+ // create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_db.js
+ installPackage("basics_profile");
+
+ info("Getting usage");
+
+ getCurrentUsage(grabUsageAndContinueHandler);
+ usage = yield undefined;
+
+ ok(usage > 0, "Usage is not zero");
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(true, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Checking storage file");
+
+ let file = getRelativeFile(storageFile);
+
+ let exists = file.exists();
+ ok(!exists, "Storage file doesn't exist");
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(false, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Initializing");
+
+ request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ exists = file.exists();
+ ok(exists, "Storage file does exist");
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(true, false).then(continueToNextStepSync);
+ yield undefined;
+
+ info("Initializing origin");
+
+ request = initPersistentOrigin(getCurrentPrincipal(), continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(request.result, "Origin directory was created");
+
+ for (let metadataFile of metadataFiles) {
+ file = getRelativeFile(metadataFile.path);
+
+ exists = file.exists();
+
+ if (metadataFile.shouldExistAfterInit) {
+ ok(exists, "Metadata file does exist");
+ } else {
+ ok(!exists, "Metadata file doesn't exist");
+ }
+ }
+
+ info("Verifying initialization status");
+
+ verifyInitializationStatus(true, false).then(continueToNextStepSync);
+
+ yield undefined;
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js b/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js
new file mode 100644
index 0000000000..096cf2be70
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_clearStoragesForOriginAttributesPattern.js
@@ -0,0 +1,58 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const baseRelativePath = "storage/default";
+ const userContextForRemoval = 2;
+
+ const origins = [
+ {
+ userContextId: 1,
+ baseDirName: "https+++example.com",
+ },
+
+ {
+ userContextId: userContextForRemoval,
+ baseDirName: "https+++example.com",
+ },
+
+ // TODO: Uncomment this once bug 1638831 is fixed.
+ /*
+ {
+ userContextId: userContextForRemoval,
+ baseDirName: "https+++example.org",
+ },
+ */
+ ];
+
+ function getOriginDirectory(origin) {
+ return getRelativeFile(
+ `${baseRelativePath}/${origin.baseDirName}^userContextId=` +
+ `${origin.userContextId}`
+ );
+ }
+
+ let request = init();
+ await requestFinished(request);
+
+ for (const origin of origins) {
+ const directory = getOriginDirectory(origin);
+ directory.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+
+ request = Services.qms.clearStoragesForOriginAttributesPattern(
+ `{ "userContextId": ${userContextForRemoval} }`
+ );
+ await requestFinished(request);
+
+ for (const origin of origins) {
+ const directory = getOriginDirectory(origin);
+ if (origin.userContextId === userContextForRemoval) {
+ ok(!directory.exists(), "Origin directory should have been removed");
+ } else {
+ ok(directory.exists(), "Origin directory shouldn't have been removed");
+ }
+ }
+}
diff --git a/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js b/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js
new file mode 100644
index 0000000000..1e8bed54b7
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_clearStoragesForPrincipal.js
@@ -0,0 +1,56 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is an xpcshell test for clearStoragesForPrincipal. It verifies that
+ * if the removing client is the last client in the targeting origin, then it
+ * is expected to remove the origin directory as well.
+ */
+
+async function testSteps() {
+ const testingOrigins = [
+ {
+ origin: "http://example.com",
+ path: "storage/default/http+++example.com/",
+ only_idb: false,
+ },
+ {
+ origin: "http://www.mozilla.org",
+ path: "storage/default/http+++www.mozilla.org/",
+ only_idb: true,
+ },
+ ];
+ const removingClient = "idb";
+
+ info("Installing package to create the environment");
+ // The package is manually created and it contains:
+ // - storage/default/http+++www.mozilla.org/idb/
+ // - storage/default/http+++www.example.com/idb/
+ // - storage/default/http+++www.example.com/cache/
+ installPackage("clearStoragesForPrincipal_profile");
+
+ let request;
+ let file;
+ for (let i = 0; i < testingOrigins.length; ++i) {
+ info("Clearing");
+ request = clearClient(
+ getPrincipal(testingOrigins[i].origin),
+ null,
+ removingClient
+ );
+ await requestFinished(request);
+
+ info("Verifying");
+ file = getRelativeFile(testingOrigins[i].path + removingClient);
+ ok(!file.exists(), "Client file doesn't exist");
+
+ file = getRelativeFile(testingOrigins[i].path);
+ if (testingOrigins[i].only_idb) {
+ todo(!file.exists(), "Origin file doesn't exist");
+ } else {
+ ok(file.exists(), "Origin file does exist");
+ }
+ }
+}
diff --git a/dom/quota/test/xpcshell/test_createLocalStorage.js b/dom/quota/test/xpcshell/test_createLocalStorage.js
new file mode 100644
index 0000000000..a99a458713
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_createLocalStorage.js
@@ -0,0 +1,155 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const webAppsStoreFile = "webappsstore.sqlite";
+ const lsArchiveFile = "storage/ls-archive.sqlite";
+ const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite";
+
+ function checkArchiveFileNotExists() {
+ info("Checking archive tmp file");
+
+ let archiveTmpFile = getRelativeFile(lsArchiveTmpFile);
+
+ let exists = archiveTmpFile.exists();
+ ok(!exists, "archive tmp file doesn't exist");
+
+ info("Checking archive file");
+
+ let archiveFile = getRelativeFile(lsArchiveFile);
+
+ exists = archiveFile.exists();
+ ok(!exists, "archive file doesn't exist");
+ }
+
+ function checkArchiveFileExists() {
+ info("Checking archive tmp file");
+
+ let archiveTmpFile = getRelativeFile(lsArchiveTmpFile);
+
+ let exists = archiveTmpFile.exists();
+ ok(!exists, "archive tmp file doesn't exist");
+
+ info("Checking archive file");
+
+ let archiveFile = getRelativeFile(lsArchiveFile);
+
+ exists = archiveFile.exists();
+ ok(exists, "archive file does exist");
+
+ info("Checking archive file size");
+
+ let fileSize = archiveFile.fileSize;
+ ok(fileSize > 0, "archive file size is greater than zero");
+ }
+
+ // Profile 1 - Nonexistent apps store file.
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ let appsStoreFile = getRelativeFile(webAppsStoreFile);
+
+ let exists = appsStoreFile.exists();
+ ok(!exists, "apps store file doesn't exist");
+
+ checkArchiveFileNotExists();
+
+ try {
+ request = init();
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ checkArchiveFileExists();
+
+ // Profile 2 - apps store file is a directory.
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ appsStoreFile.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ checkArchiveFileNotExists();
+
+ try {
+ request = init();
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ checkArchiveFileExists();
+
+ appsStoreFile.remove(true);
+
+ // Profile 3 - Corrupted apps store file.
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ let ostream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ ostream.init(appsStoreFile, -1, parseInt("0644", 8), 0);
+ ostream.write("foobar", 6);
+ ostream.close();
+
+ checkArchiveFileNotExists();
+
+ try {
+ request = init();
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ checkArchiveFileExists();
+
+ appsStoreFile.remove(false);
+
+ // Profile 4 - Nonupdateable apps store file.
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains storage.sqlite and webappsstore.sqlite
+ // webappstore.sqlite was taken from FF 54 to force an upgrade.
+ // There's just one record in the webappsstore2 table. The record was
+ // modified by renaming the origin attribute userContextId to userContextKey.
+ // This triggers an error during the upgrade.
+ installPackage("createLocalStorage_profile");
+
+ let fileSize = appsStoreFile.fileSize;
+ ok(fileSize > 0, "apps store file size is greater than zero");
+
+ checkArchiveFileNotExists();
+
+ try {
+ request = init();
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ checkArchiveFileExists();
+
+ appsStoreFile.remove(false);
+}
diff --git a/dom/quota/test/xpcshell/test_estimateOrigin.js b/dom/quota/test/xpcshell/test_estimateOrigin.js
new file mode 100644
index 0000000000..31d6f01686
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_estimateOrigin.js
@@ -0,0 +1,80 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/xpcshell/common/utils.js");
+
+async function verifyOriginEstimation(principal, expectedUsage, expectedLimit) {
+ info("Estimating origin");
+
+ const request = estimateOrigin(principal);
+ await requestFinished(request);
+
+ is(request.result.usage, expectedUsage, "Correct usage");
+ is(request.result.limit, expectedLimit, "Correct limit");
+}
+
+async function testSteps() {
+ // The group limit is calculated as 20% of the global limit and the minimum
+ // value of the group limit is 10 MB.
+
+ const groupLimitKB = 10 * 1024;
+ const groupLimitBytes = groupLimitKB * 1024;
+ const globalLimitKB = groupLimitKB * 5;
+ const globalLimitBytes = globalLimitKB * 1024;
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Filling origins");
+
+ await fillOrigin(getPrincipal("https://foo1.example1.com"), 100);
+ await fillOrigin(getPrincipal("https://foo2.example1.com"), 200);
+ await fillOrigin(getPrincipal("https://foo1.example2.com"), 300);
+ await fillOrigin(getPrincipal("https://foo2.example2.com"), 400);
+
+ info("Verifying origin estimations");
+
+ await verifyOriginEstimation(
+ getPrincipal("https://foo1.example1.com"),
+ 300,
+ groupLimitBytes
+ );
+ await verifyOriginEstimation(
+ getPrincipal("https://foo2.example1.com"),
+ 300,
+ groupLimitBytes
+ );
+ await verifyOriginEstimation(
+ getPrincipal("https://foo1.example2.com"),
+ 700,
+ groupLimitBytes
+ );
+ await verifyOriginEstimation(
+ getPrincipal("https://foo2.example2.com"),
+ 700,
+ groupLimitBytes
+ );
+
+ info("Persisting origin");
+
+ request = persist(getPrincipal("https://foo2.example2.com"));
+ await requestFinished(request);
+
+ info("Verifying origin estimation");
+
+ await verifyOriginEstimation(
+ getPrincipal("https://foo2.example2.com"),
+ 1000,
+ globalLimitBytes
+ );
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_getUsage.js b/dom/quota/test/xpcshell/test_getUsage.js
new file mode 100644
index 0000000000..282894beeb
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_getUsage.js
@@ -0,0 +1,129 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const origins = [
+ {
+ origin: "http://example.com",
+ persisted: false,
+ usage: 49152,
+ },
+
+ {
+ origin: "http://localhost",
+ persisted: false,
+ usage: 147456,
+ },
+
+ {
+ origin: "http://www.mozilla.org",
+ persisted: true,
+ usage: 98304,
+ },
+ ];
+
+ const allOrigins = [
+ {
+ origin: "chrome",
+ persisted: false,
+ usage: 147456,
+ },
+
+ {
+ origin: "http://example.com",
+ persisted: false,
+ usage: 49152,
+ },
+
+ {
+ origin: "http://localhost",
+ persisted: false,
+ usage: 147456,
+ },
+
+ {
+ origin: "http://www.mozilla.org",
+ persisted: true,
+ usage: 98304,
+ },
+ ];
+
+ function verifyResult(result, expectedOrigins) {
+ ok(result instanceof Array, "Got an array object");
+ ok(result.length == expectedOrigins.length, "Correct number of elements");
+
+ info("Sorting elements");
+
+ result.sort(function(a, b) {
+ let originA = a.origin;
+ let originB = b.origin;
+
+ if (originA < originB) {
+ return -1;
+ }
+ if (originA > originB) {
+ return 1;
+ }
+ return 0;
+ });
+
+ info("Verifying elements");
+
+ for (let i = 0; i < result.length; i++) {
+ let a = result[i];
+ let b = expectedOrigins[i];
+ ok(a.origin == b.origin, "Origin equals");
+ ok(a.persisted == b.persisted, "Persisted equals");
+ ok(a.usage == b.usage, "Usage equals");
+ }
+ }
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Getting usage");
+
+ getUsage(grabResultAndContinueHandler, /* getAll */ true);
+ let result = yield undefined;
+
+ info("Verifying result");
+
+ verifyResult(result, []);
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+
+ // The profile contains IndexedDB databases placed across the repositories.
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_db.js
+ installPackage("getUsage_profile");
+
+ info("Getting usage");
+
+ getUsage(grabResultAndContinueHandler, /* getAll */ false);
+ result = yield undefined;
+
+ info("Verifying result");
+
+ verifyResult(result, origins);
+
+ info("Getting usage");
+
+ getUsage(grabResultAndContinueHandler, /* getAll */ true);
+ result = yield undefined;
+
+ info("Verifying result");
+
+ verifyResult(result, allOrigins);
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_groupMismatch.js b/dom/quota/test/xpcshell/test_groupMismatch.js
new file mode 100644
index 0000000000..41a72d51ad
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_groupMismatch.js
@@ -0,0 +1,74 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that metadata files with old group information
+ * get updated. See bug 1535995.
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+async function testSteps() {
+ const metadataFile = getRelativeFile(
+ "storage/default/https+++foo.bar.mozilla-iot.org/.metadata-v2"
+ );
+
+ async function readMetadataFile() {
+ let file = await File.createFromNsIFile(metadataFile);
+
+ let buffer = await new Promise(resolve => {
+ let reader = new FileReader();
+ reader.onloadend = () => resolve(reader.result);
+ reader.readAsArrayBuffer(file);
+ });
+
+ return buffer;
+ }
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory, a script for origin
+ // initialization and the storage database:
+ // - storage/default/https+++foo.bar.mozilla-iot.org
+ // - create_db.js
+ // - storage.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Manually change the group in .metadata and .metadata-v2 from
+ // "bar.mozilla-iot.org" to "mozilla-iot.org".
+ // 2. Remove the folder "storage/temporary".
+ // 3. Remove the file "storage/ls-archive.sqlite".
+ installPackage("groupMismatch_profile");
+
+ info("Reading out contents of metadata file");
+
+ let metadataBuffer = await readMetadataFile();
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Reading out contents of metadata file");
+
+ let metadataBuffer2 = await readMetadataFile();
+
+ info("Verifying blobs differ");
+
+ ok(!compareBuffers(metadataBuffer, metadataBuffer2), "Metadata differ");
+}
diff --git a/dom/quota/test/xpcshell/test_initTemporaryStorage.js b/dom/quota/test/xpcshell/test_initTemporaryStorage.js
new file mode 100644
index 0000000000..d6a6cfcb5f
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_initTemporaryStorage.js
@@ -0,0 +1,49 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify initTemporaryStorage() does call
+ * QuotaManager::EnsureTemporaryStorageIsInitialized() which does various
+ * things, for example, it restores the directory metadata if it's broken or
+ * missing.
+ */
+
+async function testSteps() {
+ const originDirPath = "storage/default/https+++foo.example.com";
+ const metadataFileName = ".metadata-v2";
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Verifying initialization status");
+
+ await verifyInitializationStatus(true, false);
+
+ info("Creating an empty directory");
+
+ let originDir = getRelativeFile(originDirPath);
+ originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+
+ info("Initializing the temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info(
+ "Verifying directory metadata was restored after calling " +
+ "initTemporaryStorage()"
+ );
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ ok(metadataFile.exists(), "Directory metadata file does exist");
+
+ info("Verifying initialization status");
+
+ await verifyInitializationStatus(true, true);
+}
diff --git a/dom/quota/test/xpcshell/test_listOrigins.js b/dom/quota/test/xpcshell/test_listOrigins.js
new file mode 100644
index 0000000000..223e6a9401
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_listOrigins.js
@@ -0,0 +1,80 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const origins = [
+ "https://example.com",
+ "https://localhost",
+ "https://www.mozilla.org",
+ ];
+
+ function verifyResult(result, expectedOrigins) {
+ ok(result instanceof Array, "Got an array object");
+ ok(result.length == expectedOrigins.length, "Correct number of elements");
+
+ info("Sorting elements");
+
+ result.sort(function(a, b) {
+ if (a < b) {
+ return -1;
+ }
+ if (a > b) {
+ return 1;
+ }
+ return 0;
+ });
+
+ info("Verifying elements");
+
+ for (let i = 0; i < result.length; i++) {
+ ok(result[i] == expectedOrigins[i], "Result matches expected origin");
+ }
+ }
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Listing origins");
+
+ request = listOrigins();
+ await requestFinished(request);
+
+ info("Verifying result");
+
+ verifyResult(request.result, []);
+
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ request = initTemporaryOrigin("default", getPrincipal(origin));
+ await requestFinished(request);
+ }
+
+ info("Listing origins");
+
+ request = listOrigins();
+ await requestFinished(request);
+
+ info("Verifying result");
+
+ verifyResult(request.result, origins);
+}
diff --git a/dom/quota/test/xpcshell/test_originEndsWithDot.js b/dom/quota/test/xpcshell/test_originEndsWithDot.js
new file mode 100644
index 0000000000..8301b3292d
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_originEndsWithDot.js
@@ -0,0 +1,70 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+async function testSteps() {
+ // First, ensure the origin can be initialized and used by a client that uses
+ // SQLite databases.
+
+ // Todo: consider using simpleDB once it supports using storage for SQLite.
+ info("Testing SQLite database with an origin that ends with a dot");
+
+ const principal = getPrincipal("https://example.com.");
+ let request = indexedDB.openForPrincipal(principal, "myIndexedDB");
+ await openDBRequestUpgradeNeeded(request);
+
+ info("Testing simple operations");
+
+ const database = request.result;
+
+ const objectStore = database.createObjectStore("Blobs", {});
+
+ objectStore.add(getNullBlob(200), 42);
+
+ await openDBRequestSucceeded(request);
+
+ database.close();
+
+ info("Reseting");
+
+ request = reset();
+ await requestFinished(request);
+
+ let idbDB = getRelativeFile(
+ "storage/default/https+++example.com./idb/2320029346mByDIdnedxe.sqlite"
+ );
+ ok(idbDB.exists(), "IDB database was created successfully");
+
+ // Second, ensure storage initialization works fine with the origin.
+
+ info("Testing storage initialization and temporary storage initialization");
+
+ request = init();
+ await requestFinished(request);
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ // Third, ensure QMS APIs that touch the client directory for the origin work
+ // fine.
+
+ info("Testing getUsageForPrincipal");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ ok(
+ request.result instanceof Ci.nsIQuotaOriginUsageResult,
+ "The result is nsIQuotaOriginUsageResult instance"
+ );
+ ok(request.result.usage > 0, "Total usage is not empty");
+ ok(request.result.fileUsage > 0, "File usage is not empty");
+
+ info("Testing clearStoragesForPrincipal");
+
+ request = clearOrigin(principal, "default");
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_originMismatch.js b/dom/quota/test/xpcshell/test_originMismatch.js
new file mode 100644
index 0000000000..23186977b2
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_originMismatch.js
@@ -0,0 +1,75 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that temporary storage initialization should
+ * succeed while there is an origin directory that has an inconsistency between
+ * its directory name and the origin name in its directory metadata file.
+ */
+
+async function testSteps() {
+ const packages = ["originMismatch_profile", "defaultStorageDirectory_shared"];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing package");
+
+ // The profile contains:
+ // - storage.sqlite (v2_3)
+ // (A) Verify we are okay while the directory that we want to restore has
+ // already existed.
+ // - storage/default/http+++www.example.com/.metadata-v2
+ // (origin: http://www.example.com.)
+ // - storage/default/http+++www.example.com/cache/.padding
+ // - storage/default/http+++www.example.com./
+ // (B) Verify restoring origin directory succeed.
+ // - storage/default/http+++www.example.org/.metadata-v2
+ // (origin: http://www.example.org.)
+ // - storage/default/http+++www.example.org/cache/.padding
+ //
+ // ToDo: Test case like:
+ // - storage/default/http+++www.example.org(1)/.metadata-v2
+ // (origin: http://www.example.org)
+ // - storage/default/http+++www.example.org/
+ //
+ // - storage/default/http+++www.foo.com/.metadata-v2
+ // (origin: http://www.bar.com)
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Initializing storage");
+
+ request = init();
+ await requestFinished(request);
+
+ // ToDo: Remove this code once we support unknown directories in respository
+ // (bug 1594075).
+ let invalidDir = getRelativeFile("storage/default/invalid+++example.com");
+ invalidDir.remove(true);
+ invalidDir = getRelativeFile("storage/temporary/invalid+++example.com");
+ invalidDir.remove(true);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInitTemporaryStorage");
+
+ request = clear();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_originWithCaret.js b/dom/quota/test/xpcshell/test_originWithCaret.js
new file mode 100644
index 0000000000..7afce4f5cc
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_originWithCaret.js
@@ -0,0 +1,17 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+async function testSteps() {
+ Assert.throws(
+ () => {
+ const principal = getPrincipal("http://example.com^123");
+ getSimpleDatabase(principal);
+ },
+ /NS_ERROR_MALFORMED_URI/,
+ "^ is not allowed in the hostname"
+ );
+}
diff --git a/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js b/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js
new file mode 100644
index 0000000000..6cbca13d8c
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_orpahnedQuotaObject.js
@@ -0,0 +1,44 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const principal = getPrincipal("https://example.com");
+
+ info("Setting pref");
+
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Creating simpledb");
+
+ let database = getSimpleDatabase(principal);
+
+ request = database.open("data");
+ await requestFinished(request);
+
+ info("Creating localStorage");
+
+ let storage = Services.domStorageManager.createStorage(
+ null,
+ principal,
+ principal,
+ ""
+ );
+ storage.setItem("key", "value");
+
+ info("Clearing simpledb");
+
+ request = clearClient(principal, "default", "sdb");
+ await requestFinished(request);
+
+ info("Resetting localStorage");
+
+ request = resetClient(principal, "ls");
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_persist.js b/dom/quota/test/xpcshell/test_persist.js
new file mode 100644
index 0000000000..0920522973
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_persist.js
@@ -0,0 +1,121 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const origin = {
+ url: "http://default.test.persist",
+ path: "storage/default/http+++default.test.persist",
+ persistence: "default",
+ };
+
+ const metadataFileName = ".metadata-v2";
+
+ let principal = getPrincipal(origin.url);
+
+ info("Persisting an uninitialized origin");
+
+ // Origin directory doesn't exist yet, so only check the result for
+ // persisted().
+ let request = persisted(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persisted() succeeded");
+ ok(!request.result, "The origin is not persisted");
+
+ info("Verifying persist() does update the metadata");
+
+ request = persist(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persist() succeeded");
+
+ let originDir = getRelativeFile(origin.path);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ info("Reading out contents of metadata file");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler);
+ let file = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+ yield undefined;
+
+ let originPersisted = getPersistedFromMetadata(fileReader.result);
+ ok(originPersisted, "The origin is persisted");
+
+ info("Verifying persisted()");
+
+ request = persisted(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persisted() succeeded");
+ ok(request.result === originPersisted, "Persisted() concurs with metadata");
+
+ info("Clearing the origin");
+
+ // Clear the origin since we'll test the same directory again under different
+ // circumstances.
+ clearOrigin(principal, origin.persistence, continueToNextStepSync);
+ yield undefined;
+
+ info("Persisting an already initialized origin");
+
+ initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ initTemporaryOrigin(origin.persistence, principal, continueToNextStepSync);
+ yield undefined;
+
+ info("Reading out contents of metadata file");
+
+ fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+ yield undefined;
+
+ originPersisted = getPersistedFromMetadata(fileReader.result);
+ ok(!originPersisted, "The origin isn't persisted after clearing");
+
+ info("Verifying persisted()");
+
+ request = persisted(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persisted() succeeded");
+ ok(request.result === originPersisted, "Persisted() concurs with metadata");
+
+ info("Verifying persist() does update the metadata");
+
+ request = persist(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persist() succeeded");
+
+ info("Reading out contents of metadata file");
+
+ fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+ yield undefined;
+
+ originPersisted = getPersistedFromMetadata(fileReader.result);
+ ok(originPersisted, "The origin is persisted");
+
+ info("Verifying persisted()");
+
+ request = persisted(principal, continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode === NS_OK, "Persisted() succeeded");
+ ok(request.result === originPersisted, "Persisted() concurs with metadata");
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_persist_eviction.js b/dom/quota/test/xpcshell/test_persist_eviction.js
new file mode 100644
index 0000000000..9a62f91b50
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_persist_eviction.js
@@ -0,0 +1,82 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that normally the oldest origin will be
+ * evicted if the global limit is reached, but if the oldest origin is
+ * persisted or is an extension origin, then it won't be evicted.
+ */
+
+loadScript("dom/quota/test/xpcshell/common/utils.js");
+
+async function testSteps() {
+ // The group limit is calculated as 20% of the global limit and the minimum
+ // value of the group limit is 10 MB.
+
+ const groupLimitKB = 10 * 1024;
+ const globalLimitKB = groupLimitKB * 5;
+
+ setGlobalLimit(globalLimitKB);
+
+ let request = clear();
+ await requestFinished(request);
+
+ for (let persistOldestOrigin of [false, true]) {
+ info(
+ "Testing " +
+ (persistOldestOrigin ? "with" : "without") +
+ " persisting the oldest origin"
+ );
+
+ info(
+ "Step 0: Filling a moz-extension origin as the oldest origin with non-persisted data"
+ );
+
+ // Just a fake moz-extension origin to mock an extension origin.
+ let extUUID = "20445ca5-75f9-420e-a1d4-9cccccb5e891";
+ let spec = `moz-extension://${extUUID}`;
+ await fillOrigin(getPrincipal(spec), groupLimitKB * 1024);
+
+ info(
+ "Step 1: Filling five separate web origins to reach the global limit " +
+ "and trigger eviction"
+ );
+
+ for (let index = 1; index <= 5; index++) {
+ let spec = "http://example" + index + ".com";
+ if (index == 1 && persistOldestOrigin) {
+ request = persist(getPrincipal(spec));
+ await requestFinished(request);
+ }
+ await fillOrigin(getPrincipal(spec), groupLimitKB * 1024);
+ }
+
+ info("Step 2: Verifying origin directories");
+
+ for (let index = 1; index <= 5; index++) {
+ let path = "storage/default/http+++example" + index + ".com";
+ let file = getRelativeFile(path);
+ if (index == (persistOldestOrigin ? 2 : 1)) {
+ ok(!file.exists(), "The origin directory " + path + " doesn't exist");
+ } else {
+ ok(file.exists(), "The origin directory " + path + " does exist");
+ }
+ }
+
+ // Verify that the extension storage data has not been evicted (even if it wasn't marked as
+ // persisted and it was the less recently used origin).
+ let path = `storage/default/moz-extension+++${extUUID}`;
+ let file = getRelativeFile(path);
+ ok(file.exists(), "The origin directory " + path + "does exist");
+
+ request = clear();
+ await requestFinished(request);
+ }
+
+ resetGlobalLimit();
+
+ request = reset();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_persist_globalLimit.js b/dom/quota/test/xpcshell/test_persist_globalLimit.js
new file mode 100644
index 0000000000..8b15c26eae
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_persist_globalLimit.js
@@ -0,0 +1,82 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that persisted origins are always bounded by
+ * the global limit.
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+async function testSteps() {
+ const globalLimitKB = 1;
+
+ const principal = getPrincipal("https://persisted.example.com");
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ let request = clear();
+ await requestFinished(request);
+
+ for (let initializeStorageBeforePersist of [false, true]) {
+ if (initializeStorageBeforePersist) {
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing the temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+ }
+
+ info("Persisting an origin");
+
+ request = persist(principal);
+ await requestFinished(request);
+
+ info("Verifying the persisted origin is bounded by global limit");
+
+ let database = getSimpleDatabase(principal);
+
+ info("Opening a database for the persisted origin");
+
+ request = database.open("data");
+ await requestFinished(request);
+
+ try {
+ info("Writing over the limit shouldn't succeed");
+
+ request = database.write(getBuffer(globalLimitKB * 1024 + 1));
+ await requestFinished(request);
+
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ ok(
+ e.resultCode === NS_ERROR_FILE_NO_DEVICE_SPACE,
+ "Threw right result code"
+ );
+ }
+
+ info("Closing the database and clearing");
+
+ request = database.close();
+ await requestFinished(request);
+
+ request = clear();
+ await requestFinished(request);
+ }
+
+ info("Resetting limits");
+
+ resetGlobalLimit();
+
+ request = reset();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_persist_groupLimit.js b/dom/quota/test/xpcshell/test_persist_groupLimit.js
new file mode 100644
index 0000000000..202726299f
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_persist_groupLimit.js
@@ -0,0 +1,104 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that persisted origins are not constrained by
+ * the group limit. It consits of these steps:
+ * - Set the limits as small as our limits allow. This does result in needing
+ * to perform 10 megs of writes which is a lot for a test but not horrible.
+ * - Create databases for 2 origins under the same group.
+ * - Have the foo2 origin use up the shared group quota.
+ * - Verify neither origin can write additional data (via a single byte write).
+ * - Do navigator.storage.persist() for that foo2 origin.
+ * - Verify that both origins can now write an additional byte. This
+ * demonstrates that:
+ * - foo2 no longer counts against the group limit at all since foo1 can
+ * write a byte.
+ * - foo2 is no longer constrained by the group limit itself.
+ */
+async function testSteps() {
+ // The group limit is calculated as 20% of the global limit and the minimum
+ // value of the group limit is 10 MB.
+
+ const groupLimitKB = 10 * 1024;
+ const globalLimitKB = groupLimitKB * 5;
+
+ const urls = ["http://foo1.example.com", "http://foo2.example.com"];
+
+ const foo2Index = 1;
+
+ let index;
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Opening databases");
+
+ let databases = [];
+ for (index = 0; index < urls.length; index++) {
+ let database = getSimpleDatabase(getPrincipal(urls[index]));
+
+ request = database.open("data");
+ await requestFinished(request);
+
+ databases.push(database);
+ }
+
+ info("Filling up the whole group");
+
+ try {
+ request = databases[foo2Index].write(new ArrayBuffer(groupLimitKB * 1024));
+ await requestFinished(request);
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ info("Verifying no more data can be written");
+
+ for (index = 0; index < urls.length; index++) {
+ try {
+ request = databases[index].write(new ArrayBuffer(1));
+ await requestFinished(request);
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ ok(
+ e.resultCode == NS_ERROR_FILE_NO_DEVICE_SPACE,
+ "Threw right result code"
+ );
+ }
+ }
+
+ info("Persisting origin");
+
+ request = persist(getPrincipal(urls[foo2Index]));
+ await requestFinished(request);
+
+ info("Verifying more data data can be written");
+
+ for (index = 0; index < urls.length; index++) {
+ try {
+ request = databases[index].write(new ArrayBuffer(1));
+ await requestFinished(request);
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+ }
+
+ info("Closing databases");
+
+ for (index = 0; index < urls.length; index++) {
+ request = databases[index].close();
+ await requestFinished(request);
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_removeLocalStorage.js b/dom/quota/test/xpcshell/test_removeLocalStorage.js
new file mode 100644
index 0000000000..bae4ad1649
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_removeLocalStorage.js
@@ -0,0 +1,89 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const lsArchiveFile = "storage/ls-archive.sqlite";
+ const lsArchiveTmpFile = "storage/ls-archive-tmp.sqlite";
+ const lsDir = "storage/default/http+++localhost/ls";
+
+ info("Setting pref");
+
+ SpecialPowers.setBoolPref(
+ "dom.storage.enable_unsupported_legacy_implementation",
+ true
+ );
+
+ // Profile 1
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+
+ installPackage("removeLocalStorage1_profile");
+
+ info("Checking ls archive tmp file");
+
+ let archiveTmpFile = getRelativeFile(lsArchiveTmpFile);
+
+ let exists = archiveTmpFile.exists();
+ ok(exists, "ls archive tmp file does exist");
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Checking ls archive file");
+
+ exists = archiveTmpFile.exists();
+ ok(!exists, "ls archive tmp file doesn't exist");
+
+ // Profile 2
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+
+ installPackage("removeLocalStorage2_profile");
+
+ info("Checking ls archive file");
+
+ let archiveFile = getRelativeFile(lsArchiveFile);
+
+ exists = archiveFile.exists();
+ ok(exists, "ls archive file does exist");
+
+ info("Checking ls dir");
+
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(exists, "ls directory does exist");
+
+ info("Initializing");
+
+ request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Checking ls archive file");
+
+ exists = archiveFile.exists();
+ ok(!exists, "ls archive file doesn't exist");
+
+ info("Checking ls dir");
+
+ exists = dir.exists();
+ ok(!exists, "ls directory doesn't exist");
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_simpledb.js b/dom/quota/test/xpcshell/test_simpledb.js
new file mode 100644
index 0000000000..b698d778e0
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_simpledb.js
@@ -0,0 +1,6 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+loadScript("dom/quota/test/common/test_simpledb.js");
diff --git a/dom/quota/test/xpcshell/test_specialOrigins.js b/dom/quota/test/xpcshell/test_specialOrigins.js
new file mode 100644
index 0000000000..d66700d359
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_specialOrigins.js
@@ -0,0 +1,55 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const origins = [
+ {
+ path: "storage/default/file+++UNIVERSAL_FILE_URI_ORIGIN",
+ url: "file:///Test/test.html",
+ persistence: "default",
+ },
+ ];
+
+ info("Setting pref");
+
+ SpecialPowers.setBoolPref("security.fileuri.strict_origin_policy", false);
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Creating origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.path);
+ originDir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origin directories");
+
+ for (let origin of origins) {
+ let result;
+
+ try {
+ request = initTemporaryOrigin(
+ origin.persistence,
+ getPrincipal(origin.url)
+ );
+ result = await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ ok(!result, "Origin directory wasn't created");
+ }
+}
diff --git a/dom/quota/test/xpcshell/test_storagePressure.js b/dom/quota/test/xpcshell/test_storagePressure.js
new file mode 100644
index 0000000000..2e95b2d749
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_storagePressure.js
@@ -0,0 +1,135 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that the storage pressure event is fired when
+ * the eviction process is not able to free some space when a quota client
+ * attempts to write over the global limit or when the global limit is reduced
+ * below the global usage.
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+function awaitStoragePressure() {
+ let promise_resolve;
+
+ let promise = new Promise(function(resolve) {
+ promise_resolve = resolve;
+ });
+
+ function observer(subject, topic) {
+ ok(true, "Got the storage pressure event");
+
+ Services.obs.removeObserver(observer, topic);
+
+ let usage = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ promise_resolve(usage);
+ }
+
+ Services.obs.addObserver(observer, "QuotaManager::StoragePressure");
+
+ return promise;
+}
+
+async function testSteps() {
+ const globalLimitKB = 2;
+
+ const principal = getPrincipal("https://example.com");
+
+ info("Setting limits");
+
+ setGlobalLimit(globalLimitKB);
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Persisting and filling an origin");
+
+ // We need to persist the origin first to omit the group limit checks.
+ // Otherwise, we would have to fill five separate origins.
+ request = persist(principal);
+ await requestFinished(request);
+
+ let database = getSimpleDatabase(principal);
+
+ request = database.open("data");
+ await requestFinished(request);
+
+ try {
+ request = database.write(getBuffer(globalLimitKB * 1024));
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ info("Testing storage pressure by writing over the global limit");
+
+ info("Storing one more byte to get the storage pressure event while writing");
+
+ let promiseStoragePressure = awaitStoragePressure();
+
+ try {
+ request = database.write(getBuffer(1));
+ await requestFinished(request);
+
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ ok(
+ e.resultCode === NS_ERROR_FILE_NO_DEVICE_SPACE,
+ "Threw right result code"
+ );
+ }
+
+ info("Checking the storage pressure event");
+
+ let usage = await promiseStoragePressure;
+ ok(usage == globalLimitKB * 1024, "Got correct usage");
+
+ info("Testing storage pressure by reducing the global limit");
+
+ info(
+ "Reducing the global limit to get the storage pressuse event while the" +
+ " temporary storage is being initialized"
+ );
+
+ setGlobalLimit(globalLimitKB - 1);
+
+ request = reset();
+ await requestFinished(request);
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ promiseStoragePressure = awaitStoragePressure();
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Checking the storage pressure event");
+
+ usage = await promiseStoragePressure;
+ ok(usage == globalLimitKB * 1024, "Got correct usage");
+
+ info("Resetting limits");
+
+ resetGlobalLimit();
+
+ request = reset();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_tempMetadataCleanup.js b/dom/quota/test/xpcshell/test_tempMetadataCleanup.js
new file mode 100644
index 0000000000..981e1a14d3
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_tempMetadataCleanup.js
@@ -0,0 +1,45 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const tempMetadataFiles = [
+ "storage/permanent/chrome/.metadata-tmp",
+ "storage/permanent/chrome/.metadata-v2-tmp",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Installing package");
+
+ installPackage("tempMetadataCleanup_profile");
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ info("Initializing origin");
+
+ request = initPersistentOrigin(getCurrentPrincipal(), continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+
+ for (let tempMetadataFile of tempMetadataFiles) {
+ info("Checking temp metadata file");
+
+ let file = getRelativeFile(tempMetadataFile);
+
+ let exists = file.exists();
+ ok(!exists, "Temp metadata file doesn't exist");
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/test_unaccessedOrigins.js b/dom/quota/test/xpcshell/test_unaccessedOrigins.js
new file mode 100644
index 0000000000..93b38501f4
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_unaccessedOrigins.js
@@ -0,0 +1,168 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const SEC_PER_MONTH = 60 * 60 * 24 * 30;
+
+async function testSteps() {
+ function getHostname(index) {
+ return "www.example" + index + ".com";
+ }
+
+ function getOrigin(index) {
+ return "https://" + getHostname(index);
+ }
+
+ function getOriginDir(index) {
+ return getRelativeFile("storage/default/https+++" + getHostname(index));
+ }
+
+ function updateOriginLastAccessTime(index, deltaSec) {
+ let originDir = getOriginDir(index);
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(".metadata-v2");
+
+ let fileRandomAccessStream = Cc[
+ "@mozilla.org/network/file-random-access-stream;1"
+ ].createInstance(Ci.nsIFileRandomAccessStream);
+ fileRandomAccessStream.init(metadataFile, -1, -1, 0);
+
+ let binaryInputStream = Cc[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Ci.nsIBinaryInputStream);
+ binaryInputStream.setInputStream(fileRandomAccessStream);
+
+ let lastAccessTime = binaryInputStream.read64();
+
+ let seekableStream = fileRandomAccessStream.QueryInterface(
+ Ci.nsISeekableStream
+ );
+ seekableStream.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
+
+ binaryOutputStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ binaryOutputStream.setOutputStream(fileRandomAccessStream);
+
+ binaryOutputStream.write64(lastAccessTime + deltaSec * PR_USEC_PER_SEC);
+
+ binaryOutputStream.close();
+
+ binaryInputStream.close();
+ }
+
+ function verifyOriginDir(index, shouldExist) {
+ let originDir = getOriginDir(index);
+ let exists = originDir.exists();
+ if (shouldExist) {
+ ok(exists, "Origin directory does exist");
+ } else {
+ ok(!exists, "Origin directory doesn't exist");
+ }
+ }
+
+ info("Setting prefs");
+
+ Services.prefs.setBoolPref("dom.quotaManager.loadQuotaFromCache", false);
+ Services.prefs.setBoolPref("dom.quotaManager.checkQuotaInfoLoadTime", true);
+ Services.prefs.setIntPref(
+ "dom.quotaManager.longQuotaInfoLoadTimeThresholdMs",
+ 0
+ );
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origins");
+
+ for (let index = 0; index < 30; index++) {
+ request = initTemporaryOrigin("default", getPrincipal(getOrigin(index)));
+ await requestFinished(request);
+ }
+
+ info("Updating last access time of selected origins");
+
+ for (let index = 0; index < 10; index++) {
+ updateOriginLastAccessTime(index, -14 * SEC_PER_MONTH);
+ }
+
+ for (let index = 10; index < 20; index++) {
+ updateOriginLastAccessTime(index, -7 * SEC_PER_MONTH);
+ }
+
+ info("Resetting");
+
+ request = reset();
+ await requestFinished(request);
+
+ info("Setting pref");
+
+ Services.prefs.setIntPref(
+ "dom.quotaManager.unaccessedForLongTimeThresholdSec",
+ 13 * SEC_PER_MONTH
+ );
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Verifying origin directories");
+
+ for (let index = 0; index < 10; index++) {
+ verifyOriginDir(index, false);
+ }
+ for (let index = 10; index < 30; index++) {
+ verifyOriginDir(index, true);
+ }
+
+ info("Resetting");
+
+ request = reset();
+ await requestFinished(request);
+
+ info("Setting pref");
+
+ Services.prefs.setIntPref(
+ "dom.quotaManager.unaccessedForLongTimeThresholdSec",
+ 6 * SEC_PER_MONTH
+ );
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Verifying origin directories");
+
+ for (let index = 0; index < 20; index++) {
+ verifyOriginDir(index, false);
+ }
+ for (let index = 20; index < 30; index++) {
+ verifyOriginDir(index, true);
+ }
+
+ info("Resetting");
+
+ request = reset();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/test_unknownFiles.js b/dom/quota/test/xpcshell/test_unknownFiles.js
new file mode 100644
index 0000000000..9ddfbed56b
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_unknownFiles.js
@@ -0,0 +1,106 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that init, initTemporaryStorage,
+ * getUsageForPrincipal and clearStoragesForPrincipal are able to ignore
+ * unknown files and directories in the storage/default directory and its
+ * subdirectories.
+ */
+async function testSteps() {
+ const principal = getPrincipal("http://example.com");
+
+ async function testFunctionality(testFunction) {
+ const modes = [
+ {
+ initializedStorage: false,
+ initializedTemporaryStorage: false,
+ },
+ {
+ initializedStorage: true,
+ initializedTemporaryStorage: false,
+ },
+ {
+ initializedStorage: true,
+ initializedTemporaryStorage: true,
+ },
+ ];
+
+ for (const mode of modes) {
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains unknown files and unknown directories placed
+ // across the repositories, origin directories and client directories.
+ // The file make_unknownFiles.js was run locally, specifically it was
+ // temporarily enabled in xpcshell.ini and then executed:
+ // mach test --interactive dom/quota/test/xpcshell/make_unknownFiles.js
+ installPackage("unknownFiles_profile");
+
+ if (mode.initializedStorage) {
+ info("Initializing storage");
+
+ request = init();
+ await requestFinished(request);
+ }
+
+ if (mode.initializedTemporaryStorage) {
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+ }
+
+ info("Verifying initialization status");
+
+ await verifyInitializationStatus(
+ mode.initializedStorage,
+ mode.initializedTemporaryStorage
+ );
+
+ await testFunction(
+ mode.initializedStorage,
+ mode.initializedTemporaryStorage
+ );
+
+ info("Clearing");
+
+ request = clear();
+ await requestFinished(request);
+ }
+ }
+
+ // init and initTemporaryStorage functionality is tested in the
+ // testFunctionality function as part of the multi mode testing
+
+ info("Testing getUsageForPrincipal functionality");
+
+ await testFunctionality(async function() {
+ info("Getting origin usage");
+
+ request = getOriginUsage(principal);
+ await requestFinished(request);
+
+ ok(
+ request.result instanceof Ci.nsIQuotaOriginUsageResult,
+ "The result is nsIQuotaOriginUsageResult instance"
+ );
+ is(request.result.usage, 115025, "Correct total usage");
+ is(request.result.fileUsage, 200, "Correct file usage");
+ });
+
+ info("Testing clearStoragesForPrincipal functionality");
+
+ await testFunctionality(async function() {
+ info("Clearing origin");
+
+ request = clearOrigin(principal, "default");
+ await requestFinished(request);
+ });
+}
diff --git a/dom/quota/test/xpcshell/test_unsetLastAccessTime.js b/dom/quota/test/xpcshell/test_unsetLastAccessTime.js
new file mode 100644
index 0000000000..96155af397
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_unsetLastAccessTime.js
@@ -0,0 +1,68 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+async function testSteps() {
+ const metadataFile = getRelativeFile(
+ "storage/default/https+++foo.example.com/.metadata-v2"
+ );
+
+ function getLastAccessTime() {
+ let fileInputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+
+ fileInputStream.init(metadataFile, -1, -1, 0);
+
+ let binaryInputStream = Cc[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Ci.nsIBinaryInputStream);
+
+ binaryInputStream.setInputStream(fileInputStream);
+
+ let lastAccessTime = BigInt.asIntN(64, BigInt(binaryInputStream.read64()));
+
+ binaryInputStream.close();
+
+ return lastAccessTime;
+ }
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains one initialized origin directory and the storage
+ // database:
+ // - storage/default/https+++foo.example.com
+ // - storage.sqlite
+ // The file make_unsetLastAccessTime.js was run locally, specifically it was
+ // temporarily enabled in xpcshell.ini and then executed:
+ // mach test --interactive dom/quota/test/xpcshell/make_unsetLastAccessTime.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ // 2. Remove the file "storage/ls-archive.sqlite".
+ installPackage("unsetLastAccessTime_profile");
+
+ info("Verifying last access time");
+
+ ok(getLastAccessTime() == INT64_MIN, "Correct last access time");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Verifying last access time");
+
+ ok(getLastAccessTime() != INT64_MIN, "Correct last access time");
+}
diff --git a/dom/quota/test/xpcshell/test_validOrigins.js b/dom/quota/test/xpcshell/test_validOrigins.js
new file mode 100644
index 0000000000..4c758d6bf5
--- /dev/null
+++ b/dom/quota/test/xpcshell/test_validOrigins.js
@@ -0,0 +1,99 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Use initOrigin to test the operation of the origin parser on a list of URLs
+// we should support. If the origin doesn't parse, then initOrigin will throw an
+// exception (and potentially MOZ_ASSERT under debug builds). Handling of
+// obsolete or invalid origins is handled in other test files.
+async function testSteps() {
+ const basePath = "storage/default/";
+ const longExampleOriginSubstring = "a".repeat(
+ 255 - "https://example..com".length
+ );
+ const origins = [
+ // General
+ {
+ dirName: "https+++example.com",
+ url: "https://example.com",
+ },
+ {
+ dirName: "https+++smaug----.github.io",
+ url: "https://smaug----.github.io/",
+ },
+ // About
+ {
+ dirName: "about+home",
+ url: "about:home",
+ },
+ {
+ dirName: "about+reader",
+ url: "about:reader",
+ },
+ // IPv6
+ {
+ dirName: "https+++[++]",
+ url: "https://[::]",
+ },
+ {
+ dirName: "https+++[ffff+ffff+ffff+ffff+ffff+ffff+ffff+ffff]",
+ url: "https://[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff]",
+ },
+ {
+ dirName: "http+++[2010+836b+4179++836b+4179]",
+ url: "http://[2010:836B:4179::836B:4179]:80",
+ },
+ {
+ dirName: "https+++[++ffff+8190+3426]",
+ url: "https://[::FFFF:129.144.52.38]",
+ },
+ // MAX_PATH on Windows (260); storage/default/https+++example.{a....a}.com
+ // should have already exceeded the MAX_PATH limitation on Windows.
+ // There is a limitation (255) for each component on Windows so that we can
+ // only let the component be 255 chars and expect the wwhole path to be
+ // greater then 260.
+ {
+ dirName: `https+++example.${longExampleOriginSubstring}.com`,
+ url: `https://example.${longExampleOriginSubstring}.com`,
+ },
+ // EndingWithPeriod
+ {
+ dirName: "https+++example.com.",
+ url: "https://example.com.",
+ },
+ ];
+
+ info("Initializing");
+
+ let request = init();
+ await requestFinished(request);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ for (let origin of origins) {
+ info(`Testing ${origin.url}`);
+
+ try {
+ request = initTemporaryOrigin("default", getPrincipal(origin.url));
+ await requestFinished(request);
+
+ ok(true, "Should not have thrown");
+ } catch (ex) {
+ ok(false, "Should not have thrown");
+ }
+
+ let dir = getRelativeFile(basePath + origin.dirName);
+ ok(dir.exists(), "Origin was created");
+ ok(
+ origin.dirName === dir.leafName,
+ `Origin ${origin.dirName} was created expectedly`
+ );
+ }
+
+ request = clear();
+ await requestFinished(request);
+}
diff --git a/dom/quota/test/xpcshell/unknownFiles_profile.zip b/dom/quota/test/xpcshell/unknownFiles_profile.zip
new file mode 100644
index 0000000000..96d636780d
--- /dev/null
+++ b/dom/quota/test/xpcshell/unknownFiles_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip b/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip
new file mode 100644
index 0000000000..2b14ca7276
--- /dev/null
+++ b/dom/quota/test/xpcshell/unsetLastAccessTime_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json
new file mode 100644
index 0000000000..eff5918e98
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.json
@@ -0,0 +1,64 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "https+++www.mozilla.org^userContextId=1",
+ "dir": true,
+ "entries": [
+ {
+ "name": "sdb",
+ "dir": true,
+ "entries": [{ "name": "data.sdb", "dir": false }]
+ },
+ { "name": ".metadata-v2", "dir":false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "https+++www.mozilla.org^userContextId=1",
+ "dir": true,
+ "entries": [
+ {
+ "name": "sdb",
+ "dir": true,
+ "entries": [{ "name": "data.sdb", "dir": false }]
+ },
+ { "name": ".metadata-v2", "dir":false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip
new file mode 100644
index 0000000000..c09b503c18
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/cacheVersion1_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/head.js b/dom/quota/test/xpcshell/upgrades/head.js
new file mode 100644
index 0000000000..5c36d82ca6
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/head.js
@@ -0,0 +1,14 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// The path to the top level directory.
+const depth = "../../../../../";
+
+loadScript("dom/quota/test/xpcshell/common/head.js");
+
+function loadScript(path) {
+ let uri = Services.io.newFileURI(do_get_file(depth + path));
+ Services.scriptloader.loadSubScript(uri.spec);
+}
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json
new file mode 100644
index 0000000000..db66d824eb
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.json
@@ -0,0 +1,63 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "indexedDB",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ },
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip
new file mode 100644
index 0000000000..63936ecf9a
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBAndPersistentStorageDirectory_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json
new file mode 100644
index 0000000000..7916c25b73
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.json
@@ -0,0 +1,55 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "indexedDB",
+ "dir": true,
+ "entries": [
+ { "name": "1007+f+app+++system.gaiamobile.org", "dir": true },
+ { "name": "http+++www.mozilla.org", "dir": true },
+ { "name": "1007+t+https+++developer.cdn.mozilla.net", "dir": true }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip
new file mode 100644
index 0000000000..a0a56a77df
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_flatOriginDirectories_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json
new file mode 100644
index 0000000000..715c954915
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.json
@@ -0,0 +1,65 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "indexedDB",
+ "dir": true,
+ "entries": [
+ {
+ "name": "1007+f+app+++system.gaiamobile.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip
new file mode 100644
index 0000000000..589e65ec82
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/indexedDBDirectory_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip
new file mode 100644
index 0000000000..a17b90dedf
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/localStorageArchive1upgrade_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip
new file mode 100644
index 0000000000..cf5b29adae
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/localStorageArchive4upgrade_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip b/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip
new file mode 100644
index 0000000000..2f11abe858
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/localStorageArchiveDowngrade_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json
new file mode 100644
index 0000000000..a25f257573
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.json
@@ -0,0 +1,63 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ },
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip
new file mode 100644
index 0000000000..9ddd9af6a9
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentAndDefaultStorageDirectory_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json
new file mode 100644
index 0000000000..c80a3cc283
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.json
@@ -0,0 +1,64 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ { "name": "1007+f+app+++system.gaiamobile.org", "dir": true },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true
+ },
+ { "name": "http+++www.mozilla.org", "dir": true }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip
new file mode 100644
index 0000000000..436ebf9070
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_flatOriginDirectories_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json
new file mode 100644
index 0000000000..98d23af161
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.json
@@ -0,0 +1,92 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "1007+f+app+++system.gaiamobile.org",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false }
+ ]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false }
+ ]
+ },
+ { "name": "http+++www.mozilla.org+8080", "dir": true }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org+8080",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip
new file mode 100644
index 0000000000..0c5200adf4
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_originDirectories_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json
new file mode 100644
index 0000000000..f5d6a4a749
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.json
@@ -0,0 +1,382 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "persistent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "1007+f+app+++system.gaiamobile.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++Users+joe+c+++index.html",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "moz-safe-about+home",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "https+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++Users+joe+index.html",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++localhost",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++c++",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++c++Users+joe+index.html",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++c++Users+joe+",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++127.0.0.1",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++++index.html",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++www.mozilla.org+8080",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "moz-safe-about+++home",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "indexeddb+++fx-devtools",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "https+++www.mozilla.org+8080",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "file++++Users+joe+",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ {
+ "name": "1007+f+app+++system.gaiamobile.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++localhost",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++localhost+82",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ { "name": "http+++localhost+81", "dir": true }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "file++++Users+joe+c+++index.html",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "https+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++Users+joe+index.html",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++localhost",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++c++",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++c++Users+joe+index.html",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++c++Users+joe+",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++127.0.0.1",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++++index.html",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org+8080",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "https+++www.mozilla.org+8080",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "file++++Users+joe+",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++localhost",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++localhost+82",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++localhost+81",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-safe-about+home",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "indexeddb+++fx-devtools",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip
new file mode 100644
index 0000000000..ab8c045be9
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/persistentStorageDirectory_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js
new file mode 100644
index 0000000000..f697d9167e
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive1upgrade.js
@@ -0,0 +1,65 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that local storage directories are removed
+ * during local storage archive upgrade from version 0 to version 1.
+ * See bug 1546305.
+ */
+
+async function testSteps() {
+ const lsDirs = [
+ "storage/default/http+++example.com/ls",
+ "storage/default/http+++localhost/ls",
+ "storage/default/http+++www.mozilla.org/ls",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains three initialized origin directories with local
+ // storage data, local storage archive, a script for origin initialization,
+ // the storage database and the web apps store database:
+ // - storage/default/https+++example.com
+ // - storage/default/https+++localhost
+ // - storage/default/https+++www.mozilla.org
+ // - storage/ls-archive.sqlite
+ // - create_db.js
+ // - storage.sqlite
+ // - webappsstore.sqlite
+ // The file create_db.js in the package was run locally (with a build that
+ // doesn't support local storage archive upgrades), specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ installPackage("localStorageArchive1upgrade_profile");
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(exists, "ls directory does exist");
+ }
+
+ request = init();
+ request = await requestFinished(request);
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(!exists, "ls directory doesn't exist");
+ }
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js
new file mode 100644
index 0000000000..0b0fcfccd6
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchive4upgrade.js
@@ -0,0 +1,107 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that local storage directories are removed
+ * during local storage archive upgrade from version 3 to version 4.
+ * See bug 1549654.
+ */
+
+async function testSteps() {
+ const lsDirs = [
+ "storage/default/http+++localhost/ls",
+ "storage/default/http+++www.mozilla.org/ls",
+ "storage/default/http+++example.com/ls",
+ ];
+
+ const principalInfos = [
+ "http://localhost",
+ "http://www.mozilla.org",
+ "http://example.com",
+ ];
+
+ const data = [
+ { key: "foo0", value: "bar" },
+ { key: "foo1", value: "A" },
+ { key: "foo2", value: "A".repeat(100) },
+ ];
+
+ function getLocalStorage(principal) {
+ return Services.domStorageManager.createStorage(
+ null,
+ principal,
+ principal,
+ ""
+ );
+ }
+
+ info("Setting pref");
+
+ // xpcshell globals don't have associated clients in the Clients API sense, so
+ // we need to disable client validation so that this xpcshell test is allowed
+ // to use LocalStorage.
+ Services.prefs.setBoolPref("dom.storage.client_validation", false);
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains three initialized origin directories with local
+ // storage data, local storage archive, a script for origin initialization,
+ // the storage database and the web apps store database:
+ // - storage/default/https+++example.com
+ // - storage/default/https+++localhost
+ // - storage/default/https+++www.mozilla.org
+ // - storage/ls-archive.sqlite
+ // - create_db.js
+ // - storage.sqlite
+ // - webappsstore.sqlite
+ // The file create_db.js in the package was run locally (with a build with
+ // local storage archive version 3), specifically it was temporarily added to
+ // xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ installPackage("localStorageArchive4upgrade_profile");
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(exists, "ls directory does exist");
+ }
+
+ request = init();
+ request = await requestFinished(request);
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(!exists, "ls directory doesn't exist");
+ }
+
+ info("Getting storages");
+
+ let storages = [];
+ for (let i = 0; i < principalInfos.length; i++) {
+ let storage = getLocalStorage(getPrincipal(principalInfos[i]));
+ storages.push(storage);
+ }
+
+ info("Verifying data");
+
+ for (let i = 0; i < storages.length; i++) {
+ is(storages[i].getItem(data[i].key), data[i].value, "Correct value");
+ }
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js b/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js
new file mode 100644
index 0000000000..8ca46f01d5
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_localStorageArchiveDowngrade.js
@@ -0,0 +1,66 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that local storage directories are removed
+ * during local storage archive downgrade from any future version to current
+ * version. See bug 1546305.
+ */
+
+async function testSteps() {
+ const lsDirs = [
+ "storage/default/http+++example.com/ls",
+ "storage/default/http+++localhost/ls",
+ "storage/default/http+++www.mozilla.org/ls",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Installing package");
+
+ // The profile contains three initialized origin directories with local
+ // storage data, local storage archive, a script for origin initialization,
+ // the storage database and the web apps store database:
+ // - storage/default/https+++example.com
+ // - storage/default/https+++localhost
+ // - storage/default/https+++www.mozilla.org
+ // - storage/ls-archive.sqlite
+ // - create_db.js
+ // - storage.sqlite
+ // - webappsstore.sqlite
+ // The file create_db.js in the package was run locally (with a build that
+ // supports local storage archive upgrades and local storage archive version
+ // set to max integer), specifically it was temporarily added to xpcshell.ini
+ // and then executed:
+ // mach xpcshell-test --interactive dom/localstorage/test/xpcshell/create_db.js
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ installPackage("localStorageArchiveDowngrade_profile");
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(exists, "ls directory does exist");
+ }
+
+ request = init();
+ request = await requestFinished(request);
+
+ info("Checking ls dirs");
+
+ for (let lsDir of lsDirs) {
+ let dir = getRelativeFile(lsDir);
+
+ exists = dir.exists();
+ ok(!exists, "ls directory doesn't exist");
+ }
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js b/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js
new file mode 100644
index 0000000000..e9424e20c6
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeCacheFrom1.js
@@ -0,0 +1,79 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify UpgradeCacheFrom1To2 method.
+ */
+
+async function testSteps() {
+ const packages = [
+ // Storage used prior FF 88 (cache version 1).
+ // The profile contains one initialized origin directory with simple
+ // database data, a script for origin initialization and the storage
+ // database:
+ // - storage/default/https+++www.mozilla.org^userContextId=1
+ // - create_db.js
+ // - storage.sqlite
+ // The file create_db.js in the package was run locally, specifically it was
+ // temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test dom/quota/test/xpcshell/upgrades/create_db.js
+ // --interactive
+ // Note: to make it become the profile in the test, additional manual steps
+ // are needed.
+ // 1. Remove the folder "storage/temporary".
+ // 2. Remove the file "storage/ls-archive.sqlite".
+ "cacheVersion1_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+ const principal = getPrincipal("https://www.mozilla.org", {
+ userContextId: 1,
+ });
+ const originUsage = 100;
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing package");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ await requestFinished(request);
+
+ info("Getting origin usage");
+
+ request = getOriginUsage(principal, /* fromMemory */ true);
+ await requestFinished(request);
+
+ info("Verifying origin usage");
+
+ is(request.result.usage, originUsage, "Correct origin usage");
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js
new file mode 100644
index 0000000000..f33ddac8ca
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromFlatOriginDirectories.js
@@ -0,0 +1,187 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// A flat origin directory is an origin directory with no sub directories for
+// quota clients. The upgrade was initially done lazily and an empty .metadata
+// file was used to indicate a successful upgrade.
+
+function* testSteps() {
+ const setups = [
+ {
+ packages: [
+ // Storage used prior FF 22 (indexedDB/ directory with flat origin
+ // directories).
+ // FF 26 renamed indexedDB/ to storage/persistent and the lazy upgrade
+ // of flat origin directories remained. There's a test for that below.
+ "indexedDBDirectory_flatOriginDirectories_profile",
+ "../indexedDBDirectory_shared",
+ ],
+ origins: [
+ {
+ oldPath: "indexedDB/1007+f+app+++system.gaiamobile.org",
+ },
+
+ {
+ oldPath: "indexedDB/1007+t+https+++developer.cdn.mozilla.net",
+ },
+
+ {
+ oldPath: "indexedDB/http+++www.mozilla.org",
+ newPath: "storage/default/http+++www.mozilla.org",
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ },
+ ],
+ },
+
+ {
+ packages: [
+ // Storage used by FF 26-35 (storage/persistent/ directory with not yet
+ // upgraded flat origin directories).
+ // FF 36 renamed storage/persistent/ to storage/default/ and all not
+ // yet upgraded flat origin directories were upgraded. There's a
+ // separate test for that:
+ // test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.
+ "persistentStorageDirectory_flatOriginDirectories_profile",
+ "../persistentStorageDirectory_shared",
+ ],
+ origins: [
+ {
+ oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org",
+ },
+
+ {
+ oldPath:
+ "storage/persistent/1007+t+https+++developer.cdn.mozilla.net",
+ },
+
+ {
+ oldPath: "storage/persistent/http+++www.mozilla.org",
+ newPath: "storage/default/http+++www.mozilla.org",
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ },
+ ],
+ },
+ ];
+
+ const metadataFileName = ".metadata";
+
+ for (const setup of setups) {
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(setup.packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(setup.packages);
+
+ info("Verifying storage");
+
+ verifyStorage(setup.packages, "afterInstall");
+
+ info("Checking origin directories");
+
+ for (const origin of setup.origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ let idbDir = originDir.clone();
+ idbDir.append("idb");
+
+ exists = idbDir.exists();
+ ok(!exists, "idb directory doesn't exist");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ ok(!exists, "Metadata file doesn't exist");
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(setup.packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ try {
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+ } catch (ex) {}
+
+ info("Checking origin directories");
+
+ for (const origin of setup.origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ let idbDir = originDir.clone();
+ idbDir.append("idb");
+
+ exists = idbDir.exists();
+ ok(exists, "idb directory does exist");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ ok(exists, "Metadata file does exist");
+ }
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of setup.origins) {
+ if (origin.newPath) {
+ info("Initializing origin");
+
+ let principal = getPrincipal(origin.url);
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+ }
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js
new file mode 100644
index 0000000000..b1b0966d67
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory.js
@@ -0,0 +1,121 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify
+ * MaybeUpgradeFromIndexedDBDirectoryToPersistentStorageDirectory method.
+ */
+
+function* testSteps() {
+ const origins = [
+ {
+ oldPath: "indexedDB/http+++www.mozilla.org",
+ newPath: "storage/default/http+++www.mozilla.org",
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "indexedDB/1007+f+app+++system.gaiamobile.org",
+ },
+
+ {
+ oldPath: "indexedDB/1007+t+https+++developer.cdn.mozilla.net",
+ },
+ ];
+
+ const packages = [
+ // Storage used prior FF 26 (indexedDB/ directory).
+ "indexedDBDirectory_profile",
+ "../indexedDBDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing package");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+ }
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ if (origin.newPath) {
+ info("Initializing origin");
+
+ let principal = getPrincipal(origin.url);
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js
new file mode 100644
index 0000000000..a7beced885
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromIndexedDBDirectory_removeOldDirectory.js
@@ -0,0 +1,86 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that the old directory is removed in
+ * MaybeUpgradeFromIndexedDBDirectoryToPersistentStorageDirectory method.
+ */
+
+async function testSteps() {
+ const url = "http://www.mozilla.org";
+ const persistence = "default";
+
+ const packages = [
+ // Storage used by FF 26-35 (storage/persistent/ directory and re-created
+ // indexedDB directory by an older FF).
+ "indexedDBAndPersistentStorageDirectory_profile",
+ "../persistentStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking directories");
+
+ let indexedDBDir = getRelativeFile("indexedDB");
+ let exists = indexedDBDir.exists();
+ ok(exists, "IndexedDB directory does exist");
+
+ let persistentStorageDir = getRelativeFile("storage/persistent");
+ exists = persistentStorageDir.exists();
+ ok(exists, "Persistent storage directory does exist");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking directories");
+
+ indexedDBDir = getRelativeFile("indexedDB");
+ exists = indexedDBDir.exists();
+ ok(!exists, "IndexedDB directory doesn't exist");
+
+ // FF 36 renamed storage/persistent/ to storage/default/ so it can't exist
+ // either.
+ persistentStorageDir = getRelativeFile("storage/persistent");
+ exists = persistentStorageDir.exists();
+ ok(!exists, "Persistent storage directory doesn't exist");
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origin");
+
+ request = initTemporaryOrigin(persistence, getPrincipal(url));
+ await requestFinished(request);
+
+ ok(!request.result, "Origin directory wasn't created");
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js
new file mode 100644
index 0000000000..efac150067
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory.js
@@ -0,0 +1,378 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify
+ * MaybeUpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory method.
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+function* testSteps() {
+ const origins = [
+ {
+ oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org",
+ },
+
+ {
+ oldPath: "storage/persistent/1007+t+https+++developer.cdn.mozilla.net",
+ },
+
+ {
+ oldPath: "storage/persistent/chrome",
+ newPath: "storage/permanent/chrome",
+ chrome: true,
+ persistence: "persistent",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++",
+ newPath: "storage/default/file++++",
+ url: "file:///",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++++index.html",
+ newPath: "storage/default/file++++++index.html",
+ url: "file:///+/index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++++index.html",
+ newPath: "storage/default/file++++++index.html",
+ url: "file://///index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++Users+joe+",
+ newPath: "storage/default/file++++Users+joe+",
+ url: "file:///Users/joe/",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++Users+joe+c+++index.html",
+ newPath: "storage/default/file++++Users+joe+c+++index.html",
+ url: "file:///Users/joe/c++/index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++Users+joe+c+++index.html",
+ newPath: "storage/default/file++++Users+joe+c+++index.html",
+ url: "file:///Users/joe/c///index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++Users+joe+index.html",
+ newPath: "storage/default/file++++Users+joe+index.html",
+ url: "file:///Users/joe/index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++c++",
+ newPath: "storage/default/file++++c++",
+ url: "file:///c:/",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++c++Users+joe+",
+ newPath: "storage/default/file++++c++Users+joe+",
+ url: "file:///c:/Users/joe/",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/file++++c++Users+joe+index.html",
+ newPath: "storage/default/file++++c++Users+joe+index.html",
+ url: "file:///c:/Users/joe/index.html",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/http+++127.0.0.1",
+ newPath: "storage/default/http+++127.0.0.1",
+ url: "http://127.0.0.1",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/http+++localhost",
+ newPath: "storage/default/http+++localhost",
+ url: "http://localhost",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/http+++www.mozilla.org",
+ newPath: "storage/default/http+++www.mozilla.org",
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/http+++www.mozilla.org+8080",
+ newPath: "storage/default/http+++www.mozilla.org+8080",
+ url: "http://www.mozilla.org:8080",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/https+++www.mozilla.org",
+ newPath: "storage/default/https+++www.mozilla.org",
+ url: "https://www.mozilla.org",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/https+++www.mozilla.org+8080",
+ newPath: "storage/default/https+++www.mozilla.org+8080",
+ url: "https://www.mozilla.org:8080",
+ persistence: "default",
+ },
+
+ {
+ oldPath: "storage/persistent/indexeddb+++fx-devtools",
+ newPath: "storage/permanent/indexeddb+++fx-devtools",
+ url: "indexeddb://fx-devtools",
+ persistence: "persistent",
+ },
+
+ {
+ oldPath: "storage/persistent/moz-safe-about+++home",
+ },
+
+ {
+ oldPath: "storage/persistent/moz-safe-about+home",
+ newPath: "storage/permanent/moz-safe-about+home",
+ url: "moz-safe-about:home",
+ persistence: "persistent",
+ },
+
+ {
+ oldPath:
+ "storage/persistent/resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data",
+ newPath:
+ "storage/permanent/resource+++fx-share-addon-at-mozilla-dot-org-fx-share-addon-data",
+ url: "resource://fx-share-addon-at-mozilla-dot-org-fx-share-addon-data",
+ persistence: "persistent",
+ },
+
+ {
+ oldPath: "storage/temporary/1007+f+app+++system.gaiamobile.org",
+ },
+
+ {
+ oldPath: "storage/temporary/1007+t+https+++developer.cdn.mozilla.net",
+ },
+
+ // The .metadata file was intentionally appended for this origin directory
+ // to test recovery from unfinished upgrades (some metadata files can be
+ // already upgraded).
+ {
+ oldPath: "storage/temporary/chrome",
+ newPath: "storage/temporary/chrome",
+ metadataUpgraded: true,
+ chrome: true,
+ persistence: "temporary",
+ },
+
+ {
+ oldPath: "storage/temporary/http+++localhost",
+ newPath: "storage/temporary/http+++localhost",
+ url: "http://localhost",
+ persistence: "temporary",
+ },
+
+ // The .metadata file was intentionally removed for this origin directory
+ // to test restoring during upgrade.
+ {
+ oldPath: "storage/temporary/http+++localhost+81",
+ newPath: "storage/temporary/http+++localhost+81",
+ metadataRemoved: true,
+ url: "http://localhost:81",
+ persistence: "temporary",
+ },
+
+ // The .metadata file was intentionally truncated for this origin directory
+ // to test restoring during upgrade.
+ {
+ oldPath: "storage/temporary/http+++localhost+82",
+ newPath: "storage/temporary/http+++localhost+82",
+ url: "http://localhost:82",
+ persistence: "temporary",
+ },
+ ];
+
+ const metadataFileName = ".metadata";
+
+ const packages = [
+ // Storage used by 26-35 (storage/persistent/ directory, tracked only
+ // timestamp in .metadata for persistent storage and isApp not tracked in
+ // .metadata for temporary storage).
+ "persistentStorageDirectory_profile",
+ "../persistentStorageDirectory_shared",
+ ];
+
+ let metadataBuffers = [];
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ if (origin.newPath) {
+ info("Reading out contents of metadata file");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ if (origin.metadataRemoved) {
+ metadataBuffers.push(new ArrayBuffer(0));
+ } else {
+ File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler);
+ let file = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+
+ yield undefined;
+
+ metadataBuffers.push(fileReader.result);
+ }
+
+ if (origin.newPath != origin.oldPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+ }
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ if (!origin.newPath || origin.newPath != origin.oldPath) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+
+ if (origin.newPath) {
+ let originDir = getRelativeFile(origin.newPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ info("Reading out contents of metadata file");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler);
+ let file = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+
+ yield undefined;
+
+ let metadataBuffer = fileReader.result;
+
+ info("Verifying blobs differ");
+
+ if (origin.metadataUpgraded) {
+ ok(
+ compareBuffers(metadataBuffer, metadataBuffers.shift()),
+ "Metadata doesn't differ"
+ );
+ } else {
+ ok(
+ !compareBuffers(metadataBuffer, metadataBuffers.shift()),
+ "Metadata differ"
+ );
+ }
+ }
+ }
+
+ info("Initializing");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ if (origin.newPath) {
+ info("Initializing origin");
+
+ let principal;
+ if (origin.chrome) {
+ principal = getCurrentPrincipal();
+ } else {
+ principal = getPrincipal(origin.url);
+ }
+
+ if (origin.persistence == "persistent") {
+ request = initPersistentOrigin(principal, continueToNextStepSync);
+ } else {
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ }
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js
new file mode 100644
index 0000000000..e9f1ae2291
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js
@@ -0,0 +1,102 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify that the old directory is removed in
+ * MaybeUpgradeFromPersistentStorageDirectoryToDefaultStorageDirectory method.
+ */
+
+async function testSteps() {
+ const url = "http://www.mozilla.org";
+ const persistence = "default";
+ const lastAccessed = 0x0005330925e07841;
+
+ const packages = [
+ // Storage used by FF 36-48 (storage/default/ directory and re-created
+ // storage/persistent/ directory by an older FF).
+ "persistentAndDefaultStorageDirectory_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking directories");
+
+ let persistentStorageDir = getRelativeFile("storage/persistent");
+ let exists = persistentStorageDir.exists();
+ ok(exists, "Persistent storage directory does exist");
+
+ let defaultStorageDir = getRelativeFile("storage/default");
+ exists = defaultStorageDir.exists();
+ ok(exists, "Default storage directory does exist");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization and getting
+ // usage is able to ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/permanent/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking directories");
+
+ persistentStorageDir = getRelativeFile("storage/persistent");
+ exists = persistentStorageDir.exists();
+ ok(!exists, "Persistent storage directory doesn't exist");
+
+ defaultStorageDir = getRelativeFile("storage/default");
+ exists = defaultStorageDir.exists();
+ ok(exists, "Default storage directory does exist");
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage();
+ await requestFinished(request);
+
+ info("Initializing origin");
+
+ request = initTemporaryOrigin(persistence, getPrincipal(url));
+ await requestFinished(request);
+
+ ok(!request.result, "Origin directory wasn't created");
+
+ info("Getting usage");
+
+ request = getUsage(function() {}, /* getAll */ true);
+ await requestFinished(request);
+
+ info("Verifying result");
+
+ const result = request.result;
+ is(result.length, 1, "Correct number of usage results");
+
+ info("Verifying usage result");
+
+ const usageResult = result[0];
+ ok(usageResult.origin == url, "Origin equals");
+ ok(usageResult.lastAccessed == lastAccessed, "LastAccessed equals");
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js
new file mode 100644
index 0000000000..cac8ec3b16
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js
@@ -0,0 +1,162 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function* testSteps() {
+ const origins = [
+ {
+ oldPath: "storage/persistent/1007+f+app+++system.gaiamobile.org",
+ upgraded: true,
+ },
+
+ {
+ oldPath: "storage/persistent/1007+t+https+++developer.cdn.mozilla.net",
+ upgraded: true,
+ },
+
+ {
+ oldPath: "storage/persistent/http+++www.mozilla.org",
+ newPath: "storage/default/http+++www.mozilla.org",
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ upgraded: true,
+ },
+ {
+ oldPath: "storage/persistent/http+++www.mozilla.org+8080",
+ newPath: "storage/default/http+++www.mozilla.org+8080",
+ url: "http://www.mozilla.org:8080",
+ persistence: "default",
+ },
+ ];
+
+ const metadataFileName = ".metadata";
+
+ const packages = [
+ // Storage used by FF 26-35 (storage/persistent/ directory with already
+ // upgraded origin directories and not yet upgraded flat origin
+ // directories).
+ "persistentStorageDirectory_originDirectories_profile",
+ "../persistentStorageDirectory_shared",
+ ];
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking origin directories");
+
+ for (const origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ let idbDir = originDir.clone();
+ idbDir.append("idb");
+
+ exists = idbDir.exists();
+ if (origin.upgraded) {
+ ok(exists, "idb directory does exist");
+ } else {
+ ok(!exists, "idb directory doesn't exist");
+ }
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ if (origin.upgraded) {
+ ok(exists, "Metadata file does exist");
+ } else {
+ ok(!exists, "Metadata file doesn't exist");
+ }
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization and getting
+ // usage is able to ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking origin directories");
+
+ for (const origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+
+ if (origin.newPath) {
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ let idbDir = originDir.clone();
+ idbDir.append("idb");
+
+ exists = idbDir.exists();
+ ok(exists, "idb directory does exist");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ ok(exists, "Metadata file does exist");
+ }
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ if (origin.newPath) {
+ info("Initializing origin");
+
+ let principal = getPrincipal(origin.url);
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js
new file mode 100644
index 0000000000..bd6a010cf8
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom0_0.js
@@ -0,0 +1,158 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify UpgradeStorageFrom0_0To1_0 method.
+ */
+
+function* testSteps() {
+ const origins = [
+ {
+ path: "storage/default/1007+f+app+++system.gaiamobile.org",
+ obsolete: true,
+ },
+
+ {
+ path: "storage/default/1007+t+https+++developer.cdn.mozilla.net",
+ obsolete: true,
+ },
+
+ {
+ path: "storage/default/http+++www.mozilla.org",
+ obsolete: false,
+ url: "http://www.mozilla.org",
+ persistence: "default",
+ },
+ ];
+
+ const storageFileName = "storage.sqlite";
+ const metadataFileName = ".metadata";
+ const metadata2FileName = ".metadata-v2";
+
+ const packages = [
+ // Storage used by FF 36-48 (storage/default/ directory, but no
+ // storage.sqlite and no .metadata-v2 files).
+ "version0_0_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking storage file");
+
+ let storageFile = getRelativeFile(storageFileName);
+
+ let exists = storageFile.exists();
+ ok(!exists, "Storage file doesn't exist");
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.path);
+
+ exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ ok(exists, "Metadata file does exist");
+
+ let metadata2File = originDir.clone();
+ metadata2File.append(metadata2FileName);
+
+ exists = metadata2File.exists();
+ ok(!exists, "Metadata file doesn't exist");
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ exists = storageFile.exists();
+ ok(exists, "Storage file does exist");
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.path);
+
+ exists = originDir.exists();
+ if (origin.obsolete) {
+ ok(!exists, "Origin directory doesn't exist");
+ } else {
+ ok(exists, "Origin directory does exist");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ exists = metadataFile.exists();
+ ok(exists, "Metadata file does exist");
+
+ let metadata2File = originDir.clone();
+ metadata2File.append(metadata2FileName);
+
+ exists = metadata2File.exists();
+ ok(exists, "Metadata file does exist");
+ }
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ if (!origin.obsolete) {
+ info("Initializing origin");
+
+ let principal = getPrincipal(origin.url);
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js
new file mode 100644
index 0000000000..34508f85e8
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_idb.js
@@ -0,0 +1,43 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify indexedDB::QuotaClient::UpgradeStorageFrom1_0To2_0
+ * method.
+ */
+
+async function testSteps() {
+ const packages = [
+ // Storage used by FF 49-54 (storage version 1.0 with idb directory).
+ "version1_0_idb_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Initializing");
+
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js
new file mode 100644
index 0000000000..17946ffff5
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeAppsData.js
@@ -0,0 +1,101 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify MaybeRemoveAppsData method.
+ */
+
+function* testSteps() {
+ const origins = [
+ {
+ path: "storage/default/http+++www.mozilla.org",
+ obsolete: false,
+ },
+
+ {
+ path: "storage/default/app+++system.gaiamobile.org^appId=1007",
+ obsolete: true,
+ },
+
+ {
+ path:
+ "storage/default/https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1",
+ obsolete: true,
+ },
+ ];
+
+ const packages = [
+ // Storage used by FF 49-54 (storage version 1.0 with apps data).
+ "version1_0_appsData_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.path);
+
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once getting usage is able to ignore unknown
+ // directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/permanent/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.path);
+
+ let exists = originDir.exists();
+ if (origin.obsolete) {
+ ok(!exists, "Origin directory doesn't exist");
+ } else {
+ ok(exists, "Origin directory does exist");
+ }
+ }
+
+ info("Getting usage");
+
+ getUsage(grabResultAndContinueHandler, /* getAll */ true);
+ let result = yield undefined;
+
+ info("Verifying result");
+
+ is(result.length, 1, "Correct number of usage results");
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js
new file mode 100644
index 0000000000..b37d00bbf8
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_removeMorgueDirectory.js
@@ -0,0 +1,60 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify MaybeRemoveMorgueDirectory method.
+ */
+
+function* testSteps() {
+ const morgueFile = "storage/default/http+++example.com/morgue";
+
+ const packages = [
+ // Storage used by FF 49-54 (storage version 1.0 with morgue directory).
+ "version1_0_morgueDirectory_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking morgue file");
+
+ let file = getRelativeFile(morgueFile);
+
+ let exists = file.exists();
+ ok(exists, "Morgue file does exist");
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ info("Checking morgue file");
+
+ exists = file.exists();
+ ok(!exists, "Morgue file doesn't exist");
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js
new file mode 100644
index 0000000000..407a04c1f8
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js
@@ -0,0 +1,179 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify MaybeStripObsoleteOriginAttributes method.
+ */
+
+loadScript("dom/quota/test/common/file.js");
+
+function* testSteps() {
+ const origins = [
+ {
+ oldPath:
+ "storage/permanent/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ newPath:
+ "storage/permanent/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f",
+ persistence: "persistent",
+ },
+
+ {
+ oldPath:
+ "storage/temporary/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ newPath:
+ "storage/temporary/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f",
+ persistence: "temporary",
+ },
+
+ {
+ oldPath:
+ "storage/default/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ newPath:
+ "storage/default/moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ url: "moz-extension://8ea6d31b-917c-431f-a204-15b95e904d4f",
+ persistence: "default",
+ },
+ ];
+
+ const metadataFileName = ".metadata-v2";
+
+ const packages = [
+ // Storage used by FF 49-54 (storage version 1.0 with obsolete origin
+ // attributes).
+ "version1_0_obsoleteOriginAttributes_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ let metadataBuffers = [];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ info("Reading out contents of metadata file");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler);
+ let file = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+
+ yield undefined;
+
+ metadataBuffers.push(fileReader.result);
+
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+ }
+
+ info("Initializing");
+
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ // TODO: Remove this block once temporary storage initialization is able to
+ // ignore unknown directories.
+ getRelativeFile("storage/default/invalid+++example.com").remove(false);
+ getRelativeFile("storage/temporary/invalid+++example.com").remove(false);
+
+ info("Checking origin directories");
+
+ for (let origin of origins) {
+ let originDir = getRelativeFile(origin.oldPath);
+ let exists = originDir.exists();
+ ok(!exists, "Origin directory doesn't exist");
+
+ originDir = getRelativeFile(origin.newPath);
+ exists = originDir.exists();
+ ok(exists, "Origin directory does exist");
+
+ info("Reading out contents of metadata file");
+
+ let metadataFile = originDir.clone();
+ metadataFile.append(metadataFileName);
+
+ File.createFromNsIFile(metadataFile).then(grabArgAndContinueHandler);
+ let file = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(file);
+
+ yield undefined;
+
+ let metadataBuffer = fileReader.result;
+
+ info("Verifying blobs differ");
+
+ ok(
+ !compareBuffers(metadataBuffer, metadataBuffers.shift()),
+ "Metadata differ"
+ );
+ }
+
+ info("Initializing temporary storage");
+
+ request = initTemporaryStorage(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Initializing origins");
+
+ for (const origin of origins) {
+ info("Initializing origin");
+
+ let principal = getPrincipal(origin.url);
+ if (origin.persistence == "persistent") {
+ request = initPersistentOrigin(principal, continueToNextStepSync);
+ } else {
+ request = initTemporaryOrigin(
+ origin.persistence,
+ principal,
+ continueToNextStepSync
+ );
+ }
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ ok(!request.result, "Origin directory wasn't created");
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js
new file mode 100644
index 0000000000..55edfa6055
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_0.js
@@ -0,0 +1,97 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify UpgradeStorageFrom2_0To2_1 method.
+ */
+
+function* testSteps() {
+ const origins = [
+ "storage/default/chrome/",
+ "storage/default/http+++www.mozilla.org/",
+ ];
+ const paddingFilePath = "cache/.padding";
+
+ const packages = [
+ // Storage used by FF 55-56 (storage version 2.0).
+ // The profile contains two cache storages:
+ // - storage/default/chrome/cache,
+ // - storage/default/http+++www.mozilla.org/cache
+ // The file create_cache.js in the package was run locally, specifically it
+ // was temporarily added to xpcshell.ini and then executed:
+ // mach xpcshell-test --interactive dom/quota/test/xpcshell/create_cache.js
+ // Note: it only creates the directory "storage/default/chrome/cache".
+ // To make it become the profile in the test, two more manual steps are
+ // needed.
+ // 1. Remove the folder "storage/temporary".
+ // 2. Copy the content under the "storage/default/chrome" to
+ // "storage/default/http+++www.mozilla.org".
+ // 3. Manually create an asmjs folder under the
+ // "storage/default/http+++www.mozilla.org/".
+ "version2_0_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ clear(continueToNextStepSync);
+ yield undefined;
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking padding files before upgrade (storage version 2.0)");
+
+ for (let origin of origins) {
+ let paddingFile = getRelativeFile(origin + paddingFilePath);
+ let exists = paddingFile.exists();
+ ok(!exists, "Padding file doesn't exist");
+ }
+
+ info("Initializing");
+
+ // Initialize to trigger storage upgrade from version 2.0.
+ let request = init(continueToNextStepSync);
+ yield undefined;
+
+ ok(request.resultCode == NS_OK, "Initialization succeeded");
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ info("Checking padding files after upgrade");
+
+ for (let origin of origins) {
+ let paddingFile = getRelativeFile(origin + paddingFilePath);
+ let exists = paddingFile.exists();
+ ok(exists, "Padding file does exist");
+
+ info("Reading out contents of padding file");
+
+ File.createFromNsIFile(paddingFile).then(grabArgAndContinueHandler);
+ let domFile = yield undefined;
+
+ let fileReader = new FileReader();
+ fileReader.onload = continueToNextStepSync;
+ fileReader.readAsArrayBuffer(domFile);
+ yield undefined;
+
+ let paddingFileInfo = new Float64Array(fileReader.result);
+ ok(paddingFileInfo.length == 1, "Padding file does take 64 bytes.");
+ ok(paddingFileInfo[0] == 0, "Padding size does default to zero.");
+ }
+
+ finishTest();
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js
new file mode 100644
index 0000000000..eed7ed21d5
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_1.js
@@ -0,0 +1,85 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify UpgradeStorageFrom2_1To2_2 method (removal of
+ * obsolete origins, deprecated clients and unknown temporary files).
+ */
+
+async function testSteps() {
+ const filePaths = [
+ // Obsolete origins:
+ "storage/default/chrome+++content+browser.xul",
+
+ "storage/default/moz-safe-about+++home",
+
+ // TODO: These three origins don't belong here! They were added one release
+ // later and the origin parser was fixed to handle these origins one
+ // release later as well, so users which already upgraded to 2.2 may
+ // still have issues related to these origins!
+ "storage/default/about+home+1",
+
+ "storage/default/about+home+1+q",
+
+ // about:reader?url=xxx (before bug 1422456)
+ "storage/default/about+reader+url=https%3A%2F%2Fexample.com",
+
+ // Deprecated client:
+ "storage/default/https+++example.com/asmjs",
+
+ // Unknown temporary file:
+ "storage/default/https+++example.com/idb/UUID123.tmp",
+ ];
+
+ const packages = [
+ // Storage used by FF 57-67 (storage version 2.1 with obsolete origins, a
+ // deprecated client and an unknown temporary file).
+ "version2_1_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ info("Checking files and directories before upgrade (storage version 2.1)");
+
+ for (const filePath of filePaths) {
+ let file = getRelativeFile(filePath);
+ let exists = file.exists();
+ ok(exists, "File or directory does exist");
+ }
+
+ info("Initializing");
+
+ // Initialize to trigger storage upgrade from version 2.1
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ info("Checking files and directories after upgrade");
+
+ for (const filePath of filePaths) {
+ let file = getRelativeFile(filePath);
+ let exists = file.exists();
+ ok(!exists, "File or directory does not exist");
+ }
+}
diff --git a/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js
new file mode 100644
index 0000000000..8f41c05b49
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/test_upgradeStorageFrom2_2.js
@@ -0,0 +1,64 @@
+/**
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * This test is mainly to verify UpgradeStorageFrom2_2To2_3 method.
+ */
+
+async function testSteps() {
+ const packages = [
+ // Storage used by FF 68-69 (storage version 2.2).
+ "version2_2_profile",
+ "../defaultStorageDirectory_shared",
+ ];
+
+ function verifyDatabaseTable(shouldExist) {
+ let file = getRelativeFile("storage.sqlite");
+ let conn = Services.storage.openUnsharedDatabase(file);
+
+ let exists = conn.tableExists("database");
+ if (shouldExist) {
+ ok(exists, "Database table does exist");
+ } else {
+ ok(!exists, "Database table does not exist");
+ }
+
+ conn.close();
+ }
+
+ info("Clearing");
+
+ let request = clear();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "beforeInstall");
+
+ info("Installing packages");
+
+ installPackages(packages);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInstall");
+
+ verifyDatabaseTable(/* shouldExist */ false);
+
+ info("Initializing");
+
+ // Initialize to trigger storage upgrade from version 2.2
+ request = init();
+ await requestFinished(request);
+
+ info("Verifying storage");
+
+ verifyStorage(packages, "afterInit");
+
+ request = reset();
+ await requestFinished(request);
+
+ verifyDatabaseTable(/* shouldExist */ true);
+}
diff --git a/dom/quota/test/xpcshell/upgrades/version0_0_profile.json b/dom/quota/test/xpcshell/upgrades/version0_0_profile.json
new file mode 100644
index 0000000000..05c0d27fe3
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version0_0_profile.json
@@ -0,0 +1,88 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "1007+f+app+++system.gaiamobile.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "1007+t+https+++developer.cdn.mozilla.org",
+ "dir": true,
+ "entries": [{ "name": ".metadata", "dir": false }]
+ },
+ {
+ "name": "https+++developer.cdn.mozilla.org",
+ "dir": true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "todo": "This shouldn't exist, it regressed after accidental changes done in bug 1320404",
+ "name": "https+++developer.cdn.mozilla.net",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "https+++developer.cdn.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip b/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip
new file mode 100644
index 0000000000..5ef577191c
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version0_0_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json
new file mode 100644
index 0000000000..88f5d5dcee
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.json
@@ -0,0 +1,72 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "app+++system.gaiamobile.org^appId=1007",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "https+++developer.cdn.mozilla.net^appId=1007&inBrowser=1",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip
new file mode 100644
index 0000000000..582edb43af
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_appsData_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json
new file mode 100644
index 0000000000..a8fb8b2260
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.json
@@ -0,0 +1,73 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-safe-about+home",
+ "dir": true,
+ "entries": [
+ {
+ "name": "idb",
+ "dir": true,
+ "entries": [
+ { "name": "631132235dGb", "dir": true },
+ { "name": "631132235dGb.files", "dir": true },
+ { "name": "631132235dGb.sqlite", "dir": false }
+ ]
+ },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir":false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-safe-about+home",
+ "dir": true,
+ "entries": [
+ {
+ "name": "idb",
+ "dir": true,
+ "entries": [
+ { "name": "631132235dGb.files", "dir": true },
+ { "name": "631132235dGb.sqlite", "dir": false }
+ ]
+ },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir":false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip
new file mode 100644
index 0000000000..8abfae79c2
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_idb_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json
new file mode 100644
index 0000000000..855f7846bc
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.json
@@ -0,0 +1,57 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++example.com",
+ "dir": true,
+ "entries": [
+ { "name": "morgue", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "http+++example.com",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip
new file mode 100644
index 0000000000..88543784ec
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_morgueDirectory_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json
new file mode 100644
index 0000000000..071c4413f4
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.json
@@ -0,0 +1,112 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f^addonId=indexedDB-test%40kmaglione.mozilla.com",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "permanent",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ {
+ "name": "temporary",
+ "dir": true,
+ "entries": [
+ {
+ "name": "moz-extension+++8ea6d31b-917c-431f-a204-15b95e904d4f",
+ "dir": true,
+ "entries": [
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip
new file mode 100644
index 0000000000..2b4125edf9
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version1_0_obsoleteOriginAttributes_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version2_0_profile.json b/dom/quota/test/xpcshell/upgrades/version2_0_profile.json
new file mode 100644
index 0000000000..04ad73eae3
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_0_profile.json
@@ -0,0 +1,105 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [
+ {
+ "name": "cache",
+ "dir": true,
+ "entries": [
+ { "name": "morgue", "dir": true },
+ { "name": "caches.sqlite", "dir": false }
+ ]
+ },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ { "name": "asmjs", "dir": true },
+ {
+ "name": "cache",
+ "dir": true,
+ "entries": [
+ { "name": "morgue", "dir": true },
+ { "name": "caches.sqlite", "dir": false }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "chrome",
+ "dir": true,
+ "entries": [
+ {
+ "name": "cache",
+ "dir": true,
+ "entries": [
+ { "name": "morgue", "dir": true },
+ { "name": ".padding", "dir": false },
+ { "name": "caches.sqlite", "dir": false }
+ ]
+ },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ },
+ {
+ "name": "http+++www.mozilla.org",
+ "dir": true,
+ "entries": [
+ {
+ "name": "cache",
+ "dir": true,
+ "entries": [
+ { "name": "morgue", "dir": true },
+ { "name": ".padding", "dir": false },
+ { "name": "caches.sqlite", "dir": false }
+ ]
+ },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip
new file mode 100644
index 0000000000..c140df56e4
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_0_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version2_1_profile.json b/dom/quota/test/xpcshell/upgrades/version2_1_profile.json
new file mode 100644
index 0000000000..a7866d1123
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_1_profile.json
@@ -0,0 +1,69 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ { "name": "about+home+1", "dir": true },
+ { "name": "about+home+1+q", "dir": true },
+ {
+ "name": "about+reader+url=https%3A%2F%2Fexample.com",
+ "dir": true
+ },
+ { "name": "chrome+++content+browser.xul", "dir": true },
+ {
+ "name": "https+++example.com",
+ "dir": true,
+ "entries": [
+ { "name": "asmjs", "dir": true },
+ {
+ "name": "idb",
+ "dir": true,
+ "entries": [{ "name": "UUID123.tmp", "dir": false }]
+ }
+ ]
+ },
+ { "name": "moz-safe-about+++home", "dir": true }
+ ]
+ }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [
+ {
+ "name": "default",
+ "dir": true,
+ "entries": [
+ {
+ "name": "https+++example.com",
+ "dir": true,
+ "entries": [
+ { "name": "idb", "dir": true },
+ { "name": ".metadata", "dir": false },
+ { "name": ".metadata-v2", "dir": false }
+ ]
+ }
+ ]
+ },
+ { "name": "ls-archive.sqlite", "dir": false }
+ ]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip
new file mode 100644
index 0000000000..908dac7058
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_1_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/version2_2_profile.json b/dom/quota/test/xpcshell/upgrades/version2_2_profile.json
new file mode 100644
index 0000000000..4b7265e3b4
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_2_profile.json
@@ -0,0 +1,18 @@
+[
+ { "key": "beforeInstall", "entries": [] },
+ {
+ "key": "afterInstall",
+ "entries": [{ "name": "storage.sqlite", "dir": false }]
+ },
+ {
+ "key": "afterInit",
+ "entries": [
+ {
+ "name": "storage",
+ "dir": true,
+ "entries": [{ "name": "ls-archive.sqlite", "dir": false }]
+ },
+ { "name": "storage.sqlite", "dir": false }
+ ]
+ }
+]
diff --git a/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip b/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip
new file mode 100644
index 0000000000..b6ae7e7d76
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/version2_2_profile.zip
Binary files differ
diff --git a/dom/quota/test/xpcshell/upgrades/xpcshell.ini b/dom/quota/test/xpcshell/upgrades/xpcshell.ini
new file mode 100644
index 0000000000..8698e4bff4
--- /dev/null
+++ b/dom/quota/test/xpcshell/upgrades/xpcshell.ini
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+head = head.js
+support-files =
+ cacheVersion1_profile.json
+ cacheVersion1_profile.zip
+ indexedDBAndPersistentStorageDirectory_profile.json
+ indexedDBAndPersistentStorageDirectory_profile.zip
+ indexedDBDirectory_flatOriginDirectories_profile.json
+ indexedDBDirectory_flatOriginDirectories_profile.zip
+ indexedDBDirectory_profile.json
+ indexedDBDirectory_profile.zip
+ localStorageArchive1upgrade_profile.zip
+ localStorageArchive4upgrade_profile.zip
+ localStorageArchiveDowngrade_profile.zip
+ persistentAndDefaultStorageDirectory_profile.json
+ persistentAndDefaultStorageDirectory_profile.zip
+ persistentStorageDirectory_flatOriginDirectories_profile.json
+ persistentStorageDirectory_flatOriginDirectories_profile.zip
+ persistentStorageDirectory_originDirectories_profile.json
+ persistentStorageDirectory_originDirectories_profile.zip
+ persistentStorageDirectory_profile.json
+ persistentStorageDirectory_profile.zip
+ version0_0_profile.json
+ version0_0_profile.zip
+ version1_0_appsData_profile.json
+ version1_0_appsData_profile.zip
+ version1_0_idb_profile.json
+ version1_0_idb_profile.zip
+ version1_0_morgueDirectory_profile.json
+ version1_0_morgueDirectory_profile.zip
+ version1_0_obsoleteOriginAttributes_profile.json
+ version1_0_obsoleteOriginAttributes_profile.zip
+ version2_0_profile.json
+ version2_0_profile.zip
+ version2_1_profile.json
+ version2_1_profile.zip
+ version2_2_profile.json
+ version2_2_profile.zip
+
+[test_localStorageArchive1upgrade.js]
+[test_localStorageArchive4upgrade.js]
+[test_localStorageArchiveDowngrade.js]
+[test_upgradeCacheFrom1.js]
+[test_upgradeFromFlatOriginDirectories.js]
+[test_upgradeFromIndexedDBDirectory.js]
+[test_upgradeFromIndexedDBDirectory_removeOldDirectory.js]
+[test_upgradeFromPersistentStorageDirectory.js]
+[test_upgradeFromPersistentStorageDirectory_removeOldDirectory.js]
+[test_upgradeFromPersistentStorageDirectory_upgradeOriginDirectories.js]
+[test_upgradeStorageFrom0_0.js]
+[test_upgradeStorageFrom1_0_idb.js]
+[test_upgradeStorageFrom1_0_removeAppsData.js]
+[test_upgradeStorageFrom1_0_removeMorgueDirectory.js]
+[test_upgradeStorageFrom1_0_stripObsoleteOriginAttributes.js]
+[test_upgradeStorageFrom2_0.js]
+[test_upgradeStorageFrom2_1.js]
+[test_upgradeStorageFrom2_2.js]
diff --git a/dom/quota/test/xpcshell/xpcshell.ini b/dom/quota/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..2b85faed16
--- /dev/null
+++ b/dom/quota/test/xpcshell/xpcshell.ini
@@ -0,0 +1,61 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+head = head.js
+tags = condprof
+support-files =
+ basics_profile.zip
+ clearStoragesForPrincipal_profile.zip
+ createLocalStorage_profile.zip
+ defaultStorageDirectory_shared.json
+ defaultStorageDirectory_shared.zip
+ getUsage_profile.zip
+ groupMismatch_profile.zip
+ indexedDBDirectory_shared.json
+ indexedDBDirectory_shared.zip
+ originMismatch_profile.json
+ originMismatch_profile.zip
+ persistentStorageDirectory_shared.json
+ persistentStorageDirectory_shared.zip
+ removeLocalStorage1_profile.zip
+ removeLocalStorage2_profile.zip
+ tempMetadataCleanup_profile.zip
+ unknownFiles_profile.zip
+
+[make_unknownFiles.js]
+skip-if = true # Only used for recreating unknownFiles_profile.zip
+[make_unsetLastAccessTime.js]
+skip-if = true # Only used for recreating unsetLastAccessTime_profile.zip
+[test_allowListFiles.js]
+[test_basics.js]
+[test_bad_origin_directory.js]
+[test_createLocalStorage.js]
+[test_clearStoragesForPrincipal.js]
+[test_clearStoragesForOriginAttributesPattern.js]
+[test_estimateOrigin.js]
+[test_getUsage.js]
+[test_groupMismatch.js]
+[test_initTemporaryStorage.js]
+[test_listOrigins.js]
+[test_originEndsWithDot.js]
+[test_originMismatch.js]
+[test_originWithCaret.js]
+[test_orpahnedQuotaObject.js]
+[test_persist.js]
+[test_persist_eviction.js]
+[test_persist_globalLimit.js]
+[test_persist_groupLimit.js]
+[test_removeLocalStorage.js]
+[test_simpledb.js]
+[test_specialOrigins.js]
+[test_storagePressure.js]
+skip-if = condprof
+[test_tempMetadataCleanup.js]
+[test_unaccessedOrigins.js]
+[test_unknownFiles.js]
+[test_unsetLastAccessTime.js]
+support-files =
+ unsetLastAccessTime_profile.zip
+[test_validOrigins.js]