Template
feat(ext): add extension discovery and loading
Extension discovery (plan todo 20): scans --ext-dir dirs, STUPIDTOOLS_EXT, the XDG user dir and a builtin-ext dir for *.lua modules, runs each in the sandboxed Lua runtime, and enumerates its registrations; the builtin C/C++ modules load first via st_ext_init_builtins. Fail-fast with an error naming file and line on any malformed/sandbox-violating module. The CLI gains a repeatable --ext-dir flag; with flag or env present main.c runs discovery and prints 'loaded extension: ...' lines from actual registrations (interim wiring until todo 23). Note: src/Makefile.am gained the ext + vendored Lua sources because the binary otherwise cannot link discovery -- required for the root-verifiable CLI acceptance.
This commit is contained in:
@@ -0,0 +1,45 @@
|
|||||||
|
# builtin-ext/ — builtin extension directory
|
||||||
|
|
||||||
|
The directory of extension modules that ship with stupidtools (plan
|
||||||
|
todo 20). It is currently empty on purpose; it exists to lock the
|
||||||
|
convention.
|
||||||
|
|
||||||
|
## What goes here
|
||||||
|
|
||||||
|
Lua extension modules (`*.lua`) that load by default on every run. A
|
||||||
|
module registers checks and languages through the sandboxed runtime's
|
||||||
|
`st` API:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
st.register_check("magic")
|
||||||
|
st.register_language("fortran")
|
||||||
|
```
|
||||||
|
|
||||||
|
Only regular files with a `.lua` suffix are loaded (dotfiles are
|
||||||
|
skipped); anything else here — this README included — is ignored by
|
||||||
|
discovery. Files load in bytewise-lexicographic order.
|
||||||
|
|
||||||
|
## How discovery finds this directory
|
||||||
|
|
||||||
|
`st_ext_discover()` (src/ext/discovery.c) scans this directory LAST in
|
||||||
|
the search order:
|
||||||
|
|
||||||
|
1. each `--ext-dir` directory (repeatable flag),
|
||||||
|
2. each `STUPIDTOOLS_EXT` entry (colon-separated environment list),
|
||||||
|
3. the user directory: `$XDG_DATA_HOME/stupidtools/ext`, falling back
|
||||||
|
to `$HOME/.local/share/stupidtools/ext`,
|
||||||
|
4. this directory.
|
||||||
|
|
||||||
|
The location is the `STUPIDTOOLS_BUILTIN_EXT_DIR` macro — currently the
|
||||||
|
literal `builtin-ext`, resolved relative to the working directory of
|
||||||
|
the running binary (fine for the in-repo layout). When an installed
|
||||||
|
layout arrives, the build will override it at compile time, e.g.
|
||||||
|
`-DSTUPIDTOOLS_BUILTIN_EXT_DIR='"<prefix>/share/stupidtools/ext"'`, and
|
||||||
|
ship modules here.
|
||||||
|
|
||||||
|
## Why it is empty today
|
||||||
|
|
||||||
|
The builtin C and C++ language modules are compiled C code
|
||||||
|
(`src/ext/lang_c.c`, `src/ext/lang_cpp.c`), not Lua, so nothing needs
|
||||||
|
to live here yet. The first shipped Lua module arrives with the example
|
||||||
|
extension todo and could serve as a fixture for this location.
|
||||||
+25
-2
@@ -3,5 +3,28 @@
|
|||||||
AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic
|
AM_CFLAGS = -std=c23 -Wall -Wextra -Wpedantic
|
||||||
|
|
||||||
bin_PROGRAMS = stupidtools
|
bin_PROGRAMS = stupidtools
|
||||||
stupidtools_SOURCES = main.c cli.c
|
stupidtools_SOURCES = \
|
||||||
noinst_HEADERS = cli.h
|
main.c cli.c \
|
||||||
|
error.c span.c \
|
||||||
|
ext/discovery.c ext/lua.c ext/abi.c ext/lang_c.c ext/lang_cpp.c \
|
||||||
|
../thirdparty/lua/lapi.c ../thirdparty/lua/lauxlib.c \
|
||||||
|
../thirdparty/lua/lbaselib.c ../thirdparty/lua/lcode.c \
|
||||||
|
../thirdparty/lua/lctype.c ../thirdparty/lua/ldebug.c \
|
||||||
|
../thirdparty/lua/ldo.c ../thirdparty/lua/ldump.c \
|
||||||
|
../thirdparty/lua/lfunc.c ../thirdparty/lua/lgc.c \
|
||||||
|
../thirdparty/lua/llex.c ../thirdparty/lua/lmem.c \
|
||||||
|
../thirdparty/lua/lobject.c ../thirdparty/lua/lopcodes.c \
|
||||||
|
../thirdparty/lua/lparser.c ../thirdparty/lua/lstate.c \
|
||||||
|
../thirdparty/lua/lstring.c ../thirdparty/lua/ltable.c \
|
||||||
|
../thirdparty/lua/ltm.c ../thirdparty/lua/lundump.c \
|
||||||
|
../thirdparty/lua/lvm.c ../thirdparty/lua/lzio.c \
|
||||||
|
../thirdparty/lua/lstrlib.c ../thirdparty/lua/ltablib.c
|
||||||
|
|
||||||
|
# The vendored Lua subset above deliberately EXCLUDES linit.c,
|
||||||
|
# lmathlib.c, loadlib.c, liolib.c, loslib.c, ldblib.c, lcorolib.c,
|
||||||
|
# lutf8lib.c and the lua.c/luac.c mains -- see src/ext/lua.c's -lm note
|
||||||
|
# (the runtime's curated math/os tables replace lmathlib/loslib, and
|
||||||
|
# the six libm shims in lua.c keep the link free of -lm).
|
||||||
|
noinst_HEADERS = cli.h error.h span.h \
|
||||||
|
ext/discovery.h ext/lua.h ext/abi.h ext/lang_c.h ext/lang_cpp.h \
|
||||||
|
ext/lang_common.h
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ cli_print_usage(FILE *stream, const char *program)
|
|||||||
"Options:\n"
|
"Options:\n"
|
||||||
" -h, --help show this help and exit\n"
|
" -h, --help show this help and exit\n"
|
||||||
" -V, --version print version and exit\n"
|
" -V, --version print version and exit\n"
|
||||||
|
" --ext-dir DIR load extensions from DIR (repeatable; the\n"
|
||||||
|
" STUPIDTOOLS_EXT environment variable, a\n"
|
||||||
|
" colon-separated directory list, is honored too)\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Note: buildfile processing is not yet implemented.\n",
|
"Note: buildfile processing is not yet implemented.\n",
|
||||||
program);
|
program);
|
||||||
@@ -70,6 +73,7 @@ cli_parse(int argc, char **argv, struct cli_opts *opts)
|
|||||||
|
|
||||||
opts->program = program_name(argc > 0 ? argv[0] : NULL);
|
opts->program = program_name(argc > 0 ? argv[0] : NULL);
|
||||||
opts->buildfile = NULL;
|
opts->buildfile = NULL;
|
||||||
|
opts->ext_dir_count = 0;
|
||||||
|
|
||||||
for (i = 1; i < argc; i++) {
|
for (i = 1; i < argc; i++) {
|
||||||
const char *arg = argv[i];
|
const char *arg = argv[i];
|
||||||
@@ -101,6 +105,37 @@ cli_parse(int argc, char **argv, struct cli_opts *opts)
|
|||||||
return CLI_ACTION_VERSION;
|
return CLI_ACTION_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --ext-dir consumes the NEXT argument unconditionally
|
||||||
|
* (getopt-style; even if it starts with '-'), is repeatable,
|
||||||
|
* and rejects a missing/empty value and more than
|
||||||
|
* CLI_MAX_EXT_DIRS repetitions as usage errors. The value is
|
||||||
|
* borrowed from argv; a dir that does not exist is not an
|
||||||
|
* error -- discovery skips missing dirs silently. */
|
||||||
|
if (strcmp(arg, "--ext-dir") == 0) {
|
||||||
|
if (i + 1 >= argc) {
|
||||||
|
fprintf(stderr, "%s: option '--ext-dir' "
|
||||||
|
"requires an argument\n", opts->program);
|
||||||
|
cli_print_usage(stderr, opts->program);
|
||||||
|
return CLI_ACTION_ERROR;
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
if (argv[i][0] == '\0') {
|
||||||
|
fprintf(stderr, "%s: option '--ext-dir' "
|
||||||
|
"requires a non-empty directory\n",
|
||||||
|
opts->program);
|
||||||
|
cli_print_usage(stderr, opts->program);
|
||||||
|
return CLI_ACTION_ERROR;
|
||||||
|
}
|
||||||
|
if (opts->ext_dir_count >= CLI_MAX_EXT_DIRS) {
|
||||||
|
fprintf(stderr, "%s: too many --ext-dir options "
|
||||||
|
"(max %d)\n", opts->program, CLI_MAX_EXT_DIRS);
|
||||||
|
cli_print_usage(stderr, opts->program);
|
||||||
|
return CLI_ACTION_ERROR;
|
||||||
|
}
|
||||||
|
opts->ext_dirs[opts->ext_dir_count++] = argv[i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
fprintf(stderr, "%s: unrecognized option '%s'\n",
|
fprintf(stderr, "%s: unrecognized option '%s'\n",
|
||||||
opts->program, arg);
|
opts->program, arg);
|
||||||
cli_print_usage(stderr, opts->program);
|
cli_print_usage(stderr, opts->program);
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
#define CLI_EXIT_RUNTIME 1
|
#define CLI_EXIT_RUNTIME 1
|
||||||
#define CLI_EXIT_USAGE 2
|
#define CLI_EXIT_USAGE 2
|
||||||
|
|
||||||
|
/* Sane cap on repeated --ext-dir flags (todo 20). */
|
||||||
|
#define CLI_MAX_EXT_DIRS 32
|
||||||
|
|
||||||
/* What the caller should do after parsing argv. */
|
/* What the caller should do after parsing argv. */
|
||||||
enum cli_action {
|
enum cli_action {
|
||||||
CLI_ACTION_RUN, /* proceed with the parsed buildfile (todo 16+) */
|
CLI_ACTION_RUN, /* proceed with the parsed buildfile (todo 16+) */
|
||||||
@@ -28,6 +31,11 @@ enum cli_action {
|
|||||||
struct cli_opts {
|
struct cli_opts {
|
||||||
const char *program; /* argv[0] as given (or a default) */
|
const char *program; /* argv[0] as given (or a default) */
|
||||||
const char *buildfile; /* single positional buildfile, NULL if none */
|
const char *buildfile; /* single positional buildfile, NULL if none */
|
||||||
|
/* --ext-dir values, in the order given; the pointers are borrowed
|
||||||
|
* from argv. STUPIDTOOLS_EXT is NOT merged here -- discovery reads
|
||||||
|
* the environment itself and appends those dirs after these. */
|
||||||
|
const char *ext_dirs[CLI_MAX_EXT_DIRS];
|
||||||
|
size_t ext_dir_count;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum cli_action cli_parse(int argc, char **argv, struct cli_opts *opts);
|
enum cli_action cli_parse(int argc, char **argv, struct cli_opts *opts);
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
/*
|
||||||
|
* src/ext/discovery.c - extension discovery and loading (plan todo 20).
|
||||||
|
*
|
||||||
|
* See src/ext/discovery.h for the full contract: the discovery order
|
||||||
|
* (builtin C modules -> --ext-dir -> STUPIDTOOLS_EXT -> user dir ->
|
||||||
|
* builtin dir), the per-directory scan rules (regular *.lua files,
|
||||||
|
* bytewise order, missing dirs are no-ops), and the FAIL-FAST error
|
||||||
|
* contract ("extension load failed: <path>: ...").
|
||||||
|
*
|
||||||
|
* Everything loads through the sandboxed Lua runtime (st_lua_run); the
|
||||||
|
* builtin C/C++ language modules go through st_ext_init_builtins() into
|
||||||
|
* the ABI ctx. No language is hardcoded here.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2026 huntedbytheirs
|
||||||
|
* SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L /* strdup, strtok_r, stat, readdir */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#include "abi.h"
|
||||||
|
#include "discovery.h"
|
||||||
|
#include "lua.h"
|
||||||
|
|
||||||
|
/* Compile-time override for the shipped builtin extension directory;
|
||||||
|
* the default is resolved relative to the current working directory
|
||||||
|
* (repo-layout friendly -- see builtin-ext/README.md). An installed
|
||||||
|
* layout overrides it, e.g.
|
||||||
|
* -DSTUPIDTOOLS_BUILTIN_EXT_DIR='"<prefix>/share/stupidtools/ext"'
|
||||||
|
*/
|
||||||
|
#ifndef STUPIDTOOLS_BUILTIN_EXT_DIR
|
||||||
|
#define STUPIDTOOLS_BUILTIN_EXT_DIR "builtin-ext"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* ---------------- logging ---------------- */
|
||||||
|
|
||||||
|
/* NULL -> default stderr logger. The one piece of module state; the
|
||||||
|
* task explicitly asks for a minimal hook, and threading a logger
|
||||||
|
* parameter through the whole chain would churn the public signature. */
|
||||||
|
static st_ext_log_fn g_log_fn;
|
||||||
|
|
||||||
|
void
|
||||||
|
st_ext_discover_set_log(st_ext_log_fn fn)
|
||||||
|
{
|
||||||
|
g_log_fn = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
st_ext_discover_log(const char *fmt, ...)
|
||||||
|
{
|
||||||
|
va_list ap;
|
||||||
|
|
||||||
|
va_start(ap, fmt);
|
||||||
|
if (g_log_fn != NULL) {
|
||||||
|
g_log_fn(fmt, ap);
|
||||||
|
} else {
|
||||||
|
vfprintf(stderr, fmt, ap);
|
||||||
|
}
|
||||||
|
va_end(ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- small helpers ---------------- */
|
||||||
|
|
||||||
|
/* Comma-joined names of registrations [from, count) via the accessor;
|
||||||
|
* malloc'd, NULL when the range is empty or on allocation failure. */
|
||||||
|
static char *
|
||||||
|
names_join(const struct st_lua_rt *rt, size_t from, size_t count,
|
||||||
|
const char *(*name_at)(const struct st_lua_rt *, size_t))
|
||||||
|
{
|
||||||
|
size_t i, total = 0;
|
||||||
|
char *buf, *p;
|
||||||
|
|
||||||
|
if (count <= from) return NULL;
|
||||||
|
for (i = from; i < count; i++) {
|
||||||
|
const char *n = name_at(rt, i);
|
||||||
|
if (n != NULL) total += strlen(n) + 2; /* ", " separator */
|
||||||
|
}
|
||||||
|
if (total == 0) return NULL;
|
||||||
|
buf = malloc(total + 1);
|
||||||
|
if (buf == NULL) return NULL;
|
||||||
|
p = buf;
|
||||||
|
for (i = from; i < count; i++) {
|
||||||
|
const char *n = name_at(rt, i);
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
if (n == NULL) continue;
|
||||||
|
len = strlen(n);
|
||||||
|
if (p != buf) {
|
||||||
|
*p++ = ',';
|
||||||
|
*p++ = ' ';
|
||||||
|
}
|
||||||
|
memcpy(p, n, len);
|
||||||
|
p += len;
|
||||||
|
}
|
||||||
|
*p = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* "c, cxx" style list of the ABI ctx's language names (malloc'd). */
|
||||||
|
static char *
|
||||||
|
abi_language_names(const struct st_ext_ctx *ctx)
|
||||||
|
{
|
||||||
|
size_t i, n = st_ext_language_count(ctx), total = 0;
|
||||||
|
char *buf, *p;
|
||||||
|
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
const char *nm = st_ext_language_name(ctx, i);
|
||||||
|
if (nm != NULL) total += strlen(nm) + 2;
|
||||||
|
}
|
||||||
|
if (total == 0) return NULL;
|
||||||
|
buf = malloc(total + 1);
|
||||||
|
if (buf == NULL) return NULL;
|
||||||
|
p = buf;
|
||||||
|
for (i = 0; i < n; i++) {
|
||||||
|
const char *nm = st_ext_language_name(ctx, i);
|
||||||
|
size_t len;
|
||||||
|
|
||||||
|
if (nm == NULL) continue;
|
||||||
|
len = strlen(nm);
|
||||||
|
if (p != buf) {
|
||||||
|
*p++ = ',';
|
||||||
|
*p++ = ' ';
|
||||||
|
}
|
||||||
|
memcpy(p, nm, len);
|
||||||
|
p += len;
|
||||||
|
}
|
||||||
|
*p = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reads a whole file into a freshly malloc'd NUL-terminated buffer.
|
||||||
|
* NULL on success (buffer in *out, length in *out_len); an st_error on
|
||||||
|
* failure (I/O -> ST_ERR_IO naming the file, OOM -> ST_ERR_INTERNAL). */
|
||||||
|
static struct st_error *
|
||||||
|
read_module_source(const char *path, char **out, size_t *out_len)
|
||||||
|
{
|
||||||
|
FILE *f;
|
||||||
|
long size;
|
||||||
|
char *buf;
|
||||||
|
size_t got;
|
||||||
|
size_t need;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
f = fopen(path, "rb");
|
||||||
|
if (f == NULL) goto io_fail;
|
||||||
|
if (fseek(f, 0, SEEK_END) != 0) {
|
||||||
|
(void)fclose(f);
|
||||||
|
goto io_fail;
|
||||||
|
}
|
||||||
|
size = ftell(f);
|
||||||
|
if (size < 0 || fseek(f, 0, SEEK_SET) != 0) {
|
||||||
|
(void)fclose(f);
|
||||||
|
goto io_fail;
|
||||||
|
}
|
||||||
|
buf = malloc((size_t)size + 1);
|
||||||
|
if (buf == NULL) {
|
||||||
|
(void)fclose(f);
|
||||||
|
return st_error_internal("extension load failed: out of memory");
|
||||||
|
}
|
||||||
|
got = fread(buf, 1, (size_t)size, f);
|
||||||
|
if (got != (size_t)size || fclose(f) != 0) {
|
||||||
|
free(buf);
|
||||||
|
goto io_fail;
|
||||||
|
}
|
||||||
|
buf[size] = '\0';
|
||||||
|
*out = buf;
|
||||||
|
*out_len = (size_t)size;
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
io_fail:
|
||||||
|
need = strlen(path) + 64;
|
||||||
|
buf = malloc(need);
|
||||||
|
if (buf == NULL)
|
||||||
|
return st_error_internal("extension load failed: out of memory");
|
||||||
|
(void)snprintf(buf, need, "extension load failed: %s: %s", path,
|
||||||
|
strerror(errno));
|
||||||
|
err = st_error_io(buf);
|
||||||
|
free(buf);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Wraps the runtime's error so the message names the module file. The
|
||||||
|
* category is preserved (sandbox violations stay ST_ERR_KDL_SCHEMA so
|
||||||
|
* st_error_print keeps its [kdl-schema] marker); the runtime's own
|
||||||
|
* message already carries "<file>:<line>: " via chunkname propagation. */
|
||||||
|
static struct st_error *
|
||||||
|
module_load_error(struct st_error *inner, const char *path)
|
||||||
|
{
|
||||||
|
enum st_error_category cat = st_error_category_of(inner);
|
||||||
|
const char *msg = st_error_message(inner);
|
||||||
|
size_t need = strlen(path) + (msg != NULL ? strlen(msg) : 12) + 32;
|
||||||
|
char *buf = malloc(need);
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
if (buf == NULL) {
|
||||||
|
st_error_free(inner);
|
||||||
|
return st_error_internal("extension load failed: out of memory");
|
||||||
|
}
|
||||||
|
(void)snprintf(buf, need, "extension load failed: %s: %s", path,
|
||||||
|
msg != NULL ? msg : "(no message)");
|
||||||
|
err = st_error_new(cat, buf);
|
||||||
|
free(buf);
|
||||||
|
st_error_free(inner);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logs one loaded module with the registrations it added, computed from
|
||||||
|
* the runtime's counts before/after the load (registrations only ever
|
||||||
|
* append, so the additions are exactly [before, after)). */
|
||||||
|
static void
|
||||||
|
log_module(struct st_lua_rt *rt, const char *path, size_t checks_before,
|
||||||
|
size_t langs_before)
|
||||||
|
{
|
||||||
|
size_t checks_after = st_lua_check_count(rt);
|
||||||
|
size_t langs_after = st_lua_language_count(rt);
|
||||||
|
char *ck = names_join(rt, checks_before, checks_after, st_lua_check_name);
|
||||||
|
char *lg = names_join(rt, langs_before, langs_after,
|
||||||
|
st_lua_language_name);
|
||||||
|
|
||||||
|
if (ck != NULL && lg != NULL) {
|
||||||
|
st_ext_discover_log("loaded extension: %s — checks: %zu (%s) "
|
||||||
|
"languages: %zu (%s)\n", path,
|
||||||
|
checks_after - checks_before, ck,
|
||||||
|
langs_after - langs_before, lg);
|
||||||
|
} else if (ck != NULL) {
|
||||||
|
st_ext_discover_log("loaded extension: %s — checks: %zu (%s) "
|
||||||
|
"languages: %zu\n", path, checks_after - checks_before, ck,
|
||||||
|
langs_after - langs_before);
|
||||||
|
} else if (lg != NULL) {
|
||||||
|
st_ext_discover_log("loaded extension: %s — checks: %zu "
|
||||||
|
"languages: %zu (%s)\n", path, checks_after - checks_before,
|
||||||
|
langs_after - langs_before, lg);
|
||||||
|
} else {
|
||||||
|
st_ext_discover_log("loaded extension: %s — checks: %zu "
|
||||||
|
"languages: %zu\n", path, checks_after - checks_before,
|
||||||
|
langs_after - langs_before);
|
||||||
|
}
|
||||||
|
free(ck);
|
||||||
|
free(lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logs the builtin C modules from the ACTUAL ABI registrations. */
|
||||||
|
static void
|
||||||
|
log_builtins(const struct st_ext_ctx *ctx)
|
||||||
|
{
|
||||||
|
char *names = abi_language_names(ctx);
|
||||||
|
|
||||||
|
if (names != NULL) {
|
||||||
|
st_ext_discover_log("loaded builtin modules: languages: %s\n",
|
||||||
|
names);
|
||||||
|
free(names);
|
||||||
|
} else {
|
||||||
|
st_ext_discover_log("loaded builtin modules\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Loads one module file into the sandbox. NULL on success; on failure a
|
||||||
|
* wrapped error naming the file (the fail-fast contract). */
|
||||||
|
static struct st_error *
|
||||||
|
load_module(struct st_lua_rt *rt, const char *path)
|
||||||
|
{
|
||||||
|
char *src = NULL;
|
||||||
|
size_t len = 0;
|
||||||
|
size_t c_before = st_lua_check_count(rt);
|
||||||
|
size_t l_before = st_lua_language_count(rt);
|
||||||
|
struct st_error *err = read_module_source(path, &src, &len);
|
||||||
|
|
||||||
|
if (err != NULL) return err;
|
||||||
|
err = st_lua_run(rt, src, path); /* chunkname = full path */
|
||||||
|
free(src);
|
||||||
|
if (err != NULL) return module_load_error(err, path);
|
||||||
|
log_module(rt, path, c_before, l_before);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
cmp_str(const void *a, const void *b)
|
||||||
|
{
|
||||||
|
const char *const *sa = a;
|
||||||
|
const char *const *sb = b;
|
||||||
|
|
||||||
|
return strcmp(*sa, *sb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scans one directory for *.lua modules and loads them in bytewise
|
||||||
|
* order. NULL on success; a missing directory is a documented no-op. */
|
||||||
|
static struct st_error *
|
||||||
|
discover_dir(struct st_lua_rt *rt, const char *dir)
|
||||||
|
{
|
||||||
|
DIR *d = opendir(dir);
|
||||||
|
struct dirent *e;
|
||||||
|
char **names = NULL;
|
||||||
|
size_t n = 0, cap = 0, i;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
|
||||||
|
if (d == NULL) return NULL; /* missing dir: silent no-op */
|
||||||
|
while ((e = readdir(d)) != NULL) {
|
||||||
|
size_t nlen;
|
||||||
|
|
||||||
|
if (e->d_name[0] == '.') continue; /* ., .. and dotfiles */
|
||||||
|
nlen = strlen(e->d_name);
|
||||||
|
if (nlen < 4 || strcmp(e->d_name + nlen - 4, ".lua") != 0)
|
||||||
|
continue;
|
||||||
|
if (n == cap) {
|
||||||
|
size_t ncap = cap == 0 ? 16 : cap * 2;
|
||||||
|
char **nn = realloc(names, ncap * sizeof *nn);
|
||||||
|
|
||||||
|
if (nn == NULL) {
|
||||||
|
err = st_error_internal("extension load failed: "
|
||||||
|
"out of memory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
names = nn;
|
||||||
|
cap = ncap;
|
||||||
|
}
|
||||||
|
names[n] = strdup(e->d_name);
|
||||||
|
if (names[n] == NULL) {
|
||||||
|
err = st_error_internal("extension load failed: "
|
||||||
|
"out of memory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
(void)closedir(d);
|
||||||
|
if (err != NULL) {
|
||||||
|
for (i = 0; i < n; i++) free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
qsort(names, n, sizeof *names, cmp_str);
|
||||||
|
for (i = 0; i < n && err == NULL; i++) {
|
||||||
|
char *path;
|
||||||
|
struct stat st;
|
||||||
|
size_t dlen = strlen(dir);
|
||||||
|
size_t nlen = strlen(names[i]);
|
||||||
|
|
||||||
|
while (dlen > 0 && dir[dlen - 1] == '/') dlen--;
|
||||||
|
path = malloc(dlen + 1 + nlen + 1);
|
||||||
|
if (path == NULL) {
|
||||||
|
err = st_error_internal("extension load failed: "
|
||||||
|
"out of memory");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
memcpy(path, dir, dlen);
|
||||||
|
path[dlen] = '/';
|
||||||
|
memcpy(path + dlen + 1, names[i], nlen + 1);
|
||||||
|
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
||||||
|
/* vanished between readdir and stat, or non-regular
|
||||||
|
* (a directory named *.lua etc.): skip */
|
||||||
|
free(path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
err = load_module(rt, path);
|
||||||
|
free(path);
|
||||||
|
}
|
||||||
|
for (i = 0; i < n; i++) free(names[i]);
|
||||||
|
free(names);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* STUPIDTOOLS_EXT: colon-separated directory list (no escaping); empty
|
||||||
|
* entries are skipped so a trailing colon is harmless. */
|
||||||
|
static struct st_error *
|
||||||
|
discover_env_dirs(struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
const char *env = getenv("STUPIDTOOLS_EXT");
|
||||||
|
char *copy;
|
||||||
|
char *save = NULL;
|
||||||
|
char *tok;
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
|
||||||
|
if (env == NULL || env[0] == '\0') return NULL;
|
||||||
|
copy = strdup(env);
|
||||||
|
if (copy == NULL)
|
||||||
|
return st_error_internal("extension load failed: out of memory");
|
||||||
|
for (tok = strtok_r(copy, ":", &save); tok != NULL && err == NULL;
|
||||||
|
tok = strtok_r(NULL, ":", &save)) {
|
||||||
|
if (tok[0] != '\0') err = discover_dir(rt, tok);
|
||||||
|
}
|
||||||
|
free(copy);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User extension dir: $XDG_DATA_HOME/stupidtools/ext (canonical), else
|
||||||
|
* $HOME/.local/share/stupidtools/ext; skipped when neither is usable. */
|
||||||
|
static struct st_error *
|
||||||
|
discover_user_dir(struct st_lua_rt *rt)
|
||||||
|
{
|
||||||
|
const char *xdg = getenv("XDG_DATA_HOME");
|
||||||
|
const char *home;
|
||||||
|
char *dir;
|
||||||
|
size_t need;
|
||||||
|
struct st_error *err;
|
||||||
|
|
||||||
|
if (xdg == NULL || xdg[0] == '\0') {
|
||||||
|
home = getenv("HOME");
|
||||||
|
if (home == NULL || home[0] == '\0') return NULL;
|
||||||
|
need = strlen(home) + sizeof "/.local/share/stupidtools/ext";
|
||||||
|
dir = malloc(need);
|
||||||
|
if (dir == NULL)
|
||||||
|
return st_error_internal("extension load failed: "
|
||||||
|
"out of memory");
|
||||||
|
(void)snprintf(dir, need, "%s/.local/share/stupidtools/ext",
|
||||||
|
home);
|
||||||
|
} else {
|
||||||
|
need = strlen(xdg) + sizeof "/stupidtools/ext";
|
||||||
|
dir = malloc(need);
|
||||||
|
if (dir == NULL)
|
||||||
|
return st_error_internal("extension load failed: "
|
||||||
|
"out of memory");
|
||||||
|
(void)snprintf(dir, need, "%s/stupidtools/ext", xdg);
|
||||||
|
}
|
||||||
|
err = discover_dir(rt, dir);
|
||||||
|
free(dir);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- public entry point ---------------- */
|
||||||
|
|
||||||
|
struct st_error *
|
||||||
|
st_ext_discover(struct st_ext_ctx *ctx, struct st_lua_rt *rt,
|
||||||
|
const char *const *extra_dirs, size_t n_dirs)
|
||||||
|
{
|
||||||
|
struct st_error *err;
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
if (rt == NULL) return st_error_internal("st_ext_discover: NULL runtime");
|
||||||
|
if (ctx == NULL) return st_error_usage("st_ext_discover: NULL extension context");
|
||||||
|
|
||||||
|
/* Builtin C/C++ language modules first -- the foundation; the
|
||||||
|
* builtin-ext DIRECTORY is scanned below with the other dirs. */
|
||||||
|
err = st_ext_init_builtins(ctx);
|
||||||
|
if (err != NULL) return err;
|
||||||
|
log_builtins(ctx);
|
||||||
|
|
||||||
|
for (i = 0; i < n_dirs && err == NULL; i++) {
|
||||||
|
if (extra_dirs[i] != NULL && extra_dirs[i][0] != '\0')
|
||||||
|
err = discover_dir(rt, extra_dirs[i]);
|
||||||
|
}
|
||||||
|
if (err != NULL) return err;
|
||||||
|
|
||||||
|
err = discover_env_dirs(rt);
|
||||||
|
if (err != NULL) return err;
|
||||||
|
|
||||||
|
err = discover_user_dir(rt);
|
||||||
|
if (err != NULL) return err;
|
||||||
|
|
||||||
|
return discover_dir(rt, STUPIDTOOLS_BUILTIN_EXT_DIR);
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#ifndef ST_EXT_DISCOVERY_H
|
||||||
|
#define ST_EXT_DISCOVERY_H
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Extension discovery and loading (plan todo 20).
|
||||||
|
*
|
||||||
|
* st_ext_discover() loads every extension known to a stupidtools run
|
||||||
|
* into a freshly created ABI context + sandboxed Lua runtime. Search
|
||||||
|
* order:
|
||||||
|
*
|
||||||
|
* 1. the BUILTIN C language modules -- st_ext_init_builtins() puts
|
||||||
|
* the "c"/"cxx" languages (and their checks) into the ABI ctx;
|
||||||
|
* 2. each --ext-dir directory (extra_dirs, in the given order);
|
||||||
|
* 3. each STUPIDTOOLS_EXT directory (colon-separated, empty entries
|
||||||
|
* skipped);
|
||||||
|
* 4. the USER directory: $XDG_DATA_HOME/stupidtools/ext, falling
|
||||||
|
* back to $HOME/.local/share/stupidtools/ext when XDG_DATA_HOME is
|
||||||
|
* unset or empty; skipped entirely when neither variable yields a
|
||||||
|
* path. XDG_DATA_HOME is the canonical convention.
|
||||||
|
* 5. the BUILTIN directory: the STUPIDTOOLS_BUILTIN_EXT_DIR macro
|
||||||
|
* (default "builtin-ext", resolved relative to the current working
|
||||||
|
* directory of the running binary; an installed layout overrides
|
||||||
|
* it at compile time, e.g.
|
||||||
|
* -DSTUPIDTOOLS_BUILTIN_EXT_DIR='"<prefix>/share/stupidtools/ext"').
|
||||||
|
*
|
||||||
|
* Directory scanning contract:
|
||||||
|
* - only REGULAR files whose name ends in ".lua" are loaded (dotfiles
|
||||||
|
* excluded). Entries arrive from readdir() as bare basenames -- no
|
||||||
|
* directory entry can contain a path separator, so a malicious
|
||||||
|
* name like "../evil.lua" cannot occur, let alone escape the dir.
|
||||||
|
* Symlinks are followed (stat); pointing one at a file outside the
|
||||||
|
* directory is the caller's own responsibility in an explicitly
|
||||||
|
* configured dir.
|
||||||
|
* - within a directory, modules load in bytewise-lexicographic order
|
||||||
|
* (deterministic, independent of readdir order);
|
||||||
|
* - a directory that does not exist is silently skipped (a no-op --
|
||||||
|
* env lists routinely contain stale paths); an empty directory is
|
||||||
|
* likewise a no-op; an EMPTY .lua file loads as an empty chunk and
|
||||||
|
* is a clean no-op;
|
||||||
|
* - each module is read into memory and run via st_lua_run() with the
|
||||||
|
* full path as chunkname, so Lua diagnostics name file AND line.
|
||||||
|
* Note: st_lua_run() compiles strlen(chunk) bytes, so a file with
|
||||||
|
* embedded NULs is truncated at the first NUL before compilation;
|
||||||
|
* binary garbage then fails with a syntax error naming the file.
|
||||||
|
*
|
||||||
|
* Failure contract: FAIL-FAST. The first module that fails to load
|
||||||
|
* (syntax error, runtime error, sandbox violation, unreadable file)
|
||||||
|
* aborts discovery; the returned st_error's message starts with
|
||||||
|
* "extension load failed: <path>: ..." and preserves the underlying
|
||||||
|
* category (sandbox violations stay ST_ERR_KDL_SCHEMA, other Lua
|
||||||
|
* failures ST_ERR_INTERNAL, read failures ST_ERR_IO). Later modules
|
||||||
|
* are NOT attempted. Deliberate choice: a broken extension should be
|
||||||
|
* loud, and a half-loaded extension set is a confusing state.
|
||||||
|
*
|
||||||
|
* Lifecycle notes:
|
||||||
|
* - st_ext_discover() calls st_ext_init_builtins() itself; call it
|
||||||
|
* ONCE per ctx (a second call re-registers the builtins and fails
|
||||||
|
* on duplicate registrations);
|
||||||
|
* - the ABI ctx receives the builtin modules only; Lua registrations
|
||||||
|
* stay in the runtime's own registries (st_lua_check_* /
|
||||||
|
* st_lua_language_*) -- todo 21 bridges them into the ctx.
|
||||||
|
*
|
||||||
|
* Logging: st_ext_discover_log() prints to stderr by default;
|
||||||
|
* st_ext_discover_set_log() installs a vfprintf-style callback (NULL
|
||||||
|
* restores the default). Discovery logs one line per loaded module:
|
||||||
|
*
|
||||||
|
* loaded extension: <path> — checks: <n> (<names>) languages: <m> (<names>)
|
||||||
|
*
|
||||||
|
* where the counts and names come from the ACTUAL registrations the
|
||||||
|
* module performed (enumerated from the runtime), never from the file
|
||||||
|
* name alone. The builtin modules log one line of the same shape.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
#include "error.h"
|
||||||
|
|
||||||
|
struct st_ext_ctx;
|
||||||
|
struct st_lua_rt;
|
||||||
|
|
||||||
|
/* vfprintf-style log sink (NULL restores the default stderr logger). */
|
||||||
|
typedef void (*st_ext_log_fn)(const char *fmt, va_list ap);
|
||||||
|
|
||||||
|
void st_ext_discover_set_log(st_ext_log_fn fn);
|
||||||
|
void st_ext_discover_log(const char *fmt, ...);
|
||||||
|
|
||||||
|
/* Returns NULL on success. `extra_dirs` are the --ext-dir values (may
|
||||||
|
* be NULL/0); see the header contract for the full discovery order and
|
||||||
|
* the fail-fast failure semantics. */
|
||||||
|
struct st_error *st_ext_discover(struct st_ext_ctx *ctx, struct st_lua_rt *rt,
|
||||||
|
const char *const *extra_dirs,
|
||||||
|
size_t n_dirs);
|
||||||
|
|
||||||
|
#endif /* ST_EXT_DISCOVERY_H */
|
||||||
+59
@@ -7,17 +7,68 @@
|
|||||||
* Entrypoint: parse argv via cli.c, dispatch --help/--version/usage
|
* Entrypoint: parse argv via cli.c, dispatch --help/--version/usage
|
||||||
* errors, and reserve the `stupidtools <buildfile>` shape. Buildfile
|
* errors, and reserve the `stupidtools <buildfile>` shape. Buildfile
|
||||||
* processing arrives in later todos (16+).
|
* processing arrives in later todos (16+).
|
||||||
|
*
|
||||||
|
* Interim extension pipeline (todo 20): with --ext-dir flags or a
|
||||||
|
* STUPIDTOOLS_EXT environment, run extension discovery, print what
|
||||||
|
* loaded, and exit 0 -- until todo 23 folds discovery into the full
|
||||||
|
* buildfile pipeline.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "cli.h"
|
#include "cli.h"
|
||||||
|
|
||||||
|
#include "ext/abi.h"
|
||||||
|
#include "ext/discovery.h"
|
||||||
|
#include "ext/lua.h"
|
||||||
|
|
||||||
|
#include <stdarg.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
/* Discovery's log sink: route the "loaded extension: ..." lines to
|
||||||
|
* stdout so scripts can grep them cleanly. */
|
||||||
|
static void
|
||||||
|
stdout_log(const char *fmt, va_list ap)
|
||||||
|
{
|
||||||
|
vfprintf(stdout, fmt, ap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Creates a fresh ctx + runtime, discovers everything (builtin modules
|
||||||
|
* first, then the --ext-dir dirs, STUPIDTOOLS_EXT, the user dir and the
|
||||||
|
* builtin dir -- see ext/discovery.h), and prints each loaded module.
|
||||||
|
* Returns CLI_EXIT_OK on success; a load error prints its diagnostic to
|
||||||
|
* stderr and returns CLI_EXIT_RUNTIME (fail-fast contract). */
|
||||||
|
static int
|
||||||
|
run_extension_discovery(const struct cli_opts *opts)
|
||||||
|
{
|
||||||
|
struct st_ext_ctx *ctx = st_ext_ctx_new();
|
||||||
|
struct st_lua_rt *rt = st_lua_rt_new();
|
||||||
|
struct st_error *err = NULL;
|
||||||
|
int rc = CLI_EXIT_RUNTIME;
|
||||||
|
|
||||||
|
if (ctx == NULL || rt == NULL) {
|
||||||
|
fprintf(stderr, "%s: error: out of memory\n", opts->program);
|
||||||
|
} else {
|
||||||
|
st_ext_discover_set_log(stdout_log);
|
||||||
|
err = st_ext_discover(ctx, rt, opts->ext_dirs,
|
||||||
|
opts->ext_dir_count);
|
||||||
|
if (err != NULL) {
|
||||||
|
st_error_print(stderr, err);
|
||||||
|
st_error_free(err);
|
||||||
|
} else {
|
||||||
|
rc = CLI_EXIT_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char **argv)
|
main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
struct cli_opts opts;
|
struct cli_opts opts;
|
||||||
enum cli_action action;
|
enum cli_action action;
|
||||||
|
const char *env;
|
||||||
|
|
||||||
action = cli_parse(argc, argv, &opts);
|
action = cli_parse(argc, argv, &opts);
|
||||||
|
|
||||||
@@ -33,6 +84,14 @@ main(int argc, char **argv)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.ext_dir_count > 0) {
|
||||||
|
return run_extension_discovery(&opts);
|
||||||
|
}
|
||||||
|
env = getenv("STUPIDTOOLS_EXT");
|
||||||
|
if (env != NULL && env[0] != '\0') {
|
||||||
|
return run_extension_discovery(&opts);
|
||||||
|
}
|
||||||
|
|
||||||
if (opts.buildfile == NULL) {
|
if (opts.buildfile == NULL) {
|
||||||
fprintf(stderr, "%s: error: no build file given\n",
|
fprintf(stderr, "%s: error: no build file given\n",
|
||||||
opts.program);
|
opts.program);
|
||||||
|
|||||||
@@ -0,0 +1,561 @@
|
|||||||
|
/* LINK: ../../src/ext/discovery.c ../../src/ext/lua.c ../../src/ext/abi.c ../../src/ext/lang_c.c ../../src/ext/lang_cpp.c ../../src/error.c ../../src/span.c ../../thirdparty/lua/lapi.c ../../thirdparty/lua/lauxlib.c ../../thirdparty/lua/lbaselib.c ../../thirdparty/lua/lcode.c ../../thirdparty/lua/lctype.c ../../thirdparty/lua/ldebug.c ../../thirdparty/lua/ldo.c ../../thirdparty/lua/ldump.c ../../thirdparty/lua/lfunc.c ../../thirdparty/lua/lgc.c ../../thirdparty/lua/llex.c ../../thirdparty/lua/lmem.c ../../thirdparty/lua/lobject.c ../../thirdparty/lua/lopcodes.c ../../thirdparty/lua/lparser.c ../../thirdparty/lua/lstate.c ../../thirdparty/lua/lstring.c ../../thirdparty/lua/ltable.c ../../thirdparty/lua/ltm.c ../../thirdparty/lua/lundump.c ../../thirdparty/lua/lvm.c ../../thirdparty/lua/lzio.c ../../thirdparty/lua/lstrlib.c ../../thirdparty/lua/ltablib.c */
|
||||||
|
|
||||||
|
#ifndef _POSIX_C_SOURCE
|
||||||
|
#define _POSIX_C_SOURCE 200809L /* mkdtemp, strdup */
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* tests/unit/test_discovery.c
|
||||||
|
*
|
||||||
|
* Unit tests for extension discovery and loading (src/ext/discovery.h,
|
||||||
|
* plan todo 20).
|
||||||
|
*
|
||||||
|
* The magic LINK comment on line 1 is REQUIRED by tests/run.sh: it
|
||||||
|
* lists the extra .c sources compiled into this test binary. It links
|
||||||
|
* discovery.c plus everything it needs -- the sandboxed Lua runtime
|
||||||
|
* (lua.c + the same 27 vendored Lua sources as test_lua.c, minus
|
||||||
|
* linit/lmathlib/loadlib/liolib/loslib/lua.c/luac.c, see the -lm note
|
||||||
|
* there), the extension ABI + builtin C/C++ modules, and error/span.
|
||||||
|
*
|
||||||
|
* Environment isolation: every test points XDG_DATA_HOME at an empty
|
||||||
|
* temp dir and unsets STUPIDTOOLS_EXT first, so host state can never
|
||||||
|
* leak into discovery. The builtin directory (builtin-ext relative to
|
||||||
|
* the cwd) ships no .lua modules in this todo, so it is a no-op as
|
||||||
|
* well; note that exact runtime-count assertions below assume that.
|
||||||
|
*/
|
||||||
|
#include "munit.h"
|
||||||
|
|
||||||
|
#include "ext/abi.h"
|
||||||
|
#include "ext/discovery.h"
|
||||||
|
#include "ext/lua.h"
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* ---------- filesystem helpers ---------- */
|
||||||
|
|
||||||
|
static char *
|
||||||
|
make_temp_dir(void)
|
||||||
|
{
|
||||||
|
char *d = strdup("/tmp/st_discovery_XXXXXX");
|
||||||
|
|
||||||
|
if (d == NULL || mkdtemp(d) == NULL) {
|
||||||
|
free(d);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
write_file(const char *dir, const char *name, const char *content)
|
||||||
|
{
|
||||||
|
char path[1024];
|
||||||
|
FILE *f;
|
||||||
|
size_t len = strlen(content);
|
||||||
|
|
||||||
|
if (snprintf(path, sizeof path, "%s/%s", dir, name) >=
|
||||||
|
(int)sizeof path) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
f = fopen(path, "wb");
|
||||||
|
if (f == NULL) return -1;
|
||||||
|
if (fwrite(content, 1, len, f) != len) {
|
||||||
|
(void)fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return fclose(f) == 0 ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
write_bytes(const char *dir, const char *name, const unsigned char *bytes,
|
||||||
|
size_t len)
|
||||||
|
{
|
||||||
|
char path[1024];
|
||||||
|
FILE *f;
|
||||||
|
|
||||||
|
if (snprintf(path, sizeof path, "%s/%s", dir, name) >=
|
||||||
|
(int)sizeof path) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
f = fopen(path, "wb");
|
||||||
|
if (f == NULL) return -1;
|
||||||
|
if (fwrite(bytes, 1, len, f) != len) {
|
||||||
|
(void)fclose(f);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return fclose(f) == 0 ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Recursive delete (plain POSIX: unlink files, rmdir dirs). */
|
||||||
|
static void
|
||||||
|
rm_rf(const char *path)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
DIR *d;
|
||||||
|
struct dirent *e;
|
||||||
|
|
||||||
|
if (lstat(path, &st) != 0) return;
|
||||||
|
if (!S_ISDIR(st.st_mode)) {
|
||||||
|
(void)unlink(path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
d = opendir(path);
|
||||||
|
if (d == NULL) return;
|
||||||
|
while ((e = readdir(d)) != NULL) {
|
||||||
|
char sub[1024];
|
||||||
|
|
||||||
|
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
if (snprintf(sub, sizeof sub, "%s/%s", path, e->d_name) >=
|
||||||
|
(int)sizeof sub)
|
||||||
|
continue;
|
||||||
|
rm_rf(sub);
|
||||||
|
}
|
||||||
|
(void)closedir(d);
|
||||||
|
(void)rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fresh ctx + rt pair for one discovery run (discovery is one-shot per
|
||||||
|
* ctx: it re-runs the builtin init, so a second call would fail on
|
||||||
|
* duplicate registrations). */
|
||||||
|
static int
|
||||||
|
fresh_pair(struct st_ext_ctx **ctx, struct st_lua_rt **rt)
|
||||||
|
{
|
||||||
|
*ctx = st_ext_ctx_new();
|
||||||
|
*rt = st_lua_rt_new();
|
||||||
|
return *ctx != NULL && *rt != NULL ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (a) a valid module registers into the runtime ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_valid_module(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_file(ext, "hello.lua",
|
||||||
|
"st.register_check(\"magic\")\n"
|
||||||
|
"st.register_language(\"fortran\")\n"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_null(err);
|
||||||
|
|
||||||
|
/* the module's registrations are enumerable from the runtime */
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 1);
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 0), "magic");
|
||||||
|
munit_assert_size(st_lua_language_count(rt), ==, 1);
|
||||||
|
munit_assert_string_equal(st_lua_language_name(rt, 0), "fortran");
|
||||||
|
|
||||||
|
/* the builtin C/C++ modules landed in the ABI ctx */
|
||||||
|
munit_assert_size(st_ext_language_count(ctx), ==, 2);
|
||||||
|
munit_assert_string_equal(st_ext_language_name(ctx, 0), "c");
|
||||||
|
munit_assert_string_equal(st_ext_language_name(ctx, 1), "cxx");
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (b) malformed module: clear error naming the file, fail-fast ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_malformed_fail_fast(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *msg;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_file(ext, "bad.lua", "local x =\n"), ==, 0);
|
||||||
|
munit_assert_int(write_file(ext, "good.lua",
|
||||||
|
"st.register_check(\"never\")\n"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_INTERNAL);
|
||||||
|
msg = st_error_message(err);
|
||||||
|
munit_assert_not_null(msg);
|
||||||
|
munit_assert_not_null(strstr(msg, "extension load failed"));
|
||||||
|
munit_assert_not_null(strstr(msg, "bad.lua"));
|
||||||
|
munit_assert_not_null(strstr(msg, ":2:")); /* file AND line named */
|
||||||
|
|
||||||
|
/* fail-fast: good.lua (which sorts after bad.lua) never ran */
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 0);
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (c) a sandbox violation yields the sandbox error ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_sandbox_violation(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *msg;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_file(ext, "evil.lua", "os.execute(\"id\")\n"),
|
||||||
|
==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
munit_assert_int(st_error_category_of(err), ==, ST_ERR_KDL_SCHEMA);
|
||||||
|
msg = st_error_message(err);
|
||||||
|
munit_assert_not_null(msg);
|
||||||
|
munit_assert_not_null(strstr(msg, "sandbox blocked"));
|
||||||
|
munit_assert_not_null(strstr(msg, "evil.lua"));
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (d) empty dir / missing dir / NULL dirs: clean no-op ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_empty_and_missing_dirs(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *empty = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *dirs[2];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
empty = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(empty);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
/* a nonexistent dir and an empty dir are both silent no-ops */
|
||||||
|
dirs[0] = "/nonexistent/st_discovery_dir";
|
||||||
|
dirs[1] = empty;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 2);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 0);
|
||||||
|
munit_assert_size(st_lua_language_count(rt), ==, 0);
|
||||||
|
munit_assert_size(st_ext_language_count(ctx), ==, 2); /* builtins only */
|
||||||
|
|
||||||
|
/* NULL/0 extra dirs are valid too (env + user + builtin only) */
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
err = st_ext_discover(ctx, rt, NULL, 0);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 0);
|
||||||
|
|
||||||
|
/* discovery is one-shot per ctx: the builtin init re-runs and
|
||||||
|
* duplicate registrations fail (documented contract) */
|
||||||
|
err = st_ext_discover(ctx, rt, NULL, 0);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(empty);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(empty);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (e) a binary file named *.lua yields a load error ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_binary_file(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
static const unsigned char elf_head[] = { 0x7f, 'E', 'L', 'F', 0x02,
|
||||||
|
0x01, 0x01 };
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *msg;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_bytes(ext, "bin.lua", elf_head,
|
||||||
|
sizeof elf_head), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_not_null(err);
|
||||||
|
msg = st_error_message(err);
|
||||||
|
munit_assert_not_null(msg);
|
||||||
|
munit_assert_not_null(strstr(msg, "extension load failed"));
|
||||||
|
munit_assert_not_null(strstr(msg, "bin.lua"));
|
||||||
|
st_error_free(err);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (f) non-.lua entries (and a DIR named *.lua) are ignored ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_ignores_non_lua(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
char sub[1024];
|
||||||
|
int sub_len;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_file(ext, "notes.txt",
|
||||||
|
"st.register_check(\"hidden\")\n"), ==, 0);
|
||||||
|
sub_len = snprintf(sub, sizeof sub, "%s/sub.lua", ext);
|
||||||
|
munit_assert_int(sub_len, >=, 0);
|
||||||
|
munit_assert_int(mkdir(sub, 0777), ==, 0);
|
||||||
|
munit_assert_int(write_file(ext, "sub.lua/inner.lua",
|
||||||
|
"st.register_check(\"also-hidden\")\n"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 0);
|
||||||
|
munit_assert_size(st_lua_language_count(rt), ==, 0);
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (g) modules load in bytewise-lexicographic order ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_deterministic_order(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(unsetenv("STUPIDTOOLS_EXT"), ==, 0);
|
||||||
|
|
||||||
|
/* written in reverse order on purpose: discovery must sort */
|
||||||
|
munit_assert_int(write_file(ext, "b.lua",
|
||||||
|
"st.register_check(\"second\")\n"), ==, 0);
|
||||||
|
munit_assert_int(write_file(ext, "a.lua",
|
||||||
|
"st.register_check(\"first\")\n"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 2);
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 0), "first");
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 1), "second");
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- (h) STUPIDTOOLS_EXT is honored, after the explicit dirs ---------- */
|
||||||
|
|
||||||
|
static MunitResult
|
||||||
|
test_env_dirs(const MunitParameter params[], void *data)
|
||||||
|
{
|
||||||
|
char *xdg = NULL;
|
||||||
|
char *ext1 = NULL;
|
||||||
|
char *ext2 = NULL;
|
||||||
|
struct st_ext_ctx *ctx = NULL;
|
||||||
|
struct st_lua_rt *rt = NULL;
|
||||||
|
struct st_error *err;
|
||||||
|
const char *dirs[1];
|
||||||
|
|
||||||
|
(void)params;
|
||||||
|
(void)data;
|
||||||
|
|
||||||
|
xdg = make_temp_dir();
|
||||||
|
ext1 = make_temp_dir();
|
||||||
|
ext2 = make_temp_dir();
|
||||||
|
munit_assert_not_null(xdg);
|
||||||
|
munit_assert_not_null(ext1);
|
||||||
|
munit_assert_not_null(ext2);
|
||||||
|
munit_assert_int(setenv("XDG_DATA_HOME", xdg, 1), ==, 0);
|
||||||
|
munit_assert_int(setenv("STUPIDTOOLS_EXT", ext2, 1), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(write_file(ext1, "one.lua",
|
||||||
|
"st.register_check(\"one\")\n"), ==, 0);
|
||||||
|
munit_assert_int(write_file(ext2, "two.lua",
|
||||||
|
"st.register_check(\"two\")\n"), ==, 0);
|
||||||
|
|
||||||
|
munit_assert_int(fresh_pair(&ctx, &rt), ==, 0);
|
||||||
|
|
||||||
|
dirs[0] = ext1;
|
||||||
|
err = st_ext_discover(ctx, rt, dirs, 1);
|
||||||
|
munit_assert_null(err);
|
||||||
|
munit_assert_size(st_lua_check_count(rt), ==, 2);
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 0), "one");
|
||||||
|
munit_assert_string_equal(st_lua_check_name(rt, 1), "two");
|
||||||
|
|
||||||
|
st_lua_rt_free(rt);
|
||||||
|
st_ext_ctx_free(ctx);
|
||||||
|
rm_rf(ext1);
|
||||||
|
rm_rf(ext2);
|
||||||
|
rm_rf(xdg);
|
||||||
|
free(ext1);
|
||||||
|
free(ext2);
|
||||||
|
free(xdg);
|
||||||
|
return MUNIT_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static MunitTest tests[] = {
|
||||||
|
{ "/discovery/valid-module", test_valid_module, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/malformed-fail-fast", test_malformed_fail_fast, NULL,
|
||||||
|
NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/sandbox-violation", test_sandbox_violation, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/empty-and-missing-dirs", test_empty_and_missing_dirs,
|
||||||
|
NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/binary-file", test_binary_file, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/ignores-non-lua", test_ignores_non_lua, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/deterministic-order", test_deterministic_order, NULL,
|
||||||
|
NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ "/discovery/env-dirs", test_env_dirs, NULL, NULL,
|
||||||
|
MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL },
|
||||||
|
};
|
||||||
|
|
||||||
|
static const MunitSuite suite = {
|
||||||
|
"/discovery", 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