// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "Waf2Util.h"

#include "debug.h"
#include <map>
#include <string>
#include <sstream>
#include <vector>
#include <openssl/aes.h>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <sys/stat.h>
#include <stdio.h>
#include <locale.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "CidrMatch.h"
#include "debug.h"
#include "config.h"
#include "generic_rulebase/rulebase_config.h"
#include "user_identifiers_config.h"
#include "Waf2Regex.h"
#include "ParserBinaryFile.h"
#include "ParserKnownBenignSkipper.h"

using boost::algorithm::to_lower_copy;
using namespace std;

USE_DEBUG_FLAG(D_WAAP);
USE_DEBUG_FLAG(D_WAAP_EVASIONS);
USE_DEBUG_FLAG(D_WAAP_BASE64);
USE_DEBUG_FLAG(D_WAAP_JSON);

#define MIN_HEX_LENGTH 6
#define charToDigit(c) (c - '0')

// Base64 decoding lookup table constants
#define BASE64_INVALID -1
#define BASE64_PADDING -2

// Base64 lookup table for optimized decoding
static int base64_table[256];
static bool base64_table_initialized = false;

// Initialize the base64 lookup table for optimized decoding
static void initialize_base64_table()
{
    if (base64_table_initialized) {
        return;
    }

    // Initialize all entries to invalid
    for (int i = 0; i < 256; i++) {
        base64_table[i] = BASE64_INVALID;
    }

    // Set valid base64 characters (A-Z = 0-25)
    for (int i = 0; i < 26; i++) {
        base64_table['A' + i] = i;
    }

    // Set valid base64 characters (a-z = 26-51)
    for (int i = 0; i < 26; i++) {
        base64_table['a' + i] = 26 + i;
    }

    // Set valid base64 characters (0-9 = 52-61)
    for (int i = 0; i < 10; i++) {
        base64_table['0' + i] = 52 + i;
    }

    // Set special base64 characters
    base64_table['+'] = 62;
    base64_table['/'] = 63;
    base64_table['='] = BASE64_PADDING;

    base64_table_initialized = true;
}

// See https://dev.w3.org/html5/html-author/charref
const  struct HtmlEntity g_htmlEntities[] =
{
    {"Tab;", 0x0009},
    {"NewLine;", 0x000A},
    {"nbsp;", 0x00A0},
    {"NonBreakingSpace;", 0x00A0},
    {"excl;", 0x0021},
    {"num;", 0x0023},
    {"dollar;", 0x0024},
    {"percnt;", 0x0025},
    {"lpar;", 0x0028},
    {"rpar;", 0x0029},
    {"ast;", 0x002A},
    {"midast;", 0x002A},
    {"plus;", 0x002B},
    {"comma;", 0x002C},
    {"period;", 0x002E},
    {"sol;", 0x002F},
    {"colon;", 0x003A},
    {"semi;", 0x003B},
    {"iexcl;", 0x00A1},
    {"cent;", 0x00A2},
    {"pound;", 0x00A3},
    {"curren;", 0x00A4},
    {"yen;", 0x00A5},
    {"brvbar;", 0x00A6},
    {"sect;", 0x00A7},
    {"uml;", 0x00A8},
    {"copy;", 0x00A9},
    {"ordf;", 0x00AA},
    {"laquo;", 0x00AB},
    {"not;", 0x00AC},
    {"shy;", 0x00AD},
    {"reg;", 0x00AE},
    {"macr;", 0x00AF},
    {"deg;", 0x00B0},
    {"plusmn;", 0x00B1},
    {"sup2;", 0x00B2},
    {"sup3;", 0x00B3},
    {"acute;", 0x00B4},
    {"micro;", 0x00B5},
    {"para;", 0x00B6},
    {"middot;", 0x00B7},
    {"cedil;", 0x00B8},
    {"sup1;", 0x00B9},
    {"ordm;", 0x00BA},
    {"raquo;", 0x00BB},
    {"frac14;", 0x00BC},
    {"frac12;", 0x00BD},
    {"frac34;", 0x00BE},
    {"iquest;", 0x00BF},
    {"Agrave;", 0x00C0},
    {"Aacute;", 0x00C1},
    {"Acirc;", 0x00C2},
    {"Atilde;", 0x00C3},
    {"Auml;", 0x00C4},
    {"Aring;", 0x00C5},
    {"AElig;", 0x00C6},
    {"Ccedil;", 0x00C7},
    {"Egrave;", 0x00C8},
    {"Eacute;", 0x00C9},
    {"Ecirc;", 0x00CA},
    {"Euml;", 0x00CB},
    {"Igrave;", 0x00CC},
    {"Iacute;", 0x00CD},
    {"Icirc;", 0x00CE},
    {"Iuml;", 0x00CF},
    {"ETH;", 0x00D0},
    {"Ntilde;", 0x00D1},
    {"Ograve;", 0x00D2},
    {"Oacute;", 0x00D3},
    {"Ocirc;", 0x00D4},
    {"Otilde;", 0x00D5},
    {"Ouml;", 0x00D6},
    {"times;", 0x00D7},
    {"Oslash;", 0x00D8},
    {"Ugrave;", 0x00D9},
    {"Uacute;", 0x00DA},
    {"Ucirc;", 0x00DB},
    {"Uuml;", 0x00DC},
    {"Yacute;", 0x00DD},
    {"THORN;", 0x00DE},
    {"szlig;", 0x00DF},
    {"agrave;", 0x00E0},
    {"aacute;", 0x00E1},
    {"acirc;", 0x00E2},
    {"atilde;", 0x00E3},
    {"auml;", 0x00E4},
    {"aring;", 0x00E5},
    {"aelig;", 0x00E6},
    {"ccedil;", 0x00E7},
    {"egrave;", 0x00E8},
    {"eacute;", 0x00E9},
    {"ecirc;", 0x00EA},
    {"euml;", 0x00EB},
    {"igrave;", 0x00EC},
    {"iacute;", 0x00ED},
    {"icirc;", 0x00EE},
    {"iuml;", 0x00EF},
    {"eth;", 0x00F0},
    {"ntilde;", 0x00F1},
    {"ograve;", 0x00F2},
    {"oacute;", 0x00F3},
    {"ocirc;", 0x00F4},
    {"otilde;", 0x00F5},
    {"ouml;", 0x00F6},
    {"divide;", 0x00F7},
    {"oslash;", 0x00F8},
    {"ugrave;", 0x00F9},
    {"uacute;", 0x00FA},
    {"ucirc;", 0x00FB},
    {"uuml;", 0x00FC},
    {"yacute;", 0x00FD},
    {"thorn;", 0x00FE},
    {"yuml;", 0x00FF},
    {"quot;", 0x0022},
    {"amp;", 0x0026},
    {"lt;", 0x003C},
    {"LT;", 0x003C},
    {"equals;", 0x003D},
    {"gt;", 0x003E},
    {"GT;", 0x003E},
    {"quest;", 0x003F},
    {"commat;", 0x0040},
    {"lsqb;", 0x005B},
    {"lback;", 0x005B},
    {"bsol;", 0x005C},
    {"rsqb;", 0x005D},
    {"rbrack;", 0x005D},
    {"Hat;", 0x005E},
    {"lowbar;", 0x005F},
    {"grave;", 0x0060},
    {"DiacriticalGrave;", 0x0060},
    {"lcub;", 0x007B},
    {"lbrace;", 0x007B},
    {"verbar;", 0x007C},
    {"vert;", 0x007C},
    {"VerticalLine;", 0x007C},
    {"rcub;", 0x007D},
    {"rbrace;", 0x007D},
    {"apos;", 0x0027},
    {"OElig;", 0x0152},
    {"oelig;", 0x0153},
    {"Scaron;", 0x0160},
    {"scaron;", 0x0161},
    {"Yuml;", 0x0178},
    {"circ;", 0x02C6},
    {"tilde;", 0x02DC},
    {"ensp;", 0x2002},
    {"emsp;", 0x2003},
    {"emsp13;", 0x2004},
    {"emsp14;", 0x2005},
    {"numsp;", 0x2007},
    {"puncsp;", 0x2008},
    {"thinsp;", 0x2009},
    {"ThinSpace;", 0x2009},
    {"hairsp;", 0x200A},
    {"VeryThinSpace;", 0x200A},
    {"ZeroWidthSpace;", 0x200B},
    {"NegativeVeryThinSpace;", 0x200B},
    {"NegativeThinSpace;", 0x200B},
    {"NegativeMediumSpace;", 0x200B},
    {"NegativeThickSpace;", 0x200B},
    {"zwnj;", 0x200C},
    {"zwj;", 0x200D},
    {"lrm;", 0x200E},
    {"rlm;", 0x200F},
    {"hyphen;", 0x2010},
    {"dash;", 0x2010},
    {"ndash;", 0x2013},
    {"mdash;", 0x2014},
    {"horbar;", 0x2015},
    {"Verbar;", 0x2016},
    {"Vert;", 0x2016},
    {"lsquo;", 0x2018},
    {"OpenCurlyQuote;", 0x2018},
    {"rsquo;", 0x2019},
    {"rsquor;", 0x2019},
    {"CloseCurlyQuote;", 0x2019},
    {"lsquor;", 0x201A},
    {"sbquo;", 0x201A},
    {"ldquo;", 0x201C},
    {"OpenCurlyDoubleQuote;", 0x201C},
    {"rdquo;", 0x201D},
    {"rdquor;", 0x201D},
    {"CloseCurlyDoubleQuote;", 0x201D},
    {"ldquor;", 0x201E},
    {"bdquo;", 0x201E},
    {"dagger;", 0x2020},
    {"Dagger;", 0x2021},
    {"permil;", 0x2030},
    {"lsaquo;", 0x2039},
    {"rsaquo;", 0x203A},
    {"euro;", 0x20AC},
    {"fnof;", 0x0192},
    {"Alpha;", 0x0391},
    {"Beta;", 0x0392},
    {"Gamma;", 0x0393},
    {"Delta;", 0x0394},
    {"Epsilon;", 0x0395},
    {"Zeta;", 0x0396},
    {"Eta;", 0x0397},
    {"Theta;", 0x0398},
    {"Iota;", 0x0399},
    {"Kappa;", 0x039A},
    {"Lambda;", 0x039B},
    {"Mu;", 0x039C},
    {"Nu;", 0x039D},
    {"Xi;", 0x039E},
    {"Omicron;", 0x039F},
    {"Pi;", 0x03A0},
    {"Rho;", 0x03A1},
    {"Sigma;", 0x03A3},
    {"Tau;", 0x03A4},
    {"Upsilon;", 0x03A5},
    {"Phi;", 0x03A6},
    {"Chi;", 0x03A7},
    {"Psi;", 0x03A8},
    {"Omega;", 0x03A9},
    {"alpha;", 0x03B1},
    {"beta;", 0x03B2},
    {"gamma;", 0x03B3},
    {"delta;", 0x03B4},
    {"epsilon;", 0x03B5},
    {"zeta;", 0x03B6},
    {"eta;", 0x03B7},
    {"theta;", 0x03B8},
    {"iota;", 0x03B9},
    {"kappa;", 0x03BA},
    {"lambda;", 0x03BB},
    {"mu;", 0x03BC},
    {"nu;", 0x03BD},
    {"xi;", 0x03BE},
    {"omicron;", 0x03BF},
    {"pi;", 0x03C0},
    {"rho;", 0x03C1},
    {"sigmaf;", 0x03C2},
    {"sigma;", 0x03C3},
    {"tau;", 0x03C4},
    {"upsilon;", 0x03C5},
    {"phi;", 0x03C6},
    {"chi;", 0x03C7},
    {"psi;", 0x03C8},
    {"omega;", 0x03C9},
    {"thetasym;", 0x03D1},
    {"upsih;", 0x03D2},
    {"piv;", 0x03D6},
    {"bull;", 0x2022},
    {"hellip;", 0x2026},
    {"prime;", 0x2032},
    {"Prime;", 0x2033},
    {"oline;", 0x203E},
    {"frasl;", 0x2044},
    {"MediumSpace;", 0x205F},
    {"NoBreak;", 0x2060},
    {"ApplyFunction;", 2061},
    {"af;", 2061},
    {"it;", 0x2062},
    {"InvisibleTimes;", 0x2062},
    {"ic;", 0x2063},
    {"InvisibleComma;", 0x2063},
    {"weierp;", 0x2118},
    {"image;", 0x2111},
    {"real;", 0x211C},
    {"trade;", 0x2122},
    {"alefsym;", 0x2135},
    {"larr;", 0x2190},
    {"uarr;", 0x2191},
    {"rarr;", 0x2192},
    {"darr;", 0x2193},
    {"harr;", 0x2194},
    {"crarr;", 0x21B5},
    {"lArr;", 0x21D0},
    {"uArr;", 0x21D1},
    {"rArr;", 0x21D2},
    {"dArr;", 0x21D3},
    {"hArr;", 0x21D4},
    {"forall;", 0x2200},
    {"part;", 0x2202},
    {"exist;", 0x2203},
    {"empty;", 0x2205},
    {"nabla;", 0x2207},
    {"isin;", 0x2208},
    {"notin;", 0x2209},
    {"ni;", 0x220B},
    {"prod;", 0x220F},
    {"sum;", 0x2211},
    {"minus;", 0x2212},
    {"lowast;", 0x2217},
    {"radic;", 0x221A},
    {"prop;", 0x221D},
    {"infin;", 0x221E},
    {"ang;", 0x2220},
    {"and;", 0x2227},
    {"or;", 0x2228},
    {"cap;", 0x2229},
    {"cup;", 0x222A},
    {"int;", 0x222B},
    {"there4;", 0x2234},
    {"sim;", 0x223C},
    {"cong;", 0x2245},
    {"asymp;", 0x2248},
    {"ne;", 0x2260},
    {"equiv;", 0x2261},
    {"le;", 0x2264},
    {"ge;", 0x2265},
    {"sub;", 0x2282},
    {"sup;", 0x2283},
    {"nsub;", 0x2284},
    {"sube;", 0x2286},
    {"supe;", 0x2287},
    {"oplus;", 0x2295},
    {"otimes;", 0x2297},
    {"perp;", 0x22A5},
    {"sdot;", 0x22C5},
    {"lceil;", 0x2308},
    {"rceil;", 0x2309},
    {"lfloor;", 0x230A},
    {"rfloor;", 0x230B},
    {"lang;", 0x2329},
    {"rang;", 0x232A},
    {"loz;", 0x25CA},
    {"spades;", 0x2660},
    {"clubs;", 0x2663},
    {"hearts;", 0x2665},
    {"diams;", 0x2666}
};

