From 6e6e06bdc46c2d8d24fab1bff21299cd6e86b3be Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sat, 28 Mar 2026 10:32:52 -0500 Subject: [PATCH] Fix quoted MIME-DIR parameter parsing Correct the vCard 3 parameter parser so quoted parameter values are consumed according to the MIME-DIR grammar instead of failing immediately on the opening double quote. The fix explicitly advances past the opening quote, reads the quoted qsafe-char sequence, and strips the surrounding quotes from the returned parameter value. Unquoted parameter handling is unchanged. Add private parser coverage for quoted parameter values and quoted values containing commas, plus a public regression test showing that quoted LANGUAGE and TYPE parameters are accepted by the vCard 3 parser. AI-Assisted: yes AI-Tool: OpenAI Codex / gpt-5.4 xhigh --- src/vcard/vcard3.nim | 30 +++++++++++++++++++++++++++++- tests/tvcard3.nim | 6 +++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/vcard/vcard3.nim b/src/vcard/vcard3.nim index 3b5625b..7e03b2a 100644 --- a/src/vcard/vcard3.nim +++ b/src/vcard/vcard3.nim @@ -902,11 +902,12 @@ proc readParamValue(p: var VCardParser): string = ## Read a single parameter value at the current read position or error. p.setBookmark if p.peek == '"': + discard p.read while QSAFE_CHARS.contains(p.peek): discard p.read if p.read != '"': p.error("quoted parameter value expected to end with a " & "double quote (\")") - result = p.readSinceBookmark[0 ..< ^1] + result = p.readSinceBookmark[1 ..< ^1] else: while SAFE_CHARS.contains(p.peek): discard p.read result = p.readSinceBookmark @@ -1393,6 +1394,24 @@ proc runVCard3PrivateTests*() = assert p.read == '=' assert p.readParamValue == "Fun&Games%" + # "readParamValue quoted": + block: + var p = initParser("FN;LANGUAGE=\"en\":John Smith") + assert p.readName == "FN" + assert p.read == ';' + assert p.readName == "LANGUAGE" + assert p.read == '=' + assert p.readParamValue == "en" + + # "readParamValue quoted with comma": + block: + var p = initParser("LABEL;TYPE=\"HOME,POSTAL\":123 Main St.") + assert p.readName == "LABEL" + assert p.read == ';' + assert p.readName == "TYPE" + assert p.read == '=' + assert p.readParamValue == "HOME,POSTAL" + # "readParams": block: var p = initParser("TEL;TYPE=WORK;TYPE=Fun&Games%,Extra:+15551234567") @@ -1407,6 +1426,15 @@ proc runVCard3PrivateTests*() = assert params[1].values[0] == "Fun&Games%" assert params[1].values[1] == "Extra" + # "readParams quoted": + block: + var p = initParser("FN;LANGUAGE=\"en\":John Smith") + assert p.readName == "FN" + let params = p.readParams + assert params.len == 1 + assert params[0].name == "LANGUAGE" + assert params[0].values == @["en"] + # "readValue": block: var p = initParser("TEL;TYPE=WORK:+15551234567\r\nFN:John Smith\r\n") diff --git a/tests/tvcard3.nim b/tests/tvcard3.nim index 91000de..1768e2d 100644 --- a/tests/tvcard3.nim +++ b/tests/tvcard3.nim @@ -202,9 +202,13 @@ suite "vcard/vcard3": let parsed = parseSingleVCard3(vcard3Doc( "VERSION:3.0", "FN;LANGUAGE=\"en\":John Smith", + "LABEL;TYPE=\"HOME,POSTAL\":123 Main St.", "N:Smith;John;;;")) - check parsed.fn.language == some("en") + check: + parsed.fn.language == some("en") + parsed.label.len == 1 + parsed.label[0].adrType == @["HOME,POSTAL"] test "spec: PROFILE is exposed as the standard property type": let parsed = parseSingleVCard3(vcard3Doc(