Skip to content

Commit

Permalink
Update PRODID and VERSION property handling
Browse files Browse the repository at this point in the history
Issue: `Calendar` has setters for `ProductId` and `Version` which are overridden with fixed values when serializing.

- When creating a new `Calendar` instance, `ProductId` and `Version` contain default values
- When Deserializing an iCalendar, `ProductId` and `Version` will be taken from the input
- `ProductId` and `Version` can be overridden by user code. An attempt to set as an empty string will throw.
- Update the default `PRODID` property `LibraryMetadata.ProdId` to include the ical.net assembly version. Example: "PRODID:-//github.com/ical-org/ical.net//NONSGML ical.net 5.4.3//EN"
- Modified `CalendarSerializer.SerializeToString` so that the `ProdId` or `Version` set by users do not get overridden
- Add an xmldoc description about the purpose of `ProdId` and `Version`, and about the risks when modified
- Add unit tests

Resolves ical-org#531
  • Loading branch information
axunonb committed Mar 6, 2025
1 parent a0db138 commit d368658
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 30 deletions.
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);
}
}
}
}

0 comments on commit d368658

Please sign in to comment.