Skip to content

Escape ';' in N and ADR structured component values - #40

Open
gaoflow wants to merge 1 commit into
emersion:masterfrom
gaoflow:escape-structured-semicolon
Open

Escape ';' in N and ADR structured component values#40
gaoflow wants to merge 1 commit into
emersion:masterfrom
gaoflow:escape-structured-semicolon

Conversation

@gaoflow

@gaoflow gaoflow commented Jul 28, 2026

Copy link
Copy Markdown

The bug

A literal ; inside a structured-value component (N, ADR) is silently corrupted on a round trip through the library's own API:

c := make(vcard.Card)
c.SetValue(vcard.FieldVersion, "4.0")
c.SetName(&vcard.Name{FamilyName: "a;b", GivenName: "c"})
// encode -> "N:a;b;c;;;"  (the ';' in "a;b" is emitted as a raw separator)
// decode -> Name{FamilyName: "a", GivenName: "b", ...}

FamilyName comes back as "a" and "b" leaks into GivenName. The encoder writes the literal ; unescaped, and on decode it is read (correctly, per spec) as a component separator.

Root cause

RFC 6350 section 3.4 defines a component escape set of \\, \,, \; and \n; a ; separates components only when unescaped. Name.field() / Address.field() join components with a raw ; and newName / newAddress split on a raw ;, while the value escaper handled \\, \n and \, but never the sibling \;. So a literal ; in a component has no escaped representation and round-trips as a separator. \ and , are handled and round-trip fine — ; is the missing case.

Fix

Escape and unescape structured values per component (backslash, newline, comma and semicolon) and join/split on the unescaped ;. A literal ; now serialises as \;:

N:a\;b;c;;;

Because a component is escaped as a unit, the general value escaper must not run a second time over N/ADR, so the encoder and decoder leave those to the structured path. One consequence worth calling out: the raw Field.Value of an N/ADR field is now the escaped structured value (e.g. a\;b;c;;;); the components are obtained through Name() / Address() as before. Parameter values are deliberately untouched — they are quoted, not backslash-escaped, and are not structured values.

Tests

structured_test.go covers round-trips of ;, ,, \, \n, a backslash at a component end and a literal \;, for both N and ADR, asserts the encoded form (N:a\;b;c;;;), and asserts a ; in a parameter is not structurally escaped. Full suite passes; gofmt and go vet clean.

Related but not addressed here: ORG and CLIENTPIDMAP are also ;-separated but have no typed accessors, and CATEGORIES has the same separator-vs-literal question for ,; happy to follow up if you'd like those in scope.

A literal ';' inside a structured-value component (N, ADR) was neither
escaped on encoding nor recognised as escaped on decoding, so it was
emitted as a raw component separator and corrupted the round trip:

	c.SetName(&Name{FamilyName: "a;b", GivenName: "c"})

serialised to "N:a;b;c;;;" and parsed back with FamilyName "a" and the
rest of the value shifted into the following components.

RFC 6350 section 3.4 requires a literal ';' in a component to be escaped
as '\;'; only an unescaped ';' separates components. The existing escaper
handled '\\', '\n' and '\,' but never the sibling '\;'.

Escape and unescape structured values per component (backslash, newline,
comma and semicolon) and join/split on the unescaped ';'. Because a
component is now escaped as a unit, the general value escaper must not run
again over N and ADR, so the encoder and decoder leave those values to the
structured path. Parameter values are untouched (they are not structured).

The comma and backslash escaping already in place is unchanged.
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.

1 participant