From 79b1f3b2cfff7059badfdb62e782beec28ed073f Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Sun, 22 Nov 2020 00:56:01 +0100
Subject: [PATCH 1/5] Replace confini with toml

---
 CMakeLists.txt                       |   57 +-
 controller.ini                       |   69 -
 emgauwa-controller.conf              |   66 +
 emgauwa-controller.sqlite            |  Bin 0 -> 32768 bytes
 include/cli.h                        |   13 +
 include/config.h                     |   58 +-
 include/constants.h                  |   15 +-
 include/enums.h                      |    6 -
 include/helpers.h                    |    3 -
 src/{helpers/parse_cli.c => cli.c}   |   39 +-
 src/config.c                         |  505 ++-
 src/connections.c                    |    4 +-
 src/database.c                       |    2 +-
 src/handlers/command.c               |   16 +-
 src/handlers/discovery.c             |    2 +-
 src/handlers/loop.c                  |   12 +-
 src/helpers/drop_privileges.c        |    4 +-
 src/logger.c                         |   29 +-
 src/main.c                           |   61 +-
 src/models/controller.c              |   14 +-
 src/models/junction_relay_schedule.c |    8 +-
 src/models/relay.c                   |    2 +-
 src/runners/test.c                   |   10 +-
 vendor/confini.c                     | 5016 --------------------------
 vendor/confini.h                     |  547 ---
 vendor/toml.c                        | 2274 ++++++++++++
 vendor/toml.h                        |  175 +
 27 files changed, 3081 insertions(+), 5926 deletions(-)
 delete mode 100644 controller.ini
 create mode 100644 emgauwa-controller.conf
 create mode 100644 emgauwa-controller.sqlite
 create mode 100644 include/cli.h
 rename src/{helpers/parse_cli.c => cli.c} (52%)
 delete mode 100644 vendor/confini.c
 delete mode 100644 vendor/confini.h
 create mode 100644 vendor/toml.c
 create mode 100644 vendor/toml.h

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 3223e1c..ee771eb 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,11 +12,17 @@ target_link_libraries(controller -luuid)
 
 option(WIRING_PI_DEBUG "Use WiringPi Debugging Tool (OFF)" OFF)
 
-set(CMAKE_C_FLAGS "$ENV{CFLAGS}")
-SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -std=gnu99 -Wpedantic -Werror -Wall -Wextra -ffile-prefix-map=${CMAKE_SOURCE_DIR}/src/=")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} $ENV{CFLAGS}")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D'__FILENAME__=\"$(subst $(realpath ${CMAKE_SOURCE_DIR}/src/)/,,$(abspath $<))\"'")
+set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O3 -std=gnu99 -Wpedantic -Werror -Wall -Wextra")
 
 set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -fprofile-arcs -ftest-coverage")
 
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+
+file(GLOB_RECURSE ALL_SOURCE_FILES src/*.c)
+
 if(WIRING_PI_DEBUG)
     message("Showing wiringPi calls as debug")
     add_definitions("-DWIRING_PI_DEBUG")
@@ -32,7 +38,6 @@ aux_source_directory(vendor VENDOR_SRC)
 
 add_dependencies(controller sql)
 
-configure_file("controller.ini" "controller.ini" COPYONLY)
 configure_file("version.h.in" "version.h" @ONLY)
 
 
@@ -47,21 +52,43 @@ add_custom_target(sql
 )
 
 add_custom_target(run
-    COMMAND ./controller start
+    COMMAND ${CMAKE_BINARY_DIR}/controller
     DEPENDS controller
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-)
-add_custom_target(debug
-    COMMAND valgrind ./controller start
-    DEPENDS controller
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
-)
-add_custom_target(debug-full
-    COMMAND valgrind --leak-check=full --show-leak-kinds=all ./controller start
-    DEPENDS controller
-    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
 )
+
 add_custom_target(docs
     COMMAND doxygen
     WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
 )
+
+IF(CMAKE_BUILD_TYPE MATCHES Debug)
+    message(STATUS "loading debug targets")
+    add_custom_target(debug
+        COMMAND gdb ${CMAKE_BINARY_DIR}/controller
+        DEPENDS controller
+        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+    )
+    add_custom_target(valgrind
+        COMMAND valgrind -s ${CMAKE_BINARY_DIR}/controller
+        DEPENDS controller
+        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+    )
+    add_custom_target(valgrind-leak
+        COMMAND valgrind --leak-check=full --show-leak-kinds=all ${CMAKE_BINARY_DIR}/controller
+        DEPENDS controller
+        WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
+    )
+
+    add_custom_target(
+        clang-tidy
+        COMMAND /usr/bin/clang-tidy
+        ${ALL_SOURCE_FILES}
+        --header-filter=${CMAKE_SOURCE_DIR}/include/*
+        --
+        -std=gnu99
+        -I${CMAKE_SOURCE_DIR}/include
+        -I${CMAKE_SOURCE_DIR}/vendor
+        -I${CMAKE_BINARY_DIR}
+    )
+ENDIF(CMAKE_BUILD_TYPE MATCHES Debug)
diff --git a/controller.ini b/controller.ini
deleted file mode 100644
index dfec8e3..0000000
--- a/controller.ini
+++ /dev/null
@@ -1,69 +0,0 @@
-[controller]
-name = new emgauwa device
-
-: 4422 for testing; 4421 for dev-env; 4420 for testing-env; 4419 for prod-env
-discovery-port = 4421
-: 1886 for testing; 1885 for dev-env; 1884 for testing-env; 1883 for prod-env
-mqtt-port = 1885
-mqtt-host = localhost
-
-relay-count = 10
-relays-init = 1
-
-database = controller.sqlite
-log-level = debug
-log-file = stdout
-
-[relay-0]
-driver = piface
-pin = 0
-inverted = 0
-init = 0
-
-[relay-1]
-driver = piface
-pin = 1
-inverted = 0
-init = 1
-
-[relay-2]
-driver = gpio
-pin = 5
-inverted = 1
-
-[relay-3]
-driver = gpio
-pin = 4
-inverted = 1
-
-[relay-4]
-driver = gpio
-pin = 3
-inverted = 1
-
-[relay-5]
-driver = gpio
-pin = 2
-inverted = 1
-
-[relay-6]
-driver = gpio
-pin = 1
-inverted = 1
-pulse-duration = 3
-
-[relay-7]
-driver = gpio
-pin = 0
-inverted = 1
-pulse-duration = 3
-
-[relay-8]
-driver = gpio
-pin = 16
-inverted = 1
-
-[relay-9]
-driver = gpio
-pin = 15
-inverted = 1
diff --git a/emgauwa-controller.conf b/emgauwa-controller.conf
new file mode 100644
index 0000000..dcf6f9b
--- /dev/null
+++ b/emgauwa-controller.conf
@@ -0,0 +1,66 @@
+[controller]
+database = "emgauwa-controller.sqlite"
+
+mqtt-host = "127.0.0.1"
+
+[ports]
+# 4422 for testing; 4421 for dev-env; 4420 for testing-env; 4419 for prod-env
+discovery = 4421
+# 1886 for testing; 1885 for dev-env; 1884 for testing-env; 1883 for prod-env
+mqtt = 1885
+
+[logging]
+level = "debug"
+file = "stdout"
+
+[[relays]]
+driver = "piface"
+pin = 0
+inverted = 0
+
+[[relays]]
+driver = "piface"
+pin = 1
+inverted = 0
+
+[[relays]]
+driver = "gpio"
+pin = 5
+inverted = 1
+
+[[relays]]
+driver = "gpio"
+pin = 4
+inverted = 1
+
+[[relays]]
+driver = "gpio"
+pin = 3
+inverted = 1
+
+[[relays]]
+driver = "gpio"
+pin = 2
+inverted = 1
+
+[[relays]]
+driver = "gpio"
+pin = 1
+inverted = 1
+pulse-duration = 3
+
+[[relays]]
+driver = "gpio"
+pin = 0
+inverted = 1
+pulse-duration = 3
+
+[[relays]]
+driver = "gpio"
+pin = 16
+inverted = 1
+
+[[relays]]
+driver = "gpio"
+pin = 15
+inverted = 1
diff --git a/emgauwa-controller.sqlite b/emgauwa-controller.sqlite
new file mode 100644
index 0000000000000000000000000000000000000000..2adcfc3e5f901c770e70eaa28cf2b95e0db39f2b
GIT binary patch
literal 32768
zcmeI)PmB{)90%~3dGGzEGtl?IMN2{0S=T>XSipkFKNY&&*>$mWq1%;ku_@bGtZrNA
zpCAVzh<M|sT#WHxOiT<1|DEIjA;CmXMvW#OJQ%&82mdAhx%htVcG<3lm_Q_$-^+Ht
zGjHbiI`f%A()P_x>`N8vfj3>LmhyFPld;4wO=Fkm8HUlPA0ho{H?9}((Qc+*z{mAk
zpRs=Qt+4vtu-)Gbc|l#2<?uU;&xVidRca(a0wh2JBtQZrKmsK2-w2#qZQGF@JIvD)
z_58t^;F(5ws$Q&=bJbube=Jv<dO9dHuGt+*X8c6f_p*thl<!@m)*Wy?-HryqvxWRI
zTx_<9J&9CmES+s`Y<IRZJ)ZT_ld05Tb8G8ZiiP%8UFDDZnXZ)?f5gxD>7+m5HOp$=
zK(P?(y6bq_8}?JaKK^84BAFQWThG=$=iKZQUhmn4{gK3ED(l6&KEb?WX%G6Z4rjv8
z>LY88?8Z(8!jAOznx~@83p6*-+yZo5gt_in_O@ELv)G#%+mpx~@SgAwbR2(TGCMw&
z*2C`c)7chEV|JXO)c8=xzU?d3(K(qO+c!DOS<aUNyl_t@GReminSuDmt+Cdahl6Ud
zQmA>&F=KInh~H}(#d0Bdu6ATbzZJ{n8}&+aeeQ)=%f;u;&_21H+mUG0d||vv)4JZR
z-=YJh)pv76%8k;&pz3+uVB09I^Xw+z#`ZFZtX*p!XkFFT`PPCXji5Xg%$|OGm3N%X
zf{g<>{Gi@7cJjDsNBaBCQ_1F-Q<ZYPTA7&%s<nBGR>xTLHr!mD8-KmD|FQq`bu(2d
zmGb36?r^1A$GzHDAhy|p$XNfiUPANEH5Z?^!t{T%&G+_Uy<n=(4gEun1W14cNPq-L
zfCNZ@1W14cNPq-L;5HIiV+vz1&JMo#{>2m1yWc%`W%q?G#g(gr(xH6gXnx(*j{%Ic
zAG!LxO!b4If2ffF36KB@kN^pg011!)36KB@kN^q%4+MJHptJBp0sQ`-sV@xmllo2v
zH4-2J5+DH*AOR8}0TLhq5+DH*Ac5OOKr+_vT>YBA=2|Rj&fX6aHd(JZcRPuN@BdwO
z-cWz4U)7K5d-aX_N`0z6R+n@qH4-2J5+DH*AOR8}0TLhq5+DH*Ac0#hAY5*kVMjO^
zArZo`g^d9MFoX~oTyP8~7>30yZa86Xaw8PR_y3l<Z0K+QFW>SLq5~iS5+DH*AOR8}
z0TLhq5+DH*AORA%j=(BQ-|@e6d8ORBPo<21pZ|3c#XktpBP-L>9oumo@Bg>dm-_yH
z{fin2kN^pg011!)36KB@kN^pg011%5?If^_4Vnu-4Y2V3|An6c;P?L{;Va$d|9`07
zQ*Wy`)T`=cbw-_5&#Q(isX#rY_N#F<s&=X^YJ(b3eJZM!Do2^}5BamaBEOYi%g^K|
z@{&9+Kal6-oANdJiaaA<k|*R*S&@fiPVSd`<(N#!Z89!nvQMs%J#vv`;lIMa=!2m~
z0wh2JBtQZrKmsH{0wh2JByb}FFv3mK0T+Bs;leN`cOi+%To}T%T#?|8>9}GS59xrN
z*b5FkhAA9)6q7sf2qtr22d3qS?O5fAZCK^NR_uikY{3*Eco>t1U^6BQ!9$o<NNmEY
zkl2V-A&6ry*zh2xuwerxw_!advtb>kWs5<qvc+1gvSAJO0zeE?01RMqfCn%c!2OsO
zi2JY##JyMr(2u=(p8P$SLO>rT7vNzs0lk=(5Ua6Dh*el6;BM>%4l6MQhZUHdLll#7
zSdM9N5y2`h?!qb#cVaIvScWMW^k8xZcVIFGOEE1bmS7bV3ac1M>;((-RDE4#!D38q
z!6Hm%fs1Kbdj7wzvh@6aU1jO{|LCvh|Krur^Z#|={C^!d|6d2r|95n!$wQ9wH!IEC
A+W-In

literal 0
HcmV?d00001

diff --git a/include/cli.h b/include/cli.h
new file mode 100644
index 0000000..41e7d4a
--- /dev/null
+++ b/include/cli.h
@@ -0,0 +1,13 @@
+#ifndef CONTROLLER_CLI_H
+#define CONTROLLER_CLI_H
+
+typedef struct cli
+{
+    const char *config_file;
+    int demo_mode;
+} cli_t;
+
+void
+cli_parse(int argc, const char **argv, cli_t *cli);
+
+#endif /* CONTROLLER_CLI_H */
diff --git a/include/config.h b/include/config.h
index fe5b66a..9f623af 100644
--- a/include/config.h
+++ b/include/config.h
@@ -2,7 +2,8 @@
 #define CONTROLLER_CONFIG_H
 
 #include <stdint.h>
-#include <confini.h>
+
+#include <toml.h>
 
 #include <constants.h>
 #include <enums.h>
@@ -16,27 +17,50 @@ typedef struct
     uint8_t pulse_duration;
 } config_relay_t;
 
-int
-config_load(IniDispatch *disp, void *config_void);
-
 typedef struct
 {
-    char *file;
-    char database[256];
-    char user[256];
-    char group[256];
-    int log_level;
-    FILE *log_file;
-    run_type_t run_type;
-    char name[MAX_NAME_LENGTH + 1];
-    uint16_t discovery_port;
-    uint16_t mqtt_port;
-    char mqtt_host[256];
-    uint8_t relay_count;
+    char *name;
+    char *database;
+    char *user;
+    char *group;
+    char *mqtt_host;
+    char *include;
     int relays_init;
+    uint8_t relay_count;
+
+    struct
+    {
+        int level;
+        FILE *file;
+    } logging;
+
+    struct
+    {
+        uint16_t discovery;
+        uint16_t mqtt;
+    } ports;
+
     config_relay_t *relay_configs;
 } config_t;
 
-extern config_t global_config;
+extern config_t *global_config;
+
+void
+config_init();
+
+void
+config_free();
+
+void
+config_load_string(char **holder, const char *value);
+
+void
+config_load(config_t *config, const char *cli_config_file);
+
+void
+config_load_file(config_t *config, const char *file_name);
+
+void
+config_load_directory(config_t *config, const char *directory_name);
 
 #endif //CONTROLLER_CONFIG_H
diff --git a/include/constants.h b/include/constants.h
index 0d27ab7..c195022 100644
--- a/include/constants.h
+++ b/include/constants.h
@@ -14,13 +14,6 @@
  */
 #define MAX_NAME_LENGTH 128
 
-/**
- * @brief Maximum number of dbs for the databases for the MDB_env
- *
- * Used when calling mdb_env_set_maxdbs() in database_setup()
- */
-#define MDB_MAXDBS 8
-
 /**
  * @brief How many milli seconds to wait until poll timeout in main loop
  */
@@ -28,4 +21,12 @@
 
 #define PIFACE_GPIO_BASE 200
 
+#define DEFAULT_CONTROLLER_NAME "emgauwa-controller"
+#define DEFAULT_CONFIG_PATH "emgauwa-controller.conf"
+#define DEFAULT_GLOBAL_CONFIG_PATH "/etc/emgauwa/controller.conf"
+#define DEFAULT_DATABASE_PATH "emgauwa-controller.sqlite"
+#define DEFAULT_DISCOVERY_PORT 4421
+#define DEFAULT_MQTT_PORT 1885
+#define DEFAULT_MQTT_HOST "127.0.0.1"
+
 #endif /* CONTROLLER_CONTANTS_H */
diff --git a/include/enums.h b/include/enums.h
index 032622a..8606552 100644
--- a/include/enums.h
+++ b/include/enums.h
@@ -22,10 +22,4 @@ typedef enum
     RELAY_DRIVER_PIFACE,
 } relay_driver_t;
 
-typedef enum
-{
-    RUN_TYPE_START,
-    RUN_TYPE_TEST,
-} run_type_t;
-
 #endif /* CONTROLLER_ENUMS_H */
diff --git a/include/helpers.h b/include/helpers.h
index d1d3a38..314ca05 100644
--- a/include/helpers.h
+++ b/include/helpers.h
@@ -25,9 +25,6 @@ helper_get_port(int sock);
 int
 helper_open_discovery_socket(uint16_t discovery_port);
 
-void
-helper_parse_cli(int argc, const char **argv, config_t *config);
-
 int
 helper_get_weekday(const struct tm *time_struct);
 
diff --git a/src/helpers/parse_cli.c b/src/cli.c
similarity index 52%
rename from src/helpers/parse_cli.c
rename to src/cli.c
index af93eaf..9886dbd 100644
--- a/src/helpers/parse_cli.c
+++ b/src/cli.c
@@ -3,30 +3,31 @@
 #include <stdio.h>
 
 #include <argparse.h>
+
+#include <cli.h>
 #include <config.h>
 #include <logger.h>
 #include <helpers.h>
 #include <version.h>
 
 static const char *const usage[] = {
-    "controller [options] [[--] args]",
     "controller [options]",
     NULL,
 };
 
-#define PERM_READ  (1<<0)
-#define PERM_WRITE (1<<1)
-#define PERM_EXEC  (1<<2)
-
 void
-helper_parse_cli(int argc, const char **argv, config_t *config)
+cli_parse(int argc, const char **argv, cli_t *cli)
 {
+    cli->config_file = NULL;
+    cli->demo_mode = 0;
+
     int version = 0;
     struct argparse_option options[] =
     {
         OPT_HELP(),
         OPT_GROUP("Basic options"),
-        OPT_STRING('c', "config", &config->file, "path to config file", NULL, 0, OPT_NONEG),
+        OPT_STRING('c', "config", &cli->config_file, "path to config file", NULL, 0, OPT_NONEG),
+        OPT_BOOLEAN('d', "demo", &cli->demo_mode, "demo mode", NULL, 0, OPT_NONEG),
         OPT_BOOLEAN('v', "version", &version, "print version", NULL, 0, OPT_NONEG),
         OPT_END(),
     };
@@ -38,33 +39,11 @@ helper_parse_cli(int argc, const char **argv, config_t *config)
             "\nA brief description of what the program does and how it works.",
             "\nAdditional description of the program after the description of the arguments."
     );
-    argc = argparse_parse(&argparse, argc, argv);
+    argparse_parse(&argparse, argc, argv);
 
     if(version)
     {
         printf("%s\n", EMGAUWA_CONTROLLER_VERSION);
         exit(0);
     }
-
-    if(argc == 1)
-    {
-        if(strcmp(argv[0], "start") == 0)
-        {
-            config->run_type = RUN_TYPE_START;
-            return;
-        }
-        if(strcmp(argv[0], "test") == 0)
-        {
-            config->run_type = RUN_TYPE_TEST;
-            return;
-        }
-        LOGGER_CRIT("bad action '%s' given ('start', 'test')\n", argv[0]);
-        exit(1);
-    }
-    else
-    {
-        LOGGER_CRIT("no action given ('start', 'test')\n");
-        exit(1);
-    }
-    return;
 }
diff --git a/src/config.c b/src/config.c
index 2e2fe47..df24d21 100644
--- a/src/config.c
+++ b/src/config.c
@@ -1,191 +1,442 @@
+#include <ctype.h>
+#include <dirent.h>
+#include <errno.h>
 #include <stdlib.h>
 #include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
 
-#include <helpers.h>
 #include <config.h>
+#include <constants.h>
 #include <logger.h>
-#include <confini.h>
 
-#define CONFINI_IS_KEY(SECTION, KEY) \
-    (ini_array_match(SECTION, disp->append_to, '.', disp->format) && \
-     ini_string_match_ii(KEY, disp->data, disp->format))
+config_t *global_config;
 
 static int
-config_load_log_level(IniDispatch *disp, config_t *config)
+config_load_log_level(config_t *config, char *value)
 {
-    if(strcasecmp(disp->value, "debug") == 0)
+    if(strcmp(value, "debug") == 0)
     {
         setlogmask(LOG_UPTO(LOG_DEBUG));
-        config->log_level = LOG_DEBUG;
+        config->logging.level = LOG_DEBUG;
         return 0;
     }
-    if(strcasecmp(disp->value, "info") == 0)
+    if(strcmp(value, "info") == 0)
     {
         setlogmask(LOG_UPTO(LOG_INFO));
-        config->log_level = LOG_INFO;
+        config->logging.level = LOG_INFO;
         return 0;
     }
-    if(strcasecmp(disp->value, "notice") == 0)
+    if(strcmp(value, "notice") == 0)
     {
         setlogmask(LOG_UPTO(LOG_NOTICE));
-        config->log_level = LOG_NOTICE;
+        config->logging.level = LOG_NOTICE;
         return 0;
     }
-    if(strcasecmp(disp->value, "warning") == 0)
+    if(strcmp(value, "warning") == 0)
     {
         setlogmask(LOG_UPTO(LOG_WARNING));
-        config->log_level = LOG_WARNING;
+        config->logging.level = LOG_WARNING;
         return 0;
     }
-    if(strcasecmp(disp->value, "err") == 0)
+    if(strcmp(value, "err") == 0)
     {
         setlogmask(LOG_UPTO(LOG_ERR));
-        config->log_level = LOG_ERR;
+        config->logging.level = LOG_ERR;
         return 0;
     }
-    if(strcasecmp(disp->value, "crit") == 0)
+    if(strcmp(value, "crit") == 0)
     {
         setlogmask(LOG_UPTO(LOG_CRIT));
-        config->log_level = LOG_CRIT;
+        config->logging.level = LOG_CRIT;
         return 0;
     }
-    if(strcasecmp(disp->value, "emerg") == 0)
+    if(strcmp(value, "emerg") == 0)
     {
         setlogmask(LOG_UPTO(LOG_EMERG));
-        config->log_level = LOG_EMERG;
+        config->logging.level = LOG_EMERG;
         return 0;
     }
-    LOGGER_WARNING("invalid log-level '%s'\n", disp->value);
+    LOGGER_WARNING("invalid log-level '%s'\n", value);
+
     return 0;
 }
 
 static int
-config_load_log_file(IniDispatch *disp, config_t *config)
+config_load_log_file(config_t *config, char *value)
 {
-    if(strcasecmp(disp->value, "stdout") == 0)
+    if(strcmp(value, "stdout") == 0)
     {
-        config->log_file = stdout;
+        config->logging.file = stdout;
         return 0;
     }
-    if(strcasecmp(disp->value, "stderr") == 0)
+    if(strcmp(value, "stderr") == 0)
     {
-        config->log_file = stderr;
+        config->logging.file = stderr;
         return 0;
     }
-    config->log_file = fopen(disp->value, "a+");
+    config->logging.file = fopen(value, "a+");
     return 0;
 }
 
