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