blob: 33c4ce28b361d9f72bed14e0a9d1410c0fb0b394 [file] [log] [blame]
/*******************************************************************************
* 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<string, StoragePart> SubStorage = new Dictionary<string, StoragePart>();
internal Dictionary<string, byte[]> DataStreams = new Dictionary<string, byte[]>();
}
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
/// <summary>
/// Compression using a run length encoding algorithm.
/// See MS-OVBA Section 2.4
/// </summary>
/// <param name="part">Byte array to decompress</param>
/// <returns></returns>
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<dEnd)
{
byte tokenFlags = 0;
for (int i = 0; i < 8; i++)
{
if (dPos - startPos > 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);
}
/// <summary>
/// Decompression using a run length encoding algorithm.
/// See MS-OVBA Section 2.4
/// </summary>
/// <param name="part">Byte array to decompress</param>
/// <param name="startPos"></param>
/// <returns></returns>
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]));
// }
// }
//} }
/// <summary>
/// Read the stream and return it as a byte-array
/// </summary>
/// <param name="storage"></param>
/// <param name="statstg"></param>
/// <returns></returns>
[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
}