From f24ec48b27d4cb8dd0555840206dedbb45f1423c Mon Sep 17 00:00:00 2001 From: Dirk Engling Date: Sat, 24 Oct 2026 01:17:50 +0200 Subject: Add a unit test harness and a first set of tests make check builds tests/runner from the modules that link without the io subsystem and runs it. --- tests/t_scan_urlencoded_query.c | 274 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 274 insertions(+) create mode 100644 tests/t_scan_urlencoded_query.c (limited to 'tests/t_scan_urlencoded_query.c') diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_scan_urlencoded_query.c new file mode 100644 index 0000000..4c1ef23 --- /dev/null +++ b/tests/t_scan_urlencoded_query.c @@ -0,0 +1,274 @@ +/* This software was written by Dirk Engling + It is considered beerware. Prost. Skol. Cheers or whatever. + + $id$ */ + +/* System */ +#include + +/* Opentracker */ +#include "scan_urlencoded_query.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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 9); + dest[n] = 0; + CHECK_STR(dest, "info_hash"); + + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + dest[n] = 0; + CHECK_STR(dest, "abc"); + + n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, 4); + dest[n] = 0; + CHECK_STR(dest, "port"); + + n = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM); + CHECK_INT(n, -2); + + p = space; + n = scan_urlencoded_query(&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 = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE); + CHECK_INT(n, 3); + CHECK_INT(*p, 0); + n = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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 = scan_urlencoded_query(&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"); + scan_urlencoded_skipvalue(&p); + CHECK_STR(p, "next=1"); + + test_case("skipvalue leaves a hard terminator in place"); + p = bare; + scan_urlencoded_skipvalue(&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(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), 2); + + test_case("an unknown keyword returns -3"); + p = nomatch; + CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3); + + test_case("a prefix of a keyword does not match it"); + p = prefix; + CHECK_INT(scan_find_keywords(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); +} + +void test_scan_urlencoded_query(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(); +} -- cgit v1.3