@@ -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