442 lines
11 KiB
C
442 lines
11 KiB
C
// dvxPrefs.c -- INI-based preferences system (read/write)
|
|
//
|
|
// Custom INI parser and writer. Stores entries as a dynamic array of
|
|
// section/key/value triples using stb_ds. Preserves insertion order
|
|
// on save so the file remains human-readable.
|
|
|
|
#include "dvxPrefs.h"
|
|
|
|
#include <ctype.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
// stb_ds dynamic arrays (implementation lives in libtasks.a)
|
|
#include "thirdparty/stb_ds.h"
|
|
|
|
|
|
// ============================================================
|
|
// Internal types
|
|
// ============================================================
|
|
|
|
typedef struct {
|
|
char *section;
|
|
char *key;
|
|
char *value;
|
|
} PrefsEntryT;
|
|
|
|
// Comment lines are stored to preserve them on save. A comment has
|
|
// key=NULL and value=the full line text (including the ; prefix).
|
|
// Section headers have key=NULL and value=NULL.
|
|
|
|
static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array
|
|
static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave)
|
|
|
|
|
|
// ============================================================
|
|
// Helpers
|
|
// ============================================================
|
|
|
|
static char *dupStr(const char *s) {
|
|
if (!s) {
|
|
return NULL;
|
|
}
|
|
|
|
size_t len = strlen(s);
|
|
char *d = (char *)malloc(len + 1);
|
|
|
|
if (d) {
|
|
memcpy(d, s, len + 1);
|
|
}
|
|
|
|
return d;
|
|
}
|
|
|
|
|
|
static void freeEntry(PrefsEntryT *e) {
|
|
free(e->section);
|
|
free(e->key);
|
|
free(e->value);
|
|
e->section = NULL;
|
|
e->key = NULL;
|
|
e->value = NULL;
|
|
}
|
|
|
|
|
|
// Case-insensitive string compare
|
|
static int strcmpci(const char *a, const char *b) {
|
|
for (;;) {
|
|
int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);
|
|
|
|
if (d != 0 || !*a) {
|
|
return d;
|
|
}
|
|
|
|
a++;
|
|
b++;
|
|
}
|
|
}
|
|
|
|
|
|
// Find an entry by section+key (case-insensitive). Returns index or -1.
|
|
static int32_t findEntry(const char *section, const char *key) {
|
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
|
PrefsEntryT *e = &sEntries[i];
|
|
|
|
if (e->key && e->section &&
|
|
strcmpci(e->section, section) == 0 &&
|
|
strcmpci(e->key, key) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// Find the index of a section header entry. Returns -1 if not found.
|
|
static int32_t findSection(const char *section) {
|
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
|
PrefsEntryT *e = &sEntries[i];
|
|
|
|
if (!e->key && !e->value && e->section &&
|
|
strcmpci(e->section, section) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
// Trim leading/trailing whitespace in place. Returns pointer into buf.
|
|
static char *trimInPlace(char *buf) {
|
|
while (*buf == ' ' || *buf == '\t') {
|
|
buf++;
|
|
}
|
|
|
|
char *end = buf + strlen(buf) - 1;
|
|
|
|
while (end >= buf && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
|
|
*end-- = '\0';
|
|
}
|
|
|
|
return buf;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsFree
|
|
// ============================================================
|
|
|
|
void prefsFree(void) {
|
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
|
freeEntry(&sEntries[i]);
|
|
}
|
|
|
|
arrfree(sEntries);
|
|
sEntries = NULL;
|
|
free(sFilePath);
|
|
sFilePath = NULL;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsGetBool
|
|
// ============================================================
|
|
|
|
bool prefsGetBool(const char *section, const char *key, bool defaultVal) {
|
|
const char *val = prefsGetString(section, key, NULL);
|
|
|
|
if (!val) {
|
|
return defaultVal;
|
|
}
|
|
|
|
char c = (char)tolower((unsigned char)val[0]);
|
|
|
|
if (c == 't' || c == 'y' || c == '1') {
|
|
return true;
|
|
}
|
|
|
|
if (c == 'f' || c == 'n' || c == '0') {
|
|
return false;
|
|
}
|
|
|
|
return defaultVal;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsGetInt
|
|
// ============================================================
|
|
|
|
int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) {
|
|
const char *val = prefsGetString(section, key, NULL);
|
|
|
|
if (!val) {
|
|
return defaultVal;
|
|
}
|
|
|
|
char *end = NULL;
|
|
long n = strtol(val, &end, 10);
|
|
|
|
if (end == val) {
|
|
return defaultVal;
|
|
}
|
|
|
|
return (int32_t)n;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsGetString
|
|
// ============================================================
|
|
|
|
const char *prefsGetString(const char *section, const char *key, const char *defaultVal) {
|
|
int32_t idx = findEntry(section, key);
|
|
|
|
if (idx < 0) {
|
|
return defaultVal;
|
|
}
|
|
|
|
return sEntries[idx].value;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsLoad
|
|
// ============================================================
|
|
|
|
bool prefsLoad(const char *filename) {
|
|
prefsFree();
|
|
|
|
// Always store the path so prefsSave can create the file
|
|
// even if it doesn't exist yet.
|
|
sFilePath = dupStr(filename);
|
|
|
|
FILE *fp = fopen(filename, "rb");
|
|
|
|
if (!fp) {
|
|
return false;
|
|
}
|
|
|
|
char line[512];
|
|
char *currentSection = dupStr("");
|
|
|
|
while (fgets(line, sizeof(line), fp)) {
|
|
// Strip trailing whitespace/newline
|
|
char *end = line + strlen(line) - 1;
|
|
|
|
while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) {
|
|
*end-- = '\0';
|
|
}
|
|
|
|
char *p = line;
|
|
|
|
// Skip leading whitespace
|
|
while (*p == ' ' || *p == '\t') {
|
|
p++;
|
|
}
|
|
|
|
// Blank line -- store as comment to preserve formatting
|
|
if (*p == '\0') {
|
|
PrefsEntryT e = {0};
|
|
e.section = dupStr(currentSection);
|
|
e.value = dupStr("");
|
|
arrput(sEntries, e);
|
|
continue;
|
|
}
|
|
|
|
// Comment line
|
|
if (*p == ';' || *p == '#') {
|
|
PrefsEntryT e = {0};
|
|
e.section = dupStr(currentSection);
|
|
e.value = dupStr(line);
|
|
arrput(sEntries, e);
|
|
continue;
|
|
}
|
|
|
|
// Section header
|
|
if (*p == '[') {
|
|
char *close = strchr(p, ']');
|
|
|
|
if (close) {
|
|
*close = '\0';
|
|
free(currentSection);
|
|
currentSection = dupStr(trimInPlace(p + 1));
|
|
|
|
PrefsEntryT e = {0};
|
|
e.section = dupStr(currentSection);
|
|
arrput(sEntries, e);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Key=value
|
|
char *eq = strchr(p, '=');
|
|
|
|
if (eq) {
|
|
*eq = '\0';
|
|
|
|
PrefsEntryT e = {0};
|
|
e.section = dupStr(currentSection);
|
|
e.key = dupStr(trimInPlace(p));
|
|
e.value = dupStr(trimInPlace(eq + 1));
|
|
arrput(sEntries, e);
|
|
}
|
|
}
|
|
|
|
free(currentSection);
|
|
fclose(fp);
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsRemove
|
|
// ============================================================
|
|
|
|
void prefsRemove(const char *section, const char *key) {
|
|
int32_t idx = findEntry(section, key);
|
|
|
|
if (idx >= 0) {
|
|
freeEntry(&sEntries[idx]);
|
|
arrdel(sEntries, idx);
|
|
}
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsSave
|
|
// ============================================================
|
|
|
|
bool prefsSave(void) {
|
|
if (!sFilePath) {
|
|
return false;
|
|
}
|
|
|
|
return prefsSaveAs(sFilePath);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsSaveAs
|
|
// ============================================================
|
|
|
|
bool prefsSaveAs(const char *filename) {
|
|
FILE *fp = fopen(filename, "wb");
|
|
|
|
if (!fp) {
|
|
return false;
|
|
}
|
|
|
|
const char *lastSection = "";
|
|
|
|
for (int32_t i = 0; i < arrlen(sEntries); i++) {
|
|
PrefsEntryT *e = &sEntries[i];
|
|
|
|
// Comment or blank line (key=NULL, value=text or empty)
|
|
if (!e->key && e->value) {
|
|
fprintf(fp, "%s\r\n", e->value);
|
|
continue;
|
|
}
|
|
|
|
// Section header (key=NULL, value=NULL)
|
|
if (!e->key && !e->value) {
|
|
fprintf(fp, "[%s]\r\n", e->section);
|
|
lastSection = e->section;
|
|
continue;
|
|
}
|
|
|
|
// Key=value
|
|
if (e->key && e->value) {
|
|
fprintf(fp, "%s = %s\r\n", e->key, e->value);
|
|
}
|
|
}
|
|
|
|
fclose(fp);
|
|
return true;
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsSetBool
|
|
// ============================================================
|
|
|
|
void prefsSetBool(const char *section, const char *key, bool value) {
|
|
prefsSetString(section, key, value ? "true" : "false");
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsSetInt
|
|
// ============================================================
|
|
|
|
void prefsSetInt(const char *section, const char *key, int32_t value) {
|
|
char buf[32];
|
|
snprintf(buf, sizeof(buf), "%ld", (long)value);
|
|
prefsSetString(section, key, buf);
|
|
}
|
|
|
|
|
|
// ============================================================
|
|
// prefsSetString
|
|
// ============================================================
|
|
|
|
void prefsSetString(const char *section, const char *key, const char *value) {
|
|
int32_t idx = findEntry(section, key);
|
|
|
|
if (idx >= 0) {
|
|
// Update existing entry
|
|
free(sEntries[idx].value);
|
|
sEntries[idx].value = dupStr(value);
|
|
return;
|
|
}
|
|
|
|
// Find or create section header
|
|
int32_t secIdx = findSection(section);
|
|
|
|
if (secIdx < 0) {
|
|
// Add blank line before new section (unless file is empty)
|
|
if (arrlen(sEntries) > 0) {
|
|
PrefsEntryT blank = {0};
|
|
blank.section = dupStr(section);
|
|
blank.value = dupStr("");
|
|
arrput(sEntries, blank);
|
|
}
|
|
|
|
// Add section header
|
|
PrefsEntryT secEntry = {0};
|
|
secEntry.section = dupStr(section);
|
|
arrput(sEntries, secEntry);
|
|
secIdx = arrlen(sEntries) - 1;
|
|
}
|
|
|
|
// Find insertion point: after last entry in this section
|
|
int32_t insertAt = secIdx + 1;
|
|
|
|
while (insertAt < arrlen(sEntries)) {
|
|
PrefsEntryT *e = &sEntries[insertAt];
|
|
|
|
// Stop if we've hit a different section header
|
|
if (!e->key && !e->value && e->section &&
|
|
strcmpci(e->section, section) != 0) {
|
|
break;
|
|
}
|
|
|
|
// Stop if we've hit an entry from a different section
|
|
if (e->section && strcmpci(e->section, section) != 0) {
|
|
break;
|
|
}
|
|
|
|
insertAt++;
|
|
}
|
|
|
|
// Insert new entry
|
|
PrefsEntryT newEntry = {0};
|
|
newEntry.section = dupStr(section);
|
|
newEntry.key = dupStr(key);
|
|
newEntry.value = dupStr(value);
|
|
arrins(sEntries, insertAt, newEntry);
|
|
}
|