/* cfg.c : gitconfig-style configuration file parser */ /* Copyright (c) 2012, 2026 Jon Mayo * Licensed under MIT-0 OR PUBLIC DOMAIN */ #include "cfg.h" #include #include #include struct cfg_entry { char *key, *value; }; struct cfg { struct cfg_entry *entries; int count, alloc; }; struct cfg * cfg_new(void) { return calloc(1, sizeof(struct cfg)); } void cfg_free(struct cfg *c) { int i; if (!c) return; for (i = 0; i < c->count; i++) { free(c->entries[i].key); free(c->entries[i].value); } free(c->entries); free(c); } static int cfg_set(struct cfg *c, const char *key, const char *value) { int i; char *tmp; for (i = 0; i < c->count; i++) { if (strcmp(c->entries[i].key, key) == 0) { tmp = strdup(value); if (!tmp) return -1; free(c->entries[i].value); c->entries[i].value = tmp; return 0; } } if (c->count >= c->alloc) { struct cfg_entry *ne; int na = c->alloc ? c->alloc * 2 : 16; ne = realloc(c->entries, (size_t)na * sizeof(c->entries[0])); if (!ne) return -1; c->entries = ne; c->alloc = na; } c->entries[c->count].key = strdup(key); c->entries[c->count].value = strdup(value); if (!c->entries[c->count].key || !c->entries[c->count].value) { free(c->entries[c->count].key); free(c->entries[c->count].value); return -1; } c->count++; return 0; } const char * cfg_get(const struct cfg *c, const char *key) { int i; if (!c) return NULL; for (i = 0; i < c->count; i++) if (strcmp(c->entries[i].key, key) == 0) return c->entries[i].value; return NULL; } int cfg_each(const struct cfg *c, int (*fn)(const char *key, const char *value, void *arg), void *arg) { int i, ret; if (!c) return 0; for (i = 0; i < c->count; i++) { ret = fn(c->entries[i].key, c->entries[i].value, arg); if (ret) return ret; } return 0; } static void rtrim(char *s) { char *e = s + strlen(s); while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r' || e[-1] == '\n')) e--; *e = '\0'; } /* Read a logical line with backslash-continuation and truncation detection. * Returns 1 on success, 0 on EOF, -1 on line too long. */ static int read_line(FILE *f, char *buf, size_t bufsz, int *lineno) { size_t pos = 0, end, nbs; int got = 0; for (;;) { if (bufsz - pos <= 1) return -1; if (!fgets(buf + pos, (int)(bufsz - pos), f)) return got ? 1 : 0; (*lineno)++; got = 1; pos += strlen(buf + pos); if (pos > 0 && buf[pos - 1] != '\n' && !feof(f)) return -1; end = pos; while (end > 0 && (buf[end - 1] == '\n' || buf[end - 1] == '\r')) end--; for (nbs = 0; nbs < end && buf[end - 1 - nbs] == '\\'; nbs++) ; if (nbs & 1) { buf[pos = end - 1] = '\0'; continue; } return 1; } } /* Unquote a value in place. Strips double quotes and handles * \", \\, \n, \t escapes inside quoted regions. */ static void unquote(char *s) { char *r = s, *w = s; int q = 0; while (*r) { if (*r == '"') { q = !q; r++; } else if (q && *r == '\\') { switch (r[1]) { case '"': case '\\': *w++ = r[1]; r += 2; break; case 'n': *w++ = '\n'; r += 2; break; case 't': *w++ = '\t'; r += 2; break; default: *w++ = *r++; break; } } else *w++ = *r++; } *w = '\0'; } int cfg_load(struct cfg *c, const char *path) { char buf[4096], key[4096]; FILE *f; int line, rc = -1, ret; char *section, *subsect, *p, *q, *tmp; if (!c || !(f = fopen(path, "r"))) return -1; section = strdup(""); if (!section) { fclose(f); return -1; } subsect = NULL; line = 0; while ((ret = read_line(f, buf, sizeof(buf), &line)) != 0) { if (ret < 0) { fprintf(stderr, "%s:%d: line too long\n", path, line); goto out; } /* strip comments (respecting quotes and escapes) */ for (p = buf; *p; p++) { if (*p == '#' || *p == ';') { *p = '\0'; break; } if (*p == '"') for (p++; *p && *p != '"'; p++) if (*p == '\\' && p[1]) p++; } p = buf + strspn(buf, " \t"); if (*p == '\0' || *p == '\n' || *p == '\r') continue; if (*p == '[') { p++; q = strchr(p, ']'); if (!q) { fprintf(stderr, "%s:%d: missing ']'\n", path, line); goto out; } *q = '\0'; free(subsect); subsect = NULL; q = strchr(p, '"'); if (q) { char *q2 = strchr(q + 1, '"'); if (!q2) { fprintf(stderr, "%s:%d: unterminated quote\n", path, line); goto out; } *q = '\0'; *q2 = '\0'; subsect = strdup(q + 1); if (!subsect) goto out; } p += strspn(p, " \t"); rtrim(p); tmp = strdup(p); if (!tmp) goto out; free(section); section = tmp; continue; } /* key = value */ q = strchr(p, '='); if (!q) { fprintf(stderr, "%s:%d: expected '='\n", path, line); goto out; } *q = '\0'; rtrim(p); q += 1 + strspn(q + 1, " \t"); rtrim(q); unquote(q); if (section[0] == '\0') snprintf(key, sizeof(key), "%s", p); else if (subsect) snprintf(key, sizeof(key), "%s.%s.%s", section, subsect, p); else snprintf(key, sizeof(key), "%s.%s", section, p); if (cfg_set(c, key, q) < 0) goto out; } rc = 0; out: fclose(f); free(section); free(subsect); return rc; }