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

#include "HTTPParsers.h"
#include "ParsedContentType.h"
#include <pal/text/DecodeEscapeSequences.h>
#include <pal/text/TextEncoding.h>
#include <wtf/MainThread.h>
#include <wtf/RunLoop.h>
#include <wtf/URL.h>
#include <wtf/WorkQueue.h>
#include <wtf/text/Base64.h>

#if PLATFORM(COCOA)
#include <wtf/cocoa/RuntimeApplicationChecksCocoa.h>
#endif

namespace WebCore {
namespace DataURLDecoder {

static bool shouldRemoveFragmentIdentifier(const String& mediaType)
{
#if PLATFORM(COCOA)
    if (!linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior::DataURLFragmentRemoval))
        return false;

    // HLS uses # in the middle of the manifests.
    return !equalLettersIgnoringASCIICase(mediaType, "video/mpegurl"_s)
        && !equalLettersIgnoringASCIICase(mediaType, "audio/mpegurl"_s)
        && !equalLettersIgnoringASCIICase(mediaType, "application/x-mpegurl"_s)
        && !equalLettersIgnoringASCIICase(mediaType, "vnd.apple.mpegurl"_s);
#else
    UNUSED_PARAM(mediaType);
    return true;
#endif
}

static WorkQueue& decodeQueue()
{
    static auto& queue = WorkQueue::create("org.webkit.DataURLDecoder", WorkQueue::QOS::UserInitiated).leakRef();
    return queue;
}

static Result parseMediaType(const String& mediaType)
{
    if (std::optional<ParsedContentType> parsedContentType = ParsedContentType::create(mediaType))
        return { parsedContentType->mimeType(), parsedContentType->charset(), parsedContentType->serialize(), { } };
    return { "text/plain"_s, "US-ASCII"_s, "text/plain;charset=US-ASCII"_s, { } };
}

struct DecodeTask {
    WTF_MAKE_FAST_ALLOCATED;
public:
    DecodeTask(const URL& url, const ScheduleContext& scheduleContext, DecodeCompletionHandler&& completionHandler)
        : url(url.isolatedCopy())
        , scheduleContext(scheduleContext)
        , completionHandler(WTFMove(completionHandler))
    {
    }

    bool process()
    {
        // Syntax:
        //  url := data:<header>,<encodedData>
        //  header := [<mediatype>][;base64]
        //  mediatype := [<mimetype>][;charset=<charsettype>]

        constexpr auto dataString = "data:"_s;
        ASSERT(url.string().startsWith(dataString));

        size_t headerStart = dataString.length();
        size_t headerEnd = url.string().find(',', headerStart);
        if (headerEnd == notFound)
            return false;
        if (size_t fragmentInHeader = url.string().reverseFind('#', headerEnd); fragmentInHeader != notFound)
            return false;
        size_t encodedDataStart = headerEnd == notFound ? headerEnd : headerEnd + 1;

        auto header = StringView(url.string()).substring(headerStart, headerEnd - headerStart);
        
        // There might one or two semicolons in the header, find the last one.
        size_t mediaTypeEnd = header.reverseFind(';');
        mediaTypeEnd = mediaTypeEnd == notFound ? header.length() : mediaTypeEnd;

        // formatTypeStart might be at the begining of "base64" or "charset=...".
        size_t formatTypeStart = mediaTypeEnd + 1;
        auto formatType = header.substring(formatTypeStart, header.length() - formatTypeStart);
        formatType = stripLeadingAndTrailingHTTPSpaces(formatType);

        isBase64 = equalLettersIgnoringASCIICase(formatType, "base64"_s);

        // If header does not end with "base64", mediaType should be the whole header.
        auto mediaType = (isBase64 ? header.left(mediaTypeEnd) : header).toString();
        mediaType = stripLeadingAndTrailingHTTPSpaces(mediaType);
        if (mediaType.startsWith(';'))
            mediaType = makeString("text/plain"_s, mediaType);

        if (shouldRemoveFragmentIdentifier(mediaType))
            url.removeFragmentIdentifier();

        encodedData = StringView(url.string()).substring(encodedDataStart);

        result = parseMediaType(mediaType);
        return true;
    }

    URL url;
    StringView encodedData;
    bool isBase64 { false };
    const ScheduleContext scheduleContext;
    DecodeCompletionHandler completionHandler;

    Result result;
};

static std::unique_ptr<DecodeTask> createDecodeTask(const URL& url, const ScheduleContext& scheduleContext, DecodeCompletionHandler&& completionHandler)
{
    return makeUnique<DecodeTask>(
        url,
        scheduleContext,
        WTFMove(completionHandler)
    );
}

static std::optional<Vector<uint8_t>> decodeBase64(const DecodeTask& task, Mode mode)
{
    switch (mode) {
    case Mode::ForgivingBase64:
        return base64Decode(PAL::decodeURLEscapeSequences(task.encodedData), { Base64DecodeOptions::ValidatePadding, Base64DecodeOptions::IgnoreSpacesAndNewLines, Base64DecodeOptions::DiscardVerticalTab });

    case Mode::Legacy:
        // First try base64url.
        if (auto decodedData = base64URLDecode(task.encodedData))
            return decodedData;
        // Didn't work, try unescaping and decoding as base64.
        return base64Decode(PAL::decodeURLEscapeSequences(task.encodedData), { Base64DecodeOptions::IgnoreSpacesAndNewLines, Base64DecodeOptions::DiscardVerticalTab });
    }

    RELEASE_ASSERT_NOT_REACHED();
}

static Vector<uint8_t> decodeEscaped(const DecodeTask& task)
{
    PAL::TextEncoding encodingFromCharset(task.result.charset);
    auto& encoding = encodingFromCharset.isValid() ? encodingFromCharset : PAL::UTF8Encoding();
    return PAL::decodeURLEscapeSequencesAsData(task.encodedData, encoding);
}

static std::optional<Result> decodeSynchronously(DecodeTask& task, Mode mode)
{
    if (!task.process())
        return std::nullopt;

    if (task.isBase64) {
        auto decodedData = decodeBase64(task, mode);
        if (!decodedData)
            return std::nullopt;
        task.result.data = WTFMove(*decodedData);
    } else
        task.result.data = decodeEscaped(task);

    task.result.data.shrinkToFit();
    return WTFMove(task.result);
}

void decode(const URL& url, const ScheduleContext& scheduleContext, Mode mode, DecodeCompletionHandler&& completionHandler)
{
    ASSERT(url.protocolIsData());

    decodeQueue().dispatch([decodeTask = createDecodeTask(url, scheduleContext, WTFMove(completionHandler)), mode]() mutable {
        auto result = decodeSynchronously(*decodeTask, mode);

#if USE(COCOA_EVENT_LOOP)
        auto scheduledPairs = decodeTask->scheduleContext.scheduledPairs;
#endif

        auto callCompletionHandler = [result = WTFMove(result), completionHandler = WTFMove(decodeTask->completionHandler)]() mutable {
            completionHandler(WTFMove(result));
        };

#if USE(COCOA_EVENT_LOOP)
        RunLoop::dispatch(scheduledPairs, WTFMove(callCompletionHandler));
#else
        RunLoop::main().dispatch(WTFMove(callCompletionHandler));
#endif
    });
}

std::optional<Result> decode(const URL& url, Mode mode)
{
    ASSERT(url.protocolIsData());

    auto task = createDecodeTask(url, { }, nullptr);
    return decodeSynchronously(*task, mode);
}

} // namespace DataURLDecoder
} // namespace WebCore
