saait.c - saait - the most boring static page generator | |
git clone git://git.codemadness.org/saait | |
Log | |
Files | |
Refs | |
README | |
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 valu… | |
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 strea… | |
113 ifile, ofile, strerror(errno)); | |
114 exit(1); | |
115 } | |
116 if (ferror(fpout)) { | |
117 fprintf(stderr, "%s -> %s: error writing data to stream:… | |
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(… | |
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 … | |
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… | |
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 … | |
362 global config */ | |
363 if (!c || !(v = getvar(c, key))) | |
364 v = getvar(global, key); | |
365 key[keylen] = tmpc; /* restore NUL terminator to origina… | |
366 | |
367 if (!v) { | |
368 fprintf(stderr, "%s:%zu: error: undefined variab… | |
369 name, linenr, (int)keylen, key, | |
370 forname ? " for " : "", forname ? fornam… | |
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 templates… | |
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_MA… | |
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, strerr… | |
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, strer… | |
455 exit(1); | |
456 } | |
457 | |
458 templateslen++; | |
459 /* check overflow */ | |
460 if (SIZE_MAX / templateslen < sizeof(*templates)) { | |
461 fprintf(stderr, "realloc: too many templates: %z… | |
462 exit(1); | |
463 } | |
464 templates = erealloc(templates, templateslen * sizeof(*t… | |
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("head… | |
471 b = &(t->blocks[BlockHeader]); | |
472 else if (!strncmp(ir->d_name, "item.", sizeof("i… | |
473 b = &(t->blocks[BlockItem]); | |
474 else if (!strncmp(ir->d_name, "footer.", sizeof(… | |
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'… | |
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 … | |
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", out… | |
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, … | |
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), "… | |
519 (int)(p - argv[i]), argv[i]); | |
520 else | |
521 r = snprintf(contentfile, sizeof(contentfile), "… | |
522 if (r < 0 || (size_t)r >= sizeof(contentfile)) { | |
523 fprintf(stderr, "path truncated for file: '%s'\n… | |
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 … | |
533 to override it */ | |
534 if ((p = strrchr(contentfile, '/'))) | |
535 filename = &contentfile[p - contentfile … | |
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(outputfi… | |
547 outputdir, filename); | |
548 if (r < 0 || (size_t)r >= sizeof(outputf… | |
549 fprintf(stderr, "path truncated:… | |
550 outputdir, filename); | |
551 exit(1); | |
552 } | |
553 | |
554 /* "page" template files are opened per … | |
555 as opposed to other templates */ | |
556 templates[j].fp = efopen(outputfile, "wb… | |
557 for (k = 0; k < LEN(templates[j].blocks)… | |
558 b = &templates[j].blocks[k]; | |
559 if (b->name) | |
560 writepage(templates[j].f… | |
561 b->name, argv[… | |
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->na… | |
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, … | |
582 } | |
583 | |
584 return 0; | |
585 } |