Skip to content

Commit

Permalink
FieldValue type
Browse files Browse the repository at this point in the history
Co-authored-by: GorthMohogany <[email protected]>
  • Loading branch information
oliverpool and GorthMohogany committed Oct 24, 2024
1 parent 8fda7d2 commit ae7038e
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 53 deletions.
100 changes: 79 additions & 21 deletions card.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ func (c Card) Preferred(k string) *Field {

// Value returns the first field value of the card for the given property. If
// there is no such field, it returns an empty string.
func (c Card) Value(k string) string {
func (c Card) Value(k string) FieldValue {
f := c.Get(k)
if f == nil {
return ""
Expand All @@ -156,18 +156,18 @@ func (c Card) Value(k string) string {

// AddValue adds the k, v pair to the list of field values. It appends to any
// existing values.
func (c Card) AddValue(k, v string) {
func (c Card) AddValue(k string, v FieldValue) {
c.Add(k, &Field{Value: v})
}

// SetValue sets the field k to the single value v. It replaces any existing
// value.
func (c Card) SetValue(k, v string) {
func (c Card) SetValue(k string, v FieldValue) {
c.Set(k, &Field{Value: v})
}

// PreferredValue returns the preferred field value of the card.
func (c Card) PreferredValue(k string) string {
func (c Card) PreferredValue(k string) FieldValue {
f := c.Preferred(k)
if f == nil {
return ""
Expand All @@ -176,13 +176,13 @@ func (c Card) PreferredValue(k string) string {
}

// Values returns a list of values for a given property.
func (c Card) Values(k string) []string {
func (c Card) Values(k string) []FieldValue {
fields := c[k]
if fields == nil {
return nil
}

values := make([]string, len(fields))
values := make([]FieldValue, len(fields))
for i, f := range fields {
values[i] = f.Value
}
Expand All @@ -192,7 +192,7 @@ func (c Card) Values(k string) []string {
// Kind returns the kind of the object represented by this card. If it isn't
// specified, it returns the default: KindIndividual.
func (c Card) Kind() Kind {
kind := strings.ToLower(c.Value(FieldKind))
kind := strings.ToLower(c.Value(FieldKind).String())
if kind == "" {
return KindIndividual
}
Expand All @@ -201,7 +201,7 @@ func (c Card) Kind() Kind {

// SetKind sets the kind of the object represented by this card.
func (c Card) SetKind(kind Kind) {
c.SetValue(FieldKind, string(kind))
c.SetValue(FieldKind, NewFieldValue(string(kind)))
}

// FormattedNames returns formatted names of the card. The length of the result
Expand Down Expand Up @@ -251,7 +251,7 @@ func (c Card) SetName(name *Name) {
// Gender returns this card's gender.
func (c Card) Gender() (sex Sex, identity string) {
v := c.Value(FieldGender)
parts := strings.SplitN(v, ";", 2)
parts := strings.SplitN(v.String(), ";", 2)
return Sex(strings.ToUpper(parts[0])), maybeGet(parts, 1)
}

Expand All @@ -261,7 +261,7 @@ func (c Card) SetGender(sex Sex, identity string) {
if identity != "" {
v += ";" + identity
}
c.SetValue(FieldGender, v)
c.SetValue(FieldGender, NewFieldValue(v))
}

// Addresses returns addresses of the card.
Expand Down Expand Up @@ -300,17 +300,17 @@ func (c Card) SetAddress(address *Address) {

// Categories returns category information about the card, also known as "tags".
func (c Card) Categories() []string {
return strings.Split(c.PreferredValue(FieldCategories), ",")
return c.PreferredValue(FieldCategories).Values()
}

// SetCategories sets category information about the card.
func (c Card) SetCategories(categories []string) {
c.SetValue(FieldCategories, strings.Join(categories, ","))
c.SetValue(FieldCategories, NewFieldValue(categories...))
}

// Revision returns revision information about the current card.
func (c Card) Revision() (time.Time, error) {
rev := c.Value(FieldRevision)
rev := c.Value(FieldRevision).String()
if rev == "" {
return time.Time{}, nil
}
Expand All @@ -319,16 +319,74 @@ func (c Card) Revision() (time.Time, error) {

// SetRevision sets revision information about the current card.
func (c Card) SetRevision(t time.Time) {
c.SetValue(FieldRevision, t.Format(timestampLayout))
c.SetValue(FieldRevision, NewFieldValue(t.Format(timestampLayout)))
}

// A field contains a value and some parameters.
type Field struct {
Value string
Value FieldValue
Params Params
Group string
}

// FieldValue contains the raw field value.
// Use [NewFieldValue] to construct a properly escaped field value.
type FieldValue string

var valueFormatter = strings.NewReplacer("\\", "\\\\", "\n", "\\n", ",", "\\,")
var valueParser = strings.NewReplacer("\\\\", "\\", "\\n", "\n", "\\,", ",")

// NewFieldValue creates a new FieldValue after
// escaping each part (backslash, newline, comma)
func NewFieldValue(parts ...string) FieldValue {
for i := range parts {
parts[i] = valueFormatter.Replace(parts[i])
}
return FieldValue(strings.Join(parts, ","))
}

// String returns the raw value of the field
func (fv FieldValue) String() string {
return string(fv)
}

// Values returns the field value parts, split by comma (each part being unescaped).
// Guaranteed to have a len > 0
func (fv FieldValue) Values() []string {
parts := stringsSplitUnescaped(string(fv), ',')
for i := range parts {
parts[i] = valueParser.Replace(parts[i])
}
return parts
}

// stringsSplitUnescaped splits the string when "sep" is NOT escaped
// (i.e. not precedeed by a[n unescaped] backslash)
func stringsSplitUnescaped(s string, sep rune) []string {
n := strings.Count(s, string(sep))
if n <= 0 {
return []string{s}
}
ss := make([]string, 0, n) // might not be full if some sep are escaped
escaping := false
i := 0
for j, c := range s {
if escaping {
escaping = false
continue
}
switch c {
case '\\':
escaping = true
case sep:
ss = append(ss, s[i:j])
i = j + 1
}
}
ss = append(ss, s[i:])
return ss[:len(ss):len(ss)] // clip the slice
}

// Params is a set of field parameters.
type Params map[string][]string

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

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

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

func newAddress(field *Field) *Address {
components := strings.Split(field.Value, ";")
components := strings.Split(field.Value.String(), ";")
return &Address{
field,
maybeGet(components, 0),
Expand All @@ -503,14 +561,14 @@ func (a *Address) field() *Field {
if a.Field == nil {
a.Field = new(Field)
}
a.Field.Value = strings.Join([]string{
a.Field.Value = NewFieldValue(strings.Join([]string{
a.PostOfficeBox,
a.ExtendedAddress,
a.StreetAddress,
a.Locality,
a.Region,
a.PostalCode,
a.Country,
}, ";")
}, ";"))
return a.Field
}
132 changes: 129 additions & 3 deletions card_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,60 @@ var testCardGoogle = Card{
},
}

var testCardGoogleMultiEmail = Card{
"VERSION": []*Field{{Value: "3.0"}},
"N": []*Field{{Value: "Bloggs;Joe;;;"}},
"FN": []*Field{{Value: "Joe Bloggs"}},
"EMAIL": []*Field{{
Value: `[email protected], [email protected]`,
Params: Params{"TYPE": {"INTERNET", "HOME"}},
}},
"TEL": []*Field{{
Value: "+44 20 1234 5678",
Params: Params{"TYPE": {"CELL"}},
}},
"ADR": []*Field{{
Value: ";;1 Trafalgar Square;London;;WC2N;United Kingdom",
Params: Params{"TYPE": {"HOME"}},
}},
"URL": []*Field{
{Value: "http\\://joebloggs.com", Group: "item1"},
{Value: "http\\://twitter.com/test", Group: "item2"},
},
"X-SKYPE": []*Field{{Value: "joe.bloggs"}},
"X-ABLABEL": []*Field{
{Value: "_$!<HomePage>!$_", Group: "item1"},
{Value: "Twitter", Group: "item2"},
},
}

var testCardGoogleMultiEmailComma = Card{
"VERSION": []*Field{{Value: "3.0"}},
"N": []*Field{{Value: "Bloggs;Joe;;;"}},
"FN": []*Field{{Value: "Joe Bloggs"}},
"EMAIL": []*Field{{
Value: `[email protected], joe@joebloggs\,com`,
Params: Params{"TYPE": {"INTERNET", "HOME"}},
}},
"TEL": []*Field{{
Value: "+44 20 1234 5678",
Params: Params{"TYPE": {"CELL"}},
}},
"ADR": []*Field{{
Value: ";;1 Trafalgar Square;London;;WC2N;United Kingdom",
Params: Params{"TYPE": {"HOME"}},
}},
"URL": []*Field{
{Value: "http\\://joebloggs.com", Group: "item1"},
{Value: "http\\://twitter.com/test", Group: "item2"},
},
"X-SKYPE": []*Field{{Value: "joe.bloggs"}},
"X-ABLABEL": []*Field{
{Value: "_$!<HomePage>!$_", Group: "item1"},
{Value: "Twitter", Group: "item2"},
},
}

var testCardApple = Card{
"VERSION": []*Field{{Value: "3.0"}},
"N": []*Field{{Value: "Bloggs;Joe;;;"}},
Expand Down Expand Up @@ -145,7 +199,7 @@ func TestCard(t *testing.T) {
{Value: "[email protected]", Params: Params{"TYPE": {"work"}}},
},
}
expected := []string{"[email protected]", "[email protected]"}
expected := []FieldValue{"[email protected]", "[email protected]"}
if values := cardMultipleValues.Values(FieldEmail); !reflect.DeepEqual(expected, values) {
t.Errorf("Expected card emails to be %+v but got %+v", expected, values)
}
Expand All @@ -157,13 +211,13 @@ func TestCard(t *testing.T) {
func TestCard_AddValue(t *testing.T) {
card := make(Card)

name1 := "Akiyama Mio"
name1 := FieldValue("Akiyama Mio")
card.AddValue("FN", name1)
if values := card.Values("FN"); len(values) != 1 || values[0] != name1 {
t.Errorf("Expected one FN value, got %v", values)
}

name2 := "Mio Akiyama"
name2 := FieldValue("Mio Akiyama")
card.AddValue("FN", name2)
if values := card.Values("FN"); len(values) != 2 || values[0] != name1 || values[1] != name2 {
t.Errorf("Expected two FN values, got %v", values)
Expand Down Expand Up @@ -335,3 +389,75 @@ func TestCard_Revision(t *testing.T) {
t.Errorf("Expected revision to be %v but got %v", expected, rev)
}
}

func TestCard_FieldValue(t *testing.T) {
fieldValues := map[FieldValue][]string{
"": {""},
",": {"", ""},
"\\,": {","},
",,\\,,": {"", "", ",", ""},
"geo:1.23\\,4.56": {"geo:1.23,4.56"},
"geo:1.23,4.56": {"geo:1.23", "4.56"},
"geo:1\\,23,4\\,56": {"geo:1,23", "4,56"},
}
for fv, parts := range fieldValues {
t.Run(fv.String(), func(t *testing.T) {
gotParts := fv.Values()
if !reflect.DeepEqual(parts, gotParts) {
t.Errorf("Expected parts to be %+v but got %+v", parts, gotParts)
}

gotFV := NewFieldValue(parts...)
if gotFV != fv {
t.Errorf("Expected FieldValue to be %+v but got %+v", fv, gotFV)
}
})
}
}

// go test -fuzztime=10s -fuzz=Card_FieldValueRaw
func FuzzCard_FieldValueRaw(f *testing.F) {
f.Add(``)
f.Add(`123`)
f.Add(`1,2,3`)
f.Add(`1\abc`) // missing escaping of "\"
f.Add(`1\,2,3`)
f.Add(`1\\,2,3`)
f.Fuzz(func(t *testing.T, raw string) {
fv := FieldValue(raw)
parts := fv.Values()
got1 := NewFieldValue(parts...)
if got1 != fv {
// the raw value was wrongly escaped:
// "got" should now be correctly escaped
if len(got1) <= len(fv) {
t.Errorf("Expected a larger (escaped) string than %q, got %q", fv, got1)
}
// encode again and check that we get back the same (correctly escaped) raw value
got2 := NewFieldValue(got1.Values()...)
if got1 != got2 {
t.Errorf("Expected %q, got %q", got1, got2)
}
}
})
}

func FuzzCard_FieldValueParts(f *testing.F) {
f.Add("p0", "p1")
f.Add("1,2", "3")
f.Add("1\\,2", "3")
f.Add("1\\\\,2", "3")
f.Fuzz(func(t *testing.T, part0, part1 string) {
fv := NewFieldValue(part0, part1)
got := fv.Values()
if len(got) != 2 {
t.Fatalf("Expected 2 values, got %d: %v", len(got), got)
}
if got[0] != part0 {
t.Fatalf("Expected first value %q, got %q", part0, got[0])
}
if got[1] != part1 {
t.Fatalf("Expected first value %q, got %q", part1, got[1])
}
})
}
Loading

0 comments on commit ae7038e

Please sign in to comment.