Add config module: INI loader with schema-driven defaults
Config_Def schema tables declare section/key/type/default per module. Typed getters: config_get_str, _u16, _u32, _flags. FLAGS type parses space/comma-separated tokens via a Config_Flag_Def table. config_defaults() gives schema defaults without a file. config_dump() prints effective values for diagnostics. config_cli: load a file or --defaults and dump effective config. dev/example.cfg: sample config covering node, discovery, transport. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
17
src/modules/config/Makefile
Normal file
17
src/modules/config/Makefile
Normal file
@@ -0,0 +1,17 @@
|
||||
ROOT := $(abspath ../../..)
|
||||
include $(ROOT)/common.mk
|
||||
|
||||
MODULE_BUILD = $(BUILD)/config
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
all: $(MODULE_BUILD)/config.o
|
||||
|
||||
$(MODULE_BUILD)/config.o: config.c $(ROOT)/include/config.h | $(MODULE_BUILD)
|
||||
$(CC) $(CFLAGS) -c -o $@ $<
|
||||
|
||||
$(MODULE_BUILD):
|
||||
mkdir -p $@
|
||||
|
||||
clean:
|
||||
rm -f $(MODULE_BUILD)/config.o
|
||||
264
src/modules/config/config.c
Normal file
264
src/modules/config/config.c
Normal file
@@ -0,0 +1,264 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#define MAX_ENTRIES 256
|
||||
#define MAX_LINE 512
|
||||
#define MAX_STR 256
|
||||
|
||||
struct Config_Entry {
|
||||
char section[MAX_STR];
|
||||
char key[MAX_STR];
|
||||
char value[MAX_STR];
|
||||
};
|
||||
|
||||
struct Config {
|
||||
struct Config_Entry entries[MAX_ENTRIES];
|
||||
int count;
|
||||
const struct Config_Def *schema;
|
||||
};
|
||||
|
||||
/* -- string helpers -------------------------------------------------------- */
|
||||
|
||||
static void strip(char *s) {
|
||||
/* strip leading whitespace */
|
||||
char *start = s;
|
||||
while (*start && isspace((unsigned char)*start)) { start++; }
|
||||
|
||||
/* strip trailing whitespace */
|
||||
char *end = start + strlen(start);
|
||||
while (end > start && isspace((unsigned char)*(end - 1))) { end--; }
|
||||
*end = '\0';
|
||||
|
||||
if (start != s) { memmove(s, start, (size_t)(end - start) + 1); }
|
||||
}
|
||||
|
||||
/* strip inline comment (# or ;) that appears outside a value */
|
||||
static void strip_comment(char *s) {
|
||||
for (char *p = s; *p; p++) {
|
||||
if (*p == '#' || *p == ';') {
|
||||
*p = '\0';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* replace commas with spaces so flag lists parse uniformly */
|
||||
static void commas_to_spaces(char *s) {
|
||||
for (char *p = s; *p; p++) {
|
||||
if (*p == ',') { *p = ' '; }
|
||||
}
|
||||
}
|
||||
|
||||
/* -- schema lookup --------------------------------------------------------- */
|
||||
|
||||
static const struct Config_Def *find_def(const struct Config_Def *schema,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
for (const struct Config_Def *d = schema; d->section; d++) {
|
||||
if (strcmp(d->section, section) == 0 && strcmp(d->key, key) == 0) {
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* -- entry lookup ---------------------------------------------------------- */
|
||||
|
||||
static struct Config_Entry *find_entry(struct Config *cfg,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
for (int i = 0; i < cfg->count; i++) {
|
||||
if (strcmp(cfg->entries[i].section, section) == 0
|
||||
&& strcmp(cfg->entries[i].key, key) == 0) {
|
||||
return &cfg->entries[i];
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static struct Config_Entry *add_entry(struct Config *cfg,
|
||||
const char *section, const char *key, const char *value)
|
||||
{
|
||||
if (cfg->count >= MAX_ENTRIES) { return NULL; }
|
||||
struct Config_Entry *e = &cfg->entries[cfg->count++];
|
||||
strncpy(e->section, section, MAX_STR - 1);
|
||||
strncpy(e->key, key, MAX_STR - 1);
|
||||
strncpy(e->value, value, MAX_STR - 1);
|
||||
e->section[MAX_STR - 1] = '\0';
|
||||
e->key [MAX_STR - 1] = '\0';
|
||||
e->value [MAX_STR - 1] = '\0';
|
||||
return e;
|
||||
}
|
||||
|
||||
/* -- fill defaults --------------------------------------------------------- */
|
||||
|
||||
static void fill_defaults(struct Config *cfg) {
|
||||
for (const struct Config_Def *d = cfg->schema; d->section; d++) {
|
||||
if (!find_entry(cfg, d->section, d->key)) {
|
||||
add_entry(cfg, d->section, d->key,
|
||||
d->default_val ? d->default_val : "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* -- parse ----------------------------------------------------------------- */
|
||||
|
||||
static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
||||
FILE *f = fopen(path, "r");
|
||||
if (!f) { return APP_SYSCALL_ERROR(); }
|
||||
|
||||
char line[MAX_LINE];
|
||||
char section[MAX_STR] = "";
|
||||
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
strip_comment(line);
|
||||
strip(line);
|
||||
|
||||
if (line[0] == '\0') { continue; }
|
||||
|
||||
if (line[0] == '[') {
|
||||
/* section header */
|
||||
char *end = strchr(line, ']');
|
||||
if (!end) { continue; }
|
||||
*end = '\0';
|
||||
strncpy(section, line + 1, MAX_STR - 1);
|
||||
section[MAX_STR - 1] = '\0';
|
||||
strip(section);
|
||||
continue;
|
||||
}
|
||||
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq || section[0] == '\0') { continue; }
|
||||
|
||||
*eq = '\0';
|
||||
char *key = line;
|
||||
char *val = eq + 1;
|
||||
strip(key);
|
||||
strip(val);
|
||||
|
||||
if (key[0] == '\0') { continue; }
|
||||
|
||||
/* only store keys that appear in the schema */
|
||||
const struct Config_Def *def = find_def(cfg->schema, section, key);
|
||||
if (!def) { continue; }
|
||||
|
||||
/* normalise flag values: commas → spaces */
|
||||
char normalised[MAX_STR];
|
||||
strncpy(normalised, val, MAX_STR - 1);
|
||||
normalised[MAX_STR - 1] = '\0';
|
||||
if (def->type == CONFIG_FLAGS) { commas_to_spaces(normalised); }
|
||||
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
if (e) {
|
||||
strncpy(e->value, normalised, MAX_STR - 1);
|
||||
} else {
|
||||
add_entry(cfg, section, key, normalised);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
/* -- public API ------------------------------------------------------------ */
|
||||
|
||||
struct App_Error config_load(struct Config **out, const char *path,
|
||||
const struct Config_Def *schema)
|
||||
{
|
||||
struct Config *cfg = calloc(1, sizeof(*cfg));
|
||||
if (!cfg) { return APP_SYSCALL_ERROR(); }
|
||||
cfg->schema = schema;
|
||||
|
||||
struct App_Error err = parse_file(cfg, path);
|
||||
if (!APP_IS_OK(err)) {
|
||||
free(cfg);
|
||||
return err;
|
||||
}
|
||||
|
||||
fill_defaults(cfg);
|
||||
*out = cfg;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
struct App_Error config_defaults(struct Config **out,
|
||||
const struct Config_Def *schema)
|
||||
{
|
||||
struct Config *cfg = calloc(1, sizeof(*cfg));
|
||||
if (!cfg) { return APP_SYSCALL_ERROR(); }
|
||||
cfg->schema = schema;
|
||||
fill_defaults(cfg);
|
||||
*out = cfg;
|
||||
return APP_OK;
|
||||
}
|
||||
|
||||
void config_free(struct Config *cfg) {
|
||||
free(cfg);
|
||||
}
|
||||
|
||||
const char *config_get_str(struct Config *cfg,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
return e ? e->value : "";
|
||||
}
|
||||
|
||||
uint16_t config_get_u16(struct Config *cfg,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
if (!e || e->value[0] == '\0') { return 0; }
|
||||
return (uint16_t)strtoul(e->value, NULL, 10);
|
||||
}
|
||||
|
||||
uint32_t config_get_u32(struct Config *cfg,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
if (!e || e->value[0] == '\0') { return 0; }
|
||||
return (uint32_t)strtoul(e->value, NULL, 10);
|
||||
}
|
||||
|
||||
uint32_t config_get_flags(struct Config *cfg,
|
||||
const char *section, const char *key)
|
||||
{
|
||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||
if (!e || e->value[0] == '\0') { return 0; }
|
||||
|
||||
const struct Config_Def *def = find_def(cfg->schema, section, key);
|
||||
if (!def || !def->flags) { return 0; }
|
||||
|
||||
uint32_t result = 0;
|
||||
char buf[MAX_STR];
|
||||
strncpy(buf, e->value, MAX_STR - 1);
|
||||
buf[MAX_STR - 1] = '\0';
|
||||
|
||||
char *token = strtok(buf, " \t");
|
||||
while (token) {
|
||||
for (const struct Config_Flag_Def *fd = def->flags; fd->token; fd++) {
|
||||
if (strcmp(token, fd->token) == 0) {
|
||||
result |= fd->value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
token = strtok(NULL, " \t");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void config_dump(struct Config *cfg) {
|
||||
const char *cur_section = NULL;
|
||||
for (int i = 0; i < cfg->count; i++) {
|
||||
struct Config_Entry *e = &cfg->entries[i];
|
||||
if (!cur_section || strcmp(cur_section, e->section) != 0) {
|
||||
printf("[%s]\n", e->section);
|
||||
cur_section = e->section;
|
||||
}
|
||||
printf(" %-24s = %s\n", e->key, e->value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user