Line data Source code
1 : /*
2 : * SPDX-License-Identifier: MPL-2.0
3 : *
4 : * This Source Code Form is subject to the terms of the Mozilla Public
5 : * License, v. 2.0. If a copy of the MPL was not distributed with this
6 : * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 : *
8 : * Copyright 2024, 2025 MonetDB Foundation;
9 : * Copyright August 2008 - 2023 MonetDB B.V.;
10 : * Copyright 1997 - July 2008 CWI.
11 : */
12 :
13 : #include "monetdb_config.h"
14 :
15 : #include "murltest.h"
16 : #include "msettings.h"
17 : #include "stream.h"
18 :
19 : #include <assert.h>
20 : #include <ctype.h>
21 : #include <errno.h>
22 : #include <stdio.h>
23 : #include <stdlib.h>
24 : #include <string.h>
25 :
26 : // love it when a task is so straightforward I can use global variables!
27 : static int start_line = -1;
28 : static int nstarted = 0;
29 : static msettings *mp = NULL;
30 : static msettings_allocator allocator = NULL;
31 :
32 : static
33 446 : bool verify_roundtrip(const char *location)
34 : {
35 446 : const char ch = '*';
36 446 : char buffer[1000 + 1]; // + 1 canary byte
37 446 : memset(buffer, ch, sizeof(buffer));
38 446 : const size_t buffer_size = sizeof(buffer) - 1;
39 :
40 446 : size_t length = msettings_write_url(mp, buffer, buffer_size);
41 446 : if (length == 0) {
42 0 : fprintf(stderr, "%s: msettings_write_url returned 0\n", location);
43 0 : return false;
44 : }
45 446 : if (length > buffer_size - 1) {
46 0 : fprintf(stderr, "%s: Reconstructed the URL unexpectedly large: %zu\n", location, length);
47 0 : return false;
48 : }
49 446 : if (memchr(buffer, '\0', buffer_size) == NULL) {
50 0 : fprintf(stderr, "%s: msettings_write_url didn't NUL terminate the result\n", location);
51 0 : return false;
52 : }
53 446 : if (buffer[buffer_size] != ch) {
54 0 : fprintf(stderr, "%s: msettting_write_url wrote beyond the end of the buffer\n", location);
55 0 : return false;
56 : }
57 :
58 446 : msettings *tmp = msettings_create_with(allocator, NULL);
59 446 : if (tmp == NULL) {
60 0 : fprintf(stderr, "malloc failed\n");
61 0 : return false;
62 : }
63 446 : msettings_error err = msettings_parse_url(tmp, buffer);
64 446 : if (err) {
65 0 : fprintf(stderr, "%s: Reconstructed URL <%s> couldn't be parsed: %s", location, buffer, err);
66 0 : msettings_destroy(tmp);
67 0 : return false;
68 : }
69 :
70 : mparm parm;
71 : bool ok = true;
72 13826 : for (int i = 0; (parm = mparm_enumerate(i)) != MP_UNKNOWN; i++) {
73 13380 : if (parm == MP_IGNORE)
74 892 : continue;
75 12488 : char scratch1[100], scratch2[100];
76 12488 : const char *mp_val = msetting_as_string(mp, parm, scratch1, sizeof(scratch1));
77 12488 : const char *tmp_val = msetting_as_string(tmp, parm, scratch2, sizeof(scratch2));
78 12488 : if (strcmp(mp_val, tmp_val) != 0) {
79 0 : fprintf(
80 : stderr,
81 : "%s: setting %s: reconstructed value <%s> != <%s>\n",
82 : location, mparm_name(parm), tmp_val, mp_val);
83 0 : ok = false;
84 : }
85 : }
86 446 : msettings_destroy(tmp);
87 446 : if (!ok)
88 : return false;
89 :
90 : // check if rendering to a smaller buffer returns the same length
91 : // and writes a prefix of the original.
92 :
93 : assert(length > 0); // we checked this above
94 :
95 : char buffer2[sizeof(buffer)];
96 16178 : for (size_t shorter = length; shorter > 0; shorter--) {
97 15732 : memset(buffer2, ch, sizeof(buffer));
98 15732 : size_t n = msettings_write_url(mp, buffer2, shorter);
99 15732 : if (n != length) {
100 0 : fprintf(
101 : stderr,\
102 : "%s: writing to buffer of size %zu returns %zu, expected %zu\n",
103 : location, shorter, n, length);
104 0 : return false;
105 : }
106 15732 : char *first_nul = memchr(buffer2, '\0', shorter);
107 15732 : if (first_nul == NULL) {
108 0 : fprintf(stderr, "%s: truncated <%zu> msettings_write_url didn't NUL terminate\n", location, shorter);
109 0 : return false;
110 15732 : } else if (strncmp(buffer2, buffer, shorter - 1) != 0) {
111 0 : fprintf(stderr,
112 : "%s: truncated <%zu> msettings_write_url wrote <%s> which isn't a prefix of <%s>",
113 : location, shorter,
114 : buffer2, buffer
115 : );
116 0 : return false;
117 : }
118 15396886 : for (size_t i = shorter + 1; i < sizeof(buffer); i++) {
119 15381154 : if (buffer2[i] != ch) {
120 0 : fprintf(
121 : stderr,
122 : "%s: truncated <%zu> wsettings_write_url wrote beyond end of buffer (pos %zu)\n",
123 : location, shorter, i);
124 0 : return false;
125 : }
126 : }
127 : }
128 :
129 : return true;
130 : }
131 :
132 : static bool
133 6 : handle_parse_command(const char *location, char *url)
134 : {
135 6 : const char *errmsg = msettings_parse_url(mp, url);
136 6 : if (errmsg) {
137 0 : fprintf(stderr, "%s: %s\n", location, errmsg);
138 0 : return false;
139 : }
140 :
141 6 : return verify_roundtrip(location);
142 : }
143 :
144 : static bool
145 262 : handle_accept_command(const char *location, char *url)
146 : {
147 262 : const char *errmsg = msettings_parse_url(mp, url);
148 262 : if (errmsg) {
149 0 : fprintf(stderr, "%s: %s\n", location, errmsg);
150 0 : return false;
151 : }
152 :
153 262 : const char *msg = msettings_validate(mp);
154 262 : if (msg != NULL) {
155 0 : fprintf(stderr, "%s: URL invalid: %s\n", location, msg);
156 0 : return false;
157 : }
158 262 : return verify_roundtrip(location);
159 : }
160 :
161 : static bool
162 120 : handle_reject_command(const char *location, char *url)
163 : {
164 120 : const char *errmsg = msettings_parse_url(mp, url);
165 120 : if (errmsg)
166 : return true;
167 :
168 56 : if (msettings_validate(mp) != NULL) {
169 : return true;
170 : }
171 :
172 0 : fprintf(stderr, "%s: expected URL to be rejected.\n", location);
173 0 : return false;
174 : }
175 :
176 : static bool
177 260 : handle_set_command(const char *location, const char *key, const char *value)
178 : {
179 260 : msettings_error msg = msetting_set_named(mp, true, key, value);
180 260 : if (msg) {
181 0 : fprintf(stderr, "%s: %s\n", location, msg);
182 0 : return false;
183 : }
184 260 : if (msettings_validate(mp) == NULL)
185 178 : return verify_roundtrip(location);
186 : else
187 : return true;
188 : }
189 :
190 : static bool
191 354 : ensure_valid(const char *location) {
192 354 : const char *msg = msettings_validate(mp);
193 354 : if (msg == NULL)
194 : return true;
195 0 : fprintf(stderr, "%s: invalid parameter state: %s\n", location, msg);
196 0 : return false;
197 : }
198 :
199 : static bool
200 162 : expect_bool(const char *location, const mparm parm, bool (*extract)(const msettings*), const char *value)
201 : {
202 162 : int x = msetting_parse_bool(value);
203 162 : if (x < 0) {
204 0 : fprintf(stderr, "%s: syntax error: invalid bool '%s'\n", location, value);
205 : }
206 162 : bool b = x > 0;
207 :
208 162 : bool actual;
209 162 : if (extract) {
210 88 : if (!ensure_valid(location))
211 : return false;
212 88 : actual = extract(mp);
213 : } else {
214 74 : actual = msetting_bool(mp, parm);
215 : }
216 :
217 162 : if (actual == b)
218 : return true;
219 :
220 0 : char *b_ = b ? "true" : "false";
221 0 : char *actual_ = actual ? "true" : "false";
222 0 : fprintf(stderr, "%s: expected %s, found %s\n", location, b_, actual_);
223 0 : return false;
224 :
225 : }
226 :
227 : static bool
228 102 : expect_long(const char *location, const mparm parm, long (*extract)(const msettings*), const char *value)
229 : {
230 102 : if (strlen(value) == 0) {
231 0 : fprintf(stderr, "%s: syntax error: integer value cannot be empty string\n", location);
232 0 : return false;
233 : }
234 102 : char *end = NULL;
235 102 : long n = strtol(value, &end, 10);
236 102 : if (*end != '\0') {
237 0 : fprintf(stderr, "%s: syntax error: invalid integer '%s'\n", location, value);
238 0 : return false;
239 : }
240 :
241 102 : long actual;
242 102 : if (extract) {
243 44 : if (!ensure_valid(location))
244 : return false;
245 44 : actual = extract(mp);
246 : } else {
247 58 : actual = msetting_long(mp, parm);
248 : }
249 :
250 102 : if (actual == n)
251 : return true;
252 :
253 0 : fprintf(stderr, "%s: expected %ld, found %ld\n", location, n, actual);
254 0 : return false;
255 : }
256 :
257 : static bool
258 536 : expect_string(const char *location, const mparm parm, const char *(*extract)(const msettings*), const char *value)
259 : {
260 536 : const char *actual;
261 536 : if (extract) {
262 222 : if (!ensure_valid(location))
263 : return false;
264 222 : actual = extract(mp);
265 : } else {
266 314 : actual = msetting_string(mp, parm);
267 : }
268 :
269 536 : if (strcmp(actual, value) == 0)
270 : return true;
271 :
272 0 : fprintf(stderr, "%s: expected '%s', found '%s'\n", location, value, actual);
273 0 : return false;
274 : }
275 :
276 : static const char *
277 24 : stringify_tls_verify(const msettings *mp)
278 : {
279 24 : enum msetting_tls_verify verify = msettings_connect_tls_verify(mp);
280 24 : switch (verify) {
281 : case verify_none:
282 : return "";
283 : case verify_system:
284 : return "system";
285 : case verify_cert:
286 : return "cert";
287 : case verify_hash:
288 : return "hash";
289 : default:
290 0 : assert(0 && "unknown msetting_tls_verify value");
291 : return NULL;
292 : }
293 : assert(0 && "unreachable");
294 : }
295 :
296 : static bool
297 914 : handle_expect_command(const char *location, char *key, char *value)
298 : {
299 914 : if (strcmp("valid", key) == 0) {
300 114 : int x = msetting_parse_bool(value);
301 114 : if (x < 0) {
302 0 : fprintf(stderr, "%s: invalid boolean value: %s\n", location, value);
303 0 : return false;
304 : }
305 114 : bool expected_valid = x > 0;
306 :
307 114 : bool actually_valid = msettings_validate(mp) == NULL;
308 114 : if (actually_valid != expected_valid) {
309 0 : fprintf(stderr, "%s: expected '%s', found '%s'\n",
310 : location,
311 : expected_valid ? "true" : "false",
312 : actually_valid ? "true" : "false"
313 : );
314 0 : return false;
315 : }
316 : return true;
317 : }
318 :
319 800 : if (strcmp("connect_scan", key) == 0)
320 88 : return expect_bool(location, MP_UNKNOWN, msettings_connect_scan, value);
321 712 : if (strcmp("connect_unix", key) == 0)
322 96 : return expect_string(location, MP_UNKNOWN, msettings_connect_unix, value);
323 616 : if (strcmp("connect_tcp", key) == 0)
324 88 : return expect_string(location, MP_UNKNOWN, msettings_connect_tcp, value);
325 528 : if (strcmp("connect_port", key) == 0)
326 22 : return expect_long(location, MP_UNKNOWN, msettings_connect_port, value);
327 506 : if (strcmp("connect_tls_verify", key) == 0)
328 24 : return expect_string(location, MP_UNKNOWN, stringify_tls_verify, value);
329 482 : if (strcmp("connect_certhash_digits", key) == 0)
330 2 : return expect_string(location, MP_UNKNOWN, msettings_connect_certhash_digits, value);
331 480 : if (strcmp("connect_binary", key) == 0)
332 22 : return expect_long(location, MP_UNKNOWN, msettings_connect_binary, value);
333 458 : if (strcmp("connect_clientkey", key) == 0)
334 6 : return expect_string(location, MP_UNKNOWN, msettings_connect_clientkey, value);
335 452 : if (strcmp("connect_clientcert", key) == 0)
336 6 : return expect_string(location, MP_UNKNOWN, msettings_connect_clientcert, value);
337 :
338 446 : const mparm parm = mparm_parse(key);
339 446 : if (parm == MP_UNKNOWN) {
340 0 : fprintf(stderr, "%s: unknown parameter '%s'\n:", location, key);
341 0 : return false;
342 : }
343 446 : if (parm == MP_IGNORE) {
344 0 : if (strncmp(key, "connect_", 8) == 0)
345 0 : fprintf(stderr, "%s: unknown virtual parameter '%s'\n", location, key);
346 : else
347 0 : fprintf(stderr, "%s: EXPECTing ignored parameters is not supported yet\n", location);
348 0 : return false;
349 : }
350 :
351 446 : switch (mparm_classify(parm)) {
352 74 : case MPCLASS_BOOL:
353 74 : return expect_bool(location, parm, NULL, value);
354 58 : case MPCLASS_LONG:
355 58 : return expect_long(location, parm, NULL, value);
356 314 : case MPCLASS_STRING:
357 314 : return expect_string(location, parm, NULL, value);
358 0 : default:
359 0 : fprintf(stderr, "%s: internal error: unclassified parameter %d\n", location, (int)parm);
360 0 : return false;
361 : }
362 : }
363 :
364 :
365 :
366 :
367 : static bool
368 2912 : handle_line(int lineno, const char *location, char *line, int verbose)
369 : {
370 : // first trim trailing whitespace
371 2912 : size_t n = strlen(line);
372 5824 : while (n > 0 && isspace(line[n - 1]))
373 : n--;
374 2912 : line[n] = '\0';
375 :
376 2912 : if (mp == NULL) {
377 : // not in a code block
378 1046 : if (strcmp(line, "```test") == 0) {
379 : // block starts here
380 276 : nstarted++;
381 276 : start_line = lineno;
382 276 : mp = msettings_create_with(allocator, NULL);
383 276 : if (mp == NULL) {
384 0 : fprintf(stderr, "%s: malloc failed\n", location);
385 0 : return false;
386 : }
387 276 : if (verbose >= 2)
388 0 : fprintf(stderr, "ยท %s\n", location);
389 276 : return true;
390 : } else {
391 : // ignore
392 : return true;
393 : }
394 : }
395 :
396 : // we're in a code block, does it end here?
397 1866 : if (strlen(line) > 0 && line[0] == '`') {
398 270 : if (strcmp(line, "```") == 0) {
399 : // lone backticks, block ends here
400 270 : msettings_destroy(mp);
401 270 : mp = NULL;
402 270 : if (verbose >= 3)
403 0 : fprintf(stderr, "\n");
404 270 : return true;
405 : } else {
406 0 : fprintf(stderr, "%s: unexpected backtick\n", location);
407 0 : return false;
408 : }
409 : }
410 :
411 : // this is line from a code block
412 1596 : if (verbose >= 3)
413 0 : fprintf(stderr, "%s\n", line);
414 1596 : const char *whitespace = " \t";
415 1596 : char *command = strtok(line, whitespace);
416 1596 : if (command == NULL) {
417 : // empty line
418 : return true;
419 1582 : } else if (strcasecmp(command, "ONLY") == 0) {
420 6 : char *impl = strtok(NULL, " \n");
421 6 : if (impl) {
422 6 : if (strcmp(impl, "libmapi") != 0) {
423 : // ONLY command is not about us. End the block here
424 6 : msettings_destroy(mp);
425 6 : mp = NULL;
426 : }
427 6 : return true;
428 : }
429 : // if !impl we print an error below
430 1576 : } else if (strcasecmp(command, "NOT") == 0) {
431 14 : char *impl = strtok(NULL, " \n");
432 14 : if (impl) {
433 14 : if (strcmp(impl, "libmapi") == 0) {
434 : // NOT command is about us. End the block here.
435 0 : msettings_destroy(mp);
436 0 : mp = NULL;
437 : }
438 14 : return true;
439 : }
440 : // if !impl we print an error below
441 1562 : } else if (strcasecmp(command, "PARSE") == 0) {
442 6 : char *url = strtok(NULL, "\n");
443 6 : if (url)
444 6 : return handle_parse_command(location, url);
445 1556 : } else if (strcasecmp(command, "ACCEPT") == 0) {
446 262 : char *url = strtok(NULL, "\n");
447 262 : if (url)
448 262 : return handle_accept_command(location, url);
449 1294 : } else if (strcasecmp(command, "REJECT") == 0) {
450 120 : char *url = strtok(NULL, "\n");
451 120 : if (url)
452 120 : return handle_reject_command(location, url);
453 1174 : } else if (strcasecmp(command, "SET") == 0) {
454 260 : char *key = strtok(NULL, "=");
455 260 : char *value = strtok(NULL, "\n");
456 260 : if (key)
457 282 : return handle_set_command(location, key, value ? value : "");
458 914 : } else if (strcasecmp(command, "EXPECT") == 0) {
459 914 : char *key = strtok(NULL, "=");
460 914 : char *value = strtok(NULL, "\n");
461 914 : if (key)
462 1056 : return handle_expect_command(location, key, value ? value : "");
463 : } else {
464 0 : fprintf(stderr, "%s: unknown command: %s\n", location, command);
465 0 : return false;
466 : }
467 :
468 : // if we get here, url or key was not present
469 0 : fprintf(stderr, "%s: syntax error\n", location);
470 0 : return false;
471 : }
472 :
473 : static bool
474 2 : run_tests_inner(stream *s, int verbose)
475 : {
476 2 : int orig_nstarted = nstarted;
477 2 : const char *filename = mnstr_name(s);
478 2 : char *location = malloc(strlen(filename) + 100);
479 2 : strcpy(location, filename);
480 2 : char *location_lineno = &location[strlen(filename)];
481 2 : *location_lineno++ = ':';
482 2 : *location_lineno = '\0';
483 2 : char line_buffer[1024];
484 :
485 2 : errno = 0;
486 :
487 2 : int lineno = 0;
488 :
489 2914 : while (true) {
490 2914 : lineno++;
491 2914 : sprintf(location_lineno, "%d", lineno);
492 2914 : ssize_t nread = mnstr_readline(s, line_buffer, sizeof(line_buffer));
493 2914 : if (nread == 0)
494 : break;
495 2912 : if (nread < 0) {
496 0 : if (errno) {
497 0 : fprintf(stderr, "%s: %s\n", location, strerror(errno));
498 0 : free(location);
499 0 : return false;
500 : } else {
501 : break;
502 : }
503 2912 : } else if (nread >= (ssize_t)sizeof(line_buffer) - 2) {
504 0 : fprintf(stderr, "%s: line too long\n", location);
505 :
506 : }
507 2912 : if (!handle_line(lineno, location, line_buffer, verbose)) {
508 0 : free(location);
509 0 : return false;
510 : }
511 : }
512 :
513 2 : if (mp) {
514 0 : fprintf(stderr, "%s:%d: unterminated code block starts here\n", filename, start_line);
515 0 : free(location);
516 0 : return false;
517 : }
518 :
519 2 : if (verbose >= 1) {
520 0 : fprintf(stderr, "ran %d successful tests from %s\n", nstarted - orig_nstarted, filename);
521 : }
522 :
523 2 : free(location);
524 2 : return true;
525 : }
526 :
527 : bool
528 2 : run_tests(stream *s, int verbose)
529 : {
530 2 : assert(mp == NULL);
531 2 : bool ok = run_tests_inner(s, verbose);
532 2 : if (mp) {
533 0 : msettings_destroy(mp);
534 0 : mp = NULL;
535 : }
536 2 : return ok;
537 : }
538 :
539 : // Our custom allocator stores a magic cookie before every
540 : // allocation.
541 : //
542 : // This allows us to detect that the memory we're free'ing
543 : // wasn't allocated by us.
544 : // Also, if the standard allocator free's memory allocated by
545 : // us, it or Valgrind will hopefully notice that the pointer
546 : // is wrong.
547 : static void *
548 4836 : custom_allocator(void *state, void *old, size_t size)
549 : {
550 4836 : (void)state;
551 4836 : const size_t prefix_size = 64;
552 4836 : const char cookie[] = "AllocCookie";
553 4836 : const size_t cookie_size = sizeof(cookie);
554 4836 : assert(cookie_size == 5 + 6 + 1);
555 :
556 4836 : if (old) {
557 2418 : old = (char*)old - prefix_size;
558 : // check for cookie and erase it
559 2418 : if (memcmp(old, cookie, cookie_size) != 0) {
560 0 : assert(0 && "custom allocator cookie missing");
561 : abort();
562 : }
563 2418 : memset(old, '\0', cookie_size);
564 : }
565 :
566 4836 : if (size == 0) {
567 2418 : free(old);
568 2418 : return NULL;
569 : }
570 2418 : char *new_allocation = realloc(old, size > 0 ? size + prefix_size: 0);
571 :
572 2418 : if (new_allocation) {
573 : // set magic cookie
574 2418 : memcpy(new_allocation, cookie, cookie_size);
575 2418 : new_allocation += prefix_size;
576 : }
577 :
578 : return new_allocation;
579 : }
580 :
581 :
582 : void
583 1 : use_custom_allocator(void)
584 : {
585 1 : allocator = custom_allocator;
586 1 : }
|