Skip to content

Commit

Permalink
Optimize vCardStandardReader/Writer and use LINQ.
Browse files Browse the repository at this point in the history
Filter out invalid xml characters in EncodeEscaped.
  • Loading branch information
aluxnimm committed Mar 16, 2023
1 parent e6a5782 commit 457c6f0
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,41 +47,47 @@ protected override string GetUid(DistributionList entity)

protected override string Serialize(DistributionList vcard)
{
char[] escapechars = {',', '\\', ';', '\r', '\n'};

Dictionary<string, string> escapeTokens = new Dictionary<string, string>
{ {@"\", @"\\"},
{"\n", @"\n"},
{"\r", @"\r"},
{",", @"\,"},
{";", @"\;"}
};

var builder = new StringBuilder();
builder.AppendLine("BEGIN:VLIST");
builder.AppendLine("VERSION:1.0");
builder.Append("UID:");
builder.AppendLine(vcard.Uid);
builder.Append("FN:");
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Name, escapechars));
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Name, escapeTokens));
if (!string.IsNullOrEmpty(vcard.Description))
{
builder.Append("DESCRIPTION:");
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Description.Replace("\r\n", "\n"), escapechars));
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Description.Replace("\r\n", "\n"), escapeTokens));
}

if (!string.IsNullOrEmpty(vcard.Nickname))
{
builder.Append("NICKNAME:");
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Nickname, escapechars));
builder.AppendLine(vCardStandardWriter.EncodeEscaped(vcard.Nickname, escapeTokens));
}

foreach (var member in vcard.Members)
{
builder.Append("CARD;EMAIL=");
builder.Append(member.EmailAddress);
builder.Append(";FN=");
builder.Append(vCardStandardWriter.EncodeEscaped(member.DisplayName, escapechars));
builder.Append(vCardStandardWriter.EncodeEscaped(member.DisplayName, escapeTokens));
builder.Append(":");
builder.AppendLine(member.ServerFileName);
}

foreach (var member in vcard.NonAddressBookMembers)
{
builder.Append(NonAddressBookMemberValueName + ";CN=");
builder.Append(vCardStandardWriter.EncodeEscaped(member.DisplayName, escapechars));
builder.Append(vCardStandardWriter.EncodeEscaped(member.DisplayName, escapeTokens));
builder.Append(":mailto:");
builder.AppendLine(member.EmailAddress);
}
Expand Down
87 changes: 11 additions & 76 deletions Thought.vCards/vCardStandardReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

Expand Down Expand Up @@ -254,83 +255,17 @@ public static string DecodeEscaped(string value)
if (string.IsNullOrEmpty(value))
return value;

StringBuilder builder = new StringBuilder(value.Length);

int startIndex = 0;

do
Dictionary<string, string> standardEspaceTokens = new Dictionary<string, string>
{
// Get the index of the next backslash character.
// This marks the beginning of an escape sequence.

int nextIndex = value.IndexOf('\\', startIndex);

if ((nextIndex == -1) || (nextIndex == value.Length - 1))
{
// There are no more escape codes, or the backslash
// is located at the very end of the string. The
// characters between the index and the end of the
// string need to be copied to the output buffer.

builder.Append(
value,
startIndex,
value.Length - startIndex);

break;
}
else
{
// A backslash was located somewhere in the string.
// The previous statement ensured the backslash is
// not the very last character, and therefore the
// following statement is safe.

char code = value[nextIndex + 1];

// Any characters between the starting point and
// the index must be pushed into the buffer.

builder.Append(
value,
startIndex,
nextIndex - startIndex);

switch (code)
{
case '\\':
case ',':
case ';':
case ':':

builder.Append(code);
nextIndex += 2;
break;

case 'n':
case 'N':
builder.Append('\n');
nextIndex += 2;
break;

case 'r':
case 'R':
builder.Append('\r');
nextIndex += 2;
break;

default:
builder.Append('\\');
builder.Append(code);
nextIndex += 2;
break;
}
}

startIndex = nextIndex;
} while (startIndex < value.Length);

return builder.ToString();
{@"\\", @"\"},
{@"\N", "\n"},
{@"\R", "\r"},
{@"\n", "\n"},
{@"\r", "\r"},
{@"\,",","},
{@"\;", ";"}
};
return standardEspaceTokens.Aggregate(value, (current, token) => current.Replace(token.Key, token.Value));
}

