Skip to content

Commit 0581d67

Browse files
authored
Update PRODID and VERSION property handling (#748)
* Update `PRODID` and `VERSION` property handling 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 #531 * Remove null or empty check from PRODID and VERSION see #748 (comment)
1 parent 64919b9 commit 0581d67

File tree

5 files changed

+101
-28
lines changed

5 files changed

+101
-28
lines changed

Ical.Net.Tests/DeserializationTests.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,4 +588,28 @@ public void KeepApartDtEndAndDuration_Tests(bool useDtEnd)
588588
Assert.That(calendar.Events.Single().Duration != null, Is.EqualTo(!useDtEnd));
589589
});
590590
}
591+
592+
[Test]
593+
public void CalendarWithMissingProdIdOrVersion_ShouldLeavePropertiesInvalid()
594+
{
595+
var ics = """
596+
BEGIN:VCALENDAR
597+
BEGIN:VEVENT
598+
DTSTART:20070406T230000Z
599+
DTEND:20070407T010000Z
600+
END:VEVENT
601+
END:VCALENDAR
602+
""";
603+
604+
var calendar = Calendar.Load(ics);
605+
var deserialized = new CalendarSerializer(calendar).SerializeToString();
606+
607+
Assert.Multiple(() =>
608+
{
609+
Assert.That(calendar.ProductId, Is.EqualTo(null));
610+
Assert.That(calendar.Version, Is.EqualTo(null));
611+
// The serialized calendar should not contain the PRODID or VERSION properties, which are required
612+
Assert.That(deserialized, Does.Not.Contain("PRODID:").And.Not.Contains("VERSION:"));
613+
});
614+
}
591615
}

Ical.Net.Tests/SerializationTests.cs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -525,20 +525,33 @@ public void TestRRuleUntilSerialization()
525525
Assert.That(!until.EndsWith("Z"), Is.True);
526526
}
527527

528-
[Test(Description = "PRODID and VERSION should use ical.net values instead of preserving deserialized values")]
529-
public void LibraryMetadataTests()
528+
[Test]
529+
public void ProductId_and_Version_CanBeChanged()
530530
{
531531
var c = new Calendar
532532
{
533533
ProductId = "FOO",
534-
Version = "BAR"
534+
Version = "BAR",
535535
};
536+
536537
var serialized = new CalendarSerializer().SerializeToString(c);
537-
var expectedProdid = $"PRODID:{LibraryMetadata.ProdId}";
538-
Assert.That(serialized.Contains(expectedProdid, StringComparison.Ordinal), Is.True);
538+
539+
Assert.Multiple(() =>
540+
{
541+
Assert.That(serialized, Does.Contain($"PRODID:{c.ProductId}"));
542+
Assert.That(serialized, Does.Contain($"VERSION:{c.Version}"));
543+
});
544+
}
539545

540-
var expectedVersion = $"VERSION:{LibraryMetadata.Version}";
541-
Assert.That(serialized.Contains(expectedVersion, StringComparison.Ordinal), Is.True);
546+
[Test]
547+
public void ProductId_and_Version_HaveDefaultValues()
548+
{
549+
var c = new Calendar();
550+
Assert.Multiple(() =>
551+
{
552+
Assert.That(c.ProductId, Is.EqualTo(LibraryMetadata.ProdId));
553+
Assert.That(c.Version, Is.EqualTo(LibraryMetadata.Version));
554+
});
542555
}
543556

544557
[Test]

Ical.Net/Calendar.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,16 @@ public static IList<T> Load<T>(string ical)
6161
/// </summary>
6262
public Calendar()
6363
{
64-
Name = Components.Calendar;
64+
// Note: ProductId and Version Property values will be empty before _deserialization_
65+
ProductId = LibraryMetadata.ProdId;
66+
Version = LibraryMetadata.Version;
67+
6568
Initialize();
6669
}
6770

