Fix most CFB extraction issues, remove OpenMcdf

This commit is contained in:
Matt Nadareski
2025-07-30 11:30:33 -04:00
parent 892fd945bb
commit 589214ea38
3 changed files with 653 additions and 69 deletions

View File

@@ -73,7 +73,6 @@
<ItemGroup>
<PackageReference Include="MinAsyncBridge" Version="0.12.4" Condition="$(TargetFramework.StartsWith(`net2`)) OR $(TargetFramework.StartsWith(`net3`)) OR $(TargetFramework.StartsWith(`net40`))" />
<PackageReference Include="Net35.Actions" Version="1.4.0" Condition="$(TargetFramework.StartsWith(`net2`))" />
<PackageReference Include="OpenMcdf" Version="2.4.1" Condition="!$(TargetFramework.StartsWith(`net2`)) AND !$(TargetFramework.StartsWith(`net3`))" />
<PackageReference Include="SabreTools.Compression" Version="0.6.3" />
<PackageReference Include="SabreTools.Hashing" Version="1.4.2" />
<PackageReference Include="SabreTools.IO" Version="1.6.3" />

View File

@@ -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
/// <inheritdoc/>
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;
}
/// <summary>
/// Extract all files from the CFB to an output directory
/// </summary>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if all files extracted, false otherwise</returns>
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;
}
/// <summary>
/// Extract a file from the CFB to an output directory by index
/// </summary>
/// <param name="index">Entry index to extract</param>
/// <param name="outputDirectory">Output directory to write to</param>
/// <returns>True if the file extracted, false otherwise</returns>
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;
}
/// <summary>
/// Read the entry data for a single directory entry, if possible
/// </summary>
/// <param name="entry">Entry to try to retrieve data for</param>
/// <returns>Byte array representing the entry data on success, null otherwise</returns>
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;
}
/// <remarks>Adapted from LibMSI</remarks>
@@ -162,5 +229,525 @@ namespace BinaryObjectScanner.FileType
return '.';
return '_';
}
#region REMOVE WHEN SERIALIZATION UPDATED
/// <inheritdoc/>
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<SectorNumber>();
// 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<SectorNumber>();
// 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<SectorNumber>();
// 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<DirectoryEntry>();
// 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;
}
}
/// <summary>
/// Parse a Stream into a DirectoryEntry
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled DirectoryEntry on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a FileHeader
/// </summary>
/// <param name="data">Stream to parse</param>
/// <returns>Filled FileHeader on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a sector full of sector numbers
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorShift">Sector shift from the header</param>
/// <returns>Filled sector full of sector numbers on success, null on error</returns>
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;
}
/// <summary>
/// Parse a Stream into a sector full of directory entries
/// </summary>
/// <param name="data">Stream to parse</param>
/// <param name="sectorShift">Sector shift from the header</param>
/// <param name="majorVersion">Major version from the header</param>
/// <returns>Filled sector full of directory entries on success, null on error</returns>
private static DirectoryEntry[]? ParseDirectoryEntries(Stream data, ushort sectorShift, ushort majorVersion)
{
// <see href="https://winprotocoldoc.z19.web.core.windows.net/MS-CFB/%5bMS-CFB%5d.pdf"/>
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;
}
/// <summary>
/// Get the ordered FAT sector chain for a given starting sector
/// </summary>
/// <param name="startingSector">Initial FAT sector</param>
/// <returns>Ordered list of sector numbers, null on error</returns>
private static List<SectorNumber>? 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<SectorNumber> { 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;
}
/// <summary>
/// Get the data for the FAT sector chain starting at a given starting sector
/// </summary>
/// <param name="startingSector">Initial FAT sector</param>
/// <returns>Ordered list of sector numbers, null on error</returns>
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<byte>();
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];
}
/// <summary>
/// Convert a FAT sector value to a byte offset
/// </summary>
/// <param name="sector">Sector to convert</param>
/// <returns>File offset in bytes, -1 on error</returns>
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;
}
/// <summary>
/// Get the ordered Mini FAT sector chain for a given starting sector
/// </summary>
/// <param name="startingSector">Initial Mini FAT sector</param>
/// <returns>Ordered list of sector numbers, null on error</returns>
private static List<SectorNumber>? 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<SectorNumber> { 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;
}
/// <summary>
/// Get the data for the Mini FAT sector chain starting at a given starting sector
/// </summary>
/// <param name="startingSector">Initial Mini FAT sector</param>
/// <returns>Ordered list of sector numbers, null on error</returns>
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<byte>();
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];
}
/// <summary>
/// Convert a Mini FAT sector value to a byte offset
/// </summary>
/// <param name="sector">Sector to convert</param>
/// <returns>Stream offset in bytes, -1 on error</returns>
/// <remarks>Offset is within the mini stream, not the full file</remarks>
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
}
}

View File

@@ -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 | |