diff --git a/OpenMcdf.Ole.Tests/OlePropertiesExtensionsTests.cs b/OpenMcdf.Ole.Tests/OlePropertiesExtensionsTests.cs index e92126d5..b2311edf 100644 --- a/OpenMcdf.Ole.Tests/OlePropertiesExtensionsTests.cs +++ b/OpenMcdf.Ole.Tests/OlePropertiesExtensionsTests.cs @@ -327,6 +327,88 @@ public void AddDocumentSummaryInformationCustomInfo() } } + /// As Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM, but adding user defined properties with the AddUserDefinedProperty function + [TestMethod] + public void TestAddUserDefinedProperty() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("english.presets.doc")) + stream.CopyTo(modifiedStream); + + // 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 (var cf = RootStorage.Open(modifiedStream, StorageModeFlags.LeaveOpen)) + { + CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + OlePropertiesContainer userProperties = co.UserDefinedProperties!; + 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; + + co.Save(dsiStream); + } + + ValidateAddedUserDefinedProperties(modifiedStream, 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(MemoryStream stream, DateTime testFileTimeValue) + { + using var cf = RootStorage.Open(stream); + using CfbStream cfbStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(cfbStream); + IList propArray = co.UserDefinedProperties!.Properties; + Assert.AreEqual(6, propArray.Count); + + // 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 TestAddUserDefinedPropertyShouldPreventDuplicates() + { + using MemoryStream modifiedStream = new(); + using (FileStream stream = File.OpenRead("english.presets.doc")) + stream.CopyTo(modifiedStream); + + using var cf = RootStorage.Open(modifiedStream); + CfbStream dsiStream = cf.OpenStream("\u0005DocumentSummaryInformation"); + OlePropertiesContainer co = new(dsiStream); + 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 // refs https://github.com/ironfede/openmcdf/issues/98 [TestMethod] diff --git a/OpenMcdf.Ole/OlePropertiesContainer.cs b/OpenMcdf.Ole/OlePropertiesContainer.cs index 8c8d564c..f6f66be7 100644 --- a/OpenMcdf.Ole/OlePropertiesContainer.cs +++ b/OpenMcdf.Ole/OlePropertiesContainer.cs @@ -127,6 +127,49 @@ public OleProperty CreateProperty(VTPropertyType vtPropertyType, uint propertyId public void Add(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) + { + if (this.ContainerType != ContainerType.UserDefinedProperties) + throw new InvalidOperationException(); + + // 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 (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 (PropertyNames.Count > 0) + { + uint highestIdentifier = PropertyNames.Keys.Max(); + identifier = Math.Max(highestIdentifier, 2) + 1; + } + + PropertyNames[identifier] = name; + + var op = new OleProperty(this) + { + VTType = vtPropertyType, + PropertyIdentifier = identifier + }; + + properties.Add(op); + + return op; + } + public void RemoveProperty(uint propertyIdentifier) { OleProperty? toRemove = properties.FirstOrDefault(o => o.PropertyIdentifier == propertyIdentifier);