cnfparse.c

Small library to make config parsing easier
git clone git://git.finwo.net/lib/cnfparse.c
Log | Files | Refs | README | LICENSE

commit 72076448dca192d312fd4a794f144f951c3ad231
parent 6d9c129a2ee42c448076194d825e7a4c8a26a465
Author: finwo <finwo@pm.me>
Date:   Wed,  7 Oct 2026 15:46:45 +0200

Fix empty quote directive; split quote states for human-friendly behavior

Diffstat:
MREADME.md | 8+++++++-
Msrc/cnfparse.c | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/cnfparse.h | 8+++++++-
Mtest/0000-basics.c | 110+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 180 insertions(+), 12 deletions(-)

diff --git a/README.md b/README.md @@ -52,7 +52,13 @@ void cnf_directive_free(struct cnf_directive *subject); <details> <summary>cnf_directive_read(fd)</summary> - Returns a single directive (or NULL) from the file descriptor + Returns a single directive (or NULL) from the file descriptor. + + Arguments are separated by whitespace. Double-quoted strings may contain + spaces and support the escapes `\n`, `\t`, `\r`, `\\`, `\"` and `\'`; + unknown escapes keep the character verbatim, and `\0` becomes a literal + `0` (with a warning on stderr). Single-quoted strings are literal except + for `\'`. ```C struct cnf_directive * cnf_directive_read(FILE *fd); diff --git a/src/cnfparse.c b/src/cnfparse.c @@ -4,6 +4,7 @@ extern "C" { // includes/system {{{ #include <stdbool.h> +#include <stdio.h> #include <stdlib.h> #include <string.h> // }}} @@ -16,13 +17,32 @@ extern "C" { #define STATE_BLANK 0 #define STATE_COMMENT 1 -#define STATE_QUOTE 2 -#define STATE_WORD 3 +#define STATE_DQUOTE 2 +#define STATE_SQUOTE 3 +#define STATE_WORD 4 + +// cnf_unescape(c) {{{ +// Unknown escapes keep the character verbatim +static int cnf_unescape(char c) { + switch (c) { + case 'n': return '\n'; + case 't': return '\t'; + case 'r': return '\r'; + case '\\': return '\\'; + case '"': return '"'; + case '\'': return '\''; + case '0': + fprintf(stderr, "cnfparse: warning: \\0 becomes '0', not a null byte\n"); + return '0'; + default: return c; + } +} +// }}} // cnf_directive_append(subject,value,len) {{{ // caller frees void cnf_directive_append(struct cnf_directive *subject, char *value, size_t len) { - if (!value) return; + if (!value) value = ""; // len is leading, not value char *val = calloc(1, len+1); memcpy(val, value, len); if (!subject->name) { @@ -57,7 +77,6 @@ struct cnf_directive * cnf_directive_read(FILE *fd) { int state = STATE_BLANK; char c; - char quote; bool escape; while(!feof(fd)) { @@ -80,9 +99,11 @@ struct cnf_directive * cnf_directive_read(FILE *fd) { state = STATE_COMMENT; continue; case '"': + state = STATE_DQUOTE; + escape = false; + continue; case '\'': - state = STATE_QUOTE; - quote = c; + state = STATE_SQUOTE; escape = false; continue; default: @@ -103,9 +124,34 @@ struct cnf_directive * cnf_directive_read(FILE *fd) { continue; } break; - case STATE_QUOTE: + case STATE_DQUOTE: + if (escape) { + buf_append_byte(acc, cnf_unescape(c)); + escape = false; + continue; + } + if (c == '\\') { + escape = true; + continue; + } + if (c == '"') { + cnf_directive_append(output, acc->dat, acc->len); + state = STATE_BLANK; + memset(acc->dat, 0, acc->cap); + acc->len = 0; + continue; + } + buf_append_byte(acc, c); + continue; + case STATE_SQUOTE: if (escape) { - buf_append_byte(acc, c); + // only \' is an escape inside single quotes; any other backslash stays literal + if (c == '\'') { + buf_append_byte(acc, '\''); + } else { + buf_append_byte(acc, '\\'); + buf_append_byte(acc, c); + } escape = false; continue; } @@ -113,7 +159,7 @@ struct cnf_directive * cnf_directive_read(FILE *fd) { escape = true; continue; } - if (c == quote) { + if (c == '\'') { cnf_directive_append(output, acc->dat, acc->len); state = STATE_BLANK; memset(acc->dat, 0, acc->cap); @@ -121,7 +167,7 @@ struct cnf_directive * cnf_directive_read(FILE *fd) { continue; } buf_append_byte(acc, c); - continue;; + continue; case STATE_WORD: switch(c) { case ' ': diff --git a/src/cnfparse.h b/src/cnfparse.h @@ -60,7 +60,13 @@ void cnf_directive_free(struct cnf_directive *subject); /// <details> /// <summary>cnf_directive_read(fd)</summary> /// -/// Returns a single directive (or NULL) from the file descriptor +/// Returns a single directive (or NULL) from the file descriptor. +/// +/// Arguments are separated by whitespace. Double-quoted strings may contain +/// spaces and support the escapes `\n`, `\t`, `\r`, `\\`, `\"` and `\'`; +/// unknown escapes keep the character verbatim, and `\0` becomes a literal +/// `0` (with a warning on stderr). Single-quoted strings are literal except +/// for `\'`. ///<C struct cnf_directive * cnf_directive_read(FILE *fd); ///> diff --git a/test/0000-basics.c b/test/0000-basics.c @@ -131,6 +131,107 @@ void test_escaped_quote_in_double() { fclose(fd); } +void test_escaped_newline_in_double_quotes() { + FILE *fd = open_string("op \"a\\nb\"\n"); // config text: op "a\nb" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with escaped newline returned", dir != NULL); + ASSERT_STRING_EQUALS("op", dir->name); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("a\nb", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_escaped_tab_cr_in_double_quotes() { + FILE *fd = open_string("op \"a\\tb\\rc\"\n"); // config text: op "a\tb\rc" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with escaped tab/cr returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("a\tb\rc", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_escaped_backslash_in_double_quotes() { + FILE *fd = open_string("op \"a\\\\b\"\n"); // config text: op "a\\b" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with escaped backslash returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("a\\b", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_unknown_escape_kept_verbatim() { + FILE *fd = open_string("op \"a\\zb\"\n"); // config text: op "a\zb" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with unknown escape returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("azb", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_null_escape_becomes_zero() { + FILE *fd = open_string("op \"a\\0b\"\n"); // config text: op "a\0b" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with null escape returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("a0b", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_single_quotes_keep_backslash_literal() { + FILE *fd = open_string("op 'a\\nb'\n"); // config text: op 'a\nb' + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with literal backslash in single quotes returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("a\\nb", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_escaped_single_quote_in_single_quotes() { + FILE *fd = open_string("op 'it\\'s'\n"); // config text: op 'it\'s' + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with escaped single quote returned", dir != NULL); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("it's", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_empty_double_quote_first_token() { + FILE *fd = open_string("\"\" foo\n"); // config text: "" foo + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with empty first token returned", dir != NULL); + ASSERT_STRING_EQUALS("", dir->name); + ASSERT_EQUALS(1, dir->argc); + ASSERT_STRING_EQUALS("foo", dir->argv[0]); + cnf_directive_free(dir); + fclose(fd); +} + +void test_empty_double_quote_alone() { + FILE *fd = open_string("\"\"\n"); // config text: "" + ASSERT("fmemopen succeeds", fd != NULL); + struct cnf_directive *dir = cnf_directive_read(fd); + ASSERT("directive with only an empty token returned", dir != NULL); + ASSERT_STRING_EQUALS("", dir->name); + ASSERT_EQUALS(0, dir->argc); + cnf_directive_free(dir); + fclose(fd); +} + void test_hash_in_quoted_string() { FILE *fd = open_string("set note \"a # b\"\n"); ASSERT("fmemopen succeeds", fd != NULL); @@ -242,6 +343,15 @@ int main(int argc, const char *argv[]) { RUN(test_quoted_double_args); RUN(test_quoted_single_args); RUN(test_escaped_quote_in_double); + RUN(test_escaped_newline_in_double_quotes); + RUN(test_escaped_tab_cr_in_double_quotes); + RUN(test_escaped_backslash_in_double_quotes); + RUN(test_unknown_escape_kept_verbatim); + RUN(test_null_escape_becomes_zero); + RUN(test_single_quotes_keep_backslash_literal); + RUN(test_escaped_single_quote_in_single_quotes); + RUN(test_empty_double_quote_first_token); + RUN(test_empty_double_quote_alone); RUN(test_hash_in_quoted_string); RUN(test_trailing_comment); RUN(test_comment_no_space);