simplefiles

A lightweight C CGI web application for managing files.
git clone https://scm.kuandu.systems/git-raw/simplefiles.git
Log | Files | Refs | README | LICENSE

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 		"&lt;script&gt;alert(&quot;x&quot;)&lt;/script&gt;",
    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 &amp; b"
    500 	},
    501 	{
    502 		"escape less-than",
    503 		"<script>",
    504 		"&lt;script&gt;"
    505 	},
    506 	{
    507 		"escape greater-than",
    508 		"1 > 0",
    509 		"1 &gt; 0"
    510 	},
    511 	{
    512 		"escape double quote",
    513 		"hello \"world\"",
    514 		"hello &quot;world&quot;"
    515 	},
    516 	{
    517 		"escape single quote",
    518 		"it's",
    519 		"it&#39;s"
    520 	},
    521 	{
    522 		"escape multiple characters",
    523 		"<a href=\"foo?a=1&b=2\">it's ok</a>",
    524 		"&lt;a href=&quot;foo?a=1&amp;b=2&quot;&gt;it&#39;s ok&lt;/a&gt;"
    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 }