/******************************************************************************* * You may amend and distribute as you like, but don't remove this header! * * EPPlus provides server-side generation of Excel 2007/2010 spreadsheets. * See http://www.codeplex.com/EPPlus for details. * * Copyright (C) 2011 Jan Källman * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Lesser General Public License for more details. * * The GNU Lesser General Public License can be viewed at http://www.opensource.org/licenses/lgpl-license.php * If you unfamiliar with this license or have questions about it, here is an http://www.gnu.org/licenses/gpl-faq.html * * All code and executables are provided "as is" with no warranty either express or implied. * The author accepts no liability for any damage or loss of business that this product may cause. * * Code change notes: * * Author Change Date ******************************************************************************* * Jan Källman Added 01-01-2012 * Jan Källman Added compression support 27-03-2012 *******************************************************************************/ using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Runtime.InteropServices; using comTypes = System.Runtime.InteropServices.ComTypes; using System.IO; using System.Security; namespace OfficeOpenXml.Utils { #if !MONO internal class CompoundDocument { internal class StoragePart { public StoragePart() { } internal Dictionary SubStorage = new Dictionary(); internal Dictionary DataStreams = new Dictionary(); } internal StoragePart Storage = null; internal CompoundDocument() { Storage = new CompoundDocument.StoragePart(); } internal CompoundDocument(FileInfo fi) { Read(fi); } internal CompoundDocument(ILockBytes lb) { Read(lb); } internal CompoundDocument(byte[] doc) { Read(doc); } internal void Read(FileInfo fi) { var b = File.ReadAllBytes(fi.FullName); Read(b); } [SecuritySafeCritical] internal void Read(byte[] doc) { ILockBytes lb; var iret = CreateILockBytesOnHGlobal(IntPtr.Zero, true, out lb); IntPtr buffer = Marshal.AllocHGlobal(doc.Length); Marshal.Copy(doc, 0, buffer, doc.Length); UIntPtr readSize; lb.WriteAt(0, buffer, doc.Length, out readSize); Marshal.FreeHGlobal(buffer); Read(lb); } [SecuritySafeCritical] internal void Read(ILockBytes lb) { if (StgIsStorageILockBytes(lb) == 0) { IStorage storage = null; if (StgOpenStorageOnILockBytes( lb, null, STGM.DIRECT | STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero, 0, out storage) == 0) { Storage = new StoragePart(); ReadParts(storage, Storage); Marshal.ReleaseComObject(storage); } } else { throw (new InvalidDataException(string.Format("Part is not a compound document"))); } } #region Compression /// /// Compression using a run length encoding algorithm. /// See MS-OVBA Section 2.4 /// /// Byte array to decompress /// internal static byte[] CompressPart(byte[] part) { MemoryStream ms = new MemoryStream(4096); BinaryWriter br = new BinaryWriter(ms); br.Write((byte)1); int compStart = 1; int compEnd = 4098; int decompStart = 0; int decompEnd = part.Length < 4096 ? part.Length : 4096; while (decompStart < decompEnd && compStart < compEnd) { byte[] chunk = CompressChunk(part, ref decompStart); ushort header; if (chunk == null || chunk.Length == 0) { header = 4096 | 0x600; //B=011 A=0 } else { header = (ushort)(((chunk.Length - 1) & 0xFFF)); header |= 0xB000; //B=011 A=1 br.Write(header); br.Write(chunk); } decompEnd = part.Length < decompStart + 4096 ? part.Length : decompStart+4096; } br.Flush(); return ms.ToArray(); } private static byte[] CompressChunk(byte[] buffer, ref int startPos) { var comprBuffer=new byte[4096]; int flagPos = 0; int cPos=1; int dPos = startPos; int dEnd=startPos+4096 < buffer.Length? startPos+4096 : buffer.Length; while(dPos 0) { int bestCandidate = -1; int bestLength = 0; int candidate = dPos - 1; int bitCount = GetLengthBits(dPos-startPos); int bits = (16 - bitCount); ushort lengthMask = (ushort)((0xFFFF) >> bits); while (candidate >= startPos) { if (buffer[candidate] == buffer[dPos]) { int length = 1; while (buffer.Length > dPos + length && buffer[candidate + length] == buffer[dPos + length] && length < lengthMask && dPos+length < dEnd) { length++; } if (length > bestLength) { bestCandidate = candidate; bestLength = length; if (bestLength == lengthMask) { break; } } } candidate--; } if (bestLength >= 3) //Copy token { tokenFlags |= (byte)(1 << i); UInt16 offsetMask = (ushort)~lengthMask; ushort token = (ushort)(((ushort)(dPos - (bestCandidate+1))) << (bitCount) | (ushort)(bestLength - 3)); Array.Copy(BitConverter.GetBytes(token), 0, comprBuffer, cPos, 2); dPos = dPos + bestLength; cPos += 2; //SetCopy Token } else { comprBuffer[cPos++] = buffer[dPos++]; } } else { comprBuffer[cPos++] = buffer[dPos++]; } if (dPos >= dEnd) break; } comprBuffer[flagPos] = tokenFlags; flagPos = cPos++; } var ret = new byte[cPos - 1]; Array.Copy(comprBuffer, ret, ret.Length); startPos = dEnd; return ret; } internal static byte[] DecompressPart(byte[] part) { return DecompressPart(part, 0); } /// /// Decompression using a run length encoding algorithm. /// See MS-OVBA Section 2.4 /// /// Byte array to decompress /// /// internal static byte[] DecompressPart(byte[] part, int startPos) { if (part[startPos] != 1) { return null; } MemoryStream ms = new MemoryStream(4096); int compressPos = startPos + 1; while(compressPos < part.Length-1) { DecompressChunk(ms, part, ref compressPos); } return ms.ToArray(); } private static void DecompressChunk(MemoryStream ms, byte[] compBuffer, ref int pos) { ushort header = BitConverter.ToUInt16(compBuffer, pos); int decomprPos=0; byte[] buffer = new byte[4198]; //Add an extra 100 byte. Some workbooks have overflowing worksheets. int size = (int)(header & 0xFFF)+3; int endPos = pos+size; int a = (int)(header & 0x7000) >> 12; int b = (int)(header & 0x8000) >> 15; pos += 2; if (b == 1) //Compressed chunk { while (pos < compBuffer.Length && pos < endPos) { //Decompress token byte token = compBuffer[pos++]; if (pos >= endPos) break; for (int i = 0; i < 8; i++) { //Literal token if ((token & (1 << i)) == 0) { ms.WriteByte(compBuffer[pos]); buffer[decomprPos++] = compBuffer[pos++]; } else //copy token { var t = BitConverter.ToUInt16(compBuffer, pos); int bitCount = GetLengthBits(decomprPos); int bits = (16 - bitCount); ushort lengthMask = (ushort)((0xFFFF) >> bits); UInt16 offsetMask = (ushort)~lengthMask; var length = (lengthMask & t) + 3; var offset = (offsetMask & t) >> (bitCount); int source = decomprPos - offset - 1; if (decomprPos + length >= buffer.Length) { // Be lenient on decompression, so extend our decompression // buffer. Excel generated VBA projects do encounter this issue. // One would think (not surprisingly that the VBA project spec) // over emphasizes the size restrictions of a DecompressionChunk. var largerBuffer = new byte[buffer.Length + 4098]; Array.Copy(buffer, largerBuffer, decomprPos); buffer = largerBuffer; } // Even though we've written to the MemoryStream, // We still should decompress the token into this buffer // in case a later token needs to use the bytes we're // about to decompress. for (int c = 0; c < length; c++) { ms.WriteByte(buffer[source]); //Must copy byte-wise because copytokens can overlap compressed buffer. buffer[decomprPos++] = buffer[source++]; } pos += 2; } if (pos >= endPos) break; } } return; } else //Raw chunk { ms.Write(compBuffer, pos, size); pos += size; return; } } private static int GetLengthBits(int decompPos) { if (decompPos <= 16) { return 12; } else if (decompPos <= 32) { return 11; } else if (decompPos <= 64) { return 10; } else if (decompPos <= 128) { return 9; } else if (decompPos <= 256) { return 8; } else if (decompPos <= 512) { return 7; } else if (decompPos <= 1024) { return 6; } else if (decompPos <= 2048) { return 5; } else if (decompPos <= 4096) { return 4; } else { //We should never end up here, but if so this is the formula to calculate the bits... return 12 - (int)Math.Truncate(Math.Log(decompPos - 1 >> 4, 2) + 1); } } #endregion #region "API declare" [ComImport] [Guid("0000000d-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IEnumSTATSTG { // The user needs to allocate an STATSTG array whose size is celt. [PreserveSig] uint Next( uint celt, [MarshalAs(UnmanagedType.LPArray), Out] System.Runtime.InteropServices.ComTypes.STATSTG[] rgelt, out uint pceltFetched ); void Skip(uint celt); void Reset(); [return: MarshalAs(UnmanagedType.Interface)] IEnumSTATSTG Clone(); } [ComImport] [Guid("0000000b-0000-0000-C000-000000000046")] [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] interface IStorage { void CreateStream( /* [string][in] */ string pwcsName, /* [in] */ uint grfMode, /* [in] */ uint reserved1, /* [in] */ uint reserved2, /* [out] */ out comTypes.IStream ppstm); void OpenStream( /* [string][in] */ string pwcsName, /* [unique][in] */ IntPtr reserved1, /* [in] */ uint grfMode, /* [in] */ uint reserved2, /* [out] */ out comTypes.IStream ppstm); void CreateStorage( /* [string][in] */ string pwcsName, /* [in] */ uint grfMode, /* [in] */ uint reserved1, /* [in] */ uint reserved2, /* [out] */ out IStorage ppstg); void OpenStorage( /* [string][unique][in] */ string pwcsName, /* [unique][in] */ IStorage pstgPriority, /* [in] */ STGM grfMode, /* [unique][in] */ IntPtr snbExclude, /* [in] */ uint reserved, /* [out] */ out IStorage ppstg); void CopyTo( [InAttribute] uint ciidExclude, [InAttribute] Guid[] rgiidExclude, [InAttribute] IntPtr snbExclude, [InAttribute] IStorage pstgDest ); void MoveElementTo( /* [string][in] */ string pwcsName, /* [unique][in] */ IStorage pstgDest, /* [string][in] */ string pwcsNewName, /* [in] */ uint grfFlags); void Commit( /* [in] */ uint grfCommitFlags); void Revert(); void EnumElements( /* [in] */ uint reserved1, /* [size_is][unique][in] */ IntPtr reserved2, /* [in] */ uint reserved3, /* [out] */ out IEnumSTATSTG ppenum); void DestroyElement( /* [string][in] */ string pwcsName); void RenameElement( /* [string][in] */ string pwcsOldName, /* [string][in] */ string pwcsNewName); void SetElementTimes( /* [string][unique][in] */ string pwcsName, /* [unique][in] */ System.Runtime.InteropServices.ComTypes.FILETIME pctime, /* [unique][in] */ System.Runtime.InteropServices.ComTypes.FILETIME patime, /* [unique][in] */ System.Runtime.InteropServices.ComTypes.FILETIME pmtime); void SetClass( /* [in] */ Guid clsid); void SetStateBits( /* [in] */ uint grfStateBits, /* [in] */ uint grfMask); void Stat( /* [out] */ out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, /* [in] */ uint grfStatFlag); } [ComVisible(false)] [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000000A-0000-0000-C000-000000000046")] internal interface ILockBytes { void ReadAt(long ulOffset, System.IntPtr pv, int cb, out UIntPtr pcbRead); void WriteAt(long ulOffset, System.IntPtr pv, int cb, out UIntPtr pcbWritten); void Flush(); void SetSize(long cb); void LockRegion(long libOffset, long cb, int dwLockType); void UnlockRegion(long libOffset, long cb, int dwLockType); void Stat(out System.Runtime.InteropServices.ComTypes.STATSTG pstatstg, int grfStatFlag); } [Flags] internal enum STGM : int { DIRECT = 0x00000000, TRANSACTED = 0x00010000, SIMPLE = 0x08000000, READ = 0x00000000, WRITE = 0x00000001, READWRITE = 0x00000002, SHARE_DENY_NONE = 0x00000040, SHARE_DENY_READ = 0x00000030, SHARE_DENY_WRITE = 0x00000020, SHARE_EXCLUSIVE = 0x00000010, PRIORITY = 0x00040000, DELETEONRELEASE = 0x04000000, NOSCRATCH = 0x00100000, CREATE = 0x00001000, CONVERT = 0x00020000, FAILIFTHERE = 0x00000000, NOSNAPSHOT = 0x00200000, DIRECT_SWMR = 0x00400000, } internal enum STATFLAG : uint { STATFLAG_DEFAULT = 0, STATFLAG_NONAME = 1, STATFLAG_NOOPEN = 2 } internal enum STGTY : int { STGTY_STORAGE = 1, STGTY_STREAM = 2, STGTY_LOCKBYTES = 3, STGTY_PROPERTY = 4 } [DllImport("ole32.dll")] private static extern int StgIsStorageFile( [MarshalAs(UnmanagedType.LPWStr)] string pwcsName); [DllImport("ole32.dll")] private static extern int StgIsStorageILockBytes( ILockBytes plkbyt); [DllImport("ole32.dll")] static extern int StgOpenStorage( [MarshalAs(UnmanagedType.LPWStr)] string pwcsName, IStorage pstgPriority, STGM grfMode, IntPtr snbExclude, uint reserved, out IStorage ppstgOpen); [DllImport("ole32.dll")] static extern int StgOpenStorageOnILockBytes( ILockBytes plkbyt, IStorage pStgPriority, STGM grfMode, IntPtr snbEnclude, uint reserved, out IStorage ppstgOpen); [DllImport("ole32.dll")] static extern int CreateILockBytesOnHGlobal( IntPtr hGlobal, bool fDeleteOnRelease, out ILockBytes ppLkbyt); [DllImport("ole32.dll")] static extern int StgCreateDocfileOnILockBytes(ILockBytes plkbyt, STGM grfMode, int reserved, out IStorage ppstgOpen); #endregion [SecuritySafeCritical] internal static int IsStorageFile(string Name) { return StgIsStorageFile(Name); } [SecuritySafeCritical] internal static int IsStorageILockBytes(ILockBytes lb) { return StgIsStorageILockBytes(lb); } [SecuritySafeCritical] internal static ILockBytes GetLockbyte(MemoryStream stream) { ILockBytes lb; var iret = CreateILockBytesOnHGlobal(IntPtr.Zero, true, out lb); byte[] docArray = stream.GetBuffer(); IntPtr buffer = Marshal.AllocHGlobal(docArray.Length); Marshal.Copy(docArray, 0, buffer, docArray.Length); UIntPtr readSize; lb.WriteAt(0, buffer, docArray.Length, out readSize); Marshal.FreeHGlobal(buffer); return lb; } [SecuritySafeCritical] private MemoryStream ReadParts(IStorage storage, StoragePart storagePart) { MemoryStream ret = null; comTypes.STATSTG statstg; storage.Stat(out statstg, (uint)STATFLAG.STATFLAG_DEFAULT); IEnumSTATSTG pIEnumStatStg = null; storage.EnumElements(0, IntPtr.Zero, 0, out pIEnumStatStg); comTypes.STATSTG[] regelt = { statstg }; uint fetched = 0; uint res = pIEnumStatStg.Next(1, regelt, out fetched); //if (regelt[0].pwcsName == "DataSpaces") //{ // PrintStorage(storage, regelt[0],""); //} while (res != 1) { foreach (var item in regelt) { if (item.type == 1) { IStorage subStorage; storage.OpenStorage(item.pwcsName, null, STGM.DIRECT | STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero, 0, out subStorage); StoragePart subStoragePart=new StoragePart(); storagePart.SubStorage.Add(item.pwcsName, subStoragePart); ReadParts(subStorage, subStoragePart); } else { storagePart.DataStreams.Add(item.pwcsName, GetOleStream(storage, item)); } } res = pIEnumStatStg.Next(1, regelt, out fetched); } Marshal.ReleaseComObject(pIEnumStatStg); return ret; } // Help method to print a storage part binary to c:\temp //private void PrintStorage(IStorage storage, System.Runtime.InteropServices.ComTypes.STATSTG sTATSTG, string topName) //{ // IStorage ds; // if (topName.Length > 0) // { // topName = topName[0] < 'A' ? topName.Substring(1, topName.Length - 1) : topName; // } // storage.OpenStorage(sTATSTG.pwcsName, // null, // (uint)(STGM.DIRECT | STGM.READ | STGM.SHARE_EXCLUSIVE), // IntPtr.Zero, // 0, // out ds); // System.Runtime.InteropServices.ComTypes.STATSTG statstgSub; // ds.Stat(out statstgSub, (uint)STATFLAG.STATFLAG_DEFAULT); // IEnumSTATSTG pIEnumStatStgSub = null; // System.Runtime.InteropServices.ComTypes.STATSTG[] regeltSub = { statstgSub }; // ds.EnumElements(0, IntPtr.Zero, 0, out pIEnumStatStgSub); // uint fetched = 0; // while (pIEnumStatStgSub.Next(1, regeltSub, out fetched) == 0) // { // string sName = regeltSub[0].pwcsName[0] < 'A' ? regeltSub[0].pwcsName.Substring(1, regeltSub[0].pwcsName.Length - 1) : regeltSub[0].pwcsName; // if (regeltSub[0].type == 1) // { // PrintStorage(ds, regeltSub[0], topName + sName + "_"); // } // else if(regeltSub[0].type==2) // { // File.WriteAllBytes(@"c:\temp\" + topName + sName + ".bin", GetOleStream(ds, regeltSub[0])); // } // } //} } /// /// Read the stream and return it as a byte-array /// /// /// /// [SecuritySafeCritical] private byte[] GetOleStream(IStorage storage, comTypes.STATSTG statstg) { comTypes.IStream pIStream; storage.OpenStream(statstg.pwcsName, IntPtr.Zero, (uint)(STGM.READ | STGM.SHARE_EXCLUSIVE), 0, out pIStream); byte[] data = new byte[statstg.cbSize]; pIStream.Read(data, (int)statstg.cbSize, IntPtr.Zero); Marshal.ReleaseComObject(pIStream); return data; } [SecuritySafeCritical] internal byte[] Save() { ILockBytes lb; var iret = CreateILockBytesOnHGlobal(IntPtr.Zero, true, out lb); IStorage storage = null; byte[] ret = null; //Create the document in-memory if (StgCreateDocfileOnILockBytes(lb, STGM.CREATE | STGM.READWRITE | STGM.SHARE_EXCLUSIVE | STGM.TRANSACTED, 0, out storage)==0) { foreach(var store in this.Storage.SubStorage) { CreateStore(store.Key, store.Value, storage); } CreateStreams(this.Storage, storage); lb.Flush(); //Now copy the unmanaged stream to a byte array --> memory stream var statstg = new comTypes.STATSTG(); lb.Stat(out statstg, 0); int size = (int)statstg.cbSize; IntPtr buffer = Marshal.AllocHGlobal(size); UIntPtr readSize; ret=new byte[size]; lb.ReadAt(0, buffer, size, out readSize); Marshal.Copy(buffer, ret, 0, size); Marshal.FreeHGlobal(buffer); } Marshal.ReleaseComObject(storage); Marshal.ReleaseComObject(lb); return ret; } private void CreateStore(string name, StoragePart subStore, IStorage storage) { IStorage subStorage; storage.CreateStorage(name, (uint)(STGM.CREATE | STGM.WRITE | STGM.DIRECT | STGM.SHARE_EXCLUSIVE), 0, 0, out subStorage); storage.Commit(0); foreach (var store in subStore.SubStorage) { CreateStore(store.Key, store.Value, subStorage); } CreateStreams(subStore, subStorage); } private void CreateStreams(StoragePart subStore, IStorage subStorage) { foreach (var ds in subStore.DataStreams) { comTypes.IStream stream; subStorage.CreateStream(ds.Key, (uint)(STGM.CREATE | STGM.WRITE | STGM.DIRECT | STGM.SHARE_EXCLUSIVE), 0, 0, out stream); stream.Write(ds.Value, ds.Value.Length, IntPtr.Zero); } subStorage.Commit(0); } } #endif }