/* * Copyright © 2026 Kana Steimle * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), * to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include #include #include #include #include "config.h" // To avoid allocating a bunch of empty strings for empty config values, this string will be referenced for unset strings static char *defaultstring = ""; struct variable { char *name; char *description; // May be NULL enum configtype type; union { enum strformat strfmt; enum boolformat boolfmt; enum intformat intfmt; unsigned floatdigits; }; union { char *string; bool boolean; configint_t number_int; configfloat_t number_float; } defaultval, configval, min, max; // min and max are only relevant for ints and floats bool set; // If true, use configval; otherwise use defaultval. }; struct line { bool iscomment; // If true, the line is a user's comment; otherwise it's a variable. union { int variableindex; char *comment; }; }; struct section { char *name; // May be NULL char *description; // May be NULL size_t allocvariables; size_t numvariables; struct variable *variables; size_t alloclines; size_t numlines; struct line *lines; }; struct config { char *filename; size_t allocsections; size_t numsections; struct section *sections; }; static struct section *config_get_section (config_t *config, char const *name) { if (name == NULL) return &config->sections[0]; for (size_t i=1; inumsections; ++i) { if (!strcmp(name, config->sections[i].name)) return &config->sections[i]; } return NULL; } static struct section *config_add_section (config_t *config, char const *name, char const *description) { if (config->numsections >= config->allocsections) { size_t nallocsections = 2*config->allocsections; struct section *nsections = realloc(config->sections, sizeof(*config->sections) * nallocsections); if (nsections == NULL) return NULL; config->allocsections = nallocsections; config->sections = nsections; } struct section *section = &config->sections[config->numsections]; if (name != NULL) { section->name = strdup(name); if (section->name == NULL) goto err0; } else { section->name = NULL; } if (description != NULL) { section->description = strdup(description); if (section->description == NULL) goto err1; } else { section->description = NULL; } section->numvariables = 0; section->allocvariables = 1; section->variables = malloc(sizeof(*section->variables) * section->allocvariables); if (section->variables == NULL) goto err2; section->numlines = 0; section->alloclines = 1; section->lines = malloc(sizeof(*section->lines) * section->alloclines); if (section->lines == NULL) goto err3; ++config->numsections; return section; err3: free(section->variables); err2: if (section->description != NULL) free(section->description); err1: if (section->name != NULL) free(section->name); err0: return NULL; } static struct line *config_section_add_line (struct section *section, char const *comment) { if (section->numlines >= section->alloclines) { size_t nalloclines = 2*section->alloclines; struct line *nlines = realloc(section->lines, sizeof(*section->lines) * nalloclines); if (nlines == NULL) return NULL; section->alloclines = nalloclines; section->lines = nlines; } struct line *line = §ion->lines[section->numlines]; if (comment != NULL) { line->iscomment = true; line->comment = strdup(comment); if (line->comment == NULL) return NULL; } else { line->iscomment = false; line->variableindex = -1; } ++section->numlines; return line; } static struct variable *config_get_variable (config_t *config, char const *name, char const *sectionname) { struct section *section = config_get_section(config, sectionname); if (section == NULL) return NULL; for (size_t i=0; inumvariables; ++i) { if (!strcmp(name, section->variables[i].name)) { return §ion->variables[i]; } } return NULL; } static struct variable *config_section_add_variable (struct section *section, char const *name, char const *description) { if (section->numvariables >= section->allocvariables) { size_t nallocvariables = 2*section->allocvariables; struct variable *nvariables = realloc(section->variables, sizeof(*section->variables) * nallocvariables); if (nvariables == NULL) return NULL; section->allocvariables = nallocvariables; section->variables = nvariables; } struct variable *variable = §ion->variables[section->numvariables]; variable->type = CT_UNDEFINED; variable->set = false; variable->name = strdup(name); if (name == NULL) goto err0; if (description != NULL) { variable->description = strdup(description); if (variable->description == NULL) goto err1; } else { variable->description = NULL; } struct line *line = config_section_add_line(section, NULL); if (line == NULL) goto err2; line->variableindex = section->numvariables; ++section->numvariables; return variable; err2: if (variable->description == NULL) free(variable->description); err1: free(variable->name); err0: return NULL; } static struct variable *config_add_variable (config_t *config, char const *name, char const *sectionname) { struct section *section = config_get_section(config, sectionname); if (section == NULL) { section = config_add_section(config, sectionname, NULL); } if (section == NULL) return NULL; return config_section_add_variable(section, name, NULL); } config_t *config_create (char const *filename) { config_t *config = malloc(sizeof(*config)); if (config == NULL) goto err0; config->filename = strdup(filename); if (config->filename == NULL) goto err1; config->numsections = 0; config->allocsections = 1; config->sections = malloc(sizeof(*config->sections) * config->allocsections); if (config->sections == NULL) goto err2; if (config_add_section(config, NULL, NULL) == NULL) goto err3; return config; err3: free(config->sections); err2: free(config->filename); err1: free(config); err0: return NULL; } config_t *config_load (char const *filename) { FILE *f = fopen(filename, "r"); if (f == NULL) goto err0; config_t *config = config_create(filename); if (config == NULL) goto err1; int linenum = 0; char *line = NULL; size_t linesize = 0; struct section *section = config_get_section(config, NULL); while (getline(&line, &linesize, f) >= 0) { ++linenum; if (line[0] == ';') { // System comment; discard it. continue; } if (line[0] == '#') { // User comment config_section_add_line(section, &line[1]); continue; } if (line[0] == '\n' || line[0] == '\0') { // Empty line config_section_add_line(section, NULL); continue; } char *name, *nameend; if (line[0] == '[') { // Section name name = &line[1]; nameend = strchr(line, ']'); if (nameend == NULL || nameend == name) goto err2; *nameend = '\0'; for (char *next = name; next < nameend; ++next) { if (!isalnum(*next)) goto err2; } section = config_add_section(config, name, NULL); if (section == NULL) goto err2; continue; } // Variable name = line; nameend = name; while (isalnum(*nameend)) ++nameend; if (name == nameend || *nameend == '\0') goto err2; char *value = nameend, *valueend; while (isspace(*value)) ++value; if (*value != '=') goto err2; ++value; while (isspace(*value)) ++value; *nameend = '\0'; int alreadydone = 0; switch (*value) { configint_t intvalue; configfloat_t floatvalue; case '\0': // Empty variable. While it can't be read, we should keep it so it doesn't get lost. config_section_add_variable(section, name, NULL); alreadydone = 1; break; case '\'': ++value; // Single quote string valueend = strchr(value, '\''); if (valueend == NULL) goto err2; *valueend = '\0'; config_set_string(config, name, section->name, value); break; case '\"': ++value; // Double quote string valueend = strchr(value, '"'); if (valueend == NULL) goto err2; *valueend = '\0'; config_set_string(config, name, section->name, value); break; case '#': // Hexadecimal color ++value; intvalue = strtol(value, &valueend, 16); if (*valueend == '\0') alreadydone = 1; else --valueend; config_set_int(config, name, section->name, intvalue); break; case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': case '.': case '-': // Number valueend = strchr(value, '.'); if (valueend != NULL) { floatvalue = strtod(value, &valueend); config_set_float(config, name, section->name, floatvalue); } else { intvalue = strtol(value, &valueend, 0); config_set_int(config, name, section->name, intvalue); } if (*valueend == '\0') alreadydone = 1; else --valueend; break; default: // Might be a boolean valueend = value; while (isgraph(*valueend)) ++valueend; if (*valueend == '\0') alreadydone = 1; else *valueend = '\0'; if (!strcmp(value, "true") || !strcmp(value, "on") || !strcmp(value, "yes")) { config_set_bool(config, name, section->name, true); } else if (!strcmp(value, "false") || !strcmp(value, "off") || !strcmp(value, "no")) { config_set_bool(config, name, section->name, false); } else { // Unrecognized goto err2; } break; } if (!alreadydone) { ++valueend; // Make sure there's no garbage after the value while (*valueend != '\0') { if (!isspace(*valueend)) goto err2; ++valueend; } } } free(line); fclose(f); return config; err2: fprintf(stderr, "Error parsing config file \"%s\" on line %d\n", filename, linenum); if (line != NULL) free(line); config_delete(config); err1: fclose(f); err0: return NULL; } static void config_fprint_description (FILE *f, char const *description) { char const *line = description; while (1) { char const *lineend = strchrnul(line, '\n'); fprintf(f, "; %.*s\n", (int)(lineend - line), line); if (*lineend == '\0') break; line = lineend+1; if (*line == '\0') break; } } int config_save (config_t *config) { FILE *f = fopen(config->filename, "w"); if (f == NULL) goto err0; for (size_t i=0; inumsections; ++i) { struct section *section = &config->sections[i]; if (section->description != NULL) { config_fprint_description(f, section->description); } if (section->name != NULL) { fprintf(f, "[%s]\n", section->name); } for (size_t j=0; jnumlines; ++j) { struct line *line = §ion->lines[j]; if (line->iscomment) { fprintf(f, "#%s", line->comment); } else if (line->variableindex >= 0) { struct variable *variable = §ion->variables[line->variableindex]; if (variable->description != NULL) { config_fprint_description(f, variable->description); } if (!variable->set) fputc(';', f); fprintf(f, "%s = ", variable->name); switch (variable->type) { default: break; case CT_STRING: { char const *value = variable->set ? variable->configval.string : variable->defaultval.string; switch (variable->strfmt) { default: case SF_SINGLEQUOTES: fprintf(f, "'%s'", value); break; case SF_DOUBLEQUOTES: fprintf(f, "\"%s\"", value); break; } break; } case CT_BOOL: { bool value = variable->set ? variable->configval.boolean : variable->defaultval.boolean; switch (variable->boolfmt) { default: case BF_TRUEFALSE: fprintf(f, "%s", value ? "true" : "false"); break; case BF_ONOFF: fprintf(f, "%s", value ? "on" : "off"); break; case BF_YESNO: fprintf(f, "%s", value ? "yes" : "no"); break; } break; } case CT_INT: { configint_t value = variable->set ? variable->configval.number_int : variable->defaultval.number_int; switch (variable->intfmt) { default: case IF_DECIMAL: fprintf(f, "%ld", value); break; case IF_HEX: fprintf(f, "0x%lX", value); break; case IF_COLOR: fprintf(f, "#%06lX", value); break; case IF_COLORALPHA: fprintf(f, "#%08lX", value); break; } break; } case CT_FLOAT: { configfloat_t value = variable->set ? variable->configval.number_float : variable->defaultval.number_float; fprintf(f, "%.*f", variable->floatdigits, value); break; } } fputc('\n', f); } else { fputc('\n', f); } } } fclose(f); return 0; err0: return 1; } void config_delete (config_t *config) { free(config->filename); for (int i = 0; inumsections; ++i) { struct section *section = &config->sections[i]; if (section->name != NULL) free(section->name); if (section->description != NULL) free(section->description); for (int i=0; inumlines; ++i) { struct line *line = §ion->lines[i]; if (line->iscomment) free(line->comment); } for (int i=0; inumvariables; ++i) { struct variable *variable = §ion->variables[i]; free(variable->name); if (variable->type == CT_STRING) { if (variable->defaultval.string != defaultstring) { free(variable->defaultval.string); } if (variable->set) { free(variable->configval.string); } } } free(section->variables); } free(config->sections); free(config); } enum configtype config_get_type (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) return CT_UNDEFINED; return variable->type; } static void config_set_type (struct variable *variable, enum configtype type) { if (variable->type == type) return; if (variable->type == CT_STRING) { if (variable->defaultval.string != defaultstring) free(variable->defaultval.string); if (variable->set) free(variable->configval.string); } variable->type = type; variable->set = false; switch (type) { default: break; case CT_STRING: variable->defaultval.string = defaultstring; variable->strfmt = SF_DEFAULT; break; case CT_BOOL: variable->defaultval.boolean = false; variable->boolfmt = BF_DEFAULT; break; case CT_INT: variable->defaultval.number_int = 0; variable->intfmt = IF_DEFAULT; variable->min.number_int = CONFIGINT_MIN; variable->max.number_int = CONFIGINT_MAX; break; case CT_FLOAT: variable->defaultval.number_float = 0.0; variable->floatdigits = 6; variable->min.number_float = CONFIGFLT_MIN; variable->max.number_float = CONFIGFLT_MAX; break; } } char const *config_get_string (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_STRING) return defaultstring; if (variable->set) return variable->configval.string; return variable->defaultval.string; } bool config_get_bool (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_BOOL) return false; if (variable->set) return variable->configval.boolean; return variable->defaultval.boolean; } configint_t config_get_int (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 0; if (variable->set) return variable->configval.number_int; return variable->defaultval.number_int; } configfloat_t config_get_float (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 0.0; if (variable->set) return variable->configval.number_float; return variable->defaultval.number_float; } int config_default_string (CONFIG_VARIABLE_ARGS_STRING) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; char *newstring = strdup(value); if (newstring == NULL) return 1; config_set_type(variable, CT_STRING); variable->defaultval.string = newstring; return 0; } int config_default_bool (CONFIG_VARIABLE_ARGS_BOOL) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; config_set_type(variable, CT_BOOL); variable->defaultval.boolean = value; return 0; } int config_default_int (CONFIG_VARIABLE_ARGS_INT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; config_set_type(variable, CT_INT); variable->defaultval.number_int = value; return 0; } int config_default_float (CONFIG_VARIABLE_ARGS_FLOAT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; config_set_type(variable, CT_FLOAT); variable->defaultval.number_float = value; return 0; } int config_set_string (CONFIG_VARIABLE_ARGS_STRING) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; char *newstring = strdup(value); if (newstring == NULL) return 1; if (variable->type == CT_UNDEFINED) config_set_type(variable, CT_STRING); if (variable->type != CT_STRING) { free(newstring); return 1; } variable->configval.string = newstring; variable->set = true; return 0; } int config_set_bool (CONFIG_VARIABLE_ARGS_BOOL) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; if (variable->type == CT_UNDEFINED) config_set_type(variable, CT_BOOL); if (variable->type != CT_BOOL) return 1; variable->configval.boolean = value; variable->set = true; return 0; } int config_set_int (CONFIG_VARIABLE_ARGS_INT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; if (variable->type == CT_UNDEFINED) config_set_type(variable, CT_INT); if (variable->type != CT_INT) return 1; if (value > variable->max.number_int) { variable->configval.number_int = variable->max.number_int; } else if (value < variable->min.number_int) { variable->configval.number_int = variable->min.number_int; } else { variable->configval.number_int = value; } variable->set = true; return 0; } int config_set_float (CONFIG_VARIABLE_ARGS_FLOAT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; if (variable->type == CT_UNDEFINED) config_set_type(variable, CT_FLOAT); if (variable->type != CT_FLOAT) return 1; if (value > variable->max.number_float) { variable->configval.number_float = variable->max.number_float; } else if (value < variable->min.number_float) { variable->configval.number_float = variable->min.number_float; } else { variable->configval.number_float = value; } variable->set = true; return 0; } int config_reset_variable (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) return 1; if (variable->type == CT_STRING) free(variable->configval.string); variable->set = false; return 0; } int config_describe_variable (CONFIG_VARIABLE_ARGS, char const *description) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL) variable = config_add_variable(config, name, section); if (variable == NULL) return 1; char *newstring = strdup(description); if (newstring == NULL) return 1; if (variable->description != NULL) free(variable->description); variable->description = newstring; return 0; } int config_describe_section (config_t *config, char const *name, char const *description) { struct section *section = config_get_section(config, name); if (section == NULL) { section = config_add_section(config, name, description); return section == NULL ? 1 : 0; } if (section == NULL) return 1; char *newstring = strdup(description); if (newstring == NULL) return 1; if (section->description != NULL) free(section->description); section->description = newstring; return 0; } int config_format_string (CONFIG_VARIABLE_ARGS, enum strformat format) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_STRING) return 1; variable->strfmt = format; return 0; } int config_format_bool (CONFIG_VARIABLE_ARGS, enum boolformat format) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_BOOL) return 1; variable->boolfmt = format; return 0; } int config_format_int (CONFIG_VARIABLE_ARGS, enum intformat format) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 1; variable->intfmt = format; return 0; } int config_format_float (CONFIG_VARIABLE_ARGS, unsigned digits) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 1; variable->floatdigits = digits; return 0; } int config_set_min_int (CONFIG_VARIABLE_ARGS_INT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 1; variable->min.number_int = value; return 0; } int config_set_max_int (CONFIG_VARIABLE_ARGS_INT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 1; variable->max.number_int = value; return 0; } int config_set_min_float (CONFIG_VARIABLE_ARGS_FLOAT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 1; variable->min.number_float = value; return 0; } int config_set_max_float (CONFIG_VARIABLE_ARGS_FLOAT) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 1; variable->max.number_float = value; return 0; } configint_t config_get_min_int (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 0; return variable->min.number_int; } configint_t config_get_max_int (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_INT) return 0; return variable->max.number_int; } configfloat_t config_get_min_float (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 0; return variable->min.number_float; } configfloat_t config_get_max_float (CONFIG_VARIABLE_ARGS) { struct variable *variable = config_get_variable(config, name, section); if (variable == NULL || variable->type != CT_FLOAT) return 0; return variable->max.number_float; }