Skip to content

Wrap a value that is itself quoted when SetQuotes is on - #101

Merged
brofield merged 1 commit into
brofield:masterfrom
youdie006:quote-value-roundtrip
Sep 1, 2026
Merged

brofield merged 1 commit into
brofield:masterfrom
youdie006:quote-value-roundtrip

Conversation

@youdie006

Copy link
Copy Markdown
Contributor

With SetQuotes(true), a value that itself begins and ends with a quote loses that pair on every
save/load cycle.

stored=["foo"]   saved=[k = "foo"]   reload=[foo]     lost
stored=[""]      saved=[k = ""]      reload=[]        lost
stored=["a"b"]   saved=[k = "a"b"]   reload=[a"b]     lost
stored=[ foo ]   saved=[k = " foo "] reload=[ foo ]   ok

The loader strips an outer pair from any value whose first and last characters are quotes
(SimpleIni.h:1762):

if (m_bParseQuotes) {
    --pTrail;
    if (pTrail > a_pVal && *a_pVal == '"' && *pTrail == '"') {
        ++a_pVal;
        *pTrail = 0;
    }
}

but the writer only wraps when the value has leading or trailing whitespace
(IsSingleLineQuotedValue, SimpleIni.h:1833, gating the emit at SimpleIni.h:2652). The strip
condition is wider than the wrap condition, so anything in the gap round-trips lossily.

The loader already accepts the form the writer needs to produce

Run against the unmodified header:

k = ""foo""   ->   ["foo"]
j = """"      ->   [""]

So this is not a request for escape support — I know SetQuotes is deliberately simple quoting,
and the reader's own comment says escapes are not supported. It is that the writer does not emit
what its own reader already parses.

Change

Five lines in IsSingleLineQuotedValue: also wrap when the value begins and ends with a quote.
The length check mirrors the loader's pTrail > a_pVal, so a lone " — which the loader does not
strip — is not wrapped either.

// a value that is itself quoted needs wrapping, or the loader strips its quotes
if (a_pData > pStart + 1 && *pStart == '"' && *(a_pData - 1) == '"') {
    return true;
}

All four cases above then round-trip.

Output change, stated plainly

A value whose data begins and ends with a quote now serializes as ""foo"" rather than "foo".
That matters to anyone with SetQuotes(true) who stores such a value and feeds the file to a
different INI parser. With SetQuotes(false) the output is byte-identical, and replaying the
tests/ts-quotes.cpp corpus through both builds produces byte-identical files, since no value in it
begins and ends with a quote after parsing.

The full gtest suite passes both ways — 255 tests from 25 suites, before and after. (Built by
hand: cmake was not available here, so I used the TEST_SOURCES list from tests/CMakeLists.txt
with -Wl,--wrap=malloc, and checked the round trips separately under
-fsanitize=address,undefined, which is clean — this is data loss, not memory corruption.)


Disclosure: prepared with AI assistance; I verified the round trips, the unmodified-loader
behaviour and both suite runs myself.

The loader strips an outer quote pair from any value that begins and ends
with a quote, but the writer only wraps values with leading or trailing
whitespace, so such a value lost its quotes on every save/load cycle. The
loader already reads the doubled form correctly.
@brofield

brofield commented Sep 1, 2026

Copy link
Copy Markdown
Owner

Best way to submit fixes is to have a testcase that shows the failure with the master implementation and is fixed by your patch.

@brofield

brofield commented Sep 1, 2026

Copy link
Copy Markdown
Owner

I'll add it, just for future reference. Thanks for your submission.

@brofield
brofield merged commit 3a5e854 into brofield:master Sep 1, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants