diff --git a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs
index 37988bc..a055847 100644
--- a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs
+++ b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs
@@ -175,6 +175,52 @@ public void AddProperty(OLEProperty property)
properties.Add(property);
}
+ ///
+ /// Create a new UserDefinedProperty.
+ ///
+ /// The type of property to create.
+ /// The name of the new property.
+ /// The new property.
+ /// If UserDefinedProperties aren't allowed for this container.
+ /// If a property with the name already exists."/>
+ public OLEProperty AddUserDefinedProperty(VTPropertyType vtPropertyType, string name)
+ {
+ // @@TBD@@ If this is a DocumentSummaryInfo container, we could forward the add on to that.
+ if (this.ContainerType != ContainerType.UserDefinedProperties)
+ {
+ throw new InvalidOperationException($"UserDefinedProperties are not allowed in containers of type {this.ContainerType}");
+ }
+
+ // As per https://learn.microsoft.com/en-us/openspecs/windows_protocols/MS-OLEPS/4177a4bc-5547-49fe-a4d9-4767350fd9cf
+ // the property names have to be unique, and are case insensitive.
+ if (this.PropertyNames.Any(property => property.Value.Equals(name, StringComparison.InvariantCultureIgnoreCase)))
+ {
+ throw new ArgumentException($"User defined property names must be unique and {name} already exists", nameof(name));
+ }
+
+ // Work out a property identifier - must be > 1 and unique as per
+ // https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-oleps/333959a3-a999-4eca-8627-48a224e63e77
+ uint identifier = 2;
+
+ if (this.PropertyNames.Count > 0)
+ {
+ uint highestIdentifier = this.PropertyNames.Keys.Max();
+ identifier = Math.Max(highestIdentifier, 2) + 1;
+ }
+
+ this.PropertyNames[identifier] = name;
+
+ var op = new OLEProperty(this)
+ {
+ VTType = vtPropertyType,
+ PropertyIdentifier = identifier
+ };
+
+ properties.Add(op);
+
+ return op;
+ }
+
public void RemoveProperty(uint propertyIdentifier)
{
//throw new NotImplementedException("API Unstable - Work in progress - Milestone 2.3.0.0");
diff --git a/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs b/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs
index 80e4618..98f73f9 100644
--- a/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs
+++ b/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs
@@ -286,7 +286,8 @@ public void Test_Read_Unicode_User_Properties_Dictionary()
[TestMethod]
public void Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM()
{
- File.Delete("test_add_user_defined_property.doc");
+ const string tempFileName = nameof(Test_Add_User_Defined_Property);
+ File.Delete(tempFileName);
// Test value for a VT_FILETIME property
DateTime testNow = DateTime.UtcNow;
@@ -325,37 +326,89 @@ public void Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM()
userProperties.AddProperty(doubleProperty);
co.Save(dsiStream);
- cf.SaveAs(@"test_add_user_defined_property.doc");
+ cf.SaveAs(tempFileName);
}
- using (CompoundFile cf = new CompoundFile("test_add_user_defined_property.doc"))
+ ValidateAddedUserDefinedProperties(tempFileName, testNow);
+ }
+
+ /// As Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM, but adding user defined properties with the AddUserDefinedProperty function
+ [TestMethod]
+ public void Test_Add_User_Defined_Property()
+ {
+ const string tempFileName = nameof(Test_Add_User_Defined_Property);
+ File.Delete(tempFileName);
+
+ // Test value for a VT_FILETIME property
+ DateTime testNow = DateTime.UtcNow;
+
+ // english.presets.doc has a user defined property section, but no properties other than the codepage
+ using (CompoundFile cf = new CompoundFile("english.presets.doc"))
{
- var co = cf.RootStorage.GetStream("\u0005DocumentSummaryInformation").AsOLEPropertiesContainer();
- var propArray = co.UserDefinedProperties.Properties.ToArray();
- Assert.AreEqual(6, propArray.Length);
+ var dsiStream = cf.RootStorage.GetStream("\u0005DocumentSummaryInformation");
+ var co = dsiStream.AsOLEPropertiesContainer();
+ var userProperties = co.UserDefinedProperties;
- // CodePage prop
- Assert.AreEqual(1u, propArray[0].PropertyIdentifier);
- Assert.AreEqual("0x00000001", propArray[0].PropertyName);
- Assert.AreEqual((short)-535, propArray[0].Value);
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_LPSTR, "StringProperty").Value = "Hello";
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_BOOL, "BooleanProperty").Value = true;
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_I4, "IntegerProperty").Value = 3456;
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_FILETIME, "DateProperty").Value = testNow;
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_R8, "DoubleProperty").Value = 1.234567d;
- // User properties
- Assert.AreEqual("StringProperty", propArray[1].PropertyName);
- Assert.AreEqual("Hello", propArray[1].Value);
- Assert.AreEqual(VTPropertyType.VT_LPSTR, propArray[1].VTType);
- Assert.AreEqual("BooleanProperty", propArray[2].PropertyName);
- Assert.AreEqual(true, propArray[2].Value);
- Assert.AreEqual(VTPropertyType.VT_BOOL, propArray[2].VTType);
- Assert.AreEqual("IntegerProperty", propArray[3].PropertyName);
- Assert.AreEqual(3456, propArray[3].Value);
- Assert.AreEqual(VTPropertyType.VT_I4, propArray[3].VTType);
- Assert.AreEqual("DateProperty", propArray[4].PropertyName);
- Assert.AreEqual(testNow, propArray[4].Value);
- Assert.AreEqual(VTPropertyType.VT_FILETIME, propArray[4].VTType);
- Assert.AreEqual("DoubleProperty", propArray[5].PropertyName);
- Assert.AreEqual(1.234567d, propArray[5].Value);
- Assert.AreEqual(VTPropertyType.VT_R8, propArray[5].VTType);
+ co.Save(dsiStream);
+ cf.SaveAs(tempFileName);
}
+
+ ValidateAddedUserDefinedProperties(tempFileName, testNow);
+ }
+
+ // Validate that the user defined properties added by Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM / Test_Add_User_Defined_Property are as expected
+ private static void ValidateAddedUserDefinedProperties(string filePath, DateTime testFileTimeValue)
+ {
+ using CompoundFile cf = new(filePath);
+ OLEPropertiesContainer co = cf.RootStorage.GetStream("\u0005DocumentSummaryInformation").AsOLEPropertiesContainer();
+ OLEProperty[] propArray = co.UserDefinedProperties.Properties.ToArray();
+ Assert.AreEqual(6, propArray.Length);
+
+ // CodePage prop
+ Assert.AreEqual(1u, propArray[0].PropertyIdentifier);
+ Assert.AreEqual("0x00000001", propArray[0].PropertyName);
+ Assert.AreEqual((short)-535, propArray[0].Value);
+
+ // User properties
+ Assert.AreEqual("StringProperty", propArray[1].PropertyName);
+ Assert.AreEqual("Hello", propArray[1].Value);
+ Assert.AreEqual(VTPropertyType.VT_LPSTR, propArray[1].VTType);
+ Assert.AreEqual("BooleanProperty", propArray[2].PropertyName);
+ Assert.AreEqual(true, propArray[2].Value);
+ Assert.AreEqual(VTPropertyType.VT_BOOL, propArray[2].VTType);
+ Assert.AreEqual("IntegerProperty", propArray[3].PropertyName);
+ Assert.AreEqual(3456, propArray[3].Value);
+ Assert.AreEqual(VTPropertyType.VT_I4, propArray[3].VTType);
+ Assert.AreEqual("DateProperty", propArray[4].PropertyName);
+ Assert.AreEqual(testFileTimeValue, propArray[4].Value);
+ Assert.AreEqual(VTPropertyType.VT_FILETIME, propArray[4].VTType);
+ Assert.AreEqual("DoubleProperty", propArray[5].PropertyName);
+ Assert.AreEqual(1.234567d, propArray[5].Value);
+ Assert.AreEqual(VTPropertyType.VT_R8, propArray[5].VTType);
+ }
+
+ /// The names of user defined properties must be unique - adding a duplicate should throw.
+ [TestMethod]
+ public void Test_Add_User_Defined_Property_Should_Prevent_Duplicates()
+ {
+ using var cf = new CompoundFile("english.presets.doc");
+
+ CFStream dsiStream = cf.RootStorage.GetStream("\u0005DocumentSummaryInformation");
+ OLEPropertiesContainer co = dsiStream.AsOLEPropertiesContainer();
+ OLEPropertiesContainer userProperties = co.UserDefinedProperties;
+
+ userProperties.AddUserDefinedProperty(VTPropertyType.VT_LPSTR, "StringProperty");
+
+ ArgumentException exception =
+ Assert.ThrowsException(() => userProperties.AddUserDefinedProperty(VTPropertyType.VT_LPSTR, "stringproperty"));
+
+ Assert.AreEqual("name", exception.ParamName);
}
// Try to read a document which contains Vector/String properties