diff options
author | sternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org> | 2020-09-26 16:38:05 +0200 |
---|---|---|
committer | sternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org> | 2020-09-26 16:46:24 +0200 |
commit | e949a8baea7cc2a35885f1016276d30f571ff8b6 (patch) | |
tree | d41ed74fd6c5ef7721ccc7fc52ff1bca1576301f | |
parent | baecfd220afc0dd0dca31101637e29d418e699f5 (diff) |
refactor(warteraum): replace json_output with own emitjson
Still somewhat limited, but implements everything we need in warteraum. API is very similar to json_output, but we choose to not implement buffer handling and use open_memstream instead. The FILE * abstraction suits us very well here since we just want to write chars to some kind of output.
-rw-r--r-- | README.adoc | 1 | ||||
-rw-r--r-- | default.nix | 3 | ||||
-rw-r--r-- | warteraum/build_config | 1 | ||||
-rw-r--r-- | warteraum/default.o.do | 3 | ||||
-rw-r--r-- | warteraum/emitjson.c | 169 | ||||
-rw-r--r-- | warteraum/emitjson.h | 36 | ||||
-rw-r--r-- | warteraum/main.c | 119 | ||||
-rw-r--r-- | warteraum/test/all.do | 2 | ||||
-rw-r--r-- | warteraum/test/default.exe.do | 3 | ||||
-rwxr-xr-x | warteraum/test/run | 15 | ||||
-rw-r--r-- | warteraum/test/test_emitjson.c | 54 | ||||
-rw-r--r-- | warteraum/warteraum.do | 2 |
12 files changed, 359 insertions, 49 deletions
diff --git a/README.adoc b/README.adoc index 933b274..b791064 100644 --- a/README.adoc +++ b/README.adoc @@ -196,7 +196,6 @@ Help is welcome! Some things that remain to be done: * Important: Documentation. Annoying sterni into doing it is also helping. * Make `warteraum` accept `application/json` request bodies for the `v2` API using http://www.catb.org/~esr/microjson/[microjson] (?). -* Rewrite `json_output.c` to avoid licensing issue * Polish the web frontend, test across browsers * Refresh queue regularly in the web frontend * Write more tests diff --git a/default.nix b/default.nix index 9236b3f..bee5fb2 100644 --- a/default.nix +++ b/default.nix @@ -35,7 +35,7 @@ let builtins.filter nonempty (builtins.genList stringSplitter ((builtins.stringLength s / n) + 1)); - warteraumDrv = { stdenv, redo, scrypt, scryptSalt ? null, apiTokens ? null }: + warteraumDrv = { stdenv, redo, scrypt, jq, scryptSalt ? null, apiTokens ? null }: let saltBytes = stringSegments 2 scryptSalt; saltArray = @@ -79,6 +79,7 @@ let doCheck = true; checkPhase = "./test/run"; + checkInputs = [ jq ]; installPhase = '' install -Dm755 warteraum -t $out/bin diff --git a/warteraum/build_config b/warteraum/build_config index fbb6223..3881462 100644 --- a/warteraum/build_config +++ b/warteraum/build_config @@ -9,7 +9,6 @@ CFLAGS="$CFLAGS -std=gnu99" # disable some warnings not caused by us CFLAGS="$CFLAGS -Wno-unused-command-line-argument" # nix musl-clang CFLAGS="$CFLAGS -Wno-gnu-empty-initializer" # httpserver.h -CFLAGS="$CFLAGS -Wno-unused-function" # json_output # fix build issue with glibc 2.31 on NixOS # https://github.com/jeremycw/httpserver.h/pull/43 diff --git a/warteraum/default.o.do b/warteraum/default.o.do index 9a75d37..f91bd8f 100644 --- a/warteraum/default.o.do +++ b/warteraum/default.o.do @@ -10,9 +10,8 @@ fi case "$2" in main) - redo-ifchange queue.h routing.h form.h v1_static.h + redo-ifchange queue.h routing.h form.h v1_static.h emitjson.h redo-ifchange scrypt.h tokens.h http_string.h - redo-ifchange ../third_party/json_output/json_output.h redo-ifchange ../third_party/httpserver.h/httpserver.h ;; form) diff --git a/warteraum/emitjson.c b/warteraum/emitjson.c new file mode 100644 index 0000000..f5ef6eb --- /dev/null +++ b/warteraum/emitjson.c @@ -0,0 +1,169 @@ +#include <stdio.h> +#include "emitjson.h" + +#define BUF_SIZE 512 + +void ej_putc(struct ej_context *ctx, char c) { + fputc(c, ctx->out); + ctx->written++; +} + +void ej_init(struct ej_context *ctx, FILE *out) { + ctx->out = out; + ctx->need_comma = false; + ctx->written = 0; +} + +void ej_object(struct ej_context *ctx) { + if(ctx->need_comma) { + ej_putc(ctx, ','); + } + ej_putc(ctx, '{'); + ctx->need_comma = false; +} + +void ej_object_end(struct ej_context *ctx) { + ej_putc(ctx, '}'); + ctx->need_comma = true; +} + +void ej_bind(struct ej_context *ctx, const char *b, size_t len) { + ej_string(ctx, b, len); + ctx->need_comma = false; + + ej_putc(ctx, ':'); +} + +void ej_array(struct ej_context *ctx) { + if(ctx->need_comma) { + ej_putc(ctx, ','); + } + ej_putc(ctx, '['); + ctx->need_comma = false; +} + +void ej_array_end(struct ej_context *ctx) { + ej_putc(ctx, ']'); + ctx->need_comma = true; +} + +void ej_string(struct ej_context *ctx, const char *s, size_t len) { + if(ctx->need_comma) { + ej_putc(ctx, ','); + } + + ctx->need_comma = true; + + ej_putc(ctx, '"'); + + for(size_t i = 0; i < len; i++) { + char c; + bool escape = false; + + switch(s[i]) { + case '\t': + escape = true; + c = 't'; + break; + case '\f': + escape = true; + c = 'f'; + break; + case '\r': + escape = true; + c = 'r'; + break; + case '\n': + escape = true; + c = 'n'; + break; + case '\\': + case '"': + escape = true; + default: + c = s[i]; + break; + } + + if(escape) { + ej_putc(ctx, '\\'); + } + + ej_putc(ctx, c); + } + + ej_putc(ctx, '"'); +} + +void ej_null(struct ej_context *ctx) { + if(ctx->need_comma) { + ej_putc(ctx, ','); + } + + ctx->need_comma = true; + + size_t wsize = fwrite("null", 1, sizeof("null") - 1, ctx->out); + ctx->written += wsize; +} + +void ej_bool(struct ej_context *ctx, bool b) { + if(ctx->need_comma) { + ej_putc(ctx, ','); + } + + ctx->need_comma = true; + + size_t len; + char *s; + + if(b) { + s = "true"; + len = sizeof("true") - 1; + } else { + s = "false"; + len = sizeof("false") - 1; + } + + size_t wsize = fwrite(s, 1, len, ctx->out); + ctx->written += wsize; +} + +// generics for C 🤡 +#define EJ_INT_FUN(name, type, sign) \ + void name(struct ej_context *ctx, type u) { \ + if(ctx->need_comma) { \ + ej_putc(ctx, ','); \ + } \ + \ + ctx->need_comma = true; \ + \ + char buf[BUF_SIZE]; \ + type d; \ + size_t len = 0; \ + char *start = buf + BUF_SIZE; \ + bool add_sign = false; \ + \ + if(sign && u < 0) { \ + add_sign = true; \ + u = -u; \ + } \ + \ + do { \ + start--; \ + len++; \ + d = u % 10; \ + u = u / 10; \ + *start = d + 48; \ + } while(u > 0 && len <= BUF_SIZE); \ + \ + if(add_sign) { \ + ej_putc(ctx, '-'); \ + } \ + \ + size_t wsize = fwrite(start, sizeof(char), len, ctx->out); \ + ctx->written += wsize; \ + } + +EJ_INT_FUN(ej_uint, unsigned int, false) + +EJ_INT_FUN(ej_int, int, true) diff --git a/warteraum/emitjson.h b/warteraum/emitjson.h new file mode 100644 index 0000000..0e6f673 --- /dev/null +++ b/warteraum/emitjson.h @@ -0,0 +1,36 @@ +#ifndef WARTERAUM_EMITJSON_H +#define WARTERAUM_EMITJSON_H + +#include <stdbool.h> + +struct ej_context { + FILE *out; + bool need_comma; + size_t written; +}; + +void ej_init(struct ej_context *, FILE *); + +void ej_object(struct ej_context *); +void ej_object_end(struct ej_context *); + +#define EJ_STATIC_BIND(ctx, b) \ + ej_bind(ctx, b, sizeof(b) - 1) + +void ej_bind(struct ej_context *, const char *, size_t); +//void ej_bind0(struct ej_context *, const char *, size_t); + +void ej_array(struct ej_context *); +void ej_array_end(struct ej_context *); + +void ej_string(struct ej_context *, const char *, size_t); +//void ej_string0(struct ej_context *, const char *); + +void ej_null(struct ej_context *); + +void ej_bool(struct ej_context *, bool); + +void ej_int(struct ej_context *, int); +void ej_uint(struct ej_context *, unsigned int); + +#endif diff --git a/warteraum/main.c b/warteraum/main.c index d8dbd7b..fa06871 100644 --- a/warteraum/main.c +++ b/warteraum/main.c @@ -6,7 +6,7 @@ #include <stdio.h> #include <string.h> -#include "../third_party/json_output/json_output.h" +#include "emitjson.h" #include "http_string.h" @@ -20,9 +20,6 @@ #define LISTEN_PORT 9000 -#define JSO_STATIC_PROP(s, str) \ - jso_prop_len(s, str, sizeof(str) - 1) - // compare http_string against a static string, // but optionally allow an ;… after it. // i.e. application/json;charset=utf8 matches with @@ -32,6 +29,9 @@ strncmp(a.buf, s, sizeof(s) - 1) == 0 && \ ((size_t) a.len < sizeof(s) || a.buf[sizeof(s) - 1] == ';')) +#define INTERNAL_ERROR_STATIC \ + "{\"error\":\"internal error while building error response\"}" + // Global state static struct queue flip_queue; @@ -106,21 +106,37 @@ void response_error(enum warteraum_result e, bool legacy_response, http_request_ http_response_body(response, QUEUE_ADD_V1_FAILURE, sizeof(QUEUE_ADD_V1_FAILURE) - 1); http_respond(request, response); } else { - jso_stream s; - - jso_init_growable(&s); - - jso_object(&s); - JSO_STATIC_PROP(&s, "error"); - jso_string_len(&s, errors[e].buf, (size_t) errors[e].len); - jso_end_object(&s); + size_t buf_size = 0; + char *buf = NULL; + FILE *out = open_memstream(&buf, &buf_size); + struct ej_context ctx; + bool static_buf = false; + + ej_init(&ctx, out); + + if(out == NULL) { + buf = INTERNAL_ERROR_STATIC; + buf_size = sizeof(INTERNAL_ERROR_STATIC) - 1; + + static_buf = true; + e = WARTERAUM_INTERNAL_ERROR; + } else { + ej_object(&ctx); + EJ_STATIC_BIND(&ctx, "error"); + ej_string(&ctx, errors[e].buf, (size_t) errors[e].len); + ej_object_end(&ctx); + + fclose(out); + } http_response_status(response, codes[e]); http_response_header(response, "Content-Type", "application/json"); - http_response_body(response, s.data, (int) s.pos); + http_response_body(response, buf, static_buf ? buf_size : (int) ctx.written); http_respond(request, response); - jso_close(&s); + if(!static_buf) { + free(buf); + } } } @@ -129,38 +145,49 @@ enum warteraum_result response_queue(enum warteraum_version v, http_request_t *r (void) v; // surpress warning for now unsigned int queue_length = 0; - jso_stream s; - jso_init_growable(&s); - jso_object(&s); - JSO_STATIC_PROP(&s, "queue"); - jso_array(&s); + struct ej_context ctx; + size_t buf_size = 0; + char *buf = NULL; + FILE *out = open_memstream(&buf, &buf_size); + + if(out == NULL) { + return WARTERAUM_INTERNAL_ERROR; + } + + ej_init(&ctx, out); + + ej_object(&ctx); + EJ_STATIC_BIND(&ctx, "queue"); + ej_array(&ctx); queue_foreach(flip_queue, elem) { queue_length++; - jso_object(&s); + ej_object(&ctx); - JSO_STATIC_PROP(&s, "id"); - jso_uint(&s, elem->id); + EJ_STATIC_BIND(&ctx, "id"); + ej_uint(&ctx, elem->id); - JSO_STATIC_PROP(&s, "text"); - jso_string_len(&s, elem->string, elem->string_size); + EJ_STATIC_BIND(&ctx, "text"); + ej_string(&ctx, elem->string, elem->string_size); - jso_end_object(&s); + ej_object_end(&ctx); } - jso_end_array(&s); + ej_array_end(&ctx); + + EJ_STATIC_BIND(&ctx, "length"); + ej_uint(&ctx, queue_length); + ej_object_end(&ctx); - JSO_STATIC_PROP(&s, "length"); - jso_uint(&s, queue_length); - jso_end_object(&s); + fclose(out); http_response_status(response, 200); http_response_header(response, "Content-Type", "application/json"); - http_response_body(response, s.data, (int) s.pos); + http_response_body(response, buf, (int) ctx.written); http_respond(request, response); - jso_close(&s); + free(buf); return WARTERAUM_OK; } @@ -219,22 +246,32 @@ enum warteraum_result response_queue_add(enum warteraum_version version, http_re http_response_body(response, QUEUE_ADD_V1_SUCCESS, sizeof(QUEUE_ADD_V1_SUCCESS) - 1); http_respond(request, response); } else { - jso_stream s; - jso_init_growable(&s); + struct ej_context ctx; + char *buf = NULL; + size_t buf_size = 0; + FILE *out = open_memstream(&buf, &buf_size); + + if(out == NULL) { + return WARTERAUM_INTERNAL_ERROR; + } + + ej_init(&ctx, out); + + ej_object(&ctx); + EJ_STATIC_BIND(&ctx, "id"); + ej_uint(&ctx, flip_queue.last->id); + EJ_STATIC_BIND(&ctx, "text"); + ej_string(&ctx, flip_queue.last->string, flip_queue.last->string_size); + ej_object_end(&ctx); - jso_object(&s); - JSO_STATIC_PROP(&s, "id"); - jso_uint(&s, flip_queue.last->id); - JSO_STATIC_PROP(&s, "text"); - jso_string_len(&s, flip_queue.last->string, flip_queue.last->string_size); - jso_end_object(&s); + fclose(out); http_response_status(response, 200); http_response_header(response, "Content-Type", "application/json"); - http_response_body(response, s.data, (int) s.pos); + http_response_body(response, buf, (int) ctx.written); http_respond(request, response); - jso_close(&s); + free(buf); } return WARTERAUM_OK; diff --git a/warteraum/test/all.do b/warteraum/test/all.do index 6bd544d..9fac8ba 100644 --- a/warteraum/test/all.do +++ b/warteraum/test/all.do @@ -1 +1 @@ -redo-ifchange test_queue.exe test_form.exe test_routing.exe +redo-ifchange test_queue.exe test_form.exe test_routing.exe test_emitjson.exe diff --git a/warteraum/test/default.exe.do b/warteraum/test/default.exe.do index a1e90d3..8e32f6f 100644 --- a/warteraum/test/default.exe.do +++ b/warteraum/test/default.exe.do @@ -15,6 +15,9 @@ case "$2" in OBJS="../routing.o" redo-ifchange ../http_string.h ;; + test_emitjson) + OBJS="../emitjson.o" + ;; esac redo-ifchange $OBJS diff --git a/warteraum/test/run b/warteraum/test/run index e78a487..ef85225 100755 --- a/warteraum/test/run +++ b/warteraum/test/run @@ -5,10 +5,23 @@ cd "$(dirname "$0")" redo all -TESTS="queue form routing" +TESTS="queue form routing emitjson" for t in $TESTS; do echo -e "\n# $t tests\n" "./test_$t.exe" done + +echo -e "\n# test emitjson validity\n" + +if command -v jq > /dev/null; then + if ./test_emitjson.exe -o | jq; then + echo -e "\njson validity\tok" + else + echo -e "\njson validity\tFAIL" + exit 1 + fi +else + echo -e "json validity\tskipped (missing jq)" +fi diff --git a/warteraum/test/test_emitjson.c b/warteraum/test/test_emitjson.c new file mode 100644 index 0000000..1f51adc --- /dev/null +++ b/warteraum/test/test_emitjson.c @@ -0,0 +1,54 @@ +#include <stdio.h> +#include <string.h> +#include "../emitjson.h" +#include "test.h" + +int main(int argc, char **argv) { + bool output_json = false; + + if(argc == 2 && strcmp(argv[1], "-o") == 0) { + output_json = true; + } + + struct ej_context ctx; + size_t buf_size; + char *buf = NULL; + FILE *out = open_memstream(&buf, &buf_size); + + ej_init(&ctx, out); + + ej_object(&ctx); + EJ_STATIC_BIND(&ctx, "array"); + ej_array(&ctx); + ej_uint(&ctx, 25500001); + ej_int(&ctx, -12000); + ej_int(&ctx, 10000); + ej_string(&ctx, "foo\tbar\nbaz\\", 12); + ej_null(&ctx); + ej_object(&ctx); + ej_bind(&ctx, "hello", 5); + ej_string(&ctx, "world", 5); + ej_bind(&ctx, "foo\\", 4); + ej_uint(&ctx, 42); + ej_object_end(&ctx); + ej_bool(&ctx, true); + ej_bool(&ctx, false); + ej_array_end(&ctx); + EJ_STATIC_BIND(&ctx, "number"); + ej_uint(&ctx, 1312); + ej_object_end(&ctx); + + fclose(out); + + bool len_correct = buf_size == ctx.written; + + if(output_json) { + fwrite(buf, 1, ctx.written, stdout); + } else { + test_case("Actually written size matches computed size", len_correct); + } + + free(buf); + + return !test_result; +} diff --git a/warteraum/warteraum.do b/warteraum/warteraum.do index 467872a..1e6bda2 100644 --- a/warteraum/warteraum.do +++ b/warteraum/warteraum.do @@ -1,6 +1,6 @@ source ./build_config redo-ifchange ./build_config -OBJS="../third_party/json_output/json_output.o queue.o routing.o form.o main.o" +OBJS="emitjson.o queue.o routing.o form.o main.o" DEPS="$OBJS ../third_party/httpserver.h/httpserver.h" redo-ifchange $DEPS |