Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 54 additions & 6 deletions card.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,54 @@ func maybeGet(l []string, i int) string {
return ""
}

// isStructuredValue reports whether a property's value is a list of components
// separated by ';', each escaped per RFC 6350 section 3.4.
func isStructuredValue(key string) bool {
switch strings.ToUpper(key) {
case FieldName, FieldAddress:
return true
}
return false
}

// structuredValueEscaper escapes a single structured-value component
// (RFC 6350 section 3.4): backslash first so it doesn't double-escape the rest.
var structuredValueEscaper = strings.NewReplacer("\\", "\\\\", "\n", "\\n", ",", "\\,", ";", "\\;")

// formatStructuredValue escapes each component and joins them with ';'.
func formatStructuredValue(components []string) string {
escaped := make([]string, len(components))
for i, c := range components {
escaped[i] = structuredValueEscaper.Replace(c)
}
return strings.Join(escaped, ";")
}

// parseStructuredValue splits a structured value on unescaped ';' and unescapes
// each component. It reverses formatStructuredValue.
func parseStructuredValue(value string) []string {
var components []string
var b strings.Builder
for i := 0; i < len(value); i++ {
switch c := value[i]; {
case c == '\\' && i+1 < len(value):
if n := value[i+1]; n == 'n' || n == 'N' {
b.WriteByte('\n')
} else {
b.WriteByte(n)
}
i++
case c == ';':
components = append(components, b.String())
b.Reset()
default:
b.WriteByte(c)
}
}
components = append(components, b.String())
return components
}

// A Card is an address book entry.
type Card map[string][]*Field

Expand Down Expand Up @@ -435,7 +483,7 @@ type Name struct {
}

func newName(field *Field) *Name {
components := strings.Split(field.Value, ";")
components := parseStructuredValue(field.Value)
return &Name{
field,
maybeGet(components, 0),
Expand All @@ -450,13 +498,13 @@ func (n *Name) field() *Field {
if n.Field == nil {
n.Field = new(Field)
}
n.Field.Value = strings.Join([]string{
n.Field.Value = formatStructuredValue([]string{
n.FamilyName,
n.GivenName,
n.AdditionalName,
n.HonorificPrefix,
n.HonorificSuffix,
}, ";")
})
return n.Field
}

Expand Down Expand Up @@ -486,7 +534,7 @@ type Address struct {
}

func newAddress(field *Field) *Address {
components := strings.Split(field.Value, ";")
components := parseStructuredValue(field.Value)
return &Address{
field,
maybeGet(components, 0),
Expand All @@ -503,14 +551,14 @@ func (a *Address) field() *Field {
if a.Field == nil {
a.Field = new(Field)
}
a.Field.Value = strings.Join([]string{
a.Field.Value = formatStructuredValue([]string{
a.PostOfficeBox,
a.ExtendedAddress,
a.StreetAddress,
a.Locality,
a.Region,
a.PostalCode,
a.Country,
}, ";")
})
return a.Field
}
8 changes: 7 additions & 1 deletion decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,13 @@ func parseLine(l string) (key string, field *Field, err error) {
}
}

field.Value = parseValue(l)
if isStructuredValue(key) {
// Keep the raw value; its components are unescaped on access, splitting
// on unescaped ';' (RFC 6350 section 3.4).
field.Value = l
} else {
field.Value = parseValue(l)
}
return
}

Expand Down
8 changes: 7 additions & 1 deletion encoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,13 @@ func formatLine(key string, field *Field) string {
}
}

s += ":" + formatValue(field.Value)
if isStructuredValue(key) {
// The value is already escaped per component (RFC 6350 section 3.4);
// its ';' separators must stay literal.
s += ":" + field.Value
} else {
s += ":" + formatValue(field.Value)
}
return s
}

Expand Down
101 changes: 101 additions & 0 deletions structured_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package vcard

import (
"bytes"
"strings"
"testing"
)

func roundtripCard(t *testing.T, c Card) Card {
t.Helper()
var buf bytes.Buffer
if err := NewEncoder(&buf).Encode(c); err != nil {
t.Fatal(err)
}
out, err := NewDecoder(strings.NewReader(buf.String())).Decode()
if err != nil {
t.Fatal(err)
}
return out
}

func versionedCard() Card {
c := make(Card)
c.SetValue(FieldVersion, "4.0")
return c
}

// A literal ';' inside a structured component must survive a round trip: per
// RFC 6350 section 3.4 a ';' separates components only when unescaped.
func TestStructuredName_Roundtrip(t *testing.T) {
tests := []Name{
{FamilyName: "a;b", GivenName: "c"},
{FamilyName: "de Groot", GivenName: "Rene;", AdditionalName: ";x;"},
{FamilyName: `back\slash`, GivenName: "co,mma"},
{FamilyName: `ends-with\`, GivenName: "b"},
{FamilyName: `lit\;eral`, GivenName: "c"},
{FamilyName: "line\nbreak"},
{FamilyName: "Doe", GivenName: "J."},
}
for _, want := range tests {
c := versionedCard()
c.SetName(&want)
got := roundtripCard(t, c).Name()
if got == nil {
t.Fatalf("%+v: Name() nil after round trip", want)
}
if got.FamilyName != want.FamilyName || got.GivenName != want.GivenName ||
got.AdditionalName != want.AdditionalName {
t.Errorf("round trip: got (%q,%q,%q) want (%q,%q,%q)",
got.FamilyName, got.GivenName, got.AdditionalName,
want.FamilyName, want.GivenName, want.AdditionalName)
}
}
}

func TestStructuredAddress_Roundtrip(t *testing.T) {
want := &Address{
StreetAddress: "12 Main St; Apt 3",
Locality: "A,B",
Region: `C\D`,
Country: "x;y;z",
}
c := versionedCard()
c.SetAddress(want)
got := roundtripCard(t, c).Address()
if got == nil {
t.Fatal("Address() nil after round trip")
}
if got.StreetAddress != want.StreetAddress || got.Locality != want.Locality ||
got.Region != want.Region || got.Country != want.Country {
t.Errorf("round trip: got %+v want %+v", got, want)
}
}

// The encoded form escapes a literal ';' as '\;' and keeps separators literal.
func TestStructuredName_EncodedForm(t *testing.T) {
c := versionedCard()
c.SetName(&Name{FamilyName: "a;b", GivenName: "c"})
var buf bytes.Buffer
if err := NewEncoder(&buf).Encode(c); err != nil {
t.Fatal(err)
}
const want = "N:a\\;b;c;;;"
if !strings.Contains(buf.String(), want) {
t.Errorf("encoded N line: want %q in\n%s", want, buf.String())
}
}

// Only property values are escaped, not parameter values (RFC 6350 section 3.4):
// a ';' in a parameter must not gain a structured-value backslash escape.
func TestStructuredParam_Unaffected(t *testing.T) {
c := versionedCard()
c.Set("TEL", &Field{Value: "123", Params: Params{"TYPE": {"a;b"}}})
var buf bytes.Buffer
if err := NewEncoder(&buf).Encode(c); err != nil {
t.Fatal(err)
}
if strings.Contains(buf.String(), `\;`) {
t.Errorf("parameter value was structurally escaped:\n%s", buf.String())
}
}