From 8f2a05cde6b642971bac777b30d7b881b4fd6c0f Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sat, 28 Mar 2026 10:46:37 -0500 Subject: [PATCH] Fix vCard 3 REV and KEY value handling Bring the remaining vCard 3 REV and KEY behavior into line with RFC 2426. For REV, serialize VALUE=date using the date form instead of incorrectly emitting VALUE=date-time with a timestamp payload. For KEY, stop defaulting constructed values to VALUE=uri. The vCard 3 specification defines KEY as binary by default and allows it to be reset to text, but not to uri. Tighten both construction and parsing accordingly: reject VALUE=uri for KEY, enforce the relationship between VALUE=binary and ENCODING=b, and reject VALUE=text when ENCODING=b is present. Update the regression coverage to reflect the spec boundary: PHOTO, LOGO, and SOUND may round-trip as uris; KEY may contain text that looks like a URI; KEY does not allow VALUE=uri; and vCard 3 KEY parameters still require name=value syntax. AI-Assisted: yes AI-Tool: OpenAI Codex / gpt-5.4 xhigh --- src/vcard/vcard3.nim | 26 +++++++++++++++++++++++--- tests/tvcard3.nim | 31 +++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 7 deletions(-) diff --git a/src/vcard/vcard3.nim b/src/vcard/vcard3.nim index 7e03b2a..b8953b0 100644 --- a/src/vcard/vcard3.nim +++ b/src/vcard/vcard3.nim @@ -609,11 +609,22 @@ func newVC3_Class*(value: string, group = none[string]()): VC3_Class = func newVC3_Key*( value: string, - valueType = some("uri"), + valueType = none[string](), keyType = none[string](), isInline = false, group = none[string]()): VC3_Key = + if valueType.isSome: + if valueType.get == $vtUri: + raise newException(ValueError, + "KEY content does not support the '" & $vtUri & "' value type") + elif valueType.get == $vtBinary and not isInline: + raise newException(ValueError, + "KEY content with VALUE=binary must also specify ENCODING=b") + elif valueType.get == $vtText and isInline: + raise newException(ValueError, + "KEY content with ENCODING=b cannot use VALUE=text") + return assignFields( VC3_Key(name: $pnKey, binaryType: keyType), value, valueType, keyType, isInline, group) @@ -855,7 +866,7 @@ proc serialize(r: VC3_Rev): string = if r.valueType.isSome and r.valueType.get == "date-time": result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT) elif r.valueType.isSome and r.valueType.get == "date": - result &= ";VALUE=date-time:" & r.value.format(DATETIME_FMT) + result &= ";VALUE=date:" & r.value.format(DATE_FMT) else: result &= r.value.format(DATETIME_FMT) @@ -1264,11 +1275,20 @@ proc parseContentLines*(p: var VCardParser): seq[VC3_Property] = result.add(newVC3_Class(group = group, value = p.readValue)) of $pnKey: + let valueType = params.getSingleValue("VALUE") let isInline = params.existsWithValue("ENCODING", "B") + if valueType.isSome: + if valueType.get == $vtUri: + p.error("invalid VALUE for KEY content. " & + "Expected '" & $vtText & "' or '" & $vtBinary & "'") + elif valueType.get == $vtBinary and not isInline: + p.error("KEY content with VALUE=binary must also specify ENCODING=b") + elif valueType.get == $vtText and isInline: + p.error("KEY content with ENCODING=b cannot use VALUE=text") result.add(newVC3_Key( group = group, value = p.readBinaryValue(isInline, "KEY"), - valueType = params.getSingleValue("VALUE"), + valueType = valueType, keyType = params.getSingleValue("TYPE"), isInline = isInline)) diff --git a/tests/tvcard3.nim b/tests/tvcard3.nim index 1768e2d..59a6f83 100644 --- a/tests/tvcard3.nim +++ b/tests/tvcard3.nim @@ -182,21 +182,44 @@ suite "vcard/vcard3": serialized.contains("SOUND;ENCODING=b;TYPE=WAVE:" & payload) serialized.contains("KEY;ENCODING=b;TYPE=PGP:" & payload) - test "spec: uri-backed binary properties round-trip as uris": + test "spec: PHOTO, LOGO, and SOUND may round-trip as uris": let serialized = $parseSingleVCard3(vcard3Doc( "VERSION:3.0", "FN:John Smith", "N:Smith;John;;;", "PHOTO;VALUE=uri:http://example.test/photo.jpg", "LOGO;VALUE=uri:http://example.test/logo.gif", - "SOUND;VALUE=uri:http://example.test/sound.wav", - "KEY;VALUE=uri:http://example.test/key.asc")) + "SOUND;VALUE=uri:http://example.test/sound.wav")) check: serialized.contains("PHOTO;VALUE=uri:http://example.test/photo.jpg") serialized.contains("LOGO;VALUE=uri:http://example.test/logo.gif") serialized.contains("SOUND;VALUE=uri:http://example.test/sound.wav") - serialized.contains("KEY;VALUE=uri:http://example.test/key.asc") + + test "spec: KEY does not allow uri values": + expect(VCardParsingError): + discard parseVCards(vcard3Doc( + "VERSION:3.0", + "FN:John Smith", + "N:Smith;John;;;", + "KEY;VALUE=uri:http://example.test/key.asc")) + + test "spec: KEY may contain text values that look like uris": + let serialized = $parseSingleVCard3(vcard3Doc( + "VERSION:3.0", + "FN:John Smith", + "N:Smith;John;;;", + "KEY;TYPE=PGP:http://example.test/key.pgp")) + + check serialized.contains("KEY;TYPE=PGP:http://example.test/key.pgp") + + test "spec: KEY parameters must use name=value syntax in vCard 3": + expect(VCardParsingError): + discard parseVCards(vcard3Doc( + "VERSION:3.0", + "FN:John Smith", + "N:Smith;John;;;", + "KEY;PGP:http://example.test/key.pgp")) test "spec: quoted parameter values are accepted": let parsed = parseSingleVCard3(vcard3Doc(