From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- storage/BaseVFS.cpp | 265 ++ storage/BaseVFS.h | 30 + storage/FileSystemModule.cpp | 254 ++ storage/FileSystemModule.h | 22 + storage/IStorageBindingParamsInternal.h | 54 + storage/ObfuscatingVFS.cpp | 696 +++++ storage/ObfuscatingVFS.h | 33 + storage/QuotaVFS.cpp | 621 ++++ storage/QuotaVFS.h | 33 + storage/ReadOnlyNoLockVFS.cpp | 130 + storage/SQLCollations.cpp | 183 ++ storage/SQLCollations.h | 229 ++ storage/SQLiteMutex.h | 143 + storage/StatementCache.h | 129 + storage/StorageBaseStatementInternal.cpp | 221 ++ storage/StorageBaseStatementInternal.h | 297 ++ storage/VacuumManager.cpp | 286 ++ storage/VacuumManager.h | 43 + storage/Variant.cpp | 56 + storage/Variant.h | 435 +++ storage/Variant_inl.h | 136 + storage/build/components.conf | 27 + storage/build/moz.build | 17 + storage/build/mozStorageCID.h | 43 + storage/moz.build | 112 + storage/mozIStorageAsyncConnection.idl | 405 +++ storage/mozIStorageAsyncStatement.idl | 37 + storage/mozIStorageBaseStatement.idl | 125 + storage/mozIStorageBindingParams.idl | 103 + storage/mozIStorageBindingParamsArray.idl | 34 + storage/mozIStorageCompletionCallback.idl | 24 + storage/mozIStorageConnection.idl | 283 ++ storage/mozIStorageError.idl | 149 + storage/mozIStorageFunction.idl | 40 + storage/mozIStoragePendingStatement.idl | 20 + storage/mozIStorageProgressHandler.idl | 26 + storage/mozIStorageResultSet.idl | 21 + storage/mozIStorageRow.idl | 33 + storage/mozIStorageService.idl | 290 ++ storage/mozIStorageStatement.idl | 326 +++ storage/mozIStorageStatementCallback.idl | 48 + storage/mozIStorageVacuumParticipant.idl | 65 + storage/mozIStorageValueArray.idl | 154 + storage/mozStorageArgValueArray.cpp | 182 ++ storage/mozStorageArgValueArray.h | 35 + storage/mozStorageAsyncStatement.cpp | 338 +++ storage/mozStorageAsyncStatement.h | 101 + storage/mozStorageAsyncStatementExecution.cpp | 580 ++++ storage/mozStorageAsyncStatementExecution.h | 243 ++ storage/mozStorageAsyncStatementJSHelper.cpp | 142 + storage/mozStorageAsyncStatementJSHelper.h | 58 + storage/mozStorageAsyncStatementParams.cpp | 116 + storage/mozStorageAsyncStatementParams.h | 69 + storage/mozStorageBindingParams.cpp | 431 +++ storage/mozStorageBindingParams.h | 111 + storage/mozStorageBindingParamsArray.cpp | 75 + storage/mozStorageBindingParamsArray.h | 105 + storage/mozStorageConnection.cpp | 2981 ++++++++++++++++++++ storage/mozStorageConnection.h | 569 ++++ storage/mozStorageError.cpp | 40 + storage/mozStorageError.h | 34 + storage/mozStorageHelper.h | 326 +++ storage/mozStoragePrivateHelpers.cpp | 253 ++ storage/mozStoragePrivateHelpers.h | 154 + storage/mozStorageResultSet.cpp | 47 + storage/mozStorageResultSet.h | 52 + storage/mozStorageRow.cpp | 206 ++ storage/mozStorageRow.h | 59 + storage/mozStorageSQLFunctions.cpp | 369 +++ storage/mozStorageSQLFunctions.h | 85 + storage/mozStorageService.cpp | 744 +++++ storage/mozStorageService.h | 183 ++ storage/mozStorageStatement.cpp | 843 ++++++ storage/mozStorageStatement.h | 132 + storage/mozStorageStatementData.h | 143 + storage/mozStorageStatementJSHelper.cpp | 258 ++ storage/mozStorageStatementJSHelper.h | 73 + storage/mozStorageStatementParams.cpp | 131 + storage/mozStorageStatementParams.h | 64 + storage/mozStorageStatementRow.cpp | 152 + storage/mozStorageStatementRow.h | 50 + storage/rust/Cargo.toml | 14 + storage/rust/src/lib.rs | 533 ++++ storage/storage.h | 24 + storage/style.txt | 141 + storage/test/gtest/moz.build | 40 + storage/test/gtest/storage_test_harness.cpp | 244 ++ storage/test/gtest/storage_test_harness.h | 223 ++ storage/test/gtest/test_AsXXX_helpers.cpp | 111 + storage/test/gtest/test_StatementCache.cpp | 132 + .../test_asyncStatementExecution_transaction.cpp | 445 +++ .../test_async_callbacks_with_spun_event_loops.cpp | 146 + storage/test/gtest/test_async_thread_naming.cpp | 230 ++ storage/test/gtest/test_binding_params.cpp | 186 ++ storage/test/gtest/test_deadlock_detector.cpp | 56 + storage/test/gtest/test_file_perms.cpp | 35 + .../gtest/test_interruptSynchronousConnection.cpp | 81 + storage/test/gtest/test_mutex.cpp | 73 + .../test/gtest/test_spinningSynchronousClose.cpp | 72 + storage/test/gtest/test_statement_scoper.cpp | 86 + storage/test/gtest/test_transaction_helper.cpp | 184 ++ storage/test/gtest/test_true_async.cpp | 161 ++ storage/test/gtest/test_unlock_notify.cpp | 234 ++ storage/test/moz.build | 13 + storage/test/unit/VacuumParticipant.sys.mjs | 109 + storage/test/unit/baddataDB.sqlite | Bin 0 -> 8192 bytes storage/test/unit/corruptDB.sqlite | Bin 0 -> 32772 bytes storage/test/unit/fakeDB.sqlite | 1 + storage/test/unit/goodDB.sqlite | Bin 0 -> 8192 bytes storage/test/unit/head_storage.js | 412 +++ storage/test/unit/locale_collation.txt | 174 ++ storage/test/unit/test_bug-365166.js | 23 + storage/test/unit/test_bug-393952.js | 35 + storage/test/unit/test_bug-429521.js | 49 + storage/test/unit/test_bug-444233.js | 54 + storage/test/unit/test_cache_size.js | 73 + storage/test/unit/test_chunk_growth.js | 51 + storage/test/unit/test_connection_asyncClose.js | 128 + storage/test/unit/test_connection_executeAsync.js | 175 ++ .../unit/test_connection_executeSimpleSQLAsync.js | 94 + .../test/unit/test_connection_failsafe_close.js | 35 + storage/test/unit/test_connection_interrupt.js | 125 + storage/test/unit/test_connection_online_backup.js | 211 ++ .../test/unit/test_default_journal_size_limit.js | 44 + storage/test/unit/test_js_helpers.js | 150 + storage/test/unit/test_levenshtein.js | 66 + storage/test/unit/test_like.js | 199 ++ storage/test/unit/test_like_escape.js | 72 + storage/test/unit/test_locale_collation.js | 291 ++ storage/test/unit/test_minimizeMemory.js | 23 + storage/test/unit/test_page_size_is_32k.js | 31 + storage/test/unit/test_persist_journal.js | 89 + .../unit/test_readonly-immutable-nolock_vfs.js | 47 + storage/test/unit/test_retry_on_busy.js | 199 ++ storage/test/unit/test_sqlite_secure_delete.js | 78 + storage/test/unit/test_statement_executeAsync.js | 1045 +++++++ .../unit/test_statement_wrapper_automatically.js | 166 ++ storage/test/unit/test_storage_connection.js | 997 +++++++ storage/test/unit/test_storage_ext.js | 84 + storage/test/unit/test_storage_ext_fts3.js | 91 + storage/test/unit/test_storage_ext_fts5.js | 122 + storage/test/unit/test_storage_function.js | 92 + storage/test/unit/test_storage_progresshandler.js | 112 + storage/test/unit/test_storage_service.js | 267 ++ storage/test/unit/test_storage_service_special.js | 48 + storage/test/unit/test_storage_service_unshared.js | 46 + storage/test/unit/test_storage_statement.js | 183 ++ storage/test/unit/test_storage_value_array.js | 194 ++ storage/test/unit/test_unicode.js | 91 + storage/test/unit/test_vacuum.js | 372 +++ storage/test/unit/xpcshell.toml | 96 + storage/variant/Cargo.toml | 14 + storage/variant/src/bag.rs | 135 + storage/variant/src/lib.rs | 230 ++ storage/variantToSQLiteT_impl.h | 114 + 155 files changed, 28213 insertions(+) create mode 100644 storage/BaseVFS.cpp create mode 100644 storage/BaseVFS.h create mode 100644 storage/FileSystemModule.cpp create mode 100644 storage/FileSystemModule.h create mode 100644 storage/IStorageBindingParamsInternal.h create mode 100644 storage/ObfuscatingVFS.cpp create mode 100644 storage/ObfuscatingVFS.h create mode 100644 storage/QuotaVFS.cpp create mode 100644 storage/QuotaVFS.h create mode 100644 storage/ReadOnlyNoLockVFS.cpp create mode 100644 storage/SQLCollations.cpp create mode 100644 storage/SQLCollations.h create mode 100644 storage/SQLiteMutex.h create mode 100644 storage/StatementCache.h create mode 100644 storage/StorageBaseStatementInternal.cpp create mode 100644 storage/StorageBaseStatementInternal.h create mode 100644 storage/VacuumManager.cpp create mode 100644 storage/VacuumManager.h create mode 100644 storage/Variant.cpp create mode 100644 storage/Variant.h create mode 100644 storage/Variant_inl.h create mode 100644 storage/build/components.conf create mode 100644 storage/build/moz.build create mode 100644 storage/build/mozStorageCID.h create mode 100644 storage/moz.build create mode 100644 storage/mozIStorageAsyncConnection.idl create mode 100644 storage/mozIStorageAsyncStatement.idl create mode 100644 storage/mozIStorageBaseStatement.idl create mode 100644 storage/mozIStorageBindingParams.idl create mode 100644 storage/mozIStorageBindingParamsArray.idl create mode 100644 storage/mozIStorageCompletionCallback.idl create mode 100644 storage/mozIStorageConnection.idl create mode 100644 storage/mozIStorageError.idl create mode 100644 storage/mozIStorageFunction.idl create mode 100644 storage/mozIStoragePendingStatement.idl create mode 100644 storage/mozIStorageProgressHandler.idl create mode 100644 storage/mozIStorageResultSet.idl create mode 100644 storage/mozIStorageRow.idl create mode 100644 storage/mozIStorageService.idl create mode 100644 storage/mozIStorageStatement.idl create mode 100644 storage/mozIStorageStatementCallback.idl create mode 100644 storage/mozIStorageVacuumParticipant.idl create mode 100644 storage/mozIStorageValueArray.idl create mode 100644 storage/mozStorageArgValueArray.cpp create mode 100644 storage/mozStorageArgValueArray.h create mode 100644 storage/mozStorageAsyncStatement.cpp create mode 100644 storage/mozStorageAsyncStatement.h create mode 100644 storage/mozStorageAsyncStatementExecution.cpp create mode 100644 storage/mozStorageAsyncStatementExecution.h create mode 100644 storage/mozStorageAsyncStatementJSHelper.cpp create mode 100644 storage/mozStorageAsyncStatementJSHelper.h create mode 100644 storage/mozStorageAsyncStatementParams.cpp create mode 100644 storage/mozStorageAsyncStatementParams.h create mode 100644 storage/mozStorageBindingParams.cpp create mode 100644 storage/mozStorageBindingParams.h create mode 100644 storage/mozStorageBindingParamsArray.cpp create mode 100644 storage/mozStorageBindingParamsArray.h create mode 100644 storage/mozStorageConnection.cpp create mode 100644 storage/mozStorageConnection.h create mode 100644 storage/mozStorageError.cpp create mode 100644 storage/mozStorageError.h create mode 100644 storage/mozStorageHelper.h create mode 100644 storage/mozStoragePrivateHelpers.cpp create mode 100644 storage/mozStoragePrivateHelpers.h create mode 100644 storage/mozStorageResultSet.cpp create mode 100644 storage/mozStorageResultSet.h create mode 100644 storage/mozStorageRow.cpp create mode 100644 storage/mozStorageRow.h create mode 100644 storage/mozStorageSQLFunctions.cpp create mode 100644 storage/mozStorageSQLFunctions.h create mode 100644 storage/mozStorageService.cpp create mode 100644 storage/mozStorageService.h create mode 100644 storage/mozStorageStatement.cpp create mode 100644 storage/mozStorageStatement.h create mode 100644 storage/mozStorageStatementData.h create mode 100644 storage/mozStorageStatementJSHelper.cpp create mode 100644 storage/mozStorageStatementJSHelper.h create mode 100644 storage/mozStorageStatementParams.cpp create mode 100644 storage/mozStorageStatementParams.h create mode 100644 storage/mozStorageStatementRow.cpp create mode 100644 storage/mozStorageStatementRow.h create mode 100644 storage/rust/Cargo.toml create mode 100644 storage/rust/src/lib.rs create mode 100644 storage/storage.h create mode 100644 storage/style.txt create mode 100644 storage/test/gtest/moz.build create mode 100644 storage/test/gtest/storage_test_harness.cpp create mode 100644 storage/test/gtest/storage_test_harness.h create mode 100644 storage/test/gtest/test_AsXXX_helpers.cpp create mode 100644 storage/test/gtest/test_StatementCache.cpp create mode 100644 storage/test/gtest/test_asyncStatementExecution_transaction.cpp create mode 100644 storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp create mode 100644 storage/test/gtest/test_async_thread_naming.cpp create mode 100644 storage/test/gtest/test_binding_params.cpp create mode 100644 storage/test/gtest/test_deadlock_detector.cpp create mode 100644 storage/test/gtest/test_file_perms.cpp create mode 100644 storage/test/gtest/test_interruptSynchronousConnection.cpp create mode 100644 storage/test/gtest/test_mutex.cpp create mode 100644 storage/test/gtest/test_spinningSynchronousClose.cpp create mode 100644 storage/test/gtest/test_statement_scoper.cpp create mode 100644 storage/test/gtest/test_transaction_helper.cpp create mode 100644 storage/test/gtest/test_true_async.cpp create mode 100644 storage/test/gtest/test_unlock_notify.cpp create mode 100644 storage/test/moz.build create mode 100644 storage/test/unit/VacuumParticipant.sys.mjs create mode 100644 storage/test/unit/baddataDB.sqlite create mode 100644 storage/test/unit/corruptDB.sqlite create mode 100644 storage/test/unit/fakeDB.sqlite create mode 100644 storage/test/unit/goodDB.sqlite create mode 100644 storage/test/unit/head_storage.js create mode 100644 storage/test/unit/locale_collation.txt create mode 100644 storage/test/unit/test_bug-365166.js create mode 100644 storage/test/unit/test_bug-393952.js create mode 100644 storage/test/unit/test_bug-429521.js create mode 100644 storage/test/unit/test_bug-444233.js create mode 100644 storage/test/unit/test_cache_size.js create mode 100644 storage/test/unit/test_chunk_growth.js create mode 100644 storage/test/unit/test_connection_asyncClose.js create mode 100644 storage/test/unit/test_connection_executeAsync.js create mode 100644 storage/test/unit/test_connection_executeSimpleSQLAsync.js create mode 100644 storage/test/unit/test_connection_failsafe_close.js create mode 100644 storage/test/unit/test_connection_interrupt.js create mode 100644 storage/test/unit/test_connection_online_backup.js create mode 100644 storage/test/unit/test_default_journal_size_limit.js create mode 100644 storage/test/unit/test_js_helpers.js create mode 100644 storage/test/unit/test_levenshtein.js create mode 100644 storage/test/unit/test_like.js create mode 100644 storage/test/unit/test_like_escape.js create mode 100644 storage/test/unit/test_locale_collation.js create mode 100644 storage/test/unit/test_minimizeMemory.js create mode 100644 storage/test/unit/test_page_size_is_32k.js create mode 100644 storage/test/unit/test_persist_journal.js create mode 100644 storage/test/unit/test_readonly-immutable-nolock_vfs.js create mode 100644 storage/test/unit/test_retry_on_busy.js create mode 100644 storage/test/unit/test_sqlite_secure_delete.js create mode 100644 storage/test/unit/test_statement_executeAsync.js create mode 100644 storage/test/unit/test_statement_wrapper_automatically.js create mode 100644 storage/test/unit/test_storage_connection.js create mode 100644 storage/test/unit/test_storage_ext.js create mode 100644 storage/test/unit/test_storage_ext_fts3.js create mode 100644 storage/test/unit/test_storage_ext_fts5.js create mode 100644 storage/test/unit/test_storage_function.js create mode 100644 storage/test/unit/test_storage_progresshandler.js create mode 100644 storage/test/unit/test_storage_service.js create mode 100644 storage/test/unit/test_storage_service_special.js create mode 100644 storage/test/unit/test_storage_service_unshared.js create mode 100644 storage/test/unit/test_storage_statement.js create mode 100644 storage/test/unit/test_storage_value_array.js create mode 100644 storage/test/unit/test_unicode.js create mode 100644 storage/test/unit/test_vacuum.js create mode 100644 storage/test/unit/xpcshell.toml create mode 100644 storage/variant/Cargo.toml create mode 100644 storage/variant/src/bag.rs create mode 100644 storage/variant/src/lib.rs create mode 100644 storage/variantToSQLiteT_impl.h (limited to 'storage') diff --git a/storage/BaseVFS.cpp b/storage/BaseVFS.cpp new file mode 100644 index 0000000000..f6090bf008 --- /dev/null +++ b/storage/BaseVFS.cpp @@ -0,0 +1,265 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "BaseVFS.h" + +#include +#include "sqlite3.h" +#include "mozilla/net/IOActivityMonitor.h" + +namespace { + +// The last VFS version for which this file has been updated. +constexpr int kLastKnowVfsVersion = 3; + +// The last io_methods version for which this file has been updated. +constexpr int kLastKnownIOMethodsVersion = 3; + +using namespace mozilla; +using namespace mozilla::net; + +struct BaseFile { + // Base class. Must be first + sqlite3_file base; + // The filename + char* location; + // This points to the underlying sqlite3_file + sqlite3_file pReal[1]; +}; + +int BaseClose(sqlite3_file* pFile) { + BaseFile* p = (BaseFile*)pFile; + delete[] p->location; + return p->pReal->pMethods->xClose(p->pReal); +} + +int BaseRead(sqlite3_file* pFile, void* zBuf, int iAmt, sqlite_int64 iOfst) { + BaseFile* p = (BaseFile*)pFile; + int rc = p->pReal->pMethods->xRead(p->pReal, zBuf, iAmt, iOfst); + if (rc == SQLITE_OK && IOActivityMonitor::IsActive()) { + IOActivityMonitor::Read(nsDependentCString(p->location), iAmt); + } + return rc; +} + +int BaseWrite(sqlite3_file* pFile, const void* zBuf, int iAmt, + sqlite_int64 iOfst) { + BaseFile* p = (BaseFile*)pFile; + int rc = p->pReal->pMethods->xWrite(p->pReal, zBuf, iAmt, iOfst); + if (rc == SQLITE_OK && IOActivityMonitor::IsActive()) { + IOActivityMonitor::Write(nsDependentCString(p->location), iAmt); + } + return rc; +} + +int BaseTruncate(sqlite3_file* pFile, sqlite_int64 size) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xTruncate(p->pReal, size); +} + +int BaseSync(sqlite3_file* pFile, int flags) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xSync(p->pReal, flags); +} + +int BaseFileSize(sqlite3_file* pFile, sqlite_int64* pSize) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xFileSize(p->pReal, pSize); +} + +int BaseLock(sqlite3_file* pFile, int eLock) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xLock(p->pReal, eLock); +} + +int BaseUnlock(sqlite3_file* pFile, int eLock) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xUnlock(p->pReal, eLock); +} + +int BaseCheckReservedLock(sqlite3_file* pFile, int* pResOut) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xCheckReservedLock(p->pReal, pResOut); +} + +int BaseFileControl(sqlite3_file* pFile, int op, void* pArg) { +#if defined(MOZ_SQLITE_PERSIST_AUXILIARY_FILES) + // Persist auxiliary files (-shm and -wal) on disk, because creating and + // deleting them may be expensive on slow storage. + // Only do this when there is a journal size limit, so the journal is + // truncated instead of deleted on shutdown, that feels safer if the user + // moves a database file around without its auxiliary files. + MOZ_ASSERT( + ::sqlite3_compileoption_used("DEFAULT_JOURNAL_SIZE_LIMIT"), + "A journal size limit ensures the journal is truncated on shutdown"); + if (op == SQLITE_FCNTL_PERSIST_WAL) { + *static_cast(pArg) = 1; + return SQLITE_OK; + } +#endif + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xFileControl(p->pReal, op, pArg); +} + +int BaseSectorSize(sqlite3_file* pFile) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xSectorSize(p->pReal); +} + +int BaseDeviceCharacteristics(sqlite3_file* pFile) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xDeviceCharacteristics(p->pReal); +} + +int BaseShmMap(sqlite3_file* pFile, int iPg, int pgsz, int bExtend, + void volatile** pp) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xShmMap(p->pReal, iPg, pgsz, bExtend, pp); +} + +int BaseShmLock(sqlite3_file* pFile, int offset, int n, int flags) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xShmLock(p->pReal, offset, n, flags); +} + +void BaseShmBarrier(sqlite3_file* pFile) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xShmBarrier(p->pReal); +} + +int BaseShmUnmap(sqlite3_file* pFile, int deleteFlag) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xShmUnmap(p->pReal, deleteFlag); +} + +int BaseFetch(sqlite3_file* pFile, sqlite3_int64 iOfst, int iAmt, void** pp) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xFetch(p->pReal, iOfst, iAmt, pp); +} + +int BaseUnfetch(sqlite3_file* pFile, sqlite3_int64 iOfst, void* pPage) { + BaseFile* p = (BaseFile*)pFile; + return p->pReal->pMethods->xUnfetch(p->pReal, iOfst, pPage); +} + +int BaseOpen(sqlite3_vfs* vfs, const char* zName, sqlite3_file* pFile, + int flags, int* pOutFlags) { + BaseFile* p = (BaseFile*)pFile; + if (zName) { + p->location = new char[7 + strlen(zName) + 1]; + strcpy(p->location, "file://"); + strcpy(p->location + 7, zName); + } else { + p->location = new char[8]; + strcpy(p->location, "file://"); + } + + sqlite3_vfs* origVfs = (sqlite3_vfs*)(vfs->pAppData); + int rc = origVfs->xOpen(origVfs, zName, p->pReal, flags, pOutFlags); + if (rc) { + return rc; + } + if (p->pReal->pMethods) { + // If the io_methods version is higher than the last known one, you should + // update this VFS adding appropriate IO methods for any methods added in + // the version change. + MOZ_ASSERT(p->pReal->pMethods->iVersion == kLastKnownIOMethodsVersion); + static const sqlite3_io_methods IOmethods = { + kLastKnownIOMethodsVersion, /* iVersion */ + BaseClose, /* xClose */ + BaseRead, /* xRead */ + BaseWrite, /* xWrite */ + BaseTruncate, /* xTruncate */ + BaseSync, /* xSync */ + BaseFileSize, /* xFileSize */ + BaseLock, /* xLock */ + BaseUnlock, /* xUnlock */ + BaseCheckReservedLock, /* xCheckReservedLock */ + BaseFileControl, /* xFileControl */ + BaseSectorSize, /* xSectorSize */ + BaseDeviceCharacteristics, /* xDeviceCharacteristics */ + BaseShmMap, /* xShmMap */ + BaseShmLock, /* xShmLock */ + BaseShmBarrier, /* xShmBarrier */ + BaseShmUnmap, /* xShmUnmap */ + BaseFetch, /* xFetch */ + BaseUnfetch /* xUnfetch */ + }; + pFile->pMethods = &IOmethods; + } + + return SQLITE_OK; +} + +} // namespace + +namespace mozilla::storage::basevfs { + +const char* GetVFSName(bool exclusive) { + return exclusive ? "base-vfs-excl" : "base-vfs"; +} + +UniquePtr ConstructVFS(bool exclusive) { +#if defined(XP_WIN) +# define EXPECTED_VFS "win32" +# define EXPECTED_VFS_EXCL "win32" +#else +# define EXPECTED_VFS "unix" +# define EXPECTED_VFS_EXCL "unix-excl" +#endif + + if (sqlite3_vfs_find(GetVFSName(exclusive))) { + return nullptr; + } + + bool found; + sqlite3_vfs* origVfs; + if (!exclusive) { + // Use the non-exclusive VFS. + origVfs = sqlite3_vfs_find(nullptr); + found = origVfs && origVfs->zName && !strcmp(origVfs->zName, EXPECTED_VFS); + } else { + origVfs = sqlite3_vfs_find(EXPECTED_VFS_EXCL); + found = (origVfs != nullptr); + } + if (!found) { + return nullptr; + } + + // If the VFS version is higher than the last known one, you should update + // this VFS adding appropriate methods for any methods added in the version + // change. + MOZ_ASSERT(origVfs->iVersion == kLastKnowVfsVersion); + + sqlite3_vfs vfs = { + kLastKnowVfsVersion, /* iVersion */ + origVfs->szOsFile + static_cast(sizeof(BaseFile)), /* szOsFile */ + origVfs->mxPathname, /* mxPathname */ + nullptr, /* pNext */ + GetVFSName(exclusive), /* zName */ + origVfs, /* pAppData */ + BaseOpen, /* xOpen */ + origVfs->xDelete, /* xDelete */ + origVfs->xAccess, /* xAccess */ + origVfs->xFullPathname, /* xFullPathname */ + origVfs->xDlOpen, /* xDlOpen */ + origVfs->xDlError, /* xDlError */ + origVfs->xDlSym, /* xDlSym */ + origVfs->xDlClose, /* xDlClose */ + origVfs->xRandomness, /* xRandomness */ + origVfs->xSleep, /* xSleep */ + origVfs->xCurrentTime, /* xCurrentTime */ + origVfs->xGetLastError, /* xGetLastError */ + origVfs->xCurrentTimeInt64, /* xCurrentTimeInt64 */ + origVfs->xSetSystemCall, /* xSetSystemCall */ + origVfs->xGetSystemCall, /* xGetSystemCall */ + origVfs->xNextSystemCall /* xNextSystemCall */ + }; + + return MakeUnique(vfs); +} + +} // namespace mozilla::storage::basevfs diff --git a/storage/BaseVFS.h b/storage/BaseVFS.h new file mode 100644 index 0000000000..f900bc5d21 --- /dev/null +++ b/storage/BaseVFS.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 STORAGE_BASEVFS_H_ +#define STORAGE_BASEVFS_H_ + +#include "mozilla/UniquePtr.h" + +struct sqlite3_vfs; +struct sqlite3_file; + +template +struct already_AddRefed; + +namespace mozilla::dom::quota { +class QuotaObject; +} + +namespace mozilla::storage::basevfs { + +const char* GetVFSName(bool exclusive); + +UniquePtr ConstructVFS(bool exclusive); + +} // namespace mozilla::storage::basevfs + +#endif // STORAGE_OBFUSCATINGVFS_H_ diff --git a/storage/FileSystemModule.cpp b/storage/FileSystemModule.cpp new file mode 100644 index 0000000000..df756838f8 --- /dev/null +++ b/storage/FileSystemModule.cpp @@ -0,0 +1,254 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "FileSystemModule.h" + +#include "sqlite3.h" +#include "nsComponentManagerUtils.h" +#include "nsString.h" +#include "nsIDirectoryEnumerator.h" +#include "nsIFile.h" + +namespace { + +struct VirtualTableCursorBase { + VirtualTableCursorBase() { memset(&mBase, 0, sizeof(mBase)); } + + sqlite3_vtab_cursor mBase; +}; + +struct VirtualTableCursor : public VirtualTableCursorBase { + public: + VirtualTableCursor() : mRowId(-1) { mCurrentFileName.SetIsVoid(true); } + + const nsString& DirectoryPath() const { return mDirectoryPath; } + + const nsString& CurrentFileName() const { return mCurrentFileName; } + + int64_t RowId() const { return mRowId; } + + nsresult Init(const nsAString& aPath); + nsresult NextFile(); + + private: + nsCOMPtr mEntries; + + nsString mDirectoryPath; + nsString mCurrentFileName; + + int64_t mRowId; +}; + +nsresult VirtualTableCursor::Init(const nsAString& aPath) { + nsCOMPtr directory = do_CreateInstance(NS_LOCAL_FILE_CONTRACTID); + NS_ENSURE_TRUE(directory, NS_ERROR_FAILURE); + + nsresult rv = directory->InitWithPath(aPath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = directory->GetPath(mDirectoryPath); + NS_ENSURE_SUCCESS(rv, rv); + + rv = directory->GetDirectoryEntries(getter_AddRefs(mEntries)); + NS_ENSURE_SUCCESS(rv, rv); + + rv = NextFile(); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult VirtualTableCursor::NextFile() { + bool hasMore; + nsresult rv = mEntries->HasMoreElements(&hasMore); + NS_ENSURE_SUCCESS(rv, rv); + + if (!hasMore) { + mCurrentFileName.SetIsVoid(true); + return NS_OK; + } + + nsCOMPtr entry; + rv = mEntries->GetNext(getter_AddRefs(entry)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr file = do_QueryInterface(entry); + NS_ENSURE_TRUE(file, NS_ERROR_FAILURE); + + rv = file->GetLeafName(mCurrentFileName); + NS_ENSURE_SUCCESS(rv, rv); + + mRowId++; + + return NS_OK; +} + +int Connect(sqlite3* aDB, void* aAux, int aArgc, const char* const* aArgv, + sqlite3_vtab** aVtab, char** aErr) { + static const char virtualTableSchema[] = + "CREATE TABLE fs (" + "name TEXT, " + "path TEXT" + ")"; + + int rc = sqlite3_declare_vtab(aDB, virtualTableSchema); + if (rc != SQLITE_OK) { + return rc; + } + + sqlite3_vtab* vt = new sqlite3_vtab(); + memset(vt, 0, sizeof(*vt)); + + *aVtab = vt; + + return SQLITE_OK; +} + +int Disconnect(sqlite3_vtab* aVtab) { + delete aVtab; + + return SQLITE_OK; +} + +int BestIndex(sqlite3_vtab* aVtab, sqlite3_index_info* aInfo) { + // Here we specify what index constraints we want to handle. That is, there + // might be some columns with particular constraints in which we can help + // SQLite narrow down the result set. + // + // For example, take the "path = x" where x is a directory. In this case, + // we can narrow our search to just this directory instead of the entire file + // system. This can be a significant optimization. So, we want to handle that + // constraint. To do so, we would look for two specific input conditions: + // + // 1. aInfo->aConstraint[i].iColumn == 1 + // 2. aInfo->aConstraint[i].op == SQLITE_INDEX_CONSTRAINT_EQ + // + // The first states that the path column is being used in one of the input + // constraints and the second states that the constraint involves the equal + // operator. + // + // An even more specific search would be for name='xxx', in which case we + // can limit the search to a single file, if it exists. + // + // What we have to do here is look for all of our index searches and select + // the narrowest. We can only pick one, so obviously we want the one that + // is the most specific, which leads to the smallest result set. + + for (int i = 0; i < aInfo->nConstraint; i++) { + if (aInfo->aConstraint[i].iColumn == 1 && aInfo->aConstraint[i].usable) { + if (aInfo->aConstraint[i].op & SQLITE_INDEX_CONSTRAINT_EQ) { + aInfo->aConstraintUsage[i].argvIndex = 1; + } + break; + } + + // TODO: handle single files (constrained also by the name column) + } + + return SQLITE_OK; +} + +int Open(sqlite3_vtab* aVtab, sqlite3_vtab_cursor** aCursor) { + VirtualTableCursor* cursor = new VirtualTableCursor(); + + *aCursor = reinterpret_cast(cursor); + + return SQLITE_OK; +} + +int Close(sqlite3_vtab_cursor* aCursor) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + + delete cursor; + + return SQLITE_OK; +} + +int Filter(sqlite3_vtab_cursor* aCursor, int aIdxNum, const char* aIdxStr, + int aArgc, sqlite3_value** aArgv) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + + if (aArgc <= 0) { + return SQLITE_OK; + } + + const char16_t* value = + static_cast(::sqlite3_value_text16(aArgv[0])); + + nsDependentString path(value, + ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t)); + + nsresult rv = cursor->Init(path); + NS_ENSURE_SUCCESS(rv, SQLITE_ERROR); + + return SQLITE_OK; +} + +int Next(sqlite3_vtab_cursor* aCursor) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + + nsresult rv = cursor->NextFile(); + NS_ENSURE_SUCCESS(rv, SQLITE_ERROR); + + return SQLITE_OK; +} + +int Eof(sqlite3_vtab_cursor* aCursor) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + return cursor->CurrentFileName().IsVoid() ? 1 : 0; +} + +int Column(sqlite3_vtab_cursor* aCursor, sqlite3_context* aContext, + int aColumnIndex) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + + switch (aColumnIndex) { + // name + case 0: { + const nsString& name = cursor->CurrentFileName(); + sqlite3_result_text16(aContext, name.get(), + name.Length() * sizeof(char16_t), SQLITE_TRANSIENT); + break; + } + + // path + case 1: { + const nsString& path = cursor->DirectoryPath(); + sqlite3_result_text16(aContext, path.get(), + path.Length() * sizeof(char16_t), SQLITE_TRANSIENT); + break; + } + default: + MOZ_ASSERT_UNREACHABLE("Unsupported column!"); + } + + return SQLITE_OK; +} + +int RowId(sqlite3_vtab_cursor* aCursor, sqlite3_int64* aRowid) { + VirtualTableCursor* cursor = reinterpret_cast(aCursor); + + *aRowid = cursor->RowId(); + + return SQLITE_OK; +} + +} // namespace + +namespace mozilla { +namespace storage { + +int RegisterFileSystemModule(sqlite3* aDB, const char* aName) { + static sqlite3_module module = { + 1, Connect, Connect, BestIndex, Disconnect, Disconnect, Open, + Close, Filter, Next, Eof, Column, RowId, nullptr, + nullptr, nullptr, nullptr, nullptr, nullptr, nullptr}; + + return sqlite3_create_module(aDB, aName, &module, nullptr); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/FileSystemModule.h b/storage/FileSystemModule.h new file mode 100644 index 0000000000..28731a1bdb --- /dev/null +++ b/storage/FileSystemModule.h @@ -0,0 +1,22 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_FileSystemModule_h +#define mozilla_storage_FileSystemModule_h + +#include "nscore.h" + +struct sqlite3; + +namespace mozilla { +namespace storage { + +int RegisterFileSystemModule(sqlite3* aDB, const char* aName); + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_FileSystemModule_h diff --git a/storage/IStorageBindingParamsInternal.h b/storage/IStorageBindingParamsInternal.h new file mode 100644 index 0000000000..a056ae3269 --- /dev/null +++ b/storage/IStorageBindingParamsInternal.h @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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_storage_IStorageBindingParamsInternal_h_ +#define mozilla_storage_IStorageBindingParamsInternal_h_ + +#include "nsISupports.h" + +struct sqlite3_stmt; +class mozIStorageError; + +namespace mozilla { +namespace storage { + +#define ISTORAGEBINDINGPARAMSINTERNAL_IID \ + { \ + 0x4c43d33a, 0xc620, 0x41b8, { \ + 0xba, 0x1d, 0x50, 0xc5, 0xb1, 0xe9, 0x1a, 0x04 \ + } \ + } + +/** + * Implementation-only interface for mozIStorageBindingParams. This defines the + * set of methods required by the asynchronous execution code in order to + * consume the contents stored in mozIStorageBindingParams instances. + */ +class IStorageBindingParamsInternal : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(ISTORAGEBINDINGPARAMSINTERNAL_IID) + + /** + * Binds our stored data to the statement. + * + * @param aStatement + * The statement to bind our data to. + * @return nullptr on success, or a mozIStorageError object if an error + * occurred. + */ + virtual already_AddRefed bind(sqlite3_stmt* aStatement) = 0; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(IStorageBindingParamsInternal, + ISTORAGEBINDINGPARAMSINTERNAL_IID) + +#define NS_DECL_ISTORAGEBINDINGPARAMSINTERNAL \ + already_AddRefed bind(sqlite3_stmt* aStatement) override; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_IStorageBindingParamsInternal_h_ diff --git a/storage/ObfuscatingVFS.cpp b/storage/ObfuscatingVFS.cpp new file mode 100644 index 0000000000..107384376c --- /dev/null +++ b/storage/ObfuscatingVFS.cpp @@ -0,0 +1,696 @@ +/* +** 2020-04-20 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +****************************************************************************** +** +** This file implements a VFS shim that obfuscates database content +** written to disk by applying a CipherStrategy. +** +** COMPILING +** +** This extension requires SQLite 3.32.0 or later. +** +** +** LOADING +** +** Initialize it using a single API call as follows: +** +** sqlite3_obfsvfs_init(); +** +** Obfsvfs is a VFS Shim. When loaded, "obfsvfs" becomes the new +** default VFS and it uses the prior default VFS as the next VFS +** down in the stack. This is normally what you want. However, it +** complex situations where multiple VFS shims are being loaded, +** it might be important to ensure that obfsvfs is loaded in the +** correct order so that it sequences itself into the default VFS +** Shim stack in the right order. +** +** USING +** +** Open database connections using the sqlite3_open_v2() with +** the SQLITE_OPEN_URI flag and using a URI filename that includes +** the query parameter "key=XXXXXXXXXXX..." where the XXXX... consists +** of 64 hexadecimal digits (32 bytes of content). +** +** Create a new encrypted database by opening a file that does not +** yet exist using the key= query parameter. +** +** LIMITATIONS: +** +** * An obfuscated database must be created as such. There is +** no way to convert an existing database file into an +** obfuscated database file other than to run ".dump" on the +** older database and reimport the SQL text into a new +** obfuscated database. +** +** * There is no way to change the key value, other than to +** ".dump" and restore the database +** +** * The database page size must be exactly 8192 bytes. No other +** database page sizes are currently supported. +** +** * Memory-mapped I/O does not work for obfuscated databases. +** If you think about it, memory-mapped I/O doesn't make any +** sense for obfuscated databases since you have to make a +** copy of the content to deobfuscate anyhow - you might as +** well use normal read()/write(). +** +** * Only the main database, the rollback journal, and WAL file +** are obfuscated. Other temporary files used for things like +** SAVEPOINTs or as part of a large external sort remain +** unobfuscated. +** +** * Requires SQLite 3.32.0 or later. +*/ +#include "ObfuscatingVFS.h" + +#include +#include +#include /* For debugging only */ + +#include "mozilla/dom/quota/IPCStreamCipherStrategy.h" +#include "mozilla/ScopeExit.h" +#include "nsPrintfCString.h" +#include "QuotaVFS.h" +#include "sqlite3.h" + +/* +** Forward declaration of objects used by this utility +*/ +using ObfsVfs = sqlite3_vfs; + +/* +** Useful datatype abbreviations +*/ +#if !defined(SQLITE_CORE) +using u8 = unsigned char; +#endif + +/* Access to a lower-level VFS that (might) implement dynamic loading, +** access to randomness, etc. +*/ +#define ORIGVFS(p) ((sqlite3_vfs*)((p)->pAppData)) +#define ORIGFILE(p) ((sqlite3_file*)(((ObfsFile*)(p)) + 1)) + +/* +** Database page size for obfuscated databases +*/ +#define OBFS_PGSZ 8192 + +#define WAL_FRAMEHDRSIZE 24 + +using namespace mozilla; +using namespace mozilla::dom::quota; + +/* An open file */ +struct ObfsFile { + sqlite3_file base; /* IO methods */ + const char* zFName; /* Original name of the file */ + bool inCkpt; /* Currently doing a checkpoint */ + ObfsFile* pPartner; /* Ptr from WAL to main-db, or from main-db to WAL */ + void* pTemp; /* Temporary storage for encoded pages */ + IPCStreamCipherStrategy* + encryptCipherStrategy; /* CipherStrategy for encryption */ + IPCStreamCipherStrategy* + decryptCipherStrategy; /* CipherStrategy for decryption */ +}; + +/* +** Methods for ObfsFile +*/ +static int obfsClose(sqlite3_file*); +static int obfsRead(sqlite3_file*, void*, int iAmt, sqlite3_int64 iOfst); +static int obfsWrite(sqlite3_file*, const void*, int iAmt, sqlite3_int64 iOfst); +static int obfsTruncate(sqlite3_file*, sqlite3_int64 size); +static int obfsSync(sqlite3_file*, int flags); +static int obfsFileSize(sqlite3_file*, sqlite3_int64* pSize); +static int obfsLock(sqlite3_file*, int); +static int obfsUnlock(sqlite3_file*, int); +static int obfsCheckReservedLock(sqlite3_file*, int* pResOut); +static int obfsFileControl(sqlite3_file*, int op, void* pArg); +static int obfsSectorSize(sqlite3_file*); +static int obfsDeviceCharacteristics(sqlite3_file*); +static int obfsShmMap(sqlite3_file*, int iPg, int pgsz, int, void volatile**); +static int obfsShmLock(sqlite3_file*, int offset, int n, int flags); +static void obfsShmBarrier(sqlite3_file*); +static int obfsShmUnmap(sqlite3_file*, int deleteFlag); +static int obfsFetch(sqlite3_file*, sqlite3_int64 iOfst, int iAmt, void** pp); +static int obfsUnfetch(sqlite3_file*, sqlite3_int64 iOfst, void* p); + +/* +** Methods for ObfsVfs +*/ +static int obfsOpen(sqlite3_vfs*, const char*, sqlite3_file*, int, int*); +static int obfsDelete(sqlite3_vfs*, const char* zPath, int syncDir); +static int obfsAccess(sqlite3_vfs*, const char* zPath, int flags, int*); +static int obfsFullPathname(sqlite3_vfs*, const char* zPath, int, char* zOut); +static void* obfsDlOpen(sqlite3_vfs*, const char* zPath); +static void obfsDlError(sqlite3_vfs*, int nByte, char* zErrMsg); +static void (*obfsDlSym(sqlite3_vfs* pVfs, void* p, const char* zSym))(void); +static void obfsDlClose(sqlite3_vfs*, void*); +static int obfsRandomness(sqlite3_vfs*, int nByte, char* zBufOut); +static int obfsSleep(sqlite3_vfs*, int nMicroseconds); +static int obfsCurrentTime(sqlite3_vfs*, double*); +static int obfsGetLastError(sqlite3_vfs*, int, char*); +static int obfsCurrentTimeInt64(sqlite3_vfs*, sqlite3_int64*); +static int obfsSetSystemCall(sqlite3_vfs*, const char*, sqlite3_syscall_ptr); +static sqlite3_syscall_ptr obfsGetSystemCall(sqlite3_vfs*, const char* z); +static const char* obfsNextSystemCall(sqlite3_vfs*, const char* zName); + +static const sqlite3_io_methods obfs_io_methods = { + 3, /* iVersion */ + obfsClose, /* xClose */ + obfsRead, /* xRead */ + obfsWrite, /* xWrite */ + obfsTruncate, /* xTruncate */ + obfsSync, /* xSync */ + obfsFileSize, /* xFileSize */ + obfsLock, /* xLock */ + obfsUnlock, /* xUnlock */ + obfsCheckReservedLock, /* xCheckReservedLock */ + obfsFileControl, /* xFileControl */ + obfsSectorSize, /* xSectorSize */ + obfsDeviceCharacteristics, /* xDeviceCharacteristics */ + obfsShmMap, /* xShmMap */ + obfsShmLock, /* xShmLock */ + obfsShmBarrier, /* xShmBarrier */ + obfsShmUnmap, /* xShmUnmap */ + obfsFetch, /* xFetch */ + obfsUnfetch /* xUnfetch */ +}; + +static constexpr int kKeyBytes = 32; +static constexpr int kIvBytes = IPCStreamCipherStrategy::BlockPrefixLength; +static constexpr int kClearTextPrefixBytesOnFirstPage = 32; +static constexpr int kReservedBytes = 32; +static constexpr int kBasicBlockSize = IPCStreamCipherStrategy::BasicBlockSize; +static_assert(kClearTextPrefixBytesOnFirstPage % kBasicBlockSize == 0); +static_assert(kReservedBytes % kBasicBlockSize == 0); + +/* Obfuscate a page using p->encryptCipherStrategy. +** +** A new random nonce is created and stored in the last 32 bytes +** of the page. All other bytes of the page are obfuscasted using the +** CipherStrategy. Except, for page-1 (including the SQLite +** database header) the first 32 bytes are not obfuscated +** +** Return a pointer to the obfuscated content, which is held in the +** p->pTemp buffer. Or return a NULL pointer if something goes wrong. +** Errors are reported using NS_WARNING(). +*/ +static void* obfsEncode(ObfsFile* p, /* File containing page to be obfuscated */ + u8* a, /* database page to be obfuscated */ + int nByte /* Bytes of content in a[]. Must be a multiple + of kBasicBlockSize. */ +) { + u8 aIv[kIvBytes]; + u8* pOut; + int i; + + static_assert((kIvBytes & (kIvBytes - 1)) == 0); + sqlite3_randomness(kIvBytes, aIv); + pOut = (u8*)p->pTemp; + if (pOut == nullptr) { + pOut = static_cast(sqlite3_malloc64(nByte)); + if (pOut == nullptr) { + NS_WARNING(nsPrintfCString("unable to allocate a buffer in which to" + " write obfuscated database content for %s", + p->zFName) + .get()); + return nullptr; + } + p->pTemp = pOut; + } + if (memcmp(a, "SQLite format 3", 16) == 0) { + i = kClearTextPrefixBytesOnFirstPage; + if (a[20] != kReservedBytes) { + NS_WARNING(nsPrintfCString("obfuscated database must have reserved-bytes" + " set to %d", + kReservedBytes) + .get()); + return nullptr; + } + memcpy(pOut, a, kClearTextPrefixBytesOnFirstPage); + } else { + i = 0; + } + const int payloadLength = nByte - kReservedBytes - i; + MOZ_ASSERT(payloadLength > 0); + // XXX I guess this can be done in-place as well, then we don't need the + // temporary page at all, I guess? + p->encryptCipherStrategy->Cipher( + Span{aIv}, Span{a + i, static_cast(payloadLength)}, + Span{pOut + i, static_cast(payloadLength)}); + memcpy(pOut + nByte - kReservedBytes, aIv, kIvBytes); + + return pOut; +} + +/* De-obfuscate a page using p->decryptCipherStrategy. +** +** The deobfuscation is done in-place. +** +** For pages that begin with the SQLite header text, the first +** 32 bytes are not deobfuscated. +*/ +static void obfsDecode(ObfsFile* p, /* File containing page to be obfuscated */ + u8* a, /* database page to be obfuscated */ + int nByte /* Bytes of content in a[]. Must be a multiple + of kBasicBlockSize. */ +) { + int i; + + if (memcmp(a, "SQLite format 3", 16) == 0) { + i = kClearTextPrefixBytesOnFirstPage; + } else { + i = 0; + } + const int payloadLength = nByte - kReservedBytes - i; + MOZ_ASSERT(payloadLength > 0); + p->decryptCipherStrategy->Cipher( + Span{a + nByte - kReservedBytes, kIvBytes}, + Span{a + i, static_cast(payloadLength)}, + Span{a + i, static_cast(payloadLength)}); + memset(a + nByte - kReservedBytes, 0, kIvBytes); +} + +/* +** Close an obfsucated file. +*/ +static int obfsClose(sqlite3_file* pFile) { + ObfsFile* p = (ObfsFile*)pFile; + if (p->pPartner) { + MOZ_ASSERT(p->pPartner->pPartner == p); + p->pPartner->pPartner = nullptr; + p->pPartner = nullptr; + } + sqlite3_free(p->pTemp); + + delete p->decryptCipherStrategy; + delete p->encryptCipherStrategy; + + pFile = ORIGFILE(pFile); + return pFile->pMethods->xClose(pFile); +} + +/* +** Read data from an obfuscated file. +** +** If the file is less than one full page in length, then return +** a substitute "prototype" page-1. This prototype page one +** specifies a database in WAL mode with an 8192-byte page size +** and a 32-byte reserved-bytes value. Those settings are necessary +** for obfuscation to function correctly. +*/ +static int obfsRead(sqlite3_file* pFile, void* zBuf, int iAmt, + sqlite_int64 iOfst) { + int rc; + ObfsFile* p = (ObfsFile*)pFile; + pFile = ORIGFILE(pFile); + rc = pFile->pMethods->xRead(pFile, zBuf, iAmt, iOfst); + if (rc == SQLITE_OK) { + if ((iAmt == OBFS_PGSZ || iAmt == OBFS_PGSZ + WAL_FRAMEHDRSIZE) && + !p->inCkpt) { + obfsDecode(p, ((u8*)zBuf) + iAmt - OBFS_PGSZ, OBFS_PGSZ); + } + } else if (rc == SQLITE_IOERR_SHORT_READ && iOfst == 0 && iAmt >= 100) { + static const unsigned char aEmptyDb[] = { + // Offset 0, Size 16, The header string: "SQLite format 3\000" + 0x53, 0x51, 0x4c, 0x69, 0x74, 0x65, 0x20, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x74, 0x20, 0x33, 0x00, + // XXX Add description for other fields + 0x20, 0x00, 0x02, 0x02, kReservedBytes, 0x40, 0x20, 0x20, 0x00, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, + // Offset 52, Size 4, The page number of the largest root b-tree page + // when in auto-vacuum or incremental-vacuum modes, or zero otherwise. + 0x00, 0x00, 0x00, 0x01}; + + memcpy(zBuf, aEmptyDb, sizeof(aEmptyDb)); + memset(((u8*)zBuf) + sizeof(aEmptyDb), 0, iAmt - sizeof(aEmptyDb)); + rc = SQLITE_OK; + } + return rc; +} + +/* +** Write data to an obfuscated file or journal. +*/ +static int obfsWrite(sqlite3_file* pFile, const void* zBuf, int iAmt, + sqlite_int64 iOfst) { + ObfsFile* p = (ObfsFile*)pFile; + pFile = ORIGFILE(pFile); + if (iAmt == OBFS_PGSZ && !p->inCkpt) { + zBuf = obfsEncode(p, (u8*)zBuf, iAmt); + if (zBuf == nullptr) { + return SQLITE_IOERR; + } + } + return pFile->pMethods->xWrite(pFile, zBuf, iAmt, iOfst); +} + +/* +** Truncate an obfuscated file. +*/ +static int obfsTruncate(sqlite3_file* pFile, sqlite_int64 size) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xTruncate(pFile, size); +} + +/* +** Sync an obfuscated file. +*/ +static int obfsSync(sqlite3_file* pFile, int flags) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xSync(pFile, flags); +} + +/* +** Return the current file-size of an obfuscated file. +*/ +static int obfsFileSize(sqlite3_file* pFile, sqlite_int64* pSize) { + ObfsFile* p = (ObfsFile*)pFile; + pFile = ORIGFILE(p); + return pFile->pMethods->xFileSize(pFile, pSize); +} + +/* +** Lock an obfuscated file. +*/ +static int obfsLock(sqlite3_file* pFile, int eLock) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xLock(pFile, eLock); +} + +/* +** Unlock an obfuscated file. +*/ +static int obfsUnlock(sqlite3_file* pFile, int eLock) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xUnlock(pFile, eLock); +} + +/* +** Check if another file-handle holds a RESERVED lock on an obfuscated file. +*/ +static int obfsCheckReservedLock(sqlite3_file* pFile, int* pResOut) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xCheckReservedLock(pFile, pResOut); +} + +/* +** File control method. For custom operations on an obfuscated file. +*/ +static int obfsFileControl(sqlite3_file* pFile, int op, void* pArg) { + int rc; + ObfsFile* p = (ObfsFile*)pFile; + pFile = ORIGFILE(pFile); + if (op == SQLITE_FCNTL_PRAGMA) { + char** azArg = (char**)pArg; + MOZ_ASSERT(azArg[1] != nullptr); + if (azArg[2] != nullptr && sqlite3_stricmp(azArg[1], "page_size") == 0) { + /* Do not allow page size changes on an obfuscated database */ + return SQLITE_OK; + } + } else if (op == SQLITE_FCNTL_CKPT_START || op == SQLITE_FCNTL_CKPT_DONE) { + p->inCkpt = op == SQLITE_FCNTL_CKPT_START; + if (p->pPartner) { + p->pPartner->inCkpt = p->inCkpt; + } + } + rc = pFile->pMethods->xFileControl(pFile, op, pArg); + if (rc == SQLITE_OK && op == SQLITE_FCNTL_VFSNAME) { + *(char**)pArg = sqlite3_mprintf("obfs/%z", *(char**)pArg); + } + return rc; +} + +/* +** Return the sector-size in bytes for an obfuscated file. +*/ +static int obfsSectorSize(sqlite3_file* pFile) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xSectorSize(pFile); +} + +/* +** Return the device characteristic flags supported by an obfuscated file. +*/ +static int obfsDeviceCharacteristics(sqlite3_file* pFile) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xDeviceCharacteristics(pFile); +} + +/* Create a shared memory file mapping */ +static int obfsShmMap(sqlite3_file* pFile, int iPg, int pgsz, int bExtend, + void volatile** pp) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xShmMap(pFile, iPg, pgsz, bExtend, pp); +} + +/* Perform locking on a shared-memory segment */ +static int obfsShmLock(sqlite3_file* pFile, int offset, int n, int flags) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xShmLock(pFile, offset, n, flags); +} + +/* Memory barrier operation on shared memory */ +static void obfsShmBarrier(sqlite3_file* pFile) { + pFile = ORIGFILE(pFile); + pFile->pMethods->xShmBarrier(pFile); +} + +/* Unmap a shared memory segment */ +static int obfsShmUnmap(sqlite3_file* pFile, int deleteFlag) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xShmUnmap(pFile, deleteFlag); +} + +/* Fetch a page of a memory-mapped file */ +static int obfsFetch(sqlite3_file* pFile, sqlite3_int64 iOfst, int iAmt, + void** pp) { + *pp = nullptr; + return SQLITE_OK; +} + +/* Release a memory-mapped page */ +static int obfsUnfetch(sqlite3_file* pFile, sqlite3_int64 iOfst, void* pPage) { + pFile = ORIGFILE(pFile); + return pFile->pMethods->xUnfetch(pFile, iOfst, pPage); +} + +/* +** Translate a single byte of Hex into an integer. +** This routine only works if h really is a valid hexadecimal +** character: 0..9a..fA..F +*/ +static u8 obfsHexToInt(int h) { + MOZ_ASSERT((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') || + (h >= 'A' && h <= 'F')); +#if 1 /* ASCII */ + h += 9 * (1 & (h >> 6)); +#else /* EBCDIC */ + h += 9 * (1 & ~(h >> 4)); +#endif + return (u8)(h & 0xf); +} + +/* +** Open a new file. +** +** If the file is an ordinary database file, or a rollback or WAL journal +** file, and if the key=XXXX parameter exists, then try to open the file +** as an obfuscated database. All other open attempts fall through into +** the lower-level VFS shim. +** +** If the key=XXXX parameter exists but is not 64-bytes of hex key, then +** put an error message in NS_WARNING() and return SQLITE_CANTOPEN. +*/ +static int obfsOpen(sqlite3_vfs* pVfs, const char* zName, sqlite3_file* pFile, + int flags, int* pOutFlags) { + ObfsFile* p; + sqlite3_file* pSubFile; + sqlite3_vfs* pSubVfs; + int rc, i; + const char* zKey; + u8 aKey[kKeyBytes]; + pSubVfs = ORIGVFS(pVfs); + if (flags & + (SQLITE_OPEN_MAIN_DB | SQLITE_OPEN_WAL | SQLITE_OPEN_MAIN_JOURNAL)) { + zKey = sqlite3_uri_parameter(zName, "key"); + } else { + zKey = nullptr; + } + if (zKey == nullptr) { + return pSubVfs->xOpen(pSubVfs, zName, pFile, flags, pOutFlags); + } + for (i = 0; + i < kKeyBytes && isxdigit(zKey[i * 2]) && isxdigit(zKey[i * 2 + 1]); + i++) { + aKey[i] = (obfsHexToInt(zKey[i * 2]) << 4) | obfsHexToInt(zKey[i * 2 + 1]); + } + if (i != kKeyBytes) { + NS_WARNING( + nsPrintfCString("invalid query parameter on %s: key=%s", zName, zKey) + .get()); + return SQLITE_CANTOPEN; + } + p = (ObfsFile*)pFile; + memset(p, 0, sizeof(*p)); + + auto encryptCipherStrategy = MakeUnique(); + auto decryptCipherStrategy = MakeUnique(); + + auto resetMethods = MakeScopeExit([pFile] { pFile->pMethods = nullptr; }); + + if (NS_WARN_IF(NS_FAILED(encryptCipherStrategy->Init( + CipherMode::Encrypt, Span{aKey, sizeof(aKey)}, + IPCStreamCipherStrategy::MakeBlockPrefix())))) { + return SQLITE_ERROR; + } + + if (NS_WARN_IF(NS_FAILED(decryptCipherStrategy->Init( + CipherMode::Decrypt, Span{aKey, sizeof(aKey)})))) { + return SQLITE_ERROR; + } + + pSubFile = ORIGFILE(pFile); + p->base.pMethods = &obfs_io_methods; + rc = pSubVfs->xOpen(pSubVfs, zName, pSubFile, flags, pOutFlags); + if (rc) { + return rc; + } + + resetMethods.release(); + + if (flags & (SQLITE_OPEN_WAL | SQLITE_OPEN_MAIN_JOURNAL)) { + sqlite3_file* pDb = sqlite3_database_file_object(zName); + p->pPartner = (ObfsFile*)pDb; + MOZ_ASSERT(p->pPartner->pPartner == nullptr); + p->pPartner->pPartner = p; + } + p->zFName = zName; + + p->encryptCipherStrategy = encryptCipherStrategy.release(); + p->decryptCipherStrategy = decryptCipherStrategy.release(); + + return SQLITE_OK; +} + +/* +** All other VFS methods are pass-thrus. +*/ +static int obfsDelete(sqlite3_vfs* pVfs, const char* zPath, int syncDir) { + return ORIGVFS(pVfs)->xDelete(ORIGVFS(pVfs), zPath, syncDir); +} +static int obfsAccess(sqlite3_vfs* pVfs, const char* zPath, int flags, + int* pResOut) { + return ORIGVFS(pVfs)->xAccess(ORIGVFS(pVfs), zPath, flags, pResOut); +} +static int obfsFullPathname(sqlite3_vfs* pVfs, const char* zPath, int nOut, + char* zOut) { + return ORIGVFS(pVfs)->xFullPathname(ORIGVFS(pVfs), zPath, nOut, zOut); +} +static void* obfsDlOpen(sqlite3_vfs* pVfs, const char* zPath) { + return ORIGVFS(pVfs)->xDlOpen(ORIGVFS(pVfs), zPath); +} +static void obfsDlError(sqlite3_vfs* pVfs, int nByte, char* zErrMsg) { + ORIGVFS(pVfs)->xDlError(ORIGVFS(pVfs), nByte, zErrMsg); +} +static void (*obfsDlSym(sqlite3_vfs* pVfs, void* p, const char* zSym))(void) { + return ORIGVFS(pVfs)->xDlSym(ORIGVFS(pVfs), p, zSym); +} +static void obfsDlClose(sqlite3_vfs* pVfs, void* pHandle) { + ORIGVFS(pVfs)->xDlClose(ORIGVFS(pVfs), pHandle); +} +static int obfsRandomness(sqlite3_vfs* pVfs, int nByte, char* zBufOut) { + return ORIGVFS(pVfs)->xRandomness(ORIGVFS(pVfs), nByte, zBufOut); +} +static int obfsSleep(sqlite3_vfs* pVfs, int nMicroseconds) { + return ORIGVFS(pVfs)->xSleep(ORIGVFS(pVfs), nMicroseconds); +} +static int obfsCurrentTime(sqlite3_vfs* pVfs, double* pTimeOut) { + return ORIGVFS(pVfs)->xCurrentTime(ORIGVFS(pVfs), pTimeOut); +} +static int obfsGetLastError(sqlite3_vfs* pVfs, int a, char* b) { + return ORIGVFS(pVfs)->xGetLastError(ORIGVFS(pVfs), a, b); +} +static int obfsCurrentTimeInt64(sqlite3_vfs* pVfs, sqlite3_int64* p) { + return ORIGVFS(pVfs)->xCurrentTimeInt64(ORIGVFS(pVfs), p); +} +static int obfsSetSystemCall(sqlite3_vfs* pVfs, const char* zName, + sqlite3_syscall_ptr pCall) { + return ORIGVFS(pVfs)->xSetSystemCall(ORIGVFS(pVfs), zName, pCall); +} +static sqlite3_syscall_ptr obfsGetSystemCall(sqlite3_vfs* pVfs, + const char* zName) { + return ORIGVFS(pVfs)->xGetSystemCall(ORIGVFS(pVfs), zName); +} +static const char* obfsNextSystemCall(sqlite3_vfs* pVfs, const char* zName) { + return ORIGVFS(pVfs)->xNextSystemCall(ORIGVFS(pVfs), zName); +} + +namespace mozilla::storage::obfsvfs { + +const char* GetVFSName() { return "obfsvfs"; } + +UniquePtr ConstructVFS(const char* aBaseVFSName) { + MOZ_ASSERT(aBaseVFSName); + + if (sqlite3_vfs_find(GetVFSName()) != nullptr) { + return nullptr; + } + sqlite3_vfs* const pOrig = sqlite3_vfs_find(aBaseVFSName); + if (pOrig == nullptr) { + return nullptr; + } + +#ifdef DEBUG + // If the VFS version is higher than the last known one, you should update + // this VFS adding appropriate methods for any methods added in the version + // change. + static constexpr int kLastKnownVfsVersion = 3; + MOZ_ASSERT(pOrig->iVersion <= kLastKnownVfsVersion); +#endif + + const sqlite3_vfs obfs_vfs = { + pOrig->iVersion, /* iVersion */ + static_cast(pOrig->szOsFile + sizeof(ObfsFile)), /* szOsFile */ + pOrig->mxPathname, /* mxPathname */ + nullptr, /* pNext */ + GetVFSName(), /* zName */ + pOrig, /* pAppData */ + obfsOpen, /* xOpen */ + obfsDelete, /* xDelete */ + obfsAccess, /* xAccess */ + obfsFullPathname, /* xFullPathname */ + obfsDlOpen, /* xDlOpen */ + obfsDlError, /* xDlError */ + obfsDlSym, /* xDlSym */ + obfsDlClose, /* xDlClose */ + obfsRandomness, /* xRandomness */ + obfsSleep, /* xSleep */ + obfsCurrentTime, /* xCurrentTime */ + obfsGetLastError, /* xGetLastError */ + obfsCurrentTimeInt64, /* xCurrentTimeInt64 */ + obfsSetSystemCall, /* xSetSystemCall */ + obfsGetSystemCall, /* xGetSystemCall */ + obfsNextSystemCall /* xNextSystemCall */ + }; + + return MakeUnique(obfs_vfs); +} + +already_AddRefed GetQuotaObjectForFile(sqlite3_file* pFile) { + return quotavfs::GetQuotaObjectForFile(ORIGFILE(pFile)); +} + +} // namespace mozilla::storage::obfsvfs diff --git a/storage/ObfuscatingVFS.h b/storage/ObfuscatingVFS.h new file mode 100644 index 0000000000..24d0e40a40 --- /dev/null +++ b/storage/ObfuscatingVFS.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 STORAGE_OBFUSCATINGVFS_H_ +#define STORAGE_OBFUSCATINGVFS_H_ + +#include "mozilla/UniquePtr.h" + +struct sqlite3_vfs; +struct sqlite3_file; + +template +struct already_AddRefed; + +namespace mozilla::dom::quota { +class QuotaObject; +} + +namespace mozilla::storage::obfsvfs { + +const char* GetVFSName(); + +UniquePtr ConstructVFS(const char* aBaseVFSName); + +already_AddRefed GetQuotaObjectForFile( + sqlite3_file* pFile); + +} // namespace mozilla::storage::obfsvfs + +#endif // STORAGE_OBFUSCATINGVFS_H_ diff --git a/storage/QuotaVFS.cpp b/storage/QuotaVFS.cpp new file mode 100644 index 0000000000..b9490ff0d5 --- /dev/null +++ b/storage/QuotaVFS.cpp @@ -0,0 +1,621 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "QuotaVFS.h" + +#include "mozilla/dom/quota/PersistenceType.h" +#include "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "mozilla/StaticPrefs_storage.h" +#include "nsDirectoryServiceDefs.h" +#include "nsEscape.h" +#include "sqlite3.h" + +#if defined(XP_WIN) || defined(XP_UNIX) +# include "mozilla/StaticPrefs_dom.h" +#endif + +// The last VFS version for which this file has been updated. +#define LAST_KNOWN_VFS_VERSION 3 + +// The last io_methods version for which this file has been updated. +#define LAST_KNOWN_IOMETHODS_VERSION 3 + +namespace { + +using namespace mozilla; +using namespace mozilla::dom::quota; + +struct QuotaFile { + // Base class. Must be first + sqlite3_file base; + + // quota object for this file + RefPtr quotaObject; + + // The chunk size for this file. See the documentation for + // sqlite3_file_control() and FCNTL_CHUNK_SIZE. + int fileChunkSize; + + // This contains the vfs that actually does work + sqlite3_file pReal[1]; +}; + +already_AddRefed GetQuotaObjectFromName(const char* zName) { + MOZ_ASSERT(zName); + + const char* directoryLockIdParam = + sqlite3_uri_parameter(zName, "directoryLockId"); + if (!directoryLockIdParam) { + return nullptr; + } + + nsresult rv; + const int64_t directoryLockId = + nsDependentCString(directoryLockIdParam).ToInteger64(&rv); + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv)); + + QuotaManager* quotaManager = QuotaManager::Get(); + MOZ_ASSERT(quotaManager); + + return quotaManager->GetQuotaObject(directoryLockId, + NS_ConvertUTF8toUTF16(zName)); +} + +void MaybeEstablishQuotaControl(const char* zName, QuotaFile* pFile, + int flags) { + MOZ_ASSERT(pFile); + MOZ_ASSERT(!pFile->quotaObject); + + if (!(flags & (SQLITE_OPEN_URI | SQLITE_OPEN_WAL))) { + return; + } + pFile->quotaObject = GetQuotaObjectFromName(zName); +} + +/* +** Close a QuotaFile. +*/ +int QuotaClose(sqlite3_file* pFile) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xClose(p->pReal); + if (rc == SQLITE_OK) { + delete p->base.pMethods; + p->base.pMethods = nullptr; + p->quotaObject = nullptr; +#ifdef DEBUG + p->fileChunkSize = 0; +#endif + } + return rc; +} + +/* +** Read data from a QuotaFile. +*/ +int QuotaRead(sqlite3_file* pFile, void* zBuf, int iAmt, sqlite_int64 iOfst) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xRead(p->pReal, zBuf, iAmt, iOfst); + return rc; +} + +/* +** Return the current file-size of a QuotaFile. +*/ +int QuotaFileSize(sqlite3_file* pFile, sqlite_int64* pSize) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xFileSize(p->pReal, pSize); + return rc; +} + +/* +** Write data to a QuotaFile. +*/ +int QuotaWrite(sqlite3_file* pFile, const void* zBuf, int iAmt, + sqlite_int64 iOfst) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + if (p->quotaObject) { + MOZ_ASSERT(INT64_MAX - iOfst >= iAmt); + if (!p->quotaObject->MaybeUpdateSize(iOfst + iAmt, /* aTruncate */ false)) { + return SQLITE_FULL; + } + } + rc = p->pReal->pMethods->xWrite(p->pReal, zBuf, iAmt, iOfst); + if (p->quotaObject && rc != SQLITE_OK) { + NS_WARNING( + "xWrite failed on a quota-controlled file, attempting to " + "update its current size..."); + sqlite_int64 currentSize; + if (QuotaFileSize(pFile, ¤tSize) == SQLITE_OK) { + DebugOnly res = + p->quotaObject->MaybeUpdateSize(currentSize, /* aTruncate */ true); + MOZ_ASSERT(res); + } + } + return rc; +} + +/* +** Truncate a QuotaFile. +*/ +int QuotaTruncate(sqlite3_file* pFile, sqlite_int64 size) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + if (p->quotaObject) { + if (p->fileChunkSize > 0) { + // Round up to the smallest multiple of the chunk size that will hold all + // the data. + size = + ((size + p->fileChunkSize - 1) / p->fileChunkSize) * p->fileChunkSize; + } + if (!p->quotaObject->MaybeUpdateSize(size, /* aTruncate */ true)) { + return SQLITE_FULL; + } + } + rc = p->pReal->pMethods->xTruncate(p->pReal, size); + if (p->quotaObject) { + if (rc == SQLITE_OK) { +#ifdef DEBUG + // Make sure xTruncate set the size exactly as we calculated above. + sqlite_int64 newSize; + MOZ_ASSERT(QuotaFileSize(pFile, &newSize) == SQLITE_OK); + MOZ_ASSERT(newSize == size); +#endif + } else { + NS_WARNING( + "xTruncate failed on a quota-controlled file, attempting to " + "update its current size..."); + if (QuotaFileSize(pFile, &size) == SQLITE_OK) { + DebugOnly res = + p->quotaObject->MaybeUpdateSize(size, /* aTruncate */ true); + MOZ_ASSERT(res); + } + } + } + return rc; +} + +/* +** Sync a QuotaFile. +*/ +int QuotaSync(sqlite3_file* pFile, int flags) { + QuotaFile* p = (QuotaFile*)pFile; + return p->pReal->pMethods->xSync(p->pReal, flags); +} + +/* +** Lock a QuotaFile. +*/ +int QuotaLock(sqlite3_file* pFile, int eLock) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xLock(p->pReal, eLock); + return rc; +} + +/* +** Unlock a QuotaFile. +*/ +int QuotaUnlock(sqlite3_file* pFile, int eLock) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xUnlock(p->pReal, eLock); + return rc; +} + +/* +** Check if another file-handle holds a RESERVED lock on a QuotaFile. +*/ +int QuotaCheckReservedLock(sqlite3_file* pFile, int* pResOut) { + QuotaFile* p = (QuotaFile*)pFile; + int rc = p->pReal->pMethods->xCheckReservedLock(p->pReal, pResOut); + return rc; +} + +/* +** File control method. For custom operations on a QuotaFile. +*/ +int QuotaFileControl(sqlite3_file* pFile, int op, void* pArg) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + // Hook SQLITE_FCNTL_SIZE_HINT for quota-controlled files and do the necessary + // work before passing to the SQLite VFS. + if (op == SQLITE_FCNTL_SIZE_HINT && p->quotaObject) { + sqlite3_int64 hintSize = *static_cast(pArg); + sqlite3_int64 currentSize; + rc = QuotaFileSize(pFile, ¤tSize); + if (rc != SQLITE_OK) { + return rc; + } + if (hintSize > currentSize) { + rc = QuotaTruncate(pFile, hintSize); + if (rc != SQLITE_OK) { + return rc; + } + } + } + rc = p->pReal->pMethods->xFileControl(p->pReal, op, pArg); + // Grab the file chunk size after the SQLite VFS has approved. + if (op == SQLITE_FCNTL_CHUNK_SIZE && rc == SQLITE_OK) { + p->fileChunkSize = *static_cast(pArg); + } +#ifdef DEBUG + if (op == SQLITE_FCNTL_SIZE_HINT && p->quotaObject && rc == SQLITE_OK) { + sqlite3_int64 hintSize = *static_cast(pArg); + if (p->fileChunkSize > 0) { + hintSize = ((hintSize + p->fileChunkSize - 1) / p->fileChunkSize) * + p->fileChunkSize; + } + sqlite3_int64 currentSize; + MOZ_ASSERT(QuotaFileSize(pFile, ¤tSize) == SQLITE_OK); + MOZ_ASSERT(currentSize >= hintSize); + } +#endif + return rc; +} + +/* +** Return the sector-size in bytes for a QuotaFile. +*/ +int QuotaSectorSize(sqlite3_file* pFile) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xSectorSize(p->pReal); + return rc; +} + +/* +** Return the device characteristic flags supported by a QuotaFile. +*/ +int QuotaDeviceCharacteristics(sqlite3_file* pFile) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xDeviceCharacteristics(p->pReal); + return rc; +} + +/* +** Shared-memory operations. +*/ +int QuotaShmLock(sqlite3_file* pFile, int ofst, int n, int flags) { + QuotaFile* p = (QuotaFile*)pFile; + return p->pReal->pMethods->xShmLock(p->pReal, ofst, n, flags); +} + +int QuotaShmMap(sqlite3_file* pFile, int iRegion, int szRegion, int isWrite, + void volatile** pp) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xShmMap(p->pReal, iRegion, szRegion, isWrite, pp); + return rc; +} + +void QuotaShmBarrier(sqlite3_file* pFile) { + QuotaFile* p = (QuotaFile*)pFile; + p->pReal->pMethods->xShmBarrier(p->pReal); +} + +int QuotaShmUnmap(sqlite3_file* pFile, int delFlag) { + QuotaFile* p = (QuotaFile*)pFile; + int rc; + rc = p->pReal->pMethods->xShmUnmap(p->pReal, delFlag); + return rc; +} + +int QuotaFetch(sqlite3_file* pFile, sqlite3_int64 iOff, int iAmt, void** pp) { + QuotaFile* p = (QuotaFile*)pFile; + MOZ_ASSERT(p->pReal->pMethods->iVersion >= 3); + return p->pReal->pMethods->xFetch(p->pReal, iOff, iAmt, pp); +} + +int QuotaUnfetch(sqlite3_file* pFile, sqlite3_int64 iOff, void* pResOut) { + QuotaFile* p = (QuotaFile*)pFile; + MOZ_ASSERT(p->pReal->pMethods->iVersion >= 3); + return p->pReal->pMethods->xUnfetch(p->pReal, iOff, pResOut); +} + +int QuotaOpen(sqlite3_vfs* vfs, const char* zName, sqlite3_file* pFile, + int flags, int* pOutFlags) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + int rc; + QuotaFile* p = (QuotaFile*)pFile; + + MaybeEstablishQuotaControl(zName, p, flags); + + rc = orig_vfs->xOpen(orig_vfs, zName, p->pReal, flags, pOutFlags); + if (rc != SQLITE_OK) return rc; + if (p->pReal->pMethods) { + sqlite3_io_methods* pNew = new sqlite3_io_methods; + const sqlite3_io_methods* pSub = p->pReal->pMethods; + memset(pNew, 0, sizeof(*pNew)); + // If the io_methods version is higher than the last known one, you should + // update this VFS adding appropriate IO methods for any methods added in + // the version change. + pNew->iVersion = pSub->iVersion; + MOZ_ASSERT(pNew->iVersion <= LAST_KNOWN_IOMETHODS_VERSION); + pNew->xClose = QuotaClose; + pNew->xRead = QuotaRead; + pNew->xWrite = QuotaWrite; + pNew->xTruncate = QuotaTruncate; + pNew->xSync = QuotaSync; + pNew->xFileSize = QuotaFileSize; + pNew->xLock = QuotaLock; + pNew->xUnlock = QuotaUnlock; + pNew->xCheckReservedLock = QuotaCheckReservedLock; + pNew->xFileControl = QuotaFileControl; + pNew->xSectorSize = QuotaSectorSize; + pNew->xDeviceCharacteristics = QuotaDeviceCharacteristics; + if (pNew->iVersion >= 2) { + // Methods added in version 2. + pNew->xShmMap = pSub->xShmMap ? QuotaShmMap : nullptr; + pNew->xShmLock = pSub->xShmLock ? QuotaShmLock : nullptr; + pNew->xShmBarrier = pSub->xShmBarrier ? QuotaShmBarrier : nullptr; + pNew->xShmUnmap = pSub->xShmUnmap ? QuotaShmUnmap : nullptr; + } + if (pNew->iVersion >= 3) { + // Methods added in version 3. + // SQLite 3.7.17 calls these methods without checking for nullptr first, + // so we always define them. Verify that we're not going to call + // nullptrs, though. + MOZ_ASSERT(pSub->xFetch); + pNew->xFetch = QuotaFetch; + MOZ_ASSERT(pSub->xUnfetch); + pNew->xUnfetch = QuotaUnfetch; + } + pFile->pMethods = pNew; + } + return rc; +} + +int QuotaDelete(sqlite3_vfs* vfs, const char* zName, int syncDir) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + int rc; + RefPtr quotaObject; + + if (StringEndsWith(nsDependentCString(zName), "-wal"_ns)) { + quotaObject = GetQuotaObjectFromName(zName); + } + + rc = orig_vfs->xDelete(orig_vfs, zName, syncDir); + if (rc == SQLITE_OK && quotaObject) { + MOZ_ALWAYS_TRUE(quotaObject->MaybeUpdateSize(0, /* aTruncate */ true)); + } + + return rc; +} + +int QuotaAccess(sqlite3_vfs* vfs, const char* zName, int flags, int* pResOut) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xAccess(orig_vfs, zName, flags, pResOut); +} + +int QuotaFullPathname(sqlite3_vfs* vfs, const char* zName, int nOut, + char* zOut) { +#if defined(XP_WIN) + // SQLite uses GetFullPathnameW which also normailizes file path. If a file + // component ends with a dot, it would be removed. However, it's not desired. + // + // And that would result SQLite uses wrong database and quotaObject. + // Note that we are safe to avoid the GetFullPathnameW call for \\?\ prefixed + // paths. + // And note that this hack will be removed once the issue is fixed directly in + // SQLite. + + // zName that starts with "//?/" is the case when a file URI was passed and + // zName that starts with "\\?\" is the case when a normal path was passed + // (not file URI). + if (StaticPrefs::dom_quotaManager_overrideXFullPathname() && + ((zName[0] == '/' && zName[1] == '/' && zName[2] == '?' && + zName[3] == '/') || + (zName[0] == '\\' && zName[1] == '\\' && zName[2] == '?' && + zName[3] == '\\'))) { + MOZ_ASSERT(nOut >= vfs->mxPathname); + MOZ_ASSERT(static_cast(nOut) > strlen(zName)); + + size_t index = 0; + while (zName[index] != '\0') { + if (zName[index] == '/') { + zOut[index] = '\\'; + } else { + zOut[index] = zName[index]; + } + + index++; + } + zOut[index] = '\0'; + + return SQLITE_OK; + } +#elif defined(XP_UNIX) + // SQLite canonicalizes (resolves path components) file paths on Unix which + // doesn't work well with file path sanity checks in quota manager. This is + // especially a problem on mac where /var is a symlink to /private/var. + // Since QuotaVFS is used only by quota clients which never access databases + // outside of PROFILE/storage, we override Unix xFullPathname with own + // implementation that doesn't do any canonicalization. + + if (StaticPrefs::dom_quotaManager_overrideXFullPathnameUnix()) { + if (nOut < 0) { + // Match the return code used by SQLite's xFullPathname implementation + // here and below. + return SQLITE_CANTOPEN; + } + + QM_TRY_INSPECT( + const auto& path, ([&zName]() -> Result { + NS_ConvertUTF8toUTF16 name(zName); + + if (name.First() == '/') { + return std::move(name); + } + + QM_TRY_INSPECT(const auto& file, + MOZ_TO_RESULT_INVOKE_TYPED(nsCOMPtr, + NS_GetSpecialDirectory, + NS_OS_CURRENT_WORKING_DIR)); + + QM_TRY(MOZ_TO_RESULT(file->Append(name))); + + QM_TRY_RETURN( + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, file, GetPath)); + }()), + SQLITE_CANTOPEN); + + QM_TRY_INSPECT(const auto& quotaFile, QM_NewLocalFile(path), + SQLITE_CANTOPEN); + + QM_TRY_INSPECT( + const auto& quotaPath, + MOZ_TO_RESULT_INVOKE_MEMBER_TYPED(nsString, quotaFile, GetPath), + SQLITE_CANTOPEN); + + NS_ConvertUTF16toUTF8 sqlitePath(quotaPath); + + if (sqlitePath.Length() > (unsigned int)nOut) { + return SQLITE_CANTOPEN; + } + + nsCharTraits::copy(zOut, sqlitePath.get(), sqlitePath.Length()); + zOut[sqlitePath.Length()] = '\0'; + + return SQLITE_OK; + } +#endif + + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xFullPathname(orig_vfs, zName, nOut, zOut); +} + +void* QuotaDlOpen(sqlite3_vfs* vfs, const char* zFilename) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xDlOpen(orig_vfs, zFilename); +} + +void QuotaDlError(sqlite3_vfs* vfs, int nByte, char* zErrMsg) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + orig_vfs->xDlError(orig_vfs, nByte, zErrMsg); +} + +void (*QuotaDlSym(sqlite3_vfs* vfs, void* pHdle, const char* zSym))(void) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xDlSym(orig_vfs, pHdle, zSym); +} + +void QuotaDlClose(sqlite3_vfs* vfs, void* pHandle) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + orig_vfs->xDlClose(orig_vfs, pHandle); +} + +int QuotaRandomness(sqlite3_vfs* vfs, int nByte, char* zOut) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xRandomness(orig_vfs, nByte, zOut); +} + +int QuotaSleep(sqlite3_vfs* vfs, int microseconds) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xSleep(orig_vfs, microseconds); +} + +int QuotaCurrentTime(sqlite3_vfs* vfs, double* prNow) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xCurrentTime(orig_vfs, prNow); +} + +int QuotaGetLastError(sqlite3_vfs* vfs, int nBuf, char* zBuf) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xGetLastError(orig_vfs, nBuf, zBuf); +} + +int QuotaCurrentTimeInt64(sqlite3_vfs* vfs, sqlite3_int64* piNow) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xCurrentTimeInt64(orig_vfs, piNow); +} + +static int QuotaSetSystemCall(sqlite3_vfs* vfs, const char* zName, + sqlite3_syscall_ptr pFunc) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xSetSystemCall(orig_vfs, zName, pFunc); +} + +static sqlite3_syscall_ptr QuotaGetSystemCall(sqlite3_vfs* vfs, + const char* zName) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xGetSystemCall(orig_vfs, zName); +} + +static const char* QuotaNextSystemCall(sqlite3_vfs* vfs, const char* zName) { + sqlite3_vfs* orig_vfs = static_cast(vfs->pAppData); + return orig_vfs->xNextSystemCall(orig_vfs, zName); +} + +} // namespace + +namespace mozilla::storage::quotavfs { + +const char* GetVFSName() { return "quotavfs"; } + +UniquePtr ConstructVFS(const char* aBaseVFSName) { + MOZ_ASSERT(aBaseVFSName); + + if (sqlite3_vfs_find(GetVFSName()) != nullptr) { + return nullptr; + } + sqlite3_vfs* vfs = sqlite3_vfs_find(aBaseVFSName); + if (!vfs) { + return nullptr; + } + + auto qvfs = MakeUnique(); + memset(qvfs.get(), 0, sizeof(::sqlite3_vfs)); + // If the VFS version is higher than the last known one, you should update + // this VFS adding appropriate methods for any methods added in the version + // change. + qvfs->iVersion = vfs->iVersion; + MOZ_ASSERT(vfs->iVersion <= LAST_KNOWN_VFS_VERSION); + qvfs->szOsFile = static_cast(sizeof(QuotaFile) - sizeof(sqlite3_file) + + vfs->szOsFile); + qvfs->mxPathname = vfs->mxPathname; + qvfs->zName = GetVFSName(); + qvfs->pAppData = vfs; + qvfs->xOpen = QuotaOpen; + qvfs->xDelete = QuotaDelete; + qvfs->xAccess = QuotaAccess; + qvfs->xFullPathname = QuotaFullPathname; + qvfs->xDlOpen = QuotaDlOpen; + qvfs->xDlError = QuotaDlError; + qvfs->xDlSym = QuotaDlSym; + qvfs->xDlClose = QuotaDlClose; + qvfs->xRandomness = QuotaRandomness; + qvfs->xSleep = QuotaSleep; + qvfs->xCurrentTime = QuotaCurrentTime; + qvfs->xGetLastError = QuotaGetLastError; + if (qvfs->iVersion >= 2) { + // Methods added in version 2. + qvfs->xCurrentTimeInt64 = QuotaCurrentTimeInt64; + } + if (qvfs->iVersion >= 3) { + // Methods added in version 3. + qvfs->xSetSystemCall = QuotaSetSystemCall; + qvfs->xGetSystemCall = QuotaGetSystemCall; + qvfs->xNextSystemCall = QuotaNextSystemCall; + } + return qvfs; +} + +already_AddRefed GetQuotaObjectForFile(sqlite3_file* pFile) { + MOZ_ASSERT(pFile); + + QuotaFile* p = (QuotaFile*)pFile; + RefPtr result = p->quotaObject; + return result.forget(); +} + +} // namespace mozilla::storage::quotavfs diff --git a/storage/QuotaVFS.h b/storage/QuotaVFS.h new file mode 100644 index 0000000000..32fb8ac3a8 --- /dev/null +++ b/storage/QuotaVFS.h @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 STORAGE_QUOTAVFS_H_ +#define STORAGE_QUOTAVFS_H_ + +#include "mozilla/UniquePtr.h" + +struct sqlite3_vfs; +struct sqlite3_file; + +template +struct already_AddRefed; + +namespace mozilla::dom::quota { +class QuotaObject; +} + +namespace mozilla::storage::quotavfs { + +const char* GetVFSName(); + +UniquePtr ConstructVFS(const char* aBaseVFSName); + +already_AddRefed GetQuotaObjectForFile( + sqlite3_file* pFile); + +} // namespace mozilla::storage::quotavfs + +#endif // STORAGE_QUOTAVFS_H_ diff --git a/storage/ReadOnlyNoLockVFS.cpp b/storage/ReadOnlyNoLockVFS.cpp new file mode 100644 index 0000000000..9a820085ed --- /dev/null +++ b/storage/ReadOnlyNoLockVFS.cpp @@ -0,0 +1,130 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 VFS is built on top of the (unix|win32)-none, but it additionally + * sets any opened file as immutable, that allows to also open in read-only + * mode databases using WAL, or other journals that need auxiliary files, when + * such files cannot be created. + * This is useful when trying to read from third-party databases, avoiding any + * risk of creating auxiliary files (e.g. journals). + * It can only be used on read-only connections, because being a no-lock VFS + * it would be trivial to corrupt the data. + */ + +#include "nsDebug.h" +#include "sqlite3.h" + +#define ORIGVFS(p) ((sqlite3_vfs*)((p)->pAppData)) + +#if defined(XP_WIN) +# define BASE_VFS "win32-none" +#else +# define BASE_VFS "unix-none" +#endif + +#define VFS_NAME "readonly-immutable-nolock" + +namespace { + +static int vfsOpen(sqlite3_vfs* vfs, const char* zName, sqlite3_file* pFile, + int flags, int* pOutFlags) { + if ((flags & SQLITE_OPEN_READONLY) == 0) { + // This is not done to be used in readwrite connections. + return SQLITE_CANTOPEN; + } + + sqlite3_vfs* pOrigVfs = ORIGVFS(vfs); + int rc = pOrigVfs->xOpen(pOrigVfs, zName, pFile, flags, pOutFlags); + if (rc != SQLITE_OK) { + return rc; + } + + const sqlite3_io_methods* pOrigMethods = pFile->pMethods; + + // If the IO version is higher than the last known one, you should update + // this IO adding appropriate methods for any methods added in the version + // change. + MOZ_ASSERT(pOrigMethods->iVersion <= 3); + + static const sqlite3_io_methods vfs_io_methods = { + pOrigMethods->iVersion, /* iVersion */ + pOrigMethods->xClose, /* xClose */ + pOrigMethods->xRead, /* xRead */ + pOrigMethods->xWrite, /* xWrite */ + pOrigMethods->xTruncate, /* xTruncate */ + pOrigMethods->xSync, /* xSync */ + pOrigMethods->xFileSize, /* xFileSize */ + pOrigMethods->xLock, /* xLock */ + pOrigMethods->xUnlock, /* xUnlock */ + pOrigMethods->xCheckReservedLock, /* xCheckReservedLock */ + pOrigMethods->xFileControl, /* xFileControl */ + pOrigMethods->xSectorSize, /* xSectorSize */ + [](sqlite3_file*) { + return SQLITE_IOCAP_IMMUTABLE; + }, /* xDeviceCharacteristics */ + pOrigMethods->xShmMap, /* xShmMap */ + pOrigMethods->xShmLock, /* xShmLock */ + pOrigMethods->xShmBarrier, /* xShmBarrier */ + pOrigMethods->xShmUnmap, /* xShmUnmap */ + pOrigMethods->xFetch, /* xFetch */ + pOrigMethods->xUnfetch /* xUnfetch */ + }; + pFile->pMethods = &vfs_io_methods; + if (pOutFlags) { + *pOutFlags = flags; + } + + return SQLITE_OK; +} + +} // namespace + +namespace mozilla::storage { + +UniquePtr ConstructReadOnlyNoLockVFS() { + if (sqlite3_vfs_find(VFS_NAME) != nullptr) { + return nullptr; + } + sqlite3_vfs* pOrigVfs = sqlite3_vfs_find(BASE_VFS); + if (!pOrigVfs) { + return nullptr; + } + + // If the VFS version is higher than the last known one, you should update + // this VFS adding appropriate methods for any methods added in the version + // change. + MOZ_ASSERT(pOrigVfs->iVersion <= 3); + + static const sqlite3_vfs vfs = { + pOrigVfs->iVersion, /* iVersion */ + pOrigVfs->szOsFile, /* szOsFile */ + pOrigVfs->mxPathname, /* mxPathname */ + nullptr, /* pNext */ + VFS_NAME, /* zName */ + pOrigVfs, /* pAppData */ + vfsOpen, /* xOpen */ + pOrigVfs->xDelete, /* xDelete */ + pOrigVfs->xAccess, /* xAccess */ + pOrigVfs->xFullPathname, /* xFullPathname */ + pOrigVfs->xDlOpen, /* xDlOpen */ + pOrigVfs->xDlError, /* xDlError */ + pOrigVfs->xDlSym, /* xDlSym */ + pOrigVfs->xDlClose, /* xDlClose */ + pOrigVfs->xRandomness, /* xRandomness */ + pOrigVfs->xSleep, /* xSleep */ + pOrigVfs->xCurrentTime, /* xCurrentTime */ + pOrigVfs->xGetLastError, /* xGetLastError */ + pOrigVfs->xCurrentTimeInt64, /* xCurrentTimeInt64 */ + pOrigVfs->xSetSystemCall, /* xSetSystemCall */ + pOrigVfs->xGetSystemCall, /* xGetSystemCall */ + pOrigVfs->xNextSystemCall /* xNextSystemCall */ + }; + + return MakeUnique(vfs); +} + +} // namespace mozilla::storage diff --git a/storage/SQLCollations.cpp b/storage/SQLCollations.cpp new file mode 100644 index 0000000000..3cbeafe5d0 --- /dev/null +++ b/storage/SQLCollations.cpp @@ -0,0 +1,183 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/ArrayUtils.h" +#include "mozilla/intl/Collator.h" + +#include "SQLCollations.h" + +using mozilla::intl::Collator; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Functions + +namespace { + +/** + * Helper function for the UTF-8 locale collations. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2 as provided by SQLite. It + * must be a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1 as provided by SQLite. It + * must be a non-null-terminated char* buffer. + * @param aSensitivity + * The sorting sensitivity. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationHelper8(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2, + Collator::Sensitivity aSensitivity) { + NS_ConvertUTF8toUTF16 str1(static_cast(aStr1), aLen1); + NS_ConvertUTF8toUTF16 str2(static_cast(aStr2), aLen2); + Service* serv = static_cast(aService); + return serv->localeCompareStrings(str1, str2, aSensitivity); +} + +/** + * Helper function for the UTF-16 locale collations. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2 as provided by SQLite. It + * must be a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1 as provided by SQLite. It + * must be a non-null-terminated char16_t* buffer. + * @param aSensitivity + * The sorting sensitivity. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationHelper16(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2, + Collator::Sensitivity aSensitivity) { + const char16_t* buf1 = static_cast(aStr1); + const char16_t* buf2 = static_cast(aStr2); + + // The second argument to the nsDependentSubstring constructor is exclusive: + // It points to the char16_t immediately following the last one in the target + // substring. Since aLen1 and aLen2 are in bytes, divide by sizeof(char16_t) + // so that the pointer arithmetic is correct. + nsDependentSubstring str1(buf1, buf1 + (aLen1 / sizeof(char16_t))); + nsDependentSubstring str2(buf2, buf2 + (aLen2 / sizeof(char16_t))); + Service* serv = static_cast(aService); + return serv->localeCompareStrings(str1, str2, aSensitivity); +} + +// This struct is used only by registerCollations below, but ISO C++98 forbids +// instantiating a template dependent on a locally-defined type. Boo-urns! +struct Collations { + const char* zName; + int enc; + int (*xCompare)(void*, int, const void*, int, const void*); +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Exposed Functions + +int registerCollations(sqlite3* aDB, Service* aService) { + Collations collations[] = { + {"locale", SQLITE_UTF8, localeCollation8}, + {"locale_case_sensitive", SQLITE_UTF8, localeCollationCaseSensitive8}, + {"locale_accent_sensitive", SQLITE_UTF8, localeCollationAccentSensitive8}, + {"locale_case_accent_sensitive", SQLITE_UTF8, + localeCollationCaseAccentSensitive8}, + {"locale", SQLITE_UTF16, localeCollation16}, + {"locale_case_sensitive", SQLITE_UTF16, localeCollationCaseSensitive16}, + {"locale_accent_sensitive", SQLITE_UTF16, + localeCollationAccentSensitive16}, + {"locale_case_accent_sensitive", SQLITE_UTF16, + localeCollationCaseAccentSensitive16}, + }; + + int rv = SQLITE_OK; + for (size_t i = 0; SQLITE_OK == rv && i < ArrayLength(collations); ++i) { + struct Collations* p = &collations[i]; + rv = ::sqlite3_create_collation(aDB, p->zName, p->enc, aService, + p->xCompare); + } + + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// SQL Collations + +int localeCollation8(void* aService, int aLen1, const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Base); +} + +int localeCollationCaseSensitive8(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2) { + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Case); +} + +int localeCollationAccentSensitive8(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Accent); +} + +int localeCollationCaseAccentSensitive8(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper8(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Variant); +} + +int localeCollation16(void* aService, int aLen1, const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Base); +} + +int localeCollationCaseSensitive16(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2) { + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Case); +} + +int localeCollationAccentSensitive16(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Accent); +} + +int localeCollationCaseAccentSensitive16(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2) { + return localeCollationHelper16(aService, aLen1, aStr1, aLen2, aStr2, + Collator::Sensitivity::Variant); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/SQLCollations.h b/storage/SQLCollations.h new file mode 100644 index 0000000000..1845084621 --- /dev/null +++ b/storage/SQLCollations.h @@ -0,0 +1,229 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_SQLCollations_h +#define mozilla_storage_SQLCollations_h + +#include "mozStorageService.h" +#include "nscore.h" +#include "nsString.h" + +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +/** + * Registers the collating sequences declared here with the specified + * database and Service. + * + * @param aDB + * The database we'll be registering the collations with. + * @param aService + * The Service that owns the collator used by our collations. + * @return the SQLite status code indicating success or failure. + */ +int registerCollations(sqlite3* aDB, Service* aService); + +//////////////////////////////////////////////////////////////////////////////// +//// Predefined Functions + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case- and accent-insensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollation8(void* aService, int aLen1, const void* aStr1, int aLen2, + const void* aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case-sensitive and accent-insensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseSensitive8(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case-insensitive and accent-sensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationAccentSensitive8(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2); + +/** + * Custom UTF-8 collating sequence that respects the application's locale. + * Comparison is case- and accent-sensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @param aLen2 + * The number of bytes in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseAccentSensitive8(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case- and accent-insensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollation16(void* aService, int aLen1, const void* aStr1, int aLen2, + const void* aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case-sensitive and accent-insensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseSensitive16(void* aService, int aLen1, const void* aStr1, + int aLen2, const void* aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case-insensitive and accent-sensitive. This is called by + * SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationAccentSensitive16(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2); + +/** + * Custom UTF-16 collating sequence that respects the application's locale. + * Comparison is case- and accent-sensitive. This is called by SQLite. + * + * @param aService + * The Service that owns the collator used by this collation. + * @param aLen1 + * The number of bytes (not characters) in aStr1. + * @param aStr1 + * The string to be compared against aStr2. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @param aLen2 + * The number of bytes (not characters) in aStr2. + * @param aStr2 + * The string to be compared against aStr1. It will be passed in by + * SQLite as a non-null-terminated char16_t* buffer. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative number. + * If aStr1 > aStr2, returns a positive number. If aStr1 == aStr2, + * returns 0. + */ +int localeCollationCaseAccentSensitive16(void* aService, int aLen1, + const void* aStr1, int aLen2, + const void* aStr2); + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_SQLCollations_h diff --git a/storage/SQLiteMutex.h b/storage/SQLiteMutex.h new file mode 100644 index 0000000000..b7198b1912 --- /dev/null +++ b/storage/SQLiteMutex.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_SQLiteMutex_h_ +#define mozilla_storage_SQLiteMutex_h_ + +#include "mozilla/BlockingResourceBase.h" +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +/** + * Wrapper class for sqlite3_mutexes. To be used whenever we want to use a + * sqlite3_mutex. + * + * @warning Never EVER wrap the same sqlite3_mutex with a different SQLiteMutex. + * If you do this, you void the deadlock detector's warranty! + */ +class SQLiteMutex : private BlockingResourceBase { + public: + /** + * Constructs a wrapper for a sqlite3_mutex that has deadlock detecting. + * + * @param aName + * A name which can be used to reference this mutex. + */ + explicit SQLiteMutex(const char* aName) + : BlockingResourceBase(aName, eMutex), mMutex(nullptr) {} + + /** + * Sets the mutex that we are wrapping. We generally do not have access to + * our mutex at class construction, so we have to set it once we get access to + * it. + * + * @param aMutex + * The sqlite3_mutex that we are going to wrap. + */ + void initWithMutex(sqlite3_mutex* aMutex) { + MOZ_ASSERT(aMutex, "You must pass in a valid mutex!"); + MOZ_ASSERT(!mMutex, "A mutex has already been set for this!"); + mMutex = aMutex; + } + + /** + * After a connection has been successfully closed, its mutex is a dangling + * pointer, and as such it should be destroyed. + */ + void destroy() { mMutex = NULL; } + + /** + * Acquires the mutex. + */ + void lock() { + MOZ_ASSERT(mMutex, "No mutex associated with this wrapper!"); +#if defined(DEBUG) + // While SQLite Mutexes may be recursive, in our own code we do not want to + // treat them as such. + CheckAcquire(); +#endif + + ::sqlite3_mutex_enter(mMutex); + +#if defined(DEBUG) + Acquire(); // Call is protected by us holding the mutex. +#endif + } + + /** + * Releases the mutex. + */ + void unlock() { + MOZ_ASSERT(mMutex, "No mutex associated with this wrapper!"); +#if defined(DEBUG) + // While SQLite Mutexes may be recursive, in our own code we do not want to + // treat them as such. + Release(); // Call is protected by us holding the mutex. +#endif + + ::sqlite3_mutex_leave(mMutex); + } + + /** + * Asserts that the current thread owns the mutex. + */ + void assertCurrentThreadOwns() { + MOZ_ASSERT(mMutex, "No mutex associated with this wrapper!"); + MOZ_ASSERT(::sqlite3_mutex_held(mMutex), + "Mutex is not held, but we expect it to be!"); + } + + /** + * Asserts that the current thread does not own the mutex. + */ + void assertNotCurrentThreadOwns() { + MOZ_ASSERT(mMutex, "No mutex associated with this wrapper!"); + MOZ_ASSERT(::sqlite3_mutex_notheld(mMutex), + "Mutex is held, but we expect it to not be!"); + } + + private: + sqlite3_mutex* mMutex; +}; + +/** + * Automatically acquires the mutex when it enters scope, and releases it when + * it leaves scope. + */ +class MOZ_STACK_CLASS SQLiteMutexAutoLock { + public: + explicit SQLiteMutexAutoLock(SQLiteMutex& aMutex) : mMutex(aMutex) { + mMutex.lock(); + } + + ~SQLiteMutexAutoLock() { mMutex.unlock(); } + + private: + SQLiteMutex& mMutex; +}; + +/** + * Automatically releases the mutex when it enters scope, and acquires it when + * it leaves scope. + */ +class MOZ_STACK_CLASS SQLiteMutexAutoUnlock { + public: + explicit SQLiteMutexAutoUnlock(SQLiteMutex& aMutex) : mMutex(aMutex) { + mMutex.unlock(); + } + + ~SQLiteMutexAutoUnlock() { mMutex.lock(); } + + private: + SQLiteMutex& mMutex; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_SQLiteMutex_h_ diff --git a/storage/StatementCache.h b/storage/StatementCache.h new file mode 100644 index 0000000000..3d07c4ef57 --- /dev/null +++ b/storage/StatementCache.h @@ -0,0 +1,129 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_StatementCache_h +#define mozilla_storage_StatementCache_h + +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStorageAsyncStatement.h" + +#include "nsHashKeys.h" +#include "nsInterfaceHashtable.h" + +namespace mozilla { +namespace storage { + +/** + * Class used to cache statements (mozIStorageStatement or + * mozIStorageAsyncStatement). + */ +template +class StatementCache { + public: + /** + * Constructor for the cache. + * + * @note a connection can have more than one cache. + * + * @param aConnection + * A reference to the nsCOMPtr for the connection this cache is to be + * used for. This nsCOMPtr must at least live as long as this class, + * otherwise crashes will happen. + */ + explicit StatementCache(nsCOMPtr& aConnection) + : mConnection(aConnection) {} + + /** + * Obtains a cached statement. If this statement is not yet created, it will + * be created and stored for later use. + * + * @param aQuery + * The SQL string (either a const char [] or nsACString) to get a + * cached query for. + * @return the cached statement, or null upon error. + */ + inline already_AddRefed GetCachedStatement( + const nsACString& aQuery) { + nsCOMPtr stmt; + if (!mCachedStatements.Get(aQuery, getter_AddRefs(stmt))) { + stmt = CreateStatement(aQuery); + NS_ENSURE_TRUE(stmt, nullptr); + + mCachedStatements.InsertOrUpdate(aQuery, stmt); + } + return stmt.forget(); + } + + template + MOZ_ALWAYS_INLINE already_AddRefed GetCachedStatement( + const char (&aQuery)[N]) { + nsDependentCString query(aQuery, N - 1); + return GetCachedStatement(query); + } + + /** + * Finalizes all cached statements so the database can be safely closed. The + * behavior of this cache is unspecified after this method is called. + */ + inline void FinalizeStatements() { + for (const auto& data : mCachedStatements.Values()) { + (void)data->Finalize(); + } + + // Clear the cache at this time too! + (void)mCachedStatements.Clear(); + } + + private: + inline already_AddRefed CreateStatement( + const nsACString& aQuery); + + nsInterfaceHashtable mCachedStatements; + nsCOMPtr& mConnection; +}; + +template <> +inline already_AddRefed +StatementCache::CreateStatement( + const nsACString& aQuery) { + NS_ENSURE_TRUE(mConnection, nullptr); + + nsCOMPtr stmt; + nsresult rv = mConnection->CreateStatement(aQuery, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { + nsCString error; + error.AppendLiteral("The statement '"); + error.Append(aQuery); + error.AppendLiteral("' failed to compile with the error message '"); + nsCString msg; + (void)mConnection->GetLastErrorString(msg); + error.Append(msg); + error.AppendLiteral("'."); + NS_ERROR(error.get()); + } + NS_ENSURE_SUCCESS(rv, nullptr); + + return stmt.forget(); +} + +template <> +inline already_AddRefed +StatementCache::CreateStatement( + const nsACString& aQuery) { + NS_ENSURE_TRUE(mConnection, nullptr); + + nsCOMPtr stmt; + nsresult rv = mConnection->CreateAsyncStatement(aQuery, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, nullptr); + + return stmt.forget(); +} + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_StatementCache_h diff --git a/storage/StorageBaseStatementInternal.cpp b/storage/StorageBaseStatementInternal.cpp new file mode 100644 index 0000000000..af7425c1cc --- /dev/null +++ b/storage/StorageBaseStatementInternal.cpp @@ -0,0 +1,221 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 "StorageBaseStatementInternal.h" + +#include "nsProxyRelease.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozStorageAsyncStatementExecution.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Classes + +/** + * Used to finalize an asynchronous statement on the background thread. + */ +class AsyncStatementFinalizer : public Runnable { + public: + /** + * Constructor for the event. + * + * @param aStatement + * We need the AsyncStatement to be able to get at the sqlite3_stmt; + * we only access/create it on the async event target. + * @param aConnection + * We need the connection to know what event target to release the + * statement on. We release the statement on that event target since + * releasing the statement might end up releasing the connection too. + */ + AsyncStatementFinalizer(StorageBaseStatementInternal* aStatement, + Connection* aConnection) + : Runnable("storage::AsyncStatementFinalizer"), + mStatement(aStatement), + mConnection(aConnection) {} + + NS_IMETHOD Run() override { + if (mStatement->mAsyncStatement) { + sqlite3_finalize(mStatement->mAsyncStatement); + mStatement->mAsyncStatement = nullptr; + } + + nsCOMPtr target(mConnection->eventTargetOpenedOn); + NS_ProxyRelease("AsyncStatementFinalizer::mStatement", target, + mStatement.forget()); + return NS_OK; + } + + private: + RefPtr mStatement; + RefPtr mConnection; +}; + +/** + * Finalize a sqlite3_stmt on the background thread for a statement whose + * destructor was invoked and the statement was non-null. + */ +class LastDitchSqliteStatementFinalizer : public Runnable { + public: + /** + * Event constructor. + * + * @param aConnection + * Used to keep the connection alive. If we failed to do this, it + * is possible that the statement going out of scope invoking us + * might have the last reference to the connection and so trigger + * an attempt to close the connection which is doomed to fail + * (because the asynchronous execution event target must exist which + * will trigger the failure case). + * @param aStatement + * The sqlite3_stmt to finalize. This object takes ownership / + * responsibility for the instance and all other references to it + * should be forgotten. + */ + LastDitchSqliteStatementFinalizer(RefPtr& aConnection, + sqlite3_stmt* aStatement) + : Runnable("storage::LastDitchSqliteStatementFinalizer"), + mConnection(aConnection), + mAsyncStatement(aStatement) { + MOZ_ASSERT(aConnection, "You must provide a Connection"); + } + + NS_IMETHOD Run() override { + (void)::sqlite3_finalize(mAsyncStatement); + mAsyncStatement = nullptr; + + nsCOMPtr target(mConnection->eventTargetOpenedOn); + (void)::NS_ProxyRelease("LastDitchSqliteStatementFinalizer::mConnection", + target, mConnection.forget()); + return NS_OK; + } + + private: + RefPtr mConnection; + sqlite3_stmt* mAsyncStatement; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +StorageBaseStatementInternal::StorageBaseStatementInternal() + : mNativeConnection(nullptr), mAsyncStatement(nullptr) {} + +void StorageBaseStatementInternal::asyncFinalize() { + nsIEventTarget* target = mDBConnection->getAsyncExecutionTarget(); + if (target) { + // Attempt to finalize asynchronously + nsCOMPtr event = + new AsyncStatementFinalizer(this, mDBConnection); + + // Dispatch. Note that dispatching can fail, typically if + // we have a race condition with asyncClose(). It's ok, + // let asyncClose() win. + (void)target->Dispatch(event, NS_DISPATCH_NORMAL); + } + // If we cannot get the background thread, + // mozStorageConnection::AsyncClose() has already been called and + // the statement either has been or will be cleaned up by + // internalClose(). +} + +void StorageBaseStatementInternal::destructorAsyncFinalize() { + if (!mAsyncStatement) return; + + if (IsOnCurrentSerialEventTarget(mDBConnection->eventTargetOpenedOn)) { + // If we are the owning event target (currently that means we're also the + // main thread), then we can get the async target and just dispatch to it. + nsIEventTarget* target = mDBConnection->getAsyncExecutionTarget(); + if (target) { + nsCOMPtr event = + new LastDitchSqliteStatementFinalizer(mDBConnection, mAsyncStatement); + (void)target->Dispatch(event, NS_DISPATCH_NORMAL); + } + } else { + // If we're not the owning event target, assume we're the async event + // target, and just run the statement. + nsCOMPtr event = + new LastDitchSqliteStatementFinalizer(mDBConnection, mAsyncStatement); + (void)event->Run(); + } + + // We might not be able to dispatch to the background thread, + // presumably because it is being shutdown. Since said shutdown will + // finalize the statement, we just need to clean-up around here. + mAsyncStatement = nullptr; +} + +NS_IMETHODIMP +StorageBaseStatementInternal::NewBindingParamsArray( + mozIStorageBindingParamsArray** _array) { + nsCOMPtr array = new BindingParamsArray(this); + NS_ENSURE_TRUE(array, NS_ERROR_OUT_OF_MEMORY); + + array.forget(_array); + return NS_OK; +} + +NS_IMETHODIMP +StorageBaseStatementInternal::ExecuteAsync( + mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _stmt) { + // We used to call Connection::ExecuteAsync but it takes a + // mozIStorageBaseStatement signature because it is also a public API. Since + // our 'this' has no static concept of mozIStorageBaseStatement and Connection + // would just QI it back across to a StorageBaseStatementInternal and the + // actual logic is very simple, we now roll our own. + nsTArray stmts(1); + StatementData data; + nsresult rv = getAsynchronousStatementData(data); + NS_ENSURE_SUCCESS(rv, rv); + stmts.AppendElement(data); + + // Dispatch to the background + return AsyncExecuteStatements::execute(std::move(stmts), mDBConnection, + mNativeConnection, aCallback, _stmt); +} + +template +void EscapeStringForLIKEInternal(const T& aValue, + const typename T::char_type aEscapeChar, + T& aResult) { + const typename T::char_type MATCH_ALL('%'); + const typename T::char_type MATCH_ONE('_'); + + aResult.Truncate(0); + + for (uint32_t i = 0; i < aValue.Length(); i++) { + if (aValue[i] == aEscapeChar || aValue[i] == MATCH_ALL || + aValue[i] == MATCH_ONE) { + aResult += aEscapeChar; + } + aResult += aValue[i]; + } +} + +NS_IMETHODIMP +StorageBaseStatementInternal::EscapeStringForLIKE(const nsAString& aValue, + const char16_t aEscapeChar, + nsAString& _escapedString) { + EscapeStringForLIKEInternal(aValue, aEscapeChar, _escapedString); + + return NS_OK; +} + +NS_IMETHODIMP +StorageBaseStatementInternal::EscapeUTF8StringForLIKE( + const nsACString& aValue, const char aEscapeChar, + nsACString& _escapedString) { + EscapeStringForLIKEInternal(aValue, aEscapeChar, _escapedString); + + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/StorageBaseStatementInternal.h b/storage/StorageBaseStatementInternal.h new file mode 100644 index 0000000000..685f0f79d0 --- /dev/null +++ b/storage/StorageBaseStatementInternal.h @@ -0,0 +1,297 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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_storage_StorageBaseStatementInternal_h_ +#define mozilla_storage_StorageBaseStatementInternal_h_ + +#include "nsISupports.h" +#include "nsCOMPtr.h" +#include "mozStorageHelper.h" + +struct sqlite3; +struct sqlite3_stmt; +class mozIStorageBindingParamsArray; +class mozIStorageBindingParams; +class mozIStorageStatementCallback; +class mozIStoragePendingStatement; + +namespace mozilla { +namespace storage { + +#define STORAGEBASESTATEMENTINTERNAL_IID \ + { \ + 0xd18856c9, 0xbf07, 0x4ae2, { \ + 0x94, 0x5b, 0x1a, 0xdd, 0x49, 0x19, 0x55, 0x2a \ + } \ + } + +class Connection; +class StatementData; + +class AsyncStatementFinalizer; + +/** + * Implementation-only interface and shared logix mix-in corresponding to + * mozIStorageBaseStatement. Both Statement and AsyncStatement inherit from + * this. The interface aspect makes them look the same to implementation innards + * that aren't publicly accessible. The mix-in avoids code duplication in + * common implementations of mozIStorageBaseStatement, albeit with some minor + * performance/space overhead because we have to use defines to officially + * implement the methods on Statement/AsyncStatement (and proxy to this base + * class.) + */ +class StorageBaseStatementInternal : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(STORAGEBASESTATEMENTINTERNAL_IID) + + /** + * @return the connection that this statement belongs to. + */ + Connection* getOwner() { return mDBConnection; } + + /** + * Return the asynchronous statement, creating it if required. + * + * This is for use by the asynchronous execution code for StatementData + * created by AsyncStatements. Statement internally uses this method to + * prepopulate StatementData with the sqlite3_stmt. + * + * @param[out] stmt + * The sqlite3_stmt for asynchronous use. + * @return The SQLite result code for creating the statement if created, + * SQLITE_OK if creation was not required. + */ + virtual int getAsyncStatement(sqlite3_stmt** _stmt) = 0; + + /** + * Obtains the StatementData needed for asynchronous execution. + * + * This is for use by Connection to retrieve StatementData from statements + * when executeAsync is invoked. + * + * @param[out] _data + * A reference to a StatementData object that will be populated + * upon successful execution of this method. + * @return NS_OK if we were able to assemble the data, failure otherwise. + */ + virtual nsresult getAsynchronousStatementData(StatementData& _data) = 0; + + /** + * Construct a new BindingParams to be owned by the provided binding params + * array. This method exists so that BindingParamsArray does not need + * factory logic to determine what type of BindingParams to instantiate. + * + * @param aOwner + * The binding params array to own the newly created binding params. + * @return The new mozIStorageBindingParams instance appropriate to the + * underlying statement type. + */ + virtual already_AddRefed newBindingParams( + mozIStorageBindingParamsArray* aOwner) = 0; + + protected: // mix-in bits are protected + StorageBaseStatementInternal(); + + RefPtr mDBConnection; + sqlite3* mNativeConnection; + + /** + * Our asynchronous statement. + * + * For Statement this is populated by the first invocation to + * getAsyncStatement. + * + * For AsyncStatement, this is null at creation time and initialized by the + * async thread when it calls getAsyncStatement the first time the statement + * is executed. (Or in the event of badly formed SQL, every time.) + */ + sqlite3_stmt* mAsyncStatement; + + /** + * Initiate asynchronous finalization by dispatching an event to the + * asynchronous thread to finalize mAsyncStatement. This acquires a reference + * to this statement and proxies it back to the connection's owning thread + * for release purposes. + * + * In the event the asynchronous thread is already gone or we otherwise fail + * to dispatch an event to it we failover to invoking internalAsyncFinalize + * directly. (That's what the asynchronous finalizer would have called.) + * + * @note You must not call this method from your destructor because its + * operation assumes we are still alive. Call internalAsyncFinalize + * directly in that case. + */ + void asyncFinalize(); + + /** + * Cleanup the async sqlite3_stmt stored in mAsyncStatement if it exists by + * attempting to dispatch to the asynchronous thread if available, finalizing + * on this thread if it is not. + * + * @note Call this from your destructor, call asyncFinalize otherwise. + */ + void destructorAsyncFinalize(); + + NS_IMETHOD NewBindingParamsArray(mozIStorageBindingParamsArray** _array); + NS_IMETHOD ExecuteAsync(mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _stmt); + NS_IMETHOD EscapeStringForLIKE(const nsAString& aValue, char16_t aEscapeChar, + nsAString& _escapedString); + NS_IMETHOD EscapeUTF8StringForLIKE(const nsACString& aValue, char aEscapeChar, + nsACString& _escapedString); + + // Needs access to internalAsyncFinalize + friend class AsyncStatementFinalizer; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(StorageBaseStatementInternal, + STORAGEBASESTATEMENTINTERNAL_IID) + +#define NS_DECL_STORAGEBASESTATEMENTINTERNAL \ + virtual Connection* getOwner(); \ + virtual int getAsyncStatement(sqlite3_stmt** _stmt) override; \ + virtual nsresult getAsynchronousStatementData(StatementData& _data) \ + override; \ + virtual already_AddRefed newBindingParams( \ + mozIStorageBindingParamsArray* aOwner) override; + +/** + * Helper macro to implement the proxying implementations. Because we are + * implementing methods that are part of mozIStorageBaseStatement and the + * implementation classes already use NS_DECL_MOZISTORAGEBASESTATEMENT we don't + * need to provide declaration support. + */ +#define MIX_IMPL(_class, _optionalGuard, _method, _declArgs, _invokeArgs) \ + NS_IMETHODIMP _class::_method _declArgs { \ + _optionalGuard return StorageBaseStatementInternal::_method _invokeArgs; \ + } + +/** + * Define proxying implementation for the given _class. If a state invariant + * needs to be checked and an early return possibly performed, pass the clause + * to use as _optionalGuard. + */ +#define MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL(_class, _optionalGuard) \ + MIX_IMPL(_class, _optionalGuard, NewBindingParamsArray, \ + (mozIStorageBindingParamsArray * *_array), (_array)) \ + MIX_IMPL(_class, _optionalGuard, ExecuteAsync, \ + (mozIStorageStatementCallback * aCallback, \ + mozIStoragePendingStatement * *_stmt), \ + (aCallback, _stmt)) \ + MIX_IMPL(_class, _optionalGuard, EscapeStringForLIKE, \ + (const nsAString& aValue, char16_t aEscapeChar, \ + nsAString& _escapedString), \ + (aValue, aEscapeChar, _escapedString)) \ + MIX_IMPL(_class, _optionalGuard, EscapeUTF8StringForLIKE, \ + (const nsACString& aValue, char aEscapeChar, \ + nsACString& _escapedString), \ + (aValue, aEscapeChar, _escapedString)) + +/** + * Name-building helper for BIND_GEN_IMPL. + */ +#define BIND_NAME_CONCAT(_nameBit, _concatBit) Bind##_nameBit##_concatBit + +/** + * We have type-specific convenience methods for C++ implementations in + * two different forms; by index and by name. The following macro allows + * us to avoid having to define repetitive things by hand. + * + * Because of limitations of macros and our desire to avoid requiring special + * permutations for the null and blob cases (whose argument count varies), + * we require that the argument declarations and corresponding invocation + * usages are passed in. + * + * @param _class + * The class name. + * @param _guard + * The guard clause to inject. + * @param _declName + * The argument list (with parens) for the ByName variants. + * @param _declIndex + * The argument list (with parens) for the ByIndex variants. + * @param _invArgs + * The invocation argumment list. + */ +#define BIND_GEN_IMPL(_class, _guard, _name, _declName, _declIndex, _invArgs) \ + NS_IMETHODIMP _class::BIND_NAME_CONCAT(_name, ByName) _declName { \ + _guard mozIStorageBindingParams* params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BIND_NAME_CONCAT(_name, ByName) _invArgs; \ + } \ + NS_IMETHODIMP _class::BIND_NAME_CONCAT(_name, ByIndex) _declIndex { \ + _guard mozIStorageBindingParams* params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BIND_NAME_CONCAT(_name, ByIndex) _invArgs; \ + } + +/** + * Implement BindByName/BindByIndex for the given class. + * + * @param _class The class name. + * @param _optionalGuard The guard clause to inject. + */ +#define BIND_BASE_IMPLS(_class, _optionalGuard) \ + NS_IMETHODIMP _class::BindByName(const nsACString& aName, \ + nsIVariant* aValue) { \ + _optionalGuard mozIStorageBindingParams* params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BindByName(aName, aValue); \ + } \ + NS_IMETHODIMP _class::BindByIndex(uint32_t aIndex, nsIVariant* aValue) { \ + _optionalGuard mozIStorageBindingParams* params = getParams(); \ + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); \ + return params->BindByIndex(aIndex, aValue); \ + } + +/** + * Define the various Bind*ByIndex, Bind*ByName stubs that just end up proxying + * to the params object. + */ +#define BOILERPLATE_BIND_PROXIES(_class, _optionalGuard) \ + BIND_BASE_IMPLS(_class, _optionalGuard) \ + BIND_GEN_IMPL(_class, _optionalGuard, UTF8String, \ + (const nsACString& aWhere, const nsACString& aValue), \ + (uint32_t aWhere, const nsACString& aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, String, \ + (const nsACString& aWhere, const nsAString& aValue), \ + (uint32_t aWhere, const nsAString& aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, Double, \ + (const nsACString& aWhere, double aValue), \ + (uint32_t aWhere, double aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, Int32, \ + (const nsACString& aWhere, int32_t aValue), \ + (uint32_t aWhere, int32_t aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, Int64, \ + (const nsACString& aWhere, int64_t aValue), \ + (uint32_t aWhere, int64_t aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, Null, (const nsACString& aWhere), \ + (uint32_t aWhere), (aWhere)) \ + BIND_GEN_IMPL( \ + _class, _optionalGuard, Blob, \ + (const nsACString& aWhere, const uint8_t* aValue, uint32_t aValueSize), \ + (uint32_t aWhere, const uint8_t* aValue, uint32_t aValueSize), \ + (aWhere, aValue, aValueSize)) \ + BIND_GEN_IMPL(_class, _optionalGuard, BlobArray, \ + (const nsACString& aWhere, const nsTArray& aValue), \ + (uint32_t aWhere, const nsTArray& aValue), \ + (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, StringAsBlob, \ + (const nsACString& aWhere, const nsAString& aValue), \ + (uint32_t aWhere, const nsAString& aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL(_class, _optionalGuard, UTF8StringAsBlob, \ + (const nsACString& aWhere, const nsACString& aValue), \ + (uint32_t aWhere, const nsACString& aValue), (aWhere, aValue)) \ + BIND_GEN_IMPL( \ + _class, _optionalGuard, AdoptedBlob, \ + (const nsACString& aWhere, uint8_t* aValue, uint32_t aValueSize), \ + (uint32_t aWhere, uint8_t * aValue, uint32_t aValueSize), \ + (aWhere, aValue, aValueSize)) + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_StorageBaseStatementInternal_h_ diff --git a/storage/VacuumManager.cpp b/storage/VacuumManager.cpp new file mode 100644 index 0000000000..63eb0f89b4 --- /dev/null +++ b/storage/VacuumManager.cpp @@ -0,0 +1,286 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/DebugOnly.h" + +#include "VacuumManager.h" + +#include "mozilla/ErrorNames.h" +#include "mozilla/Services.h" +#include "mozilla/Preferences.h" +#include "nsIObserverService.h" +#include "nsIFile.h" +#include "nsThreadUtils.h" +#include "mozilla/Logging.h" +#include "prtime.h" +#include "mozilla/StaticPrefs_storage.h" + +#include "mozStorageConnection.h" +#include "mozStoragePrivateHelpers.h" +#include "mozIStorageStatement.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "mozStorageHelper.h" +#include "nsXULAppAPI.h" +#include "xpcpublic.h" + +#define OBSERVER_TOPIC_IDLE_DAILY "idle-daily" + +// Used to notify the begin and end of a vacuum operation. +#define OBSERVER_TOPIC_VACUUM_BEGIN "vacuum-begin" +#define OBSERVER_TOPIC_VACUUM_END "vacuum-end" +// This notification is sent only in automation when vacuum for a database is +// skipped, and can thus be used to verify that. +#define OBSERVER_TOPIC_VACUUM_SKIP "vacuum-skip" + +// This preferences root will contain last vacuum timestamps (in seconds) for +// each database. The database filename is used as a key. +#define PREF_VACUUM_BRANCH "storage.vacuum.last." + +// Time between subsequent vacuum calls for a certain database. +#define VACUUM_INTERVAL_SECONDS (30 * 86400) // 30 days. + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla::storage { + +namespace { + +//////////////////////////////////////////////////////////////////////////////// +//// Vacuumer declaration. + +class Vacuumer final : public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + explicit Vacuumer(mozIStorageVacuumParticipant* aParticipant); + bool execute(); + + private: + nsresult notifyCompletion(bool aSucceeded); + ~Vacuumer() = default; + + nsCOMPtr mParticipant; + nsCString mDBFilename; + nsCOMPtr mDBConn; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Vacuumer implementation. + +NS_IMPL_ISUPPORTS(Vacuumer, mozIStorageCompletionCallback) + +Vacuumer::Vacuumer(mozIStorageVacuumParticipant* aParticipant) + : mParticipant(aParticipant) {} + +bool Vacuumer::execute() { + MOZ_ASSERT(NS_IsMainThread(), "Must be running on the main thread!"); + + // Get the connection and check its validity. + nsresult rv = mParticipant->GetDatabaseConnection(getter_AddRefs(mDBConn)); + if (NS_FAILED(rv) || !mDBConn) return false; + + nsCOMPtr os = mozilla::services::GetObserverService(); + + bool inAutomation = xpc::IsInAutomation(); + // Get the database filename. Last vacuum time is stored under this name + // in PREF_VACUUM_BRANCH. + nsCOMPtr databaseFile; + mDBConn->GetDatabaseFile(getter_AddRefs(databaseFile)); + if (!databaseFile) { + NS_WARNING("Trying to vacuum a in-memory database!"); + if (inAutomation && os) { + mozilla::Unused << os->NotifyObservers( + nullptr, OBSERVER_TOPIC_VACUUM_SKIP, u":memory:"); + } + return false; + } + nsAutoString databaseFilename; + rv = databaseFile->GetLeafName(databaseFilename); + NS_ENSURE_SUCCESS(rv, false); + CopyUTF16toUTF8(databaseFilename, mDBFilename); + MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); + + // Check interval from last vacuum. + int32_t now = static_cast(PR_Now() / PR_USEC_PER_SEC); + int32_t lastVacuum; + nsAutoCString prefName(PREF_VACUUM_BRANCH); + prefName += mDBFilename; + rv = Preferences::GetInt(prefName.get(), &lastVacuum); + if (NS_SUCCEEDED(rv) && (now - lastVacuum) < VACUUM_INTERVAL_SECONDS) { + // This database was vacuumed recently, skip it. + if (inAutomation && os) { + mozilla::Unused << os->NotifyObservers( + nullptr, OBSERVER_TOPIC_VACUUM_SKIP, + NS_ConvertUTF8toUTF16(mDBFilename).get()); + } + return false; + } + + // Notify that we are about to start vacuuming. The participant can opt-out + // if it cannot handle a vacuum at this time, and then we'll move to the next + // one. + bool vacuumGranted = false; + rv = mParticipant->OnBeginVacuum(&vacuumGranted); + NS_ENSURE_SUCCESS(rv, false); + if (!vacuumGranted) { + if (inAutomation && os) { + mozilla::Unused << os->NotifyObservers( + nullptr, OBSERVER_TOPIC_VACUUM_SKIP, + NS_ConvertUTF8toUTF16(mDBFilename).get()); + } + return false; + } + + // Ask for the expected page size. Vacuum can change the page size, unless + // the database is using WAL journaling. + // TODO Bug 634374: figure out a strategy to fix page size with WAL. + int32_t expectedPageSize = 0; + rv = mParticipant->GetExpectedDatabasePageSize(&expectedPageSize); + if (NS_FAILED(rv) || !Service::pageSizeIsValid(expectedPageSize)) { + NS_WARNING("Invalid page size requested for database, won't set it. "); + NS_WARNING(mDBFilename.get()); + expectedPageSize = 0; + } + + bool incremental = false; + mozilla::Unused << mParticipant->GetUseIncrementalVacuum(&incremental); + + // Notify vacuum is about to start. + if (os) { + mozilla::Unused << os->NotifyObservers( + nullptr, OBSERVER_TOPIC_VACUUM_BEGIN, + NS_ConvertUTF8toUTF16(mDBFilename).get()); + } + + rv = mDBConn->AsyncVacuum(this, incremental, expectedPageSize); + if (NS_FAILED(rv)) { + // The connection is not ready. + mozilla::Unused << Complete(rv, nullptr); + return false; + } + + return true; +} + +NS_IMETHODIMP +Vacuumer::Complete(nsresult aStatus, nsISupports* aValue) { + if (NS_SUCCEEDED(aStatus)) { + // Update last vacuum time. + int32_t now = static_cast(PR_Now() / PR_USEC_PER_SEC); + MOZ_ASSERT(!mDBFilename.IsEmpty(), "Database filename cannot be empty"); + nsAutoCString prefName(PREF_VACUUM_BRANCH); + prefName += mDBFilename; + DebugOnly rv = Preferences::SetInt(prefName.get(), now); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); + notifyCompletion(true); + return NS_OK; + } + + nsAutoCString errName; + GetErrorName(aStatus, errName); + nsCString errMsg = nsPrintfCString( + "Vacuum failed on '%s' with error %s - code %" PRIX32, mDBFilename.get(), + errName.get(), static_cast(aStatus)); + NS_WARNING(errMsg.get()); + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Error)) { + MOZ_LOG(gStorageLog, LogLevel::Error, ("%s", errMsg.get())); + } + + notifyCompletion(false); + return NS_OK; +} + +nsresult Vacuumer::notifyCompletion(bool aSucceeded) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + mozilla::Unused << os->NotifyObservers( + nullptr, OBSERVER_TOPIC_VACUUM_END, + NS_ConvertUTF8toUTF16(mDBFilename).get()); + } + + nsresult rv = mParticipant->OnEndVacuum(aSucceeded); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// VacuumManager + +NS_IMPL_ISUPPORTS(VacuumManager, nsIObserver) + +VacuumManager* VacuumManager::gVacuumManager = nullptr; + +already_AddRefed VacuumManager::getSingleton() { + // Don't allocate it in the child Process. + if (!XRE_IsParentProcess()) { + return nullptr; + } + + if (!gVacuumManager) { + auto manager = MakeRefPtr(); + MOZ_ASSERT(gVacuumManager == manager.get()); + return manager.forget(); + } + return do_AddRef(gVacuumManager); +} + +VacuumManager::VacuumManager() : mParticipants("vacuum-participant") { + MOZ_ASSERT(!gVacuumManager, + "Attempting to create two instances of the service!"); + gVacuumManager = this; +} + +VacuumManager::~VacuumManager() { + // Remove the static reference to the service. Check to make sure its us + // in case somebody creates an extra instance of the service. + MOZ_ASSERT(gVacuumManager == this, + "Deleting a non-singleton instance of the service"); + if (gVacuumManager == this) { + gVacuumManager = nullptr; + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +VacuumManager::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, OBSERVER_TOPIC_IDLE_DAILY) == 0) { + // Try to run vacuum on all registered entries. Will stop at the first + // successful one. + nsCOMArray entries; + mParticipants.GetEntries(entries); + // If there are more entries than what a month can contain, we could end up + // skipping some, since we run daily. So we use a starting index. + static const char* kPrefName = PREF_VACUUM_BRANCH "index"; + int32_t startIndex = Preferences::GetInt(kPrefName, 0); + if (startIndex >= entries.Count()) { + startIndex = 0; + } + int32_t index; + for (index = startIndex; index < entries.Count(); ++index) { + RefPtr vacuum = new Vacuumer(entries[index]); + // Only vacuum one database per day. + if (vacuum->execute()) { + break; + } + } + DebugOnly rv = Preferences::SetInt(kPrefName, index); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should be able to set a preference"); + } + + return NS_OK; +} + +} // namespace mozilla::storage diff --git a/storage/VacuumManager.h b/storage/VacuumManager.h new file mode 100644 index 0000000000..0b0dda2de5 --- /dev/null +++ b/storage/VacuumManager.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_VacuumManager_h__ +#define mozilla_storage_VacuumManager_h__ + +#include "nsCOMPtr.h" +#include "nsIObserver.h" +#include "mozIStorageVacuumParticipant.h" +#include "nsCategoryCache.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class VacuumManager final : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + + VacuumManager(); + + /** + * Obtains the VacuumManager object. + */ + static already_AddRefed getSingleton(); + + private: + ~VacuumManager(); + + static VacuumManager* gVacuumManager; + + // Cache of components registered in "vacuum-participant" category. + nsCategoryCache mParticipants; +}; + +} // namespace storage +} // namespace mozilla + +#endif diff --git a/storage/Variant.cpp b/storage/Variant.cpp new file mode 100644 index 0000000000..61cb5b0676 --- /dev/null +++ b/storage/Variant.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "Variant.h" + +#include "nsCOMPtr.h" + +extern "C" { + +using namespace mozilla::storage; + +/** + * Return the data type of the given variant. This method used to be exposed + * to XPCOM, but since bug 1507540 it's marked [notxpcom] in the interface + * definition, so we need this C function to access it from Rust. + */ +uint16_t NS_GetDataType(nsIVariant* aVariant) { + return aVariant->GetDataType(); +} + +// Convenience functions to create Storage variants from Rust. +void NS_NewStorageNullVariant(nsIVariant** aVariant) { + nsCOMPtr variant = new NullVariant(); + variant.forget(aVariant); +} + +void NS_NewStorageBooleanVariant(bool aValue, nsIVariant** aVariant) { + nsCOMPtr variant = new BooleanVariant(aValue); + variant.forget(aVariant); +} + +void NS_NewStorageIntegerVariant(int64_t aValue, nsIVariant** aVariant) { + nsCOMPtr variant = new IntegerVariant(aValue); + variant.forget(aVariant); +} + +void NS_NewStorageFloatVariant(double aValue, nsIVariant** aVariant) { + nsCOMPtr variant = new FloatVariant(aValue); + variant.forget(aVariant); +} + +void NS_NewStorageTextVariant(const nsAString& aValue, nsIVariant** aVariant) { + nsCOMPtr variant = new TextVariant(aValue); + variant.forget(aVariant); +} + +void NS_NewStorageUTF8TextVariant(const nsACString& aValue, + nsIVariant** aVariant) { + nsCOMPtr variant = new UTF8TextVariant(aValue); + variant.forget(aVariant); +} + +} // extern "C" diff --git a/storage/Variant.h b/storage/Variant.h new file mode 100644 index 0000000000..420ca94b94 --- /dev/null +++ b/storage/Variant.h @@ -0,0 +1,435 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_Variant_h__ +#define mozilla_storage_Variant_h__ + +#include + +#include "nsIVariant.h" +#include "nsString.h" +#include "nsTArray.h" + +#define VARIANT_BASE_IID \ + { /* 78888042-0fa3-4f7a-8b19-7996f99bf1aa */ \ + 0x78888042, 0x0fa3, 0x4f7a, { \ + 0x8b, 0x19, 0x79, 0x96, 0xf9, 0x9b, 0xf1, 0xaa \ + } \ + } + +/** + * This class is used by the storage module whenever an nsIVariant needs to be + * returned. We provide traits for the basic sqlite types to make use easier. + * The following types map to the indicated sqlite type: + * int64_t -> INTEGER (use IntegerVariant) + * double -> FLOAT (use FloatVariant) + * nsString -> TEXT (use TextVariant) + * nsCString -> TEXT (use UTF8TextVariant) + * uint8_t[] -> BLOB (use BlobVariant) + * nullptr -> NULL (use NullVariant) + * + * The kvstore component also reuses this class as a common implementation + * of a simple threadsafe variant for the storage of primitive values only. + * The BooleanVariant type has been introduced for kvstore use cases and should + * be enhanced to provide full boolean variant support for mozStorage. + * + * Bug 1494102 tracks that work. + */ + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Base Class + +class Variant_base : public nsIVariant { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIVARIANT + NS_DECLARE_STATIC_IID_ACCESSOR(VARIANT_BASE_IID) + + protected: + virtual ~Variant_base() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(Variant_base, VARIANT_BASE_IID) + +//////////////////////////////////////////////////////////////////////////////// +//// Traits + +/** + * Generics + */ + +template +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_EMPTY; } +}; + +template +struct variant_storage_traits { + typedef DataType ConstructorType; + typedef DataType StorageType; + static inline void storage_conversion(const ConstructorType aData, + StorageType* _storage) { + *_storage = aData; + } + + static inline void destroy(const StorageType& _storage) {} +}; + +#define NO_CONVERSION return NS_ERROR_CANNOT_CONVERT_DATA; + +template +struct variant_boolean_traits { + typedef typename variant_storage_traits::StorageType + StorageType; + static inline nsresult asBool(const StorageType&, bool*) { NO_CONVERSION } +}; + +template +struct variant_integer_traits { + typedef typename variant_storage_traits::StorageType + StorageType; + static inline nsresult asInt32(const StorageType&, int32_t*) { NO_CONVERSION } + static inline nsresult asInt64(const StorageType&, int64_t*) { NO_CONVERSION } +}; + +template +struct variant_float_traits { + typedef typename variant_storage_traits::StorageType + StorageType; + static inline nsresult asDouble(const StorageType&, double*) { NO_CONVERSION } +}; + +template +struct variant_text_traits { + typedef typename variant_storage_traits::StorageType + StorageType; + static inline nsresult asUTF8String(const StorageType&, nsACString&) { + NO_CONVERSION + } + static inline nsresult asString(const StorageType&, nsAString&) { + NO_CONVERSION + } +}; + +template +struct variant_blob_traits { + typedef typename variant_storage_traits::StorageType + StorageType; + static inline nsresult asArray(const StorageType&, uint16_t*, uint32_t*, + void**) { + NO_CONVERSION + } +}; + +#undef NO_CONVERSION + +/** + * BOOLEAN type + */ + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_BOOL; } +}; +template <> +struct variant_boolean_traits { + static inline nsresult asBool(bool aValue, bool* _result) { + *_result = aValue; + return NS_OK; + } + + // NB: It might be worth also providing conversions to int types. + + // NB: It'd be nice to implement asBool conversions for 0 and 1, too. + // That would let us clean up some conversions in Places, such as: + // https://searchfox.org/mozilla-central/rev/0640ea80fbc8d48f8b197cd363e2535c95a15eb3/toolkit/components/places/SQLFunctions.cpp#564-565 + // https://searchfox.org/mozilla-central/rev/0640ea80fbc8d48f8b197cd363e2535c95a15eb3/toolkit/components/places/SQLFunctions.cpp#1057 + // https://searchfox.org/mozilla-central/rev/0640ea80fbc8d48f8b197cd363e2535c95a15eb3/toolkit/components/places/nsNavHistory.cpp#3189 +}; + +/** + * INTEGER types + */ + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_INT64; } +}; +template <> +struct variant_integer_traits { + static inline nsresult asInt32(int64_t aValue, int32_t* _result) { + if (aValue > INT32_MAX || aValue < INT32_MIN) + return NS_ERROR_CANNOT_CONVERT_DATA; + + *_result = static_cast(aValue); + return NS_OK; + } + static inline nsresult asInt64(int64_t aValue, int64_t* _result) { + *_result = aValue; + return NS_OK; + } +}; +// xpcvariant just calls get double for integers... +template <> +struct variant_float_traits { + static inline nsresult asDouble(int64_t aValue, double* _result) { + *_result = double(aValue); + return NS_OK; + } +}; + +/** + * FLOAT types + */ + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_DOUBLE; } +}; +template <> +struct variant_float_traits { + static inline nsresult asDouble(double aValue, double* _result) { + *_result = aValue; + return NS_OK; + } +}; + +/** + * TEXT types + */ + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_ASTRING; } +}; +template <> +struct variant_storage_traits { + typedef const nsAString& ConstructorType; + typedef nsString StorageType; + static inline void storage_conversion(ConstructorType aText, + StorageType* _outData) { + *_outData = aText; + } + static inline void destroy(const StorageType& _outData) {} +}; +template <> +struct variant_text_traits { + static inline nsresult asUTF8String(const nsString& aValue, + nsACString& _result) { + CopyUTF16toUTF8(aValue, _result); + return NS_OK; + } + static inline nsresult asString(const nsString& aValue, nsAString& _result) { + _result = aValue; + return NS_OK; + } +}; + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_UTF8STRING; } +}; +template <> +struct variant_storage_traits { + typedef const nsACString& ConstructorType; + typedef nsCString StorageType; + static inline void storage_conversion(ConstructorType aText, + StorageType* _outData) { + *_outData = aText; + } + static inline void destroy(const StorageType& aData) {} +}; +template <> +struct variant_text_traits { + static inline nsresult asUTF8String(const nsCString& aValue, + nsACString& _result) { + _result = aValue; + return NS_OK; + } + static inline nsresult asString(const nsCString& aValue, nsAString& _result) { + CopyUTF8toUTF16(aValue, _result); + return NS_OK; + } +}; + +/** + * BLOB types + */ + +template <> +struct variant_traits { + static inline uint16_t type() { return nsIDataType::VTYPE_ARRAY; } +}; +template <> +struct variant_storage_traits { + typedef std::pair ConstructorType; + typedef FallibleTArray StorageType; + static inline void storage_conversion(ConstructorType aBlob, + StorageType* _outData) { + _outData->Clear(); + (void)_outData->AppendElements(static_cast(aBlob.first), + aBlob.second, fallible); + } + static inline void destroy(const StorageType& _outData) {} +}; +template <> +struct variant_storage_traits { + typedef std::pair ConstructorType; + typedef std::pair StorageType; + static inline void storage_conversion(ConstructorType aBlob, + StorageType* _outData) { + *_outData = aBlob; + } + static inline void destroy(StorageType& aData) { + if (aData.first) { + free(aData.first); + aData.first = nullptr; + } + } +}; +template <> +struct variant_blob_traits { + static inline nsresult asArray(FallibleTArray& aData, + uint16_t* _type, uint32_t* _size, + void** _result) { + // For empty blobs, we return nullptr. + if (aData.Length() == 0) { + *_result = nullptr; + *_type = nsIDataType::VTYPE_UINT8; + *_size = 0; + return NS_OK; + } + + // Otherwise, we copy the array. + *_result = moz_xmemdup(aData.Elements(), aData.Length() * sizeof(uint8_t)); + + // Set type and size + *_type = nsIDataType::VTYPE_UINT8; + *_size = aData.Length(); + return NS_OK; + } +}; + +template <> +struct variant_blob_traits { + static inline nsresult asArray(std::pair& aData, + uint16_t* _type, uint32_t* _size, + void** _result) { + // For empty blobs, we return nullptr. + if (aData.second == 0) { + *_result = nullptr; + *_type = nsIDataType::VTYPE_UINT8; + *_size = 0; + return NS_OK; + } + + // Otherwise, transfer the data out. + *_result = aData.first; + aData.first = nullptr; + MOZ_ASSERT(*_result); // We asked for it twice, better not use adopting! + + // Set type and size + *_type = nsIDataType::VTYPE_UINT8; + *_size = aData.second; + return NS_OK; + } +}; + +/** + * nullptr type + */ + +class NullVariant : public Variant_base { + public: + uint16_t GetDataType() override { return nsIDataType::VTYPE_EMPTY; } + + NS_IMETHOD GetAsAUTF8String(nsACString& _str) override { + // Return a void string. + _str.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD GetAsAString(nsAString& _str) override { + // Return a void string. + _str.SetIsVoid(true); + return NS_OK; + } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Template Implementation + +template +class Variant final : public Variant_base { + ~Variant() { variant_storage_traits::destroy(mData); } + + public: + explicit Variant( + const typename variant_storage_traits::ConstructorType + aData) { + variant_storage_traits::storage_conversion(aData, + &mData); + } + + uint16_t GetDataType() override { return variant_traits::type(); } + + NS_IMETHOD GetAsBool(bool* _boolean) override { + return variant_boolean_traits::asBool(mData, _boolean); + } + + NS_IMETHOD GetAsInt32(int32_t* _integer) override { + return variant_integer_traits::asInt32(mData, _integer); + } + + NS_IMETHOD GetAsInt64(int64_t* _integer) override { + return variant_integer_traits::asInt64(mData, _integer); + } + + NS_IMETHOD GetAsDouble(double* _double) override { + return variant_float_traits::asDouble(mData, _double); + } + + NS_IMETHOD GetAsAUTF8String(nsACString& _str) override { + return variant_text_traits::asUTF8String(mData, _str); + } + + NS_IMETHOD GetAsAString(nsAString& _str) override { + return variant_text_traits::asString(mData, _str); + } + + NS_IMETHOD GetAsArray(uint16_t* _type, nsIID*, uint32_t* _size, + void** _data) override { + return variant_blob_traits::asArray(mData, _type, _size, + _data); + } + + private: + typename variant_storage_traits::StorageType mData; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Handy typedefs! Use these for the right mapping. + +// Currently, BooleanVariant is only useful for kvstore. +// Bug 1494102 tracks implementing full boolean variant support for mozStorage. +typedef Variant BooleanVariant; + +typedef Variant IntegerVariant; +typedef Variant FloatVariant; +typedef Variant TextVariant; +typedef Variant UTF8TextVariant; +typedef Variant BlobVariant; +typedef Variant AdoptedBlobVariant; + +} // namespace storage +} // namespace mozilla + +#include "Variant_inl.h" + +#endif // mozilla_storage_Variant_h__ diff --git a/storage/Variant_inl.h b/storage/Variant_inl.h new file mode 100644 index 0000000000..2238c5f959 --- /dev/null +++ b/storage/Variant_inl.h @@ -0,0 +1,136 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +/** + * Note: This file is included by Variant.h. + */ + +#ifndef mozilla_storage_Variant_h__ +# error "Do not include this file directly!" +#endif + +#include "js/RootingAPI.h" +#include "js/Value.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Variant_base + +inline NS_IMPL_ADDREF(Variant_base) inline NS_IMPL_RELEASE( + Variant_base) inline NS_IMPL_QUERY_INTERFACE(Variant_base, nsIVariant) + + //////////////////////////////////////////////////////////////////////////////// + //// nsIVariant + + inline uint16_t Variant_base::GetDataType() { + return nsIDataType::VTYPE_VOID; +} + +inline NS_IMETHODIMP Variant_base::GetAsInt32(int32_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsInt64(int64_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsDouble(double*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsAUTF8String(nsACString&) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsAString(nsAString&) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsArray(uint16_t*, nsIID*, uint32_t*, + void**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsInt8(uint8_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsInt16(int16_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsUint8(uint8_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsUint16(uint16_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsUint32(uint32_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsUint64(uint64_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsFloat(float*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsBool(bool*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsChar(char*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsWChar(char16_t*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsID(nsID*) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsString(char**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsWString(char16_t**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsISupports(nsISupports**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsInterface(nsIID**, void**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsACString(nsACString&) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsStringWithSize(uint32_t*, char**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsWStringWithSize(uint32_t*, char16_t**) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +inline NS_IMETHODIMP Variant_base::GetAsJSVal(JS::MutableHandle) { + return NS_ERROR_CANNOT_CONVERT_DATA; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/build/components.conf b/storage/build/components.conf new file mode 100644 index 0000000000..5953e85927 --- /dev/null +++ b/storage/build/components.conf @@ -0,0 +1,27 @@ +# -*- 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 = [ + { + 'js_name': 'storage', + 'cid': '{bbbb1d61-438f-4436-92ed-8308e5830fb0}', + 'contract_ids': ['@mozilla.org/storage/service;1'], + 'interfaces': ['mozIStorageService'], + 'singleton': True, + 'type': 'mozilla::storage::Service', + 'headers': ['/storage/mozStorageService.h'], + 'constructor': 'mozilla::storage::Service::getSingleton', + }, + { + 'cid': '{3b667ee0-d2da-4ccc-9c3d-95f2ca6a8b4c}', + 'contract_ids': ['@mozilla.org/storage/vacuum;1'], + 'singleton': True, + 'type': 'mozilla::storage::VacuumManager', + 'headers': ['/storage/VacuumManager.h'], + 'constructor': 'mozilla::storage::VacuumManager::getSingleton', + 'categories': {'idle-daily': 'MozStorage Vacuum Manager'}, + }, +] diff --git a/storage/build/moz.build b/storage/build/moz.build new file mode 100644 index 0000000000..0af582bb73 --- /dev/null +++ b/storage/build/moz.build @@ -0,0 +1,17 @@ +# -*- 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 += [ + "mozStorageCID.h", +] + +XPCOM_MANIFESTS += [ + "components.conf", +] + +FINAL_LIBRARY = "xul" + +CXXFLAGS += CONFIG["SQLITE_CFLAGS"] diff --git a/storage/build/mozStorageCID.h b/storage/build/mozStorageCID.h new file mode 100644 index 0000000000..64f0b6709b --- /dev/null +++ b/storage/build/mozStorageCID.h @@ -0,0 +1,43 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 MOZSTORAGECID_H +#define MOZSTORAGECID_H + +#define MOZ_STORAGE_CONTRACTID_PREFIX "@mozilla.org/storage" + +/* b71a1f84-3a70-4d37-a348-f1ba0e27eead */ +#define MOZ_STORAGE_CONNECTION_CID \ + { \ + 0xb71a1f84, 0x3a70, 0x4d37, { \ + 0xa3, 0x48, 0xf1, 0xba, 0x0e, 0x27, 0xee, 0xad \ + } \ + } + +#define MOZ_STORAGE_CONNECTION_CONTRACTID \ + MOZ_STORAGE_CONTRACTID_PREFIX "/connection;1" + +/* bbbb1d61-438f-4436-92ed-8308e5830fb0 */ +#define MOZ_STORAGE_SERVICE_CID \ + { \ + 0xbbbb1d61, 0x438f, 0x4436, { \ + 0x92, 0xed, 0x83, 0x08, 0xe5, 0x83, 0x0f, 0xb0 \ + } \ + } + +#define MOZ_STORAGE_SERVICE_CONTRACTID \ + MOZ_STORAGE_CONTRACTID_PREFIX "/service;1" + +/* 3b667ee0-d2da-4ccc-9c3d-95f2ca6a8b4c */ +#define VACUUMMANAGER_CID \ + { \ + 0x3b667ee0, 0xd2da, 0x4ccc, { \ + 0x9c, 0x3d, 0x95, 0xf2, 0xca, 0x6a, 0x8b, 0x4c \ + } \ + } + +#define VACUUMMANAGER_CONTRACTID MOZ_STORAGE_CONTRACTID_PREFIX "/vacuum;1" + +#endif /* MOZSTORAGECID_H */ diff --git a/storage/moz.build b/storage/moz.build new file mode 100644 index 0000000000..3dc1cd4bf7 --- /dev/null +++ b/storage/moz.build @@ -0,0 +1,112 @@ +# -*- 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 = ("Toolkit", "Storage") + +DIRS += ["build"] + +TEST_DIRS += ["test"] + +XPIDL_SOURCES += [ + "mozIStorageAsyncConnection.idl", + "mozIStorageAsyncStatement.idl", + "mozIStorageBaseStatement.idl", + "mozIStorageBindingParams.idl", + "mozIStorageBindingParamsArray.idl", + "mozIStorageCompletionCallback.idl", + "mozIStorageConnection.idl", + "mozIStorageError.idl", + "mozIStorageFunction.idl", + "mozIStoragePendingStatement.idl", + "mozIStorageProgressHandler.idl", + "mozIStorageResultSet.idl", + "mozIStorageRow.idl", + "mozIStorageService.idl", + "mozIStorageStatement.idl", + "mozIStorageStatementCallback.idl", + "mozIStorageVacuumParticipant.idl", + "mozIStorageValueArray.idl", +] + +XPIDL_MODULE = "storage" + +EXPORTS += [ + "mozStorageHelper.h", +] + +EXPORTS.mozilla += [ + "storage.h", +] + +# NOTE When adding something to this list, you probably need to add it to the +# storage.h file too. +EXPORTS.mozilla.storage += [ + "mozStorageAsyncStatementParams.h", + "mozStorageStatementParams.h", + "mozStorageStatementRow.h", + "SQLiteMutex.h", + "StatementCache.h", + "Variant.h", + "Variant_inl.h", +] +# SEE ABOVE NOTE! + +UNIFIED_SOURCES += [ + "BaseVFS.cpp", + "FileSystemModule.cpp", + "mozStorageArgValueArray.cpp", + "mozStorageAsyncStatement.cpp", + "mozStorageAsyncStatementExecution.cpp", + "mozStorageAsyncStatementJSHelper.cpp", + "mozStorageAsyncStatementParams.cpp", + "mozStorageBindingParamsArray.cpp", + "mozStorageError.cpp", + "mozStoragePrivateHelpers.cpp", + "mozStorageResultSet.cpp", + "mozStorageRow.cpp", + "mozStorageService.cpp", + "mozStorageSQLFunctions.cpp", + "mozStorageStatement.cpp", + "mozStorageStatementJSHelper.cpp", + "mozStorageStatementParams.cpp", + "mozStorageStatementRow.cpp", + "ObfuscatingVFS.cpp", + "QuotaVFS.cpp", + "ReadOnlyNoLockVFS.cpp", + "SQLCollations.cpp", + "StorageBaseStatementInternal.cpp", + "VacuumManager.cpp", + "Variant.cpp", +] + +# These files need to be built separately because they #include variantToSQLiteT_impl.h. +SOURCES += [ + "mozStorageBindingParams.cpp", + "mozStorageConnection.cpp", +] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" + +# Thunderbird needs the 2-argument version of fts3_tokenizer() +if CONFIG["MOZ_THUNDERBIRD"] or CONFIG["MOZ_SUITE"]: + DEFINES["MOZ_SQLITE_FTS3_TOKENIZER"] = 1 + +# Disable auxiliary files persistence if requested. +if not CONFIG["MOZ_AVOID_DISK_REMNANT_ON_CLOSE"]: + DEFINES["MOZ_SQLITE_PERSIST_AUXILIARY_FILES"] = 1 + +LOCAL_INCLUDES += [ + "/dom/base", + "/third_party/sqlite3/src", +] + +if CONFIG["MOZ_FOLD_LIBS"]: + DEFINES["MOZ_FOLD_LIBS"] = True + +CXXFLAGS += CONFIG["SQLITE_CFLAGS"] diff --git a/storage/mozIStorageAsyncConnection.idl b/storage/mozIStorageAsyncConnection.idl new file mode 100644 index 0000000000..3105750773 --- /dev/null +++ b/storage/mozIStorageAsyncConnection.idl @@ -0,0 +1,405 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 mozIStorageCompletionCallback; +interface mozIStorageFunction; +interface mozIStorageProgressHandler; +interface mozIStorageBaseStatement; +interface mozIStorageStatement; +interface mozIStorageAsyncStatement; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface nsIFile; + +/** + * mozIStorageAsyncConnection represents an asynchronous database + * connection attached to a specific file or to an in-memory data + * storage. It is the primary interface for interacting with a + * database from the main thread, including creating prepared + * statements, executing SQL, and examining database errors. + */ +[scriptable, uuid(8bfd34d5-4ddf-4e4b-89dd-9b14f33534c6)] +interface mozIStorageAsyncConnection : nsISupports { + /** + * Transaction behavior constants. + */ + const int32_t TRANSACTION_DEFAULT = -1; + const int32_t TRANSACTION_DEFERRED = 0; + const int32_t TRANSACTION_IMMEDIATE = 1; + const int32_t TRANSACTION_EXCLUSIVE = 2; + + /** + * The default behavior for all transactions run on this connection. Defaults + * to `TRANSACTION_DEFERRED`, and can be overridden for individual + * transactions. + */ + attribute int32_t defaultTransactionType; + + /** + * The maximum number of bound parameters for statements executed on this + * connection. If your statement has more params than this limit, you'll + * need to chunk them into multiple statements. See `PlacesUtils.chunkArray` + * and its callers in Places for examples of how to do this, or read on for + * an overview. + * + * Keep in mind that the variable limit is for the _total_ number of + * parameters, including ones bound by name (using the `:VVV`, `@VVV`, or + * `?VVV` syntax) and index (`?` and `?NNN`). + * + * This means, when chunking: + * + * - If you're binding 1 param per 1 value per chunk (for example, if you + * have a list of GUIDs and a clause like `WHERE guid IN (?, ?, ?, ...)`, + * your chunk length is just `variableLimit`. + * - If you're binding 1 param per 1 value per chunk, but using that + * param in multiple positions in the query (for example, `WHERE url_hash + * IN (hash(?1), hash(?2), ...) AND url IN (?1, ?2, ...)`), you can use the + * `?NNN` syntax with a chunk length of `variableLimit`. + * - If you're binding N params per 1 value per chunk (for example, if you + * have a list of items with GUIDs and parent GUIDs, and you want to bind + * both), your chunk length is `variableLimit / N`, since you're binding + * two params for each element. + * - If you're binding K params per L values per chunk, plus M fixed ones + * (for example, `WHERE parentGuid = :parentGuid AND guid IN (?, ?, ...)`), + * your chunk length is `variableLimit - M`, to ensure there's space for the + * fixed variables. + * + * If you bind more params than this limit, `create{Async}Statement` will + * fail with a "too many SQL variables" error. + */ + readonly attribute int32_t variableLimit; + + /** + * Returns true if a transaction is active on this connection. + * + * Note that this is true if a transaction is active on the connection, + * regardless of how it was opened. There are several ways to open one: + * + * 1. Explicitly calling `beginTransaction` on a `mozIStorageConnection`. + * 2. Calling `executeSimpleSQL("BEGIN")` or + * `createStatement("BEGIN").execute()` on a `mozIStorageConnection`. + * 3. Executing an async statement, like + * `createAsyncStatement("BEGIN").executeAsync(...)`. This is what + * `Sqlite.sys.mjs` does under the hood. + * + * Because of this, it's important *not* to use this attribute to decide + * whether to *commit* the active transaction, because the caller that opened + * it may not expect that. This is why both `mozStorageTransaction` and + * `Sqlite.sys.mjs` use an internal variable (`mHasTransaction` for the former; + * `_hasInProgressTransaction` for the latter) to check if their transaction + * is already in progress, instead of just checking this attribute before + * committing. Otherwise, mozStorage might accidentally commit (or roll back!) + * a transaction started by `Sqlite.sys.mjs`, and vice versa. + */ + readonly attribute boolean transactionInProgress; + + /** + * Close this database connection, allowing all pending statements + * to complete first. + * + * @param aCallback [optional] + * A callback that will be notified when the close is completed, + * with the following arguments: + * - status: the status of the call + * - value: |null| + * + * @throws NS_ERROR_NOT_SAME_THREAD + * If called on a thread other than the one that opened it. The + * callback will not be dispatched. + * @throws NS_ERROR_NOT_INITIALIZED + * If called on a connection that has already been closed or was + * never properly opened. The callback will still be dispatched + * to the main thread despite the returned error. + * @note If this call should fail, the callback won't be invoked. + */ + void asyncClose([optional] in mozIStorageCompletionCallback aCallback); + + /** + * Forcibly closes a database connection synchronously. + * This should only be used when it's required to close and replace the + * database synchronously to return control to the consumer, for example in + * case of a detected corruption on database opening. + * Since this spins the events loop, it should be used only in very particular + * and rare situations, or it may cause unexpected consequences (crashes). + * + * @throws NS_ERROR_NOT_SAME_THREAD + * If called on a thread other than the one that opened it. + */ + [noscript] void spinningSynchronousClose(); + + /** + * Clone a database connection and make the clone read only if needed. + * SQL Functions and attached on-disk databases are applied to the new clone. + * + * @param aReadOnly + * If true, the returned database should be put into read-only mode. + * + * @param aCallback + * A callback that will be notified when the operation is complete, + * with the following arguments: + * - status: the status of the operation + * - value: in case of success, an intance of + * mozIStorageAsyncConnection cloned from this one. + * + * @throws NS_ERROR_NOT_SAME_THREAD + * If is called on a thread other than the one that opened it. + * @throws NS_ERROR_UNEXPECTED + * If this connection is a memory database. + * + * @note If your connection is already read-only, you will get a read-only + * clone. + * @note The resulting connection will implement `mozIStorageConnection`, but + * all synchronous methods will throw if called from the main thread. + * @note Due to a bug in SQLite, if you use the shared cache + * (see mozIStorageService), you end up with the same privileges as the + * first connection opened regardless of what is specified in aReadOnly. + * @note The following pragmas are copied over to a read-only clone: + * - cache_size + * - temp_store + * The following pragmas are copied over to a writeable clone: + * - cache_size + * - temp_store + * - foreign_keys + * - journal_size_limit + * - synchronous + * - wal_autocheckpoint + * All SQL functions are copied over to read-only and writeable clones. + * Additionally, all temporary tables, triggers, and views, as well as + * any indexes on temporary tables, are copied over to writeable clones. + * For temporary tables, only the schemas are copied, not their + * contents. + */ + void asyncClone(in boolean aReadOnly, + in mozIStorageCompletionCallback aCallback); + + /** + * The current database nsIFile. Null if the database + * connection refers to an in-memory database. + */ + readonly attribute nsIFile databaseFile; + + /** + * Causes any pending database operation to abort and return at the first + * opportunity. + * @note this cannot be used on mozIStorageConnection unless the connection is + * explicitly marked as `interruptible`. For more details, please + * refer to CONNECTION_INTERRUPTIBLE in mozIStorageService. + * @note operations that are nearly complete may still be able to complete. + * @throws if used on an unsupported connection type, or a closed connection. + */ + void interrupt(); + + /** + * Vacuum the main database plus all the attached one. + * If the database is in auto_vacuum = INCREMENTAL mode, this executes an + * incremental_vacuum, otherwise it will always execute a full vacuum. + * + * While it's possible to invoke this method directly, it's suggested, when + * possible, to use the VacuumManager instead. + * That means registering your component for the "vacuum-participant" XPCOM + * category, and implement the mozIStorageVacuumParticipant interface. + * + * @param [aCallback] Completion callback invoked once the operation is + * complete. + * @param [aUseIncremental] When set to true, this will try to convert the + * main schema to auto_vacuum = INCREMENTAL mode, if it's not set yet. + * When set to false, it will try to set auto_vacuum = NONE. + * Note a full vacuum will be executed if the auto_vacuum mode must be + * changed, otherwise an incremental vacuum will happen if the database + * is already in INCREMENTAL mode. + * @param [aSetPageSize] This can be used to change the database page_size, a + * full vacuum will be executed to persist the change. If the page + * size is already correct, or you pass 0, this will be a no-op. + * @throws If it's not possible to start the async vacuum operation, note in + * this case the callback won't be invoked. + * @note Vacuum will fail inside a transaction, or if there is an ongoing + * read statement. + */ + void asyncVacuum( + [optional] in mozIStorageCompletionCallback aCallback, + [optional] in boolean aUseIncremental, + [optional] in long aSetPageSize + ); + + ////////////////////////////////////////////////////////////////////////////// + //// Statement creation + + /** + * Create an asynchronous statement for the given SQL. An + * asynchronous statement can only be used to dispatch asynchronous + * requests to the asynchronous execution thread and cannot be used + * to take any synchronous actions on the database. + * + * The expression may use ? to indicate sequential numbered arguments, + * ?1, ?2 etc. to indicate specific numbered arguments or :name and + * $var to indicate named arguments. + * + * @param aSQLStatement + * The SQL statement to execute. + * @return a new mozIStorageAsyncStatement + * @note The statement is created lazily on first execution. + */ + mozIStorageAsyncStatement createAsyncStatement(in AUTF8String aSQLStatement); + + /** + * Execute an array of statements created with this connection using + * any currently bound parameters. When the array contains multiple + * statements, the execution is wrapped in a single + * transaction. These statements can be reused immediately, and + * reset does not need to be called. + * + * @param aStatements + * The array of statements to execute asynchronously, in the order they + * are given in the array. + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statements execution. + * + * @note If you have any custom defined functions, they must be + * re-entrant since they can be called on multiple threads. + */ + mozIStoragePendingStatement executeAsync( + in Array aStatements, + [optional] in mozIStorageStatementCallback aCallback + ); + + /** + * Execute asynchronously an SQL expression, expecting no arguments. + * + * @param aSQLStatement + * The SQL statement to execute + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statement execution. + */ + mozIStoragePendingStatement executeSimpleSQLAsync( + in AUTF8String aSQLStatement, + [optional] in mozIStorageStatementCallback aCallback); + + + /** + * Loads a Sqlite Run-Time Loadable Extension as defined at + * https://www.sqlite.org/loadext.html. + * Only a predetermined list of extensions can be loaded, that are statically + * linked in the shared library containing SQLite. The currently supported + * extensions are: + * - fts5 + * A Full-Text search module, see https://www.sqlite.org/fts5.html + * + * New extensions can be added to the third_party/sqlite3/ext/ folder and then + * to this list, after a Storage peer has reviewed the request by verifying + * licensing, and code reliability. + * Extensions that must be loaded for all the connections should instead use + * sqlite3_auto_extension() (this must happen after sqlite3_config(), as it + * implicitly calls sqlite3_initialize()). + * + * @param aExtensionName + * The extension to load, see the above list for supported values. + * @param aCallback + * A callback that will be notified when the operation is complete, + * with the following arguments: + * - status: the status of the operation, use this to check if loading + * the extension was successful as it may be partly asynchronous. + * - value: unused. + * @throws NS_ERROR_INVALID_ARG + * For unsupported extension names. + * @throws NS_ERROR_NOT_INITIALIZED + * If the connection is not open. + * @throws NS_ERROR_UEXPECTED + * If it was not possible to enable extensions loading. + */ + void loadExtension(in AUTF8String aExtensionName, + [optional] in mozIStorageCompletionCallback aCallback); + + ////////////////////////////////////////////////////////////////////////////// + //// Functions + + /** + * Create a new SQL function. If you use your connection on multiple threads, + * your function needs to be threadsafe, or it should only be called on one + * thread. + * + * @param aFunctionName + * The name of function to create, as seen in SQL. + * @param aNumArguments + * The number of arguments the function takes. Pass -1 for + * variable-argument functions. + * @param aFunction + * The instance of mozIStorageFunction, which implements the function + * in question. + */ + void createFunction(in AUTF8String aFunctionName, + in long aNumArguments, + in mozIStorageFunction aFunction); + + /** + * Delete custom SQL function. + * + * @param aFunctionName + * The name of function to remove. + */ + void removeFunction(in AUTF8String aFunctionName); + + /** + * Sets a progress handler. Only one handler can be registered at a time. + * If you need more than one, you need to chain them yourself. This progress + * handler should be threadsafe if you use this connection object on more than + * one thread. + * + * @param aGranularity + * The number of SQL virtual machine steps between progress handler + * callbacks. + * @param aHandler + * The instance of mozIStorageProgressHandler. + * @return previous registered handler. + */ + mozIStorageProgressHandler setProgressHandler(in int32_t aGranularity, + in mozIStorageProgressHandler aHandler); + + /** + * Remove a progress handler. + * + * @return previous registered handler. + */ + mozIStorageProgressHandler removeProgressHandler(); + + /** + * Makes a copy of a database asynchronously. This method can do an online + * backup of the database file, even if there are open connections actively + * using it (as a normal file copy can only be made if no connections are + * open on the database). + * + * While the copy is in the process of being made, the destination file + * will have a .tmp extension appended to it. In the event of a crash + * during the copy, this .tmp file will continue to exist, but will be + * an unusable partial copy. + * + * Once the copy has been completed, this method will automatically remove + * the .tmp extension. + * + * @param aDestinationFile + * The destination on the file system to write the database copy. + * @param aCallback + * A callback that will be notified when the operation is complete, + * with the following arguments: + * - status: the status of the operation, use this to check if making + * the copy was successful. + * - value: unused. + * @throws NS_ERROR_ABORT + * If the application has begun the process of shutting down already. + * @throws NS_ERROR_NOT_INITIALIZED + * If the connection has already started or completed closing. + * @throws NS_ERROR_NOT_AVAILABLE + * If the database does not support asynchronous operations. + * @throws NS_ERROR_NOT_INITIALIZED + * If the execution thread cannot be acquired. + */ + void backupToFileAsync(in nsIFile aDestinationFile, + in mozIStorageCompletionCallback aCallback); +}; diff --git a/storage/mozIStorageAsyncStatement.idl b/storage/mozIStorageAsyncStatement.idl new file mode 100644 index 0000000000..de7369fcaa --- /dev/null +++ b/storage/mozIStorageAsyncStatement.idl @@ -0,0 +1,37 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 "mozIStorageBaseStatement.idl" + +/** + * An asynchronous SQL statement. This differs from mozIStorageStatement by + * only being usable for asynchronous execution. (mozIStorageStatement can + * be used for both synchronous and asynchronous purposes.) This specialization + * for asynchronous operation allows us to avoid needing to acquire + * synchronization primitives also used by the asynchronous execution thread. + * In contrast, mozIStorageStatement may need to acquire the primitives and + * consequently can cause the main thread to lock for extended intervals while + * the asynchronous thread performs some long-running operation. + */ +[scriptable, builtinclass, uuid(52e49370-3b2e-4a27-a3fc-79e20ad4056b)] +interface mozIStorageAsyncStatement : mozIStorageBaseStatement { + /* + * 'params' provides a magic JS helper that lets you assign parameters by + * name. Unlike the helper on mozIStorageStatement, you cannot enumerate + * in order to find out what parameters are legal. + * + * This does not work for BLOBs. You must use an explicit binding API for + * that. + * + * example: + * stmt.params.foo = 1; + * stmt.params["bar"] = 2; + * let argName = "baz"; + * stmt.params[argName] = 3; + * + * readonly attribute nsIMagic params; + */ +}; diff --git a/storage/mozIStorageBaseStatement.idl b/storage/mozIStorageBaseStatement.idl new file mode 100644 index 0000000000..cd869d8fde --- /dev/null +++ b/storage/mozIStorageBaseStatement.idl @@ -0,0 +1,125 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * 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" +#include "mozIStorageBindingParams.idl" + +interface mozIStorageConnection; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface mozIStorageBindingParams; +interface mozIStorageBindingParamsArray; + +/** + * The base interface for both pure asynchronous storage statements + * (mozIStorageAsyncStatement) and 'classic' storage statements + * (mozIStorageStatement) that can be used for both synchronous and asynchronous + * purposes. + */ +[scriptable, builtinclass, uuid(16ca67aa-1325-43e2-aac7-859afd1590b2)] +interface mozIStorageBaseStatement : mozIStorageBindingParams { + /** + * Finalizes a statement so you can successfully close a database connection. + * Once a statement has been finalized it can no longer be used for any + * purpose. + * + * Statements are implicitly finalized when their reference counts hits zero. + * If you are a native (C++) caller this is accomplished by setting all of + * your nsCOMPtr instances to be NULL. If you are operating from JavaScript + * code then you cannot rely on this behavior because of the involvement of + * garbage collection. + * + * When finalizing an asynchronous statement you do not need to worry about + * whether the statement has actually been executed by the asynchronous + * thread; you just need to call finalize after your last call to executeAsync + * involving the statement. However, you do need to use asyncClose instead of + * close on the connection if any statements have been used asynchronously. + */ + void finalize(); + + /** + * Binds the array of parameters to the statement. When executeAsync is + * called, all the parameters in aParameters are bound and then executed. + * + * @param aParameters + * The array of parameters to bind to the statement upon execution. + * + * @note This is only works on statements being used asynchronously. + */ + void bindParameters(in mozIStorageBindingParamsArray aParameters); + + /** + * Creates a new mozIStorageBindingParamsArray that can be used to bind + * multiple sets of data to a statement with bindParameters. + * + * @return a mozIStorageBindingParamsArray that multiple sets of parameters + * can be bound to. + * + * @note This is only useful for statements being used asynchronously. + */ + mozIStorageBindingParamsArray newBindingParamsArray(); + + /** + * Execute a query asynchronously using any currently bound parameters. This + * statement can be reused immediately, and reset does not need to be called. + * + * @note If you have any custom defined functions, they must be re-entrant + * since they can be called on multiple threads. + * + * @param aCallback [optional] + * The callback object that will be notified of progress, errors, and + * completion. + * @return an object that can be used to cancel the statements execution. + */ + mozIStoragePendingStatement executeAsync( + [optional] in mozIStorageStatementCallback aCallback + ); + + /** + * The statement is not usable, either because it failed to initialize or + * was explicitly finalized. + */ + const long MOZ_STORAGE_STATEMENT_INVALID = 0; + /** + * The statement is usable. + */ + const long MOZ_STORAGE_STATEMENT_READY = 1; + /** + * Indicates that the statement is executing and the row getters may be used. + * + * @note This is only relevant for mozIStorageStatement instances being used + * in a synchronous fashion. + */ + const long MOZ_STORAGE_STATEMENT_EXECUTING = 2; + + /** + * Find out whether the statement is usable (has not been finalized). + */ + readonly attribute long state; + + /** + * Escape a string for SQL LIKE search. + * + * @note Consumers will have to use same escape char when doing statements + * such as: ...LIKE '?1' ESCAPE '/'... + * + * @param aValue + * The string to escape for SQL LIKE. + * @param aEscapeChar + * The escape character. + * @return an AString of an escaped version of aValue + * (%, _ and the escape char are escaped with the escape char) + * For example, we will convert "foo/bar_baz%20cheese" + * into "foo//bar/_baz/%20cheese" (if the escape char is '/'). + */ + AString escapeStringForLIKE(in AString aValue, in wchar aEscapeChar); + + /** + * The same as above, but for UTF8 strings. + */ + AUTF8String escapeUTF8StringForLIKE(in AUTF8String aValue, + in char aEscapeChar); +}; diff --git a/storage/mozIStorageBindingParams.idl b/storage/mozIStorageBindingParams.idl new file mode 100644 index 0000000000..6a5279e07d --- /dev/null +++ b/storage/mozIStorageBindingParams.idl @@ -0,0 +1,103 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * 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 nsIVariant; + +[ptr] native octetPtr(uint8_t); + +[scriptable, builtinclass, uuid(2d09f42f-966e-4663-b4b3-b0c8676bf2bf)] +interface mozIStorageBindingParams : nsISupports { + /** + * Binds aValue to the parameter with the name aName. + * + * @param aName + * The name of the parameter to bind aValue to. + * @param aValue + * The value to bind. + */ + void bindByName(in AUTF8String aName, + in nsIVariant aValue); + [noscript] void bindUTF8StringByName(in AUTF8String aName, + in AUTF8String aValue); + [noscript] void bindStringByName(in AUTF8String aName, + in AString aValue); + [noscript] void bindDoubleByName(in AUTF8String aName, + in double aValue); + [noscript] void bindInt32ByName(in AUTF8String aName, + in long aValue); + [noscript] void bindInt64ByName(in AUTF8String aName, + in long long aValue); + [noscript] void bindNullByName(in AUTF8String aName); + + // The noscript version of bindBlobByName can be used with any (const + // uint8_t*, length) pair. The scriptable version is meant for use with + // nsTArray, which is what xpconnect has to work with. + [noscript, binaryname(BindBlobByName)] + void bindBlobByNameNoscript(in AUTF8String aName, + [const] in octetPtr aValue, + in unsigned long aValueSize); + [binaryname(BindBlobArrayByName)] + void bindBlobByName(in AUTF8String aName, in Array aValue); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByName(in AUTF8String aName, in AString aValue); + void bindUTF8StringAsBlobByName(in AUTF8String aName, in AUTF8String aValue); + + // The function adopts the storage for the provided blob. After calling + // this function, mozStorage will ensure that free is called on the + // underlying pointer. + [noscript] + void bindAdoptedBlobByName(in AUTF8String aName, + in octetPtr aValue, + in unsigned long aValueSize); + + /** + * Binds aValue to the parameter with the index aIndex. + * + * @param aIndex + * The zero-based index of the parameter to bind aValue to. + * @param aValue + * The value to bind. + */ + void bindByIndex(in unsigned long aIndex, + in nsIVariant aValue); + [noscript] void bindUTF8StringByIndex(in unsigned long aIndex, + in AUTF8String aValue); + [noscript] void bindStringByIndex(in unsigned long aIndex, + in AString aValue); + [noscript] void bindDoubleByIndex(in unsigned long aIndex, + in double aValue); + [noscript] void bindInt32ByIndex(in unsigned long aIndex, + in long aValue); + [noscript] void bindInt64ByIndex(in unsigned long aIndex, + in long long aValue); + [noscript] void bindNullByIndex(in unsigned long aIndex); + + // The noscript version of bindBlobByIndex can be used with any (const + // uint8_t*, length) pair. The scriptable version is meant for use with + // nsTArray, which is what xpconnect has to work with. + [noscript, binaryname(BindBlobByIndex)] + void bindBlobByIndexNoscript(in unsigned long aIndex, + [const] in octetPtr aValue, + in unsigned long aValueSize); + [binaryname(BindBlobArrayByIndex)] + void bindBlobByIndex(in unsigned long aIndex, + in Array aValue); + + // Convenience routines for storing strings as blobs. + void bindStringAsBlobByIndex(in unsigned long aIndex, in AString aValue); + void bindUTF8StringAsBlobByIndex(in unsigned long aIndex, in AUTF8String aValue); + + // The function adopts the storage for the provided blob. After calling + // this function, mozStorage will ensure that free is called on the + // underlying pointer. + [noscript] + void bindAdoptedBlobByIndex(in unsigned long aIndex, + in octetPtr aValue, + in unsigned long aValueSize); +}; diff --git a/storage/mozIStorageBindingParamsArray.idl b/storage/mozIStorageBindingParamsArray.idl new file mode 100644 index 0000000000..5f504c0514 --- /dev/null +++ b/storage/mozIStorageBindingParamsArray.idl @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * 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 mozIStorageBindingParams; + +[scriptable, uuid(67eea5c3-4881-41ff-b0fe-09f2356aeadb)] +interface mozIStorageBindingParamsArray : nsISupports { + /** + * Creates a new mozIStorageBindingParams object that can be added to this + * array. + * + * @return a mozIStorageBindingParams object that can be used to specify + * parameters that need to be bound. + */ + mozIStorageBindingParams newBindingParams(); + + /** + * Adds the parameters to the end of this array. + * + * @param aParameters + * The parameters to add to this array. + */ + void addParams(in mozIStorageBindingParams aParameters); + + /** + * The number of mozIStorageBindingParams this object contains. + */ + readonly attribute unsigned long length; +}; diff --git a/storage/mozIStorageCompletionCallback.idl b/storage/mozIStorageCompletionCallback.idl new file mode 100644 index 0000000000..1c31cc2c26 --- /dev/null +++ b/storage/mozIStorageCompletionCallback.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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(8cbf2dc2-91e0-44bc-984f-553638412071)] +interface mozIStorageCompletionCallback : nsISupports { + /** + * Indicates that the event this callback was passed in for has completed. + * + * @param status + * The status of the call. Generally NS_OK if the operation + * completed successfully. + * @param value + * If the operation produces a result, the result. Otherwise, + * |null|. + * + * @see The calling method for expected values. + */ + void complete(in nsresult status, [optional] in nsISupports value); +}; diff --git a/storage/mozIStorageConnection.idl b/storage/mozIStorageConnection.idl new file mode 100644 index 0000000000..34217b5f42 --- /dev/null +++ b/storage/mozIStorageConnection.idl @@ -0,0 +1,283 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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" +#include "mozIStorageAsyncConnection.idl" + +%{C++ +namespace mozilla::dom::quota { +class QuotaObject; +} + +namespace mozilla::storage { +class SQLiteMutex; +class SQLiteMutexAutoLock; +} + +%} + +[ptr] native QuotaObject(mozilla::dom::quota::QuotaObject); +native SQLiteMutex(mozilla::storage::SQLiteMutex&); +native SQLiteMutexAutoLock(const mozilla::storage::SQLiteMutexAutoLock&); + +interface mozIStorageAggregateFunction; +interface mozIStorageCompletionCallback; +interface mozIStorageFunction; +interface mozIStorageProgressHandler; +interface mozIStorageBaseStatement; +interface mozIStorageStatement; +interface mozIStorageAsyncStatement; +interface mozIStorageStatementCallback; +interface mozIStoragePendingStatement; +interface nsIFile; + +/** + * mozIStorageConnection represents a database connection attached to + * a specific file or to the in-memory data storage. It is the + * primary interface for interacting with a database, including + * creating prepared statements, executing SQL, and examining database + * errors. + * + * @note From the main thread, you should rather use mozIStorageAsyncConnection. + * + * @threadsafe + */ +[scriptable, builtinclass, uuid(4aa2ac47-8d24-4004-9b31-ec0bd85f0cc3)] +interface mozIStorageConnection : mozIStorageAsyncConnection { + /** + * Closes a database connection. Callers must finalize all statements created + * for this connection prior to calling this method. It is illegal to use + * call this method if any asynchronous statements have been executed on this + * connection. + * + * @throws NS_ERROR_UNEXPECTED + * If any statement has been executed asynchronously on this object. + * @throws NS_ERROR_UNEXPECTED + * If is called on a thread other than the one that opened it. + */ + void close(); + + /** + * Clones a database connection and makes the clone read only if needed. + * SQL Functions and attached on-disk databases are applied to the new clone. + * + * @param aReadOnly + * If true, the returned database should be put into read-only mode. + * Defaults to false. + * @return the cloned database connection. + * + * @throws NS_ERROR_UNEXPECTED + * If this connection is a memory database. + * @note If your connection is already read-only, you will get a read-only + * clone. + * @note Due to a bug in SQLite, if you use the shared cache (openDatabase), + * you end up with the same privileges as the first connection opened + * regardless of what is specified in aReadOnly. + * @note The following pragmas are copied over to a read-only clone: + * - cache_size + * - temp_store + * The following pragmas are copied over to a writeable clone: + * - cache_size + * - temp_store + * - foreign_keys + * - journal_size_limit + * - synchronous + * - wal_autocheckpoint + * All SQL functions are copied over to read-only and writeable clones. + * Additionally, all temporary tables, triggers, and views, as well as + * any indexes on temporary tables, are copied over to writeable clones. + * For temporary tables, only the schemas are copied, not their + * contents. + * + */ + mozIStorageConnection clone([optional] in boolean aReadOnly); + + /** + * The default size for SQLite database pages used by mozStorage for new + * databases. + */ + readonly attribute long defaultPageSize; + + /** + * Indicates if the connection is open and ready to use. This will be false + * if the connection failed to open, or it has been closed. + */ + readonly attribute boolean connectionReady; + + /** + * lastInsertRowID returns the row ID from the last INSERT + * operation. + */ + readonly attribute long long lastInsertRowID; + + /** + * affectedRows returns the number of database rows that were changed or + * inserted or deleted by last operation. + */ + readonly attribute long affectedRows; + + /** + * The last error SQLite error code. + */ + readonly attribute long lastError; + + /** + * The last SQLite error as a string (in english, straight from the + * sqlite library). + */ + readonly attribute AUTF8String lastErrorString; + + /** + * The schema version of the database. This should not be used until the + * database is ready. The schema will be reported as zero if it is not set. + */ + attribute long schemaVersion; + + ////////////////////////////////////////////////////////////////////////////// + //// Statement creation + + /** + * Create a mozIStorageStatement for the given SQL expression. The + * expression may use ? to indicate sequential numbered arguments, + * ?1, ?2 etc. to indicate specific numbered arguments or :name and + * $var to indicate named arguments. + * + * @param aSQLStatement + * The SQL statement to execute. + * @return a new mozIStorageStatement + */ + mozIStorageStatement createStatement(in AUTF8String aSQLStatement); + + /** + * Execute a SQL expression, expecting no arguments. + * + * @param aSQLStatement The SQL statement to execute + */ + void executeSimpleSQL(in AUTF8String aSQLStatement); + + /** + * Check if the given table exists. + * + * @param aTableName + * The table to check + * @return TRUE if table exists, FALSE otherwise. + */ + boolean tableExists(in AUTF8String aTableName); + + /** + * Check if the given index exists. + * + * @param aIndexName The index to check + * @return TRUE if the index exists, FALSE otherwise. + */ + boolean indexExists(in AUTF8String aIndexName); + + ////////////////////////////////////////////////////////////////////////////// + //// Transactions + + /** + * Begin a new transaction. If a transaction is active, throws an error. + */ + void beginTransaction(); + + /** + * Commits the current transaction. If no transaction is active, + * @throws NS_ERROR_UNEXPECTED. + * @throws NS_ERROR_NOT_INITIALIZED. + */ + void commitTransaction(); + + /** + * Rolls back the current transaction. If no transaction is active, + * @throws NS_ERROR_UNEXPECTED. + * @throws NS_ERROR_NOT_INITIALIZED. + */ + void rollbackTransaction(); + + ////////////////////////////////////////////////////////////////////////////// + //// Tables + + /** + * Create the table with the given name and schema. + * + * If the table already exists, NS_ERROR_FAILURE is thrown. + * (XXX at some point in the future it will check if the schema is + * the same as what is specified, but that doesn't happen currently.) + * + * @param aTableName + * The table name to be created, consisting of [A-Za-z0-9_], and + * beginning with a letter. + * @param aTableSchema + * The schema of the table; what would normally go between the parens + * in a CREATE TABLE statement: e.g., "foo INTEGER, bar STRING". + * + * @throws NS_ERROR_FAILURE + * If the table already exists or could not be created for any other + * reason. + */ + void createTable(in string aTableName, + in string aTableSchema); + + /** + * Controls SQLITE_FCNTL_CHUNK_SIZE setting in sqlite. This helps avoid fragmentation + * by growing/shrinking the database file in SQLITE_FCNTL_CHUNK_SIZE increments. To + * conserve memory on systems short on storage space, this function will have no effect + * on mobile devices or if less than 500MiB of space is left available. + * + * @param aIncrement + * The database file will grow in multiples of chunkSize. + * @param aDatabaseName + * Sqlite database name. "" means pass NULL for zDbName to sqlite3_file_control. + * See http://sqlite.org/c3ref/file_control.html for more details. + * @throws NS_ERROR_FILE_TOO_BIG + * If the system is short on storage space. + */ + void setGrowthIncrement(in int32_t aIncrement, in AUTF8String aDatabaseName); + + /** + * Enable a predefined virtual table implementation. + * + * @param aModuleName + * The module to enable. Only "filesystem" is currently supported. + * + * @throws NS_ERROR_FAILURE + * For unknown module names. + */ + [noscript] void enableModule(in ACString aModuleName); + + /** + * Get quota objects. + * + * @param[out] aDatabaseQuotaObject + * The QuotaObject associated with the database file. + * @param[out] aJournalQuotaObject + * The QuotaObject associated with the journal file. + * + * @throws NS_ERROR_NOT_INITIALIZED. + */ + [noscript] void getQuotaObjects(out QuotaObject aDatabaseQuotaObject, + out QuotaObject aJournalQuotaObject); + + /** + * The mutex used for protection of operations (BEGIN/COMMIT/ROLLBACK) in + * mozStorageTransaction. The lock must be held in a way that spans whole + * operation, not just when accessing the nesting level. + */ + [notxpcom, nostdcall] readonly attribute SQLiteMutex sharedDBMutex; + + /** + * Helper methods for managing the transaction nesting level. The methods + * must be called with a proof of lock. Currently only used by + * mozStorageTransaction. + */ + [notxpcom, nostdcall] unsigned long getTransactionNestingLevel( + in SQLiteMutexAutoLock aProofOfLock); + + [notxpcom, nostdcall] unsigned long increaseTransactionNestingLevel( + in SQLiteMutexAutoLock aProofOfLock); + + [notxpcom, nostdcall] unsigned long decreaseTransactionNestingLevel( + in SQLiteMutexAutoLock aProofOfLock); +}; diff --git a/storage/mozIStorageError.idl b/storage/mozIStorageError.idl new file mode 100644 index 0000000000..1c6df5bb69 --- /dev/null +++ b/storage/mozIStorageError.idl @@ -0,0 +1,149 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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" + +%{C++ +#ifdef ERROR +#undef ERROR +#endif +%} + +[scriptable, uuid(1f350f96-7023-434a-8864-40a1c493aac1)] +interface mozIStorageError : nsISupports { + + /** + * General SQL error or missing database. + */ + const long ERROR = 1; + + /** + * Internal logic error. + */ + const long INTERNAL = 2; + + /** + * Access permission denied. + */ + const long PERM = 3; + + /** + * A callback routine requested an abort. + */ + const long ABORT = 4; + + /** + * The database file is locked. + */ + const long BUSY = 5; + + /** + * A table in the database is locked. + */ + const long LOCKED = 6; + + /** + * An allocation failed. + */ + const long NOMEM = 7; + + /** + * Attempt to write to a readonly database. + */ + const long READONLY = 8; + + /** + * Operation was terminated by an interrupt. + */ + const long INTERRUPT = 9; + + /** + * Some kind of disk I/O error occurred. + */ + const long IOERR = 10; + + /** + * The database disk image is malformed. + */ + const long CORRUPT = 11; + + /** + * An insertion failed because the database is full. + */ + const long FULL = 13; + + /** + * Unable to open the database file. + */ + const long CANTOPEN = 14; + + /** + * The database is empty. + */ + const long EMPTY = 16; + + /** + * The database scheme changed. + */ + const long SCHEMA = 17; + + /** + * A string or blob exceeds the size limit. + */ + const long TOOBIG = 18; + + /** + * Abort due to a constraint violation. + */ + const long CONSTRAINT = 19; + + /** + * Data type mismatch. + */ + const long MISMATCH = 20; + + /** + * Library used incorrectly. + */ + const long MISUSE = 21; + + /** + * Uses OS features not supported on the host system. + */ + const long NOLFS = 22; + + /** + * Authorization denied. + */ + const long AUTH = 23; + + /** + * Auxiliary database format error. + */ + const long FORMAT = 24; + + /** + * Attempt to bind a parameter using an out-of-range index or nonexistent + * named parameter name. + */ + const long RANGE = 25; + + /** + * File opened that is not a database file. + */ + const long NOTADB = 26; + + + /** + * Indicates what type of error occurred. + */ + readonly attribute long result; + + /** + * An error string the gives more details, if available. + */ + readonly attribute AUTF8String message; +}; diff --git a/storage/mozIStorageFunction.idl b/storage/mozIStorageFunction.idl new file mode 100644 index 0000000000..f4cd8de42e --- /dev/null +++ b/storage/mozIStorageFunction.idl @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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" + +#include "mozIStorageValueArray.idl" + +interface mozIStorageConnection; +interface nsIArray; +interface nsIVariant; + +/** + * mozIStorageFunction is to be implemented by storage consumers that + * wish to receive callbacks during the request execution. + * + * SQL can apply functions to values from tables. Examples of + * such functions are MIN(a1,a2) or SQRT(num). Many functions are + * implemented in SQL engine. + * + * This interface allows consumers to implement their own, + * problem-specific functions. + * These functions can be called from triggers, too. + * + */ +[scriptable, function, uuid(9ff02465-21cb-49f3-b975-7d5b38ceec73)] +interface mozIStorageFunction : nsISupports { + /** + * onFunctionCall is called when execution of a custom + * function should occur. + * + * @param aNumArguments The number of arguments + * @param aFunctionArguments The arguments passed in to the function + * + * @returns any value as Variant type. + */ + + nsIVariant onFunctionCall(in mozIStorageValueArray aFunctionArguments); +}; diff --git a/storage/mozIStoragePendingStatement.idl b/storage/mozIStoragePendingStatement.idl new file mode 100644 index 0000000000..d1754aec5c --- /dev/null +++ b/storage/mozIStoragePendingStatement.idl @@ -0,0 +1,20 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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, uuid(00da7d20-3768-4398-bedc-e310c324b3f0)] +interface mozIStoragePendingStatement : nsISupports { + + /** + * Cancels a pending statement, if possible. This will only fail if you try + * cancel more than once. + * + * @note For read statements (such as SELECT), you will no longer receive any + * notifications about results once cancel is called. + */ + void cancel(); +}; diff --git a/storage/mozIStorageProgressHandler.idl b/storage/mozIStorageProgressHandler.idl new file mode 100644 index 0000000000..a619365600 --- /dev/null +++ b/storage/mozIStorageProgressHandler.idl @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 mozIStorageConnection; + +/** + * mozIProgressHandler is to be implemented by storage consumers that + * wish to receive callbacks during the request execution. + */ +[scriptable, uuid(a3a6fcd4-bf89-4208-a837-bf2a73afd30c)] +interface mozIStorageProgressHandler : nsISupports { + /** + * onProgress is invoked periodically during long running calls. + * + * @param aConnection connection, for which progress handler is + * invoked. + * + * @return true to abort request, false to continue work. + */ + + boolean onProgress(in mozIStorageConnection aConnection); +}; diff --git a/storage/mozIStorageResultSet.idl b/storage/mozIStorageResultSet.idl new file mode 100644 index 0000000000..4edb08d4ca --- /dev/null +++ b/storage/mozIStorageResultSet.idl @@ -0,0 +1,21 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 mozIStorageRow; + +[scriptable, uuid(18dd7953-076d-4598-8105-3e32ad26ab24)] +interface mozIStorageResultSet : nsISupports { + + /** + * Obtains the next row from the result set from the statement that was + * executed. + * + * @returns the next row from the result set. This will be null when there + * are no more results. + */ + mozIStorageRow getNextRow(); +}; diff --git a/storage/mozIStorageRow.idl b/storage/mozIStorageRow.idl new file mode 100644 index 0000000000..f14d7c5c71 --- /dev/null +++ b/storage/mozIStorageRow.idl @@ -0,0 +1,33 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 "mozIStorageValueArray.idl" +interface nsIVariant; + +[scriptable, uuid(62d1b6bd-cbfe-4f9b-aee1-0ead4af4e6dc)] +interface mozIStorageRow : mozIStorageValueArray { + + /** + * Obtains the result of a given column specified by aIndex. + * + * @param aIndex + * Zero-based index of the result to get from the tuple. + * @returns the result of the specified column. + */ + nsIVariant getResultByIndex(in unsigned long aIndex); + + /** + * Obtains the result of a given column specified by aName. + * + * @param aName + * Name of the result to get from the tuple. + * @returns the result of the specified column. + * @note The name of a result column is the value of the "AS" clause for that + * column. If there is no AS clause then the name of the column is + * unspecified and may change from one release to the next. + */ + nsIVariant getResultByName(in AUTF8String aName); +}; diff --git a/storage/mozIStorageService.idl b/storage/mozIStorageService.idl new file mode 100644 index 0000000000..22c1dc0565 --- /dev/null +++ b/storage/mozIStorageService.idl @@ -0,0 +1,290 @@ +/* -*- Mode: idl; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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" + +%{C++ + +#include "nsLiteralString.h" + +%} + +interface mozIStorageConnection; +interface nsIFile; +interface nsIFileURL; +interface nsIPropertyBag2; +interface nsIVariant; +interface mozIStorageCompletionCallback; + +/** + * PRIVACY WARNING + * =============== + * + * Database file names can be exposed through telemetry and in crash reports on + * the https://crash-stats.mozilla.org site, to allow recognizing the affected + * database. + * if your database name may contain privacy sensitive information, e.g. an + * URL origin, you should use openDatabaseWithFileURL and pass an explicit + * TelemetryFilename to it. That name will be used both for telemetry and for + * thread names in crash reports. + * If you have different needs (e.g. using the javascript module or an async + * connection from the main thread) please coordinate with the mozStorage peers. + */ + +/** + * The mozIStorageService interface is intended to be implemented by + * a service that can create storage connections (mozIStorageConnection) + * to either a well-known profile database or to a specific database file. + * + * This is the only way to open a database connection. + * + * @note The first reference to mozIStorageService must be made on the main + * thread. + */ +[scriptable, uuid(07b6b2f5-6d97-47b4-9584-e65bc467fe9e)] +interface mozIStorageService : nsISupports { + /** + * Open the database with default flags in default mode. + */ + const unsigned long OPEN_DEFAULT = 0; + + /** + * Open the database with a shared cache. The shared-cache mode + * is more memory-efficient when many connections to the same database + * are expected, though, the connections will contend the cache resource. + * When performance matters, working without a shared-cache will + * improve concurrency. @see openUnsharedDatabase + */ + const unsigned long OPEN_SHARED = 1 << 0; + + /** + * Open the underlying database in read-only mode. + */ + const unsigned long OPEN_READONLY = 1 << 1; + + /** + * Allow simultaneous access to an asynchronous read-only database + * without any file locking. + * + * For synchronous database, the flag has no effect. + * + * Specifying the OPEN_IGNORE_LOCKING_MODE flag will automatically + * turn on the OPEN_READONLY flag. + */ + const unsigned long OPEN_IGNORE_LOCKING_MODE = 1 << 2; + + /** + * All optional connection object features are off. + */ + const unsigned long CONNECTION_DEFAULT = 0; + + /** + * Enable Interrupt-method for the synchronous connection object + * returned by openDatabase, openSpecialDatabase, openUnsharedDatabase + * or openDatabaseWithFileURL calls. + * + * When this flag is not set, Interrupt-method of a + * synchronous connection must not be used. + * + * Asynchronous connection is always interruptible and the flag + * does not change anything. + * + * The following are among the potential risks side effects of + * calling the Interrupt-method: + * - new queries started on a different thread after the + * interrupt call, but before its completion, are interrupted as if + * they had been running prior to the interrupt call. Thus thread + * synchronization is necessary. + * - calls to close the database will wait until the interruption + * finishes. + */ + const unsigned long CONNECTION_INTERRUPTIBLE = 1 << 0; + + /** + * Open an asynchronous connection to a database. + * + * This method MUST be called from the main thread. The connection object + * returned by this function is not threadsafe. You MUST use it only from + * the main thread. + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * @param aDatabaseStore Either a nsIFile representing the file that contains + * the database or a special string to open a special database. The special + * string may be: + * - "memory" to open an in-memory database. + * + * @param aOpenFlags + * A set of flags to open the database with optional features. + * Currently supports OPEN_SHARED, OPEN_READONLY and + * OPEN_IGNORE_LOCKING_MODE flags. + * For full details, please refer to the documentation of the flags. + * + * @param aConnectionFlags + * A set of flags to enable optional features for the returned + * asynchronous connection object. + * Currently supports CONNECTION_INTERRUPTIBLE flag. + * For full details, please refer to the documentation of the flag. + * + * @param aCallback A callback that will receive the result of the operation. + * In case of error, it may receive as status: + * - NS_ERROR_OUT_OF_MEMORY if allocating a new storage object fails. + * - NS_ERROR_FILE_CORRUPTED if the database file is corrupted. + * In case of success, it receives as argument the new database + * connection, as an instance of |mozIStorageAsyncConnection|. + * + * @throws NS_ERROR_INVALID_ARG if |aDatabaseStore| is neither a file nor + * one of the special strings understood by this method, or if one of + * the options passed through |aOptions| does not have + * the right type. + * @throws NS_ERROR_NOT_SAME_THREAD if called from a thread other than the + * main thread. + */ + void openAsyncDatabase(in nsIVariant aDatabaseStore, + in unsigned long aOpenFlags, + in unsigned long aConnectionFlags, + in mozIStorageCompletionCallback aCallback); + + /** + * Get a connection to a named special database storage. + * + * @param aStorageKey a string key identifying the type of storage + * requested. Valid values include: "memory". + * + * @param aName an optional string identifying the name of the database. + * If omitted, a filename of ":memory:" will be used which results in a + * private in-memory database specific to this connection, making it + * impossible to clone the in-memory database. If you want to be able to + * clone the connection (or otherwise connect to the in-memory database from + * a connection), then you must pick a name that's sufficiently unique within + * the process to not collide with other mozStorage users. + * + * @param [optional] aConnectionFlags + * A set of flags to enable optional features for the returned + * synchronous connection object. + * Currently supports CONNECTION_INTERRUPTIBLE flag. + * For full details, please refer to the documentation of the flag. + * + * @see openDatabase for restrictions on how database connections may be + * used. For the profile database, you should only access it from the main + * thread since other callers may also have connections. + * + * @returns a new mozIStorageConnection for the requested + * storage database. + * + * @throws NS_ERROR_INVALID_ARG if aStorageKey is invalid. + */ + mozIStorageConnection openSpecialDatabase( + in ACString aStorageKey, + [optional] in ACString aName, + [optional] in unsigned long aConnectionFlags); + + /** + * Open a connection to the specified file. + * + * Consumers should check mozIStorageConnection::connectionReady to ensure + * that they can use the database. If this value is false, it is strongly + * recommended that the database be backed up with + * mozIStorageConnection::backupDB so user data is not lost. + * + * ========== + * DANGER + * ========== + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * The connection object returned by this function is not threadsafe. You + * must use it only from the thread you created it from. + * + * @param aDatabaseFile + * A nsIFile that represents the database that is to be opened. + * @param [optional] aConnectionFlags + * A set of flags to enable optional features for the returned + * synchronous connection object. + * Currently supports CONNECTION_INTERRUPTIBLE flag. + * For full details, please refer to the documentation of the flag. + * + * @returns a mozIStorageConnection for the requested database file. + * + * @throws NS_ERROR_OUT_OF_MEMORY + * If allocating a new storage object fails. + * @throws NS_ERROR_FILE_CORRUPTED + * If the database file is corrupted. + */ + mozIStorageConnection openDatabase( + in nsIFile aDatabaseFile, [optional] in unsigned long aConnectionFlags); + + /** + * Open a connection to the specified file that doesn't share a sqlite cache. + * + * Without a shared-cache, each connection uses its own pages cache, which + * may be memory inefficient with a large number of connections, in such a + * case so you should use openDatabase instead. On the other side, if cache + * contention may be an issue, for instance when concurrency is important to + * ensure responsiveness, using unshared connections may be a + * performance win. + * + * ========== + * DANGER + * ========== + * + * If you have more than one connection to a file, you MUST use the EXACT + * SAME NAME for the file each time, including case. The sqlite code uses + * a simple string compare to see if there is already a connection. Opening + * a connection to "Foo.sqlite" and "foo.sqlite" will CORRUPT YOUR DATABASE. + * + * The connection object returned by this function is not threadsafe. You + * must use it only from the thread you created it from. + * + * @param aDatabaseFile + * A nsIFile that represents the database that is to be opened. + * @param [optional] aConnectionFlags + * A set of flags to enable optional features for the returned + * synchronous connection object. + * Currently supports CONNECTION_INTERRUPTIBLE flag. + * For full details, please refer to the documentation of the flag. + * + * @returns a mozIStorageConnection for the requested database file. + * + * @throws NS_ERROR_OUT_OF_MEMORY + * If allocating a new storage object fails. + * @throws NS_ERROR_FILE_CORRUPTED + * If the database file is corrupted. + */ + mozIStorageConnection openUnsharedDatabase( + in nsIFile aDatabaseFile, [optional] in unsigned long aConnectionFlags); + + /** + * See openDatabase(). Exactly the same only initialized with a file URL. + * Custom parameters can be passed to SQLite and VFS implementations through + * the query part of the URL. + * + * @param aURL + * A nsIFileURL that represents the database that is to be opened. + * @param [optional] aTelemetryFilename + * The name to use for the database in telemetry. Only needed if the + * actual filename can contain sensitive information. + * @param [optional] aConnectionFlags + * A set of flags to enable optional features for the returned + * synchronous connection object. + * Currently supports CONNECTION_INTERRUPTIBLE flag. + * For full details, please refer to the documentation of the flag. + */ + mozIStorageConnection openDatabaseWithFileURL( + in nsIFileURL aFileURL, [optional] in ACString aTelemetryFilename, + [optional] in unsigned long aConnectionFlags); +}; + +%{C++ + +constexpr auto kMozStorageMemoryStorageKey = "memory"_ns; + +%} diff --git a/storage/mozIStorageStatement.idl b/storage/mozIStorageStatement.idl new file mode 100644 index 0000000000..ad2821bb6c --- /dev/null +++ b/storage/mozIStorageStatement.idl @@ -0,0 +1,326 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 "mozIStorageBaseStatement.idl" +%{C++ +#include "mozilla/DebugOnly.h" +%} + +[ptr] native octetPtr(uint8_t); + +/** + * A SQL statement that can be used for both synchronous and asynchronous + * purposes. + */ +[scriptable, builtinclass, uuid(5f567c35-6c32-4140-828c-683ea49cfd3a)] +interface mozIStorageStatement : mozIStorageBaseStatement { + /** + * Create a clone of this statement, by initializing a new statement + * with the same connection and same SQL statement as this one. It + * does not preserve statement state; that is, if a statement is + * being executed when it is cloned, the new statement will not be + * executing. + */ + mozIStorageStatement clone(); + + /* + * Number of parameters + */ + readonly attribute unsigned long parameterCount; + + /** + * Name of nth parameter, if given + */ + AUTF8String getParameterName(in unsigned long aParamIndex); + + /** + * Returns the index of the named parameter. + * + * @param aName + * The name of the parameter you want the index for. This does not + * include the leading ':'. + * @return the index of the named parameter. + */ + unsigned long getParameterIndex(in AUTF8String aName); + + /** + * Number of columns returned + */ + readonly attribute unsigned long columnCount; + + /** + * Name of nth column + */ + AUTF8String getColumnName(in unsigned long aColumnIndex); + + /** + * Obtains the index of the column with the specified name. + * + * @param aName + * The name of the column. + * @return The index of the column with the specified name. + */ + unsigned long getColumnIndex(in AUTF8String aName); + + /** + * Reset parameters/statement execution + */ + void reset(); + + /** + * Execute the query, ignoring any results. This is accomplished by + * calling executeStep() once, and then calling reset(). + * + * Error and last insert info, etc. are available from + * the mozStorageConnection. + */ + void execute(); + + /** + * Execute a query, using any currently-bound parameters. Reset + * must be called on the statement after the last call of + * executeStep. + * + * @return a boolean indicating whether there are more rows or not; + * row data may be accessed using mozIStorageValueArray methods on + * the statement. + */ + boolean executeStep(); + + /** + * Execute a query, using any currently-bound parameters. Reset is called + * when no more data is returned. This method is only available to JavaScript + * consumers. + * + * @deprecated As of Mozilla 1.9.2 in favor of executeStep(). + * + * @return a boolean indicating whether there are more rows or not. + * + * [deprecated] boolean step(); + */ + + /** + * Obtains the current list of named parameters, which are settable. This + * property is only available to JavaScript consumers. + * + * readonly attribute mozIStorageStatementParams params; + */ + + /** + * Obtains the current row, with access to all the data members by name. This + * property is only available to JavaScript consumers. + * + * readonly attribute mozIStorageStatementRow row; + */ + + ////////////////////////////////////////////////////////////////////////////// + //// Copied contents of mozIStorageValueArray + + /** + * These type values are returned by getTypeOfIndex + * to indicate what type of value is present at + * a given column. + */ + const long VALUE_TYPE_NULL = 0; + const long VALUE_TYPE_INTEGER = 1; + const long VALUE_TYPE_FLOAT = 2; + const long VALUE_TYPE_TEXT = 3; + const long VALUE_TYPE_BLOB = 4; + + /** + * The number of entries in the array (each corresponding to a column in the + * database row) + */ + readonly attribute unsigned long numEntries; + + /** + * Indicate the data type of the current result row for the the given column. + * SQLite will perform type conversion if you ask for a value as a different + * type than it is stored as. + * + * @param aIndex + * 0-based column index. + * @return The type of the value at the given column index; one of + * VALUE_TYPE_NULL, VALUE_TYPE_INTEGER, VALUE_TYPE_FLOAT, + * VALUE_TYPE_TEXT, VALUE_TYPE_BLOB. + */ + long getTypeOfIndex(in unsigned long aIndex); + + /** + * Retrieve the contents of a column from the current result row as a + * variant. + * + * @param aIndex + * 0-based colummn index. + * @return A variant with the type of the column value. + */ + nsIVariant getVariant(in unsigned long aIndex); + + /** + * Retrieve the contents of a column from the current result row as an + * integer. + * + * @param aIndex + * 0-based colummn index. + * @return Column value interpreted as an integer per type conversion rules. + * @{ + */ + long getInt32(in unsigned long aIndex); + long long getInt64(in unsigned long aIndex); + /** @} */ + /** + * Retrieve the contents of a column from the current result row as a + * floating point double. + * + * @param aIndex + * 0-based colummn index. + * @return Column value interpreted as a double per type conversion rules. + */ + double getDouble(in unsigned long aIndex); + /** + * Retrieve the contents of a column from the current result row as a + * string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result column interpreted as a string. If the + * stored value was NULL, you will get an empty string with IsVoid set + * to distinguish it from an explicitly set empty string. + * @{ + */ + AUTF8String getUTF8String(in unsigned long aIndex); + AString getString(in unsigned long aIndex); + /** @} */ + + /** + * Retrieve the contents of a column from the current result row as a + * blob. + * + * @param aIndex + * 0-based colummn index. + * @param[out] aDataSize + * The number of bytes in the blob. + * @param[out] aData + * The contents of the BLOB. This will be NULL if aDataSize == 0. + */ + void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a String. + * No encoding conversion is performed. + */ + AString getBlobAsString(in unsigned long aIndex); + + /** + * Retrieve the contents of a Blob column from the current result row as a + * UTF8 string. + * + * @param aIndex + * 0-based colummn index. + * @return The value for the result Blob column interpreted as a UTF8 String. + * No encoding conversion is performed. + */ + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); + + /** + * Check whether the given column in the current result row is NULL. + * + * @param aIndex + * 0-based colummn index. + * @return true if the value for the result column is null. + */ + boolean getIsNull(in unsigned long aIndex); + + /** + * Returns a shared string pointer. + * + * @param aIndex + * 0-based colummn index. + * @param aByteLength + * The number of bytes in the string or blob. This is the same as the + * number of characters for UTF-8 strings, and twice the number of + * characters for UTF-16 strings. + * @param aResult + * A pointer to the string or blob. + */ + [noscript] void getSharedUTF8String(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out string aResult); + [noscript] void getSharedString(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out wstring aResult); + [noscript] void getSharedBlob(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out octetPtr aResult); + + /** + * Getters for native code that return their values as + * the return type, for convenience and sanity. + * + * Not virtual; no vtable bloat. + */ + +%{C++ + inline int32_t AsInt32(uint32_t idx) { + int32_t v = 0; + mozilla::DebugOnly rv = GetInt32(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline int64_t AsInt64(uint32_t idx) { + int64_t v = 0; + mozilla::DebugOnly rv = GetInt64(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline double AsDouble(uint32_t idx) { + double v = 0.0; + mozilla::DebugOnly rv = GetDouble(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline const char* AsSharedUTF8String(uint32_t idx, uint32_t *len) { + const char *str = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedUTF8String(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const char16_t* AsSharedWString(uint32_t idx, uint32_t *len) { + const char16_t *str = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedString(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const uint8_t* AsSharedBlob(uint32_t idx, uint32_t *len) { + const uint8_t *blob = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedBlob(idx, len, &blob); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return blob; + } + + inline bool IsNull(uint32_t idx) { + bool b = false; + mozilla::DebugOnly rv = GetIsNull(idx, &b); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "Getting value failed, wrong column index?"); + return b; + } + +%} +}; diff --git a/storage/mozIStorageStatementCallback.idl b/storage/mozIStorageStatementCallback.idl new file mode 100644 index 0000000000..e3b888a714 --- /dev/null +++ b/storage/mozIStorageStatementCallback.idl @@ -0,0 +1,48 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 expandtab + * 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 mozIStorageResultSet; +interface mozIStorageError; + +[scriptable, uuid(29383d00-d8c4-4ddd-9f8b-c2feb0f2fcfa)] +interface mozIStorageStatementCallback : nsISupports { + + /** + * Called when some result is obtained from the database. This function can + * be called more than once with a different storageIResultSet each time for + * any given asynchronous statement. + * + * @param aResultSet + * The result set containing the data from the database. + */ + void handleResult(in mozIStorageResultSet aResultSet); + + /** + * Called when some error occurs while executing the statement. This function + * may be called more than once with a different storageIError each time for + * any given asynchronous statement. + * + * @param aError + * An object containing information about the error. + */ + void handleError(in mozIStorageError aError); + + /** + * Called when the statement has finished executing. This function will only + * be called once for any given asynchronous statement. + * + * @param aReason + * Indicates if the statement is no longer executing because it either + * finished (REASON_FINISHED), was canceled (REASON_CANCELED), or + * a fatal error occurred (REASON_ERROR). + */ + const unsigned short REASON_FINISHED = 0; + const unsigned short REASON_CANCELED = 1; + const unsigned short REASON_ERROR = 2; + void handleCompletion(in unsigned short aReason); +}; diff --git a/storage/mozIStorageVacuumParticipant.idl b/storage/mozIStorageVacuumParticipant.idl new file mode 100644 index 0000000000..1515ac0925 --- /dev/null +++ b/storage/mozIStorageVacuumParticipant.idl @@ -0,0 +1,65 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozIStorageAsyncConnection; + +/** + * This interface contains the information that the Storage service needs to + * vacuum a database. This interface is created as a service through the + * category manager with the category "vacuum-participant". + * Please see https://developer.mozilla.org/en/mozIStorageVacuumParticipant for + * more information. + */ +[scriptable, uuid(8f367508-1d9a-4d3f-be0c-ac11b6dd7dbf)] +interface mozIStorageVacuumParticipant : nsISupports { + /** + * The expected page size in bytes for the database. The vacuum manager will + * try to correct the page size by executing a full vacuum. + * + * @note If the database is using the WAL journal mode, the page size won't + * be changed to the requested value. See bug 634374. + * @note Valid page size values are powers of 2 between 512 and 65536. + * The suggested value is mozIStorageConnection::defaultPageSize. + */ + readonly attribute long expectedDatabasePageSize; + + /** + * Whether the main schema should be using auto_vacuum = INCREMENTAL. + * This will cause auto_vacuum to change to INCREMENTAL if it's not set yet. + * It is not possible to change mode of any attached databases through this, + * to do that you must open a separate connection and use asyncVacuum() on it. + */ + readonly attribute boolean useIncrementalVacuum; + + /** + * Connection to the database file to be vacuumed. + */ + readonly attribute mozIStorageAsyncConnection databaseConnection; + + /** + * Notifies when a vacuum operation begins. Listeners should avoid using the + * database till onEndVacuum is received. + * + * @return true to proceed with the vacuum, false if the participant wants to + * opt-out for now, it will be retried later. Useful when participant + * is running some other heavy operation that can't be interrupted. + * + * @note When a vacuum operation starts or ends it will also dispatch global + * "vacuum-begin" and "vacuum-end" notifications through the observer + * service with the data argument being the database filename. + */ + boolean onBeginVacuum(); + + /** + * Notifies when a vacuum operation ends. + * + * @param aSucceeded + * reports if the vacuum succeeded or failed. + */ + void onEndVacuum(in boolean aSucceeded); +}; diff --git a/storage/mozIStorageValueArray.idl b/storage/mozIStorageValueArray.idl new file mode 100644 index 0000000000..98eeaaf551 --- /dev/null +++ b/storage/mozIStorageValueArray.idl @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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" +%{C++ +#include "mozilla/DebugOnly.h" +%} + +[ptr] native octetPtr(uint8_t); + +/** + * mozIStorageValueArray wraps an array of SQL values, such as a single database + * row. + */ +[scriptable, uuid(6e6306f4-ffa7-40f5-96ca-36159ce8f431)] +interface mozIStorageValueArray : nsISupports { + /** + * These type values are returned by getTypeOfIndex + * to indicate what type of value is present at + * a given column. + */ + const long VALUE_TYPE_NULL = 0; + const long VALUE_TYPE_INTEGER = 1; + const long VALUE_TYPE_FLOAT = 2; + const long VALUE_TYPE_TEXT = 3; + const long VALUE_TYPE_BLOB = 4; + + /** + * numEntries + * + * number of entries in the array (each corresponding to a column + * in the database row) + */ + readonly attribute unsigned long numEntries; + + /** + * Returns the type of the value at the given column index; + * one of VALUE_TYPE_NULL, VALUE_TYPE_INTEGER, VALUE_TYPE_FLOAT, + * VALUE_TYPE_TEXT, VALUE_TYPE_BLOB. + */ + long getTypeOfIndex(in unsigned long aIndex); + + /** + * Obtain a value for the given entry (column) index. + * Due to SQLite's type conversion rules, any of these are valid + * for any column regardless of the column's data type. However, + * if the specific type matters, getTypeOfIndex should be used + * first to identify the column type, and then the appropriate + * get method should be called. + * + * If you ask for a string value for a NULL column, you will get an empty + * string with IsVoid set to distinguish it from an explicitly set empty + * string. + */ + long getInt32(in unsigned long aIndex); + long long getInt64(in unsigned long aIndex); + double getDouble(in unsigned long aIndex); + AUTF8String getUTF8String(in unsigned long aIndex); + AString getString(in unsigned long aIndex); + + // data will be NULL if dataSize = 0 + void getBlob(in unsigned long aIndex, out unsigned long aDataSize, [array,size_is(aDataSize)] out octet aData); + AString getBlobAsString(in unsigned long aIndex); + AUTF8String getBlobAsUTF8String(in unsigned long aIndex); + boolean getIsNull(in unsigned long aIndex); + + /** + * Returns a shared string pointer. + * + * @param aIndex + * 0-based colummn index. + * @param aByteLength + * The number of bytes in the string or blob. This is the same as the + * number of characters for UTF-8 strings, and twice the number of + * characters for UTF-16 strings. + * @param aResult + * A pointer to the string or blob. + */ + [noscript] void getSharedUTF8String(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out string aResult); + [noscript] void getSharedString(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out wstring aResult); + [noscript] void getSharedBlob(in unsigned long aIndex, out unsigned long aByteLength, [shared,retval] out octetPtr aResult); + + /** + * Getters for native code that return their values as + * the return type, for convenience and sanity. + * + * Not virtual; no vtable bloat. + */ + +%{C++ + inline int32_t AsInt32(uint32_t idx) { + int32_t v = 0; + mozilla::DebugOnly rv = GetInt32(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline int64_t AsInt64(uint32_t idx) { + int64_t v = 0; + mozilla::DebugOnly rv = GetInt64(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline double AsDouble(uint32_t idx) { + double v = 0.0; + mozilla::DebugOnly rv = GetDouble(idx, &v); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return v; + } + + inline const char* AsSharedUTF8String(uint32_t idx, uint32_t *len) { + const char *str = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedUTF8String(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const char16_t* AsSharedWString(uint32_t idx, uint32_t *len) { + const char16_t *str = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedString(idx, len, &str); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return str; + } + + inline const uint8_t* AsSharedBlob(uint32_t idx, uint32_t *len) { + const uint8_t *blob = nullptr; + *len = 0; + mozilla::DebugOnly rv = GetSharedBlob(idx, len, &blob); + MOZ_ASSERT(NS_SUCCEEDED(rv) || IsNull(idx), + "Getting value failed, wrong column index?"); + return blob; + } + + inline bool IsNull(uint32_t idx) { + bool b = false; + mozilla::DebugOnly rv = GetIsNull(idx, &b); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "Getting value failed, wrong column index?"); + return b; + } + +%} + +}; diff --git a/storage/mozStorageArgValueArray.cpp b/storage/mozStorageArgValueArray.cpp new file mode 100644 index 0000000000..56e97aaa8b --- /dev/null +++ b/storage/mozStorageArgValueArray.cpp @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "nsError.h" +#include "nsString.h" + +#include "mozStoragePrivateHelpers.h" +#include "mozStorageArgValueArray.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// ArgValueArray + +ArgValueArray::ArgValueArray(int32_t aArgc, sqlite3_value** aArgv) + : mArgc(aArgc), mArgv(aArgv) {} + +NS_IMPL_ISUPPORTS(ArgValueArray, mozIStorageValueArray) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray + +NS_IMETHODIMP +ArgValueArray::GetNumEntries(uint32_t* _size) { + *_size = mArgc; + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetTypeOfIndex(uint32_t aIndex, int32_t* _type) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + int t = ::sqlite3_value_type(mArgv[aIndex]); + switch (t) { + case SQLITE_INTEGER: + *_type = VALUE_TYPE_INTEGER; + break; + case SQLITE_FLOAT: + *_type = VALUE_TYPE_FLOAT; + break; + case SQLITE_TEXT: + *_type = VALUE_TYPE_TEXT; + break; + case SQLITE_BLOB: + *_type = VALUE_TYPE_BLOB; + break; + case SQLITE_NULL: + *_type = VALUE_TYPE_NULL; + break; + default: + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetInt32(uint32_t aIndex, int32_t* _value) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_int(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetInt64(uint32_t aIndex, int64_t* _value) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_int64(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetDouble(uint32_t aIndex, double* _value) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + *_value = ::sqlite3_value_double(mArgv[aIndex]); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetUTF8String(uint32_t aIndex, nsACString& _value) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + if (::sqlite3_value_type(mArgv[aIndex]) == SQLITE_NULL) { + // NULL columns should have IsVoid set to distinguish them from an empty + // string. + _value.SetIsVoid(true); + } else { + _value.Assign( + reinterpret_cast(::sqlite3_value_text(mArgv[aIndex])), + ::sqlite3_value_bytes(mArgv[aIndex])); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetString(uint32_t aIndex, nsAString& _value) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + if (::sqlite3_value_type(mArgv[aIndex]) == SQLITE_NULL) { + // NULL columns should have IsVoid set to distinguish them from an empty + // string. + _value.SetIsVoid(true); + } else { + const char16_t* string = + static_cast(::sqlite3_value_text16(mArgv[aIndex])); + _value.Assign(string, + ::sqlite3_value_bytes16(mArgv[aIndex]) / sizeof(char16_t)); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetBlob(uint32_t aIndex, uint32_t* _size, uint8_t** _blob) { + ENSURE_INDEX_VALUE(aIndex, mArgc); + + int size = ::sqlite3_value_bytes(mArgv[aIndex]); + void* blob = moz_xmemdup(::sqlite3_value_blob(mArgv[aIndex]), size); + *_blob = static_cast(blob); + *_size = size; + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetBlobAsString(uint32_t aIndex, nsAString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +ArgValueArray::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +ArgValueArray::GetIsNull(uint32_t aIndex, bool* _isNull) { + // GetTypeOfIndex will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + + *_isNull = (type == VALUE_TYPE_NULL); + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedUTF8String(uint32_t aIndex, uint32_t* _byteLength, + const char** _string) { + *_string = reinterpret_cast(::sqlite3_value_text(mArgv[aIndex])); + if (_byteLength) { + *_byteLength = ::sqlite3_value_bytes(mArgv[aIndex]); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedString(uint32_t aIndex, uint32_t* _byteLength, + const char16_t** _string) { + *_string = + static_cast(::sqlite3_value_text16(mArgv[aIndex])); + if (_byteLength) { + *_byteLength = ::sqlite3_value_bytes16(mArgv[aIndex]); + } + return NS_OK; +} + +NS_IMETHODIMP +ArgValueArray::GetSharedBlob(uint32_t aIndex, uint32_t* _byteLength, + const uint8_t** _blob) { + *_blob = static_cast(::sqlite3_value_blob(mArgv[aIndex])); + if (_byteLength) { + *_byteLength = ::sqlite3_value_bytes(mArgv[aIndex]); + } + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageArgValueArray.h b/storage/mozStorageArgValueArray.h new file mode 100644 index 0000000000..6b4c4c20e7 --- /dev/null +++ b/storage/mozStorageArgValueArray.h @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageArgValueArray_h +#define mozStorageArgValueArray_h + +#include "mozIStorageValueArray.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +class ArgValueArray final : public mozIStorageValueArray { + public: + ArgValueArray(int32_t aArgc, sqlite3_value** aArgv); + + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGEVALUEARRAY + + private: + ~ArgValueArray() {} + + uint32_t mArgc; + sqlite3_value** mArgv; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageArgValueArray_h diff --git a/storage/mozStorageAsyncStatement.cpp b/storage/mozStorageAsyncStatement.cpp new file mode 100644 index 0000000000..b9332f043b --- /dev/null +++ b/storage/mozStorageAsyncStatement.cpp @@ -0,0 +1,338 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 +#include + +#include "nsError.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsIClassInfoImpl.h" +#include "Variant.h" + +#include "mozStorageBindingParams.h" +#include "mozStorageConnection.h" +#include "mozStorageAsyncStatementJSHelper.h" +#include "mozStorageAsyncStatementParams.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" + +#include "mozilla/Logging.h" + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// nsIClassInfo + +NS_IMPL_CI_INTERFACE_GETTER(AsyncStatement, mozIStorageAsyncStatement, + mozIStorageBaseStatement, mozIStorageBindingParams, + mozilla::storage::StorageBaseStatementInternal) + +class AsyncStatementClassInfo : public nsIClassInfo { + public: + constexpr AsyncStatementClassInfo() = default; + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + GetInterfaces(nsTArray& _array) override { + return NS_CI_INTERFACE_GETTER_NAME(AsyncStatement)(_array); + } + + NS_IMETHOD + GetScriptableHelper(nsIXPCScriptable** _helper) override { + static AsyncStatementJSHelper sJSHelper; + *_helper = &sJSHelper; + return NS_OK; + } + + NS_IMETHOD + GetContractID(nsACString& aContractID) override { + aContractID.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD + GetClassDescription(nsACString& aDesc) override { + aDesc.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD + GetClassID(nsCID** _id) override { + *_id = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetFlags(uint32_t* _flags) override { + *_flags = 0; + return NS_OK; + } + + NS_IMETHOD + GetClassIDNoAlloc(nsCID* _cid) override { return NS_ERROR_NOT_AVAILABLE; } +}; + +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementClassInfo::AddRef() { + return 2; +} +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementClassInfo::Release() { + return 1; +} +NS_IMPL_QUERY_INTERFACE(AsyncStatementClassInfo, nsIClassInfo) + +static AsyncStatementClassInfo sAsyncStatementClassInfo; + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatement + +AsyncStatement::AsyncStatement() : mFinalized(false) {} + +nsresult AsyncStatement::initialize(Connection* aDBConnection, + sqlite3* aNativeConnection, + const nsACString& aSQLStatement) { + MOZ_ASSERT(aDBConnection, "No database connection given!"); + MOZ_ASSERT(aDBConnection->isConnectionReadyOnThisThread(), + "Database connection should be valid"); + MOZ_ASSERT(aNativeConnection, "No native connection given!"); + + mDBConnection = aDBConnection; + mNativeConnection = aNativeConnection; + mSQLString = aSQLStatement; + + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Inited async statement '%s' (0x%p)", mSQLString.get(), this)); + +#ifdef DEBUG + // We want to try and test for LIKE and that consumers are using + // escapeStringForLIKE instead of just trusting user input. The idea to + // check to see if they are binding a parameter after like instead of just + // using a string. We only do this in debug builds because it's expensive! + auto c = nsCaseInsensitiveCStringComparator; + nsACString::const_iterator start, end, e; + aSQLStatement.BeginReading(start); + aSQLStatement.EndReading(end); + e = end; + while (::FindInReadable(" LIKE"_ns, start, e, c)) { + // We have a LIKE in here, so we perform our tests + // FindInReadable moves the iterator, so we have to get a new one for + // each test we perform. + nsACString::const_iterator s1, s2, s3; + s1 = s2 = s3 = start; + + if (!(::FindInReadable(" LIKE ?"_ns, s1, end, c) || + ::FindInReadable(" LIKE :"_ns, s2, end, c) || + ::FindInReadable(" LIKE @"_ns, s3, end, c))) { + // At this point, we didn't find a LIKE statement followed by ?, :, + // or @, all of which are valid characters for binding a parameter. + // We will warn the consumer that they may not be safely using LIKE. + NS_WARNING( + "Unsafe use of LIKE detected! Please ensure that you " + "are using mozIStorageAsyncStatement::escapeStringForLIKE " + "and that you are binding that result to the statement " + "to prevent SQL injection attacks."); + } + + // resetting start and e + start = e; + e = end; + } +#endif + + return NS_OK; +} + +mozIStorageBindingParams* AsyncStatement::getParams() { + nsresult rv; + + // If we do not have an array object yet, make it. + if (!mParamsArray) { + nsCOMPtr array; + rv = NewBindingParamsArray(getter_AddRefs(array)); + NS_ENSURE_SUCCESS(rv, nullptr); + + mParamsArray = static_cast(array.get()); + } + + // If there isn't already any rows added, we'll have to add one to use. + if (mParamsArray->length() == 0) { + RefPtr params(new AsyncBindingParams(mParamsArray)); + NS_ENSURE_TRUE(params, nullptr); + + rv = mParamsArray->AddParams(params); + NS_ENSURE_SUCCESS(rv, nullptr); + + // We have to unlock our params because AddParams locks them. This is safe + // because no reference to the params object was, or ever will be given out. + params->unlock(nullptr); + + // We also want to lock our array at this point - we don't want anything to + // be added to it. + mParamsArray->lock(); + } + + return *mParamsArray->begin(); +} + +/** + * If we are here then we know there are no pending async executions relying on + * us (StatementData holds a reference to us; this also goes for our own + * AsyncStatementFinalizer which proxies its release to the calling event + * target) and so it is always safe to destroy our sqlite3_stmt if one exists. + * We can be destroyed on the caller event target by + * garbage-collection/reference counting or on the async event target by the + * last execution of a statement that already lost its main-thread refs. + */ +AsyncStatement::~AsyncStatement() { + destructorAsyncFinalize(); + + // If we are getting destroyed on the wrong event target, proxy the connection + // release to the right one. + if (!IsOnCurrentSerialEventTarget(mDBConnection->eventTargetOpenedOn)) { + // NS_ProxyRelase only magic forgets for us if mDBConnection is an + // nsCOMPtr. Which it is not; it's a RefPtr. + nsCOMPtr target(mDBConnection->eventTargetOpenedOn); + NS_ProxyRelease("AsyncStatement::mDBConnection", target, + mDBConnection.forget()); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ADDREF(AsyncStatement) +NS_IMPL_RELEASE(AsyncStatement) + +NS_INTERFACE_MAP_BEGIN(AsyncStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageAsyncStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBaseStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBindingParams) + NS_INTERFACE_MAP_ENTRY(mozilla::storage::StorageBaseStatementInternal) + if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { + foundInterface = static_cast(&sAsyncStatementClassInfo); + } else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageAsyncStatement) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +Connection* AsyncStatement::getOwner() { return mDBConnection; } + +int AsyncStatement::getAsyncStatement(sqlite3_stmt** _stmt) { +#ifdef DEBUG + // Make sure we are never called on the connection's owning event target. + NS_ASSERTION( + !IsOnCurrentSerialEventTarget(mDBConnection->eventTargetOpenedOn), + "We should only be called on the async event target!"); +#endif + + if (!mAsyncStatement) { + int rc = mDBConnection->prepareStatement(mNativeConnection, mSQLString, + &mAsyncStatement); + if (rc != SQLITE_OK) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Sqlite statement prepare error: %d '%s'", rc, + ::sqlite3_errmsg(mNativeConnection))); + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Statement was: '%s'", mSQLString.get())); + *_stmt = nullptr; + return rc; + } + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Initialized statement '%s' (0x%p)", mSQLString.get(), + mAsyncStatement)); + } + + *_stmt = mAsyncStatement; + return SQLITE_OK; +} + +nsresult AsyncStatement::getAsynchronousStatementData(StatementData& _data) { + if (mFinalized) return NS_ERROR_UNEXPECTED; + + // Pass null for the sqlite3_stmt; it will be requested on demand from the + // async event target. + _data = StatementData(nullptr, bindingParamsArray(), this); + + return NS_OK; +} + +already_AddRefed AsyncStatement::newBindingParams( + mozIStorageBindingParamsArray* aOwner) { + if (mFinalized) return nullptr; + + nsCOMPtr params(new AsyncBindingParams(aOwner)); + return params.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageAsyncStatement + +// (nothing is specific to mozIStorageAsyncStatement) + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +// proxy to StorageBaseStatementInternal using its define helper. +MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL( + AsyncStatement, if (mFinalized) return NS_ERROR_UNEXPECTED;) + +NS_IMETHODIMP +AsyncStatement::Finalize() { + if (mFinalized) return NS_OK; + + mFinalized = true; + + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Finalizing statement '%s'", mSQLString.get())); + + asyncFinalize(); + + // Release the params holder, so it can release the reference to us. + mStatementParamsHolder = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatement::BindParameters(mozIStorageBindingParamsArray* aParameters) { + if (mFinalized) return NS_ERROR_UNEXPECTED; + + BindingParamsArray* array = static_cast(aParameters); + if (array->getOwner() != this) return NS_ERROR_UNEXPECTED; + + if (array->length() == 0) return NS_ERROR_UNEXPECTED; + + mParamsArray = array; + mParamsArray->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatement::GetState(int32_t* _state) { + if (mFinalized) + *_state = MOZ_STORAGE_STATEMENT_INVALID; + else + *_state = MOZ_STORAGE_STATEMENT_READY; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +BOILERPLATE_BIND_PROXIES(AsyncStatement, + if (mFinalized) return NS_ERROR_UNEXPECTED;) + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatement.h b/storage/mozStorageAsyncStatement.h new file mode 100644 index 0000000000..ceaead1771 --- /dev/null +++ b/storage/mozStorageAsyncStatement.h @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_mozStorageAsyncStatement_h_ +#define mozilla_storage_mozStorageAsyncStatement_h_ + +#include "nsString.h" + +#include "nsTArray.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozIStorageAsyncStatement.h" +#include "StorageBaseStatementInternal.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class AsyncStatementJSHelper; +class AsyncStatementParamsHolder; +class Connection; + +class AsyncStatement final : public mozIStorageAsyncStatement, + public StorageBaseStatementInternal { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEASYNCSTATEMENT + NS_DECL_MOZISTORAGEBASESTATEMENT + NS_DECL_MOZISTORAGEBINDINGPARAMS + NS_DECL_STORAGEBASESTATEMENTINTERNAL + + AsyncStatement(); + + /** + * Initializes the object on aDBConnection by preparing the SQL statement + * given by aSQLStatement. + * + * @param aDBConnection + * The Connection object this statement is associated with. + * @param aNativeConnection + * The native Sqlite connection this statement is associated with. + * @param aSQLStatement + * The SQL statement to prepare that this object will represent. + */ + nsresult initialize(Connection* aDBConnection, sqlite3* aNativeConnection, + const nsACString& aSQLStatement); + + /** + * Obtains and transfers ownership of the array of parameters that are bound + * to this statment. This can be null. + */ + inline already_AddRefed bindingParamsArray() { + return mParamsArray.forget(); + } + + private: + ~AsyncStatement(); + + /** + * @return a pointer to the BindingParams object to use with our Bind* + * method. + */ + mozIStorageBindingParams* getParams(); + + /** + * The SQL string as passed by the user. We store it because we create the + * async statement on-demand on the async thread. + */ + nsCString mSQLString; + + /** + * Holds the array of parameters to bind to this statement when we execute + * it asynchronously. + */ + RefPtr mParamsArray; + + /** + * Caches the JS 'params' helper for this statement. + */ + nsMainThreadPtrHandle mStatementParamsHolder; + + /** + * Have we been explicitly finalized by the user? + */ + bool mFinalized; + + /** + * Required for access to private mStatementParamsHolder field by + * AsyncStatementJSHelper::getParams. + */ + friend class AsyncStatementJSHelper; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatement_h_ diff --git a/storage/mozStorageAsyncStatementExecution.cpp b/storage/mozStorageAsyncStatementExecution.cpp new file mode 100644 index 0000000000..5405555de5 --- /dev/null +++ b/storage/mozStorageAsyncStatementExecution.cpp @@ -0,0 +1,580 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "sqlite3.h" + +#include "mozIStorageStatementCallback.h" +#include "mozStorageBindingParams.h" +#include "mozStorageHelper.h" +#include "mozStorageResultSet.h" +#include "mozStorageRow.h" +#include "mozStorageConnection.h" +#include "mozStorageError.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementData.h" +#include "mozStorageAsyncStatementExecution.h" + +#include "mozilla/DebugOnly.h" +#include "mozilla/Telemetry.h" + +#ifndef MOZ_STORAGE_SORTWARNING_SQL_DUMP +# include "mozilla/Logging.h" +extern mozilla::LazyLogModule gStorageLog; +#endif + +namespace mozilla { +namespace storage { + +/** + * The following constants help batch rows into result sets. + * MAX_MILLISECONDS_BETWEEN_RESULTS was chosen because any user-based task that + * takes less than 200 milliseconds is considered to feel instantaneous to end + * users. MAX_ROWS_PER_RESULT was arbitrarily chosen to reduce the number of + * dispatches to calling thread, while also providing reasonably-sized sets of + * data for consumers. Both of these constants are used because we assume that + * consumers are trying to avoid blocking their execution thread for long + * periods of time, and dispatching many small events to the calling thread will + * end up blocking it. + */ +#define MAX_MILLISECONDS_BETWEEN_RESULTS 75 +#define MAX_ROWS_PER_RESULT 15 + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncExecuteStatements + +/* static */ +nsresult AsyncExecuteStatements::execute( + StatementDataArray&& aStatements, Connection* aConnection, + sqlite3* aNativeConnection, mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _stmt) { + // Create our event to run in the background + RefPtr event = new AsyncExecuteStatements( + std::move(aStatements), aConnection, aNativeConnection, aCallback); + NS_ENSURE_TRUE(event, NS_ERROR_OUT_OF_MEMORY); + + // Dispatch it to the background + nsIEventTarget* target = aConnection->getAsyncExecutionTarget(); + + // If we don't have a valid target, this is a bug somewhere else. In the past, + // this assert found cases where a Run method would schedule a new statement + // without checking if asyncClose had been called. The caller must prevent + // that from happening or, if the work is not critical, just avoid creating + // the new statement during shutdown. See bug 718449 for an example. + MOZ_ASSERT(target); + if (!target) { + return NS_ERROR_NOT_AVAILABLE; + } + + nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + // Return it as the pending statement object and track it. + event.forget(_stmt); + return NS_OK; +} + +AsyncExecuteStatements::AsyncExecuteStatements( + StatementDataArray&& aStatements, Connection* aConnection, + sqlite3* aNativeConnection, mozIStorageStatementCallback* aCallback) + : Runnable("AsyncExecuteStatements"), + mStatements(std::move(aStatements)), + mConnection(aConnection), + mNativeConnection(aNativeConnection), + mHasTransaction(false), + mCallback(aCallback), + mCallingThread(::do_GetCurrentThread()), + mMaxWait( + TimeDuration::FromMilliseconds(MAX_MILLISECONDS_BETWEEN_RESULTS)), + mIntervalStart(TimeStamp::Now()), + mState(PENDING), + mCancelRequested(false), + mMutex(aConnection->sharedAsyncExecutionMutex), + mDBMutex(aConnection->sharedDBMutex) { + NS_ASSERTION(mStatements.Length(), "We weren't given any statements!"); +} + +AsyncExecuteStatements::~AsyncExecuteStatements() { + MOZ_ASSERT(!mCallback, "Never called the Completion callback!"); + MOZ_ASSERT(!mHasTransaction, "There should be no transaction at this point"); + if (mCallback) { + NS_ProxyRelease("AsyncExecuteStatements::mCallback", mCallingThread, + mCallback.forget()); + } +} + +bool AsyncExecuteStatements::shouldNotify() { +#ifdef DEBUG + mMutex.AssertNotCurrentThreadOwns(); + + bool onCallingThread = false; + (void)mCallingThread->IsOnCurrentThread(&onCallingThread); + NS_ASSERTION(onCallingThread, "runEvent not running on the calling thread!"); +#endif + + // We do not need to acquire mMutex here because it can only ever be written + // to on the calling thread, and the only thread that can call us is the + // calling thread, so we know that our access is serialized. + return !mCancelRequested; +} + +bool AsyncExecuteStatements::bindExecuteAndProcessStatement( + StatementData& aData, bool aLastStatement) { + mMutex.AssertNotCurrentThreadOwns(); + + sqlite3_stmt* aStatement = nullptr; + // This cannot fail; we are only called if it's available. + Unused << aData.getSqliteStatement(&aStatement); + MOZ_DIAGNOSTIC_ASSERT( + aStatement, + "bindExecuteAndProcessStatement called without an initialized statement"); + BindingParamsArray* paramsArray(aData); + + // Iterate through all of our parameters, bind them, and execute. + bool continueProcessing = true; + BindingParamsArray::iterator itr = paramsArray->begin(); + BindingParamsArray::iterator end = paramsArray->end(); + while (itr != end && continueProcessing) { + // Bind the data to our statement. + nsCOMPtr bindingInternal = + do_QueryInterface(*itr); + nsCOMPtr error = bindingInternal->bind(aStatement); + if (error) { + // Set our error state. + mState = ERROR; + + // And notify. + (void)notifyError(error); + return false; + } + + // Advance our iterator, execute, and then process the statement. + itr++; + bool lastStatement = aLastStatement && itr == end; + continueProcessing = executeAndProcessStatement(aData, lastStatement); + + // Always reset our statement. + (void)::sqlite3_reset(aStatement); + } + + return continueProcessing; +} + +bool AsyncExecuteStatements::executeAndProcessStatement(StatementData& aData, + bool aLastStatement) { + mMutex.AssertNotCurrentThreadOwns(); + + sqlite3_stmt* aStatement = nullptr; + // This cannot fail; we are only called if it's available. + Unused << aData.getSqliteStatement(&aStatement); + MOZ_DIAGNOSTIC_ASSERT( + aStatement, + "executeAndProcessStatement called without an initialized statement"); + + // Execute our statement + bool hasResults; + do { + hasResults = executeStatement(aData); + + // If we had an error, bail. + if (mState == ERROR || mState == CANCELED) return false; + + // If we have been canceled, there is no point in going on... + { + MutexAutoLock lockedScope(mMutex); + if (mCancelRequested) { + mState = CANCELED; + return false; + } + } + + // Build our result set and notify if we got anything back and have a + // callback to notify. + if (mCallback && hasResults && + NS_FAILED(buildAndNotifyResults(aStatement))) { + // We had an error notifying, so we notify on error and stop processing. + mState = ERROR; + + // Notify, and stop processing statements. + (void)notifyError(mozIStorageError::ERROR, + "An error occurred while notifying about results"); + + return false; + } + } while (hasResults); + +#ifndef MOZ_STORAGE_SORTWARNING_SQL_DUMP + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Warning)) +#endif + { + // Check to make sure that this statement was smart about what it did. + checkAndLogStatementPerformance(aStatement); + } + + // If we are done, we need to set our state accordingly while we still hold + // our mutex. We would have already returned if we were canceled or had + // an error at this point. + if (aLastStatement) mState = COMPLETED; + + return true; +} + +bool AsyncExecuteStatements::executeStatement(StatementData& aData) { + mMutex.AssertNotCurrentThreadOwns(); + + sqlite3_stmt* aStatement = nullptr; + // This cannot fail; we are only called if it's available. + Unused << aData.getSqliteStatement(&aStatement); + MOZ_DIAGNOSTIC_ASSERT( + aStatement, "executeStatement called without an initialized statement"); + + bool busyRetry = false; + while (true) { + if (busyRetry) { + busyRetry = false; + + // Yield, and try again + Unused << PR_Sleep(PR_INTERVAL_NO_WAIT); + + // Check for cancellation before retrying + { + MutexAutoLock lockedScope(mMutex); + if (mCancelRequested) { + mState = CANCELED; + return false; + } + } + } + + // lock the sqlite mutex so sqlite3_errmsg cannot change + SQLiteMutexAutoLock lockedScope(mDBMutex); + + int rc = mConnection->stepStatement(mNativeConnection, aStatement); + + // Some errors are not fatal, and we can handle them and continue. + if (rc == SQLITE_BUSY) { + ::sqlite3_reset(aStatement); + busyRetry = true; + continue; + } + + aData.MaybeRecordQueryStatus(rc); + + // Stop if we have no more results. + if (rc == SQLITE_DONE) { + return false; + } + + // If we got results, we can return now. + if (rc == SQLITE_ROW) { + return true; + } + + if (rc == SQLITE_INTERRUPT) { + mState = CANCELED; + return false; + } + + // Set an error state. + mState = ERROR; + + // Construct the error message before giving up the mutex (which we cannot + // hold during the call to notifyError). + nsCOMPtr errorObj( + new Error(rc, ::sqlite3_errmsg(mNativeConnection))); + // We cannot hold the DB mutex while calling notifyError. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + (void)notifyError(errorObj); + + // Finally, indicate that we should stop processing. + return false; + } +} + +nsresult AsyncExecuteStatements::buildAndNotifyResults( + sqlite3_stmt* aStatement) { + NS_ASSERTION(mCallback, "Trying to dispatch results without a callback!"); + mMutex.AssertNotCurrentThreadOwns(); + + // Build result object if we need it. + if (!mResultSet) mResultSet = new ResultSet(); + NS_ENSURE_TRUE(mResultSet, NS_ERROR_OUT_OF_MEMORY); + + RefPtr row(new Row()); + NS_ENSURE_TRUE(row, NS_ERROR_OUT_OF_MEMORY); + + nsresult rv = row->initialize(aStatement); + NS_ENSURE_SUCCESS(rv, rv); + + rv = mResultSet->add(row); + NS_ENSURE_SUCCESS(rv, rv); + + // If we have hit our maximum number of allowed results, or if we have hit + // the maximum amount of time we want to wait for results, notify the + // calling thread about it. + TimeStamp now = TimeStamp::Now(); + TimeDuration delta = now - mIntervalStart; + if (mResultSet->rows() >= MAX_ROWS_PER_RESULT || delta > mMaxWait) { + // Notify the caller + rv = notifyResults(); + if (NS_FAILED(rv)) return NS_OK; // we'll try again with the next result + + // Reset our start time + mIntervalStart = now; + } + + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyComplete() { + mMutex.AssertNotCurrentThreadOwns(); + NS_ASSERTION(mState != PENDING, + "Still in a pending state when calling Complete!"); + + // Reset our statements before we try to commit or rollback. If we are + // canceling and have statements that think they have pending work, the + // rollback will fail. + for (uint32_t i = 0; i < mStatements.Length(); i++) mStatements[i].reset(); + + // Release references to the statement data as soon as possible. If this + // is the last reference, statements will be finalized immediately on the + // async thread, hence avoiding several bounces between threads and possible + // race conditions with AsyncClose(). + mStatements.Clear(); + + // Handle our transaction, if we have one + if (mHasTransaction) { + SQLiteMutexAutoLock lockedScope(mDBMutex); + if (mState == COMPLETED) { + nsresult rv = mConnection->commitTransactionInternal(lockedScope, + mNativeConnection); + if (NS_FAILED(rv)) { + mState = ERROR; + // We cannot hold the DB mutex while calling notifyError. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + (void)notifyError(mozIStorageError::ERROR, + "Transaction failed to commit"); + } + } else { + DebugOnly rv = mConnection->rollbackTransactionInternal( + lockedScope, mNativeConnection); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Transaction failed to rollback"); + } + mHasTransaction = false; + } + + // This will take ownership of mCallback and make sure its destruction will + // happen on the owner thread. + Unused << mCallingThread->Dispatch( + NewRunnableMethod("AsyncExecuteStatements::notifyCompleteOnCallingThread", + this, + &AsyncExecuteStatements::notifyCompleteOnCallingThread), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyCompleteOnCallingThread() { + MOZ_ASSERT(mCallingThread->IsOnCurrentThread()); + // Take ownership of mCallback and responsibility for freeing it when we + // release it. Any notifyResultsOnCallingThread and + // notifyErrorOnCallingThread calls on the stack spinning the event loop have + // guaranteed their safety by creating their own strong reference before + // invoking the callback. + nsCOMPtr callback = std::move(mCallback); + if (callback) { + Unused << callback->HandleCompletion(mState); + } + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyError(int32_t aErrorCode, + const char* aMessage) { + mMutex.AssertNotCurrentThreadOwns(); + mDBMutex.assertNotCurrentThreadOwns(); + + if (!mCallback) return NS_OK; + + nsCOMPtr errorObj(new Error(aErrorCode, aMessage)); + NS_ENSURE_TRUE(errorObj, NS_ERROR_OUT_OF_MEMORY); + + return notifyError(errorObj); +} + +nsresult AsyncExecuteStatements::notifyError(mozIStorageError* aError) { + mMutex.AssertNotCurrentThreadOwns(); + mDBMutex.assertNotCurrentThreadOwns(); + + if (!mCallback) return NS_OK; + + Unused << mCallingThread->Dispatch( + NewRunnableMethod>( + "AsyncExecuteStatements::notifyErrorOnCallingThread", this, + &AsyncExecuteStatements::notifyErrorOnCallingThread, aError), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyErrorOnCallingThread( + mozIStorageError* aError) { + MOZ_ASSERT(mCallingThread->IsOnCurrentThread()); + // Acquire our own strong reference so that if the callback spins a nested + // event loop and notifyCompleteOnCallingThread is executed, forgetting + // mCallback, we still have a valid/strong reference that won't be freed until + // we exit. + nsCOMPtr callback = mCallback; + if (shouldNotify() && callback) { + Unused << callback->HandleError(aError); + } + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyResults() { + mMutex.AssertNotCurrentThreadOwns(); + MOZ_ASSERT(mCallback, "notifyResults called without a callback!"); + + // This takes ownership of mResultSet, a new one will be generated in + // buildAndNotifyResults() when further results will arrive. + Unused << mCallingThread->Dispatch( + NewRunnableMethod>( + "AsyncExecuteStatements::notifyResultsOnCallingThread", this, + &AsyncExecuteStatements::notifyResultsOnCallingThread, + mResultSet.forget()), + NS_DISPATCH_NORMAL); + + return NS_OK; +} + +nsresult AsyncExecuteStatements::notifyResultsOnCallingThread( + ResultSet* aResultSet) { + MOZ_ASSERT(mCallingThread->IsOnCurrentThread()); + // Acquire our own strong reference so that if the callback spins a nested + // event loop and notifyCompleteOnCallingThread is executed, forgetting + // mCallback, we still have a valid/strong reference that won't be freed until + // we exit. + nsCOMPtr callback = mCallback; + if (shouldNotify() && callback) { + Unused << callback->HandleResult(aResultSet); + } + return NS_OK; +} + +NS_IMPL_ISUPPORTS_INHERITED(AsyncExecuteStatements, Runnable, + mozIStoragePendingStatement) + +bool AsyncExecuteStatements::statementsNeedTransaction() { + // If there is more than one write statement, run in a transaction. + // Additionally, if we have only one statement but it needs a transaction, due + // to multiple BindingParams, we will wrap it in one. + for (uint32_t i = 0, transactionsCount = 0; i < mStatements.Length(); ++i) { + transactionsCount += mStatements[i].needsTransaction(); + if (transactionsCount > 1) { + return true; + } + } + return false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStoragePendingStatement + +NS_IMETHODIMP +AsyncExecuteStatements::Cancel() { +#ifdef DEBUG + bool onCallingThread = false; + (void)mCallingThread->IsOnCurrentThread(&onCallingThread); + NS_ASSERTION(onCallingThread, "Not canceling from the calling thread!"); +#endif + + // If we have already canceled, we have an error, but always indicate that + // we are trying to cancel. + NS_ENSURE_FALSE(mCancelRequested, NS_ERROR_UNEXPECTED); + + { + MutexAutoLock lockedScope(mMutex); + + // We need to indicate that we want to try and cancel now. + mCancelRequested = true; + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIRunnable + +NS_IMETHODIMP +AsyncExecuteStatements::Run() { + MOZ_ASSERT(mConnection->isConnectionReadyOnThisThread()); + + // Do not run if we have been canceled. + { + MutexAutoLock lockedScope(mMutex); + if (mCancelRequested) mState = CANCELED; + } + if (mState == CANCELED) return notifyComplete(); + + if (statementsNeedTransaction()) { + SQLiteMutexAutoLock lockedScope(mDBMutex); + if (!mConnection->transactionInProgress(lockedScope)) { + if (NS_SUCCEEDED(mConnection->beginTransactionInternal( + lockedScope, mNativeConnection, + mozIStorageConnection::TRANSACTION_IMMEDIATE))) { + mHasTransaction = true; + } +#ifdef DEBUG + else { + NS_WARNING("Unable to create a transaction for async execution."); + } +#endif + } + } + + // Execute each statement, giving the callback results if it returns any. + for (uint32_t i = 0; i < mStatements.Length(); i++) { + bool finished = (i == (mStatements.Length() - 1)); + + sqlite3_stmt* stmt; + { // lock the sqlite mutex so sqlite3_errmsg cannot change + SQLiteMutexAutoLock lockedScope(mDBMutex); + + int rc = mStatements[i].getSqliteStatement(&stmt); + if (rc != SQLITE_OK) { + // Set our error state. + mState = ERROR; + + // Build the error object; can't call notifyError with the lock held + nsCOMPtr errorObj( + new Error(rc, ::sqlite3_errmsg(mNativeConnection))); + { + // We cannot hold the DB mutex and call notifyError. + SQLiteMutexAutoUnlock unlockedScope(mDBMutex); + (void)notifyError(errorObj); + } + break; + } + } + + // If we have parameters to bind, bind them, execute, and process. + if (mStatements[i].hasParametersToBeBound()) { + if (!bindExecuteAndProcessStatement(mStatements[i], finished)) break; + } + // Otherwise, just execute and process the statement. + else if (!executeAndProcessStatement(mStatements[i], finished)) { + break; + } + } + + // If we still have results that we haven't notified about, take care of + // them now. + if (mResultSet) (void)notifyResults(); + + // Notify about completion + return notifyComplete(); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementExecution.h b/storage/mozStorageAsyncStatementExecution.h new file mode 100644 index 0000000000..a7f35dcfbe --- /dev/null +++ b/storage/mozStorageAsyncStatementExecution.h @@ -0,0 +1,243 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageAsyncStatementExecution_h +#define mozStorageAsyncStatementExecution_h + +#include "nscore.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Attributes.h" +#include "nsThreadUtils.h" + +#include "SQLiteMutex.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageStatementCallback.h" +#include "mozStorageHelper.h" + +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class Connection; +class ResultSet; +class StatementData; +} // namespace storage +} // namespace mozilla + +namespace mozilla::storage { +class AsyncExecuteStatements final : public Runnable, + public mozIStoragePendingStatement { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_MOZISTORAGEPENDINGSTATEMENT + + /** + * Describes the state of execution. + */ + enum ExecutionState { + PENDING = -1, + COMPLETED = mozIStorageStatementCallback::REASON_FINISHED, + CANCELED = mozIStorageStatementCallback::REASON_CANCELED, + ERROR = mozIStorageStatementCallback::REASON_ERROR + }; + + typedef nsTArray StatementDataArray; + + /** + * Executes a statement in the background, and passes results back to the + * caller. + * + * @param aStatements + * The statements to execute and possibly bind in the background. + * Ownership is transfered from the caller. + * @param aConnection + * The connection that created the statements to execute. + * @param aNativeConnection + * The native Sqlite connection that created the statements to execute. + * @param aCallback + * The callback that is notified of results, completion, and errors. + * @param _stmt + * The handle to control the execution of the statements. + */ + static nsresult execute(StatementDataArray&& aStatements, + Connection* aConnection, sqlite3* aNativeConnection, + mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _stmt); + + /** + * Indicates when events on the calling thread should run or not. Certain + * events posted back to the calling thread should call this see if they + * should run or not. + * + * @pre mMutex is not held + * + * @returns true if the event should notify still, false otherwise. + */ + bool shouldNotify(); + + /** + * Used by notifyComplete(), notifyError() and notifyResults() to notify on + * the calling thread. + */ + nsresult notifyCompleteOnCallingThread(); + nsresult notifyErrorOnCallingThread(mozIStorageError* aError); + nsresult notifyResultsOnCallingThread(ResultSet* aResultSet); + + private: + AsyncExecuteStatements(StatementDataArray&& aStatements, + Connection* aConnection, sqlite3* aNativeConnection, + mozIStorageStatementCallback* aCallback); + ~AsyncExecuteStatements(); + + /** + * Binds and then executes a given statement until completion, an error + * occurs, or we are canceled. If aLastStatement is true, we should set + * mState accordingly. + * + * @pre mMutex is not held + * + * @param aData + * The StatementData to bind, execute, and then process. + * @param aLastStatement + * Indicates if this is the last statement or not. If it is, we have + * to set the proper state. + * @returns true if we should continue to process statements, false otherwise. + */ + bool bindExecuteAndProcessStatement(StatementData& aData, + bool aLastStatement); + + /** + * Executes a given statement until completion, an error occurs, or we are + * canceled. If aLastStatement is true, we should set mState accordingly. + * + * @pre mMutex is not held + * + * @param aData + * The StatementData to execute, and then process. + * @param aLastStatement + * Indicates if this is the last statement or not. If it is, we have + * to set the proper state. + * @returns true if we should continue to process statements, false otherwise. + */ + bool executeAndProcessStatement(StatementData& aData, bool aLastStatement); + + /** + * Executes a statement to completion, properly handling any error conditions. + * + * @pre mMutex is not held + * + * @param aData + * The StatementData to execute to completion. + * @returns true if results were obtained, false otherwise. + */ + bool executeStatement(StatementData& aData); + + /** + * Builds a result set up with a row from a given statement. If we meet the + * right criteria, go ahead and notify about this results too. + * + * @pre mMutex is not held + * + * @param aStatement + * The statement to get the row data from. + */ + nsresult buildAndNotifyResults(sqlite3_stmt* aStatement); + + /** + * Notifies callback about completion, and does any necessary cleanup. + * + * @pre mMutex is not held + */ + nsresult notifyComplete(); + + /** + * Notifies callback about an error. + * + * @pre mMutex is not held + * @pre mDBMutex is not held + * + * @param aErrorCode + * The error code defined in mozIStorageError for the error. + * @param aMessage + * The error string, if any. + * @param aError + * The error object to notify the caller with. + */ + nsresult notifyError(int32_t aErrorCode, const char* aMessage); + nsresult notifyError(mozIStorageError* aError); + + /** + * Notifies the callback about a result set. + * + * @pre mMutex is not held + */ + nsresult notifyResults(); + + /** + * Tests whether the current statements should be wrapped in an explicit + * transaction. + * + * @return true if an explicit transaction is needed, false otherwise. + */ + bool statementsNeedTransaction(); + + StatementDataArray mStatements; + RefPtr mConnection; + sqlite3* mNativeConnection; + bool mHasTransaction; + // Note, this may not be a threadsafe object - never addref/release off + // the calling thread. We take a reference when this is created, and + // release it in the CompletionNotifier::Run() call back to this thread. + nsCOMPtr mCallback; + nsCOMPtr mCallingThread; + RefPtr mResultSet; + + /** + * The maximum amount of time we want to wait between results. Defined by + * MAX_MILLISECONDS_BETWEEN_RESULTS and set at construction. + */ + const TimeDuration mMaxWait; + + /** + * The start time since our last set of results. + */ + TimeStamp mIntervalStart; + + /** + * Indicates our state of execution. + */ + ExecutionState mState; + + /** + * Indicates if we should try to cancel at a cancelation point. + */ + bool mCancelRequested; + + /** + * This is the mutex that protects our state from changing between threads. + * This includes the following variables: + * - mCancelRequested is only set on the calling thread while the lock is + * held. It is always read from within the lock on the background thread, + * but not on the calling thread (see shouldNotify for why). + */ + Mutex& mMutex; + + /** + * The wrapped SQLite recursive connection mutex. We use it whenever we call + * sqlite3_step and care about having reliable error messages. By taking it + * prior to the call and holding it until the point where we no longer care + * about the error message, the user gets reliable error messages. + */ + SQLiteMutex& mDBMutex; +}; + +} // namespace mozilla::storage + +#endif // mozStorageAsyncStatementExecution_h diff --git a/storage/mozStorageAsyncStatementJSHelper.cpp b/storage/mozStorageAsyncStatementJSHelper.cpp new file mode 100644 index 0000000000..73366f6604 --- /dev/null +++ b/storage/mozStorageAsyncStatementJSHelper.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "nsIXPConnect.h" +#include "mozStorageAsyncStatement.h" +#include "mozStorageService.h" + +#include "nsString.h" +#include "nsServiceManagerUtils.h" + +#include "mozStorageAsyncStatementJSHelper.h" + +#include "mozStorageAsyncStatementParams.h" + +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefinePropertyById + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementJSHelper + +nsresult AsyncStatementJSHelper::getParams(AsyncStatement* aStatement, + JSContext* aCtx, JSObject* aScopeObj, + JS::Value* _params) { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageAsyncStatement::MOZ_STORAGE_STATEMENT_READY, + "Invalid state to get the params object - all calls will fail!"); +#endif + + JS::Rooted scope(aCtx, aScopeObj); + + if (!aStatement->mStatementParamsHolder) { + dom::GlobalObject global(aCtx, scope); + if (global.Failed()) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr window = + do_QueryInterface(global.GetAsSupports()); + + RefPtr params( + new AsyncStatementParams(window, aStatement)); + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); + + RefPtr paramsHolder = + new AsyncStatementParamsHolder(params); + NS_ENSURE_TRUE(paramsHolder, NS_ERROR_OUT_OF_MEMORY); + + aStatement->mStatementParamsHolder = + new nsMainThreadPtrHolder( + "Statement::mStatementParamsHolder", paramsHolder); + } + + RefPtr params( + aStatement->mStatementParamsHolder->Get()); + JSObject* obj = params->WrapObject(aCtx, nullptr); + if (!obj) { + return NS_ERROR_UNEXPECTED; + } + + _params->setObject(*obj); + return NS_OK; +} + +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementJSHelper::AddRef() { + return 2; +} +NS_IMETHODIMP_(MozExternalRefCountType) AsyncStatementJSHelper::Release() { + return 1; +} +NS_INTERFACE_MAP_BEGIN(AsyncStatementJSHelper) + NS_INTERFACE_MAP_ENTRY(nsIXPCScriptable) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME AsyncStatementJSHelper +#define XPC_MAP_QUOTED_CLASSNAME "AsyncStatementJSHelper" +#define XPC_MAP_FLAGS \ + (XPC_SCRIPTABLE_WANT_RESOLVE | XPC_SCRIPTABLE_ALLOW_PROP_MODS_DURING_RESOLVE) +#include "xpc_map_end.h" + +NS_IMETHODIMP +AsyncStatementJSHelper::Resolve(nsIXPConnectWrappedNative* aWrapper, + JSContext* aCtx, JSObject* aScopeObj, jsid aId, + bool* resolvedp, bool* _retval) { + if (!aId.isString()) return NS_OK; + + // Cast to async via mozI* since direct from nsISupports is ambiguous. + JS::Rooted scope(aCtx, aScopeObj); + JS::Rooted id(aCtx, aId); + mozIStorageAsyncStatement* iAsyncStmt = + static_cast(aWrapper->Native()); + AsyncStatement* stmt = static_cast(iAsyncStmt); + +#ifdef DEBUG + { + nsISupports* supp = aWrapper->Native(); + nsCOMPtr isStatement(do_QueryInterface(supp)); + NS_ASSERTION(isStatement, "How is this not an async statement?!"); + } +#endif + + if (::JS_LinearStringEqualsLiteral(id.toLinearString(), "params")) { + JS::Rooted val(aCtx); + nsresult rv = getParams(stmt, aCtx, scope, val.address()); + NS_ENSURE_SUCCESS(rv, rv); + *_retval = ::JS_DefinePropertyById(aCtx, scope, id, val, JSPROP_RESOLVING); + *resolvedp = true; + return NS_OK; + } + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementParamsHolder + +NS_IMPL_ISUPPORTS0(AsyncStatementParamsHolder); + +AsyncStatementParamsHolder::~AsyncStatementParamsHolder() { + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + mParams->mStatement = nullptr; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementJSHelper.h b/storage/mozStorageAsyncStatementJSHelper.h new file mode 100644 index 0000000000..ee787fca0b --- /dev/null +++ b/storage/mozStorageAsyncStatementJSHelper.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_mozStorageAsyncStatementJSHelper_h_ +#define mozilla_storage_mozStorageAsyncStatementJSHelper_h_ + +#include "nsIXPCScriptable.h" + +namespace mozilla { +namespace storage { + +class AsyncStatement; +class AsyncStatementParams; + +/** + * A modified version of StatementJSHelper that only exposes the async-specific + * 'params' helper. We do not expose 'row' or 'step' as they do not apply to + * us. + */ +class AsyncStatementJSHelper : public nsIXPCScriptable { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCSCRIPTABLE + + private: + nsresult getParams(AsyncStatement*, JSContext*, JSObject*, JS::Value*); +}; + +/** + * Wrapper used to clean up the references JS helpers hold to the statement. + * For cycle-avoidance reasons they do not hold reference-counted references, + * so it is important we do this. + */ +class AsyncStatementParamsHolder final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit AsyncStatementParamsHolder(AsyncStatementParams* aParams) + : mParams(aParams) {} + + AsyncStatementParams* Get() const { + MOZ_ASSERT(mParams); + return mParams; + } + + private: + virtual ~AsyncStatementParamsHolder(); + + RefPtr mParams; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatementJSHelper_h_ diff --git a/storage/mozStorageAsyncStatementParams.cpp b/storage/mozStorageAsyncStatementParams.cpp new file mode 100644 index 0000000000..cb8601504e --- /dev/null +++ b/storage/mozStorageAsyncStatementParams.cpp @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageAsyncStatementParams.h" + +#include "nsJSUtils.h" +#include "nsString.h" + +#include "jsapi.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/MozStorageAsyncStatementParamsBinding.h" +#include "mozStorageAsyncStatement.h" +#include "mozStoragePrivateHelpers.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// AsyncStatementParams + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AsyncStatementParams, mWindow) + +NS_INTERFACE_TABLE_HEAD(AsyncStatementParams) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(AsyncStatementParams, nsISupports) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(AsyncStatementParams) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(AsyncStatementParams) +NS_IMPL_CYCLE_COLLECTING_RELEASE(AsyncStatementParams) + +AsyncStatementParams::AsyncStatementParams(nsPIDOMWindowInner* aWindow, + AsyncStatement* aStatement) + : mWindow(aWindow), mStatement(aStatement) { + NS_ASSERTION(mStatement != nullptr, "mStatement is null"); +} + +JSObject* AsyncStatementParams::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::MozStorageAsyncStatementParams_Binding::Wrap(aCx, this, + aGivenProto); +} + +void AsyncStatementParams::NamedGetter(JSContext* aCx, const nsAString& aName, + bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + // Unfortunately there's no API that lets us return the parameter value. + aFound = false; +} + +void AsyncStatementParams::NamedSetter(JSContext* aCx, const nsAString& aName, + JS::Handle aValue, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + NS_ConvertUTF16toUTF8 name(aName); + + nsCOMPtr variant(convertJSValToVariant(aCx, aValue)); + if (!variant) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aRv = mStatement->BindByName(name, variant); +} + +void AsyncStatementParams::GetSupportedNames(nsTArray& aNames) { + // We don't know how many params there are, so we can't implement this for + // AsyncStatementParams. +} + +void AsyncStatementParams::IndexedGetter(JSContext* aCx, uint32_t aIndex, + bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + // Unfortunately there's no API that lets us return the parameter value. + aFound = false; +} + +void AsyncStatementParams::IndexedSetter(JSContext* aCx, uint32_t aIndex, + JS::Handle aValue, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + nsCOMPtr variant(convertJSValToVariant(aCx, aValue)); + if (!variant) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aRv = mStatement->BindByIndex(aIndex, variant); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageAsyncStatementParams.h b/storage/mozStorageAsyncStatementParams.h new file mode 100644 index 0000000000..c72afc2bc3 --- /dev/null +++ b/storage/mozStorageAsyncStatementParams.h @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_mozStorageAsyncStatementParams_h_ +#define mozilla_storage_mozStorageAsyncStatementParams_h_ + +#include "mozilla/Attributes.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace storage { + +class AsyncStatement; + +class AsyncStatementParams final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AsyncStatementParams) + + explicit AsyncStatementParams(nsPIDOMWindowInner* aWindow, + AsyncStatement* aStatement); + + void NamedGetter(JSContext* aCx, const nsAString& aName, bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv); + + void NamedSetter(JSContext* aCx, const nsAString& aName, + JS::Handle aValue, mozilla::ErrorResult& aRv); + + uint32_t Length() const { + // WebIDL requires a .length property when there's an indexed getter. + // Unfortunately we don't know how many params there are in the async case, + // so we have to lie. + return UINT16_MAX; + } + + void IndexedGetter(JSContext* aCx, uint32_t aIndex, bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv); + + void IndexedSetter(JSContext* aCx, uint32_t aIndex, + JS::Handle aValue, mozilla::ErrorResult& aRv); + + void GetSupportedNames(nsTArray& aNames); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + + private: + virtual ~AsyncStatementParams() {} + + nsCOMPtr mWindow; + AsyncStatement* mStatement; + + friend class AsyncStatementParamsHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozilla_storage_mozStorageAsyncStatementParams_h_ diff --git a/storage/mozStorageBindingParams.cpp b/storage/mozStorageBindingParams.cpp new file mode 100644 index 0000000000..1cd50c7a6f --- /dev/null +++ b/storage/mozStorageBindingParams.cpp @@ -0,0 +1,431 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 + +#include "mozilla/UniquePtrExtensions.h" +#include "nsString.h" + +#include "mozStorageError.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageBindingParams.h" +#include "Variant.h" + +namespace mozilla::storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Objects + +namespace { + +struct BindingColumnData { + BindingColumnData(sqlite3_stmt* aStmt, int aColumn) + : stmt(aStmt), column(aColumn) {} + sqlite3_stmt* stmt; + int column; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Variant Specialization Functions (variantToSQLiteT) + +int sqlite3_T_int(BindingColumnData aData, int aValue) { + return ::sqlite3_bind_int(aData.stmt, aData.column + 1, aValue); +} + +int sqlite3_T_int64(BindingColumnData aData, sqlite3_int64 aValue) { + return ::sqlite3_bind_int64(aData.stmt, aData.column + 1, aValue); +} + +int sqlite3_T_double(BindingColumnData aData, double aValue) { + return ::sqlite3_bind_double(aData.stmt, aData.column + 1, aValue); +} + +int sqlite3_T_text(BindingColumnData aData, const nsCString& aValue) { + return ::sqlite3_bind_text(aData.stmt, aData.column + 1, aValue.get(), + aValue.Length(), SQLITE_TRANSIENT); +} + +int sqlite3_T_text16(BindingColumnData aData, const nsString& aValue) { + return ::sqlite3_bind_text16( + aData.stmt, aData.column + 1, aValue.get(), + aValue.Length() * sizeof(char16_t), // Length in bytes! + SQLITE_TRANSIENT); +} + +int sqlite3_T_null(BindingColumnData aData) { + return ::sqlite3_bind_null(aData.stmt, aData.column + 1); +} + +int sqlite3_T_blob(BindingColumnData aData, const void* aBlob, int aSize) { + return ::sqlite3_bind_blob(aData.stmt, aData.column + 1, aBlob, aSize, free); +} + +#include "variantToSQLiteT_impl.h" + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// BindingParams + +BindingParams::BindingParams(mozIStorageBindingParamsArray* aOwningArray, + Statement* aOwningStatement) + : mLocked(false), + mOwningArray(aOwningArray), + mOwningStatement(aOwningStatement), + mParamCount(0) { + (void)mOwningStatement->GetParameterCount(&mParamCount); + mParameters.SetCapacity(mParamCount); +} + +BindingParams::BindingParams(mozIStorageBindingParamsArray* aOwningArray) + : mLocked(false), + mOwningArray(aOwningArray), + mOwningStatement(nullptr), + mParamCount(0) {} + +AsyncBindingParams::AsyncBindingParams( + mozIStorageBindingParamsArray* aOwningArray) + : BindingParams(aOwningArray) {} + +void BindingParams::lock() { + NS_ASSERTION(mLocked == false, "Parameters have already been locked!"); + mLocked = true; + + // We no longer need to hold a reference to our statement or our owning array. + // The array owns us at this point, and it will own a reference to the + // statement. + mOwningStatement = nullptr; + mOwningArray = nullptr; +} + +void BindingParams::unlock(Statement* aOwningStatement) { + NS_ASSERTION(mLocked == true, "Parameters were not yet locked!"); + mLocked = false; + mOwningStatement = aOwningStatement; +} + +const mozIStorageBindingParamsArray* BindingParams::getOwner() const { + return mOwningArray; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ISUPPORTS(BindingParams, mozIStorageBindingParams, + IStorageBindingParamsInternal) + +//////////////////////////////////////////////////////////////////////////////// +//// IStorageBindingParamsInternal + +already_AddRefed BindingParams::bind( + sqlite3_stmt* aStatement) { + // Iterate through all of our stored data, and bind it. + for (size_t i = 0; i < mParameters.Length(); i++) { + int rc = variantToSQLiteT(BindingColumnData(aStatement, i), mParameters[i]); + if (rc != SQLITE_OK) { + // We had an error while trying to bind. Now we need to create an error + // object with the right message. Note that we special case + // SQLITE_MISMATCH, but otherwise get the message from SQLite. + const char* msg = "Could not covert nsIVariant to SQLite type."; + if (rc != SQLITE_MISMATCH) + msg = ::sqlite3_errmsg(::sqlite3_db_handle(aStatement)); + + nsCOMPtr err(new Error(rc, msg)); + return err.forget(); + } + } + + return nullptr; +} + +already_AddRefed AsyncBindingParams::bind( + sqlite3_stmt* aStatement) { + // We should bind by index using the super-class if there is nothing in our + // hashtable. + if (!mNamedParameters.Count()) return BindingParams::bind(aStatement); + + nsCOMPtr err; + + for (const auto& entry : mNamedParameters) { + const nsACString& key = entry.GetKey(); + + // We do not accept any forms of names other than ":name", but we need to + // add the colon for SQLite. + nsAutoCString name(":"); + name.Append(key); + int oneIdx = ::sqlite3_bind_parameter_index(aStatement, name.get()); + + if (oneIdx == 0) { + nsAutoCString errMsg(key); + errMsg.AppendLiteral(" is not a valid named parameter."); + err = new Error(SQLITE_RANGE, errMsg.get()); + break; + } + + // XPCVariant's AddRef and Release are not thread-safe and so we must not + // do anything that would invoke them here on the async thread. As such we + // can't cram aValue into mParameters using ReplaceObjectAt so that + // we can freeload off of the BindingParams::Bind implementation. + int rc = variantToSQLiteT(BindingColumnData(aStatement, oneIdx - 1), + entry.GetWeak()); + if (rc != SQLITE_OK) { + // We had an error while trying to bind. Now we need to create an error + // object with the right message. Note that we special case + // SQLITE_MISMATCH, but otherwise get the message from SQLite. + const char* msg = "Could not covert nsIVariant to SQLite type."; + if (rc != SQLITE_MISMATCH) { + msg = ::sqlite3_errmsg(::sqlite3_db_handle(aStatement)); + } + err = new Error(rc, msg); + break; + } + } + + return err.forget(); +} + +/////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +NS_IMETHODIMP +BindingParams::BindByName(const nsACString& aName, nsIVariant* aValue) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + // Get the column index that we need to store this at. + uint32_t index; + nsresult rv = mOwningStatement->GetParameterIndex(aName, &index); + NS_ENSURE_SUCCESS(rv, rv); + + return BindByIndex(index, aValue); +} + +NS_IMETHODIMP +AsyncBindingParams::BindByName(const nsACString& aName, nsIVariant* aValue) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + RefPtr variant = convertVariantToStorageVariant(aValue); + if (!variant) return NS_ERROR_UNEXPECTED; + + mNamedParameters.InsertOrUpdate(aName, nsCOMPtr{variant}); + return NS_OK; +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringByName(const nsACString& aName, + const nsACString& aValue) { + nsCOMPtr value(new UTF8TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindStringByName(const nsACString& aName, + const nsAString& aValue) { + nsCOMPtr value(new TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindDoubleByName(const nsACString& aName, double aValue) { + nsCOMPtr value(new FloatVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindInt32ByName(const nsACString& aName, int32_t aValue) { + nsCOMPtr value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindInt64ByName(const nsACString& aName, int64_t aValue) { + nsCOMPtr value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindNullByName(const nsACString& aName) { + nsCOMPtr value(new NullVariant()); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobByName(const nsACString& aName, const uint8_t* aValue, + uint32_t aValueSize) { + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair data(static_cast(aValue), + int(aValueSize)); + nsCOMPtr value(new BlobVariant(data)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobArrayByName(const nsACString& aName, + const nsTArray& aValue) { + return BindBlobByName(aName, aValue.Elements(), aValue.Length()); +} + +NS_IMETHODIMP +BindingParams::BindStringAsBlobByName(const nsACString& aName, + const nsAString& aValue) { + return DoBindStringAsBlobByName(this, aName, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByName(const nsACString& aName, + const nsACString& aValue) { + return DoBindStringAsBlobByName(this, aName, aValue); +} + +NS_IMETHODIMP +BindingParams::BindAdoptedBlobByName(const nsACString& aName, uint8_t* aValue, + uint32_t aValueSize) { + UniqueFreePtr uniqueValue(aValue); + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair data(uniqueValue.release(), int(aValueSize)); + nsCOMPtr value(new AdoptedBlobVariant(data)); + + return BindByName(aName, value); +} + +NS_IMETHODIMP +BindingParams::BindByIndex(uint32_t aIndex, nsIVariant* aValue) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + ENSURE_INDEX_VALUE(aIndex, mParamCount); + + // Store the variant for later use. + RefPtr variant = convertVariantToStorageVariant(aValue); + if (!variant) return NS_ERROR_UNEXPECTED; + if (mParameters.Length() <= aIndex) { + (void)mParameters.SetLength(aIndex); + (void)mParameters.AppendElement(variant); + } else { + mParameters.ReplaceElementAt(aIndex, variant); + } + return NS_OK; +} + +NS_IMETHODIMP +AsyncBindingParams::BindByIndex(uint32_t aIndex, nsIVariant* aValue) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + // In the asynchronous case we do not know how many parameters there are to + // bind to, so we cannot check the validity of aIndex. + + RefPtr variant = convertVariantToStorageVariant(aValue); + if (!variant) return NS_ERROR_UNEXPECTED; + if (mParameters.Length() <= aIndex) { + mParameters.SetLength(aIndex); + mParameters.AppendElement(variant); + } else { + mParameters.ReplaceElementAt(aIndex, variant); + } + return NS_OK; +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringByIndex(uint32_t aIndex, + const nsACString& aValue) { + nsCOMPtr value(new UTF8TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindStringByIndex(uint32_t aIndex, const nsAString& aValue) { + nsCOMPtr value(new TextVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindDoubleByIndex(uint32_t aIndex, double aValue) { + nsCOMPtr value(new FloatVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindInt32ByIndex(uint32_t aIndex, int32_t aValue) { + nsCOMPtr value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindInt64ByIndex(uint32_t aIndex, int64_t aValue) { + nsCOMPtr value(new IntegerVariant(aValue)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindNullByIndex(uint32_t aIndex) { + nsCOMPtr value(new NullVariant()); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobByIndex(uint32_t aIndex, const uint8_t* aValue, + uint32_t aValueSize) { + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair data(static_cast(aValue), + int(aValueSize)); + nsCOMPtr value(new BlobVariant(data)); + NS_ENSURE_TRUE(value, NS_ERROR_OUT_OF_MEMORY); + + return BindByIndex(aIndex, value); +} + +NS_IMETHODIMP +BindingParams::BindBlobArrayByIndex(uint32_t aIndex, + const nsTArray& aValue) { + return BindBlobByIndex(aIndex, aValue.Elements(), aValue.Length()); +} + +NS_IMETHODIMP +BindingParams::BindStringAsBlobByIndex(uint32_t aIndex, + const nsAString& aValue) { + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + +NS_IMETHODIMP +BindingParams::BindUTF8StringAsBlobByIndex(uint32_t aIndex, + const nsACString& aValue) { + return DoBindStringAsBlobByIndex(this, aIndex, aValue); +} + +NS_IMETHODIMP +BindingParams::BindAdoptedBlobByIndex(uint32_t aIndex, uint8_t* aValue, + uint32_t aValueSize) { + UniqueFreePtr uniqueValue(aValue); + NS_ENSURE_ARG_MAX(aValueSize, INT_MAX); + std::pair data(uniqueValue.release(), int(aValueSize)); + nsCOMPtr value(new AdoptedBlobVariant(data)); + + return BindByIndex(aIndex, value); +} + +} // namespace mozilla::storage diff --git a/storage/mozStorageBindingParams.h b/storage/mozStorageBindingParams.h new file mode 100644 index 0000000000..cef221ecef --- /dev/null +++ b/storage/mozStorageBindingParams.h @@ -0,0 +1,111 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageBindingParams_h +#define mozStorageBindingParams_h + +#include "nsCOMArray.h" +#include "nsIVariant.h" +#include "nsInterfaceHashtable.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatement.h" +#include "mozStorageAsyncStatement.h" +#include "Variant.h" + +#include "mozIStorageBindingParams.h" +#include "IStorageBindingParamsInternal.h" + +namespace mozilla { +namespace storage { + +class BindingParams : public mozIStorageBindingParams, + public IStorageBindingParamsInternal { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEBINDINGPARAMS + NS_DECL_ISTORAGEBINDINGPARAMSINTERNAL + + /** + * Locks the parameters and prevents further modification to it (such as + * binding more elements to it). + */ + void lock(); + + /** + * Unlocks the parameters and allows modification to it again. + * + * @param aOwningStatement + * The statement that owns us. We cleared this when we were locked, + * and our invariant requires us to have this, so you need to tell us + * again. + */ + void unlock(Statement* aOwningStatement); + + /** + * @returns the pointer to the owning BindingParamsArray. Used by a + * BindingParamsArray to verify that we belong to it when added. + */ + const mozIStorageBindingParamsArray* getOwner() const; + + BindingParams(mozIStorageBindingParamsArray* aOwningArray, + Statement* aOwningStatement); + + protected: + virtual ~BindingParams() {} + + explicit BindingParams(mozIStorageBindingParamsArray* aOwningArray); + // Note that this is managed as a sparse array, so particular caution should + // be used for out-of-bounds usage. + nsTArray > mParameters; + bool mLocked; + + private: + /** + * Track the BindingParamsArray that created us until we are added to it. + * (Once we are added we are locked and no one needs to look up our owner.) + * Ref-counted since there is no invariant that guarantees it stays alive + * otherwise. This keeps mOwningStatement alive for us too since the array + * also holds a reference. + */ + nsCOMPtr mOwningArray; + /** + * Used in the synchronous binding case to map parameter names to indices. + * Not reference-counted because this is only non-null as long as mOwningArray + * is non-null and mOwningArray also holds a statement reference. + */ + Statement* mOwningStatement; + uint32_t mParamCount; +}; + +/** + * Adds late resolution of named parameters so they don't get resolved until we + * try and bind the parameters on the async thread. We also stop checking + * parameter indices for being too big since we just just don't know how many + * there are. + * + * We support *either* binding by name or binding by index. Trying to do both + * results in only binding by name at sqlite3_stmt bind time. + */ +class AsyncBindingParams : public BindingParams { + public: + NS_IMETHOD BindByName(const nsACString& aName, nsIVariant* aValue) override; + NS_IMETHOD BindByIndex(uint32_t aIndex, nsIVariant* aValue) override; + + virtual already_AddRefed bind( + sqlite3_stmt* aStatement) override; + + explicit AsyncBindingParams(mozIStorageBindingParamsArray* aOwningArray); + virtual ~AsyncBindingParams() {} + + private: + nsInterfaceHashtable mNamedParameters; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageBindingParams_h diff --git a/storage/mozStorageBindingParamsArray.cpp b/storage/mozStorageBindingParamsArray.cpp new file mode 100644 index 0000000000..1913d44733 --- /dev/null +++ b/storage/mozStorageBindingParamsArray.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageBindingParamsArray.h" +#include "mozStorageBindingParams.h" +#include "StorageBaseStatementInternal.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// BindingParamsArray + +BindingParamsArray::BindingParamsArray( + StorageBaseStatementInternal* aOwningStatement) + : mOwningStatement(aOwningStatement), mLocked(false) {} + +void BindingParamsArray::lock() { + NS_ASSERTION(mLocked == false, "Array has already been locked!"); + mLocked = true; + + // We also no longer need to hold a reference to our statement since it owns + // us. + mOwningStatement = nullptr; +} + +const StorageBaseStatementInternal* BindingParamsArray::getOwner() const { + return mOwningStatement; +} + +NS_IMPL_ISUPPORTS(BindingParamsArray, mozIStorageBindingParamsArray) + +/////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParamsArray + +NS_IMETHODIMP +BindingParamsArray::NewBindingParams(mozIStorageBindingParams** _params) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + nsCOMPtr params( + mOwningStatement->newBindingParams(this)); + NS_ENSURE_TRUE(params, NS_ERROR_UNEXPECTED); + + params.forget(_params); + return NS_OK; +} + +NS_IMETHODIMP +BindingParamsArray::AddParams(mozIStorageBindingParams* aParameters) { + NS_ENSURE_FALSE(mLocked, NS_ERROR_UNEXPECTED); + + BindingParams* params = static_cast(aParameters); + + // Check to make sure that this set of parameters was created with us. + if (params->getOwner() != this) return NS_ERROR_UNEXPECTED; + + mArray.AppendElement(params); + + // Lock the parameters only after we've successfully added them. + params->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +BindingParamsArray::GetLength(uint32_t* _length) { + *_length = length(); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageBindingParamsArray.h b/storage/mozStorageBindingParamsArray.h new file mode 100644 index 0000000000..58d5a52ab6 --- /dev/null +++ b/storage/mozStorageBindingParamsArray.h @@ -0,0 +1,105 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageBindingParamsArray_h +#define mozStorageBindingParamsArray_h + +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "mozilla/Attributes.h" + +#include "mozIStorageBindingParamsArray.h" + +namespace mozilla { +namespace storage { + +class StorageBaseStatementInternal; + +class BindingParamsArray final : public mozIStorageBindingParamsArray { + typedef nsTArray > array_type; + + ~BindingParamsArray() {} + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEBINDINGPARAMSARRAY + + explicit BindingParamsArray(StorageBaseStatementInternal* aOwningStatement); + + typedef array_type::size_type size_type; + + /** + * Locks the array and prevents further modification to it (such as adding + * more elements to it). + */ + void lock(); + + /** + * @return the pointer to the owning BindingParamsArray. + */ + const StorageBaseStatementInternal* getOwner() const; + + /** + * @return the number of elemets the array contains. + */ + size_type length() const { return mArray.Length(); } + + class iterator { + public: + iterator(BindingParamsArray* aArray, uint32_t aIndex) + : mArray(aArray), mIndex(aIndex) {} + + iterator& operator++(int) { + mIndex++; + return *this; + } + + bool operator==(const iterator& aOther) const { + return mIndex == aOther.mIndex; + } + bool operator!=(const iterator& aOther) const { return !(*this == aOther); } + mozIStorageBindingParams* operator*() { + NS_ASSERTION(mIndex < mArray->length(), + "Dereferenceing an invalid value!"); + return mArray->mArray[mIndex].get(); + } + + private: + void operator--() {} + BindingParamsArray* mArray; + uint32_t mIndex; + }; + + /** + * Obtains an iterator pointing to the beginning of the array. + */ + inline iterator begin() { + NS_ASSERTION(length() != 0, + "Obtaining an iterator to the beginning with no elements!"); + return iterator(this, 0); + } + + /** + * Obtains an iterator pointing to the end of the array. + */ + inline iterator end() { + NS_ASSERTION(mLocked, + "Obtaining an iterator to the end when we are not locked!"); + return iterator(this, length()); + } + + private: + nsCOMPtr mOwningStatement; + array_type mArray; + bool mLocked; + + friend class iterator; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageBindingParamsArray_h diff --git a/storage/mozStorageConnection.cpp b/storage/mozStorageConnection.cpp new file mode 100644 index 0000000000..e10458a68b --- /dev/null +++ b/storage/mozStorageConnection.cpp @@ -0,0 +1,2981 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "BaseVFS.h" +#include "ErrorList.h" +#include "nsError.h" +#include "nsThreadUtils.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "nsIXPConnect.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/Telemetry.h" +#include "mozilla/Mutex.h" +#include "mozilla/CondVar.h" +#include "mozilla/Attributes.h" +#include "mozilla/ErrorNames.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/quota/QuotaObject.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPrefs_storage.h" + +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageFunction.h" + +#include "mozStorageAsyncStatementExecution.h" +#include "mozStorageSQLFunctions.h" +#include "mozStorageConnection.h" +#include "mozStorageService.h" +#include "mozStorageStatement.h" +#include "mozStorageAsyncStatement.h" +#include "mozStorageArgValueArray.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementData.h" +#include "ObfuscatingVFS.h" +#include "QuotaVFS.h" +#include "StorageBaseStatementInternal.h" +#include "SQLCollations.h" +#include "FileSystemModule.h" +#include "mozStorageHelper.h" + +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "mozilla/Printf.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/RefPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsProxyRelease.h" +#include "nsStringFwd.h" +#include "nsURLHelper.h" + +#define MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH 524288000 // 500 MiB + +// Maximum size of the pages cache per connection. +#define MAX_CACHE_SIZE_KIBIBYTES 2048 // 2 MiB + +mozilla::LazyLogModule gStorageLog("mozStorage"); + +// Checks that the protected code is running on the main-thread only if the +// connection was also opened on it. +#ifdef DEBUG +# define CHECK_MAINTHREAD_ABUSE() \ + do { \ + NS_WARNING_ASSERTION( \ + eventTargetOpenedOn == GetMainThreadSerialEventTarget() || \ + !NS_IsMainThread(), \ + "Using Storage synchronous API on main-thread, but " \ + "the connection was opened on another thread."); \ + } while (0) +#else +# define CHECK_MAINTHREAD_ABUSE() \ + do { /* Nothing */ \ + } while (0) +#endif + +namespace mozilla::storage { + +using mozilla::dom::quota::QuotaObject; +using mozilla::Telemetry::AccumulateCategoricalKeyed; +using mozilla::Telemetry::LABELS_SQLITE_STORE_OPEN; +using mozilla::Telemetry::LABELS_SQLITE_STORE_QUERY; + +namespace { + +int nsresultToSQLiteResult(nsresult aXPCOMResultCode) { + if (NS_SUCCEEDED(aXPCOMResultCode)) { + return SQLITE_OK; + } + + switch (aXPCOMResultCode) { + case NS_ERROR_FILE_CORRUPTED: + return SQLITE_CORRUPT; + case NS_ERROR_FILE_ACCESS_DENIED: + return SQLITE_CANTOPEN; + case NS_ERROR_STORAGE_BUSY: + return SQLITE_BUSY; + case NS_ERROR_FILE_IS_LOCKED: + return SQLITE_LOCKED; + case NS_ERROR_FILE_READ_ONLY: + return SQLITE_READONLY; + case NS_ERROR_STORAGE_IOERR: + return SQLITE_IOERR; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + return SQLITE_FULL; + case NS_ERROR_OUT_OF_MEMORY: + return SQLITE_NOMEM; + case NS_ERROR_UNEXPECTED: + return SQLITE_MISUSE; + case NS_ERROR_ABORT: + return SQLITE_ABORT; + case NS_ERROR_STORAGE_CONSTRAINT: + return SQLITE_CONSTRAINT; + default: + return SQLITE_ERROR; + } + + MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Must return in switch above!"); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Variant Specialization Functions (variantToSQLiteT) + +int sqlite3_T_int(sqlite3_context* aCtx, int aValue) { + ::sqlite3_result_int(aCtx, aValue); + return SQLITE_OK; +} + +int sqlite3_T_int64(sqlite3_context* aCtx, sqlite3_int64 aValue) { + ::sqlite3_result_int64(aCtx, aValue); + return SQLITE_OK; +} + +int sqlite3_T_double(sqlite3_context* aCtx, double aValue) { + ::sqlite3_result_double(aCtx, aValue); + return SQLITE_OK; +} + +int sqlite3_T_text(sqlite3_context* aCtx, const nsCString& aValue) { + CheckedInt length(aValue.Length()); + if (!length.isValid()) { + return SQLITE_MISUSE; + } + ::sqlite3_result_text(aCtx, aValue.get(), length.value(), SQLITE_TRANSIENT); + return SQLITE_OK; +} + +int sqlite3_T_text16(sqlite3_context* aCtx, const nsString& aValue) { + CheckedInt n_bytes = + CheckedInt(aValue.Length()) * sizeof(char16_t); + if (!n_bytes.isValid()) { + return SQLITE_MISUSE; + } + ::sqlite3_result_text16(aCtx, aValue.get(), n_bytes.value(), + SQLITE_TRANSIENT); + return SQLITE_OK; +} + +int sqlite3_T_null(sqlite3_context* aCtx) { + ::sqlite3_result_null(aCtx); + return SQLITE_OK; +} + +int sqlite3_T_blob(sqlite3_context* aCtx, const void* aData, int aSize) { + ::sqlite3_result_blob(aCtx, aData, aSize, free); + return SQLITE_OK; +} + +#include "variantToSQLiteT_impl.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Modules + +struct Module { + const char* name; + int (*registerFunc)(sqlite3*, const char*); +}; + +Module gModules[] = {{"filesystem", RegisterFileSystemModule}}; + +//////////////////////////////////////////////////////////////////////////////// +//// Local Functions + +int tracefunc(unsigned aReason, void* aClosure, void* aP, void* aX) { + switch (aReason) { + case SQLITE_TRACE_STMT: { + // aP is a pointer to the prepared statement. + sqlite3_stmt* stmt = static_cast(aP); + // aX is a pointer to a string containing the unexpanded SQL or a comment, + // starting with "--"" in case of a trigger. + char* expanded = static_cast(aX); + // Simulate what sqlite_trace was doing. + if (!::strncmp(expanded, "--", 2)) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_STMT on %p: '%s'", aClosure, expanded)); + } else { + char* sql = ::sqlite3_expanded_sql(stmt); + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_STMT on %p: '%s'", aClosure, sql)); + ::sqlite3_free(sql); + } + break; + } + case SQLITE_TRACE_PROFILE: { + // aX is pointer to a 64bit integer containing nanoseconds it took to + // execute the last command. + sqlite_int64 time = *(static_cast(aX)) / 1000000; + if (time > 0) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("TRACE_TIME on %p: %lldms", aClosure, time)); + } + break; + } + } + return 0; +} + +void basicFunctionHelper(sqlite3_context* aCtx, int aArgc, + sqlite3_value** aArgv) { + void* userData = ::sqlite3_user_data(aCtx); + + mozIStorageFunction* func = static_cast(userData); + + RefPtr arguments(new ArgValueArray(aArgc, aArgv)); + if (!arguments) return; + + nsCOMPtr result; + nsresult rv = func->OnFunctionCall(arguments, getter_AddRefs(result)); + if (NS_FAILED(rv)) { + nsAutoCString errorMessage; + GetErrorName(rv, errorMessage); + errorMessage.InsertLiteral("User function returned ", 0); + errorMessage.Append('!'); + + NS_WARNING(errorMessage.get()); + + ::sqlite3_result_error(aCtx, errorMessage.get(), -1); + ::sqlite3_result_error_code(aCtx, nsresultToSQLiteResult(rv)); + return; + } + int retcode = variantToSQLiteT(aCtx, result); + if (retcode != SQLITE_OK) { + NS_WARNING("User function returned invalid data type!"); + ::sqlite3_result_error(aCtx, "User function returned invalid data type", + -1); + } +} + +RefPtr GetQuotaObject(sqlite3_file* aFile, bool obfuscatingVFS) { + return obfuscatingVFS + ? mozilla::storage::obfsvfs::GetQuotaObjectForFile(aFile) + : mozilla::storage::quotavfs::GetQuotaObjectForFile(aFile); +} + +/** + * This code is heavily based on the sample at: + * http://www.sqlite.org/unlock_notify.html + */ +class UnlockNotification { + public: + UnlockNotification() + : mMutex("UnlockNotification mMutex"), + mCondVar(mMutex, "UnlockNotification condVar"), + mSignaled(false) {} + + void Wait() { + MutexAutoLock lock(mMutex); + while (!mSignaled) { + (void)mCondVar.Wait(); + } + } + + void Signal() { + MutexAutoLock lock(mMutex); + mSignaled = true; + (void)mCondVar.Notify(); + } + + private: + Mutex mMutex MOZ_UNANNOTATED; + CondVar mCondVar; + bool mSignaled; +}; + +void UnlockNotifyCallback(void** aArgs, int aArgsSize) { + for (int i = 0; i < aArgsSize; i++) { + UnlockNotification* notification = + static_cast(aArgs[i]); + notification->Signal(); + } +} + +int WaitForUnlockNotify(sqlite3* aDatabase) { + UnlockNotification notification; + int srv = + ::sqlite3_unlock_notify(aDatabase, UnlockNotifyCallback, ¬ification); + MOZ_ASSERT(srv == SQLITE_LOCKED || srv == SQLITE_OK); + if (srv == SQLITE_OK) { + notification.Wait(); + } + + return srv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Local Classes + +class AsyncCloseConnection final : public Runnable { + public: + AsyncCloseConnection(Connection* aConnection, sqlite3* aNativeConnection, + nsIRunnable* aCallbackEvent) + : Runnable("storage::AsyncCloseConnection"), + mConnection(aConnection), + mNativeConnection(aNativeConnection), + mCallbackEvent(aCallbackEvent) {} + + NS_IMETHOD Run() override { + // Make sure we don't dispatch to the current thread. + MOZ_ASSERT(!IsOnCurrentSerialEventTarget(mConnection->eventTargetOpenedOn)); + + nsCOMPtr event = + NewRunnableMethod("storage::Connection::shutdownAsyncThread", + mConnection, &Connection::shutdownAsyncThread); + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event)); + + // Internal close. + (void)mConnection->internalClose(mNativeConnection); + + // Callback + if (mCallbackEvent) { + nsCOMPtr thread; + (void)NS_GetMainThread(getter_AddRefs(thread)); + (void)thread->Dispatch(mCallbackEvent, NS_DISPATCH_NORMAL); + } + + return NS_OK; + } + + ~AsyncCloseConnection() override { + NS_ReleaseOnMainThread("AsyncCloseConnection::mConnection", + mConnection.forget()); + NS_ReleaseOnMainThread("AsyncCloseConnection::mCallbackEvent", + mCallbackEvent.forget()); + } + + private: + RefPtr mConnection; + sqlite3* mNativeConnection; + nsCOMPtr mCallbackEvent; +}; + +/** + * An event used to initialize the clone of a connection. + * + * Must be executed on the clone's async execution thread. + */ +class AsyncInitializeClone final : public Runnable { + public: + /** + * @param aConnection The connection being cloned. + * @param aClone The clone. + * @param aReadOnly If |true|, the clone is read only. + * @param aCallback A callback to trigger once initialization + * is complete. This event will be called on + * aClone->eventTargetOpenedOn. + */ + AsyncInitializeClone(Connection* aConnection, Connection* aClone, + const bool aReadOnly, + mozIStorageCompletionCallback* aCallback) + : Runnable("storage::AsyncInitializeClone"), + mConnection(aConnection), + mClone(aClone), + mReadOnly(aReadOnly), + mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + nsresult rv = mConnection->initializeClone(mClone, mReadOnly); + if (NS_FAILED(rv)) { + return Dispatch(rv, nullptr); + } + return Dispatch(NS_OK, + NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, mClone)); + } + + private: + nsresult Dispatch(nsresult aResult, nsISupports* aValue) { + RefPtr event = + new CallbackComplete(aResult, aValue, mCallback.forget()); + return mClone->eventTargetOpenedOn->Dispatch(event, NS_DISPATCH_NORMAL); + } + + ~AsyncInitializeClone() override { + nsCOMPtr thread; + DebugOnly rv = NS_GetMainThread(getter_AddRefs(thread)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Handle ambiguous nsISupports inheritance. + NS_ProxyRelease("AsyncInitializeClone::mConnection", thread, + mConnection.forget()); + NS_ProxyRelease("AsyncInitializeClone::mClone", thread, mClone.forget()); + + // Generally, the callback will be released by CallbackComplete. + // However, if for some reason Run() is not executed, we still + // need to ensure that it is released here. + NS_ProxyRelease("AsyncInitializeClone::mCallback", thread, + mCallback.forget()); + } + + RefPtr mConnection; + RefPtr mClone; + const bool mReadOnly; + nsCOMPtr mCallback; +}; + +/** + * A listener for async connection closing. + */ +class CloseListener final : public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + CloseListener() : mClosed(false) {} + + NS_IMETHOD Complete(nsresult, nsISupports*) override { + mClosed = true; + return NS_OK; + } + + bool mClosed; + + private: + ~CloseListener() = default; +}; + +NS_IMPL_ISUPPORTS(CloseListener, mozIStorageCompletionCallback) + +class AsyncVacuumEvent final : public Runnable { + public: + AsyncVacuumEvent(Connection* aConnection, + mozIStorageCompletionCallback* aCallback, + bool aUseIncremental, int32_t aSetPageSize) + : Runnable("storage::AsyncVacuum"), + mConnection(aConnection), + mCallback(aCallback), + mUseIncremental(aUseIncremental), + mSetPageSize(aSetPageSize), + mStatus(NS_ERROR_UNEXPECTED) {} + + NS_IMETHOD Run() override { + // This is initially dispatched to the helper thread, then re-dispatched + // to the opener thread, where it will callback. + if (IsOnCurrentSerialEventTarget(mConnection->eventTargetOpenedOn)) { + // Send the completion event. + if (mCallback) { + mozilla::Unused << mCallback->Complete(mStatus, nullptr); + } + return NS_OK; + } + + // Ensure to invoke the callback regardless of errors. + auto guard = MakeScopeExit([&]() { + mConnection->mIsStatementOnHelperThreadInterruptible = false; + mozilla::Unused << mConnection->eventTargetOpenedOn->Dispatch( + this, NS_DISPATCH_NORMAL); + }); + + // Get list of attached databases. + nsCOMPtr stmt; + nsresult rv = mConnection->CreateStatement(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA database_list"_ns, + getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + // We must accumulate names and loop through them later, otherwise VACUUM + // will see an ongoing statement and bail out. + nsTArray schemaNames; + bool hasResult = false; + while (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString name; + rv = stmt->GetUTF8String(1, name); + if (NS_SUCCEEDED(rv) && !name.EqualsLiteral("temp")) { + schemaNames.AppendElement(name); + } + } + mStatus = NS_OK; + // Mark this vacuum as an interruptible operation, so it can be interrupted + // if the connection closes during shutdown. + mConnection->mIsStatementOnHelperThreadInterruptible = true; + for (const nsCString& schemaName : schemaNames) { + rv = this->Vacuum(schemaName); + if (NS_FAILED(rv)) { + // This is sub-optimal since it's only keeping the last error reason, + // but it will do for now. + mStatus = rv; + } + } + return mStatus; + } + + nsresult Vacuum(const nsACString& aSchemaName) { + // Abort if we're in shutdown. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return NS_ERROR_ABORT; + } + int32_t removablePages = mConnection->RemovablePagesInFreeList(aSchemaName); + if (!removablePages) { + // There's no empty pages to remove, so skip this vacuum for now. + return NS_OK; + } + nsresult rv; + bool needsFullVacuum = true; + + if (mSetPageSize) { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".page_size = "); + query.AppendInt(mSetPageSize); + nsCOMPtr stmt; + rv = mConnection->ExecuteSimpleSQL(query); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Check auto_vacuum. + { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".auto_vacuum"); + nsCOMPtr stmt; + rv = mConnection->CreateStatement(query, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasResult = false; + bool changeAutoVacuum = false; + if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + bool isIncrementalVacuum = stmt->AsInt32(0) == 2; + changeAutoVacuum = isIncrementalVacuum != mUseIncremental; + if (isIncrementalVacuum && !changeAutoVacuum) { + needsFullVacuum = false; + } + } + // Changing auto_vacuum is only supported on the main schema. + if (aSchemaName.EqualsLiteral("main") && changeAutoVacuum) { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".auto_vacuum = "); + query.AppendInt(mUseIncremental ? 2 : 0); + rv = mConnection->ExecuteSimpleSQL(query); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + if (needsFullVacuum) { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "VACUUM "); + query.Append(aSchemaName); + rv = mConnection->ExecuteSimpleSQL(query); + // TODO (Bug 1818039): Report failed vacuum telemetry. + NS_ENSURE_SUCCESS(rv, rv); + } else { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".incremental_vacuum("); + query.AppendInt(removablePages); + query.AppendLiteral(")"); + rv = mConnection->ExecuteSimpleSQL(query); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; + } + + ~AsyncVacuumEvent() override { + NS_ReleaseOnMainThread("AsyncVacuum::mConnection", mConnection.forget()); + NS_ReleaseOnMainThread("AsyncVacuum::mCallback", mCallback.forget()); + } + + private: + RefPtr mConnection; + nsCOMPtr mCallback; + bool mUseIncremental; + int32_t mSetPageSize; + Atomic mStatus; +}; + +/** + * A runnable to perform an SQLite database backup when there may be one or more + * open connections on that database. + */ +class AsyncBackupDatabaseFile final : public Runnable, public nsITimerCallback { + public: + NS_DECL_ISUPPORTS_INHERITED + + /** + * @param aConnection The connection to the database being backed up. + * @param aNativeConnection The native connection to the database being backed + * up. + * @param aDestinationFile The destination file for the created backup. + * @param aCallback A callback to trigger once the backup process has + * completed. The callback will be supplied with an nsresult + * indicating whether or not the backup was successfully + * created. This callback will be called on the + * mConnection->eventTargetOpenedOn thread. + * @throws + */ + AsyncBackupDatabaseFile(Connection* aConnection, sqlite3* aNativeConnection, + nsIFile* aDestinationFile, + mozIStorageCompletionCallback* aCallback) + : Runnable("storage::AsyncBackupDatabaseFile"), + mConnection(aConnection), + mNativeConnection(aNativeConnection), + mDestinationFile(aDestinationFile), + mCallback(aCallback), + mBackupFile(nullptr), + mBackupHandle(nullptr) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + + nsAutoString path; + nsresult rv = mDestinationFile->GetPath(path); + if (NS_FAILED(rv)) { + return Dispatch(rv, nullptr); + } + // Put a .tmp on the end of the destination while the backup is underway. + // This extension will be stripped off after the backup successfully + // completes. + path.AppendLiteral(".tmp"); + + int srv = ::sqlite3_open(NS_ConvertUTF16toUTF8(path).get(), &mBackupFile); + if (srv != SQLITE_OK) { + return Dispatch(NS_ERROR_FAILURE, nullptr); + } + + static const char* mainDBName = "main"; + + mBackupHandle = ::sqlite3_backup_init(mBackupFile, mainDBName, + mNativeConnection, mainDBName); + if (!mBackupHandle) { + MOZ_ALWAYS_TRUE(::sqlite3_close(mBackupFile) == SQLITE_OK); + return Dispatch(NS_ERROR_FAILURE, nullptr); + } + + return DoStep(); + } + + NS_IMETHOD + Notify(nsITimer* aTimer) override { return DoStep(); } + + private: + nsresult DoStep() { +#define DISPATCH_AND_RETURN_IF_FAILED(rv) \ + if (NS_FAILED(rv)) { \ + return Dispatch(rv, nullptr); \ + } + + // This guard is used to close the backup database in the event of + // some failure throughout this process. We release the exit guard + // only if we complete the backup successfully, or defer to another + // later call to DoStep. + auto guard = MakeScopeExit([&]() { + MOZ_ALWAYS_TRUE(::sqlite3_close(mBackupFile) == SQLITE_OK); + mBackupFile = nullptr; + }); + + MOZ_ASSERT(!NS_IsMainThread()); + nsAutoString originalPath; + nsresult rv = mDestinationFile->GetPath(originalPath); + DISPATCH_AND_RETURN_IF_FAILED(rv); + + nsAutoString tempPath = originalPath; + tempPath.AppendLiteral(".tmp"); + + nsCOMPtr file = + do_CreateInstance("@mozilla.org/file/local;1", &rv); + DISPATCH_AND_RETURN_IF_FAILED(rv); + + rv = file->InitWithPath(tempPath); + DISPATCH_AND_RETURN_IF_FAILED(rv); + + // The number of milliseconds to wait between each batch of copies. + static constexpr uint32_t STEP_DELAY_MS = 250; + // The number of pages to copy per step + static constexpr int COPY_PAGES = 5; + + int srv = ::sqlite3_backup_step(mBackupHandle, COPY_PAGES); + if (srv == SQLITE_OK || srv == SQLITE_BUSY || srv == SQLITE_LOCKED) { + // We're continuing the backup later. Release the guard to avoid closing + // the database. + guard.release(); + // Queue up the next step + return NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, + STEP_DELAY_MS, nsITimer::TYPE_ONE_SHOT, + GetCurrentSerialEventTarget()); + } +#ifdef DEBUG + if (srv != SQLITE_DONE) { + nsCString warnMsg; + warnMsg.AppendLiteral( + "The SQLite database copy could not be completed due to an error: "); + warnMsg.Append(::sqlite3_errmsg(mBackupFile)); + NS_WARNING(warnMsg.get()); + } +#endif + + (void)::sqlite3_backup_finish(mBackupHandle); + MOZ_ALWAYS_TRUE(::sqlite3_close(mBackupFile) == SQLITE_OK); + mBackupFile = nullptr; + + // The database is already closed, so we can release this guard now. + guard.release(); + + if (srv != SQLITE_DONE) { + NS_WARNING("Failed to create database copy."); + + // The partially created database file is not useful. Let's remove it. + rv = file->Remove(false); + if (NS_FAILED(rv)) { + NS_WARNING( + "Removing a partially backed up SQLite database file failed."); + } + + return Dispatch(convertResultCode(srv), nullptr); + } + + // Now that we've successfully created the copy, we'll strip off the .tmp + // extension. + + nsAutoString leafName; + rv = mDestinationFile->GetLeafName(leafName); + DISPATCH_AND_RETURN_IF_FAILED(rv); + + rv = file->RenameTo(nullptr, leafName); + DISPATCH_AND_RETURN_IF_FAILED(rv); + +#undef DISPATCH_AND_RETURN_IF_FAILED + return Dispatch(NS_OK, nullptr); + } + + nsresult Dispatch(nsresult aResult, nsISupports* aValue) { + RefPtr event = + new CallbackComplete(aResult, aValue, mCallback.forget()); + return mConnection->eventTargetOpenedOn->Dispatch(event, + NS_DISPATCH_NORMAL); + } + + ~AsyncBackupDatabaseFile() override { + nsresult rv; + nsCOMPtr thread = + do_QueryInterface(mConnection->eventTargetOpenedOn, &rv); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + + // Handle ambiguous nsISupports inheritance. + NS_ProxyRelease("AsyncBackupDatabaseFile::mConnection", thread, + mConnection.forget()); + NS_ProxyRelease("AsyncBackupDatabaseFile::mDestinationFile", thread, + mDestinationFile.forget()); + + // Generally, the callback will be released by CallbackComplete. + // However, if for some reason Run() is not executed, we still + // need to ensure that it is released here. + NS_ProxyRelease("AsyncInitializeClone::mCallback", thread, + mCallback.forget()); + } + + RefPtr mConnection; + sqlite3* mNativeConnection; + nsCOMPtr mTimer; + nsCOMPtr mDestinationFile; + nsCOMPtr mCallback; + sqlite3* mBackupFile; + sqlite3_backup* mBackupHandle; +}; + +NS_IMPL_ISUPPORTS_INHERITED(AsyncBackupDatabaseFile, Runnable, nsITimerCallback) + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Connection + +Connection::Connection(Service* aService, int aFlags, + ConnectionOperation aSupportedOperations, + const nsCString& aTelemetryFilename, bool aInterruptible, + bool aIgnoreLockingMode) + : sharedAsyncExecutionMutex("Connection::sharedAsyncExecutionMutex"), + sharedDBMutex("Connection::sharedDBMutex"), + eventTargetOpenedOn(WrapNotNull(GetCurrentSerialEventTarget())), + mIsStatementOnHelperThreadInterruptible(false), + mDBConn(nullptr), + mDefaultTransactionType(mozIStorageConnection::TRANSACTION_DEFERRED), + mDestroying(false), + mProgressHandler(nullptr), + mStorageService(aService), + mFlags(aFlags), + mTransactionNestingLevel(0), + mSupportedOperations(aSupportedOperations), + mInterruptible(aSupportedOperations == Connection::ASYNCHRONOUS || + aInterruptible), + mIgnoreLockingMode(aIgnoreLockingMode), + mAsyncExecutionThreadShuttingDown(false), + mConnectionClosed(false), + mGrowthChunkSize(0) { + MOZ_ASSERT(!mIgnoreLockingMode || mFlags & SQLITE_OPEN_READONLY, + "Can't ignore locking for a non-readonly connection!"); + mStorageService->registerConnection(this); + MOZ_ASSERT(!aTelemetryFilename.IsEmpty(), + "A telemetry filename should have been passed-in."); + mTelemetryFilename.Assign(aTelemetryFilename); +} + +Connection::~Connection() { + // Failsafe Close() occurs in our custom Release method because of + // complications related to Close() potentially invoking AsyncClose() which + // will increment our refcount. + MOZ_ASSERT(!mAsyncExecutionThread, + "The async thread has not been shutdown properly!"); +} + +NS_IMPL_ADDREF(Connection) + +NS_INTERFACE_MAP_BEGIN(Connection) + NS_INTERFACE_MAP_ENTRY(mozIStorageAsyncConnection) + NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor) + NS_INTERFACE_MAP_ENTRY(mozIStorageConnection) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageConnection) +NS_INTERFACE_MAP_END + +// This is identical to what NS_IMPL_RELEASE provides, but with the +// extra |1 == count| case. +NS_IMETHODIMP_(MozExternalRefCountType) Connection::Release(void) { + MOZ_ASSERT(0 != mRefCnt, "dup release"); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "Connection"); + if (1 == count) { + // If the refcount went to 1, the single reference must be from + // gService->mConnections (in class |Service|). And the code calling + // Release is either: + // - The "user" code that had created the connection, releasing on any + // thread. + // - One of Service's getConnections() callers had acquired a strong + // reference to the Connection that out-lived the last "user" reference, + // and now that just got dropped. Note that this reference could be + // getting dropped on the main thread or Connection->eventTargetOpenedOn + // (because of the NewRunnableMethod used by minimizeMemory). + // + // Either way, we should now perform our failsafe Close() and unregister. + // However, we only want to do this once, and the reality is that our + // refcount could go back up above 1 and down again at any time if we are + // off the main thread and getConnections() gets called on the main thread, + // so we use an atomic here to do this exactly once. + if (mDestroying.compareExchange(false, true)) { + // Close the connection, dispatching to the opening event target if we're + // not on that event target already and that event target is still + // accepting runnables. We do this because it's possible we're on the main + // thread because of getConnections(), and we REALLY don't want to + // transfer I/O to the main thread if we can avoid it. + if (IsOnCurrentSerialEventTarget(eventTargetOpenedOn)) { + // This could cause SpinningSynchronousClose() to be invoked and AddRef + // triggered for AsyncCloseConnection's strong ref if the conn was ever + // use for async purposes. (Main-thread only, though.) + Unused << synchronousClose(); + } else { + nsCOMPtr event = + NewRunnableMethod("storage::Connection::synchronousClose", this, + &Connection::synchronousClose); + if (NS_FAILED(eventTargetOpenedOn->Dispatch(event.forget(), + NS_DISPATCH_NORMAL))) { + // The event target was dead and so we've just leaked our runnable. + // This should not happen because our non-main-thread consumers should + // be explicitly closing their connections, not relying on us to close + // them for them. (It's okay to let a statement go out of scope for + // automatic cleanup, but not a Connection.) + MOZ_ASSERT(false, + "Leaked Connection::synchronousClose(), ownership fail."); + Unused << synchronousClose(); + } + } + + // This will drop its strong reference right here, right now. + mStorageService->unregisterConnection(this); + } + } else if (0 == count) { + mRefCnt = 1; /* stabilize */ +#if 0 /* enable this to find non-threadsafe destructors: */ + NS_ASSERT_OWNINGTHREAD(Connection); +#endif + delete (this); + return 0; + } + return count; +} + +int32_t Connection::getSqliteRuntimeStatus(int32_t aStatusOption, + int32_t* aMaxValue) { + MOZ_ASSERT(connectionReady(), "A connection must exist at this point"); + int curr = 0, max = 0; + DebugOnly rc = + ::sqlite3_db_status(mDBConn, aStatusOption, &curr, &max, 0); + MOZ_ASSERT(NS_SUCCEEDED(convertResultCode(rc))); + if (aMaxValue) *aMaxValue = max; + return curr; +} + +nsIEventTarget* Connection::getAsyncExecutionTarget() { + NS_ENSURE_TRUE(IsOnCurrentSerialEventTarget(eventTargetOpenedOn), nullptr); + + // Don't return the asynchronous event target if we are shutting down. + if (mAsyncExecutionThreadShuttingDown) { + return nullptr; + } + + // Create the async event target if there's none yet. + if (!mAsyncExecutionThread) { + // Names start with "sqldb:" followed by a recognizable name, like the + // database file name, or a specially crafted name like "memory". + // This name will be surfaced on https://crash-stats.mozilla.org, so any + // sensitive part of the file name (e.g. an URL origin) should be replaced + // by passing an explicit telemetryName to openDatabaseWithFileURL. + nsAutoCString name("sqldb:"_ns); + name.Append(mTelemetryFilename); + static nsThreadPoolNaming naming; + nsresult rv = NS_NewNamedThread(naming.GetNextThreadName(name), + getter_AddRefs(mAsyncExecutionThread)); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to create async thread."); + return nullptr; + } + mAsyncExecutionThread->SetNameForWakeupTelemetry("mozStorage (all)"_ns); + } + + return mAsyncExecutionThread; +} + +void Connection::RecordOpenStatus(nsresult rv) { + nsCString histogramKey = mTelemetryFilename; + + if (histogramKey.IsEmpty()) { + histogramKey.AssignLiteral("unknown"); + } + + if (NS_SUCCEEDED(rv)) { + AccumulateCategoricalKeyed(histogramKey, LABELS_SQLITE_STORE_OPEN::success); + return; + } + + switch (rv) { + case NS_ERROR_FILE_CORRUPTED: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_OPEN::corrupt); + break; + case NS_ERROR_STORAGE_IOERR: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_OPEN::diskio); + break; + case NS_ERROR_FILE_ACCESS_DENIED: + case NS_ERROR_FILE_IS_LOCKED: + case NS_ERROR_FILE_READ_ONLY: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_OPEN::access); + break; + case NS_ERROR_FILE_NO_DEVICE_SPACE: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_OPEN::diskspace); + break; + default: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_OPEN::failure); + } +} + +void Connection::RecordQueryStatus(int srv) { + nsCString histogramKey = mTelemetryFilename; + + if (histogramKey.IsEmpty()) { + histogramKey.AssignLiteral("unknown"); + } + + switch (srv) { + case SQLITE_OK: + case SQLITE_ROW: + case SQLITE_DONE: + + // Note that these are returned when we intentionally cancel a statement so + // they aren't indicating a failure. + case SQLITE_ABORT: + case SQLITE_INTERRUPT: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::success); + break; + case SQLITE_CORRUPT: + case SQLITE_NOTADB: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::corrupt); + break; + case SQLITE_PERM: + case SQLITE_CANTOPEN: + case SQLITE_LOCKED: + case SQLITE_READONLY: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::access); + break; + case SQLITE_IOERR: + case SQLITE_NOLFS: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::diskio); + break; + case SQLITE_FULL: + case SQLITE_TOOBIG: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::diskspace); + break; + case SQLITE_CONSTRAINT: + case SQLITE_RANGE: + case SQLITE_MISMATCH: + case SQLITE_MISUSE: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::misuse); + break; + case SQLITE_BUSY: + AccumulateCategoricalKeyed(histogramKey, LABELS_SQLITE_STORE_QUERY::busy); + break; + default: + AccumulateCategoricalKeyed(histogramKey, + LABELS_SQLITE_STORE_QUERY::failure); + } +} + +nsresult Connection::initialize(const nsACString& aStorageKey, + const nsACString& aName) { + MOZ_ASSERT(aStorageKey.Equals(kMozStorageMemoryStorageKey)); + NS_ASSERTION(!connectionReady(), + "Initialize called on already opened database!"); + MOZ_ASSERT(!mIgnoreLockingMode, "Can't ignore locking on an in-memory db."); + AUTO_PROFILER_LABEL("Connection::initialize", OTHER); + + mStorageKey = aStorageKey; + mName = aName; + + // in memory database requested, sqlite uses a magic file name + + const nsAutoCString path = + mName.IsEmpty() ? nsAutoCString(":memory:"_ns) + : "file:"_ns + mName + "?mode=memory&cache=shared"_ns; + + int srv = ::sqlite3_open_v2(path.get(), &mDBConn, mFlags, + basevfs::GetVFSName(true)); + if (srv != SQLITE_OK) { + mDBConn = nullptr; + nsresult rv = convertResultCode(srv); + RecordOpenStatus(rv); + return rv; + } + +#ifdef MOZ_SQLITE_FTS3_TOKENIZER + srv = + ::sqlite3_db_config(mDBConn, SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 1, 0); + MOZ_ASSERT(srv == SQLITE_OK, + "SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER should be enabled"); +#endif + + // Do not set mDatabaseFile or mFileURL here since this is a "memory" + // database. + + nsresult rv = initializeInternal(); + RecordOpenStatus(rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult Connection::initialize(nsIFile* aDatabaseFile) { + NS_ASSERTION(aDatabaseFile, "Passed null file!"); + NS_ASSERTION(!connectionReady(), + "Initialize called on already opened database!"); + AUTO_PROFILER_LABEL("Connection::initialize", OTHER); + + // Do not set mFileURL here since this is database does not have an associated + // URL. + mDatabaseFile = aDatabaseFile; + + nsAutoString path; + nsresult rv = aDatabaseFile->GetPath(path); + NS_ENSURE_SUCCESS(rv, rv); + + bool exclusive = StaticPrefs::storage_sqlite_exclusiveLock_enabled(); + int srv; + if (mIgnoreLockingMode) { + exclusive = false; + srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, mFlags, + "readonly-immutable-nolock"); + } else { + srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, mFlags, + basevfs::GetVFSName(exclusive)); + if (exclusive && (srv == SQLITE_LOCKED || srv == SQLITE_BUSY)) { + // Retry without trying to get an exclusive lock. + exclusive = false; + srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, + mFlags, basevfs::GetVFSName(false)); + } + } + if (srv != SQLITE_OK) { + mDBConn = nullptr; + rv = convertResultCode(srv); + RecordOpenStatus(rv); + return rv; + } + + rv = initializeInternal(); + if (exclusive && + (rv == NS_ERROR_STORAGE_BUSY || rv == NS_ERROR_FILE_IS_LOCKED)) { + // Usually SQLite will fail to acquire an exclusive lock on opening, but in + // some cases it may successfully open the database and then lock on the + // first query execution. When initializeInternal fails it closes the + // connection, so we can try to restart it in non-exclusive mode. + srv = ::sqlite3_open_v2(NS_ConvertUTF16toUTF8(path).get(), &mDBConn, mFlags, + basevfs::GetVFSName(false)); + if (srv == SQLITE_OK) { + rv = initializeInternal(); + } + } + + RecordOpenStatus(rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult Connection::initialize(nsIFileURL* aFileURL) { + NS_ASSERTION(aFileURL, "Passed null file URL!"); + NS_ASSERTION(!connectionReady(), + "Initialize called on already opened database!"); + AUTO_PROFILER_LABEL("Connection::initialize", OTHER); + + nsCOMPtr databaseFile; + nsresult rv = aFileURL->GetFile(getter_AddRefs(databaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + + // Set both mDatabaseFile and mFileURL here. + mFileURL = aFileURL; + mDatabaseFile = databaseFile; + + nsAutoCString spec; + rv = aFileURL->GetSpec(spec); + NS_ENSURE_SUCCESS(rv, rv); + + // If there is a key specified, we need to use the obfuscating VFS. + nsAutoCString query; + rv = aFileURL->GetQuery(query); + NS_ENSURE_SUCCESS(rv, rv); + + bool hasKey = false; + bool hasDirectoryLockId = false; + + MOZ_ALWAYS_TRUE(URLParams::Parse( + query, [&hasKey, &hasDirectoryLockId](const nsAString& aName, + const nsAString& aValue) { + if (aName.EqualsLiteral("key")) { + hasKey = true; + return true; + } + if (aName.EqualsLiteral("directoryLockId")) { + hasDirectoryLockId = true; + return true; + } + return true; + })); + + bool exclusive = StaticPrefs::storage_sqlite_exclusiveLock_enabled(); + + const char* const vfs = hasKey ? obfsvfs::GetVFSName() + : hasDirectoryLockId ? quotavfs::GetVFSName() + : basevfs::GetVFSName(exclusive); + + int srv = ::sqlite3_open_v2(spec.get(), &mDBConn, mFlags, vfs); + if (srv != SQLITE_OK) { + mDBConn = nullptr; + rv = convertResultCode(srv); + RecordOpenStatus(rv); + return rv; + } + + rv = initializeInternal(); + RecordOpenStatus(rv); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +nsresult Connection::initializeInternal() { + MOZ_ASSERT(mDBConn); + auto guard = MakeScopeExit([&]() { initializeFailed(); }); + + mConnectionClosed = false; + +#ifdef MOZ_SQLITE_FTS3_TOKENIZER + DebugOnly srv2 = + ::sqlite3_db_config(mDBConn, SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER, 1, 0); + MOZ_ASSERT(srv2 == SQLITE_OK, + "SQLITE_DBCONFIG_ENABLE_FTS3_TOKENIZER should be enabled"); +#endif + + // Properly wrap the database handle's mutex. + sharedDBMutex.initWithMutex(sqlite3_db_mutex(mDBConn)); + + // SQLite tracing can slow down queries (especially long queries) + // significantly. Don't trace unless the user is actively monitoring SQLite. + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + ::sqlite3_trace_v2(mDBConn, SQLITE_TRACE_STMT | SQLITE_TRACE_PROFILE, + tracefunc, this); + + MOZ_LOG( + gStorageLog, LogLevel::Debug, + ("Opening connection to '%s' (%p)", mTelemetryFilename.get(), this)); + } + + int64_t pageSize = Service::kDefaultPageSize; + + // Set page_size to the preferred default value. This is effective only if + // the database has just been created, otherwise, if the database does not + // use WAL journal mode, a VACUUM operation will updated its page_size. + nsAutoCString pageSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA page_size = "); + pageSizeQuery.AppendInt(pageSize); + int srv = executeSql(mDBConn, pageSizeQuery.get()); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + // Setting the cache_size forces the database open, verifying if it is valid + // or corrupt. So this is executed regardless it being actually needed. + // The cache_size is calculated from the actual page_size, to save memory. + nsAutoCString cacheSizeQuery(MOZ_STORAGE_UNIQUIFY_QUERY_STR + "PRAGMA cache_size = "); + cacheSizeQuery.AppendInt(-MAX_CACHE_SIZE_KIBIBYTES); + srv = executeSql(mDBConn, cacheSizeQuery.get()); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + // Register our built-in SQL functions. + srv = registerFunctions(mDBConn); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + // Register our built-in SQL collating sequences. + srv = registerCollations(mDBConn, mStorageService); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + // Set the default synchronous value. Each consumer can switch this + // accordingly to their needs. +#if defined(ANDROID) + // Android prefers synchronous = OFF for performance reasons. + Unused << ExecuteSimpleSQL("PRAGMA synchronous = OFF;"_ns); +#else + // Normal is the suggested value for WAL journals. + Unused << ExecuteSimpleSQL("PRAGMA synchronous = NORMAL;"_ns); +#endif + + // Initialization succeeded, we can stop guarding for failures. + guard.release(); + return NS_OK; +} + +nsresult Connection::initializeOnAsyncThread(nsIFile* aStorageFile) { + MOZ_ASSERT(!IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + nsresult rv = aStorageFile + ? initialize(aStorageFile) + : initialize(kMozStorageMemoryStorageKey, VoidCString()); + if (NS_FAILED(rv)) { + // Shutdown the async thread, since initialization failed. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + mAsyncExecutionThreadShuttingDown = true; + nsCOMPtr event = + NewRunnableMethod("Connection::shutdownAsyncThread", this, + &Connection::shutdownAsyncThread); + Unused << NS_DispatchToMainThread(event); + } + return rv; +} + +void Connection::initializeFailed() { + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + mConnectionClosed = true; + } + MOZ_ALWAYS_TRUE(::sqlite3_close(mDBConn) == SQLITE_OK); + mDBConn = nullptr; + sharedDBMutex.destroy(); +} + +nsresult Connection::databaseElementExists( + enum DatabaseElementType aElementType, const nsACString& aElementName, + bool* _exists) { + if (!connectionReady()) { + return NS_ERROR_NOT_AVAILABLE; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // When constructing the query, make sure to SELECT the correct db's + // sqlite_master if the user is prefixing the element with a specific db. ex: + // sample.test + nsCString query("SELECT name FROM (SELECT * FROM "); + nsDependentCSubstring element; + int32_t ind = aElementName.FindChar('.'); + if (ind == kNotFound) { + element.Assign(aElementName); + } else { + nsDependentCSubstring db(Substring(aElementName, 0, ind + 1)); + element.Assign(Substring(aElementName, ind + 1, aElementName.Length())); + query.Append(db); + } + query.AppendLiteral( + "sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type = " + "'"); + + switch (aElementType) { + case INDEX: + query.AppendLiteral("index"); + break; + case TABLE: + query.AppendLiteral("table"); + break; + } + query.AppendLiteral("' AND name ='"); + query.Append(element); + query.Append('\''); + + sqlite3_stmt* stmt; + int srv = prepareStatement(mDBConn, query, &stmt); + if (srv != SQLITE_OK) { + RecordQueryStatus(srv); + return convertResultCode(srv); + } + + srv = stepStatement(mDBConn, stmt); + // we just care about the return value from step + (void)::sqlite3_finalize(stmt); + + RecordQueryStatus(srv); + + if (srv == SQLITE_ROW) { + *_exists = true; + return NS_OK; + } + if (srv == SQLITE_DONE) { + *_exists = false; + return NS_OK; + } + + return convertResultCode(srv); +} + +bool Connection::findFunctionByInstance(mozIStorageFunction* aInstance) { + sharedDBMutex.assertCurrentThreadOwns(); + + for (const auto& data : mFunctions.Values()) { + if (data.function == aInstance) { + return true; + } + } + return false; +} + +/* static */ +int Connection::sProgressHelper(void* aArg) { + Connection* _this = static_cast(aArg); + return _this->progressHandler(); +} + +int Connection::progressHandler() { + sharedDBMutex.assertCurrentThreadOwns(); + if (mProgressHandler) { + bool result; + nsresult rv = mProgressHandler->OnProgress(this, &result); + if (NS_FAILED(rv)) return 0; // Don't break request + return result ? 1 : 0; + } + return 0; +} + +nsresult Connection::setClosedState() { + // Flag that we are shutting down the async thread, so that + // getAsyncExecutionTarget knows not to expose/create the async thread. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + NS_ENSURE_FALSE(mAsyncExecutionThreadShuttingDown, NS_ERROR_UNEXPECTED); + + mAsyncExecutionThreadShuttingDown = true; + + // Set the property to null before closing the connection, otherwise the + // other functions in the module may try to use the connection after it is + // closed. + mDBConn = nullptr; + + return NS_OK; +} + +bool Connection::operationSupported(ConnectionOperation aOperationType) { + if (aOperationType == ASYNCHRONOUS) { + // Async operations are supported for all connections, on any thread. + return true; + } + // Sync operations are supported for sync connections (on any thread), and + // async connections on a background thread. + MOZ_ASSERT(aOperationType == SYNCHRONOUS); + return mSupportedOperations == SYNCHRONOUS || !NS_IsMainThread(); +} + +nsresult Connection::ensureOperationSupported( + ConnectionOperation aOperationType) { + if (NS_WARN_IF(!operationSupported(aOperationType))) { +#ifdef DEBUG + if (NS_IsMainThread()) { + nsCOMPtr xpc = nsIXPConnect::XPConnect(); + Unused << xpc->DebugDumpJSStack(false, false, false); + } +#endif + MOZ_ASSERT(false, + "Don't use async connections synchronously on the main thread"); + return NS_ERROR_NOT_AVAILABLE; + } + return NS_OK; +} + +bool Connection::isConnectionReadyOnThisThread() { + MOZ_ASSERT_IF(connectionReady(), !mConnectionClosed); + if (mAsyncExecutionThread && mAsyncExecutionThread->IsOnCurrentThread()) { + return true; + } + return connectionReady(); +} + +bool Connection::isClosing() { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + return mAsyncExecutionThreadShuttingDown && !mConnectionClosed; +} + +bool Connection::isClosed() { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + return mConnectionClosed; +} + +bool Connection::isClosed(MutexAutoLock& lock) { return mConnectionClosed; } + +bool Connection::isAsyncExecutionThreadAvailable() { + MOZ_ASSERT(IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + return mAsyncExecutionThread && !mAsyncExecutionThreadShuttingDown; +} + +void Connection::shutdownAsyncThread() { + MOZ_ASSERT(IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + MOZ_ASSERT(mAsyncExecutionThread); + MOZ_ASSERT(mAsyncExecutionThreadShuttingDown); + + MOZ_ALWAYS_SUCCEEDS(mAsyncExecutionThread->Shutdown()); + mAsyncExecutionThread = nullptr; +} + +nsresult Connection::internalClose(sqlite3* aNativeConnection) { +#ifdef DEBUG + { // Make sure we have marked our async thread as shutting down. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + MOZ_ASSERT(mAsyncExecutionThreadShuttingDown, + "Did not call setClosedState!"); + MOZ_ASSERT(!isClosed(lockedScope), "Unexpected closed state"); + } +#endif // DEBUG + + if (MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + nsAutoCString leafName(":memory"); + if (mDatabaseFile) (void)mDatabaseFile->GetNativeLeafName(leafName); + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Closing connection to '%s'", leafName.get())); + } + + // At this stage, we may still have statements that need to be + // finalized. Attempt to close the database connection. This will + // always disconnect any virtual tables and cleanly finalize their + // internal statements. Once this is done, closing may fail due to + // unfinalized client statements, in which case we need to finalize + // these statements and close again. + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + mConnectionClosed = true; + } + + // Nothing else needs to be done if we don't have a connection here. + if (!aNativeConnection) return NS_OK; + + int srv = ::sqlite3_close(aNativeConnection); + + if (srv == SQLITE_BUSY) { + { + // Nothing else should change the connection or statements status until we + // are done here. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + // We still have non-finalized statements. Finalize them. + sqlite3_stmt* stmt = nullptr; + while ((stmt = ::sqlite3_next_stmt(aNativeConnection, stmt))) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Auto-finalizing SQL statement '%s' (%p)", ::sqlite3_sql(stmt), + stmt)); + +#ifdef DEBUG + SmprintfPointer msg = ::mozilla::Smprintf( + "SQL statement '%s' (%p) should have been finalized before closing " + "the connection", + ::sqlite3_sql(stmt), stmt); + NS_WARNING(msg.get()); +#endif // DEBUG + + srv = ::sqlite3_finalize(stmt); + +#ifdef DEBUG + if (srv != SQLITE_OK) { + SmprintfPointer msg = ::mozilla::Smprintf( + "Could not finalize SQL statement (%p)", stmt); + NS_WARNING(msg.get()); + } +#endif // DEBUG + + // Ensure that the loop continues properly, whether closing has + // succeeded or not. + if (srv == SQLITE_OK) { + stmt = nullptr; + } + } + // Scope exiting will unlock the mutex before we invoke sqlite3_close() + // again, since Sqlite will try to acquire it. + } + + // Now that all statements have been finalized, we + // should be able to close. + srv = ::sqlite3_close(aNativeConnection); + MOZ_ASSERT(false, + "Had to forcibly close the database connection because not all " + "the statements have been finalized."); + } + + if (srv == SQLITE_OK) { + sharedDBMutex.destroy(); + } else { + MOZ_ASSERT(false, + "sqlite3_close failed. There are probably outstanding " + "statements that are listed above!"); + } + + return convertResultCode(srv); +} + +nsCString Connection::getFilename() { return mTelemetryFilename; } + +int Connection::stepStatement(sqlite3* aNativeConnection, + sqlite3_stmt* aStatement) { + MOZ_ASSERT(aStatement); + + AUTO_PROFILER_LABEL_DYNAMIC_CSTR("Connection::stepStatement", OTHER, + ::sqlite3_sql(aStatement)); + + bool checkedMainThread = false; + TimeStamp startTime = TimeStamp::Now(); + + // The connection may have been closed if the executing statement has been + // created and cached after a call to asyncClose() but before the actual + // sqlite3_close(). This usually happens when other tasks using cached + // statements are asynchronously scheduled for execution and any of them ends + // up after asyncClose. See bug 728653 for details. + if (!isConnectionReadyOnThisThread()) return SQLITE_MISUSE; + + (void)::sqlite3_extended_result_codes(aNativeConnection, 1); + + int srv; + while ((srv = ::sqlite3_step(aStatement)) == SQLITE_LOCKED_SHAREDCACHE) { + if (!checkedMainThread) { + checkedMainThread = true; + if (::NS_IsMainThread()) { + NS_WARNING("We won't allow blocking on the main thread!"); + break; + } + } + + srv = WaitForUnlockNotify(aNativeConnection); + if (srv != SQLITE_OK) { + break; + } + + ::sqlite3_reset(aStatement); + } + + // Report very slow SQL statements to Telemetry + TimeDuration duration = TimeStamp::Now() - startTime; + const uint32_t threshold = NS_IsMainThread() + ? Telemetry::kSlowSQLThresholdForMainThread + : Telemetry::kSlowSQLThresholdForHelperThreads; + if (duration.ToMilliseconds() >= threshold) { + nsDependentCString statementString(::sqlite3_sql(aStatement)); + Telemetry::RecordSlowSQLStatement( + statementString, mTelemetryFilename, + static_cast(duration.ToMilliseconds())); + } + + (void)::sqlite3_extended_result_codes(aNativeConnection, 0); + // Drop off the extended result bits of the result code. + return srv & 0xFF; +} + +int Connection::prepareStatement(sqlite3* aNativeConnection, + const nsCString& aSQL, sqlite3_stmt** _stmt) { + // We should not even try to prepare statements after the connection has + // been closed. + if (!isConnectionReadyOnThisThread()) return SQLITE_MISUSE; + + bool checkedMainThread = false; + + (void)::sqlite3_extended_result_codes(aNativeConnection, 1); + + int srv; + while ((srv = ::sqlite3_prepare_v2(aNativeConnection, aSQL.get(), -1, _stmt, + nullptr)) == SQLITE_LOCKED_SHAREDCACHE) { + if (!checkedMainThread) { + checkedMainThread = true; + if (::NS_IsMainThread()) { + NS_WARNING("We won't allow blocking on the main thread!"); + break; + } + } + + srv = WaitForUnlockNotify(aNativeConnection); + if (srv != SQLITE_OK) { + break; + } + } + + if (srv != SQLITE_OK) { + nsCString warnMsg; + warnMsg.AppendLiteral("The SQL statement '"); + warnMsg.Append(aSQL); + warnMsg.AppendLiteral("' could not be compiled due to an error: "); + warnMsg.Append(::sqlite3_errmsg(aNativeConnection)); + +#ifdef DEBUG + NS_WARNING(warnMsg.get()); +#endif + MOZ_LOG(gStorageLog, LogLevel::Error, ("%s", warnMsg.get())); + } + + (void)::sqlite3_extended_result_codes(aNativeConnection, 0); + // Drop off the extended result bits of the result code. + int rc = srv & 0xFF; + // sqlite will return OK on a comment only string and set _stmt to nullptr. + // The callers of this function are used to only checking the return value, + // so it is safer to return an error code. + if (rc == SQLITE_OK && *_stmt == nullptr) { + return SQLITE_MISUSE; + } + + return rc; +} + +int Connection::executeSql(sqlite3* aNativeConnection, const char* aSqlString) { + if (!isConnectionReadyOnThisThread()) return SQLITE_MISUSE; + + AUTO_PROFILER_LABEL_DYNAMIC_CSTR("Connection::executeSql", OTHER, aSqlString); + + TimeStamp startTime = TimeStamp::Now(); + int srv = + ::sqlite3_exec(aNativeConnection, aSqlString, nullptr, nullptr, nullptr); + RecordQueryStatus(srv); + + // Report very slow SQL statements to Telemetry + TimeDuration duration = TimeStamp::Now() - startTime; + const uint32_t threshold = NS_IsMainThread() + ? Telemetry::kSlowSQLThresholdForMainThread + : Telemetry::kSlowSQLThresholdForHelperThreads; + if (duration.ToMilliseconds() >= threshold) { + nsDependentCString statementString(aSqlString); + Telemetry::RecordSlowSQLStatement( + statementString, mTelemetryFilename, + static_cast(duration.ToMilliseconds())); + } + + return srv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIInterfaceRequestor + +NS_IMETHODIMP +Connection::GetInterface(const nsIID& aIID, void** _result) { + if (aIID.Equals(NS_GET_IID(nsIEventTarget))) { + nsIEventTarget* background = getAsyncExecutionTarget(); + NS_IF_ADDREF(background); + *_result = background; + return NS_OK; + } + return NS_ERROR_NO_INTERFACE; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageConnection + +NS_IMETHODIMP +Connection::Close() { + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + return synchronousClose(); +} + +nsresult Connection::synchronousClose() { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + +#ifdef DEBUG + // Since we're accessing mAsyncExecutionThread, we need to be on the opener + // event target. We make this check outside of debug code below in + // setClosedState, but this is here to be explicit. + MOZ_ASSERT(IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); +#endif // DEBUG + + // Make sure we have not executed any asynchronous statements. + // If this fails, the mDBConn may be left open, resulting in a leak. + // We'll try to finalize the pending statements and close the connection. + if (isAsyncExecutionThreadAvailable()) { +#ifdef DEBUG + if (NS_IsMainThread()) { + nsCOMPtr xpc = nsIXPConnect::XPConnect(); + Unused << xpc->DebugDumpJSStack(false, false, false); + } +#endif + MOZ_ASSERT(false, + "Close() was invoked on a connection that executed asynchronous " + "statements. " + "Should have used asyncClose()."); + // Try to close the database regardless, to free up resources. + Unused << SpinningSynchronousClose(); + return NS_ERROR_UNEXPECTED; + } + + // setClosedState nullifies our connection pointer, so we take a raw pointer + // off it, to pass it through the close procedure. + sqlite3* nativeConn = mDBConn; + nsresult rv = setClosedState(); + NS_ENSURE_SUCCESS(rv, rv); + + return internalClose(nativeConn); +} + +NS_IMETHODIMP +Connection::SpinningSynchronousClose() { + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + if (!IsOnCurrentSerialEventTarget(eventTargetOpenedOn)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + // As currently implemented, we can't spin to wait for an existing AsyncClose. + // Our only existing caller will never have called close; assert if misused + // so that no new callers assume this works after an AsyncClose. + MOZ_DIAGNOSTIC_ASSERT(connectionReady()); + if (!connectionReady()) { + return NS_ERROR_UNEXPECTED; + } + + RefPtr listener = new CloseListener(); + rv = AsyncClose(listener); + NS_ENSURE_SUCCESS(rv, rv); + MOZ_ALWAYS_TRUE( + SpinEventLoopUntil("storage::Connection::SpinningSynchronousClose"_ns, + [&]() { return listener->mClosed; })); + MOZ_ASSERT(isClosed(), "The connection should be closed at this point"); + + return rv; +} + +NS_IMETHODIMP +Connection::AsyncClose(mozIStorageCompletionCallback* aCallback) { + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + // Check if AsyncClose or Close were already invoked. + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // The two relevant factors at this point are whether we have a database + // connection and whether we have an async execution thread. Here's what the + // states mean and how we handle them: + // + // - (mDBConn && asyncThread): The expected case where we are either an + // async connection or a sync connection that has been used asynchronously. + // Either way the caller must call us and not Close(). Nothing surprising + // about this. We'll dispatch AsyncCloseConnection to the already-existing + // async thread. + // + // - (mDBConn && !asyncThread): A somewhat unusual case where the caller + // opened the connection synchronously and was planning to use it + // asynchronously, but never got around to using it asynchronously before + // needing to shutdown. This has been observed to happen for the cookie + // service in a case where Firefox shuts itself down almost immediately + // after startup (for unknown reasons). In the Firefox shutdown case, + // we may also fail to create a new async execution thread if one does not + // already exist. (nsThreadManager will refuse to create new threads when + // it has already been told to shutdown.) As such, we need to handle a + // failure to create the async execution thread by falling back to + // synchronous Close() and also dispatching the completion callback because + // at least Places likes to spin a nested event loop that depends on the + // callback being invoked. + // + // Note that we have considered not trying to spin up the async execution + // thread in this case if it does not already exist, but the overhead of + // thread startup (if successful) is significantly less expensive than the + // worst-case potential I/O hit of synchronously closing a database when we + // could close it asynchronously. + // + // - (!mDBConn && asyncThread): This happens in some but not all cases where + // OpenAsyncDatabase encountered a problem opening the database. If it + // happened in all cases AsyncInitDatabase would just shut down the thread + // directly and we would avoid this case. But it doesn't, so for simplicity + // and consistency AsyncCloseConnection knows how to handle this and we + // act like this was the (mDBConn && asyncThread) case in this method. + // + // - (!mDBConn && !asyncThread): The database was never successfully opened or + // Close() or AsyncClose() has already been called (at least) once. This is + // undeniably a misuse case by the caller. We could optimize for this + // case by adding an additional check of mAsyncExecutionThread without using + // getAsyncExecutionTarget() to avoid wastefully creating a thread just to + // shut it down. But this complicates the method for broken caller code + // whereas we're still correct and safe without the special-case. + nsIEventTarget* asyncThread = getAsyncExecutionTarget(); + + // Create our callback event if we were given a callback. This will + // eventually be dispatched in all cases, even if we fall back to Close() and + // the database wasn't open and we return an error. The rationale is that + // no existing consumer checks our return value and several of them like to + // spin nested event loops until the callback fires. Given that, it seems + // preferable for us to dispatch the callback in all cases. (Except the + // wrong thread misuse case we bailed on up above. But that's okay because + // that is statically wrong whereas these edge cases are dynamic.) + nsCOMPtr completeEvent; + if (aCallback) { + completeEvent = newCompletionEvent(aCallback); + } + + if (!asyncThread) { + // We were unable to create an async thread, so we need to fall back to + // using normal Close(). Since there is no async thread, Close() will + // not complain about that. (Close() may, however, complain if the + // connection is closed, but that's okay.) + if (completeEvent) { + // Closing the database is more important than returning an error code + // about a failure to dispatch, especially because all existing native + // callers ignore our return value. + Unused << NS_DispatchToMainThread(completeEvent.forget()); + } + MOZ_ALWAYS_SUCCEEDS(synchronousClose()); + // Return a success inconditionally here, since Close() is unlikely to fail + // and we want to reassure the consumer that its callback will be invoked. + return NS_OK; + } + + // If we're closing the connection during shutdown, and there is an + // interruptible statement running on the helper thread, issue a + // sqlite3_interrupt() to avoid crashing when that statement takes a long + // time (for example a vacuum). + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed) && + mInterruptible && mIsStatementOnHelperThreadInterruptible) { + MOZ_ASSERT(!isClosing(), "Must not be closing, see Interrupt()"); + DebugOnly rv2 = Interrupt(); + MOZ_ASSERT(NS_SUCCEEDED(rv2)); + } + + // setClosedState nullifies our connection pointer, so we take a raw pointer + // off it, to pass it through the close procedure. + sqlite3* nativeConn = mDBConn; + rv = setClosedState(); + NS_ENSURE_SUCCESS(rv, rv); + + // Create and dispatch our close event to the background thread. + nsCOMPtr closeEvent = + new AsyncCloseConnection(this, nativeConn, completeEvent); + rv = asyncThread->Dispatch(closeEvent, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::AsyncClone(bool aReadOnly, + mozIStorageCompletionCallback* aCallback) { + AUTO_PROFILER_LABEL("Connection::AsyncClone", OTHER); + + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + if (!mDatabaseFile) return NS_ERROR_UNEXPECTED; + + int flags = mFlags; + if (aReadOnly) { + // Turn off SQLITE_OPEN_READWRITE, and set SQLITE_OPEN_READONLY. + flags = (~SQLITE_OPEN_READWRITE & flags) | SQLITE_OPEN_READONLY; + // Turn off SQLITE_OPEN_CREATE. + flags = (~SQLITE_OPEN_CREATE & flags); + } + + // The cloned connection will still implement the synchronous API, but throw + // if any synchronous methods are called on the main thread. + RefPtr clone = + new Connection(mStorageService, flags, ASYNCHRONOUS, mTelemetryFilename); + + RefPtr initEvent = + new AsyncInitializeClone(this, clone, aReadOnly, aCallback); + // Dispatch to our async thread, since the originating connection must remain + // valid and open for the whole cloning process. This also ensures we are + // properly serialized with a `close` operation, rather than race with it. + nsCOMPtr target = getAsyncExecutionTarget(); + if (!target) { + return NS_ERROR_UNEXPECTED; + } + return target->Dispatch(initEvent, NS_DISPATCH_NORMAL); +} + +nsresult Connection::initializeClone(Connection* aClone, bool aReadOnly) { + nsresult rv; + if (!mStorageKey.IsEmpty()) { + rv = aClone->initialize(mStorageKey, mName); + } else if (mFileURL) { + rv = aClone->initialize(mFileURL); + } else { + rv = aClone->initialize(mDatabaseFile); + } + if (NS_FAILED(rv)) { + return rv; + } + + auto guard = MakeScopeExit([&]() { aClone->initializeFailed(); }); + + rv = aClone->SetDefaultTransactionType(mDefaultTransactionType); + NS_ENSURE_SUCCESS(rv, rv); + + // Re-attach on-disk databases that were attached to the original connection. + { + nsCOMPtr stmt; + rv = CreateStatement("PRAGMA database_list"_ns, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasResult = false; + while (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString name; + rv = stmt->GetUTF8String(1, name); + if (NS_SUCCEEDED(rv) && !name.EqualsLiteral("main") && + !name.EqualsLiteral("temp")) { + nsCString path; + rv = stmt->GetUTF8String(2, path); + if (NS_SUCCEEDED(rv) && !path.IsEmpty()) { + nsCOMPtr attachStmt; + rv = aClone->CreateStatement("ATTACH DATABASE :path AS "_ns + name, + getter_AddRefs(attachStmt)); + NS_ENSURE_SUCCESS(rv, rv); + rv = attachStmt->BindUTF8StringByName("path"_ns, path); + NS_ENSURE_SUCCESS(rv, rv); + rv = attachStmt->Execute(); + NS_ENSURE_SUCCESS(rv, rv); + } + } + } + } + + // Copy over pragmas from the original connection. + // LIMITATION WARNING! Many of these pragmas are actually scoped to the + // schema ("main" and any other attached databases), and this implmentation + // fails to propagate them. This is being addressed on trunk. + static const char* pragmas[] = { + "cache_size", "temp_store", "foreign_keys", "journal_size_limit", + "synchronous", "wal_autocheckpoint", "busy_timeout"}; + for (auto& pragma : pragmas) { + // Read-only connections just need cache_size and temp_store pragmas. + if (aReadOnly && ::strcmp(pragma, "cache_size") != 0 && + ::strcmp(pragma, "temp_store") != 0) { + continue; + } + + nsAutoCString pragmaQuery("PRAGMA "); + pragmaQuery.Append(pragma); + nsCOMPtr stmt; + rv = CreateStatement(pragmaQuery, getter_AddRefs(stmt)); + NS_ENSURE_SUCCESS(rv, rv); + bool hasResult = false; + if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + pragmaQuery.AppendLiteral(" = "); + pragmaQuery.AppendInt(stmt->AsInt32(0)); + rv = aClone->ExecuteSimpleSQL(pragmaQuery); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + // Copy over temporary tables, triggers, and views from the original + // connections. Entities in `sqlite_temp_master` are only visible to the + // connection that created them. + if (!aReadOnly) { + rv = aClone->ExecuteSimpleSQL("BEGIN TRANSACTION"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr stmt; + rv = CreateStatement(nsLiteralCString("SELECT sql FROM sqlite_temp_master " + "WHERE type IN ('table', 'view', " + "'index', 'trigger')"), + getter_AddRefs(stmt)); + // Propagate errors, because failing to copy triggers might cause schema + // coherency issues when writing to the database from the cloned connection. + NS_ENSURE_SUCCESS(rv, rv); + bool hasResult = false; + while (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + nsAutoCString query; + rv = stmt->GetUTF8String(0, query); + NS_ENSURE_SUCCESS(rv, rv); + + // The `CREATE` SQL statements in `sqlite_temp_master` omit the `TEMP` + // keyword. We need to add it back, or we'll recreate temporary entities + // as persistent ones. `sqlite_temp_master` also holds `CREATE INDEX` + // statements, but those don't need `TEMP` keywords. + if (StringBeginsWith(query, "CREATE TABLE "_ns) || + StringBeginsWith(query, "CREATE TRIGGER "_ns) || + StringBeginsWith(query, "CREATE VIEW "_ns)) { + query.Replace(0, 6, "CREATE TEMP"); + } + + rv = aClone->ExecuteSimpleSQL(query); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = aClone->ExecuteSimpleSQL("COMMIT"_ns); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Copy any functions that have been added to this connection. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + for (const auto& entry : mFunctions) { + const nsACString& key = entry.GetKey(); + Connection::FunctionInfo data = entry.GetData(); + + rv = aClone->CreateFunction(key, data.numArgs, data.function); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to copy function to cloned connection"); + } + } + + // Load SQLite extensions that were on this connection. + // Copy into an array rather than holding the mutex while we load extensions. + nsTArray loadedExtensions; + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + AppendToArray(loadedExtensions, mLoadedExtensions); + } + for (const auto& extension : loadedExtensions) { + (void)aClone->LoadExtension(extension, nullptr); + } + + guard.release(); + return NS_OK; +} + +NS_IMETHODIMP +Connection::Clone(bool aReadOnly, mozIStorageConnection** _connection) { + MOZ_ASSERT(IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + + AUTO_PROFILER_LABEL("Connection::Clone", OTHER); + + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + int flags = mFlags; + if (aReadOnly) { + // Turn off SQLITE_OPEN_READWRITE, and set SQLITE_OPEN_READONLY. + flags = (~SQLITE_OPEN_READWRITE & flags) | SQLITE_OPEN_READONLY; + // Turn off SQLITE_OPEN_CREATE. + flags = (~SQLITE_OPEN_CREATE & flags); + } + + RefPtr clone = + new Connection(mStorageService, flags, mSupportedOperations, + mTelemetryFilename, mInterruptible); + + rv = initializeClone(clone, aReadOnly); + if (NS_FAILED(rv)) { + return rv; + } + + NS_IF_ADDREF(*_connection = clone); + return NS_OK; +} + +NS_IMETHODIMP +Connection::Interrupt() { + MOZ_ASSERT(mInterruptible, "Interrupt method not allowed"); + MOZ_ASSERT_IF(SYNCHRONOUS == mSupportedOperations, + !IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + MOZ_ASSERT_IF(ASYNCHRONOUS == mSupportedOperations, + IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + + if (isClosing()) { // Closing already in asynchronous case + return NS_OK; + } + + { + // As stated on https://www.sqlite.org/c3ref/interrupt.html, + // it is not safe to call sqlite3_interrupt() when + // database connection is closed or might close before + // sqlite3_interrupt() returns. + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + if (!isClosed(lockedScope)) { + MOZ_ASSERT(mDBConn); + ::sqlite3_interrupt(mDBConn); + } + } + + return NS_OK; +} + +NS_IMETHODIMP +Connection::AsyncVacuum(mozIStorageCompletionCallback* aCallback, + bool aUseIncremental, int32_t aSetPageSize) { + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + // Abort if we're shutting down. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return NS_ERROR_ABORT; + } + // Check if AsyncClose or Close were already invoked. + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + nsIEventTarget* asyncThread = getAsyncExecutionTarget(); + if (!asyncThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Create and dispatch our vacuum event to the background thread. + nsCOMPtr vacuumEvent = + new AsyncVacuumEvent(this, aCallback, aUseIncremental, aSetPageSize); + rv = asyncThread->Dispatch(vacuumEvent, NS_DISPATCH_NORMAL); + NS_ENSURE_SUCCESS(rv, rv); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDefaultPageSize(int32_t* _defaultPageSize) { + *_defaultPageSize = Service::kDefaultPageSize; + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetConnectionReady(bool* _ready) { + MOZ_ASSERT(IsOnCurrentSerialEventTarget(eventTargetOpenedOn)); + *_ready = connectionReady(); + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDatabaseFile(nsIFile** _dbFile) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + NS_IF_ADDREF(*_dbFile = mDatabaseFile); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastInsertRowID(int64_t* _id) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + sqlite_int64 id = ::sqlite3_last_insert_rowid(mDBConn); + *_id = id; + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetAffectedRows(int32_t* _rows) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + *_rows = ::sqlite3_changes(mDBConn); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastError(int32_t* _error) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + *_error = ::sqlite3_errcode(mDBConn); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetLastErrorString(nsACString& _errorString) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + const char* serr = ::sqlite3_errmsg(mDBConn); + _errorString.Assign(serr); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetSchemaVersion(int32_t* _version) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr stmt; + (void)CreateStatement("PRAGMA user_version"_ns, getter_AddRefs(stmt)); + NS_ENSURE_TRUE(stmt, NS_ERROR_OUT_OF_MEMORY); + + *_version = 0; + bool hasResult; + if (NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + *_version = stmt->AsInt32(0); + } + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetSchemaVersion(int32_t aVersion) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString stmt("PRAGMA user_version = "_ns); + stmt.AppendInt(aVersion); + + return ExecuteSimpleSQL(stmt); +} + +NS_IMETHODIMP +Connection::CreateStatement(const nsACString& aSQLStatement, + mozIStorageStatement** _stmt) { + NS_ENSURE_ARG_POINTER(_stmt); + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr statement(new Statement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + rv = statement->initialize(this, mDBConn, aSQLStatement); + NS_ENSURE_SUCCESS(rv, rv); + + Statement* rawPtr; + statement.forget(&rawPtr); + *_stmt = rawPtr; + return NS_OK; +} + +NS_IMETHODIMP +Connection::CreateAsyncStatement(const nsACString& aSQLStatement, + mozIStorageAsyncStatement** _stmt) { + NS_ENSURE_ARG_POINTER(_stmt); + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + RefPtr statement(new AsyncStatement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + rv = statement->initialize(this, mDBConn, aSQLStatement); + NS_ENSURE_SUCCESS(rv, rv); + + AsyncStatement* rawPtr; + statement.forget(&rawPtr); + *_stmt = rawPtr; + return NS_OK; +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQL(const nsACString& aSQLStatement) { + CHECK_MAINTHREAD_ABUSE(); + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + int srv = executeSql(mDBConn, PromiseFlatCString(aSQLStatement).get()); + return convertResultCode(srv); +} + +NS_IMETHODIMP +Connection::ExecuteAsync( + const nsTArray>& aStatements, + mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _handle) { + nsTArray stmts(aStatements.Length()); + for (uint32_t i = 0; i < aStatements.Length(); i++) { + nsCOMPtr stmt = + do_QueryInterface(aStatements[i]); + NS_ENSURE_STATE(stmt); + + // Obtain our StatementData. + StatementData data; + nsresult rv = stmt->getAsynchronousStatementData(data); + NS_ENSURE_SUCCESS(rv, rv); + + NS_ASSERTION(stmt->getOwner() == this, + "Statement must be from this database connection!"); + + // Now append it to our array. + stmts.AppendElement(data); + } + + // Dispatch to the background + return AsyncExecuteStatements::execute(std::move(stmts), this, mDBConn, + aCallback, _handle); +} + +NS_IMETHODIMP +Connection::ExecuteSimpleSQLAsync(const nsACString& aSQLStatement, + mozIStorageStatementCallback* aCallback, + mozIStoragePendingStatement** _handle) { + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + + nsCOMPtr stmt; + nsresult rv = CreateAsyncStatement(aSQLStatement, getter_AddRefs(stmt)); + if (NS_FAILED(rv)) { + return rv; + } + + nsCOMPtr pendingStatement; + rv = stmt->ExecuteAsync(aCallback, getter_AddRefs(pendingStatement)); + if (NS_FAILED(rv)) { + return rv; + } + + pendingStatement.forget(_handle); + return rv; +} + +NS_IMETHODIMP +Connection::TableExists(const nsACString& aTableName, bool* _exists) { + return databaseElementExists(TABLE, aTableName, _exists); +} + +NS_IMETHODIMP +Connection::IndexExists(const nsACString& aIndexName, bool* _exists) { + return databaseElementExists(INDEX, aIndexName, _exists); +} + +NS_IMETHODIMP +Connection::GetTransactionInProgress(bool* _inProgress) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + *_inProgress = transactionInProgress(lockedScope); + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetDefaultTransactionType(int32_t* _type) { + *_type = mDefaultTransactionType; + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetDefaultTransactionType(int32_t aType) { + NS_ENSURE_ARG_RANGE(aType, TRANSACTION_DEFERRED, TRANSACTION_EXCLUSIVE); + mDefaultTransactionType = aType; + return NS_OK; +} + +NS_IMETHODIMP +Connection::GetVariableLimit(int32_t* _limit) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + int limit = ::sqlite3_limit(mDBConn, SQLITE_LIMIT_VARIABLE_NUMBER, -1); + if (limit < 0) { + return NS_ERROR_UNEXPECTED; + } + *_limit = limit; + return NS_OK; +} + +NS_IMETHODIMP +Connection::BeginTransaction() { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + return beginTransactionInternal(lockedScope, mDBConn, + mDefaultTransactionType); +} + +nsresult Connection::beginTransactionInternal( + const SQLiteMutexAutoLock& aProofOfLock, sqlite3* aNativeConnection, + int32_t aTransactionType) { + if (transactionInProgress(aProofOfLock)) { + return NS_ERROR_FAILURE; + } + nsresult rv; + switch (aTransactionType) { + case TRANSACTION_DEFERRED: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN DEFERRED")); + break; + case TRANSACTION_IMMEDIATE: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN IMMEDIATE")); + break; + case TRANSACTION_EXCLUSIVE: + rv = convertResultCode(executeSql(aNativeConnection, "BEGIN EXCLUSIVE")); + break; + default: + return NS_ERROR_ILLEGAL_VALUE; + } + return rv; +} + +NS_IMETHODIMP +Connection::CommitTransaction() { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + return commitTransactionInternal(lockedScope, mDBConn); +} + +nsresult Connection::commitTransactionInternal( + const SQLiteMutexAutoLock& aProofOfLock, sqlite3* aNativeConnection) { + if (!transactionInProgress(aProofOfLock)) { + return NS_ERROR_UNEXPECTED; + } + nsresult rv = + convertResultCode(executeSql(aNativeConnection, "COMMIT TRANSACTION")); + return rv; +} + +NS_IMETHODIMP +Connection::RollbackTransaction() { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + return rollbackTransactionInternal(lockedScope, mDBConn); +} + +nsresult Connection::rollbackTransactionInternal( + const SQLiteMutexAutoLock& aProofOfLock, sqlite3* aNativeConnection) { + if (!transactionInProgress(aProofOfLock)) { + return NS_ERROR_UNEXPECTED; + } + + nsresult rv = + convertResultCode(executeSql(aNativeConnection, "ROLLBACK TRANSACTION")); + return rv; +} + +NS_IMETHODIMP +Connection::CreateTable(const char* aTableName, const char* aTableSchema) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SmprintfPointer buf = + ::mozilla::Smprintf("CREATE TABLE %s (%s)", aTableName, aTableSchema); + if (!buf) return NS_ERROR_OUT_OF_MEMORY; + + int srv = executeSql(mDBConn, buf.get()); + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Connection::CreateFunction(const nsACString& aFunctionName, + int32_t aNumArguments, + mozIStorageFunction* aFunction) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // Check to see if this function is already defined. We only check the name + // because a function can be defined with the same body but different names. + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_ENSURE_FALSE(mFunctions.Contains(aFunctionName), NS_ERROR_FAILURE); + + int srv = ::sqlite3_create_function( + mDBConn, nsPromiseFlatCString(aFunctionName).get(), aNumArguments, + SQLITE_ANY, aFunction, basicFunctionHelper, nullptr, nullptr); + if (srv != SQLITE_OK) return convertResultCode(srv); + + FunctionInfo info = {aFunction, aNumArguments}; + mFunctions.InsertOrUpdate(aFunctionName, info); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::RemoveFunction(const nsACString& aFunctionName) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_ENSURE_TRUE(mFunctions.Get(aFunctionName, nullptr), NS_ERROR_FAILURE); + + int srv = ::sqlite3_create_function( + mDBConn, nsPromiseFlatCString(aFunctionName).get(), 0, SQLITE_ANY, + nullptr, nullptr, nullptr, nullptr); + if (srv != SQLITE_OK) return convertResultCode(srv); + + mFunctions.Remove(aFunctionName); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetProgressHandler(int32_t aGranularity, + mozIStorageProgressHandler* aHandler, + mozIStorageProgressHandler** _oldHandler) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // Return previous one + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_IF_ADDREF(*_oldHandler = mProgressHandler); + + if (!aHandler || aGranularity <= 0) { + aHandler = nullptr; + aGranularity = 0; + } + mProgressHandler = aHandler; + ::sqlite3_progress_handler(mDBConn, aGranularity, sProgressHelper, this); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::RemoveProgressHandler(mozIStorageProgressHandler** _oldHandler) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // Return previous one + SQLiteMutexAutoLock lockedScope(sharedDBMutex); + NS_IF_ADDREF(*_oldHandler = mProgressHandler); + + mProgressHandler = nullptr; + ::sqlite3_progress_handler(mDBConn, 0, nullptr, nullptr); + + return NS_OK; +} + +NS_IMETHODIMP +Connection::SetGrowthIncrement(int32_t aChunkSize, + const nsACString& aDatabaseName) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + // Bug 597215: Disk space is extremely limited on Android + // so don't preallocate space. This is also not effective + // on log structured file systems used by Android devices +#if !defined(ANDROID) && !defined(MOZ_PLATFORM_MAEMO) + // Don't preallocate if less than 500MiB is available. + int64_t bytesAvailable; + rv = mDatabaseFile->GetDiskSpaceAvailable(&bytesAvailable); + NS_ENSURE_SUCCESS(rv, rv); + if (bytesAvailable < MIN_AVAILABLE_BYTES_PER_CHUNKED_GROWTH) { + return NS_ERROR_FILE_TOO_BIG; + } + + int srv = ::sqlite3_file_control( + mDBConn, + aDatabaseName.Length() ? nsPromiseFlatCString(aDatabaseName).get() + : nullptr, + SQLITE_FCNTL_CHUNK_SIZE, &aChunkSize); + if (srv == SQLITE_OK) { + mGrowthChunkSize = aChunkSize; + } +#endif + return NS_OK; +} + +int32_t Connection::RemovablePagesInFreeList(const nsACString& aSchemaName) { + int32_t freeListPagesCount = 0; + if (!isConnectionReadyOnThisThread()) { + MOZ_ASSERT(false, "Database connection is not ready"); + return freeListPagesCount; + } + { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".freelist_count"); + nsCOMPtr stmt; + DebugOnly rv = CreateStatement(query, getter_AddRefs(stmt)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + bool hasResult = false; + if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + freeListPagesCount = stmt->AsInt32(0); + } + } + // If there's no chunk size set, any page is good to be removed. + if (mGrowthChunkSize == 0 || freeListPagesCount == 0) { + return freeListPagesCount; + } + int32_t pageSize; + { + nsAutoCString query(MOZ_STORAGE_UNIQUIFY_QUERY_STR "PRAGMA "); + query.Append(aSchemaName); + query.AppendLiteral(".page_size"); + nsCOMPtr stmt; + DebugOnly rv = CreateStatement(query, getter_AddRefs(stmt)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + bool hasResult = false; + if (stmt && NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && hasResult) { + pageSize = stmt->AsInt32(0); + } else { + MOZ_ASSERT(false, "Couldn't get page_size"); + return 0; + } + } + return std::max(0, freeListPagesCount - (mGrowthChunkSize / pageSize)); +} + +NS_IMETHODIMP +Connection::LoadExtension(const nsACString& aExtensionName, + mozIStorageCompletionCallback* aCallback) { + AUTO_PROFILER_LABEL("Connection::LoadExtension", OTHER); + + // This is a static list of extensions we can load. + // Please use lowercase ASCII names and keep this list alphabetically ordered. + static constexpr nsLiteralCString sSupportedExtensions[] = { + // clang-format off + "fts5"_ns, + // clang-format on + }; + if (std::find(std::begin(sSupportedExtensions), + std::end(sSupportedExtensions), + aExtensionName) == std::end(sSupportedExtensions)) { + return NS_ERROR_INVALID_ARG; + } + + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + + int srv = ::sqlite3_db_config(mDBConn, SQLITE_DBCONFIG_ENABLE_LOAD_EXTENSION, + 1, nullptr); + if (srv != SQLITE_OK) { + return NS_ERROR_UNEXPECTED; + } + + // Track the loaded extension for later connection cloning operations. + { + MutexAutoLock lockedScope(sharedAsyncExecutionMutex); + if (!mLoadedExtensions.EnsureInserted(aExtensionName)) { + // Already loaded, bail out but issue a warning. + NS_WARNING(nsPrintfCString( + "Tried to register '%s' SQLite extension multiple times!", + PromiseFlatCString(aExtensionName).get()) + .get()); + return NS_OK; + } + } + + nsAutoCString entryPoint("sqlite3_"); + entryPoint.Append(aExtensionName); + entryPoint.AppendLiteral("_init"); + + RefPtr loadTask = NS_NewRunnableFunction( + "mozStorageConnection::LoadExtension", + [this, self = RefPtr(this), entryPoint, + callback = RefPtr(aCallback)]() mutable { + MOZ_ASSERT( + !NS_IsMainThread() || + (operationSupported(Connection::SYNCHRONOUS) && + eventTargetOpenedOn == GetMainThreadSerialEventTarget()), + "Should happen on main-thread only for synchronous connections " + "opened on the main thread"); +#ifdef MOZ_FOLD_LIBS + int srv = ::sqlite3_load_extension(mDBConn, + MOZ_DLL_PREFIX "nss3" MOZ_DLL_SUFFIX, + entryPoint.get(), nullptr); +#else + int srv = ::sqlite3_load_extension( + mDBConn, MOZ_DLL_PREFIX "mozsqlite3" MOZ_DLL_SUFFIX, + entryPoint.get(), nullptr); +#endif + if (!callback) { + return; + }; + RefPtr callbackTask = NS_NewRunnableFunction( + "mozStorageConnection::LoadExtension_callback", + [callback = std::move(callback), srv]() { + (void)callback->Complete(convertResultCode(srv), nullptr); + }); + if (IsOnCurrentSerialEventTarget(eventTargetOpenedOn)) { + MOZ_ALWAYS_SUCCEEDS(callbackTask->Run()); + } else { + // Redispatch the callback to the calling thread. + MOZ_ALWAYS_SUCCEEDS(eventTargetOpenedOn->Dispatch( + callbackTask.forget(), NS_DISPATCH_NORMAL)); + } + }); + + if (NS_IsMainThread() && !operationSupported(Connection::SYNCHRONOUS)) { + // This is a main-thread call to an async-only connection, thus we should + // load the library in the helper thread. + nsIEventTarget* helperThread = getAsyncExecutionTarget(); + if (!helperThread) { + return NS_ERROR_NOT_INITIALIZED; + } + MOZ_ALWAYS_SUCCEEDS( + helperThread->Dispatch(loadTask.forget(), NS_DISPATCH_NORMAL)); + } else { + // In any other case we just load the extension on the current thread. + MOZ_ALWAYS_SUCCEEDS(loadTask->Run()); + } + return NS_OK; +} + +NS_IMETHODIMP +Connection::EnableModule(const nsACString& aModuleName) { + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + for (auto& gModule : gModules) { + struct Module* m = &gModule; + if (aModuleName.Equals(m->name)) { + int srv = m->registerFunc(mDBConn, m->name); + if (srv != SQLITE_OK) return convertResultCode(srv); + + return NS_OK; + } + } + + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +Connection::GetQuotaObjects(QuotaObject** aDatabaseQuotaObject, + QuotaObject** aJournalQuotaObject) { + MOZ_ASSERT(aDatabaseQuotaObject); + MOZ_ASSERT(aJournalQuotaObject); + + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(SYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + + sqlite3_file* file; + int srv = ::sqlite3_file_control(mDBConn, nullptr, SQLITE_FCNTL_FILE_POINTER, + &file); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + sqlite3_vfs* vfs; + srv = + ::sqlite3_file_control(mDBConn, nullptr, SQLITE_FCNTL_VFS_POINTER, &vfs); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + bool obfusactingVFS = false; + + { + const nsDependentCString vfsName{vfs->zName}; + + if (vfsName == obfsvfs::GetVFSName()) { + obfusactingVFS = true; + } else if (vfsName != quotavfs::GetVFSName()) { + NS_WARNING("Got unexpected vfs"); + return NS_ERROR_FAILURE; + } + } + + RefPtr databaseQuotaObject = + GetQuotaObject(file, obfusactingVFS); + if (NS_WARN_IF(!databaseQuotaObject)) { + return NS_ERROR_FAILURE; + } + + srv = ::sqlite3_file_control(mDBConn, nullptr, SQLITE_FCNTL_JOURNAL_POINTER, + &file); + if (srv != SQLITE_OK) { + return convertResultCode(srv); + } + + RefPtr journalQuotaObject = GetQuotaObject(file, obfusactingVFS); + if (NS_WARN_IF(!journalQuotaObject)) { + return NS_ERROR_FAILURE; + } + + databaseQuotaObject.forget(aDatabaseQuotaObject); + journalQuotaObject.forget(aJournalQuotaObject); + return NS_OK; +} + +SQLiteMutex& Connection::GetSharedDBMutex() { return sharedDBMutex; } + +uint32_t Connection::GetTransactionNestingLevel( + const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) { + return mTransactionNestingLevel; +} + +uint32_t Connection::IncreaseTransactionNestingLevel( + const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) { + return ++mTransactionNestingLevel; +} + +uint32_t Connection::DecreaseTransactionNestingLevel( + const mozilla::storage::SQLiteMutexAutoLock& aProofOfLock) { + return --mTransactionNestingLevel; +} + +NS_IMETHODIMP +Connection::BackupToFileAsync(nsIFile* aDestinationFile, + mozIStorageCompletionCallback* aCallback) { + NS_ENSURE_ARG(aDestinationFile); + NS_ENSURE_ARG(aCallback); + NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD); + + // Abort if we're shutting down. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { + return NS_ERROR_ABORT; + } + // Check if AsyncClose or Close were already invoked. + if (!connectionReady()) { + return NS_ERROR_NOT_INITIALIZED; + } + nsresult rv = ensureOperationSupported(ASYNCHRONOUS); + if (NS_FAILED(rv)) { + return rv; + } + nsIEventTarget* asyncThread = getAsyncExecutionTarget(); + if (!asyncThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Create and dispatch our backup event to the execution thread. + nsCOMPtr backupEvent = + new AsyncBackupDatabaseFile(this, mDBConn, aDestinationFile, aCallback); + rv = asyncThread->Dispatch(backupEvent, NS_DISPATCH_NORMAL); + return rv; +} + +} // namespace mozilla::storage diff --git a/storage/mozStorageConnection.h b/storage/mozStorageConnection.h new file mode 100644 index 0000000000..d059c9e8be --- /dev/null +++ b/storage/mozStorageConnection.h @@ -0,0 +1,569 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_Connection_h +#define mozilla_storage_Connection_h + +#include "nsCOMPtr.h" +#include "mozilla/Atomics.h" +#include "mozilla/Mutex.h" +#include "nsProxyRelease.h" +#include "nsThreadUtils.h" +#include "nsIInterfaceRequestor.h" + +#include "nsTHashMap.h" +#include "nsTHashSet.h" +#include "mozIStorageProgressHandler.h" +#include "SQLiteMutex.h" +#include "mozIStorageConnection.h" +#include "mozStorageService.h" +#include "mozIStorageAsyncConnection.h" +#include "mozIStorageCompletionCallback.h" + +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +class nsIFile; +class nsIFileURL; +class nsIEventTarget; +class nsISerialEventTarget; +class nsIThread; + +namespace mozilla::storage { + +class Connection final : public mozIStorageConnection, + public nsIInterfaceRequestor { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEASYNCCONNECTION + NS_DECL_MOZISTORAGECONNECTION + NS_DECL_NSIINTERFACEREQUESTOR + + /** + * Indicates if a database operation is synchronous or asynchronous. + * + * - Async operations may be called from any thread for all connections. + * - Sync operations may be called from any thread for sync connections, and + * from background threads for async connections. + */ + enum ConnectionOperation { ASYNCHRONOUS, SYNCHRONOUS }; + + /** + * Structure used to describe user functions on the database connection. + */ + struct FunctionInfo { + nsCOMPtr function; + int32_t numArgs; + }; + + /** + * @param aService + * Pointer to the storage service. Held onto for the lifetime of the + * connection. + * @param aFlags + * The flags to pass to sqlite3_open_v2. + * @param aSupportedOperations + * The operation types supported on this connection. All connections + * implement both the async (`mozIStorageAsyncConnection`) and sync + * (`mozIStorageConnection`) interfaces, but async connections may not + * call sync operations from the main thread. + * @param aInterruptible + * If |true|, the pending operations can be interrupted by invokind the + * Interrupt() method. + * If |false|, method Interrupt() must not be used. + * @param aIgnoreLockingMode + * If |true|, ignore locks in force on the file. Only usable with + * read-only connections. Defaults to false. + * Use with extreme caution. If sqlite ignores locks, reads may fail + * indicating database corruption (the database won't actually be + * corrupt) or produce wrong results without any indication that has + * happened. + */ + Connection(Service* aService, int aFlags, + ConnectionOperation aSupportedOperations, + const nsCString& aTelemetryFilename, bool aInterruptible = false, + bool aIgnoreLockingMode = false); + + /** + * Creates the connection to an in-memory database. + */ + nsresult initialize(const nsACString& aStorageKey, const nsACString& aName); + + /** + * Creates the connection to the database. + * + * @param aDatabaseFile + * The nsIFile of the location of the database to open, or create if it + * does not exist. + */ + nsresult initialize(nsIFile* aDatabaseFile); + + /** + * Creates the connection to the database. + * + * @param aFileURL + * The nsIFileURL of the location of the database to open, or create if + * it does not exist. + */ + nsresult initialize(nsIFileURL* aFileURL); + + /** + * Same as initialize, but to be used on the async thread. + */ + nsresult initializeOnAsyncThread(nsIFile* aStorageFile); + + /** + * Fetches runtime status information for this connection. + * + * @param aStatusOption One of the SQLITE_DBSTATUS options defined at + * http://www.sqlite.org/c3ref/c_dbstatus_options.html + * @param [optional] aMaxValue if provided, will be set to the highest + * istantaneous value. + * @return the current value for the specified option. + */ + int32_t getSqliteRuntimeStatus(int32_t aStatusOption, + int32_t* aMaxValue = nullptr); + /** + * Registers/unregisters a commit hook callback. + * + * @param aCallbackFn a callback function to be invoked on transactions + * commit. Pass nullptr to unregister the current callback. + * @param [optional] aData if provided, will be passed to the callback. + * @see http://sqlite.org/c3ref/commit_hook.html + */ + void setCommitHook(int (*aCallbackFn)(void*), void* aData = nullptr) { + MOZ_ASSERT(mDBConn, "A connection must exist at this point"); + ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); + }; + + /** + * Gets autocommit status. + */ + bool getAutocommit() { + return mDBConn && static_cast(::sqlite3_get_autocommit(mDBConn)); + }; + + /** + * Lazily creates and returns a background execution thread. In the future, + * the thread may be re-claimed if left idle, so you should call this + * method just before you dispatch and not save the reference. + * + * This must be called from the opener thread. + * + * @return an event target suitable for asynchronous statement execution. + * @note This method will return null once AsyncClose() has been called. + */ + nsIEventTarget* getAsyncExecutionTarget(); + + /** + * Mutex used by asynchronous statements to protect state. The mutex is + * declared on the connection object because there is no contention between + * asynchronous statements (they are serialized on mAsyncExecutionThread). + * Currently protects: + * - Connection.mAsyncExecutionThreadShuttingDown + * - Connection.mConnectionClosed + * - AsyncExecuteStatements.mCancelRequested + * - Connection.mLoadedExtensions + */ + Mutex sharedAsyncExecutionMutex MOZ_UNANNOTATED; + + /** + * Wraps the mutex that SQLite gives us from sqlite3_db_mutex. This is public + * because we already expose the sqlite3* native connection and proper + * operation of the deadlock detector requires everyone to use the same single + * SQLiteMutex instance for correctness. + */ + SQLiteMutex sharedDBMutex; + + /** + * References the event target this database was opened on. + */ + const nsCOMPtr eventTargetOpenedOn; + + /** + * Closes the SQLite database, and warns about any non-finalized statements. + */ + nsresult internalClose(sqlite3* aNativeconnection); + + /** + * Shuts down the passed-in async thread. + */ + void shutdownAsyncThread(); + + /** + * Obtains the filename of the connection. Useful for logging. + */ + nsCString getFilename(); + + /** + * Creates an sqlite3 prepared statement object from an SQL string. + * + * @param aNativeConnection + * The underlying Sqlite connection to prepare the statement with. + * @param aSQL + * The SQL statement string to compile. + * @param _stmt + * New sqlite3_stmt object. + * @return the result from sqlite3_prepare_v2. + */ + int prepareStatement(sqlite3* aNativeConnection, const nsCString& aSQL, + sqlite3_stmt** _stmt); + + /** + * Performs a sqlite3_step on aStatement, while properly handling + * SQLITE_LOCKED when not on the main thread by waiting until we are notified. + * + * @param aNativeConnection + * The underlying Sqlite connection to step the statement with. + * @param aStatement + * A pointer to a sqlite3_stmt object. + * @return the result from sqlite3_step. + */ + int stepStatement(sqlite3* aNativeConnection, sqlite3_stmt* aStatement); + + /** + * Raw connection transaction management. + * + * @see BeginTransactionAs, CommitTransaction, RollbackTransaction. + */ + nsresult beginTransactionInternal( + const SQLiteMutexAutoLock& aProofOfLock, sqlite3* aNativeConnection, + int32_t aTransactionType = TRANSACTION_DEFERRED); + nsresult commitTransactionInternal(const SQLiteMutexAutoLock& aProofOfLock, + sqlite3* aNativeConnection); + nsresult rollbackTransactionInternal(const SQLiteMutexAutoLock& aProofOfLock, + sqlite3* aNativeConnection); + + /** + * Indicates if this database connection is open. + */ + inline bool connectionReady() { return mDBConn != nullptr; } + + /** + * Indicates if this database connection has an open transaction. Because + * multiple threads can execute statements on the same connection, this method + * requires proof that the caller is holding `sharedDBMutex`. + * + * Per the SQLite docs, `sqlite3_get_autocommit` returns 0 if autocommit mode + * is disabled. `BEGIN` disables autocommit mode, and `COMMIT`, `ROLLBACK`, or + * an automatic rollback re-enables it. + */ + inline bool transactionInProgress(const SQLiteMutexAutoLock& aProofOfLock) { + return !getAutocommit(); + } + + /** + * Indicates if this database connection supports the given operation. + * + * @param aOperationType + * The operation type, sync or async. + * @return `true` if the operation is supported, `false` otherwise. + */ + bool operationSupported(ConnectionOperation aOperationType); + + /** + * Thread-aware version of connectionReady, results per caller's thread are: + * - owner thread: Same as connectionReady(). True means we have a valid, + * un-closed database connection and it's not going away until you invoke + * Close() or AsyncClose(). + * - async thread: Returns true at all times because you can't schedule + * runnables against the async thread after AsyncClose() has been called. + * Therefore, the connection is still around if your code is running. + * - any other thread: Race-prone Lies! If you are main-thread code in + * mozStorageService iterating over the list of connections, you need to + * acquire the sharedAsyncExecutionMutex for the connection, invoke + * connectionReady() while holding it, and then continue to hold it while + * you do whatever you need to do. This is because of off-main-thread + * consumers like dom/cache and IndexedDB and other QuotaManager clients. + */ + bool isConnectionReadyOnThisThread(); + + /** + * True if this connection has inited shutdown. + */ + bool isClosing(); + + /** + * True if the underlying connection is closed. + * Any sqlite resources may be lost when this returns true, so nothing should + * try to use them. + * This locks on sharedAsyncExecutionMutex. + */ + bool isClosed(); + + /** + * Same as isClosed(), but takes a proof-of-lock instead of locking + * internally. + */ + bool isClosed(MutexAutoLock& lock); + + /** + * True if the async execution thread is alive and able to be used (i.e., it + * is not in the process of shutting down.) + * + * This must be called from the opener thread. + */ + bool isAsyncExecutionThreadAvailable(); + + nsresult initializeClone(Connection* aClone, bool aReadOnly); + + /** + * Records a status from a sqlite statement. + * + * @param srv The sqlite result for the failure or SQLITE_OK. + */ + void RecordQueryStatus(int srv); + + /** + * Returns the number of pages in the free list that can be removed. + * + * A database may use chunked growth to reduce filesystem fragmentation, then + * Sqlite will allocate and release multiple pages in chunks. We want to + * preserve the chunked space to reduce the likelihood of fragmentation, + * releasing free pages only when there's a large amount of them. This can be + * used to decide if it's worth vacuuming the database and how many pages can + * be vacuumed in case of incremental vacuum. + * Note this returns 0, and asserts, in case of errors. + */ + int32_t RemovablePagesInFreeList(const nsACString& aSchemaName); + + /** + * Whether the statement currently running on the helper thread can be + * interrupted. + */ + Atomic mIsStatementOnHelperThreadInterruptible; + + private: + ~Connection(); + nsresult initializeInternal(); + void initializeFailed(); + + /** + * Records the status of an attempt to load a sqlite database to telemetry. + * + * @param rv The state of the load, success or failure. + */ + void RecordOpenStatus(nsresult rv); + + /** + * Sets the database into a closed state so no further actions can be + * performed. + * + * @note mDBConn is set to nullptr in this method. + */ + nsresult setClosedState(); + + /** + * Helper for calls to sqlite3_exec. Reports long delays to Telemetry. + * + * @param aNativeConnection + * The underlying Sqlite connection to execute the query with. + * @param aSqlString + * SQL string to execute + * @return the result from sqlite3_exec. + */ + int executeSql(sqlite3* aNativeConnection, const char* aSqlString); + + /** + * Describes a certain primitive type in the database. + * + * Possible Values Are: + * INDEX - To check for the existence of an index + * TABLE - To check for the existence of a table + */ + enum DatabaseElementType { INDEX, TABLE }; + + /** + * Determines if the specified primitive exists. + * + * @param aElementType + * The type of element to check the existence of + * @param aElementName + * The name of the element to check for + * @returns true if element exists, false otherwise + */ + nsresult databaseElementExists(enum DatabaseElementType aElementType, + const nsACString& aElementName, bool* _exists); + + bool findFunctionByInstance(mozIStorageFunction* aInstance); + + static int sProgressHelper(void* aArg); + // Generic progress handler + // Dispatch call to registered progress handler, + // if there is one. Do nothing in other cases. + int progressHandler(); + + /** + * Like `operationSupported`, but throws (and, in a debug build, asserts) if + * the operation is unsupported. + */ + nsresult ensureOperationSupported(ConnectionOperation aOperationType); + + sqlite3* mDBConn; + nsCString mStorageKey; + nsCString mName; + nsCOMPtr mFileURL; + nsCOMPtr mDatabaseFile; + + /** + * Lazily created thread for asynchronous statement execution. Consumers + * should use getAsyncExecutionTarget rather than directly accessing this + * field. + * + * This must be modified only on the opener thread. + */ + nsCOMPtr mAsyncExecutionThread; + + /** + * The filename that will be reported to telemetry for this connection. By + * default this will be the leaf of the path to the database file. + */ + nsCString mTelemetryFilename; + + /** + * Stores the default behavior for all transactions run on this connection. + */ + mozilla::Atomic mDefaultTransactionType; + + /** + * Used to trigger cleanup logic only the first time our refcount hits 1. We + * may trigger a failsafe Close() that invokes SpinningSynchronousClose() + * which invokes AsyncClose() which may bump our refcount back up to 2 (and + * which will then fall back down to 1 again). It's also possible that the + * Service may bump our refcount back above 1 if getConnections() runs before + * we invoke unregisterConnection(). + */ + mozilla::Atomic mDestroying; + + /** + * Stores the mapping of a given function by name to its instance. Access is + * protected by sharedDBMutex. + */ + nsTHashMap mFunctions; + + /** + * Stores the registered progress handler for the database connection. Access + * is protected by sharedDBMutex. + */ + nsCOMPtr mProgressHandler; + + // This is here for two reasons: 1) It's used to make sure that the + // connections do not outlive the service. 2) Our custom collating functions + // call its localeCompareStrings() method. + RefPtr mStorageService; + + nsresult synchronousClose(); + + /** + * Stores the flags we passed to sqlite3_open_v2. + */ + const int mFlags; + + uint32_t mTransactionNestingLevel; + + /** + * Indicates which operations are supported on this connection. + */ + const ConnectionOperation mSupportedOperations; + + /** + * Stores whether this connection is interruptible. + */ + const bool mInterruptible; + + /** + * Stores whether we should ask sqlite3_open_v2 to ignore locking. + */ + const bool mIgnoreLockingMode; + + /** + * Set to true by Close() or AsyncClose() prior to shutdown. + * + * If false, we guarantee both that the underlying sqlite3 database + * connection is still open and that getAsyncExecutionTarget() can + * return a thread. Once true, either the sqlite3 database + * connection is being shutdown or it has been + * shutdown. Additionally, once true, getAsyncExecutionTarget() + * returns null. + * + * This variable should be accessed while holding the + * sharedAsyncExecutionMutex. + */ + bool mAsyncExecutionThreadShuttingDown; + + /** + * Set to true just prior to calling sqlite3_close on the + * connection. + * + * This variable should be accessed while holding the + * sharedAsyncExecutionMutex. + */ + bool mConnectionClosed; + + /** + * Stores the growth increment chunk size, set through SetGrowthIncrement(). + */ + Atomic mGrowthChunkSize; + + /** + * Stores a list of the SQLite extensions loaded for this connections. + * This is used to properly clone the connection. + * @note Hold sharedAsyncExecutionMutex while using this. + */ + nsTHashSet mLoadedExtensions + MOZ_GUARDED_BY(sharedAsyncExecutionMutex); +}; + +/** + * A Runnable designed to call a mozIStorageCompletionCallback on + * the appropriate thread. + */ +class CallbackComplete final : public Runnable { + public: + /** + * @param aValue The result to pass to the callback. It must + * already be owned by the main thread. + * @param aCallback The callback. It must already be owned by the + * main thread. + */ + CallbackComplete(nsresult aStatus, nsISupports* aValue, + already_AddRefed aCallback) + : Runnable("storage::CallbackComplete"), + mStatus(aStatus), + mValue(aValue), + mCallback(aCallback) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(NS_IsMainThread()); + nsresult rv = mCallback->Complete(mStatus, mValue); + + // Ensure that we release on the main thread + mValue = nullptr; + mCallback = nullptr; + return rv; + } + + private: + nsresult mStatus; + nsCOMPtr mValue; + // This is a RefPtr and not a nsCOMPtr because + // nsCOMP would cause an off-main thread QI, which + // is not a good idea (and crashes XPConnect). + RefPtr mCallback; +}; + +} // namespace mozilla::storage + +/** + * Casting Connection to nsISupports is ambiguous. + * This method handles that. + */ +inline nsISupports* ToSupports(mozilla::storage::Connection* p) { + return NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, p); +} + +#endif // mozilla_storage_Connection_h diff --git a/storage/mozStorageError.cpp b/storage/mozStorageError.cpp new file mode 100644 index 0000000000..89beccac38 --- /dev/null +++ b/storage/mozStorageError.cpp @@ -0,0 +1,40 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageError.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Error + +Error::Error(int aResult, const char* aMessage) + : mResult(aResult), mMessage(aMessage) {} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS(Error, mozIStorageError) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageError + +NS_IMETHODIMP +Error::GetResult(int32_t* _result) { + *_result = mResult; + return NS_OK; +} + +NS_IMETHODIMP +Error::GetMessage(nsACString& _message) { + _message = mMessage; + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageError.h b/storage/mozStorageError.h new file mode 100644 index 0000000000..07037fb418 --- /dev/null +++ b/storage/mozStorageError.h @@ -0,0 +1,34 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageError_h +#define mozStorageError_h + +#include "mozIStorageError.h" +#include "nsString.h" +#include "mozilla/Attributes.h" + +namespace mozilla { +namespace storage { + +class Error final : public mozIStorageError { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEERROR + + Error(int aResult, const char* aMessage); + + private: + ~Error() {} + + int mResult; + nsCString mMessage; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageError_h diff --git a/storage/mozStorageHelper.h b/storage/mozStorageHelper.h new file mode 100644 index 0000000000..35ee54faa3 --- /dev/null +++ b/storage/mozStorageHelper.h @@ -0,0 +1,326 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 MOZSTORAGEHELPER_H +#define MOZSTORAGEHELPER_H + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/ScopeExit.h" + +#include "mozilla/storage/SQLiteMutex.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozilla/DebugOnly.h" +#include "nsCOMPtr.h" +#include "nsError.h" + +/** + * This class wraps a transaction inside a given C++ scope, guaranteeing that + * the transaction will be completed even if you have an exception or + * return early. + * + * A common use is to create an instance with aCommitOnComplete = false + * (rollback), then call Commit() on this object manually when your function + * completes successfully. + * + * @note nested transactions are not supported by Sqlite, only nested + * savepoints, so if a transaction is already in progress, this object creates + * a nested savepoint to the existing transaction which is considered as + * anonymous savepoint itself. However, aType and aAsyncCommit are ignored + * in the case of nested savepoints. + * + * @param aConnection + * The connection to create the transaction on. + * @param aCommitOnComplete + * Controls whether the transaction is committed or rolled back when + * this object goes out of scope. + * @param aType [optional] + * The transaction type, as defined in mozIStorageConnection. Uses the + * default transaction behavior for the connection if unspecified. + * @param aAsyncCommit [optional] + * Whether commit should be executed asynchronously on the helper thread. + * This is a special option introduced as an interim solution to reduce + * main-thread fsyncs in Places. Can only be used on main-thread. + * + * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! + * + * Notice that async commit might cause synchronous statements to fail + * with SQLITE_BUSY. A possible mitigation strategy is to use + * PRAGMA busy_timeout, but notice that might cause main-thread jank. + * Finally, if the database is using WAL journaling mode, other + * connections won't see the changes done in async committed transactions + * until commit is complete. + * + * For all of the above reasons, this should only be used as an interim + * solution and avoided completely if possible. + */ +class mozStorageTransaction { + using SQLiteMutexAutoLock = mozilla::storage::SQLiteMutexAutoLock; + + public: + mozStorageTransaction( + mozIStorageConnection* aConnection, bool aCommitOnComplete, + int32_t aType = mozIStorageConnection::TRANSACTION_DEFAULT, + bool aAsyncCommit = false) + : mConnection(aConnection), + mType(aType), + mNestingLevel(0), + mHasTransaction(false), + mCommitOnComplete(aCommitOnComplete), + mCompleted(false), + mAsyncCommit(aAsyncCommit) {} + + ~mozStorageTransaction() { + if (mConnection && mHasTransaction && !mCompleted) { + if (mCommitOnComplete) { + mozilla::DebugOnly rv = Commit(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "A transaction didn't commit correctly"); + } else { + mozilla::DebugOnly rv = Rollback(); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "A transaction didn't rollback correctly"); + } + } + } + + /** + * Starts the transaction. + */ + nsresult Start() { + // XXX We should probably get rid of mHasTransaction and use mConnection + // for checking if a transaction has been started. However, we need to + // first stop supporting null mConnection and also move aConnection from + // the constructor to Start. + MOZ_DIAGNOSTIC_ASSERT(!mHasTransaction); + + // XXX We should probably stop supporting null mConnection. + + // XXX We should probably get rid of mCompleted and allow to start the + // transaction again if it was already committed or rolled back. + if (!mConnection || mCompleted) { + return NS_OK; + } + + SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); + + // We nee to speculatively set the nesting level to be able to decide + // if this is a top level transaction and to be able to generate the + // savepoint name. + TransactionStarted(lock); + + // If there's a failure we need to revert the speculatively set nesting + // level on the connection. + auto autoFinishTransaction = + mozilla::MakeScopeExit([&] { TransactionFinished(lock); }); + + nsAutoCString query; + + if (TopLevelTransaction(lock)) { + query.Assign("BEGIN"); + int32_t type = mType; + if (type == mozIStorageConnection::TRANSACTION_DEFAULT) { + MOZ_ALWAYS_SUCCEEDS(mConnection->GetDefaultTransactionType(&type)); + } + switch (type) { + case mozIStorageConnection::TRANSACTION_IMMEDIATE: + query.AppendLiteral(" IMMEDIATE"); + break; + case mozIStorageConnection::TRANSACTION_EXCLUSIVE: + query.AppendLiteral(" EXCLUSIVE"); + break; + case mozIStorageConnection::TRANSACTION_DEFERRED: + query.AppendLiteral(" DEFERRED"); + break; + default: + MOZ_ASSERT(false, "Unknown transaction type"); + } + } else { + query.Assign("SAVEPOINT sp"_ns + IntToCString(mNestingLevel)); + } + + nsresult rv = mConnection->ExecuteSimpleSQL(query); + NS_ENSURE_SUCCESS(rv, rv); + + autoFinishTransaction.release(); + + return NS_OK; + } + + /** + * Commits the transaction if one is in progress. If one is not in progress, + * this is a NOP since the actual owner of the transaction outside of our + * scope is in charge of finally committing or rolling back the transaction. + */ + nsresult Commit() { + // XXX Assert instead of returning NS_OK if the transaction hasn't been + // started. + if (!mConnection || mCompleted || !mHasTransaction) return NS_OK; + + SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock)); +#else + if (!CurrentTransaction(lock)) { + return NS_ERROR_NOT_AVAILABLE; + } +#endif + + mCompleted = true; + + nsresult rv; + + if (TopLevelTransaction(lock)) { + // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't + // handle it, thus the transaction might stay open until the next COMMIT. + if (mAsyncCommit) { + nsCOMPtr ps; + rv = mConnection->ExecuteSimpleSQLAsync("COMMIT"_ns, nullptr, + getter_AddRefs(ps)); + } else { + rv = mConnection->ExecuteSimpleSQL("COMMIT"_ns); + } + } else { + rv = mConnection->ExecuteSimpleSQL("RELEASE sp"_ns + + IntToCString(mNestingLevel)); + } + + NS_ENSURE_SUCCESS(rv, rv); + + TransactionFinished(lock); + + return NS_OK; + } + + /** + * Rolls back the transaction if one is in progress. If one is not in + * progress, this is a NOP since the actual owner of the transaction outside + * of our scope is in charge of finally rolling back the transaction. + */ + nsresult Rollback() { + // XXX Assert instead of returning NS_OK if the transaction hasn't been + // started. + if (!mConnection || mCompleted || !mHasTransaction) return NS_OK; + + SQLiteMutexAutoLock lock(mConnection->GetSharedDBMutex()); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + MOZ_DIAGNOSTIC_ASSERT(CurrentTransaction(lock)); +#else + if (!CurrentTransaction(lock)) { + return NS_ERROR_NOT_AVAILABLE; + } +#endif + + mCompleted = true; + + nsresult rv; + + if (TopLevelTransaction(lock)) { + // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return + // a busy error, so this handling can be removed. + do { + rv = mConnection->ExecuteSimpleSQL("ROLLBACK"_ns); + if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT); + } while (rv == NS_ERROR_STORAGE_BUSY); + } else { + const auto nestingLevelCString = IntToCString(mNestingLevel); + rv = mConnection->ExecuteSimpleSQL( + "ROLLBACK TO sp"_ns + nestingLevelCString + "; RELEASE sp"_ns + + nestingLevelCString); + } + + NS_ENSURE_SUCCESS(rv, rv); + + TransactionFinished(lock); + + return NS_OK; + } + + protected: + void TransactionStarted(const SQLiteMutexAutoLock& aProofOfLock) { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(!mHasTransaction); + MOZ_ASSERT(mNestingLevel == 0); + mHasTransaction = true; + mNestingLevel = mConnection->IncreaseTransactionNestingLevel(aProofOfLock); + } + + bool CurrentTransaction(const SQLiteMutexAutoLock& aProofOfLock) const { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mHasTransaction); + MOZ_ASSERT(mNestingLevel > 0); + return mNestingLevel == + mConnection->GetTransactionNestingLevel(aProofOfLock); + } + + bool TopLevelTransaction(const SQLiteMutexAutoLock& aProofOfLock) const { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mHasTransaction); + MOZ_ASSERT(mNestingLevel > 0); + MOZ_ASSERT(CurrentTransaction(aProofOfLock)); + return mNestingLevel == 1; + } + + void TransactionFinished(const SQLiteMutexAutoLock& aProofOfLock) { + MOZ_ASSERT(mConnection); + MOZ_ASSERT(mHasTransaction); + MOZ_ASSERT(mNestingLevel > 0); + MOZ_ASSERT(CurrentTransaction(aProofOfLock)); + mConnection->DecreaseTransactionNestingLevel(aProofOfLock); + mNestingLevel = 0; + mHasTransaction = false; + } + + nsCOMPtr mConnection; + int32_t mType; + uint32_t mNestingLevel; + bool mHasTransaction; + bool mCommitOnComplete; + bool mCompleted; + bool mAsyncCommit; +}; + +/** + * This class wraps a statement so that it is guaraneed to be reset when + * this object goes out of scope. + * + * Note that this always just resets the statement. If the statement doesn't + * need resetting, the reset operation is inexpensive. + */ +class MOZ_STACK_CLASS mozStorageStatementScoper { + public: + explicit mozStorageStatementScoper(mozIStorageStatement* aStatement) + : mStatement(aStatement) {} + ~mozStorageStatementScoper() { + if (mStatement) mStatement->Reset(); + } + + mozStorageStatementScoper(mozStorageStatementScoper&&) = default; + mozStorageStatementScoper& operator=(mozStorageStatementScoper&&) = default; + mozStorageStatementScoper(const mozStorageStatementScoper&) = delete; + mozStorageStatementScoper& operator=(const mozStorageStatementScoper&) = + delete; + + /** + * Call this to make the statement not reset. You might do this if you know + * that the statement has been reset. + */ + void Abandon() { mStatement = nullptr; } + + protected: + nsCOMPtr mStatement; +}; + +// Use this to make queries uniquely identifiable in telemetry +// statistics, especially PRAGMAs. We don't include __LINE__ so that +// queries are stable in the face of source code changes. +#define MOZ_STORAGE_UNIQUIFY_QUERY_STR "/* " __FILE__ " */ " + +#endif /* MOZSTORAGEHELPER_H */ diff --git a/storage/mozStoragePrivateHelpers.cpp b/storage/mozStoragePrivateHelpers.cpp new file mode 100644 index 0000000000..ffda8d51e8 --- /dev/null +++ b/storage/mozStoragePrivateHelpers.cpp @@ -0,0 +1,253 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "sqlite3.h" + +#include "jsfriendapi.h" + +#include "nsPrintfCString.h" +#include "nsString.h" +#include "nsError.h" +#include "mozilla/Mutex.h" +#include "mozilla/CondVar.h" +#include "nsQueryObject.h" +#include "nsThreadUtils.h" +#include "nsJSUtils.h" + +#include "Variant.h" +#include "mozStoragePrivateHelpers.h" +#include "mozIStorageCompletionCallback.h" + +#include "mozilla/Logging.h" +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +bool isErrorCode(int aSQLiteResultCode) { + // Drop off the extended result bits of the result code. + int rc = aSQLiteResultCode & 0xFF; + + return rc != SQLITE_OK && rc != SQLITE_ROW && rc != SQLITE_DONE; +} + +nsresult convertResultCode(int aSQLiteResultCode) { + // Drop off the extended result bits of the result code. + int rc = aSQLiteResultCode & 0xFF; + + switch (rc) { + case SQLITE_OK: + case SQLITE_ROW: + case SQLITE_DONE: + return NS_OK; + case SQLITE_CORRUPT: + case SQLITE_NOTADB: + return NS_ERROR_FILE_CORRUPTED; + case SQLITE_PERM: + case SQLITE_CANTOPEN: + return NS_ERROR_FILE_ACCESS_DENIED; + case SQLITE_BUSY: + return NS_ERROR_STORAGE_BUSY; + case SQLITE_LOCKED: + return NS_ERROR_FILE_IS_LOCKED; + case SQLITE_READONLY: + return NS_ERROR_FILE_READ_ONLY; + case SQLITE_IOERR: + return NS_ERROR_STORAGE_IOERR; + case SQLITE_FULL: + case SQLITE_TOOBIG: + return NS_ERROR_FILE_NO_DEVICE_SPACE; + case SQLITE_NOMEM: + return NS_ERROR_OUT_OF_MEMORY; + case SQLITE_MISUSE: + return NS_ERROR_UNEXPECTED; + case SQLITE_ABORT: + case SQLITE_INTERRUPT: + return NS_ERROR_ABORT; + case SQLITE_CONSTRAINT: + return NS_ERROR_STORAGE_CONSTRAINT; + } + + // generic error +#ifdef DEBUG + nsAutoCString message; + message.AppendLiteral("SQLite returned error code "); + message.AppendInt(rc); + message.AppendLiteral(" , Storage will convert it to NS_ERROR_FAILURE"); + NS_WARNING_ASSERTION(rc == SQLITE_ERROR, message.get()); +#endif + return NS_ERROR_FAILURE; +} + +void checkAndLogStatementPerformance(sqlite3_stmt* aStatement) { + // Check to see if the query performed sorting operations or not. If it + // did, it may need to be optimized! + int count = ::sqlite3_stmt_status(aStatement, SQLITE_STMTSTATUS_SORT, 1); + if (count <= 0) return; + + const char* sql = ::sqlite3_sql(aStatement); + + // Check to see if this is marked to not warn + if (::strstr(sql, "/* do not warn (bug ")) return; + + // CREATE INDEX always sorts (sorting is a necessary step in creating + // an index). So ignore the warning there. + if (::strstr(sql, "CREATE INDEX") || ::strstr(sql, "CREATE UNIQUE INDEX")) + return; + + nsAutoCString message("Suboptimal indexes for the SQL statement "); +#ifdef MOZ_STORAGE_SORTWARNING_SQL_DUMP + message.Append('`'); + message.Append(sql); + message.AppendLiteral("` ["); + message.AppendInt(count); + message.AppendLiteral(" sort operation(s)]"); +#else + nsPrintfCString address("0x%p", aStatement); + message.Append(address); +#endif + message.AppendLiteral(" (http://mzl.la/1FuID0j)."); + NS_WARNING(message.get()); +} + +nsIVariant* convertJSValToVariant(JSContext* aCtx, const JS::Value& aValue) { + if (aValue.isInt32()) return new IntegerVariant(aValue.toInt32()); + + if (aValue.isDouble()) return new FloatVariant(aValue.toDouble()); + + if (aValue.isString()) { + nsAutoJSString value; + if (!value.init(aCtx, aValue.toString())) return nullptr; + return new TextVariant(value); + } + + if (aValue.isBoolean()) return new IntegerVariant(aValue.isTrue() ? 1 : 0); + + if (aValue.isNull()) return new NullVariant(); + + if (aValue.isObject()) { + JS::Rooted obj(aCtx, &aValue.toObject()); + // We only support Date instances, all others fail. + bool valid; + if (!js::DateIsValid(aCtx, obj, &valid) || !valid) return nullptr; + + double msecd; + if (!js::DateGetMsecSinceEpoch(aCtx, obj, &msecd)) return nullptr; + + msecd *= 1000.0; + int64_t msec = msecd; + + return new IntegerVariant(msec); + } + + return nullptr; +} + +Variant_base* convertVariantToStorageVariant(nsIVariant* aVariant) { + RefPtr variant = do_QueryObject(aVariant); + if (variant) { + // JS helpers already convert the JS representation to a Storage Variant, + // in such a case there's nothing left to do here, so just pass-through. + return variant; + } + + if (!aVariant) return new NullVariant(); + + uint16_t dataType = aVariant->GetDataType(); + + switch (dataType) { + case nsIDataType::VTYPE_BOOL: + case nsIDataType::VTYPE_INT8: + case nsIDataType::VTYPE_INT16: + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_UINT8: + case nsIDataType::VTYPE_UINT16: + case nsIDataType::VTYPE_UINT32: + case nsIDataType::VTYPE_INT64: + case nsIDataType::VTYPE_UINT64: { + int64_t v; + nsresult rv = aVariant->GetAsInt64(&v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new IntegerVariant(v); + } + case nsIDataType::VTYPE_FLOAT: + case nsIDataType::VTYPE_DOUBLE: { + double v; + nsresult rv = aVariant->GetAsDouble(&v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new FloatVariant(v); + } + case nsIDataType::VTYPE_CHAR: + case nsIDataType::VTYPE_CHAR_STR: + case nsIDataType::VTYPE_STRING_SIZE_IS: + case nsIDataType::VTYPE_UTF8STRING: + case nsIDataType::VTYPE_CSTRING: { + nsCString v; + nsresult rv = aVariant->GetAsAUTF8String(v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new UTF8TextVariant(v); + } + case nsIDataType::VTYPE_WCHAR: + case nsIDataType::VTYPE_WCHAR_STR: + case nsIDataType::VTYPE_WSTRING_SIZE_IS: + case nsIDataType::VTYPE_ASTRING: { + nsString v; + nsresult rv = aVariant->GetAsAString(v); + NS_ENSURE_SUCCESS(rv, nullptr); + return new TextVariant(v); + } + case nsIDataType::VTYPE_ARRAY: { + uint16_t type; + nsIID iid; + uint32_t len; + void* rawArray; + // Note this copies the array data. + nsresult rv = aVariant->GetAsArray(&type, &iid, &len, &rawArray); + NS_ENSURE_SUCCESS(rv, nullptr); + if (type == nsIDataType::VTYPE_UINT8) { + std::pair v(static_cast(rawArray), len); + // Take ownership of the data avoiding a further copy. + return new AdoptedBlobVariant(v); + } + [[fallthrough]]; + } + case nsIDataType::VTYPE_EMPTY: + case nsIDataType::VTYPE_EMPTY_ARRAY: + case nsIDataType::VTYPE_VOID: + return new NullVariant(); + case nsIDataType::VTYPE_ID: + case nsIDataType::VTYPE_INTERFACE: + case nsIDataType::VTYPE_INTERFACE_IS: + default: + NS_WARNING("Unsupported variant type"); + return nullptr; + } +} + +namespace { +class CallbackEvent : public Runnable { + public: + explicit CallbackEvent(mozIStorageCompletionCallback* aCallback) + : Runnable("storage::CallbackEvent"), mCallback(aCallback) {} + + NS_IMETHOD Run() override { + (void)mCallback->Complete(NS_OK, nullptr); + return NS_OK; + } + + private: + nsCOMPtr mCallback; +}; +} // namespace +already_AddRefed newCompletionEvent( + mozIStorageCompletionCallback* aCallback) { + NS_ASSERTION(aCallback, "Passing a null callback is a no-no!"); + nsCOMPtr event = new CallbackEvent(aCallback); + return event.forget(); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStoragePrivateHelpers.h b/storage/mozStoragePrivateHelpers.h new file mode 100644 index 0000000000..9dca95392e --- /dev/null +++ b/storage/mozStoragePrivateHelpers.h @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStoragePrivateHelpers_h +#define mozStoragePrivateHelpers_h + +/** + * This file contains convenience methods for mozStorage. + */ + +#include "sqlite3.h" +#include "nsISerialEventTarget.h" +#include "nsIVariant.h" +#include "nsError.h" +#include "js/TypeDecls.h" +#include "Variant.h" + +class mozIStorageCompletionCallback; +class nsIRunnable; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Macros + +#define ENSURE_INDEX_VALUE(aIndex, aCount) \ + NS_ENSURE_TRUE(aIndex < aCount, NS_ERROR_INVALID_ARG) + +//////////////////////////////////////////////////////////////////////////////// +//// Functions + +/** + * Returns true if the given SQLite result is an error of come kind. + * + * @param aSQLiteResultCode + * The SQLite return code to check. + * @returns true if the result represents an error. + */ +bool isErrorCode(int aSQLiteResultCode); + +/** + * Converts a SQLite return code to an nsresult return code. + * + * @param aSQLiteResultCode + * The SQLite return code to convert. + * @returns the corresponding nsresult code for aSQLiteResultCode. + */ +nsresult convertResultCode(int aSQLiteResultCode); + +/** + * Checks the performance of a SQLite statement and logs a warning with + * NS_WARNING. Currently this only checks the number of sort operations done + * on a statement, and if more than zero have been done, the statement can be + * made faster with the careful use of an index. + * + * @param aStatement + * The sqlite3_stmt object to check. + */ +void checkAndLogStatementPerformance(sqlite3_stmt* aStatement); + +/** + * Convert the provided JS::Value into a variant representation if possible. + * + * @param aCtx + * The JSContext the value is from. + * @param aValue + * The JavaScript value to convert. All primitive types are supported, + * but only Date objects are supported from the Date family. Date + * objects are coerced to PRTime (nanoseconds since epoch) values. + * @return the variant if conversion was successful, nullptr if conversion + * failed. The caller is responsible for addref'ing if non-null. + */ +nsIVariant* convertJSValToVariant(JSContext* aCtx, const JS::Value& aValue); + +/** + * Convert a provided nsIVariant implementation to our own thread-safe + * refcounting implementation, if needed. + * + * @param aValue + * The original nsIVariant to be converted. + * @return a thread-safe refcounting nsIVariant implementation. + */ +Variant_base* convertVariantToStorageVariant(nsIVariant* aVariant); + +/** + * Obtains an event that will notify a completion callback about completion. + * + * @param aCallback + * The callback to be notified. + * @return an nsIRunnable that can be dispatched to the calling thread. + */ +already_AddRefed newCompletionEvent( + mozIStorageCompletionCallback* aCallback); + +/** + * Utility method to get a Blob as a string value. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult DoGetBlobAsString(T* aThis, uint32_t aIndex, V& aValue) { + typedef typename V::char_type char_type; + + uint32_t size; + char_type* blob; + nsresult rv = + aThis->GetBlob(aIndex, &size, reinterpret_cast(&blob)); + NS_ENSURE_SUCCESS(rv, rv); + + aValue.Assign(blob, size / sizeof(char_type)); + delete[] blob; + return NS_OK; +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult DoBindStringAsBlobByName(T* aThis, const nsACString& aName, + const V& aValue) { + typedef typename V::char_type char_type; + return aThis->BindBlobByName( + aName, reinterpret_cast(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + +/** + * Utility method to bind a string value as a Blob. The string expects + * the interface exposed by nsAString/nsACString/etc. + */ +template +nsresult DoBindStringAsBlobByIndex(T* aThis, uint32_t aIndex, const V& aValue) { + typedef typename V::char_type char_type; + return aThis->BindBlobByIndex( + aIndex, reinterpret_cast(aValue.BeginReading()), + aValue.Length() * sizeof(char_type)); +} + +/** + * Utility function to check if a serial event target may run runnables + * on the current thread. + */ +inline bool IsOnCurrentSerialEventTarget(nsISerialEventTarget* aTarget) { + return aTarget->IsOnCurrentThread(); +} + +} // namespace storage +} // namespace mozilla + +#endif // mozStoragePrivateHelpers_h diff --git a/storage/mozStorageResultSet.cpp b/storage/mozStorageResultSet.cpp new file mode 100644 index 0000000000..8a0ae6abd3 --- /dev/null +++ b/storage/mozStorageResultSet.cpp @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageRow.h" +#include "mozStorageResultSet.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// ResultSet + +ResultSet::ResultSet() : mCurrentIndex(0) {} + +ResultSet::~ResultSet() { mData.Clear(); } + +nsresult ResultSet::add(mozIStorageRow* aRow) { + return mData.AppendObject(aRow) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; +} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS(ResultSet, mozIStorageResultSet) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageResultSet + +NS_IMETHODIMP +ResultSet::GetNextRow(mozIStorageRow** _row) { + NS_ENSURE_ARG_POINTER(_row); + + if (mCurrentIndex >= mData.Count()) { + // Just return null here + return NS_OK; + } + + NS_ADDREF(*_row = mData.ObjectAt(mCurrentIndex++)); + return NS_OK; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageResultSet.h b/storage/mozStorageResultSet.h new file mode 100644 index 0000000000..0716504378 --- /dev/null +++ b/storage/mozStorageResultSet.h @@ -0,0 +1,52 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageResultSet_h +#define mozStorageResultSet_h + +#include "mozIStorageResultSet.h" +#include "nsCOMArray.h" +#include "mozilla/Attributes.h" +class mozIStorageRow; + +namespace mozilla { +namespace storage { + +class ResultSet final : public mozIStorageResultSet { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGERESULTSET + + ResultSet(); + + /** + * Adds a tuple to this result set. + */ + nsresult add(mozIStorageRow* aTuple); + + /** + * @returns the number of rows this result set holds. + */ + int32_t rows() const { return mData.Count(); } + + private: + ~ResultSet(); + + /** + * Stores the current index of the active result set. + */ + int32_t mCurrentIndex; + + /** + * Stores the tuples. + */ + nsCOMArray mData; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageResultSet_h diff --git a/storage/mozStorageRow.cpp b/storage/mozStorageRow.cpp new file mode 100644 index 0000000000..bee49dfd85 --- /dev/null +++ b/storage/mozStorageRow.cpp @@ -0,0 +1,206 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageRow.h" + +#include "nsString.h" + +#include "sqlite3.h" +#include "mozStoragePrivateHelpers.h" +#include "Variant.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Row + +nsresult Row::initialize(sqlite3_stmt* aStatement) { + // Get the number of results + mNumCols = ::sqlite3_column_count(aStatement); + + // Start copying over values + for (uint32_t i = 0; i < mNumCols; i++) { + // Store the value + nsIVariant* variant = nullptr; + int type = ::sqlite3_column_type(aStatement, i); + switch (type) { + case SQLITE_INTEGER: + variant = new IntegerVariant(::sqlite3_column_int64(aStatement, i)); + break; + case SQLITE_FLOAT: + variant = new FloatVariant(::sqlite3_column_double(aStatement, i)); + break; + case SQLITE_TEXT: { + const char16_t* value = static_cast( + ::sqlite3_column_text16(aStatement, i)); + nsDependentString str( + value, ::sqlite3_column_bytes16(aStatement, i) / sizeof(char16_t)); + variant = new TextVariant(str); + break; + } + case SQLITE_NULL: + variant = new NullVariant(); + break; + case SQLITE_BLOB: { + const void* data = ::sqlite3_column_blob(aStatement, i); + int size = ::sqlite3_column_bytes(aStatement, i); + variant = new BlobVariant(std::pair(data, size)); + break; + } + default: + return NS_ERROR_UNEXPECTED; + } + NS_ENSURE_TRUE(variant, NS_ERROR_OUT_OF_MEMORY); + + // Insert into our storage array + NS_ENSURE_TRUE(mData.InsertObjectAt(variant, i), NS_ERROR_OUT_OF_MEMORY); + + // Associate the name (if any) with the index + const char* name = ::sqlite3_column_name(aStatement, i); + if (!name) break; + nsAutoCString colName(name); + mNameHashtable.InsertOrUpdate(colName, i); + } + + return NS_OK; +} + +/** + * Note: This object is only ever accessed on one thread at a time. It it not + * threadsafe, but it does need threadsafe AddRef and Release. + */ +NS_IMPL_ISUPPORTS(Row, mozIStorageRow, mozIStorageValueArray) + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageRow + +NS_IMETHODIMP +Row::GetResultByIndex(uint32_t aIndex, nsIVariant** _result) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + NS_ADDREF(*_result = mData.ObjectAt(aIndex)); + return NS_OK; +} + +NS_IMETHODIMP +Row::GetResultByName(const nsACString& aName, nsIVariant** _result) { + uint32_t index; + NS_ENSURE_TRUE(mNameHashtable.Get(aName, &index), NS_ERROR_NOT_AVAILABLE); + return GetResultByIndex(index, _result); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray + +NS_IMETHODIMP +Row::GetNumEntries(uint32_t* _entries) { + *_entries = mNumCols; + return NS_OK; +} + +NS_IMETHODIMP +Row::GetTypeOfIndex(uint32_t aIndex, int32_t* _type) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + + uint16_t type = mData.ObjectAt(aIndex)->GetDataType(); + switch (type) { + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_INT64: + *_type = mozIStorageValueArray::VALUE_TYPE_INTEGER; + break; + case nsIDataType::VTYPE_DOUBLE: + *_type = mozIStorageValueArray::VALUE_TYPE_FLOAT; + break; + case nsIDataType::VTYPE_ASTRING: + *_type = mozIStorageValueArray::VALUE_TYPE_TEXT; + break; + case nsIDataType::VTYPE_ARRAY: + *_type = mozIStorageValueArray::VALUE_TYPE_BLOB; + break; + default: + *_type = mozIStorageValueArray::VALUE_TYPE_NULL; + break; + } + return NS_OK; +} + +NS_IMETHODIMP +Row::GetInt32(uint32_t aIndex, int32_t* _value) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsInt32(_value); +} + +NS_IMETHODIMP +Row::GetInt64(uint32_t aIndex, int64_t* _value) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsInt64(_value); +} + +NS_IMETHODIMP +Row::GetDouble(uint32_t aIndex, double* _value) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsDouble(_value); +} + +NS_IMETHODIMP +Row::GetUTF8String(uint32_t aIndex, nsACString& _value) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsAUTF8String(_value); +} + +NS_IMETHODIMP +Row::GetString(uint32_t aIndex, nsAString& _value) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + return mData.ObjectAt(aIndex)->GetAsAString(_value); +} + +NS_IMETHODIMP +Row::GetBlob(uint32_t aIndex, uint32_t* _size, uint8_t** _blob) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + + uint16_t type; + nsIID interfaceIID; + return mData.ObjectAt(aIndex)->GetAsArray(&type, &interfaceIID, _size, + reinterpret_cast(_blob)); +} + +NS_IMETHODIMP +Row::GetBlobAsString(uint32_t aIndex, nsAString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Row::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Row::GetIsNull(uint32_t aIndex, bool* _isNull) { + ENSURE_INDEX_VALUE(aIndex, mNumCols); + NS_ENSURE_ARG_POINTER(_isNull); + + uint16_t type = mData.ObjectAt(aIndex)->GetDataType(); + *_isNull = type == nsIDataType::VTYPE_EMPTY; + return NS_OK; +} + +NS_IMETHODIMP +Row::GetSharedUTF8String(uint32_t, uint32_t*, char const**) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Row::GetSharedString(uint32_t, uint32_t*, const char16_t**) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +Row::GetSharedBlob(uint32_t, uint32_t*, const uint8_t**) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageRow.h b/storage/mozStorageRow.h new file mode 100644 index 0000000000..b816781719 --- /dev/null +++ b/storage/mozStorageRow.h @@ -0,0 +1,59 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageRow_h +#define mozStorageRow_h + +#include "mozIStorageRow.h" +#include "nsCOMArray.h" +#include "nsTHashMap.h" +#include "mozilla/Attributes.h" +class nsIVariant; +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class Row final : public mozIStorageRow { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGEROW + NS_DECL_MOZISTORAGEVALUEARRAY + + Row() : mNumCols(0) {} + + /** + * Initializes the object with the given statement. Copies the values from + * the statement. + * + * @param aStatement + * The sqlite statement to pull results from. + */ + nsresult initialize(sqlite3_stmt* aStatement); + + private: + ~Row() {} + + /** + * The number of columns in this tuple. + */ + uint32_t mNumCols; + + /** + * Stores the data in the tuple. + */ + nsCOMArray mData; + + /** + * Maps a given name to a column index. + */ + nsTHashMap mNameHashtable; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageRow_h diff --git a/storage/mozStorageSQLFunctions.cpp b/storage/mozStorageSQLFunctions.cpp new file mode 100644 index 0000000000..c05010c2ff --- /dev/null +++ b/storage/mozStorageSQLFunctions.cpp @@ -0,0 +1,369 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/ArrayUtils.h" + +#include "mozStorageSQLFunctions.h" +#include "nsTArray.h" +#include "nsUnicharUtils.h" +#include +#include "sqlite3.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Local Helper Functions + +namespace { + +/** + * Performs the LIKE comparison of a string against a pattern. For more detail + * see http://www.sqlite.org/lang_expr.html#like. + * + * @param aPatternItr + * An iterator at the start of the pattern to check for. + * @param aPatternEnd + * An iterator at the end of the pattern to check for. + * @param aStringItr + * An iterator at the start of the string to check for the pattern. + * @param aStringEnd + * An iterator at the end of the string to check for the pattern. + * @param aEscapeChar + * The character to use for escaping symbols in the pattern. + * @return 1 if the pattern is found, 0 otherwise. + */ +int likeCompare(nsAString::const_iterator aPatternItr, + nsAString::const_iterator aPatternEnd, + nsAString::const_iterator aStringItr, + nsAString::const_iterator aStringEnd, char16_t aEscapeChar) { + const char16_t MATCH_ALL('%'); + const char16_t MATCH_ONE('_'); + + bool lastWasEscape = false; + while (aPatternItr != aPatternEnd) { + /** + * What we do in here is take a look at each character from the input + * pattern, and do something with it. There are 4 possibilities: + * 1) character is an un-escaped match-all character + * 2) character is an un-escaped match-one character + * 3) character is an un-escaped escape character + * 4) character is not any of the above + */ + if (!lastWasEscape && *aPatternItr == MATCH_ALL) { + // CASE 1 + /** + * Now we need to skip any MATCH_ALL or MATCH_ONE characters that follow a + * MATCH_ALL character. For each MATCH_ONE character, skip one character + * in the pattern string. + */ + while (*aPatternItr == MATCH_ALL || *aPatternItr == MATCH_ONE) { + if (*aPatternItr == MATCH_ONE) { + // If we've hit the end of the string we are testing, no match + if (aStringItr == aStringEnd) return 0; + aStringItr++; + } + aPatternItr++; + } + + // If we've hit the end of the pattern string, match + if (aPatternItr == aPatternEnd) return 1; + + while (aStringItr != aStringEnd) { + if (likeCompare(aPatternItr, aPatternEnd, aStringItr, aStringEnd, + aEscapeChar)) { + // we've hit a match, so indicate this + return 1; + } + aStringItr++; + } + + // No match + return 0; + } + if (!lastWasEscape && *aPatternItr == MATCH_ONE) { + // CASE 2 + if (aStringItr == aStringEnd) { + // If we've hit the end of the string we are testing, no match + return 0; + } + aStringItr++; + lastWasEscape = false; + } else if (!lastWasEscape && *aPatternItr == aEscapeChar) { + // CASE 3 + lastWasEscape = true; + } else { + // CASE 4 + if (::ToUpperCase(*aStringItr) != ::ToUpperCase(*aPatternItr)) { + // If we've hit a point where the strings don't match, there is no match + return 0; + } + aStringItr++; + lastWasEscape = false; + } + + aPatternItr++; + } + + return aStringItr == aStringEnd; +} + +/** + * Compute the Levenshtein Edit Distance between two strings. + * + * @param aStringS + * a string + * @param aStringT + * another string + * @param _result + * an outparam that will receive the edit distance between the arguments + * @return a Sqlite result code, e.g. SQLITE_OK, SQLITE_NOMEM, etc. + */ +int levenshteinDistance(const nsAString& aStringS, const nsAString& aStringT, + int* _result) { + // Set the result to a non-sensical value in case we encounter an error. + *_result = -1; + + const uint32_t sLen = aStringS.Length(); + const uint32_t tLen = aStringT.Length(); + + if (sLen == 0) { + *_result = tLen; + return SQLITE_OK; + } + if (tLen == 0) { + *_result = sLen; + return SQLITE_OK; + } + + // Notionally, Levenshtein Distance is computed in a matrix. If we + // assume s = "span" and t = "spam", the matrix would look like this: + // s --> + // t s p a n + // | 0 1 2 3 4 + // V s 1 * * * * + // p 2 * * * * + // a 3 * * * * + // m 4 * * * * + // + // Note that the row width is sLen + 1 and the column height is tLen + 1, + // where sLen is the length of the string "s" and tLen is the length of "t". + // The first row and the first column are initialized as shown, and + // the algorithm computes the remaining cells row-by-row, and + // left-to-right within each row. The computation only requires that + // we be able to see the current row and the previous one. + + // Allocate memory for two rows. + AutoTArray row1; + AutoTArray row2; + + // Declare the raw pointers that will actually be used to access the memory. + int* prevRow = row1.AppendElements(sLen + 1); + int* currRow = row2.AppendElements(sLen + 1); + + // Initialize the first row. + for (uint32_t i = 0; i <= sLen; i++) prevRow[i] = i; + + const char16_t* s = aStringS.BeginReading(); + const char16_t* t = aStringT.BeginReading(); + + // Compute the empty cells in the "matrix" row-by-row, starting with + // the second row. + for (uint32_t ti = 1; ti <= tLen; ti++) { + // Initialize the first cell in this row. + currRow[0] = ti; + + // Get the character from "t" that corresponds to this row. + const char16_t tch = t[ti - 1]; + + // Compute the remaining cells in this row, left-to-right, + // starting at the second column (and first character of "s"). + for (uint32_t si = 1; si <= sLen; si++) { + // Get the character from "s" that corresponds to this column, + // compare it to the t-character, and compute the "cost". + const char16_t sch = s[si - 1]; + int cost = (sch == tch) ? 0 : 1; + + // ............ We want to calculate the value of cell "d" from + // ...ab....... the previously calculated (or initialized) cells + // ...cd....... "a", "b", and "c", where d = min(a', b', c'). + // ............ + int aPrime = prevRow[si - 1] + cost; + int bPrime = prevRow[si] + 1; + int cPrime = currRow[si - 1] + 1; + currRow[si] = std::min(aPrime, std::min(bPrime, cPrime)); + } + + // Advance to the next row. The current row becomes the previous + // row and we recycle the old previous row as the new current row. + // We don't need to re-initialize the new current row since we will + // rewrite all of its cells anyway. + int* oldPrevRow = prevRow; + prevRow = currRow; + currRow = oldPrevRow; + } + + // The final result is the value of the last cell in the last row. + // Note that that's now in the "previous" row, since we just swapped them. + *_result = prevRow[sLen]; + return SQLITE_OK; +} + +// This struct is used only by registerFunctions below, but ISO C++98 forbids +// instantiating a template dependent on a locally-defined type. Boo-urns! +struct Functions { + const char* zName; + int nArg; + int enc; + void* pContext; + void (*xFunc)(::sqlite3_context*, int, sqlite3_value**); +}; + +} // namespace + +//////////////////////////////////////////////////////////////////////////////// +//// Exposed Functions + +int registerFunctions(sqlite3* aDB) { + Functions functions[] = { + {"lower", 1, SQLITE_UTF16, 0, caseFunction}, + {"lower", 1, SQLITE_UTF8, 0, caseFunction}, + {"upper", 1, SQLITE_UTF16, (void*)1, caseFunction}, + {"upper", 1, SQLITE_UTF8, (void*)1, caseFunction}, + + {"like", 2, SQLITE_UTF16, 0, likeFunction}, + {"like", 2, SQLITE_UTF8, 0, likeFunction}, + {"like", 3, SQLITE_UTF16, 0, likeFunction}, + {"like", 3, SQLITE_UTF8, 0, likeFunction}, + + {"levenshteinDistance", 2, SQLITE_UTF16, 0, levenshteinDistanceFunction}, + {"levenshteinDistance", 2, SQLITE_UTF8, 0, levenshteinDistanceFunction}, + + {"utf16Length", 1, SQLITE_UTF16, 0, utf16LengthFunction}, + {"utf16Length", 1, SQLITE_UTF8, 0, utf16LengthFunction}, + }; + + int rv = SQLITE_OK; + for (size_t i = 0; SQLITE_OK == rv && i < ArrayLength(functions); ++i) { + struct Functions* p = &functions[i]; + rv = ::sqlite3_create_function(aDB, p->zName, p->nArg, p->enc, p->pContext, + p->xFunc, nullptr, nullptr); + } + + return rv; +} + +//////////////////////////////////////////////////////////////////////////////// +//// SQL Functions + +void caseFunction(sqlite3_context* aCtx, int aArgc, sqlite3_value** aArgv) { + NS_ASSERTION(1 == aArgc, "Invalid number of arguments!"); + + const char16_t* value = + static_cast(::sqlite3_value_text16(aArgv[0])); + nsAutoString data(value, + ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t)); + bool toUpper = ::sqlite3_user_data(aCtx) ? true : false; + + if (toUpper) + ::ToUpperCase(data); + else + ::ToLowerCase(data); + + // Set the result. + ::sqlite3_result_text16(aCtx, data.get(), data.Length() * sizeof(char16_t), + SQLITE_TRANSIENT); +} + +/** + * This implements the like() SQL function. This is used by the LIKE operator. + * The SQL statement 'A LIKE B' is implemented as 'like(B, A)', and if there is + * an escape character, say E, it is implemented as 'like(B, A, E)'. + */ +void likeFunction(sqlite3_context* aCtx, int aArgc, sqlite3_value** aArgv) { + NS_ASSERTION(2 == aArgc || 3 == aArgc, "Invalid number of arguments!"); + + if (::sqlite3_value_bytes(aArgv[0]) > + ::sqlite3_limit(::sqlite3_context_db_handle(aCtx), + SQLITE_LIMIT_LIKE_PATTERN_LENGTH, -1)) { + ::sqlite3_result_error(aCtx, "LIKE or GLOB pattern too complex", + SQLITE_TOOBIG); + return; + } + + if (!::sqlite3_value_text16(aArgv[0]) || !::sqlite3_value_text16(aArgv[1])) + return; + + const char16_t* a = + static_cast(::sqlite3_value_text16(aArgv[1])); + int aLen = ::sqlite3_value_bytes16(aArgv[1]) / sizeof(char16_t); + nsDependentString A(a, aLen); + + const char16_t* b = + static_cast(::sqlite3_value_text16(aArgv[0])); + int bLen = ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t); + nsDependentString B(b, bLen); + NS_ASSERTION(!B.IsEmpty(), "LIKE string must not be null!"); + + char16_t E = 0; + if (3 == aArgc) + E = static_cast(::sqlite3_value_text16(aArgv[2]))[0]; + + nsAString::const_iterator itrString, endString; + A.BeginReading(itrString); + A.EndReading(endString); + nsAString::const_iterator itrPattern, endPattern; + B.BeginReading(itrPattern); + B.EndReading(endPattern); + ::sqlite3_result_int( + aCtx, likeCompare(itrPattern, endPattern, itrString, endString, E)); +} + +void levenshteinDistanceFunction(sqlite3_context* aCtx, int aArgc, + sqlite3_value** aArgv) { + NS_ASSERTION(2 == aArgc, "Invalid number of arguments!"); + + // If either argument is a SQL NULL, then return SQL NULL. + if (::sqlite3_value_type(aArgv[0]) == SQLITE_NULL || + ::sqlite3_value_type(aArgv[1]) == SQLITE_NULL) { + ::sqlite3_result_null(aCtx); + return; + } + + const char16_t* a = + static_cast(::sqlite3_value_text16(aArgv[0])); + int aLen = ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t); + + const char16_t* b = + static_cast(::sqlite3_value_text16(aArgv[1])); + int bLen = ::sqlite3_value_bytes16(aArgv[1]) / sizeof(char16_t); + + // Compute the Levenshtein Distance, and return the result (or error). + int distance = -1; + const nsDependentString A(a, aLen); + const nsDependentString B(b, bLen); + int status = levenshteinDistance(A, B, &distance); + if (status == SQLITE_OK) { + ::sqlite3_result_int(aCtx, distance); + } else if (status == SQLITE_NOMEM) { + ::sqlite3_result_error_nomem(aCtx); + } else { + ::sqlite3_result_error(aCtx, "User function returned error code", -1); + } +} + +void utf16LengthFunction(sqlite3_context* aCtx, int aArgc, + sqlite3_value** aArgv) { + NS_ASSERTION(1 == aArgc, "Invalid number of arguments!"); + + int len = ::sqlite3_value_bytes16(aArgv[0]) / sizeof(char16_t); + + // Set the result. + ::sqlite3_result_int(aCtx, len); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageSQLFunctions.h b/storage/mozStorageSQLFunctions.h new file mode 100644 index 0000000000..2a3f1f3d7e --- /dev/null +++ b/storage/mozStorageSQLFunctions.h @@ -0,0 +1,85 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageSQLFunctions_h +#define mozStorageSQLFunctions_h + +#include "sqlite3.h" +#include "nscore.h" + +namespace mozilla { +namespace storage { + +/** + * Registers the functions declared here with the specified database. + * + * @param aDB + * The database we'll be registering the functions with. + * @return the SQLite status code indicating success or failure. + */ +int registerFunctions(sqlite3* aDB); + +//////////////////////////////////////////////////////////////////////////////// +//// Predefined Functions + +/** + * Overridden function to perform the SQL functions UPPER and LOWER. These + * support unicode, which the default implementations do not do. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void caseFunction(sqlite3_context* aCtx, int aArgc, sqlite3_value** aArgv); + +/** + * Overridden function to perform the SQL function LIKE. This supports unicode, + * which the default implementation does not do. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void likeFunction(sqlite3_context* aCtx, int aArgc, sqlite3_value** aArgv); + +/** + * An implementation of the Levenshtein Edit Distance algorithm for use in + * Sqlite queries. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void levenshteinDistanceFunction(sqlite3_context* aCtx, int aArgc, + sqlite3_value** aArgv); + +/** + * An alternative string length function that uses XPCOM string classes for + * string length calculation. + * + * @param aCtx + * The sqlite_context that this function is being called on. + * @param aArgc + * The number of arguments the function is being called with. + * @param aArgv + * An array of the arguments the functions is being called with. + */ +void utf16LengthFunction(sqlite3_context* aCtx, int aArgc, + sqlite3_value** aArgv); + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageSQLFunctions_h diff --git a/storage/mozStorageService.cpp b/storage/mozStorageService.cpp new file mode 100644 index 0000000000..78aebb5d8d --- /dev/null +++ b/storage/mozStorageService.cpp @@ -0,0 +1,744 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "BaseVFS.h" +#include "mozilla/Attributes.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsIFile.h" +#include "nsIFileURL.h" +#include "mozStorageService.h" +#include "mozStorageConnection.h" +#include "nsComponentManagerUtils.h" +#include "nsEmbedCID.h" +#include "nsExceptionHandler.h" +#include "nsThreadUtils.h" +#include "mozStoragePrivateHelpers.h" +#include "nsIObserverService.h" +#include "nsIPropertyBag2.h" +#include "ObfuscatingVFS.h" +#include "QuotaVFS.h" +#include "mozilla/Services.h" +#include "mozilla/LateWriteChecks.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStoragePendingStatement.h" +#include "mozilla/StaticPrefs_storage.h" +#include "mozilla/intl/Collator.h" +#include "mozilla/intl/LocaleService.h" + +#include "sqlite3.h" +#include "mozilla/AutoSQLiteLifetime.h" + +#ifdef XP_WIN +// "windows.h" was included and it can #define lots of things we care about... +# undef CompareString +#endif + +using mozilla::intl::Collator; + +namespace mozilla::storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Memory Reporting + +#ifdef MOZ_DMD +mozilla::Atomic gSqliteMemoryUsed; +#endif + +static int64_t StorageSQLiteDistinguishedAmount() { + return ::sqlite3_memory_used(); +} + +/** + * Passes a single SQLite memory statistic to a memory reporter callback. + * + * @param aHandleReport + * The callback. + * @param aData + * The data for the callback. + * @param aConn + * The SQLite connection. + * @param aPathHead + * Head of the path for the memory report. + * @param aKind + * The memory report statistic kind, one of "stmt", "cache" or + * "schema". + * @param aDesc + * The memory report description. + * @param aOption + * The SQLite constant for getting the measurement. + * @param aTotal + * The accumulator for the measurement. + */ +static void ReportConn(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, Connection* aConn, + const nsACString& aPathHead, const nsACString& aKind, + const nsACString& aDesc, int32_t aOption, + size_t* aTotal) { + nsCString path(aPathHead); + path.Append(aKind); + path.AppendLiteral("-used"); + + int32_t val = aConn->getSqliteRuntimeStatus(aOption); + aHandleReport->Callback(""_ns, path, nsIMemoryReporter::KIND_HEAP, + nsIMemoryReporter::UNITS_BYTES, int64_t(val), aDesc, + aData); + *aTotal += val; +} + +// Warning: To get a Connection's measurements requires holding its lock. +// There may be a delay getting the lock if another thread is accessing the +// Connection. This isn't very nice if CollectReports is called from the main +// thread! But at the time of writing this function is only called when +// about:memory is loaded (not, for example, when telemetry pings occur) and +// any delays in that case aren't so bad. +NS_IMETHODIMP +Service::CollectReports(nsIHandleReportCallback* aHandleReport, + nsISupports* aData, bool aAnonymize) { + size_t totalConnSize = 0; + { + nsTArray> connections; + getConnections(connections); + + for (uint32_t i = 0; i < connections.Length(); i++) { + RefPtr& conn = connections[i]; + + // Someone may have closed the Connection, in which case we skip it. + // Note that we have consumers of the synchronous API that are off the + // main-thread, like the DOM Cache and IndexedDB, and as such we must be + // sure that we have a connection. + MutexAutoLock lockedAsyncScope(conn->sharedAsyncExecutionMutex); + if (!conn->connectionReady()) { + continue; + } + + nsCString pathHead("explicit/storage/sqlite/"); + // This filename isn't privacy-sensitive, and so is never anonymized. + pathHead.Append(conn->getFilename()); + pathHead.Append('/'); + + SQLiteMutexAutoLock lockedScope(conn->sharedDBMutex); + + constexpr auto stmtDesc = + "Memory (approximate) used by all prepared statements used by " + "connections to this database."_ns; + ReportConn(aHandleReport, aData, conn, pathHead, "stmt"_ns, stmtDesc, + SQLITE_DBSTATUS_STMT_USED, &totalConnSize); + + constexpr auto cacheDesc = + "Memory (approximate) used by all pager caches used by connections " + "to this database."_ns; + ReportConn(aHandleReport, aData, conn, pathHead, "cache"_ns, cacheDesc, + SQLITE_DBSTATUS_CACHE_USED_SHARED, &totalConnSize); + + constexpr auto schemaDesc = + "Memory (approximate) used to store the schema for all databases " + "associated with connections to this database."_ns; + ReportConn(aHandleReport, aData, conn, pathHead, "schema"_ns, schemaDesc, + SQLITE_DBSTATUS_SCHEMA_USED, &totalConnSize); + } + +#ifdef MOZ_DMD + if (::sqlite3_memory_used() != int64_t(gSqliteMemoryUsed)) { + NS_WARNING( + "memory consumption reported by SQLite doesn't match " + "our measurements"); + } +#endif + } + + int64_t other = static_cast(::sqlite3_memory_used() - totalConnSize); + + MOZ_COLLECT_REPORT("explicit/storage/sqlite/other", KIND_HEAP, UNITS_BYTES, + other, "All unclassified sqlite memory."); + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Service + +NS_IMPL_ISUPPORTS(Service, mozIStorageService, nsIObserver, nsIMemoryReporter) + +Service* Service::gService = nullptr; + +already_AddRefed Service::getSingleton() { + if (gService) { + return do_AddRef(gService); + } + + // The first reference to the storage service must be obtained on the + // main thread. + NS_ENSURE_TRUE(NS_IsMainThread(), nullptr); + RefPtr service = new Service(); + if (NS_SUCCEEDED(service->initialize())) { + // Note: This is cleared in the Service destructor. + gService = service.get(); + return service.forget(); + } + + return nullptr; +} + +int Service::AutoVFSRegistration::Init(UniquePtr aVFS) { + MOZ_ASSERT(!mVFS); + if (aVFS) { + mVFS = std::move(aVFS); + return sqlite3_vfs_register(mVFS.get(), 0); + } + NS_WARNING("Failed to register VFS"); + return SQLITE_OK; +} + +Service::AutoVFSRegistration::~AutoVFSRegistration() { + if (mVFS) { + int rc = sqlite3_vfs_unregister(mVFS.get()); + if (rc != SQLITE_OK) { + NS_WARNING("Failed to unregister sqlite vfs wrapper."); + } + } +} + +Service::Service() + : mMutex("Service::mMutex"), + mRegistrationMutex("Service::mRegistrationMutex"), + mLastSensitivity(mozilla::intl::Collator::Sensitivity::Base) {} + +Service::~Service() { + mozilla::UnregisterWeakMemoryReporter(this); + mozilla::UnregisterStorageSQLiteDistinguishedAmount(); + + gService = nullptr; +} + +void Service::registerConnection(Connection* aConnection) { + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + (void)mConnections.AppendElement(aConnection); +} + +void Service::unregisterConnection(Connection* aConnection) { + // If this is the last Connection it might be the only thing keeping Service + // alive. So ensure that Service is destroyed only after the Connection is + // cleanly unregistered and destroyed. + RefPtr kungFuDeathGrip(this); + RefPtr forgettingRef; + { + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + + for (uint32_t i = 0; i < mConnections.Length(); ++i) { + if (mConnections[i] == aConnection) { + // Because dropping the final reference can potentially result in + // spinning a nested event loop if the connection was not properly + // shutdown, we want to do that outside this loop so that we can finish + // mutating the array and drop our mutex. + forgettingRef = std::move(mConnections[i]); + mConnections.RemoveElementAt(i); + break; + } + } + } + + MOZ_ASSERT(forgettingRef, + "Attempt to unregister unknown storage connection!"); + + // Do not proxy the release anywhere, just let this reference drop here. (We + // previously did proxy the release, but that was because we invoked Close() + // in the destructor and Close() likes to complain if it's not invoked on the + // opener event target, so it was essential that the last reference be dropped + // on the opener event target. We now enqueue Close() inside our caller, + // Release(), so it doesn't actually matter what thread our reference drops + // on.) +} + +void Service::getConnections( + /* inout */ nsTArray>& aConnections) { + mRegistrationMutex.AssertNotCurrentThreadOwns(); + MutexAutoLock mutex(mRegistrationMutex); + aConnections.Clear(); + aConnections.AppendElements(mConnections); +} + +void Service::minimizeMemory() { + nsTArray> connections; + getConnections(connections); + + for (uint32_t i = 0; i < connections.Length(); i++) { + RefPtr conn = connections[i]; + // For non-main-thread owning/opening threads, we may be racing against them + // closing their connection or their thread. That's okay, see below. + if (!conn->connectionReady()) { + continue; + } + + constexpr auto shrinkPragma = "PRAGMA shrink_memory"_ns; + + if (!conn->operationSupported(Connection::SYNCHRONOUS)) { + // This is a mozIStorageAsyncConnection, it can only be used on the main + // thread, so we can do a straight API call. + nsCOMPtr ps; + DebugOnly rv = conn->ExecuteSimpleSQLAsync( + shrinkPragma, nullptr, getter_AddRefs(ps)); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should have purged sqlite caches"); + } else if (IsOnCurrentSerialEventTarget(conn->eventTargetOpenedOn)) { + if (conn->isAsyncExecutionThreadAvailable()) { + nsCOMPtr ps; + DebugOnly rv = conn->ExecuteSimpleSQLAsync( + shrinkPragma, nullptr, getter_AddRefs(ps)); + MOZ_ASSERT(NS_SUCCEEDED(rv), "Should have purged sqlite caches"); + } else { + conn->ExecuteSimpleSQL(shrinkPragma); + } + } else { + // We are on the wrong event target, the query should be executed on the + // opener event target, so we must dispatch to it. + // It's possible the connection is already closed or will be closed by the + // time our runnable runs. ExecuteSimpleSQL will safely return with a + // failure in that case. If the event target is shutting down or shut + // down, the dispatch will fail and that's okay. + nsCOMPtr event = NewRunnableMethod( + "Connection::ExecuteSimpleSQL", conn, &Connection::ExecuteSimpleSQL, + shrinkPragma); + Unused << conn->eventTargetOpenedOn->Dispatch(event, NS_DISPATCH_NORMAL); + } + } +} + +UniquePtr ConstructReadOnlyNoLockVFS(); + +static const char* sObserverTopics[] = {"memory-pressure", + "xpcom-shutdown-threads"}; + +nsresult Service::initialize() { + MOZ_ASSERT(NS_IsMainThread(), "Must be initialized on the main thread"); + + int rc = AutoSQLiteLifetime::getInitResult(); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + /** + * The virtual file system hierarchy + * + * obfsvfs + * | + * | + * | + * quotavfs + * / \ + * / \ + * / \ + * / \ + * / \ + * base-vfs-excl base-vfs + * / \ / \ + * / \ / \ + * / \ / \ + * unix-excl win32 unix win32 + */ + + rc = mBaseSqliteVFS.Init(basevfs::ConstructVFS(false)); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + rc = mBaseExclSqliteVFS.Init(basevfs::ConstructVFS(true)); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + rc = mQuotaSqliteVFS.Init(quotavfs::ConstructVFS(basevfs::GetVFSName( + StaticPrefs::storage_sqlite_exclusiveLock_enabled()))); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + rc = + mObfuscatingSqliteVFS.Init(obfsvfs::ConstructVFS(quotavfs::GetVFSName())); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + rc = mReadOnlyNoLockSqliteVFS.Init(ConstructReadOnlyNoLockVFS()); + if (rc != SQLITE_OK) { + return convertResultCode(rc); + } + + nsCOMPtr os = mozilla::services::GetObserverService(); + NS_ENSURE_TRUE(os, NS_ERROR_FAILURE); + + for (auto& sObserverTopic : sObserverTopics) { + nsresult rv = os->AddObserver(this, sObserverTopic, false); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mozilla::RegisterWeakMemoryReporter(this); + mozilla::RegisterStorageSQLiteDistinguishedAmount( + StorageSQLiteDistinguishedAmount); + + return NS_OK; +} + +int Service::localeCompareStrings(const nsAString& aStr1, + const nsAString& aStr2, + Collator::Sensitivity aSensitivity) { + // The mozilla::intl::Collator is not thread safe, since the Collator::Options + // can be changed. + MutexAutoLock mutex(mMutex); + + Collator* collator = getCollator(); + if (!collator) { + NS_ERROR("Storage service has no collation"); + return 0; + } + + if (aSensitivity != mLastSensitivity) { + Collator::Options options{}; + options.sensitivity = aSensitivity; + auto result = mCollator->SetOptions(options); + + if (result.isErr()) { + NS_WARNING("Could not configure the mozilla::intl::Collation."); + return 0; + } + mLastSensitivity = aSensitivity; + } + + return collator->CompareStrings(aStr1, aStr2); +} + +Collator* Service::getCollator() { + mMutex.AssertCurrentThreadOwns(); + + if (mCollator) { + return mCollator.get(); + } + + auto result = mozilla::intl::LocaleService::TryCreateComponent(); + if (result.isErr()) { + NS_WARNING("Could not create mozilla::intl::Collation."); + return nullptr; + } + + mCollator = result.unwrap(); + + // Sort in a case-insensitive way, where "base" letters are considered + // equal, e.g: a = á, a = A, a ≠ b. + Collator::Options options{}; + options.sensitivity = Collator::Sensitivity::Base; + auto optResult = mCollator->SetOptions(options); + + if (optResult.isErr()) { + NS_WARNING("Could not configure the mozilla::intl::Collation."); + mCollator = nullptr; + return nullptr; + } + + return mCollator.get(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageService + +NS_IMETHODIMP +Service::OpenSpecialDatabase(const nsACString& aStorageKey, + const nsACString& aName, uint32_t aConnectionFlags, + mozIStorageConnection** _connection) { + if (!aStorageKey.Equals(kMozStorageMemoryStorageKey)) { + return NS_ERROR_INVALID_ARG; + } + + const bool interruptible = + aConnectionFlags & mozIStorageService::CONNECTION_INTERRUPTIBLE; + + int flags = SQLITE_OPEN_READWRITE; + + if (!aName.IsEmpty()) { + flags |= SQLITE_OPEN_URI; + } + + RefPtr msc = + new Connection(this, flags, Connection::SYNCHRONOUS, + kMozStorageMemoryStorageKey, interruptible); + const nsresult rv = msc->initialize(aStorageKey, aName); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +namespace { + +class AsyncInitDatabase final : public Runnable { + public: + AsyncInitDatabase(Connection* aConnection, nsIFile* aStorageFile, + int32_t aGrowthIncrement, + mozIStorageCompletionCallback* aCallback) + : Runnable("storage::AsyncInitDatabase"), + mConnection(aConnection), + mStorageFile(aStorageFile), + mGrowthIncrement(aGrowthIncrement), + mCallback(aCallback) { + MOZ_ASSERT(NS_IsMainThread()); + } + + NS_IMETHOD Run() override { + MOZ_ASSERT(!NS_IsMainThread()); + nsresult rv = mConnection->initializeOnAsyncThread(mStorageFile); + if (NS_FAILED(rv)) { + return DispatchResult(rv, nullptr); + } + + if (mGrowthIncrement >= 0) { + // Ignore errors. In the future, we might wish to log them. + (void)mConnection->SetGrowthIncrement(mGrowthIncrement, ""_ns); + } + + return DispatchResult( + NS_OK, NS_ISUPPORTS_CAST(mozIStorageAsyncConnection*, mConnection)); + } + + private: + nsresult DispatchResult(nsresult aStatus, nsISupports* aValue) { + RefPtr event = + new CallbackComplete(aStatus, aValue, mCallback.forget()); + return NS_DispatchToMainThread(event); + } + + ~AsyncInitDatabase() { + NS_ReleaseOnMainThread("AsyncInitDatabase::mStorageFile", + mStorageFile.forget()); + NS_ReleaseOnMainThread("AsyncInitDatabase::mConnection", + mConnection.forget()); + + // Generally, the callback will be released by CallbackComplete. + // However, if for some reason Run() is not executed, we still + // need to ensure that it is released here. + NS_ReleaseOnMainThread("AsyncInitDatabase::mCallback", mCallback.forget()); + } + + RefPtr mConnection; + nsCOMPtr mStorageFile; + int32_t mGrowthIncrement; + RefPtr mCallback; +}; + +} // namespace + +NS_IMETHODIMP +Service::OpenAsyncDatabase(nsIVariant* aDatabaseStore, uint32_t aOpenFlags, + uint32_t /* aConnectionFlags */, + mozIStorageCompletionCallback* aCallback) { + if (!NS_IsMainThread()) { + return NS_ERROR_NOT_SAME_THREAD; + } + NS_ENSURE_ARG(aDatabaseStore); + NS_ENSURE_ARG(aCallback); + + const bool shared = aOpenFlags & mozIStorageService::OPEN_SHARED; + const bool ignoreLockingMode = + aOpenFlags & mozIStorageService::OPEN_IGNORE_LOCKING_MODE; + // Specifying ignoreLockingMode will force use of the readOnly flag: + const bool readOnly = + ignoreLockingMode || (aOpenFlags & mozIStorageService::OPEN_READONLY); + int flags = readOnly ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE; + + nsCOMPtr storageFile; + nsCOMPtr dbStore; + nsresult rv = aDatabaseStore->GetAsISupports(getter_AddRefs(dbStore)); + if (NS_SUCCEEDED(rv)) { + // Generally, aDatabaseStore holds the database nsIFile. + storageFile = do_QueryInterface(dbStore, &rv); + if (NS_FAILED(rv)) { + return NS_ERROR_INVALID_ARG; + } + + nsCOMPtr cloned; + rv = storageFile->Clone(getter_AddRefs(cloned)); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + storageFile = std::move(cloned); + + if (!readOnly) { + // Ensure that SQLITE_OPEN_CREATE is passed in for compatibility reasons. + flags |= SQLITE_OPEN_CREATE; + } + + // Apply the shared-cache option. + flags |= shared ? SQLITE_OPEN_SHAREDCACHE : SQLITE_OPEN_PRIVATECACHE; + } else { + // Sometimes, however, it's a special database name. + nsAutoCString keyString; + rv = aDatabaseStore->GetAsACString(keyString); + if (NS_FAILED(rv) || !keyString.Equals(kMozStorageMemoryStorageKey)) { + return NS_ERROR_INVALID_ARG; + } + + // Just fall through with nullptr storageFile, this will cause the storage + // connection to use a memory DB. + } + + // Create connection on this thread, but initialize it on its helper thread. + nsAutoCString telemetryFilename; + if (!storageFile) { + telemetryFilename.Assign(kMozStorageMemoryStorageKey); + } else { + rv = storageFile->GetNativeLeafName(telemetryFilename); + NS_ENSURE_SUCCESS(rv, rv); + } + RefPtr msc = + new Connection(this, flags, Connection::ASYNCHRONOUS, telemetryFilename, + /* interruptible */ true, ignoreLockingMode); + nsCOMPtr target = msc->getAsyncExecutionTarget(); + MOZ_ASSERT(target, + "Cannot initialize a connection that has been closed already"); + + RefPtr asyncInit = new AsyncInitDatabase( + msc, storageFile, /* growthIncrement */ -1, aCallback); + return target->Dispatch(asyncInit, nsIEventTarget::DISPATCH_NORMAL); +} + +NS_IMETHODIMP +Service::OpenDatabase(nsIFile* aDatabaseFile, uint32_t aConnectionFlags, + mozIStorageConnection** _connection) { + NS_ENSURE_ARG(aDatabaseFile); + + const bool interruptible = + aConnectionFlags & mozIStorageService::CONNECTION_INTERRUPTIBLE; + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + const int flags = + SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE | SQLITE_OPEN_CREATE; + nsAutoCString telemetryFilename; + nsresult rv = aDatabaseFile->GetNativeLeafName(telemetryFilename); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr msc = new Connection(this, flags, Connection::SYNCHRONOUS, + telemetryFilename, interruptible); + rv = msc->initialize(aDatabaseFile); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +NS_IMETHODIMP +Service::OpenUnsharedDatabase(nsIFile* aDatabaseFile, uint32_t aConnectionFlags, + mozIStorageConnection** _connection) { + NS_ENSURE_ARG(aDatabaseFile); + + const bool interruptible = + aConnectionFlags & mozIStorageService::CONNECTION_INTERRUPTIBLE; + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + const int flags = + SQLITE_OPEN_READWRITE | SQLITE_OPEN_PRIVATECACHE | SQLITE_OPEN_CREATE; + nsAutoCString telemetryFilename; + nsresult rv = aDatabaseFile->GetNativeLeafName(telemetryFilename); + NS_ENSURE_SUCCESS(rv, rv); + RefPtr msc = new Connection(this, flags, Connection::SYNCHRONOUS, + telemetryFilename, interruptible); + rv = msc->initialize(aDatabaseFile); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +NS_IMETHODIMP +Service::OpenDatabaseWithFileURL(nsIFileURL* aFileURL, + const nsACString& aTelemetryFilename, + uint32_t aConnectionFlags, + mozIStorageConnection** _connection) { + NS_ENSURE_ARG(aFileURL); + + const bool interruptible = + aConnectionFlags & mozIStorageService::CONNECTION_INTERRUPTIBLE; + + // Always ensure that SQLITE_OPEN_CREATE is passed in for compatibility + // reasons. + const int flags = SQLITE_OPEN_READWRITE | SQLITE_OPEN_SHAREDCACHE | + SQLITE_OPEN_CREATE | SQLITE_OPEN_URI; + nsresult rv; + nsAutoCString telemetryFilename; + if (!aTelemetryFilename.IsEmpty()) { + telemetryFilename = aTelemetryFilename; + } else { + nsCOMPtr databaseFile; + rv = aFileURL->GetFile(getter_AddRefs(databaseFile)); + NS_ENSURE_SUCCESS(rv, rv); + rv = databaseFile->GetNativeLeafName(telemetryFilename); + NS_ENSURE_SUCCESS(rv, rv); + } + RefPtr msc = new Connection(this, flags, Connection::SYNCHRONOUS, + telemetryFilename, interruptible); + rv = msc->initialize(aFileURL); + NS_ENSURE_SUCCESS(rv, rv); + + msc.forget(_connection); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// nsIObserver + +NS_IMETHODIMP +Service::Observe(nsISupports*, const char* aTopic, const char16_t*) { + if (strcmp(aTopic, "memory-pressure") == 0) { + minimizeMemory(); + } else if (strcmp(aTopic, "xpcom-shutdown-threads") == 0) { + // The Service is kept alive by our strong observer references and + // references held by Connection instances. Since we're about to remove the + // former and then wait for the latter ones to go away, it behooves us to + // hold a strong reference to ourselves so our calls to getConnections() do + // not happen on a deleted object. + RefPtr kungFuDeathGrip = this; + + nsCOMPtr os = mozilla::services::GetObserverService(); + + for (auto& sObserverTopic : sObserverTopics) { + (void)os->RemoveObserver(this, sObserverTopic); + } + + SpinEventLoopUntil("storage::Service::Observe(xpcom-shutdown-threads)"_ns, + [&]() -> bool { + // We must wait until all the closing connections are + // closed. + nsTArray> connections; + getConnections(connections); + for (auto& conn : connections) { + if (conn->isClosing()) { + return false; + } + } + return true; + }); + +#ifdef DEBUG + nsTArray> connections; + getConnections(connections); + for (uint32_t i = 0, n = connections.Length(); i < n; i++) { + if (!connections[i]->isClosed()) { + // getFilename is only the leaf name for the database file, + // so it shouldn't contain privacy-sensitive information. + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::StorageConnectionNotClosed, + connections[i]->getFilename()); + printf_stderr("Storage connection not closed: %s", + connections[i]->getFilename().get()); + MOZ_CRASH(); + } + } +#endif + } + + return NS_OK; +} + +} // namespace mozilla::storage diff --git a/storage/mozStorageService.h b/storage/mozStorageService.h new file mode 100644 index 0000000000..fc8686f122 --- /dev/null +++ b/storage/mozStorageService.h @@ -0,0 +1,183 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 MOZSTORAGESERVICE_H +#define MOZSTORAGESERVICE_H + +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsIMemoryReporter.h" +#include "nsIObserver.h" +#include "nsTArray.h" +#include "mozilla/Mutex.h" +#include "mozilla/UniquePtr.h" +#include "mozilla/intl/Collator.h" + +#include "mozIStorageService.h" + +class nsIMemoryReporter; +struct sqlite3_vfs; +namespace mozilla::intl { +class Collator; +} + +namespace mozilla::storage { + +class Connection; +class Service : public mozIStorageService, + public nsIObserver, + public nsIMemoryReporter { + public: + /** + * Initializes the service. This must be called before any other function! + */ + nsresult initialize(); + + /** + * Compares two strings using the Service's locale-aware collation. + * + * @param aStr1 + * The string to be compared against aStr2. + * @param aStr2 + * The string to be compared against aStr1. + * @param aSensitivity + * The sorting sensitivity. + * @return aStr1 - aStr2. That is, if aStr1 < aStr2, returns a negative + * number. If aStr1 > aStr2, returns a positive number. If + * aStr1 == aStr2, returns 0. + */ + int localeCompareStrings(const nsAString& aStr1, const nsAString& aStr2, + mozilla::intl::Collator::Sensitivity aSensitivity); + + static already_AddRefed getSingleton(); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGESERVICE + NS_DECL_NSIOBSERVER + NS_DECL_NSIMEMORYREPORTER + + /** + * Returns a boolean value indicating whether or not the given page size is + * valid (currently understood as a power of 2 between 512 and 65536). + */ + static bool pageSizeIsValid(int32_t aPageSize) { + return aPageSize == 512 || aPageSize == 1024 || aPageSize == 2048 || + aPageSize == 4096 || aPageSize == 8192 || aPageSize == 16384 || + aPageSize == 32768 || aPageSize == 65536; + } + + static const int32_t kDefaultPageSize = 32768; + + /** + * Registers the connection with the storage service. Connections are + * registered so they can be iterated over. + * + * @pre mRegistrationMutex is not held + * + * @param aConnection + * The connection to register. + */ + void registerConnection(Connection* aConnection); + + /** + * Unregisters the connection with the storage service. + * + * @pre mRegistrationMutex is not held + * + * @param aConnection + * The connection to unregister. + */ + void unregisterConnection(Connection* aConnection); + + /** + * Gets the list of open connections. Note that you must test each + * connection with mozIStorageConnection::connectionReady before doing + * anything with it, and skip it if it's not ready. + * + * @pre mRegistrationMutex is not held + * + * @param aConnections + * An inout param; it is cleared and the connections are appended to + * it. + * @return The open connections. + */ + void getConnections(nsTArray >& aConnections); + + private: + Service(); + virtual ~Service(); + + /** + * Used for 1) locking around calls when initializing connections so that we + * can ensure that the state of sqlite3_enable_shared_cache is sane and 2) + * synchronizing access to mLocaleCollation. + */ + Mutex mMutex MOZ_UNANNOTATED; + + struct AutoVFSRegistration { + int Init(UniquePtr aVFS); + ~AutoVFSRegistration(); + + private: + UniquePtr mVFS; + }; + + // The order of these members should match the order of Init calls in + // initialize(), to ensure that the unregistration takes place in the reverse + // order. + AutoVFSRegistration mBaseSqliteVFS; + AutoVFSRegistration mBaseExclSqliteVFS; + AutoVFSRegistration mQuotaSqliteVFS; + AutoVFSRegistration mObfuscatingSqliteVFS; + AutoVFSRegistration mReadOnlyNoLockSqliteVFS; + + /** + * Protects mConnections. + */ + Mutex mRegistrationMutex MOZ_UNANNOTATED; + + /** + * The list of connections we have created. Modifications to it are + * protected by |mRegistrationMutex|. + */ + nsTArray > mConnections; + + /** + * Frees as much heap memory as possible from all of the known open + * connections. + */ + void minimizeMemory(); + + /** + * Lazily creates and returns a collator created from the application's + * locale that all statements of all Connections of this Service may use. + * Since the collator's lifetime is that of the Service and no statement may + * execute outside the lifetime of the Service, this method returns a raw + * pointer. + */ + mozilla::intl::Collator* getCollator(); + + /** + * Lazily created collator that all statements of all Connections of this + * Service may use. The collator is created from the application's locale. + * + * @note The collator is not thread-safe since the options can be changed + * between calls. Access should be synchronized. + */ + mozilla::UniquePtr mCollator = nullptr; + + nsCOMPtr mProfileStorageFile; + + nsCOMPtr mStorageSQLiteReporter; + + static Service* gService; + + mozilla::intl::Collator::Sensitivity mLastSensitivity; +}; + +} // namespace mozilla::storage + +#endif /* MOZSTORAGESERVICE_H */ diff --git a/storage/mozStorageStatement.cpp b/storage/mozStorageStatement.cpp new file mode 100644 index 0000000000..6de46e6ea8 --- /dev/null +++ b/storage/mozStorageStatement.cpp @@ -0,0 +1,843 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 +#include + +#include "nsError.h" +#include "nsThreadUtils.h" +#include "nsIClassInfoImpl.h" +#include "Variant.h" + +#include "mozIStorageError.h" + +#include "mozStorageBindingParams.h" +#include "mozStorageConnection.h" +#include "mozStorageStatementJSHelper.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatementParams.h" +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" + +#include "mozilla/Logging.h" +#include "mozilla/Printf.h" +#include "mozilla/ProfilerLabels.h" + +extern mozilla::LazyLogModule gStorageLog; + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// nsIClassInfo + +NS_IMPL_CI_INTERFACE_GETTER(Statement, mozIStorageStatement, + mozIStorageBaseStatement, mozIStorageBindingParams, + mozIStorageValueArray, + mozilla::storage::StorageBaseStatementInternal) + +class StatementClassInfo : public nsIClassInfo { + public: + constexpr StatementClassInfo() = default; + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD + GetInterfaces(nsTArray& _array) override { + return NS_CI_INTERFACE_GETTER_NAME(Statement)(_array); + } + + NS_IMETHOD + GetScriptableHelper(nsIXPCScriptable** _helper) override { + static StatementJSHelper sJSHelper; + *_helper = &sJSHelper; + return NS_OK; + } + + NS_IMETHOD + GetContractID(nsACString& aContractID) override { + aContractID.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD + GetClassDescription(nsACString& aDesc) override { + aDesc.SetIsVoid(true); + return NS_OK; + } + + NS_IMETHOD + GetClassID(nsCID** _id) override { + *_id = nullptr; + return NS_OK; + } + + NS_IMETHOD + GetFlags(uint32_t* _flags) override { + *_flags = 0; + return NS_OK; + } + + NS_IMETHOD + GetClassIDNoAlloc(nsCID* _cid) override { return NS_ERROR_NOT_AVAILABLE; } +}; + +NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::AddRef() { + return 2; +} +NS_IMETHODIMP_(MozExternalRefCountType) StatementClassInfo::Release() { + return 1; +} +NS_IMPL_QUERY_INTERFACE(StatementClassInfo, nsIClassInfo) + +static StatementClassInfo sStatementClassInfo; + +//////////////////////////////////////////////////////////////////////////////// +//// Statement + +Statement::Statement() + : mDBStatement(nullptr), + mParamCount(0), + mResultColumnCount(0), + mExecuting(false), + mQueryStatusRecorded(false), + mHasExecuted(false) {} + +nsresult Statement::initialize(Connection* aDBConnection, + sqlite3* aNativeConnection, + const nsACString& aSQLStatement) { + MOZ_ASSERT(aDBConnection, "No database connection given!"); + MOZ_ASSERT(aDBConnection->isConnectionReadyOnThisThread(), + "Database connection should be valid"); + MOZ_ASSERT(!mDBStatement, "Statement already initialized!"); + MOZ_ASSERT(aNativeConnection, "No native connection given!"); + + int srv = aDBConnection->prepareStatement( + aNativeConnection, PromiseFlatCString(aSQLStatement), &mDBStatement); + if (srv != SQLITE_OK) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Sqlite statement prepare error: %d '%s'", srv, + ::sqlite3_errmsg(aNativeConnection))); + MOZ_LOG(gStorageLog, LogLevel::Error, + ("Statement was: '%s'", PromiseFlatCString(aSQLStatement).get())); + + aDBConnection->RecordQueryStatus(srv); + mQueryStatusRecorded = true; + return convertResultCode(srv); + } + + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Initialized statement '%s' (0x%p)", + PromiseFlatCString(aSQLStatement).get(), mDBStatement)); + + mDBConnection = aDBConnection; + mNativeConnection = aNativeConnection; + mParamCount = ::sqlite3_bind_parameter_count(mDBStatement); + mResultColumnCount = ::sqlite3_column_count(mDBStatement); + mColumnNames.Clear(); + + nsCString* columnNames = mColumnNames.AppendElements(mResultColumnCount); + for (uint32_t i = 0; i < mResultColumnCount; i++) { + const char* name = ::sqlite3_column_name(mDBStatement, i); + columnNames[i].Assign(name); + } + +#ifdef DEBUG + // We want to try and test for LIKE and that consumers are using + // escapeStringForLIKE instead of just trusting user input. The idea to + // check to see if they are binding a parameter after like instead of just + // using a string. We only do this in debug builds because it's expensive! + auto c = nsCaseInsensitiveCStringComparator; + nsACString::const_iterator start, end, e; + aSQLStatement.BeginReading(start); + aSQLStatement.EndReading(end); + e = end; + while (::FindInReadable(" LIKE"_ns, start, e, c)) { + // We have a LIKE in here, so we perform our tests + // FindInReadable moves the iterator, so we have to get a new one for + // each test we perform. + nsACString::const_iterator s1, s2, s3; + s1 = s2 = s3 = start; + + if (!(::FindInReadable(" LIKE ?"_ns, s1, end, c) || + ::FindInReadable(" LIKE :"_ns, s2, end, c) || + ::FindInReadable(" LIKE @"_ns, s3, end, c))) { + // At this point, we didn't find a LIKE statement followed by ?, :, + // or @, all of which are valid characters for binding a parameter. + // We will warn the consumer that they may not be safely using LIKE. + NS_WARNING( + "Unsafe use of LIKE detected! Please ensure that you " + "are using mozIStorageStatement::escapeStringForLIKE " + "and that you are binding that result to the statement " + "to prevent SQL injection attacks."); + } + + // resetting start and e + start = e; + e = end; + } +#endif + + return NS_OK; +} + +mozIStorageBindingParams* Statement::getParams() { + nsresult rv; + + // If we do not have an array object yet, make it. + if (!mParamsArray) { + nsCOMPtr array; + rv = NewBindingParamsArray(getter_AddRefs(array)); + NS_ENSURE_SUCCESS(rv, nullptr); + + mParamsArray = static_cast(array.get()); + } + + // If there isn't already any rows added, we'll have to add one to use. + if (mParamsArray->length() == 0) { + RefPtr params(new BindingParams(mParamsArray, this)); + NS_ENSURE_TRUE(params, nullptr); + + rv = mParamsArray->AddParams(params); + NS_ENSURE_SUCCESS(rv, nullptr); + + // We have to unlock our params because AddParams locks them. This is safe + // because no reference to the params object was, or ever will be given out. + params->unlock(this); + + // We also want to lock our array at this point - we don't want anything to + // be added to it. Nothing has, or will ever get a reference to it, but we + // will get additional safety checks via assertions by doing this. + mParamsArray->lock(); + } + + return *mParamsArray->begin(); +} + +void Statement::MaybeRecordQueryStatus(int srv, bool isResetting) { + // If the statement hasn't been executed synchronously since it was last reset + // or created then there is no need to record anything. Asynchronous + // statements have their status tracked and recorded by StatementData. + if (!mHasExecuted) { + return; + } + + if (!isResetting && !isErrorCode(srv)) { + // Non-errors will be recorded when finalizing. + return; + } + + // We only record a status if no status has been recorded previously. + if (!mQueryStatusRecorded && mDBConnection) { + mDBConnection->RecordQueryStatus(srv); + } + + // Allow another status to be recorded if we are resetting this statement. + mQueryStatusRecorded = !isResetting; +} + +Statement::~Statement() { (void)internalFinalize(true); } + +//////////////////////////////////////////////////////////////////////////////// +//// nsISupports + +NS_IMPL_ADDREF(Statement) +NS_IMPL_RELEASE(Statement) + +NS_INTERFACE_MAP_BEGIN(Statement) + NS_INTERFACE_MAP_ENTRY(mozIStorageStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBaseStatement) + NS_INTERFACE_MAP_ENTRY(mozIStorageBindingParams) + NS_INTERFACE_MAP_ENTRY(mozIStorageValueArray) + NS_INTERFACE_MAP_ENTRY(mozilla::storage::StorageBaseStatementInternal) + if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { + foundInterface = static_cast(&sStatementClassInfo); + } else + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, mozIStorageStatement) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// StorageBaseStatementInternal + +Connection* Statement::getOwner() { return mDBConnection; } + +int Statement::getAsyncStatement(sqlite3_stmt** _stmt) { + // If we have no statement, we shouldn't be calling this method! + NS_ASSERTION(mDBStatement != nullptr, "We have no statement to clone!"); + + // If we do not yet have a cached async statement, clone our statement now. + if (!mAsyncStatement) { + nsDependentCString sql(::sqlite3_sql(mDBStatement)); + int rc = mDBConnection->prepareStatement(mNativeConnection, sql, + &mAsyncStatement); + if (rc != SQLITE_OK) { + mDBConnection->RecordQueryStatus(rc); + *_stmt = nullptr; + return rc; + } + + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Cloned statement 0x%p to 0x%p", mDBStatement, mAsyncStatement)); + } + + *_stmt = mAsyncStatement; + return SQLITE_OK; +} + +nsresult Statement::getAsynchronousStatementData(StatementData& _data) { + if (!mDBStatement) return NS_ERROR_UNEXPECTED; + + sqlite3_stmt* stmt; + int rc = getAsyncStatement(&stmt); + if (rc != SQLITE_OK) return convertResultCode(rc); + + _data = StatementData(stmt, bindingParamsArray(), this); + + return NS_OK; +} + +already_AddRefed Statement::newBindingParams( + mozIStorageBindingParamsArray* aOwner) { + nsCOMPtr params = new BindingParams(aOwner, this); + return params.forget(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatement + +// proxy to StorageBaseStatementInternal using its define helper. +MIXIN_IMPL_STORAGEBASESTATEMENTINTERNAL(Statement, (void)0;) + +NS_IMETHODIMP +Statement::Clone(mozIStorageStatement** _statement) { + RefPtr statement(new Statement()); + NS_ENSURE_TRUE(statement, NS_ERROR_OUT_OF_MEMORY); + + nsAutoCString sql(::sqlite3_sql(mDBStatement)); + nsresult rv = statement->initialize(mDBConnection, mNativeConnection, sql); + NS_ENSURE_SUCCESS(rv, rv); + + statement.forget(_statement); + return NS_OK; +} + +NS_IMETHODIMP +Statement::Finalize() { return internalFinalize(false); } + +nsresult Statement::internalFinalize(bool aDestructing) { + if (!mDBStatement) return NS_OK; + + int srv = SQLITE_OK; + + { + // If the statement ends up being finalized twice, the second finalization + // would apply to a dangling pointer and may cause unexpected consequences. + // Thus we must be sure that the connection state won't change during this + // operation, to avoid racing with finalizations made by the closing + // connection. See Connection::internalClose(). + MutexAutoLock lockedScope(mDBConnection->sharedAsyncExecutionMutex); + if (!mDBConnection->isClosed(lockedScope)) { + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Finalizing statement '%s' during garbage-collection", + ::sqlite3_sql(mDBStatement))); + srv = ::sqlite3_finalize(mDBStatement); + } +#ifdef DEBUG + else { + // The database connection is closed. The sqlite + // statement has either been finalized already by the connection + // or is about to be finalized by the connection. + // + // Finalizing it here would be useless and segfaultish. + // + // Note that we can't display the statement itself, as the data structure + // is not valid anymore. However, the address shown here should help + // developers correlate with the more complete debug message triggered + // by AsyncClose(). + + SmprintfPointer msg = ::mozilla::Smprintf( + "SQL statement (%p) should have been finalized" + " before garbage-collection. For more details on this statement, set" + " NSPR_LOG_MESSAGES=mozStorage:5 .", + mDBStatement); + NS_WARNING(msg.get()); + + // Use %s so we aren't exposing random strings to printf interpolation. + MOZ_LOG(gStorageLog, LogLevel::Warning, ("%s", msg.get())); + } +#endif // DEBUG + } + + // This will be a no-op if the status has already been recorded or if this + // statement has not been executed. Async statements have their status + // tracked and recorded in StatementData. + MaybeRecordQueryStatus(srv, true); + + mDBStatement = nullptr; + + if (mAsyncStatement) { + // If the destructor called us, there are no pending async statements (they + // hold a reference to us) and we can/must just kill the statement directly. + if (aDestructing) + destructorAsyncFinalize(); + else + asyncFinalize(); + } + + // Release the holders, so they can release the reference to us. + mStatementParamsHolder = nullptr; + mStatementRowHolder = nullptr; + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Statement::GetParameterCount(uint32_t* _parameterCount) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + *_parameterCount = mParamCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetParameterName(uint32_t aParamIndex, nsACString& _name) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + ENSURE_INDEX_VALUE(aParamIndex, mParamCount); + + const char* name = + ::sqlite3_bind_parameter_name(mDBStatement, aParamIndex + 1); + if (name == nullptr) { + // this thing had no name, so fake one + nsAutoCString fakeName(":"); + fakeName.AppendInt(aParamIndex); + _name.Assign(fakeName); + } else { + _name.Assign(nsDependentCString(name)); + } + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetParameterIndex(const nsACString& aName, uint32_t* _index) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + // We do not accept any forms of names other than ":name", but we need to add + // the colon for SQLite. + nsAutoCString name(":"); + name.Append(aName); + int ind = ::sqlite3_bind_parameter_index(mDBStatement, name.get()); + if (ind == 0) // Named parameter not found. + return NS_ERROR_INVALID_ARG; + + *_index = ind - 1; // SQLite indexes are 1-based, we are 0-based. + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnCount(uint32_t* _columnCount) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + *_columnCount = mResultColumnCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnName(uint32_t aColumnIndex, nsACString& _name) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + ENSURE_INDEX_VALUE(aColumnIndex, mResultColumnCount); + + const char* cname = ::sqlite3_column_name(mDBStatement, aColumnIndex); + _name.Assign(nsDependentCString(cname)); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetColumnIndex(const nsACString& aName, uint32_t* _index) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + // Surprisingly enough, SQLite doesn't provide an API for this. We have to + // determine it ourselves sadly. + for (uint32_t i = 0; i < mResultColumnCount; i++) { + if (mColumnNames[i].Equals(aName)) { + *_index = i; + return NS_OK; + } + } + + return NS_ERROR_INVALID_ARG; +} + +NS_IMETHODIMP +Statement::Reset() { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + +#ifdef DEBUG + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Resetting statement: '%s'", ::sqlite3_sql(mDBStatement))); + + checkAndLogStatementPerformance(mDBStatement); +#endif + + mParamsArray = nullptr; + (void)sqlite3_reset(mDBStatement); + (void)sqlite3_clear_bindings(mDBStatement); + + mExecuting = false; + + // This will be a no-op if the status has already been recorded or if this + // statement has not been executed. Async statements have their status + // tracked and recorded in StatementData. + MaybeRecordQueryStatus(SQLITE_OK, true); + mHasExecuted = false; + + return NS_OK; +} + +NS_IMETHODIMP +Statement::BindParameters(mozIStorageBindingParamsArray* aParameters) { + NS_ENSURE_ARG_POINTER(aParameters); + + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + BindingParamsArray* array = static_cast(aParameters); + if (array->getOwner() != this) return NS_ERROR_UNEXPECTED; + + if (array->length() == 0) return NS_ERROR_UNEXPECTED; + + mParamsArray = array; + mParamsArray->lock(); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::Execute() { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + bool ret; + nsresult rv = ExecuteStep(&ret); + nsresult rv2 = Reset(); + + return NS_FAILED(rv) ? rv : rv2; +} + +NS_IMETHODIMP +Statement::ExecuteStep(bool* _moreResults) { + AUTO_PROFILER_LABEL("Statement::ExecuteStep", OTHER); + + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + // Bind any parameters first before executing. + if (mParamsArray) { + // If we have more than one row of parameters to bind, they shouldn't be + // calling this method (and instead use executeAsync). + if (mParamsArray->length() != 1) return NS_ERROR_UNEXPECTED; + + BindingParamsArray::iterator row = mParamsArray->begin(); + nsCOMPtr bindingInternal = + do_QueryInterface(*row); + nsCOMPtr error = bindingInternal->bind(mDBStatement); + if (error) { + int32_t srv; + (void)error->GetResult(&srv); + return convertResultCode(srv); + } + + // We have bound, so now we can clear our array. + mParamsArray = nullptr; + } + int srv = mDBConnection->stepStatement(mNativeConnection, mDBStatement); + mHasExecuted = true; + MaybeRecordQueryStatus(srv); + + if (srv != SQLITE_ROW && srv != SQLITE_DONE && + MOZ_LOG_TEST(gStorageLog, LogLevel::Debug)) { + nsAutoCString errStr; + (void)mDBConnection->GetLastErrorString(errStr); + MOZ_LOG(gStorageLog, LogLevel::Debug, + ("Statement::ExecuteStep error: %s", errStr.get())); + } + + // SQLITE_ROW and SQLITE_DONE are non-errors + if (srv == SQLITE_ROW) { + // we got a row back + mExecuting = true; + *_moreResults = true; + return NS_OK; + } else if (srv == SQLITE_DONE) { + // statement is done (no row returned) + mExecuting = false; + *_moreResults = false; + return NS_OK; + } else if (srv == SQLITE_BUSY || srv == SQLITE_MISUSE) { + mExecuting = false; + } else if (mExecuting) { + MOZ_LOG(gStorageLog, LogLevel::Error, + ("SQLite error after mExecuting was true!")); + mExecuting = false; + } + + return convertResultCode(srv); +} + +NS_IMETHODIMP +Statement::GetState(int32_t* _state) { + if (!mDBStatement) + *_state = MOZ_STORAGE_STATEMENT_INVALID; + else if (mExecuting) + *_state = MOZ_STORAGE_STATEMENT_EXECUTING; + else + *_state = MOZ_STORAGE_STATEMENT_READY; + + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageValueArray (now part of mozIStorageStatement too) + +NS_IMETHODIMP +Statement::GetNumEntries(uint32_t* _length) { + *_length = mResultColumnCount; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetTypeOfIndex(uint32_t aIndex, int32_t* _type) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) return NS_ERROR_UNEXPECTED; + + int t = ::sqlite3_column_type(mDBStatement, aIndex); + switch (t) { + case SQLITE_INTEGER: + *_type = mozIStorageStatement::VALUE_TYPE_INTEGER; + break; + case SQLITE_FLOAT: + *_type = mozIStorageStatement::VALUE_TYPE_FLOAT; + break; + case SQLITE_TEXT: + *_type = mozIStorageStatement::VALUE_TYPE_TEXT; + break; + case SQLITE_BLOB: + *_type = mozIStorageStatement::VALUE_TYPE_BLOB; + break; + case SQLITE_NULL: + *_type = mozIStorageStatement::VALUE_TYPE_NULL; + break; + default: + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetInt32(uint32_t aIndex, int32_t* _value) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_int(mDBStatement, aIndex); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetInt64(uint32_t aIndex, int64_t* _value) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_int64(mDBStatement, aIndex); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetDouble(uint32_t aIndex, double* _value) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) return NS_ERROR_UNEXPECTED; + + *_value = ::sqlite3_column_double(mDBStatement, aIndex); + + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetUTF8String(uint32_t aIndex, nsACString& _value) { + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + if (type == mozIStorageStatement::VALUE_TYPE_NULL) { + // NULL columns should have IsVoid set to distinguish them from the empty + // string. + _value.SetIsVoid(true); + } else { + const char* value = reinterpret_cast( + ::sqlite3_column_text(mDBStatement, aIndex)); + _value.Assign(value, ::sqlite3_column_bytes(mDBStatement, aIndex)); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetString(uint32_t aIndex, nsAString& _value) { + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + if (type == mozIStorageStatement::VALUE_TYPE_NULL) { + // NULL columns should have IsVoid set to distinguish them from the empty + // string. + _value.SetIsVoid(true); + } else { + const char16_t* value = static_cast( + ::sqlite3_column_text16(mDBStatement, aIndex)); + _value.Assign(value, ::sqlite3_column_bytes16(mDBStatement, aIndex) / + sizeof(char16_t)); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetVariant(uint32_t aIndex, nsIVariant** _value) { + if (!mDBStatement) { + return NS_ERROR_NOT_INITIALIZED; + } + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr variant; + int type = ::sqlite3_column_type(mDBStatement, aIndex); + switch (type) { + case SQLITE_INTEGER: + variant = + new IntegerVariant(::sqlite3_column_int64(mDBStatement, aIndex)); + break; + case SQLITE_FLOAT: + variant = new FloatVariant(::sqlite3_column_double(mDBStatement, aIndex)); + break; + case SQLITE_TEXT: { + const char16_t* value = static_cast( + ::sqlite3_column_text16(mDBStatement, aIndex)); + nsDependentString str( + value, + ::sqlite3_column_bytes16(mDBStatement, aIndex) / sizeof(char16_t)); + variant = new TextVariant(str); + break; + } + case SQLITE_NULL: + variant = new NullVariant(); + break; + case SQLITE_BLOB: { + int size = ::sqlite3_column_bytes(mDBStatement, aIndex); + const void* data = ::sqlite3_column_blob(mDBStatement, aIndex); + variant = new BlobVariant(std::pair(data, size)); + break; + } + } + NS_ENSURE_TRUE(variant, NS_ERROR_UNEXPECTED); + + variant.forget(_value); + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetBlob(uint32_t aIndex, uint32_t* _size, uint8_t** _blob) { + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED; + + ENSURE_INDEX_VALUE(aIndex, mResultColumnCount); + + if (!mExecuting) return NS_ERROR_UNEXPECTED; + + int size = ::sqlite3_column_bytes(mDBStatement, aIndex); + void* blob = nullptr; + if (size) { + blob = moz_xmemdup(::sqlite3_column_blob(mDBStatement, aIndex), size); + } + + *_blob = static_cast(blob); + *_size = size; + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetBlobAsString(uint32_t aIndex, nsAString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Statement::GetBlobAsUTF8String(uint32_t aIndex, nsACString& aValue) { + return DoGetBlobAsString(this, aIndex, aValue); +} + +NS_IMETHODIMP +Statement::GetSharedUTF8String(uint32_t aIndex, uint32_t* _byteLength, + const char** _value) { + *_value = reinterpret_cast( + ::sqlite3_column_text(mDBStatement, aIndex)); + if (_byteLength) { + *_byteLength = ::sqlite3_column_bytes(mDBStatement, aIndex); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetSharedString(uint32_t aIndex, uint32_t* _byteLength, + const char16_t** _value) { + *_value = static_cast( + ::sqlite3_column_text16(mDBStatement, aIndex)); + if (_byteLength) { + *_byteLength = ::sqlite3_column_bytes16(mDBStatement, aIndex); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetSharedBlob(uint32_t aIndex, uint32_t* _byteLength, + const uint8_t** _blob) { + *_blob = + static_cast(::sqlite3_column_blob(mDBStatement, aIndex)); + if (_byteLength) { + *_byteLength = ::sqlite3_column_bytes(mDBStatement, aIndex); + } + return NS_OK; +} + +NS_IMETHODIMP +Statement::GetIsNull(uint32_t aIndex, bool* _isNull) { + // Get type of Index will check aIndex for us, so we don't have to. + int32_t type; + nsresult rv = GetTypeOfIndex(aIndex, &type); + NS_ENSURE_SUCCESS(rv, rv); + *_isNull = (type == mozIStorageStatement::VALUE_TYPE_NULL); + return NS_OK; +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageBindingParams + +BOILERPLATE_BIND_PROXIES(Statement, + if (!mDBStatement) return NS_ERROR_NOT_INITIALIZED;) + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatement.h b/storage/mozStorageStatement.h new file mode 100644 index 0000000000..e472a804c2 --- /dev/null +++ b/storage/mozStorageStatement.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 mozStorageStatement_h +#define mozStorageStatement_h + +#include "nsString.h" + +#include "nsTArray.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageStatementData.h" +#include "mozIStorageStatement.h" +#include "mozIStorageValueArray.h" +#include "StorageBaseStatementInternal.h" +#include "mozilla/Attributes.h" + +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { +class StatementJSHelper; +class Connection; +class StatementParamsHolder; +class StatementRowHolder; + +class Statement final : public mozIStorageStatement, + public mozIStorageValueArray, + public StorageBaseStatementInternal { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENT + NS_DECL_MOZISTORAGEBASESTATEMENT + NS_DECL_MOZISTORAGEBINDINGPARAMS + // NS_DECL_MOZISTORAGEVALUEARRAY (methods in mozIStorageStatement) + NS_DECL_STORAGEBASESTATEMENTINTERNAL + + Statement(); + + /** + * Initializes the object on aDBConnection by preparing the SQL statement + * given by aSQLStatement. + * + * @param aDBConnection + * The Connection object this statement is associated with. + * @param aNativeConnection + * The native Sqlite connection this statement is associated with. + * @param aSQLStatement + * The SQL statement to prepare that this object will represent. + */ + nsresult initialize(Connection* aDBConnection, sqlite3* aNativeConnection, + const nsACString& aSQLStatement); + + /** + * Obtains the native statement pointer. + */ + inline sqlite3_stmt* nativeStatement() { return mDBStatement; } + + /** + * Obtains and transfers ownership of the array of parameters that are bound + * to this statment. This can be null. + */ + inline already_AddRefed bindingParamsArray() { + return mParamsArray.forget(); + } + + private: + ~Statement(); + + sqlite3_stmt* mDBStatement; + uint32_t mParamCount; + uint32_t mResultColumnCount; + nsTArray mColumnNames; + bool mExecuting; + + // Tracks whether the status for this statement has been recorded since it was + // last reset or created. + bool mQueryStatusRecorded; + // Tracks whether this statement has been executed since it was last reset or + // created. + bool mHasExecuted; + + /** + * @return a pointer to the BindingParams object to use with our Bind* + * method. + */ + mozIStorageBindingParams* getParams(); + + /** + * Records a query status result in telemetry. If a result has already been + * recorded for this statement then this does nothing. Otherwise the result + * is recorded if it is an error or if this is the final result. + */ + void MaybeRecordQueryStatus(int srv, bool isResetting = false); + + /** + * Holds the array of parameters to bind to this statement when we execute + * it asynchronously. + */ + RefPtr mParamsArray; + + /** + * The following two members are only used with the JS helper. They cache + * the row and params objects. + */ + nsMainThreadPtrHandle mStatementParamsHolder; + nsMainThreadPtrHandle mStatementRowHolder; + + /** + * Internal version of finalize that allows us to tell it if it is being + * called from the destructor so it can know not to dispatch events that + * require a reference to us. + * + * @param aDestructing + * Is the destructor calling? + */ + nsresult internalFinalize(bool aDestructing); + + friend class StatementJSHelper; +}; + +inline nsISupports* ToSupports(Statement* p) { + return NS_ISUPPORTS_CAST(mozIStorageStatement*, p); +} + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageStatement_h diff --git a/storage/mozStorageStatementData.h b/storage/mozStorageStatementData.h new file mode 100644 index 0000000000..1008496671 --- /dev/null +++ b/storage/mozStorageStatementData.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 sts=2 et + * 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 mozStorageStatementData_h +#define mozStorageStatementData_h + +#include "sqlite3.h" + +#include "nsTArray.h" +#include "MainThreadUtils.h" + +#include "mozStorageBindingParamsArray.h" +#include "mozStorageConnection.h" +#include "StorageBaseStatementInternal.h" +#include "mozStoragePrivateHelpers.h" + +struct sqlite3_stmt; + +namespace mozilla { +namespace storage { + +class StatementData { + public: + StatementData(sqlite3_stmt* aStatement, + already_AddRefed aParamsArray, + StorageBaseStatementInternal* aStatementOwner) + : mStatement(aStatement), + mParamsArray(aParamsArray), + mQueryStatusRecorded(false), + mStatementOwner(aStatementOwner) { + MOZ_ASSERT(mStatementOwner, "Must have a statement owner!"); + } + StatementData(const StatementData& aSource) + : mStatement(aSource.mStatement), + mParamsArray(aSource.mParamsArray), + mQueryStatusRecorded(false), + mStatementOwner(aSource.mStatementOwner) { + MOZ_ASSERT(mStatementOwner, "Must have a statement owner!"); + } + StatementData() : mStatement(nullptr), mQueryStatusRecorded(false) {} + ~StatementData() { + // We need to ensure that mParamsArray is released on the main thread, + // as the binding arguments may be XPConnect values, which are safe + // to release only on the main thread. + NS_ReleaseOnMainThread("StatementData::mParamsArray", + mParamsArray.forget()); + } + + /** + * Return the sqlite statement, fetching it from the storage statement. In + * the case of AsyncStatements this may actually create the statement + */ + inline int getSqliteStatement(sqlite3_stmt** _stmt) { + if (!mStatement) { + int rc = mStatementOwner->getAsyncStatement(&mStatement); + MaybeRecordQueryStatus(rc); + NS_ENSURE_TRUE(rc == SQLITE_OK, rc); + } + *_stmt = mStatement; + return SQLITE_OK; + } + + operator BindingParamsArray*() const { return mParamsArray; } + + /** + * NULLs out our sqlite3_stmt (it is held by the owner) after reseting it and + * clear all bindings to it. This is expected to occur on the async thread. + */ + inline void reset() { + MOZ_ASSERT(mStatementOwner, "Must have a statement owner!"); + // In the AsyncStatement case we may never have populated mStatement if the + // AsyncExecuteStatements got canceled or a failure occurred in constructing + // the statement. + if (mStatement) { + (void)::sqlite3_reset(mStatement); + (void)::sqlite3_clear_bindings(mStatement); + mStatement = nullptr; + + if (!mQueryStatusRecorded) { + mStatementOwner->getOwner()->RecordQueryStatus(SQLITE_OK); + } + } + } + + /** + * Indicates if this statement has parameters to be bound before it is + * executed. + * + * @return true if the statement has parameters to bind against, false + * otherwise. + */ + inline bool hasParametersToBeBound() const { return !!mParamsArray; } + /** + * Indicates the number of implicit statements generated by this statement + * requiring a transaction for execution. For example a single statement + * with N BindingParams will execute N implicit staments. + * + * @return number of statements requiring a transaction for execution. + * + * @note In the case of AsyncStatements this may actually create the + * statement. + */ + inline uint32_t needsTransaction() { + MOZ_ASSERT(!NS_IsMainThread()); + // Be sure to use the getSqliteStatement helper, since sqlite3_stmt_readonly + // can only analyze prepared statements and AsyncStatements are prepared + // lazily. + sqlite3_stmt* stmt; + int rc = getSqliteStatement(&stmt); + if (SQLITE_OK != rc || ::sqlite3_stmt_readonly(stmt)) { + return 0; + } + return mParamsArray ? mParamsArray->length() : 1; + } + + void MaybeRecordQueryStatus(int srv) { + if (mQueryStatusRecorded || !isErrorCode(srv)) { + return; + } + + mStatementOwner->getOwner()->RecordQueryStatus(srv); + mQueryStatusRecorded = true; + } + + private: + sqlite3_stmt* mStatement; + RefPtr mParamsArray; + bool mQueryStatusRecorded; + + /** + * We hold onto a reference of the statement's owner so it doesn't get + * destroyed out from under us. + */ + nsCOMPtr mStatementOwner; +}; + +} // namespace storage +} // namespace mozilla + +#endif // mozStorageStatementData_h diff --git a/storage/mozStorageStatementJSHelper.cpp b/storage/mozStorageStatementJSHelper.cpp new file mode 100644 index 0000000000..c891e955a2 --- /dev/null +++ b/storage/mozStorageStatementJSHelper.cpp @@ -0,0 +1,258 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "nsIXPConnect.h" +#include "mozStorageStatement.h" +#include "mozStorageService.h" + +#include "nsString.h" +#include "nsServiceManagerUtils.h" + +#include "mozStorageStatementJSHelper.h" + +#include "mozStorageStatementRow.h" +#include "mozStorageStatementParams.h" + +#include "jsapi.h" +#include "js/PropertyAndElement.h" // JS_DefineFunction, JS_DefineProperty, JS_DefinePropertyById +#include "js/Value.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// Global Functions + +static bool stepFunc(JSContext* aCtx, uint32_t argc, JS::Value* _vp) { + JS::CallArgs args = CallArgsFromVp(argc, _vp); + + nsCOMPtr xpc(nsIXPConnect::XPConnect()); + nsCOMPtr wrapper; + + if (!args.thisv().isObject()) { + ::JS_ReportErrorASCII(aCtx, "mozIStorageStatement::step() requires object"); + return false; + } + + JS::Rooted obj(aCtx, &args.thisv().toObject()); + nsresult rv = + xpc->GetWrappedNativeOfJSObject(aCtx, obj, getter_AddRefs(wrapper)); + if (NS_FAILED(rv)) { + ::JS_ReportErrorASCII( + aCtx, "mozIStorageStatement::step() could not obtain native statement"); + return false; + } + +#ifdef DEBUG + { + nsCOMPtr isStatement( + do_QueryInterface(wrapper->Native())); + NS_ASSERTION(isStatement, "How is this not a statement?!"); + } +#endif + + Statement* stmt = static_cast( + static_cast(wrapper->Native())); + + bool hasMore = false; + rv = stmt->ExecuteStep(&hasMore); + if (NS_SUCCEEDED(rv) && !hasMore) { + args.rval().setBoolean(false); + (void)stmt->Reset(); + return true; + } + + if (NS_FAILED(rv)) { + ::JS_ReportErrorASCII(aCtx, + "mozIStorageStatement::step() returned an error"); + return false; + } + + args.rval().setBoolean(hasMore); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +//// StatementJSHelper + +nsresult StatementJSHelper::getRow(Statement* aStatement, JSContext* aCtx, + JSObject* aScopeObj, JS::Value* _row) { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING, + "Invalid state to get the row object - all calls will fail!"); +#endif + + JS::Rooted scope(aCtx, aScopeObj); + + if (!aStatement->mStatementRowHolder) { + dom::GlobalObject global(aCtx, scope); + if (global.Failed()) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr window = + do_QueryInterface(global.GetAsSupports()); + + RefPtr row(new StatementRow(window, aStatement)); + NS_ENSURE_TRUE(row, NS_ERROR_OUT_OF_MEMORY); + + RefPtr rowHolder = new StatementRowHolder(row); + NS_ENSURE_TRUE(rowHolder, NS_ERROR_OUT_OF_MEMORY); + + aStatement->mStatementRowHolder = + new nsMainThreadPtrHolder( + "Statement::mStatementRowHolder", rowHolder); + } + + RefPtr row(aStatement->mStatementRowHolder->Get()); + JSObject* obj = row->WrapObject(aCtx, nullptr); + if (!obj) { + return NS_ERROR_UNEXPECTED; + } + + _row->setObject(*obj); + return NS_OK; +} + +nsresult StatementJSHelper::getParams(Statement* aStatement, JSContext* aCtx, + JSObject* aScopeObj, JS::Value* _params) { + MOZ_ASSERT(NS_IsMainThread()); + +#ifdef DEBUG + int32_t state; + (void)aStatement->GetState(&state); + NS_ASSERTION(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY, + "Invalid state to get the params object - all calls will fail!"); +#endif + + JS::Rooted scope(aCtx, aScopeObj); + + if (!aStatement->mStatementParamsHolder) { + dom::GlobalObject global(aCtx, scope); + if (global.Failed()) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr window = + do_QueryInterface(global.GetAsSupports()); + + RefPtr params(new StatementParams(window, aStatement)); + NS_ENSURE_TRUE(params, NS_ERROR_OUT_OF_MEMORY); + + RefPtr paramsHolder = + new StatementParamsHolder(params); + NS_ENSURE_TRUE(paramsHolder, NS_ERROR_OUT_OF_MEMORY); + + aStatement->mStatementParamsHolder = + new nsMainThreadPtrHolder( + "Statement::mStatementParamsHolder", paramsHolder); + } + + RefPtr params(aStatement->mStatementParamsHolder->Get()); + JSObject* obj = params->WrapObject(aCtx, nullptr); + if (!obj) { + return NS_ERROR_UNEXPECTED; + } + + _params->setObject(*obj); + return NS_OK; +} + +NS_IMETHODIMP_(MozExternalRefCountType) StatementJSHelper::AddRef() { + return 2; +} +NS_IMETHODIMP_(MozExternalRefCountType) StatementJSHelper::Release() { + return 1; +} +NS_INTERFACE_MAP_BEGIN(StatementJSHelper) + NS_INTERFACE_MAP_ENTRY(nsIXPCScriptable) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +//////////////////////////////////////////////////////////////////////////////// +//// nsIXPCScriptable + +#define XPC_MAP_CLASSNAME StatementJSHelper +#define XPC_MAP_QUOTED_CLASSNAME "StatementJSHelper" +#define XPC_MAP_FLAGS \ + (XPC_SCRIPTABLE_WANT_RESOLVE | XPC_SCRIPTABLE_ALLOW_PROP_MODS_DURING_RESOLVE) +#include "xpc_map_end.h" + +NS_IMETHODIMP +StatementJSHelper::Resolve(nsIXPConnectWrappedNative* aWrapper, JSContext* aCtx, + JSObject* aScopeObj, jsid aId, bool* aResolvedp, + bool* _retval) { + if (!aId.isString()) return NS_OK; + + JS::Rooted scope(aCtx, aScopeObj); + JS::Rooted id(aCtx, aId); + +#ifdef DEBUG + { + nsCOMPtr isStatement( + do_QueryInterface(aWrapper->Native())); + NS_ASSERTION(isStatement, "How is this not a statement?!"); + } +#endif + + Statement* stmt = static_cast( + static_cast(aWrapper->Native())); + + JSLinearString* str = id.toLinearString(); + if (::JS_LinearStringEqualsLiteral(str, "step")) { + *_retval = ::JS_DefineFunction(aCtx, scope, "step", stepFunc, 0, + JSPROP_RESOLVING) != nullptr; + *aResolvedp = true; + return NS_OK; + } + + JS::Rooted val(aCtx); + + if (::JS_LinearStringEqualsLiteral(str, "row")) { + nsresult rv = getRow(stmt, aCtx, scope, val.address()); + NS_ENSURE_SUCCESS(rv, rv); + *_retval = ::JS_DefinePropertyById(aCtx, scope, id, val, JSPROP_RESOLVING); + *aResolvedp = true; + return NS_OK; + } + + if (::JS_LinearStringEqualsLiteral(str, "params")) { + nsresult rv = getParams(stmt, aCtx, scope, val.address()); + NS_ENSURE_SUCCESS(rv, rv); + *_retval = ::JS_DefinePropertyById(aCtx, scope, id, val, JSPROP_RESOLVING); + *aResolvedp = true; + return NS_OK; + } + + return NS_OK; +} + +NS_IMPL_ISUPPORTS0(StatementParamsHolder); + +StatementParamsHolder::~StatementParamsHolder() { + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + mParams->mStatement = nullptr; +} + +NS_IMPL_ISUPPORTS0(StatementRowHolder); + +StatementRowHolder::~StatementRowHolder() { + MOZ_ASSERT(NS_IsMainThread()); + // We are considered dead at this point, so any wrappers for row or params + // need to lose their reference to the statement. + mRow->mStatement = nullptr; +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementJSHelper.h b/storage/mozStorageStatementJSHelper.h new file mode 100644 index 0000000000..0061d7d6a0 --- /dev/null +++ b/storage/mozStorageStatementJSHelper.h @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 MOZSTORAGESTATEMENTJSHELPER_H +#define MOZSTORAGESTATEMENTJSHELPER_H + +#include "nsIXPCScriptable.h" + +class Statement; + +namespace mozilla { +namespace storage { + +class StatementParams; +class StatementRow; + +class StatementJSHelper : public nsIXPCScriptable { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIXPCSCRIPTABLE + + private: + nsresult getRow(Statement*, JSContext*, JSObject*, JS::Value*); + nsresult getParams(Statement*, JSContext*, JSObject*, JS::Value*); +}; + +/** + * Wrappers used to clean up the references JS helpers hold to the statement. + * For cycle-avoidance reasons they do not hold reference-counted references, + * so it is important we do this. + */ + +class StatementParamsHolder final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit StatementParamsHolder(StatementParams* aParams) : mParams(aParams) {} + + StatementParams* Get() const { + MOZ_ASSERT(mParams); + return mParams; + } + + private: + virtual ~StatementParamsHolder(); + + RefPtr mParams; +}; + +class StatementRowHolder final : public nsISupports { + public: + NS_DECL_ISUPPORTS + + explicit StatementRowHolder(StatementRow* aRow) : mRow(aRow) {} + + StatementRow* Get() const { + MOZ_ASSERT(mRow); + return mRow; + } + + private: + virtual ~StatementRowHolder(); + + RefPtr mRow; +}; + +} // namespace storage +} // namespace mozilla + +#endif // MOZSTORAGESTATEMENTJSHELPER_H diff --git a/storage/mozStorageStatementParams.cpp b/storage/mozStorageStatementParams.cpp new file mode 100644 index 0000000000..2d29e53a87 --- /dev/null +++ b/storage/mozStorageStatementParams.cpp @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "mozStorageStatementParams.h" + +#include "nsJSUtils.h" +#include "nsString.h" + +#include "jsapi.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/MozStorageStatementParamsBinding.h" +#include "mozStoragePrivateHelpers.h" +#include "mozStorageStatement.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// StatementParams + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(StatementParams, mWindow) + +NS_INTERFACE_TABLE_HEAD(StatementParams) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(StatementParams, nsISupports) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(StatementParams) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(StatementParams) +NS_IMPL_CYCLE_COLLECTING_RELEASE(StatementParams) + +StatementParams::StatementParams(nsPIDOMWindowInner* aWindow, + Statement* aStatement) + : mWindow(aWindow), mStatement(aStatement), mParamCount(0) { + NS_ASSERTION(mStatement != nullptr, "mStatement is null"); + (void)mStatement->GetParameterCount(&mParamCount); +} + +JSObject* StatementParams::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::MozStorageStatementParams_Binding::Wrap(aCx, this, aGivenProto); +} + +void StatementParams::NamedGetter(JSContext* aCx, const nsAString& aName, + bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + // Unfortunately there's no API that lets us return the parameter value. + aFound = false; +} + +void StatementParams::NamedSetter(JSContext* aCx, const nsAString& aName, + JS::Handle aValue, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + NS_ConvertUTF16toUTF8 name(aName); + + // Check to see if there's a parameter with this name. + nsCOMPtr variant(convertJSValToVariant(aCx, aValue)); + if (!variant) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aRv = mStatement->BindByName(name, variant); +} + +void StatementParams::GetSupportedNames(nsTArray& aNames) { + if (!mStatement) { + return; + } + + for (uint32_t i = 0; i < mParamCount; i++) { + // Get the name of our parameter. + nsAutoCString name; + nsresult rv = mStatement->GetParameterName(i, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + // But drop the first character, which is going to be a ':'. + name = Substring(name, 1); + aNames.AppendElement(NS_ConvertUTF8toUTF16(name)); + } +} + +void StatementParams::IndexedGetter(JSContext* aCx, uint32_t aIndex, + bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + // Unfortunately there's no API that lets us return the parameter value. + aFound = false; +} + +void StatementParams::IndexedSetter(JSContext* aCx, uint32_t aIndex, + JS::Handle aValue, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + nsCOMPtr variant(convertJSValToVariant(aCx, aValue)); + if (!variant) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + + aRv = mStatement->BindByIndex(aIndex, variant); +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementParams.h b/storage/mozStorageStatementParams.h new file mode 100644 index 0000000000..6164f9854e --- /dev/null +++ b/storage/mozStorageStatementParams.h @@ -0,0 +1,64 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 MOZSTORAGESTATEMENTPARAMS_H +#define MOZSTORAGESTATEMENTPARAMS_H + +#include "mozilla/Attributes.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace storage { + +class Statement; + +class StatementParams final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StatementParams) + + explicit StatementParams(nsPIDOMWindowInner* aWindow, Statement* aStatement); + + void NamedGetter(JSContext* aCx, const nsAString& aName, bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv); + + void NamedSetter(JSContext* aCx, const nsAString& aName, + JS::Handle aValue, mozilla::ErrorResult& aRv); + + uint32_t Length() const { return mParamCount; } + + void IndexedGetter(JSContext* aCx, uint32_t aIndex, bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv); + + void IndexedSetter(JSContext* aCx, uint32_t aIndex, + JS::Handle aValue, mozilla::ErrorResult& aRv); + + void GetSupportedNames(nsTArray& aNames); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + + private: + ~StatementParams() {} + + nsCOMPtr mWindow; + Statement* mStatement; + uint32_t mParamCount; + + friend class StatementParamsHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif /* MOZSTORAGESTATEMENTPARAMS_H */ diff --git a/storage/mozStorageStatementRow.cpp b/storage/mozStorageStatementRow.cpp new file mode 100644 index 0000000000..7d203d7513 --- /dev/null +++ b/storage/mozStorageStatementRow.cpp @@ -0,0 +1,152 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "nsString.h" + +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/MozStorageStatementRowBinding.h" +#include "mozStorageStatementRow.h" +#include "mozStorageStatement.h" + +#include "jsapi.h" +#include "js/Array.h" // JS::NewArrayObject +#include "js/PropertyAndElement.h" // JS_DefineElement +#include "js/Value.h" + +#include "xpc_make_class.h" + +namespace mozilla { +namespace storage { + +//////////////////////////////////////////////////////////////////////////////// +//// StatementRow + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(StatementRow, mWindow) + +NS_INTERFACE_TABLE_HEAD(StatementRow) + NS_WRAPPERCACHE_INTERFACE_TABLE_ENTRY + NS_INTERFACE_TABLE(StatementRow, nsISupports) + NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(StatementRow) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(StatementRow) +NS_IMPL_CYCLE_COLLECTING_RELEASE(StatementRow) + +StatementRow::StatementRow(nsPIDOMWindowInner* aWindow, Statement* aStatement) + : mWindow(aWindow), mStatement(aStatement) {} + +JSObject* StatementRow::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return dom::MozStorageStatementRow_Binding::Wrap(aCx, this, aGivenProto); +} + +void StatementRow::NamedGetter(JSContext* aCx, const nsAString& aName, + bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv) { + if (!mStatement) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + + nsCString name = NS_ConvertUTF16toUTF8(aName); + + uint32_t idx; + { + nsresult rv = mStatement->GetColumnIndex(name, &idx); + if (NS_FAILED(rv)) { + // It's highly likely that the name doesn't exist, so let the JS engine + // check the prototype chain and throw if that doesn't have the property + // either. + aFound = false; + return; + } + } + + int32_t type; + aRv = mStatement->GetTypeOfIndex(idx, &type); + if (aRv.Failed()) { + return; + } + + switch (type) { + case mozIStorageValueArray::VALUE_TYPE_INTEGER: + case mozIStorageValueArray::VALUE_TYPE_FLOAT: { + double dval; + aRv = mStatement->GetDouble(idx, &dval); + if (aRv.Failed()) { + return; + } + aResult.set(::JS_NumberValue(dval)); + break; + } + case mozIStorageValueArray::VALUE_TYPE_TEXT: { + uint32_t bytes; + const char16_t* sval = reinterpret_cast( + static_cast(mStatement) + ->AsSharedWString(idx, &bytes)); + JSString* str = + ::JS_NewUCStringCopyN(aCx, sval, bytes / sizeof(char16_t)); + if (!str) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + aResult.setString(str); + break; + } + case mozIStorageValueArray::VALUE_TYPE_BLOB: { + uint32_t length; + const uint8_t* blob = static_cast(mStatement) + ->AsSharedBlob(idx, &length); + JS::Rooted obj(aCx, JS::NewArrayObject(aCx, length)); + if (!obj) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + aResult.setObject(*obj); + + // Copy the blob over to the JS array. + for (uint32_t i = 0; i < length; i++) { + if (!::JS_DefineElement(aCx, obj, i, blob[i], JSPROP_ENUMERATE)) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + } + break; + } + case mozIStorageValueArray::VALUE_TYPE_NULL: + aResult.setNull(); + break; + default: + NS_ERROR("unknown column type returned, what's going on?"); + break; + } + aFound = true; +} + +void StatementRow::GetSupportedNames(nsTArray& aNames) { + if (!mStatement) { + return; + } + + uint32_t columnCount; + nsresult rv = mStatement->GetColumnCount(&columnCount); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + for (uint32_t i = 0; i < columnCount; i++) { + nsAutoCString name; + nsresult rv = mStatement->GetColumnName(i, name); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + aNames.AppendElement(NS_ConvertUTF8toUTF16(name)); + } +} + +} // namespace storage +} // namespace mozilla diff --git a/storage/mozStorageStatementRow.h b/storage/mozStorageStatementRow.h new file mode 100644 index 0000000000..319645638d --- /dev/null +++ b/storage/mozStorageStatementRow.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 MOZSTORAGESTATEMENTROW_H +#define MOZSTORAGESTATEMENTROW_H + +#include "mozilla/Attributes.h" +#include "nsPIDOMWindow.h" +#include "nsWrapperCache.h" + +namespace mozilla { +class ErrorResult; + +namespace storage { + +class Statement; + +class StatementRow final : public nsISupports, public nsWrapperCache { + public: + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(StatementRow) + + explicit StatementRow(nsPIDOMWindowInner* aWindow, Statement* aStatement); + + void NamedGetter(JSContext* aCx, const nsAString& aName, bool& aFound, + JS::MutableHandle aResult, + mozilla::ErrorResult& aRv); + void GetSupportedNames(nsTArray& aNames); + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + nsPIDOMWindowInner* GetParentObject() const { return mWindow; } + + private: + ~StatementRow() {} + + nsCOMPtr mWindow; + Statement* mStatement; + + friend class StatementRowHolder; +}; + +} // namespace storage +} // namespace mozilla + +#endif /* MOZSTORAGESTATEMENTROW_H */ diff --git a/storage/rust/Cargo.toml b/storage/rust/Cargo.toml new file mode 100644 index 0000000000..bf65f94146 --- /dev/null +++ b/storage/rust/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "storage" +description = "Rust bindings for mozStorage." +version = "0.1.0" +authors = ["Lina Cambridge "] +edition = "2018" +license = "MPL-2.0" + +[dependencies] +libc = "0.2" +nserror = { path = "../../xpcom/rust/nserror" } +nsstring = { path = "../../xpcom/rust/nsstring" } +storage_variant = { path = "../variant" } +xpcom = { path = "../../xpcom/rust/xpcom" } diff --git a/storage/rust/src/lib.rs b/storage/rust/src/lib.rs new file mode 100644 index 0000000000..91984bcafc --- /dev/null +++ b/storage/rust/src/lib.rs @@ -0,0 +1,533 @@ +/* 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/. */ + +//! A Rust wrapper for mozStorage. +//! +//! mozStorage wraps the SQLite C API with support for XPCOM data structures, +//! asynchronous statement execution, cleanup on shutdown, and connection +//! cloning that propagates attached databases, pragmas, functions, and +//! temporary entities. It also collects timing and memory usage stats for +//! telemetry, and supports detailed statement logging. Additionally, mozStorage +//! makes it possible to use the same connection handle from JS and native +//! (C++ and Rust) code. +//! +//! Most mozStorage objects, like connections, statements, result rows, +//! and variants, are thread-safe. Each connection manages a background +//! thread that can be used to execute statements asynchronously, without +//! blocking the main thread. +//! +//! This crate provides a thin wrapper to make mozStorage easier to use +//! from Rust. It only wraps the synchronous API, so you can either manage +//! the entire connection from a background thread, or use the `moz_task` +//! crate to dispatch tasks to the connection's async thread. Executing +//! synchronous statements on the main thread is not supported, and will +//! assert in debug builds. + +#![allow(non_snake_case)] + +use std::{borrow::Cow, convert::TryFrom, error, fmt, ops::Deref, result}; + +use nserror::{nsresult, NS_ERROR_NO_INTERFACE, NS_ERROR_UNEXPECTED}; +use nsstring::nsCString; +use storage_variant::VariantType; +use xpcom::{ + getter_addrefs, + interfaces::{ + mozIStorageAsyncConnection, mozIStorageConnection, mozIStorageStatement, nsIEventTarget, + nsIThread, + }, + RefPtr, XpCom, +}; + +const SQLITE_OK: i32 = 0; + +pub type Result = result::Result; + +/// `Conn` wraps a `mozIStorageConnection`. +#[derive(Clone)] +pub struct Conn { + handle: RefPtr, +} + +// This is safe as long as our `mozIStorageConnection` is an instance of +// `mozilla::storage::Connection`, which is atomically reference counted. +unsafe impl Send for Conn {} +unsafe impl Sync for Conn {} + +impl Conn { + /// Wraps a `mozIStorageConnection` in a `Conn`. + #[inline] + pub fn wrap(connection: RefPtr) -> Conn { + Conn { handle: connection } + } + + /// Returns the wrapped `mozIStorageConnection`. + #[inline] + pub fn connection(&self) -> &mozIStorageConnection { + &self.handle + } + + /// Returns the maximum number of bound parameters for statements executed + /// on this connection. + pub fn variable_limit(&self) -> Result { + let mut limit = 0i32; + let rv = unsafe { self.handle.GetVariableLimit(&mut limit) }; + if rv.failed() { + return Err(Error::Limit); + } + usize::try_from(limit).map_err(|_| Error::Limit) + } + + /// Returns the async thread for this connection. This can be used + /// with `moz_task` to run synchronous statements on the storage + /// thread, without blocking the main thread. + pub fn thread(&self) -> Result> { + let target = self.handle.get_interface::(); + target + .and_then(|t| t.query_interface::()) + .ok_or(Error::NoThread) + } + + /// Prepares a SQL statement. `query` should only contain one SQL statement. + /// If `query` contains multiple statements, only the first will be prepared, + /// and the rest will be ignored. + pub fn prepare>(&self, query: Q) -> Result { + let statement = self.call_and_wrap_error(DatabaseOp::Prepare, || { + getter_addrefs(|p| unsafe { + self.handle + .CreateStatement(&*nsCString::from(query.as_ref()), p) + }) + })?; + Ok(Statement { + conn: self, + handle: statement, + }) + } + + /// Executes a SQL statement. `query` may contain one or more + /// semicolon-separated SQL statements. + pub fn exec>(&self, query: Q) -> Result<()> { + self.call_and_wrap_error(DatabaseOp::Exec, || { + unsafe { + self.handle + .ExecuteSimpleSQL(&*nsCString::from(query.as_ref())) + } + .to_result() + }) + } + + /// Opens a transaction with the default transaction behavior for this + /// connection. The transaction should be committed when done. Uncommitted + /// `Transaction`s will automatically roll back when they go out of scope. + pub fn transaction(&mut self) -> Result { + let behavior = self.get_default_transaction_behavior(); + Transaction::new(self, behavior) + } + + /// Indicates if a transaction is currently open on this connection. + /// Attempting to open a new transaction when one is already in progress + /// will fail with a "cannot start a transaction within a transaction" + /// error. + /// + /// Note that this is `true` even if the transaction was started by another + /// caller, like `Sqlite.sys.mjs` or `mozStorageTransaction` from C++. See the + /// explanation above `mozIStorageConnection.transactionInProgress` for why + /// this matters. + pub fn transaction_in_progress(&self) -> Result { + let mut in_progress = false; + unsafe { self.handle.GetTransactionInProgress(&mut in_progress) }.to_result()?; + Ok(in_progress) + } + + /// Opens a transaction with the requested behavior. + pub fn transaction_with_behavior( + &mut self, + behavior: TransactionBehavior, + ) -> Result { + Transaction::new(self, behavior) + } + + fn get_default_transaction_behavior(&self) -> TransactionBehavior { + let mut typ = 0i32; + let rv = unsafe { self.handle.GetDefaultTransactionType(&mut typ) }; + if rv.failed() { + return TransactionBehavior::Deferred; + } + match typ { + mozIStorageAsyncConnection::TRANSACTION_IMMEDIATE => TransactionBehavior::Immediate, + mozIStorageAsyncConnection::TRANSACTION_EXCLUSIVE => TransactionBehavior::Exclusive, + _ => TransactionBehavior::Deferred, + } + } + + /// Invokes a storage operation and returns the last SQLite error if the + /// operation fails. This lets `Conn::{prepare, exec}` and + /// `Statement::{step, execute}` return more detailed errors, as the + /// `nsresult` codes that mozStorage uses are often too generic. For + /// example, `NS_ERROR_FAILURE` might be anything from a SQL syntax error + /// to an invalid column name in a trigger. + /// + /// Note that the last error may not be accurate if the underlying + /// `mozIStorageConnection` is used concurrently from multiple threads. + /// Multithreaded callers that share a connection should serialize their + /// uses. + fn call_and_wrap_error( + &self, + op: DatabaseOp, + func: impl FnOnce() -> result::Result, + ) -> Result { + func().or_else(|rv| -> Result { + let mut code = 0i32; + unsafe { self.handle.GetLastError(&mut code) }.to_result()?; + Err(if code != SQLITE_OK { + let mut message = nsCString::new(); + unsafe { self.handle.GetLastErrorString(&mut *message) }.to_result()?; + Error::Database { + rv, + op, + code, + message, + } + } else { + rv.into() + }) + }) + } +} + +pub enum TransactionBehavior { + Deferred, + Immediate, + Exclusive, +} + +pub struct Transaction<'c> { + conn: &'c mut Conn, + active: bool, +} + +impl<'c> Transaction<'c> { + /// Opens a transaction on `conn` with the given `behavior`. + fn new(conn: &'c mut Conn, behavior: TransactionBehavior) -> Result> { + conn.exec(match behavior { + TransactionBehavior::Deferred => "BEGIN DEFERRED", + TransactionBehavior::Immediate => "BEGIN IMMEDIATE", + TransactionBehavior::Exclusive => "BEGIN EXCLUSIVE", + })?; + Ok(Transaction { conn, active: true }) + } + + /// Commits the transaction. + pub fn commit(mut self) -> Result<()> { + if self.active { + self.conn.exec("COMMIT")?; + self.active = false; + } + Ok(()) + } + + /// Rolls the transaction back. + pub fn rollback(mut self) -> Result<()> { + self.abort() + } + + fn abort(&mut self) -> Result<()> { + if self.active { + self.conn.exec("ROLLBACK")?; + self.active = false; + } + Ok(()) + } +} + +impl<'c> Deref for Transaction<'c> { + type Target = Conn; + + fn deref(&self) -> &Conn { + self.conn + } +} + +impl<'c> Drop for Transaction<'c> { + fn drop(&mut self) { + let _ = self.abort(); + } +} + +pub struct Statement<'c> { + conn: &'c Conn, + handle: RefPtr, +} + +impl<'c> Statement<'c> { + /// Binds a parameter at the given `index` to the prepared statement. + /// `value` is any type that can be converted into a `Variant`. + pub fn bind_by_index(&mut self, index: u32, value: V) -> Result<()> { + let variant = value.into_variant(); + unsafe { self.handle.BindByIndex(index as u32, variant.coerce()) } + .to_result() + .map_err(|rv| Error::BindByIndex { + rv, + data_type: V::type_name(), + index, + }) + } + + /// Binds a parameter with the given `name` to the prepared statement. + pub fn bind_by_name, V: VariantType>(&mut self, name: N, value: V) -> Result<()> { + let name = name.as_ref(); + let variant = value.into_variant(); + unsafe { + self.handle + .BindByName(&*nsCString::from(name), variant.coerce()) + } + .to_result() + .map_err(|rv| Error::BindByName { + rv, + data_type: V::type_name(), + name: name.into(), + }) + } + + /// Executes the statement and returns the next row of data. + pub fn step<'s>(&'s mut self) -> Result>> { + let has_more = self.conn.call_and_wrap_error(DatabaseOp::Step, || { + let mut has_more = false; + unsafe { self.handle.ExecuteStep(&mut has_more) }.to_result()?; + Ok(has_more) + })?; + Ok(if has_more { Some(Step(self)) } else { None }) + } + + /// Executes the statement once, discards any data, and resets the + /// statement. + pub fn execute(&mut self) -> Result<()> { + self.conn.call_and_wrap_error(DatabaseOp::Execute, || { + unsafe { self.handle.Execute() }.to_result() + }) + } + + /// Resets the prepared statement so that it's ready to be executed + /// again, and clears any bound parameters. + pub fn reset(&mut self) -> Result<()> { + unsafe { self.handle.Reset() }.to_result()?; + Ok(()) + } + + fn get_column_index(&self, name: &str) -> Result { + let mut index = 0u32; + let rv = unsafe { + self.handle + .GetColumnIndex(&*nsCString::from(name), &mut index) + }; + if rv.succeeded() { + Ok(index) + } else { + Err(Error::InvalidColumn { + rv, + name: name.into(), + }) + } + } + + fn get_column_value(&self, index: u32) -> result::Result { + let variant = getter_addrefs(|p| unsafe { self.handle.GetVariant(index, p) })?; + let value = T::from_variant(variant.coerce())?; + Ok(value) + } +} + +impl<'c> Drop for Statement<'c> { + fn drop(&mut self) { + unsafe { self.handle.Finalize() }; + } +} + +/// A step is the next row in the result set for a statement. +pub struct Step<'c, 's>(&'s mut Statement<'c>); + +impl<'c, 's> Step<'c, 's> { + /// Returns the value of the column at `index` for the current row. + pub fn get_by_index(&self, index: u32) -> Result { + self.0 + .get_column_value(index) + .map_err(|rv| Error::GetByIndex { + rv, + data_type: T::type_name(), + index, + }) + } + + /// A convenience wrapper that returns the default value for the column + /// at `index` if `NULL`. + pub fn get_by_index_or_default(&self, index: u32) -> T { + self.get_by_index(index).unwrap_or_default() + } + + /// Returns the value of the column specified by `name` for the current row. + pub fn get_by_name, T: VariantType>(&self, name: N) -> Result { + let name = name.as_ref(); + let index = self.0.get_column_index(name)?; + self.0 + .get_column_value(index) + .map_err(|rv| Error::GetByName { + rv, + data_type: T::type_name(), + name: name.into(), + }) + } + + /// Returns the default value for the column with the given `name`, or the + /// default if the column is `NULL`. + pub fn get_by_name_or_default, T: VariantType + Default>(&self, name: N) -> T { + self.get_by_name(name).unwrap_or_default() + } +} + +/// A database operation, included for better context in error messages. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum DatabaseOp { + Exec, + Prepare, + Step, + Execute, +} + +impl DatabaseOp { + /// Returns a description of the operation to include in an error message. + pub fn what(&self) -> &'static str { + match self { + DatabaseOp::Exec => "execute SQL string", + DatabaseOp::Prepare => "prepare statement", + DatabaseOp::Step => "step statement", + DatabaseOp::Execute => "execute statement", + } + } +} + +/// Storage errors. +#[derive(Debug)] +pub enum Error { + /// A connection doesn't have a usable async thread. The connection might be + /// closed, or the thread manager may have shut down. + NoThread, + + /// Failed to get a limit for a database connection. + Limit, + + /// A database operation failed. The error includes a SQLite result code, + /// and an explanation string. + Database { + rv: nsresult, + op: DatabaseOp, + code: i32, + message: nsCString, + }, + + /// A parameter with the given data type couldn't be bound at this index, + /// likely because the index is out of range. + BindByIndex { + rv: nsresult, + data_type: Cow<'static, str>, + index: u32, + }, + + /// A parameter with the given type couldn't be bound to this name, likely + /// because the statement doesn't have a matching `:`-prefixed parameter + /// with the name. + BindByName { + rv: nsresult, + data_type: Cow<'static, str>, + name: String, + }, + + /// A column with this name doesn't exist. + InvalidColumn { rv: nsresult, name: String }, + + /// A value of the given type couldn't be accessed at this index. This is + /// the error returned when a type conversion fails; for example, requesting + /// an `nsString` instead of an `Option` when the column is `NULL`. + GetByIndex { + rv: nsresult, + data_type: Cow<'static, str>, + index: u32, + }, + + /// A value of the given type couldn't be accessed for the column with + /// this name. + GetByName { + rv: nsresult, + data_type: Cow<'static, str>, + name: String, + }, + + /// A storage operation failed for other reasons. + Other(nsresult), +} + +impl error::Error for Error { + fn source(&self) -> Option<&(dyn error::Error + 'static)> { + None + } +} + +impl From for Error { + fn from(rv: nsresult) -> Error { + Error::Other(rv) + } +} + +impl From for nsresult { + fn from(err: Error) -> nsresult { + match err { + Error::NoThread => NS_ERROR_NO_INTERFACE, + Error::Limit => NS_ERROR_UNEXPECTED, + Error::Database { rv, .. } + | Error::BindByIndex { rv, .. } + | Error::BindByName { rv, .. } + | Error::InvalidColumn { rv, .. } + | Error::GetByIndex { rv, .. } + | Error::GetByName { rv, .. } + | Error::Other(rv) => rv, + } + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Error::NoThread => f.write_str("Async thread unavailable for storage connection"), + Error::Limit => f.write_str("Failed to get limit for storage connection"), + Error::Database { + op, code, message, .. + } => { + if message.is_empty() { + write!(f, "Failed to {} with code {}", op.what(), code) + } else { + write!( + f, + "Failed to {} with code {} ({})", + op.what(), + code, + message + ) + } + } + Error::BindByIndex { + data_type, index, .. + } => write!(f, "Can't bind {} at {}", data_type, index), + Error::BindByName { + data_type, name, .. + } => write!(f, "Can't bind {} to named parameter {}", data_type, name), + Error::InvalidColumn { name, .. } => write!(f, "Column {} doesn't exist", name), + Error::GetByIndex { + data_type, index, .. + } => write!(f, "Can't get {} at {}", data_type, index), + Error::GetByName { + data_type, name, .. + } => write!(f, "Can't get {} for column {}", data_type, name), + Error::Other(rv) => write!(f, "Storage operation failed with {}", rv.error_name()), + } + } +} diff --git a/storage/storage.h b/storage/storage.h new file mode 100644 index 0000000000..6c47755d88 --- /dev/null +++ b/storage/storage.h @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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_storage_h_ +#define mozilla_storage_h_ + +//////////////////////////////////////////////////////////////////////////////// +//// Public Interfaces + +#include "mozStorageCID.h" +#include "mozIStorageStatement.h" +#include "mozIStorageBindingParams.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Native Language Helpers + +#include "mozStorageHelper.h" +#include "mozilla/storage/StatementCache.h" +#include "mozilla/storage/Variant.h" + +#endif // mozilla_storage_h_ diff --git a/storage/style.txt b/storage/style.txt new file mode 100644 index 0000000000..03652e6066 --- /dev/null +++ b/storage/style.txt @@ -0,0 +1,141 @@ +Storage Module Style Guidelines + +These guidelines should be followed for all new code in this module. Reviewers +will be enforcing them, so please obey them! + +* All code should be contained within the namespace mozilla::storage at a + minimum. The use of namespaces is strongly encouraged. + +* All functions being called in the global namespace should be prefixed with + "::" to indicate that they are in the global namespace. + +* The indentation level to use in source code is two spaces. No tabs, please! + +* All files should have the following emacs and vim mode lines: + -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + +* All functions that are not XPCOM should start with a lowercase letter. + +* Function arguments that are not out parameters should be prefixed with a (for + pArameter), and use CamelCase. + +* Function arguments that are out parameters should be prefixed with an + underscore and have a descriptive name. + +* Function declarations should include javadoc style comments. + +* Javadoc @param tags should have the parameter description start on a new line + aligned with the variable name. See the example below. + +* Javadoc @return (note: non-plural) continuation lines should be lined up with + the initial comment. See the example below. + +* Javadoc @throws, like @param, should have the exception type on the same line + as the @throws and the description on a new line indented to line up with + the type of the exception. + +* For function implementations, each argument should be on its own line. + +* All variables should use camelCase. + +* The use of bool is encouraged whenever the variable does not have the + potential to go through xpconnect. + +* For pointer variable types, include a space after the type before the asterisk + and no space between the asterisk and variable name. + +* If any part of an if-else block requires braces, all blocks need braces. + +* Every else should be on a newline after a brace. + +* Bracing should start on the line after a function and class definition. This + goes for JavaScript code as well as C++ code. + +* If a return value is not going to be checked, the return value should be + explicitly casted to void (C style cast). + + +BIG EXAMPLE: + +*** Header *** + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* 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_storage_FILENAME_h_ +#define mozilla_storage_FILENAME_h_ + +namespace mozilla { +namespace storage { + +class Foo : public Bar + , public Baz +{ +public: + /** + * Brief function summary. + * + * @param aArg1 + * Description description description description description etc etc + * next line of description. + * @param aArg2 + * Description description description. + * @return Description description description description description etc etc + * next line of description. + * + * @throws NS_ERROR_FAILURE + * Okay, so this is for JavaScript code, but you probably get the + * idea. + */ + int chew(int aArg1, int aArg2); +}; + +} // storage +} // mozilla + +#endif // mozilla_storage_FILENAME_h_ + + +*** Implementation *** + +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* 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/. */ + +NS_IMPL_ISUPPORTS( + Foo +, IBar +, IBaz +) + +Foo::Foo( + LongArgumentLineThatWouldOtherwiseOverflow *aArgument1 +) +: mField1(0) +, mField2(0) +{ + someMethodWithLotsOfParamsOrJustLongParameters( + mLongFieldNameThatIsJustified, + mMaybeThisOneIsLessJustifiedButBoyIsItLong, + 15 + ); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Separate sections of the file like this + +int +Foo::chew(int aArg1, int aArg2) +{ + (void)functionReturningAnIgnoredValue(); + + ::functionFromGlobalNamespaceWithVoidReturnValue(); + + return 0; +} diff --git a/storage/test/gtest/moz.build b/storage/test/gtest/moz.build new file mode 100644 index 0000000000..db15c440f8 --- /dev/null +++ b/storage/test/gtest/moz.build @@ -0,0 +1,40 @@ +# -*- 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/. + +UNIFIED_SOURCES += [ + "storage_test_harness.cpp", + "test_AsXXX_helpers.cpp", + "test_async_callbacks_with_spun_event_loops.cpp", + "test_async_thread_naming.cpp", + "test_asyncStatementExecution_transaction.cpp", + "test_binding_params.cpp", + "test_file_perms.cpp", + "test_interruptSynchronousConnection.cpp", + "test_mutex.cpp", + "test_spinningSynchronousClose.cpp", + "test_statement_scoper.cpp", + "test_StatementCache.cpp", + "test_transaction_helper.cpp", + "test_true_async.cpp", + "test_unlock_notify.cpp", +] + +if ( + CONFIG["MOZ_DEBUG"] + and CONFIG["OS_ARCH"] not in ("WINNT") + and CONFIG["OS_TARGET"] != "Android" +): + # FIXME bug 523392: test_deadlock_detector doesn't like Windows + # Bug 1054249: Doesn't work on Android + UNIFIED_SOURCES += [ + "test_deadlock_detector.cpp", + ] + +LOCAL_INCLUDES += [ + "../..", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/storage/test/gtest/storage_test_harness.cpp b/storage/test/gtest/storage_test_harness.cpp new file mode 100644 index 0000000000..29caa1792b --- /dev/null +++ b/storage/test/gtest/storage_test_harness.cpp @@ -0,0 +1,244 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 storage_test_harness_ +#define storage_test_harness_ + +#include "storage_test_harness.h" + +already_AddRefed getService() { + nsCOMPtr ss = + do_CreateInstance("@mozilla.org/storage/service;1"); + do_check_true(ss); + return ss.forget(); +} + +already_AddRefed getMemoryDatabase() { + nsCOMPtr ss = getService(); + nsCOMPtr conn; + nsresult rv = ss->OpenSpecialDatabase( + kMozStorageMemoryStorageKey, VoidCString(), + mozIStorageService::CONNECTION_DEFAULT, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +already_AddRefed getDatabase(nsIFile* aDBFile, + uint32_t aConnectionFlags) { + nsCOMPtr dbFile; + nsresult rv; + if (!aDBFile) { + MOZ_RELEASE_ASSERT(NS_IsMainThread(), "Can't get tmp dir off mainthread."); + (void)NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile)); + NS_ASSERTION(dbFile, "The directory doesn't exists?!"); + + rv = dbFile->Append(u"storage_test_db.sqlite"_ns); + do_check_success(rv); + } else { + dbFile = aDBFile; + } + + nsCOMPtr ss = getService(); + nsCOMPtr conn; + rv = ss->OpenDatabase(dbFile, aConnectionFlags, getter_AddRefs(conn)); + do_check_success(rv); + return conn.forget(); +} + +NS_IMPL_ISUPPORTS(AsyncStatementSpinner, mozIStorageStatementCallback, + mozIStorageCompletionCallback) + +AsyncStatementSpinner::AsyncStatementSpinner() + : completionReason(0), mCompleted(false) {} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleResult(mozIStorageResultSet* aResultSet) { + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleError(mozIStorageError* aError) { + int32_t result; + nsresult rv = aError->GetResult(&result); + NS_ENSURE_SUCCESS(rv, rv); + nsAutoCString message; + rv = aError->GetMessage(message); + NS_ENSURE_SUCCESS(rv, rv); + + nsAutoCString warnMsg; + warnMsg.AppendLiteral( + "An error occurred while executing an async statement: "); + warnMsg.AppendInt(result); + warnMsg.Append(' '); + warnMsg.Append(message); + NS_WARNING(warnMsg.get()); + + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::HandleCompletion(uint16_t aReason) { + completionReason = aReason; + mCompleted = true; + return NS_OK; +} + +NS_IMETHODIMP +AsyncStatementSpinner::Complete(nsresult, nsISupports*) { + mCompleted = true; + return NS_OK; +} + +void AsyncStatementSpinner::SpinUntilCompleted() { + nsCOMPtr thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +NS_IMPL_ISUPPORTS(AsyncCompletionSpinner, mozIStorageCompletionCallback) + +AsyncCompletionSpinner::AsyncCompletionSpinner() + : mCompletionReason(NS_OK), mCompleted(false) {} + +NS_IMETHODIMP +AsyncCompletionSpinner::Complete(nsresult reason, nsISupports* value) { + mCompleted = true; + mCompletionReason = reason; + mCompletionValue = value; + return NS_OK; +} + +void AsyncCompletionSpinner::SpinUntilCompleted() { + nsCOMPtr thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!mCompleted && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt) { + RefPtr spinner(new AsyncStatementSpinner()); + + nsCOMPtr pendy; + (void)stmt->ExecuteAsync(spinner, getter_AddRefs(pendy)); + spinner->SpinUntilCompleted(); +} + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db) { + RefPtr spinner(new AsyncStatementSpinner()); + + db->AsyncClose(spinner); + spinner->SpinUntilCompleted(); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread* watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +nsIThread* last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex) { + if (PR_GetCurrentThread() == watched_thread) { + mutex_used_on_watched_thread = true; + } else { + last_non_watched_thread = NS_GetCurrentThread(); + } + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex) { + if (::PR_GetCurrentThread() == watched_thread) { + mutex_used_on_watched_thread = true; + } + return orig_mutex_methods.xMutexTry(mutex); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() { + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed get_conn_async_thread(mozIStorageConnection* db) { + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT 1"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr asyncThread = last_non_watched_thread; + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr target = do_GetInterface(db); + nsCOMPtr allegedAsyncThread = do_QueryInterface(target); + do_check_eq(allegedAsyncThread, asyncThread); + return asyncThread.forget(); +} + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/storage_test_harness.h b/storage/test/gtest/storage_test_harness.h new file mode 100644 index 0000000000..6f29dc43ad --- /dev/null +++ b/storage/test/gtest/storage_test_harness.h @@ -0,0 +1,223 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 storage_test_harness_h__ +#define storage_test_harness_h__ + +#include "gtest/gtest.h" + +#include "prthread.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsComponentManagerUtils.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsServiceManagerUtils.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" +#include "mozilla/ReentrantMonitor.h" + +#include "mozIStorageService.h" +#include "mozIStorageConnection.h" +#include "mozIStorageStatementCallback.h" +#include "mozIStorageCompletionCallback.h" +#include "mozIStorageBindingParamsArray.h" +#include "mozIStorageBindingParams.h" +#include "mozIStorageAsyncStatement.h" +#include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" +#include "mozIStorageError.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" + +#define do_check_true(aCondition) EXPECT_TRUE(aCondition) + +#define do_check_false(aCondition) EXPECT_FALSE(aCondition) + +#define do_check_success(aResult) do_check_true(NS_SUCCEEDED(aResult)) + +#define do_check_eq(aExpected, aActual) do_check_true(aExpected == aActual) + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +already_AddRefed getService(); + +already_AddRefed getMemoryDatabase(); + +already_AddRefed getDatabase( + nsIFile* aDBFile = nullptr, + uint32_t aConnectionFlags = mozIStorageService::CONNECTION_DEFAULT); + +class AsyncStatementSpinner : public mozIStorageStatementCallback, + public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGESTATEMENTCALLBACK + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncStatementSpinner(); + + void SpinUntilCompleted(); + + uint16_t completionReason; + + protected: + virtual ~AsyncStatementSpinner() = default; + volatile bool mCompleted; +}; + +class AsyncCompletionSpinner : public mozIStorageCompletionCallback { + public: + NS_DECL_ISUPPORTS + NS_DECL_MOZISTORAGECOMPLETIONCALLBACK + + AsyncCompletionSpinner(); + + void SpinUntilCompleted(); + + nsresult mCompletionReason; + nsCOMPtr mCompletionValue; + + protected: + virtual ~AsyncCompletionSpinner() = default; + volatile bool mCompleted; +}; + +#define NS_DECL_ASYNCSTATEMENTSPINNER \ + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Execute an async statement, blocking the main thread until we get the + * callback completion notification. + */ +void blocking_async_execute(mozIStorageBaseStatement* stmt); + +/** + * Invoke AsyncClose on the given connection, blocking the main thread until we + * get the completion notification. + */ +void blocking_async_close(mozIStorageConnection* db); + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +extern sqlite3_mutex_methods orig_mutex_methods; +extern sqlite3_mutex_methods wrapped_mutex_methods; + +extern bool mutex_used_on_watched_thread; +extern PRThread* watched_thread; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +extern nsIThread* last_non_watched_thread; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex* mutex); + +extern "C" int wrapped_MutexTry(sqlite3_mutex* mutex); + +class HookSqliteMutex { + public: + HookSqliteMutex() { + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok( + ::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); + } + + ~HookSqliteMutex() { + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &orig_mutex_methods)); + do_check_ok(sqlite3_initialize()); + } +}; + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread(); + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public mozilla::Runnable { + public: + explicit ThreadWedger(nsIEventTarget* aTarget) + : mozilla::Runnable("ThreadWedger"), + mReentrantMonitor("thread wedger"), + unwedged(false) { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) automon.Wait(); + + return NS_OK; + } + + void unwedge() { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + + private: + mozilla::ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed get_conn_async_thread(mozIStorageConnection* db); + +#endif // storage_test_harness_h__ diff --git a/storage/test/gtest/test_AsXXX_helpers.cpp b/storage/test/gtest/test_AsXXX_helpers.cpp new file mode 100644 index 0000000000..2320b8383c --- /dev/null +++ b/storage/test/gtest/test_AsXXX_helpers.cpp @@ -0,0 +1,111 @@ +/* + *Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +#include "storage_test_harness.h" +#include "mozIStorageRow.h" +#include "mozIStorageResultSet.h" +#include "nsComponentManagerUtils.h" + +/** + * This file tests AsXXX (AsInt32, AsInt64, ...) helpers. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Event Loop Spinning + +class Spinner : public AsyncStatementSpinner { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_ASYNCSTATEMENTSPINNER + Spinner() {} + + protected: + ~Spinner() override = default; +}; + +NS_IMPL_ISUPPORTS_INHERITED0(Spinner, AsyncStatementSpinner) + +NS_IMETHODIMP +Spinner::HandleResult(mozIStorageResultSet* aResultSet) { + nsCOMPtr row; + do_check_true(NS_SUCCEEDED(aResultSet->GetNextRow(getter_AddRefs(row))) && + row); + + do_check_eq(row->AsInt32(0), 0); + do_check_eq(row->AsInt64(0), 0); + do_check_eq(row->AsDouble(0), 0.0); + + uint32_t len = 100; + do_check_eq(row->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(row->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + + do_check_eq(row->IsNull(0), true); + return NS_OK; +} + +TEST(storage_AsXXX_helpers, NULLFallback) +{ + nsCOMPtr db(getMemoryDatabase()); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr valueArray = do_QueryInterface(stmt); + do_check_true(valueArray); + + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore); + + do_check_eq(stmt->AsInt32(0), 0); + do_check_eq(stmt->AsInt64(0), 0); + do_check_eq(stmt->AsDouble(0), 0.0); + uint32_t len = 100; + do_check_eq(stmt->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(stmt->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(stmt->IsNull(0), true); + + do_check_eq(valueArray->AsInt32(0), 0); + do_check_eq(valueArray->AsInt64(0), 0); + do_check_eq(valueArray->AsDouble(0), 0.0); + len = 100; + do_check_eq(valueArray->AsSharedUTF8String(0, &len), (const char*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedWString(0, &len), (const char16_t*)nullptr); + do_check_eq(len, 0); + len = 100; + do_check_eq(valueArray->AsSharedBlob(0, &len), (const uint8_t*)nullptr); + do_check_eq(len, 0); + do_check_eq(valueArray->IsNull(0), true); +} + +TEST(storage_AsXXX_helpers, asyncNULLFallback) +{ + nsCOMPtr db(getMemoryDatabase()); + + nsCOMPtr stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + + nsCOMPtr pendingStmt; + do_check_true( + NS_SUCCEEDED(stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt)))); + do_check_true(pendingStmt); + stmt->Finalize(); + RefPtr asyncSpin(new Spinner()); + db->AsyncClose(asyncSpin); + asyncSpin->SpinUntilCompleted(); +} diff --git a/storage/test/gtest/test_StatementCache.cpp b/storage/test/gtest/test_StatementCache.cpp new file mode 100644 index 0000000000..9908a34d79 --- /dev/null +++ b/storage/test/gtest/test_StatementCache.cpp @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "mozilla/Attributes.h" +#include "mozilla/storage/StatementCache.h" +using namespace mozilla::storage; + +/** + * This file test our statement cache in StatementCache.h. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +class SyncCache : public StatementCache { + public: + explicit SyncCache(nsCOMPtr& aConnection) + : StatementCache(aConnection) {} +}; + +class AsyncCache : public StatementCache { + public: + explicit AsyncCache(nsCOMPtr& aConnection) + : StatementCache(aConnection) {} +}; + +/** + * Wraps nsCString so we can not implement the same functions twice for each + * type. + */ +class StringWrapper : public nsCString { + public: + MOZ_IMPLICIT StringWrapper(const char* aOther) { this->Assign(aOther); } +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +// This is some gtest magic that allows us to parameterize tests by |const +// char[]| and |StringWrapper|. +template +class storage_StatementCache : public ::testing::Test {}; +typedef ::testing::Types TwoStringTypes; + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedStatement) { + nsCOMPtr db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeStatements) { + nsCOMPtr db(getMemoryDatabase()); + SyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->Close()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, GetCachedAsyncStatement) { + nsCOMPtr db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Make sure we get a statement back with the right state. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_READY, state); + + // Check to make sure we get the same copy the second time we ask. + nsCOMPtr stmt2 = cache.GetCachedStatement(sql); + do_check_true(stmt2); + do_check_eq(stmt.get(), stmt2.get()); +} + +TYPED_TEST_SUITE(storage_StatementCache, TwoStringTypes); +TYPED_TEST(storage_StatementCache, FinalizeAsyncStatements) { + nsCOMPtr db(getMemoryDatabase()); + AsyncCache cache(db); + + TypeParam sql = "SELECT * FROM sqlite_master"; + + // Get a statement, and then tell the cache to finalize. + nsCOMPtr stmt = cache.GetCachedStatement(sql); + do_check_true(stmt); + + cache.FinalizeStatements(); + + // We should be in an invalid state at this point. + int32_t state; + do_check_success(stmt->GetState(&state)); + do_check_eq(mozIStorageBaseStatement::MOZ_STORAGE_STATEMENT_INVALID, state); + + // Should be able to close the database now too. + do_check_success(db->AsyncClose(nullptr)); +} diff --git a/storage/test/gtest/test_asyncStatementExecution_transaction.cpp b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp new file mode 100644 index 0000000000..0e7dec1e8a --- /dev/null +++ b/storage/test/gtest/test_asyncStatementExecution_transaction.cpp @@ -0,0 +1,445 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" + +#include "mozStorageConnection.h" + +#include "sqlite3.h" + +using namespace mozilla; +using namespace mozilla::storage; + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +/** + * Commit hook to detect transactions. + * + * @param aArg + * An integer pointer that will be incremented for each commit. + */ +int commit_hook(void* aArg) { + int* arg = static_cast(aArg); + (*arg)++; + return 0; +} + +/** + * Executes the passed-in statements and checks if a transaction is created. + * When done statements are finalized and database connection is closed. + * + * @param aDB + * The database connection. + * @param aStmts + * Vector of statements. + * @param aStmtsLen + * Number of statements. + * @param aTransactionExpected + * Whether a transaction is expected or not. + */ +void check_transaction(mozIStorageConnection* aDB, + const nsTArray>& aStmts, + bool aTransactionExpected) { + // -- install a transaction commit hook. + int commit = 0; + static_cast(aDB)->setCommitHook(commit_hook, &commit); + + RefPtr asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr asyncPend; + do_check_success( + aDB->ExecuteAsync(aStmts, asyncSpin, getter_AddRefs(asyncPend))); + do_check_true(asyncPend); + + // -- complete the execution + asyncSpin->SpinUntilCompleted(); + + // -- uninstall the transaction commit hook. + static_cast(aDB)->setCommitHook(nullptr); + + // -- check transaction + do_check_eq(aTransactionExpected, !!commit); + + // -- check that only one transaction was created. + if (aTransactionExpected) { + do_check_eq(1, commit); + } + + // -- cleanup + for (uint32_t i = 0; i < aStmts.Length(); ++i) { + aStmts[i]->Finalize(); + } + blocking_async_close(aDB); +} + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +/** + * Test that executing multiple readonly AsyncStatements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncReadStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts.Clone(), false); +} + +/** + * Test that executing multiple readonly Statements doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleAsyncReadWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleReadWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple AsyncStatements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleAsyncWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateAsyncStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateAsyncStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing multiple Statements causing writes creates a + * single transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleWriteStatements) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt1; + db->CreateStatement("CREATE TABLE test1 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt1)); + + nsCOMPtr stmt2; + db->CreateStatement("CREATE TABLE test2 (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt2)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt1)), + ToRefPtr(std::move(stmt2)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement doesn't create a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("SELECT * FROM sqlite_master"_ns, getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single AsyncStatement causing writes creates a + * transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleAsyncWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single Statement causing writes creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, SingleWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single read-only AsyncStatement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single read-only Statement with multiple params + * doesn't create a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsReadStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("SELECT :param FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, false); +} + +/** + * Test that executing a single write AsyncStatement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, + MultipleParamsAsyncWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateAsyncStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} + +/** + * Test that executing a single write Statement with multiple params + * creates a transaction. + */ +TEST(storage_asyncStatementExecution_transaction, MultipleParamsWriteStatement) +{ + nsCOMPtr db(getMemoryDatabase()); + + // -- create a table for writes + nsCOMPtr tableStmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(tableStmt)); + tableStmt->Execute(); + tableStmt->Finalize(); + + // -- create statements and execute them + nsCOMPtr stmt; + db->CreateStatement("DELETE FROM test WHERE id = :param"_ns, + getter_AddRefs(stmt)); + + // -- bind multiple BindingParams + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + for (int32_t i = 0; i < 2; i++) { + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("param"_ns, 1); + paramsArray->AddParams(params); + } + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + + nsTArray> stmts = { + ToRefPtr(std::move(stmt)), + }; + + check_transaction(db, stmts, true); +} diff --git a/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp new file mode 100644 index 0000000000..bd437b61ec --- /dev/null +++ b/storage/test/gtest/test_async_callbacks_with_spun_event_loops.cpp @@ -0,0 +1,146 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +#include "storage_test_harness.h" +#include "prthread.h" +#include "nsIInterfaceRequestorUtils.h" +#include "mozilla/Attributes.h" + +#include "sqlite3.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * Spins the events loop for current thread until aCondition is true. + */ +void spin_events_loop_until_true(const bool* const aCondition) { + nsCOMPtr thread(::do_GetCurrentThread()); + nsresult rv = NS_OK; + bool processed = true; + while (!(*aCondition) && NS_SUCCEEDED(rv)) { + rv = thread->ProcessNextEvent(true, &processed); + } +} + +//////////////////////////////////////////////////////////////////////////////// +//// mozIStorageStatementCallback implementation + +class UnownedCallback final : public mozIStorageStatementCallback { + public: + NS_DECL_ISUPPORTS + + // Whether the object has been destroyed. + static bool sAlive; + // Whether the first result was received. + static bool sResult; + // Whether an error was received. + static bool sError; + + explicit UnownedCallback(mozIStorageConnection* aDBConn) + : mDBConn(aDBConn), mCompleted(false) { + sAlive = true; + sResult = false; + sError = false; + } + + private: + ~UnownedCallback() { + sAlive = false; + blocking_async_close(mDBConn); + } + + public: + NS_IMETHOD HandleResult(mozIStorageResultSet* aResultSet) override { + sResult = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleError(mozIStorageError* aError) override { + sError = true; + spin_events_loop_until_true(&mCompleted); + if (!sAlive) { + MOZ_CRASH("The statement callback was destroyed prematurely."); + } + return NS_OK; + } + + NS_IMETHOD HandleCompletion(uint16_t aReason) override { + mCompleted = true; + return NS_OK; + } + + protected: + nsCOMPtr mDBConn; + bool mCompleted; +}; + +NS_IMPL_ISUPPORTS(UnownedCallback, mozIStorageStatementCallback) + +bool UnownedCallback::sAlive = false; +bool UnownedCallback::sResult = false; +bool UnownedCallback::sError = false; + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_async_callbacks_with_spun_event_loops, + SpinEventsLoopInHandleResult) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + for (int32_t i = 0; i < 30; ++i) { + stmt->BindInt32ByIndex(0, i); + stmt->Execute(); + stmt->Reset(); + } + stmt->Finalize(); + + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + nsCOMPtr ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sResult); +} + +TEST(storage_async_callbacks_with_spun_event_loops, SpinEventsLoopInHandleError) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a test table and populate it. + nsCOMPtr stmt; + db->CreateStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + stmt->Execute(); + stmt->Finalize(); + + // This will cause a constraint error. + db->CreateStatement("INSERT INTO test (id) VALUES (1)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr ps; + do_check_success( + stmt->ExecuteAsync(new UnownedCallback(db), getter_AddRefs(ps))); + stmt->Finalize(); + + spin_events_loop_until_true(&UnownedCallback::sError); +} diff --git a/storage/test/gtest/test_async_thread_naming.cpp b/storage/test/gtest/test_async_thread_naming.cpp new file mode 100644 index 0000000000..77d088f3e5 --- /dev/null +++ b/storage/test/gtest/test_async_thread_naming.cpp @@ -0,0 +1,230 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "nsVariant.h" +#include "storage_test_harness.h" +#include "nsThreadUtils.h" +#include "nsIURI.h" +#include "nsIFileURL.h" +#include "nsIVariant.h" +#include "nsNetUtil.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_async_thread_naming, MemoryDatabase) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + printf("%s", name.get()); + do_check_true(StringBeginsWith(name, "sqldb:memory"_ns)); + + blocking_async_close(db); +} + +TEST(storage_async_thread_naming, FileDatabase) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr ss = getService(); + nsCOMPtr conn; + do_check_success(ss->OpenDatabase(dbFile, 0, getter_AddRefs(conn))); + + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + + { + nsCOMPtr clone; + do_check_success(conn->Clone(true, getter_AddRefs(clone))); + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + blocking_async_close(clone); + } + + blocking_async_close(conn); +} + +TEST(storage_async_thread_naming, FileUnsharedDatabase) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr ss = getService(); + nsCOMPtr conn; + do_check_success(ss->OpenUnsharedDatabase(dbFile, 0, getter_AddRefs(conn))); + + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + + blocking_async_close(conn); +} + +TEST(storage_async_thread_naming, FileURLDatabase) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr uri; + do_check_success(NS_NewFileURI(getter_AddRefs(uri), dbFile)); + nsCOMPtr fileUrl = do_QueryInterface(uri); + nsCOMPtr ss = getService(); + nsCOMPtr conn; + do_check_success(ss->OpenDatabaseWithFileURL(fileUrl, EmptyCString(), 0, + getter_AddRefs(conn))); + + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + + blocking_async_close(conn); +} + +TEST(storage_async_thread_naming, OverrideFileURLDatabase) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr uri; + do_check_success(NS_NewFileURI(getter_AddRefs(uri), dbFile)); + nsCOMPtr fileUrl = do_QueryInterface(uri); + nsCOMPtr ss = getService(); + nsCOMPtr conn; + nsAutoCString override("override"_ns); + do_check_success( + ss->OpenDatabaseWithFileURL(fileUrl, override, 0, getter_AddRefs(conn))); + + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(override); + do_check_true(StringBeginsWith(name, expected)); + + blocking_async_close(conn); +} + +TEST(storage_async_thread_naming, AsyncOpenDatabase) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr ss = getService(); + + RefPtr completionSpinner = + new AsyncCompletionSpinner(); + RefPtr variant = new nsVariant(); + variant->SetAsInterface(NS_GET_IID(nsIFile), dbFile); + do_check_success(ss->OpenAsyncDatabase(variant, 0, 0, completionSpinner)); + completionSpinner->SpinUntilCompleted(); + nsCOMPtr conn( + do_QueryInterface(completionSpinner->mCompletionValue)); + nsCOMPtr target(get_conn_async_thread(conn)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + + blocking_async_close(conn); +} + +TEST(storage_async_thread_naming, AsyncClone) +{ + HookSqliteMutex hook; + + nsAutoString filename(u"test_thread_name.sqlite"_ns); + nsCOMPtr dbFile; + do_check_success(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(dbFile))); + do_check_success(dbFile->Append(filename)); + nsCOMPtr ss = getService(); + + nsCOMPtr conn; + do_check_success(ss->OpenDatabase(dbFile, 0, getter_AddRefs(conn))); + + RefPtr completionSpinner = + new AsyncCompletionSpinner(); + RefPtr variant = new nsVariant(); + variant->SetAsInterface(NS_GET_IID(nsIFile), dbFile); + do_check_success(conn->AsyncClone(true, completionSpinner)); + completionSpinner->SpinUntilCompleted(); + nsCOMPtr clone( + do_QueryInterface(completionSpinner->mCompletionValue)); + nsCOMPtr target(get_conn_async_thread(clone)); + do_check_true(target); + PRThread* prThread; + target->GetPRThread(&prThread); + do_check_true(prThread); + nsAutoCString name(PR_GetThreadName(prThread)); + nsAutoCString expected("sqldb:"_ns); + expected.Append(NS_ConvertUTF16toUTF8(filename)); + do_check_true(StringBeginsWith(name, expected)); + + blocking_async_close(conn); + blocking_async_close(clone); +} diff --git a/storage/test/gtest/test_binding_params.cpp b/storage/test/gtest/test_binding_params.cpp new file mode 100644 index 0000000000..a9ca1799f9 --- /dev/null +++ b/storage/test/gtest/test_binding_params.cpp @@ -0,0 +1,186 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "mozilla/ArrayUtils.h" +#include "mozStorageHelper.h" + +using namespace mozilla; + +/** + * This file tests binding and reading out string parameters through the + * mozIStorageStatement API. + */ + +TEST(storage_binding_params, ASCIIString) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + nsAutoCString inserted("I'm an ASCII string"); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + nsAutoCString result; + { + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + } + + do_check_true(result == inserted); + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, CString) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a string through the table, and ensure it comes out as expected. + static const char sCharArray[] = + "I'm not a \xff\x00\xac\xde\xbb ASCII string!"; + nsAutoCString inserted(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(inserted.Length() == ArrayLength(sCharArray) - 1); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, inserted))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == inserted); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} + +TEST(storage_binding_params, UTFStrings) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create table with a single string column. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (str STRING)"_ns); + + // Create statements to INSERT and SELECT the string. + nsCOMPtr insert, select; + (void)db->CreateStatement("INSERT INTO test (str) VALUES (?1)"_ns, + getter_AddRefs(insert)); + (void)db->CreateStatement("SELECT str FROM test"_ns, getter_AddRefs(select)); + + // Roundtrip a UTF8 string through the table, using UTF8 input and output. + static const char sCharArray[] = R"(I'm a ûüâäç UTF8 string!)"; + nsAutoCString insertedUTF8(sCharArray, ArrayLength(sCharArray) - 1); + do_check_true(insertedUTF8.Length() == ArrayLength(sCharArray) - 1); + NS_ConvertUTF8toUTF16 insertedUTF16(insertedUTF8); + do_check_true(insertedUTF8 == NS_ConvertUTF16toUTF8(insertedUTF16)); + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindUTF8StringByIndex(0, insertedUTF8))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF8 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); + + // Roundtrip the same string using UTF16 input and UTF8 output. + { + mozStorageStatementScoper scoper(insert); + bool hasResult; + do_check_true(NS_SUCCEEDED(insert->BindStringByIndex(0, insertedUTF16))); + do_check_true(NS_SUCCEEDED(insert->ExecuteStep(&hasResult))); + do_check_false(hasResult); + } + + { + nsAutoCString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetUTF8String(0, result))); + + do_check_true(result == insertedUTF8); + } + + // Use UTF16 input and UTF16 output. + { + nsAutoString result; + + mozStorageStatementScoper scoper(select); + bool hasResult; + do_check_true(NS_SUCCEEDED(select->ExecuteStep(&hasResult))); + do_check_true(hasResult); + do_check_true(NS_SUCCEEDED(select->GetString(0, result))); + + do_check_true(result == insertedUTF16); + } + + (void)db->ExecuteSimpleSQL("DELETE FROM test"_ns); +} diff --git a/storage/test/gtest/test_deadlock_detector.cpp b/storage/test/gtest/test_deadlock_detector.cpp new file mode 100644 index 0000000000..2679cf26f1 --- /dev/null +++ b/storage/test/gtest/test_deadlock_detector.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- + * vim: sw=2 ts=4 et : + * 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/. */ + +// Note: This file is essentially a copy of +// xpcom/tests/gtest/TestDeadlockDetector.cpp, but all mutexes were turned into +// SQLiteMutexes. We use #include and some macros to avoid actual source code +// duplication. + +#include "mozilla/CondVar.h" +#include "mozilla/RecursiveMutex.h" +#include "mozilla/ReentrantMonitor.h" +#include "SQLiteMutex.h" + +// We need this one so _gdb_sleep_duration is also in "storage" namespace +#include "mozilla/gtest/MozHelpers.h" + +#include "gtest/gtest.h" + +using namespace mozilla; + +/** + * Helper class to allocate a sqlite3_mutex for our SQLiteMutex. + */ +class TestMutex : public mozilla::storage::SQLiteMutex { + public: + explicit TestMutex(const char* aName) + : mozilla::storage::SQLiteMutex(aName), + mInner(sqlite3_mutex_alloc(SQLITE_MUTEX_FAST)) { + NS_ASSERTION(mInner, "could not allocate a sqlite3_mutex"); + initWithMutex(mInner); + } + + ~TestMutex() { sqlite3_mutex_free(mInner); } + + void Lock() { lock(); } + + void Unlock() { unlock(); } + + private: + sqlite3_mutex* mInner; +}; + +// These are the two macros that differentiate this file from the XPCOM one. +#define MUTEX TestMutex +#define TESTNAME(name) storage_##name + +// Bug 1473531: the test storage_DeadlockDetectorTest.storage_Sanity5DeathTest +// times out on macosx ccov builds +#if defined(XP_MACOSX) && defined(MOZ_CODE_COVERAGE) +# define DISABLE_STORAGE_SANITY5_DEATH_TEST +#endif + +#include "../../../xpcom/tests/gtest/TestDeadlockDetector.cpp" diff --git a/storage/test/gtest/test_file_perms.cpp b/storage/test/gtest/test_file_perms.cpp new file mode 100644 index 0000000000..5e54f082ce --- /dev/null +++ b/storage/test/gtest/test_file_perms.cpp @@ -0,0 +1,35 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" +#include "nsIFile.h" +#include "prio.h" + +/** + * This file tests that the file permissions of the sqlite files match what + * we request they be + */ + +TEST(storage_file_perms, Test) +{ + nsCOMPtr db(getDatabase()); + nsCOMPtr dbFile; + do_check_success(db->GetDatabaseFile(getter_AddRefs(dbFile))); + + uint32_t perms = 0; + do_check_success(dbFile->GetPermissions(&perms)); + + // This reflexts the permissions defined by SQLITE_DEFAULT_FILE_PERMISSIONS in + // third_party/sqlite3/src/Makefile.in and must be kept in sync with that +#ifdef ANDROID + do_check_true(perms == (PR_IRUSR | PR_IWUSR)); +#elif defined(XP_WIN) + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH | + PR_IWOTH)); +#else + do_check_true(perms == (PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IROTH)); +#endif +} diff --git a/storage/test/gtest/test_interruptSynchronousConnection.cpp b/storage/test/gtest/test_interruptSynchronousConnection.cpp new file mode 100644 index 0000000000..dfa19bc86e --- /dev/null +++ b/storage/test/gtest/test_interruptSynchronousConnection.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "mozilla/SpinEventLoopUntil.h" + +class SynchronousConnectionInterruptionTest : public ::testing::Test { + protected: + void SetUp() override { + mConnection = + getDatabase(nullptr, mozIStorageService::CONNECTION_INTERRUPTIBLE); + ASSERT_TRUE(mConnection); + + ASSERT_EQ(NS_OK, NS_NewNamedThread("Test Thread", getter_AddRefs(mThread))); + } + + void TearDown() override { + // We might close the database connection early in test cases. + mozilla::Unused << mConnection->Close(); + + ASSERT_EQ(NS_OK, mThread->Shutdown()); + } + + nsCOMPtr mConnection; + + nsCOMPtr mThread; + + bool mDone = false; +}; + +TEST_F(SynchronousConnectionInterruptionTest, + shouldBeAbleToInterruptInfiniteOperation) { + // Delay is modest because we don't want to get interrupted by + // some unrelated hang or memory guard + const uint32_t delayMs = 500; + + ASSERT_EQ(NS_OK, mThread->DelayedDispatch( + NS_NewRunnableFunction( + "InterruptRunnable", + [this]() { + ASSERT_EQ(NS_OK, mConnection->Interrupt()); + mDone = true; + }), + delayMs)); + + const nsCString infiniteQuery = + "WITH RECURSIVE test(n) " + "AS (VALUES(1) UNION ALL SELECT n + 1 FROM test) " + "SELECT t.n FROM test, test AS t;"_ns; + nsCOMPtr stmt; + ASSERT_EQ(NS_OK, + mConnection->CreateStatement(infiniteQuery, getter_AddRefs(stmt))); + ASSERT_EQ(NS_ERROR_ABORT, stmt->Execute()); + ASSERT_EQ(NS_OK, stmt->Finalize()); + + ASSERT_TRUE(mDone); + + ASSERT_EQ(NS_OK, mConnection->Close()); +} + +TEST_F(SynchronousConnectionInterruptionTest, interruptAfterCloseWillFail) { + ASSERT_EQ(NS_OK, mConnection->Close()); + + ASSERT_EQ( + NS_OK, + mThread->Dispatch(NS_NewRunnableFunction("InterruptRunnable", [this]() { + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Interrupt()); + mDone = true; + }))); + + ASSERT_TRUE(mozilla::SpinEventLoopUntil("interruptAfterCloseWillFail"_ns, + [this]() { return mDone; })); + + ASSERT_EQ(NS_ERROR_NOT_INITIALIZED, mConnection->Close()); +} diff --git a/storage/test/gtest/test_mutex.cpp b/storage/test/gtest/test_mutex.cpp new file mode 100644 index 0000000000..db1a645e09 --- /dev/null +++ b/storage/test/gtest/test_mutex.cpp @@ -0,0 +1,73 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "SQLiteMutex.h" + +using namespace mozilla; +using namespace mozilla::storage; + +/** + * This file test our sqlite3_mutex wrapper in SQLiteMutex.h. + */ + +TEST(storage_mutex, AutoLock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic locking wrapper works as expected. + mutex.assertNotCurrentThreadOwns(); + { + SQLiteMutexAutoLock lockedScope(mutex); + mutex.assertCurrentThreadOwns(); + } + mutex.assertNotCurrentThreadOwns(); + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} + +TEST(storage_mutex, AutoUnlock) +{ + int lockTypes[] = { + SQLITE_MUTEX_FAST, + SQLITE_MUTEX_RECURSIVE, + }; + for (int lockType : lockTypes) { + // Get our test mutex (we have to allocate a SQLite mutex of the right type + // too!). + SQLiteMutex mutex("TestMutex"); + sqlite3_mutex* inner = sqlite3_mutex_alloc(lockType); + do_check_true(inner); + mutex.initWithMutex(inner); + + // And test that our automatic unlocking wrapper works as expected. + { + SQLiteMutexAutoLock lockedScope(mutex); + + { + SQLiteMutexAutoUnlock unlockedScope(mutex); + mutex.assertNotCurrentThreadOwns(); + } + mutex.assertCurrentThreadOwns(); + } + + // Free the wrapped mutex - we don't need it anymore. + sqlite3_mutex_free(inner); + } +} diff --git a/storage/test/gtest/test_spinningSynchronousClose.cpp b/storage/test/gtest/test_spinningSynchronousClose.cpp new file mode 100644 index 0000000000..81f9bee11a --- /dev/null +++ b/storage/test/gtest/test_spinningSynchronousClose.cpp @@ -0,0 +1,72 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" +#include "prinrval.h" + +using namespace mozilla; + +/** + * Helper to verify that the event loop was spun. As long as this is dispatched + * prior to a call to Close()/SpinningSynchronousClose() we are guaranteed this + * will be run if the event loop is spun to perform a close. This is because + * SpinningSynchronousClose must spin the event loop to realize the close + * completed and our runnable will already be enqueued and therefore run before + * the AsyncCloseConnection's callback. Note that this invariant may be + * violated if our runnables end up in different queues thanks to Quantum + * changes, so this test may need to be updated if the close dispatch changes. + */ +class CompletionRunnable final : public Runnable { + public: + explicit CompletionRunnable() + : Runnable("CompletionRunnable"), mDone(false) {} + + NS_IMETHOD Run() override { + mDone = true; + return NS_OK; + } + + bool mDone; +}; + +// Can only run in optimized builds, or it would assert. +#ifndef DEBUG +TEST(storage_spinningSynchronousClose, CloseOnAsync) +{ + nsCOMPtr db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Wrongly use Close() instead of AsyncClose(). + RefPtr event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_false(NS_SUCCEEDED(db->Close())); + do_check_true(event->mDone); +} +#endif + +TEST(storage_spinningSynchronousClose, spinningSynchronousCloseOnAsync) +{ + nsCOMPtr db(getMemoryDatabase()); + // Run an async statement. + nsCOMPtr stmt; + do_check_success(db->CreateAsyncStatement( + "CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, getter_AddRefs(stmt))); + nsCOMPtr p; + do_check_success(stmt->ExecuteAsync(nullptr, getter_AddRefs(p))); + do_check_success(stmt->Finalize()); + + // Use the spinning close API. + RefPtr event = new CompletionRunnable(); + NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL); + do_check_success(db->SpinningSynchronousClose()); + do_check_true(event->mDone); +} diff --git a/storage/test/gtest/test_statement_scoper.cpp b/storage/test/gtest/test_statement_scoper.cpp new file mode 100644 index 0000000000..3327b0ff6b --- /dev/null +++ b/storage/test/gtest/test_statement_scoper.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "mozStorageHelper.h" + +/** + * This file test our statement scoper in mozStorageHelper.h. + */ + +TEST(storage_statement_scoper, automatic_reset) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + } + + // And we should be ready again. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); +} + +TEST(storage_statement_scoper, Abandon) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Need to create a table to populate sqlite_master with an entry. + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + + nsCOMPtr stmt; + (void)db->CreateStatement("SELECT * FROM sqlite_master"_ns, + getter_AddRefs(stmt)); + + // Reality check - make sure we start off in the right state. + int32_t state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_READY); + + // Start executing the statement, which will put it into an executing state. + { + mozStorageStatementScoper scoper(stmt); + bool hasMore; + do_check_true(NS_SUCCEEDED(stmt->ExecuteStep(&hasMore))); + + // Reality check that we are executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == + mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); + + // And call Abandon. We should not reset now when we fall out of scope. + scoper.Abandon(); + } + + // And we should still be executing. + state = -1; + (void)stmt->GetState(&state); + do_check_true(state == mozIStorageStatement::MOZ_STORAGE_STATEMENT_EXECUTING); +} diff --git a/storage/test/gtest/test_transaction_helper.cpp b/storage/test/gtest/test_transaction_helper.cpp new file mode 100644 index 0000000000..d11f631967 --- /dev/null +++ b/storage/test/gtest/test_transaction_helper.cpp @@ -0,0 +1,184 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +#include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast(aDB)->getAutocommit()); +} + +/** + * This file tests our Transaction helper in mozStorageHelper.h. + */ + +TEST(storage_transaction_helper, Commit) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, call Commit, and make sure that it does + // exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, Rollback) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, call Rollback, and make sure that it does + // not exists after the transaction falls out of scope. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Rollback(); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, AutoCommit) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it exists after the + // transaction falls out of scope. This means the Commit was successful. + { + mozStorageTransaction transaction(db, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); +} + +TEST(storage_transaction_helper, AutoRollback) +{ + nsCOMPtr db(getMemoryDatabase()); + + // Create a table in a transaction, and make sure that it does not exists + // after the transaction falls out of scope. This means the Rollback was + // successful. + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + } + do_check_false(has_transaction(db)); + + bool exists = true; + (void)db->TableExists("test"_ns, &exists); + do_check_false(exists); +} + +TEST(storage_transaction_helper, null_database_connection) +{ + // We permit the use of the Transaction helper when passing a null database + // in, so we need to make sure this still works without crashing. + mozStorageTransaction transaction(nullptr, false); + do_check_success(transaction.Start()); + do_check_true(NS_SUCCEEDED(transaction.Commit())); + do_check_true(NS_SUCCEEDED(transaction.Rollback())); +} + +TEST(storage_transaction_helper, async_Commit) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr wedger(new ThreadWedger(target)); + + { + mozStorageTransaction transaction( + db, false, mozIStorageConnection::TRANSACTION_DEFERRED, true); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr stmt; + (void)db->CreateAsyncStatement("SELECT NULL"_ns, getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists("test"_ns, &exists); + do_check_true(exists); + + blocking_async_close(db); +} + +TEST(storage_transaction_helper, Nesting) +{ + nsCOMPtr db(getMemoryDatabase()); + + { + mozStorageTransaction transaction(db, false); + do_check_success(transaction.Start()); + do_check_true(has_transaction(db)); + do_check_success( + db->ExecuteSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns)); + + { + mozStorageTransaction nestedTransaction(db, false); + do_check_success(nestedTransaction.Start()); + do_check_true(has_transaction(db)); + do_check_success(db->ExecuteSimpleSQL( + "CREATE TABLE nested_test (id INTEGER PRIMARY KEY)"_ns)); + +#ifndef MOZ_DIAGNOSTIC_ASSERT_ENABLED + do_check_true(transaction.Commit() == NS_ERROR_NOT_AVAILABLE); + do_check_true(transaction.Rollback() == NS_ERROR_NOT_AVAILABLE); +#endif + } + + bool exists = false; + do_check_success(db->TableExists("nested_test"_ns, &exists)); + do_check_false(exists); + + (void)transaction.Commit(); + } + do_check_false(has_transaction(db)); + + bool exists = false; + do_check_success(db->TableExists("test"_ns, &exists)); + do_check_true(exists); +} diff --git a/storage/test/gtest/test_true_async.cpp b/storage/test/gtest/test_true_async.cpp new file mode 100644 index 0000000000..3b54b73b3a --- /dev/null +++ b/storage/test/gtest/test_true_async.cpp @@ -0,0 +1,161 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 "storage_test_harness.h" + +//////////////////////////////////////////////////////////////////////////////// +//// Tests + +TEST(storage_true_async, TrueAsyncStatement) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // Start watching for forbidden mutex usage. + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind ordinally + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (?)"_ns, + getter_AddRefs(stmt)); + stmt->BindInt32ByIndex(0, 1); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - statement with something to bind by name + db->CreateAsyncStatement("INSERT INTO test (id) VALUES (:id)"_ns, + getter_AddRefs(stmt)); + nsCOMPtr paramsArray; + stmt->NewBindingParamsArray(getter_AddRefs(paramsArray)); + nsCOMPtr params; + paramsArray->NewBindingParams(getter_AddRefs(params)); + params->BindInt32ByName("id"_ns, 2); + paramsArray->AddParams(params); + params = nullptr; + stmt->BindParameters(paramsArray); + paramsArray = nullptr; + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(mutex_used_on_watched_thread); + + // - now, make sure creating a sync statement does trigger our guard. + // (If this doesn't happen, our test is bunk and it's important to know that.) + nsCOMPtr syncStmt; + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(syncStmt)); + syncStmt->Finalize(); + do_check_true(mutex_used_on_watched_thread); + + blocking_async_close(db); +} + +/** + * Test that cancellation before a statement is run successfully stops the + * statement from executing. + */ +TEST(storage_true_async, AsyncCancellation) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + RefPtr wedger(new ThreadWedger(target)); + + // -- create statements and cancel them + // - async + nsCOMPtr asyncStmt; + db->CreateAsyncStatement( + "CREATE TABLE asyncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(asyncStmt)); + + RefPtr asyncSpin(new AsyncStatementSpinner()); + nsCOMPtr asyncPend; + (void)asyncStmt->ExecuteAsync(asyncSpin, getter_AddRefs(asyncPend)); + do_check_true(asyncPend); + asyncPend->Cancel(); + + // - sync + nsCOMPtr syncStmt; + db->CreateStatement("CREATE TABLE syncTable (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(syncStmt)); + + RefPtr syncSpin(new AsyncStatementSpinner()); + nsCOMPtr syncPend; + (void)syncStmt->ExecuteAsync(syncSpin, getter_AddRefs(syncPend)); + do_check_true(syncPend); + syncPend->Cancel(); + + // -- unwedge the async thread + wedger->unwedge(); + + // -- verify that both statements report they were canceled + asyncSpin->SpinUntilCompleted(); + do_check_true(asyncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + syncSpin->SpinUntilCompleted(); + do_check_true(syncSpin->completionReason == + mozIStorageStatementCallback::REASON_CANCELED); + + // -- verify that neither statement constructed their tables + nsresult rv; + bool exists; + rv = db->TableExists("asyncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + rv = db->TableExists("syncTable"_ns, &exists); + do_check_true(rv == NS_OK); + do_check_false(exists); + + // -- cleanup + asyncStmt->Finalize(); + syncStmt->Finalize(); + blocking_async_close(db); +} + +/** + * Test that the destructor for an asynchronous statement which has a + * sqlite3_stmt will dispatch that statement to the async thread for + * finalization rather than trying to finalize it on the main thread + * (and thereby running afoul of our mutex use detector). + */ +TEST(storage_true_async, AsyncDestructorFinalizesOnAsyncThread) +{ + HookSqliteMutex hook; + + nsCOMPtr db(getMemoryDatabase()); + watch_for_mutex_use_on_this_thread(); + + // -- create an async statement + nsCOMPtr stmt; + db->CreateAsyncStatement("CREATE TABLE test (id INTEGER PRIMARY KEY)"_ns, + getter_AddRefs(stmt)); + + // -- execute it so it gets a sqlite3_stmt that needs to be finalized + blocking_async_execute(stmt); + do_check_false(mutex_used_on_watched_thread); + + // -- forget our reference + stmt = nullptr; + + // -- verify the mutex was not touched + do_check_false(mutex_used_on_watched_thread); + + // -- make sure the statement actually gets finalized / cleanup + // the close will assert if we failed to finalize! + blocking_async_close(db); +} diff --git a/storage/test/gtest/test_unlock_notify.cpp b/storage/test/gtest/test_unlock_notify.cpp new file mode 100644 index 0000000000..93cc5b09a8 --- /dev/null +++ b/storage/test/gtest/test_unlock_notify.cpp @@ -0,0 +1,234 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim set:sw=2 ts=2 et lcs=trail\:.,tab\:>~ : */ +/* 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 "storage_test_harness.h" + +#include "mozilla/ReentrantMonitor.h" +#include "nsThreadUtils.h" +#include "mozIStorageStatement.h" + +/** + * This file tests that our implementation around sqlite3_unlock_notify works + * as expected. + */ + +//////////////////////////////////////////////////////////////////////////////// +//// Helpers + +enum State { STARTING, WRITE_LOCK, READ_LOCK, TEST_DONE }; + +class DatabaseLocker : public mozilla::Runnable { + public: + explicit DatabaseLocker(const char* aSQL, nsIFile* aDBFile = nullptr) + : mozilla::Runnable("DatabaseLocker"), + monitor("DatabaseLocker::monitor"), + mSQL(aSQL), + mState(STARTING), + mDBFile(aDBFile) {} + + void RunInBackground() { + (void)NS_NewNamedThread("DatabaseLocker", getter_AddRefs(mThread)); + do_check_true(mThread); + + do_check_success(mThread->Dispatch(this, NS_DISPATCH_NORMAL)); + } + + void Shutdown() { + if (mThread) { + mThread->Shutdown(); + } + } + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + + nsCOMPtr db(getDatabase(mDBFile)); + + nsCString sql(mSQL); + nsCOMPtr stmt; + do_check_success(db->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + do_check_success(stmt->ExecuteStep(&hasResult)); + + Notify(WRITE_LOCK); + WaitFor(TEST_DONE); + + return NS_OK; + } + + void WaitFor(State aState) { + monitor.AssertCurrentThreadIn(); + while (mState != aState) { + do_check_success(monitor.Wait()); + } + } + + void Notify(State aState) { + monitor.AssertCurrentThreadIn(); + mState = aState; + do_check_success(monitor.Notify()); + } + + mozilla::ReentrantMonitor monitor MOZ_UNANNOTATED; + + protected: + nsCOMPtr mThread; + const char* const mSQL; + State mState; + nsCOMPtr mDBFile; +}; + +class DatabaseTester : public DatabaseLocker { + public: + DatabaseTester(mozIStorageConnection* aConnection, const char* aSQL) + : DatabaseLocker(aSQL), mConnection(aConnection) {} + + NS_IMETHOD Run() override { + mozilla::ReentrantMonitorAutoEnter lock(monitor); + WaitFor(READ_LOCK); + + nsCString sql(mSQL); + nsCOMPtr stmt; + do_check_success(mConnection->CreateStatement(sql, getter_AddRefs(stmt))); + + bool hasResult; + nsresult rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + // Finalize our statement and null out our connection before notifying to + // ensure that we close on the proper thread. + rv = stmt->Finalize(); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + mConnection = nullptr; + + Notify(TEST_DONE); + + return NS_OK; + } + + private: + nsCOMPtr mConnection; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Test Functions + +void setup() { + nsCOMPtr db(getDatabase()); + + // Create and populate a dummy table. + nsresult rv = db->ExecuteSimpleSQL(nsLiteralCString( + "CREATE TABLE test (id INTEGER PRIMARY KEY, data STRING)")); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('foo')"_ns); + do_check_success(rv); + rv = db->ExecuteSimpleSQL("INSERT INTO test (data) VALUES ('bar')"_ns); + do_check_success(rv); + rv = + db->ExecuteSimpleSQL("CREATE UNIQUE INDEX unique_data ON test (data)"_ns); + do_check_success(rv); +} + +void test_step_locked_does_not_block_main_thread() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = db->CreateStatement( + "INSERT INTO test (data) VALUES ('test1')"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + nsCOMPtr dbFile; + db->GetDatabaseFile(getter_AddRefs(dbFile)); + RefPtr locker( + new DatabaseLocker("SELECT * FROM test", dbFile)); + do_check_true(locker); + { + mozilla::ReentrantMonitorAutoEnter lock(locker->monitor); + locker->RunInBackground(); + + // Wait for the locker to notify us that it has locked the database + // properly. + locker->WaitFor(WRITE_LOCK); + + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_eq(rv, NS_ERROR_FILE_IS_LOCKED); + + locker->Notify(TEST_DONE); + } + locker->Shutdown(); +} + +void test_drop_index_does_not_loop() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr tester = + new DatabaseTester(db, "DROP INDEX unique_data"); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +void test_drop_table_does_not_loop() { + nsCOMPtr db(getDatabase()); + + // Need to prepare our statement ahead of time so we make sure to only test + // step and not prepare. + nsCOMPtr stmt; + nsresult rv = + db->CreateStatement("SELECT * FROM test"_ns, getter_AddRefs(stmt)); + do_check_success(rv); + + RefPtr tester(new DatabaseTester(db, "DROP TABLE test")); + do_check_true(tester); + { + mozilla::ReentrantMonitorAutoEnter lock(tester->monitor); + tester->RunInBackground(); + + // Hold a read lock on the database, and then let the tester try to execute. + bool hasResult; + rv = stmt->ExecuteStep(&hasResult); + do_check_success(rv); + do_check_true(hasResult); + tester->Notify(READ_LOCK); + + // Make sure the tester finishes its test before we move on. + tester->WaitFor(TEST_DONE); + } + tester->Shutdown(); +} + +TEST(storage_unlock_notify, Test) +{ + // These must execute in order. + setup(); + test_step_locked_does_not_block_main_thread(); + test_drop_index_does_not_loop(); + test_drop_table_does_not_loop(); +} diff --git a/storage/test/moz.build b/storage/test/moz.build new file mode 100644 index 0000000000..7dd21f5fe2 --- /dev/null +++ b/storage/test/moz.build @@ -0,0 +1,13 @@ +# -*- 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/. + +XPCSHELL_TESTS_MANIFESTS += ["unit/xpcshell.toml"] + +TEST_DIRS += ["gtest"] + +TESTING_JS_MODULES += [ + "unit/VacuumParticipant.sys.mjs", +] diff --git a/storage/test/unit/VacuumParticipant.sys.mjs b/storage/test/unit/VacuumParticipant.sys.mjs new file mode 100644 index 0000000000..9f1c39826e --- /dev/null +++ b/storage/test/unit/VacuumParticipant.sys.mjs @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This testing component is used in test_vacuum* tests. + +const CAT_NAME = "vacuum-participant"; +const CONTRACT_ID = "@unit.test.com/test-vacuum-participant;1"; + +import { MockRegistrar } from "resource://testing-common/MockRegistrar.sys.mjs"; +import { TestUtils } from "resource://testing-common/TestUtils.sys.mjs"; + +export class VacuumParticipant { + #dbConn; + #expectedPageSize = 0; + #useIncrementalVacuum = false; + #grant = false; + + /** + * Build a VacuumParticipant instance. + * Note: After creation you must await instance.promiseRegistered() to ensure + * Category Caches have been updated. + * + * @param {mozIStorageAsyncConnection} databaseConnection + * The connection to be vacuumed. + * @param {Number} [expectedPageSize] + * Used to change the database page size. + * @param {boolean} [useIncrementalVacuum] + * Whether to enable incremental vacuum on the database. + * @param {boolean} [grant] + * Whether the vacuum operation should be granted. + */ + constructor( + databaseConnection, + { expectedPageSize = 0, useIncrementalVacuum = false, grant = true } = {} + ) { + this.#dbConn = databaseConnection; + + // Register as the only participant. + this.#unregisterAllParticipants(); + this.#registerAsParticipant(); + + this.#expectedPageSize = expectedPageSize; + this.#useIncrementalVacuum = useIncrementalVacuum; + this.#grant = grant; + + this.QueryInterface = ChromeUtils.generateQI([ + "mozIStorageVacuumParticipant", + ]); + } + + promiseRegistered() { + // The category manager dispatches change notifications to the main thread, + // so we must wait one tick. + return TestUtils.waitForTick(); + } + + #registerAsParticipant() { + MockRegistrar.register(CONTRACT_ID, this); + Services.catMan.addCategoryEntry( + CAT_NAME, + "vacuumParticipant", + CONTRACT_ID, + false, + false + ); + } + + #unregisterAllParticipants() { + // First unregister other participants. + for (let { data: entry } of Services.catMan.enumerateCategory(CAT_NAME)) { + Services.catMan.deleteCategoryEntry("vacuum-participant", entry, false); + } + } + + async dispose() { + this.#unregisterAllParticipants(); + MockRegistrar.unregister(CONTRACT_ID); + await new Promise(resolve => { + this.#dbConn.asyncClose(resolve); + }); + } + + get expectedDatabasePageSize() { + return this.#expectedPageSize; + } + + get useIncrementalVacuum() { + return this.#useIncrementalVacuum; + } + + get databaseConnection() { + return this.#dbConn; + } + + onBeginVacuum() { + if (!this.#grant) { + return false; + } + Services.obs.notifyObservers(null, "test-begin-vacuum"); + return true; + } + onEndVacuum(succeeded) { + Services.obs.notifyObservers( + null, + succeeded ? "test-end-vacuum-success" : "test-end-vacuum-failure" + ); + } +} diff --git a/storage/test/unit/baddataDB.sqlite b/storage/test/unit/baddataDB.sqlite new file mode 100644 index 0000000000..5b2f9da3d6 Binary files /dev/null and b/storage/test/unit/baddataDB.sqlite differ diff --git a/storage/test/unit/corruptDB.sqlite b/storage/test/unit/corruptDB.sqlite new file mode 100644 index 0000000000..b234246cac Binary files /dev/null and b/storage/test/unit/corruptDB.sqlite differ diff --git a/storage/test/unit/fakeDB.sqlite b/storage/test/unit/fakeDB.sqlite new file mode 100644 index 0000000000..5f7498bfc2 --- /dev/null +++ b/storage/test/unit/fakeDB.sqlite @@ -0,0 +1 @@ +BACON diff --git a/storage/test/unit/goodDB.sqlite b/storage/test/unit/goodDB.sqlite new file mode 100644 index 0000000000..b06884672f Binary files /dev/null and b/storage/test/unit/goodDB.sqlite differ diff --git a/storage/test/unit/head_storage.js b/storage/test/unit/head_storage.js new file mode 100644 index 0000000000..ba1b76ad0b --- /dev/null +++ b/storage/test/unit/head_storage.js @@ -0,0 +1,412 @@ +/* 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/. */ + +var { XPCOMUtils } = ChromeUtils.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); +var { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" +); + +ChromeUtils.defineESModuleGetters(this, { + FileUtils: "resource://gre/modules/FileUtils.sys.mjs", + Sqlite: "resource://gre/modules/Sqlite.sys.mjs", + TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", + TestUtils: "resource://testing-common/TestUtils.sys.mjs", +}); + +const OPEN_HISTOGRAM = "SQLITE_STORE_OPEN"; +const QUERY_HISTOGRAM = "SQLITE_STORE_QUERY"; + +const TELEMETRY_VALUES = { + success: 0, + failure: 1, + access: 2, + diskio: 3, + corrupt: 4, + busy: 5, + misuse: 6, + diskspace: 7, +}; + +do_get_profile(); +var gDBConn = null; + +const TEST_DB_NAME = "test_storage.sqlite"; +function getTestDB() { + var db = Services.dirsvc.get("ProfD", Ci.nsIFile); + db.append(TEST_DB_NAME); + return db; +} + +/** + * Obtains a corrupt database to test against. + */ +function getCorruptDB() { + return do_get_file("corruptDB.sqlite"); +} + +/** + * Obtains a fake (non-SQLite format) database to test against. + */ +function getFakeDB() { + return do_get_file("fakeDB.sqlite"); +} + +/** + * Delete the test database file. + */ +function deleteTestDB() { + print("*** Storage Tests: Trying to remove file!"); + var dbFile = getTestDB(); + if (dbFile.exists()) { + try { + dbFile.remove(false); + } catch (e) { + /* stupid windows box */ + } + } +} + +function cleanup() { + // close the connection + print("*** Storage Tests: Trying to close!"); + getOpenedDatabase().close(); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Use asyncClose to cleanup a connection. Synchronous by means of internally + * spinning an event loop. + */ +function asyncCleanup() { + let closed = false; + + // close the connection + print("*** Storage Tests: Trying to asyncClose!"); + getOpenedDatabase().asyncClose(function () { + closed = true; + }); + + let tm = Cc["@mozilla.org/thread-manager;1"].getService(); + tm.spinEventLoopUntil("Test(head_storage.js:asyncCleanup)", () => closed); + + // we need to null out the database variable to get a new connection the next + // time getOpenedDatabase is called + gDBConn = null; + + // removing test db + deleteTestDB(); +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection shares its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedDatabase(connectionFlags = 0) { + if (!gDBConn) { + gDBConn = Services.storage.openDatabase(getTestDB(), connectionFlags); + + // Clear out counts for any queries that occured while opening the database. + TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + } + return gDBConn; +} + +/** + * Get a connection to the test database. Creates and caches the connection + * if necessary, otherwise reuses the existing cached connection. This + * connection doesn't share its cache. + * + * @returns the mozIStorageConnection for the file. + */ +function getOpenedUnsharedDatabase() { + if (!gDBConn) { + gDBConn = Services.storage.openUnsharedDatabase(getTestDB()); + } + return gDBConn; +} + +/** + * Obtains a specific database to use. + * + * @param aFile + * The nsIFile representing the db file to open. + * @returns the mozIStorageConnection for the file. + */ +function getDatabase(aFile) { + return Services.storage.openDatabase(aFile); +} + +function createStatement(aSQL) { + return getOpenedDatabase().createStatement(aSQL); +} + +/** + * Creates an asynchronous SQL statement. + * + * @param aSQL + * The SQL to parse into a statement. + * @returns a mozIStorageAsyncStatement from aSQL. + */ +function createAsyncStatement(aSQL) { + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +/** + * Invoke the given function and assert that it throws an exception expressing + * the provided error code in its 'result' attribute. JS function expressions + * can be used to do this concisely. + * + * Example: + * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction()); + * + * @param aErrorCode + * The error code to expect from invocation of aFunction. + * @param aFunction + * The function to invoke and expect an XPCOM-style error from. + */ +function expectError(aErrorCode, aFunction) { + let exceptionCaught = false; + try { + aFunction(); + } catch (e) { + if (e.result != aErrorCode) { + do_throw( + "Got an exception, but the result code was not the expected " + + "one. Expected " + + aErrorCode + + ", got " + + e.result + ); + } + exceptionCaught = true; + } + if (!exceptionCaught) { + do_throw(aFunction + " should have thrown an exception but did not!"); + } +} + +/** + * Run a query synchronously and verify that we get back the expected results. + * + * @param aSQLString + * The SQL string for the query. + * @param aBind + * The value to bind at index 0. + * @param aResults + * A list of the expected values returned in the sole result row. + * Express blobs as lists. + */ +function verifyQuery(aSQLString, aBind, aResults) { + let stmt = getOpenedDatabase().createStatement(aSQLString); + stmt.bindByIndex(0, aBind); + try { + Assert.ok(stmt.executeStep()); + let nCols = stmt.numEntries; + if (aResults.length != nCols) { + do_throw( + "Expected " + + aResults.length + + " columns in result but " + + "there are only " + + aResults.length + + "!" + ); + } + for (let iCol = 0; iCol < nCols; iCol++) { + let expectedVal = aResults[iCol]; + let valType = stmt.getTypeOfIndex(iCol); + if (expectedVal === null) { + Assert.equal(stmt.VALUE_TYPE_NULL, valType); + Assert.ok(stmt.getIsNull(iCol)); + } else if (typeof expectedVal == "number") { + if (Math.floor(expectedVal) == expectedVal) { + Assert.equal(stmt.VALUE_TYPE_INTEGER, valType); + Assert.equal(expectedVal, stmt.getInt32(iCol)); + } else { + Assert.equal(stmt.VALUE_TYPE_FLOAT, valType); + Assert.equal(expectedVal, stmt.getDouble(iCol)); + } + } else if (typeof expectedVal == "string") { + Assert.equal(stmt.VALUE_TYPE_TEXT, valType); + Assert.equal(expectedVal, stmt.getUTF8String(iCol)); + } else { + // blob + Assert.equal(stmt.VALUE_TYPE_BLOB, valType); + let count = { value: 0 }, + blob = { value: null }; + stmt.getBlob(iCol, count, blob); + Assert.equal(count.value, expectedVal.length); + for (let i = 0; i < count.value; i++) { + Assert.equal(expectedVal[i], blob.value[i]); + } + } + } + } finally { + stmt.finalize(); + } +} + +/** + * Return the number of rows in the able with the given name using a synchronous + * query. + * + * @param aTableName + * The name of the table. + * @return The number of rows. + */ +function getTableRowCount(aTableName) { + var currentRows = 0; + var countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM " + aTableName + ); + try { + Assert.ok(countStmt.executeStep()); + currentRows = countStmt.row.count; + } finally { + countStmt.finalize(); + } + return currentRows; +} + +// Promise-Returning Functions + +function asyncClone(db, readOnly) { + return new Promise((resolve, reject) => { + db.asyncClone(readOnly, function (status, db2) { + if (Components.isSuccessCode(status)) { + resolve(db2.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(status); + } + }); + }); +} + +function asyncClose(db) { + return new Promise((resolve, reject) => { + db.asyncClose(function (status) { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }); +} + +function mapOptionsToFlags(aOptions, aMapping) { + let result = aMapping.default; + Object.entries(aOptions || {}).forEach(([optionName, isTrue]) => { + if (aMapping.hasOwnProperty(optionName) && isTrue) { + result |= aMapping[optionName]; + } + }); + return result; +} + +function getOpenFlagsMap() { + return { + default: Ci.mozIStorageService.OPEN_DEFAULT, + shared: Ci.mozIStorageService.OPEN_SHARED, + readOnly: Ci.mozIStorageService.OPEN_READONLY, + ignoreLockingMode: Ci.mozIStorageService.OPEN_IGNORE_LOCKING_MODE, + }; +} + +function getConnectionFlagsMap() { + return { + default: Ci.mozIStorageService.CONNECTION_DEFAULT, + interruptible: Ci.mozIStorageService.CONNECTION_INTERRUPTIBLE, + }; +} + +function openAsyncDatabase(file, options) { + return new Promise((resolve, reject) => { + const openFlags = mapOptionsToFlags(options, getOpenFlagsMap()); + const connectionFlags = mapOptionsToFlags(options, getConnectionFlagsMap()); + + Services.storage.openAsyncDatabase( + file, + openFlags, + connectionFlags, + function (status, db) { + if (Components.isSuccessCode(status)) { + resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); + } else { + reject(status); + } + } + ); + }); +} + +function executeAsync(statement, onResult) { + return new Promise((resolve, reject) => { + statement.executeAsync({ + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +function executeMultipleStatementsAsync(db, statements, onResult) { + return new Promise((resolve, reject) => { + db.executeAsync(statements, { + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +function executeSimpleSQLAsync(db, query, onResult) { + return new Promise((resolve, reject) => { + db.executeSimpleSQLAsync(query, { + handleError(error) { + reject(error); + }, + handleResult(result) { + if (onResult) { + onResult(result); + } else { + do_throw("No results were expected"); + } + }, + handleCompletion(result) { + resolve(result); + }, + }); + }); +} + +cleanup(); diff --git a/storage/test/unit/locale_collation.txt b/storage/test/unit/locale_collation.txt new file mode 100644 index 0000000000..86f50579bb --- /dev/null +++ b/storage/test/unit/locale_collation.txt @@ -0,0 +1,174 @@ + +! +" +# +$ +% +& +' +( +) +* ++ +, +- +. +/ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +: +; +< += +> +? +@ +A +B +C +D +E +F +G +H +I +J +K +L +M +N +O +P +Q +R +S +T +U +V +W +X +Y +Z +[ +\ +] +^ +_ +` +a +b +c +d +e +f +g +h +i +j +k +l +m +n +o +p +q +r +s +t +u +v +w +x +y +z +{ +| +} +~ +ludwig van beethoven +Ludwig van Beethoven +Ludwig van beethoven +Jane +jane +JANE +jAne +jaNe +janE +JAne +JaNe +JanE +JANe +JaNE +JAnE +jANE +Umberto Eco +Umberto eco +umberto eco +umberto Eco +UMBERTO ECO +ace +bash +*ace +!ace +%ace +~ace +#ace +cork +denizen +[denizen] +(denizen) +{denizen} +/denizen/ +#denizen# +$denizen$ +@denizen +elf +full +gnome +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Japanese +gnomic investigation of typological factors in the grammaticalization process of Malayo-Polynesian substaratum in the protoAltaic vocabulary core in the proto-layers of pre-historic Javanese +hint +Internationalization +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization +Zinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizatio +n +Zinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalization internationalizationinternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTioninternationalizationinternationalizationinternationalizationinternationalization +ZinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizationinternationalizaTion +jostle +kin +Laymen +lumens +Lumens +motleycrew +motley crew +niven's creative talents +nivens creative talents +opie +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rockies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the Rokkies +posh restaurants in surbanized and still urban incorporated subsection of this beautifl city in the rockies +quilt's +quilts +quilt +Rondo +street +tamale oxidization and iodization in rapid progress +tamale oxidization and iodization in rapid Progress +until +vera +Wobble +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoneme +Xanadu's legenary imaginary floccinaucinihilipilification in localization of theoretical portions of glottochronological understanding of the phoname +yearn +zodiac +a +å diff --git a/storage/test/unit/test_bug-365166.js b/storage/test/unit/test_bug-365166.js new file mode 100644 index 0000000000..aef53e60f2 --- /dev/null +++ b/storage/test/unit/test_bug-365166.js @@ -0,0 +1,23 @@ +// Testcase for bug 365166 - crash [@ strlen] calling +// mozIStorageStatement::getColumnName of a statement created with +// "PRAGMA user_version" or "PRAGMA schema_version" +function run_test() { + test("user"); + test("schema"); + + function test(param) { + var colName = param + "_version"; + var sql = "PRAGMA " + colName; + + var file = getTestDB(); + var conn = Services.storage.openDatabase(file); + var statement = conn.createStatement(sql); + try { + // This shouldn't crash: + Assert.equal(statement.getColumnName(0), colName); + } finally { + statement.reset(); + statement.finalize(); + } + } +} diff --git a/storage/test/unit/test_bug-393952.js b/storage/test/unit/test_bug-393952.js new file mode 100644 index 0000000000..a91bcb034e --- /dev/null +++ b/storage/test/unit/test_bug-393952.js @@ -0,0 +1,35 @@ +/* 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/. */ + +// Testcase for bug 393952: crash when I try to VACUUM and one of the tables +// has a UNIQUE text column. StorageUnicodeFunctions::likeFunction() +// needs to handle null aArgv[0] and aArgv[1] + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT UNIQUE"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.reset(); + stmt.finalize(); +} + +function test_vacuum() { + var stmt = createStatement("VACUUM;"); + stmt.executeStep(); + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_vacuum]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_bug-429521.js b/storage/test/unit/test_bug-429521.js new file mode 100644 index 0000000000..1e647e984a --- /dev/null +++ b/storage/test/unit/test_bug-429521.js @@ -0,0 +1,49 @@ +/* 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/. */ + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement( + "INSERT INTO t1 (x) VALUES ('/mozilla.org/20070129_1/Europe/Berlin')" + ); + stmt.execute(); + stmt.finalize(); +} + +function test_bug429521() { + var stmt = createStatement( + "SELECT DISTINCT(zone) FROM (" + + "SELECT x AS zone FROM t1 WHERE x LIKE '/mozilla.org%'" + + ");" + ); + + print("*** test_bug429521: started"); + + try { + while (stmt.executeStep()) { + print("*** test_bug429521: step() Read wrapper.row.zone"); + + // BUG: the print commands after the following statement + // are never executed. Script stops immediately. + stmt.row.zone; + + print("*** test_bug429521: step() Read wrapper.row.zone finished"); + } + } catch (e) { + print("*** test_bug429521: " + e); + } + + print("*** test_bug429521: finished"); + + stmt.finalize(); +} + +function run_test() { + setup(); + + test_bug429521(); + + cleanup(); +} diff --git a/storage/test/unit/test_bug-444233.js b/storage/test/unit/test_bug-444233.js new file mode 100644 index 0000000000..934f69ad60 --- /dev/null +++ b/storage/test/unit/test_bug-444233.js @@ -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/. */ + +function setup() { + // Create the table + getOpenedDatabase().createTable( + "test_bug444233", + "id INTEGER PRIMARY KEY, value TEXT" + ); + + // Insert dummy data, using wrapper methods + var stmt = createStatement( + "INSERT INTO test_bug444233 (value) VALUES (:value)" + ); + stmt.params.value = "value1"; + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO test_bug444233 (value) VALUES (:value)"); + stmt.params.value = "value2"; + stmt.execute(); + stmt.finalize(); +} + +function test_bug444233() { + print("*** test_bug444233: started"); + + // Check that there are 2 results + var stmt = createStatement("SELECT COUNT(*) AS number FROM test_bug444233"); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.row.number); + stmt.reset(); + stmt.finalize(); + + print("*** test_bug444233: doing delete"); + + // Now try to delete using IN + // Cheating since we know ids are 1,2 + try { + var ids = [1, 2]; + stmt = createStatement("DELETE FROM test_bug444233 WHERE id IN (:ids)"); + stmt.params.ids = ids; + } catch (e) { + print("*** test_bug444233: successfully caught exception"); + } + stmt.finalize(); +} + +function run_test() { + setup(); + test_bug444233(); + cleanup(); +} diff --git a/storage/test/unit/test_cache_size.js b/storage/test/unit/test_cache_size.js new file mode 100644 index 0000000000..bd56de64cd --- /dev/null +++ b/storage/test/unit/test_cache_size.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that dbs of various page sizes are using the right cache +// size (bug 703113). + +/** + * In order to change the cache size, we must open a DB, change the page + * size, create a table, close the DB, then re-open the DB. We then check + * the cache size after reopening. + * + * @param dbOpener + * function that opens the DB specified in file + * @param file + * file holding the database + * @param pageSize + * the DB's page size + * @param expectedCacheSize + * the expected cache size for the given page size + */ +function check_size(dbOpener, file, pageSize, expectedCacheSize) { + // Open the DB, immediately change its page size. + let db = dbOpener(file); + db.executeSimpleSQL("PRAGMA page_size = " + pageSize); + + // Check the page size change worked. + let stmt = db.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, pageSize); + stmt.finalize(); + + // Create a simple table. + db.executeSimpleSQL("CREATE TABLE test ( id INTEGER PRIMARY KEY )"); + + // Close and re-open the DB. + db.close(); + db = dbOpener(file); + + // Check cache size is as expected. + stmt = db.createStatement("PRAGMA cache_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.cache_size, expectedCacheSize); + stmt.finalize(); +} + +function new_file(name) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + Assert.ok(!file.exists()); + return file; +} + +function run_test() { + const kExpectedCacheSize = -2048; // 2MiB + + let pageSizes = [1024, 4096, 32768]; + + for (let i = 0; i < pageSizes.length; i++) { + let pageSize = pageSizes[i]; + check_size( + getDatabase, + new_file("shared" + pageSize), + pageSize, + kExpectedCacheSize + ); + check_size( + Services.storage.openUnsharedDatabase, + new_file("unshared" + pageSize), + pageSize, + kExpectedCacheSize + ); + } +} diff --git a/storage/test/unit/test_chunk_growth.js b/storage/test/unit/test_chunk_growth.js new file mode 100644 index 0000000000..4bcdf86ce3 --- /dev/null +++ b/storage/test/unit/test_chunk_growth.js @@ -0,0 +1,51 @@ +// This file tests SQLITE_FCNTL_CHUNK_SIZE behaves as expected + +function run_sql(d, sql) { + var stmt = d.createStatement(sql); + stmt.execute(); + stmt.finalize(); +} + +function new_file(name) { + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name); + return file; +} + +function get_size(name) { + return new_file(name).fileSize; +} + +function run_test() { + const filename = "chunked.sqlite"; + const CHUNK_SIZE = 512 * 1024; + var d = getDatabase(new_file(filename)); + try { + d.setGrowthIncrement(CHUNK_SIZE, ""); + } catch (e) { + if (e.result != Cr.NS_ERROR_FILE_TOO_BIG) { + throw e; + } + print("Too little free space to set CHUNK_SIZE!"); + return; + } + run_sql(d, "CREATE TABLE bloat(data varchar)"); + + var orig_size = get_size(filename); + /* Dump in at least 32K worth of data. + * While writing ensure that the file size growth in chunksize set above. + */ + const str1024 = new Array(1024).join("T"); + for (var i = 0; i < 32; i++) { + run_sql(d, "INSERT INTO bloat VALUES('" + str1024 + "')"); + var size = get_size(filename); + // Must not grow in small increments. + Assert.ok(size == orig_size || size >= CHUNK_SIZE); + } + /* In addition to growing in chunk-size increments, the db + * should shrink in chunk-size increments too. + */ + run_sql(d, "DELETE FROM bloat"); + run_sql(d, "VACUUM"); + Assert.ok(get_size(filename) >= CHUNK_SIZE); +} diff --git a/storage/test/unit/test_connection_asyncClose.js b/storage/test/unit/test_connection_asyncClose.js new file mode 100644 index 0000000000..2d89087c5c --- /dev/null +++ b/storage/test/unit/test_connection_asyncClose.js @@ -0,0 +1,128 @@ +/* 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/. */ + +/* + * Thorough branch coverage for asyncClose. + * + * Coverage of asyncClose by connection state at time of AsyncClose invocation: + * - (asyncThread && mDBConn) => AsyncCloseConnection used, actually closes + * - test_asyncClose_does_not_complete_before_statements + * - test_double_asyncClose_throws + * - test_asyncClose_does_not_throw_without_callback + * - (asyncThread && !mDBConn) => AsyncCloseConnection used, although no close + * is required. Note that this is only possible in the event that + * openAsyncDatabase was used and we failed to open the database. + * Additionally, the async connection will never be exposed to the caller and + * AsyncInitDatabase will be the one to (automatically) call AsyncClose. + * - test_asyncClose_failed_open + * - (!asyncThread && mDBConn) => Close() invoked, actually closes + * - test_asyncClose_on_sync_db + * - (!asyncThread && !mDBConn) => Close() invoked, no close needed, errors. + * This happens if the database has already been closed. + * - test_double_asyncClose_throws + */ + +/** + * Sanity check that our close indeed happens after asynchronously executed + * statements scheduled during the same turn of the event loop. Note that we + * just care that the statement says it completed without error, we're not + * worried that the close will happen and then the statement will magically + * complete. + */ +add_task(async function test_asyncClose_does_not_complete_before_statements() { + let db = Services.storage.openDatabase(getTestDB()); + let stmt = db.createStatement("SELECT * FROM sqlite_master"); + // Issue the executeAsync but don't yield for it... + let asyncStatementPromise = executeAsync(stmt); + stmt.finalize(); + + // Issue the close. (And now the order of yielding doesn't matter.) + // Branch coverage: (asyncThread && mDBConn) + await asyncClose(db); + equal( + await asyncStatementPromise, + Ci.mozIStorageStatementCallback.REASON_FINISHED + ); +}); + +/** + * Open an async database (ensures the async thread is created) and then invoke + * AsyncClose() twice without yielding control flow. The first will initiate + * the actual async close after calling setClosedState which synchronously + * impacts what the second call will observe. The second call will then see the + * async thread is not available and fall back to invoking Close() which will + * notice the mDBConn is already gone. + */ +if (!AppConstants.DEBUG) { + add_task(async function test_double_asyncClose_throws() { + let db = await openAsyncDatabase(getTestDB()); + + // (Don't yield control flow yet, save the promise for after we make the + // second call.) + // Branch coverage: (asyncThread && mDBConn) + let realClosePromise = await asyncClose(db); + try { + // Branch coverage: (!asyncThread && !mDBConn) + db.asyncClose(); + ok(false, "should have thrown"); + } catch (e) { + equal(e.result, Cr.NS_ERROR_NOT_INITIALIZED); + } + + await realClosePromise; + }); +} + +/** + * Create a sync db connection and never take it asynchronous and then call + * asyncClose on it. This will bring the async thread to life to perform the + * shutdown to avoid blocking the main thread, although we won't be able to + * tell the difference between this happening and the method secretly shunting + * to close(). + */ +add_task(async function test_asyncClose_on_sync_db() { + let db = Services.storage.openDatabase(getTestDB()); + + // Branch coverage: (!asyncThread && mDBConn) + await asyncClose(db); + ok(true, "closed sync connection asynchronously"); +}); + +/** + * Fail to asynchronously open a DB in order to get an async thread existing + * without having an open database and asyncClose invoked. As per the file + * doc-block, note that asyncClose will automatically be invoked by the + * AsyncInitDatabase when it fails to open the database. We will never be + * provided with a reference to the connection and so cannot call AsyncClose on + * it ourselves. + */ +add_task(async function test_asyncClose_failed_open() { + // This will fail and the promise will be rejected. + let openPromise = openAsyncDatabase(getFakeDB()); + await openPromise.then( + () => { + ok(false, "we should have failed to open the db; this test is broken!"); + }, + () => { + ok(true, "correctly failed to open db; bg asyncClose should happen"); + } + ); + // (NB: we are unable to observe the thread shutdown, but since we never open + // a database, this test is not going to interfere with other tests so much.) +}); + +// THE TEST BELOW WANTS TO BE THE LAST TEST WE RUN. DO NOT MAKE IT SAD. +/** + * Verify that asyncClose without a callback does not explode. Without a + * callback the shutdown is not actually observable, so we run this test last + * in order to avoid weird overlaps. + */ +add_task(async function test_asyncClose_does_not_throw_without_callback() { + let db = await openAsyncDatabase(getTestDB()); + // Branch coverage: (asyncThread && mDBConn) + db.asyncClose(); + ok(true, "if we shutdown cleanly and do not crash, then we succeeded"); +}); +// OBEY SHOUTING UPPER-CASE COMMENTS. +// ADD TESTS ABOVE THE FORMER TEST, NOT BELOW IT. diff --git a/storage/test/unit/test_connection_executeAsync.js b/storage/test/unit/test_connection_executeAsync.js new file mode 100644 index 0000000000..a77299ba3b --- /dev/null +++ b/storage/test/unit/test_connection_executeAsync.js @@ -0,0 +1,175 @@ +/* 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 file tests the functionality of mozIStorageConnection::executeAsync for + * both mozIStorageStatement and mozIStorageAsyncStatement. + * + * A single database connection is used for the entirety of the test, which is + * a legacy thing, but we otherwise use the modern promise-based driver and + * async helpers. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +add_task(async function test_first_create_and_add() { + // synchronously open the database and let gDBConn hold onto it because we + // use this database + let db = getOpenedDatabase(); + // synchronously set up our table *that will be used for the rest of the file* + db.executeSimpleSQL( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + + let stmts = []; + stmts[0] = db.createStatement( + "INSERT INTO test (id, string, number, nuller, blober) VALUES (?, ?, ?, ?, ?)" + ); + stmts[0].bindByIndex(0, INTEGER); + stmts[0].bindByIndex(1, TEXT); + stmts[0].bindByIndex(2, REAL); + stmts[0].bindByIndex(3, null); + stmts[0].bindBlobByIndex(4, BLOB, BLOB.length); + stmts[1] = getOpenedDatabase().createAsyncStatement( + "INSERT INTO test (string, number, nuller, blober) VALUES (?, ?, ?, ?)" + ); + stmts[1].bindByIndex(0, TEXT); + stmts[1].bindByIndex(1, REAL); + stmts[1].bindByIndex(2, null); + stmts[1].bindBlobByIndex(3, BLOB, BLOB.length); + + // asynchronously execute the statements + let execResult = await executeMultipleStatementsAsync( + db, + stmts, + function (aResultSet) { + ok(false, "we only did inserts so we should not have gotten results!"); + } + ); + equal( + Ci.mozIStorageStatementCallback.REASON_FINISHED, + execResult, + "execution should have finished successfully." + ); + + // Check that the result is in the table + let stmt = db.createStatement( + "SELECT string, number, nuller, blober FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + try { + Assert.ok(stmt.executeStep()); + Assert.equal(TEXT, stmt.getString(0)); + Assert.equal(REAL, stmt.getDouble(1)); + Assert.ok(stmt.getIsNull(2)); + let count = { value: 0 }; + let blob = { value: null }; + stmt.getBlob(3, count, blob); + Assert.equal(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blob.value[i]); + } + } finally { + stmt.finalize(); + } + + // Make sure we have two rows in the table + stmt = db.createStatement("SELECT COUNT(1) FROM test"); + try { + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + } finally { + stmt.finalize(); + } + + stmts[0].finalize(); + stmts[1].finalize(); +}); + +add_task(async function test_last_multiple_bindings_on_statements() { + // This tests to make sure that we pass all the statements multiply bound + // parameters when we call executeAsync. + const AMOUNT_TO_ADD = 5; + const ITERATIONS = 5; + + let stmts = []; + let db = getOpenedDatabase(); + let sqlString = + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)"; + // We run the same statement twice, and should insert 2 * AMOUNT_TO_ADD. + for (let i = 0; i < ITERATIONS; i++) { + // alternate the type of statement we create + if (i % 2) { + stmts[i] = db.createStatement(sqlString); + } else { + stmts[i] = db.createAsyncStatement(sqlString); + } + + let params = stmts[i].newBindingParamsArray(); + for (let j = 0; j < AMOUNT_TO_ADD; j++) { + let bp = params.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB); + params.addParams(bp); + } + stmts[i].bindParameters(params); + } + + // Get our current number of rows in the table. + let currentRows = 0; + let countStmt = getOpenedDatabase().createStatement( + "SELECT COUNT(1) AS count FROM test" + ); + try { + Assert.ok(countStmt.executeStep()); + currentRows = countStmt.row.count; + } finally { + countStmt.reset(); + } + + // Execute asynchronously. + let execResult = await executeMultipleStatementsAsync( + db, + stmts, + function (aResultSet) { + ok(false, "we only did inserts so we should not have gotten results!"); + } + ); + equal( + Ci.mozIStorageStatementCallback.REASON_FINISHED, + execResult, + "execution should have finished successfully." + ); + + // Check to make sure we added all of our rows. + try { + Assert.ok(countStmt.executeStep()); + Assert.equal(currentRows + ITERATIONS * AMOUNT_TO_ADD, countStmt.row.count); + } finally { + countStmt.finalize(); + } + + stmts.forEach(stmt => stmt.finalize()); + + // we are the last test using this connection and since it has gone async + // we *must* call asyncClose on it. + await asyncClose(db); + gDBConn = null; +}); + +// If you add a test down here you will need to move the asyncClose or clean +// things up a little more. diff --git a/storage/test/unit/test_connection_executeSimpleSQLAsync.js b/storage/test/unit/test_connection_executeSimpleSQLAsync.js new file mode 100644 index 0000000000..00bdda7e03 --- /dev/null +++ b/storage/test/unit/test_connection_executeSimpleSQLAsync.js @@ -0,0 +1,94 @@ +/* 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 file tests the functionality of + * mozIStorageAsyncConnection::executeSimpleSQLAsync. + */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; + +add_task(async function test_create_and_add() { + let adb = await openAsyncDatabase(getTestDB()); + + let completion = await executeSimpleSQLAsync( + adb, + "CREATE TABLE test (id INTEGER, string TEXT, number REAL)" + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + completion = await executeSimpleSQLAsync( + adb, + "INSERT INTO test (id, string, number) " + + "VALUES (" + + INTEGER + + ', "' + + TEXT + + '", ' + + REAL + + ")" + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + + let result = null; + + completion = await executeSimpleSQLAsync( + adb, + "SELECT string, number FROM test WHERE id = 1", + function (aResultSet) { + result = aResultSet.getNextRow(); + Assert.equal(2, result.numEntries); + Assert.equal(TEXT, result.getString(0)); + Assert.equal(REAL, result.getDouble(1)); + } + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, completion); + Assert.notEqual(result, null); + result = null; + + await executeSimpleSQLAsync( + adb, + "SELECT COUNT(0) FROM test", + function (aResultSet) { + result = aResultSet.getNextRow(); + Assert.equal(1, result.getInt32(0)); + } + ); + + Assert.notEqual(result, null); + + await asyncClose(adb); +}); + +add_task(async function test_asyncClose_does_not_complete_before_statement() { + let adb = await openAsyncDatabase(getTestDB()); + let executed = false; + + let reason = await executeSimpleSQLAsync( + adb, + "SELECT * FROM test", + function (aResultSet) { + let result = aResultSet.getNextRow(); + + Assert.notEqual(result, null); + Assert.equal(3, result.numEntries); + Assert.equal(INTEGER, result.getInt32(0)); + Assert.equal(TEXT, result.getString(1)); + Assert.equal(REAL, result.getDouble(2)); + executed = true; + } + ); + + Assert.equal(Ci.mozIStorageStatementCallback.REASON_FINISHED, reason); + + // Ensure that the statement executed to completion. + Assert.ok(executed); + + await asyncClose(adb); +}); diff --git a/storage/test/unit/test_connection_failsafe_close.js b/storage/test/unit/test_connection_failsafe_close.js new file mode 100644 index 0000000000..d4fa6d1503 --- /dev/null +++ b/storage/test/unit/test_connection_failsafe_close.js @@ -0,0 +1,35 @@ +/* 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 file tests edge-cases related to mozStorageService::unregisterConnection + * in the face of failsafe closing at destruction time which results in + * SpinningSynchronousClose being invoked which can "resurrect" the connection + * and result in a second call to unregisterConnection. + * + * See https://bugzilla.mozilla.org/show_bug.cgi?id=1413501 for more context. + */ + +add_task(async function test_failsafe_close_of_async_connection() { + // get the db + let db = getOpenedDatabase(); + + // do something async + let callbackInvoked = new Promise(resolve => { + db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER)", { + handleCompletion: resolve, + }); + }); + + // drop our reference and force a GC so the only live reference is owned by + // the async statement. + db = gDBConn = null; + // (we don't need to cycle collect) + Cu.forceGC(); + + // now we need to wait for that callback to have completed. + await callbackInvoked; + + Assert.ok(true, "if we shutdown cleanly and do not crash, then we succeeded"); +}); diff --git a/storage/test/unit/test_connection_interrupt.js b/storage/test/unit/test_connection_interrupt.js new file mode 100644 index 0000000000..e257e9af82 --- /dev/null +++ b/storage/test/unit/test_connection_interrupt.js @@ -0,0 +1,125 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests the functionality of mozIStorageAsyncConnection::interrupt +// in the asynchronous case. +add_task(async function test_wr_async_conn() { + // Interrupt cannot be used on R/W async connections. + let db = await openAsyncDatabase(getTestDB()); + await db.interrupt(); + info("should be able to interrupt a R/W async connection"); + await asyncClose(db); +}); + +add_task(async function test_closed_conn() { + let db = await openAsyncDatabase(getTestDB(), { readOnly: true }); + await asyncClose(db); + Assert.throws( + () => db.interrupt(), + /NS_ERROR_NOT_INITIALIZED/, + "interrupt() should throw if invoked on a closed connection" + ); +}); + +add_task( + { + // We use a timeout in the test that may be insufficient on Android emulators. + // We don't really need the Android coverage, so skip on Android. + skip_if: () => AppConstants.platform == "android", + }, + async function test_async_conn() { + let db = await openAsyncDatabase(getTestDB(), { readOnly: true }); + // This query is built to hang forever. + let stmt = db.createAsyncStatement(` + WITH RECURSIVE test(n) AS ( + VALUES(1) + UNION ALL + SELECT n + 1 FROM test + ) + SELECT t.n + FROM test,test AS t`); + + let completePromise = new Promise((resolve, reject) => { + let listener = { + handleResult(aResultSet) { + reject(); + }, + handleError(aError) { + reject(); + }, + handleCompletion(aReason) { + resolve(aReason); + }, + }; + stmt.executeAsync(listener); + stmt.finalize(); + }); + + // Wait for the statement to be executing. + // This is not rock-solid, see the discussion in bug 1320301. A better + // approach will be evaluated in a separate bug. + await new Promise(resolve => do_timeout(500, resolve)); + + db.interrupt(); + + Assert.equal( + await completePromise, + Ci.mozIStorageStatementCallback.REASON_CANCELED, + "Should have been canceled" + ); + + await asyncClose(db); + } +); + +add_task( + { + // We use a timeout in the test that may be insufficient on Android emulators. + // We don't really need the Android coverage, so skip on Android. + skip_if: () => AppConstants.platform == "android", + }, + async function test_async_conn() { + let db = await openAsyncDatabase(getTestDB()); + // This query is built to hang forever. + let stmt = db.createAsyncStatement(` + WITH RECURSIVE test(n) AS ( + VALUES(1) + UNION ALL + SELECT n + 1 FROM test + ) + SELECT t.n + FROM test,test AS t`); + + let completePromise = new Promise((resolve, reject) => { + let listener = { + handleResult(aResultSet) { + reject(); + }, + handleError(aError) { + reject(); + }, + handleCompletion(aReason) { + resolve(aReason); + }, + }; + stmt.executeAsync(listener); + stmt.finalize(); + }); + + // Wait for the statement to be executing. + // This is not rock-solid, see the discussion in bug 1320301. A better + // approach will be evaluated in a separate bug. + await new Promise(resolve => do_timeout(500, resolve)); + + // We are going to interrupt a database connection + db.interrupt(); + + Assert.equal( + await completePromise, + Ci.mozIStorageStatementCallback.REASON_CANCELED, + "Should have been able to cancel even for R/W database" + ); + + await asyncClose(db); + } +); diff --git a/storage/test/unit/test_connection_online_backup.js b/storage/test/unit/test_connection_online_backup.js new file mode 100644 index 0000000000..3599d8c824 --- /dev/null +++ b/storage/test/unit/test_connection_online_backup.js @@ -0,0 +1,211 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * This test file tests the backupToFileAsync function on + * mozIStorageAsyncConnection, which is implemented for mozStorageConnection. + * (but not implemented for mozilla::dom::cache::Connection). + */ + +// The name of the backup database file that will be created. +const BACKUP_FILE_NAME = "test_storage.sqlite.backup"; +// The number of rows to insert into the test table in the source +// database. +const TEST_ROWS = 10; +// The page size to set on the source database. During setup, we assert that +// this does not match the default page size. +const TEST_PAGE_SIZE = 512; + +/** + * This setup function creates a table inside of the test database and inserts + * some test rows. Critically, it keeps the connection to the database _open_ + * so that we can test the scenario where a database is copied with existing + * open connections. + * + * The database is closed in a cleanup function. + */ +add_setup(async () => { + let conn = await openAsyncDatabase(getTestDB()); + + Assert.notEqual( + conn.defaultPageSize, + TEST_PAGE_SIZE, + "Should not default to having the TEST_PAGE_SIZE" + ); + + await executeSimpleSQLAsync(conn, "PRAGMA page_size = " + TEST_PAGE_SIZE); + + let createStmt = conn.createAsyncStatement("CREATE TABLE test(name TEXT)"); + await executeAsync(createStmt); + createStmt.finalize(); + + registerCleanupFunction(async () => { + await asyncClose(conn); + }); +}); + +/** + * Erases the test table and inserts TEST_ROWS rows into it. + * + * @param {mozIStorageAsyncConnection} connection + * The connection to use to prepare the database. + * @returns {Promise} + */ +async function prepareSourceDatabase(connection) { + await executeSimpleSQLAsync(connection, "DELETE from test"); + for (let i = 0; i < TEST_ROWS; ++i) { + let name = `Database row #${i}`; + let stmt = connection.createAsyncStatement( + "INSERT INTO test (name) VALUES (:name)" + ); + stmt.params.name = name; + let result = await executeAsync(stmt); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result), `Inserted test row #${i}`); + } +} + +/** + * Gets the test DB prepared with the testing table and rows. + * + * @returns {Promise} + */ +async function getPreparedAsyncDatabase() { + let connection = await openAsyncDatabase(getTestDB()); + await prepareSourceDatabase(connection); + return connection; +} + +/** + * Creates a copy of the database connected to via connection, and + * returns an nsIFile pointing at the created copy file once the + * copy is complete. + * + * @param {mozIStorageAsyncConnection} connection + * A connection to a database that should be copied. + * @returns {Promise} + */ +async function createCopy(connection) { + let destFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME); + let destFile = await IOUtils.getFile(destFilePath); + Assert.ok( + !(await IOUtils.exists(destFilePath)), + "Backup file shouldn't exist yet." + ); + + await new Promise(resolve => { + connection.backupToFileAsync(destFile, result => { + Assert.ok(Components.isSuccessCode(result)); + resolve(result); + }); + }); + + return destFile; +} + +/** + * Opens up the database at file, asserts that the page_size matches + * TEST_PAGE_SIZE, and that the number of rows in the test table matches + * expectedEntries. Closes the connection after these assertions. + * + * @param {nsIFile} file + * The database file to be opened and queried. + * @param {number} [expectedEntries=TEST_ROWS] + * The expected number of rows in the test table. Defaults to TEST_ROWS. + * @returns {Promise} + */ +async function assertSuccessfulCopy(file, expectedEntries = TEST_ROWS) { + let conn = await openAsyncDatabase(file); + + await executeSimpleSQLAsync(conn, "PRAGMA page_size", resultSet => { + let result = resultSet.getNextRow(); + Assert.equal(TEST_PAGE_SIZE, result.getResultByIndex(0).getAsUint32()); + }); + + let stmt = conn.createAsyncStatement("SELECT COUNT(*) FROM test"); + let results = await new Promise(resolve => { + executeAsync(stmt, resolve); + }); + stmt.finalize(); + let row = results.getNextRow(); + let count = row.getResultByName("COUNT(*)"); + Assert.equal(count, expectedEntries, "Got the expected entries"); + + Assert.ok( + !file.leafName.endsWith(".tmp"), + "Should not end in .tmp extension" + ); + + await asyncClose(conn); +} + +/** + * Test the basic behaviour of backupToFileAsync, and ensure that the copied + * database has the same characteristics and contents as the source database. + */ +add_task(async function test_backupToFileAsync() { + let newConnection = await getPreparedAsyncDatabase(); + let copyFile = await createCopy(newConnection); + Assert.ok( + await IOUtils.exists(copyFile.path), + "A new file was created by backupToFileAsync" + ); + + await assertSuccessfulCopy(copyFile); + await IOUtils.remove(copyFile.path); + await asyncClose(newConnection); +}); + +/** + * Tests that if insertions are underway during a copy, that those insertions + * show up in the copied database. + */ +add_task(async function test_backupToFileAsync_during_insert() { + let newConnection = await getPreparedAsyncDatabase(); + const NEW_ENTRIES = 5; + + let copyFilePromise = createCopy(newConnection); + let inserts = []; + for (let i = 0; i < NEW_ENTRIES; ++i) { + let name = `New database row #${i}`; + let stmt = newConnection.createAsyncStatement( + "INSERT INTO test (name) VALUES (:name)" + ); + stmt.params.name = name; + inserts.push(executeAsync(stmt)); + stmt.finalize(); + } + await Promise.all(inserts); + let copyFile = await copyFilePromise; + + Assert.ok( + await IOUtils.exists(copyFile.path), + "A new file was created by backupToFileAsync" + ); + + await assertSuccessfulCopy(copyFile, TEST_ROWS + NEW_ENTRIES); + await IOUtils.remove(copyFile.path); + await asyncClose(newConnection); +}); + +/** + * Tests the behaviour of backupToFileAsync as exposed through Sqlite.sys.mjs. + */ +add_task(async function test_backupToFileAsync_via_Sqlite_module() { + let xpcomConnection = await getPreparedAsyncDatabase(); + let moduleConnection = await Sqlite.openConnection({ + path: xpcomConnection.databaseFile.path, + }); + + let copyFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME); + await moduleConnection.backup(copyFilePath); + let copyFile = await IOUtils.getFile(copyFilePath); + Assert.ok(await IOUtils.exists(copyFilePath), "A new file was created"); + + await assertSuccessfulCopy(copyFile); + await IOUtils.remove(copyFile.path); + await moduleConnection.close(); + await asyncClose(xpcomConnection); +}); diff --git a/storage/test/unit/test_default_journal_size_limit.js b/storage/test/unit/test_default_journal_size_limit.js new file mode 100644 index 0000000000..f2d28b9aa4 --- /dev/null +++ b/storage/test/unit/test_default_journal_size_limit.js @@ -0,0 +1,44 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests defaul journal_size_limit + +async function check_journal_size(db) { + let stmt = db.createAsyncStatement("PRAGMA journal_size_limit"); + let value = await new Promise((resolve, reject) => { + stmt.executeAsync({ + handleResult(resultSet) { + resolve(resultSet.getNextRow().getResultByIndex(0)); + }, + handleError(error) { + reject(); + }, + handleCompletion() {}, + }); + }); + Assert.greater(value, 0, "There is a positive journal_size_limit"); + stmt.finalize(); + await new Promise(resolve => db.asyncClose(resolve)); +} + +async function getDbPath(name) { + let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite"); + Assert.ok(!(await IOUtils.exists(path))); + return path; +} + +add_task(async function () { + await check_journal_size( + Services.storage.openDatabase( + new FileUtils.File(await getDbPath("journal")) + ) + ); + await check_journal_size( + Services.storage.openUnsharedDatabase( + new FileUtils.File(await getDbPath("journalUnshared")) + ) + ); + await check_journal_size( + await openAsyncDatabase(new FileUtils.File(await getDbPath("journalAsync"))) + ); +}); diff --git a/storage/test/unit/test_js_helpers.js b/storage/test/unit/test_js_helpers.js new file mode 100644 index 0000000000..da533a78fc --- /dev/null +++ b/storage/test/unit/test_js_helpers.js @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set sw=2 ts=2 sts=2 et : */ +/** + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * This file tests that the JS language helpers in various ways. + */ + +// Test Functions + +function test_params_enumerate() { + let stmt = createStatement("SELECT * FROM test WHERE id IN (:a, :b, :c)"); + + // Make sure they are right. + let expected = [0, 1, 2, "a", "b", "c", "length"]; + let index = 0; + for (let name in stmt.params) { + if (name == "QueryInterface") { + continue; + } + Assert.equal(name, expected[index++]); + } + Assert.equal(index, 7); +} + +function test_params_prototype() { + let stmt = createStatement("SELECT * FROM sqlite_master"); + + // Set a property on the prototype and make sure it exist (will not be a + // bindable parameter, however). + Object.getPrototypeOf(stmt.params).test = 2; + Assert.equal(stmt.params.test, 2); + + delete Object.getPrototypeOf(stmt.params).test; + stmt.finalize(); +} + +function test_row_prototype() { + let stmt = createStatement("SELECT * FROM sqlite_master"); + + Assert.ok(stmt.executeStep()); + + // Set a property on the prototype and make sure it exists (will not be in the + // results, however). + Object.getPrototypeOf(stmt.row).test = 2; + Assert.equal(stmt.row.test, 2); + + // Clean up after ourselves. + delete Object.getPrototypeOf(stmt.row).test; + stmt.finalize(); +} + +function test_row_enumerate() { + let stmt = createStatement("SELECT * FROM test"); + + Assert.ok(stmt.executeStep()); + + let expected = ["id", "string"]; + let expected_values = [123, "foo"]; + let index = 0; + for (let name in stmt.row) { + Assert.equal(name, expected[index]); + Assert.equal(stmt.row[name], expected_values[index]); + index++; + } + Assert.equal(index, 2); + + // Save off the row helper, then forget the statement and trigger a GC. We + // want to ensure that if the row helper is retained but the statement is + // destroyed, that no crash occurs and that the late access attempt simply + // throws an error. + let savedOffRow = stmt.row; + stmt = null; + Cu.forceGC(); + Assert.throws( + () => { + return savedOffRow.string; + }, + /NS_ERROR_NOT_INITIALIZED/, + "GC'ed statement should throw" + ); +} + +function test_params_gets_sync() { + // Added for bug 562866. + /* + let stmt = createStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +function test_params_gets_async() { + // Added for bug 562866. + /* + let stmt = createAsyncStatement( + "SELECT * FROM test WHERE id IN (:a, :b, :c)" + ); + + // Make sure we do not assert in getting the value. + let originalCount = Object.getOwnPropertyNames(stmt.params).length; + let expected = ["a", "b", "c"]; + for (let name of expected) { + stmt.params[name]; + } + + // Now make sure we didn't magically get any additional properties. + let finalCount = Object.getOwnPropertyNames(stmt.params).length; + do_check_eq(originalCount + expected.length, finalCount); + */ +} + +// Test Runner + +var tests = [ + test_params_enumerate, + test_params_prototype, + test_row_enumerate, + test_row_prototype, + test_params_gets_sync, + test_params_gets_async, +]; +function run_test() { + cleanup(); + + // Create our database. + getOpenedDatabase().executeSimpleSQL( + "CREATE TABLE test (id INTEGER PRIMARY KEY, string TEXT)" + ); + getOpenedDatabase().executeSimpleSQL( + "INSERT INTO test (id, string) VALUES (123, 'foo')" + ); + + // Run the tests. + tests.forEach(test => test()); +} diff --git a/storage/test/unit/test_levenshtein.js b/storage/test/unit/test_levenshtein.js new file mode 100644 index 0000000000..a92b3a9559 --- /dev/null +++ b/storage/test/unit/test_levenshtein.js @@ -0,0 +1,66 @@ +/* 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 file tests the Levenshtein Distance function we've registered. + +function createUtf16Database() { + print("Creating the in-memory UTF-16-encoded database."); + let conn = Services.storage.openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + Assert.ok(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + Assert.ok(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +function check_levenshtein(db, s, t, expectedDistance) { + var stmt = db.createStatement("SELECT levenshteinDistance(:s, :t) AS result"); + stmt.params.s = s; + stmt.params.t = t; + try { + Assert.ok(stmt.executeStep()); + Assert.equal(expectedDistance, stmt.row.result); + } finally { + stmt.reset(); + stmt.finalize(); + } +} + +function testLevenshtein(db) { + // Basic tests. + check_levenshtein(db, "", "", 0); + check_levenshtein(db, "foo", "", 3); + check_levenshtein(db, "", "bar", 3); + check_levenshtein(db, "yellow", "hello", 2); + check_levenshtein(db, "gumbo", "gambol", 2); + check_levenshtein(db, "kitten", "sitten", 1); + check_levenshtein(db, "sitten", "sittin", 1); + check_levenshtein(db, "sittin", "sitting", 1); + check_levenshtein(db, "kitten", "sitting", 3); + check_levenshtein(db, "Saturday", "Sunday", 3); + check_levenshtein(db, "YHCQPGK", "LAHYQQKPGKA", 6); + + // Test SQL NULL handling. + check_levenshtein(db, "foo", null, null); + check_levenshtein(db, null, "bar", null); + check_levenshtein(db, null, null, null); + + // The levenshteinDistance function allocates temporary memory on the stack + // if it can. Test some strings long enough to force a heap allocation. + var dots1000 = Array(1001).join("."); + var dashes1000 = Array(1001).join("-"); + check_levenshtein(db, dots1000, dashes1000, 1000); +} + +function run_test() { + testLevenshtein(getOpenedDatabase()); + testLevenshtein(createUtf16Database()); +} diff --git a/storage/test/unit/test_like.js b/storage/test/unit/test_like.js new file mode 100644 index 0000000000..91674a8816 --- /dev/null +++ b/storage/test/unit/test_like.js @@ -0,0 +1,199 @@ +/* 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 file tests our LIKE implementation since we override it for unicode + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement("INSERT INTO t1 (x) VALUES ('a')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ab')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('acd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('abd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bc')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('bcd')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('xyz')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('CDE')"); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES ('ABC abc xyz')"); + stmt.execute(); + stmt.finalize(); +} + +function test_count() { + var stmt = createStatement("SELECT count(*) FROM t1;"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 12); + stmt.reset(); + stmt.finalize(); +} + +function test_like_1() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "abc"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_2() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "ABC"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_3() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "aBc"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_4() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "abc%"); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_5() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "a_c"); + var solutions = ["abc", "ABC"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_6() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "ab%d"); + var solutions = ["abcd", "abd"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_7() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "a_c%"); + var solutions = ["abc", "abcd", "ABC", "ABC abc xyz"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +function test_like_8() { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?;"); + stmt.bindByIndex(0, "%bcd"); + var solutions = ["abcd", "bcd"]; + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(stmt.executeStep()); + Assert.ok(solutions.includes(stmt.getString(0))); + Assert.ok(!stmt.executeStep()); + stmt.reset(); + stmt.finalize(); +} + +var tests = [ + test_count, + test_like_1, + test_like_2, + test_like_3, + test_like_4, + test_like_5, + test_like_6, + test_like_7, + test_like_8, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_like_escape.js b/storage/test/unit/test_like_escape.js new file mode 100644 index 0000000000..66d6d1d583 --- /dev/null +++ b/storage/test/unit/test_like_escape.js @@ -0,0 +1,72 @@ +/* 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/. */ + +const LATIN1_AE = "\xc6"; +const LATIN1_ae = "\xe6"; + +function setup() { + getOpenedDatabase().createTable("t1", "x TEXT"); + + var stmt = createStatement( + "INSERT INTO t1 (x) VALUES ('foo/bar_baz%20cheese')" + ); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("INSERT INTO t1 (x) VALUES (?1)"); + // insert LATIN_ae, but search on LATIN_AE + stmt.bindByIndex(0, "foo%20" + LATIN1_ae + "/_bar"); + stmt.execute(); + stmt.finalize(); +} + +function test_escape_for_like_ascii() { + const paramForLike = "oo/bar_baz%20chees"; + const escapeChar = "/"; + + for (const utf8 of [false, true]) { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLikeEscaped = utf8 + ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar) + : stmt.escapeStringForLIKE(paramForLike, escapeChar); + // verify that we escaped / _ and % + Assert.equal(paramForLikeEscaped, "oo//bar/_baz/%20chees"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%"); + stmt.executeStep(); + Assert.equal("foo/bar_baz%20cheese", stmt.getString(0)); + stmt.finalize(); + } +} + +function test_escape_for_like_non_ascii() { + const paramForLike = "oo%20" + LATIN1_AE + "/_ba"; + const escapeChar = "/"; + + for (const utf8 of [false, true]) { + var stmt = createStatement("SELECT x FROM t1 WHERE x LIKE ?1 ESCAPE '/'"); + var paramForLikeEscaped = utf8 + ? stmt.escapeUTF8StringForLIKE(paramForLike, escapeChar) + : stmt.escapeStringForLIKE(paramForLike, escapeChar); + // verify that we escaped / _ and % + Assert.equal(paramForLikeEscaped, "oo/%20" + LATIN1_AE + "///_ba"); + // prepend and append with % for "contains" + stmt.bindByIndex(0, "%" + paramForLikeEscaped + "%"); + stmt.executeStep(); + Assert.equal("foo%20" + LATIN1_ae + "/_bar", stmt.getString(0)); + stmt.finalize(); + } +} + +var tests = [test_escape_for_like_ascii, test_escape_for_like_non_ascii]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_locale_collation.js b/storage/test/unit/test_locale_collation.js new file mode 100644 index 0000000000..96f2415451 --- /dev/null +++ b/storage/test/unit/test_locale_collation.js @@ -0,0 +1,291 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +/** + * Bug 499990 - Locale-aware collation + * + * Tests our custom, locale-aware collating sequences. + */ + +// The name of the file containing the strings we'll sort during this test. +// The file's data is taken from intl/locale/tests/sort/us-ascii_base.txt and +// and intl/locale/tests/sort/us-ascii_sort.txt. +const DATA_BASENAME = "locale_collation.txt"; + +// The test data from DATA_BASENAME is read into this array. +var gStrings; + +// A connection to our in-memory UTF-16-encoded database. +var gUtf16Conn; + +// Helper Functions + +/** + * Since we create a UTF-16 database we have to clean it up, in addition to + * the normal cleanup of Storage tests. + */ +function cleanupLocaleTests() { + print("-- Cleaning up test_locale_collation.js suite."); + gUtf16Conn.close(); + cleanup(); +} + +/** + * Creates a test database similar to the default one created in + * head_storage.js, except that this one uses UTF-16 encoding. + * + * @return A connection to the database. + */ +function createUtf16Database() { + print("Creating the in-memory UTF-16-encoded database."); + let conn = Services.storage.openSpecialDatabase("memory"); + conn.executeSimpleSQL("PRAGMA encoding = 'UTF-16'"); + + print("Make sure the encoding was set correctly and is now UTF-16."); + let stmt = conn.createStatement("PRAGMA encoding"); + Assert.ok(stmt.executeStep()); + let enc = stmt.getString(0); + stmt.finalize(); + + // The value returned will actually be UTF-16le or UTF-16be. + Assert.ok(enc === "UTF-16le" || enc === "UTF-16be"); + + return conn; +} + +/** + * Compares aActual to aExpected, ensuring that the numbers and orderings of + * the two arrays' elements are the same. + * + * @param aActual + * An array of strings retrieved from the database. + * @param aExpected + * An array of strings to which aActual should be equivalent. + */ +function ensureResultsAreCorrect(aActual, aExpected) { + print("Actual results: " + aActual); + print("Expected results: " + aExpected); + + Assert.equal(aActual.length, aExpected.length); + for (let i = 0; i < aActual.length; i++) { + Assert.equal(aActual[i], aExpected[i]); + } +} + +/** + * Synchronously SELECTs all rows from the test table of the given database + * using the given collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows are + * ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + * @return The resulting strings in an array. + */ +function getResults(aCollation, aConn) { + let results = []; + let stmt = aConn.createStatement( + "SELECT t FROM test ORDER BY t COLLATE " + aCollation + " ASC" + ); + while (stmt.executeStep()) { + results.push(stmt.row.t); + } + stmt.finalize(); + return results; +} + +/** + * Inserts strings into our test table of the given database in the order given. + * + * @param aStrings + * An array of strings. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function initTableWithStrings(aStrings, aConn) { + print("Initializing test table."); + + aConn.executeSimpleSQL("DROP TABLE IF EXISTS test"); + aConn.createTable("test", "t TEXT"); + let stmt = aConn.createStatement("INSERT INTO test (t) VALUES (:t)"); + aStrings.forEach(function (str) { + stmt.params.t = str; + stmt.execute(); + stmt.reset(); + }); + stmt.finalize(); +} + +/** + * Returns a sorting function suitable for passing to Array.prototype.sort(). + * The returned function uses the application's locale to compare strings. + * + * @param aCollation + * The name of one of our custom locale collations. The sorting + * strength is computed from this value. + * @return A function to use as a sorting callback. + */ +function localeCompare(aCollation) { + let sensitivity; + + switch (aCollation) { + case "locale": + sensitivity = "base"; + break; + case "locale_case_sensitive": + sensitivity = "case"; + break; + case "locale_accent_sensitive": + sensitivity = "accent"; + break; + case "locale_case_accent_sensitive": + sensitivity = "variant"; + break; + default: + do_throw("Error in test: unknown collation '" + aCollation + "'"); + break; + } + const collation = new Intl.Collator("en", { sensitivity }); + return function (aStr1, aStr2) { + return collation.compare(aStr1, aStr2); + }; +} + +/** + * Reads in the test data from the file DATA_BASENAME and returns it as an array + * of strings. + * + * @return The test data as an array of strings. + */ +function readTestData() { + print("Reading in test data."); + + let file = do_get_file(DATA_BASENAME); + + let istream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + istream.init(file, -1, -1, 0); + istream.QueryInterface(Ci.nsILineInputStream); + + let line = {}; + let lines = []; + while (istream.readLine(line)) { + lines.push(line.value); + } + istream.close(); + + return lines; +} + +/** + * Gets the results from the given database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + * @param aConn + * A connection to either the UTF-8 database or the UTF-16 database. + */ +function runTest(aCollation, aConn) { + ensureResultsAreCorrect( + getResults(aCollation, aConn), + gStrings.slice(0).sort(localeCompare(aCollation)) + ); +} + +/** + * Gets the results from the UTF-8 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf8Test(aCollation) { + runTest(aCollation, getOpenedDatabase()); +} + +/** + * Gets the results from the UTF-16 database using the given collation and + * ensures that they match gStrings sorted by the same collation. + * + * @param aCollation + * The name of one of our custom locale collations. The rows from the + * database and the expected results are ordered by this collation. + */ +function runUtf16Test(aCollation) { + runTest(aCollation, gUtf16Conn); +} + +/** + * Sets up the test suite. + */ +function setup() { + print("-- Setting up the test_locale_collation.js suite."); + + gStrings = readTestData(); + + initTableWithStrings(gStrings, getOpenedDatabase()); + + gUtf16Conn = createUtf16Database(); + initTableWithStrings(gStrings, gUtf16Conn); +} + +// Test Runs + +var gTests = [ + { + desc: "Case and accent sensitive UTF-8", + run: () => runUtf8Test("locale_case_accent_sensitive"), + }, + + { + desc: "Case sensitive, accent insensitive UTF-8", + run: () => runUtf8Test("locale_case_sensitive"), + }, + + { + desc: "Case insensitive, accent sensitive UTF-8", + run: () => runUtf8Test("locale_accent_sensitive"), + }, + + { + desc: "Case and accent insensitive UTF-8", + run: () => runUtf8Test("locale"), + }, + + { + desc: "Case and accent sensitive UTF-16", + run: () => runUtf16Test("locale_case_accent_sensitive"), + }, + + { + desc: "Case sensitive, accent insensitive UTF-16", + run: () => runUtf16Test("locale_case_sensitive"), + }, + + { + desc: "Case insensitive, accent sensitive UTF-16", + run: () => runUtf16Test("locale_accent_sensitive"), + }, + + { + desc: "Case and accent insensitive UTF-16", + run: () => runUtf16Test("locale"), + }, +]; + +function run_test() { + setup(); + gTests.forEach(function (test) { + print("-- Running test: " + test.desc); + test.run(); + }); + cleanupLocaleTests(); +} diff --git a/storage/test/unit/test_minimizeMemory.js b/storage/test/unit/test_minimizeMemory.js new file mode 100644 index 0000000000..e694ddc186 --- /dev/null +++ b/storage/test/unit/test_minimizeMemory.js @@ -0,0 +1,23 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// This file tests that invoking `Service::minimizeMemory` succeeds for sync +// and async connections. + +function minimizeMemory() { + Services.storage + .QueryInterface(Ci.nsIObserver) + .observe(null, "memory-pressure", null); +} + +add_task(async function test_minimizeMemory_async_connection() { + let db = await openAsyncDatabase(getTestDB()); + minimizeMemory(); + await asyncClose(db); +}); + +add_task(async function test_minimizeMemory_sync_connection() { + let db = getOpenedDatabase(); + minimizeMemory(); + db.close(); +}); diff --git a/storage/test/unit/test_page_size_is_32k.js b/storage/test/unit/test_page_size_is_32k.js new file mode 100644 index 0000000000..4a91d13fdc --- /dev/null +++ b/storage/test/unit/test_page_size_is_32k.js @@ -0,0 +1,31 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests that dbs are using 32k pagesize + +const kExpectedPageSize = 32768; // 32K +const kExpectedCacheSize = -2048; // 2MiB + +function check_size(db) { + var stmt = db.createStatement("PRAGMA page_size"); + stmt.executeStep(); + Assert.equal(stmt.getInt32(0), kExpectedPageSize); + stmt.finalize(); + stmt = db.createStatement("PRAGMA cache_size"); + stmt.executeStep(); + Assert.equal(stmt.getInt32(0), kExpectedCacheSize); + stmt.finalize(); +} + +function new_file(name) { + var file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + Assert.ok(!file.exists()); + return file; +} + +function run_test() { + check_size(getDatabase(new_file("shared32k"))); + check_size(Services.storage.openUnsharedDatabase(new_file("unshared32k"))); +} diff --git a/storage/test/unit/test_persist_journal.js b/storage/test/unit/test_persist_journal.js new file mode 100644 index 0000000000..1eea5c703e --- /dev/null +++ b/storage/test/unit/test_persist_journal.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + * https://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests the journal persists on close. + +async function check_journal_persists(db, journal) { + let path = db.databaseFile.path; + info(`testing ${path}`); + await new Promise(resolve => { + db.executeSimpleSQLAsync(`PRAGMA journal_mode = ${journal}`, { + handleCompletion: resolve, + }); + }); + + await new Promise(resolve => { + db.executeSimpleSQLAsync("CREATE TABLE test (id INTEGER PRIMARY KEY)", { + handleCompletion: resolve, + }); + }); + + if (journal == "wal") { + Assert.ok(await IOUtils.exists(path + "-wal"), "-wal exists before close"); + Assert.greater( + (await IOUtils.stat(path + "-wal")).size, + 0, + "-wal size is non-zero" + ); + } else { + Assert.ok( + await IOUtils.exists(path + "-journal"), + "-journal exists before close" + ); + Assert.equal( + (await IOUtils.stat(path + "-journal")).size, + 0, + "-journal is truncated after every transaction" + ); + } + + await new Promise(resolve => db.asyncClose(resolve)); + + if (journal == "wal") { + Assert.ok(await IOUtils.exists(path + "-wal"), "-wal persists after close"); + Assert.equal( + (await IOUtils.stat(path + "-wal")).size, + 0, + "-wal has been truncated" + ); + } else { + Assert.ok( + await IOUtils.exists(path + "-journal"), + "-journal persists after close" + ); + Assert.equal( + (await IOUtils.stat(path + "-journal")).size, + 0, + "-journal has been truncated" + ); + } +} + +async function getDbPath(name) { + let path = PathUtils.join(PathUtils.profileDir, name + ".sqlite"); + Assert.ok(!(await IOUtils.exists(path)), "database should not exist"); + return path; +} + +add_task(async function () { + for (let journal of ["truncate", "wal"]) { + await check_journal_persists( + Services.storage.openDatabase( + new FileUtils.File(await getDbPath(`shared-${journal}`)) + ), + journal + ); + await check_journal_persists( + Services.storage.openUnsharedDatabase( + new FileUtils.File(await getDbPath(`unshared-${journal}`)) + ), + journal + ); + await check_journal_persists( + await openAsyncDatabase( + new FileUtils.File(await getDbPath(`async-${journal}`)) + ), + journal + ); + } +}); diff --git a/storage/test/unit/test_readonly-immutable-nolock_vfs.js b/storage/test/unit/test_readonly-immutable-nolock_vfs.js new file mode 100644 index 0000000000..5c11150b0a --- /dev/null +++ b/storage/test/unit/test_readonly-immutable-nolock_vfs.js @@ -0,0 +1,47 @@ +/* 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 file tests the readonly-immutable-nolock VFS. + +add_task(async function test() { + const path = PathUtils.join(PathUtils.profileDir, "ro"); + await IOUtils.makeDirectory(path); + const dbpath = PathUtils.join(path, "test-immutable.sqlite"); + + let conn = await Sqlite.openConnection({ path: dbpath }); + await conn.execute("PRAGMA journal_mode = WAL"); + await conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + Assert.ok(await IOUtils.exists(dbpath + "-wal"), "wal journal exists"); + await conn.close(); + + // The wal should have been merged at this point, but just in case... + info("Remove auxiliary files and set the folder as readonly"); + await IOUtils.remove(dbpath + "-wal", { ignoreAbsent: true }); + await IOUtils.setPermissions(path, 0o555); + registerCleanupFunction(async () => { + await IOUtils.setPermissions(path, 0o777); + await IOUtils.remove(path, { recursive: true }); + }); + + // Windows doesn't disallow creating files in read only folders. + if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") { + await Assert.rejects( + Sqlite.openConnection({ path: dbpath, readOnly: true }), + /NS_ERROR_FILE/, + "Should not be able to open the db because it can't create a wal journal" + ); + } + + // Open the database with ignoreLockingMode. + let conn2 = await Sqlite.openConnection({ + path: dbpath, + ignoreLockingMode: true, + }); + await conn2.execute("SELECT * FROM sqlite_master"); + Assert.ok( + !(await IOUtils.exists(dbpath + "-wal")), + "wal journal was not created" + ); + await conn2.close(); +}); diff --git a/storage/test/unit/test_retry_on_busy.js b/storage/test/unit/test_retry_on_busy.js new file mode 100644 index 0000000000..bf7cdafdad --- /dev/null +++ b/storage/test/unit/test_retry_on_busy.js @@ -0,0 +1,199 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* eslint-disable mozilla/no-arbitrary-setTimeout */ + +const { setTimeout } = ChromeUtils.importESModule( + "resource://gre/modules/Timer.sys.mjs" +); + +function getProfileFile(name) { + let file = do_get_profile(); + file.append(name); + return file; +} + +function promiseAsyncDatabase(name, openOptions = 0) { + return new Promise((resolve, reject) => { + let file = getProfileFile(name); + const connOptions = Ci.mozIStorageService.CONNECTION_DEFAULT; + Services.storage.openAsyncDatabase( + file, + openOptions, + connOptions, + (status, connection) => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to open database: ${status}`)); + } else { + connection.QueryInterface(Ci.mozIStorageAsyncConnection); + resolve(connection); + } + } + ); + }); +} + +function promiseClone(db, readOnly = false) { + return new Promise((resolve, reject) => { + db.asyncClone(readOnly, (status, clone) => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to clone connection: ${status}`)); + } else { + clone.QueryInterface(Ci.mozIStorageAsyncConnection); + resolve(clone); + } + }); + }); +} + +function promiseClose(db) { + return new Promise((resolve, reject) => { + db.asyncClose(status => { + if (!Components.isSuccessCode(status)) { + reject(new Error(`Failed to close connection: ${status}`)); + } else { + resolve(); + } + }); + }); +} + +function promiseExecuteStatement(statement) { + return new Promise((resolve, reject) => { + let rows = []; + statement.executeAsync({ + handleResult(resultSet) { + let row = null; + do { + row = resultSet.getNextRow(); + if (row) { + rows.push(row); + } + } while (row); + }, + handleError(error) { + reject(new Error(`Failed to execute statement: ${error.message}`)); + }, + handleCompletion(reason) { + if (reason == Ci.mozIStorageStatementCallback.REASON_FINISHED) { + resolve(rows); + } else { + reject(new Error("Statement failed to execute or was cancelled")); + } + }, + }); + }); +} + +add_task(async function test_retry_on_busy() { + info("Open first writer in WAL mode and set up schema"); + let db1 = await promiseAsyncDatabase("retry-on-busy.sqlite"); + + let walStmt = db1.createAsyncStatement(`PRAGMA journal_mode = WAL`); + await promiseExecuteStatement(walStmt); + let createAStmt = db1.createAsyncStatement(`CREATE TABLE a( + b INTEGER PRIMARY KEY + )`); + await promiseExecuteStatement(createAStmt); + let createPrevAStmt = db1.createAsyncStatement(`CREATE TEMP TABLE prevA( + curB INTEGER PRIMARY KEY, + prevB INTEGER NOT NULL + )`); + await promiseExecuteStatement(createPrevAStmt); + let createATriggerStmt = db1.createAsyncStatement(` + CREATE TEMP TRIGGER a_afterinsert_trigger + AFTER UPDATE ON a FOR EACH ROW + BEGIN + REPLACE INTO prevA(curB, prevB) VALUES(NEW.b, OLD.b); + END`); + await promiseExecuteStatement(createATriggerStmt); + + info("Open second writer"); + let db2 = await promiseClone(db1); + + info("Attach second writer to new database"); + let attachStmt = db2.createAsyncStatement(`ATTACH :path AS newDB`); + attachStmt.bindByName( + "path", + getProfileFile("retry-on-busy-attach.sqlite").path + ); + await promiseExecuteStatement(attachStmt); + + info("Create triggers on second writer"); + let createCStmt = db2.createAsyncStatement(`CREATE TABLE newDB.c( + d INTEGER PRIMARY KEY + )`); + await promiseExecuteStatement(createCStmt); + let createCTriggerStmt = db2.createAsyncStatement(` + CREATE TEMP TRIGGER c_afterdelete_trigger + AFTER DELETE ON c FOR EACH ROW + BEGIN + INSERT INTO a(b) VALUES(OLD.d); + END`); + await promiseExecuteStatement(createCTriggerStmt); + + info("Begin transaction on second writer"); + let begin2Stmt = db2.createAsyncStatement("BEGIN IMMEDIATE"); + await promiseExecuteStatement(begin2Stmt); + + info( + "Begin transaction on first writer; should busy-wait until second writer is done" + ); + let begin1Stmt = db1.createAsyncStatement("BEGIN IMMEDIATE"); + let promise1Began = promiseExecuteStatement(begin1Stmt); + let update1Stmt = db1.createAsyncStatement(`UPDATE a SET b = 3 WHERE b = 1`); + let promise1Updated = promiseExecuteStatement(update1Stmt); + + info("Wait 5 seconds"); + await new Promise(resolve => setTimeout(resolve, 5000)); + + info("Commit transaction on second writer"); + let insertIntoA2Stmt = db2.createAsyncStatement(`INSERT INTO a(b) VALUES(1)`); + await promiseExecuteStatement(insertIntoA2Stmt); + let insertIntoC2Stmt = db2.createAsyncStatement(`INSERT INTO c(d) VALUES(2)`); + await promiseExecuteStatement(insertIntoC2Stmt); + let deleteFromC2Stmt = db2.createAsyncStatement(`DELETE FROM c`); + await promiseExecuteStatement(deleteFromC2Stmt); + let commit2Stmt = db2.createAsyncStatement("COMMIT"); + await promiseExecuteStatement(commit2Stmt); + + info("Await and commit transaction on first writer"); + await promise1Began; + await promise1Updated; + + let commit1Stmt = db1.createAsyncStatement("COMMIT"); + await promiseExecuteStatement(commit1Stmt); + + info("Verify our writes succeeded"); + let select1Stmt = db2.createAsyncStatement("SELECT b FROM a"); + let rows = await promiseExecuteStatement(select1Stmt); + deepEqual( + rows.map(row => row.getResultByName("b")), + [2, 3] + ); + + info("Clean up"); + for (let stmt of [ + walStmt, + createAStmt, + createPrevAStmt, + createATriggerStmt, + attachStmt, + createCStmt, + createCTriggerStmt, + begin2Stmt, + begin1Stmt, + insertIntoA2Stmt, + insertIntoC2Stmt, + deleteFromC2Stmt, + + commit2Stmt, + update1Stmt, + commit1Stmt, + select1Stmt, + ]) { + stmt.finalize(); + } + await promiseClose(db1); + await promiseClose(db2); +}); diff --git a/storage/test/unit/test_sqlite_secure_delete.js b/storage/test/unit/test_sqlite_secure_delete.js new file mode 100644 index 0000000000..7beb1f1764 --- /dev/null +++ b/storage/test/unit/test_sqlite_secure_delete.js @@ -0,0 +1,78 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + *vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 file tests to make sure that SQLite was compiled with + * SQLITE_SECURE_DELETE=1. + */ + +// Helper Methods + +/** + * Reads the contents of a file and returns it as a string. + * + * @param aFile + * The file to return from. + * @return the contents of the file in the form of a string. + */ +function getFileContents(aFile) { + let fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(aFile, -1, 0, 0); + + let bstream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( + Ci.nsIBinaryInputStream + ); + bstream.setInputStream(fstream); + return bstream.readBytes(bstream.available()); +} + +// Tests + +add_test(function test_delete_removes_data() { + const TEST_STRING = "SomeRandomStringToFind"; + + let file = getTestDB(); + let db = Services.storage.openDatabase(file); + + // Create the table and insert the data. + db.createTable("test", "data TEXT"); + let stmt = db.createStatement("INSERT INTO test VALUES(:data)"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + + // Make sure this test is actually testing what it thinks by making sure the + // string shows up in the database. Because the previous statement was + // automatically wrapped in a transaction, the contents are already on disk. + let contents = getFileContents(file); + Assert.notEqual(-1, contents.indexOf(TEST_STRING)); + + // Delete the data, and then close the database. + stmt = db.createStatement("DELETE FROM test WHERE data = :data"); + stmt.params.data = TEST_STRING; + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + db.close(); + + // Check the file to see if the string can be found. + contents = getFileContents(file); + Assert.equal(-1, contents.indexOf(TEST_STRING)); + + run_next_test(); +}); + +function run_test() { + cleanup(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_executeAsync.js b/storage/test/unit/test_statement_executeAsync.js new file mode 100644 index 0000000000..dab15121a5 --- /dev/null +++ b/storage/test/unit/test_statement_executeAsync.js @@ -0,0 +1,1045 @@ +/* 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 file tests the functionality of mozIStorageBaseStatement::executeAsync + * for both mozIStorageStatement and mozIStorageAsyncStatement. + */ + +// This file uses the internal _quit from testing/xpcshell/head.js */ +/* global _quit */ + +const INTEGER = 1; +const TEXT = "this is test text"; +const REAL = 3.23; +const BLOB = [1, 2]; + +/** + * Execute the given statement asynchronously, spinning an event loop until the + * async statement completes. + * + * @param aStmt + * The statement to execute. + * @param [aOptions={}] + * @param [aOptions.error=false] + * If true we should expect an error whose code we do not care about. If + * a numeric value, that's the error code we expect and require. If we + * are expecting an error, we expect a completion reason of REASON_ERROR. + * Otherwise we expect no error notification and a completion reason of + * REASON_FINISHED. + * @param [aOptions.cancel] + * If true we cancel the pending statement and additionally return the + * pending statement in case you want to further manipulate it. + * @param [aOptions.returnPending=false] + * If true we keep the pending statement around and return it to you. We + * normally avoid doing this to try and minimize the amount of time a + * reference is held to the returned pending statement. + * @param [aResults] + * If omitted, we assume no results rows are expected. If it is a + * number, we assume it is the number of results rows expected. If it is + * a function, we assume it is a function that takes the 1) result row + * number, 2) result tuple, 3) call stack for the original call to + * execAsync as arguments. If it is a list, we currently assume it is a + * list of functions where each function is intended to evaluate the + * result row at that ordinal position and takes the result tuple and + * the call stack for the original call. + */ +function execAsync(aStmt, aOptions, aResults) { + let caller = Components.stack.caller; + if (aOptions == null) { + aOptions = {}; + } + + let resultsExpected; + let resultsChecker; + if (aResults == null) { + resultsExpected = 0; + } else if (typeof aResults == "number") { + resultsExpected = aResults; + } else if (typeof aResults == "function") { + resultsChecker = aResults; + } else { + // array + resultsExpected = aResults.length; + resultsChecker = function (aResultNum, aTup, aCaller) { + aResults[aResultNum](aTup, aCaller); + }; + } + let resultsSeen = 0; + + let errorCodeExpected = false; + let reasonExpected = Ci.mozIStorageStatementCallback.REASON_FINISHED; + let altReasonExpected = null; + if ("error" in aOptions) { + errorCodeExpected = aOptions.error; + if (errorCodeExpected) { + reasonExpected = Ci.mozIStorageStatementCallback.REASON_ERROR; + } + } + let errorCodeSeen = false; + + if ("cancel" in aOptions && aOptions.cancel) { + altReasonExpected = Ci.mozIStorageStatementCallback.REASON_CANCELED; + } + + let completed = false; + + let listener = { + handleResult(aResultSet) { + let row, + resultsSeenThisCall = 0; + while ((row = aResultSet.getNextRow()) != null) { + if (resultsChecker) { + resultsChecker(resultsSeen, row, caller); + } + resultsSeen++; + resultsSeenThisCall++; + } + + if (!resultsSeenThisCall) { + do_throw("handleResult invoked with 0 result rows!"); + } + }, + handleError(aError) { + if (errorCodeSeen) { + do_throw("handleError called when we already had an error!"); + } + errorCodeSeen = aError.result; + }, + handleCompletion(aReason) { + if (completed) { + // paranoia check + do_throw("Received a second handleCompletion notification!", caller); + } + + if (resultsSeen != resultsExpected) { + do_throw( + "Expected " + + resultsExpected + + " rows of results but " + + "got " + + resultsSeen + + " rows!", + caller + ); + } + + if (errorCodeExpected && !errorCodeSeen) { + do_throw("Expected an error, but did not see one.", caller); + } else if (errorCodeExpected != errorCodeSeen) { + do_throw( + "Expected error code " + + errorCodeExpected + + " but got " + + errorCodeSeen, + caller + ); + } + + if (aReason != reasonExpected && aReason != altReasonExpected) { + do_throw( + "Expected reason " + + reasonExpected + + (altReasonExpected ? " or " + altReasonExpected : "") + + " but got " + + aReason, + caller + ); + } + + completed = true; + }, + }; + + let pending; + // Only get a pending reference if we're supposed to do. + // (note: This does not stop XPConnect from holding onto one currently.) + if ( + ("cancel" in aOptions && aOptions.cancel) || + ("returnPending" in aOptions && aOptions.returnPending) + ) { + pending = aStmt.executeAsync(listener); + } else { + aStmt.executeAsync(listener); + } + + if ("cancel" in aOptions && aOptions.cancel) { + pending.cancel(); + } + + Services.tm.spinEventLoopUntil( + "Test(test_statement_executeAsync.js:execAsync)", + () => completed || _quit + ); + + return pending; +} + +/** + * Make sure that illegal SQL generates the expected runtime error and does not + * result in any crashes. Async-only since the synchronous case generates the + * error synchronously (and is tested elsewhere). + */ +function test_illegal_sql_async_deferred() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + // gibberish + let stmt = makeTestStatement("I AM A ROBOT. DO AS I SAY."); + execAsync(stmt, { error: Ci.mozIStorageError.ERROR }); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.failure, + 1 + ); + histogram.clear(); + + // legal SQL syntax, but with semantics issues. + stmt = makeTestStatement("SELECT destination FROM funkytown"); + execAsync(stmt, { error: Ci.mozIStorageError.ERROR }); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.failure, + 1 + ); + histogram.clear(); + + run_next_test(); +} +test_illegal_sql_async_deferred.asyncOnly = true; + +function test_create_table() { + // Ensure our table doesn't exist + Assert.ok(!getOpenedDatabase().tableExists("test")); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "CREATE TABLE test (" + + "id INTEGER, " + + "string TEXT, " + + "number REAL, " + + "nuller NULL, " + + "blober BLOB" + + ")" + ); + execAsync(stmt); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + // Check that the table has been created + Assert.ok(getOpenedDatabase().tableExists("test")); + + histogram.clear(); + + // Verify that it's created correctly (this will throw if it wasn't) + let checkStmt = getOpenedDatabase().createStatement( + "SELECT id, string, number, nuller, blober FROM test" + ); + checkStmt.finalize(); + + // Nothing has executed so the histogram should be empty. + Assert.ok(!histogram.snapshot().values); + + run_next_test(); +} + +function test_add_data() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + stmt.bindByIndex(3, null); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(0, INTEGER); + + execAsync(stmt); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + // Check that the result is in the table + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + INTEGER, + [TEXT, REAL, null, BLOB] + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_get_data() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement( + "SELECT string, number, nuller, blober, id FROM test WHERE id = ?" + ); + stmt.bindByIndex(0, INTEGER); + execAsync(stmt, {}, [ + function (tuple) { + Assert.notEqual(null, tuple); + + // Check that it's what we expect + Assert.ok(!tuple.getIsNull(0)); + Assert.equal(tuple.getResultByName("string"), tuple.getResultByIndex(0)); + Assert.equal(TEXT, tuple.getResultByName("string")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + tuple.getTypeOfIndex(0) + ); + + Assert.ok(!tuple.getIsNull(1)); + Assert.equal(tuple.getResultByName("number"), tuple.getResultByIndex(1)); + Assert.equal(REAL, tuple.getResultByName("number")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + tuple.getTypeOfIndex(1) + ); + + Assert.ok(tuple.getIsNull(2)); + Assert.equal(tuple.getResultByName("nuller"), tuple.getResultByIndex(2)); + Assert.equal(null, tuple.getResultByName("nuller")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + tuple.getTypeOfIndex(2) + ); + + Assert.ok(!tuple.getIsNull(3)); + var blobByName = tuple.getResultByName("blober"); + Assert.equal(BLOB.length, blobByName.length); + var blobByIndex = tuple.getResultByIndex(3); + Assert.equal(BLOB.length, blobByIndex.length); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blobByName[i]); + Assert.equal(BLOB[i], blobByIndex[i]); + } + var count = { value: 0 }; + var blob = { value: null }; + tuple.getBlob(3, count, blob); + Assert.equal(BLOB.length, count.value); + for (let i = 0; i < BLOB.length; i++) { + Assert.equal(BLOB[i], blob.value[i]); + } + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + tuple.getTypeOfIndex(3) + ); + + Assert.ok(!tuple.getIsNull(4)); + Assert.equal(tuple.getResultByName("id"), tuple.getResultByIndex(4)); + Assert.equal(INTEGER, tuple.getResultByName("id")); + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + tuple.getTypeOfIndex(4) + ); + }, + ]); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_tuple_out_of_bounds() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement("SELECT string FROM test"); + execAsync(stmt, {}, [ + function (tuple) { + Assert.notEqual(null, tuple); + + // Check all out of bounds - should throw + var methods = [ + "getTypeOfIndex", + "getInt32", + "getInt64", + "getDouble", + "getUTF8String", + "getString", + "getIsNull", + ]; + for (var i in methods) { + try { + tuple[methods[i]](tuple.numEntries); + do_throw("did not throw :("); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + } + + // getBlob requires more args... + try { + var blob = { value: null }; + var size = { value: 0 }; + tuple.getBlob(tuple.numEntries, blob, size); + do_throw("did not throw :("); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ILLEGAL_VALUE, e.result); + } + }, + ]); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 1 + ); + histogram.clear(); + + run_next_test(); +} + +function test_no_listener_works_on_success() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_results() { + var stmt = makeTestStatement("SELECT ?"); + stmt.bindByIndex(0, 1); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_no_listener_works_on_error() { + // commit without a transaction will trigger an error + var stmt = makeTestStatement("COMMIT"); + stmt.executeAsync(); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +function test_partial_listener_works() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + stmt.executeAsync({ + handleResult(aResultSet) {}, + }); + stmt.executeAsync({ + handleError(aError) {}, + }); + stmt.executeAsync({ + handleCompletion(aReason) {}, + }); + stmt.finalize(); + + // Run the next test. + run_next_test(); +} + +/** + * Dubious cancellation test that depends on system loading may or may not + * succeed in canceling things. It does at least test if calling cancel blows + * up. test_AsyncCancellation in test_true_async.cpp is our test that canceling + * actually works correctly. + */ +function test_immediate_cancellation() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + execAsync(stmt, { cancel: true }); + stmt.finalize(); + run_next_test(); +} + +/** + * Test that calling cancel twice throws the second time. + */ +function test_double_cancellation() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, { cancel: true }); + // And cancel again - expect an exception + expectError(Cr.NS_ERROR_UNEXPECTED, () => pendingStatement.cancel()); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that nothing untoward happens if we try and cancel something after it + * has fully run to completion. + */ +function test_cancellation_after_execution() { + var stmt = makeTestStatement("DELETE FROM test WHERE id = ?"); + stmt.bindByIndex(0, 0); + let pendingStatement = execAsync(stmt, { returnPending: true }); + // (the statement has fully executed at this point) + // canceling after the statement has run to completion should not throw! + pendingStatement.cancel(); + + stmt.finalize(); + run_next_test(); +} + +/** + * Verifies that a single statement can be executed more than once. Might once + * have been intended to also ensure that callback notifications were not + * incorrectly interleaved, but that part was brittle (it's totally fine for + * handleResult to get called multiple times) and not comprehensive. + */ +function test_double_execute() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + var stmt = makeTestStatement("SELECT 1"); + execAsync(stmt, null, 1); + execAsync(stmt, null, 1); + stmt.finalize(); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + TEST_DB_NAME, + TELEMETRY_VALUES.success, + 2 + ); + histogram.clear(); + + run_next_test(); +} + +function test_finalized_statement_does_not_crash() { + var stmt = makeTestStatement("SELECT * FROM TEST"); + stmt.finalize(); + // we are concerned about a crash here; an error is fine. + try { + stmt.executeAsync(); + } catch (ex) { + // Do nothing. + } + + // Run the next test. + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by index. + */ +function test_bind_direct_binding_params_by_index() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + let insertId = nextUniqueId++; + stmt.bindByIndex(0, insertId); + stmt.bindByIndex(1, TEXT); + stmt.bindByIndex(2, REAL); + stmt.bindByIndex(3, null); + stmt.bindBlobByIndex(4, BLOB, BLOB.length); + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB] + ); + run_next_test(); +} + +/** + * Bind by mozIStorageBindingParams on the mozIStorageBaseStatement by name. + */ +function test_bind_direct_binding_params_by_name() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + let insertId = nextUniqueId++; + stmt.bindByName("int", insertId); + stmt.bindByName("text", TEXT); + stmt.bindByName("real", REAL); + stmt.bindByName("null", null); + stmt.bindBlobByName("blob", BLOB); + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller, blober FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null, BLOB] + ); + run_next_test(); +} + +function test_bind_js_params_helper_by_index() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params[3] = null; + stmt.params[2] = REAL; + stmt.params[1] = TEXT; + stmt.params[0] = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null] + ); + run_next_test(); +} + +function test_bind_js_params_helper_by_name() { + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, NULL)" + ); + let insertId = nextUniqueId++; + // we cannot bind blobs this way; no blober + stmt.params.null = null; + stmt.params.real = REAL; + stmt.params.text = TEXT; + stmt.params.int = insertId; + execAsync(stmt); + stmt.finalize(); + verifyQuery( + "SELECT string, number, nuller FROM test WHERE id = ?", + insertId, + [TEXT, REAL, null] + ); + run_next_test(); +} + +function test_bind_multiple_rows_by_index() { + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (?, ?, ?, ?, ?)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByIndex(0, INTEGER); + bp.bindByIndex(1, TEXT); + bp.bindByIndex(2, REAL); + bp.bindByIndex(3, null); + bp.bindBlobByIndex(4, BLOB, BLOB.length); + array.addParams(bp); + Assert.equal(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +function test_bind_multiple_rows_by_name() { + const AMOUNT_TO_ADD = 5; + var stmt = makeTestStatement( + "INSERT INTO test (id, string, number, nuller, blober) " + + "VALUES (:int, :text, :real, :null, :blob)" + ); + var array = stmt.newBindingParamsArray(); + for (let i = 0; i < AMOUNT_TO_ADD; i++) { + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + bp.bindByName("text", TEXT); + bp.bindByName("real", REAL); + bp.bindByName("null", null); + bp.bindBlobByName("blob", BLOB); + array.addParams(bp); + Assert.equal(array.length, i + 1); + } + stmt.bindParameters(array); + + let rowCount = getTableRowCount("test"); + execAsync(stmt); + Assert.equal(rowCount + AMOUNT_TO_ADD, getTableRowCount("test")); + stmt.finalize(); + run_next_test(); +} + +/** + * Verify that a mozIStorageStatement instance throws immediately when we + * try and bind to an illegal index. + */ +function test_bind_out_of_bounds_sync_immediate() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => bp.bindByIndex(1, INTEGER)); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindBlobByIndex(1, BLOB, BLOB.length) + ); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_sync_immediate.syncOnly = true; + +/** + * Verify that a mozIStorageAsyncStatement reports an error asynchronously when + * we bind to an illegal index. + */ +function test_bind_out_of_bounds_async_deferred() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // There is no difference between variant and blob binding for async purposes. + bp.bindByIndex(1, INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, { error: Ci.mozIStorageError.RANGE }); + + stmt.finalize(); + run_next_test(); +} +test_bind_out_of_bounds_async_deferred.asyncOnly = true; + +function test_bind_no_such_name_sync_immediate() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + // Check variant binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindByName("doesnotexist", INTEGER) + ); + // Check blob binding. + expectError(Cr.NS_ERROR_INVALID_ARG, () => + bp.bindBlobByName("doesnotexist", BLOB) + ); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_sync_immediate.syncOnly = true; + +function test_bind_no_such_name_async_deferred() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:foo)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + + bp.bindByName("doesnotexist", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + execAsync(stmt, { error: Ci.mozIStorageError.RANGE }); + + stmt.finalize(); + run_next_test(); +} +test_bind_no_such_name_async_deferred.asyncOnly = true; + +function test_bind_bogus_type_by_index() { + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (?)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByIndex(0, run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_bogus_type_by_name() { + // We try to bind a JS Object here that should fail to bind. + let stmt = makeTestStatement("INSERT INTO test (blober) VALUES (:blob)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + Assert.throws(() => bp.bindByName("blob", run_test), /NS_ERROR_UNEXPECTED/); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_already_locked() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + + // We should get an error after we call addParams and try to bind again. + expectError(Cr.NS_ERROR_UNEXPECTED, () => bp.bindByName("int", INTEGER)); + + stmt.finalize(); + run_next_test(); +} + +function test_bind_params_array_already_locked() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp1 = array.newBindingParams(); + bp1.bindByName("int", INTEGER); + array.addParams(bp1); + let bp2 = array.newBindingParams(); + stmt.bindParameters(array); + bp2.bindByName("int", INTEGER); + + // We should get an error after we have bound the array to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array.addParams(bp2)); + + stmt.finalize(); + run_next_test(); +} + +function test_no_binding_params_from_locked_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array = stmt.newBindingParamsArray(); + let bp = array.newBindingParams(); + bp.bindByName("int", INTEGER); + array.addParams(bp); + stmt.bindParameters(array); + + // We should not be able to get a new BindingParams object after we have bound + // to the statement. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array.newBindingParams()); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array1 = stmt.newBindingParamsArray(); + let array2 = stmt.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + + // We should not be able to add bp to array2 since it was created from array1. + expectError(Cr.NS_ERROR_UNEXPECTED, () => array2.addParams(bp)); + + stmt.finalize(); + run_next_test(); +} + +function test_not_right_owning_statement() { + let stmt1 = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + let stmt2 = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let array1 = stmt1.newBindingParamsArray(); + stmt2.newBindingParamsArray(); + let bp = array1.newBindingParams(); + bp.bindByName("int", INTEGER); + array1.addParams(bp); + + // We should not be able to bind array1 since it was created from stmt1. + expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt2.bindParameters(array1)); + + stmt1.finalize(); + stmt2.finalize(); + run_next_test(); +} + +function test_bind_empty_array() { + let stmt = makeTestStatement("INSERT INTO test (id) VALUES (:int)"); + + let paramsArray = stmt.newBindingParamsArray(); + + // We should not be able to bind this array to the statement because it is + // empty. + expectError(Cr.NS_ERROR_UNEXPECTED, () => stmt.bindParameters(paramsArray)); + + stmt.finalize(); + run_next_test(); +} + +function test_multiple_results() { + let expectedResults = getTableRowCount("test"); + // Sanity check - we should have more than one result, but let's be sure. + Assert.ok(expectedResults > 1); + + // Now check that we get back two rows of data from our async query. + let stmt = makeTestStatement("SELECT * FROM test"); + execAsync(stmt, {}, expectedResults); + + stmt.finalize(); + run_next_test(); +} + +// Test Runner + +const TEST_PASS_SYNC = 0; +const TEST_PASS_ASYNC = 1; +/** + * We run 2 passes against the test. One where makeTestStatement generates + * synchronous (mozIStorageStatement) statements and one where it generates + * asynchronous (mozIStorageAsyncStatement) statements. + * + * Because of differences in the ability to know the number of parameters before + * dispatching, some tests are sync/async specific. These functions are marked + * with 'syncOnly' or 'asyncOnly' attributes and run_next_test knows what to do. + */ +var testPass = TEST_PASS_SYNC; + +/** + * Create a statement of the type under test per testPass. + * + * @param aSQL + * The SQL string from which to build a statement. + * @return a statement of the type under test per testPass. + */ +function makeTestStatement(aSQL) { + if (testPass == TEST_PASS_SYNC) { + return getOpenedDatabase().createStatement(aSQL); + } + return getOpenedDatabase().createAsyncStatement(aSQL); +} + +var tests = [ + test_illegal_sql_async_deferred, + test_create_table, + test_add_data, + test_get_data, + test_tuple_out_of_bounds, + test_no_listener_works_on_success, + test_no_listener_works_on_results, + test_no_listener_works_on_error, + test_partial_listener_works, + test_immediate_cancellation, + test_double_cancellation, + test_cancellation_after_execution, + test_double_execute, + test_finalized_statement_does_not_crash, + test_bind_direct_binding_params_by_index, + test_bind_direct_binding_params_by_name, + test_bind_js_params_helper_by_index, + test_bind_js_params_helper_by_name, + test_bind_multiple_rows_by_index, + test_bind_multiple_rows_by_name, + test_bind_out_of_bounds_sync_immediate, + test_bind_out_of_bounds_async_deferred, + test_bind_no_such_name_sync_immediate, + test_bind_no_such_name_async_deferred, + test_bind_bogus_type_by_index, + test_bind_bogus_type_by_name, + test_bind_params_already_locked, + test_bind_params_array_already_locked, + test_bind_empty_array, + test_no_binding_params_from_locked_array, + test_not_right_owning_array, + test_not_right_owning_statement, + test_multiple_results, +]; +var index = 0; + +const STARTING_UNIQUE_ID = 2; +var nextUniqueId = STARTING_UNIQUE_ID; + +function run_next_test() { + function _run_next_test() { + // use a loop so we can skip tests... + while (index < tests.length) { + let test = tests[index++]; + // skip tests not appropriate to the current test pass + if ( + (testPass == TEST_PASS_SYNC && "asyncOnly" in test) || + (testPass == TEST_PASS_ASYNC && "syncOnly" in test) + ) { + continue; + } + + // Asynchronous tests means that exceptions don't kill the test. + try { + print("****** Running the next test: " + test.name); + test(); + return; + } catch (e) { + do_throw(e); + } + } + + // if we only completed the first pass, move to the next pass + if (testPass == TEST_PASS_SYNC) { + print("********* Beginning mozIStorageAsyncStatement pass."); + testPass++; + index = 0; + // a new pass demands a new database + asyncCleanup(); + nextUniqueId = STARTING_UNIQUE_ID; + _run_next_test(); + return; + } + + // we did some async stuff; we need to clean up. + asyncCleanup(); + do_test_finished(); + } + + // Don't actually schedule another test if we're quitting. + if (!_quit) { + // For saner stacks, we execute this code RSN. + executeSoon(_run_next_test); + } +} + +function run_test() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + cleanup(); + + do_test_pending(); + run_next_test(); +} diff --git a/storage/test/unit/test_statement_wrapper_automatically.js b/storage/test/unit/test_statement_wrapper_automatically.js new file mode 100644 index 0000000000..8ff203f7c7 --- /dev/null +++ b/storage/test/unit/test_statement_wrapper_automatically.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + vim:set ts=2 sw=2 sts=2 et: + * 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 file tests the functions of mozIStorageStatementWrapper + +function setup() { + getOpenedDatabase().createTable( + "test", + "id INTEGER PRIMARY KEY, val NONE,alt_val NONE" + ); +} + +/** + * A convenience wrapper for do_check_eq. Calls do_check_eq on aActualVal + * and aReturnedVal, with one caveat. + * + * Date objects are converted before parameter binding to PRTime's (microsecs + * since epoch). They are not reconverted when retrieved from the database. + * This function abstracts away this reconversion so that you can pass in, + * for example: + * + * checkVal(new Date(), aReturnedVal) // this + * checkVal(new Date().valueOf() * 1000.0, aReturnedVal) // instead of this + * + * Should any other types require conversion in the future, their conversions + * may also be abstracted away here. + * + * @param aActualVal + * the value inserted into the database + * @param aReturnedVal + * the value retrieved from the database + */ +function checkVal(aActualVal, aReturnedVal) { + if (aActualVal instanceof Date) { + aActualVal = aActualVal.valueOf() * 1000.0; + } + Assert.equal(aActualVal, aReturnedVal); +} + +/** + * Removes all rows from our test table. + */ +function clearTable() { + var stmt = createStatement("DELETE FROM test"); + stmt.execute(); + stmt.finalize(); + ensureNumRows(0); +} + +/** + * Ensures that the number of rows in our test table is equal to aNumRows. + * Calls do_check_eq on aNumRows and the value retrieved by SELECT'ing COUNT(*). + * + * @param aNumRows + * the number of rows our test table should contain + */ +function ensureNumRows(aNumRows) { + var stmt = createStatement("SELECT COUNT(*) AS number FROM test"); + Assert.ok(stmt.step()); + Assert.equal(aNumRows, stmt.row.number); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to a single parameter. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckSingleParam(aVal) { + clearTable(); + + var stmt = createStatement("INSERT INTO test (val) VALUES (:val)"); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val FROM test WHERE id = 1"); + Assert.ok(stmt.step()); + checkVal(aVal, stmt.row.val); + stmt.reset(); + stmt.finalize(); +} + +/** + * Inserts aVal into our test table and checks that insertion was successful by + * retrieving the newly inserted value from the database and comparing it + * against aVal. aVal is bound to two separate parameters, both of which are + * checked against aVal. + * + * @param aVal + * value to insert into our test table and check + */ +function insertAndCheckMultipleParams(aVal) { + clearTable(); + + var stmt = createStatement( + "INSERT INTO test (val, alt_val) VALUES (:val, :val)" + ); + stmt.params.val = aVal; + stmt.execute(); + stmt.finalize(); + + ensureNumRows(1); + + stmt = createStatement("SELECT val, alt_val FROM test WHERE id = 1"); + Assert.ok(stmt.step()); + checkVal(aVal, stmt.row.val); + checkVal(aVal, stmt.row.alt_val); + stmt.reset(); + stmt.finalize(); +} + +/** + * A convenience function that prints out a description of aVal using + * aVal.toString and aVal.toSource. Output is useful when the test fails. + * + * @param aVal + * a value inserted or to be inserted into our test table + */ +function printValDesc(aVal) { + try { + var toSource = aVal.toSource(); + } catch (ex) { + toSource = ""; + } + print( + "Testing value: toString=" + + aVal + + (toSource ? " toSource=" + toSource : "") + ); +} + +function run_test() { + setup(); + + // function JSValStorageStatementBinder in + // storage/mozStorageStatementParams.cpp tells us that the following types + // and only the following types are valid as statement parameters: + var vals = [ + 1337, // int + 3.1337, // double + "foo", // string + true, // boolean + null, // null + new Date(), // Date object + ]; + + vals.forEach(function (val) { + printValDesc(val); + print("Single parameter"); + insertAndCheckSingleParam(val); + print("Multiple parameters"); + insertAndCheckMultipleParams(val); + }); + + cleanup(); +} diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js new file mode 100644 index 0000000000..e7175a9613 --- /dev/null +++ b/storage/test/unit/test_storage_connection.js @@ -0,0 +1,997 @@ +/* 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 file tests the functions of mozIStorageConnection + +function fetchAllNames(conn) { + let names = []; + let stmt = conn.createStatement(`SELECT name FROM test ORDER BY name`); + while (stmt.executeStep()) { + names.push(stmt.getUTF8String(0)); + } + stmt.finalize(); + return names; +} + +// Test Functions + +add_task(async function test_connectionReady_open() { + // there doesn't seem to be a way for the connection to not be ready (unless + // we close it with mozIStorageConnection::Close(), but we don't for this). + // It can only fail if GetPath fails on the database file, or if we run out + // of memory trying to use an in-memory database + + var msc = getOpenedDatabase(); + Assert.ok(msc.connectionReady); +}); + +add_task(async function test_connectionReady_closed() { + // This also tests mozIStorageConnection::Close() + + var msc = getOpenedDatabase(); + msc.close(); + Assert.ok(!msc.connectionReady); + gDBConn = null; // this is so later tests don't start to fail. +}); + +add_task(async function test_databaseFile() { + var msc = getOpenedDatabase(); + Assert.ok(getTestDB().equals(msc.databaseFile)); +}); + +add_task(async function test_tableExists_not_created() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.tableExists("foo")); +}); + +add_task(async function test_indexExists_not_created() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.indexExists("foo")); +}); + +add_task(async function test_temp_tableExists_and_indexExists() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL( + "CREATE TEMP TABLE test_temp(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)" + ); + Assert.ok(msc.tableExists("test_temp")); + + msc.executeSimpleSQL("CREATE INDEX test_temp_ind ON test_temp (name)"); + Assert.ok(msc.indexExists("test_temp_ind")); + + msc.executeSimpleSQL("DROP INDEX test_temp_ind"); + msc.executeSimpleSQL("DROP TABLE test_temp"); +}); + +add_task(async function test_createTable_not_created() { + var msc = getOpenedDatabase(); + msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + Assert.ok(msc.tableExists("test")); +}); + +add_task(async function test_indexExists_created() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("CREATE INDEX name_ind ON test (name)"); + Assert.ok(msc.indexExists("name_ind")); +}); + +add_task(async function test_createTable_already_created() { + var msc = getOpenedDatabase(); + Assert.ok(msc.tableExists("test")); + Assert.throws( + () => msc.createTable("test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/ + ); +}); + +add_task(async function test_attach_createTable_tableExists_indexExists() { + var msc = getOpenedDatabase(); + var file = do_get_file("storage_attach.sqlite", true); + var msc2 = getDatabase(file); + msc.executeSimpleSQL("ATTACH DATABASE '" + file.path + "' AS sample"); + + Assert.ok(!msc.tableExists("sample.test")); + msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"); + Assert.ok(msc.tableExists("sample.test")); + Assert.throws( + () => msc.createTable("sample.test", "id INTEGER PRIMARY KEY, name TEXT"), + /NS_ERROR_FAILURE/ + ); + + Assert.ok(!msc.indexExists("sample.test_ind")); + msc.executeSimpleSQL("CREATE INDEX sample.test_ind ON test (name)"); + Assert.ok(msc.indexExists("sample.test_ind")); + + msc.executeSimpleSQL("DETACH DATABASE sample"); + msc2.close(); + try { + file.remove(false); + } catch (e) { + // Do nothing. + } +}); + +add_task(async function test_lastInsertRowID() { + var msc = getOpenedDatabase(); + msc.executeSimpleSQL("INSERT INTO test (name) VALUES ('foo')"); + Assert.equal(1, msc.lastInsertRowID); +}); + +add_task(async function test_transactionInProgress_no() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); +}); + +add_task(async function test_transactionInProgress_yes() { + var msc = getOpenedDatabase(); + msc.beginTransaction(); + Assert.ok(msc.transactionInProgress); + msc.commitTransaction(); + Assert.ok(!msc.transactionInProgress); + + msc.beginTransaction(); + Assert.ok(msc.transactionInProgress); + msc.rollbackTransaction(); + Assert.ok(!msc.transactionInProgress); +}); + +add_task(async function test_commitTransaction_no_transaction() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); + Assert.throws(() => msc.commitTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(async function test_rollbackTransaction_no_transaction() { + var msc = getOpenedDatabase(); + Assert.ok(!msc.transactionInProgress); + Assert.throws(() => msc.rollbackTransaction(), /NS_ERROR_UNEXPECTED/); +}); + +add_task(async function test_get_schemaVersion_not_set() { + Assert.equal(0, getOpenedDatabase().schemaVersion); +}); + +add_task(async function test_set_schemaVersion() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_set_schemaVersion_same() { + var msc = getOpenedDatabase(); + const version = 1; + msc.schemaVersion = version; // should still work ok + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_set_schemaVersion_negative() { + var msc = getOpenedDatabase(); + const version = -1; + msc.schemaVersion = version; + Assert.equal(version, msc.schemaVersion); +}); + +add_task(async function test_createTable() { + var temp = getTestDB().parent; + temp.append("test_db_table"); + try { + var con = Services.storage.openDatabase(temp); + con.createTable("a", ""); + } catch (e) { + if (temp.exists()) { + try { + temp.remove(false); + } catch (e2) { + // Do nothing. + } + } + Assert.ok( + e.result == Cr.NS_ERROR_NOT_INITIALIZED || e.result == Cr.NS_ERROR_FAILURE + ); + } finally { + if (con) { + con.close(); + } + } +}); + +add_task(async function test_defaultSynchronousAtNormal() { + getOpenedDatabase(); + var stmt = createStatement("PRAGMA synchronous;"); + try { + stmt.executeStep(); + Assert.equal(1, stmt.getInt32(0)); + } finally { + stmt.reset(); + stmt.finalize(); + } +}); + +// must be ran before executeAsync tests +add_task(async function test_close_does_not_spin_event_loop() { + // We want to make sure that the event loop on the calling thread does not + // spin when close is called. + let event = { + ran: false, + run() { + this.ran = true; + }, + }; + + // Post the event before we call close, so it would run if the event loop was + // spun during close. + Services.tm.dispatchToMainThread(event); + + // Sanity check, then close the database. Afterwards, we should not have ran! + Assert.ok(!event.ran); + getOpenedDatabase().close(); + Assert.ok(!event.ran); + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; +}); + +add_task( + async function test_asyncClose_succeeds_with_finalized_async_statement() { + // XXX this test isn't perfect since we can't totally control when events will + // run. If this paticular function fails randomly, it means we have a + // real bug. + + // We want to make sure we create a cached async statement to make sure that + // when we finalize our statement, we end up finalizing the async one too so + // close will succeed. + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + await asyncClose(getOpenedDatabase()); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + } +); + +// Would assert on debug builds. +if (!AppConstants.DEBUG) { + add_task(async function test_close_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement( + "SELECT * FROM test -- test_close_then_release_statement" + ); + db.close(); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); + + add_task(async function test_asyncClose_then_release_statement() { + // Testing the behavior in presence of a bad client that finalizes + // statements after the database has been async closed (typically by + // letting the gc finalize the statement). + let db = getOpenedDatabase(); + let stmt = createStatement( + "SELECT * FROM test -- test_asyncClose_then_release_statement" + ); + await asyncClose(db); + stmt.finalize(); // Finalize too late - this should not crash + + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); +} + +// In debug builds this would cause a fatal assertion. +if (!AppConstants.DEBUG) { + add_task(async function test_close_fails_with_async_statement_ran() { + let stmt = createStatement("SELECT * FROM test"); + stmt.executeAsync(); + stmt.finalize(); + + let db = getOpenedDatabase(); + Assert.throws(() => db.close(), /NS_ERROR_UNEXPECTED/); + // Reset gDBConn so that later tests will get a new connection object. + gDBConn = null; + }); +} + +add_task(async function test_clone_optional_param() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // A write statement should not fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "dwitte"; + stmt.execute(); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + // Additionally check that it is a connection on the same database. + Assert.ok(db1.databaseFile.equals(db2.databaseFile)); + + db1.close(); + db2.close(); +}); + +async function standardAsyncTest(promisedDB, name, shouldInit = false) { + info("Performing standard async test " + name); + + let adb = await promisedDB; + Assert.ok(adb instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb instanceof Ci.mozIStorageConnection); + + if (shouldInit) { + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + await executeAsync(stmt); + stmt.finalize(); + } + + // Generate a name to insert and fetch back + name = "worker bee " + Math.random() + " (" + name + ")"; + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = name; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == name) { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + await asyncClose(adb); + + info("Standard async test " + name + " complete"); +} + +add_task(async function test_open_async() { + await standardAsyncTest(openAsyncDatabase(getTestDB(), null), "default"); + await standardAsyncTest(openAsyncDatabase(getTestDB()), "no optional arg"); + await standardAsyncTest( + openAsyncDatabase(getTestDB(), { shared: false, interruptible: true }), + "non-default options" + ); + await standardAsyncTest( + openAsyncDatabase("memory"), + "in-memory database", + true + ); + await standardAsyncTest( + openAsyncDatabase("memory", { shared: false }), + "in-memory database and options", + true + ); + + info("Testing async opening with readonly option"); + const impliedReadOnlyOption = { ignoreLockingMode: true }; + + let raised = false; + let adb = await openAsyncDatabase(getTestDB(), impliedReadOnlyOption); + let stmt = adb.createAsyncStatement("CREATE TABLE test(name TEXT)"); + try { + await executeAsync(stmt); // This should throw + } catch (e) { + raised = true; + } finally { + if (stmt) { + stmt.finalize(); + } + if (adb) { + await asyncClose(adb); + } + } + + Assert.ok(raised); +}); + +add_task(async function test_async_open_with_shared_cache() { + info("Testing that opening with a shared cache doesn't break stuff"); + let adb = await openAsyncDatabase(getTestDB(), { shared: true }); + + let stmt = adb.createAsyncStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "clockworker"; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data"); + stmt = adb.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == "clockworker") { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + await asyncClose(adb); +}); + +add_task(async function test_clone_trivial_async() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + info("AsyncClone connection"); + let clone = await asyncClone(db, true); + Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(clone instanceof Ci.mozIStorageConnection); + info("Close connection"); + await asyncClose(db); + info("Close clone"); + await asyncClose(clone); +}); + +add_task(async function test_clone_no_optional_param_async() { + "use strict"; + info("Testing async cloning"); + let adb1 = await openAsyncDatabase(getTestDB(), null); + Assert.ok(adb1 instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb1 instanceof Ci.mozIStorageConnection); + + info("Cloning database"); + + let adb2 = await asyncClone(adb1); + info( + "Testing that the cloned db is a mozIStorageAsyncConnection " + + "and not a mozIStorageConnection" + ); + Assert.ok(adb2 instanceof Ci.mozIStorageAsyncConnection); + Assert.ok(adb2 instanceof Ci.mozIStorageConnection); + + info("Inserting data into source db"); + let stmt = adb1.createAsyncStatement( + "INSERT INTO test (name) VALUES (:name)" + ); + + stmt.params.name = "yoric"; + let result = await executeAsync(stmt); + info("Request complete"); + stmt.finalize(); + Assert.ok(Components.isSuccessCode(result)); + info("Extracting data from clone db"); + stmt = adb2.createAsyncStatement("SELECT * FROM test"); + let found = false; + await executeAsync(stmt, function (results) { + info("Data has been extracted"); + for ( + let row = results.getNextRow(); + row != null; + row = results.getNextRow() + ) { + if (row.getResultByName("name") == "yoric") { + found = true; + break; + } + } + }); + Assert.ok(found); + stmt.finalize(); + info("Closing databases"); + await asyncClose(adb2); + info("First db closed"); + + await asyncClose(adb1); + info("Second db closed"); +}); + +add_task(async function test_clone_readonly() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + // A write statement should fail here. + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "reed"; + expectError(Cr.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(async function test_clone_shared_readonly() { + let db1 = Services.storage.openDatabase(getTestDB()); + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + let stmt = db2.createStatement("INSERT INTO test (name) VALUES (:name)"); + stmt.params.name = "parker"; + // TODO currently SQLite does not actually work correctly here. The behavior + // we want is commented out, and the current behavior is being tested + // for. Our IDL comments will have to be updated when this starts to + // work again. + stmt.execute(); + // expectError(Components.results.NS_ERROR_FILE_READ_ONLY, () => stmt.execute()); + stmt.finalize(); + + // And a read statement should succeed. + stmt = db2.createStatement("SELECT * FROM test"); + Assert.ok(stmt.executeStep()); + stmt.finalize(); + + db1.close(); + db2.close(); +}); + +add_task(async function test_close_clone_fails() { + let calls = ["openDatabase", "openUnsharedDatabase"]; + calls.forEach(function (methodName) { + let db = Services.storage[methodName](getTestDB()); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + }); +}); + +add_task(async function test_clone_copies_functions() { + const FUNC_NAME = "test_func"; + let calls = ["openDatabase", "openUnsharedDatabase"]; + let functionMethods = ["createFunction"]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = Services.storage[methodName](getTestDB()); + // Create a function for db1. + db1[functionMethod](FUNC_NAME, 1, { + onFunctionCall: () => 0, + onStep: () => 0, + onFinal: () => 0, + }); + + // Clone it, and make sure the function exists still. + let db2 = db1.clone(readOnly); + // Note: this would fail if the function did not exist. + let stmt = db2.createStatement( + "SELECT " + FUNC_NAME + "(id) FROM test" + ); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(async function test_clone_copies_overridden_functions() { + const FUNC_NAME = "lower"; + function test_func() { + this.called = false; + } + test_func.prototype = { + onFunctionCall() { + this.called = true; + }, + onStep() { + this.called = true; + }, + onFinal: () => 0, + }; + + let calls = ["openDatabase", "openUnsharedDatabase"]; + let functionMethods = ["createFunction"]; + calls.forEach(function (methodName) { + [true, false].forEach(function (readOnly) { + functionMethods.forEach(function (functionMethod) { + let db1 = Services.storage[methodName](getTestDB()); + // Create a function for db1. + let func = new test_func(); + db1[functionMethod](FUNC_NAME, 1, func); + Assert.ok(!func.called); + + // Clone it, and make sure the function gets called. + let db2 = db1.clone(readOnly); + let stmt = db2.createStatement( + "SELECT " + FUNC_NAME + "(id) FROM test" + ); + stmt.executeStep(); + Assert.ok(func.called); + stmt.finalize(); + db1.close(); + db2.close(); + }); + }); + }); +}); + +add_task(async function test_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: true }, + { name: "journal_size_limit", value: 524288, copied: true }, + { name: "synchronous", value: 2, copied: true }, + { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + Assert.notEqual(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + let validate = pragma.copied ? "equal" : "notEqual"; + Assert[validate](pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(async function test_readonly_clone_copies_pragmas() { + const PRAGMAS = [ + { name: "cache_size", value: 500, copied: true }, + { name: "temp_store", value: 2, copied: true }, + { name: "foreign_keys", value: 1, copied: false }, + { name: "journal_size_limit", value: 524288, copied: false }, + { name: "synchronous", value: 2, copied: false }, + { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, + { name: "ignore_check_constraints", value: 1, copied: false }, + ]; + + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + // Sanity check initial values are different from enforced ones. + PRAGMAS.forEach(function (pragma) { + let stmt = db1.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + Assert.notEqual(pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + // Execute pragmas. + PRAGMAS.forEach(function (pragma) { + db1.executeSimpleSQL("PRAGMA " + pragma.name + " = " + pragma.value); + }); + + let db2 = db1.clone(true); + Assert.ok(db2.connectionReady); + + // Check cloned connection inherited pragma values. + PRAGMAS.forEach(function (pragma) { + let stmt = db2.createStatement("PRAGMA " + pragma.name); + Assert.ok(stmt.executeStep()); + let validate = pragma.copied ? "equal" : "notEqual"; + Assert[validate](pragma.value, stmt.getInt32(0)); + stmt.finalize(); + }); + + db1.close(); + db2.close(); +}); + +add_task(async function test_clone_attach_database() { + let db1 = Services.storage.openUnsharedDatabase(getTestDB()); + + let c = 0; + function attachDB(conn, name) { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append("test_storage_" + ++c + ".sqlite"); + let db = Services.storage.openUnsharedDatabase(file); + conn.executeSimpleSQL( + `ATTACH DATABASE '${db.databaseFile.path}' AS ${name}` + ); + db.executeSimpleSQL(`CREATE TABLE test_${name}(name TEXT);`); + db.close(); + } + attachDB(db1, "attached_1"); + attachDB(db1, "attached_2"); + db1.executeSimpleSQL(` + CREATE TEMP TRIGGER test_temp_afterinsert_trigger + AFTER DELETE ON test_attached_1 FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`); + + // These should not throw. + let stmt = db1.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db1.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + db1.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('asuth')"); + db1.executeSimpleSQL("DELETE FROM test_attached_1"); + Assert.ok(fetchAllNames(db1).includes("asuth")); + + // R/W clone. + let db2 = db1.clone(); + Assert.ok(db2.connectionReady); + + // These should not throw. + stmt = db2.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db2.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + db2.executeSimpleSQL("INSERT INTO test_attached_1(name) VALUES('past')"); + db2.executeSimpleSQL("DELETE FROM test_attached_1"); + let newNames = fetchAllNames(db2); + Assert.ok(newNames.includes("past")); + Assert.deepEqual(fetchAllNames(db1), newNames); + + // R/O clone. + let db3 = db1.clone(true); + Assert.ok(db3.connectionReady); + + // These should not throw. + stmt = db3.createStatement("SELECT * FROM attached_1.sqlite_master"); + stmt.finalize(); + stmt = db3.createStatement("SELECT * FROM attached_2.sqlite_master"); + stmt.finalize(); + + db1.close(); + db2.close(); + db3.close(); +}); + +add_task(async function test_async_clone_with_temp_trigger_and_table() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Set up tables on original connection"); + let createQueries = [ + `CREATE TEMP TABLE test_temp(name TEXT)`, + `CREATE INDEX test_temp_idx ON test_temp(name)`, + `CREATE TEMP TRIGGER test_temp_afterdelete_trigger + AFTER DELETE ON test_temp FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`, + ]; + for (let query of createQueries) { + let stmt = db.createAsyncStatement(query); + await executeAsync(stmt); + stmt.finalize(); + } + + info("Create read-write clone with temp tables"); + let readWriteClone = await asyncClone(db, false); + Assert.ok(readWriteClone instanceof Ci.mozIStorageAsyncConnection); + + info("Insert into temp table on read-write clone"); + let insertStmt = readWriteClone.createAsyncStatement(` + INSERT INTO test_temp(name) VALUES('mak'), ('standard8'), ('markh')`); + await executeAsync(insertStmt); + insertStmt.finalize(); + + info("Fire temp trigger on read-write clone"); + let deleteStmt = readWriteClone.createAsyncStatement(` + DELETE FROM test_temp`); + await executeAsync(deleteStmt); + deleteStmt.finalize(); + + info("Read from original connection"); + let names = fetchAllNames(db); + Assert.ok(names.includes("mak")); + Assert.ok(names.includes("standard8")); + Assert.ok(names.includes("markh")); + + info("Create read-only clone"); + let readOnlyClone = await asyncClone(db, true); + Assert.ok(readOnlyClone instanceof Ci.mozIStorageAsyncConnection); + + info("Read-only clone shouldn't have temp entities"); + let badStmt = readOnlyClone.createAsyncStatement(`SELECT 1 FROM test_temp`); + await Assert.rejects(executeAsync(badStmt), Ci.mozIStorageError); + badStmt.finalize(); + + info("Clean up"); + for (let conn of [db, readWriteClone, readOnlyClone]) { + await asyncClose(conn); + } +}); + +add_task(async function test_sync_clone_in_transaction() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Begin transaction on main connection"); + db.beginTransaction(); + + info("Create temp table and trigger in transaction"); + let createQueries = [ + `CREATE TEMP TABLE test_temp(name TEXT)`, + `CREATE TEMP TRIGGER test_temp_afterdelete_trigger + AFTER DELETE ON test_temp FOR EACH ROW + BEGIN + INSERT INTO test(name) VALUES(OLD.name); + END`, + ]; + for (let query of createQueries) { + db.executeSimpleSQL(query); + } + + info("Clone main connection while transaction is in progress"); + let clone = db.clone(/* aReadOnly */ false); + + // Dropping the table also drops `test_temp_afterdelete_trigger`. + info("Drop temp table on main connection"); + db.executeSimpleSQL(`DROP TABLE test_temp`); + + info("Commit transaction"); + db.commitTransaction(); + + info("Clone connection should still have temp entities"); + let readTempStmt = clone.createStatement(`SELECT 1 FROM test_temp`); + readTempStmt.execute(); + readTempStmt.finalize(); + + info("Clean up"); + + db.close(); + clone.close(); +}); + +add_task(async function test_sync_clone_with_function() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Create SQL function"); + function storeLastInsertedNameFunc() { + this.name = null; + } + storeLastInsertedNameFunc.prototype = { + onFunctionCall(args) { + this.name = args.getUTF8String(0); + }, + }; + let func = new storeLastInsertedNameFunc(); + db.createFunction("store_last_inserted_name", 1, func); + + info("Create temp trigger on main connection"); + db.executeSimpleSQL(` + CREATE TEMP TRIGGER test_afterinsert_trigger + AFTER INSERT ON test FOR EACH ROW + BEGIN + SELECT store_last_inserted_name(NEW.name); + END`); + + info("Clone main connection"); + let clone = db.clone(/* aReadOnly */ false); + + info("Write to clone"); + clone.executeSimpleSQL(`INSERT INTO test(name) VALUES('kit')`); + + Assert.equal(func.name, "kit"); + + info("Clean up"); + db.close(); + clone.close(); +}); + +add_task(async function test_defaultTransactionType() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.ok(db instanceof Ci.mozIStorageAsyncConnection); + + info("Verify default transaction type"); + Assert.equal( + db.defaultTransactionType, + Ci.mozIStorageConnection.TRANSACTION_DEFERRED + ); + + info("Test other transaction types"); + for (let type of [ + Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE, + Ci.mozIStorageConnection.TRANSACTION_EXCLUSIVE, + ]) { + db.defaultTransactionType = type; + Assert.equal(db.defaultTransactionType, type); + } + + info("Should reject unknown transaction types"); + Assert.throws( + () => + (db.defaultTransactionType = + Ci.mozIStorageConnection.TRANSACTION_DEFAULT), + /NS_ERROR_ILLEGAL_VALUE/ + ); + + db.defaultTransactionType = Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE; + + info("Clone should inherit default transaction type"); + let clone = await asyncClone(db, true); + Assert.ok(clone instanceof Ci.mozIStorageAsyncConnection); + Assert.equal( + clone.defaultTransactionType, + Ci.mozIStorageConnection.TRANSACTION_IMMEDIATE + ); + + info("Begin immediate transaction on main connection"); + db.beginTransaction(); + + info("Queue immediate transaction on clone"); + let stmts = [ + clone.createAsyncStatement(`BEGIN IMMEDIATE TRANSACTION`), + clone.createAsyncStatement(`DELETE FROM test WHERE name = 'new'`), + clone.createAsyncStatement(`COMMIT`), + ]; + let promiseStmtsRan = stmts.map(stmt => executeAsync(stmt)); + + info("Commit immediate transaction on main connection"); + db.executeSimpleSQL(`INSERT INTO test(name) VALUES('new')`); + db.commitTransaction(); + + info("Wait for transaction to succeed on clone"); + await Promise.all(promiseStmtsRan); + + info("Clean up"); + for (let stmt of stmts) { + stmt.finalize(); + } + await asyncClose(clone); + await asyncClose(db); +}); + +add_task(async function test_variableLimit() { + info("Open connection"); + let db = Services.storage.openDatabase(getTestDB()); + Assert.equal(db.variableLimit, 32766, "Should return default limit"); + await asyncClose(db); +}); + +add_task(async function test_getInterface() { + let db = getOpenedDatabase(); + let target = db + .QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIEventTarget); + // Just check that target is non-null. Other tests will ensure that it has + // the correct value. + Assert.ok(target != null); + + await asyncClose(db); + gDBConn = null; +}); diff --git a/storage/test/unit/test_storage_ext.js b/storage/test/unit/test_storage_ext.js new file mode 100644 index 0000000000..748ac86750 --- /dev/null +++ b/storage/test/unit/test_storage_ext.js @@ -0,0 +1,84 @@ +/* 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 file tests basics of loading SQLite extension. + +const VALID_EXTENSION_NAME = "fts5"; + +add_setup(async function () { + cleanup(); +}); + +add_task(async function test_valid_call() { + info("Testing valid call"); + let conn = getOpenedUnsharedDatabase(); + + await new Promise((resolve, reject) => { + conn.loadExtension(VALID_EXTENSION_NAME, status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }); + + cleanup(); +}); + +add_task(async function test_invalid_calls() { + info("Testing invalid calls"); + let conn = getOpenedUnsharedDatabase(); + + await Assert.rejects( + new Promise((resolve, reject) => { + conn.loadExtension("unknown", status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }), + /NS_ERROR_ILLEGAL_VALUE/, + "Should fail loading unknown extension" + ); + + cleanup(); + + await Assert.rejects( + new Promise((resolve, reject) => { + conn.loadExtension(VALID_EXTENSION_NAME, status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }), + /NS_ERROR_NOT_INITIALIZED/, + "Should fail loading extension on a closed connection" + ); +}); + +add_task(async function test_more_invalid_calls() { + let conn = getOpenedUnsharedDatabase(); + let promiseClosed = asyncClose(conn); + + await Assert.rejects( + new Promise((resolve, reject) => { + conn.loadExtension(VALID_EXTENSION_NAME, status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }), + /NS_ERROR_NOT_INITIALIZED/, + "Should fail loading extension on a closing connection" + ); + + await promiseClosed; +}); diff --git a/storage/test/unit/test_storage_ext_fts3.js b/storage/test/unit/test_storage_ext_fts3.js new file mode 100644 index 0000000000..bcc9491a93 --- /dev/null +++ b/storage/test/unit/test_storage_ext_fts3.js @@ -0,0 +1,91 @@ +/* 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 file tests support for the fts3 (full-text index) module. + +// Example statements in these tests are taken from the Full Text Index page +// on the SQLite wiki: http://www.sqlite.org/cvstrac/wiki?p=FullTextIndex + +function test_table_creation() { + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "CREATE VIRTUAL TABLE recipe USING fts3(name, ingredients)" + ); + + Assert.ok(msc.tableExists("recipe")); +} + +function test_insertion() { + var msc = getOpenedUnsharedDatabase(); + + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli stew', 'broccoli peppers cheese tomatoes')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin stew', 'pumpkin onions garlic celery')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('broccoli pie', 'broccoli cheese onions flour')" + ); + msc.executeSimpleSQL( + "INSERT INTO recipe (name, ingredients) VALUES " + + "('pumpkin pie', 'pumpkin sugar flour butter')" + ); + + var stmt = msc.createStatement("SELECT COUNT(*) FROM recipe"); + stmt.executeStep(); + + Assert.equal(stmt.getInt32(0), 4); + + stmt.reset(); + stmt.finalize(); +} + +function test_selection() { + var msc = getOpenedUnsharedDatabase(); + + var stmt = msc.createStatement( + "SELECT rowid, name, ingredients FROM recipe WHERE name MATCH 'pie'" + ); + + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 3); + Assert.equal(stmt.getString(1), "broccoli pie"); + Assert.equal(stmt.getString(2), "broccoli cheese onions flour"); + + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getInt32(0), 4); + Assert.equal(stmt.getString(1), "pumpkin pie"); + Assert.equal(stmt.getString(2), "pumpkin sugar flour butter"); + + Assert.ok(!stmt.executeStep()); + + stmt.reset(); + stmt.finalize(); +} + +var tests = [test_table_creation, test_insertion, test_selection]; + +function run_test() { + // It's extra important to start from scratch, since these tests won't work + // with an existing shared cache connection, so we do it even though the last + // test probably did it already. + cleanup(); + + try { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + } finally { + // It's extra important to clean up afterwards, since later tests that use + // a shared cache connection will not be able to read the database we create, + // so we do this in a finally block to ensure it happens even if some of our + // tests fail. + cleanup(); + } +} diff --git a/storage/test/unit/test_storage_ext_fts5.js b/storage/test/unit/test_storage_ext_fts5.js new file mode 100644 index 0000000000..7adf4e8b83 --- /dev/null +++ b/storage/test/unit/test_storage_ext_fts5.js @@ -0,0 +1,122 @@ +/* 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 file tests support for the fts5 extension. + +// Some example statements in this tests are taken from the SQLite FTS5 +// documentation page: https://sqlite.org/fts5.html + +add_setup(async function () { + cleanup(); +}); + +add_task(async function test_synchronous() { + info("Testing synchronous connection"); + let conn = getOpenedUnsharedDatabase(); + Assert.throws( + () => + conn.executeSimpleSQL( + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);" + ), + /NS_ERROR_FAILURE/, + "Should not be able to use FTS5 without loading the extension" + ); + + await loadFTS5Extension(conn); + + conn.executeSimpleSQL( + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize='trigram');" + ); + + conn.executeSimpleSQL( + `INSERT INTO email(sender, title, body) VALUES + ('Mark', 'Fox', 'The quick brown fox jumps over the lazy dog.'), + ('Marco', 'Cat', 'The quick brown cat jumps over the lazy dog.'), + ('James', 'Hamster', 'The quick brown hamster jumps over the lazy dog.')` + ); + + var stmt = conn.createStatement( + `SELECT sender, title, highlight(email, 2, '<', '>') + FROM email + WHERE email MATCH 'ham' + ORDER BY bm25(email)` + ); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.getString(0), "James"); + Assert.equal(stmt.getString(1), "Hamster"); + Assert.equal( + stmt.getString(2), + "The quick brown ster jumps over the lazy dog." + ); + stmt.reset(); + stmt.finalize(); + + cleanup(); +}); + +add_task(async function test_asynchronous() { + info("Testing asynchronous connection"); + let conn = await openAsyncDatabase(getTestDB()); + + await Assert.rejects( + executeSimpleSQLAsync( + conn, + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);" + ), + err => err.message.startsWith("no such module"), + "Should not be able to use FTS5 without loading the extension" + ); + + await loadFTS5Extension(conn); + + await executeSimpleSQLAsync( + conn, + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);" + ); + + await asyncClose(conn); + await IOUtils.remove(getTestDB().path, { ignoreAbsent: true }); +}); + +add_task(async function test_clone() { + info("Testing cloning synchronous connection loads extensions in clone"); + let conn1 = getOpenedUnsharedDatabase(); + await loadFTS5Extension(conn1); + + let conn2 = conn1.clone(false); + conn2.executeSimpleSQL( + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);" + ); + + conn2.close(); + cleanup(); +}); + +add_task(async function test_asyncClone() { + info("Testing asynchronously cloning connection loads extensions in clone"); + let conn1 = getOpenedUnsharedDatabase(); + await loadFTS5Extension(conn1); + + let conn2 = await asyncClone(conn1, false); + await executeSimpleSQLAsync( + conn2, + "CREATE VIRTUAL TABLE email USING fts5(sender, title, body);" + ); + + await asyncClose(conn2); + await asyncClose(conn1); + await IOUtils.remove(getTestDB().path, { ignoreAbsent: true }); +}); + +async function loadFTS5Extension(conn) { + await new Promise((resolve, reject) => { + conn.loadExtension("fts5", status => { + if (Components.isSuccessCode(status)) { + resolve(); + } else { + reject(status); + } + }); + }); +} diff --git a/storage/test/unit/test_storage_function.js b/storage/test/unit/test_storage_function.js new file mode 100644 index 0000000000..1b11aa4a52 --- /dev/null +++ b/storage/test/unit/test_storage_function.js @@ -0,0 +1,92 @@ +/* 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 file tests the custom functions + +var testNums = [1, 2, 3, 4]; + +function setup() { + getOpenedDatabase().createTable("function_tests", "id INTEGER PRIMARY KEY"); + + var stmt = createStatement("INSERT INTO function_tests (id) VALUES(?1)"); + for (let i = 0; i < testNums.length; ++i) { + stmt.bindByIndex(0, testNums[i]); + stmt.execute(); + } + stmt.reset(); + stmt.finalize(); +} + +var testSquareFunction = { + calls: 0, + + onFunctionCall(val) { + ++this.calls; + return val.getInt32(0) * val.getInt32(0); + }, +}; + +function test_function_registration() { + var msc = getOpenedDatabase(); + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_no_double_registration() { + var msc = getOpenedDatabase(); + try { + msc.createFunction("test_square", 2, testSquareFunction); + do_throw("We shouldn't get here!"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FAILURE, e.result); + } +} + +function test_function_removal() { + var msc = getOpenedDatabase(); + msc.removeFunction("test_square"); + // Should be Ok now + msc.createFunction("test_square", 1, testSquareFunction); +} + +function test_function_aliases() { + var msc = getOpenedDatabase(); + msc.createFunction("test_square2", 1, testSquareFunction); +} + +function test_function_call() { + var stmt = createStatement("SELECT test_square(id) FROM function_tests"); + while (stmt.executeStep()) { + // Do nothing. + } + Assert.equal(testNums.length, testSquareFunction.calls); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +function test_function_result() { + var stmt = createStatement("SELECT test_square(42) FROM function_tests"); + stmt.executeStep(); + Assert.equal(42 * 42, stmt.getInt32(0)); + testSquareFunction.calls = 0; + stmt.finalize(); +} + +var tests = [ + test_function_registration, + test_function_no_double_registration, + test_function_removal, + test_function_aliases, + test_function_call, + test_function_result, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_progresshandler.js b/storage/test/unit/test_storage_progresshandler.js new file mode 100644 index 0000000000..e2e01eb2ff --- /dev/null +++ b/storage/test/unit/test_storage_progresshandler.js @@ -0,0 +1,112 @@ +/* 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 file tests the custom progress handlers + +function setup() { + var msc = getOpenedDatabase(); + msc.createTable("handler_tests", "id INTEGER PRIMARY KEY, num INTEGER"); + msc.beginTransaction(); + + var stmt = createStatement( + "INSERT INTO handler_tests (id, num) VALUES(?1, ?2)" + ); + for (let i = 0; i < 100; ++i) { + stmt.bindByIndex(0, i); + stmt.bindByIndex(1, Math.floor(Math.random() * 1000)); + stmt.execute(); + } + stmt.reset(); + msc.commitTransaction(); + stmt.finalize(); +} + +var testProgressHandler = { + calls: 0, + abort: false, + + onProgress(comm) { + ++this.calls; + return this.abort; + }, +}; + +function test_handler_registration() { + var msc = getOpenedDatabase(); + msc.setProgressHandler(10, testProgressHandler); +} + +function test_handler_return() { + var msc = getOpenedDatabase(); + var oldH = msc.setProgressHandler(5, testProgressHandler); + Assert.ok(oldH instanceof Ci.mozIStorageProgressHandler); +} + +function test_handler_removal() { + var msc = getOpenedDatabase(); + msc.removeProgressHandler(); + var oldH = msc.removeProgressHandler(); + Assert.equal(oldH, null); +} + +function test_handler_call() { + var msc = getOpenedDatabase(); + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2" + ); + while (stmt.executeStep()) { + // Do nothing. + } + Assert.ok(testProgressHandler.calls > 0); + stmt.finalize(); +} + +function test_handler_abort() { + var msc = getOpenedDatabase(); + testProgressHandler.abort = true; + msc.setProgressHandler(50, testProgressHandler); + // Some long-executing request + var stmt = createStatement( + "SELECT SUM(t1.num * t2.num) FROM handler_tests AS t1, handler_tests AS t2" + ); + + const SQLITE_INTERRUPT = 9; + try { + while (stmt.executeStep()) { + // Do nothing. + } + do_throw("We shouldn't get here!"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_ABORT, e.result); + Assert.equal(SQLITE_INTERRUPT, msc.lastError); + } + try { + stmt.finalize(); + do_throw("We shouldn't get here!"); + } catch (e) { + // finalize should return the error code since we encountered an error + Assert.equal(Cr.NS_ERROR_ABORT, e.result); + Assert.equal(SQLITE_INTERRUPT, msc.lastError); + } +} + +var tests = [ + test_handler_registration, + test_handler_return, + test_handler_removal, + test_handler_call, + test_handler_abort, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service.js b/storage/test/unit/test_storage_service.js new file mode 100644 index 0000000000..62a933cfbf --- /dev/null +++ b/storage/test/unit/test_storage_service.js @@ -0,0 +1,267 @@ +/* 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 file tests the functions of mozIStorageService except for +// openSpecialDatabase, which is tested by test_storage_service_special.js and +// openUnsharedDatabase, which is tested by test_storage_service_unshared.js. + +function test_openDatabase_null_file() { + try { + Services.storage.openDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openDatabase_file_DNE() { + // the file should be created after calling + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + var db = getTestDB(); + Assert.ok(!db.exists()); + Services.storage.openDatabase(db); + Assert.ok(db.exists()); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.success, + 1 + ); +} + +function test_openDatabase_file_exists() { + // it should already exist from our last test + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + var db = getTestDB(); + Assert.ok(db.exists()); + Services.storage.openDatabase(db); + Assert.ok(db.exists()); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.success, + 1 + ); +} + +function test_corrupt_db_throws_with_openDatabase() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + let db = getCorruptDB(); + + try { + getDatabase(db); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_fake_db_throws_with_openDatabase() { + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + let db = getFakeDB(); + + try { + getDatabase(db); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_CORRUPTED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + db.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_openDatabase_directory() { + let dir = getTestDB().parent; + dir.append("test_storage_temp"); + if (dir.exists()) { + dir.remove(true); + } + dir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755); + Assert.ok(dir.exists()); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(OPEN_HISTOGRAM); + + try { + getDatabase(dir); + do_throw("should not be here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_FILE_ACCESS_DENIED, e.result); + } + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + dir.leafName, + TELEMETRY_VALUES.access, + 1 + ); + + dir.remove(true); +} + +function test_read_gooddb() { + let file = do_get_file("goodDB.sqlite"); + let db = getDatabase(file); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + db.executeSimpleSQL("SELECT * FROM Foo;"); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.success, + 1 + ); + + histogram.clear(); + + let stmt = db.createStatement("SELECT id from Foo"); + + while (true) { + if (!stmt.executeStep()) { + break; + } + } + + stmt.finalize(); + + // A single statement should count as a single access. + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.success, + 1 + ); + + histogram.clear(); + + Assert.throws( + () => db.executeSimpleSQL("INSERT INTO Foo (rowid) VALUES ('test');"), + /NS_ERROR_FAILURE/, + "Executing sql should fail." + ); + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.misuse, + 1 + ); +} + +function test_read_baddb() { + let file = do_get_file("baddataDB.sqlite"); + let db = getDatabase(file); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + + Assert.throws( + () => db.executeSimpleSQL("SELECT * FROM Foo"), + /NS_ERROR_FILE_CORRUPTED/, + "Executing sql should fail." + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); + + histogram.clear(); + + let stmt = db.createStatement("SELECT * FROM Foo"); + Assert.throws( + () => stmt.executeStep(), + /NS_ERROR_FILE_CORRUPTED/, + "Executing a statement should fail." + ); + + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.corrupt, + 1 + ); +} + +function test_busy_telemetry() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + let file = do_get_file("goodDB.sqlite"); + let conn1 = Services.storage.openUnsharedDatabase(file); + let conn2 = Services.storage.openUnsharedDatabase(file); + + conn1.beginTransaction(); + conn1.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)"); + + let histogram = TelemetryTestUtils.getAndClearKeyedHistogram(QUERY_HISTOGRAM); + Assert.throws( + () => + conn2.executeSimpleSQL("CREATE TABLE test_busy (id INTEGER PRIMARY KEY)"), + /NS_ERROR_STORAGE_BUSY/, + "Nested transaction on second connection should fail" + ); + TelemetryTestUtils.assertKeyedHistogramValue( + histogram, + file.leafName, + TELEMETRY_VALUES.busy, + 1 + ); + + conn1.rollbackTransaction(); +} + +var tests = [ + test_openDatabase_null_file, + test_openDatabase_file_DNE, + test_openDatabase_file_exists, + test_corrupt_db_throws_with_openDatabase, + test_fake_db_throws_with_openDatabase, + test_openDatabase_directory, + test_read_gooddb, + test_read_baddb, + test_busy_telemetry, +]; + +function run_test() { + // Thunderbird doesn't have one or more of the probes used in this test. + // Ensure the data is collected anyway. + Services.prefs.setBoolPref( + "toolkit.telemetry.testing.overrideProductsCheck", + true + ); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_service_special.js b/storage/test/unit/test_storage_service_special.js new file mode 100644 index 0000000000..50cde042af --- /dev/null +++ b/storage/test/unit/test_storage_service_special.js @@ -0,0 +1,48 @@ +/* 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 file tests the openSpecialDatabase function of mozIStorageService. + +add_task(async function test_invalid_storage_key() { + try { + Services.storage.openSpecialDatabase("abcd"); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +}); + +add_task(async function test_memdb_close_clone_fails() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.close(); + expectError(Cr.NS_ERROR_NOT_INITIALIZED, () => db.clone()); + } +}); + +add_task(async function test_memdb_no_file_on_disk() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.close(); + for (const dirKey of ["CurWorkD", "ProfD"]) { + const dir = Services.dirsvc.get(dirKey, Ci.nsIFile); + const file = dir.clone(); + file.append(!name ? ":memory:" : "file:foo?mode=memory&cache=shared"); + Assert.ok(!file.exists()); + } + } +}); + +add_task(async function test_memdb_sharing() { + for (const name of [null, "foo"]) { + const db = Services.storage.openSpecialDatabase("memory", name); + db.executeSimpleSQL("CREATE TABLE test(name TEXT)"); + const db2 = Services.storage.openSpecialDatabase("memory", name); + Assert.ok(!!name == db2.tableExists("test")); + db.close(); + db2.close(); + } +}); diff --git a/storage/test/unit/test_storage_service_unshared.js b/storage/test/unit/test_storage_service_unshared.js new file mode 100644 index 0000000000..da1865dfa6 --- /dev/null +++ b/storage/test/unit/test_storage_service_unshared.js @@ -0,0 +1,46 @@ +/* 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 file tests the openUnsharedDatabase function of mozIStorageService. + +function test_openUnsharedDatabase_null_file() { + try { + Services.storage.openUnsharedDatabase(null); + do_throw("We should not get here!"); + } catch (e) { + print(e); + print("e.result is " + e.result); + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } +} + +function test_openUnsharedDatabase_file_DNE() { + // the file should be created after calling + var db = getTestDB(); + Assert.ok(!db.exists()); + Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists()); +} + +function test_openUnsharedDatabase_file_exists() { + // it should already exist from our last test + var db = getTestDB(); + Assert.ok(db.exists()); + Services.storage.openUnsharedDatabase(db); + Assert.ok(db.exists()); +} + +var tests = [ + test_openUnsharedDatabase_null_file, + test_openUnsharedDatabase_file_DNE, + test_openUnsharedDatabase_file_exists, +]; + +function run_test() { + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_statement.js b/storage/test/unit/test_storage_statement.js new file mode 100644 index 0000000000..3daf3e7812 --- /dev/null +++ b/storage/test/unit/test_storage_statement.js @@ -0,0 +1,183 @@ +/* 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 file tests the functions of mozIStorageStatement + +function setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); +} + +function test_parameterCount_none() { + var stmt = createStatement("SELECT * FROM test"); + Assert.equal(0, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_parameterCount_one() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + Assert.equal(1, stmt.parameterCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterName() { + var stmt = createStatement("SELECT * FROM test WHERE id = :id"); + Assert.equal(":id", stmt.getParameterName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_different() { + var stmt = createStatement( + "SELECT * FROM test WHERE id = :id OR name = :name" + ); + Assert.equal(0, stmt.getParameterIndex("id")); + Assert.equal(1, stmt.getParameterIndex("name")); + stmt.reset(); + stmt.finalize(); +} + +function test_getParameterIndex_same() { + var stmt = createStatement( + "SELECT * FROM test WHERE id = :test OR name = :test" + ); + Assert.equal(0, stmt.getParameterIndex("test")); + stmt.reset(); + stmt.finalize(); +} + +function test_columnCount() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1 OR name = ?2"); + Assert.equal(2, stmt.columnCount); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnName() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal("id", stmt.getColumnName(1)); + Assert.equal("name", stmt.getColumnName(0)); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_same_case() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal(0, stmt.getColumnIndex("name")); + Assert.equal(1, stmt.getColumnIndex("id")); + stmt.reset(); + stmt.finalize(); +} + +function test_getColumnIndex_different_case() { + var stmt = createStatement("SELECT name, id FROM test"); + try { + Assert.equal(0, stmt.getColumnIndex("NaMe")); + do_throw("should not get here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } + try { + Assert.equal(1, stmt.getColumnIndex("Id")); + do_throw("should not get here"); + } catch (e) { + Assert.equal(Cr.NS_ERROR_INVALID_ARG, e.result); + } + stmt.reset(); + stmt.finalize(); +} + +function test_state_ready() { + var stmt = createStatement("SELECT name, id FROM test"); + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.reset(); + stmt.finalize(); +} + +function test_state_executing() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.execute(); + stmt.finalize(); + + stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state + ); + stmt.executeStep(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_EXECUTING, + stmt.state + ); + stmt.reset(); + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + stmt.finalize(); +} + +function test_state_after_finalize() { + var stmt = createStatement("SELECT name, id FROM test"); + stmt.executeStep(); + stmt.finalize(); + Assert.equal( + Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_INVALID, + stmt.state + ); +} + +function test_failed_execute() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + stmt.execute(); + stmt.finalize(); + var id = getOpenedDatabase().lastInsertRowID; + stmt = createStatement("INSERT INTO test(id, name) VALUES(:id, 'bar')"); + stmt.params.id = id; + try { + // Should throw a constraint error + stmt.execute(); + do_throw("Should have seen a constraint error"); + } catch (e) { + Assert.equal(getOpenedDatabase().lastError, Ci.mozIStorageError.CONSTRAINT); + } + Assert.equal(Ci.mozIStorageStatement.MOZ_STORAGE_STATEMENT_READY, stmt.state); + // Should succeed without needing to reset the statement manually + stmt.finalize(); +} + +function test_bind_undefined() { + var stmt = createStatement("INSERT INTO test (name) VALUES ('foo')"); + + expectError(Cr.NS_ERROR_ILLEGAL_VALUE, () => stmt.bindParameters(undefined)); + + stmt.finalize(); +} + +var tests = [ + test_parameterCount_none, + test_parameterCount_one, + test_getParameterName, + test_getParameterIndex_different, + test_getParameterIndex_same, + test_columnCount, + test_getColumnName, + test_getColumnIndex_same_case, + test_getColumnIndex_different_case, + test_state_ready, + test_state_executing, + test_state_after_finalize, + test_failed_execute, + test_bind_undefined, +]; + +function run_test() { + setup(); + + for (var i = 0; i < tests.length; i++) { + tests[i](); + } + + cleanup(); +} diff --git a/storage/test/unit/test_storage_value_array.js b/storage/test/unit/test_storage_value_array.js new file mode 100644 index 0000000000..6a8f1fb6a2 --- /dev/null +++ b/storage/test/unit/test_storage_value_array.js @@ -0,0 +1,194 @@ +/* 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 file tests the functions of mozIStorageValueArray + +add_task(async function setup() { + getOpenedDatabase().createTable( + "test", + "id INTEGER PRIMARY KEY, name TEXT," + + "number REAL, nuller NULL, blobber BLOB" + ); + + var stmt = createStatement( + "INSERT INTO test (name, number, blobber) VALUES (?1, ?2, ?3)" + ); + stmt.bindByIndex(0, "foo"); + stmt.bindByIndex(1, 2.34); + stmt.bindBlobByIndex(2, [], 0); + stmt.execute(); + + stmt.bindByIndex(0, ""); + stmt.bindByIndex(1, 1.23); + stmt.bindBlobByIndex(2, [1, 2], 2); + stmt.execute(); + + stmt.reset(); + stmt.finalize(); + + registerCleanupFunction(cleanup); +}); + +add_task(async function test_getIsNull_for_null() { + var stmt = createStatement("SELECT nuller, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.ok(stmt.getIsNull(0)); // null field + Assert.ok(stmt.getIsNull(1)); // data is null if size is 0 + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getIsNull_for_non_null() { + var stmt = createStatement("SELECT name, blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.ok(!stmt.getIsNull(0)); + Assert.ok(!stmt.getIsNull(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_null() { + var stmt = createStatement("SELECT nuller FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_NULL, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_integer() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_INTEGER, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_float() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_FLOAT, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_text() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_TEXT, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_value_type_blob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal( + Ci.mozIStorageValueArray.VALUE_TYPE_BLOB, + stmt.getTypeOfIndex(0) + ); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_numEntries_one() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(1, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_numEntries_all() { + var stmt = createStatement("SELECT * FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(5, stmt.numEntries); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getInt() { + var stmt = createStatement("SELECT id FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(2, stmt.getInt32(0)); + Assert.equal(2, stmt.getInt64(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getDouble() { + var stmt = createStatement("SELECT number FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal(1.23, stmt.getDouble(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getUTF8String() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 1); + Assert.ok(stmt.executeStep()); + + Assert.equal("foo", stmt.getUTF8String(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getString() { + var stmt = createStatement("SELECT name FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + Assert.equal("", stmt.getString(0)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_getBlob() { + var stmt = createStatement("SELECT blobber FROM test WHERE id = ?1"); + stmt.bindByIndex(0, 2); + Assert.ok(stmt.executeStep()); + + var count = { value: 0 }; + var arr = { value: null }; + stmt.getBlob(0, count, arr); + Assert.equal(2, count.value); + Assert.equal(1, arr.value[0]); + Assert.equal(2, arr.value[1]); + stmt.reset(); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_unicode.js b/storage/test/unit/test_unicode.js new file mode 100644 index 0000000000..0785a54a42 --- /dev/null +++ b/storage/test/unit/test_unicode.js @@ -0,0 +1,91 @@ +/* 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 file tests the unicode functions that we have added + +const LATIN1_AE = "\xc6"; // "Æ" +const LATIN1_ae = "\xe6"; // "æ" + +add_task(async function setup() { + getOpenedDatabase().createTable("test", "id INTEGER PRIMARY KEY, name TEXT"); + + var stmt = createStatement("INSERT INTO test (name, id) VALUES (?1, ?2)"); + stmt.bindByIndex(0, LATIN1_AE); + stmt.bindByIndex(1, 1); + stmt.execute(); + stmt.bindByIndex(0, "A"); + stmt.bindByIndex(1, 2); + stmt.execute(); + stmt.bindByIndex(0, "b"); + stmt.bindByIndex(1, 3); + stmt.execute(); + stmt.bindByIndex(0, LATIN1_ae); + stmt.bindByIndex(1, 4); + stmt.execute(); + stmt.finalize(); + + registerCleanupFunction(cleanup); +}); + +add_task(async function test_upper_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = upper('a')" + ); + Assert.ok(stmt.executeStep()); + Assert.equal("A", stmt.getString(0)); + Assert.equal(2, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_upper_non_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = upper(?1)" + ); + stmt.bindByIndex(0, LATIN1_ae); + Assert.ok(stmt.executeStep()); + Assert.equal(LATIN1_AE, stmt.getString(0)); + Assert.equal(1, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_lower_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = lower('B')" + ); + Assert.ok(stmt.executeStep()); + Assert.equal("b", stmt.getString(0)); + Assert.equal(3, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_lower_non_ascii() { + var stmt = createStatement( + "SELECT name, id FROM test WHERE name = lower(?1)" + ); + stmt.bindByIndex(0, LATIN1_AE); + Assert.ok(stmt.executeStep()); + Assert.equal(LATIN1_ae, stmt.getString(0)); + Assert.equal(4, stmt.getInt32(1)); + stmt.reset(); + stmt.finalize(); +}); + +add_task(async function test_like_search_different() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_AE); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + stmt.finalize(); +}); + +add_task(async function test_like_search_same() { + var stmt = createStatement("SELECT COUNT(*) FROM test WHERE name LIKE ?1"); + stmt.bindByIndex(0, LATIN1_ae); + Assert.ok(stmt.executeStep()); + Assert.equal(2, stmt.getInt32(0)); + stmt.finalize(); +}); diff --git a/storage/test/unit/test_vacuum.js b/storage/test/unit/test_vacuum.js new file mode 100644 index 0000000000..a4cfdf714f --- /dev/null +++ b/storage/test/unit/test_vacuum.js @@ -0,0 +1,372 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +// This file tests the Vacuum Manager and asyncVacuum(). + +const { VacuumParticipant } = ChromeUtils.importESModule( + "resource://testing-common/VacuumParticipant.sys.mjs" +); + +/** + * Sends a fake idle-daily notification to the VACUUM Manager. + */ +function synthesize_idle_daily() { + Cc["@mozilla.org/storage/vacuum;1"] + .getService(Ci.nsIObserver) + .observe(null, "idle-daily", null); +} + +/** + * Returns a new nsIFile reference for a profile database. + * @param filename for the database, excluded the .sqlite extension. + */ +function new_db_file(name = "testVacuum") { + let file = Services.dirsvc.get("ProfD", Ci.nsIFile); + file.append(name + ".sqlite"); + return file; +} + +function reset_vacuum_date(name = "testVacuum") { + let date = parseInt(Date.now() / 1000 - 31 * 86400); + // Set last VACUUM to a date in the past. + Services.prefs.setIntPref(`storage.vacuum.last.${name}.sqlite`, date); + return date; +} + +function get_vacuum_date(name = "testVacuum") { + return Services.prefs.getIntPref(`storage.vacuum.last.${name}.sqlite`, 0); +} + +add_setup(async function () { + // turn on Cu.isInAutomation + Services.prefs.setBoolPref( + "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", + true + ); +}); + +add_task(async function test_common_vacuum() { + let last_vacuum_date = reset_vacuum_date(); + info("Test that a VACUUM correctly happens and all notifications are fired."); + let promiseTestVacuumBegin = TestUtils.topicObserved("test-begin-vacuum"); + let promiseTestVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + let promiseVacuumBegin = TestUtils.topicObserved("vacuum-begin"); + let promiseVacuumEnd = TestUtils.topicObserved("vacuum-end"); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()) + ); + await participant.promiseRegistered(); + synthesize_idle_daily(); + // Wait for notifications. + await Promise.all([ + promiseTestVacuumBegin, + promiseTestVacuumEnd, + promiseVacuumBegin, + promiseVacuumEnd, + ]); + Assert.greater(get_vacuum_date(), last_vacuum_date); + await participant.dispose(); +}); + +add_task(async function test_skipped_if_recent_vacuum() { + info("Test that a VACUUM is skipped if it was run recently."); + Services.prefs.setIntPref( + "storage.vacuum.last.testVacuum.sqlite", + parseInt(Date.now() / 1000) + ); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()) + ); + await participant.promiseRegistered(); + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_page_size_change() { + info("Test that a VACUUM changes page_size"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + info("Check initial page size."); + let stmt = conn.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, conn.defaultPageSize); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { expectedPageSize: 1024 }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that page size was updated."); + stmt = conn.createStatement("PRAGMA page_size"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.page_size, 1024); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_skipped_optout_vacuum() { + info("Test that a VACUUM is skipped if the participant wants to opt-out."); + reset_vacuum_date(); + + let participant = new VacuumParticipant( + Services.storage.openDatabase(new_db_file()), + { grant: false } + ); + await participant.promiseRegistered(); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_memory_database_crash() { + info("Test that we don't crash trying to vacuum a memory database"); + reset_vacuum_date(); + + let participant = new VacuumParticipant( + Services.storage.openSpecialDatabase("memory") + ); + await participant.promiseRegistered(); + // Wait for VACUUM skipped notification. + let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); + + synthesize_idle_daily(); + + // Check that VACUUM has been skipped. + await promiseSkipped; + + await participant.dispose(); +}); + +add_task(async function test_async_connection() { + info("Test we can vacuum an async connection"); + reset_vacuum_date(); + + let conn = await openAsyncDatabase(new_db_file()); + await populateFreeList(conn); + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + await participant.dispose(); +}); + +add_task(async function test_change_to_incremental_vacuum() { + info("Test we can change to incremental vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + info("Check initial vacuum."); + let stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 0); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { useIncrementalVacuum: true }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that auto_vacuum was updated."); + stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 2); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_change_from_incremental_vacuum() { + info("Test we can change from incremental vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + conn.executeSimpleSQL("PRAGMA auto_vacuum = 2"); + info("Check initial vacuum."); + let stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 2); + stmt.finalize(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn, { + useIncrementalVacuum: false, + }); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + info("Check that auto_vacuum was updated."); + stmt = conn.createStatement("PRAGMA auto_vacuum"); + Assert.ok(stmt.executeStep()); + Assert.equal(stmt.row.auto_vacuum, 0); + stmt.finalize(); + + await participant.dispose(); +}); + +add_task(async function test_attached_vacuum() { + info("Test attached database is not a problem"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + let conn2 = Services.storage.openDatabase(new_db_file("attached")); + + info("Attach " + conn2.databaseFile.path); + conn.executeSimpleSQL( + `ATTACH DATABASE '${conn2.databaseFile.path}' AS attached` + ); + await asyncClose(conn2); + let stmt = conn.createStatement("PRAGMA database_list"); + let schemas = []; + while (stmt.executeStep()) { + schemas.push(stmt.row.name); + } + Assert.deepEqual(schemas, ["main", "attached"]); + stmt.finalize(); + + await populateFreeList(conn); + await populateFreeList(conn, "attached"); + + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + await participant.dispose(); +}); + +add_task(async function test_vacuum_fail() { + info("Test a failed vacuum"); + reset_vacuum_date(); + + let conn = Services.storage.openDatabase(new_db_file()); + // Cannot vacuum in a transaction. + conn.beginTransaction(); + await populateFreeList(conn); + + let participant = new VacuumParticipant(conn); + await participant.promiseRegistered(); + let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-failure"); + synthesize_idle_daily(); + await promiseVacuumEnd; + + conn.commitTransaction(); + await participant.dispose(); +}); + +add_task(async function test_async_vacuum() { + // Since previous tests already go through most cases, this only checks + // the basics of directly calling asyncVacuum(). + info("Test synchronous connection"); + let conn = Services.storage.openDatabase(new_db_file()); + await populateFreeList(conn); + let rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + await asyncClose(conn); + + info("Test asynchronous connection"); + conn = await openAsyncDatabase(new_db_file()); + await populateFreeList(conn); + rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + await asyncClose(conn); +}); + +// Chunked growth is disabled on Android, so this test is pointless there. +add_task( + { skip_if: () => AppConstants.platform == "android" }, + async function test_vacuum_growth() { + // Tests vacuum doesn't nullify chunked growth. + let conn = Services.storage.openDatabase(new_db_file("incremental")); + conn.executeSimpleSQL("PRAGMA auto_vacuum = INCREMENTAL"); + conn.setGrowthIncrement(2 * conn.defaultPageSize, ""); + await populateFreeList(conn); + let stmt = conn.createStatement("PRAGMA freelist_count"); + let count = 0; + Assert.ok(stmt.executeStep()); + count = stmt.row.freelist_count; + stmt.reset(); + Assert.greater(count, 2, "There's more than 2 page in freelist"); + + let rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }, true); + }); + Assert.ok(Components.isSuccessCode(rv)); + + Assert.ok(stmt.executeStep()); + Assert.equal( + stmt.row.freelist_count, + 2, + "chunked growth space was preserved" + ); + stmt.reset(); + + // A full vacuuum should not be executed if there's less free pages than + // chunked growth. + rv = await new Promise(resolve => { + conn.asyncVacuum(status => { + resolve(status); + }); + }); + Assert.ok(Components.isSuccessCode(rv)); + + Assert.ok(stmt.executeStep()); + Assert.equal( + stmt.row.freelist_count, + 2, + "chunked growth space was preserved" + ); + stmt.finalize(); + + await asyncClose(conn); + } +); + +async function populateFreeList(conn, schema = "main") { + await executeSimpleSQLAsync(conn, `CREATE TABLE ${schema}.test (id TEXT)`); + await executeSimpleSQLAsync( + conn, + `INSERT INTO ${schema}.test + VALUES ${Array.from({ length: 3000 }, () => Math.random()).map( + v => "('" + v + "')" + )}` + ); + await executeSimpleSQLAsync(conn, `DROP TABLE ${schema}.test`); +} diff --git a/storage/test/unit/xpcshell.toml b/storage/test/unit/xpcshell.toml new file mode 100644 index 0000000000..2560bf3281 --- /dev/null +++ b/storage/test/unit/xpcshell.toml @@ -0,0 +1,96 @@ +[DEFAULT] +head = "head_storage.js" +support-files = [ + "baddataDB.sqlite", + "corruptDB.sqlite", + "fakeDB.sqlite", + "goodDB.sqlite", + "locale_collation.txt", + "VacuumParticipant.sys.mjs", +] + +["test_bug-365166.js"] + +["test_bug-393952.js"] + +["test_bug-429521.js"] + +["test_bug-444233.js"] + +["test_cache_size.js"] + +["test_chunk_growth.js"] +# Bug 676981: test fails consistently on Android +fail-if = ["os == 'android'"] + +["test_connection_asyncClose.js"] + +["test_connection_executeAsync.js"] + +["test_connection_executeSimpleSQLAsync.js"] + +["test_connection_failsafe_close.js"] +# The failsafe close mechanism asserts when performing SpinningSynchronousClose +# on debug builds, so we can only test on non-debug builds. +skip-if = ["debug"] + +["test_connection_interrupt.js"] + +["test_connection_online_backup.js"] + +["test_default_journal_size_limit.js"] + +["test_js_helpers.js"] + +["test_levenshtein.js"] + +["test_like.js"] + +["test_like_escape.js"] + +["test_locale_collation.js"] + +["test_minimizeMemory.js"] + +["test_page_size_is_32k.js"] + +["test_persist_journal.js"] + +["test_readonly-immutable-nolock_vfs.js"] + +["test_retry_on_busy.js"] + +["test_sqlite_secure_delete.js"] + +["test_statement_executeAsync.js"] + +["test_statement_wrapper_automatically.js"] + +["test_storage_connection.js"] +# Bug 676981: test fails consistently on Android +fail-if = ["os == 'android'"] + +["test_storage_ext.js"] + +["test_storage_ext_fts3.js"] +skip-if = ["appname != 'thunderbird' && appname != 'seamonkey'"] + +["test_storage_ext_fts5.js"] + +["test_storage_function.js"] + +["test_storage_progresshandler.js"] + +["test_storage_service.js"] + +["test_storage_service_special.js"] + +["test_storage_service_unshared.js"] + +["test_storage_statement.js"] + +["test_storage_value_array.js"] + +["test_unicode.js"] + +["test_vacuum.js"] diff --git a/storage/variant/Cargo.toml b/storage/variant/Cargo.toml new file mode 100644 index 0000000000..26e30ea1ce --- /dev/null +++ b/storage/variant/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "storage_variant" +version = "0.1.0" +authors = [ + "Lina Cambridge ", + "Myk Melez " +] +license = "MPL-2.0" + +[dependencies] +libc = "0.2" +nserror = { path = "../../xpcom/rust/nserror" } +nsstring = { path = "../../xpcom/rust/nsstring" } +xpcom = { path = "../../xpcom/rust/xpcom" } diff --git a/storage/variant/src/bag.rs b/storage/variant/src/bag.rs new file mode 100644 index 0000000000..91653ba226 --- /dev/null +++ b/storage/variant/src/bag.rs @@ -0,0 +1,135 @@ +/* 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/. */ + +use nserror::{nsresult, NS_ERROR_CANNOT_CONVERT_DATA, NS_OK}; +use nsstring::nsString; +use xpcom::{ + getter_addrefs, + interfaces::{nsIProperty, nsIPropertyBag, nsIWritablePropertyBag}, + RefPtr, XpCom, +}; + +use crate::{NsIVariantExt, VariantType}; + +extern "C" { + fn NS_NewHashPropertyBag(bag: *mut *const nsIWritablePropertyBag); +} + +/// A hash property bag backed by storage variant values. +pub struct HashPropertyBag(RefPtr); + +// This is safe as long as our `nsIWritablePropertyBag` is an instance of +// `mozilla::nsHashPropertyBag`, which is atomically reference counted, and +// all properties are backed by `Storage*Variant`s, all of which are +// thread-safe. +unsafe impl Send for HashPropertyBag {} +unsafe impl Sync for HashPropertyBag {} + +impl Default for HashPropertyBag { + fn default() -> HashPropertyBag { + // This is safe to unwrap because `NS_NewHashPropertyBag` is infallible. + let bag = getter_addrefs(|p| { + unsafe { NS_NewHashPropertyBag(p) }; + NS_OK + }) + .unwrap(); + HashPropertyBag(bag) + } +} + +impl HashPropertyBag { + /// Creates an empty property bag. + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Creates a property bag from an instance of `nsIPropertyBag`, cloning its + /// contents. The `source` bag can only contain primitive values for which + /// the `VariantType` trait is implemented. Attempting to clone a bag with + /// unsupported types, such as arrays, interface pointers, and `jsval`s, + /// fails with `NS_ERROR_CANNOT_CONVERT_DATA`. + /// + /// `clone_from_bag` can be used to clone a thread-unsafe `nsIPropertyBag`, + /// like one passed from JavaScript via XPConnect, into one that can be + /// shared across threads. + pub fn clone_from_bag(source: &nsIPropertyBag) -> Result { + let enumerator = getter_addrefs(|p| unsafe { source.GetEnumerator(p) })?; + let b = HashPropertyBag::new(); + while { + let mut has_more = false; + unsafe { enumerator.HasMoreElements(&mut has_more) }.to_result()?; + has_more + } { + let element = getter_addrefs(|p| unsafe { enumerator.GetNext(p) })?; + let property = element + .query_interface::() + .ok_or(NS_ERROR_CANNOT_CONVERT_DATA)?; + let mut name = nsString::new(); + unsafe { property.GetName(&mut *name) }.to_result()?; + let value = getter_addrefs(|p| unsafe { property.GetValue(p) })?; + unsafe { b.0.SetProperty(&*name, value.try_clone()?.coerce()) }.to_result()?; + } + Ok(b) + } + + /// Returns the value for a property name. Fails with `NS_ERROR_FAILURE` + /// if the property doesn't exist, or `NS_ERROR_CANNOT_CONVERT_DATA` if the + /// property exists, but is not of the value type `V`. + pub fn get(&self, name: K) -> Result + where + K: AsRef, + V: VariantType, + { + getter_addrefs(|p| unsafe { self.0.GetProperty(&*nsString::from(name.as_ref()), p) }) + .and_then(|v| V::from_variant(v.coerce())) + } + + /// Returns the value for a property name, or the default if not set or + /// not of the value type `V`. + #[inline] + pub fn get_or_default(&self, name: K) -> V + where + K: AsRef, + V: VariantType + Default, + { + self.get(name).unwrap_or_default() + } + + /// Sets a property with the name to the value, overwriting any previous + /// value. + pub fn set(&mut self, name: K, value: V) + where + K: AsRef, + V: VariantType, + { + let v = value.into_variant(); + unsafe { + // This is safe to unwrap because + // `nsHashPropertyBagBase::SetProperty` only returns an error if `v` + // is a null pointer. + self.0 + .SetProperty(&*nsString::from(name.as_ref()), v.coerce()) + .to_result() + .unwrap() + } + } + + /// Deletes a property with the name. Returns `true` if the property + /// was previously in the bag, `false` if not. + pub fn delete(&mut self, name: impl AsRef) -> bool { + unsafe { + self.0 + .DeleteProperty(&*nsString::from(name.as_ref())) + .to_result() + .is_ok() + } + } + + /// Returns a reference to the backing `nsIWritablePropertyBag`. + #[inline] + pub fn bag(&self) -> &nsIWritablePropertyBag { + &self.0 + } +} diff --git a/storage/variant/src/lib.rs b/storage/variant/src/lib.rs new file mode 100644 index 0000000000..4734705299 --- /dev/null +++ b/storage/variant/src/lib.rs @@ -0,0 +1,230 @@ +/* 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/. */ + +extern crate libc; +extern crate nserror; +extern crate nsstring; +extern crate xpcom; + +mod bag; + +use std::{ + borrow::Cow, + convert::{TryFrom, TryInto}, +}; + +use libc::c_double; +use nserror::{nsresult, NS_ERROR_CANNOT_CONVERT_DATA, NS_OK}; +use nsstring::{nsACString, nsAString, nsCString, nsString}; +use xpcom::{getter_addrefs, interfaces::nsIVariant, RefPtr}; + +pub use crate::bag::HashPropertyBag; + +extern "C" { + fn NS_GetDataType(variant: *const nsIVariant) -> u16; + fn NS_NewStorageNullVariant(result: *mut *const nsIVariant); + fn NS_NewStorageBooleanVariant(value: bool, result: *mut *const nsIVariant); + fn NS_NewStorageIntegerVariant(value: i64, result: *mut *const nsIVariant); + fn NS_NewStorageFloatVariant(value: c_double, result: *mut *const nsIVariant); + fn NS_NewStorageTextVariant(value: *const nsAString, result: *mut *const nsIVariant); + fn NS_NewStorageUTF8TextVariant(value: *const nsACString, result: *mut *const nsIVariant); +} + +// These are the relevant parts of the nsXPTTypeTag enum in xptinfo.h, +// which nsIVariant.idl reflects into the nsIDataType struct class and uses +// to constrain the values of nsIVariant::dataType. +#[repr(u16)] +#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +pub enum DataType { + Int32 = 2, + Int64 = 3, + Double = 9, + Bool = 10, + Void = 13, + CharStr = 15, + WCharStr = 16, + StringSizeIs = 20, + WStringSizeIs = 21, + Utf8String = 24, + CString = 25, + AString = 26, + EmptyArray = 254, + Empty = 255, +} + +impl TryFrom for DataType { + type Error = nsresult; + + /// Converts a raw type tag for an `nsIVariant` into a `DataType` variant. + /// Returns `NS_ERROR_CANNOT_CONVERT_DATA` if the type isn't one that we + /// support. + fn try_from(raw: u16) -> Result { + Ok(match raw { + 2 => DataType::Int32, + 3 => DataType::Int64, + 9 => DataType::Double, + 10 => DataType::Bool, + 13 => DataType::Void, + 15 => DataType::CharStr, + 16 => DataType::WCharStr, + 20 => DataType::StringSizeIs, + 21 => DataType::WStringSizeIs, + 24 => DataType::Utf8String, + 25 => DataType::CString, + 26 => DataType::AString, + 254 => DataType::EmptyArray, + 255 => DataType::Empty, + _ => Err(NS_ERROR_CANNOT_CONVERT_DATA)?, + }) + } +} + +/// Extension methods implemented on `nsIVariant` types, to make them easier +/// to work with. +pub trait NsIVariantExt { + /// Returns the raw type tag for this variant. Call + /// `DataType::try_from()` on this tag to turn it into a `DataType`. + fn get_data_type(&self) -> u16; + + /// Tries to clone this variant, failing with `NS_ERROR_CANNOT_CONVERT_DATA` + /// if its type is unsupported. + fn try_clone(&self) -> Result, nsresult>; +} + +impl NsIVariantExt for nsIVariant { + fn get_data_type(&self) -> u16 { + unsafe { NS_GetDataType(self) } + } + + fn try_clone(&self) -> Result, nsresult> { + Ok(match self.get_data_type().try_into()? { + DataType::Bool => bool::from_variant(self)?.into_variant(), + DataType::Int32 => i32::from_variant(self)?.into_variant(), + DataType::Int64 => i64::from_variant(self)?.into_variant(), + DataType::Double => f64::from_variant(self)?.into_variant(), + DataType::AString | DataType::WCharStr | DataType::WStringSizeIs => { + nsString::from_variant(self)?.into_variant() + } + DataType::CString + | DataType::CharStr + | DataType::StringSizeIs + | DataType::Utf8String => nsCString::from_variant(self)?.into_variant(), + DataType::Void | DataType::EmptyArray | DataType::Empty => ().into_variant(), + }) + } +} + +pub trait VariantType { + fn type_name() -> Cow<'static, str>; + fn into_variant(self) -> RefPtr; + fn from_variant(variant: &nsIVariant) -> Result + where + Self: Sized; +} + +/// Implements traits to convert between variants and their types. +macro_rules! variant { + ($typ:ident, $constructor:ident, $getter:ident) => { + impl VariantType for $typ { + fn type_name() -> Cow<'static, str> { + stringify!($typ).into() + } + fn into_variant(self) -> RefPtr { + // getter_addrefs returns a Result, nsresult>, + // but we know that our $constructor is infallible, so we can + // safely unwrap and return the RefPtr. + getter_addrefs(|p| { + unsafe { $constructor(self.into(), p) }; + NS_OK + }) + .unwrap() + } + fn from_variant(variant: &nsIVariant) -> Result<$typ, nsresult> { + let mut result = $typ::default(); + let rv = unsafe { variant.$getter(&mut result) }; + if rv.succeeded() { + Ok(result) + } else { + Err(rv) + } + } + } + }; + (* $typ:ident, $constructor:ident, $getter:ident) => { + impl VariantType for $typ { + fn type_name() -> Cow<'static, str> { + stringify!($typ).into() + } + fn into_variant(self) -> RefPtr { + // getter_addrefs returns a Result, nsresult>, + // but we know that our $constructor is infallible, so we can + // safely unwrap and return the RefPtr. + getter_addrefs(|p| { + unsafe { $constructor(&*self, p) }; + NS_OK + }) + .unwrap() + } + fn from_variant(variant: &nsIVariant) -> Result<$typ, nsresult> { + let mut result = $typ::new(); + let rv = unsafe { variant.$getter(&mut *result) }; + if rv.succeeded() { + Ok(result) + } else { + Err(rv) + } + } + } + }; +} + +// The unit type (()) is a reasonable equivalation of the null variant. +// The macro can't produce its implementations of VariantType, however, +// so we implement them concretely. +impl VariantType for () { + fn type_name() -> Cow<'static, str> { + "()".into() + } + fn into_variant(self) -> RefPtr { + // getter_addrefs returns a Result, nsresult>, + // but we know that NS_NewStorageNullVariant is infallible, so we can + // safely unwrap and return the RefPtr. + getter_addrefs(|p| { + unsafe { NS_NewStorageNullVariant(p) }; + NS_OK + }) + .unwrap() + } + fn from_variant(_variant: &nsIVariant) -> Result { + Ok(()) + } +} + +impl VariantType for Option +where + T: VariantType, +{ + fn type_name() -> Cow<'static, str> { + format!("Option<{}>", T::type_name()).into() + } + fn into_variant(self) -> RefPtr { + match self { + Some(v) => v.into_variant(), + None => ().into_variant(), + } + } + fn from_variant(variant: &nsIVariant) -> Result { + Ok(match variant.get_data_type().try_into() { + Ok(DataType::Void) | Ok(DataType::EmptyArray) | Ok(DataType::Empty) => None, + _ => Some(VariantType::from_variant(variant)?), + }) + } +} + +variant!(bool, NS_NewStorageBooleanVariant, GetAsBool); +variant!(i32, NS_NewStorageIntegerVariant, GetAsInt32); +variant!(i64, NS_NewStorageIntegerVariant, GetAsInt64); +variant!(f64, NS_NewStorageFloatVariant, GetAsDouble); +variant!(*nsString, NS_NewStorageTextVariant, GetAsAString); +variant!(*nsCString, NS_NewStorageUTF8TextVariant, GetAsAUTF8String); diff --git a/storage/variantToSQLiteT_impl.h b/storage/variantToSQLiteT_impl.h new file mode 100644 index 0000000000..cadaa3f176 --- /dev/null +++ b/storage/variantToSQLiteT_impl.h @@ -0,0 +1,114 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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/. */ + +// Note: we are already in the namepace mozilla::storage + +// Note 2: whoever #includes this file must provide implementations of +// sqlite3_T_* prior. + +//////////////////////////////////////////////////////////////////////////////// +//// variantToSQLiteT Implementation + +template +int variantToSQLiteT(T aObj, nsIVariant* aValue) { + // Allow to return nullptr not wrapped to nsIVariant for speed. + if (!aValue) return sqlite3_T_null(aObj); + + uint16_t valueType = aValue->GetDataType(); + switch (valueType) { + case nsIDataType::VTYPE_INT8: + case nsIDataType::VTYPE_INT16: + case nsIDataType::VTYPE_INT32: + case nsIDataType::VTYPE_UINT8: + case nsIDataType::VTYPE_UINT16: { + int32_t value; + nsresult rv = aValue->GetAsInt32(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int(aObj, value); + } + case nsIDataType::VTYPE_UINT32: // Try to preserve full range + case nsIDataType::VTYPE_INT64: + // Data loss possible, but there is no unsigned types in SQLite + case nsIDataType::VTYPE_UINT64: { + int64_t value; + nsresult rv = aValue->GetAsInt64(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int64(aObj, value); + } + case nsIDataType::VTYPE_FLOAT: + case nsIDataType::VTYPE_DOUBLE: { + double value; + nsresult rv = aValue->GetAsDouble(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_double(aObj, value); + } + case nsIDataType::VTYPE_BOOL: { + bool value; + nsresult rv = aValue->GetAsBool(&value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_int(aObj, value ? 1 : 0); + } + case nsIDataType::VTYPE_CHAR: + case nsIDataType::VTYPE_CHAR_STR: + case nsIDataType::VTYPE_STRING_SIZE_IS: + case nsIDataType::VTYPE_UTF8STRING: + case nsIDataType::VTYPE_CSTRING: { + nsAutoCString value; + // GetAsAUTF8String should never perform conversion when coming from + // 8-bit string types, and thus can accept strings with arbitrary encoding + // (including UTF8 and ASCII). + nsresult rv = aValue->GetAsAUTF8String(value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_text(aObj, value); + } + case nsIDataType::VTYPE_WCHAR: + case nsIDataType::VTYPE_WCHAR_STR: + case nsIDataType::VTYPE_WSTRING_SIZE_IS: + case nsIDataType::VTYPE_ASTRING: { + nsAutoString value; + // GetAsAString does proper conversion to UCS2 from all string-like types. + // It can be used universally without problems (unless someone implements + // their own variant, but that's their problem). + nsresult rv = aValue->GetAsAString(value); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + return sqlite3_T_text16(aObj, value); + } + case nsIDataType::VTYPE_VOID: + case nsIDataType::VTYPE_EMPTY: + case nsIDataType::VTYPE_EMPTY_ARRAY: + return sqlite3_T_null(aObj); + case nsIDataType::VTYPE_ARRAY: { + uint16_t arrayType; + nsIID iid; + uint32_t count; + void* data; + nsresult rv = aValue->GetAsArray(&arrayType, &iid, &count, &data); + NS_ENSURE_SUCCESS(rv, SQLITE_MISMATCH); + + // Check to make sure it's a supported type. + NS_ASSERTION(arrayType == nsIDataType::VTYPE_UINT8, + "Invalid type passed! You may leak!"); + if (arrayType != nsIDataType::VTYPE_UINT8) { + // Technically this could leak with certain data types, but somebody was + // being stupid passing us this anyway. + free(data); + return SQLITE_MISMATCH; + } + + // Finally do our thing. The function should free the array accordingly! + int rc = sqlite3_T_blob(aObj, data, count); + return rc; + } + // Maybe, it'll be possible to convert these + // in future too. + case nsIDataType::VTYPE_ID: + case nsIDataType::VTYPE_INTERFACE: + case nsIDataType::VTYPE_INTERFACE_IS: + default: + return SQLITE_MISMATCH; + } + return SQLITE_OK; +} -- cgit v1.2.3