/* 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 = 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(); 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_length_only(); case_fmt_zero_length(); case_fmt_worst_case_fits(); case_fmt_decoder_disagrees_on_dollar(); }