summaryrefslogtreecommitdiff
path: root/tests/t_scan_urlencoded_query.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/t_scan_urlencoded_query.c')
-rw-r--r--tests/t_scan_urlencoded_query.c372
1 files changed, 372 insertions, 0 deletions
diff --git a/tests/t_scan_urlencoded_query.c b/tests/t_scan_urlencoded_query.c
new file mode 100644
index 0000000..f660ea3
--- /dev/null
+++ b/tests/t_scan_urlencoded_query.c
@@ -0,0 +1,372 @@
1/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
2 It is considered beerware. Prost. Skol. Cheers or whatever.
3
4 $id$ */
5
6/* System */
7#include <string.h>
8
9/* Opentracker */
10#include "scan_urlencoded_query.h"
11#include "test.h"
12
13/* The scanner returns the number of bytes written to the destination, or
14 -1 for a parse error and -2 for a hard terminator encountered before any
15 output was produced. It advances the source pointer past the separator it
16 stopped on, except for hard terminators and the '&' ending a parameter,
17 which it leaves for the next call to see. */
18
19static void case_path(void) {
20 char query[] = "/announce?info_hash=A";
21 char dest[64];
22 char *p = query;
23 ssize_t n;
24
25 test_case("path stops at the question mark");
26 n = scan_urlencoded_query(&p, dest, SCAN_PATH);
27 CHECK_INT(n, 9);
28 dest[n] = 0;
29 CHECK_STR(dest, "/announce");
30 CHECK_INT(*p, 'i');
31}
32
33static void case_param_and_value(void) {
34 char query[] = "info_hash=abc&port=80";
35 char dest[64];
36 char *p = query;
37 ssize_t n;
38
39 test_case("parameter stops at '=', value stops at '&'");
40 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM);
41 CHECK_INT(n, 9);
42 dest[n] = 0;
43 CHECK_STR(dest, "info_hash");
44
45 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
46 CHECK_INT(n, 3);
47 dest[n] = 0;
48 CHECK_STR(dest, "abc");
49
50 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM);
51 CHECK_INT(n, 4);
52 dest[n] = 0;
53 CHECK_STR(dest, "port");
54
55 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
56 CHECK_INT(n, 2);
57 dest[n] = 0;
58 CHECK_STR(dest, "80");
59}
60
61static void case_percent_decoding(void) {
62 char query[] = "%41%42%43&x=1";
63 char dest[64];
64 char *p = query;
65 ssize_t n;
66
67 test_case("percent escapes decode");
68 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
69 CHECK_INT(n, 3);
70 CHECK_MEM(dest, "ABC", 3);
71}
72
73static void case_binary_info_hash(void) {
74 /* The twenty byte info_hash is what the announce path really decodes */
75 char query[] = "%00%01%02%ff%fe%7f%80ABCDEFGHIJKLM&x=1";
76 unsigned char want[] = {0x00, 0x01, 0x02, 0xff, 0xfe, 0x7f, 0x80, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M'};
77 char dest[64];
78 char *p = query;
79 ssize_t n;
80
81 test_case("twenty byte binary value survives decoding");
82 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
83 CHECK_INT(n, 20);
84 CHECK_MEM(dest, want, 20);
85}
86
87static void case_in_place(void) {
88 char query[] = "%41%42&x=1";
89 char *p = query;
90 ssize_t n;
91
92 test_case("decoding in place over the source is safe");
93 n = scan_urlencoded_query(&p, query, SCAN_SEARCHPATH_VALUE);
94 CHECK_INT(n, 2);
95 CHECK_MEM(query, "AB", 2);
96}
97
98static void case_bad_escape(void) {
99 char query[] = "%zz";
100 char dest[64];
101 char *p = query;
102 ssize_t n;
103
104 test_case("a non hexadecimal escape is a parse error");
105 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
106 CHECK_INT(n, -1);
107}
108
109static void case_truncated_escape(void) {
110 char query[] = "%4";
111 char dest[64];
112 char *p = query;
113 ssize_t n;
114
115 test_case("an escape cut short by the terminator is a parse error");
116 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
117 CHECK_INT(n, -1);
118}
119
120static void case_terminator(void) {
121 char empty[] = "";
122 char space[] = " rest";
123 char dest[64];
124 char *p = empty;
125 ssize_t n;
126
127 test_case("a hard terminator before any output reports end of input");
128 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM);
129 CHECK_INT(n, -2);
130
131 p = space;
132 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_PARAM);
133 CHECK_INT(n, -2);
134}
135
136static void case_terminator_after_output(void) {
137 char query[] = "abc";
138 char dest[64];
139 char *p = query;
140 ssize_t n;
141
142 test_case("a hard terminator after output is left for the next call");
143 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
144 CHECK_INT(n, 3);
145 CHECK_INT(*p, 0);
146 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
147 CHECK_INT(n, -2);
148}
149
150/* The character matrix makes '?' terminate a path only, and '=' terminate a
151 path or a parameter only. Both are ordinary characters inside a value, so a
152 value is free to contain a nested query string. */
153
154static void case_separators_are_contextual(void) {
155 char question[] = "a?b&x=1";
156 char equals[] = "a=b&x=1";
157 char dest[64];
158 char *p = question;
159 ssize_t n;
160
161 test_case("a question mark is literal inside a value");
162 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
163 CHECK_INT(n, 3);
164 dest[n] = 0;
165 CHECK_STR(dest, "a?b");
166
167 test_case("an equals sign is literal inside a value");
168 p = equals;
169 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
170 CHECK_INT(n, 3);
171 dest[n] = 0;
172 CHECK_STR(dest, "a=b");
173}
174
175static void case_ampersand_in_path(void) {
176 char query[] = "/path&x";
177 char dest[64];
178 char *p = query;
179 ssize_t n;
180
181 test_case("an ampersand in a path is a parse error");
182 n = scan_urlencoded_query(&p, dest, SCAN_PATH);
183 CHECK_INT(n, -1);
184}
185
186static void case_raw_high_byte(void) {
187 char query[] = {'a', 'b', (char)0x80, 0};
188 char dest[64];
189 char *p = query;
190 ssize_t n;
191
192 test_case("an undecoded byte above ASCII is a parse error");
193 n = scan_urlencoded_query(&p, dest, SCAN_SEARCHPATH_VALUE);
194 CHECK_INT(n, -1);
195}
196
197static void case_skipvalue(void) {
198 char query[] = "abc&next=1";
199 char bare[] = "abc";
200 char *p = query;
201
202 test_case("skipvalue steps over the value and its separator");
203 scan_urlencoded_skipvalue(&p);
204 CHECK_STR(p, "next=1");
205
206 test_case("skipvalue leaves a hard terminator in place");
207 p = bare;
208 scan_urlencoded_skipvalue(&p);
209 CHECK_INT(*p, 0);
210}
211
212static void case_find_keywords(void) {
213 static const ot_keywords keywords[] = {{"port", 1}, {"left", 2}, {"compact", 3}, {NULL, -3}};
214 char match[] = "left=0";
215 char nomatch[] = "bogus=1";
216 char prefix[] = "por=1";
217 char *p = match;
218
219 test_case("a known keyword returns its value");
220 CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), 2);
221
222 test_case("an unknown keyword returns -3");
223 p = nomatch;
224 CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3);
225
226 test_case("a prefix of a keyword does not match it");
227 p = prefix;
228 CHECK_INT(scan_find_keywords(keywords, &p, SCAN_SEARCHPATH_PARAM), -3);
229}
230
231static void case_fixed_int(void) {
232 char positive[] = "123";
233 char negative[] = "-45";
234 char trailing[] = "12a";
235 int value;
236
237 test_case("scan_fixed_int returns the count of unparsed bytes");
238 CHECK_INT(scan_fixed_int(positive, 3, &value), 0);
239 CHECK_INT(value, 123);
240
241 CHECK_INT(scan_fixed_int(negative, 3, &value), 0);
242 CHECK_INT(value, -45);
243
244 CHECK_INT(scan_fixed_int(trailing, 3, &value), 1);
245 CHECK_INT(value, 12);
246}
247
248static void case_fixed_int_bounded(void) {
249 char digits[] = "12345";
250 int value;
251
252 test_case("scan_fixed_int honours the length it is given");
253 CHECK_INT(scan_fixed_int(digits, 3, &value), 0);
254 CHECK_INT(value, 123);
255}
256
257
258/* The encoding direction. Which bytes survive unescaped is fixed by what
259 existing parsers of the urlencoded full scrape expect, so these cases pin
260 the set rather than describe a preference. */
261
262static void case_fmt_passes_safe_bytes(void) {
263 const uint8_t src[] = "aZ09$-./=_~";
264 char dest[64];
265 size_t n;
266
267 test_case("alphanumerics and the safe punctuation pass through");
268 n = fmt_urlencoded(dest, src, 11);
269 dest[n] = 0;
270 CHECK_INT(n, 11);
271 CHECK_STR(dest, "aZ09$-./=_~");
272}
273
274static void case_fmt_escapes_the_rest(void) {
275 const uint8_t src[] = {' ', '%', '&', '+', ':', '?'};
276 char dest[64];
277 size_t n;
278
279 test_case("everything else becomes a percent escape");
280 n = fmt_urlencoded(dest, src, sizeof(src));
281 dest[n] = 0;
282 CHECK_INT(n, 18);
283 CHECK_STR(dest, "%20%25%26%2b%3a%3f");
284}
285
286static void case_fmt_hex_is_lower_case(void) {
287 const uint8_t src[] = {0x00, 0xab, 0xff};
288 char dest[64];
289 size_t n;
290
291 test_case("escapes use lower case hex digits");
292 n = fmt_urlencoded(dest, src, sizeof(src));
293 dest[n] = 0;
294 CHECK_INT(n, 9);
295 CHECK_STR(dest, "%00%ab%ff");
296}
297
298static void case_fmt_zero_length(void) {
299 char dest[4];
300
301 test_case("a zero length input writes nothing");
302 memset(dest, 0x5a, sizeof(dest));
303 CHECK_INT(fmt_urlencoded(dest, (const uint8_t *)"abc", 0), 0);
304 CHECK_MEM(dest, "\x5a\x5a\x5a\x5a", 4);
305}
306
307static void case_fmt_worst_case_fits(void) {
308 uint8_t src[20];
309 char dest[61];
310 size_t i, n;
311
312 test_case("a hash of entirely unsafe bytes needs three times its length");
313 for (i = 0; i < sizeof(src); ++i)
314 src[i] = 0xff;
315 n = fmt_urlencoded(dest, src, sizeof(src));
316 dest[n] = 0;
317 CHECK_INT(n, 60);
318 for (i = 0; i < 20; ++i)
319 if (memcmp(dest + 3 * i, "%ff", 3)) {
320 test_failf(__FILE__, __LINE__, "escape %zu is wrong", i);
321 return;
322 }
323 ++test_checks;
324}
325
326static void case_fmt_decoder_disagrees_on_dollar(void) {
327 /* Documenting a known asymmetry rather than a wish. Of the bytes the
328 encoder passes through unescaped, '$' is the only one the decoder then
329 refuses, and it refuses the whole string rather than stopping short.
330 Output of the urlencoded full scrape therefore does not round trip
331 through scan_urlencoded_query when a hash contains 0x24. Harmless today
332 because only third party tooling reads that output. */
333 const uint8_t src[] = {'a', '$', 'b'};
334 char dest[16];
335 char scanned[16];
336 char *p = dest;
337 size_t n;
338 ssize_t got;
339
340 test_case("the encoder passes '$' but the decoder rejects it");
341 n = fmt_urlencoded(dest, src, sizeof(src));
342 dest[n] = 0;
343 CHECK_STR(dest, "a$b");
344
345 got = scan_urlencoded_query(&p, scanned, SCAN_SEARCHPATH_VALUE);
346 CHECK_INT(got, -1);
347}
348
349void test_scan_urlencoded_query(void) {
350 case_path();
351 case_param_and_value();
352 case_percent_decoding();
353 case_binary_info_hash();
354 case_in_place();
355 case_bad_escape();
356 case_truncated_escape();
357 case_terminator();
358 case_terminator_after_output();
359 case_separators_are_contextual();
360 case_ampersand_in_path();
361 case_raw_high_byte();
362 case_skipvalue();
363 case_find_keywords();
364 case_fixed_int();
365 case_fixed_int_bounded();
366 case_fmt_passes_safe_bytes();
367 case_fmt_escapes_the_rest();
368 case_fmt_hex_is_lower_case();
369 case_fmt_zero_length();
370 case_fmt_worst_case_fits();
371 case_fmt_decoder_disagrees_on_dollar();
372}