6871
private void Initialize()
6972
{
73+
Name = Components.Calendar;
7074
_mUniqueComponents = new UniqueComponentListProxy<IUniqueComponent>(Children);
7175
_mEvents = new UniqueComponentListProxy<CalendarEvent>(Children);
7276
_mTodos = new UniqueComponentListProxy<Todo>(Children);
@@ -144,16 +148,36 @@ public override int GetHashCode()
144148
public virtual ICalendarObjectList<VTimeZone> TimeZones => _mTimeZones;
145149

146150
/// <summary>
147-
/// A collection of <see cref="Todo"/> components in the iCalendar.
151+
/// A collection of <see cref="CalendarComponents.Todo"/> components in the iCalendar.
148152
/// </summary>
149153
public virtual IUniqueComponentList<Todo> Todos => _mTodos;
150154

155+
/// <summary>
156+
/// Gets or sets the version of the iCalendar definition. The default is <see cref="LibraryMetadata.Version"/>
157+
/// as per RFC 5545 Section 3.7.4 and must be specified.
158+
/// <para/>
159+
/// It specifies the identifier corresponding to the highest version number of the iCalendar specification
160+
/// that is required in order to interpret the iCalendar object.
161+
/// <para/>
162+
/// <b>Do not change unless you are sure about the consequences.</b>
163+
/// <para/>
164+
/// The default value does not apply to deserialized objects.
165+
/// </summary>
151166
public virtual string Version
152167
{
153168
get => Properties.Get<string>("VERSION");
154169
set => Properties.Set("VERSION", value);
155170
}
156171

172+
/// <summary>
173+
/// Gets or sets the product ID of the iCalendar, which typically contains the name of the software
174+
/// that created the iCalendar. The default is <see cref="LibraryMetadata.ProdId"/>.
175+
/// <para/>
176+
/// <b>Be careful when setting a custom value</b>, as it is free-form text that must conform to the iCalendar specification
177+
/// (RFC 5545 Section 3.7.3). The product ID must be specified.
178+
/// <para/>
179+
/// The default value does not apply to deserialized objects.
180+
/// </summary>
157181
public virtual string ProductId
158182
{
159183
get => Properties.Get<string>("PRODID");

Ical.Net/Constants.cs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
// Licensed under the MIT license.
44
//
55

6+
#nullable enable
67
using System;
8+
using System.Diagnostics;
79

810
namespace Ical.Net;
911

@@ -124,7 +126,7 @@ public class SerializationConstants
124126
}
125127

126128
/// <summary>
127-
/// Status codes available to an <see cref="Components.Event"/> item
129+
/// Status codes available to an <see cref="CalendarComponents.CalendarEvent"/> item
128130
/// </summary>
129131
public static class EventStatus
130132
{
@@ -137,7 +139,7 @@ public static class EventStatus
137139
}
138140

139141
/// <summary>
140-
/// Status codes available to a <see cref="Todo"/> item.
142+
/// Status codes available to a <see cref="CalendarComponents.Todo"/> item.
141143
/// </summary>
142144
public static class TodoStatus
143145
{
@@ -152,7 +154,7 @@ public static class TodoStatus
152154
}
153155

154156
/// <summary>
155-
/// Status codes available to a <see cref="Journal"/> entry.
157+
/// Status codes available to a <see cref="CalendarComponents.Journal"/> entry.
156158
/// </summary>
157159
public static class JournalStatus
158160
{
@@ -235,8 +237,32 @@ public static class TransparencyType
235237

236238
public static class LibraryMetadata
237239
{
240+
private static readonly string _assemblyVersion = GetAssemblyVersion();
241+
242+
/// <summary>
243+
/// The <c>VERSION</c> property for iCalendar objects generated by this library (RFC 5545 Section 3.7.4),
244+
/// unless overridden by user code.
245+
/// </summary>
238246
public const string Version = "2.0";
239-
public static readonly string ProdId = "-//github.com/ical-org/ical.net//NONSGML ical.net 4.0//EN";
247+
248+
/// <summary>
249+
/// The default <c>PRODID</c> property for iCalendar objects generated by this library (RFC 5545 Section 3.7.3),
250+
/// unless overridden by user code.
251+
/// <remarks>
252+
/// The text between the double slashes represents the organization or software that created the iCalendar object.
253+
/// </remarks>
254+
/// </summary>
255+
public static string ProdId => $"-//github.com/ical-org/ical.net//NONSGML ical.net {_assemblyVersion}//EN";
256+
257+
private static string GetAssemblyVersion()
258+
{
259+
var assembly = typeof(LibraryMetadata).Assembly;
260+
var fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
261+
// Prefer the file version, but fall back to the assembly version if it's not available.
262+
return fileVersionInfo.FileVersion
263+
?? assembly.GetName().Version?.ToString() // will only change for major versions
264+
?? "1.0.0.0";
265+
}
240266
}
241267

242268
public static class CalendarScales

Ical.Net/Serialization/CalendarSerializer.cs

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,6 @@ public CalendarSerializer(SerializationContext ctx) : base(ctx) { }
2727

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

30-
public override string SerializeToString(object obj)
31-
{
32-
if (obj is Calendar)
33-
{
34-
// If we're serializing a calendar, we should indicate that we're using ical.net to do the work
35-
var calendar = (Calendar) obj;
36-
calendar.Version = LibraryMetadata.Version;
37-
calendar.ProductId = LibraryMetadata.ProdId;
38-
39-
return base.SerializeToString(calendar);
40-
}
41-
42-
return base.SerializeToString(obj);
43-
}
4430

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

@@ -70,4 +56,4 @@ public int Compare(ICalendarProperty x, ICalendarProperty y)
7056
: string.Compare(x.Name, y.Name, StringComparison.OrdinalIgnoreCase);
7157
}
7258
}
73-
}
59+
}

0 commit comments

Comments
 (0)