From 589214ea38662f0083a40443fe5218e950da89e3 Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Wed, 30 Jul 2025 11:30:33 -0400 Subject: [PATCH] Fix most CFB extraction issues, remove OpenMcdf --- .../BinaryObjectScanner.csproj | 1 - BinaryObjectScanner/FileType/CFB.cs | 717 ++++++++++++++++-- README.md | 4 +- 3 files changed, 653 insertions(+), 69 deletions(-) diff --git a/BinaryObjectScanner/BinaryObjectScanner.csproj b/BinaryObjectScanner/BinaryObjectScanner.csproj index 6b3763df..c238a37f 100644 --- a/BinaryObjectScanner/BinaryObjectScanner.csproj +++ b/BinaryObjectScanner/BinaryObjectScanner.csproj @@ -73,7 +73,6 @@ - diff --git a/BinaryObjectScanner/FileType/CFB.cs b/BinaryObjectScanner/FileType/CFB.cs index d3994395..da796a9e 100644 --- a/BinaryObjectScanner/FileType/CFB.cs +++ b/BinaryObjectScanner/FileType/CFB.cs @@ -1,10 +1,11 @@ using System; +using System.Collections.Generic; using System.IO; using System.Text; using BinaryObjectScanner.Interfaces; -#if NET40_OR_GREATER || NETCOREAPP -using OpenMcdf; -#endif +using SabreTools.IO.Extensions; +using SabreTools.Models.CFB; +using static SabreTools.Models.CFB.Constants; namespace BinaryObjectScanner.FileType { @@ -26,76 +27,142 @@ namespace BinaryObjectScanner.FileType /// public bool Extract(Stream? stream, string file, string outDir, bool includeDebug) { -#if NET20 || NET35 - // Not supported for .NET Framework 2.0 or .NET Framework 3.5 due to library support - return false; -#else - if (stream == null || !stream.CanRead) + // Get a wrapper for the CFB + var model = Deserialize(stream); + var cfb = new SabreTools.Serialization.Wrappers.CFB(model, stream); + if (cfb?.Model == null) return false; + // Loop through and extract all files + Directory.CreateDirectory(outDir); + ExtractAll(cfb, outDir); + + return true; + } + + /// + /// Extract all files from the CFB to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + private static bool ExtractAll(SabreTools.Serialization.Wrappers.CFB? cfb, string outputDirectory) + { + // If we have no files + if (cfb?.Model?.DirectoryEntries == null || cfb.Model.DirectoryEntries.Length == 0) + return false; + + // Loop through and extract all directory entries to the output + bool allExtracted = true; + for (int i = 0; i < cfb.Model.DirectoryEntries.Length; i++) + { + allExtracted &= ExtractEntry(cfb, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the CFB to an output directory by index + /// + /// Entry index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + private static bool ExtractEntry(SabreTools.Serialization.Wrappers.CFB cfb, int index, string outputDirectory) + { + // If we have no entries + if (cfb?.Model?.DirectoryEntries == null || cfb.Model.DirectoryEntries.Length == 0) + return false; + + // If we have an invalid index + if (index < 0 || index >= cfb.Model.DirectoryEntries.Length) + return false; + + // Get the entry information + var entry = cfb.Model.DirectoryEntries[index]; + if (entry == null) + return false; + + // Only try to extract stream objects + if (entry.ObjectType != ObjectType.StreamObject) + return true; + + // Get the entry data + byte[]? data = GetDirectoryEntryData(cfb, entry); + if (data == null) + return false; + + // If we have an invalid output directory + if (string.IsNullOrEmpty(outputDirectory)) + return false; + + // Ensure the output filename is trimmed + string filename = entry.Name ?? $"entry{index}"; + byte[] nameBytes = Encoding.UTF8.GetBytes(filename); + if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80) + filename = Encoding.UTF8.GetString(nameBytes, 3, nameBytes.Length - 3); + + foreach (char c in Path.GetInvalidFileNameChars()) + { + filename = filename.Replace(c, '_'); + } + + // Ensure directory separators are consistent + if (Path.DirectorySeparatorChar == '\\') + filename = filename.Replace('/', '\\'); + else if (Path.DirectorySeparatorChar == '/') + filename = filename.Replace('\\', '/'); + + // Ensure the full output directory exists + filename = Path.Combine(outputDirectory, filename); + var directoryName = Path.GetDirectoryName(filename); + if (directoryName != null && !Directory.Exists(directoryName)) + Directory.CreateDirectory(directoryName); + + // Try to write the data try { - using var msi = new CompoundFile(stream, CFSUpdateMode.ReadOnly, CFSConfiguration.Default); - msi.RootStorage.VisitEntries((e) => - { - try - { - if (!e.IsStream) - return; - - var str = msi.RootStorage.GetStream(e.Name); - if (str == null) - return; - - byte[] strData = str.GetData(); - if (strData == null) - return; - - var decoded = DecodeStreamName(e.Name)?.TrimEnd('\0'); - if (decoded == null) - return; - - byte[] nameBytes = Encoding.UTF8.GetBytes(e.Name); - - // UTF-8 encoding of 0x4840. - if (nameBytes[0] == 0xe4 && nameBytes[1] == 0xa1 && nameBytes[2] == 0x80) - decoded = decoded.Substring(3); - - foreach (char c in Path.GetInvalidFileNameChars()) - { - decoded = decoded.Replace(c, '_'); - } - - // Ensure directory separators are consistent - if (Path.DirectorySeparatorChar == '\\') - decoded = decoded.Replace('/', '\\'); - else if (Path.DirectorySeparatorChar == '/') - decoded = decoded.Replace('\\', '/'); - - // Ensure the full output directory exists - decoded = Path.Combine(outDir, decoded); - var directoryName = Path.GetDirectoryName(decoded); - if (directoryName != null && !Directory.Exists(directoryName)) - Directory.CreateDirectory(directoryName); - - using Stream fs = File.OpenWrite(decoded); - fs.Write(strData, 0, strData.Length); - fs.Flush(); - } - catch (Exception ex) - { - if (includeDebug) Console.WriteLine(ex); - } - }, recursive: true); - - return true; + // Open the output file for writing + using FileStream fs = File.OpenWrite(filename); + fs.Write(data); + fs.Flush(); } - catch (Exception ex) + catch { - if (includeDebug) Console.WriteLine(ex); return false; } -#endif + + return true; + } + + /// + /// Read the entry data for a single directory entry, if possible + /// + /// Entry to try to retrieve data for + /// Byte array representing the entry data on success, null otherwise + private static byte[]? GetDirectoryEntryData(SabreTools.Serialization.Wrappers.CFB cfb, DirectoryEntry entry) + { + // If the CFB is invalid + if (cfb.Model?.Header == null) + return null; + + // Only try to extract stream objects + if (entry.ObjectType != ObjectType.StreamObject) + return null; + + // Determine which FAT is being used + bool miniFat = entry.StreamSize < cfb.Model.Header.MiniStreamCutoffSize; + + // Get the chain data + var chain = miniFat + ? GetMiniFATSectorChainData(cfb, (SectorNumber)entry.StartingSectorLocation) + : GetFATSectorChainData(cfb, (SectorNumber)entry.StartingSectorLocation); + if (chain == null) + return null; + + // Return only the proper amount of data + byte[] data = new byte[entry.StreamSize]; + Array.Copy(chain, 0, data, 0, (int)Math.Min(chain.Length, (long)entry.StreamSize)); + return data; } /// Adapted from LibMSI @@ -162,5 +229,525 @@ namespace BinaryObjectScanner.FileType return '.'; return '_'; } + + #region REMOVE WHEN SERIALIZATION UPDATED + + /// + private static Binary? Deserialize(Stream? data) + { + // If the data is invalid + if (data == null || !data.CanRead) + return null; + + try + { + // Create a new binary to fill + var binary = new Binary(); + + #region Header + + // Try to parse the file header + var fileHeader = ParseFileHeader(data); + if (fileHeader?.Signature != SignatureUInt64) + return null; + if (fileHeader.ByteOrder != 0xFFFE) + return null; + if (fileHeader.MajorVersion == 3 && fileHeader.SectorShift != 0x0009) + return null; + else if (fileHeader.MajorVersion == 4 && fileHeader.SectorShift != 0x000C) + return null; + if (fileHeader.MajorVersion == 3 && fileHeader.NumberOfDirectorySectors != 0) + return null; + if (fileHeader.MiniStreamCutoffSize != 0x00001000) + return null; + + // Set the file header + binary.Header = fileHeader; + + #endregion + + #region DIFAT Sector Numbers + + // Create a DIFAT sector table + var difatSectors = new List(); + + // Add the sectors from the header + if (fileHeader.DIFAT != null) + difatSectors.AddRange(fileHeader.DIFAT); + + // Loop through and add the DIFAT sectors + var currentSector = (SectorNumber?)fileHeader.FirstDIFATSectorLocation; + for (int i = 0; i < fileHeader.NumberOfDIFATSectors; i++) + { + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + difatSectors.AddRange(sectorNumbers); + } + + // Get the next sector from the DIFAT + currentSector = difatSectors[i]; + } + + // Assign the DIFAT sectors table + binary.DIFATSectorNumbers = [.. difatSectors]; + + #endregion + + #region FAT Sector Numbers + + // Create a FAT sector table + var fatSectors = new List(); + + // Loop through and add the FAT sectors + for (int i = 0; i < fileHeader.NumberOfFATSectors; i++) + { + // Get the next sector from the DIFAT + currentSector = binary.DIFATSectorNumbers[i]; + + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + fatSectors.AddRange(sectorNumbers); + } + } + + // Assign the FAT sectors table + binary.FATSectorNumbers = [.. fatSectors]; + + #endregion + + #region Mini FAT Sector Numbers + + // Create a mini FAT sector table + var miniFatSectors = new List(); + + // Loop through and add the mini FAT sectors + currentSector = (SectorNumber)fileHeader.FirstMiniFATSectorLocation; + for (int i = 0; i < fileHeader.NumberOfMiniFATSectors; i++) + { + // If we have a readable sector + if (currentSector <= SectorNumber.MAXREGSECT) + { + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var sectorNumbers = ParseSectorNumbers(data, fileHeader.SectorShift); + if (sectorNumbers == null) + return null; + + // Add the sector shifts + miniFatSectors.AddRange(sectorNumbers); + } + + // Get the next sector from the FAT + currentSector = binary.FATSectorNumbers[(int)currentSector]; + } + + // Assign the mini FAT sectors table + binary.MiniFATSectorNumbers = [.. miniFatSectors]; + + #endregion + + #region Directory Entries + + // Get the offset of the first directory sector + long firstDirectoryOffset = (long)(fileHeader.FirstDirectorySectorLocation * Math.Pow(2, fileHeader.SectorShift)); + if (firstDirectoryOffset < 0 || firstDirectoryOffset >= data.Length) + return null; + + // Seek to the first directory sector + data.Seek(firstDirectoryOffset, SeekOrigin.Begin); + + // Create a directory sector table + var directorySectors = new List(); + + // Get the number of directory sectors + uint directorySectorCount = 0; + switch (fileHeader.MajorVersion) + { + case 3: + directorySectorCount = int.MaxValue; + break; + case 4: + directorySectorCount = fileHeader.NumberOfDirectorySectors; + break; + } + + // Loop through and add the directory sectors + currentSector = (SectorNumber)fileHeader.FirstDirectorySectorLocation; + for (int i = 0; i < directorySectorCount; i++) + { + // If we have an end of chain + if (currentSector == SectorNumber.ENDOFCHAIN) + break; + + // If we have an unusable sector for a version 3 file + if (directorySectorCount == int.MaxValue && currentSector > SectorNumber.MAXREGSECT) + break; + + // Get the new next sector information + long sectorOffset = (long)((long)(currentSector + 1) * Math.Pow(2, fileHeader.SectorShift)); + if (sectorOffset < 0 || sectorOffset >= data.Length) + return null; + + // Seek to the next sector + data.Seek(sectorOffset, SeekOrigin.Begin); + + // Try to parse the sectors + var directoryEntries = ParseDirectoryEntries(data, fileHeader.SectorShift, fileHeader.MajorVersion); + if (directoryEntries == null) + return null; + + // Add the sector shifts + directorySectors.AddRange(directoryEntries); + + // Get the next sector from the DIFAT + var fat = binary.DIFATSectorNumbers[(int)currentSector]; + currentSector = binary.FATSectorNumbers[(int)fat]; + } + + // Assign the Directory sectors table + binary.DirectoryEntries = [.. directorySectors]; + + #endregion + + return binary; + } + catch + { + // Ignore the actual error + return null; + } + } + + /// + /// Parse a Stream into a DirectoryEntry + /// + /// Stream to parse + /// Filled DirectoryEntry on success, null on error + private static DirectoryEntry ParseDirectoryEntry(Stream data) + { + var obj = new DirectoryEntry(); + + byte[] name = data.ReadBytes(64); + obj.Name = DecodeStreamName(Encoding.Unicode.GetString(name))?.TrimEnd('\0'); + obj.NameLength = data.ReadUInt16LittleEndian(); + obj.ObjectType = (ObjectType)data.ReadByteValue(); + obj.ColorFlag = (ColorFlag)data.ReadByteValue(); + obj.LeftSiblingID = (StreamID)data.ReadUInt32LittleEndian(); + obj.RightSiblingID = (StreamID)data.ReadUInt32LittleEndian(); + obj.ChildID = (StreamID)data.ReadUInt32LittleEndian(); + obj.CLSID = data.ReadGuid(); + obj.StateBits = data.ReadUInt32LittleEndian(); + obj.CreationTime = data.ReadUInt64LittleEndian(); + obj.ModifiedTime = data.ReadUInt64LittleEndian(); + obj.StartingSectorLocation = data.ReadUInt32LittleEndian(); + obj.StreamSize = data.ReadUInt64LittleEndian(); + + return obj; + } + + /// + /// Parse a Stream into a FileHeader + /// + /// Stream to parse + /// Filled FileHeader on success, null on error + private static FileHeader ParseFileHeader(Stream data) + { + var obj = new FileHeader(); + + obj.Signature = data.ReadUInt64LittleEndian(); + obj.CLSID = data.ReadGuid(); + obj.MinorVersion = data.ReadUInt16LittleEndian(); + obj.MajorVersion = data.ReadUInt16LittleEndian(); + obj.ByteOrder = data.ReadUInt16LittleEndian(); + obj.SectorShift = data.ReadUInt16LittleEndian(); + obj.MiniSectorShift = data.ReadUInt16LittleEndian(); + obj.Reserved = data.ReadBytes(6); + obj.NumberOfDirectorySectors = data.ReadUInt32LittleEndian(); + obj.NumberOfFATSectors = data.ReadUInt32LittleEndian(); + obj.FirstDirectorySectorLocation = data.ReadUInt32LittleEndian(); + obj.TransactionSignatureNumber = data.ReadUInt32LittleEndian(); + obj.MiniStreamCutoffSize = data.ReadUInt32LittleEndian(); + obj.FirstMiniFATSectorLocation = data.ReadUInt32LittleEndian(); + obj.NumberOfMiniFATSectors = data.ReadUInt32LittleEndian(); + obj.FirstDIFATSectorLocation = data.ReadUInt32LittleEndian(); + obj.NumberOfDIFATSectors = data.ReadUInt32LittleEndian(); + obj.DIFAT = new SectorNumber[109]; + for (int i = 0; i < 109; i++) + { + obj.DIFAT[i] = (SectorNumber)data.ReadUInt32LittleEndian(); + } + + // Skip rest of sector for version 4 + if (obj.MajorVersion == 4) + _ = data.ReadBytes(3584); + + return obj; + } + + /// + /// Parse a Stream into a sector full of sector numbers + /// + /// Stream to parse + /// Sector shift from the header + /// Filled sector full of sector numbers on success, null on error + private static SectorNumber[] ParseSectorNumbers(Stream data, ushort sectorShift) + { + int sectorCount = (int)(Math.Pow(2, sectorShift) / sizeof(uint)); + var sectorNumbers = new SectorNumber[sectorCount]; + + for (int i = 0; i < sectorNumbers.Length; i++) + { + sectorNumbers[i] = (SectorNumber)data.ReadUInt32LittleEndian(); + } + + return sectorNumbers; + } + + /// + /// Parse a Stream into a sector full of directory entries + /// + /// Stream to parse + /// Sector shift from the header + /// Major version from the header + /// Filled sector full of directory entries on success, null on error + private static DirectoryEntry[]? ParseDirectoryEntries(Stream data, ushort sectorShift, ushort majorVersion) + { + // + int directoryEntrySize = 128; + + int dirsPerSector = (int)(Math.Pow(2, sectorShift) / directoryEntrySize); + var directoryEntries = new DirectoryEntry[dirsPerSector]; + + for (int i = 0; i < directoryEntries.Length; i++) + { + var directoryEntry = ParseDirectoryEntry(data); + + // Handle version 3 entries + if (majorVersion == 3) + directoryEntry.StreamSize &= 0x0000FFFF; + + directoryEntries[i] = directoryEntry; + } + + return directoryEntries; + } + + /// + /// Get the ordered FAT sector chain for a given starting sector + /// + /// Initial FAT sector + /// Ordered list of sector numbers, null on error + private static List? GetFATSectorChain(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber? startingSector) + { + // If we have an invalid sector + if (startingSector == null || startingSector < 0 || cfb.Model.FATSectorNumbers == null || (long)startingSector >= cfb.Model.FATSectorNumbers.Length) + return null; + + // Setup the returned list + var sectors = new List { startingSector.Value }; + + var lastSector = startingSector; + while (true) + { + if (lastSector == null) + break; + + // Get the next sector from the lookup table + var nextSector = cfb.Model.FATSectorNumbers[(uint)lastSector!.Value]; + + // If we have an invalid sector + if (nextSector >= SectorNumber.MAXREGSECT) + break; + + // Add the next sector to the list and replace the last sector + sectors.Add(nextSector); + lastSector = nextSector; + } + + return sectors; + } + + /// + /// Get the data for the FAT sector chain starting at a given starting sector + /// + /// Initial FAT sector + /// Ordered list of sector numbers, null on error + private static byte[]? GetFATSectorChainData(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber startingSector) + { + // Get the sector chain first + var sectorChain = GetFATSectorChain(cfb, startingSector); + if (sectorChain == null) + return null; + + // Sequentially read the sectors + var data = new List(); + for (int i = 0; i < sectorChain.Count; i++) + { + // Try to get the sector data offset + int sectorDataOffset = (int)FATSectorToFileOffset(cfb, sectorChain[i]); + if (sectorDataOffset < 0 || sectorDataOffset >= cfb.GetEndOfFile()) + return null; + + // Try to read the sector data + var sectorData = cfb.ReadFromDataSource(sectorDataOffset, (int)cfb.SectorSize); + if (sectorData == null) + return null; + + // Add the sector data to the output + data.AddRange(sectorData); + } + + return [.. data]; + } + + /// + /// Convert a FAT sector value to a byte offset + /// + /// Sector to convert + /// File offset in bytes, -1 on error + private static long FATSectorToFileOffset(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber? sector) + { + // If we have an invalid sector number + if (sector == null || sector > SectorNumber.MAXREGSECT) + return -1; + + // Convert based on the sector shift value + return (long)(sector + 1) * cfb.SectorSize; + } + + /// + /// Get the ordered Mini FAT sector chain for a given starting sector + /// + /// Initial Mini FAT sector + /// Ordered list of sector numbers, null on error + private static List? GetMiniFATSectorChain(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber? startingSector) + { + // If we have an invalid sector + if (startingSector == null || startingSector < 0 || cfb.Model.MiniFATSectorNumbers == null || (long)startingSector >= cfb.Model.MiniFATSectorNumbers.Length) + return null; + + // Setup the returned list + var sectors = new List { startingSector.Value }; + + var lastSector = startingSector; + while (true) + { + if (lastSector == null) + break; + + // Get the next sector from the lookup table + var nextSector = cfb.Model.MiniFATSectorNumbers[(uint)lastSector!.Value]; + + // If we have an invalid sector + if (nextSector >= SectorNumber.MAXREGSECT) + break; + + // Add the next sector to the list and replace the last sector + sectors.Add(nextSector); + lastSector = nextSector; + } + + return sectors; + } + + /// + /// Get the data for the Mini FAT sector chain starting at a given starting sector + /// + /// Initial Mini FAT sector + /// Ordered list of sector numbers, null on error + private static byte[]? GetMiniFATSectorChainData(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber startingSector) + { + // Ignore invalid data + if (cfb.Model?.Header == null || cfb.Model.DirectoryEntries == null || cfb.Model.DirectoryEntries.Length == 0) + return null; + + // Get the mini stream offset + uint miniStreamSectorLocation = cfb.Model.DirectoryEntries[0].StartingSectorLocation; + + // Get the mini stream data + var miniStreamData = GetFATSectorChainData(cfb, (SectorNumber)miniStreamSectorLocation); + if (miniStreamData == null) + return null; + + // Get the sector chain + var sectorChain = GetMiniFATSectorChain(cfb, startingSector); + if (sectorChain == null) + return null; + + // Sequentially read the sectors + var data = new List(); + for (int i = 0; i < sectorChain.Count; i++) + { + // Try to get the mini stream data offset + int streamDataOffset = (int)MiniFATSectorToMiniStreamOffset(cfb, sectorChain[i]); + if (streamDataOffset < 0 || streamDataOffset > miniStreamData.Length) + return null; + + // Try to read the sector data + var sectorData = miniStreamData.ReadBytes(ref streamDataOffset, (int)cfb.MiniSectorSize); + if (sectorData == null) + return null; + + // Add the sector data to the output + data.AddRange(sectorData); + } + + return [.. data]; + } + + /// + /// Convert a Mini FAT sector value to a byte offset + /// + /// Sector to convert + /// Stream offset in bytes, -1 on error + /// Offset is within the mini stream, not the full file + private static long MiniFATSectorToMiniStreamOffset(SabreTools.Serialization.Wrappers.CFB cfb, SectorNumber? sector) + { + // If we have an invalid sector number + if (sector == null || sector > SectorNumber.MAXREGSECT) + return -1; + + // Get the mini stream location + return (long)sector * cfb.MiniSectorSize; + } + + #endregion } } diff --git a/README.md b/README.md index 81790748..4bf11b32 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ The following non-project libraries (or ports thereof) are used for file handlin - [LessIO](https://github.com/activescott/LessIO) - Used by libmspack4n for IO handling - [libmspack4n](https://github.com/activescott/libmspack4n) MS-CAB extraction [Unused in .NET Framework 2.0/3.5, non-Windows, and non-x86 builds due to Windows-specific libraries] -- [OpenMcdf](https://github.com/ironfede/openmcdf) - MSI extraction - [SharpCompress](https://github.com/adamhathcock/sharpcompress) - Common archive format extraction - [StormLibSharp](https://github.com/robpaveza/stormlibsharp) - MoPaQ extraction [Unused in .NET Framework 2.0/3.5/4.0, non-Windows, and non-x86 builds due to Windows-specific libraries] - [UnshieldSharp](https://github.com/mnadareski/UnshieldSharp) - InstallShield CAB extraction @@ -40,7 +39,6 @@ Binary Object Scanner strives to have both full compatibility for scanning acros - **bzip2 archive** - Extraction is only supported on .NET Framework 4.6.2 and higher due to `SharpCompress` support limitations - **gzip archive** - Extraction is only supported on .NET Framework 4.6.2 and higher due to `SharpCompress` support limitations - **MS-CAB** - Extraction is only supported in Windows x86 builds running .NET Framework 4.5.2 and higher due to native DLL requirements -- **MSI** - Extraction is only supported on .NET Framework 4.0 and higher due to `OpenMcdf` support limitations - **MoPaQ** - Extraction is only supported in Windows x86 builds running .NET Framework 4.5.2 and higher due to native DLL requirements - **PKZIP and derived files (ZIP, etc.)** - Extraction is only supported on .NET Framework 4.6.2 and higher - **RAR archive** - Extraction is only supported on .NET Framework 4.6.2 and higher due to `SharpCompress` support limitations @@ -202,7 +200,7 @@ Below is a list of container formats that are supported in some way: | BD+ SVM | Yes | Yes | N/A | | | BFPK custom archive format | Yes | Yes | Yes | | | bzip2 archive | No | Yes | Yes | Via `SharpCompress` | -| Compound File Binary (CFB) | Yes* | Yes | Yes | Via `OpenMcdf`, only CFB common pieces printable | +| Compound File Binary (CFB) | Yes* | Yes | Yes | Only CFB common pieces printable | | gzip archive | No | Yes | Yes | Via `SharpCompress` | | Half-Life Game Cache File (GCF) | Yes | Yes | Yes | | | Half-Life Level (BSP) | Yes | Yes | Yes | |