blob: 409a3388a0b8ecf9ab71049b729fde3927c48eb5 [file] [log] [blame]
/*
* Copyright (C) 2008-2021 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "SQLiteStorageArea.h"
#include "Logging.h"
#include <WebCore/SQLiteFileSystem.h>
#include <WebCore/SQLiteStatement.h>
#include <WebCore/SQLiteTransaction.h>
#include <WebCore/StorageMap.h>
namespace WebKit {
constexpr Seconds transactionDuration { 500_ms };
constexpr unsigned maximumSizeForValuesKeptInMemory { 1 * KB };
constexpr auto createItemTableStatementAlternative = "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)"_s;
constexpr auto createItemTableStatement = "CREATE TABLE ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL)"_s;
ASCIILiteral SQLiteStorageArea::statementString(StatementType type) const
{
switch (type) {
case StatementType::CountItems:
return "SELECT COUNT(*) FROM ItemTable"_s;
case StatementType::DeleteItem:
return "DELETE FROM ItemTable WHERE key=?"_s;
case StatementType::DeleteAllItems:
return "DELETE FROM ItemTable"_s;
case StatementType::GetItem:
return "SELECT value FROM ItemTable WHERE key=?"_s;
case StatementType::GetAllItems:
return "SELECT key, value FROM ItemTable"_s;
case StatementType::SetItem:
return "INSERT INTO ItemTable VALUES (?, ?)"_s;
case StatementType::Invalid:
break;
}
ASSERT_NOT_REACHED();
return ""_s;
}
SQLiteStorageArea::SQLiteStorageArea(unsigned quota, const WebCore::ClientOrigin& origin, const String& path, Ref<WorkQueue>&& workQueue)
: StorageAreaBase(quota, origin)
, m_path(path)
, m_queue(WTFMove(workQueue))
{
ASSERT(!isMainRunLoop());
for (size_t i = 0; i < static_cast<size_t>(StatementType::Invalid); ++i)
m_cachedStatements.append(nullptr);
}
void SQLiteStorageArea::close()
{
ASSERT(!isMainRunLoop());
m_cache = std::nullopt;
commitTransactionIfNecessary();
for (size_t i = 0; i < static_cast<size_t>(StatementType::Invalid); ++i)
m_cachedStatements[i] = nullptr;
m_database = nullptr;
}
SQLiteStorageArea::~SQLiteStorageArea()
{
ASSERT(!isMainRunLoop());
bool databaseIsEmpty = isEmpty();
close();
if (databaseIsEmpty)
WebCore::SQLiteFileSystem::deleteDatabaseFile(m_path);
}
bool SQLiteStorageArea::isEmpty()
{
ASSERT(!isMainRunLoop());
if (m_cache)
return m_cache->isEmpty();
if (!prepareDatabase(ShouldCreateIfNotExists::No))
return true;
if (!m_database)
return true;
auto statement = cachedStatement(StatementType::CountItems);
if (!statement || statement->step() != SQLITE_ROW) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::isEmpty failed on executing statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return true;
}
return !statement->columnInt(0);
}
void SQLiteStorageArea::clear()
{
ASSERT(!isMainRunLoop());
close();
WebCore::SQLiteFileSystem::deleteDatabaseFile(m_path);
notifyListenersAboutClear();
}
bool SQLiteStorageArea::createTableIfNecessary()
{
if (!m_database)
return false;
String statement = m_database->tableSQL("ItemTable"_s);
if (statement == createItemTableStatement || statement == createItemTableStatementAlternative)
return true;
// Table exists but statement is wrong; drop it.
if (!statement.isEmpty()) {
if (!m_database->executeCommand("DROP TABLE ItemTable"_s)) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::createTableIfNecessary failed to drop existing item table (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return false;
}
}
// Table does not exist.
if (!m_database->executeCommand(createItemTableStatement)) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::createTableIfNecessary failed to create item table (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return false;
}
return true;
}
bool SQLiteStorageArea::prepareDatabase(ShouldCreateIfNotExists shouldCreateIfNotExists)
{
if (m_database && m_database->isOpen())
return true;
m_database = nullptr;
if (shouldCreateIfNotExists == ShouldCreateIfNotExists::No && !FileSystem::fileExists(m_path))
return true;
m_database = makeUnique<WebCore::SQLiteDatabase>();
FileSystem::makeAllDirectories(FileSystem::parentPath(m_path));
if (!m_database->open(m_path)) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::prepareDatabase failed to open database at '%s'", m_path.utf8().data());
m_database = nullptr;
return false;
}
// Since a WorkQueue isn't bound to a specific thread, we need to disable threading check.
// We will never access the database from different threads simultaneously.
m_database->disableThreadingChecks();
if (!createTableIfNecessary()) {
m_database = nullptr;
return false;
}
if (quota() != WebCore::StorageMap::noQuota)
m_database->setMaximumSize(quota());
return true;
}
void SQLiteStorageArea::startTransactionIfNecessary()
{
if (!m_transaction)
m_transaction = makeUnique<WebCore::SQLiteTransaction>(*m_database);
if (m_transaction->inProgress())
return;
m_transaction->begin();
m_queue->dispatchAfter(transactionDuration, [weakThis = WeakPtr { *this }] {
if (!weakThis || !weakThis->m_transaction)
return;
auto transaction = std::exchange(weakThis->m_transaction, nullptr);
transaction->commit();
});
}
WebCore::SQLiteStatementAutoResetScope SQLiteStorageArea::cachedStatement(StatementType type)
{
ASSERT(m_database);
ASSERT(type < StatementType::Invalid);
auto index = static_cast<uint8_t>(type);
if (!m_cachedStatements[index]) {
if (auto result = m_database->prepareHeapStatement(statementString(type)))
m_cachedStatements[index] = result.value().moveToUniquePtr();
}
return WebCore::SQLiteStatementAutoResetScope { m_cachedStatements[index].get() };
}
Expected<String, StorageError> SQLiteStorageArea::getItem(const String& key)
{
if (m_cache) {
auto iterator = m_cache->find(key);
if (iterator == m_cache->end())
return String();
if (!iterator->value.isNull())
return iterator->value;
}
return getItemFromDatabase(key);
}
Expected<String, StorageError> SQLiteStorageArea::getItemFromDatabase(const String& key)
{
if (!prepareDatabase(ShouldCreateIfNotExists::No))
return makeUnexpected(StorageError::Database);
if (!m_database)
return makeUnexpected(StorageError::ItemNotFound);
auto statement = cachedStatement(StatementType::GetItem);
if (!statement || statement->bindText(1, key)) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::getItemFromDatabase failed on creating statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
int result = statement->step();
if (result == SQLITE_ROW)
return statement->columnBlobAsString(0);
if (result != SQLITE_DONE) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::getItemFromDatabase failed on stepping statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
return makeUnexpected(StorageError::ItemNotFound);
}
HashMap<String, String> SQLiteStorageArea::allItems()
{
ASSERT(!isMainRunLoop());
if (!prepareDatabase(ShouldCreateIfNotExists::No) || !m_database)
return HashMap<String, String> { };
HashMap<String, String> items;
if (m_cache) {
items.reserveInitialCapacity(m_cache->size());
for (auto& [key, value] : *m_cache) {
if (!value.isNull()) {
items.add(key, value);
continue;
}
if (auto result = getItemFromDatabase(key))
items.add(key, result.value());
}
return items;
}
// Import from database.
auto statement = cachedStatement(StatementType::GetAllItems);
if (!statement) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::getAllItems failed on creating statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return { };
}
m_cache = HashMap<String, String> { };
int result = statement->step();
while (result == SQLITE_ROW) {
String key = statement->columnText(0);
String value = statement->columnBlobAsString(1);
if (!key.isNull() && !value.isNull()) {
m_cache->add(key, value.sizeInBytes() > maximumSizeForValuesKeptInMemory ? String() : value);
items.add(WTFMove(key), WTFMove(value));
}
result = statement->step();
}
if (result != SQLITE_DONE)
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::getAllItems failed on executing statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return items;
}
Expected<void, StorageError> SQLiteStorageArea::setItem(IPC::Connection::UniqueID connection, StorageAreaImplIdentifier storageAreaImplID, String&& key, String&& value, const String& urlString)
{
ASSERT(!isMainRunLoop());
if (!prepareDatabase(ShouldCreateIfNotExists::Yes))
return makeUnexpected(StorageError::Database);
startTransactionIfNecessary();
String oldValue;
if (auto valueOrError = getItem(key))
oldValue = valueOrError.value();
auto statement = cachedStatement(StatementType::SetItem);
if (!statement || statement->bindText(1, key) || statement->bindBlob(2, value)) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::setItem failed on creating statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
int result = statement->step();
if (result == SQLITE_FULL)
return makeUnexpected(StorageError::QuotaExceeded);
if (result != SQLITE_DONE) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::setItem failed on stepping statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
dispatchEvents(connection, storageAreaImplID, key, oldValue, value, urlString);
if (m_cache)
m_cache->set(WTFMove(key), value.sizeInBytes() > maximumSizeForValuesKeptInMemory ? String() : WTFMove(value));
return { };
}
Expected<void, StorageError> SQLiteStorageArea::removeItem(IPC::Connection::UniqueID connection, StorageAreaImplIdentifier storageAreaImplID, const String& key, const String& urlString)
{
ASSERT(!isMainRunLoop());
if (!prepareDatabase(ShouldCreateIfNotExists::No))
return makeUnexpected(StorageError::Database);
if (!m_database)
return makeUnexpected(StorageError::ItemNotFound);
startTransactionIfNecessary();
String oldValue;
if (auto valueOrError = getItem(key))
oldValue = valueOrError.value();
else
return makeUnexpected(StorageError::ItemNotFound);
auto statement = cachedStatement(StatementType::DeleteItem);
if (!statement || statement->bindText(1, key) || statement->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::removeItem failed on executing statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
dispatchEvents(connection, storageAreaImplID, key, oldValue, String(), urlString);
if (m_cache)
m_cache->remove(key);
return { };
}
Expected<void, StorageError> SQLiteStorageArea::clear(IPC::Connection::UniqueID connection, StorageAreaImplIdentifier storageAreaImplID, const String& urlString)
{
ASSERT(!isMainRunLoop());
if (!prepareDatabase(ShouldCreateIfNotExists::No))
return makeUnexpected(StorageError::Database);
if (m_cache && m_cache->isEmpty())
return makeUnexpected(StorageError::ItemNotFound);
if (m_cache)
m_cache->clear();
if (!m_database)
return makeUnexpected(StorageError::ItemNotFound);
startTransactionIfNecessary();
auto statement = cachedStatement(StatementType::DeleteAllItems);
if (!statement || statement->step() != SQLITE_DONE) {
RELEASE_LOG_ERROR(Storage, "SQLiteStorageArea::clear failed on executing statement (%d) - %s", m_database->lastError(), m_database->lastErrorMsg());
return makeUnexpected(StorageError::Database);
}
if (m_database->lastChanges() <= 0)
return makeUnexpected(StorageError::ItemNotFound);
dispatchEvents(connection, storageAreaImplID, String(), String(), String(), urlString);
return { };
}
void SQLiteStorageArea::commitTransactionIfNecessary()
{
if (auto transaction = std::exchange(m_transaction, nullptr))
transaction->commit();
}
void SQLiteStorageArea::handleLowMemoryWarning()
{
ASSERT(!isMainRunLoop());
if (m_database && m_database->isOpen())
m_database->releaseMemory();
}
} // namespace WebKit