/** @file
 * @brief URL decoding as described by RFC3986.
 */
/* Copyright (C) 2011,2012,2015,2022 Olly Betts
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

#ifndef OMEGA_INCLUDED_URLDECODE_H
#define OMEGA_INCLUDED_URLDECODE_H

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <string>
#include "stringutils.h"

struct CGIParameterHandler {
    void operator()(const std::string&, const std::string&) const;
};

template<typename I>
inline void
url_decode(const CGIParameterHandler & handle_parameter, I begin, I end)
{
    bool seen_equals = false;
    std::string var, val;
    while (begin != end) {
        unsigned char ch = *begin;
        ++begin;
process_ch:
        if (ch == '&') {
            if (!seen_equals)
                swap(var, val);
            if (!var.empty())
                handle_parameter(var, val);
            var.resize(0);
            val.resize(0);
            seen_equals = false;
            continue;
        }

        switch (ch) {
            case '%': {
                if (begin == end)
                    break;
                unsigned char hex1 = *begin;
                ++begin;
                if (begin == end || !C_isxdigit(hex1)) {
                    val += ch;
                    ch = hex1;
                    if (begin == end)
                        break;
                    goto process_ch;
                }
                unsigned char hex2 = *begin;
                ++begin;
                if (!C_isxdigit(hex2)) {
                    val += ch;
                    val += hex1;
                    ch = hex2;
                    if (begin == end)
                        break;
                    goto process_ch;
                }
                ch = hex_decode(hex1, hex2);
                break;
            }
            case '+':
                ch = ' ';
                break;
            case '=':
                if (seen_equals)
                    break;
                seen_equals = true;
                swap(var, val);
                continue;
        }
        val += ch;
    }
    if (!seen_equals)
        swap(var, val);
    if (!var.empty())
        handle_parameter(var, val);
}

class CStringItor {
    const char* p = NULL;

    void operator++(int);

  public:
    CStringItor() { }

    explicit CStringItor(const char * p_) : p(p_) {
        if (!*p) p = NULL;
    }

    unsigned char operator*() const { return *p; }

    CStringItor & operator++() {
        if (!*++p) p = NULL;
        return *this;
    }

    friend bool operator==(const CStringItor& a, const CStringItor& b);
    friend bool operator!=(const CStringItor& a, const CStringItor& b);
};

inline bool
operator==(const CStringItor& a, const CStringItor& b)
{
    return a.p == b.p;
}

inline bool
operator!=(const CStringItor& a, const CStringItor& b)
{
    return !(a == b);
}

class StdinItor {
    size_t count;

    mutable int current = EOF;

    void operator++(int);

  public:
    StdinItor() { }

    explicit StdinItor(size_t count_) : count(count_), current(256) { }

    unsigned char operator*() const {
        if (current == 256)
            current = std::getchar();
        return current;
    }

    StdinItor & operator++() {
        if (count--)
            current = std::getchar();
        else
            current = EOF;
        return *this;
    }

    friend bool operator==(const StdinItor& a, const StdinItor& b);
    friend bool operator!=(const StdinItor& a, const StdinItor& b);
};

inline bool
operator==(const StdinItor& a, const StdinItor& b)
{
    return a.current == b.current;
}

inline bool
operator!=(const StdinItor& a, const StdinItor& b)
{
    return !(a == b);
}

// First group is RFC3986 reserved "gen-delims", except []@: (which are safe
// to decode if they occur after the "authority".
//
// Second group is RFC3986 reserved "sub-delims", except !$'()*,; (which are
// actually safe to decode in practice) and &+= (which are OK to decode if they
// aren't in the "query" part).
//
// We also need to leave an encoded "%" alone.  We should probably leave an
// encoded "/" alone too (though we shouldn't encounter one in a database
// created by omindex, unless it was in the base URL specified by the user).
//
// This prettifying is aimed at URLs produced by omindex, so we don't currently
// try to decode the query or fragment parts of the URL at all.  We can probably
// safely decode the query in a similar way, but also leaving &+= alone.

enum {
    // Always unsafe.
    UNSAFE,
    // Always safe.
    OK,
    // Always safe (and 8, 9, a, b, A or B).
    OK89AB,
    // Safe after a '/'.
    INPATH,
    // Start of a 2 byte UTF-8 sequence.
    SEQ2,
    // Start of a 3 byte UTF-8 sequence.
    SEQ3,
    // Start of a 4 byte UTF-8 sequence.
    SEQ4
};

static const char url_chars[256] = {
    // 0x00-0x07
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x08-0x0f
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x10-0x17
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x18-0x1f
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // ' '      !       "       #       $       %       &       '
    OK,         OK,     OK,     UNSAFE, OK,     UNSAFE, OK,     OK,
    // (        )       *       +       ,       -       .       /
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     UNSAFE,
    // 0        1       2       3       4       5       6       7
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     OK,
    // 8        9       :       ;       <       =       >       ?
    OK89AB,     OK89AB, INPATH, OK,     OK,     OK,     OK,     UNSAFE,
    // @        A       B       C       D       E       F       G
    INPATH,     OK89AB, OK89AB, OK,     OK,     OK,     OK,     OK,
    // H        I       J       K       L       M       N       O
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     OK,
    // P        Q       R       S       T       U       V       W
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     OK,
    // X        Y       Z       [       \       ]       ^       _
    OK,         OK,     OK,     INPATH, OK,     INPATH, OK,     OK,
    // `        a       b       c       d       e       f       g
    OK,         OK89AB, OK89AB, OK,     OK,     OK,     OK,     OK,
    // h        i       j       k       l       m       n       o
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     OK,
    // p        q       r       s       t       u       v       w
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     OK,
    // x        y       z       {       |       }       ~       0x7f
    OK,         OK,     OK,     OK,     OK,     OK,     OK,     UNSAFE,
    // 0x80     0x81    0x82    0x83    0x84    0x85    0x86    0x87
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x88     0x89    0x8a    0x8b    0x8c    0x8d    0x8e    0x8f
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x90     0x91    0x92    0x93    0x94    0x95    0x96    0x97
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0x98     0x99    0x9a    0x9b    0x9c    0x9d    0x9e    0x9f
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0xa0     0xa1    0xa2    0xa3    0xa4    0xa5    0xa6    0xa7
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0xa8     0xa9    0xaa    0xab    0xac    0xad    0xae    0xaf
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0xb0     0xb1    0xb2    0xb3    0xb4    0xb5    0xb6    0xb7
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0xb8     0xb9    0xba    0xbb    0xbc    0xbd    0xbe    0xbf
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE,
    // 0xc0     0xc1    0xc2    0xc3    0xc4    0xc5    0xc6    0xc7
    UNSAFE,     UNSAFE, SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,
    // 0xc8     0xc9    0xca    0xcb    0xcc    0xcd    0xce    0xcf
    SEQ2,       SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,
    // 0xd0     0xd1    0xd2    0xd3    0xd4    0xd5    0xd6    0xd7
    SEQ2,       SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,
    // 0xd8     0xd9    0xda    0xdb    0xdc    0xdd    0xde    0xdf
    SEQ2,       SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,   SEQ2,
    // 0xe0     0xe1    0xe2    0xe3    0xe4    0xe5    0xe6    0xe7
    SEQ3,       SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,
    // 0xe8     0xe9    0xea    0xeb    0xec    0xed    0xee    0xef
    SEQ3,       SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,   SEQ3,
    // 0xf0     0xf1    0xf2    0xf3    0xf4    0xf5    0xf6    0xf7
    SEQ4,       SEQ4,   SEQ4,   SEQ4,   SEQ4,   UNSAFE, UNSAFE, UNSAFE,
    // 0xf8     0xf9    0xfa    0xfb    0xfc    0xfd    0xfe    0xff
    UNSAFE,     UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE, UNSAFE
};

// Test if the 3 characters of s from offset i are '%', one of [89abAB]
// and a hex digit.
inline bool
encoded_ucont(const std::string & s, size_t i)
{
    return s[i] == '%' &&
        url_chars[static_cast<unsigned char>(s[i + 1])] == OK89AB &&
        C_isxdigit(s[i + 2]);
}

/** Prettify a URL.
 *
 *  Undo RFC3986 escaping which doesn't affect semantics in practice, to make
 *  a prettier version of a URL to show the user, but which should still work
 *  if copied and pasted.
 */
