Template
feat(detect): add probe code generator with result cache
This commit is contained in:
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* probe.c - configure-time probe snippet generator (todo 12).
|
||||
*
|
||||
* Emits POSIX-sh that probes a check AT CONFIGURE TIME, inside the
|
||||
* generated ./configure. This module is a pure code generator: it never
|
||||
* runs a compiler, never writes probe sources, never executes anything. All
|
||||
* context (CC, cross_compiling, compiler id, cache/log/tmp paths) is shell
|
||||
* variables established by the configure preamble (todo 16); see probe.h
|
||||
* for the full contract, the cache-key scheme, and the emitted shape.
|
||||
*
|
||||
* The two load-bearing safety decisions:
|
||||
* - The C source is embedded with `printf '%s' <st_sh_quote(c_source)> >
|
||||
* "$st_probe_src"` — a single-quoted literal (not a here-document), so
|
||||
* arbitrary bytes incl. newlines and quotes in the C source are inert.
|
||||
* st_sh_quote (todo 15) is the ONLY safe embedder; everything else here
|
||||
* is hand-written fixed shell text or identifier-validated names.
|
||||
* - checkname is validated as a POSIX identifier before interpolation,
|
||||
* because it lands in variable-NAME positions (have_<name>,
|
||||
* ac_cv_<name>_$st_cc_id, a temp filename) that cannot be quoted.
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#include "detect/probe.h"
|
||||
|
||||
#include "error.h"
|
||||
#include "gen/sh_emit.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ---- small helpers ----------------------------------------------------- */
|
||||
|
||||
/* True when `s` is a valid POSIX shell identifier: letter/underscore, then
|
||||
* letters/digits/underscores, non-empty. */
|
||||
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;
|
||||
}
|
||||
|
||||
/* Write each argv element as a single-quoted literal, space-separated.
|
||||
* Returns 0 on success. Returns -1 with *err set (ST_ERR_INTERNAL) when
|
||||
* quoting fails (NUL byte / OOM); I/O errors are detected later via
|
||||
* ferror(out) and are NOT reported here. */
|
||||
static int
|
||||
emit_argv_quoted(FILE *out, char *const *argv, struct st_error **err)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
for (i = 0; argv[i] != NULL; i++) {
|
||||
char *q = st_sh_quote_ex(argv[i], err);
|
||||
|
||||
if (q == NULL) {
|
||||
if (err != NULL && *err == NULL) {
|
||||
*err = st_error_internal("out of memory quoting probe argv");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (i > 0) {
|
||||
(void)fputc(' ', out);
|
||||
}
|
||||
(void)fputs(q, out);
|
||||
free(q);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Emit a COMMAND probe's argv as a shell command. All elements except the
|
||||
* LAST are hardcoded literals owned by checks.c (the program name "command"
|
||||
* / "pkg-config" plus fixed flags like "-v"/"--cflags"/"--libs"), so they
|
||||
* are written verbatim; the LAST element is the user-supplied target
|
||||
* (program/package name) and is single-quoted. Returns 0, or -1 with *err
|
||||
* set when quoting fails; I/O errors are caught via ferror. */
|
||||
static int
|
||||
emit_command_argv(FILE *out, char *const *argv, struct st_error **err)
|
||||
{
|
||||
size_t n = 0;
|
||||
size_t i;
|
||||
|
||||
while (argv[n] != NULL) {
|
||||
n++;
|
||||
}
|
||||
for (i = 0; i < n; i++) {
|
||||
if (i > 0) {
|
||||
(void)fputc(' ', out);
|
||||
}
|
||||
if (i + 1 == n) {
|
||||
char *q = st_sh_quote_ex(argv[i], err);
|
||||
|
||||
if (q == NULL) {
|
||||
if (err != NULL && *err == NULL) {
|
||||
*err = st_error_internal("out of memory quoting probe "
|
||||
"argv");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
(void)fputs(q, out);
|
||||
free(q);
|
||||
} else {
|
||||
(void)fputs(argv[i], out);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- the cache boilerplate -------------------------------------------- */
|
||||
|
||||
/* Emitted verbatim (POSIX-sh only). Uses st_sh_emit_str, so no format
|
||||
* processing — the `%` in `printf '%s'` and `${var#*=}` stays literal. */
|
||||
static const char CACHE_FUNCS[] =
|
||||
"# --- configure-time probe cache (config.cache) ----------------------\n"
|
||||
"# Emitted by st_probe_emit_cache_functions (todo 12). The probe\n"
|
||||
"# snippets read results via st_cache_get and record them via\n"
|
||||
"# st_cache_set. Cache key: ac_cv_<checkname>_<compiler-id>.\n"
|
||||
"\n"
|
||||
": \"${CC:=cc}\"\n"
|
||||
": \"${CFLAGS:=}\"\n"
|
||||
": \"${st_cc_id:=unknown}\"\n"
|
||||
": \"${cross_compiling:=}\"\n"
|
||||
": \"${config_cache:=./config.cache}\"\n"
|
||||
": \"${config_log:=./config.log}\"\n"
|
||||
": \"${st_tmpdir:=${TMPDIR:-/tmp}}\"\n"
|
||||
"\n"
|
||||
"# st_cache_get KEY: print the cached value (yes/no) and return 0, or\n"
|
||||
"# return 1 when config.cache is absent or KEY is not recorded. The\n"
|
||||
"# key is matched with grep -F (literal, no regex) anchored by its\n"
|
||||
"# trailing '='; keys and values never contain '='.\n"
|
||||
"st_cache_get() {\n"
|
||||
" st_cv_line=\n"
|
||||
" [ -f \"$config_cache\" ] || return 1\n"
|
||||
" st_cv_line=$(grep -F -- \"$1=\" \"$config_cache\" 2>/dev/null | tail -n 1)\n"
|
||||
" [ -n \"$st_cv_line\" ] || return 1\n"
|
||||
" printf '%s\\n' \"${st_cv_line#*=}\"\n"
|
||||
" return 0\n"
|
||||
"}\n"
|
||||
"\n"
|
||||
"# st_cache_set KEY VALUE: record VALUE (yes/no) under KEY, replacing\n"
|
||||
"# any earlier line for the same key. Creates config.cache when absent.\n"
|
||||
"st_cache_set() {\n"
|
||||
" st_cv_tmp=\"$config_cache.tmp.$$\"\n"
|
||||
" if [ -f \"$config_cache\" ]; then\n"
|
||||
" grep -v -F -- \"$1=\" \"$config_cache\" > \"$st_cv_tmp\" 2>/dev/null\n"
|
||||
" mv -f \"$st_cv_tmp\" \"$config_cache\"\n"
|
||||
" fi\n"
|
||||
" printf '%s=%s\\n' \"$1\" \"$2\" >> \"$config_cache\"\n"
|
||||
" rm -f \"$st_cv_tmp\"\n"
|
||||
"}\n"
|
||||
"\n";
|
||||
|
||||
struct st_error *
|
||||
st_probe_emit_cache_functions(FILE *out)
|
||||
{
|
||||
if (out == NULL) {
|
||||
return st_error_usage("st_probe_emit_cache_functions: NULL stream");
|
||||
}
|
||||
if (st_sh_emit_str(out, CACHE_FUNCS) < 0) {
|
||||
return st_error_io("I/O error emitting cache boilerplate");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- the per-check snippet -------------------------------------------- */
|
||||
|
||||
struct st_error *
|
||||
st_probe_emit_snippet(const char *checkname, const struct st_check_probe *probe,
|
||||
FILE *out)
|
||||
{
|
||||
struct st_error *err = NULL;
|
||||
const char *mode_name;
|
||||
char *qsrc = NULL;
|
||||
bool is_command;
|
||||
bool is_run;
|
||||
|
||||
if (out == NULL || checkname == NULL || probe == NULL) {
|
||||
return st_error_usage("st_probe_emit_snippet: NULL argument");
|
||||
}
|
||||
if (!valid_ident(checkname)) {
|
||||
return st_error_usage("st_probe_emit_snippet: checkname must be a "
|
||||
"POSIX shell identifier");
|
||||
}
|
||||
if (probe->mode < ST_PROBE_PREPROCESS || probe->mode > ST_PROBE_COMMAND) {
|
||||
return st_error_usage("st_probe_emit_snippet: unknown probe mode");
|
||||
}
|
||||
mode_name = st_check_probe_mode_name(probe->mode);
|
||||
is_command = (probe->mode == ST_PROBE_COMMAND);
|
||||
is_run = (probe->mode == ST_PROBE_RUN);
|
||||
|
||||
if (is_command) {
|
||||
if (probe->command == NULL || probe->command[0] == NULL) {
|
||||
return st_error_usage("st_probe_emit_snippet: COMMAND probe has "
|
||||
"no command argv");
|
||||
}
|
||||
} else {
|
||||
if (probe->c_source == NULL) {
|
||||
return st_error_usage("st_probe_emit_snippet: probe has no C "
|
||||
"source");
|
||||
}
|
||||
qsrc = st_sh_quote_ex(probe->c_source, &err);
|
||||
if (err != NULL) {
|
||||
return err;
|
||||
}
|
||||
if (qsrc == NULL) {
|
||||
return st_error_internal("st_probe_emit_snippet: out of memory "
|
||||
"quoting C source");
|
||||
}
|
||||
}
|
||||
|
||||
(void)fprintf(out, "# probe: %s check '%s'\n", mode_name, checkname);
|
||||
(void)fprintf(out, "st_cv_key=ac_cv_%s_$st_cc_id\n", checkname);
|
||||
|
||||
if (is_run) {
|
||||
/* cross-compile guard FIRST: skip, warn, mark, default "no", and do
|
||||
* NOT read or write the cache (a cross default must never poison a
|
||||
* native build's cache entry). */
|
||||
(void)fprintf(out,
|
||||
"if [ \"$cross_compiling\" = \"yes\" ]; then\n"
|
||||
" st_cross_%s=yes\n"
|
||||
" have_%s=no\n"
|
||||
" echo \"warning: skipping runtime check '%s' while "
|
||||
"cross-compiling\" >&2\n"
|
||||
"elif st_cv_val=$(st_cache_get \"$st_cv_key\"); then\n"
|
||||
" have_%s=$st_cv_val\n"
|
||||
"else\n"
|
||||
" have_%s=no\n",
|
||||
checkname, checkname, checkname, checkname, checkname);
|
||||
} else {
|
||||
(void)fprintf(out,
|
||||
"if st_cv_val=$(st_cache_get \"$st_cv_key\"); then\n"
|
||||
" have_%s=$st_cv_val\n"
|
||||
"else\n"
|
||||
" have_%s=no\n",
|
||||
checkname, checkname);
|
||||
}
|
||||
|
||||
/* ---- probe body -------------------------------------------------- */
|
||||
if (is_command) {
|
||||
(void)fputs(" if ", out);
|
||||
if (emit_command_argv(out, probe->command, &err) < 0) {
|
||||
free(qsrc);
|
||||
return err;
|
||||
}
|
||||
(void)fprintf(out,
|
||||
" >>\"$config_log\" 2>&1; then\n"
|
||||
" have_%s=yes\n"
|
||||
" fi\n",
|
||||
checkname);
|
||||
} else {
|
||||
(void)fprintf(out,
|
||||
" st_probe_src=\"$st_tmpdir/st_probe_%s.$$.c\"\n"
|
||||
" printf '%%s' %s > \"$st_probe_src\"\n",
|
||||
checkname, qsrc);
|
||||
switch (probe->mode) {
|
||||
case ST_PROBE_PREPROCESS:
|
||||
(void)fputs(" if $CC $CFLAGS", out);
|
||||
if (probe->extra_args != NULL) {
|
||||
(void)fputc(' ', out);
|
||||
if (emit_argv_quoted(out, probe->extra_args, &err) < 0) {
|
||||
free(qsrc);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
(void)fprintf(out,
|
||||
" -E \"$st_probe_src\" >>\"$config_log\" 2>&1; then\n"
|
||||
" have_%s=yes\n"
|
||||
" fi\n"
|
||||
" rm -f \"$st_probe_src\"\n",
|
||||
checkname);
|
||||
break;
|
||||
case ST_PROBE_COMPILE:
|
||||
(void)fputs(" if $CC $CFLAGS", out);
|
||||
if (probe->extra_args != NULL) {
|
||||
(void)fputc(' ', out);
|
||||
if (emit_argv_quoted(out, probe->extra_args, &err) < 0) {
|
||||
free(qsrc);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
(void)fprintf(out,
|
||||
" -c \"$st_probe_src\" -o \"$st_probe_src.o\" "
|
||||
">>\"$config_log\" 2>&1; then\n"
|
||||
" have_%s=yes\n"
|
||||
" fi\n"
|
||||
" rm -f \"$st_probe_src\" \"$st_probe_src.o\"\n",
|
||||
checkname);
|
||||
break;
|
||||
case ST_PROBE_LINK:
|
||||
(void)fputs(" if $CC $CFLAGS", out);
|
||||
if (probe->extra_args != NULL) {
|
||||
(void)fputc(' ', out);
|
||||
if (emit_argv_quoted(out, probe->extra_args, &err) < 0) {
|
||||
free(qsrc);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
(void)fprintf(out,
|
||||
" \"$st_probe_src\" -o \"$st_probe_src.bin\" "
|
||||
">>\"$config_log\" 2>&1; then\n"
|
||||
" have_%s=yes\n"
|
||||
" fi\n"
|
||||
" rm -f \"$st_probe_src\" \"$st_probe_src.bin\"\n",
|
||||
checkname);
|
||||
break;
|
||||
case ST_PROBE_RUN:
|
||||
(void)fputs(" if $CC $CFLAGS", out);
|
||||
if (probe->extra_args != NULL) {
|
||||
(void)fputc(' ', out);
|
||||
if (emit_argv_quoted(out, probe->extra_args, &err) < 0) {
|
||||
free(qsrc);
|
||||
return err;
|
||||
}
|
||||
}
|
||||
(void)fprintf(out,
|
||||
" \"$st_probe_src\" -o \"$st_probe_src.bin\" "
|
||||
">>\"$config_log\" 2>&1; then\n"
|
||||
" if \"$st_probe_src.bin\" >>\"$config_log\" 2>&1; "
|
||||
"then\n"
|
||||
" have_%s=yes\n"
|
||||
" fi\n"
|
||||
" fi\n"
|
||||
" rm -f \"$st_probe_src\" \"$st_probe_src.bin\"\n",
|
||||
checkname);
|
||||
break;
|
||||
default:
|
||||
free(qsrc);
|
||||
return st_error_usage("st_probe_emit_snippet: unknown probe mode");
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- cache write + close ---------------------------------------- */
|
||||
(void)fprintf(out, " st_cache_set \"$st_cv_key\" \"$have_%s\"\n"
|
||||
"fi\n", checkname);
|
||||
|
||||
free(qsrc);
|
||||
if (ferror(out)) {
|
||||
return st_error_io("I/O error emitting probe snippet");
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* ---- a feature's list of checks --------------------------------------- */
|
||||
|
||||
struct st_error *
|
||||
st_probe_emit_all(const struct st_probe_entry *entries, size_t count,
|
||||
FILE *out)
|
||||
{
|
||||
size_t i;
|
||||
|
||||
if (count > 0 && entries == NULL) {
|
||||
return st_error_usage("st_probe_emit_all: NULL entries");
|
||||
}
|
||||
for (i = 0; i < count; i++) {
|
||||
struct st_error *err;
|
||||
|
||||
if (i > 0 && fputc('\n', out) == EOF) {
|
||||
return st_error_io("I/O error emitting probe snippet");
|
||||
}
|
||||
err = st_probe_emit_snippet(entries[i].name, entries[i].probe, out);
|
||||
if (err != NULL) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* probe.h - configure-time probe snippet generator (todo 12).
|
||||
*
|
||||
* MODEL (the thing to internalize before reading anything else): this
|
||||
* module NEVER executes a probe. It is a pure CODE GENERATOR: given a
|
||||
* check's probe spec (struct st_check_probe, todo 11), it EMITS a POSIX-sh
|
||||
* snippet that, when it runs inside the generated ./configure at configure
|
||||
* time, writes the probe's C source to a temp file, drives the detected
|
||||
* toolchain ($CC -c / -o / run), captures the exit status, and records
|
||||
* have_<checkname>=yes|no. The generator is therefore STATELESS — it takes
|
||||
* no context object, because all probe context (compiler, flags,
|
||||
* cross-compile flag, compiler id, cache/log/tmp paths) lives in the
|
||||
* configure-time shell environment, not in the generator process.
|
||||
*
|
||||
* CONFIG.SHELL VARIABLE CONTRACT (todo 16's preamble MUST establish these;
|
||||
* the boilerplate below emits conservative defaults):
|
||||
*
|
||||
* CC the C compiler command (word-split; default "cc")
|
||||
* CFLAGS extra compiler flags (default empty)
|
||||
* cross_compiling "yes" when cross-compiling (--host != --build), else
|
||||
* unset/empty. RUN probes test `[ "$cross_compiling" =
|
||||
* "yes" ]` and skip execution when set.
|
||||
* st_cc_id compiler identity string used in cache keys (default
|
||||
* "unknown"). Must be a shell-safe token free of `=` and
|
||||
* regex metacharacters (compiler ids are, e.g.
|
||||
* "gcc"/"clang"); it is interpolated into a cache key
|
||||
* that st_cache_get matches with grep -F.
|
||||
* config_cache path to the runtime result cache (default ./config.cache)
|
||||
* config_log path probe stdout/stderr is appended to (default ./config.log)
|
||||
* st_tmpdir directory for temp probe files (default ${TMPDIR:-/tmp})
|
||||
*
|
||||
* CACHE KEY SCHEME
|
||||
* ----------------
|
||||
* One config.cache line per check per compiler, value `yes` or `no`:
|
||||
*
|
||||
* ac_cv_<checkname>_<compiler-id>=yes|no
|
||||
*
|
||||
* `<checkname>` is the unique name passed to st_probe_emit_snippet (see
|
||||
* below); `<compiler-id>` is the configure-time $st_cc_id. This is the
|
||||
* ac_cv_func_strdup pattern generalized: the key is derived from the check
|
||||
* AND the compiler so that re-running configure under a different compiler
|
||||
* re-probes instead of trusting a foreign cache entry.
|
||||
*
|
||||
* EMITTED SNIPPET SHAPE (per check)
|
||||
* ---------------------------------
|
||||
* # probe: <mode> check '<checkname>'
|
||||
* st_cv_key=ac_cv_<checkname>_$st_cc_id
|
||||
* if st_cv_val=$(st_cache_get "$st_cv_key"); then
|
||||
* have_<checkname>=$st_cv_val
|
||||
* else
|
||||
* have_<checkname>=no
|
||||
* <mode-specific probe body, sets have_<checkname>=yes on success>
|
||||
* st_cache_set "$st_cv_key" "$have_<checkname>"
|
||||
* fi
|
||||
*
|
||||
* RUN probes wrap that in a cross-compile guard FIRST:
|
||||
* if [ "$cross_compiling" = "yes" ]; then
|
||||
* st_cross_<checkname>=yes
|
||||
* have_<checkname>=no
|
||||
* echo "warning: skipping runtime check '<checkname>' while
|
||||
* cross-compiling" >&2
|
||||
* elif st_cv_val=$(st_cache_get "$st_cv_key"); then
|
||||
* have_<checkname>=$st_cv_val
|
||||
* else
|
||||
* ...compile + run...
|
||||
* fi
|
||||
* The cross-skipped result is NOT written to config.cache (a cross default
|
||||
* must never poison a native build's cache entry).
|
||||
*
|
||||
* Mode bodies (all probe stdout+stderr appended to "$config_log" via
|
||||
* >>"$config_log" 2>&1; temp files removed with rm -f; the .c source is
|
||||
* written with `printf '%s' <st_sh_quote(c_source)> > "$st_probe_src"` —
|
||||
* single-quote wrapping, NOT a here-document, so a C source containing
|
||||
* arbitrary bytes incl. newlines and the delimiter-like text stays inert):
|
||||
* COMPILE $CC $CFLAGS <extra> -c "$src" -o "$src.o"
|
||||
* LINK $CC $CFLAGS <extra> "$src" -o "$src.bin"
|
||||
* RUN compile+link, then "$src.bin"
|
||||
* COMMAND <command argv>: every element except the LAST is a hardcoded
|
||||
* literal (program name "command"/"pkg-config" + fixed flags)
|
||||
* and is emitted verbatim; the LAST element (the user-supplied
|
||||
* target) is single-quoted. e.g. `command -v 'pkg-config'`,
|
||||
* `pkg-config --cflags --libs 'openssl'`. (A future kind
|
||||
* supplying a user-controlled non-final element must quote it.)
|
||||
* PREPROCESS $CC $CFLAGS <extra> -E "$src" (reserved; unused by v1)
|
||||
*
|
||||
* CHECKNAME CONTRACT
|
||||
* ------------------
|
||||
* `checkname` MUST be a valid POSIX shell identifier ([A-Za-z_][A-Za-z0-9_]*).
|
||||
* It is interpolated into the variable names have_<checkname> /
|
||||
* st_cross_<checkname> / ac_cv_<checkname>_$st_cc_id and a temp filename,
|
||||
* none of which can be shell-quoted. st_probe_emit_snippet validates this
|
||||
* and returns ST_ERR_USAGE on violation (mirroring sh_emit.h's documented
|
||||
* trust boundary for `var`, but enforced rather than trusted because the
|
||||
* value reaches a variable-NAME position). The caller chooses a unique
|
||||
* checkname per check: for a single-check feature that is the feature name
|
||||
* (so have_<feature> is set directly); for a multi-check feature the caller
|
||||
* derives distinct names and todo 14/16 ANDs them into have_<feature>.
|
||||
*
|
||||
* VERSION CONSTRAINT
|
||||
* ------------------
|
||||
* probe->version_constraint is NOT consumed by the emitted snippet. For
|
||||
* pkg_config the constraint operator was already folded into the command
|
||||
* argv by checks.c (e.g. --atleast-version=1.1); for library the constraint
|
||||
* is recorded only and todo 16 enforces it (via pkg-config when available).
|
||||
* This field is simply ignored here.
|
||||
*
|
||||
* ERRORS
|
||||
* ------
|
||||
* All functions return NULL on success. Failures return an owned st_error:
|
||||
* ST_ERR_USAGE for NULL arguments, an invalid checkname, an out-of-range
|
||||
* probe mode, a COMMAND probe with no command argv, or a non-COMMAND probe
|
||||
* with no C source; ST_ERR_INTERNAL when st_sh_quote fails (NUL byte / OOM,
|
||||
* propagated) or quoting allocation fails; ST_ERR_IO when writing to the
|
||||
* stream fails (detected via ferror after emission).
|
||||
*
|
||||
* Copyright (c) 2026 huntedbytheirs
|
||||
* SPDX-License-Identifier: BSD-3-Clause
|
||||
*/
|
||||
|
||||
#ifndef ST_DETECT_PROBE_H
|
||||
#define ST_DETECT_PROBE_H
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "detect/checks.h" /* struct st_check_probe, enum st_check_probe_mode */
|
||||
|
||||
struct st_error;
|
||||
|
||||
/* Emit the config.cache boilerplate: the st_cache_get / st_cache_set shell
|
||||
* functions plus the conservative defaults for the contract variables
|
||||
* (CC/CFLAGS/cross_compiling/st_cc_id/config_cache/config_log/st_tmpdir).
|
||||
* This is emitted ONCE per configure script, before any probe snippet. The
|
||||
* two functions are:
|
||||
* st_cache_get KEY -> echoes the cached value (yes/no) and returns 0,
|
||||
* or returns 1 when config.cache is absent or KEY
|
||||
* is not recorded.
|
||||
* st_cache_set KEY VALUE -> records VALUE (yes/no) under KEY, replacing
|
||||
* any prior line for the same key; creates
|
||||
* config.cache when absent.
|
||||
* POSIX-only (no `local`, no arrays, no `==`). */
|
||||
struct st_error *st_probe_emit_cache_functions(FILE *out);
|
||||
|
||||
/* Emit the POSIX-sh snippet that probes ONE check at configure time.
|
||||
* Appends to `out`; sets have_<checkname>=yes|no (always, on every path).
|
||||
* Returns NULL on success. */
|
||||
struct st_error *st_probe_emit_snippet(const char *checkname,
|
||||
const struct st_check_probe *probe,
|
||||
FILE *out);
|
||||
|
||||
/* One entry of a feature's check list: a unique check name paired with its
|
||||
* probe spec. `name` is the have_<name> variable + cache-key base. */
|
||||
struct st_probe_entry {
|
||||
const char *name; /* unique POSIX identifier */
|
||||
const struct st_check_probe *probe; /* borrowed; not freed here */
|
||||
};
|
||||
|
||||
/* Emit a feature's list of checks (each via st_probe_emit_snippet). A blank
|
||||
* line separates checks. Returns NULL on success, or the first error. */
|
||||
struct st_error *st_probe_emit_all(const struct st_probe_entry *entries,
|
||||
size_t count, FILE *out);
|
||||
|
||||
#endif /* ST_DETECT_PROBE_H */
|
||||
@@ -0,0 +1,758 @@
|
||||
/* LINK: ../../src/detect/probe.c ../../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/gen/sh_emit.c ../../src/error.c ../../src/span.c */
|
||||
|
||||
#ifndef _POSIX_C_SOURCE
|
||||
#define _POSIX_C_SOURCE 200809L /* mkdtemp, system/WEXITSTATUS */
|
||||
#endif
|
||||
|
||||
/*
|
||||
* tests/unit/test_probe.c
|
||||
*
|
||||
* Unit tests for the configure-time probe snippet generator (todo 12):
|
||||
* src/detect/probe.c. THE MODEL: probes do NOT run here, at stupidtools
|
||||
* generation time — this module only EMITS POSIX-sh that runs at configure
|
||||
* time inside the generated ./configure. These tests prove that emitted
|
||||
* shell is syntactically valid (sh/bash/zsh -n), genuinely executes (real
|
||||
* `cc` against real headers), short-circuits on config.cache, cross-compile
|
||||
* guards RUN probes, and keeps hostile probe targets inert via st_sh_quote.
|
||||
*
|
||||
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh (extra .c
|
||||
* sources, relative to tests/unit/). probe.c needs sh_emit.c (st_sh_quote)
|
||||
* + error.c + span.c; checks.c/check_registry.c/parser.c/lexer.c/value.c/
|
||||
* schema.c are linked so canonical probes are built END-TO-END from DSL
|
||||
* source (st_check_probe_build), exercising the real checks.c -> probe.c
|
||||
* contract rather than hand-built structs alone.
|
||||
*/
|
||||
#include "munit.h"
|
||||
|
||||
#include "detect/check_registry.h"
|
||||
#include "detect/checks.h"
|
||||
#include "detect/probe.h"
|
||||
#include "error.h"
|
||||
#include "kdl/ast.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.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_probe_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); /* best-effort; mkdtemp names are [A-Za-z0-9_]* */
|
||||
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);
|
||||
}
|
||||
|
||||
/* ---- probe construction ------------------------------------------------- */
|
||||
|
||||
/* Parse `src` as one check node, resolve its kind, build its probe spec
|
||||
* into `*out` (fully heap-owned). Asserts each step succeeds and frees the
|
||||
* document before returning. The caller frees via st_check_probe_free. */
|
||||
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;
|
||||
|
||||
doc = st_kdl_parse(src, "t.kdl", &err);
|
||||
munit_assert_null(err);
|
||||
munit_assert_not_null(doc);
|
||||
node = doc->nodes;
|
||||
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;
|
||||
}
|
||||
|
||||
/* ---- emit helpers ------------------------------------------------------ */
|
||||
|
||||
/* Emit one check's snippet into a temp file, return its bytes (NUL-
|
||||
* terminated). Returns NULL on any failure. The buffer is static (munit
|
||||
* forks per test, so it is never shared across tests). */
|
||||
static const char *
|
||||
emit_snippet_text(const char *checkname, const struct st_check_probe *p)
|
||||
{
|
||||
static char buf[8192];
|
||||
char path[600];
|
||||
FILE *f;
|
||||
size_t n;
|
||||
struct st_error *err;
|
||||
int m;
|
||||
|
||||
m = mkpath(path, sizeof path, "snippet.sh");
|
||||
if (m < 0) {
|
||||
return NULL;
|
||||
}
|
||||
f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
err = st_probe_emit_snippet(checkname, p, f);
|
||||
fclose(f);
|
||||
if (err != NULL) {
|
||||
st_error_free(err);
|
||||
return NULL;
|
||||
}
|
||||
f = fopen(path, "r");
|
||||
if (f == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
n = fread(buf, 1, sizeof buf - 1, f);
|
||||
fclose(f);
|
||||
buf[n] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* The configure-time environment the script's stub preamble plants. */
|
||||
struct env {
|
||||
const char *cc; /* CC value; NULL -> "cc" */
|
||||
const char *cc_id; /* st_cc_id; NULL -> "gcc" */
|
||||
const char *cross; /* "yes" -> cross_compiling=yes; else unset */
|
||||
};
|
||||
|
||||
/* Write a runnable script: shebang, preamble assignments, the cache
|
||||
* boilerplate, ONE snippet, and a dump of have_<name> + st_cross_<name>
|
||||
* into have.out / cross.out under temp_dir. Returns 0 on success. */
|
||||
static int
|
||||
write_script(const char *checkname, const struct st_check_probe *p,
|
||||
const struct env *env)
|
||||
{
|
||||
char path[600];
|
||||
FILE *f;
|
||||
struct st_error *err;
|
||||
int m;
|
||||
|
||||
m = mkpath(path, sizeof path, "run.sh");
|
||||
if (m < 0) {
|
||||
return -1;
|
||||
}
|
||||
f = fopen(path, "w");
|
||||
if (f == NULL) {
|
||||
return -1;
|
||||
}
|
||||
(void)fprintf(f, "#!/bin/sh\n");
|
||||
(void)fprintf(f, "CC='%s'\n",
|
||||
env != NULL && env->cc != NULL ? env->cc : "cc");
|
||||
(void)fprintf(f, "st_cc_id='%s'\n",
|
||||
env != NULL && env->cc_id != NULL ? env->cc_id : "gcc");
|
||||
if (env != NULL && env->cross != NULL && strcmp(env->cross, "yes") == 0) {
|
||||
(void)fprintf(f, "cross_compiling=yes\n");
|
||||
}
|
||||
(void)fprintf(f, "config_cache='%s/config.cache'\n", temp_dir);
|
||||
(void)fprintf(f, "config_log='%s/config.log'\n", temp_dir);
|
||||
(void)fprintf(f, "st_tmpdir='%s'\n", temp_dir);
|
||||
err = st_probe_emit_cache_functions(f);
|
||||
if (err != NULL) {
|
||||
st_error_free(err);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
err = st_probe_emit_snippet(checkname, p, f);
|
||||
if (err != NULL) {
|
||||
st_error_free(err);
|
||||
fclose(f);
|
||||
return -1;
|
||||
}
|
||||
(void)fprintf(f, "printf '%%s\\n' \"$have_%s\" > '%s/have.out'\n",
|
||||
checkname, temp_dir);
|
||||
(void)fprintf(f, "printf '%%s\\n' \"${st_cross_%s:-}\" > '%s/cross.out'\n",
|
||||
checkname, temp_dir);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
/* As run_sh, but capture the script's stderr into `err_path`. */
|
||||
static int
|
||||
run_sh_err(const char *path, const char *err_path)
|
||||
{
|
||||
char cmd[1400];
|
||||
int rc;
|
||||
int n = snprintf(cmd, sizeof cmd, "sh '%s' 2>'%s'", path, err_path);
|
||||
|
||||
if (n < 0 || (size_t)n >= sizeof cmd) {
|
||||
return -1;
|
||||
}
|
||||
rc = system(cmd);
|
||||
if (rc == -1) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(rc);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/* "<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);
|
||||
}
|
||||
|
||||
/* ---- (a) snippet content ---------------------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_header_snippet_content(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
const char *s;
|
||||
|
||||
(void)build_from("header \"unistd.h\"", &p);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_COMPILE);
|
||||
|
||||
s = emit_snippet_text("unistd_h", &p);
|
||||
munit_assert_not_null(s);
|
||||
munit_assert_not_null(strstr(s, "$CC"));
|
||||
munit_assert_not_null(strstr(s, "-c"));
|
||||
munit_assert_not_null(strstr(s, "have_unistd_h"));
|
||||
munit_assert_not_null(strstr(s, "have_unistd_h=yes"));
|
||||
munit_assert_not_null(strstr(s, "have_unistd_h=no"));
|
||||
munit_assert_not_null(strstr(s, "#include <unistd.h>"));
|
||||
munit_assert_not_null(strstr(s, "ac_cv_unistd_h_$st_cc_id"));
|
||||
munit_assert_not_null(strstr(s, "st_cache_get"));
|
||||
munit_assert_not_null(strstr(s, "st_cache_set"));
|
||||
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (b) real execution: yes and no ----------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_header_runs_yes_no(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe yes = { 0 };
|
||||
struct st_check_probe no = { 0 };
|
||||
char run[600];
|
||||
int m;
|
||||
|
||||
(void)build_from("header \"unistd.h\"", &yes);
|
||||
munit_assert_int(write_script("unistd_h", &yes, NULL), ==, 0);
|
||||
m = mkpath(run, sizeof run, "run.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
munit_assert_int(run_sh(run), ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "yes");
|
||||
|
||||
(void)build_from("header \"nope_missing_xyz.h\"", &no);
|
||||
munit_assert_int(write_script("nope", &no, NULL), ==, 0);
|
||||
munit_assert_int(run_sh(run), ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "no");
|
||||
|
||||
st_check_probe_free(&yes);
|
||||
st_check_probe_free(&no);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (c) RUN probe: cross-compile guard ------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_run_cross_guard(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct env cross = { "/bin/false", "gcc", "yes" };
|
||||
const char *s;
|
||||
char run[600];
|
||||
char errp[600];
|
||||
char logpath[600];
|
||||
int m;
|
||||
int rc;
|
||||
|
||||
(void)build_from("sizeof \"long\"", &p);
|
||||
munit_assert_int(p.mode, ==, ST_PROBE_RUN);
|
||||
|
||||
/* static: the emitted snippet carries a cross_compiling guard */
|
||||
s = emit_snippet_text("sz", &p);
|
||||
munit_assert_not_null(s);
|
||||
munit_assert_not_null(strstr(s, "cross_compiling"));
|
||||
|
||||
/* dynamic: cross_compiling=yes + a failing CC -> skip, no compile */
|
||||
munit_assert_int(write_script("sz", &p, &cross), ==, 0);
|
||||
m = mkpath(run, sizeof run, "run.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
m = mkpath(errp, sizeof errp, "cross.err");
|
||||
munit_assert_int(m, >, 0);
|
||||
rc = run_sh_err(run, errp);
|
||||
munit_assert_int(rc, ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "no");
|
||||
munit_assert_string_equal(read_result("cross.out"), "yes");
|
||||
/* the warning landed on stderr */
|
||||
{
|
||||
const char *e = read_result("cross.err");
|
||||
|
||||
munit_assert_true(strstr(e, "cross-compiling") != NULL);
|
||||
}
|
||||
/* no compile was attempted: config.log was never created */
|
||||
m = mkpath(logpath, sizeof logpath, "config.log");
|
||||
munit_assert_int(m, >, 0);
|
||||
munit_assert_int(access(logpath, F_OK), ==, -1);
|
||||
|
||||
/* dynamic: cross_compiling unset -> actually compiles and runs */
|
||||
munit_assert_int(write_script("sz", &p, NULL), ==, 0);
|
||||
munit_assert_int(run_sh(run), ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "yes");
|
||||
munit_assert_string_equal(read_result("cross.out"), "");
|
||||
/* the run output (sizeof value) landed in config.log */
|
||||
munit_assert_int(access(logpath, F_OK), ==, 0);
|
||||
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (d) COMMAND probe: argv quoting ---------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_command_probe_quoting(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe prog = { 0 };
|
||||
struct st_check_probe pk = { 0 };
|
||||
const char *s;
|
||||
|
||||
(void)build_from("program \"pkg-config\"", &prog);
|
||||
s = emit_snippet_text("pgc", &prog);
|
||||
munit_assert_not_null(s);
|
||||
munit_assert_not_null(strstr(s, "command -v 'pkg-config'"));
|
||||
|
||||
(void)build_from("pkg_config \"openssl\"", &pk);
|
||||
s = emit_snippet_text("ossl", &pk);
|
||||
munit_assert_not_null(s);
|
||||
munit_assert_not_null(strstr(s, "pkg-config --cflags --libs 'openssl'"));
|
||||
|
||||
st_check_probe_free(&prog);
|
||||
st_check_probe_free(&pk);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (e) config.cache short-circuit ----------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_cache_short_circuit(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
struct env bad = { "/bin/false", "gcc", NULL };
|
||||
char cache[600];
|
||||
char run[600];
|
||||
char logpath[600];
|
||||
FILE *f;
|
||||
int m;
|
||||
int wr;
|
||||
|
||||
/* pre-seed the cache with a "yes" for a header that does NOT exist,
|
||||
* then point CC at /bin/false: only a cache hit can yield yes. */
|
||||
(void)build_from("header \"nope_missing_xyz.h\"", &p);
|
||||
m = mkpath(cache, sizeof cache, "config.cache");
|
||||
munit_assert_int(m, >, 0);
|
||||
f = fopen(cache, "w");
|
||||
munit_assert_not_null(f);
|
||||
wr = fputs("ac_cv_cached_gcc=yes\n", f);
|
||||
munit_assert_int(wr, !=, EOF);
|
||||
fclose(f);
|
||||
|
||||
munit_assert_int(write_script("cached", &p, &bad), ==, 0);
|
||||
m = mkpath(run, sizeof run, "run.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
munit_assert_int(run_sh(run), ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "yes");
|
||||
|
||||
/* no probe was attempted (compiler never ran) */
|
||||
m = mkpath(logpath, sizeof logpath, "config.log");
|
||||
munit_assert_int(m, >, 0);
|
||||
munit_assert_int(access(logpath, F_OK), ==, -1);
|
||||
|
||||
/* the cached line was consumed, not overwritten with "no" */
|
||||
{
|
||||
const char *c = read_result("config.cache");
|
||||
|
||||
munit_assert_not_null(strstr(c, "ac_cv_cached_gcc=yes"));
|
||||
}
|
||||
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitResult
|
||||
test_cache_write(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe p = { 0 };
|
||||
char run[600];
|
||||
const char *c;
|
||||
int m;
|
||||
|
||||
(void)build_from("header \"unistd.h\"", &p);
|
||||
munit_assert_int(write_script("writecc", &p, NULL), ==, 0);
|
||||
m = mkpath(run, sizeof run, "run.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
munit_assert_int(run_sh(run), ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "yes");
|
||||
|
||||
c = read_result("config.cache");
|
||||
munit_assert_not_null(strstr(c, "ac_cv_writecc_gcc=yes"));
|
||||
|
||||
st_check_probe_free(&p);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (f) syntax: sh -n / bash -n / zsh -n + banned-construct sweep ---- */
|
||||
|
||||
static MunitResult
|
||||
test_syntax_and_banned(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
static const char *const banned[] = {
|
||||
"[[ ", "]]", "local ", "==", "<<<", "&>", "set -e",
|
||||
};
|
||||
struct st_check_probe hdr = { 0 };
|
||||
struct st_check_probe fn = { 0 };
|
||||
struct st_check_probe sz = { 0 };
|
||||
struct st_check_probe prog = { 0 };
|
||||
char path[600];
|
||||
char *bytes;
|
||||
FILE *f;
|
||||
long n;
|
||||
size_t i;
|
||||
int m;
|
||||
|
||||
(void)build_from("header \"unistd.h\"", &hdr);
|
||||
(void)build_from("function \"strdup\"", &fn);
|
||||
(void)build_from("sizeof \"long\"", &sz);
|
||||
(void)build_from("program \"pkg-config\"", &prog);
|
||||
|
||||
m = mkpath(path, sizeof path, "all.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_probe_emit_cache_functions(f));
|
||||
munit_assert_null(st_probe_emit_snippet("hdr", &hdr, f));
|
||||
munit_assert_null(st_probe_emit_snippet("fn", &fn, f));
|
||||
munit_assert_null(st_probe_emit_snippet("sz", &sz, f));
|
||||
munit_assert_null(st_probe_emit_snippet("prog", &prog, f));
|
||||
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);
|
||||
|
||||
st_check_probe_free(&hdr);
|
||||
st_check_probe_free(&fn);
|
||||
st_check_probe_free(&sz);
|
||||
st_check_probe_free(&prog);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- (g) injection: hostile probe target stays inert ------------------ */
|
||||
|
||||
static MunitResult
|
||||
test_injection_inert(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
char marker[600];
|
||||
char csrc[700];
|
||||
char run[600];
|
||||
int m;
|
||||
int rc;
|
||||
struct st_check_probe p;
|
||||
|
||||
/* A C source that, if the shell ever interpreted it (unquoted), would
|
||||
* remove `marker`'s directory. st_sh_quote makes it a literal; the
|
||||
* compile just fails and the shell never executes the embedded rm. */
|
||||
m = snprintf(marker, sizeof marker, "%s/victim.txt", temp_dir);
|
||||
munit_assert_int(m, >, 0);
|
||||
m = snprintf(csrc, sizeof csrc,
|
||||
"#include <unistd.h>\n; rm -rf %s; \"\n", marker);
|
||||
munit_assert_int(m, >, 0);
|
||||
p = (struct st_check_probe){
|
||||
.mode = ST_PROBE_COMPILE,
|
||||
.c_source = csrc,
|
||||
};
|
||||
munit_assert_int(write_script("hostile", &p, NULL), ==, 0);
|
||||
m = mkpath(run, sizeof run, "run.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
rc = run_sh(run);
|
||||
munit_assert_int(rc, ==, 0);
|
||||
munit_assert_string_equal(read_result("have.out"), "no");
|
||||
munit_assert_int(access(marker, F_OK), ==, -1);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- emit_all: a feature's list of checks ----------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_emit_all(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe a = { .mode = ST_PROBE_COMPILE,
|
||||
.c_source = "#include <unistd.h>\n" };
|
||||
struct st_check_probe b = { .mode = ST_PROBE_COMPILE,
|
||||
.c_source = "#include <stddef.h>\n" };
|
||||
struct st_probe_entry entries[2];
|
||||
char path[600];
|
||||
FILE *f;
|
||||
struct st_error *err;
|
||||
char *bytes;
|
||||
long n;
|
||||
int m;
|
||||
|
||||
entries[0].name = "alpha";
|
||||
entries[0].probe = &a;
|
||||
entries[1].name = "beta";
|
||||
entries[1].probe = &b;
|
||||
|
||||
m = mkpath(path, sizeof path, "all2.sh");
|
||||
munit_assert_int(m, >, 0);
|
||||
f = fopen(path, "w");
|
||||
munit_assert_not_null(f);
|
||||
munit_assert_null(st_probe_emit_cache_functions(f));
|
||||
err = st_probe_emit_all(entries, 2, f);
|
||||
munit_assert_null(err);
|
||||
fclose(f);
|
||||
|
||||
f = fopen(path, "rb");
|
||||
munit_assert_not_null(f);
|
||||
munit_assert_int(fseek(f, 0, SEEK_END), ==, 0);
|
||||
n = ftell(f);
|
||||
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';
|
||||
munit_assert_not_null(strstr(bytes, "have_alpha"));
|
||||
munit_assert_not_null(strstr(bytes, "have_beta"));
|
||||
munit_assert_not_null(strstr(bytes, "ac_cv_alpha_$st_cc_id"));
|
||||
munit_assert_not_null(strstr(bytes, "ac_cv_beta_$st_cc_id"));
|
||||
free(bytes);
|
||||
|
||||
/* empty list is a clean no-op */
|
||||
f = tmpfile();
|
||||
munit_assert_not_null(f);
|
||||
err = st_probe_emit_all(NULL, 0, f);
|
||||
munit_assert_null(err);
|
||||
fclose(f);
|
||||
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
/* ---- error handling ---------------------------------------------------- */
|
||||
|
||||
static MunitResult
|
||||
test_errors(const MunitParameter params[], void *data)
|
||||
{
|
||||
(void)params;
|
||||
(void)data;
|
||||
struct st_check_probe good = { .mode = ST_PROBE_COMPILE,
|
||||
.c_source = "#include <x.h>\n" };
|
||||
struct st_check_probe nocmd = { .mode = ST_PROBE_COMMAND,
|
||||
.command = NULL };
|
||||
struct st_check_probe nosrc = { .mode = ST_PROBE_COMPILE,
|
||||
.c_source = NULL };
|
||||
struct st_check_probe badmode = { .mode = (enum st_check_probe_mode)999,
|
||||
.c_source = "x" };
|
||||
FILE *f = tmpfile();
|
||||
struct st_error *err;
|
||||
|
||||
munit_assert_not_null(f);
|
||||
|
||||
err = st_probe_emit_snippet(NULL, &good, f);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("bad name", &good, f);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("ok", NULL, f);
|
||||
munit_assert_not_null(err);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("ok", &good, NULL);
|
||||
munit_assert_not_null(err);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("ok", &badmode, f);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("ok", &nocmd, f);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_snippet("ok", &nosrc, f);
|
||||
munit_assert_not_null(err);
|
||||
munit_assert_int(st_error_category_of(err), ==, ST_ERR_USAGE);
|
||||
st_error_free(err);
|
||||
|
||||
err = st_probe_emit_cache_functions(NULL);
|
||||
munit_assert_not_null(err);
|
||||
st_error_free(err);
|
||||
|
||||
/* a valid emit returns no error */
|
||||
err = st_probe_emit_snippet("ok", &good, f);
|
||||
munit_assert_null(err);
|
||||
|
||||
fclose(f);
|
||||
return MUNIT_OK;
|
||||
}
|
||||
|
||||
static MunitTest tests[] = {
|
||||
{ "/probe/header-snippet-content", test_header_snippet_content, setup,
|
||||
teardown, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/header-runs-yes-no", test_header_runs_yes_no, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/run-cross-guard", test_run_cross_guard, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/command-quoting", test_command_probe_quoting, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/cache-short-circuit", test_cache_short_circuit, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/cache-write", test_cache_write, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/syntax-banned", test_syntax_and_banned, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/injection-inert", test_injection_inert, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/emit-all", test_emit_all, setup, teardown,
|
||||
MUNIT_TEST_OPTION_NONE, NULL },
|
||||
{ "/probe/errors", test_errors, NULL, NULL, MUNIT_TEST_OPTION_NONE,
|
||||
NULL },
|
||||
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||
};
|
||||
|
||||
static const MunitSuite suite = {
|
||||
"/probe", 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