From 6837c5ac80e3624c6dc3a82d03e50d68b2f5a989 Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Mon, 26 Oct 2026 17:13:36 +0100 Subject: Second run of making variable and function names more readable, urlencoder now matches overall naming scheme --- tests/t_urlencoded.c | 372 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 tests/t_urlencoded.c (limited to 'tests/t_urlencoded.c') diff --git a/tests/t_urlencoded.c b/tests/t_urlencoded.c new file mode 100644 index 0000000..1239e6b --- /dev/null +++ b/tests/t_urlencoded.c @@ -0,0 +1,372 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include + +/* Opentracker */ +#include "ot_urlencoded.h" +#include "test.h" + +/* The scanner returns the number of bytes written to the destination, or + -1 for a parse error and -2 for a hard terminator encountered before any + output was produced. It advances the source pointer past the separator it + stopped on, except for hard terminators and the '&' ending a parameter, + which it leaves for the next call to see. */ + +static void case_path(void) { + char query[] = "/announce?info_hash=A"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("path stops at the question mark"); + n = urlencoded_parse(&p, dest, SCAN_PATH); + CHECK_INT(n, 9); + dest[n] = 0; + CHECK_STR(dest, "/announce"); + CHECK_INT(*p, 'i'); +} + +static void case_param_and_value(void) { + char query[] = "info_hash=abc&port=80"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("parameter stops at '=', value stops at '&'"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 9); + dest[n] = 0; + CHECK_STR(dest, "info_hash"); + + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "abc"); + + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 4); + dest[n] = 0; + CHECK_STR(dest, "port"); + + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 2); + dest[n] = 0; + CHECK_STR(dest, "80"); +} + +static void case_percent_decoding(void) { + char query[] = "%41%42%43&x=1"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("percent escapes decode"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + CHECK_MEM(dest, "ABC", 3); +} + +static void case_binary_info_hash(void) { + /* The twenty byte info_hash is what the announce path really decodes */ + char query[] = "%00%01%02%ff%fe%7f%80ABCDEFGHIJKLM&x=1"; + unsigned char want[] = {0x00, 0x01, 0x02, 0xff, 0xfe, 0x7f, 0x80, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'}; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("twenty byte binary value survives decoding"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 20); + CHECK_MEM(dest, want, 20); +} + +static void case_in_place(void) { + char query[] = "%41%42&x=1"; + char *p = query; + ssize_t n; + + test_case("decoding in place over the source is safe"); + n = urlencoded_parse(&p, query, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 2); + CHECK_MEM(query, "AB", 2); +} + +static void case_bad_escape(void) { + char query[] = "%zz"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("a non hexadecimal escape is a parse error"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_truncated_escape(void) { + char query[] = "%4"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an escape cut short by the terminator is a parse error"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_terminator(void) { + char empty[] = ""; + char space[] = " rest"; + char dest[64]; + char *p = empty; + ssize_t n; + + test_case("a hard terminator before any output reports end of input"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, -2); + + p = space; + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, -2); +} + +static void case_terminator_after_output(void) { + char query[] = "abc"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("a hard terminator after output is left for the next call"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + CHECK_INT(*p, 0); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -2); +} + +/* The character matrix makes '?' terminate a path only, and '=' terminate a + path or a parameter only. Both are ordinary characters inside a value, so a + value is free to contain a nested query string. */ + +static void case_separators_are_contextual(void) { + char question[] = "a?b&x=1"; + char equals[] = "a=b&x=1"; + char dest[64]; + char *p = question; + ssize_t n; + + test_case("a question mark is literal inside a value"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "a?b"); + + test_case("an equals sign is literal inside a value"); + p = equals; + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "a=b"); +} + +static void case_ampersand_in_path(void) { + char query[] = "/path&x"; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an ampersand in a path is a parse error"); + n = urlencoded_parse(&p, dest, SCAN_PATH); + CHECK_INT(n, -1); +} + +static void case_raw_high_byte(void) { + char query[] = {'a', 'b', (char)0x80, 0}; + char dest[64]; + char *p = query; + ssize_t n; + + test_case("an undecoded byte above ASCII is a parse error"); + n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, -1); +} + +static void case_skipvalue(void) { + char query[] = "abc&next=1"; + char bare[] = "abc"; + char *p = query; + + test_case("skipvalue steps over the value and its separator"); + urlencoded_skip_value(&p); + CHECK_STR(p, "next=1"); + + test_case("skipvalue leaves a hard terminator in place"); + p = bare; + urlencoded_skip_value(&p); + CHECK_INT(*p, 0); +} + +static void case_find_keywords(void) { + static const ot_keywords keywords[] = {{"port", 1}, {"left", 2}, {"compact", 3}, {NULL, -3}}; + char match[] = "left=0"; + char nomatch[] = "bogus=1"; + char prefix[] = "por=1"; + char *p = match; + + test_case("a known keyword returns its value"); + CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); + + test_case("an unknown keyword returns -3"); + p = nomatch; + CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); + + test_case("a prefix of a keyword does not match it"); + p = prefix; + CHECK_INT(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); +} + +static void case_fixed_int(void) { + char positive[] = "123"; + char negative[] = "-45"; + char trailing[] = "12a"; + int value; + + test_case("scan_fixed_int returns the count of unparsed bytes"); + CHECK_INT(scan_fixed_int(positive, 3, &value), 0); + CHECK_INT(value, 123); + + CHECK_INT(scan_fixed_int(negative, 3, &value), 0); + CHECK_INT(value, -45); + + CHECK_INT(scan_fixed_int(trailing, 3, &value), 1); + CHECK_INT(value, 12); +} + +static void case_fixed_int_bounded(void) { + char digits[] = "12345"; + int value; + + test_case("scan_fixed_int honours the length it is given"); + CHECK_INT(scan_fixed_int(digits, 3, &value), 0); + 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 = urlencoded_format(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 = urlencoded_format(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 = urlencoded_format(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_INT(n, 9); + CHECK_STR(dest, "%00%ab%ff"); +} + +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(urlencoded_format(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 = urlencoded_format(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 urlencoded_parse 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 = urlencoded_format(dest, src, sizeof(src)); + dest[n] = 0; + CHECK_STR(dest, "a$b"); + + got = urlencoded_parse(&p, scanned, SCAN_SEARCHPATH_VALUE); + CHECK_INT(got, -1); +} + +void test_urlencoded(void) { + case_path(); + case_param_and_value(); + case_percent_decoding(); + case_binary_info_hash(); + case_in_place(); + case_bad_escape(); + case_truncated_escape(); + case_terminator(); + case_terminator_after_output(); + case_separators_are_contextual(); + case_ampersand_in_path(); + case_raw_high_byte(); + case_skipvalue(); + 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_zero_length(); + case_fmt_worst_case_fits(); + case_fmt_decoder_disagrees_on_dollar(); +} -- cgit v1.3