From 514135cefe2379d77990e3f4924920e67f636ae4 Mon Sep 17 00:00:00 2001 From: ema Date: Thu, 25 Dec 2025 02:14:44 +0800 Subject: [PATCH] Prepare to implement for Compound File Binary --- .../CompoundFileComImport.cs | 531 ++++++++++++++++++ .../CompoundFileExtractor.cs | 127 +++++ 2 files changed, 658 insertions(+) create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileComImport.cs create mode 100644 QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileComImport.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileComImport.cs new file mode 100644 index 0000000..a4eda5f --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileComImport.cs @@ -0,0 +1,531 @@ +// 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; +} diff --git a/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs new file mode 100644 index 0000000..23837f6 --- /dev/null +++ b/QuickLook.Plugin/QuickLook.Plugin.ArchiveViewer/CompoundFileBinary/CompoundFileExtractor.cs @@ -0,0 +1,127 @@ +// 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.IO; +using System.Runtime.InteropServices.ComTypes; + +namespace QuickLook.Plugin.ArchiveViewer.CompoundFileBinary; + +/// +/// Utility class to extract streams and storages from a COM compound file (IStorage) into the file system. +/// This is a thin managed wrapper that enumerates entries inside the compound file and writes streams to disk. +/// +public static class CompoundFileExtractor +{ + /// + /// Extracts all streams and storages from the compound file at + /// into the specified . Directory structure inside the compound + /// file is preserved. + /// + /// Path to the compound file (OLE compound file / structured storage). + /// Destination directory to write extracted files and directories to. If it does not exist it will be created. + public static void ExtractToDirectory(string compoundFilePath, string destinationDirectory) + { + if (!Directory.Exists(destinationDirectory)) + { + Directory.CreateDirectory(destinationDirectory); + } + + // Open the compound file as an IStorage implementation wrapped by DisposableIStorage. + using DisposableIStorage storage = new(compoundFilePath, STGM.DIRECT | STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero); + IEnumerator enumerator = storage.EnumElements(); + + // Enumerate all elements (streams and storages) at the root of the compound file. + while (enumerator.MoveNext()) + { + STATSTG entryStat = enumerator.Current; + + // STGTY_STREAM indicates the element is a stream (treat as a file). + if (entryStat.type == (int)STGTY.STGTY_STREAM) + { + ExtractStreamToDirectory(storage, entryStat.pwcsName, destinationDirectory); + } + // STGTY_STORAGE indicates the element is a nested storage (treat as a directory). + else if (entryStat.type == (int)STGTY.STGTY_STORAGE) + { + ExtractStorageToDirectory(storage, entryStat.pwcsName, destinationDirectory); + } + } + } + + /// + /// Extracts a single stream from the provided and writes it to . + /// + /// The parent storage that contains the stream. + /// Name of the stream inside the compound file. + /// Directory to write the extracted stream to. + private static void ExtractStreamToDirectory(DisposableIStorage storage, string entryName, string destinationDirectory) + { + // Build target file path for the stream extraction. + string outputPath = Path.Combine(destinationDirectory, entryName); + + // Open the stream for reading from the compound file. + using DisposableIStream stream = storage.OpenStream(entryName, IntPtr.Zero, STGM.READ | STGM.SHARE_EXCLUSIVE); + + // Query stream statistics to determine its size. + STATSTG streamStat = stream.Stat((int)STATFLAG.STATFLAG_DEFAULT); + + // Allocate a buffer exactly the size of the stream and read it in one call. + // Note: cbSize is an unsigned 64-bit value in the native struct; the wrapper exposes it as an int/long depending on implementation. + byte[] buffer = new byte[streamStat.cbSize]; + stream.Read(buffer, buffer.Length); + + // Write the stream contents to disk. This will overwrite existing files. + File.WriteAllBytes(outputPath, buffer); + } + + /// + /// Extracts a nested storage (directory) recursively. Creates a corresponding directory on disk and + /// extracts its child streams and storages. + /// + /// The parent storage that contains the nested storage. + /// Name of the nested storage inside the compound file. + /// Directory on disk that will contain the created directory for this nested storage. + private static void ExtractStorageToDirectory(DisposableIStorage storage, string entryName, string parentDirectory) + { + string currentDirectory = Path.Combine(parentDirectory, entryName); + if (!Directory.Exists(currentDirectory)) + { + Directory.CreateDirectory(currentDirectory); + } + + // Open the nested storage and enumerate its elements recursively. + using DisposableIStorage subStorage = storage.OpenStorage(entryName, null, STGM.READ | STGM.SHARE_EXCLUSIVE, IntPtr.Zero); + IEnumerator enumerator = subStorage.EnumElements(); + while (enumerator.MoveNext()) + { + STATSTG entryStat = enumerator.Current; + + // STGTY_STREAM indicates the element is a stream (treat as a file). + if (entryStat.type == (int)STGTY.STGTY_STREAM) + { + ExtractStreamToDirectory(subStorage, entryStat.pwcsName, currentDirectory); + } + // STGTY_STORAGE indicates the element is a nested storage (treat as a directory). + else if (entryStat.type == (int)STGTY.STGTY_STORAGE) + { + ExtractStorageToDirectory(subStorage, entryStat.pwcsName, currentDirectory); + } + } + } +}