Skip to content
Merged
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
28 changes: 28 additions & 0 deletions Ical.Net.Tests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -654,4 +654,32 @@ public void TextValueEscapesSpecialCharacters(string originalText, string serial
var resultCal = Calendar.Load(result)!;
Assert.That(resultCal.Events[0]!.Description, Is.EqualTo(deserializedText ?? originalText));
}

private static readonly object[] MultiValueSeparatorCases =
{
// categories, expected serialized CATEGORIES line
new object[] { new[] { "a", "b" }, @"CATEGORIES:a,b" }, // plain separator
new object[] { new[] { "a,b", "c" }, @"CATEGORIES:a\,b,c" }, // escaped comma stays inside a value
new object[] { new[] { @"a\", "b" }, @"CATEGORIES:a\\,b" }, // escaped backslash before a separator
new object[] { new[] { @"a\,b" }, @"CATEGORIES:a\\\,b" }, // escaped backslash + escaped comma = one value
new object[] { new[] { @"a\\", "b" }, @"CATEGORIES:a\\\\,b" }, // two escaped backslashes before a separator
new object[] { new[] { "a", @"b\" }, @"CATEGORIES:a,b\\" }, // trailing backslash on the last value
new object[] { new[] { @"x\", @"y\", "z" }, @"CATEGORIES:x\\,y\\,z" }, // several escaped backslashes before separators
};

[TestCaseSource(nameof(MultiValueSeparatorCases))]
public void MultiValueTextRoundTripsAcrossEscapedSeparators(string[] categories, string serializedLine)
{
var calendar = new Calendar();
var calEvent = new CalendarEvent { Summary = "x" };
foreach (var category in categories)
calEvent.Categories.Add(category);
calendar.Events.Add(calEvent);

var result = new CalendarSerializer().SerializeToString(calendar);
Assert.That(result, Does.Contain(serializedLine));

var roundTripped = Calendar.Load(result)!.Events[0]!.Categories.ToList();
Assert.That(roundTripped, Is.EqualTo(categories));
}
}
55 changes: 46 additions & 9 deletions Ical.Net/Serialization/DataTypes/StringSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Ical.Net.DataTypes;

namespace Ical.Net.Serialization.DataTypes;
Expand Down Expand Up @@ -342,7 +341,45 @@ private static string EscapeWithSearchValues(string valueToEscape)
return string.Join(",", values);
}

internal static readonly Regex UnescapedCommas = new Regex(@"(?<!\\),", RegexOptions.Compiled, RegexDefaults.Timeout);
// A value scan stops at an unescaped comma (a separator) or a backslash
// (which escapes the following character).
private static readonly char[] _valueSeparators = ['\\', ','];

/// <summary>
/// Splits a serialized multi-value TEXT property on its separator commas. A comma
/// separates values only when it is not escaped; a backslash escapes the following
/// character, so a comma preceded by an odd number of backslashes belongs to a value
/// (RFC 5545 3.1.1). Mirrors the left-to-right consumption used by <see cref="Unescape"/>.
/// </summary>
internal static IEnumerable<string> SplitOnUnescapedCommas(string value)
Comment thread
maknapp marked this conversation as resolved.
{
var start = 0;
var pos = 0;

while (pos < value.Length)
Comment thread
maknapp marked this conversation as resolved.
{
var next = value.IndexOfAny(_valueSeparators, pos);
if (next < 0)
{
break;
}

if (value[next] == '\\')
{
// Backslash escapes the next character; skip the pair.
pos = next + 2;
}
else
{
yield return value.Substring(start, next - start);
start = next + 1;
pos = next + 1;
}
}

yield return value.Substring(start);
}

public override object? Deserialize(TextReader? tr)
{
if (tr == null)
Expand All @@ -367,14 +404,14 @@ private static string EscapeWithSearchValues(string valueToEscape)
AssociatedObject = context.Peek() as ICalendarObject
};

var encodedValues = serializeAsList ? UnescapedCommas.Split(value) : new[] { value };
var escapedValues = encodedValues.Select(v => Decode(dt, v)).ToList();
var values = escapedValues.Select(Unescape).ToList();

if (values.Count == 1)
if (!serializeAsList)
{
return values[0];
return Unescape(Decode(dt, value));
}
return values;

var values = SplitOnUnescapedCommas(value)
.Select(v => Unescape(Decode(dt, v)))
.ToList();
return values.Count == 1 ? values[0] : values;
}
}
Loading