Skip to content

Commit 2f9506f

Browse files
author
Marcus Markiewicz
committed
feat: unescape ICS text in calendar entries
- Implemented UnescapeIcsText method to normalize escaped characters in ICS data. - Updated parsing logic to utilize UnescapeIcsText for SUMMARY and DESCRIPTION fields. - Ensured href extraction handles unescaped text correctly.
1 parent 26bab84 commit 2f9506f

File tree

1 file changed

+62
-10
lines changed

1 file changed

+62
-10
lines changed

src/ComingUpNextTray/Services/CalendarService.cs

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,7 @@ internal static IReadOnlyList<CalendarEntry> ParseIcs(string ics, DateTime? now
386386
switch (key)
387387
{
388388
case "SUMMARY":
389-
current.Title = value;
389+
current.Title = UnescapeIcsText(value);
390390

391391
// Mark simple summary markers
392392
if (!string.IsNullOrWhiteSpace(value))
@@ -479,29 +479,32 @@ internal static IReadOnlyList<CalendarEntry> ParseIcs(string ics, DateTime? now
479479
int hrefIdx = -1;
480480
string? href = null;
481481

482+
// Unescape DESCRIPTION text so any escaped commas/semicolons/newlines are normalized.
483+
string desc = UnescapeIcsText(value);
484+
482485
// Look for href="..."
483-
hrefIdx = value.IndexOf("href=\"", StringComparison.OrdinalIgnoreCase);
486+
hrefIdx = desc.IndexOf("href=\"", StringComparison.OrdinalIgnoreCase);
484487
if (hrefIdx >= 0)
485488
{
486489
int hrefStart = hrefIdx + 6; // length of href="
487-
int hrefEnd = value.IndexOf('"', hrefStart);
490+
int hrefEnd = desc.IndexOf('"', hrefStart);
488491
if (hrefEnd > hrefStart)
489492
{
490-
href = value[hrefStart..hrefEnd];
493+
href = desc[hrefStart..hrefEnd];
491494
}
492495
}
493496

494497
// Look for href='...'
495498
if (href is null)
496499
{
497-
hrefIdx = value.IndexOf("href='", StringComparison.OrdinalIgnoreCase);
500+
hrefIdx = desc.IndexOf("href='", StringComparison.OrdinalIgnoreCase);
498501
if (hrefIdx >= 0)
499502
{
500503
int hrefStart2 = hrefIdx + 6; // length of href='
501-
int hrefEnd2 = value.IndexOf('\'', hrefStart2);
504+
int hrefEnd2 = desc.IndexOf('\'', hrefStart2);
502505
if (hrefEnd2 > hrefStart2)
503506
{
504-
href = value[hrefStart2..hrefEnd2];
507+
href = desc[hrefStart2..hrefEnd2];
505508
}
506509
}
507510
}
@@ -513,10 +516,10 @@ internal static IReadOnlyList<CalendarEntry> ParseIcs(string ics, DateTime? now
513516
else
514517
{
515518
// Fallback: simple http substring extraction (existing behavior).
516-
int idx = value.IndexOf("http", StringComparison.OrdinalIgnoreCase);
519+
int idx = desc.IndexOf("http", StringComparison.OrdinalIgnoreCase);
517520
if (idx >= 0)
518521
{
519-
string? segment = value[idx..].Split('\n', ' ', '\r', '\t').FirstOrDefault();
522+
string? segment = desc[idx..].Split('\n', ' ', '\r', '\t').FirstOrDefault();
520523
if (!string.IsNullOrWhiteSpace(segment))
521524
{
522525
TryAssignUrl(current, segment.Trim());
@@ -583,7 +586,7 @@ internal static Models.IcsInspectionResult InspectIcsDiagnostics(string ics, Dat
583586
{
584587
if (line.StartsWith("SUMMARY:", System.StringComparison.OrdinalIgnoreCase))
585588
{
586-
summary = line.Substring(8).Trim();
589+
summary = UnescapeIcsText(line.Substring(8).Trim());
587590
}
588591

589592
if (line.StartsWith("DTSTART", System.StringComparison.OrdinalIgnoreCase))
@@ -853,6 +856,55 @@ private static void TryAssignUrl(CalendarEntry entry, string candidate)
853856
}
854857
}
855858

859+
// Unescape ICS TEXT per RFC 5545 section 3.3.11: backslash escapes for COMMA, SEMICOLON, BACKSLASH and NEWLINE
860+
private static string UnescapeIcsText(string raw)
861+
{
862+
if (string.IsNullOrEmpty(raw))
863+
{
864+
return raw;
865+
}
866+
867+
StringBuilder sb = new StringBuilder(raw.Length);
868+
for (int i = 0; i < raw.Length; i++)
869+
{
870+
char c = raw[i];
871+
if (c == '\\' && i + 1 < raw.Length)
872+
{
873+
char next = raw[i + 1];
874+
switch (next)
875+
{
876+
case 'n':
877+
case 'N':
878+
sb.Append('\n');
879+
i++;
880+
break;
881+
case ',':
882+
sb.Append(',');
883+
i++;
884+
break;
885+
case ';':
886+
sb.Append(';');
887+
i++;
888+
break;
889+
case '\\':
890+
sb.Append('\\');
891+
i++;
892+
break;
893+
default:
894+
// Unknown escape, keep both chars
895+
sb.Append(c);
896+
break;
897+
}
898+
}
899+
else
900+
{
901+
sb.Append(c);
902+
}
903+
}
904+
905+
return sb.ToString();
906+
}
907+
856908
private static string ComputeHash(byte[] data)
857909
{
858910
// Return a hex-encoded SHA256 of the content for quick comparisons.

0 commit comments

Comments
 (0)