ApplicationCacheStorage.cpp   [plain text]


/*
 * Copyright (C) 2008, 2009, 2010, 2011 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. ``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
 * 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 "ApplicationCacheStorage.h"

#include "ApplicationCache.h"
#include "ApplicationCacheGroup.h"
#include "ApplicationCacheHost.h"
#include "ApplicationCacheResource.h"
#include "SQLiteDatabaseTracker.h"
#include "SQLiteStatement.h"
#include "SQLiteTransaction.h"
#include "SecurityOrigin.h"
#include "SecurityOriginData.h"
#include <wtf/FileSystem.h>
#include <wtf/StdLibExtras.h>
#include <wtf/URL.h>
#include <wtf/UUID.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringBuilder.h>

namespace WebCore {

template <class T>
class StorageIDJournal {
public:  
    ~StorageIDJournal()
    {
        for (auto& record : m_records)
            record.restore();
    }

    void add(T* resource, unsigned storageID)
    {
        m_records.append(Record(resource, storageID));
    }

    void commit()
    {
        m_records.clear();
    }

private:
    class Record {
    public:
        Record() : m_resource(nullptr), m_storageID(0) { }
        Record(T* resource, unsigned storageID) : m_resource(resource), m_storageID(storageID) { }

        void restore()
        {
            m_resource->setStorageID(m_storageID);
        }

    private:
        T* m_resource;
        unsigned m_storageID;
    };

    Vector<Record> m_records;
};

static unsigned urlHostHash(const URL& url)
{
    StringView host = url.host();
    if (host.is8Bit())
        return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(host.characters8(), host.length()));
    return AlreadyHashed::avoidDeletedValue(StringHasher::computeHashAndMaskTop8Bits(host.characters16(), host.length()));
}

ApplicationCacheGroup* ApplicationCacheStorage::loadCacheGroup(const URL& manifestURL)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    if (!m_database.isOpen())
        return nullptr;

    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL AND manifestURL=?");
    if (statement.prepare() != SQLITE_OK)
        return nullptr;
    
    statement.bindText(1, manifestURL);
   
    int result = statement.step();
    if (result == SQLITE_DONE)
        return nullptr;
    
    if (result != SQLITE_ROW) {
        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
        return nullptr;
    }
    
    unsigned newestCacheStorageID = static_cast<unsigned>(statement.getColumnInt64(2));

    auto cache = loadCache(newestCacheStorageID);
    if (!cache)
        return nullptr;
        
    auto& group = *new ApplicationCacheGroup(*this, manifestURL);
    group.setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
    group.setNewestCache(cache.releaseNonNull());
    return &group;
}    

ApplicationCacheGroup* ApplicationCacheStorage::findOrCreateCacheGroup(const URL& manifestURL)
{
    ASSERT(!manifestURL.hasFragmentIdentifier());

    auto result = m_cachesInMemory.add(manifestURL, nullptr);
    if (!result.isNewEntry) {
        ASSERT(result.iterator->value);
        return result.iterator->value;
    }

    // Look up the group in the database
    auto* group = loadCacheGroup(manifestURL);
    
    // If the group was not found we need to create it
    if (!group) {
        group = new ApplicationCacheGroup(*this, manifestURL);
        m_cacheHostSet.add(urlHostHash(manifestURL));
    }

    result.iterator->value = group;
    return group;
}

ApplicationCacheGroup* ApplicationCacheStorage::findInMemoryCacheGroup(const URL& manifestURL) const
{
    return m_cachesInMemory.get(manifestURL);
}

void ApplicationCacheStorage::loadManifestHostHashes()
{
    static bool hasLoadedHashes = false;
    
    if (hasLoadedHashes)
        return;
    
    // We set this flag to true before the database has been opened
    // to avoid trying to open the database over and over if it doesn't exist.
    hasLoadedHashes = true;
    
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    if (!m_database.isOpen())
        return;

    // Fetch the host hashes.
    SQLiteStatement statement(m_database, "SELECT manifestHostHash FROM CacheGroups");    
    if (statement.prepare() != SQLITE_OK)
        return;
    
    while (statement.step() == SQLITE_ROW)
        m_cacheHostSet.add(static_cast<unsigned>(statement.getColumnInt64(0)));
}    

ApplicationCacheGroup* ApplicationCacheStorage::cacheGroupForURL(const URL& url)
{
    ASSERT(!url.hasFragmentIdentifier());
    
    loadManifestHostHashes();
    
    // Hash the host name and see if there's a manifest with the same host.
    if (!m_cacheHostSet.contains(urlHostHash(url)))
        return nullptr;

    // Check if a cache already exists in memory.
    for (const auto& group : m_cachesInMemory.values()) {
        ASSERT(!group->isObsolete());

        if (!protocolHostAndPortAreEqual(url, group->manifestURL()))
            continue;
        
        if (ApplicationCache* cache = group->newestCache()) {
            ApplicationCacheResource* resource = cache->resourceForURL(url);
            if (!resource)
                continue;
            if (resource->type() & ApplicationCacheResource::Foreign)
                continue;
            return group;
        }
    }
    
    if (!m_database.isOpen())
        return nullptr;
        
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    // Check the database. Look for all cache groups with a newest cache.
    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
    if (statement.prepare() != SQLITE_OK)
        return nullptr;
    
    int result;
    while ((result = statement.step()) == SQLITE_ROW) {
        URL manifestURL = URL({ }, statement.getColumnText(1));

        if (m_cachesInMemory.contains(manifestURL))
            continue;

        if (!protocolHostAndPortAreEqual(url, manifestURL))
            continue;

        // We found a cache group that matches. Now check if the newest cache has a resource with
        // a matching URL.
        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
        auto cache = loadCache(newestCacheID);
        if (!cache)
            continue;

        auto* resource = cache->resourceForURL(url);
        if (!resource)
            continue;
        if (resource->type() & ApplicationCacheResource::Foreign)
            continue;

        auto& group = *new ApplicationCacheGroup(*this, manifestURL);
        group.setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
        group.setNewestCache(cache.releaseNonNull());
        m_cachesInMemory.set(group.manifestURL(), &group);

        return &group;
    }

    if (result != SQLITE_DONE)
        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
    
    return nullptr;
}

ApplicationCacheGroup* ApplicationCacheStorage::fallbackCacheGroupForURL(const URL& url)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    ASSERT(!url.hasFragmentIdentifier());

    // Check if an appropriate cache already exists in memory.
    for (auto* group : m_cachesInMemory.values()) {
        ASSERT(!group->isObsolete());

        if (ApplicationCache* cache = group->newestCache()) {
            URL fallbackURL;
            if (cache->isURLInOnlineWhitelist(url))
                continue;
            if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
                continue;
            if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
                continue;
            return group;
        }
    }
    
    if (!m_database.isOpen())
        return nullptr;
        
    // Check the database. Look for all cache groups with a newest cache.
    SQLiteStatement statement(m_database, "SELECT id, manifestURL, newestCache FROM CacheGroups WHERE newestCache IS NOT NULL");
    if (statement.prepare() != SQLITE_OK)
        return nullptr;
    
    int result;
    while ((result = statement.step()) == SQLITE_ROW) {
        URL manifestURL = URL({ }, statement.getColumnText(1));

        if (m_cachesInMemory.contains(manifestURL))
            continue;

        // Fallback namespaces always have the same origin as manifest URL, so we can avoid loading caches that cannot match.
        if (!protocolHostAndPortAreEqual(url, manifestURL))
            continue;

        // We found a cache group that matches. Now check if the newest cache has a resource with
        // a matching fallback namespace.
        unsigned newestCacheID = static_cast<unsigned>(statement.getColumnInt64(2));
        auto cache = loadCache(newestCacheID);

        URL fallbackURL;
        if (cache->isURLInOnlineWhitelist(url))
            continue;
        if (!cache->urlMatchesFallbackNamespace(url, &fallbackURL))
            continue;
        if (cache->resourceForURL(fallbackURL)->type() & ApplicationCacheResource::Foreign)
            continue;

        auto& group = *new ApplicationCacheGroup(*this, manifestURL);
        group.setStorageID(static_cast<unsigned>(statement.getColumnInt64(0)));
        group.setNewestCache(cache.releaseNonNull());

        m_cachesInMemory.set(group.manifestURL(), &group);

        return &group;
    }

    if (result != SQLITE_DONE)
        LOG_ERROR("Could not load cache group, error \"%s\"", m_database.lastErrorMsg());
    
    return nullptr;
}

void ApplicationCacheStorage::cacheGroupDestroyed(ApplicationCacheGroup& group)
{
    if (group.isObsolete()) {
        ASSERT(!group.storageID());
        ASSERT(m_cachesInMemory.get(group.manifestURL()) != &group);
        return;
    }

    ASSERT(m_cachesInMemory.get(group.manifestURL()) == &group);

    m_cachesInMemory.remove(group.manifestURL());
    
    // If the cache group is half-created, we don't want it in the saved set (as it is not stored in database).
    if (!group.storageID())
        m_cacheHostSet.remove(urlHostHash(group.manifestURL()));
}

void ApplicationCacheStorage::cacheGroupMadeObsolete(ApplicationCacheGroup& group)
{
    ASSERT(m_cachesInMemory.get(group.manifestURL()) == &group);
    ASSERT(m_cacheHostSet.contains(urlHostHash(group.manifestURL())));

    if (auto* newestCache = group.newestCache())
        remove(newestCache);

    m_cachesInMemory.remove(group.manifestURL());
    m_cacheHostSet.remove(urlHostHash(group.manifestURL()));
}

void ApplicationCacheStorage::setMaximumSize(int64_t size)
{
    m_maximumSize = size;
}

int64_t ApplicationCacheStorage::maximumSize() const
{
    return m_maximumSize;
}

bool ApplicationCacheStorage::isMaximumSizeReached() const
{
    return m_isMaximumSizeReached;
}

int64_t ApplicationCacheStorage::spaceNeeded(int64_t cacheToSave)
{
    int64_t spaceNeeded = 0;
    long long fileSize = 0;
    if (!FileSystem::getFileSize(m_cacheFile, fileSize))
        return 0;

    int64_t currentSize = fileSize + flatFileAreaSize();

    // Determine the amount of free space we have available.
    int64_t totalAvailableSize = 0;
    if (m_maximumSize < currentSize) {
        // The max size is smaller than the actual size of the app cache file.
        // This can happen if the client previously imposed a larger max size
        // value and the app cache file has already grown beyond the current
        // max size value.
        // The amount of free space is just the amount of free space inside
        // the database file. Note that this is always 0 if SQLite is compiled
        // with AUTO_VACUUM = 1.
        totalAvailableSize = m_database.freeSpaceSize();
    } else {
        // The max size is the same or larger than the current size.
        // The amount of free space available is the amount of free space
        // inside the database file plus the amount we can grow until we hit
        // the max size.
        totalAvailableSize = (m_maximumSize - currentSize) + m_database.freeSpaceSize();
    }

    // The space needed to be freed in order to accommodate the failed cache is
    // the size of the failed cache minus any already available free space.
    spaceNeeded = cacheToSave - totalAvailableSize;
    // The space needed value must be positive (or else the total already
    // available free space would be larger than the size of the failed cache and
    // saving of the cache should have never failed).
    ASSERT(spaceNeeded);
    return spaceNeeded;
}

void ApplicationCacheStorage::setDefaultOriginQuota(int64_t quota)
{
    m_defaultOriginQuota = quota;
}

bool ApplicationCacheStorage::calculateQuotaForOrigin(const SecurityOrigin& origin, int64_t& quota)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    // If an Origin record doesn't exist, then the COUNT will be 0 and quota will be 0.
    // Using the count to determine if a record existed or not is a safe way to determine
    // if a quota of 0 is real, from the record, or from null.
    SQLiteStatement statement(m_database, "SELECT COUNT(quota), quota FROM Origins WHERE origin=?");
    if (statement.prepare() != SQLITE_OK)
        return false;

    statement.bindText(1, origin.data().databaseIdentifier());
    int result = statement.step();

    // Return the quota, or if it was null the default.
    if (result == SQLITE_ROW) {
        bool wasNoRecord = statement.getColumnInt64(0) == 0;
        quota = wasNoRecord ? m_defaultOriginQuota : statement.getColumnInt64(1);
        return true;
    }

    LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
    return false;
}

bool ApplicationCacheStorage::calculateUsageForOrigin(const SecurityOrigin* origin, int64_t& usage)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    // If an Origins record doesn't exist, then the SUM will be null,
    // which will become 0, as expected, when converting to a number.
    SQLiteStatement statement(m_database, "SELECT SUM(Caches.size)"
                                          "  FROM CacheGroups"
                                          " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
                                          " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
                                          " WHERE Origins.origin=?");
    if (statement.prepare() != SQLITE_OK)
        return false;

    statement.bindText(1, origin->data().databaseIdentifier());
    int result = statement.step();

    if (result == SQLITE_ROW) {
        usage = statement.getColumnInt64(0);
        return true;
    }

    LOG_ERROR("Could not get the quota of an origin, error \"%s\"", m_database.lastErrorMsg());
    return false;
}

bool ApplicationCacheStorage::calculateRemainingSizeForOriginExcludingCache(const SecurityOrigin& origin, ApplicationCache* cache, int64_t& remainingSize)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    if (!m_database.isOpen())
        return false;

    // Remaining size = total origin quota - size of all caches with origin excluding the provided cache.
    // Keep track of the number of caches so we can tell if the result was a calculation or not.
    const char* query;
    int64_t excludingCacheIdentifier = cache ? cache->storageID() : 0;
    if (excludingCacheIdentifier != 0) {
        query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
                "  FROM CacheGroups"
                " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
                " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
                " WHERE Origins.origin=?"
                "   AND Caches.id!=?";
    } else {
        query = "SELECT COUNT(Caches.size), Origins.quota - SUM(Caches.size)"
                "  FROM CacheGroups"
                " INNER JOIN Origins ON CacheGroups.origin = Origins.origin"
                " INNER JOIN Caches ON CacheGroups.id = Caches.cacheGroup"
                " WHERE Origins.origin=?";
    }

    SQLiteStatement statement(m_database, query);
    if (statement.prepare() != SQLITE_OK)
        return false;

    statement.bindText(1, origin.data().databaseIdentifier());
    if (excludingCacheIdentifier != 0)
        statement.bindInt64(2, excludingCacheIdentifier);
    int result = statement.step();

    // If the count was 0 that then we have to query the origin table directly
    // for its quota. Otherwise we can use the calculated value.
    if (result == SQLITE_ROW) {
        int64_t numberOfCaches = statement.getColumnInt64(0);
        if (numberOfCaches == 0)
            calculateQuotaForOrigin(origin, remainingSize);
        else
            remainingSize = statement.getColumnInt64(1);
        return true;
    }

    LOG_ERROR("Could not get the remaining size of an origin's quota, error \"%s\"", m_database.lastErrorMsg());
    return false;
}

bool ApplicationCacheStorage::storeUpdatedQuotaForOrigin(const SecurityOrigin* origin, int64_t quota)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(true);
    if (!m_database.isOpen())
        return false;

    if (!ensureOriginRecord(origin))
        return false;

    SQLiteStatement updateStatement(m_database, "UPDATE Origins SET quota=? WHERE origin=?");
    if (updateStatement.prepare() != SQLITE_OK)
        return false;

    updateStatement.bindInt64(1, quota);
    updateStatement.bindText(2, origin->data().databaseIdentifier());

    return executeStatement(updateStatement);
}

bool ApplicationCacheStorage::executeSQLCommand(const String& sql)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    ASSERT(m_database.isOpen());
    
    bool result = m_database.executeCommand(sql);
    if (!result)
        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", 
                  sql.utf8().data(), m_database.lastErrorMsg());

    return result;
}

// Update the schemaVersion when the schema of any the Application Cache
// SQLite tables changes. This allows the database to be rebuilt when
// a new, incompatible change has been introduced to the database schema.
static const int schemaVersion = 7;
    
void ApplicationCacheStorage::verifySchemaVersion()
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());

    int version = SQLiteStatement(m_database, "PRAGMA user_version").getColumnInt(0);
    if (version == schemaVersion)
        return;

    // Version will be 0 if we just created an empty file. Trying to delete tables would cause errors, because they don't exist yet.
    if (version)
        deleteTables();

    // Update user version.
    SQLiteTransaction setDatabaseVersion(m_database);
    setDatabaseVersion.begin();

    char userVersionSQL[32];
    int unusedNumBytes = snprintf(userVersionSQL, sizeof(userVersionSQL), "PRAGMA user_version=%d", schemaVersion);
    ASSERT_UNUSED(unusedNumBytes, static_cast<int>(sizeof(userVersionSQL)) >= unusedNumBytes);

    SQLiteStatement statement(m_database, userVersionSQL);
    if (statement.prepare() != SQLITE_OK)
        return;
    
    executeStatement(statement);
    setDatabaseVersion.commit();
}
    
void ApplicationCacheStorage::openDatabase(bool createIfDoesNotExist)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    if (m_database.isOpen())
        return;

    // The cache directory should never be null, but if it for some weird reason is we bail out.
    if (m_cacheDirectory.isNull())
        return;

    m_cacheFile = FileSystem::pathByAppendingComponent(m_cacheDirectory, "ApplicationCache.db");
    if (!createIfDoesNotExist && !FileSystem::fileExists(m_cacheFile))
        return;

    FileSystem::makeAllDirectories(m_cacheDirectory);
    m_database.open(m_cacheFile);
    
    if (!m_database.isOpen())
        return;
    
    verifySchemaVersion();
    
    // Create tables
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheGroups (id INTEGER PRIMARY KEY AUTOINCREMENT, "
                      "manifestHostHash INTEGER NOT NULL ON CONFLICT FAIL, manifestURL TEXT UNIQUE ON CONFLICT FAIL, newestCache INTEGER, origin TEXT)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS Caches (id INTEGER PRIMARY KEY AUTOINCREMENT, cacheGroup INTEGER, size INTEGER)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheWhitelistURLs (url TEXT NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheAllowsAllNetworkRequests (wildcard INTEGER NOT NULL ON CONFLICT FAIL, cache INTEGER NOT NULL ON CONFLICT FAIL)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS FallbackURLs (namespace TEXT NOT NULL ON CONFLICT FAIL, fallbackURL TEXT NOT NULL ON CONFLICT FAIL, "
                      "cache INTEGER NOT NULL ON CONFLICT FAIL)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheEntries (cache INTEGER NOT NULL ON CONFLICT FAIL, type INTEGER, resource INTEGER NOT NULL)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL ON CONFLICT FAIL, "
                      "statusCode INTEGER NOT NULL, responseURL TEXT NOT NULL, mimeType TEXT, textEncodingName TEXT, headers TEXT, data INTEGER NOT NULL ON CONFLICT FAIL)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS CacheResourceData (id INTEGER PRIMARY KEY AUTOINCREMENT, data BLOB, path TEXT)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS DeletedCacheResources (id INTEGER PRIMARY KEY AUTOINCREMENT, path TEXT)");
    executeSQLCommand("CREATE TABLE IF NOT EXISTS Origins (origin TEXT UNIQUE ON CONFLICT IGNORE, quota INTEGER NOT NULL ON CONFLICT FAIL)");

    // When a cache is deleted, all its entries and its whitelist should be deleted.
    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheDeleted AFTER DELETE ON Caches"
                      " FOR EACH ROW BEGIN"
                      "  DELETE FROM CacheEntries WHERE cache = OLD.id;"
                      "  DELETE FROM CacheWhitelistURLs WHERE cache = OLD.id;"
                      "  DELETE FROM CacheAllowsAllNetworkRequests WHERE cache = OLD.id;"
                      "  DELETE FROM FallbackURLs WHERE cache = OLD.id;"
                      " END");

    // When a cache entry is deleted, its resource should also be deleted.
    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheEntryDeleted AFTER DELETE ON CacheEntries"
                      " FOR EACH ROW BEGIN"
                      "  DELETE FROM CacheResources WHERE id = OLD.resource;"
                      " END");

    // When a cache resource is deleted, its data blob should also be deleted.
    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDeleted AFTER DELETE ON CacheResources"
                      " FOR EACH ROW BEGIN"
                      "  DELETE FROM CacheResourceData WHERE id = OLD.data;"
                      " END");
    
    // When a cache resource is deleted, if it contains a non-empty path, that path should
    // be added to the DeletedCacheResources table so the flat file at that path can
    // be deleted at a later time.
    executeSQLCommand("CREATE TRIGGER IF NOT EXISTS CacheResourceDataDeleted AFTER DELETE ON CacheResourceData"
                      " FOR EACH ROW"
                      " WHEN OLD.path NOT NULL BEGIN"
                      "  INSERT INTO DeletedCacheResources (path) values (OLD.path);"
                      " END");
}

bool ApplicationCacheStorage::executeStatement(SQLiteStatement& statement)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    bool result = statement.executeCommand();
    if (!result)
        LOG_ERROR("Application Cache Storage: failed to execute statement \"%s\" error \"%s\"", 
                  statement.query().utf8().data(), m_database.lastErrorMsg());
    
    return result;
}    

bool ApplicationCacheStorage::store(ApplicationCacheGroup* group, GroupStorageIDJournal* journal)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    ASSERT(group->storageID() == 0);
    ASSERT(journal);

    // For some reason, an app cache may be partially written to disk. In particular, there may be
    // a cache group with an identical manifest URL and associated cache entries. We want to remove
    // this cache group and its associated cache entries so that we can create it again (below) as
    // a way to repair it.
    deleteCacheGroupRecord(group->manifestURL());

    SQLiteStatement statement(m_database, "INSERT INTO CacheGroups (manifestHostHash, manifestURL, origin) VALUES (?, ?, ?)");
    if (statement.prepare() != SQLITE_OK)
        return false;

    statement.bindInt64(1, urlHostHash(group->manifestURL()));
    statement.bindText(2, group->manifestURL());
    statement.bindText(3, group->origin().data().databaseIdentifier());

    if (!executeStatement(statement))
        return false;

    unsigned groupStorageID = static_cast<unsigned>(m_database.lastInsertRowID());

    if (!ensureOriginRecord(&group->origin()))
        return false;

    group->setStorageID(groupStorageID);
    journal->add(group, 0);
    return true;
}    

bool ApplicationCacheStorage::store(ApplicationCache* cache, ResourceStorageIDJournal* storageIDJournal)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    ASSERT(cache->storageID() == 0);
    ASSERT(cache->group()->storageID() != 0);
    ASSERT(storageIDJournal);
    
    SQLiteStatement statement(m_database, "INSERT INTO Caches (cacheGroup, size) VALUES (?, ?)");
    if (statement.prepare() != SQLITE_OK)
        return false;

    statement.bindInt64(1, cache->group()->storageID());
    statement.bindInt64(2, cache->estimatedSizeInStorage());

    if (!executeStatement(statement))
        return false;
    
    unsigned cacheStorageID = static_cast<unsigned>(m_database.lastInsertRowID());

    // Store all resources
    for (auto& resource : cache->resources().values()) {
        unsigned oldStorageID = resource->storageID();
        if (!store(resource.get(), cacheStorageID))
            return false;

        // Storing the resource succeeded. Log its old storageID in case
        // it needs to be restored later.
        storageIDJournal->add(resource.get(), oldStorageID);
    }
    
    // Store the online whitelist
    const Vector<URL>& onlineWhitelist = cache->onlineWhitelist();
    {
        for (auto& whitelistURL : onlineWhitelist) {
            SQLiteStatement statement(m_database, "INSERT INTO CacheWhitelistURLs (url, cache) VALUES (?, ?)");
            statement.prepare();

            statement.bindText(1, whitelistURL);
            statement.bindInt64(2, cacheStorageID);

            if (!executeStatement(statement))
                return false;
        }
    }

    // Store online whitelist wildcard flag.
    {
        SQLiteStatement statement(m_database, "INSERT INTO CacheAllowsAllNetworkRequests (wildcard, cache) VALUES (?, ?)");
        statement.prepare();

        statement.bindInt64(1, cache->allowsAllNetworkRequests());
        statement.bindInt64(2, cacheStorageID);

        if (!executeStatement(statement))
            return false;
    }
    
    // Store fallback URLs.
    const FallbackURLVector& fallbackURLs = cache->fallbackURLs();
    {
        for (auto& fallbackURL : fallbackURLs) {
            SQLiteStatement statement(m_database, "INSERT INTO FallbackURLs (namespace, fallbackURL, cache) VALUES (?, ?, ?)");
            statement.prepare();

            statement.bindText(1, fallbackURL.first);
            statement.bindText(2, fallbackURL.second);
            statement.bindInt64(3, cacheStorageID);

            if (!executeStatement(statement))
                return false;
        }
    }

    cache->setStorageID(cacheStorageID);
    return true;
}

bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, unsigned cacheStorageID)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    ASSERT(cacheStorageID);
    ASSERT(!resource->storageID());
    
    openDatabase(true);

    // openDatabase(true) could still fail, for example when cacheStorage is full or no longer available.
    if (!m_database.isOpen())
        return false;

    // First, insert the data
    SQLiteStatement dataStatement(m_database, "INSERT INTO CacheResourceData (data, path) VALUES (?, ?)");
    if (dataStatement.prepare() != SQLITE_OK)
        return false;
    

    String fullPath;
    if (!resource->path().isEmpty())
        dataStatement.bindText(2, FileSystem::pathGetFileName(resource->path()));
    else if (shouldStoreResourceAsFlatFile(resource)) {
        // First, check to see if creating the flat file would violate the maximum total quota. We don't need
        // to check the per-origin quota here, as it was already checked in storeNewestCache().
        if (m_database.totalSize() + flatFileAreaSize() + static_cast<int64_t>(resource->data().size()) > m_maximumSize) {
            m_isMaximumSizeReached = true;
            return false;
        }
        
        String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
        FileSystem::makeAllDirectories(flatFileDirectory);

        String extension;
        
        String fileName = resource->response().suggestedFilename();
        size_t dotIndex = fileName.reverseFind('.');
        if (dotIndex != notFound && dotIndex < (fileName.length() - 1))
            extension = fileName.substring(dotIndex);

        String path;
        if (!writeDataToUniqueFileInDirectory(resource->data(), flatFileDirectory, path, extension))
            return false;
        
        fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
        resource->setPath(fullPath);
        dataStatement.bindText(2, path);
    } else {
        if (resource->data().size())
            dataStatement.bindBlob(1, resource->data().data(), resource->data().size());
    }
    
    if (!dataStatement.executeCommand()) {
        // Clean up the file which we may have written to:
        if (!fullPath.isEmpty())
            FileSystem::deleteFile(fullPath);

        return false;
    }

    unsigned dataId = static_cast<unsigned>(m_database.lastInsertRowID());

    // Then, insert the resource
    
    // Serialize the headers
    StringBuilder stringBuilder;
    
    for (const auto& header : resource->response().httpHeaderFields()) {
        stringBuilder.append(header.key);
        stringBuilder.append(':');
        stringBuilder.append(header.value);
        stringBuilder.append('\n');
    }
    
    String headers = stringBuilder.toString();
    
    SQLiteStatement resourceStatement(m_database, "INSERT INTO CacheResources (url, statusCode, responseURL, headers, data, mimeType, textEncodingName) VALUES (?, ?, ?, ?, ?, ?, ?)");
    if (resourceStatement.prepare() != SQLITE_OK)
        return false;
    
    // The same ApplicationCacheResource are used in ApplicationCacheResource::size()
    // to calculate the approximate size of an ApplicationCacheResource object. If
    // you change the code below, please also change ApplicationCacheResource::size().
    resourceStatement.bindText(1, resource->url());
    resourceStatement.bindInt64(2, resource->response().httpStatusCode());
    resourceStatement.bindText(3, resource->response().url());
    resourceStatement.bindText(4, headers);
    resourceStatement.bindInt64(5, dataId);
    resourceStatement.bindText(6, resource->response().mimeType());
    resourceStatement.bindText(7, resource->response().textEncodingName());

    if (!executeStatement(resourceStatement))
        return false;

    unsigned resourceId = static_cast<unsigned>(m_database.lastInsertRowID());
    
    // Finally, insert the cache entry
    SQLiteStatement entryStatement(m_database, "INSERT INTO CacheEntries (cache, type, resource) VALUES (?, ?, ?)");
    if (entryStatement.prepare() != SQLITE_OK)
        return false;
    
    entryStatement.bindInt64(1, cacheStorageID);
    entryStatement.bindInt64(2, resource->type());
    entryStatement.bindInt64(3, resourceId);
    
    if (!executeStatement(entryStatement))
        return false;
    
    // Did we successfully write the resource data to a file? If so,
    // release the resource's data and free up a potentially large amount
    // of memory:
    if (!fullPath.isEmpty())
        resource->data().clear();

    resource->setStorageID(resourceId);
    return true;
}

bool ApplicationCacheStorage::storeUpdatedType(ApplicationCacheResource* resource, ApplicationCache* cache)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    ASSERT_UNUSED(cache, cache->storageID());
    ASSERT(resource->storageID());

    // First, insert the data
    SQLiteStatement entryStatement(m_database, "UPDATE CacheEntries SET type=? WHERE resource=?");
    if (entryStatement.prepare() != SQLITE_OK)
        return false;

    entryStatement.bindInt64(1, resource->type());
    entryStatement.bindInt64(2, resource->storageID());

    return executeStatement(entryStatement);
}

bool ApplicationCacheStorage::store(ApplicationCacheResource* resource, ApplicationCache* cache)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    ASSERT(cache->storageID());
    
    openDatabase(true);

    if (!m_database.isOpen())
        return false;
 
    m_isMaximumSizeReached = false;
    m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());

    SQLiteTransaction storeResourceTransaction(m_database);
    storeResourceTransaction.begin();
    
    if (!store(resource, cache->storageID())) {
        checkForMaxSizeReached();
        return false;
    }

    // A resource was added to the cache. Update the total data size for the cache.
    SQLiteStatement sizeUpdateStatement(m_database, "UPDATE Caches SET size=size+? WHERE id=?");
    if (sizeUpdateStatement.prepare() != SQLITE_OK)
        return false;

    sizeUpdateStatement.bindInt64(1, resource->estimatedSizeInStorage());
    sizeUpdateStatement.bindInt64(2, cache->storageID());

    if (!executeStatement(sizeUpdateStatement))
        return false;
    
    storeResourceTransaction.commit();
    return true;
}

bool ApplicationCacheStorage::ensureOriginRecord(const SecurityOrigin* origin)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    SQLiteStatement insertOriginStatement(m_database, "INSERT INTO Origins (origin, quota) VALUES (?, ?)");
    if (insertOriginStatement.prepare() != SQLITE_OK)
        return false;

    insertOriginStatement.bindText(1, origin->data().databaseIdentifier());
    insertOriginStatement.bindInt64(2, m_defaultOriginQuota);
    if (!executeStatement(insertOriginStatement))
        return false;

    return true;
}

bool ApplicationCacheStorage::checkOriginQuota(ApplicationCacheGroup* group, ApplicationCache* oldCache, ApplicationCache* newCache, int64_t& totalSpaceNeeded)
{
    // Check if the oldCache with the newCache would reach the per-origin quota.
    int64_t remainingSpaceInOrigin;
    auto& origin = group->origin();
    if (calculateRemainingSizeForOriginExcludingCache(origin, oldCache, remainingSpaceInOrigin)) {
        if (remainingSpaceInOrigin < newCache->estimatedSizeInStorage()) {
            int64_t quota;
            if (calculateQuotaForOrigin(origin, quota)) {
                totalSpaceNeeded = quota - remainingSpaceInOrigin + newCache->estimatedSizeInStorage();
                return false;
            }

            ASSERT_NOT_REACHED();
            totalSpaceNeeded = 0;
            return false;
        }
    }

    return true;
}

bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup& group, ApplicationCache* oldCache, FailureReason& failureReason)
{
    openDatabase(true);

    if (!m_database.isOpen())
        return false;

    m_isMaximumSizeReached = false;
    m_database.setMaximumSize(m_maximumSize - flatFileAreaSize());

    SQLiteTransaction storeCacheTransaction(m_database);
    
    storeCacheTransaction.begin();

    // Check if this would reach the per-origin quota.
    int64_t totalSpaceNeededIgnored;
    if (!checkOriginQuota(&group, oldCache, group.newestCache(), totalSpaceNeededIgnored)) {
        failureReason = OriginQuotaReached;
        return false;
    }

    GroupStorageIDJournal groupStorageIDJournal;
    if (!group.storageID()) {
        // Store the group
        if (!store(&group, &groupStorageIDJournal)) {
            checkForMaxSizeReached();
            failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
            return false;
        }
    }
    
    ASSERT(group.newestCache());
    ASSERT(!group.isObsolete());
    ASSERT(!group.newestCache()->storageID());
    
    // Log the storageID changes to the in-memory resource objects. The journal
    // object will roll them back automatically in case a database operation
    // fails and this method returns early.
    ResourceStorageIDJournal resourceStorageIDJournal;

    // Store the newest cache
    if (!store(group.newestCache(), &resourceStorageIDJournal)) {
        checkForMaxSizeReached();
        failureReason = isMaximumSizeReached() ? TotalQuotaReached : DiskOrOperationFailure;
        return false;
    }
    
    // Update the newest cache in the group.
    
    SQLiteStatement statement(m_database, "UPDATE CacheGroups SET newestCache=? WHERE id=?");
    if (statement.prepare() != SQLITE_OK) {
        failureReason = DiskOrOperationFailure;
        return false;
    }
    
    statement.bindInt64(1, group.newestCache()->storageID());
    statement.bindInt64(2, group.storageID());
    
    if (!executeStatement(statement)) {
        failureReason = DiskOrOperationFailure;
        return false;
    }
    
    groupStorageIDJournal.commit();
    resourceStorageIDJournal.commit();
    storeCacheTransaction.commit();
    return true;
}

bool ApplicationCacheStorage::storeNewestCache(ApplicationCacheGroup& group)
{
    // Ignore the reason for failing, just attempt the store.
    FailureReason ignoredFailureReason;
    return storeNewestCache(group, nullptr, ignoredFailureReason);
}

template<typename CharacterType>
static inline void parseHeader(const CharacterType* header, unsigned headerLength, ResourceResponse& response)
{
    ASSERT(find(header, headerLength, ':') != notFound);
    unsigned colonPosition = find(header, headerLength, ':');

    // Save memory by putting the header names into atom strings so each is stored only once,
    // even though the setHTTPHeaderField function does not require an atom string.
    AtomString headerName { header, colonPosition };
    String headerValue { header + colonPosition + 1, headerLength - colonPosition - 1 };

    response.setHTTPHeaderField(headerName, headerValue);
}

static inline void parseHeaders(const String& headers, ResourceResponse& response)
{
    unsigned startPos = 0;
    size_t endPos;
    while ((endPos = headers.find('\n', startPos)) != notFound) {
        ASSERT(startPos != endPos);

        if (headers.is8Bit())
            parseHeader(headers.characters8() + startPos, endPos - startPos, response);
        else
            parseHeader(headers.characters16() + startPos, endPos - startPos, response);
        
        startPos = endPos + 1;
    }
    
    if (startPos != headers.length()) {
        if (headers.is8Bit())
            parseHeader(headers.characters8(), headers.length(), response);
        else
            parseHeader(headers.characters16(), headers.length(), response);
    }
}
    
RefPtr<ApplicationCache> ApplicationCacheStorage::loadCache(unsigned storageID)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    SQLiteStatement cacheStatement(m_database,
                                   "SELECT url, statusCode, type, mimeType, textEncodingName, headers, CacheResourceData.data, CacheResourceData.path FROM CacheEntries INNER JOIN CacheResources ON CacheEntries.resource=CacheResources.id "
                                   "INNER JOIN CacheResourceData ON CacheResourceData.id=CacheResources.data WHERE CacheEntries.cache=?");
    if (cacheStatement.prepare() != SQLITE_OK) {
        LOG_ERROR("Could not prepare cache statement, error \"%s\"", m_database.lastErrorMsg());
        return nullptr;
    }
    
    cacheStatement.bindInt64(1, storageID);

    auto cache = ApplicationCache::create();

    String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);

    int result;
    while ((result = cacheStatement.step()) == SQLITE_ROW) {
        URL url({ }, cacheStatement.getColumnText(0));
        
        int httpStatusCode = cacheStatement.getColumnInt(1);

        unsigned type = static_cast<unsigned>(cacheStatement.getColumnInt64(2));

        Vector<char> blob;
        cacheStatement.getColumnBlobAsVector(6, blob);
        
        auto data = SharedBuffer::create(WTFMove(blob));
        
        String path = cacheStatement.getColumnText(7);
        long long size = 0;
        if (path.isEmpty())
            size = data->size();
        else {
            path = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
            FileSystem::getFileSize(path, size);
        }
        
        String mimeType = cacheStatement.getColumnText(3);
        String textEncodingName = cacheStatement.getColumnText(4);
        
        ResourceResponse response(url, mimeType, size, textEncodingName);
        response.setHTTPStatusCode(httpStatusCode);

        String headers = cacheStatement.getColumnText(5);
        parseHeaders(headers, response);
        
        auto resource = ApplicationCacheResource::create(url, response, type, WTFMove(data), path);

        if (type & ApplicationCacheResource::Manifest)
            cache->setManifestResource(WTFMove(resource));
        else
            cache->addResource(WTFMove(resource));
    }

    if (result != SQLITE_DONE)
        LOG_ERROR("Could not load cache resources, error \"%s\"", m_database.lastErrorMsg());

    if (!cache->manifestResource()) {
        LOG_ERROR("Could not load application cache because there was no manifest resource");
        return nullptr;
    }

    // Load the online whitelist
    SQLiteStatement whitelistStatement(m_database, "SELECT url FROM CacheWhitelistURLs WHERE cache=?");
    if (whitelistStatement.prepare() != SQLITE_OK)
        return nullptr;
    whitelistStatement.bindInt64(1, storageID);
    
    Vector<URL> whitelist;
    while ((result = whitelistStatement.step()) == SQLITE_ROW) 
        whitelist.append(URL({ }, whitelistStatement.getColumnText(0)));

    if (result != SQLITE_DONE)
        LOG_ERROR("Could not load cache online whitelist, error \"%s\"", m_database.lastErrorMsg());

    cache->setOnlineWhitelist(whitelist);

    // Load online whitelist wildcard flag.
    SQLiteStatement whitelistWildcardStatement(m_database, "SELECT wildcard FROM CacheAllowsAllNetworkRequests WHERE cache=?");
    if (whitelistWildcardStatement.prepare() != SQLITE_OK)
        return nullptr;
    whitelistWildcardStatement.bindInt64(1, storageID);
    
    result = whitelistWildcardStatement.step();
    if (result != SQLITE_ROW)
        LOG_ERROR("Could not load cache online whitelist wildcard flag, error \"%s\"", m_database.lastErrorMsg());

    cache->setAllowsAllNetworkRequests(whitelistWildcardStatement.getColumnInt64(0));

    if (whitelistWildcardStatement.step() != SQLITE_DONE)
        LOG_ERROR("Too many rows for online whitelist wildcard flag");

    // Load fallback URLs.
    SQLiteStatement fallbackStatement(m_database, "SELECT namespace, fallbackURL FROM FallbackURLs WHERE cache=?");
    if (fallbackStatement.prepare() != SQLITE_OK)
        return nullptr;
    fallbackStatement.bindInt64(1, storageID);
    
    FallbackURLVector fallbackURLs;
    while ((result = fallbackStatement.step()) == SQLITE_ROW) 
        fallbackURLs.append(std::make_pair(URL({ }, fallbackStatement.getColumnText(0)), URL({ }, fallbackStatement.getColumnText(1))));

    if (result != SQLITE_DONE)
        LOG_ERROR("Could not load fallback URLs, error \"%s\"", m_database.lastErrorMsg());

    cache->setFallbackURLs(fallbackURLs);
    
    cache->setStorageID(storageID);

    return cache;
}    
    
void ApplicationCacheStorage::remove(ApplicationCache* cache)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    if (!cache->storageID())
        return;
    
    openDatabase(false);
    if (!m_database.isOpen())
        return;

    ASSERT(cache->group());
    ASSERT(cache->group()->storageID());

    // All associated data will be deleted by database triggers.
    SQLiteStatement statement(m_database, "DELETE FROM Caches WHERE id=?");
    if (statement.prepare() != SQLITE_OK)
        return;
    
    statement.bindInt64(1, cache->storageID());
    executeStatement(statement);

    cache->clearStorageID();

    if (cache->group()->newestCache() == cache) {
        // Currently, there are no triggers on the cache group, which is why the cache had to be removed separately above.
        SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
        if (groupStatement.prepare() != SQLITE_OK)
            return;
        
        groupStatement.bindInt64(1, cache->group()->storageID());
        executeStatement(groupStatement);

        cache->group()->clearStorageID();
    }
    
    checkForDeletedResources();
}    

void ApplicationCacheStorage::empty()
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    
    if (!m_database.isOpen())
        return;
    
    // Clear cache groups, caches, cache resources, and origins.
    executeSQLCommand("DELETE FROM CacheGroups");
    executeSQLCommand("DELETE FROM Caches");
    executeSQLCommand("DELETE FROM Origins");
    
    // Clear the storage IDs for the caches in memory.
    // The caches will still work, but cached resources will not be saved to disk 
    // until a cache update process has been initiated.
    for (auto* group : m_cachesInMemory.values())
        group->clearStorageID();

    checkForDeletedResources();
}
    
void ApplicationCacheStorage::deleteTables()
{
    empty();
    m_database.clearAllTables();
}
    
bool ApplicationCacheStorage::shouldStoreResourceAsFlatFile(ApplicationCacheResource* resource)
{
    auto& type = resource->response().mimeType();
    return startsWithLettersIgnoringASCIICase(type, "audio/") || startsWithLettersIgnoringASCIICase(type, "video/");
}
    
bool ApplicationCacheStorage::writeDataToUniqueFileInDirectory(SharedBuffer& data, const String& directory, String& path, const String& fileExtension)
{
    String fullPath;
    
    do {
        path = FileSystem::encodeForFileName(createCanonicalUUIDString()) + fileExtension;
        // Guard against the above function being called on a platform which does not implement
        // createCanonicalUUIDString().
        ASSERT(!path.isEmpty());
        if (path.isEmpty())
            return false;
        
        fullPath = FileSystem::pathByAppendingComponent(directory, path);
    } while (FileSystem::directoryName(fullPath) != directory || FileSystem::fileExists(fullPath));
    
    FileSystem::PlatformFileHandle handle = FileSystem::openFile(fullPath, FileSystem::FileOpenMode::Write);
    if (!handle)
        return false;
    
    int64_t writtenBytes = FileSystem::writeToFile(handle, data.data(), data.size());
    FileSystem::closeFile(handle);
    
    if (writtenBytes != static_cast<int64_t>(data.size())) {
        FileSystem::deleteFile(fullPath);
        return false;
    }
    
    return true;
}

Optional<Vector<URL>> ApplicationCacheStorage::manifestURLs()
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    if (!m_database.isOpen())
        return WTF::nullopt;

    SQLiteStatement selectURLs(m_database, "SELECT manifestURL FROM CacheGroups");

    if (selectURLs.prepare() != SQLITE_OK)
        return WTF::nullopt;

    Vector<URL> urls;
    while (selectURLs.step() == SQLITE_ROW)
        urls.append(URL({ }, selectURLs.getColumnText(0)));

    return urls;
}

bool ApplicationCacheStorage::deleteCacheGroupRecord(const String& manifestURL)
{
    ASSERT(SQLiteDatabaseTracker::hasTransactionInProgress());
    SQLiteStatement idStatement(m_database, "SELECT id FROM CacheGroups WHERE manifestURL=?");
    if (idStatement.prepare() != SQLITE_OK)
        return false;

    idStatement.bindText(1, manifestURL);

    int result = idStatement.step();
    if (result != SQLITE_ROW)
        return false;

    int64_t groupId = idStatement.getColumnInt64(0);

    SQLiteStatement cacheStatement(m_database, "DELETE FROM Caches WHERE cacheGroup=?");
    if (cacheStatement.prepare() != SQLITE_OK)
        return false;

    SQLiteStatement groupStatement(m_database, "DELETE FROM CacheGroups WHERE id=?");
    if (groupStatement.prepare() != SQLITE_OK)
        return false;

    cacheStatement.bindInt64(1, groupId);
    executeStatement(cacheStatement);
    groupStatement.bindInt64(1, groupId);
    executeStatement(groupStatement);
    return true;
}

bool ApplicationCacheStorage::deleteCacheGroup(const String& manifestURL)
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    SQLiteTransaction deleteTransaction(m_database);

    // Check to see if the group is in memory.
    if (auto* group = m_cachesInMemory.get(manifestURL))
        cacheGroupMadeObsolete(*group);
    else {
        // The cache group is not in memory, so remove it from the disk.
        openDatabase(false);
        if (!m_database.isOpen())
            return false;
        if (!deleteCacheGroupRecord(manifestURL)) {
            LOG_ERROR("Could not delete cache group record, error \"%s\"", m_database.lastErrorMsg());
            return false;
        }
    }

    deleteTransaction.commit();

    checkForDeletedResources();

    return true;
}

void ApplicationCacheStorage::vacuumDatabaseFile()
{
    SQLiteTransactionInProgressAutoCounter transactionCounter;

    openDatabase(false);
    if (!m_database.isOpen())
        return;

    m_database.runVacuumCommand();
}

void ApplicationCacheStorage::checkForMaxSizeReached()
{
    if (m_database.lastError() == SQLITE_FULL)
        m_isMaximumSizeReached = true;
}
    
void ApplicationCacheStorage::checkForDeletedResources()
{
    openDatabase(false);
    if (!m_database.isOpen())
        return;

    // Select only the paths in DeletedCacheResources that do not also appear in CacheResourceData:
    SQLiteStatement selectPaths(m_database, "SELECT DeletedCacheResources.path "
        "FROM DeletedCacheResources "
        "LEFT JOIN CacheResourceData "
        "ON DeletedCacheResources.path = CacheResourceData.path "
        "WHERE (SELECT DeletedCacheResources.path == CacheResourceData.path) IS NULL");
    
    if (selectPaths.prepare() != SQLITE_OK)
        return;
    
    if (selectPaths.step() != SQLITE_ROW)
        return;
    
    do {
        String path = selectPaths.getColumnText(0);
        if (path.isEmpty())
            continue;
        
        String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
        String fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
        
        // Don't exit the flatFileDirectory! This should only happen if the "path" entry contains a directory 
        // component, but protect against it regardless.
        if (FileSystem::directoryName(fullPath) != flatFileDirectory)
            continue;
        
        FileSystem::deleteFile(fullPath);
    } while (selectPaths.step() == SQLITE_ROW);
    
    executeSQLCommand("DELETE FROM DeletedCacheResources");
}
    
long long ApplicationCacheStorage::flatFileAreaSize()
{
    openDatabase(false);
    if (!m_database.isOpen())
        return 0;
    
    SQLiteStatement selectPaths(m_database, "SELECT path FROM CacheResourceData WHERE path NOT NULL");

    if (selectPaths.prepare() != SQLITE_OK) {
        LOG_ERROR("Could not load flat file cache resource data, error \"%s\"", m_database.lastErrorMsg());
        return 0;
    }

    long long totalSize = 0;
    String flatFileDirectory = FileSystem::pathByAppendingComponent(m_cacheDirectory, m_flatFileSubdirectoryName);
    while (selectPaths.step() == SQLITE_ROW) {
        String path = selectPaths.getColumnText(0);
        String fullPath = FileSystem::pathByAppendingComponent(flatFileDirectory, path);
        long long pathSize = 0;
        if (!FileSystem::getFileSize(fullPath, pathSize))
            continue;
        totalSize += pathSize;
    }
    
    return totalSize;
}

Vector<Ref<SecurityOrigin>> ApplicationCacheStorage::originsWithCache()
{
    auto urls = manifestURLs();
    if (!urls)
        return { };

    // Multiple manifest URLs might share the same SecurityOrigin, so we might be creating extra, wasted origins here.
    // The current schema doesn't allow for a more efficient way of building this list.
    Vector<Ref<SecurityOrigin>> origins;
    origins.reserveInitialCapacity(urls->size());
    for (auto& url : *urls)
        origins.uncheckedAppend(SecurityOrigin::create(url));
    return origins;
}

void ApplicationCacheStorage::deleteAllEntries()
{
    empty();
    vacuumDatabaseFile();
}

void ApplicationCacheStorage::deleteAllCaches()
{
    auto origins = originsWithCache();
    for (auto& origin : origins)
        deleteCacheForOrigin(origin);

    vacuumDatabaseFile();
}

void ApplicationCacheStorage::deleteCacheForOrigin(const SecurityOrigin& securityOrigin)
{
    auto urls = manifestURLs();
    if (!urls) {
        LOG_ERROR("Failed to retrieve ApplicationCache manifest URLs");
        return;
    }

    URL originURL(URL(), securityOrigin.toString());

    for (const auto& url : *urls) {
        if (!protocolHostAndPortAreEqual(url, originURL))
            continue;

        if (auto* group = findInMemoryCacheGroup(url))
            group->makeObsolete();
        else
            deleteCacheGroup(url);
    }
}

int64_t ApplicationCacheStorage::diskUsageForOrigin(const SecurityOrigin& securityOrigin)
{
    int64_t usage = 0;
    calculateUsageForOrigin(&securityOrigin, usage);
    return usage;
}

ApplicationCacheStorage::ApplicationCacheStorage(const String& cacheDirectory, const String& flatFileSubdirectoryName)
    : m_cacheDirectory(cacheDirectory)
    , m_flatFileSubdirectoryName(flatFileSubdirectoryName)
{
}

} // namespace WebCore