Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 24 additions & 0 deletions Ical.Net.Tests/DeserializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -588,4 +588,28 @@ public void KeepApartDtEndAndDuration_Tests(bool useDtEnd)
Assert.That(calendar.Events.Single().Duration != null, Is.EqualTo(!useDtEnd));
});
}

[Test]
public void CalendarWithMissingProdIdOrVersion_ShouldLeavePropertiesInvalid()
{
var ics = """
BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20070406T230000Z
DTEND:20070407T010000Z
END:VEVENT
END:VCALENDAR
""";

var calendar = Calendar.Load(ics);
var deserialized = new CalendarSerializer(calendar).SerializeToString();

Assert.Multiple(() =>
{
Assert.That(calendar.ProductId, Is.EqualTo(null));
Assert.That(calendar.Version, Is.EqualTo(null));
// The serialized calendar should not contain the PRODID or VERSION properties, which are required
Assert.That(deserialized, Does.Not.Contain("PRODID:").And.Not.Contains("VERSION:"));
});
}
}
38 changes: 31 additions & 7 deletions Ical.Net.Tests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,20 +525,44 @@ public void TestRRuleUntilSerialization()
Assert.That(!until.EndsWith("Z"), Is.True);
}

[Test(Description = "PRODID and VERSION should use ical.net values instead of preserving deserialized values")]
public void LibraryMetadataTests()
[Test]
public void ProductId_and_Version_CanBeChanged()
{
var c = new Calendar
{
ProductId = "FOO",
Version = "BAR"
Version = "BAR",
};

var serialized = new CalendarSerializer().SerializeToString(c);
var expectedProdid = $"PRODID:{LibraryMetadata.ProdId}";
Assert.That(serialized.Contains(expectedProdid, StringComparison.Ordinal), Is.True);

Assert.Multiple(() =>
{
Assert.That(serialized, Does.Contain($"PRODID:{c.ProductId}"));
Assert.That(serialized, Does.Contain($"VERSION:{c.Version}"));
});
}

[Test]
public void ProductId_and_Version_CannotBeSetAsEmpty()
{
var c = new Calendar();
Assert.Multiple(() =>
{
Assert.That(() => c.ProductId = string.Empty, Throws.ArgumentException);
Assert.That(() => c.Version = string.Empty, Throws.ArgumentException);
});
}

var expectedVersion = $"VERSION:{LibraryMetadata.Version}";
Assert.That(serialized.Contains(expectedVersion, StringComparison.Ordinal), Is.True);
[Test]
public void ProductId_and_Version_HaveDefaultValues()
{
var c = new Calendar();
Assert.Multiple(() =>
{
Assert.That(c.ProductId, Is.EqualTo(LibraryMetadata.ProdId));
Assert.That(c.Version, Is.EqualTo(LibraryMetadata.Version));
});
}

[Test]
Expand Down
46 changes: 42 additions & 4 deletions Ical.Net/Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,16 @@ public static IList<T> Load<T>(string ical)
/// </summary>
public Calendar()
{
Name = Components.Calendar;
// Note: ProductId and Version Property values will be empty before _deserialization_
ProductId = LibraryMetadata.ProdId;
Version = LibraryMetadata.Version;

Initialize();
}

private void Initialize()
{
Name = Components.Calendar;
_mUniqueComponents = new UniqueComponentListProxy<IUniqueComponent>(Children);
_mEvents = new UniqueComponentListProxy<CalendarEvent>(Children);
_mTodos = new UniqueComponentListProxy<Todo>(Children);
Expand Down Expand Up @@ -143,20 +147,54 @@ public override int GetHashCode()
public virtual ICalendarObjectList<VTimeZone> TimeZones => _mTimeZones;

/// <summary>
/// A collection of <see cref="Todo"/> components in the iCalendar.
/// A collection of <see cref="CalendarComponents.Todo"/> components in the iCalendar.
/// </summary>
public virtual IUniqueComponentList<Todo> Todos => _mTodos;

/// <summary>
/// Gets or sets the version of the iCalendar definition. The default is <see cref="LibraryMetadata.Version"/>
/// as per RFC 5545 Section 3.7.4 and must be specified.
/// <para/>
/// It specifies the identifier corresponding to the highest version number of the iCalendar specification
/// that is required in order to interpret the iCalendar object.
/// <para/>
/// <b>Do not change unless you are sure about the consequences.</b>
/// <para/>
/// The default value does not apply to deserialized objects.
/// </summary>
public virtual string Version
{
get => Properties.Get<string>("VERSION");
set => Properties.Set("VERSION", value);
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Version to set must not be null or empty");
}
Properties.Set("VERSION", value);
}
}

