From c86b1251a36e9c2152a9f9abcb568759ea3c680a Mon Sep 17 00:00:00 2001 From: Matt Nadareski Date: Sat, 16 Sep 2023 23:09:07 -0400 Subject: [PATCH] Extraction can go in `FileType` --- BinaryObjectScanner.FileType/BFPK.cs | 97 +- BinaryObjectScanner.FileType/BSP.cs | 281 ++- BinaryObjectScanner.FileType/GCF.cs | 122 +- BinaryObjectScanner.FileType/MicrosoftCAB.cs | 191 +- BinaryObjectScanner.FileType/PAK.cs | 94 +- BinaryObjectScanner.FileType/PFF.cs | 79 +- BinaryObjectScanner.FileType/Quantum.cs | 162 ++ BinaryObjectScanner.FileType/SGA.cs | 244 ++- BinaryObjectScanner.FileType/VBSP.cs | 99 +- BinaryObjectScanner.FileType/VPK.cs | 152 +- BinaryObjectScanner.FileType/WAD.cs | 90 +- BinaryObjectScanner.FileType/XZP.cs | 104 +- BinaryObjectScanner.Wrappers/Extraction.cs | 1682 ------------------ Test/Extractor.cs | 62 +- 14 files changed, 1736 insertions(+), 1723 deletions(-) create mode 100644 BinaryObjectScanner.FileType/Quantum.cs delete mode 100644 BinaryObjectScanner.Wrappers/Extraction.cs diff --git a/BinaryObjectScanner.FileType/BFPK.cs b/BinaryObjectScanner.FileType/BFPK.cs index d5088e60..1615dbde 100644 --- a/BinaryObjectScanner.FileType/BFPK.cs +++ b/BinaryObjectScanner.FileType/BFPK.cs @@ -1,7 +1,8 @@ using System; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; +using SharpCompress.Compressors; +using SharpCompress.Compressors.Deflate; namespace BinaryObjectScanner.FileType { @@ -37,7 +38,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Extract all files - bfpk.ExtractAll(tempPath); + ExtractAll(bfpk, tempPath); return tempPath; } @@ -47,5 +48,97 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the BFPK to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.BFPK item, string outputDirectory) + { + // If we have no files + if (item.Model.Files == null || item.Model.Files.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Files.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the BFPK to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.BFPK item, int index, string outputDirectory) + { + // If we have no files + if (item.Model.Files == null || item.Model.Files.Length == 0) + return false; + + // If we have an invalid index + if (index < 0 || index >= item.Model.Files.Length) + return false; + + // Get the file information + var file = item.Model.Files[index]; + if (file == null) + return false; + + // Get the read index and length + int offset = file.Offset + 4; + int compressedSize = file.CompressedSize; + + // Some files can lack the length prefix + if (compressedSize > item.GetEndOfFile()) + { + offset -= 4; + compressedSize = file.UncompressedSize; + } + + try + { + // Ensure the output directory exists + Directory.CreateDirectory(outputDirectory); + + // Create the output path + string filePath = Path.Combine(outputDirectory, file.Name ?? $"file{index}"); + using (FileStream fs = File.OpenWrite(filePath)) + { + // Read the data block +#if NET48 + byte[] data = item.ReadFromDataSource(offset, compressedSize); +#else + byte[]? data = item.ReadFromDataSource(offset, compressedSize); +#endif + if (data == null) + return false; + + // If we have uncompressed data + if (compressedSize == file.UncompressedSize) + { + fs.Write(data, 0, compressedSize); + } + else + { + MemoryStream ms = new MemoryStream(data); + ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress); + zs.CopyTo(fs); + } + } + + return true; + } + catch + { + return false; + } + } } } diff --git a/BinaryObjectScanner.FileType/BSP.cs b/BinaryObjectScanner.FileType/BSP.cs index 7e709cd2..5c88f935 100644 --- a/BinaryObjectScanner.FileType/BSP.cs +++ b/BinaryObjectScanner.FileType/BSP.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,8 +37,8 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - bsp.ExtractAllLumps(tempPath); - bsp.ExtractAllTextures(tempPath); + ExtractAllLumps(bsp, tempPath); + ExtractAllTextures(bsp, tempPath); return tempPath; } @@ -48,5 +48,280 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all lumps from the BSP to an output directory + /// + /// Output directory to write to + /// True if all lumps extracted, false otherwise + public static bool ExtractAllLumps(SabreTools.Serialization.Wrappers.BSP item, string outputDirectory) + { + // If we have no lumps + if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Lumps.Length; i++) + { + allExtracted &= ExtractLump(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a lump from the BSP to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the lump extracted, false otherwise + public static bool ExtractLump(SabreTools.Serialization.Wrappers.BSP item, int index, string outputDirectory) + { + // If we have no lumps + if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) + return false; + + // If the lumps index is invalid + if (index < 0 || index >= item.Model.Lumps.Length) + return false; + + // Get the lump + var lump = item.Model.Lumps[index]; + if (lump == null) + return false; + + // Read the data +#if NET48 + byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#else + byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#endif + if (data == null) + return false; + + // Create the filename + string filename = $"lump_{index}.bin"; + switch (index) + { + case SabreTools.Models.BSP.Constants.HL_BSP_LUMP_ENTITIES: + filename = "entities.ent"; + break; + case SabreTools.Models.BSP.Constants.HL_BSP_LUMP_TEXTUREDATA: + filename = "texture_data.bin"; + break; + } + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } + + /// + /// Extract all textures from the BSP to an output directory + /// + /// Output directory to write to + /// True if all textures extracted, false otherwise + public static bool ExtractAllTextures(SabreTools.Serialization.Wrappers.BSP item, string outputDirectory) + { + // If we have no textures + if (item.Model.TextureHeader?.Offsets == null || item.Model.TextureHeader.Offsets.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.TextureHeader.Offsets.Length; i++) + { + allExtracted &= ExtractTexture(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a texture from the BSP to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the texture extracted, false otherwise + public static bool ExtractTexture(SabreTools.Serialization.Wrappers.BSP item, int index, string outputDirectory) + { + // If we have no textures + if (item.Model.Textures == null || item.Model.Textures.Length == 0) + return false; + + // If the texture index is invalid + if (index < 0 || index >= item.Model.Textures.Length) + return false; + + // Get the texture + var texture = item.Model.Textures[index]; + if (texture == null) + return false; + + // Read the data +#if NET48 + byte[] data = CreateTextureData(texture); +#else + byte[]? data = CreateTextureData(texture); +#endif + if (data == null) + return false; + + // Create the filename + string filename = $"{texture.Name}.bmp"; + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } + + /// + /// Create a bitmap from the texture and palette data + /// + /// Texture object to format + /// Byte array representing the texture as a bitmap +#if NET48 + private static byte[] CreateTextureData(SabreTools.Models.BSP.Texture texture) +#else + private static byte[]? CreateTextureData(SabreTools.Models.BSP.Texture texture) +#endif + { + // If there's no palette data + if (texture.PaletteData == null || texture.PaletteData.Length == 0) + return null; + + // If there's no texture data + if (texture.TextureData == null || texture.TextureData.Length == 0) + return null; + + // Create the bitmap file header + var fileHeader = new SabreTools.Models.BMP.BITMAPFILEHEADER() + { + Type = ('M' << 8) | 'B', + Size = 14 + 40 + (texture.PaletteSize * 4) + (texture.Width * texture.Height), + OffBits = 14 + 40 + (texture.PaletteSize * 4), + }; + + // Create the bitmap info header + var infoHeader = new SabreTools.Models.BMP.BITMAPINFOHEADER + { + Size = 40, + Width = (int)texture.Width, + Height = (int)texture.Height, + Planes = 1, + BitCount = 8, + SizeImage = 0, + ClrUsed = texture.PaletteSize, + ClrImportant = texture.PaletteSize, + }; + + // Reformat the palette data + byte[] paletteData = new byte[texture.PaletteSize * 4]; + for (uint i = 0; i < texture.PaletteSize; i++) + { + paletteData[i * 4 + 0] = texture.PaletteData[i * 3 + 2]; + paletteData[i * 4 + 1] = texture.PaletteData[i * 3 + 1]; + paletteData[i * 4 + 2] = texture.PaletteData[i * 3 + 0]; + paletteData[i * 4 + 3] = 0; + } + + // Reformat the pixel data + byte[] pixelData = new byte[texture.Width * texture.Height]; + for (uint i = 0; i < texture.Width; i++) + { + for (uint j = 0; j < texture.Height; j++) + { + pixelData[i + ((texture.Height - 1 - j) * texture.Width)] = texture.TextureData[i + j * texture.Width]; + } + } + + // Build the file data + List buffer = new List(); + + // Bitmap file header + buffer.AddRange(BitConverter.GetBytes(fileHeader.Type)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Size)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved1)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved2)); + buffer.AddRange(BitConverter.GetBytes(fileHeader.OffBits)); + + // Bitmap info header + buffer.AddRange(BitConverter.GetBytes(infoHeader.Size)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Width)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Height)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Planes)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.BitCount)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.Compression)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.SizeImage)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.XPelsPerMeter)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.YPelsPerMeter)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrUsed)); + buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrImportant)); + + // Palette data + buffer.AddRange(paletteData); + + // Pixel data + buffer.AddRange(pixelData); + + return buffer.ToArray(); + } } } diff --git a/BinaryObjectScanner.FileType/GCF.cs b/BinaryObjectScanner.FileType/GCF.cs index 7ec31e13..18681bdd 100644 --- a/BinaryObjectScanner.FileType/GCF.cs +++ b/BinaryObjectScanner.FileType/GCF.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +37,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - gcf.ExtractAll(tempPath); + ExtractAll(gcf, tempPath); return tempPath; } @@ -47,5 +47,123 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the GCF to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.GCF item, string outputDirectory) + { + // If we have no files + if (item.Files == null || item.Files.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Files.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the GCF to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.GCF item, int index, string outputDirectory) + { + // If we have no files + if (item.Files == null || item.Files.Length == 0 || item.DataBlockOffsets == null) + return false; + + // If the files index is invalid + if (index < 0 || index >= item.Files.Length) + return false; + + // Get the file + var file = item.Files[index]; + if (file?.BlockEntries == null || file.Size == 0) + return false; + + // If the file is encrypted -- TODO: Revisit later + if (file.Encrypted) + return false; + + // Get all data block offsets needed for extraction + var dataBlockOffsets = new List(); + for (int i = 0; i < file.BlockEntries.Length; i++) + { + var blockEntry = file.BlockEntries[i]; + if (blockEntry == null) + continue; + + uint dataBlockIndex = blockEntry.FirstDataBlockIndex; + long blockEntrySize = blockEntry.FileDataSize; + while (blockEntrySize > 0) + { + long dataBlockOffset = item.DataBlockOffsets[dataBlockIndex++]; + dataBlockOffsets.Add(dataBlockOffset); + blockEntrySize -= item.Model.DataBlockHeader?.BlockSize ?? 0; + } + } + + // Create the filename +#if NET48 + string filename = file.Path; +#else + string? filename = file.Path; +#endif + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + // Now read the data sequentially and write out while we have data left + long fileSize = file.Size; + for (int i = 0; i < dataBlockOffsets.Count; i++) + { + int readSize = (int)Math.Min(item.Model.DataBlockHeader?.BlockSize ?? 0, fileSize); +#if NET48 + byte[] data = item.ReadFromDataSource((int)dataBlockOffsets[i], readSize); +#else + byte[]? data = item.ReadFromDataSource((int)dataBlockOffsets[i], readSize); +#endif + if (data == null) + return false; + + fs.Write(data, 0, data.Length); + } + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.FileType/MicrosoftCAB.cs b/BinaryObjectScanner.FileType/MicrosoftCAB.cs index 01431d84..c5314ebf 100644 --- a/BinaryObjectScanner.FileType/MicrosoftCAB.cs +++ b/BinaryObjectScanner.FileType/MicrosoftCAB.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -42,7 +42,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // If entry extraction fails - bool success = cabFile.ExtractAll(tempPath); + bool success = ExtractAll(cabFile, tempPath); if (!success) return null; @@ -54,5 +54,192 @@ namespace BinaryObjectScanner.FileType return null; } } + + #region Folders + + /// + /// Get the uncompressed data associated with a folder + /// + /// Folder index to check + /// Byte array representing the data, null on error + /// All but uncompressed are unimplemented +#if NET48 + public static byte[] GetUncompressedData(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, int folderIndex) +#else + public static byte[]? GetUncompressedData(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, int folderIndex) +#endif + { + // If we have an invalid folder index + if (folderIndex < 0 || item.Model.Folders == null || folderIndex >= item.Model.Folders.Length) + return null; + + // Get the folder header + var folder = item.Model.Folders[folderIndex]; + if (folder == null) + return null; + + // If we have invalid data blocks + if (folder.DataBlocks == null || folder.DataBlocks.Length == 0) + return null; + + // Setup LZX decompression + var lzx = new Compression.LZX.State(); + Compression.LZX.Decompressor.Init(((ushort)folder.CompressionType >> 8) & 0x1f, lzx); + + // Setup MS-ZIP decompression + Compression.MSZIP.State mszip = new Compression.MSZIP.State(); + + // Setup Quantum decompression + var qtm = new Compression.Quantum.State(); + Compression.Quantum.Decompressor.InitState(qtm, folder); + + List data = new List(); + foreach (var dataBlock in folder.DataBlocks) + { + if (dataBlock == null) + continue; + +#if NET48 + byte[] decompressed = new byte[dataBlock.UncompressedSize]; +#else + byte[]? decompressed = new byte[dataBlock.UncompressedSize]; +#endif + switch (folder.CompressionType & SabreTools.Models.MicrosoftCabinet.CompressionType.MASK_TYPE) + { + case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_NONE: + decompressed = dataBlock.CompressedData; + break; + case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_MSZIP: + decompressed = new byte[SabreTools.Models.Compression.MSZIP.Constants.ZIPWSIZE]; + Compression.MSZIP.Decompressor.Decompress(mszip, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); + Array.Resize(ref decompressed, dataBlock.UncompressedSize); + break; + case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_QUANTUM: + Compression.Quantum.Decompressor.Decompress(qtm, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); + break; + case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_LZX: + Compression.LZX.Decompressor.Decompress(state: lzx, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); + break; + default: + return null; + } + + if (decompressed != null) + data.AddRange(decompressed); + } + + return data.ToArray(); + } + + #endregion + + #region Files + + /// + /// Extract all files from the MS-CAB to an output directory + /// + /// Output directory to write to + /// True if all filez extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, string outputDirectory) + { + // If we have no files + if (item.Model.Files == null || item.Model.Files.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Files.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the MS-CAB to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, int index, string outputDirectory) + { + // If we have an invalid file index + if (index < 0 || item.Model.Files == null || index >= item.Model.Files.Length) + return false; + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Ensure the directory exists + Directory.CreateDirectory(outputDirectory); + + // Get the file header + var file = item.Model.Files[index]; + if (file == null || file.FileSize == 0) + return false; + + // Create the output filename + string fileName = Path.Combine(outputDirectory, file.Name ?? $"file{index}"); + + // Get the file data, if possible +#if NET48 + byte[] fileData = GetFileData(item, index); +#else + byte[]? fileData = GetFileData(item, index); +#endif + if (fileData == null) + return false; + + // Write the file data + using (FileStream fs = File.OpenWrite(fileName)) + { + fs.Write(fileData, 0, fileData.Length); + } + + return true; + } + + /// + /// Get the uncompressed data associated with a file + /// + /// File index to check + /// Byte array representing the data, null on error +#if NET48 + public static byte[] GetFileData(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, int fileIndex) +#else + public static byte[]? GetFileData(SabreTools.Serialization.Wrappers.MicrosoftCabinet item, int fileIndex) +#endif + { + // If we have an invalid file index + if (fileIndex < 0 || item.Model.Files == null || fileIndex >= item.Model.Files.Length) + return null; + + // Get the file header + var file = item.Model.Files[fileIndex]; + if (file == null || file.FileSize == 0) + return null; + + // Get the parent folder data +#if NET48 + byte[] folderData = GetUncompressedData(item, (int)file.FolderIndex); +#else + byte[]? folderData = GetUncompressedData(item, (int)file.FolderIndex); +#endif + if (folderData == null || folderData.Length == 0) + return null; + + // Create the output file data + byte[] fileData = new byte[file.FileSize]; + if (folderData.Length < file.FolderStartOffset + file.FileSize) + return null; + + // Get the segment that represents this file + Array.Copy(folderData, file.FolderStartOffset, fileData, 0, file.FileSize); + return fileData; + } + + #endregion } } diff --git a/BinaryObjectScanner.FileType/PAK.cs b/BinaryObjectScanner.FileType/PAK.cs index 6a49b37f..458d3944 100644 --- a/BinaryObjectScanner.FileType/PAK.cs +++ b/BinaryObjectScanner.FileType/PAK.cs @@ -1,7 +1,6 @@ using System; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +36,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - pak.ExtractAll(tempPath); + ExtractAll(pak, tempPath); return tempPath; } @@ -47,5 +46,96 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the PAK to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.PAK item, string outputDirectory) + { + // If we have no directory items + if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.DirectoryItems.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the PAK to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.PAK item, int index, string outputDirectory) + { + // If we have no directory items + if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) + return false; + + // If the directory item index is invalid + if (index < 0 || index >= item.Model.DirectoryItems.Length) + return false; + + // Get the directory item + var directoryItem = item.Model.DirectoryItems[index]; + if (directoryItem == null) + return false; + + // Read the item data +#if NET48 + byte[] data = item.ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); +#else + byte[]? data = item.ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); +#endif + if (data == null) + return false; + + // Create the filename +#if NET48 + string filename = directoryItem.ItemName; +#else + string? filename = directoryItem.ItemName; +#endif + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.FileType/PFF.cs b/BinaryObjectScanner.FileType/PFF.cs index c7273f48..7515cc4f 100644 --- a/BinaryObjectScanner.FileType/PFF.cs +++ b/BinaryObjectScanner.FileType/PFF.cs @@ -1,7 +1,6 @@ using System; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +36,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Extract all files - pff.ExtractAll(tempPath); + ExtractAll(pff, tempPath); return tempPath; } @@ -47,5 +46,81 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all segments from the PFF to an output directory + /// + /// Output directory to write to + /// True if all segments extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.PFF item, string outputDirectory) + { + // If we have no segments + if (item.Model.Segments == null || item.Model.Segments.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Segments.Length; i++) + { + allExtracted &= ExtractSegment(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a segment from the PFF to an output directory by index + /// + /// Segment index to extract + /// Output directory to write to + /// True if the segment extracted, false otherwise + public static bool ExtractSegment(SabreTools.Serialization.Wrappers.PFF item, int index, string outputDirectory) + { + // If we have no segments + if (item.Model.Header?.NumberOfFiles == null || item.Model.Header.NumberOfFiles == 0 || item.Model.Segments == null || item.Model.Segments.Length == 0) + return false; + + // If we have an invalid index + if (index < 0 || index >= item.Model.Segments.Length) + return false; + + // Get the segment information + var file = item.Model.Segments[index]; + if (file == null) + return false; + + // Get the read index and length + int offset = (int)file.FileLocation; + int size = (int)file.FileSize; + + try + { + // Ensure the output directory exists + Directory.CreateDirectory(outputDirectory); + + // Create the output path + string filePath = Path.Combine(outputDirectory, file.FileName ?? $"file{index}"); + using (FileStream fs = File.OpenWrite(filePath)) + { + // Read the data block +#if NET48 + byte[] data = item.ReadFromDataSource(offset, size); +#else + byte[]? data = item.ReadFromDataSource(offset, size); +#endif + if (data == null) + return false; + + // Write the data -- TODO: Compressed data? + fs.Write(data, 0, size); + } + + return true; + } + catch + { + return false; + } + } } } diff --git a/BinaryObjectScanner.FileType/Quantum.cs b/BinaryObjectScanner.FileType/Quantum.cs new file mode 100644 index 00000000..6a7ece44 --- /dev/null +++ b/BinaryObjectScanner.FileType/Quantum.cs @@ -0,0 +1,162 @@ +using System; +using System.IO; +using BinaryObjectScanner.Interfaces; + +namespace BinaryObjectScanner.FileType +{ + /// + /// Quantum Archive + /// + public class Quantum : IExtractable + { + /// + public string Extract(string file, bool includeDebug) + { + if (!File.Exists(file)) + return null; + + using (var fs = File.Open(file, FileMode.Open, FileAccess.Read, FileShare.Read)) + { + return Extract(fs, file, includeDebug); + } + } + + /// + public string Extract(Stream stream, string file, bool includeDebug) + { + try + { + // Create the wrapper + SabreTools.Serialization.Wrappers.Quantum quantum = SabreTools.Serialization.Wrappers.Quantum.Create(stream); + if (quantum == null) + return null; + + // Create a temp output directory + string tempPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(tempPath); + + // Extract all files + ExtractAll(quantum, tempPath); + + return tempPath; + } + catch (Exception ex) + { + if (includeDebug) Console.WriteLine(ex.Message); + return null; + } + } + + /// + /// Extract all files from the Quantum archive to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.Quantum item, string outputDirectory) + { + // If we have no files + if (item.Model.FileList == null || item.Model.FileList.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.FileList.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the Quantum archive to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.Quantum item, int index, string outputDirectory) + { + // If we have no files + if (item.Model.Header == null || item.Model.Header.FileCount == 0 || item.Model.FileList == null || item.Model.FileList.Length == 0) + return false; + + // If we have an invalid index + if (index < 0 || index >= item.Model.FileList.Length) + return false; + + // Get the file information + var fileDescriptor = item.Model.FileList[index]; + + // Read the entire compressed data + int compressedDataOffset = (int)item.Model.CompressedDataOffset; + int compressedDataLength = item.GetEndOfFile() - compressedDataOffset; +#if NET48 + byte[] compressedData = item.ReadFromDataSource(compressedDataOffset, compressedDataLength); +#else + byte[]? compressedData = item.ReadFromDataSource(compressedDataOffset, compressedDataLength); +#endif + + // TODO: Figure out decompression + // - Single-file archives seem to work + // - Single-file archives with files that span a window boundary seem to work + // - The first files in each archive seem to work + return false; + + // // Setup the decompression state + // State state = new State(); + // Decompressor.InitState(state, TableSize, CompressionFlags); + + // // Decompress the entire array + // int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize); + // byte[] decompressedData = new byte[decompressedDataLength]; + // Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData); + + // // Read the data + // int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize); + // byte[] data = new byte[fileDescriptor.ExpandedFileSize]; + // Array.Copy(decompressedData, offset, data, 0, data.Length); + + // // Loop through all files before the current + // for (int i = 0; i < index; i++) + // { + // // Decompress the next block of data + // byte[] tempData = new byte[FileList[i].ExpandedFileSize]; + // int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData); + // compressedData = new ReadOnlySpan(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray(); + // } + + // // Read the data + // byte[] data = new byte[fileDescriptor.ExpandedFileSize]; + // _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data); + + // // Create the filename + // string filename = fileDescriptor.FileName; + + // // If we have an invalid output directory + // if (string.IsNullOrWhiteSpace(outputDirectory)) + // return false; + + // // Create the full output path + // filename = Path.Combine(outputDirectory, filename); + + // // Ensure the output directory is created + // Directory.CreateDirectory(Path.GetDirectoryName(filename)); + + // // Try to write the data + // try + // { + // // Open the output file for writing + // using (Stream fs = File.OpenWrite(filename)) + // { + // fs.Write(data, 0, data.Length); + // } + // } + // catch + // { + // return false; + // } + + return true; + } + } +} diff --git a/BinaryObjectScanner.FileType/SGA.cs b/BinaryObjectScanner.FileType/SGA.cs index 6ac18636..e80fafcd 100644 --- a/BinaryObjectScanner.FileType/SGA.cs +++ b/BinaryObjectScanner.FileType/SGA.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.IO; +using System.Linq; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; +using ICSharpCode.SharpZipLib.Zip.Compression; namespace BinaryObjectScanner.FileType { @@ -37,7 +39,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - sga.ExtractAll(tempPath); + ExtractAll(sga, tempPath); return tempPath; } @@ -47,5 +49,243 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the SGA to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.SGA item, string outputDirectory) + { + // Get the number of files + int filesLength; + switch (item.Model.Header?.MajorVersion) + { + case 4: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; + case 5: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; + case 6: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; + case 7: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?.Length ?? 0; break; + default: return false; + } + + // If we have no files + if (filesLength == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < filesLength; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the SGA to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.SGA item, int index, string outputDirectory) + { + // Get the number of files + int filesLength; + switch (item.Model.Header?.MajorVersion) + { + case 4: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; + case 5: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; + case 6: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; + case 7: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?.Length ?? 0; break; + default: return false; + } + + // If we have no files + if (filesLength == 0) + return false; + + // If the files index is invalid + if (index < 0 || index >= filesLength) + return false; + + // Get the files +#if NET48 + object file; +#else + object? file; +#endif + switch (item.Model.Header?.MajorVersion) + { + case 4: file = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?[index]; break; + case 5: file = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?[index]; break; + case 6: file = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?[index]; break; + case 7: file = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?[index]; break; + default: return false; + } + + if (file == null) + return false; + + // Create the filename +#if NET48 + string filename; +#else + string? filename; +#endif + switch (item.Model.Header?.MajorVersion) + { + case 4: + case 5: filename = (file as SabreTools.Models.SGA.File4)?.Name; break; + case 6: filename = (file as SabreTools.Models.SGA.File6)?.Name; break; + case 7: filename = (file as SabreTools.Models.SGA.File7)?.Name; break; + default: return false; + } + + // Loop through and get all parent directories +#if NET48 + var parentNames = new List { filename }; +#else + var parentNames = new List { filename }; +#endif + + // Get the parent directory +#if NET48 + object folder; +#else + object? folder; +#endif + switch (item.Model.Header?.MajorVersion) + { +#if NET48 + case 4: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 5: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 6: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 7: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; +#else + case 4: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 5: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 6: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; + case 7: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; +#endif + default: return false; + } + + // If we have a parent folder + if (folder != null) + { + switch (item.Model.Header?.MajorVersion) + { + case 4: parentNames.Add((folder as SabreTools.Models.SGA.Folder4)?.Name); break; + case 5: + case 6: + case 7: parentNames.Add((folder as SabreTools.Models.SGA.Folder5)?.Name); break; + default: return false; + } + } + + // TODO: Should the section name/alias be used in the path as well? + + // Reverse and assemble the filename + parentNames.Reverse(); + filename = Path.Combine(parentNames.Cast().ToArray()); + + // Get the file offset + long fileOffset; + switch (item.Model.Header?.MajorVersion) + { + case 4: + case 5: fileOffset = (file as SabreTools.Models.SGA.File4)?.Offset ?? 0; break; + case 6: fileOffset = (file as SabreTools.Models.SGA.File6)?.Offset ?? 0; break; + case 7: fileOffset = (file as SabreTools.Models.SGA.File7)?.Offset ?? 0; break; + default: return false; + } + + // Adjust the file offset + switch (item.Model.Header?.MajorVersion) + { + case 4: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; + case 5: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; + case 6: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; + case 7: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; + default: return false; + }; + + // Get the file sizes + long fileSize, outputFileSize; + switch (item.Model.Header?.MajorVersion) + { + case 4: + case 5: + fileSize = (file as SabreTools.Models.SGA.File4)?.SizeOnDisk ?? 0; + outputFileSize = (file as SabreTools.Models.SGA.File4)?.Size ?? 0; + break; + case 6: + fileSize = (file as SabreTools.Models.SGA.File6)?.SizeOnDisk ?? 0; + outputFileSize = (file as SabreTools.Models.SGA.File6)?.Size ?? 0; + break; + case 7: + fileSize = (file as SabreTools.Models.SGA.File7)?.SizeOnDisk ?? 0; + outputFileSize = (file as SabreTools.Models.SGA.File7)?.Size ?? 0; + break; + default: return false; + } + + // Read the compressed data directly +#if NET48 + byte[] compressedData = item.ReadFromDataSource((int)fileOffset, (int)fileSize); +#else + byte[]? compressedData = item.ReadFromDataSource((int)fileOffset, (int)fileSize); +#endif + if (compressedData == null) + return false; + + // If the compressed and uncompressed sizes match + byte[] data; + if (fileSize == outputFileSize) + { + data = compressedData; + } + else + { + // Decompress the data + data = new byte[outputFileSize]; + Inflater inflater = new Inflater(); + inflater.SetInput(compressedData); + inflater.Inflate(data); + } + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return false; + } } } diff --git a/BinaryObjectScanner.FileType/VBSP.cs b/BinaryObjectScanner.FileType/VBSP.cs index 19983e4e..8249ad3c 100644 --- a/BinaryObjectScanner.FileType/VBSP.cs +++ b/BinaryObjectScanner.FileType/VBSP.cs @@ -1,7 +1,6 @@ using System; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +36,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - vbsp.ExtractAllLumps(tempPath); + ExtractAllLumps(vbsp, tempPath); return tempPath; } @@ -47,5 +46,101 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all lumps from the VBSP to an output directory + /// + /// Output directory to write to + /// True if all lumps extracted, false otherwise + public static bool ExtractAllLumps(SabreTools.Serialization.Wrappers.VBSP item, string outputDirectory) + { + // If we have no lumps + if (item.Model.Header?.Lumps == null || item.Model.Header.Lumps.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Header.Lumps.Length; i++) + { + allExtracted &= ExtractLump(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a lump from the VBSP to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the lump extracted, false otherwise + public static bool ExtractLump(SabreTools.Serialization.Wrappers.VBSP item, int index, string outputDirectory) + { + // If we have no lumps + if (item.Model.Header?.Lumps == null || item.Model.Header.Lumps.Length == 0) + return false; + + // If the lumps index is invalid + if (index < 0 || index >= item.Model.Header.Lumps.Length) + return false; + + // Get the lump + var lump = item.Model.Header.Lumps[index]; + if (lump == null) + return false; + + // Read the data +#if NET48 + byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#else + byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#endif + if (data == null) + return false; + + // Create the filename + string filename = $"lump_{index}.bin"; + switch (index) + { + case SabreTools.Models.VBSP.Constants.HL_VBSP_LUMP_ENTITIES: + filename = "entities.ent"; + break; + case SabreTools.Models.VBSP.Constants.HL_VBSP_LUMP_PAKFILE: + filename = "pakfile.zip"; + break; + } + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.FileType/VPK.cs b/BinaryObjectScanner.FileType/VPK.cs index 79574f54..7b01ed0e 100644 --- a/BinaryObjectScanner.FileType/VPK.cs +++ b/BinaryObjectScanner.FileType/VPK.cs @@ -1,7 +1,8 @@ using System; using System.IO; +using System.Linq; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; +using SabreTools.IO; namespace BinaryObjectScanner.FileType { @@ -37,7 +38,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - vpk.ExtractAll(tempPath); + ExtractAll(vpk, tempPath); return tempPath; } @@ -47,5 +48,152 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the VPK to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.VPK item, string outputDirectory) + { + // If we have no directory items + if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.DirectoryItems.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the VPK to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.VPK item, int index, string outputDirectory) + { + // If we have no directory items + if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) + return false; + + // If the directory item index is invalid + if (index < 0 || index >= item.Model.DirectoryItems.Length) + return false; + + // Get the directory item + var directoryItem = item.Model.DirectoryItems[index]; + if (directoryItem?.DirectoryEntry == null) + return false; + + // If we have an item with no archive +#if NET48 + byte[] data; +#else + byte[]? data; +#endif + if (directoryItem.DirectoryEntry.ArchiveIndex == SabreTools.Models.VPK.Constants.HL_VPK_NO_ARCHIVE) + { + if (directoryItem.PreloadData == null) + return false; + + data = directoryItem.PreloadData; + } + else + { + // If we have invalid archives + if (item.ArchiveFilenames == null || item.ArchiveFilenames.Length == 0) + return false; + + // If we have an invalid index + if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= item.ArchiveFilenames.Length) + return false; + + // Get the archive filename + string archiveFileName = item.ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex]; + if (string.IsNullOrWhiteSpace(archiveFileName)) + return false; + + // If the archive doesn't exist + if (!File.Exists(archiveFileName)) + return false; + + // Try to open the archive +#if NET48 + Stream archiveStream = null; +#else + Stream? archiveStream = null; +#endif + try + { + // Open the archive + archiveStream = File.OpenRead(archiveFileName); + + // Seek to the data + archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin); + + // Read the directory item bytes + data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength); + } + catch + { + return false; + } + finally + { + archiveStream?.Close(); + } + + // If we have preload data, prepend it + if (data != null && directoryItem.PreloadData != null) + data = directoryItem.PreloadData.Concat(data).ToArray(); + } + + // If there is nothing to write out + if (data == null) + return false; + + // Create the filename + string filename = $"{directoryItem.Name}.{directoryItem.Extension}"; + if (!string.IsNullOrWhiteSpace(directoryItem.Path)) + filename = Path.Combine(directoryItem.Path, filename); + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.FileType/WAD.cs b/BinaryObjectScanner.FileType/WAD.cs index 9c72f6f4..728961bb 100644 --- a/BinaryObjectScanner.FileType/WAD.cs +++ b/BinaryObjectScanner.FileType/WAD.cs @@ -1,7 +1,6 @@ using System; using System.IO; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +36,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - wad.ExtractAllLumps(tempPath); + ExtractAllLumps(wad, tempPath); return tempPath; } @@ -47,5 +46,92 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all lumps from the WAD to an output directory + /// + /// Output directory to write to + /// True if all lumps extracted, false otherwise + public static bool ExtractAllLumps(SabreTools.Serialization.Wrappers.WAD item, string outputDirectory) + { + // If we have no lumps + if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) + return false; + + // Loop through and extract all lumps to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.Lumps.Length; i++) + { + allExtracted &= ExtractLump(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a lump from the WAD to an output directory by index + /// + /// Lump index to extract + /// Output directory to write to + /// True if the lump extracted, false otherwise + public static bool ExtractLump(SabreTools.Serialization.Wrappers.WAD item, int index, string outputDirectory) + { + // If we have no lumps + if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) + return false; + + // If the lumps index is invalid + if (index < 0 || index >= item.Model.Lumps.Length) + return false; + + // Get the lump + var lump = item.Model.Lumps[index]; + if (lump == null) + return false; + + // Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture) +#if NET48 + byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#else + byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); +#endif + if (data == null) + return false; + + // Create the filename + string filename = $"{lump.Name}.lmp"; + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.FileType/XZP.cs b/BinaryObjectScanner.FileType/XZP.cs index 49e675f5..f6d78286 100644 --- a/BinaryObjectScanner.FileType/XZP.cs +++ b/BinaryObjectScanner.FileType/XZP.cs @@ -1,7 +1,7 @@ using System; using System.IO; +using System.Linq; using BinaryObjectScanner.Interfaces; -using BinaryObjectScanner.Wrappers; namespace BinaryObjectScanner.FileType { @@ -37,7 +37,7 @@ namespace BinaryObjectScanner.FileType Directory.CreateDirectory(tempPath); // Loop through and extract all files - xzp.ExtractAll(tempPath); + ExtractAll(xzp, tempPath); return tempPath; } @@ -47,5 +47,105 @@ namespace BinaryObjectScanner.FileType return null; } } + + /// + /// Extract all files from the XZP to an output directory + /// + /// Output directory to write to + /// True if all files extracted, false otherwise + public static bool ExtractAll(SabreTools.Serialization.Wrappers.XZP item, string outputDirectory) + { + // If we have no directory entries + if (item.Model.DirectoryEntries == null || item.Model.DirectoryEntries.Length == 0) + return false; + + // Loop through and extract all files to the output + bool allExtracted = true; + for (int i = 0; i < item.Model.DirectoryEntries.Length; i++) + { + allExtracted &= ExtractFile(item, i, outputDirectory); + } + + return allExtracted; + } + + /// + /// Extract a file from the XZP to an output directory by index + /// + /// File index to extract + /// Output directory to write to + /// True if the file extracted, false otherwise + public static bool ExtractFile(SabreTools.Serialization.Wrappers.XZP item, int index, string outputDirectory) + { + // If we have no directory entries + if (item.Model.DirectoryEntries == null || item.Model.DirectoryEntries.Length == 0) + return false; + + // If we have no directory items + if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) + return false; + + // If the directory entry index is invalid + if (index < 0 || index >= item.Model.DirectoryEntries.Length) + return false; + + // Get the directory entry + var directoryEntry = item.Model.DirectoryEntries[index]; + if (directoryEntry == null) + return false; + + // Get the associated directory item + var directoryItem = item.Model.DirectoryItems.Where(di => di?.FileNameCRC == directoryEntry.FileNameCRC).FirstOrDefault(); + if (directoryItem == null) + return false; + + // Load the item data +#if NET48 + byte[] data = item.ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); +#else + byte[]? data = item.ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); +#endif + if (data == null) + return false; + + // Create the filename +#if NET48 + string filename = directoryItem.Name; +#else + string? filename = directoryItem.Name; +#endif + + // If we have an invalid output directory + if (string.IsNullOrWhiteSpace(outputDirectory)) + return false; + + // Create the full output path + filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); + + // Ensure the output directory is created +#if NET48 + string directoryName = Path.GetDirectoryName(filename); +#else + string? directoryName = Path.GetDirectoryName(filename); +#endif + if (directoryName != null) + Directory.CreateDirectory(directoryName); + + // Try to write the data + try + { + // Open the output file for writing + using (Stream fs = File.OpenWrite(filename)) + { + fs.Write(data, 0, data.Length); + } + } + catch + { + return false; + } + + return true; + } } } diff --git a/BinaryObjectScanner.Wrappers/Extraction.cs b/BinaryObjectScanner.Wrappers/Extraction.cs deleted file mode 100644 index 2aba7080..00000000 --- a/BinaryObjectScanner.Wrappers/Extraction.cs +++ /dev/null @@ -1,1682 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using ICSharpCode.SharpZipLib.Zip.Compression; -using SabreTools.IO; -using SabreTools.Serialization.Wrappers; -using SharpCompress.Compressors; -using SharpCompress.Compressors.Deflate; - -namespace BinaryObjectScanner.Wrappers -{ - public static class Extraction - { - #region BFPK - - /// - /// Extract all files from the BFPK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this BFPK item, string outputDirectory) - { - // If we have no files - if (item.Model.Files == null || item.Model.Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Files.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the BFPK to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this BFPK item, int index, string outputDirectory) - { - // If we have no files - if (item.Model.Files == null || item.Model.Files.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= item.Model.Files.Length) - return false; - - // Get the file information - var file = item.Model.Files[index]; - if (file == null) - return false; - - // Get the read index and length - int offset = file.Offset + 4; - int compressedSize = file.CompressedSize; - - // Some files can lack the length prefix - if (compressedSize > item.GetEndOfFile()) - { - offset -= 4; - compressedSize = file.UncompressedSize; - } - - try - { - // Ensure the output directory exists - Directory.CreateDirectory(outputDirectory); - - // Create the output path - string filePath = Path.Combine(outputDirectory, file.Name ?? $"file{index}"); - using (FileStream fs = File.OpenWrite(filePath)) - { - // Read the data block -#if NET48 - byte[] data = item.ReadFromDataSource(offset, compressedSize); -#else - byte[]? data = item.ReadFromDataSource(offset, compressedSize); -#endif - if (data == null) - return false; - - // If we have uncompressed data - if (compressedSize == file.UncompressedSize) - { - fs.Write(data, 0, compressedSize); - } - else - { - MemoryStream ms = new MemoryStream(data); - ZlibStream zs = new ZlibStream(ms, CompressionMode.Decompress); - zs.CopyTo(fs); - } - } - - return true; - } - catch - { - return false; - } - } - - #endregion - - #region BSP - - /// - /// Extract all lumps from the BSP to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public static bool ExtractAllLumps(this BSP item, string outputDirectory) - { - // If we have no lumps - if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Lumps.Length; i++) - { - allExtracted &= item.ExtractLump(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a lump from the BSP to an output directory by index - /// - /// Lump index to extract - /// Output directory to write to - /// True if the lump extracted, false otherwise - public static bool ExtractLump(this BSP item, int index, string outputDirectory) - { - // If we have no lumps - if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= item.Model.Lumps.Length) - return false; - - // Get the lump - var lump = item.Model.Lumps[index]; - if (lump == null) - return false; - - // Read the data -#if NET48 - byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"lump_{index}.bin"; - switch (index) - { - case SabreTools.Models.BSP.Constants.HL_BSP_LUMP_ENTITIES: - filename = "entities.ent"; - break; - case SabreTools.Models.BSP.Constants.HL_BSP_LUMP_TEXTUREDATA: - filename = "texture_data.bin"; - break; - } - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - /// - /// Extract all textures from the BSP to an output directory - /// - /// Output directory to write to - /// True if all textures extracted, false otherwise - public static bool ExtractAllTextures(this BSP item, string outputDirectory) - { - // If we have no textures - if (item.Model.TextureHeader?.Offsets == null || item.Model.TextureHeader.Offsets.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.TextureHeader.Offsets.Length; i++) - { - allExtracted &= item.ExtractTexture(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a texture from the BSP to an output directory by index - /// - /// Lump index to extract - /// Output directory to write to - /// True if the texture extracted, false otherwise - public static bool ExtractTexture(this BSP item, int index, string outputDirectory) - { - // If we have no textures - if (item.Model.Textures == null || item.Model.Textures.Length == 0) - return false; - - // If the texture index is invalid - if (index < 0 || index >= item.Model.Textures.Length) - return false; - - // Get the texture - var texture = item.Model.Textures[index]; - if (texture == null) - return false; - - // Read the data -#if NET48 - byte[] data = CreateTextureData(texture); -#else - byte[]? data = CreateTextureData(texture); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"{texture.Name}.bmp"; - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - /// - /// Create a bitmap from the texture and palette data - /// - /// Texture object to format - /// Byte array representing the texture as a bitmap -#if NET48 - private static byte[] CreateTextureData(SabreTools.Models.BSP.Texture texture) -#else - private static byte[]? CreateTextureData(SabreTools.Models.BSP.Texture texture) -#endif - { - // If there's no palette data - if (texture.PaletteData == null || texture.PaletteData.Length == 0) - return null; - - // If there's no texture data - if (texture.TextureData == null || texture.TextureData.Length == 0) - return null; - - // Create the bitmap file header - var fileHeader = new SabreTools.Models.BMP.BITMAPFILEHEADER() - { - Type = ('M' << 8) | 'B', - Size = 14 + 40 + (texture.PaletteSize * 4) + (texture.Width * texture.Height), - OffBits = 14 + 40 + (texture.PaletteSize * 4), - }; - - // Create the bitmap info header - var infoHeader = new SabreTools.Models.BMP.BITMAPINFOHEADER - { - Size = 40, - Width = (int)texture.Width, - Height = (int)texture.Height, - Planes = 1, - BitCount = 8, - SizeImage = 0, - ClrUsed = texture.PaletteSize, - ClrImportant = texture.PaletteSize, - }; - - // Reformat the palette data - byte[] paletteData = new byte[texture.PaletteSize * 4]; - for (uint i = 0; i < texture.PaletteSize; i++) - { - paletteData[i * 4 + 0] = texture.PaletteData[i * 3 + 2]; - paletteData[i * 4 + 1] = texture.PaletteData[i * 3 + 1]; - paletteData[i * 4 + 2] = texture.PaletteData[i * 3 + 0]; - paletteData[i * 4 + 3] = 0; - } - - // Reformat the pixel data - byte[] pixelData = new byte[texture.Width * texture.Height]; - for (uint i = 0; i < texture.Width; i++) - { - for (uint j = 0; j < texture.Height; j++) - { - pixelData[i + ((texture.Height - 1 - j) * texture.Width)] = texture.TextureData[i + j * texture.Width]; - } - } - - // Build the file data - List buffer = new List(); - - // Bitmap file header - buffer.AddRange(BitConverter.GetBytes(fileHeader.Type)); - buffer.AddRange(BitConverter.GetBytes(fileHeader.Size)); - buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved1)); - buffer.AddRange(BitConverter.GetBytes(fileHeader.Reserved2)); - buffer.AddRange(BitConverter.GetBytes(fileHeader.OffBits)); - - // Bitmap info header - buffer.AddRange(BitConverter.GetBytes(infoHeader.Size)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.Width)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.Height)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.Planes)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.BitCount)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.Compression)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.SizeImage)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.XPelsPerMeter)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.YPelsPerMeter)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrUsed)); - buffer.AddRange(BitConverter.GetBytes(infoHeader.ClrImportant)); - - // Palette data - buffer.AddRange(paletteData); - - // Pixel data - buffer.AddRange(pixelData); - - return buffer.ToArray(); - } - - #endregion - - #region GCF - - /// - /// Extract all files from the GCF to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this GCF item, string outputDirectory) - { - // If we have no files - if (item.Files == null || item.Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Files.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the GCF to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this GCF item, int index, string outputDirectory) - { - // If we have no files - if (item.Files == null || item.Files.Length == 0 || item.DataBlockOffsets == null) - return false; - - // If the files index is invalid - if (index < 0 || index >= item.Files.Length) - return false; - - // Get the file - var file = item.Files[index]; - if (file?.BlockEntries == null || file.Size == 0) - return false; - - // If the file is encrypted -- TODO: Revisit later - if (file.Encrypted) - return false; - - // Get all data block offsets needed for extraction - var dataBlockOffsets = new List(); - for (int i = 0; i < file.BlockEntries.Length; i++) - { - var blockEntry = file.BlockEntries[i]; - if (blockEntry == null) - continue; - - uint dataBlockIndex = blockEntry.FirstDataBlockIndex; - long blockEntrySize = blockEntry.FileDataSize; - while (blockEntrySize > 0) - { - long dataBlockOffset = item.DataBlockOffsets[dataBlockIndex++]; - dataBlockOffsets.Add(dataBlockOffset); - blockEntrySize -= item.Model.DataBlockHeader?.BlockSize ?? 0; - } - } - - // Create the filename -#if NET48 - string filename = file.Path; -#else - string? filename = file.Path; -#endif - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - // Now read the data sequentially and write out while we have data left - long fileSize = file.Size; - for (int i = 0; i < dataBlockOffsets.Count; i++) - { - int readSize = (int)Math.Min(item.Model.DataBlockHeader?.BlockSize ?? 0, fileSize); -#if NET48 - byte[] data = item.ReadFromDataSource((int)dataBlockOffsets[i], readSize); -#else - byte[]? data = item.ReadFromDataSource((int)dataBlockOffsets[i], readSize); -#endif - if (data == null) - return false; - - fs.Write(data, 0, data.Length); - } - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region MS-CAB - - #region Folders - - /// - /// Get the uncompressed data associated with a folder - /// - /// Folder index to check - /// Byte array representing the data, null on error - /// All but uncompressed are unimplemented -#if NET48 - public static byte[] GetUncompressedData(this MicrosoftCabinet item, int folderIndex) -#else - public static byte[]? GetUncompressedData(this MicrosoftCabinet item, int folderIndex) -#endif - { - // If we have an invalid folder index - if (folderIndex < 0 || item.Model.Folders == null || folderIndex >= item.Model.Folders.Length) - return null; - - // Get the folder header - var folder = item.Model.Folders[folderIndex]; - if (folder == null) - return null; - - // If we have invalid data blocks - if (folder.DataBlocks == null || folder.DataBlocks.Length == 0) - return null; - - // Setup LZX decompression - var lzx = new Compression.LZX.State(); - Compression.LZX.Decompressor.Init(((ushort)folder.CompressionType >> 8) & 0x1f, lzx); - - // Setup MS-ZIP decompression - Compression.MSZIP.State mszip = new Compression.MSZIP.State(); - - // Setup Quantum decompression - var qtm = new Compression.Quantum.State(); - Compression.Quantum.Decompressor.InitState(qtm, folder); - - List data = new List(); - foreach (var dataBlock in folder.DataBlocks) - { - if (dataBlock == null) - continue; - -#if NET48 - byte[] decompressed = new byte[dataBlock.UncompressedSize]; -#else - byte[]? decompressed = new byte[dataBlock.UncompressedSize]; -#endif - switch (folder.CompressionType & SabreTools.Models.MicrosoftCabinet.CompressionType.MASK_TYPE) - { - case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_NONE: - decompressed = dataBlock.CompressedData; - break; - case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_MSZIP: - decompressed = new byte[SabreTools.Models.Compression.MSZIP.Constants.ZIPWSIZE]; - Compression.MSZIP.Decompressor.Decompress(mszip, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); - Array.Resize(ref decompressed, dataBlock.UncompressedSize); - break; - case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_QUANTUM: - Compression.Quantum.Decompressor.Decompress(qtm, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); - break; - case SabreTools.Models.MicrosoftCabinet.CompressionType.TYPE_LZX: - Compression.LZX.Decompressor.Decompress(state: lzx, dataBlock.CompressedSize, dataBlock.CompressedData, dataBlock.UncompressedSize, decompressed); - break; - default: - return null; - } - - if (decompressed != null) - data.AddRange(decompressed); - } - - return data.ToArray(); - } - - #endregion - - #region Files - - /// - /// Extract all files from the MS-CAB to an output directory - /// - /// Output directory to write to - /// True if all filez extracted, false otherwise - public static bool ExtractAll(this MicrosoftCabinet item, string outputDirectory) - { - // If we have no files - if (item.Model.Files == null || item.Model.Files.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Files.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the MS-CAB to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this MicrosoftCabinet item, int index, string outputDirectory) - { - // If we have an invalid file index - if (index < 0 || item.Model.Files == null || index >= item.Model.Files.Length) - return false; - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Ensure the directory exists - Directory.CreateDirectory(outputDirectory); - - // Get the file header - var file = item.Model.Files[index]; - if (file == null || file.FileSize == 0) - return false; - - // Create the output filename - string fileName = Path.Combine(outputDirectory, file.Name ?? $"file{index}"); - - // Get the file data, if possible -#if NET48 - byte[] fileData = item.GetFileData(index); -#else - byte[]? fileData = item.GetFileData(index); -#endif - if (fileData == null) - return false; - - // Write the file data - using (FileStream fs = File.OpenWrite(fileName)) - { - fs.Write(fileData, 0, fileData.Length); - } - - return true; - } - - /// - /// Get the uncompressed data associated with a file - /// - /// File index to check - /// Byte array representing the data, null on error -#if NET48 - public static byte[] GetFileData(this MicrosoftCabinet item, int fileIndex) -#else - public static byte[]? GetFileData(this MicrosoftCabinet item, int fileIndex) -#endif - { - // If we have an invalid file index - if (fileIndex < 0 || item.Model.Files == null || fileIndex >= item.Model.Files.Length) - return null; - - // Get the file header - var file = item.Model.Files[fileIndex]; - if (file == null || file.FileSize == 0) - return null; - - // Get the parent folder data -#if NET48 - byte[] folderData = item.GetUncompressedData((int)file.FolderIndex); -#else - byte[]? folderData = item.GetUncompressedData((int)file.FolderIndex); -#endif - if (folderData == null || folderData.Length == 0) - return null; - - // Create the output file data - byte[] fileData = new byte[file.FileSize]; - if (folderData.Length < file.FolderStartOffset + file.FileSize) - return null; - - // Get the segment that represents this file - Array.Copy(folderData, file.FolderStartOffset, fileData, 0, file.FileSize); - return fileData; - } - - #endregion - - #endregion - - #region PAK - - /// - /// Extract all files from the PAK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this PAK item, string outputDirectory) - { - // If we have no directory items - if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.DirectoryItems.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the PAK to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this PAK item, int index, string outputDirectory) - { - // If we have no directory items - if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) - return false; - - // If the directory item index is invalid - if (index < 0 || index >= item.Model.DirectoryItems.Length) - return false; - - // Get the directory item - var directoryItem = item.Model.DirectoryItems[index]; - if (directoryItem == null) - return false; - - // Read the item data -#if NET48 - byte[] data = item.ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); -#else - byte[]? data = item.ReadFromDataSource((int)directoryItem.ItemOffset, (int)directoryItem.ItemLength); -#endif - if (data == null) - return false; - - // Create the filename -#if NET48 - string filename = directoryItem.ItemName; -#else - string? filename = directoryItem.ItemName; -#endif - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region PFF - - /// - /// Extract all segments from the PFF to an output directory - /// - /// Output directory to write to - /// True if all segments extracted, false otherwise - public static bool ExtractAll(this PFF item, string outputDirectory) - { - // If we have no segments - if (item.Model.Segments == null || item.Model.Segments.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Segments.Length; i++) - { - allExtracted &= item.ExtractSegment(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a segment from the PFF to an output directory by index - /// - /// Segment index to extract - /// Output directory to write to - /// True if the segment extracted, false otherwise - public static bool ExtractSegment(this PFF item, int index, string outputDirectory) - { - // If we have no segments - if (item.Model.Header?.NumberOfFiles == null || item.Model.Header.NumberOfFiles == 0 || item.Model.Segments == null || item.Model.Segments.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= item.Model.Segments.Length) - return false; - - // Get the segment information - var file = item.Model.Segments[index]; - if (file == null) - return false; - - // Get the read index and length - int offset = (int)file.FileLocation; - int size = (int)file.FileSize; - - try - { - // Ensure the output directory exists - Directory.CreateDirectory(outputDirectory); - - // Create the output path - string filePath = Path.Combine(outputDirectory, file.FileName ?? $"file{index}"); - using (FileStream fs = File.OpenWrite(filePath)) - { - // Read the data block -#if NET48 - byte[] data = item.ReadFromDataSource(offset, size); -#else - byte[]? data = item.ReadFromDataSource(offset, size); -#endif - if (data == null) - return false; - - // Write the data -- TODO: Compressed data? - fs.Write(data, 0, size); - } - - return true; - } - catch - { - return false; - } - } - - #endregion - - #region Quantum - - /// - /// Extract all files from the Quantum archive to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this Quantum item, string outputDirectory) - { - // If we have no files - if (item.Model.FileList == null || item.Model.FileList.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.FileList.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the Quantum archive to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this Quantum item, int index, string outputDirectory) - { - // If we have no files - if (item.Model.Header == null || item.Model.Header.FileCount == 0 || item.Model.FileList == null || item.Model.FileList.Length == 0) - return false; - - // If we have an invalid index - if (index < 0 || index >= item.Model.FileList.Length) - return false; - - // Get the file information - var fileDescriptor = item.Model.FileList[index]; - - // Read the entire compressed data - int compressedDataOffset = (int)item.Model.CompressedDataOffset; - int compressedDataLength = item.GetEndOfFile() - compressedDataOffset; -#if NET48 - byte[] compressedData = item.ReadFromDataSource(compressedDataOffset, compressedDataLength); -#else - byte[]? compressedData = item.ReadFromDataSource(compressedDataOffset, compressedDataLength); -#endif - - // TODO: Figure out decompression - // - Single-file archives seem to work - // - Single-file archives with files that span a window boundary seem to work - // - The first files in each archive seem to work - return false; - - // // Setup the decompression state - // State state = new State(); - // Decompressor.InitState(state, TableSize, CompressionFlags); - - // // Decompress the entire array - // int decompressedDataLength = (int)FileList.Sum(fd => fd.ExpandedFileSize); - // byte[] decompressedData = new byte[decompressedDataLength]; - // Decompressor.Decompress(state, compressedData.Length, compressedData, decompressedData.Length, decompressedData); - - // // Read the data - // int offset = (int)FileList.Take(index).Sum(fd => fd.ExpandedFileSize); - // byte[] data = new byte[fileDescriptor.ExpandedFileSize]; - // Array.Copy(decompressedData, offset, data, 0, data.Length); - - // // Loop through all files before the current - // for (int i = 0; i < index; i++) - // { - // // Decompress the next block of data - // byte[] tempData = new byte[FileList[i].ExpandedFileSize]; - // int lastRead = Decompressor.Decompress(state, compressedData.Length, compressedData, tempData.Length, tempData); - // compressedData = new ReadOnlySpan(compressedData, (lastRead), compressedData.Length - (lastRead)).ToArray(); - // } - - // // Read the data - // byte[] data = new byte[fileDescriptor.ExpandedFileSize]; - // _ = Decompressor.Decompress(state, compressedData.Length, compressedData, data.Length, data); - - // // Create the filename - // string filename = fileDescriptor.FileName; - - // // If we have an invalid output directory - // if (string.IsNullOrWhiteSpace(outputDirectory)) - // return false; - - // // Create the full output path - // filename = Path.Combine(outputDirectory, filename); - - // // Ensure the output directory is created - // Directory.CreateDirectory(Path.GetDirectoryName(filename)); - - // // Try to write the data - // try - // { - // // Open the output file for writing - // using (Stream fs = File.OpenWrite(filename)) - // { - // fs.Write(data, 0, data.Length); - // } - // } - // catch - // { - // return false; - // } - - return true; - } - - #endregion - - #region SGA - - /// - /// Extract all files from the SGA to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this SGA item, string outputDirectory) - { - // Get the number of files - int filesLength; - switch (item.Model.Header?.MajorVersion) - { - case 4: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; - case 5: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; - case 6: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; - case 7: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?.Length ?? 0; break; - default: return false; - } - - // If we have no files - if (filesLength == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < filesLength; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the SGA to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this SGA item, int index, string outputDirectory) - { - // Get the number of files - int filesLength; - switch (item.Model.Header?.MajorVersion) - { - case 4: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?.Length ?? 0; break; - case 5: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?.Length ?? 0; break; - case 6: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?.Length ?? 0; break; - case 7: filesLength = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?.Length ?? 0; break; - default: return false; - } - - // If we have no files - if (filesLength == 0) - return false; - - // If the files index is invalid - if (index < 0 || index >= filesLength) - return false; - - // Get the files -#if NET48 - object file; -#else - object? file; -#endif - switch (item.Model.Header?.MajorVersion) - { - case 4: file = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Files?[index]; break; - case 5: file = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Files?[index]; break; - case 6: file = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Files?[index]; break; - case 7: file = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Files?[index]; break; - default: return false; - } - - if (file == null) - return false; - - // Create the filename -#if NET48 - string filename; -#else - string? filename; -#endif - switch (item.Model.Header?.MajorVersion) - { - case 4: - case 5: filename = (file as SabreTools.Models.SGA.File4)?.Name; break; - case 6: filename = (file as SabreTools.Models.SGA.File6)?.Name; break; - case 7: filename = (file as SabreTools.Models.SGA.File7)?.Name; break; - default: return false; - } - - // Loop through and get all parent directories -#if NET48 - var parentNames = new List { filename }; -#else - var parentNames = new List { filename }; -#endif - - // Get the parent directory -#if NET48 - object folder; -#else - object? folder; -#endif - switch (item.Model.Header?.MajorVersion) - { -#if NET48 - case 4: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 5: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 6: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 7: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Folders?.FirstOrDefault(f => index >= f.FileStartIndex && index <= f.FileEndIndex); break; -#else - case 4: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory4)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 5: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory5)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 6: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory6)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; - case 7: folder = (item.Model.Directory as SabreTools.Models.SGA.Directory7)?.Folders?.FirstOrDefault(f => f != null && index >= f.FileStartIndex && index <= f.FileEndIndex); break; -#endif - default: return false; - } - - // If we have a parent folder - if (folder != null) - { - switch (item.Model.Header?.MajorVersion) - { - case 4: parentNames.Add((folder as SabreTools.Models.SGA.Folder4)?.Name); break; - case 5: - case 6: - case 7: parentNames.Add((folder as SabreTools.Models.SGA.Folder5)?.Name); break; - default: return false; - } - } - - // TODO: Should the section name/alias be used in the path as well? - - // Reverse and assemble the filename - parentNames.Reverse(); - filename = Path.Combine(parentNames.Cast().ToArray()); - - // Get the file offset - long fileOffset; - switch (item.Model.Header?.MajorVersion) - { - case 4: - case 5: fileOffset = (file as SabreTools.Models.SGA.File4)?.Offset ?? 0; break; - case 6: fileOffset = (file as SabreTools.Models.SGA.File6)?.Offset ?? 0; break; - case 7: fileOffset = (file as SabreTools.Models.SGA.File7)?.Offset ?? 0; break; - default: return false; - } - - // Adjust the file offset - switch (item.Model.Header?.MajorVersion) - { - case 4: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; - case 5: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header4)?.FileDataOffset ?? 0; break; - case 6: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; - case 7: fileOffset += (item.Model.Header as SabreTools.Models.SGA.Header6)?.FileDataOffset ?? 0; break; - default: return false; - }; - - // Get the file sizes - long fileSize, outputFileSize; - switch (item.Model.Header?.MajorVersion) - { - case 4: - case 5: - fileSize = (file as SabreTools.Models.SGA.File4)?.SizeOnDisk ?? 0; - outputFileSize = (file as SabreTools.Models.SGA.File4)?.Size ?? 0; - break; - case 6: - fileSize = (file as SabreTools.Models.SGA.File6)?.SizeOnDisk ?? 0; - outputFileSize = (file as SabreTools.Models.SGA.File6)?.Size ?? 0; - break; - case 7: - fileSize = (file as SabreTools.Models.SGA.File7)?.SizeOnDisk ?? 0; - outputFileSize = (file as SabreTools.Models.SGA.File7)?.Size ?? 0; - break; - default: return false; - } - - // Read the compressed data directly -#if NET48 - byte[] compressedData = item.ReadFromDataSource((int)fileOffset, (int)fileSize); -#else - byte[]? compressedData = item.ReadFromDataSource((int)fileOffset, (int)fileSize); -#endif - if (compressedData == null) - return false; - - // If the compressed and uncompressed sizes match - byte[] data; - if (fileSize == outputFileSize) - { - data = compressedData; - } - else - { - // Decompress the data - data = new byte[outputFileSize]; - Inflater inflater = new Inflater(); - inflater.SetInput(compressedData); - inflater.Inflate(data); - } - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return false; - } - - #endregion - - #region VBSP - - /// - /// Extract all lumps from the VBSP to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public static bool ExtractAllLumps(this VBSP item, string outputDirectory) - { - // If we have no lumps - if (item.Model.Header?.Lumps == null || item.Model.Header.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Header.Lumps.Length; i++) - { - allExtracted &= item.ExtractLump(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a lump from the VBSP to an output directory by index - /// - /// Lump index to extract - /// Output directory to write to - /// True if the lump extracted, false otherwise - public static bool ExtractLump(this VBSP item, int index, string outputDirectory) - { - // If we have no lumps - if (item.Model.Header?.Lumps == null || item.Model.Header.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= item.Model.Header.Lumps.Length) - return false; - - // Get the lump - var lump = item.Model.Header.Lumps[index]; - if (lump == null) - return false; - - // Read the data -#if NET48 - byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"lump_{index}.bin"; - switch (index) - { - case SabreTools.Models.VBSP.Constants.HL_VBSP_LUMP_ENTITIES: - filename = "entities.ent"; - break; - case SabreTools.Models.VBSP.Constants.HL_VBSP_LUMP_PAKFILE: - filename = "pakfile.zip"; - break; - } - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region VPK - - /// - /// Extract all files from the VPK to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this VPK item, string outputDirectory) - { - // If we have no directory items - if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.DirectoryItems.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the VPK to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this VPK item, int index, string outputDirectory) - { - // If we have no directory items - if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) - return false; - - // If the directory item index is invalid - if (index < 0 || index >= item.Model.DirectoryItems.Length) - return false; - - // Get the directory item - var directoryItem = item.Model.DirectoryItems[index]; - if (directoryItem?.DirectoryEntry == null) - return false; - - // If we have an item with no archive -#if NET48 - byte[] data; -#else - byte[]? data; -#endif - if (directoryItem.DirectoryEntry.ArchiveIndex == SabreTools.Models.VPK.Constants.HL_VPK_NO_ARCHIVE) - { - if (directoryItem.PreloadData == null) - return false; - - data = directoryItem.PreloadData; - } - else - { - // If we have invalid archives - if (item.ArchiveFilenames == null || item.ArchiveFilenames.Length == 0) - return false; - - // If we have an invalid index - if (directoryItem.DirectoryEntry.ArchiveIndex < 0 || directoryItem.DirectoryEntry.ArchiveIndex >= item.ArchiveFilenames.Length) - return false; - - // Get the archive filename - string archiveFileName = item.ArchiveFilenames[directoryItem.DirectoryEntry.ArchiveIndex]; - if (string.IsNullOrWhiteSpace(archiveFileName)) - return false; - - // If the archive doesn't exist - if (!File.Exists(archiveFileName)) - return false; - - // Try to open the archive -#if NET48 - Stream archiveStream = null; -#else - Stream? archiveStream = null; -#endif - try - { - // Open the archive - archiveStream = File.OpenRead(archiveFileName); - - // Seek to the data - archiveStream.Seek(directoryItem.DirectoryEntry.EntryOffset, SeekOrigin.Begin); - - // Read the directory item bytes - data = archiveStream.ReadBytes((int)directoryItem.DirectoryEntry.EntryLength); - } - catch - { - return false; - } - finally - { - archiveStream?.Close(); - } - - // If we have preload data, prepend it - if (data != null && directoryItem.PreloadData != null) - data = directoryItem.PreloadData.Concat(data).ToArray(); - } - - // If there is nothing to write out - if (data == null) - return false; - - // Create the filename - string filename = $"{directoryItem.Name}.{directoryItem.Extension}"; - if (!string.IsNullOrWhiteSpace(directoryItem.Path)) - filename = Path.Combine(directoryItem.Path, filename); - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region WAD - - /// - /// Extract all lumps from the WAD to an output directory - /// - /// Output directory to write to - /// True if all lumps extracted, false otherwise - public static bool ExtractAllLumps(this WAD item, string outputDirectory) - { - // If we have no lumps - if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) - return false; - - // Loop through and extract all lumps to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.Lumps.Length; i++) - { - allExtracted &= item.ExtractLump(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a lump from the WAD to an output directory by index - /// - /// Lump index to extract - /// Output directory to write to - /// True if the lump extracted, false otherwise - public static bool ExtractLump(this WAD item, int index, string outputDirectory) - { - // If we have no lumps - if (item.Model.Lumps == null || item.Model.Lumps.Length == 0) - return false; - - // If the lumps index is invalid - if (index < 0 || index >= item.Model.Lumps.Length) - return false; - - // Get the lump - var lump = item.Model.Lumps[index]; - if (lump == null) - return false; - - // Read the data -- TODO: Handle uncompressed lumps (see BSP.ExtractTexture) -#if NET48 - byte[] data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#else - byte[]? data = item.ReadFromDataSource((int)lump.Offset, (int)lump.Length); -#endif - if (data == null) - return false; - - // Create the filename - string filename = $"{lump.Name}.lmp"; - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - #endregion - - #region XZP - - /// - /// Extract all files from the XZP to an output directory - /// - /// Output directory to write to - /// True if all files extracted, false otherwise - public static bool ExtractAll(this XZP item, string outputDirectory) - { - // If we have no directory entries - if (item.Model.DirectoryEntries == null || item.Model.DirectoryEntries.Length == 0) - return false; - - // Loop through and extract all files to the output - bool allExtracted = true; - for (int i = 0; i < item.Model.DirectoryEntries.Length; i++) - { - allExtracted &= item.ExtractFile(i, outputDirectory); - } - - return allExtracted; - } - - /// - /// Extract a file from the XZP to an output directory by index - /// - /// File index to extract - /// Output directory to write to - /// True if the file extracted, false otherwise - public static bool ExtractFile(this XZP item, int index, string outputDirectory) - { - // If we have no directory entries - if (item.Model.DirectoryEntries == null || item.Model.DirectoryEntries.Length == 0) - return false; - - // If we have no directory items - if (item.Model.DirectoryItems == null || item.Model.DirectoryItems.Length == 0) - return false; - - // If the directory entry index is invalid - if (index < 0 || index >= item.Model.DirectoryEntries.Length) - return false; - - // Get the directory entry - var directoryEntry = item.Model.DirectoryEntries[index]; - if (directoryEntry == null) - return false; - - // Get the associated directory item - var directoryItem = item.Model.DirectoryItems.Where(di => di?.FileNameCRC == directoryEntry.FileNameCRC).FirstOrDefault(); - if (directoryItem == null) - return false; - - // Load the item data -#if NET48 - byte[] data = item.ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); -#else - byte[]? data = item.ReadFromDataSource((int)directoryEntry.EntryOffset, (int)directoryEntry.EntryLength); -#endif - if (data == null) - return false; - - // Create the filename -#if NET48 - string filename = directoryItem.Name; -#else - string? filename = directoryItem.Name; -#endif - - // If we have an invalid output directory - if (string.IsNullOrWhiteSpace(outputDirectory)) - return false; - - // Create the full output path - filename = Path.Combine(outputDirectory, filename ?? $"file{index}"); - - // Ensure the output directory is created -#if NET48 - string directoryName = Path.GetDirectoryName(filename); -#else - string? directoryName = Path.GetDirectoryName(filename); -#endif - if (directoryName != null) - Directory.CreateDirectory(directoryName); - - // Try to write the data - try - { - // Open the output file for writing - using (Stream fs = File.OpenWrite(filename)) - { - fs.Write(data, 0, data.Length); - } - } - catch - { - return false; - } - - return true; - } - - #endregion - } -} \ No newline at end of file diff --git a/Test/Extractor.cs b/Test/Extractor.cs index ba4769e9..1342f862 100644 --- a/Test/Extractor.cs +++ b/Test/Extractor.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Text; using BinaryObjectScanner.Compression; using BinaryObjectScanner.Utilities; -using BinaryObjectScanner.Wrappers; using OpenMcdf; using SabreTools.IO; using SabreTools.Serialization.Wrappers; @@ -127,7 +126,7 @@ namespace Test try { // Extract the BFPK contents to the directory - bfpk.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.BFPK.ExtractAll(bfpk, outputDirectory); } catch (Exception ex) { @@ -154,8 +153,8 @@ namespace Test try { // Extract the BSP contents to the directory - bsp.ExtractAllLumps(outputDirectory); - bsp.ExtractAllTextures(outputDirectory); + BinaryObjectScanner.FileType.BSP.ExtractAllLumps(bsp, outputDirectory); + BinaryObjectScanner.FileType.BSP.ExtractAllTextures(bsp, outputDirectory); } catch (Exception ex) { @@ -260,7 +259,7 @@ namespace Test try { // Extract the GCF contents to the directory - gcf.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.GCF.ExtractAll(gcf, outputDirectory); } catch (Exception ex) { @@ -404,7 +403,7 @@ namespace Test try { // Extract the MS-CAB contents to the directory - cabinet.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.MicrosoftCAB.ExtractAll(cabinet, outputDirectory); } catch (Exception ex) { @@ -529,7 +528,7 @@ namespace Test try { // Extract the PAK contents to the directory - pak.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.PAK.ExtractAll(pak, outputDirectory); } catch (Exception ex) { @@ -556,7 +555,7 @@ namespace Test try { // Extract the PFF contents to the directory - pff.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.PFF.ExtractAll(pff, outputDirectory); } catch (Exception ex) { @@ -605,6 +604,33 @@ namespace Test } } + // Quantum + else if (ft == SupportedFileType.Quantum) + { + // Build the archive information + Console.WriteLine("Extracting Quantum contents"); + Console.WriteLine(); + + var quantum = Quantum.Create(stream); + if (quantum == null) + { + Console.WriteLine("Something went wrong parsing Quantum"); + Console.WriteLine(); + return; + } + + try + { + // Extract the Quantum contents to the directory + BinaryObjectScanner.FileType.Quantum.ExtractAll(quantum, outputDirectory); + } + catch (Exception ex) + { + Console.WriteLine($"Something went wrong extracting Quantum: {ex}"); + Console.WriteLine(); + } + } + // RAR else if (ft == SupportedFileType.RAR) { @@ -662,11 +688,11 @@ namespace Test try { // Extract the SGA contents to the directory - sga.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.SGA.ExtractAll(sga, outputDirectory); } catch (Exception ex) { - Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}"); + Console.WriteLine($"Something went wrong extracting SGA: {ex}"); Console.WriteLine(); } } @@ -728,11 +754,11 @@ namespace Test try { // Extract the VBSP contents to the directory - vbsp.ExtractAllLumps(outputDirectory); + BinaryObjectScanner.FileType.VBSP.ExtractAllLumps(vbsp, outputDirectory); } catch (Exception ex) { - Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}"); + Console.WriteLine($"Something went wrong extracting VBSP: {ex}"); Console.WriteLine(); } } @@ -755,11 +781,11 @@ namespace Test try { // Extract the VPK contents to the directory - vpk.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.VPK.ExtractAll(vpk, outputDirectory); } catch (Exception ex) { - Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}"); + Console.WriteLine($"Something went wrong extracting VPK: {ex}"); Console.WriteLine(); } } @@ -782,11 +808,11 @@ namespace Test try { // Extract the WAD contents to the directory - wad.ExtractAllLumps(outputDirectory); + BinaryObjectScanner.FileType.WAD.ExtractAllLumps(wad, outputDirectory); } catch (Exception ex) { - Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}"); + Console.WriteLine($"Something went wrong extracting WAD: {ex}"); Console.WriteLine(); } } @@ -835,11 +861,11 @@ namespace Test try { // Extract the XZP contents to the directory - xzp.ExtractAll(outputDirectory); + BinaryObjectScanner.FileType.XZP.ExtractAll(xzp, outputDirectory); } catch (Exception ex) { - Console.WriteLine($"Something went wrong extracting MS-CAB: {ex}"); + Console.WriteLine($"Something went wrong extracting XZP: {ex}"); Console.WriteLine(); } }