App-Ariza.git | runner/src/ | config.c
/* bin/<exec>.ariza — the sidecar the runner reads at startup.
*
* Two decisions are visible in this file, and they are the interesting
* ones about the whole runner.
*
* The first is that there is a sidecar at all. Baking the target
* script and the environment into the executable at build time would
* mean one compiled runner per app, per version, per platform, produced
* by whoever built the bundle — i.e. a C toolchain in the bundling
* path. A generic runner plus a text file next to it keeps the
* executable a fixed, pinned, hash-verified artefact and puts the
* per-bundle facts in something a user can read, diff and correct.
*
* The second is that the environment arrives as ORDERED DIRECTIVES
* rather than as named settings. An earlier draft had `sqlcipher_dir`
* and `sqlcipher_lib` keys and a hardcoded NOTCURSES_NATIVE_DATA_DIR,
* which meant the runner — a pinned binary, updated on its own release
* cadence — had to learn about every native dependency any bundle might
* ever carry. It does not. It applies `set`, `unset` and
* `prepend-path` lines in the order it finds them, and what those lines
* mean is entirely the business of whatever rendered the file. Adding
* a dependency to a bundle is then a change in the renderer and nothing
* else: no new sidecar keys, no new runner, no version skew between the
* two.
*
* The parser is deliberately lenient in one direction and unforgiving
* in the other: whitespace, comments, CRLF and a stray BOM are all
* tolerated, because that is what an editor produces; an unrecognised
* directive is a hard error naming the line, because a bundle whose
* configuration says something this runner cannot do would otherwise
* start an app with a silently incomplete environment. */
#include <stdlib.h>
#include "ariza_runner.h"
static const arz_char *skip_bom(const arz_char *p)
{
#ifdef _WIN32
/* MultiByteToWideChar leaves a UTF-8 BOM as one U+FEFF unit. */
if (p[0] == (arz_char)0xFEFF)
return p + 1;
#else
if ((unsigned char)p[0] == 0xEF && (unsigned char)p[1] == 0xBB &&
(unsigned char)p[2] == 0xBF)
return p + 3;
#endif
return p;
}
static int is_blank(arz_char c)
{
return c == ARZ_T(' ') || c == ARZ_T('\t') || c == ARZ_T('\r');
}
static arz_char *copy_range(const arz_char *from, const arz_char *to)
{
size_t n = (size_t)(to - from);
arz_char *out = malloc((n + 1) * sizeof(*out));
size_t i;
if (out == NULL)
return NULL;
for (i = 0; i < n; i++)
out[i] = from[i];
out[n] = ARZ_T('\0');
return out;
}
/* Every `{` in a value has to open either `{root}` or `{{`. Checked at
* parse time, where there is a line number to report, so that
* arz_expand can be total. */
static int tokens_are_known(const arz_char *from, const arz_char *to)
{
const arz_char *p = from;
while (p < to) {
if (*p != ARZ_T('{')) {
p++;
continue;
}
if (p + 1 < to && p[1] == ARZ_T('{')) {
p += 2;
continue;
}
if (to - p >= 6 && p[1] == ARZ_T('r') && p[2] == ARZ_T('o') &&
p[3] == ARZ_T('o') && p[4] == ARZ_T('t') &&
p[5] == ARZ_T('}')) {
p += 6;
continue;
}
return 0;
}
return 1;
}
static arz_status push_op(arz_config *cfg, arz_op_kind kind, arz_char *name,
arz_char *value)
{
/* Doubling from four: a bundle has a handful of directives, and a
* fixed cap is exactly the kind of limit that turns into a bug the
* first time a recipe adds a fifth library. */
if (cfg->op_count % 4 == 0) {
size_t want = cfg->op_count + 4;
arz_env_op *grown = realloc(cfg->ops, want * sizeof(*grown));
if (grown == NULL) {
free(name);
free(value);
return ARZ_E_NOMEM;
}
cfg->ops = grown;
}
cfg->ops[cfg->op_count].kind = kind;
cfg->ops[cfg->op_count].name = name;
cfg->ops[cfg->op_count].value = value;
cfg->op_count++;
return ARZ_OK;
}
/* One directive line, already trimmed at both ends and known to be
* neither blank nor a comment. */
static arz_status parse_line(arz_config *cfg, const arz_char *line,
const arz_char *end)
{
const arz_char *verb = line;
const arz_char *verb_end = line;
const arz_char *arg;
size_t vlen;
while (verb_end < end && !is_blank(*verb_end))
verb_end++;
vlen = (size_t)(verb_end - verb);
arg = verb_end;
while (arg < end && is_blank(*arg))
arg++;
if (arz_eq_ascii(verb, vlen, "target") ||
arz_eq_ascii(verb, vlen, "app-exec") ||
arz_eq_ascii(verb, vlen, "app-display")) {
arz_char **slot;
arz_char *copy;
if (arg == end)
return ARZ_E_SYNTAX;
copy = copy_range(arg, end);
if (copy == NULL)
return ARZ_E_NOMEM;
if (arz_eq_ascii(verb, vlen, "target"))
slot = &cfg->target;
else if (arz_eq_ascii(verb, vlen, "app-exec"))
slot = &cfg->app_exec;
else
slot = &cfg->app_display;
/* A repeated directive takes its last value. */
free(*slot);
*slot = copy;
return ARZ_OK;
}
if (arz_eq_ascii(verb, vlen, "set")) {
const arz_char *eq = arg;
arz_char *name;
arz_char *value;
while (eq < end && *eq != ARZ_T('='))
eq++;
/* `set` needs a name and an '='; the value after it may be
* empty, which is a set-but-empty variable and a different
* thing from `unset`. */
if (eq == end || eq == arg)
return ARZ_E_SYNTAX;
if (!tokens_are_known(eq + 1, end))
return ARZ_E_SYNTAX;
name = copy_range(arg, eq);
value = copy_range(eq + 1, end);
if (name == NULL || value == NULL) {
free(name);
free(value);
return ARZ_E_NOMEM;
}
return push_op(cfg, ARZ_OP_SET, name, value);
}
if (arz_eq_ascii(verb, vlen, "unset")) {
arz_char *name;
if (arg == end)
return ARZ_E_SYNTAX;
name = copy_range(arg, end);
if (name == NULL)
return ARZ_E_NOMEM;
return push_op(cfg, ARZ_OP_UNSET, name, NULL);
}
if (arz_eq_ascii(verb, vlen, "prepend-path")) {
arz_char *value;
if (arg == end)
return ARZ_E_SYNTAX;
if (!tokens_are_known(arg, end))
return ARZ_E_SYNTAX;
value = copy_range(arg, end);
if (value == NULL)
return ARZ_E_NOMEM;
return push_op(cfg, ARZ_OP_PREPEND_PATH, NULL, value);
}
/* Fail closed. A directive this runner does not implement is one
* whose effect on the environment would simply be absent, and an
* app that starts with half its libraries unfindable is a much
* worse failure than one that does not start. */
return ARZ_E_SYNTAX;
}
arz_status arz_config_parse(const arz_char *text, arz_config *cfg,
size_t *error_line)
{
const arz_char *p;
size_t lineno = 0;
cfg->target = NULL;
cfg->app_display = NULL;
cfg->app_exec = NULL;
cfg->ops = NULL;
cfg->op_count = 0;
if (error_line != NULL)
*error_line = 0;
if (text == NULL)
return ARZ_OK;
p = skip_bom(text);
while (*p != ARZ_T('\0')) {
const arz_char *line = p;
const arz_char *end;
arz_status st;
lineno++;
while (*p != ARZ_T('\0') && *p != ARZ_T('\n'))
p++;
end = p;
if (*p == ARZ_T('\n'))
p++;
while (line < end && is_blank(*line))
line++;
while (end > line && is_blank(end[-1]))
end--;
if (line == end || *line == ARZ_T('#'))
continue;
st = parse_line(cfg, line, end);
if (st != ARZ_OK) {
if (error_line != NULL && st == ARZ_E_SYNTAX)
*error_line = lineno;
return st;
}
}
return ARZ_OK;
}
static int empty(const arz_char *s)
{
return s == NULL || s[0] == ARZ_T('\0');
}
arz_key arz_config_missing(const arz_config *cfg)
{
if (empty(cfg->target))
return ARZ_KEY_TARGET;
if (empty(cfg->app_display))
return ARZ_KEY_APP_DISPLAY;
if (empty(cfg->app_exec))
return ARZ_KEY_APP_EXEC;
return ARZ_KEY_NONE;
}
const char *arz_key_name(arz_key key)
{
switch (key) {
case ARZ_KEY_NONE:
return "";
case ARZ_KEY_TARGET:
return "target";
case ARZ_KEY_APP_DISPLAY:
return "app-display";
case ARZ_KEY_APP_EXEC:
return "app-exec";
default:
return "";
}
}
void arz_config_free(arz_config *cfg)
{
size_t i;
if (cfg == NULL)
return;
free(cfg->target);
free(cfg->app_display);
free(cfg->app_exec);
for (i = 0; i < cfg->op_count; i++) {
free(cfg->ops[i].name);
free(cfg->ops[i].value);
}
free(cfg->ops);
cfg->target = NULL;
cfg->app_display = NULL;
cfg->app_exec = NULL;
cfg->ops = NULL;
cfg->op_count = 0;
}