summaryrefslogtreecommitdiff
path: root/tests/t_scan_urlencoded_query.c
blob: 6fa34e0c9d631b906f69612b12b2f8e4a4b0aca5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
/* This software was written by Dirk Engling <erdgeist@erdgeist.org>
   It is considered beerware. Prost. Skol. Cheers or whatever.

   $id$ */

/* System */
#include <string.h>

/* 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();
}