Template
feat(detect): implement check registry probe specs
This commit is contained in:
@@ -0,0 +1,769 @@
|
||||
/*
|
||||
* checks.c - probe-source specs for the 8 check kinds (todo 11).
|
||||
*
|
||||
* Builds a `struct st_check_probe` for a schema-validated CHECK node, per
|
||||
* the per-kind table documented in detect/checks.h. This module builds
|
||||
* SPECS ONLY: it does not execute probes (todo 12) and does not emit shell
|
||||
* text (todo 16). See the header for the struct contract and ownership.
|
||||
*
|
||||
* DESIGN NOTES (the decisions that are NOT obvious from the header):
|
||||
*
|
||||
* - FUNCTION probes carry `-fno-builtin` in extra_args. GCC treats
|
||||
* ~1000 names (strdup, printf, malloc, ...) as builtins and, with
|
||||
* `-Wbuiltin-declaration-mismatch` (on by default), warns on ANY
|
||||
* declaration whose signature differs from the builtin's — and there
|
||||
* is no single generic signature compatible with every builtin. A
|
||||
* generic `extern void NAME(void); ... NAME();` is therefore wrapped
|
||||
* in `-fno-builtin` so the link probe resolves the REAL library
|
||||
* symbol (also preventing builtin inlining from giving a false
|
||||
* positive). The function is declared `void (void)` and CALLED; the C
|
||||
* ABI does not name-mangle, so linking resolves the symbol NAME
|
||||
* regardless of its true signature — the probe only checks symbol
|
||||
* presence and never runs the function.
|
||||
*
|
||||
* - LIBRARY probes link a trivial `int main(void){return 0;}` against
|
||||
* `-l<name>`. The DSL carries only the library name (no function, as
|
||||
* AC_CHECK_LIB requires), so "present and linkable" is the honest
|
||||
* v1 meaning: the link fails if `-l<name>` cannot be found or linked.
|
||||
*
|
||||
* - VERSION constraints (`library`/`pkg_config` carry an optional
|
||||
* `version "<op><version>"` pair) are PARSED here (`<op>` ∈
|
||||
* >= <= = > <; `<version>` dotted-numeric) and the raw string is
|
||||
* recorded in probe->version_constraint. v1 enforcement:
|
||||
* * library: the probe is the plain `-l` link probe UNCHANGED; the
|
||||
* constraint is recorded for todo 16 to enforce via pkg-config
|
||||
* (or a library symbol check) when available.
|
||||
* * pkg_config: the op maps to a pkg-config version flag —
|
||||
* `>=` → `--atleast-version=`, `<=` → `--max-version=`,
|
||||
* `=` → `--exact-version=` — added before `--cflags --libs`.
|
||||
* Strict `>` and `<` have no pkg-config flag: they are recorded
|
||||
* in version_constraint only (todo 16 may reject or handle them).
|
||||
*
|
||||
* - HEADER names are embedded as `#include <NAME>` when the name is
|
||||
* "plain" (only [A-Za-z0-9_./+-]); otherwise as `#include "..."` with
|
||||
* `"` and `\` C-escaped, so a hostile name (`"; rm -rf /`) stays a
|
||||
* literal string and cannot break out of the directive. Any control
|
||||
* byte (newline/NUL/...) in a header/function/type name is rejected.
|
||||
*
|
||||
* - TYPE and SIZEOF names are embedded verbatim in `sizeof(...)`; only
|
||||
* control bytes are rejected (a `unsigned long`-style multi-token
|
||||
* type is legitimate, and a bogus type fails the compile probe
|
||||
* naturally). v1 does NOT add an AC_INCLUDES_DEFAULT-style header
|
||||
* list, so `type "size_t"` tests only types visible without includes.
|
||||
*
|
||||
* ERRORS AND SPANS
|
||||
* ----------------
|
||||
* Failures are owned ST_ERR_KDL_SCHEMA errors with the span heap-allocated
|
||||
* IN THE SAME BLOCK as the error (the parser.c pattern), at the offending
|
||||
* token. See checks.h.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "detect/checks.h"
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
#include "kdl/value.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* ---- owned errors ----------------------------------------------------- */
|
||||
|
||||
/* Build an owned ST_ERR_KDL_SCHEMA error whose span lives in the same
|
||||
* allocation as the error struct (aligned right after it). Returns NULL
|
||||
* only on allocation failure. */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Re-own a value-model (ST_ERR_KDL_PARSE) error as a schema error: copy
|
||||
* the message and the span VALUE into a fresh block. The caller frees the
|
||||
* original error. */
|
||||
static struct st_error *
|
||||
schema_err_from(const struct st_error *e)
|
||||
{
|
||||
struct st_span sp;
|
||||
|
||||
if (e->span != NULL) {
|
||||
sp = *e->span;
|
||||
} else {
|
||||
sp = (struct st_span){ NULL, 0, 0 };
|
||||
}
|
||||
return err_at_owned(sp, e->message != NULL ? e->message : "");
|
||||
}
|
||||
|
||||
/* ---- small allocators -------------------------------------------------- */
|
||||
|
||||
/* Duplicate a string without relying on POSIX strdup (which strict C23
|
||||
* hides behind _POSIX_C_SOURCE). Returns NULL on OOM. */
|
||||
static char *
|
||||
xstrdup(const char *s)
|
||||
{
|
||||
size_t n = strlen(s);
|
||||
char *p = malloc(n + 1);
|
||||
|
||||
if (p != NULL) {
|
||||
memcpy(p, s, n + 1);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
/* vsnprintf-based string builder. Returns a malloc'd NUL-terminated string
|
||||
* or NULL on OOM / formatting failure. */
|
||||
static char *
|
||||
strbuild(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_list ap2;
|
||||
int n;
|
||||
char *out;
|
||||
|
||||
va_start(ap, fmt);
|
||||
va_copy(ap2, ap);
|
||||
n = vsnprintf(NULL, 0, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0) {
|
||||
va_end(ap2);
|
||||
return NULL;
|
||||
}
|
||||
out = malloc((size_t)n + 1);
|
||||
if (out == NULL) {
|
||||
va_end(ap2);
|
||||
return NULL;
|
||||
}
|
||||
(void)vsnprintf(out, (size_t)n + 1, fmt, ap2);
|
||||
va_end(ap2);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Free a NULL-terminated argv of heap strings (each element freed, then
|
||||
* the array). NULL is a safe no-op. */
|
||||
static void
|
||||
argv_free(char **av)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (av == NULL) {
|
||||
return;
|
||||
}
|
||||
for (i = 0; av[i] != NULL; i++) {
|
||||
free(av[i]);
|
||||
}
|
||||
free(av);
|
||||
}
|
||||
|
||||
/* ---- argument extraction ---------------------------------------------- */
|
||||
|
||||
/* Interpret `tok` (with optional annotation) as the non-empty unannotated
|
||||
* string a check slot requires; on success *out owns the string. `ctx`
|
||||
* names the slot in messages, e.g. "check 'header' argument". */
|
||||
static struct st_error *
|
||||
require_string_arg(const struct st_kdl_token_ref *tok,
|
||||
const struct st_kdl_token_ref *ann,
|
||||
const char *ctx, char **out)
|
||||
{
|
||||
struct st_kdl_value v;
|
||||
struct st_error *e;
|
||||
char msg[192];
|
||||
|
||||
*out = NULL;
|
||||
e = st_kdl_value_from_token_annotated(tok, ann, &v);
|
||||
if (e != NULL) {
|
||||
struct st_error *r = schema_err_from(e);
|
||||
|
||||
st_error_free(e);
|
||||
return r;
|
||||
}
|
||||
if (v.annotation != NULL) {
|
||||
snprintf(msg, sizeof msg, "%s must not carry a type annotation", ctx);
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
if (v.kind != ST_KDL_VAL_STRING) {
|
||||
snprintf(msg, sizeof msg, "%s must be a non-empty string (got %s)",
|
||||
ctx, st_kdl_value_kind_name(v.kind));
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
if (v.as.str[0] == '\0') {
|
||||
snprintf(msg, sizeof msg, "%s must be a non-empty string", ctx);
|
||||
e = err_at_owned(tok->span, msg);
|
||||
st_kdl_value_free(&v);
|
||||
return e;
|
||||
}
|
||||
*out = v.as.str;
|
||||
v.as.str = NULL;
|
||||
st_kdl_value_free(&v); /* frees the annotation only */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- token safety checks (C-embedding boundaries) --------------------- */
|
||||
|
||||
/* True when `s` contains a control byte (including NUL); these cannot
|
||||
* appear in any C source token we embed verbatim. */
|
||||
static bool
|
||||
has_control_byte(const char *s)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)s;
|
||||
|
||||
for (; *p != '\0'; p++) {
|
||||
if (*p < 0x20u || *p == 0x7fu) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* True when `s` is a plain header name (only [A-Za-z0-9_./+-]). */
|
||||
static bool
|
||||
is_plain_header_name(const char *s)
|
||||
{
|
||||
const unsigned char *p = (const unsigned char *)s;
|
||||
|
||||
if (*p == '\0') {
|
||||
return false;
|
||||
}
|
||||
for (; *p != '\0'; p++) {
|
||||
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
|
||||
(*p >= '0' && *p <= '9') || *p == '_' || *p == '.' ||
|
||||
*p == '/' || *p == '+' || *p == '-')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* True when `s` is a valid C identifier (letter/underscore, then letters,
|
||||
* digits, underscores). */
|
||||
static bool
|
||||
is_c_identifier(const char *s)
|
||||
{
|
||||
const unsigned char *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;
|
||||
}
|
||||
|
||||
/* Build the include target for a header name: `<name>` for plain names,
|
||||
* `"..."` (with `"` and `\` C-escaped) otherwise. Returns a malloc'd
|
||||
* string, or NULL with *err set when the name carries a control byte. */
|
||||
static char *
|
||||
header_ref(const char *name, struct st_error **err)
|
||||
{
|
||||
if (err != NULL) {
|
||||
*err = NULL;
|
||||
}
|
||||
if (has_control_byte(name)) {
|
||||
char msg[192];
|
||||
|
||||
snprintf(msg, sizeof msg,
|
||||
"check 'header': header name contains a control character");
|
||||
if (err != NULL) {
|
||||
*err = st_error_kdl_schema(msg);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
if (is_plain_header_name(name)) {
|
||||
return strbuild("<%s>", name);
|
||||
}
|
||||
{
|
||||
/* escape `\` and `"` for the quoted include form */
|
||||
size_t i;
|
||||
size_t cap = strlen(name) * 2 + 3; /* 2x for worst-case escapes + "" NUL */
|
||||
char *out = malloc(cap);
|
||||
size_t n = 0;
|
||||
|
||||
if (out == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
out[n++] = '"';
|
||||
for (i = 0; name[i] != '\0'; i++) {
|
||||
if (name[i] == '\\' || name[i] == '"') {
|
||||
out[n++] = '\\';
|
||||
}
|
||||
out[n++] = name[i];
|
||||
}
|
||||
out[n++] = '"';
|
||||
out[n] = '\0';
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- version constraint parsing --------------------------------------- */
|
||||
|
||||
/* Parse "<op><version>". On success returns 0 and sets *ver (a pointer
|
||||
* into `s`, NUL-terminated because `s` is). The version part must be
|
||||
* non-empty dotted-numeric (digits and '.', at least one digit). Returns
|
||||
* -1 otherwise. */
|
||||
static int
|
||||
parse_constraint(const char *s, const char **ver)
|
||||
{
|
||||
size_t ol = 0;
|
||||
const char *v;
|
||||
size_t i;
|
||||
size_t ndigits = 0;
|
||||
|
||||
if (s[0] == '>' && s[1] == '=') {
|
||||
ol = 2;
|
||||
} else if (s[0] == '<' && s[1] == '=') {
|
||||
ol = 2;
|
||||
} else if (s[0] == '>' || s[0] == '<' || s[0] == '=') {
|
||||
ol = 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
v = s + ol;
|
||||
if (v[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0; v[i] != '\0'; i++) {
|
||||
if (v[i] >= '0' && v[i] <= '9') {
|
||||
ndigits++;
|
||||
} else if (v[i] != '.') {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (ndigits == 0) {
|
||||
return -1;
|
||||
}
|
||||
*ver = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- per-kind source builders ----------------------------------------- */
|
||||
|
||||
/* The trivial main shared by library and compiler_flag probes. */
|
||||
static char *
|
||||
trivial_main_source(void)
|
||||
{
|
||||
return xstrdup(
|
||||
"int main(void)\n"
|
||||
"{\n"
|
||||
" return 0;\n"
|
||||
"}\n");
|
||||
}
|
||||
|
||||
/* ---- public API ------------------------------------------------------- */
|
||||
|
||||
struct st_error *
|
||||
st_check_probe_build(enum st_check_kind kind, const struct st_kdl_node *node,
|
||||
struct st_check_probe *out)
|
||||
{
|
||||
struct st_check_probe tmp = { ST_PROBE_PREPROCESS, NULL, NULL, NULL,
|
||||
NULL };
|
||||
const struct st_check_shape *shape;
|
||||
struct st_error *err = NULL;
|
||||
char *arg = NULL;
|
||||
char *constraint = NULL; /* raw "<op><version>" for library/pkg_config */
|
||||
const char *con_ver = NULL; /* parsed version pointer into `constraint` */
|
||||
char ctx[96];
|
||||
|
||||
if (out != NULL) {
|
||||
*out = (struct st_check_probe){ ST_PROBE_PREPROCESS, NULL, NULL, NULL,
|
||||
NULL };
|
||||
}
|
||||
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
|
||||
return st_error_kdl_schema(
|
||||
"st_check_probe_build: unknown check kind");
|
||||
}
|
||||
if (node == NULL) {
|
||||
return st_error_kdl_schema("check node is NULL");
|
||||
}
|
||||
|
||||
shape = &st_check_shapes[kind];
|
||||
|
||||
/* the target argument */
|
||||
if (node->args == NULL) {
|
||||
char msg[192];
|
||||
|
||||
snprintf(msg, sizeof msg,
|
||||
"check '%s' requires an argument (the %s)", shape->name,
|
||||
shape->arg_meaning);
|
||||
err = err_at_owned(node->name.span, msg);
|
||||
goto done;
|
||||
}
|
||||
snprintf(ctx, sizeof ctx, "check '%s' argument", shape->name);
|
||||
err = require_string_arg(&node->args->value, node->args->annotation, ctx,
|
||||
&arg);
|
||||
if (err != NULL) {
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* the optional `version` keyword + constraint pair (library/pkg_config) */
|
||||
if (shape->optional_args == 2 && node->args->next != NULL) {
|
||||
const struct st_kdl_arg *vk = node->args->next;
|
||||
char msg[192];
|
||||
|
||||
if (vk->annotation != NULL ||
|
||||
!(vk->value.kind == ST_TOK_IDENT && vk->value.len == 7 &&
|
||||
memcmp(vk->value.text, "version", 7) == 0)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check '%s': expected the keyword 'version' as the second "
|
||||
"argument", shape->name);
|
||||
err = err_at_owned(vk->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
if (vk->next == NULL) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check '%s': 'version' requires the version constraint "
|
||||
"argument", shape->name);
|
||||
err = err_at_owned(vk->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
snprintf(ctx, sizeof ctx, "check '%s' version", shape->name);
|
||||
err = require_string_arg(&vk->next->value, vk->next->annotation, ctx,
|
||||
&constraint);
|
||||
if (err != NULL) {
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
/* Validate "<op><version>" for BOTH kinds; `library` only records it
|
||||
* (enforcement is todo 16), so this runs even where it is not applied. */
|
||||
if (constraint != NULL) {
|
||||
char msg[192];
|
||||
|
||||
if (parse_constraint(constraint, &con_ver) != 0) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check '%s': malformed version constraint '%s' "
|
||||
"(expected '<op><version>', e.g. '>=1.0')", shape->name,
|
||||
constraint);
|
||||
err = err_at_owned(node->args->next->next->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case ST_CHECK_HEADER: {
|
||||
char *ref = header_ref(arg, &err);
|
||||
|
||||
if (err != NULL) {
|
||||
goto done;
|
||||
}
|
||||
if (ref == NULL) {
|
||||
err = st_error_internal("out of memory building header probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.mode = ST_PROBE_COMPILE;
|
||||
tmp.c_source = strbuild("#include %s\n", ref);
|
||||
free(ref);
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building header probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_FUNCTION: {
|
||||
char msg[192];
|
||||
|
||||
if (!is_c_identifier(arg)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check 'function': function name '%s' is not a valid C "
|
||||
"identifier", arg);
|
||||
err = err_at_owned(node->args->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
tmp.mode = ST_PROBE_LINK;
|
||||
tmp.c_source = strbuild(
|
||||
"extern void %s(void);\n"
|
||||
"int main(void)\n"
|
||||
"{\n"
|
||||
" %s();\n"
|
||||
" return 0;\n"
|
||||
"}\n",
|
||||
arg, arg);
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building function probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args = calloc(2, sizeof(char *));
|
||||
if (tmp.extra_args == NULL) {
|
||||
err = st_error_internal("out of memory building function probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args[0] = xstrdup("-fno-builtin");
|
||||
if (tmp.extra_args[0] == NULL) {
|
||||
err = st_error_internal("out of memory building function probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_LIBRARY: {
|
||||
tmp.mode = ST_PROBE_LINK;
|
||||
tmp.c_source = trivial_main_source();
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building library probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args = calloc(2, sizeof(char *));
|
||||
if (tmp.extra_args == NULL) {
|
||||
err = st_error_internal("out of memory building library probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args[0] = strbuild("-l%s", arg);
|
||||
if (tmp.extra_args[0] == NULL) {
|
||||
err = st_error_internal("out of memory building library probe");
|
||||
goto done;
|
||||
}
|
||||
if (constraint != NULL) {
|
||||
tmp.version_constraint = xstrdup(constraint);
|
||||
if (tmp.version_constraint == NULL) {
|
||||
err = st_error_internal("out of memory building library "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_TYPE: {
|
||||
char msg[192];
|
||||
|
||||
if (has_control_byte(arg)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check 'type': type name contains a control character");
|
||||
err = err_at_owned(node->args->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
tmp.mode = ST_PROBE_COMPILE;
|
||||
tmp.c_source = strbuild(
|
||||
"_Static_assert(sizeof(%s) != 0, \"type must be a complete "
|
||||
"object type\");\n",
|
||||
arg);
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building type probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_SIZEOF: {
|
||||
char msg[192];
|
||||
|
||||
if (has_control_byte(arg)) {
|
||||
snprintf(msg, sizeof msg,
|
||||
"check 'sizeof': type name contains a control character");
|
||||
err = err_at_owned(node->args->value.span, msg);
|
||||
goto done;
|
||||
}
|
||||
tmp.mode = ST_PROBE_RUN;
|
||||
tmp.c_source = strbuild(
|
||||
"#include <stdio.h>\n"
|
||||
"_Static_assert(sizeof(%s) != 0, \"type must be a complete "
|
||||
"object type\");\n"
|
||||
"int main(void)\n"
|
||||
"{\n"
|
||||
" printf(\"%%zu\\n\", sizeof(%s));\n"
|
||||
" return 0;\n"
|
||||
"}\n",
|
||||
arg, arg);
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building sizeof probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_PROGRAM: {
|
||||
tmp.mode = ST_PROBE_COMMAND;
|
||||
tmp.command = calloc(4, sizeof(char *));
|
||||
if (tmp.command == NULL) {
|
||||
err = st_error_internal("out of memory building program probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.command[0] = xstrdup("command");
|
||||
tmp.command[1] = xstrdup("-v");
|
||||
tmp.command[2] = xstrdup(arg);
|
||||
if (tmp.command[0] == NULL || tmp.command[1] == NULL ||
|
||||
tmp.command[2] == NULL) {
|
||||
err = st_error_internal("out of memory building program probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_COMPILER_FLAG: {
|
||||
tmp.mode = ST_PROBE_COMPILE;
|
||||
tmp.c_source = trivial_main_source();
|
||||
if (tmp.c_source == NULL) {
|
||||
err = st_error_internal("out of memory building compiler_flag "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args = calloc(2, sizeof(char *));
|
||||
if (tmp.extra_args == NULL) {
|
||||
err = st_error_internal("out of memory building compiler_flag "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.extra_args[0] = xstrdup(arg);
|
||||
if (tmp.extra_args[0] == NULL) {
|
||||
err = st_error_internal("out of memory building compiler_flag "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ST_CHECK_PKG_CONFIG: {
|
||||
const char *ver_flag = NULL;
|
||||
|
||||
if (constraint != NULL) {
|
||||
if (constraint[0] == '>' && constraint[1] == '=') {
|
||||
ver_flag = "--atleast-version=";
|
||||
} else if (constraint[0] == '<' && constraint[1] == '=') {
|
||||
ver_flag = "--max-version=";
|
||||
} else if (constraint[0] == '=') {
|
||||
ver_flag = "--exact-version=";
|
||||
}
|
||||
/* strict '>' and '<' have no pkg-config flag: recorded only */
|
||||
tmp.version_constraint = xstrdup(constraint);
|
||||
if (tmp.version_constraint == NULL) {
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
tmp.mode = ST_PROBE_COMMAND;
|
||||
if (ver_flag != NULL) {
|
||||
char *flag = strbuild("%s%s", ver_flag, con_ver);
|
||||
|
||||
if (flag == NULL) {
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.command = calloc(6, sizeof(char *));
|
||||
if (tmp.command == NULL) {
|
||||
free(flag);
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.command[0] = xstrdup("pkg-config");
|
||||
tmp.command[1] = flag;
|
||||
tmp.command[2] = xstrdup("--cflags");
|
||||
tmp.command[3] = xstrdup("--libs");
|
||||
tmp.command[4] = xstrdup(arg);
|
||||
if (tmp.command[0] == NULL || tmp.command[2] == NULL ||
|
||||
tmp.command[3] == NULL || tmp.command[4] == NULL) {
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
tmp.command = calloc(5, sizeof(char *));
|
||||
if (tmp.command == NULL) {
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
tmp.command[0] = xstrdup("pkg-config");
|
||||
tmp.command[1] = xstrdup("--cflags");
|
||||
tmp.command[2] = xstrdup("--libs");
|
||||
tmp.command[3] = xstrdup(arg);
|
||||
if (tmp.command[0] == NULL || tmp.command[1] == NULL ||
|
||||
tmp.command[2] == NULL || tmp.command[3] == NULL) {
|
||||
err = st_error_internal("out of memory building pkg_config "
|
||||
"probe");
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
err = st_error_kdl_schema("st_check_probe_build: unknown check kind");
|
||||
goto done;
|
||||
}
|
||||
|
||||
done:
|
||||
free(arg);
|
||||
free(constraint);
|
||||
if (err != NULL) {
|
||||
st_check_probe_free(&tmp);
|
||||
return err;
|
||||
}
|
||||
if (out != NULL) {
|
||||
*out = tmp;
|
||||
} else {
|
||||
st_check_probe_free(&tmp);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void
|
||||
st_check_probe_free(struct st_check_probe *p)
|
||||
{
|
||||
if (p == NULL) {
|
||||
return;
|
||||
}
|
||||
free(p->c_source);
|
||||
free(p->version_constraint);
|
||||
argv_free(p->command);
|
||||
argv_free(p->extra_args);
|
||||
p->mode = ST_PROBE_PREPROCESS;
|
||||
p->c_source = NULL;
|
||||
p->version_constraint = NULL;
|
||||
p->command = NULL;
|
||||
p->extra_args = NULL;
|
||||
}
|
||||
|
||||
const char *
|
||||
st_check_probe_mode_name(enum st_check_probe_mode mode)
|
||||
{
|
||||
switch (mode) {
|
||||
case ST_PROBE_PREPROCESS:
|
||||
return "preprocess";
|
||||
case ST_PROBE_COMPILE:
|
||||
return "compile";
|
||||
case ST_PROBE_LINK:
|
||||
return "link";
|
||||
case ST_PROBE_RUN:
|
||||
return "run";
|
||||
case ST_PROBE_COMMAND:
|
||||
return "command";
|
||||
default:
|
||||
return "?";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* checks.h - probe-source specs for the 8 check kinds (todo 11).
|
||||
*
|
||||
* Given a feature's CHECK node (already schema-validated by src/kdl/schema.c),
|
||||
* this module builds the exact PROBE SPEC that todo 12 (probe code generator)
|
||||
* turns into configure-time compile/link/run/command invocations and todo 16
|
||||
* (configure assembly) substitutes the results of. It builds SPECS ONLY: it
|
||||
* never executes a probe, never touches the filesystem beyond heap
|
||||
* allocation, and never emits shell text.
|
||||
*
|
||||
* THE PROBE SPEC (the contract todo 12/16 consume)
|
||||
* ------------------------------------------------
|
||||
*
|
||||
* enum st_check_probe_mode {
|
||||
* ST_PROBE_PREPROCESS, // -E: header existence via the preprocessor
|
||||
* ST_PROBE_COMPILE, // compile a C translation unit (-c)
|
||||
* ST_PROBE_LINK, // compile + link a C program
|
||||
* ST_PROBE_RUN, // compile + link + RUN, capture stdout
|
||||
* ST_PROBE_COMMAND, // run an external command via execvp (argv)
|
||||
* };
|
||||
*
|
||||
* struct st_check_probe {
|
||||
* enum st_check_probe_mode mode;
|
||||
* char *c_source; // C snippet; NULL for COMMAND probes
|
||||
* char **command; // argv (NULL-terminated); NULL unless
|
||||
* // mode == ST_PROBE_COMMAND
|
||||
* char **extra_args; // argv (NULL-terminated) of extra
|
||||
* // compile/link flags (e.g. "-lpthread",
|
||||
* // "-fsanitize=address",
|
||||
* // "-fno-builtin"); NULL when empty
|
||||
* char *version_constraint; // raw "<op><version>" string for
|
||||
* // library/pkg_config; NULL when absent
|
||||
* };
|
||||
*
|
||||
* - `command` and `extra_args` are exec-style argv arrays (char **, first
|
||||
* element is argv[0] for `command`, a flag for `extra_args`; both
|
||||
* NULL-terminated). The program/package name a COMMAND probe targets is
|
||||
* carried as a SINGLE argv element, never concatenated into a shell
|
||||
* string — todo 12/16 shell-quote it at emission (st_sh_quote) so
|
||||
* injection is structurally impossible here.
|
||||
* - `c_source` is a complete, self-contained C23 translation unit (or a
|
||||
* standalone declaration for compile-only probes) that compiles clean
|
||||
* under `-std=c23 -Wall -Wextra -Wpedantic`.
|
||||
*
|
||||
* PER-KIND SPEC TABLE
|
||||
* -------------------
|
||||
* header "pthread.h" COMPILE `#include <pthread.h>` (angle form for
|
||||
* plain names; `"..."` escaped form for
|
||||
* names with spaces/quotes/metachars)
|
||||
* function "strdup" LINK `extern void strdup(void); ...`
|
||||
* + a call; extra_args = {"-fno-builtin"}
|
||||
* library "pthread" LINK trivial `int main(void){return 0;}`
|
||||
* + extra_args = {"-lpthread"}
|
||||
* type "size_t" COMPILE `_Static_assert(sizeof(size_t) != 0,...)`
|
||||
* sizeof "long" RUN `_Static_assert(...)` sentinel + a
|
||||
* printf of `sizeof(long)` (run path)
|
||||
* program "pkg-config" COMMAND argv {command, -v, pkg-config}
|
||||
* compiler_flag "-fsani..." COMPILE trivial main + extra_args = {flag}
|
||||
* pkg_config "openssl" COMMAND argv {pkg-config, --cflags, --libs, pkg}
|
||||
*
|
||||
* OWNERSHIP
|
||||
* ---------
|
||||
* st_check_probe_build() writes a FULLY HEAP-OWNED probe into a caller
|
||||
* provided struct; st_check_probe_free() releases it (NULL is a safe no-op).
|
||||
* On error `out` is left zeroed and an owned st_error is returned.
|
||||
*
|
||||
* ERRORS
|
||||
* ------
|
||||
* Failures return an owned st_error of category ST_ERR_KDL_SCHEMA whose span
|
||||
* is heap-allocated IN THE SAME BLOCK as the error (the parser.c pattern), at
|
||||
* the offending token: a NULL node, a missing/non-string target argument, a
|
||||
* malformed version constraint, a non-identifier function name, or a control
|
||||
* character in an embedded C token. An out-of-range kind is the one span-less
|
||||
* case (defensive; no source position exists). On allocation failure the
|
||||
* error is a plain ST_ERR_INTERNAL with no span (and, as throughout this
|
||||
* codebase, may itself be NULL under catastrophic OOM).
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_DETECT_CHECKS_H
|
||||
#define ST_DETECT_CHECKS_H
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "detect/check_registry.h" /* enum st_check_kind */
|
||||
|
||||
struct st_error;
|
||||
struct st_kdl_node;
|
||||
|
||||
/* The probe MODE: what todo 12 must do with the snippet to test the check.
|
||||
* PREPROCESS is reserved for a future header fast-path; none of the 8 v1
|
||||
* kinds emit it (header uses COMPILE). */
|
||||
enum st_check_probe_mode {
|
||||
ST_PROBE_PREPROCESS = 0, /* -E the snippet (unused by v1 kinds) */
|
||||
ST_PROBE_COMPILE, /* compile the snippet (-c) */
|
||||
ST_PROBE_LINK, /* compile + link the snippet */
|
||||
ST_PROBE_RUN, /* compile + link + run, capture stdout */
|
||||
ST_PROBE_COMMAND, /* exec the `command` argv */
|
||||
};
|
||||
|
||||
/* A built probe spec (see the header comment for the per-kind table). */
|
||||
struct st_check_probe {
|
||||
enum st_check_probe_mode mode;
|
||||
char *c_source; /* heap C snippet; NULL for COMMAND probes */
|
||||
char **command; /* heap argv (NULL-terminated); NULL unless
|
||||
mode == ST_PROBE_COMMAND */
|
||||
char **extra_args; /* heap argv (NULL-terminated) of extra
|
||||
compile/link flags; NULL when empty */
|
||||
char *version_constraint; /* heap raw "<op><version>"; NULL when absent */
|
||||
};
|
||||
|
||||
/* Build the probe spec for `kind` from a schema-validated CHECK node. The
|
||||
* node's FIRST positional argument is the target; for library/pkg_config the
|
||||
* optional `version` keyword + constraint pair is read too. On success
|
||||
* returns NULL and fills `*out` (a fully heap-owned probe). On failure
|
||||
* returns an owned error (see ERRORS) and leaves `*out` zeroed. */
|
||||
struct st_error *st_check_probe_build(enum st_check_kind kind,
|
||||
const struct st_kdl_node *node,
|
||||
struct st_check_probe *out);
|
||||
|
||||
/* Release a probe spec produced by st_check_probe_build. NULL is a safe
|
||||
* no-op; the probe is zeroed after freeing. */
|
||||
void st_check_probe_free(struct st_check_probe *p);
|
||||
|
||||
/* Stable display name for a probe mode, e.g. "compile". Out-of-range modes
|
||||
* yield "?" rather than indexing out of range. */
|
||||
const char *st_check_probe_mode_name(enum st_check_probe_mode mode);
|
||||
|
||||
#endif /* ST_DETECT_CHECKS_H */
|
||||
@@ -0,0 +1,482 @@
|
||||
/* LINK: ../../src/detect/checks.c ../../src/detect/check_registry.c ../../src/kdl/schema.c ../../src/kdl/parser.c ../../src/kdl/lexer.c ../../src/kdl/value.c ../../src/error.c ../../src/span.c */
|
||||
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L /* popen/pclose, sys/wait.h */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* tests/unit/test_checks.c
|
||||
*
|
||||
* Unit tests for the check registry implementation (todo 11): for each of
|
||||
* the 8 check kinds, src/detect/checks.c builds the exact PROBE SPEC
|
||||
* (mode + C snippet + command argv + extra args) that todo 12 turns into
|
||||
* configure-time compile/link/run/command invocations. This module builds
|
||||
* specs ONLY - it never executes a probe and never emits shell text.
|
||||
*
|
||||
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it lists
|
||||
* the extra .c sources compiled into this test binary (paths relative to
|
||||
* tests/unit/). checks.c needs check_registry.c (the shape table) + value.c
|
||||
* (arg extraction); parser.c + lexer.c build documents end-to-end from
|
||||
* source text; error.c + span.c via error.c's st_span_print; schema.c
|
||||
* linked per the task's file list.
|
||||
*/
|
||||
#include "munit.h"
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
#include "detect/checks.h"
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* ---- helpers ---------------------------------------------------------- */
|
||||
|
||||
/* Parse a single-node document and return its one node (parse success is
|
||||
* asserted). The caller frees the returned document. */
|
||||
static struct st_kdl_node *
|
||||
parse_one_node(const char *src, struct st_kdl_document **doc_out)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
|
||||
*doc_out = st_kdl_parse(src, "t.kdl", &err);
|
||||
munit_assert_null(err);
|
||||
munit_assert_not_null(*doc_out);
|
||||
return (*doc_out)->nodes;
|
||||
}
|
||||
|
||||
/* Parse `src` as a single check node, resolve its kind, and build its
|
||||
* probe spec into `*out`. Asserts parse + kind resolution + build success
|
||||
* and frees the document before returning (the probe is fully heap-owned
|
||||
* and independent of the document). Returns the resolved kind. */
|
||||
static enum st_check_kind
|
||||
build_from(const char *src, struct st_check_probe *out)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
enum st_check_kind kind;
|
||||
|
||||
node = parse_one_node(src, &doc);
|
||||
kind = st_check_kind_from_node(node, &err);
|
||||
munit_assert_null(err);
|
||||
munit_assert_int(kind, !=, ST_CHECK_KIND_COUNT);
|
||||
munit_assert_null(st_check_probe_build(kind, node, out));
|
||||
st_kdl_document_free(doc);
|
||||
return kind;
|
||||
}
|
||||
|
||||
/* Compile `src` through a real cc on stdin (compile-only), with an
|
||||
* optional leading flag ("" for none, or e.g. "-fno-builtin "). Returns
|
||||
* the compiler's exit status, or -1 on any setup failure. The cc command
|
||||
* is a fixed literal; `src` reaches cc via stdin, never via the shell. */
|
||||
static int
|
||||
cc_compile_stdin(const char *src, const char *flag)
|
||||
{
|
||||
char cmd[256];
|
||||
FILE *p;
|
||||
int n;
|
||||
int rc;
|
||||
size_t len = strlen(src);
|
||||
|
||||
n = snprintf(cmd, sizeof cmd,
|
||||
"cc -std=c23 -Wall -Wextra -Wpedantic %s-c -x c - -o /dev/null",
|
||||
flag != NULL ? flag : "");
|
||||
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||
return -1;
|
||||
}
|
||||
p = popen(cmd, "w");
|
||||
if (p == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (fwrite(src, 1, len, p) != len) {
|
||||
(void)pclose(p);
|
||||
return -1;
|
||||
}
|
||||
rc = pclose(p);
|
||||
if (rc == -1 || !WIFEXITED(rc)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(rc);
|
||||
}
|
||||
|
||||
/* ---- tests ------------------------------------------------------------ */
|
||||
|
||||
/* (a) header "pthread.h" -> COMPILE probe whose source includes the
|
||||
* header with the angle-bracket form. */
|
||||
static MunitResult
|
||||
test_header(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("header \"pthread.h\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_HEADER);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "#include <pthread.h>") != NULL);
|
||||
munit_assert_null(p.command);
|
||||
munit_assert_null(p.extra_args);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (b) function "strdup" -> LINK probe whose source declares and calls the
|
||||
* symbol; -fno-builtin so GCC's builtin machinery can't reject/mis-type
|
||||
* the declaration. */
|
||||
static MunitResult
|
||||
test_function(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("function \"strdup\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_FUNCTION);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "extern void strdup(void)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "strdup();") != NULL);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-fno-builtin");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (c) library "pthread" -> LINK probe with extra_arg "-lpthread". */
|
||||
static MunitResult
|
||||
test_library(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("library \"pthread\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_LIBRARY);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-lpthread");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (d) type "size_t" -> COMPILE probe using sizeof(size_t) in a static
|
||||
* assertion. */
|
||||
static MunitResult
|
||||
test_type(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("type \"size_t\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_TYPE);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "sizeof(size_t)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (e) sizeof "long" -> RUN-capable probe: a _Static_assert sentinel plus a
|
||||
* printf run path. */
|
||||
static MunitResult
|
||||
test_sizeof(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("sizeof \"long\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_SIZEOF);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_RUN);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_true(strstr(p.c_source, "sizeof(long)") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "_Static_assert") != NULL);
|
||||
munit_assert_true(strstr(p.c_source, "printf") != NULL);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (f) program "pkg-config" -> COMMAND argv {command, -v, pkg-config}. */
|
||||
static MunitResult
|
||||
test_program(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("program \"pkg-config\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_PROGRAM);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_null(p.c_source);
|
||||
munit_assert_not_null(p.command);
|
||||
munit_assert_string_equal(p.command[0], "command");
|
||||
munit_assert_string_equal(p.command[1], "-v");
|
||||
munit_assert_string_equal(p.command[2], "pkg-config");
|
||||
munit_assert_null(p.command[3]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (g) compiler_flag "-fsanitize=address" -> COMPILE probe with the flag as
|
||||
* an extra_arg. */
|
||||
static MunitResult
|
||||
test_compiler_flag(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("compiler_flag \"-fsanitize=address\"",
|
||||
&p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_COMPILER_FLAG);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_not_null(p.c_source);
|
||||
munit_assert_not_null(p.extra_args);
|
||||
munit_assert_string_equal(p.extra_args[0], "-fsanitize=address");
|
||||
munit_assert_null(p.extra_args[1]);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (h) pkg_config "openssl" -> COMMAND argv {pkg-config, --cflags, --libs,
|
||||
* openssl}. */
|
||||
static MunitResult
|
||||
test_pkg_config(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
enum st_check_kind kind = build_from("pkg_config \"openssl\"", &p);
|
||||
|
||||
munit_assert_int(kind, ==, ST_CHECK_PKG_CONFIG);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_null(p.c_source);
|
||||
munit_assert_not_null(p.command);
|
||||
munit_assert_string_equal(p.command[0], "pkg-config");
|
||||
munit_assert_string_equal(p.command[1], "--cflags");
|
||||
munit_assert_string_equal(p.command[2], "--libs");
|
||||
munit_assert_string_equal(p.command[3], "openssl");
|
||||
munit_assert_null(p.command[4]);
|
||||
munit_assert_null(p.version_constraint);
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* The optional `version` constraint: library records it (and still links
|
||||
* with plain -l); pkg_config maps >= to --atleast-version=. */
|
||||
static MunitResult
|
||||
test_version_constraints(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe lib = { 0 };
|
||||
struct st_check_probe pk = { 0 };
|
||||
struct st_check_probe pkeq = { 0 };
|
||||
|
||||
(void)build_from("library \"pthread\" version \">=2.0\"", &lib);
|
||||
munit_assert_int(lib.mode, ==, ST_PROBE_LINK);
|
||||
munit_assert_string_equal(lib.extra_args[0], "-lpthread");
|
||||
munit_assert_string_equal(lib.version_constraint, ">=2.0");
|
||||
|
||||
(void)build_from("pkg_config \"openssl\" version \">=1.1\"", &pk);
|
||||
munit_assert_int(pk.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_string_equal(pk.command[0], "pkg-config");
|
||||
munit_assert_string_equal(pk.command[1], "--atleast-version=1.1");
|
||||
munit_assert_string_equal(pk.command[2], "--cflags");
|
||||
munit_assert_string_equal(pk.command[3], "--libs");
|
||||
munit_assert_string_equal(pk.command[4], "openssl");
|
||||
munit_assert_null(pk.command[5]);
|
||||
munit_assert_string_equal(pk.version_constraint, ">=1.1");
|
||||
|
||||
(void)build_from("pkg_config \"openssl\" version \"=1.2\"", &pkeq);
|
||||
munit_assert_string_equal(pkeq.command[1], "--exact-version=1.2");
|
||||
|
||||
st_check_probe_free(&lib);
|
||||
st_check_probe_free(&pk);
|
||||
st_check_probe_free(&pkeq);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* A malformed version constraint (no operator) is rejected with a schema
|
||||
* error naming the check. */
|
||||
static MunitResult
|
||||
test_malformed_constraint(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
struct st_check_probe p = { 0 };
|
||||
|
||||
node = parse_one_node("library \"l\" version \"7.0\"", &doc);
|
||||
err = st_check_probe_build(ST_CHECK_LIBRARY, node, &p);
|
||||
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), "malformed version "
|
||||
"constraint") != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (j) unknown kind / NULL node / missing argument all error cleanly. */
|
||||
static MunitResult
|
||||
test_bad_input(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_error *err = NULL;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct st_kdl_document *doc = NULL;
|
||||
struct st_kdl_node *node;
|
||||
|
||||
err = st_check_probe_build(ST_CHECK_KIND_COUNT, NULL, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_check_probe_build(ST_CHECK_HEADER, NULL, &p);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||
st_error_free(err);
|
||||
|
||||
node = parse_one_node("header", &doc);
|
||||
err = st_check_probe_build(ST_CHECK_HEADER, node, &p);
|
||||
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), "requires an argument")
|
||||
!= NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* (i) COMPILE-PROOF: three generated snippets actually compile under
|
||||
* `cc -std=c23 -Wall -Wextra -Wpedantic -c -x c -` (exit 0); the function
|
||||
* snippet additionally compiles clean with its own -fno-builtin arg. */
|
||||
static MunitResult
|
||||
test_compile_proof(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe hdr = { 0 };
|
||||
struct st_check_probe lib = { 0 };
|
||||
struct st_check_probe sz = { 0 };
|
||||
struct st_check_probe fn = { 0 };
|
||||
|
||||
(void)build_from("header \"pthread.h\"", &hdr);
|
||||
(void)build_from("library \"pthread\"", &lib);
|
||||
(void)build_from("sizeof \"long\"", &sz);
|
||||
(void)build_from("function \"strdup\"", &fn);
|
||||
|
||||
munit_assert_int(cc_compile_stdin(hdr.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(lib.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(sz.c_source, ""), ==, 0);
|
||||
munit_assert_int(cc_compile_stdin(fn.c_source, "-fno-builtin "), ==, 0);
|
||||
|
||||
st_check_probe_free(&hdr);
|
||||
st_check_probe_free(&lib);
|
||||
st_check_probe_free(&sz);
|
||||
st_check_probe_free(&fn);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* Adversarial: a header name with metacharacters yields a single-line
|
||||
* quoted include (no injection, valid C); a non-identifier function name
|
||||
* errors; a COMMAND probe carries its program name as ONE argv element. */
|
||||
static MunitResult
|
||||
test_adversarial(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct st_error *err = NULL;
|
||||
struct st_kdl_document *doc = NULL;
|
||||
size_t len;
|
||||
|
||||
(void)build_from("header \"\\\"; rm -rf /\"", &p);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
munit_assert_true(strncmp(p.c_source, "#include \"", 10) == 0);
|
||||
munit_assert_true(strstr(p.c_source, "rm -rf /") != NULL);
|
||||
munit_assert_null(strchr(p.c_source, '<'));
|
||||
len = strlen(p.c_source);
|
||||
munit_assert_true(len >= 1);
|
||||
munit_assert_true(p.c_source[len - 1] == '\n');
|
||||
munit_assert_true(strchr(p.c_source, '\n') == &p.c_source[len - 1]);
|
||||
st_check_probe_free(&p);
|
||||
|
||||
doc = st_kdl_parse("function \"foo bar\"", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
err = st_check_probe_build(ST_CHECK_FUNCTION, doc->nodes, &p);
|
||||
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), "not a valid C "
|
||||
"identifier") != NULL);
|
||||
st_error_free(err);
|
||||
st_kdl_document_free(doc);
|
||||
|
||||
doc = NULL;
|
||||
err = NULL;
|
||||
doc = st_kdl_parse("program \"pkg-config; rm -rf /\"", "t.kdl", &err);
|
||||
munit_assert_not_null(doc);
|
||||
munit_assert_null(err);
|
||||
munit_assert_null(st_check_probe_build(ST_CHECK_PROGRAM, doc->nodes, &p));
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMMAND);
|
||||
munit_assert_string_equal(p.command[2], "pkg-config; rm -rf /");
|
||||
munit_assert_null(p.command[3]);
|
||||
st_check_probe_free(&p);
|
||||
st_kdl_document_free(doc);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitTest tests[] = {
|
||||
{ "/checks/header", test_header, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/function", test_function, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/library", test_library, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/type", test_type, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/sizeof", test_sizeof, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/program", test_program, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/compiler-flag", test_compiler_flag, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/pkg-config", test_pkg_config, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/version-constraints", test_version_constraints, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/malformed-constraint", test_malformed_constraint, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/bad-input", test_bad_input, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ "/checks/compile-proof", test_compile_proof, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/checks/adversarial", test_adversarial, NULL, NULL,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
};
|
||||
|
||||
static const MunitSuite suite = {
|
||||
"/checks", 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