saait.c - saait - the most boring static page generator
HTML git clone git://git.codemadness.org/saait
DIR Log
DIR Files
DIR Refs
DIR README
DIR LICENSE
---
saait.c (12892B)
---
1 #include <ctype.h>
2 #include <dirent.h>
3 #include <errno.h>
4 #include <limits.h>
5 #include <stdio.h>
6 #include <stdint.h>
7 #include <stdlib.h>
8 #include <string.h>
9
10 /* OpenBSD pledge(2) */
11 #ifdef __OpenBSD__
12 #include <unistd.h>
13 #else
14 #define pledge(p1,p2) 0
15 #endif
16
17 /* This is the blocksize of my disk, use atleast an equal or higher value and
18 a multiple of 2 for better performance ((struct stat).st_blksize). */
19 #define READ_BUF_SIZ 16384
20 #define LEN(s) (sizeof(s)/sizeof(*s))
21
22 enum { BlockHeader = 0, BlockItem, BlockFooter, BlockLast };
23
24 struct variable {
25 char *key, *value;
26 struct variable *next;
27 };
28
29 struct block {
30 char *name; /* filename */
31 char *data; /* content (set at runtime) */
32 };
33
34 struct template {
35 char *name;
36 /* blocks: header, item, footer */
37 struct block blocks[BlockLast];
38 /* output FILE * (set at runtime) */
39 FILE *fp;
40 };
41
42 static const char *configfile = "config.cfg";
43 static const char *outputdir = "output";
44 static const char *templatedir = "templates";
45
46 static struct variable *global; /* global config variables */
47
48 char *
49 estrdup(const char *s)
50 {
51 char *p;
52
53 if (!(p = strdup(s))) {
54 fprintf(stderr, "strdup: %s\n", strerror(errno));
55 exit(1);
56 }
57 return p;
58 }
59
60 void *
61 ecalloc(size_t nmemb, size_t size)
62 {
63 void *p;
64
65 if (!(p = calloc(nmemb, size))) {
66 fprintf(stderr, "calloc: %s\n", strerror(errno));
67 exit(1);
68 }
69 return p;
70 }
71
72 void *
73 erealloc(void *ptr, size_t size)
74 {
75 void *p;
76
77 if (!(p = realloc(ptr, size))) {
78 fprintf(stderr, "realloc: %s\n", strerror(errno));
79 exit(1);
80 }
81 return p;
82 }
83
84 FILE *
85 efopen(const char *path, const char *mode)
86 {
87 FILE *fp;
88
89 if (!(fp = fopen(path, mode))) {
90 fprintf(stderr, "fopen: %s, mode: %s: %s\n",
91 path, mode, strerror(errno));
92 exit(1);
93 }
94 return fp;
95 }
96
97 void
98 catfile(FILE *fpin, const char *ifile, FILE *fpout, const char *ofile)
99 {
100 char buf[READ_BUF_SIZ];
101 size_t r;
102
103 while (!feof(fpin)) {
104 if (!(r = fread(buf, 1, sizeof(buf), fpin)))
105 break;
106 if ((fwrite(buf, 1, r, fpout)) != r)
107 break;
108 if (r != sizeof(buf))
109 break;
110 }
111 if (ferror(fpin)) {
112 fprintf(stderr, "%s -> %s: error reading data from stream: %s\n",
113 ifile, ofile, strerror(errno));
114 exit(1);
115 }
116 if (ferror(fpout)) {
117 fprintf(stderr, "%s -> %s: error writing data to stream: %s\n",
118 ifile, ofile, strerror(errno));
119 exit(1);
120 }
121 }
122
123 char *
124 readfile(const char *file)
125 {
126 FILE *fp;
127 char *buf;
128 size_t n, len = 0, size = 0;
129
130 fp = efopen(file, "rb");
131 buf = ecalloc(1, size + 1); /* always allocate an empty buffer */
132 while (!feof(fp)) {
133 if (len + READ_BUF_SIZ + 1 > size) {
134 /* allocate size: common case is small textfiles */
135 size += READ_BUF_SIZ;
136 buf = erealloc(buf, size + 1);
137 }
138 if (!(n = fread(&buf[len], 1, READ_BUF_SIZ, fp)))
139 break;
140 len += n;
141 buf[len] = '\0';
142 if (n != READ_BUF_SIZ)
143 break;
144 }
145 if (ferror(fp)) {
146 fprintf(stderr, "fread: file: %s: %s\n", file, strerror(errno));
147 exit(1);
148 }
149 fclose(fp);
150
151 return buf;
152 }
153
154 struct variable *
155 newvar(const char *key, const char *value)
156 {
157 struct variable *v;
158
159 v = ecalloc(1, sizeof(*v));
160 v->key = estrdup(key);
161 v->value = estrdup(value);
162
163 return v;
164 }
165
166 /* uses var->key as key */
167 void
168 setvar(struct variable **vars, struct variable *var, int override)
169 {
170 struct variable *p, *v;
171
172 /* new */
173 if (!*vars) {
174 *vars = var;
175 return;
176 }
177
178 /* search: set or append */
179 for (p = NULL, v = *vars; v; v = v->next, p = v) {
180 if (!strcmp(var->key, v->key)) {
181 if (!override)
182 return;
183 /* NOTE: keep v->next */
184 var->next = v->next;
185 if (p)
186 p->next = var;
187 else
188 *vars = var;
189 free(v->key);
190 free(v->value);
191 free(v);
192 return;
193 }
194 /* append */
195 if (!v->next) {
196 var->next = NULL;
197 v->next = var;
198 return;
199 }
200 }
201 }
202
203 struct variable *
204 getvar(struct variable *vars, char *key)
205 {
206 struct variable *v;
207
208 for (v = vars; v; v = v->next)
209 if (!strcmp(key, v->key))
210 return v;
211 return NULL;
212 }
213
214 void
215 freevars(struct variable *vars)
216 {
217 struct variable *v, *tmp;
218
219 for (v = vars; v; ) {
220 tmp = v->next;
221 free(v->key);
222 free(v->value);
223 free(v);
224 v = tmp;
225 }
226 }
227
228 struct variable *
229 parsevars(const char *file, const char *s)
230 {
231 struct variable *vars = NULL, *v;
232 const char *keystart, *keyend, *valuestart, *valueend;
233 size_t linenr = 1;
234
235 for (; *s; ) {
236 if (*s == '\r' || *s == '\n') {
237 linenr += (*s == '\n');
238 s++;
239 continue;
240 }
241
242 /* comment start with #, skip to newline */
243 if (*s == '#') {
244 s++;
245 s = &s[strcspn(s, "\n")];
246 continue;
247 }
248
249 /* trim whitespace before key */
250 s = &s[strspn(s, " \t")];
251
252 keystart = s;
253 s = &s[strcspn(s, "=\r\n")];
254 if (*s != '=') {
255 fprintf(stderr, "%s:%zu: error: no variable\n",
256 file, linenr);
257 exit(1);
258 }
259
260 /* trim whitespace at end of key: but whitespace inside names
261 are allowed */
262 for (keyend = s++; keyend > keystart &&
263 (keyend[-1] == ' ' || keyend[-1] == '\t');
264 keyend--)
265 ;
266 /* no variable name: skip */
267 if (keystart == keyend) {
268 fprintf(stderr, "%s:%zu: error: invalid variable\n",
269 file, linenr);
270 exit(1);
271 }
272
273 /* trim whitespace before value */
274 valuestart = &s[strspn(s, " \t")];
275 s = &s[strcspn(s, "\r\n")];
276 valueend = s;
277
278 v = ecalloc(1, sizeof(*v));
279 v->key = ecalloc(1, keyend - keystart + 1);
280 memcpy(v->key, keystart, keyend - keystart);
281 v->value = ecalloc(1, valueend - valuestart + 1);
282 memcpy(v->value, valuestart, valueend - valuestart);
283
284 setvar(&vars, v, 1);
285 }
286 return vars;
287 }
288
289 struct variable *
290 readconfig(const char *file)
291 {
292 struct variable *c;
293 char *data;
294
295 data = readfile(file);
296 c = parsevars(file, data);
297 free(data);
298
299 return c;
300 }
301
302 /* Escape characters below as HTML 2.0 / XML 1.0. */
303 void
304 xmlencode(const char *s, FILE *fp)
305 {
306 for (; *s; s++) {
307 switch (*s) {
308 case '<': fputs("<", fp); break;
309 case '>': fputs(">", fp); break;
310 case '\'': fputs("'", fp); break;
311 case '&': fputs("&", fp); break;
312 case '"': fputs(""", fp); break;
313 default: fputc(*s, fp);
314 }
315 }
316 }
317
318 void
319 writepage(FILE *fp, const char *name, const char *forname,
320 struct variable *c, char *s)
321 {
322 FILE *fpin;
323 struct variable *v;
324 char *key;
325 size_t keylen, linenr = 1;
326 int op, tmpc;
327
328 for (; *s; s++) {
329 op = *s;
330 switch (*s) {
331 case '#': /* insert value non-escaped */
332 case '$': /* insert value escaped */
333 case '%': /* insert contents of filename set in variable */
334 if (*(s + 1) == '{') {
335 s += 2;
336 break;
337 }
338 fputc(*s, fp);
339 continue;
340 case '\n':
341 linenr++; /* FALLTHROUGH */
342 default:
343 fputc(*s, fp);
344 continue;
345 }
346
347 /* variable case */
348 for (; *s && isspace((unsigned char)*s); s++)
349 ;
350 key = s;
351 for (keylen = 0; *s && *s != '}'; s++)
352 keylen++;
353 /* trim right whitespace */
354 for (; keylen && isspace((unsigned char)key[keylen - 1]); )
355 keylen--;
356
357 /* temporary NUL terminate */
358 tmpc = key[keylen];
359 key[keylen] = '\0';
360
361 /* lookup variable in config, if no config or not found look in
362 global config */
363 if (!c || !(v = getvar(c, key)))
364 v = getvar(global, key);
365 key[keylen] = tmpc; /* restore NUL terminator to original */
366
367 if (!v) {
368 fprintf(stderr, "%s:%zu: error: undefined variable: '%.*s'%s%s\n",
369 name, linenr, (int)keylen, key,
370 forname ? " for " : "", forname ? forname : "");
371 exit(1);
372 }
373
374 switch (op) {
375 case '#':
376 fputs(v->value, fp);
377 break;
378 case '$':
379 xmlencode(v->value, fp);
380 break;
381 case '%':
382 if (!v->value[0])
383 break;
384 fpin = efopen(v->value, "rb");
385 catfile(fpin, v->value, fp, name);
386 fclose(fpin);
387 break;
388 }
389 }
390 }
391
392 void
393 usage(const char *argv0)
394 {
395 fprintf(stderr, "%s [-c configfile] [-o outputdir] [-t templatesdir] "
396 "pages...\n", argv0);
397 exit(1);
398 }
399
400 int
401 main(int argc, char *argv[])
402 {
403 struct template *t, *templates = NULL;
404 struct block *b;
405 struct variable *c, *v;
406 DIR *bdir, *idir;
407 struct dirent *ir, *br;
408 char file[PATH_MAX + 1], contentfile[PATH_MAX + 1], path[PATH_MAX + 1];
409 char outputfile[PATH_MAX + 1], *p, *filename;
410 size_t i, j, k, templateslen;
411 int argi, r;
412
413 if (pledge("stdio cpath rpath wpath", NULL) == -1) {
414 fprintf(stderr, "pledge: %s\n", strerror(errno));
415 return 1;
416 }
417
418 for (argi = 1; argi < argc; argi++) {
419 if (argv[argi][0] != '-')
420 break;
421 if (argi + 1 >= argc)
422 usage(argv[0]);
423 switch (argv[argi][1]) {
424 case 'c': configfile = argv[++argi]; break;
425 case 'o': outputdir = argv[++argi]; break;
426 case 't': templatedir = argv[++argi]; break;
427 default: usage(argv[0]); break;
428 }
429 }
430
431 /* global config */
432 global = readconfig(configfile);
433
434 /* load templates, must start with "header.", "item." or "footer." */
435 templateslen = 0;
436 if (!(bdir = opendir(templatedir))) {
437 fprintf(stderr, "opendir: %s: %s\n", templatedir, strerror(errno));
438 exit(1);
439 }
440
441 while ((br = readdir(bdir))) {
442 if (br->d_name[0] == '.')
443 continue;
444
445 r = snprintf(path, sizeof(path), "%s/%s", templatedir,
446 br->d_name);
447 if (r < 0 || (size_t)r >= sizeof(path)) {
448 fprintf(stderr, "path truncated: '%s/%s'\n",
449 templatedir, br->d_name);
450 exit(1);
451 }
452
453 if (!(idir = opendir(path))) {
454 fprintf(stderr, "opendir: %s: %s\n", path, strerror(errno));
455 exit(1);
456 }
457
458 templateslen++;
459 /* check overflow */
460 if (SIZE_MAX / templateslen < sizeof(*templates)) {
461 fprintf(stderr, "realloc: too many templates: %zu\n", templateslen);
462 exit(1);
463 }
464 templates = erealloc(templates, templateslen * sizeof(*templates));
465 t = &templates[templateslen - 1];
466 memset(t, 0, sizeof(struct template));
467 t->name = estrdup(br->d_name);
468
469 while ((ir = readdir(idir))) {
470 if (!strncmp(ir->d_name, "header.", sizeof("header.") - 1))
471 b = &(t->blocks[BlockHeader]);
472 else if (!strncmp(ir->d_name, "item.", sizeof("item.") - 1))
473 b = &(t->blocks[BlockItem]);
474 else if (!strncmp(ir->d_name, "footer.", sizeof("footer.") - 1))
475 b = &(t->blocks[BlockFooter]);
476 else
477 continue;
478
479 r = snprintf(file, sizeof(file), "%s/%s", path,
480 ir->d_name);
481 if (r < 0 || (size_t)r >= sizeof(file)) {
482 fprintf(stderr, "path truncated: '%s/%s'\n",
483 path, ir->d_name);
484 exit(1);
485 }
486 b->name = estrdup(file);
487 b->data = readfile(file);
488 }
489 closedir(idir);
490 }
491 closedir(bdir);
492
493 /* open output files for templates and write header, except for "page" */
494 for (i = 0; i < templateslen; i++) {
495 /* "page" is a special case */
496 if (!strcmp(templates[i].name, "page"))
497 continue;
498 r = snprintf(file, sizeof(file), "%s/%s", outputdir,
499 templates[i].name);
500 if (r < 0 || (size_t)r >= sizeof(file)) {
501 fprintf(stderr, "path truncated: '%s/%s'\n", outputdir,
502 templates[i].name);
503 exit(1);
504 }
505 templates[i].fp = efopen(file, "wb");
506
507 /* header */
508 b = &templates[i].blocks[BlockHeader];
509 if (b->name)
510 writepage(templates[i].fp, b->name, NULL, NULL, b->data);
511 }
512
513 /* pages */
514 for (i = argi; i < (size_t)argc; i++) {
515 c = readconfig(argv[i]);
516
517 if ((p = strrchr(argv[i], '.')))
518 r = snprintf(contentfile, sizeof(contentfile), "%.*s.html",
519 (int)(p - argv[i]), argv[i]);
520 else
521 r = snprintf(contentfile, sizeof(contentfile), "%s.html", argv[i]);
522 if (r < 0 || (size_t)r >= sizeof(contentfile)) {
523 fprintf(stderr, "path truncated for file: '%s'\n", argv[i]);
524 exit(1);
525 }
526 /* set contentfile, but allow to override it */
527 setvar(&c, newvar("contentfile", contentfile), 0);
528
529 if ((v = getvar(c, "filename"))) {
530 filename = v->value;
531 } else {
532 /* set output filename (with path removed), but allow
533 to override it */
534 if ((p = strrchr(contentfile, '/')))
535 filename = &contentfile[p - contentfile + 1];
536 else
537 filename = contentfile;
538
539 setvar(&c, newvar("filename", filename), 0);
540 }
541
542 /* item blocks */
543 for (j = 0; j < templateslen; j++) {
544 /* "page" is a special case */
545 if (!strcmp(templates[j].name, "page")) {
546 r = snprintf(outputfile, sizeof(outputfile), "%s/%s",
547 outputdir, filename);
548 if (r < 0 || (size_t)r >= sizeof(outputfile)) {
549 fprintf(stderr, "path truncated: '%s/%s'\n",
550 outputdir, filename);
551 exit(1);
552 }
553
554 /* "page" template files are opened per item
555 as opposed to other templates */
556 templates[j].fp = efopen(outputfile, "wb");
557 for (k = 0; k < LEN(templates[j].blocks); k++) {
558 b = &templates[j].blocks[k];
559 if (b->name)
560 writepage(templates[j].fp,
561 b->name, argv[i], c,
562 b->data);
563 }
564 fclose(templates[j].fp);
565 } else {
566 b = &templates[j].blocks[BlockItem];
567 if (b->name)
568 writepage(templates[j].fp, b->name,
569 argv[i], c, b->data);
570 }
571 }
572 freevars(c);
573 }
574
575 /* write footer, except for "page" */
576 for (i = 0; i < templateslen; i++) {
577 if (!strcmp(templates[i].name, "page"))
578 continue;
579 b = &templates[i].blocks[BlockFooter];
580 if (b->name)
581 writepage(templates[i].fp, b->name, NULL, NULL, b->data);
582 }
583
584 return 0;
585 }