Rewrite config storage: typed union instead of raw strings
Config_Entry now holds a union {s, u16, u32, flags} typed at parse time.
Getters read directly from the union — no string conversion at access time.
config_dump reconstructs flag display as 'token | token' from the bitmask.
Separators in flag values: comma, pipe, and whitespace all accepted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,13 @@
|
|||||||
struct Config_Entry {
|
struct Config_Entry {
|
||||||
char section[MAX_STR];
|
char section[MAX_STR];
|
||||||
char key[MAX_STR];
|
char key[MAX_STR];
|
||||||
char value[MAX_STR];
|
Config_Type type;
|
||||||
|
union {
|
||||||
|
char s[MAX_STR];
|
||||||
|
uint16_t u16;
|
||||||
|
uint32_t u32;
|
||||||
|
uint32_t flags;
|
||||||
|
} val;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Config {
|
struct Config {
|
||||||
@@ -25,36 +31,68 @@ struct Config {
|
|||||||
/* -- string helpers -------------------------------------------------------- */
|
/* -- string helpers -------------------------------------------------------- */
|
||||||
|
|
||||||
static void strip(char *s) {
|
static void strip(char *s) {
|
||||||
/* strip leading whitespace */
|
|
||||||
char *start = s;
|
char *start = s;
|
||||||
while (*start && isspace((unsigned char)*start)) { start++; }
|
while (*start && isspace((unsigned char)*start)) { start++; }
|
||||||
|
|
||||||
/* strip trailing whitespace */
|
|
||||||
char *end = start + strlen(start);
|
char *end = start + strlen(start);
|
||||||
while (end > start && isspace((unsigned char)*(end - 1))) { end--; }
|
while (end > start && isspace((unsigned char)*(end - 1))) { end--; }
|
||||||
*end = '\0';
|
*end = '\0';
|
||||||
|
|
||||||
if (start != s) { memmove(s, start, (size_t)(end - start) + 1); }
|
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) {
|
static void strip_comment(char *s) {
|
||||||
for (char *p = s; *p; p++) {
|
for (char *p = s; *p; p++) {
|
||||||
if (*p == '#' || *p == ';') {
|
if (*p == '#' || *p == ';') { *p = '\0'; break; }
|
||||||
*p = '\0';
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* normalise flag separators: comma, pipe, and whitespace all become space */
|
||||||
|
static void normalise_separators(char *s) {
|
||||||
|
for (char *p = s; *p; p++) {
|
||||||
|
if (*p == ',' || *p == '|') { *p = ' '; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -- parse typed value ----------------------------------------------------- */
|
||||||
|
|
||||||
|
static uint32_t parse_flags(const char *raw, const struct Config_Flag_Def *defs) {
|
||||||
|
char buf[MAX_STR];
|
||||||
|
strncpy(buf, raw, MAX_STR - 1);
|
||||||
|
buf[MAX_STR - 1] = '\0';
|
||||||
|
normalise_separators(buf);
|
||||||
|
|
||||||
|
uint32_t bits = 0;
|
||||||
|
char *tok = strtok(buf, " \t");
|
||||||
|
while (tok) {
|
||||||
|
for (const struct Config_Flag_Def *fd = defs; fd && fd->token; fd++) {
|
||||||
|
if (strcmp(tok, fd->token) == 0) { bits |= fd->value; break; }
|
||||||
|
}
|
||||||
|
tok = strtok(NULL, " \t");
|
||||||
|
}
|
||||||
|
return bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_entry_value(struct Config_Entry *e,
|
||||||
|
const struct Config_Def *def, const char *raw)
|
||||||
|
{
|
||||||
|
e->type = def->type;
|
||||||
|
switch (def->type) {
|
||||||
|
case CONFIG_STRING:
|
||||||
|
strncpy(e->val.s, raw, MAX_STR - 1);
|
||||||
|
e->val.s[MAX_STR - 1] = '\0';
|
||||||
|
break;
|
||||||
|
case CONFIG_UINT16:
|
||||||
|
e->val.u16 = (uint16_t)strtoul(raw, NULL, 10);
|
||||||
|
break;
|
||||||
|
case CONFIG_UINT32:
|
||||||
|
e->val.u32 = (uint32_t)strtoul(raw, NULL, 10);
|
||||||
|
break;
|
||||||
|
case CONFIG_FLAGS:
|
||||||
|
e->val.flags = parse_flags(raw, def->flags);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/* replace commas with spaces so flag lists parse uniformly */
|
/* -- entry table ----------------------------------------------------------- */
|
||||||
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,
|
static const struct Config_Def *find_def(const struct Config_Def *schema,
|
||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
@@ -67,8 +105,6 @@ static const struct Config_Def *find_def(const struct Config_Def *schema,
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- entry lookup ---------------------------------------------------------- */
|
|
||||||
|
|
||||||
static struct Config_Entry *find_entry(struct Config *cfg,
|
static struct Config_Entry *find_entry(struct Config *cfg,
|
||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
@@ -82,16 +118,13 @@ static struct Config_Entry *find_entry(struct Config *cfg,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static struct Config_Entry *add_entry(struct Config *cfg,
|
static struct Config_Entry *add_entry(struct Config *cfg,
|
||||||
const char *section, const char *key, const char *value)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
if (cfg->count >= MAX_ENTRIES) { return NULL; }
|
if (cfg->count >= MAX_ENTRIES) { return NULL; }
|
||||||
struct Config_Entry *e = &cfg->entries[cfg->count++];
|
struct Config_Entry *e = &cfg->entries[cfg->count++];
|
||||||
|
memset(e, 0, sizeof(*e));
|
||||||
strncpy(e->section, section, MAX_STR - 1);
|
strncpy(e->section, section, MAX_STR - 1);
|
||||||
strncpy(e->key, key, 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;
|
return e;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,14 +132,14 @@ static struct Config_Entry *add_entry(struct Config *cfg,
|
|||||||
|
|
||||||
static void fill_defaults(struct Config *cfg) {
|
static void fill_defaults(struct Config *cfg) {
|
||||||
for (const struct Config_Def *d = cfg->schema; d->section; d++) {
|
for (const struct Config_Def *d = cfg->schema; d->section; d++) {
|
||||||
if (!find_entry(cfg, d->section, d->key)) {
|
if (find_entry(cfg, d->section, d->key)) { continue; }
|
||||||
add_entry(cfg, d->section, d->key,
|
struct Config_Entry *e = add_entry(cfg, d->section, d->key);
|
||||||
d->default_val ? d->default_val : "");
|
if (!e) { continue; }
|
||||||
}
|
set_entry_value(e, d, d->default_val ? d->default_val : "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* -- parse ----------------------------------------------------------------- */
|
/* -- file parser ----------------------------------------------------------- */
|
||||||
|
|
||||||
static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
||||||
FILE *f = fopen(path, "r");
|
FILE *f = fopen(path, "r");
|
||||||
@@ -118,11 +151,9 @@ static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
|||||||
while (fgets(line, sizeof(line), f)) {
|
while (fgets(line, sizeof(line), f)) {
|
||||||
strip_comment(line);
|
strip_comment(line);
|
||||||
strip(line);
|
strip(line);
|
||||||
|
|
||||||
if (line[0] == '\0') { continue; }
|
if (line[0] == '\0') { continue; }
|
||||||
|
|
||||||
if (line[0] == '[') {
|
if (line[0] == '[') {
|
||||||
/* section header */
|
|
||||||
char *end = strchr(line, ']');
|
char *end = strchr(line, ']');
|
||||||
if (!end) { continue; }
|
if (!end) { continue; }
|
||||||
*end = '\0';
|
*end = '\0';
|
||||||
@@ -134,31 +165,20 @@ static struct App_Error parse_file(struct Config *cfg, const char *path) {
|
|||||||
|
|
||||||
char *eq = strchr(line, '=');
|
char *eq = strchr(line, '=');
|
||||||
if (!eq || section[0] == '\0') { continue; }
|
if (!eq || section[0] == '\0') { continue; }
|
||||||
|
|
||||||
*eq = '\0';
|
*eq = '\0';
|
||||||
char *key = line;
|
char *key = line;
|
||||||
char *val = eq + 1;
|
char *val = eq + 1;
|
||||||
strip(key);
|
strip(key);
|
||||||
strip(val);
|
strip(val);
|
||||||
|
|
||||||
if (key[0] == '\0') { continue; }
|
if (key[0] == '\0') { continue; }
|
||||||
|
|
||||||
/* only store keys that appear in the schema */
|
|
||||||
const struct Config_Def *def = find_def(cfg->schema, section, key);
|
const struct Config_Def *def = find_def(cfg->schema, section, key);
|
||||||
if (!def) { continue; }
|
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);
|
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||||
if (e) {
|
if (!e) { e = add_entry(cfg, section, key); }
|
||||||
strncpy(e->value, normalised, MAX_STR - 1);
|
if (!e) { continue; }
|
||||||
} else {
|
set_entry_value(e, def, val);
|
||||||
add_entry(cfg, section, key, normalised);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fclose(f);
|
fclose(f);
|
||||||
@@ -175,10 +195,7 @@ struct App_Error config_load(struct Config **out, const char *path,
|
|||||||
cfg->schema = schema;
|
cfg->schema = schema;
|
||||||
|
|
||||||
struct App_Error err = parse_file(cfg, path);
|
struct App_Error err = parse_file(cfg, path);
|
||||||
if (!APP_IS_OK(err)) {
|
if (!APP_IS_OK(err)) { free(cfg); return err; }
|
||||||
free(cfg);
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
fill_defaults(cfg);
|
fill_defaults(cfg);
|
||||||
*out = cfg;
|
*out = cfg;
|
||||||
@@ -204,51 +221,28 @@ const char *config_get_str(struct Config *cfg,
|
|||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||||
return e ? e->value : "";
|
return (e && e->type == CONFIG_STRING) ? e->val.s : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
uint16_t config_get_u16(struct Config *cfg,
|
uint16_t config_get_u16(struct Config *cfg,
|
||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||||
if (!e || e->value[0] == '\0') { return 0; }
|
return (e && e->type == CONFIG_UINT16) ? e->val.u16 : 0;
|
||||||
return (uint16_t)strtoul(e->value, NULL, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t config_get_u32(struct Config *cfg,
|
uint32_t config_get_u32(struct Config *cfg,
|
||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||||
if (!e || e->value[0] == '\0') { return 0; }
|
return (e && e->type == CONFIG_UINT32) ? e->val.u32 : 0;
|
||||||
return (uint32_t)strtoul(e->value, NULL, 10);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t config_get_flags(struct Config *cfg,
|
uint32_t config_get_flags(struct Config *cfg,
|
||||||
const char *section, const char *key)
|
const char *section, const char *key)
|
||||||
{
|
{
|
||||||
struct Config_Entry *e = find_entry(cfg, section, key);
|
struct Config_Entry *e = find_entry(cfg, section, key);
|
||||||
if (!e || e->value[0] == '\0') { return 0; }
|
return (e && e->type == CONFIG_FLAGS) ? e->val.flags : 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) {
|
void config_dump(struct Config *cfg) {
|
||||||
@@ -259,6 +253,33 @@ void config_dump(struct Config *cfg) {
|
|||||||
printf("[%s]\n", e->section);
|
printf("[%s]\n", e->section);
|
||||||
cur_section = e->section;
|
cur_section = e->section;
|
||||||
}
|
}
|
||||||
printf(" %-24s = %s\n", e->key, e->value);
|
|
||||||
|
switch (e->type) {
|
||||||
|
case CONFIG_STRING:
|
||||||
|
printf(" %-24s = %s\n", e->key, e->val.s);
|
||||||
|
break;
|
||||||
|
case CONFIG_UINT16:
|
||||||
|
printf(" %-24s = %u\n", e->key, (unsigned)e->val.u16);
|
||||||
|
break;
|
||||||
|
case CONFIG_UINT32:
|
||||||
|
printf(" %-24s = %u\n", e->key, e->val.u32);
|
||||||
|
break;
|
||||||
|
case CONFIG_FLAGS: {
|
||||||
|
const struct Config_Def *def = find_def(cfg->schema, e->section, e->key);
|
||||||
|
char display[MAX_STR] = "";
|
||||||
|
if (def && def->flags) {
|
||||||
|
for (const struct Config_Flag_Def *fd = def->flags; fd->token; fd++) {
|
||||||
|
if (e->val.flags & fd->value) {
|
||||||
|
if (display[0] != '\0') {
|
||||||
|
strncat(display, " | ", sizeof(display) - strlen(display) - 1);
|
||||||
|
}
|
||||||
|
strncat(display, fd->token, sizeof(display) - strlen(display) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf(" %-24s = %s\n", e->key, display[0] ? display : "(none)");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user