From 9a37c039c3086f6485c696c8cb02ed718399eefb Mon Sep 17 00:00:00 2001 From: Jamiras Date: Thu, 16 Jul 2026 16:56:37 -0600 Subject: [PATCH 1/6] allow quotes in conf values --- libretro-common/file/config_file.c | 42 ++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/libretro-common/file/config_file.c b/libretro-common/file/config_file.c index afa9cfa2bfa7..b00eea084a26 100644 --- a/libretro-common/file/config_file.c +++ b/libretro-common/file/config_file.c @@ -229,10 +229,26 @@ static char *config_file_extract_value(char *line) /* If this a ("), then value string is empty */ if (*line != '"') { + char c; /* Find the next (") character */ - while (line[idx] && (line[idx] != '\"')) + while ((c = line[idx]) && c != '\"') idx++; + /* If it's a quote preceed by a backslash, unescape it and find the next (") character */ + if (c && line[idx - 1] == '\\') + { + size_t read_idx = idx + 1; /* Skip over the quote */ + + do + { + line[idx - 1] = '\"'; /* Replace the backslash with a quote */ + + while ((c = line[read_idx++]) && c != '\"') + line[idx++] = c; + + } while (c && line[idx - 1] == '\\'); /* If it's another escaped quote, keep going */ + } + line[idx] = '\0'; if ((value = line) && *value) return strdup(value); @@ -1462,7 +1478,29 @@ void config_file_dump(config_file_t *conf, FILE *file, bool sort) while (list) { if (!list->readonly && list->key) - fprintf(file, "%s = \"%s\"\n", list->key, list->value); + { + const char* quote = strchr(list->value, '\"'); + if (!quote) + { + fprintf(file, "%s = \"%s\"\n", list->key, list->value); + } + else + { + const char* start = list->value; + fprintf(file, "%s = \"", list->key); + do + { + if (quote > start) + fwrite(start, 1, quote - start, file); + fputc('\\', file); + fputc('\"', file); + start = quote + 1; + quote = strchr(start, '\"'); + } while (quote); + + fprintf(file, "%s\"\n", start); + } + } list = list->next; } From c6b5a938b2fcbbe3b17b6a5f84b004713568e11b Mon Sep 17 00:00:00 2001 From: Jamiras Date: Fri, 17 Jul 2026 07:06:16 -0600 Subject: [PATCH 2/6] force CI rebuild From 4b44c16961fbcd628a9494f37d1bce1c7bada943 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Mon, 20 Jul 2026 08:12:52 -0600 Subject: [PATCH 3/6] support backslash as last character of quoted string --- libretro-common/file/config_file.c | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/libretro-common/file/config_file.c b/libretro-common/file/config_file.c index b00eea084a26..6b1dcf866cef 100644 --- a/libretro-common/file/config_file.c +++ b/libretro-common/file/config_file.c @@ -241,11 +241,18 @@ static char *config_file_extract_value(char *line) do { - line[idx - 1] = '\"'; /* Replace the backslash with a quote */ - - while ((c = line[read_idx++]) && c != '\"') - line[idx++] = c; - + c = line[read_idx]; + if (!c) + { + /* If the quote is the last character of the line, assume it's not escaped */ + } + else + { + line[idx - 1] = '\"'; /* Replace the backslash with a quote */ + + while ((c = line[read_idx++]) && c != '\"') + line[idx++] = c; + } } while (c && line[idx - 1] == '\\'); /* If it's another escaped quote, keep going */ } From 71a67bea94293acdd645a23fe2f135a629a9c17d Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 9 Aug 2026 07:02:11 -0600 Subject: [PATCH 4/6] handle # in escaped-quoted string and trailing whitespace --- libretro-common/file/config_file.c | 53 ++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/libretro-common/file/config_file.c b/libretro-common/file/config_file.c index 74bbc206f2e5..16b1f88a81d0 100644 --- a/libretro-common/file/config_file.c +++ b/libretro-common/file/config_file.c @@ -379,6 +379,17 @@ static char *config_file_strip_comment(char *str) /* Search for the end of the string literal * value */ char *literal_end = strchr(literal_start + 1, '\"'); + while (literal_end && literal_end[-1] == '\\') + { + if (literal_end > comment) + { + comment = strchr(literal_end, '#'); + if (!comment) + return NULL; + } + + literal_end = strchr(literal_end + 1, '\"'); + } /* Check whether string literal end occurs * *after* the comment character @@ -430,27 +441,33 @@ static char *config_file_extract_value(char *line, unsigned p_opts, char *end = strchr(line, '\"'); idx = end ? (size_t)(end - line) : strlen(line); - /* If it's a quote preceed by a backslash, unescape it and find the next (") character */ + /* If it's a quote preceded by a backslash, unescape it and find the next (") character */ if (end && end[-1] == '\\') { - size_t read_idx = idx + 1; /* Skip over the quote */ - char c; - - do + const char* read = end; + const char* next_quote; + + /* Only treat the quote as escaped if another (") exists later on the + * line; otherwise it terminates the value. This handles values ending + * in '\', or where trailing whitespace exists after the quote. */ + next_quote = strchr(end + 1, '\"'); + if (next_quote) { - c = line[read_idx]; - if (!c) - { - /* If the quote is the last character of the line, assume it's not escaped */ - } - else - { - line[idx - 1] = '\"'; /* Replace the backslash with a quote */ - - while ((c = line[read_idx++]) && c != '\"') - line[idx++] = c; - } - } while (c && line[idx - 1] == '\\'); /* If it's another escaped quote, keep going */ + do { + /* shift the text to overwrite the last backslash */ + const size_t copy_len = next_quote - read; + memcpy(&line[idx - 1], read, copy_len); + idx += copy_len - 1; + + /* if this quote is not escaped, stop */ + if (next_quote[-1] != '\\') + break; + + /* consume the backslash and find the next quote */ + read = next_quote; + next_quote = strchr(read + 1, '\"'); + } while (next_quote); + } } line[idx] = '\0'; From 7ce078cd694893c7e80ed013055eb83c3ebb28f8 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 9 Aug 2026 07:14:34 -0600 Subject: [PATCH 5/6] memcpy behavior with overlapping buffer is not guaranteed --- libretro-common/file/config_file.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libretro-common/file/config_file.c b/libretro-common/file/config_file.c index 16b1f88a81d0..4354f1d53de3 100644 --- a/libretro-common/file/config_file.c +++ b/libretro-common/file/config_file.c @@ -455,9 +455,9 @@ static char *config_file_extract_value(char *line, unsigned p_opts, { do { /* shift the text to overwrite the last backslash */ - const size_t copy_len = next_quote - read; - memcpy(&line[idx - 1], read, copy_len); - idx += copy_len - 1; + --idx; + while (read < next_quote) + line[idx++] = *read++; /* if this quote is not escaped, stop */ if (next_quote[-1] != '\\') From 5b6f6496058500750520a57363b36478e94efb15 Mon Sep 17 00:00:00 2001 From: Jamiras Date: Sun, 9 Aug 2026 07:32:58 -0600 Subject: [PATCH 6/6] update comments --- libretro-common/file/config_file.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/libretro-common/file/config_file.c b/libretro-common/file/config_file.c index 4354f1d53de3..5f2287dd1862 100644 --- a/libretro-common/file/config_file.c +++ b/libretro-common/file/config_file.c @@ -448,8 +448,8 @@ static char *config_file_extract_value(char *line, unsigned p_opts, const char* next_quote; /* Only treat the quote as escaped if another (") exists later on the - * line; otherwise it terminates the value. This handles values ending - * in '\', or where trailing whitespace exists after the quote. */ + * line; otherwise it terminates the value. This handles values ending in + * backslash (\), or where trailing whitespace exists after the quote. */ next_quote = strchr(end + 1, '\"'); if (next_quote) { @@ -463,7 +463,9 @@ static char *config_file_extract_value(char *line, unsigned p_opts, if (next_quote[-1] != '\\') break; - /* consume the backslash and find the next quote */ + /* find the next quote and loop. if one is not found, the last + * escape sequence should be treated as two separate characters + * (\") and the string is considered to be terminated. */ read = next_quote; next_quote = strchr(read + 1, '\"'); } while (next_quote);