IconDatabase.cpp   [plain text]


/*
 * Copyright (C) 2019 Igalia S.L.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public License
 * along with this library; see the file COPYING.LIB.  If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include "IconDatabase.h"

#include "Logging.h"
#include <WebCore/BitmapImage.h>
#include <WebCore/Image.h>
#include <WebCore/SQLiteTransaction.h>
#include <WebCore/SharedBuffer.h>
#include <wtf/FileSystem.h>
#include <wtf/RunLoop.h>
#include <wtf/glib/RunLoopSourcePriority.h>
#include <wtf/threads/BinarySemaphore.h>

namespace WebKit {
using namespace WebCore;

// This version number is in the DB and marks the current generation of the schema
// Currently, a mismatched schema causes the DB to be wiped and reset.
static const int currentDatabaseVersion = 6;

// Icons expire once every 4 days.
static const Seconds iconExpirationTime { 60 * 60 * 24 * 4 };

// We are not interested in icons that have been unused for more than 30 days.
static const Seconds notUsedIconExpirationTime { 60 * 60 * 24 * 30 };

// Loaded icons are cleared after 30 seconds of being requested.
static const Seconds loadedIconExpirationTime { 30_s };

IconDatabase::IconDatabase(const String& path, AllowDatabaseWrite allowDatabaseWrite)
    : m_workQueue(WorkQueue::create("org.webkit.IconDatabase"))
    , m_allowDatabaseWrite(allowDatabaseWrite)
    , m_clearLoadedIconsTimer(RunLoop::main(), this, &IconDatabase::clearLoadedIconsTimerFired)
{
    ASSERT(isMainThread());
    m_clearLoadedIconsTimer.setPriority(RunLoopSourcePriority::ReleaseUnusedResourcesTimer);

    // We initialize the database synchronously, it's hopefully fast enough because it makes
    // the implementation a lot simpler.
    BinarySemaphore semaphore;
    m_workQueue->dispatch([&] {
        if (allowDatabaseWrite == AllowDatabaseWrite::No && !FileSystem::fileExists(path)) {
            semaphore.signal();
            return;
        }

        auto databaseDirectory = FileSystem::directoryName(path);
        FileSystem::makeAllDirectories(databaseDirectory);
        if (!m_db.open(path)) {
            LOG_ERROR("Unable to open favicon database at path %s - %s", path.utf8().data(), m_db.lastErrorMsg());
            semaphore.signal();
            return;
        }

        auto databaseVersionNumber = SQLiteStatement(m_db, "SELECT value FROM IconDatabaseInfo WHERE key = 'Version';").getColumnInt(0);
        if (databaseVersionNumber > currentDatabaseVersion) {
            LOG(IconDatabase, "Database version number %d is greater than our current version number %d - closing the database to prevent overwriting newer versions",
                databaseVersionNumber, currentDatabaseVersion);
            m_db.close();
            semaphore.signal();
            return;
        }

        if (databaseVersionNumber < currentDatabaseVersion) {
            if (m_allowDatabaseWrite == AllowDatabaseWrite::No) {
                m_db.close();
                semaphore.signal();
                return;
            }

            m_db.clearAllTables();
        }

        // Reduce sqlite RAM cache size from default 2000 pages (~1.5kB per page). 3MB of cache for icon database is overkill.
        SQLiteStatement(m_db, "PRAGMA cache_size = 200;").executeCommand();

        if (allowDatabaseWrite == AllowDatabaseWrite::Yes) {
            m_pruneTimer = makeUnique<RunLoop::Timer<IconDatabase>>(RunLoop::current(), this, &IconDatabase::pruneTimerFired);
            m_pruneTimer->setPriority(RunLoopSourcePriority::ReleaseUnusedResourcesTimer);
        }

        if (!createTablesIfNeeded())
            populatePageURLToIconURLMap();

        semaphore.signal();
    });
    semaphore.wait();
}

IconDatabase::~IconDatabase()
{
    ASSERT(isMainThread());

    BinarySemaphore semaphore;
    m_workQueue->dispatch([&] {
        if (m_db.isOpen()) {
            m_pruneTimer = nullptr;
            clearStatements();
            m_db.close();
        }
        semaphore.signal();
    });
    semaphore.wait();
}

bool IconDatabase::createTablesIfNeeded()
{
    ASSERT(!isMainThread());

    if (m_db.tableExists("IconInfo") && m_db.tableExists("IconData") && m_db.tableExists("PageURL") && m_db.tableExists("IconDatabaseInfo"))
        return false;

    if (m_allowDatabaseWrite == AllowDatabaseWrite::No) {
        m_db.close();
        return false;
    }

    m_db.clearAllTables();

    if (!m_db.executeCommand("CREATE TABLE PageURL (url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,iconID INTEGER NOT NULL ON CONFLICT FAIL);")) {
        LOG_ERROR("Could not create PageURL table in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE INDEX PageURLIndex ON PageURL (url);")) {
        LOG_ERROR("Could not create PageURL index in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE TABLE IconInfo (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, url TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT FAIL, stamp INTEGER);")) {
        LOG_ERROR("Could not create IconInfo table in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE INDEX IconInfoIndex ON IconInfo (url, iconID);")) {
        LOG_ERROR("Could not create PageURL index in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE TABLE IconData (iconID INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE ON CONFLICT REPLACE, data BLOB);")) {
        LOG_ERROR("Could not create IconData table in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE INDEX IconDataIndex ON IconData (iconID);")) {
        LOG_ERROR("Could not create PageURL index in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand("CREATE TABLE IconDatabaseInfo (key TEXT NOT NULL ON CONFLICT FAIL UNIQUE ON CONFLICT REPLACE,value TEXT NOT NULL ON CONFLICT FAIL);")) {
        LOG_ERROR("Could not create IconDatabaseInfo table in database (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }
    if (!m_db.executeCommand(String("INSERT INTO IconDatabaseInfo VALUES ('Version', ") + String::number(currentDatabaseVersion) + ");")) {
        LOG_ERROR("Could not insert icon database version into IconDatabaseInfo table (%i) - %s", m_db.lastError(), m_db.lastErrorMsg());
        m_db.close();
        return false;
    }

    return true;
}

void IconDatabase::populatePageURLToIconURLMap()
{
    ASSERT(!isMainThread());

    if (!m_db.isOpen())
        return;

    String importQuery = makeString("SELECT PageURL.url, IconInfo.url, IconInfo.stamp FROM PageURL INNER JOIN IconInfo ON PageURL.iconID=IconInfo.iconID WHERE IconInfo.stamp > ", floor((WallTime::now() - notUsedIconExpirationTime).secondsSinceEpoch().seconds()), ';');
    SQLiteStatement query(m_db, importQuery);
    if (query.prepare() != SQLITE_OK) {
        LOG_ERROR("Unable to prepare icon url import query");
        return;
    }

    auto result = query.step();
    LockHolder lockHolder(m_pageURLToIconURLMapLock);
    while (result == SQLITE_ROW) {
        m_pageURLToIconURLMap.set(query.getColumnText(0), query.getColumnText(1));
        result = query.step();
    }

    startPruneTimer();
}

void IconDatabase::clearStatements()
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());

    m_iconIDForIconURLStatement = nullptr;
    m_setIconIDForPageURLStatement = nullptr;
    m_iconDataStatement = nullptr;
    m_addIconStatement = nullptr;
    m_addIconDataStatement = nullptr;
    m_updateIconTimestampStatement = nullptr;
    m_deletePageURLsForIconStatement = nullptr;
    m_deleteIconDataStatement = nullptr;
    m_deleteIconStatement = nullptr;
    m_pruneIconsStatement = nullptr;
}

void IconDatabase::pruneTimerFired()
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());

    if (!m_pruneIconsStatement) {
        m_pruneIconsStatement = makeUnique<SQLiteStatement>(m_db, "DELETE FROM IconInfo WHERE stamp <= (?);");
        if (m_pruneIconsStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement pruneIcons failed");
            m_pruneIconsStatement = nullptr;
            return;
        }
    }

    if (m_pruneIconsStatement->bindInt64(1, floor((WallTime::now() - notUsedIconExpirationTime).secondsSinceEpoch().seconds())) != SQLITE_OK) {
        LOG_ERROR("FaviconDatabse::pruneTimerFired failed: %s", m_db.lastErrorMsg());
        return;
    }

    SQLiteTransaction transaction(m_db);
    transaction.begin();
    if (m_pruneIconsStatement->step() == SQLITE_DONE) {
        m_db.executeCommand("DELETE FROM IconData WHERE iconID NOT IN (SELECT iconID FROM IconInfo);");
        m_db.executeCommand("DELETE FROM PageURL WHERE iconID NOT IN (SELECT iconID FROM IconInfo);");
    }
    m_pruneIconsStatement->reset();

    transaction.commit();
}

void IconDatabase::startPruneTimer()
{
    ASSERT(!isMainThread());

    if (!m_pruneTimer || !m_db.isOpen())
        return;

    if (m_pruneTimer->isActive())
        m_pruneTimer->stop();
    m_pruneTimer->startOneShot(10_s);
}

void IconDatabase::clearLoadedIconsTimerFired()
{
    ASSERT(isMainThread());

    LockHolder lockHolder(m_loadedIconsLock);
    auto now = MonotonicTime::now();
    Vector<String> iconsToRemove;
    for (auto iter : m_loadedIcons) {
        if (now - iter.value.second >= loadedIconExpirationTime)
            iconsToRemove.append(iter.key);
    }

    for (auto& iconURL : iconsToRemove)
        m_loadedIcons.remove(iconURL);

    if (!m_loadedIcons.isEmpty())
        startClearLoadedIconsTimer();
}

void IconDatabase::startClearLoadedIconsTimer()
{
    ASSERT(isMainThread());

    if (m_clearLoadedIconsTimer.isActive())
        return;

    m_clearLoadedIconsTimer.startOneShot(loadedIconExpirationTime);
}

Optional<int64_t> IconDatabase::iconIDForIconURL(const String& iconURL, bool& expired)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());

    if (!m_iconIDForIconURLStatement) {
        m_iconIDForIconURLStatement = makeUnique<SQLiteStatement>(m_db, "SELECT IconInfo.iconID, IconInfo.stamp FROM IconInfo WHERE IconInfo.url = (?);");
        if (m_iconIDForIconURLStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement iconIDForIconURL failed");
            m_iconIDForIconURLStatement = nullptr;
            return WTF::nullopt;
        }
    }

    if (m_iconIDForIconURLStatement->bindText(1, iconURL) != SQLITE_OK) {
        LOG_ERROR("FaviconDatabse::iconIDForIconURL failed: %s", m_db.lastErrorMsg());
        return WTF::nullopt;
    }

    Optional<int64_t> result;
    if (m_iconIDForIconURLStatement->step() == SQLITE_ROW) {
        result = m_iconIDForIconURLStatement->getColumnInt64(0);
        expired = m_iconIDForIconURLStatement->getColumnInt64(1) <= floor((WallTime::now() - iconExpirationTime).secondsSinceEpoch().seconds());
    }

    m_iconIDForIconURLStatement->reset();
    return result;
}

bool IconDatabase::setIconIDForPageURL(int64_t iconID, const String& pageURL)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());
    ASSERT(m_allowDatabaseWrite == AllowDatabaseWrite::Yes);

    if (!m_setIconIDForPageURLStatement) {
        m_setIconIDForPageURLStatement = makeUnique<SQLiteStatement>(m_db, "INSERT INTO PageURL (url, iconID) VALUES ((?), ?);");
        if (m_setIconIDForPageURLStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement setIconIDForPageURL failed");
            m_setIconIDForPageURLStatement = nullptr;
            return false;
        }
    }

    if (m_setIconIDForPageURLStatement->bindText(1, pageURL) != SQLITE_OK
        || m_setIconIDForPageURLStatement->bindInt64(2, iconID) != SQLITE_OK) {
        LOG_ERROR("FaviconDatabse::setIconIDForPageURL failed: %s", m_db.lastErrorMsg());
        return false;
    }

    if (m_setIconIDForPageURLStatement->step() != SQLITE_DONE)
        ASSERT_NOT_REACHED();

    m_setIconIDForPageURLStatement->reset();
    return true;
}

Vector<char> IconDatabase::iconData(int64_t iconID)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());

    if (!m_iconDataStatement) {
        m_iconDataStatement = makeUnique<SQLiteStatement>(m_db, "SELECT IconData.data FROM IconData WHERE IconData.iconID = (?);");
        if (m_iconDataStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement iconData failed");
            m_iconDataStatement = nullptr;
            return { };
        }
    }

    if (m_iconDataStatement->bindInt64(1, iconID) != SQLITE_OK) {
        LOG_ERROR("IconDatabase::iconData failed: %s", m_db.lastErrorMsg());
        return { };
    }

    Vector<char> result;
    if (m_iconDataStatement->step() == SQLITE_ROW)
        m_iconDataStatement->getColumnBlobAsVector(0, result);

    m_iconDataStatement->reset();
    return result;
}

Optional<int64_t> IconDatabase::addIcon(const String& iconURL, const Vector<char>& iconData)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());
    ASSERT(m_allowDatabaseWrite == AllowDatabaseWrite::Yes);

    if (!m_addIconStatement) {
        m_addIconStatement = makeUnique<SQLiteStatement>(m_db, "INSERT INTO IconInfo (url, stamp) VALUES (?, 0);");
        if (m_addIconStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement addIcon failed");
            m_addIconStatement = nullptr;
            return WTF::nullopt;
        }
    }
    if (!m_addIconDataStatement) {
        m_addIconDataStatement = makeUnique<SQLiteStatement>(m_db, "INSERT INTO IconData (iconID, data) VALUES (?, ?);");
        if (m_addIconDataStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement addIconData failed");
            m_addIconDataStatement = nullptr;
            return WTF::nullopt;
        }
    }

    if (m_addIconStatement->bindText(1, iconURL) != SQLITE_OK) {
        LOG_ERROR("IconDatabase::addIcon failed: %s", m_db.lastErrorMsg());
        return WTF::nullopt;
    }

    m_addIconStatement->step();
    m_addIconStatement->reset();

    auto iconID = m_db.lastInsertRowID();
    if (m_addIconDataStatement->bindInt64(1, iconID) != SQLITE_OK || m_addIconDataStatement->bindBlob(2, iconData.data(), iconData.size()) != SQLITE_OK) {
        LOG_ERROR("IconDatabase::addIcon failed: %s", m_db.lastErrorMsg());
        return WTF::nullopt;
    }

    m_addIconDataStatement->step();
    m_addIconDataStatement->reset();

    return iconID;
}

void IconDatabase::updateIconTimestamp(int64_t iconID, int64_t timestamp)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());
    ASSERT(m_allowDatabaseWrite == AllowDatabaseWrite::Yes);

    if (!m_updateIconTimestampStatement) {
        m_updateIconTimestampStatement = makeUnique<SQLiteStatement>(m_db, "UPDATE IconInfo SET stamp = ? WHERE iconID = ?;");
        if (m_updateIconTimestampStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement updateIconTimestamp failed");
            m_updateIconTimestampStatement = nullptr;
            return;
        }
    }

    if (m_updateIconTimestampStatement->bindInt64(1, timestamp) != SQLITE_OK || m_updateIconTimestampStatement->bindInt64(2, iconID) != SQLITE_OK) {
        LOG_ERROR("IconDatabase::updateIconTimestamp failed: %s", m_db.lastErrorMsg());
        return;
    }

    m_updateIconTimestampStatement->step();
    m_updateIconTimestampStatement->reset();
}

void IconDatabase::deleteIcon(int64_t iconID)
{
    ASSERT(!isMainThread());
    ASSERT(m_db.isOpen());
    ASSERT(m_allowDatabaseWrite == AllowDatabaseWrite::Yes);

    if (!m_deletePageURLsForIconStatement) {
        m_deletePageURLsForIconStatement = makeUnique<SQLiteStatement>(m_db, "DELETE FROM PageURL WHERE PageURL.iconID = (?);");
        if (m_deletePageURLsForIconStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement deletePageURLsForIcon failed");
            m_deletePageURLsForIconStatement = nullptr;
            return;
        }
    }
    if (!m_deleteIconDataStatement) {
        m_deleteIconDataStatement = makeUnique<SQLiteStatement>(m_db, "DELETE FROM IconData WHERE IconData.iconID = (?);");
        if (m_deleteIconDataStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement deleteIcon failed");
            m_deleteIconDataStatement = nullptr;
            return;
        }
    }
    if (!m_deleteIconStatement) {
        m_deleteIconStatement = makeUnique<SQLiteStatement>(m_db, "DELETE FROM IconInfo WHERE IconInfo.iconID = (?);");
        if (m_deleteIconStatement->prepare() != SQLITE_OK) {
            LOG_ERROR("Preparing statement deleteIcon failed");
            m_deleteIconStatement = nullptr;
            return;
        }
    }

    if (m_deletePageURLsForIconStatement->bindInt64(1, iconID) != SQLITE_OK
        || m_deleteIconDataStatement->bindInt64(1, iconID) != SQLITE_OK
        || m_deleteIconStatement->bindInt64(1, iconID) != SQLITE_OK) {
        LOG_ERROR("IconDatabase::deleteIcon failed: %s", m_db.lastErrorMsg());
        return;
    }

    m_deletePageURLsForIconStatement->step();
    m_deleteIconDataStatement->step();
    m_deleteIconStatement->step();

    m_deletePageURLsForIconStatement->reset();
    m_deleteIconDataStatement->reset();
    m_deleteIconStatement->reset();
}

void IconDatabase::checkIconURLAndSetPageURLIfNeeded(const String& iconURL, const String& pageURL, AllowDatabaseWrite allowDatabaseWrite, CompletionHandler<void(bool, bool)>&& completionHandler)
{
    ASSERT(isMainThread());

    m_workQueue->dispatch([this, protectedThis = makeRef(*this), iconURL = iconURL.isolatedCopy(), pageURL = pageURL.isolatedCopy(), allowDatabaseWrite, completionHandler = WTFMove(completionHandler)]() mutable {
        bool result = false;
        bool changed = false;
        if (m_db.isOpen()) {
            bool canWriteToDatabase = m_allowDatabaseWrite == AllowDatabaseWrite::Yes && allowDatabaseWrite == AllowDatabaseWrite::Yes;
            bool expired = false;
            String cachedIconURL;
            {
                LockHolder lockHolder(m_pageURLToIconURLMapLock);
                cachedIconURL = m_pageURLToIconURLMap.get(pageURL);
            }
            if (cachedIconURL == iconURL)
                result = true;
            else if (auto iconID = iconIDForIconURL(iconURL, expired)) {
                if (expired && canWriteToDatabase) {
                    SQLiteTransaction transaction(m_db);
                    transaction.begin();
                    deleteIcon(iconID.value());
                    transaction.commit();
                } else {
                    result = true;
                    if (!canWriteToDatabase || setIconIDForPageURL(iconID.value(), pageURL)) {
                        LockHolder lockHolder(m_pageURLToIconURLMapLock);
                        m_pageURLToIconURLMap.set(pageURL, iconURL);
                        changed = true;
                    }
                }
            } else if (!canWriteToDatabase) {
                bool foundInMemoryCache;
                {
                    LockHolder lockHolder(m_loadedIconsLock);
                    foundInMemoryCache = m_loadedIcons.contains(iconURL);
                }

                if (foundInMemoryCache) {
                    result = true;
                    LockHolder lockHolder(m_pageURLToIconURLMapLock);
                    m_pageURLToIconURLMap.set(pageURL, iconURL);
                    changed = true;
                }
            }
        }
        startPruneTimer();
        RunLoop::main().dispatch([result, changed, completionHandler = WTFMove(completionHandler)]() mutable {
            completionHandler(result, changed);
        });
    });
}

void IconDatabase::loadIconForPageURL(const String& pageURL, AllowDatabaseWrite allowDatabaseWrite, CompletionHandler<void(NativeImagePtr&&)>&& completionHandler)
{
    ASSERT(isMainThread());

    m_workQueue->dispatch([this, protectedThis = makeRef(*this), pageURL = pageURL.isolatedCopy(), allowDatabaseWrite, timestamp = WallTime::now().secondsSinceEpoch(), completionHandler = WTFMove(completionHandler)]() mutable {
        Optional<int64_t> iconID;
        Vector<char> iconData;
        String iconURL;
        {
            LockHolder lockHolder(m_pageURLToIconURLMapLock);
            iconURL = m_pageURLToIconURLMap.get(pageURL);
        }
        if (m_db.isOpen() && !iconURL.isEmpty()) {
            bool expired;
            iconID = iconIDForIconURL(iconURL, expired);
            if (iconID) {
                LockHolder lockHolder(m_loadedIconsLock);
                if (!m_loadedIcons.contains(iconURL)) {
                    iconData = this->iconData(iconID.value());
                    m_loadedIcons.set(iconURL, std::make_pair<NativeImagePtr, MonotonicTime>(nullptr, { }));
                }
            }
            bool canWriteToDatabase = m_allowDatabaseWrite == AllowDatabaseWrite::Yes && allowDatabaseWrite == AllowDatabaseWrite::Yes;
            if (iconID && canWriteToDatabase)
                updateIconTimestamp(iconID.value(), timestamp.secondsAs<int64_t>());
        }
        startPruneTimer();
        RunLoop::main().dispatch([this, protectedThis = makeRef(*this), iconURL = WTFMove(iconURL), iconData = WTFMove(iconData), completionHandler = WTFMove(completionHandler)]() mutable {
            if (iconURL.isEmpty()) {
                completionHandler(nullptr);
                return;
            }

            LockHolder lockHolder(m_loadedIconsLock);
            auto it = m_loadedIcons.find(iconURL);
            if (it != m_loadedIcons.end() && it->value.first) {
                auto icon = it->value.first;
                it->value.second = MonotonicTime::now();
                startClearLoadedIconsTimer();
                lockHolder.unlockEarly();
                completionHandler(WTFMove(icon));
                return;
            }

            auto addResult = m_loadedIcons.set(iconURL, std::make_pair<NativeImagePtr, MonotonicTime>(nullptr, MonotonicTime::now()));
            if (!iconData.isEmpty()) {
                auto image = BitmapImage::create();
                if (image->setData(SharedBuffer::create(WTFMove(iconData)), true) < EncodedDataStatus::SizeAvailable) {
                    completionHandler(nullptr);
                    return;
                }
                addResult.iterator->value.first = image->nativeImageForCurrentFrame();
            }

            auto icon = addResult.iterator->value.first;
            startClearLoadedIconsTimer();
            lockHolder.unlockEarly();
            completionHandler(WTFMove(icon));
        });
    });
}

String IconDatabase::iconURLForPageURL(const String& pageURL)
{
    ASSERT(isMainThread());

    LockHolder lockHolder(m_pageURLToIconURLMapLock);
    return m_pageURLToIconURLMap.get(pageURL);
}

void IconDatabase::setIconForPageURL(const String& iconURL, const unsigned char* iconData, size_t iconDataSize, const String& pageURL, AllowDatabaseWrite allowDatabaseWrite, CompletionHandler<void(bool)>&& completionHandler)
{
    ASSERT(isMainThread());

    // If database write is not allowed load the icon to cache it in memory only.
    if (m_allowDatabaseWrite == AllowDatabaseWrite::No || allowDatabaseWrite == AllowDatabaseWrite::No) {
        bool result = true;
        {
            LockHolder lockHolder(m_loadedIconsLock);
            auto addResult = m_loadedIcons.set(iconURL, std::make_pair<NativeImagePtr, MonotonicTime>(nullptr, { }));
            if (iconDataSize) {
                auto image = BitmapImage::create();
                if (image->setData(SharedBuffer::create(iconData, iconDataSize), true) < EncodedDataStatus::SizeAvailable)
                    result = false;
                else
                    addResult.iterator->value.first = image->nativeImageForCurrentFrame();
            }
        }
        startClearLoadedIconsTimer();
        m_workQueue->dispatch([this, protectedThis = makeRef(*this), result, iconURL = iconURL.isolatedCopy(), pageURL = pageURL.isolatedCopy(), completionHandler = WTFMove(completionHandler)]() mutable {
            {
                LockHolder lockHolder(m_pageURLToIconURLMapLock);
                m_pageURLToIconURLMap.set(pageURL, iconURL);
            }
            RunLoop::main().dispatch([result, completionHandler = WTFMove(completionHandler)]() mutable {
                completionHandler(result);
            });
        });
        return;
    }

    Vector<char> data;
    data.reserveInitialCapacity(iconDataSize);
    data.append(reinterpret_cast<const char*>(iconData), iconDataSize);
    m_workQueue->dispatch([this, protectedThis = makeRef(*this), iconURL = iconURL.isolatedCopy(), iconData = WTFMove(data), pageURL = pageURL.isolatedCopy(), completionHandler = WTFMove(completionHandler)]() mutable {
        bool result = false;
        if (m_db.isOpen()) {
            SQLiteTransaction transaction(m_db);
            transaction.begin();

            bool expired = false;
            auto iconID = iconIDForIconURL(iconURL, expired);
            if (!iconID)
                iconID = addIcon(iconURL, iconData);

            if (iconID) {
                result = true;
                if (setIconIDForPageURL(iconID.value(), pageURL)) {
                    LockHolder lockHolder(m_pageURLToIconURLMapLock);
                    m_pageURLToIconURLMap.set(pageURL, iconURL);
                }
            }

            transaction.commit();
        }
        startPruneTimer();
        RunLoop::main().dispatch([result, completionHandler = WTFMove(completionHandler)]() mutable {
            completionHandler(result);
        });
    });
}

void IconDatabase::clear(CompletionHandler<void()>&& completionHandler)
{
    ASSERT(isMainThread());

    {
        LockHolder lockHolder(m_loadedIconsLock);
        m_loadedIcons.clear();
    }
    m_workQueue->dispatch([this, protectedThis = makeRef(*this), completionHandler = WTFMove(completionHandler)]() mutable {
        {
            LockHolder lockHolder(m_pageURLToIconURLMapLock);
            m_pageURLToIconURLMap.clear();
        }

        if (m_db.isOpen() && m_allowDatabaseWrite == AllowDatabaseWrite::Yes) {
            clearStatements();
            m_db.clearAllTables();
            m_db.runVacuumCommand();
            createTablesIfNeeded();
        }

        RunLoop::main().dispatch([completionHandler = WTFMove(completionHandler)]() mutable {
            completionHandler();
        });
    });
}

} // namespace WebKit