const size_t g_htmlEntitiesCount = sizeof(g_htmlEntities) / sizeof(g_htmlEntities[0]);

const char* g_htmlTags[] = {
    "a",
    "abbr",
    "acronym",
    "address",
    "applet",
    "embed",
    "object",
    "area",
    "article",
    "aside",
    "audio",
    "b",
    "base",
    "basefont",
    "bdi",
    "bdo",
    "big",
    "blockquote",
    "body",
    "br",
    "button",
    "canvas",
    "caption",
    "center",
    "cite",
    "code",
    "col",
    "colgroup",
    "datalist",
    "dd",
    "del",
    "details",
    "dfn",
    "dialog",
    "dir",
    "ul",
    "div",
    "dl",
    "dt",
    "em",
    "fieldset",
    "figcaption",
    "figure",
    "font",
    "footer",
    "form",
    "frame",
    "frameset",
    "h1",
    "h6",
    "head",
    "header",
    "hr",
    "html",
    "i",
    "iframe",
    "img",
    "input",
    "ins",
    "kbd",
    "keygen",
    "label",
    "legend",
    "li",
    "link",
    "main",
    "map",
    "mark",
    "menu",
    "menuitem",
    "meta",
    "meter",
    "nav",
    "noframes",
    "noscript",
    "ol",
    "optgroup",
    "option",
    "output",
    "p",
    "param",
    "pre",
    "progress",
    "q",
    "rp",
    "rt",
    "ruby",
    "s",
    "samp",
    "script",
    "section",
    "select",
    "small",
    "source",
    "video",
    "span",
    "strike",
    "strong",
    "style",
    "sub",
    "summary",
    "sup",
    "table",
    "tbody",
    "td",
    "textarea",
    "tfoot",
    "th",
    "thead",
    "time",
    "title",
    "tr",
    "track",
    "tt",
    "u",
    "var",
    "wbr",
    "event-source",
    "math",
    "svg",
    "h1",
    "h2",
    "h3",
    "h4",
    "h5",
    "h6"
};

static const string b64_prefix("base64,");
// Empirically calculated entropy threshold for base64 encoded data, value above it is considered as base64 encoded
static const double base64_entropy_threshold = 4.01;

const size_t g_htmlTagsCount = sizeof(g_htmlTags) / sizeof(g_htmlTags[0]);

bool startsWithHtmlTagName(const char* text) {
    for (size_t index = 0; index < g_htmlTagsCount; ++index) {
        // Return true if text starts with one of html tags
        if (my_stristarts_with(text, g_htmlTags[index])) {
            // starts with html tag, followed by space/tab/crlf character (see man isspace(),
            // or ends with '>' character.
            char termChar = text[strlen(g_htmlTags[index])];
            if (isspace(termChar) || termChar == '>' || termChar == '/') {
                return true;
            }
        }
    }

    return false;
}

string normalize_uri(const string& uri) {
    string result;
    string::const_iterator mark = uri.begin();
    bool isNumeric = false;

    string::const_iterator it = uri.begin();
    for (; it != uri.end() && *it != '?'; ++it) {
        if (*it == '/') {
            if (mark != it) {
                if (isNumeric) {
                    result += "_num";
                }
                else {
                    result.append(mark, it);
                }
            }

            result += "/";
            mark = it + 1;
            isNumeric = true;
            continue;
        }

        // reset isNumeric flag on first non-digit character in the path element string
        if (!isdigit(*it)) {
            isNumeric = false;
        }
    }

    // At this point, "it" points to where scanning stopped (can be end of uri string or the '?' character)
    // Append the rest of the string (or "_num" if last uri part was all numeric) - to the output.
    if (mark != it) {
        if (isNumeric) {
            result += "_num";
        }
        else {
            result.append(mark, it);
        }
    }

    return result;
}

string
normalize_param(const string& param)
{
    string result;
    string::const_iterator mark = param.begin();
    bool isNumeric = true;
    bool isHex = true;

    string::const_iterator it = param.begin();
    for (; it != param.end(); ++it) {
        if (!isalnum(*it)) {
            if (mark != it) {
                if (isNumeric || (isHex && it - mark >= MIN_HEX_LENGTH)) {
                    result += "_num";
                }
                else {
                    result.append(mark, it);
                }
            }

            result += *it;
            mark = it + 1;
            isNumeric = true;
            isHex = true;
            continue;
        }

        // reset isNumeric flag on first non-digit character in the path element string
        if (isHex && !isdigit(*it)) {
            if (!isHexDigit(*it)) {
                isHex = false;
            }
            isNumeric = false;
        }
    }

    // At this point, "it" points to where scanning stopped (can be end of uri string or the '?' character)
    // Append the rest of the string (or "_num" if last uri part was all numeric) - to the output.
    if (mark != it) {
        if (isNumeric || (isHex && it - mark >= MIN_HEX_LENGTH)) {
            result += "_num";
        }
        else {
            result.append(mark, it);
        }
    }

    return result;
}

