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.

- 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

Resolves ical-org#531
  • Loading branch information
axunonb committed Mar 6, 2025
1 parent a0db138 commit 4697ac5
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 21 deletions.
34 changes: 24 additions & 10 deletions Ical.Net.Tests/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,20 +525,34 @@ 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()
[TestCase("FOO", "BAR")]
[TestCase("", "")]
public void LibraryMetadataTests(string prodId, string version)
{
var c = new Calendar
var c = new Calendar();
if (!string.IsNullOrEmpty(prodId))
{
ProductId = "FOO",
Version = "BAR"
};
c.ProductId = prodId; // the default can be overwritten by user
}
if (!string.IsNullOrEmpty(prodId))
{
c.Version = "BAR"; // this setter is internal
}

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

var expectedVersion = $"VERSION:{LibraryMetadata.Version}";
Assert.That(serialized.Contains(expectedVersion, StringComparison.Ordinal), Is.True);
Assert.Multiple(() =>
{
Assert.That(serialized,
!string.IsNullOrEmpty(prodId)
? Does.Contain("PRODID:" + c.ProductId)
: Does.Contain("PRODID:" + LibraryMetadata.ProdId));

Assert.That(serialized,
!string.IsNullOrEmpty(prodId)
? Does.Contain("VERSION:" + c.Version)
: Does.Contain("VERSION:" + LibraryMetadata.Version));
});
}

[Test]
Expand Down
17 changes: 16 additions & 1 deletion Ical.Net/Calendar.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ public static IList<T> Load<T>(string ical)
/// </summary>
public Calendar()
{
Name = Components.Calendar;
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 @@ -147,12 +147,27 @@ public override int GetHashCode()
/// </summary>
public virtual IUniqueComponentList<Todo> Todos => _mTodos;

/// <summary>
/// Gets or sets the version of the iCalendar definition, which is <c>2.0</c> as per RFC 5545 Section 3.7.4
/// <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>
/// </summary>
public virtual string Version
{
get => Properties.Get<string>("VERSION");
set => Properties.Set("VERSION", value);
}

/// <summary>
/// Gets or sets the product ID of the iCalendar, which typically and by default contains the name of the software
/// that created the iCalendar, i.e. "ical.net", and the version.
/// <para/>
/// <b>Be careful when setting this value</b>, as it is free-form text that must conform to the iCalendar specification
/// (RFC 5545 Section 3.7.3).
/// </summary>
public virtual string ProductId
{
get => Properties.Get<string>("PRODID");
Expand Down
33 changes: 29 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,31 @@ 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).
/// </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 <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 readonly 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
19 changes: 13 additions & 6 deletions Ical.Net/Serialization/CalendarSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,19 @@ public CalendarSerializer(SerializationContext ctx) : base(ctx) { }

public override string SerializeToString(object obj)
{
if (obj is Calendar)
if (obj is Calendar 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;
// Ensure the calendar has a version and product ID

if (string.IsNullOrEmpty(calendar.Version))
{
calendar.Version = LibraryMetadata.Version;
}

if (string.IsNullOrEmpty(calendar.ProductId))
{
calendar.ProductId = LibraryMetadata.ProdId;
}

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

0 comments on commit 4697ac5

Please sign in to comment.