saait

Simple static page generator
git clone https://git.sinitax.com/codemadness/saait
Log | Files | Refs | README | LICENSE | Upstream | sfeed.txt

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
     22enum { BlockHeader = 0, BlockItem, BlockFooter, BlockLast };
     23
     24struct variable {
     25	char *key, *value;
     26	struct variable *next;
     27};
     28
     29struct block {
     30	char *name; /* filename */
     31	char *data; /* content (set at runtime) */
     32};
     33
     34struct 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
     42static const char *configfile  = "config.cfg";
     43static const char *outputdir   = "output";
     44static const char *templatedir = "templates";
     45
     46static struct variable *global; /* global config variables */
     47
     48char *
     49estrdup(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
     60void *
     61ecalloc(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
     72void *
     73erealloc(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
     84FILE *
     85efopen(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
     97void
     98catfile(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
    123char *
    124readfile(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
    154struct variable *
    155newvar(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 */
    167void
    168setvar(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
    203struct variable *
    204getvar(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
    214void
    215freevars(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
    228struct variable *
    229parsevars(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
    289struct variable *
    290readconfig(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. */
    303void
    304xmlencode(const char *s, FILE *fp)
    305{
    306	for (; *s; s++) {
    307		switch (*s) {
    308		case '<':  fputs("&lt;",   fp); break;
    309		case '>':  fputs("&gt;",   fp); break;
    310		case '\'': fputs("&#39;",  fp); break;
    311		case '&':  fputs("&amp;",  fp); break;
    312		case '"':  fputs("&quot;", fp); break;
    313		default:   fputc(*s, fp);
    314		}
    315	}
    316}
    317
    318void
    319writepage(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
    392void
    393usage(const char *argv0)
    394{
    395	fprintf(stderr, "%s [-c configfile] [-o outputdir] [-t templatesdir] "
    396	                "pages...\n", argv0);
    397	exit(1);
    398}
    399
    400int
    401main(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}