/// <summary>
/// Gets or sets the product ID of the iCalendar, which typically contains the name of the software
/// that created the iCalendar. The default is <see cref="LibraryMetadata.ProdId"/>.
/// <para/>
/// <b>Be careful when setting a custom value</b>, as it is free-form text that must conform to the iCalendar specification
/// (RFC 5545 Section 3.7.3). The product ID must be specified.
/// <para/>
/// The default value does not apply to deserialized objects.
/// </summary>
public virtual string ProductId
{
get => Properties.Get<string>("PRODID");
set => Properties.Set("PRODID", value);
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Product ID to set must not be null or empty");
}
Properties.Set("PRODID", value);
}
}

public virtual string Scale
Expand Down
34 changes: 30 additions & 4 deletions Ical.Net/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
// Licensed under the MIT license.
//

#nullable enable
using System;
using System.Diagnostics;

namespace Ical.Net;

Expand Down Expand Up @@ -124,7 +126,7 @@ public class SerializationConstants
}

/// <summary>
/// Status codes available to an <see cref="Components.Event"/> item
/// Status codes available to an <see cref="CalendarComponents.CalendarEvent"/> item
/// </summary>
public static class EventStatus
{
Expand All @@ -137,7 +139,7 @@ public static class EventStatus
}

/// <summary>
/// Status codes available to a <see cref="Todo"/> item.
/// Status codes available to a <see cref="CalendarComponents.Todo"/> item.
/// </summary>
public static class TodoStatus
{
Expand All @@ -152,7 +154,7 @@ public static class TodoStatus
}

/// <summary>
/// Status codes available to a <see cref="Journal"/> entry.
/// Status codes available to a <see cref="CalendarComponents.Journal"/> entry.
/// </summary>
public static class JournalStatus
{
Expand Down Expand Up @@ -235,8 +237,32 @@ public static class TransparencyType

public static class LibraryMetadata
{
private static readonly string _assemblyVersion = GetAssemblyVersion();

/// <summary>
/// The <c>VERSION</c> property for iCalendar objects generated by this library (RFC 5545 Section 3.7.4),
/// unless overridden by user code.
/// </summary>
public const string Version = "2.0";
public static readonly string ProdId = "-//github.com/ical-org/ical.net//NONSGML ical.net 4.0//EN";

/// <summary>
/// The default <c>PRODID</c> property for iCalendar objects generated by this library (RFC 5545 Section 3.7.3),
/// unless overridden by user code.
/// <remarks>
/// The text between the double slashes represents the organization or software that created the iCalendar object.
/// </remarks>
/// </summary>
public static string ProdId => $"-//github.com/ical-org/ical.net//NONSGML ical.net {_assemblyVersion}//EN";

private static string GetAssemblyVersion()
{
var assembly = typeof(LibraryMetadata).Assembly;
var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
// Prefer the file version, but fall back to the assembly version if it's not available.
return fileVersionInfo.FileVersion
?? assembly.GetName().Version?.ToString() // will only change for major versions
?? "1.0.0.0";
}
}

public static class CalendarScales
Expand Down
16 changes: 1 addition & 15 deletions Ical.Net/Serialization/CalendarSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,6 @@ public CalendarSerializer(SerializationContext ctx) : base(ctx) { }

protected override IComparer<ICalendarProperty> PropertySorter => new CalendarPropertySorter();

public override string SerializeToString(object obj)
{
if (obj is Calendar)
{
// If we're serializing a calendar, we should indicate that we're using ical.net to do the work
var calendar = (Calendar) obj;
calendar.Version = LibraryMetadata.Version;
calendar.ProductId = LibraryMetadata.ProdId;

return base.SerializeToString(calendar);
}

return base.SerializeToString(obj);
}

public override object Deserialize(TextReader tr) => null;

Expand Down Expand Up @@ -70,4 +56,4 @@ public int Compare(ICalendarProperty x, ICalendarProperty y)
: string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
}
}
}
}