diff --git a/Ical.Net.Tests/SerializationTests.cs b/Ical.Net.Tests/SerializationTests.cs index 5919b5a5..1c101101 100644 --- a/Ical.Net.Tests/SerializationTests.cs +++ b/Ical.Net.Tests/SerializationTests.cs @@ -7,6 +7,7 @@ using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; @@ -611,4 +612,35 @@ public void TestConvertToInt32WithNegativeNumberInDifferentCultures(string cultu } } + [Test] + public void CalendarSerialization_ShouldDefaultTo_Utf8NoBom() + { + var calendar = new Calendar(); + calendar.Events.Add(new CalendarEvent + { + Summary = "Sample Event", + DtStart = new CalDateTime(2025, 6, 10, 9, 0, 0), + DtEnd = new CalDateTime(2025, 6, 10, 10, 0, 0) + }); + + // Serialize with default encoding (should be UTF-8 without BOM) + using var msNoBom = new MemoryStream(); + new CalendarSerializer().Serialize(calendar, msNoBom); + var noBomBytes = msNoBom.ToArray(); + + // Serialize with explicit UTF-8 BOM + using var msWithBom = new MemoryStream(); + new CalendarSerializer().Serialize(calendar, msWithBom, new UTF8Encoding(true)); + var withBomBytes = msWithBom.ToArray(); + + // UTF-8 BOM is 0xEF,0xBB,0xBF + bool HasBom(byte[] bytes) => + bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF; + + Assert.Multiple(() => + { + Assert.That(HasBom(noBomBytes), Is.False, "Stream should not contain a UTF-8 BOM"); + Assert.That(HasBom(withBomBytes), Is.True, "Stream should contain a UTF-8 BOM"); + }); + } } diff --git a/Ical.Net/Serialization/ISerializer.cs b/Ical.Net/Serialization/ISerializer.cs index d6c5f1f1..1a25cbfb 100644 --- a/Ical.Net/Serialization/ISerializer.cs +++ b/Ical.Net/Serialization/ISerializer.cs @@ -14,6 +14,6 @@ public interface ISerializer : IServiceProvider SerializationContext SerializationContext { get; set; } Type TargetType { get; } - void Serialize(object obj, Stream stream, Encoding encoding); + void Serialize(object obj, Stream stream, Encoding? encoding = null); object? Deserialize(Stream stream, Encoding encoding); } diff --git a/Ical.Net/Serialization/SerializerBase.cs b/Ical.Net/Serialization/SerializerBase.cs index 0960293a..4b2b24c6 100644 --- a/Ical.Net/Serialization/SerializerBase.cs +++ b/Ical.Net/Serialization/SerializerBase.cs @@ -43,13 +43,28 @@ protected SerializerBase(SerializationContext ctx) return obj; } - public void Serialize(object obj, Stream stream, Encoding encoding) + /// + /// Serializes the specified object to the provided stream using the specified encoding. + /// + /// This method writes the serialized representation of the object to the stream without closing + /// the stream, allowing the caller to continue using it. + /// bytes. + /// The object to serialize. Must not be null. + /// The stream to which the serialized data will be written. Must be writable. + /// + /// The character encoding to use for serialization. + /// If or missing, UTF-8 encoding + /// without a byte order mark (BOM) is used. + /// A BOM is incompatible with many iCalendar apps. + /// + public void Serialize(object obj, Stream stream, Encoding? encoding = null) { - // NOTE: we don't use a 'using' statement here because - // we don't want the stream to be closed by this serialization. - // Fixes bug #3177278 - Serialize closes stream + // Ensure that no BOM is written to the stream + encoding ??= new UTF8Encoding(false); - const int defaultBuffer = 1024; //This is StreamWriter's built-in default buffer size + //This is StreamWriter's built-in default buffer size + const int defaultBuffer = 1024; + // Important: leave the stream open so that the caller can continue to use it using var sw = new StreamWriter(stream, encoding, defaultBuffer, leaveOpen: true); // Push the current object onto the serialization stack