diff --git a/BinaryObjectScanner/FileType/MicrosoftCAB.cs b/BinaryObjectScanner/FileType/MicrosoftCAB.cs
index fc33f6e3..fda63a84 100644
--- a/BinaryObjectScanner/FileType/MicrosoftCAB.cs
+++ b/BinaryObjectScanner/FileType/MicrosoftCAB.cs
@@ -31,69 +31,30 @@ namespace BinaryObjectScanner.FileType
public bool Extract(Stream? stream, string file, string outDir, bool includeDebug)
{
#if NET20 || NET35 || !WINX86
- // TODO: Use MicrosoftCabinet.OpenSet when Serialization is updated
+ // TODO: Remove once Serialization is updated
+
+ // Get a wrapper for the set
+ var current = OpenSet(file);
+ if (current?.Model?.Header == null)
+ return false;
+
try
{
- // Create the wrapper
- var cabArchive = SabreTools.Serialization.Wrappers.MicrosoftCabinet.Create(stream);
- if (cabArchive?.Model?.Folders == null || cabArchive.Model.Folders.Length == 0)
- return false;
-
- // Loop through the folders
- for (int f = 0; f < cabArchive!.Model.Folders.Length; f++)
+ // Loop through the cabinets
+ do
{
- // Get the current folder for processing
- var folder = cabArchive.Model.Folders[f];
-
- // Decompress the blocks, if possible
- using var blockStream = DecompressBlocks(cabArchive, file, folder, f);
- if (blockStream == null || blockStream.Length == 0)
- continue;
-
- // Ensure files
- var files = GetFiles(cabArchive, f);
- if (files.Length == 0)
- continue;
-
- // Loop through the files
- for (int i = 0; i < files.Length; i++)
- {
- try
- {
- var compressedFile = files[i];
- blockStream.Seek(compressedFile.FolderStartOffset, SeekOrigin.Begin);
- byte[] fileData = blockStream.ReadBytes((int)compressedFile.FileSize);
-
- // Ensure directory separators are consistent
- string fileName = compressedFile.Name!;
- if (Path.DirectorySeparatorChar == '\\')
- fileName = fileName.Replace('/', '\\');
- else if (Path.DirectorySeparatorChar == '/')
- fileName = fileName.Replace('\\', '/');
-
- string tempFile = Path.Combine(outDir, fileName);
- var directoryName = Path.GetDirectoryName(tempFile);
- if (directoryName != null && !Directory.Exists(directoryName))
- Directory.CreateDirectory(directoryName);
-
- using var of = File.OpenWrite(tempFile);
- of.Write(fileData, 0, fileData.Length);
- of.Flush();
- }
- catch (Exception ex)
- {
- if (includeDebug) Console.WriteLine(ex);
- }
- }
+ ExtractCabinet(current, file, outDir, forwardOnly: true, includeDebug);
+ current = OpenNext(current, file);
}
-
- return true;
+ while (current?.Model?.Header != null);
}
catch (Exception ex)
{
if (includeDebug) Console.WriteLine(ex);
return false;
}
+
+ return true;
#else
try
{
@@ -136,19 +97,159 @@ namespace BinaryObjectScanner.FileType
#endif
}
+ ///
+ /// Open a cabinet set for reading, if possible
+ ///
+ /// Filename for one cabinet in the set
+ /// Wrapper representing the set, null on error
+ /// TODO: Remove once Serialization is updated
+ private static SabreTools.Serialization.Wrappers.MicrosoftCabinet? OpenSet(string? filename)
+ {
+ // If the file is invalid
+ if (string.IsNullOrEmpty(filename))
+ return null;
+ else if (!File.Exists(filename!))
+ return null;
+
+ // Get the full file path and directory
+ filename = Path.GetFullPath(filename);
+
+ // Read in the current file and try to parse
+ var stream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ var current = SabreTools.Serialization.Wrappers.MicrosoftCabinet.Create(stream);
+ if (current?.Model?.Header == null)
+ return null;
+
+ // Seek to the first part of the cabinet set
+ while (current.Model.Header?.CabinetPrev != null)
+ {
+ // Attempt to open the previous cabinet
+ var prev = OpenPrevious(current, filename);
+ if (prev?.Model?.Header == null)
+ break;
+
+ // Assign previous as new current
+ current = prev;
+ }
+
+ // Return the start of the set
+ return current;
+ }
+
+ ///
+ /// Extract a cabinet file to an output directory, if possible
+ ///
+ /// Filename for one cabinet in the set, if available
+ /// Path to the output directory
+ /// True to include debug data, false otherwise
+ /// Indicates if decompression should be done forward-only
+ /// Indicates if all files were able to be extracted
+ /// TODO: Remove once Serialization is updated
+ private static bool ExtractCabinet(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename, string outDir, bool forwardOnly, bool includeDebug)
+ {
+ // If the archive is invalid
+ if (cabArchive?.Model?.Folders == null || cabArchive.Model.Folders.Length == 0)
+ return false;
+
+ try
+ {
+ // Loop through the folders
+ for (int f = 0; f < cabArchive.Model.Folders.Length; f++)
+ {
+ var folder = cabArchive.Model.Folders[f];
+ ExtractFolder(cabArchive, filename, outDir, folder, f, forwardOnly, includeDebug);
+ }
+
+ return true;
+ }
+ catch (Exception ex)
+ {
+ if (includeDebug) Console.WriteLine(ex);
+ return false;
+ }
+ }
+
+ ///
+ /// Extract the contents of a single folder
+ ///
+ /// Filename for one cabinet in the set, if available
+ /// Path to the output directory
+ /// Folder containing the blocks to decompress
+ /// Index of the folder in the cabinet
+ /// Indicates if decompression should be done forward-only
+ /// True to include debug data, false otherwise
+ /// TODO: Remove once Serialization is updated
+ private static void ExtractFolder(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive,
+ string? filename,
+ string outDir,
+ CFFOLDER? folder,
+ int folderIndex,
+ bool forwardOnly,
+ bool includeDebug)
+ {
+ // Decompress the blocks, if possible
+ using var blockStream = DecompressBlocks(cabArchive, filename, folder, folderIndex, forwardOnly);
+ if (blockStream == null || blockStream.Length == 0)
+ return;
+
+ // Ensure files
+ var files = GetFiles(cabArchive, folderIndex);
+ if (files.Length == 0)
+ return;
+
+ // Loop through the files
+ for (int i = 0; i < files.Length; i++)
+ {
+ try
+ {
+ var compressedFile = files[i];
+ blockStream.Seek(compressedFile.FolderStartOffset, SeekOrigin.Begin);
+ byte[] fileData = blockStream.ReadBytes((int)compressedFile.FileSize);
+
+ // Ensure directory separators are consistent
+ string fileName = compressedFile.Name!;
+ if (Path.DirectorySeparatorChar == '\\')
+ fileName = fileName.Replace('/', '\\');
+ else if (Path.DirectorySeparatorChar == '/')
+ fileName = fileName.Replace('\\', '/');
+
+ string tempFile = Path.Combine(outDir, fileName);
+ var directoryName = Path.GetDirectoryName(tempFile);
+ if (directoryName != null && !Directory.Exists(directoryName))
+ Directory.CreateDirectory(directoryName);
+
+ using var of = File.OpenWrite(tempFile);
+ of.Write(fileData, 0, fileData.Length);
+ of.Flush();
+ }
+ catch (Exception ex)
+ {
+ if (includeDebug) Console.WriteLine(ex);
+ }
+ }
+ }
+
///
/// Decompress all blocks for a folder
///
- private Stream? DecompressBlocks(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string file, CFFOLDER? folder, int folderIndex)
+ /// Filename for one cabinet in the set, if available
+ /// Folder containing the blocks to decompress
+ /// Index of the folder in the cabinet
+ /// Indicates if decompression should be done forward-only
+ /// Stream representing the decompressed data on success, null otherwise
+ /// TODO: Remove once Serialization is updated
+ private static Stream? DecompressBlocks(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename, CFFOLDER? folder, int folderIndex, bool forwardOnly)
{
// Ensure data blocks
- var dataBlocks = GetDataBlocks(cabArchive, file, folder, folderIndex);
+ var dataBlocks = forwardOnly
+ ? GetDataBlocksForward(cabArchive, filename, folder, folderIndex)
+ : GetDataBlocks(cabArchive, filename, folder, folderIndex);
if (dataBlocks == null || dataBlocks.Length == 0)
return null;
// Setup decompressors
var mszip = SabreTools.Compression.MSZIP.Decompressor.Create();
- uint quantumWindowBits = (uint)(((ushort)folder!.CompressionType >> 8) & 0x1f);
+ //uint quantumWindowBits = (uint)(((ushort)folder.CompressionType >> 8) & 0x1f);
// Loop through the data blocks
var ms = new MemoryStream();
@@ -158,65 +259,77 @@ namespace BinaryObjectScanner.FileType
if (db?.CompressedData == null)
continue;
- // Uncompressed data
- if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_NONE)
+ // Get the compression type
+ var compressionType = GetCompressionType(folder!);
+ switch (compressionType)
{
- ms.Write(db.CompressedData, 0, db.CompressedData.Length);
- ms.Flush();
- }
+ // Uncompressed data
+ case CompressionType.TYPE_NONE:
+ ms.Write(db.CompressedData, 0, db.CompressedData.Length);
+ ms.Flush();
+ break;
- // MS-ZIP
- else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_MSZIP)
- {
- long position = ms.Position;
- mszip.CopyTo(db.CompressedData, ms);
- long decompressedSize = ms.Position - position;
+ // MS-ZIP
+ case CompressionType.TYPE_MSZIP:
+ long preMsZipPosition = ms.Position;
+ mszip.CopyTo(db.CompressedData, ms);
+ long msZipDecompressedSize = ms.Position - preMsZipPosition;
- // Pad to the correct size but throw a warning about this
- if (decompressedSize < db.UncompressedSize)
- {
- Console.Error.WriteLine($"Data block {i} in folder {folderIndex} had mismatching sizes. Expected: {db.UncompressedSize}, Got: {decompressedSize}");
- byte[] padding = new byte[db.UncompressedSize - decompressedSize];
- ms.Write(padding, 0, padding.Length);
- }
- }
+ // Pad to the correct size but throw a warning about this
+ if (msZipDecompressedSize < db.UncompressedSize)
+ {
+ Console.Error.WriteLine($"Data block {i} in folder {folderIndex} had mismatching sizes. Expected: {db.UncompressedSize}, Got: {msZipDecompressedSize}");
+ byte[] padding = new byte[db.UncompressedSize - msZipDecompressedSize];
+ ms.Write(padding, 0, padding.Length);
+ }
- // Quantum
- else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_QUANTUM)
- {
- var quantum = SabreTools.Compression.Quantum.Decompressor.Create(db.CompressedData, quantumWindowBits);
- byte[] data = quantum.Process();
- ms.Write(data, 0, data.Length);
- ms.Flush();
- }
+ break;
- // LZX
- else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_LZX)
- {
- // TODO: Unsupported
- continue;
- }
+ // Quantum
+ case CompressionType.TYPE_QUANTUM:
+ // TODO: Unsupported
+ break;
- // Unknown
- else
- {
- continue;
+ // LZX
+ case CompressionType.TYPE_LZX:
+ // TODO: Unsupported
+ break;
}
}
return ms;
}
+ ///
+ /// Get the unmasked compression type for a folder
+ ///
+ /// Folder to get the compression type for
+ /// Compression type on success, on error
+ private static CompressionType GetCompressionType(CFFOLDER folder)
+ {
+ if ((folder!.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_NONE)
+ return CompressionType.TYPE_NONE;
+ else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_MSZIP)
+ return CompressionType.TYPE_MSZIP;
+ else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_QUANTUM)
+ return CompressionType.TYPE_QUANTUM;
+ else if ((folder.CompressionType & CompressionType.MASK_TYPE) == CompressionType.TYPE_LZX)
+ return CompressionType.TYPE_LZX;
+ else
+ return (CompressionType)ushort.MaxValue;
+ }
+
///
/// Get the set of data blocks for a folder
///
+ /// Filename for one cabinet in the set, if available
+ /// Folder containing the blocks to decompress
+ /// Index of the folder in the cabinet
+ /// Indicates if previous cabinets should be ignored
+ /// Indicates if next cabinets should be ignored
+ /// Array of data blocks on success, null otherwise
/// TODO: Remove once Serialization is updated
- private CFDATA[]? GetDataBlocks(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive,
- string file,
- CFFOLDER? folder,
- int folderIndex,
- bool skipPrev = false,
- bool skipNext = false)
+ private static CFDATA[]? GetDataBlocks(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename, CFFOLDER? folder, int folderIndex, bool skipPrev = false, bool skipNext = false)
{
// Skip invalid folders
if (folder?.DataBlocks == null || folder.DataBlocks.Length == 0)
@@ -227,28 +340,34 @@ namespace BinaryObjectScanner.FileType
if (files.Length == 0)
return folder.DataBlocks;
- // Check if the folder spans backward
+ // Check if the folder spans in either direction
+ bool spanPrev = Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_FROM_PREV || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT);
+ bool spanNext = Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_TO_NEXT || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT);
+
+ // If the folder spans backward and Prev is not being skipped
CFDATA[] prevBlocks = [];
- if (!skipPrev && Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_FROM_PREV || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT))
+ if (!skipPrev && spanPrev)
{
- var prev = OpenPrevious(cabArchive, file);
+ // Get all blocks from Prev
+ var prev = OpenPrevious(cabArchive, filename);
if (prev?.Model?.Header != null && prev.Model.Folders != null)
{
int prevFolderIndex = prev.Model.Header.FolderCount;
var prevFolder = prev.Model.Folders[prevFolderIndex - 1];
- prevBlocks = GetDataBlocks(prev, file, prevFolder, prevFolderIndex, skipNext: true) ?? [];
+ prevBlocks = GetDataBlocks(prev, filename, prevFolder, prevFolderIndex, skipNext: true) ?? [];
}
}
- // Check if the folder spans forward
+ // If the folder spans forward and Next is not being skipped
CFDATA[] nextBlocks = [];
- if (!skipNext && Array.Exists(files, f => f.FolderIndex == FolderIndex.CONTINUED_TO_NEXT || f.FolderIndex == FolderIndex.CONTINUED_PREV_AND_NEXT))
+ if (!skipNext && spanNext)
{
- var next = OpenNext(cabArchive, file);
+ // Get all blocks from Prev
+ var next = OpenNext(cabArchive, filename);
if (next?.Model?.Header != null && next.Model.Folders != null)
{
var nextFolder = next.Model.Folders[0];
- nextBlocks = GetDataBlocks(next, file, nextFolder, 0, skipPrev: true) ?? [];
+ nextBlocks = GetDataBlocks(next, filename, nextFolder, 0, skipPrev: true) ?? [];
}
}
@@ -256,11 +375,22 @@ namespace BinaryObjectScanner.FileType
return [.. prevBlocks, .. folder.DataBlocks, .. nextBlocks];
}
+ ///
+ /// Get the set of data blocks for a folder using forward reading only
+ ///
+ /// Filename for one cabinet in the set, if available
+ /// Folder containing the blocks to decompress
+ /// Index of the folder in the cabinet
+ /// Array of data blocks on success, null otherwise
+ /// TODO: Remove once Serialization is updated
+ private static CFDATA[]? GetDataBlocksForward(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename, CFFOLDER? folder, int folderIndex)
+ => GetDataBlocks(cabArchive, filename, folder, folderIndex, skipPrev: true, skipNext: false);
+
///
/// Get all files for the current folder index
///
/// TODO: Remove once Serialization is updated
- private CFFILE[] GetFiles(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, int folderIndex)
+ private static CFFILE[] GetFiles(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, int folderIndex)
{
// Ignore invalid archives
if (cabArchive.Model.Files == null)
@@ -281,7 +411,7 @@ namespace BinaryObjectScanner.FileType
/// Get the corrected folder index
///
/// TODO: Remove once Serialization is updated
- private int GetFolderIndex(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, CFFILE file)
+ private static int GetFolderIndex(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, CFFILE file)
{
return file.FolderIndex switch
{
@@ -295,28 +425,26 @@ namespace BinaryObjectScanner.FileType
///
/// Open the next archive, if possible
///
+ /// Filename for one cabinet in the set
/// TODO: Remove once Serialization is updated
- private SabreTools.Serialization.Wrappers.MicrosoftCabinet? OpenNext(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string file)
+ private static SabreTools.Serialization.Wrappers.MicrosoftCabinet? OpenNext(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename)
{
// Ignore invalid archives
- if (cabArchive.Model.Header == null)
+ if (cabArchive.Model?.Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
- file = Path.GetFullPath(file);
+ filename = Path.GetFullPath(filename);
// Get if the cabinet has a next part
string? next = cabArchive.Model.Header.CabinetNext;
if (string.IsNullOrEmpty(next))
return null;
- // Get the full next path, if possible
- if (file != null)
- {
- string? folder = Path.GetDirectoryName(file);
- if (folder != null)
- next = Path.Combine(folder, next);
- }
+ // Get the full next path
+ string? folder = Path.GetDirectoryName(filename);
+ if (folder != null)
+ next = Path.Combine(folder, next);
// Open and return the next cabinet
var fs = File.Open(next, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
@@ -326,28 +454,26 @@ namespace BinaryObjectScanner.FileType
///
/// Open the previous archive, if possible
///
+ /// Filename for one cabinet in the set
/// TODO: Remove once Serialization is updated
- private SabreTools.Serialization.Wrappers.MicrosoftCabinet? OpenPrevious(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string file)
+ private static SabreTools.Serialization.Wrappers.MicrosoftCabinet? OpenPrevious(SabreTools.Serialization.Wrappers.MicrosoftCabinet cabArchive, string? filename)
{
// Ignore invalid archives
- if (cabArchive.Model.Header == null)
+ if (cabArchive.Model?.Header == null || string.IsNullOrEmpty(filename))
return null;
// Normalize the filename
- file = Path.GetFullPath(file);
+ filename = Path.GetFullPath(filename);
// Get if the cabinet has a previous part
string? prev = cabArchive.Model.Header.CabinetPrev;
if (string.IsNullOrEmpty(prev))
return null;
- // Get the full previous path, if possible
- if (file != null)
- {
- string? folder = Path.GetDirectoryName(file);
- if (folder != null)
- prev = Path.Combine(folder, prev);
- }
+ // Get the full next path
+ string? folder = Path.GetDirectoryName(filename);
+ if (folder != null)
+ prev = Path.Combine(folder, prev);
// Open and return the previous cabinet
var fs = File.Open(prev, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);