html.c (9178B)
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <string.h> 4 #include <unistd.h> 5 6 #include "html.h" 7 8 #define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0])) 9 10 struct test_case { 11 const char *name; 12 const char *template; 13 const struct html_var *vars; 14 size_t nvars; 15 const struct html_block *blocks; 16 size_t nblocks; 17 const char *expected; 18 int expected_result; 19 }; 20 21 struct escape_test_case { 22 const char *name; 23 const char *input; 24 const char *expected; 25 }; 26 /* --- Test data --- */ 27 28 /* Basic Variables */ 29 30 static const struct html_var basic_vars[] = { 31 {"NAME", "SimpleFiles"}, 32 {"MAX_UPLOAD", "100 MB"}, 33 }; 34 /* FILES: normal rows */ 35 static const struct html_var file1_vars[] = { 36 {"NAME", "one.txt"}, 37 {"SIZE", "123 B"}, 38 }; 39 40 static const struct html_var file2_vars[] = { 41 {"NAME", "two.txt"}, 42 {"SIZE", "456 B"}, 43 }; 44 45 static const struct html_row file_rows[] = { 46 {file1_vars, 2}, 47 {file2_vars, 2}, 48 }; 49 50 static const struct html_block files_block[] = { 51 {"FILES", file_rows, 2}, 52 }; 53 /* FILES: empty block */ 54 static const struct html_block empty_block[] = { 55 {"FILES", NULL, 0}, 56 }; 57 /* FILES: variable precedence */ 58 static const struct html_var global_vars[] = { 59 {"NAME", "global"}, 60 }; 61 62 static const struct html_var row_vars[] = { 63 {"NAME", "row"}, 64 }; 65 66 static const struct html_row override_rows[] = { 67 {row_vars, 1}, 68 }; 69 70 static const struct html_block override_block[] = { 71 {"FILES", override_rows, 1}, 72 }; 73 /* FILES: global fallback */ 74 75 static const struct html_var site_vars[] = { 76 {"SITE", "SimpleFiles"}, 77 }; 78 79 static const struct html_var row_name_vars[] = { 80 {"NAME", "test.txt"}, 81 }; 82 83 static const struct html_row fallback_rows[] = { 84 {row_name_vars, 1}, 85 }; 86 87 static const struct html_block fallback_block[] = { 88 {"FILES", fallback_rows, 1}, 89 }; 90 91 static const struct html_var empty_vars[] = { 92 {"NAME", ""}, 93 }; 94 /* FILES: HTML escaping */ 95 96 static const struct html_var escaped_row_vars[] = { 97 {"NAME", "<script>alert(\"x\")</script>"}, 98 }; 99 100 static const struct html_row escaped_rows[] = { 101 {escaped_row_vars, 1}, 102 }; 103 104 static const struct html_block escaped_block[] = { 105 {"FILES", escaped_rows, 1}, 106 }; 107 /* FILES: empty variable value */ 108 109 static const struct html_var empty_row_vars[] = { 110 {"NAME", ""}, 111 }; 112 113 static const struct html_row empty_value_rows[] = { 114 {empty_row_vars, 1}, 115 }; 116 117 static const struct html_block empty_value_block[] = { 118 {"FILES", empty_value_rows, 1}, 119 }; 120 /* --- Test helpers --- */ 121 122 static int 123 run_test(const struct test_case * test) 124 { 125 FILE *template; 126 FILE *output; 127 char path[] = "/tmp/html-test-XXXXXX"; 128 char buf[4096]; 129 size_t n; 130 int fd; 131 int result; 132 int failures = 0; 133 134 fd = mkstemp(path); 135 if (fd == -1) { 136 perror("mkstemp"); 137 return 1; 138 } 139 template = fdopen(fd, "w+"); 140 if (template == NULL) { 141 perror("fdopen"); 142 close(fd); 143 unlink(path); 144 return 1; 145 } 146 if (fputs(test->template, template) == EOF) { 147 perror("fputs"); 148 fclose(template); 149 unlink(path); 150 return 1; 151 } 152 fflush(template); 153 rewind(template); 154 155 output = tmpfile(); 156 if (output == NULL) { 157 perror("tmpfile"); 158 fclose(template); 159 unlink(path); 160 return 1; 161 } 162 result = render_template(output, path, 163 test->vars, test->nvars, test->blocks, test->nblocks); 164 165 if (result != test->expected_result) { 166 fprintf(stderr, 167 "FAIL: %s: render_template() returned %d, " 168 "expected %d\n", 169 test->name, 170 result, 171 test->expected_result); 172 failures++; 173 goto done; 174 } 175 if (result == 0) { 176 fflush(output); 177 rewind(output); 178 179 n = fread(buf, 1, sizeof(buf) - 1, output); 180 buf[n] = '\0'; 181 182 if (strcmp(buf, test->expected) != 0) { 183 fprintf(stderr, 184 "FAIL: %s: output was:\n" 185 "---\n%s---\n" 186 "expected:\n" 187 "---\n%s---\n", 188 test->name, 189 buf, 190 test->expected); 191 failures++; 192 goto done; 193 } 194 } 195 printf("ok: %s\n", test->name); 196 197 done: 198 fclose(output); 199 fclose(template); 200 unlink(path); 201 202 return failures; 203 } 204 205 static int 206 run_escape_test(const struct escape_test_case * test) 207 { 208 FILE *output; 209 char buf[4096]; 210 size_t n; 211 int failures = 0; 212 213 output = tmpfile(); 214 if (output == NULL) { 215 perror("tmpfile"); 216 return 1; 217 } 218 html_escape(output, test->input); 219 220 fflush(output); 221 rewind(output); 222 223 n = fread(buf, 1, sizeof(buf) - 1, output); 224 buf[n] = '\0'; 225 226 if (strcmp(buf, test->expected) != 0) { 227 fprintf(stderr, 228 "FAIL: %s: html_escape() output was:\n" 229 "---\n%s---\n" 230 "expected:\n" 231 "---\n%s---\n", 232 test->name, 233 buf, 234 test->expected); 235 failures++; 236 } 237 if (failures == 0) 238 printf("ok: %s\n", test->name); 239 240 fclose(output); 241 242 return failures; 243 } 244 /* --- Template rendering tests --- */ 245 static const struct test_case tests[] = { 246 { 247 "plain text", 248 "hello world\n", 249 NULL, 250 0, 251 NULL, 252 0, 253 "hello world\n", 254 0 255 }, 256 { 257 "one variable", 258 "hello {{NAME}}\n", 259 basic_vars, 260 ARRAY_SIZE(basic_vars), 261 NULL, 262 0, 263 "hello SimpleFiles\n", 264 0 265 }, 266 { 267 "multiple variables", 268 "{{NAME}}: max {{MAX_UPLOAD}}\n", 269 basic_vars, 270 ARRAY_SIZE(basic_vars), 271 NULL, 272 0, 273 "SimpleFiles: max 100 MB\n", 274 0 275 }, 276 { 277 "unknown variable", 278 "hello {{MISSING}}\n", 279 NULL, 280 0, 281 NULL, 282 0, 283 NULL, 284 -1 285 }, 286 { 287 "html is preserved", 288 "<h1>{{NAME}}</h1>\n" 289 "<p>Upload a file.</p>\n", 290 basic_vars, 291 ARRAY_SIZE(basic_vars), 292 NULL, 293 0, 294 "<h1>SimpleFiles</h1>\n" 295 "<p>Upload a file.</p>\n", 296 0 297 }, 298 { 299 "block with two rows", 300 "<ul>\n" 301 "{{#FILES}}" 302 "<li>{{NAME}} ({{SIZE}})</li>\n" 303 "{{/FILES}}" 304 "</ul>\n", 305 NULL, 306 0, 307 files_block, 308 ARRAY_SIZE(files_block), 309 "<ul>\n" 310 "<li>one.txt (123 B)</li>\n" 311 "<li>two.txt (456 B)</li>\n" 312 "</ul>\n", 313 0 314 }, 315 { 316 "empty block", 317 "before\n" 318 "{{#FILES}}" 319 "<li>{{NAME}}</li>\n" 320 "{{/FILES}}" 321 "after\n", 322 NULL, 323 0, 324 empty_block, 325 ARRAY_SIZE(empty_block), 326 "before\n" 327 "after\n", 328 0 329 }, 330 { 331 "row variable overrides global variable", 332 "{{#FILES}}{{NAME}}{{/FILES}}", 333 global_vars, 334 ARRAY_SIZE(global_vars), 335 override_block, 336 ARRAY_SIZE(override_block), 337 "row", 338 0 339 }, 340 { 341 "global variable available inside block", 342 "{{#FILES}}{{SITE}}: {{NAME}}{{/FILES}}", 343 site_vars, 344 ARRAY_SIZE(site_vars), 345 fallback_block, 346 ARRAY_SIZE(fallback_block), 347 "SimpleFiles: test.txt", 348 0 349 }, 350 { 351 "unknown variable inside block", 352 "{{#FILES}}{{MISSING}}{{/FILES}}", 353 NULL, 354 0, 355 files_block, 356 ARRAY_SIZE(files_block), 357 NULL, 358 -1 359 }, 360 { 361 "unknown block", 362 "before{{#MISSING}}x{{/MISSING}}after", 363 NULL, 364 0, 365 files_block, 366 ARRAY_SIZE(files_block), 367 NULL, 368 -1 369 }, 370 { 371 "missing closing block", 372 "{{#FILES}}{{NAME}}", 373 NULL, 374 0, 375 files_block, 376 ARRAY_SIZE(files_block), 377 NULL, 378 -1 379 }, 380 { 381 "malformed block opening", 382 "{{#FILES", 383 NULL, 384 0, 385 files_block, 386 ARRAY_SIZE(files_block), 387 NULL, 388 -1 389 }, 390 { 391 "unterminated variable", 392 "hello {{NAME", 393 basic_vars, 394 ARRAY_SIZE(basic_vars), 395 NULL, 396 0, 397 NULL, 398 -1 399 }, 400 { 401 "empty variable", 402 "hello {{}}", 403 basic_vars, 404 ARRAY_SIZE(basic_vars), 405 NULL, 406 0, 407 NULL, 408 -1 409 }, 410 { 411 "overlong variable name", 412 "{{AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA}}", 413 basic_vars, 414 ARRAY_SIZE(basic_vars), 415 NULL, 416 0, 417 NULL, 418 -1 419 }, 420 { 421 "empty variable value", 422 "before[{{NAME}}]after", 423 empty_vars, 424 ARRAY_SIZE(empty_vars), 425 NULL, 426 0, 427 "before[]after", 428 0 429 }, 430 { 431 "HTML escaping inside block", 432 "{{#FILES}}{{NAME}}{{/FILES}}", 433 NULL, 434 0, 435 escaped_block, 436 ARRAY_SIZE(escaped_block), 437 "<script>alert("x")</script>", 438 0 439 }, 440 { 441 "unterminated variable with whitespace", 442 "hello {{ world", 443 basic_vars, 444 ARRAY_SIZE(basic_vars), 445 NULL, 446 0, 447 NULL, 448 -1 449 }, 450 { 451 "block with static text", 452 "before{{#FILES}}hello{{/FILES}}after", 453 NULL, 454 0, 455 files_block, 456 ARRAY_SIZE(files_block), 457 "beforehellohelloafter", 458 0 459 }, 460 { 461 "empty row variable value", 462 "{{#FILES}}[{{NAME}}]{{/FILES}}", 463 NULL, 464 0, 465 empty_value_block, 466 ARRAY_SIZE(empty_value_block), 467 "[]", 468 0 469 }, 470 { 471 "text surrounding block", 472 "before\n" 473 "{{#FILES}}" 474 "{{NAME}}\n" 475 "{{/FILES}}" 476 "after\n", 477 NULL, 478 0, 479 files_block, 480 ARRAY_SIZE(files_block), 481 "before\n" 482 "one.txt\n" 483 "two.txt\n" 484 "after\n", 485 0 486 }, 487 }; 488 /* --- HTML escaping tests --- */ 489 490 static const struct escape_test_case escape_tests[] = { 491 { 492 "escape plain text", 493 "hello world", 494 "hello world" 495 }, 496 { 497 "escape ampersand", 498 "a & b", 499 "a & b" 500 }, 501 { 502 "escape less-than", 503 "<script>", 504 "<script>" 505 }, 506 { 507 "escape greater-than", 508 "1 > 0", 509 "1 > 0" 510 }, 511 { 512 "escape double quote", 513 "hello \"world\"", 514 "hello "world"" 515 }, 516 { 517 "escape single quote", 518 "it's", 519 "it's" 520 }, 521 { 522 "escape multiple characters", 523 "<a href=\"foo?a=1&b=2\">it's ok</a>", 524 "<a href="foo?a=1&b=2">it's ok</a>" 525 }, 526 { 527 "escape empty string", 528 "", 529 "" 530 }, 531 }; 532 533 int 534 main(void) 535 { 536 size_t i; 537 int failures = 0; 538 539 for (i = 0; i < ARRAY_SIZE(tests); 540 i++) 541 failures += run_test(&tests[i]); 542 543 for (i = 0; i < ARRAY_SIZE(escape_tests); 544 i++) 545 failures += run_escape_test(&escape_tests[i]); 546 547 if (failures != 0) { 548 fprintf(stderr, 549 "%d test(s) failed\n", 550 failures); 551 return EXIT_FAILURE; 552 } 553 printf("all tests passed\n"); 554 return EXIT_SUCCESS; 555 }