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/runner.c | 2 +- tests/t_hex.c | 28 +-- tests/t_iovec.c | 20 +-- tests/t_scan_urlencoded_query.c | 372 ---------------------------------------- tests/t_urlencoded.c | 372 ++++++++++++++++++++++++++++++++++++++++ tests/test.h | 2 +- 6 files changed, 398 insertions(+), 398 deletions(-) delete mode 100644 tests/t_scan_urlencoded_query.c create mode 100644 tests/t_urlencoded.c (limited to 'tests') diff --git a/tests/runner.c b/tests/runner.c index 335d83d..9e5c99f 100644 --- a/tests/runner.c +++ b/tests/runner.c @@ -14,7 +14,7 @@ struct module { void (*run)(void); }; -static const struct module modules[] = {{"scan_urlencoded_query", test_scan_urlencoded_query}, +static const struct module modules[] = {{"ot_urlencoded", test_urlencoded}, {"ot_iovec", test_iovec}, {"ot_vector", test_vector}, {"ot_random", test_random}, diff --git a/tests/t_hex.c b/tests/t_hex.c index 7cce8ac..a761737 100644 --- a/tests/t_hex.c +++ b/tests/t_hex.c @@ -18,7 +18,7 @@ static void case_encode(void) { test_case("a hash encodes to forty upper case digits and a terminator"); memset(dest, 0x7f, sizeof(dest)); - CHECK_PTR(hex_fromhash(dest, sample), dest); + CHECK_PTR(hex_from_hash(dest, sample), dest); CHECK_STR(dest, "000123456789ABCDEFFF10203040506070809" "0A0"); CHECK_INT(strlen(dest), 40); CHECK_INT((unsigned char)dest[41], 0x7f); @@ -28,7 +28,7 @@ static void case_decode(void) { ot_hash hash; test_case("forty digits decode back to the hash"); - CHECK(hex_tohash(hash, "000123456789ABCDEFFF10203040506070809" "0A0")); + CHECK(hex_to_hash(hash, "000123456789ABCDEFFF10203040506070809" "0A0")); CHECK_MEM(hash, sample, sizeof(sample)); } @@ -41,8 +41,8 @@ static void case_round_trip(void) { for (i = 0; i < 256; ++i) { uint8_t probe[20]; memset(probe, (uint8_t)i, sizeof(probe)); - hex_fromhash(text, probe); - if (!hex_tohash(hash, text) || memcmp(hash, probe, sizeof(probe))) { + hex_from_hash(text, probe); + if (!hex_to_hash(hash, text) || memcmp(hash, probe, sizeof(probe))) { test_failf(__FILE__, __LINE__, "round trip failed for byte value %zu", i); return; } @@ -54,8 +54,8 @@ static void case_decode_is_case_insensitive(void) { ot_hash upper, lower; test_case("lower case digits decode the same as upper case"); - CHECK(hex_tohash(upper, "ABCDEF0123456789ABCDEF0123456789ABCDEF01")); - CHECK(hex_tohash(lower, "abcdef0123456789abcdef0123456789abcdef01")); + CHECK(hex_to_hash(upper, "ABCDEF0123456789ABCDEF0123456789ABCDEF01")); + CHECK(hex_to_hash(lower, "abcdef0123456789abcdef0123456789abcdef01")); CHECK_MEM(lower, upper, sizeof(ot_hash)); } @@ -63,18 +63,18 @@ static void case_rejects_non_digits(void) { ot_hash hash; test_case("a non hex character anywhere rejects the whole string"); - CHECK_INT(hex_tohash(hash, "g00123456789ABCDEFFF102030405060708090A0"), 0); - CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF102030405060708090Ag"), 0); - CHECK_INT(hex_tohash(hash, "000123456789ABCDEF:F102030405060708090A"), 0); + CHECK_INT(hex_to_hash(hash, "g00123456789ABCDEFFF102030405060708090A0"), 0); + CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF102030405060708090Ag"), 0); + CHECK_INT(hex_to_hash(hash, "000123456789ABCDEF:F102030405060708090A"), 0); } static void case_rejects_short_input(void) { ot_hash hash; test_case("a string shorter than forty digits is rejected"); - CHECK_INT(hex_tohash(hash, ""), 0); - CHECK_INT(hex_tohash(hash, "00"), 0); - CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF102030405060708090A"), 0); + CHECK_INT(hex_to_hash(hash, ""), 0); + CHECK_INT(hex_to_hash(hash, "00"), 0); + CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF102030405060708090A"), 0); } static void case_leaves_hash_untouched_on_failure(void) { @@ -82,7 +82,7 @@ static void case_leaves_hash_untouched_on_failure(void) { test_case("a rejected string leaves the destination untouched"); memset(hash, 0x5a, sizeof(hash)); - CHECK_INT(hex_tohash(hash, "000123456789ABCDEFFF10203040506070809XX0"), 0); + CHECK_INT(hex_to_hash(hash, "000123456789ABCDEFFF10203040506070809XX0"), 0); CHECK_MEM(hash, "\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a\x5a", sizeof(hash)); } @@ -96,7 +96,7 @@ static void case_stops_at_the_terminator(void) { memcpy(guarded, "0012", 4); guarded[4] = 0; memset(guarded + 5, 'f', 3); - CHECK_INT(hex_tohash(hash, guarded), 0); + CHECK_INT(hex_to_hash(hash, guarded), 0); } static void case_isdigit(void) { diff --git a/tests/t_iovec.c b/tests/t_iovec.c index 89f3fda..2553141 100644 --- a/tests/t_iovec.c +++ b/tests/t_iovec.c @@ -18,13 +18,13 @@ static void case_increase_and_length(void) { void *first, *second; test_case("increase appends an entry and length is their sum"); - first = iovec_increase(&entries, &vector, 100); + first = iovec_new_chunk(&entries, &vector, 100); CHECK(first != NULL); CHECK_INT(entries, 1); CHECK_INT(vector[0].iov_len, 100); CHECK_PTR(vector[0].iov_base, first); - second = iovec_increase(&entries, &vector, 50); + second = iovec_new_chunk(&entries, &vector, 50); CHECK(second != NULL); CHECK_INT(entries, 2); CHECK_INT(vector[1].iov_len, 50); @@ -38,8 +38,8 @@ static void case_free_resets(void) { struct iovec *vector = NULL; test_case("free releases the entries and resets the vector"); - CHECK(iovec_increase(&entries, &vector, 10) != NULL); - CHECK(iovec_increase(&entries, &vector, 10) != NULL); + CHECK(iovec_new_chunk(&entries, &vector, 10) != NULL); + CHECK(iovec_new_chunk(&entries, &vector, 10) != NULL); iovec_free(&entries, &vector); CHECK_INT(entries, 0); CHECK_PTR(vector, NULL); @@ -52,12 +52,12 @@ static void case_fixlast_shrinks(void) { char *block; test_case("fixlast trims the last entry to the bytes actually used"); - CHECK(iovec_increase(&entries, &vector, 64) != NULL); - block = iovec_increase(&entries, &vector, 1000); + CHECK(iovec_new_chunk(&entries, &vector, 64) != NULL); + block = iovec_new_chunk(&entries, &vector, 1000); CHECK(block != NULL); memset(block, 'x', 20); - iovec_fixlast(&entries, &vector, block + 20); + iovec_trim_last(&entries, &vector, block + 20); CHECK_INT(entries, 2); CHECK_INT(vector[1].iov_len, 20); CHECK_INT(iovec_length(&entries, (const struct iovec **)&vector), 84); @@ -71,7 +71,7 @@ static void case_fixlast_on_empty(void) { struct iovec *vector = NULL; test_case("fixlast on an empty vector does nothing"); - iovec_fixlast(&entries, &vector, NULL); + iovec_trim_last(&entries, &vector, NULL); CHECK_INT(entries, 0); CHECK_PTR(vector, NULL); } @@ -103,9 +103,9 @@ static void case_fix_increase(void) { char *block; test_case("fix_increase trims the previous entry and adds a new one"); - block = iovec_increase(&entries, &vector, 500); + block = iovec_new_chunk(&entries, &vector, 500); CHECK(block != NULL); - block = iovec_fix_increase_or_free(&entries, &vector, block + 30, 200); + block = iovec_next_chunk(&entries, &vector, block + 30, 200); CHECK(block != NULL); CHECK_INT(entries, 2); CHECK_INT(vector[0].iov_len, 30); diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_scan_urlencoded_query.c deleted file mode 100644 index cb6c982..0000000 --- a/tests/t_scan_urlencoded_query.c +++ /dev/null @@ -1,372 +0,0 @@ -/* 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); -} - - -/* 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 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 = urlencoded_format(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(); - 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(); -} 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(); +} diff --git a/tests/test.h b/tests/test.h index bc8c3bd..cebd08a 100644 --- a/tests/test.h +++ b/tests/test.h @@ -76,7 +76,7 @@ extern unsigned long test_failures; } while (0) /* One entry point per module under test */ -void test_scan_urlencoded_query(void); +void test_urlencoded(void); void test_iovec(void); void test_vector(void); void test_random(void); -- cgit v1.3