/* * 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_" = "yes" ]` and * `[ "$st_os" = '' ]`; 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 #include #include #include /* ---- 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_: $have_ = "yes" */ ST_WHEN_OS, /* os=: $st_os = '' */ ST_WHEN_NOT, /* ! */ ST_WHEN_AND, /* && */ ST_WHEN_OR, /* || */ }; 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=` 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_ 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; }