/*
 * Copyright (C) 2015-2022 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 "NetworkCacheBlobStorage.h"

#include "Logging.h"
#include "NetworkCacheFileSystem.h"
#include <fcntl.h>
#include <wtf/FileSystem.h>
#include <wtf/Locker.h>
#include <wtf/RunLoop.h>
#include <wtf/SHA1.h>

#if !OS(WINDOWS)
#include <sys/stat.h>
#include <unistd.h>
#endif

namespace WebKit {
namespace NetworkCache {

#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
WTF_MAKE_TZONE_ALLOCATED_IMPL(BlobStorage::MemoryCache);

static BlobStorage::Blob copyBlob(const BlobStorage::Blob& blob)
{
    return { blob.data.copyData(), blob.hash };
}

BlobStorage::MemoryCache::MemoryCache(unsigned fileLimit)
    : m_limit(fileLimit)
{ }

std::optional<BlobStorage::Blob> BlobStorage::MemoryCache::get(const String& path)
{
    Locker locker { m_lock };
    auto it = m_cache.find(path);
    if (it == m_cache.end())
        return std::nullopt;

    const Blob& blob = it->value;
    return copyBlob(blob);
}

void BlobStorage::MemoryCache::add(const String& path, const BlobStorage::Blob& blob)
{
    Locker locker { m_lock };
    m_cache.set(path, copyBlob(blob));
    if (m_cache.size() > m_limit)
        m_cache.remove(m_cache.random());
}

void BlobStorage::MemoryCache::remove(const String& path)
{
    Locker locker { m_lock };
    m_cache.remove(path);
}
#endif

WTF_MAKE_TZONE_ALLOCATED_IMPL(BlobStorage);

BlobStorage::BlobStorage(const String& blobDirectoryPath, Salt salt, unsigned memoryCacheFileLimit)
    : m_blobDirectoryPath(crossThreadCopy(blobDirectoryPath))
    , m_salt(salt)
#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    , m_memoryCache(memoryCacheFileLimit ? WTF::makeUnique<MemoryCache>(memoryCacheFileLimit) : nullptr)
#endif
{
#if !ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    UNUSED_PARAM(memoryCacheFileLimit);
#endif
}

String BlobStorage::blobDirectoryPathIsolatedCopy() const
{
    return m_blobDirectoryPath.isolatedCopy();
}

void BlobStorage::synchronize()
{
    ASSERT(!RunLoop::isMain());

    auto blobDirectoryPath = blobDirectoryPathIsolatedCopy();
    FileSystem::makeAllDirectories(blobDirectoryPath);

    m_approximateSize = 0;
    auto blobDirectory = blobDirectoryPath;
    traverseDirectory(blobDirectory, [this, &blobDirectory](const String& name, DirectoryEntryType type) {
        if (type != DirectoryEntryType::File)
            return;
        auto path = FileSystem::pathByAppendingComponent(blobDirectory, name);
        auto linkCount = FileSystem::hardLinkCount(path);
        // No clients left for this blob.
        if (linkCount && *linkCount == 1) {
            FileSystem::deleteFile(path);

#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
            if (m_memoryCache)
                m_memoryCache->remove(path);
#endif
        }
        else
            m_approximateSize += FileSystem::fileSize(path).value_or(0);
    });

    LOG(NetworkCacheStorage, "(NetworkProcess) blob synchronization completed approximateSize=%zu", approximateSize());
}

String BlobStorage::blobPathForHash(const SHA1::Digest& hash) const
{
    auto hashAsString = SHA1::hexDigest(hash);
    return FileSystem::pathByAppendingComponent(blobDirectoryPathIsolatedCopy(), StringView::fromLatin1(byteCast<Latin1Character>(hashAsString.span())));
}

BlobStorage::Blob BlobStorage::add(const String& path, const Data& data, bool addToMemoryCache)
{
#if !ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    UNUSED_PARAM(addToMemoryCache);
#endif

    ASSERT(!RunLoop::isMain());

    auto hash = computeSHA1(data, m_salt);
    if (data.isEmpty())
        return { data, hash };

    String blobPath = blobPathForHash(hash);
    
    FileSystem::deleteFile(path);
#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    if (m_memoryCache)
        m_memoryCache->remove(path);
#endif

    bool blobExists = FileSystem::fileExists(blobPath);
    if (blobExists) {
        if (FileSystem::makeSafeToUseMemoryMapForPath(blobPath)) {
            auto existingData = mapFile(blobPath);
            if (bytesEqual(existingData, data)) {
                if (!FileSystem::hardLink(blobPath, path))
                    WTFLogAlways("Failed to create hard link from %s to %s", blobPath.utf8().data(), path.utf8().data());
                BlobStorage::Blob result { existingData, hash };
#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
                if (addToMemoryCache && m_memoryCache)
                    m_memoryCache->add(path, result);
#endif
                return result;
            }
        }
        FileSystem::deleteFile(blobPath);
    }

    auto mappedData = data.mapToFile(blobPath);
    if (mappedData.isNull())
        return { };

    if (!FileSystem::hardLink(blobPath, path))
        WTFLogAlways("Failed to create hard link from %s to %s", blobPath.utf8().data(), path.utf8().data());

    m_approximateSize += mappedData.size();

    BlobStorage::Blob result { mappedData, hash };
#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    if (addToMemoryCache && m_memoryCache)
        m_memoryCache->add(path, result);
#endif
    return result;
}

BlobStorage::Blob BlobStorage::get(const String& path)
{
    ASSERT(!RunLoop::isMain());

#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    if (m_memoryCache) {
        if (auto blob = m_memoryCache->get(path))
            return copyBlob(*blob);
    }
#endif

    auto data = mapFile(path);
    return { data, computeSHA1(data, m_salt) };
}

void BlobStorage::remove(const String& path)
{
    ASSERT(!RunLoop::isMain());

    FileSystem::deleteFile(path);
#if ENABLE(NETWORK_CACHE_BLOB_STORAGE_MEMORY_CACHE)
    if (m_memoryCache)
        m_memoryCache->remove(path);
#endif
}

unsigned BlobStorage::shareCount(const String& path)
{
    ASSERT(!RunLoop::isMain());

    auto linkCount = FileSystem::hardLinkCount(path);
    if (!linkCount)
        return 0;
    // Link count is 2 in the single client case (the blob file and a link).
    return *linkCount - 1;
}

}
}
