summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore13
-rw-r--r--Makefile145
-rw-r--r--deps.mk37
-rwxr-xr-xmkdeps.sh15
-rw-r--r--src/api.js6
-rw-r--r--src/catalog.c501
-rw-r--r--src/catalog.h33
-rw-r--r--src/config.h.in12
-rw-r--r--src/db.js45
-rw-r--r--src/i18n.c68
-rw-r--r--src/i18n.h55
-rw-r--r--src/ircd.js4
-rw-r--r--src/logerr.c301
-rw-r--r--src/logerr.h23
-rw-r--r--src/napi-sqlite.c103
-rw-r--r--src/papo.en.msg70
-rw-r--r--src/sql/config.sql34
-rwxr-xr-xtests/assert-deps.sh45
-rwxr-xr-xtests/c-lint.sh65
-rwxr-xr-xtests/cli-opts.sh4
-rw-r--r--tests/slurp.c69
-rw-r--r--tests/slurp.h11
-rw-r--r--tests/tests-lib.c28
-rw-r--r--tests/tests-lib.h15
-rwxr-xr-xtools/cdeps.sh225
25 files changed, 1831 insertions, 96 deletions
diff --git a/.gitignore b/.gitignore
index 4673063..ea353c9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,17 @@
+/*.bin
+/*.so
+/src/config.h
+/src/*.o
+/src/*.lo
+/src/*.to
+/src/*.ta
+/src/*.t
+/src/*.c.txt
+/src/*.cat
/doc/*.[0-9]
/doc/*.3js
/src/index.js
/node_modules/
-/src/napi-sqlite.lo
/src/napi-sqlite.node
+/tests/tests-lib.o
+/tests/slurp.o
diff --git a/Makefile b/Makefile
index e266dcd..ba4d24a 100644
--- a/Makefile
+++ b/Makefile
@@ -15,15 +15,22 @@ SRCDIR = $(PREFIX)/src/$(NAME)
SHAREDIR = $(PREFIX)/share
LOCALEDIR = $(SHAREDIR)/locale
MANDIR = $(SHAREDIR)/man
+CFLAGS.a = $(CFLAGS)
CFLAGS.so = $(CFLAGS) -fPIC
+LDFLAGS.a = $(LDFLAGS)
LDFLAGS.so = $(LDFLAGS) -shared
+LDLIBS.a = $(LDLIBS)
+LDLIBS.so = $(LDLIBS)
+EXT.so = .so
+EXEC = ./
## Where to store the installation. Empty by default.
DESTDIR =
+LDLIBS =
.SUFFIXES:
-.SUFFIXES: .in .c .lo .msg .cat
+.SUFFIXES: .in .c .o .lo .to .ta .t .msg .cat
.in:
sed \
@@ -35,11 +42,23 @@ DESTDIR =
< $< > $@
if [ -x $< ]; then chmod +x $@; fi
+.c.o:
+ $(CC) $(CFLAGS.a) -o $@ -c $<
+
.c.lo:
- $(CC) $(CFLAGS.so) -o $@ -c $<
+ $(CC) $(CFLAGS.so) -o $@ -c $<
+
+.c.to:
+ $(CC) $(CFLAGS.a) -DTEST -o $@ -c $<
+
+.ta.t:
+ $(CC) $(LDFLAGS.a) -o $@ $< $(LDLIBS.a)
+all:
+include deps.mk
+
manpages.en.in = \
doc/$(NAME).README.en.7.in \
doc/$(NAME).CHANGELOG.en.7.in \
@@ -52,25 +71,38 @@ manpages.en.in = \
manpages.in = $(manpages.en.in)
manpages = $(manpages.in:.in=)
-sources.js = \
- src/api.js \
- src/cli.js \
- src/ircd.js \
- src/utils.js \
- src/web.js \
-
-tests.js = \
- tests/js/ircd.js \
- tests/js/utils.js \
- tests/js/web.js \
+catalogs.en.msg = src/$(NAME).en.msg
+catalogs.msg = $(catalogs.en.msg)
+catalogs.cat = $(catalogs.msg:.msg=.cat)
+sources.o = $(sources.c:.c=.o)
+sources.lo = $(sources.c:.c=.lo)
+sources.to = $(sources.c:.c=.to)
+sources.ta = $(sources.c:.c=.ta)
+sources.t = $(sources.c:.c=.t)
sources = \
- $(sources.js) \
+ $(sources.c) \
+ $(sources.c:.c=.h) \
+ src/config.h.in \
+ src/config.h \
+ $(catalogs.msg) \
+ $(sources.js) \
derived-assets = \
+ $(NAME).bin \
+ lib$(NAME)$(EXT.so) \
+ src/config.h \
$(manpages) \
+ $(catalogs.cat) \
+ $(sources.o) \
+ $(sources.lo) \
+ $(sources.to) \
+ $(sources.ta) \
+ $(sources.t) \
+ tests/tests-lib.o \
+ tests/slurp.o \
src/index.js \
node_modules/dir.sentinel \
node_modules/ \
@@ -79,8 +111,10 @@ derived-assets = \
src/napi-sqlite.node \
side-assets = \
- ircd.sock \
- web.sock \
+ src/logerr.c.txt \
+ src/catalog.c.txt \
+ ircd.sock \
+ web.sock \
@@ -89,10 +123,14 @@ side-assets = \
all: $(derived-assets)
-$(manpages) src/napi-sqlite.lo: Makefile
+$(NAME).bin:
+ ln -fs src/cli $@
-src/napi-sqlite.node: src/napi-sqlite.lo
- $(CC) $(LDFLAGS.so) -o $@ src/napi-sqlite.lo
+$(manpages) src/config.h: Makefile deps.mk
+$(sources.o) $(sources.lo) $(sources.to): src/config.h Makefile deps.mk
+tests/tests-lib.o: tests/tests-lib.h src/config.h Makefile deps.mk
+$(sources.to): tests/tests-lib.h
+$(sources.ta): tests/tests-lib.o
src/index.js:
ln -fs api.js $@
@@ -108,6 +146,22 @@ node_modules/$(NAME): node_modules/dir.sentinel
node_modules/: node_modules/dir.sentinel node_modules/$(NAME)
+src/napi-sqlite.node: lib$(NAME)$(EXT.so)
+ ln -f lib$(NAME)$(EXT.so) $@
+
+lib$(NAME)$(EXT.so): $(sources.lo) src/napi-sqlite.lo
+ $(CC) $(LDFLAGS.so) -o $@ $(sources.lo) src/napi-sqlite.lo $(LDLIBS.so)
+
+$(sources.ta):
+ $(AR) $(ARFLAGS) $@ $?
+
+src/$(NAME).en.cat: src/i18n.t
+ env DUMP_TRANSLATABLE_STRINGS=1 $(EXEC)src/i18n.t > $*.msg.new
+ cmp -s $*.msg.new $*.msg || mv $*.msg.new $*.msg
+ rm -f $*.msg.new
+ gencat $@ $*.msg
+ touch $@
+
.SUFFIXES: .js .js-t
@@ -115,7 +169,34 @@ tests.js-t = $(tests.js:.js=.js-t)
$(tests.js-t):
node $*.js
-check-t: $(tests.js-t)
+check-js-t: $(tests.js-t)
+
+.SUFFIXES: .t-run
+sources.t-run = $(sources.c:.c=.t-run)
+$(sources.t-run):
+ $(EXEC)$*.t
+
+check-t-run: $(sources.t-run)
+
+check-t: check-js-t check-t-run
+
+
+
+.SUFFIXES: .c-lint
+sources.c-lint = $(sources.c:.c=.c-lint)
+$(sources.c-lint):
+ sh tests/c-lint.sh $*.c
+
+check-lint: $(sources.c-lint)
+
+
+integration-tests = \
+ tests/cli-opts.sh \
+
+$(integration-tests): $(NAME).bin ALWAYS
+ sh $@ $(EXEC)$(NAME).bin
+
+check-integration: $(integration-tests)
tests/assert-clean.sh tests/assert-install.sh tests/assert-uninstall.sh: all
@@ -134,7 +215,7 @@ check-asserts: $(assert-tests)
## Run all tests. Each test suite is isolated, so that a parallel
## build can run tests at the same time. The required artifacts
## are created if missing.
-check: check-t check-asserts
+check: check-t check-lint check-integration check-asserts
## Remove *all* derived artifacts produced during the build.
@@ -149,38 +230,44 @@ install: all
mkdir -p \
'$(DESTDIR)$(BINDIR)' \
'$(DESTDIR)$(JSLIBDIR)'
- cp src/*.js '$(DESTDIR)$(JSLIBDIR)'
+ cp -P src/*.js '$(DESTDIR)$(JSLIBDIR)'
ln -fs '$(DESTDIR)$(JSLIBDIR)'/cli.js '$(DESTDIR)$(BINDIR)'/$(NAME)
for f in $(sources); do \
dir='$(DESTDIR)$(SRCDIR)'/"`dirname "$${f#src/}"`"; \
mkdir -p "$$dir"; \
cp -P "$$f" "$$dir"; \
done
+ for l in en $(TRANSLATIONS); do \
+ dir='$(DESTDIR)$(LOCALEDIR)'/"$$l"/LC_MESSAGES; \
+ mkdir -p "$$dir"; \
+ cp src/$(NAME)."$$l".cat "$$dir"/$(NAME).cat; \
+ done
sh tools/manpages.sh -ip '$(DESTDIR)$(MANDIR)' $(manpages)
## Uninstalls from $(DESTDIR)$(PREFIX). This is a perfect mirror
## of the "install" target, and removes *all* that was installed.
## A dedicated test asserts that this is always true.
uninstall:
- rm -f \
- '$(DESTDIR)$(BINDIR)'/$(NAME)
+ rm -rf \
+ '$(DESTDIR)$(BINDIR)'/$(NAME) \
+ '$(DESTDIR)$(SRCDIR)'
rm -rf '$(DESTDIR)$(JSLIBDIR)'
- for f in $(sources); do \
- rm -f '$(DESTDIR)$(SRCDIR)'/$${f#src/}; \
+ for l in en $(TRANSLATIONS); do \
+ rm -f '$(DESTDIR)$(LOCALEDIR)'/"$$l"/LC_MESSAGES/$(NAME).cat; \
done
sh tools/manpages.sh -up '$(DESTDIR)$(MANDIR)' $(manpages)
-run-ircd:
+run-ircd: all
rm -f ircd.sock
./src/cli.js ircd ircd.sock
-run-web:
+run-web: all
rm -f web.sock
./src/cli.js web web.sock
## Run the web and IRC server locally.
-run: node_modules/$(NAME) src/index.js
+run: all
$(MAKE) run-ircd & $(MAKE) run-web & wait
diff --git a/deps.mk b/deps.mk
new file mode 100644
index 0000000..2fb63c8
--- /dev/null
+++ b/deps.mk
@@ -0,0 +1,37 @@
+sources.c = \
+ src/catalog.c \
+ src/i18n.c \
+ src/logerr.c \
+
+sources.js = \
+ src/api.js \
+ src/db.js \
+ src/ircd.js \
+ src/utils.js \
+ src/web.js \
+
+tests.js = \
+ tests/js/ircd.js \
+ tests/js/utils.js \
+ tests/js/web.js \
+
+src/catalog.o src/catalog.lo src/catalog.to: src/catalog.h
+src/i18n.o src/i18n.lo src/i18n.to: src/i18n.h
+src/logerr.o src/logerr.lo src/logerr.to: src/logerr.h
+
+src/catalog.ta: src/catalog.to
+src/i18n.ta: src/i18n.to
+src/logerr.ta: src/logerr.to
+
+src/catalog.t-run: src/catalog.t
+src/i18n.t-run: src/i18n.t
+src/logerr.t-run: src/logerr.t
+
+
+src/catalog.o src/catalog.lo src/catalog.to: src/logerr.h src/../tests/slurp.h
+src/i18n.o src/i18n.lo src/i18n.to: src/logerr.h src/catalog.h
+src/logerr.o src/logerr.lo src/logerr.to: src/../tests/slurp.h
+
+src/catalog.ta: src/logerr.o src/../tests/slurp.o
+src/i18n.ta: src/logerr.o src/catalog.o
+src/logerr.ta: src/../tests/slurp.o
diff --git a/mkdeps.sh b/mkdeps.sh
new file mode 100755
index 0000000..12d10f0
--- /dev/null
+++ b/mkdeps.sh
@@ -0,0 +1,15 @@
+#!/bin/sh
+set -eu
+
+varlist() {
+ printf '%s = \\\n' "$1"
+ sed 's|^\(.*\)$|\t\1 \\|'
+ printf '\n'
+}
+
+export LANG=POSIX.UTF-8
+find src/*.c -not -name napi-sqlite.c | sort | varlist 'sources.c'
+find src/*.js -not -name index.js | sort | varlist 'sources.js'
+find tests/js/*.js | sort | varlist 'tests.js'
+
+sh tools/cdeps.sh `find src/*.c -not -name napi-sqlite.c | sort`
diff --git a/src/api.js b/src/api.js
index 17223cd..7f0991f 100644
--- a/src/api.js
+++ b/src/api.js
@@ -2,19 +2,19 @@ const { eq } = require("./utils.js");
const ircd = require("./ircd.js");
const web = require("./web.js");
-const main = () => {
+const main = async () => {
if (process.argv.length === 3 && process.argv[2] === "-V") {
console.log("papo 1970-01-01 0.1.0");
return;
}
if (process.argv[2] === "ircd") {
- ircd.app(process.argv[3]);
+ await ircd.app(process.argv[3]);
return;
}
if (process.argv[2] === "web") {
- web.app(process.argv[3]);
+ await web.app(process.argv[3]);
return;
}
diff --git a/src/catalog.c b/src/catalog.c
new file mode 100644
index 0000000..c3ec9b2
--- /dev/null
+++ b/src/catalog.c
@@ -0,0 +1,501 @@
+#include "catalog.h"
+#include "logerr.h"
+
+#include <assert.h>
+#include <errno.h>
+#include <nl_types.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef TEST
+#include "../tests/tests-lib.h"
+#include "../tests/slurp.h"
+
+static const char *const
+FNAME = __FILE__ ".txt";
+
+enum TEST_MSGCATALOG_ID {
+ MSG_X_FIRST = 1,
+ MSG_X_1,
+ MSG_X_2,
+ MSG_X_LAST,
+ MSG_STANDALONE,
+};
+
+static const char *const
+TEST_MSGS[] = {
+ "",
+ [MSG_X_FIRST]="First line\n",
+ [MSG_X_1]="a second\n",
+ [MSG_X_2]="a third\n",
+ [MSG_X_LAST]="and the last one\n",
+ [MSG_STANDALONE]="single line message\n",
+ NULL
+};
+#endif
+
+
+
+static const char *const
+CATALOG_NAME = NAME_MACRO_STRING;
+
+static nl_catd
+catalog_descriptor = NULL;
+
+static const char *const
+NLSPATH = LOCALEDIR_MACRO_STRING "/%l_%t/LC_MESSAGES/%N.cat" ":"
+ LOCALEDIR_MACRO_STRING "/%l/LC_MESSAGES/%N.cat";
+
+static const char *const
+NLSPATH_KEY = "NLSPATH";
+
+
+int
+i18n_init(void) {
+ int rc = 0;
+
+ static const int should_overwrite = 0;
+ if (setenv(NLSPATH_KEY, NLSPATH, should_overwrite)) {
+ logerr("setenv(\"%s\", \"%s\", 0): %s\n", NLSPATH_KEY,
+ NLSPATH, strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ catalog_descriptor = catopen(CATALOG_NAME, 0);
+ if (catalog_descriptor && catalog_descriptor == (nl_catd)-1) {
+ logerr("catopen(\"%s\", 0): %s\n", CATALOG_NAME, strerror(errno));
+ catalog_descriptor = NULL;
+ rc = -1;
+ goto out;
+ }
+
+out:
+ return rc;
+}
+
+#ifdef TEST
+static int
+test_i18n_init(void) {
+ int rc = 0;
+
+ test_start("i18n_init()");
+
+ {
+ testing("simple call without touching the environment");
+
+ static const int should_overwrite = 1;
+ if (setenv(NLSPATH_KEY, "src/%N.en.cat", should_overwrite)) {
+ logerr("setenv(\"%s\", \"src/%%N.en.cat\", 1): %s\n",
+ NLSPATH_KEY, strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ if (i18n_init()) {
+ logerr("i18n_init()\n");
+ rc = -1;
+ goto out;
+ }
+
+ test_ok();
+ }
+
+out:
+ if (i18n_destroy()) {
+ logerr("i18n_destroy()\n");
+ rc = -1;
+ }
+ return rc;
+}
+#endif
+
+
+int
+i18n_destroy(void) {
+ int rc = 0;
+
+ if (catalog_descriptor) {
+ if (catclose(catalog_descriptor)) {
+ logerr("catclose(...): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+ }
+
+out:
+ if (catalog_descriptor) {
+ catalog_descriptor = NULL;
+ }
+ return rc;
+}
+
+#ifdef TEST
+static int
+test_i18n_destroy(void) {
+ int rc = 0;
+
+ test_start("i18n_destroy()");
+
+ {
+ testing("simple call without init first");
+
+ if (i18n_destroy()) {
+ logerr("i18n_destroy()\n");
+ rc = -1;
+ goto out;
+ }
+
+ test_ok();
+ }
+
+out:
+ return rc;
+}
+#endif
+
+
+/**
+ * Infallible: always returns a valid string, no matter what.
+ */
+const char *
+s(const char* const MSGS[], const int msg_id) {
+ assert(msg_id > 0);
+ // FIXME: assert within bounds!
+ // printf("sizeof(MSGS): %ld\n", sizeof(MSGS));
+ if (!catalog_descriptor) {
+ return MSGS[msg_id];
+ }
+
+ errno = 0;
+ const char *const ret =
+ catgets(catalog_descriptor, NL_SETD, msg_id, MSGS[msg_id]);
+ if (errno) {
+ logerr("catgets(%d): %s\n", msg_id, strerror(errno));
+ }
+
+ return ret;
+}
+
+#ifdef TEST
+static int
+test_s(void) {
+ int rc = 0;
+
+ test_start("_()");
+ FILE *file = NULL;
+ char *str = NULL;
+
+ {
+ testing("empty string");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ // FIXME: implement correct test
+
+
+
+ test_ok();
+ }
+
+out:
+ if (str) {
+ free(str);
+ }
+ if (file) {
+ if (fclose(file)) {
+ logerr("fclose(file): %s\n", strerror(errno));
+ rc = -1;
+ }
+ }
+ return rc;
+}
+#endif
+
+
+int
+s_print_msgs(
+ const char *const MSGS[],
+ FILE *restrict stream,
+ const int msg_begin,
+ const int msg_end
+) {
+ int rc = 0;
+
+ for (int i = msg_begin; i <= msg_end; i++) {
+ if (fprintf(stream, "%s", s(MSGS, i)) < 0) {
+ logerr("fprintf(stream, \"%%s\", _(%d)): %s\n", i,
+ strerror(errno));
+ rc = -1;
+ goto out;
+ }
+ }
+
+out:
+ return rc;
+}
+
+#ifdef TEST
+static int
+test_s_print_msgs(void) {
+ int rc = 0;
+
+ test_start("s_print_msgs()");
+ FILE *file = NULL;
+ char *str = NULL;
+
+ {
+ testing("message in range");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ if (s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_LAST)) {
+ logerr("print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_LAST)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ logerr("fclose(file): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ logerr("slurp_for_tests(FNAME, &str)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "First line\n"
+ "a second\n"
+ "a third\n"
+ "and the last one\n"
+ ;
+
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+ {
+ testing("range begin and end is the same");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ logerr("fopen(FNAME, \"w\"): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ if (s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_FIRST)) {
+ logerr("s_print_msgs(TEST_MSGS, file, MSG_X_FIRST, MSG_X_FIRST)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ logerr("fclose(file): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ logerr("slurp_for_tests(FNAME, &str)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "First line\n";
+
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+
+out:
+ if (str) {
+ free(str);
+ }
+ if (file) {
+ if (fclose(file)) {
+ logerr("fclose(file): %s\n", strerror(errno));
+ rc = -1;
+ }
+ }
+ return rc;
+}
+#endif
+
+
+int
+s_print_msg(const char *const MSGS[], FILE *const fd, const int msg_id) {
+ return s_print_msgs(MSGS, fd, msg_id, msg_id);
+}
+
+#ifdef TEST
+static int
+test_s_print_msg(void) {
+ int rc = 0;
+
+ test_start("s_print_msg()");
+ FILE *file = NULL;
+ char *str = NULL;
+
+ {
+ testing("simple individual message");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ logerr("fopen(FNAME, \"w\"): %s\n");
+ rc = -1;
+ goto out;
+ }
+
+ if (s_print_msg(TEST_MSGS, file, MSG_STANDALONE)) {
+ logerr("s_print_msg(TEST_MSGS, file, MSG_STANDALONE)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ logerr("fopen(file): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ logerr("slurp_for_tests(FNAME, &str)\n");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "single line message\n";
+
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+
+out:
+ if (str) {
+ free(str);
+ }
+ if (file) {
+ if (fclose(file)) {
+ logerr("fclose(file): %s\n", strerror(errno));
+ rc = -1;
+ }
+ }
+ return rc;
+}
+#endif
+
+
+int
+dump_translatable_strings(const char *const MSGS[]) {
+ int rc = 0;
+
+ for (size_t i = 1; MSGS[i]; i++) {
+ if (printf("%ld ", i) < 0) {
+ logerr("printf(\"%%ld\", %d): %s\n", i);
+ rc = -1;
+ goto out;
+ }
+
+ for (size_t j = 0; MSGS[i][j]; j++) {
+ if (MSGS[i][j] == '\n') {
+ if (printf("\\n") < 0) {
+ logerr("printf(\"\\\\n\"): %s\n",
+ strerror(errno));
+ rc = -1;
+ goto out;
+ }
+ } else {
+ if (printf("%c", MSGS[i][j]) < 0) {
+ logerr("printf(\"%%c\", "
+ "MSGS[%ld][%ld]): %s\n",
+ i, j, strerror(errno));
+ rc = -1;
+ goto out;
+ }
+ }
+ }
+
+ if (printf("\n\n") < 0) {
+ logerr("printf(\"\\n\\n\"): %s\n", strerror(errno));
+ rc = -1;
+ goto out;
+ }
+ }
+
+out:
+ return rc;
+}
+
+#ifdef TEST
+int
+main(void) {
+ int rc = 0;
+
+ if (test_i18n_init()) {
+ logerr("test_i18n_init()\n");
+ rc = -1;
+ goto out;
+ }
+
+ if (test_i18n_destroy()) {
+ logerr("test_i18n_destroy()\n");
+ rc = -1;
+ goto out;
+ }
+
+ if (test_s()) {
+ logerr("test_s()\n");
+ rc = -1;
+ goto out;
+ }
+
+ if (test_s_print_msgs()) {
+ logerr("test_s_print_msgs()\n");
+ rc = -1;
+ goto out;
+ }
+
+ if (test_s_print_msg()) {
+ logerr("test_s_print_msg()\n");
+ rc = -1;
+ goto out;
+ }
+
+out:
+ return !!rc;
+}
+#endif
diff --git a/src/catalog.h b/src/catalog.h
new file mode 100644
index 0000000..9f237a9
--- /dev/null
+++ b/src/catalog.h
@@ -0,0 +1,33 @@
+#ifndef CATALOG_H
+#define CATALOG_H
+
+#include "config.h"
+
+#include <stdio.h>
+
+
+int
+i18n_init(void);
+
+int
+i18n_destroy(void);
+
+const char *
+s(const char *const MSGS[], const int msg_id);
+
+int
+s_print_msgs(
+ const char *const MSGS[],
+ FILE *restrict stream,
+ const int msg_begin,
+ const int msg_end
+);
+
+int
+s_print_msg(const char *const MSGS[], FILE *restrict stream, const int msg_id);
+
+int
+dump_translatable_strings(const char *const MSGS[]);
+
+
+#endif
diff --git a/src/config.h.in b/src/config.h.in
new file mode 100644
index 0000000..4867dc0
--- /dev/null
+++ b/src/config.h.in
@@ -0,0 +1,12 @@
+#ifndef CONFIG_H
+#define CONFIG_H
+
+#define _XOPEN_SOURCE 700
+#define _POSIX_C_SOURCE 200809L
+
+#define VERSION_MACRO_STRING "@VERSION@"
+#define DATE_MACRO_STRING "@DATE@"
+#define NAME_MACRO_STRING "@NAME@"
+#define LOCALEDIR_MACRO_STRING "@LOCALEDIR@"
+
+#endif
diff --git a/src/db.js b/src/db.js
new file mode 100644
index 0000000..e735ab1
--- /dev/null
+++ b/src/db.js
@@ -0,0 +1,45 @@
+const fs = require("node:fs");
+const sqlite = require("./napi-sqlite.node");
+
+// const value = 8;
+// console.log(`${value} time 2 =`, sqlite.my_function(value));
+
+const CONFIG_FILE = __dirname + "/sql/config.sql";
+const MIGRATIONS_DIR = __dirname + "/sql/migrations/";
+
+let db = null;
+const init = async () => {
+ console.log({
+ sqlite,
+ });
+ console.log(`sqlite.myfn(2): ${sqlite.myfn(2)}`);
+ console.log(`sqlite.open(2): ${sqlite.open(2)}`);
+ /*
+ const config = fs.readFileSync(CONFIG_FILE, "UTF-8");
+ const migrations = fs.readdirSync(MIGRATIONS_DIR, "UTF-8");
+
+ await exec(config);
+ await exec(`
+ CREATE TABLE IF NOT EXISTS migrations (
+ filename TEXT PRIMARY KEY
+ );
+ `);
+ const done = await run(`
+ SELECT filename FROM migrations;
+ `);
+
+ // FIXME: sort
+ const pending = new Set(migrations).difference(new Set(done));
+
+ await exec("BEGIN TRANSACTION;");
+ for (const p of pending) {
+ await exec(fs.readFileSync(MIGRATIONS_DIR + p, "UTF-8"));
+ await exec(`INSERT INTO migrations (filename) VALUES (?)`, p);
+ }
+ await exec("COMMIT TRANSACTION;");
+ */
+};
+
+module.exports = {
+ init,
+};
diff --git a/src/i18n.c b/src/i18n.c
new file mode 100644
index 0000000..245ad84
--- /dev/null
+++ b/src/i18n.c
@@ -0,0 +1,68 @@
+#include "i18n.h"
+
+#ifdef TEST
+#include "logerr.h"
+#include "catalog.h"
+
+#include <stdlib.h>
+#endif
+
+const char *const
+MSGS[] = {
+ "",
+ [MSG_USAGE_FIRST]="Usage:\n",
+ [MSG_USAGE_1]=" " NAME_MACRO_STRING " -p FILE [-o DIRECTORY]\n",
+ [MSG_USAGE_2]=" " NAME_MACRO_STRING " -l FILE [-o DIRECTORY]\n",
+ [MSG_USAGE_LAST]=" " NAME_MACRO_STRING " [-hV]\n",
+ [MSG_HELP_FIRST]="\n",
+ [MSG_HELP_1]="\n",
+ [MSG_HELP_2]="Options:\n",
+ [MSG_HELP_3]=" -p FILE parser file to be processed\n",
+ [MSG_HELP_4]=" -l FILE lexer file to be processed\n",
+ [MSG_HELP_5]=" -o DIRECTORY output where to place the\n",
+ [MSG_HELP_6]=" generated files (default .)\n",
+ [MSG_HELP_7]=" -h, --help show this help message\n",
+ [MSG_HELP_8]=" -V, --version print the version number\n",
+ [MSG_HELP_9]="\n",
+ [MSG_HELP_10]="\n",
+ [MSG_HELP_11]="Run the " NAME_MACRO_STRING "(1) parser program.\n",
+ [MSG_HELP_12]="\n",
+ [MSG_HELP_13]="Here is the explanation for what it does, and the synopsis\n",
+ [MSG_HELP_14]="of its usage.\n",
+ [MSG_HELP_15]="\n",
+ [MSG_HELP_16]="See \"man " NAME_MACRO_STRING "\" for usage information and\n",
+ [MSG_HELP_17]="\"man " NAME_MACRO_STRING ".tutorial\" for a beginner introduction.\n",
+ [MSG_HELP_18]="\n",
+ [MSG_HELP_19]="\n",
+ [MSG_HELP_20]="Examples:\n",
+ [MSG_HELP_21]="\n",
+ [MSG_HELP_22]=" Do a one-line parser:\n",
+ [MSG_HELP_23]="\n",
+ [MSG_HELP_24]=" $ " NAME_MACRO_STRING " run md.grammar < README.md\n",
+ [MSG_HELP_25]="\n",
+ [MSG_HELP_26]="\n",
+ [MSG_HELP_27]=" Compile the grammer:\n",
+ [MSG_HELP_28]="\n",
+ [MSG_HELP_LAST]=" $ " NAME_MACRO_STRING " build csv.grammar > dunno.alsodunno\n",
+ [MSG_VERSION]= NAME_MACRO_STRING " " VERSION_MACRO_STRING " " DATE_MACRO_STRING "\n",
+ NULL
+};
+
+
+#ifdef TEST
+int
+main(void) {
+ int rc = 0;
+
+ if (getenv("DUMP_TRANSLATABLE_STRINGS")) {
+ if (dump_translatable_strings(MSGS)) {
+ logerr("dump_translatable_strings(MSGS)\n");
+ rc = -1;
+ goto out;
+ }
+ }
+
+out:
+ return !!rc;
+}
+#endif
diff --git a/src/i18n.h b/src/i18n.h
new file mode 100644
index 0000000..8245564
--- /dev/null
+++ b/src/i18n.h
@@ -0,0 +1,55 @@
+#ifndef I18N_H
+#define I18N_H
+
+#include "config.h"
+#include "catalog.h"
+
+#include <stdio.h>
+
+
+enum MSGCATALOG_ID {
+ MSG_USAGE_FIRST = 1,
+ MSG_USAGE_1,
+ MSG_USAGE_2,
+ MSG_USAGE_LAST,
+ MSG_HELP_FIRST,
+ MSG_HELP_1,
+ MSG_HELP_2,
+ MSG_HELP_3,
+ MSG_HELP_4,
+ MSG_HELP_5,
+ MSG_HELP_6,
+ MSG_HELP_7,
+ MSG_HELP_8,
+ MSG_HELP_9,
+ MSG_HELP_10,
+ MSG_HELP_11,
+ MSG_HELP_12,
+ MSG_HELP_13,
+ MSG_HELP_14,
+ MSG_HELP_15,
+ MSG_HELP_16,
+ MSG_HELP_17,
+ MSG_HELP_18,
+ MSG_HELP_19,
+ MSG_HELP_20,
+ MSG_HELP_21,
+ MSG_HELP_22,
+ MSG_HELP_23,
+ MSG_HELP_24,
+ MSG_HELP_25,
+ MSG_HELP_26,
+ MSG_HELP_27,
+ MSG_HELP_28,
+ MSG_HELP_LAST,
+ MSG_VERSION,
+};
+
+
+extern const char *const
+MSGS[];
+
+#define _(msg_id) s(MSGS, msg_id)
+
+
+#endif
diff --git a/src/ircd.js b/src/ircd.js
index affc986..6a02bd6 100644
--- a/src/ircd.js
+++ b/src/ircd.js
@@ -1,11 +1,13 @@
const net = require("node:net");
+const db = require("./db.js");
const server = net.createServer(socket => {
socket.write("olar\r\n");
socket.pipe(socket);
});
-const app = udsPath => {
+const app = async udsPath => {
+ await db.init();
server.listen(udsPath, () => {
console.log("I'm ircd.");
});
diff --git a/src/logerr.c b/src/logerr.c
new file mode 100644
index 0000000..936bd28
--- /dev/null
+++ b/src/logerr.c
@@ -0,0 +1,301 @@
+#include "logerr.h"
+
+#include <stdarg.h>
+#include <stdlib.h>
+
+#ifdef TEST
+#include "../tests/tests-lib.h"
+#include "../tests/slurp.h"
+#include <assert.h>
+#include <errno.h>
+#include <string.h>
+#endif
+
+
+void
+vlogerr(
+ const char *const file,
+ const char *const function,
+ const int lineno,
+ FILE *restrict stream,
+ const char *restrict format,
+ ...
+) {
+ (void)fprintf(stream, "%s:%s:%d: ", file, function, lineno);
+
+ va_list args;
+ va_start(args, format);
+ (void)vfprintf(stream, format, args);
+ va_end(args);
+}
+
+#ifdef TEST
+static const char *const
+FNAME = __FILE__ ".txt";
+
+static int
+test_vlogerr(void) {
+ int rc = 0;
+
+ test_start("vlogerr()");
+ FILE *file = NULL;
+ char *str = NULL;
+
+ {
+ testing("empty varargs");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ vlogerr(__FILE__, __func__, __LINE__, file,
+ "");
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ perror("fclose(file)");
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ perror("slurp_for_tests(FNAME, &str)");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "src/logerr.c:test_vlogerr:54: ";
+
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+ {
+ testing("a newline only");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ vlogerr(__FILE__, __func__, __LINE__, file,
+ "\n");
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ perror("fclose(file)");
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ perror("slurp_for_tests(FNAME, &str)");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "src/logerr.c:test_vlogerr:91: \n";
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+ {
+ testing("static format string");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ vlogerr(__FILE__, __func__, __LINE__, file,
+ "some static string\n");
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ perror("fclose(file)");
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ perror("slurp_for_tests(FNAME, &str)");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "src/logerr.c:test_vlogerr:127: some static string\n";
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+ {
+ testing("single arg format string");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ vlogerr(__FILE__, __func__, __LINE__, file,
+ "fn(%s)\n", "an-arg");
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ perror("fclose(file)");
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ perror("slurp_for_tests(FNAME, &str)");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "src/logerr.c:test_vlogerr:163: fn(an-arg)\n";
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+ {
+ testing("multiple format strings");
+
+ file = fopen(FNAME, "w");
+ if (!file) {
+ perror("fopen(FNAME, \"w\")");
+ rc = -1;
+ goto out;
+ }
+
+ vlogerr(__FILE__, __func__, __LINE__, file,
+ "int (%d), string (%s) and char (%c)\n",
+ 123,
+ "another-str",
+ 'z');
+
+ const int ret = fclose(file);
+ file = NULL;
+ if (ret) {
+ perror("fclose(file)");
+ rc = -1;
+ goto out;
+ }
+
+ if (slurp_for_tests(FNAME, &str)) {
+ perror("slurp_for_tests(FNAME, &str)");
+ rc = -1;
+ goto out;
+ }
+
+ const char *const expected =
+ "src/logerr.c:test_vlogerr:199: "
+ "int (123), string (another-str) and char (z)\n";
+ assert(strcmp(expected, str) == 0);
+
+ free(str);
+ str = NULL;
+
+ test_ok();
+ }
+
+out:
+ if (str) {
+ free(str);
+ }
+ if (file) {
+ if (fclose(file)) {
+ perror("fclose(file)");
+ rc = -1;
+ }
+ }
+ return rc;
+}
+
+static int
+test_logerr(void) {
+ int rc = 0;
+
+ test_start("logerr()");
+
+ {
+ testing("can be called with an empty string");
+
+ logerr("");
+
+ test_ok();
+ }
+ {
+ testing("can be called with a static string");
+
+ logerr("some err\n");
+
+ test_ok();
+ }
+ {
+ testing("can be called with a formatted string");
+
+ logerr("some err: %s\n", strerror(errno));
+
+ test_ok();
+ }
+ {
+ testing("can be called with formatting arguments");
+
+ logerr("int: %d\nstr: %s\n", 123, "an example string");
+
+ test_ok();
+ }
+
+ return rc;
+}
+
+
+int
+main(void) {
+ int rc = 0;
+
+ if (test_vlogerr()) {
+ perror("test_vlogerr()");
+ rc = -1;
+ goto out;
+ }
+
+ if (test_logerr()) {
+ perror("test_logerr()");
+ rc = -1;
+ goto out;
+ }
+
+out:
+ return !!rc;
+}
+#endif
diff --git a/src/logerr.h b/src/logerr.h
new file mode 100644
index 0000000..77f98bd
--- /dev/null
+++ b/src/logerr.h
@@ -0,0 +1,23 @@
+#ifndef LOGERR_H
+#define LOGERR_H
+
+#include "config.h"
+
+#include <stdio.h>
+
+
+void
+vlogerr(
+
+ const char *const file,
+ const char *const function,
+ const int lineno,
+ FILE *restrict stream,
+ const char *restrict format,
+ ...
+);
+
+#define logerr(...) vlogerr(__FILE__, __func__, __LINE__, stderr, __VA_ARGS__)
+
+
+#endif
diff --git a/src/napi-sqlite.c b/src/napi-sqlite.c
index 7c0b872..df3b042 100644
--- a/src/napi-sqlite.c
+++ b/src/napi-sqlite.c
@@ -1,7 +1,18 @@
+#include <stdio.h>
+
#include <node/node_api.h>
+/*
+FIXME
+static const napi_type_tag SQLITE_DB_TYPE_TAG = {
+ 0x0e9614d459f746cc, 0x88b814a5dc5c4cf7
+};
+*/
+
static napi_value
-my_function(napi_env env, napi_callback_info info) {
+myfn(napi_env env, napi_callback_info info) {
+ napi_value ret = NULL;
+
napi_status status;
size_t argc = 1;
int number;
@@ -11,51 +22,99 @@ my_function(napi_env env, napi_callback_info info) {
status = napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Failed to parse arguments FIXME i18n");
- // FIXME: does execution somehow halt here? Or is it missing a
- // return?
+ goto out;
}
status = napi_get_value_int32(env, argv[0], &number);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Invalid number was passed as argument FIXME i18n");
- // FIXME: return?
+ goto out;
}
number = number * 2;
status = napi_create_int32(env, number, &my_number);
if (status != napi_ok) {
napi_throw_error(env, NULL, "Unable to create return value FIXME i18n");
- // FIXME: return?
+ goto out;
}
+ ret = my_number;
+
+out:
+ return ret;
+}
- return my_number;
+static napi_value
+open(napi_env env, napi_callback_info info) {
+ (void)env;
+ (void)info;
+ return NULL;
}
static napi_value
+close(napi_env env, napi_callback_info info) {
+ (void)env;
+ (void)info;
+ return NULL;
+}
+
+static const struct {
+ const char *label;
+ napi_value(*const handle)(napi_env env, napi_callback_info info);
+} fns[] = {
+ { .label = "myfn", .handle = myfn, },
+ { .label = "open", .handle = open, },
+ { .label = "close", .handle = close, },
+ { NULL, NULL },
+};
+
+static napi_value
init(napi_env env, napi_value exports) {
+ napi_value ret = exports;
+
napi_status status;
- napi_value fn;
- status = napi_create_function(env, NULL, 0, my_function, NULL, &fn);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to wrap native function FIXME i18n");
- // FIXME: return?
- }
+ for (size_t i = 0; fns[i].label && fns[i].handle; i++) {
+ napi_value fn;
+ status = napi_create_function(
+ env,
+ fns[i].label,
+ NAPI_AUTO_LENGTH,
+ fns[i].handle,
+ "xucrutes",
+ &fn
+ );
+ if (status != napi_ok) {
+ ret = NULL;
+ napi_throw_error(
+ env,
+ "SQLITE_FN_CREATE",
+ "Unable to wrap native function FIXME i18n"
+ );
+ goto out;
+ }
- status = napi_set_named_property(env, exports, "my_function", fn);
- if (status != napi_ok) {
- napi_throw_error(env, NULL, "Unable to populate exports FIXME i18n");
- // FIXME: return?
+ status = napi_set_named_property(
+ env,
+ exports,
+ fns[i].label,
+ fn
+ );
+ if (status != napi_ok) {
+ ret = NULL;
+ napi_throw_error(
+ env,
+ "SQLITE_FN_SETNAME",
+ "Unable to populate exports FIXME i18n"
+ );
+ goto out;
+ }
}
- return exports;
+out:
+ return ret;
}
-napi_value
-sqlite_napi_init(napi_env env, napi_value exports) {
+NAPI_MODULE_INIT() {
return init(env, exports);
}
-
-//NAPI_MODULE(NODE_GYP_MODULE_NAME, sqlite_napi_init)
-NAPI_MODULE(FIXME_CAN_THIS_BE_ANYTHING, sqlite_napi_init)
diff --git a/src/papo.en.msg b/src/papo.en.msg
new file mode 100644
index 0000000..f1679a3
--- /dev/null
+++ b/src/papo.en.msg
@@ -0,0 +1,70 @@
+1 Usage:\n
+
+2 papo -p FILE [-o DIRECTORY]\n
+
+3 papo -l FILE [-o DIRECTORY]\n
+
+4 papo [-hV]\n
+
+5 \n
+
+6 \n
+
+7 Options:\n
+
+8 -p FILE parser file to be processed\n
+
+9 -l FILE lexer file to be processed\n
+
+10 -o DIRECTORY output where to place the\n
+
+11 generated files (default .)\n
+
+12 -h, --help show this help message\n
+
+13 -V, --version print the version number\n
+
+14 \n
+
+15 \n
+
+16 Run the papo(1) parser program.\n
+
+17 \n
+
+18 Here is the explanation for what it does, and the synopsis\n
+
+19 of its usage.\n
+
+20 \n
+
+21 See "man papo" for usage information and\n
+
+22 "man papo.tutorial" for a beginner introduction.\n
+
+23 \n
+
+24 \n
+
+25 Examples:\n
+
+26 \n
+
+27 Do a one-line parser:\n
+
+28 \n
+
+29 $ papo run md.grammar < README.md\n
+
+30 \n
+
+31 \n
+
+32 Compile the grammer:\n
+
+33 \n
+
+34 $ papo build csv.grammar > dunno.alsodunno\n
+
+35 papo 0.1.0 1970-01-01\n
+
diff --git a/src/sql/config.sql b/src/sql/config.sql
new file mode 100644
index 0000000..53eb279
--- /dev/null
+++ b/src/sql/config.sql
@@ -0,0 +1,34 @@
+; "Litestream requires periodic but short write locks on the database when
+; checkpointing occurs":
+; https://litestream.io/tips/#busy-timeout
+PRAGMA busy_timeout = 5000;
+
+; "Litestream only works with the SQLite WAL journaling mode":
+; https://litestream.io/tips/#wal-journal-mode
+PRAGMA journal_mode = WAL;
+
+; "(...) change the synchronous mode to NORMAL (it typically defaults to FULL)":
+; https://litestream.io/tips/#synchronous-pragma
+; "WAL mode is safe from corruption with synchronous=NORMAL":
+; https://www.sqlite.org/pragma.html#pragma_synchronous
+PRAGMA synchronous = NORMAL;
+
+; "(...) can perform a checkpoint in between Litestream-initiated checkpoints
+; and cause Litestream to miss a WAL file":
+; https://litestream.io/tips/#disable-autocheckpoints-for-high-write-load-servers
+PRAGMA wal_autocheckpoint = 0;
+
+; "This pragma does a low-level formatting and consistency check of the
+; database":
+; https://www.sqlite.org/pragma.html#pragma_integrity_check
+PRAGMA integrity_check;
+
+; "The foreign_key_check pragma checks the database, or the table called
+ \"table-name\", for foreign key constraints that are violated":
+; https://www.sqlite.org/pragma.html#pragma_foreign_key_check
+PRAGMA foreign_key_check;
+
+
+CREATE TABLE IF NO EXISTS migrations (
+ name TEXT PRIMARY KEY
+);
diff --git a/tests/assert-deps.sh b/tests/assert-deps.sh
index 9bfc5ea..b73933d 100755
--- a/tests/assert-deps.sh
+++ b/tests/assert-deps.sh
@@ -1,45 +1,10 @@
#!/bin/sh
set -eu
-if [ ! -e .git ]; then
- echo "Not in a Git repository, skipping \"$0\"." >&2
- exit
-fi
-
-
. tools/lib.sh
-F="$(mkstemp)"
-trap 'rm -f "$F"' EXIT
-
-
-awk '
- $0 == "sources.js = \\" { sources = 1; next }
- $0 == "tests.js = \\" { tests = 1; next }
- sources == 1 && $0 == "" { sources = 2; next }
- tests == 1 && $0 == "" { tests = 2; next }
-
- sources == 1 || tests == 1 {
- print $1
- }
-
- END {
- if (sources != 2) {
- print "Could not find $(sources.js) in Makefile." \
- > "/dev/stderr"
- exit 2
- }
- if (tests != 2) {
- print "Could not find $(tests.js) in Makefile." \
- > "/dev/stderr"
- exit 2
- }
- }
-' Makefile | LANG=POSIX.UTF-8 sort > "$F"
-
-printf '%s: all JavaScript sources are listed in the Makefile...' \
- "$(yellow "$0")" >&2
-git ls-files src/*.js tests/js/*.js |
- LANG=POSIX.UTF-8 sort |
- diff -U10 "$F" -
-printf ' %s\n' "$(green 'OK')" >&2
+{
+ printf '%s: all deps.mk is up-to-date...' "$(yellow "$0")"
+ sh mkdeps.sh | diff -U10 deps.mk -
+ printf ' %s\n' "$(green 'OK')"
+} >&2
diff --git a/tests/c-lint.sh b/tests/c-lint.sh
new file mode 100755
index 0000000..37822f3
--- /dev/null
+++ b/tests/c-lint.sh
@@ -0,0 +1,65 @@
+#!/bin/sh
+set -eu
+
+awk '
+BEGIN {
+ ret = 0
+ msg = "function not on the start of the line:"
+}
+
+/^[a-zA-Z0-9_]+ [^=]+\(/ {
+ if (ret == 0) {
+ print msg
+ }
+ printf "%s:%s:%s\n", FILENAME, FNR, $0
+ ret = 1
+}
+
+END {
+ exit ret
+}
+' "$@"
+
+
+awk '
+BEGIN {
+ ret = 0
+ static = 1
+ msg = "non-static function is not declared in a header:"
+}
+
+/^[a-zA-Z0-9_]+\(.*$/ && static == 0 {
+ split($0, line, /\(/)
+ fn_name = line[1]
+ if (fn_name != "main" && fn_name != "LLVMFuzzerTestOneInput") {
+ header = substr(FILENAME, 0, length(FILENAME) - 2) ".h"
+ if (system("grep -q ^\"" fn_name "\" \"" header "\"")) {
+ if (ret == 0) {
+ print msg
+ }
+ printf "%s:%s:%s\n", FILENAME, FNR, $0
+ ret = 1
+ }
+ }
+}
+
+/^static / {
+ static = 1
+}
+
+!/^static / {
+ static = 0
+}
+
+END {
+ exit ret
+}
+' "$@"
+
+
+RE='[a-z]+\(\) {'
+if grep -Eq "$RE" "$@"; then
+ echo 'Functions with no argument without explicit "void" parameter:' >&2
+ grep -En "$RE" "$@"
+ exit 1
+fi
diff --git a/tests/cli-opts.sh b/tests/cli-opts.sh
new file mode 100755
index 0000000..a25b66c
--- /dev/null
+++ b/tests/cli-opts.sh
@@ -0,0 +1,4 @@
+#!/bin/sh
+set -eu
+
+"$@" -V
diff --git a/tests/slurp.c b/tests/slurp.c
new file mode 100644
index 0000000..683a126
--- /dev/null
+++ b/tests/slurp.c
@@ -0,0 +1,69 @@
+#include "slurp.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+
+int
+slurp_for_tests(const char *const FNAME, char **strref) {
+ int rc = 0;
+
+ FILE *file = NULL;
+ char *str = NULL;
+
+ file = fopen(FNAME, "r");
+ if (!file) {
+ perror("fopen(FNAME, \"r\")");
+ rc = -1;
+ goto out;
+ }
+
+ if (fseek(file, 0L, SEEK_END)) {
+ perror("fseek(file, 0L, SEEK_END)");
+ rc = -1;
+ goto out;
+ }
+
+ const long lsize = ftell(file);
+ if (lsize == -1) {
+ perror("ftell(file)");
+ rc = -1;
+ goto out;
+ }
+ const size_t size = (size_t)lsize + sizeof(char);
+
+ if (fseek(file, 0L, SEEK_SET)) {
+ perror("fseek(file, 0L, SEEK_SET)");
+ rc = -1;
+ goto out;
+ }
+
+ str = malloc(size);
+ if (!str) {
+ perror("malloc(...)");
+ rc = -1;
+ goto out;
+ }
+
+ if (fread(str, sizeof(char), size - 1, file) != size - 1) {
+ perror("fread(...)");
+ rc = -1;
+ goto out;
+ }
+ str[size - 1] = '\0';
+ *strref = str;
+
+out:
+ if (file) {
+ if (fclose(file)) {
+ perror("flcose(file");
+ rc = -1;
+ }
+ }
+
+ if (rc) {
+ if (str) {
+ free(str);
+ }
+ }
+ return rc;
+}
diff --git a/tests/slurp.h b/tests/slurp.h
new file mode 100644
index 0000000..056c77d
--- /dev/null
+++ b/tests/slurp.h
@@ -0,0 +1,11 @@
+#ifndef SLURP_H
+#define SLURP_H
+
+#include "../src/config.h"
+
+
+int
+slurp_for_tests(const char *const FNAME, char **strref);
+
+
+#endif
diff --git a/tests/tests-lib.c b/tests/tests-lib.c
new file mode 100644
index 0000000..e46fb2d
--- /dev/null
+++ b/tests/tests-lib.c
@@ -0,0 +1,28 @@
+#include "tests-lib.h"
+#include <stdio.h>
+#include <assert.h>
+
+#define COLOUR_RESET "\033[0m"
+#define COLOUR_GREEN "\033[0;32m"
+#define COLOUR_YELLOW "\033[0;33m"
+
+void
+test_start(const char *const name) {
+ assert(fprintf(stdout, "%s:\n", name) > 0);
+}
+
+void
+testing(const char *const message) {
+ assert(
+ fprintf(
+ stdout,
+ COLOUR_YELLOW "testing" COLOUR_RESET ": %s...",
+ message
+ ) > 0
+ );
+}
+
+void
+test_ok(void) {
+ assert(fprintf(stdout, " " COLOUR_GREEN "OK" COLOUR_RESET ".\n") > 0);
+}
diff --git a/tests/tests-lib.h b/tests/tests-lib.h
new file mode 100644
index 0000000..a1e67ba
--- /dev/null
+++ b/tests/tests-lib.h
@@ -0,0 +1,15 @@
+#ifndef TESTS_LIB_H
+#define TESTS_LIB_H
+
+#include "../src/config.h"
+
+void
+test_start(const char *const name);
+
+void
+testing(const char *const message);
+
+void
+test_ok(void);
+
+#endif
diff --git a/tools/cdeps.sh b/tools/cdeps.sh
new file mode 100755
index 0000000..dd48d74
--- /dev/null
+++ b/tools/cdeps.sh
@@ -0,0 +1,225 @@
+#!/bin/sh
+set -eu
+
+. tools/lib.sh
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ tools/cdeps.sh FILE...
+ tools/cdeps.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ FILE toplevel entrypoint file
+
+
+ Given a list of C FILEs, generate the Makefile dependencies
+ between them.
+
+ We have 3 types of object files:
+ - .o: plain object files;
+ - .lo: object files compiled as relocatable so it can be
+ included in a shared library;
+ - .to: compiled with -DTEST so it can expose its embedded unit
+ tests;
+
+ We also have 2 aggregate files:
+ - .ta: an ar(1)-chive that includes all the .o dependencies the
+ $NAME.ta, plus the $NAME.to object. The goal is to have
+ "dep1.o", "dep2.o", ... "depN.o" included in the archive,
+ alongside "$NAME.to", so that recompiling "depN.o" would
+ replace only this file in the archive;
+ - .t: an executable "$NAME.t" derived from just linking together
+ all the objects inside a ".ta". Since the "main()" function
+ was only exposed in the "$NAME.to" via the -DTEST flag, this
+ executable is the runnable instance of all unit tests present
+ in "$NAME.c". Its exit code determines if its test suite
+ execution is successful.
+
+ Also in order to run the unit tests without having to relink
+ them on each run, we have:
+ - .t-run: a dedicated virtual target that does nothing but
+ execute the tests. In order to assert the binaries exist,
+ each "$NAME.t-run" virtual target depends on the equivalent
+ "$NAME".t physical target.
+
+ There are 2 types of dependencies that are generated:
+ 1. self dependencies;
+ 2. inter dependencies.
+
+ The self dependencies are the ones across different
+ manifestations of the same file so all derived assets are
+ correctly kept up-to-date:
+ - $NAME.o $NAME.lo $NAME.to: $NAME.h
+
+ As the .SUFFIXES rule already covers the dependency to the
+ orinal $NAME.c file, all we do is say that whenever the public
+ interface of these binaries change, they need to be
+ recompiled;
+ - $NAME.ta: $NAME.to
+
+ We make sure to include in each test archive (ta) file its own
+ binary with unit tests. We include the "depN.o" dependencies
+ later;
+ - $NAME.t-run: $NAME.t
+
+ Enforce that the binary exists before we run them.
+
+ After we establish the self dependencies, we scrub each file's
+ content looking for `#include "..."` lines that denote
+ dependency to other C file. Once we do that we'll have:
+ - $NAME.o $NAME.lo $NAME.to: dep1.h dep2.h ... depN.h
+
+ We'll recompile our file when its public header changes. When
+ only the body of the code changes we don't recompile, only
+ later relink;
+ - $NAME.ta: dep1.o dep2.o ... depN.o
+
+ Make sure to include all required dependencies in the $NAME.t
+ binary so that the later linking works properly.
+
+ So if we have file1.c, file2.c and file3.c with their respective
+ headers, where file2.c and file3.c depend of file1.c, i.e. they
+ have `#include "file.h"` in their code, and file3.c depend of
+ file2.c, the expected output is:
+
+ file1.o file1.lo file1.to: file1.h
+ file2.o file2.lo file2.to: file2.h
+ file3.o file3.lo file3.to: file3.h
+
+ file1.ta: file1.to
+ file2.ta: file2.to
+ file3.ta: file3.to
+
+ file1.t-run: file1.t
+ file2.t-run: file2.t
+ file3.t-run: file3.t
+
+
+ file1.o file1.lo file1.to:
+ file2.o file2.lo file2.to: file1.h
+ file3.o file3.lo file3.to: file1.h file2.h
+
+ file1.ta:
+ file2.ta: file1.o
+ file3.ta: file1.o file2.o
+
+ This ensures that only the minimal amount of files need to get
+ recompiled, but no less.
+
+
+ Examples:
+
+ Get deps for all files in 'src/' but 'src/main.c':
+
+ $ sh tools/cdeps.sh `find src/*.c -not -name 'main.c'`
+
+
+ Emit dependencies for all C files in a Git repository:
+
+ $ sh tools/cdeps.sh `git ls-files | grep '\.c$'`
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ (--)
+ break
+ ;;
+ (--help)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ (h)
+ usage
+ help
+ exit
+ ;;
+ (*)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+FILE="${1:-}"
+eval "$(assert_arg "$FILE" 'FILE')"
+
+
+
+each_f() {
+ fn="$1"
+ shift
+ for file in "$@"; do
+ f="${file%.c}"
+ "$fn" "$f"
+ done
+ printf '\n'
+}
+
+self_header_deps() {
+ printf '%s.o\t%s.lo\t%s.to:\t%s.h\n' "$1" "$1" "$1" "$1"
+}
+
+self_ta_deps() {
+ printf '%s.ta:\t%s.to\n' "$1" "$1"
+}
+
+self_trun_deps() {
+ printf '%s.t-run:\t%s.t\n' "$1" "$1"
+}
+
+deps_for() {
+ ext="$2"
+ for file in $(awk -F'"' '/^#include "/ { print $2 }' "$1.c"); do
+ if [ "$file" = 'config.h' ]; then
+ continue
+ fi
+ if [ "$(basename "$file")" = 'tests-lib.h' ]; then
+ continue
+ fi
+ f="$(dirname "$1")/$file"
+ if [ "$f" = "$1.h" ]; then
+ continue
+ fi
+ printf '%s\n' "${f%.h}$2"
+ done
+}
+
+rebuild_deps() {
+ printf '\n'
+ printf '%s.o\t%s.lo\t%s.to:' "$1" "$1" "$1"
+ printf ' %s' $(deps_for "$1" .h) | sed 's|^ $||'
+}
+
+archive_deps() {
+ printf '\n'
+ printf '%s.ta:' "$1"
+ printf ' %s' $(deps_for "$1" .o) | sed 's|^ $||'
+}
+
+
+each_f self_header_deps "$@"
+each_f self_ta_deps "$@"
+each_f self_trun_deps "$@"
+
+each_f rebuild_deps "$@"
+each_f archive_deps "$@"