/*
 * Copyright (C) 1996-2026 The Squid Software Foundation and contributors
 *
 * Squid software is distributed under GPLv2+ license and includes
 * contributions from numerous individuals and organizations.
 * Please see the COPYING and CONTRIBUTORS files for details.
 */

#include "squid.h"
#include "unitTestMain.h"

#include <cassert>
#include <cppunit/extensions/HelperMacros.h>

/* Being a C library code it is best bodily included and tested with C++ type-safe techniques. */
#include "lib/rfc1738.c"

/**
 * Test the URL coder RFC 1738 Engine
 */
class TestRfc1738 : public CPPUNIT_NS::TestFixture
{
    CPPUNIT_TEST_SUITE(TestRfc1738);
    CPPUNIT_TEST(testUrlDecode);
    CPPUNIT_TEST(testUrlEncode);

    CPPUNIT_TEST(PercentZeroNullDecoding);
    CPPUNIT_TEST_SUITE_END();

public:
protected:
    void testUrlDecode();
    void testUrlEncode();

    // bugs.
    void PercentZeroNullDecoding();
};
CPPUNIT_TEST_SUITE_REGISTRATION(TestRfc1738);

/* Regular Format de-coding tests */
void TestRfc1738::testUrlDecode()
{
    char *unescaped_str;

    /* regular URL-path */
    unescaped_str = xstrdup("%2Fdata%2Fsource%2Fpath");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "/data/source/path",18)==0);
    xfree(unescaped_str);

    /* path in full URL */
    unescaped_str = xstrdup("http://foo.invalid%2Fdata%2Fsource%2Fpath");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "http://foo.invalid/data/source/path",36)==0);
    xfree(unescaped_str);

// TODO query string...

    /* Newline %0A encoded */
    unescaped_str = xstrdup("w%0Ard");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w\nrd",5)==0);
    xfree(unescaped_str);

    /* Handle Un-encoded % */
    unescaped_str = xstrdup("w%rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%rd",5)==0);
    xfree(unescaped_str);

    /* Handle encoded % */
    unescaped_str = xstrdup("w%%rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%rd",5)==0);
    xfree(unescaped_str);

    /* Handle mixed-encoded % */
    unescaped_str = xstrdup("w%%%rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%%rd",6)==0);
    xfree(unescaped_str);

    /* A corrupt string */
    unescaped_str = xstrdup("Bad String %1");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "Bad String %1",14)==0);
    xfree(unescaped_str);

    /* A partly corrupt string */
    unescaped_str = xstrdup("Bad String %1A%3");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "Bad String \032%3",15)==0);
    xfree(unescaped_str);

    /* A non corrupt string */
    unescaped_str = xstrdup("Good String %1A");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "Good String \032",14)==0);
    xfree(unescaped_str);
}

/**
 * Public API is formed of a triplet of encode functions mapping to the rfc1738_do_encode() engine.
 *
 * Flags:
 * rfc1738_escape == 0
 * rfc1738_escape_unescaped == -1
 * rfc1738_escape_part == 1
 */
void TestRfc1738::testUrlEncode()
{
    char *result;

    /* TEST: Escaping only unsafe characters */

    /* regular URL (no encoding needed) */
    result = rfc1738_do_escape("http://foo.invalid/data/source/path", RFC1738_ESCAPE_UNSAFE);
    CPPUNIT_ASSERT(memcmp(result, "http://foo.invalid/data/source/path",36)==0);

    /* long string of unsafe # characters */
    result = rfc1738_do_escape("################ ################ ################ ################ ################ ################ ################ ################", RFC1738_ESCAPE_UNSAFE);
    CPPUNIT_ASSERT(memcmp(result, "%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%20%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23%23",406)==0);

    /* TEST: escaping only reserved characters */

    /* regular URL (full encoding requested) */
    result = rfc1738_do_escape("http://foo.invalid/data/source/path", RFC1738_ESCAPE_RESERVED);
    CPPUNIT_ASSERT(memcmp(result, "http%3A%2F%2Ffoo.invalid%2Fdata%2Fsource%2Fpath",48)==0);

    /* regular path (encoding wanted for ALL special chars) */
    result = rfc1738_do_escape("/data/source/path", RFC1738_ESCAPE_RESERVED);
    CPPUNIT_ASSERT(memcmp(result, "%2Fdata%2Fsource%2Fpath",24)==0);

    /* TEST: safety-escaping a string already partially escaped */

    /* escaping of dangerous characters in a partially escaped string */
    result = rfc1738_do_escape("http://foo.invalid/data%2Fsource[]", RFC1738_ESCAPE_UNESCAPED);
    CPPUNIT_ASSERT(memcmp(result, "http://foo.invalid/data%2Fsource%5B%5D",39)==0);

    /* escaping of hexadecimal 0xFF characters in a partially escaped string */
    result = rfc1738_do_escape("http://foo.invalid/data%2Fsource\xFF\xFF", RFC1738_ESCAPE_UNESCAPED);
    CPPUNIT_ASSERT(memcmp(result, "http://foo.invalid/data%2Fsource%FF%FF",39)==0);

}

/** SECURITY BUG TESTS: avoid null truncation attacks by skipping %00 bytes */
void TestRfc1738::PercentZeroNullDecoding()
{
    char *unescaped_str;

    /* Attack with %00 encoded NULL */
    unescaped_str = xstrdup("w%00rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%00rd",7)==0);
    xfree(unescaped_str);

    /* Attack with %0 encoded NULL */
    unescaped_str = xstrdup("w%0rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%0rd",6)==0);
    xfree(unescaped_str);

    /* Handle '0' bytes embedded in encoded % */
    unescaped_str = xstrdup("w%%00%rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%00%rd",8)==0);
    xfree(unescaped_str);

    /* Handle NULL bytes with encoded % */
    unescaped_str = xstrdup("w%%%00%rd");
    rfc1738_unescape(unescaped_str);
    CPPUNIT_ASSERT(memcmp(unescaped_str, "w%%00%rd",9)==0);
    xfree(unescaped_str);
}

int
main(int argc, char *argv[])
{
    return TestProgram().run(argc, argv);
}

