summaryrefslogtreecommitdiff
path: root/tests/t_urlencoded.c
diff options
context:
space:
mode:
Diffstat (limited to 'tests/t_urlencoded.c')
-rw-r--r--tests/t_urlencoded.c343
1 files changed, 343 insertions, 0 deletions
diff --git a/tests/t_urlencoded.c b/tests/t_urlencoded.c
new file mode 100644
index 0000000..6e0ee31
--- /dev/null
+++ b/tests/t_urlencoded.c
@@ -0,0 +1,343 @@
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 "ot_urlencoded.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 = urlencoded_parse(&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 = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM);
41 CHECK_INT(n, 9);
42 dest[n] = 0;
43 CHECK_STR(dest, "info_hash");
44
45 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE);
46 CHECK_INT(n, 3);
47 dest[n] = 0;
48 CHECK_STR(dest, "abc");
49
50 n = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM);
51 CHECK_INT(n, 4);
52 dest[n] = 0;
53 CHECK_STR(dest, "port");
54
55 n = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_PARAM);
129 CHECK_INT(n, -2);
130
131 p = space;
132 n = urlencoded_parse(&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 = urlencoded_parse(&p, dest, SCAN_SEARCHPATH_VALUE);
144 CHECK_INT(n, 3);
145 CHECK_INT(*p, 0);
146 n = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 = urlencoded_parse(&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 urlencoded_skip_value(&p);
204 CHECK_STR(p, "next=1");
205
206 test_case("skipvalue leaves a hard terminator in place");
207 p = bare;
208 urlencoded_skip_value(&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(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), 2);
221
222 test_case("an unknown keyword returns -3");
223 p = nomatch;
224 CHECK_INT(urlencoded_match_keyword(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(urlencoded_match_keyword(keywords, &p, SCAN_SEARCHPATH_PARAM), -3);
229}
230
231/* The encoding direction. Which bytes survive unescaped is fixed by what
232 existing parsers of the urlencoded full scrape expect, so these cases pin
233 the set rather than describe a preference. */
234
235static void case_fmt_passes_safe_bytes(void) {
236 const uint8_t src[] = "aZ09$-./=_~";
237 char dest[64];
238 size_t n;
239
240 test_case("alphanumerics and the safe punctuation pass through");
241 n = urlencoded_format(dest, src, 11);
242 dest[n] = 0;
243 CHECK_INT(n, 11);
244 CHECK_STR(dest, "aZ09$-./=_~");
245}
246
247static void case_fmt_escapes_the_rest(void) {
248 const uint8_t src[] = {' ', '%', '&', '+', ':', '?'};
249 char dest[64];
250 size_t n;
251
252 test_case("everything else becomes a percent escape");
253 n = urlencoded_format(dest, src, sizeof(src));
254 dest[n] = 0;
255 CHECK_INT(n, 18);
256 CHECK_STR(dest, "%20%25%26%2b%3a%3f");
257}
258
259static void case_fmt_hex_is_lower_case(void) {
260 const uint8_t src[] = {0x00, 0xab, 0xff};
261 char dest[64];
262 size_t n;
263
264 test_case("escapes use lower case hex digits");
265 n = urlencoded_format(dest, src, sizeof(src));
266 dest[n] = 0;
267 CHECK_INT(n, 9);
268 CHECK_STR(dest, "%00%ab%ff");
269}
270
271static void case_fmt_zero_length(void) {
272 char dest[4];
273
274 test_case("a zero length input writes nothing");
275 memset(dest, 0x5a, sizeof(dest));
276 CHECK_INT(urlencoded_format(dest, (const uint8_t *)"abc", 0), 0);
277 CHECK_MEM(dest, "\x5a\x5a\x5a\x5a", 4);
278}
279
280static void case_fmt_worst_case_fits(void) {
281 uint8_t src[20];
282 char dest[61];
283 size_t i, n;
284
285 test_case("a hash of entirely unsafe bytes needs three times its length");
286 for (i = 0; i < sizeof(src); ++i)
287 src[i] = 0xff;
288 n = urlencoded_format(dest, src, sizeof(src));
289 dest[n] = 0;
290 CHECK_INT(n, 60);
291 for (i = 0; i < 20; ++i)
292 if (memcmp(dest + 3 * i, "%ff", 3)) {
293 test_failf(__FILE__, __LINE__, "escape %zu is wrong", i);
294 return;
295 }
296 ++test_checks;
297}
298
299static void case_fmt_decoder_disagrees_on_dollar(void) {
300 /* Documenting a known asymmetry rather than a wish. Of the bytes the
301 encoder passes through unescaped, '$' is the only one the decoder then
302 refuses, and it refuses the whole string rather than stopping short.
303 Output of the urlencoded full scrape therefore does not round trip
304 through urlencoded_parse when a hash contains 0x24. Harmless today
305 because only third party tooling reads that output. */
306 const uint8_t src[] = {'a', '$', 'b'};
307 char dest[16];
308 char scanned[16];
309 char *p = dest;
310 size_t n;
311 ssize_t got;
312
313 test_case("the encoder passes '$' but the decoder rejects it");
314 n = urlencoded_format(dest, src, sizeof(src));
315 dest[n] = 0;
316 CHECK_STR(dest, "a$b");
317
318 got = urlencoded_parse(&p, scanned, SCAN_SEARCHPATH_VALUE);
319 CHECK_INT(got, -1);
320}
321
322void test_urlencoded(void) {
323 case_path();
324 case_param_and_value();
325 case_percent_decoding();
326 case_binary_info_hash();
327 case_in_place();
328 case_bad_escape();
329 case_truncated_escape();
330 case_terminator();
331 case_terminator_after_output();
332 case_separators_are_contextual();
333 case_ampersand_in_path();
334 case_raw_high_byte();
335 case_skipvalue();
336 case_find_keywords();
337 case_fmt_passes_safe_bytes();
338 case_fmt_escapes_the_rest();
339 case_fmt_hex_is_lower_case();
340 case_fmt_zero_length();
341 case_fmt_worst_case_fits();
342 case_fmt_decoder_disagrees_on_dollar();
343}