Files
stupidtools/src/detect/resolve.c
T

870 lines
23 KiB
C

/*
* resolve.c - feature resolution + `when` guards (todo 14).
*
* Pure code generator: it PARSES the `when` guard grammar into an AST and
* EMITS POSIX-sh that evaluates guards and aggregates feature results at
* CONFIGURE time. It never runs a probe and never evaluates a guard
* itself. See resolve.h for the guard grammar, the os-mapping table, the
* $st_os contract, the emitted guard shape, and the feature-aggregation
* contract that todo 16 consumes.
*
* Safety decisions:
* - Guard leaves emit `[ "$have_<ident>" = "yes" ]` and
* `[ "$st_os" = '<name>' ]`; the ident is validated as a POSIX shell
* identifier (it lands in a variable-NAME position and cannot be
* quoted), the os name is validated against the same charset AND
* single-quoted on emission (belt and braces).
* - Binary nodes re-parenthesize with `( ... )` because POSIX sh gives
* `&&`/`||` equal left-associative precedence, unlike the guard
* grammar's or-over-and hierarchy.
* - The os table is a single source of truth: st_os_normalize (C) and
* st_resolve_emit_os_norm (shell) both derive from os_map[].
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "detect/resolve.h"
#include "error.h"
#include "gen/sh_emit.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- the os mapping table (single source of truth) -------------------- */
struct st_os_entry {
const char *uname_s; /* raw `uname -s` output */
const char *norm; /* canonical name for os= guards */
};
static const struct st_os_entry os_map[] = {
{ "Linux", "linux" },
{ "Darwin", "macos" },
{ "FreeBSD", "bsd" },
{ "OpenBSD", "bsd" },
{ "NetBSD", "bsd" },
};
#define OS_MAP_COUNT (sizeof(os_map) / sizeof(os_map[0]))
const char *
st_os_normalize(const char *uname_s)
{
size_t i;
if (uname_s == NULL) {
return "other";
}
for (i = 0; i < OS_MAP_COUNT; i++) {
if (strcmp(uname_s, os_map[i].uname_s) == 0) {
return os_map[i].norm;
}
}
return "other";
}
/* ---- identifiers ------------------------------------------------------ */
static bool
valid_ident(const char *s)
{
const unsigned char *p;
if (s == NULL || *s == '\0') {
return false;
}
p = (const unsigned char *)s;
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
*p == '_')) {
return false;
}
for (p++; *p != '\0'; p++) {
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
(*p >= '0' && *p <= '9') || *p == '_')) {
return false;
}
}
return true;
}
/* Length-bounded identifier check (word slices are not NUL-terminated). */
static bool
valid_ident_n(const char *s, size_t n)
{
size_t i;
if (s == NULL || n == 0) {
return false;
}
if (!((s[0] >= 'a' && s[0] <= 'z') || (s[0] >= 'A' && s[0] <= 'Z') ||
s[0] == '_')) {
return false;
}
for (i = 1; i < n; i++) {
if (!((s[i] >= 'a' && s[i] <= 'z') || (s[i] >= 'A' && s[i] <= 'Z') ||
(s[i] >= '0' && s[i] <= '9') || s[i] == '_')) {
return false;
}
}
return true;
}
/* ---- owned errors (same-block span, the parser.c pattern) ------------- */
static struct st_error *
err_at_owned(struct st_span sp, const char *msg)
{
const size_t align = _Alignof(struct st_span);
const size_t esize =
(sizeof(struct st_error) + align - 1) & ~(align - 1);
struct st_error *e;
struct st_span *spc;
size_t mlen;
if (msg == NULL) {
msg = "";
}
e = malloc(esize + sizeof(struct st_span));
if (e == NULL) {
return NULL;
}
mlen = strlen(msg);
e->message = malloc(mlen + 1);
if (e->message == NULL) {
free(e);
return NULL;
}
memcpy(e->message, msg, mlen + 1);
e->category = ST_ERR_KDL_SCHEMA;
spc = (struct st_span *)((unsigned char *)e + esize);
*spc = sp;
e->span = spc;
return e;
}
/* ---- the guard AST ---------------------------------------------------- */
enum st_when_kind {
ST_WHEN_HAVE = 0, /* have_<ident>: $have_<ident> = "yes" */
ST_WHEN_OS, /* os=<name>: $st_os = '<name>' */
ST_WHEN_NOT, /* ! <left> */
ST_WHEN_AND, /* <left> && <right> */
ST_WHEN_OR, /* <left> || <right> */
};
struct st_when_ast {
enum st_when_kind kind;
char *text; /* HAVE: ident; OS: name; else NULL */
struct st_when_ast *left; /* NOT operand; AND/OR left */
struct st_when_ast *right; /* AND/OR right */
};
static char *
dup_n(const char *s, size_t n)
{
char *p = malloc(n + 1);
if (p == NULL) {
return NULL;
}
memcpy(p, s, n);
p[n] = '\0';
return p;
}
static struct st_when_ast *
leaf(enum st_when_kind kind, const char *text, size_t len)
{
struct st_when_ast *a = calloc(1, sizeof(*a));
if (a == NULL) {
return NULL;
}
a->kind = kind;
if (text != NULL) {
a->text = dup_n(text, len);
if (a->text == NULL) {
free(a);
return NULL;
}
}
return a;
}
void
st_when_free(struct st_when_ast *a)
{
if (a == NULL) {
return;
}
st_when_free(a->left);
st_when_free(a->right);
free(a->text);
free(a);
}
/* ---- the guard lexer -------------------------------------------------- */
enum gtok_kind {
GTOK_EOF = 0,
GTOK_HAVE, /* text = ident (slice) */
GTOK_OS, /* text = name (slice) */
GTOK_AND,
GTOK_OR,
GTOK_NOT,
GTOK_LPAREN,
GTOK_RPAREN,
GTOK_UNKNOWN, /* word that is none of the above */
GTOK_HAVE_EMPTY, /* word == "have_" */
GTOK_HAVE_BAD, /* have_ + invalid ident */
GTOK_OS_EMPTY, /* word == "os=" */
GTOK_OS_BAD, /* os= + invalid name */
};
struct gtok {
enum gtok_kind kind;
size_t off; /* byte offset of the token start in the guard */
const char *text; /* slice into the guard (word, or ident/name) */
size_t len;
};
struct glex {
const char *s;
size_t len;
size_t pos;
struct gtok cur;
};
static bool
is_space(unsigned char c)
{
return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\v' ||
c == '\f';
}
/* Classify a word slice (not NUL-terminated). For HAVE/OS success the
* text/len are narrowed to the ident/name; for error kinds they stay the
* whole word so the parser can echo it. */
static void
classify_word(struct gtok *t, const char *w, size_t wlen, size_t off)
{
t->off = off;
t->text = w;
t->len = wlen;
if (wlen == 3 && memcmp(w, "and", 3) == 0) {
t->kind = GTOK_AND;
} else if (wlen == 2 && memcmp(w, "or", 2) == 0) {
t->kind = GTOK_OR;
} else if (wlen == 3 && memcmp(w, "not", 3) == 0) {
t->kind = GTOK_NOT;
} else if (wlen >= 5 && memcmp(w, "have_", 5) == 0) {
if (wlen == 5) {
t->kind = GTOK_HAVE_EMPTY;
} else if (!valid_ident_n(w + 5, wlen - 5)) {
t->kind = GTOK_HAVE_BAD;
} else {
t->kind = GTOK_HAVE;
t->text = w + 5;
t->len = wlen - 5;
}
} else if (wlen >= 3 && memcmp(w, "os=", 3) == 0) {
if (wlen == 3) {
t->kind = GTOK_OS_EMPTY;
} else if (!valid_ident_n(w + 3, wlen - 3)) {
t->kind = GTOK_OS_BAD;
} else {
t->kind = GTOK_OS;
t->text = w + 3;
t->len = wlen - 3;
}
} else {
t->kind = GTOK_UNKNOWN;
}
}
static void
next_tok(struct glex *lx)
{
const char *s = lx->s;
size_t n = lx->len;
size_t i = lx->pos;
size_t start;
while (i < n && is_space((unsigned char)s[i])) {
i++;
}
lx->pos = i;
lx->cur.text = NULL;
lx->cur.len = 0;
lx->cur.kind = GTOK_EOF;
if (i >= n) {
lx->cur.off = n;
return;
}
if (s[i] == '(') {
lx->cur.kind = GTOK_LPAREN;
lx->cur.off = i;
lx->cur.text = s + i;
lx->cur.len = 1;
lx->pos = i + 1;
return;
}
if (s[i] == ')') {
lx->cur.kind = GTOK_RPAREN;
lx->cur.off = i;
lx->cur.text = s + i;
lx->cur.len = 1;
lx->pos = i + 1;
return;
}
start = i;
while (i < n && !is_space((unsigned char)s[i]) && s[i] != '(' &&
s[i] != ')') {
i++;
}
lx->pos = i;
classify_word(&lx->cur, s + start, i - start, start);
}
/* ---- the guard parser ------------------------------------------------- */
struct parse_ctx {
struct glex lx;
const struct st_span *base; /* NULL -> offset span into the guard */
};
/* 1-based line/col of byte offset `off` within `s`. */
static struct st_span
off_span(const char *s, size_t off)
{
struct st_span sp = { NULL, 1, 1 };
size_t i;
for (i = 0; i < off && s[i] != '\0'; i++) {
if (s[i] == '\n') {
sp.line++;
sp.col = 1;
} else {
sp.col++;
}
}
return sp;
}
static struct st_error *
parse_error(const struct parse_ctx *pc, size_t off, const char *msg)
{
struct st_span sp;
if (pc->base != NULL) {
sp = *pc->base;
} else {
sp = off_span(pc->lx.s, off);
}
return err_at_owned(sp, msg);
}
/* Copy the current token's word into `buf` for message text. */
static const char *
word_into(const struct gtok *t, char *buf, size_t cap)
{
size_t n = t->len < cap - 1 ? t->len : cap - 1;
if (t->text == NULL) {
n = 0;
}
if (n > 0) {
memcpy(buf, t->text, n);
}
buf[n] = '\0';
return buf;
}
static struct st_error *
unexpected(struct parse_ctx *pc)
{
const struct gtok *t = &pc->lx.cur;
char w[65];
char msg[160];
switch (t->kind) {
case GTOK_UNKNOWN:
snprintf(msg, sizeof msg, "unknown token '%s' in when guard",
word_into(t, w, sizeof w));
break;
case GTOK_HAVE_EMPTY:
snprintf(msg, sizeof msg,
"missing feature name after 'have_' in when guard");
break;
case GTOK_HAVE_BAD:
snprintf(msg, sizeof msg,
"invalid feature name '%s' after 'have_' in when guard",
word_into(t, w, sizeof w));
break;
case GTOK_OS_EMPTY:
snprintf(msg, sizeof msg, "missing os name after 'os=' in when "
"guard");
break;
case GTOK_OS_BAD:
snprintf(msg, sizeof msg,
"invalid os name '%s' after 'os=' in when guard",
word_into(t, w, sizeof w));
break;
case GTOK_EOF:
snprintf(msg, sizeof msg, "unexpected end of when guard expression");
break;
default:
snprintf(msg, sizeof msg, "unexpected token '%s' in when guard",
word_into(t, w, sizeof w));
break;
}
return parse_error(pc, t->off, msg);
}
static struct st_error *
parse_or(struct parse_ctx *pc, struct st_when_ast **out);
static struct st_error *
parse_primary(struct parse_ctx *pc, struct st_when_ast **out)
{
const struct gtok *t = &pc->lx.cur;
*out = NULL;
switch (t->kind) {
case GTOK_LPAREN: {
struct st_when_ast *inner = NULL;
struct st_error *e;
next_tok(&pc->lx);
e = parse_or(pc, &inner);
if (e != NULL) {
return e;
}
if (pc->lx.cur.kind != GTOK_RPAREN) {
struct st_error *e2 = parse_error(pc, pc->lx.cur.off,
"expected ')' in when guard");
st_when_free(inner);
return e2;
}
next_tok(&pc->lx);
*out = inner;
return NULL;
}
case GTOK_HAVE: {
struct st_when_ast *a = leaf(ST_WHEN_HAVE, t->text, t->len);
if (a == NULL) {
return st_error_internal("out of memory parsing when guard");
}
next_tok(&pc->lx);
*out = a;
return NULL;
}
case GTOK_OS: {
struct st_when_ast *a = leaf(ST_WHEN_OS, t->text, t->len);
if (a == NULL) {
return st_error_internal("out of memory parsing when guard");
}
next_tok(&pc->lx);
*out = a;
return NULL;
}
default:
return unexpected(pc);
}
}
static struct st_error *
parse_not(struct parse_ctx *pc, struct st_when_ast **out)
{
if (pc->lx.cur.kind == GTOK_NOT) {
struct st_when_ast *inner = NULL;
struct st_when_ast *a;
struct st_error *e;
next_tok(&pc->lx);
e = parse_not(pc, &inner);
if (e != NULL) {
return e;
}
a = calloc(1, sizeof(*a));
if (a == NULL) {
st_when_free(inner);
return st_error_internal("out of memory parsing when guard");
}
a->kind = ST_WHEN_NOT;
a->left = inner;
*out = a;
return NULL;
}
return parse_primary(pc, out);
}
static struct st_error *
parse_and(struct parse_ctx *pc, struct st_when_ast **out)
{
struct st_when_ast *lhs = NULL;
struct st_error *e;
e = parse_not(pc, &lhs);
if (e != NULL) {
return e;
}
while (pc->lx.cur.kind == GTOK_AND) {
struct st_when_ast *rhs = NULL;
struct st_when_ast *join;
next_tok(&pc->lx);
e = parse_not(pc, &rhs);
if (e != NULL) {
st_when_free(lhs);
return e;
}
join = calloc(1, sizeof(*join));
if (join == NULL) {
st_when_free(lhs);
st_when_free(rhs);
return st_error_internal("out of memory parsing when guard");
}
join->kind = ST_WHEN_AND;
join->left = lhs;
join->right = rhs;
lhs = join;
}
*out = lhs;
return NULL;
}
static struct st_error *
parse_or(struct parse_ctx *pc, struct st_when_ast **out)
{
struct st_when_ast *lhs = NULL;
struct st_error *e;
e = parse_and(pc, &lhs);
if (e != NULL) {
return e;
}
while (pc->lx.cur.kind == GTOK_OR) {
struct st_when_ast *rhs = NULL;
struct st_when_ast *join;
next_tok(&pc->lx);
e = parse_and(pc, &rhs);
if (e != NULL) {
st_when_free(lhs);
return e;
}
join = calloc(1, sizeof(*join));
if (join == NULL) {
st_when_free(lhs);
st_when_free(rhs);
return st_error_internal("out of memory parsing when guard");
}
join->kind = ST_WHEN_OR;
join->left = lhs;
join->right = rhs;
lhs = join;
}
*out = lhs;
return NULL;
}
struct st_error *
st_when_parse_at(const char *guard, const struct st_span *at,
struct st_when_ast **out)
{
struct parse_ctx pc;
struct st_when_ast *ast = NULL;
struct st_error *e;
if (out == NULL) {
return st_error_usage("st_when_parse: NULL out");
}
*out = NULL;
if (guard == NULL || guard[0] == '\0') {
return NULL; /* absent guard = always true */
}
pc.lx.s = guard;
pc.lx.len = strlen(guard);
pc.lx.pos = 0;
pc.base = at;
next_tok(&pc.lx);
e = parse_or(&pc, &ast);
if (e != NULL) {
st_when_free(ast);
return e;
}
if (pc.lx.cur.kind != GTOK_EOF) {
e = unexpected(&pc);
st_when_free(ast);
return e;
}
*out = ast;
return NULL;
}
struct st_error *
st_when_parse(const char *guard, struct st_when_ast **out)
{
return st_when_parse_at(guard, NULL, out);
}
/* ---- guard emission --------------------------------------------------- */
static struct st_error *
emit_node(FILE *out, const struct st_when_ast *n)
{
switch (n->kind) {
case ST_WHEN_HAVE:
if (fprintf(out, "[ \"$have_%s\" = \"yes\" ]", n->text) < 0) {
return st_error_io("I/O error emitting when guard");
}
break;
case ST_WHEN_OS: {
char *q = st_sh_quote(n->text);
int rc;
if (q == NULL) {
return st_error_internal("out of memory quoting os name");
}
rc = fprintf(out, "[ \"$st_os\" = %s ]", q);
free(q);
if (rc < 0) {
return st_error_io("I/O error emitting when guard");
}
break;
}
case ST_WHEN_NOT: {
struct st_error *e;
if (fputs("! ( ", out) == EOF) {
return st_error_io("I/O error emitting when guard");
}
e = emit_node(out, n->left);
if (e != NULL) {
return e;
}
if (fputs(" )", out) == EOF) {
return st_error_io("I/O error emitting when guard");
}
break;
}
case ST_WHEN_AND:
case ST_WHEN_OR: {
const char *op = (n->kind == ST_WHEN_AND) ? "&&" : "||";
struct st_error *e;
if (fputs("( ", out) == EOF) {
return st_error_io("I/O error emitting when guard");
}
e = emit_node(out, n->left);
if (e != NULL) {
return e;
}
if (fprintf(out, " ) %s ( ", op) < 0) {
return st_error_io("I/O error emitting when guard");
}
e = emit_node(out, n->right);
if (e != NULL) {
return e;
}
if (fputs(" )", out) == EOF) {
return st_error_io("I/O error emitting when guard");
}
break;
}
default:
return st_error_internal("st_when_emit: corrupted guard AST");
}
return NULL;
}
struct st_error *
st_when_emit(FILE *out, const struct st_when_ast *ast)
{
if (out == NULL) {
return st_error_usage("st_when_emit: NULL stream");
}
if (ast == NULL) {
/* absent guard = always true; `:` is the no-op command */
if (fputs(":", out) == EOF) {
return st_error_io("I/O error emitting when guard");
}
return NULL;
}
return emit_node(out, ast);
}
/* ---- os normalization boilerplate ------------------------------------- */
struct st_error *
st_resolve_emit_os_norm(FILE *out)
{
size_t i;
if (out == NULL) {
return st_error_usage("st_resolve_emit_os_norm: NULL stream");
}
if (fputs(
"# st_os holds the raw `uname -s` output (set by the configure\n"
"# preamble). st_os_norm rewrites it IN PLACE to the canonical\n"
"# name that `os=<name>` guards compare against.\n"
"st_os_norm() {\n"
" case \"$st_os\" in\n",
out) == EOF) {
return st_error_io("I/O error emitting st_os_norm");
}
for (i = 0; i < OS_MAP_COUNT; i++) {
char *pat = st_sh_quote(os_map[i].uname_s);
char *val = st_sh_quote(os_map[i].norm);
int rc;
if (pat == NULL || val == NULL) {
free(pat);
free(val);
return st_error_internal("out of memory emitting st_os_norm");
}
rc = fprintf(out, " %s) st_os=%s ;;\n", pat, val);
free(pat);
free(val);
if (rc < 0) {
return st_error_io("I/O error emitting st_os_norm");
}
}
if (fputs(" *) st_os='other' ;;\n"
" esac\n"
"}\n",
out) == EOF) {
return st_error_io("I/O error emitting st_os_norm");
}
if (ferror(out)) {
return st_error_io("I/O error emitting st_os_norm");
}
return NULL;
}
/* ---- feature aggregation ---------------------------------------------- */
int
st_resolve_check_name(const char *name, size_t index, size_t count,
char *out, size_t out_cap)
{
int rc;
if (name == NULL || out == NULL || out_cap == 0) {
return -1;
}
if (!valid_ident(name)) {
return -1;
}
if (count == 0 || index >= count) {
return -1;
}
if (count == 1) {
rc = snprintf(out, out_cap, "%s", name);
} else {
rc = snprintf(out, out_cap, "%s_%zu", name, index);
}
if (rc < 0 || (size_t)rc >= out_cap) {
return -1;
}
return 0;
}
struct st_error *
st_resolve_emit_feature(FILE *out, const struct st_resolve_feature *f)
{
bool direct;
size_t i;
struct st_error *e;
if (out == NULL || f == NULL) {
return st_error_usage("st_resolve_emit_feature: NULL argument");
}
if (!valid_ident(f->name)) {
return st_error_usage("st_resolve_emit_feature: feature name must "
"be a POSIX shell identifier");
}
if (f->check_count == 0) {
return st_error_usage("st_resolve_emit_feature: feature has no "
"checks");
}
if (f->check_names == NULL) {
return st_error_usage("st_resolve_emit_feature: NULL check_names");
}
for (i = 0; i < f->check_count; i++) {
if (!valid_ident(f->check_names[i])) {
return st_error_usage("st_resolve_emit_feature: check name "
"must be a POSIX shell identifier");
}
}
if (fprintf(out, "# feature '%s'\n", f->name) < 0) {
return st_error_io("I/O error emitting feature resolution");
}
/* The single check whose name IS the feature name was already set to
* have_<name> by todo 12's snippet — nothing to aggregate. */
direct = (f->check_count == 1 &&
strcmp(f->check_names[0], f->name) == 0);
if (!direct) {
if (f->check_count == 1) {
if (fprintf(out, "have_%s=$have_%s\n", f->name,
f->check_names[0]) < 0) {
return st_error_io("I/O error emitting feature resolution");
}
} else {
if (fputs("if ", out) == EOF) {
return st_error_io("I/O error emitting feature resolution");
}
for (i = 0; i < f->check_count; i++) {
if (i > 0 && fputs(" && ", out) == EOF) {
return st_error_io("I/O error emitting feature "
"resolution");
}
if (fprintf(out, "[ \"$have_%s\" = \"yes\" ]",
f->check_names[i]) < 0) {
return st_error_io("I/O error emitting feature "
"resolution");
}
}
if (fprintf(out, "; then\n have_%s=yes\nelse\n "
"have_%s=no\nfi\n", f->name, f->name) < 0) {
return st_error_io("I/O error emitting feature resolution");
}
}
}
if (f->guard != NULL) {
if (fputs("if ", out) == EOF) {
return st_error_io("I/O error emitting feature resolution");
}
e = st_when_emit(out, f->guard);
if (e != NULL) {
return e;
}
if (fprintf(out, "; then\n :\nelse\n have_%s=no\nfi\n",
f->name) < 0) {
return st_error_io("I/O error emitting feature resolution");
}
}
if (ferror(out)) {
return st_error_io("I/O error emitting feature resolution");
}
return NULL;
}