/*
 * Copyright (C) 2004, 2007, 2008, 2011, 2012 Apple Inc. All rights reserved.
 * Copyright (C) 2012 Research In Motion Limited. 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 COMPUTER, 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 "KURL.h"

#include "DecodeEscapeSequences.h"
#include "KURLHash.h"
#include "TextEncoding.h"
#include "URLParser.h"
#include <stdio.h>
#include <wtf/HashMap.h>
#if !USE(WTFURL)
#include <wtf/HexNumber.h>
#endif
#include <wtf/NeverDestroyed.h>
#include <wtf/StdLibExtras.h>
#include <wtf/text/CString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringHash.h>
#include <wtf/text/StringView.h>

#if USE(ICU_UNICODE)
#include <unicode/uidna.h>
#elif USE(QT4_UNICODE)
#include <QUrl>
#elif USE(GLIB_UNICODE)
#include <glib.h>
#include <wtf/gobject/GOwnPtr.h>
#endif

// FIXME: This file makes too much use of the + operator on String.
// We either have to optimize that operator so it doesn't involve
// so many allocations, or change this to use StringBuffer instead.

using namespace std;
using namespace WTF;

namespace WebCore {

typedef Vector<char, 512> CharBuffer;
typedef Vector<UChar, 512> UCharBuffer;

static const unsigned invalidPortNumber = 0xFFFF;

static ALWAYS_INLINE bool isLetterMatchIgnoringCase(UChar character, char lowercaseLetter)
{
    ASSERT(isASCIILower(lowercaseLetter));
    return (character | 0x20) == lowercaseLetter;
}

#if !USE(GOOGLEURL) && !USE(WTFURL)

static ALWAYS_INLINE bool isLetterMatchIgnoringCase(char character, char lowercaseLetter)
{
    ASSERT(isASCIILower(lowercaseLetter));
    return (character | 0x20) == lowercaseLetter;
}

static void appendDecodedHostname(StringBuilder& buffer, const UChar* str, unsigned strLen);

static ALWAYS_INLINE bool isSchemeCharacterMatchIgnoringCase(char character, char schemeCharacter)
{
    ASSERT(isSchemeChar(character));
    ASSERT(schemeCharacter & 0x20);
    ASSERT(isASCIILower(schemeCharacter) || (!isASCIIUpper(schemeCharacter) && isSchemeChar(schemeCharacter)));
    return (character | 0x20) == schemeCharacter;
}

// Copies the source to the destination, assuming all the source characters are ASCII.
// The destination buffer must be large enough. Null characters are allowed in the
// source string, and no attempt is made to null-terminate the destination buffer.
static void copyASCII(const String& string, char* destination)
{
    if (string.is8Bit())
        memcpy(destination, string.characters8(), string.length());
    else {
        auto source = string.characters16();
        for (unsigned i = 0, length = string.length(); i < length; i++)
            destination[i] = static_cast<char>(source[i]);
    }
}

DLL_PUBLIC KURL::KURL(ParsedURLStringTag, const String& absoluteURL)
{
	URLParser parser(absoluteURL);
	*this = parser.result();
#if OS(WINDOWS)
	// FIXME(148598): Work around Windows local file handling bug in CFNetwork
	ASSERT(isLocalFile() || url == m_string);
#else
	ASSERT(absoluteURL == m_string);
#endif
}

DLL_PUBLIC KURL::KURL(const KURL& base, const String& relative, const URLTextEncoding* encoding)
{
    *this = URLParser(relative, base, encoding).result();
}

static bool shouldTrimFromURL(UChar character)
{
    // Ignore leading/trailing whitespace and control characters.
    return character <= ' ';
}

KURL KURL::isolatedCopy() const
{
    KURL result = *this;
    result.m_string = result.m_string.isolatedCopy();
    return result;
}

String KURL::lastPathComponent() const
{
    if (!hasPath())
        return String();

    unsigned end = m_pathEnd - 1;
    if (m_string[end] == '/')
        --end;

    size_t start = m_string.reverseFind('/', end);
    if (start < pathStart())
        return String();
    ++start;

    return m_string.substring(start, end - start + 1);
}

DLL_PUBLIC unsigned KURL::pathStart() const
{
    unsigned start = m_hostEnd + m_portLength;
    if (start == m_schemeEnd + 1
        && start + 1 < m_string.length()
        && m_string[start] == '/' && m_string[start + 1] == '.')
        start += 2;
    return start;
}

bool KURL::hasSpecialScheme() const
{
    // https://url.spec.whatwg.org/#special-scheme
    return protocolIs("ftp")
        || protocolIs("file")
        || protocolIs("http")
        || protocolIs("https")
        || protocolIs("ws")
        || protocolIs("wss");
}

DLL_PUBLIC StringView KURL::host() const
{
    if (!m_isValid)
        return StringView();

    auto start = hostStart();
    return StringView(m_string).substring(start, m_hostEnd - start);
}

DLL_PUBLIC Optional<uint16_t> KURL::port() const
{
	return m_portLength ? parseUInt16(StringView(m_string).substring(m_hostEnd + 1, m_portLength - 1)) : WTF::Nullopt;
}

String KURL::hostAndPort() const
{
    if (auto port = this->port())
        return makeString(host(), ':', String::number(port.value()));
    return host().toString();
}

String KURL::protocolHostAndPort() const
{
    if (!hasCredentials())
        return m_string.substring(0, pathStart());

    return makeString(
        StringView(m_string).substring(0, m_userStart),
        StringView(m_string).substring(hostStart(), pathStart() - hostStart())
    );
}

static Optional<LChar> decodeEscapeSequence(StringView input, unsigned index, unsigned length)
{
    if (index + 3 > length || input[index] != '%')
        return WTF::Nullopt;
    auto digit1 = input[index + 1];
    auto digit2 = input[index + 2];
    if (!isASCIIHexDigit(digit1) || !isASCIIHexDigit(digit2))
        return WTF::Nullopt;
    return toASCIIHexValue(digit1, digit2);
}

static String decodeEscapeSequencesFromParsedURL(StringView input)
{
    ASSERT(input.isAllASCII());

    auto length = input.length();
    if (length < 3 || !input.contains('%'))
        return input.toString();

    // FIXME: This 100 is arbitrary. Should make a histogram of how this function is actually used to choose a better value.
    Vector<LChar, 100> percentDecoded;
    percentDecoded.reserveInitialCapacity(length);
    for (unsigned i = 0; i < length; ) {
        if (auto decodedCharacter = decodeEscapeSequence(input, i, length)) {
            percentDecoded.uncheckedAppend(*decodedCharacter);
            i += 3;
        } else {
            percentDecoded.uncheckedAppend(input[i]);
            ++i;
        }
    }

    // FIXME: Is UTF-8 always the correct encoding?
    // FIXME: This returns a null string when we encounter an invalid UTF-8 sequence. Is that OK?
    return String::fromUTF8(percentDecoded.data(), percentDecoded.size());
}

String KURL::user() const
{
    return decodeEscapeSequencesFromParsedURL(encodedUser());
}

String KURL::password() const
{
    return decodeEscapeSequencesFromParsedURL(encodedPassword());
}

String KURL::encodedUser() const
{
    return m_string.substring(m_userStart, m_userEnd - m_userStart);
}

String KURL::encodedPassword() const
{
    if (m_passwordEnd == m_userEnd)
        return String();

    return m_string.substring(m_userEnd + 1, m_passwordEnd - m_userEnd - 1);
}

#if !USE(CF)

String KURL::fileSystemPath() const
{
    if (!isLocalFile())
        return String();

    return decodeEscapeSequencesFromParsedURL(path());
}

#endif

#ifdef NDEBUG

static inline void assertProtocolIsGood(StringView)
{
}

#else

static void assertProtocolIsGood(StringView protocol)
{
    for (size_t i = 0; i < protocol.length(); ++i) {
        const char c = protocol[i];
        ASSERT(c > ' ' && c < 0x7F && !(c >= 'A' && c <= 'Z'));
    }
}

#endif

Optional<uint16_t> defaultPortForProtocol(StringView protocol)
{
    return URLParser::defaultPortForProtocol(protocol);
}

bool isDefaultPortForProtocol(uint16_t port, StringView protocol)
{
    return defaultPortForProtocol(protocol) == port;
}

DLL_PUBLIC bool KURL::protocolIs(const char* protocol) const
{
#ifndef NDEBUG
    assertProtocolIsGood(StringView(reinterpret_cast<const LChar*>(protocol), strlen(protocol)));
#endif

    // JavaScript URLs are "valid" and should be executed even if KURL decides they are invalid.
    // The free function protocolIsJavaScript() should be used instead. 
    ASSERT(!equalLettersIgnoringASCIICase(protocol, String("javascript")));

    if (!m_isValid)
        return false;

    // Do the comparison without making a new string object.
    for (unsigned i = 0; i < m_schemeEnd; ++i) {
        if (!protocol[i] || !isSchemeCharacterMatchIgnoringCase(m_string[i], protocol[i]))
            return false;
    }
    return !protocol[m_schemeEnd]; // We should have consumed all characters in the argument.
}

bool KURL::protocolIs(StringView protocol) const
{
    assertProtocolIsGood(protocol);

    if (!m_isValid)
        return false;

    if (m_schemeEnd != protocol.length())
        return false;

    // Do the comparison without making a new string object.
    for (unsigned i = 0; i < m_schemeEnd; ++i) {
        if (!isSchemeCharacterMatchIgnoringCase(m_string[i], protocol[i]))
            return false;
    }
    return true;
}

DLL_PUBLIC String KURL::unicode() const
{
    if (!m_isValid)
    	return String();

	StringBuilder result;
    auto characters = StringView(m_string).upconvertedCharacters();
	result.appendUnchecked(characters.get(), hostStart());
	appendDecodedHostname(result, characters.get() + hostStart(), hostEnd() - hostStart());
	result.appendUnchecked(characters.get() + hostEnd(), pathStart() - hostEnd());
	result.append(decodeURLEscapeSequences(m_string.substring(pathStart(), pathEnd() - pathStart())));
	result.appendUnchecked(characters.get() + pathEnd(), m_string.length() - pathEnd());
	return result.toString();
}

bool KURL::setProtocol(StringView newProtocol)
{
    // Firefox and IE remove everything after the first ':'.
    auto newProtocolPrefix = newProtocol.left(newProtocol.find(':'));
    auto newProtocolCanonicalized = URLParser::maybeCanonicalizeScheme(newProtocolPrefix);
    if (!newProtocolCanonicalized)
        return false;

    if (!m_isValid) {
        parse(makeString(*newProtocolCanonicalized , ":", m_string));
        return true;
    }

    // FIXME: https://bugs.webkit.org/show_bug.cgi?id=229427
    if (URLParser::isSpecialScheme(this->protocol()) && !URLParser::isSpecialScheme(*newProtocolCanonicalized))
        return true;

    if ((m_passwordEnd != m_userStart || port()) && *newProtocolCanonicalized == "file")
        return true;

    if (protocolIsFile() && host().isEmpty())
        return true;

    parse(makeString(*newProtocolCanonicalized, StringView(m_string).substring(m_schemeEnd)));
    return true;
}

// Appends the punycoded hostname identified by the given string and length to
// the output buffer. The result will not be null terminated.
// Return value of false means error in encoding.
static bool appendEncodedHostname(UCharBuffer& buffer, StringView string)
{
    // hostnameBuffer needs to be big enough to hold an IDN-encoded name.
    // For host names bigger than this, we won't do IDN encoding, which is almost certainly OK.
    if (string.length() > URLParser::hostnameBufferLength || string.isAllASCII()) {
        append(buffer, string);
        return true;
    }

    UChar hostnameBuffer[URLParser::hostnameBufferLength];
    UErrorCode error = U_ZERO_ERROR;
    UIDNAInfo processingDetails = UIDNA_INFO_INITIALIZER;
    int32_t numCharactersConverted = uidna_nameToASCII(&URLParser::internationalDomainNameTranscoder(),
        string.upconvertedCharacters(), string.length(), hostnameBuffer, URLParser::hostnameBufferLength, &processingDetails, &error);

    if (U_SUCCESS(error) && !(processingDetails.errors & ~URLParser::allowedNameToASCIIErrors) && numCharactersConverted) {
        buffer.append(hostnameBuffer, numCharactersConverted);
        return true;
    }
    return false;
}

unsigned KURL::credentialsEnd() const
{
    // Include '@' too if we have it.
    unsigned end = m_passwordEnd;
    if (end != m_hostEnd && m_string[end] == '@')
        end += 1;
    return end;
}

static bool forwardSlashHashOrQuestionMark(UChar c)
{
    return c == '/'
        || c == '#'
        || c == '?';
}

static bool slashHashOrQuestionMark(UChar c)
{
    return forwardSlashHashOrQuestionMark(c) || c == '\\';
}

void KURL::setHost(StringView newHost)
{
    if (!m_isValid)
        return;

    if (newHost.contains(':') && !newHost.startsWith('['))
        return;

    auto index = newHost.find(hasSpecialScheme() ? slashHashOrQuestionMark : forwardSlashHashOrQuestionMark);
    if (index != notFound)
        newHost = newHost.substring(0, index);

    Vector<UChar, 512> encodedHostName;
    if (hasSpecialScheme() && !appendEncodedHostname(encodedHostName, newHost))
        return;

    bool slashSlashNeeded = m_userStart == m_schemeEnd + 1;
    parse(makeString(
    	m_string.left(hostStart()),
    	slashSlashNeeded ? "//" : "",
		hasSpecialScheme() ? StringView(encodedHostName.data(), encodedHostName.size()) : newHost,
		m_string.substring(m_hostEnd)
	));
}

void KURL::removePort()
{
    if (!m_portLength)
        return;
    parse(makeString(StringView(m_string).left(m_hostEnd), StringView(m_string).substring(m_hostEnd + m_portLength)));
}

void KURL::setPort(Optional<uint16_t> port)
{
    if (!m_isValid)
        return;

    if (!port) {
        remove(m_hostEnd, m_portLength);
        return;
    }

    parse(makeString(
        StringView(m_string).left(m_hostEnd),
        ':',
        String::number(static_cast<unsigned>(*port)),
        StringView(m_string).substring(pathStart())
    ));
}

static unsigned countASCIIDigits(StringView string)
{
    unsigned length = string.length();
    for (unsigned count = 0; count < length; ++count) {
        if (!isASCIIDigit(string[count]))
            return count;
    }
    return length;
}

void KURL::setHostAndPort(StringView hostAndPort)
{
    if (!m_isValid)
        return;

    {
		auto index = hostAndPort.find(hasSpecialScheme() ? slashHashOrQuestionMark : forwardSlashHashOrQuestionMark);
		if (index != notFound)
			hostAndPort = hostAndPort.left(index);
    }

    auto colonIndex = hostAndPort.reverseFind(':');
    if (!colonIndex)
        return;

    auto ipv6Separator = hostAndPort.reverseFind(']');
    if (colonIndex == notFound || (ipv6Separator != notFound && ipv6Separator > colonIndex)) {
        setHost(hostAndPort);
        return;
    }

    auto portString = hostAndPort.substring(colonIndex + 1);
    auto hostName = hostAndPort.left(colonIndex);
    // Multiple colons are acceptable only in case of IPv6.
    if (hostName.contains(':') && ipv6Separator == notFound)
        return;

    unsigned portLength = countASCIIDigits(portString);
    if (!portLength) {
        setHost(hostName);
        return;
    }
    portString = portString.left(portLength);
	if (!parseUInt16(portString))
		portString = StringView();

    UCharBuffer encodedHostName;
    if (hasSpecialScheme() && !appendEncodedHostname(encodedHostName, hostName))
        return;

    bool slashSlashNeeded = m_userStart == m_schemeEnd + 1U;
    parse(makeString(
		m_string.left(hostStart()),
		slashSlashNeeded ? "//" : "",
		hasSpecialScheme() ? StringView(encodedHostName.data(), encodedHostName.size()) : hostName,
		portString.isEmpty() ? "" : ":",
		portString,
        StringView(m_string).substring(pathStart())
    ));
}

void KURL::removeHostAndPort()
{
    if (m_isValid)
        remove(hostStart(), pathStart() - hostStart());
}

static String percentEncodeCharacters(StringView input, bool(*shouldEncode)(UChar))
{
    auto encode = [shouldEncode] (StringView input) {
        CString utf8 = input.utf8();
        auto* data = utf8.data();
        StringBuilder builder;
        auto length = utf8.length();
        for (unsigned j = 0; j < length; j++) {
            auto c = data[j];
            if (shouldEncode(c)) {
                builder.append('%');
                builder.append(upperNibbleToASCIIHexDigit(c));
                builder.append(lowerNibbleToASCIIHexDigit(c));
            } else
                builder.append(c);
        }
        return builder.toString();
    };

    for (size_t i = 0; i < input.length(); ++i) {
        if (UNLIKELY(shouldEncode(input[i])))
            return encode(input);
    }
    return input.toString();
}

void KURL::parse(const String& string)
{
    *this = URLParser(string).result();
}

void KURL::parseAllowingC0AtEnd(const String& string)
{
    *this = URLParser(string, KURL(), URLTextEncodingSentinelAllowingC0AtEnd).result();
}

void KURL::remove(unsigned start, unsigned length)
{
    if (!length)
        return;
    ASSERT(start < m_string.length());
    ASSERT(length <= m_string.length() - start);

    String stringAfterRemoval = WTFMove(m_string);
    stringAfterRemoval.remove(start, length);
    parse(stringAfterRemoval);
}

void KURL::setUser(StringView newUser)
{
    if (!m_isValid)
        return;

    unsigned end = m_userEnd;
    if (!newUser.isEmpty()) {
        bool slashSlashNeeded = m_userStart == m_schemeEnd + 1;
        bool needSeparator = end == m_hostEnd || (end == m_passwordEnd && m_string[end] != '@');
        parse(makeString(
            StringView(m_string).left(m_userStart),
            slashSlashNeeded ? "//" : "",
            percentEncodeCharacters(newUser.toStringWithoutCopying(), URLParser::isInUserInfoEncodeSet),
            needSeparator ? "@" : "",
            StringView(m_string).substring(end)
        ));
    } else {
        // Remove '@' if we now have neither user nor password.
        if (m_userEnd == m_passwordEnd && end != m_hostEnd && m_string[end] == '@')
            end += 1;
        remove(m_userStart, end - m_userStart);
    }
}

void KURL::setPassword(StringView newPassword)
{
    if (!m_isValid)
        return;

    if (!newPassword.isEmpty()) {
        bool needLeadingSlashes = m_userEnd == m_schemeEnd + 1U;
        parse(makeString(
            StringView(m_string).left(m_userEnd),
            needLeadingSlashes ? "//:" : ":",
            percentEncodeCharacters(newPassword.toStringWithoutCopying(), URLParser::isInUserInfoEncodeSet),
            '@',
            StringView(m_string).substring(credentialsEnd())
        ));
    } else {
        unsigned end = m_userStart == m_userEnd ? credentialsEnd() : m_passwordEnd;
        remove(m_userEnd, end - m_userEnd);
    }
}

void KURL::removeCredentials()
{
    if (!m_isValid)
        return;

    remove(m_userStart, credentialsEnd() - m_userStart);
}

void KURL::setFragmentIdentifier(StringView identifier)
{
    if (!m_isValid)
        return;

    parseAllowingC0AtEnd(makeString(StringView(m_string).left(m_queryEnd), '#', identifier));
}

void KURL::maybeTrimTrailingSpacesFromOpaquePath()
{
    if (!m_isValid || !hasOpaquePath() || hasFragmentIdentifier() || hasQuery())
        return;

    parse(makeString(StringView(m_string).left(m_pathEnd)));
}

DLL_PUBLIC void KURL::removeFragmentIdentifier()
{
    if (!m_isValid)
        return;

    m_string = m_string.left(m_queryEnd);

    maybeTrimTrailingSpacesFromOpaquePath();
}

void KURL::removeQueryAndFragmentIdentifier()
{
    if (!m_isValid)
        return;

    m_string = m_string.left(m_pathEnd);
    m_queryEnd = m_pathEnd;

    maybeTrimTrailingSpacesFromOpaquePath();
}

void KURL::setQuery(StringView newQuery)
{
    // FIXME: Consider renaming this function to setEncodedQuery and/or calling percentEncodeCharacters the way setPath does.
    // https://webkit.org/b/161176

    if (!m_isValid)
        return;

    parseAllowingC0AtEnd(makeString(
        StringView(m_string).left(m_pathEnd),
        (!newQuery.startsWith('?') && !newQuery.isNull()) ? "?" : "",
        newQuery,
        StringView(m_string).substring(m_queryEnd)
    ));

    if (newQuery.isNull())
        maybeTrimTrailingSpacesFromOpaquePath();
}

static String escapePathWithoutCopying(StringView path)
{
    auto questionMarkOrNumberSignOrNonASCII = [] (UChar character) {
        return character == '?' || character == '#' || !isASCII(character);
    };
    return percentEncodeCharacters(path.toStringWithoutCopying(), questionMarkOrNumberSignOrNonASCII);
}

void KURL::setPath(StringView path)
{
    if (!m_isValid)
        return;

    parseAllowingC0AtEnd(makeString(
        StringView(m_string).left(pathStart()),
        path.startsWith('/') || (path.startsWith('\\') && (hasSpecialScheme() || protocolIs("file"))) || (!hasSpecialScheme() && path.isEmpty() && m_schemeEnd + 1U < pathStart()) ? "" : "/",
        !hasSpecialScheme() && host().isEmpty() && path.startsWith("//") && path.length() > 2 ? "/." : "",
        escapePathWithoutCopying(path),
        StringView(m_string).substring(m_pathEnd)
    ));
}

StringView KURL::stringWithoutQueryOrFragmentIdentifier() const
{
    if (!m_isValid)
        return m_string;

    return StringView(m_string).left(pathEnd());
}

String KURL::stringWithoutFragmentIdentifier() const
{
    if (!m_isValid)
        return m_string;

    return m_string.left(m_queryEnd);
}

StringView KURL::viewWithoutFragmentIdentifier() const
{
    if (!m_isValid)
        return m_string;

    return StringView(m_string).left(m_queryEnd);
}

String decodeURLEscapeSequences(const String& string)
{
    if (string.isEmpty())
        return string;
    return decodeEscapeSequences<URLEscapeSequence>(string, UTF8Encoding());
}

String decodeURLEscapeSequences(const String& string, const TextEncoding& encoding)
{
    if (string.isEmpty())
        return string;
    return decodeEscapeSequences<URLEscapeSequence>(string, encoding);
}

template<size_t length>
static inline bool equal(const char* a, const char (&b)[length])
{
    for (size_t i = 0; i < length; ++i) {
        if (a[i] != b[i])
            return false;
    }
    return true;
}

template<size_t lengthB>
static inline bool equal(const char* stringA, size_t lengthA, const char (&stringB)[lengthB])
{
    return lengthA == lengthB && equal(stringA, stringB);
}

bool equalIgnoringFragmentIdentifier(const KURL& a, const KURL& b)
{
    return a.stringWithoutFragmentIdentifier() == b.stringWithoutFragmentIdentifier();
}

bool protocolHostAndPortAreEqual(const KURL& a, const KURL& b)
{
    if (a.m_schemeEnd != b.m_schemeEnd)
        return false;

    unsigned hostStartA = a.hostStart();
    unsigned hostLengthA = a.hostEnd() - hostStartA;
    unsigned hostStartB = b.hostStart();
    unsigned hostLengthB = b.hostEnd() - b.hostStart();
    if (hostLengthA != hostLengthB)
        return false;

    // Check the scheme
    for (unsigned i = 0; i < a.m_schemeEnd; ++i) {
        if (a.string()[i] != b.string()[i])
            return false;
    }

    // And the host
    for (unsigned i = 0; i < hostLengthA; ++i) {
        if (a.string()[hostStartA + i] != b.string()[hostStartB + i])
            return false;
    }

    if (a.port() != b.port())
        return false;

    return true;
}

bool KURL::isMatchingDomain(const String& domain) const
{
	// FIXME: Consider moving this to an appropriate place in WebCore's plug-in code; don't want people tempted to use this instead of SecurityOrigin.
    if (isNull())
        return false;

    if (domain.isEmpty())
        return true;

    if (!protocolIsInHTTPFamily())
        return false;

    auto host = this->host();
    if (!host.endsWith(domain))
        return false;

    return host.length() == domain.length() || host[host.length() - domain.length() - 1] == '.';
}

// FIXME: Rename this so it's clear that it does the appropriate escaping for KURL query field values.
String encodeWithURLEscapeSequences(const String& input)
{
    return percentEncodeCharacters(input, URLParser::isInUserInfoEncodeSet);
}

// Appends the unicode hostname identified by the given string and length to
// the output buffer. The result will not be null terminated.
static void appendDecodedHostname(StringBuilder& buffer, const UChar* str, unsigned strLen)
{
    const unsigned hostnameBufferLength = 2048;

    if (strLen > hostnameBufferLength) {
        buffer.appendUnchecked(str, strLen);
        return;
    }

#if USE(ICU_UNICODE)
    UChar hostnameBuffer[hostnameBufferLength];
    UErrorCode error = U_ZERO_ERROR;
    int32_t numCharactersConverted = uidna_IDNToUnicode(str, strLen, hostnameBuffer, hostnameBufferLength, UIDNA_ALLOW_UNASSIGNED, 0, &error);

    // conversion can fail if icudata does not include support for punycode conversions
    if (error == U_ZERO_ERROR)
        buffer.appendUnchecked(hostnameBuffer, numCharactersConverted);
    else
        buffer.appendUnchecked(str, strLen);
#endif
}

void KURL::copyToBuffer(CharBuffer& buffer) const
{
    // FIXME: This throws away the high bytes of all the characters in the string!
    // That's fine for a valid KURL, which is all ASCII, but not for invalid URLs.
    buffer.resize(m_string.length());
    copyASCII(m_string, buffer.data());
}

static bool protocolIsInternal(StringView string, const char* protocol)
{
    assertProtocolIsGood(protocol);
    bool isLeading = true;
    for (auto codeUnit : string.codeUnits()) {
        if (isLeading) {
            // Skip leading whitespace and control characters.
            if (shouldTrimFromURL(codeUnit))
                continue;
            isLeading = false;
        } else {
            // Skip tabs and newlines even later in the protocol.
            if (codeUnit == '\t' || codeUnit == '\r' || codeUnit == '\n')
                continue;
        }

        char expectedCharacter = *protocol++;
        if (!expectedCharacter)
            return codeUnit == ':';
        if (!isASCIIAlphaCaselessEqual(codeUnit, expectedCharacter))
            return false;
    }
    return false;
}

bool protocolIs(const String& url, const char* protocol)
{
    return protocolIsInternal(url, protocol);
}

inline bool KURL::protocolIs(const String& string, const char* protocol)
{
    return WebCore::protocolIsInternal(string, protocol);
}

#ifndef NDEBUG
void KURL::print() const
{
    printf("%s\n", m_string.utf8().data());
}
#endif

#endif // !USE(GOOGLEURL) && !USE(WTFURL)

String KURL::strippedForUseAsReferrer() const
{
    if (!m_isValid)
        return m_string;

    unsigned end = credentialsEnd();

    if (m_userStart == end && m_queryEnd == m_string.length())
        return m_string;

    return makeString(
        StringView(m_string).substring(0, m_userStart),
        StringView(m_string).substring(end, m_queryEnd - end)
    );
}

bool protocolIsJavaScript(const String& url)
{
    return protocolIsInternal(url, "javascript");
}

bool protocolIsJavaScript(StringView url)
{
    return protocolIsInternal(url, "javascript");
}

bool protocolIsInHTTPFamily(const String& url)
{
    auto length = url.length();
    // Do the comparison without making a new string object.
    return length >= 5
        && isASCIIAlphaCaselessEqual(url[0], 'h')
        && isASCIIAlphaCaselessEqual(url[1], 't')
        && isASCIIAlphaCaselessEqual(url[2], 't')
        && isASCIIAlphaCaselessEqual(url[3], 'p')
        && (url[4] == ':' || (isASCIIAlphaCaselessEqual(url[4], 's') && length >= 6 && url[5] == ':'));
}

const KURL& blankURL()
{
    static NeverDestroyed<KURL> staticBlankURL(KURL(), "about:blank");
    return staticBlankURL;
}

const KURL& aboutSrcDocURL()
{
    static NeverDestroyed<KURL> staticAboutSrcDocURL(KURL(), "about:srcdoc");
    return staticAboutSrcDocURL;
}

bool KURL::protocolIsAbout() const
{
    return protocolIs("about");
}

bool portAllowed(const KURL& url)
{
    auto port = url.port();

    // Since most URLs don't have a port, return early for the "no port" case.
    if (!port)
        return true;

    // This blocked port list matches the port blocking that Mozilla implements.
    // See http://www.mozilla.org/projects/netlib/PortBanning.html for more information.
    static const uint16_t blockedPortList[] = {
        1,    // tcpmux
        7,    // echo
        9,    // discard
        11,   // systat
        13,   // daytime
        15,   // netstat
        17,   // qotd
        19,   // chargen
        20,   // FTP-data
        21,   // FTP-control
        22,   // SSH
        23,   // telnet
        25,   // SMTP
        37,   // time
        42,   // name
        43,   // nicname
        53,   // domain
        77,   // priv-rjs
        79,   // finger
        87,   // ttylink
        95,   // supdup
        101,  // hostriame
        102,  // iso-tsap
        103,  // gppitnp
        104,  // acr-nema
        109,  // POP2
        110,  // POP3
        111,  // sunrpc
        113,  // auth
        115,  // SFTP
        117,  // uucp-path
        119,  // nntp
        123,  // NTP
        135,  // loc-srv / epmap
        139,  // netbios
        143,  // IMAP2
        179,  // BGP
        389,  // LDAP
        465,  // SMTP+SSL
        512,  // print / exec
        513,  // login
        514,  // shell
        515,  // printer
        526,  // tempo
        530,  // courier
        531,  // Chat
        532,  // netnews
        540,  // UUCP
        556,  // remotefs
        563,  // NNTP+SSL
        587,  // ESMTP
        601,  // syslog-conn
        636,  // LDAP+SSL
        993,  // IMAP+SSL
        995,  // POP3+SSL
        2049, // NFS
        3659, // apple-sasl / PasswordServer [Apple addition]
        4045, // lockd
        6000, // X11
        6665, // Alternate IRC [Apple addition]
        6666, // Alternate IRC [Apple addition]
        6667, // Standard IRC [Apple addition]
        6668, // Alternate IRC [Apple addition]
        6669, // Alternate IRC [Apple addition]
    };
    const unsigned short* const blockedPortListEnd = blockedPortList + WTF_ARRAY_LENGTH(blockedPortList);

#ifndef NDEBUG
    // The port list must be sorted for binary_search to work.
    static bool checkedPortList = false;
    if (!checkedPortList) {
        for (const unsigned short* p = blockedPortList; p != blockedPortListEnd - 1; ++p)
            ASSERT(*p < *(p + 1));
        checkedPortList = true;
    }
#endif

    // If the port is not in the blocked port list, allow it.
    if (!binary_search(blockedPortList, blockedPortListEnd, port.value()))
        return true;

    // Allow ports 21 and 22 for FTP URLs, as Mozilla does.
    if ((port.value() == 21 || port.value() == 22) && url.protocolIs("ftp"))
        return true;

    // Allow any port number in a file KURL, since the port number is ignored.
    if (url.protocolIs("file"))
        return true;

#if PLATFORM(BLACKBERRY)
    if (url.protocolIs("local"))
        return true;
#endif

    return false;
}

String mimeTypeFromDataURL(const String& dataURL)
{
    ASSERT(protocolIsInternal(dataURL, "data"));

    // FIXME: What's the right behavior when the KURL has a comma first, but a semicolon later?
    // Currently this code will break at the semicolon in that case; should add a test.
    auto index = dataURL.find(';', 5);
    if (index == notFound)
        index = dataURL.find(',', 5);
    if (index == notFound) {
        // FIXME: There was an old comment here that made it sound like this should be returning text/plain.
        // But we have been returning empty string here for some time, so not changing its behavior at this time.
        return emptyString();
    }
    if (index == 5)
        return ASCIILiteral("text/plain");
    ASSERT(index >= 5);
    return dataURL.substring(5, index - 5).convertToASCIILowercase();
}

String KURL::stringCenterEllipsizedToLength(unsigned length) const
{
    if (string().length() <= length)
        return string();

    return makeString(string().left(length / 2 - 1), "...", string().right(length / 2 - 2));
}

KURL KURL::fileURLWithFileSystemPath(StringView path)
{
    return KURL(KURL(), makeString(
        "file://",
        path.startsWith('/') ? "" : "/",
        escapePathWithoutCopying(path)
    ));
}

StringView KURL::queryWithLeadingQuestionMark() const
{
    if (m_queryEnd <= m_pathEnd)
        return StringView();

    return StringView(m_string).substring(m_pathEnd, m_queryEnd - m_pathEnd);
}

StringView KURL::fragmentIdentifierWithLeadingNumberSign() const
{
    if (!m_isValid || m_string.length() <= m_queryEnd)
        return StringView();

    return StringView(m_string).substring(m_queryEnd);
}

bool KURL::isAboutBlank() const
{
    return protocolIsAbout() && path() == "blank";
}

bool KURL::isAboutSrcDoc() const
{
    return protocolIsAbout() && path() == "srcdoc";
}

#if !PLATFORM(COCOA) && !USE(SOUP)

static bool isIPv4Address(StringView string)
{
    auto count = 0;

    for (const auto octet : string.splitAllowingEmptyEntries('.')) {
        if (count >= 4)
            return false;

        const auto length = octet.length();
        if (!length || length > 3)
            return false;

        auto value = 0;
        for (auto i = 0u; i < length; ++i) {
            const auto digit = octet[i];

            // Prohibit leading zeroes.
            if (digit > '9' || digit < (!i && length > 1 ? '1' : '0'))
                return false;

            value = 10 * value + (digit - '0');
        }

        if (value > 255)
            return false;

        count++;
    }

    return (count == 4);
}

static bool isIPv6Address(StringView string)
{
    enum SkipState { None, WillSkip, Skipping, Skipped, Final };
    auto skipState = None;
    auto count = 0;

    for (const auto hextet : string.splitAllowingEmptyEntries(':')) {
        if (count >= 8 || skipState == Final)
            return false;

        const auto length = hextet.length();
        if (!length) {
            // :: may be used anywhere to skip 1 to 8 hextets, but only once.
            if (skipState == Skipped)
                return false;

            if (skipState == None)
                skipState = !count ? WillSkip : Skipping;
            else if (skipState == WillSkip)
                skipState = Skipping;
            else
                skipState = Final;
            continue;
        }

        if (skipState == WillSkip)
            return false;

        if (skipState == Skipping)
            skipState = Skipped;

        if (length > 4) {
            // An IPv4 address may be used in place of the final two hextets.
            if ((skipState == None && count != 6) || (skipState == Skipped && count >= 6) || !isIPv4Address(hextet))
                return false;

            skipState = Final;
            continue;
        }

        for (const auto codeUnit : hextet.codeUnits()) {
            // IPv6 allows leading zeroes.
            if (!isASCIIHexDigit(codeUnit))
                return false;
        }

        count++;
    }

    return (count == 8 && skipState == None) || skipState == Skipped || skipState == Final;
}

bool KURL::hostIsIPAddress(StringView host)
{
    return host.contains(':') ? isIPv6Address(host) : isIPv4Address(host);
}

#endif

Vector<std::pair<String, String>> differingQueryParameters(const KURL& firstURL, const KURL& secondURL)
{
    auto firstQueryParameters = URLParser::parseURLEncodedForm(firstURL.query());
    auto secondQueryParameters = URLParser::parseURLEncodedForm(secondURL.query());

    if (firstQueryParameters.isEmpty())
        return secondQueryParameters;

    if (secondQueryParameters.isEmpty())
        return firstQueryParameters;

    auto compare = [] (const std::pair<String, String>& a, const std::pair<String, String>& b) {
        if (int result = codePointCompare(a.first, b.first))
            return result;
        return codePointCompare(a.second, b.second);

    };
    auto comparesLessThan = [compare] (const std::pair<String, String>& a, const std::pair<String, String>& b) {
        return compare(a, b) < 0;
    };

    std::sort(firstQueryParameters.begin(), firstQueryParameters.end(), comparesLessThan);
    std::sort(secondQueryParameters.begin(), secondQueryParameters.end(), comparesLessThan);
    size_t totalFirstQueryParameters = firstQueryParameters.size();
    size_t totalSecondQueryParameters = secondQueryParameters.size();
    size_t indexInFirstQueryParameters = 0;
    size_t indexInSecondQueryParameters = 0;
    Vector<std::pair<String, String>> differingQueryParameters;
    while (indexInFirstQueryParameters < totalFirstQueryParameters && indexInSecondQueryParameters < totalSecondQueryParameters) {
        int comparison = compare(firstQueryParameters[indexInFirstQueryParameters], secondQueryParameters[indexInSecondQueryParameters]);
        if (comparison < 0) {
            differingQueryParameters.append(firstQueryParameters[indexInFirstQueryParameters]);
            indexInFirstQueryParameters++;
        } else if (comparison > 0) {
            differingQueryParameters.append(secondQueryParameters[indexInSecondQueryParameters]);
            indexInSecondQueryParameters++;
        } else {
            indexInFirstQueryParameters++;
            indexInSecondQueryParameters++;
        }
    }

    while (indexInFirstQueryParameters < totalFirstQueryParameters) {
        differingQueryParameters.append(firstQueryParameters[indexInFirstQueryParameters]);
        indexInFirstQueryParameters++;
    }

    while (indexInSecondQueryParameters < totalSecondQueryParameters) {
        differingQueryParameters.append(secondQueryParameters[indexInSecondQueryParameters]);
        indexInSecondQueryParameters++;
    }

    return differingQueryParameters;
}

static StringView substringIgnoringQueryAndFragments(const KURL& url)
{
    if (!url.isValid())
        return StringView(url.string());

    return StringView(url.string()).left(url.pathEnd());
}

bool isEqualIgnoringQueryAndFragments(const KURL& a, const KURL& b)
{
    return substringIgnoringQueryAndFragments(a) == substringIgnoringQueryAndFragments(b);
}

void removeQueryParameters(KURL& url, const HashSet<String>& keysToRemove)
{
    if (keysToRemove.isEmpty())
        return;

    StringBuilder queryWithoutRemovalKeys;
    for (auto& parameter : URLParser::parseURLEncodedForm(url.query())) {
        if (!keysToRemove.contains(parameter.first)) {
            queryWithoutRemovalKeys.append(queryWithoutRemovalKeys.isEmpty() ? "" : "&");
            queryWithoutRemovalKeys.append(parameter.first);
            queryWithoutRemovalKeys.append('=');
            queryWithoutRemovalKeys.append(parameter.second);
        }
    }

    url.setQuery(queryWithoutRemovalKeys.toString());
}

}