#endregion
Expand Down
135 changes: 53 additions & 82 deletions Thought.vCards/vCardStandardWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
* ======================================================================= */

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

using System.Xml;

namespace Thought.vCards
{
/// <summary>
Expand All @@ -22,25 +25,33 @@ public class vCardStandardWriter : vCardWriter
private string productId;
private const string TYPE = "TYPE";


/// <summary>
/// The characters that are escaped per the original
/// vCard specification.
/// </summary>
private readonly char[] standardEscapedCharacters =
new char[] {',', '\\', ';', '\r', '\n'};


/// </summary>
private readonly Dictionary<string, string> standardEspaceTokens = new Dictionary<string, string>
{
{@"\", @"\\"},
{"\n", @"\n"},
{"\r", @"\r"},
{",", @"\,"},
{";", @"\;"}
};

/// <summary>
/// The characters that are escaped by Microsoft Outlook.
/// </summary>
/// <remarks>
/// Microsoft Outlook does not property decode escaped
/// commas in values.
/// </remarks>
private readonly char[] outlookEscapedCharacters =
new char[] {'\\', ';', '\r', '\n'};

/// </remarks>
private readonly Dictionary<string, string> outlookEspaceTokens = new Dictionary<string, string>
{
{@"\", @"\\"},
{"\n", @"\n"},
{"\r", @"\r"},
{";", @"\;"}
};

/// <summary>
/// Creates a new instance of the standard writer.
Expand Down Expand Up @@ -1507,95 +1518,55 @@ public string EncodeEscaped(string value)
(this.options & vCardStandardWriterOptions.IgnoreCommas) ==
vCardStandardWriterOptions.IgnoreCommas)
{
return EncodeEscaped(value, outlookEscapedCharacters);
return EncodeEscaped(value, outlookEspaceTokens);
}
else
{
return EncodeEscaped(value, standardEscapedCharacters);
return EncodeEscaped(value, standardEspaceTokens);
}
}

#endregion

#region [ EncodeEscaped(string, char[]) ]
#region [ EncodeEscaped(string, Dictionary<string,string>) ]

/// <summary>
/// Encodes a character array using simple escape sequences.
/// </summary>
public static string EncodeEscaped(
string value,
char[] escaped)
public static string EncodeEscaped(string value, Dictionary<string,string> escaped)
{
if (escaped == null)
throw new ArgumentNullException("escaped");

if (string.IsNullOrEmpty(value))
return value;

StringBuilder buffer = new StringBuilder();

int startIndex = 0;

do
{
// Get the index of the next character
// to be escaped (e.g. the next semicolon).

int nextIndex = value.IndexOfAny(escaped, startIndex);

if (nextIndex == -1)
{
// No more characters need to be escaped.
// Any characters between the start index
// and the end of the string can be copied
// to the buffer.

buffer.Append(
value,
startIndex,
value.Length - startIndex);

break;
}
else
{
char replacement;
switch (value[nextIndex])
{
case '\n':
replacement = 'n';
break;

case '\r':
replacement = 'r';
break;

default:
replacement = value[nextIndex];
break;
}

buffer.Append(
value,
startIndex,
nextIndex - startIndex);

buffer.Append('\\');
buffer.Append(replacement);

startIndex = nextIndex + 1;
}
} while (startIndex < value.Length);

return buffer.ToString();

// The following must be encoded:
//
// Backslash (\\)
// Colon (\:)
// Semicolon (\;)
return value;

string escapedValue = escaped.Aggregate(value, (current, token) => current.Replace(token.Key, token.Value));

// filter out invalid xml characters
const char _replacementCharacter = '\uFFFD';
int length = escapedValue.Length;
StringBuilder stringBuilder = new StringBuilder(length);

for (int i = 0; i < length; ++i)
{
if (XmlConvert.IsXmlChar(escapedValue[i]))
{
stringBuilder.Append(escapedValue[i]);
}
else if (i + 1 < length && XmlConvert.IsXmlSurrogatePair(escapedValue[i + 1], escapedValue[i]))
{
stringBuilder.Append(escapedValue[i]);
stringBuilder.Append(escapedValue[i + 1]);
++i;
}
else
{
stringBuilder.Append(_replacementCharacter);
}
}
return stringBuilder.ToString();
}

#endregion

#region [ EncodeQuotedPrintable ]
Expand Down

0 comments on commit 457c6f0

Please sign in to comment.