From fe8afc8b062e39bc972beb8ab812d4f28806dd59 Mon Sep 17 00:00:00 2001 From: oliverpool Date: Thu, 24 Oct 2024 22:36:42 +0200 Subject: [PATCH] encode: wrap long lines --- encoder.go | 28 ++++++++++++++++ encoder_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 106 insertions(+), 9 deletions(-) diff --git a/encoder.go b/encoder.go index 974cefc..b20c5e1 100644 --- a/encoder.go +++ b/encoder.go @@ -5,6 +5,7 @@ import ( "io" "sort" "strings" + "unicode/utf8" ) // An Encoder formats cards. @@ -69,6 +70,33 @@ func formatLine(key string, field *Field) string { } s += ":" + formatValue(field.Value) + + // Content lines SHOULD be folded to a maximum width of 75 octets, excluding the line break. + const maxLen = 74 // -1 for the leading space on the new line + if newlines := (len(s) - 2) / maxLen; newlines > 0 { + var sb strings.Builder + sb.Grow(len(s) + newlines*len("\r\n ")) + end := 1 + maxLen + for !utf8.RuneStart(s[end]) { // Multi-octet characters MUST remain contiguous. + end-- + } + sb.WriteString(s[:end]) + start := end + for start < len(s) { + sb.WriteString("\r\n ") + end := start + maxLen + if end > len(s) { + end = len(s) + } else { + for !utf8.RuneStart(s[end]) { // Multi-octet characters MUST remain contiguous. + end-- + } + } + sb.WriteString(s[start:end]) + start = end + } + return sb.String() + } return s } diff --git a/encoder_test.go b/encoder_test.go index 6cfff47..64ae026 100644 --- a/encoder_test.go +++ b/encoder_test.go @@ -3,6 +3,7 @@ package vcard import ( "bytes" "reflect" + "strings" "testing" ) @@ -14,7 +15,7 @@ func TestEncoder(t *testing.T) { expected := "BEGIN:VCARD\r\nVERSION:4.0\r\nCLIENTPIDMAP:1;urn:uuid:53e374d9-337e-4727-8803-a1e9c14e0556\r\nEMAIL;PID=1.1:jdoe@example.com\r\nFN;PID=1.1:J. Doe\r\nN:Doe;J.;;;\r\nUID:urn:uuid:4fbe8971-0bc3-424c-9c26-36c3e1eff6b1\r\nEND:VCARD\r\n" if b.String() != expected { - t.Errorf("Excpected vcard to be %q, but got %q", expected, b.String()) + t.Errorf("Expected vcard to be %q, but got %q", expected, b.String()) } card, err := NewDecoder(&b).Decode() @@ -27,16 +28,84 @@ func TestEncoder(t *testing.T) { } } -func TestFormatLine_withGroup(t *testing.T) { - l := formatLine("FN", &Field{ - Value: "Akiyama Mio", - Group: "item1", +func TestFormatLine(t *testing.T) { + t.Run("withGroup", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: "Akiyama Mio", + Group: "item1", + }) + + expected := "item1.FN:Akiyama Mio" + if l != expected { + t.Errorf("Expected formatted line with group to be %q, but got %q", expected, l) + } }) + t.Run("wrapping", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 16), + Group: "wrapme", + }) - expected := "item1.FN:Akiyama Mio" - if l != expected { - t.Errorf("Excpected formatted line with group to be %q, but got %q", expected, l) - } + expected := "wrapme.FN:12345678901234567890123456789012345678901234567890123456789012345\r\n 67890123456789012345678901234567890123456789012345678901234567890123456789\r\n 012345678901234567890" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) + t.Run("wrapping_at_limit", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 6) + "12345", + Group: "wrapme", + }) + + expected := "wrapme.FN:12345678901234567890123456789012345678901234567890123456789012345" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) + t.Run("wrapping_after_limit", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 6) + "123451", + Group: "wrapme", + }) + + expected := "wrapme.FN:12345678901234567890123456789012345678901234567890123456789012345\r\n 1" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) + t.Run("wrapping_limit_unicode_start", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 6) + "1234€", + Group: "wrapme", + }) + + expected := "wrapme.FN:1234567890123456789012345678901234567890123456789012345678901234\r\n €" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) + t.Run("wrapping_limit_unicode_later", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 13) + "12345678€", + Group: "wrapme", + }) + + expected := "wrapme.FN:12345678901234567890123456789012345678901234567890123456789012345\r\n 6789012345678901234567890123456789012345678901234567890123456789012345678\r\n €" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) + t.Run("wrapping_very_long", func(t *testing.T) { + l := formatLine("FN", &Field{ + Value: strings.Repeat("1234567890", 16), + Group: "wrapme", + }) + + expected := "wrapme.FN:12345678901234567890123456789012345678901234567890123456789012345\r\n 67890123456789012345678901234567890123456789012345678901234567890123456789\r\n 012345678901234567890" + if l != expected { + t.Errorf("Expected wrapped line to be %q, but got %q", expected, l) + } + }) } var testValue = []struct {