From 8854c48400168c15ded5b1dc73aabf454ef2f3b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20M=C3=B6llendorf?= Date: Sat, 20 Apr 2024 22:56:54 +0200 Subject: [PATCH] Add support for escaped quotes, semicolon and hash in quoted values This commit will fix an inconsistency in the behavior of `iniparser_set()` and `iniparser_load()`. It will also make sure that dumping working dictionary results in an INI file which provides the same dictionary after loading this INI. The inconsistency is: - `iniparser_set()` supports escaped quotes, semicolon and hash in values: ``` "str\"ing" => str"ing "str;ing" => str;ing "str#ing" => str#ing ``` -`iniparser_load()` does not support semicolon and hash in unquoted values: ``` str;ing => str str#ing => str ``` Which is correct, because in this case semicolon and hash indicate that the rest of the line is a comment. - `iniparser_load()` supports quotes in unquoted values ``` str"ing => str"ing ``` - `iniparser_load()` supports semicolon and hash in quoted values: ``` "str;ing" => str;ing "str#ing" => str#ing ``` However, before this commit: - `iniparser_load()` did not support escaped quotes in quoted values: ``` "str\"ing" => str\ ``` And: - `iniparser_dump_ini()` wrote all values without quotes The implemented fix is: - `iniparser_load()` supports escaping iff the value is quoted - `iniparser_dump_ini()` will quote all values and escape '\' and '"' These changes do not break existing test cases. refs #97 --- src/iniparser.c | 78 +++++++++++++++++++++++++++++++++++++++---- src/iniparser.h | 18 ++++++++++ test/test_iniparser.c | 58 ++++++++++++++++++++++++++------ 3 files changed, 136 insertions(+), 18 deletions(-) diff --git a/src/iniparser.c b/src/iniparser.c index 0924d06..1e1c62f 100644 --- a/src/iniparser.c +++ b/src/iniparser.c @@ -247,6 +247,26 @@ void iniparser_dump(const dictionary * d, FILE * f) return ; } +static void escape_value(char *escaped, char *value) { + char c; + int v = 0; + int e = 0; + + if(!escaped || !value) + return; + + while((c = value[v]) != '\0') { + if(c == '\\' || c == '"') { + escaped[e] = '\\'; + e++; + } + escaped[e] = c; + v++; + e++; + } + escaped[e] = '\0'; +} + /*-------------------------------------------------------------------------*/ /** @brief Save a dictionary to a loadable ini file @@ -263,6 +283,7 @@ void iniparser_dump_ini(const dictionary * d, FILE * f) size_t i ; size_t nsec ; const char * secname ; + char escaped[ASCIILINESZ+1] = ""; if (d==NULL || f==NULL) return ; @@ -272,7 +293,8 @@ void iniparser_dump_ini(const dictionary * d, FILE * f) for (i=0 ; isize ; i++) { if (d->key[i]==NULL) continue ; - fprintf(f, "%s = %s\n", d->key[i], d->val[i]); + escape_value(escaped, d->val[i]); + fprintf(f, "%s = \"%s\"\n", d->key[i], escaped); } return ; } @@ -301,6 +323,7 @@ void iniparser_dumpsection_ini(const dictionary * d, const char * s, FILE * f) size_t j ; char keym[ASCIILINESZ+1]; int seclen ; + char escaped[ASCIILINESZ+1] = ""; if (d==NULL || f==NULL) return ; if (! iniparser_find_entry(d, s)) return ; @@ -312,10 +335,8 @@ void iniparser_dumpsection_ini(const dictionary * d, const char * s, FILE * f) if (d->key[j]==NULL) continue ; if (!strncmp(d->key[j], keym, seclen+1)) { - fprintf(f, - "%-30s = %s\n", - d->key[j]+seclen+1, - d->val[j] ? d->val[j] : ""); + escape_value(escaped, d->val[j]); + fprintf(f, "%-30s = \"%s\"\n", d->key[j]+seclen+1, escaped); } } fprintf(f, "\n"); @@ -642,6 +663,44 @@ void iniparser_unset(dictionary * ini, const char * entry) dictionary_unset(ini, strlwc(entry, tmp_str, sizeof(tmp_str))); } +static void parse_quoted_value(char *value, char quote) { + char c; + char *quoted; + int q = 0, v = 0; + int esc = 0; + + if(!value) + return; + + quoted = xstrdup(value); + + if(!quoted) { + iniparser_error_callback("iniparser: memory allocation failure\n"); + goto end_of_value; + } + + while((c = quoted[q]) != '\0') { + if(!esc) { + if(c == '\\') { + esc = 1; + q++; + continue; + } + + if(c == quote) { + goto end_of_value; + } + } + esc = 0; + value[v] = c; + v++; + q++; + } +end_of_value: + value[v] = '\0'; + free(quoted); +} + /*-------------------------------------------------------------------------*/ /** @brief Load a single line from an INI file @@ -661,6 +720,7 @@ static line_status iniparser_line( line_status sta ; char * line = NULL; size_t len ; + int d_quote; line = xstrdup(input_line); len = strstrip(line); @@ -678,11 +738,15 @@ static line_status iniparser_line( strstrip(section); strlwc(section, section, len); sta = LINE_SECTION ; - } else if (sscanf (line, "%[^=] = \"%[^\"]\"", key, value) == 2 - || sscanf (line, "%[^=] = '%[^\']'", key, value) == 2) { + } else if ((d_quote = sscanf (line, "%[^=] = \"%[^\n]\"", key, value)) == 2 + || sscanf (line, "%[^=] = '%[^\n]'", key, value) == 2) { /* Usual key=value with quotes, with or without comments */ strstrip(key); strlwc(key, key, len); + if(d_quote == 2) + parse_quoted_value(value, '"'); + else + parse_quoted_value(value, '\''); /* Don't strip spaces from values surrounded with quotes */ sta = LINE_VALUE ; } else if (sscanf (line, "%[^=] = %[^;#]", key, value) == 2) { diff --git a/src/iniparser.h b/src/iniparser.h index df5306f..ea00b1a 100644 --- a/src/iniparser.h +++ b/src/iniparser.h @@ -82,6 +82,13 @@ const char * iniparser_getsecname(const dictionary * d, int n); This function dumps a given dictionary into a loadable ini file. It is Ok to specify @c stderr or @c stdout as output files. + + All values are quoted these charecters are escaped: + + ” - the quote character (e.g. “String with ”Quotes””) + + \ - the backslash character (e.g. “C:\tmp”) + */ /*--------------------------------------------------------------------------*/ @@ -382,6 +389,17 @@ int iniparser_find_entry(const dictionary * ini, const char * entry) ; should not be accessed directly, but through accessor functions instead. + Iff the value is a quoted string it supports some escape sequences: + + ” - the quote character (e.g. “String with ”Quotes””) + + \ - the backslash character (e.g. “C:\tmp”) + + Escape sequences always start with a backslash. Additional escape sequences + might be added in the future. Backslash characters must be escaped. Any other + sequence then those outlined above is invalid and may lead to unpredictable + results. + The returned dictionary must be freed using iniparser_freedict(). */ /*--------------------------------------------------------------------------*/ diff --git a/test/test_iniparser.c b/test/test_iniparser.c index 03ae112..455b3e8 100644 --- a/test/test_iniparser.c +++ b/test/test_iniparser.c @@ -1066,11 +1066,7 @@ void Test_iniparser_quotes(CuTest *tc) goto rm_ini; } - /* - * iniparser_load() does not support escaped quotes in values if they are - * quoted - */ - CuAssertStrEquals(tc, "str\\", iniparser_getstring(dic, + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, QUOTES_INI_SEC ":" QUOTES_INI_ATTR0, NULL)); /* * iniparser_load() supports semicolon and hash in values if they are @@ -1089,7 +1085,7 @@ void Test_iniparser_quotes(CuTest *tc) /* * iniparser_load() supports quotes in attributes */ - CuAssertStrEquals(tc, "str\\", iniparser_getstring(dic, + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, QUOTES_INI_SEC ":" "str\"ing", NULL)); /* * iniparser_load() does not support semicolon or hash in attributes @@ -1101,8 +1097,50 @@ void Test_iniparser_quotes(CuTest *tc) /* * iniparser_load() does support colon in attributes */ - CuAssertStrEquals(tc, "str\\", iniparser_getstring(dic, + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, QUOTES_INI_SEC ":" "str:ing", NULL)); + ini = fopen(TMP_INI_PATH, "w+"); + + if (!ini) { + fprintf(stderr, "iniparser: cannot open %s\n", TMP_INI_PATH); + goto del_dic; + } + + /** + * Test iniparser_dump_ini() + */ + iniparser_dump_ini(dic, ini); + fclose(ini); + dictionary_del(dic); + dic = iniparser_load(TMP_INI_PATH); + + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR0, NULL)); + CuAssertStrEquals(tc, QUOTES_INI_VAL1, iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR1, NULL)); + CuAssertStrEquals(tc, QUOTES_INI_VAL2, iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR2, NULL)); + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR3, NULL)); + CuAssertStrEquals(tc, "str", iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR4, NULL)); + CuAssertStrEquals(tc, "str", iniparser_getstring(dic, + QUOTES_INI_SEC ":" QUOTES_INI_ATTR5, NULL)); + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, + QUOTES_INI_SEC ":" "str\"ing", NULL)); + CuAssertStrEquals(tc, NULL, iniparser_getstring(dic, + QUOTES_INI_SEC ":" "str;ing", NULL)); + CuAssertStrEquals(tc, NULL, iniparser_getstring(dic, + QUOTES_INI_SEC ":" "str#ing", NULL)); + CuAssertStrEquals(tc, QUOTES_INI_VAL0, iniparser_getstring(dic, + QUOTES_INI_SEC ":" "str:ing", NULL)); + dictionary_del(dic); + ret = remove(TMP_INI_PATH); + + if (ret) { + fprintf(stderr, "cannot remove file: %s\n", TMP_INI_PATH); + return; + } /** * test iniparser_set() */ @@ -1197,8 +1235,7 @@ void Test_iniparser_quotes(CuTest *tc) goto rm_ini; } - /* iniparser_load() does not support ; in values */ - CuAssertStrEquals(tc, "str", iniparser_getstring(dic, + CuAssertStrEquals(tc, QUOTES_INI_VAL1, iniparser_getstring(dic, QUOTES_INI_SEC ":" QUOTES_INI_ATTR1, NULL)); dictionary_del(dic); ret = remove(TMP_INI_PATH); @@ -1250,8 +1287,7 @@ void Test_iniparser_quotes(CuTest *tc) goto rm_ini; } - /* iniparser_load() does not support ; in values */ - CuAssertStrEquals(tc, "str", iniparser_getstring(dic, + CuAssertStrEquals(tc, QUOTES_INI_VAL2, iniparser_getstring(dic, QUOTES_INI_SEC ":" QUOTES_INI_ATTR2, NULL)); del_dic: dictionary_del(dic);