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;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* feature "name" { <checks> }: the name shape (no properties) plus a
|
/* feature "name" when="<expr>" { <checks> }: the name shape plus an
|
||||||
* children block of CHECK nodes. Each child's NAME is dispatched through
|
* OPTIONAL `when` property (todo 14's guard placement, spelled
|
||||||
* the feature-check registry (src/detect/check_registry.h, todo 10) to
|
* `feature "pthread" when="os=linux" { ... }` — a KDL key=value property,
|
||||||
* one of the 8 kinds - header, function, library, type, sizeof,
|
* value = a non-empty unannotated string; the guard STRING is parsed by
|
||||||
* program, compiler_flag, pkg_config - and its argument/property shape
|
* src/detect/resolve.c, not here) and a children block of CHECK nodes.
|
||||||
* is enforced from the registry table (see validate_check). */
|
* 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 *
|
static struct st_error *
|
||||||
validate_feature(const struct st_kdl_node *n)
|
validate_feature(const struct st_kdl_node *n)
|
||||||
{
|
{
|
||||||
@@ -561,6 +565,8 @@ validate_feature(const struct st_kdl_node *n)
|
|||||||
struct st_error *e;
|
struct st_error *e;
|
||||||
char *name_str = NULL;
|
char *name_str = NULL;
|
||||||
struct st_kdl_node *c;
|
struct st_kdl_node *c;
|
||||||
|
struct st_kdl_prop *p;
|
||||||
|
bool seen_when = false;
|
||||||
|
|
||||||
e = one_name_arg(n, "feature", &name_str);
|
e = one_name_arg(n, "feature", &name_str);
|
||||||
if (e != NULL) {
|
if (e != NULL) {
|
||||||
@@ -568,15 +574,40 @@ validate_feature(const struct st_kdl_node *n)
|
|||||||
}
|
}
|
||||||
name_into(&n->args->value, name, sizeof name);
|
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 key[65];
|
||||||
char msg[192];
|
char msg[192];
|
||||||
|
|
||||||
name_into(&n->props->key, key, sizeof key);
|
name_into(&p->key, key, sizeof key);
|
||||||
snprintf(msg, sizeof msg, "feature '%s' has unexpected property "
|
e = name_equals(&p->key, "when", &is_when);
|
||||||
"'%s'", name, key);
|
if (e != NULL) {
|
||||||
e = err_at_owned(n->props->key.span, msg);
|
goto done;
|
||||||
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) {
|
for (c = n->children; c != NULL; c = c->next) {
|
||||||
|
|||||||
@@ -0,0 +1,732 @@
|
|||||||
|
/* LINK: ../../src/detect/resolve.c ../../src/gen/sh_emit.c ../../src/kdl/schema.c ../../src/detect/check_registry.c ../../src/kdl/value.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/error.c ../../src/span.c */
|
||||||
|
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L /* mkdtemp, system/WEXITSTATUS */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* tests/unit/test_resolve.c
|
||||||
|
*
|
||||||
|
* Unit tests for feature resolution + `when` guards (todo 14):
|
||||||
|
* src/detect/resolve.c. THE MODEL: guards are PARSED + EMITTED here, at
|
||||||
|
* stupidtools generation time, but they are EVALUATED at configure time
|
||||||
|
* inside the generated ./configure — against the shell variables
|
||||||
|
* $have_<feature> (set by todo 12's probe snippets) and the normalized
|
||||||
|
* $st_os. These tests prove the emitted guard shell is syntactically valid
|
||||||
|
* (sh/bash/zsh -n), genuinely evaluates (real `sh` execution against
|
||||||
|
* seeded variables), parses malformed guards into spanned errors, maps the
|
||||||
|
* os table both C-side and via the emitted shell, and that the schema
|
||||||
|
* accepts the `when` feature property (the todo-14 DSL placement).
|
||||||
|
*
|
||||||
|
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c
|
||||||
|
* sources, relative to tests/unit/). resolve.c needs sh_emit.c (st_sh_quote)
|
||||||
|
* + error.c + span.c; schema.c/check_registry.c/value.c/parser.c/lexer.c
|
||||||
|
* are linked for the schema `when` acceptance test (st_kdl_parse +
|
||||||
|
* st_kdl_validate).
|
||||||
|
*/
|
||||||
|
#include "munit.h"
|
||||||
|
|
||||||
|
#include "detect/resolve.h"
|
||||||
|
#include "error.h"
|
||||||
|
#include "kdl/ast.h"
|
||||||
|
#include "kdl/schema.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* ---- per-test temp dir ------------------------------------------------- */
|
||||||
|
|
||||||
|
static char temp_dir[128];
|
||||||
|
|
||||||
|
static void *
|
||||||
|
setup(const MunitParameter params[], void *user_data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)user_data;
|
||||||
|
int n = snprintf(temp_dir, sizeof temp_dir, "/tmp/st_resolve_XXXXXX");
|
||||||
|
|
||||||
|
if (n < 0) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (mkdtemp(temp_dir) == NULL) {
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return (void *)1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
teardown(void *fixture)
|
||||||
|
{
|
||||||
|
char cmd[160];
|
||||||
|
int n;
|
||||||
|
|
||||||
|
if (fixture == NULL || temp_dir[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
n = snprintf(cmd, sizeof cmd, "rm -rf -- '%s'", temp_dir);
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)system(cmd);
|
||||||
|
temp_dir[0] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Join temp_dir/`name` into `buf`; returns the snprintf result so callers
|
||||||
|
* can assert it without ever stringifying a %-carrying format (the munit
|
||||||
|
* %s-stringification trap, see .omo/notepads/stupidtools/learnings.md). */
|
||||||
|
static int
|
||||||
|
mkpath(char *buf, size_t sz, const char *name)
|
||||||
|
{
|
||||||
|
return snprintf(buf, sz, "%s/%s", temp_dir, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read a tiny result file under temp_dir, strip the trailing newline.
|
||||||
|
* Returns a static buffer (empty string when the file is missing). */
|
||||||
|
static const char *
|
||||||
|
read_result(const char *file)
|
||||||
|
{
|
||||||
|
static char buf[256];
|
||||||
|
char path[600];
|
||||||
|
FILE *f;
|
||||||
|
size_t n;
|
||||||
|
int m;
|
||||||
|
|
||||||
|
m = mkpath(path, sizeof path, file);
|
||||||
|
if (m < 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
f = fopen(path, "r");
|
||||||
|
if (f == NULL) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
n = fread(buf, 1, sizeof buf - 1, f);
|
||||||
|
fclose(f);
|
||||||
|
buf[n] = '\0';
|
||||||
|
buf[strcspn(buf, "\n")] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run "sh <path>" via system(); return the shell's exit status or -1. */
|
||||||
|
static int
|
||||||
|
run_sh(const char *path)
|
||||||
|
{
|
||||||
|
char cmd[700];
|
||||||
|
int rc;
|
||||||
|
int n = snprintf(cmd, sizeof cmd, "sh '%s'", path);
|
||||||
|
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rc = system(cmd);
|
||||||
|
if (rc == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run "sh <path>" capturing stdout into out_path; return exit status. */
|
||||||
|
static int
|
||||||
|
run_sh_out(const char *path, const char *out_path)
|
||||||
|
{
|
||||||
|
char cmd[1400];
|
||||||
|
int rc;
|
||||||
|
int n = snprintf(cmd, sizeof cmd, "sh '%s' > '%s' 2>/dev/null", path,
|
||||||
|
out_path);
|
||||||
|
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rc = system(cmd);
|
||||||
|
if (rc == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "<shell> -n <path>": return the shell's exit status or -1. */
|
||||||
|
static int
|
||||||
|
syntax_check(const char *shell, const char *path)
|
||||||
|
{
|
||||||
|
char cmd[700];
|
||||||
|
int rc;
|
||||||
|
int n = snprintf(cmd, sizeof cmd, "%s -n '%s'", shell, path);
|
||||||
|
|
||||||
|
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
rc = system(cmd);
|
||||||
|
if (rc == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return WEXITSTATUS(rc);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- guard emission helpers ------------------------------------------- */
|
||||||
|
|
||||||
|
/* Parse + emit a guard into a static buffer; returns the emitted bytes. */
|
||||||
|
static const char *
|
||||||
|
emit_guard_text(const char *guard)
|
||||||
|
{
|
||||||
|
static char buf[1024];
|
||||||
|
FILE *f = tmpfile();
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
size_t n;
|
||||||
|
|
||||||
|
munit_assert_not_null(f);
|
||||||
|
munit_assert_null(st_when_parse(guard, &ast));
|
||||||
|
munit_assert_not_null(ast);
|
||||||
|
munit_assert_null(st_when_emit(f, ast));
|
||||||
|
st_when_free(ast);
|
||||||
|
munit_assert_int(fflush(f), ==, 0);
|
||||||
|
munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0);
|
||||||
|
n = fread(buf, 1, sizeof buf - 1, f);
|
||||||
|
fclose(f);
|
||||||
|
buf[n] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write a script that evaluates `guard` under `assigns` (raw "var=value"
|
||||||
|
* lines, test-controlled constants) and echoes yes/no to stdout; capture
|
||||||
|
* and return the result ("yes" or "no"). */
|
||||||
|
static const char *
|
||||||
|
guard_eval(const char *guard, const char *const *assigns, size_t nassign)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
char out[600];
|
||||||
|
FILE *f;
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
size_t i;
|
||||||
|
int m;
|
||||||
|
|
||||||
|
m = mkpath(path, sizeof path, "guard.sh");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
m = mkpath(out, sizeof out, "guard.out");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
|
||||||
|
f = fopen(path, "w");
|
||||||
|
munit_assert_not_null(f);
|
||||||
|
(void)fprintf(f, "#!/bin/sh\n");
|
||||||
|
for (i = 0; i < nassign; i++) {
|
||||||
|
(void)fprintf(f, "%s\n", assigns[i]);
|
||||||
|
}
|
||||||
|
(void)fprintf(f, "if ");
|
||||||
|
munit_assert_null(st_when_parse(guard, &ast));
|
||||||
|
munit_assert_not_null(ast);
|
||||||
|
munit_assert_null(st_when_emit(f, ast));
|
||||||
|
st_when_free(ast);
|
||||||
|
(void)fprintf(f, "; then\n echo yes\nelse\n echo no\nfi\n");
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
munit_assert_int(run_sh_out(path, out), ==, 0);
|
||||||
|
return read_result("guard.out");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Emit one feature's resolution under `assigns`, then dump have_<name>
|
||||||
|
* into feature.out; return that value. */
|
||||||
|
static const char *
|
||||||
|
feature_eval(const struct st_resolve_feature *f,
|
||||||
|
const char *const *assigns, size_t nassign)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
char out[600];
|
||||||
|
FILE *fp;
|
||||||
|
size_t i;
|
||||||
|
int m;
|
||||||
|
|
||||||
|
m = mkpath(path, sizeof path, "feature.sh");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
m = mkpath(out, sizeof out, "feature.out");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
|
||||||
|
fp = fopen(path, "w");
|
||||||
|
munit_assert_not_null(fp);
|
||||||
|
(void)fprintf(fp, "#!/bin/sh\n");
|
||||||
|
for (i = 0; i < nassign; i++) {
|
||||||
|
(void)fprintf(fp, "%s\n", assigns[i]);
|
||||||
|
}
|
||||||
|
munit_assert_null(st_resolve_emit_feature(fp, f));
|
||||||
|
(void)fprintf(fp, "printf '%%s\\n' \"$have_%s\" > '%s'\n", f->name, out);
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
munit_assert_int(run_sh(path), ==, 0);
|
||||||
|
return read_result("feature.out");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Run the emitted st_os_norm() over `raw`, returning the normalized name
|
||||||
|
* the shell produced (the raw uname -s string is assigned to $st_os). */
|
||||||
|
static const char *
|
||||||
|
os_norm_shell(const char *raw)
|
||||||
|
{
|
||||||
|
char path[600];
|
||||||
|
char out[600];
|
||||||
|
FILE *f;
|
||||||
|
int m;
|
||||||
|
|
||||||
|
m = mkpath(path, sizeof path, "osnorm.sh");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
m = mkpath(out, sizeof out, "osnorm.out");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
|
||||||
|
f = fopen(path, "w");
|
||||||
|
munit_assert_not_null(f);
|
||||||
|
(void)fprintf(f, "#!/bin/sh\n");
|
||||||
|
munit_assert_null(st_resolve_emit_os_norm(f));
|
||||||
|
(void)fprintf(f, "st_os='%s'\nst_os_norm\nprintf '%%s\\n' \"$st_os\" > '%s'\n",
|
||||||
|
raw, out);
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
munit_assert_int(run_sh(path), ==, 0);
|
||||||
|
return read_result("osnorm.out");
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (content) parse + emit ------------------------------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_parse_emit_content(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
const char *s;
|
||||||
|
|
||||||
|
/* os=linux and have_pthread parses and emits the two leaf tests ANDed */
|
||||||
|
s = emit_guard_text("os=linux and have_pthread");
|
||||||
|
munit_assert_not_null(strstr(s, "\"$st_os\" = 'linux'"));
|
||||||
|
munit_assert_not_null(strstr(s, "\"$have_pthread\" = \"yes\""));
|
||||||
|
munit_assert_not_null(strstr(s, "&&"));
|
||||||
|
|
||||||
|
/* not os=macos: negated os test */
|
||||||
|
s = emit_guard_text("not os=macos");
|
||||||
|
munit_assert_not_null(strstr(s, "!"));
|
||||||
|
munit_assert_not_null(strstr(s, "\"$st_os\" = 'macos'"));
|
||||||
|
|
||||||
|
/* NULL / empty guard -> NULL ast (always true) */
|
||||||
|
munit_assert_null(st_when_parse(NULL, &ast));
|
||||||
|
munit_assert_null(ast);
|
||||||
|
munit_assert_null(st_when_parse("", &ast));
|
||||||
|
munit_assert_null(ast);
|
||||||
|
|
||||||
|
/* NULL out is a usage error, not a crash */
|
||||||
|
{
|
||||||
|
struct st_error *err = st_when_parse("os=linux", NULL);
|
||||||
|
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||||
|
st_error_free(err);
|
||||||
|
}
|
||||||
|
/* NULL stream is a usage error */
|
||||||
|
{
|
||||||
|
struct st_when_ast *a = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
munit_assert_null(st_when_parse("os=linux", &a));
|
||||||
|
munit_assert_not_null(a);
|
||||||
|
err = st_when_emit(NULL, a);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||||
|
st_error_free(err);
|
||||||
|
st_when_free(a);
|
||||||
|
}
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (a) os=linux and have_pthread: real sh eval ---------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_guard_eval(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const yes[] = { "have_pthread=yes", "st_os=linux" };
|
||||||
|
static const char *const no[] = { "have_pthread=no", "st_os=linux" };
|
||||||
|
|
||||||
|
munit_assert_string_equal(
|
||||||
|
guard_eval("os=linux and have_pthread", yes, 2), "yes");
|
||||||
|
munit_assert_string_equal(
|
||||||
|
guard_eval("os=linux and have_pthread", no, 2), "no");
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (b) not os=macos: true on linux, false on macos ------------------ */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_not_os(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const linux[] = { "st_os=linux" };
|
||||||
|
static const char *const macos[] = { "st_os=macos" };
|
||||||
|
|
||||||
|
munit_assert_string_equal(guard_eval("not os=macos", linux, 1), "yes");
|
||||||
|
munit_assert_string_equal(guard_eval("not os=macos", macos, 1), "no");
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (c) precedence: have_a or (have_b and os=linux) ------------------ */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_precedence(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const c1[] = { "have_a=yes", "have_b=no",
|
||||||
|
"st_os=macos" };
|
||||||
|
static const char *const c2[] = { "have_a=no", "have_b=yes",
|
||||||
|
"st_os=linux" };
|
||||||
|
static const char *const c3[] = { "have_a=no", "have_b=yes",
|
||||||
|
"st_os=macos" };
|
||||||
|
|
||||||
|
/* c1: have_a true -> true regardless of the right operand */
|
||||||
|
munit_assert_string_equal(
|
||||||
|
guard_eval("have_a or (have_b and os=linux)", c1, 3), "yes");
|
||||||
|
/* c2: have_a false, (have_b && os=linux) true -> true */
|
||||||
|
munit_assert_string_equal(
|
||||||
|
guard_eval("have_a or (have_b and os=linux)", c2, 3), "yes");
|
||||||
|
/* c3: have_a false and (have_b && os=linux) false -> false */
|
||||||
|
munit_assert_string_equal(
|
||||||
|
guard_eval("have_a or (have_b and os=linux)", c3, 3), "no");
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (d) malformed guards -> spanned errors --------------------------- */
|
||||||
|
|
||||||
|
static void
|
||||||
|
assert_parse_error(const char *guard, const char *needle, size_t col)
|
||||||
|
{
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
|
||||||
|
err = st_when_parse(guard, &ast);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_null(ast);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||||
|
munit_assert_not_null(err->span);
|
||||||
|
munit_assert_size(err->span->line, ==, 1);
|
||||||
|
munit_assert_size(err->span->col, ==, col);
|
||||||
|
munit_assert_true(strstr(st_error_message(err), needle) != NULL);
|
||||||
|
st_error_free(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_parse_errors(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
/* the acceptance case: an unbalanced paren */
|
||||||
|
assert_parse_error("((x", "unknown token 'x'", 3);
|
||||||
|
/* empty feature name after have_ */
|
||||||
|
assert_parse_error("have_", "missing feature name", 1);
|
||||||
|
/* invalid feature identifier */
|
||||||
|
assert_parse_error("have_1x", "invalid feature name", 1);
|
||||||
|
/* empty os name */
|
||||||
|
assert_parse_error("os=", "missing os name", 1);
|
||||||
|
/* unknown word */
|
||||||
|
assert_parse_error("bogus", "unknown token 'bogus'", 1);
|
||||||
|
/* unterminated group */
|
||||||
|
assert_parse_error("(", "unexpected end", 2);
|
||||||
|
/* trailing close paren */
|
||||||
|
assert_parse_error("os=linux )", "unexpected token ')'", 10);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (e) os mapping: C-side table + emitted shell --------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_os_mapping(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
/* C-side mapping function */
|
||||||
|
munit_assert_string_equal(st_os_normalize("Linux"), "linux");
|
||||||
|
munit_assert_string_equal(st_os_normalize("Darwin"), "macos");
|
||||||
|
munit_assert_string_equal(st_os_normalize("FreeBSD"), "bsd");
|
||||||
|
munit_assert_string_equal(st_os_normalize("OpenBSD"), "bsd");
|
||||||
|
munit_assert_string_equal(st_os_normalize("NetBSD"), "bsd");
|
||||||
|
munit_assert_string_equal(st_os_normalize("Solaris"), "other");
|
||||||
|
munit_assert_string_equal(st_os_normalize("Linux-gnu"), "other");
|
||||||
|
munit_assert_string_equal(st_os_normalize(NULL), "other");
|
||||||
|
|
||||||
|
/* the emitted st_os_norm() reproduces the mapping under real sh */
|
||||||
|
munit_assert_string_equal(os_norm_shell("Linux"), "linux");
|
||||||
|
munit_assert_string_equal(os_norm_shell("Darwin"), "macos");
|
||||||
|
munit_assert_string_equal(os_norm_shell("FreeBSD"), "bsd");
|
||||||
|
munit_assert_string_equal(os_norm_shell("OpenBSD"), "bsd");
|
||||||
|
munit_assert_string_equal(os_norm_shell("Solaris"), "other");
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- (f) emitted guard shells pass sh -n / bash -n / zsh -n ----------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_syntax(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const banned[] = {
|
||||||
|
"[[ ", "]]", "local ", "==", "<<<", "&>", "set -e",
|
||||||
|
};
|
||||||
|
static const char *const guards[] = {
|
||||||
|
"os=linux and have_pthread",
|
||||||
|
"not os=macos",
|
||||||
|
"have_a or (have_b and os=linux)",
|
||||||
|
"not (have_a and not os=bsd)",
|
||||||
|
"os=other",
|
||||||
|
};
|
||||||
|
char path[600];
|
||||||
|
FILE *f;
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
size_t i;
|
||||||
|
int m;
|
||||||
|
char *bytes;
|
||||||
|
long n;
|
||||||
|
|
||||||
|
m = mkpath(path, sizeof path, "syntax.sh");
|
||||||
|
munit_assert_int(m, >, 0);
|
||||||
|
f = fopen(path, "w");
|
||||||
|
munit_assert_not_null(f);
|
||||||
|
(void)fprintf(f, "#!/bin/sh\n");
|
||||||
|
munit_assert_null(st_resolve_emit_os_norm(f));
|
||||||
|
for (i = 0; i < sizeof guards / sizeof guards[0]; i++) {
|
||||||
|
(void)fprintf(f, "if ");
|
||||||
|
munit_assert_null(st_when_parse(guards[i], &ast));
|
||||||
|
munit_assert_not_null(ast);
|
||||||
|
munit_assert_null(st_when_emit(f, ast));
|
||||||
|
st_when_free(ast);
|
||||||
|
(void)fprintf(f, "; then :; fi\n");
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
munit_assert_int(syntax_check("sh", path), ==, 0);
|
||||||
|
munit_assert_int(syntax_check("bash", path), ==, 0);
|
||||||
|
munit_assert_int(syntax_check("zsh", path), ==, 0);
|
||||||
|
|
||||||
|
/* banned-construct sweep on the real emitted bytes */
|
||||||
|
f = fopen(path, "rb");
|
||||||
|
munit_assert_not_null(f);
|
||||||
|
munit_assert_int(fseek(f, 0, SEEK_END), ==, 0);
|
||||||
|
n = ftell(f);
|
||||||
|
munit_assert_int(n, >, 0);
|
||||||
|
munit_assert_int(fseek(f, 0, SEEK_SET), ==, 0);
|
||||||
|
bytes = munit_malloc((size_t)n + 1);
|
||||||
|
munit_assert_size(fread(bytes, 1, (size_t)n, f), ==, (size_t)n);
|
||||||
|
fclose(f);
|
||||||
|
bytes[n] = '\0';
|
||||||
|
for (i = 0; i < sizeof banned / sizeof banned[0]; i++) {
|
||||||
|
munit_assert_null(strstr(bytes, banned[i]));
|
||||||
|
}
|
||||||
|
free(bytes);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- feature aggregation: multi-check AND ----------------------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_emit_feature_aggregate(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
static const char *const both[] = { "have_pthread_0=yes",
|
||||||
|
"have_pthread_1=yes" };
|
||||||
|
static const char *const one[] = { "have_pthread_0=yes",
|
||||||
|
"have_pthread_1=no" };
|
||||||
|
static const char *const none[] = { "have_pthread_0=no",
|
||||||
|
"have_pthread_1=no" };
|
||||||
|
const char *check_names[2] = { "pthread_0", "pthread_1" };
|
||||||
|
struct st_resolve_feature f = { "pthread", check_names, 2, NULL };
|
||||||
|
|
||||||
|
munit_assert_string_equal(feature_eval(&f, both, 2), "yes");
|
||||||
|
munit_assert_string_equal(feature_eval(&f, one, 2), "no");
|
||||||
|
munit_assert_string_equal(feature_eval(&f, none, 2), "no");
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- feature guard gating: when-guard forces have_<f>=no ------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_emit_feature_guard(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
const char *check_names[1] = { "pthread" };
|
||||||
|
struct st_when_ast *ast = NULL;
|
||||||
|
struct st_resolve_feature f;
|
||||||
|
static const char *const linux[] = { "have_pthread=yes", "st_os=linux" };
|
||||||
|
static const char *const macos[] = { "have_pthread=yes", "st_os=macos" };
|
||||||
|
|
||||||
|
munit_assert_null(st_when_parse("os=linux", &ast));
|
||||||
|
f.name = "pthread";
|
||||||
|
f.check_names = check_names;
|
||||||
|
f.check_count = 1;
|
||||||
|
f.guard = ast;
|
||||||
|
|
||||||
|
/* guard holds -> the probe's have_pthread=yes survives */
|
||||||
|
munit_assert_string_equal(feature_eval(&f, linux, 2), "yes");
|
||||||
|
/* guard fails -> have_pthread is forced to no */
|
||||||
|
munit_assert_string_equal(feature_eval(&f, macos, 2), "no");
|
||||||
|
|
||||||
|
st_when_free(ast);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- check-name scheme (the todo-12/16 naming contract) --------------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_check_name(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
char buf[64];
|
||||||
|
|
||||||
|
/* single-check feature -> the feature name itself */
|
||||||
|
munit_assert_int(st_resolve_check_name("pthread", 0, 1, buf,
|
||||||
|
sizeof buf), ==, 0);
|
||||||
|
munit_assert_string_equal(buf, "pthread");
|
||||||
|
|
||||||
|
/* multi-check feature -> <name>_0, <name>_1, ... */
|
||||||
|
munit_assert_int(st_resolve_check_name("pthread", 0, 2, buf,
|
||||||
|
sizeof buf), ==, 0);
|
||||||
|
munit_assert_string_equal(buf, "pthread_0");
|
||||||
|
munit_assert_int(st_resolve_check_name("pthread", 1, 2, buf,
|
||||||
|
sizeof buf), ==, 0);
|
||||||
|
munit_assert_string_equal(buf, "pthread_1");
|
||||||
|
|
||||||
|
/* invalid feature name / out-of-range index / zero count -> -1 */
|
||||||
|
munit_assert_int(st_resolve_check_name("1bad", 0, 1, buf,
|
||||||
|
sizeof buf), ==, -1);
|
||||||
|
munit_assert_int(st_resolve_check_name("pthread", 1, 1, buf,
|
||||||
|
sizeof buf), ==, -1);
|
||||||
|
munit_assert_int(st_resolve_check_name("pthread", 0, 0, buf,
|
||||||
|
sizeof buf), ==, -1);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- schema: the `when` feature property (todo-14 placement) ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_schema_when(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
struct st_kdl_document *doc;
|
||||||
|
|
||||||
|
/* a feature may carry one `when "<expr>"` property */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"pthread\" when=\"os=linux\" "
|
||||||
|
"{ header \"pthread.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
munit_assert_null(st_kdl_validate(doc));
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
/* the guard string is NOT parsed by the schema (that is resolve's job):
|
||||||
|
* a malformed guard is still structurally a non-empty string */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"pthread\" when=\"((x\" "
|
||||||
|
"{ header \"pthread.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
munit_assert_null(st_kdl_validate(doc));
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
/* `when` must be a non-empty string */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"pthread\" when=42 "
|
||||||
|
"{ header \"pthread.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
err = st_kdl_validate(doc);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||||
|
munit_assert_true(strstr(st_error_message(err), "non-empty string") !=
|
||||||
|
NULL);
|
||||||
|
st_error_free(err);
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
/* empty when string is rejected too */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"pthread\" when=\"\" "
|
||||||
|
"{ header \"pthread.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
err = st_kdl_validate(doc);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_true(strstr(st_error_message(err), "non-empty string") !=
|
||||||
|
NULL);
|
||||||
|
st_error_free(err);
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
/* duplicate `when` is rejected */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"pthread\" when=\"os=linux\" when=\"os=bsd\" "
|
||||||
|
"{ header \"pthread.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
err = st_kdl_validate(doc);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_true(strstr(st_error_message(err), "duplicate 'when'") !=
|
||||||
|
NULL);
|
||||||
|
st_error_free(err);
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
/* a non-when property on a feature is still rejected (unchanged) */
|
||||||
|
doc = st_kdl_parse("project \"p\" version \"1.0\"\n"
|
||||||
|
"feature \"f\" foo=1 { header \"h.h\" }",
|
||||||
|
"t.kdl", &err);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_not_null(doc);
|
||||||
|
err = st_kdl_validate(doc);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_true(strstr(st_error_message(err),
|
||||||
|
"unexpected property 'foo'") != NULL);
|
||||||
|
st_error_free(err);
|
||||||
|
st_kdl_document_free(doc);
|
||||||
|
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MunitTest tests[] = {
|
||||||
|
{ "/resolve/parse-emit-content", test_parse_emit_content, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/guard-eval", test_guard_eval, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/not-os", test_not_os, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/precedence", test_precedence, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/parse-errors", test_parse_errors, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/os-mapping", test_os_mapping, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/syntax", test_syntax, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/emit-feature-aggregate", test_emit_feature_aggregate, setup,
|
||||||
|
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/emit-feature-guard", test_emit_feature_guard, setup, teardown,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/check-name", test_check_name, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/resolve/schema-when", test_schema_when, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const MunitSuite suite = {
|
||||||
|
"/resolve", tests, NULL, 1, MUNIT_SUITE_OPTION_NONE,
|
||||||
|
};
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[MUNIT_ARRAY_PARAM(argc + 1)])
|
||||||
|
{
|
||||||
|
return munit_suite_main(&suite, NULL, argc, argv);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user