Template
feat(detect): add feature resolution and when-guards
This commit is contained in:
@@ -0,0 +1,869 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* resolve.h - feature resolution + `when` guards (todo 14).
|
||||
*
|
||||
* Two jobs live here, both PURE CODE GENERATION — this module never runs
|
||||
* a probe and never evaluates a guard itself; the emitted shell does that
|
||||
* at CONFIGURE time inside the generated ./configure:
|
||||
*
|
||||
* (1) FEATURE RESOLUTION: combine each feature's per-check results (the
|
||||
* have_<checkname>=yes|no variables set by todo 12's snippets) into
|
||||
* the single availability boolean have_<feature>. A single-check
|
||||
* feature names its check after the feature (have_<feature> is set
|
||||
* directly by the probe); a multi-check feature derives distinct
|
||||
* names (<feature>_0, <feature>_1, ...) and this module ANDs them.
|
||||
*
|
||||
* (2) THE `when` GUARD LANGUAGE: parse a guard STRING (the DSL's `when`
|
||||
* property) into an opaque AST and emit a POSIX-sh boolean test that
|
||||
* evaluates it against the configure-time variables $have_<feature>
|
||||
* and the normalized $st_os.
|
||||
*
|
||||
* THE `when` DSL PLACEMENT (pinned here; extends the todo-9 grammar)
|
||||
* ----------------------------------------------------------------------
|
||||
* `when` is an OPTIONAL property on a `feature` node, spelling (a KDL
|
||||
* `key=value` property, exactly like an option's `default=#bool`):
|
||||
*
|
||||
* feature "pthread" when="os=linux" { header "pthread.h" }
|
||||
*
|
||||
* Its value is a non-empty KDL string (enforced by src/kdl/schema.c). The
|
||||
* guard STRING is NOT parsed by the schema — only resolve parses it (todo
|
||||
* 16 extracts the string, calls st_when_parse_at with the feature node's
|
||||
* span, then st_resolve_emit_feature). `target`/`option` take no `when`.
|
||||
*
|
||||
* GUARD GRAMMAR
|
||||
* -------------
|
||||
* expr := or_expr
|
||||
* or_expr := and_expr ('or' and_expr)*
|
||||
* and_expr := not_expr ('and' not_expr)*
|
||||
* not_expr := 'not' not_expr | primary
|
||||
* primary := '(' expr ')' | 'have_' <ident> | 'os=' <name>
|
||||
*
|
||||
* Tokens are whitespace-insensitive: `and`, `or`, `not`, `(`, `)`,
|
||||
* `have_<ident>` (ident = [A-Za-z_][A-Za-z0-9_]*), and `os=<name>` (name
|
||||
* uses the same identifier charset). `have_<ident>` reads the
|
||||
* configure-time $have_<ident> (= "yes"|"no"); `os=<name>` compares the
|
||||
* normalized $st_os for equality. Guards may reference OTHER features'
|
||||
* availability (have_<bar>) — todo 16 orders features so referenced
|
||||
* results are already computed; a self-referencing feature is a todo-16
|
||||
* cycle to reject, not something resolve checks.
|
||||
*
|
||||
* THE $st_os CONTRACT (todo 16 preamble)
|
||||
* --------------------------------------
|
||||
* The configure preamble captures `uname -s` into $st_os, then calls the
|
||||
* emitted st_os_norm() (see st_resolve_emit_os_norm), which rewrites it
|
||||
* IN PLACE to the canonical name guards compare against:
|
||||
*
|
||||
* Linux -> linux; Darwin -> macos; FreeBSD -> bsd;
|
||||
* OpenBSD/NetBSD -> bsd; anything else -> other
|
||||
*
|
||||
* The canonical table is the single source of truth: st_os_normalize()
|
||||
* (C side) and the emitted st_os_norm() (shell side) both derive from it.
|
||||
* os= guards therefore compare the ALREADY-normalized $st_os, so the
|
||||
* emitted test is `[ "$st_os" = '<name>' ]` — no uname string is ever
|
||||
* baked into a guard.
|
||||
*
|
||||
* EMITTED GUARD SHAPE (what todo 16 wraps)
|
||||
* ----------------------------------------
|
||||
* st_when_emit writes a SELF-CONTAINED boolean condition, designed to sit
|
||||
* between `if` and `; then`:
|
||||
*
|
||||
* if <emitted>; then ... fi
|
||||
*
|
||||
* Leaves: [ "$have_<ident>" = "yes" ] [ "$st_os" = '<name>' ]
|
||||
* NOT: ! ( <child> )
|
||||
* AND/OR: ( <left> ) && ( <right> ) ( <left> ) || ( <right> )
|
||||
*
|
||||
* The subshell grouping `( ... )` is deliberate: POSIX sh gives `&&` and
|
||||
* `||` EQUAL (left-associative) precedence, unlike C, so the AST's
|
||||
* structure must be re-parenthesized on emission. Only `[`, `!`, `&&`,
|
||||
* `||`, `(`, `)` are used — no `[[`, no `==`, no `local`; the output
|
||||
* passes sh -n / bash -n / zsh -n. A NULL AST (empty/absent guard) is
|
||||
* "always true": st_when_emit writes the no-op command `:`.
|
||||
*
|
||||
* FEATURE AGGREGATION (the todo-16 contract)
|
||||
* ------------------------------------------
|
||||
* st_resolve_emit_feature() emits, in order:
|
||||
* 1. a `# feature 'NAME'` comment;
|
||||
* 2. the have_<name> AND-aggregation — nothing for a single check whose
|
||||
* name IS the feature name (the probe set it directly); a plain copy
|
||||
* `have_<name>=$have_<c0>` for a single differently-named check; and
|
||||
* an `if [ ... ] && [ ... ]; then have_<name>=yes; else
|
||||
* have_<name>=no; fi` for multiple checks;
|
||||
* 3. the `when` gate (when f->guard != NULL):
|
||||
* `if <guard>; then :; else have_<name>=no; fi`.
|
||||
* todo 16 emits the per-check probe snippets (todo 12) BEFORE calling
|
||||
* this, so the aggregation always reads already-set have_<checkname>.
|
||||
* Semantics pinned: have_<feature> = (AND of its checks) AND (guard).
|
||||
*
|
||||
* CHECK-NAME SCHEME
|
||||
* -----------------
|
||||
* st_resolve_check_name(name, index, count, ...) derives the per-check
|
||||
* name todo 16 passes to st_probe_emit_snippet: count == 1 -> the feature
|
||||
* name itself (have_<name> set directly); count > 1 -> "<name>_<index>".
|
||||
*
|
||||
* ERRORS
|
||||
* ------
|
||||
* st_when_parse/st_when_parse_at return an owned ST_ERR_KDL_SCHEMA error
|
||||
* on a malformed guard, with a span heap-allocated IN THE SAME BLOCK as
|
||||
* the error (the parser.c pattern). Without a base span (st_when_parse)
|
||||
* the span points INTO the guard string (file = NULL, line/col = the
|
||||
* offending token). With a base span (st_when_parse_at) the error carries
|
||||
* that span — the `when` node's span, so todo 16 can attribute the error
|
||||
* to the build file. Emitters follow the probe.c convention: NULL args /
|
||||
* invalid identifiers -> ST_ERR_USAGE, quoting failure -> ST_ERR_INTERNAL,
|
||||
* write failure -> ST_ERR_IO.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_DETECT_RESOLVE_H
|
||||
#define ST_DETECT_RESOLVE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
struct st_error;
|
||||
struct st_span;
|
||||
|
||||
/* ---- the `when` guard AST (opaque) ------------------------------------ */
|
||||
|
||||
struct st_when_ast;
|
||||
|
||||
/* Parse a `when` guard string into an AST. NULL or empty guard -> *out is
|
||||
* left NULL ("always true") and NULL is returned. On a malformed guard
|
||||
* returns an owned ST_ERR_KDL_SCHEMA error (span points into the guard
|
||||
* string, file = NULL). On catastrophic OOM the returned error may itself
|
||||
* be NULL (codebase-wide convention). *out is untouched on error. */
|
||||
struct st_error *st_when_parse(const char *guard, struct st_when_ast **out);
|
||||
|
||||
/* As st_when_parse, but on error the error carries `at` (borrowed) as its
|
||||
* span instead of a guard-offset span. Use it when the guard came from a
|
||||
* KDL node so the error points at the `when` property's span. `at` may be
|
||||
* NULL (then behaves like st_when_parse). */
|
||||
struct st_error *st_when_parse_at(const char *guard, const struct st_span *at,
|
||||
struct st_when_ast **out);
|
||||
|
||||
/* Release an AST. NULL is a safe no-op. */
|
||||
void st_when_free(struct st_when_ast *ast);
|
||||
|
||||
/* Emit the guard as a self-contained POSIX-sh boolean condition (see the
|
||||
* header's EMITTED GUARD SHAPE). NULL AST -> emits `:` ("always true").
|
||||
* Returns NULL on success, an owned error otherwise. */
|
||||
struct st_error *st_when_emit(FILE *out, const struct st_when_ast *ast);
|
||||
|
||||
/* ---- the os mapping --------------------------------------------------- */
|
||||
|
||||
/* Map a raw `uname -s` string to its canonical name: Linux->linux,
|
||||
* Darwin->macos, FreeBSD/OpenBSD/NetBSD->bsd, anything else (incl. NULL)
|
||||
* -> other. The returned pointer is static. */
|
||||
const char *st_os_normalize(const char *uname_s);
|
||||
|
||||
/* Emit the st_os_norm() shell function that rewrites $st_os in place to
|
||||
* its canonical name (the same table as st_os_normalize). POSIX-sh only.
|
||||
* Returns NULL on success. */
|
||||
struct st_error *st_resolve_emit_os_norm(FILE *out);
|
||||
|
||||
/* ---- feature aggregation (the todo-16 contract) ----------------------- */
|
||||
|
||||
/* One feature's resolution model. Everything is borrowed: the strings and
|
||||
* the guard AST are owned by the caller and must outlive the emit call
|
||||
* (the emit does not free the AST). */
|
||||
struct st_resolve_feature {
|
||||
const char *name; /* feature name -> have_<name> */
|
||||
const char *const *check_names;/* [check_count] have_<cn> variables */
|
||||
size_t check_count; /* >= 1 */
|
||||
struct st_when_ast *guard; /* parsed guard; NULL = no guard */
|
||||
};
|
||||
|
||||
/* Emit one feature's resolution (aggregation + when-gate) as described in
|
||||
* the header. Returns NULL on success, an owned error otherwise. */
|
||||
struct st_error *st_resolve_emit_feature(FILE *out,
|
||||
const struct st_resolve_feature *f);
|
||||
|
||||
/* Derive the checkname todo 16 passes to st_probe_emit_snippet for
|
||||
* `name`'s check at 0-based `index` when the feature has `count` checks.
|
||||
* count == 1 -> the feature name itself; count > 1 -> "<name>_<index>".
|
||||
* Writes into `out` (capacity `out_cap`). Returns 0 on success, -1 on
|
||||
* NULL args, an invalid feature name, a zero count, an out-of-range
|
||||
* index, or truncation. */
|
||||
int st_resolve_check_name(const char *name, size_t index, size_t count,
|
||||
char *out, size_t out_cap);
|
||||
|
||||
#endif /* ST_DETECT_RESOLVE_H */
|
||||
+43
-12
@@ -548,12 +548,16 @@ validate_check(const struct st_kdl_node *c, enum st_check_kind kind,
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* feature "name" { <checks> }: the name shape (no properties) plus a
|
||||
* children block of CHECK nodes. Each child's NAME is dispatched through
|
||||
* the feature-check registry (src/detect/check_registry.h, todo 10) to
|
||||
* one of the 8 kinds - header, function, library, type, sizeof,
|
||||
* program, compiler_flag, pkg_config - and its argument/property shape
|
||||
* is enforced from the registry table (see validate_check). */
|
||||
/* feature "name" when="<expr>" { <checks> }: the name shape plus an
|
||||
* OPTIONAL `when` property (todo 14's guard placement, spelled
|
||||
* `feature "pthread" when="os=linux" { ... }` — a KDL key=value property,
|
||||
* value = a non-empty unannotated string; the guard STRING is parsed by
|
||||
* src/detect/resolve.c, not here) and a children block of CHECK nodes.
|
||||
* Each child's NAME is dispatched through the feature-check registry
|
||||
* (src/detect/check_registry.h, todo 10) to one of the 8 kinds - header,
|
||||
* function, library, type, sizeof, program, compiler_flag, pkg_config -
|
||||
* and its argument/property shape is enforced from the registry table
|
||||
* (see validate_check). Any property other than `when` is rejected. */
|
||||
static struct st_error *
|
||||
validate_feature(const struct st_kdl_node *n)
|
||||
{
|
||||
@@ -561,6 +565,8 @@ validate_feature(const struct st_kdl_node *n)
|
||||
struct st_error *e;
|
||||
char *name_str = NULL;
|
||||
struct st_kdl_node *c;
|
||||
struct st_kdl_prop *p;
|
||||
bool seen_when = false;
|
||||
|
||||
e = one_name_arg(n, "feature", &name_str);
|
||||
if (e != NULL) {
|
||||
@@ -568,15 +574,40 @@ validate_feature(const struct st_kdl_node *n)
|
||||
}
|
||||
name_into(&n->args->value, name, sizeof name);
|
||||
|
||||
if (n->props != NULL) {
|
||||
for (p = n->props; p != NULL; p = p->next) {
|
||||
bool is_when = false;
|
||||
char key[65];
|
||||
char msg[192];
|
||||
|
||||
name_into(&n->props->key, key, sizeof key);
|
||||
snprintf(msg, sizeof msg, "feature '%s' has unexpected property "
|
||||
"'%s'", name, key);
|
||||
e = err_at_owned(n->props->key.span, msg);
|
||||
goto done;
|
||||
name_into(&p->key, key, sizeof key);
|
||||
e = name_equals(&p->key, "when", &is_when);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (!is_when) {
|
||||
snprintf(msg, sizeof msg, "feature '%s' has unexpected "
|
||||
"property '%s'", name, key);
|
||||
e = err_at_owned(p->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (seen_when) {
|
||||
snprintf(msg, sizeof msg, "feature '%s' has a duplicate 'when' "
|
||||
"property", name);
|
||||
e = err_at_owned(p->key.span, msg);
|
||||
goto done;
|
||||
}
|
||||
seen_when = true;
|
||||
{
|
||||
char cctx[192];
|
||||
char *tmp = NULL;
|
||||
|
||||
snprintf(cctx, sizeof cctx, "feature '%s': 'when'", name);
|
||||
e = require_string_arg(&p->value, p->annotation, cctx, &tmp);
|
||||
free(tmp);
|
||||
if (e != NULL) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (c = n->children; c != NULL; c = c->next) {
|
||||
|
||||
Reference in New Issue
Block a user