From fd397a3c4321669b5ff03525c1df4f23a4e547df Mon Sep 17 00:00:00 2001 From: Richard Webb Date: Mon, 18 Sep 2023 20:07:01 +0100 Subject: [PATCH] Rework the property factory to allow stream specific factories, and tweak the logic for deciding whether a property value should be padded. --- OpenMcdf.sln | 1 + .../OLEProperties/OLEPropertiesContainer.cs | 7 +- .../OLEProperties/PropertyFactory.cs | 76 +++++++++++----- .../OLEProperties/PropertySetStream.cs | 11 ++- .../OLEProperties/TypedPropertyValue.cs | 85 +++++++++--------- .../OLEPropertiesExtensionsTest.cs | 22 +++++ .../Test/TestFiles/SampleWorkBook_bug98.xls | Bin 0 -> 19456 bytes 7 files changed, 128 insertions(+), 74 deletions(-) create mode 100644 sources/Test/TestFiles/SampleWorkBook_bug98.xls diff --git a/OpenMcdf.sln b/OpenMcdf.sln index f2cc4d1a..80183e9b 100644 --- a/OpenMcdf.sln +++ b/OpenMcdf.sln @@ -34,6 +34,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestFiles", "TestFiles", "{ sources\TestFiles\report.xls = sources\TestFiles\report.xls sources\TestFiles\reportREAD.xls = sources\TestFiles\reportREAD.xls sources\TestFiles\report_name_fix.xls = sources\TestFiles\report_name_fix.xls + sources\Test\TestFiles\SampleWorkBook_bug98.xls = sources\Test\TestFiles\SampleWorkBook_bug98.xls sources\TestFiles\testbad.ole = sources\TestFiles\testbad.ole sources\Test\TestFiles\winUnicodeDictionary.doc = sources\Test\TestFiles\winUnicodeDictionary.doc sources\Test\TestFiles\wstr_presets.doc = sources\Test\TestFiles\wstr_presets.doc diff --git a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs index e3b66889..0dc62fbc 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/OLEPropertiesContainer.cs @@ -227,9 +227,12 @@ public void Save(CFStream cfStream) } }; + PropertyFactory factory = + this.ContainerType == ContainerType.DocumentSummaryInfo ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; + foreach (var op in this.Properties) { - ITypedPropertyValue p = PropertyFactory.Instance.NewProperty(op.VTType, this.Context.CodePage); + ITypedPropertyValue p = factory.NewProperty(op.VTType, this.Context.CodePage, op.PropertyIdentifier); p.Value = op.Value; ps.PropertySet0.Properties.Add(p); ps.PropertySet0.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); @@ -264,7 +267,7 @@ public void Save(CFStream cfStream) // Add the properties themselves foreach (var op in this.UserDefinedProperties.Properties) { - ITypedPropertyValue p = PropertyFactory.Instance.NewProperty(op.VTType, ps.PropertySet1.PropertyContext.CodePage); + ITypedPropertyValue p = DefaultPropertyFactory.Instance.NewProperty(op.VTType, ps.PropertySet1.PropertyContext.CodePage, op.PropertyIdentifier); p.Value = op.Value; ps.PropertySet1.Properties.Add(p); ps.PropertySet1.PropertyIdentifierAndOffsets.Add(new PropertyIdentifierAndOffset() { PropertyIdentifier = op.PropertyIdentifier, Offset = 0 }); diff --git a/sources/OpenMcdf.Extensions/OLEProperties/PropertyFactory.cs b/sources/OpenMcdf.Extensions/OLEProperties/PropertyFactory.cs index e651cbb3..80777d8b 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/PropertyFactory.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/PropertyFactory.cs @@ -1,35 +1,20 @@ using OpenMcdf.Extensions.OLEProperties.Interfaces; using System; -using System.Collections.Generic; using System.Text; using System.IO; -using System.Threading; namespace OpenMcdf.Extensions.OLEProperties { - internal class PropertyFactory + internal abstract class PropertyFactory { - private static ThreadLocal instance - = new ThreadLocal(() => { return new PropertyFactory(); }); - - public static PropertyFactory Instance + static PropertyFactory() { - get - { - #if NETSTANDARD2_0_OR_GREATER - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif - return instance.Value; - } - } - - private PropertyFactory() - { - } - public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, bool isVariant = false) + public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant = false) { ITypedPropertyValue pr = null; @@ -75,9 +60,11 @@ public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, bool pr = new VT_UI8_Property(vType, isVariant); break; case VTPropertyType.VT_BSTR: - case VTPropertyType.VT_LPSTR: pr = new VT_LPSTR_Property(vType, codePage, isVariant); break; + case VTPropertyType.VT_LPSTR: + pr = CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); + break; case VTPropertyType.VT_LPWSTR: pr = new VT_LPWSTR_Property(vType, codePage, isVariant); break; @@ -94,7 +81,7 @@ public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, bool pr = new VT_EMPTY_Property(vType, isVariant); break; case VTPropertyType.VT_VARIANT_VECTOR: - pr = new VT_VariantVector(vType, codePage, isVariant); + pr = new VT_VariantVector(vType, codePage, isVariant, this, propertyIdentifier); break; case VTPropertyType.VT_CF: pr = new VT_CF_Property(vType, isVariant); @@ -110,6 +97,11 @@ public ITypedPropertyValue NewProperty(VTPropertyType vType, int codePage, bool return pr; } + protected virtual ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) + { + return new VT_LPSTR_Property(vType, codePage, isVariant); + } + #region Property implementations private class VT_EMPTY_Property : TypedPropertyValue @@ -396,7 +388,7 @@ public override void WriteScalarValue(System.IO.BinaryWriter bw, DateTime pValue } } - private class VT_LPSTR_Property : TypedPropertyValue + protected class VT_LPSTR_Property : TypedPropertyValue { private byte[] data; @@ -435,6 +427,14 @@ public override void WriteScalarValue(BinaryWriter bw, string pValue) } } + protected class VT_Unaligned_LPSTR_Property : VT_LPSTR_Property + { + public VT_Unaligned_LPSTR_Property(VTPropertyType vType, int codePage, bool isVariant) : base(vType, codePage, isVariant) + { + this.NeedsPadding = false; + } + } + private class VT_LPWSTR_Property : TypedPropertyValue { @@ -621,10 +621,15 @@ public override void WriteScalarValue(BinaryWriter bw, object pValue) private class VT_VariantVector : TypedPropertyValue { private readonly int codePage; + private readonly PropertyFactory factory; + private readonly uint propertyIdentifier; - public VT_VariantVector(VTPropertyType vType, int codePage, bool isVariant) : base(vType, isVariant) + public VT_VariantVector(VTPropertyType vType, int codePage, bool isVariant, PropertyFactory factory, uint propertyIdentifier) : base(vType, isVariant) { this.codePage = codePage; + this.factory = factory; + this.propertyIdentifier = propertyIdentifier; + this.NeedsPadding = false; } public override object ReadScalarValue(System.IO.BinaryReader br) @@ -632,7 +637,7 @@ public override object ReadScalarValue(System.IO.BinaryReader br) VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); br.ReadUInt16(); // Ushort Padding - ITypedPropertyValue p = PropertyFactory.Instance.NewProperty(vType, codePage, true); + ITypedPropertyValue p = factory.NewProperty(vType, codePage, propertyIdentifier, true); p.Read(br); return p; } @@ -648,4 +653,27 @@ public override void WriteScalarValue(BinaryWriter bw, object pValue) #endregion } + + // The default property factory. + internal sealed class DefaultPropertyFactory : PropertyFactory + { + public static PropertyFactory Instance { get; } = new DefaultPropertyFactory(); + } + + // A separate factory for DocumentSummaryInformation properties, to handle special cases with unaligned strings. + internal sealed class DocumentSummaryInfoPropertyFactory : PropertyFactory + { + public static PropertyFactory Instance { get; } = new DocumentSummaryInfoPropertyFactory(); + + protected override ITypedPropertyValue CreateLpstrProperty(VTPropertyType vType, int codePage, uint propertyIdentifier, bool isVariant) + { + // PIDDSI_HEADINGPAIR and PIDDSI_DOCPARTS use unaligned (unpadded) strings - the others are padded + if (propertyIdentifier == 0x0000000C || propertyIdentifier == 0x0000000D) + { + return new VT_Unaligned_LPSTR_Property(vType, codePage, isVariant); + } + + return base.CreateLpstrProperty(vType, codePage, propertyIdentifier, isVariant); + } + } } diff --git a/sources/OpenMcdf.Extensions/OLEProperties/PropertySetStream.cs b/sources/OpenMcdf.Extensions/OLEProperties/PropertySetStream.cs index 8ee40ce9..673f82e4 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/PropertySetStream.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/PropertySetStream.cs @@ -50,6 +50,9 @@ public void Read(System.IO.BinaryReader br) PropertySet0.Size = br.ReadUInt32(); PropertySet0.NumProperties = br.ReadUInt32(); + // Create appropriate property factory based on the stream type + Guid docSummaryGuid = new Guid(WellKnownFMTID.FMTID_DocSummaryInformation); + PropertyFactory factory = FMTID0 == docSummaryGuid ? DocumentSummaryInfoPropertyFactory.Instance : DefaultPropertyFactory.Instance; // Read property offsets (P0) for (int i = 0; i < PropertySet0.NumProperties; i++) @@ -66,7 +69,7 @@ public void Read(System.IO.BinaryReader br) for (int i = 0; i < PropertySet0.NumProperties; i++) { br.BaseStream.Seek(Offset0 + PropertySet0.PropertyIdentifierAndOffsets[i].Offset, System.IO.SeekOrigin.Begin); - PropertySet0.Properties.Add(ReadProperty(PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet0.PropertyContext.CodePage, br)); + PropertySet0.Properties.Add(ReadProperty(PropertySet0.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet0.PropertyContext.CodePage, br, factory)); } if (NumPropertySets == 2) @@ -91,7 +94,7 @@ public void Read(System.IO.BinaryReader br) for (int i = 0; i < PropertySet1.NumProperties; i++) { br.BaseStream.Seek(Offset1 + PropertySet1.PropertyIdentifierAndOffsets[i].Offset, System.IO.SeekOrigin.Begin); - PropertySet1.Properties.Add(ReadProperty(PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet1.PropertyContext.CodePage, br)); + PropertySet1.Properties.Add(ReadProperty(PropertySet1.PropertyIdentifierAndOffsets[i].PropertyIdentifier, PropertySet1.PropertyContext.CodePage, br, DefaultPropertyFactory.Instance)); } } } @@ -222,14 +225,14 @@ public void Write(System.IO.BinaryWriter bw) - private IProperty ReadProperty(uint propertyIdentifier, int codePage, BinaryReader br) + private IProperty ReadProperty(uint propertyIdentifier, int codePage, BinaryReader br, PropertyFactory factory) { if (propertyIdentifier != 0) { VTPropertyType vType = (VTPropertyType)br.ReadUInt16(); br.ReadUInt16(); // Ushort Padding - ITypedPropertyValue pr = PropertyFactory.Instance.NewProperty(vType, codePage); + ITypedPropertyValue pr = factory.NewProperty(vType, codePage, propertyIdentifier); pr.Read(br); return pr; diff --git a/sources/OpenMcdf.Extensions/OLEProperties/TypedPropertyValue.cs b/sources/OpenMcdf.Extensions/OLEProperties/TypedPropertyValue.cs index f0314f91..dece59d6 100644 --- a/sources/OpenMcdf.Extensions/OLEProperties/TypedPropertyValue.cs +++ b/sources/OpenMcdf.Extensions/OLEProperties/TypedPropertyValue.cs @@ -44,6 +44,8 @@ public bool IsVariant get { return isVariant; } } + protected virtual bool NeedsPadding { get; set; } = true; + private PropertyDimensions CheckPropertyDimensions(VTPropertyType vtType) { if ((((ushort)vtType) & 0x1000) != 0) @@ -73,40 +75,50 @@ public virtual object Value public void Read(System.IO.BinaryReader br) { long currentPos = br.BaseStream.Position; - int size = 0; - int m = 0; switch (this.PropertyDimensions) { case PropertyDimensions.IsScalar: - this.propertyValue = ReadScalarValue(br); - size = (int)(br.BaseStream.Position - currentPos); + { + this.propertyValue = ReadScalarValue(br); + int size = (int)(br.BaseStream.Position - currentPos); - m = (int)size % 4; + int m = (int)size % 4; + + if (m > 0 && this.NeedsPadding) + br.ReadBytes(4 - m); // padding + } - if (m > 0 && !IsVariant) - br.ReadBytes(4 - m); // padding break; case PropertyDimensions.IsVector: - uint nItems = br.ReadUInt32(); + { + uint nItems = br.ReadUInt32(); - List res = new List(); + List res = new List(); - for (int i = 0; i < nItems; i++) - { - T s = ReadScalarValue(br); + for (int i = 0; i < nItems; i++) + { + T s = ReadScalarValue(br); - res.Add(s); - } + res.Add(s); - this.propertyValue = res; - size = (int)(br.BaseStream.Position - currentPos); + // The padding in a vector can be per-item + int itemSize = (int)(br.BaseStream.Position - currentPos); - m = (int)size % 4; - if (m > 0 && !IsVariant) - br.ReadBytes(4 - m); // padding + int pad = (int)itemSize % 4; + if (pad > 0 && this.NeedsPadding) + br.ReadBytes(4 - pad); // padding + } + + this.propertyValue = res; + int size = (int)(br.BaseStream.Position - currentPos); + + int m = (int)size % 4; + if (m > 0 && this.NeedsPadding) + br.ReadBytes(4 - m); // padding + } break; default: break; @@ -120,7 +132,6 @@ public void Write(BinaryWriter bw) long currentPos = bw.BaseStream.Position; int size = 0; int m = 0; - bool needsPadding = HasPadding(); switch (this.PropertyDimensions) { @@ -133,7 +144,7 @@ public void Write(BinaryWriter bw) size = (int)(bw.BaseStream.Position - currentPos); m = (int)size % 4; - if (m > 0 && needsPadding) + if (m > 0 && this.NeedsPadding) for (int i = 0; i < 4 - m; i++) // padding bw.Write((byte)0); break; @@ -147,37 +158,23 @@ public void Write(BinaryWriter bw) for (int i = 0; i < ((List)this.propertyValue).Count; i++) { WriteScalarValue(bw, ((List)this.propertyValue)[i]); + + size = (int)(bw.BaseStream.Position - currentPos); + m = (int)size % 4; + + if (m > 0 && this.NeedsPadding) + for (int q = 0; q < 4 - m; q++) // padding + bw.Write((byte)0); } size = (int)(bw.BaseStream.Position - currentPos); m = (int)size % 4; - if (m > 0 && needsPadding) - for (int i = 0; i < m; i++) // padding + if (m > 0 && this.NeedsPadding) + for (int i = 0; i < 4 - m; i++) // padding bw.Write((byte)0); break; } } - - private bool HasPadding() - { - - VTPropertyType vt = (VTPropertyType)((ushort)this.VTType & 0x00FF); - - switch (vt) - { - case VTPropertyType.VT_LPSTR: - if (this.IsVariant) return false; - if (dim == PropertyDimensions.IsVector) return false; - break; - case VTPropertyType.VT_VARIANT_VECTOR: - if (dim == PropertyDimensions.IsVector) return false; - break; - default: - return true; - } - - return true; - } } } diff --git a/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs b/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs index eaa8b5f8..d27bcd1e 100644 --- a/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs +++ b/sources/Test/OpenMcdf.Extensions.Test/OLEPropertiesExtensionsTest.cs @@ -3,6 +3,7 @@ using System.IO; using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; +using System.Collections.Generic; using OpenMcdf.Extensions.OLEProperties; namespace OpenMcdf.Extensions.Test @@ -440,5 +441,26 @@ public void Test_DOCUMENT_SUMMARY_INFO_ADD_CUSTOM() Assert.AreEqual(VTPropertyType.VT_FILETIME, propArray[4].VTType); } } + + // Try to read a document which contains Vector/String properties + // refs https://github.com/ironfede/openmcdf/issues/98 + [TestMethod] + public void Test_SUMMARY_INFO_READ_LPWSTRING_VECTOR() + { + using (CompoundFile cf = new CompoundFile("SampleWorkBook_bug98.xls")) + { + var co = cf.RootStorage.GetStream("\u0005DocumentSummaryInformation").AsOLEPropertiesContainer(); + + var docPartsProperty = co.Properties.FirstOrDefault(property => property.PropertyIdentifier == 13); //13 == PIDDSI_DOCPARTS + + Assert.IsNotNull(docPartsProperty); + + var docPartsValues = docPartsProperty.Value as IEnumerable; + Assert.AreEqual(3, docPartsValues.Count()); + Assert.AreEqual("Sheet1\0", docPartsValues.ElementAt(0)); + Assert.AreEqual("Sheet2\0", docPartsValues.ElementAt(1)); + Assert.AreEqual("Sheet3\0", docPartsValues.ElementAt(2)); + } + } } } diff --git a/sources/Test/TestFiles/SampleWorkBook_bug98.xls b/sources/Test/TestFiles/SampleWorkBook_bug98.xls new file mode 100644 index 0000000000000000000000000000000000000000..063c04f5b831dbef016db7bae0fb5047488121a0 GIT binary patch literal 19456 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 literal 0 HcmV?d00001