Mercurial > sci
changeset 26:7e10cace67a3
scid: add basic mustache support
author | David Demelier <markand@malikania.fr> |
---|---|
date | Tue, 02 Aug 2022 13:24:13 +0200 |
parents | c40f98360ac9 |
children | dae2de19ca5d |
files | .clang Makefile config.mk extern/LICENSE.libmustache4c.txt extern/VERSION.libmustache4c.txt extern/libmustache4c/mustache.c extern/libmustache4c/mustache.h lib/db.c lib/db.h lib/types.h lib/util.c scid/http.c scid/main.c scid/page-api-jobresults.c scid/page-api-jobs.c scid/page-api-projects.c scid/page-api-todo.c scid/page-api-workers.c scid/page-index.c scid/page-index.h scid/page-static.c scid/page-static.h scid/page.c scid/page.h scid/scid.c scid/scid.h sql/init.sql sql/job-list.sql sql/jobresult-list-by-job-group.sql sql/jobresult-list-by-job.sql sql/jobresult-list-by-worker.sql themes/bulma/fragments/footer.html themes/bulma/fragments/header.html themes/bulma/pages/index.html |
diffstat | 34 files changed, 2206 insertions(+), 185 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.clang Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,1 @@ +-Ilib
--- a/Makefile Mon Jul 25 21:22:13 2022 +0200 +++ b/Makefile Tue Aug 02 13:24:13 2022 +0200 @@ -21,26 +21,30 @@ include config.mk LIBSCI= lib/libsci.a -LIBSCI_SRCS= extern/libsqlite/sqlite3.c \ - lib/apic.c \ - lib/db.c \ - lib/log.c \ - lib/strlcpy.c \ - lib/strtonum.c \ - lib/types.c \ +LIBSCI_SRCS= extern/libsqlite/sqlite3.c \ + lib/apic.c \ + lib/db.c \ + lib/log.c \ + lib/strlcpy.c \ + lib/strtonum.c \ + lib/types.c \ lib/util.c LIBSCI_OBJS= ${LIBSCI_SRCS:.c=.o} LIBSCI_DEPS= ${LIBSCI_SRCS:.c=.d} -SQL_SRCS= sql/init.sql \ - sql/job-add.sql \ - sql/job-todo.sql \ - sql/jobresult-add.sql \ - sql/project-find.sql \ - sql/project-list.sql \ - sql/project-save.sql \ - sql/worker-find.sql \ - sql/worker-list.sql \ +SQL_SRCS= sql/init.sql \ + sql/job-add.sql \ + sql/job-list.sql \ + sql/job-todo.sql \ + sql/jobresult-add.sql \ + sql/jobresult-list-by-job.sql \ + sql/jobresult-list-by-job-group.sql \ + sql/jobresult-list-by-worker.sql \ + sql/project-find.sql \ + sql/project-list.sql \ + sql/project-save.sql \ + sql/worker-find.sql \ + sql/worker-list.sql \ sql/worker-save.sql SQL_OBJS= ${SQL_SRCS:.sql=.h} @@ -50,13 +54,17 @@ SCICTL_DEPS= ${SCICTL_SRCS:.c=.d} SCID= scid/scid -SCID_SRCS= scid/http.c \ - scid/main.c \ - scid/page-api-jobresults.c \ - scid/page-api-jobs.c \ - scid/page-api-projects.c \ - scid/page-api-todo.c \ - scid/page-api-workers.c \ +SCID_SRCS= extern/libmustache4c/mustache.c \ + scid/http.c \ + scid/main.c \ + scid/scid.c \ + scid/page-api-jobresults.c \ + scid/page-api-jobs.c \ + scid/page-api-projects.c \ + scid/page-api-todo.c \ + scid/page-api-workers.c \ + scid/page-index.c \ + scid/page-static.c \ scid/page.c SCID_OBJS= ${SCID_SRCS:.c=.o} SCID_DEPS= ${SCID_SRCS:.c=.d} @@ -88,6 +96,7 @@ INCS= -Iextern/libsqlite \ -Iextern/libutlist \ -Iextern/libgreatest \ + -Iextern/libmustache4c \ -Ilib \ -I. DEFS= -DVARDIR=\"${VARDIR}\" \
--- a/config.mk Mon Jul 25 21:22:13 2022 +0200 +++ b/config.mk Tue Aug 02 13:24:13 2022 +0200 @@ -1,5 +1,5 @@ CC= cc -CFLAGS= -g -O0 -Wall -Wextra +CFLAGS= -g -O0 -Wall -Wextra -fsanitize=address #CFLAGS= -Wall -Wextra -fsanitize=address,undefined -g -O0 #LDFLAGS= -fsanitize=address,undefined
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/LICENSE.libmustache4c.txt Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,22 @@ + +# The MIT License (MIT) + +Copyright © 2017 Martin Mitáš + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the “Software”), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/VERSION.libmustache4c.txt Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,1 @@ +1d6b25ac1ef93cf3cf032684d4689b2a7789e41f
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/libmustache4c/mustache.c Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,1167 @@ +/* + * Mustache4C + * (http://github.com/mity/mustache4c) + * + * Copyright (c) 2017 Martin Mitas + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#include "mustache.h" + +#include <errno.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <sys/types.h> /* for size_t */ + + +#ifdef _MSC_VER + /* MSVC does not understand "inline" when building as pure C (not C++). + * However it understands "__inline" */ + #ifndef __cplusplus + #define inline __inline + #endif +#endif + + +#define MUSTACHE_DEFAULTOPENER "{{" +#define MUSTACHE_DEFAULTCLOSER "}}" +#define MUSTACHE_MAXOPENERLENGTH 32 +#define MUSTACHE_MAXCLOSERLENGTH 32 + + +/********************** + *** Growing Buffer *** + **********************/ + +typedef struct MUSTACHE_BUFFER { + uint8_t* data; + size_t n; + size_t alloc; +} MUSTACHE_BUFFER; + +static inline void +mustache_buffer_free(MUSTACHE_BUFFER* buf) +{ + free(buf->data); +} + +static int +mustache_buffer_insert(MUSTACHE_BUFFER* buf, size_t off, const void* data, size_t n) +{ + if(buf->n + n > buf->alloc) { + size_t new_alloc = (buf->n + n) * 2; + uint8_t* new_data; + + new_data = (uint8_t*) realloc(buf->data, new_alloc); + if(new_data == NULL) + return -1; + + buf->data = new_data; + buf->alloc = new_alloc; + } + + if(off < buf->n) + memmove(buf->data + off + n, buf->data + off, buf->n - off); + + memcpy(buf->data + off, data, n); + buf->n += n; + return 0; +} + +static inline int +mustache_buffer_append(MUSTACHE_BUFFER* buf, const void* data, size_t n) +{ + return mustache_buffer_insert(buf, (size_t) buf->n, data, n); +} + +static int +mustache_buffer_insert_num(MUSTACHE_BUFFER* buf, size_t off, uint64_t num) +{ + uint8_t tmp[16]; + size_t n = 0; + + tmp[15 - n++] = num & 0x7f; + + while(1) { + num = num >> 7; + if(num == 0) + break; + tmp[15 - n++] = 0x80 | (num & 0x7f); + } + + return mustache_buffer_insert(buf, off, tmp+16-n, n); +} + +static inline int +mustache_buffer_append_num(MUSTACHE_BUFFER* buf, uint64_t num) +{ + return mustache_buffer_insert_num(buf, buf->n, num); +} + +static uint64_t +mustache_decode_num(const uint8_t* data, size_t off, size_t* p_off) +{ + uint64_t num = 0; + + while(data[off] >= 0x80) { + num |= (data[off++] & 0x7f); + num = num << 7; + } + + num |= data[off++]; + *p_off = off; + return num; +} + + +/**************************** + *** Stack Implementation *** + ****************************/ + +typedef MUSTACHE_BUFFER MUSTACHE_STACK; + +static inline void +mustache_stack_free(MUSTACHE_STACK* stack) +{ + mustache_buffer_free(stack); +} + +static inline int +mustache_stack_is_empty(MUSTACHE_STACK* stack) +{ + return (stack->n == 0); +} + +static inline int +mustache_stack_push(MUSTACHE_STACK* stack, uintptr_t item) +{ + return mustache_buffer_append(stack, &item, sizeof(uintptr_t)); +} + +static inline uintptr_t +mustache_stack_peek(MUSTACHE_STACK* stack) +{ + return *((uintptr_t*)(stack->data + (stack->n - sizeof(uintptr_t)))); +} + +static inline uintptr_t +mustache_stack_pop(MUSTACHE_STACK* stack) +{ + uintptr_t item = mustache_stack_peek(stack); + stack->n -= sizeof(uintptr_t); + return item; +} + + +/*************************** + *** Parsing & Compiling *** + ***************************/ + +#define MUSTACHE_ISANYOF2(ch, ch1, ch2) ((ch) == (ch1) || (ch) == (ch2)) +#define MUSTACHE_ISANYOF4(ch, ch1, ch2, ch3, ch4) ((ch) == (ch1) || (ch) == (ch2) || (ch) == (ch3) || (ch) == (ch4)) + +#define MUSTACHE_ISWHITESPACE(ch) MUSTACHE_ISANYOF4((ch), ' ', '\t', '\v', '\f') +#define MUSTACHE_ISNEWLINE(ch) MUSTACHE_ISANYOF2((ch), '\r', '\n') + +/* Keep in sync with MUSTACHE_ERR_xxx constants. */ +static const char* mustache_err_messages[] = { + "Success.", + "Tag opener has no closer.", + "Tag closer has no opener.", + "Tag closer is incompatible with its opener.", + "Tag has no name.", + "Tag name is invalid.", + "Section-opening tag has no closer.", + "Section-closing tag has no opener.", + "Name of section-closing tag does not match corresponding section-opening tag.", + "The section-opening is located here.", + "Invalid specification of delimiters." +}; + +/* For the given template, we construct list of MUSTACHE_TAGINFO structures. + * Along the way, we also check for any parsing errors and report them + * to the app. + */ + +typedef enum MUSTACHE_TAGTYPE { + MUSTACHE_TAGTYPE_NONE = 0, + MUSTACHE_TAGTYPE_DELIM, /* {{=@ @=}} */ + MUSTACHE_TAGTYPE_COMMENT, /* {{! comment }} */ + MUSTACHE_TAGTYPE_VAR, /* {{ var }} */ + MUSTACHE_TAGTYPE_VERBATIMVAR, /* {{{ var }}} */ + MUSTACHE_TAGTYPE_VERBATIMVAR2, /* {{& var }} */ + MUSTACHE_TAGTYPE_OPENSECTION, /* {{# section }} */ + MUSTACHE_TAGTYPE_OPENSECTIONINV, /* {{^ section }} */ + MUSTACHE_TAGTYPE_CLOSESECTION, /* {{/ section }} */ + MUSTACHE_TAGTYPE_CLOSESECTIONINV, + MUSTACHE_TAGTYPE_PARTIAL, /* {{> partial }} */ + MUSTACHE_TAGTYPE_INDENT /* for internal purposes. */ +} MUSTACHE_TAGTYPE; + +typedef struct MUSTACHE_TAGINFO { + MUSTACHE_TAGTYPE type; + size_t line; + size_t col; + size_t beg; + size_t end; + size_t name_beg; + size_t name_end; +} MUSTACHE_TAGINFO; + +static void +mustache_parse_error(int err_code, const char* msg, + unsigned line, unsigned column, void* parser_data) +{ + /* noop */ + (void)err_code; + (void)msg; + (void)line; + (void)column; + (void)parser_data; +} + +static int +mustache_is_std_closer(const char* closer, size_t closer_len) +{ + size_t off; + + for(off = 0; off < closer_len; off++) { + if(closer[off] != '}') + return 0; + } + + return 1; +} + +static int +mustache_validate_tagname(const char* tagname, size_t size) +{ + size_t off; + + if(size == 1 && tagname[0] == '.') + return 0; + + /* Verify there is no whitespace and that '.' is used only as a delimiter + * of non-empty tokens. */ + if(tagname[0] == '.' || tagname[size-1] == '.') + return -1; + for(off = 0; off < size; off++) { + if(MUSTACHE_ISWHITESPACE(tagname[off])) + return -1; + if(tagname[off] == '.' && off+1 < size && tagname[off+1] == '.') + return -1; + } + + return 0; +} + +static int +mustache_validate_sections(const char* templ_data, MUSTACHE_BUFFER* tags_buffer, + const MUSTACHE_PARSER* parser, void* parser_data) +{ + MUSTACHE_TAGINFO* tags = (MUSTACHE_TAGINFO*) tags_buffer->data; + unsigned n_tags = tags_buffer->n / sizeof(MUSTACHE_TAGINFO); + unsigned i; + MUSTACHE_STACK section_stack = { 0 }; + MUSTACHE_TAGINFO* opener; + int n_errors = 0; + int ret = -1; + + for(i = 0; i < n_tags; i++) { + switch(tags[i].type) { + case MUSTACHE_TAGTYPE_OPENSECTION: + case MUSTACHE_TAGTYPE_OPENSECTIONINV: + if(mustache_stack_push(§ion_stack, (uintptr_t) &tags[i]) != 0) + goto err; + break; + + case MUSTACHE_TAGTYPE_CLOSESECTION: + case MUSTACHE_TAGTYPE_CLOSESECTIONINV: + if(mustache_stack_is_empty(§ion_stack)) { + parser->parse_error(MUSTACHE_ERR_DANGLINGSECTIONCLOSER, + mustache_err_messages[MUSTACHE_ERR_DANGLINGSECTIONCLOSER], + (unsigned)tags[i].line, (unsigned)tags[i].col, + parser_data); + n_errors++; + } else { + opener = (MUSTACHE_TAGINFO*) mustache_stack_pop(§ion_stack); + + if(opener->name_end - opener->name_beg != tags[i].name_end - tags[i].name_beg || + strncmp(templ_data + opener->name_beg, + templ_data + tags[i].name_beg, + opener->name_end - opener->name_beg) != 0) + { + parser->parse_error(MUSTACHE_ERR_SECTIONNAMEMISMATCH, + mustache_err_messages[MUSTACHE_ERR_SECTIONNAMEMISMATCH], + (unsigned)tags[i].line, (unsigned)tags[i].col, + parser_data); + parser->parse_error(MUSTACHE_ERR_SECTIONOPENERHERE, + mustache_err_messages[MUSTACHE_ERR_SECTIONOPENERHERE], + (unsigned)opener->line, (unsigned)opener->col, + parser_data); + n_errors++; + } + + if(opener->type == MUSTACHE_TAGTYPE_OPENSECTIONINV) + tags[i].type = MUSTACHE_TAGTYPE_CLOSESECTIONINV; + } + break; + + default: + break; + } + } + + if(!mustache_stack_is_empty(§ion_stack)) { + while(!mustache_stack_is_empty(§ion_stack)) { + opener = (MUSTACHE_TAGINFO*) mustache_stack_pop(§ion_stack); + + parser->parse_error(MUSTACHE_ERR_DANGLINGSECTIONOPENER, + mustache_err_messages[MUSTACHE_ERR_DANGLINGSECTIONOPENER], + (unsigned)opener->line, (unsigned)opener->col, + parser_data); + n_errors++; + } + } + + if(n_errors == 0) + ret = 0; + +err: + mustache_stack_free(§ion_stack); + return ret; +} + +static int +mustache_parse_delimiters(const char* delim_spec, size_t size, + char* opener, size_t* p_opener_len, + char* closer, size_t* p_closer_len) +{ + size_t opener_beg, opener_end; + size_t closer_beg, closer_end; + + opener_beg = 0; + + opener_end = opener_beg; + while(opener_end < size) { + if(MUSTACHE_ISWHITESPACE(delim_spec[opener_end])) + break; + if(delim_spec[opener_end] == '=') + return -1; + opener_end++; + } + if(opener_end <= opener_beg || opener_end - opener_beg > MUSTACHE_MAXOPENERLENGTH) + return -1; + + closer_beg = opener_end; + while(closer_beg < size) { + if(!MUSTACHE_ISWHITESPACE(delim_spec[closer_beg])) + break; + closer_beg++; + } + if(closer_beg <= opener_end) + return -1; + + closer_end = closer_beg; + while(closer_end < size) { + if(MUSTACHE_ISWHITESPACE(delim_spec[closer_end])) + return -1; + closer_end++; + } + if(closer_end <= closer_beg || closer_end - closer_beg > MUSTACHE_MAXCLOSERLENGTH) + return -1; + if(closer_end != size) + return -1; + + memcpy(opener, delim_spec + opener_beg, opener_end - opener_beg); + *p_opener_len = opener_end - opener_beg; + memcpy(closer, delim_spec + closer_beg, closer_end - closer_beg); + *p_closer_len = closer_end - closer_beg; + return 0; +} + +static int +mustache_parse(const char* templ_data, size_t templ_size, + const MUSTACHE_PARSER* parser, void* parser_data, + MUSTACHE_TAGINFO** p_tags, unsigned* p_n_tags) +{ + int n_errors = 0; + char opener[MUSTACHE_MAXOPENERLENGTH] = MUSTACHE_DEFAULTOPENER; + char closer[MUSTACHE_MAXCLOSERLENGTH] = MUSTACHE_DEFAULTCLOSER; + size_t opener_len; + size_t closer_len; + size_t off = 0; + size_t line = 1; + size_t col = 1; + MUSTACHE_TAGINFO current_tag; + MUSTACHE_BUFFER tags = { 0 }; + + /* If this template will ever be used as a partial, it may inherit an + * extra indentation from parent template, so we mark every line beginning + * with the dummy tag for further processing in mustache_compile(). */ + if(off < templ_size) { + current_tag.type = MUSTACHE_TAGTYPE_INDENT; + current_tag.beg = off; + current_tag.end = off; + if(mustache_buffer_append(&tags, ¤t_tag, sizeof(MUSTACHE_TAGINFO)) != 0) + goto err; + } + + current_tag.type = MUSTACHE_TAGTYPE_NONE; + + opener_len = strlen(MUSTACHE_DEFAULTOPENER); + closer_len = strlen(MUSTACHE_DEFAULTCLOSER); + + while(off < templ_size) { + int is_opener, is_closer; + + is_opener = (off + opener_len <= templ_size && memcmp(templ_data+off, opener, opener_len) == 0); + is_closer = (off + closer_len <= templ_size && memcmp(templ_data+off, closer, closer_len) == 0); + if(is_opener && is_closer) { + /* Opener and closer may be defined to be the same string. + * Consider for example "{{=@ @=}}". + * Determine the real meaning from current parser state: + */ + if(current_tag.type == MUSTACHE_TAGTYPE_NONE) + is_closer = 0; + else + is_opener = 0; + } + + if(is_opener) { + /* Handle tag opener "{{" */ + + if(current_tag.type != MUSTACHE_TAGTYPE_NONE && current_tag.type != MUSTACHE_TAGTYPE_COMMENT) { + /* Opener after some previous opener??? */ + parser->parse_error(MUSTACHE_ERR_DANGLINGTAGOPENER, + mustache_err_messages[MUSTACHE_ERR_DANGLINGTAGOPENER], + (unsigned)current_tag.line, (unsigned)current_tag.col, + parser_data); + n_errors++; + current_tag.type = MUSTACHE_TAGTYPE_NONE; + } + + current_tag.line = line; + current_tag.col = col; + current_tag.beg = off; + off += opener_len; + + if(off < templ_size) { + switch(templ_data[off]) { + case '=': current_tag.type = MUSTACHE_TAGTYPE_DELIM; off++; break; + case '!': current_tag.type = MUSTACHE_TAGTYPE_COMMENT; off++; break; + case '{': current_tag.type = MUSTACHE_TAGTYPE_VERBATIMVAR; off++; break; + case '&': current_tag.type = MUSTACHE_TAGTYPE_VERBATIMVAR2; off++; break; + case '#': current_tag.type = MUSTACHE_TAGTYPE_OPENSECTION; off++; break; + case '^': current_tag.type = MUSTACHE_TAGTYPE_OPENSECTIONINV; off++; break; + case '/': current_tag.type = MUSTACHE_TAGTYPE_CLOSESECTION; off++; break; + case '>': current_tag.type = MUSTACHE_TAGTYPE_PARTIAL; off++; break; + default: current_tag.type = MUSTACHE_TAGTYPE_VAR; break; + } + } + + while(off < templ_size && MUSTACHE_ISWHITESPACE(templ_data[off])) + off++; + current_tag.name_beg = off; + + col += current_tag.name_beg - current_tag.beg; + } else if(is_closer && current_tag.type == MUSTACHE_TAGTYPE_NONE) { + /* Invalid closer. */ + parser->parse_error(MUSTACHE_ERR_DANGLINGTAGCLOSER, + mustache_err_messages[MUSTACHE_ERR_DANGLINGTAGCLOSER], + (unsigned) line, (unsigned) col, parser_data); + n_errors++; + off++; + col++; + } else if(is_closer) { + /* Handle tag closer "}}" */ + + current_tag.name_end = off; + off += closer_len; + col += closer_len; + if(current_tag.type == MUSTACHE_TAGTYPE_VERBATIMVAR) { + /* Eat the extra '}'. Note it may be after the found + * closer (if closer is "}}" or before it for a custom + * closer. */ + if(current_tag.name_end > current_tag.name_beg && + templ_data[current_tag.name_end-1] == '}') { + current_tag.name_end--; + } else if(mustache_is_std_closer(closer, closer_len) && + off < templ_size && templ_data[off] == '}') { + off++; + col++; + } else { + parser->parse_error(MUSTACHE_ERR_INCOMPATIBLETAGCLOSER, + mustache_err_messages[MUSTACHE_ERR_INCOMPATIBLETAGCLOSER], + (unsigned) line, (unsigned) col, parser_data); + n_errors++; + } + } else if(current_tag.type == MUSTACHE_TAGTYPE_DELIM) { + /* Maybe we are not really the closer. Maybe the directive + * does not change the closer so we are the "new closer" in + * something like "{{=<something> }}=}}". */ + if(templ_data[current_tag.name_end - 1] != '=' && + off + closer_len < templ_size && + templ_data[off] == '=' && + memcmp(templ_data + off + 1, closer, closer_len) == 0) + { + current_tag.name_end += closer_len + 1; + off += closer_len + 1; + col += closer_len + 1; + } + + if(templ_data[current_tag.name_end - 1] != '=') { + parser->parse_error(MUSTACHE_ERR_INCOMPATIBLETAGCLOSER, + mustache_err_messages[MUSTACHE_ERR_INCOMPATIBLETAGCLOSER], + (unsigned) line, (unsigned) col, parser_data); + n_errors++; + } else if(current_tag.name_end > current_tag.name_beg) { + current_tag.name_end--; /* Consume the closer's '=' */ + } + } + + current_tag.end = off; + + /* If the tag is standalone, expand it to consume also any + * preceding whitespace and also one new-line (before or after). */ + if(current_tag.type != MUSTACHE_TAGTYPE_VAR && + current_tag.type != MUSTACHE_TAGTYPE_VERBATIMVAR && + current_tag.type != MUSTACHE_TAGTYPE_VERBATIMVAR2 && + (current_tag.end >= templ_size || MUSTACHE_ISNEWLINE(templ_data[current_tag.end]))) + { + size_t tmp_off = current_tag.beg; + while(tmp_off > 0 && MUSTACHE_ISWHITESPACE(templ_data[tmp_off-1])) + tmp_off--; + if(tmp_off == 0 || MUSTACHE_ISNEWLINE(templ_data[tmp_off-1])) { + current_tag.beg = tmp_off; + + if(current_tag.end < templ_size && templ_data[current_tag.end] == '\r') + current_tag.end++; + if(current_tag.end < templ_size && templ_data[current_tag.end] == '\n') + current_tag.end++; + } + } + + while(current_tag.name_end > current_tag.name_beg && + MUSTACHE_ISWHITESPACE(templ_data[current_tag.name_end-1])) + current_tag.name_end--; + + if(current_tag.type != MUSTACHE_TAGTYPE_COMMENT) { + if(current_tag.name_end <= current_tag.name_beg) { + parser->parse_error(MUSTACHE_ERR_NOTAGNAME, + mustache_err_messages[MUSTACHE_ERR_NOTAGNAME], + (unsigned)current_tag.line, (unsigned)current_tag.col, + parser_data); + n_errors++; + } + } + + if(current_tag.type == MUSTACHE_TAGTYPE_DELIM) { + if(mustache_parse_delimiters(templ_data + current_tag.name_beg, + current_tag.name_end - current_tag.name_beg, + opener, &opener_len, closer, &closer_len) != 0) + { + parser->parse_error(MUSTACHE_ERR_INVALIDDELIMITERS, + mustache_err_messages[MUSTACHE_ERR_INVALIDDELIMITERS], + (unsigned)current_tag.line, (unsigned)current_tag.col, + parser_data); + n_errors++; + } + + /* From now on, ignore this tag. */ + current_tag.type = MUSTACHE_TAGTYPE_COMMENT; + } + + if(current_tag.type != MUSTACHE_TAGTYPE_COMMENT) { + if(mustache_validate_tagname(templ_data + current_tag.name_beg, + current_tag.name_end - current_tag.name_beg) != 0) { + parser->parse_error(MUSTACHE_ERR_INVALIDTAGNAME, + mustache_err_messages[MUSTACHE_ERR_INVALIDTAGNAME], + (unsigned)current_tag.line, (unsigned)current_tag.col, + parser_data); + n_errors++; + } + } + + /* Remember the tag info. */ + if(mustache_buffer_append(&tags, ¤t_tag, sizeof(MUSTACHE_TAGINFO)) != 0) + goto err; + + current_tag.type = MUSTACHE_TAGTYPE_NONE; + } else if(MUSTACHE_ISNEWLINE(templ_data[off])) { + /* Handle end of line. */ + + if(current_tag.type != MUSTACHE_TAGTYPE_NONE && current_tag.type != MUSTACHE_TAGTYPE_COMMENT) { + parser->parse_error(MUSTACHE_ERR_DANGLINGTAGOPENER, + mustache_err_messages[MUSTACHE_ERR_DANGLINGTAGOPENER], + (unsigned)current_tag.line, (unsigned)current_tag.col, + parser_data); + n_errors++; + current_tag.type = MUSTACHE_TAGTYPE_NONE; + } + + /* New line may be formed by digraph "\r\n". */ + if(templ_data[off] == '\r') + off++; + if(off < templ_size && templ_data[off] == '\n') + off++; + + if(current_tag.type == MUSTACHE_TAGTYPE_NONE && off < templ_size) { + current_tag.type = MUSTACHE_TAGTYPE_INDENT; + current_tag.beg = off; + current_tag.end = off; + if(mustache_buffer_append(&tags, ¤t_tag, sizeof(MUSTACHE_TAGINFO)) != 0) + goto err; + current_tag.type = MUSTACHE_TAGTYPE_NONE; + } + + line++; + col = 1; + } else { + /* Handle any other character. */ + off++; + col++; + } + } + + if(mustache_validate_sections(templ_data, &tags, parser, parser_data) != 0) + goto err; + + /* Add an extra dummy tag marking end of the template. */ + current_tag.type = MUSTACHE_TAGTYPE_NONE; + current_tag.beg = templ_size; + current_tag.end = templ_size; + if(mustache_buffer_append(&tags, ¤t_tag, sizeof(MUSTACHE_TAGINFO)) != 0) + goto err; + + /* Success? */ + if(n_errors == 0) { + *p_tags = (MUSTACHE_TAGINFO*) tags.data; + *p_n_tags = tags.n / sizeof(MUSTACHE_TAGINFO); + return 0; + } + + /* Error path. */ +err: + mustache_buffer_free(&tags); + *p_tags = NULL; + *p_n_tags = 0; + return -1; +} + + +/* The compiled template is a sequence of following instruction types. + * The instructions have two types of arguments: + * -- NUM: a number encoded with mustache_buffer_[append|insert]_num(). + * -- STR: a string (always preceded with a NUM denoting its length). + */ + +/* Instruction denoting end of template. + */ +#define MUSTACHE_OP_EXIT 0 + +/* Instruction for outputting a literal text. + * + * Arg #1: Length of the literal string (NUM). + * Arg #2: The literal string (STR). + */ +#define MUSTACHE_OP_LITERAL 1 + +/* Instruction to resolve a tag name. + * + * Arg #1: (Relative) setjmp value (NUM). + * Arg #2: Count of names (NUM). + * Arg #3: Length of the 1st tag name (NUM). + * Arg #4: The tag name (STR). + * etc. (more names follow, up to the count in arg #2) + * + * Registers: reg_node is set to the resolved node, or NULL. + * reg_jmpaddr is set to address where some next instruction may + * want to jump on some condition. + */ +#define MUSTACHE_OP_RESOLVE_setjmp 2 + +/* Instruction to resolve a tag name. + * + * Arg #1: Count of names (NUM). + * Arg #2: Length of the tag name (NUM). + * Arg #3: The tag name (STR). + * etc. (more names follow, up to the count in arg #1) + * + * Registers: reg_node is set to the resolved node, or NULL. + */ +#define MUSTACHE_OP_RESOLVE 3 + +/* Instructions to output a node. + * + * Registers: If it is not NULL, reg_node determines the node to output. + * Otherwise, it is noop. + */ +#define MUSTACHE_OP_OUTVERBATIM 4 +#define MUSTACHE_OP_OUTESCAPED 5 + +/* Instruction to enter a node in register reg_node, i.e. to change a lookup + * context for resolve instructions. + * + * Registers: If it is not NULL, reg_node is pushed to the stack. + * Otherwise, program counter is changed to address in reg_jmpaddr. + */ +#define MUSTACHE_OP_ENTER 6 + +/* Instruction to leave a node. The top node in the lookup context stack is + * popped out. + * + * Arg #1: (Relative) setjmp value (NUM) for jumping back for next loop iteration. + */ +#define MUSTACHE_OP_LEAVE 7 + +/* Instruction to open inverted section. + * Note there is no MUSTACHE_OP_LEAVEINV instruction as it is noop. + * + * Registers: If reg_node is NULL, continues normally. + * Otherwise, program counter is changed to address in reg_jmpaddr. + */ +#define MUSTACHE_OP_ENTERINV 8 + +/* Instruction to enter a partial. + * + * Arg #1: Length of the partial name (NUM). + * Arg #2: The partial name (STR). + * Arg #3: Length of the indentation string (NUM). + * Arg #4: Indentation, i.e. string composed of whitespace characters (STR). + */ +#define MUSTACHE_OP_PARTIAL 9 + +/* Instruction to insert extra indentation (inherited from parent templates). + */ +#define MUSTACHE_OP_INDENT 10 + + +static int +mustache_compile_tagname(MUSTACHE_BUFFER* insns, const char* name, size_t size) +{ + unsigned n_tokens = 1; + unsigned i; + size_t tok_beg, tok_end; + + if(size == 1 && name[0] == '.') { + /* Implicit iterator. */ + n_tokens = 0; + } else { + for(i = 0; i < size; i++) { + if(name[i] == '.') + n_tokens++; + } + } + + if(mustache_buffer_append_num(insns, n_tokens) != 0) + return -1; + + tok_beg = 0; + for(i = 0; i < n_tokens; i++) { + tok_end = tok_beg; + while(tok_end < size && name[tok_end] != '.') + tok_end++; + + if(mustache_buffer_append_num(insns, tok_end - tok_beg) != 0) + return -1; + if(mustache_buffer_append(insns, name + tok_beg, tok_end - tok_beg) != 0) + return -1; + + tok_beg = tok_end + 1; + } + + return 0; +} + +MUSTACHE_TEMPLATE* +mustache_compile(const char* templ_data, size_t templ_size, + const MUSTACHE_PARSER* parser, void* parser_data, + unsigned flags) +{ + (void)flags; + + static const MUSTACHE_PARSER default_parser = { mustache_parse_error }; + MUSTACHE_TAGINFO* tags = NULL; + unsigned n_tags; + size_t off; + size_t jmp_pos; + MUSTACHE_TAGINFO* tag; + MUSTACHE_BUFFER insns = { 0 }; + MUSTACHE_STACK jmp_pos_stack = { 0 }; + int done = 0; + int success = 0; + size_t indent_len; + + if(parser == NULL) + parser = &default_parser; + + /* Collect all tags from the template. */ + if(mustache_parse(templ_data, templ_size, parser, parser_data, &tags, &n_tags) != 0) + goto err; + + /* Build the template */ +#define APPEND(data, n) \ + do { \ + if(mustache_buffer_append(&insns, (data), (n)) != 0) \ + goto err; \ + } while(0) + +#define APPEND_NUM(num) \ + do { \ + if(mustache_buffer_append_num(&insns, (uint64_t)(num)) != 0) \ + goto err; \ + } while(0) + +#define APPEND_TAGNAME(tag) \ + do { \ + if(mustache_compile_tagname(&insns, templ_data + (tag)->name_beg, \ + (tag)->name_end - (tag)->name_beg) != 0) \ + goto err; \ + } while(0) + +#define INSERT_NUM(pos, num) \ + do { \ + if(mustache_buffer_insert_num(&insns, (pos), (uint64_t)(num)) != 0) \ + goto err; \ + } while(0) + +#define PUSH_JMP_POS() \ + do { \ + if(mustache_stack_push(&jmp_pos_stack, insns.n) != 0) \ + goto err; \ + } while(0) + +#define POP_JMP_POS() ((size_t) mustache_stack_pop(&jmp_pos_stack)) + + off = 0; + tag = &tags[0]; + while(1) { + if(off < tag->beg) { + /* Handle literal text before the next tag. */ + APPEND_NUM(MUSTACHE_OP_LITERAL); + APPEND_NUM(tag->beg - off); + APPEND(templ_data + off, tag->beg - off); + off = tag->beg; + } + + switch(tag->type) { + case MUSTACHE_TAGTYPE_VAR: + case MUSTACHE_TAGTYPE_VERBATIMVAR: + case MUSTACHE_TAGTYPE_VERBATIMVAR2: + APPEND_NUM(MUSTACHE_OP_RESOLVE); + APPEND_TAGNAME(tag); + APPEND_NUM((tag->type == MUSTACHE_TAGTYPE_VAR) ? + MUSTACHE_OP_OUTESCAPED : MUSTACHE_OP_OUTVERBATIM); + break; + + case MUSTACHE_TAGTYPE_OPENSECTION: + APPEND_NUM(MUSTACHE_OP_RESOLVE_setjmp); + PUSH_JMP_POS(); + APPEND_TAGNAME(tag); + APPEND_NUM(MUSTACHE_OP_ENTER); + PUSH_JMP_POS(); + break; + + case MUSTACHE_TAGTYPE_CLOSESECTION: + APPEND_NUM(MUSTACHE_OP_LEAVE); + APPEND_NUM(insns.n - POP_JMP_POS()); + jmp_pos = POP_JMP_POS(); + INSERT_NUM(jmp_pos, insns.n - jmp_pos); + break; + + case MUSTACHE_TAGTYPE_OPENSECTIONINV: + APPEND_NUM(MUSTACHE_OP_RESOLVE_setjmp); + PUSH_JMP_POS(); + APPEND_TAGNAME(tag); + APPEND_NUM(MUSTACHE_OP_ENTERINV); + break; + + case MUSTACHE_TAGTYPE_CLOSESECTIONINV: + jmp_pos = POP_JMP_POS(); + INSERT_NUM(jmp_pos, insns.n - jmp_pos); + break; + + case MUSTACHE_TAGTYPE_PARTIAL: + APPEND_NUM(MUSTACHE_OP_PARTIAL); + APPEND_NUM(tag->name_end - tag->name_beg); + APPEND(templ_data + tag->name_beg, tag->name_end - tag->name_beg); + indent_len = 0; + while(MUSTACHE_ISWHITESPACE(templ_data[tag->beg + indent_len])) + indent_len++; + APPEND_NUM(indent_len); + APPEND(templ_data + tag->beg, indent_len); + break; + + case MUSTACHE_TAGTYPE_INDENT: + APPEND_NUM(MUSTACHE_OP_INDENT); + break; + + case MUSTACHE_TAGTYPE_NONE: + APPEND_NUM(MUSTACHE_OP_EXIT); + done = 1; + break; + + default: + break; + } + + if(done) + break; + + off = tag->end; + tag++; + } + + success = 1; + +err: + free(tags); + mustache_buffer_free(&jmp_pos_stack); + if(success) { + return (MUSTACHE_TEMPLATE*) insns.data; + } else { + mustache_buffer_free(&insns); + return NULL; + } +} + +void +mustache_release(MUSTACHE_TEMPLATE* t) +{ + if(t == NULL) + return; + + free(t); +} + + +/********************************** + *** Applying Compiled Template *** + **********************************/ + +int +mustache_process(const MUSTACHE_TEMPLATE* t, + const MUSTACHE_RENDERER* renderer, void* renderer_data, + const MUSTACHE_DATAPROVIDER* provider, void* provider_data) +{ + const uint8_t* insns = (const uint8_t*) t; + size_t reg_pc = 0; /* Program counter register. */ + size_t reg_jmpaddr; /* Jump target address register. */ + void* reg_node = NULL; /* Working node register. */ + int done = 0; + MUSTACHE_STACK node_stack = { 0 }; + MUSTACHE_STACK index_stack = { 0 }; + MUSTACHE_STACK partial_stack = { 0 }; + MUSTACHE_BUFFER indent_buffer = { 0 }; + int ret = -1; + +#define PUSH_NODE() \ + do { \ + if(mustache_stack_push(&node_stack, (uintptr_t) reg_node) != 0) \ + goto err; \ + } while(0) + +#define POP_NODE() ((void*) mustache_stack_pop(&node_stack)) + +#define PEEK_NODE() ((void*) mustache_stack_peek(&node_stack)) + +#define PUSH_INDEX(index) \ + do { \ + if(mustache_stack_push(&index_stack, (uintptr_t) (index)) != 0) \ + goto err; \ + } while(0) + +#define POP_INDEX() ((unsigned) mustache_stack_pop(&index_stack)) + + reg_node = provider->get_root(provider_data); + PUSH_NODE(); + + while(!done) { + unsigned opcode = (unsigned) mustache_decode_num(insns, reg_pc, ®_pc); + + switch(opcode) { + case MUSTACHE_OP_LITERAL: + { + size_t n = (size_t) mustache_decode_num(insns, reg_pc, ®_pc); + if(renderer->out_verbatim((const char*)(insns + reg_pc), n, renderer_data) != 0) + goto err; + reg_pc += n; + break; + } + + case MUSTACHE_OP_RESOLVE_setjmp: + { + size_t jmp_len = (size_t) mustache_decode_num(insns, reg_pc, ®_pc); + reg_jmpaddr = reg_pc + jmp_len; + /* Pass through */ + } + + case MUSTACHE_OP_RESOLVE: + { + unsigned n_names = (unsigned) mustache_decode_num(insns, reg_pc, ®_pc); + unsigned i; + + if(n_names == 0) { + /* Implicit iterator. */ + reg_node = PEEK_NODE(); + break; + } + + for(i = 0; i < n_names; i++) { + size_t name_len = (size_t) mustache_decode_num(insns, reg_pc, ®_pc); + const char* name = (const char*)(insns + reg_pc); + reg_pc += name_len; + + if(i == 0) { + void** nodes = (void**) node_stack.data; + size_t n_nodes = node_stack.n / sizeof(void*); + + while(n_nodes-- > 0) { + reg_node = provider->get_child_by_name(nodes[n_nodes], + name, name_len, provider_data); + if(reg_node != NULL) + break; + } + } else if(reg_node != NULL) { + reg_node = provider->get_child_by_name(reg_node, + name, name_len, provider_data); + } + } + break; + } + + case MUSTACHE_OP_OUTVERBATIM: + case MUSTACHE_OP_OUTESCAPED: + if(reg_node != NULL) { + int (*out)(const char*, size_t, void*); + + out = (opcode == MUSTACHE_OP_OUTVERBATIM) ? + renderer->out_verbatim : renderer->out_escaped; + if(provider->dump(reg_node, out, renderer_data, provider_data) != 0) + goto err; + } + break; + + case MUSTACHE_OP_ENTER: + if(reg_node != NULL) { + PUSH_NODE(); + reg_node = provider->get_child_by_index(reg_node, 0, provider_data); + if(reg_node != NULL) { + PUSH_NODE(); + PUSH_INDEX(0); + } else { + (void) POP_NODE(); + } + } + if(reg_node == NULL) + reg_pc = reg_jmpaddr; + break; + + case MUSTACHE_OP_LEAVE: + { + size_t jmp_base = reg_pc; + size_t jmp_len = (size_t) mustache_decode_num(insns, reg_pc, ®_pc); + unsigned index = POP_INDEX(); + + (void) POP_NODE(); + reg_node = provider->get_child_by_index(PEEK_NODE(), ++index, provider_data); + if(reg_node != NULL) { + PUSH_NODE(); + PUSH_INDEX(index); + reg_pc = jmp_base - jmp_len; + } else { + (void) POP_NODE(); + } + break; + } + + case MUSTACHE_OP_ENTERINV: + if(reg_node == NULL || provider->get_child_by_index(reg_node, + 0, provider_data) == NULL) { + /* Resolve failed: Noop, continue normally. */ + } else { + reg_pc = reg_jmpaddr; + } + break; + + case MUSTACHE_OP_PARTIAL: + { + size_t name_len; + const char* name; + size_t indent_len; + const char* indent; + MUSTACHE_TEMPLATE* partial; + + name_len = mustache_decode_num(insns, reg_pc, ®_pc); + name = (const char*) (insns + reg_pc); + reg_pc += name_len; + + indent_len = mustache_decode_num(insns, reg_pc, ®_pc); + indent = (const char*) (insns + reg_pc); + reg_pc += indent_len; + + partial = provider->get_partial(name, name_len, provider_data); + if(partial != NULL) { + if(mustache_stack_push(&partial_stack, (uintptr_t) insns) != 0) + goto err; + if(mustache_stack_push(&partial_stack, (uintptr_t) reg_pc) != 0) + goto err; + if(mustache_stack_push(&partial_stack, (uintptr_t) indent_len) != 0) + goto err; + if(mustache_buffer_append(&indent_buffer, indent, indent_len) != 0) + goto err; + reg_pc = 0; + insns = (uint8_t*) partial; + } + break; + } + + case MUSTACHE_OP_INDENT: + if(renderer->out_verbatim((const char*)(indent_buffer.data), + indent_buffer.n, renderer_data) != 0) + goto err; + break; + + case MUSTACHE_OP_EXIT: + if(mustache_stack_is_empty(&partial_stack)) { + done = 1; + } else { + size_t indent_len = (size_t) mustache_stack_pop(&partial_stack); + reg_pc = (size_t) mustache_stack_pop(&partial_stack); + insns = (uint8_t*) mustache_stack_pop(&partial_stack); + + indent_buffer.n -= indent_len; + } + break; + } + } + + /* Success. */ + ret = 0; + +err: + mustache_stack_free(&node_stack); + mustache_stack_free(&index_stack); + mustache_stack_free(&partial_stack); + mustache_buffer_free(&indent_buffer); + return ret; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/extern/libmustache4c/mustache.h Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,212 @@ +/* + * Mustache4C + * (http://github.com/mity/mustache4c) + * + * Copyright (c) 2017 Martin Mitas + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ + +#ifndef MUSTACHE4C_H +#define MUSTACHE4C_H + +#include <stdlib.h> + +#ifdef __cplusplus +extern "C" { +#endif + + +typedef struct MUSTACHE_TEMPLATE MUSTACHE_TEMPLATE; + + +#define MUSTACHE_ERR_SUCCESS (0) +#define MUSTACHE_ERR_DANGLINGTAGOPENER (1) +#define MUSTACHE_ERR_DANGLINGTAGCLOSER (2) +#define MUSTACHE_ERR_INCOMPATIBLETAGCLOSER (3) +#define MUSTACHE_ERR_NOTAGNAME (4) +#define MUSTACHE_ERR_INVALIDTAGNAME (5) +#define MUSTACHE_ERR_DANGLINGSECTIONOPENER (6) +#define MUSTACHE_ERR_DANGLINGSECTIONCLOSER (7) +#define MUSTACHE_ERR_SECTIONNAMEMISMATCH (8) +#define MUSTACHE_ERR_SECTIONOPENERHERE (9) +#define MUSTACHE_ERR_INVALIDDELIMITERS (10) + + +typedef struct MUSTACHE_PARSER { + void (*parse_error)(int /*err_code*/, const char* /*msg*/, + unsigned /*line*/, unsigned /*column*/, void* /*parser_data*/); +} MUSTACHE_PARSER; + + +/** + * An interface the application has to implement, in order to output the result + * of template processing. + */ +typedef struct MUSTACHE_RENDERER { + /** + * Called to output the given text as it is. + * + * Non-zero return value aborts mustache_process(). + */ + int (*out_verbatim)(const char* /*output*/, size_t /*size*/, void* /*renderer_data*/); + + /** + * Called to output the given text. Implementation has to escape it + * appropriately with respect to the output format. E.g. for HTML output, + * "<" should be translated to "<" etc. + * + * Non-zero return value aborts mustache_process(). + * + * If no escaping is desired, it can be pointer to the same function + * as out_verbatim. + */ + int (*out_escaped)(const char* /*output*/, size_t /*size*/, void* /*renderer_data*/); +} MUSTACHE_RENDERER; + + +/** + * An interface the application has to implement, in order to feed + * mustache_process() with data the template asks for. + * + * Tree hierarchy, immutable during the mustache_process() call, is assumed. + * Each node of the hierarchy has to be uniquely identified by some pointer. + * + * The mustache_process() never dereferences any of the pointers. It only + * uses them to refer to that node when calling any data provider callback. + */ +typedef struct MUSTACHE_DATAPROVIDER { + /** + * Called to output contents of the given node. One of the MUSTACHE_PARSER + * output functions is provided, depending on the type of the mustache tag + * (`{{...}}` versus `{{{...}}}` ). Implementation of dump() may call that + * function arbitrarily. + * + * In many applications, it is not desirable/expected to be able dumping + * specific nodes (e.g. if the node is list or array forming the data + * tree hierarchy). In such cases, the implementation is allowed to just + * return zero without calling the provided callback at all, output some + * dummy string (e.g. "<<object>>"), or return non-zero value as an error + * sign, depending what makes better sense for the application. + * + * Implementation of dump() must propagate renderer_data into the + * callback as its last argument. + * + * Non-zero return value aborts mustache_process(). Typically, the + * implementations should do so if any call of out_fn callback fails. + */ + int (*dump)(void* /*node*/, int (* /*out_fn*/)(const char*, size_t, void*), + void* /*renderer_data*/, void* /*provider_data*/); + + /** + * Called once at the start of mustache_process(). It sets the initial + * lookup context. */ + void* (*get_root)(void* /*provider_data*/); + + /** + * Called to get named item of the current node, or NULL if there is no item. + * + * If the node is not of appropriate type (e.g. if it is an array of + * values), NULL has to be returned. + */ + void* (*get_child_by_name)(void* /*node*/, const char* /*name*/, + size_t /*size*/, void* /*provider_data*/); + + /** + * Called to get an indexed item of the current node, or NULL if there is + * no such item. + * + * The main use is for iterating over arrays. + * + * However note that accordingly to the mustache specification, single + * values (except FALSE, NULL, or empty lists) have to be iterable too. + * For such simple values, the callback should return the node itself + * for index 0, and NULL for any other index. + */ + void* (*get_child_by_index)(void* /*node*/, unsigned /*index*/, + void* /*provider_data*/); + + /** + * Called to get a partial template when mustache_process() handles + * a partial tag `{{>name}}`. + * + * Implementation should perform lookup for the template (compile it, if + * it is not), and return the template handle. + * + * If the lookup fails, the implementation reports it by returning NULL. + */ + MUSTACHE_TEMPLATE* (*get_partial)(const char* /*name*/, size_t /*size*/, + void* /*provider_data*/); +} MUSTACHE_DATAPROVIDER; + + +/** + * Compile template text into a form suitable for mustache_process(). + * + * If application processes multiple input data with a single template, it is + * recommended to cache and reuse the compiled template as much as possible, + * as the compiling may be relatively time-consuming operation. + * + * @param templ_data Text of the template. + * @param templ_size Length of the template text. + * @param parser Pointer to structure with parser callbacks. May be @c NULL. + * @param parser_data Pointer just propagated into the parser callbacks. + * @param flags Unused, use zero. + * @return Pointer to the compiled template, or @c NULL on an error. + */ +MUSTACHE_TEMPLATE* mustache_compile(const char* templ_data, size_t templ_size, + const MUSTACHE_PARSER* parser, void* parser_data, + unsigned flags); + +/** + * Release the template compiled with @c mustache_compile(). + * + * @param t The template. + */ +void mustache_release(MUSTACHE_TEMPLATE* t); + +/** + * Process the template. + * + * The function outputs (via MUSTACHE_RENDERER::out_verbatim()) most of the + * text of the template. Whenever it reaches a mustache tag, it calls + * appropriate callback of MUSTACHE_DATAPROVIDER to change lookup context + * or a callback of MUSTACHE_RENDERER to output contents of the current + * context. + * + * @param t The template. + * @param renderer Pointer to structure with output callbacks. + * @param render_data Pointer just propagated to the output callbacks. + * @param provider Pointer to structure with data-providing callbacks. + * @param provider_dara Pointer just propagated to the data-providing callbacks. + * @return Zero on success, non-zero on failure. + * + * Note this operation can fail only if any callback returns an error + * and aborts the operation. + */ +int mustache_process(const MUSTACHE_TEMPLATE* t, + const MUSTACHE_RENDERER* renderer, void* renderer_data, + const MUSTACHE_DATAPROVIDER* provider, void* provider_data); + + +#ifdef __cplusplus +} +#endif + +#endif /* MUSTACHE4C_H */
--- a/lib/db.c Mon Jul 25 21:22:13 2022 +0200 +++ b/lib/db.c Tue Aug 02 13:24:13 2022 +0200 @@ -31,8 +31,12 @@ #include "sql/init.h" #include "sql/job-add.h" +#include "sql/job-list.h" #include "sql/job-todo.h" #include "sql/jobresult-add.h" +#include "sql/jobresult-list-by-job.h" +#include "sql/jobresult-list-by-job-group.h" +#include "sql/jobresult-list-by-worker.h" #include "sql/project-find.h" #include "sql/project-list.h" #include "sql/project-save.h" @@ -81,6 +85,29 @@ job->project_name = util_strdup(CHAR(sqlite3_column_text(stmt, 2))); } +static inline char * +dup(sqlite3_stmt *stmt, int col) +{ + const unsigned char *s; + + if ((s = sqlite3_column_text(stmt, col))) + return util_strdup(CHAR(s)); + + return util_strdup(""); +} + +static void +jobresult_unpacker(sqlite3_stmt *stmt, void *data) +{ + struct jobresult *r = data; + + r->id = sqlite3_column_int64(stmt, 0); + r->job_id = sqlite3_column_int64(stmt, 1); + r->worker_name = util_strdup(CHAR(sqlite3_column_text(stmt, 2))); + r->exitcode = sqlite3_column_int(stmt, 3); + r->log = dup(stmt, 4); +} + static void vbind(sqlite3_stmt *stmt, const char *fmt, va_list ap) { @@ -131,42 +158,10 @@ return ret; } -#if 0 - -static int -update(const char *sql, const char *fmt, ...) -{ - assert(sql); - assert(fmt); - - sqlite3_stmt *stmt = NULL; - va_list ap; - int ret = 1; - - if (sqlite3_prepare(db, sql, -1, &stmt, NULL) != SQLITE_OK) - return log_warn("db: %s", sqlite3_errmsg(db)), -1; - - va_start(ap, fmt); - vbind(stmt, fmt, ap); - va_end(ap); - - if (sqlite3_step(stmt) != SQLITE_DONE) - log_warn("db: %s", sqlite3_errmsg(db)); - else - ret = 0; - - sqlite3_finalize(stmt); - - return ret; -} - -#endif - static ssize_t list(struct list *sel, const char *sql, const char *args, ...) { sqlite3_stmt *stmt = NULL; - va_list ap; int step; ssize_t ret = -1; @@ -296,12 +291,28 @@ .unpack = job_unpacker, .data = jobs, .datasz = jobsz, - .elemwidth = sizeof (*jobs), + .elemwidth = sizeof (*jobs) }; return list(&sel, CHAR(sql_job_todo), "ssz", worker, worker, jobsz); } +ssize_t +db_job_list(struct job *jobs, size_t jobsz, const char *project) +{ + assert(jobs); + assert(project); + + struct list sel = { + .unpack = job_unpacker, + .data = jobs, + .datasz = jobsz, + .elemwidth = sizeof (*jobs) + }; + + return list(&sel, CHAR(sql_job_list), "sz", project, jobsz); +} + int db_jobresult_add(struct jobresult *r) { @@ -311,6 +322,52 @@ r->worker_name, r->exitcode, r->log)) < 0 ? -1 : 0; } +ssize_t +db_jobresult_list_by_job(struct jobresult *r, size_t rsz, intmax_t job_id) +{ + assert(r); + + struct list sel = { + .unpack = jobresult_unpacker, + .data = r, + .datasz = rsz, + .elemwidth = sizeof (*r) + }; + + return list(&sel, CHAR(sql_jobresult_list_by_job), "jz", job_id, rsz); +} + +ssize_t +db_jobresult_list_by_job_group(struct jobresult *r, size_t rsz, intmax_t job_id) +{ + assert(r); + + struct list sel = { + .unpack = jobresult_unpacker, + .data = r, + .datasz = rsz, + .elemwidth = sizeof (*r) + }; + + return list(&sel, CHAR(sql_jobresult_list_by_job_group), "jz", job_id, rsz); +} + +ssize_t +db_jobresult_list_by_worker(struct jobresult *r, size_t rsz, const char *worker) +{ + assert(r); + assert(worker); + + struct list sel = { + .unpack = jobresult_unpacker, + .data = r, + .datasz = rsz, + .elemwidth = sizeof (*r) + }; + + return list(&sel, CHAR(sql_jobresult_list_by_worker), "sz", worker, rsz); +} + void db_finish(void) {
--- a/lib/db.h Mon Jul 25 21:22:13 2022 +0200 +++ b/lib/db.h Tue Aug 02 13:24:13 2022 +0200 @@ -37,9 +37,21 @@ ssize_t db_job_todo(struct job *, size_t, const char *); +ssize_t +db_job_list(struct job *, size_t, const char *); + int db_jobresult_add(struct jobresult *); +ssize_t +db_jobresult_list_by_job(struct jobresult *, size_t, intmax_t); + +ssize_t +db_jobresult_list_by_job_group(struct jobresult *, size_t, intmax_t); + +ssize_t +db_jobresult_list_by_worker(struct jobresult *, size_t, const char *); + int db_project_save(struct project *);
--- a/lib/types.h Mon Jul 25 21:22:13 2022 +0200 +++ b/lib/types.h Tue Aug 02 13:24:13 2022 +0200 @@ -33,7 +33,7 @@ struct jobresult { intmax_t id; - int job_id; + intmax_t job_id; char *worker_name; int exitcode; char *log;
--- a/lib/util.c Mon Jul 25 21:22:13 2022 +0200 +++ b/lib/util.c Tue Aug 02 13:24:13 2022 +0200 @@ -173,19 +173,6 @@ return ret; } -const char * -util_path(const char *filename) -{ - assert(filename); - - /* Build path to the template file. */ - static char path[PATH_MAX]; - - //snprintf(path, sizeof (path), "%s/%s", config.themedir, filename); - - return path; -} - void util_die(const char *fmt, ...) {
--- a/scid/http.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/http.c Tue Aug 02 13:24:13 2022 +0200 @@ -32,9 +32,11 @@ #include "page-api-projects.h" #include "page-api-todo.h" #include "page-api-workers.h" +#include "page-index.h" #include "page.h" enum page { + PAGE_INDEX, /* Job results at index. */ PAGE_API, PAGE_LAST /* Not used. */ }; @@ -58,14 +60,16 @@ if (strncmp(req->path, apis[i].prefix, strlen(apis[i].prefix)) == 0) return apis[i].handler(req); - page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html"); + page(req, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html", NULL); } static const char *pages[] = { + [PAGE_INDEX] = "", [PAGE_API] = "api" }; static void (*handlers[])(struct kreq *req) = { + [PAGE_INDEX] = page_index, [PAGE_API] = dispatch_api }; @@ -77,7 +81,7 @@ log_debug("http: accessing page '%s'", req->path); if (req->page == PAGE_LAST) - page(req, NULL, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html"); + page(req, KHTTP_404, KMIME_TEXT_HTML, "pages/404.html", NULL); else handlers[req->page](req); }
--- a/scid/main.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/main.c Tue Aug 02 13:24:13 2022 +0200 @@ -16,56 +16,38 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include <limits.h> #include <stdio.h> -#include <stdnoreturn.h> -#include <string.h> +#include <stdlib.h> #include <unistd.h> -#include "db.h" -#include "log.h" #include "http.h" +#include "scid.h" +#include "util.h" -static char dbpath[PATH_MAX] = VARDIR "/db/sci/sci.db"; - -noreturn static void +static void usage(void) { - fprintf(stderr, "usage: %s [-d database] [-s sock]\n", getprogname()); + fprintf(stderr, "usage: scid [-d database] [-s sock] [-t themedir]\n"); exit(1); } -static void -init(void) -{ - log_open("scid"); - log_info("opening database %s", dbpath); - - if (db_open(dbpath) < 0) - log_die("abort: unable to open database"); -} - -static void -finish(void) -{ - db_finish(); - log_finish(); -} - int main(int argc, char **argv) { int ch; void (*run)(void) = &(http_cgi_run); - while ((ch = getopt(argc, argv, "d:f")) != -1) { + while ((ch = getopt(argc, argv, "d:ft:")) != -1) { switch (ch) { case 'd': - strlcpy(dbpath, optarg, sizeof (dbpath)); + util_strlcpy(scid.dbpath, optarg, sizeof (scid.dbpath)); break; case 'f': run = &(http_fcgi_run); break; + case 't': + util_strlcpy(scid.themedir, optarg, sizeof (scid.themedir)); + break; default: usage(); break; @@ -75,10 +57,10 @@ argc -= optind; argv += optind; - init(); + scid_init(); for (;;) run(); - finish(); + scid_finish(); }
--- a/scid/page-api-jobresults.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-api-jobresults.c Tue Aug 02 13:24:13 2022 +0200 @@ -55,9 +55,9 @@ post(struct kreq *r) { if (r->fieldsz < 1) - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); else if (save(r->fields[0].val) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -76,7 +76,7 @@ post(r); break; default: - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); break; } }
--- a/scid/page-api-jobs.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-api-jobs.c Tue Aug 02 13:24:13 2022 +0200 @@ -55,9 +55,9 @@ post(struct kreq *r) { if (r->fieldsz < 1) - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); else if (save(r->fields[0].val) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -76,7 +76,7 @@ post(r); break; default: - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); break; } }
--- a/scid/page-api-projects.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-api-projects.c Tue Aug 02 13:24:13 2022 +0200 @@ -88,7 +88,7 @@ struct project project = {0}; if (db_project_find(&project, name) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { push(r, &project); project_finish(&project); @@ -102,7 +102,7 @@ ssize_t projectsz; if ((projectsz = db_project_list(projects, UTIL_SIZE(projects))) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -143,9 +143,9 @@ post(struct kreq *r) { if (r->fieldsz < 1) - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); else if (save(r->fields[0].val) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -167,7 +167,7 @@ post(r); break; default: - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); break; }
--- a/scid/page-api-todo.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-api-todo.c Tue Aug 02 13:24:13 2022 +0200 @@ -87,7 +87,7 @@ ssize_t jobsz; if ((jobsz = db_job_todo(jobs, UTIL_SIZE(jobs), util_basename(r->path))) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -110,7 +110,7 @@ get(r); break; default: - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); break; } }
--- a/scid/page-api-workers.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-api-workers.c Tue Aug 02 13:24:13 2022 +0200 @@ -70,7 +70,7 @@ struct worker worker = {0}; if (db_worker_find(&worker, name) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { push(r, &worker); worker_finish(&worker); @@ -84,7 +84,7 @@ ssize_t workersz; if ((workersz = db_worker_list(workers, UTIL_SIZE(workers))) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -125,9 +125,9 @@ post(struct kreq *r) { if (r->fieldsz < 1) - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); else if (save(r->fields[0].val) < 0) - page(r, NULL, KHTTP_500, KMIME_APP_JSON, NULL); + page(r, KHTTP_500, KMIME_APP_JSON, NULL, NULL); else { khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_APP_JSON]); khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); @@ -149,7 +149,7 @@ post(r); break; default: - page(r, NULL, KHTTP_400, KMIME_APP_JSON, NULL); + page(r, KHTTP_400, KMIME_APP_JSON, NULL, NULL); break; } }
--- a/scid/page-index.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page-index.c Tue Aug 02 13:24:13 2022 +0200 @@ -16,79 +16,112 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ -#include <sys/types.h> -#include <assert.h> -#include <stdarg.h> -#include <stdint.h> +#include <errno.h> +#include <string.h> -#include <kcgi.h> - -#include "database.h" -#include "fragment-paste.h" -#include "page-index.h" +#include "log.h" +#include "config.h" +#include "db.h" #include "page.h" -#include "paste.h" +#include "types.h" #include "util.h" -struct template { - struct kreq *req; - const struct paste *pastes; - size_t pastesz; -}; +/* + * Document we create for templatize. + * + * { + * "projects: [ + * { + * "name": "project name", + * "description": "project short description", + * "url": "project URL or homepage", + * "jobs": [ + * { + * "job": job-id, + * "tag": "job tag / revision", + * "success": true, // on success (absent otherwise) + * "failed: true // on failure (absent otherwise) + * } + * ] + * } + * ] + * } + */ -static const char *keywords[] = { - "pastes" -}; +static json_t * +make_job(const struct job *job) +{ + struct jobresult res[SCI_WORKER_MAX]; + ssize_t resz; + json_t *doc = NULL; -static int -template(size_t keyword, void *arg) -{ - struct template *tp = arg; + doc = json_pack("{sI ss}", + "id", (json_int_t)job->id, + "tag", job->tag + ); + + /* Find every job result associated to see if there are failures. */ + resz = db_jobresult_list_by_job_group(res, UTIL_SIZE(res), job->id); + + for (ssize_t i = 0; i < resz; ++i) + if (res[i].exitcode) + json_object_set_new(doc, "failed", json_true()); + + if (!json_object_get(doc, "failed")) + json_object_set_new(doc, "success", json_true()); - switch (keyword) { - case 0: - for (size_t i = 0; i < tp->pastesz; ++i) - fragment_paste(tp->req, &tp->pastes[i]); - break; - default: - break; + return doc; +} + +static json_t * +make_jobs(const char *project) +{ + struct job jobs[10]; + ssize_t jobsz; + json_t *array = NULL, *obj; + + if ((jobsz = db_job_list(jobs, UTIL_SIZE(jobs), project)) >= 0) { + if (!(array = json_array())) + return NULL; + for (ssize_t i = 0; i < jobsz; ++i) + if ((obj = make_job(&jobs[i]))) + json_array_append(array, obj); } - return 1; + return array; +} + +static json_t * +make_project(const struct project *project) +{ + return json_pack("{ss ss ss so*}", + "name", project->name, + "description", project->desc, + "url", project->url, + "jobs", make_jobs(project->name) + ); } static void get(struct kreq *r) { - struct paste pastes[10] = {0}; - size_t pastesz = NELEM(pastes); + (void)r; + struct project projects[SCI_PROJECT_MAX] = {0}; + ssize_t projectsz = 0; + json_t *array; - if (!database_recents(pastes, &pastesz)) - page(r, NULL, KHTTP_500, "pages/500.html"); - else - page_index_render(r, pastes, pastesz); - - for (size_t i = 0; i < pastesz; ++i) - paste_finish(&pastes[i]); -} + /* 'projects' array. */ + if (!(array = json_array())) + log_die("page-index: %s", strerror(ENOMEM)); -void -page_index_render(struct kreq *r, const struct paste *pastes, size_t pastesz) -{ - struct template data = { - .req = r, - .pastes = pastes, - .pastesz = pastesz - }; + projectsz = db_project_list(projects, UTIL_SIZE(projects)); - struct ktemplate kt = { - .key = keywords, - .keysz = NELEM(keywords), - .arg = &data, - .cb = template - }; + for (ssize_t i = 0; i < projectsz; ++i) + json_array_append(array, make_project(&projects[i])); - page(r, &kt, KHTTP_200, "pages/index.html"); + page(r, KHTTP_200, KMIME_TEXT_HTML, "pages/index.html", json_pack("{so}", + "projects", array + )); } void @@ -99,7 +132,7 @@ get(r); break; default: - page(r, NULL, KHTTP_400, "400.html"); + page(r, KHTTP_400, KMIME_TEXT_HTML, "pages/400.html", NULL); break; } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scid/page-index.h Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,27 @@ +/* + * page-index.h -- / route + * + * Copyright (c) 2021 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef SCI_PAGE_INDEX_H +#define SCI_PAGE_INDEX_H + +struct kreq; + +void +page_index(struct kreq *); + +#endif /* !SCI_PAGE_INDEX_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scid/page-static.c Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,65 @@ +/* + * page-static.c -- page /static + * + * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/types.h> +#include <sys/stat.h> +#include <assert.h> +#include <limits.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdio.h> + +#include "config.h" +#include "page.h" +#include "scid.h" + +static void +get(struct kreq *req) +{ + struct stat st; + char path[PATH_MAX]; + + snprintf(path, sizeof (path), "%s%s", scid.themedir, req->fullpath); + + if (stat(path, &st) < 0) + page(req, KHTTP_404, KMIME_TEXT_HTML, "404.html", NULL); + else { + khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); + khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[req->mime]); + khttp_head(req, kresps[KRESP_CONTENT_LENGTH], + "%llu", (unsigned long long)(st.st_size)); + khttp_body(req); + khttp_template(req, NULL, path); + khttp_free(req); + } +} + +void +page_static(struct kreq *r) +{ + assert(r); + + switch (r->method) { + case KMETHOD_GET: + get(r); + break; + default: + page(r, KHTTP_400, KMIME_TEXT_HTML, "400.html", NULL); + break; + } +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scid/page-static.h Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,27 @@ +/* + * page-static.h -- page /static + * + * Copyright (c) 2020-2022 David Demelier <markand@malikania.fr> + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef IMGUP_PAGE_STATIC_H +#define IMGUP_PAGE_STATIC_H + +struct kreq; + +void +page_static(struct kreq *); + +#endif /* !IMGUP_PAGE_STATIC_H */
--- a/scid/page.c Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page.c Tue Aug 02 13:24:13 2022 +0200 @@ -16,24 +16,304 @@ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include <sys/stat.h> +#include <assert.h> +#include <errno.h> +#include <fcntl.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include <mustache.h> + +#include "log.h" #include "page.h" +#include "scid.h" #include "util.h" +static void +parse_error(int code, const char *msg, unsigned int line, unsigned int col, void *data) +{ + (void)code; + + const char *path = data; + + log_warn("page: %s:%u:%u:%s", path, line, col, msg); +} + +static int +out_verbatim(const char *output, size_t size, void *data) +{ + FILE *fp = data; + + if (fwrite(output, 1, size, fp) != size) + return -1; + + return 0; +} + +static int +out_escaped(const char *output, size_t size, void *data) +{ + FILE *fp = data; + + for (size_t i = 0; i < size; ++i) { + switch (output[i]) { + case '"': + if (out_verbatim(""", 6, fp) < 0) + return -1; + break; + case '&': + if (out_verbatim("&", 5, fp) < 0) + return -1; + break; + case '<': + if (out_verbatim("<", 4, fp) < 0) + return -1; + break; + case '>': + if (out_verbatim(">", 4, fp) < 0) + return -1; + break; + default: + if (out_verbatim(&output[i], 1, fp) <0) + return -1; + break; + } + } + + return 0; +} + +static inline int +out_int(FILE *fp, intmax_t val) +{ + char buf[64] = {0}; + + snprintf(buf, sizeof (buf), "%jd", val); + + return out_verbatim(buf, strlen(buf), fp); +} + +static inline int +out_double(FILE *fp, double val) +{ + char buf[64] = {0}; + + snprintf(buf, sizeof (buf), "%f", val); + + return out_verbatim(buf, strlen(buf), fp); +} + +static int +dump(void *node, int (*outputter)(const char *, size_t, void *), void *rdata, void *pdata) +{ + (void)pdata; + + FILE *fp = rdata; + const json_t *value = node; + + switch (json_typeof(value)) { + case JSON_OBJECT: + case JSON_ARRAY: + /* This indicates a document construction error. */ + // TODO: change to error log. + abort(); + break; + case JSON_STRING: + return outputter(json_string_value(value), json_string_length(value), fp); + case JSON_INTEGER: + return out_int(fp, json_integer_value(value)); + case JSON_REAL: + return out_double(fp, json_real_value(value)); + case JSON_TRUE: + return out_verbatim("true", 4, fp); + case JSON_FALSE: + return out_verbatim("false", 5, fp); + default: + break; + } + + return 0; +} + +static void * +get_root(void *pdata) +{ + return pdata; +} + +static void * +get_child_by_name(void *node, const char *name, size_t size, void *pdata) +{ + (void)pdata; + + json_t *value = node; + +#if 1 + printf("-> Seeking name '%.*s'\n", (int)size, name); + printf("-> Type of node: %d (%p)\n", json_typeof(value), value); +#endif + + if (json_is_object(value)) + return json_object_getn(node, name, size); + + return NULL; +} + +static void * +get_child_by_index(void *node, unsigned int index, void *pdata) +{ + (void)pdata; + + json_t *value = node; + +#if 1 + printf("-> Seeking index '%u'\n", index); + printf("-> Type of node: %d (%p)\n", json_typeof(value), value); +#endif + + if (json_is_array(value)) + return json_array_get(node, index); + + return NULL; +} + +static MUSTACHE_TEMPLATE * +get_partial(const char *name, size_t size, void *pdata) +{ + (void)name; + (void)size; + (void)pdata; + + return NULL; +} + +static char * +readall(const char *file) +{ + int fd; + char *ret = NULL; + struct stat st; + + if ((fd = open(file, O_RDONLY)) < 0) + return NULL; + if (fstat(fd, &st) < 0) + goto exit; + if (!(ret = calloc(1, st.st_size + 1))) + goto exit; + if (read(fd, ret, st.st_size) != st.st_size) { + free(ret); + ret = NULL; + } + +exit: + close(fd); + + return ret; +} + +static int +process(const char *path, const char *input, FILE *fp, json_t *doc) +{ + MUSTACHE_PARSER parser = { + .parse_error = parse_error + }; + MUSTACHE_RENDERER rdr = { + .out_verbatim = out_verbatim, + .out_escaped = out_escaped + }; + MUSTACHE_DATAPROVIDER pv = { + .dump = dump, + .get_root = get_root, + .get_child_by_name = get_child_by_name, + .get_child_by_index = get_child_by_index, + .get_partial = get_partial + }; + MUSTACHE_TEMPLATE *tmpl; + int status; + + if (!(tmpl = mustache_compile(input, strlen(input), &parser, (void *)path, 0))) + return -1; + + status = mustache_process(tmpl, &rdr, fp, &pv, doc); + mustache_release(tmpl); + + return status ? -1 : 0; +} + +char * +templatize(const char *path, json_t *doc) +{ + assert(path); + + char *in = NULL, *out = NULL; + size_t outsz = 0; + FILE *fp; + + if (!(fp = open_memstream(&out, &outsz))) + goto exit; + if (!(in = readall(path))) + goto exit; + + /* Process template but don't return a partially written file. */ + if (process(path, in, fp, doc) < 0) { + fclose(fp); + free(out); + fp = NULL; + out = NULL; + errno = EINVAL; + } + +exit: + if (!out) + log_warn("page: %s: %s", path, strerror(errno)); + if (fp) + fclose(fp); + + free(in); + json_decref(doc); + + return out; +} + +static void +render(struct kreq *req, const char *path, json_t *doc) +{ + char *content; + + if (!(content = templatize(path, doc))) + log_warn("page: unable to templatize: %s", strerror(errno)); + if (doc) + json_decref(doc); + + khttp_printf(req, "%s", content); + + free(content); +} + void page(struct kreq *req, - const struct ktemplate *tmpl, enum khttp status, enum kmime mime, - const char *file) + const char *path, + json_t *doc) { + assert(req); + assert(!path || (path && doc)); + khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]); khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[status]); khttp_body(req); - if (file) { - khttp_template(req, NULL, util_path("fragments/header.html")); - khttp_template(req, tmpl, util_path(file)); - khttp_template(req, NULL, util_path("fragments/footer.html")); + if (path) { + printf("document is: [%s]\n", json_dumps(doc, JSON_INDENT(2))); + /* TODO: add title in the header. */ + render(req, scid_theme_path("fragments/header.html"), NULL); + render(req, scid_theme_path(path), doc); + render(req, scid_theme_path("fragments/footer.html"), NULL); } khttp_free(req);
--- a/scid/page.h Mon Jul 25 21:22:13 2022 +0200 +++ b/scid/page.h Tue Aug 02 13:24:13 2022 +0200 @@ -22,9 +22,12 @@ #include <sys/types.h> #include <stdarg.h> #include <stdint.h> + #include <kcgi.h> +#include <jansson.h> + void -page(struct kreq *, const struct ktemplate *, enum khttp, enum kmime, const char *); +page(struct kreq *, enum khttp, enum kmime, const char *, json_t *); #endif /* !SCI_PAGE_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scid/scid.c Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,40 @@ +#include <assert.h> +#include <stdio.h> + +#include "db.h" +#include "log.h" +#include "scid.h" + +struct scid scid = { + .dbpath = VARDIR "/db/sci/sci.db" +}; + +void +scid_init(void) +{ + log_open("scid"); + log_info("opening database %s", scid.dbpath); + + if (db_open(scid.dbpath) < 0) + log_die("abort: unable to open database"); +} + +const char * +scid_theme_path(const char *filename) +{ + assert(filename); + + /* Build path to the template file. */ + static _Thread_local char path[PATH_MAX]; + + snprintf(path, sizeof (path), "%s/%s", scid.themedir, filename); + + return path; +} + +void +scid_finish(void) +{ + db_finish(); + log_finish(); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/scid/scid.h Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,20 @@ +#ifndef SCID_H +#define SCID_H + +#include <limits.h> + +extern struct scid { + char themedir[PATH_MAX]; + char dbpath[PATH_MAX]; +} scid; + +void +scid_init(void); + +const char * +scid_theme_path(const char *); + +void +scid_finish(void); + +#endif /* !SCID_H */
--- a/sql/init.sql Mon Jul 25 21:22:13 2022 +0200 +++ b/sql/init.sql Tue Aug 02 13:24:13 2022 +0200 @@ -37,7 +37,7 @@ ); CREATE TABLE IF NOT EXISTS jobresult( - `job_id` INTEGER NOT NULL REFERENCES job (id), + `job_id` INTEGER NOT NULL REFERENCES job (rowid), `worker_name` INTEGER NOT NULL REFERENCES worker (name), `exitcode` INTEGER DEFAULT 0, `console` TEXT DEFAULT NULL,
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/job-list.sql Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,6 @@ + SELECT rowid + , * + FROM `job` + WHERE `project_name` = ? +ORDER BY `date` ASC + LIMIT ?
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/jobresult-list-by-job-group.sql Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,7 @@ + SELECT rowid + , * + , MAX(`date`) + FROM `jobresult` + WHERE `job_id` = ? +GROUP BY `worker_name` + LIMIT ?
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/jobresult-list-by-job.sql Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,6 @@ + SELECT rowid + , * + FROM `jobresult` + WHERE `job_id` = ? + ORDER BY `job_id` ASC + LIMIT ?
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sql/jobresult-list-by-worker.sql Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,5 @@ + SELECT * + FROM `jobresult` + WHERE `worker_name` = ? + ORDER BY `job_id` ASC + LIMIT ?
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/themes/bulma/fragments/footer.html Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,3 @@ + </div> + </section> +</body>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/themes/bulma/fragments/header.html Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,12 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <title>sci</title> + <link rel="stylesheet" href="/static/bulma.min.css" /> + </head> + + <body> + <section class="section"> + <div class="container">
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/themes/bulma/pages/index.html Tue Aug 02 13:24:13 2022 +0200 @@ -0,0 +1,33 @@ +<div class="columns" style="flex-wrap: wrap"> +{{#projects}} +<div class="card"> + <div class="card-content"> + <div class="media"> + <div class="media-content"> + <p class="title is-4"><a href="/projects/{{name}}">{{name}}</a></p> + <p class="subtitle is-6">{{description}}</p> + </div> + </div> + + <div class="content"> + {{#jobs}} + <table> + {{/jobs}} + + {{#jobs}} + <tr> + <td><a href="jobs/{{job}}">job {{id}} ({{tag}})</td> + </tr> + {{/jobs}} + + {{#jobs}} + </table> + <br/> + <p class="is-size-7">{{count_ok}} jobs over {{count_failed}} have failed</p> + {{/jobs}} + + {{^jobs}} + <p>No jobs yet.</p> + {{/jobs}} + </div> +{{/projects}}