Template
813 lines
25 KiB
C
813 lines
25 KiB
C
/*
|
|
* schema.c - stupidtools DSL schema validation (todo 9).
|
|
*
|
|
* Validates a parsed KDL document (src/kdl/ast.h) against the stupidtools
|
|
* DSL. The grammar is pinned in schema.h - the contract for todos 10/16/23.
|
|
* This module only READS the AST; it never modifies or frees it.
|
|
*
|
|
* Type checks go through the value model (src/kdl/value.h): names, files,
|
|
* version strings and check arguments must be non-empty unannotated
|
|
* strings; an option's `default` must be an unannotated boolean. Value-
|
|
* model failures (bad escapes, overflow) are re-owned as schema errors.
|
|
*
|
|
* ERRORS AND SPANS
|
|
* ----------------
|
|
* Every failure returns an owned st_error of category ST_ERR_KDL_SCHEMA
|
|
* 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 is borrowed-by-value from the AST's
|
|
* token refs, whose `file` pointer in turn borrows the caller's source
|
|
* buffer - the same lifetime contract as the parser. The single span-less
|
|
* error is "missing project" on an empty document: there is no source
|
|
* position (or file name) to point at.
|
|
*
|
|
* Copyright (c) 2026 huntedbytheirs
|
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
*/
|
|
|
|
#include "kdl/schema.h"
|
|
|
|
#include "detect/check_registry.h"
|
|
#include "error.h"
|
|
#include "kdl/ast.h"
|
|
#include "kdl/value.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 : "");
|
|
}
|
|
|
|
/* Interpret a token ref through the value model, converting any failure
|
|
* into an owned schema error (the original is freed here). On success
|
|
* `*out` holds the interpreted value (caller frees via
|
|
* st_kdl_value_free). */
|
|
static struct st_error *
|
|
value_interpret(const struct st_kdl_token_ref *tok,
|
|
const struct st_kdl_token_ref *ann,
|
|
struct st_kdl_value *out)
|
|
{
|
|
struct st_error *e = st_kdl_value_from_token_annotated(tok, ann, out);
|
|
|
|
if (e != NULL) {
|
|
struct st_error *r = schema_err_from(e);
|
|
|
|
st_error_free(e);
|
|
return r;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ---- naming helpers --------------------------------------------------- */
|
|
|
|
/* Copy a node name / property key into a fixed buffer for MESSAGE text,
|
|
* truncating to fit. Identifiers are copied raw (the common case, no
|
|
* allocation); other string forms are interpreted via the value model,
|
|
* falling back to "?" when that fails (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);
|
|
}
|
|
}
|
|
|
|
/* Compare a node name against a literal, accepting every string form
|
|
* (identifiers compared directly; other forms via the value model).
|
|
* Sets *out and returns NULL on success, or an owned schema error when
|
|
* the name is a string the value model cannot interpret. */
|
|
static struct st_error *
|
|
name_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;
|
|
}
|
|
}
|
|
|
|
/* ---- argument helpers ------------------------------------------------- */
|
|
|
|
/* Interpret `tok` (with optional annotation) as the non-empty unannotated
|
|
* string a DSL slot requires; on success *out owns the string (caller
|
|
* frees). `ctx` names the slot in messages, e.g. "target name". */
|
|
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 = value_interpret(tok, ann, &v);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
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;
|
|
}
|
|
|
|
/* The shared name shape of target/feature/option: exactly one non-empty
|
|
* string argument. `ctx` is the node kind ("target"). On success *out
|
|
* owns the name (caller frees). */
|
|
static struct st_error *
|
|
one_name_arg(const struct st_kdl_node *n, const char *ctx, char **out)
|
|
{
|
|
char msg[192];
|
|
char cctx[96];
|
|
|
|
*out = NULL;
|
|
if (n->args == NULL) {
|
|
snprintf(msg, sizeof msg, "%s requires a name argument", ctx);
|
|
return err_at_owned(n->name.span, msg);
|
|
}
|
|
if (n->args->next != NULL) {
|
|
char nm[65];
|
|
|
|
name_into(&n->args->value, nm, sizeof nm);
|
|
snprintf(msg, sizeof msg, "%s '%s' takes exactly one argument",
|
|
ctx, nm);
|
|
return err_at_owned(n->args->next->value.span, msg);
|
|
}
|
|
snprintf(cctx, sizeof cctx, "%s name", ctx);
|
|
return require_string_arg(&n->args->value, n->args->annotation, cctx,
|
|
out);
|
|
}
|
|
|
|
/* ---- per-node validators ---------------------------------------------- */
|
|
|
|
/* project "name" version "semver": the name is the first positional
|
|
* argument; the version string follows the LITERAL `version` keyword.
|
|
* No properties, no children, exactly three arguments. */
|
|
static struct st_error *
|
|
validate_project(const struct st_kdl_node *n)
|
|
{
|
|
struct st_kdl_arg *a = n->args;
|
|
struct st_kdl_arg *ver_kw;
|
|
char msg[192];
|
|
char name[65];
|
|
char ctx[96];
|
|
struct st_error *e;
|
|
char *name_str = NULL;
|
|
char *ver_str = NULL;
|
|
|
|
if (a == NULL) {
|
|
return err_at_owned(n->name.span,
|
|
"project requires a name argument");
|
|
}
|
|
e = require_string_arg(&a->value, a->annotation, "project name",
|
|
&name_str);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
name_into(&a->value, name, sizeof name);
|
|
|
|
a = a->next;
|
|
if (a == NULL) {
|
|
snprintf(msg, sizeof msg,
|
|
"project '%s' requires a version (expected: project \"name\" "
|
|
"version \"semver\")", name);
|
|
e = err_at_owned(n->name.span, msg);
|
|
goto done;
|
|
}
|
|
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,
|
|
"project '%s': expected the keyword 'version' as the second "
|
|
"argument", name);
|
|
e = err_at_owned(a->value.span, msg);
|
|
goto done;
|
|
}
|
|
ver_kw = a;
|
|
a = a->next;
|
|
if (a == NULL) {
|
|
snprintf(msg, sizeof msg,
|
|
"project '%s': 'version' requires the version string argument",
|
|
name);
|
|
e = err_at_owned(ver_kw->value.span, msg);
|
|
goto done;
|
|
}
|
|
snprintf(ctx, sizeof ctx, "project '%s' version", name);
|
|
e = require_string_arg(&a->value, a->annotation, ctx, &ver_str);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
if (a->next != NULL) {
|
|
snprintf(msg, sizeof msg,
|
|
"project '%s' takes exactly three arguments: the name, the "
|
|
"'version' keyword, and the version string", name);
|
|
e = err_at_owned(a->next->value.span, msg);
|
|
goto done;
|
|
}
|
|
if (n->props != NULL) {
|
|
char key[65];
|
|
|
|
name_into(&n->props->key, key, sizeof key);
|
|
snprintf(msg, sizeof msg, "project '%s' has unexpected property "
|
|
"'%s'", name, key);
|
|
e = err_at_owned(n->props->key.span, msg);
|
|
goto done;
|
|
}
|
|
if (n->children != NULL) {
|
|
snprintf(msg, sizeof msg, "project '%s' takes no children", name);
|
|
e = err_at_owned(n->children->name.span, msg);
|
|
goto done;
|
|
}
|
|
e = NULL;
|
|
done:
|
|
free(name_str);
|
|
free(ver_str);
|
|
return e;
|
|
}
|
|
|
|
/* One `src "<file>"` or `feature "<name>"` child of a target: exactly one
|
|
* non-empty string argument, no properties, no children. */
|
|
static struct st_error *
|
|
validate_target_child(const struct st_kdl_node *c, const char *ctx,
|
|
bool is_src)
|
|
{
|
|
const char *kind = is_src ? "src" : "feature";
|
|
const char *slot = is_src ? "file" : "name";
|
|
char cctx[192];
|
|
char msg[192];
|
|
|
|
if (c->args == NULL) {
|
|
snprintf(msg, sizeof msg, "%s '%s' requires a %s argument", ctx,
|
|
kind, slot);
|
|
return err_at_owned(c->name.span, msg);
|
|
}
|
|
if (c->args->next != NULL) {
|
|
snprintf(msg, sizeof msg, "%s '%s' takes exactly one argument",
|
|
ctx, kind);
|
|
return err_at_owned(c->args->next->value.span, msg);
|
|
}
|
|
if (c->props != NULL) {
|
|
snprintf(msg, sizeof msg, "%s '%s' takes no properties", ctx, kind);
|
|
return err_at_owned(c->props->key.span, msg);
|
|
}
|
|
if (c->children != NULL) {
|
|
snprintf(msg, sizeof msg, "%s '%s' takes no children", ctx, kind);
|
|
return err_at_owned(c->children->name.span, msg);
|
|
}
|
|
{
|
|
char *tmp = NULL;
|
|
struct st_error *e;
|
|
|
|
snprintf(cctx, sizeof cctx, "%s '%s' %s", ctx, kind, slot);
|
|
e = require_string_arg(&c->args->value, c->args->annotation, cctx,
|
|
&tmp);
|
|
free(tmp);
|
|
return e;
|
|
}
|
|
}
|
|
|
|
/* target "name" { src ...; feature ...; }: the name shape plus a children
|
|
* block whose nodes are only `src` (a file) or `feature` (a reference to
|
|
* a top-level feature by name). Presence-checking the reference against
|
|
* the document is OPTIONAL for this todo - only the shape is enforced. */
|
|
static struct st_error *
|
|
validate_target(const struct st_kdl_node *n)
|
|
{
|
|
char msg[256];
|
|
char name[65];
|
|
char ctx[96];
|
|
struct st_error *e;
|
|
char *name_str = NULL;
|
|
struct st_kdl_node *c;
|
|
|
|
e = one_name_arg(n, "target", &name_str);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
name_into(&n->args->value, name, sizeof name);
|
|
|
|
if (n->props != NULL) {
|
|
char key[65];
|
|
|
|
name_into(&n->props->key, key, sizeof key);
|
|
snprintf(msg, sizeof msg, "target '%s' has unexpected property "
|
|
"'%s'", name, key);
|
|
e = err_at_owned(n->props->key.span, msg);
|
|
goto done;
|
|
}
|
|
|
|
snprintf(ctx, sizeof ctx, "target '%s'", name);
|
|
for (c = n->children; c != NULL; c = c->next) {
|
|
bool is_src = false;
|
|
bool is_feat = false;
|
|
|
|
e = name_equals(&c->name, "src", &is_src);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
e = name_equals(&c->name, "feature", &is_feat);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
if (is_src || is_feat) {
|
|
e = validate_target_child(c, ctx, is_src);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
continue;
|
|
}
|
|
{
|
|
char cname[65];
|
|
|
|
name_into(&c->name, cname, sizeof cname);
|
|
snprintf(msg, sizeof msg,
|
|
"%s has unknown child node '%s' (expected 'src' or "
|
|
"'feature')", ctx, cname);
|
|
e = err_at_owned(c->name.span, msg);
|
|
goto done;
|
|
}
|
|
}
|
|
e = NULL;
|
|
done:
|
|
free(name_str);
|
|
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. 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 name[65];
|
|
struct st_error *e;
|
|
char *name_str = NULL;
|
|
struct st_kdl_node *c;
|
|
|
|
e = one_name_arg(n, "feature", &name_str);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
name_into(&n->args->value, name, sizeof name);
|
|
|
|
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 "
|
|
"'%s'", name, key);
|
|
e = err_at_owned(n->props->key.span, msg);
|
|
goto done;
|
|
}
|
|
|
|
for (c = n->children; c != NULL; c = c->next) {
|
|
/*
|
|
* 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.
|
|
*/
|
|
enum st_check_kind kind;
|
|
struct st_error *ke = NULL;
|
|
|
|
kind = st_check_kind_from_node(c, &ke);
|
|
if (ke != NULL) {
|
|
e = ke;
|
|
goto done;
|
|
}
|
|
e = validate_check(c, kind, name);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
}
|
|
e = NULL;
|
|
done:
|
|
free(name_str);
|
|
return e;
|
|
}
|
|
|
|
/* option "name" (default=<bool>)?: the name shape, no children, and at
|
|
* most one `default` property whose value must be an unannotated boolean
|
|
* (#true/#false) once interpreted through the value model. */
|
|
static struct st_error *
|
|
validate_option(const struct st_kdl_node *n)
|
|
{
|
|
char msg[256];
|
|
char name[65];
|
|
struct st_error *e;
|
|
char *name_str = NULL;
|
|
struct st_kdl_prop *p;
|
|
bool seen_default = false;
|
|
|
|
e = one_name_arg(n, "option", &name_str);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
name_into(&n->args->value, name, sizeof name);
|
|
|
|
if (n->children != NULL) {
|
|
snprintf(msg, sizeof msg, "option '%s' takes no children", name);
|
|
e = err_at_owned(n->children->name.span, msg);
|
|
goto done;
|
|
}
|
|
|
|
for (p = n->props; p != NULL; p = p->next) {
|
|
bool is_def = false;
|
|
|
|
e = name_equals(&p->key, "default", &is_def);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
if (!is_def) {
|
|
char key[65];
|
|
|
|
name_into(&p->key, key, sizeof key);
|
|
snprintf(msg, sizeof msg,
|
|
"option '%s' has unexpected property '%s' (only 'default' "
|
|
"is allowed)", name, key);
|
|
e = err_at_owned(p->key.span, msg);
|
|
goto done;
|
|
}
|
|
if (seen_default) {
|
|
snprintf(msg, sizeof msg,
|
|
"option '%s' has a duplicate 'default' property", name);
|
|
e = err_at_owned(p->key.span, msg);
|
|
goto done;
|
|
}
|
|
seen_default = true;
|
|
{
|
|
struct st_kdl_value v;
|
|
|
|
e = value_interpret(&p->value, p->annotation, &v);
|
|
if (e != NULL) {
|
|
goto done;
|
|
}
|
|
if (v.annotation != NULL) {
|
|
snprintf(msg, sizeof msg,
|
|
"option '%s': 'default' must not carry a type "
|
|
"annotation", name);
|
|
e = err_at_owned(p->value.span, msg);
|
|
st_kdl_value_free(&v);
|
|
goto done;
|
|
}
|
|
if (v.kind != ST_KDL_VAL_BOOL) {
|
|
snprintf(msg, sizeof msg,
|
|
"option '%s': 'default' must be a boolean (got %s)",
|
|
name, st_kdl_value_kind_name(v.kind));
|
|
e = err_at_owned(p->value.span, msg);
|
|
st_kdl_value_free(&v);
|
|
goto done;
|
|
}
|
|
st_kdl_value_free(&v);
|
|
}
|
|
}
|
|
e = NULL;
|
|
done:
|
|
free(name_str);
|
|
return e;
|
|
}
|
|
|
|
/* ---- top level -------------------------------------------------------- */
|
|
|
|
/* Classify a top-level node name. */
|
|
enum top_kind {
|
|
TOP_PROJECT = 0,
|
|
TOP_TARGET,
|
|
TOP_FEATURE,
|
|
TOP_OPTION,
|
|
TOP_UNKNOWN,
|
|
};
|
|
|
|
static struct st_error *
|
|
classify_top(const struct st_kdl_node *n, enum top_kind *out)
|
|
{
|
|
static const char *const names[] = { "project", "target", "feature",
|
|
"option" };
|
|
static const enum top_kind kinds[] = { TOP_PROJECT, TOP_TARGET,
|
|
TOP_FEATURE, TOP_OPTION };
|
|
size_t i;
|
|
|
|
for (i = 0; i < sizeof(names) / sizeof(names[0]); i++) {
|
|
bool eq = false;
|
|
struct st_error *e = name_equals(&n->name, names[i], &eq);
|
|
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
if (eq) {
|
|
*out = kinds[i];
|
|
return NULL;
|
|
}
|
|
}
|
|
*out = TOP_UNKNOWN;
|
|
return NULL;
|
|
}
|
|
|
|
/* ---- public API ------------------------------------------------------- */
|
|
|
|
struct st_error *
|
|
st_kdl_validate(const struct st_kdl_document *doc)
|
|
{
|
|
struct st_kdl_node *n;
|
|
bool saw_goal = false; /* saw at least one target or feature */
|
|
struct st_error *e;
|
|
|
|
if (doc == NULL || doc->nodes == NULL) {
|
|
/* No source position (or file name) exists to point at. */
|
|
return st_error_kdl_schema("missing required 'project' node");
|
|
}
|
|
|
|
n = doc->nodes;
|
|
{
|
|
enum top_kind kind;
|
|
|
|
e = classify_top(n, &kind);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
if (kind != TOP_PROJECT) {
|
|
char nm[65];
|
|
char msg[192];
|
|
|
|
name_into(&n->name, nm, sizeof nm);
|
|
snprintf(msg, sizeof msg,
|
|
"expected 'project' as the first top-level node, got '%s'",
|
|
nm);
|
|
return err_at_owned(n->name.span, msg);
|
|
}
|
|
e = validate_project(n);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
}
|
|
|
|
for (n = n->next; n != NULL; n = n->next) {
|
|
enum top_kind kind;
|
|
|
|
e = classify_top(n, &kind);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
switch (kind) {
|
|
case TOP_PROJECT:
|
|
return err_at_owned(n->name.span, "duplicate 'project' node");
|
|
case TOP_TARGET:
|
|
e = validate_target(n);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
saw_goal = true;
|
|
break;
|
|
case TOP_FEATURE:
|
|
e = validate_feature(n);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
saw_goal = true;
|
|
break;
|
|
case TOP_OPTION:
|
|
e = validate_option(n);
|
|
if (e != NULL) {
|
|
return e;
|
|
}
|
|
break;
|
|
case TOP_UNKNOWN: {
|
|
char nm[65];
|
|
char msg[192];
|
|
|
|
name_into(&n->name, nm, sizeof nm);
|
|
snprintf(msg, sizeof msg,
|
|
"unknown top-level node '%s' (expected 'target', "
|
|
"'feature', or 'option')", nm);
|
|
return err_at_owned(n->name.span, msg);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!saw_goal) {
|
|
return err_at_owned(doc->nodes->name.span,
|
|
"missing 'target' or 'feature' node after 'project'");
|
|
}
|
|
return NULL;
|
|
}
|