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;
|
||||
}
|
||||
Reference in New Issue
Block a user