void unescapeUnicode(string& text) {
    string::iterator it = text.begin();
    string::iterator result = it;
    char acc[16];   // accumulates characters we are parsing and do not want to copy directly.
                    // max len really possible is "\u00000000" + 1 char = 11 chars
    char* pAcc = NULL; // when non-NULL, points where to put next character inside acc buffer
    int digitsAnticipated = 0; // in state STATE_ESCAPE, how many hex digits we anticipate to be parsed
    uint32_t code = 0; // The Unicode codepoint value can't be larger than 32 bits
    char* p;
    // in state STATE_ESCAPE_X, how many non-zerohex digits discovered - to eliminate leading zeroes like \x000012
    int nonZeroHexCounter = 0;
    enum {
        STATE_COPY,
        STATE_FLUSH,
        STATE_ESCAPE,
        STATE_ESCAPE_U,
        STATE_ESCAPE_X
    } state = STATE_COPY;

    for (; it != text.end() && result <= it; ++it) {
        const char ch = *it;

        switch (state) {
        case STATE_FLUSH: {
            // flush any accumulated left-overs into output buffer
            if (pAcc) {
                for (p = acc; p < pAcc; p++) {
                    *result++ = *p;
                }
                pAcc = NULL; // clear the acc buffer after we flushed it
            }
            state = STATE_COPY;
            // fall-through
            //RB: why no break?
        }
        // fallthrough
        case STATE_COPY: {

            if (ch == '\\') {
                // start accumulating characters instead of copying them
                pAcc = acc;
                state = STATE_ESCAPE;
                break;
            }
            break;
        }
        case STATE_ESCAPE: {
            // decide which kind of escape
            if (ch == 'u') {
                digitsAnticipated = 4; // parse/skip 4 hex digits
                code = 0;
                state = STATE_ESCAPE_U;
            }
            else if (ch == 'U') {
                digitsAnticipated = 8; // parse/skip 8 hex digits
                code = 0;
                state = STATE_ESCAPE_U;
            }
            else if (ch == 'x') {
                digitsAnticipated = 1; // anticipate at least one HEX digit after \x
                code = 0;
                nonZeroHexCounter = 0;
                state = STATE_ESCAPE_X;
            }
            else {
                // this is invalid escape sequence: rollback and copy this character too
                state = STATE_FLUSH;
            }
            break;
        }
        case STATE_ESCAPE_U: {
            if (isHexDigit(ch)) {
                // accumulate code value
                code = (code << 4) + (isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10);
                digitsAnticipated--;

                if (digitsAnticipated == 0) {
                    // only output ASCII codes <= 127. "swallow" all unicode.
                    if (code <= 127) {
                        *result++ = (char)code;
                    }
                    else if (isSpecialUnicode(code)) {
                        *result++ = convertSpecialUnicode(code);
                    }

                    if (pAcc) {
                        pAcc = NULL; // throw away the accumulated source (escaped) sequencec.
                    }

                    // not STATE_COPY to avoid outputting current ch verbatim.
                    // FLUSH will output nothing because there's no ACC
                    state = STATE_FLUSH;
                    break;
                }

            }
            else {
                // invalid (non-hex) digit enountered
                state = STATE_FLUSH;
            }

            break;
        }
        case STATE_ESCAPE_X: {
            if (isHexDigit(ch)) {
                if ((nonZeroHexCounter) > 1) {
                    *result++ = (char)code;
                    if (pAcc) {
                        pAcc = NULL; // throw away the accumulated source (escaped) sequence.
                    }
                    state = STATE_COPY;
                } else {
                    code = (code << 4) + (isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10);
                    code &= 0xFF; // clamp the code value to two last digits
                    // Once at least one valid hex digit is here the sequence is considered
                    // valid and there's no need to accumulate it anymore.
                    if (pAcc) {
                        pAcc = NULL;
                    }

                    if (digitsAnticipated > 0) {
                        digitsAnticipated--;
                    }
                    if (code) {
                        nonZeroHexCounter++;
                    }
                }
            } else {
                // According to C standard, '\x' sequence must be followed by at least 1 valid hex digit
                if (digitsAnticipated > 0) {
                    // This is first character right after the '\x' sequence,
                    // and it is not a valid hex. This is bad sequence.
                    state = STATE_FLUSH;
                } else {
                    // We found non-hex character that terminates our \xhhhhh... sequence
                    *result++ = (char)code;
                    if (pAcc) {
                        pAcc = NULL; // throw away the accumulated source (escaped) sequence.
                    }

                    if (ch == '\\') {
                        // start accumulating characters instead of copying them
                        pAcc = acc;
                        state = STATE_ESCAPE;
                        break;
                    }

                    // STATE_COPY will cause current character (sequence terminator)
                    // to be output verbatim.
                    state = STATE_COPY;
                }
            }

            if (digitsAnticipated > 0) {
                digitsAnticipated--;
            }
            break;
        }
        }

        // Copy to output
        if (state == STATE_COPY) {
            *result++ = ch;
        }

        dbgAssertOpt(!pAcc || size_t(pAcc - acc) < sizeof(acc))
            << AlertInfo(AlertTeam::WAAP, "WAAP string unescape") <<
            "Buffer overflow detected";
        // Accumulate
        if (pAcc && size_t(pAcc - acc) < sizeof(acc)) {
            // Ensure we don't have buffer overflow
            *pAcc++ = ch;
        }
    }

    dbgTrace(D_WAAP) << " - LOOP FINISHED with state=" << state << "; digitsAnticipated=" <<
        digitsAnticipated << ", acc='" << string(acc, pAcc ? (int)(pAcc - acc) : 0) << "'";

    // Output code if we just finished decoding an escape sequence succesully and reached end of string
    if (state == STATE_ESCAPE_U && digitsAnticipated == 0 && result < text.end()) {
        // only output ASCII codes <= 127. "swallow" all unicode.
        if (code <= 127) {
            *result++ = (char)code;
        }
        else if (isSpecialUnicode(code)) {
            *result++ = convertSpecialUnicode(code);
        }

        if (pAcc) {
            pAcc = NULL; // throw away the accumulated source (escaped) sequencec.
        }
    }
    else if (state == STATE_ESCAPE_X) {
        if (isSpecialUnicode(code)) {
            *result++ = convertSpecialUnicode(code);
        }
        else if (digitsAnticipated == 0)
        {
            *result++ = (char)code;
        }
    }

    // flush any accumulated left-overs into output buffer
    if (pAcc) {
        for (p = acc; p < pAcc && result < text.end(); p++) {
            *result++ = *p;
        }
    }

    text.erase(result, text.end());
}

// Attempts to validate and decode utf7-encoded chunk.
// Returns "next" iterator to position where to continue parsing for next chunks, and
// fills the "decoded" string with decoded data.
// If failed, the "next" will be equal to passed "it", and empty string put in "decoded".
inline const string::const_iterator
decodeUTF7Chunk(string::const_iterator it, string::const_iterator end, string& decoded) {
    decoded.clear();
    unsigned char val = 0;
    uint32_t acc = 0;
    int acc_bits = 0; // how many bits are filled in acc
    string::const_iterator next = it;

    while (it != end) {
        unsigned char c = *it;

        if (c >= 'A' && c <= 'Z') {
            val = c - 'A';
        }
        else if (c >= 'a' && c <= 'z') {
            val = c - 'a' + 26;
        }
        else if (c >= '0' && c <= '9') {
            val = c - '0' + 52;
        }
        else if (c == '+') {
            val = 62;
        }
        else if (c == '/') {
            val = 63;
        }
        else if (c == '-') {
            // end of encoded sequence (the '-' itself must not be output)
            if (!decoded.empty()) {
                next = it;
                return next; // successfully decoded. Returns decoded data in "decoded" parameter
            }

            decoded.clear(); // discard partial data
            return next;
        }
        else {
            decoded.clear(); // discard partial data
            return next;
        }

        acc = acc << 6 | val;
        acc_bits += 6;

        if (acc_bits >= 16) { // we got 16 bits or more in the accumulator
            int code = (acc >> (acc_bits - 16)) & 0xFFFF;

            // Take into account still-printable Unicode characters, convert them back to ASCII
            if (isSpecialUnicode(code)) {
                code = convertSpecialUnicode(code);
            }

            // Stop and return empty if we hit at least one non-printable character
            if (!isprint(code) && code != 0) {
                decoded.clear(); // discard partial data
                return next;
            }

            decoded += (char)code;
            acc_bits -= 16;
            acc &= (1 - (1 << acc_bits)); // leave only acc_bits low bits in the acc, clear the rest.
        }

        it++;
    }

    decoded.clear(); // discard partial data
    return next;
}

string filterUTF7(const string& text) {
    string result;
    string decoded;
    decoded.reserve(8);
    result.reserve(text.length());

    for (string::const_iterator it = text.begin(); it != text.end(); ++it) {
        if (*it == '+') {
            if (it + 1 == text.end()) { // "+" at end of string
                result += *it;
            }
            else if (*(it + 1) == '-') {
                // '+-' combination is converted to single '+'
                result += '+';
                it++; // this skips the "-"
                if (it == text.end()) {
                    break;
                }
            }
            else {
                // attempt to decode chunk
                it = decodeUTF7Chunk(it + 1, text.end(), decoded);
                if (decoded.empty()) { // decoding failed
                    result += '+';
                    result += *it;
                }
                else { // decoding succeeded
                    result += decoded;
                }
            }
        }
        else {
            result += *it;
        }
    }

    return result;
}

//  Decides the status of a Base64 decoded string based on various parameters.
//  @param decoded The decoded string.
//  @param entropy The entropy of the original encoded string.
//  @param decoded_entropy The entropy of the decoded string.
//  @param spacer_count The number of spacer characters in the decoded string.
//  @param nonPrintableCharsCount The count of non-printable characters in the decoded string.
//  @param clear_on_error Flag indicating whether to clear the decoded string on error.
//  @param terminatorCharsSeen The number of terminator characters seen.
//  @param called_with_prefix Flag indicating if the function was called with a prefix.
//  @return The status of the Base64 decoding process.
//
//  Idea:
//  Check if input chunk should be replaced by decoded, suspected to be checked both as encoded and decoded
//  or cleaned as binary data. Additional case - define as not base64 encoded.
//  - in case decoded size less 5 - return invalid
//  - check entropy delta based on that base64 encoded data has higher entropy than decoded, usually delta = 0.25
//  - this check should rize suspect but cannot work vice versa
//  check if decoded chunk has more than 10% of non-printable characters - this is supect for binary data encoded
//   - if no suspect for binary data and entropy is suspected, check empiric conditions to decide if this binary data
//     or invalid decoding
//   - if suspect for binary data, first check is we have entropy suspection
//     - if entropy is suspected and chunk is short and it have more than 25% of nonprintables, return invalid
//       since this is not base64 encoded data
//     - if entropy is not suspected and chunk is short and it have more than 50% of nonprintables, return invalid
//       since this is not base64 encoded data
//     - if entropy is suspected and chunk size is between 64-1024, perform additional empiric test
//       This test will define if  returm value should be treated as suspected or as binary data(cleared)

