feat(detect): define feature-check registry schema

This commit is contained in:
2026-08-28 22:09:45 -04:00
parent 219ea3fb4f
commit 00f4f0e1a0
5 changed files with 1079 additions and 44 deletions
+251
View File
@@ -0,0 +1,251 @@
/*
* check_registry.c - declarative feature-check kinds for stupidtools
* (todo 10).
*
* The 8 check kinds a `feature` node's children can carry, their DSL
* keyword spellings, and their argument/property shapes - as DATA (the
* st_check_shapes table), plus the node->kind mapping. See
* detect/check_registry.h for the semantics; this module deliberately
* does NOT generate probes (todo 11) and does NOT execute anything.
*
* Check node names may be any KDL string form (identifier, quoted,
* raw, multi-line): non-identifier names are interpreted through the
* value model (kdl/value.h) before comparison. A value-model failure on
* the name (bad escape etc.) is re-owned as a schema error, exactly
* like src/kdl/schema.c does.
*
* ERRORS AND SPANS
* ----------------
* st_check_kind_from_node() reports failures as owned ST_ERR_KDL_SCHEMA
* errors whose span is heap-allocated IN THE SAME BLOCK as the error
* struct (the parser.c pattern), so err->span stays valid until
* st_error_free() and is never dangling. The span points at the node's
* NAME token (its `file` pointer borrows the caller's source buffer -
* the same lifetime contract as the parser). The single span-less error
* is the defensive NULL-node case: no source position exists to point
* at.
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#include "detect/check_registry.h"
#include "error.h"
#include "kdl/ast.h"
#include "kdl/value.h"
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* ---- the shape table (the data contract for todos 11/16) ------------- */
/* `library` and `pkg_config` carry an optional version constraint as two
* further positional args: the literal `version` keyword followed by the
* constraint string (see the header comment). */
const struct st_check_shape st_check_shapes[ST_CHECK_KIND_COUNT] = {
{ ST_CHECK_HEADER, "header", "header name", 1, 0 },
{ ST_CHECK_FUNCTION, "function", "function name", 1, 0 },
{ ST_CHECK_LIBRARY, "library", "library name (linked as -l<name>)",
1, 2 },
{ ST_CHECK_TYPE, "type", "type name", 1, 0 },
{ ST_CHECK_SIZEOF, "sizeof", "type name", 1, 0 },
{ ST_CHECK_PROGRAM, "program", "program name (checked via "
"command -v)", 1, 0 },
{ ST_CHECK_COMPILER_FLAG, "compiler_flag", "compiler flag", 1, 0 },
{ ST_CHECK_PKG_CONFIG, "pkg_config", "pkg-config package name", 1,
2 },
};
/* ---- 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 : "");
}
/* ---- name matching ---------------------------------------------------- */
/* Compare a node name token against a literal, accepting every string
* form (identifiers compared directly; other forms via the value
* model). Returns NULL and sets *out on success, or an owned schema
* error when the name is a string the value model cannot interpret. */
static struct st_error *
ref_equals(const struct st_kdl_token_ref *name, const char *s, bool *out)
{
if (name->kind == ST_TOK_IDENT) {
*out = name->len == strlen(s) &&
memcmp(name->text, s, name->len) == 0;
return NULL;
}
{
struct st_kdl_value v;
struct st_error *e = st_kdl_value_from_token(name, &v);
if (e != NULL) {
struct st_error *r = schema_err_from(e);
st_error_free(e);
*out = false;
return r;
}
*out = v.kind == ST_KDL_VAL_STRING && strcmp(v.as.str, s) == 0;
st_kdl_value_free(&v);
return NULL;
}
}
/* Copy a node name token into a fixed buffer for MESSAGE text,
* truncating to fit (message-only; never fatal). */
static void
name_into(const struct st_kdl_token_ref *name, char *buf, size_t cap)
{
if (cap == 0) {
return;
}
if (name->kind == ST_TOK_IDENT) {
size_t n = name->len < cap - 1 ? name->len : cap - 1;
memcpy(buf, name->text, n);
buf[n] = '\0';
return;
}
{
struct st_kdl_value v;
struct st_error *e = st_kdl_value_from_token(name, &v);
if (e == NULL && v.kind == ST_KDL_VAL_STRING) {
snprintf(buf, cap, "%s", v.as.str);
} else {
snprintf(buf, cap, "?");
}
st_error_free(e);
st_kdl_value_free(&v);
}
}
/* ---- public API ------------------------------------------------------- */
enum st_check_kind
st_check_kind_from_node(const struct st_kdl_node *node,
struct st_error **err)
{
char msg[192];
size_t i;
if (err != NULL) {
*err = NULL;
}
if (node == NULL) {
if (err != NULL) {
*err = st_error_kdl_schema("check node is NULL");
}
return ST_CHECK_KIND_COUNT;
}
if (node->name.len == 0) {
if (err != NULL) {
*err = err_at_owned(node->name.span,
"check node has an empty name");
}
return ST_CHECK_KIND_COUNT;
}
for (i = 0; i < ST_CHECK_KIND_COUNT; i++) {
bool eq = false;
struct st_error *e = ref_equals(&node->name,
st_check_shapes[i].name, &eq);
if (e != NULL) {
if (err != NULL) {
*err = e;
} else {
st_error_free(e);
}
return ST_CHECK_KIND_COUNT;
}
if (eq) {
return st_check_shapes[i].kind;
}
}
if (err != NULL) {
char nm[65];
name_into(&node->name, nm, sizeof nm);
snprintf(msg, sizeof msg,
"unknown check kind '%s' (expected 'header', 'function', "
"'library', 'type', 'sizeof', 'program', 'compiler_flag', "
"or 'pkg_config')", nm);
*err = err_at_owned(node->name.span, msg);
}
return ST_CHECK_KIND_COUNT;
}
const char *
st_check_kind_name(enum st_check_kind kind)
{
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
return "?";
}
return st_check_shapes[kind].name;
}
const struct st_check_shape *
st_check_kind_shape(enum st_check_kind kind)
{
if ((size_t)kind >= ST_CHECK_KIND_COUNT) {
return NULL;
}
return &st_check_shapes[kind];
}
size_t
st_check_kind_count(void)
{
return ST_CHECK_KIND_COUNT;
}
+140
View File
@@ -0,0 +1,140 @@
/*
* check_registry.h - declarative feature-check kinds for stupidtools
* (todo 10).
*
* A `feature "name" { check* }` block's children are CHECK nodes. Each
* check node's NAME is one of eight fixed keywords - the check KINDS -
* and its FIRST (and only) positional argument is the TARGET the check
* is about:
*
* check := <name> <arg> props*
*
* The eight kinds mirror GNU Autoconf's "Existing Tests" (manual §5) and
* pkg-config's PKG_CHECK_MODULES:
*
* header arg = header name -> #include <arg> probe
* e.g. header "pthread.h"
* function arg = function name -> link probe for the symbol
* e.g. function "strdup"
* library arg = library name -> -l<arg> link probe
* e.g. library "pthread"
* type arg = type name -> compile probe for the type
* e.g. type "size_t"
* sizeof arg = type name -> sizeof(<arg>) probe
* e.g. sizeof "long"
* program arg = program name -> command -v <arg> probe
* e.g. program "pkg-config"
* compiler_flag arg = flag -> compile probe with <arg> added
* e.g. compiler_flag "-fsanitize=address"
* pkg_config arg = package name -> pkg-config --cflags --libs <arg>
* e.g. pkg_config "openssl"
*
* WHAT THIS TODO OWNS
* -------------------
* Only the DECLARATIVE SHAPE: the kind enumeration, the kind<->keyword
* mapping, and per-kind argument/property shape as DATA (the table
* below). It deliberately does NOT implement probe generation (todo 11)
* or probe execution/caching (todo 12). src/kdl/schema.c consumes the
* mapping + table to enforce the DSL (see its todo-10 HOOK); todos 11
* and 16 consume the same table to generate probes and substitute
* results - THIS HEADER IS THE CONTRACT for them.
*
* OPTIONAL `version` CONSTRAINT (library and pkg_config)
* ------------------------------------------------------
* `library` and `pkg_config` may carry an optional version constraint,
* spelled EXACTLY like the project node's version (pinned by todo 9):
* the literal keyword `version` followed by a constraint string, as two
* further positional arguments:
*
* library "curl" version ">=7.0"
* pkg_config "openssl" version ">=1.1"
*
* The constraint is "<op><version>": <op> one of >= <= = > <, <version>
* a dotted numeric version. Todo 10 pins only the SHAPE (the keyword +
* a non-empty unannotated string); the OPERATOR SEMANTICS are
* interpreted by todo 11 (probe generation) and todo 16 (result
* substitution). The spelling is an argument pair, not a KDL property,
* because this parser classifies `version "..."` as two positional
* arguments - the same reason the project node spells it that way. The
* property form `version="..."` is NOT part of the DSL and is rejected
* (see PROPERTIES below).
*
* PROPERTIES
* ----------
* No check kind accepts properties in this todo (the grammar's `props*`
* is always empty for now); the schema validator rejects them with a
* spanned error. The shape table carries no props column for the same
* reason - todos 11/16 derive everything from the argument counts.
*
* ERRORS
* ------
* st_check_kind_from_node() reports unknown/empty check names as owned
* ST_ERR_KDL_SCHEMA errors whose span is heap-allocated IN THE SAME
* BLOCK as the error (the parser.c pattern), so err->span stays valid
* until st_error_free(err). The span points at the node's name token.
*
* Copyright (c) 2026 huntedbytheirs
* SPDX-License-Identifier: BSD-3-Clause
*/
#ifndef ST_DETECT_CHECK_REGISTRY_H
#define ST_DETECT_CHECK_REGISTRY_H
#include <stddef.h>
struct st_error;
struct st_kdl_node;
/* The eight declarative check kinds, in canonical order. */
enum st_check_kind {
ST_CHECK_HEADER = 0, /* header "pthread.h" */
ST_CHECK_FUNCTION, /* function "strdup" */
ST_CHECK_LIBRARY, /* library "pthread" -> -lpthread */
ST_CHECK_TYPE, /* type "size_t" */
ST_CHECK_SIZEOF, /* sizeof "long" */
ST_CHECK_PROGRAM, /* program "pkg-config" -> command -v */
ST_CHECK_COMPILER_FLAG, /* compiler_flag "-fsanitize=address" */
ST_CHECK_PKG_CONFIG, /* pkg_config "openssl" */
ST_CHECK_KIND_COUNT, /* sentinel: number of real kinds above */
};
/* The DSL shape of one check kind, as DATA (consumed by the schema
* validator and by todos 11/16). */
struct st_check_shape {
enum st_check_kind kind; /* the kind this row describes */
const char *name; /* DSL keyword spelling, e.g. "header" */
const char *arg_meaning; /* what the required argument names, e.g.
"header name" */
size_t required_args; /* positional args the node MUST carry */
size_t optional_args; /* further positional args tolerated; 2
for library/pkg_config = the literal
`version` keyword followed by the
version constraint string (see the
header comment); 0 elsewhere */
};
/* The registry table: one row per kind, indexed by the ST_CHECK_*
* values (st_check_shapes[k].kind == k). */
extern const struct st_check_shape st_check_shapes[ST_CHECK_KIND_COUNT];
/* Map a feature's check child node to its kind by the node NAME. The
* name may be any KDL string form (bare identifier or quoted). Returns
* the kind and leaves *err untouched on success. On failure returns
* ST_CHECK_KIND_COUNT and sets *err to an owned ST_ERR_KDL_SCHEMA error
* whose span points at the node's name: an unknown keyword, an empty
* name, or (defensively) a NULL node. */
enum st_check_kind st_check_kind_from_node(const struct st_kdl_node *node,
struct st_error **err);
/* Stable DSL keyword for a kind (e.g. "header"). Kinds outside the
* table yield "?" rather than indexing out of range. */
const char *st_check_kind_name(enum st_check_kind kind);
/* The shape-table row for a kind, or NULL when `kind` is not a real
* kind. */
const struct st_check_shape *st_check_kind_shape(enum st_check_kind kind);
/* Number of real kinds (ST_CHECK_KIND_COUNT). */
size_t st_check_kind_count(void);
#endif /* ST_DETECT_CHECK_REGISTRY_H */
+125 -37
View File
@@ -27,6 +27,7 @@
#include "kdl/schema.h"
#include "detect/check_registry.h"
#include "error.h"
#include "kdl/ast.h"
#include "kdl/value.h"
@@ -438,16 +439,124 @@ done:
return e;
}
/* One check child of a feature: its kind has already been resolved by
* the registry (st_check_kind_from_node). Enforce the kind's DSL shape
* from the registry table - the required positional argument (a
* non-empty unannotated string), at most the kind's optional arguments
* (for library/pkg_config: the literal `version` keyword followed by a
* non-empty unannotated constraint string, mirroring the project
* node's spelling), no properties, and no children. */
static struct st_error *
validate_check(const struct st_kdl_node *c, enum st_check_kind kind,
const char *feature)
{
const struct st_check_shape *shape = st_check_kind_shape(kind);
char msg[384];
char cname[65];
char cctx[192];
struct st_error *e;
struct st_kdl_arg *a;
size_t nargs = 1;
size_t max_args;
size_t i;
char *tmp = NULL;
name_into(&c->name, cname, sizeof cname);
if (c->args == NULL) {
snprintf(msg, sizeof msg,
"feature '%s': check '%s' requires an argument (the %s)",
feature, cname, shape->arg_meaning);
return err_at_owned(c->name.span, msg);
}
for (a = c->args->next; a != NULL; a = a->next) {
nargs++;
}
max_args = shape->required_args + shape->optional_args;
if (nargs > max_args) {
/* span the first argument beyond the kind's shape */
a = c->args;
for (i = 0; i < max_args; i++) {
a = a->next;
}
if (shape->optional_args == 0) {
snprintf(msg, sizeof msg,
"feature '%s': check '%s' takes exactly one argument",
feature, cname);
} else {
snprintf(msg, sizeof msg,
"feature '%s': check '%s' takes at most three arguments "
"(the %s, then the optional 'version' keyword and its "
"constraint)", feature, cname, shape->arg_meaning);
}
return err_at_owned(a->value.span, msg);
}
snprintf(cctx, sizeof cctx, "feature '%s': check '%s' argument",
feature, cname);
e = require_string_arg(&c->args->value, c->args->annotation, cctx,
&tmp);
free(tmp);
if (e != NULL) {
return e;
}
if (shape->optional_args == 2) {
a = c->args->next;
if (a != NULL) {
if (a->annotation != NULL ||
!(a->value.kind == ST_TOK_IDENT && a->value.len == 7 &&
memcmp(a->value.text, "version", 7) == 0)) {
snprintf(msg, sizeof msg,
"feature '%s': check '%s': expected the keyword "
"'version' as the second argument", feature, cname);
return err_at_owned(a->value.span, msg);
}
if (a->next == NULL) {
snprintf(msg, sizeof msg,
"feature '%s': check '%s': 'version' requires the "
"version constraint argument", feature, cname);
return err_at_owned(a->value.span, msg);
}
{
char vctx[192];
snprintf(vctx, sizeof vctx,
"feature '%s': check '%s' version", feature,
cname);
e = require_string_arg(&a->next->value,
a->next->annotation, vctx, &tmp);
free(tmp);
if (e != NULL) {
return e;
}
}
}
}
if (c->props != NULL) {
char key[65];
name_into(&c->props->key, key, sizeof key);
snprintf(msg, sizeof msg,
"feature '%s': check '%s' has unexpected property '%s' "
"(checks take no properties)", feature, cname, key);
return err_at_owned(c->props->key.span, msg);
}
if (c->children != NULL) {
snprintf(msg, sizeof msg,
"feature '%s': check '%s' takes no children", feature, cname);
return err_at_owned(c->children->name.span, msg);
}
return NULL;
}
/* feature "name" { <checks> }: the name shape (no properties) plus a
* children block of CHECK nodes, validated STRUCTURALLY ONLY. The 8
* check kinds and their exact argument/property shapes are todo 10's job;
* until its registry lands, every child is accepted provided it has at
* least one non-empty string argument and no children (its properties
* are deliberately left for todo 10 to validate). */
* children block of CHECK nodes. Each child's NAME is dispatched through
* the feature-check registry (src/detect/check_registry.h, todo 10) to
* one of the 8 kinds - header, function, library, type, sizeof,
* program, compiler_flag, pkg_config - and its argument/property shape
* is enforced from the registry table (see validate_check). */
static struct st_error *
validate_feature(const struct st_kdl_node *n)
{
char msg[192];
char name[65];
struct st_error *e;
char *name_str = NULL;
@@ -461,6 +570,7 @@ validate_feature(const struct st_kdl_node *n)
if (n->props != NULL) {
char key[65];
char msg[192];
name_into(&n->props->key, key, sizeof key);
snprintf(msg, sizeof msg, "feature '%s' has unexpected property "
@@ -471,44 +581,22 @@ validate_feature(const struct st_kdl_node *n)
for (c = n->children; c != NULL; c = c->next) {
/*
* HOOK (todo 10): dispatch on the check node NAME through the
* HOOK (todo 10): dispatch the check node NAME through the
* feature-check registry (src/detect/check_registry.h) to
* validate the 8 kinds - header, function, library, type,
* sizeof, program, compiler_flag, pkg_config - and their
* argument/property shapes. Until then, structural checks only:
* argument/property shapes.
*/
if (c->args == NULL) {
char cname[65];
enum st_check_kind kind;
struct st_error *ke = NULL;
name_into(&c->name, cname, sizeof cname);
snprintf(msg, sizeof msg,
"feature '%s': check '%s' requires an argument", name,
cname);
e = err_at_owned(c->name.span, msg);
kind = st_check_kind_from_node(c, &ke);
if (ke != NULL) {
e = ke;
goto done;
}
{
char cname[65];
char cctx[192];
char *tmp = NULL;
name_into(&c->name, cname, sizeof cname);
snprintf(cctx, sizeof cctx, "feature '%s': check '%s' argument",
name, cname);
e = require_string_arg(&c->args->value, c->args->annotation,
cctx, &tmp);
free(tmp);
if (e != NULL) {
goto done;
}
}
if (c->children != NULL) {
char cname[65];
name_into(&c->name, cname, sizeof cname);
snprintf(msg, sizeof msg,
"feature '%s': check '%s' takes no children", name, cname);
e = err_at_owned(c->children->name.span, msg);
e = validate_check(c, kind, name);
if (e != NULL) {
goto done;
}
}