inline void
url_prettify(std::string & url)
{
    size_t pcent = url.find('%');
    // Fast path for URLs without a '%' in.
    if (pcent == std::string::npos)
        return;

    if (url.size() < 3)
        return;

    // Don't try to decode the query or fragment, and don't try to decode if
    // there aren't 2 characters after the '%'.
    size_t pretty_limit = std::min(url.find_first_of("?#"), url.size() - 2);
    if (pcent >= pretty_limit)
        return;

    size_t slash = std::string::npos;
    size_t start = 0;
    std::string in;
    swap(in, url);
    url.reserve(in.size());
    while (true) {
        // We've checked there are at least two bytes after the '%' already.
        if (C_isxdigit(in[pcent + 1]) && C_isxdigit(in[pcent + 2])) {
            unsigned char ch = hex_decode(in[pcent + 1], in[pcent + 2]);
            bool safe = true;
            switch (url_chars[ch]) {
                case UNSAFE:
                    safe = false;
                    break;
                case SEQ2:
                    if (in.size() - (pcent + 2) < 3 ||
                        !encoded_ucont(in, pcent + 3)) {
                        safe = false;
                        break;
                    }
                    url.append(in, start, pcent - start);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    start = pcent;
                    break;
                case SEQ3:
                    if (in.size() - (pcent + 2) < 3 * 2 ||
                        !encoded_ucont(in, pcent + 3) ||
                        !encoded_ucont(in, pcent + 6) ||
                        (ch == 0xe0 && in[pcent + 4] <= '9')) {
                        safe = false;
                        break;
                    }
                    url.append(in, start, pcent - start);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    start = pcent;
                    break;
                case SEQ4:
                    if (in.size() - (pcent + 2) < 3 * 3 ||
                        !encoded_ucont(in, pcent + 3) ||
                        !encoded_ucont(in, pcent + 6) ||
                        !encoded_ucont(in, pcent + 9) ||
                        (ch == 0xf0 && in[pcent + 4] == '8') ||
                        (ch == 0xf4 && in[pcent + 4] >= '9')) {
                        safe = false;
                        break;
                    }
                    url.append(in, start, pcent - start);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    url += char(ch);
                    pcent += 3;
                    ch = hex_decode(in[pcent + 1], in[pcent + 2]);
                    start = pcent;
                    break;
                case INPATH:
                    // ':' is safe to decode if there is a single '/' earlier in
                    // the URL.
                    if (slash == std::string::npos) {
                        // Lazily set slash to the position of the first single '/'.
                        const char * d = in.data();
                        slash = 0;
                        while (true) {
                            const void* s = std::memchr(d + slash, '/',
                                                        pretty_limit - slash);
                            if (s == NULL) {
                                slash = in.size();
                                break;
                            }
                            slash = reinterpret_cast<const char *>(s) - d;
                            if (slash == in.size() - 1 || d[slash + 1] != '/')
                                break;
                            ++slash;
                            while (++slash < in.size() - 1 && d[slash] == '/') { }
                        }
                    }
                    safe = (pcent > slash);
                    break;
            }

            if (safe) {
                url.append(in, start, pcent - start);
                url += char(ch);
                pcent += 3;
                start = pcent;
            } else {
                pcent += 3;
            }
        } else {
            ++pcent;
        }
        pcent = in.find('%', pcent);

        if (pcent >= pretty_limit) {
            url.append(in, start, std::string::npos);
            return;
        }
    }
}

#endif // OMEGA_INCLUDED_URLDECODE_H