base64_decode_status decideStatusBase64Decoded(
    string& decoded,
    double entropy,
    double decoded_entropy,
    size_t spacer_count,
    size_t nonPrintableCharsCount,
    bool clear_on_error,
    double terminatorCharsSeen,
    bool called_with_prefix
)
{
    base64_decode_status tmp_status = B64_DECODE_OK;
    if (entropy - decoded_entropy + terminatorCharsSeen < BASE64_ENTROPY_THRESHOLD_DELTA) {
        dbgTrace(D_WAAP_BASE64)
            << "The chunk is under suspect to be base64,"
            << "use dual processing because entropy delta is too low";
        tmp_status = B64_DECODE_SUSPECTED;
    }

    bool empiric_condition = false;
    if (decoded.size() >= 5) {
        if (spacer_count > 1) {
            nonPrintableCharsCount = nonPrintableCharsCount - spacer_count + 1;
        }
        dbgTrace(D_WAAP_BASE64)
                << "(before test for unprintables):  decoded.size="
                << decoded.size()
                << ", nonPrintableCharsCount="
                << nonPrintableCharsCount
                << ", clear_on_error="
                << clear_on_error
                << ", called_with_prefix="
                << called_with_prefix;
        if (nonPrintableCharsCount * 10 < decoded.size()) {
            dbgTrace(D_WAAP_BASE64)
                << "(decode/replace due  to small amount of nonprintables): will decide based on entropy values";
        } else { // more than 10% of non-printable characters
            dbgTrace(D_WAAP_BASE64) << "large amount of nonporintables";
            if (tmp_status == B64_DECODE_SUSPECTED) {
                // entropy - decoded_entropy + terminatorCharsSeen < 0.25
                if (decoded.size() < 16 &&  nonPrintableCharsCount * 4 > decoded.size())  {
                    decoded.clear();
                    return B64_DECODE_INVALID;
                }
                dbgTrace(D_WAAP_BASE64)
                    << "(large amount of nonporintables + entropy suspect), check emprirics because decoded."
                    << " terminatorCharsSeen="
                    << terminatorCharsSeen;
                    // empiric test based on investigation of real payloads
                empiric_condition = entropy < decoded_entropy
                    && entropy > BASE64_ENTROPY_BASE_THRESHOLD
                    && decoded_entropy > BASE64_ENTROPY_DECODED_THRESHOLD
                    && !called_with_prefix
                    && decoded.size() > BASE64_MIN_SIZE_LIMIT
                    && decoded.size() < BASE64_MAX_SIZE_LIMIT;
                if (!empiric_condition) {
                    if (clear_on_error) decoded.clear();
                    return B64_DECODE_SUSPECTED;
                } else {
                    if (clear_on_error) decoded.clear();
                    tmp_status = B64_DECODE_OK;
                }
            } else { // entropy - decoded_entropy + terminatorCharsSeen >= 0.25
                // one more empiric based on uT and real payloads
                if (decoded.size() < 16
                        && nonPrintableCharsCount * 2 > decoded.size()
                        && terminatorCharsSeen == 0) {
                    decoded.clear();
                    return B64_DECODE_INVALID;
                }
                dbgTrace(D_WAAP_BASE64)
                    << "(delete as binary content) because decoded. Return B64_DECODE_INCOMPLETE";
                if (clear_on_error) decoded.clear();
                return B64_DECODE_INCOMPLETE;
            }
        } // less than 10% of non-printable characters
        dbgTrace(D_WAAP_BASE64)
            << "After handling unprintables checking status";
        if (tmp_status == B64_DECODE_OK) {
            dbgTrace(D_WAAP_BASE64) <<  "replacing with decoded data, return B64_DECODE_OK";
            return B64_DECODE_OK;
        } else { // tmp_status == B64_DECODE_SUSPECTED, entropy - decoded_entropy + terminatorCharsSeen < 0.25
            dbgTrace(D_WAAP_BASE64) << "Suspected due to entropy, making empiric test";
            // and one more empiric test based on investigation of real payloads
            empiric_condition = entropy < decoded_entropy
                && entropy > BASE64_ENTROPY_BASE_THRESHOLD
                && decoded_entropy > BASE64_ENTROPY_DECODED_THRESHOLD
                && !called_with_prefix
                && decoded.size() > BASE64_MIN_SIZE_LIMIT
                && decoded.size() < BASE64_MAX_SIZE_LIMIT
                && nonPrintableCharsCount != 0;
            if (empiric_condition) {
                dbgTrace(D_WAAP_BASE64) << "Empiric test failed, non-base64 chunk, return B64_DECODE_INVALID";
                decoded.clear();
                return B64_DECODE_INVALID;
            }
            dbgTrace(D_WAAP_BASE64) << "Empiric test passed, return B64_DECODE_SUSPECTED";
            return B64_DECODE_SUSPECTED;
        }
        return B64_DECODE_OK; // successfully decoded. Returns decoded data in "decoded" parameter
    }

    // If decoded size is too small - leave the encoded value (return false)
    decoded.clear(); // discard partial data
    dbgTrace(D_WAAP_BASE64)
        << "(leave as-is) because decoded too small. decoded.size="
        << decoded.size();
    return B64_DECODE_INVALID;
}

//
// Base64 decoding with direct array lookup
// Performance optimization notes:
// - Replaced unordered_map lookups with direct array indexing
// - Improved cache locality with a simple 256-element array
// - Eliminated hash computation and collision handling overhead
// - One-time initialization of the lookup table
//
// The table maps each ASCII character to its base64 value:
// - For valid base64 characters (A-Z, a-z, 0-9, +, /): returns 0-63 value
// - For padding character ('='): returns BASE64_PADDING (-2)
// - For invalid characters: returns BASE64_INVALID (-1)
//

// Attempts to validate and decode base64-encoded chunk.
// Value is the full value inside which potential base64-encoded chunk was found,
// it and end point to start and end of that chunk.
// Returns true if succesful (and fills the "decoded" string with decoded data).
// Success criterias:
//  0. encoded sequence covers the whole value (doesn't have any characters around it)
//  1. encoded sequence consist of base64 alphabet (may end with zero, one or two '=' characters')
//  2. length of encoded sequence is exactly divisible by 4.
//  3. length of decoded is minimum 5 characters.
//  4. percent of non-printable characters (!isprint())
//     in decoded data is less than 10% (statistical garbage detection).
// Returns false above checks fail.
base64_decode_status decodeBase64Chunk(
        const string& value,
        string::const_iterator it,
        string::const_iterator end,
        string& decoded,
        bool clear_on_error,
        bool called_with_prefix)
{
    // Initialize the base64 lookup table on first call
    initialize_base64_table();

    decoded.clear();
    uint32_t acc = 0;
    int acc_bits = 0; // how many bits are filled in acc
    int terminatorCharsSeen = 0; // whether '=' character was seen, and how many of them.
    uint32_t nonPrintableCharsCount = 0;
    uint32_t spacer_count = 0;
    uint32_t length = end - it;

    dbgTrace(D_WAAP)
        << "value='"
        << value
        << "' match='"
        << string(it, end)
        << "' clear_on_error='"
        << clear_on_error
        << "' called_with_prefix='"
        << called_with_prefix
        << "'";
    string::const_iterator begin = it;

    // The encoded data length (without the "base64," prefix) should be exactly divisible by 4
    // len % 4 is not 0 i.e. this is not base64
        if ((end - it) % 4 == 1) {
            dbgTrace(D_WAAP_BASE64)
                << "(leave as-is) because encoded data length should not be <4*x + 1>.";
            return B64_DECODE_INVALID;
        }

        // Fixed arrays for character counting - Proposal 1 optimization
        uint32_t original_counts[256] = {0};     // Count for each ASCII character in original
        uint32_t decoded_counts[256] = {0};      // Count for each byte value in decoded

        while (it != end) {
            unsigned char c = *it;

            if (terminatorCharsSeen) {
                // terminator characters must all be '=', until end of match.
                if (c != '=') {
                    dbgTrace(D_WAAP_BASE64)
                    << "(leave as-is) because terminator characters must all be '=' until end of match.";
                    return B64_DECODE_INVALID;
                }

                // We should see 0, 1 or 2 (no more) terminator characters
                terminatorCharsSeen++;

                if (terminatorCharsSeen > 2) {
                    dbgTrace(D_WAAP_BASE64) << "(leave as-is) because terminatorCharsSeen > 2";
                    return B64_DECODE_INVALID;
                }

                // allow for more terminator characters
                it++;
                original_counts[static_cast<unsigned char>(c)]++;
                continue;
            }

            // Use lookup table for faster decoding
            int lookup_value = base64_table[static_cast<unsigned char>(c)];

            if (lookup_value == BASE64_PADDING) {
                // Start tracking terminator characters ('=')
                terminatorCharsSeen++;
                it++;
                original_counts[static_cast<unsigned char>(c)]++;
                continue;
            }
            else if (lookup_value == BASE64_INVALID) {
                dbgTrace(D_WAAP_BASE64)
                    << "(leave as-is) because of non-base64 character ('"
                    << c
                    << "', ASCII "
                    << (unsigned int)c
                    << ", offset "
                    << (it-begin)
                    << ")";
                return B64_DECODE_INVALID; // non-base64 character
            }

            unsigned char val = static_cast<unsigned char>(lookup_value);

            acc = (acc << 6) | val;
            acc_bits += 6;

            if (acc_bits >= 8) {
                int code = (acc >> (acc_bits - 8)) & 0xFF;
                // only leave low "acc_bits-8" bits, clear all higher bits
                uint32_t mask = ~(1 << (acc_bits - 8));
                acc &= mask;
                acc_bits -= 8;

                // Count non-printable characters seen
                if (!isprint(code) && (code != '\n') && (code != '\t')) {
                    nonPrintableCharsCount++;
                }
                if (code == '\r') {
                    spacer_count++;
                }

                decoded += (char)code;
                decoded_counts[static_cast<unsigned char>(code)]++;
            }

            it++;
            original_counts[static_cast<unsigned char>(c)]++;
        }

        // end of encoded sequence decoded.

        dbgTrace(D_WAAP_BASE64)
            << "decoding done: decoded.size="
            << decoded.size()
            << ", uncorrected nonPrintableCharsCount="
            << nonPrintableCharsCount
            << ", spacer_count = "
            << spacer_count
            << ", decoded size = "
            << decoded.size()
            << "; decoded='"
            << decoded << "'";

        double entropy = 0;
        double p = 0;
        double decoded_entropy = 0;

        // Calculate entropy from original character counts
        for (int i = 0; i < 256; i++) {
            if (original_counts[i] > 0) {
                p = static_cast<double>(original_counts[i]) / length;
                entropy -= p * std::log2(p);
            }
        }

        // Calculate entropy from decoded character counts
        for (int i = 0; i < 256; i++) {
            if (decoded_counts[i] > 0) {
                p = static_cast<double>(decoded_counts[i]) / decoded.size();
                decoded_entropy -= p * std::log2(p);
            }
        }
        dbgTrace(D_WAAP_BASE64)
            << "Base entropy = "
            << entropy
            << " Decoded_entropy = "
            << decoded_entropy
            << "length = "
            << length;

        base64_decode_status return_status = decideStatusBase64Decoded(
            decoded,
            entropy,
            decoded_entropy,
            spacer_count,
            nonPrintableCharsCount,
            clear_on_error,
            terminatorCharsSeen,
            called_with_prefix
        );

        dbgTrace(D_WAAP_BASE64)
            << "After decideStatusBase64Decoded return_status="
            << return_status;

        return return_status;

}

