From 2cb9b191b66a69aba15ebdefe8cd4bdd962e5b2b Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sat, 24 Oct 2026 05:49:51 +0200 Subject: Add tests for the url escaper --- tests/t_scan_urlencoded_query.c | 107 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_scan_urlencoded_query.c index 4c1ef23..6fa34e0 100644 --- a/tests/t_scan_urlencoded_query.c +++ b/tests/t_scan_urlencoded_query.c @@ -254,6 +254,106 @@ static void case_fixed_int_bounded(void) { CHECK_INT(value, 123); } + +/* The encoding direction. Which bytes survive unescaped is fixed by what + existing parsers of the urlencoded full scrape expect, so these cases pin + the set rather than describe a preference. */ + +static void case_fmt_passes_safe_bytes(void) { + const uint8_t src[] = "aZ09$-./=_~"; + char dest[64]; + size_t n; + + test_case("alphanumerics and the safe punctuation pass through"); + n = fmt_urlencoded(dest, src, 11); + dest[n] = 0; + CHECK_INT(n, 11); + CHECK_STR(dest, "aZ09$-./=_~"); +} + +static void case_fmt_escapes_the_rest(void) { + const uint8_t src[] = {' ', '%', '&', '+', ':', '?'}; + char dest[64]; + size_t n; + + test_case("everything else becomes a percent escape"); + n = fmt_urlencoded(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_INT(n, 18); + CHECK_STR(dest, "%20%25%26%2b%3a%3f"); +} + +static void case_fmt_hex_is_lower_case(void) { + const uint8_t src[] = {0x00, 0xab, 0xff}; + char dest[64]; + size_t n; + + test_case("escapes use lower case hex digits"); + n = fmt_urlencoded(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_INT(n, 9); + CHECK_STR(dest, "%00%ab%ff"); +} + +static void case_fmt_length_only(void) { + const uint8_t src[] = {'a', 0xff, 'b'}; + + test_case("a null destination reports the length without writing"); + CHECK_INT(fmt_urlencoded(NULL, src, sizeof(src)), 5); + CHECK_INT(fmt_urlencoded(NULL, src, 0), 0); +} + +static void case_fmt_zero_length(void) { + char dest[4]; + + test_case("a zero length input writes nothing"); + memset(dest, 0x5a, sizeof(dest)); + CHECK_INT(fmt_urlencoded(dest, (const uint8_t *)"abc", 0), 0); + CHECK_MEM(dest, "\x5a\x5a\x5a\x5a", 4); +} + +static void case_fmt_worst_case_fits(void) { + uint8_t src[20]; + char dest[61]; + size_t i, n; + + test_case("a hash of entirely unsafe bytes needs three times its length"); + for (i = 0; i < sizeof(src); ++i) + src[i] = 0xff; + n = fmt_urlencoded(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_INT(n, 60); + for (i = 0; i < 20; ++i) + if (memcmp(dest + 3 * i, "%ff", 3)) { + test_failf(__FILE__, __LINE__, "escape %zu is wrong", i); + return; + } + ++test_checks; +} + +static void case_fmt_decoder_disagrees_on_dollar(void) { + /* Documenting a known asymmetry rather than a wish. Of the bytes the + encoder passes through unescaped, '$' is the only one the decoder then + refuses, and it refuses the whole string rather than stopping short. + Output of the urlencoded full scrape therefore does not round trip + through scan_urlencoded_query when a hash contains 0x24. Harmless today + because only third party tooling reads that output. */ + const uint8_t src[] = {'a', '$', 'b'}; + char dest[16]; + char scanned[16]; + char *p = dest; + size_t n; + ssize_t got; + + test_case("the encoder passes '$' but the decoder rejects it"); + n = fmt_urlencoded(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_STR(dest, "a$b"); + + got = scan_urlencoded_query(&p, scanned, SCAN_SEARCHPATH_VALUE); + CHECK_INT(got, -1); +} + void test_scan_urlencoded_query(void) { case_path(); case_param_and_value(); @@ -271,4 +371,11 @@ void test_scan_urlencoded_query(void) { case_find_keywords(); case_fixed_int(); case_fixed_int_bounded(); + case_fmt_passes_safe_bytes(); + case_fmt_escapes_the_rest(); + case_fmt_hex_is_lower_case(); + case_fmt_length_only(); + case_fmt_zero_length(); + case_fmt_worst_case_fits(); + case_fmt_decoder_disagrees_on_dollar(); } -- cgit v1.3