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);