URI:
       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("&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 
          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 }