// Attempts to detect and validate base64 chunk.
// Value is the full value inside which potential base64-encoded chunk was found,
// it and end point to start and end of that chunk.
// Returns true if succesful (and fills the "decoded" string with decoded data).
// Success criterias:
//  0. encoded sequence covers the whole value (doesn't have any characters around it)
//  1. encoded sequence consist of base64 alphabet (may end with zero, one or two '=' characters')
//  2. length of encoded sequence is exactly divisible by 4.
//  3. length of decoded is minimum 5 characters.
//  4. percent of non-printable characters (!isprint())
//     in decoded data is less than 10% (statistical garbage detection).
// Returns false above checks fail.
bool
b64DecodeChunk(
    const string& value,
    string::const_iterator it,
    string::const_iterator end,
    string& decoded)
{

    dbgTrace(D_WAAP_BASE64) << "b64DecodeChunk: value='" << value << "' match='" << string(it, end) << "'";

    // skip "base64," prefix if the line is starting with it.
    unsigned int len = end - it;
    if (len >= b64_prefix.size() &&
        it[0] == 'b' && it[1] == 'a' && it[2] == 's' && it[3] ==
        'e' && it[4] == '6' && it[5] == '4' && it[6] == ',') {
        it = it + b64_prefix.size(); // skip the prefix
    }
    else {
        // If the base64 candidate match within value is surrounded by other dat
        // (doesn't cover the value fully) - ignore the match.
        // This will result in the match being scanned raw.
        // Note that this purposedly doesn't include matches starting with "base64,"
        // prefix: we do want those prefixed matches to be decoded!
        if (it != value.begin() || end != value.end()) {
            dbgTrace(D_WAAP_BASE64) << "b64DecodeChunk: (leave as-is) because match is surrounded by other data.";
            return false;
        }
    }
    base64_decode_status status = decodeBase64Chunk(value, it, end, decoded);
    dbgTrace(D_WAAP_BASE64) << "b64DecodeChunk: status = " << status;
    return status != B64_DECODE_INVALID;
}

vector<string> split(const string& s, char delim) {
    vector<string> elems;
    stringstream ss(s);
    string value;
    while (getline(ss, value, delim)) {
        elems.push_back(Waap::Util::trim(value));
    }
    return elems;
}

