// Copyright © 2017-2025 QL-Win Contributors
//
// This file is part of QuickLook program.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using FILETIME = System.Runtime.InteropServices.ComTypes.FILETIME;
using STATSTG = System.Runtime.InteropServices.ComTypes.STATSTG;
namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary;
///
/// A disposable wrapper for a COM instance.
/// Provides convenience methods for reading, writing and querying stream metadata
/// while ensuring the underlying COM object is released when disposed.
///
public class DisposableIStream : IDisposable
{
///
/// The underlying COM stream object.
///
public IStream Stream { get; private set; }
///
/// Wrap an existing .
///
/// The COM IStream to wrap.
public DisposableIStream(IStream stream)
{
Stream = stream;
}
///
/// Open a named stream from the given storage and wrap it.
///
/// Parent storage containing the stream.
/// Stream name within the storage.
/// Access mode flags (STGM).
public DisposableIStream(IStorage storage, string name, STGM mode)
{
storage.OpenStream(name, IntPtr.Zero, mode, 0, out IStream stream);
Stream = stream;
}
///
/// Read up to bytes from the stream into .
/// Returns the number of bytes actually read.
///
/// Destination buffer.
/// Maximum number of bytes to read.
/// Number of bytes read.
public int Read(byte[] buffer, int length)
{
// Use unmanaged memory to receive the number of bytes read from IStream.Read.
nint pcbRead = Marshal.AllocHGlobal(sizeof(int));
Stream.Read(buffer, length, pcbRead);
int bytesRead = Marshal.ReadInt32(pcbRead);
Marshal.FreeHGlobal(pcbRead);
return bytesRead;
}
///
/// Query the STATSTG information for this stream.
///
/// Flags controlling returned data (see ).
/// A describing the stream.
public STATSTG Stat(int statFlag)
{
Stream.Stat(out STATSTG statstg, statFlag);
return statstg;
}
///
/// Write bytes from into the stream.
///
/// Source buffer containing data to write.
/// Number of bytes from buffer to write.
public void Write(byte[] buffer, int length)
{
nint pcbWritten = Marshal.AllocHGlobal(sizeof(int));
Stream.Write(buffer, length, pcbWritten);
Marshal.FreeHGlobal(pcbWritten);
}
///
/// Releases the wrapped COM instance.
///
public void Dispose()
{
if (Stream != null)
{
_ = Marshal.ReleaseComObject(Stream);
Stream = null!;
}
}
}
///
/// A disposable wrapper for a COM instance.
/// Provides helper methods to open nested storages/streams and enumerate children.
///
public class DisposableIStorage : IDisposable
{
///
/// Create a new structured storage file at using the provided .
/// This wraps StgCreateStorageEx and throws an exception on failure.
///
/// Filesystem path for the new storage.
/// STGM flags controlling creation mode.
/// A new wrapping the created storage.
public static DisposableIStorage CreateStorage(string filePath, STGM mode)
{
// GUID for property set storage (V4); passed to StgCreateStorageEx.
Guid propertySetStorageId = new("0000013A-0000-0000-C000-000000000046");
STGOPTIONS options;
options.usVersion = 1;
options.reserved = 0;
options.ulSectorSize = 4096;
int hr = Ole32.StgCreateStorageEx(filePath, mode, STGFMT.STGFMT_DOCFILE, 0, ref options, IntPtr.Zero, ref propertySetStorageId, out IStorage storage);
if (hr != HRESULT.S_OK)
{
Exception ex = Marshal.GetExceptionForHR(hr);
throw new Exception("Error while creating file: " + (ex?.Message));
}
return new DisposableIStorage(storage);
}
///
/// The underlying COM IStorage instance.
///
public IStorage Storage { get; private set; }
private DisposableIStorage(IStorage storage)
{
Storage = storage;
}
///
/// Open an existing structured storage file and wrap it.
/// This calls StgOpenStorage and throws an exception on failure.
///
/// Path to the storage file to open.
/// STGM flags controlling open mode.
/// Reserved parameter passed to native API for exclude names mask.
public DisposableIStorage(string filePath, STGM mode, nint excludeNames)
{
int hr = Ole32.StgOpenStorage(filePath, null, mode, excludeNames, 0, out IStorage storage);
if (hr != HRESULT.S_OK)
{
Exception ex = Marshal.GetExceptionForHR(hr);
throw new Exception("Error while opening file: " + (ex?.Message));
}
Storage = storage;
}
///
/// Open a nested storage (child directory-like storage) and return a new wrapper.
///
/// Name of the nested storage.
/// Optional priority storage parameter for native call.
/// STGM access flags.
/// Reserved exclude names mask.
/// A wrapping the opened nested storage.
public DisposableIStorage OpenStorage(string name, IStorage priorityStorage, STGM mode, nint excludeNames)
{
Storage.OpenStorage(name, priorityStorage, mode, excludeNames, 0, out IStorage subStorage);
return new DisposableIStorage(subStorage);
}
///
/// Open a named stream from this storage and return a disposable wrapper for it.
///
/// Name of the stream.
/// Reserved pointer passed to native call.
/// STGM access flags.
/// A wrapping the opened stream.
public DisposableIStream OpenStream(string name, nint reserved1, STGM mode)
{
Storage.OpenStream(name, reserved1, mode, 0, out IStream stream);
return new DisposableIStream(stream);
}
///
/// Create a new stream within this storage and return a wrapper for writing.
///
/// Name for the new stream.
/// STGM flags controlling creation mode.
/// A for the created stream.
public DisposableIStream CreateStream(string name, STGM mode)
{
Storage.CreateStream(name, mode, 0, 0, out IStream stream);
return new DisposableIStream(stream);
}
///
/// Enumerate child elements (streams and storages) of this storage.
/// Each yielded describes a single child element.
///
///
/// The caller should not assume ownership of the returned structures; they are copies of the native data.
///
/// An enumerator yielding entries.
public IEnumerator EnumElements()
{
Storage.EnumElements(0, IntPtr.Zero, 0, out IEnumSTATSTG enumStatStg);
STATSTG[] statStg = new STATSTG[1];
while (enumStatStg.Next(1, statStg, out uint fetched) == 0 && fetched > 0)
{
yield return statStg[0];
}
_ = Marshal.ReleaseComObject(enumStatStg);
}
///
/// Releases the wrapped COM instance.
///
public void Dispose()
{
if (Storage != null)
{
_ = Marshal.ReleaseComObject(Storage);
Storage = null!;
}
}
}
///
/// Enumerates the STATSTG structures returned by .
///
[ComImport]
[Guid("0000000d-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IEnumSTATSTG
{
///
/// Retrieves a specified number of items in the enumeration sequence.
///
/// The number of items to be retrieved.
/// An array of STATSTG items.
/// The number of items actually retrieved.
/// S_OK if the number of items supplied is celt; otherwise, S_FALSE.
[PreserveSig]
public uint Next(uint requestedCount, [MarshalAs(UnmanagedType.LPArray), Out] STATSTG[] elements, out uint fetchedCount);
///
/// Skips a specified number of items in the enumeration sequence.
///
/// The number of items to be skipped.
public void Skip(uint count);
///
/// Resets the enumeration sequence to the beginning.
///
public void Reset();
///
/// Creates a new enumerator that contains the same enumeration state as the current one.
///
/// A clone of the current enumerator.
[return: MarshalAs(UnmanagedType.Interface)]
public IEnumSTATSTG Clone();
}
///
/// Provides methods for creating and managing the root storage object, child storage objects, and stream objects.
/// Represents the COM IStorage interface.
///
[ComImport]
[Guid("0000000b-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IStorage
{
///
/// Creates a new stream object in this storage object with the specified name and access mode.
///
/// Name of the stream to create.
/// STGM flags that specify access and creation options.
/// Reserved; must be zero.
/// Reserved; must be zero.
/// Receives the created instance.
public void CreateStream(string name, STGM mode, uint reserved1, uint reserved2, out IStream stream);
///
/// Opens the specified stream object in this storage object and returns a pointer to the interface.
///
/// Name of the stream to open.
/// Reserved pointer passed to native call (typically IntPtr.Zero).
/// STGM flags that specify access mode.
/// Reserved; must be zero.
/// Receives the opened instance.
public void OpenStream(string name, nint reserved1, STGM mode, uint reserved2, out IStream stream);
///
/// Creates a new storage object (nested storage) with the specified name and access mode.
///
/// Name of the nested storage to create.
/// STGM flags that specify access and creation options.
/// Reserved; must be zero.
/// Reserved; must be zero.
/// Receives the created instance.
public void CreateStorage(string name, STGM mode, uint reserved1, uint reserved2, out IStorage storage);
///
/// Opens an existing nested storage object by name and returns a pointer to the interface.
///
/// Name of the nested storage to open.
/// Optional priority storage used by the native API.
/// STGM flags that specify access mode.
/// Reserved exclude names mask.
/// Reserved; must be zero.
/// Receives the opened instance.
public void OpenStorage(string name, IStorage priorityStorage, STGM mode, nint excludeNames, uint reserved, out IStorage storage);
///
/// Copies the specified elements and interfaces from this storage object to another storage object.
///
/// Number of interface IDs in the excluded list.
/// GUID identifying interfaces to exclude (native signature uses a pointer/array).
/// Reserved exclude names mask.
/// Destination storage that receives the copied elements.
public void CopyTo(uint excludedInterfaceCount, Guid excludedInterfaceIds, nint excludeNames, IStorage destStorage);
///
/// Moves or renames an element from this storage to a destination storage.
///
/// The current name of the element to move.
/// Destination storage to receive the element.
/// New name for the element in the destination storage.
/// Flags that control the operation.
public void MoveElementTo(string name, IStorage destStorage, string newName, uint flags);
///
/// Commits changes made to this storage object to the underlying storage medium.
///
/// Flags that control commit behavior.
public void Commit(uint commitFlags);
///
/// Discards changes that have been made to this storage object since the last commit.
///
public void Revert();
///
/// Enumerates the elements contained in this storage object.
///
/// Reserved value passed to the native API.
/// Reserved pointer passed to the native API.
/// Reserved value passed to the native API.
/// Receives an enumerator for the elements.
public void EnumElements(uint reserved1, nint reserved2, uint reserved3, out IEnumSTATSTG enumStat);
///
/// Destroys the specified element (stream or storage) within this storage object.
///
/// Name of the element to destroy.
public void DestroyElement(string name);
///
/// Renames an existing element within this storage object.
///
/// Current name of the element.
/// New name for the element.
public void RenameElement(string oldName, string newName);
///
/// Sets the creation, access and modification times for the specified element.
///
/// Name of the element whose timestamps will be updated.
/// Creation time to set.
/// Last access time to set.
/// Last modification time to set.
public void SetElementTimes(string name, FILETIME creationTime, FILETIME accessTime, FILETIME modificationTime);
///
/// Sets the class identifier (CLSID) for this storage object.
///
/// CLSID to associate with the storage.
public void SetClass(Guid clsid);
///
/// Sets state bits for this storage object.
///
/// State bits to set.
/// Mask specifying which bits to change.
public void SetStateBits(uint stateBits, uint mask);
///
/// Retrieves the STATSTG structure that contains statistical information about this storage object or an element.
///
/// Receives the STATSTG structure.
/// Specifies whether the name is returned and/or other options (see ).
public void Stat(out STATSTG statStg, uint statFlag);
}
///
/// The STGM constants are flags that indicate conditions for creating and deleting the object and access modes for the object.
/// These are passed to storage and stream creation/opening methods.
///
[Flags]
public 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,
}
///
/// Specifies whether the STATSTG structure contains the name of the storage object.
///
public enum STATFLAG : uint
{
STATFLAG_DEFAULT = 0,
STATFLAG_NONAME = 1,
STATFLAG_NOOPEN = 2,
}
///
/// The STGTY enumeration values specify the type of a storage object.
/// STGTY_STORAGE represents a nested storage (similar to a directory).
/// STGTY_STREAM represents a stream (similar to a file) inside a storage.
///
public enum STGTY : int
{
///
/// A nested storage object (treat as a directory when extracting).
///
STGTY_STORAGE = 1,
///
/// A stream object (treat as a file when extracting).
///
STGTY_STREAM = 2,
STGTY_LOCKBYTES = 3,
STGTY_PROPERTY = 4,
}
///
/// The STGFMT enumeration values specify the format of a storage object.
///
public enum STGFMT : int
{
STGFMT_STORAGE = 0,
STGFMT_FILE = 3,
STGFMT_ANY = 4,
STGFMT_DOCFILE = 5,
}
[StructLayout(LayoutKind.Sequential)]
public struct STGOPTIONS
{
public ushort usVersion;
public ushort reserved;
public uint ulSectorSize;
}
file static class Ole32
{
///
/// Determines whether the given path is a structured storage file.
///
[DllImport("ole32.dll")]
public static extern int StgIsStorageFile([MarshalAs(UnmanagedType.LPWStr)] string filePath);
///
/// Opens an existing compound file and returns an IStorage interface.
/// This is the managed signature for the native StgOpenStorage function.
///
[DllImport("ole32.dll")]
public static extern int StgOpenStorage(
[MarshalAs(UnmanagedType.LPWStr)] string filePath,
IStorage priorityStorage,
STGM mode,
nint excludeNames,
uint reserved,
out IStorage openStorage);
///
/// Creates a new structured storage file. Managed signature for StgCreateStorageEx.
///
[DllImport("ole32.dll")]
public static extern int StgCreateStorageEx(
[MarshalAs(UnmanagedType.LPWStr)] string filePath,
STGM mode,
STGFMT format,
uint attrs,
ref STGOPTIONS options,
nint securityDescriptor,
ref Guid riid,
out IStorage openObject);
}
file static class HRESULT
{
///
/// Success HRESULT code.
///
public const int S_OK = 0;
}