From 523f6e02260a8ff541d3b625cae4b62d19527ac3 Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Thu, 7 Mar 2024 16:09:24 +0000 Subject: [PATCH 1/3] Add an AddUserDefinedProperty function to OLEPropertiesContainer --- .../OLEProperties/OLEPropertiesContainer.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs index 37988bc..351ea90 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs @@ -175,6 +175,47 @@ 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, of null if this container can't contain user defined properties. + public OLEProperty AddUserDefinedProperty(VTPropertyType vtPropertyType, string name) + { + if (this.ContainerType != ContainerType.UserDefinedProperties) + return null; + + // 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"); From a462c09608802e12c8d1e89c54f272a88bd7209c Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sat, 12 Oct 2024 16:32:16 +0100 Subject: [PATCH 2/3] Add some unit tests --- .../OLEPropertiesExtensionsTest.cs | 105 +++++++++++++----- 1 file changed, 79 insertions(+), 26 deletions(-) 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 From 0cdb88718a2ceeaab23ec174e36fab0eebd18c1e Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Sun, 13 Oct 2024 15:40:04 +0100 Subject: [PATCH 3/3] Tweak error handling --- .../OLEProperties/OLEPropertiesContainer.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs index 351ea90..a055847 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs @@ -180,11 +180,16 @@ public void AddProperty(OLEProperty property) /// /// The type of property to create. /// The name of the new property. - /// The new property, of null if this container can't contain user defined properties. + /// 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) - return null; + { + 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. @@ -192,7 +197,7 @@ public OLEProperty AddUserDefinedProperty(VTPropertyType vtPropertyType, string { 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;