namespace Waap {
namespace Util {

#define B64_TRAILERCHAR '='

static bool err = false;
// based on malicious JSON "{1:\x00}"
static const int minimal_legal_json_size = 8;

static const SingleRegex invalid_hex_evasion_re(
    "%([g-zG-Z][0-9a-zA-Z]|[0-9a-zA-Z][g-zG-Z])",
    err,
    "invalid_hex_evasion"
);
static const SingleRegex broken_utf_evasion_re(
    "(?:^|[^%])(%[0-9a-f]%[0-9a-f])",
    err,
    "broken_utf_evasion"
);
static const SingleRegex csp_report_policy_re(
    "default-src\\s+[^\\w]+.*report-uri\\s+[^\\w]+",
    err,
    "csp_report_policy"
);
static const SingleRegex base64_key_value_detector_re(
    "^[^<>{};,&\\?|=\\s]+={1}\\s*.+",
    err,
    "base64_key_value");
static const SingleRegex json_key_value_detector_re(
    "\\A[^<>{};,&\\?|=\\s]+=[{\\[][^;\",}\\]]*[,:\"].+[\\s\\S]",
    err,
    "json_key_value");
static const SingleRegex base64_key_detector_re(
    "^[^<>{};,&\\?|=\\s]+={1}",
    err,
    "base64_key");
// Matches valid Base64 prefixes: "data:" URIs with optional parameters or bare "base64,"
// strings. Ensures correct MIME types, strict Base64 encoding with padding, and anchoring
// at string-start or after '=' (with optional whitespace). Prevents invalid injections.
static const SingleRegex base64_prefix_detector_re(
    "^data:[a-zA-Z0-9.+-]+/[a-zA-Z0-9.+-]+(?:;[a-zA-Z0-9.+-]+=[a-zA-Z0-9.+-]+)*;base64,[A-Za-z0-9+/]+"
    "={0,2}|^base64,[A-Za-z0-9+/]+={0,2}|=\\s*data:[a-zA-Z0-9.+-]+/[a-zA-Z0-9.+-]+(?:;[a-zA-Z0-9.+-]+"
    "=[a-zA-Z0-9.+-]+)*;base64,[A-Za-z0-9+/]+={0,2}|=\\s*base64,[A-Za-z0-9+/]+={0,2}",
    err,
    "base64_prefix");
static const SingleRegex percent_encoding_re(
    "%[A-Fa-f0-9]{2}",
    err,
    "percent_encoding");
static const SingleRegex nosql_key_evasion_detector_re(
    "\\w{1,48}\\s*[\\[\\{\\(]\\s*\\$\\w{1,48}\\s*[\\]\\}\\)]",
    err,
    "nosql_key_evasion_detector");

// looks for combination <param>={<some text>*:<some text>*}
//used to allow parsing param=JSON to reduce false positives
bool detectJSONasParameter(const string &string_buffer,
        string &key,
        string &value)
{
    key.clear();
    value.clear();
    bool is_json_candidate_detected = json_key_value_detector_re.hasMatch(string_buffer);

    if (is_json_candidate_detected) {
        dbgTrace(D_WAAP_JSON) << "===JSONdetect===:  json_key_value_detector_re test passed - looking for key";
        string::const_iterator it = string_buffer.begin();
        for (; it != string_buffer.end(); ++it) {
            if (*it != '{') {
                continue;
            }
            // candidate should have size 8 or more - minimum for JSON with attack
            if ((string_buffer.end() - it) < minimal_legal_json_size) {
                dbgTrace(D_WAAP_JSON)
                    << "===JSONdetect===: candidate is shorter then the length"
                    "of the shortest known json attack which is: " << minimal_legal_json_size;
                return false;
            }

            key = std::string(string_buffer.begin(), it-1);
            value = std::string(it, string_buffer.end());
            break;
        }
    }
    dbgTrace(D_WAAP_JSON)
    << "===JSONdetect===:  key = '"
    << key
    << "', value = '"
    << value <<"'";
    return is_json_candidate_detected;
}

static void b64TestChunk(const string &s,
        string::const_iterator chunkStart,
        string::const_iterator chunkEnd,
        RegexSubCallback_f cb,
        int &decodedCount,
        int &deletedCount,
        string &outStr)
{
    dbgTrace(D_WAAP_BASE64) << " ===b64TestChunk===:  starting with = '" << s << "'";
    size_t chunkLen = (chunkEnd - chunkStart);

    if ((chunkEnd - chunkStart) > static_cast<int>(b64_prefix.size()) &&
            chunkStart[0] == 'b' && chunkStart[1] == 'a' && chunkStart[2] == 's' && chunkStart[3] == 'e' &&
            chunkStart[4] == '6' && chunkStart[5] == '4' && chunkStart[6] == ',') {
        chunkLen -= b64_prefix.size();
    }

    string repl;
    dbgTrace(D_WAAP_BASE64) << " ===b64TestChunk===:  chunkLen = " << chunkLen;
    if (cb(s, chunkStart, chunkEnd, repl)) {
        // Succesfully matched b64 chunk
        if (!repl.empty()) {
            outStr += repl;
            decodedCount++;
        }
        else {
            deletedCount++;
        }
    }
    else {
        // Chunk was not processed - put original text
        size_t from = chunkStart - s.begin();
        size_t len = chunkEnd - chunkStart;
        outStr += s.substr(from, len);
    }
}

bool detectBase64Chunk(
        const string &s,
        string::const_iterator &start,
        string::const_iterator &end)
{
    dbgTrace(D_WAAP_BASE64) << " ===detectBase64Chunk===:  starting with = '" << s << "'";
    string::const_iterator it = s.begin();
    start = s.end();
    end = s.end();

    //detect "base64," prefix to start search after this
    for (; it != s.end()-7; it++) {
        if (it[0] == 'b' && it[1] == 'a' && it[2] == 's' && it[3] ==
                'e' && it[4] == '6' && it[5] == '4' && it[6] == ',') {
            it = it + 7;
            dbgTrace(D_WAAP_BASE64) << " ===detectBase64Chunk===:  prefix skipped = '" << *it << "'";
            break;
        }
    }

    //look for start of encoded string
    dbgTrace(D_WAAP_BASE64) << " ===detectBase64Chunk===:  B64 itself = '" << *it << "'";
    bool isB64AlphaChar = Waap::Util::isAlphaAsciiFast(*it) || isdigit(*it) || *it=='/' || *it=='+';

    if (isB64AlphaChar) {
        // start tracking potential b64 chunk - just check its size
        dbgTrace(D_WAAP_BASE64) << " ===detectBase64Chunk===:  isB64AlphaChar = true, '" << *it << "'";
        start = it;
        end = s.end();
        return true;
    }
    // non base64 before supposed chunk - will not process
    return false;
}

base64_decode_status
processDecodedChunk(
    const string &s,
    string::const_iterator start,
    string::const_iterator end,
    string &value,
    BinaryFileType &binaryFileType,
    bool called_with_prefix = false
)
{
    base64_decode_status retVal = decodeBase64Chunk(s, start, end, value, false, called_with_prefix);
    dbgTrace(D_WAAP_BASE64) << " ===isBase64PrefixProcessingOK===: after decode. retVal=" << retVal
                            << " value.size()=" << value.size();
    if (retVal != B64_DECODE_INVALID && !value.empty()) {
        binaryFileType = ParserBinaryFile::detectBinaryFileHeader(value);
        if (retVal == B64_DECODE_INCOMPLETE) value.clear();
    }
    return retVal;
}

bool isBase64PrefixProcessingOK (
        const string &s,
        string &value,
        BinaryFileType &binaryFileType)
{
    string::const_iterator start, end;
    base64_decode_status retVal = B64_DECODE_INVALID;
    dbgTrace(D_WAAP_BASE64) << " ===isBase64PrefixProcessingOK===: before regex for prefix for string '" << s << "'";
    if (base64_prefix_detector_re.hasMatch(s)) {
        dbgTrace(D_WAAP_BASE64) << " ===isBase64PrefixProcessingOK===: prefix detected on string '" << s << "'";
        if (detectBase64Chunk(s, start, end)) {
            dbgTrace(D_WAAP_BASE64) << " ===isBase64PrefixProcessingOK===: chunk detected";
            if ((start != s.end()) && (end == s.end())) {
                dbgTrace(D_WAAP_BASE64) << " ===isBase64PrefixProcessingOK===: chunk detected but not complete";
                retVal = processDecodedChunk(s, start, end, value, binaryFileType, true);
                dbgTrace(D_WAAP_BASE64)
                    << " ===isBase64PrefixProcessingOK===: after processDecodedChunk retVal = "
                    << retVal
                    << " binaryFileType = "
                    << binaryFileType;
            }
        } else if (start != s.end()) {
            dbgTrace(D_WAAP_BASE64)
                << " ===isBase64PrefixProcessingOK===: chunk not detected. searching for known file header only";
            end = (start + MAX_HEADER_LOOKUP < s.end()) ? start + MAX_HEADER_LOOKUP : s.end();
            processDecodedChunk(s, start, end, value, binaryFileType);
            value.clear();
            dbgTrace(D_WAAP_BASE64)
                << " ===isBase64PrefixProcessingOK===: after processDecodedChunk binaryFileType = "
                << binaryFileType;
            return binaryFileType != Waap::Util::BinaryFileType::FILE_TYPE_NONE;
        }
    }
    dbgTrace(D_WAAP_BASE64)
        << " ===isBase64PrefixProcessingOK===: retVal = "
        << retVal
        << " binaryFileType = "
        << binaryFileType;
    return retVal != B64_DECODE_INVALID;
}

base64_variants b64Test (
        const string &s,
        string &key,
        string &value,
        BinaryFileType &binaryFileType,
        const size_t offset)
{

    key.clear();
    binaryFileType = Waap::Util::BinaryFileType::FILE_TYPE_NONE;
    auto begin = s.begin() + offset;
    dbgTrace(D_WAAP_BASE64)
        << " ===b64Test===: string =  "
        << s
        << " key = "
        << key
        << " value = "
        << value
        << " offset = "
        << offset;

    // Minimal length
    if (s.size() < 8 + offset) {
        return CONTINUE_AS_IS;
    }
    dbgTrace(D_WAAP_BASE64) << " ===b64Test===: minimal lenght test passed";

    std::string prefix_decoded_val;
    auto it = begin;

    // 1st check if we have key candidate
    if (base64_key_value_detector_re.hasMatch(s)) {
        base64_stage state = BEFORE_EQUAL;
        dbgTrace(D_WAAP_BASE64) << " ===b64Test===: testB64Key test passed - looking for key";
        for (; (it != s.end()) && (state != DONE) && (state != MISDETECT); ++it) {
            switch(state) {
            case BEFORE_EQUAL:
                if (*it != '=') {
                    key += string(1, *it);
                } else {
                    key += string(1, *it);
                    state = EQUAL;
                }
                break;
            case EQUAL:
                if (*it == '=') {
                    it = begin;
                    state=MISDETECT;
                    continue;
                }
                if (*it == ' ') {
                    //skip whitespaces - we don't need them in key
                    continue;
                } else {
                    state = DONE;
                }
                break;
            case DONE:
                continue;
            default:
                break;
            }

        }
        dbgTrace(D_WAAP_BASE64) << " ===b64Test===: detected key = " << key;
        if (it == s.end() || state == MISDETECT) {
            dbgTrace(D_WAAP_BASE64) << " ===b64Test===: detected  *it = s.end()" << *it;
            if (key.size() > 0) {
                it = begin;
                key.clear();
            }
        } else {
            it--;

            dbgTrace(D_WAAP_BASE64) << " ===b64Test===: Key is OK  *it = " << *it;
        }
    }

    dbgTrace(D_WAAP_BASE64) << " ===b64Test===: after processing key = '" << key << "'";
    bool found = isBase64PrefixProcessingOK(s, prefix_decoded_val, binaryFileType);
    dbgTrace(D_WAAP_BASE64) << " ===b64Test===: after prefix test found = "
            << found << " new value is '" << prefix_decoded_val << "' - done";
    if (found) {
        value = prefix_decoded_val;
        if (key.empty()) {
            return SINGLE_B64_CHUNK_CONVERT;
        } else {
            key.pop_back();
            return KEY_VALUE_B64_PAIR;
        }
    }

    auto start = s.end();
    dbgTrace(D_WAAP_BASE64) << " ===b64Test===:  B64 itself = " << *it << " =======";
    bool isB64AlphaChar = Waap::Util::isAlphaAsciiFast(*it) || isdigit(*it) || *it=='/' || *it=='+';
    if (isB64AlphaChar) {
        // 1st char is potential b64, let's try to convert this
        dbgTrace(D_WAAP_BASE64) <<
            " ===b64Test===:  Start tracking potential b64 chunk = " << *it << " =======";
        start = it;
    }
    else {
        dbgTrace(D_WAAP_BASE64) <<
            " ===b64Test===: Non base64 before supposed chunk - will not process = " << *it << " =======";
        return CONTINUE_AS_IS;
    }

    if (start != s.end()) {
        // key is not empty, it should be tested for correct format (i.e. key=b64val and not splittable)
        // else leave it as is
        dbgTrace(D_WAAP_BASE64) << " ===b64Test===:BEFORE TESTING KEY key = '" << key << "'";
        if (!key.empty()) {
            if (!base64_key_detector_re.hasMatch(key)) {
                dbgTrace(D_WAAP_BASE64) << " ===b64Test===: Key is NOT GOOD regex key = '" << key << "'";
                return CONTINUE_AS_IS;
            }
            // remove '=' as last char in key - we don't need it
            key.pop_back();
            dbgTrace(D_WAAP_BASE64) << " ===b64Test===: FINAL key = '" << key << "'";
        }
        base64_decode_status decode_chunk_status = decodeBase64Chunk(s, start, s.end(), value);

        dbgTrace(D_WAAP_BASE64)
            << " ===b64Test===: After testing and conversion value = "
            << value
            << "decode_chunk_status = '"
            << decode_chunk_status
            <<"'";
        if (decode_chunk_status == B64_DECODE_INVALID) {
            key.clear();
            value.clear();
            return CONTINUE_AS_IS;
        }

        if (decode_chunk_status == B64_DECODE_INCOMPLETE) {
                value.clear();
        }

        if (decode_chunk_status == B64_DECODE_SUSPECTED) {
            return CONTINUE_DUAL_SCAN;
        }

        dbgTrace(D_WAAP_BASE64)
            << " ===b64Test===: After tpassed retVal check = "
            << value
            << "decode_chunk_status = '"
            << decode_chunk_status
            <<"'"
            << "key = '"
            << key
            << "'";
        if (key.empty()) {
            return SINGLE_B64_CHUNK_CONVERT;
        } else {
            return KEY_VALUE_B64_PAIR;
        }

    } else {
        // There are symbols after base64 chunk - leave as is, may be it will be splitted no next step
        key.clear();
        value.clear();
        return CONTINUE_AS_IS;
    }
}

void b64Decode(
        const string &s,
        RegexSubCallback_f cb,
        int &decodedCount,
        int &deletedCount,
        string &outStr)
{
    decodedCount = 0;
    deletedCount = 0;
    outStr = "";
    int offsetFix = 0;
    dbgTrace(D_WAAP_BASE64) << " ===b64Decode===:  starting with = '" << s << "'";
    string::const_iterator it = s.begin();

    // Minimal length
    if (s.end() - it < 8) {
        return;
    }

    // Search for substrings that match these criterias:
    //  1. substring length is divisible by 4
    //  2. substring contains only letters a-z, 0-9, '/' or '+' except last 1 or two characters that can be '='

    string::const_iterator chunkStart = s.end();
    for (; it != s.end(); ++it) {
        bool isB64AlphaChar = Waap::Util::isAlphaAsciiFast(*it) || isdigit(*it) || *it=='/' || *it=='+';
        if (chunkStart == s.end()) {
            if (isB64AlphaChar) {
                // start tracking potential b64 chunk
                chunkStart = it;
            }
            else {
                // Add anything before the potential match
                outStr += string(1, *it);
            }
        }
        else {
            // tracking b64 chunk
            if (!isB64AlphaChar) {
                if (*it == ',') {
                    // Check back and skip the "base64," prefix
                    if (chunkStart + b64_prefix.size() - 1 == it) {
                        string cand(chunkStart, it + 1);
                        if (cand == b64_prefix) {
                            offsetFix = b64_prefix.size();
                            continue;
                        }
                    }
                }

                size_t chunkLen = (it - chunkStart) - offsetFix;
                size_t chunkRem = chunkLen % 4;

                // Allow only one or two '=' characters at the end of the match
                if ((*it == B64_TRAILERCHAR) && (chunkRem == 2 || chunkRem == 3)) {
                    continue;
                }

                // Decode and add chunk
                dbgTrace(D_WAAP_BASE64)
                    << " ===b64Decode===:  chunkStart = "
                    << *chunkStart
                    << " it = "
                    << *it;
                b64TestChunk(s, chunkStart, it, cb, decodedCount, deletedCount, outStr);

                // stop tracking b64 chunk
                outStr += string(1, *it); // put the character that terminated the chunk
                chunkStart = s.end();
                offsetFix = 0;
            }
        }
    }

    if (chunkStart != s.end()) {
        dbgTrace(D_WAAP_BASE64) << " ===b64Decode===:  chunkStart = " << *chunkStart;
        b64TestChunk(s, chunkStart, it, cb, decodedCount, deletedCount, outStr);
    }
}

// Base64 functions stolen from orchestration_tools.cc
static const string base64_base_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

string
base64Encode(const string &input)
{
    string out;
    int val = 0, val_base = -6;
    for (unsigned char c : input) {
        val = (val << 8) + c;
        val_base += 8;
        while (val_base >= 0) {
            out.push_back(base64_base_str[(val >> val_base) & 0x3F]);
            val_base -= 6;
        }
    }
    // -6 indicates the number of bits to take from each character
    // (6 bits is enough to present a range of 0 to 63)
    if (val_base > -6) out.push_back(base64_base_str[((val << 8) >> (val_base + 8)) & 0x3F]);
    while (out.size() % 4) out.push_back('=');
    return out;
}

bool find_in_map_of_stringlists_keys(const string &what, const map_of_stringlists_t &where)
{
    for (map_of_stringlists_t::const_iterator it = where.begin(); it != where.end(); ++it) {
        if (it->first.find(what) != string::npos) {
            return true;
        }
    }

    return false;
}

void remove_in_map_of_stringlists_keys(const string &what, map_of_stringlists_t &where)
{
    map_of_stringlists_t::iterator it = where.begin();

    while (it != where.end()) {
        if (it->first.find(what) != string::npos) {
            it = where.erase(it);
        }
        else {
            it++;
        }
    }
}

void remove_startswith(vector<string> &vec, const string &prefix)
{
    vec.erase(
        remove_if(vec.begin(), vec.end(),
            [&prefix](const string &kw)
            {
                return boost::starts_with(kw, prefix);
            }
        ),
        vec.end()
    );
}

string AES128Decrypt(
    string& key,
    string& iv,
    string& message)
{
    unsigned char* outdata = new unsigned char[message.length()];

    // data structure that contains the key itself
    AES_KEY aes_key;

    // set the encryption key
    AES_set_decrypt_key((const unsigned char*)key.c_str(), 128, &aes_key);

    AES_cbc_encrypt(
        (unsigned char*)message.c_str(),
        outdata, message.length(),
        &aes_key, (unsigned char*)iv.c_str(),
        AES_DECRYPT
        );

    // get value without padding
    size_t padding = outdata[message.length() - 1]; // last byte contain padding size
    string result = string((const char*)outdata, message.length() - padding);

    delete[] outdata;
    return result;
}

string
base64Decode(const string &input)
{
    string out;
    vector<int> T(256, -1);
    for (int i = 0; i < 64; i++) {
        T[base64_base_str[i]] = i;
    }
    int val = 0, valb = -8;
    for (unsigned char c : input) {
        if (T[c] == -1) break;
        val = (val << 6) + T[c];
        valb += 6;
        if (valb >= 0) {
            out.push_back(char((val >> valb) & 0xFF));
            valb -= 8;
        }
    }
    return out;
}

bool
isGzipped(const string &stream)
{
    if (stream.size() < 2) return false;
    auto unsinged_stream = reinterpret_cast<const u_char *>(stream.data());
    return unsinged_stream[0] == 0x1f && unsinged_stream[1] == 0x8b;
}

bool
containsInvalidUtf8(const string &payload)
{
    return invalid_hex_evasion_re.hasMatch(payload);
}

bool
containsPercentEncoding(const string &payload)
{
    static const size_t min_matches = 2;
    vector<RegexMatch> regex_matches;
    size_t counter = percent_encoding_re.findAllMatches(payload, regex_matches, min_matches);
    // Check if there are at least two matches
    return counter >= min_matches;
}

string
unescapeInvalidUtf8(const string &payload)
{
    dbgFlow(D_WAAP_EVASIONS);
    vector<RegexMatchRange> regex_matches;
    invalid_hex_evasion_re.findMatchRanges(payload, regex_matches);

    string unescaped_text = payload;
    for (const auto &match : regex_matches) {
        static const int evasion_pattern_length = 3;

        int num = 0;
        size_t pos = match.start + 1;
        for (; pos < match.end; pos++) {
            const char &byte = unescaped_text[pos];
            if (isdigit(byte)) {
                num = (num << 4) + charToDigit(byte);
            } else {
                num = (num << 4) + ((tolower(byte) - 'a') + 10);
            }
        }

        char buf[evasion_pattern_length];
        snprintf(buf, evasion_pattern_length, "%02x", (num & 0xff));
        unescaped_text.replace(match.start + 1, evasion_pattern_length - 1, buf);

        dbgTrace(D_WAAP_EVASIONS) << "Value after conversion: decimal = " << num << ", hex = " << buf;
    }

    dbgTrace(D_WAAP_EVASIONS) << "unescaped_text: " << unescaped_text;

    return unescaped_text;
}

Maybe<std::string>
containsBrokenUtf8(const string &payload, const string &unquoted_payload)
{
    if (broken_utf_evasion_re.hasMatch(unquoted_payload)) {
        return unquoted_payload;
    } else if (broken_utf_evasion_re.hasMatch(payload)) {
        return payload;
    } else {
        return genError("does not contain broken-down UTF8");
    }
}

string
unescapeBrokenUtf8(const string &payload)
{
    string unescaped_text;
    unescaped_text.reserve(payload.length());

    int prev_esc_pos = -1;
    for (size_t pos = 0; pos < payload.length(); ++pos) {
        char c = payload[pos];
        if (c == '%') {
            // skip copying current '%' when encountered with the 2nd '%'
            // that follows and followed by only one hex digit
            if (prev_esc_pos >= 0 && pos-prev_esc_pos == 2 && isxdigit(payload[pos-1]) &&
                    pos+1 < payload.length() && isxdigit(payload[pos+1]) ) {
                prev_esc_pos = -1;
                continue;
            }
            // mark current '%' only when not following another '%'
            if (prev_esc_pos < 0 || pos-prev_esc_pos > 1) {
                prev_esc_pos = pos;
            }
        }
        unescaped_text += c;
    }

    dbgTrace(D_WAAP_EVASIONS) << "unescaped_text: " << unescaped_text;
    return unescaped_text;
}

bool
containsCspReportPolicy(const string &payload)
{
    return csp_report_policy_re.hasMatch(payload);
}

string
charToString(const char* s, int slen)
{
    if (!s || slen == 0) return "";

    return string(s, slen);
}

string
vecToString(const vector<string>& vec, char delim) {
    ostringstream vts;

    string delimStr;
    delimStr.push_back(delim);
    if (delim != '\n')
    {
        delimStr.push_back(' ');
    }

    vts << "[";
    if (!vec.empty())
    {
        // Convert all but the last element to avoid a trailing ","
        copy(vec.begin(), vec.end() - 1,
            ostream_iterator<string>(vts, delimStr.c_str()));

        // Now add the last element with no delimiter
        vts << vec.back();
    }
    else
    {
        return string();
    }
    vts << "]";


    return vts.str();
}


string
obfuscateXor(const string& toEncrypt) {
    char key[] = "CHECKPOINT"; //Any chars will work
    string output = toEncrypt;

    for (size_t i = 0; i < toEncrypt.size(); i++) {
        output[i] = toEncrypt[i] ^ key[i % ((sizeof(key)-1) / sizeof(char))];
    }

    return output;
}

string
obfuscateXorBase64(const string& toEncrypt) {
    return base64Encode(obfuscateXor(toEncrypt));
}

string injectSpacesToString(const string& str) {
    string retStr = "";
    if (str.length() == 0)
    {
        return retStr;
    }
    retStr.resize(str.length() * 2, ' ');
    for (size_t i = 0; i < str.length(); i++)
    {
        retStr[i * 2] = str[i];
    }
    retStr.pop_back();
    return retStr;
}

ReportIS::Severity computeSeverityFromThreatLevel(ThreatLevel threatLevel) {
    if (threatLevel == NO_THREAT) {
        return ReportIS::Severity::INFO;
    }
    else if (threatLevel == THREAT_INFO) {
        return ReportIS::Severity::LOW;
    }
    else if (threatLevel == LOW_THREAT) {
        return ReportIS::Severity::MEDIUM;
    }
    else if (threatLevel == MEDIUM_THREAT) {
        return ReportIS::Severity::HIGH;
    }

    return ReportIS::Severity::CRITICAL;
}

ReportIS::Priority computePriorityFromThreatLevel(ThreatLevel threatLevel) {
    if (threatLevel == NO_THREAT) {
        return ReportIS::Priority::LOW;
    }
    else if (threatLevel == THREAT_INFO) {
        return ReportIS::Priority::MEDIUM;
    }
    else if (threatLevel == LOW_THREAT) {
        return ReportIS::Priority::MEDIUM;
    }
    else if (threatLevel == MEDIUM_THREAT) {
        return ReportIS::Priority::HIGH;
    }

    return ReportIS::Priority::HIGH;
}

string computeConfidenceFromThreatLevel(ThreatLevel threatLevel)
{
    switch(threatLevel) {
        case NO_THREAT: return "Low";
        case THREAT_INFO: return "Low";
        case LOW_THREAT: return "Medium";
        case MEDIUM_THREAT: return "High";
        case HIGH_THREAT: return "Very High";
    }
    dbgWarning(D_WAAP) << "Reached impossible threat level value of: " << static_cast<int>(threatLevel);
    return "Low";
}

void decodePercentEncoding(string &text, bool decodePlus)
{
    // Replace %xx sequences by their single-character equivalents.
    // Do not replace the '+' symbol by space character because this would corrupt some base64 source strings
    // (base64 alphabet includes the '+' character).
    text.erase(
        unquote_plus(text.begin(), text.end(), checkUrlEncoded(text.data(), text.size()), decodePlus), text.end()
    );
    dbgTrace(D_WAAP) << "decodePercentEncoding: (after unquote_plus) '" << text << "'";
}

// Try to detect/decode UTF16 (detecting either BE and LE variant).
// The function uses statistics to try to guess whether UTF-16 is present and its exact variant (Big/Little endianess)
// If UTF-16 value is detected, value in cur_val is converted to utf8 in-place for use in later processing.
void decodeUtf16Value(const ValueStatsAnalyzer &valueStats, string &cur_val)
{
    // Do not change cur_val if UTF16 is not detected
    if (!valueStats.isUTF16) {
        return;
    }

    dbgTrace(D_WAAP) << "decoding UTF-16 into UTF-8 in-place";

    bool isBigEndian = false;
    size_t pos = 0;

    // First, detect BOM as a hint of UTF16-BE vs. LE variant. See https://unicode.org/faq/utf_bom.html#utf8-4
    if (cur_val[0] == (char)0xFE && cur_val[1] == (char)0xFF) {
        // UTF16-BE hint
        isBigEndian = true;
        // Skip the BOM
        pos++;
    }
    else if (cur_val[0] == (char)0xFF && cur_val[1] == (char)0xFE) {
        // UTF16-LE hint
        isBigEndian = false;
        // Skip the BOM
        pos++;
    }
    else {
        isBigEndian = (valueStats.longestZerosSeq[0] > valueStats.longestZerosSeq[1]);
    }

    // Decode utf16 into utf8
    string utf8Out;
    for (; pos<cur_val.length()/2; ++pos) {
        unsigned int code;

        if (isBigEndian) {
            code = (cur_val[pos*2] << 8) + cur_val[pos*2+1];
        }
        else {
            code = (cur_val[pos*2+1] << 8) + cur_val[pos*2];
        }

        // Encode UTF code point as UTF-8 bytes
        if (code < 0x80) {
            utf8Out += code;
        }
        else if (code < 0x800 ) {
            utf8Out += (code >> 6) | 0xC0;
            utf8Out += (code & 0x3F) | 0x80;
        }
        else { // (code <= 0xFFFF : always true because code type is unsigned short which is 16-bit
            utf8Out += (code >> 12) | 0xE0;
            utf8Out += ((code >> 6) & 0x3F) | 0x80;
            utf8Out += (code & 0x3F) | 0x80;
        }
    }

    // Return the value converted from UTF-16 to UTF-8
    cur_val = utf8Out;
}

bool testNoSQLKeySuspect(const string &key) {
    return Waap::Util::nosql_key_evasion_detector_re.hasMatch(key);
}

bool testUrlBareUtf8Evasion(const string &line) {
    size_t percentPos = 0;

    while (percentPos < line.size()) {
        percentPos = line.find("%", percentPos);

        if (percentPos == string::npos) {
            return false;
        }

        if (percentPos + 2 < line.size() && tolower(line[percentPos + 1]) == 'c' && line[percentPos + 2] == '0') {
            // found "%c0"
            return true;
        }

        // Continue searching from next character after '%'
        percentPos++;
    }

    return false;
}

bool testUrlBadUtf8Evasion(const string &line) {
    size_t percentPos = 0;

    while (percentPos < line.size()) {
        percentPos = line.find("%", percentPos);

        if (percentPos == string::npos) {
            return false;
        }

        if (percentPos + 2 < line.size() && tolower(line[percentPos + 1]) == 'c' && line[percentPos + 2] == '1') {
            // found "%c1"
            return true;
        }

        // Continue searching from next character after '%'
        percentPos++;
    }

    return false;
}

string urlDecode(string src) {
    src.erase(unquote_plus(src.begin(), src.end(), true, false), src.end());
    return src;
}

// LCOV_EXCL_START Reason: The function will be deleted on another task
string
stripOptionalPort(const string::const_iterator &first, const string::const_iterator &last)
{
    // Microsoft XFF+IPv6+Port yikes - see also here https://github.com/eclipse/jetty.project/issues/3630
    if (*first == '[') {
        // Possible bracketed IPv6 address such as "[2001:db8::1]" + optional numeric ":<port>"
        auto close_bracket = find(first + 1, last, ']');
        if (close_bracket == last) return string(first, last);
        return string(first+1, close_bracket);
    }

    auto first_colon = find(first, last, ':');
    if (first_colon == last) return string(first, last);

    // If there is more than one colon it means its probably IPv6 address without brackets
    auto second_colon = find(first_colon + 1, last, ':');
    if (second_colon != last) return string(first, last);

    // If there's only one colon it can't be IPv6 and can only be IPv4 with port
    return string(first, first_colon);
}

bool
isIpTrusted(const string &ip, const vector<string> &trusted_ips)
{
    Waap::Util::CIDRData cidr_data;
    for (const auto &trusted_ip : trusted_ips) {
        if (
            ip == trusted_ip ||
            (Waap::Util::isCIDR(trusted_ip, cidr_data) && Waap::Util::cidrMatch(ip, cidr_data))
        ) {
            return true;
        }
    }
    return false;
}

string extractForwardedIp(const string &x_forwarded_hdr_val)
{
    vector<string> xff_splitted = split(x_forwarded_hdr_val, ',');
    vector<string> trusted_ips;
    string forward_ip;

    auto identify_config = getConfigurationWithCache<UsersAllIdentifiersConfig>(
        "rulebase",
        "usersIdentifiers"
    );

    if (!identify_config.ok()) {
        dbgDebug(D_WAAP) << "did not find xff definition in policy";
    } else {
        trusted_ips = (*identify_config).getHeaderValuesFromConfig("x-forwarded-for");
    }

    if (xff_splitted.size() > 0)
    {
        for (size_t k = 0; k < xff_splitted.size(); ++k)
        {
            string optional_result = trim(xff_splitted[k]);
            optional_result = stripOptionalPort(optional_result.cbegin(), optional_result.cend());
            if (isIpAddress(optional_result))
            {
                if (!isIpTrusted(optional_result, trusted_ips) && !trusted_ips.empty()) {
                    return "";
                } else if (forward_ip.empty()) {
                    forward_ip = optional_result;
                }
            }
        }
    }
    return forward_ip;
}

bool isUuid(const string& str) {
    if (str.length() != 36) {
        return false;
    }
    static bool err;
    static const SingleRegex uuid_detector_re(
        "[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}",
        err,
        "uuid_detector"
    );
    // Check if the string matches the UUID format
    return uuid_detector_re.hasMatch(str);
}

bool isIpAddress(const string &ip_address)
{
    struct in_addr source_inaddr;
    struct in6_addr source_inaddr6;

    // check from which type the target ip and check if ip belongs to is mask ip
    //convert sourceip to ip v4 or v6.
    bool isIpV4 = inet_pton(AF_INET, ip_address.c_str(), &source_inaddr) == 1;
    bool isIpV6 = inet_pton(AF_INET6, ip_address.c_str(), &source_inaddr6) == 1;

    return isIpV4 || isIpV6;
}

// LCOV_EXCL_STOP

string extractKeyValueFromCookie(const string &cookie, const string &key)
{
    string source = "";
    vector<string> cookie_splitted = split(cookie, ';');
    for (size_t l = 0; l < cookie_splitted.size(); ++l)
    {
        vector<string> cookie_key_splitted = split(cookie_splitted[l], '=');
        if (cookie_key_splitted.empty())
        {
            dbgWarning(D_WAAP) << "Failed to split the key-value from: " << cookie_splitted[l];
            continue;
        }
        if (cookie_key_splitted[0] == key)
        {
            source = cookie_key_splitted[1];

            if (key == "_oauth2_proxy")
            {
                source = Waap::Util::base64Decode(source);

                vector<string> currentUserIdentifier_splitted = split(source, '|');

                if (currentUserIdentifier_splitted.size() > 0)
                {
                    source = currentUserIdentifier_splitted[0];
                }
            }
            break;
        }
    }
    dbgTrace(D_WAAP) << "extracted source from Cookie:" << key << " : " << source;
    return source;
}

bool vectorStringContain(const vector<string>& vec, const string& str)
{
    for(auto &param : vec) {
        if(param.compare(str) == 0)
        {
            return true;
        }
    }
    return false;
}

ContentType detectContentType(const char* hdr_value) {
    const char* plus_p = ::strrchr(hdr_value, '+');
    // Detect XML content type if Content-Type header value ends with "+xml".
    // For example: "application/xhtml+xml", or "image/svg+xml"
    // For reference: see first answer here:
    // https://stackoverflow.com/questions/2965587/valid-content-type-for-xml-html-and-xhtml-documents
    if (plus_p && my_stricmp(plus_p + 1, "xml")) {
        return CONTENT_TYPE_XML;
    }

    const char* slash_p = ::strrchr(hdr_value, '/');

    if (slash_p) {
        // Detect XML content type if Content-Type header value ends with "/xml"
        if (my_stricmp(slash_p + 1, "xml")) {
            return CONTENT_TYPE_XML;
        }

        // Detect JSON content type if Content-Type header value is application/json or ends with "/json"
        if (my_stricmp(slash_p + 1, "json") || my_stristarts_with(hdr_value, "application/json")) {
            return CONTENT_TYPE_JSON;
        }

        // Detect Graphql content type if Content-Type header value is application/graphql
        if (my_stristarts_with(hdr_value, "application/graphql")) {
            return CONTENT_TYPE_GQL;
        }

        // Detect HTML content type
        if (my_stristarts_with(hdr_value, "text/html")) {
            return CONTENT_TYPE_HTML;
        }

        // Detect Multiplart Form Data content type
        if (my_stristarts_with(hdr_value, "multipart/form-data")) {
            return CONTENT_TYPE_MULTIPART_FORM;
        }

        // Detect URL Encoded content type
        if (my_stristarts_with(hdr_value, "application/x-www-form-urlencoded")) {
            return CONTENT_TYPE_URLENCODED;
        }

        // Detect binary xml file type
        if (my_stristarts_with(hdr_value, "application/vnd.ms-sync.wbxml")) {
            return CONTENT_TYPE_WBXML;
        }
    }

    return CONTENT_TYPE_UNKNOWN;
}

string convertParamTypeToStr(ParamType type)
{
    switch (type)
    {
    case UNKNOWN_PARAM_TYPE:
        return "unknown";
    case HTML_PARAM_TYPE:
        return "html_input";
    case URL_PARAM_TYPE:
        return "urls";
    case FREE_TEXT_PARAM_TYPE:
        return "free_text";
    case FREE_TEXT_FRENCH_PARAM_TYPE:
        return "free_text_french";
    case PIPE_PARAM_TYPE:
        return "pipes";
    case LONG_RANDOM_TEXT_PARAM_TYPE:
        return "long_random_text";
    case BASE64_PARAM_TYPE:
        return "base64";
    case ADMINISTRATOR_CONFIG_PARAM_TYPE:
        return "administration_config";
    case FILE_PATH_PARAM_TYPE:
        return "local_file_path";
    case SEMICOLON_DELIMITED_PARAM_TYPE:
        return "semicolon_delimiter";
    case ASTERISK_DELIMITED_PARAM_TYPE:
        return "asterisk_delimiter";
    case COMMA_DELIMITED_PARAM_TYPE:
        return "comma_delimiter";
    case AMPERSAND_DELIMITED_PARAM_TYPE:
        return "ampersand_delimiter";
    case BINARY_PARAM_TYPE:
        return "binary_input";
    default:
        dbgWarning(D_WAAP) << "unrecognized type " << to_string(type);
        return "unrecognized type";
    }
}

ParamType convertTypeStrToEnum(const string& typeStr)
{
    static unordered_map<string, ParamType> sNameTypeMap = {
    {"unknown", ParamType::UNKNOWN_PARAM_TYPE},
    {"administration_config", ParamType::ADMINISTRATOR_CONFIG_PARAM_TYPE},
    {"base64", ParamType::BASE64_PARAM_TYPE },
    {"free_text", ParamType::FREE_TEXT_PARAM_TYPE},
    {"free_text_french", ParamType::FREE_TEXT_FRENCH_PARAM_TYPE},
    {"html_input", ParamType::HTML_PARAM_TYPE},
    {"long_random_text", ParamType::LONG_RANDOM_TEXT_PARAM_TYPE},
    {"pipes", ParamType::PIPE_PARAM_TYPE},
    {"urls", ParamType::URL_PARAM_TYPE},
    {"local_file_path", ParamType::FILE_PATH_PARAM_TYPE},
    {"semicolon_delimiter", ParamType::SEMICOLON_DELIMITED_PARAM_TYPE},
    {"asterisk_delimiter", ParamType::ASTERISK_DELIMITED_PARAM_TYPE},
    {"comma_delimiter", ParamType::COMMA_DELIMITED_PARAM_TYPE},
    {"ampersand_delimiter", ParamType::AMPERSAND_DELIMITED_PARAM_TYPE},
    {"binary_input", ParamType::BINARY_PARAM_TYPE}
    };

    auto mapItr = sNameTypeMap.find(typeStr);
    if (mapItr != sNameTypeMap.end())
    {
        return mapItr->second;
    }
    dbgWarning(D_WAAP) << "unrecognized parameter type name: " << typeStr;
    return UNKNOWN_PARAM_TYPE;

}


}
}