-int
-config_load(IniDispatch *disp, void *config_void)
+static void
+config_init_relay_configs(config_t *config)
 {
-    config_t *config = (config_t*)config_void;
-    char relay_section_name[10]; // "relay-255\0" is longest name 
-
-    if(disp->type == INI_KEY)
+    config->relay_configs = malloc(sizeof(config_relay_t) * config->relay_count);
+    for(uint8_t i = 0; i < config->relay_count; ++i)
     {
-        if(CONFINI_IS_KEY("controller", "name"))
+        config->relay_configs[i].driver = RELAY_DRIVER_NONE;
+        config->relay_configs[i].inverted = 0;
+        config->relay_configs[i].init = -1;
+        config->relay_configs[i].pin = 0;
+        config->relay_configs[i].pulse_duration = 0;
+    }
+}
+
+static void
+config_load_section_controller(config_t *config, toml_table_t* controller)
+{
+    toml_datum_t config_entry;
+
+    config_entry = toml_string_in(controller, "name");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->name, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_string_in(controller, "database");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->database, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_string_in(controller, "user");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->user, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_string_in(controller, "group");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->group, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_string_in(controller, "mqtt-host");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->mqtt_host, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_int_in(controller, "relays-init");
+    if(config_entry.ok)
+    {
+        config->relays_init = config_entry.u.i;
+    }
+
+
+    config_entry = toml_string_in(controller, "include");
+    if(config_entry.ok)
+    {
+        config_load_string(&config->include, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+}
+
+static void
+config_load_section_logging(config_t *config, toml_table_t* logging)
+{
+    toml_datum_t config_entry;
+
+    config_entry = toml_string_in(logging, "level");
+    if(config_entry.ok)
+    {
+        config_load_log_level(config, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+
+    config_entry = toml_string_in(logging, "file");
+    if(config_entry.ok)
+    {
+        config_load_log_file(config, config_entry.u.s);
+        free(config_entry.u.s);
+    }
+} 
+
+static void
+config_load_section_ports(config_t *config, toml_table_t* ports)
+{
+    toml_datum_t config_entry;
+
+    config_entry = toml_int_in(ports, "discovery");
+    if(config_entry.ok)
+    {
+        config->ports.discovery = config_entry.u.i;
+    }
+
+    config_entry = toml_int_in(ports, "mqtt");
+    if(config_entry.ok)
+    {
+        config->ports.mqtt = config_entry.u.i;
+    }
+}
+
+static void
+config_load_section_relay(config_t *config, toml_table_t* relay, int relay_num)
+{
+    toml_datum_t config_entry;
+
+    config_entry = toml_int_in(relay, "pin");
+    if(config_entry.ok)
+    {
+        config->relay_configs[relay_num].pin = config_entry.u.i;
+    }
+
+    config_entry = toml_int_in(relay, "inverted");
+    if(config_entry.ok)
+    {
+        config->relay_configs[relay_num].inverted = config_entry.u.i;
+    }
+
+    config_entry = toml_int_in(relay, "init");
+    if(config_entry.ok)
+    {
+        config->relay_configs[relay_num].init = config_entry.u.i;
+    }
+
+    config_entry = toml_int_in(relay, "pulse-duration");
+    if(config_entry.ok)
+    {
+        config->relay_configs[relay_num].pulse_duration = config_entry.u.i;
+    }
+
+    config_entry = toml_string_in(relay, "driver");
+    if(config_entry.ok)
+    {
+        for(int i = 0; config_entry.u.s[i] != '\0'; ++i)
         {
-            strncpy(config->name, disp->value, MAX_NAME_LENGTH);
-            config->name[MAX_NAME_LENGTH] = '\0';
-            return 0;
+            config_entry.u.s[i] = tolower(config_entry.u.s[i]);
         }
-        if(CONFINI_IS_KEY("controller", "database"))
+
+        if(strcmp(config_entry.u.s, "gpio") == 0)
         {
-            strcpy(config->database, disp->value);
-            return 0;
+            config->relay_configs[relay_num].driver = RELAY_DRIVER_GPIO;
         }
-        if(CONFINI_IS_KEY("controller", "user"))
+        else if(strcmp(config_entry.u.s, "piface") == 0)
         {
-            strcpy(config->user, disp->value);
-            return 0;
+            config->relay_configs[relay_num].driver = RELAY_DRIVER_PIFACE;
         }
-        if(CONFINI_IS_KEY("controller", "group"))
+        else
         {
-            strcpy(config->group, disp->value);
-            return 0;
-        }
-        if(CONFINI_IS_KEY("controller", "log-level"))
-        {
-            return config_load_log_level(disp, config);
-        }
-        if(CONFINI_IS_KEY("controller", "log-file"))
-        {
-            return config_load_log_file(disp, config);
-        }
-        if(CONFINI_IS_KEY("controller", "discovery-port"))
-        {
-            config->discovery_port = atoi(disp->value);
-            return 0;
-        }
-        if(CONFINI_IS_KEY("controller", "mqtt-port"))
-        {
-            config->mqtt_port = atoi(disp->value);
-            return 0;
-        }
-        if(CONFINI_IS_KEY("controller", "mqtt-host"))
-        {
-            strcpy(config->mqtt_host, disp->value);
-            return 0;
-        }
-        if(CONFINI_IS_KEY("controller", "relays-init"))
-        {
-            config->relays_init = atoi(disp->value);
-            return 0;
-        }
-        if(CONFINI_IS_KEY("controller", "relay-count"))
-        {
-            config->relay_count = atoi(disp->value);
-            config->relay_configs = malloc(sizeof(config_relay_t) * config->relay_count);
-            for(uint8_t i = 0; i < config->relay_count; ++i)
-            {
-                config->relay_configs[i].driver = RELAY_DRIVER_NONE;
-                config->relay_configs[i].inverted = 0;
-                config->relay_configs[i].init = -1;
-                config->relay_configs[i].pin = 0;
-                config->relay_configs[i].pulse_duration = 0;
-            }
-            return 0;
-        }
-        for(uint8_t i = 0; i < config->relay_count; ++i)
-        {
-            sprintf(relay_section_name, "relay-%u", i);
-            if(CONFINI_IS_KEY(relay_section_name, "pin"))
-            {
-                config->relay_configs[i].pin = atoi(disp->value);
-                return 0;
-            }
-            if(CONFINI_IS_KEY(relay_section_name, "inverted"))
-            {
-                config->relay_configs[i].inverted = atoi(disp->value);
-                return 0;
-            }
-            if(CONFINI_IS_KEY(relay_section_name, "init"))
-            {
-                config->relay_configs[i].init = atoi(disp->value);
-                return 0;
-            }
-            if(CONFINI_IS_KEY(relay_section_name, "pulse-duration"))
-            {
-                config->relay_configs[i].pulse_duration = atoi(disp->value);
-                return 0;
-            }
-            if(CONFINI_IS_KEY(relay_section_name, "driver"))
-            {
-                if(strcasecmp(disp->value, "gpio") == 0)
-                {
-                    config->relay_configs[i].driver = RELAY_DRIVER_GPIO;
-                    return 0;
-                }
-                if(strcasecmp(disp->value, "piface") == 0)
-                {
-                    config->relay_configs[i].driver = RELAY_DRIVER_PIFACE;
-                    return 0;
-                }
-                LOGGER_WARNING("invalid driver '%s' in section '%s'\n", disp->value, relay_section_name);
-                return 0;
-            }
+            LOGGER_WARNING("invalid driver '%s' in section for relay %d\n", config_entry.u.s, relay_num);
         }
+        free(config_entry.u.s);
+    }
+}
+
+void
+config_init()
+{
+    global_config = calloc(1, sizeof(config_t));
+
+    config_load_string(&global_config->name, DEFAULT_CONTROLLER_NAME);
+
+    config_load_string(&global_config->database, DEFAULT_DATABASE_PATH);
+
+    config_load_string(&global_config->mqtt_host, DEFAULT_MQTT_HOST);
+
+
+    global_config->user = NULL;
+    global_config->group = NULL;
+
+    global_config->include = NULL;
+
+    global_config->relays_init = 0;
+    global_config->relay_count = 0;
+
+    config_load_string(&global_config->mqtt_host, "127.0.0.1");
+
+    global_config->ports.mqtt = DEFAULT_MQTT_PORT;
+    global_config->ports.discovery = DEFAULT_DISCOVERY_PORT;
+
+    global_config->logging.level = LOG_DEBUG;
+    global_config->logging.file = stdout;
+}
+
+void
+config_free()
+{
+    free(global_config->name);
+    free(global_config->database);
+    free(global_config->user);
+    free(global_config->group);
+    free(global_config->mqtt_host);
+    free(global_config->include);
+
+    free(global_config);
+}
+
+void
+config_load_string(char **holder, const char *value)
+{
+    if(*holder)
+    {
+        free(*holder);
+    }
+    size_t value_len = strlen(value);
+
+    char *new_holder = malloc(sizeof(char) * (value_len + 1));
+    strcpy(new_holder, value);
+    new_holder[value_len] = '\0';
+
+    *holder = new_holder;
+}
+
+static int
+config_try_file(const char *path)
+{
+    if(access(path, F_OK) != 0)
+    {
+        return 1; 
+    }
+    if(access(path, R_OK) != 0)
+    {
+        return 1; 
     }
     return 0;
 }
+
+void
+config_load(config_t *config, const char *cli_config_file)
+{
+    if(cli_config_file)
+    {
+        if(config_try_file(cli_config_file) == 0)
+        {
+            config_load_file(config, cli_config_file);
+            return;
+        }
+        LOGGER_CRIT("unable to open the passed config file '%s'\n", cli_config_file);
+        exit(1);
+    }
+    if(config_try_file(DEFAULT_CONFIG_PATH) == 0)
+    {
+        config_load_file(config, DEFAULT_CONFIG_PATH);
+        return;
+    }
+    if(config_try_file(DEFAULT_GLOBAL_CONFIG_PATH) == 0)
+    {
+        config_load_file(config, DEFAULT_GLOBAL_CONFIG_PATH);
+        return;
+    }
+}
+
+void
+config_load_file(config_t *config, const char *file_name)
+{
+    FILE *fp;
+    toml_table_t* config_toml;
+    char errbuf[256];
+
+    /* Open the file and parse content */
+    fp = fopen(file_name, "r");
+    if(fp == NULL) {
+        LOGGER_CRIT("unable to open config file '%s'\n", file_name);
+        exit(1);
+    }
+    config_toml = toml_parse_file(fp, errbuf, sizeof(errbuf));
+    fclose(fp);
+    if(config_toml == NULL) {
+        LOGGER_CRIT("unable to parse config file '%s': %s\n", file_name, errbuf);
+        exit(1);
+    }
+
+    toml_table_t* controller = toml_table_in(config_toml, "controller");
+    if(controller)
+    {
+        config_load_section_controller(config, controller);
+    }
+
+    toml_table_t* logging = toml_table_in(config_toml, "logging");
+    if(logging)
+    {
+        config_load_section_logging(config, logging);
+    }
+
+    toml_table_t* ports = toml_table_in(config_toml, "ports");
+    if(ports)
+    {
+        config_load_section_ports(config, ports);
+    }
+
+    toml_array_t* relays = toml_array_in(config_toml, "relays");
+    if(relays)
+    {
+        config->relay_count = toml_array_nelem(relays);
+        config_init_relay_configs(config);
+
+        for(int i = 0; i < config->relay_count; ++i)
+        {
+            config_load_section_relay(config, toml_table_at(relays, i), i);
+        }
+    }
+
+    toml_free(config_toml);
+
+    LOGGER_DEBUG("Loaded config from %s\n", file_name);
+}
+
+void
+config_load_directory(config_t *config, const char *directory_name)
+{
+    struct dirent *directory_entry;
+    DIR *directory;
+
+    (void)config;
+
+    directory = opendir(directory_name);
+    if(directory == NULL)
+    {
+        LOGGER_CRIT("cannot open directory '%s': %s\n", directory_name, strerror(errno));
+        exit(1);
+    }
+
+    while((directory_entry = readdir(directory)) != NULL)
+    {
+        struct stat sb;
+        const char *entry_name = directory_entry->d_name;
+
+        size_t copied = 0;
+
+        // Add 2 for '/' and '\0'.
+        size_t path_len = strlen(directory_name) + strlen(entry_name) + 1;
+        char *path = malloc(sizeof(char) * (path_len + 1));
+        path[0] = '\0';
+
+        strncat(path + copied, directory_name, path_len - copied);
+        copied = strlen(path);
+        if(path[copied - 1] != '/')
+        {
+            strncat(path + copied, "/", path_len - copied);
+            copied = strlen(path);
+        }
+        strncat(path + copied, entry_name, path_len - copied);
+
+        if(stat(path, &sb))
+        {
+            LOGGER_WARNING("failed to get info for '%s': %s\n", path, strerror(errno));
+        }
+        if(S_ISREG(sb.st_mode))
+        {
+            config_load_file(config, path);
+        }
+        free(path);
+    }
+
+    closedir(directory);
+}
diff --git a/src/connections.c b/src/connections.c
index f32c062..45cb4af 100644
--- a/src/connections.c
+++ b/src/connections.c
@@ -11,7 +11,7 @@ struct mg_connection*
 connection_discovery_bind(struct mg_mgr *mgr)
 {
     char address[32];
-    sprintf(address, "udp://0.0.0.0:%u", global_config.discovery_port);
+    sprintf(address, "udp://0.0.0.0:%u", global_config->ports.discovery);
     struct mg_connection *c = mg_bind(mgr, address, handler_discovery);
     return c;
 }
@@ -29,7 +29,7 @@ struct mg_connection*
 connection_mqtt_connect(struct mg_mgr *mgr)
 {
     char address[512];
-    sprintf(address, "tcp://%s:%u", global_config.mqtt_host, global_config.mqtt_port);
+    sprintf(address, "tcp://%s:%u", global_config->mqtt_host, global_config->ports.mqtt);
     struct mg_connection *c = mg_connect(mgr, address, handler_mqtt);
     return c;
 }
diff --git a/src/database.c b/src/database.c
index 1b6b36e..28392e5 100644
--- a/src/database.c
+++ b/src/database.c
@@ -14,7 +14,7 @@ static int in_transaction;
 void
 database_init()
 {
-    int rc = sqlite3_open(global_config.database, &global_database);
+    int rc = sqlite3_open(global_config->database, &global_database);
 
     if(rc)
     {
diff --git a/src/handlers/command.c b/src/handlers/command.c
index d2c04c9..9aa3984 100644
--- a/src/handlers/command.c
+++ b/src/handlers/command.c
@@ -53,9 +53,9 @@ handler_command_relay_pulse(mpack_node_t map)
 {
     uint8_t relay_num = mpack_node_u8(mpack_node_map_uint(map, COMMAND_MAPPING_RELAY_NUM));
 
-    if(relay_num > global_config.relay_count)
+    if(relay_num > global_config->relay_count)
     {
-        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config.relay_count);
+        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config->relay_count);
         return;
     }
 
@@ -65,7 +65,7 @@ handler_command_relay_pulse(mpack_node_t map)
     int duration = mpack_node_u8(mpack_node_map_uint(map, COMMAND_MAPPING_PULSE_DURATION));
     if(duration == 0)
     {
-        duration = global_config.relay_configs[relay_num].pulse_duration;
+        duration = global_config->relay_configs[relay_num].pulse_duration;
     }
     target_relay->pulse_timer = duration;
     LOGGER_DEBUG("pulsing relay %d for %ds\n", relay_num, duration);
@@ -87,9 +87,9 @@ handler_command_relay_name_set(mpack_node_t map)
     uint8_t relay_num = mpack_node_u8(mpack_node_map_uint(map, COMMAND_MAPPING_RELAY_NUM));
     const char *relay_name = mpack_node_str(mpack_node_map_uint(map, COMMAND_MAPPING_NAME));
 
-    if(relay_num > global_config.relay_count)
+    if(relay_num > global_config->relay_count)
     {
-        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config.relay_count);
+        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config->relay_count);
         return;
     }
     relay_set_name(global_controller->relays[relay_num], relay_name);
@@ -120,7 +120,7 @@ handler_command_schedule_update(mpack_node_t map)
 
     int *relay_ids = junction_relay_schedule_get_relay_ids_with_schedule(new_schedule->id);
 
-    for(int i = 0; i < global_config.relay_count; ++i)
+    for(int i = 0; i < global_config->relay_count; ++i)
     {
         for(int j = 0; relay_ids[j] != 0; ++j)
         {
@@ -140,9 +140,9 @@ handler_command_relay_schedules_set(mpack_node_t map)
 {
     uint8_t relay_num = mpack_node_u8(mpack_node_map_uint(map, COMMAND_MAPPING_RELAY_NUM));
 
-    if(relay_num > global_config.relay_count)
+    if(relay_num > global_config->relay_count)
     {
-        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config.relay_count);
+        LOGGER_WARNING("relay %d is not available (relay count: %d\n", relay_num, global_config->relay_count);
         return;
     }
 
diff --git a/src/handlers/discovery.c b/src/handlers/discovery.c
index db95e08..2cbbbbb 100644
--- a/src/handlers/discovery.c
+++ b/src/handlers/discovery.c
@@ -53,7 +53,7 @@ handler_discovery(struct mg_connection *c, int ev, void *ev_data)
         mpack_write_uint(&writer, DISCOVERY_MAPPING_COMMAND_PORT);
         mpack_write_u16(&writer, global_controller->command_port);
         mpack_write_uint(&writer, DISCOVERY_MAPPING_RELAY_COUNT);
-        mpack_write_u8(&writer, global_config.relay_count);
+        mpack_write_u8(&writer, global_config->relay_count);
         mpack_write_uint(&writer, DISCOVERY_MAPPING_NAME);
         mpack_write_cstr(&writer, global_controller->name);
         mpack_finish_map(&writer);
diff --git a/src/handlers/loop.c b/src/handlers/loop.c
index 2076952..51a0776 100644
--- a/src/handlers/loop.c
+++ b/src/handlers/loop.c
@@ -28,13 +28,13 @@ handler_loop(struct mg_connection *c_mqtt)
     localtime_r(&timestamp, &time_last);
     timestamp = time(NULL);
     localtime_r(&timestamp, &time_now);
-    for(uint_fast8_t i = 0; i < global_config.relay_count; ++i)
+    for(uint_fast8_t i = 0; i < global_config->relay_count; ++i)
     {
         relay_t *relay = global_controller->relays[i];
         int is_on = 0;
         
         int is_on_schedule = relay_is_on_schedule(relay, &time_now);
-        int pulse_relay = global_config.relay_configs[i].pulse_duration;
+        int pulse_relay = global_config->relay_configs[i].pulse_duration;
 
         if(is_on_schedule)
         {
@@ -73,17 +73,17 @@ handler_loop(struct mg_connection *c_mqtt)
         }
         relay->is_on = is_on;
 
-        if(global_config.relay_configs[i].inverted)
+        if(global_config->relay_configs[i].inverted)
         {
             is_on = !is_on;
         }
-        switch(global_config.relay_configs[i].driver)
+        switch(global_config->relay_configs[i].driver)
         {
             case RELAY_DRIVER_GPIO:
-                driver_gpio_set(global_config.relay_configs[i].pin, is_on);
+                driver_gpio_set(global_config->relay_configs[i].pin, is_on);
                 break;
             case RELAY_DRIVER_PIFACE:
-                driver_piface_set(global_config.relay_configs[i].pin, is_on);
+                driver_piface_set(global_config->relay_configs[i].pin, is_on);
                 break;
             default:
                 LOGGER_WARNING("relay %d is not using a driver\n", i);
diff --git a/src/helpers/drop_privileges.c b/src/helpers/drop_privileges.c
index e2dde8a..df0ba7f 100644
--- a/src/helpers/drop_privileges.c
+++ b/src/helpers/drop_privileges.c
@@ -59,8 +59,8 @@ get_gid_for_group(char *group)
 int
 helper_drop_privileges()
 {
-    uid_t uid = get_uid_for_user(global_config.user);
-    gid_t gid = get_gid_for_group(global_config.group);
+    uid_t uid = get_uid_for_user(global_config->user);
+    gid_t gid = get_gid_for_group(global_config->group);
 
     LOGGER_DEBUG("drop privileges to %lu:%lu\n", uid, gid);
 
diff --git a/src/logger.c b/src/logger.c
index 2869668..2561462 100644
--- a/src/logger.c
+++ b/src/logger.c
@@ -17,10 +17,11 @@ const char *COLOR_EMERG = COLOR_MAGENTA;
 void
 logger_log(int level, const char *filename, int line, const char *func, const char *msg, ...)
 {
-    if(global_config.log_level < level)
+    if(global_config->logging.level < level)
     {
         return;
     }
+    va_list args;
     const char *level_str;
     const char *color;
 
@@ -62,24 +63,28 @@ logger_log(int level, const char *filename, int line, const char *func, const ch
     time_t rawtime;
     time(&rawtime);
     strftime(timestamp_str, 32, "%Y-%m-%d %H:%M:%S", localtime(&rawtime));
+    size_t timestamp_len = strlen(timestamp_str);
 
-    char *buffer = malloc(sizeof(char) * (128 + strlen(msg)));
-    sprintf(buffer, "%s[%5s] %s:%d:%s " COLOR_NONE "%s", color, level_str, filename, line, func, msg);
+    char *buffer = malloc(sizeof(char) * (128 + strlen(msg) + timestamp_len));
+    sprintf(buffer, "%s %s[%5s] %s:%d:%s " COLOR_NONE "%s", timestamp_str, color, level_str, filename, line, func, msg);
 
-    //fprintf(stream, "%s %s:%d:%s " COLOR_NONE, timestamp_str, filename, line, func);
-
-    va_list args;
+    // start arg va_list and find log_len
     va_start(args, msg);
-    vsyslog(level, buffer, args);
+    size_t log_len = vsnprintf(NULL, 0, buffer, args); // NOLINT(clang-analyzer-valist.Uninitialized): clang-tidy bug
     va_end(args);
 
-    char *buffer_timed = malloc(sizeof(char) * (strlen(timestamp_str) + strlen(buffer) + 2));
-    sprintf(buffer_timed, "%s %s", timestamp_str, buffer);
+    char *log_line = malloc(sizeof(char) * (log_len + 1));
+
+    // start arg va_list again and write log_line
     va_start(args, msg);
-    vfprintf(global_config.log_file, buffer_timed, args);
-    fflush(global_config.log_file);
+    vsprintf(log_line, buffer, args); // NOLINT(clang-analyzer-valist.Uninitialized): clang-tidy bug
     va_end(args);
 
+    syslog(level, "%s", log_line + timestamp_len + 1);
+
+    fprintf(global_config->logging.file, "%s", log_line);
+    fflush(global_config->logging.file);
+
     free(buffer);
-    free(buffer_timed);
+    free(log_line);
 }
diff --git a/src/main.c b/src/main.c
index 17149e2..d4906b5 100644
--- a/src/main.c
+++ b/src/main.c
@@ -6,6 +6,7 @@
 #include <signal.h>
 #include <syslog.h>
 
+#include <cli.h>
 #include <logger.h>
 #include <mongoose.h>
 #include <models/controller.h>
@@ -21,9 +22,6 @@
 #include <wiringPi.h>
 #include <piFace.h>
 #include <wiring_debug.h>
-#include <confini.h>
-
-config_t global_config;
 
 static struct mg_mgr mgr;
 
@@ -43,7 +41,7 @@ terminate(int signum)
     controller_free(global_controller);
 
     LOGGER_DEBUG("freeing relay configs config\n");
-    free(global_config.relay_configs);
+    free(global_config->relay_configs);
 
     exit(signum);
 }
@@ -66,45 +64,28 @@ main(int argc, const char** argv)
     signal(SIGABRT, terminate);
     signal(SIGTERM, terminate);
 
+    openlog("emgauwa-controller", 0, LOG_USER);
     setlogmask(LOG_UPTO(LOG_INFO));
 
 
     /******************** LOAD CONFIG ********************/
 
-    global_config.file = "controller.ini";
-    global_config.discovery_port = 4421;
-    global_config.mqtt_port = 1885;
+    config_init();
 
-    global_config.relay_count = 0;
-    global_config.relays_init = -1;
+    cli_t cli;
+    cli_parse(argc, argv, &cli);
 
-    global_config.log_level = LOG_INFO;
-    global_config.log_file = stdout;
+    config_load(global_config, cli.config_file);
 
-    strcpy(global_config.user, "");
-    strcpy(global_config.group, "");
-
-    helper_parse_cli(argc, argv, &global_config);
-
-    FILE * const ini_file = fopen(global_config.file, "rb");
-    if(ini_file == NULL)
+    if(global_config->logging.file == NULL)
     {
-        LOGGER_CRIT("config file '%s' was not found\n", global_config.file);
-        exit(1);
+        global_config->logging.file = stdout;
     }
-    if(load_ini_file( ini_file, INI_DEFAULT_FORMAT, NULL, config_load, &global_config))
-    {
-        LOGGER_CRIT("unable to parse ini file\n");
-        exit(1);
-    }
- 
-    fclose(ini_file);
 
-    if(global_config.log_file == NULL)
+    if(global_config->include)
     {
-        global_config.log_file = stdout;
+        config_load_directory(global_config, global_config->include);
     }
-    openlog("emgauwa-controller", 0, LOG_USER);
 
     if(sizeof(time_t) < 8)
     {
@@ -146,38 +127,38 @@ main(int argc, const char** argv)
 
     int piface_setup = 0;
 
-    for(uint_fast8_t i = 0; i < global_config.relay_count; ++i)
+    for(uint_fast8_t i = 0; i < global_config->relay_count; ++i)
     {
-        int relay_default = global_config.relay_configs[i].init;
+        int relay_default = global_config->relay_configs[i].init;
         if(relay_default == -1)
         {
-            relay_default = global_config.relays_init;
+            relay_default = global_config->relays_init;
         }
         if(relay_default == -1)
         {
-            relay_default = global_config.relay_configs[i].inverted;
+            relay_default = global_config->relay_configs[i].inverted;
         }
 
-        if(global_config.relay_configs[i].driver == RELAY_DRIVER_GPIO)
+        if(global_config->relay_configs[i].driver == RELAY_DRIVER_GPIO)
         {
-            pinMode(global_config.relay_configs[i].pin, OUTPUT);
-            driver_gpio_set(global_config.relay_configs[i].pin, relay_default);
+            pinMode(global_config->relay_configs[i].pin, OUTPUT);
+            driver_gpio_set(global_config->relay_configs[i].pin, relay_default);
         }
-        if(global_config.relay_configs[i].driver == RELAY_DRIVER_PIFACE)
+        if(global_config->relay_configs[i].driver == RELAY_DRIVER_PIFACE)
         {
             if(!piface_setup)
             {
                 piFaceSetup(PIFACE_GPIO_BASE);
                 piface_setup = 1;
             }
-            driver_piface_set(global_config.relay_configs[i].pin, relay_default);
+            driver_piface_set(global_config->relay_configs[i].pin, relay_default);
         }
     }
 
 
     /******************** CHECK FOR TESTING RUN ********************/
 
-    if(global_config.run_type == RUN_TYPE_TEST)
+    if(cli.demo_mode)
     {
         runner_test(global_controller);
         terminate(0);
diff --git a/src/models/controller.c b/src/models/controller.c
index 729404b..fd28506 100644
--- a/src/models/controller.c
+++ b/src/models/controller.c
@@ -56,9 +56,9 @@ controller_db_select_mapper(sqlite3_stmt *stmt)
                 break;
         }
     }
-    new_controller->relays = malloc(sizeof(relay_t) * global_config.relay_count);
+    new_controller->relays = malloc(sizeof(relay_t) * global_config->relay_count);
     uint8_t i;
-    for(i = 0; i < global_config.relay_count; ++i)
+    for(i = 0; i < global_config->relay_count; ++i)
     {
         new_controller->relays[i] = relay_load(i);
         if(!new_controller->relays[i])
@@ -198,14 +198,14 @@ controller_create(void)
     new_controller->id = 0;
     uuid_generate(new_controller->uid);
 
-    strncpy(new_controller->name, global_config.name, MAX_NAME_LENGTH);
+    strncpy(new_controller->name, global_config->name, MAX_NAME_LENGTH);
     new_controller->name[MAX_NAME_LENGTH] = '\0';
 
     new_controller->command_port = 0;
 
-    new_controller->relays = malloc(sizeof(relay_t) * global_config.relay_count);
+    new_controller->relays = malloc(sizeof(relay_t) * global_config->relay_count);
     uint8_t i;
-    for(i = 0; i < global_config.relay_count; ++i)
+    for(i = 0; i < global_config->relay_count; ++i)
     {
         new_controller->relays[i] = relay_load(i);
         if(!new_controller->relays[i])
@@ -228,7 +228,7 @@ controller_set_name(controller_t *controller, const char *name)
 void
 controller_free(controller_t *controller)
 {
-    for(int i = 0; i < global_config.relay_count; ++i)
+    for(int i = 0; i < global_config->relay_count; ++i)
     {
         relay_free(controller->relays[i]);
     }
@@ -249,7 +249,7 @@ controller_debug(controller_t *controller)
     LOGGER_DEBUG("(1/3) %s @ %p\n", uuid_str, (void*)controller);
     LOGGER_DEBUG("(2/3) name: %s\n", controller->name);
     LOGGER_DEBUG("(3/3) relays @ %p:\n", (void*)controller->relays);
-    for(int i = 0; i < global_config.relay_count; ++i)
+    for(int i = 0; i < global_config->relay_count; ++i)
     {
         relay_debug(controller->relays[i]);
     }
diff --git a/src/models/junction_relay_schedule.c b/src/models/junction_relay_schedule.c
index 8d623c0..e1aa9ba 100644
--- a/src/models/junction_relay_schedule.c
+++ b/src/models/junction_relay_schedule.c
@@ -24,12 +24,12 @@ junction_relay_schedule_insert(uint8_t weekday, int relay_id, int schedule_id)
     if (rc != SQLITE_DONE)
     {
         LOGGER_ERR("error inserting data: %s\n", sqlite3_errmsg(global_database));
-        return false;
+        return 0;
     }
 
     sqlite3_finalize(stmt);
 
-    return true;
+    return 1;
 }
 
 int
@@ -67,12 +67,12 @@ junction_relay_schedule_insert_weekdays(int relay_id, int *schedule_ids)
     if (rc != SQLITE_DONE)
     {
         LOGGER_ERR("error inserting data: %s", sqlite3_errmsg(global_database));
-        return false;
+        return 0;
     }
 
     sqlite3_finalize(stmt);
 
-    return true;
+    return 1;
 }
 
 int
diff --git a/src/models/relay.c b/src/models/relay.c
index 38c3aa0..8e0e37a 100644
--- a/src/models/relay.c
+++ b/src/models/relay.c
@@ -73,7 +73,7 @@ relay_db_select(sqlite3_stmt *stmt)
 
     int row = 0;
 
-    while(true)
+    for(;;)
     {
         int s;
 
diff --git a/src/runners/test.c b/src/runners/test.c
index 2eec714..b6e948b 100644
--- a/src/runners/test.c
+++ b/src/runners/test.c
@@ -9,22 +9,22 @@ void
 runner_test()
 {
     // from x down to 0 to turn all relays off in the end
-    for(uint_fast8_t i = 0; i < global_config.relay_count; ++i)
+    for(uint_fast8_t i = 0; i < global_config->relay_count; ++i)
     {
         for(int test_run = 2; test_run >= 0; --test_run)
         {
             int is_active = test_run % 2;
-            if(global_config.relay_configs[i].inverted)
+            if(global_config->relay_configs[i].inverted)
             {
                 is_active = !is_active;
             }
-            switch(global_config.relay_configs[i].driver)
+            switch(global_config->relay_configs[i].driver)
             {
                 case RELAY_DRIVER_GPIO:
-                    driver_gpio_set(global_config.relay_configs[i].pin, is_active);
+                    driver_gpio_set(global_config->relay_configs[i].pin, is_active);
                     break;
                 case RELAY_DRIVER_PIFACE:
-                    driver_piface_set(global_config.relay_configs[i].pin, is_active);
+                    driver_piface_set(global_config->relay_configs[i].pin, is_active);
                     break;
                 default:
                     LOGGER_WARNING("relay %d is not using a driver\n", i);
diff --git a/vendor/confini.c b/vendor/confini.c
deleted file mode 100644
index 25353a7..0000000
--- a/vendor/confini.c
+++ /dev/null
@@ -1,5016 +0,0 @@
-/*  -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-  */
-/*  Please make sure that the TAB width in your editor is set to 4 spaces  */
-
-/**
-
-	@file		confini.c
-	@brief		libconfini functions
-	@author		Stefano Gioffr&eacute;
-	@copyright	GNU General Public License, version 3 or any later version
-	@version	1.14.0
-	@date		2016-2020
-	@see		https://madmurphy.github.io/libconfini
-
-**/
-
-
-           /*/|
-          (_|_)      _ _ _                      __ _       _
-                    | (_) |__   ___ ___  _ __  / _(_)_ __ (_)
-                    | | | '_ \ / __/ _ \| '_ \| |_| | '_ \| |
-                    | | | |_) | (_| (_) | | | |  _| | | | | |
-                    |_|_|_.__/ \___\___/|_| |_|_| |_|_| |_|_|      _ _
-                                                                  ( | )
-                                                                  |/*/
-
-
-
-/**
-
-
-	@def		INIFORMAT_TABLE_AS(_____)
-
-	Content of the table:
-
-	- Bits 1-19: INI syntax
-	- Bits 20-22: INI semantics
-	- Bits 23-24: Human syntax (disabled entries)
-
-
-
-	@typedef	int (* IniStatsHandler) (
-					IniStatistics * statistics,
-					void * user_data
-				)
-
-	@param		statistics
-					A pointer to the #IniStatistics to handle
-	@param		user_data
-					The custom argument previously passed to the caller function
-
-
-
-	@typedef	int (* IniDispHandler) (
-					IniDispatch *dispatch,
-					void * user_data
-				)
-
-	@param		dispatch
-					A pointer to the #IniDispatch to handle
-	@param		user_data
-					The custom argument previously passed to the caller function
-
-
-
-	@typedef	int (* IniStrHandler) (
-					char * ini_string,
-					size_t string_length,
-					size_t string_num,
-					IniFormat format,
-					void * user_data
-				)
-
-	@param		ini_string
-					The INI string to handle
-	@param		string_length
-					The length of the INI string in bytes
-	@param		string_num
-					The unique number that identifies @p ini_string within a
-					sequence of INI strings; it equals zero if @p ini_string is the
-					first or the only member of the sequence
-	@param		format
-					The format of the INI file from which @p ini_string has been extracted
-	@param		user_data
-					The custom argument previously passed to the caller function
-
-
-
-	@typedef	int (* IniSubstrHandler) (
-					const char * ini_string,
-					size_t fragm_offset,
-					size_t fragm_length,
-					size_t fragm_num,
-					IniFormat format,
-					void * user_data
-				)
-
-	@param		ini_string
-					The INI string containing the fragment to handle
-	@param		fragm_offset
-					The offset of the selected fragment in bytes
-	@param		fragm_length
-					The length of the selected fragment in bytes
-	@param		fragm_num
-					The unique number that identifies the selected fragment within a
-					sequence of fragments of @p ini_string; it equals zero if the
-					fragment is the first or the only member of the sequence
-	@param		format
-					The format of the INI file from which @p ini_string has been
-					extracted
-	@param		user_data
-					The custom argument previously passed to the caller function
-
-
-
-	@struct		IniFormat
-
-	@property	IniFormat::delimiter_symbol
-					The key-value delimiter character (ASCII only allowed); if set
-					to `\0`, any space is delimiter
-					(`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`); if, within the format
-					given, `IniFormat::delimiter_symbol` matches a metacharacter
-					(`'\\'`, `'\''`, `'\"'`), its role as metacharacter will have
-					higher priority than its role as delimiter symbol (i.e., the
-					format will have no key-value delimiter); you may use the
-					#IniDelimiters `enum` for this.
-	@property	IniFormat::case_sensitive
-					If set to `true`, string comparisons will be always
-					case-sensitive.
-	@property	IniFormat::semicolon_marker
-					The rule of the semicolon character (use `enum`
-					#IniCommentMarker for this).
-	@property	IniFormat::hash_marker
-					The rule of the hash character (use `enum` #IniCommentMarker for
-					this).
-	@property	IniFormat::section_paths
-					Defines whether and how the format supports sections (use `enum`
-					#IniSectionPaths for this).
-	@property	IniFormat::multiline_nodes
-					Defines which class of entries are allowed to be multi-line (use
-					`enum` #IniMultiline for this).
-	@property	IniFormat::no_spaces_in_names
-					If set to `true`, key and section names containing spaces (even
-					within quotes) will be rendered as #INI_UNKNOWN. Note that
-					setting #IniFormat::delimiter_symbol to #INI_ANY_SPACE will not
-					automatically set this option to `true` (spaces will still be
-					allowed in section names).
-	@property	IniFormat::no_single_quotes
-					If set to `true`, the single-quote character (`'`) will be
-					considered as a normal character.
-	@property	IniFormat::no_double_quotes
-					If set to `true`, the double-quote character (`"`) will be
-					considered as a normal character.
-	@property	IniFormat::implicit_is_not_empty
-					If set to `true`, implicit keys (see @ref libconfini) will
-					be always dispatched using the values given by the global
-					variables #INI_GLOBAL_IMPLICIT_VALUE and
-					#INI_GLOBAL_IMPLICIT_V_LEN for the fields #IniDispatch::value
-					and to #IniDispatch::v_len respectively; if set to `false`,
-					implicit keys will be considered to be empty keys.
-	@property	IniFormat::do_not_collapse_values
-					If set to `true`, sequences of one or more spaces in values
-					(`/\s+/`) will be dispatched verbatim.
-	@property	IniFormat::preserve_empty_quotes
-					If set to `true`, and if single/double quotes are
-					metacharacters, ensures that, within values, empty strings
-					enclosed between quotes (`""` or `''`) will not be collapsed
-					together with the spaces that surround them. This option is
-					useful for values containing space-delimited arrays, in order to
-					preserve their empty members -- as in, for instance:
-					`coordinates = "" ""`. Note that, in section and key names,
-					empty strings enclosed between quotes are _always_ collapsed
-					together with their surrounding spaces.
-	@property	IniFormat::disabled_after_space
-					If set to `true`, what follows `/[#;]\s/` is allowed to be
-					parsed as a disabled entry.
-	@property	IniFormat::disabled_can_be_implicit
-					If set to `false`, comments that do not contain a key-value
-					delimiter will never be parsed as disabled keys, but always as
-					simple comments (even if the format supports implicit keys).
-
-
-
-	@struct		IniStatistics
-
-	@property	IniStatistics::format
-					The format of the INI file (see #IniFormat)
-	@property	IniStatistics::bytes
-					The size in bytes of the parsed file
-	@property	IniStatistics::members
-					The size in number of members (nodes) of the parsed file -- this
-					number always equals the number of dispatches that will be sent
-					by #load_ini_file(), #load_ini_path() or #strip_ini_cache()
-
-
-
-	@struct		IniDispatch
-
-	@property	IniDispatch::format
-					The format of the INI file (see #IniFormat)
-	@property	IniDispatch::type
-					The dispatch type (see `enum` #IniNodeType)
-	@property	IniDispatch::data
-					#IniDispatch::data can contain a comment, a section path or a
-					key name depending, on #IniDispatch::type; cannot be `NULL`
-	@property	IniDispatch::value
-					It can be the value of a key element, an empty string or it can
-					point to the address pointed by the global variable
-					#INI_GLOBAL_IMPLICIT_VALUE (_the latter is the only case in
-					which `IniDispatch::value` can be `NULL`_)
-	@property	IniDispatch::append_to
-					The current section path; cannot be `NULL`
-	@property	IniDispatch::d_len
-					The length of the string #IniDispatch::data
-	@property	IniDispatch::v_len
-					The length of the string #IniDispatch::value
-	@property	IniDispatch::at_len
-					The length of the string #IniDispatch::append_to
-	@property	IniDispatch::dispatch_id
-					The dispatch number (the first dispatch is number zero)
-
-
-**/
-
-
-
-		/*\
-		|*|
-		|*|     LOCAL ENVIRONMENT
-		|*|    ________________________________
-		\*/
-
-
-
-		/*  PREPROCESSOR PREAMBLE  */
-
-
-/*  String concatenation facilities  */
-#define __PP_CAT__(STR1, STR2) STR1##STR2
-#define __PP_UCAT__(STR1, STR2) STR1##_##STR2
-#define __PP_EVALUCAT__(STR1, STR2) __PP_UCAT__(STR1, STR2)
-
-
-
-		/*  PREPROCESSOR ENVIRONMENT  */
-
-
-#ifndef CONFINI_IO_FLAVOR
-/**
-
-	@brief			The I/O API to use (possibly overridden via
-					`-DCONFINI_IO_FLAVOR=[FLAVOR]`)
-
-	Possible values are `CONFINI_STANDARD` and `CONFINI_POSIX`
-
-**/
-#define CONFINI_IO_FLAVOR CONFINI_STANDARD
-#endif
-
-
-
-		/*  PREPROCESSOR GLUE  */
-
-
-#define _LIBCONFINI_PRIVATE_ITEM_(GROUP, ITEM) \
-	__PP_EVALUCAT__(__PP_CAT__(_LIB, GROUP), __PP_CAT__(ITEM, _))
-#define _LIBCONFINI_CURRENT_FLAVOR_GET_(NAME) \
-	_LIBCONFINI_PRIVATE_ITEM_(CONFINI_IO_FLAVOR, NAME)
-#define _LIBCONFINI_IS_FLAVOR_(NAME) \
-	_LIBCONFINI_PRIVATE_ITEM_(CONFINI_IO_FLAVOR, FLAVOR) == \
-		_LIBCONFINI_PRIVATE_ITEM_(NAME, FLAVOR)
-
-
-
-		/*  AVAILABLE I/O FLAVORS  */
-
-
-/*  `-DCONFINI_IO_FLAVOR=CONFINI_STANDARD` (C99 Standard, default, REQUIRED)  */
-#define _LIBCONFINI_STANDARD_SEOF_FN_(FILEPTR) fseek(FILEPTR, 0, SEEK_END)
-#define _LIBCONFINI_STANDARD_FT_FN_(FILEPTR) ftell(FILEPTR)
-#define _LIBCONFINI_STANDARD_FT_T_ long signed int
-/*  Any unique non-zero integer to identify this I/O API  */
-#define _LIBCONFINI_STANDARD_FLAVOR_ 1
-
-/*  `-DCONFINI_IO_FLAVOR=CONFINI_POSIX`  */
-#define _LIBCONFINI_POSIX_SEOF_FN_(FILEPTR) fseeko(FILEPTR, 0, SEEK_END)
-#define _LIBCONFINI_POSIX_FT_FN_(FILEPTR) ftello(FILEPTR)
-#define _LIBCONFINI_POSIX_FT_T_ off_t
-/*  Any unique non-zero integer to identify this I/O API  */
-#define _LIBCONFINI_POSIX_FLAVOR_ 2
-
-/*  Define `_POSIX_C_SOURCE` macro when `CONFINI_IO_FLAVOR == CONFINI_POSIX`  */
-#if _LIBCONFINI_IS_FLAVOR_(CONFINI_POSIX)
-#ifndef _POSIX_C_SOURCE
-#define _POSIX_C_SOURCE 200809L
-#endif
-#endif
-
-/*  It is possible to add other I/O APIs here. Feel	free to contribute!  */
-
-
-
-		/*  CHECKS  */
-
-
-#if _LIBCONFINI_PRIVATE_ITEM_(CONFINI_IO_FLAVOR, FLAVOR) == 0
-#error Unsupported I/O API defined in macro CONFINI_IO_FLAVOR
-#endif
-
-
-
-		/*  HEADERS  */
-
-#include <stdlib.h>
-#include "confini.h"
-
-
-
-		/*  ALIASES  */
-
-
-#define _LIBCONFINI_FALSE_ 0
-#define _LIBCONFINI_TRUE_ 1
-#define _LIBCONFINI_CHARBOOL_ unsigned char
-#define _LIBCONFINI_SIMPLE_SPACE_ ' '
-#define _LIBCONFINI_HT_ '\t'
-#define _LIBCONFINI_FF_ '\f'
-#define _LIBCONFINI_VT_ '\v'
-#define _LIBCONFINI_CR_ '\r'
-#define _LIBCONFINI_LF_ '\n'
-#define _LIBCONFINI_BACKSLASH_ '\\'
-#define _LIBCONFINI_OPEN_SECTION_ '['
-#define _LIBCONFINI_CLOSE_SECTION_ ']'
-#define _LIBCONFINI_SUBSECTION_ '.'
-#define _LIBCONFINI_SEMICOLON_ ';'
-#define _LIBCONFINI_HASH_ '#'
-#define _LIBCONFINI_DOUBLE_QUOTES_ '"'
-#define _LIBCONFINI_SINGLE_QUOTES_ '\''
-#define _LIBCONFINI_SEEK_EOF_(FILEPTR) \
-	_LIBCONFINI_CURRENT_FLAVOR_GET_(SEOF_FN)(FILEPTR)
-#define _LIBCONFINI_FTELL_(FILEPTR) \
-	_LIBCONFINI_CURRENT_FLAVOR_GET_(FT_FN)(FILEPTR)
-#define _LIBCONFINI_OFF_T_ \
-	_LIBCONFINI_CURRENT_FLAVOR_GET_(FT_T)
-
-
-
-		/*  FUNCTIONAL MACROS AND CONSTANTS  */
-
-
-/*  The character that will replace sequences of one or more spaces (`/\s+/`)  */
-#define _LIBCONFINI_COLLAPSED_ _LIBCONFINI_SIMPLE_SPACE_
-
-
-/*
-
-	These may be any character in theory... But after the left-trim of each node
-	leading spaces work pretty well as metacharacters...
-
-*/
-/*  Internal marker of standard comments  */
-#define _LIBCONFINI_SC_INT_MARKER_ _LIBCONFINI_SIMPLE_SPACE_
-/*  Internal marker of inline comments  */
-#define _LIBCONFINI_IC_INT_MARKER_ _LIBCONFINI_HT_
-
-
-/*
-
-	Checks whether a character can be escaped within a given format
-
-*/
-#define _LIBCONFINI_IS_ESC_CHAR_(CHR, FMT) ( \
-		CHR == _LIBCONFINI_BACKSLASH_ ? \
-			!INIFORMAT_HAS_NO_ESC(FMT) \
-		: CHR == _LIBCONFINI_DOUBLE_QUOTES_ ? \
-			!FMT.no_double_quotes \
-		: \
-			CHR == _LIBCONFINI_SINGLE_QUOTES_ && !FMT.no_single_quotes \
-	)
-
-
-/*
-
-	Checks whether a character represents any marker within a given format
-
-*/
-#define _LIBCONFINI_IS_ANY_MARKER_(CHR, FMT) ( \
-		CHR == _LIBCONFINI_HASH_ ? \
-			FMT.hash_marker != INI_IS_NOT_A_MARKER \
-		: \
-			CHR == _LIBCONFINI_SEMICOLON_ && FMT.semicolon_marker != INI_IS_NOT_A_MARKER \
-	)
-
-
-/*
-
-	Checks whether a character represents any marker except `INI_IGNORE` within a
-	given format
-
-*/
-#define _LIBCONFINI_IS_COM_MARKER_(CHR, FMT) ( \
-		CHR == _LIBCONFINI_HASH_ ? \
-			FMT.hash_marker != INI_IS_NOT_A_MARKER && FMT.hash_marker != INI_IGNORE \
-		: \
-			CHR == _LIBCONFINI_SEMICOLON_ && FMT.semicolon_marker != INI_IS_NOT_A_MARKER && FMT.semicolon_marker != INI_IGNORE \
-	)
-
-
-/*
-
-	Checks whether a character represents a marker of type `INI_DISABLED_OR_COMMENT`
-	within a given format
-
-*/
-#define _LIBCONFINI_IS_DIS_MARKER_(CHR, FMT) ( \
-		CHR == _LIBCONFINI_HASH_ ? \
-			FMT.hash_marker == INI_DISABLED_OR_COMMENT \
-		: \
-			CHR == _LIBCONFINI_SEMICOLON_ && FMT.semicolon_marker == INI_DISABLED_OR_COMMENT \
-	)
-
-
-/*
-
-	Checks whether a character represents a marker of type `INI_IGNORE` within a
-	given format
-
-*/
-#define _LIBCONFINI_IS_IGN_MARKER_(CHR, FMT) ( \
-		CHR == _LIBCONFINI_HASH_ ? \
-			FMT.hash_marker == INI_IGNORE \
-		: \
-			CHR == _LIBCONFINI_SEMICOLON_ && FMT.semicolon_marker == INI_IGNORE \
-	)
-
-
-/*
-
-	Checks whether a pointer is within the range
-	`INI_GLOBAL_IMPLICIT_VALUE >= PTR <= INI_GLOBAL_IMPLICIT_VALUE + INI_GLOBAL_IMPLICIT_V_LEN`
-
-*/
-#define _LIBCONFINI_IMPLICIT_RANGE_(PTR) \
-	(PTR >= INI_GLOBAL_IMPLICIT_VALUE && PTR <= INI_GLOBAL_IMPLICIT_VALUE + INI_GLOBAL_IMPLICIT_V_LEN)
-
-
-/*
-
-	Maybe in the future there will be support for UTF-8 casefold (although probably
-	not -- see "Code considerations" in the Manual), but for now only ASCII...
-
-*/
-#define _LIBCONFINI_CHR_CASEFOLD_(CHR) (CHR > 0x40 && CHR < 0x5b ? CHR | 0x60 : CHR)
-
-
-/*
-
-	Possible depths of `_LIBCONFINI_SPACES_` (see function #is_some_space()).
-
-	Please, consider the following three constants as belonging together to a
-	virtual opaque `enum`.
-
-*/
-#define _LIBCONFINI_WITH_EOL_ -1
-#define _LIBCONFINI_NO_EOL_ 1
-#define _LIBCONFINI_JUST_S_T_ 3
-
-
-/*
-
-	Other constants related to `_LIBCONFINI_SPACES_`
-
-*/
-#define _LIBCONFINI_EOL_IDX_ 0
-#define _LIBCONFINI_SPALEN_ 6
-
-
-/*
-
-	The list of space characters -- do not change its order or its content!
-
-*/
-static const char _LIBCONFINI_SPACES_[_LIBCONFINI_SPALEN_] = {
-	_LIBCONFINI_LF_,
-	_LIBCONFINI_CR_,
-	_LIBCONFINI_VT_,
-	_LIBCONFINI_FF_,
-	_LIBCONFINI_HT_,
-	_LIBCONFINI_SIMPLE_SPACE_
-};
-
-
-/**
-
-	@brief			A list of possible string representations of boolean pairs
-
-	There may be infinite pairs here. Each pair must be organized according to the
-	following order:
-
-	1. Signifier of `false`
-	2. Signifier of `true`
-
-	@note	Everything **must** be lowercase in this list.
-
-**/
-static const char * const INI_BOOLEANS[][2] = {
-	{ "no", "yes" },
-	{ "false", "true" },
-	{ "off", "on" }
-};
-
-
-
-		/*  ABSTRACT UTILITIES  */
-
-
-/**
-
-	@brief			Checks whether a character is a space
-	@param			chr				The target character
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			A boolean: `true` if the character matches, `false` otherwise
-
-**/
-static inline _LIBCONFINI_CHARBOOL_ is_some_space (const char chr, const int8_t depth) {
-	register int8_t idx = depth;
-	while (++idx < _LIBCONFINI_SPALEN_ && chr != _LIBCONFINI_SPACES_[idx]);
-	return idx < _LIBCONFINI_SPALEN_;
-}
-
-
-/**
-
-	@brief			Soft left trim -- does not change the buffer
-	@param			str				The target string
-	@param			offs			The offset where to start the left trim
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			The offset of the first non-space character
-
-**/
-static inline size_t ltrim_s (const char * const str, const size_t offs, const int8_t depth) {
-	register size_t idx = offs;
-	while (is_some_space(str[idx++], depth));
-	return idx - 1;
-}
-
-
-/**
-
-	@brief			Hard left trim -- **does** change the buffer
-	@param			str				The target string
-	@param			offs			The offset where to start the left trim
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			The offset of the first non-space character
-
-**/
-static inline size_t ltrim_h (char * const str, const size_t offs, const int8_t depth) {
-	register size_t idx = offs;
-	while (is_some_space(str[idx], depth)) { str[idx++] = '\0'; }
-	return idx;
-}
-
-
-/**
-
-	@brief			Shifting left trim -- **does** change the buffer
-	@param			str				The target string
-	@param			offs			The offset where to start the left trim
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			The new length of the string
-
-**/
-static inline size_t ltrim_hh (char * const str, const size_t offs, const int8_t depth) {
-	register size_t idx_d = offs, idx_s = offs;
-	while (is_some_space(str[idx_s++], depth));
-	if (--idx_s - idx_d) {
-		while ((str[idx_d++] = str[idx_s++]));
-		for (idx_s = idx_d; str[idx_s]; str[idx_s++] = '\0');
-		return idx_d - 1;
-	}
-	while (str[idx_s++]);
-	return idx_s - 1;
-}
-
-
-/**
-
-	@brief			Soft right trim -- does not change the buffer
-	@param			str				The target string
-	@param			len				The length of the string
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			The length of the string until the last non-space character
-
-**/
-static inline size_t rtrim_s (const char * const str, const size_t len, const int8_t depth) {
-	register size_t idx = len + 1;
-	while (--idx > 0 && is_some_space(str[idx - 1], depth));
-	return idx;
-}
-
-
-/**
-
-	@brief			Hard right trim -- **does** change the buffer
-	@param			str				The target string
-	@param			len				The length of the string
-	@param			depth			What is actually considered a space (possible
-									values: `_LIBCONFINI_WITH_EOL_`,
-									`_LIBCONFINI_NO_EOL_`, `_LIBCONFINI_JUST_S_T_`)
-	@return			The new length of the string
-
-**/
-static inline size_t rtrim_h (char * const str, const size_t len, const int8_t depth) {
-	register size_t idx = len;
-	while (idx > 0 && is_some_space(str[idx - 1], depth)) { str[--idx] = '\0'; }
-	return idx;
-}
-
-
-/**
-
-	@brief			Unescaped soft right trim (right trim of `/(?:\s|\\[\n\r])+$/`)
-					-- does not change the buffer
-	@param			str				The target string
-	@param			len				The length of the string
-	@return			The length of the string until the last non-space character
-
-**/
-static inline size_t urtrim_s (const char * const str, const size_t len) {
-
-	register uint8_t abcd = 1;
-	register size_t idx = len;
-
-
-	/* \                                /\
-	\ */     continue_urtrim:          /* \
-	 \/     ______________________     \ */
-
-
-	if (idx < 1) {
-
-		return idx;
-
-	}
-
-	switch (str[--idx]) {
-
-		case _LIBCONFINI_VT_:
-		case _LIBCONFINI_FF_:
-		case _LIBCONFINI_HT_:
-		case _LIBCONFINI_SIMPLE_SPACE_:
-
-			abcd = 1;
-			goto continue_urtrim;
-
-		case _LIBCONFINI_LF_:
-		case _LIBCONFINI_CR_:
-
-			abcd = 3;
-			goto continue_urtrim;
-
-		case _LIBCONFINI_BACKSLASH_:
-
-			if (abcd >>= 1) {
-
-				goto continue_urtrim;
-
-			}
-
-	}
-
-	return idx + 1;
-
-}
-
-
-/**
-
-	@brief			Converts an ASCII string to lower case
-	@param			str			The target string
-	@return			Nothing
-
-**/
-static inline void string_tolower (char * const str) {
-	for (register char * chrptr = str; *chrptr; chrptr++) {
-		*chrptr = _LIBCONFINI_CHR_CASEFOLD_(*chrptr);
-	}
-}
-
-
-
-		/*  CONCRETE UTILITIES  */
-
-
-/**
-
-	@brief			Unparsed hard left trim (left trim of
-					`/^(?:\s+|\\[\n\r]|''|"")+/`) -- **does** change the buffer
-	@param			srcstr			The target string (it may contain multi-line
-									escape sequences)
-	@param			offs			The offset where to start the left trim
-	@param			format			The format of the INI file
-	@return			The offset of the first non-trivial character
-
-**/
-static inline size_t qultrim_h (char * const srcstr, const size_t offs, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		Erase the previous character
-		FLAG_64		Erase this character
-		FLAG_128	Continue the loop
-
-	*/
-
-	register uint8_t abcd = (format.no_double_quotes ? 130 : 128) | format.no_single_quotes;
-	size_t idx = offs;
-
-	do {
-
-		abcd	=	!(abcd & 28) && is_some_space(srcstr[idx], _LIBCONFINI_NO_EOL_) ?
-						(abcd & 207) | 64
-					: !(abcd & 12) && (srcstr[idx] == _LIBCONFINI_LF_ || srcstr[idx] == _LIBCONFINI_CR_) ?
-						(
-							abcd & 16 ?
-								(abcd & 239) | 96
-							:
-								abcd | 64
-						)
-					: !(abcd & 25) && srcstr[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							abcd & 4 ?
-								(abcd & 235) | 96
-							:
-								(abcd & 143) | 4
-						)
-					: !(abcd & 22) && srcstr[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							abcd & 8 ?
-								(abcd & 231) | 96
-							:
-								(abcd & 159) | 8
-						)
-					: srcstr[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd & 159) ^ 16
-					:
-						abcd & 31;
-
-
-		if (abcd & 32) {
-
-			srcstr[idx - 1] = '\0';
-
-		}
-
-		if (abcd & 64) {
-
-			srcstr[idx] = '\0';
-
-		}
-
-		idx++;
-
-	} while (abcd & 128);
-
-	return abcd & 28 ? idx - 2 : idx - 1;
-
-}
-
-
-/**
-
-	@brief			Soft left trim within an unparsed disabled entry (left trim of
-					`/(?:(?:^|\\?[\n\r])[ \t\v\f]*(?:#(?:[ \t\v\f]|''|"")*)?)+/`)
-					-- does not change the buffer
-	@param			srcstr			The target string (it may contain multi-line
-									escape sequences)
-	@param			offs			The offset where to start the left trim
-	@param			format			The format of the INI file
-	@return			The offset of the first non-trivial character
-
-**/
-static inline size_t dqultrim_s (const char * const srcstr, const size_t offs, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (7 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		A new line has just begun
-		FLAG_64		Continue the left trim
-
-	*/
-
-
-	register uint16_t abcd	=	format.no_single_quotes |
-								(format.no_double_quotes << 1) |
-								96;
-
-	register size_t idx = offs;
-
-	do {
-
-		abcd	=	is_some_space(srcstr[idx], _LIBCONFINI_NO_EOL_) ?
-						(
-							abcd & 28 ?
-								abcd & 63
-							:
-								abcd
-						)
-					: srcstr[idx] == _LIBCONFINI_LF_ || srcstr[idx] == _LIBCONFINI_CR_ ?
-						(
-							abcd & 12 ?
-								(abcd & 47) | 32
-							:
-								(abcd & 111) | 32
-						)
-					: srcstr[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(
-							abcd & 28 ?
-								(abcd & 31) | 16
-							:
-								(abcd & 95) | 16
-						)
-					: (abcd & 32) && _LIBCONFINI_IS_ANY_MARKER_(srcstr[idx], format) ?
-						abcd & 79
-					: !(abcd & 54) && srcstr[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(abcd & 95) ^ 8
-					: !(abcd & 57) && srcstr[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(abcd & 95) ^ 4
-					: srcstr[idx] ?
-						abcd & 31
-					:
-						abcd & 19;
-
-
-		idx++;
-
-	} while (abcd & 64);
-
-	return abcd & 28 ? idx - 2 : idx - 1;
-
-}
-
-
-/**
-
-	@brief			Gets the position of the first occurence out of quotes of a
-					given character, stopping after a given number of charcters
-	@param			str				The string where to search
-	@param			chr				The character to to search
-	@param			len				The maximum number of characters to read
-	@param			format			The format of the INI file
-	@return			The offset of the first occurence of @p chr, or @p len if
-					@p chr has not been not found
-
-**/
-static inline size_t getn_metachar_pos (const char * const str, const char chr, const size_t len, const IniFormat format) {
-
-	size_t idx = 0;
-
-	/*
-
-	Mask `abcd` (5 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-
-	*/
-
-	for (
-
-		register uint8_t abcd = (format.no_double_quotes << 1) | format.no_single_quotes;
-
-			idx < len && ((abcd & 12) || (chr ? str[idx] != chr : !is_some_space(str[idx], _LIBCONFINI_WITH_EOL_)));
-
-		abcd	=	str[idx] == _LIBCONFINI_BACKSLASH_ ? abcd ^ 16
-					: !(abcd & 22) && str[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ? abcd ^ 8
-					: !(abcd & 25) && str[idx] == _LIBCONFINI_SINGLE_QUOTES_ ? abcd ^ 4
-					: abcd & 15,
-		idx++
-
-	);
-
-	return idx;
-
-}
-
-
-/**
-
-	@brief			Gets the position of the first occurence out of quotes of a
-					given character
-	@param			str				The string where to search
-	@param			chr				The character to to search
-	@param			format			The format of the INI file
-	@return			The offset of the first occurence of @p chr or the length of
-					@p str if @p chr has not been not found
-
-**/
-static inline size_t get_metachar_pos (const char * const str, const char chr, const IniFormat format) {
-
-	size_t idx = 0;
-
-	/*
-
-	Mask `abcd` (5 bits used):
-
-		--> As in #getn_metachar_pos()
-
-	*/
-
-	for (
-
-		register uint8_t abcd = (format.no_double_quotes << 1) | format.no_single_quotes;
-
-			str[idx] && ((abcd & 12) || (chr ? str[idx] != chr : !is_some_space(str[idx], _LIBCONFINI_NO_EOL_)));
-
-		abcd	=	str[idx] == _LIBCONFINI_BACKSLASH_ ? abcd ^ 16
-					: !(abcd & 22) && str[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ? abcd ^ 8
-					: !(abcd & 25) && str[idx] == _LIBCONFINI_SINGLE_QUOTES_ ? abcd ^ 4
-					: abcd & 15,
-		idx++
-
-	);
-
-	return idx;
-
-}
-
-
-/**
-
-	@brief			Replaces `/\\(\n\r?|\r\n?)[\t \v\f]*[#;]/` or `/\\(\n\r?|\r\n?)/`
-					with `"$1"`
-	@param			srcstr			The target string (it may contain multi-line
-									escape sequences)
-	@param			len				Length of the string
-	@param			is_disabled		The string represents a disabled entry
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-**/
-static size_t unescape_cr_lf (char * const srcstr, const size_t len, const _LIBCONFINI_CHARBOOL_ is_disabled, const IniFormat format) {
-
-	register size_t idx_s = 0, idx_d = 0;
-	register uint8_t eol_i = _LIBCONFINI_EOL_IDX_;
-	register _LIBCONFINI_CHARBOOL_ is_escaped = _LIBCONFINI_FALSE_;
-	size_t probe;
-
-	while (idx_s < len) {
-
-		if (
-			is_escaped && (
-				srcstr[idx_s] == _LIBCONFINI_SPACES_[eol_i] || srcstr[idx_s] == _LIBCONFINI_SPACES_[eol_i ^= 1]
-			)
-		) {
-
-			srcstr[idx_d - 1] = srcstr[idx_s++];
-
-			if (srcstr[idx_s] == _LIBCONFINI_SPACES_[eol_i ^ 1]) {
-
-				srcstr[idx_d++] = srcstr[idx_s++];
-
-			}
-
-			if (is_disabled) {
-
-				probe = ltrim_s(srcstr, idx_s, _LIBCONFINI_NO_EOL_);
-
-				if (_LIBCONFINI_IS_DIS_MARKER_(srcstr[probe], format)) {
-
-					idx_s = probe + 1;
-
-				}
-
-			}
-
-			is_escaped = _LIBCONFINI_FALSE_;
-
-		} else {
-
-			is_escaped	=	srcstr[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-								!is_escaped
-							:
-								_LIBCONFINI_FALSE_;
-
-
-			srcstr[idx_d++] = srcstr[idx_s++];
-
-		}
-
-	}
-
-	for (idx_s = idx_d; idx_s < len; srcstr[idx_s++] = '\0');
-
-	return idx_d;
-
-}
-
-
-/**
-
-	@brief			Sanitizes a section path
-	@param			secpath			The section path
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-	Out of quotes, similar to ECMAScript
-	`secpath.replace(/\.*\s*$|(?:\s*(\.))+\s*|^\s+/g, "$1").replace(/\s+/g, " ")`
-
-	A section path can start with a dot (append), but cannot end with a dot. Spaces
-	surrounding dots will be removed. Fragments surrounded by single or double
-	quotes (if these are enabled) are prevented from changes.
-
-**/
-static size_t sanitize_section_path (char * const secpath, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (12 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		These are initial spaces
-		FLAG_64		This is a space out of quotes
-		FLAG_128	This is a dot out of quotes
-		FLAG_256	This is anything *but* an opening single/double quote
-		FLAG_512	Don't ignore the last two characters
-		FLAG_1024	Don't overwrite the previous character
-		FLAG_2048	Path contains at least one name
-
-	*/
-
-	register uint16_t abcd = (format.no_double_quotes ? 1826 : 1824) | format.no_single_quotes;
-	register size_t idx_s = 0, idx_d = 0;
-
-	for (; secpath[idx_s]; idx_s++) {
-
-		/*  Revision #2  */
-
-		abcd	=	!(abcd & 12) && is_some_space(secpath[idx_s], _LIBCONFINI_WITH_EOL_) ?
-						(
-							abcd & 224 ?
-								(abcd & 3055) | 832
-							:
-								(abcd & 4079) | 1856
-						)
-					: !(abcd & 12) && secpath[idx_s] == _LIBCONFINI_SUBSECTION_ ?
-						(
-							abcd & (abcd & 32 ? 128 : 192) ?
-								(abcd & 2959) | 896
-							:
-								(abcd & 3983) | 1920
-						)
-					: !(abcd & 25) && secpath[idx_s] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							~abcd & 4 ?
-								(abcd & 3839) | 1540
-							: abcd & 256 ?
-								(abcd & 3867) | 3840
-							:
-								(abcd & 3579) | 1280
-						)
-					: !(abcd & 22) && secpath[idx_s] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							~abcd & 8 ?
-								(abcd & 3839) | 1544
-							: abcd & 256 ?
-								(abcd & 3863) | 3840
-							:
-								(abcd & 3575) | 1280
-						)
-					: secpath[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-						((abcd & 3871) | 3840) ^ 16
-					:
-						(abcd & 3855) | 3840;
-
-
-		if (abcd & 512) {
-
-			secpath[
-				abcd & 1024 ?
-					idx_d++
-				: idx_d ?
-					idx_d - 1
-				:
-					idx_d
-			]					=	!(~abcd & 384) ?
-										_LIBCONFINI_SUBSECTION_
-									: !(~abcd & 320) ?
-										_LIBCONFINI_COLLAPSED_
-									:
-										secpath[idx_s];
-
-		} else if (idx_d) {
-
-			idx_d--;
-
-		}
-
-	}
-
-	for (
-
-		idx_s	=	idx_d && (abcd & 2048) && (abcd & 192) ?
-						--idx_d
-					:
-						idx_d;
-
-			secpath[idx_s];
-
-		secpath[idx_s++] = '\0'
-
-	);
-
-	return idx_d;
-
-}
-
-
-/**
-
-	@brief			Out of quotes similar to ECMAScript
-					`ini_string.replace(/''|""/g, "").replace(/^[\n\r]\s*|\s+/g, " ")`
-	@param			ini_string		The string to collapse -- multi-line escape
-									sequences must be already unescaped at
-									this stage
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-**/
-static size_t collapse_everything (char * const ini_string, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (9 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		This is *not* a space out of quotes
-		FLAG_64		This is an opening single/double quote
-		FLAG_128	Don't ignore this character
-		FLAG_256	Jump this character and the one before this
-
-	*/
-
-	register size_t idx_s = 0, idx_d = 0;
-
-	register uint16_t abcd	=	(is_some_space(*ini_string, _LIBCONFINI_WITH_EOL_) ? 128 : 160) |
-								(format.no_double_quotes << 1) |
-								format.no_single_quotes;
-
-
-	for (; ini_string[idx_s]; idx_s++) {
-
-		/*  Revision #2  */
-
-		abcd	=	!(abcd & 12) && is_some_space(ini_string[idx_s], _LIBCONFINI_WITH_EOL_) ?
-						(
-							abcd & 32 ?
-								(abcd & 143) | 128
-							:
-								abcd & 47
-						)
-					: !(abcd & 25) && ini_string[idx_s] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							~abcd & 4 ?
-								(abcd & 239) | 196
-							: abcd & 64 ?
-								(abcd & 299) | 256
-							:
-								(abcd & 171) | 160
-						)
-					: !(abcd & 22) && ini_string[idx_s] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							~abcd & 8 ?
-								(abcd & 239) | 200
-							: abcd & 64 ?
-								(abcd & 295) | 256
-							:
-								(abcd & 167) | 160
-						)
-					: ini_string[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-						((abcd & 191) | 160) ^ 16
-					:
-						(abcd & 175) | 160;
-
-
-		if (abcd & 256) {
-
-			idx_d--;
-
-		} else if (abcd & 128) {
-
-			ini_string[idx_d++] = abcd & 44 ? ini_string[idx_s] : _LIBCONFINI_COLLAPSED_;
-
-		}
-
-	}
-
-	for (
-
-		idx_s	=	!(abcd & 32) && idx_d ?
-						--idx_d
-					:
-						idx_d;
-
-			ini_string[idx_s];
-
-		ini_string[idx_s++] = '\0'
-
-	);
-
-	return idx_d;
-
-}
-
-
-/**
-
-	@brief			Out of quotes similar to ECMAScript
-					`ini_string.replace(/\s+/g, " ")`
-	@param			ini_string		The string to collapse -- multi-line escape
-									sequences must be already unescaped at this
-									stage
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-**/
-static size_t collapse_spaces (char * const ini_string, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (7 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		This is a space out of quotes
-		FLAG_64		Jump this character
-
-	*/
-
-	register uint8_t abcd = (format.no_double_quotes ? 34 : 32) | format.no_single_quotes;
-	register size_t idx_s = 0;
-	size_t idx_d = 0;
-
-	for (; ini_string[idx_s]; idx_s++) {
-
-		/*  Revision #1  */
-
-		abcd	=	!(abcd & 12) && is_some_space(ini_string[idx_s], _LIBCONFINI_WITH_EOL_) ?
-						(
-							abcd & 32 ?
-								(abcd & 111) | 64
-							:
-								(abcd & 47) | 32
-						)
-					: !(abcd & 25) && ini_string[idx_s] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(abcd & 15) ^ 4
-					: !(abcd & 22) && ini_string[idx_s] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(abcd & 15) ^ 8
-					: ini_string[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd & 31) ^ 16
-					:
-						abcd & 15;
-
-
-		if (~abcd & 64) {
-
-			ini_string[idx_d++] = abcd & 32 ? _LIBCONFINI_COLLAPSED_ : ini_string[idx_s];
-
-		}
-
-	}
-
-	for (
-
-		idx_s	=	(abcd & 32) && idx_d ?
-						--idx_d
-					:
-						idx_d;
-
-			ini_string[idx_s];
-
-		ini_string[idx_s++] = '\0'
-
-	);
-
-	return idx_d;
-
-}
-
-
-/**
-
-	@brief			Similar to ECMAScript `str.replace(/''|""/g, "")`
-	@param			str				The string to collapse
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-**/
-static size_t collapse_empty_quotes (char * const str, const IniFormat format) {
-
-	/*
-
-	Mask `abcd` (7 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes
-		FLAG_32		This is an opening single/double quote
-		FLAG_64		These are empty quotes
-
-	*/
-
-	register uint8_t abcd = (format.no_double_quotes << 1) | format.no_single_quotes;
-	register size_t lshift = ltrim_s(str, 0, _LIBCONFINI_WITH_EOL_), idx = lshift;
-
-	for (; str[idx]; idx++) {
-
-		/*  Revision #1  */
-
-		abcd	=	str[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd & 31) ^ 16
-					: !(abcd & 22) && str[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							~abcd & 40 ?
-								((abcd & 47) | 32) ^ 8
-							:
-								(abcd & 71) | 64
-						)
-					: !(abcd & 25) && str[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							~abcd & 36 ?
-								((abcd & 47) | 32) ^ 4
-							:
-								(abcd & 75) | 64
-						)
-					:
-						abcd & 15;
-
-
-		str[idx - lshift] = str[idx];
-
-		if (abcd & 64) {
-
-			lshift += 2;
-
-		}
-
-	}
-
-	for (idx -= lshift; str[idx]; str[idx++] = '\0');
-
-	return rtrim_h(str, idx - lshift, _LIBCONFINI_WITH_EOL_);
-
-}
-
-
-/**
-
-	@brief			Removes all comment initializers (`#` and/or `;`) from the
-					beginning of each line of a comment
-	@param			srcstr			The comment to parse (it may contain multi-line
-									escape sequences)
-	@param			len				The length of @p srcstr
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-	- In multi-line comments: `srcstr.replace(/^[#;]+|(\n\r?|\r\n?)[\t \v\f]*[#;]+/g, "$1")`
-	- In single-line comments: `srcstr.replace(/^[#;]+/, "")`
-
-	The argument @p srcstr may begin with a comment initializer (`#` or `;`
-	depending on the format), or with the character that immediately follows it.
-
-**/
-static size_t uncomment (char * const srcstr, size_t len, const IniFormat format) {
-
-	register size_t idx_s = 0, idx_d = 0;
-
-	if (format.multiline_nodes == INI_MULTILINE_EVERYWHERE) {
-
-		/*
-
-			The comment can be multi-line
-
-		*/
-
-		/*
-
-		Mask `abcd` (6 bits used):
-
-			FLAG_1		Don't erase any character
-			FLAG_2		We are in an odd sequence of backslashes
-			FLAG_4		This new line character is escaped
-			FLAG_8		This character is a comment character and follows
-						`/(\n\s*|\r\s*)/`
-			FLAG_16		This character is a part of a group of spaces that follow
-						a new line (`/(\n|\r)[\t \v\f]+/`)
-			FLAG_32		This character is *not* a new line character (`/[\r\n]/`)
-
-		*/
-
-		for (register uint8_t abcd = 8; idx_s < len; idx_s++) {
-
-			abcd	=	srcstr[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-							((abcd & 35) | 32) ^ 2
-						: srcstr[idx_s] == _LIBCONFINI_LF_ || srcstr[idx_s] == _LIBCONFINI_CR_ ?
-							(abcd << 1) & 4
-						: !(abcd & 32) && _LIBCONFINI_IS_ANY_MARKER_(srcstr[idx_s], format) ?
-							(abcd & 40) | 8
-						: !(abcd & 40) && is_some_space(srcstr[idx_s], _LIBCONFINI_NO_EOL_) ?
-							(abcd & 57) | 16
-						:
-							(abcd & 33) | 32;
-
-
-			if (!(abcd & 25)) {
-
-				srcstr[abcd & 4 ? idx_d - 1 : idx_d++] = srcstr[idx_s];
-
-			} else if (!(abcd & 28)) {
-
-				idx_d++;
-
-			}
-
-		}
-
-	} else {
-
-		/*
-
-			The comment cannot be multi-line
-
-		*/
-
-		for (; idx_s < len && _LIBCONFINI_IS_ANY_MARKER_(srcstr[idx_s], format); idx_s++);
-
-		if (!idx_s) {
-
-			return len;
-
-		}
-
-		for (; idx_s < len; srcstr[idx_d++] = srcstr[idx_s++]);
-
-	}
-
-	for (idx_s = idx_d; idx_s < len; srcstr[idx_s++] = '\0');
-
-	return idx_d;
-
-}
-
-
-/**
-
-	@brief			Tries to determine the type of a member "as if it was active"
-	@param			srcstr			String containing an individual node (it may
-									contain multi-line escape sequences)
-	@param			len				Length of the node
-	@param			allow_implicit	A boolean: `true` if keys without a key-value
-									delimiter are allowed, `false` otherwise
-	@param			format			The format of the INI file
-	@return			The node type (see header)
-
-**/
-static uint8_t get_type_as_active (
-	const char * const srcstr,
-	const size_t len,
-	const _LIBCONFINI_CHARBOOL_ allow_implicit,
-	const IniFormat format
-) {
-
-	const _LIBCONFINI_CHARBOOL_ invalid_delimiter = _LIBCONFINI_IS_ESC_CHAR_(format.delimiter_symbol, format);
-
-	if (
-		!len || _LIBCONFINI_IS_ANY_MARKER_(*srcstr, format) || (
-			*((unsigned char *) srcstr) == format.delimiter_symbol && !invalid_delimiter
-		)
-	) {
-
-		return INI_UNKNOWN;
-
-	}
-
-	register uint16_t abcd;
-	register size_t idx;
-
-	if (format.section_paths != INI_NO_SECTIONS && *srcstr == _LIBCONFINI_OPEN_SECTION_) {
-
-		if (format.no_spaces_in_names) {
-
-			/*
-
-				Search for the CLOSE SECTION character and possible spaces in names
-				-- i.e., ECMAScript `/[^\.\s]\s+[^\.\s]/g.test(srcstr)`. The
-				algorithm is made more complex by the fact that LF and CR characters
-				are still escaped at this stage.
-
-			*/
-
-			/*
-
-			Mask `abcd` (10 bits used):
-
-				FLAG_1		Single quotes are not metacharacters (const)
-				FLAG_2		Double quotes are not metacharacters (const)
-				FLAG_4		Only one level of nesting is allowed (const)
-				FLAG_8		Unescaped single quotes are odd right now
-				FLAG_16		Unescaped double quotes are odd right now
-				FLAG_32		We are in an odd sequence of backslashes
-				FLAG_64		This is a space
-				FLAG_128	What follows cannot contain spaces
-				FLAG_256	Continue the loop
-				FLAG_512	Section path is *not* valid
-
-			*/
-
-
-			idx = 1;
-
-			abcd	=	(format.section_paths == INI_ONE_LEVEL_ONLY ? 772: 768) |
-						(format.no_double_quotes << 1) |
-						format.no_single_quotes;
-
-
-			do {
-
-				/*  Revision #2  */
-
-				abcd	=	idx >= len ?
-								abcd & 767
-							: !(abcd & 42) && srcstr[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-								(abcd & 991) ^ 16
-							: !(abcd & 49) && srcstr[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-								(abcd & 991) ^ 8
-							: srcstr[idx] == _LIBCONFINI_LF_ || srcstr[idx] == _LIBCONFINI_CR_ ?
-								(abcd & 991) | 64
-							: is_some_space(srcstr[idx], _LIBCONFINI_NO_EOL_) ?
-								(
-									~abcd & 32 ?
-										abcd | 64
-									: ~abcd & 192 ?
-										(abcd & 991) | 192
-									:
-										(abcd & 767) | 128
-								)
-							: !(abcd & 28) && srcstr[idx] == _LIBCONFINI_SUBSECTION_ ?
-								(
-									~abcd & 224 ?
-										abcd & 799
-									:
-										abcd & 767
-								)
-							: !(abcd & 24) && srcstr[idx] == _LIBCONFINI_CLOSE_SECTION_ ?
-								(
-									~abcd & 224 ?
-										abcd & 159
-									:
-										abcd & 767
-								)
-							: srcstr[idx] == _LIBCONFINI_BACKSLASH_ ?
-								(
-									~abcd & 32 ?
-										abcd | 32
-									: ~abcd & 192 ?
-										(abcd & 991) | 128
-									:
-										(abcd & 735)
-								)
-							: ~abcd & 192 ?
-								(abcd & 927) | 128
-							:
-								(abcd & 671) | 128;
-
-
-				idx++;
-
-			} while (abcd & 256);
-
-			if (abcd & 512) {
-
-				return INI_UNKNOWN;
-
-			}
-
-		} else if ((idx = getn_metachar_pos(srcstr, _LIBCONFINI_CLOSE_SECTION_, len, format) + 1) > len) {
-
-			return INI_UNKNOWN;
-
-		}
-
-		/*
-
-			Scan for possible non-space characters following the CLOSE SECTION
-			character: if found the node cannot represent a section path (but it can
-			possibly represent a key). Empty quotes surrounded by spaces will be
-			tolerated.
-
-		*/
-
-		/*
-
-		Recycling variable `abcd` (6 bits used)...:
-
-			FLAG_1		Single quotes are not metacharacters (const)
-			FLAG_2		Double quotes are not metacharacters (const)
-			FLAG_4		Unescaped single quotes are odd right now
-			FLAG_8		Unescaped double quotes are odd right now
-			FLAG_16		We are in an odd sequence of backslashes
-			FLAG_32		Continue the loop
-
-		*/
-
-		abcd = 32 | (format.no_double_quotes << 1) | format.no_single_quotes;
-
-
-		/* \                                /\
-		\ */     nonspace_check:           /* \
-		 \/     ______________________     \ */
-
-
-		if (abcd) {
-
-			if (idx >= len) {
-
-				return INI_SECTION;
-
-			}
-
-			switch (srcstr[idx++]) {
-
-				case _LIBCONFINI_VT_:
-				case _LIBCONFINI_FF_:
-				case _LIBCONFINI_HT_:
-				case _LIBCONFINI_SIMPLE_SPACE_:
-
-					abcd = abcd & 28 ? 0 : abcd & 47;
-					goto nonspace_check;
-
-				case _LIBCONFINI_LF_:
-				case _LIBCONFINI_CR_:
-
-					abcd = abcd & 12 ? 0 : abcd & 47;
-					goto nonspace_check;
-
-				case _LIBCONFINI_BACKSLASH_:
-
-					abcd = abcd & 28 ? 0 : abcd | 16;
-					goto nonspace_check;
-
-				case _LIBCONFINI_DOUBLE_QUOTES_:
-
-					abcd = abcd & 22 ? 0 : (abcd & 47) ^ 8;
-					goto nonspace_check;
-
-				case _LIBCONFINI_SINGLE_QUOTES_:
-
-					abcd = abcd & 25 ? 0 : (abcd & 47) ^ 4;
-					goto nonspace_check;
-
-			}
-
-		}
-
-	}
-
-	/*
-
-		It can be just a key...
-
-	*/
-
-	if (invalid_delimiter && !allow_implicit) {
-
-		return INI_UNKNOWN;
-
-	}
-
-	/*
-
-	Recycling variable `abcd` (2 bits used)...:
-
-		FLAG_1		The delimiter **must** be present
-		FLAG_2		Search for spaces in names
-
-	*/
-
-	abcd = (format.no_spaces_in_names << 1) | (allow_implicit ? 0 : 1);
-
-	if (abcd) {
-
-		idx = getn_metachar_pos(srcstr, (char) format.delimiter_symbol, len, format);
-
-		if ((abcd & 1) && idx == len) {
-
-			return INI_UNKNOWN;
-
-		}
-
-		if (abcd & 2) {
-
-			idx = urtrim_s(srcstr, idx);
-
-			do {
-
-				if (is_some_space(srcstr[--idx], _LIBCONFINI_WITH_EOL_)) {
-
-					return INI_UNKNOWN;
-
-				}
-
-			} while (idx);
-
-		}
-
-	}
-
-	return INI_KEY;
-
-}
-
-
-/**
-
-	@brief			Examines a (single-/multi-line) segment and checks whether
-					it contains more than just one node
-	@param			srcstr			Segment to examine (it may contain multi-line
-									escape sequences)
-	@param			format			The format of the INI file
-	@return			Number of entries found
-
-**/
-static size_t further_cuts (char * const srcstr, const IniFormat format) {
-
-	/*
-
-	Shared flags of mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Do not allow disabled entries after space (const)
-		FLAG_8		Formats supports multi-line entries everywhere (const)
-		FLAG_16		Formats supports multi-line entries everywhere except in
-					comments (const)
-		FLAG_32		Unescaped single quotes are odd right now
-		FLAG_64		Unescaped double quotes are odd right now
-		FLAG_128	We are in an odd sequence of backslashes
-
-	*/
-
-	register uint16_t	abcd	=	((format.disabled_after_space << 2) ^ 4) |
-									(format.no_double_quotes << 1) |
-									format.no_single_quotes | (
-										format.multiline_nodes == INI_MULTILINE_EVERYWHERE ?
-											8
-										: format.multiline_nodes == INI_BUT_COMMENTS ?
-											16
-										:
-											0
-									);
-
-
-	register size_t idx;
-	size_t focus_at, unparsable_at, search_at = 0, num_entries = 0;
-
-
-	/* \                                /\
-	\ */     search_for_cuts:          /* \
-	 \/     ______________________     \ */
-
-
-	if (!srcstr[search_at]) {
-
-		return num_entries;
-
-	}
-
-	unparsable_at = 0;
-
-	abcd	=	_LIBCONFINI_IS_DIS_MARKER_(srcstr[search_at], format) && (
-					!(abcd & 4) || !is_some_space(srcstr[search_at + 1], _LIBCONFINI_NO_EOL_)
-				) ?
-					(abcd & 31) | 2560
-				: _LIBCONFINI_IS_IGN_MARKER_(srcstr[search_at], format) ?
-					(abcd & 8 ? (abcd & 31) | 1024 : abcd & 31)
-				: (abcd & 8) && (
-					srcstr[search_at] == _LIBCONFINI_IC_INT_MARKER_ || _LIBCONFINI_IS_ANY_MARKER_(srcstr[search_at], format)
-				) ?
-					(abcd & 31) | 3072
-				:
-					(abcd & 31) | 2048;
-
-
-	if (abcd & 2048) {
-
-		num_entries++;
-
-	}
-
-	if (abcd & 1536) {
-
-		/*
-
-			Node starts with `/[;#]/` and can be a disabled entry in any format, or
-			a simple comment or a block that must be ignored in multi-line formats
-
-		*/
-
-		/*
-
-		Mask `abcd` (14 bits used):
-
-			FLAG_256	This or the previous character was not a space
-			FLAG_512	We are in a disabled entry or a comment (semi-const)
-			FLAG_1024	We are in a simple comment or in a block that must be ignored
-						and format supports multi-line entries (semi-const)
-			FLAG_2048	We are *not* in a block that must be ignored (semi-const)
-			FLAG_4096	We have *not* just found an inline comment nested within a
-						disabled entry
-			FLAG_8192	We had previously found an inline comment nested in this
-						segment, but the entry that preceded it had been checked and
-						did not seem to represent a valid disabled entry
-
-			NOTE:	For FLAG_1-FLAG_16 I will keep the values already assigned at
-					the beginning of the function; all other flags are already set
-					to zero. For the meaning of flags FLAG_1-FLAG_128 see the
-					beginning of the function.
-
-		*/
-
-		idx = ltrim_s(srcstr, search_at + 1, _LIBCONFINI_NO_EOL_) - 1;
-
-
-		/* \                                /\
-		\ */     inactive_cut:             /* \
-		 \/     ______________________     \ */
-
-
-		switch (srcstr[++idx]) {
-
-			case '\0':
-
-				/*  End of string  */
-
-				if (~abcd & 8) {
-
-					/*
-
-						Check if this is a valid disabled entry. If it is not,
-						search for line breaks.
-
-						If the code has reached this point it means that according
-						to the format disabled entries can be multi-line but
-						comments cannot, and #get_type_as_active() has never been
-						invoked on this entry.
-
-					*/
-
-					focus_at = dqultrim_s(srcstr, search_at, format);
-
-					if (
-						srcstr[focus_at] && !get_type_as_active(
-							srcstr + focus_at,
-							idx - focus_at,
-							format.disabled_can_be_implicit,
-							format
-						)
-					) {
-
-						srcstr[search_at] = _LIBCONFINI_SC_INT_MARKER_;
-						unparsable_at = search_at + 1;
-
-					}
-
-				}
-
-				break;
-
-			case _LIBCONFINI_LF_:
-			case _LIBCONFINI_CR_:
-
-				/*
-
-					Line break has been found in a multi-line disabled entry or
-					a comment. Search for `/\\(?:\n\r?|\r\n?)\s*[^;#]/`.
-
-				*/
-
-				focus_at = dqultrim_s(srcstr, search_at, format);
-				idx = ltrim_s(srcstr, idx + 1, _LIBCONFINI_WITH_EOL_);
-
-				if (
-					abcd & 2048 ?
-						!(
-							_LIBCONFINI_IS_DIS_MARKER_(srcstr[idx], format) && (abcd & 24) && (
-								(~abcd & 516) || !is_some_space(srcstr[idx + 1], _LIBCONFINI_NO_EOL_)
-							)
-						) && !(
-							_LIBCONFINI_IS_COM_MARKER_(srcstr[idx], format) && (abcd & 8) && (
-								((abcd ^ 512) & 8704) || !get_type_as_active(
-									srcstr + focus_at,
-									idx - focus_at,
-									format.disabled_can_be_implicit,
-									format
-								)
-							)
-						)
-					:
-						!_LIBCONFINI_IS_IGN_MARKER_(srcstr[idx], format)
-				) {
-
-					rtrim_h(srcstr, idx, _LIBCONFINI_WITH_EOL_);
-					search_at = qultrim_h(srcstr, idx, format);
-					goto search_for_cuts;
-
-				}
-
-				/*
-
-					No case break here, keep it like this! `case /[ \t\v\f]/` must
-					follow (switch case fallthrough).
-
-				*/
-                __attribute__((fallthrough));
-
-			case _LIBCONFINI_VT_:
-			case _LIBCONFINI_FF_:
-			case _LIBCONFINI_HT_:
-			case _LIBCONFINI_SIMPLE_SPACE_:
-
-				abcd = (abcd & 15999) | 4096;
-				goto inactive_cut;
-
-			case _LIBCONFINI_BACKSLASH_:
-
-				abcd = (abcd | 4352) ^ 128;
-				goto inactive_cut;
-
-			default:
-
-				abcd	=	!(abcd & 1376) && (~abcd & 8200) && _LIBCONFINI_IS_ANY_MARKER_(srcstr[idx], format) ?
-								(abcd & 12159) | 256
-							: !(abcd & 162) && srcstr[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-								((abcd & 16255) | 4352) ^ 64
-							: !(abcd & 193) && srcstr[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-								((abcd & 16255) | 4352) ^ 32
-							:
-								(abcd & 16255) | 4352;
-
-
-				if (abcd & 4096) {
-
-					goto inactive_cut;
-
-				}
-
-				if (~abcd & 8192) {
-
-					/*
-
-						Inline comment has been found in a (supposedly) disabled entry.
-
-					*/
-
-					focus_at = dqultrim_s(srcstr, search_at, format);
-
-					if (get_type_as_active(
-						srcstr + focus_at,
-						idx - focus_at,
-						format.disabled_can_be_implicit,
-						format
-					)) {
-
-						if (!_LIBCONFINI_IS_IGN_MARKER_(srcstr[idx], format)) {
-
-							srcstr[idx] = _LIBCONFINI_IC_INT_MARKER_;
-							num_entries++;
-
-						}
-
-						srcstr[idx - 1] = '\0';
-
-						if (abcd & 8) {
-
-							goto inactive_cut;
-
-						}
-
-						unparsable_at = idx + 1;
-
-					} else {
-
-						abcd |= 8192;
-						srcstr[search_at] = _LIBCONFINI_SC_INT_MARKER_;
-
-						if (abcd & 8) {
-
-							goto inactive_cut;
-
-						}
-
-						unparsable_at = search_at + 1;
-
-					}
-
-				}
-
-				/*  No case break here (last case)  */
-
-		}
-
-	} else if (_LIBCONFINI_IS_ANY_MARKER_(srcstr[search_at], format)) {
-
-		/*
-
-			Node starts with `/[;#]/` but cannot be multi-line or represent a
-			disabled entry
-
-		*/
-
-		unparsable_at = search_at + 1;
-
-	} else {
-
-		/*
-
-			Node is active: search for inline comments
-
-		*/
-
-		/*
-
-		Recycling variable `abcd` (11 bits used)...:
-
-			FLAG_256	Comment marker follows an escaped new line made of only one
-						character (i.e., `"\\\n"` or `"\\\r"` but neither `"\\\r\n"`
-						or `"\\\n\r"`)
-			FLAG_512	This was neither a hash nor a semicolon character
-			FLAG_1024	This was not a space
-
-		NOTE:	For FLAG_1-FLAG_16 I will keep the values already assigned at the
-				beginning of the function; all other flags are already set to zero
-				(see previous usage of `abcd` within this function), with the only
-				exception of FLAG_2048, which I am going to overwrite immediately.
-				For the meaning of flags FLAG_1-FLAG_128 see the beginning of the
-				function.
-
-		*/
-
-		abcd = (abcd & 2047) | 1536;
-		idx = search_at;
-
-
-		/* \                                /\
-		\ */     active_cut:               /* \
-		 \/     ______________________     \ */
-
-
-		switch (srcstr[++idx]) {
-
-			case '\0':
-
-				/*  End of string  */
-				break;
-
-			case _LIBCONFINI_VT_:
-			case _LIBCONFINI_FF_:
-			case _LIBCONFINI_HT_:
-			case _LIBCONFINI_SIMPLE_SPACE_:
-
-				abcd = (abcd & 639) | 512;
-				goto active_cut;
-
-			case _LIBCONFINI_LF_:
-			case _LIBCONFINI_CR_:
-
-				abcd = (abcd & 639) | ((abcd << 1) & 256) | 512;
-				goto active_cut;
-
-			case _LIBCONFINI_BACKSLASH_:
-
-				abcd = ((abcd & 1791) | 1536) ^ 128;
-				goto active_cut;
-
-			default:
-
-				abcd	=	_LIBCONFINI_IS_ANY_MARKER_(srcstr[idx], format) ?
-								abcd & 1407
-							: !(abcd & 162) && srcstr[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-								((abcd & 1791) | 1536) ^ 64
-							: !(abcd & 193) && srcstr[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-								((abcd & 1791) | 1536) ^ 32
-							:
-								(abcd & 1791) | 1536;
-
-				if (abcd & 1760) {
-
-					goto active_cut;
-
-				}
-
-				/*
-
-					Inline comment has been found in an active entry.
-
-				*/
-
-				if (abcd & 256) {
-
-					/*
-
-						Remove the backslash if the comment immediately follows an
-						escaped new line expressed by one chararacter
-						(`/\\[\r\n]/`). In case of CR + LF or LF + CR
-						(`/\\\n\r|\\\r\n/`) the backslash will be removed later by
-						#strip_ini_cache().
-
-					*/
-
-					srcstr[idx - 2] = '\0';
-
-				}
-
-				srcstr[idx - 1] = '\0';
-
-				if (!_LIBCONFINI_IS_IGN_MARKER_(srcstr[idx], format)) {
-
-					srcstr[idx] = _LIBCONFINI_IC_INT_MARKER_;
-
-					if (abcd & 8) {
-
-						search_at = idx;
-						goto search_for_cuts;
-
-					}
-
-					num_entries++;
-
-				} else if (abcd & 8) {
-
-					search_at = idx;
-					goto search_for_cuts;
-
-				}
-
-				unparsable_at = idx + 1;
-				/*  No case break here (last case)  */
-
-		}
-
-	}
-
-	if (unparsable_at) {
-
-		/*
-
-			Cut unparsable multi-line comments
-
-		*/
-
-		for (idx = unparsable_at; srcstr[idx]; idx++) {
-
-			if (srcstr[idx] == _LIBCONFINI_LF_ || srcstr[idx] == _LIBCONFINI_CR_) {
-
-				search_at = qultrim_h(srcstr, idx, format);
-				goto search_for_cuts;
-
-			}
-
-		}
-
-	}
-
-	return num_entries;
-
-}
-
-/** @startfnlist **/
-
-
-
-		/*\
-		|*|
-		|*|     GLOBAL ENVIRONMENT
-		|*|    ________________________________
-		\*/
-
-
-
-		/*  LIBRARY'S MAIN FUNCTIONS  */
-
-
-												/** @utility{strip_ini_cache} **/
-/**
-
-	@brief			Parses and tokenizes a buffer containing an INI file, then
-					dispatches its content to a custom callback
-	@param			ini_source		The buffer containing the INI file to tokenize
-	@param			ini_length		The length of @p ini_source without counting the
-									NUL terminator (if any -- se below)
-	@param			format			The format of the INI file
-	@param			f_init			The function that will be invoked before the
-									first dispatch, or `NULL`
-	@param			f_foreach		The function that will be invoked for each
-									dispatch, or `NULL`
-	@param			user_data		A custom argument, or `NULL`
-	@return			Zero for success, otherwise an error code (see `enum`
-					#ConfiniInterruptNo)
-
-	The @p ini_source parameter must be a valid pointer to a buffer of size
-	@p ini_length + 1 and cannot be `NULL`. The @p ini_source string does not need
-	to be NUL-terminated, but _it does need one extra byte where to append a NUL
-	terminator_ -- in fact, as soon as this function is invoked,
-	`ini_source[ini_length]` will be immediately set to `\0`.
-
-	In most cases, as when using `strlen()` for computing @p ini_length, this is not
-	a concern, since `ini_source[ini_length]` will always be `\0` by the very
-	definition of `strlen()`, and will only get overwritten with the same value.
-	However, if you are passing a substring of a string, for example the fragment
-	`foo=bar` of the string `foo=barracuda`, you must expect the string to be
-	immediately truncated into `foo=bar\0acuda`.
-
-	In other words, @p ini_source must point to a memory location where at least
-	`ini_length + 1` bytes are freely usable.
-
-	The user given function @p f_init (see #IniStatsHandler data type) will be
-	invoked with two arguments: `statistics` (a pointer to an #IniStatistics
-	structure containing some properties about the file read) and `user_data` (the
-	custom argument @p user_data previously passed). If @p f_init returns a non-zero
-	value the caller function will be interrupted.
-
-	The user given function @p f_foreach (see #IniDispHandler data type) will be
-	invoked with two arguments: `dispatch` (a pointer to an #IniDispatch structure
-	containing the parsed member of the INI file) and `user_data` (the custom
-	argument @p user_data previously passed). If @p f_foreach returns a non-zero
-	value the caller function will be interrupted.
-
-	After invoking `strip_ini_cache()`, the buffer pointed by the @p ini_source
-	parameter must be considered as a _corrupted buffer_ and should be freed or
-	overwritten. For more information about this function, please refer to the
-	@ref libconfini.
-
-	The parsing algorithms used by **libconfini** are able to parse any type of file
-	encoded in 8-bit code units, as long as the characters that match the regular
-	expression `/[\s\[\]\.\\;#"']/` refer to the same code points they refer to in
-	ASCII (as they do, for example, in UTF-8 and ISO-8859-1), independently of
-	platform-specific conventions.
-
-	@note	In order to be null-byte-injection-safe, before dispatching the parsed
-			content this function will strip all `NUL` characters possibly present
-			in the buffer (with the exception of the last one).
-
-	Possible return values are: #CONFINI_SUCCESS, #CONFINI_IINTR, #CONFINI_FEINTR,
-	#CONFINI_EOOR.
-
-	@include topics/strip_ini_cache.c
-
-**/
-int strip_ini_cache (
-	register char * const ini_source,
-	const size_t ini_length,
-	const IniFormat format,
-	const IniStatsHandler f_init,
-	const IniDispHandler f_foreach,
-	void * const user_data
-) {
-
-	const _LIBCONFINI_CHARBOOL_ valid_delimiter = !_LIBCONFINI_IS_ESC_CHAR_(format.delimiter_symbol, format);
-	_LIBCONFINI_CHARBOOL_ tmp_bool;
-	register size_t idx, tmp_fast_size_t_1, tmp_fast_size_t_2;
-	size_t tmp_size_t_1, tmp_size_t_2;
-
-	ini_source[ini_length] = '\0';
-
-	/*
-
-		PART ONE: Examine and isolate each segment
-
-	*/
-
-	#define __ISNT_ESCAPED__ tmp_bool
-	#define __LSHIFT__ tmp_fast_size_t_1
-	#define __EOL_N__ tmp_fast_size_t_2
-	#define __N_MEMBERS__ tmp_size_t_1
-	#define __NODE_AT__ tmp_size_t_2
-
-	/*  UTF-8 BOM  */
-	__LSHIFT__	=	*((unsigned char *) ini_source) == 0xEF &&
-					*((unsigned char *) ini_source + 1) == 0xBB &&
-					*((unsigned char *) ini_source + 2) == 0xBF
-					? 3 : 0;
-
-
-	for (
-
-		__N_MEMBERS__ = 0,
-		__EOL_N__ = _LIBCONFINI_EOL_IDX_,
-		__ISNT_ESCAPED__ = _LIBCONFINI_TRUE_,
-		__NODE_AT__ = 0,
-		idx = __LSHIFT__;
-
-			idx < ini_length;
-
-		idx++
-
-	) {
-
-		ini_source[idx - __LSHIFT__] = ini_source[idx];
-
-		if (ini_source[idx] == _LIBCONFINI_SPACES_[__EOL_N__] || ini_source[idx] == _LIBCONFINI_SPACES_[__EOL_N__ ^= 1]) {
-
-			if (format.multiline_nodes == INI_NO_MULTILINE || __ISNT_ESCAPED__) {
-
-				ini_source[idx - __LSHIFT__] = '\0';
-				__N_MEMBERS__ += further_cuts(ini_source + qultrim_h(ini_source, __NODE_AT__, format), format);
-				__NODE_AT__ = idx - __LSHIFT__ + 1;
-
-			} else if (ini_source[idx + 1] == _LIBCONFINI_SPACES_[__EOL_N__ ^ 1]) {
-
-				idx++;
-				ini_source[idx - __LSHIFT__] = ini_source[idx];
-
-			}
-
-			__ISNT_ESCAPED__ = _LIBCONFINI_TRUE_;
-
-		} else if (ini_source[idx] == _LIBCONFINI_BACKSLASH_) {
-
-			__ISNT_ESCAPED__ = !__ISNT_ESCAPED__;
-
-		} else if (ini_source[idx]) {
-
-			__ISNT_ESCAPED__ = _LIBCONFINI_TRUE_;
-
-		} else {
-
-			/*  Remove `NUL` characters from the buffer (if any)  */
-			__LSHIFT__++;
-
-		}
-
-	}
-
-	const size_t real_length = idx - __LSHIFT__;
-
-	while (idx > real_length) {
-
-		ini_source[--idx] = '\0';
-
-	}
-
-	__N_MEMBERS__ += further_cuts(ini_source + qultrim_h(ini_source, __NODE_AT__, format), format);
-
-	/*  Debug  */
-
-	/*
-
-	for (size_t tmp = 0; tmp < ini_length + 1; tmp++) {
-		putchar(ini_source[tmp] == 0 ? '$' : ini_source[tmp]);
-	}
-	putchar('\n');
-
-	*/
-
-	IniStatistics this_doc = {
-		.format = format,
-		.bytes = ini_length,
-		.members = __N_MEMBERS__
-	};
-
-	if (f_init && f_init(&this_doc, user_data)) {
-
-		return CONFINI_IINTR;
-
-	}
-
-	#undef __NODE_AT__
-	#undef __N_MEMBERS__
-	#undef __EOL_N__
-	#undef __LSHIFT__
-	#undef __ISNT_ESCAPED__
-
-	/*
-
-		PART TWO: Dispatch the parsed input
-
-	*/
-
-	if (!f_foreach) {
-
-		return CONFINI_SUCCESS;
-
-	}
-
-	#define __ITER__ tmp_fast_size_t_1
-	#define __NODE_AT__ tmp_fast_size_t_2
-	#define __PARENT_IS_DISABLED__ tmp_bool
-	#define __REAL_PARENT_LEN__ tmp_size_t_1
-	#define __CURR_PARENT_LEN__ tmp_size_t_2
-
-	__REAL_PARENT_LEN__ = 0, __CURR_PARENT_LEN__ = 0;
-
-	char
-		* curr_parent_str = ini_source + real_length,
-		* subparent_str = curr_parent_str,
-		* real_parent_str = curr_parent_str;
-
-	IniDispatch dsp = {
-		.format = format,
-		.dispatch_id = 0
-	};
-
-	__PARENT_IS_DISABLED__ = _LIBCONFINI_FALSE_;
-
-	for (__NODE_AT__ = 0, idx = 0; idx <= real_length; idx++) {
-
-		if (ini_source[idx]) {
-
-			continue;
-
-		}
-
-		if (!ini_source[__NODE_AT__] || _LIBCONFINI_IS_IGN_MARKER_(ini_source[__NODE_AT__], format)) {
-
-			__NODE_AT__ = idx + 1;
-			continue;
-
-		}
-
-		if (dsp.dispatch_id >= this_doc.members) {
-
-			return CONFINI_EOOR;
-
-		}
-
-		dsp.data = ini_source + __NODE_AT__;
-		dsp.d_len = idx - __NODE_AT__;
-
-		/*  Set `dsp.value` to an empty string  */
-		dsp.value = ini_source + idx;
-
-		if (
-			_LIBCONFINI_IS_DIS_MARKER_(*dsp.data, format) && (
-				format.disabled_after_space || !is_some_space(dsp.data[1], _LIBCONFINI_NO_EOL_)
-			)
-		) {
-
-			__ITER__ = dqultrim_s(dsp.data, 0, format);
-
-			dsp.type = get_type_as_active(
-				dsp.data + __ITER__,
-				dsp.d_len - __ITER__,
-				format.disabled_can_be_implicit,
-				format
-			);
-
-			if (dsp.type) {
-
-				dsp.data += __ITER__;
-				dsp.d_len -= __ITER__;
-
-				/*
-
-				// Not strictly needed...
-				for (; __ITER__ > 0; dsp.data[--__ITER__] = '\0');
-
-				*/
-
-			}
-
-			dsp.type |= 4;
-
-		} else {
-
-			switch (*dsp.data) {
-
-				default:
-
-					if (!_LIBCONFINI_IS_ANY_MARKER_(*dsp.data, format)) {
-
-						dsp.type = get_type_as_active(dsp.data, dsp.d_len, _LIBCONFINI_TRUE_, format);
-						break;
-
-					}
-
-					/*
-
-						No case break here, keep it like this!
-						`case _LIBCONFINI_SC_INT_MARKER_` must follow
-						(switch case fallthrough).
-
-					*/
-                    __attribute__((fallthrough));
-
-				case _LIBCONFINI_SC_INT_MARKER_:
-
-					/*
-
-					// Not strictly needed...
-					*dsp.data = '\0';
-
-					*/
-
-					dsp.type = INI_COMMENT;
-
-					break;
-
-				case _LIBCONFINI_IC_INT_MARKER_:
-
-					/*
-
-					// Not strictly needed...
-					*dsp.data = '\0';
-
-					*/
-
-					dsp.type = INI_INLINE_COMMENT;
-					/*  No case break here (last case)  */
-
-			}
-
-		}
-
-		if (__CURR_PARENT_LEN__ && *subparent_str) {
-
-			__ITER__ = 0;
-
-			do {
-
-				curr_parent_str[__ITER__ + __CURR_PARENT_LEN__] = subparent_str[__ITER__];
-
-			} while (subparent_str[__ITER__++]);
-
-			__CURR_PARENT_LEN__ += __ITER__ - 1;
-			subparent_str = curr_parent_str + __CURR_PARENT_LEN__;
-
-		}
-
-		if (__PARENT_IS_DISABLED__ && !(dsp.type & 4)) {
-
-			real_parent_str[__REAL_PARENT_LEN__] = '\0';
-			__CURR_PARENT_LEN__ = __REAL_PARENT_LEN__;
-			curr_parent_str = real_parent_str;
-			__PARENT_IS_DISABLED__ = _LIBCONFINI_FALSE_;
-
-		} else if (!__PARENT_IS_DISABLED__ && dsp.type == INI_DISABLED_SECTION) {
-
-			__REAL_PARENT_LEN__ = __CURR_PARENT_LEN__;
-			real_parent_str = curr_parent_str;
-			__PARENT_IS_DISABLED__ = _LIBCONFINI_TRUE_;
-
-		}
-
-		dsp.append_to = curr_parent_str;
-		dsp.at_len = __CURR_PARENT_LEN__;
-
-		if (dsp.type == INI_COMMENT || dsp.type == INI_INLINE_COMMENT) {
-
-			dsp.d_len = uncomment(++dsp.data, dsp.d_len - 1, format);
-
-		} else if (format.multiline_nodes != INI_NO_MULTILINE) {
-
-			dsp.d_len = unescape_cr_lf(dsp.data, dsp.d_len, dsp.type & 4, format);
-
-		}
-
-		switch (dsp.type) {
-
-			/*
-
-			case INI_UNKNOWN:
-
-				// Do nothing
-
-				break;
-
-			*/
-
-			case INI_SECTION:
-			case INI_DISABLED_SECTION:
-
-				*dsp.data++ = '\0';
-				__ITER__ = getn_metachar_pos(dsp.data, _LIBCONFINI_CLOSE_SECTION_, dsp.d_len, format);
-
-				while (dsp.data[__ITER__]) {
-
-					dsp.data[__ITER__++] = '\0';
-
-				}
-
-				dsp.d_len	=	format.section_paths == INI_ONE_LEVEL_ONLY ?
-									collapse_everything(dsp.data, format)
-								:
-									sanitize_section_path(dsp.data, format);
-
-
-				if (format.section_paths == INI_ONE_LEVEL_ONLY || *dsp.data != _LIBCONFINI_SUBSECTION_) {
-
-					/*
-
-						Append to root (this is an absolute path)
-
-					*/
-
-					curr_parent_str = dsp.data;
-					__CURR_PARENT_LEN__ = dsp.d_len;
-					subparent_str = ini_source + idx;
-					dsp.append_to = subparent_str;
-					dsp.at_len = 0;
-
-				} else if (format.section_paths == INI_ABSOLUTE_ONLY || !__CURR_PARENT_LEN__) {
-
-					/*
-
-						Append to root and remove the leading dot (parent is root or
-						relative paths are not allowed)
-
-					*/
-
-					curr_parent_str = ++dsp.data;
-					__CURR_PARENT_LEN__ = --dsp.d_len;
-					subparent_str = ini_source + idx;
-					dsp.append_to = subparent_str;
-					dsp.at_len = 0;
-
-				} else if (dsp.d_len != 1) {
-
-					/*
-
-						Append to the current parent (this is a relative path
-						and parent is not root)
-
-					*/
-
-					subparent_str = dsp.data;
-
-				}
-
-				if (INI_GLOBAL_LOWERCASE_MODE && !format.case_sensitive) {
-
-					string_tolower(dsp.data);
-
-				}
-
-				break;
-
-			case INI_KEY:
-			case INI_DISABLED_KEY:
-
-				if (
-					valid_delimiter && (
-						__ITER__ = getn_metachar_pos(dsp.data, (char) dsp.format.delimiter_symbol, dsp.d_len, format)
-					) < dsp.d_len
-				) {
-
-					dsp.data[__ITER__] = '\0';
-					dsp.value = dsp.data + __ITER__ + 1;
-
-
-					switch ((format.preserve_empty_quotes << 1) | format.do_not_collapse_values) {
-
-						case 0:	dsp.v_len = collapse_everything(dsp.value, format); break;
-
-						case 1:	dsp.v_len = collapse_empty_quotes(dsp.value, format); break;
-
-						case 2:	dsp.v_len = collapse_spaces(dsp.value, format); break;
-
-						case 4:
-
-							dsp.value += ltrim_h(dsp.value, 0, _LIBCONFINI_WITH_EOL_);
-							dsp.v_len = rtrim_h(dsp.value, dsp.d_len + dsp.data - dsp.value, _LIBCONFINI_WITH_EOL_);
-							/*  No case break here (last case)  */
-
-					}
-
-				} else if (format.implicit_is_not_empty) {
-
-					dsp.value = INI_GLOBAL_IMPLICIT_VALUE;
-					dsp.v_len = INI_GLOBAL_IMPLICIT_V_LEN;
-
-				}
-
-				dsp.d_len = collapse_everything(dsp.data, format);
-
-				if (INI_GLOBAL_LOWERCASE_MODE && !format.case_sensitive) {
-
-					string_tolower(dsp.data);
-
-				}
-
-				break;
-
-			case INI_COMMENT:
-			case INI_INLINE_COMMENT:
-
-				dsp.append_to = ini_source + idx;
-				dsp.at_len = 0;
-				/*  No case break here (last case)  */
-
-		}
-
-		if (f_foreach(&dsp, user_data)) {
-
-			return CONFINI_FEINTR;
-
-		}
-
-		dsp.dispatch_id++;
-		__NODE_AT__ = idx + 1;
-
-	}
-
-	#undef __CURR_PARENT_LEN__
-	#undef __REAL_PARENT_LEN__
-	#undef __PARENT_IS_DISABLED__
-	#undef __NODE_AT__
-	#undef __ITER__
-
-	return CONFINI_SUCCESS;
-
-}
-
-
-													/** @utility{load_ini_file} **/
-/**
-
-	@brief			Parses an INI file and dispatches its content to a custom
-					callback using a `FILE` structure as argument
-	@param			ini_file		The `FILE` handle pointing to the INI file to
-									parse
-	@param			format			The format of the INI file
-	@param			f_init			The function that will be invoked before the
-									first dispatch, or `NULL`
-	@param			f_foreach		The function that will be invoked for each
-									dispatch, or `NULL`
-	@param			user_data		A custom argument, or `NULL`
-	@return			Zero for success, otherwise an error code (see `enum`
-					#ConfiniInterruptNo)
-
-	@note	This function is absent if the `--without-io-api` option was passed to
-			the `configure` script when the library was compiled
-
-	The @p ini_file parameter must be a `FILE` handle with read privileges. On some
-	platforms, such as Microsoft Windows, it might be needed to add the binary
-	specifier to the mode string (`"b"`) in order to prevent discrepancies between
-	the physical size of the file and its computed size:
-
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
-	FILE * my_file = fopen("example.conf", "rb");
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	For the two parameters @p f_init and @p f_foreach see function
-	#strip_ini_cache().
-
-	The parsing algorithms used by **libconfini** are able to parse any type of file
-	encoded in 8-bit code units, as long as the characters that match the regular
-	expression `/[\s\[\]\.\\;#"']/` refer to the same code points they refer to in
-	ASCII (as they do, for example, in UTF-8 and ISO-8859-1), independently of
-	platform-specific conventions.
-
-	@note	In order to be null-byte-injection safe, `NUL` characters, if present in
-			the file, will be removed from the dispatched strings.
-
-	Possible return values are: #CONFINI_SUCCESS, #CONFINI_IINTR, #CONFINI_FEINTR,
-	#CONFINI_ENOMEM, #CONFINI_EIO, #CONFINI_EOOR, #CONFINI_EBADF, #CONFINI_EFBIG.
-
-	@include topics/load_ini_file.c
-
-**/
-int load_ini_file (
-	FILE * const ini_file,
-	const IniFormat format,
-	const IniStatsHandler f_init,
-	const IniDispHandler f_foreach,
-	void * const user_data
-) {
-
-	_LIBCONFINI_OFF_T_ file_size;
-
-	if (_LIBCONFINI_SEEK_EOF_(ini_file) || (file_size = _LIBCONFINI_FTELL_(ini_file)) < 0) {
-
-		return CONFINI_EBADF;
-
-	}
-
-	if ((size_t) file_size > SIZE_MAX) {
-
-		return CONFINI_EFBIG;
-
-	}
-
-	char * const cache = (char *) malloc((size_t) file_size + 1);
-
-	if (!cache) {
-
-		return CONFINI_ENOMEM;
-
-	}
-
-	rewind(ini_file);
-
-	if (fread(cache, 1, (size_t) file_size, ini_file) < (size_t) file_size) {
-
-		free(cache);
-		return CONFINI_EIO;
-
-	}
-
-	const int return_value = strip_ini_cache(cache, (size_t) file_size, format, f_init, f_foreach, user_data);
-
-	free(cache);
-	return return_value;
-
-}
-
-
-													/** @utility{load_ini_path} **/
-/**
-
-	@brief			Parses an INI file and dispatches its content to a custom
-					callback using a path as argument
-	@param			path			The path of the INI file
-	@param			format			The format of the INI file
-	@param			f_init			The function that will be invoked before the
-									first dispatch, or `NULL`
-	@param			f_foreach		The function that will be invoked for each
-									dispatch, or `NULL`
-	@param			user_data		A custom argument, or `NULL`
-	@return			Zero for success, otherwise an error code (see `enum`
-					#ConfiniInterruptNo)
-
-	@note	This function is absent if the `--without-io-api` option was passed to
-			the `configure` script when the library was compiled
-
-	For the two parameters @p f_init and @p f_foreach see function
-	#strip_ini_cache().
-
-	The parsing algorithms used by **libconfini** are able to parse any type of file
-	encoded in 8-bit code units, as long as the characters that match the regular
-	expression `/[\s\[\]\.\\;#"']/` refer to the same code points they refer to in
-	ASCII (as they do, for example, in UTF-8 and ISO-8859-1), independently of
-	platform-specific conventions.
-
-	@note	In order to be null-byte-injection safe, `NUL` characters, if present in
-			the file, will be removed from the dispatched strings.
-
-	Possible return values are: #CONFINI_SUCCESS, #CONFINI_IINTR, #CONFINI_FEINTR,
-	#CONFINI_ENOENT, #CONFINI_ENOMEM, #CONFINI_EIO, #CONFINI_EOOR, #CONFINI_EBADF,
-	#CONFINI_EFBIG.
-
-	@include topics/load_ini_path.c
-
-**/
-int load_ini_path (
-	const char * const path,
-	const IniFormat format,
-	const IniStatsHandler f_init,
-	const IniDispHandler f_foreach,
-	void * const user_data
-) {
-
-	FILE * const ini_file = fopen(path, "rb");
-
-	if (!ini_file) {
-
-		return CONFINI_ENOENT;
-
-	}
-
-	_LIBCONFINI_OFF_T_ file_size;
-
-	if (_LIBCONFINI_SEEK_EOF_(ini_file) || (file_size = _LIBCONFINI_FTELL_(ini_file)) < 0) {
-
-		return CONFINI_EBADF;
-
-	}
-
-	if ((size_t) file_size > SIZE_MAX) {
-
-		return CONFINI_EFBIG;
-
-	}
-
-	char * const cache = (char *) malloc((size_t) file_size + 1);
-
-	if (!cache) {
-
-		return CONFINI_ENOMEM;
-
-	}
-
-	rewind(ini_file);
-
-	if (fread(cache, 1, (size_t) file_size, ini_file) < (size_t) file_size) {
-
-		free(cache);
-		return CONFINI_EIO;
-
-	}
-
-	/*  No checks here, as there is nothing we can do about it...  */
-	fclose(ini_file);
-
-	const int return_value = strip_ini_cache(cache, (size_t) file_size, format, f_init, f_foreach, user_data);
-
-	free(cache);
-	return return_value;
-
-}
-
-
-
-		/*  OTHER UTILITIES (NOT REQUIRED BY LIBCONFINI'S MAIN FUNCTIONS)  */
-
-
-											/** @utility{ini_string_match_ss} **/
-/**
-
-	@brief			Compares two simple strings and checks whether they match
-	@param			simple_string_a		The first simple string
-	@param			simple_string_b		The second simple string
-	@param			format				The format of the INI file
-	@return			A boolean: `true` if the two strings match, `false` otherwise
-
-	Simple strings are user-given strings or the result of #ini_string_parse(). The
-	@p format argument is used for the following fields:
-
-	- `format.case_sensitive`
-
-**/
-bool ini_string_match_ss (
-	const char * const simple_string_a,
-	const char * const simple_string_b,
-	const IniFormat format
-) {
-
-	register size_t idx = 0;
-
-	if (format.case_sensitive) {
-
-		do {
-
-			if (simple_string_a[idx] != simple_string_b[idx]) {
-
-				return _LIBCONFINI_FALSE_;
-
-			}
-
-		} while (simple_string_a[idx++]);
-
-		return _LIBCONFINI_TRUE_;
-
-	}
-
-	do {
-
-		if (_LIBCONFINI_CHR_CASEFOLD_(simple_string_a[idx]) != _LIBCONFINI_CHR_CASEFOLD_(simple_string_b[idx])) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-	} while (simple_string_a[idx++]);
-
-	return _LIBCONFINI_TRUE_;
-
-}
-
-
-											/** @utility{ini_string_match_si} **/
-/**
-
-	@brief			Compares a simple string and an INI string and and checks
-					whether they match
-	@param			ini_string		The INI string escaped according to
-									@p format
-	@param			simple_string	The simple string
-	@param			format			The format of the INI file
-	@return			A boolean: `true` if the two strings match, `false` otherwise
-
-	This function grants that the result of the comparison between a simple string
-	and an INI string
-
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
-	printf(
-		"%s\n",
-		ini_string_match_si(my_simple_string, my_ini_string, format) ?
-			"They match"
-		:
-			"They don't match"
-	);
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	will always match the result of the _literal_ comparison between the simple
-	string and the INI string after the latter has been parsed by
-	#ini_string_parse() when `format.do_not_collapse_values` is set to `false`.
-
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
-	ini_string_parse(my_ini_string, format);
-
-	printf(
-		"%s\n",
-		ini_string_match_ss(my_simple_string, my_ini_string, format) ?
-			"They match"
-		:
-			"They don't match"
-	);
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	INI strings are the strings typically dispatched by #load_ini_file(),
-	#load_ini_path() or #strip_ini_cache(), which may contain quotes and the three
-	escape sequences `\\`, `\'` and `\"`. Simple strings are user-given strings or
-	the result of #ini_string_parse().
-
-	In order to be suitable for both names and values, **this function always
-	considers sequences of one or more spaces out of quotes in the INI string as
-	collapsed**, even when `format.do_not_collapse_values` is set to `true`.
-
-	The @p format argument is used for the following fields:
-
-	- `format.case_sensitive`
-	- `format.no_double_quotes`
-	- `format.no_single_quotes`
-	- `format.multiline_nodes`
-
-	@include topics/ini_string_match_si.c
-
-**/
-bool ini_string_match_si (
-	const char * const simple_string,
-	const char * const ini_string,
-	const IniFormat format
-) {
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Format supports escape sequences (const)
-		FLAG_8		Unescaped single quotes are odd right now
-		FLAG_16		Unescaped double quotes are odd right now
-		FLAG_32		This is an escaped single/double quote in a format that supports
-					single/double quotes
-		FLAG_64		This is a space
-		FLAG_128	Skip this character
-
-	*/
-
-	register uint8_t abcd	=	INIFORMAT_HAS_NO_ESC(format) ?
-									67
-								:
-									68 | (format.no_double_quotes << 1) | format.no_single_quotes;
-
-	register size_t idx_i = 0;
-	size_t idx_s = 0, nbacksl = 0;
-
-
-	/* \                                /\
-	\ */     si_match:                 /* \
-	 \/     ______________________     \ */
-
-
-	if ((abcd & 4) && ini_string[idx_i] == _LIBCONFINI_BACKSLASH_) {
-
-		for (abcd &= 63, nbacksl++; ini_string[++idx_i] == _LIBCONFINI_BACKSLASH_; nbacksl++);
-
-	}
-
-	abcd	=	!(abcd & 10) && ini_string[idx_i] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-					(
-						nbacksl & 1 ?
-							(abcd & 63) | 32
-						:
-							((abcd & 223) | 128) ^ 16
-					)
-				: !(abcd & 17) && ini_string[idx_i] == _LIBCONFINI_SINGLE_QUOTES_ ?
-					(
-						nbacksl & 1 ?
-							(abcd & 63) | 32
-						:
-							((abcd & 223) | 128) ^ 8
-					)
-				: (abcd & 24) || !is_some_space(ini_string[idx_i], _LIBCONFINI_WITH_EOL_) ?
-					abcd & 31
-				: abcd & 64 ?
-					(abcd & 223) | 128
-				:
-					(abcd & 95) | 64;
-
-
-	if (nbacksl) {
-
-		nbacksl = ((abcd & 32 ? nbacksl : nbacksl + 1) >> 1) + 1;
-
-		while (--nbacksl) {
-
-			if (simple_string[idx_s++] != _LIBCONFINI_BACKSLASH_) {
-
-				return _LIBCONFINI_FALSE_;
-
-			}
-
-		}
-
-	}
-
-	if ((abcd & 128) || ((abcd & 64) && !simple_string[idx_s])) {
-
-		idx_i++;
-		goto si_match;
-
-	}
-
-	if (
-		abcd & 64 ?
-			simple_string[idx_s] != _LIBCONFINI_COLLAPSED_ || !simple_string[idx_s + 1]
-		: format.case_sensitive ?
-			ini_string[idx_i] != simple_string[idx_s]
-		:
-			_LIBCONFINI_CHR_CASEFOLD_(ini_string[idx_i]) != _LIBCONFINI_CHR_CASEFOLD_(simple_string[idx_s])
-	) {
-
-		return _LIBCONFINI_FALSE_;
-
-	}
-
-	idx_s++;
-
-	if (ini_string[idx_i++]) {
-
-		goto si_match;
-
-	}
-
-	return _LIBCONFINI_TRUE_;
-
-}
-
-
-											/** @utility{ini_string_match_ii} **/
-/**
-
-	@brief			Compares two INI strings and checks whether they match
-	@param			ini_string_a	The first INI string unescaped according to
-									@p format
-	@param			ini_string_b	The second INI string unescaped according to
-									@p format
-	@param			format			The format of the INI file
-	@return			A boolean: `true` if the two strings match, `false` otherwise
-
-	This function grants that the result of the comparison between two INI strings
-
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
-	printf(
-		"%s\n",
-		ini_string_match_ii(my_ini_string_1, my_ini_string_2, format) ?
-			"They match"
-		:
-			"They don't match"
-	);
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	will always match the result of the _literal_ comparison between the same two
-	INI strings after these have been parsed by #ini_string_parse() when
-	`format.do_not_collapse_values` is set to `false`.
-
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~{.c}
-	ini_string_parse(my_ini_string_1, format);
-	ini_string_parse(my_ini_string_2, format);
-
-	printf("%s\n",
-		ini_string_match_ss(my_ini_string_1, my_ini_string_2, format) ?
-			"They match"
-		:
-			"They don't match"
-	);
-	~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-	INI strings are the strings typically dispatched by #load_ini_file(),
-	#load_ini_path() or #strip_ini_cache(), which may contain quotes and the three
-	escape sequences `\\`, `\'` and `\"`.
-
-	In order to be suitable for both names and values, **this function always
-	considers sequences of one or more spaces out of quotes in both strings as
-	collapsed**, even when `format.do_not_collapse_values` is set to `true`.
-
-	The @p format argument is used for the following fields:
-
-	- `format.case_sensitive`
-	- `format.no_double_quotes`
-	- `format.no_single_quotes`
-	- `format.multiline_nodes`
-
-**/
-bool ini_string_match_ii (
-	const char * const ini_string_a,
-	const char * const ini_string_b,
-	const IniFormat format
-) {
-
-	const _LIBCONFINI_CHARBOOL_ has_escape = !INIFORMAT_HAS_NO_ESC(format);
-	register uint8_t side = 1;
-	register _LIBCONFINI_CHARBOOL_ turn_allowed = _LIBCONFINI_TRUE_;
-	uint8_t abcd_pair[2];
-	const char * chrptr_pair[2] = { ini_string_a, ini_string_b };
-	size_t nbacksl_pair[2];
-
-	/*
-
-	Masks `abcd_pair[0]` and  `abcd_pair[1]` (7 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes and format supports
-					escape sequences
-		FLAG_32		This is a space
-		FLAG_64		Skip this character
-
-	*/
-
-	abcd_pair[1] = *abcd_pair = 32 | (format.no_double_quotes << 1) | format.no_single_quotes;
-
-
-	/* \                                /\
-	\ */     ii_match:                 /* \
-	 \/     ______________________     \ */
-
-
-	nbacksl_pair[side] = 0;
-
-	if (has_escape && *chrptr_pair[side] == _LIBCONFINI_BACKSLASH_) {
-
-		for (nbacksl_pair[side]++; *(++chrptr_pair[side]) == _LIBCONFINI_BACKSLASH_; nbacksl_pair[side]++);
-
-		abcd_pair[side] = nbacksl_pair[side] & 1 ? (abcd_pair[side] & 31) | 16 : abcd_pair[side] & 15;
-
-		if (
-			(
-				(abcd_pair[side] & 9) || *chrptr_pair[side] != _LIBCONFINI_SINGLE_QUOTES_
-			) && (
-				(abcd_pair[side] & 6) || *chrptr_pair[side] != _LIBCONFINI_DOUBLE_QUOTES_
-			)
-		) {
-
-			nbacksl_pair[side]++;
-
-		}
-
-	} else {
-
-		abcd_pair[side]	=	!(abcd_pair[side] & 25) && *chrptr_pair[side] == _LIBCONFINI_SINGLE_QUOTES_ ?
-								((abcd_pair[side] & 111) | 64) ^ 4
-							: !(abcd_pair[side] & 22) && *chrptr_pair[side] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-								((abcd_pair[side] & 111) | 64) ^ 8
-							: !(abcd_pair[side] & 12) && is_some_space(*chrptr_pair[side], _LIBCONFINI_WITH_EOL_) ?
-								(abcd_pair[side] & 111) | 96
-							: *chrptr_pair[side] ?
-								abcd_pair[side] & 47
-							:
-								abcd_pair[side] & 15;
-
-
-		if (abcd_pair[side] & 64) {
-
-			chrptr_pair[side]++;
-			goto ii_match;
-
-		}
-
-	}
-
-	if (side && turn_allowed) {
-
-		side ^= 1;
-		goto ii_match;
-
-	}
-
-	turn_allowed = _LIBCONFINI_TRUE_;
-
-	if (*nbacksl_pair || nbacksl_pair[1]) {
-
-		if (*nbacksl_pair >> 1 != nbacksl_pair[1] >> 1) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-		side = 1;
-		goto ii_match;
-
-	}
-
-	if (
-		(
-			abcd_pair[side ^ 1] & 32 ?
-				!(abcd_pair[side] & 32)
-			:
-				abcd_pair[(side ^= 1) ^ 1] & 32
-		) && *chrptr_pair[side]
-	) {
-
-		if (*chrptr_pair[side]++ != _LIBCONFINI_COLLAPSED_) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-		abcd_pair[side ^ 1] &= 95;
-		turn_allowed = _LIBCONFINI_FALSE_;
-		goto ii_match;
-
-	}
-
-	if (
-		format.case_sensitive ?
-			**chrptr_pair != *chrptr_pair[1]
-		:
-			_LIBCONFINI_CHR_CASEFOLD_(**chrptr_pair) != _LIBCONFINI_CHR_CASEFOLD_(*chrptr_pair[1])
-	) {
-
-		return _LIBCONFINI_FALSE_;
-
-	}
-
-	if (**chrptr_pair) {
-
-		(*chrptr_pair)++;
-
-	}
-
-	if (*chrptr_pair[1]) {
-
-		chrptr_pair[1]++;
-
-	}
-
-	if (**chrptr_pair || *chrptr_pair[1]) {
-
-		*abcd_pair &= 95;
-		abcd_pair[1] &= 95;
-		side = 1;
-		goto ii_match;
-
-	}
-
-	return _LIBCONFINI_TRUE_;
-
-}
-
-
-												/** @utility{ini_array_match} **/
-/**
-
-	@brief			Compares two INI arrays and checks whether they match
-	@param			ini_string_a	The first INI array
-	@param			ini_string_b	The second INI array
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			A boolean: `true` if the two arrays match, `false` otherwise
-
-	This function grants that the result of the comparison between two INI arrays
-	will always match the the _literal_ comparison between the individual members
-	of both arrays after these have been parsed, one by one, by #ini_string_parse()
-	(with `format.do_not_collapse_values` set to `false`).
-
-	This function can be used, with `'.'` as delimiter, to compare section paths.
-
-	INI strings are the strings typically dispatched by #load_ini_file(),
-	#load_ini_path() or #strip_ini_cache(), which may contain quotes and the three
-	escape sequences `\\`, `\'` and `\"`.
-
-	In order to be suitable for both names and values, **this function always
-	considers sequences of one or more spaces out of quotes in both strings as
-	collapsed**, even when `format.do_not_collapse_values` is set to `true`.
-
-	The @p format argument is used for the following fields:
-
-	- `format.case_sensitive`
-	- `format.no_double_quotes`
-	- `format.no_single_quotes`
-	- `format.multiline_nodes`
-
-**/
-bool ini_array_match (
-	const char * const ini_string_a,
-	const char * const ini_string_b,
-	const char delimiter,
-	const IniFormat format
-) {
-
-	if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		/*
-
-			We have no delimiters (array has only one member)
-
-		*/
-
-		return ini_string_match_ii(ini_string_a, ini_string_b, format);
-
-	}
-
-	const _LIBCONFINI_CHARBOOL_ has_escape = !INIFORMAT_HAS_NO_ESC(format);
-	register uint8_t side = 1;
-	register _LIBCONFINI_CHARBOOL_ turn_allowed = _LIBCONFINI_TRUE_;
-	uint8_t abcd_pair[2];
-	size_t nbacksl_pair[2];
-	const char * chrptr_pair[2] = {
-		ini_string_a + ltrim_s(ini_string_a, 0, _LIBCONFINI_WITH_EOL_),
-		ini_string_b + ltrim_s(ini_string_b, 0, _LIBCONFINI_WITH_EOL_)
-	};
-
-	/*
-
-	Masks `abcd_pair[0]` and  `abcd_pair[1]` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		We are in an odd sequence of backslashes and format supports
-					escape sequences
-		FLAG_32		This is a space
-		FLAG_64		This is a delimiter
-		FLAG_128	Skip this character
-
-	*/
-
-	abcd_pair[1] = *abcd_pair = 32 | (format.no_double_quotes << 1) | format.no_single_quotes;
-
-
-	/* \                                /\
-	\ */     delimited_match:          /* \
-	 \/     ______________________     \ */
-
-
-	nbacksl_pair[side] = 0;
-
-	if (has_escape && *chrptr_pair[side] == _LIBCONFINI_BACKSLASH_) {
-
-		for (nbacksl_pair[side]++; *(++chrptr_pair[side]) == _LIBCONFINI_BACKSLASH_; nbacksl_pair[side]++);
-
-		abcd_pair[side] = nbacksl_pair[side] & 1 ? (abcd_pair[side] & 31) | 16 : abcd_pair[side] & 15;
-
-		if (
-			(
-				(abcd_pair[side] & 9) || *chrptr_pair[side] != _LIBCONFINI_SINGLE_QUOTES_
-			) && (
-				(abcd_pair[side] & 6) || *chrptr_pair[side] != _LIBCONFINI_DOUBLE_QUOTES_
-			)
-		) {
-
-			nbacksl_pair[side]++;
-
-		}
-
-	} else {
-
-		abcd_pair[side]	=	!(abcd_pair[side] & 12) && is_some_space(*chrptr_pair[side], _LIBCONFINI_WITH_EOL_) ?
-								(
-									delimiter || (abcd_pair[side] & 64) ?
-										(abcd_pair[side] & 239) | 160
-									:
-										(abcd_pair[side] & 111) | 96
-								)
-							: delimiter && !(abcd_pair[side] & 12) && *chrptr_pair[side] == delimiter ?
-								(abcd_pair[side] & 111) | 96
-							: !(abcd_pair[side] & 25) && *chrptr_pair[side] == _LIBCONFINI_SINGLE_QUOTES_ ?
-								((abcd_pair[side] & 175) | 128) ^ 4
-							: !(abcd_pair[side] & 22) && *chrptr_pair[side] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-								((abcd_pair[side] & 175) | 128) ^ 8
-							: *chrptr_pair[side] ?
-								abcd_pair[side] & 47
-							: delimiter ?
-								abcd_pair[side] & 15
-							:
-								(abcd_pair[side] & 79) ^ 64;
-
-
-		if (abcd_pair[side] & 128) {
-
-			chrptr_pair[side]++;
-			goto delimited_match;
-
-		}
-
-	}
-
-	if (side && turn_allowed) {
-
-		side ^= 1;
-		goto delimited_match;
-
-	}
-
-	turn_allowed = _LIBCONFINI_TRUE_;
-
-	if (*nbacksl_pair || nbacksl_pair[1]) {
-
-		if (*nbacksl_pair >> 1 != nbacksl_pair[1] >> 1) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-		side = 1;
-		goto delimited_match;
-
-	}
-
-	if ((*abcd_pair ^ abcd_pair[1]) & 64) {
-
-		return _LIBCONFINI_FALSE_;
-
-	}
-
-	if (
-		!(
-			abcd_pair[side ^ 1] & 32 ?
-				abcd_pair[side] & 96
-			:
-				(abcd_pair[(side ^= 1) ^ 1] & 96) ^ 32
-		) && *chrptr_pair[side]
-	) {
-
-		if (*chrptr_pair[side]++ != _LIBCONFINI_COLLAPSED_) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-		abcd_pair[side ^ 1] &= 223;
-		turn_allowed = _LIBCONFINI_FALSE_;
-		goto delimited_match;
-
-	}
-
-	if (~*abcd_pair & 64) {
-
-		if (
-			format.case_sensitive ?
-				**chrptr_pair != *chrptr_pair[1]
-			:
-				_LIBCONFINI_CHR_CASEFOLD_(**chrptr_pair) != _LIBCONFINI_CHR_CASEFOLD_(*chrptr_pair[1])
-		) {
-
-			return _LIBCONFINI_FALSE_;
-
-		}
-
-		*abcd_pair &= 223;
-		abcd_pair[1] &= 223;
-
-	}
-
-	if (**chrptr_pair) {
-
-		(*chrptr_pair)++;
-
-	}
-
-	if (*chrptr_pair[1]) {
-
-		chrptr_pair[1]++;
-
-	}
-
-	if (**chrptr_pair || *chrptr_pair[1]) {
-
-		side = 1;
-		goto delimited_match;
-
-	}
-
-	return _LIBCONFINI_TRUE_;
-
-}
-
-
-													/** @utility{ini_unquote} **/
-/**
-
-	@brief			Unescapes `\'`, `\"`, and `\\` and removes all unescaped quotes
-					(if single/double quotes are considered metacharacters in
-					respect to the format given)
-	@param			ini_string		The string to be unescaped
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-	This function is very similar to #ini_string_parse(), except that does not
-	bother collapsing the sequences of more than one space that might result from
-	removing empty quotes. Its purpose is to be used to parse key and section names,
-	since these are always dispatched as already collapsed. In order to parse
-	values, or array parts listed in values, please use #ini_string_parse().
-
-	If you only need to compare @p ini_string with another string, consider to use
-	#ini_string_match_si() and #ini_string_match_ii() instead of parsing the former
-	and perform a simple comparison afterwards. These two functions are in fact able
-	to check directly for equality between unparsed INI strings without actually
-	modifiyng them.
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well). If the string does not contain quotes, or if quotes are
-	considered to be normal characters, no changes will be made.
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE this function is
-			no-op and will only return the value of #INI_GLOBAL_IMPLICIT_V_LEN minus
-			the offset of @p ini_string within #INI_GLOBAL_IMPLICIT_VALUE.
-
-	The @p format argument is used for the following fields:
-
-	- `format.no_single_quotes`
-	- `format.no_double_quotes`
-	- `format.multiline_nodes`
-
-	@include topics/ini_string_parse.c
-
-**/
-size_t ini_unquote (char * const ini_string, const IniFormat format) {
-
-	if (_LIBCONFINI_IMPLICIT_RANGE_(ini_string)) {
-
-		return INI_GLOBAL_IMPLICIT_V_LEN + INI_GLOBAL_IMPLICIT_VALUE - ini_string;
-
-	}
-
-	register size_t idx = 0;
-
-	if (INIFORMAT_HAS_NO_ESC(format)) {
-
-		/*
-
-			There are no escape sequences... I will just return the length of the
-			string...
-
-		*/
-
-		while (ini_string[idx++]);
-
-		return idx - 1;
-
-	}
-
-	size_t lshift = 0, nbacksl = 0;
-
-	/*
-
-	Mask `abcd` (6 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Unescaped single quotes are odd right now
-		FLAG_8		Unescaped double quotes are odd right now
-		FLAG_16		This is an unescaped single quote and format supports single
-					quotes
-		FLAG_32		This is an unescaped double quote and format supports double
-					quotes
-
-	*/
-
-	for (register uint8_t abcd = (format.no_double_quotes << 1) | format.no_single_quotes; ini_string[idx]; idx++) {
-
-		abcd	=	!(abcd & 6) && ini_string[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(abcd & 47) | 32
-					: !(abcd & 9) && ini_string[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(abcd & 31) | 16
-					:
-						abcd & 15;
-
-
-		if (!(nbacksl & 1) && (abcd & 48)) {
-
-			abcd ^= (abcd >> 2) & 12;
-			lshift++;
-			continue;
-
-		}
-
-		if (ini_string[idx] == _LIBCONFINI_BACKSLASH_) {
-
-			nbacksl++;
-
-		} else {
-
-			if ((nbacksl & 1) && (abcd & 48)) {
-
-				lshift++;
-
-			}
-
-			lshift += nbacksl >> 1;
-			nbacksl = 0;
-
-		}
-
-		if (lshift) {
-
-			ini_string[idx - lshift] = ini_string[idx];
-
-		}
-
-	}
-
-	lshift += nbacksl >> 1;
-
-	for (idx -= lshift; ini_string[idx]; ini_string[idx++] = '\0');
-
-	return idx - lshift;
-
-}
-
-
-												/** @utility{ini_string_parse} **/
-/**
-
-	@brief			Unescapes `\'`, `\"`, and `\\` and removes all unescaped quotes
-					(if single/double quotes are considered metacharacters in
-					respect to the format given); if the format allows it, sequences
-					of one or more spaces out of quotes will be collapsed
-	@param			ini_string		The string to be unescaped
-	@param			format			The format of the INI file
-	@return			The new length of the string
-
-	This function is meant to be used to parse values. In order to parse key and
-	section names please use #ini_unquote().
-
-	If you only need to compare @p ini_string with another string, consider to use
-	#ini_string_match_si() and #ini_string_match_ii() instead of parsing the former
-	and perform a simple comparison afterwards. These two functions are in fact able
-	to check directly for equality between unparsed INI strings without actually
-	modifying them.
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well). If `format.do_not_collapse_values` is set to non-zero, spaces
-	surrounding empty quotes will be collapsed together with the latter.
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE this function is
-			no-op and will only return the value of #INI_GLOBAL_IMPLICIT_V_LEN minus
-			the offset of @p ini_string within #INI_GLOBAL_IMPLICIT_VALUE.
-
-	The @p format argument is used for the following fields:
-
-	- `format.no_single_quotes`
-	- `format.no_double_quotes`
-	- `format.multiline_nodes`
-	- `format.do_not_collapse_values`
-
-	@note	`format.multiline_nodes` is used only to figure out whether there are
-			escape sequences or not. For all other purposes new line characters will
-			be considered to be equal to any other space character, even if the
-			format is not multi-line -- new line characters should never appear in
-			non-multi-line formats.
-
-	@include topics/ini_string_parse.c
-
-**/
-size_t ini_string_parse (char * const ini_string, const IniFormat format) {
-
-	if (_LIBCONFINI_IMPLICIT_RANGE_(ini_string)) {
-
-		return INI_GLOBAL_IMPLICIT_V_LEN + INI_GLOBAL_IMPLICIT_VALUE - ini_string;
-
-	}
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Do not collapse spaces within members (const)
-		FLAG_8		Unescaped single quotes are odd right now
-		FLAG_16		Unescaped double quotes are odd right now
-		FLAG_32		This is an *escaped* single/double quote and format supports
-					single/double quotes
-		FLAG_64		This is a space
-		FLAG_128	Skip this character
-
-	*/
-
-	register uint8_t abcd	=	(format.do_not_collapse_values ? 68 : 64) |
-								(format.no_double_quotes << 1) |
-								format.no_single_quotes;
-
-	size_t idx, lshift;
-
-	if (format.multiline_nodes == INI_NO_MULTILINE) {
-
-		switch (abcd) {
-
-			case 64 | 2 | 1 :
-
-				/*
-
-					There are no escape sequences, but spaces might still need to
-					be collapsed.
-
-				*/
-
-				for (idx = 0, lshift = 0; ini_string[idx]; idx++) {
-
-					abcd = !is_some_space(ini_string[idx], _LIBCONFINI_WITH_EOL_) ? 3 : abcd & 64 ? 195 : 67;
-
-					if (abcd & 128) {
-
-						lshift++;
-
-					} else {
-
-						ini_string[idx - lshift] = abcd & 64 ? _LIBCONFINI_COLLAPSED_ : ini_string[idx];
-
-					}
-
-				}
-
-				for (
-
-					idx	-=	(abcd & 64) && lshift < idx ?
-								++lshift
-							:
-								lshift;
-
-						ini_string[idx];
-
-					ini_string[idx++] = '\0'
-
-				);
-
-				return idx - lshift;
-
-			case 64 | 4 | 2 | 1 :
-
-				/*
-
-					There are no escape sequences and spaces don't need to be
-					collapsed, but left and right trim might still be necessary.
-
-				*/
-
-				return rtrim_h(ini_string, ltrim_hh(ini_string, 0, _LIBCONFINI_WITH_EOL_), _LIBCONFINI_WITH_EOL_);
-
-		}
-
-	}
-
-	/*
-
-		There might be escape sequences...
-
-	*/
-
-	size_t nbacksl = 0;
-
-	for (idx = lshift = 0; ini_string[idx]; idx++) {
-
-		abcd	=	!(abcd & 10) && ini_string[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							nbacksl & 1 ?
-								(abcd & 63) | 32
-							:
-								((abcd & 223) | 128) ^ 16
-						)
-					: !(abcd & 17) && ini_string[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							nbacksl & 1 ?
-								(abcd & 63) | 32
-							:
-								((abcd & 223) | 128) ^ 8
-						)
-					: (abcd & 28) || !is_some_space(ini_string[idx], _LIBCONFINI_WITH_EOL_) ?
-						abcd & 31
-					: abcd & 64 ?
-						(abcd & 223) | 128
-					:
-						(abcd & 95) | 64;
-
-
-		if (abcd & 128) {
-
-			lshift++;
-			continue;
-
-		}
-
-		if (ini_string[idx] == _LIBCONFINI_BACKSLASH_) {
-
-			nbacksl++;
-
-		} else {
-
-			if (abcd & 32) {
-
-				lshift++;
-
-			}
-
-			lshift += nbacksl >> 1;
-			nbacksl = 0;
-
-		}
-
-		ini_string[idx - lshift] = abcd & 64 ? _LIBCONFINI_COLLAPSED_ : ini_string[idx];
-
-	}
-
-	lshift += nbacksl >> 1;
-
-	for (
-
-		idx	-=	(abcd & 64) && lshift < idx ?
-					++lshift
-				:
-					lshift;
-
-			ini_string[idx];
-
-		ini_string[idx++] = '\0'
-
-	);
-
-	return	(abcd & 28) ^ 4 ?
-				idx - lshift
-			:
-				rtrim_h(ini_string, idx - lshift, _LIBCONFINI_WITH_EOL_);
-
-}
-
-
-											/** @utility{ini_array_get_length} **/
-/**
-
-	@brief			Gets the length of a stringified INI array in number of members
-	@param			ini_string		The stringified array (it can be `NULL`)
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			The length of the INI array
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-**/
-size_t ini_array_get_length (
-	const char * const ini_string,
-	const char delimiter,
-	const IniFormat format
-) {
-
-	if (!ini_string) {
-
-		return 0;
-
-	}
-
-	if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		/*
-
-			We have no delimiters (array has only one member)
-
-		*/
-
-		return 1;
-
-	}
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Delimiter is not any space (const)
-		FLAG_8		Unescaped single quotes are odd right now
-		FLAG_16		Unescaped double quotes are odd right now
-		FLAG_32		We are in an odd sequence of backslashes
-		FLAG_64		This is a space
-		FLAG_128	This is a delimiter
-
-	*/
-
-	register uint8_t abcd	=	(delimiter ? 64 : 68) |
-								(format.no_double_quotes << 1) |
-								format.no_single_quotes;
-
-	size_t counter = 0;
-
-	for (size_t idx = 0; ini_string[idx]; idx++) {
-
-		/*  Revision #1  */
-
-		abcd	=	!(abcd & 28) && ini_string[idx] == delimiter ?
-						(abcd & 159) | 128
-					: !(abcd & 24) && is_some_space(ini_string[idx], _LIBCONFINI_WITH_EOL_) ?
-						(
-							(abcd & 68) ^ 4 ?
-								(abcd & 95) | 64
-							:
-								(abcd & 223) | 192
-						)
-					: ini_string[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd & 63) ^ 32
-					: !(abcd & 42) && ini_string[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(abcd & 31) ^ 16
-					: !(abcd & 49) && ini_string[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(abcd & 31) ^ 8
-					:
-						abcd & 31;
-
-
-		if (abcd & 128) {
-
-			counter++;
-
-		}
-
-	}
-
-	return	!counter || (~abcd & 68) ?
-				counter + 1
-			:
-				counter;
-
-}
-
-
-												/** @utility{ini_array_foreach} **/
-/**
-
-	@brief			Calls a custom function for each member of a stringified INI
-					array, without modifying the content of the buffer -- useful for
-					read-only (`const`) stringified arrays
-	@param			ini_string		The stringified array (it can be `NULL`)
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@param			f_foreach		The function that will be invoked for each array
-									member
-	@param			user_data		A custom argument, or `NULL`
-	@return			Zero for success, otherwise an error code (see `enum`
-					#ConfiniInterruptNo)
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	The user given function @p f_foreach (see #IniSubstrHandler data type) will be
-	invoked with six arguments: `ini_string`, `memb_offset` (the offset of the
-	member in bytes), `memb_length` (the length of the member in bytes), `memb_num`
-	(the offset of the member in number of members), `format` (the format of the INI
-	file), `user_data` (the custom argument @p user_data previously passed). If
-	@p f_foreach returns a non-zero value the function #ini_array_foreach() will be
-	interrupted.
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	Possible return values are: #CONFINI_SUCCESS, #CONFINI_FEINTR.
-
-	@include topics/ini_array_foreach.c.
-
-**/
-int ini_array_foreach (
-	const char * const ini_string,
-	const char delimiter,
-	const IniFormat format,
-	const IniSubstrHandler f_foreach,
-	void * const user_data
-) {
-
-	if (!ini_string) {
-
-		return CONFINI_SUCCESS;
-
-	}
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Delimiter is not any space (const)
-		FLAG_8		Unescaped single quotes are odd until now
-		FLAG_16		Unescaped double quotes are odd until now
-		FLAG_32		We are in an odd sequence of backslashes
-		FLAG_64		This is not a delimiter
-		FLAG_128	Stop the loop
-
-	*/
-
-	register size_t idx;
-	size_t offs = ltrim_s(ini_string, 0, _LIBCONFINI_WITH_EOL_);
-
-	if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		/*
-
-			We have no delimiters (array has only one member)
-
-		*/
-
-		idx = 0;
-
-		while (ini_string[idx++]);
-
-		return	f_foreach(ini_string, offs, rtrim_s(ini_string + offs, idx - offs - 1, _LIBCONFINI_WITH_EOL_), 0, format, user_data) ?
-					CONFINI_FEINTR
-				:
-					CONFINI_SUCCESS;
-
-	}
-
-	register uint8_t abcd	=	(delimiter ? 4 : 0) |
-								(format.no_double_quotes << 1) |
-								format.no_single_quotes;
-
-	size_t memb_num = 0;
-
-	idx = offs;
-
-	do {
-
-		abcd	=	(delimiter ? ini_string[idx] == delimiter : is_some_space(ini_string[idx], _LIBCONFINI_WITH_EOL_)) ?
-						abcd & 159
-					: ini_string[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd | 64) ^ 32
-					: !(abcd & 42) && ini_string[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						((abcd & 223) | 64) ^ 16
-					: !(abcd & 49) && ini_string[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						((abcd & 223) | 64) ^ 8
-					: ini_string[idx] ?
-						(abcd & 223) | 64
-					:
-						128;
-
-
-		if (!(abcd & 88)) {
-
-			if (
-				f_foreach(
-					ini_string,
-					offs,
-					rtrim_s(ini_string + offs, idx - offs, _LIBCONFINI_WITH_EOL_),
-					memb_num++,
-					format,
-					user_data
-				)
-			) {
-
-				return CONFINI_FEINTR;
-
-			}
-
-			offs = abcd & 128 ? idx + 1 : ltrim_s(ini_string, idx + 1, _LIBCONFINI_WITH_EOL_);
-
-		}
-
-		idx = abcd & 216 ? idx + 1 : offs;
-
-	} while (!(abcd & 128) && ((abcd & 92) || ini_string[idx]));
-
-	return CONFINI_SUCCESS;
-
-}
-
-
-												/** @utility{ini_array_shift} **/
-/**
-
-	@brief			Shifts the location pointed by @p ini_strptr to the next member
-					of the INI array (without modifying the content of the buffer),
-					or to `NULL` if the INI array has no more members  -- useful for
-					read-only (`const`) stringified arrays
-	@param			ini_strptr		The memory location of the stringified array --
-									it cannot be `NULL`, but it can point to `NULL`
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			The length of the array member that has been left behind
-
-	Usually @p ini_strptr comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	@include topics/ini_array_shift.c
-
-**/
-size_t ini_array_shift (const char ** const ini_strptr, const char delimiter, const IniFormat format) {
-
-	size_t toklen = 0;
-
-	if (*ini_strptr && !_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		if (!delimiter) {
-
-			toklen = ltrim_s(*ini_strptr, 0, _LIBCONFINI_WITH_EOL_);
-
-		}
-
-		toklen += get_metachar_pos(*ini_strptr + toklen, delimiter, format);
-		*ini_strptr += toklen;
-		toklen = rtrim_s(*ini_strptr - toklen, toklen, _LIBCONFINI_WITH_EOL_);
-
-		if (**ini_strptr) {
-
-			*ini_strptr += ltrim_s(*ini_strptr, 1, _LIBCONFINI_WITH_EOL_);
-
-			if (delimiter || **ini_strptr) {
-
-				return toklen;
-
-			}
-
-		}
-
-	}
-
-	*ini_strptr = (char *) 0;
-	return toklen;
-
-}
-
-
-												/** @utility{ini_array_collapse} **/
-/**
-
-	@brief			Compresses the distribution of the data in a stringified INI
-					array by removing all the white spaces that surround its
-					delimiters, empty quotes, collapsable spaces, etc
-	@param			ini_string		The stringified array
-	@param			delimiter		The delimiter between the array members --
-									if zero (`INI_ANY_SPACE`) any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			The new length of the stringified array
-
-	Out of quotes similar to ECMAScript
-	`ini_string.replace(new RegExp("^\\s+|\\s*(?:(" + delimiter + ")\\s*|($))", "g"), "$1$2")`.
-	If #INI_ANY_SPACE (`0`) is used as delimiter, one or more different spaces
-	(`/[\t \v\f\n\r]+/`) will be always collapsed to one space, independently of
-	what the format says.
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	This function can be useful before invoking `memcpy()` using @p ini_string as
-	source, when saving memory is a priority.
-
-	The @p format argument is used for the following fields:
-
-	- `format.no_single_quotes`
-	- `format.no_double_quotes`
-	- `format.do_not_collapse_values`
-	- `format.preserve_empty_quotes`
-
-	Examples:
-
-	1. Using comma as delimiter:
-	   - Before: `&nbsp;first&nbsp;&nbsp; ,&nbsp;&nbsp;&nbsp; second&nbsp;&nbsp;
-	     ,&nbsp;&nbsp; third&nbsp;&nbsp; ,&nbsp; etc.&nbsp;&nbsp;`
-	   - After: `first,second,third,etc.`
-	2. Using `INI_ANY_SPACE` as delimiter:
-	   - Before: `&nbsp;&nbsp;first&nbsp;&nbsp;&nbsp; second&nbsp;&nbsp;&nbsp;
-	     third&nbsp;&nbsp;&nbsp;&nbsp; etc.&nbsp;&nbsp;&nbsp;`
-	   - After: `first second third etc.`
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE this function is
-			no-op and will only return the value of #INI_GLOBAL_IMPLICIT_V_LEN minus
-			the offset of @p ini_string within #INI_GLOBAL_IMPLICIT_VALUE.
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	@include topics/ini_array_collapse.c
-
-	@note	The actual space occupied by the array might get reduced further after
-			each member is parsed by #ini_string_parse().
-
-**/
-size_t ini_array_collapse (char * const ini_string, const char delimiter, const IniFormat format) {
-
-	if (_LIBCONFINI_IMPLICIT_RANGE_(ini_string)) {
-
-		return INI_GLOBAL_IMPLICIT_V_LEN + INI_GLOBAL_IMPLICIT_VALUE - ini_string;
-
-	}
-
-	if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		/*
-
-			We have no delimiters (array has only one member)
-
-		*/
-
-		switch ((format.preserve_empty_quotes << 1) | format.do_not_collapse_values) {
-
-			case 0: return collapse_everything(ini_string, format);
-			case 1: return collapse_empty_quotes(ini_string, format);
-			case 2: return collapse_spaces(ini_string, format);
-			case 3: return rtrim_h(ini_string, ltrim_hh(ini_string, 0, _LIBCONFINI_WITH_EOL_), _LIBCONFINI_WITH_EOL_);
-
-		}
-
-	}
-
-	/*
-
-	Mask `abcd` (16 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Do not collapse spaces within members (const)
-		FLAG_8		Preserve empty quotes (const)
-		FLAG_16		Any space is delimiter (const)
-		FLAG_32		Unescaped single quotes are odd right now
-		FLAG_64		Unescaped double quotes are odd right now
-		FLAG_128	We are in an odd sequence of backslashes
-		FLAG_256	This is *not* a delimiter out of quotes
-		FLAG_512	This is *not* a space out of quotes
-		FLAG_1024	These are some quotes
-		FLAG_2048	These are some quotes or among the last spaces are some empty
-					quotes
-		FLAG_4096	Save current `idx_d` in `fallback`
-		FLAG_8192	Restore `idx_d` from `fallback` before writing
-		FLAG_16384	Decrease `idx_d` before writing
-		FLAG_32768	Keep increasing `idx_d` after writing
-
-	*/
-
-	size_t idx_s = 0, idx_d = 0, fallback = 0;
-
-	register uint16_t abcd		=	(delimiter ? 0 : 16) |
-									(format.preserve_empty_quotes << 3) |
-									(format.do_not_collapse_values << 2) |
-									(format.no_double_quotes << 1) |
-									format.no_single_quotes;
-
-
-	for (; ini_string[idx_s]; idx_s++) {
-
-		/*  Revision #1  */
-
-		abcd	=	!(abcd & 112) && ini_string[idx_s] == delimiter ?
-						(
-							(abcd & 536) && ((abcd & 1560) ^ 8) && ((abcd & 1560) ^ 1544) && ((abcd & 1304) ^ 1032) ?
-								(abcd & 33407) | 33280
-							:
-								(abcd & 41599) | 41472
-						)
-					: !(abcd & 96) && is_some_space(ini_string[idx_s], _LIBCONFINI_WITH_EOL_) ?
-						(
-							!((abcd & 1816) ^ 1800) ?
-								(abcd & 43391) | 40960
-							: !(~abcd & 1560) ?
-								(abcd & 41087) | 40960
-							: !((abcd & 536) ^ 528) || !((abcd & 1560) ^ 536) || !((abcd & 1560) ^ 1048) ?
-								(abcd & 32895) | 32768
-							: !(abcd & 540) || !((abcd & 1564) ^ 8) || !((abcd & 536) ^ 16) || !((abcd & 1560) ^ 24) ?
-								abcd & 2431
-							: ((abcd & 540) ^ 4) && ((abcd & 796) ^ 12) && ((abcd & 1564) ^ 12) && ((abcd & 1308) ^ 1032) ?
-								(abcd & 39295) | 36864
-							:
-								(abcd & 35199) | 32768
-						)
-					: !(abcd & 193) && ini_string[idx_s] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						(
-							!((abcd & 3896) ^ 8) ?
-								(abcd & 44927) | 44064
-							: !((abcd & 3896) ^ 2056) ?
-								(abcd & 36735) | 36128
-							: !((abcd & 1056) ^ 32) ?
-								(abcd & 33631) | 33536
-							: !(abcd & 40) || !(~abcd & 1064) ?
-								((abcd & 36735) | 35840) ^ 32
-							: ((abcd & 1064) ^ 1032) && ((abcd & 1064) ^ 1056) ?
-								(abcd & 40831) | 39968
-							:
-								((abcd & 20351) | 19456) ^ 32
-						)
-					: !(abcd & 162) && ini_string[idx_s] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						(
-							!((abcd & 3928) ^ 8) ?
-								(abcd & 44927) | 44096
-							: !((abcd & 3928) ^ 2056) ?
-								(abcd & 36735) | 36160
-							: !((abcd & 1088) ^ 64) ?
-								(abcd & 33599) | 33536
-							: !(abcd & 72) || !(~abcd & 1096) ?
-								((abcd & 36735) | 35840) ^ 64
-							: ((abcd & 1096) ^ 1088) && ((abcd & 1096) ^ 1032) ?
-								(abcd & 40831) | 40000
-							:
-								((abcd & 20351) | 19456) ^ 64
-						)
-					: ini_string[idx_s] == _LIBCONFINI_BACKSLASH_ ?
-						(
-							(abcd & 888) && ((abcd & 1144) ^ 1032) && ((abcd & 1144) ^ 1048) && ((abcd & 2936) ^ 8) ?
-								((abcd & 33791) | 33536) ^ 128
-							:
-								((abcd & 41983) | 41728) ^ 128
-						)
-					: (abcd & 888) && ((abcd & 1144) ^ 1032) && ((abcd & 1144) ^ 1048) && ((abcd & 2936) ^ 8) ?
-						(abcd & 33663) | 33536
-					:
-						(abcd & 41855) | 41728;
-
-
-		ini_string[
-			abcd & 16384 ?
-				--idx_d
-			: abcd & 8192 ?
-				(idx_d = fallback)
-			: abcd & 4096 ?
-				(fallback = idx_d)
-			:
-				idx_d
-		]							=	(abcd & 1636) && ((abcd & 1392) ^ 16) ?
-											ini_string[idx_s]
-										:
-											_LIBCONFINI_COLLAPSED_;
-
-
-		if (abcd & 32768) {
-
-			idx_d++;
-
-		}
-
-	}
-
-	for (
-
-		idx_s	=	((abcd & 16) && !idx_d) || (!(~abcd & 1040) && idx_d < 4) ?
-						(idx_d = 0)
-					: !(abcd & 536) || !(~abcd & 1544) || !((abcd & 1560) ^ 8) || !((abcd & 1304) ^ 1032) ?
-						(idx_d = fallback)
-					: !((abcd & 1624) ^ 1104) || !((abcd & 1592) ^ 1072) ?
-						(idx_d -= 2)
-					: ((abcd & 1552) ^ 16) && ((abcd & 632) ^ 16) && ((abcd & 1624) ^ 1616) && ((abcd & 1592) ^ 1584) ?
-						idx_d
-					:
-						--idx_d;
-
-			ini_string[idx_s];
-
-		ini_string[idx_s++] = '\0'
-
-	);
-
-	return idx_d;
-
-}
-
-
-												/** @utility{ini_array_break} **/
-/**
-
-	@brief			Replaces the first delimiter found (together with the spaces
-					that surround it) with `\0`
-	@param			ini_string		The stringified array (it can be `NULL`)
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			A pointer to the remaining INI array or `NULL` if the remaining
-					array is empty
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	Similarly to `strtok_r()` this function can be used only once for a given
-	string.
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE this function is
-			no-op and will return `NULL`.
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	@include topics/ini_array_break.c
-
-**/
-char * ini_array_break (char * const ini_string, const char delimiter, const IniFormat format) {
-
-	if (ini_string && !_LIBCONFINI_IMPLICIT_RANGE_(ini_string)) {
-
-		char * remnant;
-
-		if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-			/*
-
-				We have no delimiters (array has only one member)
-
-			*/
-
-			remnant = ini_string;
-
-			while (*remnant++);
-
-			rtrim_h(ini_string, remnant - ini_string - 1, _LIBCONFINI_WITH_EOL_);
-
-		} else {
-
-			remnant = ini_string + get_metachar_pos(ini_string, delimiter, format);
-
-			if (*remnant) {
-
-				*remnant = '\0';
-				rtrim_h(ini_string, remnant - ini_string, _LIBCONFINI_WITH_EOL_);
-				remnant += ltrim_h(remnant, 1, _LIBCONFINI_WITH_EOL_);
-
-				if (delimiter || *remnant) {
-
-					return remnant;
-
-				}
-
-			}
-
-		}
-
-	}
-
-	return (char *) 0;
-
-}
-
-
-												/** @utility{ini_array_release} **/
-/**
-
-	@brief			Replaces the first delimiter found (together with the spaces
-					that surround it) with `\0`, then shifts the location pointed by
-					@p ini_strptr to the next member of the INI array, or to `NULL`
-					if the INI array has no more members
-	@param			ini_strptr		The memory location of the stringified array --
-									it cannot be `NULL`, but it can point to `NULL`
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@return			The array member that has been released
-
-	Usually @p ini_strptr comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	Similarly to `strtok_r()` this function can be used only once for a given
-	string.
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE this function is
-			no-op and will set @p ini_strptr to `NULL`.
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	@include topics/ini_array_release.c
-
-**/
-char * ini_array_release (char ** const ini_strptr, const char delimiter, const IniFormat format) {
-
-	char * const token = *ini_strptr;
-
-	if (token && !_LIBCONFINI_IMPLICIT_RANGE_(token) && !_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		*ini_strptr += get_metachar_pos(*ini_strptr, delimiter, format);
-
-		if (**ini_strptr) {
-
-			**ini_strptr = '\0';
-			rtrim_h(token, *ini_strptr - token, _LIBCONFINI_WITH_EOL_);
-			*ini_strptr += ltrim_h(*ini_strptr, 1, _LIBCONFINI_WITH_EOL_);
-
-			if (delimiter || **ini_strptr) {
-
-				return token;
-
-			}
-
-		}
-
-	}
-
-	*ini_strptr = (char *) 0;
-	return token;
-
-}
-
-
-												/** @utility{ini_array_split} **/
-/**
-
-	@brief			Splits a stringified INI array into NUL-separated members and
-					calls a custom function for each member
-	@param			ini_string		The stringified array (it cannot be `NULL`)
-	@param			delimiter		The delimiter between the array members -- if
-									zero (see #INI_ANY_SPACE), any space is
-									delimiter (`/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`)
-	@param			format			The format of the INI file
-	@param			f_foreach		The function that will be invoked for each array
-									member
-	@param			user_data		A custom argument, or `NULL`
-	@return			Zero for success, otherwise an error code (see `enum`
-					#ConfiniInterruptNo)
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	The user given function @p f_foreach (see #IniStrHandler data type) will be
-	invoked with five arguments: `member` (the member of the array), `memb_length`
-	(the length of the member in bytes), `memb_num` (the offset of the member in
-	number of members), `format` (the format of the INI file), `user_data` (the
-	custom argument @p user_data previously passed). If @p f_foreach returns a
-	non-zero value the function #ini_array_split() will be interrupted.
-
-	Similarly to `strtok_r()` this function can be used only once for a given
-	string.
-
-	@note	If @p ini_string comes from #INI_GLOBAL_IMPLICIT_VALUE or is `NULL` this
-			function is no-op and will return an error code.
-
-	@note	If @p delimiter matches a metacharacter within the format given (`'\\'`,
-			`'\''` or `'\"'`), its role as metacharacter will have higher priority
-			than its role as delimiter (i.e., the array will have no delimiters and
-			will contain only one member).
-
-	Possible return values are: #CONFINI_SUCCESS, #CONFINI_EROADDR, #CONFINI_FEINTR.
-
-	@include topics/ini_array_split.c.
-
-**/
-int ini_array_split (
-	char * const ini_string,
-	const char delimiter,
-	const IniFormat format,
-	const IniStrHandler f_foreach,
-	void * const user_data
-) {
-
-	if (!ini_string || _LIBCONFINI_IMPLICIT_RANGE_(ini_string)) {
-
-		return CONFINI_EROADDR;
-
-	}
-
-	/*
-
-	Mask `abcd` (8 bits used):
-
-		FLAG_1		Single quotes are not metacharacters (const)
-		FLAG_2		Double quotes are not metacharacters (const)
-		FLAG_4		Delimiter is not any space (const)
-		FLAG_8		Unescaped single quotes are odd until now
-		FLAG_16		Unescaped double quotes are odd until now
-		FLAG_32		We are in an odd sequence of backslashes
-		FLAG_64		This is not a delimiter
-		FLAG_128	Stop the loop
-
-	*/
-
-	register size_t idx;
-	size_t offs = ltrim_h(ini_string, 0, _LIBCONFINI_WITH_EOL_);
-
-	if (_LIBCONFINI_IS_ESC_CHAR_(delimiter, format)) {
-
-		/*
-
-			We have no delimiters (array has only one member)
-
-		*/
-
-		idx = 0;
-
-		while (ini_string[idx++]);
-
-		return	f_foreach(ini_string + offs, rtrim_h(ini_string + offs, idx - offs - 1, _LIBCONFINI_WITH_EOL_), 0, format, user_data) ?
-					CONFINI_FEINTR
-				:
-					CONFINI_SUCCESS;
-
-	}
-
-	register uint8_t abcd	=	(delimiter ? 4 : 0) |
-								(format.no_double_quotes << 1) |
-								format.no_single_quotes;
-
-	size_t memb_num = 0;
-
-	idx = offs;
-
-
-	do {
-
-		abcd	=	(delimiter ? ini_string[idx] == delimiter : is_some_space(ini_string[idx], _LIBCONFINI_WITH_EOL_)) ?
-						abcd & 159
-					: ini_string[idx] == _LIBCONFINI_BACKSLASH_ ?
-						(abcd | 64) ^ 32
-					: !(abcd & 42) && ini_string[idx] == _LIBCONFINI_DOUBLE_QUOTES_ ?
-						((abcd & 223) | 64) ^ 16
-					: !(abcd & 49) && ini_string[idx] == _LIBCONFINI_SINGLE_QUOTES_ ?
-						((abcd & 223) | 64) ^ 8
-					: ini_string[idx] ?
-						(abcd & 223) | 64
-					:
-						128;
-
-
-		if (!(abcd & 88)) {
-
-			ini_string[idx] = '\0';
-
-			if (
-				f_foreach(
-					ini_string + offs,
-					rtrim_h(ini_string + offs, idx - offs, _LIBCONFINI_WITH_EOL_),
-					memb_num++,
-					format,
-					user_data
-				)
-			) {
-
-				return CONFINI_FEINTR;
-
-			}
-
-			offs = abcd & 128 ? idx + 1 : ltrim_h(ini_string, idx + 1, _LIBCONFINI_WITH_EOL_);
-
-		}
-
-		idx = abcd & 216 ? idx + 1 : offs;
-
-	} while (!(abcd & 128) && ((abcd & 92) || ini_string[idx]));
-
-	return CONFINI_SUCCESS;
-
-}
-
-
-									/** @utility{ini_global_set_lowercase_mode} **/
-/**
-
-	@brief			Sets the value of the global variable
-					#INI_GLOBAL_LOWERCASE_MODE
-	@param			lowercase		The new value
-	@return			Nothing
-
-	If @p lowercase is `true`, key and section names in case-insensitive INI formats
-	will be dispatched lowercase, verbatim otherwise (default value: `true`).
-
-	@warning	This function changes the value of one or more global variables. In
-				order to be thread-safe this function should be used only once at
-				beginning of execution, or otherwise a mutex logic must be
-				introduced.
-
-**/
-void ini_global_set_lowercase_mode (const bool lowercase) {
-
-	INI_GLOBAL_LOWERCASE_MODE = lowercase;
-
-}
-
-
-									/** @utility{ini_global_set_implicit_value} **/
-/**
-
-	@brief			Sets the value to be to be assigned to implicit keys
-	@param			implicit_value		The string to be used as implicit value
-										(usually `"YES"`, or `"TRUE"`)
-	@param			implicit_v_len		The length of @p implicit_value (usually
-										`0`, independently of its real length)
-	@return			Nothing
-
-	@warning	This function changes the value of one or more global variables. In
-				order to be thread-safe this function should be used only once at
-				beginning of execution, or otherwise a mutex logic must be
-				introduced.
-
-	@include topics/ini_global_set_implicit_value.c
-
-**/
-void ini_global_set_implicit_value (char * const implicit_value, const size_t implicit_v_len) {
-
-	INI_GLOBAL_IMPLICIT_VALUE = implicit_value;
-	INI_GLOBAL_IMPLICIT_V_LEN = implicit_v_len;
-
-}
-
-
-														/** @utility{ini_fton} **/
-/**
-
-	@brief			Calculates the #IniFormatNum of an #IniFormat
-	@param			source			The #IniFormat to compute
-	@return			The unique unsigned integer that identifies the format given
-
-**/
-IniFormatNum ini_fton (const IniFormat source) {
-
-	#define __INIFORMAT_ID__(NAME, OFFSET, SIZE, DEFVAL) (source.NAME << OFFSET) |
-
-	return INIFORMAT_TABLE_AS(__INIFORMAT_ID__) 0;
-
-	#undef __INIFORMAT_ID__
-
-}
-
-
-														/** @utility{ini_ntof} **/
-/**
-
-	@brief			Constructs a new #IniFormat according to an #IniFormatNum
-	@param			format_num		The #IniFormatNum to parse
-	@return			The new #IniFormat constructed
-
-	@note	If @p format_num `>` `16777215` it will be truncated to 24 bits.
-
-**/
-IniFormat ini_ntof (const IniFormatNum format_num) {
-
-	#define __MAX_1_BITS__ 1
-	#define __MAX_2_BITS__ 3
-	#define __MAX_3_BITS__ 7
-	#define __MAX_4_BITS__ 15
-	#define __MAX_5_BITS__ 31
-	#define __MAX_6_BITS__ 63
-	#define __MAX_7_BITS__ 127
-	#define __MAX_8_BITS__ 255
-	#define __INIFORMAT_PROPERTIES__(NAME, OFFSET, SIZE, DEFVAL) \
-		(unsigned char) ((format_num >> OFFSET) & __MAX_##SIZE##_BITS__),
-
-	return (IniFormat) { INIFORMAT_TABLE_AS(__INIFORMAT_PROPERTIES__) };
-
-	#undef __INIFORMAT_PROPERTIES__
-	#undef __MAX_8_BITS__
-	#undef __MAX_7_BITS__
-	#undef __MAX_6_BITS__
-	#undef __MAX_5_BITS__
-	#undef __MAX_4_BITS__
-	#undef __MAX_3_BITS__
-	#undef __MAX_2_BITS__
-	#undef __MAX_1_BITS__
-
-}
-
-
-													/** @utility{ini_get_bool} **/
-/**
-
-	@brief			Checks whether a string matches one of the booleans listed in
-					the private constant #INI_BOOLEANS (case-insensitive)
-	@param			ini_string			A string to check (it can be `NULL`)
-	@param			when_fail			A value that is returned if no matching
-										boolean is found
-	@return			The matching boolean (`0` or `1`) or @p when_fail if
-					@p ini_string does not contain a valid INI boolean
-
-	Usually @p ini_string comes from an #IniDispatch (but any other string may be
-	used as well).
-
-	@include miscellanea/typed_ini.c
-
-**/
-int ini_get_bool (const char * const ini_string, const int when_fail) {
-
-	if (!ini_string) {
-
-		return when_fail;
-
-	}
-
-	register uint8_t bool_idx;
-	register size_t pair_idx, chr_idx;
-
-	for (pair_idx = 0; pair_idx < sizeof(INI_BOOLEANS) / sizeof(const char * const [2]); pair_idx++) {
-
-		for (bool_idx = 0; bool_idx < 2; bool_idx++) {
-
-			chr_idx = 0;
-
-			while (_LIBCONFINI_CHR_CASEFOLD_(ini_string[chr_idx]) == INI_BOOLEANS[pair_idx][bool_idx][chr_idx]) {
-
-				if (!ini_string[chr_idx++]) {
-
-					return (int) bool_idx;
-
-				}
-
-			}
-
-		}
-
-	}
-
-	return when_fail;
-
-}
-
-
-
-		/*  LINKS - In case you don't have `#include <stdlib.h>` in your source  */
-
-
-/**
-
-	@alias{ini_get_int}
-		Pointer to
-		[`atoi()`](http://www.gnu.org/software/libc/manual/html_node/Parsing-of-Integers.html#index-atoi)
-	@alias{ini_get_lint}
-		Pointer to
-		[`atol()`](http://www.gnu.org/software/libc/manual/html_node/Parsing-of-Integers.html#index-atol)
-	@alias{ini_get_llint}
-		Pointer to
-		[`atoll()`](http://www.gnu.org/software/libc/manual/html_node/Parsing-of-Integers.html#index-atoll)
-	@alias{ini_get_double}
-		Pointer to
-		[`atof()`](http://www.gnu.org/software/libc/manual/html_node/Parsing-of-Integers.html#index-atof)
-
-**/
-
-int (* const ini_get_int) (const char * ini_string) = atoi;
-
-long int (* const ini_get_lint) (const char * ini_string) = atol;
-
-long long int (* const ini_get_llint) (const char * ini_string) = atoll;
-
-double (* const ini_get_double) (const char * ini_string) = atof;
-
-/*  Legacy support -- please **do not use this**!  */
-#ifdef ini_get_float
-#undef ini_get_float
-#endif
-double (* const ini_get_float) (const char * ini_string) = atof;
-
-
-
-		/*  GLOBAL VARIABLES  */
-
-
-bool INI_GLOBAL_LOWERCASE_MODE = _LIBCONFINI_FALSE_;
-
-char * INI_GLOBAL_IMPLICIT_VALUE = (char *) 0;
-
-size_t INI_GLOBAL_IMPLICIT_V_LEN = 0;
-
-
-
-/** @endfnlist **/
-
-/*  EOF  */
-
diff --git a/vendor/confini.h b/vendor/confini.h
deleted file mode 100644
index c4c0068..0000000
--- a/vendor/confini.h
+++ /dev/null
@@ -1,547 +0,0 @@
-/*  -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 4; tab-width: 4 -*-  */
-
-/**
-
-    @file       confini.h
-    @brief      libconfini header
-    @author     Stefano Gioffr&eacute;
-    @copyright  GNU General Public License, version 3 or any later version
-    @version    1.14.0
-    @date       2016-2020
-    @see        https://madmurphy.github.io/libconfini
-
-**/
-
-
-#ifndef _LIBCONFINI_HEADER_
-#define _LIBCONFINI_HEADER_
-
-
-
-#include <stdio.h>
-#include <stdbool.h>
-#include <stdint.h>
-
-
-
-#ifdef __cplusplus
-extern "C" {
-#endif
-
-
-
-/*  PRIVATE (HEADER-SCOPED) MACROS  */
-
-
-#define __INIFORMAT_TABLE_CB_FIELDS__(NAME, OFFSET, SIZE, DEFVAL) \
-    unsigned char NAME:SIZE;
-#define __INIFORMAT_TABLE_CB_DEFAULT__(NAME, OFFSET, SIZE, DEFVAL) DEFVAL,
-#define __INIFORMAT_TABLE_CB_ZERO__(NAME, OFFSET, SIZE, DEFVAL) 0,
-#define _LIBCONFINI_INIFORMAT_TYPE_ \
-    struct IniFormat { INIFORMAT_TABLE_AS(__INIFORMAT_TABLE_CB_FIELDS__) }
-#define _LIBCONFINI_DEFAULT_FORMAT_ \
-    { INIFORMAT_TABLE_AS(__INIFORMAT_TABLE_CB_DEFAULT__) }
-#define _LIBCONFINI_UNIXLIKE_FORMAT_ \
-    { INIFORMAT_TABLE_AS(__INIFORMAT_TABLE_CB_ZERO__) }
-
-
-
-/*  PUBLIC MACROS  */
-
-
-/**
-    @brief  Calls a user-given macro (that accepts four arguments) for each row
-            of the table
-**/
-/*
-    NOTE: The following table and the order of its rows **define** (and link
-    together) both the #IniFormat and #IniFormatNum data types declared in this
-    header
-*/
-#define INIFORMAT_TABLE_AS(_____)                 /*  IniFormat table  *\
-
-        NAME                      BIT  SIZE DEFAULT
-                                                                      */\
- _____( delimiter_symbol,         0,   7,   INI_EQUALS                ) \
- _____( case_sensitive,           7,   1,   false                     )/*
-                                                                      */\
- _____( semicolon_marker,         8,   2,   INI_DISABLED_OR_COMMENT   ) \
- _____( hash_marker,              10,  2,   INI_DISABLED_OR_COMMENT   ) \
- _____( section_paths,            12,  2,   INI_ABSOLUTE_AND_RELATIVE ) \
- _____( multiline_nodes,          14,  2,   INI_MULTILINE_EVERYWHERE  )/*
-                                                                      */\
- _____( no_single_quotes,         16,  1,   false                     ) \
- _____( no_double_quotes,         17,  1,   false                     ) \
- _____( no_spaces_in_names,       18,  1,   false                     ) \
- _____( implicit_is_not_empty,    19,  1,   false                     ) \
- _____( do_not_collapse_values,   20,  1,   false                     ) \
- _____( preserve_empty_quotes,    21,  1,   false                     ) \
- _____( disabled_after_space,     22,  1,   false                     ) \
- _____( disabled_can_be_implicit, 23,  1,   false                     )
-
-
-
-/**
-    @brief  Checks whether a format does **not** support escape sequences
-**/
-#define INIFORMAT_HAS_NO_ESC(FORMAT) \
-    (FORMAT.multiline_nodes == INI_NO_MULTILINE && \
-    FORMAT.no_double_quotes && FORMAT.no_single_quotes)
-
-
-
-/*  PUBLIC TYPEDEFS  */
-
-
-/**
-    @brief  24-bit bitfield representing the format of an INI file (INI
-            dialect)
-**/
-typedef _LIBCONFINI_INIFORMAT_TYPE_ IniFormat;
-
-
-/**
-    @brief  Global statistics about an INI file
-**/
-typedef struct IniStatistics {
-    const IniFormat format;
-    const size_t bytes;
-    const size_t members;
-} IniStatistics;
-
-
-/**
-    @brief  Dispatch of a single INI node
-**/
-typedef struct IniDispatch {
-    const IniFormat format;
-    uint8_t type;
-    char * data;
-    char * value;
-    const char * append_to;
-    size_t d_len;
-    size_t v_len;
-    size_t at_len;
-    size_t dispatch_id;
-} IniDispatch;
-
-
-/**
-    @brief  The unique ID of an INI format (24-bit maximum)
-**/
-typedef uint32_t IniFormatNum;
-
-
-/**
-    @brief  Callback function for handling an #IniStatistics structure
-**/
-typedef int (* IniStatsHandler) (
-    IniStatistics * statistics,
-    void * user_data
-);
-
-
-/**
-    @brief  Callback function for handling an #IniDispatch structure
-**/
-typedef int (* IniDispHandler) (
-    IniDispatch * dispatch,
-    void * user_data
-);
-
-
-/**
-    @brief  Callback function for handling an INI string belonging to a
-            sequence of INI strings
-**/
-typedef int (* IniStrHandler) (
-    char * ini_string,
-    size_t string_length,
-    size_t string_num,
-    IniFormat format,
-    void * user_data
-);
-
-
-/**
-    @brief  Callback function for handling a selected fragment of an INI string
-**/
-typedef int (* IniSubstrHandler) (
-    const char * ini_string,
-    size_t fragm_offset,
-    size_t fragm_length,
-    size_t fragm_num,
-    IniFormat format,
-    void * user_data
-);
-
-
-
-/*  PUBLIC FUNCTIONS  */
-
-
-extern int strip_ini_cache (
-    register char * const ini_source,
-    const size_t ini_length,
-    const IniFormat format,
-    const IniStatsHandler f_init,
-    const IniDispHandler f_foreach,
-    void * const user_data
-);
-
-
-extern int load_ini_file (
-    FILE * const ini_file,
-    const IniFormat format,
-    const IniStatsHandler f_init,
-    const IniDispHandler f_foreach,
-    void * const user_data
-);
-
-
-extern int load_ini_path (
-    const char * const path,
-    const IniFormat format,
-    const IniStatsHandler f_init,
-    const IniDispHandler f_foreach,
-    void * const user_data
-);
-
-
-extern bool ini_string_match_ss (
-    const char * const simple_string_a,
-    const char * const simple_string_b,
-    const IniFormat format
-);
-
-
-extern bool ini_string_match_si (
-    const char * const simple_string,
-    const char * const ini_string,
-    const IniFormat format
-);
-
-
-extern bool ini_string_match_ii (
-    const char * const ini_string_a,
-    const char * const ini_string_b,
-    const IniFormat format
-);
-
-
-extern bool ini_array_match (
-    const char * const ini_string_a,
-    const char * const ini_string_b,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern size_t ini_unquote (
-    char * const ini_string,
-    const IniFormat format
-);
-
-
-extern size_t ini_string_parse (
-    char * const ini_string,
-    const IniFormat format
-);
-
-
-extern size_t ini_array_get_length (
-    const char * const ini_string,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern int ini_array_foreach (
-    const char * const ini_string,
-    const char delimiter,
-    const IniFormat format,
-    const IniSubstrHandler f_foreach,
-    void * const user_data
-);
-
-
-extern size_t ini_array_shift (
-    const char ** const ini_strptr,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern size_t ini_array_collapse (
-    char * const ini_string,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern char * ini_array_break (
-    char * const ini_string,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern char * ini_array_release (
-    char ** const ini_strptr,
-    const char delimiter,
-    const IniFormat format
-);
-
-
-extern int ini_array_split (
-    char * const ini_string,
-    const char delimiter,
-    const IniFormat format,
-    const IniStrHandler f_foreach,
-    void * const user_data
-);
-
-
-extern void ini_global_set_lowercase_mode (
-    const bool lowercase
-);
-
-
-extern void ini_global_set_implicit_value (
-    char * const implicit_value,
-    const size_t implicit_v_len
-);
-
-
-extern IniFormatNum ini_fton (
-    const IniFormat format
-);
-
-
-extern IniFormat ini_ntof (
-    const IniFormatNum format_id
-);
-
-
-extern int ini_get_bool (
-    const char * const ini_string,
-    const int when_fail
-);
-
-
-
-/*  PUBLIC LINKS  */
-
-
-extern int (* const ini_get_int) (
-    const char * ini_string
-);
-
-
-extern long int (* const ini_get_lint) (
-    const char * ini_string
-);
-
-
-extern long long int (* const ini_get_llint) (
-    const char * ini_string
-);
-
-
-extern double (* const ini_get_double) (
-    const char * ini_string
-);
-
-
-/**
-    @brief  Legacy support, soon to be replaced with a `float` data type --
-            please **do not use `ini_get_float()`!**
-**/
-#define ini_get_float \
-    _Pragma("GCC warning \"function `ini_get_float()` is deprecated for parsing a `double` data type; use `ini_get_double()` instead\"") \
-    ini_get_double
-
-
-
-/*  PUBLIC CONSTANTS AND VARIABLES  */
-
-
-/**
-    @brief  Error mask (flags not present in user-generated interruptions)
-**/
-#define CONFINI_ERROR 252
-
-
-/**
-    @brief  Error codes
-**/
-enum ConfiniInterruptNo {
-    CONFINI_SUCCESS = 0,    /**< There have been no interruptions, everything
-                                 went well [value=0] **/
-    CONFINI_IINTR = 1,      /**< Interrupted by the user during `f_init()`
-                                 [value=1] **/
-    CONFINI_FEINTR = 2,     /**< Interrupted by the user during `f_foreach()`
-                                 [value=2] **/
-    CONFINI_ENOENT = 4,     /**< File inaccessible [value=4] **/
-    CONFINI_ENOMEM = 5,     /**< Error allocating virtual memory [value=5] **/
-    CONFINI_EIO = 6,        /**< Error reading the file [value=6] **/
-    CONFINI_EOOR = 7,       /**< Out-of-range error: callbacks are more than
-                                 expected [value=7] **/
-    CONFINI_EBADF = 8,      /**< The stream specified is not a seekable stream
-                                 [value=8] **/
-    CONFINI_EFBIG = 9,      /**< File too large [value=9] **/
-    CONFINI_EROADDR = 10    /**< Address is read-only [value=10] **/
-};
-
-
-/**
-    @brief  INI node types
-**/
-enum IniNodeType {
-    INI_UNKNOWN = 0,            /**< This is a node impossible to categorize
-                                     [value=0] **/
-    INI_VALUE = 1,              /**< Not used by **libconfini** (values are
-                                     dispatched together with keys) -- but
-                                     available for user's implementations
-                                     [value=1] **/
-    INI_KEY = 2,                /**< This is a key [value=2] **/
-    INI_SECTION = 3,            /**< This is a section or a section path
-                                     [value=3] **/
-    INI_COMMENT = 4,            /**< This is a comment [value=4] **/
-    INI_INLINE_COMMENT = 5,     /**< This is an inline comment [value=5] **/
-    INI_DISABLED_KEY = 6,       /**< This is a disabled key [value=6] **/
-    INI_DISABLED_SECTION = 7    /**< This is a disabled section path
-                                     [value=7] **/
-};
-
-
-/**
-    @brief  Common array and key-value delimiters (but a delimiter may also be
-            any other ASCII character not present in this list)
-**/
-enum IniDelimiters {
-    INI_ANY_SPACE = 0,  /**< In multi-line INIs:
-                             `/(?:\\(?:\n\r?|\r\n?)|[\t \v\f])+/`, in
-                             non-multi-line INIs: `/[\t \v\f])+/` **/
-    INI_EQUALS = '=',   /**< Equals character (`=`) **/
-    INI_COLON = ':',    /**< Colon character (`:`) **/
-    INI_DOT = '.',      /**< Dot character (`.`) **/
-    INI_COMMA = ','     /**< Comma character (`,`) **/
-};
-
-
-/**
-    @brief  Possible values of #IniFormat::semicolon_marker and
-            #IniFormat::hash_marker (i.e., meaning of `/\s+;/` and `/\s+#/` in
-            respect to a format)
-**/
-enum IniCommentMarker {
-    INI_DISABLED_OR_COMMENT = 0,    /**< This marker opens a comment or a
-                                         disabled entry **/
-    INI_ONLY_COMMENT = 1,           /**< This marker opens a comment **/
-    INI_IGNORE = 2,                 /**< This marker opens a comment that has
-                                         been marked for deletion and must not
-                                         be dispatched or counted **/
-    INI_IS_NOT_A_MARKER = 3         /**< This is not a marker at all, but a
-                                         normal character instead **/
-};
-
-
-/**
-    @brief  Possible values of #IniFormat::section_paths
-**/
-enum IniSectionPaths {
-    INI_ABSOLUTE_AND_RELATIVE = 0,  /**< Section paths starting with a dot
-                                         express nesting to the current parent,
-                                         to root otherwise **/
-    INI_ABSOLUTE_ONLY = 1,          /**< Section paths starting with a dot will
-                                         be cleaned of their leading dot and
-                                         appended to root **/
-    INI_ONE_LEVEL_ONLY = 2,         /**< Format supports sections, but the dot
-                                         does not express nesting and is not a
-                                         meta-character **/
-    INI_NO_SECTIONS = 3             /**< Format does *not* support sections --
-                                         `/\[[^\]]*\]/g`, if any, will be
-                                         treated as keys! **/
-};
-
-
-/**
-    @brief  Possible values of #IniFormat::multiline_nodes
-**/
-enum IniMultiline {
-    INI_MULTILINE_EVERYWHERE = 0,       /**< Comments, section paths and keys
-                                             -- disabled or not -- are allowed
-                                             to be multi-line **/
-    INI_BUT_COMMENTS = 1,               /**< Only section paths and keys --
-                                             disabled or not -- are allowed to
-                                             be multi-line **/
-    INI_BUT_DISABLED_AND_COMMENTS = 2,  /**< Only active section paths and
-                                             active keys are allowed to be
-                                             multi-line **/
-    INI_NO_MULTILINE = 3                /**< Multi-line escape sequences are
-                                             disabled **/
-};
-
-
-/**
-    @brief  A model format for standard INI files
-**/
-static const IniFormat INI_DEFAULT_FORMAT = _LIBCONFINI_DEFAULT_FORMAT_;
-
-
-/**
-    @brief  A model format for Unix-like .conf files (where space characters
-            are delimiters between keys and values)
-**/
-/*  All fields are set to `0` here.  */
-static const IniFormat INI_UNIXLIKE_FORMAT = _LIBCONFINI_UNIXLIKE_FORMAT_;
-
-
-/**
-    @brief  If set to `true`, key and section names in case-insensitive INI
-            formats will be dispatched lowercase, verbatim otherwise (default
-            value: `false`)
-**/
-extern bool INI_GLOBAL_LOWERCASE_MODE;
-
-
-/**
-    @brief  Value to be assigned to implicit keys (default value: `NULL`)
-**/
-extern char * INI_GLOBAL_IMPLICIT_VALUE;
-
-
-/**
-    @brief  Length of the value assigned to implicit keys (default value: `0`)
-**/
-extern size_t INI_GLOBAL_IMPLICIT_V_LEN;
-
-
-
-/*  CLEAN THE PRIVATE ENVIRONMENT  */
-
-
-#undef _LIBCONFINI_UNIXLIKE_FORMAT_
-#undef _LIBCONFINI_DEFAULT_FORMAT_
-#undef _LIBCONFINI_INIFORMAT_TYPE_
-#undef __INIFORMAT_TABLE_CB_ZERO__
-#undef __INIFORMAT_TABLE_CB_DEFAULT__
-#undef __INIFORMAT_TABLE_CB_FIELDS__
-
-
-
-/*  END OF `_LIBCONFINI_HEADER_`  */
-
-
-#ifdef __cplusplus
-}
-#endif
-
-
-#endif
-
-
-
-/*  EOF  */
-
diff --git a/vendor/toml.c b/vendor/toml.c
new file mode 100644
index 0000000..98f765b
--- /dev/null
+++ b/vendor/toml.c
@@ -0,0 +1,2274 @@
+/*
+
+  MIT License
+
+  Copyright (c) 2017 - 2019 CK Tan 
+  https://github.com/cktan/tomlc99
+  
+  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.
+
+*/
+#define _POSIX_C_SOURCE 200809L
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+#include <errno.h>
+#include <stdint.h>
+#include <ctype.h>
+#include <string.h>
+#include <stdbool.h>
+#include "toml.h"
+
+
+static void* (*ppmalloc)(size_t) = malloc;
+static void (*ppfree)(void*) = free;
+
+void toml_set_memutil(void* (*xxmalloc)(size_t),
+					  void	(*xxfree)(void*))
+{
+	if (xxmalloc) ppmalloc = xxmalloc;
+	if (xxfree) ppfree = xxfree;
+}
+
+
+#define MALLOC(a)	  ppmalloc(a)
+#define FREE(a)		  ppfree(a)
+
+static void* CALLOC(size_t nmemb, size_t sz)
+{
+	int nb = sz * nmemb;
+	void* p = MALLOC(nb);
+	if (p) {
+		memset(p, 0, nb);
+	}
+	return p;
+}
+
+
+static char* STRDUP(const char* s)
+{
+	int len = strlen(s);
+	char* p = MALLOC(len+1);
+	if (p) {
+		memcpy(p, s, len);
+		p[len] = 0;
+	}
+	return p;
+}
+
+static char* STRNDUP(const char* s, size_t n)
+{
+	size_t len = strnlen(s, n);
+	char* p = MALLOC(len+1);
+	if (p) {
+		memcpy(p, s, len);
+		p[len] = 0;
+	}
+	return p;
+}
+
+
+
+/**
+ * Convert a char in utf8 into UCS, and store it in *ret.
+ * Return #bytes consumed or -1 on failure.
+ */
+int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret)
+{
+	const unsigned char* buf = (const unsigned char*) orig;
+	unsigned i = *buf++;
+	int64_t v;
+	
+	/* 0x00000000 - 0x0000007F:
+	   0xxxxxxx
+	*/
+	if (0 == (i >> 7)) {
+		if (len < 1) return -1;
+		v = i;
+		return *ret = v, 1;
+	}
+	/* 0x00000080 - 0x000007FF:
+	   110xxxxx 10xxxxxx
+	*/
+	if (0x6 == (i >> 5)) {
+		if (len < 2) return -1;
+		v = i & 0x1f;
+		for (int j = 0; j < 1; j++) {
+			i = *buf++;
+			if (0x2 != (i >> 6)) return -1;
+			v = (v << 6) | (i & 0x3f);
+		}
+		return *ret = v, (const char*) buf - orig;
+	}
+
+	/* 0x00000800 - 0x0000FFFF:
+	   1110xxxx 10xxxxxx 10xxxxxx
+	*/
+	if (0xE == (i >> 4)) {
+		if (len < 3) return -1;
+		v = i & 0x0F;
+		for (int j = 0; j < 2; j++) {
+			i = *buf++;
+			if (0x2 != (i >> 6)) return -1;
+			v = (v << 6) | (i & 0x3f);
+		}
+		return *ret = v, (const char*) buf - orig;
+	}
+
+	/* 0x00010000 - 0x001FFFFF:
+	   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (0x1E == (i >> 3)) {
+		if (len < 4) return -1;
+		v = i & 0x07;
+		for (int j = 0; j < 3; j++) {
+			i = *buf++;
+			if (0x2 != (i >> 6)) return -1;
+			v = (v << 6) | (i & 0x3f);
+		}
+		return *ret = v, (const char*) buf - orig;
+	}
+	
+	/* 0x00200000 - 0x03FFFFFF:
+	   111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (0x3E == (i >> 2)) {
+		if (len < 5) return -1;
+		v = i & 0x03;
+		for (int j = 0; j < 4; j++) {
+			i = *buf++;
+			if (0x2 != (i >> 6)) return -1;
+			v = (v << 6) | (i & 0x3f);
+		}
+		return *ret = v, (const char*) buf - orig;
+	}
+
+	/* 0x04000000 - 0x7FFFFFFF:
+	   1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (0x7e == (i >> 1)) {
+		if (len < 6) return -1;
+		v = i & 0x01;
+		for (int j = 0; j < 5; j++) {
+			i = *buf++;
+			if (0x2 != (i >> 6)) return -1;
+			v = (v << 6) | (i & 0x3f);
+		}
+		return *ret = v, (const char*) buf - orig;
+	}
+	return -1;
+}
+
+
+/**
+ *	Convert a UCS char to utf8 code, and return it in buf.
+ *	Return #bytes used in buf to encode the char, or 
+ *	-1 on error.
+ */
+int toml_ucs_to_utf8(int64_t code, char buf[6])
+{
+	/* http://stackoverflow.com/questions/6240055/manually-converting-unicode-codepoints-into-utf-8-and-utf-16 */
+	/* The UCS code values 0xd800–0xdfff (UTF-16 surrogates) as well
+	 * as 0xfffe and 0xffff (UCS noncharacters) should not appear in
+	 * conforming UTF-8 streams.
+	 */
+	if (0xd800 <= code && code <= 0xdfff) return -1;
+	if (0xfffe <= code && code <= 0xffff) return -1;
+
+	/* 0x00000000 - 0x0000007F:
+	   0xxxxxxx
+	*/
+	if (code < 0) return -1;
+	if (code <= 0x7F) {
+		buf[0] = (unsigned char) code;
+		return 1;
+	}
+
+	/* 0x00000080 - 0x000007FF:
+	   110xxxxx 10xxxxxx
+	*/
+	if (code <= 0x000007FF) {
+		buf[0] = 0xc0 | (code >> 6);
+		buf[1] = 0x80 | (code & 0x3f);
+		return 2;
+	}
+
+	/* 0x00000800 - 0x0000FFFF:
+	   1110xxxx 10xxxxxx 10xxxxxx
+	*/
+	if (code <= 0x0000FFFF) {
+		buf[0] = 0xe0 | (code >> 12);
+		buf[1] = 0x80 | ((code >> 6) & 0x3f);
+		buf[2] = 0x80 | (code & 0x3f);
+		return 3;
+	}
+
+	/* 0x00010000 - 0x001FFFFF:
+	   11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (code <= 0x001FFFFF) {
+		buf[0] = 0xf0 | (code >> 18);
+		buf[1] = 0x80 | ((code >> 12) & 0x3f);
+		buf[2] = 0x80 | ((code >> 6) & 0x3f);
+		buf[3] = 0x80 | (code & 0x3f);
+		return 4;
+	}
+
+	/* 0x00200000 - 0x03FFFFFF:
+	   111110xx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (code <= 0x03FFFFFF) {
+		buf[0] = 0xf8 | (code >> 24);
+		buf[1] = 0x80 | ((code >> 18) & 0x3f);
+		buf[2] = 0x80 | ((code >> 12) & 0x3f);
+		buf[3] = 0x80 | ((code >> 6) & 0x3f);
+		buf[4] = 0x80 | (code & 0x3f);
+		return 5;
+	}
+	
+	/* 0x04000000 - 0x7FFFFFFF:
+	   1111110x 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx 10xxxxxx
+	*/
+	if (code <= 0x7FFFFFFF) {
+		buf[0] = 0xfc | (code >> 30);
+		buf[1] = 0x80 | ((code >> 24) & 0x3f);
+		buf[2] = 0x80 | ((code >> 18) & 0x3f);
+		buf[3] = 0x80 | ((code >> 12) & 0x3f);
+		buf[4] = 0x80 | ((code >> 6) & 0x3f);
+		buf[5] = 0x80 | (code & 0x3f);
+		return 6;
+	}
+
+	return -1;
+}
+
+/*
+ *	TOML has 3 data structures: value, array, table. 
+ *	Each of them can have identification key.
+ */
+typedef struct toml_keyval_t toml_keyval_t;
+struct toml_keyval_t {
+	const char* key;		/* key to this value */
+	const char* val;		/* the raw value */
+};
+
+
+struct toml_array_t {
+	const char* key;		/* key to this array */
+	int kind; /* element kind: 'v'alue, 'a'rray, or 't'able */
+	int type; /* for value kind: 'i'nt, 'd'ouble, 'b'ool, 's'tring, 't'ime, 'D'ate, 'T'imestamp */
+	
+	int nelem;			/* number of elements */
+	union {
+		char** val;
+		toml_array_t** arr;
+		toml_table_t** tab;
+	} u;
+};
+	
+
+struct toml_table_t {
+	const char* key;		/* key to this table */
+	bool implicit;		/* table was created implicitly */
+
+	/* key-values in the table */
+	int			nkval;
+	toml_keyval_t** kval;
+
+	/* arrays in the table */
+	int		   narr;
+	toml_array_t** arr;
+
+	/* tables in the table */
+	int		   ntab;
+	toml_table_t** tab;
+};
+
+
+static inline void xfree(const void* x) { if (x) FREE((void*)(intptr_t)x); }
+
+
+enum tokentype_t {
+	INVALID,
+	DOT,
+	COMMA,
+	EQUAL,
+	LBRACE,
+	RBRACE,
+	NEWLINE,
+	LBRACKET,
+	RBRACKET,
+	STRING,
+};
+typedef enum tokentype_t tokentype_t;
+
+typedef struct token_t token_t;
+struct token_t {
+	tokentype_t tok;
+	int		lineno;
+	char*	ptr;		/* points into context->start */
+	int		len;
+	int		eof;
+};
+
+
+typedef struct context_t context_t;
+struct context_t {
+	char* start;
+	char* stop;
+	char* errbuf;
+	int	  errbufsz;
+
+	token_t tok;
+	toml_table_t* root;
+	toml_table_t* curtab;
+
+	struct {
+		int	top;
+		char*	key[10];
+		token_t tok[10];
+	} tpath;
+
+};
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x)	 STRINGIFY(x)
+#define FLINE __FILE__ ":" TOSTRING(__LINE__)
+
+static int next_token(context_t* ctx, int dotisspecial);
+
+/*
+  Error reporting. Call when an error is detected. Always return -1.
+*/
+static int e_outofmemory(context_t* ctx, const char* fline)
+{
+	snprintf(ctx->errbuf, ctx->errbufsz, "ERROR: out of memory (%s)", fline);
+	return -1;
+}
+
+
+static int e_internal(context_t* ctx, const char* fline)
+{
+	snprintf(ctx->errbuf, ctx->errbufsz, "internal error (%s)", fline);
+	return -1;
+}
+
+static int e_syntax(context_t* ctx, int lineno, const char* msg)
+{
+	snprintf(ctx->errbuf, ctx->errbufsz, "line %d: %s", lineno, msg);
+	return -1;
+}
+
+static int e_badkey(context_t* ctx, int lineno)
+{
+	snprintf(ctx->errbuf, ctx->errbufsz, "line %d: bad key", lineno);
+	return -1;
+}
+
+static int e_keyexists(context_t* ctx, int lineno)
+{
+	snprintf(ctx->errbuf, ctx->errbufsz, "line %d: key exists", lineno);
+	return -1;
+}
+
+static void* expand(void* p, int sz, int newsz)
+{
+	void* s = MALLOC(newsz);
+	if (!s) return 0;
+	
+	memcpy(s, p, sz);
+	FREE(p);
+	return s;
+}
+
+static void** expand_ptrarr(void** p, int n)
+{
+	void** s = MALLOC((n+1) * sizeof(void*));
+	if (!s) return 0;
+
+	s[n] = 0;
+	memcpy(s, p, n * sizeof(void*));
+	FREE(p);
+	return s;
+}
+
+
+static char* norm_lit_str(const char* src, int srclen,
+						  int multiline,
+						  char* errbuf, int errbufsz)
+{
+	char* dst = 0;		/* will write to dst[] and return it */
+	int	  max = 0;		/* max size of dst[] */
+	int	  off = 0;		/* cur offset in dst[] */
+	const char* sp = src;
+	const char* sq = src + srclen;
+	int ch;
+
+	/* scan forward on src */
+	for (;;) {
+		if (off >=	max - 10) { /* have some slack for misc stuff */
+			int newmax = max + 50;
+			char* x = expand(dst, max, newmax);
+			if (!x) {
+				xfree(dst);
+				snprintf(errbuf, errbufsz, "out of memory");
+				return 0;
+			}
+			dst = x;
+			max = newmax;
+		}
+	
+		/* finished? */
+		if (sp >= sq) break; 
+	
+		ch = *sp++;
+		/* control characters other than tab is not allowed */
+		if ((0 <= ch && ch <= 0x08)
+			|| (0x0a <= ch && ch <= 0x1f)
+			|| (ch == 0x7f)) {
+			if (! (multiline && (ch == '\r' || ch == '\n'))) {
+				xfree(dst);
+				snprintf(errbuf, errbufsz, "invalid char U+%04x", ch);
+				return 0;
+			}
+		}
+			
+		// a plain copy suffice
+		dst[off++] = ch;
+	}
+
+	dst[off++] = 0;
+	return dst;
+}
+
+
+
+
+/* 
+ * Convert src to raw unescaped utf-8 string.
+ * Returns NULL if error with errmsg in errbuf.
+ */
+static char* norm_basic_str(const char* src, int srclen,
+							int multiline,
+							char* errbuf, int errbufsz)
+{
+	char* dst = 0;		/* will write to dst[] and return it */
+	int	  max = 0;		/* max size of dst[] */
+	int	  off = 0;		/* cur offset in dst[] */
+	const char* sp = src;
+	const char* sq = src + srclen;
+	int ch;
+
+	/* scan forward on src */
+	for (;;) {
+		if (off >=	max - 10) { /* have some slack for misc stuff */
+			int newmax = max + 50;
+			char* x = expand(dst, max, newmax);
+			if (!x) {
+				xfree(dst);
+				snprintf(errbuf, errbufsz, "out of memory");
+				return 0;
+			}
+			dst = x;
+			max = newmax;
+		}
+	
+		/* finished? */
+		if (sp >= sq) break; 
+	
+		ch = *sp++;
+		if (ch != '\\') {
+			/* these chars must be escaped: U+0000 to U+0008, U+000A to U+001F, U+007F */
+			if ((0 <= ch && ch <= 0x08)
+				|| (0x0a <= ch && ch <= 0x1f)
+				|| (ch == 0x7f)) {
+				if (! (multiline && (ch == '\r' || ch == '\n'))) {
+					xfree(dst);
+					snprintf(errbuf, errbufsz, "invalid char U+%04x", ch);
+					return 0;
+				}
+			}
+			
+			// a plain copy suffice
+			dst[off++] = ch;
+			continue;
+		}
+
+		/* ch was backslash. we expect the escape char. */
+		if (sp >= sq) {
+			snprintf(errbuf, errbufsz, "last backslash is invalid");
+			xfree(dst);
+			return 0;
+		}
+
+		/* for multi-line, we want to kill line-ending-backslash ... */
+		if (multiline) {
+
+			// if there is only whitespace after the backslash ...
+			if (sp[strspn(sp, " \t\r")] == '\n') {
+				/* skip all the following whitespaces */
+				sp += strspn(sp, " \t\r\n");
+				continue;
+			}
+		}
+
+		/* get the escaped char */
+		ch = *sp++;
+		switch (ch) {
+		case 'u': case 'U':
+			{
+				int64_t ucs = 0;
+				int nhex = (ch == 'u' ? 4 : 8);
+				for (int i = 0; i < nhex; i++) {
+					if (sp >= sq) {
+						snprintf(errbuf, errbufsz, "\\%c expects %d hex chars", ch, nhex);
+						xfree(dst);
+						return 0;
+					}
+					ch = *sp++;
+					int v = ('0' <= ch && ch <= '9')
+						? ch - '0'
+						: (('A' <= ch && ch <= 'F') ? ch - 'A' + 10 : -1);
+					if (-1 == v) {
+						snprintf(errbuf, errbufsz, "invalid hex chars for \\u or \\U");
+						xfree(dst);
+						return 0;
+					}
+					ucs = ucs * 16 + v;
+				}
+				int n = toml_ucs_to_utf8(ucs, &dst[off]);
+				if (-1 == n) {
+					snprintf(errbuf, errbufsz, "illegal ucs code in \\u or \\U");
+					xfree(dst);
+					return 0;
+				}
+				off += n;
+			}
+			continue;
+
+		case 'b': ch = '\b'; break;
+		case 't': ch = '\t'; break;
+		case 'n': ch = '\n'; break;
+		case 'f': ch = '\f'; break;
+		case 'r': ch = '\r'; break;
+		case '"':  ch = '"'; break;
+		case '\\': ch = '\\'; break;
+		default: 
+			snprintf(errbuf, errbufsz, "illegal escape char \\%c", ch);
+			xfree(dst);
+			return 0;
+		}
+
+		dst[off++] = ch;
+	}
+
+	// Cap with NUL and return it.
+	dst[off++] = 0; 
+	return dst;
+}
+
+
+/* Normalize a key. Convert all special chars to raw unescaped utf-8 chars. */
+static char* normalize_key(context_t* ctx, token_t strtok)
+{
+	const char* sp = strtok.ptr;
+	const char* sq = strtok.ptr + strtok.len;
+	int lineno = strtok.lineno;
+	char* ret;
+	int ch = *sp;
+	char ebuf[80];
+
+	/* handle quoted string */
+	if (ch == '\'' || ch == '\"') {
+		/* if ''' or """, take 3 chars off front and back. Else, take 1 char off. */
+		int multiline = 0;
+		if (sp[1] == ch && sp[2] == ch)	 {
+			sp += 3, sq -= 3;
+			multiline = 1;
+		}
+		else
+			sp++, sq--;
+
+		if (ch == '\'') {
+			/* for single quote, take it verbatim. */
+			if (! (ret = STRNDUP(sp, sq - sp))) {
+				e_outofmemory(ctx, FLINE);
+				return 0;
+			}
+		} else {
+			/* for double quote, we need to normalize */
+			ret = norm_basic_str(sp, sq - sp, multiline, ebuf, sizeof(ebuf));
+			if (!ret) {
+				e_syntax(ctx, lineno, ebuf);
+				return 0;
+			}
+		}
+
+		/* newlines are not allowed in keys */
+		if (strchr(ret, '\n')) {
+			xfree(ret);
+			e_badkey(ctx, lineno);
+			return 0;
+		}
+		return ret;
+	}
+	
+	/* for bare-key allow only this regex: [A-Za-z0-9_-]+ */
+	const char* xp;
+	for (xp = sp; xp != sq; xp++) {
+		int k = *xp;
+		if (isalnum(k)) continue;
+		if (k == '_' || k == '-') continue;
+		e_badkey(ctx, lineno);
+		return 0;
+	}
+
+	/* dup and return it */
+	if (! (ret = STRNDUP(sp, sq - sp))) {
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	return ret;
+}
+
+
+/* 
+ * Look up key in tab. Return 0 if not found, or
+ * 'v'alue, 'a'rray or 't'able depending on the element.
+ */
+static int check_key(toml_table_t* tab, const char* key,
+					 toml_keyval_t** ret_val,
+					 toml_array_t** ret_arr,
+					 toml_table_t** ret_tab)
+{
+	int i;
+	void* dummy;
+
+	if (!ret_tab) ret_tab = (toml_table_t**) &dummy;
+	if (!ret_arr) ret_arr = (toml_array_t**) &dummy;
+	if (!ret_val) ret_val = (toml_keyval_t**) &dummy;
+
+	*ret_tab = 0; *ret_arr = 0; *ret_val = 0;
+	
+	for (i = 0; i < tab->nkval; i++) {
+		if (0 == strcmp(key, tab->kval[i]->key)) {
+			*ret_val = tab->kval[i];
+			return 'v';
+		}
+	}
+	for (i = 0; i < tab->narr; i++) {
+		if (0 == strcmp(key, tab->arr[i]->key)) {
+			*ret_arr = tab->arr[i];
+			return 'a';
+		}
+	}
+	for (i = 0; i < tab->ntab; i++) {
+		if (0 == strcmp(key, tab->tab[i]->key)) {
+			*ret_tab = tab->tab[i];
+			return 't';
+		}
+	}
+	return 0;
+}
+
+
+static int key_kind(toml_table_t* tab, const char* key)
+{
+	return check_key(tab, key, 0, 0, 0);
+}
+
+/* Create a keyval in the table. 
+ */
+static toml_keyval_t* create_keyval_in_table(context_t* ctx, toml_table_t* tab, token_t keytok)
+{
+	/* first, normalize the key to be used for lookup. 
+	 * remember to free it if we error out. 
+	 */
+	char* newkey = normalize_key(ctx, keytok);
+	if (!newkey) return 0;
+
+	/* if key exists: error out. */
+	toml_keyval_t* dest = 0;
+	if (key_kind(tab, newkey)) {
+		xfree(newkey);
+		e_keyexists(ctx, keytok.lineno);
+		return 0;
+	}
+
+	/* make a new entry */
+	int n = tab->nkval;
+	toml_keyval_t** base;
+	if (0 == (base = (toml_keyval_t**) expand_ptrarr((void**)tab->kval, n))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	tab->kval = base;
+	
+	if (0 == (base[n] = (toml_keyval_t*) CALLOC(1, sizeof(*base[n])))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	dest = tab->kval[tab->nkval++];
+
+	/* save the key in the new value struct */
+	dest->key = newkey;
+	return dest;
+}
+
+
+/* Create a table in the table.
+ */
+static toml_table_t* create_keytable_in_table(context_t* ctx, toml_table_t* tab, token_t keytok)
+{
+	/* first, normalize the key to be used for lookup. 
+	 * remember to free it if we error out. 
+	 */
+	char* newkey = normalize_key(ctx, keytok);
+	if (!newkey) return 0;
+
+	/* if key exists: error out */
+	toml_table_t* dest = 0;
+	if (check_key(tab, newkey, 0, 0, &dest)) {
+		xfree(newkey);		 /* don't need this anymore */
+	
+		/* special case: if table exists, but was created implicitly ... */
+		if (dest && dest->implicit) {
+			/* we make it explicit now, and simply return it. */
+			dest->implicit = false;
+			return dest;
+		}
+		e_keyexists(ctx, keytok.lineno);
+		return 0;
+	}
+
+	/* create a new table entry */
+	int n = tab->ntab;
+	toml_table_t** base;
+	if (0 == (base = (toml_table_t**) expand_ptrarr((void**)tab->tab, n))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	tab->tab = base;
+	
+	if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	dest = tab->tab[tab->ntab++];
+	
+	/* save the key in the new table struct */
+	dest->key = newkey;
+	return dest;
+}
+
+
+/* Create an array in the table.
+ */
+static toml_array_t* create_keyarray_in_table(context_t* ctx,
+											  toml_table_t* tab,
+											  token_t keytok,
+											  char kind)
+{
+	/* first, normalize the key to be used for lookup. 
+	 * remember to free it if we error out. 
+	 */
+	char* newkey = normalize_key(ctx, keytok);
+	if (!newkey) return 0;
+	
+	/* if key exists: error out */
+	if (key_kind(tab, newkey)) {
+		xfree(newkey);		 /* don't need this anymore */
+		e_keyexists(ctx, keytok.lineno);
+		return 0;
+	}
+
+	/* make a new array entry */
+	int n = tab->narr;
+	toml_array_t** base;
+	if (0 == (base = (toml_array_t**) expand_ptrarr((void**)tab->arr, n))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	tab->arr = base;
+	
+	if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) {
+		xfree(newkey);
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	toml_array_t* dest = tab->arr[tab->narr++];
+
+	/* save the key in the new array struct */
+	dest->key = newkey;
+	dest->kind = kind;
+	return dest;
+}
+
+/* Create an array in an array 
+ */
+static toml_array_t* create_array_in_array(context_t* ctx,
+										   toml_array_t* parent)
+{
+	const int n = parent->nelem;
+	toml_array_t** base;
+	if (0 == (base = (toml_array_t**) expand_ptrarr((void**)parent->u.arr, n))) {
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	parent->u.arr = base;
+	parent->nelem++;
+	
+	if (0 == (base[n] = (toml_array_t*) CALLOC(1, sizeof(*base[n])))) {
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+
+	return parent->u.arr[n];
+}
+
+/* Create a table in an array 
+ */
+static toml_table_t* create_table_in_array(context_t* ctx,
+										   toml_array_t* parent)
+{
+	int n = parent->nelem;
+	toml_table_t** base;
+	if (0 == (base = (toml_table_t**) expand_ptrarr((void**)parent->u.tab, n))) {
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+	parent->u.tab = base;
+	
+	if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) {
+		e_outofmemory(ctx, FLINE);
+		return 0;
+	}
+
+	return parent->u.tab[parent->nelem++];
+}
+
+
+static int skip_newlines(context_t* ctx, int isdotspecial)
+{
+	while (ctx->tok.tok == NEWLINE) {
+		if (next_token(ctx, isdotspecial)) return -1;
+		if (ctx->tok.eof) break;
+	}
+	return 0;
+}
+
+
+static int parse_keyval(context_t* ctx, toml_table_t* tab);
+
+static inline int eat_token(context_t* ctx, tokentype_t typ, int isdotspecial, const char* fline)
+{
+	if (ctx->tok.tok != typ) 
+		return e_internal(ctx, fline);
+
+	if (next_token(ctx, isdotspecial))
+		return -1;
+
+	return 0;
+}
+
+
+
+/* We are at '{ ... }'.
+ * Parse the table.
+ */
+static int parse_table(context_t* ctx, toml_table_t* tab)
+{
+	if (eat_token(ctx, LBRACE, 1, FLINE))
+		return -1;
+
+	for (;;) {
+		if (ctx->tok.tok == NEWLINE) 
+			return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table");
+
+		/* until } */
+		if (ctx->tok.tok == RBRACE)
+			break;
+
+		if (ctx->tok.tok != STRING) 
+			return e_syntax(ctx, ctx->tok.lineno, "expect a string");
+
+		if (parse_keyval(ctx, tab))
+			return -1;
+		
+		if (ctx->tok.tok == NEWLINE) 
+			return e_syntax(ctx, ctx->tok.lineno, "newline not allowed in inline table");
+
+		/* on comma, continue to scan for next keyval */
+		if (ctx->tok.tok == COMMA) {
+			if (eat_token(ctx, COMMA, 1, FLINE))
+				return -1;
+			continue;
+		}
+		break;
+	}
+
+	if (eat_token(ctx, RBRACE, 1, FLINE))
+		return -1;
+	return 0;
+}
+
+static int valtype(const char* val)
+{
+	toml_timestamp_t ts;
+	if (*val == '\'' || *val == '"') return 's';
+	if (0 == toml_rtob(val, 0)) return 'b';
+	if (0 == toml_rtoi(val, 0)) return 'i';
+	if (0 == toml_rtod(val, 0)) return 'd';
+	if (0 == toml_rtots(val, &ts)) {
+		if (ts.year && ts.hour) return 'T'; /* timestamp */
+		if (ts.year) return 'D'; /* date */
+		return 't'; /* time */
+	}
+	return 'u'; /* unknown */
+}
+
+
+/* We are at '[...]' */
+static int parse_array(context_t* ctx, toml_array_t* arr)
+{
+	if (eat_token(ctx, LBRACKET, 0, FLINE)) return -1;
+	
+	for (;;) {
+		if (skip_newlines(ctx, 0)) return -1;
+	
+		/* until ] */
+		if (ctx->tok.tok == RBRACKET) break;
+
+		switch (ctx->tok.tok) {
+		case STRING:
+			{
+				char* val = ctx->tok.ptr;
+				int	  vlen = ctx->tok.len;
+
+				/* set array kind if this will be the first entry */
+				if (arr->kind == 0) arr->kind = 'v';
+				/* check array kind */
+				if (arr->kind != 'v') 
+					return e_syntax(ctx, ctx->tok.lineno, "a string array can only contain strings");
+
+				/* make a new value in array */
+				char** tmp = (char**) expand_ptrarr((void**)arr->u.val, arr->nelem);
+				if (!tmp) 
+					return e_outofmemory(ctx, FLINE);
+				
+				arr->u.val = tmp;
+				if (! (val = STRNDUP(val, vlen))) 
+					return e_outofmemory(ctx, FLINE);
+
+				arr->u.val[arr->nelem++] = val;
+
+				/* set array type if this is the first entry, or check that the types matched. */
+				if (arr->nelem == 1) 
+					arr->type = valtype(arr->u.val[0]);
+				else if (arr->type != valtype(val)) {
+					return e_syntax(ctx, ctx->tok.lineno,
+								   "array type mismatch while processing array of values");
+				}
+
+				if (eat_token(ctx, STRING, 0, FLINE)) return -1;
+				break;
+			}
+
+		case LBRACKET:
+			{ /* [ [array], [array] ... ] */
+				/* set the array kind if this will be the first entry */
+				if (arr->kind == 0) arr->kind = 'a';
+				/* check array kind */
+				if (arr->kind != 'a') {
+					return e_syntax(ctx, ctx->tok.lineno,
+									"array type mismatch while processing array of arrays");
+				}
+				toml_array_t* subarr = create_array_in_array(ctx, arr);
+				if (!subarr) return -1;
+				if (parse_array(ctx, subarr)) return -1;
+				break;
+			}
+
+		case LBRACE:
+			{ /* [ {table}, {table} ... ] */
+				/* set the array kind if this will be the first entry */
+				if (arr->kind == 0) arr->kind = 't';
+				/* check array kind */
+				if (arr->kind != 't') {
+					return e_syntax(ctx, ctx->tok.lineno,
+									"array type mismatch while processing array of tables");
+				}
+				toml_table_t* subtab = create_table_in_array(ctx, arr);
+				if (!subtab) return -1;
+				if (parse_table(ctx, subtab)) return -1;
+				break;
+			}
+		
+		default:
+			return e_syntax(ctx, ctx->tok.lineno, "syntax error");
+		}
+
+		if (skip_newlines(ctx, 0)) return -1;
+
+		/* on comma, continue to scan for next element */
+		if (ctx->tok.tok == COMMA) {
+			if (eat_token(ctx, COMMA, 0, FLINE)) return -1;
+			continue;
+		}
+		break;
+	}
+
+	if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1;
+	return 0;
+}
+
+
+/* handle lines like these:
+   key = "value"
+   key = [ array ]
+   key = { table }
+*/
+static int parse_keyval(context_t* ctx, toml_table_t* tab)
+{
+	token_t key = ctx->tok;
+	if (eat_token(ctx, STRING, 1, FLINE)) return -1;
+	
+	if (ctx->tok.tok == DOT) {	
+	/* handle inline dotted key. 
+		   e.g. 
+		   physical.color = "orange"
+		   physical.shape = "round"
+		*/
+		toml_table_t* subtab = 0;
+		{
+			char* subtabstr = normalize_key(ctx, key);
+			subtab = toml_table_in(tab, subtabstr);
+			xfree(subtabstr);
+		}
+		if (!subtab) {
+			subtab = create_keytable_in_table(ctx, tab, key);
+			if (!subtab) return -1;
+		}
+		if (next_token(ctx, 1)) return -1;
+		if (parse_keyval(ctx, subtab)) return -1;
+		return 0;
+	}
+
+	if (ctx->tok.tok != EQUAL) {
+		return e_syntax(ctx, ctx->tok.lineno, "missing =");
+	}
+
+	if (next_token(ctx, 0)) return -1;
+
+	switch (ctx->tok.tok) {
+	case STRING:
+		{ /* key = "value" */
+			toml_keyval_t* keyval = create_keyval_in_table(ctx, tab, key);
+			if (!keyval) return -1;
+			token_t val = ctx->tok;
+			
+			assert(keyval->val == 0);
+			if (! (keyval->val = STRNDUP(val.ptr, val.len))) 
+				return e_outofmemory(ctx, FLINE);
+
+			if (next_token(ctx, 1)) return -1;
+		
+			return 0;
+		}
+
+	case LBRACKET:
+		{ /* key = [ array ] */
+			toml_array_t* arr = create_keyarray_in_table(ctx, tab, key, 0);
+			if (!arr) return -1;
+			if (parse_array(ctx, arr)) return -1;
+			return 0;
+		}
+
+	case LBRACE:
+		{ /* key = { table } */
+			toml_table_t* nxttab = create_keytable_in_table(ctx, tab, key);
+			if (!nxttab) return -1;
+			if (parse_table(ctx, nxttab)) return -1;
+			return 0;
+		}
+
+	default:
+		return e_syntax(ctx, ctx->tok.lineno, "syntax error");
+	}
+	return 0;
+}
+
+
+typedef struct tabpath_t tabpath_t;
+struct tabpath_t {
+	int		cnt;
+	token_t key[10];
+};
+
+/* at [x.y.z] or [[x.y.z]]
+ * Scan forward and fill tabpath until it enters ] or ]]
+ * There will be at least one entry on return.
+ */
+static int fill_tabpath(context_t* ctx)
+{
+	int lineno = ctx->tok.lineno;
+	int i;
+	
+	/* clear tpath */
+	for (i = 0; i < ctx->tpath.top; i++) {
+		char** p = &ctx->tpath.key[i];
+		xfree(*p);
+		*p = 0;
+	}
+	ctx->tpath.top = 0;
+	
+	for (;;) {
+		if (ctx->tpath.top >= 10)
+			return e_syntax(ctx, lineno, "table path is too deep; max allowed is 10.");
+
+		if (ctx->tok.tok != STRING) 
+			return e_syntax(ctx, lineno, "invalid or missing key");
+
+		char* key = normalize_key(ctx, ctx->tok);
+		if (!key) return -1;
+		ctx->tpath.tok[ctx->tpath.top] = ctx->tok;
+		ctx->tpath.key[ctx->tpath.top] = key;
+		ctx->tpath.top++;
+	
+		if (next_token(ctx, 1)) return -1;
+
+		if (ctx->tok.tok == RBRACKET) break;
+
+		if (ctx->tok.tok != DOT) 
+			return e_syntax(ctx, lineno, "invalid key");
+
+		if (next_token(ctx, 1)) return -1;
+	}
+
+	if (ctx->tpath.top <= 0)
+		return e_syntax(ctx, lineno, "empty table selector");
+
+	return 0;
+}
+
+
+/* Walk tabpath from the root, and create new tables on the way.
+ * Sets ctx->curtab to the final table.
+ */
+static int walk_tabpath(context_t* ctx)
+{
+	/* start from root */
+	toml_table_t* curtab = ctx->root;
+	
+	for (int i = 0; i < ctx->tpath.top; i++) {
+		const char* key = ctx->tpath.key[i];
+
+		toml_keyval_t* nextval = 0;
+		toml_array_t* nextarr = 0;
+		toml_table_t* nexttab = 0;
+		switch (check_key(curtab, key, &nextval, &nextarr, &nexttab)) {
+		case 't':
+			/* found a table. nexttab is where we will go next. */
+			break;
+
+		case 'a':
+			/* found an array. nexttab is the last table in the array. */
+			if (nextarr->kind != 't') 
+				return e_internal(ctx, FLINE);
+
+			if (nextarr->nelem == 0) 
+				return e_internal(ctx, FLINE);
+
+			nexttab = nextarr->u.tab[nextarr->nelem-1];
+			break;
+
+		case 'v':
+			return e_keyexists(ctx, ctx->tpath.tok[i].lineno);
+
+		default:
+			{ /* Not found. Let's create an implicit table. */
+				int n = curtab->ntab;
+				toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)curtab->tab, n);
+				if (0 == base) 
+					return e_outofmemory(ctx, FLINE);
+
+				curtab->tab = base;
+		
+				if (0 == (base[n] = (toml_table_t*) CALLOC(1, sizeof(*base[n])))) 
+					return e_outofmemory(ctx, FLINE);
+		
+				if (0 == (base[n]->key = STRDUP(key))) 
+					return e_outofmemory(ctx, FLINE);
+		
+				nexttab = curtab->tab[curtab->ntab++];
+		
+				/* tabs created by walk_tabpath are considered implicit */
+				nexttab->implicit = true;
+			}
+			break;
+		}
+
+		/* switch to next tab */
+		curtab = nexttab;
+	}
+
+	/* save it */
+	ctx->curtab = curtab;
+
+	return 0;
+}
+
+	
+/* handle lines like [x.y.z] or [[x.y.z]] */
+static int parse_select(context_t* ctx)
+{
+	assert(ctx->tok.tok == LBRACKET);
+	
+	/* true if [[ */
+	int llb = (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == '[');
+	/* need to detect '[[' on our own because next_token() will skip whitespace, 
+	   and '[ [' would be taken as '[[', which is wrong. */
+
+	/* eat [ or [[ */
+	if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1;
+	if (llb) {
+		assert(ctx->tok.tok == LBRACKET);
+		if (eat_token(ctx, LBRACKET, 1, FLINE)) return -1;
+	}
+
+	if (fill_tabpath(ctx)) return -1;
+
+	/* For [x.y.z] or [[x.y.z]], remove z from tpath. 
+	 */
+	token_t z = ctx->tpath.tok[ctx->tpath.top-1];
+	xfree(ctx->tpath.key[ctx->tpath.top-1]);
+	ctx->tpath.top--;
+
+	/* set up ctx->curtab */
+	if (walk_tabpath(ctx)) return -1;
+
+	if (! llb) {
+		/* [x.y.z] -> create z = {} in x.y */
+		toml_table_t* curtab = create_keytable_in_table(ctx, ctx->curtab, z);
+		if (!curtab) return -1;
+		ctx->curtab = curtab;
+	} else {
+		/* [[x.y.z]] -> create z = [] in x.y */
+		toml_array_t* arr = 0;
+		{
+			char* zstr = normalize_key(ctx, z);
+			if (!zstr) return -1;
+			arr = toml_array_in(ctx->curtab, zstr);
+			xfree(zstr);
+		}
+		if (!arr) {
+			arr = create_keyarray_in_table(ctx, ctx->curtab, z, 't');
+			if (!arr) return -1;
+		}
+		if (arr->kind != 't') 
+			return e_syntax(ctx, z.lineno, "array mismatch");
+
+		/* add to z[] */
+		toml_table_t* dest;
+		{
+			int n = arr->nelem;
+			toml_table_t** base = (toml_table_t**) expand_ptrarr((void**)arr->u.tab, n);
+			if (0 == base) 
+				return e_outofmemory(ctx, FLINE);
+
+			arr->u.tab = base;
+		
+			if (0 == (base[n] = CALLOC(1, sizeof(*base[n])))) 
+				return e_outofmemory(ctx, FLINE);
+		
+			if (0 == (base[n]->key = STRDUP("__anon__"))) 
+				return e_outofmemory(ctx, FLINE);
+		
+			dest = arr->u.tab[arr->nelem++];
+		}
+
+		ctx->curtab = dest;
+	}
+
+	if (ctx->tok.tok != RBRACKET) {
+		return e_syntax(ctx, ctx->tok.lineno, "expects ]");
+	}
+	if (llb) {
+		if (! (ctx->tok.ptr + 1 < ctx->stop && ctx->tok.ptr[1] == ']')) {
+			return e_syntax(ctx, ctx->tok.lineno, "expects ]]");
+		}
+		if (eat_token(ctx, RBRACKET, 1, FLINE)) return -1;
+	}
+	
+	if (eat_token(ctx, RBRACKET, 1, FLINE))
+		return -1;
+	
+	if (ctx->tok.tok != NEWLINE) 
+		return e_syntax(ctx, ctx->tok.lineno, "extra chars after ] or ]]");
+
+	return 0;
+}
+
+
+
+
+toml_table_t* toml_parse(char* conf,
+						 char* errbuf,
+						 int errbufsz)
+{
+	context_t ctx;
+
+	// clear errbuf 
+	if (errbufsz <= 0) errbufsz = 0;
+	if (errbufsz > 0)  errbuf[0] = 0;
+
+	// init context 
+	memset(&ctx, 0, sizeof(ctx));
+	ctx.start = conf;
+	ctx.stop = ctx.start + strlen(conf);
+	ctx.errbuf = errbuf;
+	ctx.errbufsz = errbufsz;
+
+	// start with an artificial newline of length 0
+	ctx.tok.tok = NEWLINE; 
+	ctx.tok.lineno = 1;
+	ctx.tok.ptr = conf;
+	ctx.tok.len = 0;
+
+	// make a root table
+	if (0 == (ctx.root = CALLOC(1, sizeof(*ctx.root)))) {
+		e_outofmemory(&ctx, FLINE);
+		// Do not goto fail, root table not set up yet
+		return 0;
+	}
+
+	// set root as default table
+	ctx.curtab = ctx.root;
+
+	/* Scan forward until EOF */
+	for (token_t tok = ctx.tok; ! tok.eof ; tok = ctx.tok) {
+		switch (tok.tok) {
+		
+		case NEWLINE:
+			if (next_token(&ctx, 1)) goto fail;
+			break;
+		
+		case STRING:
+			if (parse_keyval(&ctx, ctx.curtab)) goto fail;
+			
+			if (ctx.tok.tok != NEWLINE) {
+				e_syntax(&ctx, ctx.tok.lineno, "extra chars after value");
+				goto fail;
+			}
+
+			if (eat_token(&ctx, NEWLINE, 1, FLINE)) goto fail;
+			break;
+		
+		case LBRACKET:	/* [ x.y.z ] or [[ x.y.z ]] */
+			if (parse_select(&ctx)) goto fail;
+			break;
+		
+		default:
+			e_syntax(&ctx, tok.lineno, "syntax error");
+			goto fail;
+		}
+	}
+
+	/* success */
+	for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]);
+	return ctx.root;
+
+fail:
+	// Something bad has happened. Free resources and return error.
+	for (int i = 0; i < ctx.tpath.top; i++) xfree(ctx.tpath.key[i]);
+	toml_free(ctx.root);
+	return 0;
+}
+
+
+toml_table_t* toml_parse_file(FILE* fp,
+							  char* errbuf,
+							  int errbufsz)
+{
+	int bufsz = 0;
+	char* buf = 0;
+	int off = 0;
+
+	/* read from fp into buf */
+	while (! feof(fp)) {
+
+		if (off == bufsz) {
+			int xsz = bufsz + 1000;
+			char* x = expand(buf, bufsz, xsz);
+			if (!x) {
+				snprintf(errbuf, errbufsz, "out of memory");
+				xfree(buf);
+				return 0;
+			}
+			buf = x;
+			bufsz = xsz;
+		}
+	
+		errno = 0;
+		int n = fread(buf + off, 1, bufsz - off, fp);
+		if (ferror(fp)) {
+			snprintf(errbuf, errbufsz, "%s",
+					 errno ? strerror(errno) : "Error reading file");
+			xfree(buf);
+			return 0;
+		}
+		off += n;
+	}
+
+	/* tag on a NUL to cap the string */
+	if (off == bufsz) {
+		int xsz = bufsz + 1;
+		char* x = expand(buf, bufsz, xsz);
+		if (!x) {
+			snprintf(errbuf, errbufsz, "out of memory");
+			xfree(buf);
+			return 0;
+		}
+		buf = x;
+		bufsz = xsz;
+	}
+	buf[off] = 0; 
+
+	/* parse it, cleanup and finish */
+	toml_table_t* ret = toml_parse(buf, errbuf, errbufsz);
+	xfree(buf);
+	return ret;
+}
+
+
+static void xfree_kval(toml_keyval_t* p)
+{
+	if (!p) return;
+	xfree(p->key);
+	xfree(p->val);
+	xfree(p);
+}
+
+static void xfree_tab(toml_table_t* p);
+
+static void xfree_arr(toml_array_t* p)
+{
+	if (!p) return;
+
+	xfree(p->key);
+	switch (p->kind) {
+	case 'v':
+		for (int i = 0; i < p->nelem; i++) xfree(p->u.val[i]);
+		xfree(p->u.val);
+		break;
+
+	case 'a':
+		for (int i = 0; i < p->nelem; i++) xfree_arr(p->u.arr[i]);
+		xfree(p->u.arr);
+		break;
+
+	case 't':
+		for (int i = 0; i < p->nelem; i++) xfree_tab(p->u.tab[i]);
+		xfree(p->u.tab);
+		break;
+	}
+
+	xfree(p);
+}
+
+
+static void xfree_tab(toml_table_t* p)
+{
+	int i;
+	
+	if (!p) return;
+	
+	xfree(p->key);
+	
+	for (i = 0; i < p->nkval; i++) xfree_kval(p->kval[i]);
+	xfree(p->kval);
+
+	for (i = 0; i < p->narr; i++) xfree_arr(p->arr[i]);
+	xfree(p->arr);
+
+	for (i = 0; i < p->ntab; i++) xfree_tab(p->tab[i]);
+	xfree(p->tab);
+
+	xfree(p);
+}
+
+
+void toml_free(toml_table_t* tab)
+{
+	xfree_tab(tab);
+}
+
+
+static void set_token(context_t* ctx, tokentype_t tok, int lineno, char* ptr, int len)
+{
+	token_t t;
+	t.tok = tok;
+	t.lineno = lineno;
+	t.ptr = ptr;
+	t.len = len;
+	t.eof = 0;
+	ctx->tok = t;
+}
+
+static void set_eof(context_t* ctx, int lineno)
+{
+	set_token(ctx, NEWLINE, lineno, ctx->stop, 0);
+	ctx->tok.eof = 1;
+}
+
+
+/* Scan p for n digits compositing entirely of [0-9] */
+static int scan_digits(const char* p, int n)
+{
+	int ret = 0;
+	for ( ; n > 0 && isdigit(*p); n--, p++) {
+		ret = 10 * ret + (*p - '0');
+	}
+	return n ? -1 : ret;
+}
+
+static int scan_date(const char* p, int* YY, int* MM, int* DD)
+{
+	int year, month, day;
+	year = scan_digits(p, 4);
+	month = (year >= 0 && p[4] == '-') ? scan_digits(p+5, 2) : -1;
+	day = (month >= 0 && p[7] == '-') ? scan_digits(p+8, 2) : -1;
+	if (YY) *YY = year;
+	if (MM) *MM = month;
+	if (DD) *DD = day;
+	return (year >= 0 && month >= 0 && day >= 0) ? 0 : -1;
+}
+
+static int scan_time(const char* p, int* hh, int* mm, int* ss)
+{
+	int hour, minute, second;
+	hour = scan_digits(p, 2);
+	minute = (hour >= 0 && p[2] == ':') ? scan_digits(p+3, 2) : -1;
+	second = (minute >= 0 && p[5] == ':') ? scan_digits(p+6, 2) : -1;
+	if (hh) *hh = hour;
+	if (mm) *mm = minute;
+	if (ss) *ss = second;
+	return (hour >= 0 && minute >= 0 && second >= 0) ? 0 : -1;
+}
+	
+
+static int scan_string(context_t* ctx, char* p, int lineno, int dotisspecial)
+{
+	char* orig = p;
+	if (0 == strncmp(p, "'''", 3)) {
+		p = strstr(p + 3, "'''");
+		if (0 == p) {
+			return e_syntax(ctx, lineno, "unterminated triple-s-quote");
+		}
+
+		set_token(ctx, STRING, lineno, orig, p + 3 - orig);
+		return 0;
+	}
+
+	if (0 == strncmp(p, "\"\"\"", 3)) {
+		int hexreq = 0;		/* #hex required */
+		int escape = 0;
+		int qcnt = 0;		/* count quote */
+		for (p += 3; *p && qcnt < 3; p++) {
+			if (escape) {
+				escape = 0;
+				if (strchr("btnfr\"\\", *p)) continue;
+				if (*p == 'u') { hexreq = 4; continue; }
+				if (*p == 'U') { hexreq = 8; continue; }
+				if (p[strspn(p, " \t\r")] == '\n') continue; /* allow for line ending backslash */
+				return e_syntax(ctx, lineno, "bad escape char");
+			}
+			if (hexreq) {
+				hexreq--;
+				if (strchr("0123456789ABCDEF", *p)) continue;
+				return e_syntax(ctx, lineno, "expect hex char");
+			}
+			if (*p == '\\') { escape = 1; continue; }
+			qcnt = (*p == '"') ? qcnt + 1 : 0; 
+		}
+		if (qcnt != 3) {
+			return e_syntax(ctx, lineno, "unterminated triple-quote");
+		}
+
+		set_token(ctx, STRING, lineno, orig, p - orig);
+		return 0;
+	}
+
+	if ('\'' == *p) {
+		for (p++; *p && *p != '\n' && *p != '\''; p++);
+		if (*p != '\'') {
+			return e_syntax(ctx, lineno, "unterminated s-quote");
+		}
+
+		set_token(ctx, STRING, lineno, orig, p + 1 - orig);
+		return 0;
+	}
+
+	if ('\"' == *p) {
+		int hexreq = 0;		/* #hex required */
+		int escape = 0;
+		for (p++; *p; p++) {
+			if (escape) {
+				escape = 0;
+				if (strchr("btnfr\"\\", *p)) continue;
+				if (*p == 'u') { hexreq = 4; continue; }
+				if (*p == 'U') { hexreq = 8; continue; }
+				return e_syntax(ctx, lineno, "bad escape char");
+			}
+			if (hexreq) {
+				hexreq--;
+				if (strchr("0123456789ABCDEF", *p)) continue;
+				return e_syntax(ctx, lineno, "expect hex char");
+			}
+			if (*p == '\\') { escape = 1; continue; }
+			if (*p == '\n') break;
+			if (*p == '"') break;
+		}
+		if (*p != '"') {
+			return e_syntax(ctx, lineno, "unterminated quote");
+		}
+
+		set_token(ctx, STRING, lineno, orig, p + 1 - orig);
+		return 0;
+	}
+
+	/* check for timestamp without quotes */
+	if (0 == scan_date(p, 0, 0, 0) || 0 == scan_time(p, 0, 0, 0)) {
+		// forward thru the timestamp
+		for ( ; strchr("0123456789.:+-T Z", toupper(*p)); p++);
+		// squeeze out any spaces at end of string
+		for ( ; p[-1] == ' '; p--);
+		// tokenize
+		set_token(ctx, STRING, lineno, orig, p - orig);
+		return 0;
+	}
+
+	/* literals */
+	for ( ; *p && *p != '\n'; p++) {
+		int ch = *p;
+		if (ch == '.' && dotisspecial) break;
+		if ('A' <= ch && ch <= 'Z') continue;
+		if ('a' <= ch && ch <= 'z') continue;
+		if (strchr("0123456789+-_.", ch)) continue;
+		break;
+	}
+
+	set_token(ctx, STRING, lineno, orig, p - orig);
+	return 0;
+}
+
+
+static int next_token(context_t* ctx, int dotisspecial)
+{
+	int	  lineno = ctx->tok.lineno;
+	char* p = ctx->tok.ptr;
+	int i;
+
+	/* eat this tok */
+	for (i = 0; i < ctx->tok.len; i++) {
+		if (*p++ == '\n')
+			lineno++;
+	}
+
+	/* make next tok */
+	while (p < ctx->stop) {
+		/* skip comment. stop just before the \n. */
+		if (*p == '#') {
+			for (p++; p < ctx->stop && *p != '\n'; p++);
+			continue;
+		}
+
+		if (dotisspecial && *p == '.') {
+			set_token(ctx, DOT, lineno, p, 1);
+			return 0;
+		}
+	
+		switch (*p) {
+		case ',': set_token(ctx, COMMA, lineno, p, 1); return 0;
+		case '=': set_token(ctx, EQUAL, lineno, p, 1); return 0;
+		case '{': set_token(ctx, LBRACE, lineno, p, 1); return 0;
+		case '}': set_token(ctx, RBRACE, lineno, p, 1); return 0;
+		case '[': set_token(ctx, LBRACKET, lineno, p, 1); return 0;
+		case ']': set_token(ctx, RBRACKET, lineno, p, 1); return 0;
+		case '\n': set_token(ctx, NEWLINE, lineno, p, 1); return 0;
+		case '\r': case ' ': case '\t':
+			/* ignore white spaces */
+			p++;
+			continue;
+		}
+
+		return scan_string(ctx, p, lineno, dotisspecial);
+	}
+
+	set_eof(ctx, lineno);
+	return 0;
+}
+
+
+const char* toml_key_in(const toml_table_t* tab, int keyidx)
+{
+	if (keyidx < tab->nkval) return tab->kval[keyidx]->key;
+	
+	keyidx -= tab->nkval;
+	if (keyidx < tab->narr)	 return tab->arr[keyidx]->key;
+	
+	keyidx -= tab->narr;
+	if (keyidx < tab->ntab)	 return tab->tab[keyidx]->key;
+
+	return 0;
+}
+
+toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key)
+{
+	int i;
+	for (i = 0; i < tab->nkval; i++) {
+		if (0 == strcmp(key, tab->kval[i]->key))
+			return tab->kval[i]->val;
+	}
+	return 0;
+}
+
+toml_array_t* toml_array_in(const toml_table_t* tab, const char* key)
+{
+	int i;
+	for (i = 0; i < tab->narr; i++) {
+		if (0 == strcmp(key, tab->arr[i]->key))
+			return tab->arr[i];
+	}
+	return 0;
+}
+
+
+toml_table_t* toml_table_in(const toml_table_t* tab, const char* key)
+{
+	int i;
+	for (i = 0; i < tab->ntab; i++) {
+		if (0 == strcmp(key, tab->tab[i]->key))
+			return tab->tab[i];
+	}
+	return 0;
+}
+
+toml_raw_t toml_raw_at(const toml_array_t* arr, int idx)
+{
+	if (arr->kind != 'v')
+		return 0;
+	if (! (0 <= idx && idx < arr->nelem))
+		return 0;
+	return arr->u.val[idx];
+}
+
+char toml_array_kind(const toml_array_t* arr)
+{
+	return arr->kind;
+}
+
+char toml_array_type(const toml_array_t* arr)
+{
+	if (arr->kind != 'v')
+		return 0;
+
+	if (arr->nelem == 0)
+		return 0;
+
+	return arr->type;
+}
+
+
+int toml_array_nelem(const toml_array_t* arr)
+{
+	return arr->nelem;
+}
+
+const char* toml_array_key(const toml_array_t* arr)
+{
+	return arr ? arr->key : (const char*) NULL;
+}
+
+int toml_table_nkval(const toml_table_t* tab)
+{
+	return tab->nkval;
+}
+
+int toml_table_narr(const toml_table_t* tab)
+{
+	return tab->narr;
+}
+
+int toml_table_ntab(const toml_table_t* tab)
+{
+	return tab->ntab;
+}
+
+const char* toml_table_key(const toml_table_t* tab)
+{
+	return tab ? tab->key : (const char*) NULL;
+}
+
+toml_array_t* toml_array_at(const toml_array_t* arr, int idx)
+{
+	if (arr->kind != 'a')
+		return 0;
+	if (! (0 <= idx && idx < arr->nelem))
+		return 0;
+	return arr->u.arr[idx];
+}
+
+toml_table_t* toml_table_at(const toml_array_t* arr, int idx)
+{
+	if (arr->kind != 't')
+		return 0;
+	if (! (0 <= idx && idx < arr->nelem))
+		return 0;
+	return arr->u.tab[idx];
+}
+
+
+int toml_rtots(toml_raw_t src_, toml_timestamp_t* ret)
+{
+	if (! src_) return -1;
+	
+	const char* p = src_;
+	int must_parse_time = 0;
+	
+	memset(ret, 0, sizeof(*ret));
+
+	int* year = &ret->__buffer.year;
+	int* month = &ret->__buffer.month;
+	int* day = &ret->__buffer.day;
+	int* hour = &ret->__buffer.hour;
+	int* minute = &ret->__buffer.minute;
+	int* second = &ret->__buffer.second;
+	int* millisec = &ret->__buffer.millisec;
+
+	/* parse date YYYY-MM-DD */
+	if (0 == scan_date(p, year, month, day)) {
+		ret->year = year;
+		ret->month = month;
+		ret->day = day;
+		
+		p += 10;
+		if (*p) {
+			// parse the T or space separator
+			if (*p != 'T' && *p != ' ') return -1;
+			must_parse_time = 1;
+			p++;
+		}
+	}
+
+	/* parse time HH:MM:SS */
+	if (0 == scan_time(p, hour, minute, second)) {
+		ret->hour	= hour;
+		ret->minute = minute;
+		ret->second = second;
+
+		/* optionally, parse millisec */
+		p += 8;
+		if (*p == '.') {
+			char* qq;
+			p++;
+			errno = 0;
+			*millisec = strtol(p, &qq, 0);
+			if (errno) {
+				return -1;
+			}
+			while (*millisec > 999) {
+				*millisec /= 10;
+			}
+
+			ret->millisec = millisec;
+			p = qq;
+		}
+
+		if (*p) {
+			/* parse and copy Z */
+			char* z = ret->__buffer.z;
+			ret->z = z;
+			if (*p == 'Z' || *p == 'z') {
+				*z++ = 'Z'; p++;
+				*z = 0;
+				
+			} else if (*p == '+' || *p == '-') {
+				*z++ = *p++;
+				
+				if (! (isdigit(p[0]) && isdigit(p[1]))) return -1;
+				*z++ = *p++;
+				*z++ = *p++;
+				
+				if (*p == ':') {
+					*z++ = *p++;
+					
+					if (! (isdigit(p[0]) && isdigit(p[1]))) return -1;
+					*z++ = *p++;
+					*z++ = *p++;
+				}
+				
+				*z = 0;
+			}
+		}
+	}
+	if (*p != 0)
+		return -1;
+	
+	if (must_parse_time && !ret->hour)
+		return -1;
+
+	return 0;
+}
+
+
+/* Raw to boolean */
+int toml_rtob(toml_raw_t src, int* ret_)
+{
+	if (!src) return -1;
+	int dummy;
+	int* ret = ret_ ? ret_ : &dummy;
+	
+	if (0 == strcmp(src, "true")) {
+		*ret = 1;
+		return 0;
+	}
+	if (0 == strcmp(src, "false")) {
+		*ret = 0;
+		return 0;
+	}
+	return -1;
+}
+
+
+/* Raw to integer */
+int toml_rtoi(toml_raw_t src, int64_t* ret_)
+{
+	if (!src) return -1;
+	
+	char buf[100];
+	char* p = buf;
+	char* q = p + sizeof(buf);
+	const char* s = src;
+	int base = 0;
+	int64_t dummy;
+	int64_t* ret = ret_ ? ret_ : &dummy;
+	
+
+	/* allow +/- */
+	if (s[0] == '+' || s[0] == '-')
+		*p++ = *s++;
+	
+	/* disallow +_100 */
+	if (s[0] == '_')
+		return -1;
+
+	/* if 0 ... */
+	if ('0' == s[0]) {
+		switch (s[1]) {
+		case 'x': base = 16; s += 2; break;
+		case 'o': base = 8; s += 2; break;
+		case 'b': base = 2; s += 2; break;
+		case '\0': return *ret = 0, 0;
+		default:
+			/* ensure no other digits after it */
+			if (s[1]) return -1;
+		}
+	}
+
+	/* just strip underscores and pass to strtoll */
+	while (*s && p < q) {
+		int ch = *s++;
+		switch (ch) {
+		case '_':
+			// disallow '__'
+			if (s[0] == '_') return -1; 
+			continue;			/* skip _ */
+		default:
+			break;
+		}
+		*p++ = ch;
+	}
+	if (*s || p == q) return -1;
+
+	/* last char cannot be '_' */
+	if (s[-1] == '_') return -1;
+	
+	/* cap with NUL */
+	*p = 0;
+
+	/* Run strtoll on buf to get the integer */
+	char* endp;
+	errno = 0;
+	*ret = strtoll(buf, &endp, base);
+	return (errno || *endp) ? -1 : 0;
+}
+
+
+int toml_rtod_ex(toml_raw_t src, double* ret_, char* buf, int buflen)
+{
+	if (!src) return -1;
+	
+	char* p = buf;
+	char* q = p + buflen;
+	const char* s = src;
+	double dummy;
+	double* ret = ret_ ? ret_ : &dummy;
+	
+
+	/* allow +/- */
+	if (s[0] == '+' || s[0] == '-')
+		*p++ = *s++;
+
+	/* disallow +_1.00 */
+	if (s[0] == '_')
+		return -1;
+
+	/* disallow +.99 */
+	if (s[0] == '.')
+		return -1;
+		
+	/* zero must be followed by . or 'e', or NUL */
+	if (s[0] == '0' && s[1] && !strchr("eE.", s[1]))
+		return -1;
+
+	/* just strip underscores and pass to strtod */
+	while (*s && p < q) {
+		int ch = *s++;
+		switch (ch) {
+		case '.':
+			if (s[-2] == '_') return -1;
+			if (s[0] == '_') return -1;
+			break;
+		case '_':
+			// disallow '__'
+			if (s[0] == '_') return -1; 
+			continue;			/* skip _ */
+		default:
+			break;
+		}
+		*p++ = ch;
+	}
+	if (*s || p == q) return -1; /* reached end of string or buffer is full? */
+	
+	/* last char cannot be '_' */
+	if (s[-1] == '_') return -1;
+
+	if (p != buf && p[-1] == '.') 
+		return -1; /* no trailing zero */
+
+	/* cap with NUL */
+	*p = 0;
+
+	/* Run strtod on buf to get the value */
+	char* endp;
+	errno = 0;
+	*ret = strtod(buf, &endp);
+	return (errno || *endp) ? -1 : 0;
+}
+
+int toml_rtod(toml_raw_t src, double* ret_)
+{
+	char buf[100];
+	return toml_rtod_ex(src, ret_, buf, sizeof(buf));
+}
+
+
+
+
+int toml_rtos(toml_raw_t src, char** ret)
+{
+	int multiline = 0;
+	const char* sp;
+	const char* sq;
+	
+	*ret = 0;
+	if (!src) return -1;
+
+	int qchar = src[0];
+	int srclen = strlen(src);
+	if (! (qchar == '\'' || qchar == '"')) {
+		return -1;
+	}
+	
+	// triple quotes?
+	if (qchar == src[1] && qchar == src[2]) {
+		multiline = 1;
+		sp = src + 3;
+		sq = src + srclen - 3;
+		/* last 3 chars in src must be qchar */
+		if (! (sp <= sq && sq[0] == qchar && sq[1] == qchar && sq[2] == qchar)) 
+			return -1;
+		
+		/* skip new line immediate after qchar */
+		if (sp[0] == '\n')
+			sp++;
+		else if (sp[0] == '\r' && sp[1] == '\n')
+			sp += 2;
+		
+	} else {
+		sp = src + 1;
+		sq = src + srclen - 1;
+		/* last char in src must be qchar */
+		if (! (sp <= sq && *sq == qchar))
+			return -1;
+	}
+	
+	if (qchar == '\'') {
+		*ret = norm_lit_str(sp, sq - sp,
+							multiline,
+							0, 0);
+	} else {
+		*ret = norm_basic_str(sp, sq - sp,
+							  multiline,
+							  0, 0);
+	}
+	
+	return *ret ? 0 : -1;
+}
+
+
+toml_datum_t toml_string_at(const toml_array_t* arr, int idx)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_at(arr, idx);
+	if (raw) {
+		ret.ok = (0 == toml_rtos(raw, &ret.u.s));
+	}
+	return ret;
+}
+
+toml_datum_t toml_bool_at(const toml_array_t* arr, int idx)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_at(arr, idx);
+	if (raw) {
+		ret.ok = (0 == toml_rtob(raw, &ret.u.b));
+	}
+	return ret;
+}
+
+toml_datum_t toml_int_at(const toml_array_t* arr, int idx)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_at(arr, idx);
+	if (raw) {
+		ret.ok = (0 == toml_rtoi(raw, &ret.u.i));
+	}
+	return ret;
+}	
+
+toml_datum_t toml_double_at(const toml_array_t* arr, int idx)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_at(arr, idx);
+	if (raw) {
+		ret.ok = (0 == toml_rtod(raw, &ret.u.d));
+	}
+	return ret;
+}	
+
+toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx)
+{
+	toml_timestamp_t ts;
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_at(arr, idx);
+	if (raw) {
+		ret.ok = (0 == toml_rtots(raw, &ts));
+		if (ret.ok) {
+			ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts)));
+			if (ret.ok) {
+				*ret.u.ts = ts;
+			}
+		}
+	}
+	return ret;
+}	
+
+toml_datum_t toml_string_in(const toml_table_t* arr, const char* key)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_in(arr, key);
+	if (raw) {
+		ret.ok = (0 == toml_rtos(raw, &ret.u.s));
+	}
+	return ret;
+}
+
+toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_in(arr, key);
+	if (raw) {
+		ret.ok = (0 == toml_rtob(raw, &ret.u.b));
+	}
+	return ret;
+}
+
+toml_datum_t toml_int_in(const toml_table_t* arr, const char* key)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_in(arr, key);
+	if (raw) {
+		ret.ok = (0 == toml_rtoi(raw, &ret.u.i));
+	}
+	return ret;
+}	
+
+toml_datum_t toml_double_in(const toml_table_t* arr, const char* key)
+{
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_in(arr, key);
+	if (raw) {
+		ret.ok = (0 == toml_rtod(raw, &ret.u.d));
+	}
+	return ret;
+}	
+
+toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key)
+{
+	toml_timestamp_t ts;
+	toml_datum_t ret;
+	memset(&ret, 0, sizeof(ret));
+	toml_raw_t raw = toml_raw_in(arr, key);
+	if (raw) {
+		ret.ok = (0 == toml_rtots(raw, &ts));
+		if (ret.ok) {
+			ret.ok = !!(ret.u.ts = malloc(sizeof(*ret.u.ts)));
+			if (ret.ok) {
+				*ret.u.ts = ts;
+			}
+		}
+	}
+	return ret;
+}	
diff --git a/vendor/toml.h b/vendor/toml.h
new file mode 100644
index 0000000..19f6f64
--- /dev/null
+++ b/vendor/toml.h
@@ -0,0 +1,175 @@
+/*
+  MIT License
+  
+  Copyright (c) 2017 - 2019 CK Tan
+  https://github.com/cktan/tomlc99
+  
+  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 TOML_H
+#define TOML_H
+
+
+#include <stdio.h>
+#include <stdint.h>
+
+
+#ifdef __cplusplus
+#define TOML_EXTERN extern "C"
+#else
+#define TOML_EXTERN extern
+#endif
+
+typedef struct toml_timestamp_t toml_timestamp_t;
+typedef struct toml_table_t toml_table_t;
+typedef struct toml_array_t toml_array_t;
+typedef struct toml_datum_t toml_datum_t;
+
+/* Parse a file. Return a table on success, or 0 otherwise. 
+ * Caller must toml_free(the-return-value) after use.
+ */
+TOML_EXTERN toml_table_t* toml_parse_file(FILE* fp, 
+										  char* errbuf,
+										  int errbufsz);
+
+/* Parse a string containing the full config. 
+ * Return a table on success, or 0 otherwise.
+ * Caller must toml_free(the-return-value) after use.
+ */
+TOML_EXTERN toml_table_t* toml_parse(char* conf, /* NUL terminated, please. */
+									 char* errbuf,
+									 int errbufsz);
+
+/* Free the table returned by toml_parse() or toml_parse_file(). Once 
+ * this function is called, any handles accessed through this tab 
+ * directly or indirectly are no longer valid.
+ */
+TOML_EXTERN void toml_free(toml_table_t* tab);
+
+
+/* Timestamp types. The year, month, day, hour, minute, second, z 
+ * fields may be NULL if they are not relevant. e.g. In a DATE
+ * type, the hour, minute, second and z fields will be NULLs.
+ */
+struct toml_timestamp_t {
+	struct { /* internal. do not use. */
+		int year, month, day;
+		int hour, minute, second, millisec;
+		char z[10];
+	} __buffer;
+	int *year, *month, *day;
+	int *hour, *minute, *second, *millisec;
+	char* z;
+};
+
+
+/*-----------------------------------------------------------------
+ *  Enhanced access methods 
+ */
+struct toml_datum_t {
+	int ok;
+	union {
+		toml_timestamp_t* ts; /* ts must be freed after use */
+		char*   s; /* string value. s must be freed after use */
+		int     b; /* bool value */
+		int64_t i; /* int value */
+		double  d; /* double value */
+	} u;
+};
+
+/* on arrays: */
+/* ... retrieve size of array. */
+TOML_EXTERN int toml_array_nelem(const toml_array_t* arr);
+/* ... retrieve values using index. */
+TOML_EXTERN toml_datum_t toml_string_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_bool_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_int_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_double_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_datum_t toml_timestamp_at(const toml_array_t* arr, int idx);
+/* ... retrieve array or table using index. */
+TOML_EXTERN toml_array_t* toml_array_at(const toml_array_t* arr, int idx);
+TOML_EXTERN toml_table_t* toml_table_at(const toml_array_t* arr, int idx);
+
+/* on tables: */
+/* ... retrieve the key in table at keyidx. Return 0 if out of range. */
+TOML_EXTERN const char* toml_key_in(const toml_table_t* tab, int keyidx);
+/* ... retrieve values using key. */
+TOML_EXTERN toml_datum_t toml_string_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_bool_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_int_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_double_in(const toml_table_t* arr, const char* key);
+TOML_EXTERN toml_datum_t toml_timestamp_in(const toml_table_t* arr, const char* key);
+/* .. retrieve array or table using key. */
+TOML_EXTERN toml_array_t* toml_array_in(const toml_table_t* tab,
+										const char* key);
+TOML_EXTERN toml_table_t* toml_table_in(const toml_table_t* tab,
+										const char* key);
+
+/*-----------------------------------------------------------------
+ * lesser used 
+ */
+/* Return the array kind: 't'able, 'a'rray, 'v'alue */
+TOML_EXTERN char toml_array_kind(const toml_array_t* arr);
+
+/* For array kind 'v'alue, return the type of values 
+   i:int, d:double, b:bool, s:string, t:time, D:date, T:timestamp
+   0 if unknown
+*/
+TOML_EXTERN char toml_array_type(const toml_array_t* arr);
+
+/* Return the key of an array */
+TOML_EXTERN const char* toml_array_key(const toml_array_t* arr);
+
+/* Return the number of key-values in a table */
+TOML_EXTERN int toml_table_nkval(const toml_table_t* tab);
+
+/* Return the number of arrays in a table */
+TOML_EXTERN int toml_table_narr(const toml_table_t* tab);
+
+/* Return the number of sub-tables in a table */
+TOML_EXTERN int toml_table_ntab(const toml_table_t* tab);
+
+/* Return the key of a table*/
+TOML_EXTERN const char* toml_table_key(const toml_table_t* tab);
+
+/*--------------------------------------------------------------
+ * misc 
+ */
+TOML_EXTERN int toml_utf8_to_ucs(const char* orig, int len, int64_t* ret);
+TOML_EXTERN int toml_ucs_to_utf8(int64_t code, char buf[6]);
+TOML_EXTERN void toml_set_memutil(void* (*xxmalloc)(size_t),
+								  void	(*xxfree)(void*));
+
+
+/*--------------------------------------------------------------
+ *  deprecated 
+ */
+/* A raw value, must be processed by toml_rto* before using. */
+typedef const char* toml_raw_t;
+TOML_EXTERN toml_raw_t toml_raw_in(const toml_table_t* tab, const char* key);
+TOML_EXTERN toml_raw_t toml_raw_at(const toml_array_t* arr, int idx);
+TOML_EXTERN int toml_rtos(toml_raw_t s, char** ret);
+TOML_EXTERN int toml_rtob(toml_raw_t s, int* ret);
+TOML_EXTERN int toml_rtoi(toml_raw_t s, int64_t* ret);
+TOML_EXTERN int toml_rtod(toml_raw_t s, double* ret);
+TOML_EXTERN int toml_rtod_ex(toml_raw_t s, double* ret, char* buf, int buflen);
+TOML_EXTERN int toml_rtots(toml_raw_t s, toml_timestamp_t* ret);
+
+
+#endif /* TOML_H */

From 5d3c94ef8ce450312df64c586323902d98e454f3 Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Sun, 22 Nov 2020 01:37:34 +0100
Subject: [PATCH 2/5] Bump version and fix gitea release

---
 .drone.yml     | 4 +---
 CMakeLists.txt | 2 +-
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/.drone.yml b/.drone.yml
index 6e3a605..4b72953 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -20,9 +20,7 @@ steps:
     api_key:
       from_secret: gitea_token
     base_url: https://git.serguzim.me
-    files:
-    - build/controller
-    - build/controller.ini
+    title: ${DRONE_TAG}
   when:
     event: tag
 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index ee771eb..c564e1f 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,6 +1,6 @@
 cmake_minimum_required (VERSION 3.7)
 project(controller
-        VERSION 0.3.7
+        VERSION 0.4.0
         LANGUAGES C)
 
 add_executable(controller src/main.c)

From 943dd8e0d11147bf7c7e5121f57e072b2bb41591 Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Sun, 22 Nov 2020 01:44:08 +0100
Subject: [PATCH 3/5] Make logger buffer bigger

---
 src/logger.c | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/logger.c b/src/logger.c
index 2561462..290c5b3 100644
--- a/src/logger.c
+++ b/src/logger.c
@@ -65,7 +65,13 @@ logger_log(int level, const char *filename, int line, const char *func, const ch
     strftime(timestamp_str, 32, "%Y-%m-%d %H:%M:%S", localtime(&rawtime));
     size_t timestamp_len = strlen(timestamp_str);
 
-    char *buffer = malloc(sizeof(char) * (128 + strlen(msg) + timestamp_len));
+    size_t buffer_size = 128;
+    buffer_size += timestamp_len;
+    buffer_size += strlen(filename);
+    buffer_size += strlen(func);
+    buffer_size += strlen(msg);
+
+    char *buffer = malloc(sizeof(char) * (buffer_size);
     sprintf(buffer, "%s %s[%5s] %s:%d:%s " COLOR_NONE "%s", timestamp_str, color, level_str, filename, line, func, msg);
 
     // start arg va_list and find log_len

From c49ada88c3dd9f49c0aea515e80a7612de08c684 Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Sun, 22 Nov 2020 01:45:45 +0100
Subject: [PATCH 4/5] Fix typo

---
 src/logger.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/logger.c b/src/logger.c
index 290c5b3..b5df434 100644
--- a/src/logger.c
+++ b/src/logger.c
@@ -71,7 +71,7 @@ logger_log(int level, const char *filename, int line, const char *func, const ch
     buffer_size += strlen(func);
     buffer_size += strlen(msg);
 
-    char *buffer = malloc(sizeof(char) * (buffer_size);
+    char *buffer = malloc(sizeof(char) * (buffer_size));
     sprintf(buffer, "%s %s[%5s] %s:%d:%s " COLOR_NONE "%s", timestamp_str, color, level_str, filename, line, func, msg);
 
     // start arg va_list and find log_len

From 5d98a0579bbe951fe71ac331ee3d39e8e9399ea7 Mon Sep 17 00:00:00 2001
From: Tobias Reisinger <tobias@msrg.cc>
Date: Wed, 17 Apr 2024 00:01:25 +0200
Subject: [PATCH 5/5] Fix issues (remove lmdb.h references, fix mongoose
 warning)

---
 .envrc                      | 1 +
 .gitignore                  | 5 +++++
 include/models/controller.h | 1 -
 include/models/relay.h      | 1 -
 include/models/schedule.h   | 1 -
 shell.nix                   | 9 +++++++++
 src/main.c                  | 1 -
 vendor/mongoose.c           | 2 +-
 8 files changed, 16 insertions(+), 5 deletions(-)
 create mode 100644 .envrc
 create mode 100644 shell.nix

diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..1d953f4
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use nix
diff --git a/.gitignore b/.gitignore
index b944f3b..4dca3a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,8 @@ docs/
 build/
 
 include/sql/*.h
+
+
+# Added by cargo
+
+/target
diff --git a/include/models/controller.h b/include/models/controller.h
index 1cf4fad..10d5827 100644
--- a/include/models/controller.h
+++ b/include/models/controller.h
@@ -3,7 +3,6 @@
 
 #include <uuid/uuid.h>
 #include <stdint.h>
-#include <lmdb.h>
 
 #include <config.h>
 #include <models/relay.h>
diff --git a/include/models/relay.h b/include/models/relay.h
index bbac159..498ea30 100644
--- a/include/models/relay.h
+++ b/include/models/relay.h
@@ -3,7 +3,6 @@
 
 #include <stdint.h>
 #include <time.h>
-#include <lmdb.h>
 
 #include <constants.h>
 #include <models/schedule.h>
diff --git a/include/models/schedule.h b/include/models/schedule.h
index 212cd81..ae91e14 100644
--- a/include/models/schedule.h
+++ b/include/models/schedule.h
@@ -3,7 +3,6 @@
 
 #include <stdint.h>
 #include <uuid/uuid.h>
-#include <lmdb.h>
 
 #include <models/period.h>
 
diff --git a/shell.nix b/shell.nix
new file mode 100644
index 0000000..fc2d27a
--- /dev/null
+++ b/shell.nix
@@ -0,0 +1,9 @@
+with import <nixpkgs> {};
+mkShell {
+	nativeBuildInputs = [
+        cmake
+        sqlite
+        util-linux
+        wiringpi
+	];
+}
diff --git a/src/main.c b/src/main.c
index d4906b5..ad4abd6 100644
--- a/src/main.c
+++ b/src/main.c
@@ -2,7 +2,6 @@
 #include <string.h>
 #include <stdio.h>
 #include <time.h>
-#include <lmdb.h>
 #include <signal.h>
 #include <syslog.h>
 
diff --git a/vendor/mongoose.c b/vendor/mongoose.c
index e2088d0..ab9c0db 100644
--- a/vendor/mongoose.c
+++ b/vendor/mongoose.c
@@ -1108,7 +1108,7 @@ void cs_md5_update(cs_md5_ctx *ctx, const unsigned char *buf, size_t len) {
   memcpy(ctx->in, buf, len);
 }
 
-void cs_md5_final(unsigned char digest[16], cs_md5_ctx *ctx) {
+void cs_md5_final(unsigned char *digest, cs_md5_ctx *ctx) {
   unsigned count;
   unsigned char *p;
   uint32_t *a;