Fix vCard 3 inline binary round-tripping

Decode ENCODING=b payloads for PHOTO, LOGO, SOUND, and KEY when parsing
so serializing a parsed card does not base64-encode already encoded wire
data a second time. Add regression coverage for both inline binary and
VALUE=uri round-trips.

AI-Assisted: yes
AI-Tool: OpenAI Codex /gpt-5.4 xhigh
This commit is contained in:
2026-03-28 10:13:45 -05:00
parent 466e47fd36
commit 35377f5a25
2 changed files with 42 additions and 8 deletions

View File

@@ -963,6 +963,20 @@ proc readTextValueList(
result = @[p.readTextValue] result = @[p.readTextValue]
while seps.contains(p.peek): result.add(p.readTextValue(ignorePrefix = seps)) while seps.contains(p.peek): result.add(p.readTextValue(ignorePrefix = seps))
proc readBinaryValue(
p: var VCardParser,
isInline: bool,
propName: string
): string =
let value = p.readValue
if not isInline:
return value
try:
return base64.decode(value)
except ValueError:
p.error("invalid base64 value for the " & propName & " content type")
proc parseContentLines*(p: var VCardParser): seq[VC3_Property] = proc parseContentLines*(p: var VCardParser): seq[VC3_Property] =
result = @[] result = @[]
@@ -1035,12 +1049,13 @@ proc parseContentLines*(p: var VCardParser): seq[VC3_Property] =
result.add(assignCommon(newVC3_Nickname(value = p.readValue))) result.add(assignCommon(newVC3_Nickname(value = p.readValue)))
of $pnPhoto: of $pnPhoto:
let isInline = params.existsWithValue("ENCODING", "B")
result.add(newVC3_Photo( result.add(newVC3_Photo(
group = group, group = group,
value = p.readValue, value = p.readBinaryValue(isInline, "PHOTO"),
valueType = params.getSingleValue("VALUE"), valueType = params.getSingleValue("VALUE"),
binaryType = params.getSingleValue("TYPE"), binaryType = params.getSingleValue("TYPE"),
isInline = params.existsWithValue("ENCODING", "B"))) isInline = isInline))
of $pnBday: of $pnBday:
let valueType = params.getSingleValue("VALUE") let valueType = params.getSingleValue("VALUE")
@@ -1121,12 +1136,13 @@ proc parseContentLines*(p: var VCardParser): seq[VC3_Property] =
result.add(assignCommon(newVC3_Role(value = p.readValue))) result.add(assignCommon(newVC3_Role(value = p.readValue)))
of $pnLogo: of $pnLogo:
let isInline = params.existsWithValue("ENCODING", "B")
result.add(newVC3_Logo( result.add(newVC3_Logo(
group = group, group = group,
value = p.readValue, value = p.readBinaryValue(isInline, "LOGO"),
valueType = params.getSingleValue("VALUE"), valueType = params.getSingleValue("VALUE"),
binaryType = params.getSingleValue("TYPE"), binaryType = params.getSingleValue("TYPE"),
isInline = params.existsWithValue("ENCODING", "B"))) isInline = isInline))
of $pnAgent: of $pnAgent:
let valueParam = params.getSingleValue("VALUE") let valueParam = params.getSingleValue("VALUE")
@@ -1182,12 +1198,13 @@ proc parseContentLines*(p: var VCardParser): seq[VC3_Property] =
result.add(assignCommon(newVC3_SortString(value = p.readValue))) result.add(assignCommon(newVC3_SortString(value = p.readValue)))
of $pnSound: of $pnSound:
let isInline = params.existsWithValue("ENCODING", "B")
result.add(newVC3_Sound( result.add(newVC3_Sound(
group = group, group = group,
value = p.readValue, value = p.readBinaryValue(isInline, "SOUND"),
valueType = params.getSingleValue("VALUE"), valueType = params.getSingleValue("VALUE"),
binaryType = params.getSingleValue("TYPE"), binaryType = params.getSingleValue("TYPE"),
isInline = params.existsWithValue("ENCODING", "B"))) isInline = isInline))
of $pnUid: of $pnUid:
result.add(newVC3_UID(group = group, value = p.readValue)) result.add(newVC3_UID(group = group, value = p.readValue))
@@ -1204,12 +1221,13 @@ proc parseContentLines*(p: var VCardParser): seq[VC3_Property] =
result.add(newVC3_Class(group = group, value = p.readValue)) result.add(newVC3_Class(group = group, value = p.readValue))
of $pnKey: of $pnKey:
let isInline = params.existsWithValue("ENCODING", "B")
result.add(newVC3_Key( result.add(newVC3_Key(
group = group, group = group,
value = p.readValue, value = p.readBinaryValue(isInline, "KEY"),
valueType = params.getSingleValue("VALUE"), valueType = params.getSingleValue("VALUE"),
keyType = params.getSingleValue("TYPE"), keyType = params.getSingleValue("TYPE"),
isInline = params.existsWithValue("ENCODING", "B"))) isInline = isInline))
else: else:
if not name.startsWith("X-"): if not name.startsWith("X-"):

View File

@@ -153,6 +153,22 @@ suite "vcard/vcard3":
serialized.contains("SOUND;ENCODING=b;TYPE=WAVE:" & payload) serialized.contains("SOUND;ENCODING=b;TYPE=WAVE:" & payload)
serialized.contains("KEY;ENCODING=b;TYPE=PGP:" & payload) serialized.contains("KEY;ENCODING=b;TYPE=PGP:" & payload)
test "spec: uri-backed binary properties 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"))
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: quoted parameter values are accepted": test "spec: quoted parameter values are accepted":
let parsed = parseSingleVCard3(vcard3Doc( let parsed = parseSingleVCard3(vcard3Doc(
"VERSION:3.0", "VERSION:3.0",